Skip to content

Commit 22142e0

Browse files
committed
The S3 USB-wedge repair never ran: there is no Restart-PnpDevice, so pnputil does it
Found mid flash cycle on 2026-09-17. The helper and the scheduled task wrapping it both called a cmdlet Windows PowerShell does not have, failed on every device, and looked exactly like a board that would not come back. The script uses pnputil /restart-device now, falls back to Disable+Enable, restarts the composite parent rather than its MI_ children, and says when it is not elevated. The guide says how to read the task's real result.
1 parent 4f6ffb3 commit 22142e0

2 files changed

Lines changed: 50 additions & 3 deletions

File tree

docs/agent-guide.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,21 @@ To lose the prompt as well, an administrator can register a scheduled task
412412
that runs the restart with SYSTEM privileges and let ordinary accounts start
413413
it; an agent must not create that task itself.
414414

415+
**Check what that task actually runs, because there is no `Restart-PnpDevice`.**
416+
Windows PowerShell's PnpDevice module ships Get-, Enable- and Disable-PnpDevice
417+
and nothing else, so the obvious-looking one-liner fails with
418+
`CommandNotFoundException` on every device. The task then completes, reports
419+
`LastTaskResult 1`, and from the outside is indistinguishable from a board that
420+
refused to come back — which is how the 2026-09-17 pin-move run spent its S3
421+
half on a repair that had never worked. `schtasks /run` returns SUCCESS for
422+
*starting* the task, never for what it did: read
423+
`Get-ScheduledTaskInfo -TaskName <name> | Select LastTaskResult` instead.
424+
`pnputil /restart-device <instance-id>` is the mechanism that works;
425+
`tools/windows/restart-esp-usb.ps1` uses it now, with Disable+Enable as the
426+
fallback, and says so when it is not elevated rather than blaming the board.
427+
The task registered on this machine still carries the old one-liner and needs
428+
re-registering by hand.
429+
415430
### Ctrl-C is not an interrupt inside `atexit`
416431

417432
CircuitPython does not arm Ctrl-C as an interrupt character while an `atexit`

tools/windows/restart-esp-usb.ps1

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,61 @@
1414
# powershell.exe -NoProfile -Command "Start-Process powershell -Verb RunAs `
1515
# -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','<this file>'"
1616
#
17+
# **There is no `Restart-PnpDevice`.** Windows PowerShell's PnpDevice module
18+
# ships Get-, Enable- and Disable-PnpDevice and nothing else, so a script that
19+
# calls Restart-PnpDevice fails with CommandNotFoundException on every device
20+
# and exits 1 -- while a scheduled task wrapping it reports LastTaskResult 1
21+
# and looks, from the outside, exactly like a board that refused to restart.
22+
# Found the expensive way on 2026-09-17, mid flash cycle. `pnputil
23+
# /restart-device` is the real mechanism; Disable+Enable is the fallback for a
24+
# Windows older than 2004.
1725
[CmdletBinding()]
1826
param(
1927
# Espressif's vendor ID. Narrow it (e.g. 'USB\VID_303A&PID_4001*') when two
2028
# boards are attached and only one should be restarted.
2129
[string]$Match = 'USB\VID_303A*'
2230
)
2331

24-
$devices = Get-PnpDevice -PresentOnly | Where-Object { $_.InstanceId -like $Match }
32+
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
33+
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
34+
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
35+
Write-Output "not elevated -- every restart below will fail with Access is denied"
36+
}
37+
38+
# Restart the composite parent, not its MI_ children: re-enumerating the
39+
# parent brings the interfaces with it, and a child whose parent is about to
40+
# vanish reports a confusing failure of its own.
41+
$devices = Get-PnpDevice -PresentOnly |
42+
Where-Object { $_.InstanceId -like $Match -and $_.InstanceId -notmatch '&MI_' }
2543
if (-not $devices) {
2644
Write-Output "no device matching $Match is attached"
2745
exit 1
2846
}
47+
48+
$failed = 0
2949
foreach ($d in $devices) {
3050
Write-Output ("restarting {0} [{1}]" -f $d.InstanceId, $d.Status)
51+
$out = & pnputil.exe /restart-device $d.InstanceId 2>&1
52+
if ($LASTEXITCODE -eq 0 -and ($out -join ' ') -notmatch 'Failed to restart') {
53+
Write-Output " ok (pnputil)"
54+
continue
55+
}
56+
Write-Output (" pnputil: {0}" -f (($out | Where-Object { $_ -match '\S' }) -join '; '))
3157
try {
32-
Restart-PnpDevice -InstanceId $d.InstanceId -Confirm:$false -ErrorAction Stop
33-
Write-Output " ok"
58+
Disable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false -ErrorAction Stop
59+
Start-Sleep -Milliseconds 700
60+
Enable-PnpDevice -InstanceId $d.InstanceId -Confirm:$false -ErrorAction Stop
61+
Write-Output " ok (disable/enable)"
3462
} catch {
3563
Write-Output (" failed: {0}" -f $_.Exception.Message)
64+
$failed++
3665
}
3766
}
67+
3868
Start-Sleep -Seconds 3
3969
Get-PnpDevice -PresentOnly |
4070
Where-Object { $_.InstanceId -like 'USB\VID_303A*' } |
4171
Select-Object Status, InstanceId |
4272
Format-Table -AutoSize | Out-String | Write-Output
73+
74+
exit $(if ($failed) { 1 } else { 0 })

0 commit comments

Comments
 (0)