diff --git a/src/main/python/keyboard_comm.py b/src/main/python/keyboard_comm.py index 2cec868..e32f709 100644 --- a/src/main/python/keyboard_comm.py +++ b/src/main/python/keyboard_comm.py @@ -10,6 +10,7 @@ from kle_serial import Serial as KleSerial from unlocker import Unlocker from util import MSG_LEN, hid_send, chunks +CMD_VIA_GET_PROTOCOL_VERSION = 0x01 CMD_VIA_GET_KEYBOARD_VALUE = 0x02 CMD_VIA_SET_KEYBOARD_VALUE = 0x03 CMD_VIA_GET_KEYCODE = 0x04 @@ -61,7 +62,7 @@ class Keyboard: self.macro = b"" self.vibl = False - self.vial_protocol = self.keyboard_id = -1 + self.via_protocol = self.vial_protocol = self.keyboard_id = -1 def reload(self, sideload_json=None): """ Load information about the keyboard: number of layers, physical key layout """ @@ -84,6 +85,9 @@ class Keyboard: def reload_layout(self, sideload_json=None): """ Requests layout data from the current device """ + data = self.usb_send(self.dev, struct.pack("B", CMD_VIA_GET_PROTOCOL_VERSION), retries=20) + self.via_protocol = struct.unpack(">H", data[1:3])[0] + if sideload_json is not None: payload = sideload_json else: @@ -276,6 +280,8 @@ class Keyboard: # TODO: macros should be serialized in a portable format instead of base64 string # i.e. use a custom structure (as keycodes numbers can change, etc) data["macro"] = base64.b64encode(self.macro).decode("utf-8") + data["vial_protocol"] = self.vial_protocol + data["via_protocol"] = self.via_protocol return json.dumps(data).encode("utf-8") @@ -299,13 +305,15 @@ class Keyboard: self.set_layout_options(data["layout_options"]) - # we need to unlock the keyboard before we can restore the macros, lock it afterwards - # only do that if it's different from current macros - macro = base64.b64decode(data["macro"]) - if macro != self.macro: - Unlocker.unlock(self) - self.set_macro(macro) - self.lock() + json_vial_protocol = data.get("vial_protocol", 1) + if json_vial_protocol == self.vial_protocol: + # we need to unlock the keyboard before we can restore the macros, lock it afterwards + # only do that if it's different from current macros + macro = base64.b64decode(data["macro"]) + if macro != self.macro: + Unlocker.unlock(self) + self.set_macro(macro) + self.lock() def reset(self): self.usb_send(self.dev, struct.pack("B", 0xB)) diff --git a/src/main/python/macro_action.py b/src/main/python/macro_action.py index 76e28b3..d0bd3be 100644 --- a/src/main/python/macro_action.py +++ b/src/main/python/macro_action.py @@ -12,9 +12,12 @@ from util import tr MACRO_SEQUENCE_KEYCODES = KEYCODES_BASIC + KEYCODES_ISO + KEYCODES_MEDIA KC_A = MACRO_SEQUENCE_KEYCODES[0] +SS_QMK_PREFIX = 1 + SS_TAP_CODE = 1 SS_DOWN_CODE = 2 SS_UP_CODE = 3 +SS_DELAY_CODE = 4 class BasicAction(QObject): @@ -43,7 +46,7 @@ class ActionText(BasicAction): def delete(self): self.text.deleteLater() - def serialize(self): + def serialize(self, vial_protocol): return self.text.text().encode("utf-8") def on_change(self): @@ -123,8 +126,11 @@ class ActionSequence(BasicAction): def serialize_prefix(self): raise NotImplementedError - def serialize(self): - out = b"" + def serialize(self, vial_protocol): + if vial_protocol >= 2: + out = b"\x01" + else: + out = b"" for k in self.sequence: out += self.serialize_prefix() out += struct.pack("B", k.code) @@ -147,3 +153,30 @@ class ActionTap(ActionSequence): def serialize_prefix(self): return b"\x01" + + +class ActionDelay(BasicAction): + + def __init__(self, container, delay=0): + super().__init__(container) + self.text = QLineEdit() + self.text.setText(str(delay)) + self.text.textChanged.connect(self.on_change) + + def insert(self, row): + self.container.addWidget(self.text, row, 2) + + def remove(self): + self.container.removeWidget(self.text) + + def delete(self): + self.text.deleteLater() + + def on_change(self): + self.changed.emit() + + def serialize(self, vial_protocol): + if vial_protocol < 2: + raise RuntimeError("ActionDelay can only be used with vial_protocol>=2") + delay = int(self.text.text()) + return struct.pack("BBBB", SS_QMK_PREFIX, SS_DELAY_CODE, (delay % 255) + 1, (delay // 255) + 1) diff --git a/src/main/python/macro_line.py b/src/main/python/macro_line.py index d32e8b1..0d63986 100644 --- a/src/main/python/macro_line.py +++ b/src/main/python/macro_line.py @@ -3,7 +3,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, Qt from PyQt5.QtWidgets import QHBoxLayout, QToolButton, QComboBox -from macro_action import ActionText, ActionDown, ActionUp, ActionTap +from macro_action import ActionText, ActionDown, ActionUp, ActionTap, ActionDelay class MacroLine(QObject): @@ -19,6 +19,10 @@ class MacroLine(QObject): self.parent = parent self.container = parent.container + if self.parent.parent.keyboard.vial_protocol >= 2: + self.types = self.types[:] + ["Delay"] + self.type_to_cls = self.type_to_cls[:] + [ActionDelay] + self.arrows = QHBoxLayout() self.btn_up = QToolButton() self.btn_up.setText("▲") @@ -87,5 +91,5 @@ class MacroLine(QObject): def on_change(self): self.changed.emit() - def serialize(self): - return self.action.serialize() + def serialize(self, vial_protocol): + return self.action.serialize(vial_protocol) diff --git a/src/main/python/macro_recorder.py b/src/main/python/macro_recorder.py index 7db6601..d8ca082 100644 --- a/src/main/python/macro_recorder.py +++ b/src/main/python/macro_recorder.py @@ -7,7 +7,7 @@ from PyQt5.QtWidgets import QPushButton, QGridLayout, QHBoxLayout, QToolButton, from basic_editor import BasicEditor from keycodes import Keycode -from macro_action import ActionText, ActionTap, ActionDown, ActionUp, SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE +from macro_action import ActionText, ActionTap, ActionDown, ActionUp, ActionDelay, SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE, SS_DELAY_CODE from macro_key import KeyString, KeyDown, KeyUp, KeyTap from macro_line import MacroLine from macro_optimizer import macro_optimize @@ -22,9 +22,11 @@ class MacroTab(QVBoxLayout): record = pyqtSignal(object, bool) record_stop = pyqtSignal() - def __init__(self, enable_recorder): + def __init__(self, parent, enable_recorder): super().__init__() + self.parent = parent + self.lines = [] self.container = QGridLayout() @@ -121,10 +123,10 @@ class MacroTab(QVBoxLayout): def serialize(self): out = b"" for line in self.lines: - out += line.serialize() + out += line.serialize(self.parent.keyboard.vial_protocol) return out - def deserialize(self, data): + def deserialize_v1(self, data): self.clear() sequence = [] @@ -160,6 +162,63 @@ class MacroTab(QVBoxLayout): cls = {SS_TAP_CODE: ActionTap, SS_DOWN_CODE: ActionDown, SS_UP_CODE: ActionUp}[s[0]] self.add_action(cls(self.container, keycodes)) + def deserialize_v2(self, data): + self.clear() + + sequence = [] + data = bytearray(data) + while len(data) > 0: + if data[0] == SS_QMK_PREFIX: + if data[1] in [SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE]: + # append to previous *_CODE if it's the same type, otherwise create a new entry + if len(sequence) > 0 and isinstance(sequence[-1], list) and sequence[-1][0] == data[1]: + sequence[-1][1].append(data[2]) + else: + sequence.append([data[1], [data[2]]]) + + for x in range(3): + data.pop(0) + elif data[1] == SS_DELAY_CODE: + # decode the delay + delay = (data[2] - 1) + (data[3] - 1) * 255 + sequence.append([SS_DELAY_CODE, delay]) + + for x in range(4): + data.pop(0) + else: + # append to previous string if it is a string, otherwise create a new entry + ch = chr(data[0]) + if len(sequence) > 0 and isinstance(sequence[-1], str): + sequence[-1] += ch + else: + sequence.append(ch) + data.pop(0) + for s in sequence: + if isinstance(s, str): + self.add_action(ActionText(self.container, s)) + else: + args = None + if s[0] in [SS_TAP_CODE, SS_DOWN_CODE, SS_UP_CODE]: + # map integer values to qmk keycodes + args = [] + for code in s[1]: + keycode = Keycode.find_outer_keycode(code) + if keycode: + args.append(keycode) + elif s[0] == SS_DELAY_CODE: + args = s[1] + + if args is not None: + cls = {SS_TAP_CODE: ActionTap, SS_DOWN_CODE: ActionDown, SS_UP_CODE: ActionUp, + SS_DELAY_CODE: ActionDelay}[s[0]] + self.add_action(cls(self.container, args)) + + def deserialize(self, data): + if self.parent.keyboard.vial_protocol >= 2: + return self.deserialize_v2(data) + else: + return self.deserialize_v1(data) + def on_change(self): self.changed.emit() @@ -211,7 +270,7 @@ class MacroRecorder(BasicEditor): self.tabs = QTabWidget() for x in range(32): - tab = MacroTab(self.recorder is not None) + tab = MacroTab(self, self.recorder is not None) tab.changed.connect(self.on_change) tab.record.connect(self.on_record) tab.record_stop.connect(self.on_tab_stop)