diff --git a/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md b/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md new file mode 100644 index 0000000..8de294e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/adafruit_circuitpython_pr.md @@ -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. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..041a337 --- /dev/null +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/failure-help-text.yml b/.github/workflows/failure-help-text.yml new file mode 100644 index 0000000..0b1194f --- /dev/null +++ b/.github/workflows/failure-help-text.yml @@ -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 diff --git a/.github/workflows/release_gh.yml b/.github/workflows/release_gh.yml new file mode 100644 index 0000000..9acec60 --- /dev/null +++ b/.github/workflows/release_gh.yml @@ -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 }} diff --git a/.github/workflows/release_pypi.yml b/.github/workflows/release_pypi.yml new file mode 100644 index 0000000..c16b495 --- /dev/null +++ b/.github/workflows/release_pypi.yml @@ -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 }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ff19dde --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..fcb7778 --- /dev/null +++ b/.readthedocs.yaml @@ -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 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a773adf --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,158 @@ + +# 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 don’t 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 . + +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 community’s 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. diff --git a/LICENSE b/LICENSE index 20fa55e..d70326e 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000..3f92dfc --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -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 +reason–for example, because of any applicable exception or limitation to copyright–then +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. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 0000000..204b93d --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +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. diff --git a/LICENSES/Unlicense.txt b/LICENSES/Unlicense.txt new file mode 100644 index 0000000..24a8f90 --- /dev/null +++ b/LICENSES/Unlicense.txt @@ -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 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..4ed4e8e --- /dev/null +++ b/README.rst @@ -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 `_ +* `Bus Device `_ + +Please ensure all dependencies are available on the CircuitPython filesystem. +This is easily achieved by downloading +`the Adafruit library and driver bundle `_ +or individual libraries can be installed using +`circup `_. + + +Installing from PyPI +===================== + +On supported GNU/Linux systems like the Raspberry Pi, you can install the driver locally `from +PyPI `_. +To install for current user: + +.. code-block:: shell + + pip3 install 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. + +Documentation +============= +API documentation for this library can be found on `Read the Docs `_. + +For information on building library documentation, please check out +`this guide `_. + +Contributing +============ + +Contributions are welcome! Please read our `Code of Conduct +`_ +before contributing to help this project stay welcoming. diff --git a/README.rst.license b/README.rst.license new file mode 100644 index 0000000..532888d --- /dev/null +++ b/README.rst.license @@ -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 diff --git a/adafruit_rfm/__init__.py b/adafruit_rfm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/adafruit_rfm/rfm69.py b/adafruit_rfm/rfm69.py new file mode 100644 index 0000000..4ff3c54 --- /dev/null +++ b/adafruit_rfm/rfm69.py @@ -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 diff --git a/adafruit_rfm/rfm9x.py b/adafruit_rfm/rfm9x.py new file mode 100644 index 0000000..d89000a --- /dev/null +++ b/adafruit_rfm/rfm9x.py @@ -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 diff --git a/adafruit_rfm/rfm9xfsk.py b/adafruit_rfm/rfm9xfsk.py new file mode 100644 index 0000000..4394cde --- /dev/null +++ b/adafruit_rfm/rfm9xfsk.py @@ -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 diff --git a/adafruit_rfm/rfm_common.py b/adafruit_rfm/rfm_common.py new file mode 100644 index 0000000..ed5a55a --- /dev/null +++ b/adafruit_rfm/rfm_common.py @@ -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() + """ diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000..5aca983 Binary files /dev/null and b/docs/_static/favicon.ico differ diff --git a/docs/_static/favicon.ico.license b/docs/_static/favicon.ico.license new file mode 100644 index 0000000..86a3fbf --- /dev/null +++ b/docs/_static/favicon.ico.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..d82934e --- /dev/null +++ b/docs/api.rst @@ -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: diff --git a/docs/api.rst.license b/docs/api.rst.license new file mode 100644 index 0000000..c7437b1 --- /dev/null +++ b/docs/api.rst.license @@ -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 diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..e474773 --- /dev/null +++ b/docs/conf.py @@ -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", + ), +] diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..28d502e --- /dev/null +++ b/docs/examples.rst @@ -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: diff --git a/docs/examples.rst.license b/docs/examples.rst.license new file mode 100644 index 0000000..c7437b1 --- /dev/null +++ b/docs/examples.rst.license @@ -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 diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..7e9b6d2 --- /dev/null +++ b/docs/index.rst @@ -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 + Download Library Bundle + CircuitPython Reference Documentation + CircuitPython Support Forum + Discord Chat + Adafruit Learning System + Adafruit Blog + Adafruit Store + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/index.rst.license b/docs/index.rst.license new file mode 100644 index 0000000..c7437b1 --- /dev/null +++ b/docs/index.rst.license @@ -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 diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..979f568 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2021 Kattni Rembor for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense + +sphinx +sphinxcontrib-jquery +sphinx-rtd-theme diff --git a/examples/rfm_fsk_node1.py b/examples/rfm_fsk_node1.py new file mode 100644 index 0000000..3cf612f --- /dev/null +++ b/examples/rfm_fsk_node1.py @@ -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) diff --git a/examples/rfm_fsk_node2.py b/examples/rfm_fsk_node2.py new file mode 100644 index 0000000..95d3626 --- /dev/null +++ b/examples/rfm_fsk_node2.py @@ -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) diff --git a/examples/rfm_raw.py b/examples/rfm_raw.py new file mode 100644 index 0000000..ac5e24b --- /dev/null +++ b/examples/rfm_raw.py @@ -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") diff --git a/examples/rfm_rh_asyncio_listen.py b/examples/rfm_rh_asyncio_listen.py new file mode 100644 index 0000000..8293bcf --- /dev/null +++ b/examples/rfm_rh_asyncio_listen.py @@ -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()) diff --git a/examples/rfm_rh_asyncio_listen_ack.py b/examples/rfm_rh_asyncio_listen_ack.py new file mode 100644 index 0000000..93f4eec --- /dev/null +++ b/examples/rfm_rh_asyncio_listen_ack.py @@ -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()) diff --git a/examples/rfm_rh_asyncio_node1.py b/examples/rfm_rh_asyncio_node1.py new file mode 100644 index 0000000..114ee69 --- /dev/null +++ b/examples/rfm_rh_asyncio_node1.py @@ -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()) diff --git a/examples/rfm_rh_base.py b/examples/rfm_rh_base.py new file mode 100644 index 0000000..a44b753 --- /dev/null +++ b/examples/rfm_rh_base.py @@ -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}") diff --git a/examples/rfm_rh_base_ack.py b/examples/rfm_rh_base_ack.py new file mode 100644 index 0000000..8d3f2c6 --- /dev/null +++ b/examples/rfm_rh_base_ack.py @@ -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}") diff --git a/examples/rfm_rh_node1.py b/examples/rfm_rh_node1.py new file mode 100644 index 0000000..e232034 --- /dev/null +++ b/examples/rfm_rh_node1.py @@ -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 diff --git a/examples/rfm_rh_node1_ack.py b/examples/rfm_rh_node1_ack.py new file mode 100644 index 0000000..baadfd3 --- /dev/null +++ b/examples/rfm_rh_node1_ack.py @@ -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) diff --git a/examples/rfm_simpletest.py b/examples/rfm_simpletest.py new file mode 100644 index 0000000..9ec4de1 --- /dev/null +++ b/examples/rfm_simpletest.py @@ -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") diff --git a/examples/rfm_transmit.py b/examples/rfm_transmit.py new file mode 100644 index 0000000..48e8b3a --- /dev/null +++ b/examples/rfm_transmit.py @@ -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")) diff --git a/optional_requirements.txt b/optional_requirements.txt new file mode 100644 index 0000000..d4e27c4 --- /dev/null +++ b/optional_requirements.txt @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries +# +# SPDX-License-Identifier: Unlicense diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..81061b4 --- /dev/null +++ b/pyproject.toml @@ -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"]}} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..35ac53b --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..db37c83 --- /dev/null +++ b/ruff.toml @@ -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"