From 96e968729df2888ff6d820a18f817095cc5f9e70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20Kiss?= Date: Sun, 17 Sep 2017 14:45:20 +0200 Subject: [PATCH] feat(device): Read user config from eeprom (#413) * feat(device): Read user config from eeprom * read data from eeprom * fix user config serialization * fix device connected detection * not allow override default config is eeprom is empty * add error handling to eeprom parsing * colorize log output * add USB[T] feature * add class name to USB[T] log * remove redundant error log msg * Add USB[T] to Apply user config --- .../uhk-agent/src/services/device.service.ts | 75 ++++++++++++++++--- .../src/services/uhk-hid-device.service.ts | 26 +++---- packages/uhk-common/src/util/ipcEvents.ts | 2 + .../app/services/device-renderer.service.ts | 9 +++ .../src/app/store/actions/user-config.ts | 12 +++ packages/uhk-web/src/app/store/effects/app.ts | 2 +- .../uhk-web/src/app/store/effects/device.ts | 13 +++- .../src/app/store/effects/user-config.ts | 42 ++++++++++- .../src/app/store/reducers/app.reducer.ts | 15 +++- .../renderer/services/electron-log.service.ts | 21 +++++- packages/usb/read-config.js | 1 + 11 files changed, 185 insertions(+), 33 deletions(-) diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index 4488fe75..1c0340fb 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -18,9 +18,11 @@ 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 } @@ -31,6 +33,15 @@ enum EepromTransfer { 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)); /** @@ -38,6 +49,7 @@ const snooze = ms => new Promise(resolve => setTimeout(resolve, ms)); * Functionality: * - Detect device is connected or not * - Send UserConfiguration to the UHK Device + * - Read UserConfiguration from the UHK Device */ export class DeviceService { private pollTimer$: Subscription; @@ -48,6 +60,7 @@ export class DeviceService { private device: UhkHidDeviceService) { this.pollUhkDevice(); ipcMain.on(IpcEvents.device.saveUserConfiguration, this.saveUserConfiguration.bind(this)); + ipcMain.on(IpcEvents.device.loadUserConfiguration, this.loadUserConfiguration.bind(this)); logService.debug('[DeviceService] init success'); } @@ -59,6 +72,38 @@ export class DeviceService { return this.connected; } + /** + * Return with the actual UserConfiguration from UHK Device + * @returns {Promise} + */ + public async loadUserConfiguration(event: Electron.Event): Promise { + let response = []; + + try { + this.logService.debug('[DeviceService] USB[T]: Read user configuration size from keyboard'); + const configSize = await this.getUserConfigSizeFromKeyboard(); + const chunkSize = 63; + let offset = 0; + let configBuffer = new Buffer(0); + + this.logService.debug('[DeviceService] USB[T]: Read user configuration from keyboard'); + while (offset < configSize) { + const chunkSizeToRead = Math.min(chunkSize, configSize - offset); + const writeBuffer = Buffer.from([Command.ReadUserConfig, 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); + } catch (error) { + this.logService.error('[DeviceService] getUserConfigFromEeprom error', error); + } finally { + this.device.close(); + } + + event.sender.send(IpcEvents.device.loadUserConfigurationReply, JSON.stringify(response)); + } + /** * HID API not support device attached and detached event. * This method check the keyboard is attached to the computer or not. @@ -76,25 +121,38 @@ export class DeviceService { .do((connected: boolean) => { this.connected = connected; this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, connected); - this.logService.info(`Device connection state changed to: ${connected}`); + this.logService.info(`[DeviceService] Device connection state changed to: ${connected}`); }) .subscribe(); } + /** + * Return the UserConfiguration size from the UHK Device + * @returns {Promise} + */ + private async getUserConfigSizeFromKeyboard(): Promise { + const buffer = await this.device.write(new Buffer([Command.GetProperty, SystemPropertyIds.UserConfigSize])); + const configSize = buffer[1] + (buffer[2] << 8); + this.logService.debug('[DeviceService] User config size:', configSize); + return configSize; + } + private async saveUserConfiguration(event: Electron.Event, json: string): Promise { const response = new IpcResponse(); try { - this.sendUserConfigToKeyboard(json); + 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(); - this.device.close(); response.success = true; - this.logService.info('transferring finished'); } catch (error) { this.logService.error('[DeviceService] Transferring error', error); response.error = {message: error.message}; + } finally { + this.device.close(); } event.sender.send(IpcEvents.device.saveUserConfigurationReply, response); @@ -114,19 +172,14 @@ export class DeviceService { 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]); await this.device.write(applyBuffer); - this.logService.info('[DeviceService] Transferring finished'); } private async writeUserConfigToEeprom(): Promise { - this.logService.info('[DeviceService] Start write user configuration to eeprom'); - - const buffer = await this.device.write(new Buffer([Command.LaunchEepromTransfer, EepromTransfer.WriteUserConfig])); + await this.device.write(new Buffer([Command.LaunchEepromTransfer, EepromTransfer.WriteUserConfig])); await this.waitUntilKeyboardBusy(); - - this.logService.info('[DeviceService] End write user configuration to eeprom'); } private async waitUntilKeyboardBusy(): Promise { diff --git a/packages/uhk-agent/src/services/uhk-hid-device.service.ts b/packages/uhk-agent/src/services/uhk-hid-device.service.ts index aac6d4ff..fc0222c7 100644 --- a/packages/uhk-agent/src/services/uhk-hid-device.service.ts +++ b/packages/uhk-agent/src/services/uhk-hid-device.service.ts @@ -6,6 +6,17 @@ 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 @@ -32,17 +43,6 @@ export class UhkHidDeviceService { 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 @@ -109,7 +109,7 @@ export class UhkHidDeviceService { return reject(err); } const logString = UhkHidDeviceService.bufferToString(receivedData); - this.logService.debug('[UhkHidDevice] Transfer UHK ===> Agent: ', logString); + this.logService.debug('[UhkHidDevice] USB[R]:', logString); if (receivedData[0] !== 0) { return reject(new Error(`Communications error with UHK. Response code: ${receivedData[0]}`)); @@ -119,7 +119,7 @@ export class UhkHidDeviceService { }); const sendData = UhkHidDeviceService.getTransferData(buffer); - this.logService.debug('[UhkHidDevice] Transfer Agent ===> UHK: ', UhkHidDeviceService.bufferToString(sendData)); + this.logService.debug('[UhkHidDevice] USB[W]:', UhkHidDeviceService.bufferToString(sendData)); device.write(sendData); }); } diff --git a/packages/uhk-common/src/util/ipcEvents.ts b/packages/uhk-common/src/util/ipcEvents.ts index 953c069e..63ed2486 100644 --- a/packages/uhk-common/src/util/ipcEvents.ts +++ b/packages/uhk-common/src/util/ipcEvents.ts @@ -22,6 +22,8 @@ class Device { public static readonly deviceConnectionStateChanged = 'device-connection-state-changed'; public static readonly saveUserConfiguration = 'device-save-user-configuration'; public static readonly saveUserConfigurationReply = 'device-save-user-configuration-reply'; + public static readonly loadUserConfiguration = 'device-load-user-configuration'; + public static readonly loadUserConfigurationReply = 'device-load-user-configuration-reply'; } export class IpcEvents { diff --git a/packages/uhk-web/src/app/services/device-renderer.service.ts b/packages/uhk-web/src/app/services/device-renderer.service.ts index 6bbc8d29..70411d78 100644 --- a/packages/uhk-web/src/app/services/device-renderer.service.ts +++ b/packages/uhk-web/src/app/services/device-renderer.service.ts @@ -9,6 +9,7 @@ import { SaveConfigurationReplyAction, SetPrivilegeOnLinuxReplyAction } from '../store/actions/device'; +import { LoadUserConfigFromDeviceReplyAction } from '../store/actions/user-config'; @Injectable() export class DeviceRendererService { @@ -28,6 +29,10 @@ export class DeviceRendererService { this.ipcRenderer.send(IpcEvents.device.saveUserConfiguration, JSON.stringify(buffer)); } + loadUserConfiguration(): void { + this.ipcRenderer.send(IpcEvents.device.loadUserConfiguration); + } + private registerEvents(): void { this.ipcRenderer.on(IpcEvents.device.deviceConnectionStateChanged, (event: string, arg: boolean) => { this.dispachStoreAction(new ConnectionStateChangedAction(arg)); @@ -40,6 +45,10 @@ export class DeviceRendererService { this.ipcRenderer.on(IpcEvents.device.saveUserConfigurationReply, (event: string, response: IpcResponse) => { this.dispachStoreAction(new SaveConfigurationReplyAction(response)); }); + + this.ipcRenderer.on(IpcEvents.device.loadUserConfigurationReply, (event: string, response: string) => { + this.dispachStoreAction(new LoadUserConfigFromDeviceReplyAction(JSON.parse(response))); + }); } private dispachStoreAction(action: Action): void { diff --git a/packages/uhk-web/src/app/store/actions/user-config.ts b/packages/uhk-web/src/app/store/actions/user-config.ts index 3960b294..90c67214 100644 --- a/packages/uhk-web/src/app/store/actions/user-config.ts +++ b/packages/uhk-web/src/app/store/actions/user-config.ts @@ -8,6 +8,8 @@ const PREFIX = '[user-config] '; // tslint:disable-next-line:variable-name export const ActionTypes = { LOAD_USER_CONFIG: type(PREFIX + 'Load User Config'), + LOAD_USER_CONFIG_FROM_DEVICE: type(PREFIX + 'Load User Config from Device'), + LOAD_USER_CONFIG_FROM_DEVICE_REPLY: type(PREFIX + 'Load User Config from Device reply'), LOAD_USER_CONFIG_SUCCESS: type(PREFIX + 'Load User Config Success'), SAVE_USER_CONFIG_SUCCESS: type(PREFIX + 'Save User Config Success') }; @@ -16,6 +18,16 @@ export class LoadUserConfigAction implements Action { type = ActionTypes.LOAD_USER_CONFIG; } +export class LoadUserConfigFromDeviceAction implements Action { + type = ActionTypes.LOAD_USER_CONFIG_FROM_DEVICE; +} + +export class LoadUserConfigFromDeviceReplyAction implements Action { + type = ActionTypes.LOAD_USER_CONFIG_FROM_DEVICE_REPLY; + + constructor(public payload: Array) { } +} + export class LoadUserConfigSuccessAction implements Action { type = ActionTypes.LOAD_USER_CONFIG_SUCCESS; diff --git a/packages/uhk-web/src/app/store/effects/app.ts b/packages/uhk-web/src/app/store/effects/app.ts index 65f6e1fa..6330a5f7 100644 --- a/packages/uhk-web/src/app/store/effects/app.ts +++ b/packages/uhk-web/src/app/store/effects/app.ts @@ -30,7 +30,7 @@ export class ApplicationEffects { this.logService.info('Renderer appStart effect end'); }); - @Effect({ dispatch: false }) + @Effect({dispatch: false}) showNotification$: Observable = this.actions$ .ofType(ActionTypes.APP_SHOW_NOTIFICATION) .map(toPayload) diff --git a/packages/uhk-web/src/app/store/effects/device.ts b/packages/uhk-web/src/app/store/effects/device.ts index bdbbf20f..18affe60 100644 --- a/packages/uhk-web/src/app/store/effects/device.ts +++ b/packages/uhk-web/src/app/store/effects/device.ts @@ -15,7 +15,8 @@ import 'rxjs/add/operator/withLatestFrom'; import { NotificationType, IpcResponse } from 'uhk-common'; import { ActionTypes, - ConnectionStateChangedAction, HideSaveToKeyboardButton, + ConnectionStateChangedAction, + HideSaveToKeyboardButton, PermissionStateChangedAction, SaveToKeyboardSuccessAction, SaveToKeyboardSuccessFailed @@ -25,10 +26,11 @@ import { ShowNotificationAction } from '../actions/app'; import { AppState } from '../index'; import { UserConfiguration } from '../../config-serializer/config-items/user-configuration'; import { UhkBuffer } from '../../config-serializer/uhk-buffer'; +import { LoadUserConfigFromDeviceAction } from '../actions/user-config'; @Injectable() export class DeviceEffects { - @Effect({dispatch: false}) + @Effect() deviceConnectionStateChange$: Observable = this.actions$ .ofType(ActionTypes.CONNECTION_STATE_CHANGED) .map(toPayload) @@ -39,6 +41,13 @@ export class DeviceEffects { else { this.router.navigate(['/detection']); } + }) + .switchMap((connected: boolean) => { + if (connected) { + return Observable.of(new LoadUserConfigFromDeviceAction()); + } + + return Observable.empty(); }); @Effect({dispatch: false}) diff --git a/packages/uhk-web/src/app/store/effects/user-config.ts b/packages/uhk-web/src/app/store/effects/user-config.ts index 8f29392a..1d9170d5 100644 --- a/packages/uhk-web/src/app/store/effects/user-config.ts +++ b/packages/uhk-web/src/app/store/effects/user-config.ts @@ -11,7 +11,7 @@ import 'rxjs/add/operator/withLatestFrom'; import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/observable/of'; -import { NotificationType } from 'uhk-common'; +import { LogService, NotificationType } from 'uhk-common'; import { ActionTypes, @@ -29,6 +29,8 @@ import { MacroActions } from '../actions/macro'; import { UndoUserConfigData } from '../../models/undo-user-config-data'; import { ShowNotificationAction, DismissUndoNotificationAction } from '../actions/app'; import { ShowSaveToKeyboardButtonAction } from '../actions/device'; +import { DeviceRendererService } from '../../services/device-renderer.service'; +import { UhkBuffer } from '../../config-serializer/uhk-buffer'; @Injectable() export class UserConfigEffects { @@ -86,10 +88,46 @@ export class UserConfigEffects { return [new LoadUserConfigSuccessAction(config), go(payload.path)]; }); + @Effect({dispatch: false}) loadUserConfigFromDevice$ = this.actions$ + .ofType(ActionTypes.LOAD_USER_CONFIG_FROM_DEVICE) + .do(() => this.deviceRendererService.loadUserConfiguration()); + + @Effect() loadUserConfigFromDeviceReply$ = this.actions$ + .ofType(ActionTypes.LOAD_USER_CONFIG_FROM_DEVICE_REPLY) + .map(action => action.payload) + .switchMap((data: Array) => { + try { + let userConfig; + if (data.length > 0) { + const uhkBuffer = new UhkBuffer(); + let hasNonZeroValue = false; + for (const num of data) { + if (num > 0) { + hasNonZeroValue = true; + } + uhkBuffer.writeUInt8(num); + } + uhkBuffer.offset = 0; + userConfig = new UserConfiguration(); + userConfig.fromBinary(uhkBuffer); + + if (hasNonZeroValue) { + return Observable.of(new LoadUserConfigSuccessAction(userConfig)); + } + } + } catch (err) { + this.logService.error('Eeprom parse error:', err); + } + + return Observable.empty(); + }); + constructor(private actions$: Actions, private dataStorageRepository: DataStorageRepositoryService, private store: Store, - private defaultUserConfigurationService: DefaultUserConfigurationService) { + private defaultUserConfigurationService: DefaultUserConfigurationService, + private deviceRendererService: DeviceRendererService, + private logService: LogService) { } private getUserConfiguration() { diff --git a/packages/uhk-web/src/app/store/reducers/app.reducer.ts b/packages/uhk-web/src/app/store/reducers/app.reducer.ts index 4dcf5500..bf148559 100644 --- a/packages/uhk-web/src/app/store/reducers/app.reducer.ts +++ b/packages/uhk-web/src/app/store/reducers/app.reducer.ts @@ -13,13 +13,15 @@ export interface State { navigationCountAfterNotification: number; prevUserConfig?: UserConfiguration; runningInElectron: boolean; + userConfigLoading: boolean; } const initialState: State = { started: false, showAddonMenu: false, navigationCountAfterNotification: 0, - runningInElectron: runInElectron() + runningInElectron: runInElectron(), + userConfigLoading: true }; export function reducer(state = initialState, action: Action) { @@ -76,7 +78,16 @@ export function reducer(state = initialState, action: Action) { case UserConfigActionTypes.SAVE_USER_CONFIG_SUCCESS: { return { ...state, - prevUserConfig: action.payload + prevUserConfig: action.payload, + userConfigLoading: false + }; + } + + case UserConfigActionTypes.LOAD_USER_CONFIG_FROM_DEVICE: + case UserConfigActionTypes.LOAD_USER_CONFIG: { + return { + ...state, + userConfigLoading: true }; } 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 b94ff541..bd2411f0 100644 --- a/packages/uhk-web/src/renderer/services/electron-log.service.ts +++ b/packages/uhk-web/src/renderer/services/electron-log.service.ts @@ -4,10 +4,27 @@ import * as util from 'util'; import { LogService } from 'uhk-common'; +const transferRegExp = /USB\[T]:/; +const writeRegExp = /USB\[W]:/; +const readRegExp = /USB\[R]: 00/; +const errorRegExp = /(?:(USB\[R]: ([^0]|0[^0])))/; + // 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; + console.debug = (...args: any[]): void => { + if (writeRegExp.test(args[0])) { + console.log('%c' + args[0], 'color:blue'); + } else if (readRegExp.test(args[0])) { + console.log('%c' + args[0], 'color:green'); + } else if (errorRegExp.test(args[0])) { + console.log('%c' + args[0], 'color:red'); + }else if (transferRegExp.test(args[0])) { + console.log('%c' + args[0], 'color:orange'); + } else { + console.log(...args); + } + }; } /** @@ -21,7 +38,7 @@ if (console.debug) { */ @Injectable() export class ElectronLogService implements LogService { - private static getErrorText(args: any) { + public static getErrorText(args: any) { return util.inspect(args); } diff --git a/packages/usb/read-config.js b/packages/usb/read-config.js index 1f6e84ed..fbe2c518 100755 --- a/packages/usb/read-config.js +++ b/packages/usb/read-config.js @@ -27,6 +27,7 @@ while(offset < configSize) { const usbCommand = isHardwareConfig ? uhk.usbCommands.readHardwareConfig : uhk.usbCommands.readUserConfig; chunkSizeToRead = Math.min(chunkSize, configSize - offset); buffer = Buffer.from([usbCommand, chunkSizeToRead, offset & 0xff, offset >> 8]); + console.log('write to keyboard', uhk.bufferToString(buffer)); device.write(uhk.getTransferData(buffer)); buffer = Buffer.from(device.readSync()); console.log('read-config-chunk', uhk.bufferToString(buffer));