docs(series4): the Micromate USB host is FTDI + CDC-ACM only -- not Prolific

A Micromate on an RX55 was unreachable from THOR.  Isolated layer by layer with
bridges/mm_probe.py, and the answer turned out to be the cable.

The USB-A port is a HOST port with a fixed driver set, identical in both firmware
lines:

    CDCACM  FTDI  MFS (mass storage)  PRINTER  HUB  HC  USBH

11 FTDISER strings and 22 CDCACM strings in each image, and ZERO matches for
prolific / pl2303 / cp210 / ch34 / silabs in either.  So a Prolific PL2303 cable
(VID 067b) cannot work with a Micromate on any firmware -- no driver, no
enumeration, no serial path.  It needs an FTDI cable (VID 0403) or CDC-ACM.

The isolation method is the part worth keeping.  Eliminated in turn: public IP
(static APN), firewall (probe got TCP connect in 268 ms from a whitelisted
source), network path, baud, the unit's modem-type setting, and THOR itself
(the probe bypasses it).  Then the decisive pair -- a Linux box was swapped in
for the unit on the SAME cable and modem:

  * POLL arrived at the serial side byte-perfect
  * a canned reply came back over TCP, full round trip in 700 ms

So the modem, PAD config, firewall and network are all clean, and the only
element that changed is what sits at the end of the cable.  Substituting a
known-good device converts "the unit is not answering" into "the unit is not
receiving" -- very different problems.  Adds scratch/fake_unit.py for that.

Flagged: this explains the BENCH setup conclusively.  It does NOT establish the
cause of the 2026-09-22 field outage, which reportedly worked at first and
degraded -- a wrong cable would not do that.  Kept separate until that unit's
cable is identified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
This commit is contained in:
2026-09-25 17:48:36 -04:00
co-authored by Claude Opus 5
parent 7b2aa871bd
commit 84ca10eb3c
2 changed files with 87 additions and 0 deletions
+54
View File
@@ -2067,6 +2067,60 @@ leak is reproducible and attributable to a specific UI action.
3. **Log what changed, not what did not.** `timed out: False` 178 times is 3. **Log what changed, not what did not.** `timed out: False` 178 times is
noise; the five `True` entries are the signal, and they are buried. noise; the five `True` entries are the signal, and they are buried.
## The Micromate's USB host supports FTDI and CDC-ACM only — not Prolific
**Established 2026-09-25**, by isolating a bench modem link layer by layer and
then confirming against both firmware images.
### The USB-A port is a HOST port with a fixed driver set
```
usbHostDelete_CDCACM CDC-ACM — the standard USB-serial class
usbHostDelete_FTDI FTDI chips (FTDISER handlers, set_line_coding, etc.)
usbHostDelete_MFS mass storage (thumb drives)
usbHostDelete_PRINTER printers
usbHostDelete_HUB / HC / USBH
```
**Identical in `11.0CB` and `11.0BD`** — 11 `FTDISER` strings and 22 `CDCACM`
strings in each, and **zero** matches for `prolific`, `pl2303`, `cp210`, `ch34`
or `silabs` in either. The firmware line makes no difference here.
⚠ **So a Prolific PL2303 cable (VID `067b`) cannot work with a Micromate.** The
unit has no driver, never enumerates the device, and the serial path simply does
not exist. Use an **FTDI** cable (VID `0403`) or a CDC-ACM one.
### How this was isolated — the method is reusable
A Micromate on an RX55 was unreachable from THOR. Rather than guess, each layer
was eliminated in turn with `bridges/mm_probe.py`:
| layer | test | result |
|---|---|---|
| unit's public IP | static APN `mw01.VZWSTATIC` | ruled out |
| firewall / trusted IPs | probe from a whitelisted source | **TCP connect ok, 268 ms** |
| network path | same | ruled out |
| baud | 115200 both ends | ruled out |
| unit modem-type setting | set back to `generic` | ruled out |
| THOR | probe bypasses it entirely and still failed | ruled out |
| **modem TCP→serial** | swapped the unit for a Linux box on the same cable | **POLL arrived byte-perfect** |
| **modem serial→TCP** | that box answered with a canned POLL reply | **round trip in 700 ms** |
The last two are the decisive pair. With a laptop standing in for the unit on
the *same cable and modem*, the full round trip works — so the modem, the PAD
configuration, the firewall and the network are all clean, and the only element
that changed is what sits at the end of the cable.
`scratch/fake_unit.py`-style substitution is worth remembering: **replace the
device with something known-good and re-run the same test.** It converts "the
unit is not answering" into "the unit is not receiving", which are very
different problems.
⚠ This explains the **bench** setup conclusively. Whether it explains the
2026-09-22 field outage is **not** established — that unit reportedly worked at
first and degraded, which a wrong cable would not do. Treat them as separate
until the field unit's cable is identified.
## Known-good RV55 config for a Micromate (2026-09-25) ## Known-good RV55 config for a Micromate (2026-09-25)
Read off a unit **deployed and working for months**. This is the reference to Read off a unit **deployed and working for months**. This is the reference to
+33
View File
@@ -0,0 +1,33 @@
"""Pretend to be a Micromate on a serial port: log what arrives, reply to POLL.
Proves the modem's return path (serial -> TCP) independently of the real unit.
"""
import os, select, sys, termios, time
path, baud = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 115200
B = {9600: termios.B9600, 38400: termios.B38400, 115200: termios.B115200}[baud]
fd = os.open(path, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
a = termios.tcgetattr(fd)
a[0] = a[1] = a[3] = 0
a[2] = termios.CS8 | termios.CREAD | termios.CLOCAL
a[4] = a[5] = B
a[6] = list(a[6]); a[6][termios.VMIN] = 0; a[6][termios.VTIME] = 0
termios.tcsetattr(fd, termios.TCSANOW, a)
termios.tcflush(fd, termios.TCIOFLUSH)
# A real POLL probe reply, captured from UM12947 on 2026-09-24.
REPLY = bytes.fromhex("0200c5a4000000000000300000000000000099") + b"\x03"
print(f"fake unit on {path} @ {baud}; will answer any inbound frame", flush=True)
while True:
r, _, _ = select.select([fd], [], [], 1.0)
if not r:
continue
data = os.read(fd, 4096)
if not data:
continue
ts = time.strftime("%H:%M:%S")
print(f"{ts} IN {len(data):3} B {data.hex(' ')}", flush=True)
time.sleep(0.02)
os.write(fd, REPLY)
print(f"{ts} OUT {len(REPLY):3} B {REPLY.hex(' ')} <- canned POLL reply", flush=True)