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:
committed by
László Monda
parent
f608791a09
commit
297fd3be79
1
packages/uhk-agent/src/custom_types/decompress.d.ts
vendored
Normal file
1
packages/uhk-agent/src/custom_types/decompress.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'decompress';
|
||||
@@ -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);
|
||||
|
||||
7
packages/uhk-agent/src/models/tmp-firmware.ts
Normal file
7
packages/uhk-agent/src/models/tmp-firmware.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { SynchrounousResult } from 'tmp';
|
||||
|
||||
export interface TmpFirmware {
|
||||
rightFirmwarePath: string;
|
||||
leftFirmwarePath: string;
|
||||
tmpDirectory: SynchrounousResult;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
37
packages/uhk-agent/src/util/save-extract-firmware.ts
Normal file
37
packages/uhk-agent/src/util/save-extract-firmware.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user