From 901a5eb5d1c4cf22cef5c48109a35dd13a3a6067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Kiss?= Date: Thu, 14 Sep 2017 00:58:41 +0200 Subject: [PATCH] refactor: Add UhkHidDevice wrapper that unified the USB communication (#414) * refactor: Add UhkHidDevice wrapper that unified the USB communication * fix(log): Hack Replace console.debug to console.log --- packages/uhk-agent/package-lock.json | 20 +- packages/uhk-agent/src/electron-main.ts | 8 +- .../uhk-agent/src/services/app.service.ts | 6 +- .../uhk-agent/src/services/device.service.ts | 126 ++++-------- .../uhk-agent/src/services/logger.service.ts | 2 + .../src/services/uhk-hid-device.service.ts | 181 ++++++++++++++++++ .../renderer/services/electron-log.service.ts | 6 + 7 files changed, 250 insertions(+), 99 deletions(-) create mode 100644 packages/uhk-agent/src/services/uhk-hid-device.service.ts diff --git a/packages/uhk-agent/package-lock.json b/packages/uhk-agent/package-lock.json index 4b5c621f..75f7924c 100644 --- a/packages/uhk-agent/package-lock.json +++ b/packages/uhk-agent/package-lock.json @@ -3,9 +3,9 @@ "lockfileVersion": 1, "dependencies": { "@types/node": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.39.tgz", - "integrity": "sha512-KQHAZeVsk4UIT9XaR6cn4WpHZzimK6UBD1UomQKfQQFmTlUHaNBzeuov+TM4+kigLO0IJt4I5OOsshcCyA9gSA==" + "version": "7.0.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-7.0.43.tgz", + "integrity": "sha512-7scYwwfHNppXvH/9JzakbVxk0o0QUILVk1Lv64GRaxwPuGpnF1QBiwdvhDpLcymb8BpomQL3KYoWKq3wUdDMhQ==" }, "abbrev": { "version": "1.1.0", @@ -392,11 +392,11 @@ } }, "electron": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-1.7.5.tgz", - "integrity": "sha1-BloxAr+LhxAt9QxQmF/v5sVpBFs=", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/electron/-/electron-1.7.6.tgz", + "integrity": "sha1-+2nqMb0D3w7/JH8m8LU4vSm27nI=", "requires": { - "@types/node": "7.0.39", + "@types/node": "7.0.43", "electron-download": "3.3.0", "extract-zip": "1.6.5" } @@ -432,9 +432,9 @@ "integrity": "sha1-ihBD4ys6HaHD9VPc4oznZCRhZ+M=" }, "electron-log": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.6.tgz", - "integrity": "sha1-zPo+CbOfMhRoyQpkJwE4CjQHnxo=" + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/electron-log/-/electron-log-2.2.9.tgz", + "integrity": "sha512-WNMSipQYurNxY14RO6IKgcxcZg1e4aNVpUUJK9q7Bqe0TZEKn1e5h4HiQKhTgVLqKrUn++ugOZrty450P9vpjA==" }, "electron-rebuild": { "version": "1.6.0", diff --git a/packages/uhk-agent/src/electron-main.ts b/packages/uhk-agent/src/electron-main.ts index 83b7d527..926f668d 100644 --- a/packages/uhk-agent/src/electron-main.ts +++ b/packages/uhk-agent/src/electron-main.ts @@ -16,6 +16,7 @@ import { logger } from './services/logger.service'; import { AppUpdateService } from './services/app-update.service'; import { AppService } from './services/app.service'; import { SudoService } from './services/sudo.service'; +import { UhkHidDeviceService } from './services/uhk-hid-device.service'; const optionDefinitions = [ { name: 'addons', type: Boolean, defaultOption: false } @@ -32,6 +33,7 @@ let win: Electron.BrowserWindow; autoUpdater.logger = logger; let deviceService: DeviceService; +let uhkHidDeviceService: UhkHidDeviceService; let appUpdateService: AppUpdateService; let appService: AppService; let sudoService: SudoService; @@ -49,9 +51,10 @@ function createWindow() { }); win.setMenuBarVisibility(false); win.maximize(); - deviceService = new DeviceService(logger, win); + uhkHidDeviceService = new UhkHidDeviceService(logger); + deviceService = new DeviceService(logger, win, uhkHidDeviceService); appUpdateService = new AppUpdateService(logger, win, app); - appService = new AppService(logger, win, deviceService, options); + appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService); sudoService = new SudoService(logger); // and load the index.html of the app. @@ -74,6 +77,7 @@ function createWindow() { deviceService = null; appUpdateService = null; appService = null; + uhkHidDeviceService = null; sudoService = null; }); diff --git a/packages/uhk-agent/src/services/app.service.ts b/packages/uhk-agent/src/services/app.service.ts index 2ea616b3..28112737 100644 --- a/packages/uhk-agent/src/services/app.service.ts +++ b/packages/uhk-agent/src/services/app.service.ts @@ -3,12 +3,14 @@ import { ipcMain, BrowserWindow } from 'electron'; import { CommandLineArgs, IpcEvents, AppStartInfo, LogService } from 'uhk-common'; import { MainServiceBase } from './main-service-base'; import { DeviceService } from './device.service'; +import { UhkHidDeviceService } from './uhk-hid-device.service'; export class AppService extends MainServiceBase { constructor(protected logService: LogService, protected win: Electron.BrowserWindow, private deviceService: DeviceService, - private options: CommandLineArgs) { + private options: CommandLineArgs, + private uhkHidDeviceService: UhkHidDeviceService) { super(logService, win); ipcMain.on(IpcEvents.app.getAppStartInfo, this.handleAppStartInfo.bind(this)); @@ -20,7 +22,7 @@ export class AppService extends MainServiceBase { const response: AppStartInfo = { commandLineArgs: this.options, deviceConnected: this.deviceService.isConnected, - hasPermission: this.deviceService.hasPermission() + hasPermission: this.uhkHidDeviceService.hasPermission() }; this.logService.info('getStartInfo response:', response); return event.sender.send(IpcEvents.app.getAppStartInfoReply, response); diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index f1a43422..e877de6b 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -1,10 +1,10 @@ -import { ipcMain, BrowserWindow } from 'electron'; +import { ipcMain } from 'electron'; import { Constants, IpcEvents, LogService, IpcResponse } from 'uhk-common'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; -import { Device, devices, HID } from 'node-hid'; +import { Device, devices } from 'node-hid'; import 'rxjs/add/observable/interval'; import 'rxjs/add/operator/startWith'; @@ -12,45 +12,47 @@ import 'rxjs/add/operator/map'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/distinctUntilChanged'; +import { UhkHidDeviceService } from './uhk-hid-device.service'; + +/** + * UHK USB Communications command. All communication package should have start with a command code. + */ enum Command { UploadConfig = 8, ApplyConfig = 9 } +/** + * IpcMain pair of the UHK Communication + * Functionality: + * - Detect device is connected or not + * - Send UserConfiguration to the UHK Device + */ export class DeviceService { - private static convertBufferToIntArray(buffer: Buffer): number[] { - return Array.prototype.slice.call(buffer, 0); - } - private pollTimer$: Subscription; private connected: boolean = false; constructor(private logService: LogService, - private win: Electron.BrowserWindow) { + private win: Electron.BrowserWindow, + private device: UhkHidDeviceService) { this.pollUhkDevice(); ipcMain.on(IpcEvents.device.saveUserConfiguration, this.saveUserConfiguration.bind(this)); - logService.info('DeviceService init success'); + logService.debug('[DeviceService] init success'); } + /** + * Return with true is an UHK Device is connected to the computer. + * @returns {boolean} + */ public get isConnected(): boolean { return this.connected; } - public hasPermission(): boolean { - try { - const devs = devices(); - return true; - } catch (err) { - this.logService.error('[DeviceService] hasPermission', err); - } - - return false; - } - /** * HID API not support device attached and detached event. * This method check the keyboard is attached to the computer or not. * Every second check the HID device list. + * @private */ private pollUhkDevice(): void { this.pollTimer$ = Observable.interval(1000) @@ -68,59 +70,43 @@ export class DeviceService { .subscribe(); } - private saveUserConfiguration(event: Electron.Event, json: string): void { + /** + * IpcMain handler. Send the UserConfiguration to the UHK Device and send a response with the result. + * @param {Electron.Event} event - ipc event + * @param {string} json - UserConfiguration in JSON format + * @returns {Promise} + * @private + */ + private async saveUserConfiguration(event: Electron.Event, json: string): Promise { const response = new IpcResponse(); try { const buffer: Buffer = new Buffer(JSON.parse(json).data); const fragments = this.getTransferBuffers(buffer); - const device = this.getDevice(); - device.read((err, data) => { - if (err) { - this.logService.error('Send data to device err:', err); - } - this.logService.debug('send data to device response:', data.toString()); - }); - for (const fragment of fragments) { - const transferData = this.getTransferData(fragment); - this.logService.debug('Fragment: ', JSON.stringify(transferData)); - device.write(transferData); + await this.device.write(fragment); } const applyBuffer = new Buffer([Command.ApplyConfig]); - const applyTransferData = this.getTransferData(applyBuffer); - this.logService.debug('Fragment: ', JSON.stringify(applyTransferData)); - device.write(applyTransferData); - device.close(); + await this.device.write(applyBuffer); + this.device.close(); response.success = true; - this.logService.info('transferring finished'); + this.logService.info('[DeviceService] Transferring finished'); } catch (error) { - this.logService.error('transferring error', error); - response.error = { message: error.message }; + this.logService.error('[DeviceService] Transferring error', error); + response.error = {message: error.message}; } event.sender.send(IpcEvents.device.saveUserConfigurationReply, response); } - private getTransferData(buffer: Buffer): number[] { - const data = DeviceService.convertBufferToIntArray(buffer); - // if data start with 0 need to add additional leading zero because HID API remove it. - // https://github.com/node-hid/node-hid/issues/187 - if (data.length > 0 && data[0] === 0) { - data.unshift(0); - } - - // From HID API documentation: - // http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af - // The first byte of data[] must contain the Report ID. - // For devices which only support a single report, this must be set to 0x0. - data.unshift(0); - - return data; - } - + /** + * Split the whole UserConfiguration package into 64 byte fragments + * @param {Buffer} configBuffer + * @returns {Buffer[]} + * @private + */ private getTransferBuffers(configBuffer: Buffer): Buffer[] { const fragments: Buffer[] = []; const MAX_SENDING_PAYLOAD_SIZE = Constants.MAX_PAYLOAD_SIZE - 4; @@ -134,34 +120,4 @@ export class DeviceService { return fragments; } - - /** - * Return the 0 interface of the keyboard. - * @returns {HID} - */ - private getDevice(): HID { - try { - const devs = devices(); - this.logService.silly('Available devices:', devs); - - const dev = devs.find((x: Device) => - x.vendorId === Constants.VENDOR_ID && - x.productId === Constants.PRODUCT_ID && - ((x.usagePage === 128 && x.usage === 129) || x.interface === 0)); - - if (!dev) { - this.logService.info('[DeviceService] UHK Device not found:'); - return null; - } - const device = new HID(dev.path); - this.logService.info('Used device:', dev); - return device; - } - catch (err) { - this.logService.error('Can not create device:', err); - } - - return null; - } - } diff --git a/packages/uhk-agent/src/services/logger.service.ts b/packages/uhk-agent/src/services/logger.service.ts index b23422db..b6e27ad4 100644 --- a/packages/uhk-agent/src/services/logger.service.ts +++ b/packages/uhk-agent/src/services/logger.service.ts @@ -1,4 +1,6 @@ import * as log from 'electron-log'; log.transports.file.level = 'debug'; +log.transports.console.level = 'debug'; +log.transports.rendererConsole.level = 'debug'; export const logger = log; diff --git a/packages/uhk-agent/src/services/uhk-hid-device.service.ts b/packages/uhk-agent/src/services/uhk-hid-device.service.ts new file mode 100644 index 00000000..aac6d4ff --- /dev/null +++ b/packages/uhk-agent/src/services/uhk-hid-device.service.ts @@ -0,0 +1,181 @@ +import { Device, devices, HID } from 'node-hid'; + +import { Constants, LogService } from 'uhk-common'; + +/** + * HID API wrapper to support unified logging and async write + */ +export class UhkHidDeviceService { + /** + * Create the communication package that will send over USB and + * - add usb report code as 1st byte + * - https://github.com/node-hid/node-hid/issues/187 issue + * @param {Buffer} buffer + * @returns {number[]} + * @private + * @static + */ + private static getTransferData(buffer: Buffer): number[] { + const data = UhkHidDeviceService.convertBufferToIntArray(buffer); + // if data start with 0 need to add additional leading zero because HID API remove it. + // https://github.com/node-hid/node-hid/issues/187 + if (data.length > 0 && data[0] === 0 && process.platform === 'win32') { + data.unshift(0); + } + + // From HID API documentation: + // http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af + // The first byte of data[] must contain the Report ID. + // For devices which only support a single report, this must be set to 0x0. + data.unshift(0); + + return data; + } + + /** + * Convert the Buffer to number[] + * @param {Buffer} buffer + * @returns {number[]} + * @private + * @static + */ + private static convertBufferToIntArray(buffer: Buffer): number[] { + return Array.prototype.slice.call(buffer, 0); + } + + /** + * Convert buffer to space separated hexadecimal string + * @param {Buffer} buffer + * @returns {string} + * @private + * @static + */ + private static bufferToString(buffer: Array): string { + let str = ''; + for (let i = 0; i < buffer.length; i++) { + let hex = buffer[i].toString(16) + ' '; + if (hex.length <= 2) { + hex = '0' + hex; + } + str += hex; + } + return str; + } + + /** + * Internal variable that represent the USB UHK device + * @private + */ + private _device: HID; + + constructor(private logService: LogService) { + } + + /** + * Return true if the app has right to communicate over the USB. + * Need only on linux. + * If return false need to run {project-root}/rules/setup-rules.sh or + * the Agent will ask permission to run at the first time. + * @returns {boolean} + */ + public hasPermission(): boolean { + try { + devices(); + return true; + } catch (err) { + this.logService.error('[UhkHidDevice] hasPermission', err); + } + + return false; + } + + /** + * Send data to the UHK device and wait for the response. + * Throw an error when 1st byte of the response is not 0 + * @param {Buffer} buffer + * @returns {Promise} + */ + public async write(buffer: Buffer): Promise { + return new Promise((resolve, reject) => { + const device = this.getDevice(); + + if (!device) { + return reject(new Error('[UhkHidDevice] Device is not connected')); + } + + device.read((err: any, receivedData: Array) => { + if (err) { + this.logService.error('[UhkHidDevice] Transfer error: ', err); + return reject(err); + } + const logString = UhkHidDeviceService.bufferToString(receivedData); + this.logService.debug('[UhkHidDevice] Transfer UHK ===> Agent: ', logString); + + if (receivedData[0] !== 0) { + return reject(new Error(`Communications error with UHK. Response code: ${receivedData[0]}`)); + } + + return resolve(Buffer.from(receivedData)); + }); + + const sendData = UhkHidDeviceService.getTransferData(buffer); + this.logService.debug('[UhkHidDevice] Transfer Agent ===> UHK: ', UhkHidDeviceService.bufferToString(sendData)); + device.write(sendData); + }); + } + + /** + * Close the communication chanel with UHK Device + */ + public close(): void { + if (!this._device) { + return; + } + + this._device.close(); + this._device = null; + } + + /** + * Return the stored version of HID device. If not exist try to initialize. + * @returns {HID} + * @private + */ + private getDevice() { + if (!this._device) { + this._device = this.connectToDevice(); + } + + return this._device; + } + + /** + * Initialize new UHK HID device. + * @returns {HID} + */ + private connectToDevice(): HID { + try { + const devs = devices(); + this.logService.debug('[DeviceService] Available devices:', devs); + + const dev = devs.find((x: Device) => + x.vendorId === Constants.VENDOR_ID && + x.productId === Constants.PRODUCT_ID && + ((x.usagePage === 128 && x.usage === 129) || x.interface === 0)); + + if (!dev) { + this.logService.info('[DeviceService] UHK Device not found:'); + return null; + } + const device = new HID(dev.path); + this.logService.info('[DeviceService] Used device:', dev); + return device; + } + catch (err) { + this.logService.error('[DeviceService] Can not create device:', err); + } + + return null; + } + +} diff --git a/packages/uhk-web/src/renderer/services/electron-log.service.ts b/packages/uhk-web/src/renderer/services/electron-log.service.ts index da3725a0..b94ff541 100644 --- a/packages/uhk-web/src/renderer/services/electron-log.service.ts +++ b/packages/uhk-web/src/renderer/services/electron-log.service.ts @@ -4,6 +4,12 @@ import * as util from 'util'; import { LogService } from 'uhk-common'; +// https://github.com/megahertz/electron-log/issues/44 +// console.debug starting with Chromium 58 this method is a no-op on Chromium browsers. +if (console.debug) { + console.debug = console.log; +} + /** * This service use the electron-log package to write log in file. * The logger usable in main and renderer process.