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.
- simantic-0.3.0/PKG-INFO +165 -0
- simantic-0.3.0/README.md +143 -0
- {simantic-0.2.0 → simantic-0.3.0}/pyproject.toml +1 -1
- simantic-0.3.0/src/simantic/__init__.py +56 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/_cli.py +10 -13
- simantic-0.3.0/src/simantic/_elf.py +39 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/_locate.py +0 -8
- simantic-0.3.0/src/simantic/_replx.py +108 -0
- simantic-0.3.0/src/simantic/_rust.py +128 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/engine.py +80 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/install.py +81 -10
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/pytest_plugin.py +128 -72
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/session.py +180 -104
- {simantic-0.2.0 → simantic-0.3.0}/tests/test_install.py +70 -15
- simantic-0.3.0/tests/test_pytest_surface.py +60 -0
- simantic-0.3.0/tests/test_rust_backend.py +218 -0
- {simantic-0.2.0 → simantic-0.3.0}/tests/test_spool.py +2 -2
- simantic-0.2.0/PKG-INFO +0 -219
- simantic-0.2.0/README.md +0 -197
- simantic-0.2.0/src/simantic/__init__.py +0 -85
- simantic-0.2.0/src/simantic/agent.py +0 -175
- simantic-0.2.0/src/simantic/analog.py +0 -94
- simantic-0.2.0/src/simantic/pyrite.py +0 -72
- simantic-0.2.0/src/simantic/report.py +0 -194
- simantic-0.2.0/tests/fixtures/divider/divider.sim.toml +0 -29
- simantic-0.2.0/tests/test_agent.py +0 -108
- simantic-0.2.0/tests/test_plan.py +0 -52
- simantic-0.2.0/tests/test_pyrite.py +0 -81
- simantic-0.2.0/tests/test_report.py +0 -164
- {simantic-0.2.0 → simantic-0.3.0}/.github/workflows/ci.yml +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/.gitignore +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/LICENSE +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/PUBLISHING.md +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/docs/session-api.md +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/examples/parallel_sweep.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/examples/step_and_peek.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/__main__.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/auth.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/fixtures.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/mcu.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/src/simantic/telemetry.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/tests/conftest.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/tests/test_auth.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/tests/test_fixtures.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/tests/test_packaging.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/tests/test_session.py +0 -0
- {simantic-0.2.0 → simantic-0.3.0}/tests/test_telemetry.py +0 -0
simantic-0.3.0/PKG-INFO
ADDED
|
@@ -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.
|
simantic-0.3.0/README.md
ADDED
|
@@ -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.
|
|
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
|
|
63
|
-
#
|
|
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
|
|
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)
|