From d205ab2330de3fa3bba887cd5eab80042a177acb Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:55:49 -0500 Subject: [PATCH] usb-restart accepts an ESP32-S3 in ROM download mode A chip in ROM download mode has no USB serial number, so Windows names its node after the chip's MAC address, colons included (USB\VID_303A&PID_0009\ 48:27:E2:E9:B0:10). The allow rule refused it. The serial's character class now admits ':' in both the CLI and the task script; everything else stays as strict: one device per run, parent node only, no wildcards or other characters. The installer self-checks the new shape. --- cli/src/mpftp/espusb.py | 9 ++++--- cli/tests/test_esp_usb_restart.py | 27 +++++++++++++++++++ docs/agent-guide.md | 6 +++++ .../windows/install-restart-esp-usb-task.ps1 | 11 ++++++++ tools/windows/restart-esp-usb.ps1 | 9 ++++--- 5 files changed, 56 insertions(+), 6 deletions(-) diff --git a/cli/src/mpftp/espusb.py b/cli/src/mpftp/espusb.py index 23540ca..a579145 100644 --- a/cli/src/mpftp/espusb.py +++ b/cli/src/mpftp/espusb.py @@ -40,9 +40,12 @@ #: trailing ``; calc`` or a second path component cannot ride along, and the #: ``\\`` after the product id #: rejects ``&MI_00`` children -- restarting the parent brings its interfaces -#: with it. The task's PowerShell side enforces this same rule independently; -#: this copy is here so a bad id is refused before it is ever written down. -INSTANCE_ID_RE = re.compile(r"^USB\\VID_(303A|1A86)&PID_[0-9A-Fa-f]{4}\\[0-9A-Za-z&_.\-]+$") +#: with it. The serial may contain colons because a chip in ROM download mode +#: has no USB serial number, so Windows uses its MAC address +#: (``USB\VID_303A&PID_0009\48:27:E2:E9:B0:10``). The task's PowerShell side +#: enforces this same rule independently; this copy is here so a bad id is +#: refused before it is ever written down. +INSTANCE_ID_RE = re.compile(r"^USB\\VID_(303A|1A86)&PID_[0-9A-Fa-f]{4}\\[0-9A-Za-z&_.:\-]+$") MAX_REQUEST_CHARS = 200 _TASK_WAIT_SECS = 45.0 diff --git a/cli/tests/test_esp_usb_restart.py b/cli/tests/test_esp_usb_restart.py index 5c88dc4..92f1d2e 100644 --- a/cli/tests/test_esp_usb_restart.py +++ b/cli/tests/test_esp_usb_restart.py @@ -42,6 +42,9 @@ ABSENT = r"USB\VID_303A&PID_4003\DEADBEEFDEADBEEF" CH343 = r"USB\VID_1A86&PID_55D3\5ABA052144" CH343_ABSENT = r"USB\VID_1A86&PID_55D3\DEADBEEF00" +ROM_MODE = r"USB\VID_303A&PID_0009\48:27:E2:E9:B0:10" +ROM_MODE_ABSENT = r"USB\VID_303A&PID_0009\DE:AD:BE:EF:00:01" +ROM_MODE_MI_CHILD = r"USB\VID_303A&PID_0009&MI_00\48:27:E2:E9:B0:10" CH343_MI_CHILD = r"USB\VID_1A86&PID_55D2&MI_00\6&1B2C3D4&0&0000" FTDI = r"USB\VID_0403&PID_6001\A50285BI" CP210X = r"USB\VID_10C4&PID_EA60\0001" @@ -71,6 +74,19 @@ def test_a_ch343_mi_child_is_refused(self): with self.assertRaises(ValueError): espusb.validate_instance_id(CH343_MI_CHILD) + def test_a_rom_download_mode_id_with_colons_is_accepted(self): + # A chip in ROM download mode has no USB serial; Windows uses its MAC. + self.assertEqual(espusb.validate_instance_id(ROM_MODE), ROM_MODE) + + def test_a_colon_cannot_smuggle_an_mi_child_or_a_wildcard(self): + for bad in (ROM_MODE_MI_CHILD, ROM_MODE + "*", ROM_MODE + "?", + r"USB\VID_303A&PID_0009\48:27:*", r"USB\VID_303A&PID_0009\48:27:E2:E9:B0:1?", + r"USB\VID_303A&PID_0009\*:*", ROM_MODE + "; calc", ROM_MODE + " calc", + ROM_MODE + '"', ROM_MODE + "'", ROM_MODE + "\\x", r"USB\VID_303A&PID_0009\:\x", + ROM_MODE + "\n" + ROM_MODE): + with self.assertRaises(ValueError, msg=repr(bad)): + espusb.validate_instance_id(bad) + def test_a_ch343_with_a_command_or_wildcard_is_refused(self): for bad in (CH343 + "; calc", CH343 + '" ; calc ; "', r"USB\VID_1A86&PID_55D3\*", r"USB\VID_1A86*", CH343 + "\n" + CH343): @@ -273,6 +289,17 @@ def test_a_ch343_id_passes_the_allow_list(self): def test_a_ch343_mi_child_is_refused(self): self.assertEqual(self._run(CH343_MI_CHILD + "\n"), 3) + def test_a_rom_download_mode_id_with_colons_passes_the_allow_list(self): + # Past the allow-list (not 3), stopped at the presence check (4). + self.assertEqual(self._run(ROM_MODE_ABSENT + "\n"), 4) + + def test_a_colon_id_cannot_smuggle_an_mi_child_or_a_wildcard(self): + for bad in (ROM_MODE_MI_CHILD, ROM_MODE_ABSENT + "*", + ROM_MODE_ABSENT + "?", r"USB\VID_303A&PID_0009\DE:AD:*", r"USB\VID_303A&PID_0009\*:*", + ROM_MODE_ABSENT + "; calc", ROM_MODE_ABSENT + " calc", ROM_MODE_ABSENT + '"'): + with self.subTest(bad=bad): + self.assertEqual(self._run(bad + "\n"), 3) + def test_a_ch343_with_a_trailing_command_is_refused(self): self.assertEqual(self._run(CH343 + "; calc\n"), 3) diff --git a/docs/agent-guide.md b/docs/agent-guide.md index f10a400..912e75b 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -689,6 +689,12 @@ instance id: an Espressif board on its native USB (`VID_303A`), and a WCH CH343 USB-serial bridge (`VID_1A86`), the UART port on the P4 and many other boards. Nothing else is accepted. +An Espressif board sitting in ROM download mode is covered too. It has no USB +serial number, so Windows names the node after the chip's MAC address, colons +included: `USB\VID_303A&PID_0009\48:27:E2:E9:B0:10`. `--list` prints it that +way and `--instance` accepts it. A machine that installed the task before this +needs the install run again; until then a colon id is refused with exit 3. + **A CH343 port that answers "Incorrect function".** When a COM port behind a CH343 fails every open with Windows error 1, "Incorrect function", the node usually still reads `Status OK` and `mpftp ports` still lists it. The vendor diff --git a/tools/windows/install-restart-esp-usb-task.ps1 b/tools/windows/install-restart-esp-usb-task.ps1 index 7c6d179..802bb33 100644 --- a/tools/windows/install-restart-esp-usb-task.ps1 +++ b/tools/windows/install-restart-esp-usb-task.ps1 @@ -224,6 +224,17 @@ if ($LASTEXITCODE -ne 4) { } Write-Output "self-check a CH343 instance id passes the allow-list (exit 4, no such device)" +# And the ROM download mode shape: no USB serial number, so Windows names the +# node after the chip's MAC address, colons included. An older installed script +# refuses it with 3. +& powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $target ` + -InstanceId 'USB\VID_303A&PID_0009\DE:AD:BE:EF:00:01' -DryRun | Out-Null +if ($LASTEXITCODE -ne 4) { + Write-Error "self-check failed: a well-formed ROM download mode id (MAC with colons) for an absent device should exit 4, got $LASTEXITCODE" + exit 1 +} +Write-Output "self-check a ROM download mode instance id (MAC with colons) passes the allow-list (exit 4, no such device)" + if (-not (Test-Path -LiteralPath $keep)) { Write-Error "self-check failed: $keep is missing, so the request directory could be emptied and converted into a junction" exit 1 diff --git a/tools/windows/restart-esp-usb.ps1 b/tools/windows/restart-esp-usb.ps1 index 9aa2b3e..e869abd 100644 --- a/tools/windows/restart-esp-usb.ps1 +++ b/tools/windows/restart-esp-usb.ps1 @@ -108,9 +108,12 @@ if (-not $LogPath) { # product id is deliberate: it admits the composite *parent* and rejects its # `&MI_00` children -- re-enumerating the parent brings the interfaces with it, # and a child whose parent is about to vanish reports a confusing failure of -# its own. The vendor alternation is the only thing WCH added: the rest of the -# rule is unchanged, and still names one device. -$ALLOWED_INSTANCE_ID = '^USB\\VID_(303A|1A86)&PID_[0-9A-Fa-f]{4}\\[0-9A-Za-z&_.\-]+$' +# its own. The vendor alternation is the only thing WCH added, and the colon in +# the serial is the only thing ROM download mode added: a chip in that mode has no +# USB serial number, so Windows uses the chip's MAC address, with colons +# (USB\VID_303A&PID_0009\48:27:E2:E9:B0:10). Neither widens the rule beyond one +# device: no `*`, `?`, space, quote or backslash is admitted. +$ALLOWED_INSTANCE_ID = '^USB\\VID_(303A|1A86)&PID_[0-9A-Fa-f]{4}\\[0-9A-Za-z&_.:\-]+$' # Espressif boards change identity on a restart (ROM download mode); nothing else # here does. $ESPRESSIF_INSTANCE_ID = '^USB\\VID_303A&'