From 7b0dd16564716fe519a50fcd63e0db3a49f1f97d Mon Sep 17 00:00:00 2001 From: Matthew Tai Date: Sat, 13 May 2023 22:38:53 -0700 Subject: [PATCH] Refactor to mirror the code-structure of the original RadioHead Arduino library * Adds configurable bitrates with GFSK modulation * Adds asyncio support so recv/send are no longer blocking * Removes TX/RX register polling in favor of pin IRQ and countio * Cleanup RPi Interrupt example, addresses #36 --- README.rst | 13 +- adafruit_rfm69.py | 1544 +++++++++++++++++------------- docs/conf.py | 16 +- docs/examples.rst | 4 +- examples/rfm69_1_quickstart.py | 103 ++ examples/rfm69_2_with_acks.py | 110 +++ examples/rfm69_3_aio_with_ack.py | 138 +++ examples/rfm69_header.py | 48 - examples/rfm69_node1.py | 68 -- examples/rfm69_node1_ack.py | 70 -- examples/rfm69_node1_bonnet.py | 62 +- examples/rfm69_node2.py | 66 -- examples/rfm69_node2_ack.py | 61 -- examples/rfm69_rpi_interrupt.py | 114 ++- examples/rfm69_rpi_simpletest.py | 75 -- examples/rfm69_simpletest.py | 79 -- examples/rfm69_throughput.py | 153 +++ examples/rfm69_transmit.py | 60 -- requirements.txt | 2 + 19 files changed, 1520 insertions(+), 1266 deletions(-) create mode 100644 examples/rfm69_1_quickstart.py create mode 100644 examples/rfm69_2_with_acks.py create mode 100644 examples/rfm69_3_aio_with_ack.py delete mode 100644 examples/rfm69_header.py delete mode 100644 examples/rfm69_node1.py delete mode 100644 examples/rfm69_node1_ack.py delete mode 100644 examples/rfm69_node2.py delete mode 100644 examples/rfm69_node2_ack.py delete mode 100644 examples/rfm69_rpi_simpletest.py delete mode 100644 examples/rfm69_simpletest.py create mode 100644 examples/rfm69_throughput.py delete mode 100644 examples/rfm69_transmit.py diff --git a/README.rst b/README.rst index b002352..5339163 100644 --- a/README.rst +++ b/README.rst @@ -23,17 +23,19 @@ receiving of packets with RFM69 series radios (433/915Mhz). .. warning:: This is NOT for LoRa radios! -.. note:: This is a 'best effort' at receiving data using pure Python code--there is not interrupt - support so you might lose packets if they're sent too quickly for the board to process them. - You will have the most luck using this in simple low bandwidth scenarios like sending and - receiving a 60 byte packet at a time--don't try to receive many kilobytes of data at a time! +.. note:: For reliable transmissions, use aio_send_with_ack and aio_recv_with_ack. + With ACKs enabled, observed effective throughput of 14-28kbps with bitrates of 38-250kbps! + Tested on ESP32-S3 Dependencies ============= This driver depends on: * `Adafruit CircuitPython `_ +* `Asyncio `_ * `Bus Device `_ +* `Ticks `_ + Please ensure all dependencies are available on the CircuitPython filesystem. This is easily achieved by downloading @@ -79,7 +81,8 @@ To set it to 1000000 use : .. code-block:: python # Initialze RFM radio - rfm9x = adafruit_rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ,baudrate=1000000) + spi_device = adafruit_rfm69.RFM69.spi_device(spi, CS, baudrate=1000000) + rfm9x = adafruit_rfm69.RFM69(spi_device, IRQ, RESET, RADIO_FREQ_MHZ) Documentation diff --git a/adafruit_rfm69.py b/adafruit_rfm69.py index f32ecae..864d2ae 100644 --- a/adafruit_rfm69.py +++ b/adafruit_rfm69.py @@ -1,5 +1,4 @@ -# SPDX-FileCopyrightText: 2017 Tony DiCola for Adafruit Industries -# +# SPDX-FileCopyrightText: 2017 Tony DiCola for Adafruit Industries # pylint: disable=too-many-lines # SPDX-License-Identifier: MIT """ @@ -11,12 +10,11 @@ .. warning:: This is NOT for LoRa radios! -.. note:: This is a 'best effort' at receiving data using pure Python code--there is not interrupt - support so you might lose packets if they're sent too quickly for the board to process them. - You will have the most luck using this in simple low bandwidth scenarios like sending and - receiving a 60 byte packet at a time--don't try to receive many kilobytes of data at a time! +.. note:: For reliable transmissions, use aio_send_with_ack and aio_recv_with_ack. + With ACKs enabled, observed effective throughput of 14-28kbps with bitrates of 38-250kbps! + Tested on ESP32-S3 -* Author(s): Tony DiCola, Jerry Needell +* Author(s): Tony DiCola, Jerry Needell, Matthew Tai Implementation Notes -------------------- @@ -46,26 +44,25 @@ * Adafruit CircuitPython firmware for the ESP8622 and M0-based boards: https://github.com/adafruit/circuitpython/releases * Adafruit's Bus Device library: https://github.com/adafruit/Adafruit_CircuitPython_BusDevice +* Adafruit's asyncio library: https://github.com/adafruit/Adafruit_CircuitPython_asyncio + """ import random import time +import asyncio +import countio import adafruit_bus_device.spi_device as spidev -from micropython import const - -HAS_SUPERVISOR = False -try: - import supervisor - - HAS_SUPERVISOR = hasattr(supervisor, "ticks_ms") -except ImportError: - pass +from adafruit_ticks import ticks_ms, ticks_add, ticks_less +from micropython import const +from digitalio import DigitalInOut +# Try/Except everything used for typing try: - from typing import Callable, Optional, Type + from typing import Optional, Type from circuitpython_typing import WriteableBuffer, ReadableBuffer - from digitalio import DigitalInOut from busio import SPI + from microcontroller import Pin except ImportError: pass @@ -111,294 +108,317 @@ _TEST_PA2_NORMAL = const(0x70) _TEST_PA2_BOOST = const(0x7C) +_REG_DIOMAPPING1_RX_PAYLOAD_READY = const(0b01) +_REG_DIOMAPPING1_TX_PACKET_SENT = const(0b00) +_REG_IRQ_FLAGS1_MODE_READY = const(0x80) + +_REG_TEMP1_TEMP_MEAS_START = const(0x80) +_REG_TEMP1_TEMP_MEAS_RUNNING = const(0x40) + # The crystal oscillator frequency and frequency synthesizer step size. # See the datasheet for details of this calculation. _FXOSC = 32000000.0 _FSTEP = _FXOSC / 524288 # RadioHead specific compatibility constants. -_RH_BROADCAST_ADDRESS = const(0xFF) +RH_BROADCAST_ADDRESS = const(0xFF) + +# RegDataModul - Defaults for using RadioHead GFSK modem configs (see RadioHead) +# 0b_00_____ = DATAMODE_PACKET +# 0b___00___ = MODULATIONTYPE_FSK +# 0b______01 = MODULATIONSHAPING_FSK_BT1_0 +_RH_DATAMODUL_GFSK = const(0b00000001) + +# RegDataModul - Defaults for using RadioHead GFSK modem configs (see RadioHead) +# 0b1_______ = PACKETFORMAT_VARIABLE +# 0b_10_____ = DCFREE_WHITENING +# 0b___1____ = CRC_ON +# 0b____0___ = CRC_AUTO_CLEAR_OFF +# 0b_____00_ = ADDRESSFILTERING_NONE +_RH_PACKETCONFIG1_WHITE = const(0b11010000) + +# bitrate: (frequency_deviation, RxBW, AfcBW) +# GFSK (BT=1.0), No Manchester, whitening, CRC, no address filtering +# AFC BW == RX BW == 2 x bit rate +RH_BITRATE_2000 = const(2000) +RH_BITRATE_55555 = const(55555) +RH_BITRATE_2400 = const(2400) +RH_BITRATE_4800 = const(4800) +RH_BITRATE_9600 = const(9600) +RH_BITRATE_19200 = const(19200) +RH_BITRATE_38400 = const(38400) +RH_BITRATE_57600 = const(57600) +RH_BITRATE_125000 = const(125000) +RH_BITRATE_250000 = const(250000) + +_RH_BITRATE_TO_CONFIG_MAP = { + RH_BITRATE_2000: (5000, 0xF4, 0xF5), # GFSK_Rb2Fd5 + RH_BITRATE_55555: (50000, 0x42, 0x42), # GFSK_Rb55555Fd50 + RH_BITRATE_2400: (4800, 0xF4, 0xF4), # GFSK_Rb2_4Fd4_8 + RH_BITRATE_4800: (9600, 0xF4, 0xF4), # GFSK_Rb4_8Fd9_6 + RH_BITRATE_9600: (19200, 0xF4, 0xF4), # GFSK_Rb9_6Fd19_2 + RH_BITRATE_19200: (38400, 0xF3, 0xF3), # GFSK_Rb19_2Fd38_4 + RH_BITRATE_38400: (76800, 0xF2, 0xF2), # GFSK_Rb38_4Fd76_8 + RH_BITRATE_57600: (120000, 0xE2, 0xE2), # GFSK_Rb57_6Fd120 + RH_BITRATE_125000: (125000, 0xE1, 0xE1), # GFSK_Rb125Fd125 + RH_BITRATE_250000: (250000, 0xE0, 0xE0), # GFSK_Rb250Fd250 +} + +# User facing constants: +SLEEP_MODE = const(0b000) +STANDBY_MODE = const(0b001) +FS_MODE = const(0b010) +TX_MODE = const(0b011) +RX_MODE = const(0b100) + + +class RHPacket: # pylint: disable=too-few-public-methods + """ + RadioHead Packet as returned by aio_recv + """ + + def __init__(self, payload: ReadableBuffer, rssi: int, time_: float): + self.dest = payload[0] + self.src = payload[1] + self.sequence_number = payload[2] + self.flags = payload[3] + + self.data = payload[4:] + self.rssi = rssi + self.time = time_ # Time in seconds + + +class _Register: # pylint: disable=too-few-public-methods + address = None + value = None + + def __init__(self, value): + self.value = value + + +class _Bits: # pylint: disable=too-few-public-methods + def __init__(self, *, offset: int = 0, bits: int = 1) -> None: + assert 0 <= offset <= 7 + assert 1 <= bits <= 8 + assert (offset + bits) <= 8 + self._mask = 0 + for _ in range(bits): + self._mask <<= 1 + self._mask |= 1 + self._mask <<= offset + self._offset = offset + + def __get__(self, obj: Optional["_Register"], objtype: Type["_Register"]): + return (obj.value & self._mask) >> self._offset + + def __set__(self, obj: Optional["_Register"], val: int) -> None: + reg_value = obj.value + reg_value &= ~self._mask + reg_value |= (val & 0xFF) << self._offset + obj.value = reg_value + + +class _RegPaLevel(_Register): # pylint: disable=too-few-public-methods + address = _REG_PA_LEVEL + pa_0_on = _Bits(offset=7) + pa_1_on = _Bits(offset=6) + pa_2_on = _Bits(offset=5) + output_power = _Bits(offset=0, bits=5) + + +class _RegDioMapping1(_Register): # pylint: disable=too-few-public-methods + address = _REG_DIO_MAPPING1 + dio_0_mapping = _Bits(offset=6, bits=2) + dio_1_mapping = _Bits(offset=4, bits=2) + dio_2_mapping = _Bits(offset=2, bits=2) + dio_3_mapping = _Bits(offset=0, bits=2) + + +class _RegIrqFlags2(_Register): # pylint: disable=too-few-public-methods + address = _REG_IRQ_FLAGS2 + fifo_full = _Bits(offset=7) + fifo_not_empty = _Bits(offset=6) + fifo_level = _Bits(offset=5) + fifo_overrun = _Bits(offset=4) + packet_sent = _Bits(offset=3) + payload_ready = _Bits(offset=2) + crc_ok = _Bits(offset=1) + + +class _RegSyncConfig(_Register): # pylint: disable=too-few-public-methods + address = _REG_SYNC_CONFIG + sync_on = _Bits(offset=7) + sync_size = _Bits(offset=3, bits=3) + + +class _RegPacketConfig2(_Register): # pylint: disable=too-few-public-methods + address = _REG_PACKET_CONFIG2 + inter_packet_rx_delay = _Bits(offset=4, bits=3) + restart_rx = _Bits(offset=2) + auto_rx_restart_on = _Bits(offset=1) + aes_on = _Bits(offset=0) + + +_RH_DEFAULT_SYNC_WORD = b"\x2D\xD4" +_RH_DEFAULT_PREAMBLE_LENGTH = const(4) +_RH_DEFAULT_BITRATE = RH_BITRATE_250000 +_RH_DEFAULT_TX_POWER = const(13) +_RH_MIN_FIFO_LENGTH = const(5) + +_RH_RELIABLE_DGRAM_TIMEOUT = 0.200 # in seconds +_RH_RELIABLE_DGRAM_RETRIES = const(3) + # The acknowledgement bit in the FLAGS # The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved # for application layer use. -_RH_FLAGS_ACK = const(0x80) -_RH_FLAGS_RETRY = const(0x40) +_RH_RELIABLE_DGRAM_PACKET_FLAGS_ACK = const(0x80) +_RH_RELIABLE_DGRAM_PACKET_FLAGS_RETRY = const(0x40) +_RH_ENABLE_EXPLICIT_RETRY_DEDUP = False -# User facing constants: -SLEEP_MODE = 0b000 -STANDBY_MODE = 0b001 -FS_MODE = 0b010 -TX_MODE = 0b011 -RX_MODE = 0b100 -# supervisor.ticks_ms() contants -_TICKS_PERIOD = const(1 << 29) -_TICKS_MAX = const(_TICKS_PERIOD - 1) -_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2) - -# Disable the silly too many instance members warning. Pylint has no knowledge -# of the context and is merely guessing at the proper amount of members. This -# is a complex chip which requires exposing many attributes and state. Disable -# the warning to work around the error. -# pylint: disable=too-many-instance-attributes - -# disable another pylint nit-pick -# pylint: disable=too-many-public-methods - - -def ticks_diff(ticks1: int, ticks2: int) -> int: - """Compute the signed difference between two ticks values - assuming that they are within 2**28 ticks - """ - diff = (ticks1 - ticks2) & _TICKS_MAX - diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD - return diff - - -def check_timeout(flag: Callable, limit: float) -> bool: - """test for timeout waiting for specified flag""" - timed_out = False - if HAS_SUPERVISOR: - start = supervisor.ticks_ms() - while not timed_out and not flag(): - if ticks_diff(supervisor.ticks_ms(), start) >= limit * 1000: - timed_out = True - else: - start = time.monotonic() - while not timed_out and not flag(): - if time.monotonic() - start >= limit: - timed_out = True - return timed_out - - -class RFM69: + +def aio_to_blocking(in_fxn): + """Convenience decorator to turn aio methods into blocking calls""" + + def wrapper(self, *args, **kwargs): + return asyncio.run(in_fxn(self, *args, **kwargs)) + + return wrapper + + +class RFM69: # pylint: disable=too-many-instance-attributes,too-many-public-methods """Interface to a RFM69 series packet radio. Allows simple sending and receiving of wireless data at supported frequencies of the radio (433/915mhz). - :param busio.SPI spi: The SPI bus connected to the chip. Ensure SCK, MOSI, and MISO are - connected. - :param ~digitalio.DigitalInOut cs: A DigitalInOut object connected to the chip's CS/chip select + :param spidev.SPIDevice spi: The SPIDevice build using RFM69.spi_device(spi, cs, baudrate) + :param ~microcontroller.Pin irq: A Pin object connected to the chip's DIO0/board's IRQ line. - :param ~digitalio.DigitalInOut reset: A DigitalInOut object connected to the chip's RST/reset + :param ~microcontroller.Pin reset: A Pin object connected to the chip's RST/reset line. :param int frequency: The center frequency to configure for radio transmission and reception. Must be a frequency supported by your hardware (i.e. either 433 or 915mhz). - :param bytes sync_word: A byte string up to 8 bytes long which represents the syncronization - word used by received and transmitted packets. Read the datasheet for a full understanding - of this value! However by default the library will set a value that matches the RadioHead - Arduino library. - :param int preamble_length: The number of bytes to pre-pend to a data packet as a preamble. - This is by default 4 to match the RadioHead library. - :param bytes encryption_key: A 16 byte long string that represents the AES encryption key to use - when encrypting and decrypting packets. Both the transmitter and receiver MUST have the - same key value! By default no encryption key is set or used. + :param int bitrate: Set radio's bitrate to RH_BITRATE_XXX. Updates Freq Deviation, Afc Bw, + and Rx Bw to match RadioHead's GFSK settings. Set to lower bitrates to increase range. + Default is 250000. :param bool high_power: Indicate if the chip is a high power variant that supports boosted transmission power. The default is True as it supports the common RFM69HCW modules sold by Adafruit. - .. note:: The D0/interrupt line is currently unused by this module and can remain unconnected. - - Remember this library makes a best effort at receiving packets with pure Python code. Trying - to receive packets too quickly will result in lost data so limit yourself to simple scenarios - of sending and receiving single packets at a time. - - Also note this library tries to be compatible with raw RadioHead Arduino library communication. - This means the library sets up the radio modulation to match RadioHead's default of GFSK - encoding, 250kbit/s bitrate, and 250khz frequency deviation. To change this requires explicitly - setting the radio's bitrate and encoding register bits. Read the datasheet and study the init - function to see an example of this--advanced users only! Advanced RadioHead features like - address/node specific packets or "reliable datagram" delivery are supported however due to the - limitations noted, "reliable datagram" is still subject to missed packets but with it, the - sender is notified if a packe has potentially been missed. + This library is a Python port of the RadioHead Arduino library. + For compatibility, this library sets the radio modulation to match RadioHead's default of GFSK + encoding with configurable bitrates from 2-250kbit/s. Advanced RadioHead features like + address/node specific packets or "reliable datagram" delivery are supported but are subject to + missed packets. In "reliable datagram" delivery, the sender can see if packets are not ACKed. """ # Global buffer for SPI commands. _BUFFER = bytearray(4) - class _RegisterBits: - # Class to simplify access to the many configuration bits avaialable - # on the chip's registers. This is a subclass here instead of using - # a higher level module to increase the efficiency of memory usage - # (all of the instances of this bit class will share the same buffer - # used by the parent RFM69 class instance vs. each having their own - # buffer and taking too much memory). - - # Quirk of pylint that it requires public methods for a class. This - # is a decorator class in Python and by design it has no public methods. - # Instead it uses dunder accessors like get and set below. For some - # reason pylint can't figure this out so disable the check. - # pylint: disable=too-few-public-methods - - # Again pylint fails to see the true intent of this code and warns - # against private access by calling the write and read functions below. - # This is by design as this is an internally used class. Disable the - # check from pylint. - # pylint: disable=protected-access - - def __init__(self, address: int, *, offset: int = 0, bits: int = 1) -> None: - assert 0 <= offset <= 7 - assert 1 <= bits <= 8 - assert (offset + bits) <= 8 - self._address = address - self._mask = 0 - for _ in range(bits): - self._mask <<= 1 - self._mask |= 1 - self._mask <<= offset - self._offset = offset - - def __get__(self, obj: Optional["RFM69"], objtype: Type["RFM69"]): - reg_value = obj._read_u8(self._address) - return (reg_value & self._mask) >> self._offset - - def __set__(self, obj: Optional["RFM69"], val: int) -> None: - reg_value = obj._read_u8(self._address) - reg_value &= ~self._mask - reg_value |= (val & 0xFF) << self._offset - obj._write_u8(self._address, reg_value) - - # Control bits from the registers of the chip: - data_mode = _RegisterBits(_REG_DATA_MOD, offset=5, bits=2) - modulation_type = _RegisterBits(_REG_DATA_MOD, offset=3, bits=2) - modulation_shaping = _RegisterBits(_REG_DATA_MOD, offset=0, bits=2) - temp_start = _RegisterBits(_REG_TEMP1, offset=3) - temp_running = _RegisterBits(_REG_TEMP1, offset=2) - sync_on = _RegisterBits(_REG_SYNC_CONFIG, offset=7) - sync_size = _RegisterBits(_REG_SYNC_CONFIG, offset=3, bits=3) - aes_on = _RegisterBits(_REG_PACKET_CONFIG2, offset=0) - pa_0_on = _RegisterBits(_REG_PA_LEVEL, offset=7) - pa_1_on = _RegisterBits(_REG_PA_LEVEL, offset=6) - pa_2_on = _RegisterBits(_REG_PA_LEVEL, offset=5) - output_power = _RegisterBits(_REG_PA_LEVEL, offset=0, bits=5) - rx_bw_dcc_freq = _RegisterBits(_REG_RX_BW, offset=5, bits=3) - rx_bw_mantissa = _RegisterBits(_REG_RX_BW, offset=3, bits=2) - rx_bw_exponent = _RegisterBits(_REG_RX_BW, offset=0, bits=3) - afc_bw_dcc_freq = _RegisterBits(_REG_AFC_BW, offset=5, bits=3) - afc_bw_mantissa = _RegisterBits(_REG_AFC_BW, offset=3, bits=2) - afc_bw_exponent = _RegisterBits(_REG_AFC_BW, offset=0, bits=3) - packet_format = _RegisterBits(_REG_PACKET_CONFIG1, offset=7, bits=1) - dc_free = _RegisterBits(_REG_PACKET_CONFIG1, offset=5, bits=2) - crc_on = _RegisterBits(_REG_PACKET_CONFIG1, offset=4, bits=1) - crc_auto_clear_off = _RegisterBits(_REG_PACKET_CONFIG1, offset=3, bits=1) - address_filter = _RegisterBits(_REG_PACKET_CONFIG1, offset=1, bits=2) - mode_ready = _RegisterBits(_REG_IRQ_FLAGS1, offset=7) - dio_0_mapping = _RegisterBits(_REG_DIO_MAPPING1, offset=6, bits=2) - - # pylint: disable=too-many-statements - def __init__( # pylint: disable=invalid-name + def __init__( # pylint: disable=too-many-arguments self, - spi: SPI, - cs: DigitalInOut, - reset: DigitalInOut, + spi_device: spidev.SPIDevice, + irq: Pin, + reset: Pin, frequency: int, + bitrate: int = RH_BITRATE_250000, *, - sync_word: bytes = b"\x2D\xD4", - preamble_length: int = 4, - encryption_key: Optional[bytes] = None, high_power: bool = True, - baudrate: int = 2000000 + supports_interrupts: bool = False, ) -> None: - self._tx_power = 13 - self.high_power = high_power - # Device support SPI mode 0 (polarity & phase = 0) up to a max of 10mhz. - self._device = spidev.SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=0) - # Setup reset as a digital output that's low. - self._reset = reset - self._reset.switch_to_output(value=False) - self.reset() # Reset the chip. + ########## + # Publically accessible members + ########## + self.address = RH_BROADCAST_ADDRESS + self.tx_success = 0 + self.rx_success = 0 + + self.rx_packet = None + self.send_retransmissions = 0 + self.set_op_mode_timeout = 1.000 # in seconds + + # Supports Interrupts + # CircuitPython: False + # MicroPython: True* (cannot use self.handle_interrupt as-is + # due to ISR accessing SPI) + # RPi: True (not tested) + # + # NOTE: When setting supports_interrupts = True, you MUST setup your own + # ISRs and callbacks... self.handle_interrupt *may* be used + # on RaspberryPi but this is untested + self.platform_supports_interrupts = supports_interrupts + self.rx_polling_interval = 0.001 # in seconds + self.tx_polling_interval = 0.001 # in seconds + + ########## + # Private members used in RadioHead + ########## + self._promiscuous = False + self._last_sequence_number = 0 + self._seen_ids = bytearray(256) + + ########## + # Private members used in RFM69 + ########## + self._is_high_power = high_power + self._tx_power = _RH_DEFAULT_TX_POWER + self._mode = None + + self._device = spi_device + # Check the version of the chip. - version = self._read_u8(_REG_VERSION) - if version != 0x24: - raise RuntimeError("Invalid RFM69 version, check wiring!") - self.idle() # Enter idle state. - # Setup the chip in a similar way to the RadioHead RFM69 library. + self._validate_chip_version() + + # Setup reset as a digital output that's low. + self._irq_counter = countio.Counter(irq, edge=countio.Edge.RISE) + + self._reset_digital_pin = DigitalInOut(reset) + self._reset_digital_pin.switch_to_output(value=False) + self._reset() # Reset the chip. + + self.set_mode_standby() # Enter standby state. + + ########## + # Configure the Radio in a similar way to the RadioHead RFM69 library. + # Assumes GFSK, excluded OOK and FSK for simplicity + ########## # Set FIFO TX condition to not empty and the default FIFO threshold to 15. - self._write_u8(_REG_FIFO_THRESH, 0b10001111) + self._spi_write_u8(_REG_FIFO_THRESH, 0b10001111) # Configure low beta off. - self._write_u8(_REG_TEST_DAGC, 0x30) - # Disable boost. - self._write_u8(_REG_TEST_PA1, _TEST_PA1_NORMAL) - self._write_u8(_REG_TEST_PA2, _TEST_PA2_NORMAL) + self._spi_write_u8(_REG_TEST_DAGC, 0x30) + # Set the syncronization word. - self.sync_word = sync_word - self.preamble_length = preamble_length # Set the preamble length. - self.frequency_mhz = frequency # Set frequency. - self.encryption_key = encryption_key # Set encryption key. - # Configure modulation for RadioHead library GFSK_Rb250Fd250 mode - # by default. Users with advanced knowledge can manually reconfigure - # for any other mode (consulting the datasheet is absolutely - # necessary!). - self.modulation_shaping = 0b01 # Gaussian filter, BT=1.0 - self.bitrate = 250000 # 250kbs - self.frequency_deviation = 250000 # 250khz - self.rx_bw_dcc_freq = 0b111 # RxBw register = 0xE0 - self.rx_bw_mantissa = 0b00 - self.rx_bw_exponent = 0b000 - self.afc_bw_dcc_freq = 0b111 # AfcBw register = 0xE0 - self.afc_bw_mantissa = 0b00 - self.afc_bw_exponent = 0b000 - self.packet_format = 1 # Variable length. - self.dc_free = 0b10 # Whitening - # Set transmit power to 13 dBm, a safe value any module supports. - self.tx_power = 13 + self.set_sync_word(_RH_DEFAULT_SYNC_WORD) + self.set_modem_config(bitrate) + self.set_preamble_length( + _RH_DEFAULT_PREAMBLE_LENGTH + ) # Set the preamble length. + self.set_frequency_mhz(frequency) # Set frequency. + self.set_encryption_key(None) # Set encryption key. - # initialize last RSSI reading - self.last_rssi = 0.0 - """The RSSI of the last received packet. Stored when the packet was received. - This instantaneous RSSI value may not be accurate once the - operating mode has been changed. - """ - # initialize timeouts and delays delays - self.ack_wait = 0.5 - """The delay time before attempting a retry after not receiving an ACK""" - self.receive_timeout = 0.5 - """The amount of time to poll for a received packet. - If no packet is received, the returned packet will be None - """ - self.xmit_timeout = 2.0 - """The amount of time to wait for the HW to transmit the packet. - This is mainly used to prevent a hang due to a HW issue - """ - self.ack_retries = 5 - """The number of ACK retries before reporting a failure.""" - self.ack_delay = None - """The delay time before attemting to send an ACK. - If ACKs are being missed try setting this to .1 or .2. - """ - # initialize sequence number counter for reliabe datagram mode - self.sequence_number = 0 - # create seen Ids list - self.seen_ids = bytearray(256) - # initialize packet header - # node address - default is broadcast - self.node = _RH_BROADCAST_ADDRESS - """The default address of this Node. (0-255). - If not 255 (0xff) then only packets address to this node will be accepted. - First byte of the RadioHead header. - """ - # destination address - default is broadcast - self.destination = _RH_BROADCAST_ADDRESS - """The default destination address for packet transmissions. (0-255). - If 255 (0xff) then any receiving node should accept the packet. - Second byte of the RadioHead header. - """ - # ID - contains seq count for reliable datagram mode - self.identifier = 0 - """Automatically set to the sequence number when send_with_ack() used. - Third byte of the RadioHead header. - """ - # flags - identifies ack/reetry packet for reliable datagram mode - self.flags = 0 - """Upper 4 bits reserved for use by Reliable Datagram Mode. - Lower 4 bits may be used to pass information. - Fourth byte of the RadioHead header. + # Set transmit power to 13 dBm, a safe value any module supports. + self.set_tx_power(_RH_DEFAULT_TX_POWER) + + def __del__(self): + self._irq_counter.deinit() + + ##### + # Begin functions specific to CircuitPython + ##### + @classmethod + def spi_device( + cls, spi: SPI, cs: Pin, baudrate: int = 2000000 # pylint: disable=invalid-name + ) -> spidev.SPIDevice: + """Builds a SPI Device with SPI baudrate defauled to 2mhz + SPI Device is a required argument for RFM69() + + Device supports SPI mode 0 (polarity & phase = 0) up to a max of 10mhz. + Ensure SCK, MOSI, and MISO are connected. """ + return spidev.SPIDevice( + spi, DigitalInOut(cs), baudrate=baudrate, polarity=0, phase=0 + ) - # pylint: enable=too-many-statements - - # pylint: disable=no-member - # Reconsider this disable when it can be tested. - def _read_into( + def _spi_read_into( self, address: int, buf: WriteableBuffer, length: Optional[int] = None ) -> None: # Read a number of bytes from the specified address into the provided @@ -412,12 +432,16 @@ def _read_into( device.write(self._BUFFER, end=1) device.readinto(buf, end=length) - def _read_u8(self, address: int) -> int: + def _spi_read_u8(self, address: int) -> int: # Read a single byte from the provided address and return it. - self._read_into(address, self._BUFFER, length=1) + self._spi_read_into(address, self._BUFFER, length=1) return self._BUFFER[0] - def _write_from( + def _spi_read_register(self, register_cls: _Register) -> _Register: + value = self._spi_read_u8(register_cls.address) + return register_cls(value) + + def _spi_write_from( self, address: int, buf: ReadableBuffer, length: Optional[int] = None ) -> None: # Write a number of bytes to the provided address and taken from the @@ -431,7 +455,7 @@ def _write_from( device.write(self._BUFFER, end=1) device.write(buf, end=length) # send data - def _write_u8(self, address: int, val: int) -> None: + def _spi_write_u8(self, address: int, val: int) -> None: # Write a byte register to the chip. Specify the 7-bit address and the # 8-bit value to write to that address. with self._device as device: @@ -440,164 +464,147 @@ def _write_u8(self, address: int, val: int) -> None: self._BUFFER[1] = val & 0xFF device.write(self._BUFFER, end=2) - def reset(self) -> None: - """Perform a reset of the chip.""" - # See section 7.2.2 of the datasheet for reset description. - self._reset.value = True - time.sleep(0.0001) # 100 us - self._reset.value = False - time.sleep(0.005) # 5 ms - - def set_boost(self, setting: int) -> None: - """Set preamp boost if needed.""" - if self._tx_power >= 18: - self._write_u8(_REG_TEST_PA1, setting) - self._write_u8(_REG_TEST_PA2, setting) - - def idle(self) -> None: - """Enter idle standby mode (switching off high power amplifiers if necessary).""" - # Like RadioHead library, turn off high power boost if enabled. - self.set_boost(_TEST_PA1_NORMAL) - self.operation_mode = STANDBY_MODE + def _spi_write_register(self, register_obj: _Register) -> None: + address = register_obj.address + value = register_obj.value + self._spi_write_u8(address, value) + + ##### + # Begin RadioHead ported functions from RHGenericDriver + # Public : Python ports of RadioHead functions + # Private: Python helper/convenice functions that do NOT exist in RadioHead + ##### + async def wait_for_packet_sent(self): # pylint: disable=missing-function-docstring + counter = self._irq_counter + send_polling = self.tx_polling_interval + + # Wait for the TX mode to change, should happen in aio_send (no timeout parameter) + is_tx_mode = bool(self._mode == TX_MODE) + is_packet_sent = bool(counter.count) + while is_tx_mode and not is_packet_sent: + await asyncio.sleep(send_polling) + is_tx_mode = bool(self._mode == TX_MODE) + is_packet_sent = bool(counter.count) + + return is_packet_sent + + async def wait_for_payload_ready( + self, timeout=1.0 + ): # pylint: disable=missing-function-docstring + end_time = ticks_add(ticks_ms(), int(timeout * 1000)) + + counter = self._irq_counter + recv_polling = self.rx_polling_interval + + is_rx_mode = bool(self._mode == RX_MODE) + is_payload_ready = bool(counter.count) + is_time_remaining = ticks_less(ticks_ms(), end_time) + while is_rx_mode and not is_payload_ready and is_time_remaining: + await asyncio.sleep(recv_polling) + is_rx_mode = bool(self._mode == RX_MODE) + is_payload_ready = bool(counter.count) + is_time_remaining = ticks_less(ticks_ms(), end_time) + + return is_payload_ready + + ##### + # Begin RadioHead ported functions from RH_RF69 + # Public : Python ports of RadioHead functions + # Private: Python helper/convenice functions that do NOT exist in RadioHead + ##### + def _validate_chip_version(self): + version = self._spi_read_u8(_REG_VERSION) + if version != 0x24: + raise RuntimeError("Invalid RFM69 version, check wiring!") - def sleep(self) -> None: - """Enter sleep mode.""" - self.operation_mode = SLEEP_MODE + def handle_interrupt( + self, is_packet_sent=None, is_payload_ready=None + ): # pylint: disable=missing-function-docstring + """Interrupt handler when IRQ goes high. - def listen(self) -> None: - """Listen for packets to be received by the chip. Use :py:func:`receive` to listen, wait - and retrieve packets as they're available. + Checks for PacketSent or PayloadReady events """ - # Like RadioHead library, turn off high power boost if enabled. - self.set_boost(_TEST_PA1_NORMAL) - # Enable payload ready interrupt for D0 line. - self.dio_0_mapping = 0b01 - # Enter RX mode (will clear FIFO!). - self.operation_mode = RX_MODE - - def transmit(self) -> None: - """Transmit a packet which is queued in the FIFO. This is a low level function for - entering transmit mode and more. For generating and transmitting a packet of data use - :py:func:`send` instead. + if is_packet_sent is None and is_payload_ready is None: + reg_irq_flags2 = self._spi_read_register(_RegIrqFlags2) + is_packet_sent = bool(reg_irq_flags2.packet_sent) + is_payload_ready = bool(reg_irq_flags2.payload_ready) + + if self._mode == TX_MODE and is_packet_sent: + self.set_mode_standby() + self.tx_success += 1 + elif self._mode == RX_MODE and is_payload_ready: + # Log the RSSI and reception time in uController time + recv_rssi = self.get_rssi() + recv_time = ticks_ms() / 1000.0 + + # Stop RX so we can read 1 packet and not worry about receiving + # yet another packet + self.set_mode_standby() + + self.read_fifo(recv_rssi, recv_time) + + def read_fifo(self, recv_rssi: float, recv_time: float) -> bytearray: """ - # Like RadioHead library, turn on high power boost if enabled. - self.set_boost(_TEST_PA1_BOOST) - # Enable packet sent interrupt for D0 line. - self.dio_0_mapping = 0b00 - # Enter TX mode (will clear FIFO!). - self.operation_mode = TX_MODE - - @property - def temperature(self) -> float: + Internally used function to read the RFM69 FIFO via SPI + Applies RadioHead address filtering + """ + # Read the length of the FIFO. + fifo_length = self._spi_read_u8(_REG_FIFO) + + # Handle if the received packet is too small to include the 4 byte + # RadioHead header and at least one byte of data --reject this packet and ignore it. + if ( + fifo_length < _RH_MIN_FIFO_LENGTH + ): # read and clear the FIFO if anything in it + return + + fifo_data = bytearray(fifo_length) + self._spi_read_into(_REG_FIFO, fifo_data, fifo_length) + + # Filter packets not explicitly destined for me + packet_dest = fifo_data[0] + is_for_me = bool(packet_dest == self.address) or bool( + packet_dest == RH_BROADCAST_ADDRESS + ) + if self._promiscuous or is_for_me: + self.rx_packet = RHPacket(fifo_data, recv_rssi, recv_time) + self.rx_success += 1 + + def get_temperature(self) -> float: """The internal temperature of the chip in degrees Celsius. Be warned this is not calibrated or very accurate. .. warning:: Reading this will STOP any receiving/sending that might be happening! """ # Start a measurement then poll the measurement finished bit. - self.temp_start = 1 - while self.temp_running > 0: - pass + self._spi_write_u8(_REG_TEMP1, _REG_TEMP1_TEMP_MEAS_START) + + temp_running = True + while temp_running: + temp_value = self._spi_read_u8(_REG_TEMP1) + temp_running = bool(temp_value & _REG_TEMP1_TEMP_MEAS_RUNNING) + # Grab the temperature value and convert it to Celsius. # This uses the same observed value formula from the Radiohead library. - temp = self._read_u8(_REG_TEMP2) + temp = self._spi_read_u8(_REG_TEMP2) return 166.0 - temp - @property - def operation_mode(self) -> int: - """The operation mode value. Unless you're manually controlling the chip you shouldn't - change the operation_mode with this property as other side-effects are required for - changing logical modes--use :py:func:`idle`, :py:func:`sleep`, :py:func:`transmit`, - :py:func:`listen` instead to signal intent for explicit logical modes. - """ - op_mode = self._read_u8(_REG_OP_MODE) - return (op_mode >> 2) & 0b111 - - @operation_mode.setter - def operation_mode(self, val: int) -> None: - assert 0 <= val <= 4 - # Set the mode bits inside the operation mode register. - op_mode = self._read_u8(_REG_OP_MODE) - op_mode &= 0b11100011 - op_mode |= val << 2 - self._write_u8(_REG_OP_MODE, op_mode) - # Wait for mode to change by polling interrupt bit. - if HAS_SUPERVISOR: - start = supervisor.ticks_ms() - while not self.mode_ready: - if ticks_diff(supervisor.ticks_ms(), start) >= 1000: - raise TimeoutError("Operation Mode failed to set.") - else: - start = time.monotonic() - while not self.mode_ready: - if time.monotonic() - start >= 1: - raise TimeoutError("Operation Mode failed to set.") - - @property - def sync_word(self) -> bytearray: - """The synchronization word value. This is a byte string up to 8 bytes long (64 bits) - which indicates the synchronization word for transmitted and received packets. Any - received packet which does not include this sync word will be ignored. The default value - is 0x2D, 0xD4 which matches the RadioHead RFM69 library. Setting a value of None will - disable synchronization word matching entirely. - """ - # Handle when sync word is disabled.. - if not self.sync_on: - return None - # Sync word is not disabled so read the current value. - sync_word_length = self.sync_size + 1 # Sync word size is offset by 1 - # according to datasheet. - sync_word = bytearray(sync_word_length) - self._read_into(_REG_SYNC_VALUE1, sync_word) - return sync_word - - @sync_word.setter - def sync_word(self, val: Optional[bytearray]) -> None: - # Handle disabling sync word when None value is set. - if val is None: - self.sync_on = 0 - else: - # Check sync word is at most 8 bytes. - assert 1 <= len(val) <= 8 - # Update the value, size and turn on the sync word. - self._write_from(_REG_SYNC_VALUE1, val) - self.sync_size = len(val) - 1 # Again sync word size is offset by - # 1 according to datasheet. - self.sync_on = 1 - - @property - def preamble_length(self) -> int: - """The length of the preamble for sent and received packets, an unsigned 16-bit value. - Received packets must match this length or they are ignored! Set to 4 to match the - RadioHead RFM69 library. - """ - msb = self._read_u8(_REG_PREAMBLE_MSB) - lsb = self._read_u8(_REG_PREAMBLE_LSB) - return ((msb << 8) | lsb) & 0xFFFF - - @preamble_length.setter - def preamble_length(self, val: int) -> None: - assert 0 <= val <= 65535 - self._write_u8(_REG_PREAMBLE_MSB, (val >> 8) & 0xFF) - self._write_u8(_REG_PREAMBLE_LSB, val & 0xFF) - - @property - def frequency_mhz(self) -> float: - """The frequency of the radio in Megahertz. Only the allowed values for your radio must be - specified (i.e. 433 vs. 915 mhz)! - """ + def get_frequency_mhz(self) -> float: + """The frequency of the radio in Megahertz. (i.e. 433 vs. 915 mhz)!""" # FRF register is computed from the frequency following the datasheet. # See section 6.2 and FRF register description. # Read bytes of FRF register and assemble into a 24-bit unsigned value. - msb = self._read_u8(_REG_FRF_MSB) - mid = self._read_u8(_REG_FRF_MID) - lsb = self._read_u8(_REG_FRF_LSB) + msb = self._spi_read_u8(_REG_FRF_MSB) + mid = self._spi_read_u8(_REG_FRF_MID) + lsb = self._spi_read_u8(_REG_FRF_LSB) frf = ((msb << 16) | (mid << 8) | lsb) & 0xFFFFFF frequency = (frf * _FSTEP) / 1000000.0 return frequency - @frequency_mhz.setter - def frequency_mhz(self, val: float) -> None: + def set_frequency_mhz(self, val: float) -> None: + """The frequency of the radio in Megahertz. Only the allowed values for your radio must be + specified (i.e. 433 vs. 915 mhz)! + """ assert 290 <= val <= 1020 # Calculate FRF register 24-bit value using section 6.2 of the datasheet. frf = int((val * 1000000.0) / _FSTEP) & 0xFFFFFF @@ -605,70 +612,161 @@ def frequency_mhz(self, val: float) -> None: msb = frf >> 16 mid = (frf >> 8) & 0xFF lsb = frf & 0xFF - self._write_u8(_REG_FRF_MSB, msb) - self._write_u8(_REG_FRF_MID, mid) - self._write_u8(_REG_FRF_LSB, lsb) + self._spi_write_u8(_REG_FRF_MSB, msb) + self._spi_write_u8(_REG_FRF_MID, mid) + self._spi_write_u8(_REG_FRF_LSB, lsb) - @property - def encryption_key(self) -> bytearray: - """The AES encryption key used to encrypt and decrypt packets by the chip. This can be set - to None to disable encryption (the default), otherwise it must be a 16 byte long byte - string which defines the key (both the transmitter and receiver must use the same key - value). + def get_rssi(self) -> float: + """The received strength indicator (in dBm). + May be inaccuate if not read immediatey. last_rssi contains the value read immediately + receipt of the last packet. """ - # Handle if encryption is disabled. - if self.aes_on == 0: - return None - # Encryption is enabled so read the key and return it. - key = bytearray(16) - self._read_into(_REG_AES_KEY1, key) - return key + # Read RSSI register and convert to value using formula in datasheet. + return -self._spi_read_u8(_REG_RSSI_VALUE) / 2.0 - @encryption_key.setter - def encryption_key(self, val: bytearray) -> None: - # Handle if unsetting the encryption key (None value). - if val is None: - self.aes_on = 0 - else: - # Set the encryption key and enable encryption. - assert len(val) == 16 - self._write_from(_REG_AES_KEY1, val) - self.aes_on = 1 + def get_op_mode(self) -> int: + """The operation mode value.""" + op_mode = self._spi_read_u8(_REG_OP_MODE) + return (op_mode >> 2) & 0b111 + + def set_op_mode(self, val: int) -> None: + """The operation mode value. Unless you're manually controlling the chip you shouldn't + change the operation_mode with this property as other side-effects are required for + changing logical modes--use :py:func:`set_mode_standby`, :py:func:`set_mode_sleep`, + :py:func:`set_mode_tx`, :py:func:`set_mode_rx` insteads. + """ + assert 0 <= val <= 4 + + # Set the mode bits inside the operation mode register. + op_mode = self._spi_read_u8(_REG_OP_MODE) + op_mode &= 0b11100011 + op_mode |= val << 2 + self._spi_write_u8(_REG_OP_MODE, op_mode) + + # Locally cache the mode + self._mode = val + + # Blocking call until ModeReady + # TODO: Monitor IRQs via DIO5 pin if available... + # For now fallback to polling the register + end_time = ticks_add(ticks_ms(), int(self.set_op_mode_timeout * 1000)) + is_time_remaining = True + while is_time_remaining: + irq_flags1 = self._spi_read_u8(_REG_IRQ_FLAGS1) + # check if ModeReady + if bool(irq_flags1 & _REG_IRQ_FLAGS1_MODE_READY): + return + + is_time_remaining = ticks_less(ticks_ms(), end_time) - @property - def tx_power(self) -> int: + raise TimeoutError("Operation Mode failed to set.") + + def _set_boost(self, pa1_setting: int, pa2_setting: int) -> None: + """Set preamp boost if needed.""" + if self._tx_power >= 18: + self._spi_write_u8(_REG_TEST_PA1, pa1_setting) + self._spi_write_u8(_REG_TEST_PA2, pa2_setting) + + def set_mode_standby(self) -> bool: + """Enter standby mode (switching off high power amplifiers if necessary).""" + # Like RadioHead library, turn off high power boost if enabled. + if self._mode == STANDBY_MODE: + return False + + self._set_boost(_TEST_PA1_NORMAL, _TEST_PA2_NORMAL) + self.set_op_mode(STANDBY_MODE) + return True + + def set_mode_sleep(self) -> None: + """Enter sleep mode.""" + if self._mode == SLEEP_MODE: + return False + + self.set_op_mode(SLEEP_MODE) + return True + + def set_mode_rx(self) -> None: + """Listen for packets to be received by the chip. Use :py:func:`receive` to listen, wait + and retrieve packets as they're available. + """ + if self._mode == RX_MODE: + return False + + # Like RadioHead library, turn off high power boost if enabled. + self._set_boost(_TEST_PA1_NORMAL, _TEST_PA2_NORMAL) + + # Enable payload ready interrupt for D0 line. + reg_dio_mapping1 = self._spi_read_register(_RegDioMapping1) + reg_dio_mapping1.dio_0_mapping = _REG_DIOMAPPING1_RX_PAYLOAD_READY + self._spi_write_register(reg_dio_mapping1) + + # Reset the IRQ counter + self._irq_counter.reset() + + # Enter RX mode (will clear FIFO!). + self.set_op_mode(RX_MODE) + return True + + def set_mode_tx(self) -> None: + """Transmit a packet which is queued in the FIFO. This is a low level function for + entering transmit mode and more. For generating and transmitting a packet of data use + :py:func:`send` instead. + """ + if self._mode == TX_MODE: + return False + + # Like RadioHead library, turn on high power boost if enabled. + self._set_boost(_TEST_PA1_BOOST, _TEST_PA2_BOOST) + + # Enable "packet sent" interrupt for IRQ line. + reg_dio_mapping1 = self._spi_read_register(_RegDioMapping1) + reg_dio_mapping1.dio_0_mapping = _REG_DIOMAPPING1_TX_PACKET_SENT + self._spi_write_register(reg_dio_mapping1) + + # Reset the IRQ counter + self._irq_counter.reset() + + # Enter TX mode (will clear FIFO!). + self.set_op_mode(TX_MODE) + return True + + def get_tx_power(self) -> int: """The transmit power in dBm. Can be set to a value from -2 to 20 for high power devices - (RFM69HCW, high_power=True) or -18 to 13 for low power devices. Only integer power - levels are actually set (i.e. 12.5 will result in a value of 12 dBm). + (RFM69HCW, high_power=True) or -18 to 13 for low power devices. """ # Follow table 10 truth table from the datasheet for determining power # level from the individual PA level bits and output power register. - pa0 = self.pa_0_on - pa1 = self.pa_1_on - pa2 = self.pa_2_on - current_output_power = self.output_power + reg_pa_level = self._spi_read_register(_RegPaLevel) + + pa0 = reg_pa_level.pa_0_on + pa1 = reg_pa_level.pa_1_on + pa2 = reg_pa_level.pa_2_on + current_output_power = reg_pa_level.output_power if pa0 and not pa1 and not pa2: # -18 to 13 dBm range return -18 + current_output_power if not pa0 and pa1 and not pa2: # -2 to 13 dBm range return -18 + current_output_power - if not pa0 and pa1 and pa2 and not self.high_power: + if not pa0 and pa1 and pa2 and not self._is_high_power: # 2 to 17 dBm range return -14 + current_output_power - if not pa0 and pa1 and pa2 and self.high_power: + if not pa0 and pa1 and pa2 and self._is_high_power: # 5 to 20 dBm range return -11 + current_output_power raise RuntimeError("Power amps state unknown!") - @tx_power.setter - def tx_power(self, val: float): + def set_tx_power(self, val: float): + """The transmit power in dBm. Can be set to a value from -2 to 20 for high power devices + (RFM69HCW, high_power=True) or -18 to 13 for low power devices. Only integer power + levels are actually set (i.e. 12.5 will result in a value of 12 dBm). + """ val = int(val) # Determine power amplifier and output power values depending on # high power state and requested power. pa_0_on = pa_1_on = pa_2_on = 0 output_power = 0 - if self.high_power: + if self._is_high_power: # Handle high power mode. assert -2 <= val <= 20 pa_1_on = 1 @@ -688,255 +786,385 @@ def tx_power(self, val: float): pa_0_on = 1 output_power = val + 18 # Set power amplifiers and output power as computed above. - self.pa_0_on = pa_0_on - self.pa_1_on = pa_1_on - self.pa_2_on = pa_2_on - self.output_power = output_power + reg_pa_level = self._spi_read_register(_RegPaLevel) + reg_pa_level.pa_0_on = pa_0_on + reg_pa_level.pa_1_on = pa_1_on + reg_pa_level.pa_2_on = pa_2_on + reg_pa_level.output_power = output_power + self._spi_write_register(reg_pa_level) + self._tx_power = val - @property - def rssi(self) -> float: - """The received strength indicator (in dBm). - May be inaccuate if not read immediatey. last_rssi contains the value read immediately - receipt of the last packet. + def set_modem_config(self, bitrate=_RH_DEFAULT_BITRATE): + """Primary method to update the configured bitrate for this radio + Updates modulation to be compatible with RadioHead library + + See _RH_BITRATE_TO_CONFIG_MAP """ - # Read RSSI register and convert to value using formula in datasheet. - return -self._read_u8(_REG_RSSI_VALUE) / 2.0 + assert ( + bitrate in _RH_BITRATE_TO_CONFIG_MAP + ), f"Invalid bitrate, {bitrate} not in {_RH_BITRATE_TO_CONFIG_MAP.keys()}" + + # Configure modulation for RadioHead library, see _RH_BITRATE_TO_CONFIG_MAP + self._spi_write_u8(_REG_DATA_MOD, _RH_DATAMODUL_GFSK) + self._spi_write_u8(_REG_PACKET_CONFIG1, _RH_PACKETCONFIG1_WHITE) - @property - def bitrate(self) -> float: + frequency_deviation, rx_bw, afc_bw = _RH_BITRATE_TO_CONFIG_MAP[bitrate] + + self._set_bitrate(bitrate) + self._set_frequency_deviation(frequency_deviation) + self._spi_write_u8(_REG_RX_BW, rx_bw) + self._spi_write_u8(_REG_AFC_BW, afc_bw) + + def get_bitrate(self) -> float: """The modulation bitrate in bits/second (or chip rate if Manchester encoding is enabled). Can be a value from ~489 to 32mbit/s, but see the datasheet for the exact supported values. """ - msb = self._read_u8(_REG_BITRATE_MSB) - lsb = self._read_u8(_REG_BITRATE_LSB) + msb = self._spi_read_u8(_REG_BITRATE_MSB) + lsb = self._spi_read_u8(_REG_BITRATE_LSB) return _FXOSC / ((msb << 8) | lsb) - @bitrate.setter - def bitrate(self, val: float) -> None: + def _set_bitrate(self, val: float) -> None: assert (_FXOSC / 65535) <= val <= 32000000.0 # Round up to the next closest bit-rate value with addition of 0.5. bitrate = int((_FXOSC / val) + 0.5) & 0xFFFF - self._write_u8(_REG_BITRATE_MSB, bitrate >> 8) - self._write_u8(_REG_BITRATE_LSB, bitrate & 0xFF) + self._spi_write_u8(_REG_BITRATE_MSB, bitrate >> 8) + self._spi_write_u8(_REG_BITRATE_LSB, bitrate & 0xFF) - @property - def frequency_deviation(self) -> float: + def get_frequency_deviation(self) -> float: """The frequency deviation in Hertz.""" - msb = self._read_u8(_REG_FDEV_MSB) - lsb = self._read_u8(_REG_FDEV_LSB) + msb = self._spi_read_u8(_REG_FDEV_MSB) + lsb = self._spi_read_u8(_REG_FDEV_LSB) return _FSTEP * ((msb << 8) | lsb) - @frequency_deviation.setter - def frequency_deviation(self, val: float) -> None: + def _set_frequency_deviation(self, val: float) -> None: assert 0 <= val <= (_FSTEP * 16383) # fdev is a 14-bit unsigned value # Round up to the next closest integer value with addition of 0.5. fdev = int((val / _FSTEP) + 0.5) & 0x3FFF - self._write_u8(_REG_FDEV_MSB, fdev >> 8) - self._write_u8(_REG_FDEV_LSB, fdev & 0xFF) + self._spi_write_u8(_REG_FDEV_MSB, fdev >> 8) + self._spi_write_u8(_REG_FDEV_LSB, fdev & 0xFF) + + def get_preamble_length(self) -> int: + """The length of the preamble for sent and received packets, an unsigned 16-bit value. + Received packets must match this length or they are ignored! Set to 4 to match the + RadioHead RFM69 library. + """ + msb = self._spi_read_u8(_REG_PREAMBLE_MSB) + lsb = self._spi_read_u8(_REG_PREAMBLE_LSB) + return ((msb << 8) | lsb) & 0xFFFF + + def set_preamble_length(self, val: int) -> None: + """The length of the preamble for sent and received packets, an unsigned 16-bit value. + Received packets must match this length or they are ignored! Set to 4 to match the + RadioHead RFM69 library. + """ + assert 0 <= val <= 65535 + self._spi_write_u8(_REG_PREAMBLE_MSB, (val >> 8) & 0xFF) + self._spi_write_u8(_REG_PREAMBLE_LSB, val & 0xFF) + self.preamble_length = val + + def get_sync_word(self) -> bytearray: + """The synchronization word value. This is a byte string up to 8 bytes long (64 bits) + which indicates the synchronization word for transmitted and received packets. Any + received packet which does not include this sync word will be ignored. The default value + is 0x2D, 0xD4 which matches the RadioHead RFM69 library. Setting a value of None will + disable synchronization word matching entirely. + """ + reg_sync_config = self._spi_read_register(_RegSyncConfig) + + # Handle when sync word is disabled.. + if not reg_sync_config.sync_on: + return None + # Sync word is not disabled so read the current value. + sync_word_length = ( + reg_sync_config.sync_size + 1 + ) # Sync word size is offset by 1 + # according to datasheet. + sync_word = bytearray(sync_word_length) + self._spi_read_into(_REG_SYNC_VALUE1, sync_word) + return sync_word + + def set_sync_word(self, val: Optional[bytearray]) -> None: + """The synchronization word value. This is a byte string up to 8 bytes long (64 bits) + which indicates the synchronization word for transmitted and received packets. Any + received packet which does not include this sync word will be ignored. The default value + is 0x2D, 0xD4 which matches the RadioHead RFM69 library. Setting a value of None will + disable synchronization word matching entirely. + """ + # Handle disabling sync word when None value is set. + reg_sync_config = self._spi_read_register(_RegSyncConfig) + sync_on = 0 + if val is not None: + # Check sync word is at most 8 bytes. + assert 1 <= len(val) <= 8 + # Update the value, size and turn on the sync word. + self._spi_write_from(_REG_SYNC_VALUE1, val) + reg_sync_config.sync_size = ( + len(val) - 1 + ) # Again sync word size is offset by + # 1 according to datasheet. + sync_on = 1 + + reg_sync_config.sync_on = sync_on + self._spi_write_register(reg_sync_config) + + def get_encryption_key(self) -> bytearray: + """The AES encryption key used to encrypt and decrypt packets by the chip. This can be set + to None to disable encryption (the default), otherwise it must be a 16 byte long byte + string which defines the key (both the transmitter and receiver must use the same key + value). + """ + # Handle if encryption is disabled. + reg_packet_config2 = self._spi_read_register(_RegPacketConfig2) + if not reg_packet_config2.aes_on: + return None + + # Encryption is enabled so read the key and return it. + key = bytearray(16) + self._spi_read_into(_REG_AES_KEY1, key) + return key - def packet_sent(self) -> bool: - """Transmit status""" - return (self._read_u8(_REG_IRQ_FLAGS2) & 0x8) >> 3 + def set_encryption_key(self, val: bytearray) -> None: + """The AES encryption key used to encrypt and decrypt packets by the chip. This can be set + to None to disable encryption (the default), otherwise it must be a 16 byte long byte + string which defines the key (both the transmitter and receiver must use the same key + value). + """ + # Handle if unsetting the encryption key (None value). + reg_packet_config2 = self._spi_read_register(_RegPacketConfig2) + aes_on = 0 + if val is not None: + # Set the encryption key and enable encryption. + assert len(val) == 16 + self._spi_write_from(_REG_AES_KEY1, val) + aes_on = 1 - def payload_ready(self) -> bool: - """Receive status""" - return (self._read_u8(_REG_IRQ_FLAGS2) & 0x4) >> 2 + reg_packet_config2.aes_on = aes_on + self._spi_write_register(reg_packet_config2) - # pylint: disable=too-many-branches - def send( + def _reset(self) -> None: + """Perform a reset of the chip.""" + # See section 7.2.2 of the datasheet for reset description. + self._reset_digital_pin.value = True + time.sleep(0.0001) # 100 us + self._reset_digital_pin.value = False + time.sleep(0.005) # 5 ms + + def available(self): # pylint: disable=missing-function-docstring + if self._mode == TX_MODE: + return False + + self.set_mode_rx() + if self.platform_supports_interrupts: + return bool(self.rx_packet) + return True + + async def aio_recv(self, *, timeout=1.0) -> RHPacket: + """Wait to receive a packet from the receiver. If a packet is found the RHPacket returned, + otherwise None is returned (which indicates timeout elapsed OR interrupted by transmit). + """ + packet = None + + # Monitor IRQ pin - sets DIO0 pin to PayloadReady + if not self.available(): + return packet + + if not self.platform_supports_interrupts: + # Wait until we get PayloadReady IRQ + is_payload_read = await self.wait_for_payload_ready(timeout=timeout) + self.handle_interrupt(is_payload_ready=is_payload_read) + + packet = self.rx_packet + self.rx_packet = None + return packet + + recv = aio_to_blocking(aio_recv) + + async def aio_send( self, + dest: int, data: ReadableBuffer, *, - keep_listening: bool = False, - destination: Optional[int] = None, - node: Optional[int] = None, - identifier: Optional[int] = None, - flags: Optional[int] = None + sequence_number: int = 0, + flags: int = 0, ) -> bool: """Send a string of data using the transmitter. You can only send 60 bytes at a time (limited by chip's FIFO size and appended headers). This appends a 4 byte header to be compatible with the RadioHead library. The header defaults to using the initialized attributes: - (destination,node,identifier,flags) - It may be temporarily overidden via the kwargs - destination,node,identifier,flags. + (dest,src,sequence_number,flags) + It may be temporarily overidden via the kwargs - dest,sequence_number,flags. Values passed via kwargs do not alter the attribute settings. - The keep_listening argument should be set to True if you want to start listening - automatically after the packet is sent. The default setting is False. - - Returns: True if success or False if the send timed out. """ - # Disable pylint warning to not use length as a check for zero. - # This is a puzzling warning as the below code is clearly the most - # efficient and proper way to ensure a precondition that the provided - # buffer be within an expected range of bounds. Disable this check. - # pylint: disable=len-as-condition - assert 0 < len(data) <= 60 - # pylint: enable=len-as-condition - self.idle() # Stop receiving to clear FIFO and keep it clear. + assert len(data) <= 60 + + # If we're still in TX_MODE, wait until PacketSent + is_packet_sent = await self.wait_for_packet_sent() + if not self.platform_supports_interrupts: + self.handle_interrupt(is_packet_sent=is_packet_sent) + + # Move to Standby + # NOTE: Clears FIFO if RX or even in TX (which is why we block above) + self.set_mode_standby() + # Fill the FIFO with a packet to send. # Combine header and data to form payload - payload = bytearray(5) - payload[0] = 4 + len(data) - if destination is None: # use attribute - payload[1] = self.destination - else: # use kwarg - payload[1] = destination - if node is None: # use attribute - payload[2] = self.node - else: # use kwarg - payload[2] = node - if identifier is None: # use attribute - payload[3] = self.identifier - else: # use kwarg - payload[3] = identifier - if flags is None: # use attribute - payload[4] = self.flags - else: # use kwarg - payload[4] = flags - payload = payload + data + payload = bytearray(5 + len(data)) + payload[0] = 4 + len(data) # RFM69 FIFO packet length + payload[1] = dest # RH destination + payload[2] = self.address # RH source + payload[3] = sequence_number # RH sequence number + payload[4] = flags # RH flags + payload[5:] = data # bytes to transmit + # Write payload to transmit fifo - self._write_from(_REG_FIFO, payload) - # Turn on transmit mode to send out the packet. - self.transmit() - # Wait for packet sent interrupt with explicit polling (not ideal but - # best that can be done right now without interrupts). - timed_out = check_timeout(self.packet_sent, self.xmit_timeout) - # Listen again if requested. - if keep_listening: - self.listen() - else: # Enter idle mode to stop receiving other packets. - self.idle() - return not timed_out - - def send_with_ack(self, data: int) -> bool: - """Reliable Datagram mode: - Send a packet with data and wait for an ACK response. - The packet header is automatically generated. - If enabled, the packet transmission will be retried on failure + self._spi_write_from(_REG_FIFO, payload) + + # Monitor IRQ pin + self.set_mode_tx() # Sets DIO0 pin to PacketSent + + if not self.platform_supports_interrupts: + is_packet_sent = await self.wait_for_packet_sent() + self.handle_interrupt(is_packet_sent=is_packet_sent) + + return True + + send = aio_to_blocking(aio_send) + + ##### + # Begin RadioHead ported functions from RHReliableDatagram + # Public : Python ports of RadioHead functions + # Private: Python helper/convenice functions that do NOT exist in RadioHead + ##### + async def aio_send_with_ack( + self, + dest: int, + data: ReadableBuffer, + *, + max_attempts: int = _RH_RELIABLE_DGRAM_RETRIES, + ack_timeout: float = _RH_RELIABLE_DGRAM_TIMEOUT, + ): """ - if self.ack_retries: - retries_remaining = self.ack_retries - else: - retries_remaining = 1 - got_ack = False - self.sequence_number = (self.sequence_number + 1) & 0xFF - while not got_ack and retries_remaining: - self.identifier = self.sequence_number - self.send(data, keep_listening=True) - # Don't look for ACK from Broadcast message - if self.destination == _RH_BROADCAST_ADDRESS: - got_ack = True - else: - # wait for a packet from our destination - ack_packet = self.receive(timeout=self.ack_wait, with_header=True) - if ack_packet is not None: - if ack_packet[3] & _RH_FLAGS_ACK: - # check the ID - if ack_packet[2] == self.identifier: - got_ack = True - break - # pause before next retry -- random delay - if not got_ack: - # delay by random amount before next try - time.sleep(self.ack_wait + self.ack_wait * random.random()) - retries_remaining = retries_remaining - 1 - # set retry flag in packet header - self.flags |= _RH_FLAGS_RETRY - self.flags = 0 # clear flags - return got_ack - - def receive( + Send data and listen for ACK. If no ACK within (ack_timeout, 2*ack_timeout), retransmit + and listen up to max_attempts. Worst case time: (TX time * 2 ack_timeout) * max_attempts. + + Return + ack_packet: RHPacket if acked, None otherwise + """ + self._last_sequence_number = (self._last_sequence_number + 1) & 0xFF + sequence_number = self._last_sequence_number + attempts = 0 # "retries" in RadioHead library, really this is an attempt + flags = 0 + + while attempts <= max_attempts: + attempts += 1 + + # Mark this as a RETRY + if attempts > 1: + flags |= _RH_RELIABLE_DGRAM_PACKET_FLAGS_RETRY + + await self.aio_send( + dest=dest, data=data, sequence_number=sequence_number, flags=flags + ) + + # Never wait for ACKs to broadcast messages + if dest == RH_BROADCAST_ADDRESS: + return None + + if attempts > 1: + self.send_retransmissions += 1 + + # Introduce a random delay so we avoid TX collisions from other nodes + random_delay = ack_timeout + (ack_timeout * random.random()) + ack_packet = await self.aio_recv(timeout=random_delay) + + # We didn't get a response in time, try re-sending + if ack_packet is None: + continue + + ack_packet_src = ack_packet.src + ack_packet_sequence_number = ack_packet.sequence_number + + is_ack = bool(ack_packet.flags & _RH_RELIABLE_DGRAM_PACKET_FLAGS_ACK) + is_sequence_number = bool(ack_packet_sequence_number == sequence_number) + + is_seen_before = bool( + ack_packet_sequence_number == self._seen_ids[ack_packet_src] + ) + + # This is the ACK we're looking for, return True! + if ( + bool(ack_packet_src == dest) + and bool(ack_packet.dest == self.address) + and is_ack + and is_sequence_number + ): + return ack_packet + + # ACK a previously received packet + if not is_ack and is_seen_before: + await self.aio_acknowledge_packet(ack_packet) + + return None + + aio_send_to_wait = aio_send_with_ack + + send_with_ack = aio_to_blocking(aio_send_with_ack) + send_to_wait = send_with_ack + + async def aio_acknowledge_packet(self, packet: RHPacket): + """ + Instead of (ID, _from), use packet + """ + return await self.aio_send( + dest=packet.src, + data=b"!", + sequence_number=packet.sequence_number, + flags=packet.flags | _RH_RELIABLE_DGRAM_PACKET_FLAGS_ACK, + ) + + acknowledge_packet = aio_to_blocking(aio_acknowledge_packet) + + async def aio_recv_with_ack( self, *, - keep_listening: bool = True, - with_ack: bool = False, - timeout: Optional[float] = None, - with_header: bool = False - ) -> int: - """Wait to receive a packet from the receiver. If a packet is found the payload bytes - are returned, otherwise None is returned (which indicates the timeout elapsed with no - reception). - If keep_listening is True (the default) the chip will immediately enter listening mode - after reception of a packet, otherwise it will fall back to idle mode and ignore any - future reception. - All packets must have a 4 byte header for compatibilty with the - RadioHead library. - The header consists of 4 bytes (To,From,ID,Flags). The default setting will strip - the header before returning the packet to the caller. - If with_header is True then the 4 byte header will be returned with the packet. - The payload then begins at packet[4]. - If with_ack is True, send an ACK after receipt (Reliable Datagram mode) + timeout=1.0, + ) -> RHPacket: """ - timed_out = False - if timeout is None: - timeout = self.receive_timeout - if timeout is not None: - # Wait for the payload_ready signal. This is not ideal and will - # surely miss or overflow the FIFO when packets aren't read fast - # enough, however it's the best that can be done from Python without - # interrupt supports. - # Make sure we are listening for packets. - self.listen() - timed_out = check_timeout(self.payload_ready, timeout) - # Payload ready is set, a packet is in the FIFO. - packet = None - # save last RSSI reading - self.last_rssi = self.rssi - # Enter idle mode to stop receiving other packets. - self.idle() - if not timed_out: - # Read the length of the FIFO. - fifo_length = self._read_u8(_REG_FIFO) - # Handle if the received packet is too small to include the 4 byte - # RadioHead header and at least one byte of data --reject this packet and ignore it. - if fifo_length > 0: # read and clear the FIFO if anything in it - packet = bytearray(fifo_length) - self._read_into(_REG_FIFO, packet, fifo_length) - - if fifo_length < 5: - packet = None - else: - if ( - self.node != _RH_BROADCAST_ADDRESS - and packet[0] != _RH_BROADCAST_ADDRESS - and packet[0] != self.node - ): - packet = None - # send ACK unless this was an ACK or a broadcast - elif ( - with_ack - and ((packet[3] & _RH_FLAGS_ACK) == 0) - and (packet[0] != _RH_BROADCAST_ADDRESS) - ): - # delay before sending Ack to give receiver a chance to get ready - if self.ack_delay is not None: - time.sleep(self.ack_delay) - # send ACK packet to sender (data is b'!') - self.send( - b"!", - destination=packet[1], - node=packet[0], - identifier=packet[2], - flags=(packet[3] | _RH_FLAGS_ACK), - ) - # reject Retries if we have seen this idetifier from this source before - if (self.seen_ids[packet[1]] == packet[2]) and ( - packet[3] & _RH_FLAGS_RETRY - ): - packet = None - else: # save the packet identifier for this source - self.seen_ids[packet[1]] = packet[2] - if ( - not with_header and packet is not None - ): # skip the header if not wanted - packet = packet[4:] - # Listen again if necessary and return the result packet. - if keep_listening: - self.listen() - else: - # Enter idle mode to stop receiving other packets. - self.idle() - return packet + Receive data and send back an ACK on receipt when explicitly address to me. + + Return + packet: RHPacket received + """ + packet = await self.aio_recv(timeout=timeout) + if packet is None: + return packet + + packet_src = packet.src + packet_flags = packet.flags + packet_sequence_number = packet.sequence_number + + # Never ACK an ACK + if packet_flags & _RH_RELIABLE_DGRAM_PACKET_FLAGS_ACK: + return None + + # ACK a packet that was destined for us (do not ack Broadcast) + if packet.dest == self.address: + await self.aio_acknowledge_packet(packet) + + is_retry = packet_flags & _RH_RELIABLE_DGRAM_PACKET_FLAGS_RETRY + is_new_sequence_number = bool( + packet_sequence_number != self._seen_ids[packet_src] + ) + # RH_ENABLE_EXPLICIT_RETRY_DEDUP + if (_RH_ENABLE_EXPLICIT_RETRY_DEDUP and not is_retry) or is_new_sequence_number: + self._seen_ids[packet_src] = packet_sequence_number + return packet + + return None + + aio_recv_from_ack = aio_recv_with_ack + recv_with_ack = aio_to_blocking(aio_recv_with_ack) + recv_from_ack = recv_with_ack diff --git a/docs/conf.py b/docs/conf.py index 29bc050..2f569b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,10 +24,24 @@ # Uncomment the below if you use native CircuitPython modules such as # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. -# autodoc_mock_imports = ["adafruit_bus_device", "micropython"] +autodoc_mock_imports = [ + "adafruit_bus_device", + "adafruit_ticks", + "countio", + "digitalio", + "micropython", +] intersphinx_mapping = { "python": ("https://docs.python.org/3", None), + "adafruit_ticks": ( + "https://docs.circuitpython.org/projects/ticks/en/latest/", + None, + ), + "asyncio": ( + "https://docs.circuitpython.org/projects/asyncio/en/latest/", + None, + ), "BusDevice": ( "https://docs.circuitpython.org/projects/busdevice/en/latest/", None, diff --git a/docs/examples.rst b/docs/examples.rst index 1603391..582f1a4 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -3,6 +3,6 @@ Simple test Ensure your device works with this simple test. -.. literalinclude:: ../examples/rfm69_simpletest.py - :caption: examples/rfm69_simpletest.py +.. literalinclude:: ../examples/rfm69_1_quickstart.py + :caption: examples/rfm69_1_quickstart.py :linenos: diff --git a/examples/rfm69_1_quickstart.py b/examples/rfm69_1_quickstart.py new file mode 100644 index 0000000..b157a94 --- /dev/null +++ b/examples/rfm69_1_quickstart.py @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Example to send a packet periodically + +import time +import board +import adafruit_rfm69 + +# Define pins connected to the chip. +RFM69_IRQ = board.D9 +RFM69_CS = board.CE1 +RFM69_RESET = board.D25 + +# Define radio parameters. +RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your +# module! Can be a value like 915.0, 433.0, etc. + +# Define src/dst addresses for Node 1+2 respectively +SRC_ADDRESS = adafruit_rfm69.RH_BROADCAST_ADDRESS +DEST_ADDRESS = adafruit_rfm69.RH_BROADCAST_ADDRESS + +# If you want to test addresses message, folllow Steps 2a+2b +# Step 2a - Addressed message, uncomment for Node 1 +# SRC_ADDRESS = 100 +# DEST_ADDRESS = 200 + +# Step 2b - Addressed message, uncomment for Node 2 +# SRC_ADDRESS = 200 +# DEST_ADDRESS = 100 + +assert SRC_ADDRESS is not None and DEST_ADDRESS is not None + +# set the time interval (seconds) for sending packets +TX_INTERVAL = 10 + + +def init_client(): + # Initialize SPI device. + in_spi = board.SPI() + in_spi_device = adafruit_rfm69.RFM69.spi_device(in_spi, RFM69_CS) + + # Initialze RFM radio + client = adafruit_rfm69.RFM69(in_spi_device, RFM69_IRQ, RFM69_RESET, RADIO_FREQ_MHZ) + client.address = SRC_ADDRESS + # Optionally set an encryption key (16 byte AES key). MUST match both + # on the transmitter and receiver (or be set to None to disable/the default). + client.set_encryption_key( + b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" + ) + + print("===== Initializing RFM69 client =====") + print(f"Temperature: {client.get_temperature()}C") + print(f"Frequency: {client.get_frequency_mhz()}mhz") + print(f"Bit rate: {client.get_bitrate() / 1000}kbit/s") + print(f"Frequency deviation: {client.get_frequency_deviation()}hz") + return client + + +def print_packet(packet): + if not packet: + return + + # Received a packet! + # Print out the raw bytes of the packet: + print("===== Received packet =====") + print(f"Dest: {hex(packet.dest)}") + print(f"Src: {hex(packet.src)}") + print(f"Seq No: {hex(packet.sequence_number)}") + print(f"Flags: {hex(packet.flags)}") + + print(f"Payload: {packet.data.decode('utf-8')}") + print(f"RSSI: {packet.rssi}") + print(f"Time: {packet.time}") + + +# initialize counter +counter = 0 +rfm69_client = init_client() + +# Send a message +rfm69_client.send( + DEST_ADDRESS, bytes(f"quickstart {rfm69_client.address}: {counter}", "utf-8") +) + +print("Waiting for packets...") +time_now = time.monotonic() +while True: + # Look for a new packet - wait up to 5 seconds: + rx_packet = rfm69_client.recv(timeout=5.0) + if rx_packet is not None: + print_packet(rx_packet) + + # send reading after any packet received + if time.monotonic() - time_now > TX_INTERVAL: + # reset timeer + time_now = time.monotonic() + # clear flag to send data + counter = counter + 1 + rfm69_client.send( + DEST_ADDRESS, + bytes(f"quickstart {rfm69_client.address}: {counter}", "UTF-8"), + ) diff --git a/examples/rfm69_2_with_acks.py b/examples/rfm69_2_with_acks.py new file mode 100644 index 0000000..972081c --- /dev/null +++ b/examples/rfm69_2_with_acks.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries +# SPDX-License-Identifier: MIT + +# Example to send a packet periodically between addressed nodes with ACK + +import time +import board +import adafruit_rfm69 + +# Define pins connected to the chip. +RFM69_IRQ = board.D9 +RFM69_CS = board.CE1 +RFM69_RESET = board.D25 + +# Define radio parameters. +RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your +# module! Can be a value like 915.0, 433.0, etc. + +# Define src/dst addresses for Node 1+2 respectively +SRC_ADDRESS = None +DEST_ADDRESS = None + +# Step 1a - Uncomment for Node 1 +# SRC_ADDRESS = 100 +# DEST_ADDRESS = 200 + +# Step 1b - Uncomment for Node 2 +# SRC_ADDRESS = 200 +# DEST_ADDRESS = 100 + +assert SRC_ADDRESS is not None and DEST_ADDRESS is not None + +# set the time interval (seconds) for sending packets +TX_INTERVAL = 10 + + +def init_client(): + # Initialize SPI device. + in_spi = board.SPI() + in_spi_device = adafruit_rfm69.RFM69.spi_device(in_spi, RFM69_CS) + + # Initialze RFM radio + client = adafruit_rfm69.RFM69(in_spi_device, RFM69_IRQ, RFM69_RESET, RADIO_FREQ_MHZ) + client.address = SRC_ADDRESS + # Optionally set an encryption key (16 byte AES key). MUST match both + # on the transmitter and receiver (or be set to None to disable/the default). + client.set_encryption_key( + b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" + ) + + print("===== Initializing RFM69 client =====") + print(f"Temperature: {client.get_temperature()}C") + print(f"Frequency: {client.get_frequency_mhz()}mhz") + print(f"Bit rate: {client.get_bitrate() / 1000}kbit/s") + print(f"Frequency deviation: {client.get_frequency_deviation()}hz") + return client + + +def print_packet(packet): + if not packet: + return + + # Received a packet! + # Print out the raw bytes of the packet: + print("===== Received packet =====") + print(f"Dest: {hex(packet.dest)}") + print(f"Src: {hex(packet.src)}") + print(f"Seq No: {hex(packet.sequence_number)}") + print(f"Flags: {hex(packet.flags)}") + + print(f"Payload: {packet.data.decode('utf-8')}") + print(f"RSSI: {packet.rssi}") + print(f"Time: {packet.time}") + + +rfm69_client = init_client() +# initialize counter +counter = 0 +ack_failed_counter = 0 +# send startup message from my_node +rfm69_client.send_with_ack( + DEST_ADDRESS, bytes(f"with_ack {rfm69_client.address}: {counter}", "UTF-8") +) + +# Wait to receive packets. +print("Waiting for packets...") +# initialize flag and timer +time_now = time.monotonic() +while True: + # Look for a new packet: only accept if addresses to my_node + rx_packet = rfm69_client.recv_with_ack() + # If no packet was received during the timeout then None is returned. + if rx_packet is None: + continue + + # Received a packet! + # Print out the raw bytes of the packet: + print_packet(rx_packet) + + # send reading after any packet received + if time.monotonic() - time_now > TX_INTERVAL: + # reset timeer + time_now = time.monotonic() + counter += 1 + # send a mesage to destination_node from my_node + if not rfm69_client.send_with_ack( + DEST_ADDRESS, bytes(f"with_ack {rfm69_client.address}: {counter}", "UTF-8") + ): + ack_failed_counter += 1 + print(" No Ack: ", counter, ack_failed_counter) diff --git a/examples/rfm69_3_aio_with_ack.py b/examples/rfm69_3_aio_with_ack.py new file mode 100644 index 0000000..310dc3c --- /dev/null +++ b/examples/rfm69_3_aio_with_ack.py @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: 2023 Matthew Tai +# SPDX-License-Identifier: MIT + +# Example to use asyncio and aio_send_with_ack +# This example simultaneously +# 1) Changes the colorwheel on a Neopixel +# 2) TX'ing or RX'ing via the radio + +import asyncio +import board +import neopixel +import rainbowio +from adafruit_ticks import ticks_ms + +import adafruit_rfm69 + +# Define pins connected to the chip. +RFM69_IRQ = board.D9 +RFM69_CS = board.CE1 +RFM69_RESET = board.D25 + +# Define radio parameters. +RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your +# module! Can be a value like 915.0, 433.0, etc. + +# Define src/dst addresses for Node 1+2 respectively +SRC_ADDRESS = None +DEST_ADDRESS = None + +# Step 1a - Uncomment for Node 1 +# SRC_ADDRESS = 100 +# DEST_ADDRESS = 200 + +# Step 1b - Uncomment for Node 2 +# SRC_ADDRESS = 200 +# DEST_ADDRESS = 100 + +assert SRC_ADDRESS is not None and DEST_ADDRESS is not None + +# set the time interval (seconds) for sending packets +TX_INTERVAL = 10 + + +def init_client(): + # Initialize SPI device. + in_spi = board.SPI() + in_spi_device = adafruit_rfm69.RFM69.spi_device(in_spi, RFM69_CS) + + # Initialze RFM radio + client = adafruit_rfm69.RFM69(in_spi_device, RFM69_IRQ, RFM69_RESET, RADIO_FREQ_MHZ) + client.address = SRC_ADDRESS + # Optionally set an encryption key (16 byte AES key). MUST match both + # on the transmitter and receiver (or be set to None to disable/the default). + client.set_encryption_key( + b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" + ) + + print("===== Initializing RFM69 client =====") + print(f"Temperature: {client.get_temperature()}C") + print(f"Frequency: {client.get_frequency_mhz()}mhz") + print(f"Bit rate: {client.get_bitrate() / 1000}kbit/s") + print(f"Frequency deviation: {client.get_frequency_deviation()}hz") + return client + + +def init_neopixel(): + pixel = neopixel.NeoPixel(board.NEOPIXEL, 1) + pixel.brightness = 0.3 + return pixel + + +async def rainbow(pixel, interval=0.020): + while True: + for color_value in range(255): + pixel[0] = rainbowio.colorwheel(color_value) + await asyncio.sleep(interval) + + +async def send_packets(rfm69_client, interval=1.0): + rfm69_client.address = SRC_ADDRESS + + x = 0 + while True: + message = f"aio {rfm69_client.address}: {x}" + ack_packet = await rfm69_client.aio_send_with_ack( + DEST_ADDRESS, message.encode("utf-8") + ) + if ack_packet is not None: + assert DEST_ADDRESS == ack_packet.src + print( + "[{}] TX {} ({} db) | {} ".format( + int(ticks_ms() / 1000.0), DEST_ADDRESS, ack_packet.rssi, message + ) + ) + else: + print( + "[{}] TX {} (!! db) | {} ".format( + int(ticks_ms() / 1000.0), DEST_ADDRESS, message + ) + ) + + x += 1 + await asyncio.sleep(interval) + + +async def read_packets(rfm69_client, interval=5.0): + rfm69_client.address = DEST_ADDRESS + + while True: + current_packet = await rfm69_client.aio_recv_with_ack(timeout=interval) + if current_packet is None: + print("[{}] RX !!".format(int(ticks_ms() / 1000.0))) + continue + + print( + "[{}] RX {} ({} dB) | {}".format( + int(current_packet.time), + current_packet.src, + current_packet.rssi, + current_packet.data.decode("utf-8"), + ) + ) + + +async def main(): + pixel = init_neopixel() + rfm69_client = init_client() + + coros = [rainbow(pixel)] + if SRC_ADDRESS == 100: + coros.append(send_packets(rfm69_client)) + elif SRC_ADDRESS == 200: + coros.append(read_packets(rfm69_client)) + + await asyncio.gather(*[asyncio.create_task(current_coro) for current_coro in coros]) + + +asyncio.run(main()) diff --git a/examples/rfm69_header.py b/examples/rfm69_header.py deleted file mode 100644 index 973fe85..0000000 --- a/examples/rfm69_header.py +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries -# SPDX-License-Identifier: MIT - -# Example to display raw packets including header - -# -import board -import busio -import digitalio -import adafruit_rfm69 - -# set the time interval (seconds) for sending packets -transmit_interval = 10 - -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. - -# Define pins connected to the chip. -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) - -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) - -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) - -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) - -# Wait to receive packets. -print("Waiting for packets...") -# initialize flag and timer -while True: - # Look for a new packet: only accept if addresses to my_node - packet = rfm69.receive(with_header=True) - # If no packet was received during the timeout then None is returned. - if packet is not None: - # Received a packet! - # Print out the raw bytes of the packet: - print("Received (raw header):", [hex(x) for x in packet[0:4]]) - print("Received (raw payload): {0}".format(packet[4:])) - print("RSSI: {0}".format(rfm69.last_rssi)) - # send reading after any packet received diff --git a/examples/rfm69_node1.py b/examples/rfm69_node1.py deleted file mode 100644 index e956c25..0000000 --- a/examples/rfm69_node1.py +++ /dev/null @@ -1,68 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries -# SPDX-License-Identifier: MIT - -# Example to send a packet periodically between addressed nodes - -import time -import board -import busio -import digitalio -import adafruit_rfm69 - - -# set the time interval (seconds) for sending packets -transmit_interval = 10 - -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. - -# Define pins connected to the chip. -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) - -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) - -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) - -# set node addresses -rfm69.node = 1 -rfm69.destination = 2 -# initialize counter -counter = 0 -# send a broadcast message from my_node with ID = counter -rfm69.send( - bytes("Startup message {} from node {}".format(counter, rfm69.node), "UTF-8") -) - -# Wait to receive packets. -print("Waiting for packets...") -now = time.monotonic() -while True: - # Look for a new packet: only accept if addresses to my_node - packet = rfm69.receive(with_header=True) - # If no packet was received during the timeout then None is returned. - if packet is not None: - # Received a packet! - # Print out the raw bytes of the packet: - print("Received (raw header):", [hex(x) for x in packet[0:4]]) - print("Received (raw payload): {0}".format(packet[4:])) - print("Received RSSI: {0}".format(rfm69.last_rssi)) - if time.monotonic() - now > transmit_interval: - now = time.monotonic() - counter = counter + 1 - # send a mesage to destination_node from my_node - rfm69.send( - bytes( - "message number {} from node {}".format(counter, rfm69.node), "UTF-8" - ), - keep_listening=True, - ) - button_pressed = None diff --git a/examples/rfm69_node1_ack.py b/examples/rfm69_node1_ack.py deleted file mode 100644 index f5c6b3a..0000000 --- a/examples/rfm69_node1_ack.py +++ /dev/null @@ -1,70 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries -# SPDX-License-Identifier: MIT - -# Example to send a packet periodically between addressed nodes with ACK - -import time -import board -import busio -import digitalio -import adafruit_rfm69 - -# set the time interval (seconds) for sending packets -transmit_interval = 10 - -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. - -# Define pins connected to the chip. -# set GPIO pins as necessary -- this example is for Raspberry Pi -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) - -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) - -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) - -# set delay before sending ACK -rfm69.ack_delay = 0.1 -# set node addresses -rfm69.node = 1 -rfm69.destination = 2 -# initialize counter -counter = 0 -ack_failed_counter = 0 -# send startup message from my_node -rfm69.send_with_ack(bytes("startup message from node {}".format(rfm69.node), "UTF-8")) - -# Wait to receive packets. -print("Waiting for packets...") -# initialize flag and timer -time_now = time.monotonic() -while True: - # Look for a new packet: only accept if addresses to my_node - packet = rfm69.receive(with_ack=True, with_header=True) - # If no packet was received during the timeout then None is returned. - if packet is not None: - # Received a packet! - # Print out the raw bytes of the packet: - print("Received (raw header):", [hex(x) for x in packet[0:4]]) - print("Received (raw payload): {0}".format(packet[4:])) - print("RSSI: {0}".format(rfm69.last_rssi)) - # send reading after any packet received - if time.monotonic() - time_now > transmit_interval: - # reset timeer - time_now = time.monotonic() - counter += 1 - # send a mesage to destination_node from my_node - if not rfm69.send_with_ack( - bytes("message from node node {} {}".format(rfm69.node, counter), "UTF-8") - ): - ack_failed_counter += 1 - print(" No Ack: ", counter, ack_failed_counter) diff --git a/examples/rfm69_node1_bonnet.py b/examples/rfm69_node1_bonnet.py index dbc3770..f3c3d29 100644 --- a/examples/rfm69_node1_bonnet.py +++ b/examples/rfm69_node1_bonnet.py @@ -39,25 +39,44 @@ height = display.height -# set the time interval (seconds) for sending packets -transmit_interval = 10 - # Define radio parameters. RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your # module! Can be a value like 915.0, 433.0, etc. # Define pins connected to the chip. -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) +RFM69_IRQ = board.D9 +RFM69_CS = board.CE1 +RFM69_RESET = board.D25 + + +def print_packet(packet): + if not packet: + return + + # Received a packet! + # Print out the raw bytes of the packet: + print(f"Received (dest): {hex(packet.dest)}") + print(f"Received (src): {hex(packet.src)}") + print(f"Received (sequence_number): {hex(packet.sequence_number)}") + print(f"Received (flags): {hex(packet.flags)}") -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) + print(f"Received (raw payload): {packet.data}") + print(f"RSSI: {packet.rssi}") + print(f"Time: {packet.time}") + + +# Initialize SPI device. +in_spi = board.SPI() +in_spi_device = adafruit_rfm69.RFM69.spi_device(in_spi, RFM69_CS) # Initialze RFM radio + # Attempt to set up the RFM69 Module try: - rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) + rfm69_client = adafruit_rfm69.RFM69( + in_spi_device, RFM69_IRQ, RFM69_RESET, RADIO_FREQ_MHZ + ) display.text("RFM69: Detected", 0, 0, 1) except RuntimeError: # Thrown on version mismatch @@ -68,18 +87,19 @@ # Optionally set an encryption key (16 byte AES key). MUST match both # on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( +rfm69_client.set_encryption_key( b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" ) # set node addresses -rfm69.node = 1 -rfm69.destination = 2 +rfm69_client.address = 1 +destination = 2 # initialize counter counter = 0 # send a broadcast message from my_node with ID = counter -rfm69.send( - bytes("Startup message {} from node {}".format(counter, rfm69.node), "UTF-8") +rfm69_client.send( + destination, + bytes(f"Startup message {counter} from node {rfm69_client.address}", "UTF-8"), ) # Wait to receive packets. @@ -87,14 +107,12 @@ button_pressed = None while True: # Look for a new packet: only accept if addresses to my_node - packet = rfm69.receive(with_header=True) + rx_packet = rfm69_client.recv() # If no packet was received during the timeout then None is returned. - if packet is not None: + if rx_packet is not None: # Received a packet! # Print out the raw bytes of the packet: - print("Received (raw header):", [hex(x) for x in packet[0:4]]) - print("Received (raw payload): {0}".format(packet[4:])) - print("Received RSSI: {0}".format(rfm69.last_rssi)) + print_packet(rx_packet) # Check buttons if not btnA.value: button_pressed = "A" @@ -118,13 +136,11 @@ if button_pressed is not None: counter = counter + 1 # send a mesage to destination_node from my_node - rfm69.send( + rfm69_client.send( + destination, bytes( - "message number {} from node {} button {}".format( - counter, rfm69.node, button_pressed - ), + f"msg {counter} from node {rfm69_client.address} button {button_pressed}", "UTF-8", ), - keep_listening=True, ) button_pressed = None diff --git a/examples/rfm69_node2.py b/examples/rfm69_node2.py deleted file mode 100644 index 9d506eb..0000000 --- a/examples/rfm69_node2.py +++ /dev/null @@ -1,66 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries -# SPDX-License-Identifier: MIT - -# Example to send a packet periodically between addressed nodes - -import time -import board -import busio -import digitalio -import adafruit_rfm69 - -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. - -# Define pins connected to the chip. -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) - -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) - -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) - -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) - -# set node addresses -rfm69.node = 2 -rfm69.destination = 1 -# initialize counter -counter = 0 -# send a broadcast message from my_node with ID = counter -rfm69.send(bytes("startup message from node {} ".format(rfm69.node), "UTF-8")) - -# Wait to receive packets. -print("Waiting for packets...") -# initialize flag and timer -time_now = time.monotonic() -while True: - # Look for a new packet: only accept if addresses to my_node - packet = rfm69.receive(with_header=True) - # If no packet was received during the timeout then None is returned. - if packet is not None: - # Received a packet! - # Print out the raw bytes of the packet: - print("Received (raw header):", [hex(x) for x in packet[0:4]]) - print("Received (raw payload): {0}".format(packet[4:])) - print("Received RSSI: {0}".format(rfm69.last_rssi)) - # send reading after any packet received - counter = counter + 1 - # after 10 messages send a response to destination_node from my_node with ID = counter&0xff - if counter % 10 == 0: - time.sleep(0.5) # brief delay before responding - rfm69.identifier = counter & 0xFF - rfm69.send( - bytes( - "message number {} from node {} ".format(counter, rfm69.node), - "UTF-8", - ), - keep_listening=True, - ) diff --git a/examples/rfm69_node2_ack.py b/examples/rfm69_node2_ack.py deleted file mode 100644 index 4a72d3a..0000000 --- a/examples/rfm69_node2_ack.py +++ /dev/null @@ -1,61 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries -# SPDX-License-Identifier: MIT - -# Example to receive addressed packed with ACK and send a response - -import time -import board -import busio -import digitalio -import adafruit_rfm69 - -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. - -# Define pins connected to the chip. -# set GPIO pins as necessary - this example is for Raspberry Pi -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) - -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) - -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) - -# set delay before transmitting ACK (seconds) -rfm69.ack_delay = 0.1 -# set node addresses -rfm69.node = 2 -rfm69.destination = 1 -# initialize counter -counter = 0 -ack_failed_counter = 0 - -# Wait to receive packets. -print("Waiting for packets...") -while True: - # Look for a new packet: only accept if addresses to my_node - packet = rfm69.receive(with_ack=True, with_header=True) - # If no packet was received during the timeout then None is returned. - if packet is not None: - # Received a packet! - # Print out the raw bytes of the packet: - print("Received (raw header):", [hex(x) for x in packet[0:4]]) - print("Received (raw payload): {0}".format(packet[4:])) - print("RSSI: {0}".format(rfm69.last_rssi)) - # send response 2 sec after any packet received - time.sleep(2) - counter += 1 - # send a mesage to destination_node from my_node - if not rfm69.send_with_ack( - bytes("response from node {} {}".format(rfm69.node, counter), "UTF-8") - ): - ack_failed_counter += 1 - print(" No Ack: ", counter, ack_failed_counter) diff --git a/examples/rfm69_rpi_interrupt.py b/examples/rfm69_rpi_interrupt.py index 7f7c6a5..719f101 100644 --- a/examples/rfm69_rpi_interrupt.py +++ b/examples/rfm69_rpi_interrupt.py @@ -7,74 +7,87 @@ # CircuitPython does not support interrupts so it will not work on Circutpython boards import time import board -import busio -import digitalio import RPi.GPIO as io import adafruit_rfm69 +# Define radio parameters. +RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your +# module! Can be a value like 915.0, 433.0, etc. -# setup interrupt callback function -def rfm69_callback(rfm69_irq): - global packet_received # pylint: disable=global-statement - print( - "IRQ detected on pin {0} payload_ready {1} ".format( - rfm69_irq, rfm69.payload_ready - ) +# Define pins connected to the chip. +RFM69_IRQ = 22 +RFM69_CS = board.CE1 +RFM69_RESET = board.D25 + +SRC_ADDRESS = adafruit_rfm69.RH_BROADCAST_ADDRESS +DEST_ADDRESS = adafruit_rfm69.RH_BROADCAST_ADDRESS + + +def init_client(): + # Initialize SPI device. + in_spi = board.SPI() + in_spi_device = adafruit_rfm69.RFM69.spi_device(in_spi, RFM69_CS) + + # Initialze RFM radio + client = adafruit_rfm69.RFM69(in_spi_device, RFM69_IRQ, RFM69_RESET, RADIO_FREQ_MHZ) + client.address = SRC_ADDRESS + + # Optionally set an encryption key (16 byte AES key). MUST match both + # on the transmitter and receiver (or be set to None to disable/the default). + client.set_encryption_key( + b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" ) - # see if this was a payload_ready interrupt ignore if not - if rfm69.payload_ready: - packet = rfm69.receive(timeout=None) - if packet is not None: - # Received a packet! - packet_received = True - # Print out the raw bytes of the packet: - print("Received (raw bytes): {0}".format(packet)) - print([hex(x) for x in packet]) - print("RSSI: {0}".format(rfm69.last_rssi)) + print("===== Initializing RFM69 client =====") + print(f"Temperature: {client.get_temperature()}C") + print(f"Frequency: {client.get_frequency_mhz()}mhz") + print(f"Bit rate: {client.get_bitrate() / 1000}kbit/s") + print(f"Frequency deviation: {client.get_frequency_deviation()}hz") + return client -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. -# Define pins connected to the chip, use these if wiring up the breakout according to the guide: -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) +def print_packet(packet): + if not packet: + return + + # Received a packet! + # Print out the raw bytes of the packet: + print(f"Received (dest): {hex(packet.dest)}") + print(f"Received (src): {hex(packet.src)}") + print(f"Received (sequence_number): {hex(packet.sequence_number)}") + print(f"Received (flags): {hex(packet.flags)}") + + print(f"Received (raw payload): {packet.data}") + print(f"RSSI: {packet.rssi}") + print(f"Time: {packet.time}") -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) +rfm69_client = init_client() -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) +# Magic incantation that tells us we can support interrupts +rfm69_client.platform_supports_interrupts = True + + +# setup interrupt callback function +def rfm69_callback(rfm69_irq): + print(f"IRQ detected on pin {rfm69_irq}") + rfm69_client.handle_interrupt() -# Print out some chip state: -print("Temperature: {0}C".format(rfm69.temperature)) -print("Frequency: {0}mhz".format(rfm69.frequency_mhz)) -print("Bit rate: {0}kbit/s".format(rfm69.bitrate / 1000)) -print("Frequency deviation: {0}hz".format(rfm69.frequency_deviation)) # configure the interrupt pin and event handling. -RFM69_G0 = 22 io.setmode(io.BCM) -io.setup(RFM69_G0, io.IN, pull_up_down=io.PUD_DOWN) # activate input -io.add_event_detect(RFM69_G0, io.RISING) -io.add_event_callback(RFM69_G0, rfm69_callback) -packet_received = False +io.setup(RFM69_IRQ, io.IN, pull_up_down=io.PUD_DOWN) # activate input +io.add_event_detect(RFM69_IRQ, io.RISING) +io.add_event_callback(RFM69_IRQ, rfm69_callback) # Send a packet. Note you can only send a packet up to 60 bytes in length. # This is a limitation of the radio packet size, so if you need to send larger # amounts of data you will need to break it into smaller send calls. Each send # call will wait for the previous one to finish before continuing. -rfm69.send(bytes("Hello world!\r\n", "utf-8"), keep_listening=True) +rfm69_client.send(DEST_ADDRESS, bytes("Hello world!\r\n", "utf-8")) print("Sent hello world message!") -# If you don't wawnt to send a message to start you can just start lintening -# rmf69.listen() +# Just start listening +rfm69_client.available() # Wait to receive packets. Note that this library can't receive data at a fast # rate, in fact it can only receive and process one 60 byte packet at a time. @@ -85,9 +98,10 @@ def rfm69_callback(rfm69_irq): # the loop is where you can do any desire processing # the global variable packet_received can be used to determine if a packet was received. while True: - # the sleep time is arbitrary since any incomming packe will trigger an interrupt + # the sleep time is arbitrary since any incoming packet will trigger an interrupt # and be received. time.sleep(0.1) - if packet_received: + if rfm69_client.rx_packet: print("received message!") - packet_received = False + print_packet(rfm69_client.rx_packet) + rfm69_client.rx_packet = None diff --git a/examples/rfm69_rpi_simpletest.py b/examples/rfm69_rpi_simpletest.py deleted file mode 100644 index 333afb2..0000000 --- a/examples/rfm69_rpi_simpletest.py +++ /dev/null @@ -1,75 +0,0 @@ -# SPDX-FileCopyrightText: 2018 Tony DiCola for Adafruit Industries -# SPDX-License-Identifier: MIT - -# Simple example to send a message and then wait indefinitely for messages -# to be received. This uses the default RadioHead compatible GFSK_Rb250_Fd250 -# modulation and packet format for the radio. - -import board -import busio -import digitalio - -import adafruit_rfm69 - - -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. - -# Define pins connected to the chip, use these if wiring up the breakout according to the guide: -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) -# Or uncomment and instead use these if using a Feather M0 RFM69 board -# and the appropriate CircuitPython build: -# CS = digitalio.DigitalInOut(board.RFM69_CS) -# RESET = digitalio.DigitalInOut(board.RFM69_RST) - - -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) - -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) - -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) - -# Print out some chip state: -print("Temperature: {0}C".format(rfm69.temperature)) -print("Frequency: {0}mhz".format(rfm69.frequency_mhz)) -print("Bit rate: {0}kbit/s".format(rfm69.bitrate / 1000)) -print("Frequency deviation: {0}hz".format(rfm69.frequency_deviation)) - -# Send a packet. Note you can only send a packet up to 60 bytes in length. -# This is a limitation of the radio packet size, so if you need to send larger -# amounts of data you will need to break it into smaller send calls. Each send -# call will wait for the previous one to finish before continuing. -rfm69.send(bytes("Hello world!\r\n", "utf-8")) -print("Sent hello world message!") - -# Wait to receive packets. Note that this library can't receive data at a fast -# rate, in fact it can only receive and process one 60 byte packet at a time. -# This means you should only use this for low bandwidth scenarios, like sending -# and receiving a single message at a time. -print("Waiting for packets...") -while True: - packet = rfm69.receive() - # Optionally change the receive timeout from its default of 0.5 seconds: - # packet = rfm69.receive(timeout=5.0) - # If no packet was received during the timeout then None is returned. - if packet is None: - # Packet has not been received - print("Received nothing! Listening again...") - else: - # Received a packet! - # Print out the raw bytes of the packet: - print("Received (raw bytes): {0}".format(packet)) - # And decode to ASCII text and print it too. Note that you always - # receive raw bytes and need to convert to a text format like ASCII - # if you intend to do string processing on your data. Make sure the - # sending side is sending ASCII data before you try to decode! - packet_text = str(packet, "ascii") - print("Received (ASCII): {0}".format(packet_text)) diff --git a/examples/rfm69_simpletest.py b/examples/rfm69_simpletest.py deleted file mode 100644 index d160090..0000000 --- a/examples/rfm69_simpletest.py +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-FileCopyrightText: 2018 Tony DiCola for Adafruit Industries -# SPDX-License-Identifier: MIT - -# Simple example to send a message and then wait indefinitely for messages -# to be received. This uses the default RadioHead compatible GFSK_Rb250_Fd250 -# modulation and packet format for the radio. -import board -import busio -import digitalio - -import adafruit_rfm69 - - -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. - -# Define pins connected to the chip, use these if wiring up the breakout according to the guide: -CS = digitalio.DigitalInOut(board.D5) -RESET = digitalio.DigitalInOut(board.D6) -# Or uncomment and instead use these if using a Feather M0 RFM69 board -# and the appropriate CircuitPython build: -# CS = digitalio.DigitalInOut(board.RFM69_CS) -# RESET = digitalio.DigitalInOut(board.RFM69_RST) - -# Define the onboard LED -LED = digitalio.DigitalInOut(board.D13) -LED.direction = digitalio.Direction.OUTPUT - -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) - -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) - -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) - -# Print out some chip state: -print("Temperature: {0}C".format(rfm69.temperature)) -print("Frequency: {0}mhz".format(rfm69.frequency_mhz)) -print("Bit rate: {0}kbit/s".format(rfm69.bitrate / 1000)) -print("Frequency deviation: {0}hz".format(rfm69.frequency_deviation)) - -# Send a packet. Note you can only send a packet up to 60 bytes in length. -# This is a limitation of the radio packet size, so if you need to send larger -# amounts of data you will need to break it into smaller send calls. Each send -# call will wait for the previous one to finish before continuing. -rfm69.send(bytes("Hello world!\r\n", "utf-8")) -print("Sent hello world message!") - -# Wait to receive packets. Note that this library can't receive data at a fast -# rate, in fact it can only receive and process one 60 byte packet at a time. -# This means you should only use this for low bandwidth scenarios, like sending -# and receiving a single message at a time. -print("Waiting for packets...") -while True: - packet = rfm69.receive() - # Optionally change the receive timeout from its default of 0.5 seconds: - # packet = rfm69.receive(timeout=5.0) - # If no packet was received during the timeout then None is returned. - if packet is None: - # Packet has not been received - LED.value = False - print("Received nothing! Listening again...") - else: - # Received a packet! - LED.value = True - # Print out the raw bytes of the packet: - print("Received (raw bytes): {0}".format(packet)) - # And decode to ASCII text and print it too. Note that you always - # receive raw bytes and need to convert to a text format like ASCII - # if you intend to do string processing on your data. Make sure the - # sending side is sending ASCII data before you try to decode! - packet_text = str(packet, "ascii") - print("Received (ASCII): {0}".format(packet_text)) diff --git a/examples/rfm69_throughput.py b/examples/rfm69_throughput.py new file mode 100644 index 0000000..1a9b4df --- /dev/null +++ b/examples/rfm69_throughput.py @@ -0,0 +1,153 @@ +# SPDX-FileCopyrightText: 2023 Matthew Tai +# SPDX-License-Identifier: MIT +""" + Send, no acks [% achieved vs theoretical] + 250000 => 73kbps [29%] + 125000 => 55kbps [44%] + 57600 => 35kbps [60%] + 38400 => 26kbps [67%] + 19200 => 14.8kbps [77%] + + Send, acks [% achieved vs theoretical] + (receiver using this library as well) + ===== + 66 byte packets, 500 packets (Most time in TX) + 250000 => 27kbps [11%] + 125000 => 26kbps [21%] + 57600 => 19kbps [33%] + 38400 => 15kbps [39%] + 19200 => 10kbps [52%] + + 36 byte packets, 500 packets (Mid time in TX) + 250000 => 17kbps [ 7%] + 125000 => 14kbps [11%] + 57600 => 12kbps [21%] + 38400 => 10kbps [26%] + 19200 => 7.5kbps [39%] + + 7 byte packets, 500 packets (Least time in TX, library limited) + 250000 => 3.5kbps [ 1.8%] + 125000 => 3.8kbps [ 3.0%] + 57600 => 3.0kbps [ 5.2%] + 38400 => 2.5kbps [ 6.5%] + 19200 => 2.2kbps [11.4%] +""" + +import asyncio +import board +import keypad +from adafruit_ticks import ticks_ms, ticks_diff, ticks_add, ticks_less +import adafruit_rfm69 + + +SENDER_ADDRESS = 0x10 +LISTENER_ADDRESS = 0x01 + +# Define radio parameters. +RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your +# module! Can be a value like 915.0, 433.0, etc. + +# Define pins connected to the chip, use these if wiring up the breakout according to the guide: +RFM69_RESET = board.D11 +RFM69_CS = board.D10 +RFM69_IRQ = board.D9 + +BITRATES_TO_TEST = [ + adafruit_rfm69.RH_BITRATE_250000, + adafruit_rfm69.RH_BITRATE_125000, + adafruit_rfm69.RH_BITRATE_57600, + adafruit_rfm69.RH_BITRATE_38400, + adafruit_rfm69.RH_BITRATE_19200, +] + + +def init_client(): + # Initialize SPI device. + in_spi = board.SPI() + in_spi_device = adafruit_rfm69.RFM69.spi_device(in_spi, RFM69_CS) + + # Initialze RFM radio + rfm69_client = adafruit_rfm69.RFM69( + in_spi_device, RFM69_IRQ, RFM69_RESET, RADIO_FREQ_MHZ + ) + # Optionally set an encryption key (16 byte AES key). MUST match both + # on the transmitter and receiver (or be set to None to disable/the default). + # rfm69_client.set_encryption_key( + # b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" + # ) + + print("===== Initializing RFM69 client =====") + print(f"Temperature: {rfm69_client.get_temperature()}C") + print(f"Frequency: {rfm69_client.get_frequency_mhz()}mhz") + print(f"Bit rate: {rfm69_client.get_bitrate() / 1000}kbit/s") + print(f"Frequency deviation: {rfm69_client.get_frequency_deviation()}hz") + return rfm69_client + + +async def send_rfm69_packets(rfm69_client, bitrate): + """Continuous transmission of 66 byte packets kbytes on Adafruit ESP32-S3 TFT Feather + Testing demonstrates CPU-bound and/or relatively interrupt processing via Python + """ + rfm69_client.address = SENDER_ADDRESS + rfm69_client.set_modem_config(bitrate) + + x = 0 + message = "@" * 1 + encoded_message = message.encode("utf-8") + ticks_start = ticks_ms() + while x < 500: + await rfm69_client.aio_send_with_ack(LISTENER_ADDRESS, encoded_message) + x += 1 + + ticks_end = ticks_ms() + + bytes_transmitted = (len(message) + 6) * x + time_elapsed = ticks_diff(ticks_end, ticks_start) / 1000.0 + bytes_per_sec = bytes_transmitted / time_elapsed + bits_per_sec = bytes_per_sec * 8 + print(f"SEND {bytes_transmitted} bytes in {time_elapsed} seconds") + print(f"{bitrate} :: {bytes_per_sec} bytes/sec :: {bits_per_sec} bits/sec") + + +async def read_rfm69_packets(rfm69_client, bitrate, interval=5.0): + rfm69_client.address = LISTENER_ADDRESS + rfm69_client.set_modem_config(bitrate) + + while True: + await rfm69_client.aio_recv_with_ack(timeout=interval) + + +def set_bitrate(bitrate_idx): + bitrate_idx_to_select = bitrate_idx % len(BITRATES_TO_TEST) + test_bitrate = BITRATES_TO_TEST[bitrate_idx_to_select] + print(f"Bitrate: {test_bitrate}") + return test_bitrate + + +async def main(): + rfm69_client = init_client() + + print("Select bitrate within next 5 seconds...") + bitrate_idx = 0 + bitrate = set_bitrate(bitrate_idx) + button_keys = keypad.Keys((board.BUTTON,), value_when_pressed=False, pull=True) + + end_ticks = ticks_add(ticks_ms(), 5 * 1000) + while ticks_less(ticks_ms(), end_ticks): + while button_keys.events: + event = button_keys.events.get() + if event.pressed: + bitrate_idx += 1 + bitrate = set_bitrate(bitrate_idx) + + await asyncio.sleep(0.100) + + print(f"Starting test with bitrate {bitrate}") + # Step 1a - Uncomment for sender + await send_rfm69_packets(rfm69_client, bitrate) + + # Step 2b - Uncomment for listener + # await read_rfm69_packets(rfm69_client, bitrate) + + +asyncio.run(main()) diff --git a/examples/rfm69_transmit.py b/examples/rfm69_transmit.py deleted file mode 100644 index 1431b22..0000000 --- a/examples/rfm69_transmit.py +++ /dev/null @@ -1,60 +0,0 @@ -# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries -# SPDX-License-Identifier: MIT - -# Example to send a packet periodically - -import time -import board -import busio -import digitalio -import adafruit_rfm69 - -# set the time interval (seconds) for sending packets -transmit_interval = 10 - -# Define radio parameters. -RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your -# module! Can be a value like 915.0, 433.0, etc. - -# Define pins connected to the chip. -CS = digitalio.DigitalInOut(board.CE1) -RESET = digitalio.DigitalInOut(board.D25) - -# Initialize SPI bus. -spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) - -# Initialze RFM radio -rfm69 = adafruit_rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ) - -# Optionally set an encryption key (16 byte AES key). MUST match both -# on the transmitter and receiver (or be set to None to disable/the default). -rfm69.encryption_key = ( - b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08" -) - -# initialize counter -counter = 0 -# send a broadcast mesage -rfm69.send(bytes("message number {}".format(counter), "UTF-8")) - -# Wait to receive packets. -print("Waiting for packets...") -# initialize flag and timer -send_reading = False -time_now = time.monotonic() -while True: - # Look for a new packet - wait up to 5 seconds: - packet = rfm69.receive(timeout=5.0) - # If no packet was received during the timeout then None is returned. - if packet is not None: - # Received a packet! - # Print out the raw bytes of the packet: - print("Received (raw bytes): {0}".format(packet)) - # send reading after any packet received - if time.monotonic() - time_now > transmit_interval: - # reset timeer - time_now = time.monotonic() - # clear flag to send data - send_reading = False - counter = counter + 1 - rfm69.send(bytes("message number {}".format(counter), "UTF-8")) diff --git a/requirements.txt b/requirements.txt index 76ea39a..8b462e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,7 @@ # SPDX-License-Identifier: Unlicense Adafruit-Blinka +adafruit-circuitpython-asyncio adafruit-circuitpython-busdevice +adafruit-circuitpython-ticks adafruit-circuitpython-typing