Compare commits
308 Commits
v1.1.3
...
fix-firmwa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3659b76373 | ||
|
|
fae83a4148 | ||
|
|
ac76674469 | ||
|
|
239c989cbe | ||
|
|
0914a1496b | ||
|
|
79f467603a | ||
|
|
c4269d4cf1 | ||
|
|
ee28065046 | ||
|
|
a64a44fe41 | ||
|
|
4147243565 | ||
|
|
905007a597 | ||
|
|
c3fec647b6 | ||
|
|
5f5aff71a5 | ||
|
|
f6bef928fe | ||
|
|
5ab5ad5f0c | ||
|
|
4d2d6d40fb | ||
|
|
db2e14a852 | ||
|
|
757a201c47 | ||
|
|
c2c997c9b0 | ||
|
|
0015e6deb3 | ||
|
|
1134c1d16e | ||
|
|
1b569aa82b | ||
|
|
66d6802920 | ||
|
|
bf1689dd34 | ||
|
|
f5e05f19b6 | ||
|
|
05b8fc89fe | ||
|
|
cbccaba1c5 | ||
|
|
a409c219d8 | ||
|
|
2b84101975 | ||
|
|
1c551c0669 | ||
|
|
fe24ebbefc | ||
|
|
a4cb4b7e0c | ||
|
|
cba93a41e0 | ||
|
|
9652152225 | ||
|
|
ffcfce04d5 | ||
|
|
7c6da6c4e6 | ||
|
|
4c57f28a3c | ||
|
|
4677829d56 | ||
|
|
e812b7dfcc | ||
|
|
0290dee35c | ||
|
|
3c6c12d7a8 | ||
|
|
01aa9f119c | ||
|
|
2850658055 | ||
|
|
c1e295c92d | ||
|
|
40d29886cb | ||
|
|
a02c4deae1 | ||
|
|
c1b7078549 | ||
|
|
293affacb3 | ||
|
|
c41450d0dd | ||
|
|
a9ec8efb2b | ||
|
|
d7ecd5a083 | ||
|
|
8c4b13cd94 | ||
|
|
b4ec69ff6d | ||
|
|
62f69b5f70 | ||
|
|
e2fda9201f | ||
|
|
bf9bfb8d82 | ||
|
|
02089d166c | ||
|
|
3df6e8214b | ||
|
|
fbc8970baa | ||
|
|
88b3b2fb65 | ||
|
|
41f73acfce | ||
|
|
9d41f40d93 | ||
|
|
56ebfba8ad | ||
|
|
400f01dabe | ||
|
|
c64515885b | ||
|
|
82c9126d82 | ||
|
|
999feea488 | ||
|
|
4dd2400d36 | ||
|
|
5657579d6d | ||
|
|
58b83f2b6b | ||
|
|
b991e2c9d4 | ||
|
|
e6495ded05 | ||
|
|
21cab7e755 | ||
|
|
2dfb61a38e | ||
|
|
2c93975c77 | ||
|
|
427f706287 | ||
|
|
1b59d96296 | ||
|
|
20e2c8bbbc | ||
|
|
c2f47f18b0 | ||
|
|
1ee31003fd | ||
|
|
4abdac1ef6 | ||
|
|
32db3817e2 | ||
|
|
264752530f | ||
|
|
33c910d70c | ||
|
|
b1b2f1d431 | ||
|
|
c3a6b373d4 | ||
|
|
f94b221ee9 | ||
|
|
3a1451a6a9 | ||
|
|
7a2c8cb2e4 | ||
|
|
de71f6f88c | ||
|
|
5fd555c214 | ||
|
|
9a0b0f9df1 | ||
|
|
12d361eb2e | ||
|
|
2c041b64ff | ||
|
|
8947f2dedf | ||
|
|
6b4cf41357 | ||
|
|
c36156b497 | ||
|
|
d674fd4490 | ||
|
|
8acc33e719 | ||
|
|
2855480d6a | ||
|
|
eee3322082 | ||
|
|
bb31c2cefa | ||
|
|
e18a98d8bb | ||
|
|
3964698cf7 | ||
|
|
bfc08edfce | ||
|
|
6e2115ac74 | ||
|
|
1eb8720305 | ||
|
|
c9f052b8c7 | ||
|
|
58ee42fcc2 | ||
|
|
9112b597f8 | ||
|
|
8c7d625573 | ||
|
|
52a57c0e87 | ||
|
|
a52b34fc3f | ||
|
|
1a14ac020e | ||
|
|
330f7e72be | ||
|
|
1ed6669ced | ||
|
|
108d60a497 | ||
|
|
1a9bd7de83 | ||
|
|
10cd06c70b | ||
|
|
84b6c33c54 | ||
|
|
18808eae9c | ||
|
|
ca74b0a76b | ||
|
|
b45d60efb2 | ||
|
|
a4e3696078 | ||
|
|
2b963993d2 | ||
|
|
e333022043 | ||
|
|
2e2a59ccb8 | ||
|
|
6f073ad718 | ||
|
|
166834e46c | ||
|
|
0ff2364b9e | ||
|
|
2cbfc6a11e | ||
|
|
404ccc7b2b | ||
|
|
42e413ab65 | ||
|
|
d57ef66038 | ||
|
|
843b4cbf68 | ||
|
|
7e4b7c5c8b | ||
|
|
2ff65537a0 | ||
|
|
6e2b1fb18d | ||
|
|
3e621a2818 | ||
|
|
247ec4c1b2 | ||
|
|
edcff069fd | ||
|
|
8afdeac306 | ||
|
|
425f861451 | ||
|
|
5a843ed02c | ||
|
|
f3bd83af03 | ||
|
|
0b3fca63b7 | ||
|
|
cbd4460df0 | ||
|
|
b941bd9a75 | ||
|
|
439b84affc | ||
|
|
66d5302e6f | ||
|
|
9e2e2b9c5c | ||
|
|
aa243ac7b0 | ||
|
|
eb421e0681 | ||
|
|
63aae8f578 | ||
|
|
e577454a31 | ||
|
|
e802bb0052 | ||
|
|
b98e5df20a | ||
|
|
7332105edb | ||
|
|
6a4feaf18d | ||
|
|
ee637d7958 | ||
|
|
8d161ce8ff | ||
|
|
8010bd8195 | ||
|
|
059f1d5505 | ||
|
|
123cab5724 | ||
|
|
c16365a0e5 | ||
|
|
a21d278c0c | ||
|
|
0466916be1 | ||
|
|
9a845d8f6a | ||
|
|
9ae1673499 | ||
|
|
2d5a5e7aef | ||
|
|
3e4d439852 | ||
|
|
aba0b09109 | ||
|
|
af608ee17d | ||
|
|
df817e86d6 | ||
|
|
a7d3b62512 | ||
|
|
b41f14192a | ||
|
|
475ec71983 | ||
|
|
b5cff2fa93 | ||
|
|
80e8c014ec | ||
|
|
67d42f666c | ||
|
|
fa32f95438 | ||
|
|
06878dd56a | ||
|
|
374f6a3e6e | ||
|
|
0b9c804a3d | ||
|
|
365a459d61 | ||
|
|
cec891a2c0 | ||
|
|
8eb8aa3032 | ||
|
|
38184e7968 | ||
|
|
f6092ea195 | ||
|
|
ac7d66e338 | ||
|
|
b82a1da92a | ||
|
|
b8859f7b64 | ||
|
|
a04fa67446 | ||
|
|
ac89aff018 | ||
|
|
e7cf8dc966 | ||
|
|
d0102f5bdb | ||
|
|
eb0daadf98 | ||
|
|
49d6ca173d | ||
|
|
a3eb6a6b7e | ||
|
|
144ed57b20 | ||
|
|
6086ddabf0 | ||
|
|
84f378a276 | ||
|
|
648e8d5f2c | ||
|
|
15df8d7129 | ||
|
|
cfc0af9655 | ||
|
|
f02e3181a6 | ||
|
|
3d59bcf97e | ||
|
|
5e4fc983fb | ||
|
|
32d9635b34 | ||
|
|
3978011d2e | ||
|
|
cd1952a7df | ||
|
|
4251477451 | ||
|
|
873f1de1ef | ||
|
|
150f993e5f | ||
|
|
06e76e5e0f | ||
|
|
a208a264c7 | ||
|
|
114014fa13 | ||
|
|
94cfd9d2e9 | ||
|
|
0aa9c73b4b | ||
|
|
5234f85dbe | ||
|
|
bd8a2f704f | ||
|
|
439886d69f | ||
|
|
b2a37795e3 | ||
|
|
440db56080 | ||
|
|
337e6e6bb6 | ||
|
|
a1aeda3d35 | ||
|
|
c6a83f8c9b | ||
|
|
0f24427628 | ||
|
|
f52dc36a6a | ||
|
|
63a936968d | ||
|
|
cabfde7963 | ||
|
|
79628c2351 | ||
|
|
762fa6f8bf | ||
|
|
a258c097a9 | ||
|
|
41faa98fcd | ||
|
|
c4d7318686 | ||
|
|
9ef11eaa34 | ||
|
|
f34cb2df56 | ||
|
|
83b9f0d1e9 | ||
|
|
7d81cf0c6a | ||
|
|
82b76a9455 | ||
|
|
4ae577f936 | ||
|
|
81a83994ab | ||
|
|
1d3a3c7f5f | ||
|
|
8bb645125d | ||
|
|
9471b31a5d | ||
|
|
ffa52757c9 | ||
|
|
ee53a0df9b | ||
|
|
8e20c85e07 | ||
|
|
65ea786358 | ||
|
|
1035837b3b | ||
|
|
18fc2e6b3f | ||
|
|
fc728697d7 | ||
|
|
cdf3caee9e | ||
|
|
0a4d3a002e | ||
|
|
d11c532ea4 | ||
|
|
1ff51697b1 | ||
|
|
ab8ae31324 | ||
|
|
daa0e723b1 | ||
|
|
609aba856a | ||
|
|
a6678bd537 | ||
|
|
6c4f580fc2 | ||
|
|
ea41661c65 | ||
|
|
c553c7b63b | ||
|
|
e5988aa800 | ||
|
|
ae319c607f | ||
|
|
5d23ad1c9e | ||
|
|
55eef50da7 | ||
|
|
653465f0e0 | ||
|
|
2cf8044987 | ||
|
|
3c056a7255 | ||
|
|
091796d13c | ||
|
|
eb97dd844f | ||
|
|
17693ec8fe | ||
|
|
7c7ce8f50f | ||
|
|
e294727ac5 | ||
|
|
f29d64c803 | ||
|
|
0385b0ce29 | ||
|
|
b526274cd7 | ||
|
|
88c16af4a9 | ||
|
|
05ac9a6832 | ||
|
|
04aa5236c2 | ||
|
|
ec98e4e1c6 | ||
|
|
bb9ece494c | ||
|
|
217e6776ac | ||
|
|
2286218980 | ||
|
|
3d9c83f9f4 | ||
|
|
14ed163238 | ||
|
|
c815de0718 | ||
|
|
6a46556d9e | ||
|
|
f8f820529f | ||
|
|
cac11155e7 | ||
|
|
d20870f11e | ||
|
|
10ceb6c79d | ||
|
|
b38b6fa294 | ||
|
|
94c1d35429 | ||
|
|
e33cef4e89 | ||
|
|
9b815ed9c1 | ||
|
|
1d4bb6113c | ||
|
|
136120b831 | ||
|
|
cd299c06d6 | ||
|
|
e152a36ad7 | ||
|
|
e90544db33 | ||
|
|
7ceca202b4 | ||
|
|
ddc65aa54b | ||
|
|
13ec617d58 | ||
|
|
00c5b69129 | ||
|
|
6ccf005750 |
188
CHANGELOG.md
@@ -6,6 +6,192 @@ 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).
|
||||
|
||||
## [1.2.13] - 2019-07-14
|
||||
|
||||
Firmware: 8.5.**4** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.5.4)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Implement the Kinetis bootloader protocol natively instead of relying on blhost.
|
||||
- Fix device recovery mode.
|
||||
- Correctly display whether the UHK is detected.
|
||||
- Don't disable input in the key action popover after adding a layer switch action, deleting it, and trying to edit it on its layer.
|
||||
- Don't change tab immediately upon closing the key action popover.
|
||||
- Fix UI glitch that occurrs when hitting Tab after updating keymap description.
|
||||
- Make the Agent icon slightly smaller to be consistent with most application icons.
|
||||
- Redesign the About page.
|
||||
- Link the UHK knowledgebase from the help page.
|
||||
- Make the middle mouse button not open new windows on links in Agent.
|
||||
- Add top auto-update notification bar.
|
||||
- Save window state when closing Agent.
|
||||
- Hide USB usage data in console.
|
||||
|
||||
## [1.2.12] - 2018-11-14
|
||||
|
||||
Firmware: 8.**5.3** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.5.3)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- When the firmware of the right keyboard half is larger or equal than 8.4.3 then display the "Lock layer when double tapping this key" checkbox and remove "... macro playback is not implemented yet..." notices.
|
||||
- Upgrade to node-hid 0.7.3 which utilizes the hidraw USB driver on Linux instead of libusb.
|
||||
- Update udev rules for the new hidraw based node-hid.
|
||||
- Improve the "Cannot find your UHK" and the privilege escalation screens to show more relevant messages when transitioning from the libusb based node-hid to the hidraw based node-hid.
|
||||
- Fix the rendering of macro actions, so that their text doesn't overlap.
|
||||
- Add "International {1,2,3}" and "Language {1,2}" keypress actions.
|
||||
- Add icon for the Play/Pause keypress action.
|
||||
- Remove the Stop/Eject keypress action.
|
||||
- Make the "Type text" macro action accept clipboard data on Mac.
|
||||
- Display "You can't change this mapping because on the base layer a layer switcher key targets this key." in the key action popover whenever it applies.
|
||||
- Fix UI bug which could be triggered by tapping Tab in the keymap abbreviation input.
|
||||
- Don't trigger Agent shortcuts when capturing keypresses.
|
||||
- Log USB device list before checking permissions.
|
||||
- Show OS-specific modifiers in the title bar of macro actions.
|
||||
- Only show the device list on Linux when the list actually changes.
|
||||
|
||||
## [1.2.11] - 2018-10-03
|
||||
|
||||
Firmware: 8.2.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Add backspace and caps lock icons which avoids the overlap of their old texts.
|
||||
- Fix right and middle mouse click macro actions which were exchanged.
|
||||
- Include Agent version to the firmware update log.
|
||||
|
||||
## [1.2.10] - 2018-09-24
|
||||
|
||||
Firmware: 8.2.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Add History Back and History Forward scancodes.
|
||||
- Save the actual decelerated scroll speed instead of using the accelerated scroll speed by accident.
|
||||
- Allow layer switcher secondary roles only on the base layer.
|
||||
- When remapping modifiers, display a warning suggesting to remap them on all layers.
|
||||
- Display more exact instructions on the permission setup screen.
|
||||
- Set the decelerated scroll speed of the default configuration from 20 to 10.
|
||||
- Map Caps Lock without Ctrl on default keymaps.
|
||||
- Rename "Scroll Lock" to "ScrLk" and "Num Lock" to "NumLk" on keys to avoid text overlap.
|
||||
- In the scancode select2, display "Print Screen SysRq" and add SysRq above PrtScn when rendering the key.
|
||||
- Fix left and right direction titles for mouse movement macro actions.
|
||||
|
||||
## [1.2.9] - 2018-09-13
|
||||
|
||||
Firmware: 8.2.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Display OS-specific modifiers.
|
||||
- Display secondary roles.
|
||||
- Don't trigger "Remap on all layers" after leaving Agent with Alt+Tab.
|
||||
|
||||
## [1.2.8] - 2018-08-26
|
||||
|
||||
Firmware: 8.**2.5** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.5)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Uncheck the "Remap on all keymaps" and "Remap on all layers" checkboxes of the key action popover by default.
|
||||
- Bind left and right Shift on the Mouse layer of all keymaps in the default configuration.
|
||||
- Make ng2-select2 widgets faster.
|
||||
- Add note to the LED brightness page saying that current UHK versions are not backlit.
|
||||
- Fix the padding of the secondary role tooltip.
|
||||
- Remove the redundant scrollbar from the LED brightness page.
|
||||
|
||||
## [1.2.7] - 2018-07-26
|
||||
|
||||
Firmware: 8.4.0 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.4.0)] | Device Protocol: 4.4.0 | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Fix Agent startup exception on Linux by upgrading Electron builder.
|
||||
- Change the shortcut which enables the USB stack test code, so that it can be triggered with the default Mac US keymap.
|
||||
|
||||
## [1.2.6] - 2018-07-26
|
||||
|
||||
Firmware: 8.**4.0** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.4.0)] | Device Protocol: 4.**4.0** | User Config: 4.0.1 | Hardware Config: 1.0.0
|
||||
|
||||
- Replace the Linux blhost binary with a statically compiled version that doesn't use special instructions and shouldn't segfault.
|
||||
- Keep the current layer when changing keymaps.
|
||||
- Fix the sleep key of Mac keymaps.
|
||||
- Add help page.
|
||||
- Add "save to keyboard" and "remap key" shortcuts.
|
||||
- Build only AppImages for Linux.
|
||||
- Replace ng2-select2 widgets with ngx-select-ex that always shows up in the correct position.
|
||||
- Improve the phrasing of the firmware update error message.
|
||||
- Tweak unsupported Windows firmware update notification.
|
||||
- Hide the Settings menu until auto update is implemented.
|
||||
- Don't scroll when the macro tab of the key action popover gets selected.
|
||||
- Add keyboard shortcut for enabling the USB stack test mode of the firmware. `DEVICEPROTOCOL:MINOR`
|
||||
- Tone down the color of the separator line.
|
||||
|
||||
## [1.2.5] - 2018-06-26
|
||||
|
||||
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
|
||||
|
||||
- When remapping a switch keymap action on all keymaps, don't set it on its own keymap.
|
||||
- Make the key action popover always contain the action of the current key, even after cancelled.
|
||||
- Include the firmware version to be updated to the firmware update log.
|
||||
- Update the Agent icon of the side menu and the about page.
|
||||
- When remapping a key, only flash the affected key instead of all keys.
|
||||
- Fade in/out the keyboard separator line only when splitting the keyboard.
|
||||
- Only show the unsupported OS message of the firmware page on relevant Windows versions.
|
||||
- Close and reopen USB device when an error occurs.
|
||||
- Temporarily remove the export keymap feature because it's useless until import is implemented.
|
||||
|
||||
## [1.2.4] - 2018-06-21
|
||||
|
||||
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
|
||||
|
||||
- Replace Linux x86-64 blhost with a statically linked version which should make firmware updates work on every Linux distro.
|
||||
|
||||
## [1.2.3] - 2018-06-19
|
||||
|
||||
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
|
||||
|
||||
- Add checkboxes for remapping keys on all layers and/or all keymaps.
|
||||
- Add separator line between the keyboard halves.
|
||||
- Add double tap icon for switch layer actions.
|
||||
- Improve the looks and content of the tooltips of the key action popover.
|
||||
- Make the left keyboard half less likely to timeout during firmware update.
|
||||
- Terminate the firmware update process if blhost segfaults.
|
||||
- Replace the Linux x86-64 version of the blhost binary which should not make it segfault anymore.
|
||||
- Make the firmware update log shorter by listing one device per line and not repeating the list of available USB devices.
|
||||
- Make the firmware update help text shorter.
|
||||
|
||||
## [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. `DEVICEPROTOCOL:PATCH`
|
||||
- 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
|
||||
|
||||
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.
|
||||
- Make the config serializer handle long media macro actions. `USERCONFIG:PATCH`
|
||||
- Add note on the macro page explaining that the macro engine of the firmware is not ready yet.
|
||||
- Add an example to the scancode tooltip to better explain users how to invoke non-US characters.
|
||||
|
||||
## [1.2.0] - 2018-04-20
|
||||
|
||||
Firmware: 8.**2.0** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.2.0)] | Device Protocol: 4.**3.0** | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
|
||||
- Tweak the default mouse speed. This was necessary because the last firmware version adjusted speed multipliers. The mouse speed can be reset via the "Reset speeds to default" button of the "Mouse speed" page.
|
||||
- Make the newly added switch-keymap.js script utilize the new UsbCommandId_SwitchKeymap, allowing for programmatic keymap switching. `DEVICEPROTOCOL:MINOR`
|
||||
|
||||
## [1.1.5] - 2018-04-10
|
||||
|
||||
Firmware: 8.1.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.5)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
|
||||
- Don't allow to run multiple instances of Agent at the same time, but rather focus the already existing Agent window.
|
||||
|
||||
## [1.1.4] - 2018-04-09
|
||||
|
||||
Firmware: 8.1.5 [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.5)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
|
||||
- Handle privilege escalation gracefully on Linux even without PolicyKit.
|
||||
- Fix application icon path.
|
||||
- Replace application icon with a diagonal gradient based icon that should look better on desktop.
|
||||
- Make saving the configuration more robust, and add a configuration recovery screen.
|
||||
- Reposition the ISO key in the scancode list.
|
||||
|
||||
## [1.1.3] - 2018-04-06
|
||||
|
||||
Firmware: 8.1.**5** [[release](https://github.com/UltimateHackingKeyboard/firmware/releases/tag/v8.1.5)] | Device Protocol: 4.2.0 | User Config: 4.0.0 | Hardware Config: 1.0.0
|
||||
@@ -40,7 +226,7 @@ Firmware: 8.1.**2** [[release](https://github.com/UltimateHackingKeyboard/firmwa
|
||||
|
||||
## [1.1.0] - 2018-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
|
||||
|
||||
- Only accept device, keymap, and macro names upon editing if their trimmed length is non-zero.
|
||||
- Add diagnostics USB scripts, most notably /packages/usb/{get-i2c-health,set-i2c-baud-rate}.js, some utilizing new device protocol commands and properties. `DEVICEPROTOCOL:MINOR`
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# Signing the CLA
|
||||
|
||||
Before contributing to this project, you must sign [the CLA](/cla/cla-1.0.0.md).
|
||||
|
||||
To sign the CLA, add your GitHub username to the end of the CLA. Make sure that the usernames remain alphabetically sorted.
|
||||
|
||||
Then create a pull request with the title:
|
||||
|
||||
> Sign CLA
|
||||
|
||||
and with the body:
|
||||
|
||||
> I have read the Agreement, and fully agree to it by signing it with my GitHub username.
|
||||
|
||||
# Bug reports
|
||||
|
||||
If the build process fails, please open a [new issue](https://github.com/UltimateHackingKeyboard/agent/issues/new) containing the complete build log.
|
||||
|
||||
10
ISSUE_TEMPLATE
Normal file
@@ -0,0 +1,10 @@
|
||||
Before submitting a new issue, make sure to do the following:
|
||||
|
||||
1. If you're using Karabiner Elements on your Mac, close it!
|
||||
2. Install the latest Agent:
|
||||
https://github.com/UltimateHackingKeyboard/agent/releases/latest
|
||||
3. Use Agent to update to the latest firmware:
|
||||
https://github.com/UltimateHackingKeyboard/firmware/releases/latest
|
||||
4. Try to reproduce the issue, and only report it if it still persists.
|
||||
|
||||
`npm audit` related issues will be closed due to https://github.com/UltimateHackingKeyboard/agent/blob/master/NPM_UPDATES.md
|
||||
8
NPM_UPDATES.md
Normal file
@@ -0,0 +1,8 @@
|
||||
We get requests from time to time to update our NPM dependencies because they contain vulnerabilities according to `npm audit`. Such issues will be closed without further consideration due to the following reasons:
|
||||
|
||||
1. Usually, the affected packages are not runtime dependencies of Agent, but devDependencies which are only needed for developing Agent.
|
||||
2. Often times, 3rd party packages are affected by vulnerabilities which we cannot fix.
|
||||
3. We can't just blindly update all of the packages because that'd likely break Agent as it has happened in the past. Each of the updates must be carefully tested, and we don't have the manpower to do it on a daily basis.
|
||||
4. Sometimes `npm audit` signals false vulnerabilities.
|
||||
|
||||
We routinely update our dependencies on a best effort basis.
|
||||
19
README.md
@@ -5,17 +5,8 @@
|
||||
|
||||
Agent is the configuration application of the [Ultimate Hacking Keyboard](https://ultimatehackingkeyboard.com/).
|
||||
|
||||
[Give it a whirl!](http://ultimatehackingkeyboard.github.io/agent/)
|
||||
|
||||
## Two builds to rule them all
|
||||
|
||||
It's worth mentioning that Agent has two builds.
|
||||
|
||||
The **electron build** is the desktop application which is meant to be used if you have an actual UHK at hand. It starts with an opening screen which detects your UHK. You cannot get past this screen without connecting a UHK via USB.
|
||||
|
||||
The **web build** is meant to be used for demonstration purposes, so people who don't yet own a UHK can get a feel of Agent and its capabilities in their browser. Eventually, WebUSB support will be added to the web build, making it able to communicate with the UHK. Given the sandboxed nature of browsers, the web build will always lack features that the electron build offers, so this won't make the electron build obsolete.
|
||||
|
||||
The two builds share code as much as possible.
|
||||
* Try out the [web build of Agent](http://ultimatehackingkeyboard.github.io/agent/) in your browser. This is meant to be used for demonstration purposes.
|
||||
* Download the [desktop build of Agent](https://github.com/UltimateHackingKeyboard/agent/releases) from our releases page. Use this if you have an actual UHK at hand, or else you won't get past the opening screen!
|
||||
|
||||
## Building the electron application
|
||||
|
||||
@@ -33,9 +24,9 @@ For everyone else, use the appropriate package manager for your OS.
|
||||
```
|
||||
git clone git@github.com:UltimateHackingKeyboard/agent.git
|
||||
cd agent
|
||||
npm install # to install Node dependencies
|
||||
npm run build:electron # to build the agent
|
||||
npm run electron # to run the newly built agent
|
||||
npm install
|
||||
npm run build
|
||||
npm run electron
|
||||
```
|
||||
|
||||
At this point, Agent should be running on your machine.
|
||||
|
||||
@@ -7,7 +7,7 @@ environment:
|
||||
secure: 3IebpEKmC39codi1wT6dXx8mql4/mCL1JzZ7lir7GQ5MWRnCxlED2OXbiKHHigDV
|
||||
CSC_LINK: c:\projects\uhk-agent\scripts\certs\windows-cert.p12
|
||||
matrix:
|
||||
- nodejs_version: "8"
|
||||
- nodejs_version: "12.0.0"
|
||||
|
||||
cache:
|
||||
- node_modules -> package.json
|
||||
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 392 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 546 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 660 B After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 966 B After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 20 KiB |
112
cla/cla-1.0.0.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Contributor Agreement
|
||||
## Individual Contributor Exclusive License Agreement version 1.0.0
|
||||
|
||||
Thank you for your interest in contributing to Ultimate Gadget Laboratories Kft.'s Ultimate Hacking Keyboard Agent ("We" or "Us").
|
||||
|
||||
The purpose of this contributor agreement ("Agreement") is to clarify and document the rights granted by contributors to Us.
|
||||
|
||||
By signing this Agreement, you agree that the following terms apply to all of your past, present and future contributions to the project.
|
||||
|
||||
## How to use this Contributor Agreement
|
||||
|
||||
If You are an employee and have created the Contribution as part of your employment, You need to have Your employer approve this Agreement or sign the Entity version of this document. If You do not own the Copyright in the entire work of authorship, any other author of the Contribution should also sign this – in any event, please contact Us at support@UltimateHackingKeyboard.com
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
**"You"** means the individual Copyright owner who Submits a Contribution to Us.
|
||||
|
||||
**"Contribution"** means any original work of authorship, including any original modifications or additions to an existing work of authorship, Submitted by You to Us, in which You own the Copyright.
|
||||
|
||||
**"Copyright"** means all rights protecting works of authorship, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence.
|
||||
|
||||
**"Material"** means the software or documentation made available by Us to third parties. When this Agreement covers more than one software project, the Material means the software or documentation to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material.
|
||||
|
||||
**"Submit"** means any act by which a Contribution is transferred to Us by You by means of tangible or intangible media, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us, but excluding any transfer that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
|
||||
|
||||
**"Documentation"** means any non-software portion of a Contribution.
|
||||
|
||||
## 2. License grant
|
||||
|
||||
### 2.1 Copyright license to Us
|
||||
|
||||
Subject to the terms and conditions of this Agreement, You hereby grant to Us a worldwide, royalty-free, exclusive, perpetual and irrevocable (except as stated in Section 8.2) license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, under the Copyright covering the Contribution to use the Contribution by all means, including, but not limited to:
|
||||
|
||||
* publish the Contribution,
|
||||
* modify the Contribution,
|
||||
* prepare derivative works based upon or containing the Contribution and/or to combine the Contribution with other Materials,
|
||||
* reproduce the Contribution in original or modified form,
|
||||
* distribute, to make the Contribution available to the public, display and publicly perform the Contribution in original or modified form.
|
||||
|
||||
### 2.2 Moral rights
|
||||
|
||||
Moral Rights remain unaffected to the extent they are recognized and not waivable by applicable law.
|
||||
|
||||
### 2.3 Copyright license back to You
|
||||
|
||||
Upon such grant of rights to Us, We immediately grant to You a worldwide, royalty-free, non-exclusive, perpetual and irrevocable license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, under the Copyright covering the Contribution to use the Contribution by all means, including, but not limited to:
|
||||
|
||||
* publish the Contribution,
|
||||
* modify the Contribution,
|
||||
* prepare derivative works based upon or containing the Contribution and/or to combine the Contribution with other Materials,
|
||||
* reproduce the Contribution in original or modified form,
|
||||
* distribute, to make the Contribution available to the public, display and publicly perform the Contribution in original or modified form.
|
||||
|
||||
This license back is limited to the Contribution and does not provide any rights to the Material.
|
||||
|
||||
## 3. Patents
|
||||
|
||||
### 3.1 Patent license
|
||||
|
||||
Subject to the terms and conditions of this Agreement You hereby grant to Us and to recipients of Materials distributed by Us a worldwide, royalty-free, non-exclusive, perpetual and irrevocable (except as stated in Section 3.2) patent license, with the right to transfer an unlimited number of non-exclusive licenses or to grant sublicenses to third parties, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with any Material (and portions of such combination). This license applies to all patents owned or controlled by You, whether already acquired or hereafter acquired, that would be infringed by making, having made, using, selling, offering for sale, importing or otherwise transferring of Your Contribution(s) alone or by combination of Your Contribution(s) with any Material.
|
||||
|
||||
### 3.2 Revocation of patent license
|
||||
|
||||
You reserve the right to revoke the patent license stated in section 3.1 if We make any infringement claim that is targeted at your Contribution and not asserted for a Defensive Purpose. An assertion of claims of the Patents shall be considered for a "Defensive Purpose" if the claims are asserted against an entity that has filed, maintained, threatened, or voluntarily participated in a patent infringement lawsuit against Us or any of Our licensees.
|
||||
|
||||
## 4. Disclaimer
|
||||
|
||||
THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US AND BY US TO YOU. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION AND EXTENT TO THE MINIMUM PERIOD AND EXTENT PERMITTED BY LAW.
|
||||
|
||||
## 5. Consequential damage waiver
|
||||
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU OR WE BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED.
|
||||
|
||||
## 6. Approximation of disclaimer and damage waiver
|
||||
|
||||
IF THE DISCLAIMER AND DAMAGE WAIVER MENTIONED IN SECTION 4. AND SECTION 5. CANNOT BE GIVEN LEGAL EFFECT UNDER APPLICABLE LOCAL LAW, REVIEWING COURTS SHALL APPLY LOCAL LAW THAT MOST CLOSELY APPROXIMATES AN ABSOLUTE WAIVER OF ALL CIVIL OR CONTRACTUAL LIABILITY IN CONNECTION WITH THE CONTRIBUTION.
|
||||
|
||||
## 7. Term
|
||||
|
||||
7.1 This Agreement shall come into effect upon Your acceptance of the terms and conditions.
|
||||
|
||||
7.2 In the event of a termination of this Agreement Sections 4, 5, 6, 7 and 8 shall survive such termination and shall remain in full force thereafter. For the avoidance of doubt, (sub)licenses that have already been granted for Contributions at the date of the termination shall remain in full force after the termination of this Agreement.
|
||||
|
||||
## 8. Miscellaneous
|
||||
|
||||
8.1 This Agreement and all disputes, claims, actions, suits or other proceedings arising out of this agreement or relating in any way to it shall be governed by the laws of Hungary excluding its private international law provisions.
|
||||
|
||||
8.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings.
|
||||
|
||||
8.3 In case of Your death, this agreement shall continue with Your heirs. In case of more than one heir, all heirs must exercise their rights through a commonly authorized person.
|
||||
|
||||
8.4 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and that is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law.
|
||||
|
||||
8.5 You agree to notify Us of any facts or circumstances of which you become aware that would make this Agreement inaccurate in any respect.
|
||||
|
||||
## 9. Signatures
|
||||
|
||||
I have read this Agreement, and fully agree to it by signing it with my GitHub username:
|
||||
|
||||
- @attilacsanyi
|
||||
- @cokert
|
||||
- @csanyiarpad
|
||||
- @dgyimesi
|
||||
- @eltang
|
||||
- @ert78gb
|
||||
- @fjozsef
|
||||
- @laxu
|
||||
- @mondalaci
|
||||
- @NejcZdovc
|
||||
- @spuder
|
||||
- @srang
|
||||
- @Lauszus
|
||||
19492
package-lock.json
generated
107
package.json
@@ -3,10 +3,10 @@
|
||||
"private": true,
|
||||
"author": "Ultimate Gadget Laboratories",
|
||||
"main": "electron/dist/electron-main.js",
|
||||
"version": "1.1.3",
|
||||
"firmwareVersion": "8.1.5",
|
||||
"deviceProtocolVersion": "4.2.0",
|
||||
"userConfigVersion": "4.0.0",
|
||||
"version": "1.2.13",
|
||||
"firmwareVersion": "8.5.4",
|
||||
"deviceProtocolVersion": "4.4.0",
|
||||
"userConfigVersion": "4.0.1",
|
||||
"hardwareConfigVersion": "1.0.0",
|
||||
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
|
||||
"repository": {
|
||||
@@ -15,54 +15,71 @@
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"engines": {
|
||||
"node": ">=8.9.1 <9.0.0",
|
||||
"npm": ">=5.6.0 <6.0.0"
|
||||
"node": ">=12.0.0 <13.0.0",
|
||||
"npm": ">=6.9.0 <7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/decompress": "4.2.3",
|
||||
"@types/electron-devtools-installer": "2.0.2",
|
||||
"@types/electron-settings": "3.0.0",
|
||||
"@types/fs-extra": "4.0.5",
|
||||
"@types/jasmine": "2.6.0",
|
||||
"@types/file-saver": "0.0.1",
|
||||
"@types/fs-extra": "8.0.0",
|
||||
"@types/jasmine": "3.3.12",
|
||||
"@types/jasminewd2": "2.0.3",
|
||||
"@types/jquery": "3.3.29",
|
||||
"@types/jsonfile": "4.0.1",
|
||||
"@types/lodash": "4.14.136",
|
||||
"@types/node": "8.0.53",
|
||||
"@types/node-hid": "0.5.2",
|
||||
"@types/node-hid": "0.7.0",
|
||||
"@types/request": "2.0.8",
|
||||
"@types/usb": "1.1.3",
|
||||
"@types/semver": "5.5.0",
|
||||
"@types/tmp": "0.0.33",
|
||||
"autoprefixer": "6.5.3",
|
||||
"buffer": "5.0.6",
|
||||
"check-node-version": "^3.2.0",
|
||||
"copy-webpack-plugin": "5.0.0",
|
||||
"copyfiles": "^2.0.0",
|
||||
"copy-webpack-plugin": "4.0.1",
|
||||
"core-js": "2.4.1",
|
||||
"cross-env": "5.0.5",
|
||||
"decompress": "4.2.0",
|
||||
"decompress-tarbz2": "^4.1.1",
|
||||
"decompress-tarbz2": "4.1.1",
|
||||
"devtron": "1.4.0",
|
||||
"electron": "1.8.4",
|
||||
"electron-builder": "20.8.1",
|
||||
"electron": "5.0.9",
|
||||
"electron-builder": "20.34.0",
|
||||
"electron-debug": "1.5.0",
|
||||
"electron-devtools-installer": "2.2.3",
|
||||
"electron-log": "2.2.14",
|
||||
"electron-rebuild": "1.7.3",
|
||||
"electron-log": "2.2.16",
|
||||
"electron-rebuild": "1.8.5",
|
||||
"electron-settings": "3.1.4",
|
||||
"electron-updater": "2.21.4",
|
||||
"exports-loader": "0.6.3",
|
||||
"file-loader": "0.10.0",
|
||||
"fs-extra": "4.0.2",
|
||||
"fs-extra": "8.1.0",
|
||||
"gh-pages": "2.0.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"jasmine": "3.4.0",
|
||||
"jasmine-core": "3.4.0",
|
||||
"jasmine-node": "3.0.0",
|
||||
"jasmine-ts": "0.3.0",
|
||||
"jsonfile": "4.0.0",
|
||||
"lerna": "2.9.0",
|
||||
"mkdirp": "0.5.1",
|
||||
"lerna": "3.16.4",
|
||||
"lodash": "4.17.15",
|
||||
"node-hid": "0.7.9",
|
||||
"npm-run-all": "4.0.2",
|
||||
"nrf-intel-hex": "1.3.0",
|
||||
"postcss-url": "8.0.0",
|
||||
"pre-commit": "1.2.2",
|
||||
"request": "2.83.0",
|
||||
"request": "2.88.0",
|
||||
"rimraf": "2.6.1",
|
||||
"standard-version": "4.2.0",
|
||||
"stylelint": "7.13.0",
|
||||
"svg-sprite": "1.3.7",
|
||||
"ts-loader": "2.3.1",
|
||||
"ts-node": "3.0.4",
|
||||
"tslint": "5.5.0",
|
||||
"typescript": "2.5.2",
|
||||
"webpack": "2.4.1"
|
||||
"source-map-support": "0.5.9",
|
||||
"stylelint": "10.0.0",
|
||||
"svg-sprite": "1.5.0",
|
||||
"ts-loader": "5.3.3",
|
||||
"ts-node": "8.3.0",
|
||||
"tslint": "5.18.0",
|
||||
"typescript": "3.5.3",
|
||||
"webpack": "4.29.5",
|
||||
"webpack-cli": "3.2.3"
|
||||
},
|
||||
"pre-commit": [
|
||||
"precommit-msg"
|
||||
@@ -70,33 +87,21 @@
|
||||
"scripts": {
|
||||
"postinstall": "lerna bootstrap",
|
||||
"precommit-msg": "Git precommit hook is running... & exit 0",
|
||||
"test": "run-p -sn test:test-serializer test:uhk-common",
|
||||
"test:test-serializer": "lerna exec --scope test-serializer npm test",
|
||||
"test:uhk-common": "lerna exec --scope uhk-common npm test",
|
||||
"test:uhk-web": "lerna exec --scope uhk-web npm test",
|
||||
"lint": "run-s -scn lint:ts lint:style",
|
||||
"lint:ts": "run-p -sn lint:ts:electron-main lint:ts:electron-renderer lint:ts:web lint:ts:test-serializer lint:ts:uhk-usb",
|
||||
"lint:ts:electron-main": "tslint --type-check --project ./packages/uhk-agent/tsconfig.json",
|
||||
"lint:ts:electron-renderer": "tslint --type-check --project ./packages/uhk-web/src/tsconfig.renderer.json",
|
||||
"lint:ts:web": "tslint --type-check --project ./packages/uhk-web/src/tsconfig.app.json",
|
||||
"lint:ts:test-serializer": "tslint --type-check --project ./packages/test-serializer/tsconfig.json",
|
||||
"lint:ts:uhk-usb": "tslint --type-check --project ./packages/uhk-usb/tsconfig.json",
|
||||
"lint:style": "stylelint \"packages/uhk-agent/src/**/*.scss\" \"packages/uhk-web/src/**/*.scss\" --syntax scss",
|
||||
"build": "run-s build:common build:usb build:web build:electron",
|
||||
"build:web": "lerna exec --scope uhk-web npm run build",
|
||||
"build:electron": "cross-env AOT_BUILD=true run-s -sn build:electron:renderer build:electron:main",
|
||||
"build:electron:main": "lerna exec --scope uhk-agent npm run build",
|
||||
"build:electron:renderer": "lerna exec --scope uhk-web npm run build:renderer",
|
||||
"build:common": "lerna exec --scope uhk-common npm run build",
|
||||
"build:usb": "lerna exec --scope uhk-usb npm run build",
|
||||
"test": "lerna run test",
|
||||
"lint": "lerna run lint",
|
||||
"e2e": "lerna run e2e --scope uhk-web",
|
||||
"prebuild": "check-node-version --package",
|
||||
"build": "lerna run build",
|
||||
"server:web": "lerna exec --scope uhk-web npm start",
|
||||
"server:electron": "lerna exec --scope uhk-web npm run server:renderer",
|
||||
"electron": "lerna exec --scope uhk-agent npm start",
|
||||
"standard-version": "standard-version",
|
||||
"electron:spe": "lerna exec --scope uhk-agent npm run electron:spe",
|
||||
"pack": "node ./scripts/release.js",
|
||||
"sprites": "node ./scripts/generate-svg-sprites",
|
||||
"release": "node ./scripts/release.js",
|
||||
"clean": "lerna exec rimraf ./node_modules ./dist"
|
||||
},
|
||||
"dependencies": {}
|
||||
"clean": "lerna exec rimraf ./node_modules ./dist && rimraf ./node_modules ./dist ./tmp",
|
||||
"predeploy-gh-pages": "lerna run build:web --scope=uhk-web",
|
||||
"deploy-gh-pages": "gh-pages -d packages/uhk-web/dist",
|
||||
"convert-user-config-to-bin": "node -r ts-node/register ./packages/usb/user-config-json-to-bin.ts"
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/kboot/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './src';
|
||||
8
packages/kboot/jasmine.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"spec_dir": "test",
|
||||
"spec_files": [
|
||||
"**/*[sS]pec.ts"
|
||||
],
|
||||
"stopSpecOnExpectationFailure": true,
|
||||
"random": false
|
||||
}
|
||||
532
packages/kboot/package-lock.json
generated
Normal file
23
packages/kboot/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "kboot",
|
||||
"main": "dist/index.js",
|
||||
"version": "0.0.0",
|
||||
"description": "Javascript implementation of the Kinetis Bootloader protocol",
|
||||
"author": "Ultimate Gadget Laboratories",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:UltimateHackingKeyboard/agent.git"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"byte-data": "^16.0.3",
|
||||
"tslib": "^1.10.0",
|
||||
"node-hid": ">= 0.7.9"
|
||||
},
|
||||
"peer-dependencies": {},
|
||||
"scripts": {
|
||||
"build": "tsc --project ./src/tsconfig.json",
|
||||
"lint": "tslint --project tsconfig.json"
|
||||
}
|
||||
}
|
||||
65
packages/kboot/readme.md
Normal file
@@ -0,0 +1,65 @@
|
||||
Javascript implementation of the Kinetis Bootloader protocol
|
||||
============================================================
|
||||
|
||||
Based on the [Kinetis Bootloader v2.0.0 Reference Manual](https://github.com/UltimateHackingKeyboard/bootloader/blob/master/doc/Kinetis%20Bootloader%20v2.0.0%20Reference%20Manual.pdf)
|
||||
|
||||
## Supported communication channels/protocols
|
||||
- [x] USB
|
||||
- [ ] I2C
|
||||
- [ ] SPI
|
||||
- [ ] CAN
|
||||
- [ ] UART
|
||||
|
||||
## Supported Commands
|
||||
We implemented only the commands that is used in UHK software.
|
||||
If someone needs other commands, (s)he can easily implement it based on existing.
|
||||
|
||||
- [x] GetProperty
|
||||
- [ ] SetProperty
|
||||
- [ ] FlashEraseAll
|
||||
- [x] FlashEraseRegion
|
||||
- [x] FlashEraseAllUnsecure
|
||||
- [x] ReadMemory
|
||||
- [x] WriteMemory
|
||||
- [ ] FillMemory
|
||||
- [x] FlashSecurityDisable
|
||||
- [ ] Execute
|
||||
- [ ] Call
|
||||
- [x] Reset
|
||||
- [ ] FlashProgramOnce
|
||||
- [ ] FlashReadOnce
|
||||
- [ ] FlashReadResource
|
||||
- [ ] ConfigureQuadSpi
|
||||
- [ ] ReliableUpdate
|
||||
- [x] ConfigureI2c
|
||||
- [ ] ConfigureSpi
|
||||
- [ ] ConfigureCan
|
||||
|
||||
## How to use
|
||||
|
||||
```Typescript
|
||||
// Initialize peripheral
|
||||
const usbPeripheral = new UsbPeripheral({ productId: 1, vendorId: 1 });
|
||||
// Initialize Kboot
|
||||
const kboot = new KBoot(usbPeripheral);
|
||||
// Call the command
|
||||
const version = await kboot.getBootloaderVersion();
|
||||
// ... more commands
|
||||
|
||||
// Close the communication channel. Release resources
|
||||
kboot.close();
|
||||
```
|
||||
|
||||
If you have to communicate other I2C device over USB call `kboot.configureI2c(i2cId)` before the command.
|
||||
|
||||
```Typescript
|
||||
const usbPeripheral = new UsbPeripheral({ productId: 1, vendorId: 1 });
|
||||
const kboot = new KBoot(usbPeripheral);
|
||||
|
||||
// Get the bootloader version of I2C device
|
||||
await kboot.configureI2c(i2cId);
|
||||
const version = await kboot.getBootloaderVersion();
|
||||
```
|
||||
|
||||
## TODO
|
||||
- [ ] Improve exception handling
|
||||
2
packages/kboot/src/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const DEFAULT_USB_PID = 0x0073;
|
||||
export const DEFAULT_USB_VID = 0x15A2;
|
||||
23
packages/kboot/src/enums/commands.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export enum Commands {
|
||||
FlashEraseAll = 0x01,
|
||||
FlashEraseRegion = 0x02,
|
||||
ReadMemory = 0x03,
|
||||
WriteMemory = 0x04,
|
||||
FillMemory = 0x05,
|
||||
FlashSecurityDisable = 0x06,
|
||||
GetProperty = 0x07,
|
||||
ReceiveSBFile = 0x08,
|
||||
Execute = 0x09,
|
||||
Call = 0x0A,
|
||||
Reset = 0x0B,
|
||||
SetProperty = 0x0C,
|
||||
FlashEraseAllUnsecure = 0x0D,
|
||||
FlashProgramOnce = 0x0E,
|
||||
FlashReadOnce = 0x0F,
|
||||
FlashReadResource = 0x10,
|
||||
ConfigureQuadSpi = 0x11,
|
||||
ReliableUpdate = 0x12,
|
||||
ConfigureI2c = 0xc1,
|
||||
ConfigureSpi = 0xc2,
|
||||
ConfigureCan = 0xc3
|
||||
}
|
||||
5
packages/kboot/src/enums/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './commands';
|
||||
export * from './memory-ids';
|
||||
export * from './properties';
|
||||
export * from './response-codes';
|
||||
export * from './response-tags';
|
||||
5
packages/kboot/src/enums/memory-ids.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum MemoryIds {
|
||||
Internal = 0,
|
||||
Spi0 = 1,
|
||||
ExecuteOnly = 0x10
|
||||
}
|
||||
26
packages/kboot/src/enums/properties.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export enum Properties {
|
||||
BootloaderVersion = 0x01,
|
||||
AvailablePeripherals = 0x02,
|
||||
FlashStartAddress = 0x03,
|
||||
FlashSize = 0x04,
|
||||
FlashSectorSize = 0x05,
|
||||
FlashBlockCount = 0x06,
|
||||
AvailableCommands = 0x07,
|
||||
CrcCheckStatus = 0x08,
|
||||
VerifyWrites = 0x0A,
|
||||
MaxPacketSize = 0x0B,
|
||||
ReservedRegions = 0x0C,
|
||||
ValidateRegions = 0x0D,
|
||||
RAMStartAddress = 0x0E,
|
||||
RAMSize = 0x0F,
|
||||
SystemDeviceIdent = 0x10,
|
||||
FlashSecurityState = 0x11,
|
||||
UniqueDeviceIdent = 0x12,
|
||||
FlashFacSupport = 0x13,
|
||||
FlashAccessSegmentSize = 0x14,
|
||||
FlashAccessSegmentCount = 0x15,
|
||||
FlashReadMargin = 0x16,
|
||||
QspiInitStatus = 0x17,
|
||||
TargetVersion = 0x18,
|
||||
ExternalMemoryAttributes = 0x19
|
||||
}
|
||||
76
packages/kboot/src/enums/response-codes.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export enum ResponseCodes {
|
||||
// Generic status codes.
|
||||
Success = 0,
|
||||
Fail = 1,
|
||||
ReadOnly = 2,
|
||||
OutOfRange = 3,
|
||||
InvalidArgument = 4,
|
||||
|
||||
// Flash driver errors.
|
||||
FlashSizeError = 100,
|
||||
FlashAlignmentError = 101,
|
||||
FlashAddressError = 102,
|
||||
FlashAccessError = 103,
|
||||
FlashProtectionViolation = 104,
|
||||
FlashCommandFailure = 105,
|
||||
FlashUnknownProperty = 106,
|
||||
|
||||
// I2C driver errors.
|
||||
I2C_SlaveTxUnderrun = 200,
|
||||
I2C_SlaveRxOverrun = 201,
|
||||
I2C_AribtrationLost = 202,
|
||||
|
||||
// SPI driver errors.
|
||||
SPI_SlaveTxUnderrun = 300,
|
||||
SPI_SlaveRxOverrun = 301,
|
||||
|
||||
// QuadSPI driver errors
|
||||
QSPI_FlashSizeError = 400,
|
||||
QSPI_FlashAlignmentError = 401,
|
||||
QSPI_FlashAddressError = 402,
|
||||
QSPI_FlashCommandFailure = 403,
|
||||
QSPI_FlashUnknownProperty = 404,
|
||||
QSPI_NotConfigured = 405,
|
||||
QSPI_CommandNotSupported = 406,
|
||||
|
||||
// Bootloader errors.
|
||||
UnknownCommand = 10000,
|
||||
SecurityViolation = 10001,
|
||||
AbortDataPhase = 10002,
|
||||
PingError = 10003,
|
||||
NoResponse = 10004,
|
||||
NoResponseExpected = 10005,
|
||||
|
||||
// SB loader errors.
|
||||
RomLdrSectionOverrun = 10100,
|
||||
RomLdrSignature = 10101,
|
||||
RomLdrSectionLength = 10102,
|
||||
RomLdrUnencryptedOnly = 10103,
|
||||
RomLdrEOFReached = 10104,
|
||||
RomLdrChecksum = 10105,
|
||||
RomLdrCrc32Error = 10106,
|
||||
RomLdrUnknownCommand = 10107,
|
||||
RomLdrIdNotFound = 10108,
|
||||
RomLdrDataUnderrun = 10109,
|
||||
RomLdrJumpReturned = 10110,
|
||||
RomLdrCallFailed = 10111,
|
||||
RomLdrKeyNotFound = 10112,
|
||||
RomLdrSecureOnly = 10113,
|
||||
|
||||
// Memory interface errors.
|
||||
MemoryRangeInvalid = 10200,
|
||||
MemoryReadFailed = 10201,
|
||||
MemoryWriteFailed = 10202,
|
||||
|
||||
// Property store errors.
|
||||
UnknownProperty = 10300,
|
||||
ReadOnlyProperty = 10301,
|
||||
InvalidPropertyValue = 10302,
|
||||
|
||||
// Property store errors.
|
||||
AppCrcCheckPassed = 10400,
|
||||
AppCrcCheckFailed = 10401,
|
||||
AppCrcCheckInactive = 10402,
|
||||
AppCrcCheckInvalid = 10403,
|
||||
AppCrcCheckOutOfRange = 10404
|
||||
}
|
||||
7
packages/kboot/src/enums/response-tags.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum ResponseTags {
|
||||
Generic = 0xA0,
|
||||
ReadMemory = 0xA3,
|
||||
Property = 0xA7,
|
||||
FlashReadOnce = 0xAF,
|
||||
FlashReadResource = 0xB0
|
||||
}
|
||||
6
packages/kboot/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from './kboot';
|
||||
export * from './enums';
|
||||
export * from './models';
|
||||
export * from './peripheral';
|
||||
export * from './usb-peripheral';
|
||||
export * from './util';
|
||||
194
packages/kboot/src/kboot.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { debug } from 'debug';
|
||||
import { pack } from 'byte-data';
|
||||
|
||||
import { Peripheral } from './peripheral';
|
||||
import { Commands, MemoryIds, Properties, ResponseCodes, ResponseTags } from './enums';
|
||||
import { BootloaderVersion, CommandOption, CommandResponse, DataOption } from './models';
|
||||
|
||||
const logger = debug('kboot');
|
||||
|
||||
export class KBoot {
|
||||
constructor(private peripheral: Peripheral) {
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this.peripheral.open();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.peripheral.close();
|
||||
}
|
||||
|
||||
// ================= Read properties ==================
|
||||
async getProperty(property: Properties, memoryId = MemoryIds.Internal): Promise<CommandResponse> {
|
||||
const command: CommandOption = {
|
||||
command: Commands.GetProperty,
|
||||
params: [
|
||||
...pack(property, { bits: 32 }),
|
||||
...pack(memoryId, { bits: 32 })
|
||||
]
|
||||
};
|
||||
|
||||
const response = await this.peripheral.sendCommand(command);
|
||||
|
||||
if (response.tag !== ResponseTags.Property) {
|
||||
throw new Error('Response tag is not property response');
|
||||
}
|
||||
|
||||
if (response.code === ResponseCodes.UnknownProperty) {
|
||||
throw new Error('Unknown property!');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
throw new Error(`Unknown error. Error code:${response.code}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async getBootloaderVersion(): Promise<BootloaderVersion> {
|
||||
const response = await this.getProperty(Properties.BootloaderVersion);
|
||||
|
||||
const version: BootloaderVersion = {
|
||||
bugfix: response.raw[12],
|
||||
minor: response.raw[13],
|
||||
major: response.raw[14],
|
||||
protocolName: String.fromCharCode(response.raw[15])
|
||||
};
|
||||
logger('bootloader version %o');
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
// TODO: Implement other get/set property wrappers
|
||||
// ================= End read properties ==================
|
||||
|
||||
async flashSecurityDisable(key: number[]): Promise<void> {
|
||||
if (key.length !== 8) {
|
||||
throw new Error('Flash security key must be 8 byte');
|
||||
}
|
||||
|
||||
const command: CommandOption = {
|
||||
command: Commands.FlashSecurityDisable,
|
||||
params: [...key]
|
||||
};
|
||||
|
||||
const response = await this.peripheral.sendCommand(command);
|
||||
|
||||
if (response.tag !== ResponseTags.Generic) {
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
throw new Error(`Can not disable flash security`);
|
||||
}
|
||||
}
|
||||
|
||||
async flashEraseRegion(startAddress: number, count: number): Promise<void> {
|
||||
const command: CommandOption = {
|
||||
command: Commands.FlashEraseRegion,
|
||||
params: [
|
||||
...pack(startAddress, { bits: 32 }),
|
||||
...pack(count, { bits: 32 })
|
||||
]
|
||||
};
|
||||
|
||||
const response = await this.peripheral.sendCommand(command);
|
||||
|
||||
if (response.tag !== ResponseTags.Generic) {
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
throw new Error(`Can not disable flash security`);
|
||||
}
|
||||
}
|
||||
|
||||
async flashEraseAllUnsecure(): Promise<void> {
|
||||
const command: CommandOption = {
|
||||
command: Commands.FlashEraseAllUnsecure,
|
||||
params: []
|
||||
};
|
||||
|
||||
const response = await this.peripheral.sendCommand(command);
|
||||
|
||||
if (response.tag !== ResponseTags.Generic) {
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
throw new Error(`Can not disable flash security`);
|
||||
}
|
||||
}
|
||||
|
||||
async readMemory(startAddress: number, count: number): Promise<any> {
|
||||
return this.peripheral.readMemory(startAddress, count);
|
||||
}
|
||||
|
||||
async writeMemory(options: DataOption): Promise<void> {
|
||||
return this.peripheral.writeMemory(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the bootloader
|
||||
*/
|
||||
async reset(): Promise<void> {
|
||||
const command: CommandOption = {
|
||||
command: Commands.Reset,
|
||||
params: []
|
||||
};
|
||||
|
||||
let response: CommandResponse;
|
||||
try {
|
||||
response = await this.peripheral.sendCommand(command);
|
||||
} catch (error) {
|
||||
if (error.message === 'could not read from HID device') {
|
||||
logger('Ignoring missing response from reset command.');
|
||||
|
||||
this.close();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (response.tag !== ResponseTags.Generic) {
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
throw new Error(`Unknown error. Error code:${response.code}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call it before send data to I2C
|
||||
* @param address - The address of the I2C
|
||||
* @param [speed=64] - Speed of the I2C
|
||||
*/
|
||||
async configureI2c(address: number, speed = 64): Promise<void> {
|
||||
|
||||
if (address > 127) {
|
||||
throw new Error('Only 7-bit i2c address is supported');
|
||||
}
|
||||
|
||||
const command: CommandOption = {
|
||||
command: Commands.ConfigureI2c,
|
||||
params: [
|
||||
...pack(address, { bits: 32 }),
|
||||
...pack(speed, { bits: 32 })
|
||||
]
|
||||
};
|
||||
|
||||
const response = await this.peripheral.sendCommand(command);
|
||||
|
||||
if (response.tag !== ResponseTags.Generic) {
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
throw new Error(`Unknown error. Error code:${response.code}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
packages/kboot/src/models/bootloader-version.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface BootloaderVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
bugfix: number;
|
||||
protocolName: string;
|
||||
}
|
||||
7
packages/kboot/src/models/command-option.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Commands } from '../enums';
|
||||
|
||||
export interface CommandOption {
|
||||
command: Commands;
|
||||
hasDataPhase?: boolean;
|
||||
params?: number[];
|
||||
}
|
||||
7
packages/kboot/src/models/command-response.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ResponseCodes, ResponseTags } from '../enums';
|
||||
|
||||
export interface CommandResponse {
|
||||
tag: ResponseTags;
|
||||
code: ResponseCodes;
|
||||
raw: Buffer;
|
||||
}
|
||||
4
packages/kboot/src/models/data-option.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface DataOption {
|
||||
startAddress: number;
|
||||
data: Buffer;
|
||||
}
|
||||
5
packages/kboot/src/models/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './bootloader-version';
|
||||
export * from './command-option';
|
||||
export * from './command-response';
|
||||
export * from './data-option';
|
||||
export * from './usb';
|
||||
7
packages/kboot/src/models/usb.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface USB {
|
||||
vendorId: number;
|
||||
productId: number;
|
||||
interface?: number;
|
||||
usage?: number;
|
||||
usePage?: number;
|
||||
}
|
||||
13
packages/kboot/src/peripheral.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { CommandOption, CommandResponse, DataOption } from './models';
|
||||
|
||||
export interface Peripheral {
|
||||
open(): void;
|
||||
|
||||
close(): void;
|
||||
|
||||
sendCommand(options: CommandOption): Promise<CommandResponse>;
|
||||
|
||||
writeMemory(data: DataOption): Promise<void>;
|
||||
|
||||
readMemory(startAddress: number, count: number): Promise<Buffer>;
|
||||
}
|
||||
7
packages/kboot/src/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "../dist"
|
||||
}
|
||||
}
|
||||
275
packages/kboot/src/usb-peripheral.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { debug } from 'debug';
|
||||
import { devices, HID } from 'node-hid';
|
||||
import { pack } from 'byte-data';
|
||||
|
||||
import { Peripheral } from './peripheral';
|
||||
import { CommandOption, CommandResponse, DataOption, USB } from './models';
|
||||
import { convertLittleEndianNumber, convertToHexString, deviceFinder, encodeCommandOption } from './util';
|
||||
import { decodeCommandResponse } from './util/usb/decode-command-response';
|
||||
import { validateCommandParams } from './util/usb/encode-command-option';
|
||||
import { Commands, ResponseTags } from './enums';
|
||||
import { snooze } from './util/snooze';
|
||||
|
||||
const logger = debug('kboot:usb');
|
||||
const WRITE_DATA_STREAM_PACKAGE_LENGTH = 32;
|
||||
|
||||
export class UsbPeripheral implements Peripheral {
|
||||
private _device: HID;
|
||||
private _responseBuffer: Buffer;
|
||||
private _dataBuffer: Buffer;
|
||||
private _hidError: any;
|
||||
|
||||
constructor(private options: USB) {
|
||||
logger('constructor options: %o', options);
|
||||
}
|
||||
|
||||
open(): void {
|
||||
this._hidError = undefined;
|
||||
|
||||
if (this._device) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger('Available devices');
|
||||
const device = devices()
|
||||
.map(x => {
|
||||
logger('%o', x);
|
||||
|
||||
return x;
|
||||
})
|
||||
.find(deviceFinder(this.options));
|
||||
|
||||
if (!device) {
|
||||
logger('USB device can not be found %o', this.options);
|
||||
throw new Error('USB device can not be found');
|
||||
}
|
||||
|
||||
this._responseBuffer = Buffer.alloc(0);
|
||||
this._dataBuffer = Buffer.alloc(0);
|
||||
|
||||
this._device = new HID(device.path);
|
||||
this._device.on('data', this._usbDataListener.bind(this));
|
||||
this._device.on('error', this._usbErrorListener.bind(this));
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this._device) {
|
||||
this._device.close();
|
||||
this._device = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async sendCommand(options: CommandOption): Promise<CommandResponse> {
|
||||
validateCommandParams(options.params);
|
||||
const data = encodeCommandOption(options);
|
||||
this._send(data);
|
||||
|
||||
return this._getNextCommandResponse();
|
||||
}
|
||||
|
||||
writeMemory(option: DataOption): Promise<void> {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
try {
|
||||
const command: CommandOption = {
|
||||
command: Commands.WriteMemory,
|
||||
hasDataPhase: true,
|
||||
params: [
|
||||
...pack(option.startAddress, { bits: 32 }),
|
||||
...pack(option.data.length, { bits: 32 })
|
||||
]
|
||||
};
|
||||
|
||||
const firsCommandResponse = await this.sendCommand(command);
|
||||
if (firsCommandResponse.tag !== ResponseTags.Generic) {
|
||||
logger('Invalid write memory response! %o', firsCommandResponse);
|
||||
return reject(new Error('Invalid write memory response!'));
|
||||
}
|
||||
|
||||
if (firsCommandResponse.code !== 0) {
|
||||
logger('Non zero write memory response! %o', firsCommandResponse);
|
||||
return reject(new Error(`Non zero write memory response! Response code: ${firsCommandResponse.code}`));
|
||||
}
|
||||
|
||||
for (let i = 0; i < option.data.length; i = i + WRITE_DATA_STREAM_PACKAGE_LENGTH) {
|
||||
if (this._hidError) {
|
||||
logger('Throw USB error %O', this._hidError);
|
||||
|
||||
return reject(new Error('USB error while write data'));
|
||||
}
|
||||
|
||||
const slice = option.data.slice(i, i + WRITE_DATA_STREAM_PACKAGE_LENGTH);
|
||||
|
||||
const writeData = [
|
||||
2, // USB channel
|
||||
0,
|
||||
slice.length,
|
||||
0, // TODO: What is it?
|
||||
...slice
|
||||
];
|
||||
|
||||
logger('send data %o', convertToHexString(writeData));
|
||||
this._device.write(writeData);
|
||||
}
|
||||
|
||||
const secondCommandResponse = await this._getNextCommandResponse();
|
||||
if (secondCommandResponse.tag !== ResponseTags.Generic) {
|
||||
logger('Invalid write memory final response %o', secondCommandResponse);
|
||||
return reject(new Error('Invalid write memory final response!'));
|
||||
}
|
||||
|
||||
if (secondCommandResponse.code !== 0) {
|
||||
logger('Non zero write memory final response %o', secondCommandResponse);
|
||||
const msg = `Non zero write memory final response! Response code: ${secondCommandResponse.code}`;
|
||||
return reject(new Error(msg));
|
||||
}
|
||||
|
||||
resolve();
|
||||
} catch (err) {
|
||||
logger('Can not write memory data %O', err);
|
||||
reject(err);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
readMemory(startAddress: number, count: number): Promise<Buffer> {
|
||||
return new Promise<Buffer>(async (resolve, reject) => {
|
||||
try {
|
||||
const command: CommandOption = {
|
||||
command: Commands.ReadMemory,
|
||||
params: [
|
||||
...pack(startAddress, { bits: 32 }),
|
||||
...pack(count, { bits: 32 })
|
||||
]
|
||||
};
|
||||
|
||||
this._resetDataBuffer();
|
||||
this._resetResponseBuffer();
|
||||
const firsCommandResponse = await this.sendCommand(command);
|
||||
if (firsCommandResponse.tag !== ResponseTags.ReadMemory) {
|
||||
logger('Invalid read memory response %o', firsCommandResponse);
|
||||
return reject(new Error('Invalid read memory response!'));
|
||||
}
|
||||
|
||||
if (firsCommandResponse.code !== 0) {
|
||||
logger('Non zero read memory response %o', firsCommandResponse);
|
||||
return reject(new Error(`Non zero read memory response! Response code: ${firsCommandResponse.code}`));
|
||||
}
|
||||
|
||||
const byte4Number = firsCommandResponse.raw.slice(12, 15);
|
||||
const arrivingData = convertLittleEndianNumber(byte4Number);
|
||||
const memoryDataBuffer = await this._readFromDataStream(arrivingData);
|
||||
|
||||
const secondCommandResponse = await this._getNextCommandResponse();
|
||||
if (secondCommandResponse.tag !== ResponseTags.Generic) {
|
||||
logger('Invalid read memory final response %o', secondCommandResponse);
|
||||
return reject(new Error('Invalid read memory final response!'));
|
||||
}
|
||||
|
||||
if (secondCommandResponse.code !== 0) {
|
||||
logger('Non zero read memory final response %o', secondCommandResponse);
|
||||
const msg = `Non zero read memory final response! Response code: ${secondCommandResponse.code}`;
|
||||
return reject(new Error(msg));
|
||||
}
|
||||
|
||||
resolve(memoryDataBuffer);
|
||||
} catch (error) {
|
||||
logger('Read memory error %O', error);
|
||||
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _send(data: number[]): void {
|
||||
this.open();
|
||||
|
||||
logger('send data %o', `<${convertToHexString(data)}>`);
|
||||
this._device.write(data);
|
||||
}
|
||||
|
||||
private _usbDataListener(data: Buffer): void {
|
||||
logger('received data %o', `[${convertToHexString(data)}]`);
|
||||
|
||||
const channel = data[0];
|
||||
|
||||
switch (channel) {
|
||||
case 3:
|
||||
this._responseBuffer = Buffer.concat([this._responseBuffer, data]);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
this._dataBuffer = Buffer.concat([this._dataBuffer, data]);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger('Unknown USB channel %o', channel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _usbErrorListener(error: any): void {
|
||||
logger('USB stream error %O', error);
|
||||
this._hidError = error;
|
||||
}
|
||||
|
||||
private _readFromCommandStream(byte = 36, timeout = 15000): Promise<Buffer> {
|
||||
return this._readFromBuffer('_responseBuffer', byte, timeout);
|
||||
}
|
||||
|
||||
private _readFromDataStream(byte = 36, timeout = 15000): Promise<Buffer> {
|
||||
return this._readFromBuffer('_dataBuffer', byte, timeout);
|
||||
}
|
||||
|
||||
private _readFromBuffer(bufferName: string, byte: number, timeout: number): Promise<Buffer> {
|
||||
return new Promise<Buffer>(async (resolve, reject) => {
|
||||
const startTime = new Date();
|
||||
while (startTime.getTime() + timeout > new Date().getTime()) {
|
||||
|
||||
if (this._hidError) {
|
||||
const err = this._hidError;
|
||||
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
const buffer: Buffer = this[bufferName];
|
||||
if (buffer.length >= byte) {
|
||||
const data = buffer.slice(0, byte);
|
||||
|
||||
if (buffer.length === byte) {
|
||||
this[bufferName] = Buffer.alloc(0);
|
||||
} else {
|
||||
const newDataBuffer = Buffer.alloc(buffer.length - byte);
|
||||
buffer.copy(newDataBuffer, 0, byte);
|
||||
this[bufferName] = newDataBuffer;
|
||||
}
|
||||
|
||||
logger(`read from ${bufferName}: %O`, convertToHexString(data));
|
||||
|
||||
return resolve(data);
|
||||
}
|
||||
|
||||
await snooze(100);
|
||||
}
|
||||
|
||||
logger('Timeout while try to read from buffer');
|
||||
reject(new Error('Timeout while try to read from buffer'));
|
||||
});
|
||||
}
|
||||
|
||||
private _resetDataBuffer(): void {
|
||||
this._dataBuffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
private _resetResponseBuffer(): void {
|
||||
this._responseBuffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
private async _getNextCommandResponse(): Promise<CommandResponse> {
|
||||
const response = await this._readFromCommandStream();
|
||||
const commandResponse = decodeCommandResponse(response);
|
||||
logger('next command response: %o', commandResponse);
|
||||
|
||||
return commandResponse;
|
||||
}
|
||||
}
|
||||
6
packages/kboot/src/util/encode-string-to-parameters.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const encodeStringToParams = (data: string): Int8Array => {
|
||||
// const buffer = Buffer.from(data);
|
||||
|
||||
// return new Int8Array(buffer, 0);
|
||||
return new Int8Array(0);
|
||||
};
|
||||
22
packages/kboot/src/util/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export * from './encode-string-to-parameters';
|
||||
export * from './response-parser';
|
||||
export * from './usb';
|
||||
|
||||
export const convertToHexString = (arr: number[] | Buffer): string => {
|
||||
let str = '';
|
||||
|
||||
for (const n of arr) {
|
||||
let hex = n.toString(16);
|
||||
if (hex.length < 2) {
|
||||
hex = '0' + hex;
|
||||
}
|
||||
|
||||
if (str.length > 0) {
|
||||
str += ' ';
|
||||
}
|
||||
|
||||
str += hex;
|
||||
}
|
||||
|
||||
return str;
|
||||
};
|
||||
21
packages/kboot/src/util/response-parser.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ResponseCodes, ResponseTags } from '../enums';
|
||||
|
||||
export const convertLittleEndianNumber = (data: Buffer): number => {
|
||||
let value = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
value += data[i] << (8 * i);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const getResponseCode = (response: Buffer): ResponseCodes => {
|
||||
const data = response.slice(8, 11);
|
||||
|
||||
return convertLittleEndianNumber(data);
|
||||
};
|
||||
|
||||
export const getResponseTag = (response: Buffer): ResponseTags => {
|
||||
return response[4];
|
||||
};
|
||||
1
packages/kboot/src/util/snooze.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const snooze = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
18
packages/kboot/src/util/usb/decode-command-response.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CommandResponse } from '../../models';
|
||||
import { getResponseCode, getResponseTag } from '../response-parser';
|
||||
|
||||
export const decodeCommandResponse = (response: Buffer): CommandResponse => {
|
||||
if (response.length < 8) {
|
||||
throw new Error('Invalid response length!');
|
||||
}
|
||||
|
||||
if (response[0] !== 3) {
|
||||
throw new Error(`Invalid response command channel!`);
|
||||
}
|
||||
|
||||
return {
|
||||
code: getResponseCode(response),
|
||||
tag: getResponseTag(response),
|
||||
raw: response
|
||||
};
|
||||
};
|
||||
22
packages/kboot/src/util/usb/device-finder.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { Device } from 'node-hid';
|
||||
|
||||
import { USB } from '../../models';
|
||||
|
||||
export const deviceFinder = (usb: USB) => {
|
||||
return (device: Device): boolean => {
|
||||
if (device.productId !== usb.productId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (device.vendorId !== usb.vendorId) {
|
||||
return false;
|
||||
}
|
||||
// TODO: Add interface, usage and usePage filtering
|
||||
// if (!isNullOrUndefined(usb.interface) && device.interface !== -1 && device.interface === usb.interface) {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
46
packages/kboot/src/util/usb/encode-command-option.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { pack } from 'byte-data';
|
||||
|
||||
import { CommandOption } from '../../models';
|
||||
|
||||
/**
|
||||
* Encode the USB Command.
|
||||
* @param option
|
||||
*/
|
||||
export const encodeCommandOption = (option: CommandOption): number[] => {
|
||||
const payload = [
|
||||
option.command,
|
||||
option.hasDataPhase ? 1 : 0,
|
||||
0, // Reserved. Should be 0
|
||||
option.params ? option.params.length / 4 >> 0 : 0 // number of parameters
|
||||
];
|
||||
|
||||
if (option.params) {
|
||||
payload.push(...option.params);
|
||||
}
|
||||
|
||||
const header = [
|
||||
1, // Communication channel
|
||||
0, // TODO: What is it?
|
||||
...pack(payload.length, { bits: 16 }) // payload length in 2 byte
|
||||
];
|
||||
|
||||
const placeholders = new Array(32 - payload.length)
|
||||
.fill(0);
|
||||
|
||||
return [...header, ...payload, ...placeholders];
|
||||
};
|
||||
|
||||
export const validateCommandParams = (params: any[]): void => {
|
||||
if (isNullOrUndefined(params)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(params)) {
|
||||
throw new Error('Command parameters must be an array!');
|
||||
}
|
||||
|
||||
if (params.length > 28) {
|
||||
throw new Error('Maximum 7 (28 bytes) command parameters allowed!');
|
||||
}
|
||||
};
|
||||
2
packages/kboot/src/util/usb/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { deviceFinder } from './device-finder';
|
||||
export { encodeCommandOption } from './encode-command-option';
|
||||
36
packages/kboot/test/kboot.spec.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { BootloaderVersion, CommandResponse, Commands, KBoot, Peripheral, Properties, ResponseCodes, ResponseTags } from '../src';
|
||||
import { TestPeripheral } from './test-peripheral';
|
||||
|
||||
describe('kboot', () => {
|
||||
let kboot: KBoot;
|
||||
let testPeripheral: Peripheral;
|
||||
|
||||
beforeEach(() => {
|
||||
testPeripheral = new TestPeripheral();
|
||||
kboot = new KBoot(testPeripheral);
|
||||
});
|
||||
|
||||
describe('getBootloaderVersion', () => {
|
||||
it('should works', async () => {
|
||||
const sendCommandResponse: CommandResponse = {
|
||||
code: ResponseCodes.Success,
|
||||
tag: ResponseTags.Property,
|
||||
// tslint:disable-next-line:max-line-length
|
||||
raw: Buffer.from([0x03, 0x00, 0x0c, 0x00, 0xa7, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||
};
|
||||
spyOn(testPeripheral, 'sendCommand').and.returnValue(Promise.resolve(sendCommandResponse));
|
||||
const version = await kboot.getBootloaderVersion();
|
||||
const expectedVersion: BootloaderVersion = {
|
||||
protocolName: 'K',
|
||||
major: 2,
|
||||
minor: 0,
|
||||
bugfix: 0
|
||||
};
|
||||
expect(version).toEqual(expectedVersion);
|
||||
expect(testPeripheral.sendCommand).toHaveBeenCalledWith({
|
||||
command: Commands.GetProperty,
|
||||
params: [1, 0, 0, 0, 0, 0, 0, 0]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
27
packages/kboot/test/test-peripheral.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { CommandOption, CommandResponse, DataOption, Peripheral, ResponseCodes, ResponseTags } from '../src';
|
||||
|
||||
export class TestPeripheral implements Peripheral {
|
||||
close(): void {
|
||||
}
|
||||
|
||||
open(): void {
|
||||
}
|
||||
|
||||
sendCommand(options: CommandOption): Promise<CommandResponse> {
|
||||
const response = {
|
||||
tag: ResponseTags.Generic,
|
||||
code: ResponseCodes.Success,
|
||||
raw: Buffer.alloc(0)
|
||||
};
|
||||
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
writeMemory(data: DataOption): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
readMemory(startAddress: number, count: number): Promise<Buffer> {
|
||||
return Promise.resolve(Buffer.alloc(0));
|
||||
}
|
||||
}
|
||||
33
packages/kboot/test/uhk-helpers/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
import * as MemoryMap from 'nrf-intel-hex';
|
||||
|
||||
export enum UhkReenumerationModes {
|
||||
Bootloader = 'bootloader',
|
||||
Buspal = 'buspal',
|
||||
NormalKeyboard = 'normalKeyboard',
|
||||
CompatibleKeyboard = 'compatibleKeyboard'
|
||||
}
|
||||
|
||||
const USB_SCRIPTS_DIR = join(__dirname, '../../../usb');
|
||||
export const reenumerate = (mode: UhkReenumerationModes): void => {
|
||||
const reenumerateScriptFile = join(USB_SCRIPTS_DIR, 'reenumerate.js');
|
||||
const command = [reenumerateScriptFile, mode.toString()].join(' ');
|
||||
|
||||
execSync(
|
||||
command,
|
||||
{
|
||||
cwd: USB_SCRIPTS_DIR,
|
||||
stdio: [0, 1, 2]
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const readBootloaderFirmwareFromHexFile = (): Map<any, any> => {
|
||||
const hexFilePath = join(__dirname, '../../../../tmp/packages/firmware/devices/uhk60-right/firmware.hex');
|
||||
const fileContent = readFileSync(hexFilePath, { encoding: 'utf8' });
|
||||
const memoryMap = MemoryMap.fromHex(fileContent);
|
||||
|
||||
return memoryMap;
|
||||
};
|
||||
10
packages/kboot/test/util/encode-string-to-parameters.spec.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { encodeStringToParams } from '../../src/util';
|
||||
|
||||
describe('encodeStringToParams', () => {
|
||||
xit('should convert 8 character to little endian 4 byte array', () => {
|
||||
const result = encodeStringToParams('0403020108070605');
|
||||
|
||||
const expectedResult = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
17
packages/kboot/test/util/response-parser.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getResponseCode, ResponseCodes } from '../../src';
|
||||
|
||||
describe('response-parser', () => {
|
||||
describe('getResponseCode', () => {
|
||||
it('should return with success', () => {
|
||||
const buffer = Buffer.from([0x03, 0x00, 0x08, 0x00, 0xa7, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]);
|
||||
const responseCode = getResponseCode(buffer);
|
||||
expect(responseCode).toEqual(ResponseCodes.Success);
|
||||
});
|
||||
|
||||
it('should return with UnknownProperty', () => {
|
||||
const buffer = Buffer.from([0x03, 0x00, 0x08, 0x00, 0xa7, 0x00, 0x00, 0x01, 0x3c, 0x28, 0x00, 0x00]);
|
||||
const responseCode = getResponseCode(buffer);
|
||||
expect(responseCode).toEqual(ResponseCodes.UnknownProperty);
|
||||
});
|
||||
});
|
||||
});
|
||||
13
packages/kboot/test/util/usb/decode-command-response.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { decodeCommandResponse } from '../../../src/util/usb/decode-command-response';
|
||||
|
||||
describe('decodeCommandResponse', () => {
|
||||
it('should parse the command', () => {
|
||||
const arr = '03 00 0c 00 a0 00 00 02 00 00 00 00 c1 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
|
||||
.split(' ');
|
||||
const buffer = Buffer.from(arr);
|
||||
const response = decodeCommandResponse(buffer);
|
||||
|
||||
expect(response.code).toEqual(0);
|
||||
expect(response.tag).toEqual(0);
|
||||
});
|
||||
});
|
||||
16
packages/kboot/test/util/usb/encode-command-option.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CommandOption, Commands } from '../../../src';
|
||||
import { encodeCommandOption } from '../../../src/util';
|
||||
|
||||
describe('usb encodeCommandOption', () => {
|
||||
it('should convert correctly', () => {
|
||||
const option: CommandOption = {
|
||||
command: Commands.GetProperty,
|
||||
params: [1, 0, 0, 0, 0, 0, 0, 0]
|
||||
};
|
||||
|
||||
const result = encodeCommandOption(option);
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const expected = [1, 0, 0x0c, 0, 0x07, 0x00, 0x00, 0x02, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
81
packages/kboot/test/zzz-uhk-integration-tests.spec.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { reenumerate, UhkReenumerationModes, readBootloaderFirmwareFromHexFile } from './uhk-helpers';
|
||||
import { BootloaderVersion, UsbPeripheral } from '../src';
|
||||
import { KBoot } from '../src/kboot';
|
||||
import { DataOption } from '../index';
|
||||
|
||||
xdescribe('UHK Integration tests', () => {
|
||||
describe('bootloader', () => {
|
||||
let usb: UsbPeripheral;
|
||||
let kboot: KBoot;
|
||||
beforeEach(() => {
|
||||
reenumerate(UhkReenumerationModes.Bootloader);
|
||||
usb = new UsbPeripheral({ vendorId: 0x1d50, productId: 0x6120 });
|
||||
kboot = new KBoot(usb);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (usb) {
|
||||
usb.close();
|
||||
}
|
||||
|
||||
if (kboot) {
|
||||
kboot.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('get bootloader version', async () => {
|
||||
const expectedVersion: BootloaderVersion = {
|
||||
protocolName: 'K',
|
||||
major: 2,
|
||||
minor: 0,
|
||||
bugfix: 0
|
||||
};
|
||||
const version = await kboot.getBootloaderVersion();
|
||||
|
||||
expect(version).toEqual(expectedVersion);
|
||||
});
|
||||
|
||||
it('disable flash security', () => {
|
||||
const backdoorKey = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
|
||||
|
||||
return kboot
|
||||
.flashSecurityDisable(backdoorKey)
|
||||
.catch(err => {
|
||||
expect(err).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('flash erase region', () => {
|
||||
return kboot
|
||||
.flashEraseRegion(0xc000, 475136)
|
||||
.catch(err => {
|
||||
expect(err).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('read memory', () => {
|
||||
const dataLength = 128;
|
||||
return kboot
|
||||
.readMemory(0xc000, dataLength)
|
||||
.then((data: number[]) => {
|
||||
expect(data).toBeTruthy();
|
||||
expect(data.length).toEqual(dataLength);
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('write memory', async () => {
|
||||
const bootloaderMemoryMap = readBootloaderFirmwareFromHexFile();
|
||||
for (const [startAddress, data] of bootloaderMemoryMap.entries()) {
|
||||
const dataOption: DataOption = {
|
||||
startAddress,
|
||||
data
|
||||
};
|
||||
|
||||
await kboot.writeMemory(dataOption);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
6
packages/kboot/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./"
|
||||
}
|
||||
}
|
||||
895
packages/test-serializer/package-lock.json
generated
@@ -14,18 +14,12 @@
|
||||
"npm": ">=5.1.0 <6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jasmine": "2.6.0",
|
||||
"@types/node": "8.0.30",
|
||||
"jasmine": "2.8.0",
|
||||
"jasmine-core": "2.8.0",
|
||||
"jasmine-node": "2.0.0",
|
||||
"jasmine-ts": "0.2.1",
|
||||
"ts-node": "3.3.0",
|
||||
"uhk-common": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jasmine-ts --config=jasmine.json"
|
||||
"test": "jasmine-ts --config=jasmine.json",
|
||||
"lint": "tslint --project tsconfig.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
]
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// var webpack = require("webpack");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
main: __dirname + '/test-serializer.ts'
|
||||
},
|
||||
target: 'node',
|
||||
output: {
|
||||
path: __dirname,
|
||||
filename: "test-serializer.js"
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.webpack.js', '.web.js', '.ts', '.js'],
|
||||
modules: ['node_modules']
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{ test: /\.ts$/, loader: 'ts-loader', exclude: /node_modules/ }
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
// new webpack.optimize.UglifyJsPlugin({ minimize: true }),
|
||||
],
|
||||
node: {
|
||||
fs: "empty"
|
||||
}
|
||||
|
||||
}
|
||||
811
packages/uhk-agent/package-lock.json
generated
Normal file
@@ -17,23 +17,23 @@
|
||||
"command-line-args": "4.0.7",
|
||||
"decompress": "4.2.0",
|
||||
"decompress-bzip2": "4.0.0",
|
||||
"node-hid": "0.5.4",
|
||||
"node-hid": "0.7.9",
|
||||
"sudo-prompt": "7.0.0",
|
||||
"tmp": "0.0.33",
|
||||
"tslib": "1.10.0",
|
||||
"uhk-common": "^1.0.0",
|
||||
"uhk-usb": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/decompress": "4.2.0",
|
||||
"@types/node": "8.0.33",
|
||||
"@types/tmp": "0.0.33"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron ./dist/electron-main.js",
|
||||
"build": "webpack && npm run install:build-deps && npm run build:usb && npm run download-firmware && npm run copy-blhost",
|
||||
"start": "cross-env DEBUG=kboot* electron ./dist/electron-main.js",
|
||||
"electron:spe": "electron ./dist/electron-main.js --spe",
|
||||
"build": "webpack && npm run install:build-deps && npm run build:usb && npm run download-firmware && npm run copy-to-tmp-folder",
|
||||
"build:usb": "electron-rebuild -w node-hid -p -m ./dist",
|
||||
"lint": "tslint --project tsconfig.json",
|
||||
"install:build-deps": "cd ./dist && npm i",
|
||||
"download-firmware": "node ../../scripts/download-firmware.js",
|
||||
"copy-blhost": "node ../../scripts/copy-blhost.js"
|
||||
"copy-to-tmp-folder": "node ../../scripts/copy-to-tmp-folder.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/// <reference path="./custom_types/command-line-args.d.ts"/>
|
||||
|
||||
import './polyfills';
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
|
||||
import * as path from 'path';
|
||||
@@ -10,21 +10,22 @@ import * as url from 'url';
|
||||
import * as commandLineArgs from 'command-line-args';
|
||||
import { UhkHidDevice, UhkOperations } from 'uhk-usb';
|
||||
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
|
||||
import { LogRegExps } from 'uhk-common';
|
||||
import { CommandLineArgs, LogRegExps } from 'uhk-common';
|
||||
import { DeviceService } from './services/device.service';
|
||||
import { logger } from './services/logger.service';
|
||||
import { AppUpdateService } from './services/app-update.service';
|
||||
import { AppService } from './services/app.service';
|
||||
import { SudoService } from './services/sudo.service';
|
||||
import { UhkBlhost } from '../../uhk-usb/src';
|
||||
import * as isDev from 'electron-is-dev';
|
||||
import { CommandLineInputs } from './models/command-line-inputs';
|
||||
import { setMenu } from './electron-menu';
|
||||
import { loadWindowState, saveWindowState } from './util/window';
|
||||
|
||||
const optionDefinitions = [
|
||||
{name: 'addons', type: Boolean}
|
||||
{name: 'addons', type: Boolean},
|
||||
{name: 'spe', type: Boolean} // simulate privilege escalation error
|
||||
];
|
||||
|
||||
const options: CommandLineInputs = commandLineArgs(optionDefinitions);
|
||||
const options: CommandLineArgs = commandLineArgs(optionDefinitions);
|
||||
|
||||
// import './dev-extension';
|
||||
// require('electron-debug')({ showDevTools: true, enabled: true });
|
||||
@@ -35,7 +36,6 @@ let win: Electron.BrowserWindow;
|
||||
autoUpdater.logger = logger;
|
||||
|
||||
let deviceService: DeviceService;
|
||||
let uhkBlhost: UhkBlhost;
|
||||
let uhkHidDeviceService: UhkHidDevice;
|
||||
let uhkOperations: UhkOperations;
|
||||
let appUpdateService: AppUpdateService;
|
||||
@@ -60,7 +60,13 @@ if (console.debug) {
|
||||
};
|
||||
}
|
||||
|
||||
const isSecondInstance = !app.requestSingleInstanceLock();
|
||||
|
||||
function createWindow() {
|
||||
if (isSecondInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('[Electron Main] Create new window.');
|
||||
let packagesDir;
|
||||
if (isDev) {
|
||||
@@ -71,25 +77,34 @@ function createWindow() {
|
||||
|
||||
logger.info(`[Electron Main] packagesDir: ${packagesDir}`);
|
||||
|
||||
const loadedWindowState = loadWindowState();
|
||||
|
||||
// Create the browser window.
|
||||
win = new BrowserWindow({
|
||||
title: 'UHK Agent',
|
||||
width: 1024,
|
||||
height: 768,
|
||||
x: loadedWindowState.x,
|
||||
y: loadedWindowState.y,
|
||||
width: loadedWindowState.width,
|
||||
height: loadedWindowState.height,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
},
|
||||
icon: 'assets/images/agent-icon.png'
|
||||
icon: path.join(__dirname, 'renderer/assets/images/agent-app-icon.png')
|
||||
});
|
||||
win.setMenuBarVisibility(false);
|
||||
win.maximize();
|
||||
uhkHidDeviceService = new UhkHidDevice(logger);
|
||||
uhkBlhost = new UhkBlhost(logger, packagesDir);
|
||||
uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir);
|
||||
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations);
|
||||
|
||||
if (loadedWindowState.isFullScreen) {
|
||||
win.setFullScreen(true);
|
||||
} else if (loadedWindowState.isMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
|
||||
setMenu(win);
|
||||
uhkHidDeviceService = new UhkHidDevice(logger, options, packagesDir);
|
||||
uhkOperations = new UhkOperations(logger, uhkHidDeviceService, packagesDir);
|
||||
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations, packagesDir);
|
||||
appUpdateService = new AppUpdateService(logger, win, app);
|
||||
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);
|
||||
sudoService = new SudoService(logger);
|
||||
sudoService = new SudoService(logger, options);
|
||||
// and load the index.html of the app.
|
||||
|
||||
win.loadURL(url.format({
|
||||
@@ -103,13 +118,13 @@ function createWindow() {
|
||||
});
|
||||
|
||||
// Emitted when the window is closed.
|
||||
win.on('closed', () => {
|
||||
win.on('closed', async () => {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
logger.info('[Electron Main] win closed');
|
||||
win = null;
|
||||
deviceService.close();
|
||||
await deviceService.close();
|
||||
deviceService = null;
|
||||
appUpdateService = null;
|
||||
appService = null;
|
||||
@@ -124,31 +139,49 @@ function createWindow() {
|
||||
win.webContents.on('crashed', (event: any) => {
|
||||
logger.error(event);
|
||||
});
|
||||
|
||||
win.on('close', () => saveWindowState(win));
|
||||
win.on('resize', () => saveWindowState(win));
|
||||
win.on('move', () => saveWindowState(win));
|
||||
}
|
||||
|
||||
if (isSecondInstance) {
|
||||
app.quit();
|
||||
} else {
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow);
|
||||
app.on('ready', createWindow);
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
app.on('window-all-closed', () => {
|
||||
if (appUpdateService) {
|
||||
appUpdateService.saveFirtsRun();
|
||||
}
|
||||
app.exit();
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
if (appUpdateService) {
|
||||
appUpdateService.saveFirtsRun();
|
||||
}
|
||||
});
|
||||
app.on('will-quit', () => {
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('second-instance', () => {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (win) {
|
||||
if (win.isMinimized()) {
|
||||
win.restore();
|
||||
}
|
||||
win.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here
|
||||
|
||||
37
packages/uhk-agent/src/electron-menu.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { app, BrowserWindow, Menu, MenuItemConstructorOptions, systemPreferences } from 'electron';
|
||||
import * as isDev from 'electron-is-dev';
|
||||
|
||||
export const setMenu = (win: BrowserWindow): void => {
|
||||
if (process.platform !== 'darwin' || isDev) {
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{role: 'quit'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{role: 'cut'},
|
||||
{role: 'copy'},
|
||||
{role: 'paste'},
|
||||
{role: 'delete'},
|
||||
{role: 'selectall'}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// hide "Start Dictation" submenu item in Edit menu
|
||||
systemPreferences.setUserDefault('NSDisabledDictationMenuItem', 'boolean', true as any);
|
||||
// hide "Emoji & Symbols" submenu item in Edit menu
|
||||
systemPreferences.setUserDefault('NSDisabledCharacterPaletteMenuItem', 'boolean', false as any);
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
};
|
||||
@@ -1,3 +1,10 @@
|
||||
export interface CommandLineInputs {
|
||||
/**
|
||||
* addons menu visible or not
|
||||
*/
|
||||
addons?: boolean;
|
||||
/**
|
||||
* simulate privilege escalation error
|
||||
*/
|
||||
spe?: boolean;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ import { SynchrounousResult } from 'tmp';
|
||||
export interface TmpFirmware {
|
||||
rightFirmwarePath: string;
|
||||
leftFirmwarePath: string;
|
||||
packageJsonPath: string;
|
||||
tmpDirectory: SynchrounousResult;
|
||||
}
|
||||
|
||||
6
packages/uhk-agent/src/models/window-state.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Rectangle } from 'electron';
|
||||
|
||||
export interface WindowState extends Rectangle {
|
||||
isMaximized: boolean;
|
||||
isFullScreen: boolean;
|
||||
}
|
||||
@@ -14,6 +14,6 @@
|
||||
"npm": ">=5.1.0 <6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-hid": "0.5.7"
|
||||
"node-hid": "0.7.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ import * as settings from 'electron-settings';
|
||||
import * as isDev from 'electron-is-dev';
|
||||
import * as storage from 'electron-settings';
|
||||
|
||||
import { IpcEvents, LogService } from 'uhk-common';
|
||||
import { AutoUpdateSettings, IpcEvents, LogService } from 'uhk-common';
|
||||
import { MainServiceBase } from './main-service-base';
|
||||
|
||||
export class AppUpdateService extends MainServiceBase {
|
||||
|
||||
private sendAutoUpdateNotification = false;
|
||||
|
||||
constructor(protected logService: LogService,
|
||||
protected win: Electron.BrowserWindow,
|
||||
private app: Electron.App) {
|
||||
@@ -24,16 +27,21 @@ export class AppUpdateService extends MainServiceBase {
|
||||
|
||||
private initListeners() {
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
this.logService.debug('[AppUpdateService] checking for update');
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.checkingForUpdate);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', async (ev: any, info: UpdateInfo) => {
|
||||
this.logService.debug('[AppUpdateService] update available. Downloading started');
|
||||
await autoUpdater.downloadUpdate();
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.updateAvailable, info);
|
||||
});
|
||||
|
||||
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) => {
|
||||
@@ -51,6 +59,7 @@ export class AppUpdateService extends MainServiceBase {
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (ev: any, info: UpdateInfo) => {
|
||||
this.logService.debug('[AppUpdateService] update downloaded');
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.autoUpdateDownloaded, info);
|
||||
});
|
||||
|
||||
@@ -61,32 +70,46 @@ export class AppUpdateService extends MainServiceBase {
|
||||
|
||||
ipcMain.on(IpcEvents.app.appStarted, () => {
|
||||
if (this.checkForUpdateAtStartup()) {
|
||||
this.sendAutoUpdateNotification = false;
|
||||
this.logService.debug('[AppUpdateService] app started. Automatically check for update.');
|
||||
this.checkForUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => {
|
||||
this.logService.debug('[AppUpdateService] checkForUpdate request from renderer process');
|
||||
this.checkForUpdate();
|
||||
ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, (event: Electron.Event, args: any[]) => {
|
||||
const allowPrerelease: boolean = args[0];
|
||||
// tslint:disable-next-line:max-line-length
|
||||
const logMsg = `[AppUpdateService] checkForUpdate request from renderer process. Allow prerelease: ${allowPrerelease}`;
|
||||
this.logService.debug(logMsg);
|
||||
this.sendAutoUpdateNotification = true;
|
||||
this.checkForUpdate(allowPrerelease);
|
||||
});
|
||||
}
|
||||
|
||||
private checkForUpdate() {
|
||||
private checkForUpdate(allowPrerelease = false): void {
|
||||
if (isDev) {
|
||||
const msg = '[AppUpdateService] Application update is not working in dev mode.';
|
||||
this.logService.info(msg);
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
|
||||
|
||||
if (this.sendAutoUpdateNotification) {
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isFirstRun()) {
|
||||
const msg = '[AppUpdateService] Application update is skipping at first run.';
|
||||
this.logService.info(msg);
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
|
||||
|
||||
if (this.sendAutoUpdateNotification) {
|
||||
this.sendIpcToWindow(IpcEvents.autoUpdater.checkForUpdateNotAvailable, msg);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.allowPrerelease = this.allowPreRelease();
|
||||
autoUpdater.allowPrerelease = allowPrerelease;
|
||||
autoUpdater.checkForUpdates()
|
||||
.then(() => {
|
||||
this.logService.debug('[AppUpdateService] checkForUpdate success');
|
||||
@@ -107,12 +130,6 @@ export class AppUpdateService extends MainServiceBase {
|
||||
return firstRunVersion !== this.app.getVersion();
|
||||
}
|
||||
|
||||
private allowPreRelease() {
|
||||
const autoUpdateSettings = this.getAutoUpdateSettings();
|
||||
|
||||
return autoUpdateSettings && autoUpdateSettings.usePreReleaseUpdate;
|
||||
}
|
||||
|
||||
private checkForUpdateAtStartup() {
|
||||
const autoUpdateSettings = this.getAutoUpdateSettings();
|
||||
const checkForUpdate = autoUpdateSettings && autoUpdateSettings.checkForUpdateOnStartUp;
|
||||
@@ -122,10 +139,10 @@ export class AppUpdateService extends MainServiceBase {
|
||||
return checkForUpdate;
|
||||
}
|
||||
|
||||
private getAutoUpdateSettings() {
|
||||
private getAutoUpdateSettings(): AutoUpdateSettings {
|
||||
const value = storage.get('auto-update-settings');
|
||||
if (!value) {
|
||||
return {checkForUpdateOnStartUp: false, usePreReleaseUpdate: false};
|
||||
return {checkForUpdateOnStartUp: false};
|
||||
}
|
||||
|
||||
return JSON.parse(<string>value);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BrowserWindow, ipcMain, shell } from 'electron';
|
||||
import { ipcMain, shell } from 'electron';
|
||||
import { UhkHidDevice } from 'uhk-usb';
|
||||
import * as os from 'os';
|
||||
|
||||
import { AppStartInfo, IpcEvents, LogService } from 'uhk-common';
|
||||
import { MainServiceBase } from './main-service-base';
|
||||
@@ -22,13 +23,14 @@ export class AppService extends MainServiceBase {
|
||||
|
||||
private async handleAppStartInfo(event: Electron.Event) {
|
||||
this.logService.info('[AppService] getAppStartInfo');
|
||||
|
||||
const deviceConnectionState = await this.uhkHidDeviceService.getDeviceConnectionStateAsync();
|
||||
const response: AppStartInfo = {
|
||||
deviceConnectionState,
|
||||
commandLineArgs: {
|
||||
addons: this.options.addons || false
|
||||
},
|
||||
deviceConnected: this.uhkHidDeviceService.deviceConnected(),
|
||||
hasPermission: this.uhkHidDeviceService.hasPermission()
|
||||
platform: process.platform as string,
|
||||
osVersion: os.release()
|
||||
};
|
||||
this.logService.info('[AppService] getAppStartInfo response:', response);
|
||||
return event.sender.send(IpcEvents.app.getAppStartInfoReply, response);
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { ConfigurationReply, DeviceConnectionState, HardwareModules, IpcEvents, IpcResponse, LogService } from 'uhk-common';
|
||||
import { isEqual } from 'lodash';
|
||||
import {
|
||||
ConfigurationReply,
|
||||
DeviceConnectionState,
|
||||
FirmwareUpgradeIpcResponse,
|
||||
getHardwareConfigFromDeviceResponse,
|
||||
HardwareModules,
|
||||
IpcEvents,
|
||||
IpcResponse,
|
||||
LogService,
|
||||
mapObjectToUserConfigBinaryBuffer,
|
||||
SaveUserConfigurationData,
|
||||
UpdateFirmwareData
|
||||
} from 'uhk-common';
|
||||
import { snooze, UhkHidDevice, UhkOperations } from 'uhk-usb';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { emptyDir } from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import 'rxjs/add/observable/interval';
|
||||
import 'rxjs/add/operator/startWith';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/do';
|
||||
import 'rxjs/add/operator/distinctUntilChanged';
|
||||
|
||||
import { saveTmpFirmware } from '../util/save-extract-firmware';
|
||||
import { TmpFirmware } from '../models/tmp-firmware';
|
||||
import { QueueManager } from './queue-manager';
|
||||
import {
|
||||
backupUserConfiguration,
|
||||
getBackupUserConfigurationContent,
|
||||
getPackageJsonFromPathAsync,
|
||||
saveTmpFirmware
|
||||
} from '../util';
|
||||
|
||||
/**
|
||||
* IpcMain pair of the UHK Communication
|
||||
@@ -22,14 +33,20 @@ import { QueueManager } from './queue-manager';
|
||||
* - Read UserConfiguration from the UHK Device
|
||||
*/
|
||||
export class DeviceService {
|
||||
private pollTimer$: Subscription;
|
||||
private _pollerAllowed: boolean;
|
||||
private _uhkDevicePolling: boolean;
|
||||
private queueManager = new QueueManager();
|
||||
|
||||
constructor(private logService: LogService,
|
||||
private win: Electron.BrowserWindow,
|
||||
private device: UhkHidDevice,
|
||||
private operations: UhkOperations) {
|
||||
this.pollUhkDevice();
|
||||
private operations: UhkOperations,
|
||||
private rootDir: string) {
|
||||
this.startPollUhkDevice();
|
||||
this.uhkDevicePoller()
|
||||
.catch(error => {
|
||||
this.logService.error('[DeviceService] UHK Device poller error', error);
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.device.saveUserConfiguration, (...args: any[]) => {
|
||||
this.queueManager.add({
|
||||
@@ -58,7 +75,25 @@ export class DeviceService {
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.device.startConnectionPoller, this.pollUhkDevice.bind(this));
|
||||
ipcMain.on(IpcEvents.device.startConnectionPoller, this.startPollUhkDevice.bind(this));
|
||||
|
||||
ipcMain.on(IpcEvents.device.recoveryDevice, (...args: any[]) => {
|
||||
this.queueManager.add({
|
||||
method: this.recoveryDevice,
|
||||
bind: this,
|
||||
params: args,
|
||||
asynchronous: true
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.device.enableUsbStackTest, (...args: any[]) => {
|
||||
this.queueManager.add({
|
||||
method: this.enableUsbStackTest,
|
||||
bind: this,
|
||||
params: args,
|
||||
asynchronous: true
|
||||
});
|
||||
});
|
||||
|
||||
logService.debug('[DeviceService] init success');
|
||||
}
|
||||
@@ -71,17 +106,20 @@ export class DeviceService {
|
||||
let response: ConfigurationReply;
|
||||
|
||||
try {
|
||||
await this.stopPollUhkDevice();
|
||||
|
||||
await this.device.waitUntilKeyboardBusy();
|
||||
const result = await this.operations.loadConfigurations();
|
||||
const modules: HardwareModules = {
|
||||
leftModuleInfo: await this.operations.getLeftModuleVersionInfo(),
|
||||
rightModuleInfo: await this.operations.getRightModuleVersionInfo()
|
||||
};
|
||||
const modules: HardwareModules = await this.getHardwareModules(false);
|
||||
|
||||
const hardwareConfig = getHardwareConfigFromDeviceResponse(result.hardwareConfiguration);
|
||||
const uniqueId = hardwareConfig.uniqueId;
|
||||
|
||||
response = {
|
||||
success: true,
|
||||
...result,
|
||||
modules
|
||||
modules,
|
||||
backupConfiguration: await getBackupUserConfigurationContent(this.logService, uniqueId)
|
||||
};
|
||||
} catch (error) {
|
||||
response = {
|
||||
@@ -90,38 +128,78 @@ export class DeviceService {
|
||||
};
|
||||
} finally {
|
||||
this.device.close();
|
||||
this.startPollUhkDevice();
|
||||
}
|
||||
|
||||
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.stopPollTimer();
|
||||
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);
|
||||
|
||||
return {
|
||||
leftModuleInfo: {},
|
||||
rightModuleInfo: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
await this.stopPollUhkDevice();
|
||||
this.logService.info('[DeviceService] Device connection checker stopped.');
|
||||
}
|
||||
|
||||
public async updateFirmware(event: Electron.Event, args?: Array<string>): Promise<void> {
|
||||
const response = new IpcResponse();
|
||||
const response = new FirmwareUpgradeIpcResponse();
|
||||
const data: UpdateFirmwareData = JSON.parse(args[0]);
|
||||
|
||||
let firmwarePathData: TmpFirmware;
|
||||
|
||||
try {
|
||||
this.stopPollTimer();
|
||||
this.logService.debug('Agent version:', data.versionInformation.version);
|
||||
const hardwareModules = await this.getHardwareModules(false);
|
||||
this.logService.debug('Device right firmware version:', hardwareModules.rightModuleInfo.firmwareVersion);
|
||||
this.logService.debug('Device left firmware version:', hardwareModules.leftModuleInfo.firmwareVersion);
|
||||
|
||||
await this.stopPollUhkDevice();
|
||||
this.device.resetDeviceCache();
|
||||
|
||||
if (data.firmware) {
|
||||
firmwarePathData = await saveTmpFirmware(data.firmware);
|
||||
|
||||
const packageJson = await getPackageJsonFromPathAsync(firmwarePathData.packageJsonPath);
|
||||
this.logService.debug('New firmware version:', packageJson.firmwareVersion);
|
||||
|
||||
if (args && args.length > 0) {
|
||||
firmwarePathData = await saveTmpFirmware(args[0]);
|
||||
await this.operations.updateRightFirmware(firmwarePathData.rightFirmwarePath);
|
||||
await this.operations.updateLeftModule(firmwarePathData.leftFirmwarePath);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const packageJsonPath = path.join(this.rootDir, 'packages/firmware/package.json');
|
||||
const packageJson = await getPackageJsonFromPathAsync(packageJsonPath);
|
||||
this.logService.debug('New firmware version:', packageJson.firmwareVersion);
|
||||
|
||||
await this.operations.updateRightFirmware();
|
||||
await this.operations.updateLeftModule();
|
||||
}
|
||||
|
||||
response.success = true;
|
||||
response.modules = await this.getHardwareModules(false);
|
||||
} 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);
|
||||
|
||||
response.modules = await this.getHardwareModules(true);
|
||||
response.error = err;
|
||||
}
|
||||
|
||||
@@ -130,64 +208,107 @@ export class DeviceService {
|
||||
}
|
||||
|
||||
await snooze(500);
|
||||
|
||||
this.startPollUhkDevice();
|
||||
|
||||
event.sender.send(IpcEvents.device.updateFirmwareReply, response);
|
||||
}
|
||||
|
||||
public async recoveryDevice(event: Electron.Event): Promise<void> {
|
||||
const response = new FirmwareUpgradeIpcResponse();
|
||||
|
||||
try {
|
||||
await this.stopPollUhkDevice();
|
||||
|
||||
await this.operations.updateRightFirmware();
|
||||
|
||||
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);
|
||||
event.sender.send(IpcEvents.device.updateFirmwareReply, response);
|
||||
}
|
||||
|
||||
public async enableUsbStackTest(event: Electron.Event) {
|
||||
await this.device.enableUsbStackTest();
|
||||
}
|
||||
|
||||
private startPollUhkDevice(): void {
|
||||
this._pollerAllowed = true;
|
||||
}
|
||||
|
||||
private async stopPollUhkDevice(): Promise<void> {
|
||||
return new Promise<void>(async resolve => {
|
||||
this._pollerAllowed = false;
|
||||
|
||||
while (true) {
|
||||
if (!this._uhkDevicePolling) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
await snooze(100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* HID API not support device attached and detached event.
|
||||
* This method check the keyboard is attached to the computer or not.
|
||||
* Every second check the HID device list.
|
||||
* The halves are connected and merged or not.
|
||||
* Every 250ms check the HID device list.
|
||||
* @private
|
||||
*/
|
||||
private pollUhkDevice(): void {
|
||||
if (this.pollTimer$) {
|
||||
return;
|
||||
private async uhkDevicePoller(): Promise<void> {
|
||||
let savedState: DeviceConnectionState;
|
||||
|
||||
while (true) {
|
||||
if (this._pollerAllowed) {
|
||||
|
||||
this._uhkDevicePolling = true;
|
||||
|
||||
const state = await this.device.getDeviceConnectionStateAsync();
|
||||
if (!isEqual(state, savedState)) {
|
||||
savedState = state;
|
||||
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, state);
|
||||
this.logService.info('[DeviceService] Device connection state changed to:', state);
|
||||
}
|
||||
|
||||
this._uhkDevicePolling = false;
|
||||
}
|
||||
|
||||
await snooze(250);
|
||||
}
|
||||
|
||||
this.pollTimer$ = Observable.interval(1000)
|
||||
.startWith(0)
|
||||
.map(() => this.device.deviceConnected())
|
||||
.distinctUntilChanged()
|
||||
.do((connected: boolean) => {
|
||||
const response: DeviceConnectionState = {
|
||||
connected,
|
||||
hasPermission: this.device.hasPermission()
|
||||
};
|
||||
|
||||
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, response);
|
||||
this.logService.info('[DeviceService] Device connection state changed to:', response);
|
||||
})
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
private async saveUserConfiguration(event: Electron.Event, args: Array<string>): Promise<void> {
|
||||
const response = new IpcResponse();
|
||||
const json = args[0];
|
||||
const data: SaveUserConfigurationData = JSON.parse(args[0]);
|
||||
|
||||
try {
|
||||
await this.operations.saveUserConfiguration(json);
|
||||
await this.stopPollUhkDevice();
|
||||
await backupUserConfiguration(data);
|
||||
|
||||
const buffer = mapObjectToUserConfigBinaryBuffer(data.configuration);
|
||||
await this.operations.saveUserConfiguration(buffer);
|
||||
|
||||
response.success = true;
|
||||
}
|
||||
catch (error) {
|
||||
} catch (error) {
|
||||
this.logService.error('[DeviceService] Transferring error', error);
|
||||
response.error = {message: error.message};
|
||||
response.error = { message: error.message };
|
||||
} finally {
|
||||
this.device.close();
|
||||
this.startPollUhkDevice();
|
||||
}
|
||||
|
||||
event.sender.send(IpcEvents.device.saveUserConfigurationReply, response);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private stopPollTimer(): void {
|
||||
if (!this.pollTimer$) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pollTimer$.unsubscribe();
|
||||
this.pollTimer$ = null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ import * as sudo from 'sudo-prompt';
|
||||
import { dirSync } from 'tmp';
|
||||
import { emptyDir, copy } from 'fs-extra';
|
||||
|
||||
import { IpcEvents, LogService, IpcResponse } from 'uhk-common';
|
||||
import { CommandLineArgs, IpcEvents, LogService, IpcResponse } from 'uhk-common';
|
||||
|
||||
export class SudoService {
|
||||
private rootDir: string;
|
||||
|
||||
constructor(private logService: LogService) {
|
||||
constructor(private logService: LogService,
|
||||
private options: CommandLineArgs) {
|
||||
if (isDev) {
|
||||
this.rootDir = path.join(path.join(process.cwd(), process.argv[1]), '../../../../');
|
||||
} else {
|
||||
@@ -21,6 +22,19 @@ export class SudoService {
|
||||
}
|
||||
|
||||
private async setPrivilege(event: Electron.Event) {
|
||||
if (this.options.spe) {
|
||||
const error = new Error('No polkit authentication agent found.');
|
||||
this.logService.error('[SudoService] Simulate privilege escalation error ', error);
|
||||
|
||||
const response = new IpcResponse();
|
||||
response.success = false;
|
||||
response.error = {message: error.message};
|
||||
|
||||
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
await this.setPrivilegeOnLinux(event);
|
||||
@@ -28,7 +42,7 @@ export class SudoService {
|
||||
default:
|
||||
const response: IpcResponse = {
|
||||
success: false,
|
||||
error: { message: 'Permissions couldn\'t be set. Invalid platform: ' + process.platform }
|
||||
error: {message: 'Permissions couldn\'t be set. Invalid platform: ' + process.platform}
|
||||
};
|
||||
|
||||
event.sender.send(IpcEvents.device.setPrivilegeOnLinuxReply, response);
|
||||
@@ -39,7 +53,7 @@ export class SudoService {
|
||||
private async setPrivilegeOnLinux(event: Electron.Event) {
|
||||
const tmpDirectory = dirSync();
|
||||
const rulesDir = path.join(this.rootDir, 'rules');
|
||||
this.logService.debug('[SudoService] Copy rules dir', { src: rulesDir, dst: tmpDirectory.name });
|
||||
this.logService.debug('[SudoService] Copy rules dir', {src: rulesDir, dst: tmpDirectory.name});
|
||||
await copy(rulesDir, tmpDirectory.name);
|
||||
|
||||
const scriptPath = path.join(tmpDirectory.name, 'setup-rules.sh');
|
||||
@@ -55,7 +69,7 @@ export class SudoService {
|
||||
if (error) {
|
||||
this.logService.error('[SudoService] Error when set privilege: ', error);
|
||||
response.success = false;
|
||||
response.error = error;
|
||||
response.error = {message: error.message};
|
||||
} else {
|
||||
response.success = true;
|
||||
}
|
||||
|
||||
32
packages/uhk-agent/src/util/backup-user-confoguration.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { app } from 'electron';
|
||||
import { LogService, UserConfiguration, SaveUserConfigurationData } from 'uhk-common';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
export const getBackupUserConfigurationPath = (uniqueId: number): string => {
|
||||
const appDataDir = app.getPath('userData');
|
||||
|
||||
return path.join(appDataDir, `${uniqueId}.json`);
|
||||
};
|
||||
|
||||
export const backupUserConfiguration = (data: SaveUserConfigurationData): Promise<void> => {
|
||||
const backupFilePath = getBackupUserConfigurationPath(data.uniqueId);
|
||||
return fs.writeJSON(backupFilePath, data.configuration, {spaces: 2});
|
||||
};
|
||||
|
||||
export const getBackupUserConfigurationContent = async (logService: LogService, uniqueId: number): Promise<UserConfiguration> => {
|
||||
try {
|
||||
const backupFilePath = getBackupUserConfigurationPath(uniqueId);
|
||||
|
||||
if (await fs.pathExists(backupFilePath)) {
|
||||
const json = await fs.readJSON(backupFilePath);
|
||||
new UserConfiguration().fromJsonObject(json);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
logService.error('Can not load backup user configuration for device', {uniqueId, error});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as fs from 'fs';
|
||||
|
||||
export const getPackageJsonFromPathAsync = async (filePath: string): Promise<any> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(filePath, {encoding: 'utf-8'}, (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
});
|
||||
};
|
||||
3
packages/uhk-agent/src/util/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './backup-user-confoguration';
|
||||
export * from './get-package-json-from-path-async';
|
||||
export * from './save-extract-firmware';
|
||||
@@ -6,7 +6,7 @@ import * as decompressTarbz from 'decompress-tarbz2';
|
||||
|
||||
import { TmpFirmware } from '../models/tmp-firmware';
|
||||
|
||||
export async function saveTmpFirmware(data: string): Promise<TmpFirmware> {
|
||||
export async function saveTmpFirmware(data: Array<number>): Promise<TmpFirmware> {
|
||||
const tmpDirectory = dirSync();
|
||||
const zipFilePath = path.join(tmpDirectory.name, 'firmware.bz2');
|
||||
|
||||
@@ -16,15 +16,14 @@ export async function saveTmpFirmware(data: string): Promise<TmpFirmware> {
|
||||
return {
|
||||
tmpDirectory,
|
||||
rightFirmwarePath: path.join(tmpDirectory.name, 'devices/uhk60-right/firmware.hex'),
|
||||
leftFirmwarePath: path.join(tmpDirectory.name, 'modules/uhk60-left.bin')
|
||||
|
||||
leftFirmwarePath: path.join(tmpDirectory.name, 'modules/uhk60-left.bin'),
|
||||
packageJsonPath: path.join(tmpDirectory.name, 'package.json')
|
||||
};
|
||||
}
|
||||
|
||||
function writeDataToFile(data: string, filePath: string): Promise<void> {
|
||||
function writeDataToFile(data: Array<number>, filePath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const array: Array<number> = JSON.parse(data);
|
||||
const buffer = new Buffer(array);
|
||||
const buffer = Buffer.from(data);
|
||||
|
||||
fs.writeFile(filePath, buffer, err => {
|
||||
if (err) {
|
||||
|
||||
70
packages/uhk-agent/src/util/window.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as electron from 'electron';
|
||||
import * as settings from 'electron-settings';
|
||||
|
||||
import { logger } from '../services/logger.service';
|
||||
import { WindowState } from '../models/window-state';
|
||||
|
||||
const WINDOWS_SETTINGS_KEY = 'windowSettings';
|
||||
|
||||
export const windowVisibleFilter = (state: WindowState) => {
|
||||
return (display: electron.Display): boolean => (
|
||||
state.x >= display.bounds.x &&
|
||||
state.y >= display.bounds.y &&
|
||||
state.x <= display.bounds.width &&
|
||||
state.y <= display.bounds.height
|
||||
);
|
||||
};
|
||||
|
||||
export const windowVisibleOnScreen = (state: WindowState): boolean => {
|
||||
return electron.screen.getAllDisplays().some(windowVisibleFilter(state));
|
||||
};
|
||||
|
||||
export const getDefaultWindowState = () => ({
|
||||
width: 1024,
|
||||
height: 768,
|
||||
isMaximized: true
|
||||
});
|
||||
|
||||
export const loadWindowState = (): Partial<WindowState> => {
|
||||
logger.log('[WindowState] load settings');
|
||||
try {
|
||||
const loadedState = settings.get(WINDOWS_SETTINGS_KEY) as any;
|
||||
logger.log('[WindowState] loaded settings', loadedState);
|
||||
|
||||
if (!loadedState) {
|
||||
logger.log('[WindowState]save state not exists, use default');
|
||||
|
||||
return getDefaultWindowState();
|
||||
}
|
||||
|
||||
const visible = windowVisibleOnScreen(loadedState);
|
||||
logger.log('[WindowState] loaded settings is visible', visible);
|
||||
|
||||
if (visible) {
|
||||
logger.log('[WindowState] return with loaded settings');
|
||||
return loadedState;
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('[WindowState] error when parsing loaded settings', err);
|
||||
}
|
||||
|
||||
logger.log('[WindowState] return with default settings');
|
||||
|
||||
return getDefaultWindowState();
|
||||
};
|
||||
|
||||
export const saveWindowState = (win: electron.BrowserWindow) => {
|
||||
const winBounds = win.isMaximized() || win.isFullScreen()
|
||||
? loadWindowState() as any
|
||||
: win.getBounds();
|
||||
|
||||
const state: WindowState = {
|
||||
...winBounds,
|
||||
isMaximized: win.isMaximized(),
|
||||
isFullScreen: win.isFullScreen()
|
||||
};
|
||||
|
||||
logger.log('[WindowState] save settings:', state);
|
||||
settings.set(WINDOWS_SETTINGS_KEY, state as any);
|
||||
logger.log('[WindowState] save settings success');
|
||||
};
|
||||
@@ -1,21 +1,7 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2016",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
"lib": [
|
||||
"es2015.iterable",
|
||||
"es2016",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
]
|
||||
"baseUrl": "./src",
|
||||
"declaration": false
|
||||
}
|
||||
}
|
||||
|
||||
2327
packages/uhk-common/package-lock.json
generated
@@ -3,7 +3,8 @@
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Common Library contains the common code for uhk-agent (electron-main) and web (electron-renderer) modules",
|
||||
"main": "dist/index.js",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"author": "Ultimate Gadget Laboratories",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -15,19 +16,14 @@
|
||||
"copy:scancodes": "copyfiles ./src/config-serializer/config-items/scancodes.json dist",
|
||||
"copy:secondary-roles": "copyfiles ./src/config-serializer/config-items/secondaryRole.json dist",
|
||||
"test": "jasmine-ts --config=jasmine.json",
|
||||
"coverage": "nyc jasmine-ts --config=jasmine.json"
|
||||
"coverage": "nyc jasmine-ts --config=jasmine.json",
|
||||
"lint": "tslint --project tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"buffer": "5.2.1",
|
||||
"tslib": "1.10.0"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@types/jasmine": "2.6.0",
|
||||
"@types/node": "8.0.30",
|
||||
"jasmine": "2.8.0",
|
||||
"jasmine-core": "2.8.0",
|
||||
"jasmine-node": "2.0.0",
|
||||
"jasmine-ts": "0.2.1",
|
||||
"nyc": "11.2.1",
|
||||
"ts-node": "3.3.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extension": [
|
||||
".ts"
|
||||
|
||||
@@ -41,16 +41,20 @@ export class HardwareConfiguration {
|
||||
}
|
||||
|
||||
fromBinary(buffer: UhkBuffer): HardwareConfiguration {
|
||||
this.signature = buffer.readString();
|
||||
this.majorVersion = buffer.readUInt8();
|
||||
this.minorVersion = buffer.readUInt8();
|
||||
this.patchVersion = buffer.readUInt8();
|
||||
this.brandId = buffer.readUInt8();
|
||||
this.deviceId = buffer.readUInt8();
|
||||
this.uniqueId = buffer.readUInt32();
|
||||
this.isVendorModeOn = buffer.readBoolean();
|
||||
this.isIso = buffer.readBoolean();
|
||||
return this;
|
||||
try {
|
||||
this.signature = buffer.readString();
|
||||
this.majorVersion = buffer.readUInt8();
|
||||
this.minorVersion = buffer.readUInt8();
|
||||
this.patchVersion = buffer.readUInt8();
|
||||
this.brandId = buffer.readUInt8();
|
||||
this.deviceId = buffer.readUInt8();
|
||||
this.uniqueId = buffer.readUInt32();
|
||||
this.isVendorModeOn = buffer.readBoolean();
|
||||
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 {
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from './secondary-role-action';
|
||||
export * from './macro';
|
||||
export * from './module';
|
||||
export * from './module-configuration';
|
||||
export * from './mouse-speed-configuration';
|
||||
export * from './user-configuration';
|
||||
|
||||
export const SCANCODES = require('./scancodes.json');
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('keystroke-action', () => {
|
||||
|
||||
it('should not change the "type" to "shortMedia" when is "longMedia" if scancode < 256', () => {
|
||||
const action = new KeystrokeAction();
|
||||
action.type = KeystrokeType.longMedia;
|
||||
action.type = KeystrokeType.longMedia as any;
|
||||
action.scancode = 125;
|
||||
expect(action.type).toEqual(KeystrokeType.shortMedia);
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe('keystroke-action', () => {
|
||||
|
||||
it('should not change the "type" to "longMedia" when is "shortMedia" if scancode >= 256', () => {
|
||||
const action = new KeystrokeAction();
|
||||
action.type = KeystrokeType.shortMedia;
|
||||
action.type = KeystrokeType.shortMedia as any;
|
||||
action.scancode = 256;
|
||||
expect(action.type).toEqual(KeystrokeType.longMedia);
|
||||
});
|
||||
@@ -145,7 +145,7 @@ describe('keystroke-action', () => {
|
||||
});
|
||||
|
||||
it('should change the value to "longMedia" if scancode >= 256 and value "shortMedia"', () => {
|
||||
const value = KeystrokeType.shortMedia;
|
||||
const value = KeystrokeType.shortMedia as any;
|
||||
const scancode = 256;
|
||||
const action = new KeystrokeAction();
|
||||
action.scancode = scancode;
|
||||
@@ -165,7 +165,7 @@ describe('keystroke-action', () => {
|
||||
});
|
||||
|
||||
it('should change the value to "shortMedia" if scancode < 256 and value "longMedia"', () => {
|
||||
const value = KeystrokeType.longMedia;
|
||||
const value = KeystrokeType.longMedia as any;
|
||||
const scancode = 100;
|
||||
const action = new KeystrokeAction();
|
||||
action.scancode = scancode;
|
||||
|
||||
@@ -13,7 +13,7 @@ export enum KeystrokeActionFlag {
|
||||
|
||||
const KEYSTROKE_ACTION_FLAG_LENGTH = 3;
|
||||
|
||||
interface JsonObjectKeystrokeAction {
|
||||
export interface JsonObjectKeystrokeAction {
|
||||
keyActionType: string;
|
||||
scancode?: number;
|
||||
modifierMask?: number;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { binaryDefaultHelper, jsonDefaultHelper } from '../../../../test/serializer-test-helper';
|
||||
import { SwitchLayerAction } from './switch-layer-action';
|
||||
import { SwitchLayerAction, SwitchLayerMode } from './switch-layer-action';
|
||||
import { keyActionType } from './key-action';
|
||||
|
||||
// TODO: Add null, undefined, empty object, empty buffer test cases
|
||||
describe('switch-layer-action', () => {
|
||||
const action = new SwitchLayerAction(<SwitchLayerAction>{layer: 0, isLayerToggleable: false});
|
||||
const action = new SwitchLayerAction(<SwitchLayerAction>{layer: 0, switchLayerMode: SwitchLayerMode.hold});
|
||||
|
||||
it('should be instantiate', () => {
|
||||
expect(new SwitchLayerAction()).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return <SwitchLayerAction layer="0" toggle="false">', () => {
|
||||
expect(action.toString()).toEqual('<SwitchLayerAction layer="0" toggle="false">');
|
||||
it('should return <SwitchLayerAction layer="0" switchLayerMode="hold">', () => {
|
||||
expect(action.toString()).toEqual('<SwitchLayerAction layer="0" switchLayerMode="hold">');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,4 +31,20 @@ describe('switch-layer-action', () => {
|
||||
binaryDefaultHelper(action);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backward compatibility of the "toggle" property ', () => {
|
||||
it('should map toggle=false to SwitchLayerMode.holdAndDoubleTapToggle', () => {
|
||||
const oldAction = new SwitchLayerAction();
|
||||
oldAction.fromJsonObject({keyActionType: keyActionType.SwitchLayerAction, layer: 0, toggle: false});
|
||||
|
||||
expect(oldAction.switchLayerMode).toEqual(SwitchLayerMode.holdAndDoubleTapToggle);
|
||||
});
|
||||
|
||||
it('should map toggle=true to SwitchLayerMode.toggle', () => {
|
||||
const oldAction = new SwitchLayerAction();
|
||||
oldAction.fromJsonObject({keyActionType: keyActionType.SwitchLayerAction, layer: 0, toggle: true});
|
||||
|
||||
expect(oldAction.switchLayerMode).toEqual(SwitchLayerMode.toggle);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,9 +8,48 @@ export enum LayerName {
|
||||
mouse
|
||||
}
|
||||
|
||||
export enum SwitchLayerMode {
|
||||
holdAndDoubleTapToggle = 'holdAndDoubleTapToggle',
|
||||
toggle = 'toggle',
|
||||
hold = 'hold'
|
||||
}
|
||||
|
||||
export const mapSwitchLayerModeToNumber = (switchLayerMode: SwitchLayerMode): number => {
|
||||
switch (switchLayerMode) {
|
||||
case SwitchLayerMode.holdAndDoubleTapToggle:
|
||||
return 0;
|
||||
|
||||
case SwitchLayerMode.toggle:
|
||||
return 1;
|
||||
|
||||
case SwitchLayerMode.hold:
|
||||
return 2;
|
||||
|
||||
default:
|
||||
throw new Error(`Can not map ${switchLayerMode} to number`);
|
||||
}
|
||||
};
|
||||
|
||||
export const mapNumberToSwitchLayerMode = (value: number): SwitchLayerMode => {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return SwitchLayerMode.holdAndDoubleTapToggle;
|
||||
|
||||
case 1:
|
||||
return SwitchLayerMode.toggle;
|
||||
|
||||
case 2:
|
||||
return SwitchLayerMode.hold;
|
||||
|
||||
default:
|
||||
throw new Error(`Can not map "${value}" to SwitchLayerMode`);
|
||||
}
|
||||
};
|
||||
|
||||
export class SwitchLayerAction extends KeyAction {
|
||||
|
||||
isLayerToggleable: boolean;
|
||||
@assertEnum(SwitchLayerMode)
|
||||
switchLayerMode: SwitchLayerMode;
|
||||
|
||||
@assertEnum(LayerName)
|
||||
layer: LayerName;
|
||||
@@ -20,21 +59,29 @@ export class SwitchLayerAction extends KeyAction {
|
||||
if (!other) {
|
||||
return;
|
||||
}
|
||||
this.isLayerToggleable = other.isLayerToggleable;
|
||||
this.switchLayerMode = other.switchLayerMode;
|
||||
this.layer = other.layer;
|
||||
}
|
||||
|
||||
fromJsonObject(jsonObject: any): SwitchLayerAction {
|
||||
this.assertKeyActionType(jsonObject);
|
||||
this.layer = LayerName[<string>jsonObject.layer];
|
||||
this.isLayerToggleable = jsonObject.toggle;
|
||||
|
||||
// Backward compatibility when "switchLayerMode" was a boolean type as "toggle"
|
||||
if (typeof jsonObject.toggle === 'boolean') {
|
||||
this.switchLayerMode = jsonObject.toggle ? SwitchLayerMode.toggle : SwitchLayerMode.holdAndDoubleTapToggle;
|
||||
}
|
||||
else {
|
||||
this.switchLayerMode = jsonObject.switchLayerMode;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fromBinary(buffer: UhkBuffer): SwitchLayerAction {
|
||||
this.readAndAssertKeyActionId(buffer);
|
||||
this.layer = buffer.readUInt8();
|
||||
this.isLayerToggleable = buffer.readBoolean();
|
||||
this.switchLayerMode = mapNumberToSwitchLayerMode(buffer.readUInt8());
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -42,18 +89,18 @@ export class SwitchLayerAction extends KeyAction {
|
||||
return {
|
||||
keyActionType: keyActionType.SwitchLayerAction,
|
||||
layer: LayerName[this.layer],
|
||||
toggle: this.isLayerToggleable
|
||||
switchLayerMode: this.switchLayerMode
|
||||
};
|
||||
}
|
||||
|
||||
toBinary(buffer: UhkBuffer) {
|
||||
buffer.writeUInt8(KeyActionId.SwitchLayerAction);
|
||||
buffer.writeUInt8(this.layer);
|
||||
buffer.writeBoolean(this.isLayerToggleable);
|
||||
buffer.writeUInt8(mapSwitchLayerModeToNumber(this.switchLayerMode));
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<SwitchLayerAction layer="${this.layer}" toggle="${this.isLayerToggleable}">`;
|
||||
return `<SwitchLayerAction layer="${this.layer}" switchLayerMode="${this.switchLayerMode}">`;
|
||||
}
|
||||
|
||||
public getName(): string {
|
||||
|
||||
@@ -127,7 +127,7 @@ export class Keymap {
|
||||
if (currentLayerId - 1 === baseKeyAction.layer) {
|
||||
if (currentKeyAction instanceof SwitchLayerAction) {
|
||||
if (currentKeyAction.layer === baseKeyAction.layer &&
|
||||
currentKeyAction.isLayerToggleable === baseKeyAction.isLayerToggleable) {
|
||||
currentKeyAction.switchLayerMode === baseKeyAction.switchLayerMode) {
|
||||
continue;
|
||||
}
|
||||
// tslint:disable-next-line: max-line-length
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { assertEnum, assertUInt8 } from '../../assert';
|
||||
import { assertEnum, assertUInt8, assertUInt16 } from '../../assert';
|
||||
import { UhkBuffer } from '../../uhk-buffer';
|
||||
import { KeyModifiers } from '../key-modifiers';
|
||||
import { MacroAction, MacroActionId, MacroKeySubAction, macroActionType } from './macro-action';
|
||||
import { KeystrokeType } from '../key-action';
|
||||
|
||||
interface JsObjectKeyMacroAction {
|
||||
export interface JsObjectKeyMacroAction {
|
||||
macroActionType: string;
|
||||
action: string;
|
||||
type?: string;
|
||||
@@ -20,12 +20,24 @@ export class KeyMacroAction extends MacroAction {
|
||||
@assertEnum(KeystrokeType)
|
||||
type: KeystrokeType;
|
||||
|
||||
@assertUInt8
|
||||
scancode: number;
|
||||
|
||||
@assertUInt8
|
||||
modifierMask: number;
|
||||
|
||||
@assertUInt16
|
||||
private _scancode: number;
|
||||
|
||||
set scancode(scancode: number) {
|
||||
this._scancode = scancode;
|
||||
if (this.type !== KeystrokeType.shortMedia && this.type !== KeystrokeType.longMedia) {
|
||||
return;
|
||||
}
|
||||
this.type = scancode < 256 ? KeystrokeType.shortMedia : KeystrokeType.longMedia;
|
||||
}
|
||||
|
||||
get scancode() {
|
||||
return this._scancode;
|
||||
}
|
||||
|
||||
constructor(other?: KeyMacroAction) {
|
||||
super();
|
||||
if (!other) {
|
||||
@@ -33,7 +45,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
}
|
||||
this.action = other.action;
|
||||
this.type = other.type;
|
||||
this.scancode = other.scancode;
|
||||
this._scancode = other._scancode;
|
||||
this.modifierMask = other.modifierMask;
|
||||
}
|
||||
|
||||
@@ -45,7 +57,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
} else {
|
||||
this.type = KeystrokeType[jsObject.type];
|
||||
}
|
||||
this.scancode = jsObject.scancode;
|
||||
this._scancode = jsObject.scancode;
|
||||
this.modifierMask = jsObject.modifierMask;
|
||||
return this;
|
||||
}
|
||||
@@ -58,7 +70,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
this.type = keyMacroType & 0b11;
|
||||
keyMacroType >>= 2;
|
||||
if (keyMacroType & 0b10) {
|
||||
this.scancode = buffer.readUInt8();
|
||||
this._scancode = this.type === KeystrokeType.longMedia ? buffer.readUInt16() : buffer.readUInt8();
|
||||
}
|
||||
if (keyMacroType & 0b01) {
|
||||
this.modifierMask = buffer.readUInt8();
|
||||
@@ -78,7 +90,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
} else {
|
||||
jsObject.type = KeystrokeType[this.type];
|
||||
}
|
||||
jsObject.scancode = this.scancode;
|
||||
jsObject.scancode = this._scancode;
|
||||
}
|
||||
|
||||
if (this.hasModifiers()) {
|
||||
@@ -98,7 +110,11 @@ export class KeyMacroAction extends MacroAction {
|
||||
|
||||
buffer.writeUInt8(keyMacroType);
|
||||
if (this.hasScancode()) {
|
||||
buffer.writeUInt8(this.scancode);
|
||||
if (this.type === KeystrokeType.longMedia) {
|
||||
buffer.writeUInt16(this.scancode);
|
||||
} else {
|
||||
buffer.writeUInt8(this.scancode);
|
||||
}
|
||||
}
|
||||
if (this.hasModifiers()) {
|
||||
buffer.writeUInt8(this.modifierMask);
|
||||
@@ -106,7 +122,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `<KeyMacroAction action="${this.action}" scancode="${this.scancode}" modifierMask="${this.modifierMask}">`;
|
||||
return `<KeyMacroAction action="${this.action}" scancode="${this._scancode}" modifierMask="${this.modifierMask}">`;
|
||||
}
|
||||
|
||||
isModifierActive(modifier: KeyModifiers): boolean {
|
||||
@@ -114,7 +130,7 @@ export class KeyMacroAction extends MacroAction {
|
||||
}
|
||||
|
||||
hasScancode(): boolean {
|
||||
return !!this.scancode;
|
||||
return !!this._scancode;
|
||||
}
|
||||
|
||||
hasModifiers(): boolean {
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('keymap', () => {
|
||||
{
|
||||
keyActionType: 'switchLayer',
|
||||
layer: 'mod',
|
||||
toggle: false
|
||||
switchLayerMode: 'holdAndDoubleTapToggle'
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -121,7 +121,7 @@ describe('keymap', () => {
|
||||
{
|
||||
keyActionType: 'switchLayer',
|
||||
layer: 'mod',
|
||||
toggle: false
|
||||
switchLayerMode: 'holdAndDoubleTapToggle'
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -151,7 +151,7 @@ describe('keymap', () => {
|
||||
|
||||
expect(inputUserConfig.toJsonObject()).toEqual(expectedJsonConfig);
|
||||
// tslint:disable-next-line: max-line-length
|
||||
expect(console.warn).toHaveBeenCalledWith('QWERTY.layers[1]modules[0].keyActions[0] is not switch layer. <KeystrokeAction type="basic" scancode="44"> will be override with <SwitchLayerAction layer="0" toggle="false">');
|
||||
expect(console.warn).toHaveBeenCalledWith('QWERTY.layers[1]modules[0].keyActions[0] is not switch layer. <KeystrokeAction type="basic" scancode="44"> will be override with <SwitchLayerAction layer="0" switchLayerMode="holdAndDoubleTapToggle">');
|
||||
});
|
||||
|
||||
it('should normalize SwitchLayerAction if non base layer action is other SwitchLayerAction', () => {
|
||||
@@ -262,7 +262,7 @@ describe('keymap', () => {
|
||||
{
|
||||
keyActionType: 'switchLayer',
|
||||
layer: 'mod',
|
||||
toggle: false
|
||||
switchLayerMode: 'holdAndDoubleTapToggle'
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -274,7 +274,7 @@ describe('keymap', () => {
|
||||
{
|
||||
keyActionType: 'switchLayer',
|
||||
layer: 'mod',
|
||||
toggle: false
|
||||
switchLayerMode: 'holdAndDoubleTapToggle'
|
||||
}
|
||||
]
|
||||
}]
|
||||
@@ -304,6 +304,6 @@ describe('keymap', () => {
|
||||
|
||||
expect(inputUserConfig.toJsonObject()).toEqual(expectedJsonConfig);
|
||||
// tslint:disable-next-line: max-line-length
|
||||
expect(console.warn).toHaveBeenCalledWith('QWERTY.layers[1]modules[0].keyActions[0] is different switch layer. <SwitchLayerAction layer="1" toggle="false"> will be override with <SwitchLayerAction layer="0" toggle="false">');
|
||||
expect(console.warn).toHaveBeenCalledWith('QWERTY.layers[1]modules[0].keyActions[0] is different switch layer. <SwitchLayerAction layer="1" switchLayerMode="holdAndDoubleTapToggle"> will be override with <SwitchLayerAction layer="0" switchLayerMode="holdAndDoubleTapToggle">');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,12 +3,12 @@ import { UhkBuffer } from '../../uhk-buffer';
|
||||
import { MacroAction, MacroActionId, MacroMouseSubAction, macroActionType } from './macro-action';
|
||||
|
||||
export enum MouseButtons {
|
||||
Left = 1 << 0,
|
||||
Middle = 1 << 1,
|
||||
Right = 1 << 2
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
Middle = 2
|
||||
}
|
||||
|
||||
interface JsObjectMouseButtonMacroAction {
|
||||
export interface JsObjectMouseButtonMacroAction {
|
||||
macroActionType: string;
|
||||
action: string;
|
||||
mouseButtonsMask?: number;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface MouseSpeedConfiguration {
|
||||
mouseMoveInitialSpeed: number;
|
||||
mouseMoveAcceleration: number;
|
||||
mouseMoveDeceleratedSpeed: number;
|
||||
mouseMoveBaseSpeed: number;
|
||||
mouseMoveAcceleratedSpeed: number;
|
||||
mouseScrollInitialSpeed: number;
|
||||
mouseScrollAcceleration: number;
|
||||
mouseScrollDeceleratedSpeed: number;
|
||||
mouseScrollBaseSpeed: number;
|
||||
mouseScrollAcceleratedSpeed: number;
|
||||
}
|
||||
@@ -105,6 +105,10 @@
|
||||
{
|
||||
"id": "29",
|
||||
"text": "Z"
|
||||
},
|
||||
{
|
||||
"id": "100",
|
||||
"text": "| ISO"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -255,7 +259,7 @@
|
||||
},
|
||||
{
|
||||
"id": "70",
|
||||
"text": "Print Screen"
|
||||
"text": "Print Screen SysRq"
|
||||
},
|
||||
{
|
||||
"id": "72",
|
||||
@@ -314,10 +318,6 @@
|
||||
"id": "69",
|
||||
"text": "F12"
|
||||
},
|
||||
{
|
||||
"id": "100",
|
||||
"text": "| ISO"
|
||||
},
|
||||
{
|
||||
"id": "104",
|
||||
"text": "F13"
|
||||
@@ -454,14 +454,6 @@
|
||||
"scancode": 182
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "132",
|
||||
"text": "Stop/Eject",
|
||||
"additional": {
|
||||
"type": "media",
|
||||
"scancode": 204
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "133",
|
||||
"text": "Play/Pause",
|
||||
@@ -503,11 +495,19 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "138",
|
||||
"text": "WWW",
|
||||
"id": "145",
|
||||
"text": "History Back",
|
||||
"additional": {
|
||||
"type": "media",
|
||||
"scancode": 138
|
||||
"scancode": 548
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "146",
|
||||
"text": "History Forward",
|
||||
"additional": {
|
||||
"type": "media",
|
||||
"scancode": 549
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -515,14 +515,6 @@
|
||||
{
|
||||
"text": "Launch application",
|
||||
"children": [
|
||||
{
|
||||
"id": "142",
|
||||
"text": "Launch Web Browser",
|
||||
"additional": {
|
||||
"type": "media",
|
||||
"scancode": 406
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "143",
|
||||
"text": "Launch Email Client",
|
||||
@@ -687,5 +679,50 @@
|
||||
"text": "."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "International",
|
||||
"children": [
|
||||
{
|
||||
"id": "235",
|
||||
"text": "International 1",
|
||||
"additional": {
|
||||
"type": "basic",
|
||||
"scancode": 135
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "236",
|
||||
"text": "International 2",
|
||||
"additional": {
|
||||
"type": "basic",
|
||||
"scancode": 136
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "237",
|
||||
"text": "International 3",
|
||||
"additional": {
|
||||
"type": "basic",
|
||||
"scancode": 137
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "244",
|
||||
"text": "Language 1",
|
||||
"additional": {
|
||||
"type": "basic",
|
||||
"scancode": 144
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "245",
|
||||
"text": "Language 2",
|
||||
"additional": {
|
||||
"type": "basic",
|
||||
"scancode": 145
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { assertUInt8, assertUInt16 } from '../assert';
|
||||
import { assertUInt16, assertUInt8 } from '../assert';
|
||||
import { UhkBuffer } from '../uhk-buffer';
|
||||
import { Keymap } from './keymap';
|
||||
import { Macro } from './macro';
|
||||
import { ModuleConfiguration } from './module-configuration';
|
||||
import { ConfigSerializer } from '../config-serializer';
|
||||
import { KeystrokeAction, NoneAction } from './key-action';
|
||||
import { SecondaryRoleAction } from './secondary-role-action';
|
||||
import { isScancodeExists } from './scancode-checker';
|
||||
import { MouseSpeedConfiguration } from './mouse-speed-configuration';
|
||||
|
||||
export class UserConfiguration {
|
||||
export class UserConfiguration implements MouseSpeedConfiguration {
|
||||
|
||||
@assertUInt16
|
||||
userConfigMajorVersion: number;
|
||||
@@ -90,7 +94,7 @@ export class UserConfiguration {
|
||||
this.mouseMoveAcceleratedSpeed = jsonObject.mouseMoveAcceleratedSpeed;
|
||||
this.mouseScrollInitialSpeed = jsonObject.mouseScrollInitialSpeed;
|
||||
this.mouseScrollAcceleration = jsonObject.mouseScrollAcceleration;
|
||||
this.mouseScrollDeceleratedSpeed = jsonObject.mouseScrollAcceleration;
|
||||
this.mouseScrollDeceleratedSpeed = jsonObject.mouseScrollDeceleratedSpeed;
|
||||
this.mouseScrollBaseSpeed = jsonObject.mouseScrollBaseSpeed;
|
||||
this.mouseScrollAcceleratedSpeed = jsonObject.mouseScrollAcceleratedSpeed;
|
||||
this.moduleConfigurations = jsonObject.moduleConfigurations.map((moduleConfiguration: any) => {
|
||||
@@ -102,7 +106,9 @@ export class UserConfiguration {
|
||||
return macro;
|
||||
});
|
||||
this.keymaps = jsonObject.keymaps.map((keymap: any) => new Keymap().fromJsonObject(keymap, this.macros));
|
||||
this.clean();
|
||||
this.recalculateConfigurationLength();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -138,6 +144,8 @@ export class UserConfiguration {
|
||||
this.keymaps = buffer.readArray<Keymap>(uhkBuffer => new Keymap().fromBinary(uhkBuffer, this.macros));
|
||||
ConfigSerializer.resolveSwitchKeymapActions(this.keymaps);
|
||||
|
||||
this.clean();
|
||||
|
||||
if (this.userConfigurationLength === 0) {
|
||||
this.recalculateConfigurationLength();
|
||||
}
|
||||
@@ -222,4 +230,34 @@ export class UserConfiguration {
|
||||
this.deviceName = 'My UHK';
|
||||
}
|
||||
}
|
||||
|
||||
/* Remove not allowed settings/bugs
|
||||
* 1. Layer Switcher secondary roles allowed only on base layers
|
||||
*/
|
||||
private clean(): void {
|
||||
for (const keymap of this.keymaps) {
|
||||
for (let layerId = 1; layerId < keymap.layers.length; layerId++) {
|
||||
const layer = keymap.layers[layerId];
|
||||
|
||||
for (const module of layer.modules) {
|
||||
for (let keyActionId = 0; keyActionId < module.keyActions.length; keyActionId++) {
|
||||
const keyAction = module.keyActions[keyActionId];
|
||||
if (!keyAction || !(keyAction instanceof KeystrokeAction)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyAction.secondaryRoleAction === SecondaryRoleAction.fn ||
|
||||
keyAction.secondaryRoleAction === SecondaryRoleAction.mod ||
|
||||
keyAction.secondaryRoleAction === SecondaryRoleAction.mouse) {
|
||||
(keyAction as any)._secondaryRoleAction = undefined;
|
||||
}
|
||||
|
||||
if (keyAction.hasScancode() && !isScancodeExists(keyAction.scancode)) {
|
||||
module.keyActions[keyActionId] = new NoneAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// / need to load the buffer package from dependency instead of use node default buffer
|
||||
import { Buffer } from 'buffer/';
|
||||
|
||||
export class UhkBuffer {
|
||||
|
||||
static simpleElementWriter<T>(buffer: UhkBuffer, element: T): void {
|
||||
@@ -37,7 +40,7 @@ export class UhkBuffer {
|
||||
constructor() {
|
||||
this.offset = 0;
|
||||
this.bytesToBacktrack = 0;
|
||||
this.buffer = new Buffer(UhkBuffer.eepromSize);
|
||||
this.buffer = Buffer.alloc(UhkBuffer.eepromSize);
|
||||
this.buffer.fill(0);
|
||||
}
|
||||
|
||||
|
||||