simantic 0.2.0__tar.gz → 0.3.0__tar.gz

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 (47) hide show
  1. simantic-0.3.0/PKG-INFO +165 -0
  2. simantic-0.3.0/README.md +143 -0
  3. {simantic-0.2.0 → simantic-0.3.0}/pyproject.toml +1 -1
  4. simantic-0.3.0/src/simantic/__init__.py +56 -0
  5. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/_cli.py +10 -13
  6. simantic-0.3.0/src/simantic/_elf.py +39 -0
  7. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/_locate.py +0 -8
  8. simantic-0.3.0/src/simantic/_replx.py +108 -0
  9. simantic-0.3.0/src/simantic/_rust.py +128 -0
  10. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/engine.py +80 -0
  11. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/install.py +81 -10
  12. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/pytest_plugin.py +128 -72
  13. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/session.py +180 -104
  14. {simantic-0.2.0 → simantic-0.3.0}/tests/test_install.py +70 -15
  15. simantic-0.3.0/tests/test_pytest_surface.py +60 -0
  16. simantic-0.3.0/tests/test_rust_backend.py +218 -0
  17. {simantic-0.2.0 → simantic-0.3.0}/tests/test_spool.py +2 -2
  18. simantic-0.2.0/PKG-INFO +0 -219
  19. simantic-0.2.0/README.md +0 -197
  20. simantic-0.2.0/src/simantic/__init__.py +0 -85
  21. simantic-0.2.0/src/simantic/agent.py +0 -175
  22. simantic-0.2.0/src/simantic/analog.py +0 -94
  23. simantic-0.2.0/src/simantic/pyrite.py +0 -72
  24. simantic-0.2.0/src/simantic/report.py +0 -194
  25. simantic-0.2.0/tests/fixtures/divider/divider.sim.toml +0 -29
  26. simantic-0.2.0/tests/test_agent.py +0 -108
  27. simantic-0.2.0/tests/test_plan.py +0 -52
  28. simantic-0.2.0/tests/test_pyrite.py +0 -81
  29. simantic-0.2.0/tests/test_report.py +0 -164
  30. {simantic-0.2.0 → simantic-0.3.0}/.github/workflows/ci.yml +0 -0
  31. {simantic-0.2.0 → simantic-0.3.0}/.gitignore +0 -0
  32. {simantic-0.2.0 → simantic-0.3.0}/LICENSE +0 -0
  33. {simantic-0.2.0 → simantic-0.3.0}/PUBLISHING.md +0 -0
  34. {simantic-0.2.0 → simantic-0.3.0}/docs/session-api.md +0 -0
  35. {simantic-0.2.0 → simantic-0.3.0}/examples/parallel_sweep.py +0 -0
  36. {simantic-0.2.0 → simantic-0.3.0}/examples/step_and_peek.py +0 -0
  37. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/__main__.py +0 -0
  38. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/auth.py +0 -0
  39. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/fixtures.py +0 -0
  40. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/mcu.py +0 -0
  41. {simantic-0.2.0 → simantic-0.3.0}/src/simantic/telemetry.py +0 -0
  42. {simantic-0.2.0 → simantic-0.3.0}/tests/conftest.py +0 -0
  43. {simantic-0.2.0 → simantic-0.3.0}/tests/test_auth.py +0 -0
  44. {simantic-0.2.0 → simantic-0.3.0}/tests/test_fixtures.py +0 -0
  45. {simantic-0.2.0 → simantic-0.3.0}/tests/test_packaging.py +0 -0
  46. {simantic-0.2.0 → simantic-0.3.0}/tests/test_session.py +0 -0
  47. {simantic-0.2.0 → simantic-0.3.0}/tests/test_telemetry.py +0 -0
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.5
2
+ Name: simantic
3
+ Version: 0.3.0
4
+ Summary: Python SDK and pytest plugin for the Simantic circuit and firmware simulators
5
+ Project-URL: Homepage, https://simantic.dev
6
+ Project-URL: Source, https://github.com/simantic-dev/simantic-py
7
+ Project-URL: Issues, https://github.com/simantic-dev/simantic-py/issues
8
+ Author: Simantic
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: circuit,eda,firmware,kicad,pytest,simulation,spice
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: Pytest
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: pythonnet>=3.0.5
20
+ Requires-Dist: pyyaml>=6
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center">
24
+ <img src="https://simantic.dev/simantic_logo_4_full_transparent.png" alt="Simantic" width="340">
25
+ </p>
26
+
27
+ <h3 align="center">Test your firmware without a board.</h3>
28
+
29
+ <p align="center">
30
+ <a href="https://pypi.org/project/simantic/"><img src="https://img.shields.io/pypi/v/simantic.svg" alt="PyPI"></a>
31
+ <img src="https://img.shields.io/pypi/pyversions/simantic.svg" alt="Python versions">
32
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT licence">
33
+ </p>
34
+
35
+ Nothing to plug in, nothing to flash. Simantic boots your real ELF on a
36
+ simulated microcontroller and hands you the whole machine from Python. Watch it
37
+ print, press a button, read a variable straight out of RAM.
38
+
39
+ ```bash
40
+ pip install simantic
41
+ ```
42
+
43
+ ```python
44
+ from simantic import Sim
45
+
46
+ with Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") as sim:
47
+ sim.expect("ready")
48
+ sim.inject_gpio("gpioc", 13, True) # press the user button
49
+ sim.expect("button pressed")
50
+ assert sim.read_u32("press_count") == 1
51
+ ```
52
+
53
+ That is a whole test. No probe, no breakpoint, no waiting on hardware.
54
+
55
+ Three things you get that a bench cannot give you:
56
+
57
+ * **See inside.** Read any variable, register, or RTOS thread while the
58
+ firmware runs, without halting it.
59
+ * **Poke it.** Press buttons, send CAN frames, feed the radio, all from your
60
+ script.
61
+ * **Repeat exactly.** Time moves only when you ask, so a run comes out the same
62
+ every time, on your laptop and in CI.
63
+
64
+ The simulator lives inside your Python process, so there is no server to start
65
+ and no port to talk to.
66
+
67
+ > **Alpha, version 0.3.x.** We are still moving things around, so the API can
68
+ > change without a deprecation period. Pin an exact version
69
+ > (`simantic==0.3.0`) if you depend on it, and please hold off on production
70
+ > pipelines for now. Tell us what breaks.
71
+
72
+ ## Setup
73
+
74
+ `pip install` is the whole setup. The first `Sim(...)` downloads the engine it
75
+ needs into `~/.simantic/` and checks it against the published checksum. The
76
+ wheel on PyPI holds only Python code; the simulators are never inside it.
77
+
78
+ Sign in once if you want to name MCUs by part number:
79
+
80
+ ```bash
81
+ simantic auth
82
+ ```
83
+
84
+ That opens a browser tab, much like `gh auth login`, and saves a token to
85
+ `~/.sim_id`. In CI, pipe one in instead: `echo $TOKEN | simantic auth`. If you
86
+ bring your own platform file (`repl="board.repl"`), you need no account at all.
87
+
88
+ Already have the `sim` binary? Put it on PATH or point `$SIMANTIC_SIM` at it.
89
+ `simantic status` shows what resolved.
90
+
91
+ ## Pick your engine
92
+
93
+ The same script runs on either engine. You choose per simulation:
94
+
95
+ ```python
96
+ Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") # Renode engine, the default
97
+ Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2", backend="rust") # our Rust engine
98
+ ```
99
+
100
+ The Rust engine is a small extension module, runs one machine, and is
101
+ considerably faster. Anything it cannot do yet, such as multi machine scenarios
102
+ or CAN and radio injection, raises `simantic.NotSupported` and names the gap
103
+ instead of quietly doing nothing. You can follow what each engine covers in
104
+ [simantic-core#183](https://github.com/simantic-dev/simantic-core/issues/183).
105
+
106
+ ## Testing with pytest
107
+
108
+ Take the `sim` fixture and write ordinary tests:
109
+
110
+ ```python
111
+ def test_timer_irq_fires(sim):
112
+ s = sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2")
113
+ s.expect("fired=1", timeout=8)
114
+ assert s.read_u32("fired") == 1
115
+ ```
116
+
117
+ The engine starts once per worker rather than once per test, and every machine
118
+ is closed for you. When a test fails, its UART transcript is attached to the
119
+ report, because that is usually the evidence you want.
120
+
121
+ `--sim-backend=renode|rust|both` chooses the engine. With `both`, each test runs
122
+ on each and the engine name appears in the test id. Anything an engine cannot do
123
+ is reported as a skip with the reason, so one suite can target both and stay
124
+ honest about what each covers.
125
+
126
+ One tip worth real time: on the Renode engine, every hand off between Python and
127
+ the simulation costs a few hundred microseconds. Reading is free, pausing and
128
+ resuming is not. Prefer `expect()`, which crosses once, over a loop that polls
129
+ every millisecond. On the Rust engine, polling is essentially free.
130
+
131
+ If you keep `test.yaml` fixture manifests, installing the package also turns
132
+ each one into its own pytest item, so you get `-k` filtering, `--junitxml`, and
133
+ xdist for free. Multi machine manifests need the `--scenario` runner and report
134
+ as skips for now.
135
+
136
+ For a single run with no assertions in the middle, there is `run_firmware(...)`:
137
+
138
+ ```python
139
+ run = simantic.run_firmware("build/zephyr.elf", mcu="STM32F401RE",
140
+ expect=["RESULT: PASS"])
141
+ assert run.passed, run.failure_report()
142
+ ```
143
+
144
+ ## Telemetry
145
+
146
+ When you are signed in, we count the shape of a pytest session (how many tests
147
+ ran, passed, failed, skipped) and which SDK calls you make, by name only. It is
148
+ one request per pytest run, buffered in `~/.simantic/usage.jsonl`, and uploaded
149
+ at most hourly, so nothing ever waits on the network.
150
+
151
+ We do not send file paths, project names, test names, firmware, or simulation
152
+ output. Those are yours. Turn it off whenever you like:
153
+
154
+ ```bash
155
+ export SIMANTIC_TELEMETRY=0 # or DO_NOT_TRACK=1
156
+ ```
157
+
158
+ ## Questions
159
+
160
+ We would genuinely like to hear how this goes for you, especially if something
161
+ is confusing or broken. Write to **founder@simantic.dev**, or open an issue.
162
+
163
+ ## License
164
+
165
+ MIT. The simulators it drives are separate software under their own terms.
@@ -0,0 +1,143 @@
1
+ <p align="center">
2
+ <img src="https://simantic.dev/simantic_logo_4_full_transparent.png" alt="Simantic" width="340">
3
+ </p>
4
+
5
+ <h3 align="center">Test your firmware without a board.</h3>
6
+
7
+ <p align="center">
8
+ <a href="https://pypi.org/project/simantic/"><img src="https://img.shields.io/pypi/v/simantic.svg" alt="PyPI"></a>
9
+ <img src="https://img.shields.io/pypi/pyversions/simantic.svg" alt="Python versions">
10
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT licence">
11
+ </p>
12
+
13
+ Nothing to plug in, nothing to flash. Simantic boots your real ELF on a
14
+ simulated microcontroller and hands you the whole machine from Python. Watch it
15
+ print, press a button, read a variable straight out of RAM.
16
+
17
+ ```bash
18
+ pip install simantic
19
+ ```
20
+
21
+ ```python
22
+ from simantic import Sim
23
+
24
+ with Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") as sim:
25
+ sim.expect("ready")
26
+ sim.inject_gpio("gpioc", 13, True) # press the user button
27
+ sim.expect("button pressed")
28
+ assert sim.read_u32("press_count") == 1
29
+ ```
30
+
31
+ That is a whole test. No probe, no breakpoint, no waiting on hardware.
32
+
33
+ Three things you get that a bench cannot give you:
34
+
35
+ * **See inside.** Read any variable, register, or RTOS thread while the
36
+ firmware runs, without halting it.
37
+ * **Poke it.** Press buttons, send CAN frames, feed the radio, all from your
38
+ script.
39
+ * **Repeat exactly.** Time moves only when you ask, so a run comes out the same
40
+ every time, on your laptop and in CI.
41
+
42
+ The simulator lives inside your Python process, so there is no server to start
43
+ and no port to talk to.
44
+
45
+ > **Alpha, version 0.3.x.** We are still moving things around, so the API can
46
+ > change without a deprecation period. Pin an exact version
47
+ > (`simantic==0.3.0`) if you depend on it, and please hold off on production
48
+ > pipelines for now. Tell us what breaks.
49
+
50
+ ## Setup
51
+
52
+ `pip install` is the whole setup. The first `Sim(...)` downloads the engine it
53
+ needs into `~/.simantic/` and checks it against the published checksum. The
54
+ wheel on PyPI holds only Python code; the simulators are never inside it.
55
+
56
+ Sign in once if you want to name MCUs by part number:
57
+
58
+ ```bash
59
+ simantic auth
60
+ ```
61
+
62
+ That opens a browser tab, much like `gh auth login`, and saves a token to
63
+ `~/.sim_id`. In CI, pipe one in instead: `echo $TOKEN | simantic auth`. If you
64
+ bring your own platform file (`repl="board.repl"`), you need no account at all.
65
+
66
+ Already have the `sim` binary? Put it on PATH or point `$SIMANTIC_SIM` at it.
67
+ `simantic status` shows what resolved.
68
+
69
+ ## Pick your engine
70
+
71
+ The same script runs on either engine. You choose per simulation:
72
+
73
+ ```python
74
+ Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") # Renode engine, the default
75
+ Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2", backend="rust") # our Rust engine
76
+ ```
77
+
78
+ The Rust engine is a small extension module, runs one machine, and is
79
+ considerably faster. Anything it cannot do yet, such as multi machine scenarios
80
+ or CAN and radio injection, raises `simantic.NotSupported` and names the gap
81
+ instead of quietly doing nothing. You can follow what each engine covers in
82
+ [simantic-core#183](https://github.com/simantic-dev/simantic-core/issues/183).
83
+
84
+ ## Testing with pytest
85
+
86
+ Take the `sim` fixture and write ordinary tests:
87
+
88
+ ```python
89
+ def test_timer_irq_fires(sim):
90
+ s = sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2")
91
+ s.expect("fired=1", timeout=8)
92
+ assert s.read_u32("fired") == 1
93
+ ```
94
+
95
+ The engine starts once per worker rather than once per test, and every machine
96
+ is closed for you. When a test fails, its UART transcript is attached to the
97
+ report, because that is usually the evidence you want.
98
+
99
+ `--sim-backend=renode|rust|both` chooses the engine. With `both`, each test runs
100
+ on each and the engine name appears in the test id. Anything an engine cannot do
101
+ is reported as a skip with the reason, so one suite can target both and stay
102
+ honest about what each covers.
103
+
104
+ One tip worth real time: on the Renode engine, every hand off between Python and
105
+ the simulation costs a few hundred microseconds. Reading is free, pausing and
106
+ resuming is not. Prefer `expect()`, which crosses once, over a loop that polls
107
+ every millisecond. On the Rust engine, polling is essentially free.
108
+
109
+ If you keep `test.yaml` fixture manifests, installing the package also turns
110
+ each one into its own pytest item, so you get `-k` filtering, `--junitxml`, and
111
+ xdist for free. Multi machine manifests need the `--scenario` runner and report
112
+ as skips for now.
113
+
114
+ For a single run with no assertions in the middle, there is `run_firmware(...)`:
115
+
116
+ ```python
117
+ run = simantic.run_firmware("build/zephyr.elf", mcu="STM32F401RE",
118
+ expect=["RESULT: PASS"])
119
+ assert run.passed, run.failure_report()
120
+ ```
121
+
122
+ ## Telemetry
123
+
124
+ When you are signed in, we count the shape of a pytest session (how many tests
125
+ ran, passed, failed, skipped) and which SDK calls you make, by name only. It is
126
+ one request per pytest run, buffered in `~/.simantic/usage.jsonl`, and uploaded
127
+ at most hourly, so nothing ever waits on the network.
128
+
129
+ We do not send file paths, project names, test names, firmware, or simulation
130
+ output. Those are yours. Turn it off whenever you like:
131
+
132
+ ```bash
133
+ export SIMANTIC_TELEMETRY=0 # or DO_NOT_TRACK=1
134
+ ```
135
+
136
+ ## Questions
137
+
138
+ We would genuinely like to hear how this goes for you, especially if something
139
+ is confusing or broken. Write to **founder@simantic.dev**, or open an issue.
140
+
141
+ ## License
142
+
143
+ MIT. The simulators it drives are separate software under their own terms.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simantic"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Python SDK and pytest plugin for the Simantic circuit and firmware simulators"
9
9
  # PyYAML reads sim-fixtures test.yaml manifests; pythonnet hosts the
10
10
  # simulation engine (Simantic.Core, .NET) in-process for `simantic.Sim`.
@@ -0,0 +1,56 @@
1
+ """Python control of the Simantic simulators.
2
+
3
+ The firmware engine, hosted in your process.
4
+
5
+ import simantic
6
+
7
+ with simantic.Sim(elf="fw.elf", repl="board.repl") as sim: # live control
8
+ sim.expect("ready"); sim.run_for(0.5)
9
+ run = simantic.run_firmware("fw.elf", repl="board.repl",
10
+ expect=["RESULT: PASS"]) # one-shot
11
+
12
+ The engine is fetched on first use. The `sim` CLI is optional; point
13
+ `$SIMANTIC_SIM` at one, or put it on PATH.
14
+
15
+ Installing this package also registers a pytest plugin that turns `test.yaml`
16
+ fixture manifests into individually addressable pytest items.
17
+ """
18
+
19
+ from ._locate import BinaryNotFound
20
+ from .fixtures import (
21
+ Manifest,
22
+ ModelLibraryUnavailable,
23
+ UnsupportedManifest,
24
+ load_manifest,
25
+ )
26
+ from .mcu import ServerNotConfigured, SimError, SimRun, sim_binary
27
+ from .mcu import run as run_firmware
28
+ from .engine import EngineNotFound, engine_dir, rust_engine_dir
29
+ from .session import BACKENDS, ExpectTimeout, Match, Sim
30
+ from ._rust import NotSupported
31
+
32
+ __version__ = "0.3.0"
33
+
34
+ __all__ = [
35
+ # firmware
36
+ "Manifest",
37
+ "ModelLibraryUnavailable",
38
+ "ServerNotConfigured",
39
+ "SimError",
40
+ "SimRun",
41
+ "UnsupportedManifest",
42
+ "load_manifest",
43
+ "run_firmware",
44
+ "sim_binary",
45
+ # scripted sessions
46
+ "Sim",
47
+ "Match",
48
+ "ExpectTimeout",
49
+ "EngineNotFound",
50
+ "NotSupported",
51
+ "BACKENDS",
52
+ "engine_dir",
53
+ "rust_engine_dir",
54
+ # shared
55
+ "BinaryNotFound",
56
+ ]
@@ -15,16 +15,8 @@ from . import auth, install, telemetry
15
15
  from ._locate import BinaryNotFound, locate
16
16
  from .mcu import BINARY as SIM_BINARY
17
17
  from .mcu import ENV_VAR as SIM_ENV
18
- from ._locate import BINARY as ANALOG_BINARY
19
- from ._locate import ENV_VAR as ANALOG_ENV
20
- from .pyrite import BINARY as PYRITE_BINARY
21
- from .pyrite import ENV_VAR as PYRITE_ENV
22
18
 
23
- BINARIES = (
24
- (ANALOG_BINARY, ANALOG_ENV),
25
- (SIM_BINARY, SIM_ENV),
26
- (PYRITE_BINARY, PYRITE_ENV),
27
- )
19
+ BINARIES = ((SIM_BINARY, SIM_ENV),)
28
20
 
29
21
 
30
22
  def _auth(args) -> int:
@@ -47,20 +39,22 @@ def _auth(args) -> int:
47
39
 
48
40
 
49
41
  def _install(args) -> int:
50
- names = args.binary or [name for name, _ in BINARIES] + [install.ENGINE_KEY]
42
+ names = args.binary or [name for name, _ in BINARIES] + [install.ENGINE_KEY, install.RUST_ENGINE_KEY]
51
43
  failures = 0
52
44
  for name in names:
53
45
  try:
54
46
  if name == install.ENGINE_KEY:
55
47
  path = install.install_engine(force=args.force, channel=args.channel)
48
+ elif name == install.RUST_ENGINE_KEY:
49
+ path = install.install_rust_engine(force=args.force, channel=args.channel)
56
50
  else:
57
51
  path = install.install(name, force=args.force, channel=args.channel)
58
52
  print(f"{name}: {path}")
59
53
  except install.InstallError as exc:
60
54
  print(f"{name}: {exc}", file=sys.stderr)
61
55
  failures += 1
62
- # Partial success is still useful — one engine may be published and the
63
- # other not — so report it without discarding what did install.
56
+ # Partial success is still useful, so report it without discarding what
57
+ # did install.
64
58
  return 1 if failures == len(names) else 0
65
59
 
66
60
 
@@ -80,6 +74,9 @@ def _status(args) -> int:
80
74
  print(f" {name}: not found (run `simantic install {name}`)")
81
75
  engine = install.installed_engine()
82
76
  print(f" engine: {engine if engine else 'not found (fetched on first use, or `simantic install engine`)'}")
77
+ rust = install.installed_rust_engine()
78
+ missing = 'not found (fetched on first use of backend="rust", or `simantic install engine-rust`)'
79
+ print(f" engine-rust: {rust if rust else missing}")
83
80
  print(telemetry.describe())
84
81
  return 0
85
82
 
@@ -106,7 +103,7 @@ def main(argv: list[str] | None = None) -> int:
106
103
  p_auth.set_defaults(func=_auth)
107
104
 
108
105
  p_install = sub.add_parser("install", help="download simulator binaries")
109
- p_install.add_argument("binary", nargs="*", help="defaults to all known binaries and the engine")
106
+ p_install.add_argument("binary", nargs="*", help="defaults to all known binaries and both engines")
110
107
  p_install.add_argument(
111
108
  "--force", action="store_true", help="re-download even if already present"
112
109
  )
@@ -0,0 +1,39 @@
1
+ """Symbol addresses from a 32-bit little-endian ELF, with the standard library.
2
+
3
+ The Renode backend resolves symbols inside the engine. The Rust engine does
4
+ not carry a symbol table, so `Sim.symbol()` on that backend reads `.symtab`
5
+ here — the same answer, from the same file.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import struct
11
+
12
+ SHT_SYMTAB = 2
13
+ STT_FUNC = 2
14
+
15
+
16
+ def symbols(elf: bytes) -> dict[str, int]:
17
+ if elf[:4] != b"\x7fELF" or elf[4] != 1 or elf[5] != 1:
18
+ raise ValueError("only 32-bit little-endian ELF images are supported")
19
+ (shoff,) = struct.unpack_from("<I", elf, 0x20)
20
+ shentsize, shnum = struct.unpack_from("<HH", elf, 0x2E)
21
+ sections = [struct.unpack_from("<IIIIIIIIII", elf, shoff + i * shentsize) for i in range(shnum)]
22
+
23
+ out: dict[str, int] = {}
24
+ for sh in sections:
25
+ _, sh_type, _, _, offset, size, link, _, _, entsize = sh
26
+ if sh_type != SHT_SYMTAB or entsize == 0:
27
+ continue
28
+ strtab_off, strtab_size = sections[link][4], sections[link][5]
29
+ strtab = elf[strtab_off : strtab_off + strtab_size]
30
+ for i in range(size // entsize):
31
+ name_idx, value, _, info = struct.unpack_from("<IIIB", elf, offset + i * entsize)
32
+ if name_idx == 0:
33
+ continue
34
+ end = strtab.index(b"\0", name_idx)
35
+ name = strtab[name_idx:end].decode("utf-8", "replace")
36
+ if info & 0xF == STT_FUNC:
37
+ value &= ~1 # Thumb bit is a call-site convention, not the address
38
+ out.setdefault(name, value)
39
+ return out
@@ -66,11 +66,3 @@ def locate(
66
66
  f"PATH, or set ${env_var} to its location."
67
67
  )
68
68
 
69
-
70
- ENV_VAR = "SIMANTIC_ANALOG_CLI"
71
- BINARY = "analog-cli"
72
-
73
-
74
- def analog_cli(explicit: str | os.PathLike[str] | None = None) -> Path:
75
- """Resolve the analog-cli binary, or raise BinaryNotFound."""
76
- return locate(BINARY, ENV_VAR, explicit)
@@ -0,0 +1,108 @@
1
+ """Platform text for the Rust backend.
2
+
3
+ The Renode backend hands `.replx` templates to Simantic.Core, which renders
4
+ them. The Rust engine parses plain `.repl`, so the same rendering happens
5
+ here: every `{{a:b:default}}` placeholder becomes its default, and a default
6
+ that is an arithmetic expression (`84000000 / 1000000 * 1.25`) is evaluated.
7
+ Model names resolve the way `sim --mcu` does — `~/.sim_cache`, else the
8
+ backend with stored credentials, then cached — or from a local model library
9
+ when $SIMANTIC_MCU_LIB is set.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ast
15
+ import json
16
+ import os
17
+ import re
18
+ import urllib.error
19
+ import urllib.parse
20
+ import urllib.request
21
+ from pathlib import Path
22
+
23
+ from . import auth
24
+ from .fixtures import MCU_LIB_ENV, platform_path
25
+ from .mcu import SimError
26
+
27
+ MCU_DETAILS_URL = "https://drjdhqfvrttolueolzif.supabase.co/functions/v1/get-mcu-details"
28
+
29
+ _PLACEHOLDER = re.compile(r"\{\{([^}]*)\}\}")
30
+ _ARITHMETIC = re.compile(r"[0-9. */+()-]+")
31
+
32
+
33
+ def _evaluate(expr: str) -> str:
34
+ """Fold a numeric expression; anything else passes through untouched."""
35
+ expr = expr.strip()
36
+ if not _ARITHMETIC.fullmatch(expr) or re.fullmatch(r"[0-9.]+", expr):
37
+ return expr
38
+ tree = ast.parse(expr, mode="eval")
39
+
40
+ def fold(node):
41
+ if isinstance(node, ast.Expression):
42
+ return fold(node.body)
43
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
44
+ return node.value
45
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)):
46
+ a, b = fold(node.left), fold(node.right)
47
+ return {ast.Add: a + b, ast.Sub: a - b, ast.Mult: a * b, ast.Div: a / b}[type(node.op)]
48
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
49
+ return -fold(node.operand)
50
+ raise ValueError(f"unsupported expression in platform template: {expr!r}")
51
+
52
+ value = fold(tree)
53
+ return str(int(value)) if float(value).is_integer() else str(value)
54
+
55
+
56
+ def render(text: str) -> str:
57
+ """`.replx` → `.repl`: placeholders take their defaults."""
58
+ return _PLACEHOLDER.sub(lambda m: _evaluate(m.group(1).split(":")[-1]), text)
59
+
60
+
61
+ def cache_dir() -> Path:
62
+ return Path(os.environ.get("HOME", "")) / ".sim_cache"
63
+
64
+
65
+ def model_replx(mcu: str, *, use_cache: bool = True) -> str:
66
+ """The `.replx` text for a model name, like `sim --mcu`."""
67
+ if os.environ.get(MCU_LIB_ENV):
68
+ return platform_path(mcu, None, Path.cwd()).read_text()
69
+ cached = cache_dir() / f"{mcu.lower()}.json"
70
+ if use_cache and cached.exists():
71
+ replx = json.loads(cached.read_text()).get("replx")
72
+ if replx:
73
+ return replx
74
+ try:
75
+ credentials = auth.load()
76
+ except auth.NotAuthenticated as exc:
77
+ raise SimError(f"mcu={mcu!r} needs credentials to fetch the model: {exc}") from None
78
+ request = urllib.request.Request(
79
+ f"{MCU_DETAILS_URL}?model={urllib.parse.quote(mcu)}",
80
+ headers={"Authorization": f"Bearer {credentials.api_key}"},
81
+ )
82
+ try:
83
+ with urllib.request.urlopen(request, timeout=30) as response:
84
+ details = json.loads(response.read())
85
+ except urllib.error.HTTPError as exc:
86
+ raise SimError(f"mcu={mcu!r} is not a supported model (HTTP {exc.code})") from None
87
+ except urllib.error.URLError as exc:
88
+ raise SimError(f"cannot reach the model backend: {exc.reason}") from None
89
+ replx = details.get("replx")
90
+ if not replx:
91
+ raise SimError(f"the backend returned no platform for mcu={mcu!r}")
92
+ cached.parent.mkdir(parents=True, exist_ok=True)
93
+ cached.write_text(json.dumps({"model": mcu, "replx": replx, "deprecated": bool(details.get("deprecated"))}))
94
+ return replx
95
+
96
+
97
+ def platform_text(*, repl: Path | None, mcu: str | None, overlay: Path | None) -> str:
98
+ """Rendered `.repl` text for one machine."""
99
+ if repl is not None:
100
+ text = repl.read_text()
101
+ else:
102
+ assert mcu is not None
103
+ text = model_replx(mcu)
104
+ if overlay is not None:
105
+ # The platform grammar has no comment syntax; strip note lines first.
106
+ body = "\n".join(l for l in overlay.read_text().splitlines() if not l.lstrip().startswith(("#", "//")))
107
+ text = text.rstrip() + "\n\n" + body.strip() + "\n"
108
+ return render(text)