From 44d5723318a33d40f66f3362697435e11a1b14ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Monda?= Date: Tue, 10 May 2016 02:58:40 +0200 Subject: [PATCH] Add rough USB code that will have to be converted to TypeScript and cleaned up. --- usb/UhkConnection.js | 212 +++++++++++++++++++++++++++++++++++++++++++ usb/uhkcmd | 101 +++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 usb/UhkConnection.js create mode 100755 usb/uhkcmd diff --git a/usb/UhkConnection.js b/usb/UhkConnection.js new file mode 100644 index 00000000..6995e646 --- /dev/null +++ b/usb/UhkConnection.js @@ -0,0 +1,212 @@ +var usb = require('usb'); +var R = require('ramda'); +var s = require('underscore.string') + +var UhkConnection = function(selectedLogLevel) { + 'use strict'; + var self = this; + + // Public methods + + self.sendRequest = function(command, arg, callback, shouldReceiveResponse) { + if (shouldReceiveResponse === undefined) { + shouldReceiveResponse = true; + } + + var request; + if (arg === null) { + request = new Buffer([command]); + } else if (typeof arg === 'number') { + request = new Buffer([command, arg]); + } else if (typeof arg === 'string') { + var charCodes = arg.split('').map(function(char) {return char.charCodeAt(0)}); + request = new Buffer([command].concat(charCodes)); + } else { + throw new Error('UhkConnection.sendRequest(): arg is of unknown type'); + } + + log(UhkConnection.LOG_LEVELS.TRANSFER, 'Sending request', request); + setReport(request, function() { + if (shouldReceiveResponse) { + receiveResponse(UhkConnection.LOG_LEVELS.IGNORED_TRANSFER, function() { + // The first response is cached by the OS so let's ignore it and go for the second one. + receiveResponse(UhkConnection.LOG_LEVELS.TRANSFER, callback); + }); + } else { + callback(); + } + }); + }; + + // Private methods + + function setReport(message, callback) { + device.controlTransfer( + 0x21, // bmRequestType (constant for this control request) + 0x09, // bmRequest (constant for this control request) + 0, // wValue (MSB is report type, LSB is report number) + 0, // wIndex (interface number) + message, // message to be sent + callback + ); + } + + function receiveResponse(logLevel, callback) { + var endpoint = usbInterface.endpoints[0]; + var readLength = 64; + endpoint.transfer(readLength, function(error, data) { + if (error) { + log(logLevel, 'Error response received', error); + } else { + log(logLevel, 'Received response:', data); + } + callback(error, data) + }); + } + + function setConfiguration(callback) { + device.controlTransfer( // Send a Set Configuration control request + 0, // bmRequestType + 0x09, // bmRequest + 0, // wValue (Configuration value) + UhkConnection.GENERIC_HID_INTERFACE_ID, // wIndex + new Buffer(0), // message to be sent + callback // callback to be executed upon finishing the transfer + ); + } + + function log(logLevel, message) { + var args = Array.prototype.slice.call(arguments); + args.shift(); + if (logLevel & selectedLogLevel) { + console.log.apply(this, args); + } + } + + // Initialize + + var device; + var usbInterface; + + self.connect = function(errorCallback) { + pollUntil(function() { + var foundDevices = findDevices(); + if (foundDevices.length > 0) { + var foundDevice = foundDevices[0]; + console.log('UHK connected in %s mode.', + foundDevice.enumerationMode.id); + + device = foundDevice.device; + try { + device.open(); // TODO: What if multiple keyboards are plugged in? + } catch (error) { + if (error.errno === -3) { + console.log('Unable to open USB device of VID %s and PID %s. Please fix permissions!', + s.pad(UhkConnection.VENDOR_ID.toString(16), 4, '0'), + s.pad(foundDevice.enumerationMode.productId.toString(16), 4, '0')); + process.exit(1); + } else { + throw error; + } + } + + usbInterface = self.usbInterface = device.interface(0); + if (usbInterface.isKernelDriverActive()) { + usbInterface.detachKernelDriver(); + } + + process.on('exit', attachKernelDriver); + process.on('SIGINT', attachKernelDriver); + process.on('uncaughtException', attachKernelDriver); + + usbInterface.claim(); + setConfiguration(errorCallback); + return true; + } + + return false; + }, + function() { + errorCallback('Could not connect to the UHK. Is it connected to the host?'); + }); + }; + + self.waitUntilDisconnect = function(errorCallback, successCallback) { + pollUntil(function() { + if (findDevices().length === 0) { + successCallback(); + usbInterface = undefined; + return true; + } + + return false; + }, + function() { + errorCallback('Could not disconnect the UHK'); + }); + }; + + function findDevices() { + return UhkConnection.ENUMERATION_MODES.map(function(enumerationMode) { + return { + enumerationMode: enumerationMode, + device: usb.findByIds(UhkConnection.VENDOR_ID, enumerationMode.productId) + } + }).filter(R.prop('device')); + } + + function pollUntil(pollCallback, errorCallback) { + var retryTimeout = 5000; // ms + var retryInterval = 200; // ms + + function keepPolling() { + if (pollCallback()) { + return; + } + + if (retryTimeout <= 0) { + return errorCallback(); + } + + retryTimeout -= retryInterval; + setTimeout(keepPolling, retryInterval); + } + + keepPolling(); + } + + function attachKernelDriver() { + if (usbInterface) { + usbInterface.release(); + if (!usbInterface.isKernelDriverActive()) { + usbInterface.attachKernelDriver(); + } + usbInterface = undefined; + } + } +}; + +UhkConnection.VENDOR_ID = 0x16d2; // TODO: Restore to 0x16d0 for the final prototype. +UhkConnection.GENERIC_HID_INTERFACE_ID = 2; + +UhkConnection.LOG_LEVELS = { + TRANSFER: 0x01, + IGNORED_TRANSFER: 0x02, + ALL: 0xff +}; + +UhkConnection.COMMANDS = { + DETECT: -1, + REENUMERATE: 0, + READ_EEPROM: 67, + WRITE_EEPROM: 1 +}; + +UhkConnection.ENUMERATION_MODES = [ + {id:'KEYBOARD_6KRO', enumerationId:0, productId:0x05ea}, + {id:'KEYBOARD_NKRO', enumerationId:3, productId:0x05eb}, // TODO: Implement this mode in firmware. + {id:'BOOTLOADER_RIGHT', enumerationId:1, productId:0x05ec}, // CDC bootloader + {id:'BOOTLOADER_LEFT', enumerationId:2, productId:0x05ed} // USB to serial +]; + +module.exports = UhkConnection; diff --git a/usb/uhkcmd b/usb/uhkcmd new file mode 100755 index 00000000..0356d26e --- /dev/null +++ b/usb/uhkcmd @@ -0,0 +1,101 @@ +#!/usr/bin/env node +'use strict'; + +var UhkConnection = require('./lib/UhkConnection'); +var R = require('ramda'); +var path = require('path'); + +var ENUMERATION_MODE_IDS = R.pluck('id', UhkConnection.ENUMERATION_MODES); +var commands = getTypistFriendlyObjectKeys(R.keys(UhkConnection.COMMANDS)); +var args = process.argv.slice(1); +var programName = path.basename(args.shift()); +var commandArg = args.shift(); + +if (!R.contains(commandArg, commands)) { + console.error('Usage: ' + programName + ' COMMAND [ARG]'); + console.error(commandArg === undefined + ? 'No command has been specified.' + : 'Command "' + commandArg + '" is invalid.'); + console.error('Valid commands: ' + commands.join(', ')); + exitWithError(); +} + +var command = UhkConnection.COMMANDS[getTypistUnfriendlyObjectKey(commandArg)]; +var uhkConnection; + +switch (command) { + case UhkConnection.COMMANDS.DETECT: + uhkConnection = new UhkConnection(/*UhkConnection.LOG_LEVELS.TRANSFER*/); + uhkConnection.connect(function(error) { + if (error) { + console.error(error); + exitWithError(); + } + }); + break; + case UhkConnection.COMMANDS.REENUMERATE: + var enumerationModeArg = args.shift(); + var enumerationModes = getTypistFriendlyObjectKeys(ENUMERATION_MODE_IDS); + if (!R.contains(enumerationModeArg, enumerationModes)) { + console.error('Usage: %s %s {%s}', programName, commandArg, enumerationModes.join(' | ')); + console.error(enumerationModeArg === undefined + ? 'No enumeration mode has been specified.' + : 'Enumeration mode "%s" is invalid.', enumerationModeArg); + exitWithError(); + } + var enumerationMode = R.find(R.propEq('id', getTypistUnfriendlyObjectKey(enumerationModeArg)), + UhkConnection.ENUMERATION_MODES); + sendRequest(command, enumerationMode.enumerationId, function(error, data) { + console.log('Reenumerating the UHK in %s mode...', getTypistUnfriendlyObjectKey(enumerationMode.id)); + uhkConnection.waitUntilDisconnect(function() {}, function() { + console.log('UHK disconnected.'); + uhkConnection.connect(function() {}); + }); + }, false); + break; + case UhkConnection.COMMANDS.READ_EEPROM: + sendRequest(command, null, function(error, data) { + console.log(data); + }); + break; + case UhkConnection.COMMANDS.WRITE_EEPROM: + var stringToBeSaved = args.shift(); + if (!stringToBeSaved) { + console.error('A string has to be specified to be saved into the EEPROM.'); + exitWithError(); + } + sendRequest(command, stringToBeSaved, function(error, data) {}); + break; +} + +// Helper functions + +function sendRequest(command, arg, callback, shouldReceiveResponse) { + if (uhkConnection) { + uhkConnection.sendRequest(command, arg, callback, shouldReceiveResponse); + } else { + uhkConnection = new UhkConnection(/*UhkConnection.LOG_LEVELS.TRANSFER*/); + uhkConnection.connect(function(error) { + if (error) { + console.error(error); + exitWithError(); + } + + uhkConnection.sendRequest(command, arg, callback, shouldReceiveResponse); + }); + } +} + +function getTypistFriendlyObjectKeys(object) { + return object.map(function(command) { + return command.toLowerCase().replace(/_/g, '-'); + }); +} + +function getTypistUnfriendlyObjectKey(key) { + return key.toUpperCase().replace(/-/g, '_'); +} + +function exitWithError() { + process.exit(1); +}