BuddyFinder: Peer-to-Peer LoRa Direction-Finding Compass

Jan. 2026 - Jun. 2026

A pair of handheld devices that point at each other over LoRa in crowded, off-grid environments like music festivals. Custom dual-core FreeRTOS firmware on ESP32-S3, no phone or cell service required.

Embedded Systems ESP32 FreeRTOS LoRa Senior Project

Project Overview

Senior project which involved building a pair of handheld devices that point at each other in crowded, off-grid environments like outdoor music festivals. Each unit acquires its own position from a GNSS receiver, broadcasts it over 915 MHz LoRa every 3 seconds, and renders a live compass on a small TFT display showing distance and bearing to the paired buddy. An external IMU provides absolute heading so the arrow tracks the buddy in the world frame regardless of how the device is held. No Wi-Fi, cell service, or internet required.

Why Not Meshtastic

We considered Meshtastic and ultimately rejected it. Meshtastic is monolithic firmware, meaning it controls the display, the radio stack, and the UI, and only allows for custom firmware through a module API. Building our own compass and navigation logic on top of it would have meant incorporating our firmware with an large, unfamiliar codebase. We also determined that mesh routing wasn't necessary for two-device prototype. Going custom let us use FreeRTOS the way we wanted to and engage seriously with real-time design questions like ISR vs. task work, lock-free data sharing, and core pinning. Multi-peer support is left as future work, layered on top of a design we fully understand.

Hardware

Platform
Heltec Wireless Tracker V1.1: ESP32-S3FN8 dual-core MCU, SX1262 LoRa transceiver, UC6580 GNSS, and ST7735 TFT display integrated on one board
Heading sensor
Adafruit BNO055 9-DOF IMU on a dedicated I2C bus, outputs absolute orientation via onboard sensor fusion so the ESP32 does no fusion math itself
Display
ST7735 TFT (160 x 80) on its own SPI bus, so LoRa and display SPI never contend at the hardware level

System Diagrams

Firmware Architecture

Firmware is C++ on FreeRTOS, split across the ESP32-S3's two cores. A Radio task is pinned to Core 0 and fully owns the SX1262: an interrupt sets a flag, the task polls the flag every 20 ms, performs the SPI read in task context, parses the payload, and writes the buddy's GPS fix into a queue. It also wakes every 3 seconds to broadcast the local fix. Core 1 runs the Navigation task at high priority on a 50 ms cadence (20 Hz) using vTaskDelayUntil. Core 1 also runs another task at idle priority which drains the GPS UART byte-by-byte into TinyGPSPlus. The Radio task uses RadioLib's blocking transmit() call, but pinning it to its own core means the blocking SPI wait never stalls navigation or display work on Core 1.

Inter-Task Communication

Two size-1 FreeRTOS queues (ownFixQueue and peerFixQueue) connect the tasks, used with xQueueOverwrite and xQueuePeek. This gives a lock-free latest-value pattern, meaning writers overwrite old fixes and peekers read without consuming. There are no mutexes and no risk of a slow consumer holding up the rest of the system, which is exactly what we want for real-time direction-finding. If a new GPS fix arrives mid-computation, the next 20 Hz tick will pick it up. The Navigation task also enforces a 10-second peer-loss timeout, flipping the display to a 'no peer' state rather than continuing to point at a stale position.

Navigation Math and Display

Each Navigation tick peeks both GPS fix queues, reads the BNO055 for the current absolute heading, and computes great-circle distance and initial bearing between the two coordinate pairs using haversine and bearing formulas. Subtracting device heading from world-frame bearing gives the arrow direction in the device's local frame, which is what gets rendered. Using a fused-orientation IMU rather than a bare magnetometer removed the need for additional calibration and computation in firmware.

Technical Scope

Testing and Validation

What I Learned

I enjoyed thinking about the high-level architecture and breaking the project down into its sub-pieces. I liked thinking about what data needed to be shared, how long it should persist, and how to design something that was efficient and easy to understand. Testing the individual components and then integrating them was satisfying. I also learned a lot about the challenges of building on top of a large open-source project like Meshtastic, which is what pushed us toward a custom firmware stack in the first place. This was also my first major FreeRTOS project. I'd worked with it on smaller class projects, but BuddyFinder was the first time I actually got to apply what I'd learned: task design, queue-based communication, core pinning, and the trade-offs between blocking and non-blocking APIs.

What I'd Do Next

The clearest next step is multi-peer: a hardware button to cycle through a small set of buddies, eventually layered into a real routing protocol. After that, power management is the obvious gap, since the current firmware makes no effort to sleep between activity, and switching from RadioLib's blocking transmit() to interrupt-driven startTransmit() would close the small receive outage that happens during each TX. Beyond firmware, a proper enclosure with antenna routing and an integrated battery is the next physical step; the current prototypes are bare boards on breadboards.