⬆️ ⬇️

Working with peripherals from JavaScript: from theory to practice

The work of a bank employee is dangerous and difficult. In the Unified Frontal System, we try to help the bank employee and automate his work. One of the many tasks that we need to solve is to refine the thin client in order to be able to work with peripheral banking equipment. But this is not so easy to do. In this article, we will describe how we tried to solve the problem, and what it led to.



The article will be useful to architects and experienced front-end developers of systems of scale enterprises, faced with the problem of access to peripheral equipment from a thin client of their system.







And yes, let's say at once, the task is complicated by the fact that the standard approaches using ActiveX, Java Applet, browser plug-in do not suit us for reasons of security, universality and complexity with controllability and maintainability.

')

Imagine a modest (by Chinese standards) bank, which has more than 100 thousand operators in its branches. They have workplaces where they serve customers, and various peripheral devices are connected to the workplaces:



  1. Network / local printer.

  2. Receipt printer (Epson-M950 or Olivetti).

  3. POS terminal (Verifone VX820).

  4. A device for reading "tablets" (touch memory with EDS).

  5. Scanners of different types (for barcodes, passports or just documents).

  6. Webcam (take pictures of potential borrowers).

  7. Specific banking equipment - cash dispenser / receiver, etc.



An impressive list, isn't it? And with all this you need to interact from the react-application running in the browser.



Work with devices at a low level is carried out through the hardware manufacturers or the native internal development library. Installation and updating of drivers and other software at workplaces are carried out centrally. Workplaces are Windows and Internet Explorer 11 or 8. The possibility of switching to Linux and Chrome / Firefox is discussed. Hence the requirement of cross-platform and cross-browser.



The problem of heterogeneity of devices and their failures is more or less solved, but monitoring of the operation of devices is required, including the ability to take logs from the workplace. Centralized management of peripheral settings is also required.



Security requirements are to control the integrity of the code running on the client, limiting access to the periphery (access should be only from the local workplace) and a number of specific requirements for working with touch memory.



A separate question is the work of the tablet with the periphery. The idea is to replace computers with mobile devices, transferring to them all the functionality of the operator, including banking operations. In this article, we will not talk in detail about the tablets, we will definitely tell about this in another article, we’ll simply indicate that here there are issues of connecting the tablet to the internal banking network via wi-fi, and issues of working with devices connected directly to the tablet , for example, mPOS-terminals.



How we tried to tame it all, why it didn’t happen right away



From the conditions of operation should work with peripherals:



We tried to find a solution to the problem and worked out several options.



HTML5



Although it potentially has work with peripheral devices, current implementations are sharpened for mobile devices or audio / video, and are not suitable for our tasks from the word "absolutely."



Webusb



https://wicg.imtqy.com/webusb/ - firstly, not all devices are connected via USB. Secondly, WebUSB support is currently not available anywhere except for the experimental feature in Chrome. So, too, does not suit us.



ActiveX



The method is old-fashioned and proven - wrappers are made over drivers, installed as local OCX or DLL, accessed through ActiveXObject:



var printer = new ActiveXObject("LocalPrinterComponent.Printer"); printer.print(data + "\r\n"); 


But it only works in IE and Windows. Despite attempts to make ActiveX technology portable, Microsoft abandoned ActiveX development in favor of plugin technology.



Browser Plugins



It also does not suit us, is not targeted, binds to the browser and limits the versatility.



Java native components



A service is placed on localhost, which is accessible from JavaScript. From this, too, decided to refuse, because implementation is more time consuming, you need to use a web server or write your own.



Applet



Not targeted, there are problems with permissions on the local device.



And what to do?



As a result, we stopped at the following peripheral architecture:







The client application in JavaScript accesses the local service through the module working with the peripheral API, which is the binding on the client part of socket.io .



The jobs are set node.js , which runs under the service account at the start of the OS. In node.js, our bootstrap module works, which is responsible for loading npm modules for working with peripheral devices from the server to the local file system. The client code generates an event, the code and the version of the module that works with the device, the method being called and its parameters are passed as attributes:



 <html> <head> <title>Test hello.socket.io</title> <script src="socket.io.min.js"></script> <script> var socket = io.connect('http://localhost:8055'); socket.on('eSACExec', function (data) { var newLi = document.createElement('li'); newLi.style.color = "green"; newLi.innerHTML = data.data + ' from ' + data.from; document.getElementById("list").appendChild(newLi); console.log(data.data); }); socket.on('eSACOnError', function (data) { var newLi = document.createElement('li'); newLi.style.color = "red"; newLi.innerHTML = data.message; document.getElementById("list").appendChild(newLi); console.log(data.error); }); function sendHello() { socket.emit('eSASExec', {module: 'api.system.win.window', version:'0.0.1', repoURL: 'git+https://github.com/Gromatchikov/', method: 'sayHello', params: {hello: 'Server API'}}); } </script> </head> <body> <ol id="list"> </ol> <button onclick="sendHello()">send</button> </body> </html> 


Also bootstrap is responsible for working with platform services (downloading logs from the workplace at the request of the administrator, etc.)



For each peripheral device there is a module for working with it, which is executed in node.js. Bootstrap proxies the call to the module method:



 var http = require('http'); var sio = require('socket.io'); var bootstrapSA = require('./bootstrap.js'); var modules = new Object(); function SAServer() { if (!(this instanceof SAServer)) { return new SAServer(); } this.app = http.createServer(); this.sioApp = sio.listen(this.app); this.sioApp.sockets.on('connection', function (client) { client.on('eSASExec', function (data) { try { console.info('SAserver event eSASExec:'); console.info(data); bootstrapSA.demandModuleProperty(modules, data.repoURL, data.module, data.version, function (module) { var fullModuleName = bootstrapSA.getFullName(data.module, data.version) var api = modules[fullModuleName]; if (api != undefined) { var result = api[data.method](data.params); console.info('Result eSASExec', result); client.emit('eSACExec', {data: result || '', from: fullModuleName}); } else { console.error('[', 'Error ', fullModuleName, '] is ', undefined); client.emit('eSACOnError', { message: 'Error define api module' + fullModuleName}); } }); } catch (_error) { console.error('[', 'Error eSASExec', ( _error.code ? _error.code : ''), ']', _error); client.emit('eSACOnError', { message: 'Error :(', error: _error.stack || ''}); } }); client.on('disconnect', () => { console.log('User disconnected'); }); client.on('eSASStop', () => { process.exit(0); }); console.info('User connected'); }); console.info('System API server loaded'); } //  SAServer.prototype.start = function startSAServer(port) { //   this.currentPort = port; if (this.currentPort == undefined) { this.currentPort = process.env.npm_package_config_port; } this.app.listen(this.currentPort); console.log('System API server running at http://127.0.0.1:'+ this.currentPort); bootstrapSA.inspect('parent object', modules); }; module.exports = SAServer; 


The module works with the low-level API of the operating system or driver via the npm node.js "ffi" module:



 //    JavaScript (UTF-8)  UTF-16 function TEXT(text){ return new Buffer(text, 'ucs2').toString('binary'); } var FFI = require('ffi'); //   user32.dll var user32 = new FFI.Library('user32', { 'MessageBoxW': [ 'int32', [ 'int32', 'string', 'string', 'int32' ] ] }); function WindowSA() { if (!(this instanceof WindowSA)) { return new WindowSA(); } console.log('Window system API module loaded'); } WindowSA.prototype.sayHello = function sayHello(params) { //   var OK_or_Cancel = user32.MessageBoxW(0, TEXT(', "' + params.hello + '"!'), TEXT(' '), 1); }; module.exports = WindowSA; 


When the client application accesses bootstrap, passing the module and version, bootstrap checks the local storage. If the required module is not in the local storage, it is pumped from the server. Thus, only drivers and node.js with bootstrap are installed centrally, and npm modules for working with devices are downloaded in runtime. But this function is unlikely to be used in the industrial configuration, so we assume that when remotely installing device drivers on employees' workplaces, the corresponding npm peripheral API module will be installed, which is a JS bundle.



 var loadModule = function (obj, repoURL, name, version, callback) { var mod; try { //todo mod = require(fullModuleName);    major.minor   fix console.log('System API module "%s" require...', getFullName(name, version)); mod = new require(name)(); return mod; } catch (err){ errFlag = true; console.error('Error require', err.code, err); if (err.code == 'MODULE_NOT_FOUND') { installModule(obj, repoURL, name, version, callback); } } } //   ,      //todo       name@version (fullModuleName) var demandModuleProperty = function (obj, repoURL, name, version, callback) { var fullModuleName = getFullName(name, version); var errFlag = false; if (!obj.hasOwnProperty(fullModuleName)) { console.log('System API module "%s" defining', fullModuleName); var mod = loadModule(obj, repoURL, name, version, callback); if (mod == undefined){ return; } Object.defineProperty(obj, fullModuleName, { configurable: true, enumerable: true, get: function () { Object.defineProperty(obj, fullModuleName, { configurable: false, enumerable: true, value: mod }); console.log('System API module "%s" defined', fullModuleName); inspect('parent object', obj); inspect(fullModuleName, obj[fullModuleName]); } }); } if (!errFlag) { console.log('Callback for "%s"', fullModuleName); callback(obj[fullModuleName]); } }; //    //todo       name@version,   major.minor   fix var installModule = function(obj, repoURL, name, version, callback){ console.log('Installing module %s version %s', name, version); var fullURL = getFullURL(repoURL, name, version); npm.load({progress: true, '--save-optional': true, '--force': true, '--ignore-scripts': true},function(err) { // handle errors // install module npm.commands.install([fullURL], function(er, data) { // log errors or data if (!er){ console.info('System API module "%s" installed', name); //    demandModuleProperty(obj, repoURL, name, version, callback); } else { console.error('Error NPM Install', er.code, er); } }); npm.on('log', function(message) { // log installation progress console.log('NPM logs:' + message); }); }); }; 


The solution has not yet been implemented, we are working on it and will definitely tell you about the results in another article. In the meantime, we want to ask you what you think about the chosen approach? What pitfalls await us? We invite everyone to take part in the discussion in the comments.

Source: https://habr.com/ru/post/330374/



All Articles