crosspad-mcp-server 9.1.0 → 9.2.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/plugin.json +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +129 -29
- package/dist/index.js.map +1 -1
- package/dist/tools/stm-build.d.ts +23 -0
- package/dist/tools/stm-build.js +78 -0
- package/dist/tools/stm-build.js.map +1 -0
- package/dist/tools/stm-flash.d.ts +36 -0
- package/dist/tools/stm-flash.js +112 -0
- package/dist/tools/stm-flash.js.map +1 -0
- package/dist/tools/trace-session.d.ts +12 -0
- package/dist/tools/trace-session.js +69 -0
- package/dist/tools/trace-session.js.map +1 -1
- package/dist/tools/trace-webui.d.ts +14 -7
- package/dist/tools/trace-webui.js +77 -33
- package/dist/tools/trace-webui.js.map +1 -1
- package/dist/tools/trace-write.d.ts +9 -0
- package/dist/tools/trace-write.js +32 -0
- package/dist/tools/trace-write.js.map +1 -0
- package/dist/utils/userConfig.d.ts +9 -0
- package/dist/utils/userConfig.js.map +1 -1
- package/package.json +2 -1
- package/skills/swd-tracer/SKILL.md +48 -0
- package/tracer/PROTOCOL.md +69 -0
- package/tracer/swd_tracer.py +460 -15
- package/tracer/ui/config.js +40 -0
- package/tracer/ui/connection.js +170 -0
- package/tracer/ui/index.html +50 -798
- package/tracer/ui/main.js +30 -0
- package/tracer/ui/plot.js +337 -0
- package/tracer/ui/signals.js +39 -0
- package/tracer/ui/state.js +57 -0
- package/tracer/ui/stats.js +52 -0
- package/tracer/ui/style.css +97 -0
- package/tracer/ui/symbols.js +80 -0
- package/tracer/ui/toolbar.js +95 -0
- package/tracer/ui/util.js +62 -0
- package/tracer/ui/watchlist.js +179 -0
- package/vscode-extension/README.md +32 -0
- package/vscode-extension/extension.js +56 -0
- package/vscode-extension/package.json +14 -0
package/tracer/swd_tracer.py
CHANGED
|
@@ -52,11 +52,19 @@ def _iter_addr_globals(dwarf):
|
|
|
52
52
|
info_die = die
|
|
53
53
|
name = die.attributes.get("DW_AT_name")
|
|
54
54
|
if name is None:
|
|
55
|
-
spec = die.attributes.get("DW_AT_specification")
|
|
56
|
-
|
|
55
|
+
spec = (die.attributes.get("DW_AT_specification")
|
|
56
|
+
or die.attributes.get("DW_AT_abstract_origin"))
|
|
57
57
|
if spec is not None:
|
|
58
|
+
# The reference form decides the offset basis:
|
|
59
|
+
# DW_FORM_ref_addr is an absolute .debug_info offset, so it
|
|
60
|
+
# must NOT have cu_offset added; the CU-relative forms
|
|
61
|
+
# (ref1/2/4/8/ref_udata) are relative to the CU and do.
|
|
58
62
|
try:
|
|
59
|
-
|
|
63
|
+
if spec.form == "DW_FORM_ref_addr":
|
|
64
|
+
refaddr = spec.value
|
|
65
|
+
else:
|
|
66
|
+
refaddr = spec.value + cu.cu_offset
|
|
67
|
+
info_die = cu.dwarfinfo.get_DIE_from_refaddr(refaddr)
|
|
60
68
|
name = info_die.attributes.get("DW_AT_name")
|
|
61
69
|
except Exception:
|
|
62
70
|
pass
|
|
@@ -687,6 +695,328 @@ def _expand_spec(spec, table):
|
|
|
687
695
|
specs = [_spec_name(base, p) for p in parts_lists]
|
|
688
696
|
return specs, len(specs)
|
|
689
697
|
|
|
698
|
+
# Raw absolute-address spec (PROTOCOL §1.2): read MCU memory straight by address
|
|
699
|
+
# with no DWARF symbol — for peripheral registers / arbitrary RAM. Because there
|
|
700
|
+
# is no type to infer size/encoding from, the type tag is explicit (default u32).
|
|
701
|
+
# @0x40021000 -> u32 at 0x40021000
|
|
702
|
+
# @0x40021000:u16 -> u8|u16|u32 / i8|i16|i32 / f32
|
|
703
|
+
# @0x20000000:u8[16] -> 16 consecutive elements (expands like an array)
|
|
704
|
+
_RAW_RE = re.compile(
|
|
705
|
+
r"^@(0[xX][0-9A-Fa-f]+|\d+)" # 1: address (hex 0x… or decimal)
|
|
706
|
+
r"(?::([uif](?:8|16|32)))?" # 2: optional type tag
|
|
707
|
+
r"(?:\[(\d+)\])?$") # 3: optional element count
|
|
708
|
+
_RAW_TYPE = {
|
|
709
|
+
"u8": ("uint", 1), "u16": ("uint", 2), "u32": ("uint", 4),
|
|
710
|
+
"i8": ("int", 1), "i16": ("int", 2), "i32": ("int", 4),
|
|
711
|
+
"f32": ("float", 4),
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
def _resolve_raw(spec):
|
|
715
|
+
"""Resolve a raw @address spec.
|
|
716
|
+
|
|
717
|
+
Returns None when `spec` is not a raw spec at all (no leading '@'), so the
|
|
718
|
+
caller falls through to DWARF symbol resolution. Otherwise returns
|
|
719
|
+
{"n": count, "elems": [{name,address,size,encoding}, ...]} — a malformed raw
|
|
720
|
+
spec yields n=0/elems=[] (reported unresolved by the caller).
|
|
721
|
+
"""
|
|
722
|
+
if not spec.startswith("@"):
|
|
723
|
+
return None
|
|
724
|
+
m = _RAW_RE.match(spec)
|
|
725
|
+
if not m:
|
|
726
|
+
return {"n": 0, "elems": []} # looks raw but malformed
|
|
727
|
+
addr = int(m.group(1), 0)
|
|
728
|
+
tname = m.group(2) or "u32"
|
|
729
|
+
enc, size = _RAW_TYPE[tname]
|
|
730
|
+
count = int(m.group(3)) if m.group(3) else 1
|
|
731
|
+
elems = []
|
|
732
|
+
for i in range(count):
|
|
733
|
+
a = addr + i * size
|
|
734
|
+
# Single element keeps the user's spec verbatim as its name; an expanded
|
|
735
|
+
# block re-renders each element's concrete address so names stay unique.
|
|
736
|
+
nm = spec if count == 1 else "@0x%X:%s" % (a, tname)
|
|
737
|
+
elems.append({"name": nm, "address": a, "size": size, "encoding": enc})
|
|
738
|
+
return {"n": count, "elems": elems}
|
|
739
|
+
|
|
740
|
+
def _split_transform(spec):
|
|
741
|
+
"""Peel a trailing bit/mask transform suffix (#... or &...) off a spec.
|
|
742
|
+
|
|
743
|
+
Base specs never contain '#' or '&', so the earliest occurrence of either
|
|
744
|
+
starts the suffix. Returns (base, suffix) with the sigil kept on the suffix,
|
|
745
|
+
or (spec, None) when there is no transform.
|
|
746
|
+
"""
|
|
747
|
+
idxs = [i for i in (spec.find("#"), spec.find("&")) if i >= 0]
|
|
748
|
+
if not idxs:
|
|
749
|
+
return spec, None
|
|
750
|
+
i = min(idxs)
|
|
751
|
+
return spec[:i], spec[i:]
|
|
752
|
+
|
|
753
|
+
def _parse_transform(suffix, size):
|
|
754
|
+
"""Validate a transform suffix against the base byte size; return a transform
|
|
755
|
+
dict or None. bits = size*8. '#N' bit, '#hi:lo' range (inclusive, hi>=lo),
|
|
756
|
+
'&0xMASK' mask (normalized to the lowest set bit)."""
|
|
757
|
+
bits = size * 8
|
|
758
|
+
if suffix.startswith("#"):
|
|
759
|
+
body = suffix[1:]
|
|
760
|
+
if ":" in body:
|
|
761
|
+
parts = body.split(":")
|
|
762
|
+
if len(parts) != 2:
|
|
763
|
+
return None
|
|
764
|
+
try:
|
|
765
|
+
hi, lo = int(parts[0]), int(parts[1])
|
|
766
|
+
except ValueError:
|
|
767
|
+
return None
|
|
768
|
+
if lo < 0 or hi < lo or hi >= bits:
|
|
769
|
+
return None
|
|
770
|
+
return {"kind": "range", "hi": hi, "lo": lo}
|
|
771
|
+
try:
|
|
772
|
+
n = int(body)
|
|
773
|
+
except ValueError:
|
|
774
|
+
return None
|
|
775
|
+
if n < 0 or n >= bits:
|
|
776
|
+
return None
|
|
777
|
+
return {"kind": "bit", "n": n}
|
|
778
|
+
if suffix.startswith("&"):
|
|
779
|
+
try:
|
|
780
|
+
mask = int(suffix[1:], 0)
|
|
781
|
+
except ValueError:
|
|
782
|
+
return None
|
|
783
|
+
if mask <= 0 or mask >= (1 << bits):
|
|
784
|
+
return None
|
|
785
|
+
shift = (mask & -mask).bit_length() - 1 # ffs: index of lowest set bit
|
|
786
|
+
return {"kind": "mask", "mask": mask, "shift": shift}
|
|
787
|
+
return None
|
|
788
|
+
|
|
789
|
+
def _parse_value(text, encoding, size):
|
|
790
|
+
"""Parse the RHS literal per the target encoding. int: hex 0x.. or decimal
|
|
791
|
+
(range-checked to the byte size, two's-complement for signed); float: f32."""
|
|
792
|
+
text = text.strip()
|
|
793
|
+
if encoding == "float":
|
|
794
|
+
return float(text)
|
|
795
|
+
v = int(text, 0) # 0x.. hex or decimal
|
|
796
|
+
bits = size * 8
|
|
797
|
+
if encoding in ("int", "char"):
|
|
798
|
+
lo, hi = -(1 << (bits - 1)), (1 << (bits - 1)) - 1
|
|
799
|
+
if not (lo <= v <= hi):
|
|
800
|
+
raise ValueError("value out of range for i%d" % bits)
|
|
801
|
+
if v < 0:
|
|
802
|
+
v &= (1 << bits) - 1 # store two's-complement
|
|
803
|
+
else:
|
|
804
|
+
if not (0 <= v < (1 << bits)):
|
|
805
|
+
raise ValueError("value out of range for u%d" % bits)
|
|
806
|
+
return v
|
|
807
|
+
|
|
808
|
+
def _parse_write_spec(spec, table):
|
|
809
|
+
"""'target=value' -> resolved write descriptor (see Interfaces)."""
|
|
810
|
+
if "=" not in spec:
|
|
811
|
+
return {"ok": False, "spec": spec, "error": "missing '=' (use target=value)"}
|
|
812
|
+
left, right = spec.split("=", 1)
|
|
813
|
+
left, right = left.strip(), right.strip()
|
|
814
|
+
# Transforms (#bit / &mask) are read-only, not supported on write targets.
|
|
815
|
+
base, suffix = _split_transform(left)
|
|
816
|
+
if suffix is not None:
|
|
817
|
+
return {"ok": False, "spec": spec,
|
|
818
|
+
"error": "transforms (#bit / &mask) are read-only, not supported on write targets"}
|
|
819
|
+
# Resolve the LHS address/size/encoding using the read resolvers.
|
|
820
|
+
raw = _resolve_raw(left)
|
|
821
|
+
if raw is not None:
|
|
822
|
+
if raw["n"] != 1:
|
|
823
|
+
return {"ok": False, "spec": spec,
|
|
824
|
+
"error": "block write unsupported; write one element"}
|
|
825
|
+
d = raw["elems"][0]
|
|
826
|
+
else:
|
|
827
|
+
d = _resolve_spec(left, table)
|
|
828
|
+
if not d:
|
|
829
|
+
return {"ok": False, "spec": spec, "error": "unknown target: %s" % left}
|
|
830
|
+
try:
|
|
831
|
+
val = _parse_value(right, d["encoding"], d["size"])
|
|
832
|
+
except ValueError as e:
|
|
833
|
+
return {"ok": False, "spec": spec, "error": str(e)}
|
|
834
|
+
return {"ok": True, "name": left, "address": d["address"],
|
|
835
|
+
"size": d["size"], "encoding": d["encoding"], "value": val}
|
|
836
|
+
|
|
837
|
+
# Cortex-M architectural memory map (ARMv6/7/8-M) — identical across STM32 series.
|
|
838
|
+
_SRAM = (0x20000000, 0x3FFFFFFF)
|
|
839
|
+
_ARCH_REGIONS = [
|
|
840
|
+
_SRAM, # SRAM
|
|
841
|
+
(0x40000000, 0x5FFFFFFF), # Peripheral + IOPORT
|
|
842
|
+
(0xE0000000, 0xE00FFFFF), # PPB / SCS (NVIC, SCB, DWT...)
|
|
843
|
+
]
|
|
844
|
+
|
|
845
|
+
def _check_allowed(addr, size, ram_regions=None):
|
|
846
|
+
"""Return None if [addr, addr+size) is a permitted write target, else an
|
|
847
|
+
error string. Code region 0x0..0x1FFFFFFF (flash/system/option) is blocked."""
|
|
848
|
+
if size not in (1, 2, 4):
|
|
849
|
+
return "bad access size %r" % size
|
|
850
|
+
if addr % size != 0:
|
|
851
|
+
return "0x%08X not %d-byte aligned" % (addr, size)
|
|
852
|
+
end = addr + size - 1
|
|
853
|
+
for lo, hi in _ARCH_REGIONS:
|
|
854
|
+
if lo <= addr and end <= hi:
|
|
855
|
+
# SRAM refinement: if pack RAM regions are known, require containment.
|
|
856
|
+
if (lo, hi) == _SRAM and ram_regions:
|
|
857
|
+
if not any(rl <= addr and end <= rh for rl, rh in ram_regions):
|
|
858
|
+
return "0x%08X outside mapped SRAM" % addr
|
|
859
|
+
return None
|
|
860
|
+
return "0x%08X outside write allowlist (Code region / unmapped is blocked)" % addr
|
|
861
|
+
|
|
862
|
+
def _ram_regions_from(session):
|
|
863
|
+
"""Best-effort (lo,hi) inclusive RAM ranges from the pyOCD memory map; None if
|
|
864
|
+
the (generic) target exposes none."""
|
|
865
|
+
try:
|
|
866
|
+
mm = session.target.get_memory_map()
|
|
867
|
+
rams = [(r.start, r.end) for r in mm if r.type.name.lower() == "ram"]
|
|
868
|
+
return rams or None
|
|
869
|
+
except Exception:
|
|
870
|
+
return None
|
|
871
|
+
|
|
872
|
+
_WSIZE = {1: "write8", 2: "write16", 4: "write32"}
|
|
873
|
+
_RSIZE = {1: "read8", 2: "read16", 4: "read32"}
|
|
874
|
+
|
|
875
|
+
def do_write(target, descriptors, ram_regions):
|
|
876
|
+
"""Apply each resolved write non-halting; read-back old/new for confirmation."""
|
|
877
|
+
out = []
|
|
878
|
+
for d in descriptors:
|
|
879
|
+
if not d.get("ok"):
|
|
880
|
+
out.append({"name": d.get("name", d.get("spec")), "ok": False,
|
|
881
|
+
"error": d["error"]})
|
|
882
|
+
continue
|
|
883
|
+
addr, size = d["address"], d["size"]
|
|
884
|
+
guard = _check_allowed(addr, size, ram_regions)
|
|
885
|
+
if guard:
|
|
886
|
+
out.append({"name": d["name"], "address": addr, "size": size,
|
|
887
|
+
"ok": False, "error": "write blocked: " + guard})
|
|
888
|
+
continue
|
|
889
|
+
try:
|
|
890
|
+
old = getattr(target, _RSIZE[size])(addr)
|
|
891
|
+
getattr(target, _WSIZE[size])(addr, d["value"] & ((1 << (size * 8)) - 1))
|
|
892
|
+
new = getattr(target, _RSIZE[size])(addr)
|
|
893
|
+
out.append({"name": d["name"], "address": addr, "size": size,
|
|
894
|
+
"ok": True, "old": old, "new": new})
|
|
895
|
+
except Exception as e:
|
|
896
|
+
out.append({"name": d["name"], "address": addr, "size": size,
|
|
897
|
+
"ok": False, "error": str(e)})
|
|
898
|
+
return out
|
|
899
|
+
|
|
900
|
+
def cmd_write(args):
|
|
901
|
+
specs = [s for s in args.writes.split(";") if s.strip()]
|
|
902
|
+
try:
|
|
903
|
+
table = build_symbol_table(args.elf)
|
|
904
|
+
except Exception as e:
|
|
905
|
+
print(json.dumps({"type": "error", "error": "ELF/DWARF error: %s" % e}), flush=True)
|
|
906
|
+
return
|
|
907
|
+
descriptors = [_parse_write_spec(s, table) for s in specs]
|
|
908
|
+
session, cerr, _ = _try_open_session(args.probe, args.target, args.connect_timeout)
|
|
909
|
+
if session is None:
|
|
910
|
+
print(json.dumps({"type": "write_result", "ok": False, "error": cerr,
|
|
911
|
+
"results": []}), flush=True)
|
|
912
|
+
return
|
|
913
|
+
try:
|
|
914
|
+
results = do_write(session.target, descriptors, _ram_regions_from(session))
|
|
915
|
+
finally:
|
|
916
|
+
try: session.close()
|
|
917
|
+
except Exception: pass
|
|
918
|
+
print(json.dumps({"type": "write_result",
|
|
919
|
+
"ok": all(r["ok"] for r in results) if results else False,
|
|
920
|
+
"results": results}), flush=True)
|
|
921
|
+
|
|
922
|
+
def _resolve_func(elf_path, name):
|
|
923
|
+
"""Entry address of a function symbol from the ELF .symtab (STT_FUNC)."""
|
|
924
|
+
from elftools.elf.elffile import ELFFile
|
|
925
|
+
with open(elf_path, "rb") as f:
|
|
926
|
+
elf = ELFFile(f)
|
|
927
|
+
symtab = elf.get_section_by_name(".symtab")
|
|
928
|
+
if symtab is None:
|
|
929
|
+
return None
|
|
930
|
+
for sym in symtab.iter_symbols():
|
|
931
|
+
if sym.name == name and sym["st_info"]["type"] == "STT_FUNC":
|
|
932
|
+
return sym["st_value"] & ~1 # clear Thumb bit
|
|
933
|
+
return None
|
|
934
|
+
|
|
935
|
+
_CALL_CTX = ["r0","r1","r2","r3","r4","r5","r6","r7","r8","r9","r10","r11","r12",
|
|
936
|
+
"sp","lr","pc","xpsr","primask"]
|
|
937
|
+
|
|
938
|
+
def do_call(target, entry, args, ret_type, timeout_s):
|
|
939
|
+
"""AAPCS function-call thunk. Halts the core, runs func(args), restores full
|
|
940
|
+
context, resumes. Returns {ok,r0,decoded?} or {ok:False,error}."""
|
|
941
|
+
from pyocd.core.target import Target
|
|
942
|
+
if len(args) > 4:
|
|
943
|
+
return {"ok": False, "error": "max 4 args (r0-r3); got %d" % len(args)}
|
|
944
|
+
saved = {}
|
|
945
|
+
trap = None
|
|
946
|
+
try:
|
|
947
|
+
target.halt()
|
|
948
|
+
for r in _CALL_CTX:
|
|
949
|
+
saved[r] = target.read_core_register(r)
|
|
950
|
+
# Return trap = reset-handler entry from the ACTIVE vector table (VTOR),
|
|
951
|
+
# so the call thunk is MCU-agnostic (not tied to the STM32 flash alias at
|
|
952
|
+
# 0x08000000). VTOR low bits are reserved/zero; mask for safe alignment.
|
|
953
|
+
# VTOR=0 after reset aliases the table at 0x00000000.
|
|
954
|
+
vtor = target.read32(0xE000ED08) & 0xFFFFFF00
|
|
955
|
+
trap = target.read32(vtor + 4) & ~1
|
|
956
|
+
target.set_breakpoint(trap, Target.BreakpointType.HW)
|
|
957
|
+
for i, a in enumerate(args):
|
|
958
|
+
target.write_core_register("r%d" % i, a & 0xFFFFFFFF)
|
|
959
|
+
target.write_core_register("primask", 1) # mask ISRs during thunk
|
|
960
|
+
target.write_core_register("lr", trap | 1) # Thumb return
|
|
961
|
+
target.write_core_register("pc", entry)
|
|
962
|
+
xpsr = saved["xpsr"] | (1 << 24) # ensure Thumb (T) bit
|
|
963
|
+
target.write_core_register("xpsr", xpsr)
|
|
964
|
+
target.resume()
|
|
965
|
+
deadline = time.monotonic() + timeout_s
|
|
966
|
+
while time.monotonic() < deadline:
|
|
967
|
+
if target.get_state() == Target.State.HALTED:
|
|
968
|
+
break
|
|
969
|
+
time.sleep(0.002)
|
|
970
|
+
else:
|
|
971
|
+
target.halt()
|
|
972
|
+
return {"ok": False, "error":
|
|
973
|
+
"call timed out after %gms (function did not return)" % (timeout_s * 1000)}
|
|
974
|
+
r0 = target.read_core_register("r0")
|
|
975
|
+
res = {"ok": True, "r0": r0}
|
|
976
|
+
if ret_type and ret_type != "u32":
|
|
977
|
+
res["decoded"] = _decode(r0.to_bytes(4, "little"), 0, *_RAW_TYPE[ret_type])
|
|
978
|
+
return res
|
|
979
|
+
except Exception as e:
|
|
980
|
+
return {"ok": False, "error": str(e)}
|
|
981
|
+
finally:
|
|
982
|
+
try:
|
|
983
|
+
if trap is not None:
|
|
984
|
+
target.remove_breakpoint(trap)
|
|
985
|
+
except Exception:
|
|
986
|
+
pass
|
|
987
|
+
for r, v in saved.items():
|
|
988
|
+
try:
|
|
989
|
+
target.write_core_register(r, v) # full context restore
|
|
990
|
+
except Exception:
|
|
991
|
+
pass
|
|
992
|
+
try:
|
|
993
|
+
target.resume() # firmware continues pre-call
|
|
994
|
+
except Exception:
|
|
995
|
+
pass
|
|
996
|
+
|
|
997
|
+
def cmd_call(args):
|
|
998
|
+
if not args.confirm:
|
|
999
|
+
print(json.dumps({"type": "call_result", "ok": False,
|
|
1000
|
+
"error": "call requires --confirm"}), flush=True)
|
|
1001
|
+
return
|
|
1002
|
+
entry = _resolve_func(args.elf, args.func_name)
|
|
1003
|
+
if entry is None:
|
|
1004
|
+
print(json.dumps({"type": "call_result", "ok": False,
|
|
1005
|
+
"error": "unknown function: %s" % args.func_name}), flush=True)
|
|
1006
|
+
return
|
|
1007
|
+
argv = [int(a, 0) for a in args.args.split(",") if a.strip()] if args.args else []
|
|
1008
|
+
session, cerr, _ = _try_open_session(args.probe, args.target, args.connect_timeout)
|
|
1009
|
+
if session is None:
|
|
1010
|
+
print(json.dumps({"type": "call_result", "ok": False, "error": cerr}), flush=True)
|
|
1011
|
+
return
|
|
1012
|
+
try:
|
|
1013
|
+
res = do_call(session.target, entry, argv, args.ret_type, args.timeout)
|
|
1014
|
+
finally:
|
|
1015
|
+
try: session.close()
|
|
1016
|
+
except Exception: pass
|
|
1017
|
+
res["type"] = "call_result"
|
|
1018
|
+
print(json.dumps(res), flush=True)
|
|
1019
|
+
|
|
690
1020
|
def _resolve_specs(specs, table):
|
|
691
1021
|
"""Resolve a list of specs, applying §1.1 array/vector/matrix expansion.
|
|
692
1022
|
|
|
@@ -700,22 +1030,55 @@ def _resolve_specs(specs, table):
|
|
|
700
1030
|
"""
|
|
701
1031
|
resolved, unresolved = [], []
|
|
702
1032
|
for spec in specs:
|
|
703
|
-
|
|
1033
|
+
base, suffix = _split_transform(spec)
|
|
1034
|
+
# §1.2: raw @address specs bypass DWARF entirely.
|
|
1035
|
+
raw = _resolve_raw(base)
|
|
1036
|
+
if raw is not None:
|
|
1037
|
+
if raw["n"] > EXPAND_CAP:
|
|
1038
|
+
unresolved.append("%s (expands to %d > %d)" % (spec, raw["n"], EXPAND_CAP))
|
|
1039
|
+
elif not raw["elems"]:
|
|
1040
|
+
unresolved.append(spec)
|
|
1041
|
+
elif suffix is not None:
|
|
1042
|
+
if raw["n"] != 1:
|
|
1043
|
+
unresolved.append("%s (transform on expanding spec unsupported)" % spec)
|
|
1044
|
+
else:
|
|
1045
|
+
t = _parse_transform(suffix, raw["elems"][0]["size"])
|
|
1046
|
+
if t is None:
|
|
1047
|
+
unresolved.append(spec)
|
|
1048
|
+
else:
|
|
1049
|
+
e = dict(raw["elems"][0]); e["transform"] = t; e["name"] = spec
|
|
1050
|
+
resolved.append(e)
|
|
1051
|
+
else:
|
|
1052
|
+
resolved.extend(raw["elems"])
|
|
1053
|
+
continue
|
|
1054
|
+
elems, n = _expand_spec(base, table)
|
|
704
1055
|
if n > EXPAND_CAP:
|
|
705
1056
|
unresolved.append("%s (expands to %d > %d)" % (spec, n, EXPAND_CAP))
|
|
706
1057
|
continue
|
|
707
1058
|
if elems:
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1059
|
+
if suffix is not None and n != 1:
|
|
1060
|
+
unresolved.append("%s (transform on expanding spec unsupported)" % spec)
|
|
1061
|
+
continue
|
|
711
1062
|
for e in elems:
|
|
712
1063
|
r = _resolve_spec(e, table)
|
|
713
|
-
if r:
|
|
714
|
-
|
|
1064
|
+
if not r:
|
|
1065
|
+
continue
|
|
1066
|
+
if suffix is not None:
|
|
1067
|
+
t = _parse_transform(suffix, r["size"])
|
|
1068
|
+
if t is None:
|
|
1069
|
+
unresolved.append(spec); continue
|
|
1070
|
+
r = dict(r); r["transform"] = t; r["name"] = spec
|
|
1071
|
+
resolved.append(r)
|
|
715
1072
|
continue
|
|
716
1073
|
# Not expandable — try as a plain concrete scalar spec.
|
|
717
|
-
r = _resolve_spec(
|
|
1074
|
+
r = _resolve_spec(base, table)
|
|
718
1075
|
if r:
|
|
1076
|
+
if suffix is not None:
|
|
1077
|
+
t = _parse_transform(suffix, r["size"])
|
|
1078
|
+
if t is None:
|
|
1079
|
+
unresolved.append(spec)
|
|
1080
|
+
continue
|
|
1081
|
+
r = dict(r); r["transform"] = t; r["name"] = spec
|
|
719
1082
|
resolved.append(r)
|
|
720
1083
|
else:
|
|
721
1084
|
unresolved.append(spec)
|
|
@@ -742,7 +1105,8 @@ def _resolve_type_from(die, cu):
|
|
|
742
1105
|
return "uint", (bs.value if bs else 4)
|
|
743
1106
|
|
|
744
1107
|
def _coalesce(sigs):
|
|
745
|
-
"""sigs: list of {name,address,size,encoding}.
|
|
1108
|
+
"""sigs: list of {name,address,size,encoding,transform?}.
|
|
1109
|
+
Returns [(start,length,[(name,off,size,enc,transform)])]."""
|
|
746
1110
|
items = sorted(sigs, key=lambda s: s["address"])
|
|
747
1111
|
ranges = []
|
|
748
1112
|
for s in items:
|
|
@@ -750,9 +1114,10 @@ def _coalesce(sigs):
|
|
|
750
1114
|
if ranges and a <= ranges[-1][0] + ranges[-1][1] + 4: # merge if within 4 bytes of prev end
|
|
751
1115
|
start, length, members = ranges[-1]
|
|
752
1116
|
new_end = max(start + length, a + ln)
|
|
753
|
-
ranges[-1] = (start, new_end - start,
|
|
1117
|
+
ranges[-1] = (start, new_end - start,
|
|
1118
|
+
members + [(s["name"], a - start, ln, s["encoding"], s.get("transform"))])
|
|
754
1119
|
else:
|
|
755
|
-
ranges.append((a, ln, [(s["name"], 0, ln, s["encoding"])]))
|
|
1120
|
+
ranges.append((a, ln, [(s["name"], 0, ln, s["encoding"], s.get("transform"))]))
|
|
756
1121
|
return ranges
|
|
757
1122
|
|
|
758
1123
|
def _decode(buf, off, size, enc):
|
|
@@ -764,6 +1129,20 @@ def _decode(buf, off, size, enc):
|
|
|
764
1129
|
signed = enc in ("int", "char")
|
|
765
1130
|
return int.from_bytes(raw, "little", signed=signed)
|
|
766
1131
|
|
|
1132
|
+
def _apply_transform(value, size, transform):
|
|
1133
|
+
"""Apply a bit/range/mask transform to a decoded value. The value is
|
|
1134
|
+
reinterpreted UNSIGNED (masked to size*8 bits) so a signed encoding does not
|
|
1135
|
+
corrupt bit extraction."""
|
|
1136
|
+
u = value & ((1 << (size * 8)) - 1)
|
|
1137
|
+
kind = transform["kind"]
|
|
1138
|
+
if kind == "bit":
|
|
1139
|
+
return (u >> transform["n"]) & 1
|
|
1140
|
+
if kind == "range":
|
|
1141
|
+
lo, hi = transform["lo"], transform["hi"]
|
|
1142
|
+
return (u >> lo) & ((1 << (hi - lo + 1)) - 1)
|
|
1143
|
+
# mask
|
|
1144
|
+
return (u & transform["mask"]) >> transform["shift"]
|
|
1145
|
+
|
|
767
1146
|
def cmd_trace(args):
|
|
768
1147
|
names = [n for n in args.signals.split(",") if n]
|
|
769
1148
|
# §11.3 ELF / DWARF guard: a bad/missing ELF must surface as an error frame,
|
|
@@ -790,6 +1169,8 @@ def cmd_trace(args):
|
|
|
790
1169
|
sigset = {s["name"]: s for s in resolved}
|
|
791
1170
|
state = {"dirty": True, "ranges": _coalesce(list(sigset.values())),
|
|
792
1171
|
"unresolved": initial_unresolved, "signals": list(sigset.values())}
|
|
1172
|
+
pending_cmds = [] # list of dict msgs from stdin (write/call), drained in loop
|
|
1173
|
+
cmd_lock = threading.Lock()
|
|
793
1174
|
|
|
794
1175
|
def _signals_frame():
|
|
795
1176
|
"""Build the {"type":"signals",...} frame from the current poll set."""
|
|
@@ -866,6 +1247,12 @@ def cmd_trace(args):
|
|
|
866
1247
|
sigs = msg.get("signals")
|
|
867
1248
|
if isinstance(sigs, list):
|
|
868
1249
|
_apply_remove([s for s in sigs if isinstance(s, str)])
|
|
1250
|
+
elif cmd == "write" and isinstance(msg.get("writes"), list):
|
|
1251
|
+
with cmd_lock:
|
|
1252
|
+
pending_cmds.append(msg)
|
|
1253
|
+
elif cmd == "call":
|
|
1254
|
+
with cmd_lock:
|
|
1255
|
+
pending_cmds.append(msg)
|
|
869
1256
|
threading.Thread(target=stdin_reader, daemon=True).start()
|
|
870
1257
|
|
|
871
1258
|
log(f"connecting probe (serial={args.probe or 'auto'}, target={args.target}, "
|
|
@@ -887,6 +1274,7 @@ def cmd_trace(args):
|
|
|
887
1274
|
os._exit(3 if "no debug probe" in cerr else 1)
|
|
888
1275
|
|
|
889
1276
|
target = session.target
|
|
1277
|
+
ram_regions = _ram_regions_from(session)
|
|
890
1278
|
log("connected; polling (non-halting)")
|
|
891
1279
|
# §11.2 persistent-fault tracking: a single read fault = stop_suspected, but
|
|
892
1280
|
# faults lasting longer than --lost-timeout mean the probe/target is gone.
|
|
@@ -911,6 +1299,39 @@ def cmd_trace(args):
|
|
|
911
1299
|
state["ranges"] = _coalesce(list(state["signals"]))
|
|
912
1300
|
state["dirty"] = False
|
|
913
1301
|
print(json.dumps(_signals_frame()), flush=True)
|
|
1302
|
+
drained = []
|
|
1303
|
+
with cmd_lock:
|
|
1304
|
+
if pending_cmds:
|
|
1305
|
+
drained = pending_cmds[:]; pending_cmds.clear()
|
|
1306
|
+
for msg in drained:
|
|
1307
|
+
rid = msg.get("id")
|
|
1308
|
+
kind = "write_result" if msg.get("cmd") == "write" else "call_result"
|
|
1309
|
+
try:
|
|
1310
|
+
if msg.get("cmd") == "write":
|
|
1311
|
+
descs = [_parse_write_spec(s, table)
|
|
1312
|
+
for s in msg["writes"] if isinstance(s, str)]
|
|
1313
|
+
results = do_write(target, descs, ram_regions)
|
|
1314
|
+
print(json.dumps({"type": "write_result", "id": rid,
|
|
1315
|
+
"ok": all(r["ok"] for r in results) if results else False,
|
|
1316
|
+
"results": results}), flush=True)
|
|
1317
|
+
else: # call
|
|
1318
|
+
if not msg.get("confirm"):
|
|
1319
|
+
print(json.dumps({"type": "call_result", "id": rid,
|
|
1320
|
+
"ok": False, "error": "call requires confirm:true"}), flush=True)
|
|
1321
|
+
continue
|
|
1322
|
+
entry = _resolve_func(args.elf, msg.get("func", ""))
|
|
1323
|
+
if entry is None:
|
|
1324
|
+
print(json.dumps({"type": "call_result", "id": rid, "ok": False,
|
|
1325
|
+
"error": "unknown function: %s" % msg.get("func")}), flush=True)
|
|
1326
|
+
continue
|
|
1327
|
+
res = do_call(target, entry,
|
|
1328
|
+
[int(a) for a in msg.get("args", [])],
|
|
1329
|
+
msg.get("ret_type", "u32"), float(msg.get("timeout", 2.0)))
|
|
1330
|
+
res.update({"type": "call_result", "id": rid})
|
|
1331
|
+
print(json.dumps(res), flush=True)
|
|
1332
|
+
except Exception as e:
|
|
1333
|
+
print(json.dumps({"type": kind, "id": rid, "ok": False,
|
|
1334
|
+
"error": "command failed: %s" % e}), flush=True)
|
|
914
1335
|
ranges = state["ranges"]
|
|
915
1336
|
values, in_stop = {}, False
|
|
916
1337
|
for (start, length, members) in ranges:
|
|
@@ -919,8 +1340,11 @@ def cmd_trace(args):
|
|
|
919
1340
|
except Exception:
|
|
920
1341
|
in_stop = True
|
|
921
1342
|
break
|
|
922
|
-
for (name, off, size, enc) in members:
|
|
923
|
-
|
|
1343
|
+
for (name, off, size, enc, transform) in members:
|
|
1344
|
+
v = _decode(data, off, size, enc)
|
|
1345
|
+
if transform is not None:
|
|
1346
|
+
v = _apply_transform(v, size, transform)
|
|
1347
|
+
values[name] = v
|
|
924
1348
|
t = time.monotonic() - t0
|
|
925
1349
|
if in_stop:
|
|
926
1350
|
now = time.monotonic()
|
|
@@ -1076,6 +1500,27 @@ def main():
|
|
|
1076
1500
|
"read. Reports inaccessible on timeout/no-probe. Default 6.")
|
|
1077
1501
|
dp.set_defaults(func=cmd_device_state)
|
|
1078
1502
|
|
|
1503
|
+
wp = sub.add_parser("write")
|
|
1504
|
+
wp.add_argument("--elf", required=True)
|
|
1505
|
+
wp.add_argument("--writes", required=True, help="';'-separated target=value specs")
|
|
1506
|
+
wp.add_argument("--probe", default=None)
|
|
1507
|
+
wp.add_argument("--target", default="cortex_m")
|
|
1508
|
+
wp.add_argument("--connect-timeout", type=float, default=6.0)
|
|
1509
|
+
wp.set_defaults(func=cmd_write)
|
|
1510
|
+
|
|
1511
|
+
cp = sub.add_parser("call")
|
|
1512
|
+
cp.add_argument("--elf", required=True)
|
|
1513
|
+
cp.add_argument("--func", required=True, dest="func_name")
|
|
1514
|
+
cp.add_argument("--args", default=None, help="comma-separated ints (hex 0x.. or dec)")
|
|
1515
|
+
cp.add_argument("--confirm", action="store_true")
|
|
1516
|
+
cp.add_argument("--ret-type", dest="ret_type", default="u32",
|
|
1517
|
+
choices=list(_RAW_TYPE.keys()))
|
|
1518
|
+
cp.add_argument("--timeout", type=float, default=2.0)
|
|
1519
|
+
cp.add_argument("--probe", default=None)
|
|
1520
|
+
cp.add_argument("--target", default="cortex_m")
|
|
1521
|
+
cp.add_argument("--connect-timeout", type=float, default=6.0)
|
|
1522
|
+
cp.set_defaults(func=cmd_call)
|
|
1523
|
+
|
|
1079
1524
|
args = ap.parse_args()
|
|
1080
1525
|
args.func(args)
|
|
1081
1526
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* Config panel wiring (client-side only). Mirrors the side-panel controls into
|
|
2
|
+
the shared cfg object. toggleFull is exported for the keyboard shortcut. */
|
|
3
|
+
|
|
4
|
+
import { sigs, cfg } from "./state.js";
|
|
5
|
+
|
|
6
|
+
let cfgWindow, cfgFull, cfgMax, cfgYMode;
|
|
7
|
+
|
|
8
|
+
function applyFull(){
|
|
9
|
+
cfg.full=cfgFull.checked;
|
|
10
|
+
cfgWindow.disabled=cfg.full; // trailing window is meaningless in full-history
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toggleFull(){
|
|
14
|
+
if(!cfgFull)return;
|
|
15
|
+
cfgFull.checked=!cfgFull.checked;
|
|
16
|
+
applyFull();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Reflect a new visible window width (e.g. from wheel-zoom) into cfg + the input
|
|
20
|
+
so the "Trailing window" field never goes stale while zooming. */
|
|
21
|
+
export function setWindow(sec){
|
|
22
|
+
if(!(sec>0))return;
|
|
23
|
+
cfg.windowSec=sec;
|
|
24
|
+
if(cfgWindow) cfgWindow.value = sec<10 ? sec.toFixed(1) : String(Math.round(sec));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function initConfig(){
|
|
28
|
+
cfgWindow=document.getElementById("cfgWindow");
|
|
29
|
+
cfgFull=document.getElementById("cfgFull");
|
|
30
|
+
cfgMax=document.getElementById("cfgMax");
|
|
31
|
+
cfgYMode=document.getElementById("cfgYMode");
|
|
32
|
+
|
|
33
|
+
cfgWindow.onchange=()=>{ const v=parseFloat(cfgWindow.value); if(v>0) cfg.windowSec=v; };
|
|
34
|
+
cfgFull.onchange=applyFull;
|
|
35
|
+
cfgMax.onchange=()=>{
|
|
36
|
+
const v=parseInt(cfgMax.value,10);
|
|
37
|
+
if(v>=100){ cfg.maxSamples=v; for(const[,s]of sigs) while(s.data.length>v) s.data.shift(); }
|
|
38
|
+
};
|
|
39
|
+
cfgYMode.onchange=()=>{ cfg.yMode=cfgYMode.value; };
|
|
40
|
+
}
|