Experimenting with LoRa and MeshCore
I have experimented with Software Defined Radio and GNU Radio [1] before, but not that much with LoRa. LoRa is usually hidden inside a small radio module where you configure frequency, bandwidth and spreading factor, then let the chip handle everything else.
That is convenient, but it also hides all the fun parts.
I recently started to play with MeshCore [2], which is a small mesh network built on top of LoRa. It can be used to send messages without internet, mobile network or any other infrastructure.
There is actually a community around Jönköping in Sweden that has a MeshCore network running [6] which is a fun initative.
So, gr-meshcore [5] is a GNU Radio out-of-tree module for receiving and inspecting MeshCore packets.
MeshCore
MeshCore is a decentralized mesh network. A message can be sent directly to another node or forwarded by repeaters until it reaches its destination. It supports private messages, group channels, advertisements and a few different node types such as companions, repeaters and room servers.
The important thing to understand is that MeshCore is not LoRaWAN. It uses the LoRa modulation, but has its own packet format, routing and encryption on top of it.
For this experiment I used the standard MeshCore radio settings:
- Frequency: 869.618 MHz
- Bandwidth: 62.5 kHz
- Spreading factor: 8
- Coding rate: 4/5
- Private sync word: 0x12
- Explicit header and payload CRC
These parameters must match on both sides. One wrong value is enough to make the receiver see plenty of radio energy but decode absolutely nothing.
The hardware
Transmitter
I used a Heltec ESP32 LoRa 32 V4 as my real MeshCore node. It is a small ESP32 board with an SX1262 LoRa radio and a display, which makes it very convenient for experiments like this.
I flashed it with the MeshCore companion firmware and used it to send messages on both the Public and private channels.
Receiver
For the SDR side I took out my old Ettus Research USRP2 [4]. It is far from new, but it is still a very useful radio and works well with GNU Radio and UHD.
The receiver uses this antenna [7] from Amazon.
Cheap antennas are not always tuned for the frequency printed on the package, so I checked it with my NanoVNA before using it. The measurement showed a SWR of about 2.5 around 869 MHz. Not perfect, but not too bad either.
Receiving LoRa
I did not implement the LoRa physical layer myself. The gr-lora_sdr project [3] already has GNU Radio blocks for modulation and demodulation, including synchronization, deinterleaving, error correction and CRC handling.
My receive chain looks like this:
The USRP2 samples at 1 MS/s. I tune it 150 kHz away from the actual MeshCore frequency and use a frequency translating FIR filter to move the channel back to baseband while decimating the stream to 250 ksample/s.
The offset is there to keep the wanted signal away from the DC spike in the middle of a zero-IF receiver. A DC blocker removes most of it, but moving the signal away from the center made the setup much less annoying.
After filtering and AGC, gr-lora_sdr turns the radio samples into bytes. That is where my own blocks take over.
From LoRa frames to MeshCore packets
The output from a LoRa decoder is not text. A MeshCore packet contains binary headers, path hashes, encrypted data and possibly zero bytes, so treating it as a string will sooner or later cause problems.
I wrote a tagged packet sink that uses the frame_info tags from gr-lora_sdr to collect one complete frame at a time. Frames with a bad LoRa CRC are dropped and valid frames are passed on as binary-safe PDUs.
The next block parses the MeshCore packet format and exposes:
- Route type and transport codes
- Payload type and protocol version
- The hashes for the path through the mesh
- Advertisements with node name, public key, position and node type
- The original payload and packet as hexadecimal data
Most MeshCore traffic is encrypted, as it should be. Without the key, my receiver can show the packet structure but not the content.
The standard Public channel is a special case as its private key is, well, public. I added support for verifying its MAC and decrypting group text and data messages. The plaintext is only exposed if the MAC is valid.
After sending a message from the Heltec board I got this summary in the terminal:
MeshCore {"channel": "Public", "hops": 1, "payload_length": 35,
"route": "flood", "text": "Sa7mfo: Heeeeeej", "type": "group_text",
"version": 0}
Heeeeeej indeed!
Transmitting
Once receiving worked, the obvious next step was to try the other direction.
I made a second flowgraph with both RX and TX paths. The transmit side listens for a binary packet on a local UDP port, feeds it into the LoRa encoder and sends the resulting samples to the USRP2.
There is also a small meshcore-send-hex utility that validates the basic packet layout before sending it to GNU Radio. For example, this sends a structurally valid direct packet with a three-byte raw payload:
And there it is in the waterfall, a small LoRa transmission around 869.618 MHz:
The transmitter is intentionally quite limited. It accepts an already prepared MeshCore packet, but it does not create identities, encrypt messages, sign advertisements, find routes or perform carrier sensing.
In other words, gr-meshcore is a useful protocol experiment and packet inspection tool, but it is not a complete MeshCore node.
Also, transmitting with an SDR comes with responsibilities. Use low gain, enough isolation between transmitter and receiver and make sure the frequency, power and duty cycle are allowed where you live.
The result
I can now use the USRP2 (or any SDR) to receive real messages from a small LoRa board, follow the packets through the complete radio chain and inspect the MeshCore protocol in GNU Radio.
The source code, installation instructions and ready-to-run USRP2 flowgraphs are available on GitHub [5].