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
This commit is contained in:
Róbert Kiss
2017-09-14 00:58:41 +02:00
committed by László Monda
parent 8d7269a998
commit 901a5eb5d1
7 changed files with 250 additions and 99 deletions

View File

@@ -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",

View File

@@ -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;
});

View File

@@ -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);

View File

@@ -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<void>}
* @private
*/
private async saveUserConfiguration(event: Electron.Event, json: string): Promise<void> {
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;
}
}

View File

@@ -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;

View File

@@ -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<number>): 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<Buffer>}
*/
public async write(buffer: Buffer): Promise<Buffer> {
return new Promise<Buffer>((resolve, reject) => {
const device = this.getDevice();
if (!device) {
return reject(new Error('[UhkHidDevice] Device is not connected'));
}
device.read((err: any, receivedData: Array<number>) => {
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;
}
}

View File

@@ -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.