adafruit-circuitpython-rfm 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,557 @@
1
+ # SPDX-FileCopyrightText: 2024 Jerry Needell for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """
6
+
7
+ * Author(s): Jerry Needell
8
+ """
9
+
10
+ import asyncio
11
+ import random
12
+ import time
13
+
14
+ from adafruit_bus_device import spi_device
15
+
16
+ try:
17
+ from typing import Callable, Optional, Type
18
+
19
+ import busio
20
+ import digitalio
21
+ from circuitpython_typing import ReadableBuffer, WriteableBuffer
22
+
23
+ except ImportError:
24
+ pass
25
+
26
+ from micropython import const
27
+
28
+ HAS_SUPERVISOR = False
29
+
30
+ try:
31
+ import supervisor
32
+
33
+ if hasattr(supervisor, "ticks_ms"):
34
+ HAS_SUPERVISOR = True
35
+ except ImportError:
36
+ pass
37
+
38
+
39
+ __version__ = "1.0.4"
40
+ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RFM.git"
41
+
42
+
43
+ # RadioHead specific compatibility constants.
44
+ _RH_BROADCAST_ADDRESS = const(0xFF)
45
+
46
+ # The acknowledgement bit in the FLAGS
47
+ # The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved
48
+ # for application layer use.
49
+ _RH_FLAGS_ACK = const(0x80)
50
+ _RH_FLAGS_RETRY = const(0x40)
51
+
52
+
53
+ # supervisor.ticks_ms() contants
54
+ _TICKS_PERIOD = const(1 << 29)
55
+ _TICKS_MAX = const(_TICKS_PERIOD - 1)
56
+ _TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2)
57
+
58
+
59
+ def ticks_diff(ticks1: int, ticks2: int) -> int:
60
+ """Compute the signed difference between two ticks values
61
+ assuming that they are within 2**28 ticks
62
+ """
63
+ diff = (ticks1 - ticks2) & _TICKS_MAX
64
+ diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
65
+ return diff
66
+
67
+
68
+ def asyncio_to_blocking(function):
69
+ """run async function as normal blocking function"""
70
+
71
+ def blocking_function(self, *args, **kwargs):
72
+ return asyncio.run(function(self, *args, **kwargs))
73
+
74
+ return blocking_function
75
+
76
+
77
+ async def asyncio_check_timeout(flag: Callable, limit: float, timeout_poll: float) -> bool:
78
+ """test for timeout waiting for specified flag"""
79
+ timed_out = False
80
+ if HAS_SUPERVISOR:
81
+ start = supervisor.ticks_ms()
82
+ while not timed_out and not flag():
83
+ if ticks_diff(supervisor.ticks_ms(), start) >= limit * 1000:
84
+ timed_out = True
85
+ await asyncio.sleep(timeout_poll)
86
+ else:
87
+ start = time.monotonic()
88
+ while not timed_out and not flag():
89
+ if time.monotonic() - start >= limit:
90
+ timed_out = True
91
+ await asyncio.sleep(timeout_poll)
92
+
93
+ return timed_out
94
+
95
+
96
+ # pylint: disable=too-many-instance-attributes
97
+ # pylint: disable=too-many-nested-blocks
98
+ class RFMSPI:
99
+ """Base class for SPI type devices"""
100
+
101
+ class RegisterBits:
102
+ """Simplify register access"""
103
+
104
+ # Class to simplify access to the many configuration bits avaialable
105
+ # on the chip's registers. This is a subclass here instead of using
106
+ # a higher level module to increase the efficiency of memory usage
107
+ # (all of the instances of this bit class will share the same buffer
108
+ # used by the parent RFM69 class instance vs. each having their own
109
+ # buffer and taking too much memory).
110
+
111
+ # Quirk of pylint that it requires public methods for a class. This
112
+ # is a decorator class in Python and by design it has no public methods.
113
+ # Instead it uses dunder accessors like get and set below. For some
114
+ # reason pylint can't figure this out so disable the check.
115
+ # pylint: disable=too-few-public-methods
116
+
117
+ # Again pylint fails to see the true intent of this code and warns
118
+ # against private access by calling the write and read functions below.
119
+ # This is by design as this is an internally used class. Disable the
120
+ # check from pylint.
121
+ # pylint: disable=protected-access
122
+
123
+ def __init__(self, address: int, *, offset: int = 0, bits: int = 1) -> None:
124
+ assert 0 <= offset <= 7
125
+ assert 1 <= bits <= 8
126
+ assert (offset + bits) <= 8
127
+ self._address = address
128
+ self._mask = 0
129
+ for _ in range(bits):
130
+ self._mask <<= 1
131
+ self._mask |= 1
132
+ self._mask <<= offset
133
+ self._offset = offset
134
+
135
+ def __get__(self, obj: Optional["RFM"], objtype: Type["RFM"]) -> int:
136
+ reg_value = obj.read_u8(self._address)
137
+ return (reg_value & self._mask) >> self._offset
138
+
139
+ def __set__(self, obj: Optional["RFM"], val: int) -> None:
140
+ reg_value = obj.read_u8(self._address)
141
+ reg_value &= ~self._mask
142
+ reg_value |= (val & 0xFF) << self._offset
143
+ obj.write_u8(self._address, reg_value)
144
+
145
+ # pylint: disable-msg=too-many-arguments
146
+ def __init__( # noqa: PLR0913
147
+ self,
148
+ spi: busio.SPI,
149
+ cs_pin: digitalio.DigitalInOut,
150
+ baudrate: int = 5000000,
151
+ polarity: int = 0,
152
+ phase: int = 0,
153
+ ):
154
+ self.spi_device = spi_device.SPIDevice(
155
+ spi, cs_pin, baudrate=baudrate, polarity=polarity, phase=phase
156
+ )
157
+ # initialize last RSSI reading
158
+ self.last_rssi = 0.0
159
+ """The RSSI of the last received packet. Stored when the packet was received.
160
+ The instantaneous RSSI value may not be accurate once the
161
+ operating mode has been changed.
162
+ """
163
+ self.last_snr = 0.0
164
+ """The SNR of the last received packet. Stored when the packet was received.
165
+ The instantaneous SNR value may not be accurate once the
166
+ operating mode has been changed.
167
+ """
168
+ # initialize timeouts and delays delays
169
+ self.ack_wait = 0.1
170
+ """The delay time before attempting a retry after not receiving an ACK"""
171
+ self.receive_timeout = 0.5
172
+ """The amount of time to poll for a received packet.
173
+ If no packet is received, the returned packet will be None
174
+ """
175
+ self.xmit_timeout = 2.0
176
+ """The amount of time to wait for the HW to transmit the packet.
177
+ This is mainly used to prevent a hang due to a HW issue
178
+ """
179
+ self.ack_retries = 5
180
+ """The number of ACK retries before reporting a failure."""
181
+ self.ack_delay = None
182
+ """The delay time before attemting to send an ACK.
183
+ If ACKs are being missed try setting this to .1 or .2.
184
+ """
185
+ # initialize sequence number counter for reliabe datagram mode
186
+ self.sequence_number = 0
187
+ # create seen Ids list
188
+ self.seen_ids = bytearray(256)
189
+ # initialize packet header
190
+ # node address - default is broadcast
191
+ self.node = _RH_BROADCAST_ADDRESS
192
+ """The default address of this Node. (0-255).
193
+ If not 255 (0xff) then only packets address to this node will be accepted.
194
+ First byte of the RadioHead header.
195
+ """
196
+ # destination address - default is broadcast
197
+ self.destination = _RH_BROADCAST_ADDRESS
198
+ """The default destination address for packet transmissions. (0-255).
199
+ If 255 (0xff) then any receiving node should accept the packet.
200
+ Second byte of the RadioHead header.
201
+ """
202
+ # ID - contains seq count for reliable datagram mode
203
+ self.identifier = 0
204
+ """Automatically set to the sequence number when send_with_ack() used.
205
+ Third byte of the RadioHead header.
206
+ """
207
+ # flags - identifies ack/reetry packet for reliable datagram mode
208
+ self.flags = 0
209
+ """Upper 4 bits reserved for use by Reliable Datagram Mode.
210
+ Lower 4 bits may be used to pass information.
211
+ Fourth byte of the RadioHead header.
212
+ """
213
+ self.radiohead = True
214
+ """Enable RadioHead compatibility"""
215
+
216
+ self.crc_error_count = 0
217
+ self.timeout_poll = 0.001
218
+
219
+ # pylint: enable-msg=too-many-arguments
220
+
221
+ # Global buffer for SPI commands
222
+ _BUFFER = bytearray(4)
223
+
224
+ # pylint: disable=no-member
225
+ # Reconsider pylint: disable when this can be tested
226
+ def read_into(self, address: int, buf: WriteableBuffer, length: Optional[int] = None) -> None:
227
+ """Read a number of bytes from the specified address into the provided
228
+ buffer. If length is not specified (the default) the entire buffer
229
+ will be filled."""
230
+ if length is None:
231
+ length = len(buf)
232
+ with self.spi_device as device:
233
+ self._BUFFER[0] = address & 0x7F # Strip out top bit to set 0
234
+ # value (read).
235
+ device.write(self._BUFFER, end=1)
236
+ device.readinto(buf, end=length)
237
+
238
+ def read_u8(self, address: int) -> int:
239
+ """Read a single byte from the provided address and return it."""
240
+ self.read_into(address, self._BUFFER, length=1)
241
+ return self._BUFFER[0]
242
+
243
+ def write_from(self, address: int, buf: ReadableBuffer, length: Optional[int] = None) -> None:
244
+ """Write a number of bytes to the provided address and taken from the
245
+ provided buffer. If no length is specified (the default) the entire
246
+ buffer is written."""
247
+ if length is None:
248
+ length = len(buf)
249
+ with self.spi_device as device:
250
+ self._BUFFER[0] = (address | 0x80) & 0xFF # Set top bit to 1 to
251
+ # indicate a write.
252
+ device.write(self._BUFFER, end=1)
253
+ device.write(buf, end=length)
254
+
255
+ def write_u8(self, address: int, val: int) -> None:
256
+ """Write a byte register to the chip. Specify the 7-bit address and the
257
+ 8-bit value to write to that address."""
258
+ with self.spi_device as device:
259
+ self._BUFFER[0] = (address | 0x80) & 0xFF # Set top bit to 1 to indicate a write.
260
+ self._BUFFER[1] = val & 0xFF
261
+ device.write(self._BUFFER, end=2)
262
+
263
+ # pylint: disable=too-many-branches
264
+
265
+ async def asyncio_send( # noqa: PLR0912 PLR0913
266
+ self,
267
+ data: ReadableBuffer,
268
+ *,
269
+ keep_listening: bool = False,
270
+ destination: Optional[int] = None,
271
+ node: Optional[int] = None,
272
+ identifier: Optional[int] = None,
273
+ flags: Optional[int] = None,
274
+ ) -> bool:
275
+ """Send a string of data using the transmitter.
276
+ You can only send 252 bytes at a time
277
+ (limited by chip's FIFO size and appended headers).
278
+ if the propert radiohead is True then this appends a 4 byte header
279
+ to be compatible with the RadioHead library.
280
+ The header defaults to using the initialized attributes:
281
+ (destination,node,identifier,flags)
282
+ It may be temporarily overidden via the kwargs - destination,node,identifier,flags.
283
+ Values passed via kwargs do not alter the attribute settings.
284
+ The keep_listening argument should be set to True if you want to start listening
285
+ automatically after the packet is sent. The default setting is False.
286
+
287
+ Returns: True if success or False if the send timed out.
288
+ """
289
+ # Disable pylint warning to not use length as a check for zero.
290
+ # This is a puzzling warning as the below code is clearly the most
291
+ # efficient and proper way to ensure a precondition that the provided
292
+ # buffer be within an expected range of bounds. Disable this check.
293
+ # pylint: disable=len-as-condition
294
+ assert 0 < len(data) <= self.max_packet_length
295
+ # pylint: enable=len-as-condition
296
+ self.idle() # Stop receiving to clear FIFO and keep it clear.
297
+ # Combine header and data to form payload
298
+ if self.radiohead:
299
+ payload = bytearray(4)
300
+ if destination is None: # use attribute
301
+ payload[0] = self.destination
302
+ else: # use kwarg
303
+ payload[0] = destination
304
+ if node is None: # use attribute
305
+ payload[1] = self.node
306
+ else: # use kwarg
307
+ payload[1] = node
308
+ if identifier is None: # use attribute
309
+ payload[2] = self.identifier
310
+ else: # use kwarg
311
+ payload[2] = identifier
312
+ if flags is None: # use attribute
313
+ payload[3] = self.flags
314
+ else: # use kwarg
315
+ payload[3] = flags
316
+ payload = payload + data
317
+ elif destination is not None: # prepend destination for non RH packets
318
+ payload = destination.to_bytes(1, "big") + data
319
+ else:
320
+ payload = data
321
+ self.fill_fifo(payload)
322
+ # Turn on transmit mode to send out the packet.
323
+ self.transmit()
324
+ # Wait for packet_sent interrupt with explicit polling (not ideal but
325
+ # best that can be done right now without interrupts).
326
+ timed_out = await asyncio_check_timeout(
327
+ self.packet_sent, self.xmit_timeout, self.timeout_poll
328
+ )
329
+ # Listen again if necessary and return the result packet.
330
+ if keep_listening:
331
+ self.listen()
332
+ else:
333
+ # Enter idle mode to stop receiving other packets.
334
+ self.idle()
335
+ self.clear_interrupt()
336
+ return not timed_out
337
+
338
+ send = asyncio_to_blocking(asyncio_send)
339
+ """Non-asyncio wrapper to Send a string of data using the transmitter
340
+ using the same arguments and keywords as asyncio_send()
341
+ """
342
+
343
+ async def asyncio_send_with_ack(self, data: ReadableBuffer) -> bool:
344
+ """Reliable Datagram mode:
345
+ Send a packet with data and wait for an ACK response.
346
+ The packet header is automatically generated.
347
+ If enabled, the packet transmission will be retried on failure
348
+ """
349
+ if not self.radiohead:
350
+ raise RuntimeError("send_with_ack onl suppoted in RadioHead mode")
351
+ if self.ack_retries:
352
+ retries_remaining = self.ack_retries
353
+ else:
354
+ retries_remaining = 1
355
+ got_ack = False
356
+ self.sequence_number = (self.sequence_number + 1) & 0xFF
357
+ while not got_ack and retries_remaining:
358
+ self.identifier = self.sequence_number
359
+ await self.asyncio_send(data, keep_listening=True)
360
+ # Don't look for ACK from Broadcast message
361
+ if self.destination == _RH_BROADCAST_ADDRESS:
362
+ got_ack = True
363
+ else:
364
+ # wait for a packet from our destination
365
+ ack_packet = await self.asyncio_receive(timeout=self.ack_wait, with_header=True)
366
+ if ack_packet is not None:
367
+ if ack_packet[3] & _RH_FLAGS_ACK:
368
+ # check the ID
369
+ if ack_packet[2] == self.identifier:
370
+ got_ack = True
371
+ break
372
+ # pause before next retry -- random delay
373
+ if not got_ack:
374
+ # delay by random amount before next try
375
+ await asyncio.sleep(self.ack_wait + self.ack_wait * random.random())
376
+ retries_remaining = retries_remaining - 1
377
+ # set retry flag in packet header
378
+ self.flags |= _RH_FLAGS_RETRY
379
+ self.flags = 0 # clear flags
380
+ return got_ack
381
+
382
+ send_with_ack = asyncio_to_blocking(asyncio_send_with_ack)
383
+ """Non-asyncio wrapper to Send a string of data using the transmitter
384
+ using the same arguments and keywords as asyncio_send_with_ack()
385
+ """
386
+
387
+ async def asyncio_receive( # noqa: PLR0912
388
+ self,
389
+ *,
390
+ keep_listening: bool = True,
391
+ with_header: bool = False,
392
+ timeout: Optional[float] = None,
393
+ ) -> Optional[bytearray]:
394
+ """Wait to receive a packet from the receiver. If a packet is found the payload bytes
395
+ are returned, otherwise None is returned (which indicates the timeout elapsed with no
396
+ reception).
397
+ If keep_listening is True (the default) the chip will immediately enter listening mode
398
+ after reception of a packet, otherwise it will fall back to idle mode and ignore any
399
+ future reception.
400
+ Packets may have a 4-byte header for compatibility with the
401
+ RadioHead library.
402
+ The header consists of 4 bytes (To,From,ID,Flags). The default setting will strip
403
+ the header before returning the packet to the caller.
404
+ If with_header is True then the 4 byte header will be returned with the packet.
405
+ The payload then begins at packet[4].
406
+ """
407
+ if not self.radiohead and with_header:
408
+ raise RuntimeError("with_header only supported for RadioHead mode")
409
+ timed_out = False
410
+ if timeout is None:
411
+ timeout = self.receive_timeout
412
+ if timeout is not None:
413
+ # Wait for the payloadready signal. This is not ideal and will
414
+ # surely miss or overflow the FIFO when packets aren't read fast
415
+ # enough, however it's the best that can be done from Python without
416
+ # interrupt supports.
417
+ # Make sure we are listening for packets.
418
+ self.listen()
419
+ timed_out = await asyncio_check_timeout(self.payload_ready, timeout, self.timeout_poll)
420
+ # Payload ready is set, a packet is in the FIFO.
421
+ packet = None
422
+ # save last RSSI reading
423
+ self.last_rssi = self.rssi
424
+ self.last_snr = self.snr
425
+
426
+ # Enter idle mode to stop receiving other packets.
427
+ self.idle()
428
+ if not timed_out:
429
+ if self.enable_crc and self.crc_error:
430
+ self.crc_error_count += 1
431
+ else:
432
+ packet = self.read_fifo()
433
+ if (packet is not None) and self.radiohead:
434
+ if len(packet) < 5:
435
+ # reject the packet if it is too small to contain the RAdioHead Header
436
+ packet = None
437
+ if packet is not None:
438
+ if (
439
+ self.node != _RH_BROADCAST_ADDRESS # noqa: PLR1714
440
+ and packet[0] != _RH_BROADCAST_ADDRESS
441
+ and packet[0] != self.node
442
+ ):
443
+ packet = None
444
+ if not with_header and packet is not None: # skip the header if not wanted
445
+ packet = packet[4:]
446
+ # Listen again if necessary and return the result packet.
447
+ if keep_listening:
448
+ self.listen()
449
+ else:
450
+ # Enter idle mode to stop receiving other packets.
451
+ self.idle()
452
+ self.clear_interrupt()
453
+ return packet
454
+
455
+ receive = asyncio_to_blocking(asyncio_receive)
456
+ """Non-asyncio wrapper to Receive a packet
457
+ using the same arguments and keywords as asyncio_receive()
458
+ """
459
+
460
+ async def asyncio_receive_with_ack( # noqa: PLR0912
461
+ self,
462
+ *,
463
+ keep_listening: bool = True,
464
+ with_header: bool = False,
465
+ timeout: Optional[float] = None,
466
+ ) -> Optional[bytearray]:
467
+ """Wait to receive a RadioHead packet from the receiver then send an ACK packet in response.
468
+ AKA Reliable Datagram mode.
469
+ If a packet is found the payload bytes are returned, otherwise None is returned
470
+ (which indicates the timeout elapsed with no reception).
471
+ If keep_listening is True (the default) the chip will immediately enter listening mode
472
+ after receipt of a packet, otherwise it will fall back to idle mode and ignore
473
+ any incomming packets until it is called again.
474
+ All packets must have a 4-byte header for compatibility with the RadioHead library.
475
+ The header consists of 4 bytes (To,From,ID,Flags). The default setting will strip
476
+ the header before returning the packet to the caller.
477
+ If with_header is True then the 4 byte header will be returned with the packet.
478
+ The payload then begins at packet[4].
479
+ """
480
+ if not self.radiohead:
481
+ raise RuntimeError("receive_with_ack only supported for RadioHead mode")
482
+ timed_out = False
483
+ if timeout is None:
484
+ timeout = self.receive_timeout
485
+ if timeout is not None:
486
+ # Wait for the payloadready signal. This is not ideal and will
487
+ # surely miss or overflow the FIFO when packets aren't read fast
488
+ # enough, however it's the best that can be done from Python without
489
+ # interrupt supports.
490
+ # Make sure we are listening for packets.
491
+ self.listen()
492
+ timed_out = await asyncio_check_timeout(self.payload_ready, timeout, self.timeout_poll)
493
+ # Payload ready is set, a packet is in the FIFO.
494
+ packet = None
495
+ # save last RSSI reading
496
+ self.last_rssi = self.rssi
497
+ self.last_snr = self.snr
498
+
499
+ # Enter idle mode to stop receiving other packets.
500
+ self.idle()
501
+ if not timed_out:
502
+ if self.enable_crc and self.crc_error:
503
+ self.crc_error_count += 1
504
+ else:
505
+ packet = self.read_fifo()
506
+ if (packet is not None) and self.radiohead:
507
+ if len(packet) < 5:
508
+ # reject the packet if it is too small to contain the RAdioHead Header
509
+ packet = None
510
+ if packet is not None:
511
+ if (
512
+ self.node != _RH_BROADCAST_ADDRESS # noqa: PLR1714
513
+ and packet[0] != _RH_BROADCAST_ADDRESS
514
+ and packet[0] != self.node
515
+ ):
516
+ packet = None
517
+ # send ACK unless this was an ACK or a broadcast
518
+ elif ((packet[3] & _RH_FLAGS_ACK) == 0) and (
519
+ packet[0] != _RH_BROADCAST_ADDRESS
520
+ ):
521
+ # delay before sending Ack to give receiver a chance to get ready
522
+ if self.ack_delay is not None:
523
+ await asyncio.sleep(self.ack_delay)
524
+ # send ACK packet to sender (data is b'!')
525
+ await self.asyncio_send(
526
+ b"!",
527
+ destination=packet[1],
528
+ node=packet[0],
529
+ identifier=packet[2],
530
+ flags=(packet[3] | _RH_FLAGS_ACK),
531
+ )
532
+ # reject Retries if we have seen this idetifier from this source before
533
+ if (self.seen_ids[packet[1]] == packet[2]) and (
534
+ packet[3] & _RH_FLAGS_RETRY
535
+ ):
536
+ packet = None
537
+ else: # save the packet identifier for this source
538
+ self.seen_ids[packet[1]] = packet[2]
539
+ if (
540
+ packet is not None and (packet[3] & _RH_FLAGS_ACK) != 0
541
+ ): # Ignore it if it was an ACK packet
542
+ packet = None
543
+ if not with_header and packet is not None: # skip the header if not wanted
544
+ packet = packet[4:]
545
+ # Listen again if necessary and return the result packet.
546
+ if keep_listening:
547
+ self.listen()
548
+ else:
549
+ # Enter idle mode to stop receiving other packets.
550
+ self.idle()
551
+ self.clear_interrupt()
552
+ return packet
553
+
554
+ receive_with_ack = asyncio_to_blocking(asyncio_receive_with_ack)
555
+ """Non-asyncio wrapper to Receive a packet
556
+ using the same arguments and keywords as asyncio_receive_with_ack()
557
+ """