feat(config): Read / write hardware configuration area (#423)
* add write-hca.js * refactor: Move config serializer into the uhk-common package * refactor: Move getTransferBuffers into the uhk-usb package * refactor: delete obsoleted classes * build: add uhk-usb build command * refactor: move eeprom transfer to uhk-usb package * fix: Fix write-hca.js * feat: load hardware config from the device and * style: fix ts lint errors * build: fix rxjs dependency resolve * test: Add jasmine unit test framework to the tet serializer * fix(user-config): A "type": "basic", properties to the "keystroke" action types * feat(usb): set chmod+x on write-hca.js * feat(usb): Create USB logger * style: Fix type * build: Add chalk to dependencies. Chalk will colorize the output
This commit is contained in:
committed by
László Monda
parent
1122784bdb
commit
9294bede50
@@ -8,6 +8,7 @@ import { autoUpdater } from 'electron-updater';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
import * as commandLineArgs from 'command-line-args';
|
||||
import { UhkHidDevice } from 'uhk-usb';
|
||||
|
||||
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
|
||||
import { CommandLineArgs } from 'uhk-common';
|
||||
@@ -16,7 +17,6 @@ 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 }
|
||||
@@ -33,7 +33,7 @@ let win: Electron.BrowserWindow;
|
||||
autoUpdater.logger = logger;
|
||||
|
||||
let deviceService: DeviceService;
|
||||
let uhkHidDeviceService: UhkHidDeviceService;
|
||||
let uhkHidDeviceService: UhkHidDevice;
|
||||
let appUpdateService: AppUpdateService;
|
||||
let appService: AppService;
|
||||
let sudoService: SudoService;
|
||||
@@ -51,7 +51,7 @@ function createWindow() {
|
||||
});
|
||||
win.setMenuBarVisibility(false);
|
||||
win.maximize();
|
||||
uhkHidDeviceService = new UhkHidDeviceService(logger);
|
||||
uhkHidDeviceService = new UhkHidDevice(logger);
|
||||
deviceService = new DeviceService(logger, win, uhkHidDeviceService);
|
||||
appUpdateService = new AppUpdateService(logger, win, app);
|
||||
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ipcMain, BrowserWindow } from 'electron';
|
||||
import { UhkHidDevice } from 'uhk-usb';
|
||||
|
||||
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 uhkHidDeviceService: UhkHidDeviceService) {
|
||||
private uhkHidDeviceService: UhkHidDevice) {
|
||||
super(logService, win);
|
||||
|
||||
ipcMain.on(IpcEvents.app.getAppStartInfo, this.handleAppStartInfo.bind(this));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
import { Constants, IpcEvents, LogService, IpcResponse } from 'uhk-common';
|
||||
|
||||
import { IpcEvents, LogService, IpcResponse, ConfigurationReply } from 'uhk-common';
|
||||
import { Constants, EepromTransfer, SystemPropertyIds, UsbCommand } from 'uhk-usb';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Device, devices } from 'node-hid';
|
||||
import { UhkHidDevice } from 'uhk-usb';
|
||||
|
||||
import 'rxjs/add/observable/interval';
|
||||
import 'rxjs/add/operator/startWith';
|
||||
@@ -12,38 +12,6 @@ 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 {
|
||||
GetProperty = 0,
|
||||
UploadConfig = 8,
|
||||
ApplyConfig = 9,
|
||||
LaunchEepromTransfer = 12,
|
||||
ReadUserConfig = 15,
|
||||
GetKeyboardState = 16
|
||||
}
|
||||
|
||||
enum EepromTransfer {
|
||||
ReadHardwareConfig = 0,
|
||||
WriteHardwareConfig = 1,
|
||||
ReadUserConfig = 2,
|
||||
WriteUserConfig = 3
|
||||
}
|
||||
|
||||
enum SystemPropertyIds {
|
||||
UsbProtocolVersion = 0,
|
||||
BridgeProtocolVersion = 1,
|
||||
DataModelVersion = 2,
|
||||
FirmwareVersion = 3,
|
||||
HardwareConfigSize = 4,
|
||||
UserConfigSize = 5
|
||||
}
|
||||
|
||||
const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
/**
|
||||
* IpcMain pair of the UHK Communication
|
||||
* Functionality:
|
||||
@@ -57,10 +25,10 @@ export class DeviceService {
|
||||
|
||||
constructor(private logService: LogService,
|
||||
private win: Electron.BrowserWindow,
|
||||
private device: UhkHidDeviceService) {
|
||||
private device: UhkHidDevice) {
|
||||
this.pollUhkDevice();
|
||||
ipcMain.on(IpcEvents.device.saveUserConfiguration, this.saveUserConfiguration.bind(this));
|
||||
ipcMain.on(IpcEvents.device.loadUserConfiguration, this.loadUserConfiguration.bind(this));
|
||||
ipcMain.on(IpcEvents.device.loadConfigurations, this.loadConfigurations.bind(this));
|
||||
logService.debug('[DeviceService] init success');
|
||||
}
|
||||
|
||||
@@ -76,32 +44,64 @@ export class DeviceService {
|
||||
* Return with the actual UserConfiguration from UHK Device
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async loadUserConfiguration(event: Electron.Event): Promise<void> {
|
||||
public async loadConfigurations(event: Electron.Event): Promise<void> {
|
||||
try {
|
||||
const userConfiguration = await this.loadConfiguration(
|
||||
SystemPropertyIds.UserConfigSize,
|
||||
UsbCommand.ReadUserConfig,
|
||||
'user configuration');
|
||||
|
||||
const hardwareConfiguration = await this.loadConfiguration(
|
||||
SystemPropertyIds.HardwareConfigSize,
|
||||
UsbCommand.ReadHardwareConfig,
|
||||
'hardware configuration');
|
||||
|
||||
const response: ConfigurationReply = {
|
||||
success: true,
|
||||
userConfiguration,
|
||||
hardwareConfiguration
|
||||
};
|
||||
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
|
||||
} catch (error) {
|
||||
const response: ConfigurationReply = {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
|
||||
} finally {
|
||||
this.device.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return with the actual user / hardware fonfiguration from UHK Device
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
public async loadConfiguration(property: SystemPropertyIds, config: UsbCommand, configName: string): Promise<string> {
|
||||
let response = [];
|
||||
|
||||
try {
|
||||
this.logService.debug('[DeviceService] USB[T]: Read user configuration size from keyboard');
|
||||
const configSize = await this.getUserConfigSizeFromKeyboard();
|
||||
this.logService.debug(`[DeviceService] USB[T]: Read ${configName} size from keyboard`);
|
||||
const configSize = await this.getConfigSizeFromKeyboard(property);
|
||||
const chunkSize = 63;
|
||||
let offset = 0;
|
||||
let configBuffer = new Buffer(0);
|
||||
|
||||
this.logService.debug('[DeviceService] USB[T]: Read user configuration from keyboard');
|
||||
this.logService.debug(`[DeviceService] USB[T]: Read ${configName} from keyboard`);
|
||||
while (offset < configSize) {
|
||||
const chunkSizeToRead = Math.min(chunkSize, configSize - offset);
|
||||
const writeBuffer = Buffer.from([Command.ReadUserConfig, chunkSizeToRead, offset & 0xff, offset >> 8]);
|
||||
const writeBuffer = Buffer.from([config, chunkSizeToRead, offset & 0xff, offset >> 8]);
|
||||
const readBuffer = await this.device.write(writeBuffer);
|
||||
configBuffer = Buffer.concat([configBuffer, new Buffer(readBuffer.slice(1, chunkSizeToRead + 1))]);
|
||||
offset += chunkSizeToRead;
|
||||
}
|
||||
response = UhkHidDeviceService.convertBufferToIntArray(configBuffer);
|
||||
response = UhkHidDevice.convertBufferToIntArray(configBuffer);
|
||||
return Promise.resolve(JSON.stringify(response));
|
||||
} catch (error) {
|
||||
this.logService.error('[DeviceService] getUserConfigFromEeprom error', error);
|
||||
} finally {
|
||||
this.device.close();
|
||||
const errMsg = `[DeviceService] ${configName} from eeprom error`;
|
||||
this.logService.error(errMsg, error);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
event.sender.send(IpcEvents.device.loadUserConfigurationReply, JSON.stringify(response));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,11 +127,11 @@ export class DeviceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the UserConfiguration size from the UHK Device
|
||||
* Return the user / hardware configuration size from the UHK Device
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
private async getUserConfigSizeFromKeyboard(): Promise<number> {
|
||||
const buffer = await this.device.write(new Buffer([Command.GetProperty, SystemPropertyIds.UserConfigSize]));
|
||||
private async getConfigSizeFromKeyboard(property: SystemPropertyIds): Promise<number> {
|
||||
const buffer = await this.device.write(new Buffer([UsbCommand.GetProperty, property]));
|
||||
const configSize = buffer[1] + (buffer[2] << 8);
|
||||
this.logService.debug('[DeviceService] User config size:', configSize);
|
||||
return configSize;
|
||||
@@ -144,7 +144,7 @@ export class DeviceService {
|
||||
this.logService.debug('[DeviceService] USB[T]: Write user configuration to keyboard');
|
||||
await this.sendUserConfigToKeyboard(json);
|
||||
this.logService.debug('[DeviceService] USB[T]: Write user configuration to EEPROM');
|
||||
await this.writeUserConfigToEeprom();
|
||||
await this.device.writeConfigToEeprom(EepromTransfer.WriteUserConfig);
|
||||
|
||||
response.success = true;
|
||||
}
|
||||
@@ -168,48 +168,12 @@ export class DeviceService {
|
||||
*/
|
||||
private async sendUserConfigToKeyboard(json: string): Promise<void> {
|
||||
const buffer: Buffer = new Buffer(JSON.parse(json).data);
|
||||
const fragments = this.getTransferBuffers(buffer);
|
||||
const fragments = UhkHidDevice.getTransferBuffers(UsbCommand.UploadUserConfig, buffer);
|
||||
for (const fragment of fragments) {
|
||||
await this.device.write(fragment);
|
||||
}
|
||||
this.logService.debug('[DeviceService] USB[T]: Apply user configuration to keyboard');
|
||||
const applyBuffer = new Buffer([Command.ApplyConfig]);
|
||||
const applyBuffer = new Buffer([UsbCommand.ApplyConfig]);
|
||||
await this.device.write(applyBuffer);
|
||||
}
|
||||
|
||||
private async writeUserConfigToEeprom(): Promise<void> {
|
||||
await this.device.write(new Buffer([Command.LaunchEepromTransfer, EepromTransfer.WriteUserConfig]));
|
||||
await this.waitUntilKeyboardBusy();
|
||||
}
|
||||
|
||||
private async waitUntilKeyboardBusy(): Promise<void> {
|
||||
while (true) {
|
||||
const buffer = await this.device.write(new Buffer([Command.GetKeyboardState]));
|
||||
if (buffer[1] === 0) {
|
||||
break;
|
||||
}
|
||||
this.logService.debug('Keyboard is busy, wait...');
|
||||
await snooze(200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
for (let offset = 0; offset < configBuffer.length; offset += MAX_SENDING_PAYLOAD_SIZE) {
|
||||
const length = offset + MAX_SENDING_PAYLOAD_SIZE < configBuffer.length
|
||||
? MAX_SENDING_PAYLOAD_SIZE
|
||||
: configBuffer.length - offset;
|
||||
const header = new Buffer([Command.UploadConfig, length, offset & 0xFF, offset >> 8]);
|
||||
fragments.push(Buffer.concat([header, configBuffer.slice(offset, offset + length)]));
|
||||
}
|
||||
|
||||
return fragments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
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 {
|
||||
/**
|
||||
* Convert the Buffer to number[]
|
||||
* @param {Buffer} buffer
|
||||
* @returns {number[]}
|
||||
* @private
|
||||
* @static
|
||||
*/
|
||||
public static convertBufferToIntArray(buffer: Buffer): number[] {
|
||||
return Array.prototype.slice.call(buffer, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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] USB[R]:', 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] USB[W]:', 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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user