crosspad-mcp-server 8.1.2 → 9.0.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 +369 -49
- 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 +1066 -0
- package/tracer/ui/index.html +834 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export class TraceBuffer {
|
|
2
|
+
signals;
|
|
3
|
+
capacity;
|
|
4
|
+
buf = [];
|
|
5
|
+
constructor(signals, capacity) {
|
|
6
|
+
this.signals = signals;
|
|
7
|
+
this.capacity = capacity;
|
|
8
|
+
}
|
|
9
|
+
push(s) {
|
|
10
|
+
this.buf.push(s);
|
|
11
|
+
if (this.buf.length > this.capacity)
|
|
12
|
+
this.buf.shift();
|
|
13
|
+
}
|
|
14
|
+
count() { return this.buf.length; }
|
|
15
|
+
signalNames() { return [...this.signals]; }
|
|
16
|
+
/** Add a signal to the watched set (no-op if already present). Does not touch
|
|
17
|
+
* stored samples — history of a previously-removed signal survives in the ring. */
|
|
18
|
+
addSignal(name) {
|
|
19
|
+
if (!this.signals.includes(name))
|
|
20
|
+
this.signals.push(name);
|
|
21
|
+
}
|
|
22
|
+
/** Drop a signal from the watched set (no-op if absent). Stored samples are
|
|
23
|
+
* left intact so already-captured history remains queryable until it ages out. */
|
|
24
|
+
removeSignal(name) {
|
|
25
|
+
const i = this.signals.indexOf(name);
|
|
26
|
+
if (i >= 0)
|
|
27
|
+
this.signals.splice(i, 1);
|
|
28
|
+
}
|
|
29
|
+
stats(sig) {
|
|
30
|
+
const pts = this.buf.filter((s) => sig in s.values);
|
|
31
|
+
if (pts.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
let min = Infinity, max = -Infinity, sum = 0;
|
|
34
|
+
for (const p of pts) {
|
|
35
|
+
const v = p.values[sig];
|
|
36
|
+
if (v < min)
|
|
37
|
+
min = v;
|
|
38
|
+
if (v > max)
|
|
39
|
+
max = v;
|
|
40
|
+
sum += v;
|
|
41
|
+
}
|
|
42
|
+
const first = pts[0], last = pts[pts.length - 1];
|
|
43
|
+
const dt = last.t - first.t;
|
|
44
|
+
const slope = dt !== 0 ? (last.values[sig] - first.values[sig]) / dt : 0;
|
|
45
|
+
return { min, max, avg: sum / pts.length, last: last.values[sig], slope, first_t: first.t, last_t: last.t, n: pts.length };
|
|
46
|
+
}
|
|
47
|
+
downsample(sig, maxPoints, window) {
|
|
48
|
+
let pts = this.buf
|
|
49
|
+
.filter((s) => sig in s.values)
|
|
50
|
+
.map((s) => ({ t: s.t, v: s.values[sig] }));
|
|
51
|
+
if (window) {
|
|
52
|
+
const lo = window.fromT ?? -Infinity, hi = window.toT ?? Infinity;
|
|
53
|
+
pts = pts.filter((p) => p.t >= lo && p.t <= hi);
|
|
54
|
+
}
|
|
55
|
+
if (pts.length <= maxPoints)
|
|
56
|
+
return pts;
|
|
57
|
+
const last = pts[pts.length - 1];
|
|
58
|
+
if (maxPoints <= 1)
|
|
59
|
+
return [last];
|
|
60
|
+
const stride = Math.ceil(pts.length / maxPoints);
|
|
61
|
+
const out = [];
|
|
62
|
+
for (let i = 0; i < pts.length - 1; i += stride) {
|
|
63
|
+
out.push(pts[i]);
|
|
64
|
+
if (out.length >= maxPoints - 1)
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
if (out[out.length - 1].t !== last.t)
|
|
68
|
+
out.push(last);
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Read-only view of stored samples (oldest→newest). */
|
|
72
|
+
samples() { return this.buf; }
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=trace-buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-buffer.js","sourceRoot":"","sources":["../../src/tools/trace-buffer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface DeviceStateResult {
|
|
2
|
+
success: boolean;
|
|
3
|
+
regs?: Record<string, number | null>;
|
|
4
|
+
decoded?: Record<string, unknown>;
|
|
5
|
+
accessible?: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Pure: extract the device_state JSON from mixed daemon output (scan lines reverse). */
|
|
9
|
+
export declare function parseDeviceState(out: string): DeviceStateResult;
|
|
10
|
+
export declare function getDeviceState(signal?: AbortSignal): Promise<DeviceStateResult>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { runArgvStream } from "../utils/exec.js";
|
|
2
|
+
import { daemonPath, resolvedPython } from "./trace-symbols.js";
|
|
3
|
+
/** Pure: extract the device_state JSON from mixed daemon output (scan lines reverse). */
|
|
4
|
+
export function parseDeviceState(out) {
|
|
5
|
+
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
6
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
7
|
+
const l = lines[i];
|
|
8
|
+
if (l.startsWith("{") && l.includes('"device_state"')) {
|
|
9
|
+
try {
|
|
10
|
+
const o = JSON.parse(l);
|
|
11
|
+
if (o.type === "device_state") {
|
|
12
|
+
return { success: true, regs: o.regs, decoded: o.decoded, accessible: o.accessible, error: o.error };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch { /* keep scanning */ }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { success: false, error: out.split("\n").filter(Boolean).slice(-3).join(" | ") || "no output" };
|
|
19
|
+
}
|
|
20
|
+
export async function getDeviceState(signal) {
|
|
21
|
+
let out = "";
|
|
22
|
+
await runArgvStream(resolvedPython(), [daemonPath(), "device-state"], process.cwd(), (s, line) => { if (s === "stdout")
|
|
23
|
+
out += line + "\n"; }, 30_000, signal);
|
|
24
|
+
return parseDeviceState(out);
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=trace-device.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-device.js","sourceRoot":"","sources":["../../src/tools/trace-device.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAUhE,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACnE,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACnB,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACtD,IAAI,CAAC;gBACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBACxB,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;oBAC9B,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;gBACvG,CAAC;YACH,CAAC;YAAC,MAAM,CAAC,CAAC,mBAAmB,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,WAAW,EAAE,CAAC;AACzG,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAAoB;IACvD,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,MAAM,aAAa,CAAC,cAAc,EAAE,EAAE,CAAC,UAAU,EAAE,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,GAAG,EAAE,EACjF,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,QAAQ;QAAE,GAAG,IAAI,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5E,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type Severity = "blocking" | "error" | "warning" | "info";
|
|
2
|
+
export interface DoctorIssue {
|
|
3
|
+
id: string;
|
|
4
|
+
severity: Severity;
|
|
5
|
+
detail: string;
|
|
6
|
+
suggested_fix: string;
|
|
7
|
+
}
|
|
8
|
+
export interface DoctorProbe {
|
|
9
|
+
pyocdInstalled: () => boolean;
|
|
10
|
+
elfPath: () => string;
|
|
11
|
+
elfExists: () => boolean;
|
|
12
|
+
stlinkProbe: () => {
|
|
13
|
+
found: boolean;
|
|
14
|
+
serial?: string;
|
|
15
|
+
chipid?: string;
|
|
16
|
+
};
|
|
17
|
+
udevRulesPresent: () => boolean;
|
|
18
|
+
/** Which user-config keys are explicitly set (not on fallback). */
|
|
19
|
+
configKeysSet: () => string[];
|
|
20
|
+
/** §11.7 real probe-presence detection. Runs `pyocd list` (preferred, via the
|
|
21
|
+
* daemon python) or `st-info --probe` with a short timeout. `present` =
|
|
22
|
+
* at least one probe enumerated; `toolAvailable` distinguishes "no probe on
|
|
23
|
+
* USB" from "neither pyocd nor st-info installed" (the latter must NOT be a
|
|
24
|
+
* false "no probe"). */
|
|
25
|
+
probeList?: () => Promise<{
|
|
26
|
+
present: boolean;
|
|
27
|
+
toolAvailable: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export interface DoctorResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
issues: DoctorIssue[];
|
|
33
|
+
probe?: {
|
|
34
|
+
serial?: string;
|
|
35
|
+
chipid?: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare function runDoctor(p: DoctorProbe): Promise<DoctorResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Production wiring: reads the actual filesystem, runs st-info, and imports
|
|
41
|
+
* pyocd to validate the environment. Safe to call even when tools are absent.
|
|
42
|
+
*/
|
|
43
|
+
export declare function realProbe(): DoctorProbe;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/tools/trace-doctor.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { runCommand } from "../utils/exec.js";
|
|
4
|
+
import { resolveConfigValue, loadUserConfig } from "../utils/userConfig.js";
|
|
5
|
+
import { STM_ELF_DEFAULT } from "../config.js";
|
|
6
|
+
export async function runDoctor(p) {
|
|
7
|
+
const issues = [];
|
|
8
|
+
if (!p.pyocdInstalled()) {
|
|
9
|
+
issues.push({
|
|
10
|
+
id: "pyocd_missing",
|
|
11
|
+
severity: "blocking",
|
|
12
|
+
detail: "pyocd Python package not importable — required to talk to the ST-Link.",
|
|
13
|
+
suggested_fix: "Run: pip install pyocd pyelftools (use the interpreter set in config key 'pyocd_python').",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (!p.elfExists()) {
|
|
17
|
+
issues.push({
|
|
18
|
+
id: "elf_missing",
|
|
19
|
+
severity: "blocking",
|
|
20
|
+
detail: `Firmware ELF not found at ${p.elfPath()} — symbol resolution needs it.`,
|
|
21
|
+
suggested_fix: "Build a Debug firmware (cmake --build build/Debug) or set config key 'stm_elf_path' to the real ELF.",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const probe = p.stlinkProbe();
|
|
25
|
+
// st-info's negative is authoritative ONLY when the §11.7 probeList check
|
|
26
|
+
// (`pyocd list` — the actual trace mechanism) isn't wired. stlinkProbe()
|
|
27
|
+
// returns found:false when st-info is merely *not installed*, which must not
|
|
28
|
+
// block tracing if pyocd can still enumerate the probe. When probeList is
|
|
29
|
+
// present it owns probe presence (below), so skip this redundant block.
|
|
30
|
+
if (!probe.found && !p.probeList) {
|
|
31
|
+
issues.push({
|
|
32
|
+
id: "probe_missing",
|
|
33
|
+
severity: "blocking",
|
|
34
|
+
detail: "No ST-Link probe detected (st-info --probe found nothing).",
|
|
35
|
+
suggested_fix: "Connect the ST-Link, check the SWD cable and target power.",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (!p.udevRulesPresent()) {
|
|
39
|
+
issues.push({
|
|
40
|
+
id: "udev_missing",
|
|
41
|
+
severity: "warning",
|
|
42
|
+
detail: "No ST-Link udev rules found in /etc/udev/rules.d — pyOCD/libusb may be denied access without root.",
|
|
43
|
+
suggested_fix: "Install ST-Link udev rules: write /etc/udev/rules.d/49-stlinkv2.rules (SUBSYSTEMS=='usb', ATTRS{idVendor}=='0483', ATTRS{idProduct}=='3748', MODE='0666'), then: sudo udevadm control --reload-rules && sudo udevadm trigger. (st-info succeeding only proves the current user already has access.)",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// §11.7 real probe-presence check (distinct from the st-info `probe_missing`
|
|
47
|
+
// above and from the udev warning). Blocks `start` so a vanished/replug-needed
|
|
48
|
+
// ST-Link surfaces a clear, actionable error instead of a daemon connect hang.
|
|
49
|
+
// "tool missing" (neither pyocd nor st-info installed) is a non-fatal note, not
|
|
50
|
+
// a false "no probe".
|
|
51
|
+
if (p.probeList) {
|
|
52
|
+
const r = await p.probeList();
|
|
53
|
+
if (!r.toolAvailable) {
|
|
54
|
+
issues.push({
|
|
55
|
+
id: "probe_list_tool_missing",
|
|
56
|
+
severity: "info",
|
|
57
|
+
detail: "Could not run a probe-presence check — neither `pyocd list` nor `st-info` is available.",
|
|
58
|
+
suggested_fix: "Install pyocd (pip install pyocd) or stlink-tools (st-info) to enable USB probe detection.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else if (!r.present) {
|
|
62
|
+
issues.push({
|
|
63
|
+
id: "no_probe_detected",
|
|
64
|
+
severity: "error",
|
|
65
|
+
detail: "No ST-Link detected on USB (replug the probe).",
|
|
66
|
+
suggested_fix: "Reconnect the ST-Link USB cable; verify with `pyocd list` / `lsusb`.",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (p.configKeysSet().length === 0) {
|
|
71
|
+
issues.push({
|
|
72
|
+
id: "config_defaults",
|
|
73
|
+
severity: "info",
|
|
74
|
+
detail: "No user-config keys set — all paths are on built-in/env defaults.",
|
|
75
|
+
suggested_fix: "After resolving the above, persist resolved paths with crosspad_trace action=config_set.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const ok = !issues.some((i) => i.severity === "blocking" || i.severity === "error");
|
|
79
|
+
return { ok, issues, probe: probe.found ? { serial: probe.serial, chipid: probe.chipid } : undefined };
|
|
80
|
+
}
|
|
81
|
+
function resolvedElfPath() {
|
|
82
|
+
return resolveConfigValue("stm_elf_path", "CROSSPAD_STM_ELF", process.env.CROSSPAD_STM_ELF, STM_ELF_DEFAULT);
|
|
83
|
+
}
|
|
84
|
+
function resolvedPython() {
|
|
85
|
+
return resolveConfigValue("pyocd_python", "CROSSPAD_TRACE_PYTHON", process.env.CROSSPAD_TRACE_PYTHON, "python3");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Production wiring: reads the actual filesystem, runs st-info, and imports
|
|
89
|
+
* pyocd to validate the environment. Safe to call even when tools are absent.
|
|
90
|
+
*/
|
|
91
|
+
export function realProbe() {
|
|
92
|
+
return {
|
|
93
|
+
pyocdInstalled: () => {
|
|
94
|
+
const r = runCommand(`${resolvedPython()} -c "import pyocd, elftools"`, process.cwd(), 10_000);
|
|
95
|
+
return r.success;
|
|
96
|
+
},
|
|
97
|
+
elfPath: resolvedElfPath,
|
|
98
|
+
elfExists: () => {
|
|
99
|
+
try {
|
|
100
|
+
return fs.existsSync(resolvedElfPath());
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
stlinkProbe: () => {
|
|
107
|
+
const r = runCommand("st-info --probe", process.cwd(), 10_000);
|
|
108
|
+
if (!r.success || !/Found \d+ stlink/i.test(r.stdout))
|
|
109
|
+
return { found: false };
|
|
110
|
+
const serial = r.stdout.match(/serial:\s*([0-9A-Fa-f]+)/)?.[1];
|
|
111
|
+
const chipid = r.stdout.match(/chipid:\s*(0x[0-9A-Fa-f]+)/)?.[1];
|
|
112
|
+
return { found: true, serial, chipid };
|
|
113
|
+
},
|
|
114
|
+
udevRulesPresent: () => {
|
|
115
|
+
try {
|
|
116
|
+
return fs.readdirSync("/etc/udev/rules.d").some((f) => /stlink|49-stlinkv/i.test(f));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
configKeysSet: () => Object.keys(loadUserConfig()),
|
|
123
|
+
// §11.7: enumerate USB probes. Prefer `pyocd list` (same python the daemon
|
|
124
|
+
// uses, so it sees exactly what the trace will); fall back to `st-info
|
|
125
|
+
// --probe`. Short timeouts — a wedged/absent probe must fail fast, not hang.
|
|
126
|
+
probeList: async () => {
|
|
127
|
+
// `<python> -m pyocd list` prints one row per probe; "No available debug
|
|
128
|
+
// probes" (or an empty table) when none are connected.
|
|
129
|
+
const py = runCommand(`${resolvedPython()} -m pyocd list`, process.cwd(), 8_000);
|
|
130
|
+
if (py.success) {
|
|
131
|
+
const present = /\b0\d{6,}|STLink|ST-LINK|\bstlink\b/i.test(py.stdout) && !/no available debug probes/i.test(py.stdout);
|
|
132
|
+
// pyocd prints a header row even with 0 probes; treat an explicit
|
|
133
|
+
// "No available debug probes" OR a body with no probe-id rows as empty.
|
|
134
|
+
const hasRow = py.stdout.split("\n").some((l) => /^\s*\d+\s/.test(l));
|
|
135
|
+
return { present: present || hasRow, toolAvailable: true };
|
|
136
|
+
}
|
|
137
|
+
// pyocd absent/failed → try st-info.
|
|
138
|
+
const st = runCommand("st-info --probe", process.cwd(), 8_000);
|
|
139
|
+
if (st.success || st.stdout.length > 0) {
|
|
140
|
+
const present = /Found [1-9]\d* stlink/i.test(st.stdout);
|
|
141
|
+
// st-info ran (even "Found 0 stlink programmers" is a successful run).
|
|
142
|
+
const toolAvailable = /Found \d+ stlink/i.test(st.stdout) || st.success;
|
|
143
|
+
return { present, toolAvailable };
|
|
144
|
+
}
|
|
145
|
+
// Neither tool produced usable output → tool missing, not "no probe".
|
|
146
|
+
return { present: false, toolAvailable: false };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=trace-doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-doctor.js","sourceRoot":"","sources":["../../src/tools/trace-doctor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { TraceBuffer } from "./trace-buffer.js";
|
|
2
|
+
/** Render the buffer as CSV. Uses the documented samples() accessor on TraceBuffer. */
|
|
3
|
+
export declare function bufferToCsv(buf: TraceBuffer, signals: string[]): string;
|
|
4
|
+
export declare function writeCsv(path: string, buf: TraceBuffer, signals: string[]): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
/** Render the buffer as CSV. Uses the documented samples() accessor on TraceBuffer. */
|
|
3
|
+
export function bufferToCsv(buf, signals) {
|
|
4
|
+
const rows = [signals.length ? `t,${signals.join(",")}` : "t"];
|
|
5
|
+
for (const s of buf.samples()) {
|
|
6
|
+
const cells = signals.map((sig) => (sig in s.values ? String(s.values[sig]) : ""));
|
|
7
|
+
rows.push([String(s.t), ...cells].join(","));
|
|
8
|
+
}
|
|
9
|
+
return rows.join("\n");
|
|
10
|
+
}
|
|
11
|
+
export function writeCsv(path, buf, signals) {
|
|
12
|
+
fs.writeFileSync(path, bufferToCsv(buf, signals));
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=trace-export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-export.js","sourceRoot":"","sources":["../../src/tools/trace-export.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,uFAAuF;AACvF,MAAM,UAAU,WAAW,CAAC,GAAgB,EAAE,OAAiB;IAC7D,MAAM,IAAI,GAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACzE,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACnF,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,GAAgB,EAAE,OAAiB;IACxE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,WAAW,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { TraceBuffer } from "./trace-buffer.js";
|
|
2
|
+
export interface SignalSpec {
|
|
3
|
+
name: string;
|
|
4
|
+
address: number;
|
|
5
|
+
size: number;
|
|
6
|
+
encoding: string;
|
|
7
|
+
}
|
|
8
|
+
export type Frame = {
|
|
9
|
+
type: "sample";
|
|
10
|
+
t: number;
|
|
11
|
+
values: Record<string, number>;
|
|
12
|
+
} | {
|
|
13
|
+
type: "status";
|
|
14
|
+
device_state: string;
|
|
15
|
+
t?: number;
|
|
16
|
+
samples?: number;
|
|
17
|
+
} | {
|
|
18
|
+
type: "error";
|
|
19
|
+
error: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: "signals";
|
|
22
|
+
signals: SignalSpec[];
|
|
23
|
+
unresolved: string[];
|
|
24
|
+
};
|
|
25
|
+
/** Pure: parse one NDJSON line from the daemon into a Frame, or null. */
|
|
26
|
+
export declare function parseFrame(line: string): Frame | null;
|
|
27
|
+
export interface SessionOpts {
|
|
28
|
+
signals: string[];
|
|
29
|
+
rateHz: number;
|
|
30
|
+
elf?: string;
|
|
31
|
+
outFile?: string;
|
|
32
|
+
capacity?: number;
|
|
33
|
+
swo?: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare class TraceSession {
|
|
36
|
+
private opts;
|
|
37
|
+
private proc;
|
|
38
|
+
buffer: TraceBuffer;
|
|
39
|
+
deviceState: string;
|
|
40
|
+
startedAt: number;
|
|
41
|
+
filePath: string | null;
|
|
42
|
+
private stdoutBuf;
|
|
43
|
+
private onFrameExtra;
|
|
44
|
+
private pendingReconcile;
|
|
45
|
+
private termTimer;
|
|
46
|
+
private killTimer;
|
|
47
|
+
private static readonly STDERR_RING;
|
|
48
|
+
private stderrLines;
|
|
49
|
+
private stderrBuf;
|
|
50
|
+
private sawErrorFrame;
|
|
51
|
+
private firstFrameSeen;
|
|
52
|
+
private firstFrameWaiters;
|
|
53
|
+
constructor(opts: SessionOpts);
|
|
54
|
+
onFrame(cb: (f: Frame) => void): void;
|
|
55
|
+
/** Detach the frame sink so late `sample`/`status` frames during a shutdown
|
|
56
|
+
* race aren't rebroadcast after `trace_end`, and the consumer (Dashboard)
|
|
57
|
+
* doesn't hold this session longer than necessary. */
|
|
58
|
+
offFrame(): void;
|
|
59
|
+
private stoppedCbs;
|
|
60
|
+
/** Register a callback for when the daemon process has fully exited. If the
|
|
61
|
+
* process is already gone, fires synchronously. */
|
|
62
|
+
onStopped(cb: () => void): void;
|
|
63
|
+
private fireStopped;
|
|
64
|
+
/** The ELF this session traces against (explicit opt or the configured default).
|
|
65
|
+
* Used by the web UI's /symbols endpoint so autocomplete matches the trace. */
|
|
66
|
+
elfPath(): string;
|
|
67
|
+
start(): void;
|
|
68
|
+
/** §11.6: fold daemon stderr into a bounded ring (last STDERR_RING lines). */
|
|
69
|
+
private ingestStderr;
|
|
70
|
+
/** §11.6: the last `n` captured daemon stderr lines, newest last. */
|
|
71
|
+
stderrTail(n?: number): string;
|
|
72
|
+
/** Daemon exited. §11.6: if it died non-zero WITHOUT emitting an error frame,
|
|
73
|
+
* synthesize an `error: <stderr tail>` device_state so status/start never
|
|
74
|
+
* reports a stale "running" for a process that's actually dead. */
|
|
75
|
+
private onExit;
|
|
76
|
+
private ingest;
|
|
77
|
+
/** Reconcile the buffer's watched-signal set to match the daemon's authoritative
|
|
78
|
+
* list (from a "signals" frame): add any new names, drop any no longer present. */
|
|
79
|
+
private reconcileSignals;
|
|
80
|
+
/** Add signals to the live poll set (NDJSON `add` to daemon stdin).
|
|
81
|
+
* Resolves with the POST-reconcile signal-name set once the daemon's next
|
|
82
|
+
* "signals" frame is ingested (PROTOCOL §4); ~2 s timeout → resolves with the
|
|
83
|
+
* current `buffer.signalNames()` so a missing frame never hangs the caller. */
|
|
84
|
+
addSignals(specs: string[]): Promise<string[]>;
|
|
85
|
+
/** Remove signals from the live poll set (NDJSON `remove` to daemon stdin).
|
|
86
|
+
* Same post-reconcile / timeout contract as addSignals. */
|
|
87
|
+
removeSignals(specs: string[]): Promise<string[]>;
|
|
88
|
+
/** Queue a resolver fired by the next "signals" frame; falls back to the
|
|
89
|
+
* current signal set after ~2 s so callers can't hang on a silent daemon. */
|
|
90
|
+
private awaitReconcile;
|
|
91
|
+
/** §11.6: resolve on the FIRST ingested frame (signals|sample|error), or
|
|
92
|
+
* `null` on timeout / if the daemon exits first. Lets MCP `start` report a
|
|
93
|
+
* truthful device_state instead of an optimistic "running". Idempotent:
|
|
94
|
+
* if a frame was already seen before the call, resolves immediately. */
|
|
95
|
+
waitForFirstFrame(timeoutMs?: number): Promise<Frame | null>;
|
|
96
|
+
private lastFirstFrame;
|
|
97
|
+
/** Fire all pending first-frame waiters once. `f` is the triggering frame
|
|
98
|
+
* (or null when the daemon exited / spawn-errored before any frame). */
|
|
99
|
+
private resolveFirstFrame;
|
|
100
|
+
/** Cancel any pending SIGTERM/SIGKILL escalation timers (clean exit). */
|
|
101
|
+
private clearKillTimers;
|
|
102
|
+
/** §11.5 guaranteed teardown — escalate {cmd:stop} → SIGTERM → SIGKILL.
|
|
103
|
+
* Idempotent and safe to call when `proc` is already null (the daemon may
|
|
104
|
+
* have self-exited on a connect failure / crash / target reset). A wedged
|
|
105
|
+
* daemon blocked in uninterruptible libusb ignores SIGTERM, so the SIGKILL
|
|
106
|
+
* fallback is the ONLY thing that guarantees no `pkill -9` is ever needed.
|
|
107
|
+
* The escalation timers are cleared on real exit (see onExit) so a clean
|
|
108
|
+
* shutdown never fires a stray kill at a recycled PID.
|
|
109
|
+
*
|
|
110
|
+
* §12.1: stop() ONLY ends the daemon now. The web UI is owned by the
|
|
111
|
+
* persistent Dashboard singleton (see trace-webui.ts), which OUTLIVES the
|
|
112
|
+
* session — the MCP `stop` handler calls dashboard.unbind() so the server
|
|
113
|
+
* keeps listening and the dashboard tab survives across traces. */
|
|
114
|
+
stop(): void;
|
|
115
|
+
isRunning(): boolean;
|
|
116
|
+
}
|
|
117
|
+
export declare function getActiveSession(): TraceSession | null;
|
|
118
|
+
export declare function setActiveSession(s: TraceSession | null): void;
|