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` |
ShowiCUE 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` |
ShowH100i 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` |
ShowExternal 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` |
ShowHD 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` |
Show2x 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` |
Show2x External RGB Hub
4x Temperature Probe
Any PWM Fan
| -| XC7 ELITE LCD CPU Water Block | `1b1c` | `0c42` |
ShowRGB 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` |
ShowiCUE 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` |
ShowH100i 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` |
ShowExternal 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` |
ShowHD 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` |
Show2x 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` |
Show2x External RGB Hub
4x Temperature Probe
Any PWM Fan
| +| XC7 ELITE LCD CPU Water Block | `0c42` |
ShowRGB 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 }} +
+
+
+ Device +
+
+ {{ .Device.Product }}
+

+ Firmware: {{ .Device.Firmware }} +

+
+
+ Device is not connected!
+
+
+
+ {{ else }} +
+
+
+ Device +
+
+ {{ .Device.Product }}
+

+ Firmware: {{ .Device.Firmware }} +

+
+
+
+ + + + + + + + + + + + + + + + +

User Profile

Brightness

RGB Profile

Sleep

Save Profile

+ + + + + + + + + +
+
+
+
+
+ {{ 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 }} +
+
+
+
+ DPI +
+ +
+
+
+
+ + {{ 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" . }} +
+
+ +
+
+
+
+
+
+
+ Device +
+
+ {{ .Device.Product }}
+

+ Firmware: {{ .Device.Firmware }} +

+
+
+
+ + + + + + + + + + + + + + +

User Profile

Brightness

RGB Profile

Save Profile

+ + + + + + + +
+
+
+
+
+ {{ 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 }} +
+
+
+
+ DPI +
+ +
+
+
+
+ + {{ 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" . }} +
+
+ +
+
+
+
+
+
+
+ Device +
+
+ {{ .Device.Product }}
+

+ Firmware: {{ .Device.Firmware }} +

+
+
+
+ + + + + + + + + + + + + + +

Layout

User Profile

RGB Profile

Save Profile

+ + + + + + + +
+
+ {{ 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 }} + Icon + {{ else }} + {{ $keys.KeyName }} + {{ end }} +

+
+ {{ end }} +
+ {{ end }} +
+
+
+
+
+ + + + + + + +
+
+ + + + +
+
+ {{ end }} +
+
+
+
+
+ {{ template "footer" . }} +
+
+ + + + + + \ No newline at end of file