Compare commits
238 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b53751b408 | ||
|
|
8a7f30dbb1 | ||
|
|
3b67e4d71d | ||
|
|
d0626405a9 | ||
|
|
df040fb78e | ||
|
|
ea4ca7c39c | ||
|
|
c477f9bcdc | ||
|
|
abe740cf61 | ||
|
|
1003e7b14b | ||
|
|
fef24613e4 | ||
|
|
9844645409 | ||
|
|
6814c8e126 | ||
|
|
d87d770042 | ||
|
|
5dc2e6f47b | ||
|
|
9f8926e34b | ||
|
|
0bd1152a32 | ||
|
|
4a1e88ed83 | ||
|
|
9db719a54b | ||
|
|
49d31f90f7 | ||
|
|
d364ac85a6 | ||
|
|
fae83a4148 | ||
|
|
ac76674469 | ||
|
|
6867ef45f6 | ||
|
|
239c989cbe | ||
|
|
bfa8343aa5 | ||
|
|
0914a1496b | ||
|
|
a8c2866f95 | ||
|
|
79f467603a | ||
|
|
c4269d4cf1 | ||
|
|
d15e08430f | ||
|
|
ee28065046 | ||
|
|
a64a44fe41 | ||
|
|
fa5f5cdc5d | ||
|
|
4147243565 | ||
|
|
905007a597 | ||
|
|
c3fec647b6 | ||
|
|
5f5aff71a5 | ||
|
|
f65bf80c74 | ||
|
|
f6bef928fe | ||
|
|
b691f866c5 | ||
|
|
5ab5ad5f0c | ||
|
|
4e09f95653 | ||
|
|
4d2d6d40fb | ||
|
|
db2e14a852 | ||
|
|
757a201c47 | ||
|
|
37a67805ce | ||
|
|
735aae03d9 | ||
|
|
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 |
109
CHANGELOG.md
@@ -6,12 +6,119 @@ 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.
|
||||
- Animate keyboard splitting, merging, and the presence of the left half.
|
||||
- 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.
|
||||
- Provide reasonable default mouse settings for Macs.
|
||||
- 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 popver always contain the action of the current key, even after cancelled.
|
||||
- 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.
|
||||
|
||||
@@ -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.
|
||||
@@ -12,7 +12,7 @@ Agent is the configuration application of the [Ultimate Hacking Keyboard](https:
|
||||
|
||||
### Step 1: Build Dependencies
|
||||
|
||||
You'll need Node.js LTS. Use your OS package manager to install it. [Check the NodeJS site for more info.](https://nodejs.org/en/download/package-manager/ "Installing Node.js via package manager") Mac OS users can simply `brew install node` to get both. Should you need multiple Node.js versions on the same computer, use Node Version Manager for [Mac/Linux](https://github.com/creationix/nvm) or for [Windows](https://github.com/coreybutler/nvm-windows)
|
||||
You'll need Node.js 12. Use your OS package manager to install it. [Check the NodeJS site for more info.](https://nodejs.org/en/download/package-manager/ "Installing Node.js via package manager") Mac OS users can simply `brew install node` to get both. Should you need multiple Node.js versions on the same computer, use Node Version Manager for [Mac/Linux](https://github.com/creationix/nvm) or for [Windows](https://github.com/coreybutler/nvm-windows)
|
||||
|
||||
You'll also need `libusb`.
|
||||
On debian-based linux distros, `apt-get install libusb-dev libudev-dev g++` is sufficient.
|
||||
|
||||
@@ -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
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 499 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 735 B After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 913 B After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 2.7 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
|
||||
18952
package-lock.json
generated
125
package.json
@@ -3,10 +3,10 @@
|
||||
"private": true,
|
||||
"author": "Ultimate Gadget Laboratories",
|
||||
"main": "electron/dist/electron-main.js",
|
||||
"version": "1.2.5",
|
||||
"firmwareVersion": "8.2.5",
|
||||
"deviceProtocolVersion": "4.3.1",
|
||||
"userConfigVersion": "4.0.1",
|
||||
"version": "1.2.13",
|
||||
"firmwareVersion": "8.6.0",
|
||||
"deviceProtocolVersion": "4.5.0",
|
||||
"userConfigVersion": "4.1.1",
|
||||
"hardwareConfigVersion": "1.0.0",
|
||||
"description": "Agent is the configuration application of the Ultimate Hacking Keyboard.",
|
||||
"repository": {
|
||||
@@ -15,60 +15,66 @@
|
||||
},
|
||||
"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": "5.0.1",
|
||||
"@types/jasmine": "2.6.0",
|
||||
"@types/jquery": "3.3.1",
|
||||
"@types/jsonfile": "4.0.1",
|
||||
"@types/lodash-es": "4.17.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": "5.0.0",
|
||||
"@types/lodash": "4.14.136",
|
||||
"@types/node": "8.0.53",
|
||||
"@types/node-hid": "0.5.2",
|
||||
"@types/node-hid": "0.7.2",
|
||||
"@types/request": "2.0.8",
|
||||
"@types/usb": "1.1.3",
|
||||
"autoprefixer": "6.5.3",
|
||||
"@types/semver": "5.5.0",
|
||||
"@types/tmp": "0.0.33",
|
||||
"buffer": "5.0.6",
|
||||
"check-node-version": "^3.2.0",
|
||||
"copy-webpack-plugin": "4.0.1",
|
||||
"copyfiles": "^2.0.0",
|
||||
"check-node-version": "4.0.1",
|
||||
"copy-webpack-plugin": "5.0.0",
|
||||
"copyfiles": "2.1.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.44.4",
|
||||
"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.6",
|
||||
"electron-settings": "3.1.4",
|
||||
"electron-updater": "2.21.4",
|
||||
"exports-loader": "0.6.3",
|
||||
"file-loader": "0.10.0",
|
||||
"fs-extra": "5.0.0",
|
||||
"gh-pages": "1.1.0",
|
||||
"jsonfile": "4.0.0",
|
||||
"lerna": "2.9.0",
|
||||
"lodash-es": "4.17.4",
|
||||
"mkdirp": "0.5.1",
|
||||
"node-hid": "0.5.7",
|
||||
"npm-run-all": "4.0.2",
|
||||
"electron-updater": "4.1.2",
|
||||
"fs-extra": "8.1.0",
|
||||
"gh-pages": "2.0.1",
|
||||
"jasmine": "3.4.0",
|
||||
"jasmine-core": "3.4.0",
|
||||
"jasmine-node": "3.0.0",
|
||||
"jasmine-ts": "0.3.0",
|
||||
"jsonfile": "5.0.0",
|
||||
"lerna": "3.16.4",
|
||||
"lodash": "4.17.15",
|
||||
"node-hid": "0.7.9",
|
||||
"npm-run-all": "4.1.5",
|
||||
"nrf-intel-hex": "1.3.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.9.1",
|
||||
"typescript": "2.6.2",
|
||||
"webpack": "3.10.0"
|
||||
"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"
|
||||
@@ -76,37 +82,22 @@
|
||||
"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 --project ./packages/uhk-agent/tsconfig.json",
|
||||
"lint:ts:electron-renderer": "tslint --project ./packages/uhk-web/src/tsconfig.renderer.json",
|
||||
"lint:ts:web": "tslint --project ./packages/uhk-web/src/tsconfig.app.json",
|
||||
"lint:ts:test-serializer": "tslint --project ./packages/test-serializer/tsconfig.json",
|
||||
"lint:ts:uhk-usb": "tslint --project ./packages/uhk-usb/tsconfig.json",
|
||||
"lint:style": "stylelint \"packages/uhk-agent/src/**/*.scss\" \"packages/uhk-web/src/**/*.scss\" --syntax scss",
|
||||
"test": "lerna run test",
|
||||
"lint": "lerna run lint",
|
||||
"e2e": "lerna run e2e --scope uhk-web",
|
||||
"prebuild": "check-node-version --package",
|
||||
"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",
|
||||
"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",
|
||||
"electron:spe": "lerna exec --scope uhk-agent npm run electron:spe",
|
||||
"standard-version": "standard-version",
|
||||
"electron:kboot": "lerna exec --scope uhk-agent npm run electron:kboot",
|
||||
"pack": "node ./scripts/release.js",
|
||||
"sprites": "node ./scripts/generate-svg-sprites",
|
||||
"release": "node ./scripts/release.js",
|
||||
"clean": "lerna exec rimraf ./node_modules ./dist && rimraf ./node_modules ./dist",
|
||||
"predeploy-gh-pages": "run-s build:web",
|
||||
"deploy-gh-pages": "gh-pages -d packages/uhk-web/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';
|
||||
221
packages/kboot/src/kboot.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
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 {
|
||||
logger('Open peripheral');
|
||||
this.peripheral.open();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
logger('Close peripheral');
|
||||
this.peripheral.close();
|
||||
}
|
||||
|
||||
// ================= Read properties ==================
|
||||
async getProperty(property: Properties, memoryId = MemoryIds.Internal): Promise<CommandResponse> {
|
||||
logger('Start read memory %o', { property, memoryId });
|
||||
|
||||
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) {
|
||||
logger('Response tag is not property response: %d', property);
|
||||
throw new Error('Response tag is not property response');
|
||||
}
|
||||
|
||||
if (response.code === ResponseCodes.UnknownProperty) {
|
||||
logger('Unknown property %d', response.code);
|
||||
throw new Error('Unknown property!');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
logger('Unknown error %d', response.code);
|
||||
throw new Error(`Unknown error. Error code:${response.code}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async getBootloaderVersion(): Promise<BootloaderVersion> {
|
||||
logger('Start to read Bootloader Version');
|
||||
|
||||
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> {
|
||||
logger('Start flash security disable %o', { key });
|
||||
if (key.length !== 8) {
|
||||
logger('Error: Flash security key must be 8 byte. %o', key);
|
||||
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) {
|
||||
logger('Response tag is not generic response: %d', response.tag);
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
logger('Can not disable flash security: %d', response.code);
|
||||
throw new Error(`Can not disable flash security`);
|
||||
}
|
||||
}
|
||||
|
||||
async flashEraseRegion(startAddress: number, count: number): Promise<void> {
|
||||
logger('Start flash erase region');
|
||||
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) {
|
||||
logger('Response tag is not generic response: %d', response.tag);
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
logger('Can not flash erase region: %d', response.code);
|
||||
throw new Error(`Can not flash erase region`);
|
||||
}
|
||||
}
|
||||
|
||||
async flashEraseAllUnsecure(): Promise<void> {
|
||||
logger('Start flash erase all unsecure');
|
||||
const command: CommandOption = {
|
||||
command: Commands.FlashEraseAllUnsecure,
|
||||
params: []
|
||||
};
|
||||
|
||||
const response = await this.peripheral.sendCommand(command);
|
||||
|
||||
if (response.tag !== ResponseTags.Generic) {
|
||||
logger('Response tag is not generic response: %d', response.tag);
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
logger('Can not flash erase all unsecure: %d', response.code);
|
||||
throw new Error(`Can not flash erase all unsecure`);
|
||||
}
|
||||
}
|
||||
|
||||
async readMemory(startAddress: number, count: number): Promise<any> {
|
||||
logger('Start read memory %o', { startAddress, count });
|
||||
return this.peripheral.readMemory(startAddress, count);
|
||||
}
|
||||
|
||||
async writeMemory(options: DataOption): Promise<void> {
|
||||
logger('Start write memory %o', { options });
|
||||
return this.peripheral.writeMemory(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the bootloader
|
||||
*/
|
||||
async reset(): Promise<void> {
|
||||
logger('Start reset the bootloader');
|
||||
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) {
|
||||
logger('Response tag is not generic response: %d', response.tag);
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
logger('Unknown error %d', response.code);
|
||||
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> {
|
||||
logger('Start configure I2C', { address, speed });
|
||||
if (address > 127) {
|
||||
logger('Only 7-bit i2c address is supported');
|
||||
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) {
|
||||
logger('Response tag is not generic response: %d', response.tag);
|
||||
throw new Error('Response tag is not generic response');
|
||||
}
|
||||
|
||||
if (response.code !== ResponseCodes.Success) {
|
||||
logger('Unknown error %d', response.code);
|
||||
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"
|
||||
}
|
||||
}
|
||||
279
packages/kboot/src/usb-peripheral.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
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.removeAllListeners('data');
|
||||
this._device.removeAllListeners('error');
|
||||
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> {
|
||||
logger('start read from buffer %o', { bufferName, byte, timeout });
|
||||
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> {
|
||||
logger('Start read next command response');
|
||||
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"
|
||||
}
|
||||
|
||||
}
|
||||
498
packages/uhk-agent/package-lock.json
generated
@@ -17,24 +17,24 @@
|
||||
"command-line-args": "4.0.7",
|
||||
"decompress": "4.2.0",
|
||||
"decompress-bzip2": "4.0.0",
|
||||
"node-hid": "0.5.7",
|
||||
"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",
|
||||
"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-blhost",
|
||||
"electron:kboot": "cross-env DEBUG=kboot* electron ./dist/electron-main.js --useKboot",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,20 @@ import * as commandLineArgs from 'command-line-args';
|
||||
import { UhkHidDevice, UhkOperations } from 'uhk-usb';
|
||||
// import { ElectronDataStorageRepositoryService } from './services/electron-datastorage-repository.service';
|
||||
import { CommandLineArgs, LogRegExps } from 'uhk-common';
|
||||
import { UhkBlhost } from 'uhk-usb';
|
||||
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 { setMenu } from './electron-menu';
|
||||
import { loadWindowState, saveWindowState } from './util/window';
|
||||
|
||||
const optionDefinitions = [
|
||||
{name: 'addons', type: Boolean},
|
||||
{name: 'spe', type: Boolean}, // simulate privilege escalation error
|
||||
// show 'Lock layer when double tapping this key' checkbox on 'Layer' tab of the config popover
|
||||
{name: 'layer-double-tap', type: Boolean}
|
||||
{name: 'useKboot', type: Boolean} // If it is true use kboot package instead of blhost for firmware upgrade
|
||||
];
|
||||
|
||||
const options: CommandLineArgs = commandLineArgs(optionDefinitions);
|
||||
@@ -62,19 +63,7 @@ if (console.debug) {
|
||||
};
|
||||
}
|
||||
|
||||
const isSecondInstance = app.makeSingleInstance(function (commandLine, workingDirectory) {
|
||||
// Someone tried to run a second instance, we should focus our window.
|
||||
if (win) {
|
||||
if (win.isMinimized()) {
|
||||
win.restore();
|
||||
}
|
||||
win.focus();
|
||||
}
|
||||
});
|
||||
|
||||
if (isSecondInstance) {
|
||||
app.quit();
|
||||
}
|
||||
const isSecondInstance = !app.requestSingleInstanceLock();
|
||||
|
||||
function createWindow() {
|
||||
if (isSecondInstance) {
|
||||
@@ -91,22 +80,32 @@ 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: path.join(__dirname, 'renderer/assets/images/agent-app-icon.png')
|
||||
});
|
||||
win.setMenuBarVisibility(false);
|
||||
win.maximize();
|
||||
uhkHidDeviceService = new UhkHidDevice(logger, options);
|
||||
|
||||
if (loadedWindowState.isFullScreen) {
|
||||
win.setFullScreen(true);
|
||||
} else if (loadedWindowState.isMaximized) {
|
||||
win.maximize();
|
||||
}
|
||||
|
||||
setMenu(win);
|
||||
uhkHidDeviceService = new UhkHidDevice(logger, options, packagesDir);
|
||||
uhkBlhost = new UhkBlhost(logger, packagesDir);
|
||||
uhkOperations = new UhkOperations(logger, uhkBlhost, uhkHidDeviceService, packagesDir);
|
||||
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations, packagesDir);
|
||||
deviceService = new DeviceService(logger, win, uhkHidDeviceService, uhkOperations, packagesDir, options);
|
||||
appUpdateService = new AppUpdateService(logger, win, app);
|
||||
appService = new AppService(logger, win, deviceService, options, uhkHidDeviceService);
|
||||
sudoService = new SudoService(logger, options);
|
||||
@@ -123,13 +122,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;
|
||||
@@ -144,31 +143,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', () => {
|
||||
if (appUpdateService) {
|
||||
appUpdateService.saveFirtsRun();
|
||||
}
|
||||
app.exit();
|
||||
});
|
||||
app.on('window-all-closed', () => {
|
||||
if (appUpdateService) {
|
||||
appUpdateService.saveFirtsRun();
|
||||
}
|
||||
app.exit();
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
});
|
||||
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);
|
||||
};
|
||||
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,7 +5,7 @@ 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 {
|
||||
@@ -76,14 +76,17 @@ export class AppUpdateService extends MainServiceBase {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.autoUpdater.checkForUpdate, () => {
|
||||
this.logService.debug('[AppUpdateService] checkForUpdate request from renderer process');
|
||||
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();
|
||||
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);
|
||||
@@ -106,7 +109,7 @@ export class AppUpdateService extends MainServiceBase {
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdater.allowPrerelease = this.allowPreRelease();
|
||||
autoUpdater.allowPrerelease = allowPrerelease;
|
||||
autoUpdater.checkForUpdates()
|
||||
.then(() => {
|
||||
this.logService.debug('[AppUpdateService] checkForUpdate success');
|
||||
@@ -127,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;
|
||||
@@ -142,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);
|
||||
|
||||
@@ -23,15 +23,12 @@ export class AppService extends MainServiceBase {
|
||||
|
||||
private async handleAppStartInfo(event: Electron.Event) {
|
||||
this.logService.info('[AppService] getAppStartInfo');
|
||||
const deviceConnectionState = this.uhkHidDeviceService.getDeviceConnectionState();
|
||||
const deviceConnectionState = await this.uhkHidDeviceService.getDeviceConnectionStateAsync();
|
||||
const response: AppStartInfo = {
|
||||
deviceConnectionState,
|
||||
commandLineArgs: {
|
||||
addons: this.options.addons || false,
|
||||
layerDoubleTap: this.options['layer-double-tap'] || false
|
||||
addons: this.options.addons || false
|
||||
},
|
||||
deviceConnected: deviceConnectionState.connected,
|
||||
hasPermission: deviceConnectionState.hasPermission,
|
||||
bootloaderActive: deviceConnectionState.bootloaderActive,
|
||||
platform: process.platform as string,
|
||||
osVersion: os.release()
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { isEqual } from 'lodash';
|
||||
import {
|
||||
CommandLineArgs,
|
||||
ConfigurationReply,
|
||||
DeviceConnectionState,
|
||||
FirmwareUpgradeIpcResponse,
|
||||
@@ -9,20 +11,13 @@ import {
|
||||
IpcResponse,
|
||||
LogService,
|
||||
mapObjectToUserConfigBinaryBuffer,
|
||||
SaveUserConfigurationData
|
||||
SaveUserConfigurationData,
|
||||
UpdateFirmwareData
|
||||
} from 'uhk-common';
|
||||
import { deviceConnectionStateComparer, snooze, UhkHidDevice, UhkOperations } from 'uhk-usb';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { snooze, UhkHidDevice, UhkOperations } from 'uhk-usb';
|
||||
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 { TmpFirmware } from '../models/tmp-firmware';
|
||||
import { QueueManager } from './queue-manager';
|
||||
import {
|
||||
@@ -39,15 +34,21 @@ import {
|
||||
* - 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,
|
||||
private rootDir: string) {
|
||||
this.pollUhkDevice();
|
||||
private rootDir: string,
|
||||
private options: CommandLineArgs) {
|
||||
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({
|
||||
@@ -76,7 +77,7 @@ 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({
|
||||
@@ -87,6 +88,15 @@ export class DeviceService {
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on(IpcEvents.device.enableUsbStackTest, (...args: any[]) => {
|
||||
this.queueManager.add({
|
||||
method: this.enableUsbStackTest,
|
||||
bind: this,
|
||||
params: args,
|
||||
asynchronous: true
|
||||
});
|
||||
});
|
||||
|
||||
logService.debug('[DeviceService] init success');
|
||||
}
|
||||
|
||||
@@ -98,6 +108,8 @@ export class DeviceService {
|
||||
let response: ConfigurationReply;
|
||||
|
||||
try {
|
||||
await this.stopPollUhkDevice();
|
||||
|
||||
await this.device.waitUntilKeyboardBusy();
|
||||
const result = await this.operations.loadConfigurations();
|
||||
const modules: HardwareModules = await this.getHardwareModules(false);
|
||||
@@ -118,6 +130,7 @@ export class DeviceService {
|
||||
};
|
||||
} finally {
|
||||
this.device.close();
|
||||
this.startPollUhkDevice();
|
||||
}
|
||||
|
||||
event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response));
|
||||
@@ -131,56 +144,71 @@ export class DeviceService {
|
||||
leftModuleInfo: await this.operations.getLeftModuleVersionInfo(),
|
||||
rightModuleInfo: await this.operations.getRightModuleVersionInfo()
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
} catch (err) {
|
||||
if (!catchError) {
|
||||
return err;
|
||||
}
|
||||
|
||||
this.logService.error('[DeviceService] Read hardware modules information failed', err);
|
||||
|
||||
return {
|
||||
leftModuleInfo: {},
|
||||
rightModuleInfo: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.stopPollTimer();
|
||||
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 FirmwareUpgradeIpcResponse();
|
||||
const data: UpdateFirmwareData = JSON.parse(args[0]);
|
||||
|
||||
let firmwarePathData: TmpFirmware;
|
||||
|
||||
try {
|
||||
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();
|
||||
this.stopPollTimer();
|
||||
|
||||
if (args && args.length > 0) {
|
||||
firmwarePathData = await saveTmpFirmware(args[0]);
|
||||
if (data.firmware) {
|
||||
firmwarePathData = await saveTmpFirmware(data.firmware);
|
||||
|
||||
const packageJson = await getPackageJsonFromPathAsync(firmwarePathData.packageJsonPath);
|
||||
this.logService.debug('New firmware version:', packageJson.firmwareVersion);
|
||||
|
||||
await this.operations.updateRightFirmware(firmwarePathData.rightFirmwarePath);
|
||||
await this.operations.updateLeftModule(firmwarePathData.leftFirmwarePath);
|
||||
}
|
||||
else {
|
||||
if (this.options.useKboot) {
|
||||
await this.operations.updateRightFirmwareWithKboot(firmwarePathData.rightFirmwarePath);
|
||||
await this.operations.updateLeftModuleWithKboot(firmwarePathData.leftFirmwarePath);
|
||||
} else {
|
||||
await this.operations.updateRightFirmwareWithBlhost(firmwarePathData.rightFirmwarePath);
|
||||
await this.operations.updateLeftModuleWithBlhost(firmwarePathData.leftFirmwarePath);
|
||||
}
|
||||
} 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();
|
||||
if (this.options.useKboot) {
|
||||
await this.operations.updateRightFirmwareWithKboot();
|
||||
await this.operations.updateLeftModuleWithKboot();
|
||||
} else {
|
||||
await this.operations.updateRightFirmwareWithBlhost();
|
||||
await this.operations.updateLeftModuleWithBlhost();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -193,7 +221,7 @@ export class DeviceService {
|
||||
|
||||
await snooze(500);
|
||||
|
||||
this.pollUhkDevice();
|
||||
this.startPollUhkDevice();
|
||||
|
||||
event.sender.send(IpcEvents.device.updateFirmwareReply, response);
|
||||
}
|
||||
@@ -202,18 +230,18 @@ export class DeviceService {
|
||||
const response = new FirmwareUpgradeIpcResponse();
|
||||
|
||||
try {
|
||||
this.stopPollTimer();
|
||||
await this.stopPollUhkDevice();
|
||||
|
||||
await this.operations.updateRightFirmware();
|
||||
|
||||
await snooze(500);
|
||||
|
||||
this.pollUhkDevice();
|
||||
if (this.options.useKboot) {
|
||||
await this.operations.updateRightFirmwareWithKboot();
|
||||
} else {
|
||||
await this.operations.updateRightFirmwareWithBlhost();
|
||||
}
|
||||
|
||||
response.modules = await this.getHardwareModules(false);
|
||||
response.success = true;
|
||||
} 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);
|
||||
@@ -224,26 +252,57 @@ export class DeviceService {
|
||||
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;
|
||||
|
||||
this.pollTimer$ = Observable.interval(1000)
|
||||
.startWith(0)
|
||||
.map(() => this.device.getDeviceConnectionState())
|
||||
.distinctUntilChanged<DeviceConnectionState>(deviceConnectionStateComparer)
|
||||
.do((state: DeviceConnectionState) => {
|
||||
this.win.webContents.send(IpcEvents.device.deviceConnectionStateChanged, state);
|
||||
this.logService.info('[DeviceService] Device connection state changed to:', state);
|
||||
})
|
||||
.subscribe();
|
||||
while (true) {
|
||||
if (this._pollerAllowed) {
|
||||
this._uhkDevicePolling = true;
|
||||
try {
|
||||
|
||||
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);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logService.error('[DeviceService] Device connection state query error', err);
|
||||
}
|
||||
}
|
||||
|
||||
this._uhkDevicePolling = false;
|
||||
await snooze(250);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveUserConfiguration(event: Electron.Event, args: Array<string>): Promise<void> {
|
||||
@@ -251,32 +310,23 @@ export class DeviceService {
|
||||
const data: SaveUserConfigurationData = JSON.parse(args[0]);
|
||||
|
||||
try {
|
||||
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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -21,10 +21,9 @@ export async function saveTmpFirmware(data: string): Promise<TmpFirmware> {
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
2347
packages/uhk-common/package-lock.json
generated
@@ -16,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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,7 +15,12 @@ export enum MouseActionParam {
|
||||
scrollLeft,
|
||||
scrollRight,
|
||||
accelerate,
|
||||
decelerate
|
||||
decelerate,
|
||||
button4,
|
||||
button5,
|
||||
button6,
|
||||
button7,
|
||||
button8
|
||||
}
|
||||
|
||||
export class MouseAction extends KeyAction {
|
||||
|
||||
@@ -3,9 +3,9 @@ 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
|
||||
}
|
||||
|
||||
export interface JsObjectMouseButtonMacroAction {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -259,7 +259,7 @@
|
||||
},
|
||||
{
|
||||
"id": "70",
|
||||
"text": "Print Screen"
|
||||
"text": "Print Screen SysRq"
|
||||
},
|
||||
{
|
||||
"id": "72",
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { CommandLineArgs } from './command-line-args';
|
||||
import { DeviceConnectionState } from './device-connection-state';
|
||||
|
||||
export interface AppStartInfo {
|
||||
commandLineArgs: CommandLineArgs;
|
||||
deviceConnected: boolean;
|
||||
hasPermission: boolean;
|
||||
bootloaderActive: boolean;
|
||||
deviceConnectionState: DeviceConnectionState;
|
||||
platform: string;
|
||||
osVersion: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface AutoUpdateSettings {
|
||||
checkForUpdateOnStartUp: boolean;
|
||||
usePreReleaseUpdate: boolean;
|
||||
usePreReleaseUpdate?: boolean;
|
||||
}
|
||||
@@ -8,8 +8,7 @@ export interface CommandLineArgs {
|
||||
*/
|
||||
spe?: boolean;
|
||||
/**
|
||||
* show 'Lock layer when double tapping this key' checkbox on 'Layer' tab of the config popover
|
||||
* if it false the checkbox invisible and the value of the checkbox = true
|
||||
* If it is true use kboot package instead of blhost for firmware upgrade
|
||||
*/
|
||||
layerDoubleTap?: boolean;
|
||||
useKboot?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { UdevRulesInfo } from './udev-rules-info';
|
||||
import { HalvesInfo } from './halves-info';
|
||||
|
||||
export interface DeviceConnectionState {
|
||||
connected: boolean;
|
||||
hasPermission: boolean;
|
||||
bootloaderActive: boolean;
|
||||
zeroInterfaceAvailable: boolean;
|
||||
udevRulesInfo: UdevRulesInfo;
|
||||
halvesInfo: HalvesInfo;
|
||||
}
|
||||
|
||||
4
packages/uhk-common/src/models/halves-info.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface HalvesInfo {
|
||||
areHalvesMerged: boolean;
|
||||
isLeftHalfConnected: boolean;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HardwareModuleInfo } from './hardware-module-info';
|
||||
import { LeftModuleInfo } from './left-module-info';
|
||||
import { RightModuleInfo } from './right-module-info';
|
||||
|
||||
export interface HardwareModules {
|
||||
leftModuleInfo?: HardwareModuleInfo;
|
||||
rightModuleInfo?: HardwareModuleInfo;
|
||||
leftModuleInfo?: LeftModuleInfo;
|
||||
rightModuleInfo?: RightModuleInfo;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './auto-update-settings';
|
||||
export * from './command-line-args';
|
||||
export * from './notification';
|
||||
export * from './ipc-response';
|
||||
@@ -5,6 +6,10 @@ export * from './app-start-info';
|
||||
export * from './configuration-reply';
|
||||
export * from './version-information';
|
||||
export * from './device-connection-state';
|
||||
export * from './left-module-info';
|
||||
export * from './hardware-modules';
|
||||
export * from './hardware-module-info';
|
||||
export * from './right-module-info';
|
||||
export * from './save-user-configuration-data';
|
||||
export * from './udev-rules-info';
|
||||
export * from './update-firmware-data';
|
||||
export * from './halves-info';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface HardwareModuleInfo {
|
||||
export interface LeftModuleInfo {
|
||||
firmwareVersion?: string;
|
||||
moduleProtocolVersion?: string;
|
||||
}
|
||||
7
packages/uhk-common/src/models/right-module-info.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface RightModuleInfo {
|
||||
deviceProtocolVersion?: string;
|
||||
hardwareConfigVersion?: string;
|
||||
firmwareVersion?: string;
|
||||
moduleProtocolVersion?: string;
|
||||
userConfigVersion?: string;
|
||||
}
|
||||
16
packages/uhk-common/src/models/udev-rules-info.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* What is the state of the udev rules.
|
||||
* Only on Linux need extra udev rules.
|
||||
*/
|
||||
export enum UdevRulesInfo {
|
||||
Unknown = 'Unknown',
|
||||
Ok = 'Ok',
|
||||
/**
|
||||
* Udev rules not exists need to setup on Linux
|
||||
*/
|
||||
NeedToSetup = 'NeedToSetup',
|
||||
/**
|
||||
* Udev rules exist but different than expected on Linux
|
||||
*/
|
||||
Different = 'Different'
|
||||
}
|
||||
6
packages/uhk-common/src/models/update-firmware-data.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { VersionInformation } from './version-information';
|
||||
|
||||
export interface UpdateFirmwareData {
|
||||
versionInformation: VersionInformation;
|
||||
firmware?: Array<number>;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export namespace Constants {
|
||||
export const AGENT_GITHUB_URL = 'https://github.com/UltimateHackingKeyboard/agent';
|
||||
export const AGENT_CONTRIBUTORS_GITHUB_PAGE_URL = 'https://github.com/UltimateHackingKeyboard/agent/graphs/contributors';
|
||||
export const AGENT_CONTRIBUTORS_GITHUB_API_URL = 'https://api.github.com/repos/UltimateHackingKeyboard/agent/contributors';
|
||||
export const FIRMWARE_GITHUB_ISSUE_URL = 'https://github.com/UltimateHackingKeyboard/agent/issues/new';
|
||||
}
|
||||
|
||||
@@ -15,28 +15,6 @@ export function capitalizeFirstLetter(text: string): string {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function coerces a string into a string literal type.
|
||||
* Using tagged union types in TypeScript 2.0, this enables
|
||||
* powerful typechecking of our reducers.
|
||||
*
|
||||
* Since every action label passes through this function it
|
||||
* is a good place to ensure all of our action labels
|
||||
* are unique.
|
||||
*/
|
||||
|
||||
const typeCache: { [label: string]: boolean } = {};
|
||||
|
||||
export function type<T>(label: T | ''): T {
|
||||
if (typeCache[<string>label]) {
|
||||
throw new Error(`Action type "${label}" is not unique"`);
|
||||
}
|
||||
|
||||
typeCache[<string>label] = true;
|
||||
|
||||
return <T>label;
|
||||
}
|
||||
|
||||
export function runInElectron() {
|
||||
return window && (<any>window).process && (<any>window).process.type;
|
||||
}
|
||||
|
||||