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.
Files changed (69) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +14 -0
  3. package/.mcp.json +9 -0
  4. package/README.md +95 -0
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.js +8 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +381 -57
  10. package/dist/index.js.map +1 -1
  11. package/dist/tools/idf-flash.js +2 -2
  12. package/dist/tools/idf-flash.js.map +1 -1
  13. package/dist/tools/idf-monitor.d.ts +3 -1
  14. package/dist/tools/idf-monitor.js +19 -3
  15. package/dist/tools/idf-monitor.js.map +1 -1
  16. package/dist/tools/midi.js +20 -16
  17. package/dist/tools/midi.js.map +1 -1
  18. package/dist/tools/symbols.d.ts +3 -1
  19. package/dist/tools/symbols.js +31 -1
  20. package/dist/tools/symbols.js.map +1 -1
  21. package/dist/tools/trace-buffer.d.ts +40 -0
  22. package/dist/tools/trace-buffer.js +74 -0
  23. package/dist/tools/trace-buffer.js.map +1 -0
  24. package/dist/tools/trace-device.d.ts +10 -0
  25. package/dist/tools/trace-device.js +26 -0
  26. package/dist/tools/trace-device.js.map +1 -0
  27. package/dist/tools/trace-doctor.d.ts +43 -0
  28. package/dist/tools/trace-doctor.js +150 -0
  29. package/dist/tools/trace-doctor.js.map +1 -0
  30. package/dist/tools/trace-export.d.ts +4 -0
  31. package/dist/tools/trace-export.js +14 -0
  32. package/dist/tools/trace-export.js.map +1 -0
  33. package/dist/tools/trace-session.d.ts +118 -0
  34. package/dist/tools/trace-session.js +346 -0
  35. package/dist/tools/trace-session.js.map +1 -0
  36. package/dist/tools/trace-symbols.d.ts +24 -0
  37. package/dist/tools/trace-symbols.js +44 -0
  38. package/dist/tools/trace-symbols.js.map +1 -0
  39. package/dist/tools/trace-webui.d.ts +53 -0
  40. package/dist/tools/trace-webui.js +222 -0
  41. package/dist/tools/trace-webui.js.map +1 -0
  42. package/dist/utils/device.d.ts +5 -0
  43. package/dist/utils/device.js +43 -15
  44. package/dist/utils/device.js.map +1 -1
  45. package/dist/utils/exec.js +26 -0
  46. package/dist/utils/exec.js.map +1 -1
  47. package/dist/utils/userConfig.d.ts +13 -0
  48. package/dist/utils/userConfig.js +43 -0
  49. package/dist/utils/userConfig.js.map +1 -0
  50. package/package.json +12 -4
  51. package/skills/crosspad/SKILL.md +58 -0
  52. package/skills/crosspad/reference/faq.md +40 -0
  53. package/skills/crosspad/reference/install.md +84 -0
  54. package/skills/crosspad/reference/repos.md +29 -0
  55. package/skills/crosspad/reference/role-contributor.md +64 -0
  56. package/skills/crosspad/reference/role-fw-dev.md +44 -0
  57. package/skills/crosspad/reference/role-user.md +49 -0
  58. package/skills/crosspad/reference/tools.md +68 -0
  59. package/skills/crosspad/scripts/doctor.sh +65 -0
  60. package/skills/crosspad/scripts/setup.sh +53 -0
  61. package/skills/swd-tracer/SKILL.md +135 -0
  62. package/skills/swd-tracer/reference/signals.md +42 -0
  63. package/skills/swd-tracer/scripts/detect-env.sh +61 -0
  64. package/skills/swd-tracer/scripts/install-udev-rules.sh +25 -0
  65. package/skills/swd-tracer/scripts/setup-venv.sh +26 -0
  66. package/tracer/PROTOCOL.md +260 -0
  67. package/tracer/README.md +327 -0
  68. package/tracer/swd_tracer.py +1083 -0
  69. package/tracer/ui/index.html +834 -0
@@ -0,0 +1,327 @@
1
+ # CrossPad SWD Tracer Daemon
2
+
3
+ A single-file Python daemon (`swd_tracer.py`) that communicates with the CrossPad STM32 target over SWD using pyOCD. The Node MCP server drives this daemon as a subprocess.
4
+
5
+ ## Output contract
6
+
7
+ - **stdout**: machine-readable JSON/NDJSON only — one JSON object per line.
8
+ - **stderr**: human-readable logs, progress, and error messages only.
9
+
10
+ Never mix JSON output with log lines on stdout. The Node bridge scans stdout for JSON lines.
11
+
12
+ ## Dependencies
13
+
14
+ ```
15
+ pip install pyocd pyelftools
16
+ ```
17
+
18
+ ### Recommended: use a dedicated venv
19
+
20
+ ```bash
21
+ python3 -m venv ~/.local/share/crosspad-mcp/venv
22
+ ~/.local/share/crosspad-mcp/venv/bin/pip install pyocd pyelftools
23
+ ```
24
+
25
+ Then set the `pyocd_python` key in `~/.config/crosspad-mcp/config.json` to point at the venv interpreter:
26
+
27
+ ```json
28
+ {
29
+ "pyocd_python": "/home/<you>/.local/share/crosspad-mcp/venv/bin/python"
30
+ }
31
+ ```
32
+
33
+ This prevents conflicts with system or project Python environments. The MCP server also honours the `CROSSPAD_TRACE_PYTHON` environment variable as an override.
34
+
35
+ ## Subcommands
36
+
37
+ ### `symbols` — resolve firmware variables from DWARF
38
+
39
+ Reads an ELF built with debug info (`-g`) and emits a JSON object listing every fixed-address variable (globals and `static` locals) found in the DWARF info.
40
+
41
+ ```bash
42
+ python swd_tracer.py symbols --elf <path/to/firmware.elf> [--query <substring>]
43
+ ```
44
+
45
+ Options:
46
+
47
+ | Flag | Description |
48
+ |---|---|
49
+ | `--elf PATH` | Path to the Debug ELF (required). Must contain DWARF info. |
50
+ | `--query STR` | Case-insensitive substring filter on the symbol name (optional). |
51
+
52
+ Output (stdout):
53
+
54
+ ```json
55
+ {"symbols": [
56
+ {"name": "s_vbat_mv", "address": 536885450, "encoding": "uint", "size": 2},
57
+ {"name": "s_vbus_stm_mv","address": 536885448, "encoding": "uint", "size": 2}
58
+ ]}
59
+ ```
60
+
61
+ Fields:
62
+
63
+ | Field | Type | Description |
64
+ |---|---|---|
65
+ | `name` | string | Symbol name as it appears in DWARF. |
66
+ | `address` | number | Absolute RAM address (decimal integer). |
67
+ | `encoding` | string | Base-type encoding: `uint`, `int`, `uchar`, `char`, `bool`, `float`, `address`. |
68
+ | `size` | number | Byte size of the variable (or total array size for arrays, element size for indexed access). |
69
+
70
+ Results are de-duplicated and sorted by address.
71
+
72
+ Example — query `s_vbat_mv`:
73
+
74
+ ```bash
75
+ /path/to/venv/bin/python swd_tracer.py symbols \
76
+ --elf ~/GIT/CrossPad_STM32_r20/build/Debug/CrossPad_STM32_r20.elf \
77
+ --query s_vbat_mv
78
+ # stdout: {"symbols": [{"name": "s_vbat_mv", "address": 536885450, "encoding": "uint", "size": 2}]}
79
+ ```
80
+
81
+ ---
82
+
83
+ ### `trace` — live SWD poll loop (non-halting)
84
+
85
+ Connects to the target via pyOCD, polls the requested signals by reading RAM while the core continues to run, and emits one NDJSON frame per sample on stdout. Optionally writes a `.cptrace` file. Stops when `{"cmd":"stop"}` is received on stdin.
86
+
87
+ ```bash
88
+ python swd_tracer.py trace \
89
+ --elf <firmware.elf> \
90
+ --signals <sig1,sig2,...> \
91
+ [--rate <Hz>] \
92
+ [--out <file.cptrace>] \
93
+ [--probe <unique_id>]
94
+ ```
95
+
96
+ #### Options
97
+
98
+ | Flag | Default | Description |
99
+ |---|---|---|
100
+ | `--elf PATH` | (required) | Debug ELF for DWARF symbol resolution. |
101
+ | `--signals LIST` | (required) | Comma-separated signal names (see below). |
102
+ | `--rate HZ` | `0` (max) | Target poll rate in Hz. `0` means poll as fast as possible. |
103
+ | `--out PATH` | `None` | If given, also append samples to a `.cptrace` binary file. |
104
+ | `--probe UID` | `None` | ST-Link unique ID string; omit to auto-select the first probe. |
105
+
106
+ #### Signal name syntax — `name[index]`
107
+
108
+ Signal names may include an array index: `s_inputs[0]`, `s_adc_raw[3]`.
109
+
110
+ Resolution rules:
111
+ 1. Parse `base` and optional `[index]` from the spec.
112
+ 2. Look up `base` in the DWARF symbol table (exact match).
113
+ 3. `address = base.address + index * base.size` (element size from DWARF).
114
+ 4. A plain `name` (no brackets) resolves directly.
115
+ 5. Unknown `base` → an `error` frame is emitted and the command exits.
116
+
117
+ Example: if `s_inputs` is at address `0x20001000` with element size 1, then `s_inputs[3]` resolves to address `0x20001003`.
118
+
119
+ #### NDJSON output frames (stdout)
120
+
121
+ One JSON object per line. Three frame types:
122
+
123
+ **`sample`** — one reading of all polled signals:
124
+
125
+ ```json
126
+ {"type": "sample", "t": 0.012345, "values": {"s_vbat_mv": 3742, "s_inputs[0]": 255}}
127
+ ```
128
+
129
+ | Field | Description |
130
+ |---|---|
131
+ | `type` | `"sample"` |
132
+ | `t` | Seconds since trace start (float, 6 decimal places). |
133
+ | `values` | Object mapping each signal spec to its decoded integer or float value. |
134
+
135
+ **`status`** — device or session state change:
136
+
137
+ ```json
138
+ {"type": "status", "device_state": "stop_suspected", "t": 1.234}
139
+ {"type": "status", "device_state": "stopped", "samples": 1234}
140
+ ```
141
+
142
+ | `device_state` | Meaning |
143
+ |---|---|
144
+ | `stop_suspected` | A memory read faulted — the STM32 is probably in STOP mode. Polling pauses 200 ms and resumes. No halt is issued. |
145
+ | `stopped` | Normal exit after receiving `{"cmd":"stop"}`. `samples` field holds total sample count. |
146
+
147
+ **`error`** — fatal signal resolution failure:
148
+
149
+ ```json
150
+ {"type": "error", "error": "unknown symbols: bad_name,also_bad"}
151
+ ```
152
+
153
+ Emitted and the process exits if any requested signal cannot be found in the DWARF symbol table.
154
+
155
+ #### Stopping the daemon
156
+
157
+ Write `{"cmd":"stop"}` followed by a newline to the daemon's stdin:
158
+
159
+ ```bash
160
+ echo '{"cmd":"stop"}' | <daemon process stdin>
161
+ ```
162
+
163
+ The daemon drains the current poll iteration, closes the `.cptrace` file if open, disconnects from the probe, and emits a final `status/stopped` frame before exiting.
164
+
165
+ #### `.cptrace` file format
166
+
167
+ Binary-framed header followed by line-delimited JSON body rows.
168
+
169
+ ```
170
+ Offset Size Content
171
+ 0 4 Magic: ASCII "CPTR"
172
+ 4 4 Header length N (little-endian uint32)
173
+ 8 N JSON signal list: {"signals":[{"name":…,"encoding":…,"size":…},…]}
174
+ 8+N … Body rows, one per sample, each: JSON{"t":…,"v":{…}}\n
175
+ ```
176
+
177
+ The binary header allows fast seeking to the body start and easy validation. Body rows are line-JSON for simplicity and recoverability (a partial write is still parseable up to the last complete line).
178
+
179
+ #### Example
180
+
181
+ The daemon reads control commands (`add` / `remove` / `stop`) as NDJSON on its
182
+ **stdin**, so drive it through a FIFO you keep open for writing:
183
+
184
+ ```bash
185
+ mkfifo /tmp/trace_in
186
+ /path/to/venv/bin/python swd_tracer.py trace \
187
+ --elf ~/GIT/CrossPad_STM32_r20/build/Debug/CrossPad_STM32_r20.elf \
188
+ --signals s_vbat_mv,s_inputs[0],s_inputs[1] \
189
+ --rate 100 \
190
+ --out /tmp/session.cptrace \
191
+ < /tmp/trace_in 2>/tmp/trace.log &
192
+ DAEMON_PID=$!
193
+ exec 3>/tmp/trace_in # hold the write end open
194
+
195
+ # ... let it run for a few seconds ...
196
+
197
+ echo '{"cmd":"stop"}' >&3 # graceful stop via the stdin protocol
198
+ exec 3>&-; rm -f /tmp/trace_in # (or just: kill "$DAEMON_PID")
199
+ ```
200
+
201
+ stdout while running:
202
+
203
+ ```
204
+ {"type": "sample", "t": 0.000012, "values": {"s_vbat_mv": 3742, "s_inputs[0]": 255, "s_inputs[1]": 0}}
205
+ {"type": "sample", "t": 0.010034, "values": {"s_vbat_mv": 3741, "s_inputs[0]": 255, "s_inputs[1]": 0}}
206
+ ...
207
+ {"type": "status", "device_state": "stopped", "samples": 200}
208
+ ```
209
+
210
+ ---
211
+
212
+ ### `device-state` — deep low-power / STOP register dump (non-halting)
213
+
214
+ Reads a fixed set of STM32G0 / Cortex-M debug-bus registers and decodes key low-power bits to characterise whether the core is in run/sleep or STOP without auto-waking it (no halt, no DBGMCU debug-in-stop bits required).
215
+
216
+ ```bash
217
+ python swd_tracer.py device-state [--probe UID] [--target cortex_m]
218
+ ```
219
+
220
+ Options:
221
+
222
+ | Flag | Default | Description |
223
+ |---|---|---|
224
+ | `--probe UID` | `None` | ST-Link unique ID string; omit to auto-select the first probe. |
225
+ | `--target` | `cortex_m` | pyOCD target override. `cortex_m` (generic) is sufficient — no CMSIS pack needed for plain register reads. |
226
+
227
+ Registers read (`accessible` is `false` if any read faults — which itself indicates the core is in deep STOP):
228
+
229
+ | Register | Address | Description |
230
+ |---|---|---|
231
+ | `PWR_CR1` | `0x40007000` | Power control 1 — `LPMS[2:0]` low-power mode select |
232
+ | `PWR_SR1` | `0x40007010` | Power status 1 |
233
+ | `RCC_CR` | `0x40021000` | RCC clock control — HSI/PLL on bits |
234
+ | `RCC_CFGR` | `0x40021008` | RCC clock configuration |
235
+ | `SCB_SCR` | `0xE000ED10` | System Control Register — `SLEEPDEEP` bit (bit 2) |
236
+ | `DBGMCU_CR` | `0x40015804` | Debug MCU configuration |
237
+
238
+ Decoded fields:
239
+
240
+ | Field | Type | Description |
241
+ |---|---|---|
242
+ | `SLEEPDEEP` | bool | `true` if `SCB_SCR[2]` is set — the next WFI/WFE will enter a deep sleep / STOP mode. |
243
+ | `LPMS` | int (0-7) | Low-power mode select from `PWR_CR1[2:0]`. |
244
+ | `interpretation` | string | `"run/sleep"` when `SLEEPDEEP=false`; `"STOP/low-power likely"` when `true`. |
245
+
246
+ Output example (device running normally):
247
+
248
+ ```json
249
+ {"type": "device_state", "regs": {"PWR_CR1": 776, "PWR_SR1": 0, "RCC_CR": 62915840, "RCC_CFGR": 18, "SCB_SCR": 0, "DBGMCU_CR": 0}, "decoded": {"SLEEPDEEP": false, "LPMS": 0, "interpretation": "run/sleep"}, "accessible": true}
250
+ ```
251
+
252
+ The MCP tool exposes this as `crosspad_trace` with `action="device_state"` — it calls this subcommand and returns the `regs` and `decoded` objects in the response.
253
+
254
+ ---
255
+
256
+ ### EXPERIMENTAL: SWO / ITM channel decode
257
+
258
+ > **Status: EXPERIMENTAL — opt-in only, UNTESTED against a real ITM source.**
259
+ > The current CrossPad firmware does NOT emit ITM data on the SWO pin.
260
+ > This feature exists for future use when firmware-side ITM instrumentation is added.
261
+
262
+ #### What it does
263
+
264
+ When `--swo` is passed, the daemon additionally starts a pyOCD `SWVReader` background thread that reads raw SWO bytes from the probe, passes them through the `SWOParser`, and captures the decoded ITM stimulus-port values in a per-port accumulator. On each poll cycle the latest captured value for each mapped port is merged into the `values` object of the outgoing sample frame alongside the normal RAM-polled signals.
265
+
266
+ #### Syntax
267
+
268
+ ```
269
+ --swo PORT:NAME[,PORT:NAME,...]
270
+ ```
271
+
272
+ Each token maps an ITM stimulus port number to a signal name that will appear in the `values` field of each `sample` frame.
273
+
274
+ Examples:
275
+ ```bash
276
+ # Map ITM port 0 → "dbg_phase", port 1 → "isr_us"
277
+ python swd_tracer.py trace \
278
+ --elf firmware.elf \
279
+ --signals s_vbat_mv \
280
+ --swo 0:dbg_phase,1:isr_us \
281
+ --rate 100
282
+ ```
283
+
284
+ The MCP tool exposes this as:
285
+ ```json
286
+ { "action": "start", "signals": ["s_vbat_mv"], "swo": ["0:dbg_phase", "1:isr_us"] }
287
+ ```
288
+
289
+ #### Additional flags (SWO path only)
290
+
291
+ | Flag | Default | Description |
292
+ |---|---|---|
293
+ | `--cpu-hz` | `64000000` | Core clock frequency in Hz for SWO baud derivation. **Must match the actual firmware clock.** CrossPad r20 runs at 64 MHz; confirm in the `.ioc` / CubeMX clock config. |
294
+ | `--swo-hz` | `2000000` | Desired SWO output baud in Hz. **Must match `TPIU_ACPR` configured in the firmware.** |
295
+
296
+ These are kept at daemon-level defaults and are not currently plumbed through the MCP `swo[]` input (they can be added when there is a real firmware source to test against).
297
+
298
+ #### Fail-soft behaviour
299
+
300
+ If SWV initialisation fails (e.g. the probe does not support SWO, the target lacks ITM/TPIU, or the SWO clock cannot be set), the daemon logs a message to stderr and continues with plain RAM polling — it never crashes. ITM ports that receive no data simply produce no entry in `values` for that cycle.
301
+
302
+ #### pyOCD 0.44 API note
303
+
304
+ `SWVReader.init(sys_clock, swo_clock, console:TextIO) -> bool` — the console parameter is accepted for text output but the ITM sink is injected by re-connecting the internal `SWOParser` to our `_ITMValueSink` after `init()` returns.
305
+
306
+ ---
307
+
308
+ #### pyOCD target name note
309
+
310
+ The daemon defaults to `--target cortex_m`, the generic Cortex-M target built into
311
+ pyOCD. It needs **no CMSIS pack** and is sufficient for everything this tool does
312
+ (non-halting RAM polling + the absolute-address register reads in `device-state`).
313
+ This default is verified working on an ST-Link V2 + STM32G0Bx.
314
+
315
+ A part-specific target is **optional** — only needed if you want pyOCD's
316
+ part-aware features (flash programming, named peripherals, ITM/TPIU descriptors
317
+ for SWO). To use one, install the Keil DFP pack and pass the part name:
318
+
319
+ ```bash
320
+ pyocd pack install Keil.STM32G0xx_DFP
321
+ pyocd pack find g0b1 # list available part names
322
+ # then, e.g.:
323
+ swd_tracer.py trace ... --target stm32g0b1retx
324
+ ```
325
+
326
+ The RETx variant matches CrossPad r20; substitute the CB/CC/CE part if your flash
327
+ size differs.