31 Commits

Author SHA1 Message Date
László Monda
8bb645125d Update changelog and re-release v1.2.2 2018-05-27 02:53:10 +02:00
László Monda
9471b31a5d Instruct the user to power cycle the keyboard when encountered with an invalid hardware configuration. 2018-05-27 02:43:12 +02:00
László Monda
ffa52757c9 Upgrade to firmware 8.2.5 2018-05-27 02:31:22 +02:00
Róbert Kiss
ee53a0df9b feat: set disable style of device name in side menu (#656)
* feat: visualise disabled state of the 'Device name' input control

* fix: original value handling
2018-05-27 02:30:41 +02:00
Róbert Kiss
8e20c85e07 feat: add file upload component (#655)
The component allow upload the same file multiple times
2018-05-26 22:46:41 +02:00
László Monda
65ea786358 Don't include os.platform into the firmware update log because os.type makes it practically redundant. 2018-05-26 17:13:10 +02:00
László Monda
1035837b3b Fix lint error. 2018-05-22 15:29:48 +02:00
Róbert Kiss
18fc2e6b3f fix: permission detection when device not plugged (#653) 2018-05-22 15:11:43 +02:00
László Monda
fc728697d7 Bump version to 1.2.2 and update changelog and package.json 2018-05-22 02:19:48 +02:00
László Monda
cdf3caee9e Display device list at the beginning of the firmware update process. 2018-05-22 01:53:14 +02:00
László Monda
0a4d3a002e Include operating system type to the firmware update log. Add a new tip to the firmware page. 2018-05-22 01:16:02 +02:00
László Monda
d11c532ea4 Add a lot of useful instructions to the firmware page. 2018-05-22 00:38:27 +02:00
László Monda
1ff51697b1 Upgrade from firmware 8.2.2 to 8.2.4 2018-05-21 14:34:42 +02:00
Róbert Kiss
ab8ae31324 fix: permission detection on linux (#651) 2018-05-21 11:46:19 +02:00
László Monda
daa0e723b1 Display more detailed "invalid hardware configuration" errors. 2018-05-21 11:03:05 +02:00
Róbert Kiss
609aba856a fix: permission detection on win and mac (#650) 2018-05-21 11:00:57 +02:00
Róbert Kiss
a6678bd537 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
2018-05-21 10:57:34 +02:00
László Monda
6c4f580fc2 Check if the keyboard is in factory reset mode and if so, display a relevant instruction. 2018-05-20 01:40:45 +02:00
László Monda
ea41661c65 Add erase-user-config.js 2018-05-19 21:53:41 +02:00
László Monda
c553c7b63b Rename erase-hca.js to erase-hardware-config.js 2018-05-19 21:33:12 +02:00
László Monda
e5988aa800 Rename write-hca.js to write-hardware-config.js 2018-05-19 21:32:15 +02:00
László Monda
ae319c607f Rename write-userconfig.js to write-user-config.js 2018-05-19 21:14:48 +02:00
László Monda
5d23ad1c9e Fix write-hca.js and write-user.js, and remove write-config.js. Fixes #627. 2018-05-19 20:59:11 +02:00
László Monda
55eef50da7 Add erase-hca.js 2018-05-19 20:05:41 +02:00
Róbert Kiss
653465f0e0 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
2018-05-19 17:22:46 +02:00
László Monda
2cf8044987 Check the signature of the hardware configuration area instead of uniqueId. 2018-05-19 14:07:18 +02:00
László Monda
3c056a7255 Display "Invalid hardware configuration" when the hardware configuration area is uninitialized instead of a general error message. Improves #623. 2018-05-19 13:38:16 +02:00
Róbert Kiss
091796d13c feat: not allow non ascii character in macro action text (#645) 2018-05-17 23:56:36 +02:00
László Monda
eb97dd844f Make the bootloader timeout of the reenumerate script specifiable. 2018-05-16 23:19:36 +02:00
Róbert Kiss
17693ec8fe build: upgrade node.js => 8.11.2 (#641) 2018-05-16 22:08:11 +02:00
Róbert Kiss
7c7ce8f50f feat: only send auto update notification when user indicated the update (#640)
* feat: only send auto update notification when user indicated the update

* fix: add more logging to the auto update process
2018-05-16 22:05:13 +02:00
64 changed files with 835 additions and 310 deletions

2
.nvmrc
View File

@@ -1 +1 @@
8.9.4 8.11.2

View File

@@ -6,9 +6,24 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1
Every Agent version includes the most recent firmware version. See the [firmware changelog](https://github.com/UltimateHackingKeyboard/firmware/blob/master/CHANGELOG.md). Every Agent version includes the most recent firmware version. See the [firmware changelog](https://github.com/UltimateHackingKeyboard/firmware/blob/master/CHANGELOG.md).
## [1.2.2] - 2018-05-27
Firmware: 8.2.**5** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.3.**1** | User Config: 4.0.**1** | Hardware Config: 1.0.0
- Offer recovery for bricked right keyboard halfs.
- Detect when the hardware configuration of a device is invalid and display a notification.
- Check if the keyboard is in factory reset mode and if so, display a relevant instruction.
- Only allow ASCII characters in type text macro actions.
- Allow uploading the same file multiple times in a row.
- Only send auto update notification when the user initiates the update.
- Update the firmware versions on the firmware update page right after firmware updates.
- Add a lot of useful instructions to the firmware page to help users update the firmware.
- Add the operating system and initial device list to the firmware update log.
- Add copy to clipboard button to the top right corner of the firmware update terminal widget.
## [1.2.1] - 2018-05-12 ## [1.2.1] - 2018-05-12
Firmware: 8.2.**2** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.2)] | Device Protocol: 4.3.0| User Config: 4.0.**1** | Hardware Config: 1.0.0 Firmware: 8.2.**2** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.2)] | Device Protocol: 4.3.0 | User Config: 4.0.**1** | Hardware Config: 1.0.0
- Match for the new USB usage page and usage number. This is critical for UHKs flashed with firmware >=8.2.2 to be recognized by Agent on OSX. - Match for the new USB usage page and usage number. This is critical for UHKs flashed with firmware >=8.2.2 to be recognized by Agent on OSX.
- Make the config serializer handle long media macro actions. `USERCONFIG:PATCH` - Make the config serializer handle long media macro actions. `USERCONFIG:PATCH`

5
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "uhk-agent", "name": "uhk-agent",
"version": "1.2.0", "version": "1.2.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@@ -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

@@ -3,9 +3,9 @@
"private": true, "private": true,
"author": "Ultimate Gadget Laboratories", "author": "Ultimate Gadget Laboratories",
"main": "electron/dist/electron-main.js", "main": "electron/dist/electron-main.js",
"version": "1.2.1", "version": "1.2.2",
"firmwareVersion": "8.2.2", "firmwareVersion": "8.2.5",
"deviceProtocolVersion": "4.3.0", "deviceProtocolVersion": "4.3.1",
"userConfigVersion": "4.0.1", "userConfigVersion": "4.0.1",
"hardwareConfigVersion": "1.0.0", "hardwareConfigVersion": "1.0.0",
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.", "description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",

View File

@@ -9,6 +9,9 @@ import { IpcEvents, LogService } from 'uhk-common';
import { MainServiceBase } from './main-service-base'; import { MainServiceBase } from './main-service-base';
export class AppUpdateService extends MainServiceBase { export class AppUpdateService extends MainServiceBase {
private sendAutoUpdateNotification = false;
constructor(protected logService: LogService, constructor(protected logService: LogService,
protected win: Electron.BrowserWindow, protected win: Electron.BrowserWindow,
private app: Electron.App) { private app: Electron.App) {
@@ -24,16 +27,21 @@ export class AppUpdateService extends MainServiceBase {
private initListeners() { private initListeners() {
autoUpdater.on('checking-for-update', () => { autoUpdater.on('checking-for-update', () => {
this.logService.debug('[AppUpdateService] checking for update');
this.sendIpcToWindow(IpcEvents.autoUpdater.checkingForUpdate); this.sendIpcToWindow(IpcEvents.autoUpdater.checkingForUpdate);
}); });
autoUpdater.on('update-available', async (ev: any, info: UpdateInfo) => { autoUpdater.on('update-available', async (ev: any, info: UpdateInfo) => {
this.logService.debug('[AppUpdateService] update available. Downloading started');
await autoUpdater.downloadUpdate(); await autoUpdater.downloadUpdate();
this.sendIpcToWindow(IpcEvents.autoUpdater.updateAvailable, info); this.sendIpcToWindow(IpcEvents.autoUpdater.updateAvailable, info);
}); });
autoUpdater.on('update-not-available', (ev: any, info: UpdateInfo) => { autoUpdater.on('update-not-available', (ev: any, info: UpdateInfo) => {
this.sendIpcToWindow(IpcEvents.autoUpdater.updateNotAvailable, info); if (this.sendAutoUpdateNotification) {
this.logService.debug('[AppUpdateService] update not available');
this.sendIpcToWindow(IpcEvents.autoUpdater.updateNotAvailable, info);
}
}); });
autoUpdater.on('error', (ev: any, err: string) => { autoUpdater.on('error', (ev: any, err: string) => {
@@ -51,6 +59,7 @@ export class AppUpdateService extends MainServiceBase {
}); });
autoUpdater.on('update-downloaded', (ev: any, info: UpdateInfo) => { autoUpdater.on('update-downloaded', (ev: any, info: UpdateInfo) => {
this.logService.debug('[AppUpdateService] update downloaded');
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloaded, info); this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloaded, info);
}); });
@@ -61,12 +70,15 @@ export class AppUpdateService extends MainServiceBase {
ipcMain.on(IpcEvents.app.appStarted, () => { ipcMain.on(IpcEvents.app.appStarted, () => {
if (this.checkForUpdateAtStartup()) { if (this.checkForUpdateAtStartup()) {
this.sendAutoUpdateNotification = false;
this.logService.debug('[AppUpdateService] app started. Automatically check for update.');
this.checkForUpdate(); this.checkForUpdate();
} }
}); });
ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => { ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => {
this.logService.debug('[AppUpdateService] checkForUpdate request from renderer process'); this.logService.debug('[AppUpdateService] checkForUpdate request from renderer process');
this.sendAutoUpdateNotification = true;
this.checkForUpdate(); this.checkForUpdate();
}); });
} }
@@ -75,14 +87,22 @@ export class AppUpdateService extends MainServiceBase {
if (isDev) { if (isDev) {
const msg = '[AppUpdateService] Application update is not working in dev mode.'; const msg = '[AppUpdateService] Application update is not working in dev mode.';
this.logService.info(msg); this.logService.info(msg);
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
if (this.sendAutoUpdateNotification) {
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
}
return; return;
} }
if (this.isFirstRun()) { if (this.isFirstRun()) {
const msg = '[AppUpdateService] Application update is skipping at first run.'; const msg = '[AppUpdateService] Application update is skipping at first run.';
this.logService.info(msg); this.logService.info(msg);
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
if (this.sendAutoUpdateNotification) {
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
}
return; return;
} }

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

@@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import { import {
ConfigurationReply, ConfigurationReply,
DeviceConnectionState, DeviceConnectionState,
FirmwareUpgradeIpcResponse,
getHardwareConfigFromDeviceResponse, getHardwareConfigFromDeviceResponse,
HardwareModules, HardwareModules,
IpcEvents, IpcEvents,
@@ -10,7 +11,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 +72,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');
} }
@@ -84,10 +94,7 @@ export class DeviceService {
try { try {
await this.device.waitUntilKeyboardBusy(); await this.device.waitUntilKeyboardBusy();
const result = await this.operations.loadConfigurations(); const result = await this.operations.loadConfigurations();
const modules: HardwareModules = { const modules: HardwareModules = await this.getHardwareModules(false);
leftModuleInfo: await this.operations.getLeftModuleVersionInfo(),
rightModuleInfo: await this.operations.getRightModuleVersionInfo()
};
const hardwareConfig = getHardwareConfigFromDeviceResponse(result.hardwareConfiguration); const hardwareConfig = getHardwareConfigFromDeviceResponse(result.hardwareConfiguration);
const uniqueId = hardwareConfig.uniqueId; const uniqueId = hardwareConfig.uniqueId;
@@ -110,16 +117,36 @@ export class DeviceService {
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response)); 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 { public close(): void {
this.stopPollTimer(); this.stopPollTimer();
this.logService.info('[DeviceService] Device connection checker stopped.'); this.logService.info('[DeviceService] Device connection checker stopped.');
} }
public async updateFirmware(event: Electron.Event, args?: Array<string>): Promise<void> { public async updateFirmware(event: Electron.Event, args?: Array<string>): Promise<void> {
const response = new IpcResponse(); const response = new FirmwareUpgradeIpcResponse();
let firmwarePathData: TmpFirmware; let firmwarePathData: TmpFirmware;
try { try {
this.device.resetDeviceCache();
this.stopPollTimer(); this.stopPollTimer();
if (args && args.length > 0) { if (args && args.length > 0) {
@@ -133,10 +160,12 @@ export class DeviceService {
} }
response.success = true; response.success = true;
response.modules = await this.getHardwareModules(false);
} catch (error) { } catch (error) {
const err = {message: error.message, stack: error.stack}; const err = {message: error.message, stack: error.stack};
this.logService.error('[DeviceService] updateFirmware error', err); this.logService.error('[DeviceService] updateFirmware error', err);
response.modules = await this.getHardwareModules(true);
response.error = err; response.error = err;
} }
@@ -144,6 +173,35 @@ export class DeviceService {
await emptyDir(firmwarePathData.tmpDirectory.name); await emptyDir(firmwarePathData.tmpDirectory.name);
} }
await snooze(500);
this.pollUhkDevice();
event.sender.send(IpcEvents.device.updateFirmwareReply, response);
}
public async recoveryDevice(event: Electron.Event): Promise<void> {
const response = new FirmwareUpgradeIpcResponse();
try {
this.stopPollTimer();
await this.operations.updateRightFirmware();
await snooze(500);
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;
}
await snooze(500); await snooze(500);
event.sender.send(IpcEvents.device.updateFirmwareReply, response); event.sender.send(IpcEvents.device.updateFirmwareReply, response);
} }
@@ -161,16 +219,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

@@ -41,16 +41,20 @@ export class HardwareConfiguration {
} }
fromBinary(buffer: UhkBuffer): HardwareConfiguration { fromBinary(buffer: UhkBuffer): HardwareConfiguration {
this.signature = buffer.readString(); try {
this.majorVersion = buffer.readUInt8(); this.signature = buffer.readString();
this.minorVersion = buffer.readUInt8(); this.majorVersion = buffer.readUInt8();
this.patchVersion = buffer.readUInt8(); this.minorVersion = buffer.readUInt8();
this.brandId = buffer.readUInt8(); this.patchVersion = buffer.readUInt8();
this.deviceId = buffer.readUInt8(); this.brandId = buffer.readUInt8();
this.uniqueId = buffer.readUInt32(); this.deviceId = buffer.readUInt8();
this.isVendorModeOn = buffer.readBoolean(); this.uniqueId = buffer.readUInt32();
this.isIso = buffer.readBoolean(); this.isVendorModeOn = buffer.readBoolean();
return this; this.isIso = buffer.readBoolean();
return this;
} catch (e) {
throw new Error('Please power cycle your keyboard (Invalid hardware configuration: Index out of bounds)');
}
} }
toJsonObject(): any { toJsonObject(): any {

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

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

View File

@@ -1,3 +1,4 @@
export namespace Constants { export namespace Constants {
export const AGENT_GITHUB_URL = 'https://github.com/UltimateHackingKeyboard/agent'; export const AGENT_GITHUB_URL = 'https://github.com/UltimateHackingKeyboard/agent';
export const FIRMWARE_GITHUB_ISSUE_URL = 'https://github.com/UltimateHackingKeyboard/agent/issues/567';
} }

View File

@@ -5,10 +5,15 @@ export const getHardwareConfigFromDeviceResponse = (json: string): HardwareConfi
const hardwareConfig = new HardwareConfiguration(); const hardwareConfig = new HardwareConfiguration();
hardwareConfig.fromBinary(UhkBuffer.fromArray(data)); hardwareConfig.fromBinary(UhkBuffer.fromArray(data));
if (hardwareConfig.uniqueId > 0) { if (hardwareConfig.signature === 'FTY') {
return hardwareConfig; throw Error('The device is in factory reset mode. Power-cycle the device to use it with Agent!');
} }
return null;
if (hardwareConfig.signature !== 'UHK') {
throw Error('Please power cycle your keyboard (Invalid hardware configuration: Invalid signature)');
}
return hardwareConfig;
}; };
export const getUserConfigFromDeviceResponse = (json: string): UserConfiguration => { export const getUserConfigFromDeviceResponse = (json: string): UserConfiguration => {

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,14 +1,11 @@
{ {
"name": "uhk-usb",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true, "requires": true,
"lockfileVersion": 1,
"dependencies": { "dependencies": {
"@types/node": { "@types/node": {
"version": "8.0.28", "version": "8.0.28",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.28.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.28.tgz",
"integrity": "sha512-HupkFXEv3O3KSzcr3Ylfajg0kaerBg1DyaZzRBBQfrU3NN1mTBRE7sCveqHwXLS5Yrjvww8qFzkzYQQakG9FuQ==", "integrity": "sha512-HupkFXEv3O3KSzcr3Ylfajg0kaerBg1DyaZzRBBQfrU3NN1mTBRE7sCveqHwXLS5Yrjvww8qFzkzYQQakG9FuQ=="
"dev": true
}, },
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",

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-es'; import { 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,
@@ -13,7 +13,7 @@ import {
ModuleSlotToId, ModuleSlotToId,
UsbCommand UsbCommand
} from './constants'; } from './constants';
import { bufferToString, getTransferData, retry, snooze } from './util'; import { bufferToString, getTransferData, isUhkDevice, retry, snooze } from './util';
export const BOOTLOADER_TIMEOUT_MS = 5000; export const BOOTLOADER_TIMEOUT_MS = 5000;
@@ -50,12 +50,16 @@ export class UhkHidDevice {
return true; return true;
} }
if (!this.deviceConnected()) { const dev = devices().find((x: Device) => isUhkDevice(x) || x.productId === Constants.BOOTLOADER_ID);
if (!dev) {
return true; return true;
} }
this._hasPermission = this.getDevice() !== null; const device = new HID(dev.path);
this.close(); device.close();
this._hasPermission = true;
return this._hasPermission; return this._hasPermission;
} catch (err) { } catch (err) {
@@ -69,15 +73,24 @@ 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 (isUhkDevice(dev)) {
result.connected = true;
} else if (dev.vendorId === Constants.VENDOR_ID &&
dev.productId === Constants.BOOTLOADER_ID) {
result.bootloaderActive = true;
}
} }
return connected; return result;
} }
/** /**
@@ -144,6 +157,10 @@ export class UhkHidDevice {
} }
} }
public resetDeviceCache(): void {
this._prevDevices = {};
}
async reenumerate(enumerationMode: EnumerationModes): Promise<void> { async reenumerate(enumerationMode: EnumerationModes): Promise<void> {
const reenumMode = EnumerationModes[enumerationMode].toString(); const reenumMode = EnumerationModes[enumerationMode].toString();
this.logService.debug(`[UhkHidDevice] Start reenumeration, mode: ${reenumMode}`); this.logService.debug(`[UhkHidDevice] Start reenumeration, mode: ${reenumMode}`);
@@ -242,13 +259,7 @@ export class UhkHidDevice {
this.logService.debug('[UhkHidDevice] Available devices unchanged'); this.logService.debug('[UhkHidDevice] Available devices unchanged');
} }
const dev = devs.find((x: Device) => const dev = devs.find(isUhkDevice);
x.vendorId === Constants.VENDOR_ID &&
x.productId === Constants.PRODUCT_ID &&
// hidapi can not read the interface number on Mac, so check the usage page and usage
((x.usagePage === 128 && x.usage === 129) || // Old firmware
(x.usagePage === (0xFF00 | 0x00) && x.usage === 0x01) || // New firmware
x.interface === 0));
if (!dev) { if (!dev) {
this.logService.debug('[UhkHidDevice] UHK Device not found:'); this.logService.debug('[UhkHidDevice] UHK Device not found:');

View File

@@ -9,6 +9,7 @@ import {
} from './constants'; } from './constants';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os';
import { UhkBlhost } from './uhk-blhost'; import { UhkBlhost } from './uhk-blhost';
import { UhkHidDevice } from './uhk-hid-device'; import { UhkHidDevice } from './uhk-hid-device';
import { snooze } from './util'; import { snooze } from './util';
@@ -29,6 +30,7 @@ export class UhkOperations {
} }
public async updateRightFirmware(firmwarePath = this.getFirmwarePath()) { public async updateRightFirmware(firmwarePath = this.getFirmwarePath()) {
this.logService.debug(`[UhkOperations] Operating system: ${os.type()} ${os.release()} ${os.arch()}`);
this.logService.debug('[UhkOperations] Start flashing right firmware'); this.logService.debug('[UhkOperations] Start flashing right firmware');
const prefix = [`--usb 0x1d50,0x${EnumerationNameToProductId.bootloader.toString(16)}`]; const prefix = [`--usb 0x1d50,0x${EnumerationNameToProductId.bootloader.toString(16)}`];

View File

@@ -1,5 +1,7 @@
import { Device } from 'node-hid';
import { DeviceConnectionState, LogService } from 'uhk-common';
import { Constants, UsbCommand } from './constants'; import { Constants, UsbCommand } from './constants';
import { 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 +97,18 @@ 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;
};
export const isUhkDevice = (dev: Device): boolean => {
return dev.vendorId === Constants.VENDOR_ID &&
dev.productId === Constants.PRODUCT_ID &&
// hidapi can not read the interface number on Mac, so check the usage page and usage
((dev.usagePage === 128 && dev.usage === 129) || // Old firmware
(dev.usagePage === (0xFF00 | 0x00) && dev.usage === 0x01) || // New firmware
dev.interface === 0);
};

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

@@ -0,0 +1,10 @@
<input #inputControl
cancelable
[class]="css"
type="text"
[disabled]="disabled"
[(ngModel)]="model"
(blur)="blur()"
(focus)="focus()"
(keyup.enter)="keyEnter($event)"
(keyup)="calculateTextWidth($event.target.value)">

View File

@@ -0,0 +1,118 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef, HostListener,
Input,
Renderer2,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import * as util from '../../util';
const noop = (_: any) => {
};
@Component({
selector: 'auto-grow-input',
templateUrl: './auto-grow-input.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutoGrowInputComponent),
multi: true
}
]
})
export class AutoGrowInputComponent implements ControlValueAccessor {
@Input() maxParentWidthPercent = 1;
@Input() css: string;
@ViewChild('inputControl') inputControl: ElementRef;
disabled: boolean;
get model(): string {
return this._model;
}
set model(value: string) {
if (this._model === value) {
return;
}
this._model = value;
}
private _model: string;
private _originalModel: string;
private _onChanged = noop;
private _onTouched = noop;
constructor(private _cdRef: ChangeDetectorRef,
private _renderer: Renderer2) {
}
registerOnChange(fn: any): void {
this._onChanged = fn;
}
registerOnTouched(fn: any): void {
this._onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
if (this.disabled === isDisabled) {
return;
}
this.disabled = isDisabled;
this._cdRef.markForCheck();
}
@HostListener('window:resize')
windowResize(): void {
this.calculateTextWidth(this._model);
}
writeValue(obj: any): void {
console.log('write', new Date());
if (this.model === obj) {
return;
}
this._model = obj;
this._originalModel = obj;
this.calculateTextWidth(this._model);
this._cdRef.markForCheck();
}
focus(): void {
this._onTouched(this);
}
blur(): void {
if (!util.isValidName(this._model) || this._model.trim() === this._originalModel) {
this._model = this._originalModel;
this.calculateTextWidth(this._model);
this._cdRef.markForCheck();
return;
}
this._originalModel = this._model;
this._onChanged(this._model);
}
keyEnter(event): void {
event.target.blur();
}
calculateTextWidth(text: string): void {
const htmlInput = this.inputControl.nativeElement as HTMLInputElement;
const maxWidth = htmlInput.parentElement.parentElement.offsetWidth * this.maxParentWidthPercent;
const textWidth = util.getContentWidth(window.getComputedStyle(htmlInput), text);
this._renderer.setStyle(htmlInput, 'width', Math.min(maxWidth, textWidth) + 'px');
}
}

View File

@@ -0,0 +1 @@
export * from './auto-grow-input.component';

View File

@@ -14,11 +14,9 @@
</button> </button>
</li> </li>
<li> <li>
<label class="btn btn-default btn-file"> <file-upload (fileChanged)="changeFile($event)"
Import device configuration label="Import device configuration">
<input type="file" </file-upload>
(change)="changeFile($event)">
</label>
</li> </li>
<li> <li>
<button class="btn btn-danger" <button class="btn btn-danger"

View File

@@ -8,6 +8,7 @@ import {
SaveUserConfigInBinaryFileAction, SaveUserConfigInBinaryFileAction,
SaveUserConfigInJsonFileAction SaveUserConfigInJsonFileAction
} from '../../../store/actions/user-config'; } from '../../../store/actions/user-config';
import { UploadFileData } from '../../../models/upload-file-data';
@Component({ @Component({
selector: 'device-settings', selector: 'device-settings',
@@ -42,16 +43,7 @@ export class DeviceConfigurationComponent {
} }
} }
changeFile(event): void { changeFile(data: UploadFileData): void {
const files = event.srcElement.files; this.store.dispatch(new LoadUserConfigurationFromFileAction(data));
const fileReader = new FileReader();
fileReader.onloadend = function () {
const arrayBuffer = new Uint8Array(fileReader.result);
this.store.dispatch(new LoadUserConfigurationFromFileAction({
filename: event.srcElement.value,
data: Array.from(arrayBuffer)
}));
}.bind(this);
fileReader.readAsArrayBuffer(files[0]);
} }
} }

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

@@ -12,14 +12,17 @@
Firmware {{ hardwareModules.rightModuleInfo.firmwareVersion }} is running on the right keyboard half. Firmware {{ hardwareModules.rightModuleInfo.firmwareVersion }} is running on the right keyboard half.
</p> </p>
<p> If the update process fails, consider the following points:
<i> <ol>
Please note that the firmware update process may sometimes fail. If if fails then <li>Windows 7, Windows Vista, and Windows XP are not supported. Use Linux, OSX, Windows 10, or Windows 8.</li>
simply retry until it succeeds. If the left half becomes unresponsive after a failed <li>Connect your UHK directly to the host computer. Don't use USB hubs or KVM switches.</li>
update then retry and follow the instructions displayed during the update to fix it. <li>Run Agent directly on the host operating system. Don't use VirtualBox or VMware Workstation.</li>
We'll make the firmware update process more robust. <li>Give a try to every USB port of your computer.</li>
</i> <li>Remove every other USB device from your computer.</li>
</p> <li>If the left half becomes unresponsive after a failed update then retry and follow the instructions displayed during the update to fix it.</li>
<li>If the above fails, retry a couple of times.</li>
<li>If everything else fails, please add a new comment to <a class="link-github" (click)="openFirmwareGitHubIssuePage($event)">the GitHub issue</a>, and attach the update log.</li>
</ol>
<p> <p>
<button class="btn btn-primary" <button class="btn btn-primary"
@@ -27,27 +30,17 @@
(click)="onUpdateFirmware()"> (click)="onUpdateFirmware()">
Flash firmware {{ (getAgentVersionInfo$ | async).firmwareVersion }} (bundled with Agent) Flash firmware {{ (getAgentVersionInfo$ | async).firmwareVersion }} (bundled with Agent)
</button> </button>
<label class="btn btn-primary btn-file" <file-upload [disabled]="flashFirmwareButtonDisbabled$ | async"
[class.disabled]="flashFirmwareButtonDisbabled$ | async"> (fileChanged)="changeFile($event)"
Choose firmware file and flash it accept=".tar.bz2"
<input id="firmware-file-select" label="Choose firmware file and flash it"></file-upload>
type="file"
accept=".tar.bz2"
[disabled]="flashFirmwareButtonDisbabled$ | async"
(change)="changeFile($event)">
</label>
</p> </p>
</div> </div>
<div class="flex-grow" #scrollMe> <div class="flex-grow">
<xterm [logs]="xtermLog$ | async"></xterm> <xterm [logs]="xtermLog$ | async"></xterm>
</div> </div>
<div class="footer"> <div class="flex-footer">
<button type="button"
class="btn btn-primary ok-button"
[disabled]="firmwareOkButtonDisabled$ | async"
(click)="onOkButtonClick()">OK
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,24 +6,6 @@
width: 100%; width: 100%;
} }
.flex-container { .link-github {
height: 100%; cursor: pointer;
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

@@ -1,19 +1,21 @@
import { Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'; import { Component, OnDestroy } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { HardwareModules, VersionInformation } from 'uhk-common'; import { HardwareModules, VersionInformation } from 'uhk-common';
import { Constants } from 'uhk-common';
import { OpenUrlInNewWindowAction } from '../../../store/actions/app';
import { import {
AppState, AppState,
firmwareOkButtonDisabled,
flashFirmwareButtonDisbabled, flashFirmwareButtonDisbabled,
getAgentVersionInfo, getAgentVersionInfo,
getHardwareModules, getHardwareModules,
xtermLog xtermLog
} from '../../../store'; } from '../../../store';
import { UpdateFirmwareAction, UpdateFirmwareOkButtonAction, UpdateFirmwareWithAction } from '../../../store/actions/device'; import { UpdateFirmwareAction, UpdateFirmwareWithAction } from '../../../store/actions/device';
import { XtermLog } from '../../../models/xterm-log'; import { XtermLog } from '../../../models/xterm-log';
import { UploadFileData } from '../../../models/upload-file-data';
@Component({ @Component({
selector: 'device-firmware', selector: 'device-firmware',
@@ -26,33 +28,20 @@ import { XtermLog } from '../../../models/xterm-log';
export class DeviceFirmwareComponent implements OnDestroy { export class DeviceFirmwareComponent implements OnDestroy {
flashFirmwareButtonDisbabled$: Observable<boolean>; flashFirmwareButtonDisbabled$: Observable<boolean>;
xtermLog$: Observable<Array<XtermLog>>; xtermLog$: Observable<Array<XtermLog>>;
xtermLogSubscription: Subscription;
getAgentVersionInfo$: Observable<VersionInformation>; getAgentVersionInfo$: Observable<VersionInformation>;
firmwareOkButtonDisabled$: Observable<boolean>;
hardwareModulesSubscription: Subscription; hardwareModulesSubscription: Subscription;
hardwareModules: HardwareModules; hardwareModules: HardwareModules;
@ViewChild('scrollMe') divElement: ElementRef;
constructor(private store: Store<AppState>) { constructor(private store: Store<AppState>) {
this.flashFirmwareButtonDisbabled$ = store.select(flashFirmwareButtonDisbabled); this.flashFirmwareButtonDisbabled$ = store.select(flashFirmwareButtonDisbabled);
this.xtermLog$ = store.select(xtermLog); 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.getAgentVersionInfo$ = store.select(getAgentVersionInfo);
this.firmwareOkButtonDisabled$ = store.select(firmwareOkButtonDisabled);
this.hardwareModulesSubscription = store.select(getHardwareModules).subscribe(data => { this.hardwareModulesSubscription = store.select(getHardwareModules).subscribe(data => {
this.hardwareModules = data; this.hardwareModules = data;
}); });
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.xtermLogSubscription.unsubscribe();
this.hardwareModulesSubscription.unsubscribe(); this.hardwareModulesSubscription.unsubscribe();
} }
@@ -60,22 +49,12 @@ export class DeviceFirmwareComponent implements OnDestroy {
this.store.dispatch(new UpdateFirmwareAction()); this.store.dispatch(new UpdateFirmwareAction());
} }
onOkButtonClick(): void { changeFile(data: UploadFileData): void {
this.store.dispatch(new UpdateFirmwareOkButtonAction()); this.store.dispatch(new UpdateFirmwareWithAction(data.data));
} }
changeFile(event): void { openFirmwareGitHubIssuePage(event): void {
const files = event.srcElement.files; event.preventDefault();
this.store.dispatch(new OpenUrlInNewWindowAction(Constants.FIRMWARE_GITHUB_ISSUE_URL));
if (files.length === 0) {
return;
}
const fileReader = new FileReader();
fileReader.onloadend = function () {
const arrayBuffer = new Uint8Array(fileReader.result);
this.store.dispatch(new UpdateFirmwareWithAction(Array.prototype.slice.call(arrayBuffer)));
}.bind(this);
fileReader.readAsArrayBuffer(files[0]);
} }
} }

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">
<xterm [logs]="xtermLog$ | async"></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,34 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
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 {
flashFirmwareButtonDisbabled$: Observable<boolean>;
xtermLog$: Observable<Array<XtermLog>>;
constructor(private store: Store<AppState>) {
}
ngOnInit(): void {
this.flashFirmwareButtonDisbabled$ = this.store.select(flashFirmwareButtonDisbabled);
this.xtermLog$ = this.store.select(xtermLog);
}
onRecoveryDevice(): void {
this.store.dispatch(new RecoveryDeviceAction());
}
}

View File

@@ -0,0 +1,9 @@
<label class="btn btn-primary btn-file"
[class.disabled]="disabled">
{{ label }}
<input #inputControl
type="file"
[accept]="accept"
[disabled]="disabled"
(change)="changeFile($event)">
</label>

View File

@@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { UploadFileData } from '../../models/upload-file-data';
@Component({
selector: 'file-upload',
templateUrl: './file-upload.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileUploadComponent {
@Input() label = 'Select file';
@Input() disabled: boolean;
@Input() accept: string;
@Output() fileChanged = new EventEmitter<UploadFileData>();
changeFile(event): void {
const files = event.srcElement.files;
if (files.length === 0) {
return;
}
const fileReader = new FileReader();
fileReader.onloadend = function () {
const arrayBuffer = new Uint8Array(fileReader.result);
const target = event.target || event.srcElement || event.currentTarget;
target.value = null;
this.fileChanged.emit({
filename: event.srcElement.value,
data: Array.from(arrayBuffer)
});
}.bind(this);
fileReader.readAsArrayBuffer(files[0]);
}
}

View File

@@ -0,0 +1 @@
export * from './file-upload.component';

View File

@@ -1,5 +1,10 @@
<div> <div>
<h4>Type text</h4> <h4>Type text</h4>
<textarea #macroTextInput name="macro-text" (change)="onTextChange()" <textarea #macroTextInput
(keyup)="validate()" class="macro__text-input">{{ macroAction?.text }}</textarea> name="macro-text"
(keydown)="onKeydown($event)"
(change)="onTextChange()"
(keyup)="validate()"
(paste)="onPaste($event)"
class="macro__text-input">{{ macroAction?.text }}</textarea>
</div> </div>

View File

@@ -11,6 +11,8 @@ import { TextMacroAction } from 'uhk-common';
import { MacroBaseComponent } from '../macro-base.component'; import { MacroBaseComponent } from '../macro-base.component';
const NON_ASCII_REGEXP = /[^\x00-\x7F]/g;
@Component({ @Component({
selector: 'macro-text-tab', selector: 'macro-text-tab',
templateUrl: './macro-text.component.html', templateUrl: './macro-text.component.html',
@@ -36,6 +38,41 @@ export class MacroTextTabComponent extends MacroBaseComponent implements OnInit,
this.macroAction.text = this.input.nativeElement.value; this.macroAction.text = this.input.nativeElement.value;
} }
/**
* Not allow non ascii character
* @param $event
*/
onKeydown($event: KeyboardEvent): void {
if (new RegExp(NON_ASCII_REGEXP).test($event.key)) {
$event.preventDefault();
$event.stopPropagation();
}
}
/**
* Remove non ascii character from clipboard data
* @param $event
*/
onPaste($event: ClipboardEvent): void {
$event.preventDefault();
const textarea: HTMLTextAreaElement = this.input.nativeElement;
const data = $event.clipboardData.getData('text/plain');
const text = data && data.replace(NON_ASCII_REGEXP, '') || '';
if (text.length === 0) {
return;
}
const value = textarea.value || '';
const prefix = value.substr(0, textarea.selectionStart);
const end = textarea.selectionEnd;
const suffix = value.substr(textarea.selectionEnd);
textarea.value = prefix + text + suffix;
const correction = end === 0 ? 0 : 1;
textarea.selectionStart = textarea.selectionEnd = end + text.length - correction;
this.macroAction.text = textarea.value;
}
isMacroValid = () => !!this.input.nativeElement.value; isMacroValid = () => !!this.input.nativeElement.value;
private init = () => { private init = () => {

View File

@@ -2,13 +2,11 @@
<li class="sidebar__level-0--item"> <li class="sidebar__level-0--item">
<div class="sidebar__level-0"> <div class="sidebar__level-0">
<i class="uhk-icon uhk-icon-0401-usb-stick rotate-right"></i> <i class="uhk-icon uhk-icon-0401-usb-stick rotate-right"></i>
<input #deviceName cancelable <auto-grow-input [ngModel]="state.deviceName"
class="pane-title__name" [maxParentWidthPercent]="0.65"
type="text" [css]="'side-menu-pane-title__name'"
[readonly]="state.restoreUserConfiguration" [disabled]="state.restoreUserConfiguration || state.updatingFirmware"
(change)="editDeviceName($event.target.value)" (ngModelChange)="editDeviceName($event)"></auto-grow-input>
(keyup.enter)="deviceName.blur()"
(keyup)="calculateHeaderTextWidth($event.target.value)">
<i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'device')"></i> <i class="fa fa-chevron-up pull-right" (click)="toggleHide($event, 'device')"></i>
</div> </div>
<ul [@toggler]="animation['device']"> <ul [@toggler]="animation['device']">

View File

@@ -162,22 +162,3 @@ ul {
} }
} }
} }
.pane-title {
margin-bottom: 1em;
&__name {
border: none;
border-bottom: 2px dotted #999;
padding: 0;
margin: 0 0.25rem;
text-overflow: ellipsis;
background-color: transparent;
&:focus {
box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc;
border-color: transparent;
background-color: transparent;
}
}
}

View File

@@ -1,5 +1,4 @@
import { import {
AfterContentInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@@ -19,7 +18,6 @@ import 'rxjs/add/operator/let';
import { AppState, getSideMenuPageState } from '../../store'; import { AppState, getSideMenuPageState } from '../../store';
import { MacroActions } from '../../store/actions'; import { MacroActions } from '../../store/actions';
import * as util from '../../util';
import { RenameUserConfigurationAction } from '../../store/actions/user-config'; import { RenameUserConfigurationAction } from '../../store/actions/user-config';
import { SideMenuPageState } from '../../models/side-menu-page-state'; import { SideMenuPageState } from '../../models/side-menu-page-state';
@@ -40,7 +38,7 @@ import { SideMenuPageState } from '../../models/side-menu-page-state';
styleUrls: ['./side-menu.component.scss'], styleUrls: ['./side-menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SideMenuComponent implements AfterContentInit, OnInit, OnDestroy { export class SideMenuComponent implements OnInit, OnDestroy {
state: SideMenuPageState; state: SideMenuPageState;
animation: { [key: string]: 'active' | 'inactive' }; animation: { [key: string]: 'active' | 'inactive' };
@ViewChild('deviceName') deviceName: ElementRef; @ViewChild('deviceName') deviceName: ElementRef;
@@ -62,15 +60,10 @@ export class SideMenuComponent implements AfterContentInit, OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.stateSubscription = this.store.select(getSideMenuPageState).subscribe(data => { this.stateSubscription = this.store.select(getSideMenuPageState).subscribe(data => {
this.state = data; this.state = data;
this.setDeviceName();
this.cdRef.markForCheck(); this.cdRef.markForCheck();
}); });
} }
ngAfterContentInit(): void {
this.setDeviceName();
}
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.stateSubscription) { if (this.stateSubscription) {
this.stateSubscription.unsubscribe(); this.stateSubscription.unsubscribe();
@@ -106,24 +99,6 @@ export class SideMenuComponent implements AfterContentInit, OnInit, OnDestroy {
} }
editDeviceName(name: string): void { editDeviceName(name: string): void {
if (!util.isValidName(name) || name.trim() === this.state.deviceName) {
this.setDeviceName();
return;
}
this.store.dispatch(new RenameUserConfigurationAction(name)); this.store.dispatch(new RenameUserConfigurationAction(name));
} }
calculateHeaderTextWidth(text): void {
const htmlInput = this.deviceName.nativeElement as HTMLInputElement;
const maxWidth = htmlInput.parentElement.offsetWidth * 0.66;
const textWidth = util.getContentWidth(window.getComputedStyle(htmlInput), text);
this.renderer.setStyle(htmlInput, 'width', Math.min(maxWidth, textWidth) + 'px');
}
private setDeviceName(): void {
if (this.deviceName) {
this.renderer.setProperty(this.deviceName.nativeElement, 'value', this.state.deviceName);
this.calculateHeaderTextWidth(this.deviceName.nativeElement.value);
}
}
} }

View File

@@ -1,5 +1,17 @@
<div class="wrapper"> <div class="x-term-container">
<ul class="list-unstyled"> <div class="x-term-wrapper" #scrollMe>
<li *ngFor="let log of logs" [ngClass]="log.cssClass"><span>{{ log.message }}</span></li> <ul class="list-unstyled">
</ul> <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> </div>

View File

@@ -1,9 +1,36 @@
$scrollbar-color: #ffffff;
$scrollbar-radius: 6px;
:host { :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; 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 { .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'; import { XtermLog } from '../../models/xterm-log';
@Component({ @Component({
@@ -7,6 +7,21 @@ import { XtermLog } from '../../models/xterm-log';
styleUrls: ['./xterm.component.scss'], styleUrls: ['./xterm.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class XtermComponent { export class XtermComponent implements OnChanges {
@Input() logs: Array<XtermLog> = []; @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

@@ -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,9 @@ 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';
import { FileUploadComponent } from './components/file-upload';
import { AutoGrowInputComponent } from './components/auto-grow-input';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -176,7 +180,10 @@ import { Autofocus } from './directives/autofocus/autofocus.directive';
SliderWrapperComponent, SliderWrapperComponent,
EditableTextComponent, EditableTextComponent,
Autofocus, Autofocus,
RestoreConfigurationComponent RestoreConfigurationComponent,
RecoveryModeComponent,
FileUploadComponent,
AutoGrowInputComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@@ -211,7 +218,8 @@ import { Autofocus } from './directives/autofocus/autofocus.directive';
UhkDeviceInitializedGuard, UhkDeviceInitializedGuard,
UhkDeviceUninitializedGuard, UhkDeviceUninitializedGuard,
UhkDeviceLoadingGuard, UhkDeviceLoadingGuard,
UhkDeviceLoadedGuard UhkDeviceLoadedGuard,
UhkDeviceBootloaderNotActiveGuard
], ],
exports: [ exports: [
UhkMessageComponent, UhkMessageComponent,

View File

@@ -1,5 +1,6 @@
import { Action } from '@ngrx/store'; 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] '; const PREFIX = '[device] ';
@@ -26,7 +27,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 {
@@ -95,25 +97,23 @@ export class UpdateFirmwareWithAction implements Action {
export class UpdateFirmwareReplyAction implements Action { export class UpdateFirmwareReplyAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_REPLY; type = ActionTypes.UPDATE_FIRMWARE_REPLY;
constructor(public payload: IpcResponse) { constructor(public payload: FirmwareUpgradeIpcResponse) {
} }
} }
export class UpdateFirmwareSuccessAction implements Action { export class UpdateFirmwareSuccessAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_SUCCESS; type = ActionTypes.UPDATE_FIRMWARE_SUCCESS;
constructor(public payload: HardwareModules) {
}
} }
export class UpdateFirmwareFailedAction implements Action { export class UpdateFirmwareFailedAction implements Action {
type = ActionTypes.UPDATE_FIRMWARE_FAILED; 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 { export class ResetMouseSpeedSettingsAction implements Action {
type = ActionTypes.RESET_MOUSE_SPEED_SETTINGS; type = ActionTypes.RESET_MOUSE_SPEED_SETTINGS;
} }
@@ -140,6 +140,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
@@ -157,9 +161,9 @@ export type Actions
| UpdateFirmwareReplyAction | UpdateFirmwareReplyAction
| UpdateFirmwareSuccessAction | UpdateFirmwareSuccessAction
| UpdateFirmwareFailedAction | UpdateFirmwareFailedAction
| UpdateFirmwareOkButtonAction
| HardwareModulesLoadedAction | HardwareModulesLoadedAction
| 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

@@ -14,7 +14,7 @@ import 'rxjs/add/operator/withLatestFrom';
import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/switchMap';
import { import {
DeviceConnectionState, FirmwareUpgradeIpcResponse,
HardwareConfiguration, HardwareConfiguration,
IpcResponse, IpcResponse,
NotificationType, NotificationType,
@@ -24,6 +24,7 @@ import {
ActionTypes, ActionTypes,
ConnectionStateChangedAction, ConnectionStateChangedAction,
HideSaveToKeyboardButton, HideSaveToKeyboardButton,
RecoveryDeviceAction,
ResetUserConfigurationAction, ResetUserConfigurationAction,
RestoreUserConfigurationFromBackupSuccessAction, RestoreUserConfigurationFromBackupSuccessAction,
SaveConfigurationAction, SaveConfigurationAction,
@@ -33,14 +34,13 @@ import {
SetPrivilegeOnLinuxReplyAction, SetPrivilegeOnLinuxReplyAction,
UpdateFirmwareAction, UpdateFirmwareAction,
UpdateFirmwareFailedAction, UpdateFirmwareFailedAction,
UpdateFirmwareOkButtonAction,
UpdateFirmwareReplyAction, UpdateFirmwareReplyAction,
UpdateFirmwareSuccessAction, UpdateFirmwareSuccessAction,
UpdateFirmwareWithAction UpdateFirmwareWithAction
} from '../actions/device'; } from '../actions/device';
import { DeviceRendererService } from '../../services/device-renderer.service'; import { DeviceRendererService } from '../../services/device-renderer.service';
import { SetupPermissionErrorAction, ShowNotificationAction } from '../actions/app'; import { SetupPermissionErrorAction, ShowNotificationAction } from '../actions/app';
import { AppState } from '../index'; import { AppState, getRouterState } from '../index';
import { import {
ActionTypes as UserConfigActions, ActionTypes as UserConfigActions,
ApplyUserConfigurationFromFileAction, ApplyUserConfigurationFromFileAction,
@@ -55,11 +55,20 @@ export class DeviceEffects {
@Effect() @Effect()
deviceConnectionStateChange$: Observable<Action> = this.actions$ deviceConnectionStateChange$: Observable<Action> = this.actions$
.ofType<ConnectionStateChangedAction>(ActionTypes.CONNECTION_STATE_CHANGED) .ofType<ConnectionStateChangedAction>(ActionTypes.CONNECTION_STATE_CHANGED)
.map(action => action.payload) .withLatestFrom(this.store.select(getRouterState))
.do((state: DeviceConnectionState) => { .do(([action, route]) => {
const state = action.payload;
if (route.state && route.state.url.startsWith('/device/firmware')) {
return;
}
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(['/']);
} }
@@ -67,7 +76,9 @@ export class DeviceEffects {
this.router.navigate(['/detection']); this.router.navigate(['/detection']);
} }
}) })
.switchMap((state: DeviceConnectionState) => { .switchMap(([action, route]) => {
const state = action.payload;
if (state.connected && state.hasPermission) { if (state.connected && state.hasPermission) {
return Observable.of(new LoadConfigFromDeviceAction()); return Observable.of(new LoadConfigFromDeviceAction());
} }
@@ -90,7 +101,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
}); });
} }
@@ -198,22 +210,26 @@ export class DeviceEffects {
@Effect() updateFirmwareReply$ = this.actions$ @Effect() updateFirmwareReply$ = this.actions$
.ofType<UpdateFirmwareReplyAction>(ActionTypes.UPDATE_FIRMWARE_REPLY) .ofType<UpdateFirmwareReplyAction>(ActionTypes.UPDATE_FIRMWARE_REPLY)
.map(action => action.payload) .map(action => action.payload)
.switchMap((response: IpcResponse) => { .switchMap((response: FirmwareUpgradeIpcResponse)
: Observable<UpdateFirmwareSuccessAction | UpdateFirmwareFailedAction> => {
if (response.success) { 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$ @Effect() restoreUserConfiguration$ = this.actions$
.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

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

View File

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

View File

@@ -7,7 +7,8 @@ import {
HardwareModulesLoadedAction, HardwareModulesLoadedAction,
SaveConfigurationAction, SaveConfigurationAction,
HasBackupUserConfigurationAction, HasBackupUserConfigurationAction,
UpdateFirmwareFailedAction UpdateFirmwareFailedAction,
UpdateFirmwareSuccessAction
} from '../actions/device'; } from '../actions/device';
import { ActionTypes as AppActions, ElectronMainLogReceivedAction } from '../actions/app'; import { ActionTypes as AppActions, ElectronMainLogReceivedAction } from '../actions/app';
import { initProgressButtonState, ProgressButtonState } from './progress-button-state'; import { initProgressButtonState, ProgressButtonState } from './progress-button-state';
@@ -17,7 +18,9 @@ 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;
savingToKeyboard: boolean;
updatingFirmware: boolean; updatingFirmware: boolean;
firmwareUpdateFinished: boolean; firmwareUpdateFinished: boolean;
modules: HardwareModules; modules: HardwareModules;
@@ -29,7 +32,9 @@ 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,
savingToKeyboard: false,
updatingFirmware: false, updatingFirmware: false,
firmwareUpdateFinished: false, firmwareUpdateFinished: false,
modules: { modules: {
@@ -46,14 +51,15 @@ export const initialState: State = {
hasBackupUserConfiguration: false hasBackupUserConfiguration: false
}; };
export function reducer(state = initialState, action: Action) { export function reducer(state = initialState, action: Action): State {
switch (action.type) { switch (action.type) {
case ActionTypes.CONNECTION_STATE_CHANGED: { case ActionTypes.CONNECTION_STATE_CHANGED: {
const data = (<ConnectionStateChangedAction>action).payload; const data = (<ConnectionStateChangedAction>action).payload;
return { return {
...state, ...state,
connected: data.connected, connected: data.connected,
hasPermission: data.hasPermission hasPermission: data.hasPermission,
bootloaderActive: data.bootloaderActive
}; };
} }
@@ -129,12 +135,14 @@ export function reducer(state = initialState, action: Action) {
return { return {
...state, ...state,
updatingFirmware: false, updatingFirmware: false,
firmwareUpdateFinished: true firmwareUpdateFinished: true,
modules: (action as UpdateFirmwareSuccessAction).payload
}; };
case ActionTypes.UPDATE_FIRMWARE_FAILED: { case ActionTypes.UPDATE_FIRMWARE_FAILED: {
const data = (action as UpdateFirmwareFailedAction).payload;
const logEntry = { const logEntry = {
message: (action as UpdateFirmwareFailedAction).payload.message, message: data.error.message,
cssClass: XtermCssClass.error cssClass: XtermCssClass.error
}; };
@@ -142,6 +150,7 @@ export function reducer(state = initialState, action: Action) {
...state, ...state,
updatingFirmware: false, updatingFirmware: false,
firmwareUpdateFinished: true, firmwareUpdateFinished: true,
modules: data.modules,
log: [...state.log, logEntry] log: [...state.log, logEntry]
}; };
} }
@@ -193,6 +202,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;
} }
@@ -203,7 +219,6 @@ export const isDeviceConnected = (state: State) => state.connected || state.upda
export const hasDevicePermission = (state: State) => state.hasPermission; export const hasDevicePermission = (state: State) => state.hasPermission;
export const getSaveToKeyboardState = (state: State) => state.saveToKeyboard; export const getSaveToKeyboardState = (state: State) => state.saveToKeyboard;
export const xtermLog = (state: State) => state.log; export const xtermLog = (state: State) => state.log;
export const firmwareOkButtonDisabled = (state: State) => !state.firmwareUpdateFinished;
export const getHardwareModules = (state: State) => state.modules; export const getHardwareModules = (state: State) => state.modules;
export const getHasBackupUserConfiguration = (state: State) => state.hasBackupUserConfiguration; export const getHasBackupUserConfiguration = (state: State) => state.hasBackupUserConfiguration;
export const getBackupUserConfigurationState = (state: State): RestoreConfigurationState => { export const getBackupUserConfigurationState = (state: State): RestoreConfigurationState => {
@@ -212,3 +227,4 @@ export const getBackupUserConfigurationState = (state: State): RestoreConfigurat
hasBackupUserConfiguration: state.hasBackupUserConfiguration hasBackupUserConfiguration: state.hasBackupUserConfiguration
}; };
}; };
export const bootloaderActive = (state: State) => state.bootloaderActive;

View File

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

View File

@@ -4,6 +4,7 @@
@import '~font-awesome/scss/font-awesome'; @import '~font-awesome/scss/font-awesome';
@import './styles/tooltip'; @import './styles/tooltip';
@import './styles/uhk-icons/uhk-icon'; @import './styles/uhk-icons/uhk-icon';
@import './styles/side-menu';
html, body { html, body {
width: 100%; width: 100%;
@@ -155,3 +156,25 @@ pre {
} }
} }
} }
.flex-container {
height: 100%;
display: flex;
flex-direction: column;
}
.flex-grow {
display: flex;
flex-direction: column;
align-items: stretch;
flex: 1;
}
.flex-footer {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.ok-button {
min-width: 100px;
}

View File

@@ -0,0 +1,22 @@
.side-menu-pane-title {
margin-bottom: 1em;
&__name {
border: none;
border-bottom: 2px dotted #999;
padding: 0;
margin: 0 0.25rem;
text-overflow: ellipsis;
background-color: transparent;
&:focus {
box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc;
border-color: transparent;
background-color: transparent;
}
&:disabled {
border-bottom: none;
}
}
}

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env node
const uhk = require('./uhk');
const device = uhk.getUhkDevice();
uhk.eraseHca(device)
.catch((err)=>{
console.error(err);
});

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env node
const fs = require('fs');
const uhk = require('./uhk');
(async function() {
const device = uhk.getUhkDevice();
const buffer = new Buffer(Array(2**15-64).fill(0xff));
await uhk.writeConfig(device, buffer, false);
await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, uhk.configBufferIds.stagingUserConfig);
})();

View File

@@ -2,9 +2,12 @@
const uhk = require('./uhk'); const uhk = require('./uhk');
const program = require('commander'); const program = require('commander');
program.parse(process.argv); program
.option('-t, --timeout <ms>', 'Bootloader timeout in ms', 5000)
.parse(process.argv);
const enumerationMode = program.args[0]; const enumerationMode = program.args[0];
(async function() { (async function() {
await uhk.reenumerate(enumerationMode); await uhk.reenumerate(enumerationMode, program.timeout);
})(); })();

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 = '';
@@ -191,8 +189,7 @@ async function updateDeviceFirmware(firmwareImage, extension) {
// USB commands // USB commands
function reenumerate(enumerationMode) { function reenumerate(enumerationMode, bootloaderTimeoutMs=5000) {
const bootloaderTimeoutMs = 5000;
const pollingIntervalMs = 100; const pollingIntervalMs = 100;
let pollingTimeoutMs = 10000; let pollingTimeoutMs = 10000;
@@ -396,6 +393,12 @@ async function writeHca(device, isIso) {
await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, configBufferIds.hardwareConfig); await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, configBufferIds.hardwareConfig);
} }
async function eraseHca(device) {
const buffer = new Buffer(Array(64).fill(0xff));
await uhk.writeConfig(device, buffer, true);
await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, configBufferIds.hardwareConfig);
}
async function getModuleProperty(device, slotId, moduleProperty) { async function getModuleProperty(device, slotId, moduleProperty) {
await writeDevice(device, [uhk.usbCommands.getModuleProperty, slotId, moduleProperty]); await writeDevice(device, [uhk.usbCommands.getModuleProperty, slotId, moduleProperty]);
} }
@@ -426,6 +429,7 @@ uhk = exports = module.exports = moduleExports = {
launchEepromTransfer, launchEepromTransfer,
writeUca, writeUca,
writeHca, writeHca,
eraseHca,
getModuleProperty, getModuleProperty,
usbCommands: { usbCommands: {
getDeviceProperty : 0x00, getDeviceProperty : 0x00,

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env node
const program = require('commander');
const fs = require('fs');
const uhk = require('./uhk');
(async function() {
const device = uhk.getUhkDevice();
require('shelljs/global');
program
.usage(`config.bin`)
.option('-h, --hardware-config', 'Write the hardware config instead of the user config')
.parse(process.argv);
if (program.args.length == 0) {
console.error('No binary config file specified.');
exit(1);
}
const configBin = program.args[0];
const isHardwareConfig = program.hardwareConfig;
const configTypeString = isHardwareConfig ? 'hardware' : 'user';
const configBuffer = fs.readFileSync(configBin);
await uhk.writeUserConfig(device, configBuffer, isHardwareConfig);
})();

View File

@@ -2,7 +2,7 @@
const uhk = require('./uhk'); const uhk = require('./uhk');
if (process.argv.length < 2) { if (process.argv.length < 2) {
console.log(`use: write-hca {iso|ansi}`); console.log(`use: write-hardware-config {iso|ansi}`);
process.exit(1); process.exit(1);
} }
@@ -12,7 +12,8 @@ if (layout !== 'iso' && layout !== 'ansi') {
process.exit(1); process.exit(1);
} }
uhk.writeHca(layout === 'iso') const device = uhk.getUhkDevice();
uhk.writeHca(device, layout === 'iso')
.catch((err)=>{ .catch((err)=>{
console.error(err); console.error(err);
}); });

View File

@@ -17,5 +17,6 @@ const uhk = require('./uhk');
const device = uhk.getUhkDevice(); const device = uhk.getUhkDevice();
const configBuffer = fs.readFileSync(configPath); const configBuffer = fs.readFileSync(configPath);
await uhk.writeConfig(device, configBuffer, false); await uhk.writeConfig(device, configBuffer, false);
await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, uhk.configBufferIds.stagingUserConfig); await uhk.applyConfig(device);
await uhk.launchEepromTransfer(device, uhk.eepromOperations.write, uhk.configBufferIds.validatedUserConfig);
})(); })();