Files
macropad-itprotech/macropad_gui/backends/base.py
Bjorn Blomberg 80c6a5da1c Add the ITPROTECH MK series (8808) as a third pad family
Confirmed on the MK-09 (8808:660a, 9 keys): the vendor's .NET configurator
was decompiled for the wire format and every frame was then written to a
real pad and read back. Keys, media keys and multikey sequences are
supported; lighting from the command line.

The firmware acts on every other output report, so each report is sent
twice (as the vendor software does) and every write is verified by reading
the key back. The app now keeps one state file per pad and starts with a
known layout for a known USB id, and the editor asks the backend whether a
binding fits before offering to write it. The actions listener and the
udev rule cover every supported vendor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-23 19:24:30 +02:00

73 lines
2.5 KiB
Python

"""
What a device family has to provide.
These raise rather than returning something vague, so a half written backend
fails at the call it can't answer instead of writing wrong bytes to somebody's
hardware.
"""
class Backend:
key = "" # short id, used in settings and messages
name = "" # what a person would call this family
vid = "" # USB vendor id, lowercase hex, no 0x
pids = () # product ids this backend is CONFIRMED on
layers = 1
max_keys = 0
max_knobs = 0
sequences = False # can one control send several keys in a row?
def claims(self, vid, pid):
"""True only for ids this backend has been confirmed on."""
if vid.lower() != self.vid.lower():
return False
return not self.pids or pid.lower() in {p.lower() for p in self.pids}
def could_claim(self, vid, pid):
# Siblings share a vendor id but not always a protocol. The scan and
# the udev rule cover the whole vendor so an unconfirmed sibling is
# still found and offered detection; claims() stays strict so it is
# never written to on the strength of its vendor id alone.
return vid.lower() == self.vid.lower()
def udev_rule(self):
return (f'KERNEL=="hidraw*", ATTRS{{idVendor}}=="{self.vid}", '
f'TAG+="uaccess"')
def adopt(self, layout):
"""Where a family can say more about a shape than detection found."""
return layout
def supports(self, keys, knobs):
return 1 <= keys <= self.max_keys and 0 <= knobs <= self.max_knobs
def check(self, binding):
"""
Why this family can't hold a binding the GUI thinks is valid, or
None. Lets the editor say so while typing instead of at write time.
"""
return None
# The six a family must answer.
def action_byte(self, control):
"""The slot number this family uses for a control id."""
raise NotImplementedError
def info(self, device):
"""(keys, knobs) as the pad reports them, or None."""
raise NotImplementedError
def read_layer(self, device, layer=1, layout=None):
raise NotImplementedError
def write(self, device, control, binding, state, layer=1, _write=None):
raise NotImplementedError
def detect(self, device):
"""Read only. Must say no rather than guess."""
raise NotImplementedError
def __repr__(self):
return f"<{type(self).__name__} {self.vid}:{'/'.join(self.pids) or '*'}>"