Control a Chip Card Reader with Your Browser!
In a previous post, I showed how to establish USB connectivity using NodeJS, the popular JavaScript runtime engine. In a followup post, we saw how Websockets can be used for inter-process communication between NodeJS and any browser (even older browsers that don’t know about Websockets).
If we tie together the USB connectivity code and Websockets code, it becomes possible for a web page to control a USB device (such as our VP3300 3-way card reader, shown at right) using only JavaScript. That’s the code I want to show you today.
If you didn’t already download the scripts from my previous posts, don’t worry: The script below (around 360 lines of JavaScript) contains everything you need (except Node itself). We’ll talk about the code in a minute, but right now, let’s talk about how to use it.
HOW TO USE THE SCRIPT (THE EASY WAY)
1. Go to the ID TECH Knowledge Base and download the node-driver.zip archive. (No login required.) NOTE: This is a sizable (11 megabytes) download, because it includes Node.exe, so allow a few seconds. Also: It is for Windows machines only. Put the archive anywhere on your local machine.
2. Expand the archive. It contains all the scripts you need. (No need to copy/paste the code shown below!)
3. In the expanded archive, find the file called start.bat. Run it. (A console window will open; just leave it open.) The Node driver is now running.
4. Plug a supported device (ID TECH BTPay Mini, VP3300, UniPay III, VP8800) into your machine’s USB port. The Node driver will automatically connect to it.
5. Open the client.html file (in the archive) using a web browser. Click the Connect button. You should see a message confirming that a Websocket connection has been established on port 9901. The driver will use this connection to talk to your USB device.
HOW TO USE THE SCRIPT (ADVANCED USERS)
See the 360 or so lines of JavaScript further below? Copy and paste that whole thing into a text file and save it as, say, driver.js.
Install NodeJS on your machine if you haven’t already done so. But before running driver.js with Node, do the following.
Use npm to install the following Node modules (available on Github, etc.):
1. node-hid (for USB connectivity)
2. socket.io (for Websockets connectivity)
3. socket.io-client (so Node scripts can be socket.io clients)
Now run driver.js (the code shown below) using Node. To connect to the driver from a browser, create an HTML page that contains:
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.3/socket.io.slim.js"></script>
And also:
var socket = io( "ws://localhost:9901" );
You need to import the socket.io.slim.js script in order to use the Websockets implementation of socket.io. Use the socket variable (var socket
, above) to emit Websocket messages.
Here’s the code you need to run in Node in order to have USB connectivity in a browser, via Websockets:
// ========== usb-provider =========== var HID = require( "node-hid" ); const EventEmitter = require('events'); class USBProvider extends EventEmitter { constructor( ) { super(); var self = this; this.onerror = function(e){ console.log('error: ' +e); }; this.getDeviceHandle = function() { return deviceHandle; } var SCAN_INTERVAL = 2000; var VENDOR_ID = 0xACD; // default ID TECH vid var deviceHandle = null; // stores our handle var deviceRecord = null; // stores device record var stopKey = null; // to stop polling (if needed) // This will be called repeatedly by poll(), below function cycle() { var deviceFound = false; HID.devices().forEach( function (device, index, records ) { deviceFound = ( device.vendorId == VENDOR_ID ); if ( device.vendorId == VENDOR_ID && deviceRecord == null ) { deviceRecord = device; try { // Try to connect. deviceHandle = new HID.HID( device.vendorId, device.productId ); deviceHandle.on( 'error', self.onerror ); //self.onconnect( deviceHandle ); // HANDLE CONNECTION EVENT self.emit( 'usbconnect', deviceHandle ); console.log( "usbprovider: connect" ); } catch(e) { self.onerror("Exception caught:n" + e); self.emit('usbexception',device); } } // if if ( index == records.length - 1 && !deviceFound ) { // HANDLE DISCONNECT EVENT if ( deviceRecord != null ) { deviceRecord = deviceHandle = null; // nullify record // self.ondisconnect(); self.emit( 'usbdisconnect' ); console.log( "usbprovider: disconnect"); } // if } // if }); // forEach } // cycle this.poll = function() { this.stopKey = setInterval( cycle, SCAN_INTERVAL ); } } } // =========== socket-provider ============ // Dependency: This module uses the socket.io module. // There is a call to require("socket.io") further below, in the code. /* Usage: SocketProvider = require('socket-provider'); var sp = new SocketProvider(); sp.setPort( 9001 ); var server = sp.createServer(); // to send a message from server: server.emit( 'message', something ); (clients can now connect on port 9001) In BROWSER, do: <script src="/./browserscripts/socket.io.slim.js"></script> (Note: Get the socket.io.slim.js script from the socket.io module distro. It contains fallback code to make older browsers socket-aware.) <script> var port = 9001; var socket = io.connect('http://localhost:'+port); if ( socket ) socket.on('message', (msg)=>{ ...do something with msg... }); // Or send messages: socket.emit( 'echo', something ); </script> TO CREATE A NODE CLIENT (running headless in Node!): socket = require( 'socket.io-client' ); client = socket( "ws://localhost:9001" ); client.on <-- handle events client.emit( 'message', someObject ); */ class SocketProvider { constructor() { var PORT = null; this.setPort = function( p ) { PORT = p; }; this.createServer = function( port ) { // Create do-nothing http server var server = require('http').createServer(); // require the socket.io module and attach to server: var io = require('socket.io').listen(server); // on client connection we don't do much io.sockets.on('connection', function (connection) { console.log('A client connected...'); connection.emit('message', 'Connected!'); // Set up disconnect handler connection.on('disconnect', ()=> { console.log('Client disconnected'); } ); // broadcast 'echo' messages connection.on('echo', (m)=>{ io.sockets.emit('message', m); }); }); // on connection // OK, now let's listen for socket requests server.listen( port || PORT ); return io.sockets; // return instance of socket server }; // createServer } // constructor }; // class // =========== usb-webshim ============ /* by Kas Thomas. Copyright 2017 by ID TECH. BY YOUR USE OF THIS CODE, YOU AGREE TO USE IT USE AT YOUR OWN RISK. This shim establishes a USB-to-Websocket bridge. The strategy is 1. Create a Websocket server instance. 2. Make ourselves a client of the WS server. 3. Detect USB events. 4. Re-emit USB events onto our socket. In this way, anyone (e.g., a web browser) that chooses to be a client of the socket server can receive our USB events. NOTE: The server listens for 'echo' events and will re-broadcast them as 'message' events. So to broadcast something (to everyone), emit an 'echo' event. */ var PORT = 9901; var wsURL = "ws://localhost:"; // Include the required modules (if using modules! which right now, we're not) // var USBProvider = require('usb-provider'); // var SocketProvider = require('socket-provider'); var connect = require( 'socket.io-client' ); // Get instances of the providers var usb = new USBProvider(); var sockets = new SocketProvider(); // Set up a socket server sockets.setPort( PORT ); var ss = sockets.createServer(); // Make ourselves a client of that server var shim = connect( wsURL + PORT ); // A 'message' event with source == 'POS' and // type of 'start contactless transaction' // needs to trigger the appropriate VP3300 command. shim.on( 'message', ( msg )=> { if ( msg.source == 'POS' && msg.type == 'start contactless transaction' ) { if ( deviceHandle.getDeviceInfo().product.match(/pisces/i) ) sendDataCommand(START_CONTACTLESS_TX_PISCES , deviceHandle ); if ( deviceHandle.getDeviceInfo().product.match(/unipay/i) ) sendDataCommand(START_CONTACTLESS_TX_UNIPAYIII , deviceHandle ); else if ( deviceHandle.getDeviceInfo().product.match(/BTPay mini/i) ) sendDataCommand( START_CONTACTLESS_TX_BTPAY_MINI , deviceHandle ); else if (deviceHandle.getDeviceInfo().product.match(/goose/i) ) sendDataCommand( START_CONTACTLESS_TX_VP4880 , deviceHandle ); } // Any 'client' can send a 'raw command' with this message scheme if ( msg.source == 'client' && msg.type == 'raw command' ) sendDataCommand( msg.data, deviceHandle ); }); // ========== USB/Websockets Shim ========== // Store the USB device handle here: var deviceHandle = null; // Function to create a data object out of source, data, type dataObject = ( source, data, type ) => ({ source:source, data:data, type:type }); var dataCache = []; var START_CONTACTLESS_TX_UNIPAYIII = "5669564f746563683200024000211e9a031704129c01009f02060000000001009f03060000000000009f21031536314c6c"; var START_CONTACTLESS_TX_PISCES = '5669564f74656368320002200026001e010000009a031704109c01009f02060000000001009f03060000000000009f2103075020e354'; var START_CONTACTLESS_TX_VP4880 = "5669564f746563683200024000211e9a031704079c01009f02060000000001009f03060000000000009f2103095336b001"; var START_CONTACTLESS_TX_BTPAY_MINI = "5669564f746563683200024000211e9a031704079c01009f02060000000001009f03060000000000009f2103155324c105"; function formatViVOcommand( cmdString ) { // We will handle single-packet commands only, for now var cmd = "01" + cmdString; // Report Type 1 var array = []; cmd.match(/../g).forEach( (b)=> { var num = 1 * ('0x' + b ); array.push( num ); } ); return array; } function ViVOdevice( dh ) { if ( dh.getDeviceInfo().product.match(/goose/i) || dh.getDeviceInfo().product.match(/pisces/i) || dh.getDeviceInfo().product.match(/unipay/i) || dh.getDeviceInfo().product.match(/btpay/i) ) return true; return false; } function sendDataCommand( cmdString, deviceHandle ) { // Right now, we only handle ViVOtech2 devices if ( deviceHandle != null ) { var cmdArray = ViVOdevice( deviceHandle ) ? formatViVOcommand( cmdString ) : null; deviceHandle.write( cmdArray ); } } function isViVOtech2( data ) { return data.indexOf('5669564f746563683200') == 2; } function trimViVOdata( data ) { var lengthBytes = data.slice(24,28); var length = (1*('0x'+lengthBytes)); return data.slice(0, 28 + 2*length + 4); } // On USB connect, cache the device handle // and set a data handler usb.on( 'usbconnect', function( h ) { deviceHandle = h; // cache the handle deviceHandle.on( 'data', (data)=> { var hex = data.toString('hex') var dataIN = null; // single-packet payload if ( isViVOtech2(hex) && hex.slice(0,2) == '01' ) dataIN = trimViVOdata( hex.slice(2) ); // beginning of a multi-packet payload else if ( isViVOtech2(hex) && hex.slice(0,2) == '02' ) { dataCache.push( hex.slice(2) ); return; } // continuation packet else if ( hex.slice(0,2) == '03' ) { dataCache.push( hex.slice(2) ); return; } // final packet of multi-piece payload else if ( hex.slice(0,2) == '04' ) { dataCache.push( hex.slice(2) ); // push last packet onto array dataIN = dataCache.join(''); // get all the data dataIN = trimViVOdata( dataIN ); // trim zeros dataCache = []; // clear the cache } var msg = dataObject( 'usb', dataIN, 'data' ); shim.emit( 'echo', msg ); }); // on data // Re-emit the 'usbconnect' event as a semantically rich // 'echo' + dataObject, via the socket server var message = dataObject( 'usb', h.getDeviceInfo().product, 'connected' ); shim.emit( 'echo', message ); // send the meaningful message }); // on usbconnect // On USB disconnect, nullify the deviceHandle // and broadcast the event usb.on( 'usbdisconnect', ()=>{ deviceHandle = null; var message = dataObject( 'usb', null, 'disconnected' ); shim.emit( 'echo', message ); } ); usb.on( 'usbexception', (e)=>{ console.log('n usb exception in shim: ' + e); } }); usb.poll(); // start polling!
The first part of this code contains the USBProvider class definition (using the ECMA 6 notation supported by Node), which I talked about in an earlier post. The middle portion of the code has the SocketProvider class definition, which allows for the creation of a Websockets server. The bottom half of the code is “shim code” tying the first two parts together. It creates a USBProvider instance, and a SocketProvider instance, starts them up, and talks to each, so that commands can be sent from browser to USB device using a simple JSON data object construct:
dataObject = ( source, data, type ) => ({ source:source, data:data, type:type });
To send a raw device command to a reader, you will do something like:
var cmd = "5669564f74656368320060040000f5e1";
dobj = dataObject( 'client',cmd,'raw command');
socket.emit( 'echo', dobj ); // send command to device
The cmd string shown above represents the raw hex bytes for the “Get Terminal Settings” command for ID TECH’s UniPay III, BTPay Mini, or VP3300 devices.
In the client.html file that accompanies the node-driver.zip archive mentioned earlier, I’ve already included a method called getTerminalSettings() that sends the above command to the connected device. When you run that command in the client.html console, you get a detailed listing of device settings, as shown below:
This screenshot shows some of the default Terminal Settings of the ID TECH BTPay Mini (VP3300) product. (NOTE: Not all settings are shown because the screenshot didn’t capture the entire scrollable area.)
SUMMARY
Let’s quickly review some of the important takeaways from this post and the two prior posts on USB connectivity. We’ve seen, among other things, that:
1. With the aid of NodeJS (a deservedly popular open-source JavaScript runtime engine), you can easily talk to any USB device using JavaScript. This by itself is pretty amazing!
2. You can also easily (with 75 lines of code) use Node to create a Websocket server that can allow fast, easy, robust, and secure inter-process communication on any device that supports Node.
3. Your Node scripts can easily pass USB data (via Websockets) to any browser that supports Websockets — which is to say, any modern browser.
4. As a result, it’s easily possible to use JavaScript, running in a web browser, to control a USB device (such as a credit card reader) connected to your laptop, PC, tablet, or other host. All of this, in turn, means you can create powerful browser-based payment apps that can talk to any number of ID TECH card readers using no code other than JavaScript. There’s no need to write native code in C or C++.Thus, you can prototype almost any payment app very, very quickly, with little coding effort.
In future posts, I’ll have a lot more to say about how to use web technology to create payment apps that leverage ID TECH’s readers. Come back soon for more!
In the meantime, if you want to find out more about any of ID TECH’s products, call us toll free:
Toll Free Number
1-800-984-1010