From 8f1123ee77cc94d67046e85add34fed3b7d91b25 Mon Sep 17 00:00:00 2001 From: Ilya Zhuravlev Date: Wed, 14 Oct 2020 12:56:25 -0400 Subject: [PATCH] add keyboard rendering --- g60.json | 130 +++++++++++++++ plain60.json | 301 ++++++++++++++++++++++++++++++++++ src/main/python/enumerate.py | 7 + src/main/python/hid.py | 11 ++ src/main/python/kle_serial.py | 202 +++++++++++++++++++++++ src/main/python/main.py | 49 ++++-- src/main/python/util.py | 3 + 7 files changed, 685 insertions(+), 18 deletions(-) create mode 100644 g60.json create mode 100644 plain60.json create mode 100644 src/main/python/enumerate.py create mode 100644 src/main/python/hid.py create mode 100644 src/main/python/kle_serial.py create mode 100644 src/main/python/util.py diff --git a/g60.json b/g60.json new file mode 100644 index 0000000..b6c4cb9 --- /dev/null +++ b/g60.json @@ -0,0 +1,130 @@ +{ + "name": "g60", + "vendorId": "0xFEED", + "productId": "0x6464", + "lighting": "none", + "matrix": { + "rows": 5, + "cols": 15 + }, + "layouts": { + "keymap": [ + [ + "0,0", + "0,1", + "0,2", + "0,3", + "0,4", + "0,5", + "0,6", + "0,7", + "0,8", + "0,9", + "0,10", + "0,11", + "0,12", + "0,13", + "0,14" + ], + [ + { + "w": 1.5 + }, + "1,0", + "1,2", + "1,3", + "1,4", + "1,5", + "1,6", + "1,7", + "1,8", + "1,9", + "1,10", + "1,11", + "1,12", + "1,13", + { + "w": 1.5 + }, + "1,14" + ], + [ + { + "w": 1.75 + }, + "2,0", + "2,2", + "2,3", + "2,4", + "2,5", + "2,6", + "2,7", + "2,8", + "2,9", + "2,10", + "2,11", + "2,12", + { + "w": 2.25 + }, + "2,13" + ], + [ + { + "w": 1.25 + }, + "3,0", + "3,1", + "3,2", + "3,3", + "3,4", + "3,5", + "3,6", + "3,7", + "3,8", + "3,9", + "3,10", + "3,11", + { + "w": 1.75 + }, + "3,13", + "3,14" + ], + [ + { + "w": 1.25 + }, + "4,0", + { + "w": 1.25 + }, + "4,1", + { + "w": 1.25 + }, + "4,3", + { + "w": 6.25 + }, + "4,6", + { + "w": 1.25 + }, + "4,10", + { + "w": 1.25 + }, + "4,11", + { + "w": 1.25 + }, + "4,13", + { + "w": 1.25 + }, + "4,14" + ] + ] + } +} \ No newline at end of file diff --git a/plain60.json b/plain60.json new file mode 100644 index 0000000..ba3db32 --- /dev/null +++ b/plain60.json @@ -0,0 +1,301 @@ +{ + "name": "plain60", + "vendorId": "0x4705", + "productId": "0x0160", + "lighting": "none", + "matrix": { + "rows": 5, + "cols": 15 + }, + "layouts": { + "labels": [ + [ + "Enter Key", + "ANSI", + "ISO" + ], + "Split Left Shift", + "Split Right Shift", + "Split Backspace", + [ + "Bottom row", + "Default", + "Tsangan", + "WKL", + "HHKB" + ] + ], + "keymap": [ + [ + { + "x": 2.75, + "c": "#777777" + }, + "0,0", + { + "c": "#cccccc" + }, + "0,1", + "0,2", + "0,3", + "0,4", + "0,5", + "0,6", + "0,7", + "0,8", + "0,9", + "0,10", + "0,11", + "0,12", + { + "c": "#aaaaaa", + "w": 2 + }, + "0,14\n\n\n3,0", + { + "x": 0.5, + "c": "#cccccc" + }, + "0,13\n\n\n3,1", + "0,14\n\n\n3,1" + ], + [ + { + "x": 2.75, + "c": "#aaaaaa", + "w": 1.5 + }, + "1,0", + { + "c": "#cccccc" + }, + "1,1", + "1,2", + "1,3", + "1,4", + "1,5", + "1,6", + "1,7", + "1,8", + "1,9", + "1,10", + "1,11", + "1,12", + { + "w": 1.5 + }, + "1,13\n\n\n0,0", + { + "x": 1.5, + "c": "#777777", + "w": 1.25, + "h": 2, + "w2": 1.5, + "h2": 1, + "x2": -0.25 + }, + "2,13\n\n\n0,1" + ], + [ + { + "x": 2.75, + "c": "#aaaaaa", + "w": 1.75 + }, + "2,0", + { + "c": "#cccccc" + }, + "2,1", + "2,2", + "2,3", + "2,4", + "2,5", + "2,6", + "2,7", + "2,8", + "2,09", + "2,10", + "2,11", + { + "c": "#777777", + "w": 2.25 + }, + "2,13\n\n\n0,0", + { + "x": 0.5, + "c": "#cccccc" + }, + "2,12\n\n\n0,1" + ], + [ + { + "c": "#aaaaaa", + "w": 1.25 + }, + "3,0\n\n\n1,1", + { + "c": "#cccccc" + }, + "3,1\n\n\n1,1", + { + "x": 0.5, + "c": "#aaaaaa", + "w": 2.25 + }, + "3,0\n\n\n1,0", + { + "c": "#cccccc" + }, + "3,2", + "3,3", + "3,4", + "3,5", + "3,6", + "3,7", + "3,8", + "3,9", + "3,10", + "3,11", + { + "c": "#aaaaaa", + "w": 2.75 + }, + "3,12\n\n\n2,0", + { + "x": 0.5, + "w": 1.75 + }, + "3,12\n\n\n2,1", + "3,13\n\n\n2,1" + ], + [ + { + "x": 2.75, + "w": 1.25 + }, + "4,0\n\n\n4,0", + { + "w": 1.25 + }, + "4,1\n\n\n4,0", + { + "w": 1.25 + }, + "4,2\n\n\n4,0", + { + "c": "#cccccc", + "w": 6.25 + }, + "4,6\n\n\n4,0", + { + "c": "#aaaaaa", + "w": 1.25 + }, + "4,10\n\n\n4,0", + { + "w": 1.25 + }, + "4,11\n\n\n4,0", + { + "w": 1.25 + }, + "4,12\n\n\n4,0", + { + "w": 1.25 + }, + "4,13\n\n\n4,0" + ], + [ + { + "y": 0.25, + "x": 2.75, + "w": 1.5 + }, + "4,0\n\n\n4,1", + "4,1\n\n\n4,1", + { + "w": 1.5 + }, + "4,2\n\n\n4,1", + { + "c": "#cccccc", + "w": 7 + }, + "4,6\n\n\n4,1", + { + "c": "#aaaaaa", + "w": 1.5 + }, + "4,11\n\n\n4,1", + "4,12\n\n\n4,1", + { + "w": 1.5 + }, + "4,13\n\n\n4,1" + ], + [ + { + "x": 2.75, + "w": 1.5 + }, + "4,0\n\n\n4,2", + { + "d": true + }, + "\n\n\n4,2", + { + "w": 1.5 + }, + "4,2\n\n\n4,2", + { + "c": "#cccccc", + "w": 7 + }, + "4,6\n\n\n4,2", + { + "c": "#aaaaaa", + "w": 1.5 + }, + "4,11\n\n\n4,2", + { + "d": true + }, + "\n\n\n4,2", + { + "w": 1.5 + }, + "4,13\n\n\n4,2" + ], + [ + { + "x": 2.75, + "w": 1.5, + "d": true + }, + "\n\n\n4,3", + "4,1\n\n\n4,3", + { + "w": 1.5 + }, + "4,2\n\n\n4,3", + { + "c": "#cccccc", + "w": 7 + }, + "4,6\n\n\n4,3", + { + "c": "#aaaaaa", + "w": 1.5 + }, + "4,11\n\n\n4,3", + "4,12\n\n\n4,3", + { + "w": 1.5, + "d": true + }, + "\n\n\n4,3" + ] + ] + } +} \ No newline at end of file diff --git a/src/main/python/enumerate.py b/src/main/python/enumerate.py new file mode 100644 index 0000000..2a98738 --- /dev/null +++ b/src/main/python/enumerate.py @@ -0,0 +1,7 @@ +import hid + +VIAL_SERIAL_NUMBER_MAGIC = "vial:f64c2b3c" + +def find_vial_keyboards(): + for dev in hid.enumerate(): + print(dev) diff --git a/src/main/python/hid.py b/src/main/python/hid.py new file mode 100644 index 0000000..a1c2ca4 --- /dev/null +++ b/src/main/python/hid.py @@ -0,0 +1,11 @@ +REPORT_LEN = 32 + +def hid_send(dev, msg): + if len(report) > REPORT_LEN: + raise RuntimeError("report must be less than 64 bytes") + msg += b"\x00" * (REPORT_LEN - len(msg)) + + # add 00 at start for hidapi report id + dev.write(b"\x00" + report) + + return dev.read(REPORT_LEN) diff --git a/src/main/python/kle_serial.py b/src/main/python/kle_serial.py new file mode 100644 index 0000000..609f6c2 --- /dev/null +++ b/src/main/python/kle_serial.py @@ -0,0 +1,202 @@ +# Based on https://github.com/ijprest/kle-serial +import json +from copy import copy + + +class KeyDefaults: + + def __init__(self): + self.textColor = "#000000" + self.textSize = 3 + + +class Key: + + def __init__(self): + self.color = "#cccccc" + self.labels = [] + self.textColor = [None] * 12 + self.textSize = [] + self.default = KeyDefaults() + self.x = 0 + self.y = 0 + self.width = 1 + self.height = 1 + self.x2 = 0 + self.y2 = 0 + self.width2 = 1 + self.height2 = 1 + self.rotation_x = 0 + self.rotation_y = 0 + self.rotation_angle = 0 + self.decal = False + self.ghost = False + self.stepped = False + self.nub = False + self.profile = "" + self.sm = "" + self.sb = "" + self.st = "" + + +class KeyboardMetadata: + + def __init__(self): + self.author = "" + self.backcolor = "#eeeeee" + self.background = None + self.name = "" + self.notes = "" + self.radii = "" + self.switchBrand = "" + self.switchMount = "" + self.switchType = "" + + +class Keyboard: + + def __init__(self): + self.meta = KeyboardMetadata() + self.keys = [] + + +class Serial: + + labelMap = [ + # 0 1 2 3 4 5 6 7 8 9 10 11 # align flags + [ 0, 6, 2, 8, 9,11, 3, 5, 1, 4, 7,10], # 0 = no centering + [ 1, 7,-1,-1, 9,11, 4,-1,-1,-1,-1,10], # 1 = center x + [ 3,-1, 5,-1, 9,11,-1,-1, 4,-1,-1,10], # 2 = center y + [ 4,-1,-1,-1, 9,11,-1,-1,-1,-1,-1,10], # 3 = center x & y + [ 0, 6, 2, 8,10,-1, 3, 5, 1, 4, 7,-1], # 4 = center front (default) + [ 1, 7,-1,-1,10,-1, 4,-1,-1,-1,-1,-1], # 5 = center front & x + [ 3,-1, 5,-1,10,-1,-1,-1, 4,-1,-1,-1], # 6 = center front & y + [ 4,-1,-1,-1,10,-1,-1,-1,-1,-1,-1,-1], # 7 = center front & x & y + ] + + def reorderLabelsIn(self, labels, align): + ret = [None] * 12 + for i in range(len(labels)): + if labels[i]: + ret[self.labelMap[align][i]] = labels[i] + return ret + + def deserializeError(self, msg, data): + raise RuntimeError("Error: {} {}".format(msg, data)) + + def deserialize(self, rows): + current = Key() + kbd = Keyboard() + align = 4 + + for r in range(len(rows)): + if isinstance(rows[r], list): + for k in range(len(rows[r])): + item = rows[r][k] + if isinstance(item, str): + newKey = copy(current) + + # Calculate some generated values + newKey.width2 = current.width if newKey.width2 == 0 else current.width2 + newKey.height2 = current.height if newKey.height2 == 0 else current.height2 + newKey.labels = self.reorderLabelsIn(item.split("\n"), align) + newKey.textSize = self.reorderLabelsIn(newKey.textSize, align) + + # Clean up the data + for i in range(12): + if newKey.labels[i] is None: + newKey.textSize[i] = newKey.textColor[i] = None + if newKey.textSize[i] == newKey.default.textSize: + newKey.textSize[i] = None + if newKey.textColor[i] == newKey.default.textColor: + newKey.textColor[i] = None + + # Add the key! + kbd.keys.append(newKey) + + # Set up for the next key + current.x += current.width + current.width = current.height = 1 + current.x2 = current.y2 = current.width2 = current.height2 = 0 + current.nub = current.stepped = current.decal = False + else: + if k != 0 and ("r" in item or "rx" in item or "ry" in item): + self.deserializeError("rotation can only be specified on the first key in a row", item) + if "r" in item: + current.rotation_angle = item["r"] + if "rx" in item: + current.rotation_x = item["rx"] + if "ry" in item: + current.rotation_y = item["ry"] + if "a" in item: + align = item["a"] + if "f" in item: + current.default.textSize = item["f"] + current.textSize = [] + if "f2" in item: + for i in range(1, 12): + current.textSize[i] = item["f2"] + if "fa" in item: + current.textSize = item["fa"] + if "p" in item: + current.profile = item["p"] + if "c" in item: + current.color = item["c"] + if "t" in item: + split = item["t"].split("\n") + if split[0] != "": + current.default.textColor = split[0] + current.textColor = self.reorderLabelsIn(split, align) + if "x" in item: + current.x += item["x"] + if "y" in item: + current.y += item["y"] + if "w" in item: + current.width = current.width2 = item["w"] + if "h" in item: + current.height = current.height2 = item["h"] + if "x2" in item: + current.x2 = item["x2"] + if "y2" in item: + current.y2 = item["y2"] + if "w2" in item: + current.width2 = item["w2"] + if "h2" in item: + current.height2 = item["h2"] + if "n" in item: + current.nub = item["n"] + if "l" in item: + current.stepped = item["l"] + if "d" in item: + current.decal = item["d"] + if "g" in item and item["g"]: + current.ghost = item.g + if "sm" in item: + current.sm = item["sm"] + if "sb" in item: + current.sb = item["sb"] + if "st" in item: + current.st = item["st"] + + # End of the row + current.y += 1 + current.x = current.rotation_x + elif isinstance(item, dict): + if r != 0: + self.deserializeError("keyboard metadata must the be first element", rows[r]) + # TODO: parse prop + else: + self.deserializeError("unexpected", rows[r]) + return kbd + + +# TODO: add tests + + +# serial = Serial() +# data = open("plain60.json", "r").read() +# data = json.loads(data) +# kb = serial.deserialize(data["layouts"]["keymap"]) + +# for key in kb.keys: +# print("{} {}x{}+{}x{}".format(key.labels, key.x, key.y, key.width, key.height)) diff --git a/src/main/python/main.py b/src/main/python/main.py index 8739b0c..f07b6d0 100644 --- a/src/main/python/main.py +++ b/src/main/python/main.py @@ -3,8 +3,11 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QWidget, QTabWidget, QVBoxLayout, QPushButton, QLabel import sys +import json from flowlayout import FlowLayout +from util import tr +from kle_serial import Serial as KleSerial class TabbedKeycodes(QTabWidget): @@ -13,7 +16,7 @@ class TabbedKeycodes(QTabWidget): self.tab_basic = QWidget() layout = FlowLayout() - for lbl in ["", "TODO", "Esc", "A", "B", "C", "D", "E", "F"]: + for lbl in ["", "hello", "Esc", "A", "B", "C", "D", "E", "F"]: btn = QPushButton(lbl) btn.setFixedSize(50, 50) layout.addWidget(btn) @@ -22,14 +25,14 @@ class TabbedKeycodes(QTabWidget): self.tab_media = QWidget() self.tab_macro = QWidget() - self.addTab(self.tab_basic, "Basic") - self.addTab(self.tab_media, "Media") - self.addTab(self.tab_macro, "Macro") + self.addTab(self.tab_basic, tr("TabbedKeycodes", "Basic")) + self.addTab(self.tab_media, tr("TabbedKeycodes", "Media")) + self.addTab(self.tab_macro, tr("TabbedKeycodes", "Macro")) KEY_WIDTH = 40 KEY_HEIGHT = KEY_WIDTH -KEY_SPACING = 10 +KEY_SPACING = 4 class KeyboardContainer(QWidget): @@ -37,23 +40,32 @@ class KeyboardContainer(QWidget): def __init__(self, parent=None): super().__init__(parent) - self.setFixedSize(300, 300) + serial = KleSerial() + data = open("g60.json", "r").read() + data = json.loads(data) + kb = serial.deserialize(data["layouts"]["keymap"]) - for x, btn in enumerate(["Q", "W", "E", "R"]): - widget = QLabel(btn) + max_w = max_h = 0 + + for key in kb.keys: + widget = QLabel(str(key.labels[0])) widget.setParent(self) - widget.setStyleSheet('background-color:red;') + widget.setStyleSheet('background-color:white; border: 1px solid black') widget.setAlignment(Qt.AlignCenter) - widget.setFixedSize(KEY_WIDTH, KEY_HEIGHT) - widget.move((KEY_WIDTH + KEY_SPACING) * x, 0) + + x = (KEY_WIDTH + KEY_SPACING) * key.x + y = (KEY_HEIGHT + KEY_SPACING) * key.y + w = (KEY_WIDTH + KEY_SPACING) * key.width - KEY_SPACING + h = (KEY_HEIGHT + KEY_SPACING) * key.height - KEY_SPACING + + widget.setFixedSize(w, h) + widget.move(x, y) + print("{} {}x{}+{}x{}".format(key.labels, key.x, key.y, key.width, key.height)) + + max_w = max(max_w, x + w) + max_h = max(max_h, y + h) - for x, btn in enumerate(["A", "S", "D", "F"]): - widget = QLabel(btn) - widget.setParent(self) - widget.setStyleSheet('background-color:red;') - widget.setAlignment(Qt.AlignCenter) - widget.setFixedSize(KEY_WIDTH, KEY_HEIGHT) - widget.move((KEY_WIDTH + KEY_SPACING) * (x + 0.25), KEY_HEIGHT + KEY_SPACING) + self.setFixedSize(max_w, max_h) class MainWindow(QWidget): @@ -67,6 +79,7 @@ class MainWindow(QWidget): layout = QVBoxLayout() layout.addWidget(self.keyboard_container) + layout.setAlignment(self.keyboard_container, Qt.AlignHCenter) layout.addWidget(self.tabbed_keycodes) self.setLayout(layout) diff --git a/src/main/python/util.py b/src/main/python/util.py new file mode 100644 index 0000000..5b3530e --- /dev/null +++ b/src/main/python/util.py @@ -0,0 +1,3 @@ +from PyQt5.QtCore import QCoreApplication + +tr = QCoreApplication.translate