From c2e7778ec10bc8b6b1042bdd883c2ccd0c82ba66 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 11 Sep 2019 13:47:02 -0700 Subject: [PATCH 01/17] modified _make_crc(). will test this later also added some docstrings and other stuff to please pylint --- README.rst | 3 +- circuitpython_nrf24l01/fake_ble.py | 127 ++++++++++++++--------------- 2 files changed, 64 insertions(+), 66 deletions(-) diff --git a/README.rst b/README.rst index 5c21a5c..8e131d1 100644 --- a/README.rst +++ b/README.rst @@ -78,7 +78,8 @@ To install in a virtual environment in your current project: Pinout ====== -.. image:: ../nRF24L01_Pinout.png +.. image:: https://lastminuteengineers.com/wp-content/uploads/2018/07/Pinout-nRF24L01-Wireless-Transceiver-Module.png + :target: https://lastminuteengineers.com/nrf24l01-arduino-wireless-communication/#nrf24l01-transceiver-module-pinout The nRF24L01 is controlled through SPI so there are 3 pins (SCK, MOSI, & MISO) that can only be connected to their counterparts on the MCU (microcontroller unit). The other 2 essential pins (CE & CSN) can be connected to any digital output pins. Lastly, the only optional pin on the nRf24L01 GPIOs is the IRQ (interrupt; a digital output that's active when low) pin and is only connected to the MCU via a digital input pin during the interrupt example. The following pinout is used in the example codes of this repo's example directory. diff --git a/circuitpython_nrf24l01/fake_ble.py b/circuitpython_nrf24l01/fake_ble.py index f70f583..0612beb 100644 --- a/circuitpython_nrf24l01/fake_ble.py +++ b/circuitpython_nrf24l01/fake_ble.py @@ -43,6 +43,8 @@ import time from circuitpython_nrf24l01.rf24 import RF24 +# pylint: disable=arguments-differ + def _swap_bits(orig): """reverses the bit order into LSbit to MSBit""" reverse = 0 @@ -59,41 +61,19 @@ def _reverse_bits(orig): r += bytes([_swap_bits(byte)]) return r -def _make_CRC(data): +def _make_crc(data): """use this to create the 3 byte-long CRC data. returns a bytearray""" - # uint8_t v, t, d; - # while( len-- ) { - # d = *data++; - # for( v = 0; v < 8; v++, d >>= 1 ) { - # t = dst[0] >> 7; - # dst[0] <<= 1; - # if( dst[1] & 0x80 ) { - # dst[0] |= 1; - # } - # dst[1] <<= 1; - # if( dst[2] & 0x80 ) { - # dst[1] |= 1; - # } - # dst[2] <<= 1; - # if( t != (d&1) ) { - # dst[2] ^= 0x5B; - # dst[1] ^= 0x06; - dst = bytearray(b'\x55\x55\x55') - for d in data: + # this function was taken from source code https://github.com/adafruit/Adafruit_CircuitPython_SGP30/blob/d209c7c76f941dc60b24d85fdef177b5fb2e9943/adafruit_sgp30.py#L177 + # zlib or binascii modules would be ideal alternatives on the raspberry pi, but MicroPython & CircuitPython don't have the crc32() included in the uzlib or ubinascii modules. + crc = 0xFF + for byte in data: + crc ^= byte for _ in range(8): - t = dst[0] >> 7 - dst[0] = (dst[0] << 1) & 0xFF - if(dst[1] & 0x80): - dst[0] |= 1 - dst[1] = (dst[1] << 1) & 0xFF - if(dst[2] & 0x80): - dst[1] |= 1 - dst[2] = (dst[2] << 1) & 0xFF - if(t != (d & 1)): - dst[2] ^= 0x5B - dst[1] ^= 0x06 - d >>= 1 - return dst + if crc & 0x80: + crc = (crc << 1) ^ 0x31 + else: + crc <<= 1 + return crc & 0xFF def _ble_whitening(data, whiten_coef): """for "whiten"ing the BLE packet data according to expected parameters""" @@ -108,13 +88,13 @@ def _ble_whitening(data, whiten_coef): # } # data++; result = b'' - for d in data: # for every byte + for byte in data: # for every byte for i in range(8): if whiten_coef & 0x80: whiten_coef ^= 0x11 - d ^= 1 << i + byte ^= 1 << i whiten_coef <<= 1 - result += bytes([d]) + result += bytes([byte]) return result class FakeBLE(RF24): @@ -130,11 +110,13 @@ class FakeBLE(RF24): :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. """ - def __init__(self, spi, csn, ce, name=None, pa_level=0): - super(FakeBLE, self).__init__(spi, csn, ce, pa_level=pa_level, crc=0, dynamic_payloads=False, arc=0, address_length=4, ask_no_ack=False, irq_DF=False) + def __init__(self, spi, csn, ce, name=None, pa_level=False, irq_DR=False, irq_DS=True): + super(FakeBLE, self).__init__(spi, csn, ce, pa_level=pa_level, crc=0, dynamic_payloads=False, arc=0, address_length=4, ask_no_ack=False, irq_DF=False, irq_DR=irq_DR, irq_DS=irq_DS) self._chan = 0 + self._ble_name = None self.name = name + @property def name(self): """Represents the emulated BLE device name during braodcasts. must be a buffer protocol object (`bytearray`) , and can be any length (less than 14) of UTF-8 freindly characters. @@ -145,12 +127,27 @@ def name(self): @name.setter def name(self, n): + """The broadcasted BLE name of the nRF24L01. This is not required. In fact, setting this attribute will subtract from the available payload length (in bytes). + + * payload_length has maximum of 19 bytes when NOT broadcasting a name for itself. + * payload_length has a maximum of (17 - length of name) bytes when broadcasting a name for itself. + + """ if n is not None and 1 <= len(n) <= 12: # max defined by 1 byte payload data requisite self._ble_name = bytes([len(n) + 1]) + b'\x08' + n else: self._ble_name = None # name will not be advertised def _chan_hop(self): + """BLE protocol specs mandate the BLE device cyle through the following 3 channels: + + - nRF channel 2 == BLE channel 37 + - nRF channel 26 == BLE channel 38 + - nRF channel 80 == BLE channel 39 + + .. note:: then BLE channel number is different from the nRF channel number. + + """ self._chan = (self._chan + 1) if (self._chan + 1) < 3 else 0 self.channel = 26 if self._chan == 1 else (80 if self._chan == 2 else 2) @@ -177,7 +174,7 @@ def send(self, buf): # header == PDU type, given MAC address is random/arbitrary; type == 0x42 for Android or 0x40 for iPhone # containers = 1 byte[length] + 1 byte[type] + data; the 1 byte about container's length excludes only itself - payload = b'\x42' # init payload buffer with header type byte + payload = b'\x42' # init a temp payload buffer with header type byte # to avoid padding when dynamic_payloads is disabled, set payload_length attribute self.payload_length = len(buf) + 16 + (len(self._ble_name) if self._ble_name is not None else 0) @@ -191,7 +188,7 @@ def send(self, buf): payload += self._ble_name payload += (bytes([len(buf) + 1]) + b'\xFF' + buf) # append the data container # crc is generated from b'\x55\x55\x55' about everything except itself - payload += _make_CRC(payload) + payload += _make_crc(payload) self._chan_hop() # cycle to next BLE channel per specs # the whiten_coef value we need is the BLE channel (37,38, or 39) left shifted one whiten_coef = 37 + self._chan @@ -215,35 +212,35 @@ def send(self, buf): def open_tx_pipe(self): super(FakeBLE, self).open_tx_pipe(_reverse_bits(b'\x8E\x89\xBE\xD6')) # proper address for BLE advertisments - # @address_length.setter - # def address_length(self, t): - # super(FakeBLE, self).address_length = (4 + t * 0) + @address_length.setter + def address_length(self, t): + super(FakeBLE, self).address_length = (4 + t * 0) - # @listen.setter - # def listen(self, rx): - # if self.listening or rx: - # self._stop_listening() + @listen.setter + def listen(self, rx): + if self.listen or rx: + self._stop_listening() - # @data_rate.setter - # def data_rate(self, t): - # super(FakeBLE, self).data_rate = (1 + t * 0) + @data_rate.setter + def data_rate(self, t): + super(FakeBLE, self).data_rate = (1 + t * 0) - # @dynamic_payloads.setter - # def dynamic_payloads(self, t): - # super(FakeBLE, self).dynamic_payloads = (False & t) + @dynamic_payloads.setter + def dynamic_payloads(self, t): + super(FakeBLE, self).dynamic_payloads = (False & t) - # @auto_ack.setter - # def auto_ack(self, t): - # super(FakeBLE, self).auto_ack = (False & t) + @auto_ack.setter + def auto_ack(self, t): + super(FakeBLE, self).auto_ack = (False & t) - # @ack.setter - # def ack(self, t): - # super(FakeBLE, self).ack = (False & t) + @ack.setter + def ack(self, t): + super(FakeBLE, self).ack = (False & t) - # @crc.setter - # def crc(self, t): - # super(FakeBLE, self).crc = (0 * t) + @crc.setter + def crc(self, t): + super(FakeBLE, self).crc = (0 * t) - # @arc.setter - # def arc(self, t): - # super(FakeBLE, self).arc = (t * 0) + @arc.setter + def arc(self, t): + super(FakeBLE, self).arc = (t * 0) From c05e7d794b801173751eb171d053fd6cba37ace6 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sat, 19 Oct 2019 22:25:36 -0700 Subject: [PATCH 02/17] temp rem fake_ble.py --- circuitpython_nrf24l01/fake_ble.py | 246 ----------------------------- 1 file changed, 246 deletions(-) delete mode 100644 circuitpython_nrf24l01/fake_ble.py diff --git a/circuitpython_nrf24l01/fake_ble.py b/circuitpython_nrf24l01/fake_ble.py deleted file mode 100644 index 0612beb..0000000 --- a/circuitpython_nrf24l01/fake_ble.py +++ /dev/null @@ -1,246 +0,0 @@ -# The MIT License (MIT) -# -# 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. -""" -=========================================== -circuitpython_nrf24l01.fake_ble - FakeBLE -=========================================== - -This module uses the RF24 module to make the nRF24L01 imitate a Bluetooth-Low-Emissions (BLE) beacon. A BLE beacon can send (referred to as advertise) data to any BLE compatible device (ie smart devices with Bluetooth 4.0 or later) that is listening. - -Original research was done by `Dmitry Grinberg and his write-up (including C source code) can be found here `_ As this technique can prove invaluable in certain project designs, the code here is simply ported to work on CircuitPython. - -.. important:: Because the nRF24L01 wasn't designed for BLE advertising, it has some limitations that helps to be aware of. - - 1. the maximum payload length is shortened to 19 bytes - 2. the channels that BLE use are limited to the following three: 2.402 GHz, 2.426 GHz, and 2.480 GHz - 3. CRC is disabled in the nRF24L01 firmware as BLE requires 3 bytes and nRF24L01 only handles 2. Thus we have augmented the required 3 bytes of CRC into the payload. - 4. address length of BLE packet only uses 4 bytes, so we have set that acccordingly. - 5. the automatic acknowledgment feature of the nRF24L01 is useless when tranmitting to BLE devices, thus it is disabled as well as automatic re-transmit and custom ACK payloads (both depend on the automatic acknowledgments feature) - 6. the dynamic payloads feature of the nRF24L01 isn't compatible with BLE specifications. Thus we have disabled it in the nRF24L01 firmware and incorporated dynamic payloads properly into he payload data - 7. BLE specifications only allow using 1 Mbps RF data rate, so that too has been hard coded. - 8. both the "on data sent" & "on data ready" events control the interrupt (IRQ) pin; the other event, "on data fail", is ignored because it will never get thrown with "auto_ack" off. However the interrupt settings can be modified AFTER instantiation - -""" -import time -from circuitpython_nrf24l01.rf24 import RF24 - -# pylint: disable=arguments-differ - -def _swap_bits(orig): - """reverses the bit order into LSbit to MSBit""" - reverse = 0 - for _ in range(8): - reverse <<= 1 - reverse |= orig & 1 - orig >>= 1 - return reverse # we're done here - -def _reverse_bits(orig): - """reverses the bit order into LSbit to MSBit without touching the byte order""" - r = b'' - for byte in orig: - r += bytes([_swap_bits(byte)]) - return r - -def _make_crc(data): - """use this to create the 3 byte-long CRC data. returns a bytearray""" - # this function was taken from source code https://github.com/adafruit/Adafruit_CircuitPython_SGP30/blob/d209c7c76f941dc60b24d85fdef177b5fb2e9943/adafruit_sgp30.py#L177 - # zlib or binascii modules would be ideal alternatives on the raspberry pi, but MicroPython & CircuitPython don't have the crc32() included in the uzlib or ubinascii modules. - crc = 0xFF - for byte in data: - crc ^= byte - for _ in range(8): - if crc & 0x80: - crc = (crc << 1) ^ 0x31 - else: - crc <<= 1 - return crc & 0xFF - -def _ble_whitening(data, whiten_coef): - """for "whiten"ing the BLE packet data according to expected parameters""" - # uint8_t m; - # while(len--){ - # for(m = 1; m; m <<= 1){ - # if(whitenCoeff & 0x80){ - # whitenCoeff ^= 0x11; - # (*data) ^= m; - # } - # whitenCoeff <<= 1; - # } - # data++; - result = b'' - for byte in data: # for every byte - for i in range(8): - if whiten_coef & 0x80: - whiten_coef ^= 0x11 - byte ^= 1 << i - whiten_coef <<= 1 - result += bytes([byte]) - return result - -class FakeBLE(RF24): - """Per the limitations of this technique, only power amplifier level is available for configuration when advertising BLE data. - - :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 bytearray name: This will be the nRF24L01-emulated BLE device's broadcasted name. This is option and defaults to `None` to allow for larger paylaods because the name's byte length borrows from the same buffer space that the payload data occupies. See `name` attribute for more details. - :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. - - """ - def __init__(self, spi, csn, ce, name=None, pa_level=False, irq_DR=False, irq_DS=True): - super(FakeBLE, self).__init__(spi, csn, ce, pa_level=pa_level, crc=0, dynamic_payloads=False, arc=0, address_length=4, ask_no_ack=False, irq_DF=False, irq_DR=irq_DR, irq_DS=irq_DS) - self._chan = 0 - self._ble_name = None - self.name = name - - - @property - def name(self): - """Represents the emulated BLE device name during braodcasts. must be a buffer protocol object (`bytearray`) , and can be any length (less than 14) of UTF-8 freindly characters. - - .. note:: the BLE device's name will occupy the same space as your TX data. While space is limited to 32 bytes on the nRF24L01, actual usable BLE TX data = 16 - (name length + 2). The other 16 bytes available on the nRF24L01 TX FIFO buffer are reserved for the [arbitrary] MAC address and other BLE related stuff. - """ - return self._ble_name[2:] if self._ble_name is not None else None - - @name.setter - def name(self, n): - """The broadcasted BLE name of the nRF24L01. This is not required. In fact, setting this attribute will subtract from the available payload length (in bytes). - - * payload_length has maximum of 19 bytes when NOT broadcasting a name for itself. - * payload_length has a maximum of (17 - length of name) bytes when broadcasting a name for itself. - - """ - if n is not None and 1 <= len(n) <= 12: # max defined by 1 byte payload data requisite - self._ble_name = bytes([len(n) + 1]) + b'\x08' + n - else: - self._ble_name = None # name will not be advertised - - def _chan_hop(self): - """BLE protocol specs mandate the BLE device cyle through the following 3 channels: - - - nRF channel 2 == BLE channel 37 - - nRF channel 26 == BLE channel 38 - - nRF channel 80 == BLE channel 39 - - .. note:: then BLE channel number is different from the nRF channel number. - - """ - self._chan = (self._chan + 1) if (self._chan + 1) < 3 else 0 - self.channel = 26 if self._chan == 1 else (80 if self._chan == 2 else 2) - - def send(self, buf): - """This blocking function is used to transmit payload. - - :returns: Nothing as every transmission will register as a success under these required settings. - - :param bytearray buf: The payload to transmit. This bytearray must have a length greater than 0 and less than 20, 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. - - .. note:: If the name of the emulated BLE device is also to be braodcast, then the 'name' attribute should be set prior to calling `send()` - """ - self.ce.value = 0 # ensure power down/standby-I for proper manipulation of PWR_UP & PRIM_RX bits in CONFIG register - self.flush_tx() - self.clear_status_flags(False) # clears TX related flags only - # max payload_length = 32 - 14(header, MAC, & CRC) - 2(container header) - 3(BLE flags) = 13 - (BLE name length + 2 if any) - if not buf or len(buf) > (13 - (len(self._ble_name) if self._ble_name is not None else 0)): - raise ValueError("buf must be a buffer protocol object with a byte length of\nat least 1 and no greater than 13 - {} = {}".format((len(self._ble_name) if self._ble_name is not None else 0), 13 - (len(self._ble_name) if self._ble_name is not None else 0))) - - # BLE payload = 1 byte[header] + 1 byte[payload length] + 6 byte[MAC address] + containers + 3 byte[CRC] - # header == PDU type, given MAC address is random/arbitrary; type == 0x42 for Android or 0x40 for iPhone - # containers = 1 byte[length] + 1 byte[type] + data; the 1 byte about container's length excludes only itself - - payload = b'\x42' # init a temp payload buffer with header type byte - - # to avoid padding when dynamic_payloads is disabled, set payload_length attribute - self.payload_length = len(buf) + 16 + (len(self._ble_name) if self._ble_name is not None else 0) - # payload length excludes the header, itself, and crc lengths - payload += bytes([self.payload_length - 5]) - payload += b'\x11\x22\x33\x44\x55\x66' # a bogus MAC address - # payload will have at least 2 containers: 3 bytes of flags (required for BLE discoverable), & at least (1+2) byte of data - payload += b'\x02\x01\x06' # BLE flags for discoverability and non-pairable etc - # payload will also have to fit the optional BLE device name as a seperate container ([name length + 2] bytes) - if self._ble_name is not None: - payload += self._ble_name - payload += (bytes([len(buf) + 1]) + b'\xFF' + buf) # append the data container - # crc is generated from b'\x55\x55\x55' about everything except itself - payload += _make_crc(payload) - self._chan_hop() # cycle to next BLE channel per specs - # the whiten_coef value we need is the BLE channel (37,38, or 39) left shifted one - whiten_coef = 37 + self._chan - whiten_coef = _swap_bits(whiten_coef) | 2 - - print('transmitting {} as {}'.format(payload, _reverse_bits(_ble_whitening(payload, whiten_coef)))) - self.write(_reverse_bits(_ble_whitening(payload, whiten_coef))) # init using non-blocking helper - time.sleep(0.00001) # ensure CE pulse is >= 10 µs - # pulse is stopped here; the nRF24L01 only handles the top level payload in the FIFO. - self.ce.value = 0 # go to Standby-I power mode (power attribute still == True) - - # T_upload is done before timeout begins (after payload write action AKA upload) - timeout = (((8 * (5 + len(payload))) + 9) / 125000) + 0.0002682 - start = time.monotonic() - while not self.irq_DS 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)) - self.clear_status_flags(False) # only TX related IRQ flags - - # Altering all the following settings is disabled - def open_tx_pipe(self): - super(FakeBLE, self).open_tx_pipe(_reverse_bits(b'\x8E\x89\xBE\xD6')) # proper address for BLE advertisments - - @address_length.setter - def address_length(self, t): - super(FakeBLE, self).address_length = (4 + t * 0) - - @listen.setter - def listen(self, rx): - if self.listen or rx: - self._stop_listening() - - @data_rate.setter - def data_rate(self, t): - super(FakeBLE, self).data_rate = (1 + t * 0) - - @dynamic_payloads.setter - def dynamic_payloads(self, t): - super(FakeBLE, self).dynamic_payloads = (False & t) - - @auto_ack.setter - def auto_ack(self, t): - super(FakeBLE, self).auto_ack = (False & t) - - @ack.setter - def ack(self, t): - super(FakeBLE, self).ack = (False & t) - - @crc.setter - def crc(self, t): - super(FakeBLE, self).crc = (0 * t) - - @arc.setter - def arc(self, t): - super(FakeBLE, self).arc = (t * 0) From f688d7958836c745e8ff717e3e1dc631ea99eb8a Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sun, 20 Oct 2019 01:29:45 -0700 Subject: [PATCH 03/17] docs build & can import from package namespace --- README.rst | 11 +- circuitpython_nrf24l01/__init__.py | 6 + circuitpython_nrf24l01/fake_ble.py | 304 ++++++++++++++++++ circuitpython_nrf24l01/registers.py | 13 + .../rf24.py | 46 +-- docs/api.rst | 8 +- examples/nrf24l01_ack_payload_test.py | 13 +- examples/nrf24l01_context_test.py | 11 +- examples/nrf24l01_fake_ble_test.py | 36 +++ examples/nrf24l01_interrupt_test.py | 12 +- examples/nrf24l01_simple_test.py | 11 +- examples/nrf24l01_stream_test.py | 10 +- setup.py | 143 ++++---- 13 files changed, 492 insertions(+), 132 deletions(-) create mode 100644 circuitpython_nrf24l01/__init__.py create mode 100644 circuitpython_nrf24l01/fake_ble.py create mode 100644 circuitpython_nrf24l01/registers.py rename circuitpython_nrf24l01.py => circuitpython_nrf24l01/rf24.py (98%) create mode 100644 examples/nrf24l01_fake_ble_test.py diff --git a/README.rst b/README.rst index 7f7a161..4308fc7 100644 --- a/README.rst +++ b/README.rst @@ -20,6 +20,15 @@ Introduction Circuitpython driver library for the nRF24L01 transceiver +CircuitPython port of the nRF24L01 library from Micropython. +Original work by Damien P. George & Peter Hinch can be found `here +`_ + +The Micropython source has been rewritten to expose all the nRF24L01's features and for +compatibilty with the Raspberry Pi and other Circuitpython compatible devices. Modified by Brendan Doherty, Rhys Thomas + +* Author(s): Damien P. George, Peter Hinch, Rhys Thomas, Brendan Doherty + Features currently supported ---------------------------- @@ -29,7 +38,7 @@ Features currently supported * custom acknowledgment (ACK) payloads for bi-directional communication * flag a single payload for no acknowledgment (ACK) from the receiving nRF24L01 * "re-use the same payload" feature (for manually re-transmitting failed transmissions that remain in the buffer) -* multiple payload transmissions with one function call (MUST read documentation on the :py:meth:`~circuitpython_nrf24l01.RF24.send` function) +* multiple payload transmissions with one function call (MUST read documentation on the :py:meth:`~circuitpython_nrf24l01.RF24.send()` function) * context manager compatible for easily switching between different radio configurations using "with" statements * configure the interrupt (IRQ) pin to trigger (active low) on received, sent, and/or failed transmissions (these 3 flags control the 1 IRQ pin). There's also virtual representations of these interrupt flags available (see :py:attr:`~circuitpython_nrf24l01.RF24.irq_DR`, :py:attr:`~circuitpython_nrf24l01.RF24.irq_DS`, :py:attr:`~circuitpython_nrf24l01.RF24.irq_DF` attributes) * invoke sleep mode (AKA power down mode) for ultra-low current consumption diff --git a/circuitpython_nrf24l01/__init__.py b/circuitpython_nrf24l01/__init__.py new file mode 100644 index 0000000..a1d8ff9 --- /dev/null +++ b/circuitpython_nrf24l01/__init__.py @@ -0,0 +1,6 @@ +"""module managment for the circuitpython-nrf24l01 package""" + +from .rf24 import RF24 +from .fake_ble import FakeBLE + +__all__ = ['RF24', 'FakeBLE'] diff --git a/circuitpython_nrf24l01/fake_ble.py b/circuitpython_nrf24l01/fake_ble.py new file mode 100644 index 0000000..ce8d546 --- /dev/null +++ b/circuitpython_nrf24l01/fake_ble.py @@ -0,0 +1,304 @@ +# The MIT License (MIT) +# +# 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. +""" +FakeBLE class +--------------- + +This module uses the RF24 module to make the nRF24L01 imitate a Bluetooth-Low-Emissions (BLE) +beacon. A BLE beacon can send (referred to as advertise) data to any BLE compatible device +(ie smart devices with Bluetooth 4.0 or later) that is listening. + +Original research was done by `Dmitry Grinberg and his write-up (including C source code) can be +found here `_ As +this technique can prove invaluable in certain project designs, the code here is simply ported to +work on CircuitPython. + +.. important:: Because the nRF24L01 wasn't designed for BLE advertising, it has some limitations + that helps to be aware of. + + 1. the maximum payload length is shortened to 19 bytes + 2. the channels that BLE use are limited to the following three: 2.402 GHz, 2.426 GHz, + and 2.480 GHz + 3. CRC is disabled in the nRF24L01 firmware as BLE requires 3 bytes and nRF24L01 only handles + 2. Thus we have augmented the required 3 bytes of CRC into the payload. + 4. address length of BLE packet only uses 4 bytes, so we have set that acccordingly. + 5. the automatic acknowledgment feature of the nRF24L01 is useless when tranmitting to BLE + devices, thus it is disabled as well as automatic re-transmit and custom ACK payloads + (both depend on the automatic acknowledgments feature) + 6. the dynamic payloads feature of the nRF24L01 isn't compatible with BLE specifications. + Thus we have disabled it in the nRF24L01 firmware and incorporated dynamic payloads + properly into he payload data + 7. BLE specifications only allow using 1 Mbps RF data rate, so that too has been hard coded. + 8. both the "on data sent" & "on data ready" events control the interrupt (IRQ) pin; the + other event, "on data fail", is ignored because it will never get thrown with "auto_ack" + off. However the interrupt settings can be modified AFTER instantiation +""" +import time +from .rf24 import RF24 + +def _swap_bits(orig): + """reverses the bit order into LSbit to MSBit""" + reverse = 0 + for _ in range(8): + reverse <<= 1 + reverse |= orig & 1 + orig >>= 1 + return reverse # we're done here + +def _reverse_bits(orig): + """reverses the bit order into LSbit to MSBit without touching the byte order""" + r = b'' + for byte in orig: + r += bytes([_swap_bits(byte)]) + return r + +def _make_crc(data): + """use this to create the 3 byte-long CRC data. returns a bytearray""" + # taken from source code + # https://github.com/adafruit/Adafruit_CircuitPython_SGP30/blob/d209c7c76f941dc60b24d85fdef177b5fb2e9943/adafruit_sgp30.py#L177 + # zlib or binascii modules would be ideal alternatives on the raspberry pi, but + # MicroPython & CircuitPython doesn't have the crc32() included in the uzlib or ubinascii + # modules. + crc = 0xFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x80: + crc = (crc << 1) ^ 0x31 + else: + crc <<= 1 + return crc & 0xFF + +def _ble_whitening(data, whiten_coef): + """for "whiten"ing the BLE packet data according to expected parameters""" + # uint8_t m; + # while(len--){ + # for(m = 1; m; m <<= 1){ + # if(whitenCoeff & 0x80){ + # whitenCoeff ^= 0x11; + # (*data) ^= m; + # } + # whitenCoeff <<= 1; + # } + # data++; + result = b'' + for byte in data: # for every byte + for i in range(8): + if whiten_coef & 0x80: + whiten_coef ^= 0x11 + byte ^= 1 << i + whiten_coef <<= 1 + result += bytes([byte]) + return result + +class FakeBLE(RF24): + """Per the limitations of this technique, only power amplifier level is available for + configuration when advertising BLE data. + + :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 bytearray name: This will be the nRF24L01-emulated BLE device's broadcasted name. + This is option and defaults to `None` to allow for larger paylaods because the name's + byte length borrows from the same buffer space that the payload data occupies. See + `name` attribute for more details. + :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. + """ + def __init__(self, spi, csn, ce, name=None, pa_level=False, irq_DR=False, irq_DS=True): + super(FakeBLE, self).__init__(spi, csn, ce, + pa_level=pa_level, + crc=0, + dynamic_payloads=False, + arc=0, + address_length=4, + ask_no_ack=False, + irq_DF=False, + irq_DR=irq_DR, + irq_DS=irq_DS) + self._chan = 0 + self._ble_name = None + self.name = name + + @property + def name(self): + """Represents the emulated BLE device name during braodcasts. must be a buffer protocol + object (`bytearray`) , and can be any length (less than 14) of UTF-8 freindly characters. + + .. note:: the BLE device's name will occupy the same space as your TX data. While space + is limited to 32 bytes on the nRF24L01, actual usable BLE TX data = 16 - (name length + + 2). The other 16 bytes available on the nRF24L01 TX FIFO buffer are reserved for + the [arbitrary] MAC address and other BLE related stuff. + """ + return self._ble_name[2:] if self._ble_name is not None else None + + @name.setter + def name(self, n): + """The broadcasted BLE name of the nRF24L01. This is not required. In fact, setting this + attribute will subtract from the available payload length (in bytes). + + * payload_length has maximum of 19 bytes when NOT broadcasting a name for itself. + * payload_length has a maximum of (17 - length of name) bytes when broadcasting a + name for itself. + """ + if n is not None and 1 <= len(n) <= 12: # max defined by 1 byte payload data requisite + self._ble_name = bytes([len(n) + 1]) + b'\x08' + n + else: + self._ble_name = None # name will not be advertised + + def _chan_hop(self): + """BLE protocol specs mandate the BLE device cyle through the following 3 channels: + + - nRF channel 2 == BLE channel 37 + - nRF channel 26 == BLE channel 38 + - nRF channel 80 == BLE channel 39 + + .. note:: then BLE channel number is different from the nRF channel number. + """ + self._chan = (self._chan + 1) if (self._chan + 1) < 3 else 0 + self.channel = 26 if self._chan == 1 else (80 if self._chan == 2 else 2) + + # pylint: disable=arguments-differ + def send(self, buf): + """This blocking function is used to transmit payload. + + :returns: Nothing as every transmission will register as a success under these required + settings. + + :param bytearray buf: The payload to transmit. This bytearray must have a length greater + than 0 and less than 20, 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. + + .. note:: If the name of the emulated BLE device is also to be braodcast, then the 'name' + attribute should be set prior to calling `send()`. + """ + self.ce.value = 0 + self.flush_tx() + self.clear_status_flags(False) # clears TX related flags only + # max payload_length = 32 - 14(header, MAC, & CRC) - 2(container header) - 3(BLE flags) + # = 13 - (BLE name length + 2 if any) + name_len = len(self._ble_name) if self._ble_name is not None else 0 + if not buf or len(buf) > (13 - name_len): + raise ValueError("buf must be a buffer protocol object with a byte length of" + " at least 1 and no greater than 13 - " + "{} = {}".format(name_len, 13 - name_len)) + + # BLE payload = header(1) + payload length(1) + MAC address(6) + containers + CRC(3) bytes + # header == PDU type, given MAC address is random/arbitrary + # type == 0x42 for Android or 0x40 for iPhone + # containers (in bytes) = length(1) + type(1) + data + # the 1 byte about container's length excludes only itself + + payload = b'\x42' # init a temp payload buffer with header type byte + + # to avoid padding when dynamic_payloads is disabled, set payload_length attribute + self.payload_length = len(buf) + 16 + name_len + # payload length excludes the header, itself, and crc lengths + payload += bytes([self.payload_length - 5]) + payload += b'\x11\x22\x33\x44\x55\x66' # a bogus MAC address + # payload will have at least 2 containers: + # 3 bytes of flags (required for BLE discoverable), & at least (1+2) byte of data + payload += b'\x02\x01\x06' # BLE flags for discoverability and non-pairable etc + # payload will also have to fit the optional BLE device name as + # a seperate container ([name length + 2] bytes) + if self._ble_name is not None: + payload += self._ble_name + payload += (bytes([len(buf) + 1]) + b'\xFF' + buf) # append the data container + # crc is generated from b'\x55\x55\x55' about everything except itself + payload += _make_crc(payload) + self._chan_hop() # cycle to next BLE channel per specs + # the whiten_coef value we need is the BLE channel (37,38, or 39) left shifted one + whiten_coef = 37 + self._chan + whiten_coef = _swap_bits(whiten_coef) | 2 + + print('transmitting {} as {}'.format(payload, + _reverse_bits(_ble_whitening(payload, whiten_coef)))) + # init using non-blocking helper + self.write(_reverse_bits(_ble_whitening(payload, whiten_coef))) + time.sleep(0.00001) # ensure CE pulse is >= 10 µs + # pulse is stopped here; the nRF24L01 only handles the top level payload in the FIFO. + self.ce.value = 0 # go to Standby-I power mode (power attribute still == True) + + # T_upload is done before timeout begins (after payload write action AKA upload) + timeout = (((8 * (5 + len(payload))) + 9) / 125000) + 0.0002682 + start = time.monotonic() + while not self.irq_DS 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)) + self.clear_status_flags(False) # only TX related IRQ flags + + # Altering all the following settings is disabled + def open_tx_pipe(self): + super(FakeBLE, self).open_tx_pipe(_reverse_bits(b'\x8E\x89\xBE\xD6')) + # b'\x8E\x89\xBE\xD6' = proper address for BLE advertisments + # pylint: enable=arguments-differ + + # ignore pylint errors when overriding setter functions of inherited attributes + # pylint: disable=no-member + @RF24.address_length.setter + def address_length(self, t): + super(FakeBLE, self).address_length = (4 + t * 0) + + @RF24.listen.setter + def listen(self, rx): + if self.listen or rx: + self._stop_listening() + + @RF24.data_rate.setter + def data_rate(self, t): + super(FakeBLE, self).data_rate = (1 + t * 0) + + @RF24.dynamic_payloads.setter + def dynamic_payloads(self, t): + super(FakeBLE, self).dynamic_payloads = (False & t) + + @RF24.auto_ack.setter + def auto_ack(self, t): + super(FakeBLE, self).auto_ack = (False & t) + + @RF24.ack.setter + def ack(self, t): + super(FakeBLE, self).ack = (False & t) + + @RF24.crc.setter + def crc(self, t): + super(FakeBLE, self).crc = (0 * t) + + @RF24.arc.setter + def arc(self, t): + super(FakeBLE, self).arc = (t * 0) diff --git a/circuitpython_nrf24l01/registers.py b/circuitpython_nrf24l01/registers.py new file mode 100644 index 0000000..9356e0d --- /dev/null +++ b/circuitpython_nrf24l01/registers.py @@ -0,0 +1,13 @@ +"""nRF24L01(+) registers""" +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 rangeing [0,5]:[0xA:0xF] +RX_PW = 0x11 # RX payload widths on pipes ranging [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 enablers/disablers for dynamic payloads, auto-ACK, and custom ACK features diff --git a/circuitpython_nrf24l01.py b/circuitpython_nrf24l01/rf24.py similarity index 98% rename from circuitpython_nrf24l01.py rename to circuitpython_nrf24l01/rf24.py index 203db6f..58931e3 100644 --- a/circuitpython_nrf24l01.py +++ b/circuitpython_nrf24l01/rf24.py @@ -20,22 +20,10 @@ # 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. -# pylint: disable=too-many-lines,line-too-long,unexpected-line-ending-format,invalid-name +# pylint: disable=too-many-lines,line-too-long,invalid-name """ -======================================== -circuitpython_nrf24l01 -======================================== - -CircuitPython port of the nRF24L01 library from Micropython. -Original work by Damien P. George & Peter Hinch can be found `here -`_ - -The Micropython source has been rewritten to expose all the nRF24L01's features and for -compatibilty with the Raspberry Pi and other Circuitpython compatible devices using Adafruit's -`busio`, `adafruit_bus_device.spi_device`, and `digitalio`, modules. -Modified by Brendan Doherty, Rhys Thomas - -* Author(s): Damien P. George, Peter Hinch, Rhys Thomas, Brendan Doherty +RF24 class +------------ .. important:: The nRF24L01 has 3 key features that are very interdependent of each other. Their priority of dependence is as follows: @@ -93,25 +81,25 @@ In fact the only attributes that aren't required to match on both endpoint transceivers would be the identifying data pipe number (passed to `open_rx_pipe()`), `pa_level`, `arc`, & `ard` attributes. The ``ask_no_ack`` feature can be used despite the settings/features - configuration (see `send()` & `write()` function parameters for more details). + configuration (see :meth:`~circuitpython_nrf24l01.rf24.RF24.send` & `write()` function + parameters for more details). """ __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 -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 Amptlitude & Data Rate -RX_ADDR = 0x0a # RX pipe addresses rangeing [0,5]:[0xA:0xF] -RX_PW = 0x11 # RX payload widths on pipes ranging [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 enablers/disablers for dynamic payloads, auto-ACK, and custom ACK features +from .registers import (CONFIG, + EN_AA, + EN_RX, + SETUP_AW, + SETUP_RETR, + RF_CH, + RF_SETUP, + RX_ADDR, + RX_PW, + FIFO, + DYNPD, + FEATURE) class RF24: """A driver class for the nRF24L01(+) transceiver radios. This class aims to be compatible with diff --git a/docs/api.rst b/docs/api.rst index 5a01539..039de3e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,5 +4,11 @@ .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. use this format as the module name: "adafruit_foo.foo" -.. automodule:: circuitpython_nrf24l01 + +.. automodule:: circuitpython_nrf24l01.rf24 + :members: + +.. automodule:: circuitpython_nrf24l01.fake_ble :members: + :show-inheritance: + :exclude-members: listen, open_tx_pipe, address_length, data_rate, dynamic_payloads, auto_ack, ack, crc, arc diff --git a/examples/nrf24l01_ack_payload_test.py b/examples/nrf24l01_ack_payload_test.py index 7fc27be..22ae265 100644 --- a/examples/nrf24l01_ack_payload_test.py +++ b/examples/nrf24l01_ack_payload_test.py @@ -1,10 +1,9 @@ -''' - Templated example of using the library to transmit - and retrieve custom automatic acknowledgment payloads. - - Master transmits a dummy payload every second and prints the ACK payload. - Slave prints the received value and sends a dummy ACK payload. -''' +""" +Templated example of using the library to transmit +and retrieve custom automatic acknowledgment payloads. +master() transmits a dummy payload every second and prints the ACK payload. +slave() prints the received value and sends a dummy ACK payload. +""" # we'll be using pipe 0 to receive and transmit ACK packets import time diff --git a/examples/nrf24l01_context_test.py b/examples/nrf24l01_context_test.py index 87cd478..56c8889 100644 --- a/examples/nrf24l01_context_test.py +++ b/examples/nrf24l01_context_test.py @@ -1,9 +1,8 @@ -''' - Simple example of library usage in context. - - This will not transmit anything, but rather - display settings after changing contexts ( & thus configurations) -''' +""" +Simple example of library usage in context. +This will not transmit anything, but rather +display settings after changing contexts ( & thus configurations) +""" import board import digitalio as dio diff --git a/examples/nrf24l01_fake_ble_test.py b/examples/nrf24l01_fake_ble_test.py new file mode 100644 index 0000000..845e14b --- /dev/null +++ b/examples/nrf24l01_fake_ble_test.py @@ -0,0 +1,36 @@ +""" +This example of using the nRF24L01 as a 'fake' Buetooth Beacon +master() sends out an advertisement once a second (default 15 secs) +""" +import time +import struct +import board +import digitalio as dio +from circuitpython_nrf24l01.fake_ble import FakeBLE + +# change these (digital output) pins accordingly +ce = dio.DigitalInOut(board.D7) +csn = dio.DigitalInOut(board.D5) + +# using board.SPI() automatically selects the MCU's +# available SPI pins, board.SCK, board.MOSI, board.MISO +spi = board.SPI() # init spi bus object + +# initialize the nRF24L01 on the spi bus object as a BLE radio using +nrf = FakeBLE(spi, csn, ce, name=b'nRF24') +# the name parameter is going to be its braodcasted BLE name +# this can be changed at any time using the attribute +nrf.name = b'RFtest' + + +def master(count=15): + with nrf as ble: + ble.open_tx_pipe() # endure the tx pip is properly addressed + for i in range(count): # advertise data this many times + if (count - i) % 5 == 0 or (count - i) < 5: + print( + count - i, 'advertisment{}left to go!'.format('s ' if count - i - 1 else ' ')) + # pack into bytearray using struct.pack() + ble.send(struct.pack('i', count)) # 'i' = 4 byte integer + # channel is automatically managed by send() per BLE specs + time.sleep(1) # wait till next broadcast diff --git a/examples/nrf24l01_interrupt_test.py b/examples/nrf24l01_interrupt_test.py index 3c2637f..042d2e4 100644 --- a/examples/nrf24l01_interrupt_test.py +++ b/examples/nrf24l01_interrupt_test.py @@ -1,10 +1,10 @@ -''' - Simple example of detecting (and verifying) the IRQ - interrupt pin on the nRF24L01 +""" +Simple example of detecting (and verifying) the IRQ +interrupt pin on the nRF24L01 - Master transmits once, receives once and intentionally fails a transmit. - Slave acts as a ponging RX node to successfully complete the tests on the master. -''' +master() transmits once, receives once and intentionally fails a transmit. +slave() acts as a ponging RX node to successfully complete the tests on the master. +""" import time import board diff --git a/examples/nrf24l01_simple_test.py b/examples/nrf24l01_simple_test.py index 24ab48f..550bb72 100644 --- a/examples/nrf24l01_simple_test.py +++ b/examples/nrf24l01_simple_test.py @@ -1,9 +1,8 @@ -''' - Simple example of library usage. - - Master transmits an incrementing double every second. - Slave polls the radio and prints the received value. -''' +""" +Simple example of library usage. +master() transmits an incrementing double every second. +slave() polls the radio and prints the received value. +""" import time import struct diff --git a/examples/nrf24l01_stream_test.py b/examples/nrf24l01_stream_test.py index e23fb1f..e24b5bc 100644 --- a/examples/nrf24l01_stream_test.py +++ b/examples/nrf24l01_stream_test.py @@ -1,8 +1,8 @@ -''' - Example of library usage for streaming multiple payloads. - Master transmits an payloads until FIFO is empty. - Slave stops listening after 3 seconds of no response. -''' +""" +Example of library usage for streaming multiple payloads. +master() transmits an payloads until FIFO is empty. +slave() stops listening after 3 seconds of no response. +""" import time import board diff --git a/setup.py b/setup.py index 400d185..5b8d2d4 100644 --- a/setup.py +++ b/setup.py @@ -1,71 +1,72 @@ -"""A setuptools based setup module. - -See: -https://packaging.python.org/en/latest/distributing.html -https://github.com/pypa/sampleproject -""" - -from setuptools import setup, find_packages -# To use a consistent encoding -from codecs import open -from os import path - -here = path.abspath(path.dirname(__file__)) -repo = 'https://github.com/2bndy5/CircuitPython_nRF24L01' - -# Get the long description from the README file -with open(path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - -setup( - name='circuitpython-nrf24l01', - - use_scm_version=True, - setup_requires=['setuptools_scm'], - - description='Circuitpython driver library for the nRF24L01 transceiver', - long_description=long_description, - long_description_content_type='text/x-rst', - - # Author details - author='Brendan Doherty', - author_email='2bndy5@gmail.com', - - install_requires=[ - 'Adafruit-Blinka', - 'adafruit-circuitpython-busdevice' - ], - - # Choose your license - license='MIT', - - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries', - 'Topic :: System :: Hardware', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - ], - - # What does your project relate to? - keywords='adafruit blinka circuitpython micropython nrf24l01 nRF24L01+ raspberry pi driver radio transceiver', - - # You can just specify the packages manually here if your project is - # simple. Or you can use find_packages(). - # TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER, - # CHANGE `py_modules=['...']` TO `packages=['...']` - py_modules=['circuitpython_nrf24l01'], - - # Specifiy your homepage URL for your project here - url=repo, - - # Extra links for the sidebar on pypi - project_urls={ - 'Documentation': 'http://circuitpython-nrf24l01.rtfd.io', - }, - download_url='{}/releases'.format(repo), -) +"""A setuptools based setup module. + +See: +https://packaging.python.org/en/latest/distributing.html +https://github.com/pypa/sampleproject +""" + +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open +from os import path + +here = path.abspath(path.dirname(__file__)) +repo = 'https://github.com/2bndy5/CircuitPython_nRF24L01' + +# Get the long description from the README file +with open(path.join(here, 'README.rst'), encoding='utf-8') as f: + long_description = f.read() + +setup( + name='circuitpython-nrf24l01', + + use_scm_version=True, + setup_requires=['setuptools_scm'], + + description='Circuitpython driver library for the nRF24L01 transceiver', + long_description=long_description, + long_description_content_type='text/x-rst', + + # Author details + author='Brendan Doherty', + author_email='2bndy5@gmail.com', + + install_requires=[ + 'Adafruit-Blinka', + 'adafruit-circuitpython-busdevice' + ], + + # Choose your license + license='MIT', + + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Libraries', + 'Topic :: System :: Hardware', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + ], + + # What does your project relate to? + keywords='adafruit blinka circuitpython micropython nrf24l01 nRF24L01+' + ' raspberry pi driver radio transceiver', + + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). + # TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER, + # CHANGE `py_modules=['...']` TO `packages=['...']` + packages=['circuitpython_nrf24l01'], + + # Specifiy your homepage URL for your project here + url=repo, + + # Extra links for the sidebar on pypi + project_urls={ + 'Documentation': 'http://circuitpython-nrf24l01.rtfd.io', + }, + download_url='{}/releases'.format(repo), +) From 62cd61bfe695f2869320241747734b14cccf6f93 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sun, 20 Oct 2019 02:53:08 -0700 Subject: [PATCH 04/17] no more long lines in base RF24 class --- circuitpython_nrf24l01/rf24.py | 76 ++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index 58931e3..a47e36f 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -20,10 +20,10 @@ # 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. -# pylint: disable=too-many-lines,line-too-long,invalid-name +# pylint: disable=too-many-lines,invalid-name """ RF24 class ------------- +============== .. important:: The nRF24L01 has 3 key features that are very interdependent of each other. Their priority of dependence is as follows: @@ -101,6 +101,7 @@ DYNPD, FEATURE) + 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 @@ -166,6 +167,7 @@ class RF24: 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, @@ -184,7 +186,7 @@ def __init__(self, spi, csn, ce, irq_DF=True): # init the SPI bus and pins self.spi = SPIDevice(spi, chip_select=csn, baudrate=1250000) - self._payload_length = payload_length # inits internal attribute + self._payload_length = payload_length # inits internal attribute self.payload_length = payload_length # last address assigned to pipe0 for reading. init to None self.pipe0_read_addr = None @@ -316,6 +318,7 @@ def _reg_write(self, reg, value=None): spi.write_readinto(out_buf, in_buf) self._status = in_buf[0] # save status byte + @property def address_length(self): """This `int` attribute specifies the length (in bytes) of addresses to be used for RX/TX @@ -612,6 +615,32 @@ def send(self, buf, ask_no_ack=False): 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 @@ -622,26 +651,9 @@ def send(self, buf, ask_no_ack=False): " buffer protocol object with a byte length of\nat least 1 " "and no greater than 32".format(i)) for b in buf: - # 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 = ( 8 (bits/byte) * (1 byte preamble + address length + payload length - # + crc length) + 9 bit packet ID ) / RF data rate (in bits/sec) = OTA transmission - # time in seconds - - # spec sheet says T_irq is (0.0000082 if self.data_rate == 1 else 0.000006) seconds - timeout = (1 + (bool(self.auto_ack) and not ask_no_ack)) * (((8 * (1 + self._addr_len + len(b) + (max(0, ((self._config & 12) >> 2) - 1)))) + 9) / (((2000000 if self._rf_setup & 0x28 == 8 else 250000) if self._rf_setup & 0x28 else 1000000) / 8)) + ((2 + (bool(self.auto_ack) and not ask_no_ack)) * 0.00013) + (0.0000082 if not self._rf_setup & 0x28 else 0.000006) + ((((self._setup_retr & 0xf0) >> 4) * 250 + 380) * (self._setup_retr & 0x0f) / 1000000) + (len(b) * 64 / self.spi.baudrate) + 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) @@ -681,7 +693,8 @@ def send(self, buf, ask_no_ack=False): # from spec sheet) result = None # T_upload is done before timeout begins (after payload write action AKA upload) - timeout = (1 + (bool(self.auto_ack) and not ask_no_ack)) * (((8 * (1 + self._addr_len + len(buf) + (max(0, ((self._config & 12) >> 2) - 1)))) + 9) / (((2000000 if self._rf_setup & 0x28 == 8 else 250000) if self._rf_setup & 0x28 else 1000000) / 8)) + ((2 + (bool(self.auto_ack) and not ask_no_ack)) * 0.00013) + (0.0000082 if not self._rf_setup & 0x28 else 0.000006) + ((((self._setup_retr & 0xf0) >> 4) * 250 + 380) * (self._setup_retr & 0x0f) / 1000000) + timeout = pl_coef * (((8 * (len(buf) + 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) # print('status: DR={} DS={} DF={}'.format(self.irq_DR, self.irq_DS, self.irq_DF)) @@ -883,7 +896,8 @@ def what_happened(self, dump_pipes=False): j = i - RX_ADDR is_open = " open " if ( self._open_pipes & (1 << j)) else "closed" - print("Pipe {} ({}) bound: {}".format(j, is_open, self._reg_read_bytes(i))) + print("Pipe {} ({}) bound: {}".format( + j, is_open, self._reg_read_bytes(i))) @property def dynamic_payloads(self): @@ -1321,7 +1335,17 @@ def resend(self): start = time.monotonic() # timeout calc assumes 32 byte payload (no way to tell when payload has already been # loaded into TX FIFO) - timeout = (1 + bool(self.auto_ack)) * (((8 * (1 + self._addr_len + 32 + (max(0, ((self._config & 12) >> 2) - 1)))) + 9) / (((2000000 if self._rf_setup & 0x28 == 8 else 250000) if self._rf_setup & 0x28 else 1000000) / 8)) + ((2 + bool(self.auto_ack)) * 0.00013) + (0.0000082 if not self._rf_setup & 0x28 else 0.000006) + ((((self._setup_retr & 0xf0) >> 4) * 250 + 380) * (self._setup_retr & 0x0f) / 1000000) + 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 From a60afd093d5131fb2e52f8ac54029c6b32103ed1 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sun, 20 Oct 2019 02:57:59 -0700 Subject: [PATCH 05/17] forgot about travis checking only 1 module --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a2269b8..528f8af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ install: - pip install --force-reinstall pylint==1.9.2 script: - - pylint --disable=invalid-name,line-too-long,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-lines,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,no-member,inconsistent-return-statements circuitpython_nrf24l01.py + - pylint --disable=invalid-name,line-too-long,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-lines,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,no-member,inconsistent-return-statements circuitpython_nrf24l01/*.py - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,missing-function-docstring,invalid-name,line-too-long,multiple-imports,unexpected-line-ending-format,no-member examples/*.py) - circuitpython-build-bundles --library_location . --filename_prefix circuitpython-nrf24l01 - cd docs && sphinx-build -E -W -b html . _build/html && cd .. From 3b6e2a466b0493af6d73efb5567d447c366db3b1 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sun, 20 Oct 2019 03:03:36 -0700 Subject: [PATCH 06/17] probably do without disabling these pylint checks --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 528f8af..0eef62f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ install: - pip install --force-reinstall pylint==1.9.2 script: - - pylint --disable=invalid-name,line-too-long,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-lines,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,no-member,inconsistent-return-statements circuitpython_nrf24l01/*.py - - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,missing-function-docstring,invalid-name,line-too-long,multiple-imports,unexpected-line-ending-format,no-member examples/*.py) + - pylint --disable=invalid-name,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,inconsistent-return-statements circuitpython_nrf24l01/*.py + - ([[ ! -d "examples" ]] || pylint --disable=missing-function-docstring,invalid-name examples/*.py) - circuitpython-build-bundles --library_location . --filename_prefix circuitpython-nrf24l01 - cd docs && sphinx-build -E -W -b html . _build/html && cd .. From e1ed4186d95fe0b3f90dea1392392a76db85fc95 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sun, 20 Oct 2019 03:06:52 -0700 Subject: [PATCH 07/17] oh yea, can't avoid these for now --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0eef62f..e0fdde1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ install: - pip install --force-reinstall pylint==1.9.2 script: - - pylint --disable=invalid-name,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,inconsistent-return-statements circuitpython_nrf24l01/*.py - - ([[ ! -d "examples" ]] || pylint --disable=missing-function-docstring,invalid-name examples/*.py) + - pylint --disable=invalid-name,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,no-member,inconsistent-return-statements circuitpython_nrf24l01/*.py + - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,missing-function-docstring,invalid-name examples/*.py) - circuitpython-build-bundles --library_location . --filename_prefix circuitpython-nrf24l01 - cd docs && sphinx-build -E -W -b html . _build/html && cd .. From 532ddae06c2d4694fdc8a51bb3fe357f3b0f8a7a Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sun, 20 Oct 2019 03:34:57 -0700 Subject: [PATCH 08/17] "no-member" check should be only locally disabled --- .travis.yml | 4 ++-- circuitpython_nrf24l01/rf24.py | 3 ++- examples/nrf24l01_ack_payload_test.py | 13 +++---------- examples/nrf24l01_context_test.py | 12 +++++++----- examples/nrf24l01_fake_ble_test.py | 3 +-- examples/nrf24l01_interrupt_test.py | 11 +++-------- examples/nrf24l01_simple_test.py | 11 ++--------- examples/nrf24l01_stream_test.py | 10 ++-------- 8 files changed, 22 insertions(+), 45 deletions(-) diff --git a/.travis.yml b/.travis.yml index e0fdde1..733e470 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ install: - pip install --force-reinstall pylint==1.9.2 script: - - pylint --disable=invalid-name,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,no-member,inconsistent-return-statements circuitpython_nrf24l01/*.py - - ([[ ! -d "examples" ]] || pylint --disable=missing-docstring,missing-function-docstring,invalid-name examples/*.py) + - pylint --disable=invalid-name,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,inconsistent-return-statements circuitpython_nrf24l01/*.py + - ([[ ! -d "examples" ]] || pylint --disable=invalid-name examples/*.py) - circuitpython-build-bundles --library_location . --filename_prefix circuitpython-nrf24l01 - cd docs && sphinx-build -E -W -b html . _build/html && cd .. diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index a47e36f..97d0f9a 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -282,6 +282,7 @@ def __enter__(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: @@ -317,7 +318,7 @@ def _reg_write(self, reg, value=None): 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): diff --git a/examples/nrf24l01_ack_payload_test.py b/examples/nrf24l01_ack_payload_test.py index 22ae265..10b8a4a 100644 --- a/examples/nrf24l01_ack_payload_test.py +++ b/examples/nrf24l01_ack_payload_test.py @@ -1,11 +1,7 @@ """ -Templated example of using the library to transmit +Simple example of using the library to transmit and retrieve custom automatic acknowledgment payloads. -master() transmits a dummy payload every second and prints the ACK payload. -slave() prints the received value and sends a dummy ACK payload. """ -# we'll be using pipe 0 to receive and transmit ACK packets - import time import board import digitalio as dio @@ -45,8 +41,8 @@ # need to be in a buffer protocol object (bytearray) ACK = b'World ' - def master(count=5): # count = 5 will only transmit 5 packets + """Transmits a dummy payload every second and prints the ACK payload""" # recommended behavior is to keep in TX mode while idle nrf.listen = False # put radio in TX mode @@ -77,11 +73,8 @@ def master(count=5): # count = 5 will only transmit 5 packets time.sleep(1) counter -= 1 -# running slave to only fetch/receive count number of packets -# count = 3 will mimic a full RX FIFO behavior via nrf.listen = False - - def slave(count=3): + """Prints the received value and sends a dummy ACK payload""" # set address of TX node into an RX pipe. NOTE you MUST specify # which pipe number to use for RX, we'll be using pipe 0 nrf.open_rx_pipe(0, address) diff --git a/examples/nrf24l01_context_test.py b/examples/nrf24l01_context_test.py index 56c8889..456c58b 100644 --- a/examples/nrf24l01_context_test.py +++ b/examples/nrf24l01_context_test.py @@ -3,7 +3,6 @@ This will not transmit anything, but rather display settings after changing contexts ( & thus configurations) """ - import board import digitalio as dio from circuitpython_nrf24l01 import RF24 @@ -31,10 +30,13 @@ # CRC is set to 1 byte long # data rate is set to 250Kbps # payload length is set to 8 bytes -# address length is set to 3 bytes -basicRF = RF24(spi, csn, ce, dynamic_payloads=False, irq_DR=False, irq_DS=False, - channel=2, crc=1, data_rate=250, payload_length=8, address_length=3, ard=1000, arc=15) - +# NOTE address length is set to 3 bytes +basicRF = RF24(spi, csn, ce, + dynamic_payloads=False, + irq_DR=False, irq_DS=False, + channel=2, crc=1, data_rate=250, + payload_length=8, address_length=3, + ard=1000, arc=15) print("\nsettings configured by the nrf object") with nrf: diff --git a/examples/nrf24l01_fake_ble_test.py b/examples/nrf24l01_fake_ble_test.py index 845e14b..8166902 100644 --- a/examples/nrf24l01_fake_ble_test.py +++ b/examples/nrf24l01_fake_ble_test.py @@ -1,6 +1,5 @@ """ This example of using the nRF24L01 as a 'fake' Buetooth Beacon -master() sends out an advertisement once a second (default 15 secs) """ import time import struct @@ -22,8 +21,8 @@ # this can be changed at any time using the attribute nrf.name = b'RFtest' - def master(count=15): + """Sends out an advertisement once a second (default 15 secs)""" with nrf as ble: ble.open_tx_pipe() # endure the tx pip is properly addressed for i in range(count): # advertise data this many times diff --git a/examples/nrf24l01_interrupt_test.py b/examples/nrf24l01_interrupt_test.py index 042d2e4..670ffa6 100644 --- a/examples/nrf24l01_interrupt_test.py +++ b/examples/nrf24l01_interrupt_test.py @@ -1,11 +1,7 @@ """ Simple example of detecting (and verifying) the IRQ interrupt pin on the nRF24L01 - -master() transmits once, receives once and intentionally fails a transmit. -slave() acts as a ponging RX node to successfully complete the tests on the master. """ - import time import board import digitalio as dio @@ -30,8 +26,8 @@ nrf = RF24(spi, csn, ce) nrf.arc = 15 # turn up automatic retries to the max. default is 3 - def master(timeout=5): # will only wait 5 seconds for slave to respond + """Transmits once, receives once, and intentionally fails a transmit""" # set address of RX node into a TX pipe nrf.open_tx_pipe(address) # ensures the nRF24L01 is in TX mode @@ -85,8 +81,8 @@ def master(timeout=5): # will only wait 5 seconds for slave to respond 'IRQ on data fail is not active, check your wiring and call interrupt_config()') nrf.clear_status_flags() # clear all flags for next test - -def slave(timeout=10): # will listen for 10 seconds before timming out +def slave(timeout=10): # will listen for 10 seconds before timing out + """Acts as a ponging RX node to successfully complete the tests on the master""" # setup radio to recieve ping nrf.open_rx_pipe(0, address) nrf.listen = 1 @@ -103,7 +99,6 @@ def slave(timeout=10): # will listen for 10 seconds before timming out nrf.send(b'pong') # send a payload to complete the on data ready test # we're done on this side - print("""\ nRF24L01 Interrupt test\n\ Run master() to run IRQ pin tests\n\ diff --git a/examples/nrf24l01_simple_test.py b/examples/nrf24l01_simple_test.py index 550bb72..26f9885 100644 --- a/examples/nrf24l01_simple_test.py +++ b/examples/nrf24l01_simple_test.py @@ -1,9 +1,6 @@ """ Simple example of library usage. -master() transmits an incrementing double every second. -slave() polls the radio and prints the received value. """ - import time import struct import board @@ -25,8 +22,8 @@ # initialize the nRF24L01 on the spi bus object nrf = RF24(spi, csn, ce) - def master(count=5): # count = 5 will only transmit 5 packets + """Transmits an incrementing integer every second""" # set address of RX node into a TX pipe nrf.open_tx_pipe(address) # ensures the nRF24L01 is in TX mode @@ -54,11 +51,8 @@ def master(count=5): # count = 5 will only transmit 5 packets time.sleep(1) counter -= 1 -# running slave to only fetch/receive count number of packets -# count = 3 will mimic a full RX FIFO behavior via nrf.listen = False - - def slave(count=3): + """Polls the radio and prints the received value""" # set address of TX node into an RX pipe. NOTE you MUST specify # which pipe number to use for RX, we'll be using pipe 0 # pipe number options range [0,5] @@ -88,7 +82,6 @@ def slave(count=3): # recommended behavior is to keep in TX mode while idle nrf.listen = False # put the nRF24L01 is in TX mode - print("""\ nRF24L01 Simple test.\n\ Run slave() on receiver\n\ diff --git a/examples/nrf24l01_stream_test.py b/examples/nrf24l01_stream_test.py index e24b5bc..19d9b4b 100644 --- a/examples/nrf24l01_stream_test.py +++ b/examples/nrf24l01_stream_test.py @@ -1,9 +1,6 @@ """ Example of library usage for streaming multiple payloads. -master() transmits an payloads until FIFO is empty. -slave() stops listening after 3 seconds of no response. """ - import time import board import digitalio as dio @@ -35,8 +32,8 @@ buffers.append(buff) del buff - def master(count=1): # count = 5 will transmit the list 5 times + """Transmits a massive buffer of payloads""" # set address of RX node into a TX pipe nrf.open_tx_pipe(address) # ensures the nRF24L01 is in TX mode @@ -52,10 +49,8 @@ def master(count=1): # count = 5 will transmit the list 5 times success_percentage /= SIZE * count print('successfully sent', success_percentage * 100, '%') -# running slave to only fetch/receive & count number of packets - - def slave(timeout=5): + """Stops listening after timeout with no response""" # set address of TX node into an RX pipe. NOTE you MUST specify # which pipe number to use for RX, we'll be using pipe 0 # pipe number options range [0,5] @@ -76,7 +71,6 @@ def slave(timeout=5): # recommended behavior is to keep in TX mode while idle nrf.listen = False # put the nRF24L01 is in TX mode - print("""\ nRF24L01 Stream test\n\ Run slave() on receiver\n\ From 02e1fad3130966b38e5ce3b458deeb2920be0c6a Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Mon, 21 Oct 2019 15:44:06 -0700 Subject: [PATCH 09/17] registers now a dict in stand-alone module --- circuitpython_nrf24l01/registers.py | 27 +++--- circuitpython_nrf24l01/rf24.py | 125 +++++++++++++--------------- 2 files changed, 71 insertions(+), 81 deletions(-) diff --git a/circuitpython_nrf24l01/registers.py b/circuitpython_nrf24l01/registers.py index 9356e0d..b24d902 100644 --- a/circuitpython_nrf24l01/registers.py +++ b/circuitpython_nrf24l01/registers.py @@ -1,13 +1,16 @@ """nRF24L01(+) registers""" -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 rangeing [0,5]:[0xA:0xF] -RX_PW = 0x11 # RX payload widths on pipes ranging [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 enablers/disablers for dynamic payloads, auto-ACK, and custom ACK features + +REGISTERS = { + '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 rangeing [0,5]:[0xA:0xF] + 'RX_PW' : 0x11, # RX payload widths on pipes ranging [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 toggles for dynamic payloads, auto-ACK, and custom ACK features +} diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index 97d0f9a..15b54c0 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -88,18 +88,7 @@ __repo__ = "https://github.com/2bndy5/CircuitPython_nRF24L01.git" import time from adafruit_bus_device.spi_device import SPIDevice -from .registers import (CONFIG, - EN_AA, - EN_RX, - SETUP_AW, - SETUP_RETR, - RF_CH, - RF_SETUP, - RX_ADDR, - RX_PW, - FIFO, - DYNPD, - FEATURE) +from .registers import REGISTERS as REG class RF24: @@ -207,15 +196,15 @@ def __init__(self, spi, csn, ce, 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 + self._reg_write(REG['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 + if self._reg_read(REG['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))) + print(bin(self._reg_read(REG['CONFIG']))) raise RuntimeError("nRF24L01 Hardware not responding") # configure the SETUP_RETR register @@ -255,26 +244,26 @@ def __init__(self, spi, csn, ce, # 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 + self._reg_write(REG['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 + self._reg_write(REG['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): - self._reg_write(CONFIG, self._config & + self._reg_write(REG['CONFIG'], self._config & 0x7C) # dump IRQ and CRC data to CONFIG register - self._reg_write(RF_SETUP, self._rf_setup) # dump to RF_SETUP register + self._reg_write(REG['RF_SETUP'], self._rf_setup) # dump to RF_SETUP register # update open pipe info from current state of - self._open_pipes = self._reg_read(EN_RX) + self._open_pipes = self._reg_read(REG['EN_RX']) # EN_RXADDR register - 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 + self._reg_write(REG['DYNPD'], self._dyn_pl) # dump to DYNPD register + self._reg_write(REG['EN_AA'], self._aa) # dump to EN_AA register + self._reg_write(REG['FEATURE'], self._features) # dump to FEATURE register # dump to SETUP_RETR register - self._reg_write(SETUP_RETR, self._setup_retr) + self._reg_write(REG['SETUP_RETR'], self._setup_retr) self.address_length = self._addr_len # writes directly to SETUP_AW register self.channel = self._channel # writes directly to RF_CH register return self @@ -329,7 +318,7 @@ def address_length(self): 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 + return self._reg_read(REG['SETUP_AW']) + 2 @address_length.setter def address_length(self, length): @@ -338,7 +327,7 @@ def address_length(self, length): 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) + self._reg_write(REG['SETUP_AW'], length - 2) else: raise ValueError( "address length can only be set in range [3,5] bytes") @@ -375,19 +364,19 @@ def close_rx_pipe(self, pipe_number): """ 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 + self._open_pipes = self._reg_read(REG['EN_RX']) # refresh data # 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._reg_write_bytes(pipe_number + REG['RX_ADDR'], b'\xe7' * 5) elif pipe_number < 2: # write the full address for pipe 1 - self._reg_write_bytes(pipe_number + RX_ADDR, b'\xc2' * 5) + self._reg_write_bytes(pipe_number + REG['RX_ADDR'], b'\xc2' * 5) else: # write just LSB for 2 <= pipes >= 5 - self._reg_write(pipe_number + RX_ADDR, pipe_number + 0xc1) + self._reg_write(pipe_number + REG['RX_ADDR'], 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) + self._reg_write(REG['EN_RX'], self._open_pipes) def open_rx_pipe(self, pipe_number, address): """This function is used to open a specific data pipe for OTA (over the air) RX @@ -421,16 +410,16 @@ def open_rx_pipe(self, pipe_number, address): # open_tx_pipe() will appropriate the address on pipe 0 if auto_ack is enabled for # TX mode self.pipe0_read_addr = address - self._reg_write_bytes(RX_ADDR + pipe_number, address) + self._reg_write_bytes(REG['RX_ADDR'] + pipe_number, address) else: # only write LSByte if pipe_number is not 0 or 1 - self._reg_write(RX_ADDR + pipe_number, address[len(address) - 1]) + self._reg_write(REG['RX_ADDR'] + pipe_number, address[len(address) - 1]) # now manage the pipe - self._open_pipes = self._reg_read(EN_RX) # refresh data + self._open_pipes = self._reg_read(REG['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) + self._reg_write(REG['EN_RX'], self._open_pipes) # payload_length is handled in _start_listening() @property @@ -473,20 +462,20 @@ def _start_listening(self): self.ce.value = 0 if not self.dynamic_payloads: # using static payload lengths - self._open_pipes = self._reg_read(EN_RX) # refresh data + self._open_pipes = self._reg_read(REG['EN_RX']) # refresh data for i in range(6): # write payload_length to all open pipes using the RX_PW_Px registers if self._open_pipes & (1 << i): # is pipe open? # write accordingly - self._reg_write(RX_PW + i, self.payload_length) + self._reg_write(REG['RX_PW'] + i, self.payload_length) 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._reg_write_bytes(REG['RX_ADDR'], self.pipe0_read_addr) # power up radio & set radio in RX mode self._config = self._config & 0xFC | 3 - self._reg_write(CONFIG, self._config) + self._reg_write(REG['CONFIG'], self._config) time.sleep(0.00015) # mandatory wait time to power up radio self.flush_rx() # spec sheet says "used in RX mode" self.clear_status_flags(True, False, False) # only Data Ready flag @@ -502,7 +491,7 @@ def _stop_listening(self): 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) + self._reg_write(REG['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) @@ -816,11 +805,11 @@ def interrupt_config(self, data_recv=True, data_sent=True, data_fail=True): `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 + self._config = self._reg_read(REG['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) + self._reg_write(REG['CONFIG'], self._config) def what_happened(self, dump_pipes=False): """This debuggung function aggregates and outputs all status/condition related information @@ -893,8 +882,8 @@ def what_happened(self, dump_pipes=False): print("Primary Mode_____________{} Power Mode___________{}".format( 'RX' if self.listen else 'TX', pwr)) if dump_pipes: - for i in range(RX_ADDR, RX_ADDR + 6): - j = i - RX_ADDR + for i in range(REG['RX_ADDR'], REG['RX_ADDR'] + 6): + j = i - REG['RX_ADDR'] is_open = " open " if ( self._open_pipes & (1 << j)) else "closed" print("Pipe {} ({}) bound: {}".format( @@ -915,17 +904,17 @@ def dynamic_payloads(self): @dynamic_payloads.setter def dynamic_payloads(self, enable): assert isinstance(enable, (bool, int)) - self._features = self._reg_read(FEATURE) # refresh data + self._features = self._reg_read(REG['FEATURE']) # refresh data if self.auto_ack and not enable: # disabling and auto_ack is still on self.auto_ack = enable # disable auto_ack as this is required for it # 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) + self._reg_write(REG['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) + self._reg_write(REG['DYNPD'], self._dyn_pl) @property def payload_length(self): @@ -977,7 +966,7 @@ def auto_ack(self, enable): self.dynamic_payloads = enable # enable dynamic_payloads # 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 + self._reg_write(REG['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 @@ -1004,9 +993,9 @@ def ack(self, enable): self.auto_ack = True # ensure auto_ack feature is enabled else: # setting auto_ack feature automatically updated the _features attribute, so - self._features = self._reg_read(FEATURE) # refresh data here + self._features = self._reg_read(REG['FEATURE']) # refresh data here self._features = (self._features & 5) | (2 if enable else 0) - self._reg_write(FEATURE, self._features) + self._reg_write(REG['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 @@ -1086,7 +1075,7 @@ def data_rate(self): 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 + self._rf_setup = self._reg_read(REG['RF_SETUP']) # refresh data return (2 if self._rf_setup & 0x28 == 8 else 250) if self._rf_setup & 0x28 else 1 @data_rate.setter @@ -1098,7 +1087,7 @@ def data_rate(self, 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) + self._reg_write(REG['RF_SETUP'], self._rf_setup) else: raise ValueError( "data rate must be one of the following ([M,M,K]bps): 1, 2, 250") @@ -1110,13 +1099,13 @@ def channel(self): 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) + return self._reg_read(REG['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 + self._reg_write(REG['RF_CH'], channel) # always writes to reg else: raise ValueError("channel acn only be set in range [0,125]") @@ -1136,7 +1125,7 @@ def crc(self): .. note:: The nRF24L01 automatically enables CRC if automatic acknowledgment feature is enabled (see `auto_ack` attribute). """ - self._config = self._reg_read(CONFIG) # refresh data + self._config = self._reg_read(REG['CONFIG']) # refresh data return max(0, ((self._config & 12) >> 2) - 1) # this works @crc.setter @@ -1175,7 +1164,7 @@ def power(self): .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). """ - self._config = self._reg_read(CONFIG) # refresh data + self._config = self._reg_read(REG['CONFIG']) # refresh data return bool(self._config & 2) @power.setter @@ -1186,7 +1175,7 @@ def power(self, 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) + self._reg_write(REG['CONFIG'], self._config) # power up/down takes < 150 µs + 4 µs time.sleep(0.00016) @@ -1199,18 +1188,16 @@ def arc(self): A valid input value must be in range [0,15]. Otherwise a `ValueError` exception is thrown. Default is set to 3. """ - # SETUP_AW + 1 = SETUP_RETR register - self._setup_retr = self._reg_read(SETUP_AW + 1) # refresh data + self._setup_retr = self._reg_read(REG['SETUP_RETR']) # refresh data return self._setup_retr & 0x0f @arc.setter def arc(self, count): - # SETUP_AW + 1 = SETUP_RETR register 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_AW + 1, self._setup_retr) + self._reg_write(REG['SETUP_RETR'], self._setup_retr) else: raise ValueError( "automatic re-transmit count(/attempts) must in range [0,15]") @@ -1234,7 +1221,7 @@ def ard(self): See `data_rate` attribute on how to set the data rate of the nRF24L01's transmissions. """ - self._setup_retr = self._reg_read(SETUP_AW + 1) # refresh data + self._setup_retr = self._reg_read(REG['SETUP_RETR']) # refresh data return ((self._setup_retr & 0xf0) >> 4) * 250 + 250 @ard.setter @@ -1245,7 +1232,7 @@ def ard(self, delta_t): # save changes to register(& its Shadow) self._setup_retr = (int((delta_t - 250) / 250) << 4) | (self._setup_retr & 0x0F) - self._reg_write(SETUP_AW + 1, self._setup_retr) + self._reg_write(REG['SETUP_RETR'], self._setup_retr) else: raise ValueError("automatic re-transmit delay can only be a multiple of 250 in range " "[250,4000]") @@ -1265,8 +1252,8 @@ def pa_level(self): 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 + self._rf_setup = self._reg_read(REG['RF_SETUP']) # refresh data + return (3 - ((self._rf_setup & REG['RF_SETUP']) >> 1)) * -6 @pa_level.setter def pa_level(self, power): @@ -1276,7 +1263,7 @@ def pa_level(self, power): 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) + self._reg_write(REG['RF_SETUP'], self._rf_setup) else: raise ValueError( "power amplitude must be one of the following (dBm): -18, -12, -6, 0") @@ -1324,7 +1311,7 @@ def resend(self): 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) + self._reg_write(REG['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 @@ -1427,7 +1414,7 @@ def write(self, buf=None, ask_no_ack=False): 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._config = (self._reg_read(REG['CONFIG']) & 0x7c) | 2 self._reg_write(0, self._config) # power up/down takes < 150 µs + 4 µs time.sleep(0.00016) @@ -1504,7 +1491,7 @@ def fifo(self, tx=False, empty=None): """ 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 + self._fifo = self._reg_read(REG['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))) From 8a3e78b4314bbbe40ff706a6b1a47359f5c56481 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 6 Nov 2019 00:31:37 -0800 Subject: [PATCH 10/17] very experimental!!! - docs changes to separate basic from advanced API. - moved payload_length write to open_rx_pipe() (independent of dynamic_payloads setting) - changed open_tx_pipe() to directly write rx address when ACK is enabled - adapted examples to use GPIO4 instead of GPIO9 for nrf's CE pin --- README.rst | 2 +- circuitpython_nrf24l01/fake_ble.py | 3 - circuitpython_nrf24l01/registers.py | 24 ++-- circuitpython_nrf24l01/rf24.py | 156 +++++++++----------------- docs/api.rst | 106 ++++++++++++++++- examples/nrf24l01_ack_payload_test.py | 2 +- examples/nrf24l01_context_test.py | 2 +- examples/nrf24l01_interrupt_test.py | 2 +- examples/nrf24l01_simple_test.py | 2 +- examples/nrf24l01_stream_test.py | 2 +- 10 files changed, 173 insertions(+), 128 deletions(-) diff --git a/README.rst b/README.rst index 4308fc7..fc94404 100644 --- a/README.rst +++ b/README.rst @@ -104,7 +104,7 @@ The nRF24L01 is controlled through SPI so there are 3 pins (SCK, MOSI, & MISO) t +------------+----------------+----------------+ | VCC | 3V | 3.3V | +------------+----------------+----------------+ -| CE | GPIO9 (CE1) | D9 | +| CE | GPIO4 | D4 | +------------+----------------+----------------+ | CSN | GPIO5 | D5 | +------------+----------------+----------------+ diff --git a/circuitpython_nrf24l01/fake_ble.py b/circuitpython_nrf24l01/fake_ble.py index ce8d546..282cf8d 100644 --- a/circuitpython_nrf24l01/fake_ble.py +++ b/circuitpython_nrf24l01/fake_ble.py @@ -20,9 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. """ -FakeBLE class ---------------- - This module uses the RF24 module to make the nRF24L01 imitate a Bluetooth-Low-Emissions (BLE) beacon. A BLE beacon can send (referred to as advertise) data to any BLE compatible device (ie smart devices with Bluetooth 4.0 or later) that is listening. diff --git a/circuitpython_nrf24l01/registers.py b/circuitpython_nrf24l01/registers.py index b24d902..75b1473 100644 --- a/circuitpython_nrf24l01/registers.py +++ b/circuitpython_nrf24l01/registers.py @@ -1,16 +1,16 @@ """nRF24L01(+) registers""" REGISTERS = { - '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 rangeing [0,5]:[0xA:0xF] - 'RX_PW' : 0x11, # RX payload widths on pipes ranging [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 toggles for dynamic payloads, auto-ACK, and custom ACK features + '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 rangeing [0,5]:[0xA:0xF] + 'RX_PW' : 0x11,# RX payload widths on pipes ranging [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 toggles for dynamic payloads, auto-ACK, and custom ACK features } diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index 15b54c0..65649d1 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -22,67 +22,7 @@ # THE SOFTWARE. # pylint: disable=too-many-lines,invalid-name """ -RF24 class -============== - -.. important:: The nRF24L01 has 3 key features that are very interdependent of each other. Their - priority of dependence is as follows: - - 1. `dynamic_payloads` feature allowing either TX/RX nRF24L01 to be able to send/receive - payloads with their size written into the payloads' packet. With this disabled, both RX/TX - nRF24L01 must use matching `payload_length` attributes. - 2. `auto_ack` feature provides transmission verification by using the RX nRF24L01 to - automatically and imediatedly send an acknowledgment (ACK) packet in response to freshly - received payloads. `auto_ack` requires `dynamic_payloads` to be enabled. - 3. `ack` feature allows the MCU to append a payload to the ACK packet, thus instant - bi-directional communication. A transmitting ACK payload must be loaded into the nRF24L01's - TX FIFO buffer (done using `load_ack()`) BEFORE receiving the payload that is to be - acknowledged. Once transmitted, the payload is released from the TX FIFO buffer. This - feature obviously requires the `auto_ack` feature enabled. - -Remeber that the nRF24L01's FIFO (first-in,first-out) buffer has 3 levels. This means that there -can be up to 3 payloads waiting to be read (RX) and up to 3 payloads waiting to be transmit (TX). - -With the `auto_ack` feature enabled you get: - - * cycle redundancy checking (`crc`) automatically enabled - * to change amount of automatic re-transmit attempts and the delay time between them. See the - `arc` and `ard` attributes. - -.. note:: A word on pipes vs addresses vs channels. - - You should think of the data pipes as a vehicle that you (the payload) get into. Continuing the - analogy, the specified address is not the address of an nRF24L01 radio, rather it is more - like a route that connects the endpoints. There are only six data pipes on the nRF24L01, - thus it can simultaneously listen to a maximum of 6 other nRF24L01 radios (can only talk to - 1 at a time). When assigning addresses to a data pipe, you can use any 5 byte long address - you can think of (as long as the last byte is unique among simultaneously broadcasting - addresses), so you're not limited to communicating to the same 6 radios (more on this when - we support "Multiciever" mode). Also the radio's channel is not be confused with the - radio's pipes. Channel selection is a way of specifying a certain radio frequency - (frequency = [2400 + channel] MHz). Channel defaults to 76 (like the arduino library), but - options range from 0 to 125 -- that's 2.4 GHz to 2.525 GHz. The channel can be tweaked to - find a less occupied frequency amongst (Bluetooth & WiFi) ambient signals. - -.. warning:: For successful transmissions, most of the endpoint trasceivers' settings/features must - match. These settings/features include: - - * The RX pipe's address on the receiving nRF24L01 MUST match the TX pipe's address on the - transmitting nRF24L01 - * `address_length` - * `channel` - * `data_rate` - * `dynamic_payloads` - * `payload_length` only when `dynamic_payloads` is disabled - * `auto_ack` - * custom `ack` payloads - * `crc` - - In fact the only attributes that aren't required to match on both endpoint transceivers would - be the identifying data pipe number (passed to `open_rx_pipe()`), `pa_level`, `arc`, & - `ard` attributes. The ``ask_no_ack`` feature can be used despite the settings/features - configuration (see :meth:`~circuitpython_nrf24l01.rf24.RF24.send` & `write()` function - parameters for more details). +rf24 module containing the base class RF24 """ __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/2bndy5/CircuitPython_nRF24L01.git" @@ -90,7 +30,6 @@ from adafruit_bus_device.spi_device import SPIDevice from .registers import REGISTERS as REG - 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 @@ -156,7 +95,6 @@ class RF24: 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, @@ -343,11 +281,14 @@ def open_tx_pipe(self, address): .. 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. + 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: - self.open_rx_pipe(0, address) + # settings need to match on both transceivers: dynamic_payloads and payload_length + self._reg_write_bytes(REG['RX_ADDR'], address) # using pipe 0 # let self._open_pipes only reflect RX pipes self._reg_write_bytes(0x10, address) # 0x10 = TX_ADDR register else: @@ -355,24 +296,27 @@ def open_tx_pipe(self, address): "to the address_length attribute (currently set to" " {})".format(self.address_length)) - def close_rx_pipe(self, pipe_number): - """This function is used to close a specific data pipe for OTA (over the air) RX + 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(REG['EN_RX']) # refresh data - # 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 + REG['RX_ADDR'], b'\xe7' * 5) - elif pipe_number < 2: # write the full address for pipe 1 - self._reg_write_bytes(pipe_number + REG['RX_ADDR'], b'\xc2' * 5) - else: # write just LSB for 2 <= pipes >= 5 - self._reg_write(pipe_number + REG['RX_ADDR'], pipe_number + 0xc1) + 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 + REG['RX_ADDR'], b'\xe7' * 5) + elif pipe_number < 2: # write the full address for pipe 1 + self._reg_write_bytes(pipe_number + REG['RX_ADDR'], b'\xc2' * 5) + else: # write just LSB for 2 <= pipes >= 5 + self._reg_write(pipe_number + REG['RX_ADDR'], 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) @@ -382,18 +326,18 @@ 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 (handled when the `listen` attribute changes from `False` to `True`). + 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 of the receiving nRF24L01. This must have a - byte length equal to the `address_length` attribute (see `address_length` attribute). - Otherwise a `ValueError` exception is thrown. If using a ``pipe_number`` greater than 1 - , then only the LSByte of the address is written, so make sure LSByte (last character) - is unique among other simultaneously receiving addresses). - - .. note:: The nRF24L01 shares the addresses' MSBytes (address[0:4]) on data pipes 2 through - 5. + :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]") @@ -412,15 +356,18 @@ def open_rx_pipe(self, pipe_number, address): self.pipe0_read_addr = address self._reg_write_bytes(REG['RX_ADDR'] + pipe_number, address) else: - # only write LSByte if pipe_number is not 0 or 1 - self._reg_write(REG['RX_ADDR'] + pipe_number, address[len(address) - 1]) + # only write MSByte if pipe_number is not 0 or 1 + self._reg_write(REG['RX_ADDR'] + pipe_number, address[0]) # now manage the pipe self._open_pipes = self._reg_read(REG['EN_RX']) # refresh data # enable the specified data pipe self._open_pipes = self._open_pipes | (1 << pipe_number) self._reg_write(REG['EN_RX'], self._open_pipes) - # payload_length is handled in _start_listening() + + # now adjust payload_length accordingly despite dynamic_payload setting + # radio only uses this info in RX mode when dynamic_payloads == True + self._reg_write(REG['RX_PW'] + pipe_number, self.payload_length) @property def listen(self): @@ -461,14 +408,6 @@ def _start_listening(self): if self.ce.value: self.ce.value = 0 - if not self.dynamic_payloads: # using static payload lengths - self._open_pipes = self._reg_read(REG['EN_RX']) # refresh data - for i in range(6): - # write payload_length to all open pipes using the RX_PW_Px registers - if self._open_pipes & (1 << i): # is pipe open? - # write accordingly - self._reg_write(REG['RX_PW'] + i, self.payload_length) - 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(REG['RX_ADDR'], self.pipe0_read_addr) @@ -476,7 +415,7 @@ def _start_listening(self): # power up radio & set radio in RX mode self._config = self._config & 0xFC | 3 self._reg_write(REG['CONFIG'], self._config) - time.sleep(0.00015) # mandatory wait time to power up radio + 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 @@ -525,7 +464,7 @@ def recv(self): .. 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 - regaurded as a valid payload. + 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( @@ -852,10 +791,11 @@ def what_happened(self, dump_pipes=False): registers. Default is `False` and skips this extra information. """ watchdog = self._reg_read(8) # 8 == OBSERVE_TX register - pwr = ( - 'Standby-II' if self.ce.value else 'Standby-I') if (self._config & 2) else 'Off' 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)) @@ -880,14 +820,20 @@ def what_happened(self, dump_pipes=False): '_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', pwr)) + 'RX' if self.listen else 'TX', + ('Standby-II' if self.ce.value else 'Standby-I') if self._config & 2 else 'Off')) if dump_pipes: + shared_bytes = b'' for i in range(REG['RX_ADDR'], REG['RX_ADDR'] + 6): j = i - REG['RX_ADDR'] - is_open = " open " if ( - self._open_pipes & (1 << j)) else "closed" - print("Pipe {} ({}) bound: {}".format( - j, is_open, self._reg_read_bytes(i))) + is_open = "( open )" if ( + self._open_pipes & (1 << j)) else "(closed)" + if j <= 1: # print full address + shared_bytes = self._reg_read_bytes(i) + print("Pipe", j, is_open, "bound:", shared_bytes) + else: # print shared bytes + unique byte = actual address used by radio + specific_address = bytearray([self._reg_read(i)]) + shared_bytes[1:] + print("Pipe", j, is_open, "bound:", specific_address) @property def dynamic_payloads(self): @@ -918,7 +864,7 @@ def dynamic_payloads(self, enable): @property def payload_length(self): - """This `int` attribute specifies the length (in bytes) of payload that is regaurded, + """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. diff --git a/docs/api.rst b/docs/api.rst index 039de3e..e756c55 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,9 +4,111 @@ .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. use this format as the module name: "adafruit_foo.foo" +.. currentmodule:: circuitpython_nrf24l01.rf24 -.. automodule:: circuitpython_nrf24l01.rf24 - :members: +RF24 class +============== + +.. important:: The nRF24L01 has 3 key features that are very interdependent of each other. Their + priority of dependence is as follows: + + 1. `dynamic_payloads` feature allowing either TX/RX nRF24L01 to be able to send/receive + payloads with their size written into the payloads' packet. With this disabled, both RX/TX + nRF24L01 must use matching `payload_length` attributes. + 2. `auto_ack` feature provides transmission verification by using the RX nRF24L01 to + automatically and imediatedly send an acknowledgment (ACK) packet in response to freshly + received payloads. `auto_ack` requires `dynamic_payloads` to be enabled. + 3. `ack` feature allows the MCU to append a payload to the ACK packet, thus instant + bi-directional communication. A transmitting ACK payload must be loaded into the nRF24L01's + TX FIFO buffer (done using `load_ack()`) BEFORE receiving the payload that is to be + acknowledged. Once transmitted, the payload is released from the TX FIFO buffer. This + feature obviously requires the `auto_ack` feature enabled. + +Remeber that the nRF24L01's FIFO (first-in,first-out) buffer has 3 levels. This means that there +can be up to 3 payloads waiting to be read (RX) and up to 3 payloads waiting to be transmit (TX). + +With the `auto_ack` feature enabled you get: + + * cycle redundancy checking (`crc`) automatically enabled + * to change amount of automatic re-transmit attempts and the delay time between them. See the + `arc` and `ard` attributes. + +.. note:: A word on pipes vs addresses vs channels. + + You should think of the data pipes as a vehicle that you (the payload) get into. Continuing the + analogy, the specified address is not the address of an nRF24L01 radio, rather it is more + like a route that connects the endpoints. There are only six data pipes on the nRF24L01, + thus it can simultaneously listen to a maximum of 6 other nRF24L01 radios (can only talk to + 1 at a time). When assigning addresses to a data pipe, you can use any 5 byte long address + you can think of (as long as the last byte is unique among simultaneously broadcasting + addresses), so you're not limited to communicating to the same 6 radios (more on this when + we support "Multiciever" mode). Also the radio's channel is not be confused with the + radio's pipes. Channel selection is a way of specifying a certain radio frequency + (frequency = [2400 + channel] MHz). Channel defaults to 76 (like the arduino library), but + options range from 0 to 125 -- that's 2.4 GHz to 2.525 GHz. The channel can be tweaked to + find a less occupied frequency amongst (Bluetooth & WiFi) ambient signals. + +.. warning:: For successful transmissions, most of the endpoint trasceivers' settings/features must + match. These settings/features include: + + * The RX pipe's address on the receiving nRF24L01 MUST match the TX pipe's address on the + transmitting nRF24L01 + * `address_length` + * `channel` + * `data_rate` + * `dynamic_payloads` + * `payload_length` only when `dynamic_payloads` is disabled + * `auto_ack` + * custom `ack` payloads + * `crc` + + In fact the only attributes that aren't required to match on both endpoint transceivers would + be the identifying data pipe number (passed to `open_rx_pipe()`), `pa_level`, `arc`, & + `ard` attributes. The ``ask_no_ack`` feature can be used despite the settings/features + configuration (see :meth:`~circuitpython_nrf24l01.rf24.RF24.send` & `write()` function + parameters for more details). + +Basic API +--------- + +.. autoclass:: circuitpython_nrf24l01.rf24.RF24 + :members: address_length, open_tx_pipe, close_rx_pipe, open_rx_pipe, listen, any, recv, send + +Advanced API +------------ + +.. class:: circuitpython_nrf24l01.rf24.RF24 + + .. automethod:: what_happened + .. autoattribute:: dynamic_payloads + .. autoattribute:: payload_length + .. autoattribute:: auto_ack + .. autoattribute:: irq_DR + .. autoattribute:: irq_DF + .. autoattribute:: irq_DS + .. automethod:: clear_status_flags + .. automethod:: interrupt_config + .. autoattribute:: ack + .. automethod:: load_ack + .. automethod:: read_ack + .. autoattribute:: data_rate + .. autoattribute:: channel + .. autoattribute:: crc + .. autoattribute:: power + .. autoattribute:: arc + .. autoattribute:: ard + .. autoattribute:: pa_level + .. autoattribute:: tx_full + .. automethod:: update + .. automethod:: resend + .. automethod:: write + .. automethod:: flush_rx + .. automethod:: flush_tx + .. automethod:: fifo + .. automethod:: pipe + +Fake BLE API +============ .. automodule:: circuitpython_nrf24l01.fake_ble :members: diff --git a/examples/nrf24l01_ack_payload_test.py b/examples/nrf24l01_ack_payload_test.py index 10b8a4a..fd5dc8c 100644 --- a/examples/nrf24l01_ack_payload_test.py +++ b/examples/nrf24l01_ack_payload_test.py @@ -8,7 +8,7 @@ from circuitpython_nrf24l01 import RF24 # change these (digital output) pins accordingly -ce = dio.DigitalInOut(board.D9) # AKA board.CE1 on the rasberry pi +ce = dio.DigitalInOut(board.D4) csn = dio.DigitalInOut(board.D5) # using board.SPI() automatically selects the MCU's diff --git a/examples/nrf24l01_context_test.py b/examples/nrf24l01_context_test.py index 456c58b..b056107 100644 --- a/examples/nrf24l01_context_test.py +++ b/examples/nrf24l01_context_test.py @@ -11,7 +11,7 @@ address = b'1Node' # change these (digital output) pins accordingly -ce = dio.DigitalInOut(board.D9) # AKA board.CE1 on the rasberry pi +ce = dio.DigitalInOut(board.D4) csn = dio.DigitalInOut(board.D5) # using board.SPI() automatically selects the MCU's diff --git a/examples/nrf24l01_interrupt_test.py b/examples/nrf24l01_interrupt_test.py index 670ffa6..d595895 100644 --- a/examples/nrf24l01_interrupt_test.py +++ b/examples/nrf24l01_interrupt_test.py @@ -14,7 +14,7 @@ irq = dio.DigitalInOut(board.D4) irq.switch_to_input() # make sure its an input object # change these (digital output) pins accordingly -ce = dio.DigitalInOut(board.D9) # AKA board.CE1 on the rasberry pi +ce = dio.DigitalInOut(board.D4) csn = dio.DigitalInOut(board.D5) # using board.SPI() automatically selects the MCU's diff --git a/examples/nrf24l01_simple_test.py b/examples/nrf24l01_simple_test.py index 26f9885..4d690b1 100644 --- a/examples/nrf24l01_simple_test.py +++ b/examples/nrf24l01_simple_test.py @@ -11,7 +11,7 @@ address = b'1Node' # change these (digital output) pins accordingly -ce = dio.DigitalInOut(board.D9) # AKA board.CE1 on the rasberry pi +ce = dio.DigitalInOut(board.D4) csn = dio.DigitalInOut(board.D5) # using board.SPI() automatically selects the MCU's diff --git a/examples/nrf24l01_stream_test.py b/examples/nrf24l01_stream_test.py index 19d9b4b..83c5157 100644 --- a/examples/nrf24l01_stream_test.py +++ b/examples/nrf24l01_stream_test.py @@ -10,7 +10,7 @@ address = b'1Node' # change these (digital output) pins accordingly -ce = dio.DigitalInOut(board.D9) # AKA board.CE1 on the rasberry pi +ce = dio.DigitalInOut(board.D4) csn = dio.DigitalInOut(board.D5) # using board.SPI() automatically selects the MCU's From e97f782c90db01552eb61e4abb02ff57862d8bc3 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Thu, 7 Nov 2019 17:53:49 -0800 Subject: [PATCH 11/17] compatible with TMRh20 lib!!! --- circuitpython_nrf24l01/registers.py | 3 +- circuitpython_nrf24l01/rf24.py | 84 ++++++++++++++++------------- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/circuitpython_nrf24l01/registers.py b/circuitpython_nrf24l01/registers.py index 75b1473..33ef67f 100644 --- a/circuitpython_nrf24l01/registers.py +++ b/circuitpython_nrf24l01/registers.py @@ -12,5 +12,6 @@ 'RX_PW' : 0x11,# RX payload widths on pipes ranging [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 toggles for dynamic payloads, auto-ACK, and custom ACK features + 'FEATURE' : 0x1d,# global toggles for dynamic payloads, auto-ACK, and custom ACK features + 'TX_ADDR' : 0x10 # Address that is used for TX transmissions } diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index 65649d1..b1264d0 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -166,8 +166,7 @@ def __init__(self, spi, csn, ce, # manage dynamic_payloads, auto_ack, and ack features self._dyn_pl = 0x3F if dynamic_payloads else 0 # 0x3F == enabled on all pipes - # 0x3F == enabled on all pipes - self._aa = 0x3F if auto_ack and dynamic_payloads else 0 + 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 @@ -289,8 +288,9 @@ def open_tx_pipe(self, address): if self.auto_ack: # settings need to match on both transceivers: dynamic_payloads and payload_length self._reg_write_bytes(REG['RX_ADDR'], address) # using pipe 0 - # let self._open_pipes only reflect RX pipes - self._reg_write_bytes(0x10, address) # 0x10 = TX_ADDR register + self._open_pipes = self._open_pipes | 1 # open pipe 0 for RX-ing ACK + self._reg_write(REG['EN_RX'], self._open_pipes) + self._reg_write_bytes(REG['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" @@ -398,7 +398,7 @@ def listen(self): @listen.setter def listen(self, is_rx): assert isinstance(is_rx, (bool, int)) - if is_rx: + if self.listen != is_rx: self._start_listening() else: self._stop_listening() @@ -585,7 +585,7 @@ def send(self, buf, ask_no_ack=False): (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) + 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 @@ -609,9 +609,12 @@ def send(self, buf, ask_no_ack=False): 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 - start = time.monotonic() # 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() @@ -620,21 +623,18 @@ def send(self, buf, ask_no_ack=False): # now wait till the nRF24L01 has determined the result or timeout (based on calcs # from spec sheet) - 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 + 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 = bool(self.irq_DS) + 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 reply in input buffer + 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 @@ -758,37 +758,48 @@ def what_happened(self, dump_pipes=False): :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`` Amount of packets lost (transmission failures) + - ``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? - - ``Data Sent`` Has the TX data been sent? + (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? - - ``TX FIFO full`` Is the TX FIFO buffer full? + (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? - - ``Ask no ACK`` Is the nRF24L01 set up to transmit individual packets that don't + 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 automatic acknowledgement feature enabled? - - ``Dynamic Payloads`` Is the dynamic payload length feature enabled? + - ``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: ``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. Default is `False` and skips this extra information. + :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( @@ -824,16 +835,19 @@ def what_happened(self, dump_pipes=False): ('Standby-II' if self.ce.value else 'Standby-I') if self._config & 2 else 'Off')) if dump_pipes: shared_bytes = b'' + print('TX address____________', self._reg_read_bytes(REG['TX_ADDR'])) for i in range(REG['RX_ADDR'], REG['RX_ADDR'] + 6): j = i - REG['RX_ADDR'] - is_open = "( open )" if ( - self._open_pipes & (1 << j)) else "(closed)" + payload_width = self._reg_read(REG['RX_PW'] + j) + is_open = "( open )" if self._open_pipes & (1 << j) else "(closed)" if j <= 1: # print full address shared_bytes = self._reg_read_bytes(i) print("Pipe", j, is_open, "bound:", shared_bytes) else: # print shared bytes + unique byte = actual address used by radio specific_address = bytearray([self._reg_read(i)]) + shared_bytes[1:] print("Pipe", j, is_open, "bound:", specific_address) + if self._open_pipes & (1 << j): + print('\t\texpecting', payload_width, 'byte static payloads') @property def dynamic_payloads(self): @@ -851,8 +865,6 @@ def dynamic_payloads(self): def dynamic_payloads(self, enable): assert isinstance(enable, (bool, int)) self._features = self._reg_read(REG['FEATURE']) # refresh data - if self.auto_ack and not enable: # disabling and auto_ack is still on - self.auto_ack = enable # disable auto_ack as this is required for it # save changes to registers(& their shadows) if self._features & 4 != enable: # if not already # throw a specific global flag for enabling dynamic payloads @@ -902,14 +914,11 @@ def auto_ack(self): `dynamic_payloads` (see also the `dynamic_payloads` attribute). The `crc` attribute will remain unaffected (remains enabled) when disabling the `auto_ack` attribute. """ - return self._aa and self.dynamic_payloads + return self._aa @auto_ack.setter def auto_ack(self, enable): assert isinstance(enable, (bool, int)) - # this feature requires dynamic payloads enabled; check for that now - if not self.dynamic_payloads and enable: # if dynamic_payloads is off and this is enabling - self.dynamic_payloads = enable # enable dynamic_payloads # the following 0x3F == enabled auto_ack on all pipes self._aa = 0x3F if enable else 0 self._reg_write(REG['EN_AA'], self._aa) # 1 == EN_AA register for ACK feature @@ -928,19 +937,22 @@ def ack(self): the `auto_ack` and `dynamic_payloads` attributes (they work just fine without this feature). """ - return bool((self._features & 2) and self.auto_ack) + 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 + 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(REG['DYNPD'], self._dyn_pl) else: # setting auto_ack feature automatically updated the _features attribute, so self._features = self._reg_read(REG['FEATURE']) # refresh data here - self._features = (self._features & 5) | (2 if enable else 0) + self._features = (self._features & 5) | (6 if enable else 0) self._reg_write(REG['FEATURE'], self._features) def load_ack(self, buf, pipe_number): @@ -1110,13 +1122,13 @@ def power(self): .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). """ - self._config = self._reg_read(REG['CONFIG']) # refresh data 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(REG['CONFIG']) # refresh data if self.power != is_on: # only write changes self._config = (self._config & 0x7d) | ( From 08905f1c1907ad43e71faab17d9993ff45f545f2 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Thu, 7 Nov 2019 20:51:34 -0800 Subject: [PATCH 12/17] arduino lib compatible example --- examples/nrf24l01_2arduino_handling_data.py | 109 ++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 examples/nrf24l01_2arduino_handling_data.py diff --git a/examples/nrf24l01_2arduino_handling_data.py b/examples/nrf24l01_2arduino_handling_data.py new file mode 100644 index 0000000..e7b64e8 --- /dev/null +++ b/examples/nrf24l01_2arduino_handling_data.py @@ -0,0 +1,109 @@ +""" +Example of library driving the nRF24L01 to communicate with a nRF24L01 driven by +the TMRh20 Arduino library. The Arduino program/sketch that this example was +designed for is named GettingStarted_HandlingData.ino and can be found in the "RF24" +examples after the TMRh20 library is installed from the Arduino Library Manager. +""" +import time +import struct +import board +import digitalio as dio +from circuitpython_nrf24l01 import RF24 + +# addresses needs to be in a buffer protocol object (bytearray) +address = [b'1Node', b'2Node'] + +# change these (digital output) pins accordingly +ce = dio.DigitalInOut(board.D4) +csn = dio.DigitalInOut(board.D5) + +# using board.SPI() automatically selects the MCU's +# available SPI pins, board.SCK, board.MOSI, board.MISO +spi = board.SPI() # init spi bus object + +# initialize the nRF24L01 on the spi bus object +nrf = RF24(spi, csn, ce, ask_no_ack=False) +nrf.dynamic_payloads = False # this is the default in the TMRh20 arduino library + +# set address of TX node into a RX pipe +nrf.open_rx_pipe(1, address[1]) +# set address of RX node into a TX pipe +nrf.open_tx_pipe(address[0]) + +def master(count=5): # count = 5 will only transmit 5 packets + """Transmits an arbitrary unsigned long value every second. This method + will only try to transmit (count) number of attempts""" + + # for the "HandlingData" part of the test from the TMRh20 library example + float_value = 0.01 + while count: + nrf.listen = False # ensures the nRF24L01 is in TX mode + print("Now Sending") + start_timer = int(time.monotonic() * 1000) # start timer + # use struct.pack to packetize your data into a usable payload + # '<' means little endian byte order. + # 'L' means a single 4 byte unsigned long value. + # 'f' means a single 4 byte float value. + buffer = struct.pack(' Date: Thu, 7 Nov 2019 21:00:33 -0800 Subject: [PATCH 13/17] set payload length on open tx pipe --- circuitpython_nrf24l01/rf24.py | 1 + 1 file changed, 1 insertion(+) diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index b1264d0..c09a1ae 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -290,6 +290,7 @@ def open_tx_pipe(self, address): self._reg_write_bytes(REG['RX_ADDR'], address) # using pipe 0 self._open_pipes = self._open_pipes | 1 # open pipe 0 for RX-ing ACK self._reg_write(REG['EN_RX'], self._open_pipes) + self._reg_write(REG['RX_PW'], self.payload_length) # set expected payload_length self._reg_write_bytes(REG['TX_ADDR'], address) else: raise ValueError("address must be a buffer protocol object with a byte length\nequal " From 66aa435caa0347becb19eebeb78c3857a40a479d Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Thu, 7 Nov 2019 21:34:47 -0800 Subject: [PATCH 14/17] removed fakeBLE stuff --- circuitpython_nrf24l01/__init__.py | 3 +- circuitpython_nrf24l01/fake_ble.py | 301 ----------------------------- docs/api.rst | 8 - examples/nrf24l01_fake_ble_test.py | 35 ---- 4 files changed, 1 insertion(+), 346 deletions(-) delete mode 100644 circuitpython_nrf24l01/fake_ble.py delete mode 100644 examples/nrf24l01_fake_ble_test.py diff --git a/circuitpython_nrf24l01/__init__.py b/circuitpython_nrf24l01/__init__.py index a1d8ff9..fe0189b 100644 --- a/circuitpython_nrf24l01/__init__.py +++ b/circuitpython_nrf24l01/__init__.py @@ -1,6 +1,5 @@ """module managment for the circuitpython-nrf24l01 package""" from .rf24 import RF24 -from .fake_ble import FakeBLE -__all__ = ['RF24', 'FakeBLE'] +__all__ = ['RF24'] diff --git a/circuitpython_nrf24l01/fake_ble.py b/circuitpython_nrf24l01/fake_ble.py deleted file mode 100644 index 282cf8d..0000000 --- a/circuitpython_nrf24l01/fake_ble.py +++ /dev/null @@ -1,301 +0,0 @@ -# The MIT License (MIT) -# -# 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. -""" -This module uses the RF24 module to make the nRF24L01 imitate a Bluetooth-Low-Emissions (BLE) -beacon. A BLE beacon can send (referred to as advertise) data to any BLE compatible device -(ie smart devices with Bluetooth 4.0 or later) that is listening. - -Original research was done by `Dmitry Grinberg and his write-up (including C source code) can be -found here `_ As -this technique can prove invaluable in certain project designs, the code here is simply ported to -work on CircuitPython. - -.. important:: Because the nRF24L01 wasn't designed for BLE advertising, it has some limitations - that helps to be aware of. - - 1. the maximum payload length is shortened to 19 bytes - 2. the channels that BLE use are limited to the following three: 2.402 GHz, 2.426 GHz, - and 2.480 GHz - 3. CRC is disabled in the nRF24L01 firmware as BLE requires 3 bytes and nRF24L01 only handles - 2. Thus we have augmented the required 3 bytes of CRC into the payload. - 4. address length of BLE packet only uses 4 bytes, so we have set that acccordingly. - 5. the automatic acknowledgment feature of the nRF24L01 is useless when tranmitting to BLE - devices, thus it is disabled as well as automatic re-transmit and custom ACK payloads - (both depend on the automatic acknowledgments feature) - 6. the dynamic payloads feature of the nRF24L01 isn't compatible with BLE specifications. - Thus we have disabled it in the nRF24L01 firmware and incorporated dynamic payloads - properly into he payload data - 7. BLE specifications only allow using 1 Mbps RF data rate, so that too has been hard coded. - 8. both the "on data sent" & "on data ready" events control the interrupt (IRQ) pin; the - other event, "on data fail", is ignored because it will never get thrown with "auto_ack" - off. However the interrupt settings can be modified AFTER instantiation -""" -import time -from .rf24 import RF24 - -def _swap_bits(orig): - """reverses the bit order into LSbit to MSBit""" - reverse = 0 - for _ in range(8): - reverse <<= 1 - reverse |= orig & 1 - orig >>= 1 - return reverse # we're done here - -def _reverse_bits(orig): - """reverses the bit order into LSbit to MSBit without touching the byte order""" - r = b'' - for byte in orig: - r += bytes([_swap_bits(byte)]) - return r - -def _make_crc(data): - """use this to create the 3 byte-long CRC data. returns a bytearray""" - # taken from source code - # https://github.com/adafruit/Adafruit_CircuitPython_SGP30/blob/d209c7c76f941dc60b24d85fdef177b5fb2e9943/adafruit_sgp30.py#L177 - # zlib or binascii modules would be ideal alternatives on the raspberry pi, but - # MicroPython & CircuitPython doesn't have the crc32() included in the uzlib or ubinascii - # modules. - crc = 0xFF - for byte in data: - crc ^= byte - for _ in range(8): - if crc & 0x80: - crc = (crc << 1) ^ 0x31 - else: - crc <<= 1 - return crc & 0xFF - -def _ble_whitening(data, whiten_coef): - """for "whiten"ing the BLE packet data according to expected parameters""" - # uint8_t m; - # while(len--){ - # for(m = 1; m; m <<= 1){ - # if(whitenCoeff & 0x80){ - # whitenCoeff ^= 0x11; - # (*data) ^= m; - # } - # whitenCoeff <<= 1; - # } - # data++; - result = b'' - for byte in data: # for every byte - for i in range(8): - if whiten_coef & 0x80: - whiten_coef ^= 0x11 - byte ^= 1 << i - whiten_coef <<= 1 - result += bytes([byte]) - return result - -class FakeBLE(RF24): - """Per the limitations of this technique, only power amplifier level is available for - configuration when advertising BLE data. - - :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 bytearray name: This will be the nRF24L01-emulated BLE device's broadcasted name. - This is option and defaults to `None` to allow for larger paylaods because the name's - byte length borrows from the same buffer space that the payload data occupies. See - `name` attribute for more details. - :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. - """ - def __init__(self, spi, csn, ce, name=None, pa_level=False, irq_DR=False, irq_DS=True): - super(FakeBLE, self).__init__(spi, csn, ce, - pa_level=pa_level, - crc=0, - dynamic_payloads=False, - arc=0, - address_length=4, - ask_no_ack=False, - irq_DF=False, - irq_DR=irq_DR, - irq_DS=irq_DS) - self._chan = 0 - self._ble_name = None - self.name = name - - @property - def name(self): - """Represents the emulated BLE device name during braodcasts. must be a buffer protocol - object (`bytearray`) , and can be any length (less than 14) of UTF-8 freindly characters. - - .. note:: the BLE device's name will occupy the same space as your TX data. While space - is limited to 32 bytes on the nRF24L01, actual usable BLE TX data = 16 - (name length - + 2). The other 16 bytes available on the nRF24L01 TX FIFO buffer are reserved for - the [arbitrary] MAC address and other BLE related stuff. - """ - return self._ble_name[2:] if self._ble_name is not None else None - - @name.setter - def name(self, n): - """The broadcasted BLE name of the nRF24L01. This is not required. In fact, setting this - attribute will subtract from the available payload length (in bytes). - - * payload_length has maximum of 19 bytes when NOT broadcasting a name for itself. - * payload_length has a maximum of (17 - length of name) bytes when broadcasting a - name for itself. - """ - if n is not None and 1 <= len(n) <= 12: # max defined by 1 byte payload data requisite - self._ble_name = bytes([len(n) + 1]) + b'\x08' + n - else: - self._ble_name = None # name will not be advertised - - def _chan_hop(self): - """BLE protocol specs mandate the BLE device cyle through the following 3 channels: - - - nRF channel 2 == BLE channel 37 - - nRF channel 26 == BLE channel 38 - - nRF channel 80 == BLE channel 39 - - .. note:: then BLE channel number is different from the nRF channel number. - """ - self._chan = (self._chan + 1) if (self._chan + 1) < 3 else 0 - self.channel = 26 if self._chan == 1 else (80 if self._chan == 2 else 2) - - # pylint: disable=arguments-differ - def send(self, buf): - """This blocking function is used to transmit payload. - - :returns: Nothing as every transmission will register as a success under these required - settings. - - :param bytearray buf: The payload to transmit. This bytearray must have a length greater - than 0 and less than 20, 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. - - .. note:: If the name of the emulated BLE device is also to be braodcast, then the 'name' - attribute should be set prior to calling `send()`. - """ - self.ce.value = 0 - self.flush_tx() - self.clear_status_flags(False) # clears TX related flags only - # max payload_length = 32 - 14(header, MAC, & CRC) - 2(container header) - 3(BLE flags) - # = 13 - (BLE name length + 2 if any) - name_len = len(self._ble_name) if self._ble_name is not None else 0 - if not buf or len(buf) > (13 - name_len): - raise ValueError("buf must be a buffer protocol object with a byte length of" - " at least 1 and no greater than 13 - " - "{} = {}".format(name_len, 13 - name_len)) - - # BLE payload = header(1) + payload length(1) + MAC address(6) + containers + CRC(3) bytes - # header == PDU type, given MAC address is random/arbitrary - # type == 0x42 for Android or 0x40 for iPhone - # containers (in bytes) = length(1) + type(1) + data - # the 1 byte about container's length excludes only itself - - payload = b'\x42' # init a temp payload buffer with header type byte - - # to avoid padding when dynamic_payloads is disabled, set payload_length attribute - self.payload_length = len(buf) + 16 + name_len - # payload length excludes the header, itself, and crc lengths - payload += bytes([self.payload_length - 5]) - payload += b'\x11\x22\x33\x44\x55\x66' # a bogus MAC address - # payload will have at least 2 containers: - # 3 bytes of flags (required for BLE discoverable), & at least (1+2) byte of data - payload += b'\x02\x01\x06' # BLE flags for discoverability and non-pairable etc - # payload will also have to fit the optional BLE device name as - # a seperate container ([name length + 2] bytes) - if self._ble_name is not None: - payload += self._ble_name - payload += (bytes([len(buf) + 1]) + b'\xFF' + buf) # append the data container - # crc is generated from b'\x55\x55\x55' about everything except itself - payload += _make_crc(payload) - self._chan_hop() # cycle to next BLE channel per specs - # the whiten_coef value we need is the BLE channel (37,38, or 39) left shifted one - whiten_coef = 37 + self._chan - whiten_coef = _swap_bits(whiten_coef) | 2 - - print('transmitting {} as {}'.format(payload, - _reverse_bits(_ble_whitening(payload, whiten_coef)))) - # init using non-blocking helper - self.write(_reverse_bits(_ble_whitening(payload, whiten_coef))) - time.sleep(0.00001) # ensure CE pulse is >= 10 µs - # pulse is stopped here; the nRF24L01 only handles the top level payload in the FIFO. - self.ce.value = 0 # go to Standby-I power mode (power attribute still == True) - - # T_upload is done before timeout begins (after payload write action AKA upload) - timeout = (((8 * (5 + len(payload))) + 9) / 125000) + 0.0002682 - start = time.monotonic() - while not self.irq_DS 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)) - self.clear_status_flags(False) # only TX related IRQ flags - - # Altering all the following settings is disabled - def open_tx_pipe(self): - super(FakeBLE, self).open_tx_pipe(_reverse_bits(b'\x8E\x89\xBE\xD6')) - # b'\x8E\x89\xBE\xD6' = proper address for BLE advertisments - # pylint: enable=arguments-differ - - # ignore pylint errors when overriding setter functions of inherited attributes - # pylint: disable=no-member - @RF24.address_length.setter - def address_length(self, t): - super(FakeBLE, self).address_length = (4 + t * 0) - - @RF24.listen.setter - def listen(self, rx): - if self.listen or rx: - self._stop_listening() - - @RF24.data_rate.setter - def data_rate(self, t): - super(FakeBLE, self).data_rate = (1 + t * 0) - - @RF24.dynamic_payloads.setter - def dynamic_payloads(self, t): - super(FakeBLE, self).dynamic_payloads = (False & t) - - @RF24.auto_ack.setter - def auto_ack(self, t): - super(FakeBLE, self).auto_ack = (False & t) - - @RF24.ack.setter - def ack(self, t): - super(FakeBLE, self).ack = (False & t) - - @RF24.crc.setter - def crc(self, t): - super(FakeBLE, self).crc = (0 * t) - - @RF24.arc.setter - def arc(self, t): - super(FakeBLE, self).arc = (t * 0) diff --git a/docs/api.rst b/docs/api.rst index e756c55..39cf721 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -106,11 +106,3 @@ Advanced API .. automethod:: flush_tx .. automethod:: fifo .. automethod:: pipe - -Fake BLE API -============ - -.. automodule:: circuitpython_nrf24l01.fake_ble - :members: - :show-inheritance: - :exclude-members: listen, open_tx_pipe, address_length, data_rate, dynamic_payloads, auto_ack, ack, crc, arc diff --git a/examples/nrf24l01_fake_ble_test.py b/examples/nrf24l01_fake_ble_test.py deleted file mode 100644 index 8166902..0000000 --- a/examples/nrf24l01_fake_ble_test.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -This example of using the nRF24L01 as a 'fake' Buetooth Beacon -""" -import time -import struct -import board -import digitalio as dio -from circuitpython_nrf24l01.fake_ble import FakeBLE - -# change these (digital output) pins accordingly -ce = dio.DigitalInOut(board.D7) -csn = dio.DigitalInOut(board.D5) - -# using board.SPI() automatically selects the MCU's -# available SPI pins, board.SCK, board.MOSI, board.MISO -spi = board.SPI() # init spi bus object - -# initialize the nRF24L01 on the spi bus object as a BLE radio using -nrf = FakeBLE(spi, csn, ce, name=b'nRF24') -# the name parameter is going to be its braodcasted BLE name -# this can be changed at any time using the attribute -nrf.name = b'RFtest' - -def master(count=15): - """Sends out an advertisement once a second (default 15 secs)""" - with nrf as ble: - ble.open_tx_pipe() # endure the tx pip is properly addressed - for i in range(count): # advertise data this many times - if (count - i) % 5 == 0 or (count - i) < 5: - print( - count - i, 'advertisment{}left to go!'.format('s ' if count - i - 1 else ' ')) - # pack into bytearray using struct.pack() - ble.send(struct.pack('i', count)) # 'i' = 4 byte integer - # channel is automatically managed by send() per BLE specs - time.sleep(1) # wait till next broadcast From 62522b4641a212b490c68ac53a07066b0f7adb5a Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Thu, 7 Nov 2019 22:05:11 -0800 Subject: [PATCH 15/17] adjusted docs cuz dynPL is not req'd for autoACK --- circuitpython_nrf24l01/rf24.py | 15 ++++++--------- docs/api.rst | 6 +++--- docs/examples.rst | 9 +++++++++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index c09a1ae..9369ace 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -854,11 +854,10 @@ def what_happened(self, dump_pipes=False): def dynamic_payloads(self): """This `bool` attribute controls the nRF24L01's dynamic payload length feature. - - `True` enables nRF24L01's dynamic payload length feature. Enabling the `auto_ack` - attribute also enables `dynamic_payloads` as it is required. The `payload_length` + - `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. Disabling the - `dynamic_payloads` also disables `auto_ack` (see also the `auto_ack` attribute). + - `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)) @@ -907,12 +906,10 @@ def payload_length(self, length): def auto_ack(self): """This `bool` attribute controls the nRF24L01's automatic acknowledgment feature. - - `True` enables automatic acknowledgment packets. Enabling the `auto_ack` attribute also - enables `dynamic_payloads` as it is required. Also the CRC (cyclic redundancy checking) + - `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 - `dynamic_payloads` and `crc` attributes). - - `False` disables automatic acknowledgment packets. Disabling the `auto_ack` also disables - `dynamic_payloads` (see also the `dynamic_payloads` attribute). The `crc` attribute will + `crc` attribute). + - `False` disables automatic acknowledgment packets. The `crc` attribute will remain unaffected (remains enabled) when disabling the `auto_ack` attribute. """ return self._aa diff --git a/docs/api.rst b/docs/api.rst index 39cf721..ce42a72 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,7 +9,7 @@ RF24 class ============== -.. important:: The nRF24L01 has 3 key features that are very interdependent of each other. Their +.. important:: The nRF24L01 has 3 key features that can be interdependent of each other. Their priority of dependence is as follows: 1. `dynamic_payloads` feature allowing either TX/RX nRF24L01 to be able to send/receive @@ -17,12 +17,12 @@ RF24 class nRF24L01 must use matching `payload_length` attributes. 2. `auto_ack` feature provides transmission verification by using the RX nRF24L01 to automatically and imediatedly send an acknowledgment (ACK) packet in response to freshly - received payloads. `auto_ack` requires `dynamic_payloads` to be enabled. + received payloads. `auto_ack` does not require `dynamic_payloads` to be enabled. 3. `ack` feature allows the MCU to append a payload to the ACK packet, thus instant bi-directional communication. A transmitting ACK payload must be loaded into the nRF24L01's TX FIFO buffer (done using `load_ack()`) BEFORE receiving the payload that is to be acknowledged. Once transmitted, the payload is released from the TX FIFO buffer. This - feature obviously requires the `auto_ack` feature enabled. + feature requires the `auto_ack` and `dynamic_payloads` features enabled. Remeber that the nRF24L01's FIFO (first-in,first-out) buffer has 3 levels. This means that there can be up to 3 payloads waiting to be read (RX) and up to 3 payloads waiting to be transmit (TX). diff --git a/docs/examples.rst b/docs/examples.rst index 7192373..b044e2b 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -43,3 +43,12 @@ This is a test to show how to use "with" statements to manage multiple different .. literalinclude:: ../examples/nrf24l01_context_test.py :caption: examples/nrf24l01_context_test.py :linenos: + +Working with TMRh20's Arduino library +------------------------------------- + +This test is meant to prove compatibility with the popular Arduino library for the nRF24L01 by TMRh20 (available for install via the Arduino IDE's Library Manager). The following code has been designed/test with the TMRh20 library example named "GettingStarted_HandlingData.ino". + +.. literalinclude:: ../examples/nrf24l01_2arduino_handling_data.py + :caption: examples/nrf24l01_2arduino_handling_data.py + :linenos: From 8b09c0b294b8890b9d9e2cfcd523c92af402a35e Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sat, 9 Nov 2019 01:02:04 -0800 Subject: [PATCH 16/17] added arduino compat to supported list of features --- README.rst | 4 +- circuitpython_nrf24l01/registers.py | 17 --- circuitpython_nrf24l01/rf24.py | 210 +++++++++++++++++----------- 3 files changed, 133 insertions(+), 98 deletions(-) delete mode 100644 circuitpython_nrf24l01/registers.py diff --git a/README.rst b/README.rst index fc94404..35c1653 100644 --- a/README.rst +++ b/README.rst @@ -47,12 +47,12 @@ Features currently supported * adjust the nRF24L01's frequency channel (2.4-2.525 GHz) * adjust the nRF24L01's power amplifier level (0, -6, -12, or -18 dBm) * adjust the nRF24L01's RF data rate (250Kbps is buggy due to hardware design, but 1Mbps and 2Mbps are reliable) +* a nRF24L01 driven by this library can communicate with a nRF24L01 on an Arduino driven by the `TMRh20 RF24 library `_. See the nrf24l01_2arduino_handling_data.py code in the `examples folder of this library's repository `_ Features currently unsupported ------------------------------- -* as of yet, no [intended] implementation for Multiceiver mode (up to 6 TX nRF24L01 "talking" to 1 RX nRF24L01 simultaneously). Although this might be acheived easily using the "automatic retry delay" (:py:attr:`~circuitpython_nrf24l01.RF24.ard`) and "automatic retry count" (:py:attr:`~circuitpython_nrf24l01.RF24.arc`) attributes set accordingly (varyingly high). -* for reason(s) unknown, a nRF24L01 driven by this library will not "talk" to a nRF24L01 on an Arduino driven by the `TMRh20 RF24 library `_. There is no problems when a nRF24L01 driven by this library "talks" to another nRF24L01 that's also driven by this library. `Other Arduino-based nRF24L01 libraries are available `_, but they have not been tested to communicate with this CircuitPython-nRF24L01 library. +* as of yet, no [intended] implementation for Multiceiver mode (up to 6 TX nRF24L01 "talking" to 1 RX nRF24L01 simultaneously). Although this might be acheived easily using the "automatic retry delay" (:py:attr:`~circuitpython_nrf24l01.RF24.ard`) and "automatic retry count" (:py:attr:`~circuitpython_nrf24l01.RF24.arc`) attributes set accordingly (varyingly high -- this has not been tested). Dependencies ============= diff --git a/circuitpython_nrf24l01/registers.py b/circuitpython_nrf24l01/registers.py deleted file mode 100644 index 33ef67f..0000000 --- a/circuitpython_nrf24l01/registers.py +++ /dev/null @@ -1,17 +0,0 @@ -"""nRF24L01(+) registers""" - -REGISTERS = { - '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 rangeing [0,5]:[0xA:0xF] - 'RX_PW' : 0x11,# RX payload widths on pipes ranging [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 toggles for dynamic payloads, auto-ACK, and custom ACK features - 'TX_ADDR' : 0x10 # Address that is used for TX transmissions -} diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index 9369ace..b97ed44 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -28,7 +28,23 @@ __repo__ = "https://github.com/2bndy5/CircuitPython_nRF24L01.git" import time from adafruit_bus_device.spi_device import SPIDevice -from .registers import REGISTERS as REG + +# 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 class RF24: """A driver class for the nRF24L01(+) transceiver radios. This class aims to be compatible with @@ -111,14 +127,29 @@ def __init__(self, spi, csn, ce, irq_DR=True, irq_DS=True, irq_DF=True): - # init the SPI bus and pins - self.spi = SPIDevice(spi, chip_select=csn, baudrate=1250000) self._payload_length = payload_length # inits internal attribute self.payload_length = payload_length # last address assigned to pipe0 for reading. init to None - self.pipe0_read_addr = None self._fifo = 0 self._status = 0 + # init shadow copy of RX addresses for all pipes + self._pipes = [None, None, None, None, None, None] + 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' * 5 + else: + self._pipes[i] = b'\xc2' * 5 + 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 @@ -134,15 +165,16 @@ def __init__(self, spi, csn, ce, 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(REG['CONFIG'], self._config) # dump to register + 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(REG['CONFIG']) & 3 == 2: # if 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(REG['CONFIG']))) + else: # hardware presence check NOT passed + print(bin(self._reg_read(CONFIG))) raise RuntimeError("nRF24L01 Hardware not responding") # configure the SETUP_RETR register @@ -150,7 +182,7 @@ def __init__(self, spi, csn, ce, 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 in range " + "[250,4000]\nautomatic re-transmit count(/attempts) must range " "[0,15]") # configure the RF_SETUP register @@ -174,38 +206,53 @@ def __init__(self, spi, csn, ce, self._channel = channel self._addr_len = address_length - # init the _open_pipes attribute (reflects only RX state on each pipe) - self._open_pipes = 0 # <- means all pipes closed - 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(REG['CONFIG'], self._config | 1) # enable RX mode + 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(REG['CONFIG'], self._config & 0xC) # power down + TX 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): - self._reg_write(REG['CONFIG'], self._config & - 0x7C) # dump IRQ and CRC data to CONFIG register - self._reg_write(REG['RF_SETUP'], self._rf_setup) # dump to RF_SETUP register - # update open pipe info from current state of - self._open_pipes = self._reg_read(REG['EN_RX']) - # EN_RXADDR register - self._reg_write(REG['DYNPD'], self._dyn_pl) # dump to DYNPD register - self._reg_write(REG['EN_AA'], self._aa) # dump to EN_AA register - self._reg_write(REG['FEATURE'], self._features) # dump to FEATURE register + # 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(REG['SETUP_RETR'], self._setup_retr) + 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): + # capture pipe addresses + for i in range(6): + if i < 2: + self._pipes[i] = self._reg_read_bytes(RX_ADDR + i) + else: + self._pipes[i] = self._reg_read(RX_ADDR + i) + self._payload_widths[i] = self._reg_read(RX_PW + i) + # capture tx_address + self._tx_address = self._reg_read_bytes(TX_ADDR) + self._open_pipes = self._reg_read(EN_RX) return False # pylint: disable=no-member @@ -255,7 +302,7 @@ def address_length(self): 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(REG['SETUP_AW']) + 2 + return self._reg_read(SETUP_AW) + 2 @address_length.setter def address_length(self, length): @@ -264,7 +311,7 @@ def address_length(self, length): if 3 <= length <= 5: # address width is saved in 2 bits making range = [3,5] self._addr_len = int(length) - self._reg_write(REG['SETUP_AW'], length - 2) + self._reg_write(SETUP_AW, length - 2) else: raise ValueError( "address length can only be set in range [3,5] bytes") @@ -287,11 +334,14 @@ def open_tx_pipe(self, address): # 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._reg_write_bytes(REG['RX_ADDR'], address) # using pipe 0 + 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(REG['EN_RX'], self._open_pipes) - self._reg_write(REG['RX_PW'], self.payload_length) # set expected payload_length - self._reg_write_bytes(REG['TX_ADDR'], address) + 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._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" @@ -309,19 +359,19 @@ def close_rx_pipe(self, pipe_number, reset=True): """ if pipe_number < 0 or pipe_number > 5: raise ValueError("pipe number must be in range [0,5]") - self._open_pipes = self._reg_read(REG['EN_RX']) # refresh data + 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 + REG['RX_ADDR'], b'\xe7' * 5) + self._reg_write_bytes(pipe_number + RX_ADDR, b'\xe7' * 5) elif pipe_number < 2: # write the full address for pipe 1 - self._reg_write_bytes(pipe_number + REG['RX_ADDR'], b'\xc2' * 5) + self._reg_write_bytes(pipe_number + RX_ADDR, b'\xc2' * 5) else: # write just LSB for 2 <= pipes >= 5 - self._reg_write(pipe_number + REG['RX_ADDR'], pipe_number + 0xc1) + self._reg_write(pipe_number + RX_ADDR, 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(REG['EN_RX'], self._open_pipes) + self._reg_write(EN_RX, self._open_pipes) def open_rx_pipe(self, pipe_number, address): """This function is used to open a specific data pipe for OTA (over the air) RX @@ -354,21 +404,23 @@ def open_rx_pipe(self, pipe_number, address): # 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._reg_write_bytes(REG['RX_ADDR'] + pipe_number, address) + 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._reg_write(REG['RX_ADDR'] + pipe_number, address[0]) + 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(REG['EN_RX']) # refresh data + 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(REG['EN_RX'], self._open_pipes) + 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(REG['RX_PW'] + pipe_number, self.payload_length) + self._reg_write(RX_PW + pipe_number, self.payload_length) @property def listen(self): @@ -409,20 +461,20 @@ def _start_listening(self): if self.ce.value: self.ce.value = 0 - if self.pipe0_read_addr is not None: + 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(REG['RX_ADDR'], self.pipe0_read_addr) + self._reg_write_bytes(RX_ADDR, self._pipe0_read_addr) # power up radio & set radio in RX mode self._config = self._config & 0xFC | 3 - self._reg_write(REG['CONFIG'], self._config) + 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 + 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 + 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): @@ -431,7 +483,7 @@ def _stop_listening(self): 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(REG['CONFIG'], self._config) + 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) @@ -745,11 +797,11 @@ def interrupt_config(self, data_recv=True, data_sent=True, data_fail=True): `fifo()` will get this result) 4. if there is more data in RX FIFO, repeat from step 1 """ - self._config = self._reg_read(REG['CONFIG']) # refresh data + 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(REG['CONFIG'], self._config) + self._reg_write(CONFIG, self._config) def what_happened(self, dump_pipes=False): """This debuggung function aggregates and outputs all status/condition related information @@ -836,10 +888,10 @@ def what_happened(self, dump_pipes=False): ('Standby-II' if self.ce.value else 'Standby-I') if self._config & 2 else 'Off')) if dump_pipes: shared_bytes = b'' - print('TX address____________', self._reg_read_bytes(REG['TX_ADDR'])) - for i in range(REG['RX_ADDR'], REG['RX_ADDR'] + 6): - j = i - REG['RX_ADDR'] - payload_width = self._reg_read(REG['RX_PW'] + j) + print('TX address____________', self._reg_read_bytes(TX_ADDR)) + for i in range(RX_ADDR, RX_ADDR + 6): + j = i - RX_ADDR + payload_width = self._reg_read(RX_PW + j) is_open = "( open )" if self._open_pipes & (1 << j) else "(closed)" if j <= 1: # print full address shared_bytes = self._reg_read_bytes(i) @@ -864,15 +916,15 @@ def dynamic_payloads(self): @dynamic_payloads.setter def dynamic_payloads(self, enable): assert isinstance(enable, (bool, int)) - self._features = self._reg_read(REG['FEATURE']) # refresh data + 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(REG['FEATURE'], self._features) + self._reg_write(FEATURE, self._features) # 0x3F == all pipes have enabled dynamic payloads self._dyn_pl = 0x3F if enable else 0 - self._reg_write(REG['DYNPD'], self._dyn_pl) + self._reg_write(DYNPD, self._dyn_pl) @property def payload_length(self): @@ -919,7 +971,7 @@ 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(REG['EN_AA'], self._aa) # 1 == EN_AA register for ACK feature + 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 @@ -946,12 +998,12 @@ def ack(self, enable): self.auto_ack = True # ensure auto_ack feature is enabled # dynamic_payloads required for custom ACK payloads self._dyn_pl = 0x3F - self._reg_write(REG['DYNPD'], self._dyn_pl) + self._reg_write(DYNPD, self._dyn_pl) else: # setting auto_ack feature automatically updated the _features attribute, so - self._features = self._reg_read(REG['FEATURE']) # refresh data here + self._features = self._reg_read(FEATURE) # refresh data here self._features = (self._features & 5) | (6 if enable else 0) - self._reg_write(REG['FEATURE'], self._features) + 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 @@ -1031,7 +1083,7 @@ def data_rate(self): 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(REG['RF_SETUP']) # refresh data + 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 @@ -1043,7 +1095,7 @@ def data_rate(self, 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(REG['RF_SETUP'], self._rf_setup) + 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") @@ -1055,13 +1107,13 @@ def channel(self): 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(REG['RF_CH']) + return self._reg_read(RF_CH) @channel.setter def channel(self, channel): if 0 <= channel <= 125: self._channel = channel - self._reg_write(REG['RF_CH'], channel) # always writes to reg + self._reg_write(RF_CH, channel) # always writes to reg else: raise ValueError("channel acn only be set in range [0,125]") @@ -1081,7 +1133,7 @@ def crc(self): .. note:: The nRF24L01 automatically enables CRC if automatic acknowledgment feature is enabled (see `auto_ack` attribute). """ - self._config = self._reg_read(REG['CONFIG']) # refresh data + self._config = self._reg_read(CONFIG) # refresh data return max(0, ((self._config & 12) >> 2) - 1) # this works @crc.setter @@ -1126,12 +1178,12 @@ def power(self): 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(REG['CONFIG']) # refresh data + 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(REG['CONFIG'], self._config) + self._reg_write(CONFIG, self._config) # power up/down takes < 150 µs + 4 µs time.sleep(0.00016) @@ -1144,7 +1196,7 @@ def arc(self): 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(REG['SETUP_RETR']) # refresh data + self._setup_retr = self._reg_read(SETUP_RETR) # refresh data return self._setup_retr & 0x0f @arc.setter @@ -1153,7 +1205,7 @@ def arc(self, count): 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(REG['SETUP_RETR'], self._setup_retr) + self._reg_write(SETUP_RETR, self._setup_retr) else: raise ValueError( "automatic re-transmit count(/attempts) must in range [0,15]") @@ -1177,7 +1229,7 @@ def ard(self): See `data_rate` attribute on how to set the data rate of the nRF24L01's transmissions. """ - self._setup_retr = self._reg_read(REG['SETUP_RETR']) # refresh data + self._setup_retr = self._reg_read(SETUP_RETR) # refresh data return ((self._setup_retr & 0xf0) >> 4) * 250 + 250 @ard.setter @@ -1188,7 +1240,7 @@ def ard(self, delta_t): # save changes to register(& its Shadow) self._setup_retr = (int((delta_t - 250) / 250) << 4) | (self._setup_retr & 0x0F) - self._reg_write(REG['SETUP_RETR'], self._setup_retr) + 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]") @@ -1208,8 +1260,8 @@ def pa_level(self): Any invalid input throws a `ValueError` exception. Default is 0 dBm. """ - self._rf_setup = self._reg_read(REG['RF_SETUP']) # refresh data - return (3 - ((self._rf_setup & REG['RF_SETUP']) >> 1)) * -6 + 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): @@ -1219,7 +1271,7 @@ def pa_level(self, power): 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(REG['RF_SETUP'], self._rf_setup) + self._reg_write(RF_SETUP, self._rf_setup) else: raise ValueError( "power amplitude must be one of the following (dBm): -18, -12, -6, 0") @@ -1267,7 +1319,7 @@ def resend(self): 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(REG['FEATURE'], self._features) + 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 @@ -1370,7 +1422,7 @@ def write(self, buf=None, ask_no_ack=False): if not self.power or (self._config & 1): # ready radio if it isn't yet # also ensures tx mode - self._config = (self._reg_read(REG['CONFIG']) & 0x7c) | 2 + 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) @@ -1447,7 +1499,7 @@ def fifo(self, tx=False, empty=None): """ if (empty is None and isinstance(tx, (bool, int))) or \ (isinstance(empty, (bool, int)) and isinstance(tx, (bool, int))): - self._fifo = self._reg_read(REG['FIFO']) # refresh the data + 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))) From 254bd57ad6a3e8890fc82b1e5eae680acf93a6c3 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sat, 9 Nov 2019 01:15:15 -0800 Subject: [PATCH 17/17] pylint will never be happy w/ complex python libs --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 733e470..14f476d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ install: - pip install --force-reinstall pylint==1.9.2 script: - - pylint --disable=invalid-name,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,inconsistent-return-statements circuitpython_nrf24l01/*.py + - pylint --disable=invalid-name,too-many-instance-attributes,too-many-public-methods,too-many-arguments,too-many-function-args,too-many-locals,too-many-branches,too-many-nested-blocks,inconsistent-return-statements,too-many-statements circuitpython_nrf24l01/*.py - ([[ ! -d "examples" ]] || pylint --disable=invalid-name examples/*.py) - circuitpython-build-bundles --library_location . --filename_prefix circuitpython-nrf24l01 - cd docs && sphinx-build -E -W -b html . _build/html && cd ..