|
| 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