siligent 0.1.0__py3-none-any.whl

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.
siligent/image.py ADDED
@@ -0,0 +1,93 @@
1
+ """Read firmware images (ELF or raw binary) into loadable segments and symbols."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import struct
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+
10
+ from .errors import ImageError
11
+
12
+ PT_LOAD = 1
13
+ SHT_SYMTAB = 2
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Segment:
18
+ address: int
19
+ data: bytes
20
+
21
+ @property
22
+ def end(self) -> int:
23
+ return self.address + len(self.data)
24
+
25
+
26
+ @dataclass
27
+ class Image:
28
+ path: str
29
+ segments: list[Segment]
30
+ symbols: dict[str, tuple[int, int]] = field(default_factory=dict) # name -> (address, size)
31
+
32
+ @property
33
+ def sha256(self) -> str:
34
+ h = hashlib.sha256()
35
+ for s in sorted(self.segments, key=lambda s: s.address):
36
+ h.update(s.address.to_bytes(4, "little"))
37
+ h.update(s.data)
38
+ return h.hexdigest()
39
+
40
+ @property
41
+ def size(self) -> int:
42
+ return sum(len(s.data) for s in self.segments)
43
+
44
+ def word_at(self, address: int) -> int | None:
45
+ for s in self.segments:
46
+ if s.address <= address and address + 4 <= s.end:
47
+ off = address - s.address
48
+ return int.from_bytes(s.data[off : off + 4], "little")
49
+ return None
50
+
51
+
52
+ def load_image(path: str | Path, base_address: int | None = None) -> Image:
53
+ """Load an .elf, or a raw .bin placed at `base_address`."""
54
+ path = Path(path)
55
+ if not path.is_file():
56
+ raise ImageError(f"no such file: {path}")
57
+ raw = path.read_bytes()
58
+ if raw[:4] == b"\x7fELF":
59
+ return _load_elf(raw, str(path))
60
+ if base_address is None:
61
+ raise ImageError("a raw binary needs a base address (for example the start of flash)")
62
+ return Image(str(path), [Segment(base_address, raw)])
63
+
64
+
65
+ def _load_elf(raw: bytes, path: str) -> Image:
66
+ if raw[4] != 1 or raw[5] != 1:
67
+ raise ImageError("only 32-bit little-endian ELF files are supported")
68
+ (e_phoff, e_shoff) = struct.unpack_from("<II", raw, 0x1C)
69
+ (e_phentsize, e_phnum, e_shentsize, e_shnum) = struct.unpack_from("<HHHH", raw, 0x2A)
70
+
71
+ segments = []
72
+ for i in range(e_phnum):
73
+ p_type, p_offset, _vaddr, p_paddr, p_filesz = struct.unpack_from("<IIIII", raw, e_phoff + i * e_phentsize)
74
+ if p_type == PT_LOAD and p_filesz:
75
+ # The physical (load) address is where the bytes live in flash.
76
+ segments.append(Segment(p_paddr, raw[p_offset : p_offset + p_filesz]))
77
+
78
+ symbols: dict[str, tuple[int, int]] = {}
79
+ sections = [struct.unpack_from("<IIIIIIIIII", raw, e_shoff + i * e_shentsize) for i in range(e_shnum)]
80
+ for sh in sections:
81
+ if sh[1] != SHT_SYMTAB:
82
+ continue
83
+ sym_off, sym_size, link, entsize = sh[4], sh[5], sh[6], sh[9] or 16
84
+ str_off = sections[link][4]
85
+ for j in range(sym_size // entsize):
86
+ st_name, st_value, st_size = struct.unpack_from("<III", raw, sym_off + j * entsize)
87
+ if st_name:
88
+ end = raw.index(b"\0", str_off + st_name)
89
+ symbols[raw[str_off + st_name : end].decode(errors="replace")] = (st_value, st_size)
90
+
91
+ if not segments:
92
+ raise ImageError("the ELF file has no loadable segments")
93
+ return Image(path, segments, symbols)
siligent/log.py ADDED
@@ -0,0 +1,25 @@
1
+ """An append-only log of every action taken on the bench, refused or not."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from pathlib import Path
8
+
9
+
10
+ class ActionLog:
11
+ def __init__(self, path: str | Path) -> None:
12
+ self.path = Path(path).expanduser()
13
+ self.path.parent.mkdir(parents=True, exist_ok=True)
14
+
15
+ def record(self, action: str, outcome: str, **fields: object) -> dict:
16
+ entry = {"time": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "action": action, "outcome": outcome, **fields}
17
+ with self.path.open("a") as f:
18
+ f.write(json.dumps(entry, default=str) + "\n")
19
+ return entry
20
+
21
+ def tail(self, n: int = 20) -> list[dict]:
22
+ if not self.path.exists():
23
+ return []
24
+ lines = self.path.read_text().splitlines()[-n:]
25
+ return [json.loads(line) for line in lines if line.strip()]
siligent/mcp_server.py ADDED
@@ -0,0 +1,89 @@
1
+ """MCP server: exposes the bench to AI agents (Claude Code, Cursor, others) over stdio.
2
+
3
+ Refusals come back as normal results with "refused": true, the rule that fired
4
+ and the reason, so the agent can explain or change course instead of retrying.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from mcp.server.mcpserver import MCPServer
10
+
11
+ from . import api
12
+ from .bench import BenchConfig
13
+
14
+ INSTRUCTIONS = """\
15
+ Siligent controls a real hardware bench. Every write and flash passes a safety gate.
16
+ If a result has "refused": true, do not retry the same action: read "rule" and "reason",
17
+ then explain it to the user or choose a different approach. Reads are always allowed.
18
+ Start with detect_hardware, then board_rules to see what is allowed on this board.
19
+ Use check_image before flash or run_tests to catch problems without touching hardware.
20
+ """
21
+
22
+ server = MCPServer("siligent", instructions=INSTRUCTIONS)
23
+
24
+
25
+ def _cfg(virtual: bool = False) -> BenchConfig:
26
+ cfg = BenchConfig.load()
27
+ if virtual:
28
+ cfg.virtual = True
29
+ return cfg
30
+
31
+
32
+ @server.tool()
33
+ def detect_hardware() -> dict:
34
+ """List connected debug probes and identify the board and the safety rules that apply."""
35
+ return api.detect(_cfg())
36
+
37
+
38
+ @server.tool()
39
+ def board_rules(board: str = "stm32l476rg") -> dict:
40
+ """Show what is allowed and forbidden on a board: memory regions, protected pins, flash preconditions."""
41
+ return api.rules(board)
42
+
43
+
44
+ @server.tool()
45
+ def check_image(path: str) -> dict:
46
+ """Check a firmware file (.elf) against the board's rules without touching hardware."""
47
+ return api.check_image(path, config=_cfg())
48
+
49
+
50
+ @server.tool()
51
+ def flash(path: str, virtual: bool = False) -> dict:
52
+ """Flash a firmware file (.elf) to the board. The flash pages are backed up first.
53
+ virtual=true uses a Renode virtual board instead of real hardware."""
54
+ return api.flash(path, config=_cfg(virtual))
55
+
56
+
57
+ @server.tool()
58
+ def run_tests(path: str, timeout_s: float = 10.0, virtual: bool = False) -> dict:
59
+ """Flash test firmware built with the Siligent harness, run it, and return
60
+ pass/fail and CPU cycles per test, plus energy if a power meter is configured.
61
+ virtual=true runs it on a Renode virtual board (no hardware needed; cycle counts
62
+ are simulated, and there is no energy)."""
63
+ return api.run_tests(path, timeout_s=timeout_s, config=_cfg(virtual))
64
+
65
+
66
+ @server.tool()
67
+ def read_memory(address: int, length: int, virtual: bool = False) -> dict:
68
+ """Read up to 4096 bytes of chip memory. Returns hex."""
69
+ return api.read_memory(address, length, config=_cfg(virtual))
70
+
71
+
72
+ @server.tool()
73
+ def write_memory(address: int, data_hex: str, virtual: bool = False) -> dict:
74
+ """Write bytes to RAM or a peripheral register. Flash, option bytes and protected pins are refused."""
75
+ return api.write_memory(address, data_hex, config=_cfg(virtual))
76
+
77
+
78
+ @server.tool()
79
+ def action_log(n: int = 20) -> dict:
80
+ """The most recent bench actions, including refused ones."""
81
+ return api.action_log(n, config=_cfg())
82
+
83
+
84
+ def main() -> None:
85
+ server.run("stdio")
86
+
87
+
88
+ if __name__ == "__main__":
89
+ main()
@@ -0,0 +1,5 @@
1
+ """Power meters: anything that can sample current and voltage over a time window."""
2
+
3
+ from .base import MeterReading, PowerMeter, integrate
4
+
5
+ __all__ = ["MeterReading", "PowerMeter", "integrate"]
@@ -0,0 +1,58 @@
1
+ """The power-meter interface and the energy maths shared by every meter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from typing import Protocol
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class MeterReading:
11
+ meter: str
12
+ duration_s: float
13
+ samples: int
14
+ energy_uj: float
15
+ avg_current_a: float
16
+ min_current_a: float
17
+ max_current_a: float
18
+ avg_voltage_v: float
19
+ resolution_a: float # smallest current step the meter can see
20
+
21
+ def to_dict(self) -> dict:
22
+ d = asdict(self)
23
+ # Say plainly when the meter is too coarse for what it measured.
24
+ d["below_resolution"] = self.avg_current_a < 10 * self.resolution_a
25
+ return d
26
+
27
+
28
+ class PowerMeter(Protocol):
29
+ name: str
30
+ resolution_a: float
31
+
32
+ def start(self) -> None:
33
+ """Begin sampling in the background."""
34
+
35
+ def stop(self) -> MeterReading:
36
+ """Stop sampling and return the totals for the window."""
37
+
38
+
39
+ def integrate(meter: str, resolution_a: float, samples: list[tuple[float, float, float]]) -> MeterReading:
40
+ """Totals from (time_s, current_a, voltage_v) samples, by the trapezoid rule."""
41
+ if not samples:
42
+ return MeterReading(meter, 0.0, 0, 0.0, 0.0, 0.0, 0.0, 0.0, resolution_a)
43
+ energy_j = 0.0
44
+ for (t0, i0, v0), (t1, i1, v1) in zip(samples, samples[1:]):
45
+ energy_j += (i0 * v0 + i1 * v1) / 2 * (t1 - t0)
46
+ duration = samples[-1][0] - samples[0][0]
47
+ currents = [s[1] for s in samples]
48
+ return MeterReading(
49
+ meter=meter,
50
+ duration_s=duration,
51
+ samples=len(samples),
52
+ energy_uj=energy_j * 1e6,
53
+ avg_current_a=sum(currents) / len(currents),
54
+ min_current_a=min(currents),
55
+ max_current_a=max(currents),
56
+ avg_voltage_v=sum(s[2] for s in samples) / len(samples),
57
+ resolution_a=resolution_a,
58
+ )
@@ -0,0 +1,88 @@
1
+ """TI INA219 current/voltage sensor over I2C.
2
+
3
+ Good for active current. With a 0.1 ohm shunt it reads in 100 uA steps, so it
4
+ cannot see microcontroller sleep currents (around 1 uA). Use a uA-capable
5
+ meter for those.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import threading
11
+ import time
12
+ from typing import Protocol
13
+
14
+ from .base import MeterReading, integrate
15
+
16
+ REG_CONFIG = 0x00
17
+ REG_SHUNT = 0x01
18
+ REG_BUS = 0x02
19
+ SHUNT_LSB_V = 10e-6
20
+ BUS_LSB_V = 4e-3
21
+ # 32 V range, PGA /8 (+-320 mV), 12-bit, shunt and bus continuous: the reset default.
22
+ CONFIG_DEFAULT = 0x399F
23
+
24
+
25
+ class I2CBus(Protocol):
26
+ def read_word(self, address: int, register: int) -> int:
27
+ """Read a 16-bit register, most significant byte first (as the INA219 sends it)."""
28
+
29
+ def write_word(self, address: int, register: int, value: int) -> None:
30
+ """Write a 16-bit register, most significant byte first."""
31
+
32
+
33
+ class LinuxI2CBus:
34
+ """A Linux /dev/i2c-N bus via smbus2 (pip install siligent[ina219])."""
35
+
36
+ def __init__(self, bus: int) -> None:
37
+ from smbus2 import SMBus # imported here so the dependency stays optional
38
+
39
+ self._bus = SMBus(bus)
40
+
41
+ def read_word(self, address: int, register: int) -> int:
42
+ w = self._bus.read_word_data(address, register)
43
+ return ((w & 0xFF) << 8) | (w >> 8) # SMBus words arrive low byte first
44
+
45
+ def write_word(self, address: int, register: int, value: int) -> None:
46
+ self._bus.write_word_data(address, register, ((value & 0xFF) << 8) | (value >> 8))
47
+
48
+
49
+ class INA219:
50
+ name = "INA219"
51
+
52
+ def __init__(self, bus: I2CBus, address: int = 0x40, shunt_ohms: float = 0.1, sample_hz: float = 500) -> None:
53
+ self.bus = bus
54
+ self.address = address
55
+ self.shunt_ohms = shunt_ohms
56
+ self.period = 1.0 / sample_hz
57
+ self.resolution_a = SHUNT_LSB_V / shunt_ohms
58
+ self._samples: list[tuple[float, float, float]] = []
59
+ self._stop = threading.Event()
60
+ self._thread: threading.Thread | None = None
61
+ self.bus.write_word(self.address, REG_CONFIG, CONFIG_DEFAULT)
62
+
63
+ def read_once(self) -> tuple[float, float]:
64
+ """One (current_a, bus_voltage_v) reading."""
65
+ raw = self.bus.read_word(self.address, REG_SHUNT)
66
+ if raw & 0x8000:
67
+ raw -= 0x10000
68
+ bus = self.bus.read_word(self.address, REG_BUS)
69
+ return raw * SHUNT_LSB_V / self.shunt_ohms, (bus >> 3) * BUS_LSB_V
70
+
71
+ def _run(self) -> None:
72
+ t0 = time.monotonic()
73
+ while not self._stop.is_set():
74
+ i, v = self.read_once()
75
+ self._samples.append((time.monotonic() - t0, i, v))
76
+ time.sleep(self.period)
77
+
78
+ def start(self) -> None:
79
+ self._samples = []
80
+ self._stop.clear()
81
+ self._thread = threading.Thread(target=self._run, daemon=True)
82
+ self._thread.start()
83
+
84
+ def stop(self) -> MeterReading:
85
+ self._stop.set()
86
+ if self._thread:
87
+ self._thread.join()
88
+ return integrate(self.name, self.resolution_a, self._samples)
@@ -0,0 +1,6 @@
1
+ """Debug probes. Every probe is used through GuardedProbe, which applies the safety gate."""
2
+
3
+ from .base import ProbeBackend
4
+ from .guarded import GuardedProbe
5
+
6
+ __all__ = ["ProbeBackend", "GuardedProbe"]
@@ -0,0 +1,35 @@
1
+ """The interface a probe plugin implements. Plugins never apply safety rules themselves."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+ from ..image import Segment
8
+
9
+
10
+ class ProbeBackend(Protocol):
11
+ unique_id: str
12
+ description: str
13
+
14
+ def open(self) -> None: ...
15
+
16
+ def close(self) -> None: ...
17
+
18
+ def read32(self, address: int) -> int: ...
19
+
20
+ def read_memory(self, address: int, length: int) -> bytes: ...
21
+
22
+ def write_memory(self, address: int, data: bytes) -> None: ...
23
+
24
+ def program(self, segments: list[Segment]) -> None:
25
+ """Halt the core with a reset, then erase the sectors these segments cover and program
26
+ them. Never a whole-chip erase."""
27
+
28
+ def reset(self, halt: bool = False) -> None: ...
29
+
30
+ def halt(self) -> None: ...
31
+
32
+ def resume(self) -> None: ...
33
+
34
+ def state(self) -> str:
35
+ """'running', 'halted', 'sleeping', 'lockup' or 'reset'."""
@@ -0,0 +1,143 @@
1
+ """GuardedProbe: the only way Siligent touches a probe.
2
+
3
+ Every write and flash passes the safety gate here, and every action (allowed
4
+ or refused) is written to the action log. The CLI, the Python API and the
5
+ MCP server all go through this class, so none of them can get around the rules.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from pathlib import Path
12
+
13
+ from ..errors import SafetyRefusal
14
+ from ..image import Image, Segment
15
+ from ..log import ActionLog
16
+ from ..rules import BoardRules
17
+ from ..safety import SafetyGate
18
+ from .base import ProbeBackend
19
+
20
+ FLASH_PAGE = 2048
21
+
22
+
23
+ class GuardedProbe:
24
+ def __init__(
25
+ self,
26
+ backend: ProbeBackend,
27
+ rules: BoardRules,
28
+ gate: SafetyGate,
29
+ log: ActionLog,
30
+ snapshot_dir: str | Path,
31
+ ) -> None:
32
+ self.backend = backend
33
+ self.rules = rules
34
+ self.gate = gate
35
+ self.log = log
36
+ self.snapshot_dir = Path(snapshot_dir).expanduser()
37
+
38
+ # Reads are allowed anywhere. Some peripheral registers clear flags when read,
39
+ # so reads are still logged.
40
+ def read_memory(self, address: int, length: int) -> bytes:
41
+ data, _ = self.read_memory_checked(address, length)
42
+ return data
43
+
44
+ def read_memory_checked(self, address: int, length: int) -> tuple[bytes, bool]:
45
+ """Read memory; returns (data, halted_to_read).
46
+
47
+ While the core sleeps, the ST-LINK returns zeros for RAM and flash with no
48
+ error. So a sleeping core is halted for the read, then resumed.
49
+ """
50
+ halted = self.backend.state() == "sleeping"
51
+ if halted:
52
+ self.backend.halt()
53
+ try:
54
+ data = self.backend.read_memory(address, length)
55
+ finally:
56
+ if halted:
57
+ self.backend.resume()
58
+ self.log.record("read_memory", "ok", address=hex(address), length=length, halted_to_read=halted)
59
+ return data, halted
60
+
61
+ def read32(self, address: int) -> int:
62
+ return self.backend.read32(address)
63
+
64
+ def device_id(self) -> int:
65
+ return self.backend.read32(self.rules.idcode_address) & 0xFFF
66
+
67
+ def write_memory(self, address: int, data: bytes) -> None:
68
+ try:
69
+ self.gate.check_host_write(address, data)
70
+ except SafetyRefusal as e:
71
+ self.log.record("write_memory", "refused", rule=e.rule, address=hex(address), length=len(data))
72
+ raise
73
+ self.backend.write_memory(address, data)
74
+ self.log.record("write_memory", "ok", address=hex(address), length=len(data))
75
+
76
+ def flash(self, image: Image) -> dict:
77
+ """Check, snapshot, then program an image. Returns what was done."""
78
+ halted = False
79
+ try:
80
+ self.gate.check_bench_for_flash()
81
+ self.gate.check_image(image)
82
+ # Halt first: while the core sleeps, register reads come back as zeros.
83
+ self.backend.halt()
84
+ halted = True
85
+ dev_id = self.device_id()
86
+ if dev_id != self.rules.dev_id:
87
+ raise SafetyRefusal(
88
+ "chip.mismatch",
89
+ f"The connected chip (device ID {hex(dev_id)}) is not a {self.rules.chip} "
90
+ f"({hex(self.rules.dev_id)}); these rules do not apply to it.",
91
+ )
92
+ self.gate.check_read_protection(self.backend.read32(self.rules.optr_address))
93
+ except SafetyRefusal as e:
94
+ if halted:
95
+ self.backend.resume() # a refusal leaves the board as it was
96
+ self.log.record("flash", "refused", rule=e.rule, image=image.path, sha256=image.sha256)
97
+ raise
98
+
99
+ # A virtual board starts fresh each time, so there is nothing to back up.
100
+ snapshot = self._snapshot(image.segments) if getattr(self.backend, "persistent", True) else None
101
+ self.backend.program(image.segments)
102
+ result = {
103
+ "ok": True,
104
+ "image": image.path,
105
+ "sha256": image.sha256,
106
+ "bytes": image.size,
107
+ "snapshot": str(snapshot) if snapshot else None,
108
+ }
109
+ self.log.record("flash", "ok", **result)
110
+ return result
111
+
112
+ def reset(self, halt: bool = False) -> None:
113
+ self.backend.reset(halt=halt)
114
+ self.log.record("reset", "ok", halt=halt)
115
+
116
+ def resume(self) -> None:
117
+ self.backend.resume()
118
+
119
+ def halt(self) -> None:
120
+ self.backend.halt()
121
+
122
+ def _snapshot(self, segments: list[Segment]) -> Path:
123
+ """Save the flash pages about to be overwritten, so they can be put back.
124
+
125
+ The first flash on a device also saves the whole flash as a baseline.
126
+ """
127
+ uid = self.backend.unique_id or "unknown"
128
+ folder = self.snapshot_dir / uid
129
+ folder.mkdir(parents=True, exist_ok=True)
130
+ baseline = folder / "baseline-full-flash.bin"
131
+ if not baseline.exists():
132
+ baseline.write_bytes(self.backend.read_memory(self.rules.flash.start, self.rules.flash.size))
133
+ self.log.record("snapshot", "ok", kind="baseline", path=str(baseline))
134
+
135
+ stamp = time.strftime("%Y%m%d-%H%M%S")
136
+ path = folder / f"pages-{stamp}.bin"
137
+ with path.open("wb") as f:
138
+ for seg in segments:
139
+ start = seg.address - (seg.address % FLASH_PAGE)
140
+ end = -(-seg.end // FLASH_PAGE) * FLASH_PAGE
141
+ f.write(start.to_bytes(4, "little") + (end - start).to_bytes(4, "little"))
142
+ f.write(self.backend.read_memory(start, end - start))
143
+ return path
@@ -0,0 +1,91 @@
1
+ """ST-LINK (and other CMSIS-DAP style probes) through pyOCD."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from ..errors import HardwareError
8
+ from ..image import Segment
9
+
10
+
11
+ class PyOCDBackend:
12
+ def __init__(self, target: str, unique_id: str | None = None, frequency_hz: int = 4_000_000) -> None:
13
+ self.target_name = target
14
+ self.unique_id = unique_id or ""
15
+ self.description = ""
16
+ self.frequency_hz = frequency_hz
17
+ self._session = None
18
+ self._target = None
19
+
20
+ def open(self) -> None:
21
+ from pyocd.core.helpers import ConnectHelper
22
+
23
+ logging.getLogger("pyocd").setLevel(logging.WARNING)
24
+ session = ConnectHelper.session_with_chosen_probe(
25
+ blocking=False,
26
+ unique_id=self.unique_id or None,
27
+ options={
28
+ "target_override": self.target_name,
29
+ # Attach without halting, so reading never disturbs running firmware.
30
+ "connect_mode": "attach",
31
+ "frequency": self.frequency_hz,
32
+ "hide_programming_progress": True,
33
+ },
34
+ )
35
+ if session is None:
36
+ raise HardwareError("no debug probe found (is the board plugged in?)")
37
+ try:
38
+ session.open()
39
+ except Exception as e:
40
+ raise HardwareError(f"could not connect to the chip through the probe: {e}") from e
41
+ self._session = session
42
+ self._target = session.board.target
43
+ self.unique_id = session.probe.unique_id
44
+ self.description = session.probe.description
45
+
46
+ def close(self) -> None:
47
+ if self._session is not None:
48
+ self._session.close()
49
+ self._session = None
50
+ self._target = None
51
+
52
+ def _t(self):
53
+ if self._target is None:
54
+ raise HardwareError("probe is not open")
55
+ return self._target
56
+
57
+ def read32(self, address: int) -> int:
58
+ return self._t().read32(address)
59
+
60
+ def read_memory(self, address: int, length: int) -> bytes:
61
+ return bytes(self._t().read_memory_block8(address, length))
62
+
63
+ def write_memory(self, address: int, data: bytes) -> None:
64
+ self._t().write_memory_block8(address, list(data))
65
+
66
+ def program(self, segments: list[Segment]) -> None:
67
+ from pyocd.flash.loader import MemoryLoader
68
+
69
+ # Stop whatever firmware is running first. Erasing flash under running code
70
+ # locks the core up and makes the erase time out (seen on the real L476).
71
+ self._t().reset_and_halt()
72
+ # Sector erase only: a chip erase would wipe more than the image covers.
73
+ loader = MemoryLoader(self._session, chip_erase="sector", smart_flash=True, keep_unwritten=True)
74
+ for seg in segments:
75
+ loader.add_data(seg.address, seg.data)
76
+ loader.commit()
77
+
78
+ def reset(self, halt: bool = False) -> None:
79
+ if halt:
80
+ self._t().reset_and_halt()
81
+ else:
82
+ self._t().reset()
83
+
84
+ def halt(self) -> None:
85
+ self._t().halt()
86
+
87
+ def resume(self) -> None:
88
+ self._t().resume()
89
+
90
+ def state(self) -> str:
91
+ return self._t().get_state().name.lower()