Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 1957f24

Browse files
projectgusdpgeorge
authored andcommitted
lora: Add lora modem drivers for SX127x and SX126x.
Includes: - component oriented driver, to only install the parts that are needed - synchronous operation - async wrapper class for asynchronous operation - two examples with async & synchronous versions - documentation This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton <angus@redyak.com.au>
1 parent 7128d42 commit 1957f24

File tree

22 files changed

+4955
-0
lines changed

22 files changed

+4955
-0
lines changed

‎micropython/lora/README.md‎

Lines changed: 1156 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# LoRa Reliable Delivery Example
2+
3+
This example shows a basic custom protocol for reliable one way communication
4+
from low-power remote devices to a central base device:
5+
6+
- A single "receiver" device, running on mains power, listens continuously for
7+
messages from one or more "sender" devices. Messages are payloads inside LoRa packets,
8+
with some additional framing and address in the LoRa packet payload.
9+
- "Sender" devices are remote sensor nodes, possibly battery powered. These wake
10+
up periodically, read some data from a sensor, and send it in a message to the receiver.
11+
- Messages are transmitted "reliably" with some custom header information,
12+
meaning the receiver will acknowledge it received each message and the sender
13+
will retry sending if it doesn't receive the acknowledgement.
14+
15+
## Source Files
16+
17+
* `lora_rd_settings.py` contains some common settings that are imported by
18+
sender and receiver. These settings will need to be modified for the correct
19+
frequency and other settings, before running the examples.
20+
* `receiver.py` and `receiver_async.py` contain a synchronous (low-level API)
21+
and asynchronous (iterator API) implementation of the same receiver program,
22+
respectively. These two programs should work the same, they are intended show
23+
different ways the driver can be used.
24+
* `sender.py` and `sender_async.py` contain a synchronous (simple API) and
25+
asynchronous (async API) implementation of the same sender program,
26+
respectively. Because the standard async API resembles the Simple API, these
27+
implementations are *very* similar. The two programs should work the same,
28+
they are intended to show different ways the driver can be used.
29+
30+
## Running the examples
31+
32+
One way to run this example interactively:
33+
34+
1. Install or "freeze in" the necessary lora modem driver package (`lora-sx127x`
35+
or `lora-sx126x`) and optionally the `lora-async` package if using the async
36+
examples (see main lora `README.md` in the above directory for details).
37+
2. Edit the `lora_rd_settings.py` file to set the frequency and other protocol
38+
settings for your region and hardware (see main lora `README.md`).
39+
3. Edit the program you plan to run and fill in the `get_modem()` function with
40+
the correct modem type, pin assignments, etc. for your board (see top-level
41+
README). Note the `get_modem()` function should use the existing `lora_cfg`
42+
variable, which holds the settings imported from `lora_rd_settings.py`.
43+
4. Change to this directory in a terminal.
44+
5. Run `mpremote mount . exec receiver.py` on one board and `mpremote mount
45+
. exec sender.py` on another (or swap in `receiver_async.py` and/or
46+
`sender_async.py` as desired).
47+
48+
Consult the [mpremote
49+
documentation](https://docs.micropython.org/en/latest/reference/mpremote.html)
50+
for an explanation of these commands and the options needed to run two copies of
51+
`mpremote` on different serial ports at the same time.
52+
53+
## Automatic Performance Tuning
54+
55+
- When sending an ACK, the receiver includes the RSSI of the received
56+
packet. Senders will automatically modify their output_power to minimize the
57+
power consumption required to reach the receiver. Similarly, if no ACK is
58+
received then they will increase their output power and also re-run Image
59+
calibration in order to maximize RX performance.
60+
61+
## Message payloads
62+
63+
Messages are LoRa packets, set up as follows:
64+
65+
LoRA implicit header mode, CRCs enabled.
66+
67+
* Each remote device has a unique sixteen-bit ID (range 00x0000 to 0xFFFE). ID
68+
0xFFFF is reserved for the single receiver device.
69+
* An eight-bit message counter is used to identify duplicate messages
70+
71+
* Data message format is:
72+
- Sender ID (two bytes, little endian)
73+
- Counter byte (incremented on each new message, not incremented on retry).
74+
- Message length (1 byte)
75+
- Message (variable length)
76+
- Checksum byte (sum of all proceeding bytes in message, modulo 256). The LoRa
77+
packet has its own 16-bit CRC, this is included as an additional way to
78+
disambiguate other LoRa packets that might appear the same.
79+
80+
* After receiving a valid data message, the receiver device should send
81+
an acknowledgement message 25ms after the modem receive completed.
82+
83+
Acknowledgement message format:
84+
- 0xFFFF (receiver station ID as two bytes)
85+
- Sender's Device ID from received message (two bytes, little endian)
86+
- Counter byte from received message
87+
- Checksum byte from received message
88+
- RSSI value as received by radio (one signed byte)
89+
90+
* If the remote device doesn't receive a packet with the acknowledgement
91+
message, it retries up to a configurable number of times (default 4) with a
92+
basic exponential backoff formula.
93+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# MicroPython lora reliable_delivery example - common protocol settings
2+
# MIT license; Copyright (c) 2023 Angus Gratton
3+
4+
#
5+
######
6+
# To be able to be able to communicate, most of these settings need to match on both radios.
7+
# Consult the example README for more information about how to use the example.
8+
######
9+
10+
# LoRa protocol configuration
11+
#
12+
# Currently configured for relatively slow & low bandwidth settings, which
13+
# gives more link budget and possible range.
14+
#
15+
# These settings should match on receiver.
16+
#
17+
# Check the README and local regulations to know what configuration settings
18+
# are available.
19+
lora_cfg = {
20+
"freq_khz": 916000,
21+
"sf": 10,
22+
"bw": "62.5", # kHz
23+
"coding_rate": 8,
24+
"preamble_len": 12,
25+
"output_power": 10, # dBm
26+
}
27+
28+
# Single receiver has a fixed 16-bit ID value (senders each have a unique value).
29+
RECEIVER_ID = 0xFFFF
30+
31+
# Length of an ACK message in bytes.
32+
ACK_LENGTH = 7
33+
34+
# Send the ACK this many milliseconds after receiving a valid message
35+
#
36+
# This can be quite a bit lower (25ms or so) if wakeup times are short
37+
# and _DEBUG is turned off on the modems (logging to UART delays everything).
38+
ACK_DELAY_MS = 100
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# MicroPython lora reliable_delivery example - synchronous receiver program
2+
# MIT license; Copyright (c) 2023 Angus Gratton
3+
import struct
4+
import time
5+
import machine
6+
from machine import SPI, Pin
7+
from micropython import const
8+
from lora import RxPacket
9+
10+
from lora_rd_settings import RECEIVER_ID, ACK_LENGTH, ACK_DELAY_MS, lora_cfg
11+
12+
# Change _DEBUG to const(True) to get some additional debugging output
13+
# about timing, RSSI, etc.
14+
#
15+
# For a lot more debugging detail, go to the modem driver and set _DEBUG there to const(True)
16+
_DEBUG = const(False)
17+
18+
# Keep track of the last counter value we got from each known sender
19+
# this allows us to tell if packets are being lost
20+
last_counters = {}
21+
22+
23+
def get_modem():
24+
# from lora import SX1276
25+
# return SX1276(
26+
# spi=SPI(1, baudrate=2000_000, polarity=0, phase=0,
27+
# miso=Pin(19), mosi=Pin(27), sck=Pin(5)),
28+
# cs=Pin(18),
29+
# dio0=Pin(26),
30+
# dio1=Pin(35),
31+
# reset=Pin(14),
32+
# lora_cfg=lora_cfg,
33+
# )
34+
raise NotImplementedError("Replace this function with one that returns a lora modem instance")
35+
36+
37+
def main():
38+
print("Initializing...")
39+
modem = get_modem()
40+
41+
print("Main loop started")
42+
receiver = Receiver(modem)
43+
44+
while True:
45+
# With wait=True, this function blocks until something is received and always
46+
# returns non-None
47+
sender_id, data = receiver.recv(wait=True)
48+
49+
# Do something with the data!
50+
print(f"Received {data} from {sender_id:#x}")
51+
52+
53+
class Receiver:
54+
def __init__(self, modem):
55+
self.modem = modem
56+
self.last_counters = {} # Track the last counter value we got from each sender ID
57+
self.rx_packet = None # Reuse RxPacket object when possible, save allocation
58+
self.ack_buffer = bytearray(ACK_LENGTH) # reuse the same buffer for ACK packets
59+
self.skipped_packets = 0 # Counter of skipped packets
60+
61+
modem.calibrate()
62+
63+
# Start receiving immediately. We expect the modem to receive continuously
64+
self.will_irq = modem.start_recv(continuous=True)
65+
print("Modem initialized and started receive...")
66+
67+
def recv(self, wait=True):
68+
# Receive a packet from the sender, including sending an ACK.
69+
#
70+
# Returns a tuple of the 16-bit sender id and the sensor data payload.
71+
#
72+
# This function should be called very frequently from the main loop (at
73+
# least every ACK_DELAY_MS milliseconds), to avoid not sending ACKs in time.
74+
#
75+
# If 'wait' argument is True (default), the function blocks indefinitely
76+
# until a packet is received. If False then it will return None
77+
# if no packet is available.
78+
#
79+
# Note that because we called start_recv(continuous=True), the modem
80+
# will keep receiving on its own - even if when we call send() to
81+
# send an ACK.
82+
while True:
83+
rx = self.modem.poll_recv(rx_packet=self.rx_packet)
84+
85+
if isinstance(rx, RxPacket): # value will be True or an RxPacket instance
86+
decoded = self._handle_rx(rx)
87+
if decoded:
88+
return decoded # valid LoRa packet and valid for this application
89+
90+
if not wait:
91+
return None
92+
93+
# Otherwise, wait for an IRQ (or have a short sleep) and then poll recv again
94+
# (receiver is not a low power node, so don't bother with sleep modes.)
95+
if self.will_irq:
96+
while not self.modem.irq_triggered():
97+
machine.idle()
98+
else:
99+
time.sleep_ms(1)
100+
101+
def _handle_rx(self, rx):
102+
# Internal function to handle a received packet and either send an ACK
103+
# and return the sender and the payload, or return None if packet
104+
# payload is invalid or a duplicate.
105+
106+
if len(rx) < 5: # 4 byte header plus 1 byte checksum
107+
print("Invalid packet length")
108+
return None
109+
110+
sender_id, counter, data_len = struct.unpack("<HBB", rx)
111+
csum = rx[-1]
112+
113+
if len(rx) != data_len + 5:
114+
print("Invalid length in payload header")
115+
return None
116+
117+
calc_csum = sum(b for b in rx[:-1]) & 0xFF
118+
if csum != calc_csum:
119+
print(f"Invalid checksum. calc={calc_csum:#x} received={csum:#x}")
120+
return None
121+
122+
# Packet is valid!
123+
124+
if _DEBUG:
125+
print(f"RX {data_len} byte message RSSI {rx.rssi} at timestamp {rx.ticks_ms}")
126+
127+
# Send the ACK
128+
struct.pack_into(
129+
"<HHBBb", self.ack_buffer, 0, RECEIVER_ID, sender_id, counter, csum, rx.rssi
130+
)
131+
132+
# Time send to start as close to ACK_DELAY_MS after message was received as possible
133+
tx_at_ms = time.ticks_add(rx.ticks_ms, ACK_DELAY_MS)
134+
tx_done = self.modem.send(self.ack_buffer, tx_at_ms=tx_at_ms)
135+
136+
if _DEBUG:
137+
tx_time = time.ticks_diff(tx_done, tx_at_ms)
138+
expected = self.modem.get_time_on_air_us(ACK_LENGTH) / 1000
139+
print(f"ACK TX {tx_at_ms}ms -> {tx_done}ms took {tx_time}ms expected {expected}")
140+
141+
# Check if the data we received is fresh or stale
142+
if sender_id not in self.last_counters:
143+
print(f"New device id {sender_id:#x}")
144+
elif self.last_counters[sender_id] == counter:
145+
print(f"Duplicate packet received from {sender_id:#x}")
146+
return None
147+
elif counter != 1:
148+
# If the counter from this sender has gone up by more than 1 since
149+
# last time we got a packet, we know there is some packet loss.
150+
#
151+
# (ignore the case where the new counter is 1, as this probably
152+
# means a reset.)
153+
delta = (counter - 1 - self.last_counters[sender_id]) & 0xFF
154+
if delta:
155+
print(f"Skipped/lost {delta} packets from {sender_id:#x}")
156+
self.skipped_packets += delta
157+
158+
self.last_counters[sender_id] = counter
159+
return sender_id, rx[4:-1]
160+
161+
162+
if __name__ == "__main__":
163+
main()

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /