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.
Files changed (42) hide show
  1. package/dist/config.d.ts +7 -0
  2. package/dist/config.js +15 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.js +125 -25
  5. package/dist/index.js.map +1 -1
  6. package/dist/tools/stm-build.d.ts +23 -0
  7. package/dist/tools/stm-build.js +78 -0
  8. package/dist/tools/stm-build.js.map +1 -0
  9. package/dist/tools/stm-flash.d.ts +36 -0
  10. package/dist/tools/stm-flash.js +112 -0
  11. package/dist/tools/stm-flash.js.map +1 -0
  12. package/dist/tools/trace-session.d.ts +12 -0
  13. package/dist/tools/trace-session.js +69 -0
  14. package/dist/tools/trace-session.js.map +1 -1
  15. package/dist/tools/trace-webui.d.ts +14 -7
  16. package/dist/tools/trace-webui.js +77 -33
  17. package/dist/tools/trace-webui.js.map +1 -1
  18. package/dist/tools/trace-write.d.ts +9 -0
  19. package/dist/tools/trace-write.js +32 -0
  20. package/dist/tools/trace-write.js.map +1 -0
  21. package/dist/utils/userConfig.d.ts +9 -0
  22. package/dist/utils/userConfig.js.map +1 -1
  23. package/package.json +2 -1
  24. package/skills/swd-tracer/SKILL.md +48 -0
  25. package/tracer/PROTOCOL.md +69 -0
  26. package/tracer/swd_tracer.py +449 -12
  27. package/tracer/ui/config.js +40 -0
  28. package/tracer/ui/connection.js +170 -0
  29. package/tracer/ui/index.html +50 -798
  30. package/tracer/ui/main.js +30 -0
  31. package/tracer/ui/plot.js +337 -0
  32. package/tracer/ui/signals.js +39 -0
  33. package/tracer/ui/state.js +57 -0
  34. package/tracer/ui/stats.js +52 -0
  35. package/tracer/ui/style.css +97 -0
  36. package/tracer/ui/symbols.js +80 -0
  37. package/tracer/ui/toolbar.js +95 -0
  38. package/tracer/ui/util.js +62 -0
  39. package/tracer/ui/watchlist.js +179 -0
  40. package/vscode-extension/README.md +32 -0
  41. package/vscode-extension/extension.js +56 -0
  42. package/vscode-extension/package.json +14 -0
@@ -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
- elems, n = _expand_spec(spec, table)
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
- # Expanded (possibly to a single concrete element). Resolve each;
717
- # any element that fails to resolve is silently dropped (the spec as
718
- # a whole produced concrete names, so it isn't "unresolved").
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
- resolved.append(r)
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(spec, table)
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}. Returns [(start,length,[(name,off,size,enc)])]."""
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, members + [(s["name"], a - start, ln, s["encoding"])])
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
- values[name] = _decode(data, off, size, enc)
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
+ }