CLI reference

workbench opens the desktop application. Every other command is headless, and that split is deliberate and permanent: the headless path is what scripted runs and regression suites are built on, not a stopgap for the window.

Nothing but workbench needs a GPU, a display, or anything running anywhere else.

meshbench <command> [flags]

There are 16 commands and 170 flags between them, of which 38 exist only so a panel, a menu or a view can be reached without a click.

Everything below the first heading is generated from the flag declarations themselves, by tools/flagdoc/flagdoc.py, which builds the binary and asks it. A flag that exists is on this page and a flag that has gone is not, because CI fails when the two disagree. The examples are run by hand and held beside the commands in cmd/meshbench/commands.go; what is checked mechanically is that every flag they name is still real.

Every command also takes -h, which prints the same flags with the same defaults.

Two conventions in the tables below. A default shown as <cache>/... is the per-user cache directory, which is ~/.cache on Linux and different elsewhere; the binary prints the real path. And required in the default column means the command refuses to run without it.

Results are a best case. The model has no multipath, bare-earth terrain and an idealised demodulator. If it says a link will not work, believe it; if it says a link works marginally, go and measure.

What a flag is for#

Every flag below carries one of these, because a flag that arranges a screenshot and a flag that changes an answer are not the same kind of thing and a reference that lists them together is misleading.

formeaning
capturecapture and scripting: it reaches a view, a panel or a menu without a click, so a screenshot or a script does not need a hand on the mouse. None of these changes a result.
datawhere the inputs come from, and whether the network may be used to get them. Nothing here is part of the physics.
diagnosticmeasures the application itself rather than the network it is simulating.
outputwhere the answer is written and how much of it is said. It does not change what was computed.
resultan input the answer depends on. Change it and the number changes.

The commands#

commandwhat it doesflags
linklink budget between two points, both directions15
profileterrain profile and the worst obstruction on a path10
coveragecoverage raster from one station, written as a PNG15
spectrumwhat an SDR observer captures: waterfall PNG and audio8
terraindownload elevation tiles for an area8
boardsthe hardware profiles this build knows about0
firmwarelist, download or import MeshCore firmware7
energywill a solar node survive the winter7
airtimeLoRa time on air, as the firmware computes it4
trafficflood a message through a network and report what happened15
basemapdownload map tiles for an area8
devbuild a MeshCore checkout and give it to the workbench5
serverun a mesh and expose a companion to your app8
testrun a fixture on real firmware and check its assertions10
headlessrun the verbs over the control socket, with no window7
workbenchopen the desktop workbench: build a scenario on a map and run it43

Link budget between two points, in both directions.

meshbench link -from-lat 56.3980 -from-lon -3.4260 -from-height 20 -to-lat 56.3327 -to-lon -3.3239 -to-height 10 -offline

A mast on the hill against a repeater in the glen, 9.6 km apart. Both directions are reported because reachability is asymmetric, and this pair happens to be balanced at +5.4 dB.

flagdefaultformeaning
-freq869.525resultfrequency, MHz
-from-gain2.15resultantenna gain, dBi
-from-height10resultantenna height above ground, metres
-from-latrequiredresultlatitude of the first station
-from-lonrequiredresultlongitude of the first station
-from-tx22resulttransmit power, dBm
-offlinefalsedatanever download; answer from the cache and fail loudly otherwise
-sensitivity-137resultreceiver sensitivity, dBm
-terrain-cache<cache>/meshbench/terraindatawhere downloaded elevation tiles live
-to-gain-2resultantenna gain, dBi
-to-height1.5resultantenna height above ground, metres
-to-latrequiredresultlatitude of the second station
-to-lonrequiredresultlongitude of the second station
-to-tx22resulttransmit power, dBm
-zoom12resulttile zoom; 12 is about 30 m per pixel and matches the data

meshbench profile#

Terrain profile and the worst obstruction on a path.

meshbench profile -from-lat 56.3980 -from-lon -3.4260 -from-height 20 -to-lat 56.0700 -to-lon -3.4530 -samples 400 -offline

What stands in the way when a link budget comes back short. This one names the hill, 15.7 km along and 272 m into the path.

flagdefaultformeaning
-from-height10resultantenna height above ground, metres
-from-latrequiredresultlatitude of the first point
-from-lonrequiredresultlongitude of the first point
-offlinefalsedatanever download; answer from the cache and fail loudly otherwise
-samples200resultprofile samples
-terrain-cache<cache>/meshbench/terraindatawhere downloaded elevation tiles live
-to-height1.5resultantenna height above ground, metres
-to-latrequiredresultlatitude of the second point
-to-lonrequiredresultlongitude of the second point
-zoom12resulttile zoom; 12 is about 30 m per pixel and matches the data

meshbench coverage#

Coverage raster from one station.

meshbench coverage -lat 56.3980 -lon -3.4260 -height 20 -radius 15 -pixels 200 -o perth.png -offline

A 30 km square around the mast, at 200 by 200 cells. One-way cells get their own colour: they are neither covered nor not.

flagdefaultformeaning
-freq869.525resultfrequency, MHz
-gain2.15resultantenna gain, dBi
-height10resultantenna height above ground, metres
-latrequiredresultstation latitude
-lonrequiredresultstation longitude
-ocoverage.pngoutputoutput PNG
-offlinefalsedatanever download; answer from the cache and fail loudly otherwise
-pixels400resultraster width in pixels
-radius20resulthalf-width of the area, km
-remote-height1.5resultheight of the imagined far station, metres
-remote-tx22resultfar station transmit power, dBm
-sensitivity-137resultreceiver sensitivity, dBm
-terrain-cache<cache>/meshbench/terraindatawhere downloaded elevation tiles live
-tx22resulttransmit power, dBm
-zoom12resulttile zoom; 12 is about 30 m per pixel and matches the data

meshbench spectrum#

What an SDR observer captures.

meshbench spectrum -sf 10 -bandwidth 250 -rx -120 -o waterfall.png -wav chirp.wav

An SF10 chirp 6 dB under the noise floor, as a picture and as a sound. A chirp through a narrow filter is a rising whistle.

flagdefaultformeaning
-bandwidth250resultbandwidth, kHz
-freq869.525resultcentre frequency, MHz
-noise-figure6resultobserver noise figure, dB
-owaterfall.pngoutputwaterfall PNG
-rx-100resultreceived signal level, dBm
-sf10resultspreading factor of the transmission
-symbols8resultsymbols to capture
-wavnoneoutputalso write audio here, as an SDR would sound

meshbench terrain#

Download elevation tiles for an area.

meshbench terrain -south 56.0 -north 56.5 -west -3.6 -east -2.8 -estimate

What the download would cost, before spending it. Drop -estimate to fetch. Tiles cache permanently, so -offline answers from them afterwards.

flagdefaultformeaning
-eastrequireddataeastern edge
-estimatefalsedatareport the download and stop
-northrequireddatanorthern edge
-offlinefalsedatanever download; answer from the cache and fail loudly otherwise
-southrequireddatasouthern edge
-terrain-cache<cache>/meshbench/terraindatawhere downloaded elevation tiles live
-westrequireddatawestern edge
-zoom12datatile zoom; 12 is about 30 m per pixel and matches the data

meshbench boards#

The hardware profiles this build knows about.

meshbench boards

RADIATED is what leaves the antenna: chip power minus board loss plus the antenna it ships with. That is the number that decides range, and it is not the number on the box.

It takes no flags.

meshbench firmware#

List, download or import MeshCore firmware.

meshbench firmware -offline

What is already on this machine. Without -offline it lists the published catalogue; -get fetches one, -import takes a build of your own.

flagdefaultformeaning
-boardnonedatafilter by board, or name the board when importing
-cache<cache>/meshbench/firmwaredatawhere downloaded images live
-getnonedatadownload an image by name, e.g. RAK_4631/repeater
-importnonedataimport your own .uf2, .bin or .elf
-labelnonedatawhat to call an imported build; defaults to a timestamp
-offlinefalsedatalist and use only what is already downloaded
-rolerepeaterdatarole, when importing

meshbench energy#

Will a solar node survive the winter.

meshbench energy -lat 56.34 -panel 10 -battery 6000 -tx 22

A 10 W panel and a 6 Ah cell at Scottish latitude, over a year. Receive current, not transmit power, is what usually decides this.

flagdefaultformeaning
-always-ontrueresulta repeater listens continuously
-battery3400resultbattery capacity, mAh
-latrequiredresultlatitude, north positive
-lon0resultlongitude, east positive
-panel0resultpanel peak watts; 0 for no solar
-tilt50resultpanel tilt from horizontal, degrees
-tx22resulttransmit power, dBm

meshbench airtime#

LoRa time on air, as the firmware computes it.

meshbench airtime -sf 10 -bandwidth 250 -bytes 32

259 ms, and 139 transmissions an hour at a 1% duty cycle. The same arithmetic the firmware's own getEstAirtimeFor() does.

flagdefaultformeaning
-bandwidth250resultbandwidth, kHz
-bytes32resultpayload length
-coding-rate1result1 to 4, for 4/5 to 4/8
-sf10resultspreading factor

meshbench traffic#

Flood a message through a network and report what happened.

printf &#x27;[{"Name":"Perth Hill","HasPosition":true,"Lat":56.398,"Lon":-3.426,"HeightAGLm":20,"Kind":"repeater"},{"Name":"Abernethy Repeater","HasPosition":true,"Lat":56.33271,"Lon":-3.32386,"HeightAGLm":10,"Kind":"repeater"},{"Name":"Glenrothes","HasPosition":true,"Lat":56.198,"Lon":-3.178,"HeightAGLm":10,"Kind":"repeater"},{"Name":"Kirkcaldy","HasPosition":true,"Lat":56.113,"Lon":-3.16,"HeightAGLm":8,"Kind":"repeater"}]\n' > fife.json
meshbench traffic -nodes fife.json -from "Perth Hill" -for 20000 -offline

One message into four nodes, with a cause for every node it did not reach. Add -firmware to run real MeshCore on each instead of injecting traffic.

flagdefaultformeaning
-bandwidth250resultbandwidth, kHz
-boardRAK4631resultboard profile for imported nodes
-firmwarefalseresultrun a real MeshCore build on every node, rather than injecting traffic
-for20000resulthow long to simulate, ms
-freq869.525resultfrequency, MHz
-fromnoneresultnode to send from; the first repeater by default
-nodesnonedatascenario JSON, or a CoreScope/Beacon export
-offlinefalsedatanever download; answer from the cache and fail loudly otherwise
-sf10resultspreading factor
-sourcenonedataload nodes from a provider: corescope or beacon
-terrain-cache<cache>/meshbench/terraindatawhere downloaded elevation tiles live
-tokennonedataprovider token, if it needs one
-urlnonedataprovider base URL
-vfalseoutputprint every event rather than a summary
-zoom12resulttile zoom; 12 is about 30 m per pixel and matches the data

meshbench basemap#

Download map tiles for an area.

meshbench basemap

The layers, with the attribution each one requires. Naming one with -layer and an area downloads it.

meshbench basemap -layer carto-light -south 56.0 -north 56.5 -west -3.6 -east -2.8 -zoom 11 -estimate

36 tiles, about 1 MB. Every layer here contacts a third party.

flagdefaultformeaning
-cache<cache>/meshbench/basemapdatatile cache
-east0dataeastern edge
-estimatefalsedatareport the download and stop
-layernonedatawhich layer; omit to list them
-north0datanorthern edge
-south0datasouthern edge
-west0datawestern edge
-zoom11datatile zoom

meshbench dev#

Build a MeshCore checkout and give it to the workbench.

meshbench dev -from ~/src/MeshCore -role simple_repeater -assign=false

Builds the checkout into the firmware cache and stops there. Nothing is written into the MeshCore tree. Add -watch for a rebuild on every save, and drop -assign=false to put it on every node of that role.

flagdefaultformeaning
-assigntruecaptureassign the build to every node of that role
-from.dataa MeshCore checkout to build
-namenonedatawhat to call the build; the git branch by default
-rolesimple_repeaterdatawhich application: simple_repeater, companion_radio or simple_room_server
-watchfalsecapturerebuild and reassign whenever a source file changes

meshbench serve#

Run a mesh and expose a companion to your app.

meshbench serve -fixture fixtures/fixture-fife-strict.json

56 of the fixture's 58 nodes on real firmware - the SDR observer and the emitter boot none - and one companion on a loopback port it prints. Point a client at that address; -serial gives a pty instead.

flagdefaultformeaning
-addr127.0.0.1:0outputaddress to listen on; port 0 picks a free one
-fixturenoneresultnetwork to run; the smallest shipped one by default
-nodenoneoutputwhich companion to expose; the first one by default
-offlinefalsedatanever download; answer from the cache and fail loudly otherwise
-quietfalseoutputprint the endpoint and nothing else
-serialfalseoutputexpose a virtual serial device instead of TCP
-terrain-cache<cache>/meshbench/terraindatawhere downloaded elevation tiles live
-zoom12resulttile zoom; 12 is about 30 m per pixel and matches the data

meshbench test#

Run a fixture and check its assertions.

meshbench test -fixture fixtures/fixture-fife-strict.json -for 60000 -quiet

The one a pipeline calls. Exit 0 if every assertion passed, 1 if any failed; -junit writes a report with one case per assertion.

flagdefaultformeaning
-endpointnoneoutputserve a companion node to a real client: "tcp:<node>" or "serial:<node>"
-fixturerequiredresultfixture JSON to run
-for120000resulthow long to simulate, ms
-junitnoneoutputwrite a JUnit XML report here
-keep-node-storagefalseresultreuse what the nodes stored last run, identities and settings, as hardware would, instead of booting every node factory-fresh
-offlinefalsedatanever download; answer from the cache and fail loudly otherwise
-quietfalseoutputonly print the verdict
-seed0resultoverride the fixture's seed
-terrain-cache<cache>/meshbench/terraindatawhere downloaded elevation tiles live
-zoom12resulttile zoom; 12 is about 30 m per pixel and matches the data

meshbench headless#

Run the verbs with no window, for scripts and CI.

meshbench headless -fixture fife-strict -play -for 15s -control-socket /tmp/meshbench.sock

The same session the window builds, with nothing attached to look at it. A client connects to that socket and drives it with the verbs.

flagdefaultformeaning
-control-socketnonecapturewhere to answer: a path for a unix socket, or "tcp" for loopback with a token (the default on Windows). Two runs on one machine need two addresses
-fixturenoneresultopen this fixture or project at startup
-for0sresultexit after this long; the default is to run until interrupted
-playfalsecapturestart the run immediately
-quietfalseoutputdo not echo status lines to stderr
-seed0resultoverride the scenario's seed
-unverified-wiringfalseresultrun boards whose wiring nobody has watched boot

meshbench workbench#

Open the desktop workbench: build a scenario on a map and run it.

meshbench workbench -list-fixtures

The networks built into this binary, without opening a window.

meshbench workbench -fixture fife-strict -panel Nodes -filter Abernethy -look 56.34,-3.32,11 -quit-after 20s

One panel filling the window, filtered, over a fixed view, closing itself. That is the capture shape: every one of those flags exists so a screenshot does not need a hand on the mouse.

flagdefaultformeaning
-board-decodefalsecaptureopen the board view's console with its decode tick on, so what a companion's framed protocol says can be captured
-board-tabnonecapturewhich table the board view opens on: Radio or Wiring
-board-viewnonecaptureopen this node's board view at startup
-capturenonecapturecapture the waterfall at this node once the run has traffic
-config-sectionnonecaptureopen the Configuration page on this section
-control-socketnonecapturewhere the control socket answers: a path for a unix socket, or "tcp" for loopback with a token (the default on Windows, which has no unix socket a Python client can reach). MESHBENCH_CONTROL_SOCKET does the same, and two workbenches need two
-coveragenonecapturecompute and show coverage from this node at startup
-cpuprofilenonediagnosticwrite a CPU profile here
-drop-menunonecaptureopen this menu's dropdown at startup, so it can be captured
-energyfalsecapturerun the site study for the selected node at startup
-filternonecapturepreset the node view's search box, so a filtered table can be captured
-fixturescotland-ireland-strictresultnetwork to load: a name (see -list-fixtures) or a path to a .json
-fpsfalsediagnosticreport frames per second to stderr and /tmp/wb2-fps.log
-importnonecapturedescribe an import from this CoreScope URL at startup
-injectnonecaptureoriginate a packet at this node once running
-inject-every0scapturekeep originating at that node this often; for looking at the traffic layer
-layersnonecaptureswitch these map layers on at startup, comma separated
-licence-sectionnonecapturescope the Licences panel to one section: forks, bundled, golibs, runtime, data
-list-fixturesfalseoutputlist the built-in networks and exit
-looknonecapturestart the camera at lat,lon,level - a slippy zoom level, 0 the world and the high teens a street; a capture cannot drag the map
-memprofilenonediagnosticwrite a heap profile here on exit
-menunonecapturefire this menu action at startup, so what it opens can be captured
-node-menunonecaptureopen this node's context menu at startup
-node-tabnonecapturewhich tab a node window opens on, by name: Console, Companion, SDR, Settings, Radio, Antenna, Stats, Activity, Connect, Hardware, Output
-node-windownonecaptureopen this node's own window at startup
-open-firmwarenonecaptureopen this node's firmware list at startup
-packet-tab0capturewhich tab the packet window opens on: 0 dissection, 1 journey (the propagation graph), 2 reception ledger, 3 where it went
-panelnonecapturedraw only this panel, filling the window
-plannonecaptureplan between the selected node and this one at startup
-playfalsecapturestart the simulation immediately
-pop-outnonecaptureopen this panel in its own window at startup
-provisioningnonecaptureshow what this node is told at boot, at startup
-quit-after0scaptureexit after this long; 0 runs until closed
-save-runnonecapturesave a run record under this name, then keep running
-seed0resultoverride the scenario's seed
-sweepfalsecapturerun the default sweep at startup
-terrainfalsecaptureshade the relief at startup
-themedarkcapturedark or light
-unverified-wiringfalseresultrun boards whose emulation wiring nobody has watched boot yet
-update-checkfalsecaptureask whether a newer release exists shortly after startup, whatever the schedule says
-update-feednonedataask this URL whether a newer release exists instead of the published release feed
-versionfalseoutputprint the version and exit
-viewplancapturewhich view to open
MeshBench documentation. Built from the running application, not from mock-ups. Screenshots are window-only captures; see CLAUDE.md for the rule that keeps them current. Edit this page.