swd-tracer — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited swd-tracer (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Live, non-halting plotting of firmware variables read straight from MCU RAM over an ST-Link, à la ST-Studio / CubeMonitor — but driven by the agent. The MCP server already lists which tools exist; this skill encodes how to set the tracer up and drive it, and how to recover when the probe misbehaves.
Target: CrossPad r20 = STM32G0B1xx (Cortex-M0+) firmware in the CrossPad_STM32_r2 repo. Variables are resolved from the Debug ELF DWARF; pyOCD polls their RAM addresses while the core keeps running. (Cortex-M0+ has no ITM/SWO/DWT, so SWO/ITM "printf" is impossible — RAM polling is the mechanism.)
All operations go through one MCP tool, crosspad_trace, via its action: doctor · config_set · symbols · start · add · remove · status · read · save · device_state · ui · stop.
doctorcrosspad_trace action=doctorIt reports issues[] and whether a probe is connected. Resolve blocking issues (below) before start. If you're unsure what's configured, also run the read-only environment probe:
bash scripts/detect-env.shThe helper scripts are bundled next to this SKILL.md in scripts/. Resolve them relative to the skill directory — e.g. ~/.claude/skills/swd-tracer/scripts/… (global skill) or <crosspad-mcp>/skills/swd-tracer/scripts/… (repo / plugin).
The tracer needs four things in place. doctor / detect-env.sh tell you which are missing; fix only those.
PEP-668 locked). Create it and point config at it:
bash scripts/setup-venv.sh
crosspad_trace action=config_set key=pyocd_python value=$HOME/.local/share/crosspad-mcp/venv/bin/python~/.config/crosspad-mcp/config.json.Keys: stm_elf_path (the Debug ELF), pyocd_python, probe_serial (optional; only if multiple ST-Links), trace_dir (where .cptrace/CSV go).
crosspad_trace action=config_set key=stm_elf_path value=<repo>/build/Debug/CrossPad_STM32_r20.elf bash scripts/install-udev-rules.sh # sudo; then replug the ST-Link cmake --preset Debug && cmake --build build/DebugResolution order for every config value: config.json → env var → default. So config_set wins; CROSSPAD_STM_ELF / CROSSPAD_TRACE_PYTHON / CROSSPAD_PROBE_SERIAL / CROSSPAD_TRACE_DIR are fallbacks.
doctor → green? continue
symbols [query=…] → discover variable names (rich metadata: kind/dims/members)
start signals=[…] rate_hz=N → spawns the daemon + opens the localhost UI
ui → get the http://localhost:7373/ dashboard URL
read [max_points] [window_…] → downsampled series + per-signal stats (cheap; LLM-safe)
add / remove signals=[…] → edit the watched set on a LIVE trace (no restart)
status → device_state, sample_count, actual_fs, signals
save → export buffer to CSV
stop → end the trace (always frees the probe)rate_hz=0 means "as fast as the probe allows"; the real rate comes back as actual_fs (ST-Link V2 + pyOCD tops out ~480 Hz total across all ranges). Fewer/contiguous signals = higher Fs.
On every start (and whenever you start/restart a trace), reply with the `ui_url` as a clickable link — prominently, e.g.:
📊 Live dashboard: http://localhost:7373/ — click to open it in VS Code.
This is the primary way the user opens the view. With the recommended VS Code setting "workbench.externalUriOpeners": {"localhost:7373": "simpleBrowser"}, a click opens the dashboard inside VS Code (Simple Browser), not the system browser. Assume the driver may be seeing VS Code for the first time — give them the link every time, never make them hunt for it. The server is persistent, so once they click it the tab stays live across all later traces (it auto-reconnects).
The tool also has a safety net: with ui_open=vscode (default), if no client connects within ~30 s, it auto-opens the system browser as a fallback so a first-timer is never left looking at nothing. ui_open=browser opens the system browser immediately; ui_open=none disables both. Set the fallback delay with CROSSPAD_TRACE_OPEN_FALLBACK_MS.
| Form | Resolves to |
|---|---|
s_vbat_mv | a scalar global/static |
s_inputs[3] | array element (bounds-checked against the DWARF length) |
mat[1][2] | multi-dim element |
hpcd.Init.speed | nested struct member |
s_adc_raw / s_adc_raw[*] | whole array → expands to every element |
s_inputs[0:8] | half-open slice → elements 0..7 |
@0x40021000 | raw address (no DWARF) → one u32 at that address |
@0x50000010:u16 | typed raw read — u8 u16 u32 i8 i16 i32 f32 (default u32) |
@0x20000000:u8[16] | raw block → 16 consecutive elements (same 256 cap) |
@0x50000410:u16#5 | bit 5 of the value → 0/1 (e.g. one GPIO pin) |
@0x50000410:u16#3:0 | bit range [3:0] → field value 0..15 |
GPIOB_ODR&0x820 | masked field, normalized to the lowest set bit |
A #bit / #hi:lo / &mask suffix extracts part of the value (works on symbols and raw @addr). Trace several #bit specs on the same port to plot each pin separately as 0/1; they coalesce into one read.
Raw @address specs read MCU memory directly — peripheral registers (GPIOx->IDR, RCC->CR) or any RAM the ELF has no symbol for; they need no symbol so they work even if symbols can't find the name. Caution: reads hit the AHB while the core runs — avoid registers with read-side-effects (data registers that clear flags / pop FIFOs on read).
Expansion is capped at 256 elements. A spec that lands on an aggregate (struct with no trailing index) is reported as unresolved, not fatal. See reference/signals.md for the CrossPad-specific cheat sheet (pads, ADC channels, and the built-in g_trace_demo.* self-test signals).
crosspad_trace action=write writes=["@0x50000414:u16=0xFFFF","s_vbat_mv=4200"] — non-halting pokes by address or symbol. Allowed regions: SRAM, peripheral, PPB; the flash/Code region is blocked. Works live (during a trace) or standalone.
crosspad_trace action=call func="led_set" args=[3,16711680] confirm=true — call a firmware function (≤4 int args → r0-r3), returns r0 (ret_type decodes it). Halts the core briefly, masks interrupts, restores context and resumes. confirm:true is mandatory. Use for driving the firmware from the agent; avoid functions that block or never return (they hit the timeout and the call is rolled back).
If the firmware includes the demo module, g_trace_demo.demo_sine draws a clean sine — the fastest "is the whole pipeline alive?" check:
crosspad_trace action=start signals=["g_trace_demo.demo_sine","g_trace_demo.tick"] rate_hz=200
crosspad_trace action=read max_points=50| Symptom | Cause / fix |
|---|---|
doctor → no_probe_detected | ST-Link not on USB. Plug it in. If it vanished mid-session (gone from lsusb), a prior wedge knocked it off — physically replug it; software can't revive it. |
start returns device_state: error: no debug probe detected | Same — replug, re-run doctor. |
start → device_state: connect_timeout / daemon exits | Probe wedged in libusb. The daemon fails fast (≤8 s) + exits now; replug and retry. |
| Values frozen / every signal a flat line | Core was halted on connect. The daemon uses connect_mode=attach to avoid this; if you see it, the firmware itself may be in a fault/STOP. Check device_state. |
device_state: stop_suspected then probe_lost | MCU entered STOP (RAM unreadable) or the probe dropped. Brief STOP recovers automatically; persistent (>10 s) → daemon reports probe_lost and exits. |
| Out-of-bounds index silently "works" | It no longer does — s_adc_raw[31] on a [15] array is rejected. Use symbols to see real dims/count. |
| Deep STOP / low-power analysis | crosspad_trace action=device_state dumps PWR/RCC/SCB regs + decodes SLEEPDEEP/LPMS. |
Permission/USB errors despite lsusb showing it | Missing udev rules — run install-udev-rules.sh, replug. |
| Daemon won't die / port stuck | stop escalates SIGTERM→SIGKILL on the daemon; the UI server is persistent and keeps listening (it is NOT torn down). If a stale daemon lingers, pkill -9 -f swd_tracer.py. |
it stays up across start/stop cycles (showing a trace_end banner while idle), so an open tab survives between traces.
start waits for the daemon's first frame and reports the real state(running / connecting / error), not an optimistic guess.
stop before starting a different signal set,or use add/remove to edit the live set.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.