feat: device recovery mode (#642)

* add new page and ipc processing

* refactor: remove unused references from uhk.js

* feat: add device recovery route

* refactor: device permission

* feat: write firmware update log to the screen

* fix: xterm height

* feat: add reload button to the recovery page

* refactor: deviceConnectionState.hasPermission in appStartInfo

* refactor: use correct imports

* refactor: move .ok-button css class to the main style.scss

* feat: add bootload active route guard

* style: move RecoveryDeviceAction into new line

* feat: delete reload button

* feat: start device polling after device recovery
This commit is contained in:
Róbert Kiss
2018-05-19 17:22:46 +02:00
committed by László Monda
parent 2cf8044987
commit 653465f0e0
27 changed files with 274 additions and 64 deletions

3
package-lock.json generated
View File

@@ -5838,7 +5838,8 @@
"jsbn": { "jsbn": {
"version": "0.1.1", "version": "0.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"json-schema": { "json-schema": {
"version": "0.2.3", "version": "0.2.3",

View File

@@ -22,13 +22,14 @@ export class AppService extends MainServiceBase {
private async handleAppStartInfo(event: Electron.Event) { private async handleAppStartInfo(event: Electron.Event) {
this.logService.info('[AppService] getAppStartInfo'); this.logService.info('[AppService] getAppStartInfo');
const deviceConnectionState = this.uhkHidDeviceService.getDeviceConnectionState();
const response: AppStartInfo = { const response: AppStartInfo = {
commandLineArgs: { commandLineArgs: {
addons: this.options.addons || false addons: this.options.addons || false
}, },
deviceConnected: this.uhkHidDeviceService.deviceConnected(), deviceConnected: deviceConnectionState.connected,
hasPermission: this.uhkHidDeviceService.hasPermission() hasPermission: deviceConnectionState.hasPermission,
bootloaderActive: deviceConnectionState.bootloaderActive
}; };
this.logService.info('[AppService] getAppStartInfo response:', response); this.logService.info('[AppService] getAppStartInfo response:', response);
return event.sender.send(IpcEvents.app.getAppStartInfoReply, response); return event.sender.send(IpcEvents.app.getAppStartInfoReply, response);

View File

@@ -10,7 +10,7 @@ import {
mapObjectToUserConfigBinaryBuffer, mapObjectToUserConfigBinaryBuffer,
SaveUserConfigurationData SaveUserConfigurationData
} from 'uhk-common'; } from 'uhk-common';
import { snooze, UhkHidDevice, UhkOperations } from 'uhk-usb'; import { deviceConnectionStateComparer, snooze, UhkHidDevice, UhkOperations } from 'uhk-usb';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { emptyDir } from 'fs-extra'; import { emptyDir } from 'fs-extra';
@@ -71,6 +71,15 @@ export class DeviceService {
ipcMain.on(IpcEvents.device.startConnectionPoller, this.pollUhkDevice.bind(this)); ipcMain.on(IpcEvents.device.startConnectionPoller, this.pollUhkDevice.bind(this));
ipcMain.on(IpcEvents.device.recoveryDevice, (...args: any[]) => {
this.queueManager.add({
method: this.recoveryDevice,
bind: this,
params: args,
asynchronous: true
});
});
logService.debug('[DeviceService] init success'); logService.debug('[DeviceService] init success');
} }
@@ -148,6 +157,29 @@ export class DeviceService {
event.sender.send(IpcEvents.device.updateFirmwareReply, response); event.sender.send(IpcEvents.device.updateFirmwareReply, response);
} }
public async recoveryDevice(event: Electron.Event): Promise<void> {
const response = new IpcResponse();
try {
this.stopPollTimer();
await this.operations.updateRightFirmware();
await snooze(500);
this.pollUhkDevice();
response.success = true;
} catch (error) {
const err = {message: error.message, stack: error.stack};
this.logService.error('[DeviceService] updateFirmware error', err);
response.error = err;
}
await snooze(500);
event.sender.send(IpcEvents.device.updateFirmwareReply, 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.
@@ -161,16 +193,11 @@ export class DeviceService {
this.pollTimer$ = Observable.interval(1000) this.pollTimer$ = Observable.interval(1000)
.startWith(0) .startWith(0)
.map(() => this.device.deviceConnected()) .map(() => this.device.getDeviceConnectionState())
.distinctUntilChanged() .distinctUntilChanged<DeviceConnectionState>(deviceConnectionStateComparer)
.do((connected: boolean) => { .do((state: DeviceConnectionState) => {
const response: DeviceConnectionState = { this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, state);
connected, this.logService.info('[DeviceService] Device connection state changed to:', state);
hasPermission: this.device.hasPermission()
};
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, response);
this.logService.info('[DeviceService] Device connection state changed to:', response);
}) })
.subscribe(); .subscribe();
} }

View File

@@ -4,4 +4,5 @@ export interface AppStartInfo {
commandLineArgs: CommandLineArgs; commandLineArgs: CommandLineArgs;
deviceConnected: boolean; deviceConnected: boolean;
hasPermission: boolean; hasPermission: boolean;
bootloaderActive: boolean;
} }

View File

@@ -1,4 +1,5 @@
export interface DeviceConnectionState { export interface DeviceConnectionState {
connected: boolean; connected: boolean;
hasPermission: boolean; hasPermission: boolean;
bootloaderActive: boolean;
} }

View File

@@ -29,6 +29,7 @@ export class Device {
public static readonly updateFirmware = 'device-update-firmware'; public static readonly updateFirmware = 'device-update-firmware';
public static readonly updateFirmwareReply = 'device-update-firmware-reply'; public static readonly updateFirmwareReply = 'device-update-firmware-reply';
public static readonly startConnectionPoller = 'device-start-connection-poller'; public static readonly startConnectionPoller = 'device-start-connection-poller';
public static readonly recoveryDevice = 'device-recovery';
} }
export class IpcEvents { export class IpcEvents {

View File

@@ -1,6 +1,7 @@
export namespace Constants { export namespace Constants {
export const VENDOR_ID = 0x1D50; export const VENDOR_ID = 0x1D50;
export const PRODUCT_ID = 0x6122; export const PRODUCT_ID = 0x6122;
export const BOOTLOADER_ID = 0x6120;
export const MAX_PAYLOAD_SIZE = 64; export const MAX_PAYLOAD_SIZE = 64;
} }

View File

@@ -1,6 +1,6 @@
import { cloneDeep, isEqual } from 'lodash'; import { cloneDeep, isEqual } from 'lodash';
import { Device, devices, HID } from 'node-hid'; import { Device, devices, HID } from 'node-hid';
import { CommandLineArgs, LogService } from 'uhk-common'; import { CommandLineArgs, DeviceConnectionState, LogService } from 'uhk-common';
import { import {
ConfigBufferId, ConfigBufferId,
@@ -50,12 +50,10 @@ export class UhkHidDevice {
return true; return true;
} }
if (!this.deviceConnected()) { const devs = devices();
return true;
}
this._hasPermission = this.getDevice() !== null; this._hasPermission = devs.some((x: Device) => x.vendorId === Constants.VENDOR_ID &&
this.close(); (x.productId === Constants.PRODUCT_ID || x.productId === Constants.BOOTLOADER_ID));
return this._hasPermission; return this._hasPermission;
} catch (err) { } catch (err) {
@@ -69,15 +67,25 @@ export class UhkHidDevice {
* Return with true is an UHK Device is connected to the computer. * Return with true is an UHK Device is connected to the computer.
* @returns {boolean} * @returns {boolean}
*/ */
public deviceConnected(): boolean { public getDeviceConnectionState(): DeviceConnectionState {
const connected = devices().some((dev: Device) => dev.vendorId === Constants.VENDOR_ID && const devs = devices();
dev.productId === Constants.PRODUCT_ID); const result: DeviceConnectionState = {
bootloaderActive: false,
connected: false,
hasPermission: this.hasPermission()
};
if (!connected) { for (const dev of devs) {
this._hasPermission = false; if (dev.vendorId === Constants.VENDOR_ID &&
dev.productId === Constants.PRODUCT_ID) {
result.connected = true;
} else if (dev.vendorId === Constants.VENDOR_ID &&
dev.productId === Constants.BOOTLOADER_ID) {
result.bootloaderActive = true;
}
} }
return connected; return result;
} }
/** /**

View File

@@ -1,5 +1,5 @@
import { Constants, UsbCommand } from './constants'; import { Constants, UsbCommand } from './constants';
import { LogService } from 'uhk-common'; import { DeviceConnectionState, LogService } from 'uhk-common';
export const snooze = ms => new Promise(resolve => setTimeout(resolve, ms)); export const snooze = ms => new Promise(resolve => setTimeout(resolve, ms));
@@ -95,3 +95,9 @@ export async function retry(command: Function, maxTry = 3, logService?: LogServi
} }
} }
} }
export const deviceConnectionStateComparer = (a: DeviceConnectionState, b: DeviceConnectionState): boolean => {
return a.hasPermission === b.hasPermission
&& a.connected === b.connected
&& a.bootloaderActive === b.bootloaderActive;
};

View File

@@ -5,17 +5,19 @@ import { deviceRoutes } from './components/device';
import { addOnRoutes } from './components/add-on'; import { addOnRoutes } from './components/add-on';
import { keymapRoutes } from './components/keymap'; import { keymapRoutes } from './components/keymap';
import { macroRoutes } from './components/macro'; import { macroRoutes } from './components/macro';
import { PrivilegeCheckerComponent } from './components/privilege-checker/privilege-checker.component'; import { PrivilegeCheckerComponent } from './components/privilege-checker';
import { MissingDeviceComponent } from './components/missing-device/missing-device.component'; import { MissingDeviceComponent } from './components/missing-device';
import { UhkDeviceDisconnectedGuard } from './services/uhk-device-disconnected.guard'; import { UhkDeviceDisconnectedGuard } from './services/uhk-device-disconnected.guard';
import { UhkDeviceConnectedGuard } from './services/uhk-device-connected.guard'; import { UhkDeviceConnectedGuard } from './services/uhk-device-connected.guard';
import { UhkDeviceUninitializedGuard } from './services/uhk-device-uninitialized.guard'; import { UhkDeviceUninitializedGuard } from './services/uhk-device-uninitialized.guard';
import { UhkDeviceInitializedGuard } from './services/uhk-device-initialized.guard'; import { UhkDeviceInitializedGuard } from './services/uhk-device-initialized.guard';
import { MainPage } from './pages/main-page/main.page'; import { MainPage } from './pages/main-page/main.page';
import { agentRoutes } from './components/agent/agent.routes'; import { agentRoutes } from './components/agent';
import { LoadingDevicePageComponent } from './pages/loading-page/loading-device.page'; import { LoadingDevicePageComponent } from './pages/loading-page/loading-device.page';
import { UhkDeviceLoadingGuard } from './services/uhk-device-loading.guard'; import { UhkDeviceLoadingGuard } from './services/uhk-device-loading.guard';
import { UhkDeviceLoadedGuard } from './services/uhk-device-loaded.guard'; import { UhkDeviceLoadedGuard } from './services/uhk-device-loaded.guard';
import { RecoveryModeComponent } from './components/device';
import { UhkDeviceBootloaderNotActiveGuard } from './services/uhk-device-bootloader-not-active.guard';
const appRoutes: Routes = [ const appRoutes: Routes = [
{ {
@@ -33,6 +35,11 @@ const appRoutes: Routes = [
component: LoadingDevicePageComponent, component: LoadingDevicePageComponent,
canActivate: [UhkDeviceLoadedGuard] canActivate: [UhkDeviceLoadedGuard]
}, },
{
path: 'recovery-device',
component: RecoveryModeComponent,
canActivate: [UhkDeviceBootloaderNotActiveGuard]
},
{ {
path: '', path: '',
component: MainPage, component: MainPage,

View File

@@ -5,6 +5,7 @@ import { DeviceFirmwareComponent } from './firmware/device-firmware.component';
import { MouseSpeedComponent } from './mouse-speed/mouse-speed.component'; import { MouseSpeedComponent } from './mouse-speed/mouse-speed.component';
import { LEDBrightnessComponent } from './led-brightness/led-brightness.component'; import { LEDBrightnessComponent } from './led-brightness/led-brightness.component';
import { RestoreConfigurationComponent } from './restore-configuration/restore-configuration.component'; import { RestoreConfigurationComponent } from './restore-configuration/restore-configuration.component';
import { RecoveryModeComponent } from './recovery-mode/recovery-mode.component';
export const deviceRoutes: Routes = [ export const deviceRoutes: Routes = [
{ {
@@ -34,6 +35,10 @@ export const deviceRoutes: Routes = [
{ {
path: 'restore-user-configuration', path: 'restore-user-configuration',
component: RestoreConfigurationComponent component: RestoreConfigurationComponent
},
{
path: 'recovery-mode',
component: RecoveryModeComponent
} }
] ]
} }

View File

@@ -42,7 +42,7 @@
<div class="flex-grow" #scrollMe> <div class="flex-grow" #scrollMe>
<xterm [logs]="xtermLog$ | async"></xterm> <xterm [logs]="xtermLog$ | async"></xterm>
</div> </div>
<div class="footer"> <div class="flex-footer">
<button type="button" <button type="button"
class="btn btn-primary ok-button" class="btn btn-primary ok-button"
[disabled]="firmwareOkButtonDisabled$ | async" [disabled]="firmwareOkButtonDisabled$ | async"

View File

@@ -5,25 +5,3 @@
min-height: 100%; min-height: 100%;
width: 100%; width: 100%;
} }
.flex-container {
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
}
.flex-grow {
background-color: black;
overflow: auto;
flex: 1;
}
.footer {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.ok-button {
min-width: 100px;
}

View File

@@ -3,4 +3,5 @@ export * from './firmware/device-firmware.component';
export * from './mouse-speed/mouse-speed.component'; export * from './mouse-speed/mouse-speed.component';
export * from './led-brightness/led-brightness.component'; export * from './led-brightness/led-brightness.component';
export * from './restore-configuration/restore-configuration.component'; export * from './restore-configuration/restore-configuration.component';
export * from './recovery-mode/recovery-mode.component';
export * from './device.routes'; export * from './device.routes';

View File

@@ -0,0 +1,26 @@
<div class="full-height">
<div class="flex-container">
<div>
<h1>
<i class="fa fa-wrench"></i>
<span>Fix device</span>
</h1>
<p>
Your device seems to be broken. No worries, Agent can fix it.
</p>
<p>
<button class="btn btn-primary"
type="button"
[disabled]="flashFirmwareButtonDisbabled$ | async"
(click)="onRecoveryDevice()">Fix device
</button>
</p>
</div>
<div class="flex-grow" #scrollMe>
<xterm [logs]="xtermLog"></xterm>
</div>
<div class="flex-footer">
</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
:host {
overflow-y: auto;
display: block;
height: 100%;
width: 100%;
p {
margin: 1.5rem 0;
}
}

View File

@@ -0,0 +1,56 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { XtermLog } from '../../../models/xterm-log';
import { AppState, flashFirmwareButtonDisbabled, xtermLog } from '../../../store';
import { RecoveryDeviceAction } from '../../../store/actions/device';
@Component({
selector: 'device-recovery-mode',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './recovery-mode.component.html',
styleUrls: ['./recovery-mode.component.scss'],
host: {
'class': 'container-fluid'
}
})
export class RecoveryModeComponent implements OnInit, OnDestroy {
xtermLogSubscription: Subscription;
flashFirmwareButtonDisbabled$: Observable<boolean>;
xtermLog: Array<XtermLog>;
@ViewChild('scrollMe') divElement: ElementRef;
constructor(private store: Store<AppState>,
private cdRef: ChangeDetectorRef) {
}
ngOnInit(): void {
this.flashFirmwareButtonDisbabled$ = this.store.select(flashFirmwareButtonDisbabled);
this.xtermLogSubscription = this.store.select(xtermLog)
.subscribe(data => {
this.xtermLog = data;
this.cdRef.markForCheck();
if (this.divElement && this.divElement.nativeElement) {
setTimeout(() => {
this.divElement.nativeElement.scrollTop = this.divElement.nativeElement.scrollHeight;
});
}
});
}
ngOnDestroy(): void {
if (this.xtermLogSubscription) {
this.xtermLogSubscription.unsubscribe();
}
}
onRecoveryDevice(): void {
this.store.dispatch(new RecoveryDeviceAction());
}
}

View File

@@ -46,6 +46,10 @@ export class DeviceRendererService {
this.ipcRenderer.send(IpcEvents.device.startConnectionPoller); this.ipcRenderer.send(IpcEvents.device.startConnectionPoller);
} }
recoveryDevice(): void {
this.ipcRenderer.send(IpcEvents.device.recoveryDevice);
}
private registerEvents(): void { private registerEvents(): void {
this.ipcRenderer.on(IpcEvents.device.deviceConnectionStateChanged, (event: string, arg: DeviceConnectionState) => { this.ipcRenderer.on(IpcEvents.device.deviceConnectionStateChanged, (event: string, arg: DeviceConnectionState) => {
this.dispachStoreAction(new ConnectionStateChangedAction(arg)); this.dispachStoreAction(new ConnectionStateChangedAction(arg));

View File

@@ -0,0 +1,24 @@
import { CanActivate, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import { AppState, bootloaderActive } from '../store';
@Injectable()
export class UhkDeviceBootloaderNotActiveGuard implements CanActivate {
constructor(private store: Store<AppState>, private router: Router) { }
canActivate(): Observable<boolean> {
return this.store.select(bootloaderActive)
.do(active => {
if (!active) {
this.router.navigate(['/']);
}
});
}
}

View File

@@ -17,7 +17,8 @@ import {
DeviceFirmwareComponent, DeviceFirmwareComponent,
MouseSpeedComponent, MouseSpeedComponent,
LEDBrightnessComponent, LEDBrightnessComponent,
RestoreConfigurationComponent RestoreConfigurationComponent,
RecoveryModeComponent
} from './components/device'; } from './components/device';
import { KeymapAddComponent, KeymapEditComponent, KeymapHeaderComponent } from './components/keymap'; import { KeymapAddComponent, KeymapEditComponent, KeymapHeaderComponent } from './components/keymap';
import { LayersComponent } from './components/layers'; import { LayersComponent } from './components/layers';
@@ -105,6 +106,7 @@ import { XtermComponent } from './components/xterm/xterm.component';
import { SliderWrapperComponent } from './components/slider-wrapper/slider-wrapper.component'; import { SliderWrapperComponent } from './components/slider-wrapper/slider-wrapper.component';
import { EditableTextComponent } from './components/editable-text/editable-text.component'; import { EditableTextComponent } from './components/editable-text/editable-text.component';
import { Autofocus } from './directives/autofocus/autofocus.directive'; import { Autofocus } from './directives/autofocus/autofocus.directive';
import { UhkDeviceBootloaderNotActiveGuard } from './services/uhk-device-bootloader-not-active.guard';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -176,7 +178,8 @@ import { Autofocus } from './directives/autofocus/autofocus.directive';
SliderWrapperComponent, SliderWrapperComponent,
EditableTextComponent, EditableTextComponent,
Autofocus, Autofocus,
RestoreConfigurationComponent RestoreConfigurationComponent,
RecoveryModeComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@@ -211,7 +214,8 @@ import { Autofocus } from './directives/autofocus/autofocus.directive';
UhkDeviceInitializedGuard, UhkDeviceInitializedGuard,
UhkDeviceUninitializedGuard, UhkDeviceUninitializedGuard,
UhkDeviceLoadingGuard, UhkDeviceLoadingGuard,
UhkDeviceLoadedGuard UhkDeviceLoadedGuard,
UhkDeviceBootloaderNotActiveGuard
], ],
exports: [ exports: [
UhkMessageComponent, UhkMessageComponent,

View File

@@ -26,7 +26,8 @@ export const ActionTypes = {
MODULES_INFO_LOADED: type(PREFIX + 'module info loaded'), MODULES_INFO_LOADED: type(PREFIX + 'module info loaded'),
HAS_BACKUP_USER_CONFIGURATION: type(PREFIX + 'Store backup user configuration'), HAS_BACKUP_USER_CONFIGURATION: type(PREFIX + 'Store backup user configuration'),
RESTORE_CONFIGURATION_FROM_BACKUP: type(PREFIX + 'Restore configuration from backup'), RESTORE_CONFIGURATION_FROM_BACKUP: type(PREFIX + 'Restore configuration from backup'),
RESTORE_CONFIGURATION_FROM_BACKUP_SUCCESS: type(PREFIX + 'Restore configuration from backup success') RESTORE_CONFIGURATION_FROM_BACKUP_SUCCESS: type(PREFIX + 'Restore configuration from backup success'),
RECOVERY_DEVICE: type(PREFIX + 'Recovery device')
}; };
export class SetPrivilegeOnLinuxAction implements Action { export class SetPrivilegeOnLinuxAction implements Action {
@@ -140,6 +141,10 @@ export class RestoreUserConfigurationFromBackupSuccessAction implements Action {
type = ActionTypes.RESTORE_CONFIGURATION_FROM_BACKUP_SUCCESS; type = ActionTypes.RESTORE_CONFIGURATION_FROM_BACKUP_SUCCESS;
} }
export class RecoveryDeviceAction implements Action {
type = ActionTypes.RECOVERY_DEVICE;
}
export type Actions export type Actions
= SetPrivilegeOnLinuxAction = SetPrivilegeOnLinuxAction
| SetPrivilegeOnLinuxReplyAction | SetPrivilegeOnLinuxReplyAction
@@ -162,4 +167,5 @@ export type Actions
| RestoreUserConfigurationFromBackupAction | RestoreUserConfigurationFromBackupAction
| HasBackupUserConfigurationAction | HasBackupUserConfigurationAction
| RestoreUserConfigurationFromBackupSuccessAction | RestoreUserConfigurationFromBackupSuccessAction
| RecoveryDeviceAction
; ;

View File

@@ -68,7 +68,8 @@ export class ApplicationEffects {
new ApplyCommandLineArgsAction(appInfo.commandLineArgs), new ApplyCommandLineArgsAction(appInfo.commandLineArgs),
new ConnectionStateChangedAction({ new ConnectionStateChangedAction({
connected: appInfo.deviceConnected, connected: appInfo.deviceConnected,
hasPermission: appInfo.hasPermission hasPermission: appInfo.hasPermission,
bootloaderActive: appInfo.bootloaderActive
}) })
]; ];
}); });

View File

@@ -24,6 +24,7 @@ import {
ActionTypes, ActionTypes,
ConnectionStateChangedAction, ConnectionStateChangedAction,
HideSaveToKeyboardButton, HideSaveToKeyboardButton,
RecoveryDeviceAction,
ResetUserConfigurationAction, ResetUserConfigurationAction,
RestoreUserConfigurationFromBackupSuccessAction, RestoreUserConfigurationFromBackupSuccessAction,
SaveConfigurationAction, SaveConfigurationAction,
@@ -60,6 +61,9 @@ export class DeviceEffects {
if (!state.hasPermission) { if (!state.hasPermission) {
this.router.navigate(['/privilege']); this.router.navigate(['/privilege']);
} }
else if (state.bootloaderActive) {
this.router.navigate(['/recovery-device']);
}
else if (state.connected) { else if (state.connected) {
this.router.navigate(['/']); this.router.navigate(['/']);
} }
@@ -90,7 +94,8 @@ export class DeviceEffects {
if (response.success) { if (response.success) {
return new ConnectionStateChangedAction({ return new ConnectionStateChangedAction({
connected: true, connected: true,
hasPermission: true hasPermission: true,
bootloaderActive: false
}); });
} }
@@ -214,6 +219,10 @@ export class DeviceEffects {
.ofType<ResetUserConfigurationAction>(ActionTypes.RESTORE_CONFIGURATION_FROM_BACKUP) .ofType<ResetUserConfigurationAction>(ActionTypes.RESTORE_CONFIGURATION_FROM_BACKUP)
.map(() => new SaveConfigurationAction()); .map(() => new SaveConfigurationAction());
@Effect({dispatch: false}) recoveryDevice$ = this.actions$
.ofType<RecoveryDeviceAction>(ActionTypes.RECOVERY_DEVICE)
.do(() => this.deviceRendererService.recoveryDevice());
constructor(private actions$: Actions, constructor(private actions$: Actions,
private router: Router, private router: Router,
private deviceRendererService: DeviceRendererService, private deviceRendererService: DeviceRendererService,

View File

@@ -79,6 +79,7 @@ export const flashFirmwareButtonDisbabled = createSelector(runningInElectron, de
export const getHardwareModules = createSelector(deviceState, fromDevice.getHardwareModules); export const getHardwareModules = createSelector(deviceState, fromDevice.getHardwareModules);
export const getBackupUserConfigurationState = createSelector(deviceState, fromDevice.getBackupUserConfigurationState); export const getBackupUserConfigurationState = createSelector(deviceState, fromDevice.getBackupUserConfigurationState);
export const getRestoreUserConfiguration = createSelector(deviceState, fromDevice.getHasBackupUserConfiguration); export const getRestoreUserConfiguration = createSelector(deviceState, fromDevice.getHasBackupUserConfiguration);
export const bootloaderActive = createSelector(deviceState, fromDevice.bootloaderActive);
export const getSideMenuPageState = createSelector( export const getSideMenuPageState = createSelector(
showAddonMenu, showAddonMenu,

View File

@@ -17,6 +17,7 @@ import { RestoreConfigurationState } from '../../models/restore-configuration-st
export interface State { export interface State {
connected: boolean; connected: boolean;
hasPermission: boolean; hasPermission: boolean;
bootloaderActive: boolean;
saveToKeyboard: ProgressButtonState; saveToKeyboard: ProgressButtonState;
updatingFirmware: boolean; updatingFirmware: boolean;
firmwareUpdateFinished: boolean; firmwareUpdateFinished: boolean;
@@ -29,6 +30,7 @@ export interface State {
export const initialState: State = { export const initialState: State = {
connected: true, connected: true,
hasPermission: true, hasPermission: true,
bootloaderActive: false,
saveToKeyboard: initProgressButtonState, saveToKeyboard: initProgressButtonState,
updatingFirmware: false, updatingFirmware: false,
firmwareUpdateFinished: false, firmwareUpdateFinished: false,
@@ -53,7 +55,8 @@ export function reducer(state = initialState, action: Action) {
return { return {
...state, ...state,
connected: data.connected, connected: data.connected,
hasPermission: data.hasPermission hasPermission: data.hasPermission,
bootloaderActive: data.bootloaderActive
}; };
} }
@@ -193,6 +196,13 @@ export function reducer(state = initialState, action: Action) {
hasBackupUserConfiguration: false hasBackupUserConfiguration: false
}; };
case ActionTypes.RECOVERY_DEVICE: {
return {
...state,
updatingFirmware: true,
log: [{message: '', cssClass: XtermCssClass.standard}]
};
}
default: default:
return state; return state;
} }
@@ -212,3 +222,4 @@ export const getBackupUserConfigurationState = (state: State): RestoreConfigurat
hasBackupUserConfiguration: state.hasBackupUserConfiguration hasBackupUserConfiguration: state.hasBackupUserConfiguration
}; };
}; };
export const bootloaderActive = (state: State) => state.bootloaderActive;

View File

@@ -155,3 +155,25 @@ pre {
} }
} }
} }
.flex-container {
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
}
.flex-grow {
background-color: black;
overflow: auto;
flex: 1;
}
.flex-footer {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.ok-button {
min-width: 100px;
}

View File

@@ -1,7 +1,5 @@
const util = require('util');
const HID = require('node-hid'); const HID = require('node-hid');
const {HardwareConfiguration, UhkBuffer} = require('uhk-common'); const {HardwareConfiguration, UhkBuffer} = require('uhk-common');
const {getTransferBuffers, ConfigBufferId, UhkHidDevice, UsbCommand} = require('uhk-usb');
const Logger = require('./logger'); const Logger = require('./logger');
const debug = process.env.DEBUG; const debug = process.env.DEBUG;
@@ -18,7 +16,7 @@ const kbootCommandIdToName = {
const eepromOperationIdToName = { const eepromOperationIdToName = {
0: 'read', 0: 'read',
1: 'write', 1: 'write',
} };
function bufferToString(buffer) { function bufferToString(buffer) {
let str = ''; let str = '';