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>
This commit is contained in:
2026-10-23 19:24:30 +02:00
parent 2b75543c74
commit 80c6a5da1c
17 changed files with 1085 additions and 23 deletions

View File

@@ -243,6 +243,7 @@ class Inspector(QWidget):
self.control = None
self._loading = False
self.editing_layout = False
self.backend = None # the family of the pad plugged in, if any
v = QVBoxLayout(self)
v.setContentsMargins(24, 24, 24, 16)
@@ -596,13 +597,20 @@ class Inspector(QWidget):
self.keys_hint.setText(f"<span style='color:{BAD}'>{e}</span>")
self.changed.emit(self.control, None)
return
if b.is_sequence and not core.SEQUENCES_VERIFIED:
sequences_ok = (self.backend.sequences if self.backend is not None
else core.SEQUENCES_VERIFIED)
if b.is_sequence and not sequences_ok:
self.keys_hint.setText(
f"<span style='color:{BAD}'>Sequences aren't verified on this "
"pad yet, so they're switched off for now. One shortcut per "
"control.</span>")
self.changed.emit(self.control, None)
return
why = self.backend.check(b) if self.backend is not None else None
if why:
self.keys_hint.setText(f"<span style='color:{BAD}'>{why}</span>")
self.changed.emit(self.control, None)
return
self.keys_hint.setText(f"Sends {b.label()}")
self.changed.emit(self.control, b)
@@ -1170,6 +1178,7 @@ class Window(QMainWindow):
f"{d.device.vid}:{d.device.pid}" in self.adopted:
self.diag = core.Diagnosis("ready", f"Ready on {d.device.path}.",
device=d.device, warnings=d.warnings)
switched = self._follow_device()
colour = {"ready": GOOD, "unplugged": INK_DIM.name()}.get(self.diag.status, BAD)
self.dot.setStyleSheet(f"background: {colour}; border-radius: 4px;")
if self.diag.device:
@@ -1186,11 +1195,40 @@ class Window(QMainWindow):
self.inspector.show_problem(self.diag, self.note or self.shape_note,
offer_detect=bool(self.shape_note))
self.inspector.show_background(bool(act.listener_pid()))
if self.diag.status != old:
self._refresh(editor=False)
if self.diag.ready and old in ("unplugged", "no-interface", None):
if self.diag.status != old or switched:
self._refresh(editor=switched)
if self.diag.ready and (switched or
old in ("unplugged", "no-interface", None)):
self._read(quiet=True)
def _follow_device(self):
"""
Keep the shadow state and the drawing on the pad that is plugged in.
Each family keeps its own state file. The first time a pad with a
known USB id turns up it is drawn with the layout we know for it,
so a 9 key pad is never shown as 12 keys and 2 knobs. Returns True
when the state was switched to another pad.
"""
dev = self.diag.device if self.diag else None
if not dev:
self.inspector.backend = None
return False
self.inspector.backend = core.backend_for(dev)
ident = f"{dev.vid}:{dev.pid}"
if ident == self.state.device_id:
return False
state = core.State(device_id=ident)
if not state.path.exists():
known = core.KNOWN_LAYOUTS.get((dev.vid, dev.pid))
if known:
state.layout = known
self.state = state
self.desired.clear()
self.pending_actions.clear()
self.pad.set_layout(self.state.layout) # drops a stale selection
return True
# ------------------------------------------------------------- write
def _write(self, controls):
@@ -1339,7 +1377,7 @@ class Window(QMainWindow):
return
layout = found.layout
self.state = core.State(layout=layout)
self.state = core.State(layout=layout, device_id=f"{dev.vid}:{dev.pid}")
self.state.layout = layout
for control, binding in found.bindings.items():
self.state.record(control, binding)