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
This commit is contained in:
Róbert Kiss
2017-09-17 12:45:20 +00:00
committed by László Monda
parent d621b1e5e6
commit 96e968729d
11 changed files with 185 additions and 33 deletions
@@ -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. * UHK USB Communications command. All communication package should have start with a command code.
*/ */
enum Command { enum Command {
GetProperty = 0,
UploadConfig = 8, UploadConfig = 8,
ApplyConfig = 9, ApplyConfig = 9,
LaunchEepromTransfer = 12, LaunchEepromTransfer = 12,
ReadUserConfig = 15,
GetKeyboardState = 16 GetKeyboardState = 16
} }
@@ -31,6 +33,15 @@ enum EepromTransfer {
WriteUserConfig = 3 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)); const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));
/** /**
@@ -38,6 +49,7 @@ const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));
* Functionality: * Functionality:
* - Detect device is connected or not * - Detect device is connected or not
* - Send UserConfiguration to the UHK Device * - Send UserConfiguration to the UHK Device
* - Read UserConfiguration from the UHK Device
*/ */
export class DeviceService { export class DeviceService {
private pollTimer$: Subscription; private pollTimer$: Subscription;
@@ -48,6 +60,7 @@ export class DeviceService {
private device: UhkHidDeviceService) { private device: UhkHidDeviceService) {
this.pollUhkDevice(); this.pollUhkDevice();
ipcMain.on(IpcEvents.device.saveUserConfiguration, this.saveUserConfiguration.bind(this)); ipcMain.on(IpcEvents.device.saveUserConfiguration, this.saveUserConfiguration.bind(this));
ipcMain.on(IpcEvents.device.loadUserConfiguration, this.loadUserConfiguration.bind(this));
logService.debug('[DeviceService] init success'); logService.debug('[DeviceService] init success');
} }
@@ -59,6 +72,38 @@ export class DeviceService {
return this.connected; return this.connected;
} }
/**
* Return with the actual UserConfiguration from UHK Device
* @returns {Promise<Buffer>}
*/
public async loadUserConfiguration(event: Electron.Event): Promise<void> {
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. * HID API not support device attached and detached event.
* This method check the keyboard is attached to the computer or not. * This method check the keyboard is attached to the computer or not.
@@ -76,25 +121,38 @@ export class DeviceService {
.do((connected: boolean) => { .do((connected: boolean) => {
this.connected = connected; this.connected = connected;
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, 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(); .subscribe();
} }
/**
* Return the UserConfiguration 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]));
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<void> { private async saveUserConfiguration(event: Electron.Event, json: string): Promise<void> {
const response = new IpcResponse(); const response = new IpcResponse();
try { 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(); await this.writeUserConfigToEeprom();
this.device.close();
response.success = true; response.success = true;
this.logService.info('transferring finished');
} }
catch (error) { catch (error) {
this.logService.error('[DeviceService] Transferring error', error); this.logService.error('[DeviceService] Transferring error', error);
response.error = {message: error.message}; response.error = {message: error.message};
} finally {
this.device.close();
} }
event.sender.send(IpcEvents.device.saveUserConfigurationReply, response); event.sender.send(IpcEvents.device.saveUserConfigurationReply, response);
@@ -114,19 +172,14 @@ export class DeviceService {
for (const fragment of fragments) { for (const fragment of fragments) {
await this.device.write(fragment); 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([Command.ApplyConfig]);
await this.device.write(applyBuffer); await this.device.write(applyBuffer);
this.logService.info('[DeviceService] Transferring finished');
} }
private async writeUserConfigToEeprom(): Promise<void> { private async writeUserConfigToEeprom(): Promise<void> {
this.logService.info('[DeviceService] Start write user configuration to eeprom'); await this.device.write(new Buffer([Command.LaunchEepromTransfer, EepromTransfer.WriteUserConfig]));
const buffer = await this.device.write(new Buffer([Command.LaunchEepromTransfer, EepromTransfer.WriteUserConfig]));
await this.waitUntilKeyboardBusy(); await this.waitUntilKeyboardBusy();
this.logService.info('[DeviceService] End write user configuration to eeprom');
} }
private async waitUntilKeyboardBusy(): Promise<void> { private async waitUntilKeyboardBusy(): Promise<void> {
@@ -6,6 +6,17 @@ import { Constants, LogService } from 'uhk-common';
* HID API wrapper to support unified logging and async write * HID API wrapper to support unified logging and async write
*/ */
export class UhkHidDeviceService { 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 * Create the communication package that will send over USB and
* - add usb report code as 1st byte * - add usb report code as 1st byte
@@ -32,17 +43,6 @@ export class UhkHidDeviceService {
return data; 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 * Convert buffer to space separated hexadecimal string
* @param {Buffer} buffer * @param {Buffer} buffer
@@ -109,7 +109,7 @@ export class UhkHidDeviceService {
return reject(err); return reject(err);
} }
const logString = UhkHidDeviceService.bufferToString(receivedData); const logString = UhkHidDeviceService.bufferToString(receivedData);
this.logService.debug('[UhkHidDevice] Transfer UHK ===> Agent: ', logString); this.logService.debug('[UhkHidDevice] USB[R]:', logString);
if (receivedData[0] !== 0) { if (receivedData[0] !== 0) {
return reject(new Error(`Communications error with UHK. Response code: ${receivedData[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); 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); device.write(sendData);
}); });
} }
@@ -22,6 +22,8 @@ class Device {
public static readonly deviceConnectionStateChanged = 'device-connection-state-changed'; public static readonly deviceConnectionStateChanged = 'device-connection-state-changed';
public static readonly saveUserConfiguration = 'device-save-user-configuration'; public static readonly saveUserConfiguration = 'device-save-user-configuration';
public static readonly saveUserConfigurationReply = 'device-save-user-configuration-reply'; 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 { export class IpcEvents {
@@ -9,6 +9,7 @@ import {
SaveConfigurationReplyAction, SaveConfigurationReplyAction,
SetPrivilegeOnLinuxReplyAction SetPrivilegeOnLinuxReplyAction
} from '../store/actions/device'; } from '../store/actions/device';
import { LoadUserConfigFromDeviceReplyAction } from '../store/actions/user-config';
@Injectable() @Injectable()
export class DeviceRendererService { export class DeviceRendererService {
@@ -28,6 +29,10 @@ export class DeviceRendererService {
this.ipcRenderer.send(IpcEvents.device.saveUserConfiguration, JSON.stringify(buffer)); this.ipcRenderer.send(IpcEvents.device.saveUserConfiguration, JSON.stringify(buffer));
} }
loadUserConfiguration(): void {
this.ipcRenderer.send(IpcEvents.device.loadUserConfiguration);
}
private registerEvents(): void { private registerEvents(): void {
this.ipcRenderer.on(IpcEvents.device.deviceConnectionStateChanged, (event: string, arg: boolean) => { this.ipcRenderer.on(IpcEvents.device.deviceConnectionStateChanged, (event: string, arg: boolean) => {
this.dispachStoreAction(new ConnectionStateChangedAction(arg)); this.dispachStoreAction(new ConnectionStateChangedAction(arg));
@@ -40,6 +45,10 @@ export class DeviceRendererService {
this.ipcRenderer.on(IpcEvents.device.saveUserConfigurationReply, (event: string, response: IpcResponse) => { this.ipcRenderer.on(IpcEvents.device.saveUserConfigurationReply, (event: string, response: IpcResponse) => {
this.dispachStoreAction(new SaveConfigurationReplyAction(response)); 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 { private dispachStoreAction(action: Action): void {
@@ -8,6 +8,8 @@ const PREFIX = '[user-config] ';
// tslint:disable-next-line:variable-name // tslint:disable-next-line:variable-name
export const ActionTypes = { export const ActionTypes = {
LOAD_USER_CONFIG: type(PREFIX + 'Load User Config'), 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'), LOAD_USER_CONFIG_SUCCESS: type(PREFIX + 'Load User Config Success'),
SAVE_USER_CONFIG_SUCCESS: type(PREFIX + 'Save 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; 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<number>) { }
}
export class LoadUserConfigSuccessAction implements Action { export class LoadUserConfigSuccessAction implements Action {
type = ActionTypes.LOAD_USER_CONFIG_SUCCESS; type = ActionTypes.LOAD_USER_CONFIG_SUCCESS;
@@ -30,7 +30,7 @@ export class ApplicationEffects {
this.logService.info('Renderer appStart effect end'); this.logService.info('Renderer appStart effect end');
}); });
@Effect({ dispatch: false }) @Effect({dispatch: false})
showNotification$: Observable<Action> = this.actions$ showNotification$: Observable<Action> = this.actions$
.ofType(ActionTypes.APP_SHOW_NOTIFICATION) .ofType(ActionTypes.APP_SHOW_NOTIFICATION)
.map(toPayload) .map(toPayload)
@@ -15,7 +15,8 @@ import 'rxjs/add/operator/withLatestFrom';
import { NotificationType, IpcResponse } from 'uhk-common'; import { NotificationType, IpcResponse } from 'uhk-common';
import { import {
ActionTypes, ActionTypes,
ConnectionStateChangedAction, HideSaveToKeyboardButton, ConnectionStateChangedAction,
HideSaveToKeyboardButton,
PermissionStateChangedAction, PermissionStateChangedAction,
SaveToKeyboardSuccessAction, SaveToKeyboardSuccessAction,
SaveToKeyboardSuccessFailed SaveToKeyboardSuccessFailed
@@ -25,10 +26,11 @@ import { ShowNotificationAction } from '../actions/app';
import { AppState } from '../index'; import { AppState } from '../index';
import { UserConfiguration } from '../../config-serializer/config-items/user-configuration'; import { UserConfiguration } from '../../config-serializer/config-items/user-configuration';
import { UhkBuffer } from '../../config-serializer/uhk-buffer'; import { UhkBuffer } from '../../config-serializer/uhk-buffer';
import { LoadUserConfigFromDeviceAction } from '../actions/user-config';
@Injectable() @Injectable()
export class DeviceEffects { export class DeviceEffects {
@Effect({dispatch: false}) @Effect()
deviceConnectionStateChange$: Observable<Action> = this.actions$ deviceConnectionStateChange$: Observable<Action> = this.actions$
.ofType(ActionTypes.CONNECTION_STATE_CHANGED) .ofType(ActionTypes.CONNECTION_STATE_CHANGED)
.map(toPayload) .map(toPayload)
@@ -39,6 +41,13 @@ export class DeviceEffects {
else { else {
this.router.navigate(['/detection']); this.router.navigate(['/detection']);
} }
})
.switchMap((connected: boolean) => {
if (connected) {
return Observable.of(new LoadUserConfigFromDeviceAction());
}
return Observable.empty();
}); });
@Effect({dispatch: false}) @Effect({dispatch: false})
@@ -11,7 +11,7 @@ import 'rxjs/add/operator/withLatestFrom';
import 'rxjs/add/operator/mergeMap'; import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import { NotificationType } from 'uhk-common'; import { LogService, NotificationType } from 'uhk-common';
import { import {
ActionTypes, ActionTypes,
@@ -29,6 +29,8 @@ import { MacroActions } from '../actions/macro';
import { UndoUserConfigData } from '../../models/undo-user-config-data'; import { UndoUserConfigData } from '../../models/undo-user-config-data';
import { ShowNotificationAction, DismissUndoNotificationAction } from '../actions/app'; import { ShowNotificationAction, DismissUndoNotificationAction } from '../actions/app';
import { ShowSaveToKeyboardButtonAction } from '../actions/device'; import { ShowSaveToKeyboardButtonAction } from '../actions/device';
import { DeviceRendererService } from '../../services/device-renderer.service';
import { UhkBuffer } from '../../config-serializer/uhk-buffer';
@Injectable() @Injectable()
export class UserConfigEffects { export class UserConfigEffects {
@@ -86,10 +88,46 @@ export class UserConfigEffects {
return [new LoadUserConfigSuccessAction(config), go(payload.path)]; 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<number>) => {
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, constructor(private actions$: Actions,
private dataStorageRepository: DataStorageRepositoryService, private dataStorageRepository: DataStorageRepositoryService,
private store: Store<AppState>, private store: Store<AppState>,
private defaultUserConfigurationService: DefaultUserConfigurationService) { private defaultUserConfigurationService: DefaultUserConfigurationService,
private deviceRendererService: DeviceRendererService,
private logService: LogService) {
} }
private getUserConfiguration() { private getUserConfiguration() {
@@ -13,13 +13,15 @@ export interface State {
navigationCountAfterNotification: number; navigationCountAfterNotification: number;
prevUserConfig?: UserConfiguration; prevUserConfig?: UserConfiguration;
runningInElectron: boolean; runningInElectron: boolean;
userConfigLoading: boolean;
} }
const initialState: State = { const initialState: State = {
started: false, started: false,
showAddonMenu: false, showAddonMenu: false,
navigationCountAfterNotification: 0, navigationCountAfterNotification: 0,
runningInElectron: runInElectron() runningInElectron: runInElectron(),
userConfigLoading: true
}; };
export function reducer(state = initialState, action: Action) { export function reducer(state = initialState, action: Action) {
@@ -76,7 +78,16 @@ export function reducer(state = initialState, action: Action) {
case UserConfigActionTypes.SAVE_USER_CONFIG_SUCCESS: { case UserConfigActionTypes.SAVE_USER_CONFIG_SUCCESS: {
return { return {
...state, ...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
}; };
} }
@@ -4,10 +4,27 @@ import * as util from 'util';
import { LogService } from 'uhk-common'; 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 // https://github.com/megahertz/electron-log/issues/44
// console.debug starting with Chromium 58 this method is a no-op on Chromium browsers. // console.debug starting with Chromium 58 this method is a no-op on Chromium browsers.
if (console.debug) { 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() @Injectable()
export class ElectronLogService implements LogService { export class ElectronLogService implements LogService {
private static getErrorText(args: any) { public static getErrorText(args: any) {
return util.inspect(args); return util.inspect(args);
} }
+1
View File
@@ -27,6 +27,7 @@ while(offset < configSize) {
const usbCommand = isHardwareConfig ? uhk.usbCommands.readHardwareConfig : uhk.usbCommands.readUserConfig; const usbCommand = isHardwareConfig ? uhk.usbCommands.readHardwareConfig : uhk.usbCommands.readUserConfig;
chunkSizeToRead = Math.min(chunkSize, configSize - offset); chunkSizeToRead = Math.min(chunkSize, configSize - offset);
buffer = Buffer.from([usbCommand, chunkSizeToRead, offset & 0xff, offset >> 8]); buffer = Buffer.from([usbCommand, chunkSizeToRead, offset & 0xff, offset >> 8]);
console.log('write to keyboard', uhk.bufferToString(buffer));
device.write(uhk.getTransferData(buffer)); device.write(uhk.getTransferData(buffer));
buffer = Buffer.from(device.readSync()); buffer = Buffer.from(device.readSync());
console.log('read-config-chunk', uhk.bufferToString(buffer)); console.log('read-config-chunk', uhk.bufferToString(buffer));