meshbench 0.0.5__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.
- meshbench-0.0.5/.gitignore +61 -0
- meshbench-0.0.5/PKG-INFO +124 -0
- meshbench-0.0.5/README.md +104 -0
- meshbench-0.0.5/examples/01_blank_setup_with_a_board.py +79 -0
- meshbench-0.0.5/examples/02_two_nodes_on_a_local_build.py +116 -0
- meshbench-0.0.5/examples/03_small_mesh_with_traffic.py +62 -0
- meshbench-0.0.5/examples/04_headless_regression.py +57 -0
- meshbench-0.0.5/examples/05_two_builds_in_one_scenario.py +61 -0
- meshbench-0.0.5/examples/06_live_import_and_advert.py +61 -0
- meshbench-0.0.5/examples/07_replace_a_board_build.py +63 -0
- meshbench-0.0.5/meshbench/__init__.py +157 -0
- meshbench-0.0.5/meshbench/_socket.py +247 -0
- meshbench-0.0.5/meshbench/boundary.py +124 -0
- meshbench-0.0.5/meshbench/checks.py +225 -0
- meshbench-0.0.5/meshbench/device.py +99 -0
- meshbench-0.0.5/meshbench/errors.py +153 -0
- meshbench-0.0.5/meshbench/live.py +122 -0
- meshbench-0.0.5/meshbench/nodes.py +481 -0
- meshbench-0.0.5/meshbench/pairing.py +70 -0
- meshbench-0.0.5/meshbench/parts.py +669 -0
- meshbench-0.0.5/meshbench/pytest_plugin.py +118 -0
- meshbench-0.0.5/meshbench/sessions.py +159 -0
- meshbench-0.0.5/meshbench/sets.py +339 -0
- meshbench-0.0.5/meshbench/subscribe.py +90 -0
- meshbench-0.0.5/meshbench/types.py +508 -0
- meshbench-0.0.5/meshbench/wait.py +110 -0
- meshbench-0.0.5/meshbench/workbench.py +560 -0
- meshbench-0.0.5/pyproject.toml +73 -0
- meshbench-0.0.5/tests/test_client.py +1078 -0
- meshbench-0.0.5/tests/test_pytest_plugin.py +87 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/meshbench
|
|
2
|
+
/dist/
|
|
3
|
+
*.exe
|
|
4
|
+
|
|
5
|
+
# Build caches for MeshCore firmware refs (ADR-0009) — reproducible, large
|
|
6
|
+
/firmware-cache/
|
|
7
|
+
|
|
8
|
+
# DEM tiles; large and reproducible from the tile server
|
|
9
|
+
/dem-cache/
|
|
10
|
+
|
|
11
|
+
# Captures and IQ recordings — can be enormous (1 MB/s per receiver)
|
|
12
|
+
/captures/
|
|
13
|
+
*.pcapng
|
|
14
|
+
*.sigmf-data
|
|
15
|
+
*.cf32
|
|
16
|
+
|
|
17
|
+
# Python build artifacts, from the client under pkg/client-python and from
|
|
18
|
+
# anything that imports it. Reproducible from the source beside them, and here
|
|
19
|
+
# rather than beside the client because a test run leaves them in the working
|
|
20
|
+
# tree and the next `git add -A` on any branch picks them up - which is exactly
|
|
21
|
+
# how eleven of them got committed once.
|
|
22
|
+
__pycache__/
|
|
23
|
+
*.py[cod]
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.ruff_cache/
|
|
27
|
+
/pkg/client-python/build/
|
|
28
|
+
/pkg/client-python/dist/
|
|
29
|
+
/.venv/
|
|
30
|
+
/venv/
|
|
31
|
+
|
|
32
|
+
/.idea/
|
|
33
|
+
/.vscode/
|
|
34
|
+
*.swp
|
|
35
|
+
# Local Claude Code state, but not the skills: those describe how to drive this
|
|
36
|
+
# project and are worth sharing. Anything personal or machine-specific stays
|
|
37
|
+
# out.
|
|
38
|
+
.claude/*
|
|
39
|
+
!.claude/skills/
|
|
40
|
+
.claude/settings.local.json
|
|
41
|
+
tools/armfw/build/
|
|
42
|
+
|
|
43
|
+
# Tool binaries built at the repo root by `go build ./tools/...`. They shadow
|
|
44
|
+
# the packages they are built from, so tab-completing the name gets the binary.
|
|
45
|
+
# Every `package main` under tools/ produces one, named after its directory; the
|
|
46
|
+
# list is the whole set, because a missing one sits untracked in `git status`
|
|
47
|
+
# until a `git add -A` sweeps it in - which is how eleven Python artifacts got
|
|
48
|
+
# committed once, and how these four kept reappearing.
|
|
49
|
+
/clientgen
|
|
50
|
+
/envgen
|
|
51
|
+
/goldencap
|
|
52
|
+
/licgen
|
|
53
|
+
/mockup
|
|
54
|
+
/render
|
|
55
|
+
|
|
56
|
+
# built test binaries
|
|
57
|
+
*.test
|
|
58
|
+
|
|
59
|
+
# Written by `go test -coverprofile` for a Sonar scan; regenerated on demand.
|
|
60
|
+
coverage.out
|
|
61
|
+
.carto-key
|
meshbench-0.0.5/PKG-INFO
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: meshbench
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: Drive a MeshBench workbench: RF-accurate MeshCore simulation, from Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/MeshBench/meshbench
|
|
6
|
+
Project-URL: Issues, https://github.com/MeshBench/meshbench/issues
|
|
7
|
+
Author: MeshBench
|
|
8
|
+
License-Expression: GPL-3.0-or-later
|
|
9
|
+
Keywords: lora,mesh,meshcore,rf,simulation
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# meshbench
|
|
22
|
+
|
|
23
|
+
Drive a [MeshBench](https://github.com/MeshBench/meshbench) workbench from
|
|
24
|
+
Python: real MeshCore firmware over a sample-accurate LoRa channel, scripted.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from datetime import timedelta
|
|
28
|
+
|
|
29
|
+
from meshbench import Workbench
|
|
30
|
+
|
|
31
|
+
with Workbench.headless(fixture="fife-strict", seed=9001) as wb:
|
|
32
|
+
wb.sim.run(timedelta(minutes=5))
|
|
33
|
+
print(wb.provenance())
|
|
34
|
+
print(wb.events.total(), "events")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`headless` needs no display, no GPU and no toolkit. `Workbench.attach()`
|
|
38
|
+
connects to a workbench somebody is already looking at and never closes it.
|
|
39
|
+
|
|
40
|
+
Where more than one is running, `meshbench.sessions()` says which: the address
|
|
41
|
+
to connect on, the process, when it started, and what it has open. Pass a row
|
|
42
|
+
straight to `attach()`. A session that was killed rather than closed is not
|
|
43
|
+
listed, because the check is a dial of the address and not a look at a socket
|
|
44
|
+
file or a pid.
|
|
45
|
+
|
|
46
|
+
## Installing
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
pip install meshbench
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
You also need the `meshbench` binary on `PATH` — the package drives it, it
|
|
53
|
+
does not contain it.
|
|
54
|
+
|
|
55
|
+
## What it looks like
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
wb.project.new(place="Fife")
|
|
59
|
+
wb.nodes.place_many(
|
|
60
|
+
[
|
|
61
|
+
{"name": "R1", "kind": Kind.SIMPLE_REPEATER, "lat": 56.20, "lon": -3.20},
|
|
62
|
+
{"name": "C1", "kind": Kind.COMPANION, "lat": 56.19, "lon": -3.17},
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
wb.sim.start()
|
|
66
|
+
wb.firmware.wait_started() # a sensible default, or pass a timedelta
|
|
67
|
+
|
|
68
|
+
node = wb.nodes["C1"]
|
|
69
|
+
node.firmware = wb.firmware.find("companion-v1.17.0")
|
|
70
|
+
print(node.console.ask("get region"))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`wb.call(verb, params)` is the whole API underneath, and stays public: anything
|
|
74
|
+
this package has not shaped is one line away rather than a blocker.
|
|
75
|
+
|
|
76
|
+
## Testing firmware with it
|
|
77
|
+
|
|
78
|
+
The package registers a pytest plugin, so there is no `conftest.py` to copy:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
def test_the_flood_reaches_glenrothes(meshbench):
|
|
82
|
+
meshbench.project.open("fixtures/fixture-fife-strict.json")
|
|
83
|
+
meshbench.sim.run(timedelta(minutes=5))
|
|
84
|
+
assert meshbench.events.total() > 0
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
One workbench for the whole run — starting firmware on a real mesh is minutes,
|
|
88
|
+
not milliseconds — with the scenario cleared between tests so reuse does not
|
|
89
|
+
leak. `--meshbench-socket` attaches to one you are already running instead.
|
|
90
|
+
|
|
91
|
+
**A failing test prints the provenance**, whether or not it asked. Somebody
|
|
92
|
+
reading a failed assertion about a mesh is deciding whether their firmware
|
|
93
|
+
change broke something, and they need to know what the run assumed first.
|
|
94
|
+
|
|
95
|
+
## Two things that will bite otherwise
|
|
96
|
+
|
|
97
|
+
**Simulated time is not your time.** Every duration here is a
|
|
98
|
+
`datetime.timedelta` - Python already has a duration type, so this package does
|
|
99
|
+
not invent one - but two different clocks are measured in them:
|
|
100
|
+
|
|
101
|
+
- **the mesh's**, for `sim.run(...)`, `schedule.add(at=, every=)` and
|
|
102
|
+
`sim.wait_until(at=)`
|
|
103
|
+
- **yours**, for every `timeout` and for `sim.run(wait=)`
|
|
104
|
+
|
|
105
|
+
`sim.run(timedelta(minutes=5), wait=timedelta(minutes=60))` is five minutes of
|
|
106
|
+
the mesh's clock, and up to an hour of yours waiting for it. On 155 emulated
|
|
107
|
+
nodes that gap is the normal case, which is why they are separate arguments.
|
|
108
|
+
Every wait has a defensible default, so `wait_started()` on its own is fine.
|
|
109
|
+
|
|
110
|
+
**A node answers on its next loop.** Its loop only runs when the engine steps,
|
|
111
|
+
so reading a console straight after writing to it reads the moment *before* the
|
|
112
|
+
command was sent. Use `console.ask()`, which gives the mesh its own time first.
|
|
113
|
+
|
|
114
|
+
## Honesty
|
|
115
|
+
|
|
116
|
+
Every result comes out of a simulator that is **kinder than the air**: no
|
|
117
|
+
multipath, no body loss, no oscillator error. The measured biases are nearly
|
|
118
|
+
all in one direction, which is what makes a result usable — treat it as a best
|
|
119
|
+
case. `wb.provenance()` says what a given run assumed, and it is meant to be
|
|
120
|
+
printed above any number you publish.
|
|
121
|
+
|
|
122
|
+
## Licence
|
|
123
|
+
|
|
124
|
+
GPL-3.0-or-later, with the rest of MeshBench.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# meshbench
|
|
2
|
+
|
|
3
|
+
Drive a [MeshBench](https://github.com/MeshBench/meshbench) workbench from
|
|
4
|
+
Python: real MeshCore firmware over a sample-accurate LoRa channel, scripted.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
|
|
9
|
+
from meshbench import Workbench
|
|
10
|
+
|
|
11
|
+
with Workbench.headless(fixture="fife-strict", seed=9001) as wb:
|
|
12
|
+
wb.sim.run(timedelta(minutes=5))
|
|
13
|
+
print(wb.provenance())
|
|
14
|
+
print(wb.events.total(), "events")
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`headless` needs no display, no GPU and no toolkit. `Workbench.attach()`
|
|
18
|
+
connects to a workbench somebody is already looking at and never closes it.
|
|
19
|
+
|
|
20
|
+
Where more than one is running, `meshbench.sessions()` says which: the address
|
|
21
|
+
to connect on, the process, when it started, and what it has open. Pass a row
|
|
22
|
+
straight to `attach()`. A session that was killed rather than closed is not
|
|
23
|
+
listed, because the check is a dial of the address and not a look at a socket
|
|
24
|
+
file or a pid.
|
|
25
|
+
|
|
26
|
+
## Installing
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install meshbench
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You also need the `meshbench` binary on `PATH` — the package drives it, it
|
|
33
|
+
does not contain it.
|
|
34
|
+
|
|
35
|
+
## What it looks like
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
wb.project.new(place="Fife")
|
|
39
|
+
wb.nodes.place_many(
|
|
40
|
+
[
|
|
41
|
+
{"name": "R1", "kind": Kind.SIMPLE_REPEATER, "lat": 56.20, "lon": -3.20},
|
|
42
|
+
{"name": "C1", "kind": Kind.COMPANION, "lat": 56.19, "lon": -3.17},
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
wb.sim.start()
|
|
46
|
+
wb.firmware.wait_started() # a sensible default, or pass a timedelta
|
|
47
|
+
|
|
48
|
+
node = wb.nodes["C1"]
|
|
49
|
+
node.firmware = wb.firmware.find("companion-v1.17.0")
|
|
50
|
+
print(node.console.ask("get region"))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`wb.call(verb, params)` is the whole API underneath, and stays public: anything
|
|
54
|
+
this package has not shaped is one line away rather than a blocker.
|
|
55
|
+
|
|
56
|
+
## Testing firmware with it
|
|
57
|
+
|
|
58
|
+
The package registers a pytest plugin, so there is no `conftest.py` to copy:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
def test_the_flood_reaches_glenrothes(meshbench):
|
|
62
|
+
meshbench.project.open("fixtures/fixture-fife-strict.json")
|
|
63
|
+
meshbench.sim.run(timedelta(minutes=5))
|
|
64
|
+
assert meshbench.events.total() > 0
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
One workbench for the whole run — starting firmware on a real mesh is minutes,
|
|
68
|
+
not milliseconds — with the scenario cleared between tests so reuse does not
|
|
69
|
+
leak. `--meshbench-socket` attaches to one you are already running instead.
|
|
70
|
+
|
|
71
|
+
**A failing test prints the provenance**, whether or not it asked. Somebody
|
|
72
|
+
reading a failed assertion about a mesh is deciding whether their firmware
|
|
73
|
+
change broke something, and they need to know what the run assumed first.
|
|
74
|
+
|
|
75
|
+
## Two things that will bite otherwise
|
|
76
|
+
|
|
77
|
+
**Simulated time is not your time.** Every duration here is a
|
|
78
|
+
`datetime.timedelta` - Python already has a duration type, so this package does
|
|
79
|
+
not invent one - but two different clocks are measured in them:
|
|
80
|
+
|
|
81
|
+
- **the mesh's**, for `sim.run(...)`, `schedule.add(at=, every=)` and
|
|
82
|
+
`sim.wait_until(at=)`
|
|
83
|
+
- **yours**, for every `timeout` and for `sim.run(wait=)`
|
|
84
|
+
|
|
85
|
+
`sim.run(timedelta(minutes=5), wait=timedelta(minutes=60))` is five minutes of
|
|
86
|
+
the mesh's clock, and up to an hour of yours waiting for it. On 155 emulated
|
|
87
|
+
nodes that gap is the normal case, which is why they are separate arguments.
|
|
88
|
+
Every wait has a defensible default, so `wait_started()` on its own is fine.
|
|
89
|
+
|
|
90
|
+
**A node answers on its next loop.** Its loop only runs when the engine steps,
|
|
91
|
+
so reading a console straight after writing to it reads the moment *before* the
|
|
92
|
+
command was sent. Use `console.ask()`, which gives the mesh its own time first.
|
|
93
|
+
|
|
94
|
+
## Honesty
|
|
95
|
+
|
|
96
|
+
Every result comes out of a simulator that is **kinder than the air**: no
|
|
97
|
+
multipath, no body loss, no oscillator error. The measured biases are nearly
|
|
98
|
+
all in one direction, which is what makes a result usable — treat it as a best
|
|
99
|
+
case. `wb.provenance()` says what a given run assumed, and it is meant to be
|
|
100
|
+
printed above any number you publish.
|
|
101
|
+
|
|
102
|
+
## Licence
|
|
103
|
+
|
|
104
|
+
GPL-3.0-or-later, with the rest of MeshBench.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example 1 from #209: a blank setup, one companion, and its screen on show.
|
|
3
|
+
|
|
4
|
+
./01_blank_setup_with_a_board.py
|
|
5
|
+
|
|
6
|
+
Costs: a minute or two. wadamesh is imported, not downloaded, so it must
|
|
7
|
+
already be in the library or reachable through WADAMESH_IMAGE (see below).
|
|
8
|
+
Needs a display. It opens the node's own window on the Hardware tab at the
|
|
9
|
+
end, which is the point of it.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import timedelta
|
|
16
|
+
|
|
17
|
+
from meshbench import Board, Kind, NotFound, Role, Workbench
|
|
18
|
+
|
|
19
|
+
WADAMESH = "wadamesh"
|
|
20
|
+
# wadamesh is imported, not in the download catalogue: a built image to
|
|
21
|
+
# import if it is not already in the library. Point this at one.
|
|
22
|
+
WADAMESH_IMAGE = os.environ.get("WADAMESH_IMAGE")
|
|
23
|
+
BOARD = Board.LILYGO_TDECK
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
with Workbench.launch() as wb:
|
|
28
|
+
wb.project.new(place="Fife")
|
|
29
|
+
|
|
30
|
+
deck = wb.nodes.place(
|
|
31
|
+
"Deck",
|
|
32
|
+
kind=Kind.COMPANION,
|
|
33
|
+
lat=56.19,
|
|
34
|
+
lon=-3.17,
|
|
35
|
+
board=BOARD,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Whatever the catalogue has, so this does not go stale against a
|
|
39
|
+
# version number typed here.
|
|
40
|
+
wb.firmware.scan()
|
|
41
|
+
try:
|
|
42
|
+
build = wb.firmware.find(WADAMESH, board=BOARD)
|
|
43
|
+
except NotFound:
|
|
44
|
+
# wadamesh is imported, not downloaded - import a built image.
|
|
45
|
+
if not WADAMESH_IMAGE:
|
|
46
|
+
sys.exit(
|
|
47
|
+
f"{WADAMESH} is not in the library; set WADAMESH_IMAGE "
|
|
48
|
+
"to a built image, or import one in the workbench first"
|
|
49
|
+
)
|
|
50
|
+
build = wb.firmware.import_(
|
|
51
|
+
WADAMESH_IMAGE,
|
|
52
|
+
Role.COMPANION_RADIO_USB,
|
|
53
|
+
board=BOARD,
|
|
54
|
+
label=WADAMESH,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Applied: stop, provision, start. On a board that means an emulator,
|
|
58
|
+
# which is why the wait below is generous.
|
|
59
|
+
deck.firmware = build
|
|
60
|
+
|
|
61
|
+
wb.sim.start()
|
|
62
|
+
deck.wait_running(timedelta(minutes=5))
|
|
63
|
+
|
|
64
|
+
# The Hardware tab is where the board draws its own screen, which is
|
|
65
|
+
# the whole reason for making this node a T-Deck.
|
|
66
|
+
tab = wb.window(deck, tab="Hardware")
|
|
67
|
+
|
|
68
|
+
print(f"{deck.name} is up on {build}; its window is open on {tab}")
|
|
69
|
+
print(wb.provenance())
|
|
70
|
+
# Held open for somebody looking at it, and only then. Piped or run
|
|
71
|
+
# from CI there is nobody to press enter, and input() raises EOFError
|
|
72
|
+
# there - so an example that had done everything right ended in a
|
|
73
|
+
# traceback and a non-zero-looking failure.
|
|
74
|
+
if sys.stdin.isatty():
|
|
75
|
+
input("press enter to close the workbench ")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example 2 from #209: a fixture trimmed to two, both on a build from a
|
|
3
|
+
MeshCore checkout - and re-runnable without clearing anything down.
|
|
4
|
+
|
|
5
|
+
./02_two_nodes_on_a_local_build.py ~/src/MeshCore
|
|
6
|
+
|
|
7
|
+
The interesting half is the second run. It attaches to the workbench the first
|
|
8
|
+
one left, stops the clock, rebuilds, repoints the nodes and starts again -
|
|
9
|
+
rather than opening a fresh session and paying for the fixture twice.
|
|
10
|
+
|
|
11
|
+
Costs: minutes, mostly firmware. Real firmware on two nodes, not fifty-eight,
|
|
12
|
+
because trimming is what this example is about.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from datetime import timedelta
|
|
19
|
+
|
|
20
|
+
from meshbench import BINARY_ENV, Build, Kind, Role, Workbench
|
|
21
|
+
|
|
22
|
+
# Outskirts of Glasgow, and Glenrothes.
|
|
23
|
+
KEEP = {
|
|
24
|
+
"Glasgow-Outskirts": (55.8720, -4.3300),
|
|
25
|
+
"Glenrothes": (56.1980, -3.1780),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_from_checkout(checkout: str, wb: Workbench) -> dict[str, Build]:
|
|
30
|
+
"""Build MeshCore and import what came out, one build per role.
|
|
31
|
+
|
|
32
|
+
Both roles from one invocation, deliberately. A locally built repeater
|
|
33
|
+
compiled against a stale shim once answered console output with 0x06 where
|
|
34
|
+
the host expects 0x07: it connected, misbehaved and exited. Two arms of a
|
|
35
|
+
comparison speaking different wire protocols measure the shim, not the
|
|
36
|
+
firmware - so if either arm is built by hand, both are, the same way, at
|
|
37
|
+
the same moment.
|
|
38
|
+
"""
|
|
39
|
+
# The same binary the client is driving, not whatever is on PATH: a
|
|
40
|
+
# checkout usually has one built and not installed, and building with one
|
|
41
|
+
# while talking to another is how two arms end up on different code.
|
|
42
|
+
exe = os.environ.get(BINARY_ENV) or "meshbench"
|
|
43
|
+
|
|
44
|
+
out: dict[Role, Build] = {}
|
|
45
|
+
for role in (Role.SIMPLE_REPEATER, Role.COMPANION_RADIO):
|
|
46
|
+
# Named here rather than left to default to the git branch, because
|
|
47
|
+
# the build has to be found again afterwards and a name you chose is
|
|
48
|
+
# the only one you can look up.
|
|
49
|
+
name = f"local-{role}"
|
|
50
|
+
made = subprocess.run(
|
|
51
|
+
[exe, "dev", "-from", checkout, "-role", role, "-name", name],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
check=False,
|
|
55
|
+
)
|
|
56
|
+
if made.returncode != 0:
|
|
57
|
+
sys.exit(
|
|
58
|
+
f"building the {role}: {made.stderr.strip() or made.stdout.strip()}"
|
|
59
|
+
)
|
|
60
|
+
# meshbench dev puts the build in the cache; the library sees it.
|
|
61
|
+
wb.firmware.scan()
|
|
62
|
+
out[role] = wb.firmware.find(name)
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main() -> None:
|
|
67
|
+
if len(sys.argv) < 2:
|
|
68
|
+
sys.exit("usage: 02_two_nodes_on_a_local_build.py <path to MeshCore>")
|
|
69
|
+
checkout = sys.argv[1]
|
|
70
|
+
|
|
71
|
+
with Workbench.attach_or_launch() as wb:
|
|
72
|
+
# Whether the mesh is already the one this example is about, not
|
|
73
|
+
# whether the session is empty. A launched workbench is never empty:
|
|
74
|
+
# it opens its own default fixture, which is 311 nodes - so "is it
|
|
75
|
+
# empty" was always false, the trim below never ran, and this put a
|
|
76
|
+
# local build on a national network and reported "311 nodes" as
|
|
77
|
+
# though that had been the plan.
|
|
78
|
+
already = {n.name for n in wb.nodes.list()} == set(KEEP)
|
|
79
|
+
|
|
80
|
+
# Stop the clock before anything else. A no-op on a fresh session, and
|
|
81
|
+
# the thing that makes the second run safe on a live one.
|
|
82
|
+
wb.sim.pause()
|
|
83
|
+
|
|
84
|
+
if not already:
|
|
85
|
+
wb.project.open("fife-strict")
|
|
86
|
+
# Put them where they belong first, then delete the rest. keep is
|
|
87
|
+
# all-or-none by design, so naming a node that is not there yet
|
|
88
|
+
# refuses and removes nothing - and one of these two is never in
|
|
89
|
+
# the fixture, so the trim refused on every run that reached it.
|
|
90
|
+
for name, (lat, lon) in KEEP.items():
|
|
91
|
+
if name in wb.nodes:
|
|
92
|
+
wb.nodes[name].move(lat, lon)
|
|
93
|
+
else:
|
|
94
|
+
wb.nodes.place(name, Kind.COMPANION, lat, lon)
|
|
95
|
+
wb.nodes.keep(*KEEP)
|
|
96
|
+
wb.wait_idle(timedelta(minutes=10))
|
|
97
|
+
|
|
98
|
+
builds = build_from_checkout(checkout, wb)
|
|
99
|
+
|
|
100
|
+
# Repoint every node, applied - which stops, provisions and starts it.
|
|
101
|
+
for node in wb.nodes:
|
|
102
|
+
role = (
|
|
103
|
+
Role.COMPANION_RADIO
|
|
104
|
+
if node.info.kind == Kind.COMPANION
|
|
105
|
+
else Role.SIMPLE_REPEATER
|
|
106
|
+
)
|
|
107
|
+
node.firmware = builds[role]
|
|
108
|
+
|
|
109
|
+
wb.sim.start()
|
|
110
|
+
wb.firmware.wait_started(timedelta(minutes=10))
|
|
111
|
+
print(f"{len(wb.nodes)} nodes on a build from {checkout}")
|
|
112
|
+
print(wb.provenance())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example 3 from #209: two repeaters, two companions, one of them a T-Deck,
|
|
3
|
+
and a message to the public channel every twenty seconds.
|
|
4
|
+
|
|
5
|
+
./03_small_mesh_with_traffic.py
|
|
6
|
+
|
|
7
|
+
Needs a display: it opens the workbench so you can watch the traffic move.
|
|
8
|
+
|
|
9
|
+
Costs: about ten minutes of simulated time, and a few of yours.
|
|
10
|
+
|
|
11
|
+
The repeating traffic needed no new verb: schedule.add has taken every_ms all
|
|
12
|
+
along and nothing said so, which to somebody writing a script is the same as
|
|
13
|
+
it not existing.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from datetime import timedelta
|
|
17
|
+
|
|
18
|
+
from meshbench import Board, Class, Kind, Workbench
|
|
19
|
+
|
|
20
|
+
MESH = [
|
|
21
|
+
{"name": "R1", "kind": Kind.SIMPLE_REPEATER, "lat": 56.20, "lon": -3.20},
|
|
22
|
+
{"name": "R2", "kind": Kind.SIMPLE_REPEATER, "lat": 56.12, "lon": -3.02},
|
|
23
|
+
{"name": "C1", "kind": Kind.COMPANION, "lat": 56.19, "lon": -3.17},
|
|
24
|
+
{"name": "C2", "kind": Kind.COMPANION, "lat": 56.09, "lon": -3.10},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
with Workbench.launch() as wb:
|
|
30
|
+
wb.project.new(place="Fife")
|
|
31
|
+
wb.nodes.place_many(MESH)
|
|
32
|
+
wb.wait_idle(timedelta(minutes=10))
|
|
33
|
+
|
|
34
|
+
# C1 is the T-Deck. The board goes on before the firmware is pinned,
|
|
35
|
+
# because a host image is not a board image and setting the board
|
|
36
|
+
# clears a pin that was made for different hardware.
|
|
37
|
+
wb.nodes["C1"].board = Board.LILYGO_TDECK
|
|
38
|
+
|
|
39
|
+
# Whatever this machine holds for each role that needs one, rather
|
|
40
|
+
# than a version typed here that goes stale.
|
|
41
|
+
wb.firmware.use_what_is_here()
|
|
42
|
+
|
|
43
|
+
# Every twenty seconds, from the plain companion to the public channel.
|
|
44
|
+
# Simulated seconds - the mesh's own clock, not yours.
|
|
45
|
+
wb.schedule.add(
|
|
46
|
+
"C2",
|
|
47
|
+
"public hello",
|
|
48
|
+
at=timedelta(seconds=5),
|
|
49
|
+
every=timedelta(seconds=20),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
wb.sim.start()
|
|
53
|
+
wb.firmware.wait_started(timedelta(minutes=10))
|
|
54
|
+
wb.sim.run(timedelta(minutes=10), wait=timedelta(minutes=60))
|
|
55
|
+
|
|
56
|
+
received = [e for e in wb.events.recent(1000) if e.class_ == Class.RECEIVED]
|
|
57
|
+
print(wb.provenance())
|
|
58
|
+
print(f"{wb.events.total()} events, {len(received)} receptions in the tail")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example 4: the one CI runs.
|
|
3
|
+
|
|
4
|
+
./04_headless_regression.py [fixture] [junit.xml]
|
|
5
|
+
|
|
6
|
+
No display, no GPU, no toolkit. Opens a fixture, runs it, checks its
|
|
7
|
+
assertions, writes JUnit, and exits non-zero if the mesh stopped delivering.
|
|
8
|
+
This is the shape a MeshCore pull request would use.
|
|
9
|
+
|
|
10
|
+
Costs: as long as the fixture asks for. fife-strict at five simulated minutes
|
|
11
|
+
is a couple of minutes of wall clock with no firmware, and considerably more
|
|
12
|
+
with it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import timedelta
|
|
17
|
+
|
|
18
|
+
from meshbench import Workbench
|
|
19
|
+
|
|
20
|
+
SEED = 9001
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> int:
|
|
24
|
+
fixture = sys.argv[1] if len(sys.argv) > 1 else "fife-strict"
|
|
25
|
+
junit = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
26
|
+
|
|
27
|
+
with Workbench.headless(fixture=fixture, seed=SEED) as wb:
|
|
28
|
+
# Bring the mesh up before running the clock. sim.run only advances
|
|
29
|
+
# time; without this the firmware never starts, nothing transmits, and
|
|
30
|
+
# the run reports every assertion failed on a tree with nothing wrong
|
|
31
|
+
# with it - which is the worst thing a regression check can do.
|
|
32
|
+
wb.sim.start()
|
|
33
|
+
wb.firmware.wait_started()
|
|
34
|
+
|
|
35
|
+
wb.sim.run(timedelta(minutes=5), wait=timedelta(minutes=60))
|
|
36
|
+
|
|
37
|
+
report = wb.assertions.check()
|
|
38
|
+
|
|
39
|
+
# The report prints the caveats above the numbers itself, because this
|
|
40
|
+
# is the output somebody pastes into a pull request and the caveats are
|
|
41
|
+
# the half that gets dropped.
|
|
42
|
+
print(report)
|
|
43
|
+
print(f"{wb.events.total()} events")
|
|
44
|
+
|
|
45
|
+
if junit:
|
|
46
|
+
report.write_junit(junit)
|
|
47
|
+
|
|
48
|
+
if report.total == 0:
|
|
49
|
+
# Not a pass. A fixture with no assertions can report but cannot
|
|
50
|
+
# pass or fail, and a green tick that checked nothing is the worst
|
|
51
|
+
# outcome available here.
|
|
52
|
+
return 2
|
|
53
|
+
return 0 if report.ok else 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
sys.exit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Two builds, two nodes, one scenario - the A/B #192 was filed from.
|
|
3
|
+
|
|
4
|
+
./05_two_builds_in_one_scenario.py <stock version> <path to a local build>
|
|
5
|
+
|
|
6
|
+
The most common real use of this API, and the reason the node window grew a
|
|
7
|
+
firmware control: comparing a stock build against one with a single changed
|
|
8
|
+
constant, on the same mesh, at the same seed.
|
|
9
|
+
|
|
10
|
+
Needs a display: it opens the workbench so you can watch both arms run.
|
|
11
|
+
|
|
12
|
+
Costs: firmware on a whole fixture, so minutes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from datetime import timedelta
|
|
17
|
+
|
|
18
|
+
from meshbench import Role, Workbench
|
|
19
|
+
|
|
20
|
+
SEED = 9001
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
if len(sys.argv) < 3:
|
|
25
|
+
sys.exit(
|
|
26
|
+
"usage: 05_two_builds_in_one_scenario.py <stock version> <local build path>"
|
|
27
|
+
)
|
|
28
|
+
stock_version, local_path = sys.argv[1], sys.argv[2]
|
|
29
|
+
|
|
30
|
+
with Workbench.launch(fixture="fife-strict", seed=SEED) as wb:
|
|
31
|
+
stock = wb.firmware.find(stock_version)
|
|
32
|
+
# The application name the verbs are keyed on, not the catalogue's
|
|
33
|
+
# shorter "repeater": a build imported under a role no node has is a
|
|
34
|
+
# build nothing will ever run.
|
|
35
|
+
changed = wb.firmware.import_(local_path, Role.SIMPLE_REPEATER)
|
|
36
|
+
|
|
37
|
+
# Two nodes far enough apart to be independently interesting, one on
|
|
38
|
+
# each build. Applied, which restarts each of them.
|
|
39
|
+
a, b = list(wb.nodes)[0], list(wb.nodes)[1]
|
|
40
|
+
a.firmware = stock
|
|
41
|
+
b.firmware = changed
|
|
42
|
+
|
|
43
|
+
wb.sim.start()
|
|
44
|
+
wb.firmware.wait_started(timedelta(minutes=15))
|
|
45
|
+
wb.sim.run(timedelta(minutes=5), wait=timedelta(minutes=60))
|
|
46
|
+
|
|
47
|
+
# Per node, because the whole point is which of the two behaved
|
|
48
|
+
# differently - a total would hide it.
|
|
49
|
+
print(wb.provenance())
|
|
50
|
+
for node, build in ((a, stock), (b, changed)):
|
|
51
|
+
s = node.stat
|
|
52
|
+
print(f"{node.name:24} {str(build):32} sent {s.sent:4} heard {s.heard:4}")
|
|
53
|
+
|
|
54
|
+
# One run of one seed is one draw. A difference here is a hypothesis,
|
|
55
|
+
# not a result: run it across seeds with experiment.* before believing
|
|
56
|
+
# anything.
|
|
57
|
+
print("\none seed, one draw - vary the seed before calling this a difference")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|