Skip to content

Pre-release: the human test pass #318

Open

Description

@A13xB0

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

  1. Download the latest release artefact for Linux, chmod +x, run it.
  2. Expect: the workbench opens with no other installation, warms the default fixture, and the status bar carries the best-case honesty line.
  3. With MESHBENCH_CARTO_KEY set 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.
  4. 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.

  1. File, Open a saved network, fixture-fife-strict.
  2. Simulation, Start firmware on every node; watch the count reach 58.
  3. Press play; double-click a repeater, and on its Console tab type advert.
  4. 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

  1. In the run from scenario 2, open the Debug view.
  2. Pick a failed reception in the Inspector; read its cause.
  3. 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

  1. Configuration, RF Simulation: switch to waveform. Expect the status bar to name the mode and the run to continue without cutting a frame.
  2. Let two nearby repeaters relay the same flood; in Debug, expect collision or capture verdicts that calculated mode would not produce.
  3. Switch back to calculated; expect the mode stamped correctly again.
  • passes as described

5. SDR observer

  1. Open the Glasgow SDR Observer node window, SDR pane, serve rtl_tcp.
  2. Connect SDR++ with the documented settings (https://meshbench.github.io/docs/sdr-observer.html).
  3. 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.

  1. Add areas, fetch, commit, read traffic, apply regions - the numbered buttons in order.
  2. Expect the counts reported at each step, regions applied to a sensible number of nodes, and a flood that actually relays afterwards.
  3. 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

  1. meshbench dev -from <your MeshCore checkout>; expect the build to land in the firmware library named after the branch and assigned to repeaters.
  2. Bench view: define a two-arm sweep, your build against the release, three seeds, a control arm.
  3. 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

  1. App view, Companion bench: give me a mesh and an endpoint (TCP).
  2. Connect meshcore-cli or your own client; expect real protocol bytes, infos answered.
  3. 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

  1. Assign the Generic_E22_sx1262 published image to a single node (one board at a time; they are heavy).
  2. 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

  1. Run cookbook example 3 (small mesh with traffic) in Python: expect it to build the mesh, run, and print event counts.
  2. On the docs site, flip a ways block to Python; expect every page to remember the choice.
  3. 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

  1. Run the same fixture and seed twice (native firmware), same duration.
  2. Expect identical event totals both times.
  • passes as described

12. Docs site and READMEs, as a reader

  1. Read the Learn path start to finish (Overview through Reading a result); note anything unclear or wrong as a comment here.
  2. 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.
  3. 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

  1. added this to the v0.1.0-alpha.1 milestone on Aug 30, 2026
  2. A13xB0 commented on Sep 1, 2026

    @A13xB0
    CollaboratorAuthor

    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) CARTO attribution 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, advert at Abernethy Repeater answered -> OK - Advert sent, flood spread, Events filled (17.9k events, Sent 1834 / Received 477).

    • N1 (docs, not code): the count reaches 56, not 58. The scenario says "watch the count reach 58". firmware.state returns {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.
    • N2 (UX): opening a network does not fit the map to it. After File > Open, the viewport stays where it was, so fife-strict appears as a tiny cluster in the corner of a mostly-empty map and the flood is invisible. You have to know to press 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 duplex
    • its demodulator was already locked to <node>; a LoRa receiver decodes one packet at a time
    • decoded its header, then 133 symbol(s) were destroyed by a collision
    • would have decoded at 15.0 dB, lost to a stronger interferer

    The 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.

    • N3 (bug, code): floor is a catch-all and mislabels congestion as weak signal. EventClass (internal/sim/engine/events.go:137) prefix-matches the detail string and returns ClassFloor as its default. So receiver-lock misses and collision misses are both counted as floor - "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 classed floor and 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.
    • N4 (bug, UI): the Link panel is unusable docked. Docked in the right sidebar it renders no cut-through at all, and the pair label overflows into a one-character-wide column down the right edge, wrapping per character ("s / c / o / - / A / v / o / n ..."). Pop it out to its own window and it renders perfectly. So the panel has no minimum width and no horizontal handling.

    4. Waveform mode - PASSES, and better than asked

    Switching to waveform stamps waveform RF in the status bar and the run continues uninterrupted. The nice detail: the honesty line itself changes, from ... bare earth, ideal demodulator to ... bare earth, real coding chain - it drops the caveat that no longer applies. Switching back restores calculated RF and 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

    • N5: the packet timeline has no legend - red / orange / green marks encode something a newcomer cannot read.
    • N6: node names truncate with no tooltip in both the Nodes table and the Inspector's From/To (Abernethy Rep..., sco-tay-moncr...), and several similar names collide once cut.
    • N7 (scripting): nodes.select_many takes 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.
    • N8 (scripting): rf.mode is setter-only - calling it with no argument errors instead of returning the current mode, so a script cannot ask what mode it is in.
    • Positive: refusals are unusually good. workspace.set with a bad view answers no 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 by TestRadioStackIsDeterministic, which passes.

  3. A13xB0 commented on Sep 1, 2026

    @A13xB0
    CollaboratorAuthor

    Claude's pass, part 2 (scenarios 10 and 11)

    11. Determinism spot-check - PASSES

    Two runs of fixture-fife-permissive at the shipped seed produced 162 unique deliveries both times, and the two logs are byte-identical once the wall-clock timing line is removed. TestRadioStackIsDeterministic agrees at the frame level.

    10. Scripting and the clients - step 3 FAILS

    meshbench test -fixture fixtures/fixture-fife-strict.json -junit out.xml does check assertions and does write valid JUnit, but it exits 1, not 0, because the shipped fixture fails its own assertion:

    fixture-fife-strict: 58 nodes, 56 running firmware, SF8 at 62 kHz, seed 9001
    provisioned 56 nodes with 594 lines
        20.0 s  AngusOutlaw1: public hello from Angus
        ... six public messages from three companions ...
    FAIL  at least 10 unique deliveries
            0 unique deliveries, wanted at least 10
    meshbench: 1 of 1 assertions failed
    

    Not a flake, and not the harness: the same command on fixture-fife-permissive passes with 162 unique deliveries and exits 0. So the machinery works and it is fife-strict specifically that delivers nothing.

    • N9 (bug): fixture-fife-strict fails 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 hello with 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.
    • N10 (process, and the reason N9 survived): nothing in CI runs meshbench test. grep over .github/workflows finds 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

    scenario verdict
    1 install and first launch passes (step 4, key-unset relaunch, not done)
    2 first simulation passes, notes N1 N2
    3 debugging a missed packet passes on substance, faults N3 N4
    4 waveform mode passes (step 2, collision verdicts, not done)
    10 scripting and clients step 3 fails, N9 N10
    11 determinism passes

    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.

  4. A13xB0 commented on Sep 1, 2026

    @A13xB0
    CollaboratorAuthor

    Claude's pass, part 3: a genuinely fresh install

    Re-ran scenario 1 properly, from an empty cache. XDG_CACHE_HOME and XDG_CONFIG_HOME pointed 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.

    after a first launch, nothing clicked size
    terrain/ 513 MB, 6,233 files
    tiles/ 296 KB
    matrix/ 824 KB
    firmware/ nothing

    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.html says 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:

    measuring every link - 0% (0 of 71253)
    

    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:

    Bytes rather than one step: the job used to sit at 0 of 1 until the file landed, which on a slow link is what a stall looks like.

    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.state reporting jobs: 2, the status bar showed only 4873 links, weighted by the weaker direction's margin. The download had nowhere to appear.

    N14: no progress is reachable programmatically

    session.status returns the finished summary and never the running job. ui.state returns a bare jobs: 2 with 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.state still reported jobs: 2 after 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.list answers with a count where the rows were the question

    resource.list -> {"rows": 5}
    

    The 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.html is 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

    1. Download the tarball release, not the AppImage or the .deb: those two carry radioserver but not the emulators, at 110 MB against a 26 MB AppImage.
    2. Or, from a source checkout, build radioserver from meshcore-native with ./build.sh radioserver out, obtain a QEMU carrying our SX1262 device and configured --enable-gcrypt, and a Renode carrying the SEVONPEND fix.
    3. Put them beside the binary or in ~/.cache/meshbench/tools/. Not on PATH: a desktop application inherits nothing useful from a shell.
    4. Then download a board image from the firmware library and set a node's role to it.

    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.

  5. A13xB0 commented on Sep 1, 2026

    @A13xB0
    CollaboratorAuthor

    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/main at 8a6c509, which stamps itself dev+8eb4156. One mesh at a time. Every code reference below I re-checked against main afterwards and it still holds. Notes continue from N16.

    The headline: scenario 9 now passes - an emulated Generic_E22_sx1262 boots, adverts on the air unprompted and answers its console, three runs of three. Scenario 7 fails outright: meshbench dev truncates 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 holds radioserver, qemu-system-xtensa and renode, and the new Setup page finds all three and says where. setup.check reports "ready": 6, "missing": 1 with only the Nordic SoftDevice outstanding, which is honest and exactly the page N16 asked for.

    Three nodes on a blank network, Emu set to Generic_E22_sx1262 and pinned to the published simple_repeater v1.17.0 board image, the other two native. QEMU comes up with our own machine and radio wiring:

    qemu-system-xtensa -machine esp32,radio-path=.../nodefs/Emu/radio.sock,radio-spi=2,
      radio-nss=18,radio-busy=32,radio-dio1=33,radio-fem=13 ...
    radioserver: listening on .../nodefs/Emu/radio.sock
    radioserver: joined the engine at 127.0.0.1:38039
    radioserver: emulator connected
    

    It boots, provisions, and adverts unprompted:

    {"at_ms": 35750, "class": "sent", "detail": "114 bytes, 586 ms on air",
     "from": "Emu", "kind": "tx", "message_id": 3129343913604550904}
    {"at_ms": 36336, "class": "received", "detail": "first time this node heard the message",
     "from": "Emu", "to": "Ears1", "snr_db": 15}
    {"at_ms": 36336, "class": "received", "detail": "first time this node heard the message",
     "from": "Emu", "to": "Ears2", "snr_db": 6.96}
    

    nodes.stats puts it beyond doubt: {"name": "Emu", "backend": "emulated", "sent": 1, "last_sent_to": "Ears2", "irq_reads": 25860}, and both native nodes heard: 1 from Emu. The console answers:

    > get name
      -> > Emu
    > get repeat
      -> > on
    > advert
      -> OK - Advert sent
    

    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.

    • N17 (UX): a node's console shows nothing of its own boot. console.read on a node that has been running for minutes returns {"lines": 0}, because consoleFor attaches 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.
    • N18 (determinism): an emulated node is not deterministic in time. Same seed, same scenario, three runs: the advert went out at 35.75 s, 41.20 s and 34.79 s. The message id was identical all three times, so the content is stable and only the timing moves - the emulator free-runs against the wall rather than against the engine's clock. Worth saying out loud somewhere, because "same seed, same result" is stated as a feature and this is a real exception to it.

    7. Firmware development loop - FAILS

    N19 (bug, and the one I would fix first): meshbench dev destroys 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:

    $ MESHBENCH_CONTROL_SOCKET=/tmp/no-such-socket meshbench dev -from ~/msim/MeshCore -name local-p4a
    local-p4a  .../firmware/native/local-p4a/meshcore-simple_repeater-linux-amd64  0.6 MB
      workbench not running, so it is cached but not loaded
    -rwxr-xr-x 583768  meshcore-simple_repeater-linux-amd64
    
    $ MESHBENCH_CONTROL_SOCKET=/tmp/mb318.sock meshbench dev -from ~/msim/MeshCore -name local-p4b
    local-p4b  .../firmware/native/local-p4b/meshcore-simple_repeater-linux-amd64  0.6 MB
      in the workbench's firmware library
    -rwxr-xr-x      0  meshcore-simple_repeater-linux-amd64
    

    firmware.Build already writes the binary into the cache at native/<label>/<NativeBinaryName(role)>, and that is the path it reports. cmd_dev then hands that same path to firmware.import, which calls firmware.Import, which computes dst as exactly the same path and calls copyFile(src, dst, true). copyFile opens the source for reading and then opens the destination O_WRONLY|O_CREATE|O_TRUNC - the same file - so io.Copy reads 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:

    node.start -> engine: firmware on 1 of 3 nodes; 2 failed, first: Ears1:
      firmware: no native builds published for MeshCore local-study-07-ack-redundancy
      Build one with meshcore-native's build.sh, or set MESHBENCH_NATIVE to your own
    

    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 an MSIM-<n> and I do not have one.

    Step 1 otherwise does what it says: the build is named after the branch (study/07-ack-redundancy becomes local-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) and control (also repeater-v1.17.0) - three seeds, two senders, on fife-permissive. The run log is genuinely readable while it goes; experiment.state returns a rolling log and session.status carries {"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:

    arm runs tx rx delivered redundant collisions rx_spread at_risk_2db
    mine 3 0 0 0 0 0 0 absent
    release 3 138.7 70 58.7 11.3 323.3 0.043 0
    control 3 120.7 68 57 11 266.3 0 0
    • N20 (UX): a failed arm reads as a catastrophic result. All three mine cells errored with the N19 failure, and the per-arm table renders that as delivered: 0 beside release: 58.7 with nothing on the row to say the arm never ran. The truth is only in the per-run err field and a top-level warning. The at_risk_2db key is correctly absent rather than zero, which is exactly the distinction the code comments argue for - the same care has not reached delivered.

    • N21 (bug): the control arm is not identical to the arm it duplicates, and the seed is not what separates the runs. control and release are the same configuration and the same three seeds. They disagree on every column. Worse, look at the per-run rows:

      release 9001  tx 176  rx 74  delivered 62  collisions 493  airtime 164293
      release 9002  tx 120  rx 68  delivered 57  collisions 234  airtime 118929
      release 9003  tx 120  rx 68  delivered 57  collisions 243  airtime 118929
      control 9001  tx 120  rx 68  delivered 57  collisions 243  airtime 118929
      control 9002  tx 121  rx 68  delivered 57  collisions 278  airtime 119732
      control 9003  tx 121  rx 68  delivered 57  collisions 278  airtime 119732
      

      control at seed 9002 and at seed 9003 are byte-identical. So are release 9002 and 9003 in everything but collisions. And control at 9001 is identical to release at 9003 - a different arm, a different seed, the same row. What varies is the position in the run order, not the seed. rx_spread for control is therefore 0, 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.set with a role updates the view for every node. The engine-side loop honours the role filter; the world-side loop is

      for i := range w.Nodes {
          if node == "" || w.Nodes[i].Name == node {
              w.Nodes[i].Firmware = version
          }
      }

      so after meshbench dev the status bar says 46 nodes pinned to local-study-07-ack-redundancy (correct) while nodes.list shows 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, Fife as the area, the panel's numbered steps in order. Counts are reported at every step and they are good:

    1. add area   boundary.set "Fife" -> 2 matches; boundary.accept -> {"accepted": "Fife", "areas": 1}
    2. fetch      {"nodes": 59, "records": 488, "skipped_no_position": 44, "uncertain": 0}
    3. commit     {"nodes": 59, "strategy": "replace-all"}   ->  750 links
    4. read traffic   "read 39574 packets, 398 nodes seen"
    5. apply regions  {"applied": 44}
    

    And a flood then does relay. With regions applied before firmware starts, advert at Abernethy Repeater produced 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.

    • N23 (bug): infer.apply says 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.list reports regions on 4 of 59, and nodes.regions answers {"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 advert still floods, 22 distinct transmitters, exactly as with regions applied - adverts are unscoped;

    • a companion's public message was transmitted once and received by nobody (1 transmitter, 0 receivers), against 1 transmitter, 6 receivers for 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 repeat on its console." On a repeater holding no regions whatsoever, get repeat answers -> > 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:219 lists {"id":9,"method":"infer.result"} between infer.run and infer.apply. The live answer is infer.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 with HandleInternal and gated in socket.go, so it can never be reached from outside. reference-control.md:68 repeats the same dead step in its flow line, infer.run {hours:168} -> infer.result -> infer.apply. The ::python and ::go lanes 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-cli is not installed on this machine, so I wrote a raw socket client instead. bench.serve gave {"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_START framed as '<', little-endian length, payload, the reply is real protocol:

    frame '>' len=66 code=5   (RESP_CODE_SELF_INFO)
    050116164716516b1e93ccf945737e365342a680545bcbaa7d26d3443271f4d3951e54e2
    00000000000000000000000040d6011a40d6011a08053437313635313642
    adv_type=1  tx_dbm=22  max_tx=22
    pubkey 4716516b1e93ccf945737e365342a680545bcbaa7d26d3443271f4d3951e54e2
    

    That is what infos prints. Then bench.drop answered {"dropped": 1} and the client saw the socket die immediately: recv returned b'', and a write followed by a read also returned b''. Serving again and reconnecting worked first time and returned the same SELF_INFO.

    • N27 (UX): serving again moves the endpoint. The port is allocated with port zero, so the first serve was :46251 and 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.serve answered {"node": "SDRObs", "addr": "127.0.0.1:41477", "rate_hz": 250000} and the stream begins with a genuine dongle-info header, 52544c30 00000005 0000001d - magic RTL0, 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:

    rms peak samples above magnitude 20
    playing 14.27 89.90 744,807 of 7,487,387 (9.95%)
    paused 3.56 13.51 0 of 3,737,688 (0.00%)

    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.move calls 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.59 against 3.56, which I very nearly wrote up as a defect. It was an empty window.

    • N28 (scripting): nodes.move takes name where every other node verb takes node. nodes.move with {"node": ...} is refused with nodes.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, with MESHBENCH_BINARY pointed at the build under test, built the mesh, ran ten minutes of simulated time and printed:

    MeshBench: calculated reception, default excess loss - a best case; no multipath,
    no body loss, no oscillator error
    162 events, 63 receptions in the tail
    

    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. Clicked python once on importing.html: all six ways blocks on the page moved from gui to python and localStorage["meshbench-docs-way"] became "python". Then navigated to fixtures.html, debugging.html and testing-repeaters.html with no further clicks, and every one came up on python, 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 more h2s, 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 with aria-expanded handled 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 is rgb(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, and infer.result is the only verb the docs name that does not exist. reference-cli.md's tables match cmd/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.md now 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.

    • N32 (bug): "File, Open a saved network" cannot open a shipped fixture on a fresh install. first-simulation.md:10 and fixtures.md:20 both open with "File, then Open a saved network" and then name a shipped fixture. That menu item runs project.list, which reads only os.UserConfigDir()/meshbench/projects; nothing seeds the shipped fixtures into it and no menu item lists fixture.Embedded(). Measured live: project.list returned {"dir": "/home/alex/.config/meshbench/projects", "projects": ["fixture-scotland-ireland-permissive", "fixture-scotland-ireland-strict"]} on a machine that has been used for months, and fixture-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.
    • N33 (docs): testing.md's worked example is stale and names a node that does not exist. Line 154 shows node Kirkcaldy Companion, 58 nodes running real MeshCore firmware. There is no Kirkcaldy Companion in fixture-fife-strict; cmd_serve.go picks the first companion_radio in file order, which is AngusOutlaw1. The count is the N1 error again: firmware.state returns 56 running of 58. app-development.md:13 has the identical block correct, so this is a copy that did not get updated.
    • N34 (docs): "reach the node total" is the exact wording that produced the 56-of-58 bug. It appears in first-simulation.md:17, fixtures.md:28 and testing-repeaters.md:94. The running count reaches 56 and the node total is 58, which is what enginefirmware.go's own comment is about. Three pages tell the reader to wait for a number that does not arrive.
    • N35 (docs): concepts.md:96 lists five node kinds and there are six. It omits advanced-repeater, which is a real Kind and is present in every shipped fixture. fixtures.md:14 lists all six, so the two pages disagree.
    • N36 (docs): the glibc floor is wrong, and the example distribution is below it. docs/install.md:37 in the main repo, which getting-started.md links 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, and getting-started.md itself says 2.35. Fedora 35 ships 2.34 and would not run it.
    • N37 (docs): getting-started.md disagrees 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, downloads meshbench-linux-x86_64.tar.gz and untars it. The .deb the 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.
    • N38 (bug): the main README has a broken image. README.md:49 references docs/workbench-plan.png; the file is at docs/images/workbench-plan.png. Broken for every reader.
    • N39 (bug): two of the five README badges are dead. The CI badge .../actions/workflows/ci.yml/badge.svg returns 404 and renders broken, and the release badge returns 200 with the SVG title release: no releases or repo not found despite four real releases, v0.0.4 being the latest. Both because shields.io and camo fetch anonymously and the repository is private. The three static badge/ 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.
    • N40 (UX): the docs site is public and almost every link it makes into the repository 404s. https://meshbench.github.io/docs/ serves fine, but the clone URL, the releases page, docs/install.md linked from getting-started.md, and both meshbench-dev-skills and meshbench-scripting-skills are private and give a reader outside the org a 404. Only MeshBench/meshbench-reports resolves. 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. SIGQUIT says exactly where:

      goroutine 10 [select, 16 minutes]:
      internal/firmware.(*Bridge).WaitAdvance(...)      firmware/lockstep.go:48
      internal/sim/engine.(*Engine).runFirmware(...)    engine/firmwarerun.go:50
      internal/sim/engine.(*Engine).Step(...)           engine/engine.go:383
      internal/app/session.Register.func3.1(...)        session/verbs.go:175
      internal/app/state.(*Store).Run(...)              state/store.go:199
      

      The store's own goroutine is inside a tick, waiting for a firmware bridge to acknowledge an advance. WaitAdvance ends on an ack, on the node's done channel, or on the context - and the tick calls s.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.stop on every node before project.new made 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 "" prints loading: fixture: nothing named and carries on. w.Tick is only ever installed by installBody, which nothing on that path calls, so pressing play sets playing: true and advances sim.state's now_ms forever while the engine is never stepped: events stays 0, nothing transmits, and typed console commands queue up unanswered. sim.step is silent about it too - it returns now_ms and does nothing when w.Tick is nil. project.new installs the tick and fixes it, so a person who does File, New network never meets this; a script that launches with a mistyped -fixture gets a workbench that looks completely healthy and is inert. Measured: sim.state reported now_ms: 390910 while the engine's own clock was at 1800 ms.

    Smaller

    • N31 (scripting): nodes.list's sent and heard are always zero. Nothing in the tree ever writes state.Node.Sent or .Heard. With nodes.stats reporting Emu sent 1 and the Scoreboard panel drawing Emu 1, nodes.list still returned sent 0 heard 0 for every node. The panels read NodeStat and 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.
    • Positive, and worth recording: the new Setup page is a direct answer to N16. It names every tool, where it was found, what it cost on disk, and for the one thing missing, what to press. session.status now carries a job object with done, total and what, which closes most of N14.

    Running total

    scenario verdict
    5 SDR observer passes on substance; SDR++ itself not driven
    6 importing a real network passes, faults N23 N24 N25 N26
    7 firmware development loop fails, N19 N20 N21 N22
    8 companion app passes, note N27
    9 one emulated board passes, 3 runs of 3, notes N17 N18
    10 step 1 cookbook in Python passes
    10 step 2 docs language toggle passes
    12 docs site and READMEs passes with faults, N32 to N40

    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:

    • N19 - meshbench dev truncates 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.
    • N29 - the whole application can be wedged permanently from a menu item, with no timeout anywhere in the path to end it.
    • N32 - "File, Open a saved network" cannot open a shipped fixture on a fresh install, and that is the first instruction of the first documented walkthrough.
    • N21 - the Bench's control arm does not reproduce the arm it duplicates, two of three seeds give bit-identical rows, and rx_spread therefore 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.

  6. A13xB0 commented on Sep 2, 2026

    @A13xB0
    CollaboratorAuthor

    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 bare meshbench.exe is what the Start menu runs, and it exited 2 with nothing on screen, which is #499.

    scenario verdict
    1 install and first launch passes with #503; fails outright without it
    2 first simulation passes
    4 waveform mode passes
    9 one emulated board fails - the toolchain is found, the image cannot be fetched (#516)
    10 step 3 meshbench test -junit passes - N9 is fixed
    11 determinism passes
    3, 5, 6, 7, 8, 12, 10 steps 1-2 not reached

    1. Install and first launch - passes, and most of part 3's notes are answered

    From a genuinely empty cache (LOCALAPPDATA and APPDATA pointed 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:

    cache: 0.2 MB in 20 files
    

    Terrain is not fetched until it is allowed. sim.state says so properly:

    no terrain: nothing under this study is cached and nobody has been asked whether it may be downloaded, so this is bare earth, which the propagation model treats as free space: the most optimistic answer it has... terrain.allow settles it either way

    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:

    t+ 3s  tiles: fetching terrain, 0 MB of about 499 MB    1/6233
    t+15s  tiles: fetching terrain, 33 MB of about 499 MB   368/6233
    t+27s  tiles: fetching terrain, 59 MB of about 499 MB   768/6233
    

    N13 is fixed - session.status's job field carries that tiles job, so the running download is what surfaces rather than the links job behind it. N14 is largely answered by job.list, which returns rows with done, total, what, finished and failed. N15 is fixed - resource.list returns 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, advert at 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.mode to 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: waveform RF: reception is decided by the full receive chain - demodulation, FEC and CRC
    • back to calculated: calculated RF: reception is decided by link budgets and demodulator floors - the fast model

    N8 is fixed. rf.mode with 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

    EventClass no longer prefix-matches the detail prose - it reads ev.Class, with ClassUnclassified where it used to fall through to floor. Measured over 3,000 events of one waveform run, the classes separate properly and floor holds only real floor misses:

    collision      decoded its header, then 165 symbol(s) were destroyed by a collision
    floor          waveform: no preamble lock at -11.1 dB measured SNR
                   waveform: header unreadable at -10.5 dB measured SNR
    half-duplex    its own transmitter was keyed; LoRa is half duplex
    interference   would have decoded at -3.7 dB, lost to a stronger interferer
    receiver-busy  its demodulator was already locked to sco-tay-moncreiffe; a LoRa receiver decodes one packet at a time
    

    Part 1 found 37 of 357 misses classed floor and not one of them a floor miss. That is gone.

    10 step 3 - passes. N9 is fixed

    $ meshbench test -fixture fixtures/fixture-fife-strict.json -junit out.xml
    fixture-fife-strict: 58 nodes, 56 running firmware, SF8 at 62 kHz, seed 9001
    provisioned 56 nodes with 526 lines and 67 frames
    ok    at least 10 unique deliveries
            317 unique deliveries, wanted at least 10
    PASS: 1 assertions, 17s     exit 0
    

    N10 is still live. grep over .github/workflows still finds no invocation of meshbench 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-strict at 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:

    Emulator toolchain
      ready   radioserver          C:\Program Files\MeshBench\radioserver.exe
      ready   qemu-system-xtensa   C:\Program Files\MeshBench\qemu\bin\qemu-system-xtensa.exe
      ready   renode               C:\Program Files\MeshBench\renode_1.16.1-portable\renode.exe
      ready   s140 SoftDevice      C:\Users\amy\AppData\Local\meshbench\softdevice\s140-6.1.1
    

    setup.check reports ready: 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 offers v1.17.1 and the fetch asks for releases/tags/v1.17.1, which does not exist - MeshCore tags are repeater-v1.17.1. boardcheck/probe.go already 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:

    Windows-only faults, filed separately

    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.

  7. added
    P0Release blocking: fix before anything else
    on Sep 2, 2026
  8. A13xB0 commented on Sep 6, 2026

    @A13xB0
    CollaboratorAuthor

    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:

    bucket steps
    Views 6
    Panels 72 — every panel docked, and again popped out
    The node window 10 — one per tab
    The board view 3
    Configuration 6
    Licences 5
    Menus 7
    total 109

    Run it with:

    go build -o meshbench ./cmd/meshbench
    tools/shots/shots.py
    

    The list cannot go stale. internal/ui/workbench/shotsteps_test.go reads tools/shots/steps.json against panelMenus — 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:

    • The board view had no flag at all. It opens from a button on a node's Hardware tab and nothing else, so no capture could reach the one window this release just shipped and documented. -board-view and -board-tab now exist.
    • -play alone 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 -inject too: 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

  9. A13xB0 commented on Sep 6, 2026

    @A13xB0
    CollaboratorAuthor

    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.msi from the v0.0.7 release, SHA256 checked against the release's own SHA256SUMS (871b123c…6cfb, matches), installed with msiexec /qn, exit 0. Windows 10 Home 22H2 (19045). Driven over the control socket, with screenshots.

    > meshbench.exe -version
    meshbench v0.0.7, go1.25.13, bundled
    

    Issues opened this pass: #607, #608, #609, #610, #611, #612, #613.


    Scenario verdicts

    scenario verdict
    1 install and first launch passes; steps 3-4 not testable on a release build, see below
    2 first simulation passes, one wording note
    3 debugging a missed packet partial — causes verified, cut-through not reached
    4 waveform mode passes
    10 step 3 meshbench test -junit passes
    11 determinism passes
    12 docs and READMEs partial — #612
    5, 6, 7, 8, 9, 10 steps 1-2 not reached
    part two (109 pictures) not run — needs the source tree, and #613 breaks its framing

    1. 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 says this 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_KEY set and then unset, expecting CARTO and then a fall back to OpenStreetMap. But package.yml stamps 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:

    watermarked

    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:

    clean

    The dates settle it. Same tile, same day, four hours apart: carto-dark/9/251/158 at 12:03 clean, carto-light/9/251/158 at 15:57 watermarked.

    cached

    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 build before installing a release hits it. Filed as #607.

    2. First simulation — passes

    fixture-fife-strict opened, firmware started on 56 nodes, played, advert typed at Abernethy Repeater:

     123.530  > advert
     123.530    -> OK - Advert sent
    

    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.html says "choose fixture-fife-strict to 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:

    debug

    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.status explains itself properly: calculated RF: reception is decided by link budgets and demodulator floors - the fast model.

    waveform

    10 step 3 and 11 — both pass

    N9 is fixed and stays fixed. meshbench test -fixture fixture-fife-strict.json -junit out.xml gives 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-strict carry emoji, and every one outside the BMP is a box, everywhere a name appears:

    nodes

    map

    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 ships LICENCES/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 🌤️� and Kilcreggan Rpt ☀️��, ten occurrences across four shipped fixtures. The signature is a UTF-8 sequence cut mid-character: Lucklaw Hill in the same file is U+26F0 U+FE0F U+1F324, missing the trailing U+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 carries multi_byte_status fields 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.sock and 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.json holds 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 — -look ignores its zoom

    -look 56.33,-3.32,8 and -look 56.33,-3.32,14 render the same near-whole-world view; z16 shows Greenland. The centre is honoured, only the third field is dropped.

    look

    This matters for part two of this issue: shots.py frames its captures with -look, so every map-bearing picture in the 109 would come out at world scale — and shotsteps_test.go would 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-windows branch under docs/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

  10. A13xB0 commented on Sep 6, 2026

    @A13xB0
    CollaboratorAuthor

    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.msi from the v0.0.8 release, SHA256 checked against the release's own SHA256SUMS (71052c6a…a0ce, matches). Then, before installing:

    • uninstalled the v0.0.8 MSI I had put on for the upgrade test — Program Files\MeshBench removed cleanly, its Start menu entry with it
    • removed %LOCALAPPDATA%\meshbench (516 MB) and %APPDATA%\meshbench
    • reinstalled from the MSI, msiexec /qn, exit 0
    • launched bare meshbench.exe, which is what the Start menu shortcut runs, and touched nothing
    > meshbench.exe -version
    meshbench v0.0.8, go1.25.13, bundled
    

    Six of the seven from part 6 are fixed and verified. One is half fixed. One new P1: #622.


    The seven from part 6

    issue verdict on v0.0.8
    #607 basemap cache serves keyless tiles half fixed — new poisoning prevented, existing caches still served
    #608 emoji draw as boxes fixed — the bundle now carries the font
    #609 U+FFFD in fixtures fixed — zero, in both spellings
    #610 two workbenches, port 0 fixed
    #611 control.json orphaning fixed
    #612 control socket reference fixed for Windows and the handshake; count still says 256 for 255 verbs
    #613 -look ignores zoom fixed

    Details 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:

    emoji before and after

    #613 also unblocks part two of this issue — shots.py frames captures with -look, so the 109-picture sweep can now be taken at the zoom each step intends.

    look 12

    #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:

    terrain status line

    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:

    partial terrain: 127 of 256 sampled tiles under this study are cached and the rest is bare earth, so some of this answer is free space and nothing in it says which part

    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 to state: "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 9001 gave 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 a ground note you have to go looking for.


    Scenario verdicts

    scenario verdict
    1 install and first launch passes, with #622 against it
    2 first simulation passes
    4 waveform mode passes
    10 step 3 meshbench test -junit passes — 350/322 deliveries, exit 0, valid JUnit
    11 determinism passes — 322 both runs, output identical but for the timing line
    3 debugging a missed packet partial — causes verified, cut-through not reached
    12 docs and READMEs partial — #612's remaining count
    5, 6, 7, 8, 9, 10 steps 1-2 not reached
    part two (109 pictures) not run — needs the source tree, but #613 no longer blocks it

    Scenario 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:

    first launch

    Uninstall is clean — Program Files\MeshBench and the Start menu entry both go. The only Start menu leftover on this machine is MeshBench WiX Trial.lnk, pointing at a MeshBenchWiXTrial directory 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.allow said otherwise. v0.0.8 does not behave that way — a fresh install starts fetching terrain immediately, with ground.chosen: false, and nothing asks.

    I went looking before filing it, and this is deliberate and documented. terrain.allow says so plainly:

    Downloads are on unless somebody turned them off; there is no third state and nothing waits to be asked.

    and getting-started.html now discloses the cost up front, which is exactly what part 3 asked for and did not get:

    Budget about 1 GB for a first session … the ground under its links is roughly 500 MB of terrain that arrives on first launch.
    Bandwidth on the first run. That terrain is fetched before you have done anything else, so a metered connection is worth knowing about in advance.

    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.job carries 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-windows branch under docs/evidence/318-v0.0.8-windows/, beside part 6's under 318-v0.0.7-windows/; delete the branch whenever it has served its purpose.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG

  11. A13xB0 commented on Sep 6, 2026

    @A13xB0
    CollaboratorAuthor

    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, with XDG_CACHE_HOME and XDG_CONFIG_HOME pointed 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:

    fixture-fife-strict: 58 nodes, 56 running firmware, SF8 at 62 kHz, seed 9001
    provisioned 56 nodes with 526 lines and 67 frames
    ok    at least 10 unique deliveries
            317 unique deliveries, wanted at least 10
    JUnit written to /tmp/mbfresh/out.xml
    PASS: 1 assertions, 11s
    

    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

    # scenario result
    1 Install and first launch partly - it launches and warms, but see #616; step 4 is untestable as written (below)
    10.3 meshbench test -junit passes - assertions checked, valid JUnit, exit 0
    11 Determinism passes - 317 unique deliveries, three runs, identical
    Views (6) all six draw, switcher marks the current one passes with a caveat - see #618
    Panels (72) every panel drawn docked and popped out passes - all 72 taken, docked and popped-out sets match exactly. Layout faults in #619, #620
    Node window (10) one picture per tab fails - see #624
    Configuration (6) one per section fails - see #625
    Licences (5) one per section fails - see #625
    Menus (7) one per menu passes - all seven distinct and correctly structured

    Scenarios 2-9 and 12 not attempted this pass.

    Issues opened

    Behaviour

    Layout

    The pass's own instruments

    Small

    Corrections to this checklist

    Scenario 1 step 4 cannot be tested as written. Unsetting MESHBENCH_CARTO_KEY does not leave a release build keyless: cartokey.go falls back to defaultCartoKey, stamped by the release pipeline. Verified - a cold cache with the variable unset still fetched into tiles/carto-dark (not carto-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-strict at 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-decoded is the single step needing a hand on the mouse (tick decode in the console strip), and the emulated-board scenario (9) needs a board booted one at a time. Neither was attempted.

    Also noticed, not filed

    • Node labels collide on the map at the fixture's default zoom ("The Mysterons" over "CaptBlack-Mysteron1", "Mothy" over "Jazzy").
    • The map's layer panel cannot be collapsed and covers real nodes in a smaller window.
    • The Experiment log and the session log both carry UI chatter - showing Setup, Logs is in its own window - Window, then dock every window back - alongside what happened to the simulation.
    • Configuration > Overview reports a Vulkan device and measure links on the GPU on, with Last warm on the GPU: not yet after 679 links were measured. Might be a fallback that was not announced, might be ordering; not established either way.
  12. A13xB0 commented on Sep 6, 2026

    @A13xB0
    CollaboratorAuthor

    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.allow says "Downloads are on unless somebody turned them off; there is no third state and nothing waits to be asked", and getting-started.html now 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:

    TerrainDownloads is whether terrain may be fetched without asking. A pointer for the same reason as GPU: "not yet asked" is a third state, and it is the one a fresh install is in. A national network's ground is several hundred megabytes, which is not a thing to spend on somebody's tethered phone because they opened the application.
    — internal/app/session/prefs.go:33

    with access.go:61 collapsing 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

    Linux mine relationship
    #616 first launch spends 499 MB unasked #622 that fetch delivers 42% of the tiles two halves of one behaviour — whether it should happen, and what it does when it does
    #617 firmware refusal says "5 of 58" part 6's note on scenario 2's "reach 58" same number, different faults — theirs is a bug in firmwaregate.go, mine is the checklist's own wording. Both want fixing, or a fix gets tested against a checklist expecting 58
    #623 duplicate study areas in fixtures #609 U+FFFD in fixtures same class — bad shipped data that survived several regenerations because nothing checked. #609's fix added a guard; a duplicate-area check belongs beside it
    #624, #625 capture steps wrong #613 -look ignored its zoom same sweep — all three break part two's pictures. With #613 fixed and these two open, the 109 still cannot be trusted

    The 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:

    And #618 I had already captured both halves of without noticing: my first-launch screenshot shows Blocking 1 with the matching "something in this session cannot run" heading, and my later one shows Blocking 0 · Waiting on you 1 with "nothing is broken" — while session.status still 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

  13. A13xB0 commented on Sep 6, 2026

    @A13xB0
    CollaboratorAuthor

    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:

    board.probe Generic_E22_sx1262
      build  passed
      boot   failed   bc-under-test: firmware started but never connected within 40s
      fem/flood/power/radio/rx/tx  untested
    
    node.set_board + node.set_firmware + node.start
      ERROR: Abernethy Repeater: firmware started but never connected within 40s
    

    The cause is three steps upstream of that timeout. nodefs/<node>/emulator.log:

    qemu-system-xtensa.exe: sx1262: cannot load the chip model at
    C:\Program Files\MeshBench\libvirtualsx1262.dll: The specified module could not be found.
    

    The DLL is present and setup.check reports it ready. That message from LoadLibrary means 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's virtual-sx1262 v1.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.yml runs file on the DLL, which passes on one that cannot load, and setup.check marks 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

  14. 1 remaining item

  15. A13xB0 commented on Sep 7, 2026

    @A13xB0
    CollaboratorAuthor

    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%\meshbench and %APPDATA%\meshbench removed, meshbench-windows-x86_64-bundled.msi verified against the release's own SHA256SUMS (dd67a039…9eb2) and installed with msiexec /qn.

    meshbench v0.0.10, go1.25.13, bundled
    

    Issues opened: #650, #651, #652, #654.

    scenario verdict
    1 install and first launch passes
    2 first simulation passes
    3 debugging a missed packet passes — cut-through and causes are excellent; #651 found here
    4 waveform mode passes, but its misses are wrong: #652
    5 SDR observer passes
    6 importing a real network passes; step 3's expectation no longer holds, see below
    7 firmware development loop step 1 passes; steps 2-3 running as I write
    8 companion app passes
    9 one emulated board fails — #654
    10 scripting and the clients passes, all three steps
    11 determinism passes
    12 docs site and READMEs passes

    Nine of the previous pass's faults are confirmed fixed along the way: emoji render, 1 areas where the duplicate study area used to be, PASS: 1 assertion singular, 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 off beside 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 ready and found beside the binary, and tools/dissector/ ships, so #641 landed correctly.

    #650: the Setup page lists s140 SoftDevice twice, identical name and wording, differing only in size. They are two builds for two application base addresses (0x26000 and 0x27000), 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 s the axis shows 398-416 s and nothing earlier can be reached. The engine keeps the whole log — this is readoutTail = 2000 in readouts.go, and EventsSince already exists beside EventsTail for 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:

    • a frame whose CRC passes is recorded as a miss (4 in a sample of 347)
    • 0 codeword(s) beyond repair, 0 repaired, CRC false — 32 of 347
    • every one of them is unclassified, where calculated mode names all 1,768 of its misses
    • 78 of 81 were above 10 dB SNR, median 15.0
    • the codeword sentence ends in a dangling comma: ..., CRC false, at 15.0 dB

    5. SDR observer — passes

    sdr.serve answers with an address and 62,500 Hz. Connecting a socket gets a real RTL0 rtl_tcp header, and the stream behaves as the scenario says it should:

    playing   mean |IQ| 12.72
    paused    mean |IQ|  6.28     ratio 2.03
    

    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.apply answered {"applied": 117} and no node gained a region, in nodes.list or 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 dev against a real ripplebiz/MeshCore checkout, with MESHCORE_NATIVE pointing at meshcore-native:

    fetching the Crypto library MeshCore builds against, once
    local-main  ...\firmware\native\local-main\meshcore-simple_repeater-windows-amd64.exe  1.5 MB
      in the workbench's firmware library (1519248 bytes)
      assigned to every simple_repeater node
    

    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_repeater and 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.serve gives a TCP endpoint, and a socket speaking the companion framing gets real protocol bytes back — APP_START answered with a 51-byte reply and a public key, device query and contacts answered too. bench.drop returns {"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.probe passes every column on Heltec_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:

    emulator.log:  Attempt to set property 'apb_freq' on anonymous device
                   (type 'esp_soc.uart') after it was realized
    console:       ... entry 0x400805e4        <- last line ever printed
                   > get name                  <- typed twice, echoed twice, never answered
    nodes.stats:   running=true  sent=0  irq_reads=0  cpu_ms=0
    

    Reproduced on two nodes; stopping the node first makes no difference. The failure appears only in node.output under emulator — 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 published meshbench==0.0.10 client:

    MeshBench: calculated reception, default excess loss — a best case; no multipath, no body loss, no oscillator error
    162 events, 63 receptions in the tail
    

    Two dead ends on the way, both mine rather than the product's: the client finds the binary through MESHBENCH_BINARY, not MESHBENCH, and shutil.which("meshbench") finds nothing on Windows because the MSI does not touch PATH — so on Windows that variable is not optional. And the client in the source tree declares 0.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 localStorage key, meshbench-docs-way, so a language chosen on one page is remembered on the next.

    Step 3. meshbench test -junit exits 0, 317 unique deliveries, valid JUnit — and now reads PASS: 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-scheme and 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/brand returning 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-cli are 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

  16. A13xB0 commented on Sep 7, 2026

    @A13xB0
    CollaboratorAuthor

    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, plus collisions, airtime_ms, tx, rx.

    The control works. control and release — 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:

    release   tx 1495  rx 322  delivered 286  redundant 36  collisions 4916  airtime 1048118
    control   tx 1495  rx 322  delivered 286  redundant 36  collisions 4916  airtime 1048118
    

    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_spread is 0 on all three arms — and the documentation makes rx_spread "the number every delta must beat before it means anything". The same is true of meshbench 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 MeshCore main delivered 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

  17. A13xB0 commented on Sep 7, 2026

    @A13xB0
    CollaboratorAuthor

    Claude's pass, part 8: v0.0.10 on macOS — the first pass on a Mac

    Run against the released MeshBench-arm64-bundled.dmg from v0.0.10, SHA256 checked against the release's own SHA256SUMS (e9666b8a…2217, matches), installed by dragging to Applications. macOS 26.3 (25D125), Apple M4, 10 GPU cores. Driven over the control socket with the published meshbench-driving and meshbench-scripting skills, and the meshbench==0.0.10 client from PyPI.

    meshbench-bin v0.0.10, go1.25.14, bundled   (darwin/arm64)
    

    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-macos branch.

    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-xtensa and renode beside the binary and setup.check reports all three ready with their paths, which is the N16 story properly closed. Then nothing boots.

    board MCU what happens issue
    Generic_E22_sx1262 ESP32 ROM banner prints, then QEMU aborts: Attempt to set property 'apb_freq' on anonymous device (type 'esp_soc.uart') after it was realized #659
    Heltec_v3, Tbeam_SX1262 ESP32-S3 / ESP32 ROM loads the image and panics: Guru Meditation Error: Core 0 panic'ed (LoadStoreError) — because the downloaded image is the application-only .bin, not -merged.bin #658
    RAK_4631 nRF52840 Renode runs, loads all four flash blocks, then radiospi.lora: **no chip model: Unable to load shared library 'libdl.so.2'** — a Linux soname P/Invoked on macOS. The node sits in starting for ever with no radio #655

    #658 is not macOS-specific and is the more interesting one: firmware.download resolves the asset with ListAll and takes the first row matching (role, board, version), and upstream publishes both -merged.bin and the app-only .bin under names that differ only in that suffix. Runnable() exists one file away and rejects bin && !Merged for exactly this reason; the download path never consults it. Byte-exact against upstream: E22 got the merged file (1,212,960 bytes, partition magic aa50 at 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 is fw-<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.list reported running: 3 for the rest of the session, and wait_idle never returns. That is what hung my script while the work was already done.


    Scenario verdicts

    # scenario verdict
    1 Install and first launch passes, with #660 against it — the dmg is unsigned and the app ad-hoc signed, so Gatekeeper rejects it
    2 Your first simulation passes — 56 of 58 on firmware in 3 s, advert answered, flood reached 38 distinct transmitters
    3 Debugging a missed packet passes — causes carry real physics, and the cut-through draws docked with both directions' margins
    4 Waveform mode passes — stamped, uninterrupted, verdicts calculated cannot produce; but #664
    5 SDR observer passes — playing 2.71 % and 5.39 % of samples above the floor, paused 0.00 %, stream survived four drags
    6 Importing a real network passes — counts at every step, and a flood relays to 48 distinct transmitters
    7 Firmware development loop passes — N19 fixed, and the control arm now reproduces exactly
    8 A companion app against the mesh passes — real protocol bytes, drop kills the socket, reconnect works
    9 One emulated board FAILS — #659, #658, #655
    10 Scripting and the clients passes — cookbook 3 exits 0, meshbench test exits 0 with valid JUnit
    11 Determinism spot-check passes — 322 unique deliveries twice, output byte-identical
    12 Docs site and READMEs passes with faults — docs#34, #35, #36
    part two 115 pictures 110 captured, 3 skipped by design, 2 impossible (#668); needed #662 first

    1. 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.

    $ spctl -a -vvv -t exec /Applications/MeshBench.app
    rejected
    $ xcrun stapler validate /Applications/MeshBench.app
    MeshBench.app does not have a ticket stapled to it.
    $ codesign -dv --verbose=4 /Applications/MeshBench.app
    CodeDirectory … flags=0x2(adhoc)   Signature=adhoc   TeamIdentifier=not set
    $ codesign -dv MeshBench-arm64-bundled.dmg
    code object is not signed at all
    

    Filed as #660. docs/install.md is 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.

    the map

    2, 3, 4 — the run itself is in good shape

    fixture-fife-strict, 56 of 58 nodes on firmware in three seconds, advert at 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:

    half-duplex    its own transmitter was keyed; LoRa is half duplex
    receiver-busy  its demodulator was already locked to Dalgety Bay Repeater; a LoRa receiver decodes one packet at a time
    interference   would have decoded at 6.2 dB, lost to a stronger interferer
    collision      decoded its header, then 125 symbol(s) were destroyed by a collision it could not capture over; beyond what CR 4/8 can repair
    

    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:

    debug view

    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):

    node console

    Waveform switches mid-run without cutting a frame (357,459 → 372,783 events across the switch), stamps waveform RF and swaps the honesty line to … bare earth, real coding chain:

    waveform

    But #664: in waveform mode, 95 of 113 floor misses 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 under floor and 20 times under unclassified. 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: sdr.serve → {addr: 127.0.0.1:65243, rate_hz: 62500}, genuine RTL0 header, 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 four nodes.move calls 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.
    • Import: Fife from ScotMesh CoreScope — 60 nodes, 600 records, 41 skipped for no position, 805 links, infer.apply -> 42. N23 is fixed: nodes.list shows regions on 42 of 60, matching. A flood then relays to 48 transmitters.
    • Dev loop: N19 is fixed — meshbench dev with a workbench running now reports in 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 with MESHCORE_NATIVE set. 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 — with control reproducing release exactly. 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.
    • Companion bench: APP_START answered with a real RESP_CODE_SELF_INFO frame, bench.drop killed 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

    $ meshbench test -fixture fixture-fife-strict.json -junit out.xml
    ok    at least 10 unique deliveries
            322 unique deliveries, wanted at least 10
    PASS: 1 assertion, 8s        exit 0
    

    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 main checkout and got a VersionMismatch. That is the pairing check doing its job — the checkout carries __version__ = "0.1.0", and pip install meshbench==0.0.10 gives 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, so pip3 install meshbench on 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.py could not take a single picture on macOS: its capture() knows only spectacle and grim (#662). With a screencapture -l<windowid> branch and a small CGWindowListCopyWindowInfo helper, the whole sweep runs unchanged.

    bucket result
    Views (6) all six draw, and the switcher marks the current one
    Panels (72) 70 — panel-energy and popout-energy are impossible (#668); docked and popped-out sets otherwise match
    Node window (10) all ten, each on the tab it names — #624 is fixed, and Output has now been photographed
    Board view (3) skipped by the manifest, and unreachable here anyway for the scenario-9 reasons
    Configuration (12) all twelve — #625 is fixed, every section opens with its own settings and captions
    Licences (5) all five, every row naming component, licence and origin
    Menus (7) all seven, distinct, sectioned — and the Help menu now has Manual, which closes #626

    views

    Two faults came straight out of the pictures:

    And one the picture makes obvious rather than proves: every node reports 0 CPU and 0 memory, because nodestats.go reads /proc/<pid>/stat with 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.

    nodes running

    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.png resolves, 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.

    • docs#34 — reference-control.md puts 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.md and settings.md repeat 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.
    • docs#35 — importing.html's headline claim, "Without this step every node transmits and none relays", is not what happens: two imports differing only in infer.apply both flooded an advert to 48 distinct transmitters and 94 receptions, identical. And the "If nothing relays" checklist's item 2 tells you to ask get repeat on a node with no regions — which answers -> > on, because it is a different question.
    • docs#36 — agent-skills.html's two clone URLs are stale, one 404s, the driving skill is named meshcoresim rather than its published name meshbench-driving, and the third skill it lists is deliberately not published.
    • The published driving skill says session.describe lists every verb (it answers four facts) and gives a Linux-only socket path #663 — the skill itself says session.describe lists every verb. It answers four facts; session.verbs is the one. Both the tree's own scripting-verbs.md and verb-reference.md have 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.list calls the bundled emulators 'needed' with no path while setup.check calls them ready and names where they are #665 — resource.list calls the bundled virtual-sx1262 and qemu-system-xtensa needed with an empty path while setup.check calls them ready and 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.
    • Configuration says the GPU has measured nothing and the links job says 'on the processor' on a warm that measured nothing at all; and the Metal device is named 0x0 #672 — the Metal device reports itself as 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.
    • The GPU path does work here, which I checked before writing that: with the matrix cache moved aside, gpu.state answers last_warm: {used: true, pairs: 71253, ms: 10603}. 71,253 pairs on Metal in 10.6 seconds.
    • N29 does not reproduce — project.new on a fixture with 56 nodes running firmware completed cleanly, twice.
    • N30, N32 do not reproduce — project.list now answers with a fixtures list beside projects, so the shipped networks are reachable from File on a fresh install.
    • N31 is fixed by removal — nodes.list no longer publishes the dead sent/heard fields; nodes.stats carries the live ones.
    • link.profile accepts unknown parameters ({"wibble": 1}) without refusing, and refuses "select two nodes to cut through between" under code internal where conflict is 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.senders accepts a repeater and only refuses at run time — nine runs failed one after another with Abernethy Repeater is a simple-repeater; a message is originated by a companion, which is a clear sentence arriving nine times too late.
    • experiment.vary on the same parameter twice crosses it with itself, giving arms labelled mac318b · 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.regions answers {"nodes": 60, "regions": null} — a count where the rows were the question, though nodes.list now carries them.
    • reference-control.html says "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-decoded also needs a hand on the mouse.


    Evidence is on the evidence/issue-318-v0.0.10-macos branch under docs/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

  18. A13xB0 commented on Sep 7, 2026

    @A13xB0
    CollaboratorAuthor

    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:

    $ python tools/shots/shots.py views
    no binary at ...\meshbench: go build -o meshbench ./cmd/meshbench     <- Windows builds meshbench.exe
    
      (after satisfying that)
    the workbench refused something: [wgpu] [Warn] Disabling robustBufferAccess2 ...
                                                                           <- a warning, not a refusal
    0 of 6 captured
    

    and past those, capture() ends at sys.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: --list enumerates 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:

    page claim build verdict
    reference-cli "There are 16 commands" 16 matches, none missing either way
    reference-control "There are 256 verbs" 256 matches — and my earlier report that it listed 255 was my own miscount, corrected on #612

    All 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-scheme are 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 docs would have driven. It needs #674 first.

    🤖 Generated with Claude Code

  19. A13xB0 commented on Sep 7, 2026

    @A13xB0
    CollaboratorAuthor

    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 fix with the measurements on the PR.

    PR closes verified by
    #704 #655 a RAK_4631 boots, adverts twice unprompted, irq_reads 0 → 35,336, console answers get name
    #701 #658 all three ESP32 boards now download the merged image; a Heltec_v3 boots and adverts, no Guru Meditation
    #696 #657 three board downloads, three distinct job ids, running: 0
    #700 (from #659) the E22's probe records 8/8 untested with the abort quoted, against 2 passed
    #697 #664 227 floor misses, 0 above the threshold on the deciding number; 0 sentences under two classes
    #705 #672 device: "Apple M4"; a primed warm says the matrix already answered every pair
    #702 #673 ⌘N ⌘O ⌘S ⌘⇧S, and TestAShortcutBindsThePlatformModifier passes on darwin
    #692 #661 cpu_ms/cpu_pct/rss_bytes: null with cost_measured: false
    #691 #665, #650 bundled tools on disk with paths; the two SoftDevices named apart
    #703 #662, #666 6 of 6 views captured, our own window by pid
    #693 #668 the Energy steps carry env, and the panel registers and draws
    #694 #669 the empty state said once
    #695 #663 session.verbs and the three socket addresses, all true of this build
    #686 — 3,285 events, 3,285 lines, ratio 1.000
    #688 — a window already open switches tab
    #687 — map.basemap names what is drawn

    Small residuals, all noted on their own PRs and none of them blockers: cpu now still renders 0.0% in the Nodes table (#692), the Compare title still has no left inset (#694), ⌘⇧S has the modifiers in the wrong order for the platform (#702), and shots.py now needs pyobjc-framework-Quartz with 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:

    family what it needed measured
    nRF52840 (Renode) #704 boots, 2 adverts, console answers
    ESP32-S3 (esp32s3) #701 boots, 2 adverts, provisions
    ESP32 (esp32) QEMU sx1262-15 boots, 5 adverts, irq_reads 73,748, both neighbours heard all five

    sx1262-15 is the answer to #659 and #654. Same flash, same command line, the two binaries side by side:

    -14  ->  Unexpected error in qdev_prop_set_after_realize() … 'apb_freq' on 'esp_soc.uart'
    -15  ->  Adding SPI flash device        (runs)
    

    toolchaincatalogue.go:24 on main already 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 counted tx events rather than trusting a running flag, 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_QEMU and MESHBENCH_RENODE have 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 /Applications bundle 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

  20. A13xB0 commented on Sep 8, 2026

    @A13xB0
    CollaboratorAuthor

    Part 9: v0.0.11-dev.1 on macOS, from a genuinely fresh install

    MeshBench-arm64-bundled.dmg from v0.0.11-dev.1, SHA256 checked against the release's own SHA256SUMS (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.

    meshbench-bin v0.0.11-dev.1, go1.25.14, bundled
    qemu-system-xtensa  v9.2.2_meshbench_sx1262_15
    renode-support/peripherals/VirtualSX1262Lib.cs  carries the macOS dlopen fix
    

    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:

    board family result
    Generic_E22_sx1262 ESP32, qemu esp32 2 adverts, irq 16,912, console > E22 Repeater
    Heltec_v3 ESP32-S3, qemu esp32s3 2 adverts, irq 21,000, console > Emu
    RAK_4631 nRF52840, Renode 1 advert, irq 8,964, console > RAK4631 Repeater

    with both neighbours hearing each of them. The E22 image is the merged one (0x8000 = aa50) without being asked for, and sx1262-15 is 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:

    …simple_repeater-v1.17.1.uf2 is linked above SoftDevice s140 v6.1.1, which has not been fetched - download it in Resources first

    One resource.fetch and 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

    # scenario verdict
    1 Install and first launch passes — 513 MB of terrain, priced in bytes as it goes, ground: terrain 6233/6233, GPU warm 71,253 pairs in 8.4 s
    2 Your first simulation passes — 56 of 58 in 3 s, flood to 46 distinct transmitters, 227 receptions
    3 Debugging a missed packet passes, and better: link.result now returns both margins and every term
    4 Waveform mode passes — stamped, uninterrupted, and a floor miss now reads at -15.6 dB measured, -26.0 dB estimated
    5 SDR observer passes — playing 1.42 %, paused 0.00 %, survived the drag
    6 Importing a real network passes — 60 nodes, infer.apply -> 45 matching nodes.list, flood to 47 transmitters
    7 Firmware development loop passes — build lands at 478,504 bytes, control arm reproduces exactly, rx_spread measured, seeds vary
    8 A companion app passes — SELF_INFO on three reconnects, drop kills the socket, same endpoint each serve
    9 One emulated board passes on all three families
    10 Scripting and the clients step 3 passes (exit 0, valid JUnit); step 1 blocked, see below
    11 Determinism spot-check passes — 322 twice, byte-identical
    12 Docs and READMEs docs#34, #35, #36 all still open and unchanged
    part two 115 pictures 112 captured — and 46 of them are the wrong window, #714

    #714 — the sweep says 112 of 115 and 41 % of the pictures are of the workbench

    The one I would fix first. captureDarwin gained a guard after #703 merged:

    if int(w.get("kCGWindowLayer", 0)) != 0:
        continue

    A popped-out panel and a node window are floating windows on layer 3. So all 36 popout-* and all 10 node-* steps skip the window they are about and fall through to the main workbench, which passes every test. popout-events.png is a picture of the Plan view.

    bucket should be is
    popouts (36) 900×672 1500×963
    node window (10) 820×652 1500×963
    the other 66 1500×963 1500×963 ✓

    run() returns True, stderr is clean, the summary says 112 of 115 captured. Removing that one condition and keeping the size test restores 900×672 and 820×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 test gives 350 on a new machine and 322 ever after

    This 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:

    first run on the fresh install   ->  350 unique deliveries
    every run after it               ->  322   (nine consecutive)
    

    Isolated to persisted per-node storage, and reproducible on demand:

    MESHBENCH_NODEFS=<empty dir 1>  ->  350
    MESHBENCH_NODEFS=<empty dir 2>  ->  350
    MESHBENCH_NODEFS=<empty dir 3>  ->  350
    MESHBENCH_NODEFS=<reused dir 1> ->  322
    

    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 offers MESHBENCH_NODEFS and WipeNodeStorage; cmd_test.go uses 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 ⇧⌘S now in Apple's order, which was my nit on #702), the Compare panel saying it once, session.verbs in the published skill, map.basemap naming what is drawn, events.dump writing the whole ledger, the node window switching tab, resource.list agreeing with setup.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 now still renders 0.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 meshbench up to 0.0.10 and no 0.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 -dev build through.

    Method

    Everything above was driven over the control socket against the installed dmg, not a source build. Node counts, tx events and irq_reads rather than running flags 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

  21. A13xB0 commented on Sep 8, 2026

    @A13xB0
    CollaboratorAuthor

    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%\meshbench and %APPDATA%\meshbench moved aside, meshbench-windows-x86_64-bundled.msi verified against the release's own SHA256SUMS (41ef47fb…c63b3) and installed with msiexec /qn.

    meshbench v0.0.11-dev.1, go1.25.13, bundled
    

    Issues opened: #712, #713, #715.

    scenario verdict
    1 install and first launch passes
    2 first simulation passes
    3 debugging a missed packet passes
    4 waveform mode passes, but #652 is not fixed — see the correction below
    5 SDR observer passes
    6 importing a real network steps 1–2 pass; step 3 fails — #713
    7 firmware development loop step 1 passes; steps 2–3 run and report, but #671 stands
    8 companion app passes
    9 one emulated board passes — first time on Windows
    10 scripting and the clients steps 2–3 pass; step 1 cannot be run — #715
    11 determinism passes
    12 docs site and READMEs passes

    The new channel itself

    Worth checking first, since it is what is new. It behaves:

    update.channel                       -> {"build":"development","channel":"development"}
    update.channel {"channel":"stable"}  -> switches, and back again
    update.channel {"channel":"banana"}  -> no channel called "banana": there is stable and development
    status bar                           -> v0.0.11-dev.1 · development build · Gio
    

    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.

    Repeater ID: F5B7B54C...
    t=212.1s   sent=1  heard=2  irq_reads=7172  running=True
    console:   get name  ->  > E22 Repeater
    emulator.log:  Adding SPI flash device        <- and nothing else
    

    No apb_freq abort. #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 is sx1262_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:

    unclassified      367 of 9500  (3.9%)
    CRC-true-as-miss    2
    

    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.serve answers with an address and 62,500 Hz, a socket gets a real RTL0 rtl_tcp header, and the stream carries signal only while playing:

    playing 20s   mean |IQ| 4.34   p99.9 17.0   peak 22.5
    paused  20s   mean |IQ| 3.16   p99.9  9.3   peak 13.0
                         ratios    1.38x  1.83x      1.73x
    

    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.serve now binds 127.0.0.1 where v0.0.10 bound the machine's LAN address. That removes the Windows firewall prompt I hit last pass. bench.serve still 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:

    boundary.set/accept  -> {"accepted":"Fife","areas":1}
    import.fetch         -> {"nodes":60,"records":641,"skipped_no_position":48}
    import.commit        -> {"nodes":60,"strategy":"replace-all"}
    boundary.prune       -> {"nodes":24,"removed":36}
    firmware.set         -> {"nodes":24}
    infer.run            -> read 32,703 packets, 472 nodes seen
    infer.apply          -> {"applied":17}      regions: #sco 17, #ioi 13, #fif 9, #wls 5
    

    #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.apply ran, one advert each, counting nodes that carried the same message_id rather than nodes adverting on their own schedule:

    regions applied (17 of 24)   1 message, transmitted by 18 distinct nodes
    no regions at all            1 message, transmitted by 18 distinct nodes
    

    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, so boundary.accept must be given a name that does not distinguish them.

    7. Firmware development loop — step 1 passes; the sweep still cannot measure

    meshbench dev against a real ripplebiz/MeshCore checkout:

    local-main  ...\firmware\native\local-main\meshcore-simple_repeater-windows-amd64.exe  1.5 MB
      in the workbench's firmware library (1519248 bytes)
      assigned to every simple_repeater node
    

    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, so rx_spread is 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:

    experiment finished, 9 runs - every seed of control returned the same numbers, so the seed gives no noise floor

    That is the right sentence and it stops rx_spread: 0 reading 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. infos is answered with the node's key and name; bench.drop returns {"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:

    $ find . -name _main.id -exec sha256sum {} \; | awk '{print $1}' | sort | uniq -c
         57 fa883da57d6c45f1495011eeb4382e03ea679d4a14c676fa71e89252e8a95e95
          1 00254de360dea38cd9dcfa3707b0f02a528e7874ab61e71959e89ba2f8032b57
    

    57 of 58 byte-identical. The consequence is visible without any tooling: nodes hear plenty and know nobody.

    AngusOutlaw1  heard=168  irq=283749   ->  "0 contacts in device"
    Mothy         heard=43   irq=397276   ->  "0 contacts in device"
    

    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.dev1 finds nothing. @meshbench/client on npm stops at 0.0.10 too. The Go client works, because Go modules come from the git tag:

    go get github.com/MeshBench/meshbench/pkg/client-go/[email protected]
    -> the v0.0.11-dev.1 Go client connected to the v0.0.11-dev.1 workbench
    

    Same build, same machine: Go connects, Python cannot.

    Step 2 — all 40 pages carry the meshbench-docs-way key, so a language chosen on one is remembered on the next.

    Step 3 — meshbench test -junit exits 0, 317 unique deliveries, valid JUnit, PASS: 1 assertion singular.

    11. Determinism — passes

    Two runs at seed 9001, byte-identical output but for the wall-clock line (34s against 33s), 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-scheme are 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, and docs/images/workbench-plan.png is present at 936 kB.

    The only 404 is MeshBench/brand from the docs README, which is a deliberately private repository and explained in the sentence around it.


    Fixes from previous passes confirmed on this build

    What 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 the shots-runs-on-three-platforms branch). 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 real meshcore-cli.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG

  22. A13xB0 commented on Sep 8, 2026

    @A13xB0
    CollaboratorAuthor

    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.

    meshbench-bin v0.0.11-dev.2, go1.25.14, bundled
    qemu-system-xtensa  v9.2.2_meshbench_sx1262_15
    

    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:

    node storage: fresh, every node factory-new, removed after the run
    provisioned 56 nodes with 526 lines and 67 frames
    …
            350 unique deliveries, wanted at least 10
    PASS: 1 assertion, 8s
    

    350 on a machine whose nodefs holds 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, and nodefs is 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

    112 of 115 captured
    skipped: board-radio, board-wiring, board-decoded
    
    bucket should be is
    popouts (36) 900×672 900×672
    node window (10) 820×652 820×652
    panels, views, menus, configuration, licences (66) 1500×963 1500×963

    and the content matches: every popout carries its own title bar (MeshBench - Boards, MeshBench - Console, MeshBench - Events…) and the X is in its own window footer. 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:

    build 9001 9002 9003 4242 1 77 range
    v0.0.11-dev.1 322 323 335 327 — — 13 (4 %)
    v0.0.11-dev.2 350 343 323 312 312 320 38 (11 %)

    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.dev2 is on PyPI, the pairing check accepts it, and cookbook example 3 runs end to end:

    MeshBench: calculated reception, default excess loss — a best case; …
    124 events, 62 receptions in the tail          exit 0
    

    That was the one thing dev.1 could not do.


    Scenario verdicts

    # scenario verdict
    1 Install and first launch passes (warm profile; the fresh-install measurements are in part 9)
    2 Your first simulation passes — 56 of 58 in 3 s, flood to 46 transmitters, 227 receptions
    3 Debugging a missed packet passes — causes separate, link.result gives both margins and every term
    4 Waveform mode passes — floor at -15.8 dB measured, -26.0 dB estimated
    5 SDR observer passes — playing 1.43 %, paused 0.00 %, survived the drag
    6 Importing a real network passes — 60 nodes, infer.apply -> 45 matching nodes.list, flood to 47 transmitters
    7 Firmware development loop passes — build at 478,504 bytes, control reproduces release exactly, rx_spread measured
    8 A companion app passes — see the note below
    9 One emulated board passes on all three families, first time without any manual step
    10 Scripting and the clients passes, both steps
    11 Determinism passes — 350 three times
    12 Docs and READMEs docs#34, #35, #36 still open; docs#38 is #718's site half, in flight
    part two 115 pictures 112, all correct

    Scenario 9, all three, straight through:

    Generic_E22_sx1262  ESP32       2 adverts  irq 16,904  console  > E22 Repeater
    Heltec_v3           ESP32-S3    2 adverts  irq 21,000  console  > Emu
    RAK_4631            nRF52840    1 advert   irq  8,708  console  > Emu
    

    A correction to my own part 9 note

    I flagged the companion reconnect answering code=1 instead of SELF_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 to APP_START; wait for the queue to drain and SELF_INFO arrives as it should:

    reconnect after 0s: code=136 len=61     <- a push
    reconnect after 1s: code=136 len=46     <- a push
    reconnect after 3s: code=5   len=70     <- SELF_INFO, pubkey 1124364823df…
    

    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

    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

  23. A13xB0 commented on Sep 8, 2026

    @A13xB0
    CollaboratorAuthor

    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\MeshBench removed outright, both %LOCALAPPDATA%\meshbench and %APPDATA%\meshbench moved aside, meshbench-windows-x86_64-bundled.msi verified against the release's own SHA256SUMS (5e8dcd62…18d5c) and installed with msiexec /qn.

    meshbench v0.0.11-dev.2, go1.25.13, bundled
    QEMU emulator version 9.2.2 (v9.2.2_meshbench_sx1262_15)
    

    Issues opened: #725, #726, and meshbench.github.io#7.

    scenario verdict
    1 install and first launch passes
    2 first simulation passes
    3 debugging a missed packet passes
    4 waveform mode passes; #652 still open, though half of it is fixed
    5 SDR observer passes
    6 importing a real network passes — the corrected expectation now holds
    7 firmware development loop step 1 passes; the sweep reports, but #671 stands
    8 companion app passes; #725 found underneath it
    9 one emulated board passes
    10 scripting and the clients passes, all three steps
    11 determinism passes
    12 docs site and READMEs passes; site prose lags the fix — docs#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:

    C:\Program Files\MeshBench\qemu\bin\libstdc++-6.dll   2,667,040 bytes
    

    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.

    $ find . -name _main.id | wc -l                                            56
    $ find . -name _main.id -exec sha256sum {} \; | awk '{print $1}' | sort -u | wc -l
    56
    

    56 nodes, 56 distinct keys. The changelog's root cause — strtoul saturating where a long is 32 bits, so every seed above 2³² became 0xFFFFFFFF — also explains the one node that differed on dev.1: Abernethy Repeater drew seed 9001, the only one under the ceiling. A genuinely Windows-only fault.

    #715 — a development build had no clients. Fixed, and carefully.

    pip install meshbench                  -> 0.0.10        (stable users undisturbed)
    pip install meshbench==0.0.11.dev2     -> 0.0.11.dev2
    npm dist-tags: latest 0.0.10, dev 0.0.11-dev.2
    

    The 0.0.11.dev2 client attaches to the 0.0.11-dev.2 workbench, so the PyPI-versus-tag spelling problem is genuinely handled and not merely papered over. And the stable client's refusal is now followable:

    this client is from MeshBench 0.0.10 and this workbench is MeshBench 0.0.11-dev.2. A client and the workbench it drives must be the same release: install the 0.0.11-dev.2 client, which is published as a pre-release beside this development build and has to be asked for by exact version (pip) or the dev tag (npm), or run the 0.0.10 workbench

    #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.apply now reads:

    without it a mesh still relays every unscoped flood, adverts included, exactly as widely, but a scoped packet matches no region any repeater holds and is dropped without a word

    and #711/#716 too, which I had not filed but had run into: meshbench test now says which storage it used, and the two numbers behave exactly as described.

    node storage: fresh, every node factory-new, removed after the run   -> 350 unique deliveries
    node storage: kept at ...\nodefs: what the nodes stored last run
                  is an input to this one                                -> 317 unique deliveries
    

    9. One emulated board — passes

    On a clean install with no stray DLL, Generic_E22_sx1262 pinned to one node:

    backend=emulated
    t=175.0s   sent=1  heard=1  irq=20  running=True
    
    console:   Repeater ID: 214492886F8637C013F237257B0273B66B71684527F1669BF74C9CBFAE7FEA7A
               region default sco  ->  default scope is now sco
               get name            ->  > Abernethy Repeater
    emulator:  Adding SPI flash device        <- the only line; no apb_freq abort
    

    node.output deserves a note for being good: asking for rom on 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 for radio before 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.set with a board reports nodes: 1, and firmware.start afterwards answers {"starting": true} — but the already-running native processes keep running and the node stays backend=native. There is no firmware.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

    15,163 miss events in waveform mode
    classes: half-duplex 10223, receiver-busy 3367, floor 1031, unclassified 542
    unclassified: 542 of 15163  (3.6%)
    CRC-true recorded as a miss: 1
    details ending in a dangling comma: 0
    

    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:

    waveform: header unreadable at 15.0 dB measured, 26.9 dB estimated
    waveform: no preamble lock at 15.0 dB measured, 9.6 dB estimated
    waveform: 4 codeword(s) beyond repair, 6 repaired, CRC true, at 15.0 dB measured, 20.3 dB estimated
    

    Splitting measured from estimated SNR is a real improvement. What remains is the class itself: 3.6% are still labelled unclassified when 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 is rf.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:

    import.fetch    -> {"nodes":60,"records":639,"skipped_no_position":47}
    import.commit   -> {"nodes":60,"strategy":"replace-all"}
    boundary.prune  -> {"nodes":24,"removed":36}
    firmware.set    -> {"considered":24,"nodes":24}
    infer.run       -> read 33,103 packets, 469 nodes seen
    infer.apply     -> {"applied":17}
    

    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 new message_id is 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 regions2 on disk, but nodes.list keeps reporting regions: null for 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.set pinned the repeater build across all 24 nodes and answered {"considered":24,"nodes":24}; the 6 companions among them silently never start. firmware.needed then answers {"roles":[]} and firmware.start answers {"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. infos answers with each node's own (now distinct) key; bench.drop gives 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.

    Mothy/contacts3         key 3cd64e6b…  name 'Markinch'
    AngusOutlaw1/contacts3  key dec9b87c…  name 'SCO-ANG-02-RPT'
                            key 5a5ed324…  name 'Blairgowrie_RPT'
    
    meshcore-cli contacts   ->  "0 contacts in device"   (all of them)
    Companion panel         ->  "CONTACTS none yet - a companion learns these from adverts"
    

    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.serve binds 127.0.0.1, answers a real RTL0 header 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.serve still 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:

    $ pip install meshbench==0.0.11.dev2
    $ MESHBENCH_BINARY=... python 03_small_mesh_with_traffic.py
    control socket: tcp:127.0.0.1:53857
    MeshBench: calculated reception, default excess loss — a best case; no multipath, no body loss, no oscillator error
    162 events, 63 receptions in the tail
    

    It launches its own workbench on its own socket without disturbing the one already running, which is worth knowing. meshbench test -junit exits 0 with valid JUnit and PASS: 1 assertion. MESHBENCH_BINARY is still required on Windows, since the MSI does not touch PATH — 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-way key 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 private MeshBench/brand.

    docs#7 — the published site still carries the claim #718 corrected, in four places across importing.html, fixtures.html and reference-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 dev builds and assigns local-main correctly. The sweep gives everything step 3 asks for, and the control arm matches release exactly. And three seeds still give three byte-identical runs:

    arm           runs  delivered  redundant  rx_spread  at_risk_2db
    control          3        286         36          0            0
    local-main       3         61         63          0      0.03361
    release          3        286         36          0            0
    

    The warning the workbench prints for itself has got better again, and now says what the consequence is rather than only the fact:

    experiment finished, 9 runs — every seed of control returned the same numbers, so the seed gives no noise floor here and a difference between arms has nothing to be called larger than

    A small wart

    meshbench test -keep-node-storage while the workbench is running fails correctly on a node-storage lock — and then appends an unrelated hint, twice:

    meshbench: engine: firmware on 0 of 56 nodes; 56 failed, first: ... is already in use
      by another node process: locked by another process
    
    Build one with meshcore-native/build.sh, or check that the fixture's firmware versions
    are published: a bare v1.17.0 resolves nothing, ...
    Build one with meshcore-native/build.sh, or check that the fixture's firmware versions
    are published: a bare v1.17.0 resolves nothing, ...
    

    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 real meshcore-cli.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG

  24. A13xB0 commented on Sep 9, 2026

    @A13xB0
    CollaboratorAuthor

    Claude's pass, part 11: v0.0.11-dev.3 on Windows, all twelve scenarios

    Clean machine: dev.2 uninstalled, C:\Program Files\MeshBench removed, both %LOCALAPPDATA%\meshbench and %APPDATA%\meshbench moved aside, meshbench-windows-x86_64-bundled.msi verified against the release's own SHA256SUMS (b49a11ca…5b06) and installed with msiexec /qn.

    meshbench v0.0.11-dev.3, go1.25.13, bundled
    QEMU emulator version 9.2.2 (v9.2.2_meshbench_sx1262_15)
    

    The uninstaller left nothing behind this time, which retrospectively confirms that the stray libstdc++-6.dll I found after dev.2 was my own contamination and not the MSI's.

    Issue opened: #729. Everything else in this pass is a confirmation.

    scenario verdict
    1 install and first launch passes
    2 first simulation passes
    3 debugging a missed packet passes
    4 waveform mode passes; #652 unchanged
    5 SDR observer passes
    6 importing a real network passes
    7 firmware development loop step 1 passes; the sweep reports, #671 stands, #729 filed
    8 companion app passes — #725 fixed
    9 one emulated board passes
    10 scripting and the clients passes, all three steps
    11 determinism passes — byte-identical this time
    12 docs site and READMEs passes

    #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):

    AngusOutlaw1   Markinch, Blairgowrie_RPT, bosgag-tay-sidlaw-r, SCO-ANG-01-RPT, Leslie 🏘
                   > 5 contacts in device
    Mothy          Markinch, Leslie 🏘, Blairgowrie_RPT      > 3 contacts in device
    Jazzy          Markinch, Leslie 🏘, Blairgowrie_RPT      > 3 contacts in device
    

    The workbench's own panel lists them too, and companion.state answers "contacts": 5 with contact: 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-cli dying on a traceback. The traceback is the tell:

    UnicodeEncodeError: 'charmap' codec can't encode character '\U0001f3d8' in position 259
    

    That is 🏘, from Leslie 🏘. 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 set PYTHONIOENCODING=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, needed and start. All three do it.

    At set, a mismatched node is left alone and counted:

    firmware.set {"version":"repeater-v1.17.1"}      on 18 repeaters + 6 companions
      -> {"considered": 24, "mismatched": 6, "nodes": 18}
    
    firmware.set {"node":"CaptBlack-Mysteron1","version":"repeater-v1.17.1"}
      -> {"considered": 24, "mismatched": 1, "nodes": 0}
         and that node keeps companion-v1.17.1
    

    At needed and start, with a build that cannot resolve at all:

    firmware.set {"node":"CaptBlack-Mysteron1","version":"not-a-real-build"}  -> {"nodes": 1}
    firmware.needed -> {"roles":[{"choices":["companion-v1.17.1"],"nodes":1,"role":"companion_radio"}]}
    firmware.start  -> ERROR: no firmware for 1 of 24 nodes, so this run would be half a mesh:
                       CaptBlack-Mysteron1 (companion_radio not-a-real-build).
                       Pin one in the Firmware panel, or download it there
    

    choices naming 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 specific message_id:

    with regions applied (17 of 24)   its advert carried by 18 distinct nodes
    no regions at all                 its advert carried by 18 distinct nodes
    

    Identical, which is what #718 established should happen. Scenario 6 passes.

    9. One emulated board — passes

    backend=emulated
    t=325.2s   sent=1  irq=20  running=True
    
    console:  entry 0x400805e4 → E (9412) SPIFFS: mount failed, -10025
              region put sco    -> OK - (flood allowed)
              region default sco -> default scope is now sco
              get name          -> > Abernethy Repeater
    emulator: Adding SPI flash device      <- the only line
    

    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

    6,624 miss events in waveform mode (rf.mode confirmed "waveform" before measuring)
    classes: half-duplex 1910, receiver-busy 2989, floor 1467, unclassified 258
    unclassified: 258 of 6624  (3.9%)
    CRC-true recorded as a miss: 12
    dangling comma: 0
    

    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 says unclassified beside a sentence that knows the answer.

    7. Firmware development loop — step 1 passes; and a half-hour detour that became #729

    meshbench dev builds and assigns local-main correctly. The sweep then produced something that looked alarming:

    arm          seed       tx     rx  delivered   coll
    local-main   9001       94    124         61    226
    release      9001       94    124         61    226
    control      9001       94    124         61    226
    

    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 carries arm, seed and 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 populated err naming the missing build. So repeater_version is 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_spread is 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.channel reports development and refuses banana with the two valid names.

    2, 3 — 56 running, 18,965 events, four named miss classes, none unclassified in calculated mode.

    5 — sdr.serve binds 127.0.0.1, real RTL0 header 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.serve still binds [::] and hands out the LAN address, so scenario 8 still meets the Windows firewall where scenario 5 no longer does.

    8 — infos answers, bench.drop gives a clean EOF, 3 of 3 reconnects.

    10 — all three steps. Cookbook example 3 on the pre-release client:

    pip install meshbench            -> 0.0.10        (stable users undisturbed)
    pip install meshbench==0.0.11.dev3 -> 0.0.11.dev3
    124 events, 62 receptions in the tail
    

    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#7 as 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, main still 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 real meshcore-cli.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG

  25. A13xB0 commented on Sep 9, 2026

    @A13xB0
    CollaboratorAuthor

    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\MeshBench removed, both %LOCALAPPDATA%\meshbench and %APPDATA%\meshbench moved aside, meshbench-windows-x86_64-bundled.msi verified against the release's own SHA256SUMS (34cb298d…fd6b) and installed with msiexec /qn.

    meshbench v0.0.11-dev.4, go1.25.13, bundled
    QEMU emulator version 9.2.2 (v9.2.2_meshbench_sx1262_15)
    

    No new issues. This pass is confirmations: #729 fixed, #725 verified and closed, #726 holding.

    scenario verdict
    1 install and first launch passes
    2 first simulation passes
    3 debugging a missed packet passes
    4 waveform mode passes; #652 unchanged
    5 SDR observer passes
    6 importing a real network passes
    7 firmware development loop passes — #729 fixed; #671 stands
    8 companion app passes
    9 one emulated board passes
    10 scripting and the clients passes, all three steps
    11 determinism passes
    12 docs site and READMEs passes

    #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:

    arm          seed       tx     rx  delivered   coll
    local-main   9001       94    124         61    226
    release      9001       94    124         61    226
    control      9001       94    124         61    226
    

    — 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 file and bytes is the part that actually matters for local-main, which meshbench dev rebuilds in place — two runs a week apart under that label are not the same binary, and now you can tell.

    rx_spread is still 0 on every arm, so #671 stands.

    #725 — companion contacts. Verified and closed.

    AngusOutlaw1   Markinch, SCO-ANG-01-RPT, Leslie 🏘, Blairgowrie_RPT   > 4 contacts in device
    Mothy          Markinch, Leslie 🏘, Blairgowrie_RPT                   > 3 contacts in device
    Jazzy          Markinch, Leslie 🏘, Blairgowrie_RPT                   > 3 contacts in device
    

    Fixed on two consecutive releases, both halves agreeing — the panel, companion.state, and meshcore-cli. I closed it, with the Windows trap recorded on the issue: meshcore-cli dies on UnicodeEncodeError: '\U0001f3d8' printing Leslie 🏘 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.

    firmware.set {"version":"repeater-v1.17.1"}  on 18 repeaters + 6 companions
      -> {"considered": 24, "mismatched": 6, "nodes": 18}
    firmware: 24 of 24 running
    

    The six companions keep companion-v1.17.1 and 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.channel reports development and refuses banana naming 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.mode before 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.serve binds 127.0.0.1, real RTL0 header 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.serve still 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 that message_id: 18 distinct nodes with regions and 18 without, which is what #718 established should happen.

    8 — infos answers, contacts list, bench.drop gives a clean EOF, 3 of 3 reconnects.

    9 — backend=emulated, first transmission at t=250 s, emulator log holds only Adding SPI flash device, and the console answers:

    region default sco -> default scope is now sco
    get name           -> > Abernethy Repeater
    

    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 meshbench still resolves to stable 0.0.10 and the exact pin gives 0.0.11.dev4; cookbook example 3 runs on it (124 events, 62 receptions in the tail). meshbench test -junit exits 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, and reference-control.html twice. Nothing wrong relative to its source; tracked on MeshBench/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 real meshcore-cli.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01RSb8bN9gtL72cXAxtyTbAG

  26. added a commit that references this issue on Sep 9, 2026
  27. A13xB0 commented on Sep 9, 2026

    @A13xB0
    CollaboratorAuthor

    Part 11: v0.0.11, the release, on macOS

    MeshBench-arm64-bundled.dmg from v0.0.11. Scenario 1 was run from a genuinely clean profile — ~/Library/Caches/meshbench and ~/Library/Application Support/meshbench both moved aside first — and the rest followed on the profile that first launch built. macOS 26.3 (25D125), Apple M4.

    meshbench-bin v0.0.11, go1.25.14, bundled
    qemu-system-xtensa  v9.2.2_meshbench_sx1262_15
    

    All twelve scenarios pass. Part two is 112 of 115, every picture of the right window. One issue opened: #737.


    Scenario verdicts

    # scenario verdict
    1 Install and first launch passes — 513 MB of terrain in 2.5 min, priced in bytes, ground: terrain 6233/6233, GPU warm 71,253 pairs in 8.4 s
    2 Your first simulation passes — 56 of 58 in 3 s, flood to 44 transmitters, 183 receptions
    3 Debugging a missed packet passes — causes separate cleanly, link.result returns both margins and every term
    4 Waveform mode passes, and better than before — see #735 below
    5 SDR observer passes — 1.89 % above the floor while playing, 0.00 % paused
    6 Importing a real network passes on the air; #737 against what the view says
    7 Firmware development loop passes, and this is the best the sweep has looked — see below
    8 A companion app passes — SELF_INFO, drop kills the socket, reconnect works
    9 One emulated board passes on all three families
    10 Scripting and the clients passes, both steps
    11 Determinism passes — 318 three times
    12 Docs and READMEs docs#35 fixed; docs#34 and #36 still open
    part two 115 pictures 112, all correct windows

    The five fixes from this release, checked

    #733 — regions reach a running node. infer.apply now answers live beside applied: 0 before firmware is up, 45 after. 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.1 to role: simple_repeater gives {"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 builds with role, version, filename and bytes, and it answered a question I had before I could go looking:

    mine     rellocal            meshcore-simple_repeater-darwin-arm64  478504 bytes
    release  repeater-v1.17.1    meshcore-simple_repeater-darwin-arm64  478600 bytes
    

    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. unclassified fell from 62 to 3 in a comparable run, with collision and interference picking up what it was absorbing:

    dev.2 v0.0.11
    unclassified 62 3
    collision 90 90
    interference 301 301
    received 413 413

    #711 and #714 hold in the release. meshbench test prints node storage: fresh, every node factory-new, removed after the run, leaves nodefs at 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:

    mine     9001 tx 141 rx 170 delivered 107     control 9001 tx 130 rx 115 delivered  94
    mine     9002 tx 141 rx 193 delivered 130     control 9002 tx 141 rx 208 delivered 130
    mine     9003 tx 141 rx 168 delivered 105     control 9003 tx 137 rx 138 delivered 101
    release  9001 tx 130 rx 115 delivered  94
    release  9002 tx 141 rx 208 delivered 130
    release  9003 tx 137 rx 138 delivered 101
    

    control reproduces release run for run, the seeds move each arm, and the warning is finally the sentence the method needs:

    the seeds disagree by more than the arms do on receptions (±30.3 % within an arm, 13.2 % between them) - add repeats

    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:

    build 9001 9002 9003 4242 77 range
    0.0.11-dev.1 322 323 335 327 — 13 (4 %)
    0.0.11-dev.2 350 343 323 312 320 38 (11 %)
    0.0.11 318 284 291 261 304 57 (18 %)

    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.apply answers {applied: 45, live: 45} and within half a second everything readable says zero. Three consecutive cycles:

    cycle 1: infer.apply -> {'applied': 45, 'live': 45}
       t+0.0s live=45 list=0   t+0.4s live=0 list=0   …   t+2.0s live=0 list=0
    cycle 2: same
    cycle 3: same
    

    The engine is fine — with the view sitting at 0 of 60 a companion's scoped public message 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.html now 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.pause and sim.play are 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 now still renders 0.0% for every node while memory and cpu time correctly 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions