45 Commits

Author SHA1 Message Date
László Monda
02044ae1d0 Bump version to 1.1.1 and update changelog. 2018-02-13 03:54:28 +01:00
László Monda
3f99d47bba Use firmware 8.1.2 2018-02-13 03:40:52 +01:00
László Monda
9beadb4aac Add keymap descriptions. 2018-02-13 03:24:06 +01:00
László Monda
d9fb7a4b42 Merge branch 'master' of github.com:UltimateHackingKeyboard/agent 2018-02-13 03:05:18 +01:00
László Monda
83912ec21f Assign "switch to test keymap" action on all keymaps. 2018-02-13 03:04:36 +01:00
Róbert Kiss
6c7232a5ba feat(device): Make Agent able to unbrick bricked modules (#577) 2018-02-13 02:11:27 +01:00
László Monda
65fc8b5efb Tweak the scancode related help text. 2018-02-12 15:36:39 +01:00
Róbert Kiss
7a64191955 fix(config): Save reset user configuration in web module (#574) 2018-02-11 20:44:08 +01:00
Róbert Kiss
1a413c824e Fix 560 delete bind play macro action when macro delete (#576)
* fix(config): delete KeyAction binding of deleted macro

* refactor: use sorter import

* fix(macro): read the macro id from route params

* fix(keyAction): use NoneAction in keyAction mapping
2018-02-11 20:12:12 +01:00
Róbert Kiss
e545c9d67b feat(popover): Add Non-US help tooltip (#578) 2018-02-11 19:47:41 +01:00
László Monda
8650fef7ae Make reenumerate() more reliable. 2018-01-31 04:26:49 +01:00
László Monda
5c618869a2 Use updateDeviceFirmware(), reenumerate(), and updateModuleFirmware() in update-all-firmwares.js instead of forking processes. 2018-01-31 03:40:46 +01:00
László Monda
1b8d6949e0 Extract updateModuleFirmware() to uhk.js 2018-01-31 03:03:34 +01:00
László Monda
aabc0a8746 Use waitForKbootIdle() in update-module-firmware.js 2018-01-31 02:28:29 +01:00
László Monda
9589398834 Move waitForKbootIdle() to uhk.js 2018-01-31 02:25:37 +01:00
László Monda
933a715ea5 Extract waitForKbootIdle() 2018-01-31 02:22:41 +01:00
László Monda
df14e2d569 Extract uhk.jumpToModuleBootloader() and use it. 2018-01-31 01:17:28 +01:00
László Monda
4f8a0247d3 Call uhk.sendKbootCommandToModule() instead of forking send-kboot-command-to-module.js 2018-01-31 00:57:53 +01:00
László Monda
85ec5f6b6a Make update-module-firmware.js use reenumerate() and sendKbootCommandToModule() instead of forking more processes. 2018-01-31 00:17:27 +01:00
László Monda
739b830f47 Use uhk.writeDevice() in jump-to-module-bootloader.js 2018-01-31 00:09:16 +01:00
László Monda
482cff3d3b Extract sendKbootCommandToModule() from send-kboot-command-to-module.js to uhk.js 2018-01-30 23:44:25 +01:00
László Monda
9284ae5032 Use reenumerate() instead of forking reenumerate.js 2018-01-30 23:32:37 +01:00
László Monda
cac6fdc190 Move updateDeviceFirmware() from update-device-firmware.js to uhk.js 2018-01-30 21:48:42 +01:00
László Monda
ca9bf60a1b Extract updateDeviceFirmware() 2018-01-30 21:46:39 +01:00
László Monda
bb7edb8e4d Remove shared.js 2018-01-30 21:39:05 +01:00
László Monda
0d9c976eb8 Move execRetry() from shared.js to uhk.js 2018-01-30 21:37:27 +01:00
László Monda
288d4f75b6 Move getBlhostCmd() from shared.js to uhk.js 2018-01-30 21:35:35 +01:00
László Monda
73e07eae2d Move checkFirmwareImage() from shared.js to uhk.js 2018-01-30 21:28:00 +01:00
László Monda
8e620caac5 Use reenumerate() in update-device-firmware.js instead of forking reenumerate.js 2018-01-30 20:39:48 +01:00
László Monda
0d4e1acf76 Extract reenumerate() from reenumerate.js to uhk.js 2018-01-30 19:34:29 +01:00
László Monda
88c42d58b1 Extract reenumerate() as an async function in reenumerate.js 2018-01-30 18:36:16 +01:00
László Monda
5099e904fc Use uhk.writeDevice() in set-i2c-baud-rate.js 2018-01-30 17:30:12 +01:00
László Monda
5476f7c3a5 Extract I2C0_F. 2018-01-30 17:25:29 +01:00
László Monda
e0bb0bcca3 Add the async uhk.writeDevice() and use it in get-i2c-baud-rate.js 2018-01-30 17:21:50 +01:00
László Monda
124c3ec29b Simplify reenumerate.js mainly by using uint32ToArray() 2018-01-30 06:09:09 +01:00
László Monda
2310320b8a Replace pushUint32() with uint32ToArray() and don't use it where not needed. 2018-01-30 05:30:32 +01:00
László Monda
67346b4cda Simplify getTransferData() 2018-01-30 05:10:29 +01:00
Róbert Kiss
662ca0152f fix(keymap): Don't show keymap description in the key action popover (#572) 2018-01-29 23:40:41 +01:00
Róbert Kiss
6358528438 ci: use node 8.9.4 (#571)
Node 8.9.4 contain a few security update and use npm 5.6.0 so not need to
install npm in the build process. The build time will a few sec sorter
2018-01-29 23:20:07 +01:00
József Farkas
02f1053d46 feat(popover): sort keymaps and macros alphabetically (#534)
* feat(popover): sort keymaps and macros alphabetically

Closes #512

* small performance refactoring
2018-01-29 23:15:21 +01:00
Róbert Kiss
a44a7dc5f8 chore: use lodash-es version (#569)
lodash-es support tree shaking and the bundle size will near 2MB less
2018-01-29 23:02:17 +01:00
Róbert Kiss
02d57fdabf feat(keymap): add description to keymap (#559)
* feat(keymap): add description to keymaps

* add new feature request

* preserve new lines
2018-01-29 22:54:29 +01:00
Róbert Kiss
38f6688930 chore: upgrade electron => 1.7.11 (#568) 2018-01-29 21:26:32 +01:00
Róbert Kiss
6ca12d0ccd build: set Apple appId => com.ultimategadgetlabs.agent (#566) 2018-01-23 19:19:15 +01:00
László Monda
acd17ac657 Fix the background color of the toplevel device node. Fixes #552 2018-01-22 04:41:43 +01:00
58 changed files with 6731 additions and 5788 deletions

2
.nvmrc
View File

@@ -1 +1 @@
8.9.1 8.9.4

View File

@@ -45,7 +45,6 @@ addons:
install: install:
- nvm install - nvm install
- npm i -g npm@5.6.0
- npm install - npm install
before_script: before_script:

View File

@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
Every Agent version includes the most recent firmware version. See the [firmware changelog](https://github.com/UltimateHackingKeyboard/firmware/blob/master/CHANGELOG.md).
## [1.1.1] - 2017-02-13
Firmware: 8.1.**2** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.2)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0
- Sign Agent on OSX resulting in easier installation.
- Add per-keymap description field.
- Sort keymaps and macros alphabetically within the key action popover.
- Add tooltip regarding non-US scancodes.
- When deleting a macro, also delete the relevant play macro actions.
- Make the reset configuration button persist the reset configuration in Agent-web.
- Make Agent able to unbrick bricked modules.
- Assign "switch to test keymap" action on all keymaps in the default configuration.
- Add keymap descriptions in the default configuration.
## [1.1.0] - 2017-01-15 ## [1.1.0] - 2017-01-15
Firmware: 8.**1**.0 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.0)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0 Firmware: 8.**1**.0 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.0)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0

View File

@@ -18,7 +18,6 @@ shallow_clone: true
install: install:
- ps: Install-Product node $env:nodejs_version - ps: Install-Product node $env:nodejs_version
- npm i -g npm@5.6.0
- choco install chromium - choco install chromium
- set CI=true - set CI=true
- set PATH=%APPDATA%\npm;%PATH% - set PATH=%APPDATA%\npm;%PATH%

391
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
"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.1.0", "version": "1.1.1",
"firmwareVersion": "8.1.0", "firmwareVersion": "8.1.2",
"deviceProtocolVersion": "4.2.0", "deviceProtocolVersion": "4.2.0",
"userConfigVersion": "4.0.0", "userConfigVersion": "4.0.0",
"hardwareConfigVersion": "1.0.0", "hardwareConfigVersion": "1.0.0",
@@ -36,7 +36,7 @@
"decompress": "4.2.0", "decompress": "4.2.0",
"decompress-tarbz2": "^4.1.1", "decompress-tarbz2": "^4.1.1",
"devtron": "1.4.0", "devtron": "1.4.0",
"electron": "1.7.5", "electron": "1.7.11",
"electron-builder": "19.45.5", "electron-builder": "19.45.5",
"electron-debug": "1.4.0", "electron-debug": "1.4.0",
"electron-devtools-installer": "2.2.0", "electron-devtools-installer": "2.2.0",

View File

@@ -7,6 +7,7 @@ import { SwitchLayerAction } from './switch-layer-action';
import { SwitchKeymapAction, UnresolvedSwitchKeymapAction } from './switch-keymap-action'; import { SwitchKeymapAction, UnresolvedSwitchKeymapAction } from './switch-keymap-action';
import { MouseAction } from './mouse-action'; import { MouseAction } from './mouse-action';
import { PlayMacroAction } from './play-macro-action'; import { PlayMacroAction } from './play-macro-action';
import { NoneAction } from './none-action';
export class Helper { export class Helper {
@@ -77,6 +78,8 @@ export class Helper {
return new MouseAction().fromJsonObject(keyAction); return new MouseAction().fromJsonObject(keyAction);
case keyActionType.PlayMacroAction: case keyActionType.PlayMacroAction:
return new PlayMacroAction().fromJsonObject(keyAction, macros); return new PlayMacroAction().fromJsonObject(keyAction, macros);
case keyActionType.NoneAction:
return new NoneAction();
default: default:
throw `Invalid KeyAction.keyActionType: "${keyAction.keyActionType}"`; throw `Invalid KeyAction.keyActionType: "${keyAction.keyActionType}"`;
} }

View File

@@ -39,7 +39,8 @@ export enum ConfigBufferId {
export enum DevicePropertyIds { export enum DevicePropertyIds {
DeviceProtocolVersion = 0, DeviceProtocolVersion = 0,
ProtocolVersions = 1, ProtocolVersions = 1,
ConfigSizes = 2 ConfigSizes = 2,
CurrentKbootCommand = 3
} }
export enum EnumerationModes { export enum EnumerationModes {

View File

@@ -5,8 +5,13 @@ import * as fs from 'fs';
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';
import { convertBufferToIntArray, getTransferBuffers, DevicePropertyIds, UsbCommand, ConfigBufferId import {
} from '../index'; convertBufferToIntArray,
getTransferBuffers,
DevicePropertyIds,
UsbCommand,
ConfigBufferId
} from '../index';
import { LoadConfigurationsResult } from './models/load-configurations-result'; import { LoadConfigurationsResult } from './models/load-configurations-result';
export class UhkOperations { export class UhkOperations {
@@ -42,6 +47,13 @@ export class UhkOperations {
await snooze(1000); await snooze(1000);
await this.device.jumpToBootloaderModule(ModuleSlotToId.leftHalf); await this.device.jumpToBootloaderModule(ModuleSlotToId.leftHalf);
this.device.close(); this.device.close();
const leftModuleBricked = await this.waitForKbootIdle();
if (!leftModuleBricked) {
this.logService.error('[UhkOperations] Couldn\'t connect to the left keyboard half.');
return;
}
await this.device.reenumerate(EnumerationModes.Buspal); await this.device.reenumerate(EnumerationModes.Buspal);
this.device.close(); this.device.close();
await this.blhost.runBlhostCommandRetry([...buspalPrefix, 'get-property', '1']); await this.blhost.runBlhostCommandRetry([...buspalPrefix, 'get-property', '1']);
@@ -161,6 +173,26 @@ export class UhkOperations {
} }
} }
public async waitForKbootIdle(): Promise<boolean> {
const timeoutTime = new Date(new Date().getTime() + 30000);
while (new Date() < timeoutTime) {
const buffer = await this.device.write(new Buffer([UsbCommand.GetProperty, DevicePropertyIds.CurrentKbootCommand]));
this.device.close();
if (buffer[1] === 0) {
return true;
}
// tslint:disable-next-line: max-line-length
this.logService.info('[DeviceOperation] Cannot ping the bootloader. Please reconnect the left keyboard half. It probably needs several tries, so keep reconnecting until you see this message.');
await snooze(1000);
}
return false;
}
/** /**
* IpcMain handler. Send the UserConfiguration to the UHK Device and send a response with the result. * IpcMain handler. Send the UserConfiguration to the UHK Device and send a response with the result.
* @param {string} json - UserConfiguration in JSON format * @param {string} json - UserConfiguration in JSON format

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,7 @@
"@types/jasmine": "2.5.53", "@types/jasmine": "2.5.53",
"@types/jasminewd2": "2.0.2", "@types/jasminewd2": "2.0.2",
"@types/jquery": "3.2.9", "@types/jquery": "3.2.9",
"@types/lodash-es": "4.17.0",
"@types/node-hid": "0.5.2", "@types/node-hid": "0.5.2",
"@types/usb": "1.1.3", "@types/usb": "1.1.3",
"angular-confirmation-popover": "3.2.0", "angular-confirmation-popover": "3.2.0",
@@ -67,7 +68,7 @@
"karma-jasmine": "1.1.0", "karma-jasmine": "1.1.0",
"karma-jasmine-html-reporter": "0.2.2", "karma-jasmine-html-reporter": "0.2.2",
"less-loader": "4.0.5", "less-loader": "4.0.5",
"lodash": "4.17.4", "lodash-es": "4.17.4",
"ng2-dragula": "1.5.0", "ng2-dragula": "1.5.0",
"ng2-nouislider": "^1.7.6", "ng2-nouislider": "^1.7.6",
"ng2-select2": "1.0.0-beta.10", "ng2-select2": "1.0.0-beta.10",

View File

@@ -0,0 +1,31 @@
<div class="text-center">
<span *ngIf="showPlaceholder"
class="placeholder">
<span (click)="editText()">{{ placeholder }}</span>
</span>
<span *ngIf="showText"
class="editable">
<span (click)="editText()"
[innerHtml]="displayText"></span>
</span>
</div>
<div *ngIf="editing">
<textarea class="text-editor"
[(ngModel)]="text"
autofocus
(keydown.control.enter)="keydownEnter()"
(keydown.alt.enter)="keydownEnter()"></textarea>
<div class="pull-right buttons">
<button class="btn btn-danger"
(click)="cancelEditText()">
Cancel
</button>
<button class="btn btn-primary"
(click)="saveText()"
[disabled]="isSaveDisabled">
Update description
</button>
</div>
</div>

View File

@@ -0,0 +1,26 @@
:host {
margin-top: 0.5em;
span.placeholder {
color: gray;
display: inline-block;
.glyphicon {
color: black;
}
}
span.editable,
span.placeholder {
cursor: pointer;
}
textarea.text-editor {
display: block;
width: 100%;
}
.buttons {
margin-top: 0.5em;
}
}

View File

@@ -0,0 +1,82 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'editable-text',
templateUrl: './editable-text.component.html',
styleUrls: ['./editable-text.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => EditableTextComponent), multi: true}
]
})
export class EditableTextComponent implements ControlValueAccessor {
@Input() placeholder = 'No editable content';
text: string;
originalText: string;
editing = false;
get isSaveDisabled(): boolean {
return !this.text || this.text.trim().length === 0;
}
get displayText(): string {
return this.text && this.text.replace(/\n/g, '<br>');
}
constructor(private cdr: ChangeDetectorRef) {
}
writeValue(obj: any): void {
if (this.text === obj) {
return;
}
this.text = obj;
this.cdr.markForCheck();
}
registerOnChange(fn: any): void {
this.textChange = fn;
}
registerOnTouched(fn: any): void {
}
saveText(): void {
this.originalText = null;
this.editing = false;
this.textChange(this.text);
}
editText(): void {
this.originalText = this.text;
this.editing = true;
}
cancelEditText(): void {
this.text = this.originalText;
this.editing = false;
}
keydownEnter(): void {
if (this.isSaveDisabled) {
return;
}
this.saveText();
}
get showPlaceholder(): boolean {
return !this.editing && !this.text;
}
get showText(): boolean {
return !this.editing && !!this.text;
}
private textChange: any = () => {
}
}

View File

@@ -7,8 +7,11 @@
[selectedKey]="selectedKey" [selectedKey]="selectedKey"
[selected]="selectedKey?.layerId === index" [selected]="selectedKey?.layerId === index"
[keyboardLayout]="keyboardLayout" [keyboardLayout]="keyboardLayout"
[description]="description"
[showDescription]="true"
(keyClick)="keyClick.emit($event)" (keyClick)="keyClick.emit($event)"
(keyHover)="keyHover.emit($event)" (keyHover)="keyHover.emit($event)"
(capture)="capture.emit($event)" (capture)="capture.emit($event)"
(descriptionChanged)="descriptionChanged.emit($event)"
> >
</svg-keyboard> </svg-keyboard>

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 809 B

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { animate, keyframes, state, style, transition, trigger } from '@angular/animations'; import { animate, keyframes, state, style, transition, trigger } from '@angular/animations';
import { Layer } from 'uhk-common'; import { Layer } from 'uhk-common';
@@ -81,11 +81,14 @@ export class KeyboardSliderComponent implements OnChanges {
@Input() halvesSplit: boolean; @Input() halvesSplit: boolean;
@Input() selectedKey: { layerId: number, moduleId: number, keyId: number }; @Input() selectedKey: { layerId: number, moduleId: number, keyId: number };
@Input() keyboardLayout = KeyboardLayout.ANSI; @Input() keyboardLayout = KeyboardLayout.ANSI;
@Input() description: string;
@Output() keyClick = new EventEmitter(); @Output() keyClick = new EventEmitter();
@Output() keyHover = new EventEmitter(); @Output() keyHover = new EventEmitter();
@Output() capture = new EventEmitter(); @Output() capture = new EventEmitter();
@Output() descriptionChanged = new EventEmitter<string>();
layerAnimationState: AnimationKeyboard[]; layerAnimationState: AnimationKeyboard[];
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
if (changes['layers']) { if (changes['layers']) {
this.layerAnimationState = this.layers.map<AnimationKeyboard>(() => 'initOut'); this.layerAnimationState = this.layers.map<AnimationKeyboard>(() => 'initOut');

View File

@@ -4,7 +4,8 @@
(downloadClick)="downloadKeymap()"></keymap-header> (downloadClick)="downloadKeymap()"></keymap-header>
<svg-keyboard-wrap [keymap]="keymap$ | async" <svg-keyboard-wrap [keymap]="keymap$ | async"
[halvesSplit]="keyboardSplit" [halvesSplit]="keyboardSplit"
[keyboardLayout]="keyboardLayout$ | async"></svg-keyboard-wrap> [keyboardLayout]="keyboardLayout$ | async"
(descriptionChanged)="descriptionChanged($event)"></svg-keyboard-wrap>
</ng-template> </ng-template>
<div *ngIf="!(keymap$ | async)" class="not-found"> <div *ngIf="!(keymap$ | async)" class="not-found">

View File

@@ -18,6 +18,8 @@ import { AppState, getKeyboardLayout } from '../../../store';
import { getKeymap, getKeymaps, getUserConfiguration } from '../../../store/reducers/user-configuration'; import { getKeymap, getKeymaps, getUserConfiguration } from '../../../store/reducers/user-configuration';
import { SvgKeyboardWrapComponent } from '../../svg/wrap'; import { SvgKeyboardWrapComponent } from '../../svg/wrap';
import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum'; import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum';
import { KeymapActions } from '../../../store/actions';
import { ChangeKeymapDescription } from '../../../models/ChangeKeymapDescription';
@Component({ @Component({
selector: 'keymap-edit', selector: 'keymap-edit',
@@ -64,7 +66,7 @@ export class KeymapEditComponent {
const keymap = latest[0]; const keymap = latest[0];
const exportableJSON = latest[1]; const exportableJSON = latest[1];
const fileName = keymap.name + '_keymap.json'; const fileName = keymap.name + '_keymap.json';
saveAs(new Blob([exportableJSON], { type: 'application/json' }), fileName); saveAs(new Blob([exportableJSON], {type: 'application/json'}), fileName);
}); });
} }
@@ -73,6 +75,10 @@ export class KeymapEditComponent {
this.keyboardSplit = !this.keyboardSplit; this.keyboardSplit = !this.keyboardSplit;
} }
descriptionChanged(event: ChangeKeymapDescription): void {
this.store.dispatch(new KeymapActions.EditDescriptionAction(event));
}
private toExportableJSON(keymap: Keymap): Observable<any> { private toExportableJSON(keymap: Keymap): Observable<any> {
return this.store return this.store
.let(getUserConfiguration()) .let(getUserConfiguration())

View File

@@ -13,5 +13,5 @@
</ng-template> </ng-template>
<div *ngIf="!macro" class="not-found"> <div *ngIf="!macro" class="not-found">
There is no macro with id {{ route.params.select('id') | async }}. There is no macro with id {{ macroId }}.
</div> </div>

View File

@@ -7,7 +7,7 @@ import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/pluck'; import 'rxjs/add/operator/pluck';
import { MacroActions } from '../../../store/actions'; import { MacroActions } from '../../../store/actions';
import { AppState } from '../../../store/index'; import { AppState } from '../../../store';
import { getMacro } from '../../../store/reducers/user-configuration'; import { getMacro } from '../../../store/reducers/user-configuration';
@Component({ @Component({
@@ -21,13 +21,17 @@ import { getMacro } from '../../../store/reducers/user-configuration';
export class MacroEditComponent implements OnDestroy { export class MacroEditComponent implements OnDestroy {
macro: Macro; macro: Macro;
isNew: boolean; isNew: boolean;
macroId: number;
private subscription: Subscription; private subscription: Subscription;
constructor(private store: Store<AppState>, public route: ActivatedRoute) { constructor(private store: Store<AppState>, public route: ActivatedRoute) {
this.subscription = route this.subscription = route
.params .params
.pluck<{}, string>('id') .pluck<{}, string>('id')
.switchMap((id: string) => store.let(getMacro(+id))) .switchMap((id: string) => {
this.macroId = +id;
return store.let(getMacro(this.macroId));
})
.subscribe((macro: Macro) => { .subscribe((macro: Macro) => {
this.macro = macro; this.macro = macro;
}); });

View File

@@ -7,6 +7,10 @@
[width]="200" [width]="200"
[options]="options" [options]="options"
></select2> ></select2>
<icon name="question-circle"
data-toggle="tooltip"
title="Looking for a non-US character, but can't find it? Please note that USB keyboards send scancodes, not characters to your computer. Then your operating system translates the scancodes to characters according to your current OS keyboard layout. This means that you have to select the US-equivalent character of the desired key in Agent."
data-placement="bottom"></icon>
<capture-keystroke-button (capture)="onKeysCapture($event)" tabindex="0"></capture-keystroke-button> <capture-keystroke-button (capture)="onKeysCapture($event)" tabindex="0"></capture-keystroke-button>
</div> </div>
<div class="modifier-options"> <div class="modifier-options">

View File

@@ -10,6 +10,10 @@
position: relative; position: relative;
top: 2px; top: 2px;
} }
icon {
display: inline-block;
};
} }
.modifier-options { .modifier-options {

View File

@@ -164,12 +164,12 @@ ul {
padding: 0; padding: 0;
margin: 0 0.25rem; margin: 0 0.25rem;
text-overflow: ellipsis; text-overflow: ellipsis;
background-color: inherit; background-color: transparent;
&:focus { &:focus {
box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc; box-shadow: 0 0 0 1px #ccc, 0 0 5px 0 #ccc;
border-color: transparent; border-color: transparent;
background-color: inherit; background-color: transparent;
} }
} }
} }

View File

@@ -56,17 +56,8 @@ export class SideMenuComponent implements AfterContentInit, OnDestroy {
addon: 'active' addon: 'active'
}; };
this.keymaps$ = store.let(getKeymaps()) this.keymaps$ = store.let(getKeymaps());
.map(keymaps => keymaps.slice()) // Creating a new array reference, because the sort is working in place this.macros$ = store.let(getMacros());
.do((keymaps: Keymap[]) => {
keymaps.sort((first: Keymap, second: Keymap) => first.name.localeCompare(second.name));
});
this.macros$ = store.let(getMacros())
.map(macros => macros.slice()) // Creating a new array reference, because the sort is working in place
.do((macros: Macro[]) => {
macros.sort((first: Macro, second: Macro) => first.name.localeCompare(second.name));
});
this.showAddonMenu$ = this.store.select(showAddonMenu); this.showAddonMenu$ = this.store.select(showAddonMenu);
this.runInElectron$ = this.store.select(runningInElectron); this.runInElectron$ = this.store.select(runningInElectron);

View File

@@ -1,16 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" [attr.viewBox]="viewBox" height="100%" width="100%"> <svg xmlns="http://www.w3.org/2000/svg" [attr.viewBox]="viewBox" height="100%" width="100%">
<svg:g svg-module *ngFor="let module of modules; let i = index" <svg:g svg-module *ngFor="let module of modules; let i = index"
[coverages]="module.coverages" [coverages]="module.coverages"
[keyboardKeys]="module.keyboardKeys" [keyboardKeys]="module.keyboardKeys"
[keybindAnimationEnabled]="keybindAnimationEnabled" [keybindAnimationEnabled]="keybindAnimationEnabled"
[capturingEnabled]="capturingEnabled" [capturingEnabled]="capturingEnabled"
[attr.transform]="module.attributes.transform" [attr.transform]="module.attributes.transform"
[keyActions]="moduleConfig[i].keyActions" [keyActions]="moduleConfig[i].keyActions"
[selectedKey]="selectedKey" [selectedKey]="selectedKey"
[@split]="moduleAnimationStates[i]" [@split]="moduleAnimationStates[i]"
[selected]="selectedKey?.moduleId === i" [selected]="selectedKey?.moduleId === i"
(keyClick)="onKeyClick(i, $event.index, $event.keyTarget)" (keyClick)="onKeyClick(i, $event.index, $event.keyTarget)"
(keyHover)="onKeyHover($event.index, $event.event, $event.over, i)" (keyHover)="onKeyHover($event.index, $event.event, $event.over, i)"
(capture)="onCapture(i, $event.index, $event.captured)" (capture)="onCapture(i, $event.index, $event.captured)"
/> />
</svg> </svg>
<editable-text *ngIf="showDescription"
[ngModel]="description"
(ngModelChange)="descriptionChanged.emit($event)"
placeholder="No description provided for this keymap."></editable-text>

Before

Width:  |  Height:  |  Size: 854 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,5 +1,10 @@
:host { :host {
display: flex; display: block;
width: 100%; width: 100%;
position: relative; position: relative;
} }
editable-text {
padding-left: 2em;
padding-right: 2em;
display: block;
}

View File

@@ -31,9 +31,12 @@ export class SvgKeyboardComponent implements OnInit {
@Input() selected: boolean; @Input() selected: boolean;
@Input() halvesSplit: boolean; @Input() halvesSplit: boolean;
@Input() keyboardLayout = KeyboardLayout.ANSI; @Input() keyboardLayout = KeyboardLayout.ANSI;
@Input() description: string;
@Input() showDescription = false;
@Output() keyClick = new EventEmitter(); @Output() keyClick = new EventEmitter();
@Output() keyHover = new EventEmitter(); @Output() keyHover = new EventEmitter();
@Output() capture = new EventEmitter(); @Output() capture = new EventEmitter();
@Output() descriptionChanged = new EventEmitter<string>();
modules: SvgModule[]; modules: SvgModule[];
viewBox: string; viewBox: string;

View File

@@ -7,9 +7,11 @@
[selectedKey]="selectedKey" [selectedKey]="selectedKey"
[halvesSplit]="halvesSplit" [halvesSplit]="halvesSplit"
[keyboardLayout]="keyboardLayout" [keyboardLayout]="keyboardLayout"
[description]="keymap.description"
(keyClick)="onKeyClick($event.moduleId, $event.keyId, $event.keyTarget)" (keyClick)="onKeyClick($event.moduleId, $event.keyId, $event.keyTarget)"
(keyHover)="onKeyHover($event.moduleId, $event.event, $event.over, $event.keyId)" (keyHover)="onKeyHover($event.moduleId, $event.event, $event.over, $event.keyId)"
(capture)="onCapture($event.moduleId, $event.keyId, $event.captured)" (capture)="onCapture($event.moduleId, $event.keyId, $event.captured)"
(descriptionChanged)="onDescriptionChanged($event)"
></keyboard-slider> ></keyboard-slider>
<popover tabindex="0" [visible]="popoverShown" [keyPosition]="keyPosition" [wrapPosition]="wrapPosition" [defaultKeyAction]="popoverInitKeyAction" <popover tabindex="0" [visible]="popoverShown" [keyPosition]="keyPosition" [wrapPosition]="wrapPosition" [defaultKeyAction]="popoverInitKeyAction"
[currentKeymap]="keymap" [currentLayer]="currentLayer" (cancel)="hidePopover()" (remap)="onRemap($event)"></popover> [currentKeymap]="keymap" [currentLayer]="currentLayer" (cancel)="hidePopover()" (remap)="onRemap($event)"></popover>

View File

@@ -2,14 +2,16 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
ElementRef, ElementRef,
Renderer, EventEmitter,
HostBinding, HostBinding,
HostListener, HostListener,
Input, Input,
OnChanges, OnChanges,
OnInit, OnInit,
ViewChild, Output,
SimpleChanges Renderer,
SimpleChanges,
ViewChild
} from '@angular/core'; } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
@@ -25,10 +27,10 @@ import {
KeystrokeAction, KeystrokeAction,
Layer, Layer,
LayerName, LayerName,
SecondaryRoleAction,
MouseAction, MouseAction,
MouseActionParam, MouseActionParam,
PlayMacroAction, PlayMacroAction,
SecondaryRoleAction,
SwitchKeymapAction, SwitchKeymapAction,
SwitchLayerAction SwitchLayerAction
} from 'uhk-common'; } from 'uhk-common';
@@ -38,6 +40,7 @@ import { AppState } from '../../../store';
import { KeymapActions } from '../../../store/actions'; import { KeymapActions } from '../../../store/actions';
import { PopoverComponent } from '../../popover'; import { PopoverComponent } from '../../popover';
import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum'; import { KeyboardLayout } from '../../../keyboard/keyboard-layout.enum';
import { ChangeKeymapDescription } from '../../../models/ChangeKeymapDescription';
interface NameValuePair { interface NameValuePair {
name: string; name: string;
@@ -56,6 +59,7 @@ export class SvgKeyboardWrapComponent implements OnInit, OnChanges {
@Input() tooltipEnabled: boolean = false; @Input() tooltipEnabled: boolean = false;
@Input() halvesSplit: boolean; @Input() halvesSplit: boolean;
@Input() keyboardLayout: KeyboardLayout.ANSI; @Input() keyboardLayout: KeyboardLayout.ANSI;
@Output() descriptionChanged = new EventEmitter<ChangeKeymapDescription>();
@ViewChild(PopoverComponent, { read: ElementRef }) popover: ElementRef; @ViewChild(PopoverComponent, { read: ElementRef }) popover: ElementRef;
@@ -237,6 +241,13 @@ export class SvgKeyboardWrapComponent implements OnInit, OnChanges {
return this.currentLayer; return this.currentLayer;
} }
onDescriptionChanged(description: string): void {
this.descriptionChanged.emit({
description,
abbr: this.keymap.abbreviation
});
}
private getKeyActionContent(keyAction: KeyAction): Observable<NameValuePair[]> { private getKeyActionContent(keyAction: KeyAction): Observable<NameValuePair[]> {
if (keyAction instanceof KeystrokeAction) { if (keyAction instanceof KeystrokeAction) {
const keystrokeAction: KeystrokeAction = keyAction; const keystrokeAction: KeystrokeAction = keyAction;

View File

@@ -3,7 +3,7 @@ import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { Notification } from 'uhk-common'; import { Notification } from 'uhk-common';
import { AppState, getUndoableNotification } from '../../store/index'; import { AppState, getUndoableNotification } from '../../store';
import { DismissUndoNotificationAction, UndoLastAction } from '../../store/actions/app'; import { DismissUndoNotificationAction, UndoLastAction } from '../../store/actions/app';
@Component({ @Component({

View File

@@ -0,0 +1,16 @@
import { AfterViewInit, Directive, ElementRef } from '@angular/core';
@Directive({
selector: '[autofocus]'
})
export class Autofocus implements AfterViewInit {
constructor(private el: ElementRef) {
}
ngOnInit() {
}
ngAfterViewInit() {
this.el.nativeElement.focus();
}
}

View File

@@ -0,0 +1,4 @@
export interface ChangeKeymapDescription {
abbr: string;
description: string;
}

File diff suppressed because it is too large Load Diff

View File

@@ -101,6 +101,8 @@ 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 { XtermComponent } from './components/xterm/xterm.component'; 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 { Autofocus } from './directives/autofocus/autofocus.directive';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -169,7 +171,9 @@ import { SliderWrapperComponent } from './components/slider-wrapper/slider-wrapp
ProgressButtonComponent, ProgressButtonComponent,
LoadingDevicePageComponent, LoadingDevicePageComponent,
XtermComponent, XtermComponent,
SliderWrapperComponent SliderWrapperComponent,
EditableTextComponent,
Autofocus
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@@ -1,6 +1,7 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { KeyAction, Keymap, Macro } from 'uhk-common'; import { KeyAction, Keymap, Macro } from 'uhk-common';
import { UndoUserConfigData } from '../../models/undo-user-config-data'; import { UndoUserConfigData } from '../../models/undo-user-config-data';
import { ChangeKeymapDescription } from '../../models/ChangeKeymapDescription';
export type KeymapAction = export type KeymapAction =
KeymapActions.AddKeymapAction | KeymapActions.AddKeymapAction |
@@ -11,7 +12,8 @@ export type KeymapAction =
KeymapActions.SetDefaultAction | KeymapActions.SetDefaultAction |
KeymapActions.RemoveKeymapAction | KeymapActions.RemoveKeymapAction |
KeymapActions.SaveKeyAction | KeymapActions.SaveKeyAction |
KeymapActions.CheckMacroAction; KeymapActions.CheckMacroAction |
KeymapActions.EditDescriptionAction;
export namespace KeymapActions { export namespace KeymapActions {
export const ADD = '[Keymap] Add keymap'; export const ADD = '[Keymap] Add keymap';
@@ -98,6 +100,16 @@ export namespace KeymapActions {
payload: UndoUserConfigData payload: UndoUserConfigData
}; };
export const EDIT_DESCRIPTION = '[Keymap] Edit description';
export class EditDescriptionAction {
type = EDIT_DESCRIPTION;
constructor(public payload: ChangeKeymapDescription) {
}
}
export function loadKeymaps(): Action { export function loadKeymaps(): Action {
return { return {
type: KeymapActions.LOAD_KEYMAPS type: KeymapActions.LOAD_KEYMAPS

View File

@@ -35,10 +35,12 @@ import { ShowNotificationAction } from '../actions/app';
import { AppState } from '../index'; import { AppState } from '../index';
import { import {
ActionTypes as UserConfigActions, ActionTypes as UserConfigActions,
ApplyUserConfigurationFromFileAction,
LoadConfigFromDeviceAction, LoadConfigFromDeviceAction,
LoadResetUserConfigurationAction LoadResetUserConfigurationAction
} from '../actions/user-config'; } from '../actions/user-config';
import { DefaultUserConfigurationService } from '../../services/default-user-configuration.service'; import { DefaultUserConfigurationService } from '../../services/default-user-configuration.service';
import { DataStorageRepositoryService } from '../../services/datastorage-repository.service';
@Injectable() @Injectable()
export class DeviceEffects { export class DeviceEffects {
@@ -162,8 +164,16 @@ export class DeviceEffects {
}); });
@Effect() saveResetUserConfigurationToDevice$ = this.actions$ @Effect() saveResetUserConfigurationToDevice$ = this.actions$
.ofType(UserConfigActions.LOAD_RESET_USER_CONFIGURATION, UserConfigActions.APPLY_USER_CONFIGURATION_FROM_FILE) .ofType<ApplyUserConfigurationFromFileAction
.switchMap(() => Observable.of(new SaveConfigurationAction())); | LoadResetUserConfigurationAction>(
UserConfigActions.LOAD_RESET_USER_CONFIGURATION,
UserConfigActions.APPLY_USER_CONFIGURATION_FROM_FILE)
.map(action => action.payload)
.switchMap((config: UserConfiguration) => {
this.dataStorageRepository.saveConfig(config);
return Observable.of(new SaveConfigurationAction());
});
@Effect({dispatch: false}) updateFirmware$ = this.actions$ @Effect({dispatch: false}) updateFirmware$ = this.actions$
.ofType<UpdateFirmwareAction>(ActionTypes.UPDATE_FIRMWARE) .ofType<UpdateFirmwareAction>(ActionTypes.UPDATE_FIRMWARE)
@@ -193,6 +203,7 @@ export class DeviceEffects {
private router: Router, private router: Router,
private deviceRendererService: DeviceRendererService, private deviceRendererService: DeviceRendererService,
private store: Store<AppState>, private store: Store<AppState>,
private dataStorageRepository: DataStorageRepositoryService,
private defaultUserConfigurationService: DefaultUserConfigurationService) { private defaultUserConfigurationService: DefaultUserConfigurationService) {
} }

View File

@@ -7,14 +7,17 @@ import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/do'; import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
import 'rxjs/add/operator/pairwise';
import 'rxjs/add/operator/startWith'; import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/withLatestFrom'; import 'rxjs/add/operator/withLatestFrom';
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import { Keymap } from 'uhk-common'; import { Keymap } from 'uhk-common';
import { findNewItem } from '../../util';
import { KeymapActions } from '../actions'; import { KeymapActions } from '../actions';
import { AppState } from '../index'; import { AppState } from '../index';
import { getKeymaps } from '../reducers/user-configuration';
@Injectable() @Injectable()
export class KeymapEffects { export class KeymapEffects {
@@ -32,10 +35,10 @@ export class KeymapEffects {
@Effect({ dispatch: false }) addOrDuplicate$: any = this.actions$ @Effect({ dispatch: false }) addOrDuplicate$: any = this.actions$
.ofType(KeymapActions.ADD, KeymapActions.DUPLICATE) .ofType(KeymapActions.ADD, KeymapActions.DUPLICATE)
.withLatestFrom(this.store) .withLatestFrom(this.store.let(getKeymaps()).pairwise(), (action, latest) => latest)
.map(latest => latest[1].userConfiguration.keymaps) .do(([prevKeymaps, newKeymaps]) => {
.do(keymaps => { const newKeymap = findNewItem(prevKeymaps, newKeymaps);
this.router.navigate(['/keymap', keymaps[keymaps.length - 1].abbreviation]); this.router.navigate(['/keymap', newKeymap.abbreviation]);
}); });
@Effect({ dispatch: false }) remove$: any = this.actions$ @Effect({ dispatch: false }) remove$: any = this.actions$

View File

@@ -2,14 +2,18 @@ import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Actions, Effect } from '@ngrx/effects'; import { Actions, Effect } from '@ngrx/effects';
import { Store } from '@ngrx/store'; import { Store, Action } from '@ngrx/store';
import 'rxjs/add/operator/do'; import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
import 'rxjs/add/operator/pairwise';
import 'rxjs/add/operator/withLatestFrom'; import 'rxjs/add/operator/withLatestFrom';
import { Macro } from 'uhk-common';
import { KeymapActions, MacroActions } from '../actions'; import { KeymapActions, MacroActions } from '../actions';
import { AppState } from '../index'; import { AppState } from '../index';
import { getMacros } from '../reducers/user-configuration';
import { findNewItem } from '../../util';
@Injectable() @Injectable()
export class MacroEffects { export class MacroEffects {
@@ -27,23 +31,17 @@ export class MacroEffects {
} }
}); });
@Effect({ dispatch: false }) add$: any = this.actions$ @Effect({ dispatch: false }) addOrDuplicate$: any = this.actions$
.ofType(MacroActions.ADD) .ofType(MacroActions.ADD, MacroActions.DUPLICATE)
.withLatestFrom(this.store) .withLatestFrom(this.store.let(getMacros()).pairwise(), (action, latest) => ([action, latest[0], latest[1]]))
.map(([action, state]) => state.userConfiguration.macros) .do(([action, prevMacros, newMacros]: [Action, Macro[], Macro[]]) => {
.map(macros => macros[macros.length - 1]) const newMacro = findNewItem(prevMacros, newMacros);
.do(lastMacro => { const commands = ['/macro', newMacro.id];
this.router.navigate(['/macro', lastMacro.id, 'new']); if (action.type === MacroActions.ADD) {
commands.push('new');
}
this.router.navigate(commands);
}); });
@Effect({ dispatch: false }) duplicate: any = this.actions$ constructor(private actions$: Actions, private router: Router, private store: Store<AppState>) { }
.ofType(MacroActions.DUPLICATE)
.withLatestFrom(this.store)
.map(([action, state]) => state.userConfiguration.macros)
.map(macros => macros[macros.length - 1])
.do(lastMacro => {
this.router.navigate(['/macro', lastMacro.id]);
});
constructor(private actions$: Actions, private router: Router, private store: Store<AppState>) {}
} }

View File

@@ -81,7 +81,7 @@ export class UserConfigEffects {
@Effect() saveUserConfig$: Observable<Action> = (this.actions$ @Effect() saveUserConfig$: Observable<Action> = (this.actions$
.ofType( .ofType(
KeymapActions.ADD, KeymapActions.DUPLICATE, KeymapActions.EDIT_NAME, KeymapActions.EDIT_ABBR, KeymapActions.ADD, KeymapActions.DUPLICATE, KeymapActions.EDIT_NAME, KeymapActions.EDIT_ABBR,
KeymapActions.SET_DEFAULT, KeymapActions.REMOVE, KeymapActions.SAVE_KEY, KeymapActions.SET_DEFAULT, KeymapActions.REMOVE, KeymapActions.SAVE_KEY, KeymapActions.EDIT_DESCRIPTION,
MacroActions.ADD, MacroActions.DUPLICATE, MacroActions.EDIT_NAME, MacroActions.REMOVE, MacroActions.ADD_ACTION, MacroActions.ADD, MacroActions.DUPLICATE, MacroActions.EDIT_NAME, MacroActions.REMOVE, MacroActions.ADD_ACTION,
MacroActions.SAVE_ACTION, MacroActions.DELETE_ACTION, MacroActions.REORDER_ACTION, MacroActions.SAVE_ACTION, MacroActions.DELETE_ACTION, MacroActions.REORDER_ACTION,
ActionTypes.RENAME_USER_CONFIGURATION, ActionTypes.SET_USER_CONFIGURATION_VALUE) as ActionTypes.RENAME_USER_CONFIGURATION, ActionTypes.SET_USER_CONFIGURATION_VALUE) as

View File

@@ -7,7 +7,7 @@ export const initialState: Keymap[] = [];
export function reducer(state = initialState, action: KeymapAction): Keymap[] { export function reducer(state = initialState, action: KeymapAction): Keymap[] {
switch (action.type) { switch (action.type) {
case KeymapActions.LOAD_KEYMAPS_SUCCESS: { case KeymapActions.LOAD_KEYMAPS_SUCCESS: {
return action.payload; return (action as KeymapActions.LoadKeymapSuccessAction).payload ;
} }
default: default:

View File

@@ -4,7 +4,18 @@ import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/map';
import { KeyAction, KeyActionHelper, Keymap, Layer, Macro, Module, SwitchLayerAction, UserConfiguration } from 'uhk-common'; import {
KeyAction,
KeyActionHelper,
Keymap,
Layer,
Macro,
Module,
NoneAction,
PlayMacroAction,
SwitchLayerAction,
UserConfiguration
} from 'uhk-common';
import { KeymapActions, MacroActions } from '../actions'; import { KeymapActions, MacroActions } from '../actions';
import { AppState } from '../index'; import { AppState } from '../index';
import { ActionTypes } from '../actions/user-config'; import { ActionTypes } from '../actions/user-config';
@@ -19,7 +30,12 @@ export function reducer(state = initialState, action: Action & { payload?: any }
case ActionTypes.APPLY_USER_CONFIGURATION_FROM_FILE: case ActionTypes.APPLY_USER_CONFIGURATION_FROM_FILE:
case ActionTypes.LOAD_RESET_USER_CONFIGURATION: case ActionTypes.LOAD_RESET_USER_CONFIGURATION:
case ActionTypes.LOAD_USER_CONFIG_SUCCESS: { case ActionTypes.LOAD_USER_CONFIG_SUCCESS: {
return Object.assign(changedUserConfiguration, action.payload); Object.assign(changedUserConfiguration, action.payload);
changedUserConfiguration.keymaps = [...changedUserConfiguration.keymaps];
changedUserConfiguration.keymaps.sort((first: Keymap, second: Keymap) => first.name.localeCompare(second.name));
changedUserConfiguration.macros = [...changedUserConfiguration.macros];
changedUserConfiguration.macros.sort((first: Macro, second: Macro) => first.name.localeCompare(second.name));
return changedUserConfiguration;
} }
case KeymapActions.ADD: case KeymapActions.ADD:
@@ -29,7 +45,7 @@ export function reducer(state = initialState, action: Action & { payload?: any }
newKeymap.name = generateName(state.keymaps, newKeymap.name); newKeymap.name = generateName(state.keymaps, newKeymap.name);
newKeymap.isDefault = (state.keymaps.length === 0); newKeymap.isDefault = (state.keymaps.length === 0);
changedUserConfiguration.keymaps = state.keymaps.concat(newKeymap); changedUserConfiguration.keymaps = insertItemInNameOrder(state.keymaps, newKeymap);
break; break;
} }
case KeymapActions.EDIT_NAME: { case KeymapActions.EDIT_NAME: {
@@ -38,20 +54,27 @@ export function reducer(state = initialState, action: Action & { payload?: any }
} }
const name: string = action.payload.name.trim(); const name: string = action.payload.name.trim();
let keymapToRename: Keymap = null;
const duplicate = state.keymaps.some((keymap: Keymap) => { const duplicate = state.keymaps.some((keymap: Keymap) => {
if (keymap.abbreviation === action.payload.abbr) {
keymapToRename = keymap;
}
return keymap.name === name && keymap.abbreviation !== action.payload.abbr; return keymap.name === name && keymap.abbreviation !== action.payload.abbr;
}); });
changedUserConfiguration.keymaps = state.keymaps.map((keymap: Keymap) => { if (duplicate) {
keymap = Object.assign(new Keymap(), keymap); break;
}
if (!duplicate && keymap.abbreviation === action.payload.abbr) { const newKeymap = Object.assign(new Keymap(), keymapToRename, { name });
keymap.name = name;
}
return keymap;
});
changedUserConfiguration.keymaps = insertItemInNameOrder(
state.keymaps,
newKeymap,
keymap => keymap.abbreviation !== newKeymap.abbreviation
);
break; break;
} }
case KeymapActions.EDIT_ABBR: { case KeymapActions.EDIT_ABBR: {
@@ -163,7 +186,7 @@ export function reducer(state = initialState, action: Action & { payload?: any }
newMacro.isPrivate = true; newMacro.isPrivate = true;
newMacro.macroActions = []; newMacro.macroActions = [];
changedUserConfiguration.macros = state.macros.concat(newMacro); changedUserConfiguration.macros = insertItemInNameOrder(state.macros, newMacro);
break; break;
} }
case MacroActions.DUPLICATE: { case MacroActions.DUPLICATE: {
@@ -171,7 +194,7 @@ export function reducer(state = initialState, action: Action & { payload?: any }
newMacro.name = generateName(state.macros, newMacro.name); newMacro.name = generateName(state.macros, newMacro.name);
newMacro.id = generateMacroId(state.macros); newMacro.id = generateMacroId(state.macros);
changedUserConfiguration.macros = state.macros.concat(newMacro); changedUserConfiguration.macros = insertItemInNameOrder(state.macros, newMacro);
break; break;
} }
case MacroActions.EDIT_NAME: { case MacroActions.EDIT_NAME: {
@@ -180,25 +203,52 @@ export function reducer(state = initialState, action: Action & { payload?: any }
} }
const name: string = action.payload.name.trim(); const name: string = action.payload.name.trim();
let macroToRename: Macro = null;
const duplicate = state.macros.some((macro: Macro) => { const duplicate = state.macros.some((macro: Macro) => {
if (macro.id === action.payload.id) {
macroToRename = macro;
}
return macro.id !== action.payload.id && macro.name === name; return macro.id !== action.payload.id && macro.name === name;
}); });
changedUserConfiguration.macros = state.macros.map((macro: Macro) => { if (duplicate) {
macro = Object.assign(new Macro(), macro); break;
if (!duplicate && macro.id === action.payload.id) { }
macro.name = name;
}
return macro;
});
const newMacro = Object.assign(new Macro(), macroToRename, { name });
changedUserConfiguration.macros = insertItemInNameOrder(state.macros, newMacro, macro => macro.id !== newMacro.id);
break; break;
} }
case MacroActions.REMOVE: case MacroActions.REMOVE:
changedUserConfiguration.macros = state.macros.filter((macro: Macro) => macro.id !== action.payload); const macroId = action.payload;
changedUserConfiguration.macros = state.macros.filter((macro: Macro) => macro.id !== macroId);
for (let k = 0; k < changedUserConfiguration.keymaps.length; k++) {
const keymap = changedUserConfiguration.keymaps[k];
let hasChanges = false;
for (const layer of keymap.layers) {
for (const module of layer.modules) {
for (let ka = 0; ka < module.keyActions.length; ka++) {
const keyAction = module.keyActions[ka];
if (keyAction instanceof PlayMacroAction && keyAction.macroId === macroId) {
hasChanges = true;
module.keyActions[ka] = new NoneAction();
}
}
}
}
if (hasChanges) {
changedUserConfiguration.keymaps[k] = new Keymap(keymap);
}
}
break; break;
case MacroActions.ADD_ACTION: case MacroActions.ADD_ACTION:
changedUserConfiguration.macros = state.macros.map((macro: Macro) => { changedUserConfiguration.macros = state.macros.map((macro: Macro) => {
if (macro.id === action.payload.id) { if (macro.id === action.payload.id) {
@@ -263,6 +313,18 @@ export function reducer(state = initialState, action: Action & { payload?: any }
break; break;
} }
case KeymapActions.EDIT_DESCRIPTION: {
const data = (action as KeymapActions.EditDescriptionAction).payload;
changedUserConfiguration.keymaps = state.keymaps.map(keymap => {
if (keymap.abbreviation === data.abbr) {
keymap.description = data.description;
}
return keymap;
});
break;
}
default: default:
break; break;
} }
@@ -360,6 +422,27 @@ function generateMacroId(macros: Macro[]) {
return newId + 1; return newId + 1;
} }
function insertItemInNameOrder<T extends { name: string }>(
items: T[], newItem: T, keepItem: (item: T) => boolean = () => true
): T[] {
const newItems: T[] = [];
let added = false;
for (const item of items) {
if (!added && item.name.localeCompare(newItem.name) > 0) {
newItems.push(newItem);
added = true;
}
if (keepItem(item)) {
newItems.push(item);
}
}
if (!added) {
newItems.push(newItem);
}
return newItems;
}
function checkExistence(layers: Layer[], property: string, value: any): Layer[] { function checkExistence(layers: Layer[], property: string, value: any): Layer[] {
const keyActionsToClear: { const keyActionsToClear: {
layerIdx: number, layerIdx: number,

View File

@@ -0,0 +1,9 @@
export function findNewItem<T>(oldItems: T[], newItems: T[]): T {
for (let i = 0; i < oldItems.length; ++i) {
if (oldItems[i] !== newItems[i]) {
return newItems[i];
}
}
return newItems[newItems.length - 1];
}

View File

@@ -1,3 +1,4 @@
export * from './find-new-item';
export * from './html-helper'; export * from './html-helper';
export * from './validators'; export * from './validators';
export * from './version-helper'; export * from './version-helper';

View File

@@ -1,4 +1,4 @@
import { cloneDeep } from 'lodash'; import cloneDeep from 'lodash-es/cloneDeep';
const defaultUserConfig = { const defaultUserConfig = {
userConfigMajorVersion: 3, userConfigMajorVersion: 3,

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env node #!/usr/bin/env node
const path = require('path');
const uhk = require('./uhk'); const uhk = require('./uhk');
const device = uhk.getUhkDevice(); const device = uhk.getUhkDevice();
let buffer = new Buffer(uhk.pushUint32([uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.i2cBaudRate])); (async function() {
//console.log(buffer); let response = await uhk.writeDevice(device, [uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.i2cBaudRate]);
device.write(uhk.getTransferData(buffer)); let requestedBaudRate = uhk.getUint32(response, 2);
let response = device.readSync(); let actualBaudRate = uhk.getUint32(response, 6);
//console.log(Buffer.from(response)); let i2c0F = response[1].toString(2).padStart(8, '0');
let requestedBaudRate = uhk.getUint32(response, 2); console.log(`requestedBaudRate:${requestedBaudRate} | actualBaudRate:${actualBaudRate} | I2C0_F:0b${i2c0F}`)
let actualBaudRate = uhk.getUint32(response, 6); })();
console.log(`requestedBaudRate:${requestedBaudRate} | actualBaudRate:${actualBaudRate} | I2C0_F:0b${response[1].toString(2).padStart(8, '0')}`)

View File

@@ -51,13 +51,13 @@ function convertMs(milliseconds) {
const device = uhk.getUhkDevice(); const device = uhk.getUhkDevice();
device.write(uhk.getTransferData(new Buffer(uhk.pushUint32([uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.uptime])))); device.write(uhk.getTransferData(new Buffer([uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.uptime])));
let response = device.readSync(); let response = device.readSync();
let uptimeMs = uhk.getUint32(response, 1); let uptimeMs = uhk.getUint32(response, 1);
let uptime = convertMs(uptimeMs); let uptime = convertMs(uptimeMs);
console.log(`uptime: ${uptime.days}d ${String(uptime.hours).padStart(2, '0')}:${String(uptime.minutes).padStart(2, '0')}:${String(uptime.seconds).padStart(2, '0')}`) console.log(`uptime: ${uptime.days}d ${String(uptime.hours).padStart(2, '0')}:${String(uptime.minutes).padStart(2, '0')}:${String(uptime.seconds).padStart(2, '0')}`)
device.write(uhk.getTransferData(new Buffer(uhk.pushUint32([uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.i2cBaudRate])))); device.write(uhk.getTransferData(new Buffer([uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.i2cBaudRate])));
response = device.readSync(); response = device.readSync();
let requestedBaudRate = uhk.getUint32(response, 2); let requestedBaudRate = uhk.getUint32(response, 2);
let actualBaudRate = uhk.getUint32(response, 6); let actualBaudRate = uhk.getUint32(response, 6);

View File

@@ -15,7 +15,7 @@ function convertMs(milliseconds) {
return {days, hours, minutes, seconds}; return {days, hours, minutes, seconds};
} }
let buffer = new Buffer(uhk.pushUint32([uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.uptime])); let buffer = new Buffer([uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.uptime]);
//console.log(buffer); //console.log(buffer);
device.write(uhk.getTransferData(buffer)); device.write(uhk.getTransferData(buffer));
let response = device.readSync(); let response = device.readSync();

View File

@@ -9,6 +9,7 @@ program
const moduleSlot = program.args[0]; const moduleSlot = program.args[0];
const moduleSlotId = uhk.checkModuleSlot(moduleSlot, uhk.moduleSlotToId); const moduleSlotId = uhk.checkModuleSlot(moduleSlot, uhk.moduleSlotToId);
const device = uhk.getUhkDevice(); const device = uhk.getUhkDevice();
let transfer = new Buffer([uhk.usbCommands.jumpToModuleBootloader, moduleSlotId]);
device.write(uhk.getTransferData(transfer)); (async function() {
const response = Buffer.from(device.readSync()); await uhk.jumpToModuleBootloader(device, moduleSlotId);
})();

View File

@@ -1,52 +1,10 @@
#!/usr/bin/env node #!/usr/bin/env node
const HID = require('node-hid'); const uhk = require('./uhk');
let uhk = require('./uhk'); const program = require('commander');
var program = require('commander');
program
.option('-dt, --polling-timeout <n>', 'Polling timeout (ms)')
.option('-bt, --bootloader-timeout <n>', 'Bootloader timeout (ms)')
.option('-f, --force', 'Force reenumeration')
.parse(process.argv);
let pollingTimeoutMs = 10000;
const pollingIntervalMs = 100;
const bootloaderTimeoutMs = 5000;
let jumped = false;
program.parse(process.argv);
const enumerationMode = program.args[0]; const enumerationMode = program.args[0];
const enumerationModeId = uhk.enumerationModes[enumerationMode];
if (enumerationModeId === undefined) { (async function() {
const enumerationModes = Object.keys(uhk.enumerationModes).join(', '); await uhk.reenumerate(enumerationMode);
console.log(`Invalid enumeration mode '${enumerationMode}' is not one of: ${enumerationModes}`); })();
process.exit(1);
}
console.log(`Trying to reenumerate as ${enumerationMode}...`);
setInterval(() => {
pollingTimeoutMs -= pollingIntervalMs;
const foundDevice = HID.devices().find(device =>
device.vendorId === uhk.vendorId && device.productId === uhk.enumerationModeIdToProductId[enumerationModeId]);
if (foundDevice) {
console.log(`${enumerationMode} is up`);
process.exit(0);
}
if (pollingTimeoutMs <= 0) {
console.log(`Couldn't reenumerate as ${enumerationMode}`);
process.exit(1);
}
let device = uhk.getUhkDevice();
if (device && !jumped) {
console.log(`UHK found, reenumerating as ${enumerationMode}`);
let t = bootloaderTimeoutMs;
let message = new Buffer([uhk.usbCommands.reenumerate, enumerationModeId, t&0xff, (t&0xff<<8)>>8, (t&0xff<<16)>>16, (t&0xff<<24)>>24]);
device.write(uhk.getTransferData(message));
jumped = true;
}
}, pollingIntervalMs);

View File

@@ -33,6 +33,6 @@ if (kbootCommand !== 'idle') {
} }
const device = uhk.getUhkDevice(); const device = uhk.getUhkDevice();
let transfer = new Buffer([uhk.usbCommands.sendKbootCommandToModule, kbootCommandId, parseInt(i2cAddress)]); (async function() {
device.write(uhk.getTransferData(transfer)); await uhk.sendKbootCommandToModule(device, kbootCommandId, i2cAddress);
const response = Buffer.from(device.readSync()); })();

View File

@@ -17,8 +17,7 @@ You're free to use any value in between and test the results.`);
} }
let bps = process.argv[2]; let bps = process.argv[2];
let buffer = new Buffer(uhk.pushUint32([uhk.usbCommands.setI2cBaudRate], +bps));
//console.log(buffer); (async function() {
device.write(uhk.getTransferData(buffer)); await uhk.writeDevice(device, [uhk.usbCommands.setI2cBaudRate, ...uhk.uint32ToArray(+bps)]);
let response = device.readSync(); })();
//console.log(Buffer.from(response));

View File

@@ -1,65 +0,0 @@
require('shelljs/global');
function checkFirmwareImage(imagePath, extension) {
if (!imagePath) {
echo('No firmware image specified.');
exit(1);
}
if (!imagePath.endsWith(extension)) {
echo(`Firmware image extension is not ${extension}`);
exit(1);
}
if (!test('-f', imagePath)) {
echo('Firmware image does not exist.');
exit(1);
}
}
function getBlhostCmd(pid) {
let blhostPath;
switch (process.platform) {
case 'linux':
const arch = exec('uname -m', {silent:true}).stdout.trim();
blhostPath = `linux/${arch}/blhost`;
break;
case 'darwin':
blhostPath = 'mac/blhost';
break;
case 'win32':
blhostPath = 'win/blhost.exe';
break;
default:
echo('Your operating system is not supported.');
exit(1);
break;
}
return `${__dirname}/blhost/${blhostPath} --usb 0x1d50,0x${pid.toString(16)}`;
}
function execRetry(command) {
let firstRun = true;
let remainingRetries = 3;
let code;
do {
if (!firstRun) {
console.log(`Retrying ${command}`)
}
config.fatal = !remainingRetries;
code = exec(command).code;
config.fatal = true;
firstRun = false;
} while(code && --remainingRetries);
}
const exp = {
checkFirmwareImage,
getBlhostCmd,
execRetry,
}
Object.keys(exp).forEach(function (cmd) {
global[cmd] = exp[cmd];
});

View File

@@ -1,3 +1,4 @@
const util = require('util');
const HID = require('node-hid'); const HID = require('node-hid');
// const debug = process.env.DEBUG; // const debug = process.env.DEBUG;
const debug = true; const debug = true;
@@ -22,12 +23,16 @@ function getUint32(buffer, offset) {
return (buffer[offset]) + (buffer[offset+1] * 2**8) + (buffer[offset+2] * 2**16) + (buffer[offset+3] * 2**24); return (buffer[offset]) + (buffer[offset+1] * 2**8) + (buffer[offset+2] * 2**16) + (buffer[offset+3] * 2**24);
} }
function pushUint32(array, value) { function uint32ToArray(value) {
array.push((value >> 0) & 0xff); return [(value >> 0) & 0xff, (value >> 8) & 0xff, (value >> 16) & 0xff, (value >> 24) & 0xff];
array.push((value >> 8) & 0xff); }
array.push((value >> 16) & 0xff);
array.push((value >> 24) & 0xff); function writeDevice(device, data, options={}) {
return array; device.write(getTransferData(new Buffer(data)));
if (options.noRead) {
return Promise.resolve();
}
return util.promisify(device.read.bind(device))();
} }
function getUhkDevice() { function getUhkDevice() {
@@ -60,6 +65,60 @@ function getBootloaderDevice() {
return foundDevice; return foundDevice;
} }
function checkFirmwareImage(imagePath, extension) {
if (!imagePath) {
echo('No firmware image specified.');
exit(1);
}
if (!imagePath.endsWith(extension)) {
echo(`Firmware image extension is not ${extension}`);
exit(1);
}
if (!test('-f', imagePath)) {
echo('Firmware image does not exist.');
exit(1);
}
}
function getBlhostCmd(pid) {
let blhostPath;
switch (process.platform) {
case 'linux':
const arch = exec('uname -m', {silent:true}).stdout.trim();
blhostPath = `linux/${arch}/blhost`;
break;
case 'darwin':
blhostPath = 'mac/blhost';
break;
case 'win32':
blhostPath = 'win/blhost.exe';
break;
default:
echo('Your operating system is not supported.');
exit(1);
break;
}
return `${__dirname}/blhost/${blhostPath} --usb 0x1d50,0x${pid.toString(16)}`;
}
function execRetry(command) {
let firstRun = true;
let remainingRetries = 3;
let code;
do {
if (!firstRun) {
console.log(`Retrying ${command}`)
}
config.fatal = !remainingRetries;
code = exec(command).code;
config.fatal = true;
firstRun = false;
} while(code && --remainingRetries);
}
let configBufferIds = { let configBufferIds = {
hardwareConfig: 0, hardwareConfig: 0,
stagingUserConfig: 1, stagingUserConfig: 1,
@@ -71,15 +130,157 @@ let eepromOperations = {
write: 1, write: 1,
}; };
exports = module.exports = moduleExports = { async function updateDeviceFirmware(firmwareImage, extension) {
const usbDir = `${__dirname}`;
const blhost = uhk.getBlhostCmd(uhk.enumerationNameToProductId.bootloader);
uhk.checkFirmwareImage(firmwareImage, extension);
config.verbose = true;
await uhk.reenumerate('bootloader');
exec(`${blhost} flash-security-disable 0403020108070605`);
exec(`${blhost} flash-erase-region 0xc000 475136`);
exec(`${blhost} flash-image ${firmwareImage}`);
exec(`${blhost} reset`);
config.verbose = false;
echo('Firmware updated successfully.');
};
// USB commands
function reenumerate(enumerationMode) {
const bootloaderTimeoutMs = 5000;
const pollingIntervalMs = 100;
let pollingTimeoutMs = 10000;
let jumped = false;
return new Promise((resolve, reject) => {
const enumerationModeId = exports.enumerationModes[enumerationMode];
if (enumerationModeId === undefined) {
const enumerationModes = Object.keys(exports.enumerationModes).join(', ');
console.log(`Invalid enumeration mode '${enumerationMode}' is not one of: ${enumerationModes}`);
reject();
return;
}
console.log(`Trying to reenumerate as ${enumerationMode}...`);
const intervalId = setInterval(async function() {
pollingTimeoutMs -= pollingIntervalMs;
const foundDevice = HID.devices().find(device =>
device.vendorId === exports.vendorId && device.productId === exports.enumerationModeIdToProductId[enumerationModeId]);
if (foundDevice) {
console.log(`${enumerationMode} is up`);
resolve();
clearInterval(intervalId);
return;
}
if (pollingTimeoutMs <= 0) {
console.log(`Couldn't reenumerate as ${enumerationMode}`);
reject();
clearInterval(intervalId);
return;
}
let device = exports.getUhkDevice();
if (device && !jumped) {
console.log(`UHK found, reenumerating as ${enumerationMode}`);
await writeDevice(device, [exports.usbCommands.reenumerate, enumerationModeId, ...uint32ToArray(bootloaderTimeoutMs)], {noRead:true});
jumped = true;
}
}, pollingIntervalMs);
})
};
async function sendKbootCommandToModule(device, kbootCommandId, i2cAddress) {
return await uhk.writeDevice(device, [uhk.usbCommands.sendKbootCommandToModule, kbootCommandId, parseInt(i2cAddress)])
};
async function jumpToModuleBootloader(device, moduleSlotId) {
await uhk.writeDevice(device, [uhk.usbCommands.jumpToModuleBootloader, moduleSlotId]);
};
async function waitForKbootIdle(device) {
const intervalMs = 100;
const pingMessageInterval = 500;
let timeoutMs = 10000;
return new Promise((resolve, reject) => {
const intervalId = setInterval(async function() {
const response = await uhk.writeDevice(device, [uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.currentKbootCommand]);
const currentKbootCommand = response[1];
if (currentKbootCommand == 0) {
console.log('Bootloader pinged.');
resolve();
clearInterval(intervalId);
return;
} else if (timeoutMs % pingMessageInterval === 0) {
console.log("Cannot ping the bootloader. Please reconnect the left keyboard half. It probably needs several tries, so keep reconnecting until you see this message.");
};
timeoutMs -= intervalMs;
if (timeoutMs < 0) {
reject();
clearInterval(intervalId);
return;
}
}, intervalMs);
});
}
async function updateModuleFirmware(i2cAddress, moduleSlotId, firmwareImage) {
const usbDir = `${__dirname}`;
const blhostUsb = uhk.getBlhostCmd(uhk.enumerationNameToProductId.buspal);
const blhostBuspal = `${blhostUsb} --buspal i2c,${i2cAddress}`;
config.verbose = true;
let device = uhk.getUhkDevice();
await uhk.sendKbootCommandToModule(device, uhk.kbootCommands.ping, i2cAddress);
await uhk.jumpToModuleBootloader(device, moduleSlotId);
await uhk.waitForKbootIdle(device);
device.close();
await uhk.reenumerate('buspal');
uhk.execRetry(`${blhostBuspal} get-property 1`);
exec(`${blhostBuspal} flash-erase-all-unsecure`);
exec(`${blhostBuspal} write-memory 0x0 ${firmwareImage}`);
exec(`${blhostUsb} reset`);
await uhk.reenumerate('normalKeyboard');
device = uhk.getUhkDevice();
await uhk.sendKbootCommandToModule(device, uhk.kbootCommands.reset, i2cAddress);
await uhk.sendKbootCommandToModule(device, uhk.kbootCommands.idle, i2cAddress);
device.close();
config.verbose = false;
echo('Firmware updated successfully.');
};
uhk = exports = module.exports = moduleExports = {
bufferToString, bufferToString,
getUint16, getUint16,
getUint32, getUint32,
pushUint32, uint32ToArray,
writeDevice,
getUhkDevice, getUhkDevice,
getBootloaderDevice, getBootloaderDevice,
getTransferData, getTransferData,
checkModuleSlot, checkModuleSlot,
checkFirmwareImage,
getBlhostCmd,
execRetry,
updateDeviceFirmware,
reenumerate,
sendKbootCommandToModule,
jumpToModuleBootloader,
waitForKbootIdle,
updateModuleFirmware,
usbCommands: { usbCommands: {
getDeviceProperty : 0x00, getDeviceProperty : 0x00,
reenumerate : 0x01, reenumerate : 0x01,
@@ -175,20 +376,11 @@ function convertBufferToIntArray(buffer) {
} }
function getTransferData(buffer) { function getTransferData(buffer) {
const data = convertBufferToIntArray(buffer)
// if data start with 0 need to add additional leading zero because HID API remove it.
// https://github.com/node-hid/node-hid/issues/187
if (data.length > 0 && data[0] === 0) {
// data.unshift(0) // TODO: This has been commented out because it causes bugs on Linux and Mac. Gotta test it on Windows and fully remove it if possible.
}
// From HID API documentation: // From HID API documentation:
// http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af // http://www.signal11.us/oss/hidapi/hidapi/doxygen/html/group__API.html#gad14ea48e440cf5066df87cc6488493af
// The first byte of data[] must contain the Report ID. // The first byte of data[] must contain the Report ID.
// For devices which only support a single report, this must be set to 0x0. // For devices which only support a single report, this must be set to 0x0.
data.unshift(0) return [0, ...convertBufferToIntArray(buffer)];
return data
} }
function readLog(buffer) { function readLog(buffer) {

View File

@@ -3,6 +3,7 @@ const program = require('commander');
const tmp = require('tmp'); const tmp = require('tmp');
const decompress = require('decompress'); const decompress = require('decompress');
const decompressTarbz = require('decompress-tarbz2'); const decompressTarbz = require('decompress-tarbz2');
const uhk = require('./uhk')
require('shelljs/global'); require('shelljs/global');
(async function() { (async function() {
@@ -28,9 +29,11 @@ require('shelljs/global');
firmwarePath = tmpObj.name; firmwarePath = tmpObj.name;
} }
config.verbose = true; config.verbose = true;
exec(`${__dirname}/update-device-firmware.js ${firmwarePath}/devices/uhk60-right/firmware.hex`); console.log('Updating right firmware');
exec(`${__dirname}/reenumerate.js normalKeyboard`); await uhk.updateDeviceFirmware(`${firmwarePath}/devices/uhk60-right/firmware.hex`, 'hex');
exec(`${__dirname}/update-module-firmware.js leftHalf ${firmwarePath}/modules/uhk60-left.bin`); await uhk.reenumerate('normalKeyboard');
console.log('Updating left firmware');
await uhk.updateModuleFirmware(uhk.moduleSlotToI2cAddress.leftHalf, uhk.moduleSlotToId.leftHalf, `${firmwarePath}/modules/uhk60-left.bin`);
if (program.overwriteUserConfig) { if (program.overwriteUserConfig) {
exec(`${__dirname}/write-config.js ${firmwarePath}/devices/uhk60-right/config.bin`); exec(`${__dirname}/write-config.js ${firmwarePath}/devices/uhk60-right/config.bin`);
@@ -39,7 +42,7 @@ require('shelljs/global');
} }
config.verbose = false; config.verbose = false;
} catch(exception) { } catch (exception) {
console.error(exception.message); console.error(exception.message);
exit(1); exit(1);
} }

View File

@@ -2,27 +2,13 @@
const uhk = require('./uhk'); const uhk = require('./uhk');
const program = require('commander'); const program = require('commander');
require('shelljs/global'); require('shelljs/global');
require('./shared')
const extension = '.hex';
config.fatal = true; config.fatal = true;
const extension = '.hex';
program program
.usage(`firmwareImage${extension}`) .usage(`firmwareImage${extension}`)
.parse(process.argv) .parse(process.argv)
const firmwareImage = program.args[0]; const firmwareImage = program.args[0];
const usbDir = `${__dirname}`; uhk.updateDeviceFirmware(firmwareImage, extension);
const blhost = getBlhostCmd(uhk.enumerationNameToProductId.bootloader);
checkFirmwareImage(firmwareImage, extension);
config.verbose = true;
exec(`${usbDir}/reenumerate.js bootloader`);
exec(`${blhost} flash-security-disable 0403020108070605`);
exec(`${blhost} flash-erase-region 0xc000 475136`);
exec(`${blhost} flash-image ${firmwareImage}`);
exec(`${blhost} reset`);
config.verbose = false;
echo('Firmware updated successfully.');

View File

@@ -2,7 +2,6 @@
const uhk = require('./uhk'); const uhk = require('./uhk');
const program = require('commander'); const program = require('commander');
require('shelljs/global'); require('shelljs/global');
require('./shared');
const extension = '.bin'; const extension = '.bin';
config.fatal = true; config.fatal = true;
@@ -12,27 +11,12 @@ program
.parse(process.argv) .parse(process.argv)
let moduleSlot = program.args[0]; let moduleSlot = program.args[0];
const moduleSlotId = uhk.checkModuleSlot(moduleSlot, uhk.moduleSlotToId);
const i2cAddress = uhk.checkModuleSlot(moduleSlot, uhk.moduleSlotToI2cAddress); const i2cAddress = uhk.checkModuleSlot(moduleSlot, uhk.moduleSlotToI2cAddress);
const firmwareImage = program.args[1]; const firmwareImage = program.args[1];
checkFirmwareImage(firmwareImage, extension); uhk.checkFirmwareImage(firmwareImage, extension);
const usbDir = `${__dirname}`; (async function() {
const blhostUsb = getBlhostCmd(uhk.enumerationNameToProductId.buspal); await uhk.updateModuleFirmware(i2cAddress, moduleSlotId, firmwareImage);
const blhostBuspal = `${blhostUsb} --buspal i2c,${i2cAddress}`; })();
config.verbose = true;
exec(`${usbDir}/send-kboot-command-to-module.js ping ${moduleSlot}`);
exec(`${usbDir}/jump-to-module-bootloader.js ${moduleSlot}`);
exec(`${usbDir}/wait-for-kboot-idle.js`);
exec(`${usbDir}/reenumerate.js buspal`);
execRetry(`${blhostBuspal} get-property 1`);
exec(`${blhostBuspal} flash-erase-all-unsecure`);
exec(`${blhostBuspal} write-memory 0x0 ${firmwareImage}`);
exec(`${blhostUsb} reset`);
exec(`${usbDir}/reenumerate.js normalKeyboard`);
execRetry(`${usbDir}/send-kboot-command-to-module.js reset ${moduleSlot}`);
exec(`${usbDir}/send-kboot-command-to-module.js idle`);
config.verbose = false;
echo('Firmware updated successfully.');

View File

@@ -1,22 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
const uhk = require('./uhk'); const uhk = require('./uhk');
function getCurrentKbootCommand() {
device.write(uhk.getTransferData(new Buffer([uhk.usbCommands.getDeviceProperty, uhk.devicePropertyIds.currentKbootCommand])));
const response = Buffer.from(device.readSync());
const currentKbootCommand = response[1];
if (currentKbootCommand == 0) {
console.log('Bootloader pinged.');
process.exit(0);
} else {
console.log("Cannot ping the bootloader. Please reconnect the left keyboard half. It probably needs several tries, so keep reconnecting until you see this message.");
}
}
const device = uhk.getUhkDevice(); const device = uhk.getUhkDevice();
getCurrentKbootCommand(); (async function() {
await uhk.waitForKbootIdle(device);
setInterval(() => { })();
getCurrentKbootCommand();
}, 500);

View File

@@ -110,7 +110,7 @@ if (TEST_BUILD || gitTag) {
directories: { directories: {
app: electron_build_folder app: electron_build_folder
}, },
appId: 'com.ultimategadgetlabs.uhk.agent', appId: 'com.ultimategadgetlabs.agent',
productName: 'UHK Agent', productName: 'UHK Agent',
mac: { mac: {
category: 'public.app-category.utilities', category: 'public.app-category.utilities',