diff --git a/cli/src/mpftp/cli.py b/cli/src/mpftp/cli.py index a105c0a..e2dcf7d 100755 --- a/cli/src/mpftp/cli.py +++ b/cli/src/mpftp/cli.py @@ -1547,7 +1547,8 @@ def cmd_bootloader(ns: argparse.Namespace) -> None: def cmd_usb_restart(ns: argparse.Namespace) -> None: - """Re-enumerate an ESP32's USB node after `bootloader` wedges it (mpftp#31). + """Re-enumerate an ESP32's USB node after `bootloader` wedges it, or restart a + CH343 USB-serial bridge whose COM port answers "Incorrect function" (mpftp#31). No board connection and no elevation: this drives the SYSTEM scheduled task that does the privileged part. --status first, always -- the task on a given @@ -1556,7 +1557,7 @@ def cmd_usb_restart(ns: argparse.Namespace) -> None: from . import espusb if ns.list: - out({"devices": espusb.espressif_devices()}) + out({"devices": espusb.restartable_devices()}) return if ns.status or not ns.instance: state = espusb.task_state() @@ -2549,10 +2550,19 @@ def build_parser() -> argparse.ArgumentParser: ur = sub.add_parser( "usb-restart", - help="Windows: re-enumerate an ESP32 USB node wedged by `bootloader` (no elevation)", + help="Windows: restart an ESP32 USB node wedged by `bootloader`, or a CH343 serial port that " + "answers 'Incorrect function' (no elevation)", + ) + ur.add_argument( + "--instance", + help="Device instance id, e.g. 'USB\\VID_303A&PID_4003\\' (Espressif) or " + "'USB\\VID_1A86&PID_55D3\\' (CH343)", + ) + ur.add_argument( + "--list", + action="store_true", + help="List attached VID_303A (Espressif) and VID_1A86 (CH343) devices and their instance ids", ) - ur.add_argument("--instance", help="Device instance id, e.g. 'USB\\VID_303A&PID_4003\\'") - ur.add_argument("--list", action="store_true", help="List attached VID_303A devices and their instance ids") ur.add_argument( "--status", action="store_true", diff --git a/cli/src/mpftp/espusb.py b/cli/src/mpftp/espusb.py index dd7ecb2..23540ca 100644 --- a/cli/src/mpftp/espusb.py +++ b/cli/src/mpftp/espusb.py @@ -1,4 +1,5 @@ -"""No-UAC recovery for an ESP32 wedged on its own USB (mpftp#31). +"""No-UAC recovery for an ESP32 wedged on its own USB, or a CH343 port that +answers "Incorrect function" (mpftp#31). An ESP32-S3 whose only serial line is its native USB -- the LilyGO T-Embed -- cannot be reset by the host: DTR and RTS go nowhere, so `mpftp bootloader` asks @@ -34,13 +35,14 @@ TASK_NAME = "mpftp-restart-esp-usb" -#: The one shape a request may take: the composite parent of an Espressif -#: (VID_303A) USB device. Anchored at both ends, so a trailing ``; calc`` or a -#: second path component cannot ride along, and the ``\\`` after the product id +#: The one shape a request may take: the parent node of an Espressif (VID_303A) +#: or WCH CH343 USB-serial bridge (VID_1A86) device. Anchored at both ends, so a +#: 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&PID_[0-9A-Fa-f]{4}\\[0-9A-Za-z&_.\-]+$") +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 @@ -108,14 +110,16 @@ def log_path() -> Path: # ------------------------------------------------------------------ devices -def espressif_devices() -> list[dict[str, str]]: - """Every attached VID_303A composite parent, so ids are found and not guessed. +def restartable_devices() -> list[dict[str, str]]: + """Every attached VID_303A (Espressif) or VID_1A86 (CH343) parent node, so ids + are found and not guessed. ``&MI_`` children are left out: they are what you must *not* restart. """ out = _ps( "Get-PnpDevice -PresentOnly " - "| Where-Object { $_.InstanceId -like 'USB\\VID_303A*' -and $_.InstanceId -notmatch '&MI_' } " + "| Where-Object { ($_.InstanceId -like 'USB\\VID_303A*' -or $_.InstanceId -like 'USB\\VID_1A86*') " + "-and $_.InstanceId -notmatch '&MI_' } " "| ForEach-Object { $_.InstanceId + '|' + $_.Status + '|' + $_.FriendlyName }" ) devices = [] @@ -135,8 +139,9 @@ def validate_instance_id(instance_id: str) -> str: raise ValueError(f"instance id is {len(candidate)} chars, limit is {MAX_REQUEST_CHARS}") if not INSTANCE_ID_RE.match(candidate): raise ValueError( - f"{instance_id!r} is not an Espressif USB instance id. Expected the composite " - f"parent of a VID_303A device, e.g. 'USB\\VID_303A&PID_4003\\3485186BFCAC0000' -- " + f"{instance_id!r} is not an Espressif or CH343 USB instance id. Expected the parent " + f"node of a VID_303A (Espressif) or VID_1A86 (WCH CH343) device, e.g. " + f"'USB\\VID_303A&PID_4003\\3485186BFCAC0000' or 'USB\\VID_1A86&PID_55D3\\5ABA052144' -- " f"not an &MI_ child, not another vendor, nothing appended. " f"`mpftp usb-restart --list` prints the attached ones." ) @@ -230,7 +235,7 @@ def _task_is_running() -> bool: def restart_device(instance_id: str, wait: float = _TASK_WAIT_SECS) -> dict[str, Any]: - """Restart one Espressif USB node through the task. No elevation here. + """Restart one Espressif or CH343 USB node through the task. No elevation here. Returns the task's exit code and the tail of its transcript. The exit codes are the script's: 0 restarted, 2 no request, 3 request refused, 4 no such @@ -286,9 +291,11 @@ def restart_device(instance_id: str, wait: float = _TASK_WAIT_SECS) -> dict[str, 1: "PowerShell itself failed before the script ran -- check the task's action.", 2: "the task found no request file. Something consumed it first, or the write did not land.", 3: "the request was refused as malformed. mpftp validates the same rule before writing, " - "so this means the file was changed between the two.", - 4: "no device with that instance id is attached. It may already have re-enumerated -- " - "look for 303A:1001 on a new COM number.", + "so this means the file was changed between the two -- or, for a CH343 (VID_1A86) id, " + "that the installed script predates CH343 support and an administrator needs to run " + "tools/windows/install-restart-esp-usb-task.ps1 again.", + 4: "no device with that instance id is attached. An Espressif board may already have " + "re-enumerated -- look for 303A:1001 on a new COM number.", 5: "the restart itself failed. The transcript has pnputil's own words.", 6: "the restart was accepted and the node came back not OK -- same instance id, still " "there, still broken, and no COM port behind it. The transcript names the Problem " diff --git a/cli/tests/esp_usb_doubles.ps1 b/cli/tests/esp_usb_doubles.ps1 index fa789e6..133b39d 100644 --- a/cli/tests/esp_usb_doubles.ps1 +++ b/cli/tests/esp_usb_doubles.ps1 @@ -19,7 +19,9 @@ param( [Parameter(Mandatory = $true)][string]$Script, [Parameter(Mandatory = $true)][string]$Scenario, [Parameter(Mandatory = $true)][string]$Calls, - [Parameter(Mandatory = $true)][string]$Log + [Parameter(Mandatory = $true)][string]$Log, + # Which device the script is asked to restart; an Espressif board by default. + [string]$InstanceId = 'USB\VID_303A&PID_4003\D0UB1E0000000000' ) $script:DoubleCalls = $Calls @@ -92,6 +94,6 @@ function pnputil.exe { } } -. $Script -InstanceId 'USB\VID_303A&PID_4003\D0UB1E0000000000' -LogPath $Log +. $Script -InstanceId $InstanceId -LogPath $Log # `exit` in a dot-sourced script returns here, with its code in $LASTEXITCODE. exit $LASTEXITCODE diff --git a/cli/tests/test_esp_usb_restart.py b/cli/tests/test_esp_usb_restart.py index 735fade..5c88dc4 100644 --- a/cli/tests/test_esp_usb_restart.py +++ b/cli/tests/test_esp_usb_restart.py @@ -18,6 +18,9 @@ one-line check ``-ne 1`` -> ``-lt 1`` 1 red, two devices in one request link checks made to return $null 4 red, every link case contents echoed on a link refusal 2 red, incl. the leak assertion + +CH343 support (2026-10-20) was shown the same way, by running these against the +script as it was on main: 5 red, all the CH343 ones. """ from __future__ import annotations @@ -37,7 +40,11 @@ TEMBED = r"USB\VID_303A&PID_4003\3485186BFCAC0000" ABSENT = r"USB\VID_303A&PID_4003\DEADBEEFDEADBEEF" -P4_UART = r"USB\VID_1A86&PID_55D3\5ABA052144" +CH343 = r"USB\VID_1A86&PID_55D3\5ABA052144" +CH343_ABSENT = r"USB\VID_1A86&PID_55D3\DEADBEEF00" +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" MI_CHILD = r"USB\VID_303A&PID_4003&MI_00\6&BFF214A&0&0000" @@ -50,11 +57,31 @@ def test_a_composite_parent_is_accepted(self): def test_surrounding_whitespace_is_trimmed_not_refused(self): self.assertEqual(espusb.validate_instance_id(f" {TEMBED}\n"), TEMBED) + def test_a_ch343_parent_is_accepted(self): + self.assertEqual(espusb.validate_instance_id(CH343), CH343) + def test_another_vendor_is_refused(self): - # The bench that found mpftp#31 had a second board whose UART must not - # be bounced; a VID check is what keeps it out. + # A VID check is what keeps every other bridge out. Only Espressif and + # WCH are on the list; FTDI and Silicon Labs are not. + for other in (FTDI, CP210X): + with self.assertRaises(ValueError): + espusb.validate_instance_id(other) + + def test_a_ch343_mi_child_is_refused(self): with self.assertRaises(ValueError): - espusb.validate_instance_id(P4_UART) + espusb.validate_instance_id(CH343_MI_CHILD) + + 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): + with self.assertRaises(ValueError): + espusb.validate_instance_id(bad) + + def test_the_vid_is_not_a_prefix_match(self): + # VID_1A86 followed by more hex digits is a different vendor id. + for bad in (r"USB\VID_1A860&PID_55D3\5ABA052144", r"USB\VID_303A1&PID_4003\1"): + with self.assertRaises(ValueError): + espusb.validate_instance_id(bad) def test_an_mi_child_is_refused(self): with self.assertRaises(ValueError): @@ -235,7 +262,22 @@ def test_an_empty_request_exits_2(self): self.assertEqual(self._run(" \n\n"), 2) def test_another_vendors_device_is_refused(self): - self.assertEqual(self._run(P4_UART + "\n"), 3) + self.assertEqual(self._run(FTDI + "\n"), 3) + self.assertEqual(self._run(CP210X + "\n"), 3) + + def test_a_ch343_id_passes_the_allow_list(self): + # Well-formed, absent: past the allow-list (not 3) and stopped at the + # presence check (4), device untouched. + self.assertEqual(self._run(CH343_ABSENT + "\n"), 4) + + def test_a_ch343_mi_child_is_refused(self): + self.assertEqual(self._run(CH343_MI_CHILD + "\n"), 3) + + def test_a_ch343_with_a_trailing_command_is_refused(self): + self.assertEqual(self._run(CH343 + "; calc\n"), 3) + + def test_a_ch343_and_an_espressif_in_one_request_are_refused(self): + self.assertEqual(self._run(CH343 + "\n" + TEMBED + "\n"), 3) def test_an_mi_child_is_refused(self): self.assertEqual(self._run(MI_CHILD + "\n"), 3) @@ -387,7 +429,7 @@ def setUpClass(cls): super().setUpClass() shutil.copy(cls.DOUBLES, espusb._win_to_local(cls.work) / "esp_usb_doubles.ps1") - def _play(self, scenario: str) -> tuple[int, list[str]]: + def _play(self, scenario: str, instance_id: str | None = None) -> tuple[int, list[str]]: calls = espusb._win_to_local(self.work) / "doubles.calls" log = espusb._win_to_local(self.work) / "doubles.log" # Delete before the run, both of them: a transcript left by the last @@ -402,7 +444,8 @@ def _play(self, scenario: str) -> tuple[int, list[str]]: "-Script", self.script, "-Scenario", scenario, "-Calls", self.work + r"\doubles.calls", - "-Log", self.work + r"\doubles.log"], + "-Log", self.work + r"\doubles.log", + *(["-InstanceId", instance_id] if instance_id else [])], capture_output=True, text=True, ) seen = calls.read_text(encoding="utf-8-sig").split("\n") if calls.exists() else [] @@ -449,6 +492,40 @@ def test_a_node_that_is_gone_afterwards_is_still_a_success(self): self.assertIn("re-enumerated as a different device", self._doubles_log()) +@unittest.skipIf(_powershell() is None, "needs Windows PowerShell (interop from WSL)") +class ACh343Restart(_PlaysScenarios, unittest.TestCase): + """A CH343 comes back under the same id; an Espressif board may not. + + Nothing about a USB-serial bridge re-enumerates into download mode, so the + script's "that id is gone, look for 303A:1001" success is the Espressif + rule only. A CH343 that is not there afterwards has no COM port behind it. + """ + + def test_a_healthy_ch343_is_restarted_once(self): + code, calls = self._play("healthy", CH343) + self.assertEqual(code, 0) + self.assertEqual(calls, ["pnputil /restart-device"]) + + def test_a_ch343_that_comes_back_not_ok_is_not_a_success(self): + code, _ = self._play("error-after", CH343) + self.assertEqual(code, 6) + + def test_a_ch343_that_is_gone_afterwards_is_not_a_success(self): + code, _ = self._play("gone-after", CH343) + self.assertEqual(code, 6) + self.assertNotIn("303A:1001", self._doubles_log()) + + def test_an_espressif_that_is_gone_afterwards_still_is(self): + # The control: the Espressif rule is unchanged. + code, _ = self._play("gone-after", TEMBED) + self.assertEqual(code, 0) + + def test_a_disabled_ch343_is_enabled_before_it_is_restarted(self): + code, calls = self._play("disabled", CH343) + self.assertEqual(code, 0) + self.assertEqual(calls, ["pnputil /enable-device", "pnputil /restart-device"]) + + @unittest.skipIf(_powershell() is None, "needs Windows PowerShell (interop from WSL)") class ItNeverLeavesANodeDisabled(_PlaysScenarios, unittest.TestCase): """The restart itself, run for real over pretend hardware. diff --git a/docs/agent-guide.md b/docs/agent-guide.md index e8500c6..f10a400 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -679,10 +679,30 @@ around it:** ```bash mpftp usb-restart --status # exits 1, and says why, if it would not work -mpftp usb-restart --list # attached VID_303A devices and their instance ids +mpftp usb-restart --list # attached VID_303A (Espressif) and VID_1A86 (CH343) devices and their instance ids mpftp usb-restart --instance 'USB\VID_303A&PID_4003\' +mpftp usb-restart --instance 'USB\VID_1A86&PID_55D3\' # a CH343 serial bridge ``` +The task covers two kinds of device, one per run and always by explicit +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. + +**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 +driver (`CH343SER`) binds straight to the `USB\VID_1A86&PID_55D3\` node +(there is no composite parent above it), so restarting that node stops and +restarts the driver instance, the same thing Device Manager's Disable and +Enable does. Find the id with `--list`, restart it, and the port should +answer again under the **same** instance id and COM number. A CH343 has no +download mode to re-enumerate into, so unlike an Espressif board a node that +is *gone* afterwards is a failure (exit 6), not a success. Whether a restart +clears every "Incorrect function" is not something this documentation can +promise: it reloads the driver, which is the usual cause, and if the port is +still refusing opens afterwards the bridge itself needs unplugging. + `--instance` exits non-zero when the restart did not leave a reachable board, and `taskResult` says which wall it hit: 4 no such device, 5 the restart itself failed, 6 the restart was accepted and the node came back not OK. A 6 is the @@ -704,7 +724,9 @@ success for *starting* a task, never for what it did, so read `mpftp usb-restart` read it for you. **It needs one elevated install before any of that works**, and an agent must -not try to do it. Copy `tools/windows/` to a plain Windows path — an elevated +not try to do it. The install copies the script, so a machine that installed +before CH343 support needs it run again: until then a CH343 id is refused with +exit 3. Copy `tools/windows/` to a plain Windows path — an elevated shell cannot reliably read `\\wsl.localhost\...`, and on this bench it could not read it at all — then ask the owner to run, once, in an administrator PowerShell: @@ -722,7 +744,8 @@ The task runs as SYSTEM, so it must never execute anything an ordinary account can write: the script is installed into `C:\Program Files\mpftp`, and changing it costs another elevated install. What you *do* write unprivileged is one instance id into `C:\ProgramData\mpftp\restart-esp-usb.target`, which the -script matches whole against an allow-list for a VID_303A composite parent, +script matches whole against an allow-list for a VID_303A (Espressif) or +VID_1A86 (CH343) parent node, never evaluates, and deletes on read. Name the device explicitly — the old task matched `USB\VID_303A*` and would have bounced every Espressif board on the bench together, which on this one is a second board mid-demo. diff --git a/tools/windows/install-restart-esp-usb-task.ps1 b/tools/windows/install-restart-esp-usb-task.ps1 index 8ca9d07..7c6d179 100644 --- a/tools/windows/install-restart-esp-usb-task.ps1 +++ b/tools/windows/install-restart-esp-usb-task.ps1 @@ -1,4 +1,4 @@ -# Install (or repair) the no-UAC ESP32 USB recovery: run this ONCE, elevated. +# Install (or repair) the no-UAC ESP32 / CH343 USB recovery: run this ONCE, elevated. # # Restarting a USB device node needs elevation, and an unattended session has # nobody to click a UAC prompt. The way round that is a scheduled task running @@ -172,8 +172,8 @@ $settings.AllowDemandStart = $true Register-ScheduledTask -TaskName $TaskName -Action $action -Principal $taskPrincipal ` -Settings $settings -Description ( - 'mpftp#31: restart one Espressif USB device node named by ' + $requestPath + - '. Recovers an ESP32-S3 wedged by machine.bootloader() without a UAC prompt.') -Force | Out-Null + 'mpftp#31: restart one Espressif or WCH CH343 USB device node named by ' + $requestPath + + '. Recovers an ESP32-S3 wedged by machine.bootloader(), or a CH343 port answering Incorrect function, without a UAC prompt.') -Force | Out-Null Write-Output "registered task '$TaskName' -> $target" # Let ordinary accounts start it, which is the entire point. This is the @@ -213,6 +213,17 @@ if ($LASTEXITCODE -ne 3) { } Write-Output "self-check a malformed instance id is refused with exit 3" +# The installed script must know the CH343 rule too: a well-formed id for a +# device that is not attached gets past the allow-list and stops at "no such +# device" (4). An older installed script would refuse it with 3. +& powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $target ` + -InstanceId 'USB\VID_1A86&PID_55D3\NOSUCHDEVICE0' -DryRun | Out-Null +if ($LASTEXITCODE -ne 4) { + Write-Error "self-check failed: a well-formed CH343 id for an absent device should exit 4, got $LASTEXITCODE" + exit 1 +} +Write-Output "self-check a CH343 instance id 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 @@ -237,7 +248,8 @@ Write-Output '' Write-Output 'Done. Nothing else needs elevation. To prove it on a board, as an ordinary user:' Write-Output '' Write-Output ' mpftp usb-restart --status' -Write-Output ' mpftp usb-restart --instance "USB\VID_303A&PID_4003\"' +Write-Output ' mpftp usb-restart --instance "USB\VID_303A&PID_4003\" (Espressif)' +Write-Output ' mpftp usb-restart --instance "USB\VID_1A86&PID_55D3\" (CH343)' Write-Output '' Write-Output "or without mpftp: write the instance id into $requestPath, then" Write-Output " schtasks /run /tn $TaskName" diff --git a/tools/windows/restart-esp-usb.ps1 b/tools/windows/restart-esp-usb.ps1 index aa60bef..9aa2b3e 100644 --- a/tools/windows/restart-esp-usb.ps1 +++ b/tools/windows/restart-esp-usb.ps1 @@ -1,4 +1,4 @@ -# Restart one Espressif USB device node, named by instance id. +# Restart one Espressif or WCH CH343 USB device node, named by instance id. # # This is the repair for the ESP32-S3 native-USB wedge described in # docs/agent-guide.md: after `mpftp bootloader` or a direct @@ -9,6 +9,16 @@ # chip is really presenting; a board sitting in ROM download mode then appears # as 303A:1001 on a new COM number. # +# It also covers the WCH CH343 USB-serial bridges (VID_1A86) that sit between +# the host and many boards' UART. A CH343 port can be left answering "Incorrect +# function" (Windows error 1) to every open, with the node showing Status OK. +# The vendor driver (CH343SER) binds directly to the USB\VID_1A86 node, which is +# not a composite parent, so restarting that node unloads and reloads the +# driver instance that holds the stuck state. Unlike an Espressif board, a +# CH343 does not change identity when it comes back: the same instance id and +# the same COM number are expected, and a node that is gone afterwards is a +# failure, not a re-enumeration (exit 6). +# # Restarting a device node needs elevation, so unattended sessions reach this # script through the scheduled task `mpftp-restart-esp-usb`, which runs it as # SYSTEM and which any account may start on demand without a UAC prompt. @@ -24,7 +34,7 @@ # # What an ordinary user may write is the *request file*, and it is data, never # code. One line, matched whole against a strict allow-list for an Espressif -# instance id, never evaluated, never concatenated into a command string -- +# (VID_303A) or CH343 (VID_1A86) instance id, never evaluated, never concatenated into a command string -- # pnputil is called with an argument array. Anything else is refused with a # non-zero exit and a logged sentence. The request is consumed (deleted) on # read, so a rejected or stale one is never replayed. @@ -92,14 +102,18 @@ if (-not $LogPath) { $LogPath = Join-Path $here 'restart-esp-usb.log' } -# Espressif's vendor ID, a 4-hex-digit product id, and a serial of the +# Espressif's vendor ID (303A) or WCH's (1A86), a 4-hex-digit product id, and a serial of the # characters Windows actually puts there. Anchored both ends, so a trailing # `; calc` or a second path component cannot ride along. The `\\` after the # 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. -$ALLOWED_INSTANCE_ID = '^USB\\VID_303A&PID_[0-9A-Fa-f]{4}\\[0-9A-Za-z&_.\-]+$' +# 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&_.\-]+$' +# Espressif boards change identity on a restart (ROM download mode); nothing else +# here does. +$ESPRESSIF_INSTANCE_ID = '^USB\\VID_303A&' $MAX_REQUEST_CHARS = 200 function Write-Log { @@ -236,7 +250,7 @@ if (-not $InstanceId) { } if ($InstanceId -notmatch $ALLOWED_INSTANCE_ID) { - Write-Log ('request refused: {0} is not an Espressif USB instance id. It must match {1} -- the composite parent of a VID_303A device, not an &MI_ child and not anything else.' -f ` + Write-Log ('request refused: {0} is not an Espressif or CH343 USB instance id. It must match {1} -- the parent node of a VID_303A (Espressif) or VID_1A86 (WCH CH343) device, not an &MI_ child and not anything else.' -f ` (Show-Refused $InstanceId), $ALLOWED_INSTANCE_ID) exit 3 } @@ -353,9 +367,17 @@ if ($after) { exit 6 } } else { - # Expected, and not a failure: a board that entered ROM download mode - # re-enumerates as a different device (303A:1001 on a new COM number). - Write-Log 'that instance id is gone -- the board has re-enumerated as a different device. Look for 303A:1001 on a new COM number.' + if ($InstanceId -match $ESPRESSIF_INSTANCE_ID) { + # Expected, and not a failure: a board that entered ROM download mode + # re-enumerates as a different device (303A:1001 on a new COM number). + Write-Log 'that instance id is gone -- the board has re-enumerated as a different device. Look for 303A:1001 on a new COM number.' + } else { + # A USB-serial bridge has no download mode to re-enumerate into. Same + # id, same COM number, or it did not recover. + Write-Log 'that instance id is gone after the restart. A CH343 comes back under the same id, so there is no COM port behind this; unplug it and plug it in again.' + Write-Log '--- restart-esp-usb done ---' + exit 6 + } } Write-Log '--- restart-esp-usb done ---' exit 0