[{"data":1,"prerenderedAt":36},["ShallowReactive",2],{"project-libdiginet-protocol":3},{"page":4,"others":26},{"slug":5,"src":6,"title":7,"source":8,"toc":9,"html":25},"protocol","docs\u002Fprotocol.md","The DigiNet surface protocol (Control|24, ProControl)","https:\u002F\u002Fgithub.com\u002FFreeMixer\u002Flibdiginet\u002Fblob\u002F084e09f8eaea72a275f583b8ae148b565aa6a548\u002Fdocs\u002Fprotocol.md",[10,13,16,19,22],{"id":11,"title":12},"sources","Sources",{"id":14,"title":15},"the-protocol","The protocol",{"id":17,"title":18},"checked-against-c24-engine-renewed","Checked against C24-Engine-Renewed",{"id":20,"title":21},"assumptions","Assumptions",{"id":23,"title":24},"open-questions-and-how-to-settle-them-on-a-unit","Open questions and how to settle them on a unit","\u003Ch1 id=\"the-diginet-surface-protocol-control-24-procontrol\">The DigiNet surface protocol (Control|24, ProControl)\u003C\u002Fh1>\n\u003Cp>What is known about the protocol the Digidesign Control|24 and ProControl speak over Ethernet, gathered from the projects listed below. Every claim names its source. Nothing here was taken from a vendor document. Items marked unknown are settled by captures on a real unit (see the end of this file).\u003C\u002Fp>\n\u003Ch2 id=\"sources\">Sources\u003C\u002Fh2>\n\u003Cp>Line citations below are to these commits.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Source\u003C\u002Fth>\n\u003Cth>URL\u003C\u002Fth>\n\u003Cth>What it covers\u003C\u002Fth>\n\u003Cth>What here rests on it\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>ReaControl24, branch \u003Ccode>Release\u003C\u002Fcode>, commit \u003Ccode>2a963e9\u003C\u002Fcode> (2020-06-26). GPL-3.0-or-later\u003C\u002Ftd>\n\u003Ctd>https:\u002F\u002Fgithub.com\u002Fphunkyg\u002FReaControl24\u003C\u002Ftd>\n\u003Ctd>A Control|24 daemon and OSC client for Reaper: daemon \u003Ccode>control24d.py\u003C\u002Fcode>, control map \u003Ccode>control24map.py\u003C\u002Fcode>, client \u003Ccode>control24osc.py\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The header fields and their names, \u003Ccode>e2\u003C\u002Fcode> and \u003Ccode>a0\u003C\u002Fcode>, the counters, stop-and-wait and back-off, the batching limits, the 10 s keepalive and its bug, the button and LED map, and the fader, V-Pot, ring, meter, scribble, automation-LED and counter encodings\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>ReaControl24, branch \u003Ccode>Ftr_Version2\u003C\u002Fcode>, commit \u003Ccode>440f29a\u003C\u002Fcode> (2024-11-19)\u003C\u002Ftd>\n\u003Ctd>https:\u002F\u002Fgithub.com\u002Fphunkyg\u002FReaControl24\u002Ftree\u002FFtr_Version2\u003C\u002Ftd>\n\u003Ctd>The newer multi-device daemon \u003Ccode>ReaControl.py\u003C\u002Fcode>, \u003Ccode>ReaCommon.py\u003C\u002Fcode>, the ProControl client \u003Ccode>procontrolosc.py\u003C\u002Fcode>, \u003Ccode>pro_control_map.csv\u003C\u002Fcode>, \u003Ccode>test\u002Femulator.py\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The announce layout, broadcast and unicast, the ProControl's name, the stale fourth V-Pot byte\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>ReaControl24 issues #2 to #22\u003C\u002Ftd>\n\u003Ctd>https:\u002F\u002Fgithub.com\u002Fphunkyg\u002FReaControl24\u002Fissues\u003C\u002Ftd>\n\u003Ctd>Problems users reported\u003C\u002Ftd>\n\u003Ctd>ProControl display addressing (#12, #22) and its device byte\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>procontrol-linux, commit \u003Ccode>ecf1da8\u003C\u002Fcode> (2026-09-27). GPL-3.0-or-later; the documents are in French\u003C\u002Ftd>\n\u003Ctd>https:\u002F\u002Fgithub.com\u002Fjulienfernandez\u002Fprocontrol-linux\u003C\u002Ftd>\n\u003Ctd>A ProControl driver for Linux, with captures and measurements from a real unit. It carries lazlooose's ProControl display fixes (\u003Ccode>docs\u002Fdisplays.md\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>The header sum, \u003Ccode>e0\u003C\u002Fcode>\u002F\u003Ccode>e1\u003C\u002Fcode> and the host MAC in \u003Ccode>e1\u003C\u002Fcode>, the keepalive bug and its fix, the fader echo, the 10 Mbit\u002Fs half-duplex link, the ProControl counter and display addresses, the dangers of the \u003Ccode>0x70\u003C\u002Fcode> family\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>C24-Engine-Renewed, commit \u003Ccode>94d7a3b\u003C\u002Fcode> (2026-08-16). No licence\u003C\u002Ftd>\n\u003Ctd>https:\u002F\u002Fgithub.com\u002FDavitekk\u002FC24-Engine-Renewed\u003C\u002Ftd>\n\u003Ctd>Control|24 tools for macOS that drive a real desk: a Pro Tools HUI bridge (\u003Ccode>Control24-Engine\u002Fmotore.py\u003C\u002Fcode>), CamillaDSP and media modes (\u003Ccode>Camilla24\u002F\u003C\u002Fcode>, \u003Ccode>HUI-OS\u002F\u003C\u002Fcode>), a calculator on the keypad and counter (\u003Ccode>Calculator24\u002F\u003C\u002Fcode>), an LED finder (\u003Ccode>Control24-LED-Finder\u002F\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>The header as a host fills it, \u003Ccode>e3\u003C\u002Fcode>, the minimum frame, the ping and watchdog timings, running status, display pacing, the 14-bit fader, the ring bits (A1), the meter bits (A2), the seven-segment bits (A3), the counter's digit and dot order (A4, A5), the centre displays, the automation and counter-mode LED values, the device-name write. Every point is compared with this library in &quot;Checked against C24-Engine-Renewed&quot; below\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Neyrinck, legacy control surface research\u003C\u002Ftd>\n\u003Ctd>https:\u002F\u002Fneyrinck.com\u002Fv-control-pro\u002Flegacy-control-surface-research\u002F\u003C\u002Ftd>\n\u003Ctd>Neyrinck's one-line description of the surfaces' link: &quot;low level ethernet communications, cryptography, CRC error detection&quot;\u003C\u002Ftd>\n\u003Ctd>R3 and R11\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Chr>\n\u003Ch2 id=\"the-protocol\">The protocol\u003C\u002Fh2>\n\u003Ch3 id=\"transport\">Transport\u003C\u002Fh3>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Property\u003C\u002Fth>\n\u003Cth>Finding\u003C\u002Fth>\n\u003Cth>Status\u003C\u002Fth>\n\u003Cth>Citation\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Framing\u003C\u002Ftd>\n\u003Ctd>Raw Ethernet II, EtherType \u003Cstrong>0x885F\u003C\u002Fstrong>, no IP\u003C\u002Ftd>\n\u003Ctd>Decoded, confirmed by three projects\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24d.py:42\u003C\u002Fcode>, \u003Ccode>:123\u003C\u002Fcode>; procontrol-linux \u003Ccode>docs\u002Fprotocol.md\u003C\u002Fcode> (frame offset table)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Vendor\u003C\u002Ftd>\n\u003Ctd>Source OUI \u003Cstrong>00:a0:7e\u003C\u002Fstrong> (Digidesign)\u003C\u002Ftd>\n\u003Ctd>Decoded\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24d.py:101\u003C\u002Fcode>, \u003Ccode>:403\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Capture filter used\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>(ether dst &lt;host-mac&gt; or broadcast) and ether[12:2]=0x885f\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24d.py:69\u003C\u002Fcode>, \u003Ccode>:261\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Link\u003C\u002Ftd>\n\u003Ctd>The ProControl showed \u003Cstrong>10 Mbit\u002Fs half duplex\u003C\u002Fstrong> on a direct link. The Control\u003C\u002Ftd>\n\u003Ctd>24's port is of the same generation\u003C\u002Ftd>\n\u003Ctd>Observed (ProControl); \u003Cstrong>to verify\u003C\u002Fstrong> on the C24\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Minimum frame\u003C\u002Ftd>\n\u003Ctd>Bodies are padded to the Ethernet minimum (46-byte payload). Bytes after the logical length are padding, and a Control\u003C\u002Ftd>\n\u003Ctd>24 leaves stale bytes there: in \u003Ccode>walk2.pcap\u003C\u002Fcode> (Control\u003C\u002Ftd>\n\u003Ctd>24 1.08, a walk of every control) 402 of its 2,306 data frames carry bytes at or above 0x80 in the padding, and none carries one in its messages. The grammar (\u003Ccode>diginet.ksy\u003C\u002Fcode>) accepts a data byte at or above 0x80 where the C decoder rejects it; whether a desk ever sends one inside a message is \u003Cstrong>unmeasured\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Broadcast and unicast\u003C\u002Ftd>\n\u003Ctd>The desk \u003Cstrong>broadcasts\u003C\u002Fstrong> its announce to ff:ff:ff:ff:ff:ff. Everything else is unicast between the desk MAC and the host MAC\u003C\u002Ftd>\n\u003Ctd>Decoded\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24d.py:396-418\u003C\u002Fcode>; \u003Ccode>ReaControl.py:418-436\u003C\u002Fcode> (Ftr_Version2)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>\u003Cstrong>The header\u003C\u002Fstrong> (16 bytes after the 14-byte Ethernet header, big-endian; \u003Ccode>control24d.py:137-150\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Frame offset\u003C\u002Fth>\n\u003Cth>Size\u003C\u002Fth>\n\u003Cth>Field (ReaControl name)\u003C\u002Fth>\n\u003Cth>Meaning\u003C\u002Fth>\n\u003Cth>Status\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>14\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>numbytes\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>16 + body length (keepalive = 17, bare control frame = 16, announce = 50)\u003C\u002Ftd>\n\u003Ctd>Decoded (\u003Ccode>control24d.py:198\u003C\u002Fcode>, \u003Ccode>:221\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>16\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>unknown1\u003C\u002Fcode> \u002F &quot;parity&quot;\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>16-bit additive sum of the body.\u003C\u002Fstrong> All 418 of 418 ProControl→host frames matched (procontrol-linux \u003Ccode>tools\u002Faudit_diginet.py:26-27\u003C\u002Fcode>, \u003Ccode>docs\u002Fprotocol.md\u003C\u002Fcode> §Mise à jour). ReaControl's one non-zero value, \u003Ccode>0x0244\u003C\u002Fcode> on the clock-init frame (\u003Ccode>control24d.py:415-416\u003C\u002Fcode>), is exactly the byte sum of that body. ReaControl and C24-Engine send \u003Cstrong>zero\u003C\u002Fstrong> everywhere else, and the Control\u003C\u002Ftd>\n\u003Ctd>24 accepts it\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>18\u003C\u002Ftd>\n\u003Ctd>4\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>sendcounter\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>The sender's own sequence. ReaControl advances it by the number of commands in the frame; C24-Engine by one per frame, ACKs and pings included. The desk works with both. \u003Cstrong>The desk's own\u003C\u002Fstrong> advances by the previous data frame's \u003Ccode>numcommands\u003C\u002Fcode> (a frame carries the counter before its own commands); a retransmission carries it unchanged with \u003Ccode>retry\u003C\u002Fcode> set (\u003Ccode>r15b.pcap\u003C\u002Fcode>, \u003Ccode>walk2.pcap\u003C\u002Fcode>, Control\u003C\u002Ftd>\n\u003Ctd>24 1.08)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>22\u003C\u002Ftd>\n\u003Ctd>4\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>cmdcounter\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>In an ACK: the peer's sequence being acknowledged\u003C\u002Ftd>\n\u003Ctd>Decoded (\u003Ccode>control24d.py:439-447\u003C\u002Fcode>, \u003Ccode>:499-500\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>26\u003C\u002Ftd>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>retry\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Non-zero = the desk is retransmitting because it did not get our ACK in time\u003C\u002Ftd>\n\u003Ctd>Partly decoded (\u003Ccode>control24d.py:430-437\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>28\u003C\u002Ftd>\n\u003Ctd>1\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>c24cmd\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>0x00\u003C\u002Fcode> = data, \u003Ccode>0xA0\u003C\u002Fcode> = ACK, \u003Ccode>0xE2\u003C\u002Fcode> = online\u002Fwake, \u003Ccode>0xE0\u003C\u002Fcode> = announce (offline), \u003Ccode>0xE1\u003C\u002Fcode> = announce (in session), \u003Ccode>0xE3\u003C\u002Fcode> = disconnect\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>a0\u003C\u002Fcode>\u002F\u003Ccode>e2\u003C\u002Fcode> in \u003Ccode>control24common.py:48-51\u003C\u002Fcode>; \u003Ccode>e0\u003C\u002Fcode>\u002F\u003Ccode>e1\u003C\u002Fcode> in procontrol-linux \u003Ccode>tools\u002Fprocontrold.py:102-115\u003C\u002Fcode> and \u003Ccode>docs\u002Fsession-reference.md\u003C\u002Fcode>; \u003Ccode>e3\u003C\u002Fcode> in C24-Engine \u003Ccode>motore.py:1069-1079\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>29\u003C\u002Ftd>\n\u003Ctd>1\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>numcommands\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Number of commands in the body. A fader move \u003Ccode>B0 tt MSB (20+tt) LSB\u003C\u002Fcode> counts as one. A Control\u003C\u002Ftd>\n\u003Ctd>24 batches moves of one fader: \u003Ccode>r15b.pcap\u003C\u002Fcode> packets 596 and 613 carry 2 and 5 moves, counts 2 and 5 (A7)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>\u003Cstrong>Discovery and handshake:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Col>\n\u003Cli>The desk broadcasts an \u003Ccode>e0\u003C\u002Fcode> announce about every 7 s when idle (ProControl: 4 in 30 s, 104 in 740 s; procontrol-linux \u003Ccode>docs\u002Fobservations-2026-09-13.md:60,73\u003C\u002Fcode>). The body is 14 unknown bytes, then the version from byte 14 (\u003Ccode>v1.08\u003C\u002Fcode>: byte 14 is the letter \u003Ccode>v\u003C\u002Fcode>), then the device name from byte 24: \u003Cstrong>\u003Ccode>CNTRL|24\u003C\u002Fcode>\u003C\u002Fstrong> for a Control|24, \u003Ccode>MAINUNIT\u003C\u002Fcode> for a ProControl (\u003Ccode>control24d.py:168-176\u003C\u002Fcode>; \u003Ccode>ReaControl.py:624-636\u003C\u002Fcode>; \u003Ccode>test\u002Femulator.py:14-17\u003C\u002Fcode>). C24-Engine reads the version at body bytes 14–24 and the name at 24–32 (\u003Ccode>motore.py:1191-1198\u003C\u002Fcode>), which every announce in \u003Ccode>r15b.pcap\u003C\u002Fcode> and \u003Ccode>walk2.pcap\u003C\u002Fcode> bears out. ReaControl's layout, and this library's (\u003Ccode>diginet_frame.c:148\u003C\u002Fcode>), read the version from byte 15, so they give \u003Ccode>1.08\u003C\u002Fcode> without the \u003Ccode>v\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>The host sends \u003Ccode>e2\u003C\u002Fcode> with no body (\u003Ccode>control24d.py:414\u003C\u002Fcode>), at send counter 0: a Control|24 takes a session on no other (#41). ReaControl then sends a clock-clear frame \u003Ccode>F0 13 01 30 19 00 … F7\u003C\u002Fcode> with checksum \u003Ccode>02 44\u003C\u002Fcode> (\u003Ccode>control24d.py:415-418\u003C\u002Fcode>). After the \u003Ccode>30\u003C\u002Fcode> it carries 9 or 10 bytes: the counter address \u003Ccode>19\u003C\u002Fcode> and eight zeros, or with the dots byte as the counter message has it. The zeros add nothing to the sum, so \u003Ccode>02 44\u003C\u002Fcode> fits both. procontrol-linux showed the clock frame is not needed (\u003Ccode>docs\u002Fsession-reference.md\u003C\u002Fcode>, &quot;Essai local&quot;).\u003C\u002Fli>\n\u003Cli>The desk ACKs (\u003Ccode>a0\u003C\u002Fcode>, ack counter = our sequence). A ProControl then switches its broadcasts to \u003Cstrong>\u003Ccode>e1\u003C\u002Fcode>, which carries the host MAC\u003C\u002Fstrong>, so on a ProControl an \u003Ccode>e1\u003C\u002Fcode> announce is the &quot;online&quot; proof and an ACK alone is not (procontrol-linux \u003Ccode>docs\u002Fonline-2026-09-13.md:35-36\u003C\u002Fcode>). \u003Cstrong>A Control|24 enters its session only once the host has sent in it\u003C\u002Fstrong> (firmware 1.08, captures on the desk, #41, #48). After a boot it announces \u003Ccode>e0\u003C\u002Fcode>, answers an \u003Ccode>e2\u003C\u002Fcode> at send counter 0 with \u003Ccode>a0\u003C\u002Fcode> (an \u003Ccode>e2\u003C\u002Fcode> at any other counter it only ACKs), and keeps announcing \u003Ccode>e0\u003C\u002Fcode>. A host that goes online on that \u003Ccode>a0\u003C\u002Fcode> and sends sees every frame ACKed; at the desk's next \u003Ccode>e0\u003C\u002Fcode> it sends another \u003Ccode>e2\u003C\u002Fcode>, and 1 ms later the desk announces \u003Ccode>e1\u003C\u002Fcode> naming the host, and \u003Ccode>e1\u003C\u002Fcode> only from then on. That second \u003Ccode>e2\u003C\u002Fcode> opens a new session on the desk, which blanks its scribbles (the faders keep their place), so its first \u003Ccode>e1\u003C\u002Fcode> is ONLINE again for the host (\u003Ccode>DIGINET_R_RENEWED\u003C\u002Fcode>), which shows everything again. A host that waits for \u003Ccode>e1\u003C\u002Fcode> before it sends is answered \u003Ccode>a0\u003C\u002Fcode> to every \u003Ccode>e2\u003C\u002Fcode> and never sees one: so on a Control|24 the \u003Ccode>a0\u003C\u002Fcode> is the online proof. Out of session it still ACKs every host data frame and shows Offline. Once an \u003Ccode>e1\u003C\u002Fcode> has named the host, an \u003Ccode>e0\u003C\u002Fcode> is a desk out of session, after a reboot: it ends the session, which the host then wakes again. \u003Cstrong>A Control|24 that boots while large unknown broadcasts reach it ignores every host:\u003C\u002Fstrong> booted with reac-pw sending about 32 REAC frames of 1492 bytes a second on its link, it ACKed no \u003Ccode>e2\u003C\u002Fcode>, ours nor ReaControl24's, and stopping the traffic did not bring it back; booted on a quiet link it took the session at once. Such a desk keeps announcing and lets every \u003Ccode>e2\u003C\u002Fcode> time out, and nothing on the wire names the cause, so the session reports it on its own: after \u003Ccode>deaf_wakes\u003C\u002Fcode> (3) wakes in a row that time out while the desk still announces (an announce within \u003Ccode>offline_ms\u003C\u002Fcode>), it gives \u003Ccode>DIGINET_N_DESK_DEAF\u003C\u002Fcode> once (#35). An \u003Ccode>a0\u003C\u002Fcode> for an \u003Ccode>e2\u003C\u002Fcode>, an \u003Ccode>e1\u003C\u002Fcode> naming another host (\u003Ccode>DIGINET_N_FOREIGN\u003C\u002Fcode>, a different notice: that desk hears hosts) or a timeout with no recent announce ends the count.\u003C\u002Fli>\n\u003Cli>Disconnect is \u003Ccode>e3\u003C\u002Fcode> with no body. It drops the session (C24-Engine \u003Ccode>motore.py:1069-1079\u003C\u002Fcode>, &quot;the CORRECT way to disconnect&quot;). A Control|24 (firmware 1.08, measured 2026-10-29, #33) then keeps announcing \u003Ccode>e1\u003C\u002Fcode> naming that host for about 30 s (34 s and 30 s), until its own timeout, before it falls back to \u003Ccode>e0\u003C\u002Fcode>. With \u003Ccode>auto_wake\u003C\u002Fcode> the session therefore sends \u003Ccode>e2\u003C\u002Fcode> on an \u003Ccode>e1\u003C\u002Fcode> naming its own host as it does on an \u003Ccode>e0\u003C\u002Fcode>, so a restarted host need not wait for the desk to time out.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>C24-Engine sends \u003Ccode>e2\u003C\u002Fcode> five times 5 ms apart, waits 200 ms and then drives the desk without waiting for an ACK or an \u003Ccode>e1\u003C\u002Fcode> (\u003Ccode>motore.py:1172-1189\u003C\u002Fcode>). It treats any frame from the desk as the desk being there.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Acks, sequencing, retransmit:\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Every desk data frame gets a host ACK. ReaControl sends it about 0.8 ms after receipt (\u003Ccode>TIMING_BEFORE_ACKT\u003C\u002Fcode>, \u003Ccode>control24d.py:48\u003C\u002Fcode>, \u003Ccode>:445-447\u003C\u002Fcode>), with the desk's \u003Ccode>sendcounter\u003C\u002Fcode> in our \u003Ccode>cmdcounter\u003C\u002Fcode> field (\u003Ccode>:499-500\u003C\u002Fcode>). ReaControl does not advance its own counter on an ACK; C24-Engine does (\u003Ccode>motore.py:940-948\u003C\u002Fcode>). Both work, so the desk tolerates either.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Host→desk is stop-and-wait: one frame in flight.\u003C\u002Fstrong> \u003Ccode>receive_handler\u003C\u002Fcode> blocks on \u003Ccode>sendlock\u003C\u002Fcode> until the desk's \u003Ccode>a0\u003C\u002Fcode> arrives (\u003Ccode>control24d.py:460-468\u003C\u002Fcode>). It logs every 100 ms and never gives up or retransmits: \u003Ccode>#TODO implement daw-desk retry packets\u003C\u002Fcode> (\u003Ccode>:463\u003C\u002Fcode>). procontrol-linux kept the same one-frame window (\u003Ccode>docs\u002Ffader-echo-2026-09-14.md\u003C\u002Fcode>, &quot;une seule trame en attente d'ACK&quot;).\u003C\u002Fli>\n\u003Cli>When the desk sends a retry frame, ReaControl pauses its own sending for 300 ms (\u003Ccode>TIMING_BACKOFF\u003C\u002Fcode>, \u003Ccode>control24d.py:53\u003C\u002Fcode>, \u003Ccode>:433-437\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>C24-Engine does not wait for ACKs at all: it sends frame after frame, never retransmits, and never sets or reads the retry field (\u003Ccode>motore.py:516-550\u003C\u002Fcode>). So the desk accepts more than one host frame in flight.\u003C\u002Fli>\n\u003Cli>Batching: up to 48 commands or 314 bytes per frame (\u003Ccode>control24d.py:298-299\u003C\u002Fcode>, \u003Ccode>:333-341\u003C\u002Fcode>). C24-Engine packs whole display batches into one frame, up to 51 commands and 461 bytes when it initialises the scribble strips (\u003Ccode>motore.py:962-984\u003C\u002Fcode>) and 100 LED commands when it blanks the desk (\u003Ccode>:1036-1067\u003C\u002Fcode>), and paces display batches about 2 ms apart, &quot;so the desk's controller renders cleanly (no datamoshing)&quot; (\u003Ccode>motore.py:531-550\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Keepalive (a real bug in the reference):\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cul>\n\u003Cli>ReaControl sends a keepalive frame (body \u003Ccode>00\u003C\u002Fcode>, ncmds 1) when nothing has been sent for 10 s (\u003Ccode>control24d.py:46\u003C\u002Fcode>, \u003Ccode>:285-293\u003C\u002Fcode>, \u003Ccode>:507-511\u003C\u002Fcode>). But every ACK counts as &quot;sent&quot; (\u003Ccode>pcap_last_sent\u003C\u002Fcode>, \u003Ccode>:484\u003C\u002Fcode>), so a busy surface never sends one.\u003C\u002Fli>\n\u003Cli>procontrol-linux measured a 48 s gap between keepalives with 225 ACKs in between, and the desk fell back to \u003Ccode>e0\u003C\u002Fcode> (offline). Their fix is a keepalive every 10 s \u003Cstrong>regardless of ACKs\u003C\u002Fstrong> (\u003Ccode>docs\u002Fonline-2026-09-13.md:136-160\u003C\u002Fcode>). After the fix the longest gap was 10.048 s and the desk stayed online.\u003C\u002Fli>\n\u003Cli>C24-Engine pings every 1 s with an empty 16-byte data frame (count 0, no body), and declares the desk offline after 4 s with no traffic (\u003Ccode>motore.py:404\u003C\u002Fcode>, \u003Ccode>:951-960\u003C\u002Fcode>, \u003Ccode>:1023-1034\u003C\u002Fcode>). Its calculator pings every 2.5 s (\u003Ccode>Calculator24\u002FCalculator24.py:78-81\u003C\u002Fcode>). The desk accepts both keepalive forms, the empty frame and the body \u003Ccode>00\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>The desk-side timeout is \u003Cstrong>not measured\u003C\u002Fstrong>; &quot;about 45 s&quot; is an upper bound on the ProControl.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>One socket, every interface (libdiginet's own, #35):\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>Nothing in the protocol ties a desk to an interface: it broadcasts its announce on whatever link it is cabled to, and a host answers from that link's MAC. So one \u003Ccode>AF_PACKET\u003C\u002Fcode> socket bound to EtherType 0x885F on no interface (\u003Ccode>diginet_packet_open_any()\u003C\u002Fcode>) hears every desk on every interface, interfaces that come up later included (a USB adapter plugged in afterwards). Each frame comes with the index of the interface it arrived on (\u003Ccode>diginet_packet_recvfrom()\u003C\u002Fcode>), and a frame goes out of one named interface (\u003Ccode>diginet_packet_sendto()\u003C\u002Fcode>); a host MAC is the MAC of the desk's interface (\u003Ccode>diginet_if_mac()\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>The desk mux\u003C\u002Fstrong> (\u003Ccode>diginet_mux.h\u003C\u002Fcode>) runs one session per desk over that socket. A desk is keyed by its MAC. Its first valid announce, from a Digidesign MAC, makes it present on the interface it came in on (\u003Ccode>DIGINET_MUX_N_APPEARED\u003C\u002Fcode>); from then on its frames come from that interface only, and the same MAC on another interface while it is present (a bridge and its port both show a frame) is set aside. An attached desk's session sends out of the desk's interface, from that interface's MAC.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>A desk that moved between interfaces of one host (#40):\u003C\u002Fstrong> it keeps announcing \u003Ccode>e1\u003C\u002Fcode> naming the first interface's MAC until its own timeout, about 30 s (step 4 of the handshake). Each attached session carries this host's other interface MACs (\u003Ccode>other_host_macs\u003C\u002Fcode>, from the io's \u003Ccode>host_macs\u003C\u002Fcode>, \u003Ccode>diginet_host_macs()\u003C\u002Fcode> on the packet socket), so that \u003Ccode>e1\u003C\u002Fcode> is this host's own session, not \u003Ccode>DIGINET_N_FOREIGN\u003C\u002Fcode>, and \u003Ccode>auto_wake\u003C\u002Fcode> takes it at once from the new interface. Only an \u003Ccode>e1\u003C\u002Fcode> naming the session's own interface MAC is the online proof. A MAC of no interface of this host is still another host's session. Two daemons on two interfaces of one host are one host to the desk mux: R13's one daemon per desk holds there too.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Gone:\u003C\u002Fstrong> nothing from the desk for \u003Ccode>gone_ms\u003C\u002Fcode> (30 s), or its session ended on silence (\u003Ccode>DIGINET_R_SILENCE\u003C\u002Fcode>, nothing for \u003Ccode>offline_ms\u003C\u002Fcode>, 25 s): \u003Ccode>DIGINET_MUX_N_GONE\u003C\u002Fcode>. 30 s is more than three announce periods (6.0-9.4 s, #29) and more than \u003Ccode>offline_ms\u003C\u002Fcode>, so an attached session gives up first. A Control|24 in an \u003Ccode>e0\u003C\u002Fcode> session loses it sooner, on its first unACKed keepalive (\u003Ccode>DIGINET_R_ACK_LOST\u003C\u002Fcode>), and is then gone at \u003Ccode>gone_ms\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The capability:\u003C\u002Fstrong> \u003Ccode>cap_net_raw\u003C\u002Fcode> is checked when the socket is opened, never after. \u003Ccode>diginet-packet-helper\u003C\u002Fcode> holds it, opens the socket, passes it over a Unix socket (\u003Ccode>SCM_RIGHTS\u003C\u002Fcode>) and exits; \u003Ccode>diginet_packet_open_by_helper()\u003C\u002Fcode> starts it and takes the socket, and \u003Ccode>diginet_packet_receive_fd()\u003C\u002Fcode> takes only an \u003Ccode>AF_PACKET\u003C\u002Fcode> socket. The program that drives the desks holds no capability.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>What the socket also hears.\u003C\u002Fstrong> It hears 0x885F on every interface, a REAC trunk included, and the mux would answer a desk there. The mux reports each desk's interface; whether a host should refuse a desk on an interface that carries REAC is the host's to rule, from that interface.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3 id=\"the-command-set-body\">The command set (body)\u003C\u002Fh3>\n\u003Cp>The body is a byte stream of MIDI-shaped messages. \u003Cstrong>Running status occurs\u003C\u002Fstrong>: C24-Engine expands it before parsing (\u003Ccode>motore.py:1100-1137\u003C\u002Fcode>, \u003Ccode>:1209\u003C\u002Fcode>), and says a desk fader move arrives as \u003Ccode>B0 tt MSB (20+tt) LSB\u003C\u002Fcode> with the second \u003Ccode>B0\u003C\u002Fcode> left out. ReaControl instead splits on known status bytes and \u003Ccode>F7\u003C\u002Fcode> (\u003Ccode>control24osc.py:1175-1200\u003C\u002Fcode>), which mis-parses running status. The desk uses the same encoding in both directions for faders, buttons and LEDs.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Faders (24 motor faders)\u003C\u002Fstrong> — decoded:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Desk→host and host→desk: \u003Ccode>B0 tt MSB (20+tt) LSB\u003C\u002Fcode>, a 14-bit MIDI CC pair carrying a \u003Cstrong>10-bit\u003C\u002Fstrong> position: MSB = v&gt;&gt;3, LSB = (v&amp;7)&lt;&lt;4 (\u003Ccode>control24osc.py:166-176\u003C\u002Fcode>, \u003Ccode>:916-920\u003C\u002Fcode>, \u003Ccode>:986-1001\u003C\u002Fcode>).\n\u003Cul>\n\u003Cli>Example: v = 512 gives \u003Ccode>B0 tt 40 (20+tt) 00\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>C24-Engine writes and reads plain 14-bit values, MSB = v&gt;&gt;7, LSB = v&amp;0x7F (\u003Ccode>motore.py:585-590\u003C\u002Fcode>, \u003Ccode>:1345-1372\u003C\u002Fcode>). These are the same bytes with the low 4 bits used.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>procontrol-linux confirmed raw travel of 0–1023 on fader 1 (\u003Ccode>docs\u002Fprotocol.md\u003C\u002Fcode>, register row &quot;Fader 1 et contact&quot;).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Touch:\u003C\u002Fstrong> a strip button \u003Ccode>90 09 (40|tt)\u003C\u002Fcode> on touch, \u003Ccode>90 09 tt\u003C\u002Fcode> on release (\u003Ccode>control24map.py:942-946\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The motor holds a set-point.\u003C\u002Fstrong> ReaControl echoes the hand's position back to the desk at most every 100 ms during a move (\u003Ccode>control24osc.py:974-976\u003C\u002Fcode>, \u003Ccode>TIMING_FADER_ECHO :50\u003C\u002Fcode>), and re-sends the last position on release (\u003Ccode>:978-983\u003C\u002Fcode>).\n\u003Cul>\n\u003Cli>procontrol-linux filmed the consequence of a late echo: on release the motor drove back to the old set-point, then to the new one (\u003Ccode>docs\u002Ffader-echo-2026-09-14.md\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>Their fix: echo the exact physical value immediately on release, and refuse any other host value while the cap is touched.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Status:\u003C\u002Fstrong> encoding decoded. Whether the motor pushes against a touched cap is \u003Cstrong>unknown\u003C\u002Fstrong> on the C24.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>V-Pots (24) and jog wheel\u003C\u002Fstrong> — decoded:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Turn: \u003Ccode>B0 (40|tt) dd\u003C\u002Fcode> with delta = dd − 64 (\u003Ccode>control24map.py:20-36\u003C\u002Fcode>; \u003Ccode>control24osc.py:890-906\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>ReaControl reads a 4th &quot;velocity&quot; byte (\u003Ccode>control24osc.py:893\u003C\u002Fcode>). The ProControl logs show that byte is stale buffer content (\u003Ccode>pro_control_map.csv\u003C\u002Fcode>, rows 5\u002F15\u002F25: arbitrary values), so treat V-Pot input as 3 bytes.\u003C\u002Fli>\n\u003Cli>The jog wheel is &quot;virtual track 28&quot;: \u003Ccode>B0 5C dd\u003C\u002Fcode>, delta = dd − 64 (\u003Ccode>control24osc.py:369-373\u003C\u002Fcode>, \u003Ccode>:746-775\u003C\u002Fcode>; procontrol-linux \u003Ccode>docs\u002Fprotocol.md\u003C\u002Fcode>, &quot;Le jog \u003Ccode>b0 5c valeur\u003C\u002Fcode>&quot;). C24-Engine reads V-Pots and jog the same way, three bytes each (\u003Ccode>motore.py:1374-1391\u003C\u002Fcode>), and hands the jog on with its sign inverted relative to the V-Pots. Which way of turning gives dd &gt; 64 on each control is \u003Cstrong>unknown\u003C\u002Fstrong>.\u003C\u002Fli>\n\u003Cli>Scrub and Shuttle are buttons in zone \u003Ccode>0x1C\u003C\u002Fcode>, ids 0x12\u002F0x13 (\u003Ccode>control24map.py:746-756\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>V-Pot LED rings (15 LEDs)\u003C\u002Fstrong> — decoded:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>F0 13 01 00 (tt | 0x40·LED1) b1 b2 F7\u003C\u002Fcode>: bit 6 of the address byte lights LED 1, \u003Ccode>b1\u003C\u002Fcode> bits 6..0 are LEDs 2..8, \u003Ccode>b2\u003C\u002Fcode> bits 6..0 are LEDs 9..15.\u003C\u002Fli>\n\u003Cli>ReaControl ships dot and fill tables (\u003Ccode>control24osc.py:783-816\u003C\u002Fcode>, \u003Ccode>:864-882\u003C\u002Fcode>). C24-Engine computes the same, LED 1 in the address byte and LED 2 at bit 6 of \u003Ccode>b1\u003C\u002Fcode> (\u003Ccode>motore.py:633-643\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Buttons and LEDs\u003C\u002Fstrong> — decoded, about 200 controls mapped:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Strip buttons:\u003C\u002Fstrong> \u003Ccode>90 bb (40·pressed | tt)\u003C\u002Fcode>, where \u003Ccode>bb\u003C\u002Fcode> is the button: 00 RecArm, 01 Pan\u002FSend, 02 EQ, 03 Dynamics, 04 Switch\u002FActive, 05 auto-mode, 06 Select, 07 Solo, 08 Mute, 09 fader Touch, 0A Inserts, 0B Peak, 0C SourceToggle, 0D RollOff (\u003Ccode>control24map.py:886-962\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Section buttons:\u003C\u002Fstrong> \u003Ccode>90 bb (40·pressed | zone)\u003C\u002Fcode>, zone 0x18..0x1D (\u003Ccode>control24map.py:37-48\u003C\u002Fcode>, \u003Ccode>ChildByteMatch 0x18\u003C\u002Fcode>):\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Zone\u003C\u002Fth>\n\u003Cth>What it holds\u003C\u002Fth>\n\u003Cth>Citation\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>0x18\u003C\u002Ftd>\n\u003Ctd>F1–F10, automation enable and mode, Shift\u002FOption\u002FControl\u002FCommand modifiers, Show Values\u002FGroup\u002FNames\u002FInfo, Solo Clear, meter select\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24map.py:49-330\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>0x19\u003C\u002Ftd>\n\u003Ctd>Window, zoom presets, Nav\u002FZoom\u002FSelAdj, cursor keys, Counter Mode\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24map.py:331-446\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>0x1A\u003C\u002Ftd>\n\u003Ctd>Numeric keypad\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24map.py:447-505\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>0x1B\u003C\u002Ftd>\n\u003Ctd>Edit mode\u002Ffunction\u002Ftools, Bank Left\u002FNudge\u002FRight, Master Faders, Undo\u002FSave\u002FEsc, Groups\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24map.py:506-668\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>0x1C\u003C\u002Ftd>\n\u003Ctd>Transport and Talkback\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24map.py:669-757\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>0x1D\u003C\u002Ftd>\n\u003Ctd>Monitor, inserts, assignment, Sends Flip and A\u002FF–E\u002FJ, pans, channel-bar scroll\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>control24map.py:758-880\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cul>\n\u003Cli>\u003Cstrong>LEDs\u003C\u002Fstrong> use the same 3-byte message: \u003Ccode>90 bb (40·on | zone-or-track)\u003C\u002Fcode>. ReaControl builds its LED table by walking the input map (\u003Ccode>control24osc.py:178-208\u003C\u002Fcode>, \u003Ccode>:1005-1062\u003C\u002Fcode>). C24-Engine: \u003Ccode>send_native_button_led\u003C\u002Fcode> (\u003Ccode>motore.py:552-556\u003C\u002Fcode>) and a separately discovered LED dictionary (\u003Ccode>Control24-LED-Finder\u002Fc24_led_dictionary.json\u003C\u002Fcode>, 1149 lines, facts only).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Per-strip automation LEDs:\u003C\u002Fstrong> \u003Ccode>F0 13 01 20 tt bits F7\u003C\u002Fcode> with write 0x40, touch 0x20, latch 0x10, trim 0x08, read 0x04 (\u003Ccode>control24osc.py:1082-1098\u003C\u002Fcode>). C24-Engine uses the same values (\u003Ccode>motore.py:777-779\u003C\u002Fcode>), noting that its first version had touch and trim swapped; its docstring at \u003Ccode>:438-440\u003C\u002Fcode> still shows the swapped pair.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Counter-mode LEDs:\u003C\u002Fstrong> \u003Ccode>F0 13 01 20 19 xx F7\u003C\u002Fcode> (\u003Ccode>control24osc.py:429-456\u003C\u002Fcode>), with timecode 0x20, feet+frames 0x10, bars|beats 0x08, min:sec 0x40, all off 0x00 (\u003Ccode>motore.py:339-343\u003C\u002Fcode>, \u003Ccode>:449-470\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Status:\u003C\u002Fstrong> the map covers what a mixing console needs. Some zone assignments are ReaControl's names and are unverified on a C24.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Scribble strips\u003C\u002Fstrong> — decoded, with one open point:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>F0 13 01 40 aa 00 c1 c2 c3 c4 F7\u003C\u002Fcode>: \u003Cstrong>4 characters\u003C\u002Fstrong>, plain ASCII, padded with spaces (\u003Ccode>control24osc.py:609-673\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>Special glyphs: a &quot;dot plus small digit&quot; glyph for \u003Ccode>.1\u003C\u002Fcode>..\u003Ccode>.9\u003C\u002Fcode> is \u003Ccode>chr(ord(d) − 26)\u003C\u002Fcode>, i.e. 0x17..0x1F (\u003Ccode>control24osc.py:662-671\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>Addresses:\n\u003Cul>\n\u003Cli>ReaControl writes strip n at \u003Ccode>aa = n\u003C\u002Fcode> (0x00..0x17), plus virtual 24..31 (\u003Ccode>control24osc.py:375-376\u003C\u002Fcode>, \u003Ccode>:631\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>C24-Engine writes names at \u003Ccode>aa = 0x20+n\u003C\u002Fcode> (\u003Ccode>motore.py:962-975\u003C\u002Fcode>) and the two centre &quot;channel bar&quot; displays at \u003Ccode>0x1D\u003C\u002Fcode>\u002F\u003Ccode>0x1E\u003C\u002Fcode> (\u003Ccode>motore.py:558-565\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>On the ProControl, \u003Ccode>00..07\u003C\u002Fcode> is the top row and \u003Ccode>20..27\u003C\u002Fcode> the bottom row, 8 characters each (issue #12; procontrol-linux \u003Ccode>docs\u002Fdisplays.md\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>What \u003Ccode>0x00+n\u003C\u002Fcode> versus \u003Ccode>0x20+n\u003C\u002Fcode> means on a Control|24 (two layers, two rows, or aliases) is unknown.\u003C\u002Fstrong>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003Cli>\u003Ccode>long_scribble\u003C\u002Fcode> spreads a 96-character message over all strips (\u003Ccode>control24osc.py:338-345\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Meters\u003C\u002Fstrong> — encoding decoded, physical segment count unknown:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>F0 13 01 10 ch hi lo F7\u003C\u002Fcode>, with ch 0..23 left and 0x20+ch right; bus meters start at 24\u002F56 (\u003Ccode>control24osc.py:540-606\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>The value is a \u003Cstrong>thermometer bitmask\u003C\u002Fstrong> over 14 bits (two 7-bit bytes): n segments lit is the n lowest bits, \u003Ccode>hi\u003C\u002Fcode> = mask&gt;&gt;7, \u003Ccode>lo\u003C\u002Fcode> = mask&amp;0x7F. ReaControl's 15-step table is at \u003Ccode>:552-568\u003C\u002Fcode>; C24-Engine builds the mask the same way, at most 14 bits, and refreshes at 25 Hz (\u003Ccode>motore.py:913-928\u003C\u002Fcode>; \u003Ccode>Camilla24\u002Fcamilla_engine.py:92\u003C\u002Fcode>, &quot;the C24 bitmask holds up to 14&quot;).\u003C\u002Fli>\n\u003Cli>Whether the desk decays a lit meter on its own (as the X-Touch does) is unknown.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>Counter \u002F timecode\u003C\u002Fstrong> — decoded:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>F0 13 01 30 19 dots d8 … d1 F7\u003C\u002Fcode>: 8 seven-segment digits, the \u003Cstrong>rightmost first\u003C\u002Fstrong> (d8 … d1 with d1 the leftmost), then F7 (\u003Ccode>control24osc.py:382-425\u003C\u002Fcode>, \u003Ccode>:459-520\u003C\u002Fcode>; \u003Ccode>motore.py:616-631\u003C\u002Fcode>; \u003Ccode>HUI-OS\u002FHUI_OS.py:612-626\u003C\u002Fcode>; \u003Ccode>Calculator24\u002FCalculator24.py:83-112\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>A segment byte has \u003Cstrong>bit 6 = a, 5 = b, 4 = c, 3 = d, 2 = e, 1 = f, 0 = g\u003C\u002Fstrong>: C24-Engine draws \u003Ccode>0\u003C\u002Fcode> as 0x7E, \u003Ccode>1\u003C\u002Fcode> as 0x30, \u003Ccode>-\u003C\u002Fcode> as 0x01, \u003Ccode>E\u003C\u002Fcode> as 0x4F and \u003Ccode>r\u003C\u002Fcode> as 0x05 (\u003Ccode>motore.py:616-619\u003C\u002Fcode>; \u003Ccode>Calculator24\u002FCalculator24.py:27-31\u003C\u002Fcode>). The library's table covers 0–9 and A b C d E F H h J L o P r S U Y Z -.\u003C\u002Fli>\n\u003Cli>The \u003Ccode>dots\u003C\u002Fcode> byte: \u003Cstrong>bit k lights the dot after the k-th digit from the left\u003C\u002Fstrong> (bit 0 the leftmost). The rightmost digit has no dot bit. C24-Engine's calculator shows &quot;12.5&quot; with dots = 0x40, and its clock puts the minutes dot at bit 5 (\u003Ccode>Calculator24\u002FCalculator24.py:83-93\u003C\u002Fcode>; \u003Ccode>HUI-OS\u002FHUI_OS.py:612-621\u003C\u002Fcode>; \u003Ccode>motore.py:621-626\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>The ProControl uses address \u003Ccode>0x09\u003C\u002Fcode> and device byte \u003Ccode>00\u003C\u002Fcode> instead of \u003Ccode>0x19\u003C\u002Fcode> and \u003Ccode>01\u003C\u002Fcode> (procontrol-linux \u003Ccode>docs\u002Fdisplays.md\u003C\u002Fcode>).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>System and diagnostic family \u003Ccode>F0 13 xx 70 …\u003C\u002Fcode>\u003C\u002Fstrong> — dangerous:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Device-name write to the desk's memory: \u003Ccode>F0 13 01 70 00 6E &lt;8 chars&gt; F7\u003C\u002Fcode> (C24-Engine \u003Ccode>motore.py:566-574\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>On the ProControl the same family reads firmware memory (\u003Ccode>70 00\u003C\u002Fcode> comm processor, \u003Ccode>70 01\u003C\u002Fcode> faders) and produced spurious touch events (procontrol-linux \u003Ccode>docs\u002Fprotocol.md\u003C\u002Fcode>, top section; \u003Ccode>docs\u002Ffader-touch-recovery-2026-09-21.md\u003C\u002Fcode>).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The desk sends this family itself:\u003C\u002Fstrong> in \u003Ccode>r15b.pcap\u003C\u002Fcode> a Control|24 1.08 sent \u003Ccode>F0 13 01 70 01 4F 4B F7\u003C\u002Fcode> and \u003Ccode>F0 13 01 70 01 0A 0D F7\u003C\u002Fcode> (packets 735 and 737). What they mean is not known.\u003C\u002Fli>\n\u003Cli>A host must never send this family; the library refuses to encode it.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cstrong>The device byte\u003C\u002Fstrong> after \u003Ccode>F0 13\u003C\u002Fcode> is \u003Ccode>01\u003C\u002Fcode> for the Control|24 and \u003Ccode>00\u003C\u002Fcode> for the ProControl main unit (issue #22 trace \u003Ccode>f0 13 00 40 …\u003C\u002Fcode>; procontrol-linux \u003Ccode>docs\u002Fprotocol.md\u003C\u002Fcode>). This is a per-device personality, not a constant.\u003C\u002Fp>\n\u003Ch3 id=\"sources-and-licences\">Sources and licences\u003C\u002Fh3>\n\u003Cp>ReaControl24 and procontrol-linux are GPL-3.0-or-later, like this library. C24-Engine-Renewed carries no licence, so only facts come from it (byte layouts, bit orders, timings), never code or tables. libdiginet is a new implementation written from this description, not a port: the byte grammar lives in \u003Ccode>diginet.ksy\u003C\u002Fcode>, each field cited to the sources above.\u003C\u002Fp>\n\u003Chr>\n\u003Ch2 id=\"checked-against-c24-engine-renewed\">Checked against C24-Engine-Renewed\u003C\u002Fh2>\n\u003Cp>C24-Engine-Renewed drives a real Control|24 from a working host, so each point it touches is\ncompared here with what this library does. Citations are to its files at commit \u003Ccode>94d7a3b\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Point\u003C\u002Fth>\n\u003Cth>This library\u003C\u002Fth>\n\u003Cth>C24-Engine-Renewed\u003C\u002Fth>\n\u003Cth>Result\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Header layout\u003C\u002Ftd>\n\u003Ctd>16 bytes: \u003Ccode>numbytes\u003C\u002Fcode>, sum, send counter, ack counter, retry, command, count\u003C\u002Ftd>\n\u003Ctd>The same offsets: length at 0, counter at 4, ack counter at 8, command at 14, count at 15, the rest zero (\u003Ccode>motore.py:516-545\u003C\u002Fcode>, \u003Ccode>:930-960\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Minimum frame\u003C\u002Ftd>\n\u003Ctd>Padded to a 46-byte payload\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>bytearray(max(46, length))\u003C\u002Fcode> (\u003Ccode>motore.py:520\u003C\u002Fcode>, \u003Ccode>:538\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Command codes\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>00\u003C\u002Fcode> data, \u003Ccode>a0\u003C\u002Fcode> ACK, \u003Ccode>e2\u003C\u002Fcode> wake, \u003Ccode>e3\u003C\u002Fcode> disconnect, \u003Ccode>e0\u003C\u002Fcode>\u002F\u003Ccode>e1\u003C\u002Fcode> announce\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>00\u003C\u002Fcode>, \u003Ccode>a0\u003C\u002Fcode>, \u003Ccode>e2\u003C\u002Fcode>, \u003Ccode>e3\u003C\u002Fcode>; the announce is told apart by its length 0x32 (\u003Ccode>motore.py:930-960\u003C\u002Fcode>, \u003Ccode>:1069-1079\u003C\u002Fcode>, \u003Ccode>:1191-1198\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Header sum\u003C\u002Ftd>\n\u003Ctd>Body sum by default, zero selectable\u003C\u002Ftd>\n\u003Ctd>Always zero, and the desk works (\u003Ccode>motore.py:516-545\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees that zero is accepted. Whether the desk checks a non-zero sum (R3) stays open\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Send counter\u003C\u002Ftd>\n\u003Ctd>Advanced by the number of commands in a data frame; ACK, \u003Ccode>e2\u003C\u002Fcode> and \u003Ccode>e3\u003C\u002Fcode> do not advance it (as ReaControl does)\u003C\u002Ftd>\n\u003Ctd>Advanced by one for every frame, ACKs and pings included (\u003Ccode>motore.py:516-545\u003C\u002Fcode>, \u003Ccode>:940-960\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>The desk works with both, so it does not check the step (A8)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>ACK\u003C\u002Ftd>\n\u003Ctd>Every desk data frame is ACKed at once, with the desk's send counter in the ack counter\u003C\u002Ftd>\n\u003Ctd>The same, for every desk frame other than an announce (\u003Ccode>motore.py:1200-1201\u003C\u002Fcode>, \u003Ccode>:940-948\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Command count\u003C\u002Ftd>\n\u003Ctd>A fader move \u003Ccode>B0 tt MSB (20+tt) LSB\u003C\u002Fcode> counts one; a frame's count is its commands\u003C\u002Ftd>\n\u003Ctd>A fader move is sent with count 1, a batch with the number of its messages (\u003Ccode>motore.py:585-590\u003C\u002Fcode>, \u003Ccode>:531-545\u003C\u002Fcode>). Its calculator sends single messages with count 0x23 (\u003Ccode>Calculator24\u002FCalculator24.py:112\u003C\u002Fcode>, \u003Ccode>:155\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees (A7): the desk's own frames count one per move, 2 and 5 in batched frames. The desk seems not to check the count against the body\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Frames in flight\u003C\u002Ftd>\n\u003Ctd>One host data frame in flight, retransmitted with the retry field after 100 ms\u003C\u002Ftd>\n\u003Ctd>No wait for ACKs, no retransmission, the retry field never set or read (\u003Ccode>motore.py:516-550\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>No conflict: the desk takes more than one frame in flight (R8)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Frame size\u003C\u002Ftd>\n\u003Ctd>At most 48 commands and 314 bytes\u003C\u002Ftd>\n\u003Ctd>Up to 51 commands and 461 bytes (\u003Ccode>motore.py:962-984\u003C\u002Fcode>), and 100 commands (\u003Ccode>:1036-1067\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>No conflict: the library's limits are inside what the desk takes (R8)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Display pacing\u003C\u002Ftd>\n\u003Ctd>2 ms between frames that carry display writes\u003C\u002Ftd>\n\u003Ctd>2 ms after each display batch (\u003Ccode>motore.py:546-550\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Keepalive\u003C\u002Ftd>\n\u003Ctd>Body \u003Ccode>00\u003C\u002Fcode>, count 1, every 10 s on its own clock\u003C\u002Ftd>\n\u003Ctd>An empty data frame, count 0, every 1 s (\u003Ccode>motore.py:951-960\u003C\u002Fcode>, \u003Ccode>:1023-1034\u003C\u002Fcode>); every 2.5 s in the calculator (\u003Ccode>Calculator24\u002FCalculator24.py:78-81\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Both forms work. The desk's own timeout (R5) stays open\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Offline detection\u003C\u002Ftd>\n\u003Ctd>25 s with no frame from the desk\u003C\u002Ftd>\n\u003Ctd>4 s, with its 1 s pings keeping the desk talking (\u003Ccode>motore.py:404\u003C\u002Fcode>, \u003Ccode>:1028-1033\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>No conflict: each interval is longer than that host's keepalive\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Wake\u003C\u002Ftd>\n\u003Ctd>One \u003Ccode>e2\u003C\u002Fcode>, 3 s for its \u003Ccode>a0\u003C\u002Fcode>, then 10 s for an \u003Ccode>e1\u003C\u002Fcode> naming this host, the announce period being up to 9.4 s\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>e2\u003C\u002Fcode> five times 5 ms apart, then 200 ms, then drive the desk (\u003Ccode>motore.py:1172-1189\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>No conflict\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Announce\u003C\u002Ftd>\n\u003Ctd>Version from body byte 15, name from byte 24, 9 bytes each (ReaControl's layout)\u003C\u002Ftd>\n\u003Ctd>Version from byte 14 (10 bytes, cut at the first NUL), name from byte 24 (8 bytes) (\u003Ccode>motore.py:1191-1198\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>The name agrees. Byte 14 is the \u003Ccode>v\u003C\u002Fcode> of \u003Ccode>v1.08\u003C\u002Fcode> in every Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Host MAC in \u003Ccode>e1\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Searched for in the leading bytes (A6)\u003C\u002Ftd>\n\u003Ctd>Not read\u003C\u002Ftd>\n\u003Ctd>Measured at body offset 0 on a Control|24 (A6)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Running status\u003C\u002Ftd>\n\u003Ctd>Expanded; a data byte with no status in force is reported and skipped\u003C\u002Ftd>\n\u003Ctd>Expanded; such a byte is skipped (\u003Ccode>motore.py:1100-1137\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Fader\u003C\u002Ftd>\n\u003Ctd>10-bit position, MSB = v&gt;&gt;3, LSB = (v&amp;7)&lt;&lt;4\u003C\u002Ftd>\n\u003Ctd>14-bit, MSB = v&gt;&gt;7, LSB = v&amp;0x7F, both ways (\u003Ccode>motore.py:585-590\u003C\u002Fcode>, \u003Ccode>:1345-1372\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees: the same bytes, with the low 4 bits used\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Touch\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>90 09 (40|tt)\u003C\u002Fcode> touched, \u003Ccode>90 09 tt\u003C\u002Fcode> released\u003C\u002Ftd>\n\u003Ctd>The same (\u003Ccode>motore.py:1249-1259\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>V-Pot and jog\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>B0 (40|tt) dd\u003C\u002Fcode> and \u003Ccode>B0 5C dd\u003C\u002Fcode>, three bytes, delta = dd − 64\u003C\u002Ftd>\n\u003Ctd>The same (\u003Ccode>motore.py:1374-1391\u003C\u002Fcode>); the jog is handed on with its sign inverted relative to the V-Pots\u003C\u002Ftd>\n\u003Ctd>Agrees on the bytes. Which way of turning is dd &gt; 64 stays open (M1)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Ring LEDs\u003C\u002Ftd>\n\u003Ctd>LED 1 = bit 6 of the address byte; \u003Ccode>b1\u003C\u002Fcode> bits 6..0 = LEDs 2..8; \u003Ccode>b2\u003C\u002Fcode> bits 6..0 = LEDs 9..15\u003C\u002Ftd>\n\u003Ctd>The same (\u003Ccode>motore.py:633-643\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees (A1)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Meter bits\u003C\u002Ftd>\n\u003Ctd>n segments = the n lowest of 14 bits; \u003Ccode>hi\u003C\u002Fcode> = mask&gt;&gt;7, \u003Ccode>lo\u003C\u002Fcode> = mask&amp;0x7F\u003C\u002Ftd>\n\u003Ctd>The same, at most 14 bits (\u003Ccode>motore.py:913-926\u003C\u002Fcode>; \u003Ccode>Camilla24\u002Fcamilla_engine.py:92\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees (A2). The number of LEDs (R7) stays open\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Seven-segment bits\u003C\u002Ftd>\n\u003Ctd>Was bit 0 = a ... bit 6 = g\u003C\u002Ftd>\n\u003Ctd>Bit 6 = a ... bit 0 = g: \u003Ccode>0\u003C\u002Fcode> = 0x7E, \u003Ccode>1\u003C\u002Fcode> = 0x30, \u003Ccode>-\u003C\u002Fcode> = 0x01, \u003Ccode>E\u003C\u002Fcode> = 0x4F (\u003Ccode>motore.py:616-619\u003C\u002Fcode>; \u003Ccode>Calculator24\u002FCalculator24.py:27-31\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Contradicted; fixed.\u003C\u002Fstrong> \u003Ccode>diginet_seg7\u003C\u002Fcode> now gives bit 6 = a (A3)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Counter digit order\u003C\u002Ftd>\n\u003Ctd>Rightmost digit first on the wire\u003C\u002Ftd>\n\u003Ctd>The same (\u003Ccode>motore.py:621-631\u003C\u002Fcode>; \u003Ccode>HUI-OS\u002FHUI_OS.py:612-626\u003C\u002Fcode>; \u003Ccode>Calculator24\u002FCalculator24.py:99-112\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees (A4)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Counter dots\u003C\u002Ftd>\n\u003Ctd>Was bit k = the k-th digit on the wire, the leftmost dot refused\u003C\u002Ftd>\n\u003Ctd>Bit k = the k-th digit from the left, the rightmost digit with no bit: &quot;12.5&quot; is dots 0x40 (\u003Ccode>Calculator24\u002FCalculator24.py:83-93\u003C\u002Fcode>; \u003Ccode>HUI-OS\u002FHUI_OS.py:612-621\u003C\u002Fcode>; \u003Ccode>motore.py:621-626\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Contradicted; fixed.\u003C\u002Fstrong> \u003Ccode>DIGINET_COUNTER_DOT(d)\u003C\u002Fcode> is now bit d, and a dot on the rightmost digit is refused (A5)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Automation LEDs\u003C\u002Ftd>\n\u003Ctd>write 0x40, touch 0x20, latch 0x10, trim 0x08, read 0x04\u003C\u002Ftd>\n\u003Ctd>The same values in use (\u003Ccode>motore.py:777-779\u003C\u002Fcode>); the docstring at \u003Ccode>:438-440\u003C\u002Fcode> still has touch and trim swapped, as its code comment explains\u003C\u002Ftd>\n\u003Ctd>Agrees\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Counter-mode LEDs\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>F0 13 01 20 19 bits F7\u003C\u002Fcode>, bits passed through\u003C\u002Ftd>\n\u003Ctd>timecode 0x20, feet+frames 0x10, bars|beats 0x08, min:sec 0x40 (\u003Ccode>motore.py:339-343\u003C\u002Fcode>, \u003Ccode>:449-470\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees; the values are now in &quot;Buttons and LEDs&quot;\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Scribble strips\u003C\u002Ftd>\n\u003Ctd>4 characters at any address the caller names\u003C\u002Ftd>\n\u003Ctd>Names at \u003Ccode>0x20+n\u003C\u002Fcode>, the centre displays at \u003Ccode>0x1D\u003C\u002Fcode>\u002F\u003Ccode>0x1E\u003C\u002Fcode> (\u003Ccode>motore.py:558-564\u003C\u002Fcode>, \u003Ccode>:962-978\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees. What \u003Ccode>0x00+n\u003C\u002Fcode> is beside \u003Ccode>0x20+n\u003C\u002Fcode> (R6) stays open\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>0x70\u003C\u002Fcode> family\u003C\u002Ftd>\n\u003Ctd>Refused by the encoder, the body check and the frame builder\u003C\u002Ftd>\n\u003Ctd>Writes the desk's name with \u003Ccode>F0 13 01 70 00 6E &lt;8 chars&gt; F7\u003C\u002Fcode>, taken from a Pro Tools capture (\u003Ccode>motore.py:566-573\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Agrees that the family writes the desk's memory\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch2 id=\"assumptions\">Assumptions\u003C\u002Fh2>\n\u003Cp>Each point below is a choice libdiginet makes in one constant or one function, so a capture\nthat contradicts it is a one-line change. Where C24-Engine-Renewed decides the point, the\nlibrary follows it; a capture on our own unit is still the last word.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>#\u003C\u002Fth>\n\u003Cth>Point\u003C\u002Fth>\n\u003Cth>What the library does\u003C\u002Fth>\n\u003Cth>Evidence\u003C\u002Fth>\n\u003Cth>Still to check on the desk\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>A1\u003C\u002Ftd>\n\u003Ctd>Ring LED order inside \u003Ccode>b1\u003C\u002Fcode>\u002F\u003Ccode>b2\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Bit 6 = LED 2 ... bit 0 = LED 8 in \u003Ccode>b1\u003C\u002Fcode>; bit 6 = LED 9 ... bit 0 = LED 15 in \u003Ccode>b2\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>C24-Engine-Renewed (\u003Ccode>motore.py:633-643\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Light LED 2 alone on strip 1 **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A2\u003C\u002Ftd>\n\u003Ctd>Meter bit order\u003C\u002Ftd>\n\u003Ctd>Bit 0 of \u003Ccode>lo\u003C\u002Fcode> is the bottom segment; n segments lit = the n lowest bits of the 14\u003C\u002Ftd>\n\u003Ctd>C24-Engine-Renewed (\u003Ccode>motore.py:913-926\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>R7 **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A3\u003C\u002Ftd>\n\u003Ctd>Seven-segment bits\u003C\u002Ftd>\n\u003Ctd>Bit 6 = a, 5 = b, 4 = c, 3 = d, 2 = e, 1 = f, 0 = g\u003C\u002Ftd>\n\u003Ctd>C24-Engine-Renewed (\u003Ccode>motore.py:616-619\u003C\u002Fcode>; \u003Ccode>Calculator24\u002FCalculator24.py:27-31\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Write \u003Ccode>8\u003C\u002Fcode> with one bit cleared at a time\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A4\u003C\u002Ftd>\n\u003Ctd>Counter digit order\u003C\u002Ftd>\n\u003Ctd>The API takes the digits left to right as shown; the wire carries them right to left (\u003Ccode>d8 … d1\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>C24-Engine-Renewed (\u003Ccode>motore.py:621-631\u003C\u002Fcode>; \u003Ccode>Calculator24\u002FCalculator24.py:99-112\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Write \u003Ccode>12345678\u003C\u002Fcode> **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A5\u003C\u002Ftd>\n\u003Ctd>Counter dots\u003C\u002Ftd>\n\u003Ctd>Bit k lights the dot after the k-th digit from the left (k = 0 the leftmost). A data byte holds 7 bits, so the rightmost digit has no dot and a dot there is refused\u003C\u002Ftd>\n\u003Ctd>C24-Engine-Renewed (\u003Ccode>Calculator24\u002FCalculator24.py:83-93\u003C\u002Fcode>; \u003Ccode>HUI-OS\u002FHUI_OS.py:612-621\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>Write \u003Ccode>12.5\u003C\u002Fcode>, then each dot bit alone **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A6\u003C\u002Ftd>\n\u003Ctd>Host MAC in the \u003Ccode>e1\u003C\u002Fcode> body\u003C\u002Ftd>\n\u003Ctd>Searched for anywhere in the 15 unknown leading bytes; the probe prints the offset where it was found\u003C\u002Ftd>\n\u003Ctd>None: no source reads it\u003C\u002Ftd>\n\u003Ctd>M1 **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A7\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>numcommands\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Counts commands as the encoders make them: a fader move (\u003Ccode>B0 tt MSB (20+tt) LSB\u003C\u002Fcode>) is one command\u003C\u002Ftd>\n\u003Ctd>ReaControl24 and C24-Engine-Renewed send it so (\u003Ccode>motore.py:585-590\u003C\u002Fcode>); the desk also accepts a count that does not match (\u003Ccode>Calculator24\u002FCalculator24.py:112\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>M1 logs of desk frames: what the desk puts there **Measured (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>A8\u003C\u002Ftd>\n\u003Ctd>Counter sequence\u003C\u002Ftd>\n\u003Ctd>A data frame carries the sender's counter after adding its own commands; ACK, \u003Ccode>e2\u003C\u002Fcode> and \u003Ccode>e3\u003C\u002Fcode> carry it unchanged; an ACK echoes the peer's counter\u003C\u002Ftd>\n\u003Ctd>ReaControl24 steps by commands, C24-Engine-Renewed by frames, and the desk works with both, so the step is not checked\u003C\u002Ftd>\n\u003Ctd>M1: whether the desk's ACK echoes our counter exactly, which the transport matches on **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Ch2 id=\"open-questions-and-how-to-settle-them-on-a-unit\">Open questions and how to settle them on a unit\u003C\u002Fh2>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>#\u003C\u002Fth>\n\u003Cth>Unknown\u003C\u002Fth>\n\u003Cth>Resolve by\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>R1\u003C\u002Ftd>\n\u003Ctd>Which desk it is. The **Control\u003C\u002Ftd>\n\u003Ctd>24** (2001, 10BASE-T, Focusrite preamps) or the **C\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R2\u003C\u002Ftd>\n\u003Ctd>Link speed and duplex. A modern NIC may not negotiate 10\u002Fhalf\u003C\u002Ftd>\n\u003Ctd>M0: \u003Ccode>\u002Fsys\u002Fclass\u002Fnet\u002F*\u002Fspeed,duplex\u003C\u002Fcode>; if bad, use a cheap 10\u002F100 unmanaged switch. Keep it off the REAC interfaces\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R3\u003C\u002Ftd>\n\u003Ctd>Checksum: does the C24 verify host→desk? (Neyrinck mentions &quot;CRC&quot;)\u003C\u002Ftd>\n\u003Ctd>M1: send the sum, and if no ACK, retry with zero. Also check that desk frames in the M0 pcap match sum16\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R4\u003C\u002Ftd>\n\u003Ctd>Does the motor fight a touched cap? Does it go back to its set-point on release?\u003C\u002Ftd>\n\u003Ctd>M1 extension: command 25 %, grab the cap, move it, release, with no echo. Feel and film. Then repeat with an echo on release **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R5\u003C\u002Ftd>\n\u003Ctd>Keepalive and offline timeout. Hosts that work send one every 1 s (C24-Engine-Renewed) or 10 s with the fix above (procontrol-linux)\u003C\u002Ftd>\n\u003Ctd>M1: stretch the keepalive interval 10 → 20 → 40 s and watch \u003Ccode>e1\u003C\u002Fcode>→\u003Ccode>e0\u003C\u002Fcode> and the &quot;Offline&quot; display **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R6\u003C\u002Ftd>\n\u003Ctd>Scribble \u003Ccode>0x00+n\u003C\u002Fcode> versus \u003Ccode>0x20+n\u003C\u002Fcode>: layers, rows or aliases; the glyph set beyond ASCII\u003C\u002Ftd>\n\u003Ctd>M1: write different text to both, then sweep bytes 0x00–0x7F onto one strip **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R7\u003C\u002Ftd>\n\u003Ctd>Meter segment count (the mask holds 14), and whether a lit meter decays on its own\u003C\u002Ftd>\n\u003Ctd>Send each bitmask step to strip 1 and count the LEDs; hold a value for 10 s **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R8\u003C\u002Ftd>\n\u003Ctd>ACK window and frame size. C24-Engine-Renewed sends without waiting for ACKs and up to 461-byte bodies, so a burst is accepted; whether the desk retries or drops frames under such a burst is not known\u003C\u002Ftd>\n\u003Ctd>Load test in the probe: N commands per frame, measure retries and display garbling **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R9\u003C\u002Ftd>\n\u003Ctd>Running status and multi-command frames from the desk\u003C\u002Ftd>\n\u003Ctd>The M1 logs; the parser handles both **Measured (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R10\u003C\u002Ftd>\n\u003Ctd>Gaps in the button map, and which keys light\u003C\u002Ftd>\n\u003Ctd>Press every key during M1 (decode log); walk LEDs over zones 0x18–0x1D (Davitekk's LED-finder approach, reimplemented) **Measured 2026-10-29 (Control\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R11\u003C\u002Ftd>\n\u003Ctd>Does Pro Tools do anything we skip (the &quot;cryptography&quot; in Neyrinck's note)?\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Optional:\u003C\u002Fstrong> capture ReaControl24 driving the desk once, on a separate test machine, never a production one. That is the only way to see a known-working host conversation; Pro Tools on any machine would be even better\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R12\u003C\u002Ftd>\n\u003Ctd>Analogue section: which buttons reach the preamps, and whether they are safe\u003C\u002Ftd>\n\u003Ctd>A physical check on the unit; never inferred from LEDs\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R13\u003C\u002Ftd>\n\u003Ctd>Two hosts on one desk (a shadow console, or a second daemon)\u003C\u002Ftd>\n\u003Ctd>One daemon owning each desk, plus the transport refusing to send \u003Ccode>e2\u003C\u002Fcode> when it sees a foreign \u003Ccode>e1\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R14\u003C\u002Ftd>\n\u003Ctd>The daemon binary loses \u003Ccode>cap_net_raw\u003C\u002Fcode> on reinstall (\u003Ccode>install\u003C\u002Fcode> does not keep file capabilities)\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>getcap\u003C\u002Fcode> check in \u003Ccode>ExecStartPre\u003C\u002Fcode>; \u003Ccode>%caps\u003C\u002Fcode> in the RPM. \u003Cstrong>The packages (#35):\u003C\u002Fstrong> \u003Ccode>diginet-probe\u003C\u002Fcode> and \u003Ccode>diginet-packet-helper\u003C\u002Fcode> carry \u003Ccode>%caps(cap_net_raw=ep)\u003C\u002Fcode> in the RPM and get \u003Ccode>setcap\u003C\u002Fcode> in the Debian postinst, and \u003Ccode>tests\u002Fpackage-check.sh\u003C\u002Fcode> asks \u003Ccode>getcap\u003C\u002Fcode> for both after a clean install. A host that should hold no capability takes its socket from \u003Ccode>diginet-packet-helper\u003C\u002Fcode> instead (\u003Ccode>diginet_packet_open_by_helper()\u003C\u002Fcode>, &quot;One socket, every interface&quot; above)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R15\u003C\u002Ftd>\n\u003Ctd>What a desk keeps across sessions\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Measured 2026-10-29:\u003C\u002Fstrong> a new session (e2 taken) blanks every scribble, LED, ring, meter and the counter; an e3 leaves all of it shown under &quot;Offline&quot;; faders stay where they are either way. A host repaints everything each time the session comes online, faders included\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>R16\u003C\u002Ftd>\n\u003Ctd>Motor faders on an old desk\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>Measured 2026-10-29:\u003C\u002Fstrong> on one unit, most motors climbed when the desk was cold and up to nine stopped climbing (still pulling down) after an hour on, while every position sensor and touch read true; one fader's sensor jumped (13 -&gt; 1023 within 30 ms) and its motor hunted at full speed. The hardware wants its tracks cleaned and its motor supply checked; a host treats every fader the same\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n",[27,30,33],{"slug":28,"title":29},"","libdiginet",{"slug":31,"title":32},"building","Building libdiginet",{"slug":34,"title":35},"probe","diginet-probe: the first test on a real desk",1793954996231]