simantic 0.3.0__tar.gz → 0.4.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 → simantic-0.4.0}/.github/workflows/ci.yml +5 -12
- simantic-0.4.0/PKG-INFO +283 -0
- {simantic-0.3.0 → simantic-0.4.0}/PUBLISHING.md +11 -14
- simantic-0.4.0/README.md +261 -0
- simantic-0.4.0/docs/session-api.md +177 -0
- {simantic-0.3.0 → simantic-0.4.0}/examples/parallel_sweep.py +2 -2
- {simantic-0.3.0 → simantic-0.4.0}/examples/step_and_peek.py +2 -2
- {simantic-0.3.0 → simantic-0.4.0}/pyproject.toml +3 -3
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/__init__.py +6 -4
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/_cli.py +70 -3
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/_elf.py +7 -3
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/_replx.py +28 -9
- simantic-0.4.0/src/simantic/_rust.py +342 -0
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/engine.py +24 -0
- simantic-0.4.0/src/simantic/esp_image.py +406 -0
- simantic-0.4.0/src/simantic/fixtures.py +272 -0
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/mcu.py +38 -17
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/pytest_plugin.py +102 -35
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/session.py +138 -38
- simantic-0.4.0/tests/test_esp_image.py +274 -0
- simantic-0.4.0/tests/test_fixtures.py +225 -0
- simantic-0.4.0/tests/test_model_auth_error.py +19 -0
- simantic-0.4.0/tests/test_rust_backend.py +409 -0
- simantic-0.4.0/tests/test_session.py +104 -0
- simantic-0.3.0/PKG-INFO +0 -165
- simantic-0.3.0/README.md +0 -143
- simantic-0.3.0/docs/session-api.md +0 -108
- simantic-0.3.0/src/simantic/_rust.py +0 -128
- simantic-0.3.0/src/simantic/fixtures.py +0 -144
- simantic-0.3.0/tests/test_fixtures.py +0 -141
- simantic-0.3.0/tests/test_rust_backend.py +0 -218
- simantic-0.3.0/tests/test_session.py +0 -56
- {simantic-0.3.0 → simantic-0.4.0}/.gitignore +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/LICENSE +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/__main__.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/_locate.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/auth.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/install.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/src/simantic/telemetry.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/tests/conftest.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/tests/test_auth.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/tests/test_install.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/tests/test_packaging.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/tests/test_pytest_surface.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/tests/test_spool.py +0 -0
- {simantic-0.3.0 → simantic-0.4.0}/tests/test_telemetry.py +0 -0
|
@@ -25,8 +25,6 @@ on:
|
|
|
25
25
|
- '.claude/**'
|
|
26
26
|
- 'LICENSE'
|
|
27
27
|
pull_request:
|
|
28
|
-
# `ready_for_review` is NOT a default activity type. Without it, flipping a
|
|
29
|
-
# draft to ready fires no event, so the macOS leg gated below never runs.
|
|
30
28
|
types: [opened, synchronize, reopened, ready_for_review]
|
|
31
29
|
paths-ignore:
|
|
32
30
|
- 'docs/**'
|
|
@@ -40,20 +38,15 @@ concurrency:
|
|
|
40
38
|
|
|
41
39
|
jobs:
|
|
42
40
|
test:
|
|
43
|
-
runs-on:
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# proven on all three), and on a pull request once marked ready for review.
|
|
49
|
-
# Windows is left unconditional: at 2x it is not what the bill is made of.
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
# macOS dropped from CI entirely (ci: drop macOS, single-OS fixtures,
|
|
43
|
+
# manual-only aux checks) — the SDK is pure Python, so a macOS runner
|
|
44
|
+
# proved very little here that Linux does not, at 10x Linux's billing
|
|
45
|
+
# rate. macOS is tested locally instead.
|
|
50
46
|
timeout-minutes: 15
|
|
51
47
|
strategy:
|
|
52
48
|
fail-fast: false
|
|
53
49
|
matrix:
|
|
54
|
-
os: ${{ (github.event_name != 'pull_request' || github.event.pull_request.draft == false)
|
|
55
|
-
&& fromJSON('["ubuntu-latest", "macos-latest"]')
|
|
56
|
-
|| fromJSON('["ubuntu-latest"]') }}
|
|
57
50
|
# 3.11 is the floor (tomllib landed there); 3.13 guards the top end.
|
|
58
51
|
python: ["3.11", "3.13"]
|
|
59
52
|
steps:
|
simantic-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: simantic
|
|
3
|
+
Version: 0.4.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/pippy
|
|
7
|
+
Project-URL: Issues, https://github.com/simantic-dev/pippy/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.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.4.x.** We are still moving things around, so the API can
|
|
68
|
+
> change without a deprecation period. Pin an exact version
|
|
69
|
+
> (`simantic==0.4.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`.
|
|
86
|
+
|
|
87
|
+
Already have the `sim` binary? Put it on PATH or point `$SIMANTIC_SIM` at it.
|
|
88
|
+
`simantic status` shows what resolved.
|
|
89
|
+
|
|
90
|
+
## Pick your engine
|
|
91
|
+
|
|
92
|
+
The same script runs on either engine. You choose per simulation:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") # Renode engine, the default
|
|
96
|
+
Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2", backend="rust") # our Rust engine
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The Rust engine is a small extension module, runs one machine, and is
|
|
100
|
+
considerably faster. Anything it cannot do yet, such as multi machine scenarios
|
|
101
|
+
or CAN and radio injection, raises `simantic.NotSupported` and names the gap
|
|
102
|
+
instead of quietly doing nothing. What each engine covers is listed in
|
|
103
|
+
[the engine table](https://github.com/simantic-dev/pippy/blob/main/docs/session-api.md#what-each-engine-supports).
|
|
104
|
+
|
|
105
|
+
## ESP32 images
|
|
106
|
+
|
|
107
|
+
The ESP32-C3, C6 and P4 boot the way silicon does: Espressif's mask ROM runs
|
|
108
|
+
first, then your bootloader, then your app. `sim --elf` therefore takes one ELF
|
|
109
|
+
that carries the ROM and your whole flash image. Build it from the files your
|
|
110
|
+
ESP-IDF or PlatformIO build already produced:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
simantic esp-image --chip esp32c3 \
|
|
114
|
+
--part 0x0:bootloader.bin --part 0x8000:partitions.bin --part 0x10000:firmware.bin \
|
|
115
|
+
--flash-size 16MB -o image.elf
|
|
116
|
+
sim --elf image.elf ...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Already have a merged image from `esptool.py merge_bin`? Pass `--flash merged.bin`
|
|
120
|
+
instead of the parts. The same thing from Python is
|
|
121
|
+
`simantic.esp_image.build_image("esp32c3", flash, out="image.elf")`.
|
|
122
|
+
|
|
123
|
+
The mask ROM is Espressif's and is not bundled. On first use it is downloaded
|
|
124
|
+
from Espressif's own repositories, pinned by commit and SHA-256, and cached in
|
|
125
|
+
`~/.simantic/esp-rom`: the raw C3 and C6 dumps from
|
|
126
|
+
[espressif/qemu](https://github.com/espressif/qemu/tree/master/pc-bios), and
|
|
127
|
+
the P4 ROM from the [esp-rom-elfs](https://github.com/espressif/esp-rom-elfs)
|
|
128
|
+
release ESP-IDF installs. Offline, or with your own copy, pass `--rom`.
|
|
129
|
+
`simantic esp-rom --chip esp32c3` downloads it ahead of time and prints where it
|
|
130
|
+
went.
|
|
131
|
+
|
|
132
|
+
## Testing with pytest
|
|
133
|
+
|
|
134
|
+
Take the `sim` fixture and write ordinary tests:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
def test_timer_irq_fires(sim):
|
|
138
|
+
s = sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2")
|
|
139
|
+
s.expect("fired=1", timeout=8)
|
|
140
|
+
assert s.read_u32("fired") == 1
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The engine starts once per worker rather than once per test, and every machine
|
|
144
|
+
is closed for you. When a test fails, its UART transcript is attached to the
|
|
145
|
+
report, because that is usually the evidence you want.
|
|
146
|
+
|
|
147
|
+
### Test what went over the wire
|
|
148
|
+
|
|
149
|
+
Firmware that prints "sensor OK" is reporting its own bookkeeping. The bus
|
|
150
|
+
tells you what happened. Put a scripted device on the other end of each bus
|
|
151
|
+
(a `parts` entry per device, each backed by a few dozen lines of Python), wire
|
|
152
|
+
the media in a scenario, and assert on `frames()`:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
SCENARIO = {
|
|
156
|
+
"machines": {"dut": {
|
|
157
|
+
"mcu": "STM32H753IITX", "elf": "fc.elf",
|
|
158
|
+
"parts": [
|
|
159
|
+
{"name": "imu0", "type": "spi-device", "bus": "spi1", "cs": "PC4", "script": "icm42688.py"},
|
|
160
|
+
{"name": "baro", "type": "i2c-device", "bus": "i2c2", "address": 0x63, "script": "icp20100.py"},
|
|
161
|
+
{"name": "gpspeer", "type": "uart-device", "baud": 230400, "script": "gps.py"},
|
|
162
|
+
{"name": "canpeer", "type": "can-node", "script": "dronecan.py"},
|
|
163
|
+
],
|
|
164
|
+
}},
|
|
165
|
+
"media": [
|
|
166
|
+
{"type": "uart", "connect": ["dut.usart6", "dut.gpspeer"]},
|
|
167
|
+
{"type": "can", "connect": ["dut.fdcan1", "dut.canpeer"]},
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
def test_imu_configured_then_streams(sim):
|
|
172
|
+
s = sim(scenario=SCENARIO, machine="dut", uart="usart1")
|
|
173
|
+
s.run_for(3.0) # virtual seconds
|
|
174
|
+
imu = [f for f in s.frames() if f["label"] == "imu0"] # one dict per chip-select window
|
|
175
|
+
cfg = next(f for f in imu if f["data"][0] == 0x4F) # GYRO_CONFIG0 write
|
|
176
|
+
assert cfg["data"][1] & 0x0F == 0x06 # ODR the driver programmed
|
|
177
|
+
assert cfg["miso"][0] == 0x00 # what the slave answered
|
|
178
|
+
bursts = [f for f in imu if f["t"] > cfg["t"] and len(f["data"]) > 16]
|
|
179
|
+
assert len(bursts) > 100 # FIFO reads after configuration
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Every record carries its virtual-time stamp, so a rate is a subtraction between
|
|
183
|
+
consecutive frames and an ordering is a comparison. The scenario dict is the
|
|
184
|
+
same shape as `sim --scenario`'s YAML, and because the test owns it, the
|
|
185
|
+
negative control is the same test with a peer removed from `media`. Changing
|
|
186
|
+
what a device does (a NACK for 50 ms, a stale frame, a node that stops
|
|
187
|
+
answering at t = 5 s) is an edit to the peer script, on the virtual clock. The
|
|
188
|
+
`sim` guide covers every output and the peer API:
|
|
189
|
+
https://simantic.com/docs
|
|
190
|
+
|
|
191
|
+
`--sim-backend=renode|rust|both` chooses the engine. With `both`, each test runs
|
|
192
|
+
on each and the engine name appears in the test id. Anything an engine cannot do
|
|
193
|
+
is reported as a skip with the reason, so one suite can target both and stay
|
|
194
|
+
honest about what each covers.
|
|
195
|
+
|
|
196
|
+
`test.yaml` manifests follow the same option. On `rust` a manifest, including a
|
|
197
|
+
multi machine one with `media:`, runs to its timeout in one call and its UART
|
|
198
|
+
output is checked afterwards; `expect_frames` checks are skipped there for now.
|
|
199
|
+
`simantic.run_firmware(..., backend="rust")` does the same for a single ELF.
|
|
200
|
+
|
|
201
|
+
One tip worth real time: on the Renode engine, every hand off between Python and
|
|
202
|
+
the simulation costs a few hundred microseconds. Reading is free, pausing and
|
|
203
|
+
resuming is not. Prefer `expect()`, which crosses once, over a loop that polls
|
|
204
|
+
every millisecond. On the Rust engine, polling is essentially free.
|
|
205
|
+
|
|
206
|
+
If you keep `test.yaml` fixture manifests, installing the package also turns
|
|
207
|
+
each one into its own pytest item, so you get `-k` filtering, `--junitxml`, and
|
|
208
|
+
xdist for free. A manifest is the fire-and-forget form of a test: name the
|
|
209
|
+
machine(s) with their `parts`, the `media` that wire UART and CAN peers to
|
|
210
|
+
the firmware's controllers, how long to run, and what must and must not
|
|
211
|
+
appear:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
machines:
|
|
215
|
+
dut:
|
|
216
|
+
mcu: STM32H753ZI
|
|
217
|
+
elf: fc.elf
|
|
218
|
+
parts:
|
|
219
|
+
- { name: gpspeer, type: uart-device, baud: 230400, script: gps.py }
|
|
220
|
+
media:
|
|
221
|
+
- { type: uart, connect: [dut.usart6, dut.gpspeer] }
|
|
222
|
+
timeout: 12
|
|
223
|
+
expect: ["GPS fix: 3"]
|
|
224
|
+
expect_absent: ["RESULT: FAIL"]
|
|
225
|
+
expect_frames: ["SPI Rx cs=0 len=2 mosi=4F 06"] # same line shape as `sim --frames`
|
|
226
|
+
expect_frames_absent: ["CAN Dropped"]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Single-machine manifests (`mcu:` at the top level) are the one-machine case.
|
|
230
|
+
Models resolve through your account; no checkout of ours is needed. A model can
|
|
231
|
+
require a minimum engine version; if yours is older, the failure names the
|
|
232
|
+
version and the fix (`simantic install engine --force`).
|
|
233
|
+
|
|
234
|
+
Three more keys cover fixtures that need files or peers on the host side:
|
|
235
|
+
`sparse_files` creates blank files of a given size before the run (a blank SD
|
|
236
|
+
card, say), `networkServices` adds scripted network peers, and `{TEST_DIR}` and
|
|
237
|
+
`{WORK_DIR}` in a board file expand to the manifest's directory and a per-test
|
|
238
|
+
scratch directory. A manifest whose `sim_args` asks for a `sim`
|
|
239
|
+
command-line flag with no equivalent here is skipped, with the flag named.
|
|
240
|
+
|
|
241
|
+
A run also has a wall-clock budget: 30 s of host time by default, 100 s with
|
|
242
|
+
`wireless: true`, or whatever `wall:` says. Going over it fails the test with
|
|
243
|
+
the virtual time reached. Slow simulation is a firmware busy-wait or a model
|
|
244
|
+
gap, and the report tells you which to go find rather than waiting it out.
|
|
245
|
+
|
|
246
|
+
For a single run with no assertions in the middle, there is `run_firmware(...)`:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
run = simantic.run_firmware("build/zephyr.elf", mcu="STM32F401RE",
|
|
250
|
+
expect=["RESULT: PASS"])
|
|
251
|
+
assert run.passed, run.failure_report()
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Telemetry
|
|
255
|
+
|
|
256
|
+
Only when you are signed in, we report two things:
|
|
257
|
+
|
|
258
|
+
* **Test counts.** At the end of a pytest run, how many simulator tests passed,
|
|
259
|
+
failed and were skipped. One request per run.
|
|
260
|
+
* **Which calls you use.** The names of the SDK calls and `simantic` commands
|
|
261
|
+
you run, such as `sdk.run_firmware` or `cli.install`, and how often. They are
|
|
262
|
+
counted in `~/.simantic/usage.jsonl` and uploaded at most hourly.
|
|
263
|
+
|
|
264
|
+
Each report also carries the version of this package, your Python version,
|
|
265
|
+
operating system and CPU architecture. Reports go out when a pytest run or a
|
|
266
|
+
`simantic` command finishes, never while a simulation is running, and a failed
|
|
267
|
+
or slow request is dropped silently.
|
|
268
|
+
|
|
269
|
+
We do not send file paths, project names, test names, call arguments, firmware,
|
|
270
|
+
or simulation output. Those are yours. Turn it off whenever you like:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
export SIMANTIC_TELEMETRY=0 # or DO_NOT_TRACK=1
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Questions
|
|
277
|
+
|
|
278
|
+
We would genuinely like to hear how this goes for you, especially if something
|
|
279
|
+
is confusing or broken. Write to **founder@simantic.dev**, or open an issue.
|
|
280
|
+
|
|
281
|
+
## License
|
|
282
|
+
|
|
283
|
+
MIT. The simulators it drives are separate software under their own terms.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Publishing `simantic` to PyPI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One-time setup (done; kept for reference), then the per-release loop. Steps marked **[you]** need a
|
|
4
4
|
human with the accounts; everything else is automated in
|
|
5
|
-
`.github/workflows/
|
|
5
|
+
`.github/workflows/ci.yml`.
|
|
6
6
|
|
|
7
7
|
## One-time setup
|
|
8
8
|
|
|
@@ -18,18 +18,15 @@ the first upload so a mistake is not permanent.
|
|
|
18
18
|
|
|
19
19
|
### 2. Claim the name **[you]**
|
|
20
20
|
|
|
21
|
-
`simantic`
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
push the real tag. Claiming early is cheap insurance against someone else
|
|
25
|
-
taking it.
|
|
21
|
+
Done: `simantic` is ours on PyPI. A published version number can never be
|
|
22
|
+
reused or overwritten — only yanked — so check the version before pushing a
|
|
23
|
+
tag.
|
|
26
24
|
|
|
27
25
|
### 3. Configure Trusted Publishing **[you]**
|
|
28
26
|
|
|
29
27
|
This replaces API tokens with short-lived OIDC credentials, so there is no
|
|
30
28
|
long-lived secret in the repo. On PyPI, go to your account's **Publishing**
|
|
31
|
-
page and add a
|
|
32
|
-
yet, which is the case before the first upload):
|
|
29
|
+
page and add a publisher:
|
|
33
30
|
|
|
34
31
|
| Field | Value |
|
|
35
32
|
|---|---|
|
|
@@ -69,8 +66,8 @@ uv run --with simantic --index https://test.pypi.org/simple/ \
|
|
|
69
66
|
3. Tag and push:
|
|
70
67
|
|
|
71
68
|
```bash
|
|
72
|
-
git tag v0.
|
|
73
|
-
git push origin v0.
|
|
69
|
+
git tag v0.4.0
|
|
70
|
+
git push origin v0.4.0
|
|
74
71
|
```
|
|
75
72
|
|
|
76
73
|
The workflow tests on Linux/macOS/Windows across 3.11 and 3.13, builds the
|
|
@@ -79,9 +76,9 @@ sdist and wheel, runs `twine check`, and uploads via trusted publishing.
|
|
|
79
76
|
## Versioning
|
|
80
77
|
|
|
81
78
|
Semantic versioning on the SDK's own surface, which is independent of the
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
engine version it drives: the package is 0.4.x while the engine is 0.6.x, and
|
|
80
|
+
the two are not meant to match. The engine a model needs is declared by the
|
|
81
|
+
model (`min_sim_version`), not by this package's version.
|
|
85
82
|
|
|
86
83
|
Stay on `0.x` until the API has survived real use. Pre-1.0 signals that
|
|
87
84
|
breaking changes can still happen, which is honest for a first release.
|
simantic-0.4.0/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://simantic.dev/simantic_logo_4_full.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.4.x.** We are still moving things around, so the API can
|
|
46
|
+
> change without a deprecation period. Pin an exact version
|
|
47
|
+
> (`simantic==0.4.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`.
|
|
64
|
+
|
|
65
|
+
Already have the `sim` binary? Put it on PATH or point `$SIMANTIC_SIM` at it.
|
|
66
|
+
`simantic status` shows what resolved.
|
|
67
|
+
|
|
68
|
+
## Pick your engine
|
|
69
|
+
|
|
70
|
+
The same script runs on either engine. You choose per simulation:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") # Renode engine, the default
|
|
74
|
+
Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2", backend="rust") # our Rust engine
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The Rust engine is a small extension module, runs one machine, and is
|
|
78
|
+
considerably faster. Anything it cannot do yet, such as multi machine scenarios
|
|
79
|
+
or CAN and radio injection, raises `simantic.NotSupported` and names the gap
|
|
80
|
+
instead of quietly doing nothing. What each engine covers is listed in
|
|
81
|
+
[the engine table](https://github.com/simantic-dev/pippy/blob/main/docs/session-api.md#what-each-engine-supports).
|
|
82
|
+
|
|
83
|
+
## ESP32 images
|
|
84
|
+
|
|
85
|
+
The ESP32-C3, C6 and P4 boot the way silicon does: Espressif's mask ROM runs
|
|
86
|
+
first, then your bootloader, then your app. `sim --elf` therefore takes one ELF
|
|
87
|
+
that carries the ROM and your whole flash image. Build it from the files your
|
|
88
|
+
ESP-IDF or PlatformIO build already produced:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
simantic esp-image --chip esp32c3 \
|
|
92
|
+
--part 0x0:bootloader.bin --part 0x8000:partitions.bin --part 0x10000:firmware.bin \
|
|
93
|
+
--flash-size 16MB -o image.elf
|
|
94
|
+
sim --elf image.elf ...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Already have a merged image from `esptool.py merge_bin`? Pass `--flash merged.bin`
|
|
98
|
+
instead of the parts. The same thing from Python is
|
|
99
|
+
`simantic.esp_image.build_image("esp32c3", flash, out="image.elf")`.
|
|
100
|
+
|
|
101
|
+
The mask ROM is Espressif's and is not bundled. On first use it is downloaded
|
|
102
|
+
from Espressif's own repositories, pinned by commit and SHA-256, and cached in
|
|
103
|
+
`~/.simantic/esp-rom`: the raw C3 and C6 dumps from
|
|
104
|
+
[espressif/qemu](https://github.com/espressif/qemu/tree/master/pc-bios), and
|
|
105
|
+
the P4 ROM from the [esp-rom-elfs](https://github.com/espressif/esp-rom-elfs)
|
|
106
|
+
release ESP-IDF installs. Offline, or with your own copy, pass `--rom`.
|
|
107
|
+
`simantic esp-rom --chip esp32c3` downloads it ahead of time and prints where it
|
|
108
|
+
went.
|
|
109
|
+
|
|
110
|
+
## Testing with pytest
|
|
111
|
+
|
|
112
|
+
Take the `sim` fixture and write ordinary tests:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
def test_timer_irq_fires(sim):
|
|
116
|
+
s = sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2")
|
|
117
|
+
s.expect("fired=1", timeout=8)
|
|
118
|
+
assert s.read_u32("fired") == 1
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The engine starts once per worker rather than once per test, and every machine
|
|
122
|
+
is closed for you. When a test fails, its UART transcript is attached to the
|
|
123
|
+
report, because that is usually the evidence you want.
|
|
124
|
+
|
|
125
|
+
### Test what went over the wire
|
|
126
|
+
|
|
127
|
+
Firmware that prints "sensor OK" is reporting its own bookkeeping. The bus
|
|
128
|
+
tells you what happened. Put a scripted device on the other end of each bus
|
|
129
|
+
(a `parts` entry per device, each backed by a few dozen lines of Python), wire
|
|
130
|
+
the media in a scenario, and assert on `frames()`:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
SCENARIO = {
|
|
134
|
+
"machines": {"dut": {
|
|
135
|
+
"mcu": "STM32H753IITX", "elf": "fc.elf",
|
|
136
|
+
"parts": [
|
|
137
|
+
{"name": "imu0", "type": "spi-device", "bus": "spi1", "cs": "PC4", "script": "icm42688.py"},
|
|
138
|
+
{"name": "baro", "type": "i2c-device", "bus": "i2c2", "address": 0x63, "script": "icp20100.py"},
|
|
139
|
+
{"name": "gpspeer", "type": "uart-device", "baud": 230400, "script": "gps.py"},
|
|
140
|
+
{"name": "canpeer", "type": "can-node", "script": "dronecan.py"},
|
|
141
|
+
],
|
|
142
|
+
}},
|
|
143
|
+
"media": [
|
|
144
|
+
{"type": "uart", "connect": ["dut.usart6", "dut.gpspeer"]},
|
|
145
|
+
{"type": "can", "connect": ["dut.fdcan1", "dut.canpeer"]},
|
|
146
|
+
],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def test_imu_configured_then_streams(sim):
|
|
150
|
+
s = sim(scenario=SCENARIO, machine="dut", uart="usart1")
|
|
151
|
+
s.run_for(3.0) # virtual seconds
|
|
152
|
+
imu = [f for f in s.frames() if f["label"] == "imu0"] # one dict per chip-select window
|
|
153
|
+
cfg = next(f for f in imu if f["data"][0] == 0x4F) # GYRO_CONFIG0 write
|
|
154
|
+
assert cfg["data"][1] & 0x0F == 0x06 # ODR the driver programmed
|
|
155
|
+
assert cfg["miso"][0] == 0x00 # what the slave answered
|
|
156
|
+
bursts = [f for f in imu if f["t"] > cfg["t"] and len(f["data"]) > 16]
|
|
157
|
+
assert len(bursts) > 100 # FIFO reads after configuration
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Every record carries its virtual-time stamp, so a rate is a subtraction between
|
|
161
|
+
consecutive frames and an ordering is a comparison. The scenario dict is the
|
|
162
|
+
same shape as `sim --scenario`'s YAML, and because the test owns it, the
|
|
163
|
+
negative control is the same test with a peer removed from `media`. Changing
|
|
164
|
+
what a device does (a NACK for 50 ms, a stale frame, a node that stops
|
|
165
|
+
answering at t = 5 s) is an edit to the peer script, on the virtual clock. The
|
|
166
|
+
`sim` guide covers every output and the peer API:
|
|
167
|
+
https://simantic.com/docs
|
|
168
|
+
|
|
169
|
+
`--sim-backend=renode|rust|both` chooses the engine. With `both`, each test runs
|
|
170
|
+
on each and the engine name appears in the test id. Anything an engine cannot do
|
|
171
|
+
is reported as a skip with the reason, so one suite can target both and stay
|
|
172
|
+
honest about what each covers.
|
|
173
|
+
|
|
174
|
+
`test.yaml` manifests follow the same option. On `rust` a manifest, including a
|
|
175
|
+
multi machine one with `media:`, runs to its timeout in one call and its UART
|
|
176
|
+
output is checked afterwards; `expect_frames` checks are skipped there for now.
|
|
177
|
+
`simantic.run_firmware(..., backend="rust")` does the same for a single ELF.
|
|
178
|
+
|
|
179
|
+
One tip worth real time: on the Renode engine, every hand off between Python and
|
|
180
|
+
the simulation costs a few hundred microseconds. Reading is free, pausing and
|
|
181
|
+
resuming is not. Prefer `expect()`, which crosses once, over a loop that polls
|
|
182
|
+
every millisecond. On the Rust engine, polling is essentially free.
|
|
183
|
+
|
|
184
|
+
If you keep `test.yaml` fixture manifests, installing the package also turns
|
|
185
|
+
each one into its own pytest item, so you get `-k` filtering, `--junitxml`, and
|
|
186
|
+
xdist for free. A manifest is the fire-and-forget form of a test: name the
|
|
187
|
+
machine(s) with their `parts`, the `media` that wire UART and CAN peers to
|
|
188
|
+
the firmware's controllers, how long to run, and what must and must not
|
|
189
|
+
appear:
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
machines:
|
|
193
|
+
dut:
|
|
194
|
+
mcu: STM32H753ZI
|
|
195
|
+
elf: fc.elf
|
|
196
|
+
parts:
|
|
197
|
+
- { name: gpspeer, type: uart-device, baud: 230400, script: gps.py }
|
|
198
|
+
media:
|
|
199
|
+
- { type: uart, connect: [dut.usart6, dut.gpspeer] }
|
|
200
|
+
timeout: 12
|
|
201
|
+
expect: ["GPS fix: 3"]
|
|
202
|
+
expect_absent: ["RESULT: FAIL"]
|
|
203
|
+
expect_frames: ["SPI Rx cs=0 len=2 mosi=4F 06"] # same line shape as `sim --frames`
|
|
204
|
+
expect_frames_absent: ["CAN Dropped"]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Single-machine manifests (`mcu:` at the top level) are the one-machine case.
|
|
208
|
+
Models resolve through your account; no checkout of ours is needed. A model can
|
|
209
|
+
require a minimum engine version; if yours is older, the failure names the
|
|
210
|
+
version and the fix (`simantic install engine --force`).
|
|
211
|
+
|
|
212
|
+
Three more keys cover fixtures that need files or peers on the host side:
|
|
213
|
+
`sparse_files` creates blank files of a given size before the run (a blank SD
|
|
214
|
+
card, say), `networkServices` adds scripted network peers, and `{TEST_DIR}` and
|
|
215
|
+
`{WORK_DIR}` in a board file expand to the manifest's directory and a per-test
|
|
216
|
+
scratch directory. A manifest whose `sim_args` asks for a `sim`
|
|
217
|
+
command-line flag with no equivalent here is skipped, with the flag named.
|
|
218
|
+
|
|
219
|
+
A run also has a wall-clock budget: 30 s of host time by default, 100 s with
|
|
220
|
+
`wireless: true`, or whatever `wall:` says. Going over it fails the test with
|
|
221
|
+
the virtual time reached. Slow simulation is a firmware busy-wait or a model
|
|
222
|
+
gap, and the report tells you which to go find rather than waiting it out.
|
|
223
|
+
|
|
224
|
+
For a single run with no assertions in the middle, there is `run_firmware(...)`:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
run = simantic.run_firmware("build/zephyr.elf", mcu="STM32F401RE",
|
|
228
|
+
expect=["RESULT: PASS"])
|
|
229
|
+
assert run.passed, run.failure_report()
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Telemetry
|
|
233
|
+
|
|
234
|
+
Only when you are signed in, we report two things:
|
|
235
|
+
|
|
236
|
+
* **Test counts.** At the end of a pytest run, how many simulator tests passed,
|
|
237
|
+
failed and were skipped. One request per run.
|
|
238
|
+
* **Which calls you use.** The names of the SDK calls and `simantic` commands
|
|
239
|
+
you run, such as `sdk.run_firmware` or `cli.install`, and how often. They are
|
|
240
|
+
counted in `~/.simantic/usage.jsonl` and uploaded at most hourly.
|
|
241
|
+
|
|
242
|
+
Each report also carries the version of this package, your Python version,
|
|
243
|
+
operating system and CPU architecture. Reports go out when a pytest run or a
|
|
244
|
+
`simantic` command finishes, never while a simulation is running, and a failed
|
|
245
|
+
or slow request is dropped silently.
|
|
246
|
+
|
|
247
|
+
We do not send file paths, project names, test names, call arguments, firmware,
|
|
248
|
+
or simulation output. Those are yours. Turn it off whenever you like:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
export SIMANTIC_TELEMETRY=0 # or DO_NOT_TRACK=1
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Questions
|
|
255
|
+
|
|
256
|
+
We would genuinely like to hear how this goes for you, especially if something
|
|
257
|
+
is confusing or broken. Write to **founder@simantic.dev**, or open an issue.
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT. The simulators it drives are separate software under their own terms.
|