simantic 0.2.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.2.0/.github/workflows/ci.yml +93 -0
- simantic-0.2.0/.gitignore +6 -0
- simantic-0.2.0/LICENSE +21 -0
- simantic-0.2.0/PKG-INFO +219 -0
- simantic-0.2.0/PUBLISHING.md +98 -0
- simantic-0.2.0/README.md +197 -0
- simantic-0.2.0/docs/session-api.md +108 -0
- simantic-0.2.0/examples/parallel_sweep.py +24 -0
- simantic-0.2.0/examples/step_and_peek.py +12 -0
- simantic-0.2.0/pyproject.toml +45 -0
- simantic-0.2.0/src/simantic/__init__.py +85 -0
- simantic-0.2.0/src/simantic/__main__.py +10 -0
- simantic-0.2.0/src/simantic/_cli.py +139 -0
- simantic-0.2.0/src/simantic/_locate.py +76 -0
- simantic-0.2.0/src/simantic/agent.py +175 -0
- simantic-0.2.0/src/simantic/analog.py +94 -0
- simantic-0.2.0/src/simantic/auth.py +221 -0
- simantic-0.2.0/src/simantic/engine.py +91 -0
- simantic-0.2.0/src/simantic/fixtures.py +144 -0
- simantic-0.2.0/src/simantic/install.py +313 -0
- simantic-0.2.0/src/simantic/mcu.py +163 -0
- simantic-0.2.0/src/simantic/pyrite.py +72 -0
- simantic-0.2.0/src/simantic/pytest_plugin.py +232 -0
- simantic-0.2.0/src/simantic/report.py +194 -0
- simantic-0.2.0/src/simantic/session.py +409 -0
- simantic-0.2.0/src/simantic/telemetry.py +225 -0
- simantic-0.2.0/tests/conftest.py +6 -0
- simantic-0.2.0/tests/fixtures/divider/divider.sim.toml +29 -0
- simantic-0.2.0/tests/test_agent.py +108 -0
- simantic-0.2.0/tests/test_auth.py +246 -0
- simantic-0.2.0/tests/test_fixtures.py +141 -0
- simantic-0.2.0/tests/test_install.py +410 -0
- simantic-0.2.0/tests/test_packaging.py +50 -0
- simantic-0.2.0/tests/test_plan.py +52 -0
- simantic-0.2.0/tests/test_pyrite.py +81 -0
- simantic-0.2.0/tests/test_report.py +164 -0
- simantic-0.2.0/tests/test_session.py +56 -0
- simantic-0.2.0/tests/test_spool.py +148 -0
- simantic-0.2.0/tests/test_telemetry.py +111 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Tests every push and PR, and publishes to PyPI when a v* tag is pushed.
|
|
2
|
+
#
|
|
3
|
+
# Publishing uses PyPI Trusted Publishing (OIDC): the `pypi` environment
|
|
4
|
+
# exchanges this workflow's identity for a short-lived upload token, so there
|
|
5
|
+
# is no API token in repository secrets to leak or rotate. It must be
|
|
6
|
+
# configured once on PyPI before the first tagged release — see PUBLISHING.md.
|
|
7
|
+
#
|
|
8
|
+
# The SDK is pure Python and its tests need no simulator binary: the report
|
|
9
|
+
# fixtures are literal JSON in the shapes the schema allows, so CI stays
|
|
10
|
+
# independent of the engines this package drives.
|
|
11
|
+
|
|
12
|
+
name: ci
|
|
13
|
+
|
|
14
|
+
# DOCS ARE INERT. Prose-only commits compile nothing here. Safe only while main
|
|
15
|
+
# carries no required status checks — a path-filtered-away required check sits
|
|
16
|
+
# `Pending` forever and blocks the merge.
|
|
17
|
+
# (Actions does not support YAML anchors, hence the duplication.)
|
|
18
|
+
on:
|
|
19
|
+
push:
|
|
20
|
+
branches: [main]
|
|
21
|
+
tags: ["v*"]
|
|
22
|
+
paths-ignore:
|
|
23
|
+
- 'docs/**'
|
|
24
|
+
- '**.md'
|
|
25
|
+
- '.claude/**'
|
|
26
|
+
- 'LICENSE'
|
|
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
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
31
|
+
paths-ignore:
|
|
32
|
+
- 'docs/**'
|
|
33
|
+
- '**.md'
|
|
34
|
+
- '.claude/**'
|
|
35
|
+
- 'LICENSE'
|
|
36
|
+
|
|
37
|
+
concurrency:
|
|
38
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
39
|
+
cancel-in-progress: true
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
test:
|
|
43
|
+
runs-on: ${{ matrix.os }}
|
|
44
|
+
# The SDK is pure Python, so a macOS runner proves very little here that
|
|
45
|
+
# Linux does not — and it bills at $0.062/min against Linux's $0.006, with a
|
|
46
|
+
# 10x draw on the included-minutes allowance. Two of the six legs were
|
|
47
|
+
# macOS. It now joins on a push to main, on a v* tag (a release should be
|
|
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.
|
|
50
|
+
timeout-minutes: 15
|
|
51
|
+
strategy:
|
|
52
|
+
fail-fast: false
|
|
53
|
+
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
|
+
# 3.11 is the floor (tomllib landed there); 3.13 guards the top end.
|
|
58
|
+
python: ["3.11", "3.13"]
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
- uses: astral-sh/setup-uv@v5
|
|
62
|
+
- run: uv run --python ${{ matrix.python }} --with pytest --with . pytest tests/ -q
|
|
63
|
+
|
|
64
|
+
build:
|
|
65
|
+
needs: test
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
timeout-minutes: 10
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
- uses: astral-sh/setup-uv@v5
|
|
71
|
+
- run: uv build
|
|
72
|
+
# Catches the metadata problems PyPI rejects on upload — a malformed
|
|
73
|
+
# README, a bad classifier — while the tag can still be redone.
|
|
74
|
+
- run: uvx twine check dist/*
|
|
75
|
+
- uses: actions/upload-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
name: dist
|
|
78
|
+
path: dist/
|
|
79
|
+
|
|
80
|
+
publish:
|
|
81
|
+
needs: build
|
|
82
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
timeout-minutes: 10
|
|
85
|
+
environment: pypi
|
|
86
|
+
permissions:
|
|
87
|
+
id-token: write # required for trusted publishing; nothing else is
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/download-artifact@v4
|
|
90
|
+
with:
|
|
91
|
+
name: dist
|
|
92
|
+
path: dist/
|
|
93
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
simantic-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simantic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
simantic-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: simantic
|
|
3
|
+
Version: 0.2.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
|
+
# simantic
|
|
24
|
+
|
|
25
|
+
Python control of the [Simantic](https://simantic.dev) simulators — the
|
|
26
|
+
firmware engine hosted in your process, circuits via `analog-cli`. Everything
|
|
27
|
+
the CLIs can do, as objects and method calls: start a board or a multi-machine scenario, advance virtual
|
|
28
|
+
time by exact amounts, inject UART/GPIO/CAN/radio, read memory and RTOS state,
|
|
29
|
+
and run as many simulations in parallel as you have cores. pytest is one way
|
|
30
|
+
to use it, not a requirement.
|
|
31
|
+
|
|
32
|
+
> **Alpha — not stable.** Version 0.1.x. The API, the CLI surface, and the
|
|
33
|
+
> report schema may change without a deprecation period, and any release may
|
|
34
|
+
> break the previous one. Pin an exact version (`simantic==0.1.0`) if you
|
|
35
|
+
> depend on it. Not recommended for production pipelines yet.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install simantic
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That is the whole setup for Python. The first `Sim(...)` fetches the
|
|
42
|
+
simulation engine (Simantic.Core plus a private .NET runtime — nothing else
|
|
43
|
+
to install) into `~/.simantic/engine/<version>/`, checksum-verified against
|
|
44
|
+
the public release manifest. `simantic install` fetches it up front, along
|
|
45
|
+
with the `sim` and `analog-cli` binaries if you also want the command-line
|
|
46
|
+
tools.
|
|
47
|
+
|
|
48
|
+
A Simantic account (`simantic auth`) is needed for one thing: resolving MCU
|
|
49
|
+
models by name (`mcu="STM32F401RE"`), which are fetched from your account
|
|
50
|
+
and cached in `~/.sim_cache`. A platform file you supply (`repl=`) needs no
|
|
51
|
+
account at all.
|
|
52
|
+
|
|
53
|
+
`simantic auth` opens a browser tab to sign in — like `gh auth login` — and
|
|
54
|
+
stores the resulting token in `~/.sim_id`, the same file the CLIs use, so one
|
|
55
|
+
login covers all of them. In a script or CI, pass `--token` or pipe one in
|
|
56
|
+
(`echo $TOKEN | simantic auth`) instead of opening a browser. Create a token
|
|
57
|
+
on the dashboard's `/account/api` page. `--no-browser` falls back to an
|
|
58
|
+
interactive prompt for a pasted token.
|
|
59
|
+
|
|
60
|
+
Every download — engine or binary — is verified against the checksum in the
|
|
61
|
+
release manifest. The package on PyPI contains only Python; the simulators
|
|
62
|
+
are never in the wheel.
|
|
63
|
+
|
|
64
|
+
Already have the binaries? Point `$SIMANTIC_ANALOG_CLI` and `$SIMANTIC_SIM`
|
|
65
|
+
at them, or put them on PATH — both take precedence over a managed install.
|
|
66
|
+
`simantic status` shows what is authenticated and which binary each name
|
|
67
|
+
resolves to.
|
|
68
|
+
|
|
69
|
+
## Drive a simulation
|
|
70
|
+
|
|
71
|
+
A `Sim` is a live simulation you control. Time advances only when you ask, so
|
|
72
|
+
a script is deterministic and your think-time is free:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from simantic import Sim
|
|
76
|
+
|
|
77
|
+
with Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") as sim:
|
|
78
|
+
sim.expect("ready")
|
|
79
|
+
sim.inject_gpio("gpioc", 13, True) # press the user button
|
|
80
|
+
m = sim.expect("button pressed")
|
|
81
|
+
assert m.virtual_seconds < 0.010 # within 10 virtual ms
|
|
82
|
+
assert sim.read_u32("press_count") == 1
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The same class runs multi-machine scenarios with scripted peers
|
|
86
|
+
(`Sim(scenario={...})`). The engine lives in your process (one emulation per
|
|
87
|
+
process), so a parameter sweep is a `ProcessPoolExecutor` over plain
|
|
88
|
+
functions. See
|
|
89
|
+
[docs/session-api.md](docs/session-api.md) and `examples/`.
|
|
90
|
+
|
|
91
|
+
One-shot runs ("run 5 s, give me the transcript") are `run_firmware(...)`.
|
|
92
|
+
|
|
93
|
+
## Using it from pytest (optional)
|
|
94
|
+
|
|
95
|
+
`Sim` needs no plugin — construct it inside any test. If you also keep
|
|
96
|
+
manifests, installing the package registers two collectors that turn them
|
|
97
|
+
into individually addressable pytest items:
|
|
98
|
+
|
|
99
|
+
- `*.sim.toml` — one item per `[[test]]` table (analog)
|
|
100
|
+
- `test.yaml` — one item per fixture (firmware)
|
|
101
|
+
|
|
102
|
+
```console
|
|
103
|
+
$ pytest hardware/ firmware/
|
|
104
|
+
hardware/psu/psu.sim.toml::schematic-erc PASSED
|
|
105
|
+
hardware/psu/psu.sim.toml::rails-op PASSED
|
|
106
|
+
hardware/psu/psu.sim.toml::startup-settling FAILED
|
|
107
|
+
hardware/psu/psu.sim.toml::board-drc SKIPPED (no .kicad_pcb)
|
|
108
|
+
firmware/tests/gpio-loopback/test.yaml::gpio-loopback PASSED
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Because these are ordinary pytest items you get `-k` filtering, `--junitxml`
|
|
112
|
+
for CI, xdist parallelism, and per-test durations. Failures print the
|
|
113
|
+
runner's own explanation rather than a Python traceback:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
startup-settling (tran): fail
|
|
117
|
+
FAIL settle-time: V(OUT) measured 0.0082 (expected max 0.006, margin -0.0022)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Tests that cannot run in the current environment skip rather than fail — a
|
|
121
|
+
missing binary, an unconfigured server, an analysis the installed CLI does
|
|
122
|
+
not support, a check inapplicable to the project. A red run means a
|
|
123
|
+
simulation ran and disagreed with its expectations.
|
|
124
|
+
|
|
125
|
+
## Library
|
|
126
|
+
|
|
127
|
+
### Circuits
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import simantic
|
|
131
|
+
|
|
132
|
+
report = simantic.run_tests("hardware/psu")
|
|
133
|
+
print(f"{report.summary.passed}/{report.summary.total} passed")
|
|
134
|
+
|
|
135
|
+
for m in report.test("rails-op").measurements:
|
|
136
|
+
print(m.describe()) # out-dc: V(OUT) measured 1.597 (expected eq 1.597 +/- 0.02, margin 0.02)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A failing test is data, not an exception: it arrives in the report with its
|
|
140
|
+
measured value, declared bounds, and margin. Only conditions that prevent a
|
|
141
|
+
run at all — bad project, missing `kicad-cli`, invalid testplan — raise
|
|
142
|
+
`AnalogCliError`.
|
|
143
|
+
|
|
144
|
+
### Firmware
|
|
145
|
+
|
|
146
|
+
The shortest path is a pytest fixture — no manifest, no flags:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
def test_firmware_boots(pyrite):
|
|
150
|
+
run = pyrite("build/zephyr.elf", board="stm32f401",
|
|
151
|
+
expect=["Hello World!"], expect_absent=["FAULT"])
|
|
152
|
+
assert run.passed, run.failure_report()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`pyrite` runs the ELF offline on the pure-Rust backend and hands back the
|
|
156
|
+
UART transcript. The fixture skips when no binary is installed, so a suite
|
|
157
|
+
stays green on a machine that has not run `smtc install pyrite`.
|
|
158
|
+
|
|
159
|
+
The same runner is available as a plain function, and `sim` has its own:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
run = simantic.run_firmware(
|
|
163
|
+
"build/zephyr.elf",
|
|
164
|
+
mcu="STM32F401RE", # resolved by the backend through your account
|
|
165
|
+
expect=["RESULT: PASS"],
|
|
166
|
+
expect_absent=["RESULT: FAIL"],
|
|
167
|
+
)
|
|
168
|
+
assert run.passed, run.failure_report()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`sim` emits no structured report — the only observable is UART text — so the
|
|
172
|
+
verdict is substring matching, the same contract `test.yaml` manifests use.
|
|
173
|
+
Pass `repl=` instead of `mcu=` for a platform file you author yourself.
|
|
174
|
+
|
|
175
|
+
MCU models are not distributed with this package: `mcu=` resolves them
|
|
176
|
+
through your account. If you have a local model library, set
|
|
177
|
+
`$SIMANTIC_MCU_LIB` to resolve from it instead — which is also what applying
|
|
178
|
+
a fixture's `overlay` fragment requires.
|
|
179
|
+
|
|
180
|
+
Some installations need a separate simulation server. When one does, the SDK
|
|
181
|
+
raises `ServerNotConfigured` and the pytest plugin skips, rather than
|
|
182
|
+
reporting a firmware failure.
|
|
183
|
+
|
|
184
|
+
## Telemetry
|
|
185
|
+
|
|
186
|
+
When you are authenticated, a completed pytest session reports its **shape**
|
|
187
|
+
to your account: how many simulator tests ran, how many passed, failed, or
|
|
188
|
+
skipped, plus this package's version, your Python version, OS, and CPU
|
|
189
|
+
architecture. One request per `pytest` invocation, never per test.
|
|
190
|
+
|
|
191
|
+
It also counts **which calls you make** — SDK functions, MCP tool names, and
|
|
192
|
+
`smtc` subcommands, by name only. These are buffered in
|
|
193
|
+
`~/.simantic/usage.jsonl` and uploaded as counts at most once an hour, so no
|
|
194
|
+
simulation ever waits on the network. You can read that file at any time; it
|
|
195
|
+
is one JSON object per line and contains nothing but call names.
|
|
196
|
+
|
|
197
|
+
It does **not** send file paths, project names, test names, firmware, or
|
|
198
|
+
simulation output. Those are yours.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
export SIMANTIC_TELEMETRY=0 # or DO_NOT_TRACK=1
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`smtc status` prints exactly what is sent and whether it is on. Reporting is
|
|
205
|
+
best-effort: if it fails, is blocked, or you are offline, your tests are
|
|
206
|
+
unaffected and nothing is printed.
|
|
207
|
+
|
|
208
|
+
## Compatibility
|
|
209
|
+
|
|
210
|
+
Speaks the `analog-cli.test-report/1` schema. Additive fields within that
|
|
211
|
+
revision are tolerated; a breaking revision raises `ReportError` rather than
|
|
212
|
+
silently misreading a report.
|
|
213
|
+
|
|
214
|
+
Multi-machine `test.yaml` fixtures — those with a `machines:` map — need the
|
|
215
|
+
`--scenario` runner and are not driven yet; they report as skips.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT. The simulators it drives are separate software under their own terms.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Publishing `simantic` to PyPI
|
|
2
|
+
|
|
3
|
+
First-time setup, then the per-release loop. Steps marked **[you]** need a
|
|
4
|
+
human with the accounts; everything else is automated in
|
|
5
|
+
`.github/workflows/python.yml`.
|
|
6
|
+
|
|
7
|
+
## One-time setup
|
|
8
|
+
|
|
9
|
+
### 1. Accounts **[you]**
|
|
10
|
+
|
|
11
|
+
Create accounts on both indexes, with 2FA (PyPI requires it for publishing):
|
|
12
|
+
|
|
13
|
+
- <https://test.pypi.org/account/register/> — the rehearsal index
|
|
14
|
+
- <https://pypi.org/account/register/> — the real one
|
|
15
|
+
|
|
16
|
+
TestPyPI is a genuinely separate site with separate credentials. Use it for
|
|
17
|
+
the first upload so a mistake is not permanent.
|
|
18
|
+
|
|
19
|
+
### 2. Claim the name **[you]**
|
|
20
|
+
|
|
21
|
+
`simantic` was unregistered as of this writing. Names are first-come and a
|
|
22
|
+
published version number can never be reused or overwritten — only yanked —
|
|
23
|
+
so publish `0.1.0` to TestPyPI first, confirm it looks right, and only then
|
|
24
|
+
push the real tag. Claiming early is cheap insurance against someone else
|
|
25
|
+
taking it.
|
|
26
|
+
|
|
27
|
+
### 3. Configure Trusted Publishing **[you]**
|
|
28
|
+
|
|
29
|
+
This replaces API tokens with short-lived OIDC credentials, so there is no
|
|
30
|
+
long-lived secret in the repo. On PyPI, go to your account's **Publishing**
|
|
31
|
+
page and add a *pending* publisher (pending = the project does not exist
|
|
32
|
+
yet, which is the case before the first upload):
|
|
33
|
+
|
|
34
|
+
| Field | Value |
|
|
35
|
+
|---|---|
|
|
36
|
+
| PyPI project name | `simantic` |
|
|
37
|
+
| Owner | `simantic-dev` |
|
|
38
|
+
| Repository name | `pippy` (the GitHub repo name, not the package name) |
|
|
39
|
+
| Workflow name | `ci.yml` |
|
|
40
|
+
| Environment name | `pypi` |
|
|
41
|
+
|
|
42
|
+
Then in GitHub: **Settings → Environments → New environment → `pypi`**. Add
|
|
43
|
+
required reviewers there if you want a human approval gate before any upload.
|
|
44
|
+
|
|
45
|
+
Repeat the whole step on TestPyPI if you want the rehearsal automated;
|
|
46
|
+
otherwise do the rehearsal upload by hand (below).
|
|
47
|
+
|
|
48
|
+
## Rehearsal upload **[you]**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv build
|
|
52
|
+
uvx twine check dist/* # metadata PyPI would reject
|
|
53
|
+
uvx twine upload --repository testpypi dist/*
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then confirm a clean machine can install and import it:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv run --with simantic --index https://test.pypi.org/simple/ \
|
|
60
|
+
--index-strategy unsafe-best-match \
|
|
61
|
+
python -c "import simantic; print(simantic.__version__)"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Releasing
|
|
65
|
+
|
|
66
|
+
1. Bump `version` in `pyproject.toml` **and** `__version__` in
|
|
67
|
+
`src/simantic/__init__.py`. They are asserted equal by the test suite.
|
|
68
|
+
2. Merge to `main`.
|
|
69
|
+
3. Tag and push:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git tag v0.1.0
|
|
73
|
+
git push origin v0.1.0
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The workflow tests on Linux/macOS/Windows across 3.11 and 3.13, builds the
|
|
77
|
+
sdist and wheel, runs `twine check`, and uploads via trusted publishing.
|
|
78
|
+
|
|
79
|
+
## Versioning
|
|
80
|
+
|
|
81
|
+
Semantic versioning on the SDK's own surface, which is independent of the
|
|
82
|
+
`analog-cli` version it drives. The coupling that matters is the report
|
|
83
|
+
schema: `simantic` speaks `analog-cli.test-report/1` and refuses anything
|
|
84
|
+
else, so a schema revision in the CLI is a major bump here.
|
|
85
|
+
|
|
86
|
+
Stay on `0.x` until the API has survived real use. Pre-1.0 signals that
|
|
87
|
+
breaking changes can still happen, which is honest for a first release.
|
|
88
|
+
|
|
89
|
+
## Notes for later
|
|
90
|
+
|
|
91
|
+
- **Binary wheels.** The SDK deliberately does not bundle a simulator; it
|
|
92
|
+
resolves each binary from its `$SIMANTIC_*` variable, then a `_bin/`
|
|
93
|
+
directory inside the package, then PATH. A separate platform-specific wheel
|
|
94
|
+
can therefore drop a binary into `_bin/` and be found with no SDK change,
|
|
95
|
+
which is the seam to use if engines are ever distributed through PyPI.
|
|
96
|
+
- **Closed-source binaries are fine on PyPI.** Wheels need not contain
|
|
97
|
+
source, and the index has no open-source requirement. Keeping this SDK MIT
|
|
98
|
+
while the engines stay proprietary is a normal arrangement.
|
simantic-0.2.0/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# simantic
|
|
2
|
+
|
|
3
|
+
Python control of the [Simantic](https://simantic.dev) simulators — the
|
|
4
|
+
firmware engine hosted in your process, circuits via `analog-cli`. Everything
|
|
5
|
+
the CLIs can do, as objects and method calls: start a board or a multi-machine scenario, advance virtual
|
|
6
|
+
time by exact amounts, inject UART/GPIO/CAN/radio, read memory and RTOS state,
|
|
7
|
+
and run as many simulations in parallel as you have cores. pytest is one way
|
|
8
|
+
to use it, not a requirement.
|
|
9
|
+
|
|
10
|
+
> **Alpha — not stable.** Version 0.1.x. The API, the CLI surface, and the
|
|
11
|
+
> report schema may change without a deprecation period, and any release may
|
|
12
|
+
> break the previous one. Pin an exact version (`simantic==0.1.0`) if you
|
|
13
|
+
> depend on it. Not recommended for production pipelines yet.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install simantic
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That is the whole setup for Python. The first `Sim(...)` fetches the
|
|
20
|
+
simulation engine (Simantic.Core plus a private .NET runtime — nothing else
|
|
21
|
+
to install) into `~/.simantic/engine/<version>/`, checksum-verified against
|
|
22
|
+
the public release manifest. `simantic install` fetches it up front, along
|
|
23
|
+
with the `sim` and `analog-cli` binaries if you also want the command-line
|
|
24
|
+
tools.
|
|
25
|
+
|
|
26
|
+
A Simantic account (`simantic auth`) is needed for one thing: resolving MCU
|
|
27
|
+
models by name (`mcu="STM32F401RE"`), which are fetched from your account
|
|
28
|
+
and cached in `~/.sim_cache`. A platform file you supply (`repl=`) needs no
|
|
29
|
+
account at all.
|
|
30
|
+
|
|
31
|
+
`simantic auth` opens a browser tab to sign in — like `gh auth login` — and
|
|
32
|
+
stores the resulting token in `~/.sim_id`, the same file the CLIs use, so one
|
|
33
|
+
login covers all of them. In a script or CI, pass `--token` or pipe one in
|
|
34
|
+
(`echo $TOKEN | simantic auth`) instead of opening a browser. Create a token
|
|
35
|
+
on the dashboard's `/account/api` page. `--no-browser` falls back to an
|
|
36
|
+
interactive prompt for a pasted token.
|
|
37
|
+
|
|
38
|
+
Every download — engine or binary — is verified against the checksum in the
|
|
39
|
+
release manifest. The package on PyPI contains only Python; the simulators
|
|
40
|
+
are never in the wheel.
|
|
41
|
+
|
|
42
|
+
Already have the binaries? Point `$SIMANTIC_ANALOG_CLI` and `$SIMANTIC_SIM`
|
|
43
|
+
at them, or put them on PATH — both take precedence over a managed install.
|
|
44
|
+
`simantic status` shows what is authenticated and which binary each name
|
|
45
|
+
resolves to.
|
|
46
|
+
|
|
47
|
+
## Drive a simulation
|
|
48
|
+
|
|
49
|
+
A `Sim` is a live simulation you control. Time advances only when you ask, so
|
|
50
|
+
a script is deterministic and your think-time is free:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from simantic import Sim
|
|
54
|
+
|
|
55
|
+
with Sim(elf="fw.elf", mcu="STM32F401RE", uart="usart2") as sim:
|
|
56
|
+
sim.expect("ready")
|
|
57
|
+
sim.inject_gpio("gpioc", 13, True) # press the user button
|
|
58
|
+
m = sim.expect("button pressed")
|
|
59
|
+
assert m.virtual_seconds < 0.010 # within 10 virtual ms
|
|
60
|
+
assert sim.read_u32("press_count") == 1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The same class runs multi-machine scenarios with scripted peers
|
|
64
|
+
(`Sim(scenario={...})`). The engine lives in your process (one emulation per
|
|
65
|
+
process), so a parameter sweep is a `ProcessPoolExecutor` over plain
|
|
66
|
+
functions. See
|
|
67
|
+
[docs/session-api.md](docs/session-api.md) and `examples/`.
|
|
68
|
+
|
|
69
|
+
One-shot runs ("run 5 s, give me the transcript") are `run_firmware(...)`.
|
|
70
|
+
|
|
71
|
+
## Using it from pytest (optional)
|
|
72
|
+
|
|
73
|
+
`Sim` needs no plugin — construct it inside any test. If you also keep
|
|
74
|
+
manifests, installing the package registers two collectors that turn them
|
|
75
|
+
into individually addressable pytest items:
|
|
76
|
+
|
|
77
|
+
- `*.sim.toml` — one item per `[[test]]` table (analog)
|
|
78
|
+
- `test.yaml` — one item per fixture (firmware)
|
|
79
|
+
|
|
80
|
+
```console
|
|
81
|
+
$ pytest hardware/ firmware/
|
|
82
|
+
hardware/psu/psu.sim.toml::schematic-erc PASSED
|
|
83
|
+
hardware/psu/psu.sim.toml::rails-op PASSED
|
|
84
|
+
hardware/psu/psu.sim.toml::startup-settling FAILED
|
|
85
|
+
hardware/psu/psu.sim.toml::board-drc SKIPPED (no .kicad_pcb)
|
|
86
|
+
firmware/tests/gpio-loopback/test.yaml::gpio-loopback PASSED
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Because these are ordinary pytest items you get `-k` filtering, `--junitxml`
|
|
90
|
+
for CI, xdist parallelism, and per-test durations. Failures print the
|
|
91
|
+
runner's own explanation rather than a Python traceback:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
startup-settling (tran): fail
|
|
95
|
+
FAIL settle-time: V(OUT) measured 0.0082 (expected max 0.006, margin -0.0022)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Tests that cannot run in the current environment skip rather than fail — a
|
|
99
|
+
missing binary, an unconfigured server, an analysis the installed CLI does
|
|
100
|
+
not support, a check inapplicable to the project. A red run means a
|
|
101
|
+
simulation ran and disagreed with its expectations.
|
|
102
|
+
|
|
103
|
+
## Library
|
|
104
|
+
|
|
105
|
+
### Circuits
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import simantic
|
|
109
|
+
|
|
110
|
+
report = simantic.run_tests("hardware/psu")
|
|
111
|
+
print(f"{report.summary.passed}/{report.summary.total} passed")
|
|
112
|
+
|
|
113
|
+
for m in report.test("rails-op").measurements:
|
|
114
|
+
print(m.describe()) # out-dc: V(OUT) measured 1.597 (expected eq 1.597 +/- 0.02, margin 0.02)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A failing test is data, not an exception: it arrives in the report with its
|
|
118
|
+
measured value, declared bounds, and margin. Only conditions that prevent a
|
|
119
|
+
run at all — bad project, missing `kicad-cli`, invalid testplan — raise
|
|
120
|
+
`AnalogCliError`.
|
|
121
|
+
|
|
122
|
+
### Firmware
|
|
123
|
+
|
|
124
|
+
The shortest path is a pytest fixture — no manifest, no flags:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
def test_firmware_boots(pyrite):
|
|
128
|
+
run = pyrite("build/zephyr.elf", board="stm32f401",
|
|
129
|
+
expect=["Hello World!"], expect_absent=["FAULT"])
|
|
130
|
+
assert run.passed, run.failure_report()
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`pyrite` runs the ELF offline on the pure-Rust backend and hands back the
|
|
134
|
+
UART transcript. The fixture skips when no binary is installed, so a suite
|
|
135
|
+
stays green on a machine that has not run `smtc install pyrite`.
|
|
136
|
+
|
|
137
|
+
The same runner is available as a plain function, and `sim` has its own:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
run = simantic.run_firmware(
|
|
141
|
+
"build/zephyr.elf",
|
|
142
|
+
mcu="STM32F401RE", # resolved by the backend through your account
|
|
143
|
+
expect=["RESULT: PASS"],
|
|
144
|
+
expect_absent=["RESULT: FAIL"],
|
|
145
|
+
)
|
|
146
|
+
assert run.passed, run.failure_report()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`sim` emits no structured report — the only observable is UART text — so the
|
|
150
|
+
verdict is substring matching, the same contract `test.yaml` manifests use.
|
|
151
|
+
Pass `repl=` instead of `mcu=` for a platform file you author yourself.
|
|
152
|
+
|
|
153
|
+
MCU models are not distributed with this package: `mcu=` resolves them
|
|
154
|
+
through your account. If you have a local model library, set
|
|
155
|
+
`$SIMANTIC_MCU_LIB` to resolve from it instead — which is also what applying
|
|
156
|
+
a fixture's `overlay` fragment requires.
|
|
157
|
+
|
|
158
|
+
Some installations need a separate simulation server. When one does, the SDK
|
|
159
|
+
raises `ServerNotConfigured` and the pytest plugin skips, rather than
|
|
160
|
+
reporting a firmware failure.
|
|
161
|
+
|
|
162
|
+
## Telemetry
|
|
163
|
+
|
|
164
|
+
When you are authenticated, a completed pytest session reports its **shape**
|
|
165
|
+
to your account: how many simulator tests ran, how many passed, failed, or
|
|
166
|
+
skipped, plus this package's version, your Python version, OS, and CPU
|
|
167
|
+
architecture. One request per `pytest` invocation, never per test.
|
|
168
|
+
|
|
169
|
+
It also counts **which calls you make** — SDK functions, MCP tool names, and
|
|
170
|
+
`smtc` subcommands, by name only. These are buffered in
|
|
171
|
+
`~/.simantic/usage.jsonl` and uploaded as counts at most once an hour, so no
|
|
172
|
+
simulation ever waits on the network. You can read that file at any time; it
|
|
173
|
+
is one JSON object per line and contains nothing but call names.
|
|
174
|
+
|
|
175
|
+
It does **not** send file paths, project names, test names, firmware, or
|
|
176
|
+
simulation output. Those are yours.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
export SIMANTIC_TELEMETRY=0 # or DO_NOT_TRACK=1
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`smtc status` prints exactly what is sent and whether it is on. Reporting is
|
|
183
|
+
best-effort: if it fails, is blocked, or you are offline, your tests are
|
|
184
|
+
unaffected and nothing is printed.
|
|
185
|
+
|
|
186
|
+
## Compatibility
|
|
187
|
+
|
|
188
|
+
Speaks the `analog-cli.test-report/1` schema. Additive fields within that
|
|
189
|
+
revision are tolerated; a breaking revision raises `ReportError` rather than
|
|
190
|
+
silently misreading a report.
|
|
191
|
+
|
|
192
|
+
Multi-machine `test.yaml` fixtures — those with a `machines:` map — need the
|
|
193
|
+
`--scenario` runner and are not driven yet; they report as skips.
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT. The simulators it drives are separate software under their own terms.
|