Repository navigation
Pre-release: the human test pass #318
Description
A human pass over everything, before the first release. Each scenario has steps and an expected result; tick what holds, and leave a comment on this issue for anything that does not, with the scenario number.
Docs for reference while testing: https://meshbench.github.io/docs/
1. Install and first launch
- Download the latest release artefact for Linux,
chmod +x, run it. - Expect: the workbench opens with no other installation, warms the default fixture, and the status bar carries the best-case honesty line.
- With
MESHBENCH_CARTO_KEYset in the environment (yours is set in fish): expect the CARTO dark basemap by default, no "API KEY REQUIRED" watermark anywhere while panning and zooming. - Unset the key and relaunch: expect OpenStreetMap tiles by default and nothing broken.
- passes as described
2. Your first simulation (the docs path)
Follow https://meshbench.github.io/docs/first-simulation.html literally, as if new.
- File, Open a saved network,
fixture-fife-strict. - Simulation, Start firmware on every node; watch the count reach 58.
- Press play; double-click a repeater, and on its Console tab type
advert. - Expect: the flood spreads on the map, the Events panel fills with causes, nothing in the doc's wording disagrees with what the screen shows.
- passes as described
3. Debugging a missed packet
- In the run from scenario 2, open the Debug view.
- Pick a failed reception in the Inspector; read its cause.
- Use the map's link tool on that pair; expect the terrain cut-through with both directions' margins, and the cause to be consistent with what the cut-through shows.
- passes as described
4. Waveform mode
- Configuration, RF Simulation: switch to waveform. Expect the status bar to name the mode and the run to continue without cutting a frame.
- Let two nearby repeaters relay the same flood; in Debug, expect collision or capture verdicts that calculated mode would not produce.
- Switch back to calculated; expect the mode stamped correctly again.
- passes as described
5. SDR observer
- Open the Glasgow SDR Observer node window, SDR pane, serve rtl_tcp.
- Connect SDR++ with the documented settings (https://meshbench.github.io/docs/sdr-observer.html).
- Expect chirp bursts at channel width while playing, the bare noise floor while paused, and the stream surviving a drag of the observer across the map.
- passes as described
6. Importing a real network
Follow https://meshbench.github.io/docs/importing.html with the ScotMesh CoreScope URL, GUI tab only.
- Add areas, fetch, commit, read traffic, apply regions - the numbered buttons in order.
- Expect the counts reported at each step, regions applied to a sensible number of nodes, and a flood that actually relays afterwards.
- Deliberately skip "apply regions" on a fresh import and confirm the mesh transmits but does not relay - then check the page's "If nothing relays" checklist catches it.
- passes as described
7. Firmware development loop
meshbench dev -from <your MeshCore checkout>; expect the build to land in the firmware library named after the branch and assigned to repeaters.- Bench view: define a two-arm sweep, your build against the release, three seeds, a control arm.
- Expect: per-arm results with
delivered,redundant,rx_spread,at_risk_2db; the control arms identical; the run log readable while it goes.
- passes as described
8. A companion app against the mesh
- App view, Companion bench: give me a mesh and an endpoint (TCP).
- Connect
meshcore-clior your own client; expect real protocol bytes,infosanswered. - Press "drop every client connection"; expect the client to see a dead socket, and reconnecting to work after serving again.
- passes as described
9. One emulated board
- Assign the
Generic_E22_sx1262published image to a single node (one board at a time; they are heavy). - Expect it to boot, advert on the air unprompted, and appear in the events as a transmitter; console reachable and answering
get name.
- passes as described
10. Scripting and the clients
- Run cookbook example 3 (small mesh with traffic) in Python: expect it to build the mesh, run, and print event counts.
- On the docs site, flip a ways block to Python; expect every page to remember the choice.
meshbench test -fixture fixtures/fixture-fife-strict.json -junit out.xml: expect assertions checked, JUnit written, exit code 0.
- passes as described
11. Determinism spot-check
- Run the same fixture and seed twice (native firmware), same duration.
- Expect identical event totals both times.
- passes as described
12. Docs site and READMEs, as a reader
- Read the Learn path start to finish (Overview through Reading a result); note anything unclear or wrong as a comment here.
- Try search ("/"), the on-page rail, a phone-width window, and both light and dark themes; expect the white logo on its dark rail everywhere.
- Look at both repository READMEs on GitHub in dark and light themes; expect the card on the main repo, the theme-aware white logo on the docs repo, badges alive, no broken links.
- passes as described
Part two: every window, one picture each
The twelve scenarios above are the journeys. This part is the inventory: every panel, window, tab, section and menu the application has, so the pass can say it covered all of them rather than the ones somebody thought of.
Run the sweep, which drives the binary once per step and writes one picture per step:
go build -o meshbench ./cmd/meshbench
tools/shots/shots.py # every step, into ./shots
tools/shots/shots.py panels # or one bucket at a time
tools/shots/shots.py --list # what it would run
internal/ui/workbench/shotsteps_test.go checks the manifest against the application's own panel table, so a panel added without a step is a red build rather than a picture nobody took. Tick a bucket when every picture in it holds; comment with the step name for any that does not.
Views (6)
Each view is its own arrangement. The switcher should mark the current one.
| picture | what it is | expected |
|---|---|---|
view-plan.png |
The Plan view | the view's own panels are docked and drawn, and the view switcher marks Plan as current |
view-run.png |
The Run view | the view's own panels are docked and drawn, and the view switcher marks Run as current |
view-debug.png |
The Debug view | the view's own panels are docked and drawn, and the view switcher marks Debug as current |
view-validate.png |
The Validate view | the view's own panels are docked and drawn, and the view switcher marks Validate as current |
view-bench.png |
The Bench view | the view's own panels are docked and drawn, and the view switcher marks Bench as current |
view-app.png |
The App view | the view's own panels are docked and drawn, and the view switcher marks App as current |
- every picture in this bucket holds
Panels (72)
Each panel twice: filling the window, and popped out into a window of its own. A panel that draws docked and not in its own window is a fault this window set has had.
| picture | what it is | expected |
|---|---|---|
panel-boards.png |
The Boards panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-boundary.png |
The Boundary panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-budget.png |
The Budget panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-companion-bench.png |
The Companion bench panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-compare.png |
The Compare panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-configuration.png |
The Configuration panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-console.png |
The Console panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-energy.png |
The Energy panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-events.png |
The Events panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-experiment-log.png |
The Experiment log panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-firmware.png |
The Firmware panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-fleet.png |
The Fleet panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-import.png |
The Import panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-inspector.png |
The Inspector panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-licences.png |
The Licences panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-link.png |
The Link panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-live-feed.png |
The Live feed panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-logs.png |
The Logs panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-map.png |
The Map panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-matrix.png |
The Matrix panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-nodes.png |
The Nodes panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-nodes-running.png |
The Nodes running panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-packet.png |
The Packet panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-packet-timeline.png |
The Packet timeline panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-planning.png |
The Planning panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-provisioning.png |
The Provisioning panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-resources.png |
The Resources panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-results.png |
The Results panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-runs.png |
The Runs panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-schedule.png |
The Schedule panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-scoreboard.png |
The Scoreboard panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-setup.png |
The Setup panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-sweep.png |
The Sweep panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-timelines.png |
The Timelines panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-validate.png |
The Validate panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
panel-waterfall.png |
The Waterfall panel, filling the window | the panel draws its own content, with no empty region and no placeholder text |
popout-boards.png |
The Boards panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-boundary.png |
The Boundary panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-budget.png |
The Budget panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-companion-bench.png |
The Companion bench panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-compare.png |
The Compare panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-configuration.png |
The Configuration panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-console.png |
The Console panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-energy.png |
The Energy panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-events.png |
The Events panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-experiment-log.png |
The Experiment log panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-firmware.png |
The Firmware panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-fleet.png |
The Fleet panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-import.png |
The Import panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-inspector.png |
The Inspector panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-licences.png |
The Licences panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-link.png |
The Link panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-live-feed.png |
The Live feed panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-logs.png |
The Logs panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-map.png |
The Map panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-matrix.png |
The Matrix panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-nodes.png |
The Nodes panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-nodes-running.png |
The Nodes running panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-packet.png |
The Packet panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-packet-timeline.png |
The Packet timeline panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-planning.png |
The Planning panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-provisioning.png |
The Provisioning panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-resources.png |
The Resources panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-results.png |
The Results panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-runs.png |
The Runs panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-schedule.png |
The Schedule panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-scoreboard.png |
The Scoreboard panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-setup.png |
The Setup panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-sweep.png |
The Sweep panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-timelines.png |
The Timelines panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-validate.png |
The Validate panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
popout-waterfall.png |
The Waterfall panel popped out | a window of its own with a title bar, draggable, and the same content as the docked panel |
- every picture in this bucket holds
The node window (10)
One capture per tab.
| picture | what it is | expected |
|---|---|---|
node-console.png |
A node's own window on its Console tab | the window opens on Console with that tab's content drawn |
node-companion.png |
A node's own window on its Companion tab | the window opens on Companion with that tab's content drawn |
node-sdr.png |
A node's own window on its SDR tab | the window opens on SDR with that tab's content drawn |
node-settings.png |
A node's own window on its Settings tab | the window opens on Settings with that tab's content drawn |
node-radio.png |
A node's own window on its Radio tab | the window opens on Radio with that tab's content drawn |
node-stats.png |
A node's own window on its Stats tab | the window opens on Stats with that tab's content drawn |
node-activity.png |
A node's own window on its Activity tab | the window opens on Activity with that tab's content drawn |
node-connect.png |
A node's own window on its Connect tab | the window opens on Connect with that tab's content drawn |
node-hardware.png |
A node's own window on its Hardware tab | the window opens on Hardware with that tab's content drawn |
node-output.png |
A node's own window on its Output tab | the window opens on Output with that tab's content drawn |
- every picture in this bucket holds
The board view (3)
Both tables, and the console with its frames decoded.
| picture | what it is | expected |
|---|---|---|
board-radio.png |
The board view's Radio table | the board's panel on the left, the Radio table with a verdict on every row, and the console along the bottom |
board-wiring.png |
The board view's Wiring table | the board's panel on the left, the Wiring table with a verdict on every row, and the console along the bottom |
board-decoded.png |
The console with its frames decoded — then tick decode in the console strip | the decode tick is on and framed traffic reads as prose, with the plain boot log still beside it |
- every picture in this bucket holds
Configuration (6)
One capture per section.
| picture | what it is | expected |
|---|---|---|
config-general.png |
Configuration: the general section | the section opens with its own settings and each carries the caption that says why it matters |
config-rf.png |
Configuration: the rf section | the section opens with its own settings and each carries the caption that says why it matters |
config-terrain.png |
Configuration: the terrain section | the section opens with its own settings and each carries the caption that says why it matters |
config-traffic.png |
Configuration: the traffic section | the section opens with its own settings and each carries the caption that says why it matters |
config-provisioning.png |
Configuration: the provisioning section | the section opens with its own settings and each carries the caption that says why it matters |
config-resources.png |
Configuration: the resources section | the section opens with its own settings and each carries the caption that says why it matters |
- every picture in this bucket holds
Licences (5)
One capture per section.
| picture | what it is | expected |
|---|---|---|
licences-forks.png |
Licences: forks | every row names a component, its licence and its origin |
licences-bundled.png |
Licences: bundled | every row names a component, its licence and its origin |
licences-golibs.png |
Licences: golibs | every row names a component, its licence and its origin |
licences-runtime.png |
Licences: runtime | every row names a component, its licence and its origin |
licences-data.png |
Licences: data | every row names a component, its licence and its origin |
- every picture in this bucket holds
Menus (7)
Every dropdown. One entry lives in one menu.
| picture | what it is | expected |
|---|---|---|
menu-file.png |
The File menu, open | the menu drops with its sections, icons and shortcuts, and one entry appears in one menu only |
menu-view.png |
The View menu, open | the menu drops with its sections, icons and shortcuts, and one entry appears in one menu only |
menu-simulation.png |
The Simulation menu, open | the menu drops with its sections, icons and shortcuts, and one entry appears in one menu only |
menu-mesh.png |
The Mesh menu, open | the menu drops with its sections, icons and shortcuts, and one entry appears in one menu only |
menu-analysis.png |
The Analysis menu, open | the menu drops with its sections, icons and shortcuts, and one entry appears in one menu only |
menu-window.png |
The Window menu, open | the menu drops with its sections, icons and shortcuts, and one entry appears in one menu only |
menu-help.png |
The Help menu, open | the menu drops with its sections, icons and shortcuts, and one entry appears in one menu only |
- every picture in this bucket holds
Part three: the documentation, as a reader
The manual is part of the change, so a release is not tested until the pages have been walked against the build. A page describing a view the application no longer has is worse than no page, because somebody will follow it.
Published at https://meshbench.github.io/docs/. Walk them with:
tools/shots/shots.py docs
It opens each page in turn and prints what the page is held to. Capture anything that disagrees with the application and comment with the step name. shotsteps_test.go reads the site's own navigation table, so a page published without a step here is a red build.
Follow the guides literally, in order, as if new. That is the only way a missing step shows up: somebody who knows the application fills the gap without noticing it is there.
| page | what it is | held to |
|---|---|---|
index |
Learn - Overview | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
concepts |
Learn - What is real | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
getting-started |
Learn - Getting started | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
first-simulation |
Learn - Your first simulation | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
results |
Learn - Reading a result | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
fixtures |
Use - Shipped networks | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
importing |
Use - Importing a network | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
debugging |
Use - Debugging packet delivery | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
firmware-library |
Use - Firmware library | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
firmware-development |
Use - Firmware development | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
board-view |
Use - The board view | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
testing-repeaters |
Use - Testing a repeater | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
app-development |
Use - App development | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
testing |
Use - Testing your own code | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
experiments |
Use - Running an experiment | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
studies |
Use - Studies | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
sdr-observer |
Use - Listening with SDR++ | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
architecture |
Understand - Architecture | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
native-vs-emulated |
Understand - Native and emulated | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
rf-simulation |
Understand - RF simulation | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
rf-chain |
Understand - The RF chain | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
waveform |
Understand - Waveform mode | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
timing |
Understand - Time and determinism | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
golden-vectors |
Understand - Golden vectors | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
emulation |
Understand - Emulation | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
firmware-integration |
Understand - Running real firmware | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
what-it-does-not-do |
Understand - Accuracy and limits | every entry matches the build: it is generated, so the check is that nothing here is missing or stale, not that the prose reads well |
scripting |
Automate - Scripting a session | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
cookbook |
Automate - Scripting cookbook | every step can be followed literally, in order, and the application does what the page says at each one - no step names a control that is not there, and no screenshot shows a view that has changed |
agent-skills |
Automate - Agent skills | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
reference-cli |
Automate - CLI | every entry matches the build: it is generated, so the check is that nothing here is missing or stale, not that the prose reads well |
reference-control |
Automate - Control socket | every entry matches the build: it is generated, so the check is that nothing here is missing or stale, not that the prose reads well |
reference-python |
Automate - Python client | every entry matches the build: it is generated, so the check is that nothing here is missing or stale, not that the prose reads well |
reference-go |
Automate - Go client | every entry matches the build: it is generated, so the check is that nothing here is missing or stale, not that the prose reads well |
reference-js |
Automate - Node client | every entry matches the build: it is generated, so the check is that nothing here is missing or stale, not that the prose reads well |
settings |
Reference - Settings | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
resources |
Reference - What gets downloaded | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
tools |
Reference - External tools | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
quality-gates |
Reference - What the build enforces | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
repositories |
Reference - Repositories and licences | what it claims is true of the build in front of you, and every screenshot on it is recognisable in the application |
- every page holds
Anything that fails: comment with the scenario number, what you did, what you expected, and what happened. Screenshots welcome.
Activity
A13xB0 commentedon Sep 1, 2026
Claude's pass, part 1 (scenarios 1-4), with notes
Driven through the control socket against a windowed build, screenshotting each step. Scenarios 1-4 done here; 5-12 need things this machine or this pass cannot supply (noted at the end). Notes only - not opening issues yet, per the plan to file them after both our passes.
1. Install and first launch - PASSES
Default fixture warms (378 nodes, seed 9001), and the status bar carries the honesty line verbatim:
results are a best case: no multipath, bare earth, ideal demodulator. CARTO dark basemap is default with(c) OpenStreetMap contributors, (c) CARTOattribution and no API-key watermark while panning. I did not test the key-unset relaunch (step 4).2. First simulation - PASSES, two notes
Opened
fixture-fife-strict, firmware started, played,advertat Abernethy Repeater answered-> OK - Advert sent, flood spread, Events filled (17.9k events, Sent 1834 / Received 477).firmware.statereturns{nodes: 56, running: 56, total: 58}- the SDR observer and the emitter never boot firmware. The app is honest about this ("56 on MeshCore", "56 nodes running firmware"); it is this checklist and the docs page that say 58. Anyone following it literally waits for a number that never arrives.fit. This is the exact moment a newcomer decides nothing happened. Worth fitting on open, or at least on first play.3. Debugging a missed packet - PASSES on substance, two UI faults
The causes are genuinely good. Misses carry real physics:
its own transmitter was keyed; LoRa is half duplexits demodulator was already locked to <node>; a LoRa receiver decodes one packet at a timedecoded its header, then 133 symbol(s) were destroyed by a collisionwould have decoded at 15.0 dB, lost to a stronger interfererThe cut-through is excellent: terrain in green, first Fresnel zone as the band, both directions' margins side by side, plus
CLEAR. The tightest point is 3.5 km along, with 11 m of clearance - 95% of the first Fresnel radius. Terrain is not what limits this path; if it does not work, the answer is in the link budget rather than the profile.That verdict is consistent with the miss causes above (congestion, not terrain), which is what the scenario asks for.flooris a catch-all and mislabels congestion as weak signal.EventClass(internal/sim/engine/events.go:137) prefix-matches the detail string and returnsClassFlooras itsdefault. So receiver-lock misses and collision misses are both counted asfloor- "simply too quiet". The comment directly above the constants states the intent it breaks: "a miss that was simply too quiet ... three different problems with three different fixes". Measured on one fife run: of 357 misses, 37 were classedfloorand not one of them was a floor miss - they were receiver lock and collisions. This matters because the cards and filter chips count by class: a user reads "floor" and buys antennas when the actual fix is fewer or rescheduled transmissions. Wrong conclusion, in the direction the project cares most about.4. Waveform mode - PASSES, and better than asked
Switching to waveform stamps
waveform RFin the status bar and the run continues uninterrupted. The nice detail: the honesty line itself changes, from... bare earth, ideal demodulatorto... bare earth, real coding chain- it drops the caveat that no longer applies. Switching back restorescalculated RFand the original wording. I did not get two repeaters to relay the same flood to compare collision/capture verdicts against calculated mode (step 2).Smaller notes
Abernethy Rep...,sco-tay-moncr...), and several similar names collide once cut.nodes.select_manytakes a bare list. Handed{"names": [...]}it matches neither case in its type switch, silently deselects everything, and returns{"selected": null}as success. A silent no-op that looks like it worked.rf.modeis setter-only - calling it with no argument errors instead of returning the current mode, so a script cannot ask what mode it is in.workspace.setwith a bad view answersno view ""; there is: Plan, Run, Debug, Validate, Bench, App.Not covered
5 (SDR++), 8 (meshcore-cli), 12 (docs site as a reader) need external apps. 9 (emulated board) is blocked here:
radioserver not found- it ships from our QEMU fork and is not installed on this machine. 7, 10, 11 not reached yet; 11 is at least partly covered byTestRadioStackIsDeterministic, which passes.A13xB0 commentedon Sep 1, 2026
Claude's pass, part 2 (scenarios 10 and 11)
11. Determinism spot-check - PASSES
Two runs of
fixture-fife-permissiveat the shipped seed produced 162 unique deliveries both times, and the two logs are byte-identical once the wall-clock timing line is removed.TestRadioStackIsDeterministicagrees at the frame level.10. Scripting and the clients - step 3 FAILS
meshbench test -fixture fixtures/fixture-fife-strict.json -junit out.xmldoes check assertions and does write valid JUnit, but it exits 1, not 0, because the shipped fixture fails its own assertion:Not a flake, and not the harness: the same command on
fixture-fife-permissivepasses with 162 unique deliveries and exits 0. So the machinery works and it isfife-strictspecifically that delivers nothing.fixture-fife-strictfails its own assertion. Zero of six public messages are delivered where the fixture asks for ten. The two fixtures differ in what they schedule as well as how strict they are: permissive floods adverts from many repeaters first, strict sends only three companions'public hellowith no advert beforehand, so there may be no learned route for anything to travel. Whether the right fix is the assertion, the schedule, or the region rules needs someone who knows what the fixture was written to demonstrate. Either way a fixture that ships in the repository and fails its own check is not something to release.meshbench test.grepover.github/workflowsfinds no invocation. The assertions in the shipped fixtures are therefore never enforced, which is exactly how one drifts to zero deliveries unnoticed. This is the same shape as the always-red scheduled job in [Pre-release] Three live-firmware tests fail against real nodes, one possibly behavioural #373: an assertion nobody runs and an assertion that always fails are worth the same.I did not do step 1 (Python cookbook example 3) or step 2 (docs-site language toggle).
Running total
5, 8, 12 need external applications. 9 is blocked here (
radioserver not found, it ships from our QEMU fork). 6 and 7 not reached.Ten notes so far, N1 to N10. Two are code bugs I would want fixed before release (N3 floor misclassification, N9 the failing shipped fixture), one is a visible UI fault (N4 the Link panel docked), and N10 is the process gap that lets N9 hide.
A13xB0 commentedon Sep 1, 2026
Claude's pass, part 3: a genuinely fresh install
Re-ran scenario 1 properly, from an empty cache.
XDG_CACHE_HOMEandXDG_CONFIG_HOMEpointed at an empty directory, so this is what a first-time user gets, not what my warmed machine gets. Screenshots are of the workbench window alone rather than the desktop.N11 (the big one): first launch downloads 513 MB of terrain, unprompted
Opening the application and touching nothing pulls 513 MB across 6,233 terrain files. Measured twice from two independent empty caches, and it came to 513 MB on the nose both times, so it is the default network's fixed cost rather than a coincidence.
terrain/tiles/matrix/firmware/There is no consent step, no size shown beforehand, and no way to decline. It happens because a plain launch warms
scotland-ireland-strict, a 378-node national network with 71,253 links, and every link wants ground under it. On a metered connection, a phone hotspot, or a slow line, half a gigabyte arrives before the user has decided they want the application.The docs understate this.
getting-started.htmlsays the cache "grows with the firmware builds and map tiles you use" and budgets "200 MB for the bundle" - terrain is not mentioned, and it is by far the largest thing. A docs fix is warranted whatever is decided about the behaviour.N12: the progress line names the wrong activity, which is why it reads as stuck
The first thing on screen is:
It is not lying and it does finish, in a couple of minutes on a fast line. But the time is not going into measuring: it is going into a 513 MB download that the line never mentions. So the percentage sits at or near zero while the actual work is invisible and unnamed, which is exactly the shape of a stall. On a slow link this would sit at "0% (0 of 71253)" for a very long time with nothing saying that anything is being fetched.
This exact lesson has already been learned once in the tree, which is what makes it worth fixing rather than arguing about. From
firmwarelib.go:The firmware download was fixed for precisely this. The terrain warm was not.
N13: a running download is not surfaced at all
With a firmware download in flight and
ui.statereportingjobs: 2, the status bar showed only4873 links, weighted by the weaker direction's margin. The download had nowhere to appear.N14: no progress is reachable programmatically
session.statusreturns the finished summary and never the running job.ui.statereturns a barejobs: 2with no name, no total and no progress. A script driving the workbench cannot tell what it is waiting for or how far along it is, which is the same gap the scripting notes already warn about for waits.Related, and still live:
ui.statestill reportedjobs: 2after the firmware download had completed and the file was on disk, which is the documented "finished jobs are only marked, not removed" trap.N15:
resource.listanswers with a count where the rows were the questionThe five rows are not reachable through the verb. This is the fifth instance of a shape already fixed in four other verbs, and it bit me directly: I could not find out what the resources were without reading the source.
N16: there is no way to obtain the emulator toolchain from the GUI
The Resources page has five providers, and I checked them in the source: SoftDevice, terrain tiles, basemap, building footprints, map tiles. None of them is Renode, QEMU or
radioserver. So a user cannot fetch the emulator toolchain from the application at all, and~/.cache/meshbench/tools/does not exist on this machine, including in the real non-test install. That is why the four emulated live tests fail here.To be fair to the docs,
tools.htmlis genuinely good on this: the four search locations and their order, the tool table with environment variables, why the versions are pinned (the SX1262 device, the SEVONPEND fix), and what each bundle ships. The gap is not knowledge, it is that a source checkout has no path to those tools at all - not automatic, not offered, not in the GUI - while the docs describe the cache directory as "where the installer puts anything it downloads after the fact". For someone running a development build, no installer ever does.Credit where it is due: when you do try to start an emulated node without them, the error is excellent. It names all the search locations, says what to do, and warns that a distribution build will not work because ours carries the SX1262 device.
What a user currently has to do, start to finish
radioserverbut not the emulators, at 110 MB against a 26 MB AppImage.radioserverfrommeshcore-nativewith./build.sh radioserver out, obtain a QEMU carrying our SX1262 device and configured--enable-gcrypt, and a Renode carrying the SEVONPEND fix.~/.cache/meshbench/tools/. Not onPATH: a desktop application inherits nothing useful from a shell.Steps 2 and 3 have no support in the application whatsoever.
Running total
Scenarios 1 to 4, 10 and 11 covered across the three parts. Sixteen notes, N1 to N16. The ones I would not release without deciding on are N3 (misclassified miss causes), N9 (a shipped fixture failing its own assertion), N11 (half a gigabyte with no consent) and N16 (emulated boards unobtainable outside a tarball release).
I have not brought an emulated board up, because the toolchain to do it does not exist on this machine and cannot be fetched from the application - which is itself the finding.
A13xB0 commentedon Sep 1, 2026
Claude's pass, part 4 (scenarios 5, 6, 7, 8, 9, 10 steps 1 and 2, and 12)
Driven through the control socket against a windowed build of
origin/mainat8a6c509, which stamps itselfdev+8eb4156. One mesh at a time. Every code reference below I re-checked againstmainafterwards and it still holds. Notes continue from N16.The headline: scenario 9 now passes - an emulated
Generic_E22_sx1262boots, adverts on the air unprompted and answers its console, three runs of three. Scenario 7 fails outright:meshbench devtruncates its own build to zero bytes whenever a workbench is running, so the firmware development loop does not work at all in the only situation it is used in. And there is a way to wedge the whole application permanently that I hit twice by accident (N29).9. One emulated board - PASSES, 3 runs of 3
N16 is answered:
~/.cache/meshbench/tools/now holdsradioserver,qemu-system-xtensaandrenode, and the new Setup page finds all three and says where.setup.checkreports"ready": 6, "missing": 1with only the Nordic SoftDevice outstanding, which is honest and exactly the page N16 asked for.Three nodes on a blank network,
Emuset toGeneric_E22_sx1262and pinned to the publishedsimple_repeater v1.17.0board image, the other two native. QEMU comes up with our own machine and radio wiring:It boots, provisions, and adverts unprompted:
nodes.statsputs it beyond doubt:{"name": "Emu", "backend": "emulated", "sent": 1, "last_sent_to": "Ears2", "irq_reads": 25860}, and both native nodesheard: 1fromEmu. The console answers:Three full runs from scratch, three adverts, three consoles answering. Given how flaky this has been I ran it three times before writing that down.
console.readon a node that has been running for minutes returns{"lines": 0}, becauseconsoleForattaches the buffer on first use and only collects from that moment. Everything interesting has already happened and is only in~/.cache/meshbench/nodefs/Emu/console.log, which no part of the application shows: the ROM banner,E (1723) SPIFFS: mount failed, -10025, the repeater ID, and the whole provisioning conversation. Somebody debugging a board that will not start opens the console and finds it empty.7. Firmware development loop - FAILS
N19 (bug, and the one I would fix first):
meshbench devdestroys the build it just made, whenever a workbench is running.Same command, same checkout, twice, differing only in whether a workbench was listening on the control socket:
firmware.Buildalready writes the binary into the cache atnative/<label>/<NativeBinaryName(role)>, and that is the path it reports.cmd_devthen hands that same path tofirmware.import, which callsfirmware.Import, which computesdstas exactly the same path and callscopyFile(src, dst, true).copyFileopens the source for reading and then opens the destinationO_WRONLY|O_CREATE|O_TRUNC- the same file - soio.Copyreads from a file it has already truncated. Zero bytes. It reports success, prints "0.6 MB" (measured before the import), and says "in the workbench's firmware library".Everything downstream then fails in a way that does not name the cause:
The fix looks like a same-file guard in
copyFile, or not re-importing a path that is already inside the cache. I have not opened a PR because the branch and title want anMSIM-<n>and I do not have one.Step 1 otherwise does what it says: the build is named after the branch (
study/07-ack-redundancybecomeslocal-study-07-ack-redundancy), lands in the library, and is assigned to every repeater.Step 2 and 3. Three arms -
mine(the local build),release(repeater-v1.17.0) andcontrol(alsorepeater-v1.17.0) - three seeds, two senders, onfife-permissive. The run log is genuinely readable while it goes;experiment.statereturns a rollinglogandsession.statuscarries{"job": {"id": "experiment", "done": 6, "total": 9, "what": "running arms"}}, which is the progress N14 said was unreachable. That part is better than it was.The results are not usable:
N20 (UX): a failed arm reads as a catastrophic result. All three
minecells errored with the N19 failure, and the per-arm table renders that asdelivered: 0besiderelease: 58.7with nothing on the row to say the arm never ran. The truth is only in the per-runerrfield and a top-levelwarning. Theat_risk_2dbkey is correctly absent rather than zero, which is exactly the distinction the code comments argue for - the same care has not reacheddelivered.N21 (bug): the control arm is not identical to the arm it duplicates, and the seed is not what separates the runs.
controlandreleaseare the same configuration and the same three seeds. They disagree on every column. Worse, look at the per-run rows:controlat seed 9002 and at seed 9003 are byte-identical. So arerelease9002 and 9003 in everything but collisions. Andcontrolat 9001 is identical toreleaseat 9003 - a different arm, a different seed, the same row. What varies is the position in the run order, not the seed.rx_spreadforcontrolis therefore0, which presents three runs that are really one measurement as perfect agreement.experiment.go's own header names this as the third of the three things that decide whether the numbers mean anything: "the seed must vary across runs of one arm, or the spread being reported is one draw repeated."N22 (bug):
firmware.setwith a role updates the view for every node. The engine-side loop honours the role filter; the world-side loop isso after
meshbench devthe status bar says46 nodes pinned to local-study-07-ack-redundancy(correct) whilenodes.listshows all 58 carrying it, including nine companions, the room server, the emitter and the SDR observer - two of which never boot firmware at all.6. Importing a real network - PASSES, with two faults
ScotMesh CoreScope,
Fifeas the area, the panel's numbered steps in order. Counts are reported at every step and they are good:And a flood then does relay. With regions applied before firmware starts,
advertatAbernethy Repeaterproduced one message carried by 22 distinct transmitters to 25 receivers; later floods on the same network reached 31 and 17 transmitters. That is the thing the scenario asks for and it works.infer.applysays 44 and the view shows 4. Reproduced on three separate imports. The verb's engine-side loop matches a node by public key or by name and applies to 44; the loop that updates the snapshot matches by name only (find(w.Nodes[i].Name, "")), and the comment beside it admits the compromise - "This keeps the live view roughly in step". The run is right and everything a person or a script can see is wrong:nodes.listreports regions on 4 of 59, andnodes.regionsanswers{"nodes": 59, "regions": null}. The importing page tells the reader to "confirm the count" at this exact step; the two counts available disagree by an order of magnitude, and the smaller one looks precisely like the failure the same page warns about.Deliberately skipping "apply regions". Fresh import, no inference, everything else the same. The result is more subtle than the page says:
a repeater's
advertstill floods, 22 distinct transmitters, exactly as with regions applied - adverts are unscoped;a companion's
publicmessage was transmitted once and received by nobody (1 transmitter, 0 receivers), against1 transmitter, 6 receiversfor the same command on the same network with regions applied.N24 (docs): "every node transmits and none relays" is only true of scoped traffic. From
importing.md: "Without this step every node transmits and none relays." The first thing anyone does after an import is press advert, and that floods twenty-two hops deep with no regions at all. So the page's own headline symptom does not appear, and the operator concludes the import is fine.N25 (docs): the "If nothing relays" checklist's item 2 does not do what it says. It reads "A quiet node with no regions. The total was healthy, one repeater was not. Ask it:
get repeaton its console." On a repeater holding no regions whatsoever,get repeatanswers-> > on, because it reports whether repeating is enabled and says nothing about regions.region list, the obvious next guess, answers-> Err - ??. So the checklist item that exists to find a node with no regions cannot find one. Items 1, 3 and 4 are sound.N26 (docs): the socket lane tells you to call a verb that refuses.
importing.md:219lists{"id":9,"method":"infer.result"}betweeninfer.runandinfer.apply. The live answer isinfer.result is the workbench's own callback, not a verb to call, a good refusal to a call the documentation asked for. It is registered withHandleInternaland gated insocket.go, so it can never be reached from outside.reference-control.md:68repeats the same dead step in its flow line,infer.run {hours:168} -> infer.result -> infer.apply. The::pythonand::golanes of the same block correctly leave it out, so it is only the socket lane in both places.8. A companion app against the mesh - PASSES
meshcore-cliis not installed on this machine, so I wrote a raw socket client instead.bench.servegave{"node": "Phone", "addr": "10.100.72.98:46251"}- a LAN address rather than loopback, which the code explains and which is right for pointing a phone at it.Sending
APP_STARTframed as'<', little-endian length, payload, the reply is real protocol:That is what
infosprints. Thenbench.dropanswered{"dropped": 1}and the client saw the socket die immediately:recvreturnedb'', and a write followed by a read also returnedb''. Serving again and reconnecting worked first time and returned the sameSELF_INFO.:46251and the second:37461. Correct on its own terms - the comment explains that a fixed port collides with the last run that has not finished dying - but it means "press stop, press serve" hands the operator a new address every time, and any client that was pointed at the old one has to be reconfigured rather than reconnected.5. SDR observer - PASSES on substance; SDR++ itself not driven
SDR++ is installed here (
/usr/bin/sdrpp), but it is a GUI application and this machine belongs to somebody who is using it, so I did not open it. Instead I connected a raw rtl_tcp client to the served observer, which tests everything except SDR++'s own settings.sdr.serveanswered{"node": "SDRObs", "addr": "127.0.0.1:41477", "rate_hz": 250000}and the stream begins with a genuine dongle-info header,52544c30 00000005 0000001d- magicRTL0, tuner 5, 29 gain steps - then IQ at 494 kB/s, which is 247 k complex samples a second against a declared 250 kHz.The playing-versus-paused difference is unmistakable once there is traffic. Six adverts fired during a 30 second sample, then a 15 second sample while paused:
Bursts well above the floor while playing, the bare floor while paused, in both calculated and waveform mode (waveform measured separately: 15.56 / 97.68 / 6.26% against 4.03 / 19.14 / 0%). And the stream survived being dragged: four
nodes.movecalls across the map while a client was attached, with no break and no change in rate.One thing worth knowing for anyone repeating this: a short sample with no traffic in it looks identical played and paused. My first attempt sampled 8 seconds after a single advert and measured
rms 3.59against3.56, which I very nearly wrote up as a defect. It was an empty window.nodes.movetakesnamewhere every other node verb takesnode.nodes.movewith{"node": ...}is refused withnodes.move needs a name: which node to move, which is a clear refusal, but it is the only verb of its family spelled that way.10 step 1. Cookbook example 3 in Python - PASSES
03_small_mesh_with_traffic.py, withMESHBENCH_BINARYpointed at the build under test, built the mesh, ran ten minutes of simulated time and printed:Exit code 0, and the run log ends
t= 600.610s run finished. Ten minutes and four seconds of wall clock for six hundred seconds of simulated time, so essentially realtime on this machine, on a four-node mesh.10 step 2. The docs site's language toggle - PASSES
Verified by driving headless Chrome against the generated site over
http://with a persistent profile, not by reading the JavaScript. Clickedpythononce onimporting.html: all six ways blocks on the page moved fromguitopythonandlocalStorage["meshbench-docs-way"]became"python". Then navigated tofixtures.html,debugging.htmlandtesting-repeaters.htmlwith no further clicks, and every one came up onpython, tabs and panes together. The choice is stored on click and re-applied on load, so it is remembered as the scenario asks.12. Docs site and READMEs, as a reader - PASSES WITH FAULTS
The Learn path reads well and most of what it asserts checks out against the tree. The interactive parts are in good shape and I could not fault them.
What is fine, said once each. Search on
/focuses the box without inserting the slash, "waveform" returns nine hits with correct deep links, arrow keys select, Escape closes and blurs, and/typed inside the box inserts normally. The on-page rail renders when a page has three or moreh2s, is present at 1440 px and correctly hidden below the 1120 px breakpoint, with scrollspy. At phone width (390 px, DPR 2) the nav goes static, thirty-eight links collapse behind a toggle witharia-expandedhandled properly, search stays usable, results fit the viewport, and none of eight pages checked overflowed horizontally; wide code scrolls inside its own container. In both themes the rail isrgb(20,19,31)and the logo renders byte-identically, so the white logo on its dark rail is right everywhere. There are no broken internal links or dead anchors in the generated HTML, andinfer.resultis the only verb the docs name that does not exist.reference-cli.md's tables matchcmd/meshbench/;fixtures.md's counts are exact for all three fixtures (58/46/9, 161/142/16, 378/336/39).And N11 is partly answered:
getting-started.mdnow says "roughly 500 MB of terrain that arrives on first launch" and "Budget about 1 GB for a first session", with a bandwidth warning and the-fixture ""escape. That is the docs half of N11 fixed.first-simulation.md:10andfixtures.md:20both open with "File, then Open a saved network" and then name a shipped fixture. That menu item runsproject.list, which reads onlyos.UserConfigDir()/meshbench/projects; nothing seeds the shipped fixtures into it and no menu item listsfixture.Embedded(). Measured live:project.listreturned{"dir": "/home/alex/.config/meshbench/projects", "projects": ["fixture-scotland-ireland-permissive", "fixture-scotland-ireland-strict"]}on a machine that has been used for months, andfixture-fife-strict, the one the page tells you to pick, is not among them. On a fresh install that picker is empty. This is the first instruction of scenario 2, and it only worked for me because I opened fixtures by path over the socket.testing.md's worked example is stale and names a node that does not exist. Line 154 showsnode Kirkcaldy Companion, 58 nodes running real MeshCore firmware. There is noKirkcaldy Companioninfixture-fife-strict;cmd_serve.gopicks the firstcompanion_radioin file order, which isAngusOutlaw1. The count is the N1 error again:firmware.statereturns 56 running of 58.app-development.md:13has the identical block correct, so this is a copy that did not get updated.first-simulation.md:17,fixtures.md:28andtesting-repeaters.md:94. The running count reaches 56 and the node total is 58, which is whatenginefirmware.go's own comment is about. Three pages tell the reader to wait for a number that does not arrive.concepts.md:96lists five node kinds and there are six. It omitsadvanced-repeater, which is a realKindand is present in every shipped fixture.fixtures.md:14lists all six, so the two pages disagree.docs/install.md:37in the main repo, whichgetting-started.mdlinks to twice as the authority, says "Needs glibc 2.34 or newer (Ubuntu 22.04, Debian 12, RHEL 9, Fedora 35 ...)". The pipeline builds against 2.35, andgetting-started.mditself says 2.35. Fedora 35 ships 2.34 and would not run it.getting-started.mddisagrees with its own next paragraph about what a release contains. Line 11 says "Releases ship builds for Linux (AppImage), macOS (dmg) and Windows (zip)"; step 1, four lines later, downloadsmeshbench-linux-x86_64.tar.gzand untars it. The.debthe pipeline publishes is never mentioned, and the dmg is Apple Silicon only while the page says just "macOS (dmg)". Given N16, which build a reader picks decides whether they can run an emulated board at all, so this one has consequences.README.md:49referencesdocs/workbench-plan.png; the file is atdocs/images/workbench-plan.png. Broken for every reader..../actions/workflows/ci.yml/badge.svgreturns 404 and renders broken, and the release badge returns 200 with the SVG titlerelease: no releases or repo not founddespite four real releases,v0.0.4being the latest. Both because shields.io and camo fetch anonymously and the repository is private. The three staticbadge/shields are fine. Worth deciding before the release whether to drop them or accept that they will come alive at the same moment the repository does.https://meshbench.github.io/docs/serves fine, but the clone URL, the releases page,docs/install.mdlinked fromgetting-started.md, and bothmeshbench-dev-skillsandmeshbench-scripting-skillsare private and give a reader outside the org a 404. OnlyMeshBench/meshbench-reportsresolves. That is a consequence of the repository still being private rather than a mistake, but it is what a reader meets today, and it is the same set of links a first release announcement would send people down.The docs repo README's
<picture>block is genuinely theme-aware and correct: the light fallback is a staged image that is fully visible on white, and the white PNG is only ever served to dark. The main README's header card exists and is tracked; its raw URL 404s anonymously and 200s with a token, which is private-repo behaviour rather than a missing asset. I could not check how either README actually renders on github.com in the two themes, because the repository is private and I did not open a browser session against it, so the badge and image behaviour there is inferred from status codes plus camo's known anonymous fetching.Two faults that are not any one scenario's
N29 (bug, and the worst thing here): replacing the network while firmware is running wedges the whole application, permanently. Twice out of two attempts, calling
project.new(File, New network) while 59 nodes were running firmware and the run was playing, then starting firmware on the new network, left the workbench answering nothing at all - not the control socket, not the window, no further line in the session log. I left the first one 25 minutes and the second about 10, and neither moved.SIGQUITsays exactly where:The store's own goroutine is inside a tick, waiting for a firmware bridge to acknowledge an advance.
WaitAdvanceends on an ack, on the node'sdonechannel, or on the context - and the tick callss.eng.Step(context.Background()), so there is no context to end and no timeout. One node that neither acks nor closes parks the goroutine that answers every verb and draws every frame. There is no way back except killing it.The counter-test: pausing and calling
node.stopon every node beforeproject.newmade the identical import-and-start sequence complete in 37 seconds. That was a three-node mesh rather than fifty-nine, so treat it as suggestive about the trigger rather than proof of it. What the stack does prove is the exposure: a single bridge that neither acks nor closes can park the goroutine that answers every verb, with no timeout to end it.N30 (bug): a workbench launched with a fixture name that resolves to nothing can never run anything.
-fixture ""printsloading: fixture: nothing namedand carries on.w.Tickis only ever installed byinstallBody, which nothing on that path calls, so pressing play setsplaying: trueand advancessim.state'snow_msforever while the engine is never stepped:eventsstays 0, nothing transmits, and typed console commands queue up unanswered.sim.stepis silent about it too - it returnsnow_msand does nothing whenw.Tickis nil.project.newinstalls the tick and fixes it, so a person who does File, New network never meets this; a script that launches with a mistyped-fixturegets a workbench that looks completely healthy and is inert. Measured:sim.statereportednow_ms: 390910while the engine's own clock was at 1800 ms.Smaller
nodes.list'ssentandheardare always zero. Nothing in the tree ever writesstate.Node.Sentor.Heard. Withnodes.statsreportingEmu sent 1and the Scoreboard panel drawingEmu 1,nodes.liststill returnedsent 0 heard 0for every node. The panels readNodeStatand are fine; it is the two fields the socket publishes that are dead. Same family as N7 and N15.boundary.set "Fife"returns two entries that are both{"kind": "administrative", "name": "Fife"}. Nothing distinguishes them, so a person choosing from that list is guessing.session.statusnow carries ajobobject withdone,totalandwhat, which closes most of N14.Running total
Every scenario has now been through a pass. Forty notes, N1 to N40.
The ones I would not release without a decision on, adding to N3, N9, N11 and N16 from the earlier parts:
meshbench devtruncates its own build to zero bytes whenever a workbench is running, which makes scenario 7 impossible as written and is a one-line same-file guard away from working.rx_spreadtherefore reports perfect agreement between runs that are really one measurement.The first three are things a new user meets in their first hour. The fourth is worse in a quieter way: it is the one tool here whose whole job is to say whether a difference is real.
A13xB0 commentedon Sep 2, 2026
Claude's pass, part 5: on Windows
Windows 10 Home 22H2 (19045.6466), Intel UHD 620. First pass on this platform, so it is aimed at what only Windows can answer, plus the notes from parts 1-4 that a second machine can settle.
Driven over the control socket against
main+ #497 + #502 + #503 + #504 merged, which is what a release would be if the open Windows work lands. Scenario 1 cannot be done without #503 at all - a baremeshbench.exeis what the Start menu runs, and it exited 2 with nothing on screen, which is #499.meshbench test -junit1. Install and first launch - passes, and most of part 3's notes are answered
From a genuinely empty cache (
LOCALAPPDATAandAPPDATApointed at fresh directories), launched by a GUI-subsystem parent with no console, which is what the Start menu gives it.N11 is fixed. Part 3 measured 513 MB across 6,233 terrain files on a first launch with nothing clicked. Now, after sixty seconds of touching nothing:
Terrain is not fetched until it is allowed.
sim.statesays so properly:and the held warm reports itself as a finished-and-failed job, exactly as the scripting notes describe:
{"id":"links","finished":true,"failed":true,"what":"held: no permission to download terrain, no link measured"}.N12 is fixed. Allowing terrain, the download is now its own job, priced in bytes:
N13 is fixed -
session.status'sjobfield carries that tiles job, so the running download is what surfaces rather than the links job behind it. N14 is largely answered byjob.list, which returns rows withdone,total,what,finishedandfailed. N15 is fixed -resource.listreturns the five rows, not a count.One residual: for the first 15-20 seconds the library is genuinely empty while the published catalogue is fetched, and nothing says a fetch is in flight. I read it too early twice and concluded board images did not exist on Windows before waiting properly. They do - 72 of them.
2. First simulation - passes
fife-strict, 56 of 58 nodes on firmware,advertat Abernethy Repeater answered-> OK - Advert sent, and the flood reached 35 distinct transmitters and 222 receptions, events 791 to 7,772. N1's 56-of-58 wording is still what the checklist says.4. Waveform mode - passes
rf.modeto waveform is picked up mid-run without stopping: events went 1 to 2,401 across the switch, and the status line names the mode both ways.waveform RF: reception is decided by the full receive chain - demodulation, FEC and CRCcalculated RF: reception is decided by link budgets and demodulator floors - the fast modelN8 is fixed.
rf.modewith no argument now answers{"mode": "calculated"}instead of erroring, so a script can ask what mode it is in.3 (partly). N3 is fixed, and it is worth showing
EventClassno longer prefix-matches the detail prose - it readsev.Class, withClassUnclassifiedwhere it used to fall through tofloor. Measured over 3,000 events of one waveform run, the classes separate properly andfloorholds only real floor misses:Part 1 found 37 of 357 misses classed
floorand not one of them a floor miss. That is gone.10 step 3 - passes. N9 is fixed
N10 is still live.
grepover.github/workflowsstill finds no invocation ofmeshbench test, so the shipped fixtures' assertions are enforced by nothing. N9 was fixed by something else noticing; the gap that let it hide is unchanged.11. Determinism - passes
Two runs of
fixture-fife-strictat the shipped seed: 317 unique deliveries both times, and the two outputs are byte-identical once the timing line is removed.9. One emulated board - fails, and not for the reason part 3 predicted
N16 is answered on Windows. The Setup page finds the whole toolchain out of the MSI bundle and names where each one is:
setup.checkreportsready: 8, missing: 1, the one being board images. And that is where it stops: every published board image 404s on download (#516). The catalogue derives its version from the asset filename and discards the release's tag, so it offersv1.17.1and the fetch asks forreleases/tags/v1.17.1, which does not exist - MeshCore tags arerepeater-v1.17.1.boardcheck/probe.goalready composes the tag correctly; the download path never got it.So the toolchain problem part 3 found is solved, and a different one now stands in the same place. On a machine with no image already cached, an emulated board cannot be started at all.
Three other things came off this scenario:
firmware.librarypoints at the.sha256sidecar rather than the build, so all six builds are described as 65 bytes and the real executables appear nowhere. Running firmware is unaffected, which is why it has gone unnoticed.firmware.setacceptsboardand does not read it, pinning a board node to a native build; the failure then readsno native builds published for MeshCore v1.17.1, which sends you off to build MeshCore from source for a problem that is nothing of the kind. It accepts arbitrary unknown keys too, which is the rule Four verbs do not read the parameter their own description documents, and six fields are empty after a fixture opens #476 closed.wb.sim.start()waits for a node that has already failed to start, with no timeout and no error. It sat for over seven minutes on a three-node network where the failure was known within eight seconds. Callingfirmware.startand pollingfirmware.stateshows it immediately;sim.startnever notices.Windows-only faults, filed separately
maintreated that as a usage error, so the application started and vanished. Root-caused against the installed v0.0.5 whose SHA256 the issue names.t.Setenv("XDG_CACHE_HOME", ...)does not isolate on Windows, so fifteen test files read and write the real cache. This machine'srunsdirectory now holds 100run-0NN.jsonrecords planted by the test suite, andTestRunPanelsDrawBeforeTheDiskAnswersfails because it reads 200 where it wrote 4.go build ./...does not compile on Windows:tools/goldencapuses termios constants with no build constraint.windows-check.ymltestsmeshbench.exe check, a command that no longer exists, and hides the failure behind|| echoandcontinue-on-error.wixlbuilds none, and getting either means moving off it.Not reached
3 (the Debug view and the link tool as a person uses them), 5 (SDR++), 6 (a live CoreScope import), 7 (
meshbench dev- no MeshCore checkout here), 8 (meshcore-cli), 12 (the docs site as a reader), and 10 steps 1-2.A13xB0 commentedon Sep 6, 2026
The pass now covers the whole application rather than twelve hand-picked journeys, and every step produces a picture.
Part one is unchanged: the twelve scenarios are the paths somebody actually walks, and a sweep of every window does not replace them.
Part two is the inventory, driven from a manifest:
Run it with:
The list cannot go stale.
internal/ui/workbench/shotsteps_test.goreadstools/shots/steps.jsonagainstpanelMenus— the same table the panel registrations use — and fails when a panel has no capture step, in either direction. Verified by deleting a step:"Waterfall" has no docked capture step, so nothing takes its picture.Two things this turned up while being built:
-board-viewand-board-tabnow exist.-playalone photographs a broken panel. It advances the clock and leaves every counter at zero, which looks exactly like a panel that does not work. The traffic steps carry-injecttoo: measured 24 events / 8 sent / 16 received, against 0 of 0 for the same panel a moment earlier.🤖 Generated with Claude Code
https://claude.ai/code/session_01Q9HbD44EKWWTRYgxbFGxf6
A13xB0 commentedon Sep 6, 2026
Claude's pass, part 6: the v0.0.7 release on Windows
First pass against a published release artefact rather than a build tree. Downloaded
meshbench-windows-x86_64-bundled.msifrom the v0.0.7 release, SHA256 checked against the release's ownSHA256SUMS(871b123c…6cfb, matches), installed withmsiexec /qn, exit 0. Windows 10 Home 22H2 (19045). Driven over the control socket, with screenshots.Issues opened this pass: #607, #608, #609, #610, #611, #612, #613.
Scenario verdicts
meshbench test -junit1. Install and first launch — passes
Bare
meshbench.exe— which is what the Start menu shortcut runs — opens the workbench. #499 is fixed; part 5 could not do scenario 1 at all without #503, and this needs neither.The honesty line is present and picks up the terrain state:
results are a best case: terrain only 49% cached, no multipath, bare earth, ideal demodulator. The update check works and saysthis is the newest release: 0.0.7.Steps 3 and 4 cannot be run against a release build as written. They ask for
MESHBENCH_CARTO_KEYset and then unset, expecting CARTO and then a fall back to OpenStreetMap. Butpackage.ymlstamps the key into release binaries at link time (-X …basemap.defaultCartoKey), so unsetting the environment variable does not leave the build keyless — it leaves it on the stamped key. The steps describe a build made from source, not the thing being released. Worth rewording, or splitting into "from source" and "from a release".The API-KEY-REQUIRED scare, and what it actually was — #607
The first screenshot of the pass looked alarming:
It is not what it looks like. v0.0.7's key works and the ldflags are correct. Those are tiles cached on 2 September by a locally built binary — which has no key, because the secret only exists in CI — and the tile cache has no notion of whether a tile arrived keyed, so it re-serves them forever. Same build, same view, with the cache moved aside:
The dates settle it. Same tile, same day, four hours apart:
carto-dark/9/251/158at 12:03 clean,carto-light/9/251/158at 15:57 watermarked.Worth fixing because it is unfalsifiable from the inside — the app is doing everything right, the key is valid, and the map is still defaced — and because anyone who runs
go buildbefore installing a release hits it. Filed as #607.2. First simulation — passes
fixture-fife-strictopened, firmware started on 56 nodes, played,adverttyped at Abernethy Repeater:The flood spread and the Events panel filled with real physics — 3,280 events in the first 20 s, causes like
decoded its header, then 165 symbol(s) were destroyed by a collision it could not capture over; beyond what CR 4/8 can repair.Correction to N1 from part 1. That note said the checklist and the docs page say 58. The docs are right:
first-simulation.htmlsays "choosefixture-fife-strictto see it moving in seconds (58 nodes)", and the fixture does have 58 nodes. It is only this checklist's scenario 2 that says "watch the count reach 58" about the firmware count, which stops at 56 — the SDR observer and the emitter never boot firmware. One line to fix, here rather than in the docs.3 and the Debug view
Not completed — I read causes from the event stream rather than driving the Inspector and the link tool. The Debug view itself draws and works:
4. Waveform mode — passes
Switched mid-run; the run continued without cutting a frame (events kept climbing, clock kept advancing) and the status bar stamps the mode. Switching back to calculated stamps correctly again, and
session.statusexplains itself properly:calculated RF: reception is decided by link budgets and demodulator floors - the fast model.10 step 3 and 11 — both pass
N9 is fixed and stays fixed.
meshbench test -fixture fixture-fife-strict.json -junit out.xmlgives 317 unique deliveries, wanted at least 10,PASS: 1 assertions, 16s, exit code 0, and the JUnit parses as valid XML.Determinism: two runs at seed 9001 gave 317 unique deliveries both times, output identical but for the wall-clock line.
Two things about node names
#608 — supplementary-plane emoji draw as missing-glyph boxes. Ten of the 58 names in
fixture-fife-strictcarry emoji, and every one outside the BMP is a box, everywhere a name appears:The split is clean —
Bishop Hill ☀️🔋shows both behaviours in one string,☀(U+2600) fine and🔋(U+1F50B) a box — so it is font coverage, not mangled data. The build shipsLICENCES/bundled-noto-color-emoji.txt, so the face is bundled and licensed but is not in the chain for these codepoints. This one will bake itself into all 109 pictures of part two.#609 — three names are already broken on disk.
Drumcarrow Craig NWR ��,Kellie Law NWR 🌤️�andKilcreggan Rpt ☀️��, ten occurrences across four shipped fixtures. The signature is a UTF-8 sequence cut mid-character:Lucklaw Hillin the same file isU+26F0 U+FE0F U+1F324, missing the trailingU+FE0F— the same cut one codepoint earlier, landing on a clean boundary so it left no marker. Live CoreScope returns 50 nodes today with zero U+FFFD, and carriesmulti_byte_statusfields per node, so something upstream already watches for this.The control socket, on Windows — #610, #611, #612
Three findings, and together they are the weakest part of the Windows story.
#612 — the reference page cannot be followed here. It says the application listens on
$XDG_RUNTIME_DIR/meshbench.sockand stops there. On Windows the address is TCP on an auto port, written to%LOCALAPPDATA%\meshbench\control.json, and every connection must send{"token":"…"}as its first line. Neither is documented; I found both by probing. The same page also says "There are 256 verbs. 256 of them say what they are for … the rest are marked as not described yet" — the page carries exactly 255, and if all are described there is no "rest".#610 — two workbenches cannot both start with the default. The second refuses, blaming
tcp:127.0.0.1:0— a port nobody requested, and the sentinel for "pick any free one". Explicit ports work fine; three coexisted happily.#611 — and when a second one does start, it orphans the first.
control.jsonholds one entry, so the newcomer overwrites it. Verified with a GUI workbench playing at 264 s and a fresh headless one: both alive and answering, and a client discovering the documented way reaches the idle one. Silent, and the bad kind of silent — a script does not error, it drives the wrong session against a real engine.#613 —
-lookignores its zoom-look 56.33,-3.32,8and-look 56.33,-3.32,14render the same near-whole-world view; z16 shows Greenland. The centre is honoured, only the third field is dropped.This matters for part two of this issue:
shots.pyframes its captures with-look, so every map-bearing picture in the 109 would come out at world scale — andshotsteps_test.gowould not catch it, because a picture was taken.What this pass did not cover
Scenarios 5 (SDR observer), 6 (importing), 7 (firmware dev loop), 8 (companion bench), 9 (emulated board), and 10 steps 1-2. Part two's 109 pictures were not run — they need the source tree, and #613 would frame them wrong. Part three was walked only for the pages the findings touch.
Screenshots and crops for everything above are on the
evidence/issue-318-v0.0.7-windowsbranch underdocs/evidence/318-v0.0.7-windows/; delete the branch whenever it has served its purpose.🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
A13xB0 commentedon Sep 6, 2026
Claude's pass, part 7: v0.0.8 on Windows, from a clean machine
Re-run of part 6 against v0.0.8, and this time from a genuinely empty machine rather than a warmed one — which turned out to matter, because it surfaced something the warmed pass could not have seen.
Method. Downloaded
meshbench-windows-x86_64-bundled.msifrom the v0.0.8 release, SHA256 checked against the release's ownSHA256SUMS(71052c6a…a0ce, matches). Then, before installing:Program Files\MeshBenchremoved cleanly, its Start menu entry with it%LOCALAPPDATA%\meshbench(516 MB) and%APPDATA%\meshbenchmsiexec /qn, exit 0meshbench.exe, which is what the Start menu shortcut runs, and touched nothingSix of the seven from part 6 are fixed and verified. One is half fixed. One new P1: #622.
The seven from part 6
-lookignores zoomDetails are on each issue; the short version:
#608 was the most satisfying to confirm. The fix commit left open whether the MSI carried the font at all — it did not, and now it does (
fonts\NotoColorEmoji.ttf, 10.18 MB). v0.0.7 left, v0.0.8 right:#613 also unblocks part two of this issue —
shots.pyframes captures with-look, so the 109-picture sweep can now be taken at the zoom each step intends.#607 is the one to keep open. Keyless tiles now cache apart, which stops future poisoning. But tiles written before the fix went into the ordinary
carto-light/path, and a keyed v0.0.8 still reads it. Restoring my September 2 cache and launching v0.0.8 gives a defaced map again, with zero tiles fetched in that run — so it is reading pre-fix poison, not fetching keyless. The population this issue is about is exactly the population that already has a poisoned cache. A cache-path version bump would make the commit's "whatever is already poisoned stops being looked at" true.#622 — first launch fetches 42% of the terrain and then reports partial for ever
This is what the clean machine bought. On first launch the app fetches terrain, says "about 499 MB", finishes, and stops:
The download completes cleanly — 6,233 files, 500.45 MB, no truncated or zero-byte tiles. But the study is then stuck reporting itself as partial:
terrain.prefetch— which nothing in the interface leads you to — says the study actually wants 14,725 tiles, with 8,492 still missing, another ~680 MB. Running it to completion gets tostate: "terrain", 256/256,to_fetch: 0, and 948.94 MB on disk.So first launch delivers 42% of the tiles and 53% of the bytes, presents that as the whole job, and leaves no route to the rest.
The sharpest consequence is that results move underneath you.
meshbench test -fixture fixture-fife-strict.json -seed 9001gave 350 unique deliveries while the cache was still filling and 322 — twice, identically — once it had settled. Same fixture, same seed, same build. That is correct for terrain-dependent propagation, and determinism itself is fine; but a number copied out of a first session does not reproduce later, and the only hint is agroundnote you have to go looking for.Scenario verdicts
meshbench test -junitScenario 1's first launch is a good experience now. It opens on Setup, which says what is ready, what is blocking, what is waiting on you and what is absent, with a cost against each row and a verb behind each button:
Uninstall is clean —
Program Files\MeshBenchand the Start menu entry both go. The only Start menu leftover on this machine isMeshBench WiX Trial.lnk, pointing at aMeshBenchWiXTrialdirectory that does not exist; that is from an earlier local packaging experiment rather than from any released MSI, so it is noted rather than filed.A note on N11, which is no longer what part 5 recorded
Part 3 filed N11 (first launch downloads 513 MB of terrain unprompted) and part 5 recorded it fixed: terrain held until
terrain.allowsaid otherwise. v0.0.8 does not behave that way — a fresh install starts fetching terrain immediately, withground.chosen: false, and nothing asks.I went looking before filing it, and this is deliberate and documented.
terrain.allowsays so plainly:and
getting-started.htmlnow discloses the cost up front, which is exactly what part 3 asked for and did not get:So the design changed rather than regressed, and the docs followed it honestly. Flagging it only because this issue's own history says otherwise, and someone reading part 5 would expect a consent step that is not there. N12 and N13 are properly fixed: the progress line is priced in bytes and names the right activity, and
session.status.jobcarries the running download.The one thing the new docs get wrong is the 500 MB figure, which describes the partial fetch rather than the study — that is part of #622.
What this pass did not cover
Scenarios 5 (SDR observer), 6 (importing), 7 (firmware dev loop), 8 (companion bench), 9 (emulated board), and 10 steps 1-2. Part two's 109 pictures still need the source tree.
Evidence for this pass is on the
evidence/issue-318-v0.0.7-windowsbranch underdocs/evidence/318-v0.0.8-windows/, beside part 6's under318-v0.0.7-windows/; delete the branch whenever it has served its purpose.🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
A13xB0 commentedon Sep 6, 2026
Pass on v0.0.8, on a machine that started with nothing
Run against the released
meshbench-linux-x86_64-compact.tar.gz, not a source build, withXDG_CACHE_HOMEandXDG_CONFIG_HOMEpointed at empty directories so the whole thing was met the way a new user meets it. 109 of the 109 inventory pictures taken; every one looked at.The headline: it works from new
From an empty profile, with nothing pre-fetched:
It fetched its own firmware, brought all 56 nodes up, delivered 317 packets, checked the assertion, wrote valid JUnit and exited 0. Scenario 11 holds too: 317 on three consecutive runs, identical.
Scenario results
meshbench test -junitScenarios 2-9 and 12 not attempted this pass.
Issues opened
Behaviour
prefs.gostill documents has been collapsed to "on unless turned off", and the Resources panel's own footer ("Nothing here is fetched without being asked") is contradicted by its own "Filled itself 64.0 MB" card.boundary.accept; these files predate it.Layout
detailheader wrapped one letter per line. Popping out is not a workaround - the Resources window drawsOpen Configurationas a vertical strip of letters.The pass's own instruments
Tab, becauseAntennawas added to the enum and not to the flag's help text or the manifest. Five of ten photograph the wrong tab;Outputhas never been photographed. Four more use a repeater for tabs only a companion, an observer or a board node grows.-config-sectionand-licence-sectionnever open their panel, and five of the six configuration section names do not exist. All eleven captures in those two buckets are byte-identical pictures of the Setup page.Small
meshbench test.Corrections to this checklist
Scenario 1 step 4 cannot be tested as written. Unsetting
MESHBENCH_CARTO_KEYdoes not leave a release build keyless:cartokey.gofalls back todefaultCartoKey, stamped by the release pipeline. Verified - a cold cache with the variable unset still fetched intotiles/carto-dark(notcarto-dark-keyless) and rendered CARTO Dark with no watermark. Step 3 passes as written. Step 4 needs rewording to say what a build with no stamped key should do, and testing on a source build.The default fixture is the national one. A plain launch opens
scotland-ireland-strictat 378 nodes, which is what makes #616 cost half a gigabyte rather than 63 MB. Worth saying in step 1 so the tester knows which mesh they are looking at.One thing not checked
board-decodedis the single step needing a hand on the mouse (tickdecodein the console strip), and the emulated-board scenario (9) needs a board booted one at a time. Neither was attempted.Also noticed, not filed
showing Setup,Logs is in its own window - Window, then dock every window back- alongside what happened to the simulation.measure links on the GPUon, withLast warm on the GPU: not yetafter 679 links were measured. Might be a fallback that was not announced, might be ordering; not established either way.A13xB0 commentedon Sep 6, 2026
Part 7 addendum: reconciling the Windows and Linux passes on v0.0.8, and a correction
The Linux pass (#616-#621, #623-#626) and my Windows pass (#622) ran against v0.0.8 within minutes of each other. Nothing was filed twice, but several findings touch, and one of mine was wrong.
The correction
In part 7 I wrote that terrain downloading unprompted on a fresh install was "deliberate rather than a regression, and the docs followed it honestly", and I explicitly declined to file it. That was wrong, and #616 is right.
My evidence was documentation:
terrain.allowsays "Downloads are on unless somebody turned them off; there is no third state and nothing waits to be asked", andgetting-started.htmlnow discloses the cost and warns about metered connections. I read the docs, found them self-consistent, and stopped. The Linux pass read the source, where the three-state design is still written down along with the reason for it:with
access.go:61collapsing it. So the documentation describes the collapsed behaviour rather than justifying it, and part 5 of this issue recorded the gate working. Documentation agreeing with the build is not evidence that either matches the intent — I treated it as though it were. N11 is a regression; #616 is the issue for it.Where the two passes meet
firmwaregate.go, mine is the checklist's own wording. Both want fixing, or a fix gets tested against a checklist expecting 58-lookignored its zoomThe rest are independent: #618 (first-run banner), #619 (right-rail panels), #620 (event tables), #621 (
1 assertions), #626 (Help menu) are all things the Windows pass either did not reach or walked past.Cross-platform confirmations
I re-ran four of the Linux findings against the Windows MSI. All four reproduce, so none of them are platform-specific:
fetching terrain, 58 MB of about 499 MBwithground.chosen: false, 59.5 MB on disk 25 s after a first launch on a wiped machine.no firmware for 5 of 58 nodes … and 52 more, againstfirmware.statesaying 56.PASS: 1 assertionstwice in my own output, which I had read straight past.537327c423c188fafor both copies ofFifeinfixture-fife-strict.And #618 I had already captured both halves of without noticing: my first-launch screenshot shows
Blocking 1with the matching "something in this session cannot run" heading, and my later one showsBlocking 0 · Waiting on you 1with "nothing is broken" — whilesession.statusstill returned "this machine is not set up yet". Both screenshots are on the evidence branch and are now linked from that issue.Net position on v0.0.8
Twelve issues open against this pass across both platforms. Of the seven from part 6: five fixed and verified, #607 half fixed (existing poisoned caches still served), #612 fixed except the 256-versus-255 count.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
A13xB0 commentedon Sep 6, 2026
Part 7 addendum 2: scenario 9 on Windows — emulated boards do not boot at all
I marked scenario 9 "not reached" in parts 6 and 7. Asked to actually run it, and it fails: #632, P0.
No emulated board boots on the v0.0.8 Windows bundled MSI. Not a flake — 2 of 2 probes and the ordinary user path, identically:
The cause is three steps upstream of that timeout.
nodefs/<node>/emulator.log:The DLL is present and
setup.checkreports itready. That message fromLoadLibrarymeans a dependency is missing, and its import table names it:libstdc++-6.dll, the MinGW C++ runtime, which is nowhere on the machine and is not in the bundle. Upstream'svirtual-sx1262v1.3.0 Windows asset ships the DLL without it, so the fix belongs there — statically linking libstdc++ is the clean version. Full trace on #632.Renode loads the same library through
LoadLibraryW, so this is every emulated board on Windows, ESP32 and nRF52 alike, not just the QEMU path. Both emulators themselves are fine and run standalone (QEMU 9.2.2 meshbench fork, Renode 1.16.1).What this means for this issue. Scenario 9 is the one part of the checklist that the Windows passes have never actually completed, and it has failed for a different reason each time — #516 (image would not download, since fixed), then #514's fetcher on compact builds, now this. It is worth treating as unverified on Windows until a board is watched booting there, rather than as a scenario that keeps nearly working.
It also blocks scenario 5 (SDR observer) and the emulated half of scenario 7 from being meaningfully tested here.
A check worth adding.
package.ymlrunsfileon the DLL, which passes on one that cannot load, andsetup.checkmarks the row ready on the strength of the path existing. Something that tries to load it would have caught this before the release — the same shape as the bundle-carries-the-font check that #608 added one release ago, which is exactly the class of bug it was invented for.🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
shots: the capture manifest says what it photographs (#633)
1 remaining item
basemap, nodeview: the two remaining faults from the Windows pass (#648)
A13xB0 commentedon Sep 7, 2026
Claude's pass, part 8: v0.0.10 on Windows, all twelve scenarios
Every scenario in part one attempted this time, from a clean machine: v0.0.8 uninstalled,
%LOCALAPPDATA%\meshbenchand%APPDATA%\meshbenchremoved,meshbench-windows-x86_64-bundled.msiverified against the release's ownSHA256SUMS(dd67a039…9eb2) and installed withmsiexec /qn.Issues opened: #650, #651, #652, #654.
Nine of the previous pass's faults are confirmed fixed along the way: emoji render,
1 areaswhere the duplicate study area used to be,PASS: 1 assertionsingular, the terrain warm reaching complete, the emulator toolchain all found beside the binary.1. Install and first launch — passes
Terrain fetches on first launch and the Setup page now says so plainly, with a
Turn offbeside it. The whole warm settles at 6,233 of 6,233 tiles,state: terrain, note empty — #622's fix holds on a genuinely empty machine.The emulator toolchain is all
readyand found beside the binary, andtools/dissector/ships, so #641 landed correctly.#650: the Setup page lists
s140 SoftDevicetwice, identical name and wording, differing only in size. They are two builds for two application base addresses (0x26000and0x27000), both needed, and nothing on the row says why there are two. One row and one 838 kB download would be better than asking a user to choose between flash layouts.3. Debugging a missed packet — passes
The four miss classes carry real physics, and the Link panel draws the cut-through the scenario asks for:
Markinch ↔ Mothy, 7.5 km, both directions' margins, the first Fresnel zone as a band, edge losses along the profile, and a verdict that tells you what to do — "Terrain at 1.0 km sits 3 m above the line of sight once earth curvature is included. Raising an antenna by about that much, or moving to clear it, is what changes this - more power will not."#651 came out of using it: the Packet timeline can only draw the last 2,000 events, which on this network is about six seconds. At
t = 417 sthe axis shows 398-416 s and nothing earlier can be reached. The engine keeps the whole log — this isreadoutTail = 2000inreadouts.go, andEventsSincealready exists besideEventsTailfor exactly this.4. Waveform mode — passes, but the misses do not
It switches without cutting a frame and the status bar stamps both modes correctly. #652 is what it reports while it runs:
0 codeword(s) beyond repair, 0 repaired, CRC false— 32 of 347unclassified, where calculated mode names all 1,768 of its misses..., CRC false, at 15.0 dB5. SDR observer — passes
sdr.serveanswers with an address and 62,500 Hz. Connecting a socket gets a realRTL0rtl_tcp header, and the stream behaves as the scenario says it should:Dragging the observer 0.25° across the map while a client was reading: 1.99 MB received, no disconnection, same address afterwards.
I could not run SDR++ itself; what is verified is that the stream is a valid rtl_tcp stream at the documented rate, that it carries signal only while playing, and that it survives the drag.
6. Importing a real network — passes, with one expectation that no longer holds
The numbered steps all report their counts: 189 nodes fetched from the live ScotMesh CoreScope (599 records, 41 skipped for no position), 189 committed, a week of traffic read, 117 of 189 given regions. A flood afterwards relays widely.
Step 3 does not behave as written. It says to skip "apply regions" and confirm the mesh transmits but does not relay. On a fresh import with no regions applied, an advert from a repeater produced 94 distinct transmitters — it relays freely. Either the behaviour changed or the step is about messages rather than adverts; either way the checklist and the build disagree.
One unreproduced oddity, recorded rather than filed: on my first run
infer.applyanswered{"applied": 117}and no node gained a region, innodes.listor in its boot provisioning; a second call applied them. Four later attempts — immediate, after a 30 s wait, and with the sim playing — all applied correctly first time. I could not find the trigger, so I am not filing it, but if anyone sees regions silently not take, that is the shape.7. Firmware development loop — step 1 passes
meshbench devagainst a realripplebiz/MeshCorecheckout, withMESHCORE_NATIVEpointing atmeshcore-native:Named after the branch, cached, and assigned — exactly as the scenario describes. Both refusals on the way were good ones: a directory that is not a checkout is told
no examples/simple_repeaterand where to point-from, and a missing build harness names the three places it looked and the repository to clone.Steps 2 and 3, the two-arm sweep with a control, are running as I write this; I will follow up with the per-arm figures.
8. Companion app — passes
bench.servegives a TCP endpoint, and a socket speaking the companion framing gets real protocol bytes back —APP_STARTanswered with a 51-byte reply and a public key, device query and contacts answered too.bench.dropreturns{"dropped": 1}and the client sees the socket die. After serving again, 3 of 3 reconnects worked.Worth noting the first attempt at this failed entirely until a Windows firewall prompt was allowed — the bench binds the machine's LAN address rather than loopback, so a first run on Windows will meet the firewall.
9. One emulated board — fails: #654
Emulation itself works now.
board.probepasses every column onHeltec_v3— boot, radio, tx, rx, flood, power — so #632 is genuinely fixed and the chip model loads.But the scenario's own path does not. Assigning a published board image to a node of a loaded network gives a node that boots its bootloader, never starts MeshCore, and reports itself running:
Reproduced on two nodes; stopping the node first makes no difference. The failure appears only in
node.outputunderemulator— not in the events, not on the Hardware tab, not in the session log.10. Scripting and the clients — passes, all three steps
Step 1. Cookbook example 3 (
03_small_mesh_with_traffic.py) against the publishedmeshbench==0.0.10client:Two dead ends on the way, both mine rather than the product's: the client finds the binary through
MESHBENCH_BINARY, notMESHBENCH, andshutil.which("meshbench")finds nothing on Windows because the MSI does not touchPATH— so on Windows that variable is not optional. And the client in the source tree declares0.1.0, which the version gate refuses against a 0.0.10 workbench; the release workflow stamps it at publish time and the PyPI package is correct. The refusal itself is a model of a good error.Step 2. Every page uses the same
localStoragekey,meshbench-docs-way, so a language chosen on one page is remembered on the next.Step 3.
meshbench test -junitexits 0, 317 unique deliveries, valid JUnit — and now readsPASS: 1 assertion, so #621 is fixed.11. Determinism — passes
Two runs at seed 9001: 317 unique deliveries both times, output identical but for the wall-clock line.
12. Docs site and READMEs — passes
All 40 pages linked from the Learn overview return 200; no broken images. Search on
/, the on-page rail, a phone media query,prefers-color-schemeand the brand assets are all present and resolve. Both READMEs use a theme-aware<picture>; the main repo's five badges and sixteen links all resolve.The one apparent broken link —
MeshBench/brandreturning 404 from the docs README — is a private repository, and the sentence around it says so and explains why the assets are copied rather than fetched. Not a fault, though a reader outside the org meets a 404 with no warning.What this pass did not cover
Part two's 109 pictures and part three's page-by-page walk were not run; part three was checked only for links, assets and the pages the findings touch. SDR++ and
meshcore-cliare not on this machine, so scenarios 5 and 8 were driven with my own clients against the documented protocols.🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
A13xB0 commentedon Sep 7, 2026
Scenario 7, steps 2 and 3 — the sweep completed, and it found something
The two-arm sweep with a control ran to completion: three arms, three seeds, nine runs. Everything the scenario asks to see is reported per arm —
delivered,redundant,rx_spread,at_risk_2db, pluscollisions,airtime_ms,tx,rx.The control works.
controlandrelease— the same build under two labels — came back identical in every field, which is exactly what the scenario says to check and is the sweep's own integrity test passing:The run log was readable throughout, one line per run:
done 4/9 release, seed 9002.But the seeds do nothing, and that is #671. Within every arm, the three seeds produced three byte-identical runs, so
rx_spreadis 0 on all three arms — and the documentation makesrx_spread"the number every delta must beat before it means anything". The same is true ofmeshbench test -seed: 9001, 9002, 9003 and 4242 all give 317 unique deliveries, with the header correctly echoing the seed it was given.So scenario 7 passes as a sequence of steps — build, assign, define, run, tabulate — and the figure the comparison is supposed to be judged against is structurally zero.
For the record, the comparison itself: my build of upstream
MeshCoremaindelivered 61 against the pinned release's 286, over ten times less airtime. That may be a real difference between the two trees; the sweep offers no way to tell, which is the point of #671.🤖 Generated with Claude Code
A13xB0 commentedon Sep 7, 2026
Claude's pass, part 8: v0.0.10 on macOS — the first pass on a Mac
Run against the released
MeshBench-arm64-bundled.dmgfrom v0.0.10, SHA256 checked against the release's ownSHA256SUMS(e9666b8a…2217, matches), installed by dragging to Applications. macOS 26.3 (25D125), Apple M4, 10 GPU cores. Driven over the control socket with the publishedmeshbench-drivingandmeshbench-scriptingskills, and themeshbench==0.0.10client from PyPI.All twelve scenarios attempted. Part two run in full for the first time on macOS — 110 of 115 pictures. Screenshots on the
evidence/issue-318-v0.0.10-macosbranch.Issues opened: #655, #657, #658, #659, #660, #661, #662, #663, #664, #665, #668, #669, #672, #673, and MeshBench/docs#34, #35, #36.
The headline: scenario 9 fails on macOS, in all three board families
The thing this pass was for. The toolchain is found — the bundled dmg puts
libvirtualsx1262.dylib,qemu-system-xtensaandrenodebeside the binary andsetup.checkreports all threereadywith their paths, which is the N16 story properly closed. Then nothing boots.Generic_E22_sx1262Attempt to set property 'apb_freq' on anonymous device (type 'esp_soc.uart') after it was realizedHeltec_v3,Tbeam_SX1262Guru Meditation Error: Core 0 panic'ed (LoadStoreError)— because the downloaded image is the application-only.bin, not-merged.binRAK_4631radiospi.lora: **no chip model: Unable to load shared library 'libdl.so.2'**— a Linux soname P/Invoked on macOS. The node sits instartingfor ever with no radio#658 is not macOS-specific and is the more interesting one:
firmware.downloadresolves the asset withListAlland takes the first row matching (role, board, version), and upstream publishes both-merged.binand the app-only.binunder names that differ only in that suffix.Runnable()exists one file away and rejectsbin && !Mergedfor exactly this reason; the download path never consults it. Byte-exact against upstream: E22 got the merged file (1,212,960 bytes, partition magicaa50at 0x8000), Heltec_v3 got the app-only one (1,171,104 against the merged 1,236,640, no partition table). Every earlier pass of scenario 9 used the E22 — the one board whose asset happens to come out right.#655 is a one-line fix with the intent already written down: the comment above the P/Invoke says "Windows is the odd one; everywhere else is dlopen, and macOS resolves it in libSystem", and the attribute three lines below says
libdl.so.2.Along the way, a fourth, cross-platform: #657 —
firmware.download's job id isfw-<version>-<role>and leaves out the board, so every board image after the first appends a duplicate row that is never marked finished. All four files landed correctly;job.listreportedrunning: 3for the rest of the session, andwait_idlenever returns. That is what hung my script while the work was already done.Scenario verdicts
advertanswered, flood reached 38 distinct transmittersmeshbench testexits 0 with valid JUnit1. Install and first launch
The macOS install experience is the one thing only a Mac can report, and it is the weakest part of the first hour.
Filed as #660.
docs/install.mdis honest about it and offers three routes, which is more than most projects manage — but route 1, "Right-click the app in Applications and choose Open", stopped bypassing Gatekeeper for unnotarised apps two macOS releases ago, so that line is worth re-checking before the release.Step 3 passes as written: CARTO dark is the default, the tiles cache under
tiles/carto-dark-keyed/(so #607's fix is in), and there is no API-KEY watermark anywhere while panning. Step 4 is untestable on a release build for the reason the Linux pass already recorded — the key is stamped at link time.2, 3, 4 — the run itself is in good shape
fixture-fife-strict, 56 of 58 nodes on firmware in three seconds,advertat Abernethy Repeater answered-> OK - Advert sent, and the flood spread to 38 distinct transmitters. Miss causes separate properly, so N3 stays fixed in calculated mode:N4 is fixed — the Link panel draws its cut-through docked in the right rail, with both directions' margins and a verdict, rather than one character per line:
N5 is fixed — the packet timeline now carries
each mark is one event: sent | heard | missed. N17 is fixed — a node's console opens with everything it said before you got there (-- Abernethy Repeater said this before the console was opened --, then the whole provisioning conversation):Waveform switches mid-run without cutting a frame (357,459 → 372,783 events across the switch), stamps
waveform RFand swaps the honesty line to… bare earth, real coding chain:But #664: in waveform mode, 95 of 113
floormisses carry a positive measured SNR, 43 of them at +15.0 dB, and the same sentence —waveform: no preamble lock at 15.0 dB measured SNR— appears 43 times underfloorand 20 times underunclassified. The class is decided from the wanted signal alone and the sentence prints the composite measurement, so a miss the engine correctly called "too quiet" reads "+15.0 dB". This is N3's shape in the mode nobody re-checked when N3 was fixed.5, 6, 7, 8 — all pass, and several old notes are closed
sdr.serve→{addr: 127.0.0.1:65243, rate_hz: 62500}, genuineRTL0header, 125 kB/s = 62.5 k complex samples/s. Playing/paused separates cleanly once there is traffic in the window (rms 7.29 / peak 48.71 / 2.71 % against rms 3.56 / peak 12.98 / 0.00 %), and fournodes.movecalls across the map broke nothing. Worth repeating the Linux pass's warning: a short sample with no traffic in it looks identical either way, and my first attempt very nearly became a defect report.infer.apply -> 42. N23 is fixed:nodes.listshows regions on 42 of 60, matching. A flood then relays to 48 transmitters.meshbench devwith a workbench running now reportsin the workbench's firmware library (478504 bytes)and the file is 478,504 bytes on disk, not zero. The build works on macOS out of a MeshCore checkout withMESHCORE_NATIVEset. N21 is fixed and this is the good news of the pass: three arms (mine= my local build,release,control) over seeds 9001/9002/9003 gave byte-identical rows within each seed and genuinely different rows between seeds — rx 170 / 193 / 168 — withcontrolreproducingreleaseexactly. And the sweep says the honest thing over the top of it:the seeds disagree by more than the arms do on receptions (±7.1% within an arm, 0.0% between them) - add repeats.APP_STARTanswered with a realRESP_CODE_SELF_INFOframe,bench.dropkilled the socket (recv -> b''), and serving again reconnected first time. N27 is fixed — the second serve returned the same address.10, 11 — the clients and determinism
Two runs, 322 both times, output byte-identical once the timing line is removed. #621 is fixed — "1 assertion", singular, here and in the Schedule panel. Cookbook example 3 ran to completion and printed
162 events, 63 receptions in the tail, exactly the Linux figure.One correction to my own working: I first ran the examples with the client from a
maincheckout and got aVersionMismatch. That is the pairing check doing its job — the checkout carries__version__ = "0.1.0", andpip install meshbench==0.0.10gives the right one. Nothing is wrong here; noting it because "the client in the tree refuses the release built from the same tag" looks like a bug for about a minute. Do note the client needs Python ≥ 3.10, and macOS's system Python is 3.9.6, sopip3 install meshbenchon a stock Mac fails with "No matching distribution found" — which reads like the package not existing.Part two — 110 of 115, and what it took
shots.pycould not take a single picture on macOS: itscapture()knows onlyspectacleandgrim(#662). With ascreencapture -l<windowid>branch and a smallCGWindowListCopyWindowInfohelper, the whole sweep runs unchanged.panel-energyandpopout-energyare impossible (#668); docked and popped-out sets otherwise matchOutputhas now been photographedTwo faults came straight out of the pictures:
macOS: every menu accelerator is Ctrl rather than ⌘, in the caption and in the binding — Gio's key.ModShortcut exists for this #673 — every menu accelerator on macOS is Ctrl, in the caption and in the binding. ⌘S does nothing; Ctrl+S saves.
parseShortcutmapsCtrl+tokey.ModCtrl, and the vendored Gio already shipskey.ModShortcut(=ModCommandon darwin) for precisely this.The Compare panel prints its empty-state sentence twice — as a clipped section title and again centred — in all three of its empty states #669 — the Compare panel prints its empty-state sentence twice, once as a section title clipped by the window edge and once centred, in all three of its empty states.
And one the picture makes obvious rather than proves: every node reports 0 CPU and 0 memory, because
nodestats.goreads/proc/<pid>/statwith no build tag and no Darwin or Windows counterpart (#661). Two of three platforms therefore show a plausible zero where the reading is simply absent.Part three — the docs, as a reader
The site is in good shape and the badges and images the earlier passes found dead are all alive now: N38, N39 and N40 are fixed —
docs/images/workbench-plan.pngresolves, all five README badges return 200 (CI - passing,release: v0.0.10), and the repository is public. I walked every page in the table and link-checked all forty: no broken internal links, and one dead external one.reference-control.mdputs macOS on the Linux row,$XDG_RUNTIME_DIR/meshbench.sock. That variable is never set on a Mac; the address is~/Library/Caches/meshbench/control.sock— a different directory and a different filename.architecture.mdandsettings.mdrepeat it. This is Control socket reference is unusable on Windows, and its verb count disagrees with the page #612's shape with macOS left on the row it was already sharing.importing.html's headline claim, "Without this step every node transmits and none relays", is not what happens: two imports differing only ininfer.applyboth flooded an advert to 48 distinct transmitters and 94 receptions, identical. And the "If nothing relays" checklist's item 2 tells you to askget repeaton a node with no regions — which answers-> > on, because it is a different question.agent-skills.html's two clone URLs are stale, one 404s, the driving skill is namedmeshcoresimrather than its published namemeshbench-driving, and the third skill it lists is deliberately not published.session.describelists every verb. It answers four facts;session.verbsis the one. Both the tree's ownscripting-verbs.mdandverb-reference.mdhave it right, so it is the skill that drifted — and it is the first concrete instruction an agent reads.Smaller, and things I checked rather than assumed
resource.listcalls the bundledvirtual-sx1262andqemu-system-xtensaneededwith an empty path whilesetup.checkcalls themreadyand names where they are, on the same machine a second apart. On a bundled install the Resources panel offers to download what is already installed, and on macOS The compact build cannot complete itself off Linux: the fetcher serves no emulator for macOS or Windows #514 means that offer cannot be honoured.0x0; the machine is an Apple M4. And on any launch after the first, Configuration says "Last warm on the GPU: not yet" and the links job says "measuring every link on the processor" for a warm that measured nothing at all — those 71,253 are cache hits.gpu.stateanswerslast_warm: {used: true, pairs: 71253, ms: 10603}. 71,253 pairs on Metal in 10.6 seconds.project.newon a fixture with 56 nodes running firmware completed cleanly, twice.project.listnow answers with afixtureslist besideprojects, so the shipped networks are reachable from File on a fresh install.nodes.listno longer publishes the deadsent/heardfields;nodes.statscarries the live ones.link.profileaccepts unknown parameters ({"wibble": 1}) without refusing, and refuses "select two nodes to cut through between" under codeinternalwhereconflictis the documented meaning ("the right request in the wrong state"). A script branching on the code reports a bug rather than fixing its own selection. Not filed on its own; worth folding into whatever next touches the refusal codes.experiment.sendersaccepts a repeater and only refuses at run time — nine runs failed one after another withAbernethy Repeater is a simple-repeater; a message is originated by a companion, which is a clear sentence arriving nine times too late.experiment.varyon the same parameter twice crosses it with itself, giving arms labelledmac318b · 1.17.1— two values of one field in one arm. Refusing that, or replacing rather than crossing, would save a confusing matrix.boundary.set {"query": "Fife"}still returns two byte-identical matches. Same as Scotland in the earlier pass; nothing distinguishes them.nodes.regionsanswers{"nodes": 60, "regions": null}— a count where the rows were the question, thoughnodes.listnow carries them.reference-control.htmlsays "There are 256 verbs". The page documents 256 and the socket offers 219; the other 37 are internal callbacks, and the page marks every one of them "The workbench's own callback. The socket refuses it." So the page is honest where it counts and only the headline sentence is loose. Not filed.What this pass did not do
I did not drive SDR++ itself or
meshcore-cli(neither is installed here), and I did not photograph the board view, which needs a booted board — the thing this platform cannot currently do.board-decodedalso needs a hand on the mouse.Evidence is on the
evidence/issue-318-v0.0.10-macosbranch underdocs/evidence/318-v0.0.10-macos/, beside the earlier passes'; delete it whenever it has served its purpose.🤖 Generated with Claude Code
https://claude.ai/code/session_019eZa5JusUZ8YCxuB2g4eYY
A13xB0 commentedon Sep 7, 2026
Parts two and three, on Windows
Part two cannot be run here at all — #674. The capture sweep is Linux-only in three separate places, and I hit each in turn:
and past those,
capture()ends atsys.exit("no window capture tool found: install spectacle or grim")— KDE and Wayland respectively.So the 109-picture inventory, which exists to say the pass covered every window rather than the ones somebody thought of, is unavailable on the platform these passes keep finding the most in. The rest of the tool is sound:
--listenumerates all 117 steps correctly and the manifest test is doing its job. It is the three platform assumptions around it. A window-only grab on Windows is about fifteen lines of PowerShell — I have been using exactly that for this pass's screenshots and am happy to contribute it.Part three, the generated references, check out. Both pages that are "held to: it is generated, so the check is that nothing here is missing or stale" agree with the build:
reference-clireference-controlAll 40 pages linked from the Learn overview return 200, no broken images, and the site's search, on-page rail, phone media query and
prefers-color-schemeare all present. Both READMEs use a theme-aware<picture>; the main repo's badges and links all resolve.The page-by-page walk against the running build — following each guide literally, in order, checking that no step names a control that is not there — is the part I have not done, and the part
shots.py docswould have driven. It needs #674 first.🤖 Generated with Claude Code
shots: photograph our own window, or none (#667)
A13xB0 commentedon Sep 7, 2026
Part 8b: the fixes, reviewed on the Mac that found them
Sixteen PRs built from source with go1.25.14 on macOS 26.3 (25D125), Apple M4, and each run against the reproduction that produced its issue. All sixteen hold. None failed. Each is labelled
macos fixwith the measurements on the PR.RAK_4631boots, adverts twice unprompted,irq_reads0 → 35,336, console answersget nameHeltec_v3boots and adverts, no Guru Meditationrunning: 0untestedwith the abort quoted, against2 passedfloormisses, 0 above the threshold on the deciding number; 0 sentences under two classesdevice: "Apple M4"; a primed warm says the matrix already answered every pair⌘N ⌘O ⌘S ⌘⇧S, andTestAShortcutBindsThePlatformModifierpasses on darwincpu_ms/cpu_pct/rss_bytes: nullwithcost_measured: falseon diskwith paths; the two SoftDevices named apartenv, and the panel registers and drawssession.verbsand the three socket addresses, all true of this buildmap.basemapnames what is drawnSmall residuals, all noted on their own PRs and none of them blockers:
cpu nowstill renders0.0%in the Nodes table (#692), the Compare title still has no left inset (#694),⌘⇧Shas the modifiers in the wrong order for the platform (#702), andshots.pynow needspyobjc-framework-Quartzwith nothing documenting it (#703).Scenario 9 now passes on macOS, on all three board families
The thing this pass was for. It failed on every family this morning and passes on every family now — but it takes three separate changes, and one of them is not a PR here:
esp32s3)esp32)sx1262-15irq_reads73,748, both neighbours heard all fivesx1262-15is the answer to #659 and #654. Same flash, same command line, the two binaries side by side:toolchaincatalogue.go:24onmainalready pins-15; the v0.0.10 bundle ships-14, which is the whole of what is left there. Details on #659.Method, so the "verified" means something
Each PR was checked out with
gh, built with the same Go the release uses, and driven through the control socket against the exact reproduction from its issue — not read. Where the fix is visual (#702, #694, #688, #693) I photographed the window; where it is in an emulator (#704, #701) I booted a board and countedtxevents rather than trusting arunningflag, which is the thing this whole pass has been about.Two things about the environment worth recording for anyone repeating it: a source build is not beside the bundled emulators, so
MESHBENCH_RADIO_LIB,MESHBENCH_QEMUandMESHBENCH_RENODEhave to point at them — the refusal when they do not is excellent and names all four search locations. And #691 has to be run with the binary actually placed beside them, or its whole point is untestable.The
/Applicationsbundle has been put back exactly as it shipped: the one file I swapped in to test #704 is restored, and the test binary is removed.🤖 Generated with Claude Code
https://claude.ai/code/session_019eZa5JusUZ8YCxuB2g4eYY
A13xB0 commentedon Sep 8, 2026
Part 9: v0.0.11-dev.1 on macOS, from a genuinely fresh install
MeshBench-arm64-bundled.dmgfrom v0.0.11-dev.1, SHA256 checked against the release's ownSHA256SUMS(f71b97be…18f6, matches). The previous install and all of its state were moved aside first —/Applications/MeshBench.app,~/Library/Caches/meshbench(535 MB) and~/Library/Application Support/meshbench— so this is what a new machine gets. macOS 26.3 (25D125), Apple M4.Two issues opened: #711 and #714. Every one of the sixteen fixes I verified as PRs still holds in the release build.
Scenario 9 passes on all three board families
The thing that failed on every family a day ago:
Generic_E22_sx1262qemu esp32> E22 RepeaterHeltec_v3qemu esp32s3> EmuRAK_4631> RAK4631 Repeaterwith both neighbours hearing each of them. The E22 image is the merged one (
0x8000 = aa50) without being asked for, andsx1262-15is in the bundle, so #658 and #659 are both closed from a user's side.The nRF52 needs one step the ESP32s do not, and the refusal names it exactly:
One
resource.fetchand it boots. Worth knowing that a fresh install cannot run an nRF52 board out of the box even from the bundled dmg — the SoftDevice is Nordic's and cannot be redistributed, which the Licences page says.Scenario verdicts
ground: terrain6233/6233, GPU warm 71,253 pairs in 8.4 slink.resultnow returns both margins and every termfloormiss now readsat -15.6 dB measured, -26.0 dB estimatedinfer.apply -> 45matchingnodes.list, flood to 47 transmittersrx_spreadmeasured, seeds varySELF_INFOon three reconnects, drop kills the socket, same endpoint each serve#714 — the sweep says 112 of 115 and 41 % of the pictures are of the workbench
The one I would fix first.
captureDarwingained a guard after #703 merged:A popped-out panel and a node window are floating windows on layer 3. So all 36
popout-*and all 10node-*steps skip the window they are about and fall through to the main workbench, which passes every test.popout-events.pngis a picture of the Plan view.run()returns True, stderr is clean, the summary says112 of 115 captured. Removing that one condition and keeping the size test restores900×672and820×652. The guard is one I asked for in my review of #703 — I suggested the size test; what landed also tests the layer, and that is the half that breaks it.#711 —
meshbench testgives 350 on a new machine and 322 ever afterThis is what a fresh install bought that a warm one could not show. Same fixture, same seed, same binary, terrain complete before the first run:
Isolated to persisted per-node storage, and reproducible on demand:
Ruled out: terrain (6,233 files before the first run, unchanged after) and the firmware cache (wiped mid-test: still 322).
native.go's own comment describes this trap and offersMESHBENCH_NODEFSandWipeNodeStorage;cmd_test.gouses neither. It also probably explains the 350-vs-322 pair the v0.0.8 Windows pass attributed to a filling terrain cache (#622).Scenario 11 passes only because it is the second and third run. Run it first on a new machine and it passes on 350 instead — the check cannot see the variable it is meant to be checking.
The sixteen fixes, re-checked in the shipped build
All still hold: ⌘ accelerators (and
⇧⌘Snow in Apple's order, which was my nit on #702), the Compare panel saying it once,session.verbsin the published skill,map.basemapnaming what is drawn,events.dumpwriting the whole ledger, the node window switching tab,resource.listagreeing withsetup.check, the SoftDevices named apart,device: "Apple M4", the board probe passing nothing when the emulator never ran, the download job ids naming the board, the merged images, and the waveform miss printing the SNR that classified it. The Energy steps now capture, which is why this is 112 rather than 110.Two residuals from those PRs are unchanged in the release build and I have commented on each rather than reopening:
cpu nowstill renders0.0%in the Nodes table (#661), and the Compare section title still has no left inset (#669).10 step 1 is blocked, and not by a defect
PyPI has
meshbenchup to0.0.10and no0.0.11-dev.1, so the client refuses the workbench on the release-pairing rule — correctly, and with a good sentence. A dev release with no matching client simply cannot run the cookbook examples. Worth deciding whether dev releases publish a client or whether the pairing rule should let a-devbuild through.Method
Everything above was driven over the control socket against the installed dmg, not a source build. Node counts,
txevents andirq_readsrather thanrunningflags for the emulated boards. The old install and its 535 MB of state are in the scratchpad if anything needs comparing against v0.0.10.🤖 Generated with Claude Code
https://claude.ai/code/session_019eZa5JusUZ8YCxuB2g4eYY
A13xB0 commentedon Sep 8, 2026
Claude's pass, part 9: v0.0.11-dev.1 on Windows, all twelve scenarios
First pass against a development-channel build. Clean machine as always: v0.0.10 uninstalled,
%LOCALAPPDATA%\meshbenchand%APPDATA%\meshbenchmoved aside,meshbench-windows-x86_64-bundled.msiverified against the release's ownSHA256SUMS(41ef47fb…c63b3) and installed withmsiexec /qn.Issues opened: #712, #713, #715.
The new channel itself
Worth checking first, since it is what is new. It behaves:
The release notes say plainly that it is cut from
main, may change again, and that the stable channel will not offer it. The one thing the channel does not carry with it is its clients — #715.9. One emulated board — passes, and this is the headline
This has failed on Windows in every previous pass. With sx1262-15 bundled it now works: the ESP32 board boots MeshCore, adverts unprompted, and answers on the console.
No
apb_freqabort. #632 and the chip-model chain behind it are genuinely done on this platform.One note for whoever writes the scenario timings: it took ~200 s of simulated time to first transmission. I checked at 140 s, saw
sent=0, and would have recorded a failure had I stopped there — as I nearly did.1–3. Install, first simulation, debugging — pass
Terrain warm reaches
tiles_cached: 6233, tiles_sampled: 6233, toolchain all ready, bundled QEMU issx1262_15. 56 running, 8,581 events, four named miss classes. #617 is confirmed fixed: the refusal now reads "no firmware for 56 of 56 nodes", not "5 of 58".4. Waveform mode — passes, but I have a correction to make
Mode switches cleanly and the status bar stamps both modes. #652 is not fixed, and I said it was.
Reviewing PR #697 I reported "unclassified 81 → 0, CRC-true 4 → 0, both halves gone". Re-measured on this build, which contains that merge, over 9,500 miss events:
Essentially unchanged. My "0 of 1665" was a single unlucky sample window, and I should not have reported a clean sweep from one. Corrections are posted on both #697 and #652.
5. SDR observer — passes
sdr.serveanswers with an address and 62,500 Hz, a socket gets a realRTL0rtl_tcp header, and the stream carries signal only while playing:Dragged the observer 0.25° across the map while a client read: 1.25 MB received, no disconnection, same address afterwards.
A change worth calling out as good:
sdr.servenow binds127.0.0.1where v0.0.10 bound the machine's LAN address. That removes the Windows firewall prompt I hit last pass.bench.servestill binds[::]and reports the LAN address, so scenario 8 still meets the firewall on a first run — the fix looks like it landed on one and not the other.6. Importing a real network — steps 1–2 pass, step 3 fails
Every numbered step reports its counts, in the documented order:
#713 is step 3. It asks you to skip "apply regions" and confirm the mesh transmits but does not relay. It relays. Two arms differing only in whether
infer.applyran, one advert each, counting nodes that carried the samemessage_idrather than nodes adverting on their own schedule:Seventeen relays either way.
infer.apply's own verb reference says that without it a mesh "transmits everything, relays nothing"; the importing page's diagram says "skipped: transmits, never relays". One of those two and the engine disagree, and I cannot tell from outside which is meant to give.Also noticed:
boundary.set {"query":"Fife"}returns two entries, both{"kind":"administrative","name":"Fife"}, identical in every field shown, soboundary.acceptmust be given a name that does not distinguish them.7. Firmware development loop — step 1 passes; the sweep still cannot measure
meshbench devagainst a realripplebiz/MeshCorecheckout:The sweep produces everything step 3 asks for —
delivered,redundant,rx_spread,at_risk_2db, a readable run log, and the control arm matching release exactly, which is the control doing its job. But #671 stands: three seeds per arm give three byte-identical runs, sorx_spreadis 0 on every arm and the number every delta is supposed to beat is zero.What has changed is that the workbench now says so itself when the sweep ends:
That is the right sentence and it stops
rx_spread: 0reading as "very stable". Detail on #671.8. Companion app — passes, and a bug found underneath it
Driven with the real
meshcore-cli, as the scenario names.infosis answered with the node's key and name;bench.dropreturns{"dropped": 1}and the client sees the socket die; serving again and reconnecting works, repeatedly.Two things I got wrong on the way, recorded because they cost time: my own hand-rolled framing produced no reply and I nearly filed it — the real client works fine, so that was mine. And a "connection refused" I nearly filed as a lifecycle bug is the workbench reclaiming a port, which it says plainly when asked: "Mothy was being served to nobody, so its port was taken back; bench.serve gives it out again".
What the scenario did surface is #712, which I think is the most serious thing in this pass. Every native node shares one MeshCore identity — the same public and private key:
57 of 58 byte-identical. The consequence is visible without any tooling: nodes hear plenty and know nobody.
Every advert a node hears carries its own key, so it is discarded as self. #556 fixed this for emulated boards and recorded at the time that native nodes had distinct identities; they do not now. The seeds are not the problem — MeshBench passes a distinct one per node, and the shipped firmware run by hand with four different seeds produces four different keys. Full working on #712.
It is not new: the v0.0.10 profile I saved before wiping it has the same bytes, across a different fixture and a different install.
10. Scripting and the clients — steps 2 and 3 pass, step 1 cannot be run
Step 1 is blocked by #715. The published Python client is 0.0.10, the workbench is 0.0.11-dev.1, and the gate refuses — correctly, and with a good message. But the message says to install the 0.0.11-dev.1 client, and there isn't one: PyPI stops at 0.0.10 and
pip install meshbench==0.0.11.dev1finds nothing.@meshbench/clienton npm stops at 0.0.10 too. The Go client works, because Go modules come from the git tag:Same build, same machine: Go connects, Python cannot.
Step 2 — all 40 pages carry the
meshbench-docs-waykey, so a language chosen on one is remembered on the next.Step 3 —
meshbench test -junitexits 0, 317 unique deliveries, valid JUnit,PASS: 1 assertionsingular.11. Determinism — passes
Two runs at seed 9001, byte-identical output but for the wall-clock line (
34sagainst33s), 317 unique deliveries both times.12. Docs site and READMEs — passes
All 40 pages linked from the Learn overview return 200; all 22 distinct images return 200. Search, the on-page rail, a phone media query and
prefers-color-schemeare all present. Both READMEs use a theme-aware<picture>and all four banner assets resolve in both repositories; the main repo's badges and all 15 links resolve, anddocs/images/workbench-plan.pngis present at 936 kB.The only 404 is
MeshBench/brandfrom the docs README, which is a deliberately private repository and explained in the sentence around it.Fixes from previous passes confirmed on this build
PASS: 1 assertiontools/dissector/ships and is foundversion(6.1.1 / 7.x) and awhynaming the base address they pair with, so they can be told apartevents.dumpnow writes the whole ledger (total 2825, written 2825). The panel half looks unchanged and I have left a note on The Packet timeline forgets: it can only draw the last 2000 events, about six seconds of a busy run #651 rather than reopening itWhat this pass did not cover
Part two's 109-picture inventory still cannot be run on Windows (#649 —
tools/shots/shots.py; my fix is on theshots-runs-on-three-platformsbranch). Part three's page-by-page walk was checked for links, assets and the pages the findings touch, not read line by line. SDR++ is not on this machine, so scenario 5 was driven with my own client against the documented rtl_tcp protocol; scenario 8 used the realmeshcore-cli.🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
A13xB0 commentedon Sep 8, 2026
Part 10: v0.0.11-dev.2 on macOS
Installed over the top of the dev.1 pass, so a warm profile rather than a fresh one this time — which turned out to be the right way to test the headline fix. macOS 26.3 (25D125), Apple M4.
Six commits since dev.1 and five of them answer findings from this pass. All twelve scenarios pass, and part two is 112 of 115 with every picture of the right window. Nothing new opened.
#711 — fixed, and the provenance line is the part I did not ask for
The header now says what it did, before anything else:
350 on a machine whose
nodefsholds 395 files from yesterday's pass — the fresh-machine number is now the only number, rather than one a new user sees once. Three consecutive runs: 350, 350, 350, andnodefsis still 395 files afterwards, so the run really does make its own and remove it.That closes the reproducibility hole and it closes it in the right direction: everybody gets what a clean machine got, and the run says so rather than leaving it to be discovered.
#714 — fixed, and the sweep is trustworthy again
and the content matches: every popout carries its own title bar (
MeshBench - Boards,MeshBench - Console,MeshBench - Events…) and theX is in its own windowfooter. Against dev.1, where all 46 were pictures of the workbench.The fix is the size test alone, with the layer test gone and a comment recording what it cost — which is the shape I hoped for.
#723 — the seed reaches the firmware, and the spread widens
Six seeds on
fixture-fife-strict:Determinism is intact — three runs at the default seed gave 350 every time — so this is the seed doing more work rather than the run becoming noisy. Worth knowing for #671: on macOS the seed moved a run before this and moves it about three times as much now.
#719 — dev releases have dev clients, so scenario 10 step 1 runs
meshbench==0.0.11.dev2is on PyPI, the pairing check accepts it, and cookbook example 3 runs end to end:That was the one thing dev.1 could not do.
Scenario verdicts
link.resultgives both margins and every termfloorat-15.8 dB measured, -26.0 dB estimatedinfer.apply -> 45matchingnodes.list, flood to 47 transmittersrx_spreadmeasuredScenario 9, all three, straight through:
A correction to my own part 9 note
I flagged the companion reconnect answering
code=1instead ofSELF_INFO. That was my client, not the workbench. After a drop-and-reserve the first frames on the wire are queued push frames (0x88), not the reply toAPP_START; wait for the queue to drain andSELF_INFOarrives as it should:A real companion client demultiplexes by code; mine read the first frame and assumed. Nothing to fix, and scenario 8 passes.
Two residuals, unchanged and already commented on
cpu nowstill renders0.0%for every node in the Nodes table (nodes.stats reports 0 CPU and 0 RSS for every node off Linux: the sampler reads /proc unconditionally #661) whilememoryandcpu timecorrectly read-.Neither is new and neither is a blocker; both are on their own issues rather than reopened.
🤖 Generated with Claude Code
https://claude.ai/code/session_019eZa5JusUZ8YCxuB2g4eYY
A13xB0 commentedon Sep 8, 2026
Claude's pass, part 10: v0.0.11-dev.2 on Windows, all twelve scenarios
Clean machine again: v0.0.11-dev.1 uninstalled,
C:\Program Files\MeshBenchremoved outright, both%LOCALAPPDATA%\meshbenchand%APPDATA%\meshbenchmoved aside,meshbench-windows-x86_64-bundled.msiverified against the release's ownSHA256SUMS(5e8dcd62…18d5c) and installed withmsiexec /qn.Issues opened: #725, #726, and meshbench.github.io#7.
First, a correction to my own last report
I have to flag a contamination in the v0.0.11-dev.1 pass. When I uninstalled dev.1, one file survived in Program Files:
Byte-identical to
C:\msys64\mingw64\bin\libstdc++-6.dll. That was mine, left behind during the #632 investigation, and it sat in the directory QEMU loads from — so it could have been what made scenario 9 pass on dev.1 rather than the shipped fix. The MSI does not ship it and the uninstaller was right not to remove it.I removed it and re-ran scenario 9 on a genuinely clean install. It passes anyway, so the dev.1 result stands — but it stood by luck rather than by my care, and the same mistake would have hidden a real regression.
Three fixes from the last pass, verified
#712 — every native node shared one identity. Fixed.
56 nodes, 56 distinct keys. The changelog's root cause —
strtoulsaturating where a long is 32 bits, so every seed above 2³² became0xFFFFFFFF— also explains the one node that differed on dev.1:Abernethy Repeaterdrew seed9001, the only one under the ceiling. A genuinely Windows-only fault.#715 — a development build had no clients. Fixed, and carefully.
The
0.0.11.dev2client attaches to the0.0.11-dev.2workbench, so the PyPI-versus-tag spelling problem is genuinely handled and not merely papered over. And the stable client's refusal is now followable:#713 — regions and relaying. Fixed, in the application repo. The engine was right and the documentation was wrong, which is the answer I could not reach from outside.
infer.applynow reads:and #711/#716 too, which I had not filed but had run into:
meshbench testnow says which storage it used, and the two numbers behave exactly as described.9. One emulated board — passes
On a clean install with no stray DLL,
Generic_E22_sx1262pinned to one node:node.outputdeserves a note for being good: asking forromon this board answers "this board's console is UART0, so the ROM's own output is at the top of the serial pane rather than in one of its own", and asking forradiobefore the node has run says so rather than returning empty.One thing that cost me half an hour, offered as a usability observation rather than a bug: pinning a board image to a node does not take effect until the project is reopened.
firmware.setwith a board reportsnodes: 1, andfirmware.startafterwards answers{"starting": true}— but the already-running native processes keep running and the node staysbackend=native. There is nofirmware.stop, so there is no obvious way to restart in place. Reopening the project is what switches it. Since a board pin is precisely the change you make in order to restart a node, saying so — or restarting the affected nodes — would save the next person the same detour.4. Waveform mode — passes; #652 is half fixed
Two of the four complaints in #652 are gone: the dangling comma is fixed, and the unclassified misses now carry a real cause in their detail rather than nothing:
Splitting measured from estimated SNR is a real improvement. What remains is the class itself: 3.6% are still labelled
unclassifiedwhen the sentence beside them names the cause, and a frame whose CRC passes is still recorded as a miss.A caution about my own method here: my first attempt used
sim.mode, which is not a verb, so the call was refused and I measured calculated mode by mistake — where 0 of 15,773 are unclassified. The verb isrf.mode. I nearly reported that 0% as a fix.6. Importing a real network — passes
Every step reports its counts, and the corrected expectation now holds:
An unscoped advert is carried by 7 distinct nodes with regions applied and 7 without — which is now what the documentation says should happen, so this scenario passes where it failed last time. I confirmed the advert genuinely originates from the node I typed at (
sent 3 → 4, and the newmessage_idis first seen from that node) rather than counting somebody else's traffic.I could not complete the other half — confirming that scoped traffic is what stops. Setting a default scope over a running node's console writes the region to the node's own
regions2on disk, butnodes.listkeeps reportingregions: nullfor it, so I had no way to confirm from outside that the advert carried a scope. Recorded on #722, which is the same divergence from the other side. I am not claiming the corrected documentation is wrong — only that I could not test that half.#726 came out of this scenario.
firmware.setpinned the repeater build across all 24 nodes and answered{"considered":24,"nodes":24}; the 6 companions among them silently never start.firmware.neededthen answers{"roles":[]}andfirmware.startanswers{"starting":true}, leaving 18 of 24 running with no error anywhere — which is the "half a mesh" the excellent no-firmware guard exists to refuse, arrived at by a route the guard does not cover.8. Companion app — passes, and #725 underneath it
Driven with the real
meshcore-cli.infosanswers with each node's own (now distinct) key;bench.dropgives the client a clean EOF; serving again and reconnecting works 3 of 3.#725: a companion learns contacts, writes them to
contacts3, and reports none to anybody.Channel messages arrive fine at the same time — the panel shows "hello from Mysteron, 1 hops" — so this is specifically the contact read path. On the last pass this looked like a consequence of the shared key; it is not, and it outlived the fix.
5, 11, 12 and the rest
5 —
sdr.servebinds127.0.0.1, answers a realRTL0header at 62,500 Hz, carries signal only while playing (mean 1.39×, p99.9 1.30×), and survives a 0.25° drag with a client attached (1.25 MB, no disconnect).bench.servestill binds[::]and hands out the LAN address, so scenario 8 still meets the Windows firewall on a first run where scenario 5 no longer does.10 — all three steps pass, and step 1 for the first time on a development build. Cookbook example 3 against the pre-release client:
It launches its own workbench on its own socket without disturbing the one already running, which is worth knowing.
meshbench test -junitexits 0 with valid JUnit andPASS: 1 assertion.MESHBENCH_BINARYis still required on Windows, since the MSI does not touchPATH— same as last pass, and still worth a line in the docs.11 — two runs at seed 9001, identical but for the wall-clock line, 350 deliveries both times.
12 — 40 of 40 pages and 22 of 22 images return 200; the
meshbench-docs-waykey is on all 40; both READMEs' four banner assets resolve; the main repo's badges and all 15 links resolve. The only 404 is the deliberately privateMeshBench/brand.docs#7 — the published site still carries the claim #718 corrected, in four places across
importing.html,fixtures.htmlandreference-control.html(the last is generated and should come right on a rebuild). #718 anticipated this and said a docs PR would follow; there was no issue tracking it, so there is one now.7. Firmware development loop — step 1 passes, #671 unchanged
meshbench devbuilds and assignslocal-maincorrectly. The sweep gives everything step 3 asks for, and the control arm matches release exactly. And three seeds still give three byte-identical runs:The warning the workbench prints for itself has got better again, and now says what the consequence is rather than only the fact:
A small wart
meshbench test -keep-node-storagewhile the workbench is running fails correctly on a node-storage lock — and then appends an unrelated hint, twice:The first sentence is exactly right; the firmware-build advice has nothing to do with a lock conflict, and it is printed twice. Not filed on its own — it seemed too small beside the rest — but easy to fix if the composition is shared.
What this pass did not cover
Part two's 109-picture inventory was not run (#714/#717 landed for macOS and my Windows fix is on
shots-runs-on-three-platforms; I did not re-verify it here). Part three's page-by-page walk was checked for links, assets and the pages the findings touch. SDR++ is not on this machine, so scenario 5 was driven with my own client against the documented rtl_tcp protocol; scenario 8 used the realmeshcore-cli.🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
A13xB0 commentedon Sep 9, 2026
Claude's pass, part 11: v0.0.11-dev.3 on Windows, all twelve scenarios
Clean machine: dev.2 uninstalled,
C:\Program Files\MeshBenchremoved, both%LOCALAPPDATA%\meshbenchand%APPDATA%\meshbenchmoved aside,meshbench-windows-x86_64-bundled.msiverified against the release's ownSHA256SUMS(b49a11ca…5b06) and installed withmsiexec /qn.The uninstaller left nothing behind this time, which retrospectively confirms that the stray
libstdc++-6.dllI found after dev.2 was my own contamination and not the MSI's.Issue opened: #729. Everything else in this pass is a confirmation.
#725 — a companion shows its contacts. Fixed.
The One Definition Rule diagnosis was exactly right. With a fresh profile pulling the rebuilt binary (
companion-v1.17.1, 1,267,725 bytes against dev.2's 1,259,224):The workbench's own panel lists them too, and
companion.stateanswers"contacts": 5withcontact: Markinch,contact: Blairgowrie_RPT… in its recent log. Both halves — the wire and the view — agree.One note for anyone else verifying this on Windows. My first three attempts appeared to fail, with
meshcore-clidying on a traceback. The traceback is the tell:That is
🏘, fromLeslie 🏘. The client had the contacts and crashed printing them, on the Windows console's cp1252 default — so a fixed build looks exactly like a broken one until you setPYTHONIOENCODING=utf-8. Not MeshBench's bug, but this fixture's node names make it certain to bite, and it cost me three runs before I read the exception rather than the exit status.#726 — a wrong-role pin. Fixed, all three checkpoints.
The changelog promises the role is checked at
set,neededandstart. All three do it.At set, a mismatched node is left alone and counted:
At needed and start, with a build that cannot resolve at all:
choicesnaming the build that would fit is a nice touch beyond what I asked for. And the practical effect on the import: 24 of 24 nodes now run, where dev.2 ran 18 and said nothing.6. Importing a real network — passes
Every step reports its counts, and the corrected expectation holds. I verified the advert genuinely originates from the node I typed at (
sent 0 → 1) before counting carriers of that specificmessage_id:Identical, which is what #718 established should happen. Scenario 6 passes.
9. One emulated board — passes
Slower to first transmission than dev.2 (325 s against 175 s); both are well past the point where a short check would call it dead.
4. Waveform mode — passes; #652 unchanged
Same shape as dev.2. The improvements from last time hold — no dangling comma, and the details name a cause (
header unreadable at 15.0 dB measured, 20.3 dB estimated) — and the class field still saysunclassifiedbeside a sentence that knows the answer.7. Firmware development loop — step 1 passes; and a half-hour detour that became #729
meshbench devbuilds and assignslocal-maincorrectly. The sweep then produced something that looked alarming:On dev.1 and dev.2 the release arm gave
1495 / 322 / 286 / 4916. All three arms agreeing now is either a real result or the arms silently not switching build, and the run record cannot tell you which — it carriesarm,seedand the measurements, and nothing about the build.I settled it by defining an arm on
definitely-not-a-build-xyz, which came back with a populatederrnaming the missing build. Sorepeater_versionis honoured, the arms do switch, and the identical numbers are a real result — explained by this release's companion fix, since this sweep's senders are companions. No regression. But establishing that took three experiments and about half an hour, which is #729: a sweep that exists to compare builds should record which build each run used.rx_spreadis still 0 on every arm, so #671 stands.The rest
1 — terrain warm reaches 6,233 of 6,233, all three emulator tools found beside the binary,
update.channelreportsdevelopmentand refusesbananawith the two valid names.2, 3 — 56 running, 18,965 events, four named miss classes, none unclassified in calculated mode.
5 —
sdr.servebinds127.0.0.1, realRTL0header at 62,500 Hz, signal only while playing (mean 1.51×, p99.9 1.98×), survives a 0.25° drag with a client attached.bench.servestill binds[::]and hands out the LAN address, so scenario 8 still meets the Windows firewall where scenario 5 no longer does.8 —
infosanswers,bench.dropgives a clean EOF, 3 of 3 reconnects.10 — all three steps. Cookbook example 3 on the pre-release client:
11 — two runs at seed 9001, byte-identical including the wall-clock line, 338 unique deliveries both times. The header says
node storage: fresh, every node factory-new, removed after the run.12 — 40 of 40 pages and 22 of 22 images return 200; the ways key is on all 40; both READMEs' assets resolve; the only 404 is the deliberately private
MeshBench/brand.On the docs correction
I closed my own
meshbench.github.io#7as a duplicate — MeshBench/docs#35 is the same finding against the source, and I filed in the published-output repo without noticing it. Status: MeshBench/docs#38 is still open,mainstill carries the line, and the site still serves it in four places (importing.html,fixtures.html,reference-control.html×2). Nothing is wrong relative to its source; the correction simply has not merged yet.What this pass did not cover
Part two's 109-picture inventory (my Windows fix is still on
shots-runs-on-three-platforms). Part three was checked for links, assets and the pages the findings touch. SDR++ is not on this machine, so scenario 5 used my own client against the documented rtl_tcp protocol; scenario 8 used the realmeshcore-cli.🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
A13xB0 commentedon Sep 9, 2026
Claude's pass, part 12: v0.0.11-dev.4 on Windows, all twelve scenarios
Clean machine: dev.3 uninstalled (nothing left behind),
C:\Program Files\MeshBenchremoved, both%LOCALAPPDATA%\meshbenchand%APPDATA%\meshbenchmoved aside,meshbench-windows-x86_64-bundled.msiverified against the release's ownSHA256SUMS(34cb298d…fd6b) and installed withmsiexec /qn.No new issues. This pass is confirmations: #729 fixed, #725 verified and closed, #726 holding.
#729 — a sweep records which build each arm ran. Fixed, and it does the job.
Every run record and arm summary now carries
builds:{"arm": "local-main", "seed": 9001, "builds": [ {"role": "companion_radio", "version": "companion-v1.17.1", "file": "meshcore-companion_radio-windows-amd64.exe", "bytes": 1267725}, {"role": "simple_repeater", "version": "local-main", "file": "meshcore-simple_repeater-windows-amd64.exe", "bytes": 1519248}], "tx": 94, "rx": 124, "delivered": 61, "collisions": 226, "err": ""}and the release arm, from the same sweep:
{"role": "simple_repeater", "version": "repeater-v1.17.1", "file": "meshcore-simple_repeater-windows-amd64.exe", "bytes": 1221710}That settles in one glance what took me three experiments and half an hour last pass. The three arms still return identical numbers:
— but now the record shows 1,519,248 bytes against 1,221,710, two different binaries under two different versions. The arms switch; the builds genuinely behave the same. Last pass I could only establish that by deliberately breaking an arm on a fake version.
Two details worth praising. Including the companion build, not just the repeater under test, is what explains this sweep's history: that 1,267,725-byte companion is the ODR-fixed one, and it is why the release arm dropped from 286 delivered on dev.2 to 61 here. Without it in the record that change would still be unexplained. And carrying
fileandbytesis the part that actually matters forlocal-main, whichmeshbench devrebuilds in place — two runs a week apart under that label are not the same binary, and now you can tell.rx_spreadis still 0 on every arm, so #671 stands.#725 — companion contacts. Verified and closed.
Fixed on two consecutive releases, both halves agreeing — the panel,
companion.state, andmeshcore-cli. I closed it, with the Windows trap recorded on the issue:meshcore-clidies onUnicodeEncodeError: '\U0001f3d8'printingLeslie 🏘under cp1252, so a fixed build looks exactly like a broken one unless you read the exception rather than the exit status. That cost me three runs on dev.3.#726 — wrong-role pin. Still holding.
The six companions keep
companion-v1.17.1and the mesh starts whole, in both arms of scenario 6.The scenarios
1 — terrain warm reaches 6,233 of 6,233, all three emulator tools found beside the binary,
update.channelreportsdevelopmentand refusesbanananaming the two valid channels.2, 3 — 56 of 56 running, 2,005 events in 90 s, four named miss classes, none unclassified in calculated mode.
4 — waveform mode confirmed via
rf.modebefore measuring. 7,369 miss events: 263 unclassified (3.6%), 9 CRC-true recorded as misses, no dangling commas. Same shape as dev.2 and dev.3 — #652 unchanged, with its two improvements holding (details name a cause, comma gone).5 —
sdr.servebinds127.0.0.1, realRTL0header at 62,500 Hz, signal only while playing (mean 1.50×, p99.9 1.99×), survives a 0.25° drag with a client reading (1.25 MB, no disconnect).bench.servestill binds[::]and hands out the LAN address, so scenario 8 still meets the Windows firewall where scenario 5 no longer does.6 — every step reports its counts (60 fetched, 24 after prune, 33,358 packets read across 470 nodes, 17 regions applied). The advert is verified to originate from the node I typed at (
sent 0 → 1) before counting carriers of thatmessage_id: 18 distinct nodes with regions and 18 without, which is what #718 established should happen.8 —
infosanswers, contacts list,bench.dropgives a clean EOF, 3 of 3 reconnects.9 —
backend=emulated, first transmission at t=250 s,emulatorlog holds onlyAdding SPI flash device, and the console answers:Time to first transmission across the three dev builds: 175 s, 325 s, 250 s. All well past where a short check would call it dead.
10 — all three steps.
pip install meshbenchstill resolves to stable 0.0.10 and the exact pin gives0.0.11.dev4; cookbook example 3 runs on it (124 events, 62 receptions in the tail).meshbench test -junitexits 0 with valid JUnit; the ways key is on all 40 pages.11 — two runs at seed 9001, identical but for the wall-clock line (22 s against 21 s), 338 unique deliveries both times, header confirming fresh node storage.
12 — 40 of 40 pages and 22 of 22 images return 200; both READMEs' assets resolve; the only 404 is the deliberately private
MeshBench/brand.Still outstanding on the docs side
MeshBench/docs#38(regions) and#39(the #729 documentation) are both still open, so the published site continues to carry the old regions claim in four places —importing.html,fixtures.html, andreference-control.htmltwice. Nothing wrong relative to its source; tracked onMeshBench/docs#35.What this pass did not cover
Part two's 109-picture inventory (my Windows fix is still on
shots-runs-on-three-platforms). Part three was checked for links, assets and the pages the findings touch. SDR++ is not on this machine, so scenario 5 used my own client against the documented rtl_tcp protocol; scenario 8 used the realmeshcore-cli.🤖 Generated with Claude Code
https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG
changelog: 0.0.11 (#736)
A13xB0 commentedon Sep 9, 2026
Part 11: v0.0.11, the release, on macOS
MeshBench-arm64-bundled.dmgfrom v0.0.11. Scenario 1 was run from a genuinely clean profile —~/Library/Caches/meshbenchand~/Library/Application Support/meshbenchboth moved aside first — and the rest followed on the profile that first launch built. macOS 26.3 (25D125), Apple M4.All twelve scenarios pass. Part two is 112 of 115, every picture of the right window. One issue opened: #737.
Scenario verdicts
ground: terrain6233/6233, GPU warm 71,253 pairs in 8.4 slink.resultreturns both margins and every termSELF_INFO, drop kills the socket, reconnect worksThe five fixes from this release, checked
#733 — regions reach a running node.
infer.applynow answerslivebesideapplied:0before firmware is up,45after. The distinction is real and it is the right one to have added.#727 — a wrong-role pin is refused and counted. Pinning
companion-v1.17.1torole: simple_repeatergives{"nodes": 0, "mismatched": 48}rather than pinning it and failing minutes later.#730 — a sweep records which build each arm ran. Every arm and every run now carries
buildswith role, version, filename and bytes, and it answered a question I had before I could go looking:Two genuinely different binaries, 96 bytes apart — so the arms differing is provenance, not a fault.
#735 — waveform names a collision, and a passing CRC is a reception.
unclassifiedfell from 62 to 3 in a comparable run, withcollisionandinterferencepicking up what it was absorbing:unclassifiedcollisioninterferencereceived#711 and #714 hold in the release.
meshbench testprintsnode storage: fresh, every node factory-new, removed after the run, leavesnodefsat 0 files, and gives 318 three times running. The sweep's popouts are 900×672 and node windows 820×652.Scenario 7 is now doing its job
Three arms — my local build, the release build, and a control duplicating the release — over three seeds:
controlreproducesreleaserun for run, the seeds move each arm, and the warning is finally the sentence the method needs:Against every earlier pass, where the between-arms figure was 0.0 % and every difference cleared a noise floor of zero.
Seeds keep widening
Five seeds on
fixture-fife-strict, across the three builds I have measured:Determinism is intact throughout — three runs at the default seed gave 318 every time.
#737 — the one thing I would not ship without a decision
On a running mesh,
infer.applyanswers{applied: 45, live: 45}and within half a second everything readable says zero. Three consecutive cycles:The engine is fine — with the view sitting at
0 of 60a companion's scopedpublicmessage relayed to 49 distinct transmitters and 375 receptions. It is only what a script or a panel can read.It matters more in this release than it would have in the last one, because docs#35 landed at the same time and the corrected
importing.htmlnow sends the reader at exactly this field, twice — once at step 5 and once in the "If nothing relays" checklist. The docs fix is right; it just points at a column that reads zero on a mesh where all 45 nodes hold regions.I could not isolate the transition — in one session the count held at 45 for seven minutes of polling before I could get it to fail, and
firmware.set,console.type,nodes.select,sim.pauseandsim.playare all ruled out. Once a session is in the state it is completely repeatable, which is what the three cycles above are.Two residuals, unchanged
cpu nowstill renders0.0%for every node whilememoryandcpu timecorrectly read-(#661), and the Compare section title still sits flush at x = 0 (#669). Both already commented on their own issues; neither is new and neither blocks.One methodological note
Scenario 5's first sample read 0.00 % above the floor while playing, which looks exactly like a broken stream. It was the empty-window trap the earlier passes warned about: driving six adverts and re-sampling gave 1.89 % playing against 0.00 % paused. That is the second time in this pass series it has nearly produced a false report, and it is worth a line wherever the scenario is written down: sample a window you have put traffic into, not one you hope is busy.
🤖 Generated with Claude Code
https://claude.ai/code/session_019eZa5JusUZ8YCxuB2g4eYY