crosspad-mcp-server 9.1.1 → 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/dist/config.d.ts +7 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +125 -25
- 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 +449 -12
- 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
|
@@ -695,6 +695,328 @@ def _expand_spec(spec, table):
|
|
|
695
695
|
specs = [_spec_name(base, p) for p in parts_lists]
|
|
696
696
|
return specs, len(specs)
|
|
697
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
|
+
|
|
698
1020
|
def _resolve_specs(specs, table):
|
|
699
1021
|
"""Resolve a list of specs, applying §1.1 array/vector/matrix expansion.
|
|
700
1022
|
|
|
@@ -708,22 +1030,55 @@ def _resolve_specs(specs, table):
|
|
|
708
1030
|
"""
|
|
709
1031
|
resolved, unresolved = [], []
|
|
710
1032
|
for spec in specs:
|
|
711
|
-
|
|
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)
|
|
712
1055
|
if n > EXPAND_CAP:
|
|
713
1056
|
unresolved.append("%s (expands to %d > %d)" % (spec, n, EXPAND_CAP))
|
|
714
1057
|
continue
|
|
715
1058
|
if elems:
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1059
|
+
if suffix is not None and n != 1:
|
|
1060
|
+
unresolved.append("%s (transform on expanding spec unsupported)" % spec)
|
|
1061
|
+
continue
|
|
719
1062
|
for e in elems:
|
|
720
1063
|
r = _resolve_spec(e, table)
|
|
721
|
-
if r:
|
|
722
|
-
|
|
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)
|
|
723
1072
|
continue
|
|
724
1073
|
# Not expandable — try as a plain concrete scalar spec.
|
|
725
|
-
r = _resolve_spec(
|
|
1074
|
+
r = _resolve_spec(base, table)
|
|
726
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
|
|
727
1082
|
resolved.append(r)
|
|
728
1083
|
else:
|
|
729
1084
|
unresolved.append(spec)
|
|
@@ -750,7 +1105,8 @@ def _resolve_type_from(die, cu):
|
|
|
750
1105
|
return "uint", (bs.value if bs else 4)
|
|
751
1106
|
|
|
752
1107
|
def _coalesce(sigs):
|
|
753
|
-
"""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)])]."""
|
|
754
1110
|
items = sorted(sigs, key=lambda s: s["address"])
|
|
755
1111
|
ranges = []
|
|
756
1112
|
for s in items:
|
|
@@ -758,9 +1114,10 @@ def _coalesce(sigs):
|
|
|
758
1114
|
if ranges and a <= ranges[-1][0] + ranges[-1][1] + 4: # merge if within 4 bytes of prev end
|
|
759
1115
|
start, length, members = ranges[-1]
|
|
760
1116
|
new_end = max(start + length, a + ln)
|
|
761
|
-
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"))])
|
|
762
1119
|
else:
|
|
763
|
-
ranges.append((a, ln, [(s["name"], 0, ln, s["encoding"])]))
|
|
1120
|
+
ranges.append((a, ln, [(s["name"], 0, ln, s["encoding"], s.get("transform"))]))
|
|
764
1121
|
return ranges
|
|
765
1122
|
|
|
766
1123
|
def _decode(buf, off, size, enc):
|
|
@@ -772,6 +1129,20 @@ def _decode(buf, off, size, enc):
|
|
|
772
1129
|
signed = enc in ("int", "char")
|
|
773
1130
|
return int.from_bytes(raw, "little", signed=signed)
|
|
774
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
|
+
|
|
775
1146
|
def cmd_trace(args):
|
|
776
1147
|
names = [n for n in args.signals.split(",") if n]
|
|
777
1148
|
# §11.3 ELF / DWARF guard: a bad/missing ELF must surface as an error frame,
|
|
@@ -798,6 +1169,8 @@ def cmd_trace(args):
|
|
|
798
1169
|
sigset = {s["name"]: s for s in resolved}
|
|
799
1170
|
state = {"dirty": True, "ranges": _coalesce(list(sigset.values())),
|
|
800
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()
|
|
801
1174
|
|
|
802
1175
|
def _signals_frame():
|
|
803
1176
|
"""Build the {"type":"signals",...} frame from the current poll set."""
|
|
@@ -874,6 +1247,12 @@ def cmd_trace(args):
|
|
|
874
1247
|
sigs = msg.get("signals")
|
|
875
1248
|
if isinstance(sigs, list):
|
|
876
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)
|
|
877
1256
|
threading.Thread(target=stdin_reader, daemon=True).start()
|
|
878
1257
|
|
|
879
1258
|
log(f"connecting probe (serial={args.probe or 'auto'}, target={args.target}, "
|
|
@@ -895,6 +1274,7 @@ def cmd_trace(args):
|
|
|
895
1274
|
os._exit(3 if "no debug probe" in cerr else 1)
|
|
896
1275
|
|
|
897
1276
|
target = session.target
|
|
1277
|
+
ram_regions = _ram_regions_from(session)
|
|
898
1278
|
log("connected; polling (non-halting)")
|
|
899
1279
|
# §11.2 persistent-fault tracking: a single read fault = stop_suspected, but
|
|
900
1280
|
# faults lasting longer than --lost-timeout mean the probe/target is gone.
|
|
@@ -919,6 +1299,39 @@ def cmd_trace(args):
|
|
|
919
1299
|
state["ranges"] = _coalesce(list(state["signals"]))
|
|
920
1300
|
state["dirty"] = False
|
|
921
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)
|
|
922
1335
|
ranges = state["ranges"]
|
|
923
1336
|
values, in_stop = {}, False
|
|
924
1337
|
for (start, length, members) in ranges:
|
|
@@ -927,8 +1340,11 @@ def cmd_trace(args):
|
|
|
927
1340
|
except Exception:
|
|
928
1341
|
in_stop = True
|
|
929
1342
|
break
|
|
930
|
-
for (name, off, size, enc) in members:
|
|
931
|
-
|
|
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
|
|
932
1348
|
t = time.monotonic() - t0
|
|
933
1349
|
if in_stop:
|
|
934
1350
|
now = time.monotonic()
|
|
@@ -1084,6 +1500,27 @@ def main():
|
|
|
1084
1500
|
"read. Reports inaccessible on timeout/no-probe. Default 6.")
|
|
1085
1501
|
dp.set_defaults(func=cmd_device_state)
|
|
1086
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
|
+
|
|
1087
1524
|
args = ap.parse_args()
|
|
1088
1525
|
args.func(args)
|
|
1089
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
|
+
}
|