voidlink: decentralized mesh-network system

25 April 2025

8 min
image of the voidlink logo

VoidLink is a decentralized mesh communication system for off-grid environments. It uses LoRa to enable low-cost and portable nodes that relay messages and location data without any external infrastructure. VoidLink offers a self-sustaining alternative to satellite phones for adventurers, geologists, and emergency responders.

capstone

This was my Capstone project! In order to complete it, I teamed up with three friends and worked together over six-months. On September 29, 2024, we submitted our proposal, followed by the presentation of our finished product on April 8, 2025. During that time, we not only developed a functional prototype but also designed and printed a fully operational PCB with integrated RF and power circuitry. Furthermore, we created a full network stack with support for reliable multi-hop messaging and paired it with an intuitive user interface.

Our project was selected as the first-place🥇 winner among hundreds of entries in the faculty of engineering capstone expo. This achievement not only validated our team's dedication but also recognized the innovation and quality of our project.

Many thanks to my wonderful group mates Stefan Praniauskas, Milena Thibault and Lukas Kotuza-Janisch for their hard work and friendship.

concept

During the idea phase, we set out to experiment with wireless communication. We wanted the project to be equally unique and challenging, so we quickly dismissed the idea of using Wi-Fi or Bluetooth.

Instead, we decided to explore the world of LoRa — a long-range, low-power wireless technology. If you've never heard of it, I don't blame you - LoRa is a relatively new technology, gaining popularity for its ability to transmit data over long distances with minimal power consumption.

LoRa is particularly well-suited for applications where traditional cellular networks are unavailable, such as in remote areas or during natural disasters. This made it an ideal choice for our project, as we wanted to create a communication system that could function in off-grid environments. We envisioned a network of nodes that could communicate with each other, forming a mesh network capable of relaying messages and location data.

Adventurers, geologists, and emergency responders would use this technology to communicate and share location data without relying on external infrastructure.

hardware

The hardware module presented some of our biggest challenges and learning opportunities since this was our first time designing a PCB. We ended up with a 4-layer PCB that integrated several key components:

  • Raspberry Pi RP2350 microcontroller
  • SX1262 LoRa transceiver
  • power management system
  • e-ink display interface
  • status LEDs

image of the PCB diagram

power management

From the outset, our product was designed to be battery-powered, but we also wanted it to be capable of charging via USB-C. This meant that we had to design a power system that could handle both power sources and switch between them seamlessly. To achieve this, we created a back-to-back PMOS transistor arrangement to manage power sources and a buck converter to regulate the wide range of input voltage.

We coupled this design with a battery charging IC for supplying constant current/voltage and an under voltage lock-out (UVLO) IC to prevent the system from operating when the voltage drops below a certain threshold. This was important for ensuring that the battery was not over-discharged, and no data was corrupted.

rf design

After prototyping using an SX1262 breakout board from Waveshare, we decided to integrate the transceiver directly onto our PCB. This required careful attention to RF design principles:

  • impedance matching: we calculated and implemented precise loads for the RF traces.
  • ground planes: we placed two uninterrupted internal ground layers and an abundance of vias.
  • shielding: we added RF shielding to minimize external interference.
  • thermal considerations: we routed the transceiver's oscillator and provided cutouts for optimal heat dissipation.

We also replaced the CMOS oscillator used in the breakout board with a passive crystal oscillator which made a considerable contribution to lowering the idle power draw of our product.

mistakes and revisions

In total, we produced two official revisions. Since it was our first time, we refrained from integrating the transceiver on the first try and instead, focused on getting the microcontroller working... And, it worked, albeit with some quirks.

The most obvious mistake was that we mirrored every single external connection port. Display, transceiver and battery ports all needed to be re-wired to fit the design. For the transceiver specifically, we ended up making a daughterboard to correctly route all 13 wires.

image of the transceiver daugtherboard

On top of that, we forgot to wire the chip enable pin on our battery charging IC. The fix was simply soldering a jumper cable between the pin and 3v3 line. Thankfully, the IC was configured correctly otherwise and the battery charging worked as expected.

image of the soldered jumper cable between charging IC and 3v3 line

There were some other inconveniences with low-voltage indicators and the reset functionality. The UVLO chip did not have the drive strength to cause a reset. We had to integrate a PMOS in the new design and use the UVLO as the gate signal instead of the driver.

For revision two, we fixed all the above, integrated the transceiver onto the board, added a probe between the microcontroller's analog-to-digital converter (ADC) and the battery to measure the remaining capacity, and finally, put some silkscreen art.

image of the second revision PCB

In general, we were pleasantly surprised both revisions worked almost perfectly. Everything wrong was fixed manually. The hardware module was a smashing success.

software

We used the C SDK provided by Raspberry Pi to develop our software for the microcontroller. For the transceiver and the e-ink display, we acquired the official drivers and wrote our hardware abstraction layer (HAL) for the microcontroller.

event loop

Since the microcontroller doesn't run an operating system with multitasking capabilities, careful design was needed to handle multiple operations. We used the first of the two cores on our microcontroller to handle the main event loop with these steps:

  • check for previously received but unprocessed messages
  • check for messages waiting to be transmitted
  • transmit a message or continue listening for incoming messages
  • check for display updates
  • check for expired acknowledgements and retransmit if necessary
  • check for serial console input

We offloaded the display updates to the second core, which was responsible for rendering the e-ink screen. This allowed us to keep the display's state machine separate from the main event loop and avoid blocking the main thread.

interrupts

For truly asynchronous operations, we used interrupts to handle events without constant polling:

  • transmission complete or error interrupts
  • preamble detection interrupts (when a potential message is detected)
  • packet received interrupts (when a complete packet is received)
  • button press interrupts
  • serial console input interrupts

queues

Since interrupts can occur at any time, we needed a way to safely pass data between the main event loop and the interrupt handlers. We used a simple queue system to store incoming and outgoing messages. The main event loop would check the queues for new messages and process them, while the interrupt handlers would add new messages to the corresponding queue.

This allowed us to handle burst traffic without dropping messages and ensure that all messages were processed in order.

display

The e-ink display was a great addition to the project. We used a Waveshare 2.13" e-ink display that offered great power efficiency and excellent visibility while outdoors compared to traditional displays.

Drawing and writing on the display was trivial with the provided driver but we had to be careful about how we refreshed the screen. E-ink displays require a specific refresh sequence to avoid ghosting and artifacts. We implemented three distinct refresh modes, each with specific use cases:

  • full refresh (2 seconds):
    • flashes the screen several times to completely reset all pixels
    • eliminates any ghost images or artifacts
    • used when changing between different menus
  • fast refresh (1 second):
    • flashes the screen once
    • used primarily when waking the display from sleep mode
    • offers quicker response than full refresh while still clearing potential artifacts
  • partial refresh (0.3 seconds):
    • updates only portions of the screen that have changed
    • used for cursor movements, status updates, and small changes
    • creates a responsive feel when navigating menus
    • limited to certain numbers of consecutive uses before requiring a full refresh

We also detected when the display was idle and put it to sleep, consuming only a few microamps of current. The display would wake up when a button was pressed or when a message was received. This was crucial for our battery-powered device.

ui

The user interface consisted of a main screen that displayed the current status of the device and three sub-menus:

  • received messages
  • nearby nodes (neighbours)
  • settings

image of different display menus

With no high-level graphics library available, we had to draw everything manually. To navigate the menus, we implemented a comprehensive state machine that handled the cursor position, button presses, and screen updates.

layers

VoidLink uses the LoRa modem to transmit data over the physical medium, which acts as layer 0 (PHY). The transceiver accepts a payload of bytes and encapsulates it in a LoRa frame.

The network code builds a custom communication protocol on top of layer 0 inspired by the OSI model and the Meshtastic protocol, built in layers:

  • layer 0: physical layer using the SX1262 LoRa transceiver
  • layer 1: basic packet transmission with efficient encoding
  • layer 2: reliable messaging with acknowledgements
  • layer 3: message forwarding to create the mesh network

protocol

Our protocol was designed to be compact yet complete. Each message was condensed into a 20-byte structure to minimize transmission time while including all necessary information.

image of the message structure

Destination and source addresses are used to identify the sender and receiver of the message. The destination address can also be used to broadcast messages to all nodes in the network.

Message identifier is a rolling number that is incremented for each message sent per node. The combination of message and source identifiers allows the network to identify duplicate messages.

There are eight different message types, each with its purpose:

  • ack: acknowledgement for layer 2
  • hello: introduction message for node discovery
  • ping: periodic message to update neighbours and synchronize time
  • pong: response to a ping message
  • text: text message for user communication
  • req: request for specific information
  • res: response to a request
  • raw: raw data message for custom applications

Timestamp is used to synchronize time between nodes and calculate the round-trip time of packets. This is important for estimating the distance between nodes.

Flags are used to indicate information about the message, like if the sender requires an acknowledgement back as a response or how many more hops the message can take. These are used by layers 2 and 3 respectively.

Finally, data is the payload of the message. Depending on the message type, this data has different meanings. For example, in a response message, the data contains the value of the requested information.

mesh network

Nodes are configured to propagate messages that are not meant for them. Each node retransmits messages it receives, allowing them to reach their destination even if they are not directly in range of the original sender.

Each message carries a hop count that is decremented by each node that forwards it. When the hop count reaches zero, the message is discarded. This prevents messages from circulating indefinitely in the network.

postamble

I could not be more proud of the work we accomplished in the last six months. We started with a simple idea and turned it into a fully functional product. We learned a lot about hardware design, RF communication, and embedded software development.

While I tried to summarize the important parts in this post, there is a lot more that we did to finish this project. If you want to read it all, here is the full report. If you want to see VoidLink in action, here is the video we made for the presentation.

And of course, if you want to hack it for yourself, both hardware and software sources are available with an MIT license on github.