From f974aafbe318d820629dc8c96870ede80b5922d2 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Tue, 8 Oct 2024 06:49:32 -0700 Subject: [PATCH] add python binding --- .github/workflows/docs.yml | 2 + .github/workflows/python-packaging.yml | 192 +++++++++++++ .github/workflows/tests.yml | 2 + .gitignore | 179 +++++++++++- Cargo.toml | 2 +- cspell.config.yml | 4 + lib/src/radio/mod.rs | 2 +- lib/src/radio/rf24/crc_length.rs | 4 +- lib/src/radio/rf24/mod.rs | 2 +- pyproject.toml | 59 ++++ rf24-py/.gitignore | 72 +++++ rf24-py/Cargo.toml | 19 ++ rf24-py/README.md | 5 + rf24-py/src/enums.rs | 140 ++++++++++ rf24-py/src/lib.rs | 25 ++ rf24-py/src/radio.rs | 373 +++++++++++++++++++++++++ rf24_py.pyi | 77 +++++ 17 files changed, 1150 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/python-packaging.yml create mode 100644 pyproject.toml create mode 100644 rf24-py/.gitignore create mode 100644 rf24-py/Cargo.toml create mode 100644 rf24-py/README.md create mode 100644 rf24-py/src/enums.rs create mode 100644 rf24-py/src/lib.rs create mode 100644 rf24-py/src/radio.rs create mode 100644 rf24_py.pyi diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index af671ce..97f2724 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,6 +8,7 @@ on: - 'lib/**' - Cargo.toml - '*.md' + - .github/workflows/docs.yml pull_request: branches: [main] paths: @@ -15,6 +16,7 @@ on: - 'lib/**' - Cargo.toml - '*.md' + - .github/workflows/docs.yml jobs: supplemental: diff --git a/.github/workflows/python-packaging.yml b/.github/workflows/python-packaging.yml new file mode 100644 index 0000000..9f54771 --- /dev/null +++ b/.github/workflows/python-packaging.yml @@ -0,0 +1,192 @@ +# This file is autogenerated by maturin v1.7.4 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + branches: [main] + paths: + - rf24-py/** + - lib/src/** + - Cargo.toml + - pyproject.toml + - .github/workflows/python-packaging.yml + tags: + - '*' + pull_request: + branches: [main] + paths: + - rf24-py/** + - lib/src/** + - Cargo.toml + - pyproject.toml + - .github/workflows/python-packaging.yml + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-latest + target: x86_64 + - runner: ubuntu-latest + target: x86 + - runner: ubuntu-latest + target: aarch64 + - runner: ubuntu-latest + target: armv7 + - runner: ubuntu-latest + target: s390x + - runner: ubuntu-latest + target: ppc64le + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + musllinux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-latest + target: x86_64 + - runner: ubuntu-latest + target: x86 + - runner: ubuntu-latest + target: aarch64 + - runner: ubuntu-latest + target: armv7 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: musllinux_1_2 + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-musllinux-${{ matrix.platform.target }} + path: dist + + windows: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + architecture: ${{ matrix.platform.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: dist + + macos: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: macos-12 + target: x86_64 + - runner: macos-14 + target: aarch64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }} + needs: [linux, musllinux, windows, macos, sdist] + permissions: + # Use to sign the release artifacts + id-token: write + # Used to upload release artifacts + contents: write + # Used to generate artifact attestation + attestations: write + steps: + - uses: actions/download-artifact@v4 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: 'wheels-*/*' + - name: Publish to PyPI + if: startsWith(github.ref, 'refs/tags/') + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing wheels-*/* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 18ad6f1..1baa4dc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,12 +7,14 @@ on: - 'lib/**' - '!lib/README.md' - Cargo.toml + - .github/workflows/tests.yml pull_request: branches: [main] paths: - 'lib/**' - '!lib/README.md' - Cargo.toml + - .github/workflows/tests.yml jobs: lint: diff --git a/.gitignore b/.gitignore index 3854ba2..6b1af17 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,182 @@ - Created by https://www.toptal.com/developers/gitignore/api/rust -# Edit at https://www.toptal.com/developers/gitignore?templates=rust +# Created by https://www.toptal.com/developers/gitignore/api/rust,python +# Edit at https://www.toptal.com/developers/gitignore?templates=rust,python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json ### Rust ### # Generated by Cargo # will have compiled files and executables debug/ -target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html @@ -17,7 +188,7 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb -# End of https://www.toptal.com/developers/gitignore/api/rust +# End of https://www.toptal.com/developers/gitignore/api/rust,python # .vscode settings .vscode/ diff --git a/Cargo.toml b/Cargo.toml index 0534a5e..c898417 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["lib", "examples"] +members = ["lib", "examples", "rf24-py"] default-members = ["lib"] resolver = "2" diff --git a/cspell.config.yml b/cspell.config.yml index db9cb1c..a97d645 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -10,8 +10,12 @@ words: - Kbps - linenums - Mbps + - milliwatts - mkdocs + - pyclass - pymdownx + - pymethods + - pymodule - RETR - rustc - RXADDR diff --git a/lib/src/radio/mod.rs b/lib/src/radio/mod.rs index 1f831a1..4baaebe 100644 --- a/lib/src/radio/mod.rs +++ b/lib/src/radio/mod.rs @@ -362,7 +362,7 @@ 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, data_rate: CrcLength) -> Result<(), Self::CrcLengthErrorType>; + fn set_crc_length(&mut self, crc_length: CrcLength) -> Result<(), Self::CrcLengthErrorType>; } pub trait EsbDataRate { diff --git a/lib/src/radio/rf24/crc_length.rs b/lib/src/radio/rf24/crc_length.rs index 84724dc..0f60e34 100644 --- a/lib/src/radio/rf24/crc_length.rs +++ b/lib/src/radio/rf24/crc_length.rs @@ -24,9 +24,9 @@ where } } - fn set_crc_length(&mut self, data_rate: CrcLength) -> Result<(), Self::CrcLengthErrorType> { + fn set_crc_length(&mut self, crc_length: CrcLength) -> Result<(), Self::CrcLengthErrorType> { let crc_bin = { - match data_rate { + match crc_length { CrcLength::DISABLED => 0u8, CrcLength::BIT8 => 2u8, CrcLength::BIT16 => 3u8, diff --git a/lib/src/radio/rf24/mod.rs b/lib/src/radio/rf24/mod.rs index 2c0e58d..b1e3b37 100644 --- a/lib/src/radio/rf24/mod.rs +++ b/lib/src/radio/rf24/mod.rs @@ -125,7 +125,7 @@ where /// Is this radio a nRF24L01+ variant? /// /// The bool that this function returns is only valid _after_ calling [`RF24::init()`]. - pub fn is_plus_variant(&mut self) -> bool { + pub fn is_plus_variant(&self) -> bool { self._is_plus_variant } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..14759c4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["maturin>=1.7,<2.0"] +build-backend = "maturin" + +[project] +name = "rf24-py" +description = "A python package wrapping the nRF24 C++ libraries." +requires-python = ">=3.8" +readme = "README.md" +keywords = [ + "nrf24l01", + "nRF24L01+", + "raspberry", + "pi", + "driver", + "radio", + "transceiver", + "RF24", + "RF24Network", + "RF24Mesh", +] +license = {text = "MIT"} +authors = [ + { name = "Brendan Doherty", email = "2bndy5@gmail.com" }, +] +classifiers = [ + # https://pypi.org/pypi?%3Aaction=list_classifiers + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Hardware", + "Topic :: System :: Hardware :: Hardware Drivers", + "Topic :: System :: Networking", + "Typing :: Typed", +] +dynamic = ["version"] + +[project.urls] +Documentation = "http://nRF24.github.io/rf24-rs" +Source = "https://github.com/nRF24/rf24-rs" +Tracker = "https://github.com/nRF24/rf24-rs/issues" + +[tool.mypy] +show_error_codes = true +pretty = true + +[tool.maturin] +features = ["pyo3/extension-module"] +manifest-path = "rf24-py/Cargo.toml" \ No newline at end of file diff --git a/rf24-py/.gitignore b/rf24-py/.gitignore new file mode 100644 index 0000000..c8f0442 --- /dev/null +++ b/rf24-py/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/rf24-py/Cargo.toml b/rf24-py/Cargo.toml new file mode 100644 index 0000000..bca0419 --- /dev/null +++ b/rf24-py/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rf24-py" +version.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true +license-file.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "rf24_py" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = "0.22.3" + +[target.'cfg(target_os = "linux")'.dependencies] +linux-embedded-hal = "0.4.0" +rf24-rs = { path = "../lib" } diff --git a/rf24-py/README.md b/rf24-py/README.md new file mode 100644 index 0000000..ebf5df8 --- /dev/null +++ b/rf24-py/README.md @@ -0,0 +1,5 @@ +# rf24-py + +A python binding to the rust library [rf24-rs]. + +[rf24-rs]: https://github.com/nRF24/rf24-rs diff --git a/rf24-py/src/enums.rs b/rf24-py/src/enums.rs new file mode 100644 index 0000000..f965ccf --- /dev/null +++ b/rf24-py/src/enums.rs @@ -0,0 +1,140 @@ +use pyo3::prelude::*; + +#[cfg(target_os = "linux")] +use rf24_rs::{CrcLength, PaLevel, DataRate, FifoState}; + + +/// Power Amplifier level. The units dBm (decibel-milliwatts or dBmW) +/// represents a logarithmic signal loss. +#[pyclass(name = "PaLevel", eq, eq_int, module = "rf24_py")] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PyPaLevel { + /// | 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, +} + +#[cfg(target_os = "linux")] +impl PyPaLevel { + pub fn into_inner(self) -> PaLevel { + match self { + PyPaLevel::MIN => PaLevel::MIN, + PyPaLevel::LOW => PaLevel::LOW, + PyPaLevel::HIGH => PaLevel::HIGH, + PyPaLevel::MAX => PaLevel::MAX, + } + } + pub fn from_inner(other: PaLevel) -> PyPaLevel { + match other { + PaLevel::MIN => PyPaLevel::MIN, + PaLevel::LOW => PyPaLevel::LOW, + PaLevel::HIGH => PyPaLevel::HIGH, + PaLevel::MAX => PyPaLevel::MAX, + } + } +} + +/// How fast data moves through the air. Units are in bits per second (bps). +#[pyclass(name = "DataRate", eq, eq_int, module = "rf24_py")] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PyDataRate { + /// represents 1 Mbps + Mbps1, + /// represents 2 Mbps + Mbps2, + /// represents 250 Kbps + Kbps250, +} + +#[cfg(target_os = "linux")] +impl PyDataRate { + pub fn into_inner(self) -> DataRate { + match self { + PyDataRate::Mbps1 => DataRate::Mbps1, + PyDataRate::Mbps2 => DataRate::Mbps2, + PyDataRate::Kbps250 => DataRate::Kbps250, + } + } + pub fn from_inner(other: DataRate) -> PyDataRate { + match other { + DataRate::Mbps1 => PyDataRate::Mbps1, + DataRate::Mbps2 => PyDataRate::Mbps2, + DataRate::Kbps250 => PyDataRate::Kbps250, + } + } +} + +/// The length of a CRC checksum that is used (if any). +/// +/// Cyclical Redundancy Checking (CRC) is commonly used to ensure data integrity. +#[pyclass(name = "CrcLength", eq, eq_int, module = "rf24_py")] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PyCrcLength { + /// represents no CRC checksum is used + DISABLED, + /// represents CRC 8 bit checksum is used + BIT8, + /// represents CRC 16 bit checksum is used + BIT16, +} + +#[cfg(target_os = "linux")] +impl PyCrcLength { + pub fn into_inner(self) -> CrcLength { + match self { + PyCrcLength::DISABLED => CrcLength::DISABLED, + PyCrcLength::BIT8 => CrcLength::BIT8, + PyCrcLength::BIT16 => CrcLength::BIT16, + } + } + pub fn from_inner(other: CrcLength) -> PyCrcLength { + match other { + CrcLength::DISABLED => PyCrcLength::DISABLED, + CrcLength::BIT8 => PyCrcLength::BIT8, + CrcLength::BIT16 => PyCrcLength::BIT16, + } + } +} + +/// The possible states of a FIFO. +#[pyclass(name = "FifoState", eq, eq_int, module = "rf24_py")] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PyFifoState { + /// Represent the state of a FIFO when it is full. + Full, + /// Represent the state of a FIFO when it is empty. + Empty, + /// Represent the state of a FIFO when it is not full but not empty either. + Occupied, +} + +#[cfg(target_os = "linux")] +impl PyFifoState { + pub fn into_inner(self) -> FifoState { + match self { + PyFifoState::Full => FifoState::Full, + PyFifoState::Empty => FifoState::Empty, + PyFifoState::Occupied => FifoState::Occupied, + } + } + pub fn from_inner(other: FifoState) -> PyFifoState { + match other { + FifoState::Full => PyFifoState::Full, + FifoState::Empty => PyFifoState::Empty, + FifoState::Occupied => PyFifoState::Occupied, + } + } +} diff --git a/rf24-py/src/lib.rs b/rf24-py/src/lib.rs new file mode 100644 index 0000000..12c5ddb --- /dev/null +++ b/rf24-py/src/lib.rs @@ -0,0 +1,25 @@ +use pyo3::prelude::*; +#[cfg(target_os = "linux")] +mod radio; +mod enums; + +#[cfg(target_os = "linux")] +fn bind_radio_impl(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::() +} + +#[cfg(not(target_os = "linux"))] +fn bind_radio_impl(_m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) +} + +/// A Python module implemented in Rust. +#[pymodule] +fn rf24_py(m: &Bound<'_, PyModule>) -> PyResult<()> { + bind_radio_impl(m)?; + 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 new file mode 100644 index 0000000..c427df3 --- /dev/null +++ b/rf24-py/src/radio.rs @@ -0,0 +1,373 @@ +#![cfg(target_os = "linux")] +use crate::enums::{PyCrcLength, PyDataRate, PyFifoState, PyPaLevel}; +use linux_embedded_hal::{ + gpio_cdev::{chips, LineRequestFlags}, + spidev::{SpiModeFlags, SpidevOptions}, + CdevPin, Delay, SpidevDevice, +}; +use pyo3::{ + exceptions::{PyOSError, PyRuntimeError, PyValueError}, + prelude::*, +}; +use rf24_rs::radio::{prelude::*, RF24}; + +#[pyclass(name = "RF24", module = "rf24_py")] +pub struct PyRF24 { + inner: RF24, +} + +#[pymethods] +impl PyRF24 { + #[new] + #[pyo3( + text_signature = "(ce_pin: int, cs_pin: int, dev_gpio_chip: int = 0, dev_spi_bus: int = 0) -> RF24", + signature = (ce_pin, cs_pin, dev_gpio_chip = 0u8, dev_spi_bus = 0u8), + )] + pub fn new(ce_pin: u32, cs_pin: u8, dev_gpio_chip: u8, dev_spi_bus: u8) -> PyResult { + // get the desired "dev/gpiochip{dev_gpio_chip}" + let mut dev_gpio = chips() + .map_err(|_| PyOSError::new_err("Failed to get list of GPIO chips for the system"))? + .find(|chip| { + if let Ok(chip) = chip { + if chip.path().ends_with(dev_gpio_chip.to_string()) { + return true; + } + } + false + }) + .ok_or(PyOSError::new_err(format!( + "Could not find specified dev/gpiochip{dev_gpio_chip} for this system." + )))? + .map_err(|e| { + PyOSError::new_err(format!( + "Could not open GPIO chip dev/gpiochip{dev_gpio_chip}: {e:?}" + )) + })?; + let ce_line = dev_gpio + .get_line(ce_pin) + .map_err(|e| PyValueError::new_err(format!("GPIO{ce_pin} is unavailable: {e:?}")))?; + let ce_line_handle = ce_line + .request(LineRequestFlags::OUTPUT, 0, "rf24-rs") + .map_err(|e| PyOSError::new_err(format!("GPIO{ce_pin} is already in use: {e:?}")))?; + let ce_pin = + CdevPin::new(ce_line_handle).map_err(|e| PyOSError::new_err(format!("{e:?}")))?; + + let mut spi = + SpidevDevice::open(format!("/dev/spidev{dev_spi_bus}.{cs_pin}")).map_err(|_| { + PyOSError::new_err(format!( + "SPI bus {dev_spi_bus} with CS pin option {cs_pin} is not available in this system" + ) + ) + })?; + let config = SpidevOptions::new() + .max_speed_hz(10000000) + .mode(SpiModeFlags::SPI_MODE_0) + .bits_per_word(8) + .build(); + spi.configure(&config) + .map_err(|e| PyOSError::new_err(format!("{e:?}")))?; + + Ok(Self { + inner: RF24::new(ce_pin, spi, Delay), + }) + } + + pub fn begin(&mut self) -> PyResult<()> { + self.inner + .init() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn start_listening(&mut self) -> PyResult<()> { + self.inner + .start_listening() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn stop_listening(&mut self) -> PyResult<()> { + self.inner + .stop_listening() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn send(&mut self, buf: &[u8], ask_no_ack: bool) -> PyResult { + self.inner + .send(buf, ask_no_ack) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn write(&mut self, buf: &[u8], ask_no_ack: bool, start_tx: bool) -> PyResult { + self.inner + .write(buf, ask_no_ack, start_tx) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn read(&mut self, len: u8) -> PyResult> { + let mut buf = Vec::with_capacity(len as usize); + self.inner + .read(&mut buf, len) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}")))?; + Ok(buf) + } + + pub fn resend(&mut self) -> PyResult { + self.inner + .resend() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn rewrite(&mut self) -> PyResult<()> { + self.inner + .rewrite() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn get_last_arc(&mut self) -> PyResult { + self.inner + .get_last_arc() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn is_plus_variant(&self) -> bool { + self.inner.is_plus_variant() + } + + pub fn test_rpd(&mut self) -> PyResult { + self.inner + .test_rpd() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn start_carrier_wave(&mut self, level: PyPaLevel, channel: u8) -> PyResult<()> { + self.inner + .start_carrier_wave(level.into_inner(), channel) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn stop_carrier_wave(&mut self) -> PyResult<()> { + self.inner + .stop_carrier_wave() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn set_lna(&mut self, enable: bool) -> PyResult<()> { + self.inner + .set_lna(enable) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn allow_ack_payloads(&mut self, enable: bool) -> PyResult<()> { + self.inner + .allow_ack_payloads(enable) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + pub fn set_auto_ack(&mut self, enable: bool) -> PyResult<()> { + self.inner + .set_auto_ack(enable) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + pub fn set_auto_ack_pipe(&mut self, enable: bool, pipe: u8) -> PyResult<()> { + self.inner + .set_auto_ack_pipe(enable, pipe) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + pub fn allow_ask_no_ack(&mut self, enable: bool) -> PyResult<()> { + self.inner + .allow_ask_no_ack(enable) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + pub fn write_ack_payload(&mut self, pipe: u8, buf: &[u8]) -> PyResult { + self.inner + .write_ack_payload(pipe, buf) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + pub fn set_auto_retries(&mut self, delay: u8, count: u8) -> PyResult<()> { + self.inner + .set_auto_retries(delay, count) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn set_channel(&mut self, channel: u8) -> PyResult<()> { + self.inner + .set_channel(channel) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn get_channel(&mut self) -> PyResult { + self.inner + .get_channel() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn get_crc_length(&mut self) -> PyResult { + self.inner + .get_crc_length() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + .map(|e| PyCrcLength::from_inner(e)) + } + + 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:?}"))) + } + pub fn get_data_rate(&mut self) -> PyResult { + self.inner + .get_data_rate() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + .map(|e| PyDataRate::from_inner(e)) + } + pub fn set_data_rate(&mut self, data_rate: PyDataRate) -> PyResult<()> { + self.inner + .set_data_rate(data_rate.into_inner()) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn available(&mut self) -> PyResult { + self.inner + .available() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn available_pipe(&mut self) -> PyResult<(bool, u8)> { + let mut pipe = Some(0u8); + let result = self + .inner + .available_pipe(&mut pipe) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}")))?; + Ok((result, pipe.expect("`pipe` should be a number"))) + } + + /// Use this to discard all 3 layers in the radio's RX FIFO. + pub fn flush_rx(&mut self) -> PyResult<()> { + self.inner + .flush_rx() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + /// Use this to discard all 3 layers in the radio's TX FIFO. + pub fn flush_tx(&mut self) -> PyResult<()> { + self.inner + .flush_tx() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn get_fifo_state(&mut self, about_tx: bool) -> PyResult { + self.inner + .get_fifo_state(about_tx) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + .map(|e| PyFifoState::from_inner(e)) + } + + pub fn get_pa_level(&mut self) -> PyResult { + self.inner + .get_pa_level() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + .map(|e| PyPaLevel::from_inner(e)) + } + + 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:?}"))) + } + + pub fn set_payload_length(&mut self, length: u8) -> PyResult<()> { + self.inner + .set_payload_length(length) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn get_payload_length(&mut self) -> PyResult { + self.inner + .get_payload_length() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn set_dynamic_payloads(&mut self, enable: bool) -> PyResult<()> { + self.inner + .set_dynamic_payloads(enable) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn get_dynamic_payload_length(&mut self) -> PyResult { + self.inner + .get_dynamic_payload_length() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn open_rx_pipe(&mut self, pipe: u8, address: &[u8]) -> PyResult<()> { + self.inner + .open_rx_pipe(pipe, address) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn open_tx_pipe(&mut self, address: &[u8]) -> PyResult<()> { + self.inner + .open_tx_pipe(address) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + /// If the given `pipe` number is not in range [0, 5], then this function does nothing. + pub fn close_rx_pipe(&mut self, pipe: u8) -> PyResult<()> { + self.inner + .close_rx_pipe(pipe) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn set_address_length(&mut self, length: u8) -> PyResult<()> { + self.inner + .set_address_length(length) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn get_address_length(&mut self) -> PyResult { + self.inner + .get_address_length() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn power_down(&mut self) -> PyResult<()> { + self.inner + .power_down() + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + #[pyo3( + text_signature = "(delay: int | None = None) -> None", + signature = (delay = None), + )] + pub fn power_up(&mut self, delay: Option) -> PyResult<()> { + self.inner + .power_up(delay) + .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<()> { + self.inner + .set_status_flags(rx_dr, tx_ds, tx_df) + .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<()> { + self.inner + .clear_status_flags(rx_dr, tx_ds, tx_df) + .map_err(|e| PyRuntimeError::new_err(format!("{e:?}"))) + } + + pub fn update(&mut self) -> PyResult<()> { + self.inner + .update() + .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())) + } +} diff --git a/rf24_py.pyi b/rf24_py.pyi new file mode 100644 index 0000000..654170b --- /dev/null +++ b/rf24_py.pyi @@ -0,0 +1,77 @@ +from enum import Enum, auto + +class PaLevel(Enum): + MIN = auto() + LOW = auto() + HIGH = auto() + MAX = auto() + +class CrcLength(Enum): + DISABLED = auto() + BIT8 = auto() + BIT16 = auto() + +class FifoState(Enum): + Full = auto() + Empty = auto() + Occupied = auto() + +class DataRate(Enum): + Mbps1 = auto() + Mbps2 = auto() + Kbps250 = auto() + +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 start_listening(self) -> None: ... + def stop_listening(self) -> None: ... + def send(self, buf: bytes | bytearray, ask_no_ack: bool) -> bool: ... + def write( + self, buf: bytes | bytearray, ask_no_ack: bool, start_tx: bool + ) -> bool: ... + def read(self, len: int) -> bytes: ... + def resend(self) -> bool: ... + def rewrite(self) -> None: ... + def get_last_arc(self) -> int: ... + def is_plus_variant(self) -> bool: ... + def test_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: ... + def allow_ack_payloads(self, enable: bool) -> None: ... + def set_auto_ack(self, enable: bool) -> None: ... + def set_auto_ack_pipe(self, enable: bool, pipe: int) -> None: ... + 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: ... + 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: ... + 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: ... + 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 update(self) -> None: ... + def get_status_flags(self) -> tuple[bool, bool, bool]: ...