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

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

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

Layout

User Profile

Brightness

RGB Profile

Control Dial

Save Profile

+ + + + + + + + + + + +
+
+ {{ if eq "keyboard" $device.DeviceProfile.RGBProfile }} +
+ {{ range $keyboard.Row }} +
+ {{ range $index, $keys := .Keys }} +
+

{{ $keys.KeyName }}

+
+ {{ end }} +
+ {{ end }} +
+
+
+
+
+ + + + + + + +
+
+ + + + +
+
+ {{ end }} +
+
+
+
+
+ +
+
+ + + + + + \ No newline at end of file