Skip to content

Commit 82220ba

Browse files
authored
Merge pull request #946 from ianmcorvidae/sim-based-testing
Initial implementation of simradio-based testing with a multi-node simulated mesh
2 parents 7a7353c + 4339cda commit 82220ba

11 files changed

Lines changed: 1589 additions & 664 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717
- "3.10"
1818
- "3.11"
1919
- "3.12"
20+
- "3.13"
21+
- "3.14"
2022
steps:
2123
- uses: actions/checkout@v4
2224
- name: Install Python 3
@@ -56,12 +58,11 @@ jobs:
5658
strategy:
5759
matrix:
5860
python-version:
59-
- "3.9"
60-
- "3.10"
61-
- "3.11"
6261
- "3.12"
6362
steps:
6463
- uses: actions/checkout@v4
64+
with:
65+
persist-credentials: false
6566
- name: Install Python 3
6667
uses: actions/setup-python@v5
6768
- name: Install meshtastic from local
@@ -70,3 +71,36 @@ jobs:
7071
pip3 install poetry
7172
poetry install
7273
poetry run meshtastic --version
74+
75+
simradio_testing:
76+
runs-on: ubuntu-latest
77+
strategy:
78+
fail-fast: false
79+
matrix:
80+
channel:
81+
- beta
82+
- alpha
83+
- daily
84+
continue-on-error: ${{ matrix.channel == 'daily' }}
85+
steps:
86+
- uses: actions/checkout@v4
87+
with:
88+
persist-credentials: false
89+
- name: Install Python 3
90+
uses: actions/setup-python@v5
91+
with:
92+
python-version: "3.12"
93+
- name: Install meshtastic from local
94+
run: |
95+
python -m pip install --upgrade pip
96+
pip3 install poetry
97+
poetry install --all-extras --with dev
98+
poetry run meshtastic --version
99+
- name: Install meshtasticd (${{ matrix.channel }}) from PPA
100+
run: |
101+
sudo add-apt-repository -y ppa:meshtastic/${{ matrix.channel }}
102+
sudo apt-get update
103+
sudo apt-get install -y meshtasticd
104+
- name: Run firmware smoke tests
105+
run: poetry run pytest -m "smokevirt or smokemesh" -v
106+
timeout-minutes: 15
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Daily SimRadio Tests
2+
3+
on:
4+
schedule:
5+
- cron: 0 6 * * *
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
simradio_testing:
13+
runs-on: ubuntu-latest
14+
if: github.repository == 'meshtastic/meshtastic-python'
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
persist-credentials: false
19+
- name: Install Python 3
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: "3.12"
23+
- name: Install meshtastic from local
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip3 install poetry
27+
poetry install --all-extras --with dev
28+
poetry run meshtastic --version
29+
- name: Install meshtasticd (daily) from PPA
30+
run: |
31+
sudo add-apt-repository -y ppa:meshtastic/daily
32+
sudo apt-get update
33+
sudo apt-get install -y meshtasticd
34+
- name: Run firmware smoke tests
35+
run: poetry run pytest -m "smokevirt or smokemesh" -v
36+
timeout-minutes: 15

‎meshtastic/__main__.py‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -969,9 +969,15 @@ def setSimpleConfig(modem_preset):
969969
if args.ch_longslow:
970970
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_SLOW)
971971

972+
if args.ch_longmod:
973+
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_MODERATE)
974+
972975
if args.ch_longfast:
973976
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_FAST)
974977

978+
if args.ch_longturbo:
979+
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.LONG_TURBO)
980+
975981
if args.ch_medslow:
976982
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.MEDIUM_SLOW)
977983

@@ -984,6 +990,9 @@ def setSimpleConfig(modem_preset):
984990
if args.ch_shortfast:
985991
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_FAST)
986992

993+
if args.ch_shortturbo:
994+
setSimpleConfig(config_pb2.Config.LoRaConfig.ModemPreset.SHORT_TURBO)
995+
987996
if args.ch_set or args.ch_enable or args.ch_disable:
988997
closeNow = True
989998

@@ -1960,43 +1969,61 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
19601969

19611970
group.add_argument(
19621971
"--ch-vlongslow",
1963-
help="Change to the very long-range and slow modem preset",
1972+
help="Change to the VERY_LONG_SLOW modem preset. Deprecated since 2.5 firmware.",
19641973
action="store_true",
19651974
)
19661975

19671976
group.add_argument(
19681977
"--ch-longslow",
1969-
help="Change to the long-range and slow modem preset",
1978+
help="Change to the LONG_SLOW modem preset. Deprecated since 2.7 firmware.",
1979+
action="store_true",
1980+
)
1981+
1982+
group.add_argument(
1983+
"--ch-longmod", "--ch-longmoderate",
1984+
help="Change to the LONG_MODERATE modem preset",
19701985
action="store_true",
19711986
)
19721987

19731988
group.add_argument(
19741989
"--ch-longfast",
1975-
help="Change to the long-range and fast modem preset",
1990+
help="Change to the LONG_FAST modem preset",
1991+
action="store_true",
1992+
)
1993+
1994+
group.add_argument(
1995+
"--ch-longturbo",
1996+
help="Change to the LONG_TURBO preset",
19761997
action="store_true",
19771998
)
19781999

19792000
group.add_argument(
19802001
"--ch-medslow",
1981-
help="Change to the med-range and slow modem preset",
2002+
help="Change to the MEDIUM_SLOW modem preset",
19822003
action="store_true",
19832004
)
19842005

19852006
group.add_argument(
19862007
"--ch-medfast",
1987-
help="Change to the med-range and fast modem preset",
2008+
help="Change to the MEDIUM_FAST modem preset",
19882009
action="store_true",
19892010
)
19902011

19912012
group.add_argument(
19922013
"--ch-shortslow",
1993-
help="Change to the short-range and slow modem preset",
2014+
help="Change to the SHORT_SLOW modem preset",
19942015
action="store_true",
19952016
)
19962017

19972018
group.add_argument(
19982019
"--ch-shortfast",
1999-
help="Change to the short-range and fast modem preset",
2020+
help="Change to the SHORT_FAST modem preset",
2021+
action="store_true",
2022+
)
2023+
2024+
group.add_argument(
2025+
"--ch-shortturbo",
2026+
help="Change to the SHORT_TURBO modem preset",
20002027
action="store_true",
20012028
)
20022029

‎meshtastic/node.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,22 @@ def deleteChannel(self, channelIndex):
287287
# for sending admin channels will also change
288288
adminIndex = self.iface.localNode._getAdminChannelIndex()
289289

290+
# Snapshot serialized channel payloads from channelIndex onward so we
291+
# can avoid writing slots whose protobuf content did not change after
292+
# the shift. Use bytes (not message objects), because _fixupChannels()
293+
# mutates message fields in-place.
294+
old_channels = [
295+
self.channels[i].SerializeToString()
296+
for i in range(channelIndex, len(self.channels))
297+
]
298+
290299
self.channels.pop(channelIndex)
291300
self._fixupChannels() # expand back to 8 channels
292301

293302
index = channelIndex
294-
while index < 8:
295-
self.writeChannel(index, adminIndex=adminIndex)
303+
for old_ch in old_channels:
304+
if self.channels[index].SerializeToString() != old_ch:
305+
self.writeChannel(index, adminIndex=adminIndex)
296306
index += 1
297307

298308
# if we are updating the local node, we might end up

‎meshtastic/tests/conftest.py‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,87 @@
88
from meshtastic import mt_config
99

1010
from ..mesh_interface import MeshInterface
11+
from .firmware_harness import (
12+
CHAIN_TOPOLOGY,
13+
DEFAULT_BASE_PORT,
14+
SimMesh,
15+
find_meshtasticd,
16+
is_compatible_host,
17+
)
18+
from .fw_helpers import set_region
19+
20+
# Use a different base port for the single-node fixture so it doesn't
21+
# conflict with the multi-node mesh fixture.
22+
SINGLE_NODE_BASE_PORT = DEFAULT_BASE_PORT + 100
23+
24+
25+
def _skip_firmware_if_unavailable() -> None:
26+
"""Skip the test when meshtasticd can't run on this host."""
27+
if not is_compatible_host():
28+
pytest.skip("meshtasticd firmware tests require Linux")
29+
if find_meshtasticd() is None:
30+
pytest.skip(
31+
"meshtasticd not found — set MESHTASTICD_BIN or install it on PATH"
32+
)
33+
34+
35+
@pytest.fixture(scope="function")
36+
def firmware_node():
37+
"""A single meshtasticd sim node for smokevirt tests.
38+
39+
Function-scoped so every test gets a freshly-erased node with no
40+
state leaking from previous tests. This makes destructive commands
41+
(``--reboot``, ``--set factory_reset true``) safe to run and lets
42+
tests be order-independent.
43+
44+
Yields the SimNode instance. The node is booted with a fresh erased
45+
config and listens on localhost at its TCP port. Region is set to US
46+
so modem-preset tests work against firmware >= 2.8, which clamps
47+
presets to the legal set for the current region.
48+
"""
49+
_skip_firmware_if_unavailable()
50+
mesh = SimMesh(n_nodes=1, base_port=SINGLE_NODE_BASE_PORT)
51+
mesh.start()
52+
node = mesh.get_node(0)
53+
set_region(node.port, "US")
54+
# The region commit restarts the TCP listener, so reconnect the harness
55+
# interface in case a test wants to use it directly.
56+
if node.iface is not None:
57+
try:
58+
node.iface.close()
59+
except Exception: # pylint: disable=broad-except
60+
pass
61+
node.connect()
62+
yield node
63+
mesh.stop()
64+
65+
66+
@pytest.fixture(scope="function")
67+
def firmware_mesh():
68+
"""A 3-node chain (A-B-C) meshtasticd sim mesh for smokemesh tests.
69+
70+
Yields the SimMesh instance. Nodes are connected and the SIMULATOR_APP
71+
packet bridge is running. Region is set to US for firmware >= 2.8
72+
compatibility, interfaces are reconnected after the region change, and
73+
node DB convergence is awaited.
74+
"""
75+
_skip_firmware_if_unavailable()
76+
mesh = SimMesh(n_nodes=3, topology=CHAIN_TOPOLOGY)
77+
mesh.start()
78+
for node in mesh.nodes:
79+
set_region(node.port, "US")
80+
# The region commit restarts each node's TCP listener, so reconnect the
81+
# harness interfaces before waiting for convergence.
82+
for node in mesh.nodes:
83+
if node.iface is not None:
84+
try:
85+
node.iface.close()
86+
except Exception: # pylint: disable=broad-except
87+
pass
88+
node.connect()
89+
mesh.wait_for_convergence(timeout=30)
90+
yield mesh
91+
mesh.stop()
1192

1293

1394
@pytest.fixture

0 commit comments

Comments
 (0)