From 439b6ca35a23a9c9279fcc72c9cdf93a6785c67b Mon Sep 17 00:00:00 2001
From: Nikola <1388673+jurkovic-nikola@users.noreply.github.com>
Date: Thu, 16 Jan 2025 19:39:03 +0100
Subject: [PATCH] support for k70 mk2, darkstar (usb, wireless)
---
.gitignore | 6 +-
99-openlinkhub.rules | 5 +-
README.md | 149 +-
database/keyboard/k70mk2-eu.json | 1541 +++++++++++++++++
database/keyboard/k70mk2.json | 1541 +++++++++++++++++
src/devices/darkstarW/darkstarW.go | 1339 ++++++++++++++
src/devices/darkstarWU/darkstarWU.go | 1418 +++++++++++++++
src/devices/devices.go | 66 +-
src/devices/k70mk2/k70mk2.go | 1292 ++++++++++++++
src/devices/nightsabreW/nightsabreW.go | 2 +-
src/devices/nightsabreWU/nightsabreWU.go | 2 +-
src/devices/slipstream/slipstream.go | 46 +
src/devices/virtuosorgbXTW/virtuosorgbXTW.go | 2 +-
.../virtuosorgbXTWU/virtuosorgbXTWU.go | 2 +-
src/templates/templates.go | 3 +
static/img/icons/icon-windows.svg | 1 +
web/darkstarW.html | 210 +++
web/darkstarWU.html | 179 ++
web/k70mk2.html | 181 ++
19 files changed, 7898 insertions(+), 87 deletions(-)
create mode 100644 database/keyboard/k70mk2-eu.json
create mode 100644 database/keyboard/k70mk2.json
create mode 100644 src/devices/darkstarW/darkstarW.go
create mode 100644 src/devices/darkstarWU/darkstarWU.go
create mode 100644 src/devices/k70mk2/k70mk2.go
create mode 100644 static/img/icons/icon-windows.svg
create mode 100644 web/darkstarW.html
create mode 100644 web/darkstarWU.html
create mode 100644 web/k70mk2.html
diff --git a/.gitignore b/.gitignore
index 98e27c7..38e7323 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,8 @@ dashboard.json
database/temperatures/*.json
database/profiles/*.json
database/rgb/*.json
-database/scheduler.json
\ No newline at end of file
+database/scheduler.json
+org.openlinkhub.OpenLinkHub.json
+.flatpak-builder/
+flatpak/
+flatpak-build/
\ No newline at end of file
diff --git a/99-openlinkhub.rules b/99-openlinkhub.rules
index bb2bd2b..5dbdde4 100644
--- a/99-openlinkhub.rules
+++ b/99-openlinkhub.rules
@@ -64,4 +64,7 @@ SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1b75", MODE="0660
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1b5e", MODE="0660", OWNER="openlinkhub"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0a62", MODE="0660", OWNER="openlinkhub"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0a64", MODE="0660", OWNER="openlinkhub"
-SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1bac", MODE="0660", OWNER="openlinkhub"
\ No newline at end of file
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1bac", MODE="0660", OWNER="openlinkhub"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1b55", MODE="0660", OWNER="openlinkhub"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1b49", MODE="0660", OWNER="openlinkhub"
+SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1bb2", MODE="0660", OWNER="openlinkhub"
\ No newline at end of file
diff --git a/README.md b/README.md
index 1b622dc..d808a71 100644
--- a/README.md
+++ b/README.md
@@ -14,71 +14,73 @@ Open source Linux interface for iCUE LINK Hub and other Corsair AIOs, Hubs.
- This project is not an official Corsair product.
## Supported devices
-| Device | VID | PID | Sub Devices |
-|-------------------------------|--------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| iCUE LINK System Hub | `1b1c` | `0c3f` | Show
iCUE LINK QX RGB
iCUE LINK RX
iCUE LINK RX RGB
iCUE LINK RX MAX
iCUE LINK RX MAX RGB
iCUE LINK LX RGB
iCUE LINK H100i
iCUE LINK H115i
iCUE LINK H150i
iCUE LINK H170i
XC7 Elite
XG7
XD5 Elite
XD5 Elite LCD
VRM Cooling Module
iCUE LINK TITAN H100i
iCUE LINK TITAN H150i
iCUE LINK TITAN H115i
iCUE LINK TITAN H170i
LCD Pump Cover
iCUE LINK XG3 HYBRID
iCUE LINK ADAPTER
iCUE LINK LS350 Aurora RGB
iCUE LINK LS430 Aurora RGB |
-| iCUE COMMANDER Core | `1b1c` | `0c32`
`0c1c` | Show
H100i ELITE CAPELLIX
H115i ELITE CAPELLIX
H150i ELITE CAPELLIX
H170i ELITE CAPELLIX
H100i ELITE LCD
H150i ELITE LCD
H170i ELITE LCD
H100i ELITE LCD XT
H115i ELITE LCD XT
H150i ELITE LCD XT
H170i ELITE LCD XT
H100i ELITE CAPELLIX XT
H115i ELITE CAPELLIX XT
H150i ELITE CAPELLIX XT
H170i ELITE CAPELLIX XT
1x Temperature Probe
4-LED RGB Fan
8-LED RGB Fan
QL Fan Series
LL Fan Series
ML Fan Series
Any PWM Fan |
-| iCUE COMMANDER Core XT | `1b1c` | `0c2a` | Show
External RGB Hub
2x Temperature Probe
4-LED RGB Fan
8-LED RGB Fan
QL Fan Series
LL Fan Series
ML Fan Series
Any PWM Fan
H55 RGB AIO
H100 RGB AIO
H150 RGB AIO |
-| iCUE H100i RGB ELITE | `1b1c` | `0c35`
`0c40` | |
-| iCUE H115i RGB ELITE | `1b1c` | `0c36` | |
-| iCUE H150i RGB ELITE | `1b1c` | `0c37`
`0c41` | |
-| iCUE H100i RGB PRO XT | `1b1c` | `0c20` | |
-| iCUE H115i RGB PRO XT | `1b1c` | `0c21` | |
-| iCUE H150i RGB PRO XT | `1b1c` | `0c22` | |
-| H115i RGB PLATINUM | `1b1c` | `0c17` | |
-| H100i RGB PLATINUM | `1b1c` | `0c18` | |
-| H100i RGB PLATINUM SE | `1b1c` | `0c19` | |
-| Lighting Node CORE | `1b1c` | `0c1a` | Show
HD RGB Series Fan
LL RGB Series Fan
ML PRO RGB Series Fan
QL RGB Series Fan
8-LED Series Fan
SP RGB Series Fan |
-| Lighting Node PRO | `1b1c` | `0c0b` | Show
2x External RGB Hub
HD RGB Series Fan
LL RGB Series Fan
ML PRO RGB Series Fan
QL RGB Series Fan
8-LED Series Fan
SP RGB Series Fan |
-| Commander PRO | `1b1c` | `0c10` | Show
2x External RGB Hub
4x Temperature Probe
Any PWM Fan |
-| XC7 ELITE LCD CPU Water Block | `1b1c` | `0c42` | Show
RGB Control
LCD Control |
-| VENGEANCE RGB PRO | `1b1c` | DDR4 | |
-| VENGEANCE RGB PRO SL | `1b1c` | DDR4 | |
-| VENGEANCE RGB RT | `1b1c` | DDR4 | |
-| VENGEANCE RGB RS | `1b1c` | DDR4 | |
-| DOMINATOR PLATINUM RGB | `1b1c` | DDR4 | |
-| VENGEANCE LPX | `1b1c` | DDR4 | |
-| DOMINATOR PLATINUM | `1b1c` | DDR4 | |
-| VENGEANCE | `1b1c` | DDR5 | |
-| VENGEANCE RGB | `1b1c` | DDR5 | |
-| DOMINATOR PLATINUM RGB | `1b1c` | DDR5 | |
-| DOMINATOR TITANIUM RGB | `1b1c` | DDR5 | |
-| Slipstream Wireless | `1b1c` | `1bdc`
`1ba6`
`2b00` | K100 AIR RGB
IRONCLAW RGB WIRELESS
NIGHTSABRE WIRELESS
SCIMITAR RGB ELITE WIRELESS
M55 WIRELESS
DARK CORE RGB PRO SE WIRELESS
DARK CORE RGB PRO
M75 AIR WIRELESS
HARPOON RGB WIRELESS |
-| K55 CORE RGB | `1b1c` | `1bfe` | |
-| K65 PRO MINI | `1b1c` | `1bd7` | |
-| K70 CORE RGB | `1b1c` | `1bfd` | |
-| K70 PRO RGB | `1b1c` | `1bc6`
`1bb3` | |
-| K65 PLUS | `1b1c` | `2b10`
`2b07` | USB
Wireless |
-| K100 AIR RGB | `1b1c` | `1bab` | USB |
-| K100 | `1b1c` | `1bc5`
`1b7c`
`1b7d` | USB |
-| KATAR PRO | `1b1c` | `1b93` | DPI Control
RGB Control |
-| KATAR PRO XT | `1b1c` | `1bac` | DPI Control
RGB Control |
-| KATAR PRO WIRELESS | `1b1c` | `1b94` | DPI Control |
-| IRONCLAW RGB | `1b1c` | `1b5d` | DPI Control
RGB Control |
-| IRONCLAW RGB WIRELESS | `1b1c` | `1b4c` | DPI Control
RGB Control |
-| NIGHTSABRE WIRELESS | `1b1c` | `1bb8` | DPI Control
RGB Control |
-| SCIMITAR RGB ELITE | `1b1c` | `1be3` | DPI Control
RGB Control |
-| SCIMITAR RGB ELITE WIRELESS | `1b1c` | `1bdb` | DPI Control
RGB Control |
-| M55 | `1b1c` | `2b03` | DPI Control |
-| M55 RGB PRO | `1b1c` | `1b70` | DPI Control
RGB Control |
-| DARK CORE RGB PRO SE WIRELESS | `1b1c` | `1b7e` | DPI Control
RGB Control |
-| DARK CORE RGB PRO | `1b1c` | `1b80` | DPI Control
RGB Control |
-| M75 | `1b1c` | `1bf0` | DPI Control
RGB Control |
-| M75 AIR WIRELESS | `1b1c` | `1bf2` | DPI Control |
-| M65 RGB ULTRA | `1b1c` | `1b9e` | DPI Control
RGB Control |
-| HARPOON RGB PRO | `1b1c` | `1b75` | DPI Control
RGB Control |
-| HARPOON RGB WIRELESS | `1b1c` | `1b5e` | DPI Control
RGB Control |
-| VIRTUOSO RGB WIRELESS XT | `1b1c` | `0a62`
`0a64` | RGB Control |
-| ST100 RGB | `1b1c` | `0a34` | RGB |
-| MM700 RGB | `1b1c` | `1b9b` | RGB |
-| LT100 Smart Lighting Tower | `1b1c` | `0c23` | RGB |
-| HX1000i | `1b1c` | `1c07`
`1c1e` | Fan Control |
-| HX1200i | `1b1c` | `1c08`
`1c23` | Fan Control |
-| HX1500i | `1b1c` | `1c1f` | Fan Control |
-| HX750i | `1b1c` | `1c05` | Fan Control |
-| HX850i | `1b1c` | `1c06` | Fan Control |
-| RM850i | `1b1c` | `1c0c` | Fan Control |
-| RM1000i | `1b1c` | `1c0d` | Fan Control |
+| Device | PID | Sub Devices |
+|-------------------------------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| iCUE LINK System Hub | `0c3f` | Show
iCUE LINK QX RGB
iCUE LINK RX
iCUE LINK RX RGB
iCUE LINK RX MAX
iCUE LINK RX MAX RGB
iCUE LINK LX RGB
iCUE LINK H100i
iCUE LINK H115i
iCUE LINK H150i
iCUE LINK H170i
XC7 Elite
XG7
XD5 Elite
XD5 Elite LCD
VRM Cooling Module
iCUE LINK TITAN H100i
iCUE LINK TITAN H150i
iCUE LINK TITAN H115i
iCUE LINK TITAN H170i
LCD Pump Cover
iCUE LINK XG3 HYBRID
iCUE LINK ADAPTER
iCUE LINK LS350 Aurora RGB
iCUE LINK LS430 Aurora RGB |
+| iCUE COMMANDER Core | `0c32`
`0c1c` | Show
H100i ELITE CAPELLIX
H115i ELITE CAPELLIX
H150i ELITE CAPELLIX
H170i ELITE CAPELLIX
H100i ELITE LCD
H150i ELITE LCD
H170i ELITE LCD
H100i ELITE LCD XT
H115i ELITE LCD XT
H150i ELITE LCD XT
H170i ELITE LCD XT
H100i ELITE CAPELLIX XT
H115i ELITE CAPELLIX XT
H150i ELITE CAPELLIX XT
H170i ELITE CAPELLIX XT
1x Temperature Probe
4-LED RGB Fan
8-LED RGB Fan
QL Fan Series
LL Fan Series
ML Fan Series
Any PWM Fan |
+| iCUE COMMANDER Core XT | `0c2a` | Show
External RGB Hub
2x Temperature Probe
4-LED RGB Fan
8-LED RGB Fan
QL Fan Series
LL Fan Series
ML Fan Series
Any PWM Fan
H55 RGB AIO
H100 RGB AIO
H150 RGB AIO |
+| iCUE H100i RGB ELITE | `0c35`
`0c40` | |
+| iCUE H115i RGB ELITE | `0c36` | |
+| iCUE H150i RGB ELITE | `0c37`
`0c41` | |
+| iCUE H100i RGB PRO XT | `0c20` | |
+| iCUE H115i RGB PRO XT | `0c21` | |
+| iCUE H150i RGB PRO XT | `0c22` | |
+| H115i RGB PLATINUM | `0c17` | |
+| H100i RGB PLATINUM | `0c18` | |
+| H100i RGB PLATINUM SE | `0c19` | |
+| Lighting Node CORE | `0c1a` | Show
HD RGB Series Fan
LL RGB Series Fan
ML PRO RGB Series Fan
QL RGB Series Fan
8-LED Series Fan
SP RGB Series Fan |
+| Lighting Node PRO | `0c0b` | Show
2x External RGB Hub
HD RGB Series Fan
LL RGB Series Fan
ML PRO RGB Series Fan
QL RGB Series Fan
8-LED Series Fan
SP RGB Series Fan |
+| Commander PRO | `0c10` | Show
2x External RGB Hub
4x Temperature Probe
Any PWM Fan |
+| XC7 ELITE LCD CPU Water Block | `0c42` | Show
RGB Control
LCD Control |
+| VENGEANCE RGB PRO | DDR4 | |
+| VENGEANCE RGB PRO SL | DDR4 | |
+| VENGEANCE RGB RT | DDR4 | |
+| VENGEANCE RGB RS | DDR4 | |
+| DOMINATOR PLATINUM RGB | DDR4 | |
+| VENGEANCE LPX | DDR4 | |
+| DOMINATOR PLATINUM | DDR4 | |
+| VENGEANCE | DDR5 | |
+| VENGEANCE RGB | DDR5 | |
+| DOMINATOR PLATINUM RGB | DDR5 | |
+| DOMINATOR TITANIUM RGB | DDR5 | |
+| Slipstream Wireless | `1bdc`
`1ba6`
`2b00` | K100 AIR RGB
IRONCLAW RGB WIRELESS
NIGHTSABRE WIRELESS
SCIMITAR RGB ELITE WIRELESS
M55 WIRELESS
DARK CORE RGB PRO SE WIRELESS
DARK CORE RGB PRO
M75 AIR WIRELESS
HARPOON RGB WIRELESS
DARKSTAR WIRELESS |
+| K55 CORE RGB | `1bfe` | |
+| K65 PRO MINI | `1bd7` | |
+| K70 CORE RGB | `1bfd` | |
+| K70 PRO RGB | `1bc6`
`1bb3` | |
+| K65 PLUS | `2b10`
`2b07` | USB
Wireless |
+| K100 AIR RGB | `1bab` | USB |
+| K100 | `1bc5`
`1b7c`
`1b7d` | USB |
+| K70 RGB MK.2 | `1b55`
`1b49`
`1b6b` | USB |
+| KATAR PRO | `1b93` | DPI Control
RGB Control |
+| KATAR PRO XT | `1bac` | DPI Control
RGB Control |
+| KATAR PRO WIRELESS | `1b94` | DPI Control |
+| IRONCLAW RGB | `1b5d` | DPI Control
RGB Control |
+| IRONCLAW RGB WIRELESS | `1b4c` | DPI Control
RGB Control |
+| NIGHTSABRE WIRELESS | `1bb8` | DPI Control
RGB Control |
+| SCIMITAR RGB ELITE | `1be3` | DPI Control
RGB Control |
+| SCIMITAR RGB ELITE WIRELESS | `1bdb` | DPI Control
RGB Control |
+| M55 | `2b03` | DPI Control |
+| M55 RGB PRO | `1b70` | DPI Control
RGB Control |
+| DARK CORE RGB PRO SE WIRELESS | `1b7e` | DPI Control
RGB Control |
+| DARK CORE RGB PRO | `1b80` | DPI Control
RGB Control |
+| M75 | `1bf0` | DPI Control
RGB Control |
+| M75 AIR WIRELESS | `1bf2` | DPI Control |
+| M65 RGB ULTRA | `1b9e` | DPI Control
RGB Control |
+| HARPOON RGB PRO | `1b75` | DPI Control
RGB Control |
+| HARPOON RGB WIRELESS | `1b5e` | DPI Control
RGB Control |
+| DARKSTAR WIRELESS | `1bb2` | DPI Control
RGB Control |
+| VIRTUOSO RGB WIRELESS XT | `0a62`
`0a64` | RGB Control |
+| ST100 RGB | `0a34` | RGB |
+| MM700 RGB | `1b9b` | RGB |
+| LT100 Smart Lighting Tower | `0c23` | RGB |
+| HX1000i | `1c07`
`1c1e` | Fan Control |
+| HX1200i | `1c08`
`1c23` | Fan Control |
+| HX1500i | `1c1f` | Fan Control |
+| HX750i | `1c05` | Fan Control |
+| HX850i | `1c06` | Fan Control |
+| RM850i | `1c0c` | Fan Control |
+| RM1000i | `1c0d` | Fan Control |
## Installation (automatic)
1. Download either .deb or .rpm package from the latest Release, depends on your Linux distribution
@@ -221,22 +223,13 @@ $ docker run --network host --privileged -v ./config.json:/opt/OpenLinkHub/confi
## LCD
- LCD images / animations are located in `/opt/OpenLinkHub/database/lcd/images/`
-## Device Dashboard
+## Dashboard
- Device Dashboard is accessible by browser via link `http://127.0.0.1:27003/`
- Device Dashboard allows you to control your devices.
-## RGB Modes
-- RGB configuration is located at `database/rgb/your-device-serial.json` file.
-### Configuration
-- profiles: Custom RGB mode data
- - key: RGB profile name
- - speed: RGB effect speed, from 1 to 10
- - brightness: Color brightness, from 0.1 to 1
- - smoothness: How smooth transition from one color to another is.
- - the smoothness is in range of 1 to 40
- - start: Custom starting color in (R, G, B, brightness format)
- - end: Custom ending color (R, G, B, brightness format)
- - If you want random colors, remove data from start and end JSON block. `"start":{}` and `"end":{}`
+## RGB
+- RGB configuration is located at `database/rgb/your-device-serial.json` file
+- RGB can be configured via RGB Editor in Dashboard
## API
- OpenLinkHub ships with built-in HTTP server for device overview and control.
diff --git a/database/keyboard/k70mk2-eu.json b/database/keyboard/k70mk2-eu.json
new file mode 100644
index 0000000..5a020b5
--- /dev/null
+++ b/database/keyboard/k70mk2-eu.json
@@ -0,0 +1,1541 @@
+{
+ "key": "k70mk2-default",
+ "device": "K70 MK2",
+ "layout": "EU",
+ "rows": 6,
+ "row": {
+ "0": {
+ "keys": {
+ "1": {
+ "keyName": "icon-user.svg",
+ "width": 65,
+ "height": 40,
+ "left": 0,
+ "top": 0,
+ "packetIndex": [125],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "2": {
+ "keyName": "icon-brightness.svg",
+ "width": 65,
+ "height": 40,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [137],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "3": {
+ "keyName": "icon-lock.svg",
+ "width": 65,
+ "height": 40,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [8],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "4": {
+ "keyName": "LOGO",
+ "width": 202,
+ "height": 40,
+ "left": 400,
+ "top": 0,
+ "packetIndex": [47],
+ "color": {
+ "red": 255,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "5": {
+ "keyName": "LOGO",
+ "width": 202,
+ "height": 40,
+ "left": 0,
+ "top": 0,
+ "packetIndex": [59],
+ "color": {
+ "red": 255,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "6": {
+ "keyName": "icon-mute.svg",
+ "width": 65,
+ "height": 40,
+ "left": 436,
+ "top": 0,
+ "packetIndex": [20],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ }
+ }
+ },
+ "1": {
+ "keys": {
+ "7": {
+ "keyName": "ESC",
+ "width": 65,
+ "height": 70,
+ "left": 0,
+ "top": 50,
+ "packetIndex": [0],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "8": {
+ "keyName": "F1",
+ "width": 65,
+ "height": 70,
+ "left": 95,
+ "top": 50,
+ "packetIndex": [12],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "9": {
+ "keyName": "F2",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [24],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "10": {
+ "keyName": "F3",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [36],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "11": {
+ "keyName": "F4",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [48],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "12": {
+ "keyName": "F5",
+ "width": 65,
+ "height": 70,
+ "left": 55,
+ "top": 50,
+ "packetIndex": [60],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "13": {
+ "keyName": "F6",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [72],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "14": {
+ "keyName": "F7",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [84],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "15": {
+ "keyName": "F8",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [96],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "16": {
+ "keyName": "F9",
+ "width": 65,
+ "height": 70,
+ "left": 60,
+ "top": 50,
+ "packetIndex": [108],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "17": {
+ "keyName": "F10",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [120],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "18": {
+ "keyName": "F11",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [132],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "19": {
+ "keyName": "F12",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [6],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "20": {
+ "keyName": "PrtSc",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 50,
+ "packetIndex": [18],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "21": {
+ "keyName": "ScrLk",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [30],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "22": {
+ "keyName": "Pause",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [42],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "23": {
+ "keyName": "icon-stop.svg",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 50,
+ "packetIndex": [32],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "24": {
+ "keyName": "icon-backward.svg",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [44],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "25": {
+ "keyName": "icon-pause.svg",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [56],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "26": {
+ "keyName": "icon-forward.svg",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [68],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ }
+ }
+ },
+ "2": {
+ "keys": {
+ "27": {
+ "keyName": "` ~",
+ "width": 65,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [1],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "28": {
+ "keyName": "1",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [13],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "29": {
+ "keyName": "2",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [25],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "30": {
+ "keyName": "3",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [37],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "31": {
+ "keyName": "4",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [49],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "32": {
+ "keyName": "5",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [61],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "33": {
+ "keyName": "6",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [73],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "34": {
+ "keyName": "7",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [85],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "35": {
+ "keyName": "8",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [97],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "36": {
+ "keyName": "9",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [109],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "37": {
+ "keyName": "0",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [121],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "38": {
+ "keyName": "-",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [133],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "39": {
+ "keyName": "=",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [7],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "40": {
+ "keyName": "<---",
+ "width": 150,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [31],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "41": {
+ "keyName": "Ins",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [54],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "42": {
+ "keyName": "Home",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [66],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "43": {
+ "keyName": "PgUp",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [78],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "44": {
+ "keyName": "Num",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [80],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "45": {
+ "keyName": "/",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [92],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "46": {
+ "keyName": "*",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [104],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "47": {
+ "keyName": "-",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [116],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "3": {
+ "keys": {
+ "48": {
+ "keyName": "Tab",
+ "width": 108,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [2],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "49": {
+ "keyName": "Q",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [14],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "50": {
+ "keyName": "W",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [26],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "51": {
+ "keyName": "E",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [38],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "52": {
+ "keyName": "R",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [50],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "53": {
+ "keyName": "T",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [62],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "54": {
+ "keyName": "Z",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [74],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "55": {
+ "keyName": "U",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [86],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "56": {
+ "keyName": "I",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [98],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "57": {
+ "keyName": "O",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [110],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "58": {
+ "keyName": "P",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [122],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "59": {
+ "keyName": "[ {",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [134],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "60": {
+ "keyName": "] }",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [90],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "61": {
+ "keyName": "Enter",
+ "width": 107,
+ "height": 155,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [126],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "62": {
+ "keyName": "Del",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [43],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "63": {
+ "keyName": "End",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [55],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "64": {
+ "keyName": "PgDn",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [67],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "65": {
+ "keyName": "7",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [9],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "66": {
+ "keyName": "8",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [21],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "67": {
+ "keyName": "9",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [33],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "68": {
+ "keyName": "+",
+ "width": 65,
+ "height": 155,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [128],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "4": {
+ "keys": {
+ "69": {
+ "keyName": "CAPS",
+ "width": 108,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [3],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "70": {
+ "keyName": "A",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [15],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "71": {
+ "keyName": "S",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [27],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "72": {
+ "keyName": "D",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [39],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "73": {
+ "keyName": "F",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [51],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "74": {
+ "keyName": "G",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [63],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "75": {
+ "keyName": "H",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [75],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "76": {
+ "keyName": "J",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [87],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "77": {
+ "keyName": "K",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [99],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "78": {
+ "keyName": "L",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [111],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "79": {
+ "keyName": "; :",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [123],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "80": {
+ "keyName": "' ''",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [135],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "81": {
+ "keyName": "\\ |",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [114],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "82": {
+ "keyName": "4",
+ "width": 65,
+ "height": 70,
+ "left": 397,
+ "top": 15,
+ "packetIndex": [57],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "83": {
+ "keyName": "5",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [69],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "84": {
+ "keyName": "6",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [81],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "5": {
+ "keys": {
+ "85": {
+ "keyName": "Shift",
+ "width": 90,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [4],
+ "color": {
+ "red": 255,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "86": {
+ "keyName": "⊞",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [16],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "87": {
+ "keyName": "Y",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [28],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "88": {
+ "keyName": "X",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [40],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "89": {
+ "keyName": "C",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [52],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "90": {
+ "keyName": "V",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [64],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "91": {
+ "keyName": "B",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [76],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "92": {
+ "keyName": "N",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [88],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "93": {
+ "keyName": "M",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [100],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "94": {
+ "keyName": ", <",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [112],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "95": {
+ "keyName": ". >",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [124],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "96": {
+ "keyName": "/ ?",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [136],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "97": {
+ "keyName": "Shift",
+ "width": 205,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [79],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "98": {
+ "keyName": "↑",
+ "width": 65,
+ "height": 70,
+ "left": 105,
+ "top": 15,
+ "packetIndex": [103],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "99": {
+ "keyName": "1",
+ "width": 65,
+ "height": 70,
+ "left": 105,
+ "top": 15,
+ "packetIndex": [93],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "100": {
+ "keyName": "2",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [105],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "101": {
+ "keyName": "3",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [117],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "102": {
+ "keyName": "Enter",
+ "width": 65,
+ "height": 155,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [140],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "6": {
+ "keys": {
+ "103": {
+ "keyName": "Ctrl",
+ "width": 90,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [5],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "104": {
+ "keyName": "icon-windows.svg",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [17],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "105": {
+ "keyName": "Alt",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [29],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "106": {
+ "keyName": "----------",
+ "width": 480,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [53],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "107": {
+ "keyName": "Alt",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [89],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "108": {
+ "keyName": "icon-windows.svg",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [101],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "109": {
+ "keyName": "RC",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [113],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "110": {
+ "keyName": "Ctrl",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [91],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "111": {
+ "keyName": "←",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [115],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "112": {
+ "keyName": "↓",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [127],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "113": {
+ "keyName": "→",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [139],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "114": {
+ "keyName": "0",
+ "width": 145,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [129],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "115": {
+ "keyName": ".",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [141],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/database/keyboard/k70mk2.json b/database/keyboard/k70mk2.json
new file mode 100644
index 0000000..7f65c79
--- /dev/null
+++ b/database/keyboard/k70mk2.json
@@ -0,0 +1,1541 @@
+{
+ "key": "k70mk2-default",
+ "device": "K70 MK2",
+ "layout": "US",
+ "rows": 6,
+ "row": {
+ "0": {
+ "keys": {
+ "1": {
+ "keyName": "icon-user.svg",
+ "width": 65,
+ "height": 40,
+ "left": 0,
+ "top": 0,
+ "packetIndex": [125],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "2": {
+ "keyName": "icon-brightness.svg",
+ "width": 65,
+ "height": 40,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [137],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "3": {
+ "keyName": "icon-lock.svg",
+ "width": 65,
+ "height": 40,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [8],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "4": {
+ "keyName": "LOGO",
+ "width": 202,
+ "height": 40,
+ "left": 400,
+ "top": 0,
+ "packetIndex": [47],
+ "color": {
+ "red": 255,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "5": {
+ "keyName": "LOGO",
+ "width": 202,
+ "height": 40,
+ "left": 0,
+ "top": 0,
+ "packetIndex": [59],
+ "color": {
+ "red": 255,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "6": {
+ "keyName": "icon-mute.svg",
+ "width": 65,
+ "height": 40,
+ "left": 436,
+ "top": 0,
+ "packetIndex": [20],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ }
+ }
+ },
+ "1": {
+ "keys": {
+ "7": {
+ "keyName": "ESC",
+ "width": 65,
+ "height": 70,
+ "left": 0,
+ "top": 50,
+ "packetIndex": [0],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "8": {
+ "keyName": "F1",
+ "width": 65,
+ "height": 70,
+ "left": 95,
+ "top": 50,
+ "packetIndex": [12],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "9": {
+ "keyName": "F2",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [24],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "10": {
+ "keyName": "F3",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [36],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "11": {
+ "keyName": "F4",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [48],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "12": {
+ "keyName": "F5",
+ "width": 65,
+ "height": 70,
+ "left": 55,
+ "top": 50,
+ "packetIndex": [60],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "13": {
+ "keyName": "F6",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [72],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "14": {
+ "keyName": "F7",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [84],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "15": {
+ "keyName": "F8",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [96],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "16": {
+ "keyName": "F9",
+ "width": 65,
+ "height": 70,
+ "left": 60,
+ "top": 50,
+ "packetIndex": [108],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "17": {
+ "keyName": "F10",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [120],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "18": {
+ "keyName": "F11",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [132],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "19": {
+ "keyName": "F12",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [6],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "20": {
+ "keyName": "PrtSc",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 50,
+ "packetIndex": [18],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "21": {
+ "keyName": "ScrLk",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [30],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "22": {
+ "keyName": "Pause",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [42],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "23": {
+ "keyName": "icon-stop.svg",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 50,
+ "packetIndex": [32],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "24": {
+ "keyName": "icon-backward.svg",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [44],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "25": {
+ "keyName": "icon-pause.svg",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [56],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "26": {
+ "keyName": "icon-forward.svg",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 50,
+ "packetIndex": [68],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ }
+ }
+ },
+ "2": {
+ "keys": {
+ "27": {
+ "keyName": "` ~",
+ "width": 65,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [1],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "28": {
+ "keyName": "1",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [13],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "29": {
+ "keyName": "2",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [25],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "30": {
+ "keyName": "3",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [37],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "31": {
+ "keyName": "4",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [49],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "32": {
+ "keyName": "5",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [61],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "33": {
+ "keyName": "6",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [73],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "34": {
+ "keyName": "7",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [85],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "35": {
+ "keyName": "8",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [97],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "36": {
+ "keyName": "9",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [109],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "37": {
+ "keyName": "0",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [121],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "38": {
+ "keyName": "-",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [133],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "39": {
+ "keyName": "=",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [7],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "40": {
+ "keyName": "<---",
+ "width": 150,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [31],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "41": {
+ "keyName": "Ins",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [54],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "42": {
+ "keyName": "Home",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [66],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "43": {
+ "keyName": "PgUp",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [78],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "44": {
+ "keyName": "Num",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [80],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "45": {
+ "keyName": "/",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [92],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "46": {
+ "keyName": "*",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [104],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "47": {
+ "keyName": "-",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [116],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "3": {
+ "keys": {
+ "48": {
+ "keyName": "Tab",
+ "width": 108,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [2],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "49": {
+ "keyName": "Q",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [14],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "50": {
+ "keyName": "W",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [26],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "51": {
+ "keyName": "E",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [38],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "52": {
+ "keyName": "R",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [50],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "53": {
+ "keyName": "T",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [62],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "54": {
+ "keyName": "Y",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [74],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "55": {
+ "keyName": "U",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [86],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "56": {
+ "keyName": "I",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [98],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "57": {
+ "keyName": "O",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [110],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "58": {
+ "keyName": "P",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [122],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "59": {
+ "keyName": "[ {",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [134],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "60": {
+ "keyName": "] }",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [90],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "61": {
+ "keyName": "Enter",
+ "width": 107,
+ "height": 155,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [126],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "62": {
+ "keyName": "Del",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [43],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "63": {
+ "keyName": "End",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [55],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "64": {
+ "keyName": "PgDn",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [67],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "65": {
+ "keyName": "7",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [9],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "66": {
+ "keyName": "8",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [21],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "67": {
+ "keyName": "9",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [33],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "68": {
+ "keyName": "+",
+ "width": 65,
+ "height": 155,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [128],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "4": {
+ "keys": {
+ "69": {
+ "keyName": "CAPS",
+ "width": 108,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [3],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "70": {
+ "keyName": "A",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [15],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "71": {
+ "keyName": "S",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [27],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "72": {
+ "keyName": "D",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [39],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "73": {
+ "keyName": "F",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [51],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "74": {
+ "keyName": "G",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [63],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "75": {
+ "keyName": "H",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [75],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "76": {
+ "keyName": "J",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [87],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "77": {
+ "keyName": "K",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [99],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "78": {
+ "keyName": "L",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [111],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "79": {
+ "keyName": "; :",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [123],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "80": {
+ "keyName": "' ''",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [135],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "81": {
+ "keyName": "\\ |",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [114],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "82": {
+ "keyName": "4",
+ "width": 65,
+ "height": 70,
+ "left": 397,
+ "top": 15,
+ "packetIndex": [57],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "83": {
+ "keyName": "5",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [69],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "84": {
+ "keyName": "6",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [81],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "5": {
+ "keys": {
+ "85": {
+ "keyName": "Shift",
+ "width": 90,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [4],
+ "color": {
+ "red": 255,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "86": {
+ "keyName": "⊞",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [16],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "87": {
+ "keyName": "Z",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [28],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "88": {
+ "keyName": "X",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [40],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "89": {
+ "keyName": "C",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [52],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "90": {
+ "keyName": "V",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [64],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "91": {
+ "keyName": "B",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [76],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "92": {
+ "keyName": "N",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [88],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "93": {
+ "keyName": "M",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [100],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "94": {
+ "keyName": ", <",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [112],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "95": {
+ "keyName": ". >",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [124],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "96": {
+ "keyName": "/ ?",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [136],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "97": {
+ "keyName": "Shift",
+ "width": 205,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [79],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "98": {
+ "keyName": "↑",
+ "width": 65,
+ "height": 70,
+ "left": 105,
+ "top": 15,
+ "packetIndex": [103],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "99": {
+ "keyName": "1",
+ "width": 65,
+ "height": 70,
+ "left": 105,
+ "top": 15,
+ "packetIndex": [93],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "100": {
+ "keyName": "2",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [105],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "101": {
+ "keyName": "3",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [117],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "102": {
+ "keyName": "Enter",
+ "width": 65,
+ "height": 155,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [140],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "6": {
+ "keys": {
+ "103": {
+ "keyName": "Ctrl",
+ "width": 90,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [5],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "104": {
+ "keyName": "icon-windows.svg",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [17],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "105": {
+ "keyName": "Alt",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [29],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "106": {
+ "keyName": "----------",
+ "width": 480,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [53],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "107": {
+ "keyName": "Alt",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [89],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "108": {
+ "keyName": "icon-windows.svg",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [101],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ },
+ "svg": true
+ },
+ "109": {
+ "keyName": "RC",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [113],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "110": {
+ "keyName": "Ctrl",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [91],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "111": {
+ "keyName": "←",
+ "width": 65,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [115],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "112": {
+ "keyName": "↓",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [127],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "113": {
+ "keyName": "→",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [139],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "114": {
+ "keyName": "0",
+ "width": 145,
+ "height": 70,
+ "left": 25,
+ "top": 15,
+ "packetIndex": [129],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "115": {
+ "keyName": ".",
+ "width": 65,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [141],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/devices/darkstarW/darkstarW.go b/src/devices/darkstarW/darkstarW.go
new file mode 100644
index 0000000..118d608
--- /dev/null
+++ b/src/devices/darkstarW/darkstarW.go
@@ -0,0 +1,1339 @@
+package darkstarW
+
+// Package: CORSAIR DARKSTAR RGB Wireless
+// This is the primary package for CORSAIR IRONCLAW RGB Wireless.
+// All device actions are controlled from this package.
+// Author: Nikola Jurkovic
+// License: GPL-3.0 or later
+
+import (
+ "OpenLinkHub/src/common"
+ "OpenLinkHub/src/config"
+ "OpenLinkHub/src/logger"
+ "OpenLinkHub/src/rgb"
+ "OpenLinkHub/src/temperatures"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "github.com/sstallion/go-hid"
+ "os"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+)
+
+type ZoneColors struct {
+ Color *rgb.Color
+ ColorIndex []int
+ Name string
+}
+
+// DeviceProfile struct contains all device profile
+type DeviceProfile struct {
+ Active bool
+ Path string
+ Product string
+ Serial string
+ Brightness uint8
+ BrightnessSlider *uint8
+ OriginalBrightness uint8
+ RGBProfile string
+ Label string
+ Profile int
+ DPIColor *rgb.Color
+ ZoneColors map[int]ZoneColors
+ Profiles map[int]DPIProfile
+ SleepMode int
+}
+
+type DPIProfile struct {
+ Name string `json:"name"`
+ Value uint16
+ PackerIndex int
+ ColorIndex map[int][]int
+}
+
+type Device struct {
+ Debug bool
+ dev *hid.Device
+ listener *hid.Device
+ Manufacturer string `json:"manufacturer"`
+ Product string `json:"product"`
+ Serial string `json:"serial"`
+ Firmware string `json:"firmware"`
+ activeRgb *rgb.ActiveRGB
+ UserProfiles map[string]*DeviceProfile `json:"userProfiles"`
+ Devices map[int]string `json:"devices"`
+ DeviceProfile *DeviceProfile
+ OriginalProfile *DeviceProfile
+ Template string
+ VendorId uint16
+ ProductId uint16
+ SlipstreamId uint16
+ Brightness map[int]string
+ LEDChannels int
+ ChangeableLedChannels int
+ CpuTemp float32
+ GpuTemp float32
+ Layouts []string
+ Rgb *rgb.RGB
+ Endpoint byte
+ SleepModes map[int]string
+ Connected bool
+ Exit bool
+ mutex sync.Mutex
+ timer *time.Ticker
+ autoRefreshChan chan struct{}
+}
+
+var (
+ pwd = ""
+ cmdSoftwareMode = []byte{0x01, 0x03, 0x00, 0x02}
+ cmdHardwareMode = []byte{0x01, 0x03, 0x00, 0x01}
+ cmdSleepMode = []byte{0x01, 0x03, 0x00, 0x04}
+ cmdGetFirmware = []byte{0x02, 0x13}
+ cmdWriteColor = []byte{0x06, 0x00}
+ cmdOpenEndpoint = []byte{0x0d, 0x00, 0x01}
+ cmdOpenWriteEndpoint = []byte{0x01, 0x0d, 0x00, 0x01}
+ cmdSetDpi = []byte{0x01, 0x20, 0x00}
+ cmdSleep = map[int][]byte{0: {0x01, 0x37, 0x00}, 1: {0x01, 0x0e, 0x00}}
+ bufferSize = 64
+ bufferSizeWrite = bufferSize + 1
+ headerSize = 2
+ headerWriteSize = 4
+ minDpiValue = 100
+ maxDpiValue = 26000
+ deviceRefreshInterval = 1000
+)
+
+func Init(vendorId, slipstreamId, productId uint16, dev *hid.Device, endpoint byte, serial string) *Device {
+ // Set global working directory
+ pwd = config.GetConfig().ConfigPath
+
+ // Init new struct with HID device
+ d := &Device{
+ dev: dev,
+ Template: "darkstarW.html",
+ VendorId: vendorId,
+ ProductId: productId,
+ SlipstreamId: slipstreamId,
+ Serial: serial,
+ Endpoint: endpoint,
+ Firmware: "n/a",
+ Brightness: map[int]string{
+ 0: "RGB Profile",
+ 1: "33 %",
+ 2: "66 %",
+ 3: "100 %",
+ },
+ Product: "DARKSTAR",
+ SleepModes: map[int]string{
+ 1: "1 minute",
+ 5: "5 minutes",
+ 10: "10 minutes",
+ 15: "15 minutes",
+ 30: "30 minutes",
+ 60: "1 hour",
+ },
+ LEDChannels: 13,
+ ChangeableLedChannels: 10,
+ autoRefreshChan: make(chan struct{}),
+ timer: &time.Ticker{},
+ }
+
+ d.getDebugMode() // Debug mode
+ d.loadRgb() // Load RGB
+ d.loadDeviceProfiles() // Load all device profiles
+ d.saveDeviceProfile() // Save profile
+ d.setAutoRefresh() // Set auto device refresh
+ return d
+}
+
+// GetRgbProfiles will return RGB profiles for a target device
+func (d *Device) GetRgbProfiles() interface{} {
+ return d.Rgb
+}
+
+// Stop will stop all device operations and switch a device back to hardware mode
+func (d *Device) Stop() {
+ // Placeholder
+}
+
+// StopInternal will stop all device operations and switch a device back to hardware mode
+func (d *Device) StopInternal() {
+ d.Exit = true
+ logger.Log(logger.Fields{"serial": d.Serial, "product": d.Product}).Info("Stopping device...")
+ if d.activeRgb != nil {
+ d.activeRgb.Stop()
+ }
+
+ d.timer.Stop()
+ var once sync.Once
+ go func() {
+ once.Do(func() {
+ if d.autoRefreshChan != nil {
+ close(d.autoRefreshChan)
+ }
+ })
+ }()
+
+ if d.Connected {
+ d.setHardwareMode()
+ }
+ logger.Log(logger.Fields{"serial": d.Serial, "product": d.Product}).Info("Device stopped")
+}
+
+// SetConnected will change connected status
+func (d *Device) SetConnected(value bool) {
+ d.Connected = value
+}
+
+// Connect will connect to a device
+func (d *Device) Connect() {
+ if !d.Connected {
+ d.Connected = true
+ d.clearBuffer() // Clear previous buffers
+ d.setHardwareMode() // Activate hardware mode
+ d.setSoftwareMode() // Activate software mode
+ d.getDeviceFirmware() // Firmware
+ d.initLeds() // Init LED ports
+ d.setDeviceColor() // Device color
+ d.toggleDPI() // DPI
+ d.setSleepTimer() // Sleep
+ }
+}
+
+// clearBuffer will flush any buffer remaining in the device
+func (d *Device) clearBuffer() {
+
+}
+
+// loadRgb will load RGB file if found, or create the default.
+func (d *Device) loadRgb() {
+ rgbDirectory := pwd + "/database/rgb/"
+ rgbFilename := rgbDirectory + d.Serial + ".json"
+
+ // Check if filename has .json extension
+ if !common.IsValidExtension(rgbFilename, ".json") {
+ return
+ }
+
+ if !common.FileExists(rgbFilename) {
+ profile := rgb.GetRGB()
+ profile.Device = d.Product
+
+ // Convert to JSON
+ buffer, err := json.MarshalIndent(profile, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to encode RGB json")
+ return
+ }
+
+ // Create profile filename
+ file, err := os.Create(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to create RGB json file")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to write to RGB json file")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to close RGB json file")
+ return
+ }
+ }
+
+ file, err := os.Open(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to load RGB")
+ return
+ }
+ if err = json.NewDecoder(file).Decode(&d.Rgb); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to decode profile")
+ return
+ }
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"location": rgbFilename, "serial": d.Serial}).Warn("Failed to close file handle")
+ }
+}
+
+// GetRgbProfile will return rgb.Profile struct
+func (d *Device) GetRgbProfile(profile string) *rgb.Profile {
+ if d.Rgb == nil {
+ return nil
+ }
+
+ if val, ok := d.Rgb.Profiles[profile]; ok {
+ return &val
+ }
+ return nil
+}
+
+// GetDeviceTemplate will return device template name
+func (d *Device) GetDeviceTemplate() string {
+ return d.Template
+}
+
+// ChangeDeviceProfile will change device profile
+func (d *Device) ChangeDeviceProfile(profileName string) uint8 {
+ if profile, ok := d.UserProfiles[profileName]; ok {
+ currentProfile := d.DeviceProfile
+ currentProfile.Active = false
+ d.DeviceProfile = currentProfile
+ d.saveDeviceProfile()
+
+ // RGB reset
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+
+ newProfile := profile
+ newProfile.Active = true
+ d.DeviceProfile = newProfile
+ d.saveDeviceProfile()
+ d.setDeviceColor()
+ return 1
+ }
+ return 0
+}
+
+// saveRgbProfile will save rgb profile data
+func (d *Device) saveRgbProfile() {
+ rgbDirectory := pwd + "/database/rgb/"
+ rgbFilename := rgbDirectory + d.Serial + ".json"
+ if common.FileExists(rgbFilename) {
+ buffer, err := json.MarshalIndent(d.Rgb, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to encode RGB json")
+ return
+ }
+
+ // Create profile filename
+ file, err := os.Create(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to create RGB json file")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to write to RGB json file")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to close RGB json file")
+ return
+ }
+ }
+}
+
+// UpdateRgbProfileData will update RGB profile data
+func (d *Device) UpdateRgbProfileData(profileName string, profile rgb.Profile) uint8 {
+ if d.GetRgbProfile(profileName) == nil {
+ logger.Log(logger.Fields{"serial": d.Serial, "profile": profile}).Warn("Non-existing RGB profile")
+ return 0
+ }
+
+ pf := d.GetRgbProfile(profileName)
+ profile.StartColor.Brightness = pf.StartColor.Brightness
+ profile.EndColor.Brightness = pf.EndColor.Brightness
+ pf.StartColor = profile.StartColor
+ pf.EndColor = profile.EndColor
+ pf.Speed = profile.Speed
+
+ d.Rgb.Profiles[profileName] = *pf
+ d.saveRgbProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+}
+
+// UpdateRgbProfile will update device RGB profile
+func (d *Device) UpdateRgbProfile(_ int, profile string) uint8 {
+ if d.GetRgbProfile(profile) == nil {
+ logger.Log(logger.Fields{"serial": d.Serial, "profile": profile}).Warn("Non-existing RGB profile")
+ return 0
+ }
+ d.DeviceProfile.RGBProfile = profile // Set profile
+ d.saveDeviceProfile() // Save profile
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+}
+
+// ChangeDeviceBrightness will change device brightness
+func (d *Device) ChangeDeviceBrightness(mode uint8) uint8 {
+ d.DeviceProfile.Brightness = mode
+ d.saveDeviceProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+}
+
+// ChangeDeviceBrightnessValue will change device brightness via slider
+func (d *Device) ChangeDeviceBrightnessValue(value uint8) uint8 {
+ if value < 0 || value > 100 {
+ return 0
+ }
+
+ d.DeviceProfile.BrightnessSlider = &value
+ d.saveDeviceProfile()
+
+ if d.DeviceProfile.RGBProfile == "static" || d.DeviceProfile.RGBProfile == "mouse" {
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ }
+ return 1
+}
+
+// SchedulerBrightness will change device brightness via scheduler
+func (d *Device) SchedulerBrightness(value uint8) uint8 {
+ if value == 0 {
+ d.DeviceProfile.OriginalBrightness = *d.DeviceProfile.BrightnessSlider
+ d.DeviceProfile.BrightnessSlider = &value
+ } else {
+ d.DeviceProfile.BrightnessSlider = &d.DeviceProfile.OriginalBrightness
+ }
+
+ d.saveDeviceProfile()
+ if d.DeviceProfile.RGBProfile == "static" || d.DeviceProfile.RGBProfile == "mouse" {
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ }
+ return 1
+}
+
+// SaveUserProfile will generate a new user profile configuration and save it to a file
+func (d *Device) SaveUserProfile(profileName string) uint8 {
+ if d.DeviceProfile != nil {
+ profilePath := pwd + "/database/profiles/" + d.Serial + "-" + profileName + ".json"
+
+ newProfile := d.DeviceProfile
+ newProfile.Path = profilePath
+ newProfile.Active = false
+
+ buffer, err := json.Marshal(newProfile)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to convert to json format")
+ return 0
+ }
+
+ // Create profile filename
+ file, err := os.Create(profilePath)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to create new device profile")
+ return 0
+ }
+
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to write data")
+ return 0
+ }
+
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to close file handle")
+ return 0
+ }
+ d.loadDeviceProfiles()
+ return 1
+ }
+ return 0
+}
+
+// SaveMouseDPI will save mouse DPI
+func (d *Device) SaveMouseDPI(stages map[int]uint16) uint8 {
+ i := 0
+ if d.DeviceProfile == nil {
+ return 0
+ }
+
+ if len(stages) == 0 {
+ return 0
+ }
+
+ for key, stage := range stages {
+ if _, ok := d.DeviceProfile.Profiles[key]; ok {
+ profile := d.DeviceProfile.Profiles[key]
+ if stage > uint16(maxDpiValue) {
+ continue
+ }
+ if stage < uint16(minDpiValue) {
+ continue
+ }
+ profile.Value = stage
+ d.DeviceProfile.Profiles[key] = profile
+ i++
+ }
+ }
+
+ if i > 0 {
+ d.saveDeviceProfile()
+ d.toggleDPI()
+ return 1
+ }
+ return 0
+}
+
+// SaveMouseZoneColors will save mouse zone colors
+func (d *Device) SaveMouseZoneColors(dpi rgb.Color, zoneColors map[int]rgb.Color) uint8 {
+ i := 0
+ if d.DeviceProfile == nil {
+ return 0
+ }
+ if dpi.Red > 255 ||
+ dpi.Green > 255 ||
+ dpi.Blue > 255 ||
+ dpi.Red < 0 ||
+ dpi.Green < 0 ||
+ dpi.Blue < 0 {
+ return 0
+ }
+
+ // DPI
+ dpiColor := d.DeviceProfile.DPIColor
+ dpiColor.Red = dpi.Red
+ dpiColor.Green = dpi.Green
+ dpiColor.Blue = dpi.Blue
+ dpiColor.Hex = fmt.Sprintf("#%02x%02x%02x", int(dpi.Red), int(dpi.Green), int(dpi.Blue))
+ d.DeviceProfile.DPIColor = dpiColor
+
+ // Zone Colors
+ for key, zone := range zoneColors {
+ if zone.Red > 255 ||
+ zone.Green > 255 ||
+ zone.Blue > 255 ||
+ zone.Red < 0 ||
+ zone.Green < 0 ||
+ zone.Blue < 0 {
+ continue
+ }
+ if zoneColor, ok := d.DeviceProfile.ZoneColors[key]; ok {
+ zoneColor.Color.Red = zone.Red
+ zoneColor.Color.Green = zone.Green
+ zoneColor.Color.Blue = zone.Blue
+ zoneColor.Color.Hex = fmt.Sprintf("#%02x%02x%02x", int(zone.Red), int(zone.Green), int(zone.Blue))
+ }
+ i++
+ }
+
+ if i > 0 {
+ d.saveDeviceProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+ }
+ return 0
+}
+
+// getDebugMode will set device debug
+func (d *Device) getDebugMode() {
+ d.Debug = config.GetConfig().Debug
+}
+
+// setHardwareMode will switch a device to hardware mode
+func (d *Device) setHardwareMode() {
+ if d.Connected {
+ _, err := d.transfer(cmdHardwareMode, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to change device mode")
+ }
+ }
+}
+
+// setSoftwareMode will switch a device to software mode
+func (d *Device) setSoftwareMode() {
+ _, err := d.transfer(cmdSoftwareMode, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to change device mode")
+ }
+ d.Connected = true
+}
+
+// SetSleepMode will switch a device to sleep mode
+func (d *Device) SetSleepMode() {
+ _, err := d.transfer(cmdSleepMode, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to change device mode")
+ }
+ //d.Connected = false
+}
+
+// GetSleepMode will return current sleep mode
+func (d *Device) GetSleepMode() int {
+ if d.DeviceProfile != nil {
+ return d.DeviceProfile.SleepMode
+ }
+ return 0
+}
+
+// getDeviceFirmware will return a device firmware version out as string
+func (d *Device) getDeviceFirmware() {
+ fw, err := d.transfer(
+ cmdGetFirmware,
+ nil,
+ )
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to get device firmware")
+ }
+
+ if fw[1] != 0x02 {
+ fw, err = d.transfer(
+ cmdGetFirmware,
+ nil,
+ )
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to get device firmware")
+ }
+ }
+ v1, v2, v3 := int(fw[3]), int(fw[4]), int(binary.LittleEndian.Uint16(fw[5:7]))
+ d.Firmware = fmt.Sprintf("%d.%d.%d", v1, v2, v3)
+}
+
+// saveDeviceProfile will save device profile for persistent configuration
+func (d *Device) saveDeviceProfile() {
+ var defaultBrightness = uint8(100)
+ profilePath := pwd + "/database/profiles/" + d.Serial + ".json"
+
+ deviceProfile := &DeviceProfile{
+ Product: d.Product,
+ Serial: d.Serial,
+ Path: profilePath,
+ BrightnessSlider: &defaultBrightness,
+ OriginalBrightness: 100,
+ }
+
+ // First save, assign saved profile to a device
+ if d.DeviceProfile == nil {
+ // RGB, Label
+ deviceProfile.RGBProfile = "mouse"
+ deviceProfile.Label = "Mouse"
+ deviceProfile.Active = true
+ deviceProfile.ZoneColors = map[int]ZoneColors{
+ 0: { // Front left
+ ColorIndex: []int{0, 13, 26},
+ Color: &rgb.Color{
+ Red: 255,
+ Green: 0,
+ Blue: 0,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 255, 0, 0),
+ },
+ Name: "Front Left",
+ },
+ 1: { // Front right
+ ColorIndex: []int{1, 14, 27},
+ Color: &rgb.Color{
+ Red: 255,
+ Green: 0,
+ Blue: 0,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 255, 0, 0),
+ },
+ Name: "Front Right",
+ },
+ 2: { // Side Accent 1
+ ColorIndex: []int{3, 16, 29},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 1",
+ },
+ 3: { // Side Accent 2
+ ColorIndex: []int{4, 17, 30},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 2",
+ },
+ 4: { // Side Accent 3
+ ColorIndex: []int{5, 18, 31},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 3",
+ },
+ 5: { // Side Accent 4
+ ColorIndex: []int{6, 19, 32},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 4",
+ },
+ 6: { // Side Accent 5
+ ColorIndex: []int{7, 20, 33},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 5",
+ },
+ 7: { // Side Accent 6
+ ColorIndex: []int{8, 21, 34},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 6",
+ },
+ 8: { // Logo
+ ColorIndex: []int{9, 22, 35},
+ Color: &rgb.Color{
+ Red: 255,
+ Green: 255,
+ Blue: 0,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 255, 255, 0),
+ },
+ Name: "Logo",
+ },
+ }
+ deviceProfile.DPIColor = &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ }
+ deviceProfile.Profiles = map[int]DPIProfile{
+ 0: {
+ Name: "Stage 1",
+ Value: 400,
+ ColorIndex: map[int][]int{
+ 0: {10, 23, 36},
+ },
+ },
+ 1: {
+ Name: "Stage 2",
+ Value: 800,
+ ColorIndex: map[int][]int{
+ 0: {10, 23, 36},
+ 1: {11, 24, 37},
+ },
+ },
+ 2: {
+ Name: "Stage 3",
+ Value: 1200,
+ ColorIndex: map[int][]int{
+ 0: {11, 24, 37},
+ },
+ },
+ 3: {
+ Name: "Stage 4",
+ Value: 1600,
+ ColorIndex: map[int][]int{
+ 0: {11, 24, 37},
+ 1: {12, 25, 38},
+ },
+ },
+ 4: {
+ Name: "Stage 5",
+ Value: 3200,
+ ColorIndex: map[int][]int{
+ 0: {12, 25, 38},
+ },
+ },
+ }
+ deviceProfile.Profile = 2
+ deviceProfile.SleepMode = 15
+ } else {
+ if d.DeviceProfile.BrightnessSlider == nil {
+ deviceProfile.BrightnessSlider = &defaultBrightness
+ d.DeviceProfile.BrightnessSlider = &defaultBrightness
+ } else {
+ deviceProfile.BrightnessSlider = d.DeviceProfile.BrightnessSlider
+ }
+ deviceProfile.Active = d.DeviceProfile.Active
+ deviceProfile.Brightness = d.DeviceProfile.Brightness
+ deviceProfile.OriginalBrightness = d.DeviceProfile.OriginalBrightness
+ deviceProfile.RGBProfile = d.DeviceProfile.RGBProfile
+ deviceProfile.Label = d.DeviceProfile.Label
+ deviceProfile.Profiles = d.DeviceProfile.Profiles
+ deviceProfile.Profile = d.DeviceProfile.Profile
+ deviceProfile.DPIColor = d.DeviceProfile.DPIColor
+ deviceProfile.ZoneColors = d.DeviceProfile.ZoneColors
+ deviceProfile.SleepMode = d.DeviceProfile.SleepMode
+
+ if len(d.DeviceProfile.Path) < 1 {
+ deviceProfile.Path = profilePath
+ d.DeviceProfile.Path = profilePath
+ } else {
+ deviceProfile.Path = d.DeviceProfile.Path
+ }
+ }
+
+ // Convert to JSON
+ buffer, err := json.MarshalIndent(deviceProfile, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to convert to json format")
+ return
+ }
+
+ // Create profile filename
+ file, fileErr := os.Create(deviceProfile.Path)
+ if fileErr != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Error("Unable to create new device profile")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Error("Unable to write data")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Error("Unable to close file handle")
+ }
+
+ d.loadDeviceProfiles() // Reload
+}
+
+// UpdateSleepTimer will update device sleep timer
+func (d *Device) UpdateSleepTimer(minutes int) uint8 {
+ if d.DeviceProfile != nil {
+ d.DeviceProfile.SleepMode = minutes
+ d.saveDeviceProfile()
+ d.setSleepTimer()
+ return 1
+ }
+ return 0
+}
+
+// setSleepTimer will set device sleep timer
+func (d *Device) setSleepTimer() uint8 {
+ if d.Exit {
+ return 0
+ }
+
+ if d.DeviceProfile != nil {
+ changed := 0
+ _, err := d.transfer(cmdOpenWriteEndpoint, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Warn("Unable to change device sleep timer")
+ return 0
+ }
+
+ buf := make([]byte, 4)
+ for i := 0; i < 2; i++ {
+ command := cmdSleep[i]
+ if i == 0 {
+ buf[0] = 0x10
+ buf[1] = 0x27
+ buf[2] = 0x00
+ buf[3] = 0x00
+ } else {
+ sleep := d.DeviceProfile.SleepMode * (60 * 1000)
+ binary.LittleEndian.PutUint32(buf, uint32(sleep))
+ }
+ _, err = d.transfer(command, buf)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Warn("Unable to change device sleep timer")
+ continue
+ }
+ changed++
+ }
+
+ if changed > 0 {
+ return 1
+ }
+ }
+ return 0
+}
+
+// loadDeviceProfiles will load custom user profiles
+func (d *Device) loadDeviceProfiles() {
+ profileList := make(map[string]*DeviceProfile, 0)
+ userProfileDirectory := pwd + "/database/profiles/"
+
+ files, err := os.ReadDir(userProfileDirectory)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": userProfileDirectory, "serial": d.Serial}).Error("Unable to read content of a folder")
+ return
+ }
+
+ for _, fi := range files {
+ pf := &DeviceProfile{}
+ if fi.IsDir() {
+ continue // Exclude folders if any
+ }
+
+ // Define a full path of filename
+ profileLocation := userProfileDirectory + fi.Name()
+
+ // Check if filename has .json extension
+ if !common.IsValidExtension(profileLocation, ".json") {
+ continue
+ }
+
+ fileName := strings.Split(fi.Name(), ".")[0]
+ if m, _ := regexp.MatchString("^[a-zA-Z0-9-]+$", fileName); !m {
+ continue
+ }
+
+ fileSerial := ""
+ if strings.Contains(fileName, "-") {
+ fileSerial = strings.Split(fileName, "-")[0]
+ } else {
+ fileSerial = fileName
+ }
+
+ if fileSerial != d.Serial {
+ continue
+ }
+
+ file, err := os.Open(profileLocation)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": profileLocation}).Warn("Unable to load profile")
+ continue
+ }
+ if err = json.NewDecoder(file).Decode(pf); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": profileLocation}).Warn("Unable to decode profile")
+ continue
+ }
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"location": profileLocation, "serial": d.Serial}).Warn("Failed to close file handle")
+ }
+
+ if pf.Serial == d.Serial {
+ if fileName == d.Serial {
+ profileList["default"] = pf
+ } else {
+ name := strings.Split(fileName, "-")[1]
+ profileList[name] = pf
+ }
+ logger.Log(logger.Fields{"location": profileLocation, "serial": d.Serial}).Info("Loaded custom user profile")
+ }
+ }
+ d.UserProfiles = profileList
+ d.getDeviceProfile()
+}
+
+// getDeviceProfile will load persistent device configuration
+func (d *Device) getDeviceProfile() {
+ if len(d.UserProfiles) == 0 {
+ logger.Log(logger.Fields{"serial": d.Serial}).Warn("No profile found for device. Probably initial start")
+ } else {
+ for _, pf := range d.UserProfiles {
+ if pf.Active {
+ d.DeviceProfile = pf
+ }
+ }
+ }
+}
+
+// initLeds will initialize LED endpoint
+func (d *Device) initLeds() {
+ _, err := d.transfer(cmdOpenEndpoint, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to change device mode")
+ }
+}
+
+// setCpuTemperature will store current CPU temperature
+func (d *Device) setTemperatures() {
+ d.CpuTemp = temperatures.GetCpuTemperature()
+ d.GpuTemp = temperatures.GetGpuTemperature()
+}
+
+// setAutoRefresh will refresh device data
+func (d *Device) setAutoRefresh() {
+ d.timer = time.NewTicker(time.Duration(deviceRefreshInterval) * time.Millisecond)
+ go func() {
+ for {
+ select {
+ case <-d.timer.C:
+ if d.Exit {
+ return
+ }
+ d.setTemperatures()
+ case <-d.autoRefreshChan:
+ d.timer.Stop()
+ return
+ }
+ }
+ }()
+}
+
+// setDeviceColor will activate and set device RGB
+func (d *Device) setDeviceColor() {
+ buf := make([]byte, d.LEDChannels*3)
+ if d.DeviceProfile == nil {
+ logger.Log(logger.Fields{"serial": d.Serial}).Error("Unable to set color. DeviceProfile is null!")
+ return
+ }
+
+ // DPI
+ dpiColor := d.DeviceProfile.DPIColor
+ dpiColor.Brightness = rgb.GetBrightnessValueFloat(*d.DeviceProfile.BrightnessSlider)
+ dpiColor = rgb.ModifyBrightness(*dpiColor)
+
+ dpiLeds := d.DeviceProfile.Profiles[d.DeviceProfile.Profile]
+ for i := 0; i < len(dpiLeds.ColorIndex); i++ {
+ dpiColorIndexRange := dpiLeds.ColorIndex[i]
+ for key, dpiColorIndex := range dpiColorIndexRange {
+ switch key {
+ case 0: // Red
+ buf[dpiColorIndex] = byte(dpiColor.Red)
+ case 1: // Green
+ buf[dpiColorIndex] = byte(dpiColor.Green)
+ case 2: // Blue
+ buf[dpiColorIndex] = byte(dpiColor.Blue)
+ }
+ }
+ }
+
+ if d.DeviceProfile.RGBProfile == "mouse" {
+ for _, zoneColor := range d.DeviceProfile.ZoneColors {
+ zoneColor.Color.Brightness = rgb.GetBrightnessValueFloat(*d.DeviceProfile.BrightnessSlider)
+ zoneColor.Color = rgb.ModifyBrightness(*zoneColor.Color)
+
+ zoneColorIndexRange := zoneColor.ColorIndex
+ for key, zoneColorIndex := range zoneColorIndexRange {
+ switch key {
+ case 0: // Red
+ buf[zoneColorIndex] = byte(zoneColor.Color.Red)
+ case 1: // Green
+ buf[zoneColorIndex] = byte(zoneColor.Color.Green)
+ case 2: // Blue
+ buf[zoneColorIndex] = byte(zoneColor.Color.Blue)
+ }
+ }
+ }
+ d.writeColor(buf)
+ return
+ }
+
+ if d.DeviceProfile.RGBProfile == "static" {
+ profile := d.GetRgbProfile("static")
+ if profile == nil {
+ return
+ }
+
+ profile.StartColor.Brightness = rgb.GetBrightnessValueFloat(*d.DeviceProfile.BrightnessSlider)
+ profileColor := rgb.ModifyBrightness(profile.StartColor)
+
+ for _, zoneColor := range d.DeviceProfile.ZoneColors {
+ zoneColorIndexRange := zoneColor.ColorIndex
+ for key, zoneColorIndex := range zoneColorIndexRange {
+ switch key {
+ case 0: // Red
+ buf[zoneColorIndex] = byte(profileColor.Red)
+ case 1: // Green
+ buf[zoneColorIndex] = byte(profileColor.Green)
+ case 2: // Blue
+ buf[zoneColorIndex] = byte(profileColor.Blue)
+ }
+ }
+ }
+ d.writeColor(buf)
+ return
+ }
+
+ go func(lightChannels int) {
+ startTime := time.Now()
+ d.activeRgb = rgb.Exit()
+
+ // Generate random colors
+ d.activeRgb.RGBStartColor = rgb.GenerateRandomColor(1)
+ d.activeRgb.RGBEndColor = rgb.GenerateRandomColor(1)
+
+ for {
+ select {
+ case <-d.activeRgb.Exit:
+ return
+ default:
+ buff := make([]byte, 0)
+ rgbCustomColor := true
+ profile := d.GetRgbProfile(d.DeviceProfile.RGBProfile)
+ if profile == nil {
+ for i := 0; i < d.ChangeableLedChannels*3; i++ {
+ buff = append(buff, []byte{0, 0, 0}...)
+ }
+ logger.Log(logger.Fields{"profile": d.DeviceProfile.RGBProfile, "serial": d.Serial}).Warn("No such RGB profile found")
+ continue
+ }
+ rgbModeSpeed := common.FClamp(profile.Speed, 0.1, 10)
+ // Check if we have custom colors
+ if (rgb.Color{}) == profile.StartColor || (rgb.Color{}) == profile.EndColor {
+ rgbCustomColor = false
+ }
+
+ r := rgb.New(
+ d.ChangeableLedChannels,
+ rgbModeSpeed,
+ nil,
+ nil,
+ profile.Brightness,
+ common.Clamp(profile.Smoothness, 1, 100),
+ time.Duration(rgbModeSpeed)*time.Second,
+ rgbCustomColor,
+ )
+
+ if rgbCustomColor {
+ r.RGBStartColor = &profile.StartColor
+ r.RGBEndColor = &profile.EndColor
+ } else {
+ r.RGBStartColor = d.activeRgb.RGBStartColor
+ r.RGBEndColor = d.activeRgb.RGBEndColor
+ }
+
+ // Brightness
+ r.RGBBrightness = rgb.GetBrightnessValueFloat(*d.DeviceProfile.BrightnessSlider)
+ r.RGBStartColor.Brightness = r.RGBBrightness
+ r.RGBEndColor.Brightness = r.RGBBrightness
+
+ switch d.DeviceProfile.RGBProfile {
+ case "off":
+ {
+ for n := 0; n < d.ChangeableLedChannels; n++ {
+ buff = append(buff, []byte{0, 0, 0}...)
+ }
+ }
+ case "rainbow":
+ {
+ r.Rainbow(startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "watercolor":
+ {
+ r.Watercolor(startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "cpu-temperature":
+ {
+ r.MinTemp = profile.MinTemp
+ r.MaxTemp = profile.MaxTemp
+ r.Temperature(float64(d.CpuTemp))
+ buff = append(buff, r.Output...)
+ }
+ case "gpu-temperature":
+ {
+ r.MinTemp = profile.MinTemp
+ r.MaxTemp = profile.MaxTemp
+ r.Temperature(float64(d.GpuTemp))
+ buff = append(buff, r.Output...)
+ }
+ case "colorpulse":
+ {
+ r.Colorpulse(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "static":
+ {
+ r.Static()
+ buff = append(buff, r.Output...)
+ }
+ case "rotator":
+ {
+ r.Rotator(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "wave":
+ {
+ r.Wave(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "storm":
+ {
+ r.Storm()
+ buff = append(buff, r.Output...)
+ }
+ case "flickering":
+ {
+ r.Flickering(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "colorshift":
+ {
+ r.Colorshift(&startTime, d.activeRgb)
+ buff = append(buff, r.Output...)
+ }
+ case "circleshift":
+ {
+ r.CircleShift(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "circle":
+ {
+ r.Circle(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "spinner":
+ {
+ r.Spinner(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "colorwarp":
+ {
+ r.Colorwarp(&startTime, d.activeRgb)
+ buff = append(buff, r.Output...)
+ }
+ }
+ m := 0
+ for _, zoneColor := range d.DeviceProfile.ZoneColors {
+ zoneColorIndexRange := zoneColor.ColorIndex
+ for _, zoneColorIndex := range zoneColorIndexRange {
+ buf[zoneColorIndex] = buff[m]
+ m++
+ }
+ }
+
+ d.writeColor(buf)
+ time.Sleep(40 * time.Millisecond)
+ }
+ }
+ }(d.ChangeableLedChannels)
+}
+
+func (d *Device) ModifyDpi(increment bool) {
+ if increment {
+ if d.DeviceProfile.Profile >= 4 {
+ return
+ }
+ d.DeviceProfile.Profile++
+ } else {
+ if d.DeviceProfile.Profile <= 0 {
+ return
+ }
+ d.DeviceProfile.Profile--
+ }
+ d.saveDeviceProfile()
+ d.toggleDPI()
+}
+
+// toggleDPI will change DPI mode
+func (d *Device) toggleDPI() {
+ if d.DeviceProfile != nil {
+ profile := d.DeviceProfile.Profiles[d.DeviceProfile.Profile]
+ value := profile.Value
+
+ // Send DPI packet
+ if value < uint16(minDpiValue) {
+ value = uint16(minDpiValue)
+ }
+ if value > uint16(maxDpiValue) {
+ value = uint16(maxDpiValue)
+ }
+
+ buf := make([]byte, 2)
+ binary.LittleEndian.PutUint16(buf[0:2], value)
+ _, err := d.transfer(cmdSetDpi, buf)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "vendorId": d.VendorId}).Error("Unable to set dpi")
+ }
+
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ }
+}
+
+// writeColor will write data to the device with a specific endpoint.
+func (d *Device) writeColor(data []byte) {
+ if d.Exit {
+ return
+ }
+ buffer := make([]byte, len(data)+headerWriteSize)
+ binary.LittleEndian.PutUint16(buffer[0:2], uint16(len(data)))
+ copy(buffer[headerWriteSize:], data)
+
+ _, err := d.transfer(cmdWriteColor, buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+}
+
+// transfer will send data to a device and retrieve device output
+func (d *Device) transfer(endpoint, buffer []byte) ([]byte, error) {
+ // Packet control, mandatory for this device
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ // Create write buffer
+ bufferW := make([]byte, bufferSizeWrite)
+ bufferW[1] = d.Endpoint
+ endpointHeaderPosition := bufferW[headerSize : headerSize+len(endpoint)]
+ copy(endpointHeaderPosition, endpoint)
+ if len(buffer) > 0 {
+ copy(bufferW[headerSize+len(endpoint):headerSize+len(endpoint)+len(buffer)], buffer)
+ }
+
+ // Create read buffer
+ bufferR := make([]byte, bufferSize)
+
+ // Send command to a device
+ if _, err := d.dev.Write(bufferW); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to a device")
+ return nil, err
+ }
+
+ // Get data from a device
+ if _, err := d.dev.Read(bufferR); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to read data from device")
+ return nil, err
+ }
+ return bufferR, nil
+}
diff --git a/src/devices/darkstarWU/darkstarWU.go b/src/devices/darkstarWU/darkstarWU.go
new file mode 100644
index 0000000..3eb70e0
--- /dev/null
+++ b/src/devices/darkstarWU/darkstarWU.go
@@ -0,0 +1,1418 @@
+package darkstarWU
+
+// Package: CORSAIR DARKSTAR RGB Wireless
+// This is the primary package for CORSAIR DARKSTAR RGB Wireless.
+// All device actions are controlled from this package.
+// Author: Nikola Jurkovic
+// License: GPL-3.0 or later
+
+import (
+ "OpenLinkHub/src/common"
+ "OpenLinkHub/src/config"
+ "OpenLinkHub/src/inputmanager"
+ "OpenLinkHub/src/logger"
+ "OpenLinkHub/src/rgb"
+ "OpenLinkHub/src/temperatures"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "github.com/sstallion/go-hid"
+ "os"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+)
+
+type ZoneColors struct {
+ Color *rgb.Color
+ ColorIndex []int
+ Name string
+}
+
+// DeviceProfile struct contains all device profile
+type DeviceProfile struct {
+ Active bool
+ Path string
+ Product string
+ Serial string
+ Brightness uint8
+ RGBProfile string
+ BrightnessSlider *uint8
+ OriginalBrightness uint8
+ Label string
+ Profile int
+ DPIColor *rgb.Color
+ ZoneColors map[int]ZoneColors
+ Profiles map[int]DPIProfile
+ SleepMode int
+}
+
+type DPIProfile struct {
+ Name string `json:"name"`
+ Value uint16
+ ColorIndex map[int][]int
+}
+
+type Device struct {
+ Debug bool
+ dev *hid.Device
+ listener *hid.Device
+ Manufacturer string `json:"manufacturer"`
+ Product string `json:"product"`
+ Serial string `json:"serial"`
+ Firmware string `json:"firmware"`
+ activeRgb *rgb.ActiveRGB
+ UserProfiles map[string]*DeviceProfile `json:"userProfiles"`
+ Devices map[int]string `json:"devices"`
+ DeviceProfile *DeviceProfile
+ OriginalProfile *DeviceProfile
+ Template string
+ VendorId uint16
+ ProductId uint16
+ Brightness map[int]string
+ LEDChannels int
+ ChangeableLedChannels int
+ CpuTemp float32
+ GpuTemp float32
+ Layouts []string
+ Rgb *rgb.RGB
+ SleepModes map[int]string
+ mutex sync.Mutex
+ timerKeepAlive *time.Ticker
+ keepAliveChan chan struct{}
+ timer *time.Ticker
+ autoRefreshChan chan struct{}
+ Exit bool
+}
+
+var (
+ pwd = ""
+ cmdSoftwareMode = []byte{0x01, 0x03, 0x00, 0x02}
+ cmdHardwareMode = []byte{0x01, 0x03, 0x00, 0x01}
+ cmdGetFirmware = []byte{0x02, 0x13}
+ cmdWriteColor = []byte{0x06, 0x00}
+ cmdOpenEndpoint = []byte{0x0d, 0x00, 0x01}
+ cmdOpenWriteEndpoint = []byte{0x01, 0x0d, 0x00, 0x01}
+ cmdSetDpi = []byte{0x01, 0x20, 0x00}
+ cmdHeartbeat = []byte{0x12}
+ cmdSleep = map[int][]byte{0: {0x01, 0x37, 0x00}, 1: {0x01, 0x0e, 0x00}}
+ bufferSize = 64
+ bufferSizeWrite = bufferSize + 1
+ headerSize = 2
+ headerWriteSize = 4
+ minDpiValue = 100
+ maxDpiValue = 26000
+ deviceKeepAlive = 20000
+ deviceRefreshInterval = 1000
+)
+
+func Init(vendorId, productId uint16, key string) *Device {
+ // Set global working directory
+ pwd = config.GetConfig().ConfigPath
+
+ dev, err := hid.OpenPath(key)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "vendorId": vendorId, "productId": productId}).Error("Unable to open HID device")
+ return nil
+ }
+
+ // Init new struct with HID device
+ d := &Device{
+ dev: dev,
+ Template: "darkstarWU.html",
+ VendorId: vendorId,
+ ProductId: productId,
+ Firmware: "n/a",
+ Brightness: map[int]string{
+ 0: "RGB Profile",
+ 1: "33 %",
+ 2: "66 %",
+ 3: "100 %",
+ },
+ Product: "DARKSTAR",
+ SleepModes: map[int]string{
+ 1: "1 minute",
+ 5: "5 minutes",
+ 10: "10 minutes",
+ 15: "15 minutes",
+ 30: "30 minutes",
+ 60: "1 hour",
+ },
+ LEDChannels: 13,
+ ChangeableLedChannels: 10,
+ keepAliveChan: make(chan struct{}),
+ timerKeepAlive: &time.Ticker{},
+ autoRefreshChan: make(chan struct{}),
+ timer: &time.Ticker{},
+ }
+
+ d.getDebugMode() // Debug mode
+ d.getManufacturer() // Manufacturer
+ d.getSerial() // Serial
+ d.loadRgb() // Load RGB
+ d.loadDeviceProfiles() // Load all device profiles
+ d.saveDeviceProfile() // Save profile
+ d.getDeviceFirmware() // Firmware
+ d.setSoftwareMode() // Activate software mode
+ d.initLeds() // Init LED ports
+ d.setDeviceColor() // Device color
+ d.toggleDPI() // DPI
+ d.controlListener() // Control listener
+ d.setKeepAlive() // Keepalive
+ d.setAutoRefresh() // Set auto device refresh
+ logger.Log(logger.Fields{"serial": d.Serial, "product": d.Product}).Info("Device successfully initialized")
+ return d
+}
+
+// GetRgbProfiles will return RGB profiles for a target device
+func (d *Device) GetRgbProfiles() interface{} {
+ return d.Rgb
+}
+
+// Stop will stop all device operations and switch a device back to hardware mode
+func (d *Device) Stop() {
+ d.Exit = true
+ logger.Log(logger.Fields{"serial": d.Serial, "product": d.Product}).Info("Stopping device...")
+ if d.activeRgb != nil {
+ d.activeRgb.Stop()
+ }
+
+ d.timerKeepAlive.Stop()
+ d.timer.Stop()
+ var once sync.Once
+ go func() {
+ once.Do(func() {
+ if d.keepAliveChan != nil {
+ close(d.keepAliveChan)
+ }
+ if d.autoRefreshChan != nil {
+ close(d.autoRefreshChan)
+ }
+ })
+ }()
+
+ d.setHardwareMode()
+ if d.dev != nil {
+ err := d.dev.Close()
+ if err != nil {
+ return
+ }
+ }
+ logger.Log(logger.Fields{"serial": d.Serial, "product": d.Product}).Info("Device stopped")
+}
+
+// getManufacturer will return device manufacturer
+func (d *Device) getManufacturer() {
+ manufacturer, err := d.dev.GetMfrStr()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to get manufacturer")
+ }
+ d.Manufacturer = manufacturer
+}
+
+// getSerial will return device serial number
+func (d *Device) getSerial() {
+ serial, err := d.dev.GetSerialNbr()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to get device serial number")
+ }
+ d.Serial = serial
+}
+
+// loadRgb will load RGB file if found, or create the default.
+func (d *Device) loadRgb() {
+ rgbDirectory := pwd + "/database/rgb/"
+ rgbFilename := rgbDirectory + d.Serial + ".json"
+
+ // Check if filename has .json extension
+ if !common.IsValidExtension(rgbFilename, ".json") {
+ return
+ }
+
+ if !common.FileExists(rgbFilename) {
+ profile := rgb.GetRGB()
+ profile.Device = d.Product
+
+ // Convert to JSON
+ buffer, err := json.MarshalIndent(profile, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to encode RGB json")
+ return
+ }
+
+ // Create profile filename
+ file, err := os.Create(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to create RGB json file")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to write to RGB json file")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to close RGB json file")
+ return
+ }
+ }
+
+ file, err := os.Open(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to load RGB")
+ return
+ }
+ if err = json.NewDecoder(file).Decode(&d.Rgb); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to decode profile")
+ return
+ }
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"location": rgbFilename, "serial": d.Serial}).Warn("Failed to close file handle")
+ }
+}
+
+// GetRgbProfile will return rgb.Profile struct
+func (d *Device) GetRgbProfile(profile string) *rgb.Profile {
+ if d.Rgb == nil {
+ return nil
+ }
+
+ if val, ok := d.Rgb.Profiles[profile]; ok {
+ return &val
+ }
+ return nil
+}
+
+// GetDeviceTemplate will return device template name
+func (d *Device) GetDeviceTemplate() string {
+ return d.Template
+}
+
+// ChangeDeviceProfile will change device profile
+func (d *Device) ChangeDeviceProfile(profileName string) uint8 {
+ if profile, ok := d.UserProfiles[profileName]; ok {
+ currentProfile := d.DeviceProfile
+ currentProfile.Active = false
+ d.DeviceProfile = currentProfile
+ d.saveDeviceProfile()
+
+ // RGB reset
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+
+ newProfile := profile
+ newProfile.Active = true
+ d.DeviceProfile = newProfile
+ d.saveDeviceProfile()
+ d.setDeviceColor()
+ return 1
+ }
+ return 0
+}
+
+// saveRgbProfile will save rgb profile data
+func (d *Device) saveRgbProfile() {
+ rgbDirectory := pwd + "/database/rgb/"
+ rgbFilename := rgbDirectory + d.Serial + ".json"
+ if common.FileExists(rgbFilename) {
+ buffer, err := json.MarshalIndent(d.Rgb, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to encode RGB json")
+ return
+ }
+
+ // Create profile filename
+ file, err := os.Create(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to create RGB json file")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to write to RGB json file")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to close RGB json file")
+ return
+ }
+ }
+}
+
+// UpdateRgbProfileData will update RGB profile data
+func (d *Device) UpdateRgbProfileData(profileName string, profile rgb.Profile) uint8 {
+ if d.GetRgbProfile(profileName) == nil {
+ logger.Log(logger.Fields{"serial": d.Serial, "profile": profile}).Warn("Non-existing RGB profile")
+ return 0
+ }
+
+ pf := d.GetRgbProfile(profileName)
+ profile.StartColor.Brightness = pf.StartColor.Brightness
+ profile.EndColor.Brightness = pf.EndColor.Brightness
+ pf.StartColor = profile.StartColor
+ pf.EndColor = profile.EndColor
+ pf.Speed = profile.Speed
+
+ d.Rgb.Profiles[profileName] = *pf
+ d.saveRgbProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+}
+
+// UpdateRgbProfile will update device RGB profile
+func (d *Device) UpdateRgbProfile(_ int, profile string) uint8 {
+ if d.GetRgbProfile(profile) == nil {
+ logger.Log(logger.Fields{"serial": d.Serial, "profile": profile}).Warn("Non-existing RGB profile")
+ return 0
+ }
+ d.DeviceProfile.RGBProfile = profile // Set profile
+ d.saveDeviceProfile() // Save profile
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+}
+
+// ChangeDeviceBrightness will change device brightness
+func (d *Device) ChangeDeviceBrightness(mode uint8) uint8 {
+ d.DeviceProfile.Brightness = mode
+ d.saveDeviceProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+}
+
+// ChangeDeviceBrightnessValue will change device brightness via slider
+func (d *Device) ChangeDeviceBrightnessValue(value uint8) uint8 {
+ if value < 0 || value > 100 {
+ return 0
+ }
+
+ d.DeviceProfile.BrightnessSlider = &value
+ d.saveDeviceProfile()
+
+ if d.DeviceProfile.RGBProfile == "static" || d.DeviceProfile.RGBProfile == "mouse" {
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ }
+ return 1
+}
+
+// SchedulerBrightness will change device brightness via scheduler
+func (d *Device) SchedulerBrightness(value uint8) uint8 {
+ if value == 0 {
+ d.DeviceProfile.OriginalBrightness = *d.DeviceProfile.BrightnessSlider
+ d.DeviceProfile.BrightnessSlider = &value
+ } else {
+ d.DeviceProfile.BrightnessSlider = &d.DeviceProfile.OriginalBrightness
+ }
+
+ d.saveDeviceProfile()
+ if d.DeviceProfile.RGBProfile == "static" || d.DeviceProfile.RGBProfile == "mouse" {
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ }
+ return 1
+}
+
+// SaveUserProfile will generate a new user profile configuration and save it to a file
+func (d *Device) SaveUserProfile(profileName string) uint8 {
+ if d.DeviceProfile != nil {
+ profilePath := pwd + "/database/profiles/" + d.Serial + "-" + profileName + ".json"
+
+ newProfile := d.DeviceProfile
+ newProfile.Path = profilePath
+ newProfile.Active = false
+
+ buffer, err := json.Marshal(newProfile)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to convert to json format")
+ return 0
+ }
+
+ // Create profile filename
+ file, err := os.Create(profilePath)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to create new device profile")
+ return 0
+ }
+
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to write data")
+ return 0
+ }
+
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to close file handle")
+ return 0
+ }
+ d.loadDeviceProfiles()
+ return 1
+ }
+ return 0
+}
+
+// SaveMouseDPI will save mouse DPI
+func (d *Device) SaveMouseDPI(stages map[int]uint16) uint8 {
+ i := 0
+ if d.DeviceProfile == nil {
+ return 0
+ }
+
+ if len(stages) == 0 {
+ return 0
+ }
+
+ for key, stage := range stages {
+ if _, ok := d.DeviceProfile.Profiles[key]; ok {
+ profile := d.DeviceProfile.Profiles[key]
+ if stage > uint16(maxDpiValue) {
+ continue
+ }
+ if stage < uint16(minDpiValue) {
+ continue
+ }
+ profile.Value = stage
+ d.DeviceProfile.Profiles[key] = profile
+ i++
+ }
+ }
+
+ if i > 0 {
+ d.saveDeviceProfile()
+ d.toggleDPI()
+ return 1
+ }
+ return 0
+}
+
+// SaveMouseZoneColors will save mouse zone colors
+func (d *Device) SaveMouseZoneColors(dpi rgb.Color, zoneColors map[int]rgb.Color) uint8 {
+ i := 0
+ if d.DeviceProfile == nil {
+ return 0
+ }
+ if dpi.Red > 255 ||
+ dpi.Green > 255 ||
+ dpi.Blue > 255 ||
+ dpi.Red < 0 ||
+ dpi.Green < 0 ||
+ dpi.Blue < 0 {
+ return 0
+ }
+
+ // DPI
+ dpiColor := d.DeviceProfile.DPIColor
+ dpiColor.Red = dpi.Red
+ dpiColor.Green = dpi.Green
+ dpiColor.Blue = dpi.Blue
+ dpiColor.Hex = fmt.Sprintf("#%02x%02x%02x", int(dpi.Red), int(dpi.Green), int(dpi.Blue))
+ d.DeviceProfile.DPIColor = dpiColor
+
+ // Zone Colors
+ for key, zone := range zoneColors {
+ if zone.Red > 255 ||
+ zone.Green > 255 ||
+ zone.Blue > 255 ||
+ zone.Red < 0 ||
+ zone.Green < 0 ||
+ zone.Blue < 0 {
+ continue
+ }
+ if zoneColor, ok := d.DeviceProfile.ZoneColors[key]; ok {
+ zoneColor.Color.Red = zone.Red
+ zoneColor.Color.Green = zone.Green
+ zoneColor.Color.Blue = zone.Blue
+ zoneColor.Color.Hex = fmt.Sprintf("#%02x%02x%02x", int(zone.Red), int(zone.Green), int(zone.Blue))
+ }
+ i++
+ }
+
+ if i > 0 {
+ d.saveDeviceProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+ }
+ return 0
+}
+
+// getManufacturer will return device manufacturer
+func (d *Device) getDebugMode() {
+ d.Debug = config.GetConfig().Debug
+}
+
+// setHardwareMode will switch a device to hardware mode
+func (d *Device) setHardwareMode() {
+ _, err := d.transfer(cmdHardwareMode, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to change device mode")
+ }
+}
+
+// setSoftwareMode will switch a device to software mode
+func (d *Device) setSoftwareMode() {
+ _, err := d.transfer(cmdSoftwareMode, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to change device mode")
+ }
+}
+
+// GetSleepMode will return current sleep mode
+func (d *Device) GetSleepMode() int {
+ if d.DeviceProfile != nil {
+ return d.DeviceProfile.SleepMode
+ }
+ return 0
+}
+
+// getDeviceFirmware will return a device firmware version out as string
+func (d *Device) getDeviceFirmware() {
+ fw, err := d.transfer(
+ cmdGetFirmware,
+ nil,
+ )
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to write to a device")
+ }
+
+ v1, v2, v3 := int(fw[3]), int(fw[4]), int(binary.LittleEndian.Uint16(fw[5:7]))
+ d.Firmware = fmt.Sprintf("%d.%d.%d", v1, v2, v3)
+}
+
+// saveDeviceProfile will save device profile for persistent configuration
+func (d *Device) saveDeviceProfile() {
+ var defaultBrightness = uint8(100)
+ profilePath := pwd + "/database/profiles/" + d.Serial + ".json"
+
+ deviceProfile := &DeviceProfile{
+ Product: d.Product,
+ Serial: d.Serial,
+ Path: profilePath,
+ BrightnessSlider: &defaultBrightness,
+ OriginalBrightness: 100,
+ }
+
+ // First save, assign saved profile to a device
+ if d.DeviceProfile == nil {
+ // RGB, Label
+ deviceProfile.RGBProfile = "mouse"
+ deviceProfile.Label = "Mouse"
+ deviceProfile.Active = true
+ deviceProfile.ZoneColors = map[int]ZoneColors{
+ 0: { // Front left
+ ColorIndex: []int{0, 13, 26},
+ Color: &rgb.Color{
+ Red: 255,
+ Green: 0,
+ Blue: 0,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 255, 0, 0),
+ },
+ Name: "Front Left",
+ },
+ 1: { // Front right
+ ColorIndex: []int{1, 14, 27},
+ Color: &rgb.Color{
+ Red: 255,
+ Green: 0,
+ Blue: 0,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 255, 0, 0),
+ },
+ Name: "Front Right",
+ },
+ 2: { // Side Accent 1
+ ColorIndex: []int{3, 16, 29},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 1",
+ },
+ 3: { // Side Accent 2
+ ColorIndex: []int{4, 17, 30},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 2",
+ },
+ 4: { // Side Accent 3
+ ColorIndex: []int{5, 18, 31},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 3",
+ },
+ 5: { // Side Accent 4
+ ColorIndex: []int{6, 19, 32},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 4",
+ },
+ 6: { // Side Accent 5
+ ColorIndex: []int{7, 20, 33},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 5",
+ },
+ 7: { // Side Accent 6
+ ColorIndex: []int{8, 21, 34},
+ Color: &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ },
+ Name: "Side Accent 6",
+ },
+ 8: { // Logo
+ ColorIndex: []int{9, 22, 35},
+ Color: &rgb.Color{
+ Red: 255,
+ Green: 255,
+ Blue: 0,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 255, 255, 0),
+ },
+ Name: "Logo",
+ },
+ }
+ deviceProfile.DPIColor = &rgb.Color{
+ Red: 0,
+ Green: 255,
+ Blue: 255,
+ Brightness: 1,
+ Hex: fmt.Sprintf("#%02x%02x%02x", 0, 255, 255),
+ }
+ deviceProfile.Profiles = map[int]DPIProfile{
+ 0: {
+ Name: "Stage 1",
+ Value: 400,
+ ColorIndex: map[int][]int{
+ 0: {10, 23, 36},
+ },
+ },
+ 1: {
+ Name: "Stage 2",
+ Value: 800,
+ ColorIndex: map[int][]int{
+ 0: {10, 23, 36},
+ 1: {11, 24, 37},
+ },
+ },
+ 2: {
+ Name: "Stage 3",
+ Value: 1200,
+ ColorIndex: map[int][]int{
+ 0: {11, 24, 37},
+ },
+ },
+ 3: {
+ Name: "Stage 4",
+ Value: 1600,
+ ColorIndex: map[int][]int{
+ 0: {11, 24, 37},
+ 1: {12, 25, 38},
+ },
+ },
+ 4: {
+ Name: "Stage 5",
+ Value: 3200,
+ ColorIndex: map[int][]int{
+ 0: {12, 25, 38},
+ },
+ },
+ }
+ deviceProfile.Profile = 2
+ deviceProfile.SleepMode = 15
+ } else {
+ if d.DeviceProfile.BrightnessSlider == nil {
+ deviceProfile.BrightnessSlider = &defaultBrightness
+ d.DeviceProfile.BrightnessSlider = &defaultBrightness
+ } else {
+ deviceProfile.BrightnessSlider = d.DeviceProfile.BrightnessSlider
+ }
+ deviceProfile.Active = d.DeviceProfile.Active
+ deviceProfile.Brightness = d.DeviceProfile.Brightness
+ deviceProfile.OriginalBrightness = d.DeviceProfile.OriginalBrightness
+ deviceProfile.RGBProfile = d.DeviceProfile.RGBProfile
+ deviceProfile.Label = d.DeviceProfile.Label
+ deviceProfile.Profiles = d.DeviceProfile.Profiles
+ deviceProfile.Profile = d.DeviceProfile.Profile
+ deviceProfile.DPIColor = d.DeviceProfile.DPIColor
+ deviceProfile.ZoneColors = d.DeviceProfile.ZoneColors
+ deviceProfile.SleepMode = d.DeviceProfile.SleepMode
+
+ if len(d.DeviceProfile.Path) < 1 {
+ deviceProfile.Path = profilePath
+ d.DeviceProfile.Path = profilePath
+ } else {
+ deviceProfile.Path = d.DeviceProfile.Path
+ }
+ }
+
+ // Convert to JSON
+ buffer, err := json.MarshalIndent(deviceProfile, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to convert to json format")
+ return
+ }
+
+ // Create profile filename
+ file, fileErr := os.Create(deviceProfile.Path)
+ if fileErr != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Error("Unable to create new device profile")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Error("Unable to write data")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Error("Unable to close file handle")
+ }
+
+ d.loadDeviceProfiles() // Reload
+}
+
+// setCpuTemperature will store current CPU temperature
+func (d *Device) setTemperatures() {
+ d.CpuTemp = temperatures.GetCpuTemperature()
+ d.GpuTemp = temperatures.GetGpuTemperature()
+}
+
+// setAutoRefresh will refresh device data
+func (d *Device) setAutoRefresh() {
+ d.timer = time.NewTicker(time.Duration(deviceRefreshInterval) * time.Millisecond)
+ go func() {
+ for {
+ select {
+ case <-d.timer.C:
+ if d.Exit {
+ return
+ }
+ d.setTemperatures()
+ case <-d.autoRefreshChan:
+ d.timer.Stop()
+ return
+ }
+ }
+ }()
+}
+
+// UpdateSleepTimer will update device sleep timer
+func (d *Device) UpdateSleepTimer(minutes int) uint8 {
+ if d.DeviceProfile != nil {
+ d.DeviceProfile.SleepMode = minutes
+ d.saveDeviceProfile()
+ d.setSleepTimer()
+ return 1
+ }
+ return 0
+}
+
+// setSleepTimer will set device sleep timer
+func (d *Device) setSleepTimer() uint8 {
+ if d.DeviceProfile != nil {
+ changed := 0
+ _, err := d.transfer(cmdOpenWriteEndpoint, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Warn("Unable to change device sleep timer")
+ return 0
+ }
+
+ buf := make([]byte, 4)
+ sleep := d.DeviceProfile.SleepMode * (60 * 1000)
+ binary.LittleEndian.PutUint32(buf, uint32(sleep))
+
+ for i := 0; i < 2; i++ {
+ command := cmdSleep[i]
+ _, err = d.transfer(command, buf)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Warn("Unable to change device sleep timer")
+ continue
+ }
+ changed++
+ }
+
+ if changed > 0 {
+ return 1
+ }
+ }
+ return 0
+}
+
+// loadDeviceProfiles will load custom user profiles
+func (d *Device) loadDeviceProfiles() {
+ profileList := make(map[string]*DeviceProfile, 0)
+ userProfileDirectory := pwd + "/database/profiles/"
+
+ files, err := os.ReadDir(userProfileDirectory)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": userProfileDirectory, "serial": d.Serial}).Error("Unable to read content of a folder")
+ return
+ }
+
+ for _, fi := range files {
+ pf := &DeviceProfile{}
+ if fi.IsDir() {
+ continue // Exclude folders if any
+ }
+
+ // Define a full path of filename
+ profileLocation := userProfileDirectory + fi.Name()
+
+ // Check if filename has .json extension
+ if !common.IsValidExtension(profileLocation, ".json") {
+ continue
+ }
+
+ fileName := strings.Split(fi.Name(), ".")[0]
+ if m, _ := regexp.MatchString("^[a-zA-Z0-9-]+$", fileName); !m {
+ continue
+ }
+
+ fileSerial := ""
+ if strings.Contains(fileName, "-") {
+ fileSerial = strings.Split(fileName, "-")[0]
+ } else {
+ fileSerial = fileName
+ }
+
+ if fileSerial != d.Serial {
+ continue
+ }
+
+ file, err := os.Open(profileLocation)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": profileLocation}).Warn("Unable to load profile")
+ continue
+ }
+ if err = json.NewDecoder(file).Decode(pf); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": profileLocation}).Warn("Unable to decode profile")
+ continue
+ }
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"location": profileLocation, "serial": d.Serial}).Warn("Failed to close file handle")
+ }
+
+ if pf.Serial == d.Serial {
+ if fileName == d.Serial {
+ profileList["default"] = pf
+ } else {
+ name := strings.Split(fileName, "-")[1]
+ profileList[name] = pf
+ }
+ logger.Log(logger.Fields{"location": profileLocation, "serial": d.Serial}).Info("Loaded custom user profile")
+ }
+ }
+ d.UserProfiles = profileList
+ d.getDeviceProfile()
+}
+
+// getDeviceProfile will load persistent device configuration
+func (d *Device) getDeviceProfile() {
+ if len(d.UserProfiles) == 0 {
+ logger.Log(logger.Fields{"serial": d.Serial}).Warn("No profile found for device. Probably initial start")
+ } else {
+ for _, pf := range d.UserProfiles {
+ if pf.Active {
+ d.DeviceProfile = pf
+ }
+ }
+ }
+}
+
+// initLeds will initialize LED endpoint
+func (d *Device) initLeds() {
+ _, err := d.transfer(cmdOpenEndpoint, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to change device mode")
+ }
+}
+
+// setDeviceColor will activate and set device RGB
+func (d *Device) setDeviceColor() {
+ buf := make([]byte, d.LEDChannels*3)
+ if d.DeviceProfile == nil {
+ logger.Log(logger.Fields{"serial": d.Serial}).Error("Unable to set color. DeviceProfile is null!")
+ return
+ }
+
+ // DPI
+ dpiColor := d.DeviceProfile.DPIColor
+ dpiColor.Brightness = rgb.GetBrightnessValueFloat(*d.DeviceProfile.BrightnessSlider)
+ dpiColor = rgb.ModifyBrightness(*dpiColor)
+
+ dpiLeds := d.DeviceProfile.Profiles[d.DeviceProfile.Profile]
+ for i := 0; i < len(dpiLeds.ColorIndex); i++ {
+ dpiColorIndexRange := dpiLeds.ColorIndex[i]
+ for key, dpiColorIndex := range dpiColorIndexRange {
+ switch key {
+ case 0: // Red
+ buf[dpiColorIndex] = byte(dpiColor.Red)
+ case 1: // Green
+ buf[dpiColorIndex] = byte(dpiColor.Green)
+ case 2: // Blue
+ buf[dpiColorIndex] = byte(dpiColor.Blue)
+ }
+ }
+ }
+
+ if d.DeviceProfile.RGBProfile == "mouse" {
+ for _, zoneColor := range d.DeviceProfile.ZoneColors {
+ zoneColor.Color.Brightness = rgb.GetBrightnessValueFloat(*d.DeviceProfile.BrightnessSlider)
+ zoneColor.Color = rgb.ModifyBrightness(*zoneColor.Color)
+
+ zoneColorIndexRange := zoneColor.ColorIndex
+ for key, zoneColorIndex := range zoneColorIndexRange {
+ switch key {
+ case 0: // Red
+ buf[zoneColorIndex] = byte(zoneColor.Color.Red)
+ case 1: // Green
+ buf[zoneColorIndex] = byte(zoneColor.Color.Green)
+ case 2: // Blue
+ buf[zoneColorIndex] = byte(zoneColor.Color.Blue)
+ }
+ }
+ }
+ d.writeColor(buf)
+ return
+ }
+
+ if d.DeviceProfile.RGBProfile == "static" {
+ profile := d.GetRgbProfile("static")
+ if profile == nil {
+ return
+ }
+
+ profile.StartColor.Brightness = rgb.GetBrightnessValueFloat(*d.DeviceProfile.BrightnessSlider)
+ profileColor := rgb.ModifyBrightness(profile.StartColor)
+
+ for _, zoneColor := range d.DeviceProfile.ZoneColors {
+ zoneColorIndexRange := zoneColor.ColorIndex
+ for key, zoneColorIndex := range zoneColorIndexRange {
+ switch key {
+ case 0: // Red
+ buf[zoneColorIndex] = byte(profileColor.Red)
+ case 1: // Green
+ buf[zoneColorIndex] = byte(profileColor.Green)
+ case 2: // Blue
+ buf[zoneColorIndex] = byte(profileColor.Blue)
+ }
+ }
+ }
+ d.writeColor(buf)
+ return
+ }
+
+ go func(lightChannels int) {
+ startTime := time.Now()
+ d.activeRgb = rgb.Exit()
+
+ // Generate random colors
+ d.activeRgb.RGBStartColor = rgb.GenerateRandomColor(1)
+ d.activeRgb.RGBEndColor = rgb.GenerateRandomColor(1)
+
+ for {
+ select {
+ case <-d.activeRgb.Exit:
+ return
+ default:
+ buff := make([]byte, 0)
+ rgbCustomColor := true
+ profile := d.GetRgbProfile(d.DeviceProfile.RGBProfile)
+ if profile == nil {
+ for i := 0; i < d.ChangeableLedChannels*3; i++ {
+ buff = append(buff, []byte{0, 0, 0}...)
+ }
+ logger.Log(logger.Fields{"profile": d.DeviceProfile.RGBProfile, "serial": d.Serial}).Warn("No such RGB profile found")
+ continue
+ }
+ rgbModeSpeed := common.FClamp(profile.Speed, 0.1, 10)
+ // Check if we have custom colors
+ if (rgb.Color{}) == profile.StartColor || (rgb.Color{}) == profile.EndColor {
+ rgbCustomColor = false
+ }
+
+ r := rgb.New(
+ d.ChangeableLedChannels,
+ rgbModeSpeed,
+ nil,
+ nil,
+ profile.Brightness,
+ common.Clamp(profile.Smoothness, 1, 100),
+ time.Duration(rgbModeSpeed)*time.Second,
+ rgbCustomColor,
+ )
+
+ if rgbCustomColor {
+ r.RGBStartColor = &profile.StartColor
+ r.RGBEndColor = &profile.EndColor
+ } else {
+ r.RGBStartColor = d.activeRgb.RGBStartColor
+ r.RGBEndColor = d.activeRgb.RGBEndColor
+ }
+
+ // Brightness
+ r.RGBBrightness = rgb.GetBrightnessValueFloat(*d.DeviceProfile.BrightnessSlider)
+ r.RGBStartColor.Brightness = r.RGBBrightness
+ r.RGBEndColor.Brightness = r.RGBBrightness
+
+ switch d.DeviceProfile.RGBProfile {
+ case "off":
+ {
+ for n := 0; n < d.ChangeableLedChannels; n++ {
+ buff = append(buff, []byte{0, 0, 0}...)
+ }
+ }
+ case "rainbow":
+ {
+ r.Rainbow(startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "watercolor":
+ {
+ r.Watercolor(startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "cpu-temperature":
+ {
+ r.MinTemp = profile.MinTemp
+ r.MaxTemp = profile.MaxTemp
+ r.Temperature(float64(d.CpuTemp))
+ buff = append(buff, r.Output...)
+ }
+ case "gpu-temperature":
+ {
+ r.MinTemp = profile.MinTemp
+ r.MaxTemp = profile.MaxTemp
+ r.Temperature(float64(d.GpuTemp))
+ buff = append(buff, r.Output...)
+ }
+ case "colorpulse":
+ {
+ r.Colorpulse(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "static":
+ {
+ r.Static()
+ buff = append(buff, r.Output...)
+ }
+ case "rotator":
+ {
+ r.Rotator(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "wave":
+ {
+ r.Wave(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "storm":
+ {
+ r.Storm()
+ buff = append(buff, r.Output...)
+ }
+ case "flickering":
+ {
+ r.Flickering(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "colorshift":
+ {
+ r.Colorshift(&startTime, d.activeRgb)
+ buff = append(buff, r.Output...)
+ }
+ case "circleshift":
+ {
+ r.CircleShift(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "circle":
+ {
+ r.Circle(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "spinner":
+ {
+ r.Spinner(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "colorwarp":
+ {
+ r.Colorwarp(&startTime, d.activeRgb)
+ buff = append(buff, r.Output...)
+ }
+ }
+ m := 0
+ for _, zoneColor := range d.DeviceProfile.ZoneColors {
+ zoneColorIndexRange := zoneColor.ColorIndex
+ for _, zoneColorIndex := range zoneColorIndexRange {
+ buf[zoneColorIndex] = buff[m]
+ m++
+ }
+ }
+
+ d.writeColor(buf)
+ time.Sleep(40 * time.Millisecond)
+ }
+ }
+ }(d.ChangeableLedChannels)
+}
+
+func (d *Device) ModifyDpi(increment bool) {
+ if increment {
+ if d.DeviceProfile.Profile >= 4 {
+ return
+ }
+ d.DeviceProfile.Profile++
+ } else {
+ if d.DeviceProfile.Profile <= 0 {
+ return
+ }
+ d.DeviceProfile.Profile--
+ }
+
+ d.saveDeviceProfile()
+ d.toggleDPI()
+}
+
+// toggleDPI will change DPI mode
+func (d *Device) toggleDPI() {
+ if d.Exit {
+ return
+ }
+ if d.DeviceProfile != nil {
+ profile := d.DeviceProfile.Profiles[d.DeviceProfile.Profile]
+ value := profile.Value
+
+ // Send DPI packet
+ if value < uint16(minDpiValue) {
+ value = uint16(minDpiValue)
+ }
+ if value > uint16(maxDpiValue) {
+ value = uint16(maxDpiValue)
+ }
+
+ buf := make([]byte, 2)
+ binary.LittleEndian.PutUint16(buf[0:2], value)
+ _, err := d.transfer(cmdSetDpi, buf)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "vendorId": d.VendorId}).Error("Unable to set dpi")
+ }
+
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ }
+}
+
+// keepAlive will keep a device alive
+func (d *Device) keepAlive() {
+ if d.Exit {
+ return
+ }
+ _, err := d.transfer(cmdHeartbeat, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to a device")
+ }
+}
+
+// setKeepAlive will keep a device alive
+func (d *Device) setKeepAlive() {
+ d.timerKeepAlive = time.NewTicker(time.Duration(deviceKeepAlive) * time.Millisecond)
+ go func() {
+ for {
+ select {
+ case <-d.timerKeepAlive.C:
+ if d.Exit {
+ return
+ }
+ d.keepAlive()
+ case <-d.keepAliveChan:
+ d.timerKeepAlive.Stop()
+ return
+ }
+ }
+ }()
+}
+
+// writeColor will write data to the device with a specific endpoint.
+func (d *Device) writeColor(data []byte) {
+ if d.Exit {
+ return
+ }
+ buffer := make([]byte, len(data)+headerWriteSize)
+ binary.LittleEndian.PutUint16(buffer[0:2], uint16(len(data)))
+ copy(buffer[headerWriteSize:], data)
+
+ _, err := d.transfer(cmdWriteColor, buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+}
+
+// transfer will send data to a device and retrieve device output
+func (d *Device) transfer(endpoint, buffer []byte) ([]byte, error) {
+ // Packet control, mandatory for this device
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ // Create write buffer
+ bufferW := make([]byte, bufferSizeWrite)
+ bufferW[1] = 0x08
+ endpointHeaderPosition := bufferW[headerSize : headerSize+len(endpoint)]
+ copy(endpointHeaderPosition, endpoint)
+ if len(buffer) > 0 {
+ copy(bufferW[headerSize+len(endpoint):headerSize+len(endpoint)+len(buffer)], buffer)
+ }
+
+ // Create read buffer
+ bufferR := make([]byte, bufferSize)
+
+ // Send command to a device
+ if _, err := d.dev.Write(bufferW); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to a device")
+ return nil, err
+ }
+
+ // Get data from a device
+ if _, err := d.dev.Read(bufferR); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to read data from device")
+ return nil, err
+ }
+ return bufferR, nil
+}
+
+// getListenerData will listen for keyboard events and return data on success or nil on failure.
+// ReadWithTimeout is mandatory due to the nature of listening for events
+func (d *Device) getListenerData() []byte {
+ data := make([]byte, bufferSize)
+ n, err := d.listener.ReadWithTimeout(data, 100*time.Millisecond)
+ if err != nil || n == 0 {
+ return nil
+ }
+ return data
+}
+
+// controlListener will listen for events from the control buttons
+func (d *Device) controlListener() {
+ go func() {
+ enum := hid.EnumFunc(func(info *hid.DeviceInfo) error {
+ if info.InterfaceNbr == 2 {
+ listener, err := hid.OpenPath(info.Path)
+ if err != nil {
+ return err
+ }
+ d.listener = listener
+ }
+ return nil
+ })
+
+ err := hid.Enumerate(d.VendorId, d.ProductId, enum)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "vendorId": d.VendorId}).Error("Unable to enumerate devices")
+ }
+
+ for {
+ select {
+ default:
+ if d.Exit {
+ err = d.listener.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "vendorId": d.VendorId}).Error("Failed to close listener")
+ return
+ }
+ return
+ }
+
+ data := d.getListenerData()
+ if len(data) == 0 || data == nil {
+ continue
+ }
+
+ if data[1] == 0x02 && data[3] == 0x08 {
+ d.ModifyDpi(true)
+ } else if data[1] == 0x02 && data[3] == 0x10 {
+ d.ModifyDpi(false)
+ } else if data[1] == 0x02 && data[2] == 0x20 {
+ inputmanager.InputControl(inputmanager.Number1, d.Serial) // 1
+ } else if data[1] == 0x02 && data[2] == 0x40 {
+ inputmanager.InputControl(inputmanager.Number2, d.Serial) // 1
+ } else if data[1] == 0x02 && data[2] == 0x80 {
+ inputmanager.InputControl(inputmanager.Number3, d.Serial) // 1
+ } else if data[1] == 0x02 && data[3] == 0x01 {
+ inputmanager.InputControl(inputmanager.Number4, d.Serial) // 1
+ }
+ }
+ }
+ }()
+}
diff --git a/src/devices/devices.go b/src/devices/devices.go
index 91e1f19..4afcd1a 100644
--- a/src/devices/devices.go
+++ b/src/devices/devices.go
@@ -9,6 +9,8 @@ import (
"OpenLinkHub/src/devices/darkcorergbproWU"
"OpenLinkHub/src/devices/darkcorergbproseW"
"OpenLinkHub/src/devices/darkcorergbproseWU"
+ "OpenLinkHub/src/devices/darkstarW"
+ "OpenLinkHub/src/devices/darkstarWU"
"OpenLinkHub/src/devices/elite"
"OpenLinkHub/src/devices/harpoonW"
"OpenLinkHub/src/devices/harpoonWU"
@@ -25,6 +27,7 @@ import (
"OpenLinkHub/src/devices/k65plusW"
"OpenLinkHub/src/devices/k65pm"
"OpenLinkHub/src/devices/k70core"
+ "OpenLinkHub/src/devices/k70mk2"
"OpenLinkHub/src/devices/k70pro"
"OpenLinkHub/src/devices/katarpro"
"OpenLinkHub/src/devices/katarproW"
@@ -83,6 +86,7 @@ const (
productTypeK100Air = 107
productTypeK100AirW = 108
productTypeK100 = 109
+ productTypeK70MK2 = 110
productTypeKatarPro = 201
productTypeIronClawRgb = 202
productTypeIronClawRgbW = 203
@@ -108,6 +112,8 @@ const (
productTypeHarpoonRgbW = 223
productTypeHarpoonRgbWU = 224
productTypeKatarProXT = 225
+ productTypeDarkstarWU = 226
+ productTypeDarkstarW = 227
productTypeVirtuosoXTW = 300
productTypeVirtuosoXTWU = 301
productTypeST100 = 401
@@ -144,8 +150,8 @@ var (
interfaceId = 0
devices = make(map[string]*Device, 0)
products = make(map[string]Product, 0)
- keyboards = []uint16{7127, 7165, 7166, 7110, 7083, 11024, 11015, 7109, 7091, 7036, 7037}
- mouses = []uint16{7059, 7005, 6988, 7096, 7139, 7131, 11011, 7024, 7038, 7040, 7152, 7154, 7070, 7029, 7006, 7084}
+ keyboards = []uint16{7127, 7165, 7166, 7110, 7083, 11024, 11015, 7109, 7091, 7036, 7037, 6985, 6997}
+ mouses = []uint16{7059, 7005, 6988, 7096, 7139, 7131, 11011, 7024, 7038, 7040, 7152, 7154, 7070, 7029, 7006, 7084, 7090}
pads = []uint16{7067}
headsets = []uint16{2658, 2660}
dongles = []uint16{7132, 7078, 11008, 7060}
@@ -1408,6 +1414,23 @@ func Init() {
}
}(vendorId, productId, key)
}
+ case 6985, 6997, 7019: // K70 RGB MK2
+ {
+ go func(vendorId, productId uint16, key string) {
+ dev := k70mk2.Init(vendorId, productId, key)
+ if dev == nil {
+ return
+ }
+ devices[dev.Serial] = &Device{
+ ProductType: productTypeK70MK2,
+ Product: dev.Product,
+ Serial: dev.Serial,
+ Firmware: dev.Firmware,
+ Image: "icon-keyboard.svg",
+ Instance: dev,
+ }
+ }(vendorId, productId, key)
+ }
case 11024: // K65 PLUS USB
{
go func(vendorId, productId uint16, key string) {
@@ -1720,6 +1743,26 @@ func Init() {
}
dev.AddPairedDevice(value.ProductId, d)
}
+ case 7090: // CORSAIR DARKSTAR RGB WIRELESS Gaming Mouse
+ {
+ d := darkstarW.Init(
+ value.VendorId,
+ productId,
+ value.ProductId,
+ dev.GetDevice(),
+ value.Endpoint,
+ value.Serial,
+ )
+ devices[d.Serial] = &Device{
+ ProductType: productTypeDarkstarW,
+ Product: "DARKSTAR",
+ Serial: d.Serial,
+ Firmware: d.Firmware,
+ Image: "icon-mouse.svg",
+ Instance: d,
+ }
+ dev.AddPairedDevice(value.ProductId, d)
+ }
default:
logger.Log(logger.Fields{"productId": value.ProductId}).Warn("Unsupported device detected")
}
@@ -2094,7 +2137,24 @@ func Init() {
}
}(vendorId, productId, key)
}
- case 2658:
+ case 7090: // CORSAIR DARKSTAR RGB WIRELESS Gaming Mouse
+ {
+ go func(vendorId, productId uint16, key string) {
+ dev := darkstarWU.Init(vendorId, productId, key)
+ if dev == nil {
+ return
+ }
+ devices[dev.Serial] = &Device{
+ ProductType: productTypeDarkstarWU,
+ Product: dev.Product,
+ Serial: dev.Serial,
+ Firmware: dev.Firmware,
+ Image: "icon-mouse.svg",
+ Instance: dev,
+ }
+ }(vendorId, productId, key)
+ }
+ case 2658: // VIRTUOSO RGB WIRELESS XT
{
go func(vendorId, productId uint16, key string) {
dev := virtuosorgbXTWU.Init(vendorId, productId, key)
diff --git a/src/devices/k70mk2/k70mk2.go b/src/devices/k70mk2/k70mk2.go
new file mode 100644
index 0000000..f749eca
--- /dev/null
+++ b/src/devices/k70mk2/k70mk2.go
@@ -0,0 +1,1292 @@
+package k70mk2
+
+// Package: K70 MK2
+// This is the primary package for K70 MK2.
+// All device actions are controlled from this package.
+// Author: Nikola Jurkovic
+// License: GPL-3.0 or later
+
+import (
+ "OpenLinkHub/src/common"
+ "OpenLinkHub/src/config"
+ "OpenLinkHub/src/keyboards"
+ "OpenLinkHub/src/logger"
+ "OpenLinkHub/src/rgb"
+ "OpenLinkHub/src/temperatures"
+ "encoding/json"
+ "fmt"
+ "github.com/sstallion/go-hid"
+ "os"
+ "regexp"
+ "slices"
+ "strings"
+ "sync"
+ "time"
+)
+
+// DeviceProfile struct contains all device profile
+type DeviceProfile struct {
+ Active bool
+ Path string
+ Product string
+ Serial string
+ LCDMode uint8
+ LCDRotation uint8
+ RGBProfile string
+ Label string
+ Layout string
+ Keyboards map[string]*keyboards.Keyboard
+ Profile string
+ Profiles []string
+ BrightnessSlider uint8
+ OriginalBrightness uint8
+}
+
+type Device struct {
+ Debug bool
+ dev *hid.Device
+ listener *hid.Device
+ Manufacturer string `json:"manufacturer"`
+ Product string `json:"product"`
+ Serial string `json:"serial"`
+ Firmware string `json:"firmware"`
+ activeRgb *rgb.ActiveRGB
+ UserProfiles map[string]*DeviceProfile `json:"userProfiles"`
+ Devices map[int]string `json:"devices"`
+ DeviceProfile *DeviceProfile
+ OriginalProfile *DeviceProfile
+ Template string
+ VendorId uint16
+ ProductId uint16
+ Brightness map[int]string
+ LEDChannels int
+ CpuTemp float32
+ GpuTemp float32
+ Layouts []string
+ Rgb *rgb.RGB
+ Exit bool
+ timer *time.Ticker
+ autoRefreshChan chan struct{}
+ mutex sync.Mutex
+}
+
+var (
+ pwd = ""
+ cmdSoftwareMode = []byte{0x04, 0x02}
+ cmdHardwareMode = []byte{0x04, 0x01}
+ cmdGetFirmware = byte(0x01)
+ cmdWriteColor = byte(0x7f)
+ cmdWrite = byte(0x07)
+ cmdRead = byte(0x0e)
+ cmdActivateKey = byte(0x40)
+ deviceRefreshInterval = 1000
+ bufferSize = 64
+ bufferSizeWrite = bufferSize + 1
+ headerSize = 2
+ maxBufferSizePerRequest = 60
+ colorPacketLength = 168
+ keyboardKey = "k70mk2-default"
+ defaultLayout = "k70mk2-default-US"
+)
+
+func Init(vendorId, productId uint16, key string) *Device {
+ // Set global working directory
+ pwd = config.GetConfig().ConfigPath
+
+ dev, err := hid.OpenPath(key)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "vendorId": vendorId, "productId": productId}).Error("Unable to open HID device")
+ return nil
+ }
+
+ // Init new struct with HID device
+ d := &Device{
+ dev: dev,
+ Template: "k70mk2.html",
+ VendorId: vendorId,
+ ProductId: productId,
+ Brightness: map[int]string{
+ 0: "RGB Profile",
+ 1: "33 %",
+ 2: "66 %",
+ 3: "100 %",
+ },
+ Product: "K70 RGB MK2",
+ LEDChannels: 168,
+ Layouts: keyboards.GetLayouts(keyboardKey),
+ autoRefreshChan: make(chan struct{}),
+ listener: nil,
+ }
+
+ if d.ProductId == 7019 {
+ d.Product = "K70 RGB MK2 SE"
+ }
+
+ d.getDebugMode() // Debug mode
+ d.getManufacturer() // Manufacturer
+ d.getSerial() // Serial
+ d.loadRgb() // Load RGB
+ d.getDeviceFirmware() // Firmware
+ d.setSoftwareMode() // Activate software mode
+ d.loadDeviceProfiles() // Load all device profiles
+ d.saveDeviceProfile() // Save profile
+ d.setAutoRefresh() // Set auto device refresh
+ d.setupKeys() // Setup keys
+ d.setDeviceColor() // Device color
+ d.controlListener() // Control listener
+ logger.Log(logger.Fields{"serial": d.Serial, "product": d.Product}).Info("Device successfully initialized")
+ return d
+}
+
+// GetRgbProfiles will return RGB profiles for a target device
+func (d *Device) GetRgbProfiles() interface{} {
+ return d.Rgb
+}
+
+// Stop will stop all device operations and switch a device back to hardware mode
+func (d *Device) Stop() {
+ d.Exit = true
+ logger.Log(logger.Fields{"serial": d.Serial, "product": d.Product}).Info("Stopping device...")
+ if d.activeRgb != nil {
+ d.activeRgb.Stop()
+ }
+
+ d.timer.Stop()
+ var once sync.Once
+ go func() {
+ once.Do(func() {
+ if d.autoRefreshChan != nil {
+ close(d.autoRefreshChan)
+ }
+ })
+ }()
+
+ d.setHardwareMode()
+ if d.dev != nil {
+ err := d.dev.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to close HID device")
+ }
+ }
+ logger.Log(logger.Fields{"serial": d.Serial, "product": d.Product}).Info("Device stopped")
+}
+
+// loadRgb will load RGB file if found, or create the default.
+func (d *Device) loadRgb() {
+ rgbDirectory := pwd + "/database/rgb/"
+ rgbFilename := rgbDirectory + d.Serial + ".json"
+
+ // Check if filename has .json extension
+ if !common.IsValidExtension(rgbFilename, ".json") {
+ return
+ }
+
+ if !common.FileExists(rgbFilename) {
+ profile := rgb.GetRGB()
+ profile.Device = d.Product
+
+ // Convert to JSON
+ buffer, err := json.MarshalIndent(profile, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to encode RGB json")
+ return
+ }
+
+ // Create profile filename
+ file, err := os.Create(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to create RGB json file")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to write to RGB json file")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to close RGB json file")
+ return
+ }
+ }
+
+ file, err := os.Open(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to load RGB")
+ return
+ }
+ if err = json.NewDecoder(file).Decode(&d.Rgb); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to decode profile")
+ return
+ }
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"location": rgbFilename, "serial": d.Serial}).Warn("Failed to close file handle")
+ }
+}
+
+// GetRgbProfile will return rgb.Profile struct
+func (d *Device) GetRgbProfile(profile string) *rgb.Profile {
+ if d.Rgb == nil {
+ return nil
+ }
+
+ if val, ok := d.Rgb.Profiles[profile]; ok {
+ return &val
+ }
+ return nil
+}
+
+// GetDeviceTemplate will return device template name
+func (d *Device) GetDeviceTemplate() string {
+ return d.Template
+}
+
+// getManufacturer will return device manufacturer
+func (d *Device) getDebugMode() {
+ d.Debug = config.GetConfig().Debug
+}
+
+// getManufacturer will return device manufacturer
+func (d *Device) getManufacturer() {
+ manufacturer, err := d.dev.GetMfrStr()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to get manufacturer")
+ }
+ d.Manufacturer = manufacturer
+}
+
+// getProduct will return device name
+func (d *Device) getProduct() {
+ product, err := d.dev.GetProductStr()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to get product")
+ }
+ d.Product = product
+}
+
+// getSerial will return device serial number
+func (d *Device) getSerial() {
+ serial, err := d.dev.GetSerialNbr()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to get device serial number")
+ }
+ d.Serial = serial
+}
+
+// setHardwareMode will switch a device to hardware mode
+func (d *Device) setHardwareMode() {
+ err := d.transfer(cmdWrite, cmdHardwareMode, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to change device mode")
+ }
+}
+
+// setSoftwareMode will switch a device to software mode
+func (d *Device) setSoftwareMode() {
+ err := d.transfer(cmdWrite, cmdSoftwareMode, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to change device mode")
+ }
+}
+
+// setupKeys will initiate key setup for all keys
+func (d *Device) setupKeys() {
+ exclude := []byte{0x3f, 0x41, 0x42, 0x50, 0x53, 0x55, 0x6f, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81}
+ var buffer []byte
+ for b := byte(0x00); b <= 0x83; b++ {
+ if slices.Contains(exclude, b) {
+ continue
+ }
+
+ if b == 0x47 {
+ buffer = append(buffer, b, 0xc1)
+ } else {
+ buffer = append(buffer, b, 0xc0)
+ }
+ }
+
+ chunks := common.ProcessMultiChunkPacket(buffer, maxBufferSizePerRequest)
+ for _, chunk := range chunks {
+ buf := make([]byte, 3)
+ buf[0] = cmdActivateKey
+ buf[1] = byte(len(chunk) / 2)
+ buf[2] = 0x00
+ err := d.transfer(cmdWrite, buf, chunk)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+ }
+}
+
+// getDeviceFirmware will return a device firmware version out as string
+func (d *Device) getDeviceFirmware() {
+ buf := make([]byte, bufferSizeWrite)
+ buf[1] = cmdRead
+ buf[2] = cmdGetFirmware
+ n, err := d.dev.SendFeatureReport(buf)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to get temperature probe feature report")
+ return
+ }
+
+ n, err = d.dev.GetFeatureReport(buf[:n])
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to get temperature probe feature report")
+ return
+ }
+ buffer := buf[:n]
+ d.Firmware = fmt.Sprintf("%s.%s", fmt.Sprintf("%2x", buffer[10]), fmt.Sprintf("%2x", buffer[9]))
+}
+
+// saveDeviceProfile will save device profile for persistent configuration
+func (d *Device) saveDeviceProfile() {
+ var defaultBrightness = uint8(100)
+ profilePath := pwd + "/database/profiles/" + d.Serial + ".json"
+ keyboardMap := make(map[string]*keyboards.Keyboard, 0)
+
+ deviceProfile := &DeviceProfile{
+ Product: d.Product,
+ Serial: d.Serial,
+ Path: profilePath,
+ BrightnessSlider: defaultBrightness,
+ }
+
+ // First save, assign saved profile to a device
+ if d.DeviceProfile == nil {
+ // RGB, Label
+ deviceProfile.RGBProfile = "keyboard"
+ deviceProfile.Label = "Keyboard"
+ deviceProfile.Active = true
+ keyboardMap["default"] = keyboards.GetKeyboard(defaultLayout)
+ deviceProfile.Keyboards = keyboardMap
+ deviceProfile.Profile = "default"
+ deviceProfile.Profiles = []string{"default"}
+ deviceProfile.Layout = "US"
+ deviceProfile.BrightnessSlider = 100
+ } else {
+ if len(d.DeviceProfile.Layout) == 0 {
+ deviceProfile.Layout = "US"
+ } else {
+ deviceProfile.Layout = d.DeviceProfile.Layout
+ }
+
+ deviceProfile.Active = d.DeviceProfile.Active
+ deviceProfile.RGBProfile = d.DeviceProfile.RGBProfile
+ deviceProfile.Label = d.DeviceProfile.Label
+ deviceProfile.Profile = d.DeviceProfile.Profile
+ deviceProfile.Profiles = d.DeviceProfile.Profiles
+ deviceProfile.Keyboards = d.DeviceProfile.Keyboards
+ deviceProfile.BrightnessSlider = d.DeviceProfile.BrightnessSlider
+ if len(d.DeviceProfile.Path) < 1 {
+ deviceProfile.Path = profilePath
+ d.DeviceProfile.Path = profilePath
+ } else {
+ deviceProfile.Path = d.DeviceProfile.Path
+ }
+ deviceProfile.LCDMode = d.DeviceProfile.LCDMode
+ deviceProfile.LCDRotation = d.DeviceProfile.LCDRotation
+ }
+
+ // Convert to JSON
+ buffer, err := json.MarshalIndent(deviceProfile, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to convert to json format")
+ return
+ }
+
+ // Create profile filename
+ file, fileErr := os.Create(deviceProfile.Path)
+ if fileErr != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Error("Unable to create new device profile")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Error("Unable to write data")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": deviceProfile.Path}).Fatal("Unable to close file handle")
+ }
+
+ d.loadDeviceProfiles() // Reload
+}
+
+// loadDeviceProfiles will load custom user profiles
+func (d *Device) loadDeviceProfiles() {
+ profileList := make(map[string]*DeviceProfile, 0)
+ userProfileDirectory := pwd + "/database/profiles/"
+
+ files, err := os.ReadDir(userProfileDirectory)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": userProfileDirectory, "serial": d.Serial}).Fatal("Unable to read content of a folder")
+ }
+
+ for _, fi := range files {
+ pf := &DeviceProfile{}
+ if fi.IsDir() {
+ continue // Exclude folders if any
+ }
+
+ // Define a full path of filename
+ profileLocation := userProfileDirectory + fi.Name()
+
+ // Check if filename has .json extension
+ if !common.IsValidExtension(profileLocation, ".json") {
+ continue
+ }
+
+ fileName := strings.Split(fi.Name(), ".")[0]
+ if m, _ := regexp.MatchString("^[a-zA-Z0-9-]+$", fileName); !m {
+ continue
+ }
+
+ fileSerial := ""
+ if strings.Contains(fileName, "-") {
+ fileSerial = strings.Split(fileName, "-")[0]
+ } else {
+ fileSerial = fileName
+ }
+
+ if fileSerial != d.Serial {
+ continue
+ }
+
+ file, err := os.Open(profileLocation)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": profileLocation}).Warn("Unable to load profile")
+ continue
+ }
+ if err = json.NewDecoder(file).Decode(pf); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": profileLocation}).Warn("Unable to decode profile")
+ continue
+ }
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"location": profileLocation, "serial": d.Serial}).Warn("Failed to close file handle")
+ }
+
+ if pf.Serial == d.Serial {
+ if fileName == d.Serial {
+ profileList["default"] = pf
+ } else {
+ name := strings.Split(fileName, "-")[1]
+ profileList[name] = pf
+ }
+ logger.Log(logger.Fields{"location": profileLocation, "serial": d.Serial}).Info("Loaded custom user profile")
+ }
+ }
+ d.UserProfiles = profileList
+ d.getDeviceProfile()
+}
+
+// getDeviceProfile will load persistent device configuration
+func (d *Device) getDeviceProfile() {
+ if len(d.UserProfiles) == 0 {
+ logger.Log(logger.Fields{"serial": d.Serial}).Warn("No profile found for device. Probably initial start")
+ } else {
+ for _, pf := range d.UserProfiles {
+ if pf.Active {
+ d.DeviceProfile = pf
+ }
+ }
+ }
+}
+
+// setAutoRefresh will refresh device data
+func (d *Device) setAutoRefresh() {
+ d.timer = time.NewTicker(time.Duration(deviceRefreshInterval) * time.Millisecond)
+ go func() {
+ for {
+ select {
+ case <-d.timer.C:
+ if d.Exit {
+ return
+ }
+ d.setTemperatures()
+ case <-d.autoRefreshChan:
+ d.timer.Stop()
+ return
+ }
+ }
+ }()
+}
+
+// setCpuTemperature will store current CPU temperature
+func (d *Device) setTemperatures() {
+ d.CpuTemp = temperatures.GetCpuTemperature()
+ d.GpuTemp = temperatures.GetGpuTemperature()
+}
+
+// UpdateDeviceLabel will set / update device label
+func (d *Device) UpdateDeviceLabel(_ int, label string) uint8 {
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ d.DeviceProfile.Label = label
+ d.saveDeviceProfile()
+ return 1
+}
+
+// saveRgbProfile will save rgb profile data
+func (d *Device) saveRgbProfile() {
+ rgbDirectory := pwd + "/database/rgb/"
+ rgbFilename := rgbDirectory + d.Serial + ".json"
+ if common.FileExists(rgbFilename) {
+ buffer, err := json.MarshalIndent(d.Rgb, "", " ")
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to encode RGB json")
+ return
+ }
+
+ // Create profile filename
+ file, err := os.Create(rgbFilename)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to create RGB json file")
+ return
+ }
+
+ // Write JSON buffer to file
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to write to RGB json file")
+ return
+ }
+
+ // Close file
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial, "location": rgbFilename}).Warn("Unable to close RGB json file")
+ return
+ }
+ }
+}
+
+// UpdateRgbProfileData will update RGB profile data
+func (d *Device) UpdateRgbProfileData(profileName string, profile rgb.Profile) uint8 {
+ if d.GetRgbProfile(profileName) == nil {
+ logger.Log(logger.Fields{"serial": d.Serial, "profile": profile}).Warn("Non-existing RGB profile")
+ return 0
+ }
+
+ pf := d.GetRgbProfile(profileName)
+ profile.StartColor.Brightness = pf.StartColor.Brightness
+ profile.EndColor.Brightness = pf.EndColor.Brightness
+ pf.StartColor = profile.StartColor
+ pf.EndColor = profile.EndColor
+ pf.Speed = profile.Speed
+
+ d.Rgb.Profiles[profileName] = *pf
+ d.saveRgbProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+}
+
+// UpdateRgbProfile will update device RGB profile
+func (d *Device) UpdateRgbProfile(_ int, profile string) uint8 {
+ if d.GetRgbProfile(profile) == nil {
+ logger.Log(logger.Fields{"serial": d.Serial, "profile": profile}).Warn("Non-existing RGB profile")
+ return 0
+ }
+ d.DeviceProfile.RGBProfile = profile // Set profile
+ d.saveDeviceProfile() // Save profile
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+
+}
+
+// SchedulerBrightness will change device brightness via scheduler
+func (d *Device) SchedulerBrightness(value uint8) uint8 {
+ if value == 0 {
+ d.DeviceProfile.OriginalBrightness = d.DeviceProfile.BrightnessSlider
+ d.DeviceProfile.BrightnessSlider = value
+ } else {
+ d.DeviceProfile.BrightnessSlider = d.DeviceProfile.OriginalBrightness
+ }
+
+ d.saveDeviceProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+}
+
+// ChangeDeviceProfile will change device profile
+func (d *Device) ChangeDeviceProfile(profileName string) uint8 {
+ if profile, ok := d.UserProfiles[profileName]; ok {
+ currentProfile := d.DeviceProfile
+ currentProfile.Active = false
+ d.DeviceProfile = currentProfile
+ d.saveDeviceProfile()
+
+ // RGB reset
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+
+ newProfile := profile
+ newProfile.Active = true
+ d.DeviceProfile = newProfile
+ d.saveDeviceProfile()
+ d.setDeviceColor()
+ return 1
+ }
+ return 0
+}
+
+// ChangeKeyboardLayout will change keyboard layout
+func (d *Device) ChangeKeyboardLayout(layout string) uint8 {
+ layouts := keyboards.GetLayouts(keyboardKey)
+ if len(layouts) < 1 {
+ return 2
+ }
+
+ if slices.Contains(layouts, layout) {
+ if d.DeviceProfile != nil {
+ if _, ok := d.DeviceProfile.Keyboards["default"]; ok {
+ layoutKey := fmt.Sprintf("%s-%s", keyboardKey, layout)
+ keyboardLayout := keyboards.GetKeyboard(layoutKey)
+ if keyboardLayout == nil {
+ logger.Log(logger.Fields{"serial": d.Serial}).Warn("Trying to apply non-existing keyboard layout")
+ return 2
+ }
+
+ d.DeviceProfile.Keyboards["default"] = keyboardLayout
+ d.DeviceProfile.Layout = layout
+ d.saveDeviceProfile()
+ return 1
+ }
+ } else {
+ logger.Log(logger.Fields{"serial": d.Serial}).Warn("DeviceProfile is null")
+ return 0
+ }
+ } else {
+ logger.Log(logger.Fields{"serial": d.Serial}).Warn("No such layout")
+ return 2
+ }
+ return 0
+}
+
+// getCurrentKeyboard will return current active keyboard
+func (d *Device) getCurrentKeyboard() *keyboards.Keyboard {
+ if keyboard, ok := d.DeviceProfile.Keyboards[d.DeviceProfile.Profile]; ok {
+ return keyboard
+ }
+ return nil
+}
+
+// SaveDeviceProfile will save a new keyboard profile
+func (d *Device) SaveDeviceProfile(profileName string, new bool) uint8 {
+ if new {
+ if d.DeviceProfile == nil {
+ return 0
+ }
+
+ if slices.Contains(d.DeviceProfile.Profiles, profileName) {
+ return 2
+ }
+
+ if _, ok := d.DeviceProfile.Keyboards[profileName]; ok {
+ return 2
+ }
+
+ d.DeviceProfile.Profiles = append(d.DeviceProfile.Profiles, profileName)
+ d.DeviceProfile.Keyboards[profileName] = d.getCurrentKeyboard()
+ d.saveDeviceProfile()
+ return 1
+ } else {
+ d.saveDeviceProfile()
+ return 1
+ }
+}
+
+// UpdateKeyboardProfile will change keyboard profile
+func (d *Device) UpdateKeyboardProfile(profileName string) uint8 {
+ if d.DeviceProfile == nil {
+ return 0
+ }
+
+ if !slices.Contains(d.DeviceProfile.Profiles, profileName) {
+ return 2
+ }
+
+ if _, ok := d.DeviceProfile.Keyboards[profileName]; !ok {
+ return 2
+ }
+
+ d.DeviceProfile.Profile = profileName
+ d.saveDeviceProfile()
+ // RGB reset
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor()
+ return 1
+}
+
+// DeleteKeyboardProfile will delete keyboard profile
+func (d *Device) DeleteKeyboardProfile(profileName string) uint8 {
+ if d.DeviceProfile == nil {
+ return 0
+ }
+
+ if profileName == "default" {
+ return 3
+ }
+
+ if !slices.Contains(d.DeviceProfile.Profiles, profileName) {
+ return 2
+ }
+
+ if _, ok := d.DeviceProfile.Keyboards[profileName]; !ok {
+ return 2
+ }
+
+ index := common.IndexOfString(d.DeviceProfile.Profiles, profileName)
+ if index < 0 {
+ return 0
+ }
+
+ d.DeviceProfile.Profile = "default"
+ d.DeviceProfile.Profiles = append(d.DeviceProfile.Profiles[:index], d.DeviceProfile.Profiles[index+1:]...)
+ delete(d.DeviceProfile.Keyboards, profileName)
+
+ d.saveDeviceProfile()
+ // RGB reset
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor()
+ return 1
+}
+
+// SaveUserProfile will generate a new user profile configuration and save it to a file
+func (d *Device) SaveUserProfile(profileName string) uint8 {
+ if d.DeviceProfile != nil {
+ profilePath := pwd + "/database/profiles/" + d.Serial + "-" + profileName + ".json"
+
+ newProfile := d.DeviceProfile
+ newProfile.Path = profilePath
+ newProfile.Active = false
+
+ buffer, err := json.Marshal(newProfile)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Error("Unable to convert to json format")
+ return 0
+ }
+
+ // Create profile filename
+ file, err := os.Create(profilePath)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to create new device profile")
+ return 0
+ }
+
+ _, err = file.Write(buffer)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to write data")
+ return 0
+ }
+
+ err = file.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "location": newProfile.Path}).Error("Unable to close file handle")
+ return 0
+ }
+ d.loadDeviceProfiles()
+ return 1
+ }
+ return 0
+}
+
+// UpdateDeviceColor will update device color based on selected input
+func (d *Device) UpdateDeviceColor(keyId, keyOption int, color rgb.Color) uint8 {
+ switch keyOption {
+ case 0:
+ {
+ for rowIndex, row := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
+ for keyIndex, key := range row.Keys {
+ if keyIndex == keyId {
+ key.Color = rgb.Color{
+ Red: color.Red,
+ Green: color.Green,
+ Blue: color.Blue,
+ Brightness: 0,
+ }
+ d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row[rowIndex].Keys[keyIndex] = key
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+ }
+ }
+ }
+ }
+ case 1:
+ {
+ rowId := -1
+ for rowIndex, row := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
+ for keyIndex := range row.Keys {
+ if keyIndex == keyId {
+ rowId = rowIndex
+ break
+ }
+ }
+ }
+
+ if rowId < 0 {
+ return 0
+ }
+
+ for keyIndex, key := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row[rowId].Keys {
+ key.Color = rgb.Color{
+ Red: color.Red,
+ Green: color.Green,
+ Blue: color.Blue,
+ Brightness: 0,
+ }
+ d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row[rowId].Keys[keyIndex] = key
+ }
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+ }
+ case 2:
+ {
+ for rowIndex, row := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
+ for keyIndex, key := range row.Keys {
+ key.Color = rgb.Color{
+ Red: color.Red,
+ Green: color.Green,
+ Blue: color.Blue,
+ Brightness: 0,
+ }
+ d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row[rowIndex].Keys[keyIndex] = key
+ }
+ }
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ return 1
+ }
+ }
+ return 0
+}
+
+// setDeviceColor will activate and set device RGB
+func (d *Device) setDeviceColor() {
+ var bufR = make([]byte, colorPacketLength)
+ var bufG = make([]byte, colorPacketLength)
+ var bufB = make([]byte, colorPacketLength)
+
+ if d.DeviceProfile == nil {
+ logger.Log(logger.Fields{"serial": d.Serial}).Error("Unable to set color. DeviceProfile is null!")
+ return
+ }
+
+ if d.DeviceProfile.RGBProfile == "keyboard" {
+ if _, ok := d.DeviceProfile.Keyboards[d.DeviceProfile.Profile]; ok {
+ for _, rows := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
+ for _, keys := range rows.Keys {
+ for _, packetIndex := range keys.PacketIndex {
+ color := &rgb.Color{
+ Red: keys.Color.Red,
+ Green: keys.Color.Green,
+ Blue: keys.Color.Blue,
+ Brightness: rgb.GetBrightnessValueFloat(d.DeviceProfile.BrightnessSlider),
+ Hex: "",
+ }
+ modify := rgb.ModifyBrightness(*color)
+ bufR[packetIndex] = byte(modify.Red)
+ bufG[packetIndex] = byte(modify.Green)
+ bufB[packetIndex] = byte(modify.Blue)
+ }
+ }
+ }
+ d.writeColor(bufR, bufG, bufB) // Write color once
+ return
+ } else {
+ logger.Log(logger.Fields{"serial": d.Serial}).Error("Unable to set color. Unknown keyboard")
+ return
+ }
+ }
+
+ if d.DeviceProfile.RGBProfile == "static" {
+ profile := d.GetRgbProfile("static")
+ profileColor := rgb.ModifyBrightness(profile.StartColor)
+ for _, rows := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
+ for _, keys := range rows.Keys {
+ for _, packetIndex := range keys.PacketIndex {
+ color := &rgb.Color{
+ Red: profileColor.Red,
+ Green: profileColor.Green,
+ Blue: profileColor.Blue,
+ Brightness: rgb.GetBrightnessValueFloat(d.DeviceProfile.BrightnessSlider),
+ Hex: "",
+ }
+ modify := rgb.ModifyBrightness(*color)
+
+ bufR[packetIndex] = byte(modify.Red)
+ bufG[packetIndex] = byte(modify.Green)
+ bufB[packetIndex] = byte(modify.Blue)
+ }
+ }
+ }
+ d.writeColor(bufR, bufG, bufB) // Write color once
+ return
+ }
+
+ go func(lightChannels int) {
+ startTime := time.Now()
+ d.activeRgb = rgb.Exit()
+
+ // Generate random colors
+ d.activeRgb.RGBStartColor = rgb.GenerateRandomColor(1)
+ d.activeRgb.RGBEndColor = rgb.GenerateRandomColor(1)
+
+ for {
+ select {
+ case <-d.activeRgb.Exit:
+ return
+ default:
+ buff := make([]byte, 0)
+
+ rgbCustomColor := true
+ profile := d.GetRgbProfile(d.DeviceProfile.RGBProfile)
+ if profile == nil {
+ for i := 0; i < d.LEDChannels; i++ {
+ buff = append(buff, []byte{0, 0, 0}...)
+ }
+ logger.Log(logger.Fields{"profile": d.DeviceProfile.RGBProfile, "serial": d.Serial}).Warn("No such RGB profile found")
+ continue
+ }
+ rgbModeSpeed := common.FClamp(profile.Speed, 0.1, 10)
+ // Check if we have custom colors
+ if (rgb.Color{}) == profile.StartColor || (rgb.Color{}) == profile.EndColor {
+ rgbCustomColor = false
+ }
+
+ r := rgb.New(
+ d.LEDChannels,
+ rgbModeSpeed,
+ nil,
+ nil,
+ profile.Brightness,
+ common.Clamp(profile.Smoothness, 1, 100),
+ time.Duration(rgbModeSpeed)*time.Second,
+ rgbCustomColor,
+ )
+
+ if rgbCustomColor {
+ r.RGBStartColor = &profile.StartColor
+ r.RGBEndColor = &profile.EndColor
+ } else {
+ r.RGBStartColor = d.activeRgb.RGBStartColor
+ r.RGBEndColor = d.activeRgb.RGBEndColor
+ }
+
+ // Brightness
+ r.RGBBrightness = rgb.GetBrightnessValueFloat(d.DeviceProfile.BrightnessSlider)
+ r.RGBStartColor.Brightness = r.RGBBrightness
+ r.RGBEndColor.Brightness = r.RGBBrightness
+
+ switch d.DeviceProfile.RGBProfile {
+ case "off":
+ {
+ for n := 0; n < d.LEDChannels; n++ {
+ buff = append(buff, []byte{0, 0, 0}...)
+ }
+ }
+ case "rainbow":
+ {
+ r.Rainbow(startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "watercolor":
+ {
+ r.Watercolor(startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "cpu-temperature":
+ {
+ r.MinTemp = profile.MinTemp
+ r.MaxTemp = profile.MaxTemp
+ r.Temperature(float64(d.CpuTemp))
+ buff = append(buff, r.Output...)
+ }
+ case "gpu-temperature":
+ {
+ r.MinTemp = profile.MinTemp
+ r.MaxTemp = profile.MaxTemp
+ r.Temperature(float64(d.GpuTemp))
+ buff = append(buff, r.Output...)
+ }
+ case "colorpulse":
+ {
+ r.Colorpulse(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "static":
+ {
+ r.Static()
+ buff = append(buff, r.Output...)
+ }
+ case "rotator":
+ {
+ r.Rotator(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "wave":
+ {
+ r.Wave(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "storm":
+ {
+ r.Storm()
+ buff = append(buff, r.Output...)
+ }
+ case "flickering":
+ {
+ r.Flickering(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "colorshift":
+ {
+ r.Colorshift(&startTime, d.activeRgb)
+ buff = append(buff, r.Output...)
+ }
+ case "circleshift":
+ {
+ r.CircleShift(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "circle":
+ {
+ r.Circle(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "spinner":
+ {
+ r.Spinner(&startTime)
+ buff = append(buff, r.Output...)
+ }
+ case "colorwarp":
+ {
+ r.Colorwarp(&startTime, d.activeRgb)
+ buff = append(buff, r.Output...)
+ }
+ }
+
+ packetLen := len(buff) / 3
+ colorR := make([]byte, packetLen)
+ colorG := make([]byte, packetLen)
+ colorB := make([]byte, packetLen)
+ m := 0
+
+ for i := 0; i < packetLen; i++ {
+ colorR[i] = buff[m]
+ m++
+ colorG[i] = buff[m]
+ m++
+ colorB[i] = buff[m]
+ m++
+ }
+
+ for _, rows := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
+ for _, keys := range rows.Keys {
+ for _, packetIndex := range keys.PacketIndex {
+ bufR[packetIndex] = colorR[packetIndex]
+ bufG[packetIndex] = colorG[packetIndex]
+ bufB[packetIndex] = colorB[packetIndex]
+ }
+ }
+ }
+
+ // Send it
+ d.writeColor(bufR, bufG, bufB)
+ time.Sleep(20 * time.Millisecond)
+ }
+ }
+ }(d.LEDChannels)
+}
+
+// writeColor will write data to the device with a specific endpoint.
+// writeColor does not require endpoint closing and opening like normal Write requires.
+// Endpoint is open only once. Once the endpoint is open, color can be sent continuously.
+func (d *Device) writeColor(dataR, dataG, dataB []byte) {
+ if d.Exit {
+ return
+ }
+
+ // Red
+ chunksR := common.ProcessMultiChunkPacket(dataR, maxBufferSizePerRequest)
+ for i, chunk := range chunksR {
+ buf := make([]byte, 3)
+ buf[0] = byte(i + 1)
+ buf[1] = byte(len(chunk))
+ buf[2] = 0x00
+ err := d.transfer(cmdWriteColor, buf, chunk)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+ }
+ err := d.transfer(cmdWrite, []byte{0x28, 0x01, 0x03, 0x02}, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+
+ // Green
+ chunksG := common.ProcessMultiChunkPacket(dataG, maxBufferSizePerRequest)
+ for i, chunk := range chunksG {
+ buf := make([]byte, 3)
+ buf[0] = byte(i + 1)
+ buf[1] = byte(len(chunk))
+ buf[2] = 0x00
+ err = d.transfer(cmdWriteColor, buf, chunk)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+ }
+ err = d.transfer(cmdWrite, []byte{0x28, 0x02, 0x03, 0x02}, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+
+ // Blue
+ chunksB := common.ProcessMultiChunkPacket(dataB, maxBufferSizePerRequest)
+ for i, chunk := range chunksB {
+ buf := make([]byte, 3)
+ buf[0] = byte(i + 1)
+ buf[1] = byte(len(chunk))
+ buf[2] = 0x00
+ err = d.transfer(cmdWriteColor, buf, chunk)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+ }
+ err = d.transfer(cmdWrite, []byte{0x28, 0x03, 0x03, 0x02}, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+}
+
+// transfer will send data to a device and retrieve device output
+func (d *Device) transfer(command byte, endpoint, buffer []byte) error {
+ // Packet control, mandatory for this device
+ d.mutex.Lock()
+ defer d.mutex.Unlock()
+
+ // Create write buffer
+ bufferW := make([]byte, bufferSizeWrite)
+ bufferW[1] = command
+ endpointHeaderPosition := bufferW[headerSize : headerSize+len(endpoint)]
+ copy(endpointHeaderPosition, endpoint)
+ if len(buffer) > 0 {
+ copy(bufferW[headerSize+len(endpoint):headerSize+len(endpoint)+len(buffer)], buffer)
+ }
+
+ // Send command to a device
+ if _, err := d.dev.Write(bufferW); err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to a device")
+ return err
+ }
+ return nil
+}
+
+// getListenerData will listen for keyboard events and return data on success or nil on failure.
+// ReadWithTimeout is mandatory due to the nature of listening for events
+func (d *Device) getListenerData() []byte {
+ data := make([]byte, bufferSize)
+ n, err := d.listener.ReadWithTimeout(data, 100*time.Millisecond)
+ if err != nil || n == 0 {
+ return nil
+ }
+ return data
+}
+
+// controlListener will listen for events from the control buttons
+func (d *Device) controlListener() {
+ go func() {
+ enum := hid.EnumFunc(func(info *hid.DeviceInfo) error {
+ if info.InterfaceNbr == 0 {
+ listener, err := hid.OpenPath(info.Path)
+ if err != nil {
+ return err
+ }
+ d.listener = listener
+ }
+ return nil
+ })
+
+ err := hid.Enumerate(d.VendorId, d.ProductId, enum)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "vendorId": d.VendorId}).Error("Unable to enumerate devices")
+ }
+
+ for {
+ select {
+ default:
+ if d.Exit {
+ err = d.listener.Close()
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "vendorId": d.VendorId}).Error("Failed to close listener")
+ return
+ }
+ return
+ }
+
+ data := d.getListenerData()
+ if len(data) == 0 || data == nil {
+ continue
+ }
+
+ if data[9] == 0x80 {
+ if d.DeviceProfile != nil {
+ if d.DeviceProfile.BrightnessSlider >= 99 {
+ d.DeviceProfile.BrightnessSlider = 0
+ } else {
+ d.DeviceProfile.BrightnessSlider += 33
+ }
+ d.saveDeviceProfile()
+ if d.activeRgb != nil {
+ d.activeRgb.Exit <- true // Exit current RGB mode
+ d.activeRgb = nil
+ }
+ d.setDeviceColor() // Restart RGB
+ }
+ }
+ }
+ }
+ }()
+}
diff --git a/src/devices/nightsabreW/nightsabreW.go b/src/devices/nightsabreW/nightsabreW.go
index 7ede33c..483e51e 100644
--- a/src/devices/nightsabreW/nightsabreW.go
+++ b/src/devices/nightsabreW/nightsabreW.go
@@ -1,6 +1,6 @@
package nightsabreW
-// Package: CORSAIR IRONCLAW RGB Wireless
+// Package: CORSAIR NIGHTSABRE RGB Wireless
// This is the primary package for CORSAIR IRONCLAW RGB Wireless.
// All device actions are controlled from this package.
// Author: Nikola Jurkovic
diff --git a/src/devices/nightsabreWU/nightsabreWU.go b/src/devices/nightsabreWU/nightsabreWU.go
index 8fe29c6..f3fceba 100644
--- a/src/devices/nightsabreWU/nightsabreWU.go
+++ b/src/devices/nightsabreWU/nightsabreWU.go
@@ -1,6 +1,6 @@
package nightsabreWU
-// Package: CORSAIR IRONCLAW RGB Wireless
+// Package: CORSAIR NIGHTSABRE RGB Wireless
// This is the primary package for CORSAIR IRONCLAW RGB Wireless.
// All device actions are controlled from this package.
// Author: Nikola Jurkovic
diff --git a/src/devices/slipstream/slipstream.go b/src/devices/slipstream/slipstream.go
index 0629180..69ed32b 100644
--- a/src/devices/slipstream/slipstream.go
+++ b/src/devices/slipstream/slipstream.go
@@ -4,6 +4,7 @@ import (
"OpenLinkHub/src/config"
"OpenLinkHub/src/devices/darkcorergbproW"
"OpenLinkHub/src/devices/darkcorergbproseW"
+ "OpenLinkHub/src/devices/darkstarW"
"OpenLinkHub/src/devices/harpoonW"
"OpenLinkHub/src/devices/ironclawW"
"OpenLinkHub/src/devices/k100airW"
@@ -176,6 +177,11 @@ func (d *Device) Stop() {
dev.StopInternal()
}
}
+ if dev, found := value.(*darkstarW.Device); found {
+ if dev.Connected {
+ dev.StopInternal()
+ }
+ }
}
d.setHardwareMode()
@@ -410,6 +416,11 @@ func (d *Device) setDeviceOnlineByProductId(productId uint16) {
device.Connect()
}
}
+ if device, found := dev.(*darkstarW.Device); found {
+ if !device.Connected {
+ device.Connect()
+ }
+ }
}
}
@@ -461,6 +472,11 @@ func (d *Device) setDevicesOffline() {
device.SetConnected(false)
}
}
+ if device, found := pairedDevice.(*darkstarW.Device); found {
+ if device.Connected {
+ device.SetConnected(false)
+ }
+ }
}
}
@@ -521,6 +537,11 @@ func (d *Device) setDeviceTypeOffline(deviceType int) {
device.SetConnected(false)
}
}
+ if device, found := pairedDevice.(*darkstarW.Device); found {
+ if device.Connected {
+ device.SetConnected(false)
+ }
+ }
}
break
}
@@ -584,6 +605,11 @@ func (d *Device) setDeviceOnline(deviceType int) {
device.Connect()
}
}
+ if device, found := pairedDevice.(*darkstarW.Device); found {
+ if !device.Connected {
+ device.Connect()
+ }
+ }
}
break
case 2:
@@ -634,6 +660,11 @@ func (d *Device) setDeviceOnline(deviceType int) {
device.Connect()
}
}
+ if device, found := pairedDevice.(*darkstarW.Device); found {
+ if !device.Connected {
+ device.Connect()
+ }
+ }
}
break
}
@@ -914,6 +945,21 @@ func (d *Device) controlListener() {
break
}
}
+ if dev, found := value.(*darkstarW.Device); found {
+ if data[1] == 0x02 && data[3] == 0x08 {
+ dev.ModifyDpi(true)
+ } else if data[1] == 0x02 && data[3] == 0x10 {
+ dev.ModifyDpi(false)
+ } else if data[1] == 0x02 && data[2] == 0x20 {
+ inputmanager.InputControl(inputmanager.Number1, d.Serial) // 1
+ } else if data[1] == 0x02 && data[2] == 0x40 {
+ inputmanager.InputControl(inputmanager.Number2, d.Serial) // 1
+ } else if data[1] == 0x02 && data[2] == 0x80 {
+ inputmanager.InputControl(inputmanager.Number3, d.Serial) // 1
+ } else if data[1] == 0x02 && data[3] == 0x01 {
+ inputmanager.InputControl(inputmanager.Number4, d.Serial) // 1
+ }
+ }
}
}
}
diff --git a/src/devices/virtuosorgbXTW/virtuosorgbXTW.go b/src/devices/virtuosorgbXTW/virtuosorgbXTW.go
index 787bdb4..641b25e 100644
--- a/src/devices/virtuosorgbXTW/virtuosorgbXTW.go
+++ b/src/devices/virtuosorgbXTW/virtuosorgbXTW.go
@@ -557,7 +557,7 @@ func (d *Device) saveDeviceProfile() {
// First save, assign saved profile to a device
if d.DeviceProfile == nil {
// RGB, Label
- deviceProfile.RGBProfile = "static"
+ deviceProfile.RGBProfile = "headset"
deviceProfile.Label = "Headset"
deviceProfile.Active = true
deviceProfile.ZoneColors = map[int]ZoneColors{
diff --git a/src/devices/virtuosorgbXTWU/virtuosorgbXTWU.go b/src/devices/virtuosorgbXTWU/virtuosorgbXTWU.go
index 12ce524..a31632f 100644
--- a/src/devices/virtuosorgbXTWU/virtuosorgbXTWU.go
+++ b/src/devices/virtuosorgbXTWU/virtuosorgbXTWU.go
@@ -568,7 +568,7 @@ func (d *Device) saveDeviceProfile() {
// First save, assign saved profile to a device
if d.DeviceProfile == nil {
// RGB, Label
- deviceProfile.RGBProfile = "static"
+ deviceProfile.RGBProfile = "headset"
deviceProfile.Label = "Headset"
deviceProfile.Active = true
deviceProfile.ZoneColors = map[int]ZoneColors{
diff --git a/src/templates/templates.go b/src/templates/templates.go
index 0bf54c3..0688fbd 100644
--- a/src/templates/templates.go
+++ b/src/templates/templates.go
@@ -57,6 +57,7 @@ func Init() {
"web/k65plusW.html",
"web/k70core.html",
"web/k70pro.html",
+ "web/k70mk2.html",
"web/k55core.html",
"web/k100.html",
"web/k100air.html",
@@ -93,6 +94,8 @@ func Init() {
"web/harpoonW.html",
"web/virtuosorgbXTWU.html",
"web/virtuosorgbXTW.html",
+ "web/darkstarWU.html",
+ "web/darkstarW.html",
"web/rgb.html",
"web/temperature.html",
"web/scheduler.html",
diff --git a/static/img/icons/icon-windows.svg b/static/img/icons/icon-windows.svg
new file mode 100644
index 0000000..95a735b
--- /dev/null
+++ b/static/img/icons/icon-windows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/darkstarW.html b/web/darkstarW.html
new file mode 100644
index 0000000..4e7bf54
--- /dev/null
+++ b/web/darkstarW.html
@@ -0,0 +1,210 @@
+
+
+{{ template "header" . }}
+
+
+
+ {{ $devs := .Devices }}
+ {{ $temperatures := .Temperatures }}
+ {{ $device := .Device }}
+ {{ $rgb := .Rgb }}
+ {{ $profile := $device.DeviceProfile.Profile }}
+ {{ $deviceProfile := .Device.DeviceProfile }}
+
+
+
+
+ {{ template "navigation" . }}
+
+
+
+
+
+
+
+ {{ if eq .Device.Connected false }}
+
+
+
+ {{ else }}
+
+
+
+
+
+
+ {{ range $key, $pf := $device.DeviceProfile.Profiles }}
+
+
+
+ {{ if eq $key $device.DeviceProfile.Profile }}
+ {{ $pf.Name }} *
+ {{ else }}
+ {{ $pf.Name }}
+ {{ end }}
+
+
+
+
+
+
+
+
+
+ {{ end }}
+
+ {{ if eq "mouse" $device.DeviceProfile.RGBProfile }}
+
+
+
+ {{ range $key, $zone := $device.DeviceProfile.ZoneColors }}
+
+
+
+
{{ $zone.Name }}
+
+
+
+
+
+
+ {{ end }}
+
+ {{ end }}
+
+
+
+
+
+
+ {{ if eq "mouse" $device.DeviceProfile.RGBProfile }}
+
+ {{ end }}
+
+
+
+
+ {{ end }}
+
+
+
+
+ {{ template "footer" . }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/darkstarWU.html b/web/darkstarWU.html
new file mode 100644
index 0000000..731b55e
--- /dev/null
+++ b/web/darkstarWU.html
@@ -0,0 +1,179 @@
+
+
+{{ template "header" . }}
+
+
+
+ {{ $devs := .Devices }}
+ {{ $temperatures := .Temperatures }}
+ {{ $device := .Device }}
+ {{ $rgb := .Rgb }}
+ {{ $profile := $device.DeviceProfile.Profile }}
+ {{ $deviceProfile := .Device.DeviceProfile }}
+
+
+
+
+ {{ template "navigation" . }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ range $key, $pf := $device.DeviceProfile.Profiles }}
+
+
+
+ {{ if eq $key $device.DeviceProfile.Profile }}
+ {{ $pf.Name }} *
+ {{ else }}
+ {{ $pf.Name }}
+ {{ end }}
+
+
+
+
+
+
+
+
+
+ {{ end }}
+
+ {{ if eq "mouse" $device.DeviceProfile.RGBProfile }}
+
+
+
+ {{ range $key, $zone := $device.DeviceProfile.ZoneColors }}
+
+
+
+
{{ $zone.Name }}
+
+
+
+
+
+
+ {{ end }}
+
+ {{ end }}
+
+
+
+
+
+
+ {{ if eq "mouse" $device.DeviceProfile.RGBProfile }}
+
+ {{ end }}
+
+
+
+
+
+
+
+
+ {{ template "footer" . }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/k70mk2.html b/web/k70mk2.html
new file mode 100644
index 0000000..9c8a991
--- /dev/null
+++ b/web/k70mk2.html
@@ -0,0 +1,181 @@
+
+
+{{ template "header" . }}
+
+
+
+ {{ $devs := .Devices }}
+ {{ $temperatures := .Temperatures }}
+ {{ $device := .Device }}
+ {{ $rgb := .Rgb }}
+ {{ $profile := $device.DeviceProfile.Profile }}
+ {{ $keyboard := index $device.DeviceProfile.Keyboards $profile }}
+
+
+
+ {{ template "navigation" . }}
+
+
+
+
+
+
+
+
+
+
+ {{ if eq "keyboard" $device.DeviceProfile.RGBProfile }}
+
+ {{ range $index, $keys := $keyboard.Row }}
+ {{ if eq $index 4 }}
+
+ {{ else if eq $index 6 }}
+
+ {{ else }}
+
+ {{ end }}
+ {{ range $index, $keys := .Keys }}
+
+
+ {{ if $keys.Svg }}
+
+ {{ else }}
+ {{ $keys.KeyName }}
+ {{ end }}
+
+
+ {{ end }}
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ end }}
+
+
+
+
+
+ {{ template "footer" . }}
+
+
+
+
+
+
+
+
\ No newline at end of file