feat: update firmware version after update (#649)

* feat: add clipboard copy icon to the x-term-component

* feat: start device poll after firmware upgrade

* feat: remove the OK button from the firmware upgrade page

* feat: read the firmware after firmware upgrade

* fix: scrolling of the x-term-component

* feat: refresh the firmware version after recovery device

* fix: remove the scrollbar styling

* fix: stay on device firmware upgrade screen
This commit is contained in:
Róbert Kiss
2018-05-21 10:57:34 +02:00
committed by László Monda
parent 6c4f580fc2
commit a6678bd537
17 changed files with 182 additions and 116 deletions

View File

@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import {
ConfigurationReply,
DeviceConnectionState,
FirmwareUpgradeIpcResponse,
getHardwareConfigFromDeviceResponse,
HardwareModules,
IpcEvents,
@@ -93,10 +94,7 @@ export class DeviceService {
try {
await this.device.waitUntilKeyboardBusy();
const result = await this.operations.loadConfigurations();
const modules: HardwareModules = {
leftModuleInfo: await this.operations.getLeftModuleVersionInfo(),
rightModuleInfo: await this.operations.getRightModuleVersionInfo()
};
const modules: HardwareModules = await this.getHardwareModules(false);
const hardwareConfig = getHardwareConfigFromDeviceResponse(result.hardwareConfiguration);
const uniqueId = hardwareConfig.uniqueId;
@@ -119,13 +117,32 @@ export class DeviceService {
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
}
public async getHardwareModules(catchError: boolean): Promise<HardwareModules> {
try {
await this.device.waitUntilKeyboardBusy();
return {
leftModuleInfo: await this.operations.getLeftModuleVersionInfo(),
rightModuleInfo: await this.operations.getRightModuleVersionInfo()
};
}
catch (err) {
if (!catchError) {
return err;
}
this.logService.error('[DeviceService] Read hardware modules information failed', err);
}
}
public close(): void {
this.stopPollTimer();
this.logService.info('[DeviceService] Device connection checker stopped.');
}
public async updateFirmware(event: Electron.Event, args?: Array<string>): Promise<void> {
const response = new IpcResponse();
const response = new FirmwareUpgradeIpcResponse();
let firmwarePathData: TmpFirmware;
try {
@@ -142,10 +159,12 @@ export class DeviceService {
}
response.success = true;
response.modules = await this.getHardwareModules(false);
} catch (error) {
const err = {message: error.message, stack: error.stack};
this.logService.error('[DeviceService] updateFirmware error', err);
response.modules = await this.getHardwareModules(true);
response.error = err;
}
@@ -154,11 +173,15 @@ export class DeviceService {
}
await snooze(500);
this.pollUhkDevice();
event.sender.send(IpcEvents.device.updateFirmwareReply, response);
}
public async recoveryDevice(event: Electron.Event): Promise<void> {
const response = new IpcResponse();
const response = new FirmwareUpgradeIpcResponse();
try {
this.stopPollTimer();
@@ -168,11 +191,13 @@ export class DeviceService {
this.pollUhkDevice();
response.modules = await this.getHardwareModules(false);
response.success = true;
} catch (error) {
const err = {message: error.message, stack: error.stack};
this.logService.error('[DeviceService] updateFirmware error', err);
response.modules = await this.getHardwareModules(true);
response.error = err;
}

View File

@@ -1,4 +1,10 @@
import { HardwareModules } from './hardware-modules';
export class IpcResponse {
success: boolean;
error?: { message: string };
}
export class FirmwareUpgradeIpcResponse extends IpcResponse {
modules?: HardwareModules;
}

View File

@@ -39,15 +39,10 @@
</p>
</div>
<div class="flex-grow" #scrollMe>
<div class="flex-grow">
<xterm [logs]="xtermLog$ | async"></xterm>
</div>
<div class="flex-footer">
<button type="button"
class="btn btn-primary ok-button"
[disabled]="firmwareOkButtonDisabled$ | async"
(click)="onOkButtonClick()">OK
</button>
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
@@ -6,13 +6,12 @@ import { HardwareModules, VersionInformation } from 'uhk-common';
import {
AppState,
firmwareOkButtonDisabled,
flashFirmwareButtonDisbabled,
getAgentVersionInfo,
getHardwareModules,
xtermLog
} from '../../../store';
import { UpdateFirmwareAction, UpdateFirmwareOkButtonAction, UpdateFirmwareWithAction } from '../../../store/actions/device';
import { UpdateFirmwareAction, UpdateFirmwareWithAction } from '../../../store/actions/device';
import { XtermLog } from '../../../models/xterm-log';
@Component({
@@ -26,33 +25,20 @@ import { XtermLog } from '../../../models/xterm-log';
export class DeviceFirmwareComponent implements OnDestroy {
flashFirmwareButtonDisbabled$: Observable<boolean>;
xtermLog$: Observable<Array<XtermLog>>;
xtermLogSubscription: Subscription;
getAgentVersionInfo$: Observable<VersionInformation>;
firmwareOkButtonDisabled$: Observable<boolean>;
hardwareModulesSubscription: Subscription;
hardwareModules: HardwareModules;
@ViewChild('scrollMe') divElement: ElementRef;
constructor(private store: Store<AppState>) {
this.flashFirmwareButtonDisbabled$ = store.select(flashFirmwareButtonDisbabled);
this.xtermLog$ = store.select(xtermLog);
this.xtermLogSubscription = this.xtermLog$.subscribe(() => {
if (this.divElement && this.divElement.nativeElement) {
setTimeout(() => {
this.divElement.nativeElement.scrollTop = this.divElement.nativeElement.scrollHeight;
});
}
});
this.getAgentVersionInfo$ = store.select(getAgentVersionInfo);
this.firmwareOkButtonDisabled$ = store.select(firmwareOkButtonDisabled);
this.hardwareModulesSubscription = store.select(getHardwareModules).subscribe(data => {
this.hardwareModules = data;
});
}
ngOnDestroy(): void {
this.xtermLogSubscription.unsubscribe();
this.hardwareModulesSubscription.unsubscribe();
}
@@ -60,10 +46,6 @@ export class DeviceFirmwareComponent implements OnDestroy {
this.store.dispatch(new UpdateFirmwareAction());
}
onOkButtonClick(): void {
this.store.dispatch(new UpdateFirmwareOkButtonAction());
}
changeFile(event): void {
const files = event.srcElement.files;

View File

@@ -17,8 +17,8 @@
</button>
</p>
</div>
<div class="flex-grow" #scrollMe>
<xterm [logs]="xtermLog"></xterm>
<div class="flex-grow">
<xterm [logs]="xtermLog$ | async"></xterm>
</div>
<div class="flex-footer">
</div>

View File

@@ -1,6 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Subscription } from 'rxjs/Subscription';
import { Observable } from 'rxjs/Observable';
import { XtermLog } from '../../../models/xterm-log';
@@ -16,38 +15,17 @@ import { RecoveryDeviceAction } from '../../../store/actions/device';
'class': 'container-fluid'
}
})
export class RecoveryModeComponent implements OnInit, OnDestroy {
xtermLogSubscription: Subscription;
export class RecoveryModeComponent implements OnInit {
flashFirmwareButtonDisbabled$: Observable<boolean>;
xtermLog: Array<XtermLog>;
xtermLog$: Observable<Array<XtermLog>>;
@ViewChild('scrollMe') divElement: ElementRef;
constructor(private store: Store<AppState>,
private cdRef: ChangeDetectorRef) {
constructor(private store: Store<AppState>) {
}
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();
}
this.xtermLog$ = this.store.select(xtermLog);
}
onRecoveryDevice(): void {

View File

@@ -1,5 +1,17 @@
<div class="wrapper">
<ul class="list-unstyled">
<li *ngFor="let log of logs" [ngClass]="log.cssClass"><span>{{ log.message }}</span></li>
</ul>
<div class="x-term-container">
<div class="x-term-wrapper" #scrollMe>
<ul class="list-unstyled">
<li *ngFor="let log of logs" [ngClass]="log.cssClass"><span>{{ log.message }}</span></li>
</ul>
</div>
<div class="copy-container-wrapper">
<div class="copy-container">
<span class="fa fa-2x fa-copy"
ngxClipboard
[cbContent]="getClipboardContent()"
title="Copy to clipboard"
data-toggle="tooltip"
data-placement="top"></span>
</div>
</div>
</div>

View File

@@ -1,9 +1,36 @@
$scrollbar-color: #ffffff;
$scrollbar-radius: 6px;
:host {
background-color: yellow;
display: flex;
flex-direction: column;
align-items: stretch;
width: 100%;
height: 100%;
}
.wrapper {
.x-term-container {
display: flex;
flex: 1;
flex-direction: column;
align-items: stretch;
position: relative;
}
.x-term-wrapper {
background-color: black;
overflow: auto;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.copy-container-wrapper {
position: absolute;
top: 2px;
right: 14px;
}
.xterm-standard {

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { XtermLog } from '../../models/xterm-log';
@Component({
@@ -7,6 +7,21 @@ import { XtermLog } from '../../models/xterm-log';
styleUrls: ['./xterm.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class XtermComponent {
export class XtermComponent implements OnChanges {
@Input() logs: Array<XtermLog> = [];
@ViewChild('scrollMe') divElement: ElementRef;
ngOnChanges(changes: SimpleChanges): void {
if (changes.logs && this.divElement && this.divElement.nativeElement) {
setTimeout(() => {
this.divElement.nativeElement.scrollTop = this.divElement.nativeElement.scrollHeight;
});
}
}
getClipboardContent(): string {
return this.logs.reduce((value, line) => value + line.message + '\n', '');
}
}

View File

@@ -0,0 +1,6 @@
import { HardwareModules } from 'uhk-common';
export interface FirmwareUpgradeError {
error: any;
modules?: HardwareModules;
}

View File

@@ -1,5 +1,6 @@
import { Action } from '@ngrx/store';
import { DeviceConnectionState, HardwareModules, IpcResponse, type } from 'uhk-common';
import { DeviceConnectionState, FirmwareUpgradeIpcResponse, HardwareModules, IpcResponse, type } from 'uhk-common';
import { FirmwareUpgradeError } from '../../models/firmware-upgrade-error';
const PREFIX = '[device] ';
@@ -96,25 +97,23 @@ export class UpdateFirmwareWithAction implements Action {
export class UpdateFirmwareReplyAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_REPLY;
constructor(public payload: IpcResponse) {
constructor(public payload: FirmwareUpgradeIpcResponse) {
}
}
export class UpdateFirmwareSuccessAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_SUCCESS;
constructor(public payload: HardwareModules) {
}
}
export class UpdateFirmwareFailedAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_FAILED;
constructor(public payload: any) {
constructor(public payload: FirmwareUpgradeError) {
}
}
export class UpdateFirmwareOkButtonAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_OK_BUTTON;
}
export class ResetMouseSpeedSettingsAction implements Action {
type = ActionTypes.RESET_MOUSE_SPEED_SETTINGS;
}
@@ -162,7 +161,6 @@ export type Actions
| UpdateFirmwareReplyAction
| UpdateFirmwareSuccessAction
| UpdateFirmwareFailedAction
| UpdateFirmwareOkButtonAction
| HardwareModulesLoadedAction
| RestoreUserConfigurationFromBackupAction
| HasBackupUserConfigurationAction

View File

@@ -14,7 +14,7 @@ import 'rxjs/add/operator/withLatestFrom';
import 'rxjs/add/operator/switchMap';
import {
DeviceConnectionState,
FirmwareUpgradeIpcResponse,
HardwareConfiguration,
IpcResponse,
NotificationType,
@@ -34,14 +34,13 @@ import {
SetPrivilegeOnLinuxReplyAction,
UpdateFirmwareAction,
UpdateFirmwareFailedAction,
UpdateFirmwareOkButtonAction,
UpdateFirmwareReplyAction,
UpdateFirmwareSuccessAction,
UpdateFirmwareWithAction
} from '../actions/device';
import { DeviceRendererService } from '../../services/device-renderer.service';
import { SetupPermissionErrorAction, ShowNotificationAction } from '../actions/app';
import { AppState } from '../index';
import { AppState, getRouterState } from '../index';
import {
ActionTypes as UserConfigActions,
ApplyUserConfigurationFromFileAction,
@@ -56,8 +55,14 @@ export class DeviceEffects {
@Effect()
deviceConnectionStateChange$: Observable<Action> = this.actions$
.ofType<ConnectionStateChangedAction>(ActionTypes.CONNECTION_STATE_CHANGED)
.map(action => action.payload)
.do((state: DeviceConnectionState) => {
.withLatestFrom(this.store.select(getRouterState))
.do(([action, route]) => {
const state = action.payload;
if (route.state && route.state.url.startsWith('/device/firmware')) {
return;
}
if (!state.hasPermission) {
this.router.navigate(['/privilege']);
}
@@ -71,7 +76,9 @@ export class DeviceEffects {
this.router.navigate(['/detection']);
}
})
.switchMap((state: DeviceConnectionState) => {
.switchMap(([action, route]) => {
const state = action.payload;
if (state.connected && state.hasPermission) {
return Observable.of(new LoadConfigFromDeviceAction());
}
@@ -203,18 +210,18 @@ export class DeviceEffects {
@Effect() updateFirmwareReply$ = this.actions$
.ofType<UpdateFirmwareReplyAction>(ActionTypes.UPDATE_FIRMWARE_REPLY)
.map(action => action.payload)
.switchMap((response: IpcResponse) => {
.switchMap((response: FirmwareUpgradeIpcResponse)
: Observable<UpdateFirmwareSuccessAction | UpdateFirmwareFailedAction> => {
if (response.success) {
return Observable.of(new UpdateFirmwareSuccessAction());
return Observable.of(new UpdateFirmwareSuccessAction(response.modules));
}
return Observable.of(new UpdateFirmwareFailedAction(response.error));
return Observable.of(new UpdateFirmwareFailedAction({
error: response.error,
modules: response.modules
}));
});
@Effect({dispatch: false}) updateFirmwareOkButton$ = this.actions$
.ofType<UpdateFirmwareOkButtonAction>(ActionTypes.UPDATE_FIRMWARE_OK_BUTTON)
.do(() => this.deviceRendererService.startConnectionPoller());
@Effect() restoreUserConfiguration$ = this.actions$
.ofType<ResetUserConfigurationAction>(ActionTypes.RESTORE_CONFIGURATION_FROM_BACKUP)
.map(() => new SaveConfigurationAction());

View File

@@ -36,7 +36,7 @@ import {
import { DataStorageRepositoryService } from '../../services/datastorage-repository.service';
import { DefaultUserConfigurationService } from '../../services/default-user-configuration.service';
import { AppState, getPrevUserConfiguration, getUserConfiguration } from '../index';
import { AppState, getPrevUserConfiguration, getRouterState, getUserConfiguration } from '../index';
import { KeymapAction, KeymapActions, MacroAction, MacroActions } from '../actions';
import {
DismissUndoNotificationAction,
@@ -118,8 +118,10 @@ export class UserConfigEffects {
@Effect() loadConfigFromDeviceReply$ = this.actions$
.ofType<LoadConfigFromDeviceReplyAction>(ActionTypes.LOAD_CONFIG_FROM_DEVICE_REPLY)
.map(action => action.payload)
.mergeMap((data: ConfigurationReply): any => {
.withLatestFrom(this.store.select(getRouterState))
.mergeMap(([action, route]): any => {
const data: ConfigurationReply = action.payload;
if (!data.success) {
return [new ShowNotificationAction({
type: NotificationType.Error,
@@ -128,12 +130,16 @@ export class UserConfigEffects {
}
const result = [];
let newPageDestination = ['/'];
let newPageDestination: Array<string>;
try {
const userConfig = getUserConfigFromDeviceResponse(data.userConfiguration);
result.push(new LoadUserConfigSuccessAction(userConfig));
if (route.state && !route.state.url.startsWith('/device/firmware')) {
newPageDestination = ['/'];
}
} catch (err) {
this.logService.error('Eeprom user-config parse error:', err);
const userConfig = new UserConfiguration().fromJsonObject(data.backupConfiguration);
@@ -158,7 +164,9 @@ export class UserConfigEffects {
result.push(new HardwareModulesLoadedAction(data.modules));
this.router.navigate(newPageDestination);
if (newPageDestination) {
this.router.navigate(newPageDestination);
}
return result;
});

View File

@@ -1,6 +1,6 @@
import { createSelector } from 'reselect';
import { MetaReducer } from '@ngrx/store';
import { RouterReducerState } from '@ngrx/router-store';
import { ActionReducerMap, MetaReducer } from '@ngrx/store';
import { RouterReducerState, routerReducer } from '@ngrx/router-store';
import { storeFreeze } from 'ngrx-store-freeze';
import { Keymap, UserConfiguration } from 'uhk-common';
@@ -14,15 +14,6 @@ import { initProgressButtonState } from './reducers/progress-button-state';
import { environment } from '../../environments/environment';
import { RouterStateUrl } from './router-util';
export const reducers = {
userConfiguration: fromUserConfig.reducer,
presetKeymaps: fromPreset.reducer,
autoUpdateSettings: autoUpdateSettings.reducer,
app: fromApp.reducer,
appUpdate: fromAppUpdate.reducer,
device: fromDevice.reducer
};
// State interface for the application
export interface AppState {
userConfiguration: UserConfiguration;
@@ -34,6 +25,16 @@ export interface AppState {
device: fromDevice.State;
}
export const reducers: ActionReducerMap<AppState> = {
userConfiguration: fromUserConfig.reducer,
presetKeymaps: fromPreset.reducer,
autoUpdateSettings: autoUpdateSettings.reducer,
app: fromApp.reducer,
router: routerReducer,
appUpdate: fromAppUpdate.reducer,
device: fromDevice.reducer
};
export const metaReducers: MetaReducer<AppState>[] = environment.production
? []
: [storeFreeze];
@@ -73,7 +74,6 @@ export const saveToKeyboardState = createSelector(runningInElectron, saveToKeybo
});
export const updatingFirmware = createSelector(deviceState, fromDevice.updatingFirmware);
export const xtermLog = createSelector(deviceState, fromDevice.xtermLog);
export const firmwareOkButtonDisabled = createSelector(deviceState, fromDevice.firmwareOkButtonDisabled);
// tslint:disable-next-line: max-line-length
export const flashFirmwareButtonDisbabled = createSelector(runningInElectron, deviceState, (electron, state: fromDevice.State) => !electron || state.updatingFirmware);
export const getHardwareModules = createSelector(deviceState, fromDevice.getHardwareModules);
@@ -103,3 +103,5 @@ export const getSideMenuPageState = createSelector(
};
}
);
export const getRouterState = (state: AppState) => state.router;

View File

@@ -7,7 +7,8 @@ import {
HardwareModulesLoadedAction,
SaveConfigurationAction,
HasBackupUserConfigurationAction,
UpdateFirmwareFailedAction
UpdateFirmwareFailedAction,
UpdateFirmwareSuccessAction
} from '../actions/device';
import { ActionTypes as AppActions, ElectronMainLogReceivedAction } from '../actions/app';
import { initProgressButtonState, ProgressButtonState } from './progress-button-state';
@@ -19,6 +20,7 @@ export interface State {
hasPermission: boolean;
bootloaderActive: boolean;
saveToKeyboard: ProgressButtonState;
savingToKeyboard: boolean;
updatingFirmware: boolean;
firmwareUpdateFinished: boolean;
modules: HardwareModules;
@@ -32,6 +34,7 @@ export const initialState: State = {
hasPermission: true,
bootloaderActive: false,
saveToKeyboard: initProgressButtonState,
savingToKeyboard: false,
updatingFirmware: false,
firmwareUpdateFinished: false,
modules: {
@@ -48,7 +51,7 @@ export const initialState: State = {
hasBackupUserConfiguration: false
};
export function reducer(state = initialState, action: Action) {
export function reducer(state = initialState, action: Action): State {
switch (action.type) {
case ActionTypes.CONNECTION_STATE_CHANGED: {
const data = (<ConnectionStateChangedAction>action).payload;
@@ -132,12 +135,14 @@ export function reducer(state = initialState, action: Action) {
return {
...state,
updatingFirmware: false,
firmwareUpdateFinished: true
firmwareUpdateFinished: true,
modules: (action as UpdateFirmwareSuccessAction).payload
};
case ActionTypes.UPDATE_FIRMWARE_FAILED: {
const data = (action as UpdateFirmwareFailedAction).payload;
const logEntry = {
message: (action as UpdateFirmwareFailedAction).payload.message,
message: data.error.message,
cssClass: XtermCssClass.error
};
@@ -145,6 +150,7 @@ export function reducer(state = initialState, action: Action) {
...state,
updatingFirmware: false,
firmwareUpdateFinished: true,
modules: data.modules,
log: [...state.log, logEntry]
};
}
@@ -213,7 +219,6 @@ export const isDeviceConnected = (state: State) => state.connected || state.upda
export const hasDevicePermission = (state: State) => state.hasPermission;
export const getSaveToKeyboardState = (state: State) => state.saveToKeyboard;
export const xtermLog = (state: State) => state.log;
export const firmwareOkButtonDisabled = (state: State) => !state.firmwareUpdateFinished;
export const getHardwareModules = (state: State) => state.modules;
export const getHasBackupUserConfiguration = (state: State) => state.hasBackupUserConfiguration;
export const getBackupUserConfigurationState = (state: State): RestoreConfigurationState => {

View File

@@ -3,7 +3,7 @@ import { Action } from '@ngrx/store';
export interface ProgressButtonState {
showButton: boolean;
text: string;
showProgress: boolean;
showProgress?: boolean;
action?: Action;
}

View File

@@ -158,14 +158,14 @@ pre {
.flex-container {
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
}
.flex-grow {
background-color: black;
overflow: auto;
display: flex;
flex-direction: column;
align-items: stretch;
flex: 1;
}