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
80 changes: 74 additions & 6 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,28 @@ jobs:
for f in *.tar.gz *.tar.xz; do [ -e "$f" ] || continue; tar xf "$f" && rm "$f"; done
ls -la

- name: The radio model
# Every emulated node looks this up *before* either emulator, so a
# bundle without it ships a QEMU and a Renode that nothing can drive.
# 0.0.1 through 0.0.3 shipped exactly that: two emulators, no radio,
# and a README claiming emulated boards worked.
#
# Fetched as a release asset, the same as the emulators - radioserver-v1
# is meshcore-native#4 built once and published, not rebuilt from source
# in every job that needs it. Fatal rather than a warning, for the same
# reason the Mac's emulator fetch is: a bundle that cannot emulate is a
# broken artifact, and the way this stayed hidden was a green build.
env:
GH_TOKEN: ${{ github.token }}
run: |
set -e
gh release download radioserver-v1 --repo MeshBench/meshcore-native \
--pattern 'radioserver-linux-amd64' --dir dist/meshbench --clobber \
|| { echo "::error::no radioserver-v1 release for meshcore-native"; exit 1; }
mv dist/meshbench/radioserver-linux-amd64 dist/meshbench/radioserver
chmod +x dist/meshbench/radioserver
dist/meshbench/radioserver 2>&1 | head -1 || true

- name: Fonts and fixtures
run: |
set -e
Expand Down Expand Up @@ -185,6 +207,11 @@ 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.

Native MeshCore builds and board images are downloaded on first use and
cached under ~/.cache/meshcoresim. Nothing else is needed.

Expand Down Expand Up @@ -242,7 +269,12 @@ jobs:
APP=dist/AppDir
mkdir -p $APP/usr/bin $APP/usr/share/applications \
$APP/usr/share/metainfo $APP/usr/share/meshbench
cp dist/meshbench/meshbench $APP/usr/bin/
# 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
# .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 -r dist/meshbench/fixtures dist/meshbench/fonts \
dist/meshbench/LICENCES $APP/usr/share/meshbench/
cp packaging/meshbench.desktop $APP/usr/share/applications/
Expand Down Expand Up @@ -279,7 +311,7 @@ jobs:
mkdir -p $d
cp packaging/icons/meshbench-${px}.png $d/io.github.meshbench.meshbench.png
done
cp $APP/usr/bin/meshbench $DEB/usr/bin/
cp $APP/usr/bin/meshbench $APP/usr/bin/radioserver $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 @@ -381,6 +413,19 @@ jobs:
echo "fetching $url"
curl -fsSL -O "$url"
}
# meshcore-native's "latest" is whichever per-role MeshCore version
# was published most recently, which is not this. radioserver-v1
# is a fixed tag of its own, so it is fetched by tag rather than by
# riding a /latest that moves out from under it within a day.
fetchTag() {
url=$(curl -fsSL "https://api.github.com/repos/$1/releases/tags/$2" \
| python3 -c "import sys,json,fnmatch; r=json.load(sys.stdin); print(next((a['browser_download_url'] for a in r['assets'] if fnmatch.fnmatch(a['name'], '$3')), ''))")
if [ -z "$url" ]; then
echo "::error::no asset matching $3 in $1's $2 release"; exit 1
fi
echo "fetching $url"
curl -fsSL -O "$url"
}
APP=dist/macos/MeshBench.app
mkdir -p "$APP/Contents/Resources/emulators"
cd "$APP/Contents/Resources/emulators"
Expand All @@ -394,6 +439,11 @@ jobs:
ln -sf ../Resources/emulators/qemu/bin/qemu-system-xtensa "$APP/Contents/MacOS/qemu-system-xtensa"
[ -f "$APP/Contents/Resources/emulators/Release/osx-arm64/Renode" ] && \
ln -sf ../Resources/emulators/Release/osx-arm64/Renode "$APP/Contents/MacOS/renode"
# The radio model. A real file rather than a link: it is 40 KB and
# resolves nothing from its own path.
(cd "$APP/Contents/MacOS" && fetchTag MeshBench/meshcore-native radioserver-v1 radioserver-darwin-arm64)
mv "$APP/Contents/MacOS/radioserver-darwin-arm64" "$APP/Contents/MacOS/radioserver"
chmod +x "$APP/Contents/MacOS/radioserver"
# Signing has to happen again: the bundle was signed before these
# arrived, and adding files to a signed bundle invalidates it.
codesign --force --deep --sign - "$APP"
Expand Down Expand Up @@ -485,6 +535,17 @@ jobs:
for f in *.zip; do [ -e "$f" ] || continue; unzip -q "$f" && rm "$f"; done
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.
cd dist/meshbench
gh release download radioserver-v1 --repo MeshBench/meshcore-native \
--pattern 'radioserver-windows-amd64.exe' --dir . --clobber \
|| { echo "::error::no radioserver-v1 release for meshcore-native"; exit 1; }
mv radioserver-windows-amd64.exe radioserver.exe
cd ../..
file dist/meshbench/radioserver.exe
cat > dist/meshbench/README.txt <<'TXT'
MeshBench for Windows

Expand All @@ -493,10 +554,17 @@ jobs:
says which build this is.

Native MeshCore firmware is downloaded on first use and cached.
The QEMU and Renode emulators are in this zip, so emulated ESP32
and nRF52 boards work out of the box. They are MeshBench builds,
not upstream ones - ours carry an SX1262 device and a SEVONPEND
fix that published firmware needs.

The QEMU and Renode emulators are in this zip, with radioserver.exe,
the SX1262 model they both clock over a socket. All three are found
automatically. The emulators are MeshBench builds and not upstream
ones - ours carry an SX1262 device and a SEVONPEND fix that
published firmware needs.

Emulated boards on Windows are newer than the rest of this bundle
and have had less time on real hardware than the Linux and macOS
ones. If a board will not start, meshbench.exe workbench run from a
terminal prints what it could not find.

Licences are in LICENCES\, and in the application under
Help > Licences & attributions.
Expand Down
29 changes: 19 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ sudo apt install ./meshbench_*_amd64.deb
meshbench workbench # or find MeshBench in the applications menu
```

**Tarball** — the same application plus the QEMU and Renode emulators, for
emulating real board firmware offline:
**Tarball** — the same application plus the QEMU and Renode emulators and the
radio model they clock, for emulating real board firmware offline. The
AppImage and the `.deb` carry the radio model but not the emulators, which are
110 MB of the tarball's size:

```bash
tar xzf meshbench-linux-x86_64.tar.gz
Expand Down Expand Up @@ -80,8 +82,10 @@ Unzip `meshbench-*-windows-x86_64.zip` anywhere and run `meshbench.exe`.
Windows SmartScreen will warn about an unrecognised publisher for the same
reason macOS does — the binary is unsigned. Click **More info → Run anyway**.

Emulated ESP32 and nRF52 boards work on Windows, but the emulators are not in
the zip yet; native firmware, the channel, terrain and planning all are.
The zip carries the QEMU and Renode emulators and the radio model, so emulated
ESP32 and nRF52 boards need nothing else installed. That path is newer on
Windows than on Linux and macOS: if a board will not start, run
`meshbench.exe workbench` from a terminal and it prints what it could not find.

### What arrives later, over the network

Expand Down Expand Up @@ -230,11 +234,15 @@ at all.

### Setting one up today

Until this is packaged, an emulated node needs binaries that are not on any
distribution: a QEMU carrying our SX1262 device for the ESP32 boards, a Renode
carrying the SEVONPEND fix for the nRF52 ones, and the radio model both talk to.
Put them where the application looks and nothing else is needed — no environment
variables, no flags.
**From a release you do not have to do any of this.** The tarball, the dmg and
the Windows zip carry all three binaries beside the application, where it looks
for them first.

Building from source is the case below. An emulated node needs binaries that
are on no distribution: a QEMU carrying our SX1262 device for the ESP32 boards,
a Renode carrying the SEVONPEND fix for the nRF52 ones, and the radio model
both talk to. Put them where the application looks and nothing else is needed —
no environment variables, no flags.

```bash
mkdir -p ~/.cache/meshcoresim/tools
Expand All @@ -250,7 +258,8 @@ 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` against `VirtualSX1262.cpp` with no other dependencies.
`meshcore-native` with `./build.sh radioserver out` — 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
it. The library only offers boards with verified wiring, so anything it lists
Expand Down
23 changes: 16 additions & 7 deletions docs/tools-manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,28 @@ them dates the installer.

## What a released bundle ships

Linux only for now, by decision: mac and Windows are skipped matrix rows with
reasons in the file, not absences.
All three platforms, as of the packaging work: a Linux tarball, an arm64 dmg
and a Windows zip.

meshbench the application
qemu-system-xtensa beside it, or symlinked
renode beside it, or symlinked
radioserver beside it

A **symlink** is correct for QEMU and wrong for a copy: it resolves its own path
to find its data files, so a bare copy of the binary will not run.

Renode is not bundled yet. It is a 60 MB portable package with its own runtime,
needed only for the nRF52 boards, and the decision on whether that belongs in
the default download is open.
to find its data files, so a bare copy of the binary will not run. The Windows
zip cannot carry a symlink at all, so nothing is linked there and `lookupTool`
searches the emulators' own unpacked layouts — `qemu/bin/` and the versioned
`renode_*-portable/` — instead.

**radioserver is the one every emulated node needs**, ESP32 or nRF52, and it is
looked up before either emulator. Nothing built it until `build.sh` grew a
`radioserver` target, which is how 0.0.1 through 0.0.3 shipped both emulators
and no radio.

The AppImage and the `.deb` carry the application and radioserver but not the
emulators: those are 110 MB against a 26 MB AppImage, and the tarball is the
batteries-included download for people who want them.

## What is checked, and when

Expand Down
49 changes: 49 additions & 0 deletions internal/firmware/emulated_lookup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package firmware

// White-box, because lookupTool is where the bundles are found or not found and
// the layouts it has to know about are not visible from outside the package.

import (
"os"
"path/filepath"
"runtime"
"testing"
)

// Renode unpacks into renode_<version>-portable/, and a zip cannot carry the
// symlink the Linux tarball and the macOS bundle use. The Windows bundle
// therefore ships an emulator that was found by nobody until the versioned
// directory was searched too: the workbench reported "renode not found" with
// renode.exe sitting two directories away.
func TestLookupToolFindsRenodeInItsVersionedDirectory(t *testing.T) {
self, err := os.Executable()
if err != nil {
t.Skipf("no executable path here: %v", err)
}
name := "renode"
if runtime.GOOS == "windows" {
name += ".exe"
}
// Beside the test binary, because that is the directory lookupTool looks
// in - the same rule the shipped binary is subject to.
dir := filepath.Join(filepath.Dir(self), "renode_1.16.1-portable")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Skipf("cannot write beside the test binary: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
bin := filepath.Join(dir, name)
if err := os.WriteFile(bin, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}

// The environment variable would short-circuit the search this is testing.
t.Setenv(EnvRenode, "")

got, err := lookupTool(EnvRenode, "renode")
if err != nil {
t.Fatalf("renode not found with %s present: %v", bin, err)
}
if got != bin {
t.Errorf("found %s, wanted the bundled %s", got, bin)
}
}
12 changes: 11 additions & 1 deletion internal/firmware/emulated_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ func (e *EmulatedNode) Start(ctx context.Context, bridge string) error {
if err := os.MkdirAll(e.Dir, 0o755); err != nil {
return err
}
// A TCP port on Windows, a Unix socket everywhere else.
// A Unix socket for QEMU on Linux and macOS; a TCP port for Renode
// anywhere, and for either emulator on Windows.
//
// Renode has always used a port: it runs on Mono, whose Unix domain
// socket support is not worth betting a node on. QEMU used a socket file,
Expand Down Expand Up @@ -347,6 +348,15 @@ func lookupTool(env, name string) (string, error) {
subdirs := []string{"", "qemu/bin", "qemu-meshbench/bin"}
if self, err := os.Executable(); err == nil {
dir := filepath.Dir(self)
// Renode unpacks into a directory carrying its version, so the name
// changes with every release and cannot be listed above. Globbing for
// the shape is what the Linux tarball's symlink step already does;
// this is the same rule on the side that has to find it.
if matches, err := filepath.Glob(filepath.Join(dir, "renode*-portable")); err == nil {
for _, m := range matches {
subdirs = append(subdirs, filepath.Base(m))
}
}
for _, sub := range subdirs {
for _, cand := range candidates {
if p := filepath.Join(dir, sub, cand); fileExists(p) {
Expand Down
Loading