Serial monitor: bounded for you, persistent for the user
Opening a serial port serves two different purposes, and one script covers both.
| You debugging | The user watching | |
|---|---|---|
| Goal | capture output, act on it | keep a window open all session |
| Command | mon.ps1 -Seconds 10 |
mon.ps1 |
| Ends | on its own | on Ctrl+C |
Read the board yourself whenever you need to verify something — that is what bounded mode is for, and it returns a normal exit code.
The rule that matters most
Bounded when you run it; hand over the interactive one with a note.
mon.ps1 without -Seconds, and flash.ps1 without -NoMonitor, never
return. Running those in a tool call hangs the session while the user watches
nothing happen — the complaint that motivated this skill was literally "if you
start it, it takes forever." So:
- Debugging your own change →
mon.ps1 -Seconds 10. Bounded, safe, no handoff. - Upload inside a script →
flash.ps1 <sketch> -NoMonitor, which exits with a real status code. - The user wants to watch it themselves, now or later → don't launch it. Leave the exact command they can run whenever they want.
That last one is the part worth being deliberate about. After you set the scripts up or finish a change the user will want to observe, close with a short handoff — the command, and how to run it:
The serial monitor is ready whenever you want to check it:
! C:\path\to\scripts\mon.ps1In Claude Code the
!prefix runs it right in the session. Ctrl+C exits. If you added the profile shortcuts,monworks from any new terminal.
Adapt the wording, but keep the three parts: the literal command, how to launch it, and how to stop it. The user should never have to ask "so how do I see the output?"
Setup
Copy scripts/ next to the user's sketches, or leave it in the installed skill
folder and reference it by absolute path. Then confirm detection works — this
returns immediately:
. .\scripts\Find-BoardPort.ps1
Find-BoardPort -Explain
A COM port means you are ready. An empty result prints every COM port with its
vendor ID so the user can pick one manually with -Port.
Optional but this is usually the actual request — one-word access from any
directory. assets/profile_snippet.ps1 defines mon, flash, and boards.
Append it to $PROFILE, edit the $env:XIAO_TOOLS path at the top, and note
that the profile only loads in newly opened terminals.
# create the profile if absent, then append
if (-not (Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force }
Get-Content assets\profile_snippet.ps1 | Add-Content -Path $PROFILE -Encoding utf8
Usage the user should be told about
mon # monitor, port auto-detected
mon COM6 9600 # explicit port and baud
flash blink # compile + upload + monitor
flash blink -NoMonitor # upload only
boards # what is attached
And for your own verification, which returns:
.\scripts\mon.ps1 -Seconds 10 # read 10 s and exit
.\scripts\mon.ps1 -Seconds 10 -Send "ping" # send a line first
Bounded mode opens the port with DTR asserted, which resets the board, so output starts from a fresh boot banner rather than mid-stream.
Pitfalls
These each cost a real debugging session once.
The first "Serial Port (USB)" row is usually not your board. Dev machines tend to carry a permanently attached USB-UART bridge (CP210x, CH340, FTDI), and it frequently enumerates on a lower COM number, so a naive pick silently monitors the wrong device — a dead-quiet window with no error. Match on USB vendor ID instead:
303AEspressif,2341Arduino.Find-BoardPort.ps1does this. Note thatarduino-cli board listshows the XIAO asUnknownwith an empty FQBN column even when the esp32 core is installed, so FQBN matching alone will not find it.The COM number changes when the board is replugged. Observed jumping COM5 → COM6 across one unplug. Anything with a hard-coded port breaks the moment someone moves a cable, which is why detection runs on every launch.
arduino-cli monitorexits whenever the port drops. USB-Serial/JTAG is part of the ESP32-S3 itself rather than a separate bridge, so the port disappears on every reset and every upload. Plainarduino-cli monitortreats that as end-of-session.mon.ps1waits and reattaches, which is the only reason one invocation can cover a whole debugging session.An open monitor owns the port and uploads fail against it. There is no graceful sharing. Close the monitor with Ctrl+C, upload, reopen — or just use
flash.ps1, which sequences it correctly and says so when an upload fails while a monitor is holding the port.Attaching to an already-running board shows no boot banner. Everything printed from
setup()is long gone, so a monitor that opens onto silence looks broken when it is fine. Press the board's Reset button, or upload again, to replay startup. If the sketch only prints during setup, this is the difference between "no output" and "working".PowerShell 5.1 mangles non-ASCII in BOM-less UTF-8 scripts. It falls back to the ANSI codepage, so Korean, Japanese, and accented characters come out as mojibake — the script still runs, it just becomes unreadable. Save any
.ps1containing non-ASCII as UTF-8 with BOM:$utf8bom = New-Object System.Text.UTF8Encoding($true) $text = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8) [System.IO.File]::WriteAllText($path, $text, $utf8bom)The scripts here are pure ASCII and unaffected; this applies once someone translates the messages.
Serial.begin(115200); delay(3000);before the first print. USB CDC needs time to enumerate and early output is dropped without warning. A monitor that "misses the first lines" is usually this, not a monitor bug.The profile does not apply to already-open terminals. After editing
$PROFILE,monstays undefined in the current window. Open a new terminal or run. $PROFILE. Also note$PROFILEdiffers between Windows PowerShell 5.1 and PowerShell 7 — editing one leaves the other untouched.
Files
| Path | Purpose |
|---|---|
scripts/Find-BoardPort.ps1 |
vendor-ID port detection, dot-sourced by both scripts |
scripts/mon.ps1 |
reconnecting monitor; -Seconds N makes it bounded |
scripts/flash.ps1 |
compile + upload + monitor; -NoMonitor returns |
assets/profile_snippet.ps1 |
mon / flash / boards shortcuts for $PROFILE |
Related: xiao-esp32s3 for the compile/upload workflow and the pin map. Its
read_serial.ps1 is the same bounded reader as mon.ps1 -Seconds N; use
whichever is already installed.