Skip to content

Commit

Permalink
add python examples and refactor API a bit
Browse files Browse the repository at this point in the history
  • Loading branch information
2bndy5 committed Oct 14, 2024
1 parent 96d51e0 commit a8abb14
Show file tree
Hide file tree
Showing 37 changed files with 2,036 additions and 197 deletions.
22 changes: 20 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
- .github/workflows/tests.yml

jobs:
lint:
lint-rust:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"

Expand Down
3 changes: 3 additions & 0 deletions cspell.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ words:
- armv
- bindgen
- bytearray
- calcsize
- Cdev
- datasheet
- Doherty
- DYNPD
- eabi
- fontawesome
- gnueabihf
- gpio
- gpiochip
- gpiod
- inlinehilite
- Kbps
- linenums
Expand Down
26 changes: 24 additions & 2 deletions docs/src/api-diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,47 @@ 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 |
|:----:|:-----------------------------|
| 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).
Expand Down
176 changes: 176 additions & 0 deletions examples/python/acknowledgement_payloads.py
Original file line number Diff line number Diff line change
@@ -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<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)

# 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")
Loading

0 comments on commit a8abb14

Please sign in to comment.