feat(device): flash device firmware from Agent (#499)

* add dataModelVersion, usbProtocolVersion, slaveProtocolVersion

* read the package.json at appstart

* flash firmware

* update firmware

* fix extra resource path

* fix import modules

* update lock files

* fix imports

* terminal window

* exclude tmp folder from git repo

* ok button

* auto scroll in xterm

* fix maxTry count calculation

* optimize logging

* optimize timeout

* readSync

* Add extra delay

* fix async call

* fix error message in log

* fix ok button disable state

* retry

* list devices

* close device after reenumeration

* retry snooze

* kboot maxtry 10

* retry 100

* remove deprecated toPayload ngrx helper

* flash firmware with custom file

* fix tslint
This commit is contained in:
Róbert Kiss
2017-11-27 22:12:43 +01:00
committed by László Monda
parent f608791a09
commit 297fd3be79
52 changed files with 3914 additions and 894 deletions

View File

@@ -0,0 +1 @@
declare module 'decompress';

View File

@@ -8,15 +8,16 @@ 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 { UhkHidDevice, UhkOperations } from 'uhk-usb';
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
import { CommandLineArgs } from 'uhk-common';
import { CommandLineArgs, LogRegExps } from 'uhk-common';
import { DeviceService } from './services/device.service';
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 { UhkBlhost } from '../../uhk-usb/src';
import * as isDev from 'electron-is-dev';
const optionDefinitions = [
{name: 'addons', type: Boolean, defaultOption: false}
@@ -33,13 +34,41 @@ let win: Electron.BrowserWindow;
autoUpdater.logger = logger;
let deviceService: DeviceService;
let uhkBlhost: UhkBlhost;
let uhkHidDeviceService: UhkHidDevice;
let uhkOperations: UhkOperations;
let appUpdateService: AppUpdateService;
let appService: AppService;
let sudoService: SudoService;
// 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 = (...args: any[]): void => {
if (LogRegExps.writeRegExp.test(args[0])) {
console.log(args[0]);
} else if (LogRegExps.readRegExp.test(args[0])) {
console.log(args[0]);
} else if (LogRegExps.errorRegExp.test(args[0])) {
console.log(args[0]);
} else if (LogRegExps.transferRegExp.test(args[0])) {
console.log(args[0]);
} else {
console.log(...args);
}
};
}
function createWindow() {
logger.info('[Electron Main] Create new window.');
let packagesDir;
if (isDev) {
packagesDir = path.join(path.join(process.cwd(), process.argv[1]), '../../../../tmp');
} else {
packagesDir = path.dirname(app.getAppPath());
}
logger.info(`[Electron Main] packagesDir: ${packagesDir}`);
// Create the browser window.
win = new BrowserWindow({
@@ -54,7 +83,9 @@ function createWindow() {
win.setMenuBarVisibility(false);
win.maximize();
uhkHidDeviceService = new UhkHidDevice(logger);
deviceService = new DeviceService(logger, win, uhkHidDeviceService);
uhkBlhost = new UhkBlhost(logger, packagesDir);
uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir);
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations);
appUpdateService = new AppUpdateService(logger, win, app);
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);
sudoService = new SudoService(logger);

View File

@@ -0,0 +1,7 @@
import { SynchrounousResult } from 'tmp';
export interface TmpFirmware {
rightFirmwarePath: string;
leftFirmwarePath: string;
tmpDirectory: SynchrounousResult;
}

View File

@@ -15,5 +15,9 @@
},
"dependencies": {
"node-hid": "0.5.7"
}
},
"dataModelVersion": "1.0.0",
"usbProtocolVersion": "1.2.0",
"slaveProtocolVersion": "2.1.0",
"firmwareVersion": "3.0.0"
}

View File

@@ -1,7 +1,9 @@
import { ipcMain, BrowserWindow } from 'electron';
import { BrowserWindow, ipcMain } from 'electron';
import { UhkHidDevice } from 'uhk-usb';
import { readFile } from 'fs';
import { join } from 'path';
import { CommandLineArgs, IpcEvents, AppStartInfo, LogService } from 'uhk-common';
import { AppStartInfo, CommandLineArgs, IpcEvents, LogService } from 'uhk-common';
import { MainServiceBase } from './main-service-base';
import { DeviceService } from './device.service';
@@ -14,17 +16,44 @@ export class AppService extends MainServiceBase {
super(logService, win);
ipcMain.on(IpcEvents.app.getAppStartInfo, this.handleAppStartInfo.bind(this));
logService.info('AppService init success');
logService.info('[AppService] init success');
}
private handleAppStartInfo(event: Electron.Event) {
this.logService.info('getStartInfo');
private async handleAppStartInfo(event: Electron.Event) {
this.logService.info('[AppService] getAppStartInfo');
const packageJson = await this.getPackageJson();
const response: AppStartInfo = {
commandLineArgs: this.options,
deviceConnected: this.deviceService.isConnected,
hasPermission: this.uhkHidDeviceService.hasPermission()
hasPermission: this.uhkHidDeviceService.hasPermission(),
agentVersionInfo: {
version: packageJson.version,
dataModelVersion: packageJson.dataModelVersion,
usbProtocolVersion: packageJson.usbProtocolVersion,
slaveProtocolVersion: packageJson.slaveProtocolVersion,
firmwareVersion: packageJson.firmwareVersion
}
};
this.logService.info('getStartInfo response:', response);
this.logService.info('[AppService] getAppStartInfo response:', response);
return event.sender.send(IpcEvents.app.getAppStartInfoReply, response);
}
/**
* Read the package.json that delivered with the bundle. Do not use require('package.json')
* because the deploy process change the package.json after the build
* @returns {Promise<any>}
*/
private async getPackageJson(): Promise<any> {
return new Promise((resolve, reject) => {
readFile(join(__dirname, 'package.json'), {encoding: 'utf-8'}, (err, data) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(data));
});
});
}
}

View File

@@ -1,10 +1,20 @@
import { ipcMain } from 'electron';
import { IpcEvents, LogService, IpcResponse, ConfigurationReply } from 'uhk-common';
import { Constants, EepromTransfer, SystemPropertyIds, UsbCommand } from 'uhk-usb';
import { ConfigurationReply, IpcEvents, IpcResponse, LogService } from 'uhk-common';
import {
Constants,
convertBufferToIntArray,
EepromTransfer,
getTransferBuffers,
snooze,
SystemPropertyIds,
UhkHidDevice,
UhkOperations,
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 { emptyDir } from 'fs-extra';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/startWith';
@@ -12,6 +22,9 @@ import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/distinctUntilChanged';
import { saveTmpFirmware } from '../util/save-extract-firmware';
import { TmpFirmware } from '../models/tmp-firmware';
/**
* IpcMain pair of the UHK Communication
* Functionality:
@@ -21,14 +34,17 @@ import 'rxjs/add/operator/distinctUntilChanged';
*/
export class DeviceService {
private pollTimer$: Subscription;
private connected: boolean = false;
private connected = false;
constructor(private logService: LogService,
private win: Electron.BrowserWindow,
private device: UhkHidDevice) {
private device: UhkHidDevice,
private operations: UhkOperations) {
this.pollUhkDevice();
ipcMain.on(IpcEvents.device.saveUserConfiguration, this.saveUserConfiguration.bind(this));
ipcMain.on(IpcEvents.device.loadConfigurations, this.loadConfigurations.bind(this));
ipcMain.on(IpcEvents.device.updateFirmware, this.updateFirmware.bind(this));
ipcMain.on(IpcEvents.device.startConnectionPoller, this.pollUhkDevice.bind(this));
logService.debug('[DeviceService] init success');
}
@@ -45,6 +61,8 @@ export class DeviceService {
* @returns {Promise<Buffer>}
*/
public async loadConfigurations(event: Electron.Event): Promise<void> {
let response: ConfigurationReply;
try {
await this.device.waitUntilKeyboardBusy();
const userConfiguration = await this.loadConfiguration(
@@ -57,21 +75,22 @@ export class DeviceService {
UsbCommand.ReadHardwareConfig,
'hardware configuration');
const response: ConfigurationReply = {
response = {
success: true,
userConfiguration,
hardwareConfiguration
};
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
} catch (error) {
const response: ConfigurationReply = {
response = {
success: false,
error: error.message
};
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
} finally {
this.device.close();
}
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
}
/**
@@ -102,7 +121,7 @@ export class DeviceService {
configSize = readBuffer[3] + (readBuffer[4] << 8);
}
}
response = UhkHidDevice.convertBufferToIntArray(configBuffer);
response = convertBufferToIntArray(configBuffer);
return Promise.resolve(JSON.stringify(response));
} catch (error) {
const errMsg = `[DeviceService] ${configName} from eeprom error`;
@@ -113,9 +132,40 @@ export class DeviceService {
public close(): void {
this.connected = false;
this.pollTimer$.unsubscribe();
this.stopPollTimer();
this.logService.info('[DeviceService] Device connection checker stopped.');
}
public async updateFirmware(event: Electron.Event, data?: string): Promise<void> {
const response = new IpcResponse();
let firmwarePathData: TmpFirmware;
try {
this.stopPollTimer();
if (data) {
firmwarePathData = await saveTmpFirmware(data);
await this.operations.updateRightFirmware(firmwarePathData.rightFirmwarePath);
await this.operations.updateLeftModule(firmwarePathData.leftFirmwarePath);
}
else {
await this.operations.updateRightFirmware();
await this.operations.updateLeftModule();
}
response.success = true;
} catch (error) {
const err = {message: error.message, stack: error.stack};
this.logService.error('[DeviceService] updateFirmware error', err);
response.error = err;
}
await emptyDir(firmwarePathData.tmpDirectory.name);
await snooze(500);
event.sender.send(IpcEvents.device.updateFirmwareReply, response);
}
/**
@@ -125,6 +175,10 @@ export class DeviceService {
* @private
*/
private pollUhkDevice(): void {
if (this.pollTimer$) {
return;
}
this.pollTimer$ = Observable.interval(1000)
.startWith(0)
.map(() => {
@@ -146,6 +200,7 @@ export class DeviceService {
*/
private async getConfigSizeFromKeyboard(property: SystemPropertyIds): Promise<number> {
const buffer = await this.device.write(new Buffer([UsbCommand.GetProperty, property]));
this.device.close();
const configSize = buffer[1] + (buffer[2] << 8);
this.logService.debug('[DeviceService] User config size:', configSize);
return configSize;
@@ -182,7 +237,7 @@ export class DeviceService {
*/
private async sendUserConfigToKeyboard(json: string): Promise<void> {
const buffer: Buffer = new Buffer(JSON.parse(json).data);
const fragments = UhkHidDevice.getTransferBuffers(UsbCommand.UploadUserConfig, buffer);
const fragments = getTransferBuffers(UsbCommand.UploadUserConfig, buffer);
for (const fragment of fragments) {
await this.device.write(fragment);
}
@@ -190,4 +245,14 @@ export class DeviceService {
const applyBuffer = new Buffer([UsbCommand.ApplyConfig]);
await this.device.write(applyBuffer);
}
private stopPollTimer(): void {
if (!this.pollTimer$) {
return;
}
this.pollTimer$.unsubscribe();
this.pollTimer$ = null;
}
}

View File

@@ -10,7 +10,7 @@ export class SudoService {
constructor(private logService: LogService) {
if (isDev) {
this.rootDir = path.join(path.join(process.cwd(), process.argv[1]), '../../..');
this.rootDir = path.join(path.join(process.cwd(), process.argv[1]), '..');
} else {
this.rootDir = path.dirname(app.getAppPath());
}

View File

@@ -0,0 +1,37 @@
import * as fs from 'fs';
import * as path from 'path';
import { dirSync } from 'tmp';
import * as decompress from 'decompress';
import * as decompressTarbz from 'decompress-tarbz2';
import { TmpFirmware } from '../models/tmp-firmware';
export async function saveTmpFirmware(data: string): Promise<TmpFirmware> {
const tmpDirectory = dirSync();
const zipFilePath = path.join(tmpDirectory.name, 'firmware.bz2');
await writeDataToFile(data, zipFilePath);
await decompress(zipFilePath, tmpDirectory.name, {plugins: [decompressTarbz()]});
return {
tmpDirectory,
rightFirmwarePath: path.join(tmpDirectory.name, 'devices/uhk60-right/firmware.hex'),
leftFirmwarePath: path.join(tmpDirectory.name, 'modules/uhk60-left.bin')
};
}
function writeDataToFile(data: string, filePath: string): Promise<void> {
return new Promise((resolve, reject) => {
const array: Array<number> = JSON.parse(data);
const buffer = new Buffer(array);
fs.writeFile(filePath, buffer, err => {
if (err) {
return reject();
}
resolve();
});
});
}