diff --git a/README.md b/README.md
index 60556a2..4715ffd 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,7 @@ Open source Linux interface for iCUE LINK Hub and other Corsair AIOs, Hubs.
| K65 PRO MINI | `1b1c` | `1bd7` | |
| K70 CORE RGB | `1b1c` | `1bfd` | |
| K70 PRO RGB | `1b1c` | `1bc6` | |
+| K65 PLUS | `1b1c` | `2b10` | USB |
## Installation (automatic)
1. Download either .deb or .rpm package from the latest Release, depends on your Linux distribution
diff --git a/config.json b/config.json
index b6dd183..be0db60 100644
--- a/config.json
+++ b/config.json
@@ -10,5 +10,6 @@
"dbusMonitor": false,
"memory": false,
"memorySmBus": "i2c-0",
- "memoryType": 4
+ "memoryType": 4,
+ "exclude": []
}
\ No newline at end of file
diff --git a/database/keyboard/k65plus-eu.json b/database/keyboard/k65plus-eu.json
new file mode 100644
index 0000000..6b47d7a
--- /dev/null
+++ b/database/keyboard/k65plus-eu.json
@@ -0,0 +1,1085 @@
+{
+ "key": "k65plus-default",
+ "device": "K65 Plus Wireless",
+ "layout": "EU",
+ "rows": 6,
+ "row": {
+ "0": {
+ "keys": {
+ "1": {
+ "keyName": "ESC",
+ "width": 70,
+ "height": 70,
+ "left": 0,
+ "top": 0,
+ "packetIndex": [123],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "2": {
+ "keyName": "F1",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [174],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "3": {
+ "keyName": "F2",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [177],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "4": {
+ "keyName": "F3",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [180],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "5": {
+ "keyName": "F4",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [183],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "6": {
+ "keyName": "F5",
+ "width": 70,
+ "height": 70,
+ "left": 47,
+ "top": 0,
+ "packetIndex": [186],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "7": {
+ "keyName": "F6",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [189],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "8": {
+ "keyName": "F7",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [192],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "9": {
+ "keyName": "F8",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [195],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "10": {
+ "keyName": "F9",
+ "width": 70,
+ "height": 70,
+ "left": 47,
+ "top": 0,
+ "packetIndex": [198],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "11": {
+ "keyName": "F10",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [201],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "12": {
+ "keyName": "F11",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [204],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "13": {
+ "keyName": "F12",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [207],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "14": {
+ "keyName": "Delete",
+ "width": 70,
+ "height": 70,
+ "left": 20,
+ "top": 0,
+ "packetIndex": [228],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "1": {
+ "keys": {
+ "15": {
+ "keyName": "` ~",
+ "width": 70,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [159],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "16": {
+ "keyName": "1",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [90],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "17": {
+ "keyName": "2",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [93],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "18": {
+ "keyName": "3",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [96],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "19": {
+ "keyName": "4",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [99],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "20": {
+ "keyName": "5",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [102],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "21": {
+ "keyName": "6",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [105],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "22": {
+ "keyName": "7",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [108],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "23": {
+ "keyName": "8",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [111],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "24": {
+ "keyName": "9",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [114],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "25": {
+ "keyName": "0",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [117],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "26": {
+ "keyName": "-",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [135],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "27": {
+ "keyName": "=",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [138],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "28": {
+ "keyName": "Backspace",
+ "width": 140,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [126],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "29": {
+ "keyName": "Home",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [222],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "2": {
+ "keys": {
+ "30": {
+ "keyName": "Tab",
+ "width": 100,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [129],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "31": {
+ "keyName": "Q",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [60],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "32": {
+ "keyName": "W",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [78],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "33": {
+ "keyName": "E",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [24],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "34": {
+ "keyName": "R",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [63],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "35": {
+ "keyName": "T",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [69],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "36": {
+ "keyName": "Z",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [84],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "37": {
+ "keyName": "U",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [72],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "38": {
+ "keyName": "I",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [36],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "39": {
+ "keyName": "O",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [54],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "40": {
+ "keyName": "P",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [57],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "41": {
+ "keyName": "[ {",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [141],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "42": {
+ "keyName": "] }",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [144],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "43": {
+ "keyName": "\\ |",
+ "width": 110,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [147],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "44": {
+ "keyName": "PgUp",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [225],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "3": {
+ "keys": {
+ "45": {
+ "keyName": "Caps Lock",
+ "width": 120,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [171],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "46": {
+ "keyName": "A",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [12],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "47": {
+ "keyName": "S",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [66],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "48": {
+ "keyName": "D",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [21],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "49": {
+ "keyName": "F",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [27],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "50": {
+ "keyName": "G",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [30],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "51": {
+ "keyName": "H",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [33],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "52": {
+ "keyName": "J",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [39],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "53": {
+ "keyName": "K",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [42],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "54": {
+ "keyName": "L",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [45],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "55": {
+ "keyName": "; :",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [153],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "56": {
+ "keyName": "' ''",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [156],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "57": {
+ "keyName": "Enter",
+ "width": 175,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [120],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "58": {
+ "keyName": "PgDn",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [234],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "4": {
+ "keys": {
+ "59": {
+ "keyName": "Shift",
+ "width": 175,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [318],
+ "color": {
+ "red": 255,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "60": {
+ "keyName": "Y",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [87],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "61": {
+ "keyName": "X",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [81],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "62": {
+ "keyName": "C",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [18],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "63": {
+ "keyName": "V",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [75],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "64": {
+ "keyName": "B",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [15],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "65": {
+ "keyName": "N",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [51],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "66": {
+ "keyName": "M",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [48],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "67": {
+ "keyName": ", <",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [162],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "68": {
+ "keyName": ". >",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [165],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "69": {
+ "keyName": "/ ?",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [168],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "70": {
+ "keyName": "Shift",
+ "width": 120,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [330],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "71": {
+ "keyName": "↑",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [246],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "5": {
+ "keys": {
+ "72": {
+ "keyName": "Ctrl",
+ "width": 90,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [315],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "73": {
+ "keyName": "⊞",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [324],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "74": {
+ "keyName": "Alt",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [321],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "75": {
+ "keyName": "",
+ "width": 505,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [0, 3, 132],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "76": {
+ "keyName": "Alt",
+ "width": 60,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [333],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "77": {
+ "keyName": "Fn",
+ "width": 60,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [366],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "78": {
+ "keyName": "Ctrl",
+ "width": 60,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [327],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "79": {
+ "keyName": "←",
+ "width": 70,
+ "height": 70,
+ "left": 45,
+ "top": 15,
+ "packetIndex": [240],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "80": {
+ "keyName": "↓",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [243],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "81": {
+ "keyName": "→",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [237],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/database/keyboard/k65plus.json b/database/keyboard/k65plus.json
new file mode 100644
index 0000000..20c410b
--- /dev/null
+++ b/database/keyboard/k65plus.json
@@ -0,0 +1,1085 @@
+{
+ "key": "k65plus-default",
+ "device": "K65 Plus Wireless",
+ "layout": "US",
+ "rows": 6,
+ "row": {
+ "0": {
+ "keys": {
+ "1": {
+ "keyName": "ESC",
+ "width": 70,
+ "height": 70,
+ "left": 0,
+ "top": 0,
+ "packetIndex": [123],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "2": {
+ "keyName": "F1",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [174],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "3": {
+ "keyName": "F2",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [177],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "4": {
+ "keyName": "F3",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [180],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "5": {
+ "keyName": "F4",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [183],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "6": {
+ "keyName": "F5",
+ "width": 70,
+ "height": 70,
+ "left": 47,
+ "top": 0,
+ "packetIndex": [186],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "7": {
+ "keyName": "F6",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [189],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "8": {
+ "keyName": "F7",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [192],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "9": {
+ "keyName": "F8",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [195],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "10": {
+ "keyName": "F9",
+ "width": 70,
+ "height": 70,
+ "left": 47,
+ "top": 0,
+ "packetIndex": [198],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "11": {
+ "keyName": "F10",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [201],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "12": {
+ "keyName": "F11",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [204],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "13": {
+ "keyName": "F12",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 0,
+ "packetIndex": [207],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "14": {
+ "keyName": "Delete",
+ "width": 70,
+ "height": 70,
+ "left": 20,
+ "top": 0,
+ "packetIndex": [228],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "1": {
+ "keys": {
+ "15": {
+ "keyName": "` ~",
+ "width": 70,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [159],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "16": {
+ "keyName": "1",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [90],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "17": {
+ "keyName": "2",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [93],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "18": {
+ "keyName": "3",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [96],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "19": {
+ "keyName": "4",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [99],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "20": {
+ "keyName": "5",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [102],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "21": {
+ "keyName": "6",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [105],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "22": {
+ "keyName": "7",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [108],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "23": {
+ "keyName": "8",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [111],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "24": {
+ "keyName": "9",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [114],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "25": {
+ "keyName": "0",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [117],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "26": {
+ "keyName": "-",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [135],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "27": {
+ "keyName": "=",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [138],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "28": {
+ "keyName": "Backspace",
+ "width": 140,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [126],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "29": {
+ "keyName": "Home",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [222],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "2": {
+ "keys": {
+ "30": {
+ "keyName": "Tab",
+ "width": 100,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [129],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "31": {
+ "keyName": "Q",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [60],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "32": {
+ "keyName": "W",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [78],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "33": {
+ "keyName": "E",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [24],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "34": {
+ "keyName": "R",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [63],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "35": {
+ "keyName": "T",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [69],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "36": {
+ "keyName": "Y",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [84],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "37": {
+ "keyName": "U",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [72],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "38": {
+ "keyName": "I",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [36],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "39": {
+ "keyName": "O",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [54],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "40": {
+ "keyName": "P",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [57],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "41": {
+ "keyName": "[ {",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [141],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "42": {
+ "keyName": "] }",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [144],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "43": {
+ "keyName": "\\ |",
+ "width": 110,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [147],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "44": {
+ "keyName": "PgUp",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [225],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "3": {
+ "keys": {
+ "45": {
+ "keyName": "Caps Lock",
+ "width": 120,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [171],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "46": {
+ "keyName": "A",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [12],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "47": {
+ "keyName": "S",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [66],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "48": {
+ "keyName": "D",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [21],
+ "color": {
+ "red": 255,
+ "green": 0,
+ "blue": 0
+ }
+ },
+ "49": {
+ "keyName": "F",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [27],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "50": {
+ "keyName": "G",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [30],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "51": {
+ "keyName": "H",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [33],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "52": {
+ "keyName": "J",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [39],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "53": {
+ "keyName": "K",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [42],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "54": {
+ "keyName": "L",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [45],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "55": {
+ "keyName": "; :",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [153],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "56": {
+ "keyName": "' ''",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [156],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "57": {
+ "keyName": "Enter",
+ "width": 175,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [120],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "58": {
+ "keyName": "PgDn",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [234],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "4": {
+ "keys": {
+ "59": {
+ "keyName": "Shift",
+ "width": 175,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [318],
+ "color": {
+ "red": 255,
+ "green": 255,
+ "blue": 0
+ }
+ },
+ "60": {
+ "keyName": "Z",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [87],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "61": {
+ "keyName": "X",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [81],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "62": {
+ "keyName": "C",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [18],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "63": {
+ "keyName": "V",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [75],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "64": {
+ "keyName": "B",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [15],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "65": {
+ "keyName": "N",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [51],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "66": {
+ "keyName": "M",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [48],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "67": {
+ "keyName": ", <",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [162],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "68": {
+ "keyName": ". >",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [165],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "69": {
+ "keyName": "/ ?",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [168],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "70": {
+ "keyName": "Shift",
+ "width": 120,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [330],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "71": {
+ "keyName": "↑",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [246],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ },
+ "5": {
+ "keys": {
+ "72": {
+ "keyName": "Ctrl",
+ "width": 90,
+ "height": 70,
+ "left": 0,
+ "top": 15,
+ "packetIndex": [315],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "73": {
+ "keyName": "⊞",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [324],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "74": {
+ "keyName": "Alt",
+ "width": 90,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [321],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "75": {
+ "keyName": "",
+ "width": 505,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [0, 3, 132],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "76": {
+ "keyName": "Alt",
+ "width": 60,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [333],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "77": {
+ "keyName": "Fn",
+ "width": 60,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [366],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "78": {
+ "keyName": "Ctrl",
+ "width": 60,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [327],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "79": {
+ "keyName": "←",
+ "width": 70,
+ "height": 70,
+ "left": 45,
+ "top": 15,
+ "packetIndex": [240],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "80": {
+ "keyName": "↓",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [243],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ },
+ "81": {
+ "keyName": "→",
+ "width": 70,
+ "height": 70,
+ "left": 15,
+ "top": 15,
+ "packetIndex": [237],
+ "color": {
+ "red": 0,
+ "green": 255,
+ "blue": 255
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/common/common.go b/src/common/common.go
index 459c4b4..e316a78 100644
--- a/src/common/common.go
+++ b/src/common/common.go
@@ -1,8 +1,10 @@
package common
import (
+ "fmt"
"math"
"os"
+ "os/exec"
"path/filepath"
)
@@ -109,3 +111,21 @@ func IndexOfString(slice []string, target string) int {
}
return -1 // Return -1 if the target is not found
}
+
+// ChangeVolume will change the volume by the given percentage.
+func ChangeVolume(percent int, increases bool) error {
+ if increases {
+ return exec.Command("pactl", "set-sink-volume", "@DEFAULT_SINK@", fmt.Sprintf("+%d%%", percent)).Run()
+ } else {
+ return exec.Command("pactl", "set-sink-volume", "@DEFAULT_SINK@", fmt.Sprintf("-%d%%", percent)).Run()
+ }
+}
+
+// MuteSound mutes the default sink
+func MuteSound(mute bool) error {
+ if mute {
+ return exec.Command("pactl", "set-sink-mute", "@DEFAULT_SINK@", "1").Run()
+ } else {
+ return exec.Command("pactl", "set-sink-mute", "@DEFAULT_SINK@", "0").Run()
+ }
+}
diff --git a/src/config/config.go b/src/config/config.go
index d59c52f..0dd9b74 100755
--- a/src/config/config.go
+++ b/src/config/config.go
@@ -7,18 +7,19 @@ import (
)
type Configuration struct {
- Debug bool `json:"debug"`
- ListenPort int `json:"listenPort"`
- ListenAddress string `json:"listenAddress"`
- CPUSensorChip string `json:"cpuSensorChip"`
- Manual bool `json:"manual"`
- Frontend bool `json:"frontend"`
- RefreshOnStart bool `json:"refreshOnStart"`
- Metrics bool `json:"metrics"`
- DbusMonitor bool `json:"dbusMonitor"`
- Memory bool `json:"memory"`
- MemorySmBus string `json:"memorySmBus"`
- MemoryType int `json:"memoryType"`
+ Debug bool `json:"debug"`
+ ListenPort int `json:"listenPort"`
+ ListenAddress string `json:"listenAddress"`
+ CPUSensorChip string `json:"cpuSensorChip"`
+ Manual bool `json:"manual"`
+ Frontend bool `json:"frontend"`
+ RefreshOnStart bool `json:"refreshOnStart"`
+ Metrics bool `json:"metrics"`
+ DbusMonitor bool `json:"dbusMonitor"`
+ Memory bool `json:"memory"`
+ MemorySmBus string `json:"memorySmBus"`
+ MemoryType int `json:"memoryType"`
+ Exclude []uint16 `json:"exclude"`
ConfigPath string
}
diff --git a/src/devices/devices.go b/src/devices/devices.go
index d0beae0..03f278e 100644
--- a/src/devices/devices.go
+++ b/src/devices/devices.go
@@ -7,6 +7,7 @@ import (
"OpenLinkHub/src/devices/cpro"
"OpenLinkHub/src/devices/elite"
"OpenLinkHub/src/devices/k55core"
+ "OpenLinkHub/src/devices/k65plus"
"OpenLinkHub/src/devices/k65pm"
"OpenLinkHub/src/devices/k70core"
"OpenLinkHub/src/devices/k70pro"
@@ -38,6 +39,7 @@ const (
productTypeK70Core = 102
productTypeK55Core = 103
productTypeK70Pro = 104
+ productTypeK65Plus = 105
)
type AIOData struct {
@@ -64,6 +66,7 @@ type Device struct {
K70Core *k70core.Device `json:"k70core,omitempty"`
K55Core *k55core.Device `json:"k55core,omitempty"`
K70Pro *k70pro.Device `json:"k70pro,omitempty"`
+ K65Plus *k65plus.Device `json:"k65plus,omitempty"`
GetDevice interface{}
}
@@ -73,7 +76,7 @@ var (
interfaceId = 0
devices = make(map[string]*Device, 0)
products = make(map[string]uint16)
- keyboards = []uint16{7127, 7165, 7166, 7110}
+ keyboards = []uint16{7127, 7165, 7166, 7110, 11024}
)
// Stop will stop all active devices
@@ -158,6 +161,12 @@ func Stop() {
device.K70Pro.Stop()
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ device.K65Plus.Stop()
+ }
+ }
}
}
err := hid.Exit()
@@ -194,6 +203,12 @@ func UpdateKeyboardColor(deviceId string, keyId, keyOptions int, color rgb.Color
return device.K55Core.UpdateDeviceColor(keyId, keyOptions, color)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.UpdateDeviceColor(keyId, keyOptions, color)
+ }
+ }
}
}
return 0
@@ -359,6 +374,12 @@ func SaveKeyboardProfile(deviceId, profileName string, new bool) uint8 {
return device.K55Core.SaveKeyboardProfile(profileName, new)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.SaveKeyboardProfile(profileName, new)
+ }
+ }
}
}
return 0
@@ -392,6 +413,27 @@ func ChangeKeyboardLayout(deviceId, layout string) uint8 {
return device.K55Core.ChangeKeyboardLayout(layout)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.ChangeKeyboardLayout(layout)
+ }
+ }
+ }
+ }
+ return 0
+}
+
+// ChangeKeyboardControlDial will change keyboard control dial function
+func ChangeKeyboardControlDial(deviceId string, controlDial int) uint8 {
+ if device, ok := devices[deviceId]; ok {
+ switch device.ProductType {
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.UpdateControlDial(controlDial)
+ }
+ }
}
}
return 0
@@ -425,6 +467,12 @@ func ChangeKeyboardProfile(deviceId, profileName string) uint8 {
return device.K55Core.UpdateKeyboardProfile(profileName)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.UpdateKeyboardProfile(profileName)
+ }
+ }
}
}
return 0
@@ -458,6 +506,12 @@ func DeleteKeyboardProfile(deviceId, profileName string) uint8 {
return device.K55Core.DeleteKeyboardProfile(profileName)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.DeleteKeyboardProfile(profileName)
+ }
+ }
}
}
return 0
@@ -545,6 +599,12 @@ func SaveUserProfile(deviceId, profileName string) uint8 {
return device.K55Core.SaveUserProfile(profileName)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.SaveUserProfile(profileName)
+ }
+ }
}
}
return 0
@@ -647,6 +707,12 @@ func ChangeDeviceBrightness(deviceId string, mode uint8) uint8 {
return device.K55Core.ChangeDeviceBrightness(mode)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.ChangeDeviceBrightness(mode)
+ }
+ }
}
}
return 0
@@ -734,6 +800,12 @@ func ChangeUserProfile(deviceId, profileName string) uint8 {
return device.K55Core.ChangeDeviceProfile(profileName)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.ChangeDeviceProfile(profileName)
+ }
+ }
}
}
return 0
@@ -898,6 +970,12 @@ func UpdateDeviceLabel(deviceId string, channelId int, label string, deviceType
return device.K55Core.UpdateDeviceLabel(label)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.UpdateDeviceLabel(label)
+ }
+ }
}
}
return 0
@@ -1078,6 +1156,12 @@ func UpdateRgbProfile(deviceId string, channelId int, profile string) uint8 {
return device.K55Core.UpdateRgbProfile(profile)
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus.UpdateRgbProfile(profile)
+ }
+ }
}
}
return 0
@@ -1230,6 +1314,12 @@ func GetDevice(deviceId string) interface{} {
return device.K55Core
}
}
+ case productTypeK65Plus:
+ {
+ if device.K65Plus != nil {
+ return device.K65Plus
+ }
+ }
}
}
return nil
@@ -1279,6 +1369,11 @@ func Init() {
// USB-HID
for key, productId := range products {
+ if slices.Contains(config.GetConfig().Exclude, productId) {
+ logger.Log(logger.Fields{"productId": productId}).Warn("Product excluded via config.json")
+ continue
+ }
+
switch productId {
case 3135: // CORSAIR iCUE Link System Hub
{
@@ -1486,6 +1581,22 @@ func Init() {
}
}(vendorId, productId, key)
}
+ case 11024: // K65 PLUS USB
+ {
+ go func(vendorId, productId uint16, key string) {
+ dev := k65plus.Init(vendorId, productId, key)
+ if dev == nil {
+ return
+ }
+ devices[dev.Serial] = &Device{
+ K65Plus: dev,
+ ProductType: productTypeK65Plus,
+ Product: dev.Product,
+ Serial: dev.Serial,
+ Firmware: dev.Firmware,
+ }
+ }(vendorId, productId, key)
+ }
case 0: // Memory
{
go func(serialId string) {
diff --git a/src/devices/k55core/k55core.go b/src/devices/k55core/k55core.go
index 113696a..83ae091 100644
--- a/src/devices/k55core/k55core.go
+++ b/src/devices/k55core/k55core.go
@@ -727,6 +727,11 @@ func (d *Device) setDeviceColor() {
buffer = rgb.SetColor(reset)
d.writeColor(buffer)
+ 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" {
var buf = make([]byte, colorPacketLength)
diff --git a/src/devices/k65plus/k65plus.go b/src/devices/k65plus/k65plus.go
new file mode 100644
index 0000000..a0f8670
--- /dev/null
+++ b/src/devices/k65plus/k65plus.go
@@ -0,0 +1,1258 @@
+package k65plus
+
+import (
+ "OpenLinkHub/src/common"
+ "OpenLinkHub/src/config"
+ "OpenLinkHub/src/keyboards"
+ "OpenLinkHub/src/logger"
+ "OpenLinkHub/src/rgb"
+ "OpenLinkHub/src/temperatures"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "github.com/sstallion/go-hid"
+ "os"
+ "regexp"
+ "slices"
+ "strings"
+ "sync"
+ "time"
+)
+
+// Package: K65 Pro Mini
+// This is the primary package for K65 Pro Mini.
+// All device actions are controlled from this package.
+// Author: Nikola Jurkovic
+// License: GPL-3.0 or later
+
+// DeviceProfile struct contains all device profile
+type DeviceProfile struct {
+ Active bool
+ Path string
+ Product string
+ Serial string
+ LCDMode uint8
+ LCDRotation uint8
+ Brightness uint8
+ RGBProfile string
+ Label string
+ Layout string
+ Keyboards map[string]*keyboards.Keyboard
+ Profile string
+ Profiles []string
+ ControlDial int
+ BrightnessLevel uint16
+}
+
+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
+ Brightness map[int]string
+ LEDChannels int
+ CpuTemp float32
+ GpuTemp float32
+ Layouts []string
+ ProductId uint16
+ ControlDialOptions map[int]string
+}
+
+var (
+ pwd = ""
+ cmdSoftwareMode = []byte{0x01, 0x03, 0x00, 0x02}
+ cmdHardwareMode = []byte{0x01, 0x03, 0x00, 0x01}
+ cmdActivateLed = []byte{0x0d, 0x00, 0x22}
+ cmdBrightness = []byte{0x01, 0x02, 0x00}
+ cmdGetFirmware = []byte{0x02, 0x13}
+ dataTypeSetColor = []byte{0x12, 0x00}
+ dataTypeSubColor = []byte{0x07, 0x00}
+ cmdWriteColor = []byte{0x06, 0x00}
+ deviceRefreshInterval = 1000
+ deviceKeepAlive = 20000
+ timer = &time.Ticker{}
+ timerKeepAlive = &time.Ticker{}
+ authRefreshChan = make(chan bool)
+ keepAliveChan = make(chan bool)
+ mutex sync.Mutex
+ transferTimeout = 500
+ bufferSize = 64
+ bufferSizeWrite = bufferSize + 1
+ headerSize = 2
+ headerWriteSize = 4
+ maxBufferSizePerRequest = 61
+ colorPacketLength = 371
+ keyboardKey = "k65plus-default"
+ defaultLayout = "k65plus-default-US"
+)
+
+// Stop will stop all device operations and switch a device back to hardware mode
+func (d *Device) Stop() {
+ logger.Log(logger.Fields{"serial": d.Serial}).Info("Stopping device...")
+ if d.activeRgb != nil {
+ d.activeRgb.Stop()
+ }
+ timer.Stop()
+ authRefreshChan <- true
+
+ timerKeepAlive.Stop()
+ keepAliveChan <- true
+
+ 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")
+ }
+ }
+}
+
+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: "k65plus.html",
+ VendorId: vendorId,
+ ProductId: productId,
+ Brightness: map[int]string{
+ 0: "RGB Profile",
+ 1: "33 %",
+ 2: "66 %",
+ 3: "100 %",
+ },
+ Product: "K65 Plus Wireless",
+ LEDChannels: 123,
+ Layouts: keyboards.GetLayouts(keyboardKey),
+ ControlDialOptions: map[int]string{
+ 1: "Volume Control",
+ 2: "Brightness",
+ },
+ }
+
+ d.getDebugMode() // Debug mode
+ d.getManufacturer() // Manufacturer
+ d.getSerial() // Serial
+ d.setSoftwareMode() // Activate software mode
+ d.initLeds() // Init LED ports
+ d.getDeviceFirmware() // Firmware
+ d.loadDeviceProfiles() // Load all device profiles
+ d.saveDeviceProfile() // Save profile
+ d.setAutoRefresh() // Set auto device refresh
+ d.setKeepAlive() // Keepalive
+ d.setDeviceColor() // Device color
+ d.controlDialListener() // Control Dial
+ d.setBrightnessLevel() // Brightness
+ return d
+}
+
+// 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(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(cmdSoftwareMode, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to change device mode")
+ }
+}
+
+// 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}).Fatal("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)
+}
+
+// initLeds will initialize LED ports
+func (d *Device) initLeds() {
+ _, err := d.transfer(cmdActivateLed, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err}).Fatal("Unable to change device mode")
+ }
+ // We need to wait around 500 ms for physical ports to re-initialize
+ // After that we can grab any new connected / disconnected device values
+ time.Sleep(time.Duration(transferTimeout) * time.Millisecond)
+}
+
+// saveDeviceProfile will save device profile for persistent configuration
+func (d *Device) saveDeviceProfile() {
+ profilePath := pwd + "/database/profiles/" + d.Serial + ".json"
+ keyboardMap := make(map[string]*keyboards.Keyboard, 0)
+
+ deviceProfile := &DeviceProfile{
+ Product: d.Product,
+ Serial: d.Serial,
+ Path: profilePath,
+ }
+
+ // 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.ControlDial = 1
+ deviceProfile.BrightnessLevel = 1000
+ } else {
+ if len(d.DeviceProfile.Layout) == 0 {
+ deviceProfile.Layout = "US"
+ } else {
+ deviceProfile.Layout = d.DeviceProfile.Layout
+ }
+
+ deviceProfile.Active = d.DeviceProfile.Active
+ deviceProfile.Brightness = d.DeviceProfile.Brightness
+ 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.ControlDial = d.DeviceProfile.ControlDial
+ deviceProfile.BrightnessLevel = d.DeviceProfile.BrightnessLevel
+
+ 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
+ }
+
+ 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
+ }
+ }
+ }
+}
+
+// keepAlive will keep a device alive
+func (d *Device) keepAlive() {
+ _, err := d.transfer([]byte{0x12}, nil)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to a device")
+ }
+}
+
+// setAutoRefresh will refresh device data
+func (d *Device) setKeepAlive() {
+ timerKeepAlive = time.NewTicker(time.Duration(deviceKeepAlive) * time.Millisecond)
+ keepAliveChan = make(chan bool)
+ go func() {
+ for {
+ select {
+ case <-timerKeepAlive.C:
+ d.keepAlive()
+ case <-keepAliveChan:
+ timerKeepAlive.Stop()
+ return
+ }
+ }
+ }()
+}
+
+// setAutoRefresh will refresh device data
+func (d *Device) setAutoRefresh() {
+ timer = time.NewTicker(time.Duration(deviceRefreshInterval) * time.Millisecond)
+ authRefreshChan = make(chan bool)
+ go func() {
+ for {
+ select {
+ case <-timer.C:
+ d.setTemperatures()
+ case <-authRefreshChan:
+ 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(label string) uint8 {
+ mutex.Lock()
+ defer mutex.Unlock()
+
+ d.DeviceProfile.Label = label
+ d.saveDeviceProfile()
+ return 1
+}
+
+// UpdateRgbProfile will update device RGB profile
+func (d *Device) UpdateRgbProfile(profile string) uint8 {
+ if rgb.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
+}
+
+// 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}).Error("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
+}
+
+// SaveKeyboardProfile will save a new keyboard profile
+func (d *Device) SaveKeyboardProfile(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
+}
+
+// UpdateControlDial will update control dial function
+func (d *Device) UpdateControlDial(value int) uint8 {
+ d.DeviceProfile.ControlDial = value
+ d.saveDeviceProfile()
+ 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() {
+ // Reset
+ reset := map[int][]byte{}
+ var buffer []byte
+
+ // Reset all channels
+ color := &rgb.Color{
+ Red: 0,
+ Green: 0,
+ Blue: 0,
+ Brightness: 0,
+ }
+
+ for i := 0; i < d.LEDChannels; i++ {
+ reset[i] = []byte{
+ byte(color.Red),
+ byte(color.Green),
+ byte(color.Blue),
+ }
+ }
+
+ buffer = rgb.SetColor(reset)
+ d.writeColor(buffer)
+
+ 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" {
+ var buf = make([]byte, colorPacketLength)
+ 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 {
+ buf[packetIndex] = byte(keys.Color.Red)
+ buf[packetIndex+1] = byte(keys.Color.Green)
+ buf[packetIndex+2] = byte(keys.Color.Blue)
+ }
+ }
+ }
+ d.writeColor(buf) // 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 := rgb.GetRgbProfile("static")
+ if d.DeviceProfile.Brightness != 0 {
+ profile.StartColor.Brightness = rgb.GetBrightnessValue(d.DeviceProfile.Brightness)
+ }
+
+ profileColor := rgb.ModifyBrightness(profile.StartColor)
+ for i := 0; i < d.LEDChannels; i++ {
+ reset[i] = []byte{
+ byte(profileColor.Red),
+ byte(profileColor.Green),
+ byte(profileColor.Blue),
+ }
+ }
+ buffer = rgb.SetColor(reset)
+ d.writeColor(buffer) // Write color once
+ return
+ }
+
+ go func(lightChannels int) {
+ lock := sync.Mutex{}
+ startTime := time.Now()
+ reverse := false
+ counterColorpulse := 0
+ counterFlickering := 0
+ counterColorshift := 0
+ counterCircleshift := 0
+ counterCircle := 0
+ counterColorwarp := 0
+ counterSpinner := 0
+ counterCpuTemp := 0
+ counterGpuTemp := 0
+ var temperatureKeys *rgb.Color
+ colorwarpGeneratedReverse := false
+ d.activeRgb = rgb.Exit()
+
+ // Generate random colors
+ d.activeRgb.RGBStartColor = rgb.GenerateRandomColor(1)
+ d.activeRgb.RGBEndColor = rgb.GenerateRandomColor(1)
+
+ hue := 1
+ wavePosition := 0.0
+ for {
+ select {
+ case <-d.activeRgb.Exit:
+ return
+ default:
+ buff := make([]byte, 0)
+
+ rgbCustomColor := true
+ profile := rgb.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
+ if d.DeviceProfile.Brightness > 0 {
+ r.RGBBrightness = rgb.GetBrightnessValue(d.DeviceProfile.Brightness)
+ 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":
+ {
+ lock.Lock()
+ counterCpuTemp++
+ if counterCpuTemp >= r.Smoothness {
+ counterCpuTemp = 0
+ }
+
+ if temperatureKeys == nil {
+ temperatureKeys = r.RGBStartColor
+ }
+
+ r.MinTemp = profile.MinTemp
+ r.MaxTemp = profile.MaxTemp
+ res := r.Temperature(float64(d.CpuTemp), counterCpuTemp, temperatureKeys)
+ temperatureKeys = res
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ case "gpu-temperature":
+ {
+ lock.Lock()
+ counterGpuTemp++
+ if counterGpuTemp >= r.Smoothness {
+ counterGpuTemp = 0
+ }
+
+ if temperatureKeys == nil {
+ temperatureKeys = r.RGBStartColor
+ }
+
+ r.MinTemp = profile.MinTemp
+ r.MaxTemp = profile.MaxTemp
+ res := r.Temperature(float64(d.GpuTemp), counterGpuTemp, temperatureKeys)
+ temperatureKeys = res
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ case "colorpulse":
+ {
+ lock.Lock()
+ counterColorpulse++
+ if counterColorpulse >= r.Smoothness {
+ counterColorpulse = 0
+ }
+
+ r.Colorpulse(counterColorpulse)
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ case "static":
+ {
+ r.Static()
+ buff = append(buff, r.Output...)
+ }
+ case "rotator":
+ {
+ r.Rotator(hue)
+ buff = append(buff, r.Output...)
+ }
+ case "wave":
+ {
+ r.Wave(wavePosition)
+ buff = append(buff, r.Output...)
+ }
+ case "storm":
+ {
+ r.Storm()
+ buff = append(buff, r.Output...)
+ }
+ case "flickering":
+ {
+ lock.Lock()
+ if counterFlickering >= r.Smoothness {
+ counterFlickering = 0
+ } else {
+ counterFlickering++
+ }
+
+ r.Flickering(counterFlickering)
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ case "colorshift":
+ {
+ lock.Lock()
+ if counterColorshift >= r.Smoothness && !reverse {
+ counterColorshift = 0
+ reverse = true
+ } else if counterColorshift >= r.Smoothness && reverse {
+ counterColorshift = 0
+ reverse = false
+ }
+
+ r.Colorshift(counterColorshift, reverse)
+ counterColorshift++
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ case "circleshift":
+ {
+ lock.Lock()
+ if counterCircleshift >= lightChannels {
+ counterCircleshift = 0
+ } else {
+ counterCircleshift++
+ }
+
+ r.Circle(counterCircleshift)
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ case "circle":
+ {
+ lock.Lock()
+ if counterCircle >= lightChannels {
+ counterCircle = 0
+ } else {
+ counterCircle++
+ }
+
+ r.Circle(counterCircle)
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ case "spinner":
+ {
+ lock.Lock()
+ if counterSpinner >= lightChannels {
+ counterSpinner = 0
+ } else {
+ counterSpinner++
+ }
+ r.Spinner(counterSpinner)
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ case "colorwarp":
+ {
+ lock.Lock()
+ if counterColorwarp >= r.Smoothness {
+ if !colorwarpGeneratedReverse {
+ colorwarpGeneratedReverse = true
+ d.activeRgb.RGBStartColor = d.activeRgb.RGBEndColor
+ d.activeRgb.RGBEndColor = rgb.GenerateRandomColor(r.RGBBrightness)
+ }
+ counterColorwarp = 0
+ } else if counterColorwarp == 0 && colorwarpGeneratedReverse == true {
+ colorwarpGeneratedReverse = false
+ } else {
+ counterColorwarp++
+ }
+
+ r.Colorwarp(counterColorwarp, d.activeRgb.RGBStartColor, d.activeRgb.RGBEndColor)
+ lock.Unlock()
+ buff = append(buff, r.Output...)
+ }
+ }
+ // Send it
+ d.writeColor(buff)
+ time.Sleep(20 * time.Millisecond)
+ hue++
+ wavePosition += 0.2
+ }
+ }
+ }(d.LEDChannels)
+}
+
+// setBrightnessLevel will set global brightness level
+func (d *Device) setBrightnessLevel() {
+ if d.DeviceProfile != nil {
+ buf := make([]byte, 2)
+ binary.LittleEndian.PutUint16(buf[0:2], d.DeviceProfile.BrightnessLevel)
+ _, err := d.transfer(cmdBrightness, buf)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Warn("Unable to change brightness")
+ }
+ }
+}
+
+// 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(data []byte) {
+ buf := data
+ buf[3] = 0
+ buf[4] = 0
+ buf[5] = 0
+
+ buffer := make([]byte, len(dataTypeSetColor)+len(buf)+headerWriteSize)
+ binary.LittleEndian.PutUint16(buffer[0:2], uint16(len(buf)+2))
+ copy(buffer[headerWriteSize:headerWriteSize+len(dataTypeSetColor)], dataTypeSetColor)
+ copy(buffer[headerWriteSize+len(dataTypeSetColor):], buf)
+
+ // Split packet into chunks
+ chunks := common.ProcessMultiChunkPacket(buffer, maxBufferSizePerRequest)
+ for i, chunk := range chunks {
+ if i == 0 {
+ // Initial packet is using cmdWriteColor
+ _, err := d.transfer(cmdWriteColor, chunk)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to color endpoint")
+ }
+ } else {
+ // Chunks don't use cmdWriteColor, they use static dataTypeSubColor
+ _, err := d.transfer(dataTypeSubColor, chunk)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Error("Unable to write to 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
+ mutex.Lock()
+ defer 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
+}
+
+// controlDialListener will listen for events from the control dial
+func (d *Device) controlDialListener() {
+ pv := false
+ var brightness uint16 = 0
+
+ if d.DeviceProfile.BrightnessLevel == 0 {
+ brightness = 1000
+ } else {
+ brightness = d.DeviceProfile.BrightnessLevel
+ }
+
+ go func() {
+ buf := make([]byte, 2)
+ 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}).Fatal("Unable to enumerate devices")
+ }
+
+ // Listen loop
+ data := make([]byte, bufferSize)
+ for {
+ // Read data from the HID device
+ _, err := d.listener.Read(data)
+ if err != nil {
+ fmt.Println("Error reading from device:", err)
+ break
+ }
+ value := data[4]
+ switch d.DeviceProfile.ControlDial {
+ case 1:
+ {
+ if value == 0 && data[19] == 2 {
+ pv = pv != true
+ if e := common.MuteSound(pv); e != nil {
+ logger.Log(logger.Fields{"error": e, "serial": d.Serial}).Warn("Unable to change volume level")
+ }
+ } else {
+ increases := false
+ if value == 1 {
+ increases = true
+ }
+
+ if e := common.ChangeVolume(5, increases); e != nil {
+ logger.Log(logger.Fields{"error": e, "serial": d.Serial}).Warn("Unable to change volume level")
+ }
+ }
+ }
+ case 2:
+ {
+ if value == 0 && data[19] == 2 {
+ pv = pv != true
+ if pv {
+ brightness = 0
+ } else {
+ brightness = 1000
+ }
+ } else {
+ if value == 1 {
+ if brightness >= 1000 {
+ brightness = 1000
+ } else {
+ brightness += 100
+ }
+ } else {
+ if brightness <= 0 {
+ brightness = 0
+ } else {
+ brightness -= 100
+ }
+ }
+ }
+
+ if d.DeviceProfile != nil {
+ d.DeviceProfile.BrightnessLevel = brightness
+ d.saveDeviceProfile()
+
+ // Send it
+ binary.LittleEndian.PutUint16(buf[0:2], brightness)
+ _, err := d.transfer(cmdBrightness, buf)
+ if err != nil {
+ logger.Log(logger.Fields{"error": err, "serial": d.Serial}).Warn("Unable to change brightness")
+ }
+ }
+ }
+ }
+ time.Sleep(40 * time.Millisecond)
+ }
+ }()
+}
diff --git a/src/devices/k65pm/k65pm.go b/src/devices/k65pm/k65pm.go
index 373c6b1..b8a6fb5 100644
--- a/src/devices/k65pm/k65pm.go
+++ b/src/devices/k65pm/k65pm.go
@@ -722,19 +722,29 @@ func (d *Device) setDeviceColor() {
buffer = rgb.SetColor(reset)
d.writeColor(buffer)
+ 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" {
var buf = make([]byte, colorPacketLength)
- for _, rows := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
- for _, keys := range rows.Keys {
- for _, packetIndex := range keys.PacketIndex {
- buf[packetIndex] = byte(keys.Color.Red)
- buf[packetIndex+1] = byte(keys.Color.Green)
- buf[packetIndex+2] = byte(keys.Color.Blue)
+ 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 {
+ buf[packetIndex] = byte(keys.Color.Red)
+ buf[packetIndex+1] = byte(keys.Color.Green)
+ buf[packetIndex+2] = byte(keys.Color.Blue)
+ }
}
}
+ d.writeColor(buf) // Write color once
+ return
+ } else {
+ logger.Log(logger.Fields{"serial": d.Serial}).Error("Unable to set color. Unknown keyboard")
+ return
}
- d.writeColor(buf) // Write color once
- return
}
if d.DeviceProfile.RGBProfile == "static" {
diff --git a/src/devices/k70core/k70core.go b/src/devices/k70core/k70core.go
index 4ec0483..ed3a051 100644
--- a/src/devices/k70core/k70core.go
+++ b/src/devices/k70core/k70core.go
@@ -721,19 +721,29 @@ func (d *Device) setDeviceColor() {
buffer = rgb.SetColor(reset)
d.writeColor(buffer)
+ 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" {
var buf = make([]byte, colorPacketLength)
- for _, rows := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
- for _, keys := range rows.Keys {
- for _, packetIndex := range keys.PacketIndex {
- buf[packetIndex] = byte(keys.Color.Red)
- buf[packetIndex+1] = byte(keys.Color.Green)
- buf[packetIndex+2] = byte(keys.Color.Blue)
+ 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 {
+ buf[packetIndex] = byte(keys.Color.Red)
+ buf[packetIndex+1] = byte(keys.Color.Green)
+ buf[packetIndex+2] = byte(keys.Color.Blue)
+ }
}
}
+ d.writeColor(buf) // Write color once
+ return
+ } else {
+ logger.Log(logger.Fields{"serial": d.Serial}).Error("Unable to set color. Unknown keyboard")
+ return
}
- d.writeColor(buf) // Write color once
- return
}
if d.DeviceProfile.RGBProfile == "static" {
diff --git a/src/devices/k70pro/k70pro.go b/src/devices/k70pro/k70pro.go
index aed890a..81d4745 100644
--- a/src/devices/k70pro/k70pro.go
+++ b/src/devices/k70pro/k70pro.go
@@ -721,19 +721,29 @@ func (d *Device) setDeviceColor() {
buffer = rgb.SetColor(reset)
d.writeColor(buffer)
+ 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" {
var buf = make([]byte, colorPacketLength)
- for _, rows := range d.DeviceProfile.Keyboards[d.DeviceProfile.Profile].Row {
- for _, keys := range rows.Keys {
- for _, packetIndex := range keys.PacketIndex {
- buf[packetIndex] = byte(keys.Color.Red)
- buf[packetIndex+1] = byte(keys.Color.Green)
- buf[packetIndex+2] = byte(keys.Color.Blue)
+ 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 {
+ buf[packetIndex] = byte(keys.Color.Red)
+ buf[packetIndex+1] = byte(keys.Color.Green)
+ buf[packetIndex+2] = byte(keys.Color.Blue)
+ }
}
}
+ d.writeColor(buf) // Write color once
+ return
+ } else {
+ logger.Log(logger.Fields{"serial": d.Serial}).Error("Unable to set color. Unknown keyboard")
+ return
}
- d.writeColor(buf) // Write color once
- return
}
if d.DeviceProfile.RGBProfile == "static" {
diff --git a/src/server/requests/requests.go b/src/server/requests/requests.go
index 4f5444a..4ad339c 100755
--- a/src/server/requests/requests.go
+++ b/src/server/requests/requests.go
@@ -37,6 +37,7 @@ type Payload struct {
LcdSerial string `json:"lcdSerial"`
KeyboardProfileName string `json:"keyboardProfileName"`
KeyboardLayout string `json:"keyboardLayout"`
+ KeyboardControlDial int `json:"keyboardControlDial"`
Brightness uint8 `json:"brightness"`
Position int `json:"position"`
Direction int `json:"direction"`
@@ -551,6 +552,46 @@ func ProcessChangeKeyboardLayout(r *http.Request) *Payload {
return &Payload{Message: "Unable to change keyboard layout.", Code: http.StatusOK, Status: 0}
}
+// ProcessChangeControlDial will process POST request from a client for device control dial change
+func ProcessChangeControlDial(r *http.Request) *Payload {
+ req := &Payload{}
+ err := json.NewDecoder(r.Body).Decode(&req)
+ if err != nil {
+ logger.Log(map[string]interface{}{"error": err}).Error("Unable to decode JSON")
+ return &Payload{
+ Message: "Unable to validate your request. Please try again!",
+ Code: http.StatusOK,
+ Status: 0,
+ }
+ }
+
+ if req.KeyboardControlDial < 1 {
+ return &Payload{Message: "Invalid control dial option", Code: http.StatusOK, Status: 0}
+ }
+
+ if len(req.DeviceId) < 0 {
+ return &Payload{Message: "Non-existing device", Code: http.StatusOK, Status: 0}
+ }
+
+ if m, _ := regexp.MatchString("^[a-zA-Z0-9-]+$", req.DeviceId); !m {
+ return &Payload{Message: "Non-existing device", Code: http.StatusOK, Status: 0}
+ }
+
+ if devices.GetDevice(req.DeviceId) == nil {
+ return &Payload{Message: "Non-existing device", Code: http.StatusOK, Status: 0}
+ }
+
+ // Run it
+ status := devices.ChangeKeyboardControlDial(req.DeviceId, req.KeyboardControlDial)
+ switch status {
+ case 1:
+ return &Payload{Message: "Keyboard control dial successfully changed.", Code: http.StatusOK, Status: 1}
+ case 2:
+ return &Payload{Message: "Unable to change keyboard control dial. Please try again", Code: http.StatusOK, Status: 0}
+ }
+ return &Payload{Message: "Unable to change keyboard control dial.", Code: http.StatusOK, Status: 0}
+}
+
// ProcessDeleteKeyboardProfile will process DELETE request from a client for device profile deletion
func ProcessDeleteKeyboardProfile(r *http.Request) *Payload {
req := &Payload{}
diff --git a/src/server/server.go b/src/server/server.go
index fc46451..714c6ee 100644
--- a/src/server/server.go
+++ b/src/server/server.go
@@ -459,6 +459,17 @@ func changeKeyboardLayout(w http.ResponseWriter, r *http.Request) {
resp.Send(w)
}
+// changeControlDial handles keyboard control dial function change
+func changeControlDial(w http.ResponseWriter, r *http.Request) {
+ request := requests.ProcessChangeControlDial(r)
+ resp := &Response{
+ Code: request.Code,
+ Status: request.Status,
+ Message: request.Message,
+ }
+ resp.Send(w)
+}
+
// deleteKeyboardProfile handles deletion of keyboard profile
func deleteKeyboardProfile(w http.ResponseWriter, r *http.Request) {
request := requests.ProcessDeleteKeyboardProfile(r)
@@ -705,6 +716,8 @@ func setRoutes() *mux.Router {
HandlerFunc(deleteKeyboardProfile)
r.Methods(http.MethodPost).Path("/api/keyboard/layout").
HandlerFunc(changeKeyboardLayout)
+ r.Methods(http.MethodPost).Path("/api/keyboard/dial").
+ HandlerFunc(changeControlDial)
// Prometheus metrics
if config.GetConfig().Metrics {
diff --git a/src/templates/templates.go b/src/templates/templates.go
index e3970a3..8a28134 100644
--- a/src/templates/templates.go
+++ b/src/templates/templates.go
@@ -47,6 +47,7 @@ func Init() {
"web/xc7.html",
"web/memory.html",
"web/k65pm.html",
+ "web/k65plus.html",
"web/k70core.html",
"web/k70pro.html",
"web/k55core.html",
diff --git a/static/js/overview.js b/static/js/overview.js
index 16af50b..cac120e 100644
--- a/static/js/overview.js
+++ b/static/js/overview.js
@@ -1098,4 +1098,30 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
});
+
+ $('.controlDial').on('change', function () {
+ const deviceId = $("#deviceId").val();
+ const pf = {};
+ pf["deviceId"] = deviceId;
+ pf["keyboardControlDial"] = parseInt($(this).val());
+ const json = JSON.stringify(pf, null, 2);
+
+ $.ajax({
+ url: '/api/keyboard/dial',
+ type: 'POST',
+ data: json,
+ cache: false,
+ success: function(response) {
+ try {
+ if (response.status === 1) {
+ toast.success(response.message);
+ } else {
+ toast.warning(response.message);
+ }
+ } catch (err) {
+ toast.warning(response.message);
+ }
+ }
+ });
+ });
});
\ No newline at end of file
diff --git a/web/devices.html b/web/devices.html
index a145298..639d699 100644
--- a/web/devices.html
+++ b/web/devices.html
@@ -24,4 +24,6 @@
{{ template "k55core.html" . }}
{{ else if eq .Device.Template "k70pro.html" }}
{{ template "k70pro.html" . }}
+{{ else if eq .Device.Template "k65plus.html" }}
+{{ template "k65plus.html" . }}
{{ end }}
\ No newline at end of file
diff --git a/web/k65plus.html b/web/k65plus.html
new file mode 100644
index 0000000..05f6ce5
--- /dev/null
+++ b/web/k65plus.html
@@ -0,0 +1,258 @@
+
+
+
+
+
+ {{.Title}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $devs := .Devices }}
+ {{ $temperatures := .Temperatures }}
+ {{ $device := .Device }}
+ {{ $rgb := .Rgb }}
+ {{ $profile := $device.DeviceProfile.Profile }}
+ {{ $keyboard := index $device.DeviceProfile.Keyboards $profile }}
+
+
+
+
+
+ {{ range $d := $devs }}
+ {{ if eq $d.Serial $device.Serial }}
+
+ {{ else }}
+
+ {{ end }}
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ if eq "keyboard" $device.DeviceProfile.RGBProfile }}
+
+ {{ range $keyboard.Row }}
+
+ {{ range $index, $keys := .Keys }}
+
+ {{ end }}
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file