crosspad-mcp-server 8.1.2 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +13 -0
- package/.claude-plugin/plugin.json +14 -0
- package/.mcp.json +9 -0
- package/README.md +95 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +381 -57
- package/dist/index.js.map +1 -1
- package/dist/tools/idf-flash.js +2 -2
- package/dist/tools/idf-flash.js.map +1 -1
- package/dist/tools/idf-monitor.d.ts +3 -1
- package/dist/tools/idf-monitor.js +19 -3
- package/dist/tools/idf-monitor.js.map +1 -1
- package/dist/tools/midi.js +20 -16
- package/dist/tools/midi.js.map +1 -1
- package/dist/tools/symbols.d.ts +3 -1
- package/dist/tools/symbols.js +31 -1
- package/dist/tools/symbols.js.map +1 -1
- package/dist/tools/trace-buffer.d.ts +40 -0
- package/dist/tools/trace-buffer.js +74 -0
- package/dist/tools/trace-buffer.js.map +1 -0
- package/dist/tools/trace-device.d.ts +10 -0
- package/dist/tools/trace-device.js +26 -0
- package/dist/tools/trace-device.js.map +1 -0
- package/dist/tools/trace-doctor.d.ts +43 -0
- package/dist/tools/trace-doctor.js +150 -0
- package/dist/tools/trace-doctor.js.map +1 -0
- package/dist/tools/trace-export.d.ts +4 -0
- package/dist/tools/trace-export.js +14 -0
- package/dist/tools/trace-export.js.map +1 -0
- package/dist/tools/trace-session.d.ts +118 -0
- package/dist/tools/trace-session.js +346 -0
- package/dist/tools/trace-session.js.map +1 -0
- package/dist/tools/trace-symbols.d.ts +24 -0
- package/dist/tools/trace-symbols.js +44 -0
- package/dist/tools/trace-symbols.js.map +1 -0
- package/dist/tools/trace-webui.d.ts +53 -0
- package/dist/tools/trace-webui.js +222 -0
- package/dist/tools/trace-webui.js.map +1 -0
- package/dist/utils/device.d.ts +5 -0
- package/dist/utils/device.js +43 -15
- package/dist/utils/device.js.map +1 -1
- package/dist/utils/exec.js +26 -0
- package/dist/utils/exec.js.map +1 -1
- package/dist/utils/userConfig.d.ts +13 -0
- package/dist/utils/userConfig.js +43 -0
- package/dist/utils/userConfig.js.map +1 -0
- package/package.json +12 -4
- package/skills/crosspad/SKILL.md +58 -0
- package/skills/crosspad/reference/faq.md +40 -0
- package/skills/crosspad/reference/install.md +84 -0
- package/skills/crosspad/reference/repos.md +29 -0
- package/skills/crosspad/reference/role-contributor.md +64 -0
- package/skills/crosspad/reference/role-fw-dev.md +44 -0
- package/skills/crosspad/reference/role-user.md +49 -0
- package/skills/crosspad/reference/tools.md +68 -0
- package/skills/crosspad/scripts/doctor.sh +65 -0
- package/skills/crosspad/scripts/setup.sh +53 -0
- package/skills/swd-tracer/SKILL.md +135 -0
- package/skills/swd-tracer/reference/signals.md +42 -0
- package/skills/swd-tracer/scripts/detect-env.sh +61 -0
- package/skills/swd-tracer/scripts/install-udev-rules.sh +25 -0
- package/skills/swd-tracer/scripts/setup-venv.sh +26 -0
- package/tracer/PROTOCOL.md +260 -0
- package/tracer/README.md +327 -0
- package/tracer/swd_tracer.py +1083 -0
- package/tracer/ui/index.html +834 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swd-tracer
|
|
3
|
+
description: Use when tracing/plotting STM32 firmware variables in real time over SWD (ST-Link) on the CrossPad r20 board (CrossPad_STM32_r20 repo, STM32G0B1xx), or when the SWD tracer / pyOCD environment needs setting up or repairing. Covers the doctor→config→symbols→start→read/ui→stop workflow, signal-spec syntax (arrays/structs/whole-array expansion), configuring all four environments (pyOCD venv, user config paths, ST-Link udev rules, the Debug ELF), and recovering from no-probe / wedged-probe / halted-core / MCU-STOP conditions. The MCP tool is `crosspad_trace`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CrossPad SWD real-time tracer
|
|
7
|
+
|
|
8
|
+
Live, **non-halting** plotting of firmware variables read straight from MCU RAM
|
|
9
|
+
over an ST-Link, à la ST-Studio / CubeMonitor — but driven by the agent. The MCP
|
|
10
|
+
server already lists *which* tools exist; this skill encodes *how* to set the
|
|
11
|
+
tracer up and drive it, and how to recover when the probe misbehaves.
|
|
12
|
+
|
|
13
|
+
**Target:** CrossPad r20 = **STM32G0B1xx (Cortex-M0+)** firmware in the
|
|
14
|
+
`CrossPad_STM32_r20` repo. Variables are resolved from the **Debug ELF** DWARF;
|
|
15
|
+
pyOCD polls their RAM addresses while the core keeps running. (Cortex-M0+ has no
|
|
16
|
+
ITM/SWO/DWT, so SWO/ITM "printf" is impossible — RAM polling is the mechanism.)
|
|
17
|
+
|
|
18
|
+
All operations go through one MCP tool, `crosspad_trace`, via its `action`:
|
|
19
|
+
`doctor · config_set · symbols · start · add · remove · status · read · save ·
|
|
20
|
+
device_state · ui · stop`.
|
|
21
|
+
|
|
22
|
+
## First move: always `doctor`
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
crosspad_trace action=doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It reports `issues[]` and whether a probe is connected. Resolve blocking issues
|
|
29
|
+
(below) before `start`. If you're unsure what's configured, also run the
|
|
30
|
+
read-only environment probe:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
bash scripts/detect-env.sh
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The helper scripts are bundled next to this SKILL.md in `scripts/`. Resolve them
|
|
37
|
+
relative to the skill directory — e.g. `~/.claude/skills/swd-tracer/scripts/…`
|
|
38
|
+
(global skill) or `<crosspad-mcp>/skills/swd-tracer/scripts/…` (repo / plugin).
|
|
39
|
+
|
|
40
|
+
## Configuring the four environments
|
|
41
|
+
|
|
42
|
+
The tracer needs four things in place. `doctor` / `detect-env.sh` tell you which
|
|
43
|
+
are missing; fix only those.
|
|
44
|
+
|
|
45
|
+
1. **pyOCD venv** — the daemon runs in a Python venv (system Python is usually
|
|
46
|
+
PEP-668 locked). Create it and point config at it:
|
|
47
|
+
```
|
|
48
|
+
bash scripts/setup-venv.sh
|
|
49
|
+
crosspad_trace action=config_set key=pyocd_python value=$HOME/.local/share/crosspad-mcp/venv/bin/python
|
|
50
|
+
```
|
|
51
|
+
2. **User config paths** — persisted to `~/.config/crosspad-mcp/config.json`.
|
|
52
|
+
Keys: `stm_elf_path` (the Debug ELF), `pyocd_python`, `probe_serial`
|
|
53
|
+
(optional; only if multiple ST-Links), `trace_dir` (where `.cptrace`/CSV go).
|
|
54
|
+
```
|
|
55
|
+
crosspad_trace action=config_set key=stm_elf_path value=<repo>/build/Debug/CrossPad_STM32_r20.elf
|
|
56
|
+
```
|
|
57
|
+
3. **ST-Link udev rules** (Linux) — needed for libusb access without root:
|
|
58
|
+
```
|
|
59
|
+
bash scripts/install-udev-rules.sh # sudo; then replug the ST-Link
|
|
60
|
+
```
|
|
61
|
+
4. **Debug ELF with symbols** — build it in the firmware repo so DWARF exists:
|
|
62
|
+
```
|
|
63
|
+
cmake --preset Debug && cmake --build build/Debug
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Resolution order for every config value: `config.json` → env var → default. So
|
|
67
|
+
`config_set` wins; `CROSSPAD_STM_ELF` / `CROSSPAD_TRACE_PYTHON` /
|
|
68
|
+
`CROSSPAD_PROBE_SERIAL` / `CROSSPAD_TRACE_DIR` are fallbacks.
|
|
69
|
+
|
|
70
|
+
## The trace loop
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
doctor → green? continue
|
|
74
|
+
symbols [query=…] → discover variable names (rich metadata: kind/dims/members)
|
|
75
|
+
start signals=[…] rate_hz=N → spawns the daemon + opens the localhost UI
|
|
76
|
+
ui → get the http://localhost:7373/ dashboard URL
|
|
77
|
+
read [max_points] [window_…] → downsampled series + per-signal stats (cheap; LLM-safe)
|
|
78
|
+
add / remove signals=[…] → edit the watched set on a LIVE trace (no restart)
|
|
79
|
+
status → device_state, sample_count, actual_fs, signals
|
|
80
|
+
save → export buffer to CSV
|
|
81
|
+
stop → end the trace (always frees the probe)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`rate_hz=0` means "as fast as the probe allows"; the real rate comes back as
|
|
85
|
+
`actual_fs` (ST-Link V2 + pyOCD tops out ~480 Hz total across all ranges).
|
|
86
|
+
Fewer/contiguous signals = higher Fs.
|
|
87
|
+
|
|
88
|
+
## Signal-spec syntax
|
|
89
|
+
|
|
90
|
+
| Form | Resolves to |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `s_vbat_mv` | a scalar global/static |
|
|
93
|
+
| `s_inputs[3]` | array element (bounds-checked against the DWARF length) |
|
|
94
|
+
| `mat[1][2]` | multi-dim element |
|
|
95
|
+
| `hpcd.Init.speed` | nested struct member |
|
|
96
|
+
| `s_adc_raw` / `s_adc_raw[*]` | **whole array** → expands to every element |
|
|
97
|
+
| `s_inputs[0:8]` | half-open slice → elements 0..7 |
|
|
98
|
+
|
|
99
|
+
Expansion is capped at 256 elements. A spec that lands on an aggregate (struct
|
|
100
|
+
with no trailing index) is reported as `unresolved`, not fatal. See
|
|
101
|
+
[reference/signals.md](reference/signals.md) for the CrossPad-specific cheat sheet
|
|
102
|
+
(pads, ADC channels, and the built-in `g_trace_demo.*` self-test signals).
|
|
103
|
+
|
|
104
|
+
## Quick self-test (no real inputs needed)
|
|
105
|
+
|
|
106
|
+
If the firmware includes the demo module, `g_trace_demo.demo_sine` draws a clean
|
|
107
|
+
sine — the fastest "is the whole pipeline alive?" check:
|
|
108
|
+
```
|
|
109
|
+
crosspad_trace action=start signals=["g_trace_demo.demo_sine","g_trace_demo.tick"] rate_hz=200
|
|
110
|
+
crosspad_trace action=read max_points=50
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Troubleshooting
|
|
114
|
+
|
|
115
|
+
| Symptom | Cause / fix |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `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. |
|
|
118
|
+
| `start` returns `device_state: error: no debug probe detected` | Same — replug, re-run `doctor`. |
|
|
119
|
+
| `start` → `device_state: connect_timeout` / daemon exits | Probe wedged in libusb. The daemon fails fast (≤8 s) + exits now; replug and retry. |
|
|
120
|
+
| 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`. |
|
|
121
|
+
| `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. |
|
|
122
|
+
| 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`. |
|
|
123
|
+
| Deep STOP / low-power analysis | `crosspad_trace action=device_state` dumps PWR/RCC/SCB regs + decodes SLEEPDEEP/LPMS. |
|
|
124
|
+
| Permission/USB errors despite `lsusb` showing it | Missing udev rules — run `install-udev-rules.sh`, replug. |
|
|
125
|
+
| 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`. |
|
|
126
|
+
|
|
127
|
+
## Notes
|
|
128
|
+
|
|
129
|
+
- The web UI is loopback-only (127.0.0.1). The dashboard server is **persistent**:
|
|
130
|
+
it stays up across start/stop cycles (showing a `trace_end` banner while idle),
|
|
131
|
+
so an open tab survives between traces.
|
|
132
|
+
- `start` waits for the daemon's first frame and reports the *real* state
|
|
133
|
+
(`running` / `connecting` / `error`), not an optimistic guess.
|
|
134
|
+
- One active trace per server — `stop` before starting a different signal set,
|
|
135
|
+
or use `add`/`remove` to edit the live set.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# CrossPad r20 firmware — handy trace signals
|
|
2
|
+
|
|
3
|
+
Resolved from the Debug ELF DWARF. Names accept array/struct/expansion syntax
|
|
4
|
+
(`name`, `name[i]`, `name[i][j]`, `name.member`, `name[*]`, `name[a:b]`). Use
|
|
5
|
+
`crosspad_trace action=symbols query=<substr>` to discover more.
|
|
6
|
+
|
|
7
|
+
## Inputs (reg_map.c, `s_inputs[47]`, uint8)
|
|
8
|
+
| Spec | Meaning |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `s_inputs[0]` | encoder detent index |
|
|
11
|
+
| `s_inputs[1]` | pads 8–15 bitmap (PADS_HI) |
|
|
12
|
+
| `s_inputs[2]` | pads 0–7 bitmap (PADS_LO) |
|
|
13
|
+
| `s_inputs[3..18]` | per-pad pressure 0..127 (pad 0 = `[3]`) |
|
|
14
|
+
| `s_inputs[44]` | buttons bitmap (0x2C) |
|
|
15
|
+
| `s_inputs[*]` | whole input block (expands to 47 scalars) |
|
|
16
|
+
|
|
17
|
+
## Power / ADC (power.c)
|
|
18
|
+
| Spec | Meaning |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `s_adc_raw` | raw ADC DMA buffer, **15** uint16 channels (`ADC_BUF_SIZE`). `s_adc_raw[*]` plots all |
|
|
21
|
+
| `s_vbat_mv` | battery mV |
|
|
22
|
+
| `s_vbus_stm_mv` | VBUS (STM side) mV |
|
|
23
|
+
| `s_vbus_esp_mv` | VBUS (ESP side) mV |
|
|
24
|
+
| `s_temp_c` | die temp °C |
|
|
25
|
+
|
|
26
|
+
> NOTE: `s_adc_raw` is `[15]`, not 32 — indices ≥ 15 are rejected as out-of-bounds
|
|
27
|
+
> (the tracer bounds-checks against the DWARF array length).
|
|
28
|
+
|
|
29
|
+
## Built-in demo signals (trace_demo.c — RTT-style, for a quick self-test)
|
|
30
|
+
Cortex-M0+ has **no ITM/SWO**, so "printf over SWD" here is a RAM ring polled
|
|
31
|
+
non-intrusively. Flash the firmware, then watch:
|
|
32
|
+
|
|
33
|
+
| Spec | Meaning |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `g_trace_demo.tick` | monotonic main-loop counter |
|
|
36
|
+
| `g_trace_demo.demo_sine` | fixed-point sine, [-1000,+1000] |
|
|
37
|
+
| `g_trace_demo.demo_triangle` | fixed-point triangle, [-1000,+1000] |
|
|
38
|
+
| `g_trace_demo.loop_hz` | estimated main-loop rate |
|
|
39
|
+
| `g_trace_log_wr` | text-ring write count (watch it climb = logging active) |
|
|
40
|
+
|
|
41
|
+
`g_trace_demo.demo_sine` is the ideal "does the whole pipeline work?" signal —
|
|
42
|
+
it should draw a clean sine in the UI.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Detect the tracer environment and print a readiness report + suggested config.
|
|
3
|
+
# Read-only: writes nothing. Run this first to see what (if anything) is missing.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
|
|
6
|
+
VENV="${CROSSPAD_TRACE_VENV:-$HOME/.local/share/crosspad-mcp/venv}"
|
|
7
|
+
CFG="${XDG_CONFIG_HOME:-$HOME/.config}/crosspad-mcp/config.json"
|
|
8
|
+
|
|
9
|
+
ok() { printf ' \033[32mOK\033[0m %s\n' "$1"; }
|
|
10
|
+
miss() { printf ' \033[31mMISS\033[0m %s\n' "$1"; }
|
|
11
|
+
|
|
12
|
+
echo "== SWD tracer environment =="
|
|
13
|
+
|
|
14
|
+
# 1. pyOCD venv
|
|
15
|
+
if [ -x "$VENV/bin/python" ] && "$VENV/bin/python" -c "import pyocd, elftools" 2>/dev/null; then
|
|
16
|
+
ok "pyOCD venv: $VENV/bin/python ($("$VENV/bin/python" -c 'import pyocd;print(pyocd.__version__)' 2>/dev/null))"
|
|
17
|
+
else
|
|
18
|
+
miss "pyOCD venv at $VENV — run scripts/setup-venv.sh"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# 2. user config
|
|
22
|
+
if [ -f "$CFG" ]; then
|
|
23
|
+
ok "config: $CFG"
|
|
24
|
+
sed 's/^/ /' "$CFG"
|
|
25
|
+
else
|
|
26
|
+
miss "config $CFG — set keys via crosspad_trace action=config_set (stm_elf_path, pyocd_python)"
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# 3. Debug ELF (look in common spots if not configured)
|
|
30
|
+
ELF="${CROSSPAD_STM_ELF:-}"
|
|
31
|
+
if [ -z "$ELF" ]; then
|
|
32
|
+
ELF=$(ls "${CROSSPAD_STM_ROOT:-$HOME/GIT/CrossPad_STM32_r20}"/build/Debug/*.elf 2>/dev/null | head -1)
|
|
33
|
+
fi
|
|
34
|
+
if [ -n "$ELF" ] && [ -f "$ELF" ]; then
|
|
35
|
+
if "${VENV}/bin/python" - "$ELF" <<'PY' 2>/dev/null
|
|
36
|
+
import sys
|
|
37
|
+
from elftools.elf.elffile import ELFFile
|
|
38
|
+
sys.exit(0 if ELFFile(open(sys.argv[1],'rb')).has_dwarf_info() else 1)
|
|
39
|
+
PY
|
|
40
|
+
then ok "Debug ELF with DWARF: $ELF"
|
|
41
|
+
else miss "ELF $ELF has no DWARF — build the Debug preset with -g"
|
|
42
|
+
fi
|
|
43
|
+
else
|
|
44
|
+
miss "Debug ELF not found — build it (cmake --preset Debug && cmake --build build/Debug)"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# 4. ST-Link on USB
|
|
48
|
+
if lsusb 2>/dev/null | grep -qiE '0483:(3744|3748|374b|374d|374e|374f|3753)'; then
|
|
49
|
+
ok "ST-Link present on USB"
|
|
50
|
+
else
|
|
51
|
+
miss "ST-Link NOT on USB — plug it in (or replug if it vanished after a wedged session)"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# 5. udev rules
|
|
55
|
+
if ls /etc/udev/rules.d/*stlink* >/dev/null 2>&1; then
|
|
56
|
+
ok "ST-Link udev rules present"
|
|
57
|
+
else
|
|
58
|
+
miss "no ST-Link udev rules — run scripts/install-udev-rules.sh (needs sudo)"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo "== done =="
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Install ST-Link udev rules so pyOCD/libusb can claim the probe without root.
|
|
3
|
+
# Requires sudo. Without these, pyOCD may fail with permission/USB errors even
|
|
4
|
+
# though `lsusb` shows the device (st-info succeeding only proves the CURRENT
|
|
5
|
+
# user already happens to have access).
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
RULE=/etc/udev/rules.d/49-stlink.rules
|
|
9
|
+
echo "[udev] writing $RULE (needs sudo)"
|
|
10
|
+
sudo tee "$RULE" >/dev/null <<'EOF'
|
|
11
|
+
# ST-Link/V1
|
|
12
|
+
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3744", MODE="0666", TAG+="uaccess"
|
|
13
|
+
# ST-Link/V2
|
|
14
|
+
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", MODE="0666", TAG+="uaccess"
|
|
15
|
+
# ST-Link/V2-1
|
|
16
|
+
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", MODE="0666", TAG+="uaccess"
|
|
17
|
+
# ST-Link/V3
|
|
18
|
+
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374d", MODE="0666", TAG+="uaccess"
|
|
19
|
+
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374e", MODE="0666", TAG+="uaccess"
|
|
20
|
+
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374f", MODE="0666", TAG+="uaccess"
|
|
21
|
+
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3753", MODE="0666", TAG+="uaccess"
|
|
22
|
+
EOF
|
|
23
|
+
sudo udevadm control --reload-rules
|
|
24
|
+
sudo udevadm trigger
|
|
25
|
+
echo "[udev] DONE. Replug the ST-Link for the new rules to take effect."
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Create the pyOCD virtualenv the SWD tracer daemon runs in.
|
|
3
|
+
#
|
|
4
|
+
# The system Python is usually PEP-668 "externally managed" (Debian/Ubuntu), so
|
|
5
|
+
# pyocd/pyelftools cannot be pip-installed globally — a venv is required. The
|
|
6
|
+
# tracer's user config key `pyocd_python` must point at this venv's python.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
VENV="${CROSSPAD_TRACE_VENV:-$HOME/.local/share/crosspad-mcp/venv}"
|
|
10
|
+
|
|
11
|
+
echo "[setup-venv] target venv: $VENV"
|
|
12
|
+
if [ ! -x "$VENV/bin/python" ]; then
|
|
13
|
+
python3 -m venv "$VENV"
|
|
14
|
+
fi
|
|
15
|
+
"$VENV/bin/pip" install --quiet --upgrade pip
|
|
16
|
+
# pyocd 0.44+ for the SWD backend; pyelftools for DWARF symbol resolution.
|
|
17
|
+
"$VENV/bin/pip" install --quiet "pyocd>=0.44" pyelftools
|
|
18
|
+
|
|
19
|
+
echo "[setup-venv] installed:"
|
|
20
|
+
"$VENV/bin/python" - <<'PY'
|
|
21
|
+
import pyocd, elftools
|
|
22
|
+
print(" pyocd ", pyocd.__version__)
|
|
23
|
+
print(" pyelftools", getattr(elftools, "__version__", "?"))
|
|
24
|
+
PY
|
|
25
|
+
echo "[setup-venv] DONE. Set the tracer config:"
|
|
26
|
+
echo " crosspad_trace action=config_set key=pyocd_python value=$VENV/bin/python"
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# CrossPad SWD tracer — wire protocol v2
|
|
2
|
+
|
|
3
|
+
Single source of truth shared by the Python daemon (`tracer/swd_tracer.py`), the
|
|
4
|
+
Node session/webui layer (`src/tools/trace-*.ts`), and the browser UI
|
|
5
|
+
(`tracer/ui/index.html`). v2 adds **runtime-editable watching** (add/remove
|
|
6
|
+
signals on a live trace) and richer **signal-spec resolution**.
|
|
7
|
+
|
|
8
|
+
**v2.1 additions** (this revision): whole-array / vector / matrix **expansion**
|
|
9
|
+
(§1.1), richer **symbol metadata** for UI autocomplete (§8), a `/symbols` HTTP
|
|
10
|
+
endpoint (§9), and a hard rule that `add`/`remove` return the **post-reconcile**
|
|
11
|
+
signal set (§4, §6) and that **Fs is reported ONCE globally** (§10).
|
|
12
|
+
|
|
13
|
+
All daemon I/O is NDJSON — one JSON object per line. stdout = machine frames,
|
|
14
|
+
stderr = human logs only.
|
|
15
|
+
|
|
16
|
+
## 1. Signal spec grammar (resolved from the Debug ELF DWARF)
|
|
17
|
+
|
|
18
|
+
A *signal spec* names a memory location to poll. Supported syntax:
|
|
19
|
+
|
|
20
|
+
| Form | Meaning |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `name` | global / static variable (base type, struct, or array) |
|
|
23
|
+
| `name[i]` | array element (existing) |
|
|
24
|
+
| `name[i][j]` | multi-dimensional array element |
|
|
25
|
+
| `name.member` | struct / union member |
|
|
26
|
+
| `name.a.b` | nested member chain |
|
|
27
|
+
| `name[i].member` / `name.member[j]` | any mix of `[index]` and `.member` |
|
|
28
|
+
|
|
29
|
+
Resolution walks DWARF: base symbol → `DW_AT_type`; `[i]` consumes an
|
|
30
|
+
`DW_TAG_array_type` dimension (stride = element byte size, multi-dim handled via
|
|
31
|
+
successive `DW_TAG_subrange_type`); `.member` consumes a `DW_TAG_structure_type`/
|
|
32
|
+
`DW_TAG_union_type` member (offset = `DW_AT_data_member_location`). The final
|
|
33
|
+
node must resolve to a scalar (`DW_TAG_base_type`, pointer, or enum) — a spec
|
|
34
|
+
that lands on an aggregate is **unresolved** UNLESS it expands (see §1.1).
|
|
35
|
+
Result: `{name: <original spec>, address, size, encoding}`.
|
|
36
|
+
|
|
37
|
+
## 1.1 Whole-array / vector / matrix expansion
|
|
38
|
+
|
|
39
|
+
A spec that lands on (or leaves trailing) an array dimension **expands** into one
|
|
40
|
+
concrete scalar spec per element instead of being unresolved:
|
|
41
|
+
|
|
42
|
+
| Form | Expands to |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `vec` (bare array name, element scalar) | `vec[0]`, `vec[1]`, … `vec[N-1]` |
|
|
45
|
+
| `vec[*]` | same as bare — all elements of that dimension |
|
|
46
|
+
| `vec[a:b]` | half-open slice `vec[a]` … `vec[b-1]` |
|
|
47
|
+
| `mat` (2-D array of scalar) | every cell `mat[0][0]` … `mat[R-1][C-1]` (row-major) |
|
|
48
|
+
| `mat[*][k]` / `mat[i][*]` | the selected row/column |
|
|
49
|
+
| `arr.field` where `arr` is array-of-struct | `arr[0].field` … (array dim before a member also expands) |
|
|
50
|
+
|
|
51
|
+
Expanded element names use the concrete `name[i]` / `name[i][j]` form so the
|
|
52
|
+
buffer/UI see distinct scalars. **Cap:** an expansion exceeding **256** elements
|
|
53
|
+
is skipped and reported in the `signals` frame's `unresolved` as
|
|
54
|
+
`"<spec> (expands to <N> > 256)"`. A spec landing on a struct/union (no trailing
|
|
55
|
+
array) is still unresolved. Expansion is computed daemon-side (DWARF knows the
|
|
56
|
+
bounds) for both the initial `--signals` set and live `add` commands.
|
|
57
|
+
|
|
58
|
+
`encoding ∈ {int, uint, float, bool, char, uchar, address}`.
|
|
59
|
+
|
|
60
|
+
## 2. Daemon stdin commands (Node → daemon, NDJSON)
|
|
61
|
+
|
|
62
|
+
```jsonc
|
|
63
|
+
{"cmd":"stop"} // graceful shutdown (existing)
|
|
64
|
+
{"cmd":"add","signals":["s_vbat_mv","s_inputs[3]"]} // add to live poll set
|
|
65
|
+
{"cmd":"remove","signals":["s_inputs[3]"]} // drop from live poll set
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`add`/`remove` mutate the poll set on the running daemon. The poll loop
|
|
69
|
+
re-coalesces ranges on the next iteration. Unknown / unresolvable specs in an
|
|
70
|
+
`add` are skipped (reported via a `signals` frame's `unresolved`), never fatal.
|
|
71
|
+
`remove` of an unknown name is a no-op.
|
|
72
|
+
|
|
73
|
+
## 3. Daemon stdout frames (daemon → Node, NDJSON)
|
|
74
|
+
|
|
75
|
+
```jsonc
|
|
76
|
+
// emitted once right after connect, AND after every successful add/remove:
|
|
77
|
+
{"type":"signals","signals":[{"name","address","size","encoding"}],"unresolved":["..."]}
|
|
78
|
+
|
|
79
|
+
{"type":"sample","t":<seconds float>,"values":{"<name>":<number>, ...}}
|
|
80
|
+
{"type":"status","device_state":"stop_suspected|stopped|...","t"?,"samples"?}
|
|
81
|
+
{"type":"error","error":"<message>"}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`values` only contains the currently-watched signals; after an add the next
|
|
85
|
+
sample includes the new names, after a remove they disappear.
|
|
86
|
+
|
|
87
|
+
## 4. Node TraceSession API (TS)
|
|
88
|
+
|
|
89
|
+
- `addSignals(string[]): Promise<string[]>` — writes `{"cmd":"add",...}` to
|
|
90
|
+
daemon stdin and resolves with the **post-reconcile** signal-name set once the
|
|
91
|
+
next `{"type":"signals"}` frame arrives (timeout ~2 s → resolves with the
|
|
92
|
+
current `buffer.signalNames()`). This kills the old race where the immediate
|
|
93
|
+
return showed the pre-reconcile set.
|
|
94
|
+
- `removeSignals(string[]): Promise<string[]>` — same, for remove.
|
|
95
|
+
- On a `{"type":"signals"}` frame: reconcile `buffer` signal set
|
|
96
|
+
(`buffer.addSignal`/`removeSignal`) and forward the frame to `onFrame`
|
|
97
|
+
subscribers (so the WS rebroadcasts the new set to browsers), then resolve any
|
|
98
|
+
pending add/remove promise.
|
|
99
|
+
- `TraceBuffer` gains `addSignal(name)` / `removeSignal(name)` updating
|
|
100
|
+
`signalNames()`. Stored samples already key values by name, so history of a
|
|
101
|
+
removed signal stays until it ages out of the ring.
|
|
102
|
+
|
|
103
|
+
## 5. Node TraceWebUi — bidirectional WS
|
|
104
|
+
|
|
105
|
+
- Inbound browser → server messages (JSON): `{"cmd":"add"|"remove","signals":[...]}`.
|
|
106
|
+
Validate shape + cmd; forward to `session.addSignals/removeSignals`. Keep the
|
|
107
|
+
loopback-Origin check. Ignore malformed messages silently.
|
|
108
|
+
- Outbound server → browser: forwards every daemon frame (`sample`, `status`,
|
|
109
|
+
`error`, **`signals`**) plus the initial `{"type":"hello","signals":[...]}`.
|
|
110
|
+
|
|
111
|
+
## 6. MCP tool (`crosspad_trace`) actions
|
|
112
|
+
|
|
113
|
+
- `add` / `remove`: require an active session; `await` the session promise and
|
|
114
|
+
return the **post-reconcile** `signals` (so the MCP response matches reality,
|
|
115
|
+
including any array expansion and dropped `unresolved` specs).
|
|
116
|
+
- `symbols`: return the richer metadata of §8.
|
|
117
|
+
- All other actions unchanged.
|
|
118
|
+
|
|
119
|
+
## 7. Browser UI (talks §3/§5 over WS)
|
|
120
|
+
|
|
121
|
+
Receives `hello` → `signals` (live set updates) → `sample`/`status`. Sends
|
|
122
|
+
`add`/`remove`. Fetches `/symbols` (§9) for autocomplete. UI feature scope is
|
|
123
|
+
owned by the UI implementer; the hard contracts are the WS message shapes, the
|
|
124
|
+
`/symbols` shape (§8), and §10 (Fs shown once).
|
|
125
|
+
|
|
126
|
+
## 8. Symbol metadata (richer `symbols` output — for autocomplete)
|
|
127
|
+
|
|
128
|
+
The daemon `symbols` subcommand and the `/symbols` endpoint return, per symbol,
|
|
129
|
+
in ADDITION to the existing `{name, address, encoding, size}`:
|
|
130
|
+
|
|
131
|
+
```jsonc
|
|
132
|
+
{
|
|
133
|
+
"name": "s_adc_raw", "address": ..., "encoding": "uint", "size": 64,
|
|
134
|
+
"kind": "array", // "scalar" | "array" | "struct" | "union" | "other"
|
|
135
|
+
"dims": [32], // array only: per-dimension element counts
|
|
136
|
+
"count": 32, // array only: total elements (product of dims)
|
|
137
|
+
"elem_size": 2, // array only: one element's byte size
|
|
138
|
+
"elem_encoding": "uint", // array only: element scalar encoding
|
|
139
|
+
"members": ["a","b"] // struct/union only: member names (one level)
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`kind`/`dims`/`count`/`elem_*`/`members` are best-effort and may be omitted for
|
|
144
|
+
`other`. Back-compat: old consumers that read only `{name,address,encoding,size}`
|
|
145
|
+
keep working. The UI uses this to suggest base names, `name[i]`/`name[*]` for
|
|
146
|
+
arrays, and `name.member` for structs.
|
|
147
|
+
|
|
148
|
+
## 9. `/symbols` HTTP endpoint (browser → Node)
|
|
149
|
+
|
|
150
|
+
`GET /symbols[?query=substr]` on the same loopback origin as the UI returns
|
|
151
|
+
`{"symbols":[ ... §8 entries ... ]}` (Content-Type `application/json`). The
|
|
152
|
+
webui obtains it via the existing Node `listSymbols()` bridge against the active
|
|
153
|
+
session's ELF. Loopback-bound like the rest of the UI server.
|
|
154
|
+
|
|
155
|
+
## 10. Fs is reported ONCE, globally
|
|
156
|
+
|
|
157
|
+
Sample rate (Fs / actual_fs) is a property of the **whole trace**, not per
|
|
158
|
+
signal — every signal is sampled in the same poll loop. So Fs must be shown in
|
|
159
|
+
exactly ONE place (a single global readout / one `actual_fs` field), never
|
|
160
|
+
duplicated into each per-signal stats row. Per-signal stats keep value-domain
|
|
161
|
+
metrics only (min/max/mean/p2p/RMS/n/slope); the shared Fs lives outside them.
|
|
162
|
+
|
|
163
|
+
## 11. Robustness contract (v2.2 — fail fast, never wedge)
|
|
164
|
+
|
|
165
|
+
The probe (ST-Link V2) can vanish from USB or wedge libusb after an unclean
|
|
166
|
+
session. The tracer MUST fail fast and surface it, never hang or require
|
|
167
|
+
`pkill -9`. Hard rules:
|
|
168
|
+
|
|
169
|
+
### 11.1 Daemon connect
|
|
170
|
+
- `session_with_chosen_probe` is called with `blocking=False, return_first=True`
|
|
171
|
+
so a **missing probe returns immediately** → emit
|
|
172
|
+
`{"type":"error","error":"no debug probe detected (replug ST-Link)"}` and exit
|
|
173
|
+
code 3.
|
|
174
|
+
- Creating + opening the session runs inside a **watchdog thread** bounded by
|
|
175
|
+
`--connect-timeout` (default 8 s). If it does not complete in time the worker
|
|
176
|
+
is wedged in uninterruptible C (libusb) — flush an
|
|
177
|
+
`{"type":"error","error":"connect timeout after Ns (probe wedged? replug)"}`
|
|
178
|
+
frame and `os._exit(2)`. The main process dies; the OS reclaims the wedged
|
|
179
|
+
thread. This is the ONLY correct recovery for a libusb wedge.
|
|
180
|
+
- Any other connect exception → `{"type":"error","error":...}` + exit 1.
|
|
181
|
+
- Applies to BOTH `trace` and `device-state` (device-state uses a shorter 6 s).
|
|
182
|
+
|
|
183
|
+
### 11.2 Daemon run-loop disconnect
|
|
184
|
+
- A read fault emits `{"type":"status","device_state":"stop_suspected"}` as
|
|
185
|
+
before, BUT the daemon now tracks fault duration. If faults persist longer
|
|
186
|
+
than `--lost-timeout` (default 10 s) it emits
|
|
187
|
+
`{"type":"error","error":"probe/target lost (persistent read fault Ns)"}` and
|
|
188
|
+
exits — instead of looping `stop_suspected` forever. A successful read resets
|
|
189
|
+
the fault timer (a genuine MCU STOP that resumes keeps tracing).
|
|
190
|
+
|
|
191
|
+
### 11.3 ELF / DWARF guard
|
|
192
|
+
- `build_symbol_table` / symbol resolution is wrapped: a bad/missing ELF emits
|
|
193
|
+
`{"type":"error","error":...}` (not a raw traceback) and the daemon exits.
|
|
194
|
+
|
|
195
|
+
### 11.4 device_state vocabulary
|
|
196
|
+
`connecting` (before first frame) · `running` · `stop_suspected` · `probe_lost`
|
|
197
|
+
· `connect_timeout` · `stopped` · `error: <msg>` · `exited`.
|
|
198
|
+
|
|
199
|
+
### 11.5 Node teardown — guaranteed kill
|
|
200
|
+
`TraceSession.stop()`: write `{"cmd":"stop"}`, then `SIGTERM` after ~1.5 s, then
|
|
201
|
+
**`SIGKILL` after a further ~3 s if the process is still alive** (a wedged daemon
|
|
202
|
+
ignores SIGTERM). Always tear down the web UI first (existing behavior).
|
|
203
|
+
|
|
204
|
+
### 11.6 Node diagnostics + start truthfulness
|
|
205
|
+
- The daemon's **stderr is captured** (ring of the last ~30 lines), exposed as
|
|
206
|
+
`session.stderrTail()` and folded into the device_state/error surfaced by the
|
|
207
|
+
MCP `status`/`start`.
|
|
208
|
+
- MCP `start` **waits up to ~3 s for the first frame** (`signals`|`sample`|
|
|
209
|
+
`error`) and returns a device_state reflecting reality
|
|
210
|
+
(`running` vs `connect_timeout` vs `error: …`) instead of an optimistic
|
|
211
|
+
`running` that masks a dead connect.
|
|
212
|
+
- If the daemon exits non-zero with no error frame, `device_state` becomes
|
|
213
|
+
`error: <stderr tail>`.
|
|
214
|
+
|
|
215
|
+
### 11.7 Doctor probe presence
|
|
216
|
+
`doctor` runs a real probe-presence check (`pyocd list` via the daemon python,
|
|
217
|
+
or `st-info --probe`) and reports a **blocking** issue
|
|
218
|
+
`no_probe_detected` when none is connected (distinct from the udev warning).
|
|
219
|
+
`start` refuses (clear error) when doctor reports `no_probe_detected`.
|
|
220
|
+
|
|
221
|
+
## 12. Persistent dashboard + reconnect + auto-open (v2.3)
|
|
222
|
+
|
|
223
|
+
The dashboard must feel permanent: open it once (ideally as a VS Code **Simple
|
|
224
|
+
Browser** tab) and it survives every trace start/stop without going dead.
|
|
225
|
+
|
|
226
|
+
### 12.1 Persistent UI server (decoupled from the session)
|
|
227
|
+
- The web UI HTTP+WS server is a **module-level singleton that OUTLIVES trace
|
|
228
|
+
sessions** — `stop` ends the daemon but the server keeps listening. A
|
|
229
|
+
browser/VS Code tab stays connected across start→stop→start cycles.
|
|
230
|
+
- The singleton holds a mutable `currentSession`. `start` binds the new session
|
|
231
|
+
(subscribes to its frames); `stop` unbinds (server goes idle but stays up).
|
|
232
|
+
- `TraceSession.stop()` no longer tears the UI down; the singleton owns the
|
|
233
|
+
server lifecycle. The port (7373) is therefore stable across traces.
|
|
234
|
+
|
|
235
|
+
### 12.2 WS messages — new server→client
|
|
236
|
+
- `hello` gains fields: `{"type":"hello","active":bool,"signals":[...]}`.
|
|
237
|
+
- `{"type":"trace_start","signals":[...]}` — a new trace began; UI resets and
|
|
238
|
+
starts plotting the new set.
|
|
239
|
+
- `{"type":"trace_end"}` — the trace stopped; UI keeps the last data but shows an
|
|
240
|
+
idle/"waiting for next trace" state. The WS stays connected (server is up).
|
|
241
|
+
- `sample`/`status`/`error`/`signals` continue, scoped to the bound session.
|
|
242
|
+
|
|
243
|
+
### 12.3 `/symbols` when idle
|
|
244
|
+
With no active session, `/symbols` falls back to the **configured default ELF**
|
|
245
|
+
so autocomplete keeps working between traces.
|
|
246
|
+
|
|
247
|
+
### 12.4 Auto-open on `start` (unless already open)
|
|
248
|
+
On `start`, after ensuring the singleton server is up, open the dashboard in the
|
|
249
|
+
user's browser **only if no WS client is currently connected** (i.e. it isn't
|
|
250
|
+
already open — covers both an external browser and a VS Code Simple Browser tab).
|
|
251
|
+
Platform opener (`xdg-open`/`open`/`start`), detached, best-effort, never throws.
|
|
252
|
+
Skip when headless (no `DISPLAY`/`WAYLAND_DISPLAY` on Linux) or when
|
|
253
|
+
`CROSSPAD_TRACE_NO_BROWSER` is set. Because the server persists, a VS Code tab
|
|
254
|
+
opened once stays connected, so auto-open never re-pops it.
|
|
255
|
+
|
|
256
|
+
### 12.5 UI reconnect
|
|
257
|
+
The UI has a visible **Reconnect** button AND an automatic reconnect loop (with
|
|
258
|
+
backoff) that survives server restarts and `trace_end`. Idle shows "waiting for
|
|
259
|
+
trace…"; `trace_end` shows a "trace ended" banner; reconnect re-establishes the
|
|
260
|
+
WS and re-syncs the signal set from the fresh `hello`.
|