⬆️ ⬇️

We display data from Serial in Chrome Application





Hello, Habr!



I want to share the experience of creating a small application for Google Chrome, which interacts with the serial port.

')

Brief background. Many times I wanted the computer and the Arduino connected to it to work as a single system in which the microcontroller would be an intermediary for communicating with sensors and actuators, and the computer would be a large convenient console.



In order for this to happen, you need to either sit on the console terminal on a computer or write some small GUI's. Even the most primitive GUI's requires some disproportionate effort to create. You need to select a framework, implement a bunch of side GUI logic, compile for various platforms, deal with dependencies, package .exe, check for Mac and Vendian, etc.



It has long been heard that the API for Google Chrome applications gives access to Serial. I wanted to try and at the same time master the creation of Chrome applications as such. The result was a Serial Projector - a replacement for the regular Serial Monitor for the Arduino IDE.



The essence is simple to disgrace: the application on the whole screen displays the last text string that came through the serial port. This allows, for example, to display the readings of the device large and nude. It may be useful for all exhibitions, presentations, installations.



Details of the source code and a demonstration of work - under the cut.







How the application works



Let's take a look at the Serial Projector. All source is on GitHub .



So, what is the app for Google Chrome? By and large this is just a dynamic web page. Exactly the same as if you did it for your site. It is possible and necessary to use all the same JavaScript, CSS, HTML5, to connect third-party libraries and gadgets. I used jQuery, Backbone.js, Underscore.js. The difference is that such a page may use an additional “insecure” API for working with the user's computer. In particular, there is an API for reading and writing the serial port .



And such an application can be easily published in the Chrome Web Store, and your users can install it.



Just as it happens on mobiles, when installing, the application will ask for confirmation that you trust it to access this or that unsafe API. Their list is specified in the application description file manifest.json:



//... "permissions": [ "serial", "fullscreen" ] //... 


Serial port operation



The most interesting is in the file connection.js. Below is the class model for interacting with a serial connection. You should not read thoughtfully from top to bottom to understand everything. I will give comments below.



 var RETRY_CONNECT_MS = 1000; var Connection = Backbone.Model.extend({ defaults: { connectionId: null, path: null, bitrate: 9600, autoConnect: undefined, ports: [], buffer: null, text: '...', error: '', }, initialize: function() { chrome.serial.onReceive.addListener(this._onReceive.bind(this)); chrome.serial.onReceiveError.addListener(this._onReceiveError.bind(this)); }, enumeratePorts: function() { var self = this; chrome.serial.getDevices(function(ports) { self.set('ports', ports); self._checkPath(); }); }, hasPorts: function() { return this.get('ports').length > 0; }, autoConnect: function(enable) { this.set('autoConnect', enable); if (enable) { this._tryConnect(); } else { this._disconnect(); } }, _tryConnect: function() { if (!this.get('autoConnect')) { return; } var path = this.get('path'); var bitrate = this.get('bitrate'); if (path) { var self = this; chrome.serial.connect(path, {bitrate: bitrate}, function(connectionInfo) { self.set('buffer', new Uint8Array(0)); self.set('connectionId', connectionInfo.connectionId); }); } else { this.enumeratePorts(); setTimeout(this._tryConnect.bind(this), RETRY_CONNECT_MS); } }, _disconnect: function() { var cid = this.get('connectionId'); if (!cid) { return; } var self = this; chrome.serial.disconnect(cid, function() { self.set('connectionId', null); self.enumeratePorts(); }); }, _checkPath: function() { var path = this.get('path'); var ports = this.get('ports'); if (ports.length == 0) { this.set('path', null); return; } for (var i = 0; i < ports.length; ++i) { var port = ports[i]; if (port.path == path) { return; } } this.set('path', ports[0].path); }, _onReceive: function(receiveInfo) { var data = receiveInfo.data; data = new Uint8Array(data); this.set('buffer', catBuffers(this.get('buffer'), data)); var lbr = findLineBreak(this.get('buffer')); if (lbr !== undefined) { var txt = this.get('buffer').slice(0, lbr); this.set('buffer', this.get('buffer').slice(lbr + 1)); this.set('text', uintToString(txt)); } }, _onReceiveError: function(info) { this._disconnect(); this.set('error', info.error); this.enumeratePorts(); } }); 


Direct interaction with the Serial API can be seen in three places. Firstly, in the class constructor:



  initialize: function() { chrome.serial.onReceive.addListener(this._onReceive.bind(this)); chrome.serial.onReceiveError.addListener(this._onReceiveError.bind(this)); } 


Here we set the traditional JS event handlers. Upon successful receipt of the data portion, we will call the _onReceive method, and on any error _onReceiveError. Connections are established, but there is no connection yet. First you need to find out which Serial ports on the user's computer are currently being seen by Chrome:



  enumeratePorts: function() { var self = this; chrome.serial.getDevices(function(ports) { self.set('ports', ports); self._checkPath(); }); }, 


After polling the OS, the function passed as a parameter will be called with an array of found ports. Each item is a dictionary containing the system path to the port, a human-readable name, a USB VID & PID piece of hardware.



With the system path in hand, you can finally connect:



  chrome.serial.connect(path, {bitrate: bitrate}, function(connectionInfo) { self.set('buffer', new Uint8Array(0)); self.set('connectionId', connectionInfo.connectionId); }); 


After the connection is established, again, the provided callback with the connection parameters will be invoked. In particular, with connectionId, which is needed for most port operations.



Now consider the process of obtaining and parsing data. All of it fits in one class method:



  _onReceive: function(receiveInfo) { var data = receiveInfo.data; data = new Uint8Array(data); this.set('buffer', catBuffers(this.get('buffer'), data)); var lbr = findLineBreak(this.get('buffer')); if (lbr !== undefined) { var txt = this.get('buffer').slice(0, lbr); this.set('buffer', this.get('buffer').slice(lbr + 1)); this.set('text', uintToString(txt)); } }, 


Each time you receive a chunk of data, Chrome will call this function and send information about the received packet to it. The data itself is transmitted in the data field. It is of type ArrayBuffer, with which practically nothing can be done directly. This is not a string, this is not an array, this is just a briquette of bytes "as is".



In order to parse the briquette, you need to create a projection of the ArrayBuffer that knows how to interpret the raw data. In the case of the Arduino, the compiler is AVR GCC, the source code is written to UTF-8, and therefore the data that is sent to the regular Serial.println is transmitted in the form of UTF-8 strings.



Then everything is trivial:



A pair of helpers



To my surprise, projections, including our Uint8Array, began to support slice'ing only in recent versions of Chrome. For compatibility with older versions, the method can be implemented independently:



 Uint8Array.prototype.slice = function(begin, end) { if (typeof begin === 'undefined') { begin = 0; } if (typeof end === 'undefined') { end = Math.max(this.length, begin); } var result = new Uint8Array(end - begin); for (var i = begin; i < end; ++i) { result[i - begin] = this[i]; } return result; } 


There were also no functions for gluing arrays and turning them into regular lines in the box, therefore:



 function catBuffers(a, b) { var result = new Uint8Array(a.length + b.length); result.set(a); result.set(b, a.length); return result; } function uintToString(uintArray) { var encodedString = String.fromCharCode.apply(null, uintArray), decodedString = decodeURIComponent(escape(encodedString)); return decodedString; } 


I will not give you the code for interacting with the HTML content of the page here, because it is extremely prosaic: a couple of triples of jQuery event handlers and Backbone models.



Total



So, if you need to quickly concoct a console for your hardware project and not worry about cross-platform, creating an installer and delivering updates and fixes, Chrome Applications is a smart choice.



I hope the article showed you the big picture and you now have something to push off from. And what finally happened with us, you can watch in the next video on our YouTube channel :



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



All Articles