Merge pull request #1 from jerryneedell/jerryn_initial_commit

Initial commit for comment and review
This commit is contained in:
Scott Shawcroft 2024-08-07 11:31:39 -07:00 committed by GitHub
commit 04ccc8e873
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 4616 additions and 2 deletions

View file

@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: 2021 Adafruit Industries
#
# SPDX-License-Identifier: MIT
Thank you for contributing! Before you submit a pull request, please read the following.
Make sure any changes you're submitting are in line with the CircuitPython Design Guide, available here: https://docs.circuitpython.org/en/latest/docs/design_guide.html
If your changes are to documentation, please verify that the documentation builds locally by following the steps found here: https://adafru.it/build-docs
Before submitting the pull request, make sure you've run Pylint and Black locally on your code. You can do this manually or using pre-commit. Instructions are available here: https://adafru.it/check-your-code
Please remove all of this text before submitting. Include an explanation or list of changes included in your PR, as well as, if applicable, a link to any related issues.

14
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
name: Build CI
on: [pull_request, push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Run Build CI workflow
uses: adafruit/workflows-circuitpython-libs/build@main

19
.github/workflows/failure-help-text.yml vendored Normal file
View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2021 Scott Shawcroft for Adafruit Industries
#
# SPDX-License-Identifier: MIT
name: Failure help text
on:
workflow_run:
workflows: ["Build CI"]
types:
- completed
jobs:
post-help:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' }}
steps:
- name: Post comment to help
uses: adafruit/circuitpython-action-library-ci-failed@v1

19
.github/workflows/release_gh.yml vendored Normal file
View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
name: GitHub Release Actions
on:
release:
types: [published]
jobs:
upload-release-assets:
runs-on: ubuntu-latest
steps:
- name: Run GitHub Release CI workflow
uses: adafruit/workflows-circuitpython-libs/release-gh@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
upload-url: ${{ github.event.release.upload_url }}

19
.github/workflows/release_pypi.yml vendored Normal file
View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
name: PyPI Release Actions
on:
release:
types: [published]
jobs:
upload-release-assets:
runs-on: ubuntu-latest
steps:
- name: Run PyPI Release CI workflow
uses: adafruit/workflows-circuitpython-libs/release-pypi@main
with:
pypi-username: ${{ secrets.pypi_username }}
pypi-password: ${{ secrets.pypi_password }}

21
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.4
hooks:
- id: ruff-format
- id: ruff
args: ["--fix"]
- repo: https://github.com/fsfe/reuse-tool
rev: v3.0.1
hooks:
- id: reuse

19
.readthedocs.yaml Normal file
View file

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
build:
os: ubuntu-latest
tools:
python: "3"
python:
install:
- requirements: docs/requirements.txt
- requirements: requirements.txt

158
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,158 @@
<!--
SPDX-FileCopyrightText: 2014 Coraline Ada Ehmke
SPDX-FileCopyrightText: 2019-2021 Kattni Rembor for Adafruit Industries
SPDX-License-Identifier: CC-BY-4.0
-->
# Adafruit Community Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and leaders pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level or type of
experience, education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.
## Our Standards
We are committed to providing a friendly, safe and welcoming environment for
all.
Examples of behavior that contributes to creating and maintaining a positive environment
include:
* Be kind and courteous to others
* Using welcoming and inclusive language
* Respecting the identity of every community member, including asking for their
pronouns if uncertain
* Being respectful of differing viewpoints and experiences
* Collaborating with other community members
* Providing desired assistance and knowledge to other community members
* Being open to new information and ideas
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by community members include:
* The use of sexualized language or imagery and sexual attention or advances
* The use of inappropriate images, including in a community member's avatar
* The use of inappropriate language or profanity, including in a community member's nickname
* Any spamming, flaming, baiting or other attention-stealing behavior
* Excessive or unwelcome helping; answering outside the scope of the question
asked
* Discussion or promotion of activities or projects that intend or pose a risk of
significant harm
* Trolling, insulting/derogatory comments, and attacks of any nature (including,
but not limited to, personal or political attacks)
* Promoting or spreading disinformation, lies, or conspiracy theories against
a person, group, organisation, project, or community
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Engaging in behavior that creates an unwelcoming or uninclusive environment
* Other conduct which could reasonably be considered inappropriate
The Adafruit Community welcomes everyone and strives to create a safe space for all. It is built
around sharing and contributing to technology. We encourage discussing your thoughts, experiences,
and feelings within the scope of the community. However, there are topics that can sometimes stray
from that scope, and can lead to hurting others and create an unwelcoming, uninclusive environment.
Examples of discussion topics that have been known to stray outside the scope of the Adafruit
Community include, but are not limited to:
* Discussions regarding religion and related topics
* Discussions regarding politics and related topics
The goal of the standards and moderation guidelines outlined here is to build
and maintain a respectful community. We ask that you dont just aim to be
"technically unimpeachable", but rather try to be your best self.
We value many things beyond technical expertise, including collaboration and
supporting others within our community. Providing a positive experience for
other community members can have a much more significant impact than simply
providing the correct answer.
## Our Responsibilities
Project leaders are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project leaders have the right and responsibility to remove, edit, or
reject messages, comments, commits, code, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any community member for other behaviors that they deem
inappropriate, threatening, offensive, or harmful.
## Moderation
Instances of behaviors that violate the Adafruit Community Code of Conduct
may be reported by any member of the community. Community members are
encouraged to report these situations, including situations they witness
involving other community members.
You may report in the following ways:
In any situation, you may email <support@adafruit.com>.
On the Adafruit Discord, you may send an open message from any channel
to all Community Moderators by tagging @community moderators. You may
also send an open message from the #help-with-community channel, or a
direct message to any Community Moderator.
The source of email and direct message reports will be kept confidential.
In situations on Discord where the issue is particularly offensive, possibly
illegal, requires immediate action, or violates the Discord terms of service,
you should also report the message directly to [Discord](https://discord.com/safety).
These are the steps for upholding our communitys standards of conduct.
1. Any member of the community may report any situation that violates the
Adafruit Community Code of Conduct. All reports will be reviewed and
investigated.
2. If the behavior is a severe violation, the community member who
committed the violation may be banned immediately, without warning.
3. Otherwise, moderators will first respond to such behavior with a warning.
4. Moderators follow a soft "three strikes" policy - the community member may
be given another chance, if they are receptive to the warning and change their
behavior.
5. If the community member is unreceptive or unreasonable when warned by a
moderator, or the warning goes unheeded, they may be banned for a first or
second offense. Repeated offenses will result in the community member being
banned.
6. Disciplinary actions (warnings, bans, etc) for Code of Conduct violations apply
to the platform where the violation occurred. However, depending on the severity
of the violation, the disciplinary action may be applied across Adafruit's other
community platforms. For example, a severe violation on the Adafruit Discord
server may result in a ban on not only the Adafruit Discord server, but also on
the Adafruit GitHub organisation, Adafruit Forums, Adafruit Twitter, etc.
## Scope
This Code of Conduct and the enforcement policies listed above apply to all
Adafruit Community venues. This includes but is not limited to any community
spaces (both public and private), the entire Adafruit Discord server, and
Adafruit GitHub repositories. Examples of Adafruit Community spaces include
but are not limited to meet-ups, audio chats on the Adafruit Discord, or
interaction at a conference.
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. As a community
member, you are representing our community, and are expected to behave
accordingly.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/),
version 1.4, available on [contributor-covenant.org](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html),
and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html).
For other projects adopting the Adafruit Community Code of
Conduct, please contact the maintainers of those projects for enforcement.
If you wish to use this code of conduct for your own project, consider
explicitly mentioning your moderation policy or making a copy with your
own moderation policy so as to avoid confusion.

View file

@ -1,6 +1,6 @@
MIT License
The MIT License (MIT)
Copyright (c) 2024 Adafruit Industries
Copyright (c) 2024 Jerry Needell for Adafruit Industries
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

324
LICENSES/CC-BY-4.0.txt Normal file
View file

@ -0,0 +1,324 @@
Creative Commons Attribution 4.0 International Creative Commons Corporation
("Creative Commons") is not a law firm and does not provide legal services
or legal advice. Distribution of Creative Commons public licenses does not
create a lawyer-client or other relationship. Creative Commons makes its licenses
and related information available on an "as-is" basis. Creative Commons gives
no warranties regarding its licenses, any material licensed under their terms
and conditions, or any related information. Creative Commons disclaims all
liability for damages resulting from their use to the fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and conditions
that creators and other rights holders may use to share original works of
authorship and other material subject to copyright and certain other rights
specified in the public license below. The following considerations are for
informational purposes only, are not exhaustive, and do not form part of our
licenses.
Considerations for licensors: Our public licenses are intended for use by
those authorized to give the public permission to use material in ways otherwise
restricted by copyright and certain other rights. Our licenses are irrevocable.
Licensors should read and understand the terms and conditions of the license
they choose before applying it. Licensors should also secure all rights necessary
before applying our licenses so that the public can reuse the material as
expected. Licensors should clearly mark any material not subject to the license.
This includes other CC-licensed material, or material used under an exception
or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public licenses, a licensor
grants the public permission to use the licensed material under specified
terms and conditions. If the licensor's permission is not necessary for any
reasonfor example, because of any applicable exception or limitation to copyrightthen
that use is not regulated by the license. Our licenses grant only permissions
under copyright and certain other rights that a licensor has authority to
grant. Use of the licensed material may still be restricted for other reasons,
including because others have copyright or other rights in the material. A
licensor may make special requests, such as asking that all changes be marked
or described. Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations for the public
: wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution
4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to
be bound by the terms and conditions of this Creative Commons Attribution
4.0 International Public License ("Public License"). To the extent this Public
License may be interpreted as a contract, You are granted the Licensed Rights
in consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the Licensor
receives from making the Licensed Material available under these terms and
conditions.
Section 1 Definitions.
a. Adapted Material means material subject to Copyright and Similar Rights
that is derived from or based upon the Licensed Material and in which the
Licensed Material is translated, altered, arranged, transformed, or otherwise
modified in a manner requiring permission under the Copyright and Similar
Rights held by the Licensor. For purposes of this Public License, where the
Licensed Material is a musical work, performance, or sound recording, Adapted
Material is always produced where the Licensed Material is synched in timed
relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright and Similar
Rights in Your contributions to Adapted Material in accordance with the terms
and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights closely
related to copyright including, without limitation, performance, broadcast,
sound recording, and Sui Generis Database Rights, without regard to how the
rights are labeled or categorized. For purposes of this Public License, the
rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
d. Effective Technological Measures means those measures that, in the absence
of proper authority, may not be circumvented under laws fulfilling obligations
under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996,
and/or similar international agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or any other
exception or limitation to Copyright and Similar Rights that applies to Your
use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database, or other
material to which the Licensor applied this Public License.
g. Licensed Rights means the rights granted to You subject to the terms and
conditions of this Public License, which are limited to all Copyright and
Similar Rights that apply to Your use of the Licensed Material and that the
Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights under this
Public License.
i. Share means to provide material to the public by any means or process that
requires permission under the Licensed Rights, such as reproduction, public
display, public performance, distribution, dissemination, communication, or
importation, and to make material available to the public including in ways
that members of the public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright resulting
from Directive 96/9/EC of the European Parliament and of the Council of 11
March 1996 on the legal protection of databases, as amended and/or succeeded,
as well as other essentially equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights under
this Public License. Your has a corresponding meaning.
Section 2 Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License, the Licensor
hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive,
irrevocable license to exercise the Licensed Rights in the Licensed Material
to:
A. reproduce and Share the Licensed Material, in whole or in part; and
B. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions
and Limitations apply to Your use, this Public License does not apply, and
You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes
You to exercise the Licensed Rights in all media and formats whether now known
or hereafter created, and to make technical modifications necessary to do
so. The Licensor waives and/or agrees not to assert any right or authority
to forbid You from making technical modifications necessary to exercise the
Licensed Rights, including technical modifications necessary to circumvent
Effective Technological Measures. For purposes of this Public License, simply
making modifications authorized by this Section 2(a)(4) never produces Adapted
Material.
5. Downstream recipients.
A. Offer from the Licensor Licensed Material. Every recipient of the Licensed
Material automatically receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this Public License.
B. No downstream restrictions. You may not offer or impose any additional
or different terms or conditions on, or apply any Effective Technological
Measures to, the Licensed Material if doing so restricts exercise of the Licensed
Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed
as permission to assert or imply that You are, or that Your use of the Licensed
Material is, connected with, or sponsored, endorsed, or granted official status
by, the Licensor or others designated to receive attribution as provided in
Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not licensed under this
Public License, nor are publicity, privacy, and/or other similar personality
rights; however, to the extent possible, the Licensor waives and/or agrees
not to assert any such rights held by the Licensor to the limited extent necessary
to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties
from You for the exercise of the Licensed Rights, whether directly or through
a collecting society under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly reserves any right
to collect such royalties.
Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following
conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified form), You must:
A. retain the following if it is supplied by the Licensor with the Licensed
Material:
i. identification of the creator(s) of the Licensed Material and any others
designated to receive attribution, in any reasonable manner requested by the
Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication
of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and
include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner
based on the medium, means, and context in which You Share the Licensed Material.
For example, it may be reasonable to satisfy the conditions by providing a
URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required
by Section 3(a)(1)(A) to the extent reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's License You apply
must not prevent recipients of the Adapted Material from complying with this
Public License.
Section 4 Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to
Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract,
reuse, reproduce, and Share all or a substantial portion of the contents of
the database;
b. if You include all or a substantial portion of the database contents in
a database in which You have Sui Generis Database Rights, then the database
in which You have Sui Generis Database Rights (but not its individual contents)
is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share all or
a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace
Your obligations under this Public License where the Licensed Rights include
other Copyright and Similar Rights.
Section 5 Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the extent possible,
the Licensor offers the Licensed Material as-is and as-available, and makes
no representations or warranties of any kind concerning the Licensed Material,
whether express, implied, statutory, or other. This includes, without limitation,
warranties of title, merchantability, fitness for a particular purpose, non-infringement,
absence of latent or other defects, accuracy, or the presence or absence of
errors, whether or not known or discoverable. Where disclaimers of warranties
are not allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable to You
on any legal theory (including, without limitation, negligence) or otherwise
for any direct, special, indirect, incidental, consequential, punitive, exemplary,
or other losses, costs, expenses, or damages arising out of this Public License
or use of the Licensed Material, even if the Licensor has been advised of
the possibility of such losses, costs, expenses, or damages. Where a limitation
of liability is not allowed in full or in part, this limitation may not apply
to You.
c. The disclaimer of warranties and limitation of liability provided above
shall be interpreted in a manner that, to the extent possible, most closely
approximates an absolute disclaimer and waiver of all liability.
Section 6 Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights
licensed here. However, if You fail to comply with this Public License, then
Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section
6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured
within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
c. For the avoidance of doubt, this Section 6(b) does not affect any right
the Licensor may have to seek remedies for Your violations of this Public
License.
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material
under separate terms or conditions or stop distributing the Licensed Material
at any time; however, doing so will not terminate this Public License.
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or
conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed
Material not stated herein are separate from and independent of the terms
and conditions of this Public License.
Section 8 Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not
be interpreted to, reduce, limit, restrict, or impose conditions on any use
of the Licensed Material that could lawfully be made without permission under
this Public License.
b. To the extent possible, if any provision of this Public License is deemed
unenforceable, it shall be automatically reformed to the minimum extent necessary
to make it enforceable. If the provision cannot be reformed, it shall be severed
from this Public License without affecting the enforceability of the remaining
terms and conditions.
c. No term or condition of this Public License will be waived and no failure
to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation
upon, or waiver of, any privileges and immunities that apply to the Licensor
or You, including from the legal processes of any jurisdiction or authority.
Creative Commons is not a party to its public licenses. Notwithstanding, Creative
Commons may elect to apply one of its public licenses to material it publishes
and in those instances will be considered the "Licensor." The text of the
Creative Commons public licenses is dedicated to the public domain under the
CC0 Public Domain Dedication. Except for the limited purpose of indicating
that material is shared under a Creative Commons public license or as otherwise
permitted by the Creative Commons policies published at creativecommons.org/policies,
Creative Commons does not authorize the use of the trademark "Creative Commons"
or any other trademark or logo of Creative Commons without its prior written
consent including, without limitation, in connection with any unauthorized
modifications to any of its public licenses or any other arrangements, understandings,
or agreements concerning use of licensed material. For the avoidance of doubt,
this paragraph does not form part of the public licenses.
Creative Commons may be contacted at creativecommons.org.

19
LICENSES/MIT.txt Normal file
View file

@ -0,0 +1,19 @@
MIT License Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

20
LICENSES/Unlicense.txt Normal file
View file

@ -0,0 +1,20 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute
this software, either in source code form or as a compiled binary, for any
purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and
to the detriment of our heirs and successors. We intend this dedication to
be an overt act of relinquishment in perpetuity of all present and future
rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information,
please refer to <https://unlicense.org/>

107
README.rst Normal file
View file

@ -0,0 +1,107 @@
Introduction
============
.. image:: https://readthedocs.org/projects/adafruit-circuitpython-rfm/badge/?version=latest
:target: https://docs.circuitpython.org/projects/rfm/en/latest/
:alt: Documentation Status
.. image:: https://raw.githubusercontent.com/adafruit/Adafruit_CircuitPython_Bundle/main/badges/adafruit_discord.svg
:target: https://adafru.it/discord
:alt: Discord
.. image:: https://github.com/jerryneedell/Adafruit_CircuitPython_RFM/workflows/Build%20CI/badge.svg
:target: https://github.com/jerryneedell/Adafruit_CircuitPython_RFM/actions
:alt: Build Status
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
:target: https://github.com/astral-sh/ruff
:alt: Code Style: Ruff
Support for RFM69 and RFM9x modules
Dependencies
=============
This driver depends on:
* `Adafruit CircuitPython <https://github.com/adafruit/circuitpython>`_
* `Bus Device <https://github.com/adafruit/Adafruit_CircuitPython_BusDevice>`_
Please ensure all dependencies are available on the CircuitPython filesystem.
This is easily achieved by downloading
`the Adafruit library and driver bundle <https://circuitpython.org/libraries>`_
or individual libraries can be installed using
`circup <https://github.com/adafruit/circup>`_.
Installing from PyPI
=====================
On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from
PyPI <https://pypi.org/project/adafruit-circuitpython-rfm/>`_.
To install for current user:
.. code-block:: shell
pip3 install adafruit-circuitpython-rfm
To install system-wide (this may be required in some cases):
.. code-block:: shell
sudo pip3 install adafruit-circuitpython-rfm
To install in a virtual environment in your current project:
.. code-block:: shell
mkdir project-name && cd project-name
python3 -m venv .venv
source .env/bin/activate
pip3 install adafruit-circuitpython-rfm
Installing to a Connected CircuitPython Device with Circup
==========================================================
Make sure that you have ``circup`` installed in your Python environment.
Install it with the following command if necessary:
.. code-block:: shell
pip3 install circup
With ``circup`` installed and your CircuitPython device connected use the
following command to install:
.. code-block:: shell
circup install adafruit_rfm
Or the following command to update an existing version:
.. code-block:: shell
circup update
Usage Example
=============
See examples in the GitHub Repository. <https://github.com/jerryneedell/Adafruit_CircuitPython_RFM.git>
Documentation
=============
API documentation for this library can be found on `Read the Docs <https://docs.circuitpython.org/projects/rfm/en/latest/>`_.
For information on building library documentation, please check out
`this guide <https://learn.adafruit.com/creating-and-sharing-a-circuitpython-library/sharing-our-docs-on-readthedocs#sphinx-5-1>`_.
Contributing
============
Contributions are welcome! Please read our `Code of Conduct
<https://github.com/jerryneedell/Adafruit_CircuitPython_RFM/blob/HEAD/CODE_OF_CONDUCT.md>`_
before contributing to help this project stay welcoming.

3
README.rst.license Normal file
View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
SPDX-FileCopyrightText: Copyright (c) 2024 Jerry Needell for Adafruit Industries
SPDX-License-Identifier: MIT

0
adafruit_rfm/__init__.py Normal file
View file

650
adafruit_rfm/rfm69.py Normal file
View file

@ -0,0 +1,650 @@
# SPDX-FileCopyrightText: 2024 Jerry Needell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_rfm.rfm69`
====================================================
CircuitPython RFM69 packet radio module. This supports sending and
receiving of packets with RFM69 series radios (433/915Mhz).
.. warning:: This is NOT for LoRa radios!
.. note:: This is a 'best effort' at receiving data using pure Python code. You might lose packets
if they're sent too quickly for the board to process them.
* Author(s): Jerry Needell
"""
import time
from micropython import const
from adafruit_rfm.rfm_common import RFMSPI, ticks_diff
HAS_SUPERVISOR = False
try:
import supervisor
if hasattr(supervisor, "ticks_ms"):
HAS_SUPERVISOR = True
except ImportError:
pass
try:
from typing import Optional
import busio
import digitalio
from circuitpython_typing import ReadableBuffer
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RFM.git"
# Internal constants:
_RF69_REG_00_FIFO = const(0x00)
_RF69_REG_01_OP_MODE = const(0x01)
_RF69_REG_02_DATA_MOD = const(0x02)
_RF69_REG_03_BITRATE_MSB = const(0x03)
_RF69_REG_04_BITRATE_LSB = const(0x04)
_RF69_REG_05_FDEV_MSB = const(0x05)
_RF69_REG_06_FDEV_LSB = const(0x06)
_RF69_REG_07_FRF_MSB = const(0x07)
_RF69_REG_08_FRF_MID = const(0x08)
_RF69_REG_09_FRF_LSB = const(0x09)
_RF69_REG_10_VERSION = const(0x10)
_RF69_REG_11_PA_LEVEL = const(0x11)
_RF69_REG_13_OCP = const(0x13)
_RF69_REG_19_RX_BW = const(0x19)
_RF69_REG_1A_AFC_BW = const(0x1A)
_RF69_REG_1B_OOK_PEAK = const(0x1B)
_RF69_REG_1C_OOK_AVG = const(0x1C)
_RF69_REG_1D_OOK_FIX = const(0x1D)
_RF69_REG_24_RSSI_VALUE = const(0x24)
_RF69_REG_25_DIO_MAPPING1 = const(0x25)
_RF69_REG_27_IRQ_FLAGS1 = const(0x27)
_RF69_REG_28_IRQ_FLAGS2 = const(0x28)
_RF69_REG_2C_PREAMBLE_MSB = const(0x2C)
_RF69_REG_2D_PREAMBLE_LSB = const(0x2D)
_RF69_REG_2E_SYNC_CONFIG = const(0x2E)
_RF69_REG_2F_SYNC_VALUE1 = const(0x2F)
_RF69_REG_39_NODE_ADDR = const(0x39)
_RF69_REG_3A_BROADCAST_ADDR = const(0x3A)
_RF69_REG_37_PACKET_CONFIG1 = const(0x37)
_RF69_REG_3C_FIFO_THRESH = const(0x3C)
_RF69_REG_3D_PACKET_CONFIG2 = const(0x3D)
_RF69_REG_3E_AES_KEY1 = const(0x3E)
_RF69_REG_4E_TEMP1 = const(0x4E)
_RF69_REG_4F_TEMP2 = const(0x4F)
_RF69_REG_5A_TEST_PA1 = const(0x5A)
_RF69_REG_5C_TEST_PA2 = const(0x5C)
_RF69_REG_6F_TEST_DAGC = const(0x6F)
_TEST_PA1_NORMAL = const(0x55)
_TEST_PA1_BOOST = const(0x5D)
_TEST_PA2_NORMAL = const(0x70)
_TEST_PA2_BOOST = const(0x7C)
_OCP_NORMAL = const(0x1A)
_OCP_HIGH_POWER = const(0x0F)
# The crystal oscillator frequency and frequency synthesizer step size.
# See the datasheet for details of this calculation.
_FXOSC = 32000000.0
_FSTEP = _FXOSC / 524288
# RadioHead specific compatibility constants.
_RH_BROADCAST_ADDRESS = const(0xFF)
# The acknowledgement bit in the FLAGS
# The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved
# for application layer use.
_RH_FLAGS_ACK = const(0x80)
_RH_FLAGS_RETRY = const(0x40)
# User facing constants:
SLEEP_MODE = 0b000
STANDBY_MODE = 0b001
FS_MODE = 0b010
TX_MODE = 0b011
RX_MODE = 0b100
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
class RFM69(RFMSPI):
"""Interface to a RFM69 series packet radio. Allows simple sending and
receiving of wireless data at supported frequencies of the radio
(433/915mhz).
:param busio.SPI spi: The SPI bus connected to the chip. Ensure SCK, MOSI, and MISO are
connected.
:param ~digitalio.DigitalInOut cs: A DigitalInOut object connected to the chip's CS/chip select
line.
:param ~digitalio.DigitalInOut reset: A DigitalInOut object connected to the chip's RST/reset
line.
:param int frequency: The center frequency to configure for radio transmission and reception.
Must be a frequency supported by your hardware (i.e. either 433 or 915mhz).
:param bytes sync_word: A byte string up to 8 bytes long which represents the syncronization
word used by received and transmitted packets. Read the datasheet for a full understanding
of this value! However by default the library will set a value that matches the RadioHead
Arduino library.
:param int preamble_length: The number of bytes to pre-pend to a data packet as a preamble.
This is by default 4 to match the RadioHead library.
:param bytes encryption_key: A 16 byte long string that represents the AES encryption key to use
when encrypting and decrypting packets. Both the transmitter and receiver MUST have the
same key value! By default no encryption key is set or used.
:param bool high_power: Indicate if the chip is a high power variant that supports boosted
transmission power. The default is True as it supports the common RFM69HCW modules sold by
Adafruit.
Remember this library makes a best effort at receiving packets with pure Python code. Trying
to receive packets too quickly will result in lost data so limit yourself to simple scenarios
of sending and receiving single packets at a time.
Also note this library defaults to be compatible with RadioHead Arduino library communication.
This means the library sets up the radio modulation to match RadioHead's default of GFSK
encoding, 250kbit/s bitrate, and 250khz frequency deviation. To change this requires explicitly
setting the radio's bitrate and encoding register bits.
Read the datasheet and study the init function to see an example of this--advanced users only!
Advanced RadioHead features like address/node specific packets or "reliable datagram" delivery
are supported however due to the limitations noted, "reliable datagram" is still subject to
missed packets.
"""
# Control bits from the registers of the chip:
data_mode = RFMSPI.RegisterBits(_RF69_REG_02_DATA_MOD, offset=5, bits=2)
modulation_type = RFMSPI.RegisterBits(_RF69_REG_02_DATA_MOD, offset=3, bits=2)
modulation_shaping = RFMSPI.RegisterBits(_RF69_REG_02_DATA_MOD, offset=0, bits=2)
temp_start = RFMSPI.RegisterBits(_RF69_REG_4E_TEMP1, offset=3)
temp_running = RFMSPI.RegisterBits(_RF69_REG_4E_TEMP1, offset=2)
sync_on = RFMSPI.RegisterBits(_RF69_REG_2E_SYNC_CONFIG, offset=7)
sync_size = RFMSPI.RegisterBits(_RF69_REG_2E_SYNC_CONFIG, offset=3, bits=3)
aes_on = RFMSPI.RegisterBits(_RF69_REG_3D_PACKET_CONFIG2, offset=0)
pa_0_on = RFMSPI.RegisterBits(_RF69_REG_11_PA_LEVEL, offset=7)
pa_1_on = RFMSPI.RegisterBits(_RF69_REG_11_PA_LEVEL, offset=6)
pa_2_on = RFMSPI.RegisterBits(_RF69_REG_11_PA_LEVEL, offset=5)
output_power = RFMSPI.RegisterBits(_RF69_REG_11_PA_LEVEL, offset=0, bits=5)
rx_bw_dcc_freq = RFMSPI.RegisterBits(_RF69_REG_19_RX_BW, offset=5, bits=3)
rx_bw_mantissa = RFMSPI.RegisterBits(_RF69_REG_19_RX_BW, offset=3, bits=2)
rx_bw_exponent = RFMSPI.RegisterBits(_RF69_REG_19_RX_BW, offset=0, bits=3)
afc_bw_dcc_freq = RFMSPI.RegisterBits(_RF69_REG_1A_AFC_BW, offset=5, bits=3)
afc_bw_mantissa = RFMSPI.RegisterBits(_RF69_REG_1A_AFC_BW, offset=3, bits=2)
afc_bw_exponent = RFMSPI.RegisterBits(_RF69_REG_1A_AFC_BW, offset=0, bits=3)
packet_format = RFMSPI.RegisterBits(_RF69_REG_37_PACKET_CONFIG1, offset=7, bits=1)
dc_free = RFMSPI.RegisterBits(_RF69_REG_37_PACKET_CONFIG1, offset=5, bits=2)
crc_on = RFMSPI.RegisterBits(_RF69_REG_37_PACKET_CONFIG1, offset=4, bits=1)
crc_auto_clear_off = RFMSPI.RegisterBits(_RF69_REG_37_PACKET_CONFIG1, offset=3, bits=1)
address_filter = RFMSPI.RegisterBits(_RF69_REG_37_PACKET_CONFIG1, offset=1, bits=2)
mode_ready = RFMSPI.RegisterBits(_RF69_REG_27_IRQ_FLAGS1, offset=7)
dio_0_mapping = RFMSPI.RegisterBits(_RF69_REG_25_DIO_MAPPING1, offset=6, bits=2)
ook_thresh_type = RFMSPI.RegisterBits(_RF69_REG_1B_OOK_PEAK, offset=6, bits=2)
ook_thresh_step = RFMSPI.RegisterBits(_RF69_REG_1B_OOK_PEAK, offset=5, bits=3)
ook_peak_thresh_dec = RFMSPI.RegisterBits(_RF69_REG_1B_OOK_PEAK, offset=0, bits=3)
ook_average_thresh_filt = RFMSPI.RegisterBits(_RF69_REG_1C_OOK_AVG, offset=6, bits=2)
# pylint: disable=too-many-statements
# pylint: disable=too-many-arguments
def __init__( # noqa: PLR0913
self,
spi: busio.SPI,
cs: digitalio.DigitalInOut,
rst: digitalio.DigitalInOut,
frequency: int,
*,
sync_word: bytes = b"\x2d\xd4",
preamble_length: int = 4,
encryption_key: Optional[bytes] = None,
high_power: bool = True,
baudrate: int = 2000000,
crc: bool = True,
) -> None:
super().__init__(spi, cs, baudrate=baudrate)
self.module = "RFM69"
self.max_packet_length = 60
self.high_power = high_power
# Device support SPI mode 0 (polarity & phase = 0) up to a max of 10mhz.
# self._device = spidev.SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=0)
# Setup reset as a digital output that's low.
self._rst = rst
self._rst.switch_to_output(value=False)
self.reset() # Reset the chip.
# Check the version of the chip.
version = self.read_u8(_RF69_REG_10_VERSION)
if version not in (0x23, 0x24):
raise RuntimeError("Invalid RFM69 version, check wiring! ID found:", hex(version))
self.idle() # Enter idle state.
# Setup the chip in a similar way to the RadioHead RFM69 library.
# Set FIFO TX condition to not empty and the default FIFO threshold to 15.
self.write_u8(_RF69_REG_3C_FIFO_THRESH, 0b10001111)
# Configure low beta off.
self.write_u8(_RF69_REG_6F_TEST_DAGC, 0x30)
# Set the syncronization word.
self.sync_word = sync_word
self.preamble_length = preamble_length # Set the preamble length.
self.frequency_mhz = frequency # Set frequency.
self.encryption_key = encryption_key # Set encryption key.
# Configure modulation for RadioHead library GFSK_Rb250Fd250 mode
# by default. Users with advanced knowledge can manually reconfigure
# for any other mode (consulting the datasheet is absolutely
# necessary!).
self.modulation_shaping = 0b01 # Gaussian filter, BT=1.0
self.bitrate = 250000 # 250kbs
self.frequency_deviation = 250000 # 250khz
self.rx_bw_dcc_freq = 0b111 # RxBw register = 0xE0
self.rx_bw_mantissa = 0b00
self.rx_bw_exponent = 0b000
self.afc_bw_dcc_freq = 0b111 # AfcBw register = 0xE0
self.afc_bw_mantissa = 0b00
self.afc_bw_exponent = 0b000
self.packet_format = 1 # Variable length.
self.dc_free = 0b10 # Whitening
# Set transmit power to 13 dBm, a safe value any module supports.
self._tx_power = None
self.tx_power = 13
# Default to enable CRC checking on incoming packets.
self.enable_crc = crc
self.snr = None
def reset(self) -> None:
"""Perform a reset of the chip."""
# See section 7.2.2 of the datasheet for reset description.
self._rst.value = True
time.sleep(0.0001) # 100 us
self._rst.value = False
time.sleep(0.005) # 5 ms
def disable_boost(self) -> None:
"""Disable preamp boost."""
if self.high_power:
self.write_u8(_RF69_REG_5A_TEST_PA1, _TEST_PA1_NORMAL)
self.write_u8(_RF69_REG_5C_TEST_PA2, _TEST_PA2_NORMAL)
self.write_u8(_RF69_REG_13_OCP, _OCP_NORMAL)
def idle(self) -> None:
"""Enter idle standby mode (switching off high power amplifiers if necessary)."""
# Like RadioHead library, turn off high power boost if enabled.
self.disable_boost()
self.operation_mode = STANDBY_MODE
def sleep(self) -> None:
"""Enter sleep mode."""
self.operation_mode = SLEEP_MODE
def listen(self) -> None:
"""Listen for packets to be received by the chip. Use :py:func:`receive` to listen, wait
and retrieve packets as they're available.
"""
# Like RadioHead library, turn off high power boost if enabled.
self.disable_boost()
# Enable payload ready interrupt for D0 line.
self.dio_0_mapping = 0b01
# Enter RX mode (will clear FIFO!).
self.operation_mode = RX_MODE
def transmit(self) -> None:
"""Transmit a packet which is queued in the FIFO. This is a low level function for
entering transmit mode and more. For generating and transmitting a packet of data use
:py:func:`send` instead.
"""
# Like RadioHead library, turn on high power boost if needed.
if self.high_power and (self._tx_power >= 18):
self.write_u8(_RF69_REG_5A_TEST_PA1, _TEST_PA1_BOOST)
self.write_u8(_RF69_REG_5C_TEST_PA2, _TEST_PA2_BOOST)
self.write_u8(_RF69_REG_13_OCP, _OCP_HIGH_POWER)
# Enable packet sent interrupt for D0 line.
self.dio_0_mapping = 0b00
# Enter TX mode (will clear FIFO!).
self.operation_mode = TX_MODE
@property
def temperature(self) -> float:
"""The internal temperature of the chip in degrees Celsius. Be warned this is not
calibrated or very accurate.
.. warning:: Reading this will STOP any receiving/sending that might be happening!
"""
# Start a measurement then poll the measurement finished bit.
self.temp_start = 1
while self.temp_running > 0:
pass
# Grab the temperature value and convert it to Celsius.
# This uses the same observed value formula from the Radiohead library.
temp = self.read_u8(_RF69_REG_4F_TEMP2)
return 166.0 - temp
@property
def operation_mode(self) -> int:
"""The operation mode value. Unless you're manually controlling the chip you shouldn't
change the operation_mode with this property as other side-effects are required for
changing logical modes--use :py:func:`idle`, :py:func:`sleep`, :py:func:`transmit`,
:py:func:`listen` instead to signal intent for explicit logical modes.
"""
op_mode = self.read_u8(_RF69_REG_01_OP_MODE)
return (op_mode >> 2) & 0b111
@operation_mode.setter
def operation_mode(self, val: int) -> None:
assert 0 <= val <= 4
# Set the mode bits inside the operation mode register.
op_mode = self.read_u8(_RF69_REG_01_OP_MODE)
op_mode &= 0b11100011
op_mode |= val << 2
self.write_u8(_RF69_REG_01_OP_MODE, op_mode)
# Wait for mode to change by polling interrupt bit.
if HAS_SUPERVISOR:
start = supervisor.ticks_ms()
while not self.mode_ready:
if ticks_diff(supervisor.ticks_ms(), start) >= 1000:
raise TimeoutError("Operation Mode failed to set.")
else:
start = time.monotonic()
while not self.mode_ready:
if time.monotonic() - start >= 1:
raise TimeoutError("Operation Mode failed to set.")
@property
def sync_word(self) -> bytearray:
"""The synchronization word value. This is a byte string up to 8 bytes long (64 bits)
which indicates the synchronization word for transmitted and received packets. Any
received packet which does not include this sync word will be ignored. The default value
is 0x2D, 0xD4 which matches the RadioHead RFM69 library. Setting a value of None will
disable synchronization word matching entirely.
"""
# Handle when sync word is disabled..
if not self.sync_on:
return None
# Sync word is not disabled so read the current value.
sync_word_length = self.sync_size + 1 # Sync word size is offset by 1
# according to datasheet.
sync_word = bytearray(sync_word_length)
self.read_into(_RF69_REG_2F_SYNC_VALUE1, sync_word)
return sync_word
@sync_word.setter
def sync_word(self, val: Optional[bytearray]) -> None:
# Handle disabling sync word when None value is set.
if val is None:
self.sync_on = 0
else:
# Check sync word is at most 8 bytes.
assert 1 <= len(val) <= 8
# Update the value, size and turn on the sync word.
self.write_from(_RF69_REG_2F_SYNC_VALUE1, val)
self.sync_size = len(val) - 1 # Again sync word size is offset by
# 1 according to datasheet.
self.sync_on = 1
@property
def preamble_length(self) -> int:
"""The length of the preamble for sent and received packets, an unsigned 16-bit value.
Received packets must match this length or they are ignored! Set to 4 to match the
RadioHead RFM69 library.
"""
msb = self.read_u8(_RF69_REG_2C_PREAMBLE_MSB)
lsb = self.read_u8(_RF69_REG_2D_PREAMBLE_LSB)
return ((msb << 8) | lsb) & 0xFFFF
@preamble_length.setter
def preamble_length(self, val: int) -> None:
assert 0 <= val <= 65535
self.write_u8(_RF69_REG_2C_PREAMBLE_MSB, (val >> 8) & 0xFF)
self.write_u8(_RF69_REG_2D_PREAMBLE_LSB, val & 0xFF)
@property
def frequency_mhz(self) -> float:
"""The frequency of the radio in Megahertz. Only the allowed values for your radio must be
specified (i.e. 433 vs. 915 mhz)!
"""
# FRF register is computed from the frequency following the datasheet.
# See section 6.2 and FRF register description.
# Read bytes of FRF register and assemble into a 24-bit unsigned value.
msb = self.read_u8(_RF69_REG_07_FRF_MSB)
mid = self.read_u8(_RF69_REG_08_FRF_MID)
lsb = self.read_u8(_RF69_REG_09_FRF_LSB)
frf = ((msb << 16) | (mid << 8) | lsb) & 0xFFFFFF
frequency = (frf * _FSTEP) / 1000000.0
return frequency
@frequency_mhz.setter
def frequency_mhz(self, val: float) -> None:
assert 290 <= val <= 1020
# Calculate FRF register 24-bit value using section 6.2 of the datasheet.
frf = int((val * 1000000.0) / _FSTEP) & 0xFFFFFF
# Extract byte values and update registers.
msb = frf >> 16
mid = (frf >> 8) & 0xFF
lsb = frf & 0xFF
self.write_u8(_RF69_REG_07_FRF_MSB, msb)
self.write_u8(_RF69_REG_08_FRF_MID, mid)
self.write_u8(_RF69_REG_09_FRF_LSB, lsb)
@property
def encryption_key(self) -> bytearray:
"""The AES encryption key used to encrypt and decrypt packets by the chip. This can be set
to None to disable encryption (the default), otherwise it must be a 16 byte long byte
string which defines the key (both the transmitter and receiver must use the same key
value).
"""
# Handle if encryption is disabled.
if self.aes_on == 0:
return None
# Encryption is enabled so read the key and return it.
key = bytearray(16)
self.read_into(_RF69_REG_3E_AES_KEY1, key)
return key
@encryption_key.setter
def encryption_key(self, val: bytearray) -> None:
# Handle if unsetting the encryption key (None value).
if val is None:
self.aes_on = 0
else:
# Set the encryption key and enable encryption.
assert len(val) == 16
self.write_from(_RF69_REG_3E_AES_KEY1, val)
self.aes_on = 1
@property
def tx_power(self) -> int:
"""The transmit power in dBm. Can be set to a value from -2 to 20 for high power devices
(RFM69HCW, high_power=True) or -18 to 13 for low power devices. Only integer power
levels are actually set (i.e. 12.5 will result in a value of 12 dBm).
"""
# Follow table 10 truth table from the datasheet for determining power
# level from the individual PA level bits and output power register.
pa0 = self.pa_0_on
pa1 = self.pa_1_on
pa2 = self.pa_2_on
current_output_power = self.output_power
if pa0 and not pa1 and not pa2:
# -18 to 13 dBm range
return -18 + current_output_power
if not pa0 and pa1 and not pa2:
# -2 to 13 dBm range
return -18 + current_output_power
if not pa0 and pa1 and pa2 and self.high_power and self._tx_power < 18:
# 2 to 17 dBm range
return -14 + current_output_power
if not pa0 and pa1 and pa2 and self.high_power and self._tx_power >= 18:
# 5 to 20 dBm range
return -11 + current_output_power
raise RuntimeError("Power amps state unknown!")
@tx_power.setter
def tx_power(self, val: float):
val = int(val)
# Determine power amplifier and output power values depending on
# high power state and requested power.
pa_0_on = pa_1_on = pa_2_on = 0
output_power = 0
if self.high_power:
# Handle high power mode.
assert -2 <= val <= 20
pa_1_on = 1
if val <= 13:
output_power = val + 18
elif 13 < val <= 17:
pa_2_on = 1
output_power = val + 14
else: # power >= 18 dBm
# Note this also needs PA boost enabled separately!
pa_2_on = 1
output_power = val + 11
else:
# Handle non-high power mode.
assert -18 <= val <= 13
# Enable only power amplifier 0 and set output power.
pa_0_on = 1
output_power = val + 18
# Set power amplifiers and output power as computed above.
self.pa_0_on = pa_0_on
self.pa_1_on = pa_1_on
self.pa_2_on = pa_2_on
self.output_power = output_power
self._tx_power = val
@property
def rssi(self) -> float:
"""The received strength indicator (in dBm).
May be inaccuate if not read immediatey. last_rssi contains the value read immediately
receipt of the last packet.
"""
# Read RSSI register and convert to value using formula in datasheet.
return -self.read_u8(_RF69_REG_24_RSSI_VALUE) / 2.0
@property
def bitrate(self) -> float:
"""The modulation bitrate in bits/second (or chip rate if Manchester encoding is enabled).
Can be a value from ~489 to 32mbit/s, but see the datasheet for the exact supported
values.
"""
msb = self.read_u8(_RF69_REG_03_BITRATE_MSB)
lsb = self.read_u8(_RF69_REG_04_BITRATE_LSB)
return _FXOSC / ((msb << 8) | lsb)
@bitrate.setter
def bitrate(self, val: float) -> None:
assert (_FXOSC / 65535) <= val <= 32000000.0
# Round up to the next closest bit-rate value with addition of 0.5.
bitrate = int((_FXOSC / val) + 0.5) & 0xFFFF
self.write_u8(_RF69_REG_03_BITRATE_MSB, bitrate >> 8)
self.write_u8(_RF69_REG_04_BITRATE_LSB, bitrate & 0xFF)
@property
def frequency_deviation(self) -> float:
"""The frequency deviation in Hertz."""
msb = self.read_u8(_RF69_REG_05_FDEV_MSB)
lsb = self.read_u8(_RF69_REG_06_FDEV_LSB)
return _FSTEP * ((msb << 8) | lsb)
@frequency_deviation.setter
def frequency_deviation(self, val: float) -> None:
assert 0 <= val <= (_FSTEP * 16383) # fdev is a 14-bit unsigned value
# Round up to the next closest integer value with addition of 0.5.
fdev = int((val / _FSTEP) + 0.5) & 0x3FFF
self.write_u8(_RF69_REG_05_FDEV_MSB, fdev >> 8)
self.write_u8(_RF69_REG_06_FDEV_LSB, fdev & 0xFF)
@property
def enable_crc(self) -> bool:
"""Set to True to enable hardware CRC checking of incoming packets.
Incoming packets that fail the CRC check are not processed. Set to
False to disable CRC checking and process all incoming packets."""
return self.crc_on
@enable_crc.setter
def enable_crc(self, val: bool) -> None:
# Optionally enable CRC checking on incoming packets.
if val:
self.crc_on = 1
else:
self.crc_on = 0
@property
def crc_error(self) -> bool:
"""crc status"""
return (self.read_u8(_RF69_REG_28_IRQ_FLAGS2) & 0x2) >> 1
@property
def enable_address_filter(self) -> bool:
"""Set to True to enable address filtering.
Incoming packets that do no match the node address or broadcast address
will be ignored."""
return self.address_filter
@enable_address_filter.setter
def enable_address_filter(self, val: bool) -> None:
# Enable address filtering on incoming packets.
if val:
self.address_filter = 2 # accept node address or broadcast address
else:
self.address_filter = 0
@property
def fsk_node_address(self) -> int:
"""Node Address for Address Filtering"""
return self.read_u8(_RF69_REG_39_NODE_ADDR)
@fsk_node_address.setter
def fsk_node_address(self, val: int) -> None:
assert 0 <= val <= 255
self.write_u8(_RF69_REG_39_NODE_ADDR, val)
@property
def fsk_broadcast_address(self) -> int:
"""Node Address for Address Filtering"""
return self.read_u8(_RF69_REG_3A_BROADCAST_ADDR)
@fsk_broadcast_address.setter
def fsk_broadcast_address(self, val: int) -> None:
assert 0 <= val <= 255
self.write_u8(_RF69_REG_3A_BROADCAST_ADDR, val)
@property
def ook_fixed_threshold(self) -> int:
"""Fixed threshold for data slicer in OOK mode"""
return self.read_u8(_RF69_REG_1D_OOK_FIX)
@ook_fixed_threshold.setter
def ook_fixed_threshold(self, val: int) -> None:
assert 0 <= val <= 255
self.write_u8(_RF69_REG_1D_OOK_FIX, val)
def packet_sent(self) -> bool:
"""Transmit status"""
return (self.read_u8(_RF69_REG_28_IRQ_FLAGS2) & 0x8) >> 3
def payload_ready(self) -> bool:
"""Receive status"""
return (self.read_u8(_RF69_REG_28_IRQ_FLAGS2) & 0x4) >> 2
def clear_interrupt(self) -> None:
"""Clear interrupt flags"""
self.write_u8(_RF69_REG_27_IRQ_FLAGS1, 0xFF)
self.write_u8(_RF69_REG_28_IRQ_FLAGS2, 0xFF)
def fill_fifo(self, payload: ReadableBuffer) -> None:
"""Write the payload to the FIFO."""
complete_payload = bytearray(1) # prepend packet length to payload
complete_payload[0] = len(payload)
# put the payload lengthe in the beginning of the packet for RFM69
complete_payload = complete_payload + payload
# Write payload to transmit fifo
self.write_from(_RF69_REG_00_FIFO, complete_payload)
def read_fifo(self) -> bytearray:
"""Read the packet from the FIFO."""
# Read the length of the FIFO.
fifo_length = self.read_u8(_RF69_REG_00_FIFO)
if fifo_length > 0: # read and clear the FIFO if anything in it
packet = bytearray(fifo_length)
# read the packet
self.read_into(_RF69_REG_00_FIFO, packet, fifo_length)
return packet

533
adafruit_rfm/rfm9x.py Normal file
View file

@ -0,0 +1,533 @@
# SPDX-FileCopyrightText: 2024 Jerry Needell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_rfm.rfm9x`
====================================================
CircuitPython module for the RFM95/6/7/8 LoRa 433/915mhz radio modules.
* Author(s): Jerry Needell
"""
import time
from micropython import const
from adafruit_rfm.rfm_common import RFMSPI
try:
import busio
import digitalio
from circuitpython_typing import ReadableBuffer
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RFM.git"
# pylint: disable=duplicate-code
# Internal constants:
# Register names (FSK Mode even though we use LoRa instead, from table 85)
_RF95_REG_00_FIFO = const(0x00)
_RF95_REG_01_OP_MODE = const(0x01)
_RF95_REG_06_FRF_MSB = const(0x06)
_RF95_REG_07_FRF_MID = const(0x07)
_RF95_REG_08_FRF_LSB = const(0x08)
_RF95_REG_09_PA_CONFIG = const(0x09)
_RF95_REG_0A_PA_RAMP = const(0x0A)
_RF95_REG_0B_OCP = const(0x0B)
_RF95_REG_0C_LNA = const(0x0C)
_RF95_REG_0D_FIFO_ADDR_PTR = const(0x0D)
_RF95_REG_0E_FIFO_TX_BASE_ADDR = const(0x0E)
_RF95_REG_0F_FIFO_RX_BASE_ADDR = const(0x0F)
_RF95_REG_10_FIFO_RX_CURRENT_ADDR = const(0x10)
_RF95_REG_11_IRQ_FLAGS_MASK = const(0x11)
_RF95_REG_12_IRQ_FLAGS = const(0x12)
_RF95_REG_13_RX_NB_BYTES = const(0x13)
_RF95_REG_14_RX_HEADER_CNT_VALUE_MSB = const(0x14)
_RF95_REG_15_RX_HEADER_CNT_VALUE_LSB = const(0x15)
_RF95_REG_16_RX_PACKET_CNT_VALUE_MSB = const(0x16)
_RF95_REG_17_RX_PACKET_CNT_VALUE_LSB = const(0x17)
_RF95_REG_18_MODEM_STAT = const(0x18)
_RF95_REG_19_PKT_SNR_VALUE = const(0x19)
_RF95_REG_1A_PKT_RSSI_VALUE = const(0x1A)
_RF95_REG_1B_RSSI_VALUE = const(0x1B)
_RF95_REG_1C_HOP_CHANNEL = const(0x1C)
_RF95_REG_1D_MODEM_CONFIG1 = const(0x1D)
_RF95_REG_1E_MODEM_CONFIG2 = const(0x1E)
_RF95_REG_1F_SYMB_TIMEOUT_LSB = const(0x1F)
_RF95_REG_20_PREAMBLE_MSB = const(0x20)
_RF95_REG_21_PREAMBLE_LSB = const(0x21)
_RF95_REG_22_PAYLOAD_LENGTH = const(0x22)
_RF95_REG_23_MAX_PAYLOAD_LENGTH = const(0x23)
_RF95_REG_24_HOP_PERIOD = const(0x24)
_RF95_REG_25_FIFO_RX_BYTE_ADDR = const(0x25)
_RF95_REG_26_MODEM_CONFIG3 = const(0x26)
_RF95_REG_40_DIO_MAPPING1 = const(0x40)
_RF95_REG_41_DIO_MAPPING2 = const(0x41)
_RF95_REG_42_VERSION = const(0x42)
_RF95_REG_4B_TCXO = const(0x4B)
_RF95_REG_4D_PA_DAC = const(0x4D)
_RF95_REG_5B_FORMER_TEMP = const(0x5B)
_RF95_REG_61_AGC_REF = const(0x61)
_RF95_REG_62_AGC_THRESH1 = const(0x62)
_RF95_REG_63_AGC_THRESH2 = const(0x63)
_RF95_REG_64_AGC_THRESH3 = const(0x64)
_RF95_DETECTION_OPTIMIZE = const(0x31)
_RF95_DETECTION_THRESHOLD = const(0x37)
_RF95_PA_DAC_DISABLE = const(0x04)
_RF95_PA_DAC_ENABLE = const(0x07)
# The crystal oscillator frequency of the module
_RF95_FXOSC = 32000000.0
# The Frequency Synthesizer step = RH_RF95_FXOSC / 2^^19
_RF95_FSTEP = _RF95_FXOSC / 524288
# RadioHead specific compatibility constants.
_RH_BROADCAST_ADDRESS = const(0xFF)
# The acknowledgement bit in the FLAGS
# The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved
# for application layer use.
_RH_FLAGS_ACK = const(0x80)
_RH_FLAGS_RETRY = const(0x40)
# User facing constants:
SLEEP_MODE = 0b000
STANDBY_MODE = 0b001
FS_TX_MODE = 0b010
TX_MODE = 0b011
FS_RX_MODE = 0b100
RX_MODE = 0b101
# pylint: disable=too-many-instance-attributes
class RFM9x(RFMSPI):
"""Interface to a RFM95/6/7/8 LoRa radio module. Allows sending and
receiving bytes of data in long range LoRa mode at a support board frequency
(433/915mhz).
You must specify the following parameters:
- spi: The SPI bus connected to the radio.
- cs: The CS pin DigitalInOut connected to the radio.
- reset: The reset/RST pin DigialInOut connected to the radio.
- frequency: The frequency (in mhz) of the radio module (433/915mhz typically).
You can optionally specify:
- preamble_length: The length in bytes of the packet preamble (default 8).
- high_power: Boolean to indicate a high power board (RFM95, etc.). Default
is True for high power.
- baudrate: Baud rate of the SPI connection, default is 10mhz but you might
choose to lower to 1mhz if using long wires or a breadboard.
- agc: Boolean to Enable/Disable Automatic Gain Control - Default=False (AGC off)
- crc: Boolean to Enable/Disable Cyclic Redundancy Check - Default=True (CRC Enabled)
Remember this library makes a best effort at receiving packets with pure
Python code. Trying to receive packets too quickly will result in lost data
so limit yourself to simple scenarios of sending and receiving single
packets at a time.
Also note this library tries to be compatible with raw RadioHead Arduino
library communication. This means the library sets up the radio modulation
to match RadioHead's defaults.
Advanced RadioHead features like address/node specific packets
or "reliable datagram" delivery are supported however due to the
limitations noted, "reliable datagram" is still subject to missed packets.
"""
operation_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, bits=3)
low_frequency_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=3, bits=1)
modulation_type = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=5, bits=2)
# Long range/LoRa mode can only be set in sleep mode!
long_range_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=7, bits=1)
output_power = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, bits=4)
max_power = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, offset=4, bits=3)
pa_select = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, offset=7, bits=1)
pa_dac = RFMSPI.RegisterBits(_RF95_REG_4D_PA_DAC, bits=3)
dio0_mapping = RFMSPI.RegisterBits(_RF95_REG_40_DIO_MAPPING1, offset=6, bits=2)
auto_agc = RFMSPI.RegisterBits(_RF95_REG_26_MODEM_CONFIG3, offset=2, bits=1)
low_datarate_optimize = RFMSPI.RegisterBits(_RF95_REG_26_MODEM_CONFIG3, offset=3, bits=1)
lna_boost_hf = RFMSPI.RegisterBits(_RF95_REG_0C_LNA, offset=0, bits=2)
auto_ifon = RFMSPI.RegisterBits(_RF95_DETECTION_OPTIMIZE, offset=7, bits=1)
detection_optimize = RFMSPI.RegisterBits(_RF95_DETECTION_OPTIMIZE, offset=0, bits=3)
bw_bins = (7800, 10400, 15600, 20800, 31250, 41700, 62500, 125000, 250000)
def __init__( # noqa: PLR0913
self,
spi: busio.SPI,
cs: digitalio.DigitalInOut, # pylint: disable=invalid-name
rst: digitalio.DigitalInOut,
frequency: int,
*,
preamble_length: int = 8,
high_power: bool = True,
baudrate: int = 5000000,
agc: bool = False,
crc: bool = True,
) -> None:
super().__init__(spi, cs, baudrate=baudrate)
self.module = "RFM9X"
self.max_packet_length = 252
self.high_power = high_power
# Device support SPI mode 0 (polarity & phase = 0) up to a max of 10mhz.
# Set Default Baudrate to 5MHz to avoid problems
# self._device = spidev.SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=0)
# Setup reset as a digital output - initially High
# This line is pulled low as an output quickly to trigger a reset.
self._rst = rst
# initialize Reset High
self._rst.switch_to_output(value=True)
self.reset()
# No device type check! Catch an error from the very first request and
# throw a nicer message to indicate possible wiring problems.
version = self.read_u8(address=_RF95_REG_42_VERSION)
if version != 18:
raise RuntimeError(
"Failed to find rfm9x with expected version -- check wiring. Version found:",
hex(version),
)
# Set sleep mode, wait 10s and confirm in sleep mode (basic device check).
# Also set long range mode (LoRa mode) as it can only be done in sleep.
self.sleep()
time.sleep(0.01)
self.long_range_mode = True
if self.operation_mode != SLEEP_MODE or not self.long_range_mode:
raise RuntimeError("Failed to configure radio for LoRa mode, check wiring!")
# clear default setting for access to LF registers if frequency > 525MHz
if frequency > 525:
self.low_frequency_mode = 0
# Setup entire 256 byte FIFO
self.write_u8(_RF95_REG_0E_FIFO_TX_BASE_ADDR, 0x00)
self.write_u8(_RF95_REG_0F_FIFO_RX_BASE_ADDR, 0x00)
# Set mode idle
self.idle()
# Set frequency
self.frequency_mhz = frequency
# Set preamble length (default 8 bytes to match radiohead).
self.preamble_length = preamble_length
# Defaults set modem config to RadioHead compatible Bw125Cr45Sf128 mode.
self.signal_bandwidth = 125000
self.coding_rate = 5
self.spreading_factor = 7
# Default to enable CRC checking on incoming packets.
self.enable_crc = crc
"""CRC Enable state"""
# set AGC - Default = False
self.auto_agc = agc
"""Automatic Gain Control state"""
# Set transmit power to 13 dBm, a safe value any module supports.
self.tx_power = 13
def reset(self) -> None:
"""Perform a reset of the chip."""
# See section 7.2.2 of the datasheet for reset description.
self._rst.value = False # Set Reset Low
time.sleep(0.0001) # 100 us
self._rst.value = True # set Reset High
time.sleep(0.005) # 5 ms
def idle(self) -> None:
"""Enter idle standby mode."""
self.operation_mode = STANDBY_MODE
def sleep(self) -> None:
"""Enter sleep mode."""
self.operation_mode = SLEEP_MODE
def listen(self) -> None:
"""Listen for packets to be received by the chip. Use :py:func:`receive`
to listen, wait and retrieve packets as they're available.
"""
self.operation_mode = RX_MODE
self.dio0_mapping = 0b00 # Interrupt on rx done.
def transmit(self) -> None:
"""Transmit a packet which is queued in the FIFO. This is a low level
function for entering transmit mode and more. For generating and
transmitting a packet of data use :py:func:`send` instead.
"""
self.operation_mode = TX_MODE
self.dio0_mapping = 0b01 # Interrupt on tx done.
@property
def preamble_length(self) -> int:
"""The length of the preamble for sent and received packets, an unsigned
16-bit value. Received packets must match this length or they are
ignored! Set to 8 to match the RadioHead RFM95 library.
"""
msb = self.read_u8(_RF95_REG_20_PREAMBLE_MSB)
lsb = self.read_u8(_RF95_REG_21_PREAMBLE_LSB)
return ((msb << 8) | lsb) & 0xFFFF
@preamble_length.setter
def preamble_length(self, val: int) -> None:
assert 0 <= val <= 65535
self.write_u8(_RF95_REG_20_PREAMBLE_MSB, (val >> 8) & 0xFF)
self.write_u8(_RF95_REG_21_PREAMBLE_LSB, val & 0xFF)
@property
def frequency_mhz(self) -> Literal[433.0, 915.0]:
"""The frequency of the radio in Megahertz. Only the allowed values for
your radio must be specified (i.e. 433 vs. 915 mhz)!
"""
msb = self.read_u8(_RF95_REG_06_FRF_MSB)
mid = self.read_u8(_RF95_REG_07_FRF_MID)
lsb = self.read_u8(_RF95_REG_08_FRF_LSB)
frf = ((msb << 16) | (mid << 8) | lsb) & 0xFFFFFF
frequency = (frf * _RF95_FSTEP) / 1000000.0
return frequency
@frequency_mhz.setter
def frequency_mhz(self, val: Literal[433.0, 915.0]) -> None:
if val < 240 or val > 960:
raise RuntimeError("frequency_mhz must be between 240 and 960")
# Calculate FRF register 24-bit value.
frf = int((val * 1000000.0) / _RF95_FSTEP) & 0xFFFFFF
# Extract byte values and update registers.
msb = frf >> 16
mid = (frf >> 8) & 0xFF
lsb = frf & 0xFF
self.write_u8(_RF95_REG_06_FRF_MSB, msb)
self.write_u8(_RF95_REG_07_FRF_MID, mid)
self.write_u8(_RF95_REG_08_FRF_LSB, lsb)
@property
def tx_power(self) -> int:
"""The transmit power in dBm. Can be set to a value from 5 to 23 for
high power devices (RFM95/96/97/98, high_power=True) or -1 to 14 for low
power devices. Only integer power levels are actually set (i.e. 12.5
will result in a value of 12 dBm).
The actual maximum setting for high_power=True is 20dBm but for values > 20
the PA_BOOST will be enabled resulting in an additional gain of 3dBm.
The actual setting is reduced by 3dBm.
The reported value will reflect the reduced setting.
"""
if self.high_power:
return self.output_power + 5
return self.output_power - 1
@tx_power.setter
def tx_power(self, val: int) -> None:
val = int(val)
if self.high_power:
if val < 5 or val > 23:
raise RuntimeError("tx_power must be between 5 and 23")
# Enable power amp DAC if power is above 20 dB.
# Lower setting by 3db when PA_BOOST enabled - see Data Sheet Section 6.4
if val > 20:
self.pa_dac = _RF95_PA_DAC_ENABLE
val -= 3
else:
self.pa_dac = _RF95_PA_DAC_DISABLE
self.pa_select = True
self.output_power = (val - 5) & 0x0F
else:
assert -1 <= val <= 14
self.pa_select = False
self.max_power = 0b111 # Allow max power output.
self.output_power = (val + 1) & 0x0F
@property
def rssi(self) -> float:
"""The received strength indicator (in dBm) of the last received message."""
# Read RSSI register and convert to value using formula in datasheet.
# Remember in LoRa mode the payload register changes function to RSSI!
raw_rssi = self.read_u8(_RF95_REG_1A_PKT_RSSI_VALUE)
if self.low_frequency_mode:
raw_rssi -= 157
else:
raw_rssi -= 164
return float(raw_rssi)
@property
def snr(self) -> float:
"""The SNR (in dB) of the last received message."""
# Read SNR 0x19 register and convert to value using formula in datasheet.
# SNR(dB) = PacketSnr [twos complement] / 4
snr_byte = self.read_u8(_RF95_REG_19_PKT_SNR_VALUE)
if snr_byte > 127:
snr_byte = (256 - snr_byte) * -1
return snr_byte / 4
@property
def signal_bandwidth(self) -> int:
"""The signal bandwidth used by the radio (try setting to a higher
value to increase throughput or to a lower value to increase the
likelihood of successfully received payloads). Valid values are
listed in RFM9x.bw_bins."""
bw_id = (self.read_u8(_RF95_REG_1D_MODEM_CONFIG1) & 0xF0) >> 4
if bw_id >= len(self.bw_bins):
current_bandwidth = 500000
else:
current_bandwidth = self.bw_bins[bw_id]
return current_bandwidth
@signal_bandwidth.setter
def signal_bandwidth(self, val: int) -> None:
# Set signal bandwidth (set to 125000 to match RadioHead Bw125).
for bw_id, cutoff in enumerate(self.bw_bins):
if val <= cutoff:
break
else:
bw_id = 9
self.write_u8(
_RF95_REG_1D_MODEM_CONFIG1,
(self.read_u8(_RF95_REG_1D_MODEM_CONFIG1) & 0x0F) | (bw_id << 4),
)
if val >= 500000:
# see Semtech SX1276 errata note 2.3
self.auto_ifon = True
# see Semtech SX1276 errata note 2.1
if self.low_frequency_mode:
self.write_u8(0x36, 0x02)
self.write_u8(0x3A, 0x7F)
else:
self.write_u8(0x36, 0x02)
self.write_u8(0x3A, 0x64)
else:
# see Semtech SX1276 errata note 2.3
self.auto_ifon = False
self.write_u8(0x36, 0x03)
if val == 7800:
self.write_u8(0x2F, 0x48)
elif val >= 62500:
# see Semtech SX1276 errata note 2.3
self.write_u8(0x2F, 0x40)
else:
self.write_u8(0x2F, 0x44)
self.write_u8(0x30, 0)
@property
def coding_rate(self) -> Literal[5, 6, 7, 8]:
"""The coding rate used by the radio to control forward error
correction (try setting to a higher value to increase tolerance of
short bursts of interference or to a lower value to increase bit
rate). Valid values are limited to 5, 6, 7, or 8."""
cr_id = (self.read_u8(_RF95_REG_1D_MODEM_CONFIG1) & 0x0E) >> 1
denominator = cr_id + 4
return denominator
@coding_rate.setter
def coding_rate(self, val: Literal[5, 6, 7, 8]) -> None:
# Set coding rate (set to 5 to match RadioHead Cr45).
denominator = min(max(val, 5), 8)
cr_id = denominator - 4
self.write_u8(
_RF95_REG_1D_MODEM_CONFIG1,
(self.read_u8(_RF95_REG_1D_MODEM_CONFIG1) & 0xF1) | (cr_id << 1),
)
@property
def spreading_factor(self) -> Literal[6, 7, 8, 9, 10, 11, 12]:
"""The spreading factor used by the radio (try setting to a higher
value to increase the receiver's ability to distinguish signal from
noise or to a lower value to increase the data transmission rate).
Valid values are limited to 6, 7, 8, 9, 10, 11, or 12."""
sf_id = (self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) & 0xF0) >> 4
return sf_id
@spreading_factor.setter
def spreading_factor(self, val: Literal[6, 7, 8, 9, 10, 11, 12]) -> None:
# Set spreading factor (set to 7 to match RadioHead Sf128).
val = min(max(val, 6), 12)
if val == 6:
self.detection_optimize = 0x5
else:
self.detection_optimize = 0x3
self.write_u8(_RF95_DETECTION_THRESHOLD, 0x0C if val == 6 else 0x0A)
self.write_u8(
_RF95_REG_1E_MODEM_CONFIG2,
((self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) & 0x0F) | ((val << 4) & 0xF0)),
)
@property
def enable_crc(self) -> bool:
"""Set to True to enable hardware CRC checking of incoming packets.
Incoming packets that fail the CRC check are not processed. Set to
False to disable CRC checking and process all incoming packets."""
return (self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) & 0x04) == 0x04
@enable_crc.setter
def enable_crc(self, val: bool) -> None:
# Optionally enable CRC checking on incoming packets.
if val:
self.write_u8(
_RF95_REG_1E_MODEM_CONFIG2,
self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) | 0x04,
)
else:
self.write_u8(
_RF95_REG_1E_MODEM_CONFIG2,
self.read_u8(_RF95_REG_1E_MODEM_CONFIG2) & 0xFB,
)
@property
def crc_error(self) -> bool:
"""crc status"""
return (self.read_u8(_RF95_REG_12_IRQ_FLAGS) & 0x20) >> 5
def packet_sent(self) -> bool:
"""Transmit status"""
return (self.read_u8(_RF95_REG_12_IRQ_FLAGS) & 0x8) >> 3
def payload_ready(self) -> bool:
"""Receive status"""
return (self.read_u8(_RF95_REG_12_IRQ_FLAGS) & 0x40) >> 6
def clear_interrupt(self) -> None:
"""Clear Interrupt flags"""
self.write_u8(_RF95_REG_12_IRQ_FLAGS, 0xFF)
def fill_fifo(self, payload: ReadableBuffer) -> None:
"""len_data is not used but is here for compatibility with rfm69
Fill the FIFO with a packet to send"""
self.write_u8(_RF95_REG_0D_FIFO_ADDR_PTR, 0x00) # FIFO starts at 0.
# Write payload.
self.write_from(_RF95_REG_00_FIFO, payload)
# Write payload and header length.
self.write_u8(_RF95_REG_22_PAYLOAD_LENGTH, len(payload))
def read_fifo(self) -> bytearray:
"""Read the data from the FIFO."""
# Read the length of the FIFO.
fifo_length = self.read_u8(_RF95_REG_13_RX_NB_BYTES)
if fifo_length > 0: # read and clear the FIFO if anything in it
packet = bytearray(fifo_length)
current_addr = self.read_u8(_RF95_REG_10_FIFO_RX_CURRENT_ADDR)
self.write_u8(_RF95_REG_0D_FIFO_ADDR_PTR, current_addr)
# read the packet
self.read_into(_RF95_REG_00_FIFO, packet)
# clear interrupt
self.write_u8(_RF95_REG_12_IRQ_FLAGS, 0xFF)
return packet

574
adafruit_rfm/rfm9xfsk.py Normal file
View file

@ -0,0 +1,574 @@
# SPDX-FileCopyrightText: 2024 Jerry Needell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
`adafruit_rfm.rfm9xfsk`
====================================================
CircuitPython module for the RFM95/6/7/8 FSK 433/915mhz radio modules.
* Author(s): Jerry Needell
"""
import time
from micropython import const
from adafruit_rfm.rfm_common import RFMSPI
try:
from typing import Optional
import busio
import digitalio
from circuitpython_typing import ReadableBuffer
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RFM.git"
# pylint: disable=duplicate-code
# Internal constants:
# Register names (FSK Mode even though we use LoRa instead, from table 85)
_RF95_REG_00_FIFO = const(0x00)
_RF95_REG_01_OP_MODE = const(0x01)
_RF95_REG_02_BITRATE_MSB = const(0x02)
_RF95_REG_03_BITRATE_LSB = const(0x03)
_RF95_REG_04_FDEV_MSB = const(0x4)
_RF95_REG_05_FDEV_LSB = const(0x5)
_RF95_REG_06_FRF_MSB = const(0x06)
_RF95_REG_07_FRF_MID = const(0x07)
_RF95_REG_08_FRF_LSB = const(0x08)
_RF95_REG_09_PA_CONFIG = const(0x09)
_RF95_REG_0A_PA_RAMP = const(0x0A)
_RF95_REG_0B_OCP = const(0x0B)
_RF95_REG_0C_LNA = const(0x0C)
_RF95_REG_0D_RX_CFG = const(0x0D)
_RF95_REG_0E_RSSI_CFG = const(0x0E)
_RF95_REG_0F_RSSI_COLLISION = const(0x0F)
_RF95_REG_10_RSSI_THRESH = const(0x10)
_RF95_REG_11_RSSI_VALUE = const(0x11)
_RF95_REG_12_RX_BW = const(0x12)
_RF95_REG_13_AFC_BW = const(0x13)
_RF95_REG_14_OOK_PEAK = const(0x14)
_RF95_REG_15_OOK_FIX = const(0x15)
_RF95_REG_16_OOK_AVG = const(0x16)
_RF95_REG_1A_AFC_FEI_CTL = const(0x1A)
_RF95_REG_1B_AFC_MSB = const(0x1B)
_RF95_REG_1C_AFC_LSB = const(0x1C)
_RF95_REG_1D_FEI_MSB = const(0x1D)
_RF95_REG_1E_FEI_LSB = const(0x1E)
_RF95_REG_1F_PREAMBLE_DETECT = const(0x1F)
_RF95_REG_20_RX_TIMEOUT_1 = const(0x20)
_RF95_REG_21_RX_TIMEOUT_2 = const(0x21)
_RF95_REG_22_RX_TIMEOUT_3 = const(0x22)
_RF95_REG_23_RX_DELAY = const(0x23)
_RF95_REG_24_OSC = const(0x24)
_RF95_REG_25_PREAMBLE_MSB = const(0x25)
_RF95_REG_26_PREAMBLE_LSB = const(0x26)
_RF95_REG_27_SYNC_CONFIG = const(0x27)
_RF95_REG_28_SYNC_VALUE_1 = const(0x28)
_RF95_REG_29_SYNC_VALUE_2 = const(0x29)
_RF95_REG_2A_SYNC_VALUE_3 = const(0x2A)
_RF95_REG_2B_SYNC_VALUE_4 = const(0x2B)
_RF95_REG_2C_SYNC_VALUE_5 = const(0x2C)
_RF95_REG_2D_SYNC_VALUE_6 = const(0x2D)
_RF95_REG_2E_SYNC_VALUE_7 = const(0x2E)
_RF95_REG_2F_SYNC_VALUE_8 = const(0x2F)
_RF95_REG_30_PACKET_CONFIG_1 = const(0x30)
_RF95_REG_31_PACKET_CONFIG_2 = const(0x31)
_RF95_REG_32_PAYLOAD_LENGTH = const(0x32)
_RF95_REG_33_NODE_ADDR = const(0x33)
_RF95_REG_34_BROADCAST_ADDR = const(0x34)
_RF95_REG_35_FIFO_THRESH = const(0x35)
_RF95_REG_36_SEQ_CFG_1 = const(0x36)
_RF95_REG_37_SEQ_CFG_2 = const(0x37)
_RF95_REG_38_TIMER_RES = const(0x38)
_RF95_REG_39_TIMER1_COEF = const(0x39)
_RF95_REG_3A_TIMER2_COEF = const(0x3A)
_RF95_REG_3B_IMAGE_CAL = const(0x3B)
_RF95_REG_3C_TEMP = const(0x3C)
_RF95_REG_3D_LOW_BAT = const(0x3D)
_RF95_REG_3E_IRQ_FLAGS_1 = const(0x3E)
_RF95_REG_3F_IRQ_FLAGS_2 = const(0x3F)
_RF95_REG_40_DIO_MAPPING1 = const(0x40)
_RF95_REG_41_DIO_MAPPING2 = const(0x41)
_RF95_REG_42_VERSION = const(0x42)
_RF95_REG_44_PIII_IOP = const(0x44)
_RF95_REG_4B_TCXO = const(0x4B)
_RF95_REG_4D_PA_DAC = const(0x4D)
_RF95_REG_5B_FORMER_TEMP = const(0x5B)
_RF95_REG_5B_BIT_RATE_FRAC = const(0x5D)
_RF95_REG_61_AGC_REF = const(0x61)
_RF95_REG_62_AGC_THRESH1 = const(0x62)
_RF95_REG_63_AGC_THRESH2 = const(0x63)
_RF95_REG_64_AGC_THRESH3 = const(0x64)
_RF95_PA_DAC_DISABLE = const(0x04)
_RF95_PA_DAC_ENABLE = const(0x07)
# The crystal oscillator frequency of the module
_RF95_FXOSC = 32000000.0
# The Frequency Synthesizer step = RH_RF95_FXOSC / 2^^19
_RF95_FSTEP = _RF95_FXOSC / 524288
# RadioHead specific compatibility constants.
_RH_BROADCAST_ADDRESS = const(0xFF)
# The acknowledgement bit in the FLAGS
# The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved
# for application layer use.
_RH_FLAGS_ACK = const(0x80)
_RH_FLAGS_RETRY = const(0x40)
# User facing constants:
SLEEP_MODE = 0b000
STANDBY_MODE = 0b001
FS_TX_MODE = 0b010
TX_MODE = 0b011
FS_RX_MODE = 0b100
RX_MODE = 0b101
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
class RFM9xFSK(RFMSPI):
"""Interface to a RFM95/6/7/8 FSK radio module. Allows sending and
receiving bytes of data in FSK mode at a support board frequency
(433/915mhz).
:param busio.SPI spi: The SPI bus connected to the chip. Ensure SCK, MOSI, and MISO are
connected.
:param ~digitalio.DigitalInOut cs: A DigitalInOut object connected to the chip's CS/chip select
line.
:param ~digitalio.DigitalInOut reset: A DigitalInOut object connected to the chip's RST/reset
line.
:param int frequency: The center frequency to configure for radio transmission and reception.
Must be a frequency supported by your hardware (i.e. either 433 or 915mhz).
:param bytes sync_word: A byte string up to 8 bytes long which represents the syncronization
word used by received and transmitted packets. Read the datasheet for a full understanding
of this value! However by default the library will set a value that matches the RadioHead
Arduino library.
:param int preamble_length: The number of bytes to pre-pend to a data packet as a preamble.
This is by default 4 to match the RadioHead library.
:param bool high_power: Indicate if the chip is a high power variant that supports boosted
transmission power. The default is True as it supports the common RFM69HCW modules sold by
Adafruit.
Also note this library tries to be compatible with raw RadioHead Arduino
library communication. This means the library sets up the radio modulation
to match RadioHead's defaults.
Advanced RadioHead features like address/node specific packets
or "reliable datagram" delivery are supported however due to the
limitations noted, "reliable datagram" is still subject to missed packets.
"""
operation_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, bits=3)
low_frequency_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=3, bits=1)
modulation_type = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=5, bits=2)
modulation_shaping = RFMSPI.RegisterBits(_RF95_REG_0A_PA_RAMP, offset=5, bits=2)
# Long range/LoRa mode can only be set in sleep mode!
long_range_mode = RFMSPI.RegisterBits(_RF95_REG_01_OP_MODE, offset=7, bits=1)
sync_on = RFMSPI.RegisterBits(_RF95_REG_27_SYNC_CONFIG, offset=4, bits=1)
sync_size = RFMSPI.RegisterBits(_RF95_REG_27_SYNC_CONFIG, offset=0, bits=3)
output_power = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, bits=4)
max_power = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, offset=4, bits=3)
pa_select = RFMSPI.RegisterBits(_RF95_REG_09_PA_CONFIG, offset=7, bits=1)
pa_dac = RFMSPI.RegisterBits(_RF95_REG_4D_PA_DAC, bits=3)
dio0_mapping = RFMSPI.RegisterBits(_RF95_REG_40_DIO_MAPPING1, offset=6, bits=2)
lna_boost_hf = RFMSPI.RegisterBits(_RF95_REG_0C_LNA, offset=0, bits=2)
rx_bw_mantissa = RFMSPI.RegisterBits(_RF95_REG_12_RX_BW, offset=3, bits=2)
rx_bw_exponent = RFMSPI.RegisterBits(_RF95_REG_12_RX_BW, offset=0, bits=3)
afc_bw_mantissa = RFMSPI.RegisterBits(_RF95_REG_13_AFC_BW, offset=3, bits=2)
afc_bw_exponent = RFMSPI.RegisterBits(_RF95_REG_13_AFC_BW, offset=0, bits=3)
packet_format = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=7, bits=1)
dc_free = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=5, bits=2)
crc_on = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=4, bits=1)
crc_auto_clear_off = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=3, bits=1)
address_filter = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=1, bits=2)
crc_type = RFMSPI.RegisterBits(_RF95_REG_30_PACKET_CONFIG_1, offset=0, bits=1)
mode_ready = RFMSPI.RegisterBits(_RF95_REG_3E_IRQ_FLAGS_1, offset=7)
ook_bit_sync_on = RFMSPI.RegisterBits(_RF95_REG_14_OOK_PEAK, offset=5, bits=1)
ook_thresh_type = RFMSPI.RegisterBits(_RF95_REG_14_OOK_PEAK, offset=4, bits=2)
ook_thresh_step = RFMSPI.RegisterBits(_RF95_REG_14_OOK_PEAK, offset=0, bits=3)
ook_peak_thresh_dec = RFMSPI.RegisterBits(_RF95_REG_16_OOK_AVG, offset=5, bits=3)
ook_average_offset = RFMSPI.RegisterBits(_RF95_REG_16_OOK_AVG, offset=2, bits=2)
ook_average_thresh_filt = RFMSPI.RegisterBits(_RF95_REG_16_OOK_AVG, offset=0, bits=2)
def __init__( # noqa: PLR0913
self,
spi: busio.SPI,
cs: digitalio.DigitalInOut, # pylint: disable=invalid-name
rst: digitalio.DigitalInOut,
frequency: int,
*,
sync_word: bytes = b"\x2d\xd4",
preamble_length: int = 4,
high_power: bool = True,
baudrate: int = 5000000,
crc: bool = True,
) -> None:
super().__init__(spi, cs, baudrate=baudrate)
self.module = "RFM9X"
self.max_packet_length = 252
self.high_power = high_power
# Device support SPI mode 0 (polarity & phase = 0) up to a max of 10mhz.
# Set Default Baudrate to 5MHz to avoid problems
# self._device = spidev.SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=0)
# Setup reset as a digital output - initially High
# This line is pulled low as an output quickly to trigger a reset.
self._rst = rst
# initialize Reset High
self._rst.switch_to_output(value=True)
self.reset()
# No device type check! Catch an error from the very first request and
# throw a nicer message to indicate possible wiring problems.
version = self.read_u8(address=_RF95_REG_42_VERSION)
if version != 18:
raise RuntimeError(
"Failed to find rfm9x with expected version -- check wiring. Version found:",
hex(version),
)
# Set sleep mode, wait 10s and confirm in sleep mode (basic device check).
# Also set long range mode (LoRa mode) as it can only be done in sleep.
self.sleep()
time.sleep(0.01)
self.long_range_mode = False
if self.operation_mode != SLEEP_MODE or self.long_range_mode:
raise RuntimeError("Failed to configure radio for FSK mode, check wiring!")
# clear default setting for access to LF registers if frequency > 525MHz
if frequency > 525:
self.low_frequency_mode = 0
# Set mode idle
self.idle()
# Setup the chip in a similar way to the RadioHead RFM69 library.
# Set FIFO TX condition to not empty and the default FIFO threshold to 15.
self.write_u8(_RF95_REG_35_FIFO_THRESH, 0b10001111)
# Set the syncronization word.
self.sync_word = sync_word
self.preamble_length = preamble_length # Set the preamble length.
self.frequency_mhz = frequency # Set frequency.
# Configure modulation for RadioHead library GFSK_Rb250Fd250 mode
# by default. Users with advanced knowledge can manually reconfigure
# for any other mode (consulting the datasheet is absolutely
# necessary!).
self.modulation_shaping = 0b01 # Gaussian filter, BT=1.0
self.bitrate = 250000 # 250kbs
self.frequency_deviation = 250000 # 250khz
self.rx_bw_mantissa = 0b00
self.rx_bw_exponent = 0b000
self.afc_bw_mantissa = 0b00
self.afc_bw_exponent = 0b000
self.packet_format = 1 # Variable length.
self.dc_free = 0b10 # Whitening
# Set transmit power to 13 dBm, a safe value any module supports.
self._tx_power = 13
self.tx_power = self._tx_power
# Default to enable CRC checking on incoming packets.
self.enable_crc = crc
"""CRC Enable state"""
self.snr = None
def reset(self) -> None:
"""Perform a reset of the chip."""
# See section 7.2.2 of the datasheet for reset description.
self._rst.value = False # Set Reset Low
time.sleep(0.0001) # 100 us
self._rst.value = True # set Reset High
time.sleep(0.005) # 5 ms
def idle(self) -> None:
"""Enter idle standby mode."""
self.operation_mode = STANDBY_MODE
def sleep(self) -> None:
"""Enter sleep mode."""
self.operation_mode = SLEEP_MODE
def listen(self) -> None:
"""Listen for packets to be received by the chip. Use :py:func:`receive`
to listen, wait and retrieve packets as they're available.
"""
self.operation_mode = RX_MODE
self.dio0_mapping = 0b00 # Interrupt on rx done.
def transmit(self) -> None:
"""Transmit a packet which is queued in the FIFO. This is a low level
function for entering transmit mode and more. For generating and
transmitting a packet of data use :py:func:`send` instead.
"""
self.operation_mode = TX_MODE
self.dio0_mapping = 0b00 # Interrupt on tx done.
@property
def sync_word(self) -> bytearray:
"""The synchronization word value. This is a byte string up to 8 bytes long (64 bits)
which indicates the synchronization word for transmitted and received packets. Any
received packet which does not include this sync word will be ignored. The default value
is 0x2D, 0xD4 which matches the RadioHead RFM69 library. Setting a value of None will
disable synchronization word matching entirely.
"""
# Handle when sync word is disabled..
if not self.sync_on:
return None
# Sync word is not disabled so read the current value.
sync_word_length = self.sync_size + 1 # Sync word size is offset by 1
# according to datasheet.
sync_word = bytearray(sync_word_length)
self.read_into(_RF95_REG_28_SYNC_VALUE_1, sync_word)
return sync_word
@sync_word.setter
def sync_word(self, val: Optional[bytearray]) -> None:
# Handle disabling sync word when None value is set.
if val is None:
self.sync_on = 0
else:
# Check sync word is at most 8 bytes.
assert 1 <= len(val) <= 8
# Update the value, size and turn on the sync word.
self.write_from(_RF95_REG_28_SYNC_VALUE_1, val)
self.sync_size = len(val) - 1 # Again sync word size is offset by
# 1 according to datasheet.
self.sync_on = 1
@property
def bitrate(self) -> float:
"""The modulation bitrate in bits/second (or chip rate if Manchester encoding is enabled).
Can be a value from ~489 to 32mbit/s, but see the datasheet for the exact supported
values.
"""
msb = self.read_u8(_RF95_REG_02_BITRATE_MSB)
lsb = self.read_u8(_RF95_REG_03_BITRATE_LSB)
return _RF95_FXOSC / ((msb << 8) | lsb)
@bitrate.setter
def bitrate(self, val: float) -> None:
assert (_RF95_FXOSC / 65535) <= val <= 32000000.0
# Round up to the next closest bit-rate value with addition of 0.5.
bitrate = int((_RF95_FXOSC / val) + 0.5) & 0xFFFF
self.write_u8(_RF95_REG_02_BITRATE_MSB, bitrate >> 8)
self.write_u8(_RF95_REG_03_BITRATE_LSB, bitrate & 0xFF)
@property
def frequency_deviation(self) -> float:
"""The frequency deviation in Hertz."""
msb = self.read_u8(_RF95_REG_04_FDEV_MSB)
lsb = self.read_u8(_RF95_REG_05_FDEV_LSB)
return _RF95_FSTEP * ((msb << 8) | lsb)
@frequency_deviation.setter
def frequency_deviation(self, val: float) -> None:
assert 0 <= val <= (_RF95_FSTEP * 16383) # fdev is a 14-bit unsigned value
# Round up to the next closest integer value with addition of 0.5.
fdev = int((val / _RF95_FSTEP) + 0.5) & 0x3FFF
self.write_u8(_RF95_REG_04_FDEV_MSB, fdev >> 8)
self.write_u8(_RF95_REG_05_FDEV_LSB, fdev & 0xFF)
@property
def temperature(self) -> float:
"""The internal temperature of the chip.. See Sec 5.5.7 of the DataSheet
calibrated or very accurate.
"""
temp = self.read_u8(_RF95_REG_3C_TEMP)
return temp
@property
def preamble_length(self) -> int:
"""The length of the preamble for sent and received packets, an unsigned
16-bit value. Received packets must match this length or they are
ignored! Set to 4 to match the RF69.
"""
msb = self.read_u8(_RF95_REG_25_PREAMBLE_MSB)
lsb = self.read_u8(_RF95_REG_26_PREAMBLE_LSB)
return ((msb << 8) | lsb) & 0xFFFF
@preamble_length.setter
def preamble_length(self, val: int) -> None:
assert 0 <= val <= 65535
self.write_u8(_RF95_REG_25_PREAMBLE_MSB, (val >> 8) & 0xFF)
self.write_u8(_RF95_REG_26_PREAMBLE_LSB, val & 0xFF)
@property
def frequency_mhz(self) -> Literal[433.0, 915.0]:
"""The frequency of the radio in Megahertz. Only the allowed values for
your radio must be specified (i.e. 433 vs. 915 mhz)!
"""
msb = self.read_u8(_RF95_REG_06_FRF_MSB)
mid = self.read_u8(_RF95_REG_07_FRF_MID)
lsb = self.read_u8(_RF95_REG_08_FRF_LSB)
frf = ((msb << 16) | (mid << 8) | lsb) & 0xFFFFFF
frequency = (frf * _RF95_FSTEP) / 1000000.0
return frequency
@frequency_mhz.setter
def frequency_mhz(self, val: Literal[433.0, 915.0]) -> None:
if val < 240 or val > 960:
raise RuntimeError("frequency_mhz must be between 240 and 960")
# Calculate FRF register 24-bit value.
frf = int((val * 1000000.0) / _RF95_FSTEP) & 0xFFFFFF
# Extract byte values and update registers.
msb = frf >> 16
mid = (frf >> 8) & 0xFF
lsb = frf & 0xFF
self.write_u8(_RF95_REG_06_FRF_MSB, msb)
self.write_u8(_RF95_REG_07_FRF_MID, mid)
self.write_u8(_RF95_REG_08_FRF_LSB, lsb)
@property
def tx_power(self) -> int:
"""The transmit power in dBm. Can be set to a value from 5 to 23 for
high power devices (RFM95/96/97/98, high_power=True) or -1 to 14 for low
power devices. Only integer power levels are actually set (i.e. 12.5
will result in a value of 12 dBm).
The actual maximum setting for high_power=True is 20dBm but for values > 20
the PA_BOOST will be enabled resulting in an additional gain of 3dBm.
The actual setting is reduced by 3dBm.
The reported value will reflect the reduced setting.
"""
if self.high_power:
return self.output_power + 5
return self.output_power - 1
@tx_power.setter
def tx_power(self, val: int) -> None:
val = int(val)
if self.high_power:
if val < 5 or val > 23:
raise RuntimeError("tx_power must be between 5 and 23")
# Enable power amp DAC if power is above 20 dB.
# Lower setting by 3db when PA_BOOST enabled - see Data Sheet Section 6.4
if val > 20:
self.pa_dac = _RF95_PA_DAC_ENABLE
val -= 3
else:
self.pa_dac = _RF95_PA_DAC_DISABLE
self.pa_select = True
self.output_power = (val - 5) & 0x0F
else:
assert -1 <= val <= 14
self.pa_select = False
self.max_power = 0b111 # Allow max power output.
self.output_power = (val + 1) & 0x0F
@property
def rssi(self) -> float:
"""The received strength indicator (in dBm) of the last received message."""
# Read RSSI register and convert to value using formula in datasheet.
# Remember in LoRa mode the payload register changes function to RSSI!
raw_rssi = self.read_u8(_RF95_REG_11_RSSI_VALUE)
return -raw_rssi / 2.0
@property
def enable_crc(self) -> bool:
"""Set to True to enable hardware CRC checking of incoming packets.
Incoming packets that fail the CRC check are not processed. Set to
False to disable CRC checking and process all incoming packets."""
return self.crc_on
@enable_crc.setter
def enable_crc(self, val: bool) -> None:
# Optionally enable CRC checking on incoming packets.
if val:
self.crc_on = 1
self.crc_type = 0 # use CCITT for RF69 compatibility
else:
self.crc_on = 0
@property
def crc_error(self) -> bool:
"""crc status"""
return (self.read_u8(_RF95_REG_3F_IRQ_FLAGS_2) & 0x2) >> 1
@property
def enable_address_filter(self) -> bool:
"""Set to True to enable address filtering.
Incoming packets that do no match the node address or broadcast address
will be ignored."""
return self.address_filter
@enable_address_filter.setter
def enable_address_filter(self, val: bool) -> None:
# Enable address filtering on incoming packets.
if val:
self.address_filter = 2 # accept node address or broadcast address
else:
self.address_filter = 0
@property
def fsk_node_address(self) -> int:
"""Node Address for Address Filtering"""
return self.read_u8(_RF95_REG_33_NODE_ADDR)
@fsk_node_address.setter
def fsk_node_address(self, val: int) -> None:
assert 0 <= val <= 255
self.write_u8(_RF95_REG_33_NODE_ADDR, val)
@property
def fsk_broadcast_address(self) -> int:
"""Node Address for Address Filtering"""
return self.read_u8(_RF95_REG_34_BROADCAST_ADDR)
@fsk_broadcast_address.setter
def fsk_broadcast_address(self, val: int) -> None:
assert 0 <= val <= 255
self.write_u8(_RF95_REG_34_BROADCAST_ADDR, val)
@property
def ook_fixed_threshold(self) -> int:
"""Fixed threshold for data slicer in OOK mode"""
return self.read_u8(_RF95_REG_15_OOK_FIX)
@ook_fixed_threshold.setter
def ook_fixed_threshold(self, val: int) -> None:
assert 0 <= val <= 255
self.write_u8(_RF95_REG_15_OOK_FIX, val)
def packet_sent(self) -> bool:
"""Transmit status"""
return (self.read_u8(_RF95_REG_3F_IRQ_FLAGS_2) & 0x8) >> 3
def payload_ready(self) -> bool:
"""Receive status"""
return (self.read_u8(_RF95_REG_3F_IRQ_FLAGS_2) & 0x4) >> 2
def clear_interrupt(self) -> None:
"""Clear interrupt Flags"""
self.write_u8(_RF95_REG_3E_IRQ_FLAGS_1, 0xFF)
self.write_u8(_RF95_REG_3F_IRQ_FLAGS_2, 0xFF)
def fill_fifo(self, payload: ReadableBuffer) -> None:
"""Write the payload to the FIFO."""
complete_payload = bytearray(1) # prepend packet length to payload
complete_payload[0] = len(payload)
# put the payload lengthe in the beginning of the packet for RFM69
complete_payload = complete_payload + payload
# Write payload to transmit fifo
self.write_from(_RF95_REG_00_FIFO, complete_payload)
def read_fifo(self) -> bytearray:
"""Read the data from the FIFO."""
# Read the length of the FIFO.
fifo_length = self.read_u8(_RF95_REG_00_FIFO)
if fifo_length > 0: # read and clear the FIFO if anything in it
packet = bytearray(fifo_length)
# read the packet
self.read_into(_RF95_REG_00_FIFO, packet, fifo_length)
return packet

557
adafruit_rfm/rfm_common.py Normal file
View file

@ -0,0 +1,557 @@
# SPDX-FileCopyrightText: 2024 Jerry Needell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
* Author(s): Jerry Needell
"""
import asyncio
import random
import time
from adafruit_bus_device import spi_device
try:
from typing import Callable, Optional, Type
import busio
import digitalio
from circuitpython_typing import ReadableBuffer, WriteableBuffer
except ImportError:
pass
from micropython import const
HAS_SUPERVISOR = False
try:
import supervisor
if hasattr(supervisor, "ticks_ms"):
HAS_SUPERVISOR = True
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RFM.git"
# RadioHead specific compatibility constants.
_RH_BROADCAST_ADDRESS = const(0xFF)
# The acknowledgement bit in the FLAGS
# The top 4 bits of the flags are reserved for RadioHead. The lower 4 bits are reserved
# for application layer use.
_RH_FLAGS_ACK = const(0x80)
_RH_FLAGS_RETRY = const(0x40)
# supervisor.ticks_ms() contants
_TICKS_PERIOD = const(1 << 29)
_TICKS_MAX = const(_TICKS_PERIOD - 1)
_TICKS_HALFPERIOD = const(_TICKS_PERIOD // 2)
def ticks_diff(ticks1: int, ticks2: int) -> int:
"""Compute the signed difference between two ticks values
assuming that they are within 2**28 ticks
"""
diff = (ticks1 - ticks2) & _TICKS_MAX
diff = ((diff + _TICKS_HALFPERIOD) & _TICKS_MAX) - _TICKS_HALFPERIOD
return diff
def asyncio_to_blocking(function):
"""run async function as normal blocking function"""
def blocking_function(self, *args, **kwargs):
return asyncio.run(function(self, *args, **kwargs))
return blocking_function
async def asyncio_check_timeout(flag: Callable, limit: float, timeout_poll: float) -> bool:
"""test for timeout waiting for specified flag"""
timed_out = False
if HAS_SUPERVISOR:
start = supervisor.ticks_ms()
while not timed_out and not flag():
if ticks_diff(supervisor.ticks_ms(), start) >= limit * 1000:
timed_out = True
await asyncio.sleep(timeout_poll)
else:
start = time.monotonic()
while not timed_out and not flag():
if time.monotonic() - start >= limit:
timed_out = True
await asyncio.sleep(timeout_poll)
return timed_out
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-nested-blocks
class RFMSPI:
"""Base class for SPI type devices"""
class RegisterBits:
"""Simplify register access"""
# Class to simplify access to the many configuration bits avaialable
# on the chip's registers. This is a subclass here instead of using
# a higher level module to increase the efficiency of memory usage
# (all of the instances of this bit class will share the same buffer
# used by the parent RFM69 class instance vs. each having their own
# buffer and taking too much memory).
# Quirk of pylint that it requires public methods for a class. This
# is a decorator class in Python and by design it has no public methods.
# Instead it uses dunder accessors like get and set below. For some
# reason pylint can't figure this out so disable the check.
# pylint: disable=too-few-public-methods
# Again pylint fails to see the true intent of this code and warns
# against private access by calling the write and read functions below.
# This is by design as this is an internally used class. Disable the
# check from pylint.
# pylint: disable=protected-access
def __init__(self, address: int, *, offset: int = 0, bits: int = 1) -> None:
assert 0 <= offset <= 7
assert 1 <= bits <= 8
assert (offset + bits) <= 8
self._address = address
self._mask = 0
for _ in range(bits):
self._mask <<= 1
self._mask |= 1
self._mask <<= offset
self._offset = offset
def __get__(self, obj: Optional["RFM"], objtype: Type["RFM"]) -> int:
reg_value = obj.read_u8(self._address)
return (reg_value & self._mask) >> self._offset
def __set__(self, obj: Optional["RFM"], val: int) -> None:
reg_value = obj.read_u8(self._address)
reg_value &= ~self._mask
reg_value |= (val & 0xFF) << self._offset
obj.write_u8(self._address, reg_value)
# pylint: disable-msg=too-many-arguments
def __init__( # noqa: PLR0913
self,
spi: busio.SPI,
cs_pin: digitalio.DigitalInOut,
baudrate: int = 5000000,
polarity: int = 0,
phase: int = 0,
):
self.spi_device = spi_device.SPIDevice(
spi, cs_pin, baudrate=baudrate, polarity=polarity, phase=phase
)
# initialize last RSSI reading
self.last_rssi = 0.0
"""The RSSI of the last received packet. Stored when the packet was received.
The instantaneous RSSI value may not be accurate once the
operating mode has been changed.
"""
self.last_snr = 0.0
"""The SNR of the last received packet. Stored when the packet was received.
The instantaneous SNR value may not be accurate once the
operating mode has been changed.
"""
# initialize timeouts and delays delays
self.ack_wait = 0.1
"""The delay time before attempting a retry after not receiving an ACK"""
self.receive_timeout = 0.5
"""The amount of time to poll for a received packet.
If no packet is received, the returned packet will be None
"""
self.xmit_timeout = 2.0
"""The amount of time to wait for the HW to transmit the packet.
This is mainly used to prevent a hang due to a HW issue
"""
self.ack_retries = 5
"""The number of ACK retries before reporting a failure."""
self.ack_delay = None
"""The delay time before attemting to send an ACK.
If ACKs are being missed try setting this to .1 or .2.
"""
# initialize sequence number counter for reliabe datagram mode
self.sequence_number = 0
# create seen Ids list
self.seen_ids = bytearray(256)
# initialize packet header
# node address - default is broadcast
self.node = _RH_BROADCAST_ADDRESS
"""The default address of this Node. (0-255).
If not 255 (0xff) then only packets address to this node will be accepted.
First byte of the RadioHead header.
"""
# destination address - default is broadcast
self.destination = _RH_BROADCAST_ADDRESS
"""The default destination address for packet transmissions. (0-255).
If 255 (0xff) then any receiving node should accept the packet.
Second byte of the RadioHead header.
"""
# ID - contains seq count for reliable datagram mode
self.identifier = 0
"""Automatically set to the sequence number when send_with_ack() used.
Third byte of the RadioHead header.
"""
# flags - identifies ack/reetry packet for reliable datagram mode
self.flags = 0
"""Upper 4 bits reserved for use by Reliable Datagram Mode.
Lower 4 bits may be used to pass information.
Fourth byte of the RadioHead header.
"""
self.radiohead = True
"""Enable RadioHead compatibility"""
self.crc_error_count = 0
self.timeout_poll = 0.001
# pylint: enable-msg=too-many-arguments
# Global buffer for SPI commands
_BUFFER = bytearray(4)
# pylint: disable=no-member
# Reconsider pylint: disable when this can be tested
def read_into(self, address: int, buf: WriteableBuffer, length: Optional[int] = None) -> None:
"""Read a number of bytes from the specified address into the provided
buffer. If length is not specified (the default) the entire buffer
will be filled."""
if length is None:
length = len(buf)
with self.spi_device as device:
self._BUFFER[0] = address & 0x7F # Strip out top bit to set 0
# value (read).
device.write(self._BUFFER, end=1)
device.readinto(buf, end=length)
def read_u8(self, address: int) -> int:
"""Read a single byte from the provided address and return it."""
self.read_into(address, self._BUFFER, length=1)
return self._BUFFER[0]
def write_from(self, address: int, buf: ReadableBuffer, length: Optional[int] = None) -> None:
"""Write a number of bytes to the provided address and taken from the
provided buffer. If no length is specified (the default) the entire
buffer is written."""
if length is None:
length = len(buf)
with self.spi_device as device:
self._BUFFER[0] = (address | 0x80) & 0xFF # Set top bit to 1 to
# indicate a write.
device.write(self._BUFFER, end=1)
device.write(buf, end=length)
def write_u8(self, address: int, val: int) -> None:
"""Write a byte register to the chip. Specify the 7-bit address and the
8-bit value to write to that address."""
with self.spi_device as device:
self._BUFFER[0] = (address | 0x80) & 0xFF # Set top bit to 1 to indicate a write.
self._BUFFER[1] = val & 0xFF
device.write(self._BUFFER, end=2)
# pylint: disable=too-many-branches
async def asyncio_send( # noqa: PLR0912 PLR0913
self,
data: ReadableBuffer,
*,
keep_listening: bool = False,
destination: Optional[int] = None,
node: Optional[int] = None,
identifier: Optional[int] = None,
flags: Optional[int] = None,
) -> bool:
"""Send a string of data using the transmitter.
You can only send 252 bytes at a time
(limited by chip's FIFO size and appended headers).
if the propert radiohead is True then this appends a 4 byte header
to be compatible with the RadioHead library.
The header defaults to using the initialized attributes:
(destination,node,identifier,flags)
It may be temporarily overidden via the kwargs - destination,node,identifier,flags.
Values passed via kwargs do not alter the attribute settings.
The keep_listening argument should be set to True if you want to start listening
automatically after the packet is sent. The default setting is False.
Returns: True if success or False if the send timed out.
"""
# Disable pylint warning to not use length as a check for zero.
# This is a puzzling warning as the below code is clearly the most
# efficient and proper way to ensure a precondition that the provided
# buffer be within an expected range of bounds. Disable this check.
# pylint: disable=len-as-condition
assert 0 < len(data) <= self.max_packet_length
# pylint: enable=len-as-condition
self.idle() # Stop receiving to clear FIFO and keep it clear.
# Combine header and data to form payload
if self.radiohead:
payload = bytearray(4)
if destination is None: # use attribute
payload[0] = self.destination
else: # use kwarg
payload[0] = destination
if node is None: # use attribute
payload[1] = self.node
else: # use kwarg
payload[1] = node
if identifier is None: # use attribute
payload[2] = self.identifier
else: # use kwarg
payload[2] = identifier
if flags is None: # use attribute
payload[3] = self.flags
else: # use kwarg
payload[3] = flags
payload = payload + data
elif destination is not None: # prepend destination for non RH packets
payload = destination.to_bytes(1, "big") + data
else:
payload = data
self.fill_fifo(payload)
# Turn on transmit mode to send out the packet.
self.transmit()
# Wait for packet_sent interrupt with explicit polling (not ideal but
# best that can be done right now without interrupts).
timed_out = await asyncio_check_timeout(
self.packet_sent, self.xmit_timeout, self.timeout_poll
)
# Listen again if necessary and return the result packet.
if keep_listening:
self.listen()
else:
# Enter idle mode to stop receiving other packets.
self.idle()
self.clear_interrupt()
return not timed_out
send = asyncio_to_blocking(asyncio_send)
"""Non-asyncio wrapper to Send a string of data using the transmitter
using the same arguments and keywords as asyncio_send()
"""
async def asyncio_send_with_ack(self, data: ReadableBuffer) -> bool:
"""Reliable Datagram mode:
Send a packet with data and wait for an ACK response.
The packet header is automatically generated.
If enabled, the packet transmission will be retried on failure
"""
if not self.radiohead:
raise RuntimeError("send_with_ack onl suppoted in RadioHead mode")
if self.ack_retries:
retries_remaining = self.ack_retries
else:
retries_remaining = 1
got_ack = False
self.sequence_number = (self.sequence_number + 1) & 0xFF
while not got_ack and retries_remaining:
self.identifier = self.sequence_number
await self.asyncio_send(data, keep_listening=True)
# Don't look for ACK from Broadcast message
if self.destination == _RH_BROADCAST_ADDRESS:
got_ack = True
else:
# wait for a packet from our destination
ack_packet = await self.asyncio_receive(timeout=self.ack_wait, with_header=True)
if ack_packet is not None:
if ack_packet[3] & _RH_FLAGS_ACK:
# check the ID
if ack_packet[2] == self.identifier:
got_ack = True
break
# pause before next retry -- random delay
if not got_ack:
# delay by random amount before next try
await asyncio.sleep(self.ack_wait + self.ack_wait * random.random())
retries_remaining = retries_remaining - 1
# set retry flag in packet header
self.flags |= _RH_FLAGS_RETRY
self.flags = 0 # clear flags
return got_ack
send_with_ack = asyncio_to_blocking(asyncio_send_with_ack)
"""Non-asyncio wrapper to Send a string of data using the transmitter
using the same arguments and keywords as asyncio_send_with_ack()
"""
async def asyncio_receive( # noqa: PLR0912
self,
*,
keep_listening: bool = True,
with_header: bool = False,
timeout: Optional[float] = None,
) -> Optional[bytearray]:
"""Wait to receive a packet from the receiver. If a packet is found the payload bytes
are returned, otherwise None is returned (which indicates the timeout elapsed with no
reception).
If keep_listening is True (the default) the chip will immediately enter listening mode
after reception of a packet, otherwise it will fall back to idle mode and ignore any
future reception.
Packets may have a 4-byte header for compatibility with the
RadioHead library.
The header consists of 4 bytes (To,From,ID,Flags). The default setting will strip
the header before returning the packet to the caller.
If with_header is True then the 4 byte header will be returned with the packet.
The payload then begins at packet[4].
"""
if not self.radiohead and with_header:
raise RuntimeError("with_header only supported for RadioHead mode")
timed_out = False
if timeout is None:
timeout = self.receive_timeout
if timeout is not None:
# Wait for the payloadready signal. This is not ideal and will
# surely miss or overflow the FIFO when packets aren't read fast
# enough, however it's the best that can be done from Python without
# interrupt supports.
# Make sure we are listening for packets.
self.listen()
timed_out = await asyncio_check_timeout(self.payload_ready, timeout, self.timeout_poll)
# Payload ready is set, a packet is in the FIFO.
packet = None
# save last RSSI reading
self.last_rssi = self.rssi
self.last_snr = self.snr
# Enter idle mode to stop receiving other packets.
self.idle()
if not timed_out:
if self.enable_crc and self.crc_error:
self.crc_error_count += 1
else:
packet = self.read_fifo()
if self.radiohead:
if len(packet) < 5:
# reject the packet if it is too small to contain the RAdioHead Header
packet = None
if packet is not None:
if (
self.node != _RH_BROADCAST_ADDRESS # noqa: PLR1714
and packet[0] != _RH_BROADCAST_ADDRESS
and packet[0] != self.node
):
packet = None
if not with_header and packet is not None: # skip the header if not wanted
packet = packet[4:]
# Listen again if necessary and return the result packet.
if keep_listening:
self.listen()
else:
# Enter idle mode to stop receiving other packets.
self.idle()
self.clear_interrupt()
return packet
receive = asyncio_to_blocking(asyncio_receive)
"""Non-asyncio wrapper to Receive a packet
using the same arguments and keywords as asyncio_receive()
"""
async def asyncio_receive_with_ack( # noqa: PLR0912
self,
*,
keep_listening: bool = True,
with_header: bool = False,
timeout: Optional[float] = None,
) -> Optional[bytearray]:
"""Wait to receive a RadioHead packet from the receiver then send an ACK packet in response.
AKA Reliable Datagram mode.
If a packet is found the payload bytes are returned, otherwise None is returned
(which indicates the timeout elapsed with no reception).
If keep_listening is True (the default) the chip will immediately enter listening mode
after receipt of a packet, otherwise it will fall back to idle mode and ignore
any incomming packets until it is called again.
All packets must have a 4-byte header for compatibility with the RadioHead library.
The header consists of 4 bytes (To,From,ID,Flags). The default setting will strip
the header before returning the packet to the caller.
If with_header is True then the 4 byte header will be returned with the packet.
The payload then begins at packet[4].
"""
if not self.radiohead:
raise RuntimeError("receive_with_ack only supported for RadioHead mode")
timed_out = False
if timeout is None:
timeout = self.receive_timeout
if timeout is not None:
# Wait for the payloadready signal. This is not ideal and will
# surely miss or overflow the FIFO when packets aren't read fast
# enough, however it's the best that can be done from Python without
# interrupt supports.
# Make sure we are listening for packets.
self.listen()
timed_out = await asyncio_check_timeout(self.payload_ready, timeout, self.timeout_poll)
# Payload ready is set, a packet is in the FIFO.
packet = None
# save last RSSI reading
self.last_rssi = self.rssi
self.last_snr = self.snr
# Enter idle mode to stop receiving other packets.
self.idle()
if not timed_out:
if self.enable_crc and self.crc_error:
self.crc_error_count += 1
else:
packet = self.read_fifo()
if self.radiohead:
if len(packet) < 5:
# reject the packet if it is too small to contain the RAdioHead Header
packet = None
if packet is not None:
if (
self.node != _RH_BROADCAST_ADDRESS # noqa: PLR1714
and packet[0] != _RH_BROADCAST_ADDRESS
and packet[0] != self.node
):
packet = None
# send ACK unless this was an ACK or a broadcast
elif ((packet[3] & _RH_FLAGS_ACK) == 0) and (
packet[0] != _RH_BROADCAST_ADDRESS
):
# delay before sending Ack to give receiver a chance to get ready
if self.ack_delay is not None:
await asyncio.sleep(self.ack_delay)
# send ACK packet to sender (data is b'!')
await self.asyncio_send(
b"!",
destination=packet[1],
node=packet[0],
identifier=packet[2],
flags=(packet[3] | _RH_FLAGS_ACK),
)
# reject Retries if we have seen this idetifier from this source before
if (self.seen_ids[packet[1]] == packet[2]) and (
packet[3] & _RH_FLAGS_RETRY
):
packet = None
else: # save the packet identifier for this source
self.seen_ids[packet[1]] = packet[2]
if (
packet is not None and (packet[3] & _RH_FLAGS_ACK) != 0
): # Ignore it if it was an ACK packet
packet = None
if not with_header and packet is not None: # skip the header if not wanted
packet = packet[4:]
# Listen again if necessary and return the result packet.
if keep_listening:
self.listen()
else:
# Enter idle mode to stop receiving other packets.
self.idle()
self.clear_interrupt()
return packet
receive_with_ack = asyncio_to_blocking(asyncio_receive_with_ack)
"""Non-asyncio wrapper to Receive a packet
using the same arguments and keywords as asyncio_receive_with_ack()
"""

BIN
docs/_static/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

3
docs/_static/favicon.ico.license vendored Normal file
View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries
SPDX-License-Identifier: CC-BY-4.0

17
docs/api.rst Normal file
View file

@ -0,0 +1,17 @@
.. If you created a package, create one automodule per module in the package.
.. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py)
.. use this format as the module name: "adafruit_foo.foo"
.. automodule:: adafruit_rfm.rfm_common
:members:
.. automodule:: adafruit_rfm.rfm69
:members:
.. automodule:: adafruit_rfm.rfm9x
:members:
.. automodule:: adafruit_rfm.rfm9xfsk
:members:

4
docs/api.rst.license Normal file
View file

@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
SPDX-FileCopyrightText: Copyright (c) 2024 Jerry Needell for Adafruit Industries
SPDX-License-Identifier: MIT

186
docs/conf.py Normal file
View file

@ -0,0 +1,186 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import datetime
import os
import sys
sys.path.insert(0, os.path.abspath(".."))
# -- General configuration ------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinxcontrib.jquery",
"sphinx.ext.intersphinx",
"sphinx.ext.napoleon",
"sphinx.ext.todo",
]
# TODO: Please Read!
# Uncomment the below if you use native CircuitPython modules such as
# digitalio, micropython and busio. List the modules you use. Without it, the
# autodoc module docs will fail to generate with a warning.
# autodoc_mock_imports = ["digitalio", "busio"]
autodoc_preserve_defaults = True
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"BusDevice": ("https://docs.circuitpython.org/projects/busdevice/en/latest/", None),
"CircuitPython": ("https://docs.circuitpython.org/en/latest/", None),
}
# Show the docstring from both the class and its __init__() method.
autoclass_content = "both"
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# General information about the project.
project = "Adafruit CircuitPython RFM Library"
creation_year = "2024"
current_year = str(datetime.datetime.now().year)
year_duration = (
current_year if current_year == creation_year else creation_year + " - " + current_year
)
copyright = year_duration + " Jerry Needell"
author = "Jerry Needell"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = "1.0"
# The full version, including alpha/beta/rc tags.
release = "1.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = [
"_build",
"Thumbs.db",
".DS_Store",
".env",
"CODE_OF_CONDUCT.md",
]
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
default_role = "any"
# If true, '()' will be appended to :func: etc. cross-reference text.
#
add_function_parentheses = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# If this is True, todo emits a warning for each TODO entries. The default is False.
todo_emit_warnings = True
napoleon_numpy_docstring = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
html_favicon = "_static/favicon.ico"
# Output file base name for HTML help builder.
htmlhelp_basename = "Adafruit_CircuitPython_Rfm_Librarydoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
# Latex figure (float) alignment
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(
master_doc,
"Adafruit_CircuitPython_RFM_Library.tex",
"Adafruit CircuitPython RFM Library Documentation",
author,
"manual",
),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(
master_doc,
"Adafruit_CircuitPython_RFM_Library",
"Adafruit CircuitPython RFM Library Documentation",
[author],
1,
),
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"Adafruit_CircuitPython_RFM_Library",
"Adafruit CircuitPython RFM Library Documentation",
author,
"Adafruit_CircuitPython_RFM_Library",
"One line description of project.",
"Miscellaneous",
),
]

8
docs/examples.rst Normal file
View file

@ -0,0 +1,8 @@
Simple test
------------
Ensure your device works with this simple test.
.. literalinclude:: ../examples/rfm_simpletest.py
:caption: examples/rfm_simpletest.py
:linenos:

View file

@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
SPDX-FileCopyrightText: Copyright (c) 2024 Jerry Needell for Adafruit Industries
SPDX-License-Identifier: MIT

47
docs/index.rst Normal file
View file

@ -0,0 +1,47 @@
.. include:: ../README.rst
Table of Contents
=================
.. toctree::
:maxdepth: 4
:hidden:
self
.. toctree::
:caption: Examples
examples
.. toctree::
:caption: API Reference
:maxdepth: 3
api
.. toctree::
:caption: Tutorials
.. toctree::
:caption: Related Products
.. toctree::
:caption: Other Links
Download from GitHub <https://github.com/jerryneedell/Adafruit_CircuitPython_RFM/releases/latest>
Download Library Bundle <https://circuitpython.org/libraries>
CircuitPython Reference Documentation <https://docs.circuitpython.org>
CircuitPython Support Forum <https://forums.adafruit.com/viewforum.php?f=60>
Discord Chat <https://adafru.it/discord>
Adafruit Learning System <https://learn.adafruit.com>
Adafruit Blog <https://blog.adafruit.com>
Adafruit Store <https://www.adafruit.com>
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

4
docs/index.rst.license Normal file
View file

@ -0,0 +1,4 @@
SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
SPDX-FileCopyrightText: Copyright (c) 2024 Jerry Needell for Adafruit Industries
SPDX-License-Identifier: MIT

7
docs/requirements.txt Normal file
View file

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
sphinx
sphinxcontrib-jquery
sphinx-rtd-theme

93
examples/rfm_fsk_node1.py Normal file
View file

@ -0,0 +1,93 @@
# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries
# SPDX-License-Identifier: MIT
# Example to send a packet periodically
import time
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
# from adafruit_rfm import rfm9x
# rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
from adafruit_rfm import rfm9xfsk
rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# Disable the RadioHead Header
rfm.radiohead = False
# set the time interval (seconds) for sending packets
transmit_interval = 5
node = 1
destination = 2
rfm.enable_address_filter = True
rfm.fsk_node_address = node
rfm.fsk_broadcast_address = 0xFF
# For RFM69 only:
# Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# initialize counter
counter = 0
# send a broadcast mesage
rfm.send(bytes(f"message number {counter}", "UTF-8"), destination=destination)
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
time_now = time.monotonic()
while True:
# Look for a new packet - wait up to 5 seconds:
packet = rfm.receive(timeout=2.0)
# If no packet was received during the timeout then None is returned.
if packet is not None:
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
# send reading after any packet received
if time.monotonic() - time_now > transmit_interval:
# reset timeer
time_now = time.monotonic()
counter = counter + 1
rfm.send(bytes(f"message number {counter}", "UTF-8"), destination=destination)

93
examples/rfm_fsk_node2.py Normal file
View file

@ -0,0 +1,93 @@
# SPDX-FileCopyrightText: 2020 Jerry Needell for Adafruit Industries
# SPDX-License-Identifier: MIT
# Example to send a packet periodically
import time
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
# from adafruit_rfm import rfm9x
# rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
from adafruit_rfm import rfm9xfsk
rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# Disable the RadioHead Header
rfm.radiohead = False
# set the time interval (seconds) for sending packets
transmit_interval = 5
node = 2
destination = 1
rfm.enable_address_filter = True
rfm.fsk_node_address = node
rfm.fsk_broadcast_address = 0xFF
# For RFM69 only:
# Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# initialize counter
counter = 0
# send a broadcast mesage
rfm.send(bytes(f"message number {counter}", "UTF-8"), destination=destination)
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
time_now = time.monotonic()
while True:
# Look for a new packet - wait up to 5 seconds:
packet = rfm.receive(timeout=2.0)
# If no packet was received during the timeout then None is returned.
if packet is not None:
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
# send reading after any packet received
if time.monotonic() - time_now > transmit_interval:
# reset timeer
time_now = time.monotonic()
counter = counter + 1
rfm.send(bytes(f"message number {counter}", "UTF-8"), destination=destination)

92
examples/rfm_raw.py Normal file
View file

@ -0,0 +1,92 @@
# SPDX-FileCopyrightText: 2024 Ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# Simple demo of sending and recieving data with the RFM9x or RFM69 radios.
# Author: Jerry Needell
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
# from adafruit_rfm import rfm9x
# rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
from adafruit_rfm import rfm9xfsk
rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# Disable the RadioHead Header
rfm.radiohead = False
# Send a packet. Note you can only send a packet containing up to 60 bytes for an RFM69
# and 252 bytes forn an RFM9x.
# This is a limitation of the radio packet size, so if you need to send larger
# amounts of data you will need to break it into smaller send calls. Each send
# call will wait for the previous one to finish before continuing.
rfm.send(bytes("Hello world!\r\n", "utf-8"))
print("Sent Hello World message!")
# Wait to receive packets.
print("Waiting for packets...")
while True:
packet = rfm.receive()
# Optionally change the receive timeout from its default of 0.5 seconds:
# packet = rfm9x.receive(timeout=5.0)
# If no packet was received during the timeout then None is returned.
if packet is None:
# Packet has not been received
print("Received nothing! Listening again...")
else:
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
print("Hex data: ", [hex(x) for x in packet])
# And decode to ASCII text and print it too. Note that you always
# receive raw bytes and need to convert to a text format like ASCII
# if you intend to do string processing on your data. Make sure the
# sending side is sending ASCII data before you try to decode!
try:
packet_text = str(packet, "ascii")
print(f"Received (ASCII): {packet_text}")
except UnicodeError:
pass
# Also read the RSSI (signal strength) of the last received message and
# print it.
rssi = rfm.last_rssi
print(f"Received signal strength: {rssi} dB")

View file

@ -0,0 +1,95 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# Example using Interrupts to send a message and then wait indefinitely for messages
# to be received. Interrupts are used only for receive. sending is done with polling.
# This example is for systems that support interrupts like the Raspberry Pi with "blinka"
# CircuitPython does not support interrupts so it will not work on Circutpython boards
# Author: Tony DiCola, Jerry Needell
import asyncio
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
from adafruit_rfm import rfm9x
rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
# from adafruit_rfm import rfm9xfsk
# rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# send startup message from my_node
# rfm.send(bytes("startup message from node {}".format(rfm.node), "UTF-8"))
rfm.listen()
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
# pylint: disable=too-few-public-methods
class Packet:
"""Simple class to hold an value. Use .value to to read or write."""
def __init__(self):
self.received = False
# setup interrupt callback function
async def wait_for_packets(packet_status):
while True:
if rfm.payload_ready():
packet = await rfm.asyncio_receive(with_header=True, timeout=None)
if packet is not None:
packet_status.received = True
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
print([hex(x) for x in packet])
print(f"RSSI: {rfm.last_rssi}")
await asyncio.sleep(0.001)
async def main():
packet_status = Packet()
task1 = asyncio.create_task(wait_for_packets(packet_status))
await asyncio.gather(task1) # Don't forget "await"!
asyncio.run(main())

View file

@ -0,0 +1,96 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# Example using Interrupts to send a message and then wait indefinitely for messages
# to be received. Interrupts are used only for receive. sending is done with polling.
# This example is for systems that support interrupts like the Raspberry Pi with "blinka"
# CircuitPython does not support interrupts so it will not work on Circutpython boards
# Author: Tony DiCola, Jerry Needell
import asyncio
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
from adafruit_rfm import rfm9x
rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
# from adafruit_rfm import rfm9xfsk
# rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# send startup message from my_node
# rfm.send(bytes("startup message from node {}".format(rfm.node), "UTF-8"))
rfm.listen()
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
# pylint: disable=too-few-public-methods
class Packet:
"""Simple class to hold an value. Use .value to to read or write."""
def __init__(self):
self.received = False
# setup interrupt callback function
async def wait_for_packets(packet_status):
while True:
if rfm.payload_ready():
packet = await rfm.asyncio_receive_with_ack(with_header=True, timeout=None)
if packet is not None:
packet_status.received = True
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
print([hex(x) for x in packet])
print(f"RSSI: {rfm.last_rssi}")
await asyncio.sleep(0.001)
async def main():
packet_status = Packet()
task1 = asyncio.create_task(wait_for_packets(packet_status))
await asyncio.gather(task1) # Don't forget "await"!
asyncio.run(main())

View file

@ -0,0 +1,129 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
import asyncio
import time
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
from adafruit_rfm import rfm9x
rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
# from adafruit_rfm import rfm9xfsk
# rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# set node addresses
rfm.node = 1
rfm.destination = 100
# send startup message from my_node
rfm.send_with_ack(bytes(f"startup message from node {rfm.node}", "UTF-8"))
rfm.listen()
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
# pylint: disable=too-few-public-methods
class Packet:
"""Simple class to hold an value. Use .value to to read or write."""
def __init__(self):
self.received = False
# setup interrupt callback function
async def wait_for_packets(packet_status, lock):
while True:
if rfm.payload_ready():
if lock.locked():
print("locked waiting for receive")
async with lock:
packet = await rfm.asyncio_receive_with_ack(with_header=True, timeout=None)
if packet is not None:
packet_status.received = True
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
print([hex(x) for x in packet])
print(f"RSSI: {rfm.last_rssi}")
await asyncio.sleep(0.001)
async def send_packets(packet_status, lock):
# initialize counter
counter = 0
ack_failed_counter = 0
counter = 0
transmit_interval = 5
time_now = time.monotonic()
while True:
# If no packet was received during the timeout then None is returned.
if packet_status.received:
packet_status.received = False
if time.monotonic() - time_now > transmit_interval:
# reset timeer
time_now = time.monotonic()
counter += 1
# send a mesage to destination_node from my_node
if lock.locked():
print("locked waiting for send")
async with lock:
if not await rfm.asyncio_send_with_ack(
bytes(
f"message from node {rfm.node} {counter} {ack_failed_counter}",
"UTF-8",
)
):
ack_failed_counter += 1
print(" No Ack: ", counter, ack_failed_counter)
await asyncio.sleep(0.1)
async def main():
packet_status = Packet()
lock = asyncio.Lock()
task1 = asyncio.create_task(wait_for_packets(packet_status, lock))
task2 = asyncio.create_task(send_packets(packet_status, lock))
await asyncio.gather(task1, task2) # Don't forget "await"!
asyncio.run(main())

71
examples/rfm_rh_base.py Normal file
View file

@ -0,0 +1,71 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
from adafruit_rfm import rfm9x
rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
# from adafruit_rfm import rfm9xfsk
# rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# set node addresses
rfm.node = 100
rfm.destination = 0xFF
# send startup message from my_node
rfm.send(
bytes(f"startup message from base {rfm.node}", "UTF-8"),
keep_listening=True,
)
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
while True:
if rfm.payload_ready():
packet = rfm.receive(with_header=True, timeout=None)
if packet is not None:
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
print([hex(x) for x in packet])
print(f"RSSI: {rfm.last_rssi}")

View file

@ -0,0 +1,71 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
from adafruit_rfm import rfm9x
rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
# from adafruit_rfm import rfm9xfsk
# rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# set node addresses
rfm.node = 100
rfm.destination = 0xFF
# send startup message from my_node
rfm.send(
bytes(f"startup message from base {rfm.node}", "UTF-8"),
keep_listening=True,
)
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
while True:
if rfm.payload_ready():
packet = rfm.receive_with_ack(with_header=True, timeout=None)
if packet is not None:
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
print([hex(x) for x in packet])
print(f"RSSI: {rfm.last_rssi}")

87
examples/rfm_rh_node1.py Normal file
View file

@ -0,0 +1,87 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# Example to send a packet periodically between addressed nodes
# Author: Jerry Needell
#
import time
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
from adafruit_rfm import rfm9x
rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
# from adafruit_rfm import rfm9xfsk
# rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# set the time interval (seconds) for sending packets
transmit_interval = 2
# set node addresses
rfm.node = 1
rfm.destination = 100
# initialize counter
counter = 0
# send a broadcast message from my_node with ID = counter
rfm.send(bytes(f"Startup message {counter} from node {rfm.node}", "UTF-8"))
# Wait to receive packets.
print("Waiting for packets...")
now = time.monotonic()
while True:
# Look for a new packet: only accept if addresses to my_node
packet = rfm.receive(with_header=True, timeout=5.0)
# If no packet was received during the timeout then None is returned.
if packet is not None:
# Received a packet!
# Print out the raw bytes of the packet:
print("Received (raw header):", [hex(x) for x in packet[0:4]])
print(f"Received (raw payload): {packet[4:]}")
print(f"Received RSSI: {rfm.last_rssi}")
if time.monotonic() - now > transmit_interval:
now = time.monotonic()
counter = counter + 1
# send a mesage to destination_node from my_node
rfm.send(
bytes(f"message number {counter} from node {rfm.node}", "UTF-8"),
keep_listening=True,
)
button_pressed = None

View file

@ -0,0 +1,89 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# Example to send a packet periodically between addressed nodes with ACK
# Author: Jerry Needell
#
import time
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
from adafruit_rfm import rfm9x
rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
# from adafruit_rfm import rfm9xfsk
# rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# set the time interval (seconds) for sending packets
transmit_interval = 10
# set node addresses
rfm.node = 1
rfm.destination = 100
# initialize counter
counter = 0
ack_failed_counter = 0
# send startup message from my_node
rfm.send_with_ack(bytes(f"startup message from node {rfm.node}", "UTF-8"))
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
time_now = time.monotonic()
while True:
# Look for a new packet: only accept if addresses to my_node
packet = rfm.receive_with_ack(with_header=True)
# If no packet was received during the timeout then None is returned.
if packet is not None:
# Received a packet!
# Print out the raw bytes of the packet:
print("Received (raw header):", [hex(x) for x in packet[0:4]])
print(f"Received (raw payload): {packet[4:]}")
print(f"RSSI: {rfm.last_rssi}")
# send reading after any packet received
if time.monotonic() - time_now > transmit_interval:
# reset timeer
time_now = time.monotonic()
counter += 1
# send a mesage to destination_node from my_node
if not rfm.send_with_ack(bytes(f"message from node node {rfm.node} {counter}", "UTF-8")):
ack_failed_counter += 1
print(" No Ack: ", counter, ack_failed_counter)

View file

@ -0,0 +1,88 @@
# SPDX-FileCopyrightText: 2024 Ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# Simple demo of sending and recieving data with the RFM9x or RFM69 radios.
# Author: Jerry Needell
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
# from adafruit_rfm import rfm9x
# rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
from adafruit_rfm import rfm9xfsk
rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# Send a packet. Note you can only send a packet containing up to 60 bytes for an RFM69
# and 252 bytes forn an RFM9x.
# This is a limitation of the radio packet size, so if you need to send larger
# amounts of data you will need to break it into smaller send calls. Each send
# call will wait for the previous one to finish before continuing.
rfm.send(bytes("Hello world!\r\n", "utf-8"))
print("Sent Hello World message!")
# Wait to receive packets.
print("Waiting for packets...")
while True:
packet = rfm.receive()
# Optionally change the receive timeout from its default of 0.5 seconds:
# packet = rfm9x.receive(timeout=5.0)
# If no packet was received during the timeout then None is returned.
if packet is None:
# Packet has not been received
print("Received nothing! Listening again...")
else:
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
# And decode to ASCII text and print it too. Note that you always
# receive raw bytes and need to convert to a text format like ASCII
# if you intend to do string processing on your data. Make sure the
# sending side is sending ASCII data before you try to decode!
try:
packet_text = str(packet, "ascii")
print(f"Received (ASCII): {packet_text}")
except UnicodeError:
print("Hex data: ", [hex(x) for x in packet])
# Also read the RSSI (signal strength) of the last received message and
# print it.
rssi = rfm.last_rssi
print(f"Received signal strength: {rssi} dB")

100
examples/rfm_transmit.py Normal file
View file

@ -0,0 +1,100 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
# Example to send a packet periodically
# Author: Jerry Needell
#
import time
import board
import busio
import digitalio
# Define radio parameters.
RADIO_FREQ_MHZ = 915.0 # Frequency of the radio in Mhz. Must match your
# module! Can be a value like 915.0, 433.0, etc.
# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.CE1)
RESET = digitalio.DigitalInOut(board.D25)
# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
# Initialze RFM radio
# uncommnet the desired import and rfm initialization depending on the radio boards being used
# Use rfm9x for two RFM9x radios using LoRa
# from adafruit_rfm import rfm9x
# rfm = rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm9xfsk for two RFM9x radios or RFM9x to RFM69 using FSK
from adafruit_rfm import rfm9xfsk
rfm = rfm9xfsk.RFM9xFSK(spi, CS, RESET, RADIO_FREQ_MHZ)
# Use rfm69 for two RFM69 radios using FSK
# from adafruit_rfm import rfm69
# rfm = rfm69.RFM69(spi, CS, RESET, RADIO_FREQ_MHZ)
# For RFM69 only: Optionally set an encryption key (16 byte AES key). MUST match both
# on the transmitter and receiver (or be set to None to disable/the default).
# rfm.encryption_key = None
# rfm.encryption_key = (
# b"\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08"
# )
# for OOK on RFM69 or RFM9xFSK
# rfm.modulation_type = 1
# uncommnet to Disable the RadioHead Header
# rfm.radiohead = False
# in FSK/OOK modes rfo RFM69 or RFM9X - addresss filtering may be enabled
# rfm.enable_address_filter=True
# rfm.fsk_node_address=0x2
# rfm.fsk_broadcast_address=0xff
# set the time interval (seconds) for sending packets
transmit_interval = 5
# Note that the radio is configured in LoRa mode so you can't control sync
# word, encryption, frequency deviation, or other settings!
# You can however adjust the transmit power (in dB). The default is 13 dB but
# high power radios like the RFM95 can go up to 23 dB:
rfm.tx_power = 23
# initialize counter
counter = 0
# send a broadcast mesage
rfm.send(bytes(f"message number {counter}", "UTF-8"))
# Wait to receive packets.
print("Waiting for packets...")
# initialize flag and timer
send_reading = False
time_now = time.monotonic()
while True:
# Look for a new packet - wait up to 2 seconds:
packet = rfm.receive(timeout=2.0)
# If no packet was received during the timeout then None is returned.
if packet is not None:
# Received a packet!
# Print out the raw bytes of the packet:
print(f"Received (raw bytes): {packet}")
print("Hex data: ", [hex(x) for x in packet])
# send reading after any packet received
if time.monotonic() - time_now > transmit_interval:
# reset timeer
time_now = time.monotonic()
# clear flag to send data
send_reading = False
counter = counter + 1
rfm.send(bytes(f"message number {counter}", "UTF-8"))

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense

51
pyproject.toml Normal file
View file

@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: 2022 Alec Delaney, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2024 Jerry Needell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
[build-system]
requires = [
"setuptools",
"wheel",
"setuptools-scm",
]
[project]
name = "adafruit-circuitpython-rfm"
description = "Support for RFM69 and RFM9x modules"
version = "0.0.0+auto.0"
readme = "README.rst"
authors = [
{name = "Adafruit Industries", email = "circuitpython@adafruit.com"}
]
urls = {Homepage = "https://github.com/jerryneedell/Adafruit_CircuitPython_RFM"}
keywords = [
"adafruit",
"blinka",
"circuitpython",
"micropython",
"rfm",
"RFM69",
"RFM9x",
"radio",
]
license = {text = "MIT"}
classifiers = [
"Intended Audience :: Developers",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Embedded Systems",
"Topic :: System :: Hardware",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
]
dynamic = ["dependencies", "optional-dependencies"]
[tool.setuptools]
# TODO: IF LIBRARY FILES ARE A PACKAGE FOLDER,
# CHANGE `py_modules = ['...']` TO `packages = ['...']`
#py-modules = ["adafruit_rfm"]
packages = ["adafruit_rfm"]
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
optional-dependencies = {optional = {file = ["optional_requirements.txt"]}}

8
requirements.txt Normal file
View file

@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: Copyright (c) 2024 Jerry Needell for Adafruit Industries
#
# SPDX-License-Identifier: MIT
Adafruit-Blinka
adafruit-circuitpython-busdevice
asyncio

99
ruff.toml Normal file
View file

@ -0,0 +1,99 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
target-version = "py38"
line-length = 100
[lint]
select = ["I", "PL", "UP"]
extend-select = [
"D419", # empty-docstring
"E501", # line-too-long
"W291", # trailing-whitespace
"PLC0414", # useless-import-alias
"PLC2401", # non-ascii-name
"PLC2801", # unnecessary-dunder-call
"PLC3002", # unnecessary-direct-lambda-call
"E999", # syntax-error
"PLE0101", # return-in-init
"F706", # return-outside-function
"F704", # yield-outside-function
"PLE0116", # continue-in-finally
"PLE0117", # nonlocal-without-binding
"PLE0241", # duplicate-bases
"PLE0302", # unexpected-special-method-signature
"PLE0604", # invalid-all-object
"PLE0605", # invalid-all-format
"PLE0643", # potential-index-error
"PLE0704", # misplaced-bare-raise
"PLE1141", # dict-iter-missing-items
"PLE1142", # await-outside-async
"PLE1205", # logging-too-many-args
"PLE1206", # logging-too-few-args
"PLE1307", # bad-string-format-type
"PLE1310", # bad-str-strip-call
"PLE1507", # invalid-envvar-value
"PLE2502", # bidirectional-unicode
"PLE2510", # invalid-character-backspace
"PLE2512", # invalid-character-sub
"PLE2513", # invalid-character-esc
"PLE2514", # invalid-character-nul
"PLE2515", # invalid-character-zero-width-space
"PLR0124", # comparison-with-itself
"PLR0202", # no-classmethod-decorator
"PLR0203", # no-staticmethod-decorator
"UP004", # useless-object-inheritance
"PLR0206", # property-with-parameters
"PLR0904", # too-many-public-methods
"PLR0911", # too-many-return-statements
"PLR0912", # too-many-branches
"PLR0913", # too-many-arguments
"PLR0914", # too-many-locals
"PLR0915", # too-many-statements
"PLR0916", # too-many-boolean-expressions
"PLR1702", # too-many-nested-blocks
"PLR1704", # redefined-argument-from-local
"PLR1711", # useless-return
"C416", # unnecessary-comprehension
"PLR1733", # unnecessary-dict-index-lookup
"PLR1736", # unnecessary-list-index-lookup
# ruff reports this rule is unstable
#"PLR6301", # no-self-use
"PLW0108", # unnecessary-lambda
"PLW0120", # useless-else-on-loop
"PLW0127", # self-assigning-variable
"PLW0129", # assert-on-string-literal
"B033", # duplicate-value
"PLW0131", # named-expr-without-context
"PLW0245", # super-without-brackets
"PLW0406", # import-self
"PLW0602", # global-variable-not-assigned
"PLW0603", # global-statement
"PLW0604", # global-at-module-level
# fails on the try: import typing used by libraries
#"F401", # unused-import
"F841", # unused-variable
"E722", # bare-except
"PLW0711", # binary-op-exception
"PLW1501", # bad-open-mode
"PLW1508", # invalid-envvar-default
"PLW1509", # subprocess-popen-preexec-fn
"PLW2101", # useless-with-lock
"PLW3301", # nested-min-max
]
ignore = [
"PLR2004", # magic-value-comparison
"UP030", # format literals
"PLW1514", # unspecified-encoding
]
[format]
line-ending = "lf"