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:

b2ap3_thumbnail_consolescreenshot1.png

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

 

Leave a comment

You must be logged in to post a comment.