A lightweight mesh networking utility for Raspberry Pi. Provides text and voice communication between ARM64 devices over UDP or LoRa radio, using Codec2 for low-bitrate speech encoding and direct Linux kernel ioctls for PCM audio (no ALSA userspace library).
- Windows with Visual Studio (C++ Linux Development workload)
- WSL2 (Ubuntu) with the ARM64 cross-compiler and autotools:
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu cmake make autoconf automake libtool
- Raspberry Pi targets running Linux (ARM64)
Clone the repository and pull submodules:
git clone <repo-url>
cd advmesh
git submodule update --init --recursiveAll third-party libraries live as git submodules under extern/ and are cross-compiled for ARM64 via WSL2. A single script handles everything:
.\build_deps.ps1 # Build all dependencies
.\build_deps.ps1 -Name codec2 # Build a specific dependency
.\build_deps.ps1 -Clean # Clean rebuild allThis configures, builds, and installs each dependency to extern/<name>/install_arm64/, which the Visual Studio project references for headers and libraries.
To add a new dependency:
git submodule add <url> extern/<name>- Optionally add CMake overrides to
$DependencyOptionsinbuild_deps.ps1 - Add the dependency's install paths to
advmesh.vcxproj
Open advmesh.slnx in Visual Studio and build using one of the four configurations:
| Configuration | Toolchain | Use Case |
|---|---|---|
| Debug | ARM64 | WSL2 cross-compile (aarch64-linux-gnu-g++) |
Development from Windows |
| Release | ARM64 | WSL2 cross-compile (aarch64-linux-gnu-g++) |
Optimized build from Windows |
| Pi Debug | ARM64 | Native on-device | Building directly on the Pi |
| Pi Release | ARM64 | Native on-device | Optimized build on the Pi |
Output binary: bin/ARM64/<Configuration>/advmesh.out
Note: For Pi Debug/Pi Release configs (built on-device), install dependencies natively:
sudo apt install libcodec2-dev
Deploy the compiled binary to one or more Raspberry Pi targets:
.\deploy.ps1 # Deploy local Debug to both Pis
.\deploy.ps1 -Rebuild # Rebuild then deploy
.\deploy.ps1 -Flavor release -Rebuild # Rebuild Release then deploy
.\deploy.ps1 -Targets advmesh1 # Deploy to a specific Pi
.\deploy.ps1 -Source remote -Flavor release # Deploy Pi Release build./advmesh.out <remote_ip> [remote_port] [local_port]
./advmesh.out --lora [options]
Example — two Pis talking to each other on port 9000:
# On Pi 1 (10.0.0.232)
./advmesh.out 10.0.0.108 9000 9000
# On Pi 2 (10.0.0.108)
./advmesh.out 10.0.0.232 9000 9000Uses the Waveshare SX1262 LoRa HAT connected via UART and GPIO.
# Default settings (915.125 MHz, 2.4kbps air rate, 22dBm TX power)
./advmesh.out --lora
# Custom channel and UART device
./advmesh.out --lora --uart /dev/serial0 --channel 72
# Custom GPIO pins (non-standard HAT wiring)
./advmesh.out --lora --m0-pin 5 --m1-pin 6 --aux-pin 13| Flag | Default | Description |
|---|---|---|
--uart <device> |
/dev/ttyAMA0 |
UART device path |
--baud <rate> |
115200 |
UART baud rate |
--channel <0-83> |
65 |
LoRa channel (freq = 850.125 + ch MHz) |
--power <0-3> |
0 |
TX power: 0=22dBm, 1=17dBm, 2=13dBm, 3=10dBm |
--air-rate <0-7> |
3 |
Air data rate (3 = 4.8 kbps) |
--sub-packet <0-3> |
2 |
Sub-packet size: 0=240B, 1=128B, 2=64B, 3=32B |
--address <0-65535> |
0 |
Module address (0 = broadcast) |
--net-id <0-255> |
0 |
Network ID |
--gpio-chip <dev> |
/dev/gpiochip0 |
GPIO character device |
--m0-pin <pin> |
22 |
M0 GPIO pin number |
--m1-pin <pin> |
27 |
M1 GPIO pin number |
--aux-pin <pin> |
17 |
AUX GPIO pin number |
The HAT plugs directly onto the Raspberry Pi 40-pin header. Key connections:
| HAT Pin | Pi GPIO | Function |
|---|---|---|
| M0 | GPIO 22 | Mode control bit 0 |
| M1 | GPIO 27 | Mode control bit 1 |
| AUX | GPIO 17 | Module busy/ready indicator |
| TXD | GPIO 14 (UART TX) | Serial data to module |
| RXD | GPIO 15 (UART RX) | Serial data from module |
| VCC | 3.3V / 5V | Power supply |
| GND | GND | Ground |
Note: On Raspberry Pi, disable the serial console and enable the UART interface:
sudo raspi-config # Interface Options → Serial Port → No console, Yes UART
Type messages and press Enter to send. Type /quit to exit.
Switch between text and voice modes at runtime:
| Command | Action |
|---|---|
/voice |
Enter voice mode — captures audio from microphone, encodes with Codec2, and transmits over UDP. Incoming voice is played through the speaker. Full-duplex. |
/transmit |
Enter transmit mode — captures audio, encodes with Codec2, batches frames, and streams over radio. Half-duplex push-to-talk. |
/receive |
Enter receive mode — listens for incoming voice stream, decodes, and plays through speaker. Half-duplex. |
/stop |
Exit any voice/transmit/receive mode, return to text. |
/text |
Same as /stop — return to text mode. |
/quit |
Exit the application. |
/voice uses Codec2 at 3200 bps (full-duplex, best for UDP). /transmit and /receive auto-select codec mode based on radio type:
| Radio | Codec Mode | Batch | Latency |
|---|---|---|---|
| UDP | 3200 bps | 1 frame (20ms) | ~20ms |
| LoRa | 700C bps | 8 frames (320ms) | ~320ms |
For voice over LoRa, use /transmit on one Pi and /receive on the other:
# Pi 1 (transmitter)
./advmesh.out --lora --baud 115200 --sub-packet 2
# Type: /transmit
# Pi 2 (receiver)
./advmesh.out --lora --baud 115200 --sub-packet 2
# Type: /receiveThe stream protocol uses three packet types:
- StreamInit — announces codec mode, batch size, sample rate
- StreamData — 3-byte header (type + sequence number) + batched Codec2 frames
- StreamEnd — signals transmission complete
Sequence number gaps are detected on the receiver and filled with silence (no retransmission). Recommended LoRa settings for voice:
| Setting | Value | Flag |
|---|---|---|
| UART baud | 115200 | --baud 115200 |
| Sub-packet | 64 bytes | --sub-packet 2 |
| Air rate | 2.4 kbps (default) | --air-rate 2 |
| TX power | 22 dBm (default) | --power 0 |
Duty cycle: ISM 915 MHz bands have regulatory duty cycle limits. Use short push-to-talk bursts rather than continuous streaming.
advmesh/
├── main.cpp # Application entry point (radio init, thread wiring)
├── cli.h/.cpp # CLI argument parsing (ParseArgs, PrintUsage)
├── app.h/.cpp # Application controller (mode management, packet routing, threads)
├── radio.h # IRadio abstract interface
├── udp_radio.h/.cpp # UdpRadio implementation (UDP sockets)
├── lora_radio.h/.cpp # LoRaRadio implementation (SX1262 LoRa HAT via UART/GPIO)
├── audio.h # IAudio abstract interface
├── pcm_audio.h/.cpp # PcmAudio implementation (direct kernel PCM ioctls)
├── pcm_types.h # Linux kernel ALSA UAPI type definitions (private to PcmAudio)
├── codec.h # ICodec abstract interface
├── codec2_codec.h/.cpp # Codec2Codec implementation (codec2 wrapper)
├── voice_stream.h # Umbrella include for voice streaming TX/RX
├── voice_stream_tx.h/.cpp # VoiceStreamTx (batched voice capture and transmit)
├── voice_stream_rx.h/.cpp # VoiceStreamRx (batched voice receive and playback)
├── packet.h # Packet framing (text, voice, stream protocol)
├── latency_stats.h # Lock-free latency tracker
├── types.h # Portable type aliases (u8, u16, i16, etc.)
├── build_deps.ps1 # Cross-compile all dependencies for ARM64
├── deploy.ps1 # Deploy binary to Raspberry Pi targets
├── test_lora.py # On-device LoRa HAT diagnostic script
├── advmesh.slnx # Visual Studio solution
├── advmesh.vcxproj # Visual Studio project
└── extern/
└── codec2/ # codec2 git submodule (low-bitrate speech codec)
A future LoRaRadio class will implement IRadio for packet transmission over LoRa. Below is the feasibility analysis for real-time voice over LoRa using the project's packet format and Codec2 codec.
| Parameter | Value |
|---|---|
| Packet header | 3 bytes (1B type + 2B payload size) |
| Sample rate | 8000 Hz |
| Codec2 frame duration | 20 ms (160 samples) |
| LoRa bandwidth | 125 kHz |
| LoRa coding rate | 4/5 |
| Mode | Bitrate | Bytes/Frame | Voice Quality |
|---|---|---|---|
| CODEC2_MODE_3200 | 3200 bps | 8 B | Best |
| CODEC2_MODE_2400 | 2400 bps | 6 B | Very Good |
| CODEC2_MODE_1600 | 1600 bps | 4 B | Good |
| CODEC2_MODE_1300 | 1300 bps | 4 B | Moderate |
| CODEC2_MODE_1200 | 1200 bps | 3 B | Moderate |
| CODEC2_MODE_700C | 700 bps | 2 B | Low (robotic) |
| SF | Nominal Bitrate | Urban Range | Rural/LOS Range |
|---|---|---|---|
| SF7 | 5469 bps | ~2 km | ~8 km |
| SF8 | 3125 bps | ~3 km | ~10 km |
| SF9 | 1758 bps | ~4 km | ~13 km |
| SF10 | 977 bps | ~5 km | ~16 km |
| SF11 | 537 bps | ~7 km | ~22 km |
| SF12 | 293 bps | ~10 km | ~30 km |
Single codec frames cannot fit within one LoRa transmission — frame batching is required. Multiple 20ms codec frames are packed into a single LoRa packet to amortize preamble and header overhead.
| Batch | Payload | LoRa SF | Range | Latency | Timing Margin |
|---|---|---|---|---|---|
| 3x | 21 B | SF7 | 2–8 km | 60 ms | 5.7% |
| 5x | 33 B | SF7 | 2–8 km | 100 ms | 28.1% |
| 10x | 63 B | SF7 | 2–8 km | 200 ms | 41.0% |
| 15x | 93 B | SF8 | 3–10 km | 300 ms | 4.3% |
| 20x | 123 B | SF8 | 3–10 km | 400 ms | 7.7% |
| 50x | 303 B | SF8 | 3–10 km | 1000 ms | 17.0% |
- Codec: CODEC2_MODE_2400 (2400 bps, 6 bytes/frame) — very good voice quality
- Batch: 5 frames per packet (33 bytes payload)
- Latency: 100 ms — imperceptible to the listener
- Range: 2–8 km (urban to rural line-of-sight)
- Timing margin: 28.1% — comfortable headroom for processing
For additional range at the cost of latency, a 15-frame batch (300ms) reaches SF8 for 3–10 km, which feels like a satellite phone delay.
| Latency | Perception |
|---|---|
| < 150 ms | Feels real-time |
| 150–300 ms | Noticeable but comfortable (phone call with slight delay) |
| 300–500 ms | Satellite phone feel — requires turn-taking discipline |
| 500 ms–1 s | Walkie-talkie feel, acceptable for push-to-talk |
Most LoRa ISM bands (868 MHz EU, 915 MHz US) enforce 1–10% duty cycle limits, making continuous voice streaming infeasible. Options include:
- Amateur radio bands — no duty cycle limit (license required)
- 2.4 GHz LoRa — no duty cycle restriction (shorter range)
- Half-duplex push-to-talk — short transmission bursts within duty cycle
| Library | Purpose | License |
|---|---|---|
| codec2 | Low-bitrate speech codec (1300 bps default) | LGPL 2.1 |