Source code for circuitpython_nrf24l01.rf24

# The MIT License (MIT)
#
# Copyright (c) 2017 Damien P. George
# Copyright (c) 2019 Brendan Doherty
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""
rf24 module containing the base class RF24
"""
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/2bndy5/CircuitPython_nRF24L01.git"
import time
from adafruit_bus_device.spi_device import SPIDevice

# nRF24L01 registers
# pylint: disable=bad-whitespace
CONFIG     = 0x00 #: register for configuring IRQ, CRC, PWR & RX/TX roles
EN_AA      = 0x01 #: register for auto-ACK feature. Each bit represents this feature per pipe
EN_RX      = 0x02 #: register to open/close pipes. Each bit represents this feature per pipe
SETUP_AW   = 0x03 #: address width register
SETUP_RETR = 0x04 #: auto-retry count and delay register
RF_CH      = 0x05 #: channel register
RF_SETUP   = 0x06 #: RF Power Amplifier & Data Rate
RX_ADDR    = 0x0a #: RX pipe addresses == [0,5]:[0x0a:0x0f]
RX_PW      = 0x11 #: RX payload widths on pipes == [0,5]:[0x11,0x16]
FIFO       = 0x17 #: register containing info on both RX/TX FIFOs + re-use payload flag
DYNPD      = 0x1c #: dynamic payloads feature. Each bit represents this feature per pipe
FEATURE    = 0x1d #: global flags for dynamic payloads, custom ACK payloads, & Ask no ACK
TX_ADDR    = 0x10 #: Address that is used for TX transmissions
# pylint: enable=bad-whitespace

[docs]class RF24: """A driver class for the nRF24L01(+) transceiver radios. This class aims to be compatible with other devices in the nRF24xxx product line that implement the Nordic proprietary Enhanced ShockBurst Protocol (and/or the legacy ShockBurst Protocol), but officially only supports (through testing) the nRF24L01 and nRF24L01+ devices. :param ~busio.SPI spi: The object for the SPI bus that the nRF24L01 is connected to. .. tip:: This object is meant to be shared amongst other driver classes (like adafruit_mcp3xxx.mcp3008 for example) that use the same SPI bus. Otherwise, multiple devices on the same SPI bus with different spi objects may produce errors or undesirable behavior. :param ~digitalio.DigitalInOut csn: The digital output pin that is connected to the nRF24L01's CSN (Chip Select Not) pin. This is required. :param ~digitalio.DigitalInOut ce: The digital output pin that is connected to the nRF24L01's CE (Chip Enable) pin. This is required. :param int channel: This is used to specify a certain radio frequency that the nRF24L01 uses. Defaults to 76 and can be changed at any time by using the `channel` attribute. :param int payload_length: This is the length (in bytes) of a single payload to be transmitted or received. This is ignored if the `dynamic_payloads` attribute is enabled. Defaults to 32 and must be in range [1,32]. This can be changed at any time by using the `payload_length` attribute. :param int address_length: This is the length (in bytes) of the addresses that are assigned to the data pipes for transmitting/receiving. Defaults to 5 and must be in range [3,5]. This can be changed at any time by using the `address_length` attribute. :param int ard: This specifies the delay time (in µs) between attempts to automatically re-transmit. This can be changed at any time by using the `ard` attribute. This parameter must be a multiple of 250 in the range [250,4000]. Defualts to 1500 µs. :param int arc: This specifies the automatic re-transmit count (maximum number of automatically attempts to re-transmit). This can be changed at any time by using the `arc` attribute. This parameter must be in the range [0,15]. Defaults to 3. :param int crc: This parameter controls the CRC setting of transmitted packets. Options are ``0`` (off), ``1`` or ``2`` (byte long CRC enabled). This can be changed at any time by using the `crc` attribute. Defaults to 2. :param int data_rate: This parameter controls the RF data rate setting of transmissions. Options are ``1`` (Mbps), ``2`` (Mbps), or ``250`` (Kbps). This can be changed at any time by using the `data_rate` attribute. Defaults to 1. :param int pa_level: This parameter controls the RF power amplifier setting of transmissions. Options are ``0`` (dBm), ``-6`` (dBm), ``-12`` (dBm), or ``-18`` (dBm). This can be changed at any time by using the `pa_level` attribute. Defaults to 0. :param bool dynamic_payloads: This parameter enables/disables the dynamic payload length feature of the nRF24L01. Defaults to enabled. This can be changed at any time by using the `dynamic_payloads` attribute. :param bool auto_ack: This parameter enables/disables the automatic acknowledgment (ACK) feature of the nRF24L01. Defaults to enabled if `dynamic_payloads` is enabled. This can be changed at any time by using the `auto_ack` attribute. :param bool ask_no_ack: This represents a special flag that has to be thrown to enable a feature specific to individual payloads. Setting this parameter only enables access to this feature; it does not invoke it (see parameters for `send()` or `write()` functions). Enabling/Disabling this does not affect `auto_ack` attribute. :param bool ack: This represents a special flag that has to be thrown to enable a feature allowing custom response payloads appended to the ACK packets. Enabling this also requires the `auto_ack` attribute enabled. This can be changed at any time by using the `ack` attribute. :param bool irq_DR: When "Data is Ready", this configures the interrupt (IRQ) trigger of the nRF24L01's IRQ pin (active low). Defaults to enabled. This can be changed at any time by using the `interrupt_config()` function. :param bool irq_DS: When "Data is Sent", this configures the interrupt (IRQ) trigger of the nRF24L01's IRQ pin (active low). Defaults to enabled. This can be changed at any time by using the `interrupt_config()` function. :param bool irq_DF: When "max retry attempts are reached" (specified by the `arc` attribute), this configures the interrupt (IRQ) trigger of the nRF24L01's IRQ pin (active low) and represents transmission failure. Defaults to enabled. This can be changed at any time by using the `interrupt_config()` function. """ def __init__(self, spi, csn, ce, channel=76, payload_length=32, address_length=5, ard=1500, arc=3, crc=2, data_rate=1, pa_level=0, dynamic_payloads=True, auto_ack=True, ask_no_ack=True, ack=False, irq_DR=True, irq_DS=True, irq_DF=True): self._payload_length = payload_length # inits internal attribute self.payload_length = payload_length # last address assigned to pipe0 for reading. init to None self._fifo = 0 self._status = 0 # init shadow copy of RX addresses for all pipes self._pipes = [b'', b'', 0, 0, 0, 0] for i in range(6): # set all pipe's RX addresses to reset value if i < 2: if not i: self._pipes[i] = b'\xe7' * address_length else: self._pipes[i] = b'\xc2' * address_length else: self._pipes[i] = 0xc1 + i self._tx_address = self._pipes[0] # shadow copy of the TX_ADDR self._payload_widths = [0, 0, 0, 0, 0, 0] # payload_length specific to each pipe # shadow copy of last RX_ADDR written to pipe 0 self._pipe0_read_addr = None # needed as open_tx_pipe() appropriates pipe 0 for ACK # init the _open_pipes attribute (reflects only RX state on each pipe) self._open_pipes = 0 # <- means all pipes closed # init the SPI bus and pins self.spi = SPIDevice(spi, chip_select=csn, baudrate=1250000) # store the ce pin self.ce = ce # reset ce.value & disable the chip comms self.ce.switch_to_output(value=False) # if radio is powered up and CE is LOW: standby-I mode # if radio is powered up and CE is HIGH: standby-II mode # NOTE per spec sheet: nRF24L01+ must be in a standby or power down mode before writing # to the configuration register # configure the CONFIG register:IRQ(s) config, setup CRC feature, and trigger standby-I & # TX mode (the "| 2") if 0 <= crc <= 2: self._config = ((not irq_DR) << 6) | ((not irq_DS) << 5) | ((not irq_DF) << 4) | \ ((crc + 1) << 2 if crc else 0) | 2 self._reg_write(CONFIG, self._config) # dump to register else: raise ValueError( "CRC byte length must be an int equal to 0 (off), 1, or 2") # check for device presence by verifying nRF24L01 is in TX + standby-I mode if self._reg_read(CONFIG) & 3 == 2: # if in TX + standby-I mode self.power = False # power down else: # hardware presence check NOT passed print(bin(self._reg_read(CONFIG))) raise RuntimeError("nRF24L01 Hardware not responding") # configure the SETUP_RETR register if 250 <= ard <= 4000 and ard % 250 == 0 and 0 <= arc <= 15: self._setup_retr = (int((ard - 250) / 250) << 4) | arc else: raise ValueError("automatic re-transmit delay can only be a multiple of 250 in range " "[250,4000]\nautomatic re-transmit count(/attempts) must range " "[0,15]") # configure the RF_SETUP register if data_rate in (1, 2, 250) and pa_level in (-18, -12, -6, 0): data_rate = 0 if data_rate == 1 else ( 8 if data_rate == 2 else 0x20) pa_level = (3 - int(pa_level / -6)) * 2 self._rf_setup = data_rate | pa_level else: raise ValueError("data rate must be one of the following ([M,M,K]bps): 1, 2, 250" "\npower amplifier must be one of the following (dBm): -18, -12," " -6, 0") # manage dynamic_payloads, auto_ack, and ack features self._dyn_pl = 0x3F if dynamic_payloads else 0 # 0x3F == enabled on all pipes self._aa = 0x3F if auto_ack else 0 # 0x3F == enabled on all pipes self._features = (dynamic_payloads << 2) | ((ack if auto_ack and dynamic_payloads else False) << 1) | ask_no_ack # init the last few singleton attribute self._channel = channel self._addr_len = address_length with self: # write to registers & power up # using __enter__() configures all virtual features and settings to the hardware # registers self.ce.value = 0 # ensure standby-I mode to write to CONFIG register self._reg_write(CONFIG, self._config | 1) # enable RX mode time.sleep(0.000015) # wait time for transitioning modes RX/TX self.flush_rx() # spec sheet say "used in RX mode" self._reg_write(CONFIG, self._config & 0xC) # power down + TX mode time.sleep(0.000015) # wait time for transitioning modes RX/TX self.flush_tx() # spec sheet say "used in TX mode" self.clear_status_flags() # writes directly to STATUS register def __enter__(self): # dump IRQ and CRC data to CONFIG register self._reg_write(CONFIG, self._config & 0x7C) self._reg_write(RF_SETUP, self._rf_setup) # dump to RF_SETUP register # dump open/close pipe status to EN_RXADDR register (for all pipes) self._reg_write(EN_RX, self._open_pipes) self._reg_write(DYNPD, self._dyn_pl) # dump to DYNPD register self._reg_write(EN_AA, self._aa) # dump to EN_AA register self._reg_write(FEATURE, self._features) # dump to FEATURE register # dump to SETUP_RETR register self._reg_write(SETUP_RETR, self._setup_retr) # dump pipes' RX addresses and static payload lengths for i, address in enumerate(self._pipes): if i < 2: self._reg_write_bytes(RX_ADDR + i, address) else: self._reg_write(RX_ADDR + i, address) self._reg_write(RX_PW + i, self._payload_widths[i]) # dump last used TX address self._reg_write_bytes(TX_ADDR, self._tx_address) self.address_length = self._addr_len # writes directly to SETUP_AW register self.channel = self._channel # writes directly to RF_CH register return self def __exit__(self, *exc): return False # pylint: disable=no-member def _reg_read(self, reg): buf = bytearray(2) # 2 = 1 status byte + 1 byte of returned content with self.spi as spi: time.sleep(0.005) # time for CSN to settle spi.readinto(buf, write_value=reg) self._status = buf[0] # save status byte return buf[1] # drop status byte and return the rest def _reg_read_bytes(self, reg, buf_len=5): # allow an extra byte for status data buf = bytearray(buf_len + 1) with self.spi as spi: time.sleep(0.005) # time for CSN to settle spi.readinto(buf, write_value=reg) self._status = buf[0] # save status byte return buf[1:] # drop status byte and return the rest def _reg_write_bytes(self, reg, out_buf): out_buf = bytes([0x20 | reg]) + out_buf in_buf = bytearray(len(out_buf)) with self.spi as spi: time.sleep(0.005) # time for CSN to settle spi.write_readinto(out_buf, in_buf) self._status = in_buf[0] # save status byte def _reg_write(self, reg, value=None): if value is None: out_buf = bytes([reg]) else: out_buf = bytes([0x20 | reg, value]) in_buf = bytearray(len(out_buf)) with self.spi as spi: time.sleep(0.005) # time for CSN to settle spi.write_readinto(out_buf, in_buf) self._status = in_buf[0] # save status byte # pylint: enable=no-member @property def address_length(self): """This `int` attribute specifies the length (in bytes) of addresses to be used for RX/TX pipes. The addresses assigned to the data pipes must have byte length equal to the value set for this attribute. A valid input value must be an `int` in range [3,5]. Otherwise a `ValueError` exception is thrown. Default is set to the nRF24L01's maximum of 5. """ return self._reg_read(SETUP_AW) + 2 @address_length.setter def address_length(self, length): # nRF24L01+ must be in a standby or power down mode before writing to the configuration # registers. if 3 <= length <= 5: # address width is saved in 2 bits making range = [3,5] self._addr_len = int(length) self._reg_write(SETUP_AW, length - 2) else: raise ValueError( "address length can only be set in range [3,5] bytes")
[docs] def open_tx_pipe(self, address): """This function is used to open a data pipe for OTA (over the air) TX transmissions. :param bytearray address: The virtual address of the receiving nRF24L01. This must have a length equal to the `address_length` attribute (see `address_length` attribute). Otherwise a `ValueError` exception is thrown. The address specified here must match the address set to one of the RX data pipes of the receiving nRF24L01. .. note:: There is no option to specify which data pipe to use because the nRF24L01 only uses data pipe 0 in TX mode. Additionally, the nRF24L01 uses the same data pipe (pipe 0) for receiving acknowledgement (ACK) packets in TX mode when the `auto_ack` attribute is enabled. Thus, RX pipe 0 is appropriated with the TX address (specified here) when `auto_ack` is set to `True`. """ if len(address) == self.address_length: # if auto_ack == True, then use this TX address as the RX address for ACK if self.auto_ack: # settings need to match on both transceivers: dynamic_payloads and payload_length self._pipes[0] = address self._reg_write_bytes(RX_ADDR, address) # using pipe 0 self._open_pipes = self._open_pipes | 1 # open pipe 0 for RX-ing ACK self._reg_write(EN_RX, self._open_pipes) self._payload_widths[0] = self.payload_length self._reg_write(RX_PW, self.payload_length) # set expected payload_length self._pipes[0] = address # update the context as well self._tx_address = address self._reg_write_bytes(TX_ADDR, address) else: raise ValueError("address must be a buffer protocol object with a byte length\nequal " "to the address_length attribute (currently set to" " {})".format(self.address_length))
[docs] def close_rx_pipe(self, pipe_number, reset=True): """This function is used to close a specific data pipe from OTA (over the air) RX transmissions. :param int pipe_number: The data pipe to use for RX transactions. This must be in range [0,5]. Otherwise a `ValueError` exception is thrown. :param bool reset: `True` resets the address for the specified ``pipe_number`` to the factory address (different for each pipe). `False` leaves the address on the specified ``pipe_number`` alone. Be aware that the addresses will remain despite loss of power. """ if pipe_number < 0 or pipe_number > 5: raise ValueError("pipe number must be in range [0,5]") self._open_pipes = self._reg_read(EN_RX) # refresh data if reset:# reset pipe address accordingly if not pipe_number: # NOTE this does not clear the shadow copy (pipe0_read_addr) of address for pipe 0 self._reg_write_bytes(pipe_number + RX_ADDR, b'\xe7' * 5) self._pipes[pipe_number] = b'\xe7' * 5 elif pipe_number == 1: # write the full address for pipe 1 self._reg_write_bytes(pipe_number + RX_ADDR, b'\xc2' * 5) self._pipes[pipe_number] = b'\xc2' * 5 else: # write just MSB for 2 <= pipes <= 5 self._reg_write(pipe_number + RX_ADDR, pipe_number + 0xc1) self._pipes[pipe_number] = pipe_number + 0xc1 # disable the specified data pipe if not already if self._open_pipes & (1 << pipe_number): self._open_pipes = self._open_pipes & ~(1 << pipe_number) self._reg_write(EN_RX, self._open_pipes)
[docs] def open_rx_pipe(self, pipe_number, address): """This function is used to open a specific data pipe for OTA (over the air) RX transmissions. If `dynamic_payloads` attribute is `False`, then the `payload_length` attribute is used to specify the expected length of the RX payload on the specified data pipe. :param int pipe_number: The data pipe to use for RX transactions. This must be in range [0,5]. Otherwise a `ValueError` exception is thrown. :param bytearray address: The virtual address to the receiving nRF24L01. This must have a byte length equal to the `address_length` attribute. Otherwise a `ValueError` exception is thrown. If using a ``pipe_number`` greater than 1, then only the MSByte of the address is written, so make sure MSByte (first character) is unique among other simultaneously receiving addresses). .. note:: The nRF24L01 shares the addresses' LSBytes (address[1:5]) on data pipes 2 through 5. These shared LSBytes are determined by the address set to pipe 1. """ if pipe_number < 0 or pipe_number > 5: raise ValueError("pipe number must be in range [0,5]") if len(address) != self.address_length: raise ValueError("address must be a buffer protocol object with a byte length\nequal " "to the address_length attribute (currently set to " "{})".format(self.address_length)) # write the address if pipe_number < 2: # write entire address if pipe_number is 0 or 1 if not pipe_number: # save shadow copy of address if target pipe_number is 0. This is done to help # ensure the proper address is set to pipe 0 via _start_listening() as # open_tx_pipe() will appropriate the address on pipe 0 if auto_ack is enabled for # TX mode self._pipe0_read_addr = address self._pipes[pipe_number] = address self._reg_write_bytes(RX_ADDR + pipe_number, address) else: # only write MSByte if pipe_number is not 0 or 1 self._pipes[pipe_number] = address[0] self._reg_write(RX_ADDR + pipe_number, address[0]) # now manage the pipe self._open_pipes = self._reg_read(EN_RX) # refresh data # enable the specified data pipe self._open_pipes = self._open_pipes | (1 << pipe_number) self._reg_write(EN_RX, self._open_pipes) # now adjust payload_length accordingly despite dynamic_payload setting # radio only uses this info in RX mode when dynamic_payloads == True self._reg_write(RX_PW + pipe_number, self.payload_length) self._payload_widths[pipe_number] = self.payload_length
@property def listen(self): """An attribute to represent the nRF24L01 primary role as a radio. Setting this attribute incorporates the proper transitioning to/from RX mode as it involves playing with the `power` attribute and the nRF24L01's CE pin. This attribute does not power down the nRF24L01, but will power it up when needed; use `power` attribute set to `False` to put the nRF24L01 to sleep. A valid input value is a `bool` in which: `True` enables RX mode. Additionally, per `Appendix B of the nRF24L01+ Specifications Sheet <https://www.sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1091756>`_, this attribute flushes the RX FIFO, clears the `irq_DR` status flag, and puts nRF24L01 in power up mode. Notice the CE pin is be held HIGH during RX mode. `False` disables RX mode. As mentioned in above link, this puts nRF24L01's power in Standby-I (CE pin is LOW meaning low current & no transmissions) mode which is ideal for post-reception work. Disabing RX mode doesn't flush the RX/TX FIFO buffers, so remember to flush your 3-level FIFO buffers when appropriate using `flush_tx()` or `flush_rx()` (see also the `recv()` function). """ return self.power and bool(self._config & 1) @listen.setter def listen(self, is_rx): assert isinstance(is_rx, (bool, int)) if self.listen != is_rx: self._start_listening() else: self._stop_listening() def _start_listening(self): # ensure radio is in power down or standby-I mode if self.ce.value: self.ce.value = 0 if self._pipe0_read_addr is not None: # make sure the last call to open_rx_pipe(0) sticks if initialized self._reg_write_bytes(RX_ADDR, self._pipe0_read_addr) self._pipes[0] = self._pipe0_read_addr # update the context as well # power up radio & set radio in RX mode self._config = self._config & 0xFC | 3 self._reg_write(CONFIG, self._config) time.sleep(0.00015) # mandatory wait time to power up radio or switch modes (RX/TX) self.flush_rx() # spec sheet says "used in RX mode" self.clear_status_flags(True, False, False) # only Data Ready flag # enable radio comms self.ce.value = 1 # radio begins listening after CE pulse is > 130 µs time.sleep(0.00013) # ensure pulse is > 130 µs # nRF24L01 has just entered active RX + standby-II mode def _stop_listening(self): # ensure radio is in standby-I mode if self.ce.value: self.ce.value = 0 # set radio in TX mode as recommended behavior per spec sheet. self._config = self._config & 0xFE # does not put radio to sleep self._reg_write(CONFIG, self._config) # mandated wait for transitioning between modes RX/TX time.sleep(0.00016) # exits while still in Standby-I (low current & no transmissions)
[docs] def any(self): """This function checks if the nRF24L01 has received any data at all. Internally, this function uses `pipe()` then reports the next available payload's length (in bytes) -- if there is any. :returns: - `int` of the size (in bytes) of an available RX payload (if any). - ``0`` if there is no payload in the RX FIFO buffer. """ if self.pipe() is not None: # 0x60 == R_RX_PL_WID command return self._reg_read(0x60) # top-level payload length return 0 # RX FIFO empty
[docs] def recv(self): """This function is used to retrieve the next available payload in the RX FIFO buffer, then clears the `irq_DR` status flag. This function also serves as a helper function to `read_ack()` in TX mode to aquire any custom payload in the automatic acknowledgement (ACK) packet -- only when the `ack` attribute is enabled. :returns: A `bytearray` of the RX payload data - If the `dynamic_payloads` attribute is disabled, then the returned bytearray's length is equal to the user defined `payload_length` attribute (which defaults to 32). - If the `dynamic_payloads` attribute is enabled, then the returned bytearray's length is equal to the payload's length .. tip:: Call the `any()` function before calling `recv()` to verify that there is data to fetch. If there's no data to fetch, then the nRF24L01 returns bogus data and should not regarded as a valid payload. """ # buffer size = current payload size (0x60 = R_RX_PL_WID) + status byte curr_pl_size = self.payload_length if not self.dynamic_payloads else self._reg_read( 0x60) # get the data (0x61 = R_RX_PAYLOAD) result = self._reg_read_bytes(0x61, curr_pl_size) # clear only Data Ready IRQ flag for continued RX operations self.clear_status_flags(True, False, False) # return all available bytes from payload return result
[docs] def send(self, buf, ask_no_ack=False): """This blocking function is used to transmit payload(s). :returns: * `list` if a list or tuple of payloads was passed as the ``buf`` parameter. Each item in the returned list will contain the returned status for each corresponding payload in the list/tuple that was passed. The return statuses will be in one of the following forms: * `False` if transmission fails. * `True` if transmission succeeds. * `bytearray` when the `ack` attribute is `True`, the payload expects a responding custom ACK payload; the response is returned (upon successful transmission) as a `bytearray`. Empty ACK payloads (upon successful transmission) when the `ack` attribute is set `True` are replaced with an error message ``b'NO ACK RETURNED'``. * `None` if transmission times out meaning nRF24L01 has malfunctioned. This condition is very rare. The allowed time for transmission is calculated using `table 18 in the nRF24L01 specification sheet <https://www.sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1123001>`_ :param bytearray,list,tuple buf: The payload to transmit. This bytearray must have a length greater than 0 and less than 32, otherwise a `ValueError` exception is thrown. This can also be a list or tuple of payloads (`bytearray`); in which case, all items in the list/tuple are processed for consecutive transmissions. - If the `dynamic_payloads` attribute is disabled and this bytearray's length is less than the `payload_length` attribute, then this bytearray is padded with zeros until its length is equal to the `payload_length` attribute. - If the `dynamic_payloads` attribute is disabled and this bytearray's length is greater than `payload_length` attribute, then this bytearray's length is truncated to equal the `payload_length` attribute. :param bool ask_no_ack: Pass this parameter as `True` to tell the nRF24L01 not to wait for an acknowledgment from the receiving nRF24L01. This parameter directly controls a ``NO_ACK`` flag in the transmission's Packet Control Field (9 bits of information about the payload). Therefore, it takes advantage of an nRF24L01 feature specific to individual payloads, and its value is not saved anywhere. You do not need to specify this for every payload if the `auto_ack` attribute is disabled, however this parameter should work despite the `auto_ack` attribute's setting. .. note:: Each transmission is in the form of a packet. This packet contains sections of data around and including the payload. `See Chapter 7.3 in the nRF24L01 Specifications Sheet <https://www.sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1136318>`_ for more details. .. tip:: It is highly recommended that `auto_ack` attribute is enabled when sending multiple payloads. Test results with the `auto_ack` attribute disabled were very poor (much < 50% received). This same advice applies to the ``ask_no_ack`` parameter (leave it as `False` for multiple payloads). .. warning:: The nRF24L01 will block usage of the TX FIFO buffer upon failed transmissions. Failed transmission's payloads stay in TX FIFO buffer until the MCU calls `flush_tx()` and `clear_status_flags()`. Therefore, this function will discard failed transmissions' payloads when sending a list or tuple of payloads, so it can continue to process through the list/tuple even if any payload fails to be acknowledged. .. note:: We've tried very hard to keep nRF24L01s driven by CircuitPython devices compliant with nRF24L01s driven by the Raspberry Pi. But due to the Raspberry Pi's seemingly slower SPI speeds, we've had to resort to internally deploying `resend()` twice (at most when needed) for payloads that failed during multi-payload processing. This tactic is meant to slow down CircuitPython devices just enough for the Raspberry Pi to catch up. Transmission failures are less possible this way. """ # ensure power down/standby-I for proper manipulation of PWR_UP & PRIM_RX bits in # CONFIG register self.ce.value = 0 self.flush_tx() self.clear_status_flags(False) # clears TX related flags only # using spec sheet calculations: # timeout total = T_upload + 2 * stby2active + T_overAir + T_ack + T_irq # T_upload = payload length (in bits) / spi data rate (bits per second = # baudrate / bits per byte) # T_upload is finished before timeout begins # T_download == T_upload, however RX devices spi settings must match TX's for # accurate calc # let 2 * stby2active (in µs) ~= (2 + 1 if getting ack else 0) * 130 # let T_ack = T_overAir as the payload size is the only distictive variable between # the 2 # T_overAir (in seconds) = ( 8 (bits/byte) * (1 byte preamble + address length + # payload length + crc length) + 9 bit packet ID ) / RF data rate (in bits/sec) # spec sheet says T_irq is (0.0000082 if self.data_rate == 1 else 0.000006) seconds pl_coef = 1 + (bool(self.auto_ack) and not ask_no_ack) pl_len = 1 + self._addr_len + (max(0, ((self._config & 12) >> 2) - 1)) bitrate = ((2000000 if self._rf_setup & 0x28 == 8 else 250000) if self._rf_setup & 0x28 else 1000000) / 8 stby2active = (1 + pl_coef) * 0.00013 t_irq = 0.0000082 if not self._rf_setup & 0x28 else 0.000006 t_retry = (((self._setup_retr & 0xf0) >> 4) * 250 + 380) * \ (self._setup_retr & 0x0f) / 1000000 if isinstance(buf, (list, tuple)): # writing a set of payloads result = [] for i, b in enumerate(buf): # check invalid payloads first # this way when we raise a ValueError exception we don't leave the nRF24L01 in an # unknown frozen state. if not b or len(b) > 32: raise ValueError("buf (item {} in the list/tuple) must be a" " buffer protocol object with a byte length of\nat least 1 " "and no greater than 32".format(i)) for b in buf: timeout = pl_coef * (((8 * (len(b) + pl_len)) + 9) / bitrate) + \ stby2active + t_irq + t_retry + \ (len(b) * 64 / self.spi.baudrate) self.write(b, ask_no_ack) # wait for the ESB protocol to finish (or at least attempt) time.sleep(timeout) # TODO could do this better self.update() # update status flags if self.irq_DF: # need to clear for continuing transmissions # retry twice at most -- this seemed adaquate during testing for i in range(2): if not self.resend(): # clears flags upon entering and exiting if i: # the last try self.flush_tx() # discard failed payloads in the name of progress result.append(False) else: # resend succeeded if self.ack: # is there a custom ACK payload? result.append(self.read_ack()) else: result.append(True) break elif self.irq_DS: result.append(True) # clears TX related flags only self.clear_status_flags(False) self.ce.value = 0 return result if not buf or len(buf) > 32: raise ValueError("buf must be a buffer protocol object with a byte length of" "\nat least 1 and no greater than 32") result = None # T_upload is done before timeout begins (after payload write action AKA upload) timeout = pl_coef * (((8 * (len(buf) + pl_len)) + 9) / bitrate) + stby2active + t_irq + t_retry self.write(buf, ask_no_ack) # init using non-blocking helper time.sleep(0.00001) # ensure CE pulse is >= 10 µs # if pulse is stopped here, the nRF24L01 only handles the top level payload in the FIFO. # hold CE HIGH to continue processing through the rest of the TX FIFO bound for the # address passed to open_tx_pipe() # go to Standby-I power mode (power attribute still == True) self.ce.value = 0 # now wait till the nRF24L01 has determined the result or timeout (based on calcs # from spec sheet) start = time.monotonic() while not self.irq_DS and not self.irq_DF and (time.monotonic() - start) < timeout: self.update() # perform Non-operation command to get status byte (should be faster) # print('status: DR={} DS={} DF={}'.format(self.irq_DR, self.irq_DS, self.irq_DF)) if self.irq_DS or self.irq_DF: # transmission done # get status flags to detect error result = self.irq_DS if self.auto_ack else not self.irq_DF # read ack payload clear status flags, then power down if self.ack and self.irq_DS and not ask_no_ack: # get and save ACK payload to self.ack if user wants it result = self.read_ack() # save RX'd ACK payload to result if result is None: # can't return empty handed result = b'NO ACK RETURNED' self.clear_status_flags(False) # only TX related IRQ flags return result
@property def irq_DR(self): """A `bool` that represents the "Data Ready" interrupted flag. (read-only) * `True` represents Data is in the RX FIFO buffer * `False` represents anything depending on context (state/condition of FIFO buffers) -- usually this means the flag's been reset. Pass ``dataReady`` parameter as `True` to `clear_status_flags()` and reset this. As this is a virtual representation of the interrupt event, this attribute will always be updated despite what the actual IRQ pin is configured to do about this event. Calling this does not execute an SPI transaction. It only exposes that latest data contained in the STATUS byte that's always returned from any other SPI transactions. Use the `update()` function to manually refresh this data when needed. """ return bool(self._status & 0x40) @property def irq_DS(self): """A `bool` that represents the "Data Sent" interrupted flag. (read-only) * `True` represents a successful transmission * `False` represents anything depending on context (state/condition of FIFO buffers) -- usually this means the flag's been reset. Pass ``dataSent`` parameter as `True` to `clear_status_flags()` to reset this. As this is a virtual representation of the interrupt event, this attribute will always be updated despite what the actual IRQ pin is configured to do about this event. Calling this does not execute an SPI transaction. It only exposes that latest data contained in the STATUS byte that's always returned from any other SPI transactions. Use the `update()` function to manually refresh this data when needed. """ return bool(self._status & 0x20) @property def irq_DF(self): """A `bool` that represents the "Data Failed" interrupted flag. (read-only) * `True` signifies the nRF24L01 attemped all configured retries * `False` represents anything depending on context (state/condition) -- usually this means the flag's been reset. Pass ``dataFail`` parameter as `True` to `clear_status_flags()` to reset this. As this is a virtual representation of the interrupt event, this attribute will always be updated despite what the actual IRQ pin is configured to do about this event.see also the `arc` and `ard` attributes. Calling this does not execute an SPI transaction. It only exposes that latest data contained in the STATUS byte that's always returned from any other SPI transactions. Use the `update()` function to manually refresh this data when needed. """ return bool(self._status & 0x10) def clear_status_flags(self, data_recv=True, data_sent=True, data_fail=True): """This clears the interrupt flags in the status register. Internally, this is automatically called by `send()`, `write()`, `recv()`, and when `listen` changes from `False` to `True`. :param bool data_recv: specifies wheather to clear the "RX Data Ready" flag. :param bool data_sent: specifies wheather to clear the "TX Data Sent" flag. :param bool data_fail: specifies wheather to clear the "Max Re-transmit reached" flag. .. note:: Clearing the ``data_fail`` flag is necessary for continued transmissions from the nRF24L01 (locks the TX FIFO buffer when `irq_DF` is `True`) despite wheather or not the MCU is taking advantage of the interrupt (IRQ) pin. Call this function only when there is an antiquated status flag (after you've dealt with the specific payload related to the staus flags that were set), otherwise it can cause payloads to be ignored and occupy the RX/TX FIFO buffers. See `Appendix A of the nRF24L01+ Specifications Sheet <https://www.sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1047965>`_ for an outline of proper behavior. """ # 0x07 = STATUS register; only bits 6 through 4 are write-able self._reg_write(0x07, (data_recv << 6) | ( data_sent << 5) | (data_fail << 4)) def interrupt_config(self, data_recv=True, data_sent=True, data_fail=True): """Sets the configuration of the nRF24L01's IRQ (interrupt) pin. The signal from the nRF24L01's IRQ pin is active LOW. (write-only) :param bool data_recv: If this is `True`, then IRQ pin goes active when there is new data to read in the RX FIFO buffer. :param bool data_sent: If this is `True`, then IRQ pin goes active when a payload from TX buffer is successfully transmit. :param bool data_fail: If this is `True`, then IRQ pin goes active when maximum number of attempts to re-transmit the packet have been reached. If `auto_ack` attribute is disabled, then this IRQ event is not used. .. note:: To fetch the status (not configuration) of these IRQ flags, use the `irq_DF`, `irq_DS`, `irq_DR` attributes respectively. .. tip:: Paraphrased from nRF24L01+ Specification Sheet: The procedure for handling ``data_recv`` IRQ should be: 1. read payload through `recv()` 2. clear ``dataReady`` status flag (taken care of by using `recv()` in previous step) 3. read FIFO_STATUS register to check if there are more payloads available in RX FIFO buffer. (a call to `pipe()`, `any()` or even ``(False,True)`` as parameters to `fifo()` will get this result) 4. if there is more data in RX FIFO, repeat from step 1 """ self._config = self._reg_read(CONFIG) # refresh data # save to register and update local copy of pwr & RX/TX modes' flags self._config = (self._config & 0x0F) | (not data_fail << 4) | (not data_sent << 5) | \ (not data_recv << 6) self._reg_write(CONFIG, self._config) def what_happened(self, dump_pipes=False): """This debuggung function aggregates and outputs all status/condition related information from the nRF24L01. Some information may be irrelevant depending on nRF24L01's state/condition. :prints: - ``Channel`` The current setting of the `channel` attribute - ``RF Data Rate`` The current setting of the RF `data_rate` attribute. - ``RF Power Amplifier`` The current setting of the `pa_level` attribute. - ``CRC bytes`` The current setting of the `crc` attribute - ``Address length`` The current setting of the `address_length` attribute - ``Payload lengths`` The current setting of the `payload_length` attribute - ``Auto retry delay`` The current setting of the `ard` attribute - ``Auto retry attempts`` The current setting of the `arc` attribute - ``Packets Lost`` Total amount of packets lost (transmission failures) - ``Retry Attempts Made`` Maximum amount of attempts to re-transmit during last transmission (resets per payload) - ``IRQ - Data Ready`` The current setting of the IRQ pin on "Data Ready" event - ``IRQ - Data Sent`` The current setting of the IRQ pin on "Data Sent" event - ``IRQ - Data Fail`` The current setting of the IRQ pin on "Data Fail" event - ``Data Ready`` Is there RX data ready to be read? (state of the `irq_DR` flag) - ``Data Sent`` Has the TX data been sent? (state of the `irq_DS` flag) - ``Data Failed`` Has the maximum attempts to re-transmit been reached? (state of the `irq_DF` flag) - ``TX FIFO full`` Is the TX FIFO buffer full? (state of the `tx_full` flag) - ``TX FIFO empty`` Is the TX FIFO buffer empty? - ``RX FIFO full`` Is the RX FIFO buffer full? - ``RX FIFO empty`` Is the RX FIFO buffer empty? - ``Custom ACK payload`` Is the nRF24L01 setup to use an extra (user defined) payload attached to the acknowledgment packet? (state of the `ack` attribute) - ``Ask no ACK`` Is the nRF24L01 setup to transmit individual packets that don't require acknowledgment? - ``Automatic Acknowledgment`` Is the `auto_ack` attribute enabled? - ``Dynamic Payloads`` Is the `dynamic_payloads` attribute enabled? - ``Primary Mode`` The current mode (RX or TX) of communication of the nRF24L01 device. - ``Power Mode`` The power state can be Off, Standby-I, Standby-II, or On. :param bool dump_pipes: `True` appends the output and prints: * the current address used for TX transmissions * ``Pipe [#] ([open/closed]) bound: [address]`` where ``#`` represent the pipe number, the ``open/closed`` status is relative to the pipe's RX status, and ``address`` is read directly from the nRF24L01 registers. * if the pipe is open, then the output also prints ``expecting [X] byte static payloads`` where ``X`` is the `payload_length` (in bytes) the pipe is setup to receive when `dynamic_payloads` is disabled. Default is `False` and skips this extra information. """ watchdog = self._reg_read(8) # 8 == OBSERVE_TX register print("Channel___________________{} ~ {} GHz".format( self.channel, (self.channel + 2400) / 1000)) print("RF Data Rate______________{} {}".format( self.data_rate, "Mbps" if self.data_rate != 250 else "Kbps")) print("RF Power Amplifier________{} dbm".format(self.pa_level)) print("CRC bytes_________________{}".format(self.crc)) print("Address length____________{} bytes".format(self.address_length)) print("Payload lengths___________{} bytes".format(self.payload_length)) print("Auto retry delay__________{} microseconds".format(self.ard)) print("Auto retry attempts_______{} maximum".format(self.arc)) print("Packets Lost______________{} total".format((watchdog & 0xF0) >> 4)) print("Retry Attempts Made_______{}".format(watchdog & 0x0F)) print("IRQ - Data Ready______{} Data Ready___________{}".format( '_True' if not bool(self._config & 0x40) else 'False', self.irq_DR)) print("IRQ - Data Fail_______{} Data Failed__________{}".format( '_True' if not bool(self._config & 0x20) else 'False', self.irq_DF)) print("IRQ - Data Sent_______{} Data Sent____________{}".format( '_True' if not bool(self._config & 0x10) else 'False', self.irq_DS)) print("TX FIFO full__________{} TX FIFO empty________{}".format( '_True' if bool(self.tx_full) else 'False', bool(self.fifo(True, True)))) print("RX FIFO full__________{} RX FIFO empty________{}".format( '_True' if bool(self._fifo & 2) else 'False', bool(self._fifo & 1))) print("Ask no ACK_________{} Custom ACK Payload___{}".format( '_Allowed' if bool(self._features & 1) else 'Disabled', 'Enabled' if self.ack else 'Disabled')) print("Dynamic Payloads___{} Auto Acknowledgment__{}".format( '_Enabled' if self.dynamic_payloads else 'Disabled', 'Enabled' if self.auto_ack else 'Disabled')) print("Primary Mode_____________{} Power Mode___________{}".format( 'RX' if self.listen else 'TX', ('Standby-II' if self.ce.value else 'Standby-I') if self._config & 2 else 'Off')) if dump_pipes: print('TX address____________', self._tx_address) for i, address in enumerate(self._pipes): is_open = "( open )" if self._open_pipes & (1 << i) else "(closed)" if i <= 1: # print full address print("Pipe", i, is_open, "bound:", address) else: # print unique byte + shared bytes = actual address used by radio print("Pipe", i, is_open, "bound:", bytes([self._pipes[i]]) + self._pipes[1][1:]) if self._open_pipes & (1 << i): print('\t\texpecting', self._payload_widths[i], 'byte static payloads') @property def dynamic_payloads(self): """This `bool` attribute controls the nRF24L01's dynamic payload length feature. - `True` enables nRF24L01's dynamic payload length feature. The `payload_length` attribute is ignored when this feature is enabled. - `False` disables nRF24L01's dynamic payload length feature. Be sure to adjust the `payload_length` attribute accordingly when `dynamic_payloads` feature is disabled. """ return bool(self._dyn_pl and (self._features & 4)) @dynamic_payloads.setter def dynamic_payloads(self, enable): assert isinstance(enable, (bool, int)) self._features = self._reg_read(FEATURE) # refresh data # save changes to registers(& their shadows) if self._features & 4 != enable: # if not already # throw a specific global flag for enabling dynamic payloads self._features = (self._features & 3) | (enable << 2) self._reg_write(FEATURE, self._features) # 0x3F == all pipes have enabled dynamic payloads self._dyn_pl = 0x3F if enable else 0 self._reg_write(DYNPD, self._dyn_pl) @property def payload_length(self): """This `int` attribute specifies the length (in bytes) of payload that is regarded, meaning "how big of a payload should the radio care about?" If the `dynamic_payloads` attribute is enabled, this attribute has no affect. When `dynamic_payloads` is disabled, this attribute is used to specify the payload length when entering RX mode. A valid input value must be an `int` in range [1,32]. Otherwise a `ValueError` exception is thrown. Default is set to the nRF24L01's maximum of 32. .. note:: When `dynamic_payloads` is disabled during transmissions: - Payloads' size of greater than this attribute's value will be truncated to match. - Payloads' size of less than this attribute's value will be padded with zeros to match. """ return self._payload_length @payload_length.setter def payload_length(self, length): # max payload size is 32 bytes if not length or length <= 32: # save for access via getter property self._payload_length = length else: raise ValueError( "{}: payload length can only be set in range [1,32] bytes".format(length)) @property def auto_ack(self): """This `bool` attribute controls the nRF24L01's automatic acknowledgment feature. - `True` enables automatic acknowledgment packets. The CRC (cyclic redundancy checking) is enabled automatically by the nRF24L01 if the `auto_ack` attribute is enabled (see also `crc` attribute). - `False` disables automatic acknowledgment packets. The `crc` attribute will remain unaffected (remains enabled) when disabling the `auto_ack` attribute. """ return self._aa @auto_ack.setter def auto_ack(self, enable): assert isinstance(enable, (bool, int)) # the following 0x3F == enabled auto_ack on all pipes self._aa = 0x3F if enable else 0 self._reg_write(EN_AA, self._aa) # 1 == EN_AA register for ACK feature # nRF24L01 automatically enables CRC if ACK packets are enabled in the FEATURE register @property def ack(self): """This `bool` attribute represents the status of the nRF24L01's capability to use custom payloads as part of the automatic acknowledgment (ACK) packet. Use this attribute to set/check if the custom ACK payloads feature is enabled. - `True` enables the use of custom ACK payloads in the ACK packet when responding to receiving transmissions. As `dynamic_payloads` and `auto_ack` attributes are required for this feature to work, they are automatically enabled as needed. - `False` disables the use of custom ACK payloads. Disabling this feature does not disable the `auto_ack` and `dynamic_payloads` attributes (they work just fine without this feature). """ return bool((self._features & 2) and self.auto_ack and self.dynamic_payloads) @ack.setter def ack(self, enable): assert isinstance(enable, (bool, int)) # we need to throw the EN_ACK_PAY flag in the FEATURES register accordingly on both # TX & RX nRF24L01s if self.ack != enable: # if enabling self.auto_ack = True # ensure auto_ack feature is enabled # dynamic_payloads required for custom ACK payloads self._dyn_pl = 0x3F self._reg_write(DYNPD, self._dyn_pl) else: # setting auto_ack feature automatically updated the _features attribute, so self._features = self._reg_read(FEATURE) # refresh data here self._features = (self._features & 5) | (6 if enable else 0) self._reg_write(FEATURE, self._features) def load_ack(self, buf, pipe_number): """This allows the MCU to specify a payload to be allocated into the TX FIFO buffer for use on a specific data pipe. This payload will then be appended to the automatic acknowledgment (ACK) packet that is sent when fresh data is received on the specified pipe. See `read_ack()` on how to fetch a received custom ACK payloads. :param bytearray buf: This will be the data attached to an automatic ACK packet on the incoming transmission about the specified ``pipe_number`` parameter. This must have a length in range [1,32] bytes, otherwise a `ValueError` exception is thrown. Any ACK payloads will remain in the TX FIFO buffer until transmitted successfully or `flush_tx()` is called. :param int pipe_number: This will be the pipe number to use for deciding which transmissions get a response with the specified ``buf`` parameter's data. This number must be in range [0,5], otherwise a `ValueError` exception is thrown. :returns: `True` if payload was successfully loaded onto the TX FIFO buffer. `False` if it wasn't because TX FIFO buffer is full. .. note:: this function takes advantage of a special feature on the nRF24L01 and needs to be called for every time a customized ACK payload is to be used (not for every automatic ACK packet -- this just appends a payload to the ACK packet). The `ack`, `auto_ack`, and `dynamic_payloads` attributes are also automatically enabled by this function when necessary. .. tip:: The ACK payload must be set prior to receiving a transmission. It is also worth noting that the nRF24L01 can hold up to 3 ACK payloads pending transmission. Using this function does not over-write existing ACK payloads pending; it only adds to the queue (TX FIFO buffer) if it can. Use `flush_tx()` to discard unused ACK payloads when done listening. """ if pipe_number < 0 or pipe_number > 5: raise ValueError("pipe number must be in range [0,5]") if not buf or len(buf) > 32: raise ValueError("buf must be a buffer protocol object with a byte length of" "\nat least 1 and no greater than 32") # only prepare payload if the auto_ack attribute is enabled and ack[0] is not None if not self.ack: self.ack = True if not self.tx_full: # 0xA8 = W_ACK_PAYLOAD self._reg_write_bytes(0xA8 | pipe_number, buf) return True # payload was loaded return False # payload wasn't loaded def read_ack(self): """Allows user to read the automatic acknowledgement (ACK) payload (if any) when nRF24L01 is in TX mode. This function is called from a blocking `send()` call if the `ack` attribute is enabled. Alternatively, this function can be called directly in case of calling the non-blocking `write()` function during asychronous applications. .. warning:: In the case of asychronous applications, this function will do nothing if the status flags are cleared after calling `write()` and before calling this function. See also the `ack`, `dynamic_payloads`, and `auto_ack` attributes as they must be enabled to use custom ACK payloads. """ if self.any(): # check RX FIFO for ACK packet's payload return self.recv() return None @property def data_rate(self): """This `int` attribute specifies the nRF24L01's frequency data rate for OTA (over the air) transmissions. A valid input value is: - ``1`` sets the frequency data rate to 1 Mbps - ``2`` sets the frequency data rate to 2 Mbps - ``250`` sets the frequency data rate to 250 Kbps Any invalid input throws a `ValueError` exception. Default is 1 Mbps. .. warning:: 250 Kbps is be buggy on the non-plus models of the nRF24L01 product line. If you use 250 Kbps data rate, and some transmissions report failed by the transmitting nRF24L01, even though the same packet in question actually reports received by the receiving nRF24L01, then try a higher data rate. CAUTION: Higher data rates mean less maximum distance between nRF24L01 transceivers (and vise versa). """ self._rf_setup = self._reg_read(RF_SETUP) # refresh data return (2 if self._rf_setup & 0x28 == 8 else 250) if self._rf_setup & 0x28 else 1 @data_rate.setter def data_rate(self, speed): # nRF24L01+ must be in a standby or power down mode before writing to the configuration # registers. if speed in (1, 2, 250): if self.data_rate != speed: speed = 0 if speed == 1 else (8 if speed == 2 else 0x20) # save changes to register(& its shadow) self._rf_setup = self._rf_setup & 0xD7 | speed self._reg_write(RF_SETUP, self._rf_setup) else: raise ValueError( "data rate must be one of the following ([M,M,K]bps): 1, 2, 250") @property def channel(self): """This `int` attribute specifies the nRF24L01's frequency (in 2400 + `channel` MHz). A valid input value must be in range [0, 125] (that means [2.4, 2.525] GHz). Otherwise a `ValueError` exception is thrown. Default is 76. """ return self._reg_read(RF_CH) @channel.setter def channel(self, channel): if 0 <= channel <= 125: self._channel = channel self._reg_write(RF_CH, channel) # always writes to reg else: raise ValueError("channel acn only be set in range [0,125]") @property def crc(self): """This `int` attribute specifies the nRF24L01's CRC (cyclic redundancy checking) encoding scheme in terms of byte length. A valid input value is in range [0,2]: - ``0`` disables CRC - ``1`` enables CRC encoding scheme using 1 byte - ``2`` enables CRC encoding scheme using 2 bytes Any invalid input throws a `ValueError` exception. Default is enabled using 2 bytes. .. note:: The nRF24L01 automatically enables CRC if automatic acknowledgment feature is enabled (see `auto_ack` attribute). """ self._config = self._reg_read(CONFIG) # refresh data return max(0, ((self._config & 12) >> 2) - 1) # this works @crc.setter def crc(self, length): if 0 <= length <= 2: if self.crc != length: length = (length + 1) << 2 if length else 0 # this works # save changes to register(& its Shadow) self._config = self._config & 0x73 | length self._reg_write(0, self._config) else: raise ValueError( "CRC byte length must be an int equal to 0 (off), 1, or 2") @property def power(self): """This `bool` attribute controls the power state of the nRF24L01. This is exposed for asynchronous applications and user preference. - `False` basically puts the nRF24L01 to sleep (AKA power down mode) with ultra-low current consumption. No transmissions are executed when sleeping, but the nRF24L01 can still be accessed through SPI. Upon instantiation, this driver class puts the nRF24L01 to sleep until the MCU invokes RX/TX transmissions. This driver class doesn't power down the nRF24L01 after RX/TX transmissions are complete (avoiding the required power up/down 130 µs wait time), that preference is left to the user. - `True` powers up the nRF24L01. This is the first step towards entering RX/TX modes (see also `listen` attribute). Powering up is automatically handled by the `listen` attribute as well as the `send()` and `write()` functions. .. note:: This attribute needs to be `True` if you want to put radio on Standby-II (highest current consumption) or Standby-I (moderate current consumption) modes. TX transmissions are only executed during Standby-II by calling `send()` or `write()`. RX transmissions are received during Standby-II by setting `listen` attribute to `True` (see `Chapter 6.1.2-7 of the nRF24L01+ Specifications Sheet <https://www.sparkfun.com/ datasheets/Components/SMD/nRF24L01Pluss_Preliminary_Product_Specification_v1_0 .pdf#G1132980>`_). After using `send()` or setting `listen` to `False`, the nRF24L01 is left in Standby-I mode (see also notes on the `write()` function). """ return bool(self._config & 2) @power.setter def power(self, is_on): assert isinstance(is_on, (bool, int)) # capture surrounding flags and set PWR_UP flag according to is_on boolean self._config = self._reg_read(CONFIG) # refresh data if self.power != is_on: # only write changes self._config = (self._config & 0x7d) | ( is_on << 1) # doesn't affect TX?RX mode self._reg_write(CONFIG, self._config) # power up/down takes < 150 µs + 4 µs time.sleep(0.00016) @property def arc(self): """"This `int` attribute specifies the nRF24L01's number of attempts to re-transmit TX payload when acknowledgment packet is not received. The nRF24L01 does not attempt to re-transmit if `auto_ack` attribute is disabled. A valid input value must be in range [0,15]. Otherwise a `ValueError` exception is thrown. Default is set to 3. """ self._setup_retr = self._reg_read(SETUP_RETR) # refresh data return self._setup_retr & 0x0f @arc.setter def arc(self, count): if 0 <= count <= 15: if self.arc & 0x0F != count: # write only if needed # save changes to register(& its shadow) self._setup_retr = (self._setup_retr & 0xF0) | count self._reg_write(SETUP_RETR, self._setup_retr) else: raise ValueError( "automatic re-transmit count(/attempts) must in range [0,15]") @property def ard(self): """This `int` attribute specifies the nRF24L01's delay (in µs) between attempts to automatically re-transmit the TX payload when an expected acknowledgement (ACK) packet is not received. During this time, the nRF24L01 is listening for the ACK packet. If the `auto_ack` attribute is disabled, this attribute is not applied. A valid input value must be a multiple of 250 in range [250,4000]. Otherwise a `ValueError` exception is thrown. Default is 1500 for reliability. .. note:: Paraphrased from nRF24L01 specifications sheet: Please take care when setting this parameter. If the custom ACK payload is more than 15 bytes in 2 Mbps data rate, the `ard` must be 500µS or more. If the custom ACK payload is more than 5 bytes in 1 Mbps data rate, the `ard` must be 500µS or more. In 250kbps data rate (even when there is no custom ACK payload) the `ard` must be 500µS or more. See `data_rate` attribute on how to set the data rate of the nRF24L01's transmissions. """ self._setup_retr = self._reg_read(SETUP_RETR) # refresh data return ((self._setup_retr & 0xf0) >> 4) * 250 + 250 @ard.setter def ard(self, delta_t): if 250 <= delta_t <= 4000 and delta_t % 250 == 0: # set new ARD data and current ARC data to register if self.ard != delta_t: # write only if needed # save changes to register(& its Shadow) self._setup_retr = (int((delta_t - 250) / 250) << 4) | (self._setup_retr & 0x0F) self._reg_write(SETUP_RETR, self._setup_retr) else: raise ValueError("automatic re-transmit delay can only be a multiple of 250 in range " "[250,4000]") @property def pa_level(self): """This `int` attribute specifies the nRF24L01's power amplifier level (in dBm). Higher levels mean the transmission will cover a longer distance. Use this attribute to tweak the nRF24L01 current consumption on projects that don't span large areas. A valid input value is: - ``-18`` sets the nRF24L01's power amplifier to -18 dBm (lowest) - ``-12`` sets the nRF24L01's power amplifier to -12 dBm - ``-6`` sets the nRF24L01's power amplifier to -6 dBm - ``0`` sets the nRF24L01's power amplifier to 0 dBm (highest) Any invalid input throws a `ValueError` exception. Default is 0 dBm. """ self._rf_setup = self._reg_read(RF_SETUP) # refresh data return (3 - ((self._rf_setup & RF_SETUP) >> 1)) * -6 @pa_level.setter def pa_level(self, power): # nRF24L01+ must be in a standby or power down mode before writing to the # configuration registers. if power in (-18, -12, -6, 0): power = (3 - int(power / -6)) * 2 # this works # save changes to register (& its shadow) self._rf_setup = (self._rf_setup & 0xF9) | power self._reg_write(RF_SETUP, self._rf_setup) else: raise ValueError( "power amplitude must be one of the following (dBm): -18, -12, -6, 0") @property def tx_full(self): """An attribute to represent the nRF24L01's status flag signaling that the TX FIFO buffer is full. (read-only) Calling this does not execute an SPI transaction. It only exposes that latest data contained in the STATUS byte that's always returned from any SPI transactions with the nRF24L01. Use the `update()` function to manually refresh this data when needed. :returns: * `True` for TX FIFO buffer is full * `False` for TX FIFO buffer is not full. This doesn't mean the TX FIFO buffer is empty. """ return bool(self._status & 1) def update(self): """This function is only used to get an updated status byte over SPI from the nRF24L01 and is exposed to the MCU for asynchronous applications. Refreshing the status byte is vital to checking status of the interrupts, RX pipe number related to current RX payload, and if the TX FIFO buffer is full. This function returns nothing, but internally updates the `irq_DR`, `irq_DS`, `irq_DF`, and `tx_full` attributes. Internally this is a helper function to `pipe()`, `send()`, and `resend()` functions""" # perform non-operation to get status byte # should be faster than reading the STATUS register self._reg_write(0xFF) def resend(self): """Use this function to maunally re-send the previously failed-to-transmit payload in the top level (first out) of the TX FIFO buffer. .. note:: The nRF24L01 normally removes a payload from the TX FIFO buffer after successful transmission, but not when this function is called. The payload (successfully transmitted or not) will remain in the TX FIFO buffer until `flush_tx()` is called to remove them. Alternatively, using this function also allows the failed payload to be over-written by using `send()` or `write()` to load a new payload. """ if not self.fifo(True, True): # also updates _fifo attribute if self.irq_DF or self.irq_DS: # check and clear flags self.clear_status_flags(False) # clears TX related flags only result = None if self._features & 1 == 0: # ensure REUSE_TX_PL optional command is allowed self._features = self._features & 0xFE | 1 # set EN_DYN_ACK flag high self._reg_write(FEATURE, self._features) # payload will get re-used. This command tells the radio not pop TX payload from # FIFO on success self._reg_write(0xE3) # 0xE3 == REUSE_TX_PL command self.ce.value = 0 # this cycles the CE pin to re-enable transmission of re-used payload self.ce.value = 1 time.sleep(0.00001) # mandated 10 µs pulse # now get result self.ce.value = 0 # only send one payload start = time.monotonic() # timeout calc assumes 32 byte payload (no way to tell when payload has already been # loaded into TX FIFO) pl_coef = 1 + bool(self.auto_ack) pl_len = 1 + self._addr_len + \ (max(0, ((self._config & 12) >> 2) - 1)) bitrate = ((2000000 if self._rf_setup & 0x28 == 8 else 250000) if self._rf_setup & 0x28 else 1000000) / 8 stby2active = (1 + pl_coef) * 0.00013 t_irq = 0.0000082 if not self._rf_setup & 0x28 else 0.000006 t_retry = (((self._setup_retr & 0xf0) >> 4) * 250 + 380) * (self._setup_retr & 0x0f) / 1000000 timeout = pl_coef * (((8 * (32 + pl_len)) + 9) / bitrate) + stby2active + t_irq + t_retry while not self.irq_DS and not self.irq_DF and (time.monotonic() - start) < timeout: self.update() # perform Non-operation command to get status byte (should be faster) if self.irq_DS or self.irq_DF: # transmission done # get status flags to detect error result = bool(self.irq_DS) # read ack payload clear status flags, then power down if self.ack and self.irq_DS: # get and save ACK payload to self.ack if user wants it result = self.read_ack() # save reply in input buffer if result is None: # can't return empty handed result = b'NO ACK RETURNED' self.clear_status_flags(False) # only TX related IRQ flags return result def write(self, buf=None, ask_no_ack=False): """This non-blocking function (when used as alternative to `send()`) is meant for asynchronous applications and can only handle one payload at a time as it is a helper function to `send()`. :param bytearray buf: The payload to transmit. This bytearray must have a length greater than 0 and less than 32 bytes, otherwise a `ValueError` exception is thrown. - If the `dynamic_payloads` attribute is disabled and this bytearray's length is less than the `payload_length` attribute, then this bytearray is padded with zeros until its length is equal to the `payload_length` attribute. - If the `dynamic_payloads` attribute is disabled and this bytearray's length is greater than `payload_length` attribute, then this bytearray's length is truncated to equal the `payload_length` attribute. :param bool ask_no_ack: Pass this parameter as `True` to tell the nRF24L01 not to wait for an acknowledgment from the receiving nRF24L01. This parameter directly controls a ``NO_ACK`` flag in the transmission's Packet Control Field (9 bits of information about the payload). Therefore, it takes advantage of an nRF24L01 feature specific to individual payloads, and its value is not saved anywhere. You do not need to specify this for every payload if the `auto_ack` attribute is disabled, however this parameter should work despite the `auto_ack` attribute's setting. .. note:: Each transmission is in the form of a packet. This packet contains sections of data around and including the payload. `See Chapter 7.3 in the nRF24L01 Specifications Sheet <https://www.sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1136318>`_ for more details. This function isn't completely non-blocking as we still need to wait just under 5 ms for the CSN pin to settle (allowing a clean SPI transaction). .. note:: The nRF24L01 doesn't initiate sending until a mandatory minimum 10 µs pulse on the CE pin is acheived. That pulse is initiated before this function exits. However, we have left that 10 µs wait time to be managed by the MCU in cases of asychronous application, or it is managed by using `send()` instead of this function. If the CE pin remains HIGH for longer than 10 µs, then the nRF24L01 will continue to transmit all payloads found in the TX FIFO buffer. .. warning:: A note paraphrased from the `nRF24L01+ Specifications Sheet <https://www. sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1121422>`_: It is important to NEVER to keep the nRF24L01+ in TX mode for more than 4 ms at a time. If the [`auto_ack` and `dynamic_payloads`] features are enabled, nRF24L01+ is never in TX mode longer than 4 ms. .. tip:: Use this function at your own risk. Because of the underlying `"Enhanced ShockBurst Protocol" <https://www.sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1132607>`_, disobeying the 4 ms rule is easily avoided if you enable the `dynamic_payloads` and `auto_ack` attributes. Alternatively, you MUST use interrupt flags or IRQ pin with user defined timer(s) to AVOID breaking the 4 ms rule. If the `nRF24L01+ Specifications Sheet explicitly states this <https://www.sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1121422>`_, we have to assume radio damage or misbehavior as a result of disobeying the 4 ms rule. See also `table 18 in the nRF24L01 specification sheet <https://www.sparkfun.com/datasheets/Components/SMD/ nRF24L01Pluss_Preliminary_Product_Specification_v1_0.pdf#G1123001>`_ for calculating necessary transmission time (these calculations are used in the `send()` function). """ if not buf or len(buf) > 32: raise ValueError("buf must be a buffer protocol object with a byte length of" "\nat least 1 and no greater than 32") self.clear_status_flags(False) # only TX related IRQ flags if not self.power or (self._config & 1): # ready radio if it isn't yet # also ensures tx mode self._config = (self._reg_read(CONFIG) & 0x7c) | 2 self._reg_write(0, self._config) # power up/down takes < 150 µs + 4 µs time.sleep(0.00016) # pad out or truncate data to fill payload_length if dynamic_payloads == False if not self.dynamic_payloads: if len(buf) < self.payload_length: for _ in range(self.payload_length - len(buf)): buf += b'\x00' elif len(buf) > self.payload_length: buf = buf[:self.payload_length] # now upload the payload accordingly if ask_no_ack: # payload doesn't want acknowledgment # 0xB0 = W_TX_PAYLOAD_NO_ACK; this command works with auto_ack on or off # write appropriate command with payload self._reg_write_bytes(0xB0, buf) # print("payload doesn't want acknowledgment") else: # payload may require acknowledgment # 0xA0 = W_TX_PAYLOAD; this command works with auto_ack on or off # write appropriate command with payload self._reg_write_bytes(0xA0, buf) # print("payload does want acknowledgment") # enable radio comms so it can send the data by starting the mandatory minimum 10 µs pulse # on CE. Let send() measure this pulse for blocking reasons self.ce.value = 1 # re-used payloads start with this as well # radio will automatically go to standby-II after transmission while CE is still HIGH only # if dynamic_payloads and auto_ack are enabled def flush_rx(self): """A helper function to flush the nRF24L01's internal RX FIFO buffer. (write-only) .. note:: The nRF24L01 RX FIFO is 3 level stack that holds payload data. This means that there can be up to 3 received payloads (each of a maximum length equal to 32 bytes) waiting to be read (and popped from the stack) by `recv()` or `read_ack()`. This function clears all 3 levels. """ self._reg_write(0xE2) def flush_tx(self): """A helper function to flush the nRF24L01's internal TX FIFO buffer. (write-only) .. note:: The nRF24L01 TX FIFO is 3 level stack that holds payload data. This means that there can be up to 3 payloads (each of a maximum length equal to 32 bytes) waiting to be transmit by `send()`, `resend()` or `write()`. This function clears all 3 levels. It is worth noting that the payload data is only popped from the TX FIFO stack upon successful transmission (see also `resend()` as the handling of failed transmissions can be altered). """ self._reg_write(0xE1) def fifo(self, tx=False, empty=None): """This provides some precision determining the status of the TX/RX FIFO buffers. (read-only) :param bool tx: * `True` means information returned is about the TX FIFO buffer. * `False` means information returned is about the RX FIFO buffer. This parameter defaults to `False` when not specified. :param bool empty: * `True` tests if the specified FIFO buffer is empty. * `False` tests if the specified FIFO buffer is full. * `None` (when not specified) returns a 2 bit number representing both empty (bit 1) & full (bit 0) tests related to the FIFO buffer specified using the ``tx`` parameter. :returns: * A `bool` answer to the question: "Is the [TX/RX]:[`True`/`False`] FIFO buffer [empty/full]:[`True`/`False`]? * If the ``empty`` parameter is not specified: an `int` in range [0,2] for which: - ``1`` means the specified FIFO buffer is full - ``2`` means the specified FIFO buffer is empty - ``0`` means the specified FIFO buffer is neither full nor empty """ if (empty is None and isinstance(tx, (bool, int))) or \ (isinstance(empty, (bool, int)) and isinstance(tx, (bool, int))): self._fifo = self._reg_read(FIFO) # refresh the data if empty is None: return (self._fifo & (0x30 if tx else 0x03)) >> (4 * tx) return bool(self._fifo & ((2 - empty) << (4 * tx))) raise ValueError("Argument 1 ('tx') must always be a bool or int. Argument 2 ('empty')" ", if specified, must be a bool or int") def pipe(self): """This function returns information about the data pipe that received the next available payload in the RX FIFO buffer. :returns: - `None` if there is no payload in RX FIFO. - The `int` identifying pipe number [0,5] that received the next available payload in the RX FIFO buffer. """ self.update() # perform Non-operation command to get status byte (should be faster) pipe = (self._status & 0x0E) >> 1 # 0x0E==RX_P_NO if pipe <= 5: # is there data in RX FIFO? return pipe return None # RX FIFO is empty