diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4cdb70..34241a2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,17 +37,18 @@ jobs: - name: Install deps run: | source actions-ci/install.sh + - name: Pip install pylint, black, & Sphinx run: | - pip install --force-reinstall pylint==1.9.2 black==19.10b0 Sphinx sphinx-rtd-theme + pip install pylint black Sphinx sphinx-rtd-theme - name: Library version run: git describe --dirty --always --tags - name: PyLint run: | - pylint --disable=too-many-statements,too-many-arguments,too-many-instance-attributes,too-many-public-methods,too-many-locals circuitpython_nrf24l01/*.py + pylint --disable=too-many-instance-attributes,too-many-public-methods,too-many-locals circuitpython_nrf24l01/*.py ([[ ! -d "examples" ]] || pylint --disable=invalid-name $( find . -path "./examples/*.py" )) - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . --package_folder_prefix circuitpython_nrf24l01 + run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . --package_folder_prefix ${{ steps.repo-name.outputs.repo-name }} - name: Build docs working-directory: docs run: sphinx-build -E -W -b html . _build/html diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e4755cd..e4dd574 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,7 +40,7 @@ jobs: run: | source actions-ci/install.sh - name: Build assets - run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . --package_folder_prefix circuitpython_nrf24l01 + run: circuitpython-build-bundles --filename_prefix ${{ steps.repo-name.outputs.repo-name }} --library_location . --package_folder_prefix ${{ steps.repo-name.outputs.repo-name }} - name: Upload Release Assets # the 'official' actions version does not yet support dynamically # supplying asset names to upload. @csexton's version chosen based on diff --git a/.gitignore b/.gitignore index 01d8dc6..5c0044c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,128 @@ -*.mpy -.idea -__pycache__ -_build -*.pyc +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build*/ +bundles/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments .env -build* -bundles -*.DS_Store -.eggs -dist -**/*.egg-info +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# VS Code folder .vscode diff --git a/README.rst b/README.rst index 2781aec..ab981b9 100644 --- a/README.rst +++ b/README.rst @@ -1,241 +1,38 @@ - - -.. image:: https://readthedocs.org/projects/circuitpython-nrf24l01/badge/?version=stable - :target: https://circuitpython-nrf24l01.readthedocs.io/en/stable/ - :alt: Documentation Status - -.. image:: https://github.com/2bndy5/CircuitPython_nRF24L01/workflows/Build%20CI/badge.svg - :target: https://github.com/2bndy5/CircuitPython_nRF24L01/actions?query=workflow%3A%22Build+CI%22 - :alt: Build Status - -.. image:: https://img.shields.io/github/commits-since/2bndy5/CircuitPython_nRF24L01/latest?&style=plastic - :alt: GitHub commits since latest release (by date) - :target: https://github.com/2bndy5/CircuitPython_nRF24L01/commits/master - -.. image:: https://img.shields.io/pypi/v/circuitpython-nrf24l01.svg - :alt: latest version on PyPI - :target: https://pypi.python.org/pypi/circuitpython-nrf24l01 - -.. image:: https://pepy.tech/badge/circuitpython-nrf24l01?label=pypi%20downloads&logo=python - :alt: Total PyPI downloads - :target: https://pepy.tech/project/circuitpython-nrf24l01 - -.. image:: https://img.shields.io/github/downloads/2bndy5/CircuitPython_nRF24L01/total?color=success&label=Downloads&logo=github&style=plastic - :alt: GitHub All Release Downloads - :target: https://github.com/2bndy5/CircuitPython_nRF24L01/releases - -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 ----------------------------- - -* change the addresses' length (can be 3 to 5 bytes long) -* dynamically sized payloads (max 32 bytes each) or statically sized payloads -* automatic responding acknowledgment (ACK) for verifying transmission success -* 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 `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 `irq_dr`, `irq_ds`, `irq_df` attributes) -* invoke sleep mode (AKA power down mode) for ultra-low current consumption -* cyclic redundancy checking (CRC) up to 2 bytes long -* adjust the nRF24L01's builtin automatic re-transmit feature's parameters (`arc`: number of attempts, `ard`: delay between attempts) -* 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" (`ard`) and "automatic retry count" (`arc`) attributes set accordingly (varyingly high -- this has not been tested). - -Dependencies -============= -This driver depends on: - -* `Adafruit CircuitPython `_ -* `Bus Device `_ - -Please ensure all dependencies are available on the CircuitPython filesystem. -This is easily achieved by downloading -`the Adafruit library and driver bundle `_. - -Installing from PyPI -===================== - -On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from -PyPI `_. To install for current user: - -.. code-block:: shell - - pip3 install circuitpython-nrf24l01 - -To install system-wide (this may be required in some cases): - -.. code-block:: shell - - sudo pip3 install circuitpython-nrf24l01 - -To install in a virtual environment in your current project: - -.. code-block:: shell - - mkdir project-name && cd project-name - python3 -m venv .env - source .env/bin/activate - pip3 install circuitpython-nrf24l01 - -Pinout -====== -.. 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 library's `example directory `_. - -+------------+----------------+----------------+ -| nRF24L01 | Raspberry Pi | ItsyBitsy M4 | -+============+================+================+ -| GND | GND | GND | -+------------+----------------+----------------+ -| VCC | 3V | 3.3V | -+------------+----------------+----------------+ -| CE | GPIO4 | D4 | -+------------+----------------+----------------+ -| CSN | GPIO5 | D5 | -+------------+----------------+----------------+ -| SCK | GPIO11 (SCK) | SCK | -+------------+----------------+----------------+ -| MOSI | GPIO10 (MOSI) | MOSI | -+------------+----------------+----------------+ -| MISO | GPIO9 (MISO) | MISO | -+------------+----------------+----------------+ -| IRQ | GPIO4 | D12 | -+------------+----------------+----------------+ - -.. tip:: User reports and personal experiences have improved results if there is a capacitor of 100 mirofarads [+ another optional 0.1 microfarads capacitor for added stability] connected in parrallel to the VCC and GND pins. - -Using The Examples -================== - -See `examples `_ for testing certain features of this the library. The examples were developed and tested on both Raspberry Pi and ItsyBitsy M4. Pins have been hard coded in the examples for the corresponding device, so please adjust these accordingly to your circuitpython device if necessary. - -To run the simple example, navigate to this repository's "examples" folder in the terminal. If you're working with a CircuitPython device (not a Raspberry Pi), copy the file named "nrf24l01_simple_test.py" from this repository's "examples" folder to the root directory of your CircuitPython device's CIRCUITPY drive. Now you're ready to open a python REPR and run the following commands: - -.. code-block:: python - - >>> from nrf24l01_simple_test import * - nRF24L01 Simple test - Run slave() on receiver - Run master() on transmitter - >>> master(3) - Sending: 3 as struct: b'\x03\x00\x00\x00' - send() succeessful - Transmission took 86.0 ms - Sending: 2 as struct: b'\x02\x00\x00\x00' - send() succeessful - Transmission took 109.0 ms - Sending: 1 as struct: b'\x01\x00\x00\x00' - send() succeessful - Transmission took 109.0 ms - # these results were observed from a test on the Raspberry Pi 3 - # transmissions from a CircuitPython device took 32 to 64 ms - - -About the nRF24L01 -================== - -Here are the features listed directly from the datasheet (referenced here in the documentation as the `nRF24L01+ Specification Sheet `_): - -Key Features: -------------- - - * Worldwide 2.4GHz ISM band operation - * 250kbps, 1Mbps and 2Mbps on air data rates - * Ultra low power operation - * 11.3mA TX at 0dBm output power - * 13.5mA RX at 2Mbps air data rate - * 900nA in power down - * 26μA in standby-I - * On chip voltage regulator - * 1.9 to 3.6V supply range - * Enhanced ShockBurst™ - * Automatic packet handling - * Auto packet transaction handling - * 6 data pipe MultiCeiver™ - * Drop-in compatibility with nRF24L01 - * On-air compatible in 250kbps and 1Mbps with nRF2401A, nRF2402, nRF24E1 and nRF24E2 - * Low cost BOM - * ±60ppm 16MHz crystal - * 5V tolerant inputs - * Compact 20-pin 4x4mm QFN package - -Applications ------------- - - * Wireless PC Peripherals - * Mouse, keyboards and remotes - * 3-in-1 desktop bundles - * Advanced Media center remote controls - * VoIP headsets - * Game controllers - * Sports watches and sensors - * RF remote controls for consumer electronics - * Home and commercial automation - * Ultra low power sensor networks - * Active RFID - * Asset tracking systems - * Toys - -Future Project Ideas/Additions using the nRF24L01 (not currently supported by this circuitpython library): - - * `There's a few blog posts by Nerd Ralph demonstrating how to use the nRF24L01 via 2 or 3 pins `_ (uses custom bitbanging SPI functions and an external circuit involving a resistor and a capacitor) - * network linking layer, maybe something like `TMRh20's RF24Network `_ - * add a fake BLE module for sending BLE beacon advertisments from the nRF24L01 as outlined by `Dmitry Grinberg in his write-up (including C source code) `_. We've started developing this, but fell short of success in `the BLEfake branch of this library's repository `_ - -Where do I get 1? -================= - -See the store links on the sidebar or just google "nRF24L01". It is worth noting that you generally don't want to buy just 1 as you need 2 for testing -- 1 to send & 1 to receive and vise versa. This library has been tested on a cheaply bought 10 pack from Amazon.com using a highly recommended capacitor (100 µF) on the power pins. Don't get lost on Amazon or eBay! There are other wireless transceivers that are NOT compatible with this library. For instance, the esp8266-01 (also sold in packs) is NOT compatible with this library, but looks very similar to the nRF24L01(+) and could lead to an accidental purchase. - -Contributing -============ - -Contributions are welcome! Please read our `Code of Conduct -`_ -before contributing to help this project stay welcoming. To contribute, all you need to do is fork `this repository `_, develop your idea(s) and submit a pull request when stable. To initiate a discussion of idea(s), you need only open an issue on the aforementioned repository (doesn't have to be a bug report). - -Sphinx documentation ------------------------ - -Sphinx is used to build the documentation based on rST files and comments in the code. First, -install dependencies (feel free to reuse the virtual environment from `above `_): - -.. code-block:: shell - - python3 -m venv .env - source .env/bin/activate - pip install Sphinx sphinx-rtd-theme - -Now, once you have the virtual environment activated: - -.. code-block:: shell - - cd docs - sphinx-build -E -W -b html . _build/html - -This will output the documentation to ``docs/_build/html``. Open the index.html in your browser to -view them. It will also (due to -W) error out on any warning like Travis CI does. This is a good way to locally verify it will pass. + +.. image:: https://readthedocs.org/projects/circuitpython-nrf24l01/badge/?version=stable + :target: https://circuitpython-nrf24l01.readthedocs.io/en/stable/ + :alt: Documentation Status + +.. image:: https://github.com/2bndy5/CircuitPython_nRF24L01/workflows/Build%20CI/badge.svg + :target: https://github.com/2bndy5/CircuitPython_nRF24L01/actions?query=workflow%3A%22Build+CI%22 + :alt: Build Status + +.. image:: https://img.shields.io/pypi/v/circuitpython-nrf24l01.svg + :alt: latest version on PyPI + :target: https://pypi.python.org/pypi/circuitpython-nrf24l01 + +.. image:: https://pepy.tech/badge/circuitpython-nrf24l01?label=pypi%20downloads&logo=python + :alt: Total PyPI downloads + :target: https://pepy.tech/project/circuitpython-nrf24l01 + +Read The Docs +============= + +Documentation for this library is hosted at +`ReadTheDocs.org `_ + +About this Library +================== + +This is a Circuitpython driver library for the nRF24L01 transceiver. + +Originally this code was a Micropython module written by Damien P. George +& Peter Hinch which can still be found `here +`_ + +The Micropython source has since been rewritten to expose all the nRF24L01's +features and for Circuitpython compatible devices (including linux-based +SoC computers like the Raspberry Pi). +Modified by Brendan Doherty & Rhys Thomas. + +* Authors: Damien P. George, Peter Hinch, Rhys Thomas, Brendan Doherty diff --git a/circuitpython_nrf24l01/__init__.py b/circuitpython_nrf24l01/__init__.py index fe0189b..e69de29 100644 --- a/circuitpython_nrf24l01/__init__.py +++ b/circuitpython_nrf24l01/__init__.py @@ -1,5 +0,0 @@ -"""module managment for the circuitpython-nrf24l01 package""" - -from .rf24 import RF24 - -__all__ = ['RF24'] diff --git a/circuitpython_nrf24l01/fake_ble.py b/circuitpython_nrf24l01/fake_ble.py new file mode 100644 index 0000000..e7c7e82 --- /dev/null +++ b/circuitpython_nrf24l01/fake_ble.py @@ -0,0 +1,415 @@ +# 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. +"""Original research was done by `Dmitry Grinberg and his write-up can be +found here `_""" +from os import urandom +import struct +from .rf24 import RF24 + +def swap_bits(original): + """This function reverses the bit order for a single byte.""" + original &= 0xFF + reverse = 0 + for _ in range(8): + reverse <<= 1 + reverse |= original & 1 + original >>= 1 + return reverse + + +def reverse_bits(original): + """This function reverses the bit order for an entire + buffer protocol object.""" + ret = bytearray(len(original)) + for i, byte in enumerate(original): + ret[i] = swap_bits(byte) + return ret + + +def chunk(buf, data_type=0x16): + """This function is used to pack data values into a block of data that + make up part of the BLE payload per Bluetooth Core Specifications.""" + return bytearray([len(buf) + 1, data_type & 0xFF]) + buf + + +def crc24_ble(data, deg_poly=0x65B, init_val=0x555555): + """This function calculates a checksum of various sized buffers.""" + crc = init_val + for byte in data: + crc ^= swap_bits(byte) << 16 + for _ in range(8): + if crc & 0x800000: + crc = (crc << 1) ^ deg_poly + else: + crc <<= 1 + crc &= 0xFFFFFF + return reverse_bits((crc).to_bytes(3, "big")) + + +BLE_FREQ = (2, 26, 80) #: The BLE channel number is different from the nRF channel number. + + +class FakeBLE: + """A class to implement BLE advertisements using the nRF24L01.""" + + def __init__(self, spi, csn, ce, spi_frequency=10000000): + self._radio = RF24(spi, csn, ce, spi_frequency=spi_frequency) + self._chan = 0 + self._to_iphone = 0x42 + self._show_dbm = False + self._ble_name = None + self._mac = urandom(6) + with self: + self._radio.dynamic_payloads = False + self._radio.payload_length = 32 + self._radio.data_rate = 1 + self._radio.arc = 0 + self._radio.address_length = 4 + self._radio.open_rx_pipe(0, b"\x71\x91\x7D\x6B") + self._radio.open_tx_pipe(b"\x71\x91\x7D\x6B") + self._radio.auto_ack = False + self._radio.crc = 0 + self._radio.flush_rx() + + def __enter__(self): + self._radio = self._radio.__enter__() + return self + + def __exit__(self, *exc): + self._show_dbm = False + self._ble_name = None + return self._radio.__exit__() + + @property + def to_iphone(self): + """A `bool` to specify if advertisements should be compatible with + the iPhone.""" + return self._to_iphone == 0x40 + + @to_iphone.setter + def to_iphone(self, enable): + self._to_iphone = 0x40 if enable else 0x42 + + @property + def mac(self): + """This attribute returns a 6-byte buffer that is used as the + arbitrary mac address of the BLE device being emulated.""" + return self._mac + + @mac.setter + def mac(self, address): + if address is None: + self._mac = urandom(6) + if isinstance(address, int): + self._mac = (address).to_bytes(6, "little") + elif isinstance(address, (bytearray, bytes)): + self._mac = address + if len(self._mac) < 6: + self._mac += urandom(6 - len(self._mac)) + + @property + def name(self): + """The broadcasted BLE name of the nRF24L01.""" + return self._ble_name + + @name.setter + def name(self, n): + if n is not None: + if not isinstance(n, (bytes, bytearray)): + raise ValueError("name must be a bytearray or bytes object.") + if len(n) > (21 - self._show_dbm * 3): + raise ValueError("name length exceeds maximum.") + self._ble_name = n + + @property + def show_pa_level(self): + """If this attribute is `True`, the payload will automatically include + the nRF24L01's pa_level in the advertisement.""" + return bool(self._show_dbm) + + @show_pa_level.setter + def show_pa_level(self, enable): + if enable and len(self.name) > 16: + raise ValueError("there is not enough room to show the pa_level.") + self._show_dbm = bool(enable) + + def hop_channel(self): + """Trigger an automatic change of BLE compliant channels.""" + self._chan += 1 + if self._chan > 2: + self._chan = 0 + self._radio.channel = BLE_FREQ[self._chan] + + def whiten(self, data): + """Whitening the BLE packet data ensures there's no long repeatition + of bits.""" + data, coef = (bytearray(data), (self._chan + 37) | 0x40) + for i, byte in enumerate(data): + res, mask = (0, 1) + for _ in range(8): + if coef & 1: + coef ^= 0x88 + byte ^= mask + mask <<= 1 + coef >>= 1 + data[i] = byte ^ res + return data + + def _make_payload(self, payload): + """Assemble the entire packet to be transmitted as a payload.""" + if self.available(payload) < 0: + raise ValueError( + "Payload length exceeds maximum buffer size by " + "{} bytes".format(abs(self.available(payload))) + ) + name_length = (len(self.name) + 2) if self.name is not None else 0 + pl_size = 9 + len(payload) + name_length + self._show_dbm * 3 + buf = bytes([self._to_iphone, pl_size]) + self.mac + buf += chunk(b"\x05", 1) + pa_level = b"" + if self._show_dbm: + pa_level = chunk(struct.pack(">b", self._radio.pa_level), 0x0A) + buf += pa_level + if name_length: + buf += chunk(self.name, 0x08) + buf += payload + buf += crc24_ble(buf) + return buf + + def available(self, hypothetical=b""): + """This function will calculates how much length (in bytes) is + available in the next payload.""" + name_length = (len(self.name) + 2) if self.name is not None else 0 + return 18 - name_length - self._show_dbm * 3 - len(hypothetical) + + def advertise(self, buf=b"", data_type=0xFF): + """This blocking function is used to broadcast a payload.""" + if not isinstance(buf, (bytearray, bytes, list, tuple)): + raise ValueError("buffer is an invalid format") + payload = b"" + if isinstance(buf, (list, tuple)): + for b in buf: + payload += b + else: + payload = chunk(buf, data_type) if buf else b"" + payload = self._make_payload(payload) + self._radio.send(reverse_bits(self.whiten(payload))) + + @property + def pa_level(self): + """See :py:attr:`~circuitpython_nrf24l01.rf24.RF24.pa_level` for + more details.""" + return self._radio.pa_level + + @pa_level.setter + def pa_level(self, value): + self._radio.pa_level = value + + @property + def channel(self): + """The only allowed channels are those contained in the `BLE_FREQ` + tuple.""" + return self._radio.channel + + @channel.setter + def channel(self, value): + if value not in BLE_FREQ: + raise ValueError( + "nrf channel {} is not a valid BLE frequency".format(value) + ) + self._radio.channel = value + + @property + def payload_length(self): + """This attribute is best left at 32 bytes for all BLE + operations.""" + return self._radio.payload_length + + @payload_length.setter + def payload_length(self, value): + self._radio.payload_length = value + + @property + def power(self): + """See :py:attr:`~circuitpython_nrf24l01.rf24.RF24.power` for more + details.""" + return self._radio.power + + @power.setter + def power(self, is_on): + self._radio.power = is_on + + @property + def is_lna_enabled(self): + """See :py:attr:`~circuitpython_nrf24l01.rf24.RF24.is_lna_enabled` + for more details.""" + return self._radio.is_lna_enabled + + @property + def is_plus_variant(self): + """See :py:attr:`~circuitpython_nrf24l01.rf24.RF24.is_plus_variant` + for more details.""" + return self._radio.is_plus_variant + + def interrupt_config(self, data_recv=True, data_sent=True): + """See :py:func:`~circuitpython_nrf24l01.rf24.RF24.interrupt_config()` + for more details. + + .. warning:: The :py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_df` + attribute (and also this function's ``data_fail`` parameter) is + not implemented for BLE operations.""" + self._radio.interrupt_config(data_recv=data_recv, data_sent=data_sent) + + @property + def irq_ds(self): + """See :py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_ds` for + more details.""" + return self._radio.irq_ds + + @property + def irq_dr(self): + """See :py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_dr` for + more details.""" + return self._radio.irq_dr + + def clear_status_flags(self): + """See :py:func:`~circuitpython_nrf24l01.rf24.RF24.clear_status_flags()` + for more details.""" + self._radio.clear_status_flags() + + def update(self): + """See :py:func:`~circuitpython_nrf24l01.rf24.RF24.update()` for more + details.""" + self._radio.update() + + def what_happened(self, dump_pipes=False): + """See :py:func:`~circuitpython_nrf24l01.rf24.RF24.what_happened()` + for more details.""" + self._radio.what_happened(dump_pipes=dump_pipes) + +class ServiceData: + """An abstract helper class to package specific service data using + Bluetooth SIG defined 16-bit UUID flags to describe the data type.""" + + def __init__(self, uuid): + self._type = struct.pack("B", value) + + +class UrlServiceData(ServiceData): + """This derivitive of the `ServiceData` class can be used to represent + URL data as a `bytes` value.""" + + def __init__(self): + super().__init__(0xFEAA) + self._type += bytes([0x10]) + struct.pack(">b", -25) + + @property + def pa_level_at_1_meter(self): + """The TX power level (in dBm) at 1 meter from the nRF24L01. This + defaults to -25 (due to testing when broadcasting with 0 dBm) and must + be a 1-byte signed `int`.""" + return struct.unpack(">b", self._type[-1:])[0] + + @pa_level_at_1_meter.setter + def pa_level_at_1_meter(self, value): + self._type = self._type[:-1] + struct.pack(">b", int(value)) + + @property + def uuid(self): + return self._type[:2] + + @ServiceData.data.setter + def data(self, value): + value = value.replace("http://www.", "\x00") + value = value.replace("https://www.", "\x01") + value = value.replace("http://", "\x02") + value = value.replace("https://", "\x03") + value = value.replace(".com/", "\x00") + value = value.replace(".org/", "\x01") + value = value.replace(".edu/", "\x02") + value = value.replace(".net/", "\x03") + value = value.replace(".info/", "\x04") + value = value.replace(".biz/", "\x05") + value = value.replace(".gov/", "\x06") + value = value.replace(".com", "\x07") + value = value.replace(".org", "\x08") + value = value.replace(".edu", "\x09") + value = value.replace(".net", "\x0A") + value = value.replace(".info", "\x0B") + value = value.replace(".biz", "\x0C") + self._data = value.replace(".gov", "\x0D").encode("utf-8") diff --git a/circuitpython_nrf24l01/rf24.py b/circuitpython_nrf24l01/rf24.py index 2352144..12f48f6 100644 --- a/circuitpython_nrf24l01/rf24.py +++ b/circuitpython_nrf24l01/rf24.py @@ -20,1503 +20,781 @@ # 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 -""" -rf24 module containing the base class RF24 -""" +"""rf24 module containing the base class RF24""" __version__ = "0.0.0-auto.0" __repo__ = "https://github.com/2bndy5/CircuitPython_nRF24L01.git" import time -from adafruit_bus_device.spi_device import SPIDevice - -# nRF24L01 registers -# pylint: disable=bad-whitespace -CONFIG = 0x00 #: register for configuring IRQ, CRC, PWR & RX/TX roles -EN_AA = 0x01 #: register for auto-ACK feature. Each bit represents this feature per pipe -EN_RX = 0x02 #: register to open/close pipes. Each bit represents this feature per pipe -SETUP_AW = 0x03 #: address width register -SETUP_RETR = 0x04 #: auto-retry count and delay register -RF_CH = 0x05 #: channel register -RF_SETUP = 0x06 #: RF Power Amplifier & Data Rate -RX_ADDR = 0x0a #: RX pipe addresses == [0,5]:[0x0a:0x0f] -RX_PW = 0x11 #: RX payload widths on pipes == [0,5]:[0x11,0x16] -FIFO = 0x17 #: register containing info on both RX/TX FIFOs + re-use payload flag -DYNPD = 0x1c #: dynamic payloads feature. Each bit represents this feature per pipe -FEATURE = 0x1d #: global flags for dynamic payloads, custom ACK payloads, & Ask no ACK -TX_ADDR = 0x10 #: Address that is used for TX transmissions -# pylint: enable=bad-whitespace +from micropython import const + +try: + from ubus_device import SPIDevice +except ImportError: + from adafruit_bus_device.spi_device import SPIDevice + +CONFIGURE = const(0x00) # IRQ masking, CRC scheme, PWR control, & RX/TX roles +AUTO_ACK = const(0x01) # auto-ACK status for all pipes +OPEN_PIPES = const(0x02) # open/close RX status for all pipes +SETUP_RETR = const(0x04) # auto-retry count & delay values +RF_PA_RATE = const(0x06) # RF Power Amplifier & Data Rate values +RX_ADDR_P0 = const(0x0A) # RX pipe addresses; pipes 0-5 = 0x0A-0x0F +TX_ADDRESS = const(0x10) # Address used for TX transmissions +RX_PL_LENG = const(0x11) # RX payload widths; pipes 0-5 = 0x11-0x16 +DYN_PL_LEN = const(0x1C) # dynamic payloads status for all pipes +TX_FEATURE = const(0x1D) # dynamic TX-payloads, TX-ACK payloads, TX-NO_ACK +CSN_DELAY = 0.005 +"""The delay time (in seconds) used to let the CSN pin settle, +allowing a clean SPI transaction.""" + class RF24: - """A driver class for the nRF24L01(+) transceiver radios. This class aims to be compatible with - other devices in the nRF24xxx product line that implement the Nordic proprietary Enhanced - ShockBurst Protocol (and/or the legacy ShockBurst Protocol), but officially only supports - (through testing) the nRF24L01 and nRF24L01+ devices. - - :param ~busio.SPI spi: The object for the SPI bus that the nRF24L01 is connected to. - - .. tip:: This object is meant to be shared amongst other driver classes (like - adafruit_mcp3xxx.mcp3008 for example) that use the same SPI bus. Otherwise, multiple - devices on the same SPI bus with different spi objects may produce errors or - undesirable behavior. - :param ~digitalio.DigitalInOut csn: The digital output pin that is connected to the nRF24L01's - CSN (Chip Select Not) pin. This is required. - :param ~digitalio.DigitalInOut ce: The digital output pin that is connected to the nRF24L01's - CE (Chip Enable) pin. This is required. - :param int channel: This is used to specify a certain radio frequency that the nRF24L01 uses. - Defaults to 76 and can be changed at any time by using the `channel` attribute. - :param int payload_length: This is the length (in bytes) of a single payload to be transmitted - or received. This is ignored if the `dynamic_payloads` attribute is enabled. Defaults to 32 - and must be in range [1,32]. This can be changed at any time by using the `payload_length` - attribute. - :param int address_length: This is the length (in bytes) of the addresses that are assigned to - the data pipes for transmitting/receiving. Defaults to 5 and must be in range [3,5]. This - can be changed at any time by using the `address_length` attribute. - :param int ard: This specifies the delay time (in µs) between attempts to automatically - re-transmit. This can be changed at any time by using the `ard` attribute. This parameter - must be a multiple of 250 in the range [250,4000]. Defualts to 1500 µs. - :param int arc: This specifies the automatic re-transmit count (maximum number of automatically - attempts to re-transmit). This can be changed at any time by using the `arc` attribute. - This parameter must be in the range [0,15]. Defaults to 3. - :param int crc: This parameter controls the CRC setting of transmitted packets. Options are - ``0`` (off), ``1`` or ``2`` (byte long CRC enabled). This can be changed at any time by - using the `crc` attribute. Defaults to 2. - :param int data_rate: This parameter controls the RF data rate setting of transmissions. - Options are ``1`` (Mbps), ``2`` (Mbps), or ``250`` (Kbps). This can be changed at any time - by using the `data_rate` attribute. Defaults to 1. - :param int pa_level: This parameter controls the RF power amplifier setting of transmissions. - Options are ``0`` (dBm), ``-6`` (dBm), ``-12`` (dBm), or ``-18`` (dBm). This can be changed - at any time by using the `pa_level` attribute. Defaults to 0. - :param bool dynamic_payloads: This parameter enables/disables the dynamic payload length - feature of the nRF24L01. Defaults to enabled. This can be changed at any time by using the - `dynamic_payloads` attribute. - :param bool auto_ack: This parameter enables/disables the automatic acknowledgment (ACK) - feature of the nRF24L01. Defaults to enabled if `dynamic_payloads` is enabled. This can be - changed at any time by using the `auto_ack` attribute. - :param bool ask_no_ack: This represents a special flag that has to be thrown to enable a - feature specific to individual payloads. Setting this parameter only enables access to this - feature; it does not invoke it (see parameters for `send()` or `write()` functions). - Enabling/Disabling this does not affect `auto_ack` attribute. - :param bool ack: This represents a special flag that has to be thrown to enable a feature - allowing custom response payloads appended to the ACK packets. Enabling this also requires - the `auto_ack` attribute enabled. This can be changed at any time by using the `ack` - attribute. - :param bool irq_dr: When "Data is Ready", this configures the interrupt (IRQ) trigger of the - nRF24L01's IRQ pin (active low). Defaults to enabled. This can be changed at any time by - using the `interrupt_config()` function. - :param bool irq_ds: When "Data is Sent", this configures the interrupt (IRQ) trigger of the - nRF24L01's IRQ pin (active low). Defaults to enabled. This can be changed at any time by - using the `interrupt_config()` function. - :param bool irq_df: When "max retry attempts are reached" (specified by the `arc` attribute), - this configures the interrupt (IRQ) trigger of the nRF24L01's IRQ pin (active low) and - represents transmission failure. Defaults to enabled. This can be changed at any time by - using the `interrupt_config()` function. - """ - def __init__(self, spi, csn, ce, - channel=76, - payload_length=32, - address_length=5, - ard=1500, - arc=3, - crc=2, - data_rate=1, - pa_level=0, - dynamic_payloads=True, - auto_ack=True, - ask_no_ack=True, - ack=False, - irq_dr=True, - irq_ds=True, - irq_df=True): - self._payload_length = payload_length # inits internal attribute - self.payload_length = payload_length - # last address assigned to pipe0 for reading. init to None - self._fifo = 0 - self._status = 0 - # init shadow copy of RX addresses for all pipes - self._pipes = [b'', b'', 0, 0, 0, 0] - 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 + """A driver class for the nRF24L01(+) transceiver radios.""" + + def __init__(self, spi, csn, ce, spi_frequency=10000000): + self._spi = SPIDevice(spi, chip_select=csn, baudrate=spi_frequency) self.ce_pin = ce - # reset ce.value & disable the chip comms - self.ce_pin.switch_to_output(value=False) - # if radio is powered up and CE is LOW: standby-I mode - # if radio is powered up and CE is HIGH: standby-II mode - - # NOTE per spec sheet: nRF24L01+ must be in a standby or power down mode before writing - # to the configuration register - # configure the CONFIG register:IRQ(s) config, setup CRC feature, and trigger standby-I & - # TX mode (the "| 2") - if 0 <= crc <= 2: - self._config = ((not irq_dr) << 6) | ((not irq_ds) << 5) | ((not irq_df) << 4) | \ - ((crc + 1) << 2 if crc else 0) | 2 - self._reg_write(CONFIG, self._config) # dump to register - else: - raise ValueError( - "CRC byte length must be an int equal to 0 (off), 1, or 2") - - # check for device presence by verifying nRF24L01 is in TX + standby-I mode - if self._reg_read(CONFIG) & 3 == 2: # if in TX + standby-I mode - self.power = False # power down - else: # hardware presence check NOT passed - print(bin(self._reg_read(CONFIG))) + self.ce_pin.switch_to_output(value=False) # pre-empt standby-I mode + self._status = 0 # status byte returned on all SPI transactions + # pre-configure the CONFIGURE register: + # 0x0E = IRQs are all enabled, CRC is enabled with 2 bytes, and + # power up in TX mode + self._config = 0x0E + self._reg_write(CONFIGURE, self._config) + if self._reg_read(CONFIGURE) & 3 != 2: raise RuntimeError("nRF24L01 Hardware not responding") - - # capture all pipe's RX addresses & the TX address from last usage - for i in range(6): + self.power = False + # init shadow copy of RX addresses for all pipes for context manager + self._pipes = [bytearray(5), bytearray(5), 0xC3, 0xC4, 0xC5, 0xC6] + # _open_pipes attribute reflects only RX state on each pipe + self._open_pipes = 0 # 0 = all pipes closed + for i in range(6): # capture RX addresses from registers if i < 2: - self._pipes[i] = self._reg_read_bytes(RX_ADDR + i) + self._pipes[i] = self._reg_read_bytes(RX_ADDR_P0 + i) else: - self._pipes[i] = self._reg_read(RX_ADDR + i) - - # shadow copy of the TX_ADDR - self._tx_address = self._reg_read_bytes(TX_ADDR) - - # configure the SETUP_RETR register - if 250 <= ard <= 4000 and ard % 250 == 0 and 0 <= arc <= 15: - self._setup_retr = (int((ard - 250) / 250) << 4) | arc - else: - raise ValueError("automatic re-transmit delay can only be a multiple of 250 in range " - "[250,4000]\nautomatic re-transmit count(/attempts) must range " - "[0,15]") - - # configure the RF_SETUP register - if data_rate in (1, 2, 250) and pa_level in (-18, -12, -6, 0): - data_rate = 0 if data_rate == 1 else ( - 8 if data_rate == 2 else 0x20) - pa_level = (3 - int(pa_level / -6)) * 2 - self._rf_setup = data_rate | pa_level - else: - raise ValueError("data rate must be one of the following ([M,M,K]bps): 1, 2, 250" - "\npower amplifier must be one of the following (dBm): -18, -12," - " -6, 0") - - # manage dynamic_payloads, auto_ack, and ack features - self._dyn_pl = 0x3F if dynamic_payloads else 0 # 0x3F == enabled on all pipes - self._aa = 0x3F if auto_ack else 0 # 0x3F == enabled on all pipes - self._features = (dynamic_payloads << 2) | ((ack if auto_ack and dynamic_payloads - else False) << 1) | ask_no_ack - - # init the last few singleton attribute - self._channel = channel - self._addr_len = address_length - - with self: # write to registers & power up - # using __enter__() configures all virtual features and settings to the hardware - # registers - self.ce_pin.value = 0 # ensure standby-I mode to write to CONFIG register - self._reg_write(CONFIG, self._config | 1) # enable RX mode - time.sleep(0.000015) # wait time for transitioning modes RX/TX - self.flush_rx() # spec sheet say "used in RX mode" - self._reg_write(CONFIG, self._config & 0xC) # power down + TX mode - time.sleep(0.000015) # wait time for transitioning modes RX/TX - self.flush_tx() # spec sheet say "used in TX mode" - self.clear_status_flags() # writes directly to STATUS register + self._pipes[i] = self._reg_read(RX_ADDR_P0 + i) + # test is nRF24L01 is a plus variant using a command specific to + # non-plus variants + self._is_plus_variant = False + b4_toggle = self._reg_read(TX_FEATURE) + # derelict ACTIVATE command toggles bits in the TX_FEATURE register + self._reg_write(0x50, 0x73) + after_toggle = self._reg_read(TX_FEATURE) + if b4_toggle == after_toggle: + self._is_plus_variant = True + if not after_toggle: # if features are disabled + self._reg_write(0x50, 0x73) # ensure they're enabled + # init shadow copy of last RX_ADDR_P0 written to pipe 0 needed as + # open_tx_pipe() appropriates pipe 0 for ACK packet + self._pipe0_read_addr = None + # init shadow copy of register about FIFO info + self._fifo = 0 + # shadow copy of the TX_ADDRESS + self._tx_address = self._reg_read_bytes(TX_ADDRESS) + # pre-configure the SETUP_RETR register + self._retry_setup = 0x53 # ard = 1500; arc = 3 + # pre-configure the RF_SETUP register + self._rf_setup = 0x07 # 1 Mbps data_rate, and 0 dbm pa_level + # pre-configure dynamic_payloads & auto_ack for RX operations + self._dyn_pl = 0x3F # 0x3F = enable dynamic_payloads on all pipes + self._aa = 0x3F # 0x3F = enable auto_ack on all pipes + # pre-configure features for TX operations: + # 5 = enable dynamic_payloads, disable custom ack payloads, & + # allow ask_no_ack command + self._features = 5 + self._channel = 76 # 2.476 GHz + self._addr_len = 5 # 5-byte long addresses + self._pl_len = [32] * 6 # 32-byte static payloads for all pipes + + with self: # dumps internal attributes to all registers + self.flush_rx() + self.flush_tx() + self.clear_status_flags() def __enter__(self): - # dump IRQ and CRC data to CONFIG register - self._reg_write(CONFIG, self._config & 0x7C) - self._reg_write(RF_SETUP, self._rf_setup) # dump to RF_SETUP register - # dump open/close pipe status to EN_RXADDR register (for all pipes) - self._reg_write(EN_RX, self._open_pipes) - self._reg_write(DYNPD, self._dyn_pl) # dump to DYNPD register - self._reg_write(EN_AA, self._aa) # dump to EN_AA register - self._reg_write(FEATURE, self._features) # dump to FEATURE register - # dump to SETUP_RETR register - self._reg_write(SETUP_RETR, self._setup_retr) - # dump pipes' RX addresses and static payload lengths - for i, address in enumerate(self._pipes): + self.ce_pin.value = False + self._reg_write(CONFIGURE, self._config & 0x7C) + self._reg_write(RF_PA_RATE, self._rf_setup) + self._reg_write(OPEN_PIPES, self._open_pipes) + self._reg_write(DYN_PL_LEN, self._dyn_pl) + self._reg_write(AUTO_ACK, self._aa) + self._reg_write(TX_FEATURE, self._features) + self._reg_write(SETUP_RETR, self._retry_setup) + for i, addr in enumerate(self._pipes): if i < 2: - self._reg_write_bytes(RX_ADDR + i, address) + self._reg_write_bytes(RX_ADDR_P0 + i, addr) 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 + self._reg_write(RX_ADDR_P0 + i, addr) + self._reg_write_bytes(TX_ADDRESS, self._tx_address) + self._reg_write(0x05, self._channel) + self._reg_write(0x03, self._addr_len - 2) + self.payload_length = self._pl_len return self def __exit__(self, *exc): - self.power = 0 + self.ce_pin.value = False + self.power = False return False # pylint: disable=no-member def _reg_read(self, reg): - buf = bytearray(2) # 2 = 1 status byte + 1 byte of returned content + out_buf = bytes([reg, 0]) + in_buf = bytearray([0, 0]) with self._spi as spi: - time.sleep(0.005) # time for CSN to settle - spi.readinto(buf, write_value=reg) - self._status = buf[0] # save status byte - return buf[1] # drop status byte and return the rest + time.sleep(CSN_DELAY) + spi.write_readinto(out_buf, in_buf) + self._status = in_buf[0] + return in_buf[1] def _reg_read_bytes(self, reg, buf_len=5): - # allow an extra byte for status data - buf = bytearray(buf_len + 1) + in_buf = bytearray(buf_len + 1) + out_buf = bytes([reg]) + b"\x00" * buf_len with self._spi as spi: - time.sleep(0.005) # time for CSN to settle - spi.readinto(buf, write_value=reg) - self._status = buf[0] # save status byte - return buf[1:] # drop status byte and return the rest + time.sleep(CSN_DELAY) + spi.write_readinto(out_buf, in_buf) + self._status = in_buf[0] + return in_buf[1:] def _reg_write_bytes(self, reg, out_buf): out_buf = bytes([0x20 | reg]) + out_buf in_buf = bytearray(len(out_buf)) with self._spi as spi: - time.sleep(0.005) # time for CSN to settle + time.sleep(CSN_DELAY) spi.write_readinto(out_buf, in_buf) - self._status = in_buf[0] # save status byte + self._status = in_buf[0] def _reg_write(self, reg, value=None): - if value is None: - out_buf = bytes([reg]) - else: + out_buf = bytes([reg]) + if value is not None: out_buf = bytes([0x20 | reg, value]) in_buf = bytearray(len(out_buf)) with self._spi as spi: - time.sleep(0.005) # time for CSN to settle + time.sleep(CSN_DELAY) spi.write_readinto(out_buf, in_buf) - self._status = in_buf[0] # save status byte + self._status = in_buf[0] + # pylint: enable=no-member @property def address_length(self): - """This `int` attribute specifies the length (in bytes) of addresses to be used for RX/TX - pipes. The addresses assigned to the data pipes must have byte length equal to the value - set for this attribute. - - A valid input value must be an `int` in range [3,5]. Otherwise a `ValueError` exception is - thrown. Default is set to the nRF24L01's maximum of 5. - """ - return self._reg_read(SETUP_AW) + 2 + """This `int` attribute specifies the length (in bytes) of addresses + to be used for RX/TX pipes.""" + return self._reg_read(0x03) + 2 @address_length.setter def address_length(self, length): - # nRF24L01+ must be in a standby or power down mode before writing to the configuration - # registers. - if 3 <= length <= 5: - # address width is saved in 2 bits making range = [3,5] - self._addr_len = int(length) - self._reg_write(SETUP_AW, length - 2) - else: - raise ValueError( - "address length can only be set in range [3,5] bytes") + if not 3 <= length <= 5: + raise ValueError("address_length can only be set in range [3, 5] bytes") + self._addr_len = int(length) + self._reg_write(0x03, length - 2) def open_tx_pipe(self, address): - """This function is used to open a data pipe for OTA (over the air) TX transmissions. - - :param bytearray address: The virtual address of the receiving nRF24L01. This must have a - length equal to the `address_length` attribute (see `address_length` attribute). - Otherwise a `ValueError` exception is thrown. The address specified here must match the - address set to one of the RX data pipes of the receiving nRF24L01. - - .. note:: There is no option to specify which data pipe to use because the nRF24L01 only - uses data pipe 0 in TX mode. Additionally, the nRF24L01 uses the same data pipe (pipe - 0) for receiving acknowledgement (ACK) packets in TX mode when the `auto_ack` attribute - is enabled. Thus, RX pipe 0 is appropriated with the TX address (specified here) when - `auto_ack` is set to `True`. - """ - if len(address) == self.address_length: - # if auto_ack == True, then use this TX address as the RX address for ACK - if self.auto_ack: - # settings need to match on both transceivers: dynamic_payloads and payload_length - self._pipes[0] = address - self._reg_write_bytes(RX_ADDR, address) # using pipe 0 - self._open_pipes = self._open_pipes | 1 # open pipe 0 for RX-ing ACK - self._reg_write(EN_RX, self._open_pipes) - self._payload_widths[0] = self.payload_length - self._reg_write(RX_PW, self.payload_length) # set expected payload_length - self._pipes[0] = address # update the context as well - self._tx_address = address - self._reg_write_bytes(TX_ADDR, address) - else: - raise ValueError("address must be a buffer protocol object with a byte length\nequal " - "to the address_length attribute (currently set to" - " {})".format(self.address_length)) - - 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. - """ + """This function is used to open a data pipe for OTA (over the air) + TX transmissions.""" + if self.arc: + for i, val in enumerate(address): + self._pipes[0][i] = val + self._reg_write_bytes(RX_ADDR_P0, address) + self._open_pipes = self._open_pipes | 1 + self._reg_write(OPEN_PIPES, self._open_pipes) + for i, val in enumerate(address): + self._tx_address[i] = val + self._reg_write_bytes(TX_ADDRESS, address) + + def close_rx_pipe(self, pipe_number): + """This function is used to close a specific data pipe from OTA (over + the air) RX transmissions.""" if pipe_number < 0 or pipe_number > 5: - raise ValueError("pipe number must be in range [0,5]") - self._open_pipes = self._reg_read(EN_RX) # refresh data - if reset:# reset pipe address accordingly - if not pipe_number: - # NOTE this does not clear the shadow copy (pipe0_read_addr) of address for pipe 0 - self._reg_write_bytes(pipe_number + RX_ADDR, b'\xe7' * 5) - self._pipes[pipe_number] = b'\xe7' * 5 - elif pipe_number == 1: # write the full address for pipe 1 - self._reg_write_bytes(pipe_number + RX_ADDR, b'\xc2' * 5) - self._pipes[pipe_number] = b'\xc2' * 5 - else: # write just MSB for 2 <= pipes <= 5 - self._reg_write(pipe_number + RX_ADDR, pipe_number + 0xc1) - self._pipes[pipe_number] = pipe_number + 0xc1 - # disable the specified data pipe if not already + raise ValueError("pipe number must be in range [0, 5]") + self._open_pipes = self._reg_read(OPEN_PIPES) 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(OPEN_PIPES, 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 - transmissions. If `dynamic_payloads` attribute is `False`, then the `payload_length` - attribute is used to specify the expected length of the RX payload on the specified data - pipe. - - :param int pipe_number: The data pipe to use for RX transactions. This must be in range - [0,5]. Otherwise a `ValueError` exception is thrown. - :param bytearray address: The virtual address to the receiving nRF24L01. This must have a - byte length equal to the `address_length` attribute. Otherwise a `ValueError` - exception is thrown. If using a ``pipe_number`` greater than 1, then only the MSByte - of the address is written, so make sure MSByte (first character) is unique among other - simultaneously receiving addresses). - - .. note:: The nRF24L01 shares the addresses' LSBytes (address[1:5]) on data pipes 2 through - 5. These shared LSBytes are determined by the address set to pipe 1. - """ - if pipe_number < 0 or pipe_number > 5: - raise ValueError("pipe number must be in range [0,5]") - if len(address) != self.address_length: - raise ValueError("address must be a buffer protocol object with a byte length\nequal " - "to the address_length attribute (currently set to " - "{})".format(self.address_length)) - - # write the address - if pipe_number < 2: # write entire address if pipe_number is 0 or 1 + """This function is used to open a specific data pipe for OTA (over + the air) RX transmissions.""" + if not 0 <= pipe_number <= 5: + raise ValueError("pipe number must be in range [0, 5]") + if not address: + raise ValueError("address length cannot be 0") + if pipe_number < 2: if not pipe_number: - # save shadow copy of address if target pipe_number is 0. This is done to help - # ensure the proper address is set to pipe 0 via _start_listening() as - # open_tx_pipe() will appropriate the address on pipe 0 if auto_ack is enabled for - # TX mode self._pipe0_read_addr = address - self._pipes[pipe_number] = address - self._reg_write_bytes(RX_ADDR + pipe_number, address) + for i, val in enumerate(address): + self._pipes[pipe_number][i] = val + self._reg_write_bytes(RX_ADDR_P0 + pipe_number, address) else: - # only write MSByte if pipe_number is not 0 or 1 self._pipes[pipe_number] = address[0] - self._reg_write(RX_ADDR + pipe_number, address[0]) - - # now manage the pipe - self._open_pipes = self._reg_read(EN_RX) # refresh data - # enable the specified data pipe - self._open_pipes = self._open_pipes | (1 << pipe_number) - self._reg_write(EN_RX, self._open_pipes) - - # now adjust payload_length accordingly despite dynamic_payload setting - # radio only uses this info in RX mode when dynamic_payloads == True - self._reg_write(RX_PW + pipe_number, self.payload_length) - self._payload_widths[pipe_number] = self.payload_length + self._reg_write(RX_ADDR_P0 + pipe_number, address[0]) + self._open_pipes = self._reg_read(OPEN_PIPES) | (1 << pipe_number) + self._reg_write(OPEN_PIPES, self._open_pipes) @property def listen(self): - """An attribute to represent the nRF24L01 primary role as a radio. - - Setting this attribute incorporates the proper transitioning to/from RX mode as it involves - playing with the `power` attribute and the nRF24L01's CE pin. This attribute does not power - down the nRF24L01, but will power it up when needed; use `power` attribute set to `False` - to put the nRF24L01 to sleep. - - A valid input value is a `bool` in which: - - `True` enables RX mode. Additionally, per `Appendix B of the nRF24L01+ Specifications - Sheet - `_, this attribute - flushes the RX FIFO, clears the `irq_dr` status flag, and puts nRF24L01 in power up - mode. Notice the CE pin is be held HIGH during RX mode. - - `False` disables RX mode. As mentioned in above link, this puts nRF24L01's power in - Standby-I (CE pin is LOW meaning low current & no transmissions) mode which is ideal - for post-reception work. Disabing RX mode doesn't flush the RX/TX FIFO buffers, so - remember to flush your 3-level FIFO buffers when appropriate using `flush_tx()` or - `flush_rx()` (see also the `recv()` function). - """ + """An attribute to represent the nRF24L01 primary role as a radio.""" return self.power and bool(self._config & 1) @listen.setter def listen(self, is_rx): - assert isinstance(is_rx, (bool, int)) - if self.listen != is_rx: - self._start_listening() - else: - self._stop_listening() - - def _start_listening(self): - # ensure radio is in power down or standby-I mode - if self.ce_pin.value: + if self.listen != bool(is_rx): self.ce_pin.value = 0 - - if self._pipe0_read_addr is not None: - # make sure the last call to open_rx_pipe(0) sticks if initialized - self._reg_write_bytes(RX_ADDR, self._pipe0_read_addr) - self._pipes[0] = self._pipe0_read_addr # update the context as well - - # power up radio & set radio in RX mode - self._config = self._config & 0xFC | 3 - self._reg_write(CONFIG, self._config) - time.sleep(0.00015) # mandatory wait time to power up radio or switch modes (RX/TX) - self.flush_rx() # spec sheet says "used in RX mode" - self.clear_status_flags(True, False, False) # only Data Ready flag - - # enable radio comms - self.ce_pin.value = 1 # radio begins listening after CE pulse is > 130 µs - time.sleep(0.00013) # ensure pulse is > 130 µs - # nRF24L01 has just entered active RX + standby-II mode - - def _stop_listening(self): - # ensure radio is in standby-I mode - if self.ce_pin.value: - self.ce_pin.value = 0 - # set radio in TX mode as recommended behavior per spec sheet. - self._config = self._config & 0xFE # does not put radio to sleep - self._reg_write(CONFIG, self._config) - # mandated wait for transitioning between modes RX/TX - time.sleep(0.00016) - # exits while still in Standby-I (low current & no transmissions) + if is_rx: + if self._pipe0_read_addr is not None: + for i, val in enumerate(self._pipe0_read_addr): + self._pipes[0][i] = val + self._reg_write_bytes(RX_ADDR_P0, self._pipe0_read_addr) + self._config = (self._config & 0xFC) | 3 + self._reg_write(CONFIGURE, self._config) + time.sleep(0.00015) # mandatory wait to power up radio + self.flush_rx() + self.clear_status_flags() + self.ce_pin.value = 1 # mandatory pulse is > 130 µs + time.sleep(0.00013) + else: + self._config = self._config & 0xFE + self._reg_write(CONFIGURE, self._config) + time.sleep(0.00016) def any(self): - """This function checks if the nRF24L01 has received any data at all, and then reports the - next available payload's length (in bytes) -- if there is any. - - :returns: - - `int` of the size (in bytes) of an available RX payload (if any). - - ``0`` if there is no payload in the RX FIFO buffer. - """ - # 0x60 == R_RX_PL_WID command - return self._reg_read(0x60) # top-level payload length - - def recv(self): - """This function is used to retrieve the next available payload in the RX FIFO buffer, then - clears the `irq_dr` status flag. This function synonomous to `read_ack()`. - - :returns: A `bytearray` of the RX payload data or `None` if there is no payload - - - If the `dynamic_payloads` attribute is disabled, then the returned bytearray's length - is equal to the user defined `payload_length` attribute (which defaults to 32). - - If the `dynamic_payloads` attribute is enabled, then the returned bytearray's length - is equal to the payload's length - """ - if not self.irq_dr: + """This function checks if the nRF24L01 has received any data at all, + and then reports the next available payload's length (in bytes).""" + self.update() + if self.pipe is not None: + if self._features & 4: + return self._reg_read(0x60) + return self._pl_len[self.pipe] + return 0 + + def recv(self, length=None): + """This function is used to retrieve the next available payload in the + RX FIFO buffer, then clears the `irq_dr` status flag.""" + return_size = length if length is not None else self.any() + if not return_size: return None - # buffer size = current payload size - curr_pl_size = self.payload_length if not self.dynamic_payloads else self.any() - # get the data (0x61 = R_RX_PAYLOAD) - result = self._reg_read_bytes(0x61, curr_pl_size) - # clear only Data Ready IRQ flag for accurate RX FIFO read operations + result = self._reg_read_bytes(0x61, return_size) self.clear_status_flags(True, False, False) - # return all available bytes from payload return result - def send(self, buf, ask_no_ack=False, force_retry=0): - """This blocking function is used to transmit payload(s). - - :returns: - * `list` if a list or tuple of payloads was passed as the ``buf`` parameter. Each item - in the returned list will contain the returned status for each corresponding payload - in the list/tuple that was passed. The return statuses will be in one of the - following forms: - * `False` if transmission fails or reaches the timeout sentinal. The timeout condition - is very rare and could mean something/anything went wrong with either/both TX/RX - transceivers. The timeout sentinal for transmission is calculated using `table 18 in - the nRF24L01 specification sheet `_. - Transmission failure can only be returned if `arc` is greater than ``0``. - * `True` if transmission succeeds. - * `bytearray` or `None` when the `ack` attribute is `True`. Because the payload expects - a responding custom ACK payload, the response is returned (upon successful - transmission) as a - `bytearray` (or `None` if ACK payload is empty) - - :param bytearray,list,tuple buf: The payload to transmit. This bytearray must have a length - greater than 0 and less than 32, otherwise a `ValueError` exception is thrown. This can - also be a list or tuple of payloads (`bytearray`); in which case, all items in the - list/tuple are processed for consecutive transmissions. - - - If the `dynamic_payloads` attribute is disabled and this bytearray's length is less - than the `payload_length` attribute, then this bytearray is padded with zeros until - its length is equal to the `payload_length` attribute. - - If the `dynamic_payloads` attribute is disabled and this bytearray's length is - greater than `payload_length` attribute, then this bytearray's length is truncated to - equal the `payload_length` attribute. - :param bool ask_no_ack: Pass this parameter as `True` to tell the nRF24L01 not to wait for - an acknowledgment from the receiving nRF24L01. This parameter directly controls a - ``NO_ACK`` flag in the transmission's Packet Control Field (9 bits of information about - the payload). Therefore, it takes advantage of an nRF24L01 feature specific to - individual payloads, and its value is not saved anywhere. You do not need to specify - this for every payload if the `arc` attribute is disabled, however this parameter - will work despite the `arc` attribute's setting. - - .. note:: Each transmission is in the form of a packet. This packet contains sections - of data around and including the payload. `See Chapter 7.3 in the nRF24L01 - Specifications Sheet `_ for more - details. - :param int force_retry: The number of brute-force attempts to `resend()` a failed - transmission. Default is 0. This parameter has no affect on transmissions if `arc` is - ``0`` or ``ask_no_ack`` parameter is set to `True`. Each re-attempt still takes - advantage of `arc` & `ard` attributes. During multi-payload processing, this - parameter is meant to slow down CircuitPython devices just enough for the Raspberry - Pi to catch up (due to the Raspberry Pi's seemingly slower SPI speeds). See also - `resend()` as using this parameter carries the same implications documented there. - - .. tip:: It is highly recommended that `arc` attribute is enabled when sending - multiple payloads. Test results with the `arc` attribute disabled were very poor - (much < 50% received). This same advice applies to the ``ask_no_ack`` parameter (leave - it as `False` for multiple payloads). - .. warning:: The nRF24L01 will block usage of the TX FIFO buffer upon failed - transmissions. Failed transmission's payloads stay in TX FIFO buffer until the MCU - calls `flush_tx()` and `clear_status_flags()`. Therefore, this function will discard - failed transmissions' payloads when sending a list or tuple of payloads, so it can - continue to process through the list/tuple even if any payload fails to be - acknowledged. - """ - # ensure power down/standby-I for proper manipulation of PWR_UP & PRIM_RX bits in - # CONFIG register + def send(self, buf, ask_no_ack=False, force_retry=0, send_only=False): + """This blocking function is used to transmit payload(s).""" self.ce_pin.value = 0 - self.flush_tx() # be sure there is space in the TX FIFO - # using spec sheet calculations: - # timeout total = T_upload + 2 * stby2active + T_overAir + T_ack + T_irq + T_retry - # 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) - # T_irq (in seconds) = (0.0000082 if self.data_rate == 1 else 0.000006) - # T_retry (in microseconds)= (arc * ard) - need_ack = self._setup_retr & 0x0f and not ask_no_ack - packet_data = 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 - t_ack = (((packet_data + 32) * 8 + 9) / bitrate) if need_ack else 0 # assumes 32-byte ACK - stby2active = (1 + (need_ack)) * 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 + if isinstance(buf, (list, tuple)): result = [] - for i, b in enumerate(buf): # check invalid payloads first - # this way when we raise a ValueError exception we don't leave the nRF24L01 in an - # unknown frozen state. - if not b or len(b) > 32: - raise ValueError("buf (item {} in the list/tuple) must be a" - " buffer protocol object with a byte length of\nat least 1 " - "and no greater than 32".format(i)) - for i, b in enumerate(buf): - timeout = (((8 * (len(b) + packet_data)) + 9) / bitrate) + \ - stby2active + t_irq + t_retry + t_ack + \ - (len(b) * 64 / self._spi.baudrate) # t_upload - self.clear_status_flags(False) # clear TX related flags - self.write(b, ask_no_ack) # clears TX flags on entering - time.sleep(0.00001) - self.ce_pin.value = 0 - self._wait_for_result(timeout) # now get result - if self._setup_retr & 0x0f and self.irq_df: - # need to clear for continuing transmissions - result.append(self._attempt2resend(force_retry)) - else: # if auto_ack is disabled - if self.ack and self.irq_dr and not ask_no_ack: - result.append(self.recv()) # save ACK payload & clears RX flag - else: - result.append(self.irq_ds) # will always be True (in this case) + for b in buf: + result.append(self.send(b, ask_no_ack, force_retry, send_only)) return result - if not buf or len(buf) > 32: - raise ValueError("buf must be a buffer protocol object with a byte length of" - "\nat least 1 and no greater than 32") - # T_upload is done before timeout begins (after payload write action AKA upload) - timeout = (((8 * (len(buf) + packet_data)) + 9) / - bitrate) + stby2active + t_irq + t_retry + t_ack - self.write(buf, ask_no_ack) # init using non-blocking helper - time.sleep(0.00001) # ensure CE pulse is >= 10 µs - # if pulse is stopped here, the nRF24L01 only handles the top level payload in the FIFO. - # we could hold CE HIGH to continue processing through the rest of the TX FIFO bound for - # the address passed to open_tx_pipe() - self.ce_pin.value = 0 # go to Standby-I power mode (power attribute still True) - self._wait_for_result(timeout) - if self._setup_retr & 0x0f and self.irq_df: - # if auto-retransmit is on and last attempt failed - result = self._attempt2resend(force_retry) - else: # if auto_ack is disabled - if self.ack and self.irq_dr and not ask_no_ack: - result = self.recv() # save ACK payload & clears RX flag - else: - result = self.irq_ds # will always be True (in this case) - self.clear_status_flags(False) # only TX related IRQ flags + self.flush_tx() + if not send_only: + self.flush_rx() + self.write(buf, ask_no_ack) + time.sleep(0.00001) + self.ce_pin.value = 0 + while not self._status & 0x70: + self.update() + result = self.irq_ds + if self.irq_df: + for _ in range(force_retry): + result = self.resend(send_only) + if result is None or result: + break + if self._status & 0x60 == 0x60 and not send_only: + result = self.recv() + self.clear_status_flags(False) return result @property - def irq_dr(self): - """A `bool` that represents the "Data Ready" interrupted flag. (read-only) - - * `True` represents Data is in the RX FIFO buffer - * `False` represents anything depending on context (state/condition of FIFO buffers) -- - usually this means the flag's been reset. + def tx_full(self): + """An attribute to represent the nRF24L01's status flag signaling + that the TX FIFO buffer is full. (read-only)""" + return bool(self._status & 1) - Pass ``dataReady`` parameter as `True` to `clear_status_flags()` and reset this. As this is - a virtual representation of the interrupt event, this attribute will always be updated - despite what the actual IRQ pin is configured to do about this event. + @property + def pipe(self): + """The identifying number of the data pipe that received + the next available payload in the RX FIFO buffer. (read only)""" + result = (self._status & 0x0E) >> 1 + if result <= 5: + return result + return None - Calling this does not execute an SPI transaction. It only exposes that latest data - contained in the STATUS byte that's always returned from any other SPI transactions. Use - the `update()` function to manually refresh this data when needed. - """ + @property + def irq_dr(self): + """A `bool` that represents the "Data Ready" interrupted flag. + (read-only)""" return bool(self._status & 0x40) @property def irq_ds(self): - """A `bool` that represents the "Data Sent" interrupted flag. (read-only) - - * `True` represents a successful transmission - * `False` represents anything depending on context (state/condition of FIFO buffers) -- - usually this means the flag's been reset. - - Pass ``dataSent`` parameter as `True` to `clear_status_flags()` to reset this. As this is a - virtual representation of the interrupt event, this attribute will always be updated - despite what the actual IRQ pin is configured to do about this event. - - Calling this does not execute an SPI transaction. It only exposes that latest data - contained in the STATUS byte that's always returned from any other SPI transactions. Use - the `update()` function to manually refresh this data when needed. - """ + """A `bool` that represents the "Data Sent" interrupted flag. + (read-only)""" return bool(self._status & 0x20) @property def irq_df(self): - """A `bool` that represents the "Data Failed" interrupted flag. (read-only) - - * `True` signifies the nRF24L01 attemped all configured retries - * `False` represents anything depending on context (state/condition) -- usually this means - the flag's been reset. - - Pass ``dataFail`` parameter as `True` to `clear_status_flags()` to reset this. As this is a - virtual representation of the interrupt event, this attribute will always be updated - despite what the actual IRQ pin is configured to do about this event.see also the `arc` and - `ard` attributes. - - Calling this does not execute an SPI transaction. It only exposes that latest data - contained in the STATUS byte that's always returned from any other SPI transactions. Use - the `update()` function to manually refresh this data when needed. - """ + """A `bool` that represents the "Data Failed" interrupted flag. + (read-only)""" return bool(self._status & 0x10) def clear_status_flags(self, data_recv=True, data_sent=True, data_fail=True): - """This clears the interrupt flags in the status register. Internally, this is - automatically called by `send()`, `write()`, `recv()`, and when `listen` changes from - `False` to `True`. - - :param bool data_recv: specifies wheather to clear the "RX Data Ready" flag. - :param bool data_sent: specifies wheather to clear the "TX Data Sent" flag. - :param bool data_fail: specifies wheather to clear the "Max Re-transmit reached" flag. - - .. note:: Clearing the ``data_fail`` flag is necessary for continued transmissions from the - nRF24L01 (locks the TX FIFO buffer when `irq_df` is `True`) despite wheather or not the - MCU is taking advantage of the interrupt (IRQ) pin. Call this function only when there - is an antiquated status flag (after you've dealt with the specific payload related to - the staus flags that were set), otherwise it can cause payloads to be ignored and - occupy the RX/TX FIFO buffers. See `Appendix A of the nRF24L01+ Specifications Sheet - `_ for an outline of - proper behavior. - """ - # 0x07 = STATUS register; only bits 6 through 4 are write-able - self._reg_write(0x07, (data_recv << 6) | ( - data_sent << 5) | (data_fail << 4)) + """This clears the interrupt flags in the status register.""" + config = bool(data_recv) << 6 | bool(data_sent) << 5 | bool(data_fail) << 4 + self._reg_write(7, config) def interrupt_config(self, data_recv=True, data_sent=True, data_fail=True): - """Sets the configuration of the nRF24L01's IRQ (interrupt) pin. The signal from the - nRF24L01's IRQ pin is active LOW. (write-only) - - :param bool data_recv: If this is `True`, then IRQ pin goes active when there is new data - to read in the RX FIFO buffer. - :param bool data_sent: If this is `True`, then IRQ pin goes active when a payload from TX - buffer is successfully transmit. - :param bool data_fail: If this is `True`, then IRQ pin goes active when maximum number of - attempts to re-transmit the packet have been reached. If `auto_ack` attribute is - disabled, then this IRQ event is not used. - - .. note:: To fetch the status (not configuration) of these IRQ flags, use the `irq_df`, - `irq_ds`, `irq_dr` attributes respectively. - - .. tip:: Paraphrased from nRF24L01+ Specification Sheet: - - The procedure for handling ``data_recv`` IRQ should be: - - 1. read payload through `recv()` - 2. clear ``dataReady`` status flag (taken care of by using `recv()` in previous step) - 3. read FIFO_STATUS register to check if there are more payloads available in RX FIFO - buffer. (a call to `pipe()`, `any()` or even ``(False,True)`` as parameters to - `fifo()` will get this result) - 4. if there is more data in RX FIFO, repeat from step 1 - """ - self._config = self._reg_read(CONFIG) # refresh data - # save to register and update local copy of pwr & RX/TX modes' flags - self._config = (self._config & 0x0F) | (not data_fail << 4) | (not data_sent << 5) | \ - (not data_recv << 6) - self._reg_write(CONFIG, self._config) + """Sets the configuration of the nRF24L01's IRQ (interrupt) pin.""" + self._config = (self._reg_read(CONFIGURE) & 0x0F) | (not data_recv) << 6 + self._config |= (not data_fail) << 4 | (not data_sent) << 5 + self._reg_write(CONFIGURE, self._config) def what_happened(self, dump_pipes=False): - """This debuggung function aggregates and outputs all status/condition related information - from the nRF24L01. Some information may be irrelevant depending on nRF24L01's - state/condition. - - :prints: - - - ``Channel`` The current setting of the `channel` attribute - - ``RF Data Rate`` The current setting of the RF `data_rate` attribute. - - ``RF Power Amplifier`` The current setting of the `pa_level` attribute. - - ``CRC bytes`` The current setting of the `crc` attribute - - ``Address length`` The current setting of the `address_length` attribute - - ``Payload lengths`` The current setting of the `payload_length` attribute - - ``Auto retry delay`` The current setting of the `ard` attribute - - ``Auto retry attempts`` The current setting of the `arc` attribute - - ``Packets lost on current channel`` Total amount of packets lost (transmission - failures). This only resets when the `channel` is changed. This count will - only go up 15. - - ``Retry attempts made for last transmission`` Amount of attempts to re-transmit - during last - transmission (resets per payload) - - ``IRQ - Data Ready`` The current setting of the IRQ pin on "Data Ready" event - - ``IRQ - Data Sent`` The current setting of the IRQ pin on "Data Sent" event - - ``IRQ - Data Fail`` The current setting of the IRQ pin on "Data Fail" event - - ``Data Ready`` Is there RX data ready to be read? - (state of the `irq_dr` flag) - - ``Data Sent`` Has the TX data been sent? (state of the `irq_ds` flag) - - ``Data Failed`` Has the maximum attempts to re-transmit been reached? - (state of the `irq_df` flag) - - ``TX FIFO full`` Is the TX FIFO buffer full? (state of the `tx_full` flag) - - ``TX FIFO empty`` Is the TX FIFO buffer empty? - - ``RX FIFO full`` Is the RX FIFO buffer full? - - ``RX FIFO empty`` Is the RX FIFO buffer empty? - - ``Custom ACK payload`` Is the nRF24L01 setup to use an extra (user defined) payload - attached to the acknowledgment packet? (state of the `ack` attribute) - - ``Ask no ACK`` Is the nRF24L01 setup to transmit individual packets that don't - require acknowledgment? - - ``Automatic Acknowledgment`` Is the `auto_ack` attribute enabled? - - ``Dynamic Payloads`` Is the `dynamic_payloads` attribute enabled? - - ``Primary Mode`` The current mode (RX or TX) of communication of the nRF24L01 device. - - ``Power Mode`` The power state can be Off, Standby-I, Standby-II, or On. - - :param bool dump_pipes: `True` appends the output and prints: - - * the current address used for TX transmissions - * ``Pipe [#] ([open/closed]) bound: [address]`` where ``#`` represent the pipe number, - the ``open/closed`` status is relative to the pipe's RX status, and ``address`` is - read directly from the nRF24L01 registers. - * if the pipe is open, then the output also prints ``expecting [X] byte static - payloads`` where ``X`` is the `payload_length` (in bytes) the pipe is setup to - receive when `dynamic_payloads` is disabled. - - Default is `False` and skips this extra information. - """ - watchdog = self._reg_read(8) # 8 == OBSERVE_TX register - print("Channel___________________{} ~ {} GHz".format( - self.channel, (self.channel + 2400) / 1000)) - print("RF Data Rate______________{} {}".format( - self.data_rate, "Mbps" if self.data_rate != 250 else "Kbps")) + """This debuggung function aggregates and outputs all status/condition + related information from the nRF24L01.""" + observer = self._reg_read(8) + print("Is a plus variant_________{}".format(repr(self.is_plus_variant))) + 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( + "RF Low Noise Amplifier____{}".format( + "Enabled" if self.is_lna_enabled else "Disabled" + ) + ) print("CRC bytes_________________{}".format(self.crc)) print("Address length____________{} bytes".format(self.address_length)) - print("Payload lengths___________{} bytes".format(self.payload_length)) + print("TX Payload lengths________{} bytes".format(self.payload_length[0])) print("Auto retry delay__________{} microseconds".format(self.ard)) print("Auto retry attempts_______{} maximum".format(self.arc)) - print("Packets lost on current channel_____________________{}".format( - (watchdog & 0xF0) >> 4)) - print("Retry attempts made for last transmission___________{}".format(watchdog & 0x0F)) - print("IRQ - Data Ready______{} Data Ready___________{}".format( - '_True' if not bool(self._config & 0x40) else 'False', self.irq_dr)) - print("IRQ - Data Fail_______{} Data Failed__________{}".format( - '_True' if not bool(self._config & 0x20) else 'False', self.irq_df)) - print("IRQ - Data Sent_______{} Data Sent____________{}".format( - '_True' if not bool(self._config & 0x10) else 'False', self.irq_ds)) - print("TX FIFO full__________{} TX FIFO empty________{}".format( - '_True' if bool(self.tx_full) else 'False', bool(self.fifo(True, True)))) - print("RX FIFO full__________{} RX FIFO empty________{}".format( - '_True' if bool(self._fifo & 2) else 'False', bool(self._fifo & 1))) - print("Ask no ACK_________{} Custom ACK Payload___{}".format( - '_Allowed' if bool(self._features & 1) else 'Disabled', - 'Enabled' if self.ack else 'Disabled')) - print("Dynamic Payloads___{} Auto Acknowledgment__{}".format( - '_Enabled' if self.dynamic_payloads else 'Disabled', - 'Enabled' if self.auto_ack else 'Disabled')) - print("Primary Mode_____________{} Power Mode___________{}".format( - 'RX' if self.listen else 'TX', - ('Standby-II' if self.ce_pin.value else 'Standby-I') if self._config & 2 else 'Off')) + print( + "Packets lost on current channel_____________________{}".format( + (observer & 0xF0) >> 4 + ) + ) + print( + "Retry attempts made for last transmission___________{}".format( + observer & 0x0F + ) + ) + print( + "IRQ - Data Ready______{} Data Ready___________{}".format( + "_True" if not bool(self._config & 0x40) else "False", self.irq_dr + ) + ) + print( + "IRQ - Data Fail_______{} Data Failed__________{}".format( + "_True" if not bool(self._config & 0x10) else "False", self.irq_df + ) + ) + print( + "IRQ - Data Sent_______{} Data Sent____________{}".format( + "_True" if not bool(self._config & 0x20) else "False", self.irq_ds + ) + ) + print( + "TX FIFO full__________{} TX FIFO empty________{}".format( + "_True" if bool(self.tx_full) else "False", bool(self.fifo(True, True)) + ) + ) + print( + "RX FIFO full__________{} RX FIFO empty________{}".format( + "_True" if bool(self._fifo & 2) else "False", bool(self._fifo & 1) + ) + ) + print( + "Ask no ACK_________{} Custom ACK Payload___{}".format( + "_Allowed" if bool(self._features & 1) else "Disabled", + "Enabled" if self.ack else "Disabled", + ) + ) + print( + "Dynamic Payloads___{} Auto Acknowledgment__{}".format( + "_Enabled" + if self._dyn_pl == 0x3F + else ( + bin(self._dyn_pl).replace( + "0b", "0b" + "0" * (8 - len(bin(self._dyn_pl))) + ) + if self._dyn_pl + else "Disabled" + ), + "Enabled" + if self._aa == 0x3F + else ( + bin(self._aa).replace("0b", "0b" + "0" * (8 - len(bin(self._aa)))) + if self._aa + else "Disabled" + ), + ) + ) + print( + "Primary Mode_____________{} Power Mode___________{}".format( + "RX" if self.listen else "TX", + ("Standby-II" if self.ce_pin.value else "Standby-I") + if self._config & 2 + else "Off", + ) + ) if dump_pipes: - print('TX address____________', self._tx_address) - for i, address in enumerate(self._pipes): - is_open = "( open )" if self._open_pipes & (1 << i) else "(closed)" - if i <= 1: # print full address - print("Pipe", i, is_open, "bound:", address) - else: # print unique byte + shared bytes = actual address used by radio - print("Pipe", i, is_open, "bound:", - bytes([self._pipes[i]]) + self._pipes[1][1:]) - if self._open_pipes & (1 << i): - print('\t\texpecting', self._payload_widths[i], 'byte static payloads') + print("TX address____________", self.address()) + self._open_pipes = self._reg_read(OPEN_PIPES) + for i in range(6): + is_open = self._open_pipes & (1 << i) + print( + "Pipe", + i, + "( open )" if is_open else "(closed)", + "bound:", + self.address(i), + ) + if is_open: + print("\t\texpecting", self._pl_len[i], "byte static payloads") @property - def dynamic_payloads(self): - """This `bool` attribute controls the nRF24L01's dynamic payload length feature. + def is_plus_variant(self): + """A `bool` attribute to descibe if the nRF24L01 is a plus variant or + not (read-only).""" + return self._is_plus_variant - - `True` enables nRF24L01's dynamic payload length feature. The `payload_length` - attribute is ignored when this feature is enabled. - - `False` disables nRF24L01's dynamic payload length feature. Be sure to adjust - the `payload_length` attribute accordingly when `dynamic_payloads` feature is disabled. - """ - return bool(self._dyn_pl and (self._features & 4)) + @property + def dynamic_payloads(self): + """This `bool` attribute controls the nRF24L01's dynamic payload + length feature for each pipe.""" + self._dyn_pl = self._reg_read(DYN_PL_LEN) + self._features = self._reg_read(TX_FEATURE) + return bool(self._dyn_pl) and self._features & 4 == 4 @dynamic_payloads.setter def dynamic_payloads(self, enable): - assert isinstance(enable, (bool, int)) - self._features = self._reg_read(FEATURE) # refresh data - # save changes to registers(& their shadows) - if self._features & 4 != enable: # if not already - # throw a specific global flag for enabling dynamic payloads - self._features = (self._features & 3) | (enable << 2) - self._reg_write(FEATURE, self._features) - # 0x3F == all pipes have enabled dynamic payloads - self._dyn_pl = 0x3F if enable else 0 - self._reg_write(DYNPD, self._dyn_pl) + self._features = self._reg_read(TX_FEATURE) + if isinstance(enable, (bool, int)): + self._dyn_pl = 0x3F if enable else 0 + elif isinstance(enable, (list, tuple)): + for i, val in enumerate(enable): + if i < 6 and val >= 0: # skip pipe if val is negative + self._dyn_pl = (self._dyn_pl & ~(1 << i)) | (bool(val) << i) + else: + raise ValueError("dynamic_payloads: {} is an invalid input" % enable) + if bool(self._features & 4) != (self._dyn_pl & 1): + self._features = (self._features & 3) | ((self._dyn_pl & 1) << 2) + self._reg_write(TX_FEATURE, self._features) + if self._dyn_pl != (self._aa & self._dyn_pl): + self._aa |= self._dyn_pl + self._reg_write(AUTO_ACK, self._aa) + self._config = self._reg_read(CONFIGURE) + self._reg_write(DYN_PL_LEN, self._dyn_pl) @property def payload_length(self): - """This `int` attribute specifies the length (in bytes) of payload that is regarded, - meaning "how big of a payload should the radio care about?" If the `dynamic_payloads` - attribute is enabled, this attribute has no affect. When `dynamic_payloads` is disabled, - this attribute is used to specify the payload length when entering RX mode. - - A valid input value must be an `int` in range [1,32]. Otherwise a `ValueError` exception is - thrown. Default is set to the nRF24L01's maximum of 32. - - .. note:: When `dynamic_payloads` is disabled during transmissions: - - - Payloads' size of greater than this attribute's value will be truncated to match. - - Payloads' size of less than this attribute's value will be padded with zeros to - match. - """ - return self._payload_length + """This `int` attribute specifies the length (in bytes) of static payloads for each + pipe.""" + return self._pl_len @payload_length.setter def payload_length(self, length): - # max payload size is 32 bytes - if not length or length <= 32: - # save for access via getter property - self._payload_length = length - else: - raise ValueError( - "{}: payload length can only be set in range [1,32] bytes".format(length)) + if isinstance(length, int): + length = [length] * 6 + elif not isinstance(length, (list, tuple)): + raise ValueError("length {} is not a valid input".format(length)) + for i, val in enumerate(length): + if i < 6: + if 0 < val <= 32: # don't throw exception, just skip pipe + self._pl_len[i] = val + self._reg_write(RX_PL_LENG + i, val) @property def arc(self): - """"This `int` attribute specifies the nRF24L01's number of attempts to re-transmit TX - payload when acknowledgment packet is not received. The `auto_ack` must be enabled on the - receiving nRF24L01, otherwise this attribute will make `send()` seem like it failed. - - A valid input value must be in range [0,15]. Otherwise a `ValueError` exception is thrown. - Default is set to 3. A value of ``0`` disables the automatic re-transmit feature and - considers all payload transmissions a success. + """This `int` attribute specifies the nRF24L01's number of attempts + to re-transmit TX payload when acknowledgment packet is not received. """ - self._setup_retr = self._reg_read(SETUP_RETR) # refresh data - return self._setup_retr & 0x0f + self._retry_setup = self._reg_read(SETUP_RETR) + return self._retry_setup & 0x0F @arc.setter def arc(self, count): - if 0 <= count <= 15: - if self.arc & 0x0F != count: # write only if needed - # save changes to register(& its shadow) - self._setup_retr = (self._setup_retr & 0xF0) | count - self._reg_write(SETUP_RETR, self._setup_retr) - else: - raise ValueError( - "automatic re-transmit count(/attempts) must in range [0,15]") + if not 0 <= count <= 15: + raise ValueError("automatic re-transmit count must in range [0, 15]") + self._retry_setup = (self._retry_setup & 0xF0) | count + self._reg_write(SETUP_RETR, self._retry_setup) @property def ard(self): - """This `int` attribute specifies the nRF24L01's delay (in µs) between attempts to - automatically re-transmit the TX payload when an expected acknowledgement (ACK) packet is - not received. During this time, the nRF24L01 is listening for the ACK packet. If the - `auto_ack` attribute is disabled, this attribute is not applied. - - A valid input value must be a multiple of 250 in range [250,4000]. Otherwise a `ValueError` - exception is thrown. Default is 1500 for reliability. - - .. note:: Paraphrased from nRF24L01 specifications sheet: - - Please take care when setting this parameter. If the custom ACK payload is more than 15 - bytes in 2 Mbps data rate, the `ard` must be 500µS or more. If the custom ACK payload - is more than 5 bytes in 1 Mbps data rate, the `ard` must be 500µS or more. In 250kbps - data rate (even when there is no custom ACK payload) the `ard` must be 500µS or more. - - See `data_rate` attribute on how to set the data rate of the nRF24L01's transmissions. - """ - self._setup_retr = self._reg_read(SETUP_RETR) # refresh data - return ((self._setup_retr & 0xf0) >> 4) * 250 + 250 + """This `int` attribute specifies the nRF24L01's delay (in + microseconds) between attempts to automatically re-transmit the + TX payload when an expected acknowledgement (ACK) packet is not + received.""" + self._retry_setup = self._reg_read(SETUP_RETR) + return ((self._retry_setup & 0xF0) >> 4) * 250 + 250 @ard.setter - def ard(self, delta_t): - if 250 <= delta_t <= 4000 and delta_t % 250 == 0: - # set new ARD data and current ARC data to register - if self.ard != delta_t: # write only if needed - # save changes to register(& its Shadow) - self._setup_retr = (int((delta_t - 250) / 250) - << 4) | (self._setup_retr & 0x0F) - self._reg_write(SETUP_RETR, self._setup_retr) - else: - raise ValueError("automatic re-transmit delay can only be a multiple of 250 in range " - "[250,4000]") + def ard(self, delta): + if not 250 <= delta <= 4000: + raise ValueError("automatic re-transmit delay must be in range [250, 4000]") + self._retry_setup = (self._retry_setup & 15) | int((delta - 250) / 250) << 4 + self._reg_write(SETUP_RETR, self._retry_setup) @property def auto_ack(self): - """This `bool` attribute controls the nRF24L01's automatic acknowledgment feature during - the process of receiving a packet. - - - `True` enables transmitting automatic acknowledgment packets. The CRC (cyclic redundancy - checking) is enabled automatically by the nRF24L01 if the `auto_ack` attribute is enabled - (see also `crc` attribute). - - `False` disables transmitting automatic acknowledgment packets. The `crc` attribute will - remain unaffected when disabling the `auto_ack` attribute. - """ - return self._aa + """This `bool` attribute controls the nRF24L01's automatic + acknowledgment feature during the process of receiving a packet.""" + self._aa = self._reg_read(AUTO_ACK) + return bool(self._aa) @auto_ack.setter def auto_ack(self, enable): - assert isinstance(enable, (bool, int)) - # the following 0x3F == enabled auto_ack on all pipes - self._aa = 0x3F if enable else 0 - self._reg_write(EN_AA, self._aa) # 1 == EN_AA register for ACK feature - # nRF24L01 automatically enables CRC if ACK packets are enabled in the FEATURE register + if isinstance(enable, (bool, int)): + self._aa = 0x3F if enable else 0 + elif isinstance(enable, (list, tuple)): + for i, val in enumerate(enable): + if i < 6 and val >= 0: # skip pipe if val is negative + self._aa = (self._aa & ~(1 << i)) | (bool(val) << i) + else: + raise ValueError("auto_ack: {} is not a valid input" % enable) + self._reg_write(AUTO_ACK, self._aa) + if self._aa: # refresh crc data if enabled + self._config = self._reg_read(CONFIGURE) @property def ack(self): - """This `bool` attribute represents the status of the nRF24L01's capability to use custom - payloads as part of the automatic acknowledgment (ACK) packet. Use this attribute to - set/check if the custom ACK payloads feature is enabled. - - - `True` enables the use of custom ACK payloads in the ACK packet when responding to - receiving transmissions. As `dynamic_payloads` and `auto_ack` attributes are required for - this feature to work, they are automatically enabled as needed. - - `False` disables the use of custom ACK payloads. Disabling this feature does not disable - the `auto_ack` and `dynamic_payloads` attributes (they work just fine without this - feature). - """ - return bool((self._features & 2) and self.auto_ack and self.dynamic_payloads) + """This `bool` attribute represents the status of the nRF24L01's + capability to use custom payloads as part of the automatic + acknowledgment (ACK) packet.""" + self._aa = self._reg_read(AUTO_ACK) + self._dyn_pl = self._reg_read(DYN_PL_LEN) + self._features = self._reg_read(TX_FEATURE) + return bool((self._features & 6) == 6 and ((self._aa & self._dyn_pl) & 1)) @ack.setter def ack(self, enable): - assert isinstance(enable, (bool, int)) - # we need to throw the EN_ACK_PAY flag in the FEATURES register accordingly on both - # TX & RX nRF24L01s - if self.ack != enable: # if enabling - self.auto_ack = True # ensure auto_ack feature is enabled - # dynamic_payloads required for custom ACK payloads - self._dyn_pl = 0x3F - self._reg_write(DYNPD, self._dyn_pl) - else: - # setting auto_ack feature automatically updated the _features attribute, so - self._features = self._reg_read(FEATURE) # refresh data here - self._features = (self._features & 5) | (6 if enable else 0) - self._reg_write(FEATURE, self._features) + if self.ack != bool(enable): + self.auto_ack = (1,) + self._dyn_pl = self._dyn_pl & ~1 | 1 + self._reg_write(DYN_PL_LEN, self._dyn_pl) + self._features = self._features & 3 | 4 + self._features = self._features & 5 | bool(enable) << 1 + self._reg_write(TX_FEATURE, self._features) def load_ack(self, buf, pipe_number): - """This allows the MCU to specify a payload to be allocated into the TX FIFO buffer for use - on a specific data pipe. This payload will then be appended to the automatic acknowledgment - (ACK) packet that is sent when fresh data is received on the specified pipe. See - `read_ack()` on how to fetch a received custom ACK payloads. - - :param bytearray buf: This will be the data attached to an automatic ACK packet on the - incoming transmission about the specified ``pipe_number`` parameter. This must have a - length in range [1,32] bytes, otherwise a `ValueError` exception is thrown. Any ACK - payloads will remain in the TX FIFO buffer until transmitted successfully or - `flush_tx()` is called. - :param int pipe_number: This will be the pipe number to use for deciding which - transmissions get a response with the specified ``buf`` parameter's data. This number - must be in range [0,5], otherwise a `ValueError` exception is thrown. - - :returns: `True` if payload was successfully loaded onto the TX FIFO buffer. `False` if it - wasn't because TX FIFO buffer is full. - - .. note:: this function takes advantage of a special feature on the nRF24L01 and needs to - be called for every time a customized ACK payload is to be used (not for every - automatic ACK packet -- this just appends a payload to the ACK packet). The `ack`, - `auto_ack`, and `dynamic_payloads` attributes are also automatically enabled by this - function when necessary. - - .. tip:: The ACK payload must be set prior to receiving a transmission. It is also worth - noting that the nRF24L01 can hold up to 3 ACK payloads pending transmission. Using this - function does not over-write existing ACK payloads pending; it only adds to the queue - (TX FIFO buffer) if it can. Use `flush_tx()` to discard unused ACK payloads when done - listening. - """ + """This allows the MCU to specify a payload to be allocated into the + TX FIFO buffer for use on a specific data pipe.""" if pipe_number < 0 or pipe_number > 5: - raise ValueError("pipe number must be in range [0,5]") + raise ValueError("pipe_number must be in range [0, 5]") if not buf or len(buf) > 32: - raise ValueError("buf must be a buffer protocol object with a byte length of" - "\nat least 1 and no greater than 32") - # only prepare payload if the auto_ack attribute is enabled and ack[0] is not None - if not self.ack: + raise ValueError("payload must have a byte length in range [1, 32]") + if not bool((self._features & 6) == 6 and ((self._aa & self._dyn_pl) & 1)): self.ack = True if not self.tx_full: - # 0xA8 = W_ACK_PAYLOAD self._reg_write_bytes(0xA8 | pipe_number, buf) - return True # payload was loaded - return False # payload wasn't loaded + return True + return False def read_ack(self): - """Allows user to read the automatic acknowledgement (ACK) payload (if any) when nRF24L01 - is in TX mode. This function is called from a blocking `send()` call if the `ack` attribute - is enabled. Alternatively, this function can be called directly in case of calling the - non-blocking `write()` function during asychronous applications. This function is an alias - of `recv()` and remains for bakward compatibility with older versions of this library. - - .. note:: See also the `ack`, `dynamic_payloads`, and `auto_ack` attributes as they must be - enabled to use custom ACK payloads. - """ + """Allows user to read the automatic acknowledgement (ACK) payload (if any).""" return self.recv() @property def data_rate(self): - """This `int` attribute specifies the nRF24L01's frequency data rate for OTA (over the air) - transmissions. - - A valid input value is: - - - ``1`` sets the frequency data rate to 1 Mbps - - ``2`` sets the frequency data rate to 2 Mbps - - ``250`` sets the frequency data rate to 250 Kbps - - Any invalid input throws a `ValueError` exception. Default is 1 Mbps. - - .. warning:: 250 Kbps is be buggy on the non-plus models of the nRF24L01 product line. If - you use 250 Kbps data rate, and some transmissions report failed by the transmitting - nRF24L01, even though the same packet in question actually reports received by the - receiving nRF24L01, then try a higher data rate. CAUTION: Higher data rates mean less - maximum distance between nRF24L01 transceivers (and vise versa). - """ - self._rf_setup = self._reg_read(RF_SETUP) # refresh data - return (2 if self._rf_setup & 0x28 == 8 else 250) if self._rf_setup & 0x28 else 1 + """This `int` attribute specifies the nRF24L01's frequency data rate + for OTA (over the air) transmissions.""" + self._rf_setup = self._reg_read(RF_PA_RATE) + rf_setup = self._rf_setup & 0x28 + return (2 if rf_setup == 8 else 250) if rf_setup else 1 @data_rate.setter def data_rate(self, speed): - # nRF24L01+ must be in a standby or power down mode before writing to the configuration - # registers. - if speed in (1, 2, 250): - if self.data_rate != speed: - speed = 0 if speed == 1 else (8 if speed == 2 else 0x20) - # save changes to register(& its shadow) + if not speed in (1, 2, 250): + raise ValueError("data_rate must be 1 (Mbps), 2 (Mbps), or 250 (kbps)") + if self.is_plus_variant and speed == 250: + raise NotImplementedError( + "250 kbps data rate is not available for the non-plus " + "variants of the nRF24L01 transceivers." + ) + if self.data_rate != speed: + speed = 0 if speed == 1 else (0x20 if speed != 2 else 8) self._rf_setup = self._rf_setup & 0xD7 | speed - self._reg_write(RF_SETUP, self._rf_setup) - else: - raise ValueError( - "data rate must be one of the following ([M,M,K]bps): 1, 2, 250") + self._reg_write(RF_PA_RATE, self._rf_setup) @property def channel(self): - """This `int` attribute specifies the nRF24L01's frequency (in 2400 + `channel` MHz). - - A valid input value must be in range [0, 125] (that means [2.4, 2.525] GHz). Otherwise a - `ValueError` exception is thrown. Default is 76. - """ - return self._reg_read(RF_CH) + """This `int` attribute specifies the nRF24L01's frequency.""" + return self._reg_read(5) @channel.setter def channel(self, channel): - if 0 <= channel <= 125: - self._channel = channel - self._reg_write(RF_CH, channel) # always writes to reg - else: - raise ValueError("channel acn only be set in range [0,125]") + if not 0 <= int(channel) <= 125: + raise ValueError("channel can only be set in range [0, 125]") + self._channel = int(channel) + self._reg_write(5, self._channel) @property def crc(self): - """This `int` attribute specifies the nRF24L01's CRC (cyclic redundancy checking) encoding - scheme in terms of byte length. CRC is a way of making sure that the transmission didn't - get corrupted over the air. - - A valid input value is in range [0,2]: - - - ``0`` disables CRC (no anti-corruption of data) - - ``1`` enables CRC encoding scheme using 1 byte (weak anti-corruption of data) - - ``2`` enables CRC encoding scheme using 2 bytes (better anti-corruption of data) - - Any invalid input throws a `ValueError` exception. Default is enabled using 2 bytes. - - .. note:: The nRF24L01 automatically enables CRC if automatic acknowledgment feature is - enabled (see `auto_ack` attribute). - """ - self._config = self._reg_read(CONFIG) # refresh data - return max(0, ((self._config & 12) >> 2) - 1) # this works + """This `int` attribute specifies the nRF24L01's CRC (cyclic + redundancy checking) encoding scheme in terms of byte length.""" + self._config = self._reg_read(CONFIGURE) + return max(0, ((self._config & 0x0C) >> 2) - 1) @crc.setter def crc(self, length): - if 0 <= length <= 2: - if self.crc != length: - length = (length + 1) << 2 if length else 0 # this works - # save changes to register(& its Shadow) - self._config = self._config & 0x73 | length - self._reg_write(0, self._config) - else: - raise ValueError( - "CRC byte length must be an int equal to 0 (off), 1, or 2") + if not 0 <= length <= 2: + raise ValueError("CRC byte length must be an int equal to 0 (off), 1, or 2") + if self.crc != length: + length = (length + 1) << 2 if length else 0 + self._config = self._config & 0x73 | length + self._reg_write(0, self._config) @property def power(self): - """This `bool` attribute controls the power state of the nRF24L01. This is exposed for - asynchronous applications and user preference. - - - `False` basically puts the nRF24L01 to sleep (AKA power down mode) with ultra-low - current consumption. No transmissions are executed when sleeping, but the nRF24L01 can - still be accessed through SPI. Upon instantiation, this driver class puts the nRF24L01 - to sleep until the MCU invokes RX/TX transmissions. This driver class doesn't power down - the nRF24L01 after RX/TX transmissions are complete (avoiding the required power up/down - 130 µs wait time), that preference is left to the user. - - `True` powers up the nRF24L01. This is the first step towards entering RX/TX modes (see - also `listen` attribute). Powering up is automatically handled by the `listen` attribute - as well as the `send()` and `write()` functions. - - .. note:: This attribute needs to be `True` if you want to put radio on Standby-II (highest - current consumption) or Standby-I (moderate current consumption) modes. TX - transmissions are only executed during Standby-II by calling `send()` or `write()`. RX - transmissions are received during Standby-II by setting `listen` attribute to `True` - (see `Chapter 6.1.2-7 of the nRF24L01+ Specifications Sheet `_). After using `send()` or setting `listen` to `False`, the nRF24L01 - is left in Standby-I mode (see also notes on the `write()` function). - """ + """This `bool` attribute controls the power state of the nRF24L01.""" + self._config = self._reg_read(CONFIGURE) return bool(self._config & 2) @power.setter def power(self, is_on): - assert isinstance(is_on, (bool, int)) - # capture surrounding flags and set PWR_UP flag according to is_on boolean - self._config = self._reg_read(CONFIG) # refresh data - if self.power != is_on: - # only write changes - self._config = (self._config & 0x7d) | ( - is_on << 1) # doesn't affect TX?RX mode - self._reg_write(CONFIG, self._config) - # power up/down takes < 150 µs + 4 µs + self._config = self._reg_read(CONFIGURE) + if self.power != bool(is_on): + self._config = self._config & 0x7D | bool(is_on) << 1 + self._reg_write(CONFIGURE, self._config) time.sleep(0.00016) @property def pa_level(self): - """This `int` attribute specifies the nRF24L01's power amplifier level (in dBm). Higher - levels mean the transmission will cover a longer distance. Use this attribute to tweak the - nRF24L01 current consumption on projects that don't span large areas. - - A valid input value is: - - - ``-18`` sets the nRF24L01's power amplifier to -18 dBm (lowest) - - ``-12`` sets the nRF24L01's power amplifier to -12 dBm - - ``-6`` sets the nRF24L01's power amplifier to -6 dBm - - ``0`` sets the nRF24L01's power amplifier to 0 dBm (highest) - - Any invalid input throws a `ValueError` exception. Default is 0 dBm. - """ - self._rf_setup = self._reg_read(RF_SETUP) # refresh data - return (3 - ((self._rf_setup & RF_SETUP) >> 1)) * -6 + """This `int` attribute specifies the nRF24L01's power amplifier level (in dBm).""" + self._rf_setup = self._reg_read(RF_PA_RATE) + return (3 - ((self._rf_setup & 6) >> 1)) * -6 @pa_level.setter def pa_level(self, power): - # nRF24L01+ must be in a standby or power down mode before writing to the - # configuration registers. - if power in (-18, -12, -6, 0): - power = (3 - int(power / -6)) * 2 # this works - # save changes to register (& its shadow) - self._rf_setup = (self._rf_setup & 0xF9) | power - self._reg_write(RF_SETUP, self._rf_setup) - else: - raise ValueError( - "power amplitude must be one of the following (dBm): -18, -12, -6, 0") - - @property - def rpd(self): - """This read-only attribute returns `True` if RPD (Received Power Detector) is triggered - or `False` if not triggered. - - .. note:: The RPD flag is triggered in the following cases: - - 1. During RX mode (`listen` = `True`) and a RF transmission with a gain above a preset - (non-adjustable) -64 dBm threshold. - 2. When a packet is received (indicative of the nRF24L01 used to detect/"listen" for - incoming packets). - 3. When the nRF24L01's CE pin goes from HIGH to LOW (or when the `listen` attribute - changes from `True` to `False`). - 4. When the underlying ESB (Enhanced ShockBurst) protocol reaches a hardcoded - (non-adjustable) RX timeout. - - """ - return bool(self._reg_read(0x09)) + lna_bit = True + if isinstance(power, (list, tuple)) and len(power) > 1: + lna_bit, power = bool(power[1]), int(power[0]) + if power not in (-18, -12, -6, 0): + raise ValueError("pa_level must be -18, -12, -6, or 0 (in dBm)") + power = (3 - int(power / -6)) * 2 + self._rf_setup = (self._rf_setup & 0xF8) | power | lna_bit + self._reg_write(RF_PA_RATE, self._rf_setup) @property - def tx_full(self): - """An attribute to represent the nRF24L01's status flag signaling that the TX FIFO buffer - is full. (read-only) - - Calling this does not execute an SPI transaction. It only exposes that latest data - contained in the STATUS byte that's always returned from any SPI transactions with the - nRF24L01. Use the `update()` function to manually refresh this data when needed. - - :returns: - * `True` for TX FIFO buffer is full - * `False` for TX FIFO buffer is not full. This doesn't mean the TX FIFO buffer is - empty. - """ - return bool(self._status & 1) + def is_lna_enabled(self): + """A read-only `bool` attribute about the LNA (Low Noise Amplifier) gain + feature used in the nRF24L01-PA/LNA modules.""" + self._rf_setup = self._reg_read(RF_PA_RATE) + return bool(self._rf_setup & 1) def update(self): - """This function is only used to get an updated status byte over SPI from the nRF24L01 and - is exposed to the MCU for asynchronous applications. Refreshing the status byte is vital to - checking status of the interrupts, RX pipe number related to current RX payload, and if the - TX FIFO buffer is full. This function returns nothing, but internally updates the `irq_dr`, - `irq_ds`, `irq_df`, and `tx_full` attributes. Internally this is a helper function to - `pipe()`, `send()`, and `resend()` functions""" - # perform non-operation to get status byte - # should be faster than reading the STATUS register + """This function is only used to get an updated status byte over SPI + from the nRF24L01.""" self._reg_write(0xFF) - def resend(self): + def resend(self, send_only=False): """Use this function to maunally re-send the previous payload in the - top level (first out) of the TX FIFO buffer. All returned data follows the same patttern - that `send()` returns with the added condition that this function will return `False` - if the TX FIFO buffer is empty. - - .. note:: The nRF24L01 normally removes a payload from the TX FIFO buffer after successful - transmission, but not when this function is called. The payload (successfully - transmitted or not) will remain in the TX FIFO buffer until `flush_tx()` is called to - remove them. Alternatively, using this function also allows the failed payload to be - over-written by using `send()` or `write()` to load a new payload. - """ + top level (first out) of the TX FIFO buffer.""" result = False - if not self.fifo(True, True): # is there a pre-existing payload - self.clear_status_flags(False) # clear TX related flags - # indicate existing 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 - # timeout calc assumes 32 byte payload because there is no way to tell when payload - # has already been loaded into TX FIFO; also assemues 32-byte ACK if needed - pl_coef = 1 + bool(self._setup_retr & 0x0f) - 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 - # cycle the CE pin to re-enable transmission of re-used payload + if not self.fifo(True, True): self.ce_pin.value = 0 + if not send_only: + self.flush_rx() + self.clear_status_flags() + self._reg_write(0xE3) self.ce_pin.value = 1 time.sleep(0.00001) - self.ce_pin.value = 0 # only send one payload - self._wait_for_result(timeout) + self.ce_pin.value = 0 + while not self._status & 0x70: + self.update() result = self.irq_ds - if self.ack and self.irq_dr: # check if there is an ACK payload - result = self.recv() # save ACK payload & clear RX related IRQ flag - self.clear_status_flags(False) # only clear TX related IRQ flags + if self._status & 0x60 == 0x60 and not send_only: + result = self.recv() + self.clear_status_flags(False) return result - def write(self, buf, ask_no_ack=False): - """This non-blocking function (when used as alternative to `send()`) is meant for - asynchronous applications and can only handle one payload at a time as it is a helper - function to `send()`. - - :param bytearray buf: The payload to transmit. This bytearray must have a length greater - than 0 and less than 32 bytes, otherwise a `ValueError` exception is thrown. - - - If the `dynamic_payloads` attribute is disabled and this bytearray's length is less - than the `payload_length` attribute, then this bytearray is padded with zeros until - its length is equal to the `payload_length` attribute. - - If the `dynamic_payloads` attribute is disabled and this bytearray's length is - greater than `payload_length` attribute, then this bytearray's length is truncated to - equal the `payload_length` attribute. - :param bool ask_no_ack: Pass this parameter as `True` to tell the nRF24L01 not to wait for - an acknowledgment from the receiving nRF24L01. This parameter directly controls a - ``NO_ACK`` flag in the transmission's Packet Control Field (9 bits of information about - the payload). Therefore, it takes advantage of an nRF24L01 feature specific to - individual payloads, and its value is not saved anywhere. You do not need to specify - this for every payload if the `auto_ack` attribute is disabled, however this parameter - should work despite the `auto_ack` attribute's setting. - - .. note:: Each transmission is in the form of a packet. This packet contains sections - of data around and including the payload. `See Chapter 7.3 in the nRF24L01 - Specifications Sheet `_ for more - details. - - This function isn't completely non-blocking as we still need to wait just under 5 ms for - the CSN pin to settle (allowing a clean SPI transaction). - - .. note:: The nRF24L01 doesn't initiate sending until a mandatory minimum 10 µs pulse on - the CE pin is acheived. That pulse is initiated before this function exits. However, we - have left that 10 µs wait time to be managed by the MCU in cases of asychronous - application, or it is managed by using `send()` instead of this function. According to - the Specification sheet, if the CE pin remains HIGH for longer than 10 µs, then the - nRF24L01 will continue to transmit all payloads found in the TX FIFO buffer. - - .. warning:: A note paraphrased from the `nRF24L01+ Specifications Sheet `_: - - It is important to NEVER to keep the nRF24L01+ in TX mode for more than 4 ms at a time. - If the [`arc` attribute is] enabled, nRF24L01+ is never in TX mode longer than 4 - ms. - - .. tip:: Use this function at your own risk. Because of the underlying `"Enhanced - ShockBurst Protocol" `_, disobeying the 4 - ms rule is easily avoided if you enable the `auto_ack` attribute. Alternatively, you - MUST use interrupt flags or IRQ pin with user defined timer(s) to AVOID breaking the - 4 ms rule. If the `nRF24L01+ Specifications Sheet explicitly states this - `_, we have to assume - radio damage or misbehavior as a result of disobeying the 4 ms rule. See also `table 18 - in the nRF24L01 specification sheet `_ for calculating - necessary transmission time (these calculations are used in the `send()` and `resend()` - functions). - """ + def write(self, buf, ask_no_ack=False, write_only=False): + """This non-blocking function (when used as alternative to `send()`) + is meant for asynchronous applications and can only handle one + payload at a time as it is a helper function to `send()`.""" if not buf or len(buf) > 32: - raise ValueError("buf must be a buffer protocol object with a byte length of" - "\nat least 1 and no greater than 32") - self.clear_status_flags(False) # only TX related IRQ flags - if self._config & 3 != 2: # ready radio if it isn't yet - # ensures tx mode & powered up - self._config = (self._reg_read(CONFIG) & 0x7c) | 2 - self._reg_write(CONFIG, self._config) - time.sleep(0.00016) # power up/down takes < 150 µs + 4 µs - # pad out or truncate data to fill payload_length if dynamic_payloads == False - if not self.dynamic_payloads: - if len(buf) < self.payload_length: - for _ in range(self.payload_length - len(buf)): - buf += b'\x00' - elif len(buf) > self.payload_length: - buf = buf[:self.payload_length] - # now upload the payload accordingly with appropriate command - if ask_no_ack: # payload doesn't want acknowledgment - # ensure this feature is allowed by setting EN_DYN_ACK flag in the FEATURE register + raise ValueError("buffer must have a length in range [1, 32]") + self.clear_status_flags() + if self._config & 3 != 2: # is radio powered up in TX mode? + self._config = (self._reg_read(CONFIGURE) & 0x7C) | 2 + self._reg_write(CONFIGURE, self._config) + time.sleep(0.00016) + if not bool((self._dyn_pl & 1) and (self._features & 4)): + if len(buf) < self._pl_len[0]: + buf += b"\x00" * (self._pl_len[0] - len(buf)) + elif len(buf) > self._pl_len[0]: + buf = buf[: self._pl_len[0]] + if ask_no_ack: if self._features & 1 == 0: - self._features = self._features & 0xFE | 1 # set EN_DYN_ACK flag high - self._reg_write(FEATURE, self._features) - # write appropriate command with payload - # 0xA0 = W_TX_PAYLOAD; 0xB0 = W_TX_PAYLOAD_NO_ACK - self._reg_write_bytes(0xA0 | (ask_no_ack << 4), buf) - # enable radio comms so it can send the data by starting the mandatory minimum 10 µs pulse - # on CE. Let send() or resend() measure this pulse for blocking reasons - self.ce_pin.value = 1 - # while CE is still HIGH only if dynamic_payloads and auto_ack are enabled - # automatically goes to standby-II after successful TX of all payloads in the FIFO + self._features = self._features & 0xFE | 1 + self._reg_write(TX_FEATURE, self._features) + self._reg_write_bytes(0xA0 | (bool(ask_no_ack) << 4), buf) + if not write_only: + self.ce_pin.value = 1 def flush_rx(self): - """A helper function to flush the nRF24L01's internal RX FIFO buffer. (write-only) - - .. note:: The nRF24L01 RX FIFO is 3 level stack that holds payload data. This means that - there can be up to 3 received payloads (each of a maximum length equal to 32 bytes) - waiting to be read (and popped from the stack) by `recv()` or `read_ack()`. This - function clears all 3 levels. - """ + """A helper function to flush the nRF24L01's RX FIFO buffer.""" self._reg_write(0xE2) def flush_tx(self): - """A helper function to flush the nRF24L01's internal TX FIFO buffer. (write-only) - - .. note:: The nRF24L01 TX FIFO is 3 level stack that holds payload data. This means that - there can be up to 3 payloads (each of a maximum length equal to 32 bytes) waiting to - be transmit by `send()`, `resend()` or `write()`. This function clears all 3 levels. It - is worth noting that the payload data is only popped from the TX FIFO stack upon - successful transmission (see also `resend()` as the handling of failed transmissions - can be altered). - """ + """A helper function to flush the nRF24L01's TX FIFO buffer.""" self._reg_write(0xE1) def fifo(self, about_tx=False, check_empty=None): - """This provides some precision determining the status of the TX/RX FIFO buffers. - (read-only) - - :param bool about_tx: - * `True` means information returned is about the TX FIFO buffer. - * `False` means information returned is about the RX FIFO buffer. This parameter - defaults to `False` when not specified. - :param bool check_empty: - * `True` tests if the specified FIFO buffer is empty. - * `False` tests if the specified FIFO buffer is full. - * `None` (when not specified) returns a 2 bit number representing both empty (bit 1) & - full (bit 0) tests related to the FIFO buffer specified using the ``tx`` parameter. - :returns: - * A `bool` answer to the question: - "Is the [TX/RX]:[`True`/`False`] FIFO buffer [empty/full]:[`True`/`False`]? - * If the ``check_empty`` parameter is not specified: an `int` in range [0,2] for which: - - - ``1`` means the specified FIFO buffer is full - - ``2`` means the specified FIFO buffer is empty - - ``0`` means the specified FIFO buffer is neither full nor empty - """ - if (check_empty is None and isinstance(about_tx, (bool, int))) or \ - (isinstance(check_empty, (bool, int)) and isinstance(about_tx, (bool, int))): - self._fifo = self._reg_read(FIFO) # refresh the data - if check_empty is None: - return (self._fifo & (0x30 if about_tx else 0x03)) >> (4 * about_tx) - return bool(self._fifo & ((2 - check_empty) << (4 * about_tx))) - raise ValueError("Argument 1 ('about_tx') must always be a bool or int. Argument 2" - " ('check_empty'), if specified, must be a bool or int") - - def pipe(self): - """This function returns information about the data pipe that received the next available - payload in the RX FIFO buffer. - - :returns: - - `None` if there is no payload in RX FIFO. - - The `int` identifying pipe number [0,5] that received the next available payload in - the RX FIFO buffer. - """ - self.update() # perform Non-operation command to get status byte (should be faster) - result = (self._status & 0x0E) >> 1 # 0x0E==RX_P_NO - if result <= 5: # is there data in RX FIFO? - return result - return None # RX FIFO is empty + """This provides some precision determining the status of the TX/RX + FIFO buffers. (read-only)""" + self._fifo, about_tx = (self._reg_read(0x17), bool(about_tx)) + if check_empty is None: + return (self._fifo & (0x30 if about_tx else 0x03)) >> (4 * about_tx) + return bool(self._fifo & ((2 - bool(check_empty)) << (4 * about_tx))) def address(self, index=-1): - """Returns the current address set to a specified data pipe or the TX address. (read-only) - - :param int index: the number of the data pipe whose address is to be returned. Defaults to - ``-1``. A valid index ranges [0,5] for RX addresses or any negative `int` for the TX - address. Otherwise an `IndexError` is thown. - """ + """Returns the current address set to a specified data pipe or the TX + address. (read-only)""" if index > 5: raise IndexError("index {} is out of bounds [0,5]".format(index)) if index < 0: return self._tx_address if index <= 1: return self._pipes[index] - return bytes(self._pipes[index]) + self._pipes[1][1:] - - def _wait_for_result(self, timeout): - 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)) - - def _attempt2resend(self, attempts): - retry = False - for _ in range(attempts): - # resend() clears flags upon entering and exiting - retry = self.resend() - if retry is None or retry: - break # retry succeeded - return retry + return bytes([self._pipes[index]]) + self._pipes[1][1:] + + @property + def rpd(self): + """This read-only attribute returns `True` if RPD (Received Power + Detector) is triggered or `False` if not triggered.""" + return bool(self._reg_read(0x09)) + + def start_carrier_wave(self): + """Starts a continuous carrier wave test.""" + self.power = 0 + self.ce_pin.value = 0 + self.power = 1 + self.listen = 0 + self._rf_setup |= 0x90 + self._reg_write(RF_PA_RATE, self._rf_setup) + if not self.is_plus_variant: + self.auto_ack = False + self._retry_setup = 0 + self._reg_write(SETUP_RETR, self._retry_setup) + self._tx_address = bytearray([0xFF] * 5) + self._reg_write_bytes(TX_ADDRESS, self._tx_address) + self._reg_write_bytes(0xA0, b"\xFF" * 32) + self.crc = 0 + self.ce_pin.value = 1 + time.sleep(0.001) + self.ce_pin.value = 0 + while self._status & 0x70: + self.update() + self._reg_write(0x17, 0x40) + self.ce_pin.value = 1 + + def stop_carrier_wave(self): + """Stops a continuous carrier wave test.""" + self.ce_pin.value = 0 + self.power = 0 + self._rf_setup &= ~0x90 + self._reg_write(RF_PA_RATE, self._rf_setup) diff --git a/circuitpython_nrf24l01/rf24_lite.py b/circuitpython_nrf24l01/rf24_lite.py new file mode 100644 index 0000000..582ab3e --- /dev/null +++ b/circuitpython_nrf24l01/rf24_lite.py @@ -0,0 +1,353 @@ +# see license and copyright information in rf24.py +# pylint: disable=missing-docstring +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/2bndy5/CircuitPython_nRF24L01.git" +import time +from adafruit_bus_device.spi_device import SPIDevice + + +class RF24: + def __init__(self, spi, csn, ce, spi_frequency=10000000): + self._spi = SPIDevice(spi, chip_select=csn, baudrate=spi_frequency) + self.ce_pin = ce + self.ce_pin.switch_to_output(value=False) + self._status = 0 + self._reg_write(0, 0x0E) + if self._reg_read(0) & 3 != 2: + raise RuntimeError("nRF24L01 Hardware not responding") + self.power = False + self._reg_write(3, 3) + self._reg_write(6, 7) + self._reg_write(2, 0) + self._reg_write(0x1C, 0x3F) + self._reg_write(1, 0x3F) + self._reg_write(0x1D, 5) + self._reg_write(4, 0x53) + self._pipe0_read_addr = None + self.channel = 76 + self.payload_length = 32 + self.flush_rx() + self.flush_tx() + self.clear_status_flags() + + # pylint: disable=no-member + def _reg_read(self, reg): + out_buf = bytes([reg, 0]) + in_buf = bytearray([0, 0]) + with self._spi as spi: + time.sleep(0.005) + spi.write_readinto(out_buf, in_buf) + self._status = in_buf[0] + return in_buf[1] + + def _reg_read_bytes(self, reg, buf_len=5): + in_buf = bytearray(buf_len + 1) + out_buf = bytes([reg]) + b"\x00" * buf_len + with self._spi as spi: + time.sleep(0.005) + spi.write_readinto(out_buf, in_buf) + self._status = in_buf[0] + return in_buf[1:] + + def _reg_write_bytes(self, reg, out_buf): + out_buf = bytes([0x20 | reg]) + out_buf + in_buf = bytearray(len(out_buf)) + with self._spi as spi: + time.sleep(0.005) + spi.write_readinto(out_buf, in_buf) + self._status = in_buf[0] + + def _reg_write(self, reg, val=None): + out_buf = bytes([reg]) + if val is not None: + out_buf = bytes([0x20 | reg, val]) + in_buf = bytearray(len(out_buf)) + with self._spi as spi: + time.sleep(0.005) + spi.write_readinto(out_buf, in_buf) + self._status = in_buf[0] + + # pylint: enable=no-member + @property + def address_length(self): + return self._reg_read(0x03) + 2 + + @address_length.setter + def address_length(self, length): + if not 3 <= length <= 5: + raise ValueError("address_length must be in range [3, 5]") + self._reg_write(0x03, length - 2) + + def open_tx_pipe(self, addr): + if self.arc: + self._reg_write_bytes(0x0A, addr) + self._reg_write(2, self._reg_read(2) | 1) + self._reg_write_bytes(0x10, addr) + + def close_rx_pipe(self, pipe_num): + if pipe_num < 0 or pipe_num > 5: + raise ValueError("pipe_number must be in range [0, 5]") + open_pipes = self._reg_read(2) + if open_pipes & (1 << pipe_num): + self._reg_write(2, open_pipes & ~(1 << pipe_num)) + + def open_rx_pipe(self, pipe_num, addr): + if not 0 <= pipe_num <= 5: + raise ValueError("pipe_number must be in range [0, 5]") + if not addr: + raise ValueError("address length cannot be 0") + if pipe_num < 2: + if not pipe_num: + self._pipe0_read_addr = addr + self._reg_write_bytes(0x0A + pipe_num, addr) + else: + self._reg_write(0x0A + pipe_num, addr[0]) + self._reg_write(2, self._reg_read(2) | (1 << pipe_num)) + + @property + def listen(self): + return self._reg_read(0) & 3 == 3 + + @listen.setter + def listen(self, is_rx): + if self.listen != bool(is_rx): + self.ce_pin.value = 0 + if is_rx: + if self._pipe0_read_addr is not None: + self._reg_write_bytes(0x0A, self._pipe0_read_addr) + self._reg_write(0, (self._reg_read(0) & 0xFC) | 3) + time.sleep(0.00015) + self.flush_rx() + self.clear_status_flags() + self.ce_pin.value = 1 + time.sleep(0.00013) + else: + self._reg_write(0, self._reg_read(0) & 0xFE) + time.sleep(0.00016) + + def any(self): + if self._reg_read(0x1D) & 4 and self.pipe is not None: + return self._reg_read(0x60) + if self.pipe is not None: + return self._reg_read(0x11 + self.pipe) + return 0 + + def recv(self, length=None): + ret_size = length if length is not None else self.any() + if not ret_size: + return None + result = self._reg_read_bytes(0x61, ret_size) + self.clear_status_flags(True, False, False) + return result + + def send(self, buf, ask_no_ack=False, force_retry=0, send_only=False): + self.ce_pin.value = 0 + if isinstance(buf, (list, tuple)): + result = [] + for b in buf: + result.append(self.send(b, ask_no_ack, force_retry, send_only)) + return result + self.flush_tx() + if not send_only: + self.flush_rx() + self.write(buf, ask_no_ack) + time.sleep(0.00001) + self.ce_pin.value = 0 + while not self._status & 0x70: + self.update() + result = self.irq_ds + if self.irq_df: + for _ in range(force_retry): + result = self.resend(send_only) + if result is None or result: + break + if self._status & 0x60 == 0x60 and not send_only: + result = self.recv() + self.clear_status_flags(False) + return result + + @property + def tx_full(self): + return bool(self._status & 1) + + @property + def pipe(self): + result = (self._status & 0x0E) >> 1 + if result <= 5: + return result + return None + + @property + def irq_dr(self): + return bool(self._status & 0x40) + + @property + def irq_ds(self): + return bool(self._status & 0x20) + + @property + def irq_df(self): + return bool(self._status & 0x10) + + def clear_status_flags(self, data_recv=True, data_sent=True, data_fail=True): + config = bool(data_recv) << 6 | bool(data_sent) << 5 | bool(data_fail) << 4 + self._reg_write(7, config) + + def interrupt_config(self, data_recv=True, data_sent=True, data_fail=True): + config = (not data_recv) << 6 | (not data_fail) << 4 | (not data_sent) << 5 + self._reg_write(0, (self._reg_read(0) & 0x0F) | config) + + @property + def dynamic_payloads(self): + return bool(self._reg_read(0x1C)) and self._reg_read(0x1D) & 4 == 4 + + @dynamic_payloads.setter + def dynamic_payloads(self, enable): + self._reg_write(0x1D, (self._reg_read(0x1D) & 3) | bool(enable) << 2) + self._reg_write(0x1C, 0x3F if enable else 0) + + @property + def payload_length(self): + return self._reg_read(0x11) + + @payload_length.setter + def payload_length(self, length): + if not length or length > 32: + raise ValueError("payload_length must be in range [1, 32]") + for i in range(6): + self._reg_write(0x11 + i, length) + + @property + def arc(self): + return self._reg_read(4) & 0x0F + + @arc.setter + def arc(self, cnt): + if not 0 <= cnt <= 15: + raise ValueError("arc must be in range [0, 15]") + self._reg_write(4, (self._reg_read(4) & 0xF0) | cnt) + + @property + def ard(self): + return ((self._reg_read(4) & 0xF0) >> 4) * 250 + 250 + + @ard.setter + def ard(self, delta): + if not 250 <= delta <= 4000: + raise ValueError("ard must be in range [250, 4000]") + self._reg_write(4, (self._reg_read(4) & 0x0F) | int((delta - 250) / 250) << 4) + + @property + def ack(self): + return self._reg_read(0x1D) & 6 == 6 and bool(self._reg_read(0x1C)) + + @ack.setter + def ack(self, enable): + features = self._reg_read(0x1D) & 5 + if enable: + self._reg_write(0x1C, 0x3F) + features = (features & 3) | 4 + features |= 2 if enable else 0 + self._reg_write(0x1D, features) + + def load_ack(self, buf, pipe_num): + if 0 <= pipe_num <= 5 and (not buf or (len(buf) < 32)): + if not self._reg_read(0x1D) & 2: + self.ack = True + if not self.tx_full: + self._reg_write_bytes(0xA8 | pipe_num, buf) + return True + return False + + @property + def data_rate(self): + rf_setup = self._reg_read(6) & 0x28 + return (2 if rf_setup == 8 else 250) if rf_setup else 1 + + @data_rate.setter + def data_rate(self, speed): + speed = 0 if speed == 1 else (0x20 if speed != 2 else 8) + self._reg_write(6, (self._reg_read(6) & 0xD7) | speed) + + @property + def channel(self): + return self._reg_read(5) + + @channel.setter + def channel(self, chnl): + if not 0 <= int(chnl) <= 125: + raise ValueError("channel must be in range [0, 125]") + self._reg_write(5, int(chnl)) + + @property + def power(self): + return bool(self._reg_read(0) & 2) + + @power.setter + def power(self, is_on): + config = self._reg_read(0) + if bool(config & 2) != bool(is_on): + self._reg_write(0, config & 0x7D | bool(is_on) << 1) + time.sleep(0.00016) + + @property + def pa_level(self): + return (3 - ((self._reg_read(6) & 6) >> 1)) * -6 + + @pa_level.setter + def pa_level(self, pwr): + if pwr not in (-18, -12, -6, 0): + raise ValueError("pa_level must be -18, -12, -6, or 0") + self._reg_write(6, self._reg_read(6) & 0xF8 | (3 - int(pwr / -6)) * 2 | 1) + + def update(self): + self._reg_write(0xFF) + + def resend(self, send_only=False): + result = False + if not self.fifo(True, True): + if not send_only: + self.flush_rx() + self.clear_status_flags() + self._reg_write(0xE3) + self.ce_pin.value = 0 + self.ce_pin.value = 1 + time.sleep(0.00001) + self.ce_pin.value = 0 + while not self._status & 0x70: + self.update() + result = self.irq_ds + if self._status & 0x60 == 0x60 and not send_only: + result = self.recv() + self.clear_status_flags(False) + return result + + def write(self, buf, ask_no_ack=False, write_only=False): + if not buf or len(buf) > 32: + raise ValueError("buffer length must be in range [1, 32]") + self.clear_status_flags() + config = self._reg_read(0) + if config & 3 != 2: + self._reg_write(0, (config & 0x7C) | 2) + time.sleep(0.00016) + if not self.dynamic_payloads: + pl_width = self.payload_length + if len(buf) < pl_width: + buf += b"\x00" * pl_width - len(buf) + elif len(buf) > pl_width: + buf = buf[:pl_width] + self._reg_write_bytes(0xA0 | (ask_no_ack << 4), buf) + if not write_only: + self.ce_pin.value = 1 + + def flush_rx(self): + self._reg_write(0xE2) + + def flush_tx(self): + self._reg_write(0xE1) + + def fifo(self, about_tx=False, check_empty=None): + _fifo, about_tx = (self._reg_read(0x17), bool(about_tx)) + if check_empty is None: + return (_fifo & (0x30 if about_tx else 0x03)) >> (4 * about_tx) + return bool(_fifo & ((2 - bool(check_empty)) << (4 * about_tx))) diff --git a/docs/_static/darkness.css b/docs/_static/darkness.css index c14a4cf..6b6ab50 100644 --- a/docs/_static/darkness.css +++ b/docs/_static/darkness.css @@ -89,7 +89,16 @@ .highlight .go { color: #1afd00; - } +} + +td.linenos pre { + color: #fff; + background-color: #373737; +} + +.highlight .fm { + color: #f7ef84; +} /* ----------------------table sections---------------------------------------- */ .wy-table thead, @@ -182,7 +191,20 @@ body { } .wy-nav-content-wrap, .wy-nav-content { - background:#242424; + background:#242424; +} +.btn-neutral { + background-color:#275325 !important; + color:#fff !important; +} + +.btn-neutral:hover { + background-color:#006060 !important; + color:#fffafa +} + +.btn-neutral:visited { + color:#fff !important; } /* -------------------------------------sidebar sections------------------------------------------------ */ @@ -259,6 +281,15 @@ body { } /* -----------------------------------------API sections-------------------------------------- */ + +.wy-table-bordered-all td, .rst-content table.docutils td { + text-align: left; +} + +.rst-content table.docutils.citation, .rst-content table.docutils.footnote { + color: white; +} + .rst-content dl:not(.docutils) dl dt { background: #343434; color: #e5df8e; @@ -305,3 +336,14 @@ code, .rst-content tt, .rst-content code { .rst-content h1:hover .headerlink::after, .rst-content h2:hover .headerlink::after, .rst-content .toctree-wrapper p.caption:hover .headerlink::after, .rst-content h3:hover .headerlink::after, .rst-content h4:hover .headerlink::after, .rst-content h5:hover .headerlink::after, .rst-content h6:hover .headerlink::after, .rst-content dl dt:hover .headerlink::after, .rst-content p.caption:hover .headerlink::after, .rst-content table > caption:hover .headerlink::after, .rst-content .code-block-caption:hover .headerlink::after { color: #29ae5b !important; } + +html.writer-html4 .rst-content dl:not(.docutils)>dt, +html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple)>dt { + background: #2e363c; +} + +html.writer-html4 .rst-content dl:not(.docutils) dl:not(.field-list)>dt, +html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.glossary):not(.simple) dl:not(.field-list)>dt { + background:#2e363c; + color:#2980b1; +} diff --git a/docs/advanced_api.rst b/docs/advanced_api.rst new file mode 100644 index 0000000..e74524d --- /dev/null +++ b/docs/advanced_api.rst @@ -0,0 +1,494 @@ + +.. |irq note| replace:: parameter as `True` to + :py:func:`~circuitpython_nrf24l01.rf24.RF24.clear_status_flags()` and reset this. + As this is a virtual representation of the interrupt event, this attribute will + always be updated despite what the actual IRQ pin is configured to do about this + event. + +.. |update manually| replace:: Calling this does not execute an SPI transaction. It only + exposes that latest data contained in the STATUS byte that's always returned from any + other SPI transactions. Use the :py:func:`~circuitpython_nrf24l01.rf24.RF24.update()` + function to manually refresh this data when needed. + +Advanced API +------------ + +what_happened() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.what_happened + + Some information may be irrelevant depending on nRF24L01's state/condition. + + :prints: + + - ``Is a plus variant`` True means the transceiver is a nRF24L01+. False + means the transceiver is a nRF24L01 (not a plus variant). + - ``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 + - ``TX Payload lengths`` The current setting of the `payload_length` attribute for TX + operations (concerning data pipe 0) + - ``Auto retry delay`` The current setting of the `ard` attribute + - ``Auto retry attempts`` The current setting of the `arc` attribute + - ``Packets lost on current channel`` Total amount of packets lost (transmission + failures). This only resets when the `channel` is changed. This count will + only go up 15. + - ``Retry attempts made for last transmission`` Amount of attempts to re-transmit + during last transmission (resets per payload) + - ``IRQ - Data Ready`` The current setting of the IRQ pin on "Data Ready" event + - ``IRQ - Data Sent`` The current setting of the IRQ pin on "Data Sent" event + - ``IRQ - Data Fail`` The current setting of the IRQ pin on "Data Fail" event + - ``Data Ready`` Is there RX data ready to be read? (state of the `irq_dr` flag) + - ``Data Sent`` Has the TX data been sent? (state of the `irq_ds` flag) + - ``Data Failed`` Has the maximum attempts to re-transmit been reached? + (state of the `irq_df` flag) + - ``TX FIFO full`` Is the TX FIFO buffer full? (state of the `tx_full` flag) + - ``TX FIFO empty`` Is the TX FIFO buffer empty? + - ``RX FIFO full`` Is the RX FIFO buffer full? + - ``RX FIFO empty`` Is the RX FIFO buffer empty? + - ``Custom ACK payload`` Is the nRF24L01 setup to use an extra (user defined) payload + attached to the acknowledgment packet? (state of the `ack` attribute) + - ``Ask no ACK`` Is the nRF24L01 setup to transmit individual packets that don't + require acknowledgment? + - ``Automatic Acknowledgment`` The status of the `auto_ack` feature. If this value is a + binary representation, then each bit represents the feature's status for each pipe. + - ``Dynamic Payloads`` The status of the `dynamic_payloads` feature. If this value is a + binary representation, then each bit represents the feature's status for each pipe. + - ``Primary Mode`` The current mode (RX or TX) of communication of the nRF24L01 device. + - ``Power Mode`` The power state can be Off, Standby-I, Standby-II, or On. + + :param bool dump_pipes: `True` appends the output and prints: + + - the current address used for TX transmissions. This value is the entire content of + the nRF24L01's register about the TX address (despite what `address_length` is set + to). + - ``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 + the full value stored in the nRF24L01's RX address registers (despite what + `address_length` is set to. + - 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 for that pipe. + + Default is `False` and skips this extra information. + +is_plus_variant +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.is_plus_variant + + Upon instantiation, this class detirmines if the nRF24L01 is a plus variant or not. + +load_ack() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.load_ack + + This payload will then be appended to the automatic acknowledgment + (ACK) packet that is sent when fresh data is received on the specified pipe. See + `read_ack()` on how to fetch a received custom ACK payloads. + + :param bytearray,bytes buf: This will be the data attached to an automatic ACK packet on the + incoming transmission about the specified ``pipe_number`` parameter. This must have a + length in range [1, 32] bytes, otherwise a `ValueError` exception is thrown. Any ACK + payloads will remain in the TX FIFO buffer until transmitted successfully or + `flush_tx()` is called. + :param int pipe_number: This will be the pipe number to use for deciding which + transmissions get a response with the specified ``buf`` parameter's data. This number + must be in range [0, 5], otherwise a `ValueError` exception is thrown. + + :returns: `True` if payload was successfully loaded onto the TX FIFO buffer. `False` if it + wasn't because TX FIFO buffer is full. + + .. note:: this function takes advantage of a special feature on the nRF24L01 and needs to + be called for every time a customized ACK payload is to be used (not for every + automatic ACK packet -- this just appends a payload to the ACK packet). The `ack`, + `auto_ack`, and `dynamic_payloads` attributes are also automatically enabled by this + function when necessary. + + .. tip:: The ACK payload must be set prior to receiving a transmission. It is also worth + noting that the nRF24L01 can hold up to 3 ACK payloads pending transmission. Using this + function does not over-write existing ACK payloads pending; it only adds to the queue + (TX FIFO buffer) if it can. Use `flush_tx()` to discard unused ACK payloads when done + listening. + +read_ack() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.read_ack + + This function was internally called from a blocking `send()` call if the `ack` attribute + is enabled. Alternatively, this function can be called directly in case of calling the + non-blocking `write()` function during asychronous applications. This function is an alias + of `recv()` and remains for backward compatibility with older versions of this library. + + .. note:: See also the `ack`, `dynamic_payloads`, and `auto_ack` attributes as they must be + enabled to use custom ACK payloads. + + .. warning:: This function will be deprecated on next major release. Use `recv()` instead. + +irq_dr +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.irq_dr + + . + + :Returns: + + - `True` represents Data is in the RX FIFO buffer + - `False` represents anything depending on context (state/condition of FIFO buffers); + usually this means the flag's been reset. + + Pass ``dataReady`` |irq note| + + |update manually| + +irq_df +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.irq_df + + . + + :Returns: + + - `True` signifies the nRF24L01 attemped all configured retries + - `False` represents anything depending on context (state/condition); usually this + means the flag's been reset. + + Pass ``dataFail`` |irq note| + + |update manually| + +irq_ds +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.irq_ds + + . + + :Returns: + + - `True` represents a successful transmission + - `False` represents anything depending on context (state/condition of FIFO buffers); + usually this means the flag's been reset. + + Pass ``dataSent`` |irq note| + + |update manually| + +clear_status_flags() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.clear_status_flags + + Internally, this is automatically called by `send()`, `write()`, `recv()`, and when + `listen` changes from `False` to `True`. + + :param bool data_recv: specifies wheather to clear the "RX Data Ready" flag. + :param bool data_sent: specifies wheather to clear the "TX Data Sent" flag. + :param bool data_fail: specifies wheather to clear the "Max Re-transmit reached" flag. + + .. note:: Clearing the ``data_fail`` flag is necessary for continued transmissions from the + nRF24L01 (locks the TX FIFO buffer when `irq_df` is `True`) despite wheather or not the + MCU is taking advantage of the interrupt (IRQ) pin. Call this function only when there + is an antiquated status flag (after you've dealt with the specific payload related to + the staus flags that were set), otherwise it can cause payloads to be ignored and + occupy the RX/TX FIFO buffers. See `Appendix A of the nRF24L01+ Specifications Sheet + `_ for an outline of + proper behavior. + +power +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.power + + This is exposed for convenience. + + - `False` basically puts the nRF24L01 to sleep (AKA power down mode) with ultra-low + current consumption. No transmissions are executed when sleeping, but the nRF24L01 can + still be accessed through SPI. Upon instantiation, this driver class puts the nRF24L01 + to sleep until the MCU invokes RX/TX transmissions. This driver class doesn't power down + the nRF24L01 after RX/TX transmissions are complete (avoiding the required power up/down + 150 µs wait time), that preference is left to the application. + - `True` powers up the nRF24L01. This is the first step towards entering RX/TX modes (see + also `listen` attribute). Powering up is automatically handled by the `listen` attribute + as well as the `send()` and `write()` functions. + + .. note:: This attribute needs to be `True` if you want to put radio on Standby-II (highest + current consumption) or Standby-I (moderate current consumption) modes, which Standby + mode depends on the state of the CE pin. TX transmissions are only executed during + Standby-II by calling `send()` or `write()`. RX transmissions are received during + Standby-II by setting `listen` attribute to `True` (see `Chapter 6.1.2-7 of the + nRF24L01+ Specifications Sheet `_). After using + `send()` or setting `listen` to `False`, the nRF24L01 is left in Standby-I mode (see + also notes on the `write()` function). + +tx_full +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.tx_full + + . + + |update manually| + + :returns: + + - `True` for TX FIFO buffer is full + - `False` for TX FIFO buffer is not full. This doesn't mean the TX FIFO buffer is + empty. + +update() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.update + + Refreshing the status byte is vital to checking status of the interrupt flags, RX pipe + number related to current RX payload, and if the TX FIFO buffer is full. This function + returns nothing, but internally updates the `irq_dr`, `irq_ds`, `irq_df`, `pipe`, and + `tx_full` attributes. Internally this is a helper function to `send()`, and `resend()` + functions. + +resend() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.resend + + All returned data from this function follows the same patttern that `send()` returns with + the added condition that this function will return `False` if the TX FIFO buffer is empty. + + :param bool send_only: This parameter only applies when the `ack` attribute is set to + `True`. Pass this parameter as `True` if you want to handle fetching the ACK + payload (from the RX FIFO) seperately from the sending transmission that recieved + the ACK payload. Many other libraries' behave as though this parameter is `True` + (e.g. The popular TMRh20 Arduino RF24 library). Use `recv()` to get the ACK + payload (if there is any) from the RX FIFO. This parameter defaults to `False`. + Remember that the RX FIFO can only hold up to 3 payloads at once. + + .. note:: The nRF24L01 normally removes a payload from the TX FIFO buffer after successful + transmission, but not when this function is called. The payload (successfully + transmitted or not) will remain in the TX FIFO buffer until `flush_tx()` is called to + remove them. Alternatively, using this function also allows the failed payload to be + over-written by using `send()` or `write()` to load a new payload into the TX FIFO + buffer. + +write() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.write + + This function isn't completely non-blocking as we still need to wait 5 ms (`CSN_DELAY`) + for the CSN pin to settle (allowing an accurate SPI write transaction). Example usage of + this function can be seen in the `IRQ pin example `_ + + :param bytearray buf: The payload to transmit. This bytearray must have a length greater + than 0 and less than 32 bytes, otherwise a `ValueError` exception is thrown. + + - If the `dynamic_payloads` attribute is disabled for data pipe 0 and this bytearray's + length is less than the `payload_length` attribute for data pipe 0, then this + bytearray is padded with zeros until its length is equal to the `payload_length` + attribute for data pipe 0. + - If the `dynamic_payloads` attribute is disabled for data pipe 0 and this bytearray's + length is greater than `payload_length` attribute for data pipe 0, then this + bytearray's length is truncated to equal the `payload_length` attribute for data + pipe 0. + :param bool ask_no_ack: Pass this parameter as `True` to tell the nRF24L01 not to wait for + an acknowledgment from the receiving nRF24L01. This parameter directly controls a + ``NO_ACK`` flag in the transmission's Packet Control Field (9 bits of information about + the payload). Therefore, it takes advantage of an nRF24L01 feature specific to + individual payloads, and its value is not saved anywhere. You do not need to specify + this for every payload if the `arc` attribute is disabled, however setting this + parameter to `True` will work despite the `arc` attribute's setting. + + .. note:: Each transmission is in the form of a packet. This packet contains sections + of data around and including the payload. `See Chapter 7.3 in the nRF24L01 + Specifications Sheet `_ for more + details. + :param bool write_only: This function will not manipulate the nRF24L01's CE pin if this + parameter is `True`. The default value of `False` will ensure that the CE pin is + HIGH upon exiting this function. This function does not set the CE pin LOW at + any time. Use this parameter as `True` to fill the TX FIFO buffer before beginning + transmissions. + + .. note:: The nRF24L01 doesn't initiate sending until a mandatory minimum 10 µs pulse + on the CE pin is acheived. If the ``write_only`` parameter is `False`, then that + pulse is initiated before this function exits. However, we have left that 10 µs + wait time to be managed by the MCU in cases of asychronous application, or it is + managed by using `send()` instead of this function. According to the + Specification sheet, if the CE pin remains HIGH for longer than 10 µs, then the + nRF24L01 will continue to transmit all payloads found in the TX FIFO buffer. + + .. warning:: + A note paraphrased from the `nRF24L01+ Specifications Sheet + `_: + + It is important to NEVER to keep the nRF24L01+ in TX mode for more than 4 ms at a time. + If the [`arc` attribute is] enabled, nRF24L01+ is never in TX mode longer than 4 + ms. + + .. tip:: Use this function at your own risk. Because of the underlying + `"Enhanced ShockBurst Protocol" `_, disobeying the 4 + ms rule is easily avoided if the `arc` attribute is greater than ``0``. Alternatively, + you MUST use nRF24L01's IRQ pin and/or user-defined timer(s) to AVOID breaking the + 4 ms rule. If the `nRF24L01+ Specifications Sheet explicitly states this + `_, we have to assume + radio damage or misbehavior as a result of disobeying the 4 ms rule. See also `table 18 + in the nRF24L01 specification sheet `_ for + calculating an adequate transmission timeout sentinal. + +flush_rx() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.flush_rx + + .. note:: The nRF24L01 RX FIFO is 3 level stack that holds payload data. This means that + there can be up to 3 received payloads (each of a maximum length equal to 32 bytes) + waiting to be read (and removed from the stack) by `recv()` or `read_ack()`. This + function clears all 3 levels. + +flush_tx() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.flush_tx + + .. note:: The nRF24L01 TX FIFO is 3 level stack that holds payload data. This means that + there can be up to 3 payloads (each of a maximum length equal to 32 bytes) waiting to + be transmit by `send()`, `resend()` or `write()`. This function clears all 3 levels. It + is worth noting that the payload data is only removed from the TX FIFO stack upon + successful transmission (see also `resend()` as the handling of failed transmissions + can be altered). + +fifo() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.fifo + + :param bool about_tx: + - `True` means information returned is about the TX FIFO buffer. + - `False` means information returned is about the RX FIFO buffer. This parameter + defaults to `False` when not specified. + :param bool check_empty: + - `True` tests if the specified FIFO buffer is empty. + - `False` tests if the specified FIFO buffer is full. + - `None` (when not specified) returns a 2 bit number representing both empty (bit 1) & + full (bit 0) tests related to the FIFO buffer specified using the ``about_tx`` + parameter. + :returns: + - A `bool` answer to the question: + "Is the [TX/RX]:[`True`/`False`] FIFO buffer [empty/full]:[`True`/`False`]? + - If the ``check_empty`` parameter is not specified: an `int` in range [0,2] for which: + + - ``1`` means the specified FIFO buffer is full + - ``2`` means the specified FIFO buffer is empty + - ``0`` means the specified FIFO buffer is neither full nor empty + +pipe +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.pipe + + . + + |update manually| + + :Returns: + + - `None` if there is no payload in RX FIFO. + - The `int` identifying pipe number [0,5] that received the next + available payload in the RX FIFO buffer. + +address_length +****************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.address_length + + 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. + +address() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.address + + This function returns the full content of the nRF24L01's registers about RX/TX addresses + despite what `address_length` is set to. + + :param int index: the number of the data pipe whose address is to be returned. Defaults to + ``-1``. A valid index ranges [0,5] for RX addresses or any negative `int` for the TX + address. Otherwise an `IndexError` is thown. + +rpd +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.rpd + + The RPD flag is triggered in the following cases: + + 1. During RX mode (`listen` = `True`) and an arbitrary RF transmission with a gain + above -64 dBm threshold is/was present. + 2. When a packet is received (instigated by the nRF24L01 used to detect/"listen" for + incoming packets). + + .. note:: See also + `section 6.4 of the Specification Sheet concerning the RPD flag + `_. Ambient + temperature affects the -64 dBm threshold. The latching of this flag happens + differently under certain conditions. + +start_carrier_wave() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.start_carrier_wave + + This is a basic test of the nRF24L01's TX output. It is a commonly required + test for telecommunication regulations. Calling this function may introduce + interference with other transceivers that use frequencies in range [2.4, + 2.525] GHz. To verify that this test is working properly, use the following + code on a seperate nRF24L01 transceiver: + + .. code-block:: python + + # declare objects for SPI bus and CSN pin and CE pin + nrf. = RF24(spi, csn, ce) + # set nrf.pa_level, nrf.channel, & nrf.data_rate values to + # match the corresponding attributes on the device that is + # transmitting the carrier wave + nrf.listen = True + if nrf.rpd: + print("carrier wave detected") + + The `pa_level`, `channel` & `data_rate` attributes are vital factors to + the success of this test. Be sure these attributes are set to the desired test + conditions before calling this function. See also the `rpd` attribute. + + .. note:: To preserve backward compatibility with non-plus variants of the + nRF24L01, this function will also change certain settings if `is_plus_variant` + is `False`. These settings changes include disabling `crc`, disabling + `auto_ack`, disabling `arc`, setting `ard` to 250 microseconds, changing the + TX address to ``b"\xFF\xFF\xFF\xFF\xFF``, and loading a dummy payload into the + TX FIFO buffer while continuously behaving like `resend()` to establish the + constant carrier wave. If `is_plus_variant` is `True`, then none of these + changes to settings are needed nor applied. + +stop_carrier_wave() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.stop_carrier_wave + + See `start_carrier_wave()` for more details. + + .. note:: + Calling this function puts the nRF24L01 to sleep (AKA power down mode). diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index 323c41e..0000000 --- a/docs/api.rst +++ /dev/null @@ -1,273 +0,0 @@ - -.. If you created a package, create one automodule per module in the package. - -.. 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 - -RF24 class -============== - -Troubleshooting info --------------------- - -.. 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 - 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` 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 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). - -With the `auto_ack` feature enabled, you get: - - * cyclic 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 "parking spot" for your payload. There are only six - data pipes on the nRF24L01, thus it can simultaneously "listen" to a maximum of 6 other nRF24L01 - radios. However, it can only "talk" to 1 other nRF24L01 at a time. - - The specified address is not the address of an nRF24L01 radio, rather it is more like a path - that connects the endpoints. When assigning addresses to a data pipe, you can use any 5 byte - long address you can think of (as long as the first byte is unique among simultaneously - broadcasting addresses), so you're not limited to communicating with only the same 6 nRF24L01 - radios (more on this when we officially support "Multiciever" mode). - - Finally, 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, or other ambient signals that use the same spectrum of frequencies. - -.. 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 (passed to `open_rx_pipe()`) MUST match the - TX pipe's address on the transmitting nRF24L01 (passed to `open_tx_pipe()`) - * `address_length` - * `channel` - * `data_rate` - * `dynamic_payloads` - * `payload_length` only when `dynamic_payloads` is disabled - * `auto_ack` on the recieving nRF24L01 must be enabled if `arc` is greater than 0 on the - transmitting nRF24L01 - * 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()` or `load_ack()`), `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). - -Basic API ---------- - -Contrusctor -****************** - -.. autoclass:: circuitpython_nrf24l01.rf24.RF24 - :no-members: - -address_length -****************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.address_length - -open_tx_pipe() -****************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.open_tx_pipe - -close_rx_pipe() -****************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.close_rx_pipe - -open_rx_pipe() -****************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.open_rx_pipe - -listen -****************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.listen - -any() -****************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.any - -recv() -****************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.recv - -send() -****************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.send - -Advanced API ------------- - -what_happened() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.what_happened - -dynamic_payloads -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.dynamic_payloads - -payload_length -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.payload_length - -auto_ack -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.auto_ack - -arc -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.arc - -ard -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.ard - -ack -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.ack - -load_ack() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.load_ack - -read_ack() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.read_ack - -irq_dr -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.irq_dr - -irq_df -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.irq_df - -irq_ds -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.irq_ds - -clear_status_flags() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.clear_status_flags - -interrupt_config() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.interrupt_config - -data_rate -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.data_rate - -channel -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.channel - -crc -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.crc - -power -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.power - -pa_level -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.pa_level - -tx_full -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.tx_full - -rpd -****************************** - -.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.rpd - -update() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.update - -resend() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.resend - -write() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.write - -flush_rx() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.flush_rx - -flush_tx() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.flush_tx - -fifo() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.fifo - -pipe() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.pipe - -address() -****************************** - -.. automethod:: circuitpython_nrf24l01.rf24.RF24.address diff --git a/docs/basic_api.rst b/docs/basic_api.rst new file mode 100644 index 0000000..9e0b9c8 --- /dev/null +++ b/docs/basic_api.rst @@ -0,0 +1,217 @@ + +Basic API +--------- + +Constructor +****************** + +.. autoclass:: circuitpython_nrf24l01.rf24.RF24 + :no-members: + + This class aims to be compatible with other devices in the nRF24xxx product line that + implement the Nordic proprietary Enhanced ShockBurst Protocol (and/or the legacy + ShockBurst Protocol), but officially only supports (through testing) the nRF24L01 and + nRF24L01+ devices. + + :param ~busio.SPI spi: The object for the SPI bus that the nRF24L01 is connected to. + + .. tip:: This object is meant to be shared amongst other driver classes (like + adafruit_mcp3xxx.mcp3008 for example) that use the same SPI bus. Otherwise, multiple + devices on the same SPI bus with different spi objects may produce errors or + undesirable behavior. + :param ~digitalio.DigitalInOut csn: The digital output pin that is connected to the nRF24L01's + CSN (Chip Select Not) pin. This is required. + :param ~digitalio.DigitalInOut ce: The digital output pin that is connected to the nRF24L01's + CE (Chip Enable) pin. This is required. + :param int spi_frequency: Specify which SPI frequency (in Hz) to use on the SPI bus. This + parameter only applies to the instantiated object and is made persistent via + :py:class:`~adafruit_bus_device.spi_device.SPIDevice`. + +open_tx_pipe() +****************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.open_tx_pipe + + :param bytearray,bytes address: The virtual address of the receiving nRF24L01. The address + specified here must match the address set to one of the RX data pipes of the receiving + nRF24L01. The existing address can be altered by writting a bytearray with a length + less than 5. The nRF24L01 will use the first `address_length` number of bytes for the + RX address on the specified data pipe. + + .. 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 for data pipe 0. Thus, RX pipe 0 is appropriated with the TX address + (specified here) when `auto_ack` is enabled for data pipe 0. + +close_rx_pipe() +****************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.close_rx_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. + +open_rx_pipe() +****************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.open_rx_pipe + + If `dynamic_payloads` attribute is disabled for the specifed data pipe, then the + `payload_length` attribute is used to define the expected length of the static RX payload + on the specified data pipe. + + :param int pipe_number: The data pipe to use for RX transactions. This must be in range + [0, 5]. Otherwise a `ValueError` exception is thrown. + :param bytearray,bytes address: The virtual address to the receiving nRF24L01. 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. + The existing address can be altered by writing a bytearray with a length less than 5. + The nRF24L01 will use the first `address_length` number of bytes for the RX address on + the specified data pipe. + + .. note:: The nRF24L01 shares the addresses' last 4 LSBytes on data pipes 2 through + 5. These shared LSBytes are determined by the address set to data pipe 1. + +listen +****************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.listen + + Setting this attribute incorporates the proper transitioning to/from RX mode as it involves + playing with the `power` attribute and the nRF24L01's CE pin. This attribute does not power + down the nRF24L01, but will power it up when needed; use `power` attribute set to `False` + to put the nRF24L01 to sleep. + + A valid input value is a `bool` in which: + + - `True` enables RX mode. Additionally, per `Appendix B of the nRF24L01+ Specifications + Sheet `_, this attribute + flushes the RX FIFO, clears the `irq_dr` status flag, and puts nRF24L01 in power up + mode. Notice the CE pin is be held HIGH during RX mode. + - `False` disables RX mode. As mentioned in above link, this puts nRF24L01's power in + Standby-I (CE pin is LOW meaning low current & no transmissions) mode which is ideal + for post-reception work. Disabing RX mode doesn't flush the RX/TX FIFO buffers, so + remember to flush your 3-level FIFO buffers when appropriate using `flush_tx()` or + `flush_rx()` (see also the `recv()` function). + +any() +****************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.any + + :returns: + - `int` of the size (in bytes) of an available RX payload (if any). + - ``0`` if there is no payload in the RX FIFO buffer. + +recv() +****************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.recv + + This function can also be used to fetch the last ACK packet's payload if `ack` is enabled. + + :param int length: An optional parameter to specify how many bytes to read from the RX + FIFO buffer. This parameter is not constrained in any way. + + - If this parameter is less than the length of the first available payload in the + RX FIFO buffer, then the payload will remain in the RX FIFO buffer until the + entire payload is fetched by this function. + - If this parameter is greater than the next available payload's length, then + additional data from other payload(s) in the RX FIFO buffer are returned. + + .. note:: + The nRF24L01 will repeatedly return the last byte fetched from the RX FIFO + buffer when there is no data to return (even if the RX FIFO is empty). Be + aware that a payload is only removed from the RX FIFO buffer when the entire + payload has been fetched by this function. Notice that this function always + starts reading data from the first byte of the first available payload (if + any) in the RX FIFO buffer. Remember the RX FIFO buffer can hold up to 3 + payloads at a maximum of 32 bytes each. + :returns: + If the ``length`` parameter is not specified, then this function returns `bytearray` + of the RX payload data or `None` if there is no payload. This also depends on the + setting of `dynamic_payloads` & `payload_length` attributes. Consider the following + two scenarios: + + - If the `dynamic_payloads` attribute is disabled, then the returned bytearray's + length is equal to the user defined `payload_length` attribute for the data pipe + that received the payload. + - If the `dynamic_payloads` attribute is enabled, then the returned bytearray's length + is equal to the payload's length + + When the ``length`` parameter is specified, this function strictly returns a `bytearray` + of that length despite the contents of the RX FIFO. + +send() +****************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.send + + :returns: + - `list` if a list or tuple of payloads was passed as the ``buf`` parameter. Each item + in the returned list will contain the returned status for each corresponding payload + in the list/tuple that was passed. The return statuses will be in one of the + following forms: + - `False` if transmission fails. Transmission failure can only be detected if `arc` + is greater than ``0``. + - `True` if transmission succeeds. + - `bytearray` or `True` when the `ack` attribute is `True`. Because the payload + expects a responding custom ACK payload, the response is returned (upon successful + transmission) as a `bytearray` (or `True` if ACK payload is empty). This functionality + can be bypassed by setting the ``send_only`` parameter as `True`. + + :param bytearray,bytes,list,tuple buf: The payload to transmit. This bytearray must have a + length in range [1, 32], otherwise a `ValueError` exception is thrown. This can + also be a list or tuple of payloads (`bytearray`); in which case, all items in the + list/tuple are processed for consecutive transmissions. + + - If the `dynamic_payloads` attribute is disabled for data pipe 0 and this bytearray's + length is less than the `payload_length` attribute for pipe 0, then this bytearray + is padded with zeros until its length is equal to the `payload_length` attribute for + pipe 0. + - If the `dynamic_payloads` attribute is disabled for data pipe 0 and this bytearray's + length is greater than `payload_length` attribute for pipe 0, then this bytearray's + length is truncated to equal the `payload_length` attribute for pipe 0. + :param bool ask_no_ack: Pass this parameter as `True` to tell the nRF24L01 not to wait for + an acknowledgment from the receiving nRF24L01. This parameter directly controls a + ``NO_ACK`` flag in the transmission's Packet Control Field (9 bits of information + about the payload). Therefore, it takes advantage of an nRF24L01 feature specific to + individual payloads, and its value is not saved anywhere. You do not need to specify + this for every payload if the `arc` attribute is disabled, however setting this + parameter to `True` will work despite the `arc` attribute's setting. + + .. note:: Each transmission is in the form of a packet. This packet contains sections + of data around and including the payload. `See Chapter 7.3 in the nRF24L01 + Specifications Sheet `_ for more + details. + :param int force_retry: The number of brute-force attempts to `resend()` a failed + transmission. Default is 0. This parameter has no affect on transmissions if `arc` is + ``0`` or ``ask_no_ack`` parameter is set to `True`. Each re-attempt still takes + advantage of `arc` & `ard` attributes. During multi-payload processing, this + parameter is meant to slow down CircuitPython devices just enough for the Raspberry + Pi to catch up (due to the Raspberry Pi's seemingly slower SPI speeds). See also + notes on `resend()` as using this parameter carries the same implications documented + there. This parameter has no effect if the ``ask_no_ack`` parameter is set to `True` + or if `arc` is disabled. + :param bool send_only: This parameter only applies when the `ack` attribute is set to + `True`. Pass this parameter as `True` if you want to handle fetching the ACK + payload (from the RX FIFO) seperately from the sending transmission that recieved + the ACK payload. Many other libraries' behave as though this parameter is `True` + (e.g. The popular TMRh20 Arduino RF24 library). Use `recv()` to get the ACK + payload (if there is any) from the RX FIFO. This parameter defaults to `False`. + Remember that the RX FIFO can only hold up to 3 payloads at once. + + .. tip:: It is highly recommended that `arc` attribute is enabled (greater than ``0``) + when sending multiple payloads. Test results with the `arc` attribute disabled were + rather poor (less than 79% received by a Raspberry Pi). This same advice applies to + the ``ask_no_ack`` parameter (leave it as `False` for multiple payloads). + .. warning:: The nRF24L01 will block usage of the TX FIFO buffer upon failed + transmissions. Failed transmission's payloads stay in TX FIFO buffer until the MCU + calls `flush_tx()` and `clear_status_flags()`. Therefore, this function will discard + failed transmissions' payloads when sending a list or tuple of payloads, so it can + continue to process through the list/tuple even if any payload fails to be + acknowledged. + diff --git a/docs/ble_api.rst b/docs/ble_api.rst new file mode 100644 index 0000000..3c08282 --- /dev/null +++ b/docs/ble_api.rst @@ -0,0 +1,405 @@ + +BLE Limitations +--------------- + +This module uses the `RF24` class to make the nRF24L01 imitate a +Bluetooth-Low-Emissions (BLE) beacon. A BLE beacon can send data (referred to as +advertisements) 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 has been adapted to work with 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 **18** bytes (when not + broadcasting a device + :py:attr:`~circuitpython_nrf24l01.fake_ble.FakeBLE.name` nor + the nRF24L01 + :py:attr:`~circuitpython_nrf24l01.fake_ble.FakeBLE.show_pa_level`). + This is calculated as: + + **32** (nRF24L01 maximum) - **6** (MAC address) - **5** (required + flags) - **3** (CRC checksum) = **18** + + Use the helper function + :py:func:`~circuitpython_nrf24l01.fake_ble.FakeBLE.available()` to + detirmine if your payload can be transmit. + 2. the channels that BLE use are limited to the following three: 2.402 + GHz, 2.426 GHz, and 2.480 GHz. We have provided a tuple of these + specific channels for convenience (See `BLE_FREQ` and `hop_channel()`). + 3. :py:attr:`~circuitpython_nrf24l01.rf24.RF24.crc` is disabled in the + nRF24L01 firmware because BLE specifications require 3 bytes + (:py:func:`~circuitpython_nrf24l01.fake_ble.crc24_ble()`), and the + nRF24L01 firmware can only handle a maximum of 2. + Thus, we have appended the required 3 bytes of CRC24 into the payload. + 4. :py:attr:`~circuitpython_nrf24l01.rf24.RF24.address_length` of BLE + packet only uses 4 bytes, so we have set that accordingly. + 5. The :py:attr:`~circuitpython_nrf24l01.rf24.RF24.auto_ack` (automatic + acknowledgment) feature of the nRF24L01 is useless when tranmitting to + BLE devices, thus it is disabled as well as automatic re-transmit + (:py:attr:`~circuitpython_nrf24l01.rf24.RF24.arc`) and custom ACK + payloads (:py:attr:`~circuitpython_nrf24l01.rf24.RF24.ack`) features + which both depend on the automatic acknowledgments feature. + 6. The :py:attr:`~circuitpython_nrf24l01.rf24.RF24.dynamic_payloads` + feature of the nRF24L01 isn't compatible with BLE specifications. Thus, + we have disabled it. + 7. BLE specifications only allow using 1 Mbps RF + :py:attr:`~circuitpython_nrf24l01.rf24.RF24.data_rate`, so that too has + been hard coded. + 8. Only the "on data sent" + (:py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_ds`) & "on data ready" + (:py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_dr`) events will have + an effect on the interrupt (IRQ) pin. The "on data fail" + (:py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_df`) is never + triggered because + :py:attr:`~circuitpython_nrf24l01.rf24.RF24.arc` attribute is disabled. + +helpers +---------------- + +swap_bits() +***************** + +.. autofunction:: circuitpython_nrf24l01.fake_ble.swap_bits + + :returns: + An `int` containing the byte whose bits are reversed + compared to the value passed to the ``original`` parameter. + :param int original: This should be a single unsigned byte, meaning the + parameters value can only range from 0 to 255. + +reverse_bits() +***************** + +.. autofunction:: circuitpython_nrf24l01.fake_ble.reverse_bits + + :returns: + A `bytearray` whose byte order remains the same, but each + byte's bit order is reversed. + :param bytearray,bytes original: The original buffer whose bits are to be + reversed. + +chunk() +***************** + +.. autofunction:: circuitpython_nrf24l01.fake_ble.chunk + + :param bytearray,bytes buf: The actual data contained in the block. + :param int data_type: The type of data contained in the chunk. This is a + predefined number according to BLE specifications. The default value + ``0x16`` describes all service data. ``0xFF`` describes manufacturer + information. Any other values are not applicable to BLE + advertisements. + + .. important:: This function is called internally by + :py:func:`~circuitpython_nrf24l01.fake_ble.FakeBLE.advertise()`. + To pack multiple data values into a single payload, use this function + for each data value and pass a `list` or `tuple` of the returned + results to + :py:func:`~circuitpython_nrf24l01.fake_ble.FakeBLE.advertise()` + (see example code in documentation about + :py:func:`~circuitpython_nrf24l01.fake_ble.FakeBLE.advertise()` + for more detail). Remember that broadcasting multiple data values may + require the :py:attr:`~circuitpython_nrf24l01.fake_ble.FakeBLE.name` + be set to `None` and/or the + :py:attr:`~circuitpython_nrf24l01.fake_ble.FakeBLE.show_pa_level` be + set to `False` for reasons about the payload size with + `BLE Limitations`_. + +crc24_ble() +***************** + +.. autofunction:: circuitpython_nrf24l01.fake_ble.crc24_ble + + This is exposed for convenience but should not be used for other buffer + protocols that require big endian CRC24 format. + + :param bytearray,bytes data: The buffer of data to be uncorrupted. + :param int deg_poly: A preset "degree polynomial" in which each bit + represents a degree who's coefficient is 1. BLE specfications require + ``0x00065b`` (default value). + :param int init_val: This will be the initial value that the checksum + will use while shifting in the buffer data. BLE specfications require + ``0x555555`` (default value). + :returns: A 24-bit `bytearray` representing the checksum of the data (in + proper little endian). + +BLE_FREQ +***************** + +.. autodata:: circuitpython_nrf24l01.fake_ble.BLE_FREQ + + This tuple contains the relative predefined channels used: + + * nRF channel 2 == BLE channel 37 + * nRF channel 26 == BLE channel 38 + * nRF channel 80 == BLE channel 39 + +FakeBLE class +------------- + +.. autoclass:: circuitpython_nrf24l01.fake_ble.FakeBLE + + Per the limitations of this technique, only some of underlying + :py:class:`~circuitpython_nrf24l01.rf24.RF24` functionality is + available for configuration when implementing BLE transmissions. + See the `Available RF24 functionality`_ for more details. + + + :param ~busio.SPI spi: The object for the SPI bus that the nRF24L01 is connected to. + + .. tip:: This object is meant to be shared amongst other driver classes (like + adafruit_mcp3xxx.mcp3008 for example) that use the same SPI bus. Otherwise, multiple + devices on the same SPI bus with different spi objects may produce errors or + undesirable behavior. + :param ~digitalio.DigitalInOut csn: The digital output pin that is connected to the nRF24L01's + CSN (Chip Select Not) pin. This is required. + :param ~digitalio.DigitalInOut ce: The digital output pin that is connected to the nRF24L01's + CE (Chip Enable) pin. This is required. + :param int spi_frequency: Specify which SPI frequency (in Hz) to use on the SPI bus. This + parameter only applies to the instantiated object and is made persistent via + :py:class:`~adafruit_bus_device.spi_device.SPIDevice`. + +to_iphone +************ + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.to_iphone + + A value of `False` should still be compatible with other + Apple devices. Testing with this attribute as `False` showed + compatibility with a Mac desktop. + +mac +************ + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.mac + + You can set this attribute using a 6-byte `int` or `bytearray`. If this is + set to `None`, then a random 6-byte address is generated. + +name +************ + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.name + + This is not required. In fact setting this attribute will subtract from + the available payload length (in bytes). Set this attribute to `None` to + disable advertising the device name. + + .. note:: This information occupies (in the TX FIFO) an extra 2 bytes plus + the length of the name set by this attribute. + +show_pa_level +************* + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.show_pa_level + + The default value of `False` will exclude this optional information. + + .. note:: This information occupies (in the TX FIFO) an extra 3 bytes, and is + really only useful for some applications to calculate proximity to the + nRF24L01 transceiver. + +hop_channel() +************* + +.. automethod:: circuitpython_nrf24l01.fake_ble.FakeBLE.hop_channel + +whiten() +************* + +.. automethod:: circuitpython_nrf24l01.fake_ble.FakeBLE.whiten + + This is done according to BLE specifications. + + :param bytearray,bytes data: The packet to whiten. + :returns: A `bytearray` of the ``data`` with the whitening algorythm + applied. + + .. warning:: This function uses the currently set BLE channel as a + base case for the whitening coefficient. Do not call + `hop_channel()` before using this function to de-whiten received + payloads (which isn't officially supported yet). Note that + `advertise()` uses this function internally to prevent such + improper usage. + +available() +************* + +.. automethod:: circuitpython_nrf24l01.fake_ble.FakeBLE.available + + This is detirmined from the current state of `name` and `show_pa_level` + attributes. + + :param bytearray,bytes hypothetical: Pass a potential `chunk()` of + data to this parameter to calculate the resulting left over length + in bytes. This parameter is optional. + :returns: An `int` representing the length of available bytes for the + a single payload. + +advertise() +************* + +.. automethod:: circuitpython_nrf24l01.fake_ble.FakeBLE.advertise + + :returns: Nothing as every transmission will register as a success + under the required settings for BLE beacons. + + :param bytearray buf: The payload to transmit. This bytearray must have + a length greater than 0 and less than 22 bytes Otherwise a + `ValueError` exception is thrown whose prompt will tell you the + maximum length allowed under the current configuration. This can + also be a list or tuple of payloads (`bytearray`); in which case, + all items in the list/tuple are processed are packed into 1 + payload for a single transmissions. See example code below about + passing a `list` or `tuple` to this parameter. + :param int data_type: This is used to describe the buffer data passed + to the ``buf`` parameter. ``0x16`` describes all service data. The + default value ``0xFF`` describes manufacturer information. This + parameter is ignored when a `tuple` or `list` is passed to the + ``buf`` parameter. Any other values are not applicable to BLE + advertisements. + + .. important:: If the name and/or TX power level of the emulated BLE + device is also to be broadcast, then the `name` and/or + `show_pa_level` attribute(s) should be set prior to calling + `advertise()`. + + To pass multiple data values to the ``buf`` parameter see the + following code as an example: + + .. code-block:: python + + # let UUIDs be the 16-bit identifier that corresponds to the + # BLE service type. The following values are not compatible with + # BLE advertisements. + UUID_1 = 0x1805 + UUID_2 = 0x1806 + service1 = ServiceData(UUID_1) + service2 = ServiceData(UUID_2) + service1.data = b"some value 1" + service2.data = b"some value 2" + + # make a tuple of the buffers + buffers = ( + chunk(service1.buffer), + chunk(service2.buffer) + ) + + # let `ble` be the instantiated object of the FakeBLE class + ble.advertise(buffers) + ble.hop_channel() + +Available RF24 functionality +***************************** + +pa_level +#################### + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.pa_level + +channel +#################### + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.channel + +payload_length +#################### + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.payload_length + +power +#################### + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.power + +is_lna_enabled +#################### + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.is_lna_enabled + +is_plus_variant +#################### + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.is_plus_variant + +interrupt_config() +#################### + +.. automethod:: circuitpython_nrf24l01.fake_ble.FakeBLE.interrupt_config() + +irq_ds +#################### + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.irq_ds + +irq_dr +#################### + +.. autoattribute:: circuitpython_nrf24l01.fake_ble.FakeBLE.irq_dr + +clear_status_flags() +#################### + +.. automethod:: circuitpython_nrf24l01.fake_ble.FakeBLE.clear_status_flags() + +update() +#################### + +.. automethod:: circuitpython_nrf24l01.fake_ble.FakeBLE.update() + +what_happened() +#################### + +.. automethod:: circuitpython_nrf24l01.fake_ble.FakeBLE.what_happened() + + +Service related classes +----------------------- + +abstract parent +*************** + +.. autoclass:: circuitpython_nrf24l01.fake_ble.ServiceData + :members: + :special-members: __len__ + + :param int uuid: The 16-bit UUID `"GATT Service assigned number" + `_ defined by the + Bluetooth SIG to describe the service data. This parameter is + required. + +derivitive children +******************* + +.. autoclass:: circuitpython_nrf24l01.fake_ble.TemperatureServiceData + :show-inheritance: + + This class's `data` attribute accepts a `float` value as + input and returns a `bytes` object that conforms to the Bluetooth + Health Thermometer Measurement format as defined in the `GATT + Specifications Supplement. `_ + +.. autoclass:: circuitpython_nrf24l01.fake_ble.BatteryServiceData + :show-inheritance: + + The class's `data` attribute accepts a `int` value as + input and returns a `bytes` object that conforms to the Bluetooth + Battery Level format as defined in the `GATT Specifications + Supplement. `_ + +.. autoclass:: circuitpython_nrf24l01.fake_ble.UrlServiceData + :members: pa_level_at_1_meter + :show-inheritance: + + This class's `data` attribute accepts a `str` of URL data as input, and + returns the URL as a `bytes` object where some of the URL parts are + encoded using `Eddystone byte codes as defined by the specifications. + `_ \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index d73afd3..4927412 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,168 +1,197 @@ -# -*- coding: utf-8 -*- - -import os -import sys -sys.path.insert(0, os.path.abspath('..')) - -# -- General configuration ------------------------------------------------ - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.napoleon', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', -] - -# TODO: Please Read! -# Uncomment the below if you use native CircuitPython modules such as -# digitalio, micropython and busio. List the modules you use. Without it, the -# autodoc module docs will fail to generate with a warning. -autodoc_mock_imports = ["digitalio", "busio"] -autodoc_member_order = 'bysource' - -intersphinx_mapping = {'python': ('https://docs.python.org/3.4', None),'BusDevice': ('https://circuitpython.readthedocs.io/projects/busdevice/en/latest/', None),'CircuitPython': ('https://circuitpython.readthedocs.io/en/latest/', None)} - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'nRF24L01 Library' -copyright = u'2019 Brendan Doherty' -author = u'Brendan Doherty' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'1.0' -# The full version, including alpha/beta/rc tags. -release = u'1.0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.env', 'CODE_OF_CONDUCT.md'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# -default_role = "any" - -# If true, '()' will be appended to :func: etc. cross-reference text. -# -add_function_parentheses = True - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - -# If this is True, todo emits a warning for each TODO entries. The default is False. -todo_emit_warnings = False - -napoleon_numpy_docstring = False - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - -if not on_rtd: # only import and set the theme if we're building docs locally - try: - import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' - html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), '.'] - except: - html_theme = 'default' - html_theme_path = ['.'] -else: - html_theme_path = ['.'] - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# These paths are either relative to html_static_path -# or fully qualified paths (eg. https://...) -html_css_files = [ - 'darkness.css', -] -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# -html_favicon = '_static/favicon.ico' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'nRF24L01_Library_doc' -# html_copy_source = True -# html_show_sourcelink = True - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'nRF24L01Library.tex', u'nRF24L01 Library Documentation', - author, 'manual'), -] - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'nRF24L01library', u'nRF24L01 Library Documentation', - [author], 1) -] - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'nRF24L01Library', u' nRF24L01 Library Documentation', - author, 'nRF24L01Library', 'nRF24L01 on CircuitPython devices.', - 'Wireless'), -] +# -*- coding: utf-8 -*- +# pylint: disable=invalid-name +"""This file is for `sphinx-build` configuration""" +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) + +# -- General configuration ------------------------------------------------ + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + # "rst2pdf.pdfbuilder", # for pdf builder support +] + +# TODO: Please Read! +# Uncomment the below if you use native CircuitPython modules such as +# digitalio, micropython and busio. List the modules you use. Without it, the +# autodoc module docs will fail to generate with a warning. +autodoc_mock_imports = ["digitalio", "busio"] +autodoc_member_order = "bysource" + +intersphinx_mapping = { + "python": ("https://docs.python.org/3.4", None), + "BusDevice": ( + "https://circuitpython.readthedocs.io/projects/busdevice/en/latest/", + None, + ), + "CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None), +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +source_suffix = ".rst" + +# The master toctree document. +master_doc = "index" + +# General information about the project. +# pylint: disable=redefined-builtin +copyright = u"2019 Brendan Doherty" +# pylint: enable=redefined-builtin +project = u"nRF24L01 Library" +author = u"Brendan Doherty" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u"1.2" +# The full version, including alpha/beta/rc tags. +release = u"1.2" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", ".env", "CODE_OF_CONDUCT.md"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +default_role = "any" + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + +# If this is True, todo emits a warning for each TODO entries. The default is False. +todo_emit_warnings = False + +napoleon_numpy_docstring = False + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +on_rtd = os.environ.get("READTHEDOCS", None) == "True" + +if not on_rtd: # only import and set the theme if we're building docs locally + try: + import sphinx_rtd_theme + + html_theme = "sphinx_rtd_theme" + html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."] + except ImportError: + html_theme = "default" + html_theme_path = ["."] +else: + html_theme_path = ["."] + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +# These paths are either relative to html_static_path +# or fully qualified paths (eg. https://...) +html_css_files = [ + "darkness.css", +] +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +html_favicon = "_static/favicon.ico" + +# Output file base name for HTML help builder. +htmlhelp_basename = "nRF24L01_Library_doc" +# html_copy_source = True +# html_show_sourcelink = True + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + # + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + # + # Additional stuff for the LaTeX preamble. + # 'preamble': '', + # + # Latex figure (float) alignment + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ( + master_doc, + "nRF24L01Library.tex", + u"nRF24L01 Library Documentation", + author, + "manual", + ), +] + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, "nRF24L01library", u"nRF24L01 Library Documentation", [author], 1) +] + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + "nRF24L01Library", + u" nRF24L01 Library Documentation", + author, + "nRF24L01Library", + "nRF24L01 on CircuitPython devices.", + "Wireless", + ), +] + +# ---Options for PDF output----------------------------------------- +# requires `rst2pdf` module which is not builtin to Python 3.4 nor +# readthedocs.org's docker) +# pdf_documents = [ +# ( +# "index", +# u"CircuitPython-nRF24L01", +# u"CircuitPython-nRF24L01 library documentation", +# u"Brendan Doherty", +# ), +# ] diff --git a/docs/configure_api.rst b/docs/configure_api.rst new file mode 100644 index 0000000..89d20c0 --- /dev/null +++ b/docs/configure_api.rst @@ -0,0 +1,237 @@ + +Configuration API +----------------- + +CSN_DELAY +****************************** + +.. autodata:: circuitpython_nrf24l01.rf24.CSN_DELAY + +dynamic_payloads +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.dynamic_payloads + + Default setting is enabled on all pipes. + + - `True` or ``1`` enables nRF24L01's dynamic payload length feature for all data pipes. The + `payload_length` attribute is ignored when this feature is enabled for all + respective data pipes. + - `False` or ``0`` disables nRF24L01's dynamic payload length feature for all data pipes. + Be sure to adjust the `payload_length` attribute accordingly when this feature is + disabled for any data pipes. + - A `list` or `tuple` containing booleans or integers can be used control this feature per + data pipe. Index 0 controls this feature on data pipe 0. Indices greater than 5 will be + ignored since there are only 6 data pipes. If any index's value is less than 0 (a + negative value), then the pipe corresponding to that index will remain unaffected. + + .. note:: + This attribute mostly relates to RX operations, but data pipe 0 applies to TX + operations also. The `auto_ack` attribute is set accordingly for data pipes that + have this feature enabled. Disabling this feature for any data pipe will not + affect the `auto_ack` feature for the corresponding data pipes. + +payload_length +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.payload_length + + If the `dynamic_payloads` attribute is enabled for a certain data pipe, this attribute has + no affect on that data pipe. When `dynamic_payloads` is disabled for a certain data pipe, + this attribute is used to specify the payload length on that data pipe in RX mode. + + A valid input value must be: + + * an `int` in range [1, 32]. Otherwise a `ValueError` exception is thrown. + * a `list` or `tuple` containing integers can be used to control this attribute per + data pipe. Index 0 controls this feature on data pipe 0. Indices greater than 5 will + be ignored since there are only 6 data pipes. if a index's value is ``0``, then the + existing setting will persist (not be changed). + + Default is set to the nRF24L01's maximum of 32 (on all data pipes). + + .. note:: + This attribute mostly relates to RX operations, but data pipe 0 applies to TX + operations also. + +auto_ack +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.auto_ack + + Default setting is enabled on all data pipes. + + - `True` or ``1`` enables transmitting automatic acknowledgment packets for all data pipes. + The CRC (cyclic redundancy checking) is enabled automatically by the nRF24L01 if the + `auto_ack` attribute is enabled for any data pipe (see also `crc` attribute). + - `False` or ``0`` disables transmitting automatic acknowledgment packets for all data + pipes. The `crc` attribute will remain unaffected when disabling this attribute for any + data pipes. + - A `list` or `tuple` containing booleans or integers can be used control this feature per + data pipe. Index 0 controls this feature on data pipe 0. Indices greater than 5 will be + ignored since there are only 6 data pipes. If any index's value is less than 0 (a + negative value), then the pipe corresponding to that index will remain unaffected. + + .. note:: + This attribute mostly relates to RX operations, but data pipe 0 applies to TX + operations also. + +arc +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.arc + + The `auto_ack` attribute must be enabled on the receiving nRF24L01 respective data pipe, + otherwise this attribute will make `send()` seem like it failed. + + A valid input value must be in range [0, 15]. Otherwise a `ValueError` exception is thrown. + Default is set to 3. A value of ``0`` disables the automatic re-transmit feature and + considers all payload transmissions a success. + +ard +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.ard + + During this time, the nRF24L01 is listening for the ACK packet. If the + `auto_ack` attribute is disabled, this attribute is not applied. + + A valid input value must be in range [250, 4000]. Otherwise a `ValueError` exception is + thrown. Default is 1500 for reliability. If this is set to a value that is not multiple of + 250, then the highest multiple of 250 that is no greater than the input value is used. + + .. note:: Paraphrased from nRF24L01 specifications sheet: + + Please take care when setting this parameter. If the custom ACK payload is more than 15 + bytes in 2 Mbps data rate, the `ard` must be 500µS or more. If the custom ACK payload + is more than 5 bytes in 1 Mbps data rate, the `ard` must be 500µS or more. In 250kbps + data rate (even when there is no custom ACK payload) the `ard` must be 500µS or more. + + See `data_rate` attribute on how to set the data rate of the nRF24L01's transmissions. + +ack +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.ack + + Use this attribute to set/check if the custom ACK payloads feature is enabled. Default + setting is `False`. + + - `True` enables the use of custom ACK payloads in the ACK packet when responding to + receiving transmissions. + - `False` disables the use of custom ACK payloads in the ACK packet when responding to + receiving transmissions. + + .. important:: + As `dynamic_payloads` and `auto_ack` attributes are required for this feature to work, + they are automatically enabled (on data pipe 0) as needed. However, it is required to + enable the `auto_ack` and `dynamic_payloads` features on all applicable pipes. + Disabling this feature does not disable the `auto_ack` and `dynamic_payloads` + attributes for any data pipe; they work just fine without this feature. + +interrupt_config() +****************************** + +.. automethod:: circuitpython_nrf24l01.rf24.RF24.interrupt_config + + The digital signal from the nRF24L01's IRQ pin is active LOW. (write-only) + + :param bool data_recv: If this is `True`, then IRQ pin goes active when there is new data + to read in the RX FIFO buffer. Default setting is `True` + :param bool data_sent: If this is `True`, then IRQ pin goes active when a payload from TX + buffer is successfully transmit. Default setting is `True` + :param bool data_fail: If this is `True`, then IRQ pin goes active when maximum number of + attempts to re-transmit the packet have been reached. If `auto_ack` attribute is + disabled, then this IRQ event is not used. Default setting is `True` + + .. note:: To fetch the status (not configuration) of these IRQ flags, use the `irq_df`, + `irq_ds`, `irq_dr` attributes respectively. + + .. tip:: Paraphrased from nRF24L01+ Specification Sheet: + + The procedure for handling ``data_recv`` IRQ should be: + + 1. read payload through `recv()` + 2. clear ``dataReady`` status flag (taken care of by using `recv()` in previous step) + 3. read FIFO_STATUS register to check if there are more payloads available in RX FIFO + buffer. A call to `pipe` (may require `update()` to be called), `any()` or even + ``(False,True)`` as parameters to `fifo()` will get this result. + 4. if there is more data in RX FIFO, repeat from step 1 + +data_rate +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.data_rate + + A valid input value is: + + - ``1`` sets the frequency data rate to 1 Mbps + - ``2`` sets the frequency data rate to 2 Mbps + - ``250`` sets the frequency data rate to 250 Kbps (see warning below) + + Any invalid input throws a `ValueError` exception. Default is 1 Mbps. + + .. warning:: 250 Kbps is not available for the non-plus models of the + nRF24L01 product line. Trying to set the data rate to 250 kpbs when + `is_plus_variant` is `True` will throw a `NotImplementedError`. + +channel +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.channel + + 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`` (2.476 GHz). + +crc +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.crc + + CRC is a way of making sure that the transmission didn't get corrupted over the air. + + A valid input value must be: + + - ``0`` disables CRC (no anti-corruption of data) + - ``1`` enables CRC encoding scheme using 1 byte (weak anti-corruption of data) + - ``2`` enables CRC encoding scheme using 2 bytes (better anti-corruption of data) + + Any invalid input throws a `ValueError` exception. Default is enabled using 2 bytes. + + .. note:: The nRF24L01 automatically enables CRC if automatic acknowledgment feature is + enabled (see `auto_ack` attribute). + +pa_level +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.pa_level + + Higher levels mean the transmission will cover a longer distance. Use this attribute to + tweak the nRF24L01 current consumption on projects that don't span large areas. + + A valid input value is: + + - ``-18`` sets the nRF24L01's power amplifier to -18 dBm (lowest) + - ``-12`` sets the nRF24L01's power amplifier to -12 dBm + - ``-6`` sets the nRF24L01's power amplifier to -6 dBm + - ``0`` sets the nRF24L01's power amplifier to 0 dBm (highest) + + If this attribute is set to a `list` or `tuple`, then the list/tuple must contain the + desired power amplifier level (from list above) at index 0 and a `bool` to control + the Low Noise Amplifier (LNA) feature at index 1. All other indices will be discarded. + + .. note:: + The LNA feature only applies to the nRF24L01 (non-plus variant). This + includes boards with the RFX24C01-based PA/LNA muxing IC attached to an + SMA-type detachable antenna. + + Any invalid input will invoke the default of 0 dBm with LNA enabled. + +is_lna_enabled +****************************** + +.. autoattribute:: circuitpython_nrf24l01.rf24.RF24.is_lna_enabled + + See `pa_level` attribute about how to set this. Default is always enabled, but this + feature is specific to non-plus variants of nRF24L01 transceivers. Use + `is_plus_variant` to see if it can toggle the Low Noise Amplifier feature. diff --git a/docs/examples.rst b/docs/examples.rst index b044e2b..1475c19 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -17,15 +17,6 @@ This is a test to show how to use custom acknowledgment payloads. :caption: examples/nrf24l01_ack_payload_test.py :linenos: -IRQ Pin Example ---------------- - -This is a test to show how to use nRF24L01's interrupt pin. - -.. literalinclude:: ../examples/nrf24l01_interrupt_test.py - :caption: examples/nrf24l01_interrupt_test.py - :linenos: - Stream Example --------------- @@ -35,6 +26,15 @@ This is a test to show how to use the send() to transmit multiple payloads. :caption: examples/nrf24l01_stream_test.py :linenos: +Fake BLE Example +---------------- + +This is a test to show how to use the nRF24L01 as a BLE advertising beacon. + +.. literalinclude:: ../examples/nrf24l01_fake_ble_test.py + :caption: examples/nrf24l01_fake_ble_test.py + :linenos: + Context Example --------------- @@ -47,8 +47,17 @@ This is a test to show how to use "with" statements to manage multiple different 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". +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". If you changed the ``role`` variable in the TMRh20 sketch, you will have to adjust the addresses assigned to the pipes in this script. .. literalinclude:: ../examples/nrf24l01_2arduino_handling_data.py :caption: examples/nrf24l01_2arduino_handling_data.py :linenos: + +IRQ Pin Example +--------------- + +This is a test to show how to use nRF24L01's interrupt pin. Be aware that `send()` clears all IRQ events on exit, so we use the non-blocking `write()` instead. Also the `ack` attribute is enabled to trigger the :py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_dr` event when the master node receives ACK payloads. Simply put, this example is the most advanced example script (in this library), and it runs VERY quickly. + +.. literalinclude:: ../examples/nrf24l01_interrupt_test.py + :caption: examples/nrf24l01_interrupt_test.py + :linenos: diff --git a/docs/greetings.rst b/docs/greetings.rst new file mode 100644 index 0000000..09b9308 --- /dev/null +++ b/docs/greetings.rst @@ -0,0 +1,254 @@ + +Getting Started +================== + +This is a Circuitpython driver library for the nRF24L01(+) transceiver. + +Originally this code was a Micropython module written by Damien P. George +& Peter Hinch which can still be found `here +`_ + +The Micropython source has since been rewritten to expose all the nRF24L01's +features and for Circuitpython compatible devices (including linux-based +SoC computers like the Raspberry Pi). +Modified by Brendan Doherty & Rhys Thomas. + +* Authors: Damien P. George, Peter Hinch, Rhys Thomas, Brendan Doherty + +Features currently supported +---------------------------- + +* Change the address's length (can be 3 to 5 bytes long) +* Dynamically sized payloads (max 32 bytes each) or statically sized payloads +* Automatic responding acknowledgment (ACK) packets for verifying transmission success +* Append custom payloadsto the acknowledgment (ACK) packets for instant bi-directional communication +* Mark a single payload for no acknowledgment (ACK) from the receiving nRF24L01 (see ``ask_no_ack`` parameter for `send()` and `write()` functions) +* Invoke the "re-use the same payload" feature (for manually re-transmitting failed transmissions that + remain in the TX FIFO buffer) +* Multiple payload transmissions with one function call (MUST read documentation on the + `send()` function) +* Context manager compatible for easily switching between different radio configurations + using `with` blocks (not available in ``rf24_lite.py`` version) +* Configure the interrupt (IRQ) pin to trigger (active low) on received, sent, and/or + failed transmissions (these 3 events control 1 IRQ pin). There's also virtual + representations of these interrupt events available (see + :py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_dr`, + :py:attr:`~circuitpython_nrf24l01.rf24.RF24.irq_ds`, & `irq_df` attributes) +* Invoke sleep mode (AKA power down mode) for ultra-low current consumption +* cyclic redundancy checking (CRC) up to 2 bytes long +* Adjust the nRF24L01's builtin automatic re-transmit feature's parameters (`arc`: number + of attempts, `ard`: delay between attempts) +* 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, 1Mbps, or 2Mbps) +* An 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 `_ example. +* fake BLE module for sending BLE beacon advertisments from the nRF24L01 as outlined by `Dmitry Grinberg in his write-up (including C source code) `_. + +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" (`ard`) and "automatic retry count" (`arc`) attributes set accordingly (varyingly high), but this has not been tested. + +Dependencies +-------------------------- + +This driver depends on: + +* `Adafruit CircuitPython `_ +* `Bus Device `_ (specifically the :py:mod:`~adafruit_bus_device.spi_device`) + +Please ensure all dependencies are available on the CircuitPython filesystem. +This is easily achieved by downloading +`the Adafruit library and driver bundle `_. + +.. note:: This library supports Python 3.4 or newer, but Python 3.7 introduced + the function `time.monotonic_ns() `_ which returns an arbitrary time "counter" + as an `int` of nanoseconds. However, this function is not used in the + example scripts for backward compatibility reasons. Instead, we used + :py:func:`~time.monotonic()` which returns an arbitrary time "counter" as + a `float` of seconds. CircuitPython firmware supports both functions as of + v4.0. + +Installing from PyPI +-------------------- + +On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from +PyPI `_. To install for current user: + +.. code-block:: shell + + pip3 install circuitpython-nrf24l01 + +To install system-wide (this may be required in some cases): + +.. code-block:: shell + + sudo pip3 install circuitpython-nrf24l01 + +To install in a virtual environment in your current project: + +.. code-block:: shell + + mkdir project-name && cd project-name + python3 -m venv .env + source .env/bin/activate + pip3 install circuitpython-nrf24l01 + +Pinout +====== +.. 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 library's `example directory `_. + +.. csv-table:: + :header: nRF2401, "Raspberry Pi", "ItsyBitsy M4" + + GND, GND, GND + VCC, 3V, 3.3V + CE, GPIO4, D4 + CSN, GPIO5, D5 + SCK, "GPIO11 (SCK)", SCK + MOSI, "GPIO10 (MOSI)", MOSI + MISO, "GPIO9 (MISO)", MISO + IRQ, GPIO12, D12 + +.. tip:: User reports and personal experiences have improved results if there is a capacitor of 100 mirofarads [+ another optional 0.1 microfarads capacitor for added stability] connected in parrallel to the VCC and GND pins. + +Using The Examples +================== + +See `examples `_ for testing certain features of this the library. The examples were developed and tested on both Raspberry Pi and ItsyBitsy M4. Pins have been hard coded in the examples for the corresponding device, so please adjust these accordingly to your circuitpython device if necessary. + +To run the simple example, navigate to this repository's "examples" folder in the terminal. If you're working with a CircuitPython device (not a Raspberry Pi), copy the file named "nrf24l01_simple_test.py" from this repository's "examples" folder to the root directory of your CircuitPython device's CIRCUITPY drive. Now you're ready to open a python REPR and run the following commands: + +.. code-block:: python + + >>> from nrf24l01_simple_test import * + nRF24L01 Simple test. + Run slave() on receiver + Run master() on transmitter + >>> master() + Sending: 5 as struct: b'\x05\x00\x00\x00' + send() successful + Transmission took 36.0 ms + Sending: 4 as struct: b'\x04\x00\x00\x00' + send() successful + Transmission took 28.0 ms + Sending: 3 as struct: b'\x03\x00\x00\x00' + send() successful + Transmission took 24.0 ms + + +What to purchase +================= + +See the store links on the sidebar or just google "nRF24L01+". It is worth noting that you +generally want to buy more than 1 as you need 2 for testing -- 1 to send & 1 to receive and +vise versa. This library has been tested on a cheaply bought 6 pack from Amazon.com, but don't +take Amazon or eBay for granted! There are other wireless transceivers that are NOT compatible +with this library. For instance, the esp8266-01 (also sold in packs) is NOT compatible with +this library, but looks very similar to the nRF24L01+ and could lead to an accidental purchase. + +Power Stability +------------------- + +If you're not using a dedicated 3V regulator to supply power to the nRF24L01, +then adding capcitor(s) (100 µF + an optional 0.1µF) in parrellel (& as close +as possible) to the VCC and GND pins is highly recommended. Stablizing the power +input provides significant performance increases. More finite details about the +nRF24L01 are available from the datasheet (referenced here in the documentation as the +`nRF24L01+ Specification Sheet `_) + +About the nRF24L01+PA+LNA modules +--------------------------------- + +You may find variants of the nRF24L01 transceiver that are marketed as "nRF24L01+PA+LNA". +These modules are distinct in the fact that they come with a detachable (SMA-type) antenna. +They employ seperate RFX24C01 IC with the antenna for enhanced Power Amplification (PA) and +Low Noise Amplification (LNA) features. While they boast greater range with the same +functionality, they are subject to a couple lesser known (and lesser advertised) drawbacks: + +1. Stronger power source. Below is a chart of advertised current requirements that many MCU + boards' 3V regulators may not be able to provide (after supplying power to internal + components). + + .. csv-table:: + :header: Specification, Value + :widths: 10,5 + + "Emission mode current(peak)", "115 mA" + "Receive Mode current(peak)", "45 mA" + "Power-down mode current", "4.2 µA" +2. Needs shielding from electromagnetic interference. Shielding usually works best when + it has a path to ground (GND pin), but this connection to the GND pin is not required. + +See also the `Testing nRF24L01+PA+LNA module `_ + +nRF24L01(+) clones and counterfeits +----------------------------------- + +This library does not directly support clones/counterfeits as there is no way for the library +to differentiate between an actual nRF24L01+ and a clone/counterfeit. To determine if your +purchase is a counterfeit, please contact the retailer you purchased from (also `reading this +article and its links might help +`_). The most notable clone is the `Si24R1 `_. I could not find +the `Si24R1 datasheet `_ in english. Troubleshooting +the SI24R1 may require `replacing the onboard antenna with a wire +`_. Furthermore, the Si24R1 has different power +amplifier options as noted in the `RF_PWR section (bits 0 through 2) of the RF_SETUP register +(address 0x06) of the datasheet `_. +While the options' values differ from those identified by this library's API, the +underlying commands to configure those options are almost identical to the nRF24L01. Other +known clones include the bk242x (also known as RFM7x). + +Contributing +============ + +Contributions are welcome! Please read our `Code of Conduct +`_ +before contributing to help this project stay welcoming. To contribute, all you need to do is fork `this repository `_, develop your idea(s) and submit a pull request when stable. To initiate a discussion of idea(s), you need only open an issue on the aforementioned repository (doesn't have to be a bug report). + + +Future Project Ideas/Additions +------------------------------ + + The following are only ideas; they are not currently supported by this circuitpython library. + + * `There's a few blog posts by Nerd Ralph demonstrating how to use the nRF24L01 via 2 or 3 + pins `_ (uses custom bitbanging SPI functions and an external circuit involving a + resistor and a capacitor) + * network linking layer, maybe something like `TMRh20's RF24Network + `_ + * implement the Gazelle-based protocol used by the BBC micro-bit (`makecode.com's radio + blocks `_). + + +Sphinx documentation +----------------------- + +Sphinx is used to build the documentation based on rST files and comments in the code. First, +install dependencies (feel free to reuse the virtual environment from `above `_): + +.. code-block:: shell + + python3 -m venv .env + source .env/bin/activate + pip install Sphinx sphinx-rtd-theme + +Now, once you have the virtual environment activated: + +.. code-block:: shell + + cd docs + sphinx-build -E -W -b html . _build + +This will output the documentation to ``docs/_build``. Open the index.html in your browser to +view them. It will also (due to -W) error out on any warning like the Github action, Build CI, +does. This is a good way to locally verify it will pass. diff --git a/docs/index.rst b/docs/index.rst index 46a5a3c..6b9304a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,13 +1,12 @@ -.. include:: ../README.rst Table of Contents ================= .. toctree:: + :caption: Introduction :maxdepth: 4 - :hidden: - self + greetings .. toctree:: :caption: Examples @@ -15,10 +14,29 @@ Table of Contents examples .. toctree:: - :caption: API Reference + :caption: Troubleshooting :maxdepth: 5 - api + troubleshooting + +.. toctree:: + :caption: RF24 API Reference + + basic_api + + +.. toctree:: + + advanced_api + +.. toctree:: + + configure_api + +.. toctree:: + :caption: BLE API Reference + + ble_api .. toctree:: :caption: Store Links diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst new file mode 100644 index 0000000..e965e42 --- /dev/null +++ b/docs/troubleshooting.rst @@ -0,0 +1,170 @@ + +Troubleshooting info +==================== + +.. important:: The nRF24L01 has 3 key features that can be interdependent of each other. Their + priority of dependence is as follows: + + 1. `auto_ack` feature provides transmission verification by using the RX nRF24L01 to + automatically and imediatedly send an acknowledgment (ACK) packet in response to + received payloads. `auto_ack` does not require `dynamic_payloads` to be enabled. + 2. `dynamic_payloads` feature allows 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 + :py:attr:`~circuitpython_nrf24l01.rf24.RF24.payload_length` attributes. For + `dynamic_payloads` to be enabled, the `auto_ack` feature must be enabled. Although, + the `auto_ack` feature can be used when the `dynamic_payloads` feature is disabled. + 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 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). + +With the `auto_ack` feature enabled, you get: + + * cyclic 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 "parking spot" for your payload. There are only six + data pipes on the nRF24L01, thus it can simultaneously "listen" to a maximum of 6 other + nRF24L01 radios. However, it can only "talk" to 1 other nRF24L01 at a time). + + The specified address is not the address of an nRF24L01 radio, rather it is more like a + path that connects the endpoints. When assigning addresses to a data pipe, you can use any + 5 byte long address you can think of (as long as the first byte is unique among + simultaneously broadcasting addresses), so you're not limited to communicating with only + the same 6 nRF24L01 radios (more on this when we officially support "Multiciever" mode). + + Finnaly, 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, or other ambient signals that use the same spectrum of + frequencies. + +.. 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 (passed to `open_rx_pipe()`) MUST match + the TX pipe's address on the transmitting nRF24L01 (passed to `open_tx_pipe()`) + * `address_length` + * :py:attr:`~circuitpython_nrf24l01.rf24.RF24.channel` + * :py:attr:`~circuitpython_nrf24l01.rf24.RF24.data_rate` + * `dynamic_payloads` + * :py:attr:`~circuitpython_nrf24l01.rf24.RF24.payload_length` only when `dynamic_payloads` + is disabled + * `auto_ack` on the recieving nRF24L01 must be enabled if `arc` is greater than 0 on the + transmitting nRF24L01 + * 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()` or `load_ack()`), + :py:attr:`~circuitpython_nrf24l01.rf24.RF24.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). + +About the lite version +====================== + +This library contains a "lite" version of ``rf24.py`` titled ``rf24_lite.py``. It has been +developed to save space on microcontrollers with limited amount of RAM and/or storage (like +boards using the ATSAMD21 M0). The following functionality has been removed from the lite +version: + + * The `FakeBLE` class is not compatible with the ``rf24_lite.py`` module. + * :py:attr:`~circuitpython_nrf24l01.rf24.RF24.is_plus_variant` is removed, meaning the + lite version is not compatibility with the older non-plus variants of the nRF24L01. + * `address()` removed. + * :py:func:`~circuitpython_nrf24l01.rf24.RF24.what_happened()` removed. However you can + use the following function to dump all available registers' values (for advanced users): + + .. code-block:: python + + # let `nrf` be the instantiated RF24 object + def dump_registers(end=0x1e): + for i in range(end): + if i in (0xA, 0xB, 0x10): + print(hex(i), "=", nrf._reg_read_bytes(i)) + elif i not in (0x18, 0x19, 0x1a, 0x1b): + print(hex(i), "=", hex(nrf._reg_read(i))) + * `dynamic_payloads` applies to all pipes, not individual pipes. + * :py:attr:`~circuitpython_nrf24l01.rf24.RF24.payload_length` applies to all pipes, not + individual pipes. + * `read_ack()` removed. This is deprecated on next major release anyway; use `recv()` + instead. + * `load_ack()` is available, but it will not throw exceptions for malformed ``buf`` or + invalid ``pipe_number`` parameters. + * `crc` removed. 2-bytes encoding scheme (CRC16) is always enabled. + * `auto_ack` removed. This is always enabled for all pipes. Pass ``ask_no_ack`` parameter + as `True` to `send()` or `write()` to disable automatic acknowledgement for TX + operations. + * :py:attr:`~circuitpython_nrf24l01.rf24.RF24.is_lna_enabled` removed as it only affects + non-plus variants of the nRF24L01. + * :py:attr:`~circuitpython_nrf24l01.rf24.RF24.pa_level` is available, but it will not + accept a `list` or `tuple`. + * `rpd`, `start_carrier_wave()`, & `stop_carrier_wave()` removed. These only perform a + test of the nRF24L01's hardware. + * `CSN_DELAY` removed. This is hard-coded to 5 milliseconds + * All comments and docstrings removed, meaning ``help()`` will not provide any specific + information. Exception prompts have also been reduced and adjusted accordingly. + * Cannot switch between different radio configurations using context manager (the `with` + blocks). It is advised that only one `RF24` object be instantiated when RAM is limited + (less than or equal to 32KB). + + +Testing nRF24L01+PA+LNA module +================================= + +The following are semi-successful test results using a nRF24L01+PA+LNA module: + +The Setup +********************************* + + I wrapped the PA/LNA module with electrical tape and then foil around that (for shielding) + while being very careful to not let the foil touch any current carrying parts (like the GPIO pins and the soldier joints for the antenna mount). Then I wired up a PA/LNA module with a 3V + regulator (L4931 with a 2.2 µF capacitor between V\ :sub:`out` & GND) using my ItsyBitsy M4 + 5V (USB) pin going directly to the L4931 V\ :sub:`in` pin. The following are experiences from + running simple, ack, & stream examples with a reliable nRF24L01+ (no PA/LNA) on the other end (driven by a Raspberry Pi 2): + +Results (ordered by :py:attr:`~circuitpython_nrf24l01.rf24.RF24.pa_level` settings) +*********************************************************************************** + + * 0 dBm: ``master()`` worked the first time (during simple example) then continuously failed + (during all examples). ``slave()`` worked on simple & stream examples, but the opposing + ``master()`` node reporting that ACK packets (without payloads) were **not** received from + the PA/LNA module; ``slave()`` failed to send ACK packet payloads during the ack example. + * -6 dBm: ``master()`` worked consistently on simple, ack, & stream example. ``slave()`` worked + reliably on simple & stream examples, but failed to transmit **any** ACK packet payloads in + the ack example. + * -12 dBm: ``master()`` worked consistently on simple, ack, & stream example. ``slave()`` + worked reliably on simple & stream examples, but failed to transmit **some** ACK packet + payloads in the ack example. + * -18 dBm: ``master()`` worked consistently on simple, ack, & stream example. ``slave()`` + worked reliably on simple, ack, & stream examples, meaning **all** ACK packet payloads were + successfully transmit in the ack example. + + I should note that without shielding the PA/LNA module and using the L4931 3V regulator, + no TX transmissions got sent (including ACK packets for the + :py:attr:`~circuitpython_nrf24l01.rf24.RF24.auto-ack` feature). + +Conclusion +********************************* + + The PA/LNA modules seem to require quite a bit more power to transmit. The L4931 regulator + that I used in the tests boasts a 300 mA current limit and a typical current of 250 mA. + While the ItsyBitsy M4 boasts a 500 mA max, it would seem that much of that is consumed + internally. Since playing with the :py:attr:`~circuitpython_nrf24l01.rf24.RF24.pa_level` + is a current saving hack (as noted in the datasheet), I can only imagine that a higher power + 3V regulator may enable sending transmissions (including ACK packets -- with or without ACK + payloads attached) from PA/LNA modules using higher + :py:attr:`~circuitpython_nrf24l01.rf24.RF24.pa_level` settings. More testing is called for, + but I don't have an oscilloscope to measure the peak current draws. diff --git a/examples/nrf24l01_2arduino_handling_data.py b/examples/nrf24l01_2arduino_handling_data.py index f27de3d..8d64422 100644 --- a/examples/nrf24l01_2arduino_handling_data.py +++ b/examples/nrf24l01_2arduino_handling_data.py @@ -8,10 +8,12 @@ import struct import board import digitalio as dio -from circuitpython_nrf24l01 import RF24 +# if running this on a ATSAMD21 M0 based board +# from circuitpython_nrf24l01.rf24_lite import RF24 +from circuitpython_nrf24l01.rf24 import RF24 # addresses needs to be in a buffer protocol object (bytearray) -address = [b'1Node', b'2Node'] +address = [b"1Node", b"2Node"] # change these (digital output) pins accordingly ce = dio.DigitalInOut(board.D4) @@ -22,84 +24,110 @@ 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 +nrf = RF24(spi, csn, ce) +nrf.dynamic_payloads = False # the default in the TMRh20 arduino library + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity +nrf.pa_level = -12 # 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""" +# pylint: disable=too-few-public-methods +class DataStruct: + """A data structure to hold transmitted values as the + 'HandlingData' part of the TMRh20 library example""" + time = 0 # in milliseconds (used as start of timer) + value = 1.22 # incremented by 0.01 with every transmission +# pylint: enable=too-few-public-methods + +myData = DataStruct() + - # for the "HandlingData" part of the test from the TMRh20 library example - float_value = 0.01 +def master(count=5): # count = 5 will only transmit 5 packets + """Transmits an arbitrary unsigned long value every second""" while count: - nrf.listen = False # ensures the nRF24L01 is in TX mode + nrf.listen = False # ensures the nRF24L01 is in TX mode print("Now Sending") - start_timer = int(time.monotonic() * 1000) # start timer + myData.time = 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(' 1 - # NOTE if opening pipes outside of the "with" block, you may encounter - # conflicts in the differences between address_length attributes. - # the address_length attribute must equal the length of addresses + nrf.open_rx_pipe(5, b"1Node") # NOTE we do this inside the "with" block # display current settings of the nrf object nrf.what_happened(True) # True dumps pipe info -print("\nsettings configured by the basicRF object") -with basicRF as nerf: # the "as nerf" part is optional - nerf.open_rx_pipe(2, b'SOS') # again only uses the first character +print("\nsettings configured by the ble object") +with ble as nerf: # the "as nerf" part is optional nerf.what_happened(1) # if you examine the outputs from what_happened() you'll see: -# pipe 5 is opened using the nrf object, but closed using the basicRF object. -# pipe 2 is closed using the nrf object, but opened using the basicRF object. +# pipe 5 is opened using the nrf object, but closed using the ble object. +# pipe 0 is closed using the nrf object, but opened using the ble object. +# also notice the different addresses bound to the RX pipes # this is because the "with" statements load the existing settings # for the RF24 object specified after the word "with". -# the things that remain consistent despite the use of "with" -# statements includes the power mode (standby or sleep), and -# primary role (RX/TX mode) -# NOTE this library uses the adresses' reset values and closes all pipes upon -# instantiation +# NOTE it is not advised to manipulate seperate RF24 objects outside of the +# "with" block; you will encounter bugs about configurations when doing so. +# Be sure to use 1 "with" block per RF24 object when instantiating multiple +# RF24 objects in your program. +# NOTE exiting a "with" block will always power down the nRF24L01 +# NOTE upon instantiation, this library closes all RX pipes & +# extracts the TX/RX addresses from the nRF24L01 registers diff --git a/examples/nrf24l01_fake_ble_test.py b/examples/nrf24l01_fake_ble_test.py new file mode 100644 index 0000000..d09757d --- /dev/null +++ b/examples/nrf24l01_fake_ble_test.py @@ -0,0 +1,147 @@ +""" +This example of using the nRF24L01 as a 'fake' Buetooth Beacon + + .. warning:: ATSAMD21 M0-based boards have memory allocation + error when loading fake_ble.mpy +""" +import time +import board +import digitalio as dio +from circuitpython_nrf24l01.fake_ble import ( + chunk, + FakeBLE, + UrlServiceData, + BatteryServiceData, + TemperatureServiceData, +) + +# 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 as a BLE compliant radio +nrf = FakeBLE(spi, csn, ce) + +# the name parameter is going to be its broadcasted BLE name +# this can be changed at any time using the `name` attribute +# nrf.name = b"foobar" + +# if broadcasting to an Android, set the to_iphone attribute to False +# if broadcasting to an iPhone, set the to_iphone attribute to True +nrf.to_iphone = True # default value is False + +# you can optionally set the arbitrary MAC address to be used as the +# BLE device's MAC address. Otherwise this is randomly generated upon +# instantiation of the FakeBLE object. +# nrf.mac = b"\x19\x12\x14\x26\x09\xE0" + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceiver in close proximity to the +# BLE scanning application +nrf.pa_level = -12 + + +def _prompt(count, iterator): + if (count - iterator) % 5 == 0 or (count - iterator) < 5: + if count - iterator - 1: + print(count - iterator, "advertisments left to go!") + else: + print(count - iterator, "advertisment left to go!") + + +# create an object for manipulating the battery level data +battery_service = BatteryServiceData() +# battery level data is 1 unsigned byte representing a percentage +battery_service.data = 85 + + +def master(count=50): + """Sends out the device information twice a second.""" + # using the "with" statement is highly recommended if the nRF24L01 is + # to be used for more than a BLE configuration + with nrf as ble: + ble.name = b"nRF24L01" + # include the radio's pa_level attribute in the payload + ble.show_pa_level = True + print( + "available bytes in next payload:", + ble.available(chunk(battery_service.buffer)) + ) # using chunk() gives an accurate estimate of available bytes + for i in range(count): # advertise data this many times + if ble.available(chunk(battery_service.buffer)) >= 0: + _prompt(count, i) # something to show that it isn't frozen + # broadcast the device name, MAC address, & + # battery charge info; 0x16 means service data + ble.advertise(battery_service.buffer, data_type=0x16) + # channel hoping is recommended per BLE specs + ble.hop_channel() + # alternate advertisements to target all devices + ble.to_iphone = not ble.to_iphone + time.sleep(0.5) # wait till next broadcast + # nrf.show_pa_level & nrf.name both are set to false when + # exiting a with statement block + + +# create an object for manipulating temperature measurements +temperature_service = TemperatureServiceData() +# temperature's float data has up to 2 decimal places of percision +temperature_service.data = 42.0 + + +def send_temp(count=50): + """Sends out a fake temperature twice a second.""" + with nrf as ble: + ble.name = b"nRF24L01" + print( + "available bytes in next payload:", + ble.available(chunk(temperature_service.buffer)) + ) + for i in range(count): + if ble.available(chunk(temperature_service.buffer)) >= 0: + _prompt(count, i) + # broadcast a temperature measurement; 0x16 means service data + ble.advertise(temperature_service.buffer, data_type=0x16) + # channel hoping is recommended per BLE specs + ble.hop_channel() + ble.to_iphone = not ble.to_iphone + time.sleep(0.2) + + +# use the Eddystone protocol from Google to broadcast a URL as +# service data. We'll need an object to manipulate that also +url_service = UrlServiceData() +# the data attribute converts a URL string into a simplified +# bytes object using byte codes defined by the Eddystone protocol. +url_service.data = "http://www.google.com" +# Eddystone protocol requires an estimated TX PA level at 1 meter +# lower this estimate since we lowered the actual `ble.pa_level` +url_service.pa_level_at_1_meter = -45 # defaults to -25 dBm + +def send_url(count=50): + """Sends out a URL twice a second.""" + with nrf as ble: + print( + "available bytes in next payload:", + ble.available(chunk(url_service.buffer)) + ) + # NOTE we did NOT set a device name in this with block + for i in range(count): + # URLs easily exceed the nRF24L01's max payload length + if ble.available(chunk(url_service.buffer)) >= 0: + _prompt(count, i) + ble.advertise(url_service.buffer, 0x16) + ble.hop_channel() + ble.to_iphone = not ble.to_iphone + time.sleep(0.2) + +print( + """\ + nRF24L01 fake BLE beacon test.\n\ + Run master() to broadcast the device name, pa_level, & battery charge\n\ + Run send_temp() to broadcast the device name & a temperature\n\ + Run send_url() to broadcast a custom URL link""" +) diff --git a/examples/nrf24l01_interrupt_test.py b/examples/nrf24l01_interrupt_test.py index aa6ebe7..d3136e7 100644 --- a/examples/nrf24l01_interrupt_test.py +++ b/examples/nrf24l01_interrupt_test.py @@ -1,18 +1,22 @@ """ -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 + .. note:: this script uses the non-blocking `write()` function because + the function `send()` clears the IRQ flags upon returning """ import time import board import digitalio as dio -from circuitpython_nrf24l01 import RF24 +# if running this on a ATSAMD21 M0 based board +# from circuitpython_nrf24l01.rf24_lite import RF24 +from circuitpython_nrf24l01.rf24 import RF24 # address needs to be in a buffer protocol object (bytearray is preferred) -address = b'1Node' +address = b"1Node" # select your digital input pin that's connected to the IRQ pin on the nRF4L01 -irq = dio.DigitalInOut(board.D12) -irq.switch_to_input() # make sure its an input object +irq_pin = dio.DigitalInOut(board.D12) +irq_pin.switch_to_input() # make sure its an input object # change these (digital output) pins accordingly ce = dio.DigitalInOut(board.D4) csn = dio.DigitalInOut(board.D5) @@ -24,83 +28,115 @@ # we'll be using the dynamic payload size feature (enabled by default) # initialize the nRF24L01 on the spi bus object 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""" +# this example uses the ACK payload to trigger the IRQ pin active for +# the "on data received" event +nrf.ack = True # enable ACK payloads + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity +nrf.pa_level = -12 + + +def _ping_and_prompt(): + """transmit 1 payload, wait till irq_pin goes active, print IRQ status + flags.""" + ce.value = 1 # tell the nRF24L01 to prepare sending a single packet + time.sleep(0.00001) # mandatory 10 microsecond pulse starts transmission + ce.value = 0 # end 10 us pulse; use only 1 buffer from TX FIFO + while irq_pin.value: # IRQ pin is active when LOW + pass + print("IRQ pin went active LOW.") + nrf.update() # update irq_d? status flags + print( + "\tirq_ds: {}, irq_dr: {}, irq_df: {}".format( + nrf.irq_ds, nrf.irq_dr, nrf.irq_df + ) + ) + +def master(): + """Transmits 3 times: successfully receive ACK payload first, successfully + transmit on second, and intentionally fail transmit on the third""" # set address of RX node into a TX pipe nrf.open_tx_pipe(address) # ensures the nRF24L01 is in TX mode - nrf.listen = 0 + nrf.listen = False + # NOTE nrf.power is automatically set to True on first call to nrf.write() + # NOTE nrf.write() internally calls nrf.clear_status_flags() first + + # load 2 buffers into the TX FIFO; write_only=True leaves CE pin LOW + nrf.write(b"Ping ", write_only=True) + nrf.write(b"Pong ", write_only=True) + + # on data ready test + print("\nConfiguring IRQ pin to only ignore 'on data sent' event") + nrf.interrupt_config(data_sent=False) + print(" Pinging slave node for an ACK payload...", end=" ") + _ping_and_prompt() # CE pin is managed by this function + if nrf.irq_dr: + print("\t'on data ready' event test successful") + else: + print("\t'on data ready' event test unsucessful") # on data sent test - print("Pinging: enslaved nRF24L01 without auto_ack") - nrf.write(b'ping') - time.sleep(0.00001) # mandatory 10 microsecond pulse starts transmission - nrf.ce_pin.value = 0 # end 10 us pulse; now in active TX - while not nrf.irq_ds and not nrf.irq_df: - nrf.update() # updates the current status on IRQ flags - if nrf.irq_ds and not irq.value: - print('interrupt on data sent successful') + print("\nConfiguring IRQ pin to only ignore 'on data ready' event") + nrf.interrupt_config(data_recv=False) + print(" Pinging slave node again... ", end=" ") + _ping_and_prompt() # CE pin is managed by this function + if nrf.irq_ds: + print("\t'on data sent' event test successful") else: - print( - 'IRQ on data sent is not active, check your wiring and call interrupt_config()') - nrf.clear_status_flags() # clear all flags for next test + print("\t'on data sent' event test unsucessful") - # on data ready test - nrf.listen = 1 - nrf.open_rx_pipe(0, address) - start = time.monotonic() - while not nrf.any() and time.monotonic() - start < timeout: # wait for slave to send - pass - if nrf.any(): - print('Pong received') - if nrf.irq_dr and not irq.value: - print('interrupt on data ready successful') - else: - print( - 'IRQ on data ready is not active, check your wiring and call interrupt_config()') - nrf.flush_rx() + # trigger slave node to exit by filling the slave node's RX FIFO + print("\nSending one extra payload to fill RX FIFO on slave node.") + if nrf.send(b"Radio", send_only=True): + # when send_only parameter is True, send() ignores RX FIFO usage + print("Slave node should not be listening anymore.") else: - print('pong reception timed out!. make sure to run slave() on the other nRF24L01') - nrf.clear_status_flags() # clear all flags for next test + print("Slave node was unresponsive.") # on data fail test - nrf.listen = False # put the nRF24L01 is in TX mode - # the writing pipe should still be open since we didn't call close_tx_pipe() - nrf.flush_tx() # just in case the previous "on data sent" test failed - nrf.write(b'dummy') # slave isn't listening anymore - time.sleep(0.00001) # mandatory 10 microsecond pulse starts transmission - nrf.ce_pin.value = 0 # end 10 us pulse; now in active TX - while not nrf.irq_ds and not nrf.irq_df: # these attributes don't update themselves - nrf.update() # updates the current status on all IRQ flags (irq_dr, irq_df, irq_ds) - if nrf.irq_df and not irq.value: - print('interrupt on data fail successful') + print("\nConfiguring IRQ pin to go active for all events.") + nrf.interrupt_config() + print(" Sending a ping to inactive slave node...", end=" ") + nrf.flush_tx() # just in case any previous tests failed + nrf.write(b"Dummy", write_only=True) # CE pin is left LOW + _ping_and_prompt() # CE pin is managed by this function + if nrf.irq_df: + print("\t'on data failed' event test successful") else: - print( - 'IRQ on data fail is not active, check your wiring and call interrupt_config()') - nrf.clear_status_flags() # clear all flags for next test + print("\t'on data failed' event test unsucessful") + nrf.flush_tx() # flush artifact payload in TX FIFO from last test + # all 3 ACK payloads received were 4 bytes each, and RX FIFO is full + # so, fetching 12 bytes from the RX FIFO also flushes RX FIFO + print("\nComplete RX FIFO:", nrf.recv(12)) + -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 +def slave(timeout=6): # will listen for 6 seconds before timing out + """Only listen for 3 payload from the master node""" + # setup radio to recieve pings, fill TX FIFO with ACK payloads nrf.open_rx_pipe(0, address) - nrf.listen = 1 - start = time.monotonic() - while not nrf.any() and time.monotonic() - start < timeout: - pass # nrf.any() also updates the status byte on every call - if nrf.any(): - print("ping received. sending pong now.") - else: - print('listening timed out, please try again') - nrf.flush_rx() - nrf.listen = 0 - nrf.open_tx_pipe(address) - # send a payload to complete the on data ready test - nrf.send(b'pong', force_retry=1) - # we're done on this side - -print("""\ - nRF24L01 Interrupt test\n\ - Run master() to run IRQ pin tests\n\ - Run slave() on the non-testing nRF24L01 to complete the test successfully""") + nrf.load_ack(b"Yak ", 0) + nrf.load_ack(b"Back", 0) + nrf.load_ack(b" ACK", 0) + nrf.listen = True # start listening & clear irq_dr flag + start_timer = time.monotonic() # start timer now + while not nrf.fifo(0, 0) and time.monotonic() - start_timer < timeout: + # if RX FIFO is not full and timeout is not reached, then keep going + pass + nrf.listen = False # put nRF24L01 in Standby-I mode when idling + if not nrf.fifo(False, True): # if RX FIFO is not empty + # all 3 payloads received were 5 bytes each, and RX FIFO is full + # so, fetching 15 bytes from the RX FIFO also flushes RX FIFO + print("Complete RX FIFO:", nrf.recv(15)) + nrf.flush_tx() # discard any pending ACK payloads + + +print( + """\ + nRF24L01 Interrupt pin test.\n\ + Make sure the IRQ pin is connected to the MCU\n\ + Run slave() on receiver\n\ + Run master() on transmitter""" +) diff --git a/examples/nrf24l01_simple_test.py b/examples/nrf24l01_simple_test.py index 6ab6e5a..cf8f233 100644 --- a/examples/nrf24l01_simple_test.py +++ b/examples/nrf24l01_simple_test.py @@ -1,14 +1,16 @@ """ -Simple example of library usage. +Simple example of using the RF24 class. """ import time import struct import board import digitalio as dio -from circuitpython_nrf24l01 import RF24 +# if running this on a ATSAMD21 M0 based board +# from circuitpython_nrf24l01.rf24_lite import RF24 +from circuitpython_nrf24l01.rf24 import RF24 # addresses needs to be in a buffer protocol object (bytearray) -address = b'1Node' +address = b"1Node" # change these (digital output) pins accordingly ce = dio.DigitalInOut(board.D4) @@ -22,6 +24,11 @@ # initialize the nRF24L01 on the spi bus object nrf = RF24(spi, csn, ce) +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity +nrf.pa_level = -12 + + 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 @@ -32,23 +39,23 @@ def master(count=5): # count = 5 will only transmit 5 packets while count: # use struct.pack to packetize your data # into a usable payload - buffer = struct.pack('= SIZE / 2 + abs(SIZE / 2 - i) or j < - SIZE / 2 - abs(SIZE / 2 - i)) + 48]) - buffers.append(buff) - del buff +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity +nrf.pa_level = -12 + def master(count=1): # count = 5 will transmit the list 5 times """Transmits a massive buffer of payloads""" + # lets create a `list` of payloads to be streamed to + # the nRF24L01 running slave() + buffers = [] + # we'll use SIZE for the number of payloads in the list and the + # payloads' length + size = 32 + for i in range(size): + # prefix payload with a sequential letter to indicate which + # payloads were lost (if any) + buff = bytes([i + (65 if 0 <= i < 26 else 71)]) + for j in range(size - 1): + char = bool(j >= (size - 1) / 2 + abs((size - 1) / 2 - i)) + char |= bool(j < (size - 1) / 2 - abs((size - 1) / 2 - i)) + buff += bytes([char + 48]) + buffers.append(buff) + del buff + # set address of RX node into a TX pipe nrf.open_tx_pipe(address) # ensures the nRF24L01 is in TX mode @@ -45,12 +56,15 @@ def master(count=1): # count = 5 will transmit the list 5 times for _ in range(count): now = time.monotonic() * 1000 # start timer result = nrf.send(buffers, force_retry=2) - print('Transmission took', time.monotonic() * 1000 - now, 'ms') + print("Transmission took", time.monotonic() * 1000 - now, "ms") for r in result: successful += 1 if r else 0 - print('successfully sent {}% ({}/{})'.format( - successful / len(buffers) * 100 * count, - successful, len(buffers) * count)) + print( + "successfully sent {}% ({}/{})".format( + successful / (size* count) * 100, successful, size * count + ) + ) + def slave(timeout=5): """Stops listening after timeout with no response""" @@ -68,13 +82,16 @@ def slave(timeout=5): count += 1 # retreive the received packet's payload rx = nrf.recv() # clears flags & empties RX FIFO - print("Received (raw): {} - {}".format(repr(rx), count)) + print("Received: {} - {}".format(repr(rx), count)) now = time.monotonic() # recommended behavior is to keep in TX mode while idle nrf.listen = False # put the nRF24L01 is in TX mode -print("""\ + +print( + """\ nRF24L01 Stream test\n\ Run slave() on receiver\n\ - Run master() on transmitter""") + Run master() on transmitter""" +)