forked from micropython/micropython-lib
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsender_async.py
More file actions
205 lines (162 loc) · 7.46 KB
/
sender_async.py
File metadata and controls
205 lines (162 loc) · 7.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# MicroPython lora reliable_delivery example - asynchronous sender program
# MIT license; Copyright (c) 2023 Angus Gratton
import machine
from machine import SPI, Pin
import random
import struct
import time
import asyncio
from lora_rd_settings import RECEIVER_ID, ACK_LENGTH, ACK_DELAY_MS, lora_cfg
SLEEP_BETWEEN_MS = 5000 # Main loop should sleep this long between sending data to the receiver
MAX_RETRIES = 4 # Retry each message this often if no ACK is received
# Initial retry is after this long. Increases by 1.25x each subsequent retry.
BASE_RETRY_TIMEOUT_MS = 1000
# Add random jitter to each retry period, up to this long. Useful to prevent two
# devices ending up in sync.
RETRY_JITTER_MS = 1500
# If reported RSSI value is lower than this, increase
# output power 1dBm
RSSI_WEAK_THRESH = -110
# If reported RSSI value is higher than this, decrease
# output power 1dBm
RSSI_STRONG_THRESH = -70
# IMPORTANT: Set this to the maximum output power in dBm that is permitted in
# your regulatory environment.
OUTPUT_MAX_DBM = 15
OUTPUT_MIN_DBM = -20
def get_async_modem():
# from lora import AsyncSX1276
# return AsyncSX1276(
# spi=SPI(1, baudrate=2000_000, polarity=0, phase=0,
# miso=Pin(19), mosi=Pin(27), sck=Pin(5)),
# cs=Pin(18),
# dio0=Pin(26),
# dio1=Pin(35),
# reset=Pin(14),
# lora_cfg=lora_cfg,
# )
raise NotImplementedError("Replace this function with one that returns a lora modem instance")
def main():
modem = get_async_modem()
asyncio.run(sender_task(modem))
async def sender_task(modem):
# Unique ID of this sender, 16-bit number. This method of generating an ID is pretty crummy,
# if using this in a real application then probably better to store these in the filesystem or
# something like that
DEVICE_ID = sum(b for b in machine.unique_id()) & 0xFFFF
sender = AsyncSender(modem, DEVICE_ID)
while True:
sensor_data = await get_sensor_data()
await sender.send(sensor_data)
# Sleep until the next time we should read the sensor data and send it to
# the receiver. awaiting here means other tasks will run.
modem.sleep()
await asyncio.sleep_ms(SLEEP_BETWEEN_MS)
async def get_sensor_data():
# Return a bytes object with the latest sensor data to send to the receiver.
#
# As this is just an example, we send a dummy payload which is just a string
# containing our ticks_ms() timestamp.
#
# In a real application the sensor data should usually be binary data and
# not a string, to save transmission size.
return f"Hello, ticks_ms={time.ticks_ms()}".encode()
class AsyncSender:
def __init__(self, modem, device_id):
self.modem = modem
self.device_id = device_id
self.counter = 0
self.output_power = lora_cfg["output_power"] # start with common settings power level
self.rx_ack = None # reuse the ack message object when we can
print(f"Sender initialized with ID {device_id:#x}")
random.seed(device_id)
self.adjust_output_power(0) # set the initial value within MIN/MAX
modem.calibrate()
async def send(self, sensor_data, adjust_output_power=True):
# Send a packet of sensor data to the receiver reliably.
#
# Returns True if data was successfully sent and ACKed, False otherwise.
#
# If adjust_output_power==True then increase or decrease output power
# according to the RSSI reported in the ACK packet.
self.counter = (self.counter + 1) & 0xFF
# Prepare the simple payload with header and checksum
# See README for a summary of the simple data message format
payload = bytearray(len(sensor_data) + 5)
struct.pack_into("<HBB", payload, 0, self.device_id, self.counter, len(sensor_data))
payload[4:-1] = sensor_data
payload[-1] = sum(b for b in payload) & 0xFF
# Calculate the time on air (in milliseconds) for an ACK packet
ack_packet_ms = self.modem.get_time_on_air_us(ACK_LENGTH) // 1000 + 1
timeout = BASE_RETRY_TIMEOUT_MS
print(f"Sending {len(payload)} bytes")
# Send the payload, until we receive an acknowledgement or run out of retries
for _ in range(MAX_RETRIES):
sent_at = await self.modem.send(payload)
# We expect the receiver of a valid message to start sending the ACK
# approximately ACK_DELAY_MS after receiving the message (to allow
# the sender time to reconfigure the modem.)
#
# We start receiving as soon as we can, but allow up to
# ACK_DELAY_MS*2 of total timing leeway - plus the time on air for
# the packet itself
maybe_ack = await self.modem.recv(
ack_packet_ms + ACK_DELAY_MS * 2, rx_packet=self.rx_ack
)
# Check if the packet we received is a valid ACK
rssi = self._ack_is_valid(maybe_ack, payload[-1])
if rssi is not None: # ACK is valid
self.rx_ack == maybe_ack
delta = time.ticks_diff(maybe_ack.ticks_ms, sent_at)
print(
f"ACKed with RSSI {rssi}, {delta}ms after sent "
+ f"(skew {delta - ACK_DELAY_MS - ack_packet_ms}ms)"
)
if adjust_output_power:
if rssi > RSSI_STRONG_THRESH:
self.adjust_output_power(-1)
elif rssi < RSSI_WEAK_THRESH:
self.adjust_output_power(1)
return True
# Otherwise, prepare to sleep briefly and then retry
next_try_at = time.ticks_add(sent_at, timeout)
sleep_time = time.ticks_diff(next_try_at, time.ticks_ms()) + random.randrange(
RETRY_JITTER_MS
)
if sleep_time > 0:
self.modem.sleep()
await asyncio.sleep_ms(sleep_time)
# add 25% timeout for next iteration
timeout = (timeout * 5) // 4
print(f"Failed, no ACK after {MAX_RETRIES} retries.")
if adjust_output_power:
self.adjust_output_power(2)
self.modem.calibrate_image() # try and improve the RX sensitivity for next time
return False
def _ack_is_valid(self, maybe_ack, csum):
# Private function to verify if the RxPacket held in 'maybe_ack' is a valid ACK for the
# current device_id and counter value, and provided csum value.
#
# If it is, returns the reported RSSI value from the packet.
# If not, returns None
if (not maybe_ack) or len(maybe_ack) != ACK_LENGTH:
return None
base_id, ack_id, ack_counter, ack_csum, rssi = struct.unpack("<HHBBb", maybe_ack)
if (
base_id != RECEIVER_ID
or ack_id != self.device_id
or ack_counter != self.counter
or ack_csum != csum
):
return None
return rssi
def adjust_output_power(self, delta_dbm):
# Adjust the modem output power by +/-delta_dbm, max of OUTPUT_MAX_DBM
#
# (note: the radio may also apply its own power limit internally.)
new = max(min(self.output_power + delta_dbm, OUTPUT_MAX_DBM), OUTPUT_MIN_DBM)
self.output_power = new
print(f"New output_power {new}/{OUTPUT_MAX_DBM} (delta {delta_dbm})")
self.modem.configure({"output_power": self.output_power})
if __name__ == "__main__":
main()