You don’t need node hardware to use MeshFlow — a phone with a hotspot is a gateway on its own (the alpha). A node is what you add when you want range (Wi-Fi HaLow reaches kilometres, not metres) and a media server in the field that phones can hand their feeds to. The mesh itself is OpenMANET: an open-source, B.A.T.M.A.N.-adv tactical mesh on 802.11ah HaLow radios. MeshFlow is the video layer that runs on top of it.

The recipe is short: flash OpenMANET, copy in two binaries and the dashboard, enable a service. Everything mesh-side (radios, routing, gateways) is OpenMANET’s job and is documented by them; this page only covers what MeshFlow adds.

What a node gives you
A dashboard at http://<node>.local listing every stream on the mesh by node name · a MediaMTX media server on every node that phones, GoPros and RTSP/RTMP cameras publish to · pull-based routing across hops chosen by B.A.T.M.A.N. link quality · a cloud bridge (through a gateway node with internet) so a mesh that's split by terrain, or another mesh on another continent, joins the same network.

1. Pick hardware

MeshFlow is built for the two OpenMANET targets we run ourselves:

Node CPU Role we use it in Storage
Raspberry Pi 4 + Morse Micro / Seeed HaLow HAT ARM64 (aarch64) field node — cameras, phones, the dashboard SD card: plenty. Binaries live in /root.
GL.iNet HaLowLink 2 MIPS little-endian (mipsel_24kc), 248 MB RAM backbone / gateway (Starlink, cell uplink) 2.4 MB of writable flash. Binaries live in RAM at /tmp (see step 4).

OpenMANET’s own hardware pages are the reference for what to buy and how to assemble it: Raspberry Pi · HaLowLink 2 · all supported hardware. Other OpenMANET targets (Heltec HT-HD01, Gateworks Venice) are ARM64 too and should run the ARM64 binary, but we haven’t tested them.

2. Flash OpenMANET and join the mesh

Follow OpenMANET’s setup guide for your device — it’s the authoritative source and we don’t duplicate it:

A fresh node comes up at 10.41.254.1 (user root, no password) and its wizard sets the node type — mesh gate (has an uplink) or mesh point — and the radio. Every radio on one mesh must share the same mesh ID, password, channel and bandwidth. When batctl n on a node lists its neighbours, the mesh is up and you’re ready for MeshFlow.

Set a root password before you go further — MeshFlow's dashboard is served without a login, on the assumption that the mesh itself is the trust boundary. OpenMANET ships with SSH open and no password.

3. Download the MeshFlow node release

Current node release: 0.1.13 (matches the alpha app). Free for non-commercial use under the MeshFlow EULA; the node contains no telemetry and never phones home on its own.

FileForWhat it is
meshflow-linux-arm64Raspberry Pi 4 and other ARM64 nodesThe MeshFlow node agent (discovery, routing, dashboard, cloud bridge). 25 MB, static, no dependencies.
meshflow-linux-mipsleGL.iNet HaLowLink 2The same agent built for MIPS (`mipsle`, soft-float). 29 MB.
meshflow-ui.tar.gzevery nodeThe dashboard — the same web console you get at /app/, served by the node itself. Unpack to ui/ next to the agent.
mediamtx.ymlevery nodeOur MediaMTX configuration: RTSP :8554, HLS :8888, WebRTC :8889, control API :9997, publisher-sourced paths.
meshflow.initevery nodeOpenWrt procd service that keeps the agent and MediaMTX alive across crashes and reboots.
mediamtx (ARM64)Raspberry Pi 4MediaMTX v1.11.0, the media server, straight from its upstream release. Extract the mediamtx binary only — keep our mediamtx.yml.
mediamtx-linux-mipsleGL.iNet HaLowLink 2MediaMTX v1.11.0 built by us for MIPS (upstream publishes no MIPS build). 23 MB.
SHA256SUMSChecksums for everything above that we host. sha256sum -c SHA256SUMS

OpenMANET's Raspberry Pi image already runs its own MediaMTX for the Pi camera on RTSP port 554. MeshFlow runs a second, separate instance on its own ports, so the two don't collide and the camera feed keeps working.

4. Install on the node

The examples below use scp/ssh from your laptop with the node reachable at NODE (its mesh IP, or <name>.local). OpenMANET nodes run dropbear, which has no SFTP server: modern OpenSSH’s scp speaks SFTP by default and fails with “EOF during negotiation”, so pass -O to use the legacy protocol.

Raspberry Pi (ARM64) — installs to /root, survives reboot

NODE=10.41.113.1     # your node
# from the directory where you downloaded the release:
tar xzf mediamtx_v1.11.0_linux_arm64v8.tar.gz mediamtx
scp -O meshflow-linux-arm64 mediamtx mediamtx.yml meshflow-ui.tar.gz meshflow.init root@$NODE:/root/
ssh root@$NODE '
  cd /root && mv meshflow-linux-arm64 meshflow && chmod +x meshflow mediamtx &&
  mkdir -p ui && tar xzf meshflow-ui.tar.gz -C ui && rm meshflow-ui.tar.gz &&
  mv meshflow.init /etc/init.d/meshflow && chmod +x /etc/init.d/meshflow
'

The HaLowLink 2 has 2.4 MB of writable flash; the agent alone is 29 MB, so it runs from /tmp (a 121 MB RAM disk). That means a reboot wipes it — re-run the copy afterwards. The service script looks in /root first and falls back to /tmp automatically.

NODE=10.41.1.1       # your backbone node
scp -O meshflow-linux-mipsle mediamtx-linux-mipsle mediamtx.yml meshflow-ui.tar.gz meshflow.init root@$NODE:/tmp/
ssh root@$NODE '
  cd /tmp && mv meshflow-linux-mipsle meshflow && mv mediamtx-linux-mipsle mediamtx && chmod +x meshflow mediamtx &&
  mkdir -p ui && tar xzf meshflow-ui.tar.gz -C ui && rm meshflow-ui.tar.gz &&
  mv meshflow.init /etc/init.d/meshflow && chmod +x /etc/init.d/meshflow
'
If the flash ever fills the overlay silently mounts read-only at boot (dmesg | grep overlayfs shows failed to create directory /overlay/work/work (errno: 28)) and nothing will install or keep its exec bit. Free space by deleting through /overlay/upper/… (the underlying JFFS2 is still writable), then reboot. Never put the binaries in /root on this device.

5. Free port 80 and fix the MTU

Port 80. The MeshFlow dashboard listens on 80 (HTTP) and 443 (HTTPS) so a phone can reach it as plain http://<node>.local — browsers won’t accept a self-signed certificate on a .local name without that plain-HTTP front door. OpenMANET’s own web UI lives on :8080/:8081, but if LuCI’s uhttpd is still on 80/443, move it to 8000/4443 first:

ssh root@$NODE '
  uci set uhttpd.main.listen_http="0.0.0.0:8000"
  uci set uhttpd.main.listen_https="0.0.0.0:4443"
  uci commit uhttpd && /etc/init.d/uhttpd restart
'

(Or run the agent elsewhere with -port 8088 in the service command line — but then phones need the port typed in.) If 80 is taken, the agent logs [API] HTTP Server Error and keeps running on 443 only, which looks like “the dashboard is dead” from a phone. Check logread before anything else.

MTU. B.A.T.M.A.N.-adv adds a ~28-byte header. At the Wi-Fi default of 1500, the mesh forms (originators appear, batctl n looks fine) but unicast data is silently dropped — 100% packet loss on ping, dead video. The HaLow interface carrying batman (wlan0) must be 1532, and bat0 / br-ahwlan 1460:

ssh root@$NODE '
  uci set network.bat0.mtu="1460"; uci set network.ahwlan.mtu="1460"; uci commit network
  ip link set dev wlan0 mtu 1532; ip link set dev bat0 mtu 1460; ip link set dev br-ahwlan mtu 1460
  grep -q "wlan0 mtu 1532" /etc/rc.local || sed -i "/^exit 0/i ip link set dev wlan0 mtu 1532" /etc/rc.local
'

The service script re-applies the wlan0 MTU every time it starts, so a node that lost it comes back right.

Gateways. On the node with the internet uplink, batctl gw_mode server (OpenMANET’s mesh gate) and turn on bridge-loop avoidance if it bridges the mesh to a shared backbone like a Starlink — otherwise you get broadcast storms. Field nodes run batctl gw_mode client. OpenMANET’s wizard sets this; MeshFlow just reads it.

6. Start it

ssh root@$NODE '/etc/init.d/meshflow enable && /etc/init.d/meshflow start'

The service runs the agent as meshflow -debug -ui-path <base>/ui -name <hostname> and MediaMTX with our mediamtx.yml, respawning either if it dies. The node’s name on the dashboard is its hostname — set it with uci set system.@system[0].hostname='SCOUT-01'; uci commit system and reboot, and it’s SCOUT-01 everywhere instead of a MAC address.

If your team uses its own global network (the name and password you enter in the app), add -network-name <name> -network-pass <password> to the procd_set_param command line in /etc/init.d/meshflow so the node joins the same one — that’s what lets a gateway node bridge your mesh to the cloud and to other islands.

7. Verify

From anything on the mesh (a phone on the node’s AP counts):

  • Dashboard: http://<node-ip>/ or http://<hostname>.local/. Your node is listed by name; other MeshFlow nodes on the mesh appear as they’re discovered (UDP broadcast on 9998, a few seconds).
  • API: curl http://<node-ip>/api/info returns the node’s identity (name, addresses); curl http://<node-ip>/api/streams lists every stream it can see.
  • Media server: curl http://127.0.0.1:9997/v3/paths/list on the node itself. Publish a test feed at it — ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 -c:v libx264 -f rtsp rtsp://<node-ip>:8554/test — and it shows up on the dashboard as test on your node, and on every other node’s dashboard too.
  • Logs: logread -e meshflow (or logread -f). A healthy start logs the HTTP server on 80, the HTTPS server on 443, and MediaMTX answering on 9997.
  • The app: open the MeshFlow app on a phone joined to the node’s Wi-Fi. Node presence goes bridged, the node’s streams appear in the Streams tab, and Share to ATAK puts them on the map.

Ports a node uses

Port What
80 / 443 MeshFlow dashboard and API (HTTP / HTTPS, self-signed)
9998 / udp MeshFlow stream discovery between nodes
8554 RTSP publish + play (MediaMTX)
1935 RTMP publish — what a GoPro livestreams to (MediaMTX)
8888 HLS (MediaMTX)
8889 / 8189 udp WebRTC signalling and ICE (MediaMTX)
9997 MediaMTX control API (localhost, used by the agent)
8000 / 4443 Where we park LuCI (see step 5)

Troubleshooting

  • Mesh forms but nothing flows (neighbours listed, ping 100% loss): MTU. Step 5.
  • Dashboard unreachable from a phone but curl works on the node: the OpenWrt firewall. The node’s AP zone (ahwlan) accepts input on OpenMANET’s image; if you changed zones, allow input on the interface phones arrive from.
  • A node that was working “went dark” after a reboot (HaLowLink 2): the binaries were in /tmp. Re-copy (step 4) and start. The service is still enabled.
  • HaLowLink 2 refuses to install anything / lost exec bits: read-only overlay. See the note in step 4.
  • Powered on, batctl o empty, radio silent: dmesg | grep -i morse. SPI transfer timed out means the HaLow radio’s bus hung — physically power-cycle; software resets don’t recover it.
  • Two gateways, both on Starlink: give the second dish a different LAN subnet (e.g. 192.168.2.1) in the Starlink app or the backbone IPs collide.
  • Camera works on OpenMANET’s rtsp://…:554/rpicamera but not in MeshFlow: that stream belongs to OpenMANET’s MediaMTX. Point the camera (or an ffmpeg re-publish) at MeshFlow’s rtsp://<node>:8554/<name> and it’s on the dashboard.

Stuck? info@totaloverwatchproductions.com — send logread output and batctl n.

License

The node binaries are free for personal, hobbyist and educational use. Business, government and other revenue-generating deployments need a commercial license — an annual flat rate per organisation with no seat or node limits. The full End User License Agreement is in the release. The software contains no telemetry, no auto-update pings and no remote license checks; it is built to work air-gapped.

⬇ Node release 0.1.13 ← Back to MeshFlow