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.
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:
- Firmware images: Firmware & Releases → GitHub releases
- Raspberry Pi: Initial setup — Raspberry Pi
- HaLowLink 2: Initial setup — HaLow Link 2
- Mesh + routing: Networking and B.A.T.M.A.N. Advanced setup
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.
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.
| File | For | What it is |
|---|---|---|
| meshflow-linux-arm64 | Raspberry Pi 4 and other ARM64 nodes | The MeshFlow node agent (discovery, routing, dashboard, cloud bridge). 25 MB, static, no dependencies. |
| meshflow-linux-mipsle | GL.iNet HaLowLink 2 | The same agent built for MIPS (`mipsle`, soft-float). 29 MB. |
| meshflow-ui.tar.gz | every node | The dashboard — the same web console you get at /app/, served by the node itself. Unpack to ui/ next to the agent. |
| mediamtx.yml | every node | Our MediaMTX configuration: RTSP :8554, HLS :8888, WebRTC :8889, control API :9997, publisher-sourced paths. |
| meshflow.init | every node | OpenWrt procd service that keeps the agent and MediaMTX alive across crashes and reboots. |
| mediamtx (ARM64) | Raspberry Pi 4 | MediaMTX v1.11.0, the media server, straight from its upstream release. Extract the mediamtx binary only — keep our mediamtx.yml. |
| mediamtx-linux-mipsle | GL.iNet HaLowLink 2 | MediaMTX v1.11.0 built by us for MIPS (upstream publishes no MIPS build). 23 MB. |
| SHA256SUMS | Checksums 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
'
GL.iNet HaLowLink 2 (MIPS) — installs to /tmp, must be re-copied after a reboot
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
'
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>/orhttp://<hostname>.local/. Your node is listed by name; other MeshFlow nodes on the mesh appear as they’re discovered (UDP broadcast on9998, a few seconds). - API:
curl http://<node-ip>/api/inforeturns the node’s identity (name, addresses);curl http://<node-ip>/api/streamslists every stream it can see. - Media server:
curl http://127.0.0.1:9997/v3/paths/liston 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 asteston your node, and on every other node’s dashboard too. - Logs:
logread -e meshflow(orlogread -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
curlworks 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 oempty, radio silent:dmesg | grep -i morse.SPI transfer timed outmeans 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/rpicamerabut not in MeshFlow: that stream belongs to OpenMANET’s MediaMTX. Point the camera (or anffmpegre-publish) at MeshFlow’srtsp://<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.