End-to-end LoRa communication system: bare-metal STM32 firmware, Go backend, TypeScript dashboard, Android range-test app, and a custom PCB design.
[Phone + LoRa Board] --LoRa 433MHz--> [Base LoRa Board] --USB Serial--> [PC / Go Server]
|
:8080 HTTP + WS
|
[Web Dashboard]
Live Leaflet map with color-coded signal markers, heatmap overlay, and real-time stats.
System info, USB devices, serial ports, and LoRa module configuration via GraphQL.
| Component | Stack | Description |
|---|---|---|
| Firmware | C99, bare-metal STM32F103, SX1262 | Transparent UART-to-LoRa serial bridge |
| Backend | Go 1.24, GraphQL, SQLite, WebSocket | Dev kit dashboard + range test server |
| Frontend | Vite + TypeScript | Hardware info dashboard |
| Range App | Android / Java | GPS + LoRa signal mapper |
| Hardware | KiCad | Dog tracker PCB (ESP32 + SX1262 + GPS) |
- 2x DX-LR30 boards: STM32F103C8T6 + SX1262 LoRa + CH340C USB-serial
- Android phone with USB OTG
- Linux PC
# Firmware toolchain
sudo apt install gcc-arm-none-eabi stm32flash python3-serial
# Backend + Frontend
# Go 1.24+, Node.js + pnpm
go install github.com/air-verse/air@latest # Go hot reload
go install github.com/99designs/gqlgen/cmd/gqlgen@latest # GraphQL codegen
# Serial port access
sudo usermod -aG dialout $USERAndroid SDK at ~/Android/Sdk for building the range test app.
make install # pnpm install + go mod tidy
make dev # Start Go + Vite with hot reload- Dashboard: http://localhost:5173
- GraphQL Playground: http://localhost:8080/playground
# One command: builds firmware, starts server
./dev-all.sh
# Or manually:
cd firmware && make
cd ../backend && CGO_ENABLED=1 go build -o bin/rangetest ./cmd/rangetest
sudo ./bin/rangetest -firmware ../firmware/build/firmware.bin -listen :8080- Plug base board into PC, run the server
- Connect second board to phone via USB OTG
- Open the LoRa Range Test app, grant location permission
- Press THIS IS HOME at your starting location
- Walk around -- phone records GPS + signal strength every 5 seconds
- Readings appear as color-coded markers on the web dashboard in real time
- Out of range: phone buffers locally. Back in range: flushes in batches
- CSV log saved to
Downloads/lora_range_YYYYMMDD_HHmmss.csvon phone
cd firmware && make # Build
make flash # Flash to /dev/ttyUSB0
# Test serial bridge between two boards:
sudo screen /dev/ttyUSB0 9600 # Terminal 1
sudo screen /dev/ttyUSB1 9600 # Terminal 2
# Type in one -> appears in the other via LoRaTo enter bootloader mode: hold KEY, press RST, release KEY.
cd range_app && ./gradlew assembleDebug
adb install app/build/outputs/apk/debug/app-debug.apk| Command | Description |
|---|---|
make dev |
Start both servers with hot reload |
make dev-rangetest |
Build firmware + range test server, run with sudo |
make build |
Production build (Go binary + Vite bundle) |
make generate |
Regenerate GraphQL code from schema |
make install |
Install all dependencies |
make clean |
Remove build artifacts |
infrastructure/ HTTP server, GraphQL, WebSocket
app/ Service layer
ports/ Interfaces (SystemReader, USBReader, SerialReader, LoRa)
adapters/ Implementations (sysfs, lsusb, serial, SQLite)
domain/ Pure data structs
No HAL, no RTOS. Direct register access. ~2.7KB flash.
| Module | Purpose |
|---|---|
main.c |
UART <-> LoRa bridge loop |
sx1262.c |
SX1262 LoRa radio driver |
uart.c |
IRQ-driven USART1 with ring buffers |
spi.c |
Hardware SPI1 @ 4.5 MHz |
gpio.c |
Pin configuration |
system.c |
72 MHz clock setup, SysTick |
startup.c |
Vector table, Reset_Handler |
| Parameter | Value |
|---|---|
| Frequency | 433.0 MHz |
| Spreading Factor | 7 |
| Bandwidth | 125 kHz |
| Coding Rate | 4/5 |
| TX Power | +22 dBm |
| Max Packet | 255 bytes |
Packets are binary structs, base64-encoded over 9600 baud serial.
| Type | Code | Payload |
|---|---|---|
| Single Reading | 0x01 |
seq(2) lat(4) lon(4) rssi(1) snr(1) time_offset(2) |
| Batch | 0x02 |
count(1) + N x reading (max 13) |
| Home Location | 0x03 |
lat(4) lon(4) |
| Version | 0x04 |
major(1) minor(1) patch(1) hash(1) |
Coordinates: int32 degrees x 1e7, big-endian. Base station prepends: [RSSI:-45 SNR:12] <base64>\r\n
| RSSI | Quality | Color |
|---|---|---|
| > -70 dBm | Excellent | Green |
| -70 to -90 | Good | Yellow |
| -90 to -110 | Weak | Orange |
| < -110 dBm | Very Weak | Red |
| Route | Description |
|---|---|
GET / |
Leaflet map dashboard |
GET /ws |
WebSocket (init, reading, home messages) |
GET /api/readings |
All readings JSON |
GET /api/session |
Current session + home location |
type Query {
systemInfo: SystemInfo!
usbDevices: [USBDevice!]!
serialPorts: [SerialPort!]!
loraModules: [LoRaModule!]!
sendATCommand(port: String!, command: String!): ATResponse!
}| Packet Size | Effective Rate |
|---|---|
| 1 byte | 29 B/s |
| 32 bytes | 225 B/s |
| 100 bytes | 258 B/s |
| 250 bytes | 273 B/s |
The hardware/ directory contains a KiCad project for a dog tracker PCB (ESP32 + SX1262 + GPS + accelerometer). Includes schematic, routed PCB, Gerber files, BOM, and a JLCPCB ordering guide.
firmware/ Bare-metal STM32 + SX1262 firmware
src/ C source files
include/ CMSIS headers, version defines
Makefile ARM GCC build
range_test_pc.py PC-side ping sender
test_throughput.py Throughput measurement
backend/ Go backend
cmd/server/ Dev kit dashboard entry point
cmd/rangetest/ Range test server entry point
domain/ Data structs
ports/ Interfaces
adapters/ Implementations (sysfs, lsusb, serial, SQLite, WebSocket)
app/ Service layer
infrastructure/ HTTP + GraphQL
frontend/ Vite + TypeScript dashboard
src/components/ Card renderers (system, USB, serial, LoRa)
src/api.ts GraphQL client
range_app/ Android range test app
app/src/main/java/ GPS, serial, map, binary protocol, CSV export
hardware/ KiCad dog tracker PCB
kicad-dog-tracker/ Schematic, PCB, Gerbers
BOM.csv Bill of materials

