From a8abb14e95d21567ddfa471a85afbee8140401a2 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Mon, 14 Oct 2024 04:06:20 -0700 Subject: [PATCH] add python examples and refactor API a bit --- .github/workflows/tests.yml | 22 +- Cargo.toml | 2 +- cspell.config.yml | 3 + docs/src/api-diff.md | 26 +- examples/python/acknowledgement_payloads.py | 176 ++++++++++++++ examples/python/getting_started.py | 158 ++++++++++++ examples/python/interrupt_configure.py | 253 ++++++++++++++++++++ examples/python/manual_acknowledgements.py | 209 ++++++++++++++++ examples/python/multiceiver_demo.py | 165 +++++++++++++ examples/python/requirements-dev.txt | 2 + examples/python/requirements.txt | 1 + examples/python/scanner.py | 184 ++++++++++++++ examples/python/scanner_curses.py | 244 +++++++++++++++++++ examples/python/streaming_data.py | 180 ++++++++++++++ examples/{ => rust}/.cargo/config.toml | 0 examples/{ => rust}/Cargo.toml | 2 +- examples/{ => rust}/Embed.toml | 0 examples/{ => rust}/README.md | 0 examples/{ => rust}/src/lib.rs | 0 examples/{ => rust}/src/linux.rs | 0 examples/{ => rust}/src/main.rs | 0 examples/{ => rust}/src/rp2040.rs | 0 lib/src/enums.rs | 11 + lib/src/lib.rs | 2 +- lib/src/radio/mod.rs | 67 ++++-- lib/src/radio/rf24/power.rs | 22 ++ lib/src/radio/rf24/radio.rs | 69 ++++-- lib/src/radio/rf24/status.rs | 117 ++++----- pyproject.toml | 6 +- rf24-node/Cargo.toml | 2 +- rf24-node/package.json | 3 - rf24-node/src/enums.rs | 40 ++-- rf24-node/src/radio.rs | 60 +++-- rf24-py/src/enums.rs | 44 +++- rf24-py/src/lib.rs | 3 +- rf24-py/src/radio.rs | 87 +++++-- rf24_py.pyi | 73 ++++-- 37 files changed, 2036 insertions(+), 197 deletions(-) create mode 100644 examples/python/acknowledgement_payloads.py create mode 100644 examples/python/getting_started.py create mode 100644 examples/python/interrupt_configure.py create mode 100644 examples/python/manual_acknowledgements.py create mode 100644 examples/python/multiceiver_demo.py create mode 100644 examples/python/requirements-dev.txt create mode 100644 examples/python/requirements.txt create mode 100644 examples/python/scanner.py create mode 100644 examples/python/scanner_curses.py create mode 100644 examples/python/streaming_data.py rename examples/{ => rust}/.cargo/config.toml (100%) rename examples/{ => rust}/Cargo.toml (95%) rename examples/{ => rust}/Embed.toml (100%) rename examples/{ => rust}/README.md (100%) rename examples/{ => rust}/src/lib.rs (100%) rename examples/{ => rust}/src/linux.rs (100%) rename examples/{ => rust}/src/main.rs (100%) rename examples/{ => rust}/src/rp2040.rs (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1baa4dc..9b4e689 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ on: - .github/workflows/tests.yml jobs: - lint: + lint-rust: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -34,8 +34,26 @@ jobs: key: cargo-lib-${{ hashFiles('lib/src/**', 'lib/Cargo.toml') }} - run: just lint + lint-python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + cache: 'pip' + cache-dependency-path: 'examples/python/requirements*.txt' + - name: Install workflow tools + run: >- + python -m pip install + -r examples/python/requirements.txt + -r examples/python/requirements-dev.txt + - run: ruff check + - run: ruff format + - run: mypy + test: - needs: [lint] + needs: [lint-rust] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/Cargo.toml b/Cargo.toml index 10a6295..0876171 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["lib", "examples", "rf24-py", "rf24-node"] +members = ["lib", "examples/rust", "rf24-py", "rf24-node"] default-members = ["lib"] resolver = "2" diff --git a/cspell.config.yml b/cspell.config.yml index 988829c..79d2e99 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -5,7 +5,9 @@ words: - armv - bindgen - bytearray + - calcsize - Cdev + - datasheet - Doherty - DYNPD - eabi @@ -13,6 +15,7 @@ words: - gnueabihf - gpio - gpiochip + - gpiod - inlinehilite - Kbps - linenums diff --git a/docs/src/api-diff.md b/docs/src/api-diff.md index fa5ed04..addf243 100644 --- a/docs/src/api-diff.md +++ b/docs/src/api-diff.md @@ -7,12 +7,30 @@ There are some important design decisions here. [traits]: https://doc.rust-lang.org/book/ch10-02-traits.html [result]: https://doc.rust-lang.org/book/ch02-00-guessing-game-tutorial.html#handling-potential-failure-with-result +## `read()` length is optional + +Since the length of the buffer passed to `RF24::read()` can be determined programmatically, +it is not required to specify how many bytes to read into the buffer. + +Better yet, the number of bytes read from the RX FIFO can be determined automatically (in order of precedence): + +1. The length of the buffer passed to `buf` parameter. +2. The length of currently configured static payload size if dynamic payloads are disabled. +3. The length of the next available payload in the RX FIFO if dynamic payloads are enabled. + +If any of the above conditions evaluates to `0`, then `RF24::read()` does nothing. + +Remember, the dynamic payloads feature is toggled using `RF24::set_dynamic_payloads()`. +Static payload sizes are set using `RF24::set_payload_length()`. +If dynamic payloads are enabled then setting static payload size has no affect. + ## STATUS byte exposed As with our other implementations, the STATUS byte returned on every SPI transaction is cached to a private member. Understanding the meaning of the status byte is publicly exposed via +- `update()`: used to get an update about the status flags from the radio. - `clear_status_flags()`: with parameters to specify which flag(s) should be cleared. -- `get_status_flags()`: has a signature similar to C++ `whatHappened()` but does not clear the flags. +- `get_status_flags()`: has a signature similar to C++ `whatHappened()` but does not update nor clear the flags. - `set_status_flags()`: similar to C++ `maskIRQ()` except the boolean parameters' meaning is not reversed. | lang | only trigger on RX_DR events | @@ -20,12 +38,16 @@ As with our other implementations, the STATUS byte returned on every SPI transac | C++ | `radio.maskIRQ(false, true, true)` | | Rust | `radio.set_status_flags(true, false, false)` | + In this library, passing `true` to any parameter of `set_stats_flags()` will enable the IRQ for the corresponding event (see function's documentation). + ## No babysitting To transmit something, RF24 struct offers -- `write()`: non-blocking uploads to TX FIFO. - `send()`: blocking wrapper around `write()` +- `write()`: non-blocking uploads to TX FIFO. + + Use `update()` and `get_status_flags()` get the updated status flags to determine if transmission was successful or not. The IRQ pin can also be used to trigger calls to `update()` + `get_status_flags()`. See `set_status_flags()` about configuring the IRQ pin. There will be no equivalents to C++ `writeBlocking()`, `startFastWrite()`, `writeFast()`, `txStandby()`. Considering the exposed STATUS byte, these can all be done from the user space (if needed). diff --git a/examples/python/acknowledgement_payloads.py b/examples/python/acknowledgement_payloads.py new file mode 100644 index 0000000..ac5ee6e --- /dev/null +++ b/examples/python/acknowledgement_payloads.py @@ -0,0 +1,176 @@ +""" +Simple example of using the library to transmit +and retrieve custom automatic acknowledgment payloads. + +See documentation at https://nRF24.github.io/rf24-rs +""" + +from pathlib import Path +import time +from rf24_py import RF24, PaLevel + +print(__file__) # print example name + +# The radio's CE Pin uses a GPIO number. +# On Linux, consider the device path `/dev/gpiochip`: +# - `` is the gpio chip's identifying number. +# Using RPi4 (or earlier), this number is `0` (the default). +# Using the RPi5, this number is actually `4`. +# The radio's CE pin must connected to a pin exposed on the specified chip. +CE_PIN = 22 # for GPIO22 +# try detecting RPi5 first; fall back to default +DEV_GPIO_CHIP = 4 if Path("/dev/gpiochip4").exists() else 0 + +# The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). +# On Linux, consider the device path `/dev/spidev.`: +# - `` is the SPI bus number (defaults to `0`) +# - `` is the CSN pin (must be unique for each device on the same SPI bus) +CSN_PIN = 0 # aka CE0 for SPI bus 0 (/dev/spidev0.0) + +# create a radio object for the specified hard ware config: +radio = RF24(CE_PIN, CSN_PIN, dev_gpio_chip=DEV_GPIO_CHIP) + +# using the python keyword global is bad practice. Instead we'll use a 1 item +# list to store our integer number for the payloads' counter +counter = [0] + +# For this example, we will use different addresses +# An address need to be a buffer protocol object (bytearray) +address = [b"1Node", b"2Node"] +# It is very helpful to think of an address as a path instead of as +# an identifying device destination + +# to use different addresses on a pair of radios, we need a variable to +# uniquely identify which address this radio will use to transmit +# 0 uses address[0] to transmit, 1 uses address[1] to transmit +radio_number = bool( + int(input("Which radio is this? Enter '0' or '1'. Defaults to '0' ") or 0) +) + +# initialize the nRF24L01 on the spi bus +radio.begin() + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity of each other +radio.pa_level = PaLevel.LOW # PaLevel.MAX is default + +# ACK payloads are dynamically sized, so we need to enable that feature also +radio.set_dynamic_payloads(True) + +# to enable the custom ACK payload feature +radio.allow_ack_payloads(True) + +# set TX address of RX node into the TX pipe +radio.open_tx_pipe(address[radio_number]) # always uses pipe 0 + +# set RX address of TX node into an RX pipe +radio.open_rx_pipe(1, address[not radio_number]) # using pipe 1 + +# for debugging +# radio.print_pretty_details() + + +def master(count: int = 5): # count = 5 will only transmit 5 packets + """Transmits a payload every second and prints the ACK payload""" + radio.listen = False # put radio in TX mode + + while count: + # construct a payload to send + buffer = b"Hello \x00" + bytes([counter[0]]) + + # send the payload and prompt + start_timer = time.monotonic_ns() # start timer + result = radio.send(buffer) # save the report + end_timer = time.monotonic_ns() # stop timer + if result: + # print timer results upon transmission success + print( + "Transmission successful! Time to transmit:", + f"{int((end_timer - start_timer) / 1000)} us. Sent:", + f"{buffer[:6].decode('utf-8')}{counter[0]}", + end=" ", + ) + if radio.available(): + # print the received ACK that was automatically sent + response = radio.read() + print(f" Received: {response[:6].decode('utf-8')}{response[7:8][0]}") + counter[0] += 1 # increment payload counter + else: + print(" Received an empty ACK packet") + else: + print("Transmission failed or timed out") + time.sleep(1) # let the RX node prepare a new ACK payload + count -= 1 + + +def slave(timeout: int = 6): + """Prints the received value and sends an ACK payload""" + radio.listen = True # put radio into RX mode, power it up + + # setup the first transmission's ACK payload + buffer = b"World \x00" + bytes([counter[0]]) + # we must set the ACK payload data and corresponding + # pipe number [0,5] + radio.write_ack_payload(1, buffer) # load ACK for first response + + start = time.monotonic() # start timer + while (time.monotonic() - start) < timeout: + has_payload, pipe_number = radio.available_pipe() + if has_payload: + received = radio.read() # fetch 1 payload from RX FIFO + # increment counter from received payload + counter[0] = received[7:8][0] + 1 + print( + f"Received {len(received)} bytes on pipe {pipe_number}:", + f"{received[:6].decode('utf-8')}{received[7:8][0]} Sent:", + f"{buffer[:6].decode('utf-8')}{counter[0]}", + ) + start = time.monotonic() # reset timer + + # build a new ACK payload + buffer = b"World \x00" + bytes([counter[0]]) + radio.write_ack_payload(1, buffer) # load ACK for next response + + # recommended behavior is to keep in TX mode while idle + radio.listen = False # put radio in TX mode & flush unused ACK payloads + + +def set_role(): + """Set the role using stdin stream. Timeout arg for slave() can be + specified using a space delimiter (e.g. 'R 10' calls `slave(10)`) + + :return: + - True when role is complete & app should continue running. + - False when app should exit + """ + user_input = ( + input( + "*** Enter 'R' for receiver role.\n" + "*** Enter 'T' for transmitter role.\n" + "*** Enter 'Q' to quit example.\n" + ) + or "?" + ) + user_input = user_input.split() + if user_input[0].upper().startswith("R"): + slave(*[int(x) for x in user_input[1:2]]) + return True + if user_input[0].upper().startswith("T"): + master(*[int(x) for x in user_input[1:2]]) + return True + if user_input[0].upper().startswith("Q"): + radio.power = False + return False + print(user_input[0], "is an unrecognized input. Please try again.") + return True + + +if __name__ == "__main__": + try: + while set_role(): + pass # continue example until 'Q' is entered + except KeyboardInterrupt: + print(" Keyboard Interrupt detected. Exiting...") + radio.power = False +else: + print(" Run slave() on receiver\n Run master() on transmitter") diff --git a/examples/python/getting_started.py b/examples/python/getting_started.py new file mode 100644 index 0000000..09a8d77 --- /dev/null +++ b/examples/python/getting_started.py @@ -0,0 +1,158 @@ +""" +Simple example of using the RF24 class. + +See documentation at https://nRF24.github.io/rf24-rs +""" + +from pathlib import Path +import time +import struct +from rf24_py import RF24, PaLevel + +print(__file__) # print example name + +# The radio's CE Pin uses a GPIO number. +# On Linux, consider the device path `/dev/gpiochip`: +# - `` is the gpio chip's identifying number. +# Using RPi4 (or earlier), this number is `0` (the default). +# Using the RPi5, this number is actually `4`. +# The radio's CE pin must connected to a pin exposed on the specified chip. +CE_PIN = 22 # for GPIO22 +# try detecting RPi5 first; fall back to default +DEV_GPIO_CHIP = 4 if Path("/dev/gpiochip4").exists() else 0 + +# The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). +# On Linux, consider the device path `/dev/spidev.`: +# - `` is the SPI bus number (defaults to `0`) +# - `` is the CSN pin (must be unique for each device on the same SPI bus) +CSN_PIN = 0 # aka CE0 for SPI bus 0 (/dev/spidev0.0) + +# create a radio object for the specified hard ware config: +radio = RF24(CE_PIN, CSN_PIN, dev_gpio_chip=DEV_GPIO_CHIP) + +# using the python keyword global is bad practice. Instead we'll use a 1 item +# list to store our float number for the payloads sent +payload = [0.0] + +# For this example, we will use different addresses +# An address needs to be a buffer protocol object (bytearray) +address = [b"1Node", b"2Node"] +# It is very helpful to think of an address as a path instead of as +# an identifying device destination + +# to use different addresses on a pair of radios, we need a variable to +# uniquely identify which address this radio will use to transmit +# 0 uses address[0] to transmit, 1 uses address[1] to transmit +radio_number = bool( + int(input("Which radio is this? Enter '0' or '1'. Defaults to '0' ") or 0) +) + +# initialize the nRF24L01 on the spi bus +radio.begin() + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity of each other +radio.pa_level = PaLevel.LOW # PaLevel.MAX is default + +# set TX address of RX node into the TX pipe +radio.open_tx_pipe(address[radio_number]) # always uses pipe 0 + +# set RX address of TX node into an RX pipe +radio.open_rx_pipe(1, address[not radio_number]) # using pipe 1 + +# To save time during transmission, we'll set the payload size to be only what +# we need. A float value occupies 4 bytes in memory using struct.calcsize() +# "`: +# - `` is the gpio chip's identifying number. +# Using RPi (before RPi5), this number is `0` (the default). +# Using the RPi5, this number is actually `4`. +# The radio's CE pin must connected to a pin exposed on the specified chip. +CE_PIN = 22 # for GPIO22 +# try detecting RPi5 first; fall back to default +DEV_GPIO_CHIP = 4 if Path("/dev/gpiochip4").exists() else 0 + +# The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). +# On Linux, consider the device path `/dev/spidev.`: +# - `` is the SPI bus number (defaults to `0`) +# - `` is the CSN pin (must be unique for each device on the same SPI bus) +CSN_PIN = 0 # aka CE0 for SPI bus 0 (/dev/spidev0.0) + +# create a radio object for the specified hard ware config: +radio = RF24(CE_PIN, CSN_PIN, dev_gpio_chip=DEV_GPIO_CHIP) + +# select your digital input pin that's connected to the IRQ pin on the nRF24L01 +IRQ_PIN = 24 +chip = gpiod.Chip(f"/dev/gpiochip{DEV_GPIO_CHIP}") +# print gpio chip info +info = chip.get_info() +print(f"Using {info.name} [{info.label}] ({info.num_lines} lines)") + +# For this example, we will use different addresses +# An address need to be a buffer protocol object (bytearray) +address = [b"1Node", b"2Node"] +# It is very helpful to think of an address as a path instead of as +# an identifying device destination + +# to use different addresses on a pair of radios, we need a variable to +# uniquely identify which address this radio will use to transmit +# 0 uses address[0] to transmit, 1 uses address[1] to transmit +radio_number = bool( + int(input("Which radio is this? Enter '0' or '1'. Defaults to '0' ") or 0) +) + +# initialize the nRF24L01 on the spi bus +radio.begin() + +# this example uses the ACK payload to trigger the IRQ pin active for +# the "on data received" event +radio.allow_ack_payloads(True) # enable ACK payloads +radio.set_dynamic_payloads(True) # ACK payloads are dynamically sized + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity of each other +radio.pa_level = PaLevel.LOW # PaLevel.MAX is default + +# set TX address of RX node into the TX pipe +radio.open_tx_pipe(address[radio_number]) # always uses pipe 0 + +# set RX address of TX node into an RX pipe +radio.open_rx_pipe(1, address[not radio_number]) # using pipe 1 + +# for debugging +# radio.print_pretty_details() + +# For this example, we'll be using a payload containing +# a string that changes on every transmission. (successful or not) +# Make a couple tuples of payloads & an iterator to traverse them +pl_iterator = [0] # use a 1-item list instead of python's global keyword +tx_payloads = (b"Ping ", b"Pong ", b"Radio", b"1FAIL") +ack_payloads = (b"Yak ", b"Back", b" ACK") + + +def interrupt_handler() -> None: + """This function is called when IRQ pin is detected active LOW""" + print("\tIRQ pin went active LOW.") + radio.update() + flags: StatusFlags = radio.get_status_flags() # update IRQ status flags + print(f"\ttx_ds: {flags.tx_ds}, tx_df: {flags.tx_df}, rx_dr: {flags.rx_dr}") + if pl_iterator[0] == 0: + print("'data ready' event test", ("passed" if flags.rx_dr else "failed")) + elif pl_iterator[0] == 1: + print("'data sent' event test", ("passed" if flags.tx_ds else "failed")) + elif pl_iterator[0] == 2: + print("'data fail' event test", ("passed" if flags.tx_df else "failed")) + radio.clear_status_flags() + + +# setup IRQ GPIO pin +irq_line = gpiod.request_lines( + path=f"/dev/gpiochip{DEV_GPIO_CHIP}", + consumer="rf24_py/examples/interrupt", # optional + config={IRQ_PIN: gpiod.LineSettings(edge_detection=Edge.FALLING)}, +) + + +def _wait_for_irq(timeout: float = 5) -> bool: + """Wait till IRQ_PIN goes active (LOW). + IRQ pin is LOW when activated. Otherwise it is always HIGH + """ + # wait up to ``timeout`` seconds for event to be detected. + if not irq_line.wait_edge_events(timeout): + print(f"\tInterrupt event not detected for {timeout} seconds!") + return False + # read event from kernel buffer + for event in irq_line.read_edge_events(): + if event.line_offset == IRQ_PIN and event.event_type is event.Type.FALLING_EDGE: + return True + return False + + +def master() -> None: + """Transmits 4 times and reports results + + 1. successfully receive ACK payload first + 2. successfully transmit on second + 3. send a third payload to fill RX node's RX FIFO + (supposedly making RX node unresponsive) + 4. intentionally fail transmit on the fourth + """ + radio.listen = False # put radio in TX mode + + # on data ready test + print("\nConfiguring IRQ pin to only ignore 'on data sent' event") + radio.set_status_flags(StatusFlags(rx_dr=True, tx_ds=False, tx_df=True)) + print(" Pinging slave node for an ACK payload...") + pl_iterator[0] = 0 + radio.write(tx_payloads[0]) + if _wait_for_irq(): + interrupt_handler() + + # on "data sent" test + print("\nConfiguring IRQ pin to only ignore 'on data ready' event") + radio.set_status_flags(StatusFlags(rx_dr=False, tx_ds=True, tx_df=True)) + print(" Pinging slave node again...") + pl_iterator[0] = 1 + radio.write(tx_payloads[1]) + if _wait_for_irq(): + interrupt_handler() + + # 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.") + print("Disabling IRQ pin for all events.") + radio.set_status_flags(StatusFlags()) + if radio.send(tx_payloads[2]): + print("Slave node should not be listening anymore.") + else: + print("Slave node was unresponsive.") + + # on "data fail" test + print("\nConfiguring IRQ pin to go active for all events.") + radio.set_status_flags(StatusFlags(rx_dr=True, tx_ds=True, tx_df=True)) + print(" Sending a ping to inactive slave node...") + radio.flush_tx() # just in case any previous tests failed + pl_iterator[0] = 2 + radio.write(tx_payloads[3]) + if _wait_for_irq(): + interrupt_handler() + radio.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:", radio.read(12)) + + +def slave(timeout=6): # will listen for 6 seconds before timing out + """Only listen for 3 payload from the master node""" + # the "data ready" event will trigger in RX mode + # the "data sent" or "data fail" events will trigger when we + # receive with ACK payloads enabled (& loaded in TX FIFO) + print("\nDisabling IRQ pin for all events.") + radio.set_status_flags(StatusFlags()) + # setup radio to receive pings, fill TX FIFO with ACK payloads + radio.write_ack_payload(1, ack_payloads[0]) + radio.write_ack_payload(1, ack_payloads[1]) + radio.write_ack_payload(1, ack_payloads[2]) + radio.listen = True # start listening & clear irq_dr flag + start_timer = time.monotonic() # start timer now + while ( + not radio.get_fifo_state(False) != FifoState.Full + and time.monotonic() - start_timer < timeout + ): + # if RX FIFO is not full and timeout is not reached, then keep waiting + pass + time.sleep(0.5) # wait for last ACK payload to transmit + radio.listen = False # put radio in TX mode & discard any ACK payloads + if radio.available(): # if RX FIFO is not empty (timeout did not occur) + # 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:", radio.read(15)) + + +def set_role() -> bool: + """Set the role using stdin stream. Timeout arg for slave() can be + specified using a space delimiter (e.g. 'R 10' calls `slave(10)`) + + :return: + - True when role is complete & app should continue running. + - False when app should exit + """ + user_input = ( + input( + "*** Enter 'R' for receiver role.\n" + "*** Enter 'T' for transmitter role.\n" + "*** Enter 'Q' to quit example.\n" + ) + or "?" + ).split() + if user_input[0].upper().startswith("R"): + slave(*[int(x) for x in user_input[1:2]]) + return True + if user_input[0].upper().startswith("T"): + master() + return True + if user_input[0].upper().startswith("Q"): + radio.power = False + return False + print(user_input[0], "is an unrecognized input. Please try again.") + return True + + +if __name__ == "__main__": + try: + while set_role(): + pass # continue example until 'Q' is entered + except KeyboardInterrupt: + print(" Keyboard Interrupt detected. Exiting...") + radio.power = False +else: + print( + f"Make sure the IRQ pin is connected to the GPIO{IRQ_PIN}", + "Run slave() on receiver", + "Run master() on transmitter", + sep="\n", + ) diff --git a/examples/python/manual_acknowledgements.py b/examples/python/manual_acknowledgements.py new file mode 100644 index 0000000..aee221c --- /dev/null +++ b/examples/python/manual_acknowledgements.py @@ -0,0 +1,209 @@ +""" +Simple example of using the RF24 class to transmit and respond with +acknowledgment (ACK) transmissions. Notice that the auto-ack feature is +enabled, but this example doesn't use automatic ACK payloads because automatic +ACK payloads' data will always be outdated by 1 transmission. Instead, this +example uses a call and response paradigm. + +See documentation at https://nRF24.github.io/rf24-rs +""" + +from pathlib import Path +import struct +import time +from rf24_py import RF24, PaLevel, StatusFlags + +print(__file__) # print example name + +# The radio's CE Pin uses a GPIO number. +# On Linux, consider the device path `/dev/gpiochip`: +# - `` is the gpio chip's identifying number. +# Using RPi4 (or earlier), this number is `0` (the default). +# Using the RPi5, this number is actually `4`. +# The radio's CE pin must connected to a pin exposed on the specified chip. +CE_PIN = 22 # for GPIO22 +# try detecting RPi5 first; fall back to default +DEV_GPIO_CHIP = 4 if Path("/dev/gpiochip4").exists() else 0 + +# The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). +# On Linux, consider the device path `/dev/spidev.`: +# - `` is the SPI bus number (defaults to `0`) +# - `` is the CSN pin (must be unique for each device on the same SPI bus) +CSN_PIN = 0 # aka CE0 for SPI bus 0 (/dev/spidev0.0) + +# create a radio object for the specified hard ware config: +radio = RF24(CE_PIN, CSN_PIN, dev_gpio_chip=DEV_GPIO_CHIP) + +# For this example, we will use different addresses +# An address need to be a buffer protocol object (bytearray) +address = [b"1Node", b"2Node"] +# It is very helpful to think of an address as a path instead of as +# an identifying device destination + +# to use different addresses on a pair of radios, we need a variable to +# uniquely identify which address this radio will use to transmit +# 0 uses address[radio_number] to transmit, 1 uses address[not radio_number] to transmit +radio_number = bool( + int(input("Which radio is this? Enter '0' or '1'. Defaults to '0' ") or 0) +) + +# initialize the nRF24L01 on the spi bus +radio.begin() + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity of each other +radio.pa_level = PaLevel.LOW # PaLevel.MAX is default + +# set TX address of RX node into the TX pipe +radio.open_tx_pipe(address[radio_number]) # always uses pipe 0 + +# set RX address of TX node into an RX pipe +radio.open_rx_pipe(1, address[not radio_number]) # using pipe 1 + +# To save time during transmission, we'll set the payload size to be only what +# we need. A float value occupies 4 bytes in memory using struct.calcsize() +# "`: +# - `` is the gpio chip's identifying number. +# Using RPi4 (or earlier), this number is `0` (the default). +# Using the RPi5, this number is actually `4`. +# The radio's CE pin must connected to a pin exposed on the specified chip. +CE_PIN = 22 # for GPIO22 +# try detecting RPi5 first; fall back to default +DEV_GPIO_CHIP = 4 if Path("/dev/gpiochip4").exists() else 0 + +# The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). +# On Linux, consider the device path `/dev/spidev.`: +# - `` is the SPI bus number (defaults to `0`) +# - `` is the CSN pin (must be unique for each device on the same SPI bus) +CSN_PIN = 0 # aka CE0 for SPI bus 0 (/dev/spidev0.0) + +# create a radio object for the specified hard ware config: +radio = RF24(CE_PIN, CSN_PIN, dev_gpio_chip=DEV_GPIO_CHIP) + +# setup the addresses for all transmitting nRF24L01 nodes +addresses = [ + b"\x78" * 5, + b"\xf1\xb6\xb5\xb4\xb3", + b"\xcd\xb6\xb5\xb4\xb3", + b"\xa3\xb6\xb5\xb4\xb3", + b"\x0f\xb6\xb5\xb4\xb3", + b"\x05\xb6\xb5\xb4\xb3", +] +# It is very helpful to think of an address as a path instead of as +# an identifying device destination + +# initialize the nRF24L01 on the spi bus +radio.begin() + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity of each other +radio.pa_level = PaLevel.LOW # PaLevel.MAX is default + +# To save time during transmission, we'll set the payload size to be only what +# we need. +# 2 int occupy 8 bytes in memory using len(struct.pack()) +# "=2.0.2 diff --git a/examples/python/scanner.py b/examples/python/scanner.py new file mode 100644 index 0000000..72a6826 --- /dev/null +++ b/examples/python/scanner.py @@ -0,0 +1,184 @@ +""" +This is an example of how to use the nRF24L01's builtin +Received Power Detection (RPD) to scan for possible interference. +This example does not require a counterpart node. + +See documentation at https://nRF24.github.io/rf24-rs +""" + +from pathlib import Path +import time +from typing import Optional +from rf24_py import RF24, CrcLength, FifoState + +print(__file__) # print example name + +# The radio's CE Pin uses a GPIO number. +# On Linux, consider the device path `/dev/gpiochip`: +# - `` is the gpio chip's identifying number. +# Using RPi4 (or earlier), this number is `0` (the default). +# Using the RPi5, this number is actually `4`. +# The radio's CE pin must connected to a pin exposed on the specified chip. +CE_PIN = 22 # for GPIO22 +# try detecting RPi5 first; fall back to default +DEV_GPIO_CHIP = 4 if Path("/dev/gpiochip4").exists() else 0 + +# The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). +# On Linux, consider the device path `/dev/spidev.`: +# - `` is the SPI bus number (defaults to `0`) +# - `` is the CSN pin (must be unique for each device on the same SPI bus) +CSN_PIN = 0 # aka CE0 for SPI bus 0 (/dev/spidev0.0) + +# create a radio object for the specified hard ware config: +radio = RF24(CE_PIN, CSN_PIN, dev_gpio_chip=DEV_GPIO_CHIP) + +# initialize the nRF24L01 on the spi bus +radio.begin() + +# turn off RX features specific to the nRF24L01 module +radio.set_auto_ack(False) +radio.set_dynamic_payloads(False) +radio.crc_length = CrcLength.DISABLED +radio.set_auto_retries(0, 0) + +# use reverse engineering tactics for a better "snapshot" +radio.address_length = 2 +# The worst possible addresses. These are designed to confuse the radio into thinking +# the RF signal's preamble is part of the packet/payload. +noise_addresses = [ + b"\x55\x55", + b"\xaa\xaa", + b"\xa0\xaa", + b"\x0a\xaa", + b"\xa5\xaa", + b"\x5a\xaa", +] +for pipe, address in enumerate(noise_addresses): + radio.open_rx_pipe(pipe, address) + + +def scan(timeout: int = 30): + """Traverse the spectrum of accessible frequencies and print any detection + of ambient signals. + + :param int timeout: The number of seconds in which scanning is performed. + """ + # print the vertical header of channel numbers + print("0" * 100 + "1" * 26) + for i in range(13): + print(str(i % 10) * (10 if i < 12 else 6), sep="", end="") + print("") # endl + for i in range(126): + print(str(i % 10), sep="", end="") + print("\n" + "~" * 126) + + signals = [0] * 126 # store the signal count for each channel + sweeps = 0 # keep track of the number of sweeps made through all channels + curr_channel = 0 + start_timer = time.monotonic() # start the timer + while time.monotonic() - start_timer < timeout: + radio.channel = curr_channel + radio.listen = True # start a RX session + time.sleep(0.00013) # wait 130 microseconds + found_signal = radio.rpd + radio.listen = False # end the RX session + found_signal = found_signal or radio.rpd or radio.available() + + # count signal as interference + signals[curr_channel] += found_signal + # clear the RX FIFO if a signal was detected/captured + if found_signal: + radio.flush_rx() # flush the RX FIFO because it asserts the RPD flag + endl = False + if curr_channel >= 124: + sweeps += 1 + if int(sweeps / 100) > 0: + endl = True + sweeps = 0 + + # output the signal counts per channel + sig_cnt = signals[curr_channel] + print( + ("%X" % min(15, sig_cnt)) if sig_cnt else "-", + sep="", + end="" if curr_channel < 125 else ("\n" if endl else "\r"), + ) + curr_channel = curr_channel + 1 if curr_channel < 125 else 0 + if endl: + signals = [0] * 126 # reset the signal counts for new line + + # finish printing results and end with a new line + while curr_channel < len(signals) - 1: + curr_channel += 1 + sig_cnt = signals[curr_channel] + print(("%X" % min(15, sig_cnt)) if sig_cnt else "-", sep="", end="") + print("") + + +def hex_data_str(data: bytes) -> str: + return " ".join(["%02x" % b for b in data]) + + +def noise(timeout: float = 1, channel: Optional[int] = None): + """print a stream of detected noise for duration of time. + + :param float timeout: The number of seconds to scan for ambient noise. + :param int channel: The specific channel to focus on. If not provided, then the + radio's current setting is used. + """ + if channel is not None: + radio.channel = channel + radio.listen = True + timeout += time.monotonic() + while time.monotonic() < timeout: + signal = radio.read() + if signal: + print(hex_data_str(signal)) + radio.listen = False + while not radio.get_fifo_state(about_tx=False) != FifoState.Full: + # dump the left overs in the RX FIFO + print(hex_data_str(radio.read())) + + +def set_role(): + """Set the role using stdin stream. Timeout arg for scan() can be + specified using a space delimiter (e.g. 'S 10' calls `scan(10)`) + """ + user_input = ( + input( + "*** Enter 'S' to perform scan.\n" + "*** Enter 'N' to display noise.\n" + "*** Enter 'Q' to quit example.\n" + ) + or "?" + ) + user_input = user_input.split() + if user_input[0].upper().startswith("S"): + scan(*[int(x) for x in user_input[1:2]]) + return True + if user_input[0].upper().startswith("N"): + noise(*[int(x) for x in user_input[1:3]]) + return True + if user_input[0].upper().startswith("Q"): + radio.power = False + return False + print(user_input[0], "is an unrecognized input. Please try again.") + return True + + +print(" nRF24L01 scanner test") +print( + "!!!Make sure the terminal is wide enough for 126 characters on 1 line." + " If this line is wrapped, then the output will look bad!" +) + +if __name__ == "__main__": + try: + while set_role(): + pass # continue example until 'Q' is entered + except KeyboardInterrupt: + print(" Keyboard Interrupt detected. Powering down radio...") + radio.power = False +else: + print(" Run scan() to initiate scan for ambient signals.") + print(" Run noise() to display ambient signals' data (AKA noise).") diff --git a/examples/python/scanner_curses.py b/examples/python/scanner_curses.py new file mode 100644 index 0000000..43ccd6e --- /dev/null +++ b/examples/python/scanner_curses.py @@ -0,0 +1,244 @@ +"""A scanner example written in python using the std lib's ncurses wrapper. + +This is a good diagnostic tool to check whether you're picking a +good channel for your application. + +See documentation at https://nRF24.github.io/rf24-rs +""" + +import curses +from pathlib import Path +import time +from typing import List, Tuple +from rf24_py import RF24, DataRate, CrcLength + +print(__file__) # print example name + +# The radio's CE Pin uses a GPIO number. +# On Linux, consider the device path `/dev/gpiochip`: +# - `` is the gpio chip's identifying number. +# Using RPi4 (or earlier), this number is `0` (the default). +# Using the RPi5, this number is actually `4`. +# The radio's CE pin must connected to a pin exposed on the specified chip. +CE_PIN = 22 # for GPIO22 +# try detecting RPi5 first; fall back to default +DEV_GPIO_CHIP = 4 if Path("/dev/gpiochip4").exists() else 0 + +# The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). +# On Linux, consider the device path `/dev/spidev.`: +# - `` is the SPI bus number (defaults to `0`) +# - `` is the CSN pin (must be unique for each device on the same SPI bus) +CSN_PIN = 0 # aka CE0 for SPI bus 0 (/dev/spidev0.0) + +# create a radio object for the specified hard ware config: +radio = RF24(CE_PIN, CSN_PIN, dev_gpio_chip=DEV_GPIO_CHIP) + + +OFFERED_DATA_RATES = ["1 Mbps", "2 Mbps", "250 kbps"] +AVAILABLE_RATES = [DataRate.Mbps1, DataRate.Mbps2, DataRate.Kbps250] +TOTAL_CHANNELS = 126 +CACHE_MAX = 5 # the depth of history to calculate peaks + +# To detect noise, we'll use the worst addresses possible (a reverse engineering +# tactic). These addresses are designed to confuse the radio into thinking that the +# RF signal's preamble is part of the packet/payload. +noise_address = [ + b"\x55\x55", + b"\xaa\xaa", + b"\x0a\xaa", + b"\xa0\xaa", + b"\x00\xaa", + b"\xab\xaa", +] + + +class ChannelHistory: + def __init__(self) -> None: + #: FIFO for tracking peak decays + self._history: List[bool] = [False] * CACHE_MAX + #: for the total signal counts + self.total: int = 0 + + def push(self, value: bool) -> int: + """Push a scan result's value into history while returning the sum of cached + signals found. This function also increments the total signal count accordingly. + """ + self._history = self._history[1:] + [value] + self.total += value + return self._history.count(True) + + +#: An array of histories for each channel +stored = [ChannelHistory() for _ in range(TOTAL_CHANNELS)] + + +class ProgressBar: # pylint: disable=too-few-public-methods + """This represents a progress bar using a curses window object.""" + + def __init__( # pylint: disable=too-many-arguments,invalid-name + self, + x: int, + y: int, + cols: int, + std_scr: curses.window, + label: str, + color: int, + ): + self.x, self.y, self.width, self.win, self.color = (x, y, cols, std_scr, color) + self.win.move(self.y, self.x) + self.win.attron(curses.color_pair(self.color)) + self.win.addstr(label) # always labeled in MHz (4 digits) + for _ in range(self.width - 8): # draw the empty bar + self.win.addch(curses.ACS_HLINE) + self.win.addstr(" - ") # draw the initial signal count + self.win.attroff(curses.color_pair(self.color)) + + def update(self, completed: int, signal_count: int): + """Update the progress bar.""" + count = " - " + if signal_count: + count = " %X " % min(0xF, signal_count) + filled = (self.width - 8) * completed / CACHE_MAX + offset_x = 5 + self.win.move(self.y, self.x + offset_x) + for i in range(offset_x, self.width - 3): + bar_filled = i < (filled + offset_x) + bar_color = 5 if bar_filled else self.color + self.win.attron(curses.color_pair(bar_color)) + self.win.addch("=" if bar_filled else curses.ACS_HLINE) + self.win.attroff(curses.color_pair(bar_color)) + self.win.attron(curses.color_pair(self.color)) + self.win.addstr(count) + self.win.attroff(curses.color_pair(self.color)) + + +def init_display(window) -> List[ProgressBar]: + """Creates a table of progress bars (1 for each channel).""" + progress_bars: List[ProgressBar] = [ + ProgressBar(0, 0, 0, window, "", 0) + ] * TOTAL_CHANNELS + bar_w = int(curses.COLS / 6) + for i in range(21): # 21 rows + for j in range(i, i + (21 * 6), 21): # 6 columns + color = 7 if int(j / 21) % 2 else 3 + progress_bars[j] = ProgressBar( + x=bar_w * int(j / 21), + y=i + 3, + cols=bar_w, + std_scr=window, + label=f"{2400 + (j)} ", + color=color, + ) + return progress_bars + + +def init_radio(): + """init the radio""" + radio.begin() + radio.set_auto_ack(False) + radio.crc_length = CrcLength.DISABLED + radio.address_length = 2 + for pipe, address in enumerate(noise_address): + radio.open_rx_pipe(pipe, address) + radio.listen = True + radio.listen = False + radio.flush_rx() + + +def init_curses(): + """init the curses interface""" + std_scr = curses.initscr() + curses.noecho() + curses.cbreak() + curses.start_color() + curses.use_default_colors() + curses.init_pair(3, curses.COLOR_YELLOW, -1) + curses.init_pair(5, curses.COLOR_MAGENTA, -1) + curses.init_pair(7, curses.COLOR_WHITE, -1) + return std_scr + + +def de_init_curses(spectrum_passes: int): + """de-init the curses interface""" + curses.nocbreak() + curses.echo() + curses.endwin() + noisy_channels: int = 0 + digit_w = len(str(spectrum_passes)) + for channel, data in enumerate(stored): + if data.total: + count_padding = " " * (digit_w - len(str(data.total))) + percentage = round(data.total / spectrum_passes * 100, 3) + print( + f" {channel:>3}: {count_padding}{data.total} / {spectrum_passes} ({percentage} %)" + ) + noisy_channels += 1 + print( + f"{noisy_channels} channels detected signals out of {spectrum_passes}", + "passes on the entire spectrum.", + ) + + +def get_user_input() -> Tuple[int, int]: + """Get input parameters for the scan from the user.""" + for i, d_rate in enumerate(OFFERED_DATA_RATES): + print(f"{i + 1}. {d_rate}") + d_rate = input("Select your data rate [1, 2, 3] (defaults to 1 Mbps) ") + duration = input("How long (in seconds) to perform scan? ") + while not duration.isdigit(): + print("Please enter a positive number.") + duration = input("How long (in seconds) to perform scan? ") + return ( + max(1, min(3, 1 if not d_rate.isdigit() else int(d_rate))) - 1, + abs(int(duration)), + ) + + +def scan_channel(channel: int) -> bool: + """Scan a specified channel and report if a signal was detected.""" + radio.channel = channel + radio.listen = True + time.sleep(0.00013) + found_signal = radio.rpd + radio.listen = False + if found_signal or radio.rpd or radio.available(): + radio.flush_rx() + return True + return False + + +def main(): + spectrum_passes = 0 + data_rate, duration = get_user_input() + print(f"Scanning for {duration} seconds at {OFFERED_DATA_RATES[data_rate]}") + init_radio() + radio.data_rate = AVAILABLE_RATES[data_rate] + try: + std_scr = init_curses() + timer_prompt = "Scanning for {:>3} seconds at " + OFFERED_DATA_RATES[data_rate] + std_scr.addstr(0, 0, "Channels are labeled in MHz.") + std_scr.addstr(1, 0, "Signal counts are clamped to a single hexadecimal digit.") + bars = init_display(std_scr) + channel, val = (0, False) + end = time.monotonic() + duration + while time.monotonic() < end: + std_scr.addstr(2, 0, timer_prompt.format(int(end - time.monotonic()))) + val = scan_channel(channel) + cache_sum = stored[channel].push(val) + if stored[channel].total: + bars[channel].update(cache_sum, stored[channel].total) + std_scr.refresh() + if channel + 1 == TOTAL_CHANNELS: + channel = 0 + spectrum_passes += 1 + else: + channel += 1 + finally: + radio.power = False + de_init_curses(spectrum_passes) + + +if __name__ == "__main__": + main() +else: + print("Enter 'main()' to run the program.") diff --git a/examples/python/streaming_data.py b/examples/python/streaming_data.py new file mode 100644 index 0000000..ea51635 --- /dev/null +++ b/examples/python/streaming_data.py @@ -0,0 +1,180 @@ +""" +Example of library usage for streaming multiple payloads. + +See documentation at https://nRF24.github.io/rf24-rs +""" + +from pathlib import Path +import time +from rf24_py import RF24, PaLevel, StatusFlags + +print(__file__) # print example name + +# The radio's CE Pin uses a GPIO number. +# On Linux, consider the device path `/dev/gpiochip{N}`: +# - `{N}` is the gpio chip's identifying number. +# Using RPi4 (or earlier), this number is `0` (the default). +# Using the RPi5, this number is actually `4`. +# The radio's CE pin must connected to a pin exposed on the specified chip. +CE_PIN = 22 # for GPIO22 +# try detecting RPi5 first; fall back to default +DEV_GPIO_CHIP = 4 if Path("/dev/gpiochip4").exists() else 0 + +# The radio's CSN Pin corresponds the SPI bus's CS pin (aka CE pin). +# On Linux, consider the device path `/dev/spidev{A}.{B}`: +# - `{A}` is the SPI bus number (defaults to `0`) +# - `{B}` is the CSN pin (must be unique for each device on the same SPI bus) +CSN_PIN = 0 # aka CE0 for SPI bus 0 (/dev/spidev0.0) + +# create a radio object for the specified hard ware config: +radio = RF24(CE_PIN, CSN_PIN, dev_gpio_chip=DEV_GPIO_CHIP) + +# For this example, we will use different addresses +# An address need to be a buffer protocol object (bytearray) +address = [b"1Node", b"2Node"] +# It is very helpful to think of an address as a path instead of as +# an identifying device destination + +# to use different addresses on a pair of radios, we need a variable to +# uniquely identify which address this radio will use to transmit +# 0 uses address[0] to transmit, 1 uses address[1] to transmit +radio_number = bool( + int(input("Which radio is this? Enter '0' or '1'. Defaults to '0' ") or 0) +) + +# initialize the nRF24L01 on the spi bus +radio.begin() + +# set the Power Amplifier level to -12 dBm since this test example is +# usually run with nRF24L01 transceivers in close proximity of each other +radio.pa_level = PaLevel.LOW # PaLevel.MAX is default + +# set TX address of RX node into the TX pipe +radio.open_tx_pipe(address[radio_number]) # always uses pipe 0 + +# set RX address of TX node into an RX pipe +radio.open_rx_pipe(1, address[not radio_number]) # using pipe 1 + +# for debugging +# radio.print_pretty_details() + + +def make_buffer(buf_iter: int, size: int = 32): + """return a list of payloads""" + # we'll use `size` for the number of payloads in the list and the + # payloads' length + # prefix payload with a sequential letter to indicate which + # payloads were lost (if any) + buff = bytes([buf_iter + (65 if 0 <= buf_iter < 26 else 71)]) + for j in range(size - 1): + char = bool(j >= (size - 1) / 2 + abs((size - 1) / 2 - buf_iter)) + char |= bool(j < (size - 1) / 2 - abs((size - 1) / 2 - buf_iter)) + buff += bytes([char + 48]) + return buff + + +def master(count: int = 1, size: int = 32): + """Uses all 3 levels of the TX FIFO `RF24.writeFast()`""" + if size < 6: + print("setting size to 6;", size, "is not allowed for this test.") + size = 6 + + # save on transmission time by setting the radio to only transmit the + # number of bytes we need to transmit + radio.payload_length = size # the default is the maximum 32 bytes + + radio.listen = False # ensures the nRF24L01 is in TX mode + for cnt in range(count): # transmit the same payloads this many times + radio.flush_tx() # clear the TX FIFO so we can use all 3 levels + # NOTE the write_only parameter does not initiate sending + buf_iter = 0 # iterator of payloads for the while loop + failures = 0 # keep track of manual retries + start_timer = time.monotonic() * 1000 # start timer + while buf_iter < size: # cycle through all the payloads + buf = make_buffer(buf_iter, size) # make a payload + while not radio.write(buf): + # upload to TX FIFO failed because TX FIFO is full. + # check for transmission errors + radio.update() + flags: StatusFlags = radio.get_status_flags() + if flags.tx_df: # reception failed + failures += 1 # increment manual retries + radio.rewrite() # resets the tx_df flag and reuses payload in TX FIFO + if failures > 99: + break + if failures > 99 and buf_iter < 7 and cnt < 2: + # we need to prevent an infinite loop + print("Make sure slave() node is listening. Quitting master_fifo()") + buf_iter = size + 1 # be sure to exit the while loop + radio.flush_tx() # discard all payloads in TX FIFO + break + buf_iter += 1 + end_timer = time.monotonic() * 1000 # end timer + print( + f"Transmission took {end_timer - start_timer} ms with", + f"{failures} failures detected.", + ) + radio.flush_tx() # ensure radio exits active TX mode + + +def slave(timeout: int = 5, size: int = 32): + """Stops listening after a `timeout` with no response""" + + # save on transmission time by setting the radio to only transmit the + # number of bytes we need to transmit + radio.payload_length = size # the default is the maximum 32 bytes + + radio.start_listening() # put radio into RX mode and power up + count = 0 # keep track of the number of received payloads + start_timer = time.monotonic() # start timer + while time.monotonic() < start_timer + timeout: + if radio.available(): + count += 1 + # retrieve the received packet's payload + receive_payload = radio.read(size) + print(f"Received: {repr(receive_payload)} - {count}") + start_timer = time.monotonic() # reset timer on every RX payload + + # recommended behavior is to keep in TX mode while idle + radio.listen = False # put the nRF24L01 is in TX mode + + +def set_role(): + """Set the role using stdin stream. Timeout arg for slave() can be + specified using a space delimiter (e.g. 'R 10' calls `slave(10)`) + + :return: + - True when role is complete & app should continue running. + - False when app should exit + """ + user_input = ( + input( + "*** Enter 'R' for receiver role.\n" + "*** Enter 'T' for transmitter role.\n" + "*** Enter 'Q' to quit example.\n" + ) + or "?" + ) + user_input = user_input.split() + if user_input[0].upper().startswith("R"): + slave(*[int(x) for x in user_input[1:3]]) + return True + if user_input[0].upper().startswith("T"): + master(*[int(x) for x in user_input[1:3]]) + return True + if user_input[0].upper().startswith("Q"): + radio.power = False + return False + print(user_input[0], "is an unrecognized input. Please try again.") + return True + + +if __name__ == "__main__": + try: + while set_role(): + pass # continue example until 'Q' is entered + except KeyboardInterrupt: + print(" Keyboard Interrupt detected. Exiting...") + radio.power = False +else: + print(" Run slave() on receiver\n Run master() on transmitter") diff --git a/examples/.cargo/config.toml b/examples/rust/.cargo/config.toml similarity index 100% rename from examples/.cargo/config.toml rename to examples/rust/.cargo/config.toml diff --git a/examples/Cargo.toml b/examples/rust/Cargo.toml similarity index 95% rename from examples/Cargo.toml rename to examples/rust/Cargo.toml index 3667297..6aa8363 100644 --- a/examples/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true edition.workspace = true [dependencies] -rf24-rs = {path = "../lib"} +rf24-rs = {path = "../../lib"} embedded-hal = "1.0.0" anyhow = {version = "1.0.89", default-features = false } linux-embedded-hal = {version = "0.4.0", optional = true} diff --git a/examples/Embed.toml b/examples/rust/Embed.toml similarity index 100% rename from examples/Embed.toml rename to examples/rust/Embed.toml diff --git a/examples/README.md b/examples/rust/README.md similarity index 100% rename from examples/README.md rename to examples/rust/README.md diff --git a/examples/src/lib.rs b/examples/rust/src/lib.rs similarity index 100% rename from examples/src/lib.rs rename to examples/rust/src/lib.rs diff --git a/examples/src/linux.rs b/examples/rust/src/linux.rs similarity index 100% rename from examples/src/linux.rs rename to examples/rust/src/linux.rs diff --git a/examples/src/main.rs b/examples/rust/src/main.rs similarity index 100% rename from examples/src/main.rs rename to examples/rust/src/main.rs diff --git a/examples/src/rp2040.rs b/examples/rust/src/rp2040.rs similarity index 100% rename from examples/src/rp2040.rs rename to examples/rust/src/rp2040.rs diff --git a/lib/src/enums.rs b/lib/src/enums.rs index a653e6d..0be0870 100644 --- a/lib/src/enums.rs +++ b/lib/src/enums.rs @@ -54,3 +54,14 @@ pub enum FifoState { /// Represent the state of a FIFO when it is not full but not empty either. Occupied, } + +#[derive(Clone, Copy, Debug, Default)] +/// A struct used to describe the different interruptable events. +pub struct StatusFlags { + /// A flag to describe if RX Data Ready to read. + pub rx_dr: bool, + /// A flag to describe if TX Data Sent. + pub tx_ds: bool, + /// A flag to describe if TX Data Failed. + pub tx_df: bool, +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 230a589..a557183 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -6,7 +6,7 @@ #![no_std] mod enums; -pub use enums::{CrcLength, DataRate, FifoState, PaLevel}; +pub use enums::{CrcLength, DataRate, FifoState, PaLevel, StatusFlags}; pub mod radio; #[cfg(test)] diff --git a/lib/src/radio/mod.rs b/lib/src/radio/mod.rs index 4baaebe..9e767d5 100644 --- a/lib/src/radio/mod.rs +++ b/lib/src/radio/mod.rs @@ -1,7 +1,7 @@ mod rf24; pub use rf24::{Nrf24Error, RF24}; pub mod prelude { - use crate::enums::{CrcLength, DataRate, FifoState, PaLevel}; + use crate::enums::{CrcLength, DataRate, FifoState, PaLevel, StatusFlags}; pub trait EsbPipe { type PipeErrorType; @@ -51,9 +51,14 @@ pub mod prelude { /// Close a specified pipe from receiving data when radio is in RX role. fn close_rx_pipe(&mut self, pipe: u8) -> Result<(), Self::PipeErrorType>; + + /// Set the address length (applies to all pipes). fn set_address_length(&mut self, length: u8) -> Result<(), Self::PipeErrorType>; + + /// Get the currently configured address length (applied to all pipes). fn get_address_length(&mut self) -> Result; } + pub trait EsbChannel { type ChannelErrorType; @@ -69,24 +74,40 @@ pub mod prelude { pub trait EsbStatus { type StatusErrorType; - fn get_status_flags( - &mut self, - rx_dr: &mut Option, - tx_ds: &mut Option, - tx_df: &mut Option, - ) -> Result<(), Self::StatusErrorType>; + + /// Get the [`StatusFlags`] state that was cached from the latest SPI transaction. + fn get_status_flags(&self, flags: &mut StatusFlags); + + /// Configure which status flags trigger the radio's IRQ pin. + /// + /// Set any member of [`StatusFlags`] to `false` to have the + /// IRQ pin ignore the corresponding event. + /// By default, all events are enabled and will trigger the IRQ pin, + /// a behavior equivalent to `set_status_flags(None)`. fn set_status_flags( &mut self, - rx_dr: bool, - tx_ds: bool, - tx_df: bool, + flags: Option, ) -> Result<(), Self::StatusErrorType>; + + /// Clear the radio's IRQ status flags + /// + /// This needs to be done after the event has been handled. + /// + /// Set any member of [`StatusFlags`] to `true` to clear the corresponding + /// interrupt event. Setting any member of [`StatusFlags`] to `false` will leave + /// the corresponding status flag untouched. This means that the IRQ pin can remain + /// active (LOW) when multiple events occurred but only flag was cleared. + /// + /// Pass [`None`] to clear all status flags. fn clear_status_flags( &mut self, - rx_dr: bool, - tx_ds: bool, - tx_df: bool, + flags: Option, ) -> Result<(), Self::StatusErrorType>; + + /// Refresh the internal cache of status byte + /// (which is also saved from every SPI transaction). + /// + /// Use [`EsbStatus::get_status_flags()`] to get the updated status flags. fn update(&mut self) -> Result<(), Self::StatusErrorType>; } @@ -341,7 +362,7 @@ pub mod prelude { /// This wakes the radio from a sleep state, resulting in a /// power standby mode that allows the radio to receive or transmit data. /// - /// To ensure proper operation, this function will `delay` after the radio is awaken. + /// To ensure proper operation, this function will `delay` after the radio is powered up. /// If the `delay` parameter is given a [`Some`] value, then the this function /// will wait for the specified number of microseconds. If `delay` is a [`None`] /// value, this function will wait for 5 milliseconds. @@ -353,6 +374,11 @@ pub mod prelude { /// radio.start_listening().unwrap(); /// ``` fn power_up(&mut self, delay: Option) -> Result<(), Self::PowerErrorType>; + + /// Get the current (cached) state of the radio's power. + /// + /// Returns `true` if powered up or `false` if powered down. + fn is_powered(&self) -> bool; } pub trait EsbCrcLength { @@ -362,7 +388,8 @@ pub mod prelude { fn get_crc_length(&mut self) -> Result; /// Set the radio's CRC (Cyclical Redundancy Checksum) length - fn set_crc_length(&mut self, crc_length: CrcLength) -> Result<(), Self::CrcLengthErrorType>; + fn set_crc_length(&mut self, crc_length: CrcLength) + -> Result<(), Self::CrcLengthErrorType>; } pub trait EsbDataRate { @@ -398,6 +425,9 @@ pub mod prelude { /// Put the radio into TX role fn stop_listening(&mut self) -> Result<(), Self::RadioErrorType>; + /// Is the radio in RX mode? + fn is_listening(&self) -> bool; + /// Blocking write. /// /// This transmits a payload (given by `buf`) and returns a bool describing if @@ -451,7 +481,10 @@ pub mod prelude { /// /// The `len` parameter determines how much data is stored to `buf`. Ultimately, /// the value of `len` is restricted by the radio's maximum 32 byte limit and the - /// length of the given `buf`. - fn read(&mut self, buf: &mut [u8], len: u8) -> Result<(), Self::RadioErrorType>; + /// length of the given `buf`. Pass [`None`] to automatically use static payload length + /// (set by [`EsbPayloadLength::set_payload_length()`]) or the dynamic payload length + /// (fetched internally using [`EsbPayloadLength::get_dynamic_payload_length()`]) if + /// dynamic payload lengths are enable (see [`EsbPayloadLength::set_dynamic_payloads()`]). + fn read(&mut self, buf: &mut [u8], len: Option) -> Result; } } diff --git a/lib/src/radio/rf24/power.rs b/lib/src/radio/rf24/power.rs index 8ef8f27..af10cc1 100644 --- a/lib/src/radio/rf24/power.rs +++ b/lib/src/radio/rf24/power.rs @@ -44,6 +44,11 @@ where } Ok(()) } + + /// Is the radio powered up? + fn is_powered(&self) -> bool { + (self._config_reg & 2) != 2 + } } ///////////////////////////////////////////////////////////////////////////////// @@ -108,4 +113,21 @@ mod test { spi_mock.done(); pin_mock.done(); } + + #[test] + pub fn power_getter() { + // Create pin + let pin_expectations = []; + let mut pin_mock = PinMock::new(&pin_expectations); + + // create delay fn + let delay_mock = NoopDelay::new(); + + let spi_expectations = vec![]; + let mut spi_mock = SpiMock::new(&spi_expectations); + let radio = RF24::new(pin_mock.clone(), spi_mock.clone(), delay_mock); + assert!(radio.is_powered()); + spi_mock.done(); + pin_mock.done(); + } } diff --git a/lib/src/radio/rf24/radio.rs b/lib/src/radio/rf24/radio.rs index ef23c5f..9398047 100644 --- a/lib/src/radio/rf24/radio.rs +++ b/lib/src/radio/rf24/radio.rs @@ -1,6 +1,5 @@ use super::{commands, mnemonics, registers, Nrf24Error, RF24}; -use crate::radio::prelude::*; -use crate::DataRate; +use crate::{radio::prelude::*, DataRate, StatusFlags}; use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiDevice}; impl EsbRadio for RF24 @@ -70,7 +69,7 @@ where // Reset current status // Notice reset and flush is the last thing we do - self.clear_status_flags(true, true, true)?; + self.clear_status_flags(None)?; // Flush buffers self.flush_rx()?; @@ -100,7 +99,7 @@ where fn start_listening(&mut self) -> Result<(), Self::RadioErrorType> { self._config_reg |= 1; self.spi_write_byte(registers::CONFIG, self._config_reg)?; - self.clear_status_flags(true, true, true)?; + self.clear_status_flags(None)?; self._ce_pin.set_high().map_err(Nrf24Error::Gpo)?; // Restore the pipe0 address, if exists @@ -128,6 +127,10 @@ where self.spi_write_byte(registers::EN_RXADDR, out) } + fn is_listening(&self) -> bool { + (self._config_reg & 1) == 1 + } + /// See [`EsbRadio::send()`] for implementation-agnostic detail. /// /// This function calls [`RF24::flush_tx()`] upon entry, but it does not @@ -163,7 +166,7 @@ where ask_no_ack: bool, start_tx: bool, ) -> Result { - self.clear_status_flags(true, true, true)?; + self.clear_status_flags(None)?; if self._status & 1 == 1 { // TX FIFO is full already return Ok(false); @@ -209,17 +212,27 @@ where /// padding for the data saved to the `buf` parameter's object. /// The nRF24L01 will repeatedly use the last byte from the last /// payload even when [`RF24::read()`] is called with an empty RX FIFO. - fn read(&mut self, buf: &mut [u8], len: u8) -> Result<(), Self::RadioErrorType> { - let buf_len = (buf.len() as u8).min(len).min(32); + fn read(&mut self, buf: &mut [u8], len: Option) -> Result { + let buf_len = (buf.len() as u8) + .min(len.unwrap_or(if self._dynamic_payloads_enabled { + self.get_dynamic_payload_length()? + } else { + self._payload_length + })) + .min(32); if buf_len == 0 { - return Ok(()); + return Ok(0); } self.spi_read(buf_len, commands::R_RX_PAYLOAD)?; for i in 0..buf_len { buf[i as usize] = self._buf[i as usize + 1]; } - self.clear_status_flags(true, false, false)?; - Ok(()) + let flags = StatusFlags { + rx_dr: true, + ..Default::default() + }; + self.clear_status_flags(Some(flags))?; + Ok(buf_len) } fn resend(&mut self) -> Result { @@ -234,7 +247,12 @@ where fn rewrite(&mut self) -> Result<(), Self::RadioErrorType> { self._ce_pin.set_low().map_err(Nrf24Error::Gpo)?; - self.clear_status_flags(false, true, true)?; + let flags = StatusFlags { + rx_dr: false, + tx_ds: true, + tx_df: true, + }; + self.clear_status_flags(Some(flags))?; self.spi_read(0, commands::REUSE_TX_PL)?; self._ce_pin.set_high().map_err(Nrf24Error::Gpo) } @@ -251,8 +269,7 @@ where mod test { extern crate std; use super::{commands, registers, RF24}; - use crate::radio::prelude::*; - use crate::radio::rf24::mnemonics; + use crate::radio::{prelude::*, rf24::mnemonics}; use crate::spi_test_expects; use embedded_hal_mock::eh1::delay::NoopDelay; use embedded_hal_mock::eh1::digital::{ @@ -555,12 +572,27 @@ mod test { // create delay fn let delay_mock = NoopDelay::new(); - let mut buf = [0u8; 33]; - buf[0] = commands::R_RX_PAYLOAD; + let mut buf_static = [0u8; 33]; + buf_static[0] = commands::R_RX_PAYLOAD; + let mut buf_dynamic = [0x55u8; 33]; + buf_dynamic[0] = commands::R_RX_PAYLOAD; + buf_dynamic[1] = 32; let spi_expectations = spi_test_expects![ // read RX payload - (buf.clone().to_vec(), vec![0x55u8; 33]), + (buf_static.clone().to_vec(), vec![0x55u8; 33]), + // clear the rx_dr event + ( + vec![ + registers::STATUS | commands::W_REGISTER, + mnemonics::MASK_RX_DR, + ], + vec![0xEu8, 0u8], + ), + // read dynamic payload length + (vec![commands::R_RX_PL_WID, 0u8], vec![0xEu8, 32u8]), + // read RX payload + (buf_dynamic.clone().to_vec(), vec![0xAAu8; 33]), // clear the rx_dr event ( vec![ @@ -573,8 +605,11 @@ mod test { let mut spi_mock = SpiMock::new(&spi_expectations); let mut radio = RF24::new(pin_mock.clone(), spi_mock.clone(), delay_mock); let mut payload = [0u8; 32]; - radio.read(&mut payload, 32).unwrap(); + assert_eq!(32u8, radio.read(&mut payload, None).unwrap()); assert_eq!(payload, [0x55u8; 32]); + radio._dynamic_payloads_enabled = true; + assert_eq!(32u8, radio.read(&mut payload, None).unwrap()); + assert_eq!(payload, [0xAA; 32]); spi_mock.done(); pin_mock.done(); } diff --git a/lib/src/radio/rf24/status.rs b/lib/src/radio/rf24/status.rs index fdd1d39..e7a1fe3 100644 --- a/lib/src/radio/rf24/status.rs +++ b/lib/src/radio/rf24/status.rs @@ -1,6 +1,9 @@ use embedded_hal::{delay::DelayNs, digital::OutputPin, spi::SpiDevice}; -use crate::radio::{prelude::EsbStatus, Nrf24Error, RF24}; +use crate::{ + enums::StatusFlags, + radio::{prelude::EsbStatus, Nrf24Error, RF24}, +}; use super::{commands, mnemonics, registers}; @@ -12,58 +15,41 @@ where { type StatusErrorType = Nrf24Error; - /// Configure which status flags trigger the radio's IRQ pin. - /// - /// The supported interrupt events correspond to the parameters: - /// - `rx_dr` means "RX Data Ready" - /// - `tx_ds` means "TX Data Sent" - /// - `tx_df` means "TX Data Failed" to send - /// - /// Set any parameter to `false` to have the IRQ pin ignore the corresponding event. - /// By default, all events are enabled and will trigger the IRQ pin, a behavior - /// equivalent to `set_status_flags(true, true, true)`. fn set_status_flags( &mut self, - rx_dr: bool, - tx_ds: bool, - tx_df: bool, + flags: Option, ) -> Result<(), Self::StatusErrorType> { + let flags = flags.unwrap_or(StatusFlags { + rx_dr: true, + tx_ds: true, + tx_df: true, + }); self.spi_read(1, registers::CONFIG)?; - let mut new_config = self._buf[1] & !(3 << 4); - if !rx_dr { - new_config |= mnemonics::MASK_RX_DR; + self._config_reg = self._buf[1] & !(3 << 4); + if !flags.rx_dr { + self._config_reg |= mnemonics::MASK_RX_DR; } - if !tx_ds { - new_config |= mnemonics::MASK_TX_DS; + if !flags.tx_ds { + self._config_reg |= mnemonics::MASK_TX_DS; } - if !tx_df { - new_config |= mnemonics::MASK_MAX_RT; + if !flags.tx_df { + self._config_reg |= mnemonics::MASK_MAX_RT; } - self.spi_write_byte(registers::CONFIG, new_config) + self.spi_write_byte(registers::CONFIG, self._config_reg) } - /// Clear the radio's IRQ status flags - /// - /// This needs to be done when the event has been handled. - /// - /// The supported interrupt events correspond to the parameters: - /// - `rx_dr` means "RX Data Ready" - /// - `tx_ds` means "TX Data Sent" - /// - `tx_df` means "TX Data Failed" to send - /// - /// Set any parameter to `true` to clear the corresponding interrupt event. - /// Setting a parameter to `false` will leave the corresponding status flag untouched. - /// This means that the IRQ pin can remain active (LOW) when multiple events occurred - /// but only flag was cleared. fn clear_status_flags( &mut self, - rx_dr: bool, - tx_ds: bool, - tx_df: bool, + flags: Option, ) -> Result<(), Self::StatusErrorType> { - let new_config = (mnemonics::MASK_RX_DR * rx_dr as u8) - | (mnemonics::MASK_TX_DS * tx_ds as u8) - | (mnemonics::MASK_MAX_RT * tx_df as u8); + let flags = flags.unwrap_or(StatusFlags { + rx_dr: true, + tx_ds: true, + tx_df: true, + }); + let new_config = (mnemonics::MASK_RX_DR * flags.rx_dr as u8) + | (mnemonics::MASK_TX_DS * flags.tx_ds as u8) + | (mnemonics::MASK_MAX_RT * flags.tx_df as u8); self.spi_write_byte(registers::STATUS, new_config) } @@ -71,22 +57,10 @@ where self.spi_read(0, commands::NOP) } - fn get_status_flags( - &mut self, - rx_dr: &mut Option, - tx_ds: &mut Option, - tx_df: &mut Option, - ) -> Result<(), Self::StatusErrorType> { - if let Some(f) = rx_dr { - *f = self._status & mnemonics::MASK_RX_DR > 0; - } - if let Some(f) = tx_ds { - *f = self._status & mnemonics::MASK_TX_DS > 0; - } - if let Some(f) = tx_df { - *f = self._status & mnemonics::MASK_MAX_RT > 0; - } - Ok(()) + fn get_status_flags(&self, flags: &mut StatusFlags) { + flags.rx_dr = self._status & mnemonics::MASK_RX_DR > 0; + flags.tx_ds = self._status & mnemonics::MASK_TX_DS > 0; + flags.tx_df = self._status & mnemonics::MASK_MAX_RT > 0; } } @@ -95,6 +69,7 @@ where #[cfg(test)] mod test { extern crate std; + use crate::enums::StatusFlags; use crate::radio::prelude::EsbStatus; use crate::radio::rf24::commands; use crate::spi_test_expects; @@ -121,15 +96,11 @@ mod test { let mut spi_mock = SpiMock::new(&spi_expectations); let mut radio = RF24::new(pin_mock.clone(), spi_mock.clone(), delay_mock); radio.update().unwrap(); - let mut rx_dr = Some(false); - let mut tx_ds = Some(false); - let mut tx_df = Some(false); - radio - .get_status_flags(&mut rx_dr, &mut tx_ds, &mut tx_df) - .unwrap(); - assert!(rx_dr.is_some_and(|rv| rv)); - assert!(tx_ds.is_some_and(|rv| rv)); - assert!(tx_df.is_some_and(|rv| rv)); + let mut flags = StatusFlags::default(); + radio.get_status_flags(&mut flags); + assert!(flags.rx_dr); + assert!(flags.tx_ds); + assert!(flags.tx_df); spi_mock.done(); pin_mock.done(); } @@ -143,22 +114,20 @@ mod test { // create delay fn let delay_mock = NoopDelay::new(); - let spi_expectations = [ + let spi_expectations = spi_test_expects![ // read the CONFIG register value - SpiTransaction::transaction_start(), - SpiTransaction::transfer_in_place(vec![registers::CONFIG, 0u8], vec![0xEu8, 0xFu8]), - SpiTransaction::transaction_end(), + (vec![registers::CONFIG, 0u8], vec![0xEu8, 0xFu8]), // set the CONFIG register value to disable all IRQ events - SpiTransaction::transaction_start(), - SpiTransaction::transfer_in_place( + ( vec![registers::CONFIG | commands::W_REGISTER, 0x7Fu8], vec![0xEu8, 0u8], ), - SpiTransaction::transaction_end(), ]; let mut spi_mock = SpiMock::new(&spi_expectations); let mut radio = RF24::new(pin_mock.clone(), spi_mock.clone(), delay_mock); - radio.set_status_flags(false, false, false).unwrap(); + radio + .set_status_flags(Some(StatusFlags::default())) + .unwrap(); spi_mock.done(); pin_mock.done(); } diff --git a/pyproject.toml b/pyproject.toml index f8ff502..67596b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,11 @@ Tracker = "https://github.com/nRF24/rf24-rs/issues" [tool.mypy] show_error_codes = true pretty = true +files = [ + "examples/python/*.py", + "rf24_py.pyi" +] [tool.maturin] features = ["pyo3/extension-module"] -manifest-path = "rf24-py/Cargo.toml" \ No newline at end of file +manifest-path = "rf24-py/Cargo.toml" diff --git a/rf24-node/Cargo.toml b/rf24-node/Cargo.toml index a135875..025f5b1 100644 --- a/rf24-node/Cargo.toml +++ b/rf24-node/Cargo.toml @@ -1,6 +1,6 @@ [package] edition = "2021" -name = "rf24_rf24" +name = "rf24_node" version = "0.0.0" [lib] diff --git a/rf24-node/package.json b/rf24-node/package.json index 069c5f2..92e600e 100644 --- a/rf24-node/package.json +++ b/rf24-node/package.json @@ -28,9 +28,6 @@ "@napi-rs/cli": "^2.18.4", "@types/node": "^22.7.5" }, - "ava": { - "timeout": "3m" - }, "engines": { "node": ">= 10" }, diff --git a/rf24-node/src/enums.rs b/rf24-node/src/enums.rs index 26222cf..5fe499a 100644 --- a/rf24-node/src/enums.rs +++ b/rf24-node/src/enums.rs @@ -1,5 +1,5 @@ #[cfg(target_os = "linux")] -use rf24_rs::{CrcLength, DataRate, FifoState, PaLevel}; +use rf24_rs::{CrcLength, DataRate, FifoState, PaLevel, StatusFlags}; /// Optional configuration parameters to fine tune instantiating the RF24 object. /// Pass this object as third parameter to RF24 constructor. @@ -41,22 +41,32 @@ impl Default for HardwareConfig { /// /// These flags default to `true` if not specified for `RF24.setStatusFlags()` /// or `RF24.clearStatusFlags()`. -#[napi(object)] -pub struct StatusFlags { +#[napi(object, js_name = "StatusFlags")] +#[derive(Default)] +pub struct NodeStatusFlags { /// A flag to describe if RX Data Ready to read. - pub rx_dr: bool, + pub rx_dr: Option, /// A flag to describe if TX Data Sent. - pub tx_ds: bool, + pub tx_ds: Option, /// A flag to describe if TX Data Failed. - pub tx_df: bool, + pub tx_df: Option, } -impl Default for StatusFlags { - fn default() -> Self { +#[cfg(target_os = "linux")] +impl NodeStatusFlags { + pub fn into_inner(self) -> StatusFlags { + StatusFlags { + rx_dr: self.rx_dr.unwrap_or_default(), + tx_ds: self.tx_ds.unwrap_or_default(), + tx_df: self.tx_df.unwrap_or_default(), + } + } + + pub fn from_inner(other: StatusFlags) -> Self { Self { - rx_dr: true, - tx_ds: true, - tx_df: true, + rx_dr: Some(other.rx_dr), + tx_ds: Some(other.tx_ds), + tx_df: Some(other.tx_df), } } } @@ -105,19 +115,19 @@ pub struct AvailablePipe { #[derive(Debug, PartialEq)] pub enum NodePaLevel { /// | nRF24L01 | Si24R1 with
LNA Enabled | Si24R1 with
LNA Disabled | - /// | :-------:|:--------------------------:|:---------------------------:| + /// |:--------:|:--------------------------:|:---------------------------:| /// | -18 dBm | -6 dBm | -12 dBm | MIN, /// | nRF24L01 | Si24R1 with
LNA Enabled | Si24R1 with
LNA Disabled | - /// | :-------:|:--------------------------:|:---------------------------:| + /// |:--------:|:--------------------------:|:---------------------------:| /// | -12 dBm | 0 dBm | -4 dBm | LOW, /// | nRF24L01 | Si24R1 with
LNA Enabled | Si24R1 with
LNA Disabled | - /// | :-------:|:--------------------------:|:---------------------------:| + /// |:--------:|:--------------------------:|:---------------------------:| /// | -6 dBm | 3 dBm | 1 dBm | HIGH, /// | nRF24L01 | Si24R1 with
LNA Enabled | Si24R1 with
LNA Disabled | - /// | :-------:|:--------------------------:|:---------------------------:| + /// |:--------:|:--------------------------:|:---------------------------:| /// | 0 dBm | 7 dBm | 4 dBm | MAX, } diff --git a/rf24-node/src/radio.rs b/rf24-node/src/radio.rs index 14d9da4..90e624f 100644 --- a/rf24-node/src/radio.rs +++ b/rf24-node/src/radio.rs @@ -2,7 +2,7 @@ use crate::enums::{ AvailablePipe, HardwareConfig, NodeCrcLength, NodeDataRate, NodeFifoState, NodePaLevel, - StatusFlags, WriteConfig, + NodeStatusFlags, WriteConfig, }; use linux_embedded_hal::{ gpio_cdev::{chips, LineRequestFlags}, @@ -12,6 +12,7 @@ use linux_embedded_hal::{ use napi::{bindgen_prelude::Buffer, Error, Result, Status}; use rf24_rs::radio::{prelude::*, RF24}; +use rf24_rs::StatusFlags; #[napi(js_name = "RF24")] pub struct NodeRF24 { @@ -105,6 +106,11 @@ impl NodeRF24 { .map_err(|e| Error::new(Status::GenericFailure, format!("{e:?}"))) } + #[napi(getter)] + pub fn is_listening(&self) -> bool { + self.inner.is_listening() + } + #[napi] pub fn start_listening(&mut self) -> Result<()> { self.inner @@ -141,8 +147,9 @@ impl NodeRF24 { } #[napi] - pub fn read(&mut self, len: u8) -> Result { - self.inner + pub fn read(&mut self, len: Option) -> Result { + let len = self + .inner .read(&mut self.read_buf, len) .map_err(|e| Error::new(Status::GenericFailure, format!("{e:?}")))?; Ok(Buffer::from(&self.read_buf[0..len as usize])) @@ -169,13 +176,13 @@ impl NodeRF24 { .map_err(|e| Error::new(Status::GenericFailure, format!("{e:?}"))) } - #[napi] + #[napi(getter)] pub fn is_plus_variant(&self) -> bool { self.inner.is_plus_variant() } - #[napi] - pub fn test_rpd(&mut self) -> Result { + #[napi(getter)] + pub fn rpd(&mut self) -> Result { self.inner .test_rpd() .map_err(|e| Error::new(Status::GenericFailure, format!("{e:?}"))) @@ -414,6 +421,11 @@ impl NodeRF24 { .map_err(|e| Error::new(Status::GenericFailure, format!("{e:?}"))) } + #[napi(getter)] + pub fn is_powered(&self) -> bool { + self.inner.is_powered() + } + #[napi] pub fn power_down(&mut self) -> Result<()> { self.inner @@ -429,18 +441,26 @@ impl NodeRF24 { } #[napi] - pub fn set_status_flags(&mut self, status_flags: Option) -> Result<()> { - let status_flags = status_flags.unwrap_or_default(); + pub fn set_status_flags(&mut self, flags: Option) -> Result<()> { + let flags = flags.unwrap_or(NodeStatusFlags { + rx_dr: Some(true), + tx_ds: Some(true), + tx_df: Some(true), + }); self.inner - .set_status_flags(status_flags.rx_dr, status_flags.tx_ds, status_flags.tx_df) + .set_status_flags(Some(flags.into_inner())) .map_err(|e| Error::new(Status::GenericFailure, format!("{e:?}"))) } #[napi] - pub fn clear_status_flags(&mut self, status_flags: Option) -> Result<()> { - let status_flags = status_flags.unwrap_or_default(); + pub fn clear_status_flags(&mut self, flags: Option) -> Result<()> { + let flags = flags.unwrap_or(NodeStatusFlags { + rx_dr: Some(true), + tx_ds: Some(true), + tx_df: Some(true), + }); self.inner - .clear_status_flags(status_flags.rx_dr, status_flags.tx_ds, status_flags.tx_df) + .clear_status_flags(Some(flags.into_inner())) .map_err(|e| Error::new(Status::GenericFailure, format!("{e:?}"))) } @@ -452,17 +472,9 @@ impl NodeRF24 { } #[napi] - pub fn get_status_flags(&mut self) -> Result { - let mut rx_dr = Some(false); - let mut tx_ds = Some(false); - let mut tx_df = Some(false); - self.inner - .get_status_flags(&mut rx_dr, &mut tx_ds, &mut tx_df) - .map_err(|e| Error::new(Status::GenericFailure, format!("{e:?}")))?; - Ok(StatusFlags { - rx_dr: rx_dr.unwrap(), - tx_ds: tx_ds.unwrap(), - tx_df: tx_df.unwrap(), - }) + pub fn get_status_flags(&mut self) -> NodeStatusFlags { + let mut flags = StatusFlags::default(); + self.inner.get_status_flags(&mut flags); + NodeStatusFlags::from_inner(flags) } } diff --git a/rf24-py/src/enums.rs b/rf24-py/src/enums.rs index f965ccf..cc3a7b9 100644 --- a/rf24-py/src/enums.rs +++ b/rf24-py/src/enums.rs @@ -1,8 +1,50 @@ use pyo3::prelude::*; #[cfg(target_os = "linux")] -use rf24_rs::{CrcLength, PaLevel, DataRate, FifoState}; +use rf24_rs::{CrcLength, DataRate, FifoState, PaLevel, StatusFlags}; +#[pyclass(name = "StatusFlags", frozen, get_all, module = "rf24_py")] +#[derive(Default, Clone)] +pub struct PyStatusFlags { + /// A flag to describe if RX Data Ready to read. + pub rx_dr: bool, + /// A flag to describe if TX Data Sent. + pub tx_ds: bool, + /// A flag to describe if TX Data Failed. + pub tx_df: bool, +} + +#[pymethods] +impl PyStatusFlags { + #[new] + #[pyo3(signature = (rx_dr = false, tx_ds = false, tx_df = false))] + fn new(rx_dr: bool, tx_ds: bool, tx_df: bool) -> Self { + Self { + rx_dr, + tx_ds, + tx_df, + } + } +} + +#[cfg(target_os = "linux")] +impl PyStatusFlags { + pub fn into_inner(self) -> StatusFlags { + StatusFlags { + rx_dr: self.rx_dr, + tx_ds: self.tx_ds, + tx_df: self.tx_df, + } + } + + pub fn from_inner(other: StatusFlags) -> Self { + Self { + rx_dr: other.rx_dr, + tx_ds: other.tx_ds, + tx_df: other.tx_df, + } + } +} /// Power Amplifier level. The units dBm (decibel-milliwatts or dBmW) /// represents a logarithmic signal loss. diff --git a/rf24-py/src/lib.rs b/rf24-py/src/lib.rs index 5d50603..ceb8c69 100644 --- a/rf24-py/src/lib.rs +++ b/rf24-py/src/lib.rs @@ -1,7 +1,7 @@ use pyo3::prelude::*; // #[cfg(target_os = "linux")] -mod radio; mod enums; +mod radio; #[cfg(target_os = "linux")] fn bind_radio_impl(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -21,5 +21,6 @@ fn rf24_py(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/rf24-py/src/radio.rs b/rf24-py/src/radio.rs index 266f294..70bebab 100644 --- a/rf24-py/src/radio.rs +++ b/rf24-py/src/radio.rs @@ -1,7 +1,7 @@ #![cfg(target_os = "linux")] use std::borrow::Cow; -use crate::enums::{PyCrcLength, PyDataRate, PyFifoState, PyPaLevel}; +use crate::enums::{PyCrcLength, PyDataRate, PyFifoState, PyPaLevel, PyStatusFlags}; use linux_embedded_hal::{ gpio_cdev::{chips, LineRequestFlags}, spidev::{SpiModeFlags, SpidevOptions}, @@ -12,6 +12,7 @@ use pyo3::{ prelude::*, }; use rf24_rs::radio::{prelude::*, RF24}; +use rf24_rs::StatusFlags; #[pyclass(name = "RF24", module = "rf24_py")] pub struct PyRF24 { @@ -92,6 +93,20 @@ impl PyRF24 { .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[setter] + pub fn set_listen(&mut self, enable: bool) -> PyResult<()> { + if enable { + self.start_listening() + } else { + self.stop_listening() + } + } + + #[getter] + pub fn get_listen(&self) -> bool { + self.inner.is_listening() + } + pub fn start_listening(&mut self) -> PyResult<()> { self.inner .start_listening() @@ -124,8 +139,10 @@ impl PyRF24 { .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } - pub fn read(&mut self, len: u8) -> PyResult> { - self.inner + #[pyo3(signature = (len = None))] + pub fn read(&mut self, len: Option) -> PyResult> { + let len = self + .inner .read(&mut self.read_buf, len) .map_err(|e| PyRuntimeError::new_err(format!("{e:?}")))?; Ok(Cow::from(&self.read_buf[0..len as usize])) @@ -149,11 +166,13 @@ impl PyRF24 { .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[getter] pub fn is_plus_variant(&self) -> bool { self.inner.is_plus_variant() } - pub fn test_rpd(&mut self) -> PyResult { + #[getter] + pub fn get_rpd(&mut self) -> PyResult { self.inner .test_rpd() .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) @@ -213,18 +232,21 @@ impl PyRF24 { .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[setter] pub fn set_channel(&mut self, channel: u8) -> PyResult<()> { self.inner .set_channel(channel) .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[getter] pub fn get_channel(&mut self) -> PyResult { self.inner .get_channel() .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[getter] pub fn get_crc_length(&mut self) -> PyResult { self.inner .get_crc_length() @@ -232,12 +254,14 @@ impl PyRF24 { .map(|e| PyCrcLength::from_inner(e)) } + #[setter] pub fn set_crc_length(&mut self, crc_length: PyCrcLength) -> PyResult<()> { self.inner .set_crc_length(crc_length.into_inner()) .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[getter] pub fn get_data_rate(&mut self) -> PyResult { self.inner .get_data_rate() @@ -245,6 +269,7 @@ impl PyRF24 { .map(|e| PyDataRate::from_inner(e)) } + #[setter] pub fn set_data_rate(&mut self, data_rate: PyDataRate) -> PyResult<()> { self.inner .set_data_rate(data_rate.into_inner()) @@ -287,6 +312,7 @@ impl PyRF24 { .map(|e| PyFifoState::from_inner(e)) } + #[getter] pub fn get_pa_level(&mut self) -> PyResult { self.inner .get_pa_level() @@ -294,18 +320,21 @@ impl PyRF24 { .map(|e| PyPaLevel::from_inner(e)) } + #[setter] pub fn set_pa_level(&mut self, pa_level: PyPaLevel) -> PyResult<()> { self.inner .set_pa_level(pa_level.into_inner()) .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[setter] pub fn set_payload_length(&mut self, length: u8) -> PyResult<()> { self.inner .set_payload_length(length) .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[getter] pub fn get_payload_length(&mut self) -> PyResult { self.inner .get_payload_length() @@ -343,18 +372,36 @@ impl PyRF24 { .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[setter] pub fn set_address_length(&mut self, length: u8) -> PyResult<()> { self.inner .set_address_length(length) .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + #[getter] pub fn get_address_length(&mut self) -> PyResult { self.inner .get_address_length() .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } + + #[setter] + pub fn set_power(&mut self, enable: bool) -> PyResult<()> { + if enable { + self.power_up(None) + } + else { + self.power_down() + } + } + + #[getter] + pub fn get_power(&self) -> bool { + self.inner.is_powered() + } + pub fn power_down(&mut self) -> PyResult<()> { self.inner .power_down() @@ -371,15 +418,27 @@ impl PyRF24 { .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } - pub fn set_status_flags(&mut self, rx_dr: bool, tx_ds: bool, tx_df: bool) -> PyResult<()> { + #[pyo3(signature = (flags = None))] + pub fn set_status_flags(&mut self, flags: Option) -> PyResult<()> { + let flags = flags.unwrap_or(PyStatusFlags { + rx_dr: true, + tx_ds: true, + tx_df: true, + }); self.inner - .set_status_flags(rx_dr, tx_ds, tx_df) + .set_status_flags(Some(flags.into_inner())) .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } - pub fn clear_status_flags(&mut self, rx_dr: bool, tx_ds: bool, tx_df: bool) -> PyResult<()> { + #[pyo3(signature = (flags = None))] + pub fn clear_status_flags(&mut self, flags: Option) -> PyResult<()> { + let flags = flags.unwrap_or(PyStatusFlags { + rx_dr: true, + tx_ds: true, + tx_df: true, + }); self.inner - .clear_status_flags(rx_dr, tx_ds, tx_df) + .clear_status_flags(Some(flags.into_inner())) .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } @@ -389,13 +448,9 @@ impl PyRF24 { .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) } - pub fn get_status_flags(&mut self) -> PyResult<(bool, bool, bool)> { - let mut rx_dr = Some(false); - let mut tx_ds = Some(false); - let mut tx_df = Some(false); - self.inner - .get_status_flags(&mut rx_dr, &mut tx_ds, &mut tx_df) - .map_err(|e| PyRuntimeError::new_err(format!("{e:?}")))?; - Ok((rx_dr.unwrap(), tx_ds.unwrap(), tx_df.unwrap())) + pub fn get_status_flags(&mut self) -> PyStatusFlags { + let mut flags = StatusFlags::default(); + self.inner.get_status_flags(&mut flags); + PyStatusFlags::from_inner(flags) } } diff --git a/rf24_py.pyi b/rf24_py.pyi index 654170b..243d33e 100644 --- a/rf24_py.pyi +++ b/rf24_py.pyi @@ -1,3 +1,4 @@ +from dataclasses import dataclass from enum import Enum, auto class PaLevel(Enum): @@ -21,23 +22,39 @@ class DataRate(Enum): Mbps2 = auto() Kbps250 = auto() +@dataclass(frozen=True) +class StatusFlags: + rx_dr: bool + tx_ds: bool + tx_df: bool + + def __init__( + self, rx_dr: bool = False, tx_ds: bool = False, tx_df: bool = False + ): ... + class RF24: def __init__( self, ce_pin: int, cs_pin: int, dev_gpio_chip: int = 0, dev_spi_bus: int = 0 ) -> None: ... - def begin(self) -> bool: ... + def begin(self) -> None: ... + @property + def listen(self) -> bool: ... + @listen.setter + def listen(self, enable: bool) -> None: ... def start_listening(self) -> None: ... def stop_listening(self) -> None: ... - def send(self, buf: bytes | bytearray, ask_no_ack: bool) -> bool: ... + def send(self, buf: bytes | bytearray, ask_no_ack: bool = False) -> bool: ... def write( - self, buf: bytes | bytearray, ask_no_ack: bool, start_tx: bool + self, buf: bytes | bytearray, ask_no_ack: bool = False, start_tx: bool = True ) -> bool: ... - def read(self, len: int) -> bytes: ... + def read(self, len: int | None = None) -> bytes: ... def resend(self) -> bool: ... def rewrite(self) -> None: ... def get_last_arc(self) -> int: ... + @property def is_plus_variant(self) -> bool: ... - def test_rpd(self) -> bool: ... + @property + def rpd(self) -> bool: ... def start_carrier_wave(self, level: PaLevel, channel: int) -> None: ... def stop_carrier_wave(self) -> None: ... def set_lna(self, enable: bool) -> None: ... @@ -47,31 +64,47 @@ class RF24: def allow_ask_no_ack(self, enable: bool) -> None: ... def write_ack_payload(self, pipe: int, buf: bytes | bytearray) -> bool: ... def set_auto_retries(self, count: int, delay: int) -> None: ... - def set_channel(self, channel: int) -> None: ... - def get_channel(self) -> int: ... - def get_crc_length(self) -> CrcLength: ... - def set_crc_length(self, crc_length: CrcLength) -> None: ... - def get_data_rate(self) -> DataRate: ... - def set_data_rate(self, data_rate: DataRate) -> None: ... + @property + def channel(self) -> int: ... + @channel.setter + def channel(self, channel: int) -> None: ... + @property + def crc_length(self) -> CrcLength: ... + @crc_length.setter + def crc_length(self, crc_length: CrcLength) -> None: ... + @property + def data_rate(self) -> DataRate: ... + @data_rate.setter + def data_rate(self, data_rate: DataRate) -> None: ... def available(self) -> bool: ... def available_pipe(self) -> tuple[bool, int]: ... def flush_rx(self) -> None: ... def flush_tx(self) -> None: ... def get_fifo_state(self, about_tx: bool) -> FifoState: ... - def get_pa_level(self) -> PaLevel: ... - def set_pa_level(self, pa_level: PaLevel) -> None: ... - def set_payload_length(self, length: int) -> None: ... - def get_payload_length(self) -> int: ... + @property + def pa_level(self) -> PaLevel: ... + @pa_level.setter + def pa_level(self, pa_level: PaLevel) -> None: ... + @property + def payload_length(self) -> int: ... + @payload_length.setter + def payload_length(self, length: int) -> None: ... def set_dynamic_payloads(self, enable: bool) -> None: ... def get_dynamic_payload_length(self) -> int: ... def open_rx_pipe(self, pipe: int, address: bytes | bytearray) -> None: ... def open_tx_pipe(self, address: bytes | bytearray) -> None: ... def close_rx_pipe(self, pipe: int) -> None: ... - def set_address_length(self, length: int) -> None: ... - def get_address_length(self) -> int: ... + @property + def address_length(self) -> int: ... + @address_length.setter + def address_length(self, length: int) -> None: ... + @property + def power(self) -> bool: ... + @power.setter + def power(self, enable: bool) -> None: ... def power_down(self, delay: int | None = None) -> None: ... def power_up(self) -> None: ... - def set_status_flags(self, rx_dr: bool, tx_ds: bool, tx_df: bool) -> None: ... - def clear_status_flags(self, rx_dr: bool, tx_ds: bool, tx_df: bool) -> None: ... + def set_status_flags(self, flags: StatusFlags | None = None) -> None: ... + def clear_status_flags(self, flags: StatusFlags | None = None) -> None: ... def update(self) -> None: ... - def get_status_flags(self) -> tuple[bool, bool, bool]: ... + def get_status_flags(self) -> StatusFlags: ...