loraline
A messenger for a few friends, running on radio you own, over a channel with no company in the middle of it.
── everyone ───────────────────────── hank │╱╱╲ loraline. Signed in as hank (dd764c). Online │* 868 MHz (ch 18) SF7 BW125 8 dBm listening to something │* Listening on 868 MHz. Everyone must match. dd764c │* Idle costs 10s of radio time an hour, of 36. │* dave has just signed in. ▸ everyone │21:04 dave: antenna is in the attic now 2 online │21:04 hank: that's the whole difference ✓✓ 2/2 ● dave (1) │21:06 mira: still nothing from the cellar Online │21:07 hank: try a window. height beats power, brb dinner │ every single time ✓· 1/2 ● mira │ Online │ │ signal |||| │ .:=+#+=:-==+ │ heard 12s ago │mira is writing a message... │[everyone] ▌ 868 MHz 22s of radio time left this hour 1 waiting encrypted for the group
Three people, one radio channel, no infrastructure between them.
What it is
Two or more people plug a LoRa module into a USB port and talk to each other. There is no server, no account, no phone app and no internet connection. The radio in your house reaches the radio in theirs, and that is the entire system.
It borrows its shape from MSN Messenger: a contact list, presence, a personal message, private windows alongside the group. Not as decoration. Those things existed because the medium was thin, and this medium is thin again. When the heartbeats stop, your friend really has gone: no server is guessing on their behalf.
What it doesn't borrow is the nudge. Everything listed above reports something true about the other person. A nudge reports nothing; it only demands attention, and it can't be declined. It made sense once the online dot had stopped meaning anything and you needed a way to check whether someone was really there. Heartbeats answer that honestly, so there is nothing left for it to do.
Range is a few kilometres in a town and rather more with a window and some height. It is a tool for people who are already near each other, which is the same constraint that made small networks feel like they belonged to you.
What you need
- Two or more Waveshare USB-TO-LoRa-HF modules, or the near-identical EBYTE equivalents. They appear as a serial port and need no firmware flashing.
- An antenna each. This matters more than any setting you can change.
- Python 3.10 or newer. The interface uses the standard library's
curses; there is no UI framework to install.
The only dependencies are pyserial, and pynacl if
you want encryption.
Getting it running
Roughly half an hour from unpacking the modules to talking, and none of it needs prior experience. The walkthrough covers Windows, macOS and Linux, tells you how to test the whole thing on one computer before anyone has to walk anywhere, and lists what to do when a step doesn't work.
Set up loraline Step by step, from an empty computer to a working conversation.Radio and internet, together
A node can hold more than one bearer at a time, and one holding both becomes a bridge. That lets somebody far away join over the internet while the people near you stay entirely on radio.
# you: radio to the neighbours, socket to everyone else
python -m loraline chat --port /dev/ttyUSB0 --band eu868 \
--nick hank --tcp-listen 4242
# next door: radio only, no internet at all
python -m loraline chat --port /dev/ttyUSB0 --band eu868 --nick dave
# far away: no radio at all, so no band to set either
python -m loraline chat --nick will \
--tcp-connect your-host.example.com:4242
Relaying happens on sealed envelopes, before anything is decrypted, so the bridge passes a private message between two of its peers along without being able to read it.
Someone joining this way needs no radio and nothing configured. There are instructions for both sides: hosting, and joining from elsewhere.
This is how networks worked before there was one big one. FidoNet was thousands of machines that each called a neighbour nightly, and mail crossed continents by hopping between local links. Nobody had a global connection. They had a link to someone who had a link.
Who can read what
Every module on the channel receives every packet. A private chat built by having clients politely ignore what isn't addressed to them would not be private at all. Anyone with a modified client, or just a serial terminal, would read it.
So each install generates a keypair, kept in
~/.loraline/identity, and your address is derived from its public
half. Addresses are exchanged automatically the first time two people hear
each other.
- Group messages
- Encrypted with a passphrase you all share. Everyone who has it can read them; nobody else can.
- Private messages
- Encrypted between exactly two identities. The third person in the group cannot read them, even though their radio received every byte.
Nothing about routing travels in the open, so somebody listening nearby
cannot tell who a packet is even addressed to. Run /whois dave
and compare the fingerprint out loud to be sure of who you're talking to; key
exchange over the air is safe from a listener but not from an active
impersonator.
What it can't do
Nothing here is hidden in a footnote, because knowing the limits is most of knowing whether this is for you.
Range is decided by obstruction, not distance
| Situation | Realistic |
|---|---|
| Both modules indoors, dense city | 200 m – 1 km |
| Suburban, each near an upstairs window | 1 – 3 km |
| One end high: attic, rooftop, top floor | 3 – 8 km |
| Clear line of sight, both elevated | 15 – 40 km |
Modern low-emissivity window coatings are metal oxide and cost you 15 to 25 decibels on their own. Getting the antenna two metres higher will do more than any parameter in the config.
Airtime limits the group, not the code
Everyone announces themselves periodically, and if two people transmit at the same moment both packets are lost. Five or six people works where there is no duty cycle cap. Under the European 1% limit, three is comfortable and four is optimistic. It is a small-group tool by physics rather than by preference.
Presence is the only thing that transmits when nobody is talking, so it sets the floor on what an idle client costs. The client works that out against your duty cycle at startup and tells you the figure. If a heartbeat every minute would not fit, it slows presence down and says by how much.
It is slow, on purpose and otherwise
A forty-character message spends about 120 milliseconds on the air at spreading factor 7, or roughly 740 at spreading factor 10, and about double either figure once encrypted. The client shows the budget you have left in the hour and holds messages back rather than pretending they were sent.
That is why the European preset uses the faster, shorter-range setting by default: under a 1% cap, a high spreading factor spends the entire hourly allowance on saying hello.
Source
Around two thousand lines of Python, in five parts: the wire protocol, the session state, the transports, a driver, and a terminal interface. The session logic is pure, taking frames and clock ticks in and giving events out, so the test suite runs three conversations over a simulated lossy radio and a real socket pair without any hardware attached.
git clone https://github.com/intervalplace/loraline
cd loraline
python tests.py # three sessions, simulated radio
python tests_bridge.py # radio and internet bridged