Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/skills/meshbench-scripting/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,12 @@ matched on `floor` as the catch-all now silently sees fewer of them. Group an
- Unix socket paths are capped at 104 bytes. The scratchpad path is longer than
that, so let the client choose the address.
- **An emulated board no longer needs a hand-built toolchain.**
`resource.fetch` downloads `radioserver`, `qemu-system-xtensa` and `renode`
`resource.fetch` downloads `virtual-sx1262`, `qemu-system-xtensa` and `renode`
into `~/.cache/meshbench/tools/`, which is where a boot already looks, so no
environment variable is needed afterwards. **Pass `kind: "toolchain"`**: the
parameter defaults to `softdevice`, and a fetch that omits it asks for the
wrong thing. QEMU and Renode are published for linux/amd64 only; macOS gets
`radioserver` alone and Windows nothing, and `resource.list` says which with
the chip model alone and Windows nothing, and `resource.list` says which with
a reason.
- A killed run can leave an emulator behind. Check `pgrep -f qemu-system`.
- Firmware roles on disk: `ls ~/.cache/meshbench/firmware/native/`.
Expand Down
5 changes: 2 additions & 3 deletions .claude/skills/meshcoresim/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,13 @@ one (ADR-0010). A node runs emulated when its `Firmware.Board` is set; empty
means the host build.

**The toolchain is a download now, not a build.** `resource.fetch` with
`kind: "toolchain"` fetches `radioserver`, `qemu-system-xtensa` and `renode`
`kind: "toolchain"` fetches `virtual-sx1262`, `qemu-system-xtensa` and `renode`
into `~/.cache/meshbench/tools/`, which is step three of the lookup a boot
already performs, so nothing has to be set afterwards. The `kind` parameter
**defaults to `softdevice`**, so a fetch that omits it asks for the wrong thing.
`resource.list` says what is present and what it cost; `setup.check` says the
same beside everything else that is missing. QEMU and Renode are published for
linux/amd64 only, macOS gets `radioserver` alone, and Windows nothing, each with
linux/amd64 only, macOS gets the chip model alone, and Windows nothing, each with
its reason. An emulated nRF52 board additionally needs the Nordic s140
SoftDevice, which is its own `softdevice` resource.

Expand Down Expand Up @@ -385,7 +385,6 @@ and is not worth subtracting from another arm.
| QEMU with our SX1262, GPIO and fixes | `MeshBench/qemu` branch `meshbench-sx1262` |
| Renode with the SEVONPEND fix | `MeshBench/renode` and `MeshBench/tlib` |
| The chip model | `MeshBench/virtual-sx1262`, MIT, its own repository |
| The socket server that hosts it | `meshcore-native`, `bridge/radioserver.cpp` |
| Per-board wiring | `internal/firmware/board/board_<name>.go` |

### The chip model is a submodule, and a submodule does not follow anything
Expand Down
101 changes: 66 additions & 35 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
with: { go-version: '1.25', cache: false }

# Checked before anything is built, because a missing tool used to
# surface eight steps later as "no radioserver-v1 release" - naming a
# surface eight steps later as "no such release" - naming a
# release that exists. That was gh, which is not part of a bare Ubuntu
# image; the fetches use curl now, which is here and was already used
# for the emoji font.
Expand Down Expand Up @@ -197,15 +197,17 @@ jobs:
run: |
set -e
# No || here. It used to swallow the downloader's own error and
# report "no radioserver release", which was wrong - the release
# existed and gh was not installed - and sent the reader to the wrong
# repository. set -e stops the job; the script says why.
# report "no release", which was wrong - the release existed and gh
# was not installed - and sent the reader to the wrong repository.
# set -e stops the job; the script says why.
. packaging/emulator-pins.env
tools/fetch-release-asset.sh MeshBench/meshcore-native "$RADIOSERVER_RELEASE" \
"/$RADIOSERVER_ASSET_LINUX_AMD64\$" dist/meshbench
mv "dist/meshbench/$RADIOSERVER_ASSET_LINUX_AMD64" dist/meshbench/radioserver
chmod +x dist/meshbench/radioserver
dist/meshbench/radioserver 2>&1 | head -1 || true
tools/fetch-release-asset.sh MeshBench/virtual-sx1262 "$CHIPMODEL_RELEASE" \
"/$CHIPMODEL_ASSET_LINUX_AMD64\$" dist/meshbench
tar xzf "dist/meshbench/$CHIPMODEL_ASSET_LINUX_AMD64" -C dist/meshbench
mv dist/meshbench/virtual-sx1262-linux-amd64/lib/libvirtualsx1262.so \
dist/meshbench/
rm -rf dist/meshbench/virtual-sx1262-linux-amd64 \
"dist/meshbench/$CHIPMODEL_ASSET_LINUX_AMD64"

- name: Fonts and fixtures
run: |
Expand All @@ -217,6 +219,17 @@ jobs:
curl -fsSL -o dist/meshbench/fonts/NotoColorEmoji.ttf \
https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf \
|| echo "::warning::emoji font not fetched, bundle falls back to system fonts"
# Renode's platform descriptions and our own C# peripherals. They are
# ours, they are text, and Renode reads them at runtime rather than
# having them compiled in - so a bundle without them ships two
# emulators and can start a board on only one of them. That reads as a
# broken emulator rather than as a missing file, which is why this is
# here rather than in a note.
mkdir -p dist/meshbench/renode-support/peripherals
cp tools/renode/*.repl dist/meshbench/renode-support/
cp tools/renode/peripherals/*.cs tools/renode/peripherals/*.repl \
dist/meshbench/renode-support/peripherals/

# The shipped fixtures, so the workbench has something to open on a
# machine with nothing else on it.
cp -r fixtures dist/meshbench/fixtures
Expand Down Expand Up @@ -268,10 +281,10 @@ jobs:
qemu-system-xtensa is a symlink on purpose. QEMU resolves its own path
to find its data files, so a bare copy of the binary will not run.

radioserver is the SX1262 model the emulated firmware clocks over a
socket. It is the same chip object a native node uses in process, so
an emulated node and a native one are the same radio. Both emulators
need it; neither works without it.
libvirtualsx1262 is the SX1262 itself, loaded by whichever emulator
runs a board. It is the same model a native node links, so an emulated
node and a native one are the same radio. Both emulators need it;
neither works without it.

Native MeshCore builds and board images are downloaded on first use and
cached under ~/.cache/meshbench. Nothing else is needed.
Expand Down Expand Up @@ -330,14 +343,16 @@ jobs:
APP=dist/AppDir
mkdir -p $APP/usr/bin $APP/usr/share/applications \
$APP/usr/share/metainfo $APP/usr/share/meshbench
# radioserver too: it is 40 KB, and without it an installed
# MeshBench cannot emulate even when the user supplies their own
# QEMU. The emulators themselves stay out of the AppImage and the
# The chip model too: it is a hundred kilobytes, and without it an
# installed MeshBench cannot emulate even when the user supplies their
# own QEMU. The emulators themselves stay out of the AppImage and the
# .deb on size grounds - 110 MB against 26 - so those two are the
# application, and the tarball is the batteries-included download.
cp dist/meshbench/meshbench dist/meshbench/radioserver $APP/usr/bin/
cp dist/meshbench/meshbench dist/meshbench/libvirtualsx1262.so $APP/usr/bin/
cp -r dist/meshbench/fixtures dist/meshbench/fonts \
dist/meshbench/LICENCES $APP/usr/share/meshbench/
# Beside the binary, which is where SupportDir looks first.
cp -r dist/meshbench/renode-support $APP/usr/bin/
cp packaging/meshbench.desktop $APP/usr/share/applications/
cp packaging/io.github.meshbench.meshbench.metainfo.xml $APP/usr/share/metainfo/
for px in 16 24 32 48 64 128 256 512; do
Expand Down Expand Up @@ -372,7 +387,7 @@ jobs:
mkdir -p $d
cp packaging/icons/meshbench-${px}.png $d/io.github.meshbench.meshbench.png
done
cp $APP/usr/bin/meshbench $APP/usr/bin/radioserver $DEB/usr/bin/
cp $APP/usr/bin/meshbench $APP/usr/bin/libvirtualsx1262.so $DEB/usr/bin/
cp -r $APP/usr/share/meshbench/* $DEB/usr/share/meshbench/
cp packaging/meshbench.desktop $DEB/usr/share/applications/
cp packaging/io.github.meshbench.meshbench.metainfo.xml $DEB/usr/share/metainfo/
Expand Down Expand Up @@ -519,12 +534,22 @@ jobs:
if [ -f "$EMU/Release/osx-arm64/Renode" ]; then
ln -sf ../Resources/emulators/Release/osx-arm64/Renode "$APP/Contents/MacOS/renode"
fi
# The radio model. A real file rather than a link: it is 40 KB and
# The chip model. A real file rather than a link: it is small and
# resolves nothing from its own path.
tools/fetch-release-asset.sh MeshBench/meshcore-native "$RADIOSERVER_RELEASE" \
"/$RADIOSERVER_ASSET_DARWIN_ARM64\$" "$APP/Contents/MacOS"
mv "$APP/Contents/MacOS/$RADIOSERVER_ASSET_DARWIN_ARM64" "$APP/Contents/MacOS/radioserver"
chmod +x "$APP/Contents/MacOS/radioserver"
tools/fetch-release-asset.sh MeshBench/virtual-sx1262 "$CHIPMODEL_RELEASE" \
"/$CHIPMODEL_ASSET_DARWIN_ARM64\$" "$APP/Contents/MacOS"
tar xzf "$APP/Contents/MacOS/$CHIPMODEL_ASSET_DARWIN_ARM64" -C "$APP/Contents/MacOS"
mv "$APP/Contents/MacOS/virtual-sx1262-macos-arm64/lib/libvirtualsx1262.dylib" \
"$APP/Contents/MacOS/"
rm -rf "$APP/Contents/MacOS/virtual-sx1262-macos-arm64" \
"$APP/Contents/MacOS/$CHIPMODEL_ASSET_DARWIN_ARM64"
# Renode's platform descriptions and our own peripherals, beside the
# binary where SupportDir looks first. Without them this bundle can
# start an ESP32 board and not an nRF52 one.
mkdir -p "$APP/Contents/MacOS/renode-support/peripherals"
cp tools/renode/*.repl "$APP/Contents/MacOS/renode-support/"
cp tools/renode/peripherals/*.cs tools/renode/peripherals/*.repl \
"$APP/Contents/MacOS/renode-support/peripherals/"
# What the app actually carries, asked the way a node asks, before it
# is signed and sealed into a dmg nobody will open again.
packaging/verify-bundle.sh "$APP/Contents/MacOS" darwin-arm64
Expand Down Expand Up @@ -686,13 +711,19 @@ jobs:
ls -la
cd ../..

# The radio model. It takes the TCP transport - the simulator asks
# for ":0" on Windows for both emulators, because Windows has no
# Unix socket mingw can reach.
tools/fetch-release-asset.sh MeshBench/meshcore-native "$RADIOSERVER_RELEASE" \
"/$RADIOSERVER_ASSET_WINDOWS_AMD64\$" dist/meshbench
mv "dist/meshbench/$RADIOSERVER_ASSET_WINDOWS_AMD64" dist/meshbench/radioserver.exe
file dist/meshbench/radioserver.exe
# The chip model, which whichever emulator runs a board loads.
tools/fetch-release-asset.sh MeshBench/virtual-sx1262 "$CHIPMODEL_RELEASE" \
"/$CHIPMODEL_ASSET_WINDOWS_AMD64\$" dist/meshbench
tar xzf "dist/meshbench/$CHIPMODEL_ASSET_WINDOWS_AMD64" -C dist/meshbench
mv dist/meshbench/virtual-sx1262-windows-amd64/lib/libvirtualsx1262.dll \
dist/meshbench/
rm -rf dist/meshbench/virtual-sx1262-windows-amd64 \
"dist/meshbench/$CHIPMODEL_ASSET_WINDOWS_AMD64"
file dist/meshbench/libvirtualsx1262.dll
mkdir -p dist/meshbench/renode-support/peripherals
cp tools/renode/*.repl dist/meshbench/renode-support/
cp tools/renode/peripherals/*.cs tools/renode/peripherals/*.repl \
dist/meshbench/renode-support/peripherals/
# What the zip actually carries, asked the way a node asks.
packaging/verify-bundle.sh dist/meshbench windows-amd64
cat > dist/meshbench/README.txt <<'TXT'
Expand All @@ -711,10 +742,10 @@ jobs:
a memory stick, a build agent, or anywhere an installer is unwanted.

Whichever emulators are published for Windows are in this zip, with
radioserver.exe, the SX1262 model they clock over a socket. They are
found automatically, and they are MeshBench builds rather than
upstream ones - ours carry an SX1262 device and a SEVONPEND fix that
published firmware needs.
libvirtualsx1262.dll, the SX1262 they load. They are found
automatically, and they are MeshBench builds rather than upstream
ones - ours carry an SX1262 device and a SEVONPEND fix that published
firmware needs.

Help > Setup cannot download a replacement on Windows - it reads ELF
and Mach-O headers rather than PE - so the zip is where they come
Expand All @@ -739,7 +770,7 @@ jobs:
# agent, or anyone who wants no installer.
#
# What the installer adds is a deliberate location. This bundle finds
# the emulators and radioserver.exe beside meshbench.exe, so a user who
# the emulators and the chip model beside meshbench.exe, so a user who
# unpacks the zip into Downloads and then moves the one executable
# somewhere tidy - which is the natural thing to do with a single
# executable - has broken emulation and will not be told why. The
Expand Down
2 changes: 1 addition & 1 deletion .golangci-baseline.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
contextcheck 5
dupl 10
gosec 32
gosec 31
noctx 10
recvcheck 2
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,11 @@ nobody has watched that board do that thing.

✓ passed ✗ failed – not applicable ? not measurable yet blank not attempted

Measured on 3 September 2026 against `radioserver-v3`, one board at a time on
an idle machine. Every row above is a run from that day except the two blanks.
Measured one board at a time on an idle machine. Nine of these rows were
re-measured on 4 September 2026 against `virtual-sx1262` v1.3.0, loaded inside
the emulator, and every one reproduced what it had shown the day before through
the radio server that arrangement replaced. `LilyGo_TDeck` is the exception and
still carries its 3 September run; the two blanks have never been attempted.

What each board's row means in detail is in
[`docs/emulated-published-firmware.md`](docs/emulated-published-firmware.md).
Expand Down
5 changes: 3 additions & 2 deletions docs/emulated-published-firmware.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ back, plus the `BUSY` and `DIO1` lines. The native path keeps calling it
in-process and does not change.

The QEMU device is then a forwarder, not a second implementation. Same for
`tools/renode/peripherals/SX1262.cs`, which already assumes a socket and can
`tools/renode/peripherals/VirtualSX1262.cs`, which already assumes a socket and can
become a forwarder too, deleting about 380 lines of C#.

Its link to the RF engine already exists and stays exactly as it is.
Expand Down Expand Up @@ -160,7 +160,8 @@ Every `✗` in the **flood** column above, on both MCU families, came from two
faults in `VirtualSX1262` rather than from anything about the boards. Both are
fixed, in
[MeshBench/virtual-sx1262](https://github.com/MeshBench/virtual-sx1262) and
released as `radioserver-v3`.
released as `radioserver-v3`, which was the last of them: the chip moved into
the emulators and that process was retired.

**The carrier-detect flags outlived the carrier.** `PREAMBLE_DETECTED` and
`HEADER_VALID` were latched and never cleared when the air went quiet.
Expand Down
6 changes: 3 additions & 3 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ msiexec /i meshbench-0.2.0-windows-x86_64.msi ADDLOCAL=Complete,DesktopShortcut
`meshbench-*-windows-x86_64.zip` is the same build with nothing installed.
Unzip it anywhere and run `meshbench.exe`. Take this one for a memory stick, a
build agent, or anywhere an installer is unwanted. **Keep the folder together**:
`meshbench.exe` finds the emulators and `radioserver.exe` beside itself, so
`meshbench.exe` finds the emulators and the chip model beside itself, so
moving the `.exe` out on its own leaves a build that cannot emulate a board and
will not say why. That is the main thing the installer exists to prevent.

Expand Down Expand Up @@ -156,8 +156,8 @@ invisible here and the wrong build. Ours carry an SX1262 device and the
SEVONPEND fix respectively; a stock build starts, reports no chip or hangs, and
looks like a MeshBench fault.

From a source checkout, `radioserver` can also be built rather than fetched:
`./build.sh radioserver out` in a `MeshBench/meshcore-native` clone, then copy
From a source checkout, the chip model can also be built rather than fetched:
`./build.sh shared` in a `MeshBench/virtual-sx1262` clone, then copy
the binary into the tools directory.

Everything the application ships under is listed in **Help → Licences &
Expand Down
12 changes: 6 additions & 6 deletions docs/native-and-emulated.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ looks, so nothing else has to be set. From a script:

```
resource.list # what is here, and what could be
resource.fetch {"kind":"toolchain","name":"radioserver"}
resource.fetch {"kind":"toolchain","name":"virtual-sx1262"}
```

Doing it by hand still works, and is what to do on a platform the page says has
Expand All @@ -164,7 +164,7 @@ no build:
```bash
mkdir -p ~/.cache/meshbench/tools
ln -sf /path/to/qemu-system-xtensa ~/.cache/meshbench/tools/
cp /path/to/radioserver ~/.cache/meshbench/tools/
cp /path/to/libvirtualsx1262.so ~/.cache/meshbench/tools/
ln -sf /path/to/renode ~/.cache/meshbench/tools/ # nRF52 only
```

Expand All @@ -174,8 +174,8 @@ peripherals and platform files live in `tools/renode/` and are loaded from that
same tools directory.

A symlink is right for QEMU: it finds its own data files by resolving its real
path, so a bare copy of the binary will not run. `radioserver` builds from
`meshcore-native` with `./build.sh radioserver out` — it wants neither a
path, so a bare copy of the binary will not run. The chip model builds from
`MeshBench/virtual-sx1262` with `./build.sh shared` — it wants neither a
MeshCore checkout nor Crypto, only the chip model beside it.

Then open the firmware library, download a board image, and set a node's role to
Expand Down Expand Up @@ -248,7 +248,7 @@ described as if it were, and its radio is a stub, so it proves the mesh stack
compiles and runs on Cortex-M4 rather than that a node works.

All three paths share one chip model. `VirtualSX1262` runs in process for a
native node, and `radioserver` puts the same object behind a socket for QEMU and
native node, and QEMU and Renode load the same library for
Renode. That is deliberate: two models of one chip must agree for ever, and the
first time they drift, every comparison between an ARM node and an ESP32 node
measures our code rather than MeshCore's.
Expand Down Expand Up @@ -291,7 +291,7 @@ as though it should hold. A native node's clock is *supplied*: the tick carries
the instant, the shim runs one MeshCore loop per simulated millisecond, and the
acknowledgement means the firmware has been there. An emulated node is ticked
through exactly the same code path, and the acknowledgement means something
weaker: it comes from `radioserver`, the chip model on this side of the socket,
weaker: it comes from the chip model on this side of the socket,
because the guest is a published image with nothing in it that could receive a
tick. Meanwhile the guest executes against QEMU's or Renode's clock, neither of
which is under `-icount` or a Renode quantum, so how much firmware runs between
Expand Down
Loading
Loading