├── .github └── workflows │ └── test-and-publish.yml ├── .gitignore ├── DOCS ├── RAK811_AT_Command_Manual_V1.0.pdf └── RAK811 LoRa AT Command V1.5.pdf ├── LICENSE.txt ├── README.md ├── examples ├── abp.py ├── abp_v3.py ├── api_demo.py ├── otaa.py ├── otaa_v3.py ├── otaa_v3_class_c.py ├── p2p.py ├── p2p.sh ├── p2p_v3.py ├── p2p_v3.sh └── ttn_secrets_template.py ├── pyproject.toml ├── rak811 ├── __init__.py ├── cli.py ├── cli_v3.py ├── exception.py ├── rak811.py ├── rak811_v3.py └── serial.py ├── setup.cfg ├── setup.py ├── tests ├── test_cli.py ├── test_cli_v3.py ├── test_rak811.py ├── test_rak811_v3.py └── test_serial.py └── tox.ini /.github/workflows/test-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: Test and Publish 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.7', '3.8'] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install tox tox-gh-actions 24 | - name: Run tests 25 | run: tox 26 | - name: Upload coverage 27 | if: ${{ matrix.python-version == '3.8' }} 28 | uses: codecov/codecov-action@v2 29 | publish: 30 | needs: test 31 | runs-on: ubuntu-latest 32 | if: ${{ github.event_name == 'push' }} 33 | steps: 34 | - uses: actions/checkout@v2 35 | with: 36 | fetch-depth: 0 37 | - name: Set up Python 38 | uses: actions/setup-python@v2 39 | with: 40 | python-version: 3.8 41 | - name: Install dependencies 42 | run: | 43 | python -m pip install --upgrade pip wheel 44 | python -m pip install build 45 | - name: Build package 46 | run: | 47 | python -m build --sdist --wheel --outdir dist/ . 48 | - name: Publish untagged package to Test PyPI 49 | if: ${{ ! startsWith(github.ref, 'refs/tags/v') }} 50 | uses: pypa/gh-action-pypi-publish@master 51 | with: 52 | user: __token__ 53 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 54 | repository_url: https://test.pypi.org/legacy/ 55 | - name: Publish tagged package to PyPI 56 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 57 | uses: pypa/gh-action-pypi-publish@master 58 | with: 59 | user: __token__ 60 | password: ${{ secrets.PYPI_API_TOKEN }} 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *.swp 4 | venv 5 | ttn_secrets.py 6 | build 7 | dist 8 | .coverage 9 | htmlcov 10 | coverage.xml 11 | .tox 12 | -------------------------------------------------------------------------------- /DOCS/RAK811_AT_Command_Manual_V1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmedeeBulle/pyrak811/f08c1d83341371c9757c9324adfb73110d921b76/DOCS/RAK811_AT_Command_Manual_V1.0.pdf -------------------------------------------------------------------------------- /DOCS/RAK811 LoRa AT Command V1.5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AmedeeBulle/pyrak811/f08c1d83341371c9757c9324adfb73110d921b76/DOCS/RAK811 LoRa AT Command V1.5.pdf -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RAK811 Python 3 library for Raspberry Pi 2 | 3 | [![Latest Version](https://img.shields.io/pypi/v/rak811.svg)](https://pypi.org/project/rak811/) 4 | [![GitHub Action (Test and Publish)](https://github.com/AmedeeBulle/pyrak811/actions/workflows/test-and-publish.yml/badge.svg)](https://github.com/AmedeeBulle/pyrak811) 5 | [![codecov](https://codecov.io/gh/AmedeeBulle/pyrak811/branch/main/graph/badge.svg)](https://codecov.io/gh/AmedeeBulle/pyrak811) 6 | 7 | ## About 8 | 9 | RAK811 Python 3 library and command-line interface for use with the Raspberry Pi LoRa (p)HAT. 10 | 11 | The library exposes the RAK811 module AT commands as described in the following documents: 12 | 13 | - [RAK811 Lora AT Command User Guide V1.5](https://github.com/AmedeeBulle/pyrak811/blob/main/DOCS/RAK811%C2%A0LoRa%C2%A0AT%C2%A0Command%C2%A0V1.5.pdf) for modules with the V2.0.x firmware. 14 | - [RAK811 AT Command Manual V1.0](https://github.com/AmedeeBulle/pyrak811/blob/main/DOCS/RAK811_AT_Command_Manual_V1.0.pdf) for modules with the V3.0.x firmware. 15 | 16 | The command-line interface exposes all API calls to the command line. 17 | 18 | ## Requirements 19 | 20 | - A Raspberry Pi! 21 | - A RAK811 LoRa module ([PiSupply IoT LoRa Node pHAT for Raspberry Pi](https://uk.pi-supply.com/products/iot-lora-node-phat-for-raspberry-pi) / [RAK811 WisNode - LoRa](https://news.rakwireless.com/wisnode-lora-quick-start/)) 22 | - On the Raspberry Pi the hardware serial port must be enabled and the serial console disabled (use `raspi-config`) 23 | - The user running the application must be in the `dialout` and `gpio` groups (this is the default for the `pi` user) 24 | 25 | ## Install the rak811 package 26 | 27 | The package is installed from PyPI: 28 | 29 | ```shell 30 | sudo pip3 install rak811 31 | ``` 32 | 33 | The `pip3` command is part of the `python3-pip` package. If it is missing on your system, run: 34 | 35 | ```shell 36 | sudo apt-get install python3-pip 37 | ``` 38 | 39 | [PiSupply](https://uk.pi-supply.com/) provides [detailed instructions](https://learn.pi-supply.com/make/getting-started-with-the-raspberry-pi-lora-node-phat/) for configuring your Raspberry Pi. 40 | 41 | ## Usage 42 | 43 | ### Quick start with The Things Network 44 | 45 | #### Identify your device 46 | 47 | If you don't know the firmware level of you module run the following commands: 48 | 49 | ```shell 50 | rak811 hard-reset 51 | rak811 version 52 | ``` 53 | 54 | For V2.0.x firmware use the `rak811` command and python module, for V3.0.X use `rak811v3` command and `rak811_v3` python module. 55 | 56 | #### Register your device 57 | 58 | Register you device on [TheThingsNetwork](https://www.thethingsnetwork.org) using the unique id of your RAK811 module (Device EUI). 59 | You can retrieve your Device EUI with the following command (V2.0.x): 60 | 61 | ```shell 62 | rak811 hard-reset 63 | rak811 get-config dev_eui 64 | ``` 65 | 66 | _Note_: the `rak811 hard-reset` command is only needed once after (re)booting your Raspberry Pi to activate the module. 67 | 68 | or (V3.0.x): 69 | 70 | ```shell 71 | rak811v3 set-config lora:join_mode:0 72 | rak811v3 get-config lora:status | grep DevEui 73 | ``` 74 | 75 | #### Hello World 76 | 77 | Send your first LoRaWan message wit the following python code snippet: 78 | (The App EUI and App Key are copied verbatim from the TTN console) 79 | 80 | ```python 81 | #!/usr/bin/env python3 82 | # V2.0.x firmware 83 | from rak811.rak811 import Mode, Rak811 84 | 85 | lora = Rak811() 86 | lora.hard_reset() 87 | lora.mode = Mode.LoRaWan 88 | lora.band = 'EU868' 89 | lora.set_config(app_eui='70B3D5xxxxxxxxxx', 90 | app_key='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') 91 | lora.join_otaa() 92 | lora.dr = 5 93 | lora.send('Hello world') 94 | lora.close() 95 | ``` 96 | 97 | ```python 98 | #!/usr/bin/env python3 99 | # V3.0.x firmware 100 | from rak811.rak811_v3 import Rak811 101 | 102 | lora = Rak811() 103 | lora.set_config('lora:work_mode:0') 104 | lora.set_config('lora:join_mode:0') 105 | lora.set_config('lora:region:EU868') 106 | lora.set_config('lora:app_eui:70B3D5xxxxxxxxxx') 107 | lora.set_config('lora:app_key:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') 108 | lora.join() 109 | lora.set_config('lora:dr:5') 110 | lora.send('Hello world') 111 | lora.close() 112 | ``` 113 | 114 | Your first message should appear on the TTN console! 115 | 116 | ### Next steps 117 | 118 | See the [example directory on GitHub](https://github.com/AmedeeBulle/pyrak811/tree/main/examples): 119 | 120 | - `api_demo.py`: demo most of the V2.0.x API calls 121 | - `otaa.py` / `otaa_v3.py`: OTAA example 122 | - `otaa_v3_class_c.py`: OTAA example with device in Class C mode 123 | - `abp.py` / `abp_v3.py`: ABP example 124 | - `p2p.py` / `p2p_v3.py`: P2P example 125 | - `p2p.sh` / `p2p_v3.sh`: P2P example based on the command-line interface (see below) 126 | 127 | To run the examples, first copy the `ttn_secrets_template.py` to `ttn_secrets.py` and enter your LoRaWan [TheThingsNetwork](https://www.thethingsnetwork.org) keys. 128 | 129 | _Note_: you do not need to `hard_reset` the module each time you run a script. 130 | However you must do it the first time after a (re)boot to activate the module. 131 | 132 | ### balenaCloud 133 | 134 | Sample code to use the library with [balenaCloud](https://www.balena.io/): [ 135 | Basic RAK811 example with balenaCloud](https://github.com/AmedeeBulle/pyrak811-balena) (V2.0.x firmware). 136 | 137 | ## Command-line interface 138 | 139 | ### V2.0.x firmware 140 | 141 | The `rak811` command exposes all library calls to the command line: 142 | 143 | ```console 144 | $ rak811 --help 145 | Usage: rak811 [OPTIONS] COMMAND [ARGS]... 146 | 147 | Command line interface for the RAK811 module. 148 | 149 | Options: 150 | -v, --verbose Verbose mode 151 | --help Show this message and exit. 152 | 153 | Commands: 154 | abp-info Get ABP info. 155 | band Get/Set LoRaWan region. 156 | clear-radio-status Clear radio statistics. 157 | dr Get/set next send data rate. 158 | get-config Get LoraWan configuration. 159 | hard-reset Hardware reset of the module. 160 | join-abp Join the configured network in ABP mode. 161 | join-otaa Join the configured network in OTAA mode. 162 | link-cnt Get up & downlink counters. 163 | mode Get/Set mode to LoRaWan or LoRaP2P. 164 | radio-status Get radio statistics. 165 | recv-ex RSSI & SNR report on receive. 166 | reload Set LoRaWan or LoRaP2P configurations to default. 167 | reset Reset Module or LoRaWan stack. 168 | send Send LoRaWan message and check for downlink. 169 | set-config Set LoraWAN configuration. 170 | signal Get (RSSI,SNR) from latest received packet. 171 | sleep Enter sleep mode. 172 | version Get module version. 173 | wake-up Wake up. 174 | ``` 175 | 176 | Session example: 177 | 178 | ```console 179 | $ rak811 -v reset lora 180 | LoRa reset complete. 181 | $ rak811 -v set-config app_eui=70B3D5xxxxxxxxxx app_key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 182 | LoRaWan parameters set 183 | $ rak811 -v join-otaa 184 | Joined in OTAA mode 185 | $ rak811 -v dr 186 | 5 187 | $ rak811 -v dr 4 188 | Data rate set to 4. 189 | $ rak811 -v send Hello 190 | Message sent. 191 | No downlink available. 192 | $ rak811 -v send --port 4 --binary '01020211' 193 | Message sent. 194 | Downlink received: 195 | Port: 1 196 | RSSI: -56 197 | SNR: 31 198 | Data: 123456 199 | ``` 200 | 201 | _Note_: for your first session after boot, you will need to do a `hard-reset` instead of a `reset lora` command to activate the module. 202 | 203 | ### V3.0.x firmware 204 | 205 | The `rak811v3` command exposes the following library calls to the command line: 206 | 207 | ```console 208 | $ rak811v3 --help 209 | Usage: rak811v3 [OPTIONS] COMMAND [ARGS]... 210 | 211 | Command line interface for the RAK811 module. 212 | 213 | Options: 214 | -v, --verbose Verbose mode 215 | -d, --debug Debug mode 216 | --version Show the version and exit. 217 | --help Show this message and exit. 218 | 219 | Commands: 220 | get-config Execute get_config RAK811 command. 221 | hard-reset Hardware reset of the module. 222 | help Print module help. 223 | join Join the configured network. 224 | receive-p2p Get LoraP2P message. 225 | run Exit boot mode and enter normal mode. 226 | send Send LoRaWan message and check for downlink. 227 | send-p2p Send LoRa P2P message. 228 | send-uart Send data to UART. 229 | set-config Execute set_config RAK811 command. 230 | version Get module version. 231 | ``` 232 | 233 | Session example: 234 | 235 | ```console 236 | $ rak811v3 -v set-config lora:work_mode:0 237 | Configuration done 238 | LoRa (R) is a registered trademark or service mark of Semtech Corporation or its affiliates. LoRaWAN (R) is a licensed mark. 239 | RAK811 Version:3.0.0.14.H 240 | UART1 work mode: RUI_UART_NORMAL, 115200, N81 241 | UART3 work mode: RUI_UART_NORMAL, 115200, N81 242 | LoRa work mode:LoRaWAN, join_mode:OTAA, MulticastEnable: false, Class: A 243 | $ # The following is not necessary as in this case the module is already in OTAA mode! 244 | $ rak811v3 -v set-config lora:join_mode:0 245 | Configuration done 246 | $ rak811v3 -v set-config lora:app_eui:70B3D5xxxxxxxxxx 247 | Configuration done 248 | $ rak811v3 -v set-config lora:app_key:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 249 | Configuration done 250 | $ rak811v3 -v join 251 | Joined! 252 | $ rak811v3 get-config lora:status | grep 'Current Datarate' 253 | Current Datarate: 4 254 | $ rak811v3 -v set-config lora:dr:5 255 | Configuration done 256 | $ rak811v3 -v send 'Hello' 257 | Message sent. 258 | No downlink available. 259 | $ rak811v3 -v set-config lora:confirm:1 260 | Configuration done 261 | $ rak811v3 -v send --port 4 --binary '01020211' 262 | Message sent. 263 | Send confirmed. 264 | RSSI: -66 265 | SNR: 6 266 | ``` 267 | -------------------------------------------------------------------------------- /examples/abp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RAK811 OTAA demo. 3 | 4 | Minimalistic OTAA demo 5 | 6 | Copyright 2019, 2021 Philippe Vanhaesendonck 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | 20 | SPDX-License-Identifier: Apache-2.0 21 | """ 22 | from random import randint 23 | from sys import exit 24 | from time import sleep 25 | 26 | from rak811.rak811 import Mode, Rak811 27 | from ttn_secrets import APPS_KEY, DEV_ADDR, NWKS_KEY 28 | 29 | lora = Rak811() 30 | 31 | # Most of the setup should happen only once... 32 | print('Setup') 33 | lora.hard_reset() 34 | lora.mode = Mode.LoRaWan 35 | lora.band = 'EU868' 36 | lora.set_config(dev_addr=DEV_ADDR, 37 | apps_key=APPS_KEY, 38 | nwks_key=NWKS_KEY) 39 | 40 | print('Joining') 41 | lora.join_abp() 42 | # Note that DR is different from SF and depends on the region 43 | # See: https://docs.exploratory.engineering/lora/dr_sf/ 44 | # Set Data Rate to 5 which is SF7/125kHz for EU868 45 | lora.dr = 5 46 | 47 | print('Sending packets every minute - Interrupt to cancel loop') 48 | print('You can send downlinks from the TTN console') 49 | try: 50 | while True: 51 | print('Send packet') 52 | # Cayenne lpp random value as analog 53 | lora.send(bytes.fromhex('0102{:04x}'.format(randint(0, 0x7FFF)))) 54 | 55 | while lora.nb_downlinks: 56 | print('Received', lora.get_downlink()['data'].hex()) 57 | 58 | sleep(60) 59 | except: # noqa: E722 60 | pass 61 | 62 | print('Cleaning up') 63 | lora.close() 64 | exit(0) 65 | -------------------------------------------------------------------------------- /examples/abp_v3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RAK811 ABP demo. 3 | 4 | Minimalistic ABP demo (v3.x firmware) 5 | 6 | Copyright 2021 Philippe Vanhaesendonck 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | 20 | SPDX-License-Identifier: Apache-2.0 21 | """ 22 | import logging 23 | from random import randint 24 | from sys import exit 25 | from time import sleep 26 | from timeit import default_timer as timer 27 | from traceback import print_exc 28 | 29 | from rak811.rak811_v3 import Rak811 30 | from ttn_secrets import APPS_KEY, DEV_ADDR, NWKS_KEY 31 | 32 | # Set level to logging.DEBUG for a verbose output 33 | logging.basicConfig(level=logging.INFO) 34 | 35 | lora = Rak811() 36 | 37 | # Most of the setup should happen only once... 38 | print('Setup') 39 | # Ensure we are in LoRaWan mode 40 | lora.set_config('lora:work_mode:0') 41 | # Select ABP 42 | lora.set_config('lora:join_mode:1') 43 | # Select region 44 | lora.set_config('lora:region:EU868') 45 | # Set keys 46 | lora.set_config(f'lora:dev_addr:{DEV_ADDR}') 47 | lora.set_config(f'lora:apps_key:{APPS_KEY}') 48 | lora.set_config(f'lora:nwks_key:{NWKS_KEY}') 49 | # Set data rate 50 | # Note that DR is different from SF and depends on the region 51 | # See: https://docs.exploratory.engineering/lora/dr_sf/ 52 | # Set Data Rate to 5 which is SF7/125kHz for EU868 53 | lora.set_config('lora:dr:5') 54 | 55 | # Print config 56 | for line in lora.get_config('lora:status'): 57 | print(f' {line}') 58 | 59 | print('Joining') 60 | start_time = timer() 61 | lora.join() 62 | print('Joined in {:.2f} secs'.format(timer() - start_time)) 63 | 64 | print('Sending packets every minute - Interrupt to cancel loop') 65 | print('You can send downlinks from the TTN console') 66 | try: 67 | while True: 68 | print('Sending packet') 69 | # Cayenne lpp random value as analog 70 | start_time = timer() 71 | lora.send(bytes.fromhex('0102{:04x}'.format(randint(0, 0x7FFF)))) 72 | print('Packet sent in {:.2f} secs'.format(timer() - start_time)) 73 | 74 | while lora.nb_downlinks: 75 | print('Downlink received', lora.get_downlink()['data'].hex()) 76 | 77 | sleep(60) 78 | except KeyboardInterrupt: 79 | print() 80 | except Exception: 81 | print_exc() 82 | 83 | print('Cleaning up') 84 | lora.close() 85 | exit(0) 86 | -------------------------------------------------------------------------------- /examples/api_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RAK811 API demo. 3 | 4 | Simple demo to illustrate API usage 5 | 6 | Copyright 2019, 2021 Philippe Vanhaesendonck 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | 20 | SPDX-License-Identifier: Apache-2.0 21 | """ 22 | from sys import exit 23 | from time import sleep 24 | 25 | from rak811.rak811 import Mode, RecvEx, Reset 26 | from rak811.rak811 import Rak811, Rak811ResponseError 27 | from ttn_secrets import APP_EUI, APP_KEY 28 | 29 | config_keys = ('dev_addr', 'dev_eui', 'app_eui', 'app_key', 'nwks_key', 30 | 'apps_key', 'tx_power', 'pwr_level', 'adr', 'dr', 'public_net', 31 | 'rx_delay1', 'ch_list', 'ch_mask', 'max_chs', 'rx2', 32 | 'join_cnt', 'nbtrans', 'retrans', 'class', 'duty') 33 | 34 | print('Instanciate class') 35 | lora = Rak811() 36 | 37 | print('Hard reset board') 38 | lora.hard_reset() 39 | 40 | # System commands 41 | print('Version', lora.version) 42 | 43 | print('Sleeping') 44 | lora.sleep() 45 | sleep(5) 46 | lora.wake_up() 47 | print('Awake') 48 | 49 | print('Reset Module') 50 | lora.reset(Reset.Module) 51 | 52 | print('Reset LoRa') 53 | lora.reset(Reset.LoRa) 54 | 55 | print('Reload') 56 | lora.reload() 57 | 58 | for mode in (Mode.LoRaP2P, Mode.LoRaWan): 59 | lora.mode = mode 60 | print('Mode', lora.mode) 61 | 62 | for band in ('US915', 'EU868'): 63 | lora.band = band 64 | print('Band', lora.band) 65 | 66 | print("Configure module") 67 | lora.set_config(app_eui=APP_EUI, 68 | app_key=APP_KEY) 69 | print("Module configuration:") 70 | for config_key in config_keys: 71 | try: 72 | print('>>', config_key, lora.get_config(config_key)) 73 | except Rak811ResponseError as e: 74 | print('>>', config_key, e.errno, e.strerror) 75 | 76 | for recv_ex in (RecvEx.Disabled, RecvEx.Enabled): 77 | lora.recv_ex = recv_ex 78 | print('Recv ex', lora.recv_ex) 79 | 80 | print('Join') 81 | lora.join_otaa() 82 | 83 | print('DR', lora.dr) 84 | lora.dr = 5 85 | print('DR', lora.dr) 86 | 87 | print('Signal', lora.signal) 88 | 89 | print('Send string') 90 | lora.send('Hello') 91 | 92 | print('Signal', lora.signal) 93 | 94 | print('Link counter', lora.link_cnt) 95 | 96 | print('ABP Info', lora.abp_info) 97 | 98 | print('Send Confirmed (Cayenne LPP format)') 99 | lora.send(bytes.fromhex('016700F0'), port=11, confirm=True) 100 | 101 | print('Check for downlink messages') 102 | while lora.nb_downlinks: 103 | print('>>', lora.get_downlink()) 104 | 105 | lora.close() 106 | exit(0) 107 | -------------------------------------------------------------------------------- /examples/otaa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RAK811 OTAA demo. 3 | 4 | Minimalistic OTAA demo 5 | 6 | Copyright 2019, 2021 Philippe Vanhaesendonck 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | 20 | SPDX-License-Identifier: Apache-2.0 21 | """ 22 | from random import randint 23 | from sys import exit 24 | from time import sleep 25 | 26 | from rak811.rak811 import Mode, Rak811 27 | from ttn_secrets import APP_EUI, APP_KEY 28 | 29 | lora = Rak811() 30 | 31 | # Most of the setup should happen only once... 32 | print('Setup') 33 | lora.hard_reset() 34 | lora.mode = Mode.LoRaWan 35 | lora.band = 'EU868' 36 | lora.set_config(app_eui=APP_EUI, 37 | app_key=APP_KEY) 38 | 39 | print('Joining') 40 | lora.join_otaa() 41 | # Note that DR is different from SF and depends on the region 42 | # See: https://docs.exploratory.engineering/lora/dr_sf/ 43 | # Set Data Rate to 5 which is SF7/125kHz for EU868 44 | lora.dr = 5 45 | 46 | print('Sending packets every minute - Interrupt to cancel loop') 47 | print('You can send downlinks from the TTN console') 48 | try: 49 | while True: 50 | print('Send packet') 51 | # Cayenne lpp random value as analog 52 | lora.send(bytes.fromhex('0102{:04x}'.format(randint(0, 0x7FFF)))) 53 | 54 | while lora.nb_downlinks: 55 | print('Received', lora.get_downlink()['data'].hex()) 56 | 57 | sleep(60) 58 | except: # noqa: E722 59 | pass 60 | 61 | print('Cleaning up') 62 | lora.close() 63 | exit(0) 64 | -------------------------------------------------------------------------------- /examples/otaa_v3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RAK811 OTAA demo. 3 | 4 | Minimalistic OTAA demo (v3.x firmware) 5 | 6 | Copyright 2021 Philippe Vanhaesendonck 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | 20 | SPDX-License-Identifier: Apache-2.0 21 | """ 22 | import logging 23 | from random import randint 24 | from sys import exit 25 | from time import sleep 26 | from timeit import default_timer as timer 27 | from traceback import print_exc 28 | 29 | from rak811.rak811_v3 import Rak811 30 | from ttn_secrets import APP_EUI, APP_KEY 31 | 32 | # Set level to logging.DEBUG for a verbose output 33 | logging.basicConfig(level=logging.INFO) 34 | 35 | lora = Rak811() 36 | 37 | # Most of the setup should happen only once... 38 | print('Setup') 39 | # Ensure we are in LoRaWan mode 40 | lora.set_config('lora:work_mode:0') 41 | # Select OTAA 42 | lora.set_config('lora:join_mode:0') 43 | # Select region 44 | lora.set_config('lora:region:EU868') 45 | # Set keys 46 | lora.set_config(f'lora:app_eui:{APP_EUI}') 47 | lora.set_config(f'lora:app_key:{APP_KEY}') 48 | # Set data rate 49 | # Note that DR is different from SF and depends on the region 50 | # See: https://docs.exploratory.engineering/lora/dr_sf/ 51 | # Set Data Rate to 5 which is SF7/125kHz for EU868 52 | lora.set_config('lora:dr:5') 53 | 54 | # Print config 55 | for line in lora.get_config('lora:status'): 56 | print(f' {line}') 57 | 58 | print('Joining') 59 | start_time = timer() 60 | lora.join() 61 | print('Joined in {:.2f} secs'.format(timer() - start_time)) 62 | 63 | print('Sending packets every minute - Interrupt to cancel loop') 64 | print('You can send downlinks from the TTN console') 65 | try: 66 | while True: 67 | print('Sending packet') 68 | # Cayenne lpp random value as analog 69 | start_time = timer() 70 | lora.send(bytes.fromhex('0102{:04x}'.format(randint(0, 0x7FFF)))) 71 | print('Packet sent in {:.2f} secs'.format(timer() - start_time)) 72 | 73 | while lora.nb_downlinks: 74 | print('Downlink received', lora.get_downlink()['data'].hex()) 75 | 76 | sleep(60) 77 | except KeyboardInterrupt: 78 | print() 79 | except Exception: 80 | print_exc() 81 | 82 | print('Cleaning up') 83 | lora.close() 84 | exit(0) 85 | -------------------------------------------------------------------------------- /examples/otaa_v3_class_c.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RAK811 OTAA Class C demo. 3 | 4 | Minimalistic OTAA demo with Class C device (v3.x firmware). 5 | 6 | The module will wait on downlink message, run a task and send an the same data 7 | back as uplink 8 | 9 | The module uses receive_p2p to get out-of-band downlinks. 10 | 11 | The device must be configured as a Class C device in your LoRaWan Application 12 | Server (TTN/TTS). 13 | 14 | Note that delivery of downlinks is not guaranteed: 15 | - Device might not receive the signal 16 | - The device is not listening when in send mode 17 | 18 | Copyright 2022 Philippe Vanhaesendonck 19 | 20 | Licensed under the Apache License, Version 2.0 (the "License"); 21 | you may not use this file except in compliance with the License. 22 | You may obtain a copy of the License at 23 | 24 | http://www.apache.org/licenses/LICENSE-2.0 25 | 26 | Unless required by applicable law or agreed to in writing, software 27 | distributed under the License is distributed on an "AS IS" BASIS, 28 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | See the License for the specific language governing permissions and 30 | limitations under the License. 31 | 32 | SPDX-License-Identifier: Apache-2.0 33 | """ 34 | import logging 35 | from random import randint 36 | from sys import exit 37 | from time import sleep 38 | from timeit import default_timer as timer 39 | from traceback import print_exc 40 | 41 | from rak811.rak811_v3 import Rak811, Rak811ResponseError 42 | from ttn_secrets import APP_EUI, APP_KEY 43 | 44 | # Set level to logging.DEBUG for a verbose output 45 | logging.basicConfig(level=logging.INFO) 46 | 47 | lora = Rak811() 48 | 49 | # Most of the setup should happen only once... 50 | print("Setup") 51 | # Ensure we are in LoRaWan mode / Class C 52 | lora.set_config("lora:work_mode:0") 53 | lora.set_config("lora:class:2") 54 | # Select OTAA 55 | lora.set_config("lora:join_mode:0") 56 | # Select region 57 | lora.set_config("lora:region:EU868") 58 | # Set keys 59 | lora.set_config(f"lora:app_eui:{APP_EUI}") 60 | lora.set_config(f"lora:app_key:{APP_KEY}") 61 | # Set data rate 62 | # Note that DR is different from SF and depends on the region 63 | # See: https://docs.exploratory.engineering/lora/dr_sf/ 64 | # Set Data Rate to 5 which is SF7/125kHz for EU868 65 | lora.set_config("lora:dr:5") 66 | 67 | # Print config 68 | for line in lora.get_config("lora:status"): 69 | print(f" {line}") 70 | 71 | print("Joining") 72 | start_time = timer() 73 | lora.join() 74 | print("Joined in {:.2f} secs".format(timer() - start_time)) 75 | 76 | print("Sending initial Hello packet") 77 | start_time = timer() 78 | lora.send("Hello") 79 | print("Packet sent in {:.2f} secs".format(timer() - start_time)) 80 | print("Entering wait loop") 81 | print("You can send downlinks from the TTN console") 82 | try: 83 | while True: 84 | print("Waiting for downlinks...") 85 | try: 86 | lora.receive_p2p(60) 87 | except Rak811ResponseError as e: 88 | print("Error while waiting for downlink {}: {}".format(e.errno, e.strerror)) 89 | while lora.nb_downlinks: 90 | data = lora.get_downlink()["data"] 91 | if data != b"": 92 | print("Downlink received", data.hex()) 93 | # simulate some processing time 94 | sleep(randint(5, 10)) 95 | print("Sending back results") 96 | start_time = timer() 97 | try: 98 | lora.send(data) 99 | print("Packet sent in {:.2f} secs".format(timer() - start_time)) 100 | except Rak811ResponseError as e: 101 | print("Error while sendind data {}: {}".format(e.errno, e.strerror)) 102 | 103 | 104 | except KeyboardInterrupt: 105 | print() 106 | except Exception: 107 | print_exc() 108 | 109 | print("Cleaning up") 110 | lora.close() 111 | exit(0) 112 | -------------------------------------------------------------------------------- /examples/p2p.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RAK811 P2P demo. 3 | 4 | Send counter messages at random interval and listen the rest of the time. 5 | 6 | Start this script on 2 or more nodes an observe the packets flowing. 7 | 8 | Copyright 2019, 2021 Philippe Vanhaesendonck 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | 22 | SPDX-License-Identifier: Apache-2.0 23 | """ 24 | from random import randint 25 | from sys import exit 26 | from time import time 27 | 28 | from rak811.rak811 import Mode, Rak811 29 | 30 | # Send packet every P2P_BASE + (0..P2P_RANDOM) seconds 31 | P2P_BASE = 30 32 | P2P_RANDOM = 60 33 | 34 | # Magic key to recognize our messages 35 | P2P_MAGIC = b'\xca\xfe' 36 | 37 | lora = Rak811() 38 | 39 | # Most of the setup should happen only once... 40 | print('Setup') 41 | lora.hard_reset() 42 | lora.mode = Mode.LoRaP2P 43 | 44 | # RF configuration 45 | # - Avoid LoRaWan channels (You will get quite a lot of spurious packets!) 46 | # - Respect local regulation (frequency, power, duty cycle) 47 | lora.rf_config = { 48 | 'sf': 7, 49 | 'freq': 869.800, 50 | 'pwr': 16 51 | } 52 | 53 | print('Entering send/receive loop') 54 | counter = 0 55 | try: 56 | while True: 57 | # Calculate next message send timestamp 58 | next_send = time() + P2P_BASE + randint(0, P2P_RANDOM) 59 | # Set module in receive mode 60 | lora.rxc() 61 | # Loop until we reach the next send time 62 | # Don't enter loop for small wait times (<1 1 sec.) 63 | while (time() + 1) < next_send: 64 | wait_time = next_send - time() 65 | print('Waiting on message for {:0.0f} seconds'.format(wait_time)) 66 | # Note that you don't have to listen actively for capturing message 67 | # Once in receive mode, the library will capture all messages sent. 68 | lora.rx_get(wait_time) 69 | while lora.nb_downlinks > 0: 70 | message = lora.get_downlink() 71 | data = message['data'] 72 | if data[:len(P2P_MAGIC)] == P2P_MAGIC: 73 | print( 74 | 'Received message: {}'.format( 75 | int.from_bytes(data[len(P2P_MAGIC):], 76 | byteorder='big') 77 | ) 78 | ) 79 | print('RSSI: {}, SNR: {}'.format(message['rssi'], 80 | message['snr'])) 81 | else: 82 | print('Foreign message received') 83 | # Time to send message 84 | # Exit receive mode 85 | lora.rx_stop() 86 | counter += 1 87 | print('Send message {}'.format(counter)) 88 | lora.txc(P2P_MAGIC + bytes.fromhex('{:08x}'.format(counter))) 89 | 90 | except: # noqa: E722 91 | pass 92 | 93 | print('All done') 94 | exit(0) 95 | -------------------------------------------------------------------------------- /examples/p2p.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Simple demo of the RAK811 in P2P mode using the CLI interface 3 | # 4 | # This simple script sends random packets at random interval and 5 | # listen the rest of the time. 6 | # 7 | # Start this script on 2 or more nodes an observe the packets flowing. 8 | # 9 | # Copyright 2019, 2021 Philippe Vanhaesendonck 10 | # 11 | # Licensed under the Apache License, Version 2.0 (the "License"); 12 | # you may not use this file except in compliance with the License. 13 | # You may obtain a copy of the License at 14 | # 15 | # http://www.apache.org/licenses/LICENSE-2.0 16 | # 17 | # Unless required by applicable law or agreed to in writing, software 18 | # distributed under the License is distributed on an "AS IS" BASIS, 19 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | # See the License for the specific language governing permissions and 21 | # limitations under the License. 22 | # 23 | # SPDX-License-Identifier: Apache-2.0 24 | 25 | # Exit on errors 26 | set -e 27 | 28 | # Send packet every P2P_BASE + (0..P2P_RANDOM) seconds 29 | P2P_BASE=30 30 | P2P_RANDOM=60 31 | 32 | # Magic key to recognize our messages 33 | P2P_MAGIC="cafe" 34 | 35 | # Reset the module and set mode to P2P 36 | rak811 -v hard-reset 37 | rak811 -v mode LoRaP2P 38 | 39 | # Set the RF configuration 40 | # - Avoid LoRaWan channels (You will get quite a lot of spurious packets!) 41 | # - Respect local regulation (frequency, power, duty cycle) 42 | rak811 -v rf-config sf=7 freq=869.800 pwr=16 43 | # Display RF config 44 | rak811 -v rf-config 45 | 46 | # Enter the send/recieve loop 47 | COUNTER=0 48 | while true 49 | do 50 | # Calculate next message send timestamp 51 | NOW=$(date +%s) 52 | NEXT_MESSAGE=$(( NOW + P2P_BASE + ( RANDOM % P2P_RANDOM ) )) 53 | # Set module in receive mode 54 | rak811 -v rxc 55 | while [[ $(date +%s) -lt ${NEXT_MESSAGE} ]]; do 56 | # Remaining time until next message 57 | NOW=$(date +%s) 58 | REMAIN=$(( NEXT_MESSAGE - NOW )) 59 | echo "Waiting on message for ${REMAIN} seconds" 60 | MESSAGE=$(rak811 -v rx-get ${REMAIN}) 61 | if echo "${MESSAGE}" | grep -q ${P2P_MAGIC} 62 | then 63 | echo "Received valid message:" 64 | echo "${MESSAGE}" 65 | elif echo "${MESSAGE}" | grep -q Data 66 | then 67 | echo "Got foreign message" 68 | else 69 | echo "${MESSAGE}" 70 | fi 71 | done 72 | # Exit receive mode 73 | rak811 -v rx-stop 74 | # Send a message 75 | COUNTER=$(( COUNTER + 1 )) 76 | MESSAGE=$(printf '%s%08x' ${P2P_MAGIC} ${COUNTER}) 77 | rak811 -v txc --binary "${MESSAGE}" 78 | done 79 | -------------------------------------------------------------------------------- /examples/p2p_v3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """RAK811 P2P demo (V3 firmware). 3 | 4 | Send counter messages at random interval and listen the rest of the time. 5 | 6 | Start this script on 2 or more nodes an observe the packets flowing. 7 | 8 | Copyright 2019, 2021 Philippe Vanhaesendonck 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | 14 | http://www.apache.org/licenses/LICENSE-2.0 15 | 16 | Unless required by applicable law or agreed to in writing, software 17 | distributed under the License is distributed on an "AS IS" BASIS, 18 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | See the License for the specific language governing permissions and 20 | limitations under the License. 21 | 22 | SPDX-License-Identifier: Apache-2.0 23 | """ 24 | from random import randint 25 | from sys import exit 26 | from time import time 27 | 28 | from rak811.rak811_v3 import Rak811 29 | 30 | # Send packet every P2P_BASE + (0..P2P_RANDOM) seconds 31 | P2P_BASE = 30 32 | P2P_RANDOM = 60 33 | 34 | # Magic key to recognize our messages 35 | P2P_MAGIC = b'\xca\xfe' 36 | 37 | lora = Rak811() 38 | 39 | # Most of the setup should happen only once... 40 | print('Setup') 41 | # Set module in LoRa P2P mode 42 | response = lora.set_config('lora:work_mode:1') 43 | for r in response: 44 | print(r) 45 | 46 | # RF configuration 47 | # - Avoid LoRaWan channels (You will get quite a lot of spurious packets!) 48 | # - Respect local regulation (frequency, power, duty cycle) 49 | freq = 869.800 50 | sf = 7 51 | bw = 0 # 125KHz 52 | ci = 1 # 4/5 53 | pre = 8 54 | pwr = 16 55 | lora.set_config(f'lorap2p:{int(freq*1000*1000)}:{sf}:{bw}:{ci}:{pre}:{pwr}') 56 | 57 | print('Entering send/receive loop') 58 | counter = 0 59 | try: 60 | while True: 61 | # Calculate next message send timestamp 62 | next_send = time() + P2P_BASE + randint(0, P2P_RANDOM) 63 | # Set module in receive mode 64 | lora.set_config('lorap2p:transfer_mode:1') 65 | # Loop until we reach the next send time 66 | # Don't enter loop for small wait times (<1 1 sec.) 67 | while (time() + 1) < next_send: 68 | wait_time = next_send - time() 69 | print('Waiting on message for {:0.0f} seconds'.format(wait_time)) 70 | # Note that you don't have to listen actively for capturing message 71 | # Once in receive mode, the library will capture all messages sent. 72 | lora.receive_p2p(wait_time) 73 | while lora.nb_downlinks > 0: 74 | message = lora.get_downlink() 75 | data = message['data'] 76 | if data[:len(P2P_MAGIC)] == P2P_MAGIC: 77 | print( 78 | 'Received message: {}'.format( 79 | int.from_bytes(data[len(P2P_MAGIC):], 80 | byteorder='big') 81 | ) 82 | ) 83 | print('RSSI: {}, SNR: {}'.format(message['rssi'], 84 | message['snr'])) 85 | else: 86 | print('Foreign message received') 87 | # Time to send message 88 | counter += 1 89 | print('Send message {}'.format(counter)) 90 | # Set module in send mode 91 | lora.set_config('lorap2p:transfer_mode:2') 92 | lora.send_p2p(P2P_MAGIC + bytes.fromhex('{:08x}'.format(counter))) 93 | 94 | except: # noqa: E722 95 | pass 96 | 97 | print('All done') 98 | exit(0) 99 | -------------------------------------------------------------------------------- /examples/p2p_v3.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Simple demo of the rak811v3 in P2P mode using the CLI interface 3 | # (V3 firmware) 4 | # 5 | # This simple script sends random packets at random interval and 6 | # listen the rest of the time. 7 | # 8 | # Start this script on 2 or more nodes an observe the packets flowing. 9 | # 10 | # Copyright 2021 Philippe Vanhaesendonck 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); 13 | # you may not use this file except in compliance with the License. 14 | # You may obtain a copy of the License at 15 | # 16 | # http://www.apache.org/licenses/LICENSE-2.0 17 | # 18 | # Unless required by applicable law or agreed to in writing, software 19 | # distributed under the License is distributed on an "AS IS" BASIS, 20 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | # See the License for the specific language governing permissions and 22 | # limitations under the License. 23 | # 24 | # SPDX-License-Identifier: Apache-2.0 25 | 26 | # Exit on errors 27 | set -e 28 | 29 | # Send packet every P2P_BASE + (0..P2P_RANDOM) seconds 30 | P2P_BASE=30 31 | P2P_RANDOM=60 32 | 33 | # Magic key to recognize our messages 34 | P2P_MAGIC="cafe" 35 | 36 | # Set mode to P2P 37 | rak811v3 -v set-config 'lora:work_mode:1' 38 | 39 | # Set the RF configuration 40 | # - Avoid LoRaWan channels (You will get quite a lot of spurious packets!) 41 | # - Respect local regulation (frequency, power, duty cycle) 42 | freq=869800000 43 | sf=7 44 | bw=0 # 125KHz 45 | ci=1 # 4/5 46 | pre=8 47 | pwr=16 48 | rak811v3 -v set-config "lorap2p:${freq}:${sf}:${bw}:${ci}:${pre}:${pwr}" 49 | 50 | # Enter the send/recieve loop 51 | COUNTER=0 52 | while true 53 | do 54 | # Calculate next message send timestamp 55 | NOW=$(date +%s) 56 | NEXT_MESSAGE=$(( NOW + P2P_BASE + ( RANDOM % P2P_RANDOM ) )) 57 | # Set module in receive mode 58 | rak811v3 set-config 'lorap2p:transfer_mode:1' 59 | while [[ $(date +%s) -lt ${NEXT_MESSAGE} ]]; do 60 | # Remaining time until next message 61 | NOW=$(date +%s) 62 | REMAIN=$(( NEXT_MESSAGE - NOW )) 63 | echo "Waiting on message for ${REMAIN} seconds" 64 | MESSAGE=$(rak811v3 -v receive-p2p ${REMAIN}) 65 | if echo "${MESSAGE}" | grep -q ${P2P_MAGIC} 66 | then 67 | echo "Received valid message:" 68 | echo "${MESSAGE}" 69 | elif echo "${MESSAGE}" | grep -q Data 70 | then 71 | echo "Got foreign message" 72 | else 73 | echo "${MESSAGE}" 74 | fi 75 | done 76 | # Exit receive mode 77 | rak811v3 set-config 'lorap2p:transfer_mode:2' 78 | # Send a message 79 | COUNTER=$(( COUNTER + 1 )) 80 | MESSAGE=$(printf '%s%08x' ${P2P_MAGIC} ${COUNTER}) 81 | rak811v3 -v send-p2p --binary "${MESSAGE}" 82 | done 83 | -------------------------------------------------------------------------------- /examples/ttn_secrets_template.py: -------------------------------------------------------------------------------- 1 | """TTN secret keys template file. 2 | 3 | Copy this template to secrets.py and enter you keys 4 | 5 | Copyright 2019 Philippe Vanhaesendonck 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | SPDX-License-Identifier: Apache-2.0 20 | """ 21 | 22 | """OTAA Template.""" 23 | 24 | """Application EUI. 25 | This EUI must be in big-endian format, so most-significant-byte 26 | first. 27 | For TTN issued EUIs the first bytes should be 0x70, 0xB3, 0xD5. 28 | """ 29 | APP_EUI = '70B3D50000000000' 30 | 31 | """Application key. 32 | This key should be in big endian format (or, since it is not really a 33 | number but a block of memory, endianness does not really apply). In 34 | practice, a key taken from the TTN console can be copied as-is. 35 | """ 36 | APP_KEY = '00000000000000000000000000000000' 37 | 38 | """ABP Template.""" 39 | 40 | """Device address. 41 | The device address must be in big-endian format, so most-significant-byte 42 | first. 43 | For TTN issued addresses the first byte should be 0x26 44 | """ 45 | DEV_ADDR = '26000000' 46 | 47 | """Network Session Key. 48 | The device address must be in big-endian format, so most-significant-byte 49 | first. 50 | """ 51 | NWKS_KEY = '00000000000000000000000000000000' 52 | 53 | """App Session Key. 54 | The device address must be in big-endian format, so most-significant-byte 55 | first. 56 | """ 57 | APPS_KEY = '00000000000000000000000000000000' 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=45", 4 | "setuptools_scm[toml]>=6.2", 5 | "wheel", 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [tool.setuptools_scm] 10 | local_scheme = "no-local-version" -------------------------------------------------------------------------------- /rak811/__init__.py: -------------------------------------------------------------------------------- 1 | """Main package file. 2 | 3 | Import classes, exceptions and enums 4 | """ 5 | import pkg_resources 6 | 7 | from .exception import Rak811Error # noqa: F401 8 | 9 | try: 10 | __version__ = pkg_resources.get_distribution("rak811").version 11 | except Exception: 12 | __version__ = "unknown" 13 | -------------------------------------------------------------------------------- /rak811/cli.py: -------------------------------------------------------------------------------- 1 | """RAK811 CLI interface. 2 | 3 | Provides a command line interface for the RAK811 module (Firmware V2.0). 4 | 5 | Copyright 2019, 2021 Philippe Vanhaesendonck 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | SPDX-License-Identifier: Apache-2.0 20 | """ 21 | from json import dumps 22 | import logging 23 | 24 | import click 25 | 26 | from .rak811 import Mode, RecvEx, Reset 27 | from .rak811 import Rak811 28 | from .rak811 import Rak811Error 29 | from .rak811 import Rak811EventError, Rak811ResponseError, Rak811TimeoutError 30 | 31 | # Valid configuration keys for LoRaWan 32 | LW_CONFIG_KEYS = ('dev_addr', 'dev_eui', 'app_eui', 'app_key', 'nwks_key', 33 | 'apps_key', 'tx_power', 'pwr_level', 'adr', 'dr', 34 | 'public_net', 'rx_delay1', 'ch_list', 'ch_mask', 'max_chs', 35 | 'rx2', 'join_cnt', 'nbtrans', 'retrans', 'class', 'duty') 36 | 37 | # Valid configuration keys for LoRaP2P 38 | P2P_CONFIG_KEYS = { 39 | 'freq': click.FloatRange(min=860.000, max=929.900, clamp=False), 40 | 'sf': click.IntRange(min=6, max=12, clamp=False), 41 | 'bw': click.IntRange(min=0, max=2, clamp=False), 42 | 'cr': click.IntRange(min=1, max=4, clamp=False), 43 | 'prlen': click.IntRange(min=8, max=65535, clamp=False), 44 | 'pwr': click.IntRange(min=5, max=20, clamp=False) 45 | } 46 | 47 | 48 | class KeyValueParamTypeLW(click.ParamType): 49 | """Basic KEY=VALUE pair parameter type for LoRaWan.""" 50 | 51 | name = 'key-value-lorawan' 52 | 53 | def convert(self, value, param, ctx): 54 | try: 55 | (k, v) = value.split('=') 56 | k = k.lower() 57 | if k not in LW_CONFIG_KEYS: 58 | self.fail('{0} is not a valid config key'.format(k), 59 | param, 60 | ctx) 61 | return (k, v) 62 | except ValueError: 63 | self.fail('{0} is not a valid Key=Value parameter'.format(value), 64 | param, 65 | ctx) 66 | 67 | 68 | class KeyValueParamTypeP2P(click.ParamType): 69 | """Basic KEY=VALUE pair parameter type for LoRaP2P.""" 70 | 71 | name = 'key-value-p2p' 72 | 73 | def convert(self, value, param, ctx): 74 | try: 75 | (k, v) = value.split('=') 76 | k = k.lower() 77 | except ValueError: 78 | self.fail('{0} is not a valid Key=Value parameter'.format(value), 79 | param, 80 | ctx) 81 | if k not in P2P_CONFIG_KEYS: 82 | self.fail('{0} is not a valid config key'.format(k), 83 | param, 84 | ctx) 85 | v = P2P_CONFIG_KEYS[k].convert(v, param, ctx) 86 | return (k, v) 87 | 88 | 89 | def print_exception(e): 90 | """Print exception raised by the Rak811 library.""" 91 | if isinstance(e, Rak811ResponseError): 92 | click.echo('RAK811 response error {}: {}'.format(e.errno, e.strerror)) 93 | elif isinstance(e, Rak811EventError): 94 | click.echo('RAK811 event error {}: {}'.format(e.errno, e.strerror)) 95 | elif isinstance(e, Rak811TimeoutError): 96 | click.echo('RAK811 timeout: {}'.format(e)) 97 | else: 98 | click.echo('RAK811 unexpected exception {}'.format(e)) 99 | 100 | 101 | @click.group() 102 | @click.option( 103 | '-v', 104 | '--verbose', 105 | is_flag=True, 106 | help='Verbose mode' 107 | ) 108 | @click.option( 109 | '-d', 110 | '--debug', 111 | is_flag=True, 112 | help='Debug mode' 113 | ) 114 | @click.version_option() 115 | @click.pass_context 116 | def cli(ctx, verbose, debug): 117 | """Command line interface for the RAK811 module.""" 118 | ctx.ensure_object(dict) 119 | ctx.obj['VERBOSE'] = verbose 120 | logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) 121 | 122 | 123 | @cli.command(name='hard-reset') 124 | @click.pass_context 125 | def hard_reset(ctx): 126 | """Hardware reset of the module. 127 | 128 | Hard reset should not be required in normal operation. It needs to be 129 | issued once after host boot, or module restart. 130 | """ 131 | lora = Rak811() 132 | lora.hard_reset() 133 | if ctx.obj['VERBOSE']: 134 | click.echo('Hard reset complete') 135 | lora.close() 136 | 137 | 138 | """System commands.""" 139 | 140 | 141 | @cli.command() 142 | @click.pass_context 143 | def version(ctx): 144 | """Get module version.""" 145 | lora = Rak811() 146 | click.echo(lora.version) 147 | lora.close() 148 | 149 | 150 | @cli.command() 151 | @click.pass_context 152 | def sleep(ctx): 153 | """Enter sleep mode.""" 154 | lora = Rak811() 155 | lora.sleep() 156 | if ctx.obj['VERBOSE']: 157 | click.echo('Sleeping') 158 | lora.close() 159 | 160 | 161 | @cli.command(name='wake-up') 162 | @click.pass_context 163 | def wake_up(ctx): 164 | """Wake up.""" 165 | lora = Rak811() 166 | lora.wake_up() 167 | if ctx.obj['VERBOSE']: 168 | click.echo('Alive!') 169 | lora.close() 170 | 171 | 172 | @cli.command() 173 | @click.argument( 174 | 'reset_type', 175 | required=True, 176 | type=click.Choice(['module', 'lora']) 177 | ) 178 | @click.pass_context 179 | def reset(ctx, reset_type): 180 | """Reset Module or LoRaWan stack.""" 181 | lora = Rak811() 182 | if reset_type == 'module': 183 | lora.reset(Reset.Module) 184 | else: 185 | lora.reset(Reset.LoRa) 186 | if ctx.obj['VERBOSE']: 187 | click.echo('{0} reset complete.'.format( 188 | 'Module' if reset_type == 'module' else 'LoRa')) 189 | lora.close() 190 | 191 | 192 | @cli.command() 193 | @click.pass_context 194 | def reload(ctx): 195 | """Set LoRaWan or LoRaP2P configurations to default.""" 196 | lora = Rak811() 197 | lora.reload() 198 | if ctx.obj['VERBOSE']: 199 | click.echo('Configuration reloaded.') 200 | lora.close() 201 | 202 | 203 | @cli.command() 204 | @click.argument( 205 | 'mode', 206 | required=False, 207 | type=click.Choice(['LoRaWan', 'LoRaP2P'], case_sensitive=False) 208 | ) 209 | @click.pass_context 210 | def mode(ctx, mode): 211 | """Get/Set mode to LoRaWan or LoRaP2P.""" 212 | lora = Rak811() 213 | if mode is None: 214 | click.echo('LoRaWan' if lora.mode == Mode.LoRaWan else 'LoRaP2P') 215 | else: 216 | mode = mode.lower() 217 | if mode == 'lorawan': 218 | lora.mode = Mode.LoRaWan 219 | else: 220 | lora.mode = Mode.LoRaP2P 221 | if ctx.obj['VERBOSE']: 222 | click.echo('Mode set to {0}.'.format( 223 | 'LoRaWan' if mode == 'lorawan' else 'LoRaP2P')) 224 | lora.close() 225 | 226 | 227 | @cli.command() 228 | @click.argument( 229 | 'recv_ex', 230 | required=False, 231 | type=click.Choice(['enable', 'disable']) 232 | ) 233 | @click.pass_context 234 | def recv_ex(ctx, recv_ex): 235 | """RSSI & SNR report on receive.""" 236 | lora = Rak811() 237 | if recv_ex is None: 238 | click.echo('Enabled' if lora.recv_ex == RecvEx.Enabled else 'Disabled') 239 | else: 240 | lora.recv_ex = ( 241 | RecvEx.Enabled if recv_ex == 'enable' else RecvEx.Disabled 242 | ) 243 | if ctx.obj['VERBOSE']: 244 | click.echo('RSSI & SNR report on receive {0}.'.format( 245 | 'Enabled' if recv_ex == 'enable' else 'Disabled')) 246 | lora.close() 247 | 248 | 249 | """LoRaWan commands.""" 250 | 251 | 252 | @cli.command() 253 | @click.argument( 254 | 'band', 255 | required=False, 256 | type=click.Choice( 257 | ['EU868', 'US915', 'AU915', 'KR920', 'AS923', 'IN865'], 258 | case_sensitive=False 259 | ) 260 | ) 261 | @click.pass_context 262 | def band(ctx, band): 263 | """Get/Set LoRaWan region.""" 264 | lora = Rak811() 265 | if band is None: 266 | click.echo(lora.band) 267 | else: 268 | band = band.upper() 269 | lora.band = band 270 | if ctx.obj['VERBOSE']: 271 | click.echo('LoRaWan region set to {0}.'.format(band)) 272 | lora.close() 273 | 274 | 275 | @cli.command() 276 | @click.argument( 277 | 'key_values', 278 | metavar='KEY=VALUE...', 279 | required=True, 280 | type=KeyValueParamTypeLW(), 281 | nargs=-1 282 | ) 283 | @click.pass_context 284 | def set_config(ctx, key_values): 285 | """Set LoraWAN configuration. 286 | 287 | \b 288 | Arguments are specified as KEY=VALUE pairs, e.g.: 289 | set-config app_eui='APP_EUI' app_key='APP_KEY' 290 | """ 291 | lora = Rak811() 292 | kv_args = dict(key_values) 293 | try: 294 | lora.set_config(**kv_args) 295 | if ctx.obj['VERBOSE']: 296 | click.echo('LoRaWan parameters set') 297 | except Rak811Error as e: 298 | print_exception(e) 299 | lora.close() 300 | 301 | 302 | @cli.command() 303 | @click.argument( 304 | 'key', 305 | required=True, 306 | type=click.Choice(LW_CONFIG_KEYS) 307 | ) 308 | @click.pass_context 309 | def get_config(ctx, key): 310 | """Get LoraWan configuration.""" 311 | lora = Rak811() 312 | try: 313 | click.echo(lora.get_config(key)) 314 | except Rak811Error as e: 315 | print_exception(e) 316 | lora.close() 317 | 318 | 319 | @cli.command() 320 | @click.pass_context 321 | def join_otaa(ctx): 322 | """Join the configured network in OTAA mode.""" 323 | lora = Rak811() 324 | try: 325 | lora.join_otaa() 326 | if ctx.obj['VERBOSE']: 327 | click.echo('Joined in OTAA mode') 328 | except Rak811Error as e: 329 | print_exception(e) 330 | lora.close() 331 | 332 | 333 | @cli.command() 334 | @click.pass_context 335 | def join_abp(ctx): 336 | """Join the configured network in ABP mode.""" 337 | lora = Rak811() 338 | try: 339 | lora.join_abp() 340 | if ctx.obj['VERBOSE']: 341 | click.echo('Joined in ABP mode') 342 | except Rak811Error as e: 343 | print_exception(e) 344 | lora.close() 345 | 346 | 347 | @cli.command() 348 | @click.pass_context 349 | def signal(ctx): 350 | """Get (RSSI,SNR) from latest received packet.""" 351 | lora = Rak811() 352 | (rssi, snr) = lora.signal 353 | if ctx.obj['VERBOSE']: 354 | click.echo('RSSI: {0} - SNR: {1}'.format(rssi, snr)) 355 | else: 356 | click.echo('{} {}'.format(rssi, snr)) 357 | lora.close() 358 | 359 | 360 | @cli.command() 361 | @click.argument( 362 | 'dr', 363 | required=False, 364 | type=click.INT 365 | ) 366 | @click.pass_context 367 | def dr(ctx, dr): 368 | """Get/Set next send data rate.""" 369 | lora = Rak811() 370 | if dr is None: 371 | click.echo(lora.dr) 372 | else: 373 | try: 374 | lora.dr = dr 375 | if ctx.obj['VERBOSE']: 376 | click.echo('Data rate set to {0}.'.format(dr)) 377 | except Rak811Error as e: 378 | print_exception(e) 379 | lora.close() 380 | 381 | 382 | @cli.command() 383 | @click.pass_context 384 | def link_cnt(ctx): 385 | """Get up & downlink counters.""" 386 | lora = Rak811() 387 | (uplink, downlink) = lora.link_cnt 388 | if ctx.obj['VERBOSE']: 389 | click.echo('Uplink: {0} - Downlink: {1}'.format(uplink, downlink)) 390 | else: 391 | click.echo('{} {}'.format(uplink, downlink)) 392 | lora.close() 393 | 394 | 395 | @cli.command() 396 | @click.pass_context 397 | def abp_info(ctx): 398 | """Get ABP info. 399 | 400 | When using OTAA, returns the necessary info to re-join in ABP mode. The 401 | following tuple is returned: (NetworkID, DevAddr, Nwkskey, Appskey) 402 | """ 403 | lora = Rak811() 404 | (nwk_id, dev_addr, nwks_key, apps_key) = lora.abp_info 405 | if ctx.obj['VERBOSE']: 406 | click.echo('NwkId: {}'.format(nwk_id)) 407 | click.echo('DevAddr: {}'.format(dev_addr)) 408 | click.echo('Nwkskey: {}'.format(nwks_key)) 409 | click.echo('Appskey: {}'.format(apps_key)) 410 | else: 411 | click.echo('{} {} {} {}'.format(nwk_id, dev_addr, nwks_key, apps_key)) 412 | lora.close() 413 | 414 | 415 | @cli.command() 416 | @click.option( 417 | '-p', '--port', 418 | default=1, 419 | type=click.IntRange(1, 223), 420 | help='port number to use (1-223)' 421 | ) 422 | @click.option( 423 | '--confirm', 424 | is_flag=True, 425 | help='regular or confirmed send' 426 | ) 427 | @click.option( 428 | '--binary', 429 | is_flag=True, 430 | help='Data is binary (hex encoded)' 431 | ) 432 | @click.argument( 433 | 'data', 434 | required=True 435 | ) 436 | @click.option( 437 | '--json', 438 | is_flag=True, 439 | help='Output downlink in JSON format' 440 | ) 441 | @click.pass_context 442 | def send(ctx, port, confirm, binary, data, json): 443 | """Send LoRaWan message and check for downlink.""" 444 | if binary: 445 | try: 446 | data = bytes.fromhex(data) 447 | except ValueError: 448 | click.echo('Invalid binary data') 449 | return 450 | lora = Rak811() 451 | try: 452 | lora.send(data, confirm=confirm, port=port) 453 | except Rak811Error as e: 454 | print_exception(e) 455 | lora.close() 456 | return 457 | 458 | if ctx.obj['VERBOSE']: 459 | click.echo('Message sent.') 460 | if lora.nb_downlinks: 461 | downlink = lora.get_downlink() 462 | downlink['data'] = downlink['data'].hex() 463 | if json: 464 | click.echo(dumps(downlink, indent=4)) 465 | elif ctx.obj['VERBOSE']: 466 | click.echo('Downlink received:') 467 | click.echo('Port: {}'.format(downlink['port'])) 468 | if downlink['rssi']: 469 | click.echo('RSSI: {}'.format(downlink['rssi'])) 470 | click.echo('SNR: {}'.format(downlink['snr'])) 471 | click.echo('Data: {}'.format(downlink['data'])) 472 | else: 473 | click.echo(downlink['data']) 474 | elif ctx.obj['VERBOSE']: 475 | click.echo('No downlink available.') 476 | lora.close() 477 | 478 | 479 | @cli.command() 480 | @click.argument( 481 | 'key_values', 482 | metavar='KEY=VALUE...', 483 | required=False, 484 | type=KeyValueParamTypeP2P(), 485 | nargs=-1 486 | ) 487 | @click.pass_context 488 | def rf_config(ctx, key_values): 489 | """Get/Set LoraP2P configuration. 490 | 491 | \b 492 | Without argument, returns: 493 | frequency, sf, bw, cr, prlen, pwr 494 | 495 | \b 496 | Otherwise set rf_config, Arguments are specified as KEY=VALUE pairs: 497 | freq: frequency in MHz (860.000-929.900) 498 | sf: strength factor (6-12) 499 | bw: bandwidth (0:125KHz, 1:250KHz, 2:500KHz) 500 | cr: coding rate (1:4/5, 2:4/6, 3:4/7, 4:4/8) 501 | prlen: preamble length default (8-65535) 502 | pwr: Tx power (5-20) 503 | E.g.: rf-config freq=860.100 sf=7 pwr=16 504 | 505 | """ 506 | lora = Rak811() 507 | config = dict(key_values) 508 | if config == {}: 509 | # No parameters: returns rc_config 510 | config = lora.rf_config 511 | if ctx.obj['VERBOSE']: 512 | click.echo('Frequency: {}'.format(config['freq'])) 513 | click.echo('SF: {}'.format(config['sf'])) 514 | click.echo('BW: {}'.format(config['bw'])) 515 | click.echo('CR: {}'.format(config['cr'])) 516 | click.echo('PrLen: {}'.format(config['prlen'])) 517 | click.echo('Power: {}'.format(config['pwr'])) 518 | else: 519 | click.echo('{} {} {} {} {} {}'.format( 520 | config['freq'], config['sf'], config['bw'], config['cr'], 521 | config['prlen'], config['pwr'] 522 | )) 523 | else: 524 | # At least a parameter, set rc_config 525 | lora.rf_config = config 526 | if ctx.obj['VERBOSE']: 527 | click.echo('rf_config set: ' + ', '.join('{}={}'.format(k, v) for 528 | k, v in config.items())) 529 | 530 | lora.close() 531 | 532 | 533 | @cli.command() 534 | @click.option( 535 | '--cnt', 536 | default=1, 537 | type=click.IntRange(1, 65535), 538 | help='tx counts (1-65535)' 539 | ) 540 | @click.option( 541 | '--interval', 542 | default=60, 543 | type=click.IntRange(1, 3600), 544 | help=' tx interval (1-3600)' 545 | ) 546 | @click.option( 547 | '--binary', 548 | is_flag=True, 549 | help='Data is binary (hex encoded)' 550 | ) 551 | @click.argument( 552 | 'data', 553 | required=True 554 | ) 555 | @click.pass_context 556 | def txc(ctx, cnt, interval, binary, data): 557 | """Send LoRaP2P message.""" 558 | if binary: 559 | try: 560 | data = bytes.fromhex(data) 561 | except ValueError: 562 | click.echo('Invalid binary data') 563 | return 564 | lora = Rak811() 565 | try: 566 | lora.txc(data, cnt=cnt, interval=interval) 567 | except Rak811Error as e: 568 | print_exception(e) 569 | lora.close() 570 | return 571 | 572 | if ctx.obj['VERBOSE']: 573 | click.echo('Message sent.') 574 | lora.close() 575 | 576 | 577 | @cli.command() 578 | @click.pass_context 579 | def rxc(ctx): 580 | """Set module in LoraP2P receive mode.""" 581 | lora = Rak811() 582 | lora.rxc() 583 | if ctx.obj['VERBOSE']: 584 | click.echo('Module set in receive mode.') 585 | lora.close() 586 | 587 | 588 | @cli.command() 589 | @click.pass_context 590 | def tx_stop(ctx): 591 | """Stop LoraP2P TX.""" 592 | lora = Rak811() 593 | lora.tx_stop() 594 | if ctx.obj['VERBOSE']: 595 | click.echo('LoraP2P TX stopped.') 596 | lora.close() 597 | 598 | 599 | @cli.command() 600 | @click.pass_context 601 | def rx_stop(ctx): 602 | """Stop LoraP2P RX.""" 603 | lora = Rak811() 604 | lora.rx_stop() 605 | if ctx.obj['VERBOSE']: 606 | click.echo('LoraP2P RX stopped.') 607 | lora.close() 608 | 609 | 610 | @cli.command() 611 | @click.argument( 612 | 'timeout', 613 | required=False, 614 | default=60, 615 | type=click.INT 616 | ) 617 | @click.option( 618 | '--json', 619 | is_flag=True, 620 | help='Output message in JSON format' 621 | ) 622 | @click.pass_context 623 | def rx_get(ctx, timeout, json): 624 | """Get LoraP2P message.""" 625 | lora = Rak811() 626 | lora.rx_get(timeout) 627 | if lora.nb_downlinks: 628 | rx = lora.get_downlink() 629 | rx['data'] = rx['data'].hex() 630 | if json: 631 | click.echo(dumps(rx, indent=4)) 632 | elif ctx.obj['VERBOSE']: 633 | click.echo('Message received:') 634 | if rx['rssi']: 635 | click.echo('RSSI: {}'.format(rx['rssi'])) 636 | click.echo('SNR: {}'.format(rx['snr'])) 637 | click.echo('Data: {}'.format(rx['data'])) 638 | else: 639 | click.echo(rx['data']) 640 | elif ctx.obj['VERBOSE']: 641 | click.echo('No message available.') 642 | lora.close() 643 | 644 | 645 | @cli.command() 646 | @click.pass_context 647 | def radio_status(ctx): 648 | """Get radio statistics. 649 | 650 | Returns: TxSuccessCnt, TxErrCnt, RxSuccessCnt, RxTimeOutCnt, RxErrCnt, 651 | Rssi, Snr. 652 | """ 653 | lora = Rak811() 654 | ( 655 | tx_success_cnt, tx_err_cnt, 656 | rx_success_cnt, rx_timeout_cnt, rx_err_cnt, 657 | rssi, snr 658 | ) = lora.radio_status 659 | if ctx.obj['VERBOSE']: 660 | click.echo('TxSuccessCnt: {}'.format(tx_success_cnt)) 661 | click.echo('TxErrCnt: {}'.format(tx_err_cnt)) 662 | click.echo('RxSuccessCnt: {}'.format(rx_success_cnt)) 663 | click.echo('RxTimeOutCnt: {}'.format(rx_timeout_cnt)) 664 | click.echo('RxErrCnt: {}'.format(rx_err_cnt)) 665 | click.echo('RSSI: {}'.format(rssi)) 666 | click.echo('SNR: {}'.format(snr)) 667 | else: 668 | click.echo('{} {} {} {} {} {} {}'.format( 669 | tx_success_cnt, tx_err_cnt, 670 | rx_success_cnt, rx_timeout_cnt, rx_err_cnt, 671 | rssi, snr 672 | )) 673 | lora.close() 674 | 675 | 676 | @cli.command() 677 | @click.pass_context 678 | def clear_radio_status(ctx): 679 | """Clear radio statistics.""" 680 | lora = Rak811() 681 | lora.clear_radio_status() 682 | if ctx.obj['VERBOSE']: 683 | click.echo('Radio statistics cleared.') 684 | lora.close() 685 | 686 | 687 | if __name__ == '__main__': 688 | cli() 689 | -------------------------------------------------------------------------------- /rak811/cli_v3.py: -------------------------------------------------------------------------------- 1 | """RAK811 CLI interface. 2 | 3 | Provides a command line interface for the RAK811 module (Firmware V3.0). 4 | 5 | Copyright 2021 Philippe Vanhaesendonck 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | SPDX-License-Identifier: Apache-2.0 20 | """ 21 | from json import dumps 22 | import logging 23 | 24 | import click 25 | 26 | from .rak811_v3 import Rak811 27 | from .rak811_v3 import Rak811Error 28 | from .rak811_v3 import Rak811ResponseError, Rak811TimeoutError 29 | 30 | 31 | def print_exception(e): 32 | """Print exception raised by the Rak811 library.""" 33 | if isinstance(e, Rak811ResponseError): 34 | click.echo('RAK811 response error {}: {}'.format(e.errno, e.strerror)) 35 | elif isinstance(e, Rak811TimeoutError): 36 | click.echo('RAK811 timeout: {}'.format(e)) 37 | else: 38 | click.echo('RAK811 unexpected exception {}'.format(e)) 39 | 40 | 41 | @click.group() 42 | @click.option( 43 | '-v', 44 | '--verbose', 45 | is_flag=True, 46 | help='Verbose mode' 47 | ) 48 | @click.option( 49 | '-d', 50 | '--debug', 51 | is_flag=True, 52 | help='Debug mode' 53 | ) 54 | @click.version_option() 55 | @click.pass_context 56 | def cli(ctx, verbose, debug): 57 | """Command line interface for the RAK811 module.""" 58 | ctx.ensure_object(dict) 59 | ctx.obj['VERBOSE'] = verbose 60 | logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) 61 | 62 | 63 | @cli.command(name='hard-reset') 64 | @click.pass_context 65 | def hard_reset(ctx): 66 | """Hardware reset of the module. 67 | 68 | Hard reset should not be required in normal operation. It needs to be 69 | issued once after host boot, or module restart. 70 | """ 71 | lora = Rak811() 72 | lora.hard_reset() 73 | if ctx.obj['VERBOSE']: 74 | click.echo('Hard reset complete') 75 | lora.close() 76 | 77 | 78 | """Get / set commands.""" 79 | 80 | 81 | @cli.command() 82 | @click.argument( 83 | 'config_item', 84 | required=True 85 | ) 86 | @click.pass_context 87 | def set_config(ctx, config_item): 88 | """Execute set_config RAK811 command. 89 | 90 | \b 91 | Config items are in the format :[:]... 92 | Supported types and topics: 93 | - device: restart, sleep, boot, status, uart, uart_mode, gpio 94 | - lora: region, channel, dev_eui, app_eui, app_key, dev_addr, 95 | apps_key, nwks_key, join_mode, work_mode, ch_mask, class, 96 | confirm, dr, tx_power, adr, send_interval 97 | - lorap2p: transfer_mode, channel configuration 98 | """ 99 | lora = Rak811() 100 | try: 101 | responses = lora.set_config(config_item) 102 | if ctx.obj['VERBOSE']: 103 | click.echo('Configuration done') 104 | for response in responses: 105 | if response.strip(): 106 | click.echo(response) 107 | except Rak811Error as e: 108 | print_exception(e) 109 | lora.close() 110 | 111 | 112 | @cli.command() 113 | @click.argument( 114 | 'config_item', 115 | required=True 116 | ) 117 | @click.pass_context 118 | def get_config(ctx, config_item): 119 | """Execute get_config RAK811 command. 120 | 121 | \b 122 | Config items are in the format :[:] 123 | Supported types and topics: 124 | - device: status, gpio, adc 125 | - lora: channel, status 126 | """ 127 | lora = Rak811() 128 | try: 129 | responses = lora.get_config(config_item) 130 | for response in responses: 131 | if response.strip(): 132 | click.echo(response) 133 | except Rak811Error as e: 134 | print_exception(e) 135 | lora.close() 136 | 137 | 138 | """General AT commands.""" 139 | 140 | 141 | @cli.command() 142 | @click.pass_context 143 | def version(ctx): 144 | """Get module version.""" 145 | lora = Rak811() 146 | click.echo(lora.version) 147 | lora.close() 148 | 149 | 150 | @cli.command() 151 | @click.pass_context 152 | def help(ctx): 153 | """Print module help.""" 154 | lora = Rak811() 155 | for response in lora.help: 156 | click.echo(response) 157 | lora.close() 158 | 159 | 160 | @cli.command() 161 | @click.pass_context 162 | def run(ctx): 163 | """Exit boot mode and enter normal mode.""" 164 | lora = Rak811() 165 | lora.run() 166 | lora.close() 167 | 168 | 169 | """ Interface commands.""" 170 | 171 | 172 | @cli.command() 173 | @click.option( 174 | '-i', '--index', 175 | default='3', 176 | type=click.Choice(['1', '3']), 177 | help='UART Index (1 or 3, default 3)' 178 | ) 179 | @click.option( 180 | '--binary', 181 | is_flag=True, 182 | help='Data is binary (hex encoded)' 183 | ) 184 | @click.argument( 185 | 'data', 186 | required=True 187 | ) 188 | @click.pass_context 189 | def send_uart(ctx, index, binary, data): 190 | """Send data to UART. 191 | 192 | UART1 is the AT Command interface, so you probably want to use 193 | UART3! 194 | """ 195 | if binary: 196 | try: 197 | data = bytes.fromhex(data) 198 | except ValueError: 199 | click.echo('Invalid binary data') 200 | return 201 | lora = Rak811() 202 | try: 203 | lora.send_uart(data, int(index)) 204 | except Rak811Error as e: 205 | print_exception(e) 206 | lora.close() 207 | return 208 | 209 | if ctx.obj['VERBOSE']: 210 | click.echo('Data sent.') 211 | 212 | 213 | """LoRaWan commands.""" 214 | 215 | 216 | @cli.command() 217 | @click.pass_context 218 | def join(ctx): 219 | """Join the configured network. 220 | 221 | \b 222 | ABP requires the following parameters to be set prior join: 223 | - dev_addr 224 | - nwks_key 225 | - apps_key 226 | 227 | OTAA requires the following parameters to be set prior join: 228 | - dev_eui 229 | - app_eui 230 | - app_key 231 | """ 232 | lora = Rak811() 233 | try: 234 | lora.join() 235 | if ctx.obj['VERBOSE']: 236 | click.echo('Joined!') 237 | except Rak811Error as e: 238 | print_exception(e) 239 | lora.close() 240 | 241 | 242 | @cli.command() 243 | @click.option( 244 | '-p', '--port', 245 | default=1, 246 | type=click.IntRange(1, 223), 247 | help='port number to use (1-223)' 248 | ) 249 | @click.option( 250 | '--binary', 251 | is_flag=True, 252 | help='Data is binary (hex encoded)' 253 | ) 254 | @click.argument( 255 | 'data', 256 | required=True 257 | ) 258 | @click.option( 259 | '--json', 260 | is_flag=True, 261 | help='Output downlink in JSON format' 262 | ) 263 | @click.pass_context 264 | def send(ctx, port, binary, data, json): 265 | """Send LoRaWan message and check for downlink.""" 266 | if binary: 267 | try: 268 | data = bytes.fromhex(data) 269 | except ValueError: 270 | click.echo('Invalid binary data') 271 | return 272 | lora = Rak811() 273 | try: 274 | lora.send(data, port=port) 275 | except Rak811Error as e: 276 | print_exception(e) 277 | lora.close() 278 | return 279 | 280 | if ctx.obj['VERBOSE']: 281 | click.echo('Message sent.') 282 | if lora.nb_downlinks: 283 | downlink = lora.get_downlink() 284 | if downlink['len']: 285 | downlink['data'] = downlink['data'].hex() 286 | if json: 287 | click.echo(dumps(downlink, indent=4)) 288 | elif ctx.obj['VERBOSE']: 289 | click.echo('Downlink received:') 290 | click.echo('Port: {}'.format(downlink['port'])) 291 | click.echo('RSSI: {}'.format(downlink['rssi'])) 292 | click.echo('SNR: {}'.format(downlink['snr'])) 293 | click.echo('Data: {}'.format(downlink['data'])) 294 | else: 295 | click.echo(downlink['data']) 296 | elif ctx.obj['VERBOSE']: 297 | click.echo('Send confirmed.') 298 | click.echo('RSSI: {}'.format(downlink['rssi'])) 299 | click.echo('SNR: {}'.format(downlink['snr'])) 300 | elif ctx.obj['VERBOSE']: 301 | click.echo('No downlink available.') 302 | lora.close() 303 | 304 | 305 | @cli.command() 306 | @click.option( 307 | '--binary', 308 | is_flag=True, 309 | help='Data is binary (hex encoded)' 310 | ) 311 | @click.argument( 312 | 'data', 313 | required=True 314 | ) 315 | @click.pass_context 316 | def send_p2p(ctx, binary, data): 317 | """Send LoRa P2P message.""" 318 | if binary: 319 | try: 320 | data = bytes.fromhex(data) 321 | except ValueError: 322 | click.echo('Invalid binary data') 323 | return 324 | lora = Rak811() 325 | try: 326 | lora.send_p2p(data) 327 | except Rak811Error as e: 328 | print_exception(e) 329 | lora.close() 330 | return 331 | 332 | if ctx.obj['VERBOSE']: 333 | click.echo('Message sent.') 334 | 335 | 336 | @cli.command() 337 | @click.argument( 338 | 'timeout', 339 | required=False, 340 | default=60, 341 | type=click.INT 342 | ) 343 | @click.option( 344 | '--json', 345 | is_flag=True, 346 | help='Output message in JSON format' 347 | ) 348 | @click.pass_context 349 | def receive_p2p(ctx, timeout, json): 350 | """Get LoraP2P message.""" 351 | lora = Rak811() 352 | lora.receive_p2p(timeout) 353 | if lora.nb_downlinks: 354 | rx = lora.get_downlink() 355 | rx['data'] = rx['data'].hex() 356 | if json: 357 | click.echo(dumps(rx, indent=4)) 358 | elif ctx.obj['VERBOSE']: 359 | click.echo('Message received:') 360 | click.echo('RSSI: {}'.format(rx['rssi'])) 361 | click.echo('SNR: {}'.format(rx['snr'])) 362 | click.echo('Data: {}'.format(rx['data'])) 363 | else: 364 | click.echo(rx['data']) 365 | elif ctx.obj['VERBOSE']: 366 | click.echo('No message available.') 367 | lora.close() 368 | 369 | 370 | if __name__ == '__main__': 371 | cli() 372 | -------------------------------------------------------------------------------- /rak811/exception.py: -------------------------------------------------------------------------------- 1 | """Exceptions definition. 2 | 3 | Copyright 2019 Philippe Vanhaesendonck 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | 20 | 21 | class Rak811Error(Exception): 22 | """Generic exception class for this package.""" 23 | 24 | pass 25 | -------------------------------------------------------------------------------- /rak811/rak811.py: -------------------------------------------------------------------------------- 1 | """Interface with the RAK811 module (Firmware V2.0). 2 | 3 | Copyright 2019, 2021 Philippe Vanhaesendonck 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | from binascii import hexlify 20 | from enum import IntEnum 21 | from time import sleep 22 | 23 | from RPi import GPIO 24 | 25 | from .exception import Rak811Error 26 | from .serial import Rak811Serial, Rak811TimeoutError 27 | 28 | RESET_BCM_PORT = 17 29 | RESET_DELAY = 0.01 30 | RESET_POST = 2 31 | RESPONSE_OK = 'OK' 32 | RESPONSE_ERROR = 'ERROR' 33 | RESPONSE_EVENT = 'at+recv=' 34 | 35 | # Timeout for response and events 36 | # The RAK811 typically respond in less than 1.5 seconds 37 | RESPONSE_TIMEOUT = 5 38 | # Event wait time strongly depends on duty cycle, when sending often at high SF 39 | # the module will wait to respect the duty cycle. 40 | # In normal operation, 5 minutes should be more than enough. 41 | EVENT_TIMEOUT = 5 * 60 42 | 43 | 44 | # RAK811 error codes and associated messages 45 | class ErrorCode(IntEnum): 46 | """AT commands error codes.""" 47 | 48 | ARG_ERR = -1 49 | ARG_NOT_FIND = -2 50 | JOIN_ABP_ERR = -3 51 | JOIN_OTAA_ERR = -4 52 | NOT_JOIN = -5 53 | MAC_BUSY_ERR = -6 54 | TX_ERR = -7 55 | INTER_ERR = -8 56 | WR_CFG_ERR = -11 57 | RD_CFG_ERR = -12 58 | TX_LEN_LIMIT_ERR = -13 59 | UNKNOWN_ERR = -20 60 | 61 | 62 | ERROR_MESSAGE = { 63 | ErrorCode.ARG_ERR: 'Invalid argument', 64 | ErrorCode.ARG_NOT_FIND: 'Argument not found', 65 | ErrorCode.JOIN_ABP_ERR: 'ABP join error', 66 | ErrorCode.JOIN_OTAA_ERR: 'OTAA join error', 67 | ErrorCode.NOT_JOIN: 'Not joined', 68 | ErrorCode.MAC_BUSY_ERR: 'MAC busy', 69 | ErrorCode.TX_ERR: 'Transmit error', 70 | ErrorCode.INTER_ERR: 'Inter error', 71 | ErrorCode.WR_CFG_ERR: 'Write configuration error', 72 | ErrorCode.RD_CFG_ERR: 'Read configuration Error', 73 | ErrorCode.TX_LEN_LIMIT_ERR: 'Transmit len limit error', 74 | ErrorCode.UNKNOWN_ERR: 'Unknown error', 75 | } 76 | 77 | 78 | class EventCode(IntEnum): 79 | """AT commands event codes.""" 80 | 81 | RECV_DATA = 0 82 | TX_COMFIRMED = 1 83 | TX_UNCONFIRMED = 2 84 | JOINED_SUCCESS = 3 85 | JOINED_FAILED = 4 86 | TX_TIMEOUT = 5 87 | RX2_TIMEOUT = 6 88 | DOWNLINK_REPEATED = 7 89 | WAKE_UP = 8 90 | P2PTX_COMPLETE = 9 91 | UNKNOWN = 100 92 | 93 | 94 | EVENT_MESSAGE = { 95 | EventCode.RECV_DATA: 'Received data', 96 | EventCode.TX_COMFIRMED: 'Tx confirmed', 97 | EventCode.TX_UNCONFIRMED: 'Tx unconfirmed', 98 | EventCode.JOINED_SUCCESS: 'Join succeeded', 99 | EventCode.JOINED_FAILED: 'Join failed', 100 | EventCode.TX_TIMEOUT: 'Tx timeout', 101 | EventCode.RX2_TIMEOUT: 'Rx2 timeout', 102 | EventCode.DOWNLINK_REPEATED: 'Downlink repeated', 103 | EventCode.WAKE_UP: 'Wake up', 104 | EventCode.P2PTX_COMPLETE: 'P2P tx complete', 105 | EventCode.UNKNOWN: 'Unknown', 106 | } 107 | 108 | 109 | class Mode(IntEnum): 110 | """Module operation mode (LoRaWan or Peer to Peer).""" 111 | 112 | LoRaWan = 0 113 | LoRaP2P = 1 114 | 115 | 116 | class Reset(IntEnum): 117 | """Module reset type.""" 118 | 119 | Module = 0 120 | LoRa = 1 121 | 122 | 123 | class RecvEx(IntEnum): 124 | """Receive RSSI/SNR data with downlink packets.""" 125 | 126 | Enabled = 0 127 | Disabled = 1 128 | 129 | 130 | class Rak811ResponseError(Rak811Error): 131 | """Exception raised by response from the module. 132 | 133 | Attributes: 134 | errno -- as returned by the module 135 | strerror -- textual representation 136 | 137 | """ 138 | 139 | def __init__(self, code): 140 | """Just assign return codes.""" 141 | try: 142 | self.errno = int(code) 143 | except ValueError: 144 | self.errno = code 145 | 146 | if self.errno in ERROR_MESSAGE: 147 | self.strerror = ERROR_MESSAGE[self.errno] 148 | else: 149 | self.strerror = ERROR_MESSAGE[ErrorCode.UNKNOWN_ERR] 150 | super().__init__(('[Errno {}] {}').format(self.errno, self.strerror)) 151 | 152 | 153 | class Rak811EventError(Rak811Error): 154 | """Exception raised by module events. 155 | 156 | Attributes: 157 | errno -- as returned by the module 158 | strerror -- textual representation 159 | 160 | """ 161 | 162 | def __init__(self, status): 163 | """Just assign return status.""" 164 | try: 165 | self.errno = int(status) 166 | except ValueError: 167 | self.errno = status 168 | 169 | if self.errno in EVENT_MESSAGE: 170 | self.strerror = EVENT_MESSAGE[self.errno] 171 | else: 172 | self.strerror = EVENT_MESSAGE[EventCode.UNKNOWN] 173 | super().__init__(('[Errno {}] {}').format(self.errno, self.strerror)) 174 | 175 | 176 | class Rak811(object): 177 | """Main class.""" 178 | 179 | def __init__(self, **kwargs): 180 | """Initialise class. 181 | 182 | The serial port is immediately opened and flushed. 183 | All parameters are optional and passed to RackSerial. 184 | """ 185 | read_buffer_timeout = kwargs.pop('response_timeout', RESPONSE_TIMEOUT) 186 | self._event_timeout = kwargs.pop('event_timeout', EVENT_TIMEOUT) 187 | self._serial = Rak811Serial( 188 | read_buffer_timeout=read_buffer_timeout, 189 | **kwargs 190 | ) 191 | self._downlink = [] 192 | 193 | def close(self): 194 | """Terminates session. 195 | 196 | Terminates read thread and close serial port. 197 | """ 198 | self._serial.close() 199 | 200 | def hard_reset(self): 201 | """Hard reset of the RAK811 module. 202 | 203 | Hard reset should not be required in normal operation. It needs to be 204 | issued once after host boot, or module restart. 205 | Note that we do not cleanup() as the reset port should stay high (it is 206 | configured that way at boot time). 207 | """ 208 | GPIO.setwarnings(False) 209 | GPIO.setmode(GPIO.BCM) 210 | GPIO.setup(RESET_BCM_PORT, GPIO.OUT) 211 | GPIO.output(RESET_BCM_PORT, GPIO.LOW) 212 | sleep(RESET_DELAY) 213 | GPIO.output(RESET_BCM_PORT, GPIO.HIGH) 214 | sleep(RESET_POST) 215 | 216 | def _int(self, i): 217 | """Attempt int conversion.""" 218 | try: 219 | i = int(i) 220 | except ValueError: 221 | pass 222 | return i 223 | 224 | def _send_string(self, string): 225 | """Send string to the RAK811 module.""" 226 | self._serial.send_string(string) 227 | 228 | def _send_command(self, command): 229 | """Send AT command to the RAK811 module and return the response. 230 | 231 | Rak811ResponseError exception is raised if the command returns an 232 | error. 233 | This is a "blocking" call: if the module does not respond 234 | Rack811TimeoutError will be raised. 235 | """ 236 | self._serial.send_command(command) 237 | response = self._serial.receive() 238 | 239 | # Ignore events received while waiting on command feedback 240 | while response.startswith(RESPONSE_EVENT): 241 | response = self._serial.receive() 242 | 243 | if response.startswith(RESPONSE_OK): 244 | response = response[len(RESPONSE_OK):] 245 | elif response.startswith(RESPONSE_ERROR): 246 | raise Rak811ResponseError(response[len(RESPONSE_ERROR):]) 247 | else: 248 | raise Rak811ResponseError(response) 249 | 250 | return response 251 | 252 | def _get_events(self, timeout=None): 253 | """Get events from the RAK811 module. 254 | 255 | This is a "blocking" call: it will either return a list of events or 256 | raise a Rack811TimeoutError. 257 | """ 258 | if timeout is None: 259 | timeout = self._event_timeout 260 | 261 | return [i[len(RESPONSE_EVENT):] for i in 262 | self._serial.receive(single=False, timeout=timeout)] 263 | 264 | """System commands.""" 265 | 266 | @property 267 | def version(self): 268 | """Get module version.""" 269 | return(self._send_command('version')) 270 | 271 | def sleep(self): 272 | """Enter sleep mode.""" 273 | self._send_command('sleep') 274 | 275 | def wake_up(self): 276 | """Wake up the RAK811 module. 277 | 278 | We just send a character and wait for an event as response 279 | """ 280 | self._send_string('*') 281 | self._get_events() 282 | 283 | def reset(self, mode): 284 | """Reset Module or LoRaWan stack. 285 | 286 | Note that reset(Reset.Module) will restart the module which will wait 287 | for an hardware reset to start. 288 | """ 289 | self._send_command('reset={0}'.format(mode)) 290 | if mode == Reset.Module: 291 | self.hard_reset() 292 | 293 | def reload(self): 294 | """Set LoraWan or LoraP2P configurations to default.""" 295 | self._send_command('reload') 296 | 297 | @property 298 | def mode(self): 299 | """Get module Mode (LoRaWan or LoRaP2P).""" 300 | return(self._int(self._send_command('mode'))) 301 | 302 | @mode.setter 303 | def mode(self, value): 304 | """Set module in LoRaWan or LoRaP2P Mode.""" 305 | self._send_command('mode={0}'.format(value)) 306 | 307 | @property 308 | def recv_ex(self): 309 | """Get RSSI & SNR report on receive flag (Enabled/Disabled).""" 310 | return(self._int(self._send_command('recv_ex'))) 311 | 312 | @recv_ex.setter 313 | def recv_ex(self, value): 314 | """Set RSSI & SNR report on receive flag (Enabled/Disabled).""" 315 | self._send_command('recv_ex={0}'.format(value)) 316 | 317 | """ LoRaWan commands.""" 318 | 319 | def set_config(self, **kwargs): 320 | """Set LoraWan configuration. 321 | 322 | Parameters are specified a key/value pairs, and are passed directly to 323 | the module. 324 | 325 | E.g.: set_config(app_eui='0000000000000000', 326 | app_key='00000000000000000000000000000000', 327 | adr='on') 328 | 329 | The module saves relevant data in EEPROM, it is not necessary to set 330 | values for each session. 331 | 332 | The following parameters are accepted by the module: 333 | dev_addr: device address (4 bytes hex number) 334 | dev_eui: device EUI (8 bytes hex number, default derived from the 335 | MCU UUID 336 | app_eui: app EUI (8 bytes hex number) 337 | app_key: app key (16 bytes hex number) 338 | apps_key: application session key (16 bytes hex number) 339 | nwks_key: network session key (16 bytes hex number) 340 | tx_power: transmit power (in dBm -- deprecated, use pwr_level) 341 | pwr_level: transmit power (0-7 for EU868, region specific) 342 | adr: adr flag (on/off) 343 | dr: data rate (0-5 for EU868, region specific) 344 | public_net: public_net flag (on/off) 345 | rx_delay1: rx1 delay (0-65535 milliseconds) 346 | ch_list: channel list, see RAK documentation 347 | ch_mask: channel mask, see RAK documentation 348 | max_chs: max channels used in the region (read-only) 349 | rx2: rx2 data rate and frequency 350 | join_cnt: join count for OTAA joins (number, region specific) 351 | nbtrans: number of transmissions for unconfirmed uplink message 352 | (1-15, default 1) 353 | retrans: number of retransmissions for confirmed uplink message 354 | (1-255, default 8) 355 | class: LoRa class (0: A, 2: C) 356 | duty: respect duty cycle flag (on/off) 357 | """ 358 | self._send_command('set_config=' 359 | + '&'.join([':'.join(str(val) for val in kv) 360 | for kv in kwargs.items()])) 361 | 362 | def get_config(self, key): 363 | """Get LoraWan configuration from EEPROM. 364 | 365 | The parameter must be a key from the above list. 366 | 367 | Note: get_config returns always strings, no integer do avoid unwanted 368 | conversion for keys. 369 | """ 370 | return self._send_command('get_config={0}'.format(key)) 371 | 372 | @property 373 | def band(self): 374 | """Get LoRaWan region. 375 | 376 | Region is one of: EU868, US915, AU915, KR920, AS923, IN865. 377 | """ 378 | return(self._send_command('band')) 379 | 380 | @band.setter 381 | def band(self, region): 382 | """Set LoRaWan region. 383 | 384 | Region must be one of: EU868, US915, AU915, KR920, AS923, IN865. 385 | """ 386 | self._send_command('band={0}'.format(region)) 387 | 388 | def join_abp(self): 389 | """Join the configured network in ABP mode. 390 | 391 | ABP requires the following parameters to be set prior join: 392 | - dev_addr 393 | - nwks_key 394 | - apps_key 395 | """ 396 | self._send_command('join=abp') 397 | 398 | def join_otaa(self): 399 | """Join the configured network in OTAA mode. 400 | 401 | OTAA requires the following parameters to be set prior join: 402 | - dev_eui 403 | - app_eui 404 | - app_key 405 | 406 | This call is "blocking", it will return only after the join completes. 407 | The following exceptions can be raised: 408 | - Rak811TimeoutError: join didn't succeed in time 409 | - Rak811EventError: join failed 410 | """ 411 | self._send_command('join=otaa') 412 | # Waiting join completion 413 | for event in self._get_events(): 414 | status = event.split(',')[0] 415 | status = self._int(status) 416 | if status != EventCode.JOINED_SUCCESS: 417 | raise Rak811EventError(status) 418 | 419 | @property 420 | def signal(self): 421 | """Get (RSSI,SNR) from latest received packet.""" 422 | return(tuple(self._int(i) 423 | for i in self._send_command('signal').split(','))) 424 | 425 | @property 426 | def dr(self): 427 | """Get next send data rate.""" 428 | return(self._int(self._send_command('dr'))) 429 | 430 | @dr.setter 431 | def dr(self, value): 432 | """Set next send data rate.""" 433 | self._send_command('dr={0}'.format(value)) 434 | 435 | @property 436 | def link_cnt(self): 437 | """Get up & downlink counters. 438 | 439 | Counters are 32 bits integers in decimal format. 440 | """ 441 | return(tuple(self._int(i) 442 | for i in self._send_command('link_cnt').split(','))) 443 | 444 | @link_cnt.setter 445 | def link_cnt(self, value): 446 | """Set up & downlink counters. 447 | 448 | Counters are 32 bits integers in decimal format. 449 | """ 450 | self._send_command('link_cnt=' + ','.join(str(val) for val in value)) 451 | 452 | @property 453 | def abp_info(self): 454 | """Get ABP info. 455 | 456 | When using OTAA, returns the necessary info to re-join in ABP mode. The 457 | following tuple is returned: 458 | (NetworkID, DevAddr, Nwkskey, Appskey) 459 | """ 460 | return(tuple(self._send_command('abp_info').split(','))) 461 | 462 | def _add_downlink(self, event): 463 | """Add message to the downlink list. 464 | 465 | Event is a list: ([,][,],[,]) 466 | """ 467 | r_port = self._int(event.pop(0)) 468 | if len(event) > 2: 469 | r_rssi = self._int(event.pop(0)) 470 | r_snr = self._int(event.pop(0)) 471 | else: 472 | r_rssi = 0 473 | r_snr = 0 474 | r_len = self._int(event.pop(0)) 475 | if r_len > 0: 476 | try: 477 | r_data = bytes.fromhex(event[0]) 478 | except ValueError: 479 | r_data = '' 480 | else: 481 | r_data = '' 482 | self._downlink.append( 483 | { 484 | 'port': r_port, 485 | 'rssi': r_rssi, 486 | 'snr': r_snr, 487 | 'len': r_len, 488 | 'data': r_data, 489 | } 490 | ) 491 | 492 | def _process_events(self, timeout=None): 493 | """Process module event queue. 494 | 495 | Process event queue looking for incoming (downlink) messages. Raise 496 | errors when unexpected events are encountered. 497 | 498 | Parameter: 499 | timeout: maximum time to wait for event 500 | 501 | """ 502 | events = self._get_events(timeout) 503 | # Check for downlink 504 | for event in events: 505 | # Format: ,[,][,],[,] 506 | event_items = event.split(',') 507 | status = event_items.pop(0) 508 | status = self._int(status) 509 | if status == EventCode.RECV_DATA: 510 | self._add_downlink(event_items) 511 | # Check for errors 512 | for event in events: 513 | status = event.split(',')[0] 514 | status = self._int(status) 515 | if status not in (EventCode.RECV_DATA, 516 | EventCode.TX_COMFIRMED, 517 | EventCode.TX_UNCONFIRMED): 518 | raise Rak811EventError(status) 519 | 520 | def send(self, data, confirm=False, port=1): 521 | """Send LoRaWan message. 522 | 523 | Parameters: 524 | data: data to be sent. If the datatype is bytes it will be send 525 | as such. Strings will be converted to bytes. 526 | confirm: regular or confirmed send. 527 | port: port number to use (1-223) 528 | 529 | """ 530 | if type(data) is not bytes: 531 | data = (bytes)(data, 'utf-8') 532 | data = hexlify(data).decode('ascii') 533 | 534 | self._send_command('send=' + ','.join(( 535 | ('1' if confirm else '0'), 536 | str(port), 537 | data 538 | ))) 539 | 540 | # Process events 541 | self._process_events() 542 | 543 | @property 544 | def nb_downlinks(self): 545 | """Get the number of downlink messages in the receive buffer.""" 546 | return len(self._downlink) 547 | 548 | def get_downlink(self): 549 | """Get a downlink message from the receive buffer. 550 | 551 | Returns a dictionary with the following keys: 552 | port: port number 553 | rssi: RSSI (0 if recv_ex was disabled) 554 | snr: SNR (0 if recv_ex was disabled) 555 | len: data length 556 | data: data itself 557 | """ 558 | if len(self._downlink) == 0: 559 | return None 560 | else: 561 | return self._downlink.pop(0) 562 | 563 | """ LoraP2P commands.""" 564 | 565 | def _get_rf_config(self): 566 | """Get LoraP2P configuration. 567 | 568 | Return a dictionary. 569 | """ 570 | config = tuple(self._int(i) 571 | for i in self._send_command('rf_config').split(',')) 572 | return { 573 | 'freq': config[0] / 1000 / 1000, 574 | 'sf': config[1], 575 | 'bw': config[2], 576 | 'cr': config[3], 577 | 'prlen': config[4], 578 | 'pwr': config[5] 579 | } 580 | 581 | @property 582 | def rf_config(self): 583 | """Get LoraP2P configuration. 584 | 585 | Return a dictionary. 586 | """ 587 | return self._get_rf_config() 588 | 589 | @rf_config.setter 590 | def rf_config(self, config): 591 | """Set LoraWan P2P RF configuration parameters. 592 | 593 | Parameters are specified a key/value pairs, default are used for 594 | missing parameters. 595 | 596 | E.g.: 597 | rf_config = { 598 | 'freq': 868.700, 599 | 'sf': 7, 600 | 'bw': 0 601 | } 602 | 603 | The module saves parameters to flash, it is not necessary to set 604 | values for each session. 605 | 606 | The following parameters can be set; only specified parameters are 607 | changed, others are kept to their previous value. Values between 608 | parentheses are RAK defaults. 609 | freq: frequency in Mhz, range 860.000-929.900 Mhz (868.100) 610 | sf: spread factor, range 6-12 (12) 611 | bw: band width, values 0:125KHz, 1:250KHz, 2:500KHz (0) 612 | cr: coding rate, values 1:4/5, 2:4/6, 3:4/7, 4:4/8 (1) 613 | prlen: preamble len, range 8-65536 (8) 614 | pwr: transmit power, range 5,20 (20) 615 | """ 616 | base_config = self._get_rf_config() 617 | base_config.update(config) 618 | self._send_command( 619 | 'rf_config={0},{1},{2},{3},{4},{5}'.format( 620 | int(base_config['freq'] * 1000 * 1000), 621 | base_config['sf'], 622 | base_config['bw'], 623 | base_config['cr'], 624 | base_config['prlen'], 625 | base_config['pwr'] 626 | ) 627 | ) 628 | 629 | def txc(self, data, cnt=1, interval=60): 630 | """Send LoraP2P message. 631 | 632 | Send data using the pre-set RF parameters. 633 | For RF testing cnt can be specified to send data multiple time. 634 | The module will stop sending messages after cnt messages or when it 635 | receives a tx_stop command. 636 | The method returns after all messages have been sent. 637 | 638 | Parameters: 639 | data: data to be sent. If the datatype is bytes it will be send 640 | as such. Strings will be converted to bytes. 641 | cnt: send message cnt times 642 | interval: when sending multiple times, interval in seconds 643 | between each message. 644 | 645 | """ 646 | if type(data) is not bytes: 647 | data = (bytes)(data, 'utf-8') 648 | data = hexlify(data).decode('ascii') 649 | 650 | self._send_command('txc=' + ','.join(( 651 | str(cnt), 652 | str(interval * 1000), 653 | data 654 | ))) 655 | 656 | # Process events 657 | events = self._get_events((cnt * (interval + 10)) - interval) 658 | # Check for errors 659 | for event in events: 660 | status = event.split(',')[0] 661 | status = self._int(status) 662 | if status != EventCode.P2PTX_COMPLETE: 663 | raise Rak811EventError(status) 664 | 665 | def rxc(self, report_en=1): 666 | """Set module in LoraP2P receive mode. 667 | 668 | Module is put in receive mode until an rx_stop command is issued. 669 | Method will return immediately after the command is acknowledged (it 670 | does not wait for data) 671 | 672 | Parameter: 673 | report_en: set to 1 by default. Can be set to 0 for RF testing 674 | (documentation is not clear about this) 675 | """ 676 | self._send_command('rxc=' + str(report_en)) 677 | 678 | def tx_stop(self): 679 | """Stop LoraP2P TX. 680 | 681 | Stop LoraP2P transmission; radio will switch to sleep mode. 682 | """ 683 | self._send_command('tx_stop') 684 | 685 | def rx_stop(self): 686 | """Stop LoraP2P RX. 687 | 688 | Stop LoraP2P reception; radio will switch to sleep mode. 689 | """ 690 | self._send_command('rx_stop') 691 | 692 | def rx_get(self, timeout): 693 | """Get LoraP2P message. 694 | 695 | This is a blocking call: wait until we receive a message or we reach a 696 | timeout. 697 | 698 | The downlink receive buffer is populated, actual data is retrieved with 699 | get_downlink(). 700 | """ 701 | try: 702 | self._process_events(timeout) 703 | except Rak811TimeoutError: 704 | pass 705 | 706 | """Radio commands.""" 707 | 708 | @property 709 | def radio_status(self): 710 | """Get radio statistics. 711 | 712 | Return a tuple: (TxSuccessCnt, TxErrCnt, RxSuccessCnt, RxTimeOutCnt, 713 | RxErrCnt, Rssi, Snr) 714 | """ 715 | return(tuple(self._int(i) 716 | for i in self._send_command('status').split(','))) 717 | 718 | def clear_radio_status(self): 719 | """Clear radio statistics.""" 720 | self._send_command('status=0') 721 | -------------------------------------------------------------------------------- /rak811/rak811_v3.py: -------------------------------------------------------------------------------- 1 | """Interface with the RAK811 module (Firmware V3.0). 2 | 3 | Copyright 2021, 2022 Philippe Vanhaesendonck 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | from binascii import hexlify 20 | from enum import IntEnum 21 | from logging import getLogger 22 | from re import match 23 | from time import sleep 24 | from typing import List, Union 25 | 26 | from RPi import GPIO 27 | 28 | from .exception import Rak811Error 29 | from .serial import Rak811Serial, Rak811TimeoutError 30 | 31 | RESET_BCM_PORT = 17 32 | RESET_DELAY = 0.01 33 | RESET_POST = 2 34 | RESPONSE_OK = 'OK ' 35 | RESPONSE_INIT_OK = 'Initialization OK' 36 | RESPONSE_ERROR = 'ERROR:' 37 | RESPONSE_EVENT = 'at+recv=' 38 | 39 | # Timeout for response and events 40 | # The RAK811 typically respond in less than 1.5 seconds 41 | RESPONSE_TIMEOUT = 5 42 | # Event wait time strongly depends on duty cycle, when sending often at high SF 43 | # the module will wait to respect the duty cycle. 44 | # In normal operation, 5 minutes should be more than enough. 45 | EVENT_TIMEOUT = 5 * 60 46 | 47 | logger = getLogger(__name__) 48 | 49 | 50 | # RAK811 error codes and associated messages 51 | class ErrorCode(IntEnum): 52 | """AT commands error codes.""" 53 | 54 | ERR_001 = 1 55 | ERR_002 = 2 56 | ERR_003 = 3 57 | ERR_004 = 4 58 | ERR_005 = 5 59 | ERR_041 = 41 60 | ERR_080 = 80 61 | ERR_081 = 81 62 | ERR_082 = 82 63 | ERR_083 = 83 64 | ERR_084 = 84 65 | ERR_085 = 85 66 | ERR_086 = 86 67 | ERR_087 = 87 68 | ERR_088 = 88 69 | ERR_089 = 89 70 | ERR_090 = 90 71 | ERR_091 = 91 72 | ERR_092 = 92 73 | ERR_093 = 93 74 | ERR_094 = 94 75 | ERR_095 = 95 76 | ERR_096 = 96 77 | ERR_097 = 97 78 | ERR_098 = 98 79 | ERR_099 = 99 80 | ERR_100 = 100 81 | ERR_101 = 101 82 | ERR_102 = 102 83 | ERR_103 = 102 84 | ERR_104 = 104 85 | ERR_INVALID_EVENT = 998 86 | ERR_UNKNOWN = 999 87 | 88 | 89 | ERROR_MESSAGE = { 90 | ErrorCode.ERR_001: 'Unsupported AT command', 91 | ErrorCode.ERR_002: 'Invalid parameter in AT command', 92 | ErrorCode.ERR_003: 'Error when reading or writing flash', 93 | ErrorCode.ERR_004: 'Error reading or writing IIC', 94 | ErrorCode.ERR_005: 'Error sending through UART', 95 | ErrorCode.ERR_041: 'BLE in invalid state', 96 | ErrorCode.ERR_080: 'LoRa busy', 97 | ErrorCode.ERR_081: 'LoRa service unknown', 98 | ErrorCode.ERR_082: 'Invalid LoRa parameters', 99 | ErrorCode.ERR_083: 'Invalid LoRa frequency', 100 | ErrorCode.ERR_084: 'Invalid LoRa datarate (DR)', 101 | ErrorCode.ERR_085: 'Invalid LoRa frequency and datarate', 102 | ErrorCode.ERR_086: 'Device has not joined LoRa network', 103 | ErrorCode.ERR_087: 'Packet length too long', 104 | ErrorCode.ERR_088: 'Service closed by server', 105 | ErrorCode.ERR_089: 'Unsupported region.', 106 | ErrorCode.ERR_090: 'Restricted duty cycle', 107 | ErrorCode.ERR_091: 'No valid channel can be found.', 108 | ErrorCode.ERR_092: 'No free channel found', 109 | ErrorCode.ERR_093: 'Status is error', 110 | ErrorCode.ERR_094: 'LoRa transmiting timeout', 111 | ErrorCode.ERR_095: 'LoRa RX1 timeout', 112 | ErrorCode.ERR_096: 'LoRa RX2 timeout', 113 | ErrorCode.ERR_097: 'Error receiving RX1', 114 | ErrorCode.ERR_098: 'Error receiving RX2', 115 | ErrorCode.ERR_099: 'LoRa join failed', 116 | ErrorCode.ERR_100: 'Repeated downlink', 117 | ErrorCode.ERR_101: 'Payload size error with transmit DR', 118 | ErrorCode.ERR_102: 'Too many downlink frames lost', 119 | ErrorCode.ERR_103: 'Address fail', 120 | ErrorCode.ERR_104: 'Error verifying MIC', 121 | ErrorCode.ERR_INVALID_EVENT: 'Invalid event received', 122 | ErrorCode.ERR_UNKNOWN: 'Unknown error', 123 | } 124 | 125 | 126 | class Rak811ResponseError(Rak811Error): 127 | """Exception raised by response from the module. 128 | 129 | Attributes: 130 | errno -- as returned by the module 131 | strerror -- textual representation 132 | 133 | """ 134 | 135 | def __init__(self, code): 136 | """Just assign return codes.""" 137 | try: 138 | self.errno = int(code) 139 | except ValueError: 140 | self.errno = code 141 | 142 | if self.errno in ERROR_MESSAGE: 143 | self.strerror = ERROR_MESSAGE[self.errno] 144 | else: 145 | self.strerror = ERROR_MESSAGE[ErrorCode.ERR_UNKNOWN] 146 | super().__init__(('[Errno {}] {}').format(self.errno, self.strerror)) 147 | 148 | 149 | class Rak811(object): 150 | """Main class.""" 151 | 152 | def __init__(self, **kwargs): 153 | """Initialise class. 154 | 155 | The serial port is immediately opened and flushed. 156 | 157 | Args: 158 | response_timeout (optional): override default response timeout. 159 | Defaults to None. 160 | event_timeout (optional): override default event timeout. 161 | Defaults to None. 162 | Remainder parameters are passed to RackSerial. 163 | """ 164 | self._response_timeout = kwargs.pop('response_timeout', RESPONSE_TIMEOUT) 165 | self._event_timeout = kwargs.pop('event_timeout', EVENT_TIMEOUT) 166 | self._serial = Rak811Serial( 167 | keep_untagged=True, 168 | read_buffer_timeout=self._response_timeout, 169 | **kwargs 170 | ) 171 | self._downlink = [] 172 | 173 | def close(self) -> None: 174 | """Terminates session. 175 | 176 | Terminates read thread and close serial port. 177 | """ 178 | self._serial.close() 179 | 180 | def hard_reset(self) -> None: 181 | """Hard reset of the RAK811 module. 182 | 183 | Hard reset should not be required in normal operation. It needs to be 184 | issued once after host boot, or module restart. 185 | Note that we do not cleanup() as the reset port should stay high (it is 186 | configured that way at boot time). 187 | 188 | Note: left for historical reasons, it does not appear to be effective 189 | with the V3 firmware 190 | """ 191 | GPIO.setwarnings(False) 192 | GPIO.setmode(GPIO.BCM) 193 | GPIO.setup(RESET_BCM_PORT, GPIO.OUT) 194 | GPIO.output(RESET_BCM_PORT, GPIO.LOW) 195 | sleep(RESET_DELAY) 196 | GPIO.output(RESET_BCM_PORT, GPIO.HIGH) 197 | sleep(RESET_POST) 198 | 199 | """ Private methods.""" 200 | 201 | def _int(self, i: str) -> Union[int, str]: 202 | """Attempt int conversion. 203 | 204 | Args: 205 | i: Integer string. 206 | 207 | Returns: 208 | Integer conversion if possible, else the original string. 209 | """ 210 | try: 211 | i = int(i) 212 | except ValueError: 213 | pass 214 | return i 215 | 216 | def _send_string(self, string: str) -> None: 217 | """Send string to the RAK811 module. 218 | 219 | Args: 220 | string: String to send. 221 | """ 222 | self._serial.send_string(string) 223 | 224 | def _send_command(self, command: str, timeout: float = None) -> str: 225 | """Send AT command to the RAK811 module and return the response. 226 | 227 | This method will wait until the module answer or timeout is reached. 228 | 229 | Args: 230 | command: Command to send 231 | timeout (optional): Time to wait before raising a timeout. 232 | Defaults to None. 233 | 234 | Raises: 235 | Rak811ResponseError: Module answered with an error code. 236 | Rack811TimeoutError: No answer received and timeout is reached. 237 | 238 | Returns: 239 | Module answer. 240 | """ 241 | if timeout is None: 242 | timeout = self._response_timeout 243 | 244 | # Process possible pending events 245 | try: 246 | self._process_events(timeout=0.01) 247 | logger.debug('Pending event queue flushed') 248 | except (Rak811ResponseError, Rak811TimeoutError): 249 | pass 250 | 251 | self._serial.send_command(command) 252 | response = self._serial.receive(timeout=timeout) 253 | 254 | # Ignore anything but OK/ERROR messages 255 | while not (response.startswith(RESPONSE_OK) 256 | or response.startswith(RESPONSE_INIT_OK) 257 | or response.startswith(RESPONSE_ERROR)): 258 | response = self._serial.receive(timeout=timeout) 259 | 260 | if response.startswith(RESPONSE_ERROR): 261 | raise Rak811ResponseError(response[len(RESPONSE_ERROR):]) 262 | elif response.startswith(RESPONSE_INIT_OK): 263 | return response[len(RESPONSE_INIT_OK):] 264 | else: 265 | return response[len(RESPONSE_OK):] 266 | 267 | def _send_command_list(self, command: str) -> List[str]: 268 | """Send AT command to the RAK811 module and return the responses. 269 | 270 | Similar to _send_command, but returns the complete read buffer. It is 271 | used for commands returning several lines. 272 | 273 | This method will wait until the module answer or timeout is reached. 274 | 275 | Args: 276 | command: Command to send 277 | 278 | Raises: 279 | Rak811ResponseError: Module answered with an error code. 280 | Rack811TimeoutError: No answer received and timeout is reached. 281 | 282 | Returns: 283 | List of module answers. 284 | """ 285 | self._serial.send_command(command) 286 | 287 | prelude = [] 288 | response = [] 289 | while True: 290 | if not response: 291 | response = self._serial.receive(single=False) 292 | if response[0].startswith(RESPONSE_OK): 293 | response[0] = response[0][len(RESPONSE_OK):] 294 | break 295 | elif response[0].startswith(RESPONSE_INIT_OK): 296 | response[0] = response[0][len(RESPONSE_INIT_OK):] 297 | break 298 | elif response[0].startswith(RESPONSE_ERROR): 299 | raise Rak811ResponseError(response[0][len(RESPONSE_ERROR):]) 300 | else: 301 | # Ignore anything until we get an OK/ERROR message 302 | prelude.append(response.pop(0)) 303 | 304 | return prelude + response 305 | 306 | def _get_events(self, timeout: float = None) -> List[str]: 307 | """Get list of events from the RAK811 module. 308 | 309 | Args: 310 | timeout (optional): Time to wait before raising a timeout. 311 | Defaults to None. 312 | 313 | Raises: 314 | Rack811TimeoutError: No answer received and timeout is reached. 315 | 316 | Returns: 317 | Event list 318 | """ 319 | if timeout is None: 320 | timeout = self._event_timeout 321 | 322 | return [i[len(RESPONSE_EVENT):] if i.startswith(RESPONSE_EVENT) else i for i in 323 | self._serial.receive(single=False, timeout=timeout)] 324 | 325 | def _add_downlink(self, port: str, rssi: str, snr: str, length: str, data: str) -> None: 326 | """Add message to the downlink list. 327 | 328 | Args: 329 | port: Message port. 330 | rssi: Message RSSI. 331 | snr: Message SNR. 332 | length: Message length. 333 | data: Message data 334 | """ 335 | r_port = 0 if port is None else self._int(port) 336 | r_rssi = self._int(rssi) 337 | r_snr = self._int(snr) 338 | r_len = self._int(length) 339 | if r_len > 0: 340 | try: 341 | r_data = bytes.fromhex(data) 342 | except ValueError: 343 | r_data = b'' 344 | else: 345 | r_data = b'' 346 | self._downlink.append( 347 | { 348 | 'port': r_port, 349 | 'rssi': r_rssi, 350 | 'snr': r_snr, 351 | 'len': r_len, 352 | 'data': r_data, 353 | } 354 | ) 355 | 356 | def _process_events(self, timeout=None) -> None: 357 | """Process module event queue. 358 | 359 | Process event queue looking for incoming (downlink) messages. 360 | 361 | Args: 362 | timeout (optional): maximum time to wait for event. 363 | Defaults to None. 364 | 365 | Raises: 366 | - Rak811TimeoutError: no answer 367 | - Rak811ResponseError: error returned or unexpected response 368 | received 369 | """ 370 | events = self._get_events(timeout) 371 | # Check for downlink 372 | for event in events: 373 | # LoRaWan format: ,,,[:] 374 | # LoRa P2P format: ,,,[:] 375 | m = match(r'((\d+),)?(-?\d+),(-?\d+),(\d+)(:(.*))?$', event) 376 | if m: 377 | _, port, rssi, snr, length, _, data = m.groups() 378 | self._add_downlink(port, rssi, snr, length, data) 379 | else: 380 | raise Rak811ResponseError(ErrorCode.ERR_INVALID_EVENT) 381 | 382 | """Generic get / set commands.""" 383 | 384 | def set_config(self, config: str) -> List[str]: 385 | """Execute set_config command. 386 | 387 | The module will return: 388 | - "OK" (most cases) 389 | - "OK" / "" (device:boot) 390 | - some info / "Initialization OK" (device:restart / lora:work_mode) 391 | 392 | Args: 393 | config: Config string to send in the format :[:]... 394 | Supported types and topics: 395 | - device: restart, sleep, boot, status, uart, uart_mode, gpio 396 | - lora: region, channel, dev_eui, app_eui, app_key, dev_addr, 397 | apps_key, nwks_key, join_mode, work_mode, ch_mask, class, 398 | confirm, dr, tx_power, adr, send_interval 399 | - lorap2p: transfer_mode, channel configuration 400 | 401 | Raises: 402 | Rak811ResponseError: Module answered with an error code. 403 | Rack811TimeoutError: No answer received and timeout is reached. 404 | 405 | Returns: 406 | List of responses (Informational) 407 | """ 408 | # We use the "_list" variant to drain the buffer 409 | return self._send_command_list(f'set_config={config}') 410 | 411 | def get_config(self, config: str) -> List[str]: 412 | """Get configuration item. 413 | 414 | Args: 415 | config: Config string to send in the format :[:] 416 | Supported types and topics: 417 | - device: status, gpio, adc 418 | - lora: channel, status 419 | 420 | Raises: 421 | Rak811ResponseError: Module answered with an error code. 422 | Rack811TimeoutError: No answer received and timeout is reached. 423 | 424 | Returns: 425 | Module answers (list). 426 | """ 427 | return(self._send_command_list(f'get_config={config}')) 428 | 429 | """General AT commands.""" 430 | 431 | @property 432 | def version(self) -> str: 433 | """Get module version. 434 | 435 | Raises: 436 | - Rak811TimeoutError: no answer 437 | - Rak811ResponseError: error returned 438 | 439 | Returns: 440 | Module version. 441 | """ 442 | return(self._send_command('version')) 443 | 444 | @property 445 | def help(self) -> List[str]: 446 | """Get module help. 447 | 448 | Raises: 449 | - Rak811TimeoutError: no answer 450 | - Rak811ResponseError: error returned 451 | 452 | Returns: 453 | Module help (list). 454 | """ 455 | return(self._send_command_list('help')) 456 | 457 | def run(self) -> None: 458 | """Exit boot mode and enter normal mode. 459 | 460 | Raises: 461 | - Rak811TimeoutError: no answer 462 | - Rak811ResponseError: error returned 463 | """ 464 | self._send_command('run') 465 | 466 | """Interface commands.""" 467 | 468 | def send_uart(self, data: Union[bytes, str], index: int = 3) -> None: 469 | """Send data over UART. 470 | 471 | Args: 472 | data: data to be sent. If the datatype is bytes it will be send 473 | as such. Strings will be converted to bytes. 474 | index (optional): UART index to use (1, 3). Defaults to 3. 475 | UART1 is the AT Command interface, so you probably want to use 476 | UART3! 477 | 478 | Raises: 479 | - Rak811TimeoutError: no answer 480 | - Rak811ResponseError: error returned 481 | """ 482 | if type(data) is not bytes: 483 | data = (bytes)(data, 'utf-8') 484 | data = hexlify(data).decode('ascii') 485 | 486 | self._send_command(f'send=uart:{index}:{data}') 487 | 488 | """LoRa commands""" 489 | 490 | def join(self) -> None: 491 | """Join the configured network. 492 | 493 | ABP requires the following parameters to be set prior join: 494 | - dev_addr 495 | - nwks_key 496 | - apps_key 497 | 498 | OTAA requires the following parameters to be set prior join: 499 | - dev_eui 500 | - app_eui 501 | - app_key 502 | 503 | Raises: 504 | - Rak811TimeoutError: no answer 505 | - Rak811ResponseError: error returned 506 | """ 507 | # Extend timeout for the join command 508 | self._send_command('join', timeout=self._event_timeout) 509 | 510 | def send(self, data: Union[bytes, str], port: int = 1) -> None: 511 | """Send LoRaWan message. 512 | 513 | Args: 514 | data: data to be sent. If the datatype is bytes it will be send 515 | as such. Strings will be converted to bytes. 516 | port (optional): port number to use (1-223). Defaults to 1. 517 | 518 | Raises: 519 | - Rak811TimeoutError: no answer 520 | - Rak811ResponseError: error returned 521 | """ 522 | if type(data) is not bytes: 523 | data = (bytes)(data, 'utf-8') 524 | data = hexlify(data).decode('ascii') 525 | 526 | self._send_command(f'send=lora:{port}:{data}', timeout=self._event_timeout) 527 | 528 | # Process events - Check for downlink / send confirmation 529 | # It is issued immediately after the "OK" response so don't have to 530 | # wait long. 531 | try: 532 | self._process_events(timeout=0.1) 533 | except Rak811TimeoutError: 534 | pass 535 | if self.nb_downlinks: 536 | logger.debug('Downlink available') 537 | else: 538 | logger.debug('No downlink') 539 | 540 | @property 541 | def nb_downlinks(self) -> int: 542 | """Get the number of downlink messages in the receive buffer. 543 | 544 | Returns: 545 | Number of messages in the downlink buffer. 546 | """ 547 | return len(self._downlink) 548 | 549 | def get_downlink(self) -> str: 550 | """Get a downlink message from the receive buffer. 551 | 552 | Returns: 553 | Dictionary with the following keys: 554 | port: port number 555 | rssi: RSSI 556 | snr: SNR 557 | len: data length (0 for empty / confirmation messages) 558 | data: data itself (None is len is 0) 559 | """ 560 | if len(self._downlink) == 0: 561 | return None 562 | else: 563 | return self._downlink.pop(0) 564 | 565 | """ LoraP2P commands.""" 566 | 567 | def send_p2p(self, data: Union[bytes, str]) -> None: 568 | """Send P2P message. 569 | 570 | Args: 571 | data: data to be sent. If the datatype is bytes it will be send 572 | as such. Strings will be converted to bytes. 573 | 574 | Raises: 575 | - Rak811TimeoutError: no answer 576 | - Rak811ResponseError: error returned 577 | """ 578 | if type(data) is not bytes: 579 | data = (bytes)(data, 'utf-8') 580 | data = hexlify(data).decode('ascii') 581 | 582 | self._send_command(f'send=lorap2p:{data}') 583 | 584 | def receive_p2p(self, timeout: float) -> None: 585 | """Wait for P2P message. 586 | 587 | The method wil return when one or more messages are received or when 588 | timeout is reached. 589 | 590 | The method does not return messages, use nb_downlinks and 591 | get_downlink() to fetch downlinks. 592 | 593 | Args: 594 | timeout: maximum time to wait. 595 | """ 596 | try: 597 | self._process_events(timeout=timeout) 598 | except Rak811TimeoutError: 599 | pass 600 | if self.nb_downlinks: 601 | logger.debug('Message available') 602 | else: 603 | logger.debug('Nothing received') 604 | -------------------------------------------------------------------------------- /rak811/serial.py: -------------------------------------------------------------------------------- 1 | """RAK811 serial communication layer. 2 | 3 | Copyright 2019, 2021 Philippe Vanhaesendonck 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | from logging import getLogger 20 | from re import match 21 | from threading import Condition, Event, Thread 22 | from time import sleep 23 | from typing import List, Union 24 | 25 | from rak811.exception import Rak811Error 26 | from serial import Serial 27 | 28 | # Default instance parameters. Can be overridden at creation 29 | # Serial port configuration 30 | PORT = '/dev/serial0' 31 | BAUDRATE = 115200 32 | # Timeout for the reader thread. Any value will do, the only impact is the time 33 | # needed to stop the thread when the instance is destroyed... 34 | TIMEOUT = 2 35 | # Timeout for response and events 36 | # The RAK811 typically respond in less than 1.5 seconds 37 | READ_BUFFER_TIMEOUT = 5 38 | 39 | # Constants 40 | EOL = '\r\n' 41 | 42 | 43 | logger = getLogger(__name__) 44 | 45 | 46 | class Rak811TimeoutError(Rak811Error): 47 | """Read timeout exception.""" 48 | 49 | pass 50 | 51 | 52 | class Rak811Serial(object): 53 | """Handles serial communication between the RPi and the RAK811 module.""" 54 | 55 | def __init__(self, 56 | port=PORT, 57 | baudrate=BAUDRATE, 58 | timeout=TIMEOUT, 59 | read_buffer_timeout=READ_BUFFER_TIMEOUT, 60 | keep_untagged=False, 61 | **kwargs): 62 | """Initialise class. 63 | 64 | The serial port is immediately opened and flushed. 65 | All parameters are optional and passed to Serial. 66 | """ 67 | self._read_buffer_timeout = read_buffer_timeout 68 | self._serial = Serial(port=port, 69 | baudrate=baudrate, 70 | timeout=timeout, 71 | **kwargs) 72 | self._serial.reset_input_buffer() 73 | 74 | self._keep_untagged = keep_untagged 75 | 76 | # Mutex 77 | self._cv_serial = Condition() 78 | self._read_buffer = [] 79 | 80 | # Read thread 81 | self._read_done = Event() 82 | self._read_thread = Thread(target=self._read_loop, daemon=True) 83 | self._read_thread.start() 84 | 85 | self._alive = True 86 | logger.debug('Serial initialized') 87 | 88 | def close(self): 89 | """Release resources.""" 90 | if self._alive: 91 | self._read_done.set() 92 | self._read_thread.join() 93 | self._serial.close() 94 | self._alive = False 95 | 96 | def _read_loop(self): 97 | """Read thread. 98 | 99 | Continuously read serial. When data is available we want to read all of 100 | it and notify once: 101 | - We need to drain the input after a response. If we notify() 102 | too early the module will miss next command 103 | - We want to catch all events at the same time 104 | """ 105 | while not self._read_done.is_set(): 106 | line = self._serial.readline() 107 | if line != b'': 108 | # Not a timeout; process data stream 109 | with self._cv_serial: 110 | while True: 111 | try: 112 | line = line.decode('ascii').rstrip(EOL) 113 | except UnicodeDecodeError: 114 | # Wrong speed or port not configured properly 115 | line = '?' 116 | if match(r'^(OK|ERROR|at+)', line): 117 | logger.debug(f'Received: >{line}<') 118 | self._read_buffer.append(line) 119 | elif self._keep_untagged: 120 | logger.debug(f'Received untagged: >{line}<') 121 | self._read_buffer.append(line) 122 | else: 123 | logger.debug(f'Ignoring untagged: >{line}<') 124 | sleep(0.1) 125 | if self._serial.in_waiting > 0: 126 | line = self._serial.readline() 127 | else: 128 | break 129 | if len(self._read_buffer) > 0: 130 | self._cv_serial.notify() 131 | 132 | def receive(self, single: bool = True, timeout: int = None) -> Union[str, List[str]]: 133 | """Receive data from module. 134 | 135 | This is a blocking call: it will data or raise Rak811TimeoutError if 136 | nothing is received in time. 137 | 138 | Args: 139 | single (optional): Return single line of data when true, otherwise 140 | all available lines are returned. Defaults to True. 141 | timeout (optional): Time to wait for. Defaults to None. 142 | 143 | Raises: 144 | Rak811TimeoutError: No data received in time. 145 | 146 | Returns: 147 | Single line of data or list of lines. 148 | """ 149 | if timeout is None: 150 | timeout = self._read_buffer_timeout 151 | 152 | with self._cv_serial: 153 | while len(self._read_buffer) == 0: 154 | success = self._cv_serial.wait(timeout) 155 | if not success: 156 | raise Rak811TimeoutError('Timeout while waiting for data') 157 | if single: 158 | response = self._read_buffer.pop(0) 159 | else: 160 | response = self._read_buffer 161 | self._read_buffer = [] 162 | return response 163 | 164 | def send_string(self, string): 165 | """Send string to the module.""" 166 | logger.debug(f"Sending: >{string.encode('unicode_escape').decode('utf-8')}<") 167 | self._serial.write((bytes)(string, 'utf-8')) 168 | 169 | def send_command(self, command): 170 | """Send AT command to the module.""" 171 | self.send_string('at+{0}\r\n'.format(command)) 172 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # This includes the license file(s) in the wheel. 3 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 4 | license_files = LICENSE.txt 5 | 6 | [bdist_wheel] 7 | # This flag says to generate wheels that support both Python 2 and Python 8 | # 3. If your code will not run unchanged on both Python 2 and 3, you will 9 | # need to generate separate wheels for each Python version that you 10 | # support. Removing this line (or setting universal to 0) will prevent 11 | # bdist_wheel from trying to make a universal wheel. For more see: 12 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/#wheels 13 | universal=0 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """RAK811 library and command line utility. 2 | 3 | Setup file for the project 4 | 5 | Copyright 2019 Philippe Vanhaesendonck 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | 19 | SPDX-License-Identifier: Apache-2.0 20 | """ 21 | 22 | from os import path 23 | 24 | from setuptools import find_packages, setup 25 | 26 | here = path.abspath(path.dirname(__file__)) 27 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 28 | long_description = f.read() 29 | 30 | setup( 31 | name='rak811', 32 | use_scm_version={"local_scheme": "no-local-version"}, 33 | description='Interface for RAK811 LoRa module', 34 | long_description=long_description, 35 | long_description_content_type='text/markdown', 36 | url='https://github.com/AmedeeBulle/pyrak811', 37 | author='Philippe Vanhaesendonck', 38 | author_email='philippe.vanhaesendonck@e-bulles.be', 39 | classifiers=[ 40 | 'Development Status :: 4 - Beta', 41 | 'Intended Audience :: Developers', 42 | 'Topic :: Software Development :: Libraries', 43 | 'License :: OSI Approved :: Apache Software License', 44 | 'Programming Language :: Python :: 3', 45 | 'Operating System :: POSIX :: Linux', 46 | ], 47 | packages=find_packages(), 48 | python_requires='>=3.5', 49 | setup_requires=['setuptools_scm'], 50 | install_requires=[ 51 | 'click>=7.1', 52 | 'pyserial', 53 | 'RPi.GPIO; platform_machine=="armv7l" or platform_machine=="armv6l"', 54 | 'setuptools' 55 | ], 56 | extras_require={ 57 | 'test': [ 58 | 'flake8', 59 | 'flake8-comprehensions', 60 | 'flake8-docstrings', 61 | 'flake8-import-order', 62 | 'pep8-naming', 63 | 'pydocstyle==6.0.0', 64 | 'pytest', 65 | 'mock', 66 | 'coverage', 67 | 'tox', 68 | ], 69 | }, 70 | entry_points={ 71 | 'console_scripts': [ 72 | 'rak811=rak811.cli:cli', 73 | 'rak811v3=rak811.cli_v3:cli', 74 | ], 75 | }, 76 | 77 | ) 78 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | """Units tests for the CLI. 2 | 3 | Rak811 is tested separately and therefore mocked in this suite. 4 | """ 5 | from click.testing import CliRunner 6 | from mock import Mock, patch, PropertyMock 7 | from pytest import fixture 8 | # Ignore RPi.GPIO 9 | p = patch.dict('sys.modules', {'RPi': Mock()}) 10 | p.start() 11 | from rak811.cli import cli # noqa: E402 12 | from rak811.rak811 import Mode, RecvEx, Reset # noqa: E402 13 | from rak811.rak811 import Rak811EventError, Rak811ResponseError, \ 14 | Rak811TimeoutError # noqa: E402 15 | 16 | 17 | @fixture 18 | def mock_rak811(): 19 | with patch('rak811.cli.Rak811', autospec=True) as p: 20 | yield p 21 | 22 | 23 | @fixture 24 | def runner(): 25 | return CliRunner() 26 | 27 | 28 | def test_hard_reset(runner, mock_rak811): 29 | result = runner.invoke(cli, ['-v', 'hard-reset']) 30 | mock_rak811.return_value.hard_reset.assert_called_once() 31 | assert result.output == 'Hard reset complete\n' 32 | 33 | 34 | def test_version(runner, mock_rak811): 35 | p = PropertyMock(return_value='2.0.3.0') 36 | type(mock_rak811.return_value).version = p 37 | result = runner.invoke(cli, ['version']) 38 | p.assert_called_once_with() 39 | assert result.output == '2.0.3.0\n' 40 | 41 | 42 | def test_sleep(runner, mock_rak811): 43 | result = runner.invoke(cli, ['-v', 'sleep']) 44 | mock_rak811.return_value.sleep.assert_called_once() 45 | assert result.output == 'Sleeping\n' 46 | 47 | 48 | def test_wake(runner, mock_rak811): 49 | result = runner.invoke(cli, ['-v', 'wake-up']) 50 | mock_rak811.return_value.wake_up.assert_called_once() 51 | assert result.output == 'Alive!\n' 52 | 53 | 54 | def test_reset_module(runner, mock_rak811): 55 | result = runner.invoke(cli, ['-v', 'reset', 'module']) 56 | mock_rak811.return_value.reset.assert_called_once_with(Reset.Module) 57 | assert result.output.startswith('Module reset') 58 | 59 | 60 | def test_reset_lora(runner, mock_rak811): 61 | result = runner.invoke(cli, ['-v', 'reset', 'lora']) 62 | mock_rak811.return_value.reset.assert_called_once_with(Reset.LoRa) 63 | assert result.output.startswith('LoRa reset') 64 | 65 | 66 | def test_reload(runner, mock_rak811): 67 | result = runner.invoke(cli, ['-v', 'reload']) 68 | mock_rak811.return_value.reload.assert_called_once() 69 | assert result.output == 'Configuration reloaded.\n' 70 | 71 | 72 | def test_mode(runner, mock_rak811): 73 | p = PropertyMock(return_value=Mode.LoRaWan) 74 | type(mock_rak811.return_value).mode = p 75 | result = runner.invoke(cli, ['mode']) 76 | p.assert_called_once_with() 77 | assert result.output == 'LoRaWan\n' 78 | 79 | 80 | def test_mode_lora(runner, mock_rak811): 81 | p = PropertyMock() 82 | type(mock_rak811.return_value).mode = p 83 | result = runner.invoke(cli, ['-v', 'mode', 'LoRawan']) 84 | p.assert_called_once_with(Mode.LoRaWan) 85 | assert result.output == 'Mode set to LoRaWan.\n' 86 | 87 | 88 | def test_recv_ex(runner, mock_rak811): 89 | p = PropertyMock(return_value=RecvEx.Enabled) 90 | type(mock_rak811.return_value).recv_ex = p 91 | result = runner.invoke(cli, ['recv-ex']) 92 | p.assert_called_once_with() 93 | assert result.output == 'Enabled\n' 94 | 95 | 96 | def test_recv_ex_disabled(runner, mock_rak811): 97 | p = PropertyMock() 98 | type(mock_rak811.return_value).recv_ex = p 99 | result = runner.invoke(cli, ['-v', 'recv-ex', 'disable']) 100 | p.assert_called_once_with(RecvEx.Disabled) 101 | assert result.output == 'RSSI & SNR report on receive Disabled.\n' 102 | 103 | 104 | def test_band(runner, mock_rak811): 105 | p = PropertyMock(return_value='US915') 106 | type(mock_rak811.return_value).band = p 107 | result = runner.invoke(cli, ['band']) 108 | p.assert_called_once_with() 109 | assert result.output == 'US915\n' 110 | 111 | 112 | def test_band_eu868(runner, mock_rak811): 113 | p = PropertyMock() 114 | type(mock_rak811.return_value).band = p 115 | result = runner.invoke(cli, ['-v', 'band', 'EU868']) 116 | p.assert_called_once_with('EU868') 117 | assert result.output == 'LoRaWan region set to EU868.\n' 118 | 119 | 120 | def test_set_config(runner, mock_rak811): 121 | result = runner.invoke(cli, ['-v', 'set-config', 'dr=0', 'adr=on']) 122 | mock_rak811.return_value.set_config.assert_called_once_with(dr='0', 123 | adr='on') 124 | assert result.output == 'LoRaWan parameters set\n' 125 | 126 | 127 | def test_set_config_invalid(runner, mock_rak811): 128 | mock_rak811.return_value.set_config.side_effect = Rak811ResponseError(-1) 129 | result = runner.invoke(cli, ['-v', 'set-config', 'dr=0', 'adr=out']) 130 | mock_rak811.return_value.set_config.assert_called_once_with(dr='0', 131 | adr='out') 132 | assert result.output == 'RAK811 response error -1: Invalid argument\n' 133 | 134 | 135 | def test_set_config_nokv(runner, mock_rak811): 136 | result = runner.invoke(cli, ['-v', 'set-config', 'dr:0']) 137 | assert 'dr:0 is not a valid Key=Value parameter' in result.output 138 | 139 | 140 | def test_set_config_badkey(runner, mock_rak811): 141 | result = runner.invoke(cli, ['-v', 'set-config', 'dx=0']) 142 | assert 'dx is not a valid config key' in result.output 143 | 144 | 145 | def test_get_config(runner, mock_rak811): 146 | mock_rak811.return_value.get_config.return_value = 5 147 | result = runner.invoke(cli, ['-v', 'get-config', 'dr']) 148 | mock_rak811.return_value.get_config.assert_called_once() 149 | assert result.output == '5\n' 150 | 151 | 152 | def test_get_config_error(runner, mock_rak811): 153 | mock_rak811.return_value.get_config.side_effect = Rak811ResponseError(-1) 154 | result = runner.invoke(cli, ['-v', 'get-config', 'nwks_key']) 155 | mock_rak811.return_value.get_config.assert_called_once() 156 | assert result.output == 'RAK811 response error -1: Invalid argument\n' 157 | 158 | 159 | def test_join_otaa(runner, mock_rak811): 160 | result = runner.invoke(cli, ['-v', 'join-otaa']) 161 | mock_rak811.return_value.join_otaa.assert_called_once() 162 | assert result.output == 'Joined in OTAA mode\n' 163 | 164 | 165 | def test_join_otaa_event(runner, mock_rak811): 166 | mock_rak811.return_value.join_otaa.side_effect = Rak811EventError(4) 167 | result = runner.invoke(cli, ['-v', 'join-otaa']) 168 | mock_rak811.return_value.join_otaa.assert_called_once() 169 | assert result.output == 'RAK811 event error 4: Join failed\n' 170 | 171 | 172 | def test_join_otaa_timeout(runner, mock_rak811): 173 | mock_rak811.return_value.join_otaa.side_effect = Rak811TimeoutError( 174 | 'Timeout while waiting for event' 175 | ) 176 | result = runner.invoke(cli, ['-v', 'join-otaa']) 177 | mock_rak811.return_value.join_otaa.assert_called_once() 178 | assert result.output == 'RAK811 timeout: Timeout while waiting for event\n' 179 | 180 | 181 | def test_join_abp(runner, mock_rak811): 182 | result = runner.invoke(cli, ['-v', 'join-abp']) 183 | mock_rak811.return_value.join_abp.assert_called_once() 184 | assert result.output == 'Joined in ABP mode\n' 185 | 186 | 187 | def test_join_abp_response(runner, mock_rak811): 188 | mock_rak811.return_value.join_abp.side_effect = Rak811ResponseError(-3) 189 | result = runner.invoke(cli, ['-v', 'join-abp']) 190 | mock_rak811.return_value.join_abp.assert_called_once() 191 | assert result.output == 'RAK811 response error -3: ABP join error\n' 192 | 193 | 194 | def test_signal(runner, mock_rak811): 195 | p = PropertyMock(return_value=(-30, 26)) 196 | type(mock_rak811.return_value).signal = p 197 | result = runner.invoke(cli, ['signal']) 198 | p.assert_called_once_with() 199 | assert result.output == '-30 26\n' 200 | 201 | 202 | def test_dr(runner, mock_rak811): 203 | p = PropertyMock(return_value=5) 204 | type(mock_rak811.return_value).dr = p 205 | result = runner.invoke(cli, ['dr']) 206 | p.assert_called_once_with() 207 | assert result.output == '5\n' 208 | 209 | 210 | def test_set_dr(runner, mock_rak811): 211 | p = PropertyMock() 212 | type(mock_rak811.return_value).dr = p 213 | result = runner.invoke(cli, ['-v', 'dr', '5']) 214 | p.assert_called_once_with(5) 215 | assert result.output == 'Data rate set to 5.\n' 216 | 217 | 218 | def test_link_cnt(runner, mock_rak811): 219 | p = PropertyMock(return_value=(15, 2)) 220 | type(mock_rak811.return_value).link_cnt = p 221 | result = runner.invoke(cli, ['-v', 'link-cnt']) 222 | p.assert_called_once_with() 223 | assert result.output == 'Uplink: 15 - Downlink: 2\n' 224 | 225 | 226 | def test_abp_info(runner, mock_rak811): 227 | p = PropertyMock(return_value=( 228 | # cSpell:disable 229 | '13', 230 | '26dddddd', 231 | '9annnnnnnnnnnnnnnnnnnnnnnnnnnnnn', 232 | '0baaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 233 | # cSpell:enable 234 | )) 235 | type(mock_rak811.return_value).abp_info = p 236 | result = runner.invoke(cli, ['abp-info']) 237 | p.assert_called_once_with() 238 | assert result.output == ( 239 | # cSpell:disable 240 | '13 ' 241 | '26dddddd ' 242 | '9annnnnnnnnnnnnnnnnnnnnnnnnnnnnn ' 243 | '0baaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 244 | '\n' 245 | # cSpell:enable 246 | ) 247 | 248 | 249 | def test_abp_info_verbose(runner, mock_rak811): 250 | p = PropertyMock(return_value=( 251 | # cSpell:disable 252 | '13', 253 | '26dddddd', 254 | '9annnnnnnnnnnnnnnnnnnnnnnnnnnnnn', 255 | '0baaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 256 | # cSpell:enable 257 | )) 258 | type(mock_rak811.return_value).abp_info = p 259 | result = runner.invoke(cli, ['-v', 'abp-info']) 260 | p.assert_called_once_with() 261 | assert result.output == ( 262 | # cSpell:disable 263 | 'NwkId: 13\n' 264 | 'DevAddr: 26dddddd\n' 265 | 'Nwkskey: 9annnnnnnnnnnnnnnnnnnnnnnnnnnnnn\n' 266 | 'Appskey: 0baaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n' 267 | # cSpell:enable 268 | ) 269 | 270 | 271 | def test_send_unconfirmed(runner, mock_rak811): 272 | p = PropertyMock(return_value=0) 273 | type(mock_rak811.return_value).nb_downlinks = p 274 | result = runner.invoke(cli, ['-v', 'send', 'Hello']) 275 | mock_rak811.return_value.send.assert_called_once_with( 276 | data='Hello', 277 | confirm=False, 278 | port=1 279 | ) 280 | assert 'Message sent.' in result.output 281 | 282 | 283 | def test_send_confirmed(runner, mock_rak811): 284 | p = PropertyMock(return_value=0) 285 | type(mock_rak811.return_value).nb_downlinks = p 286 | result = runner.invoke(cli, ['-v', 'send', '--confirm', 287 | '--port', '2', 'Hello']) 288 | mock_rak811.return_value.send.assert_called_once_with( 289 | data='Hello', 290 | confirm=True, 291 | port=2 292 | ) 293 | assert 'Message sent.' in result.output 294 | assert 'No downlink available.' in result.output 295 | 296 | 297 | def test_send_binary(runner, mock_rak811): 298 | p = PropertyMock(return_value=0) 299 | type(mock_rak811.return_value).nb_downlinks = p 300 | result = runner.invoke(cli, ['-v', 'send', '--binary', '01020211']) 301 | mock_rak811.return_value.send.assert_called_once_with( 302 | data=bytes.fromhex('01020211'), 303 | confirm=False, 304 | port=1 305 | ) 306 | assert 'Message sent.' in result.output 307 | assert 'No downlink available.' in result.output 308 | 309 | 310 | def test_send_binary_invalid(runner, mock_rak811): 311 | result = runner.invoke(cli, ['-v', 'send', '--binary', '010202xx']) 312 | assert result.output == 'Invalid binary data\n' 313 | 314 | 315 | def test_send_error(runner, mock_rak811): 316 | mock_rak811.return_value.send.side_effect = Rak811EventError(5) 317 | p = PropertyMock(return_value=0) 318 | type(mock_rak811.return_value).nb_downlinks = p 319 | result = runner.invoke(cli, ['-v', 'send', '--binary', '01020211']) 320 | mock_rak811.return_value.send.assert_called_once_with( 321 | data=bytes.fromhex('01020211'), 322 | confirm=False, 323 | port=1 324 | ) 325 | assert result.output == 'RAK811 event error 5: Tx timeout\n' 326 | 327 | 328 | def test_send_receive_recv_tx(runner, mock_rak811): 329 | p = PropertyMock(return_value=1) 330 | type(mock_rak811.return_value).nb_downlinks = p 331 | mock_rak811.return_value.get_downlink.return_value = { 332 | 'port': 11, 333 | 'rssi': -34, 334 | 'snr': 27, 335 | 'len': 4, 336 | 'data': bytes.fromhex('65666768'), 337 | } 338 | result = runner.invoke(cli, ['-v', 'send', '--binary', '01020211']) 339 | mock_rak811.return_value.send.assert_called_once_with( 340 | data=bytes.fromhex('01020211'), 341 | confirm=False, 342 | port=1 343 | ) 344 | assert 'Downlink received' in result.output 345 | assert 'Port: 11' in result.output 346 | assert 'RSSI: -34' in result.output 347 | assert 'SNR: 27' in result.output 348 | assert 'Data: 65666768' in result.output 349 | 350 | 351 | def test_send_receive(runner, mock_rak811): 352 | p = PropertyMock(return_value=1) 353 | type(mock_rak811.return_value).nb_downlinks = p 354 | mock_rak811.return_value.get_downlink.return_value = { 355 | 'port': 11, 356 | 'rssi': 0, 357 | 'snr': 0, 358 | 'len': 4, 359 | 'data': bytes.fromhex('65666768'), 360 | } 361 | result = runner.invoke(cli, ['-v', 'send', '--binary', '01020211']) 362 | mock_rak811.return_value.send.assert_called_once_with( 363 | data=bytes.fromhex('01020211'), 364 | confirm=False, 365 | port=1 366 | ) 367 | assert 'Downlink received' in result.output 368 | assert 'Port: 11' in result.output 369 | assert 'RSSI' not in result.output 370 | assert 'SNR' not in result.output 371 | assert 'Data: 65666768' in result.output 372 | 373 | 374 | def test_send_receive_json(runner, mock_rak811): 375 | p = PropertyMock(return_value=1) 376 | type(mock_rak811.return_value).nb_downlinks = p 377 | mock_rak811.return_value.get_downlink.return_value = { 378 | 'port': 11, 379 | 'rssi': -34, 380 | 'snr': 27, 381 | 'len': 4, 382 | 'data': bytes.fromhex('65666768'), 383 | } 384 | result = runner.invoke(cli, ['-v', 'send', '--json', 385 | '--binary', '01020211']) 386 | mock_rak811.return_value.send.assert_called_once_with( 387 | data=bytes.fromhex('01020211'), 388 | confirm=False, 389 | port=1 390 | ) 391 | assert ('"port": 11') in result.output 392 | assert ('"rssi": -34') in result.output 393 | assert ('"snr": 27') in result.output 394 | assert ('"len": 4') in result.output 395 | assert ('"data": "65666768"') in result.output 396 | 397 | 398 | def test_set_rf_config_invalid_parameter(runner, mock_rak811): 399 | result = runner.invoke(cli, ['rf-config', 'tx=8']) 400 | assert ( 401 | "Error: Invalid value for 'KEY=VALUE...': " 402 | 'tx is not a valid config key' 403 | ) in result.output 404 | 405 | 406 | def test_set_rf_config_invalid_kv(runner, mock_rak811): 407 | result = runner.invoke(cli, ['rf-config', 'sf:8']) 408 | assert ( 409 | "Error: Invalid value for 'KEY=VALUE...': " 410 | 'sf:8 is not a valid Key=Value parameter' 411 | ) in result.output 412 | 413 | 414 | def test_set_rf_config_invalid_range(runner, mock_rak811): 415 | result = runner.invoke(cli, ['rf-config', 'sf=1']) 416 | assert ( 417 | "Error: Invalid value for 'KEY=VALUE...': " 418 | '1 is not in the range 6<=x<=12.\n' 419 | ) in result.output 420 | 421 | 422 | def test_set_rf_config_one(runner, mock_rak811): 423 | p = PropertyMock() 424 | type(mock_rak811.return_value).rf_config = p 425 | result = runner.invoke(cli, ['-v', 'rf-config', 'sf=8']) 426 | p.assert_called_once_with({ 427 | 'sf': 8 428 | }) 429 | assert result.output == 'rf_config set: sf=8\n' 430 | 431 | 432 | def test_set_rf_config_all(runner, mock_rak811): 433 | p = PropertyMock() 434 | type(mock_rak811.return_value).rf_config = p 435 | result = runner.invoke(cli, [ 436 | '-v', 'rf-config', 437 | 'freq=868.200', 438 | 'sf=8', 439 | 'bw=1', 440 | 'cr=2', 441 | 'prlen=16', 442 | 'pwr=8' 443 | ]) 444 | p.assert_called_once_with({ 445 | 'freq': 868.200, 446 | 'sf': 8, 447 | 'bw': 1, 448 | 'cr': 2, 449 | 'prlen': 16, 450 | 'pwr': 8 451 | }) 452 | assert('freq=868.2') in result.output 453 | assert('sf=8') in result.output 454 | assert('bw=1') in result.output 455 | assert('cr=2') in result.output 456 | assert('prlen=16') in result.output 457 | assert('pwr=8') in result.output 458 | 459 | 460 | def test_get_rf_config(runner, mock_rak811): 461 | p = PropertyMock(return_value={ 462 | 'freq': 868.200, 463 | 'sf': 8, 464 | 'bw': 1, 465 | 'cr': 2, 466 | 'prlen': 16, 467 | 'pwr': 8 468 | }) 469 | type(mock_rak811.return_value).rf_config = p 470 | result = runner.invoke(cli, ['rf-config']) 471 | p.assert_called_once_with() 472 | assert result.output == '868.2 8 1 2 16 8\n' 473 | 474 | 475 | def test_get_rf_config_verbose(runner, mock_rak811): 476 | p = PropertyMock(return_value={ 477 | 'freq': 868.200, 478 | 'sf': 8, 479 | 'bw': 1, 480 | 'cr': 2, 481 | 'prlen': 16, 482 | 'pwr': 8 483 | }) 484 | type(mock_rak811.return_value).rf_config = p 485 | result = runner.invoke(cli, ['-v', 'rf-config']) 486 | p.assert_called_once_with() 487 | assert result.output == ( 488 | 'Frequency: 868.2\n' 489 | 'SF: 8\n' 490 | 'BW: 1\n' 491 | 'CR: 2\n' 492 | 'PrLen: 16\n' 493 | 'Power: 8\n' 494 | ) 495 | 496 | 497 | def test_txc(runner, mock_rak811): 498 | result = runner.invoke(cli, ['-v', 'txc', 'Hello']) 499 | mock_rak811.return_value.txc.assert_called_once_with( 500 | data='Hello', 501 | cnt=1, 502 | interval=60 503 | ) 504 | assert result.output == 'Message sent.\n' 505 | 506 | 507 | def test_txc_binary(runner, mock_rak811): 508 | result = runner.invoke(cli, ['-v', 'txc', '--binary', '01020211']) 509 | mock_rak811.return_value.txc.assert_called_once_with( 510 | data=bytes.fromhex('01020211'), 511 | cnt=1, 512 | interval=60 513 | ) 514 | assert result.output == 'Message sent.\n' 515 | 516 | 517 | def test_txc_binary_invalid(runner, mock_rak811): 518 | result = runner.invoke(cli, ['-v', 'txc', '--binary', '010202xx']) 519 | assert result.output == 'Invalid binary data\n' 520 | 521 | 522 | def test_txc_error(runner, mock_rak811): 523 | mock_rak811.return_value.txc.side_effect = Rak811EventError(5) 524 | result = runner.invoke(cli, ['-v', 'txc', 'Hello']) 525 | mock_rak811.return_value.txc.assert_called_once_with( 526 | data='Hello', 527 | cnt=1, 528 | interval=60 529 | ) 530 | assert result.output == 'RAK811 event error 5: Tx timeout\n' 531 | 532 | 533 | def test_rxc(runner, mock_rak811): 534 | result = runner.invoke(cli, ['-v', 'rxc']) 535 | mock_rak811.return_value.rxc.assert_called_once() 536 | assert result.output == 'Module set in receive mode.\n' 537 | 538 | 539 | def test_tx_stop(runner, mock_rak811): 540 | result = runner.invoke(cli, ['-v', 'tx-stop']) 541 | mock_rak811.return_value.tx_stop.assert_called_once() 542 | assert result.output == 'LoraP2P TX stopped.\n' 543 | 544 | 545 | def test_rx_stop(runner, mock_rak811): 546 | result = runner.invoke(cli, ['-v', 'rx-stop']) 547 | mock_rak811.return_value.rx_stop.assert_called_once() 548 | assert result.output == 'LoraP2P RX stopped.\n' 549 | 550 | 551 | def test_rx_get_no_message(runner, mock_rak811): 552 | p = PropertyMock(return_value=0) 553 | type(mock_rak811.return_value).nb_downlinks = p 554 | result = runner.invoke(cli, ['-v', 'rx-get', '0']) 555 | mock_rak811.return_value.rx_get.assert_called_once_with(0) 556 | assert result.output == 'No message available.\n' 557 | 558 | 559 | def test_rx_get_message(runner, mock_rak811): 560 | p = PropertyMock(return_value=1) 561 | type(mock_rak811.return_value).nb_downlinks = p 562 | mock_rak811.return_value.get_downlink.return_value = { 563 | 'port': 0, 564 | 'rssi': -34, 565 | 'snr': 27, 566 | 'len': 4, 567 | 'data': bytes.fromhex('65666768'), 568 | } 569 | result = runner.invoke(cli, ['rx-get', '0']) 570 | mock_rak811.return_value.rx_get.assert_called_once_with(0) 571 | assert result.output == '65666768\n' 572 | 573 | 574 | def test_rx_get_message_verbose(runner, mock_rak811): 575 | p = PropertyMock(return_value=1) 576 | type(mock_rak811.return_value).nb_downlinks = p 577 | mock_rak811.return_value.get_downlink.return_value = { 578 | 'port': 0, 579 | 'rssi': -34, 580 | 'snr': 27, 581 | 'len': 4, 582 | 'data': bytes.fromhex('65666768'), 583 | } 584 | result = runner.invoke(cli, ['-v', 'rx-get', '0']) 585 | mock_rak811.return_value.rx_get.assert_called_once_with(0) 586 | assert 'RSSI: -34' in result.output 587 | assert 'SNR: 27' in result.output 588 | assert 'Data: 65666768' in result.output 589 | 590 | 591 | def test_rx_get_message_json(runner, mock_rak811): 592 | p = PropertyMock(return_value=1) 593 | type(mock_rak811.return_value).nb_downlinks = p 594 | mock_rak811.return_value.get_downlink.return_value = { 595 | 'port': 0, 596 | 'rssi': 0, 597 | 'snr': 0, 598 | 'len': 4, 599 | 'data': bytes.fromhex('65666768'), 600 | } 601 | result = runner.invoke(cli, ['rx-get', '--json', '0']) 602 | mock_rak811.return_value.rx_get.assert_called_once_with(0) 603 | assert '"port": 0' in result.output 604 | assert '"data": "65666768"' in result.output 605 | 606 | 607 | def test_radio_status(runner, mock_rak811): 608 | p = PropertyMock(return_value=(8, 0, 1, 0, 0, -48, 28)) 609 | type(mock_rak811.return_value).radio_status = p 610 | result = runner.invoke(cli, ['radio-status']) 611 | p.assert_called_once_with() 612 | assert result.output == ( 613 | '8 0 1 0 0 -48 28\n' 614 | ) 615 | 616 | 617 | def test_radio_status_verbose(runner, mock_rak811): 618 | p = PropertyMock(return_value=(8, 0, 1, 0, 0, -48, 28)) 619 | type(mock_rak811.return_value).radio_status = p 620 | result = runner.invoke(cli, ['-v', 'radio-status']) 621 | p.assert_called_once_with() 622 | assert result.output == ( 623 | 'TxSuccessCnt: 8\n' 624 | 'TxErrCnt: 0\n' 625 | 'RxSuccessCnt: 1\n' 626 | 'RxTimeOutCnt: 0\n' 627 | 'RxErrCnt: 0\n' 628 | 'RSSI: -48\n' 629 | 'SNR: 28\n' 630 | ) 631 | 632 | 633 | def test_clear_radio_status(runner, mock_rak811): 634 | result = runner.invoke(cli, ['-v', 'clear-radio-status']) 635 | mock_rak811.return_value.clear_radio_status.assert_called_once() 636 | assert result.output == 'Radio statistics cleared.\n' 637 | -------------------------------------------------------------------------------- /tests/test_cli_v3.py: -------------------------------------------------------------------------------- 1 | """Units tests for the CLI (V3 firmware). 2 | 3 | Rak811 is tested separately and therefore mocked in this suite. 4 | """ 5 | from click.testing import CliRunner 6 | from mock import Mock, patch, PropertyMock 7 | from pytest import fixture 8 | # Ignore RPi.GPIO 9 | p = patch.dict('sys.modules', {'RPi': Mock()}) 10 | p.start() 11 | from rak811.cli_v3 import cli # noqa: E402 12 | from rak811.rak811_v3 import Rak811ResponseError, Rak811TimeoutError # noqa: E402 13 | 14 | 15 | @fixture 16 | def mock_rak811(): 17 | with patch('rak811.cli_v3.Rak811', autospec=True) as p: 18 | yield p 19 | 20 | 21 | @fixture 22 | def runner(): 23 | return CliRunner() 24 | 25 | 26 | def test_set_config(runner, mock_rak811): 27 | mock_rak811.return_value.set_config.return_value = [' '] 28 | result = runner.invoke(cli, ['-v', 'set-config', 'lora:confirm:1']) 29 | mock_rak811.return_value.set_config.assert_called_once_with('lora:confirm:1') 30 | assert result.output == 'Configuration done\n' 31 | 32 | 33 | def test_set_config_multi(runner, mock_rak811): 34 | mock_rak811.return_value.set_config.return_value = ['LoRa work mode:LoRaWAN'] 35 | result = runner.invoke(cli, ['-v', 'set-config', 'lora:confirm:1']) 36 | mock_rak811.return_value.set_config.assert_called_once_with('lora:confirm:1') 37 | assert result.output == 'Configuration done\nLoRa work mode:LoRaWAN\n' 38 | 39 | 40 | def test_set_config_error(runner, mock_rak811): 41 | mock_rak811.return_value.set_config.side_effect = Rak811ResponseError(1) 42 | result = runner.invoke(cli, ['-v', 'set-config', 'xxx']) 43 | mock_rak811.return_value.set_config.assert_called_once_with('xxx') 44 | assert result.output == 'RAK811 response error 1: Unsupported AT command\n' 45 | 46 | 47 | def test_get_config(runner, mock_rak811): 48 | mock_rak811.return_value.get_config.return_value = ['0'] 49 | result = runner.invoke(cli, ['-v', 'get-config', 'device:gpio:2']) 50 | mock_rak811.return_value.get_config.assert_called_once() 51 | assert result.output == '0\n' 52 | 53 | 54 | def test_get_config_error(runner, mock_rak811): 55 | mock_rak811.return_value.get_config.side_effect = Rak811ResponseError(2) 56 | result = runner.invoke(cli, ['-v', 'get-config', 'xxx']) 57 | mock_rak811.return_value.get_config.assert_called_once() 58 | assert result.output == 'RAK811 response error 2: Invalid parameter in AT command\n' 59 | 60 | 61 | def test_hard_reset(runner, mock_rak811): 62 | result = runner.invoke(cli, ['-v', 'hard-reset']) 63 | mock_rak811.return_value.hard_reset.assert_called_once() 64 | assert result.output == 'Hard reset complete\n' 65 | 66 | 67 | def test_version(runner, mock_rak811): 68 | p = PropertyMock(return_value='V3.0.0.14.H') 69 | type(mock_rak811.return_value).version = p 70 | result = runner.invoke(cli, ['version']) 71 | p.assert_called_once_with() 72 | assert result.output == 'V3.0.0.14.H\n' 73 | 74 | 75 | def test_help(runner, mock_rak811): 76 | p = PropertyMock(return_value=['Help text']) 77 | type(mock_rak811.return_value).help = p 78 | result = runner.invoke(cli, ['help']) 79 | p.assert_called_once_with() 80 | assert result.output == 'Help text\n' 81 | 82 | 83 | def test_run(runner, mock_rak811): 84 | p = PropertyMock(return_value=['Initialization OK']) 85 | type(mock_rak811.return_value).run = p 86 | runner.invoke(cli, ['run']) 87 | p.assert_called_once_with() 88 | 89 | 90 | def test_send_uart(runner, mock_rak811): 91 | result = runner.invoke(cli, ['-v', 'send-uart', 'Hello']) 92 | mock_rak811.return_value.send_uart.assert_called_once_with( 93 | data='Hello', 94 | index=3 95 | ) 96 | assert 'Data sent.' in result.output 97 | 98 | 99 | def test_send_uart_binary(runner, mock_rak811): 100 | result = runner.invoke(cli, ['-v', 'send-uart', '--binary', '01020211']) 101 | mock_rak811.return_value.send_uart.assert_called_once_with( 102 | data=bytes.fromhex('01020211'), 103 | index=3 104 | ) 105 | assert 'Data sent.' in result.output 106 | 107 | 108 | def test_send_uart_binary_invalid(runner, mock_rak811): 109 | result = runner.invoke(cli, ['-v', 'send-uart', '--binary', '010202xx']) 110 | assert result.output == 'Invalid binary data\n' 111 | 112 | 113 | def test_send_uart_error(runner, mock_rak811): 114 | mock_rak811.return_value.send_uart.side_effect = Rak811ResponseError(5) 115 | result = runner.invoke(cli, ['-v', 'send-uart', 'Hello']) 116 | mock_rak811.return_value.send_uart.assert_called_once_with( 117 | data='Hello', 118 | index=3 119 | ) 120 | assert result.output == 'RAK811 response error 5: Error sending through UART\n' 121 | 122 | 123 | def test_join(runner, mock_rak811): 124 | result = runner.invoke(cli, ['-v', 'join']) 125 | mock_rak811.return_value.join.assert_called_once() 126 | assert result.output == 'Joined!\n' 127 | 128 | 129 | def test_join_error(runner, mock_rak811): 130 | mock_rak811.return_value.join.side_effect = Rak811ResponseError(99) 131 | result = runner.invoke(cli, ['-v', 'join']) 132 | mock_rak811.return_value.join.assert_called_once() 133 | assert result.output == 'RAK811 response error 99: LoRa join failed\n' 134 | 135 | 136 | def test_join_timeout(runner, mock_rak811): 137 | mock_rak811.return_value.join.side_effect = Rak811TimeoutError( 138 | 'Timeout while waiting for data' 139 | ) 140 | result = runner.invoke(cli, ['-v', 'join']) 141 | mock_rak811.return_value.join.assert_called_once() 142 | assert result.output == 'RAK811 timeout: Timeout while waiting for data\n' 143 | 144 | 145 | def test_send_unconfirmed(runner, mock_rak811): 146 | p = PropertyMock(return_value=0) 147 | type(mock_rak811.return_value).nb_downlinks = p 148 | result = runner.invoke(cli, ['-v', 'send', 'Hello']) 149 | mock_rak811.return_value.send.assert_called_once_with( 150 | data='Hello', 151 | port=1 152 | ) 153 | assert 'Message sent.' in result.output 154 | 155 | 156 | def test_send_confirmed(runner, mock_rak811): 157 | p = PropertyMock(return_value=1) 158 | type(mock_rak811.return_value).nb_downlinks = p 159 | mock_rak811.return_value.get_downlink.return_value = { 160 | 'port': 0, 161 | 'rssi': -34, 162 | 'snr': 27, 163 | 'len': 0, 164 | 'data': '', 165 | } 166 | result = runner.invoke(cli, ['-v', 'send', '--port', '2', 'Hello']) 167 | mock_rak811.return_value.send.assert_called_once_with( 168 | data='Hello', 169 | port=2 170 | ) 171 | assert 'Message sent.' in result.output 172 | assert 'Send confirmed.' in result.output 173 | 174 | 175 | def test_send_binary(runner, mock_rak811): 176 | p = PropertyMock(return_value=0) 177 | type(mock_rak811.return_value).nb_downlinks = p 178 | result = runner.invoke(cli, ['-v', 'send', '--binary', '01020211']) 179 | mock_rak811.return_value.send.assert_called_once_with( 180 | data=bytes.fromhex('01020211'), 181 | port=1 182 | ) 183 | assert 'Message sent.' in result.output 184 | assert 'No downlink available.' in result.output 185 | 186 | 187 | def test_send_binary_invalid(runner, mock_rak811): 188 | result = runner.invoke(cli, ['-v', 'send', '--binary', '010202xx']) 189 | assert result.output == 'Invalid binary data\n' 190 | 191 | 192 | def test_send_error(runner, mock_rak811): 193 | mock_rak811.return_value.send.side_effect = Rak811ResponseError(94) 194 | p = PropertyMock(return_value=0) 195 | type(mock_rak811.return_value).nb_downlinks = p 196 | result = runner.invoke(cli, ['-v', 'send', '--binary', '01020211']) 197 | mock_rak811.return_value.send.assert_called_once_with( 198 | data=bytes.fromhex('01020211'), 199 | port=1 200 | ) 201 | assert result.output == 'RAK811 response error 94: LoRa transmiting timeout\n' 202 | 203 | 204 | def test_send_receive(runner, mock_rak811): 205 | p = PropertyMock(return_value=1) 206 | type(mock_rak811.return_value).nb_downlinks = p 207 | mock_rak811.return_value.get_downlink.return_value = { 208 | 'port': 11, 209 | 'rssi': -34, 210 | 'snr': 27, 211 | 'len': 4, 212 | 'data': bytes.fromhex('65666768'), 213 | } 214 | result = runner.invoke(cli, ['-v', 'send', '--binary', '01020211']) 215 | mock_rak811.return_value.send.assert_called_once_with( 216 | data=bytes.fromhex('01020211'), 217 | port=1 218 | ) 219 | assert 'Downlink received' in result.output 220 | assert 'Port: 11' in result.output 221 | assert 'RSSI: -34' in result.output 222 | assert 'SNR: 27' in result.output 223 | assert 'Data: 65666768' in result.output 224 | 225 | 226 | def test_send_receive_json(runner, mock_rak811): 227 | p = PropertyMock(return_value=1) 228 | type(mock_rak811.return_value).nb_downlinks = p 229 | mock_rak811.return_value.get_downlink.return_value = { 230 | 'port': 11, 231 | 'rssi': -34, 232 | 'snr': 27, 233 | 'len': 4, 234 | 'data': bytes.fromhex('65666768'), 235 | } 236 | result = runner.invoke(cli, ['-v', 'send', '--json', 237 | '--binary', '01020211']) 238 | mock_rak811.return_value.send.assert_called_once_with( 239 | data=bytes.fromhex('01020211'), 240 | port=1 241 | ) 242 | assert ('"port": 11') in result.output 243 | assert ('"rssi": -34') in result.output 244 | assert ('"snr": 27') in result.output 245 | assert ('"len": 4') in result.output 246 | assert ('"data": "65666768"') in result.output 247 | 248 | 249 | def test_send_p2p(runner, mock_rak811): 250 | result = runner.invoke(cli, ['-v', 'send-p2p', 'Hello']) 251 | mock_rak811.return_value.send_p2p.assert_called_once_with( 252 | data='Hello' 253 | ) 254 | assert 'Message sent.' in result.output 255 | 256 | 257 | def test_send_p2p_binary(runner, mock_rak811): 258 | result = runner.invoke(cli, ['-v', 'send-p2p', '--binary', '01020211']) 259 | mock_rak811.return_value.send_p2p.assert_called_once_with( 260 | data=bytes.fromhex('01020211') 261 | ) 262 | assert 'Message sent.' in result.output 263 | 264 | 265 | def test_send_p2p_binary_invalid(runner, mock_rak811): 266 | result = runner.invoke(cli, ['-v', 'send-p2p', '--binary', '010202xx']) 267 | assert result.output == 'Invalid binary data\n' 268 | 269 | 270 | def test_send_p2p_error(runner, mock_rak811): 271 | mock_rak811.return_value.send_p2p.side_effect = Rak811ResponseError(93) 272 | result = runner.invoke(cli, ['-v', 'send-p2p', 'Hello']) 273 | mock_rak811.return_value.send_p2p.assert_called_once_with( 274 | data='Hello' 275 | ) 276 | assert result.output == 'RAK811 response error 93: Status is error\n' 277 | 278 | 279 | def test_receive_p2p(runner, mock_rak811): 280 | p = PropertyMock(return_value=1) 281 | type(mock_rak811.return_value).nb_downlinks = p 282 | mock_rak811.return_value.get_downlink.return_value = { 283 | 'port': 0, 284 | 'rssi': -34, 285 | 'snr': 27, 286 | 'len': 4, 287 | 'data': bytes.fromhex('65666768'), 288 | } 289 | result = runner.invoke(cli, ['-v', 'receive-p2p', '10']) 290 | mock_rak811.return_value.receive_p2p.assert_called_once_with(10) 291 | assert 'Message received' in result.output 292 | assert 'RSSI: -34' in result.output 293 | assert 'SNR: 27' in result.output 294 | assert 'Data: 65666768' in result.output 295 | 296 | 297 | def test_receive_p2p_json(runner, mock_rak811): 298 | p = PropertyMock(return_value=1) 299 | type(mock_rak811.return_value).nb_downlinks = p 300 | mock_rak811.return_value.get_downlink.return_value = { 301 | 'port': 0, 302 | 'rssi': -34, 303 | 'snr': 27, 304 | 'len': 4, 305 | 'data': bytes.fromhex('65666768'), 306 | } 307 | result = runner.invoke(cli, ['-v', 'receive-p2p', '--json', '10']) 308 | mock_rak811.return_value.receive_p2p.assert_called_once_with(10) 309 | assert ('"port": 0') in result.output 310 | assert ('"rssi": -34') in result.output 311 | assert ('"snr": 27') in result.output 312 | assert ('"len": 4') in result.output 313 | assert ('"data": "65666768"') in result.output 314 | -------------------------------------------------------------------------------- /tests/test_rak811.py: -------------------------------------------------------------------------------- 1 | """Units tests for the Rak811 class. 2 | 3 | Rak811Serial is tested separately and therefore mocked in this suite. 4 | 5 | RPi.GPIO is completely ignored as not available on all platforms, and its use 6 | by the Rak811 class very limited. 7 | """ 8 | from mock import call, Mock, patch 9 | from pytest import fixture, raises 10 | # Ignore RPi.GPIO 11 | p = patch.dict('sys.modules', {'RPi': Mock()}) 12 | p.start() 13 | from rak811.rak811 import Mode, RecvEx, Reset # noqa: E402 14 | from rak811.rak811 import Rak811, Rak811EventError, \ 15 | Rak811ResponseError # noqa: E402 16 | from rak811.rak811 import Rak811Serial # noqa: E402 17 | from rak811.rak811 import RESPONSE_TIMEOUT # noqa: E402 18 | from rak811.serial import Rak811TimeoutError # noqa: E402 19 | 20 | 21 | @fixture 22 | def lora(): 23 | """Instantiate Rak811 class though fixture.""" 24 | with patch('rak811.rak811.Rak811Serial', autospec=True): 25 | rak811 = Rak811() 26 | yield rak811 27 | rak811.close() 28 | 29 | 30 | @patch('rak811.rak811.Rak811Serial', autospec=True) 31 | def test_instantiate_default(mock_serial): 32 | """Test that Rak811 can be instantiated. 33 | 34 | Check for basic initialisation and teardown of the RackSerial. 35 | """ 36 | lora = Rak811() 37 | 38 | assert isinstance(lora._serial, Rak811Serial) 39 | mock_serial.assert_called_once_with(read_buffer_timeout=RESPONSE_TIMEOUT) 40 | lora.close() 41 | mock_serial.return_value.close.assert_called_once() 42 | 43 | 44 | @patch('rak811.rak811.Rak811Serial', autospec=True) 45 | def test_instantiate_params(mock_serial): 46 | """Test that Rak811 passes parameters passed to RackSerial.""" 47 | port = '/dev/ttyAMA0' 48 | timeout = 5 49 | lora = Rak811(port=port, timeout=timeout) 50 | mock_serial.assert_called_once_with( 51 | read_buffer_timeout=RESPONSE_TIMEOUT, 52 | port=port, 53 | timeout=timeout 54 | ) 55 | lora.close() 56 | 57 | 58 | @patch('rak811.rak811.GPIO') 59 | @patch('rak811.rak811.sleep') 60 | def test_hard_reset(mock_sleep, mock_gpio, lora): 61 | """Test hard_reset(). 62 | 63 | Test is a bit pointless, at least ensures it runs... 64 | """ 65 | lora.hard_reset() 66 | mock_gpio.setup.assert_called_once() 67 | mock_gpio.cleanup.assert_not_called() 68 | 69 | 70 | def test_int(lora): 71 | """Test _int function.""" 72 | assert lora._int(1) == 1 73 | assert lora._int('1') == 1 74 | assert lora._int('Hello') == 'Hello' 75 | 76 | 77 | def test_send_string(lora): 78 | """Test _send_string (passthrough to RackSerial).""" 79 | lora._send_string('Hello') 80 | lora._serial.send_string.assert_called_with('Hello') 81 | 82 | 83 | def test_send_command(lora): 84 | """Test _send_command.""" 85 | # Successful command 86 | lora._serial.receive.return_value = 'OK0' 87 | assert lora._send_command('dr') == '0' 88 | lora._serial.send_command.assert_called_with('dr') 89 | 90 | # Error 91 | lora._serial.receive.return_value = 'ERROR-1' 92 | with raises(Rak811ResponseError, 93 | match='-1'): 94 | lora._send_command('mode=2') 95 | 96 | # Unknown error 97 | lora._serial.receive.return_value = 'Unexpected' 98 | with raises(Rak811ResponseError, 99 | match='Unexpected'): 100 | lora._send_command('mode=2') 101 | 102 | # Events in response queue 103 | lora._serial.receive.side_effect = [ 104 | 'at+recv=2,0,0', 105 | 'OK0' 106 | ] 107 | assert lora._send_command('dr') == '0' 108 | 109 | 110 | def test_get_events(lora): 111 | """Test _get_events.""" 112 | # Successful command 113 | lora._serial.receive.return_value = [ 114 | 'at+recv=2,0,0', 115 | 'at+recv=0,1,0,0,1,55', 116 | ] 117 | events = lora._get_events() 118 | assert events.pop() == '0,1,0,0,1,55' 119 | assert events.pop() == '2,0,0' 120 | 121 | 122 | """AT command API. 123 | 124 | For the sake of simplicity we mock Rak811.__send_command and 125 | Rack811._get_events, as these have already been tested. 126 | """ 127 | 128 | 129 | @patch.object(Rak811, '_send_command', return_value='2.0.3.0') 130 | def test_version(mock_send, lora): 131 | """Test version command.""" 132 | assert lora.version == '2.0.3.0' 133 | mock_send.assert_called_once_with('version') 134 | 135 | 136 | @patch.object(Rak811, '_send_command') 137 | def test_sleep(mock_send, lora): 138 | """Test sleep command.""" 139 | lora.sleep() 140 | mock_send.assert_called_once_with('sleep') 141 | 142 | 143 | @patch.object(Rak811, '_get_events', return_value=['8,0,0']) 144 | @patch.object(Rak811, '_send_string') 145 | def test_wake_up(mock_send, mock_events, lora): 146 | """Test wake_up command.""" 147 | lora.wake_up() 148 | mock_send.assert_called_once() 149 | mock_events.assert_called_once() 150 | 151 | 152 | @patch.object(Rak811, 'hard_reset') 153 | @patch.object(Rak811, '_send_command') 154 | def test_reset_module(mock_send, mock_hard_reset, lora): 155 | """Test reset module command.""" 156 | lora.reset(Reset.Module) 157 | mock_send.assert_called_once_with('reset=0') 158 | mock_hard_reset.assert_called_once() 159 | 160 | 161 | @patch.object(Rak811, 'hard_reset') 162 | @patch.object(Rak811, '_send_command') 163 | def test_reset_lora(mock_send, mock_hard_reset, lora): 164 | """Test reset lora command.""" 165 | lora.reset(Reset.LoRa) 166 | mock_send.assert_called_once_with('reset=1') 167 | mock_hard_reset.assert_not_called() 168 | 169 | 170 | @patch.object(Rak811, '_send_command') 171 | def test_reload(mock_send, lora): 172 | """Test reload command.""" 173 | lora.reload() 174 | mock_send.assert_called_once_with('reload') 175 | 176 | 177 | @patch.object(Rak811, '_send_command') 178 | def test_set_mode(mock_send, lora): 179 | """Test mode setter.""" 180 | lora.mode = Mode.LoRaWan 181 | mock_send.assert_called_once_with('mode=0') 182 | 183 | 184 | @patch.object(Rak811, '_send_command', return_value='0') 185 | def test_get_mode(mock_send, lora): 186 | """Test mode getter.""" 187 | assert lora.mode == 0 188 | mock_send.assert_called_once_with('mode') 189 | 190 | 191 | @patch.object(Rak811, '_send_command') 192 | def test_set_recv_ex(mock_send, lora): 193 | """Test recv_ex setter.""" 194 | lora.recv_ex = RecvEx.Disabled 195 | mock_send.assert_called_once_with('recv_ex=1') 196 | 197 | 198 | @patch.object(Rak811, '_send_command', return_value='1') 199 | def test_get_recv_ex(mock_send, lora): 200 | """Test recv_ex getter.""" 201 | assert lora.recv_ex == 1 202 | mock_send.assert_called_once_with('recv_ex') 203 | 204 | 205 | @patch.object(Rak811, '_send_command') 206 | def test_set_config(mock_send, lora): 207 | """Test config setter.""" 208 | lora.set_config(adr='on', dr=5) 209 | assert len(mock_send.mock_calls) == 1 210 | assert mock_send.mock_calls[0] in ( 211 | call('set_config=adr:on&dr:5'), 212 | call('set_config=dr:5&adr:on') 213 | ) 214 | 215 | 216 | @patch.object(Rak811, '_send_command', return_value='1') 217 | def test_get_config(mock_send, lora): 218 | """Test config getter.""" 219 | assert lora.get_config('dr') == '1' 220 | mock_send.assert_called_once_with('get_config=dr') 221 | 222 | 223 | @patch.object(Rak811, '_send_command') 224 | def test_set_band(mock_send, lora): 225 | """Test band setter.""" 226 | lora.band = 'EU868' 227 | mock_send.assert_called_once_with('band=EU868') 228 | 229 | 230 | @patch.object(Rak811, '_send_command', return_value='EU868') 231 | def test_get_band(mock_send, lora): 232 | """Test band getter.""" 233 | assert lora.band == 'EU868' 234 | mock_send.assert_called_once_with('band') 235 | 236 | 237 | @patch.object(Rak811, '_send_command') 238 | def test_join_abp(mock_send, lora): 239 | """Test join_abp command.""" 240 | lora.join_abp() 241 | mock_send.assert_called_once_with('join=abp') 242 | 243 | 244 | @patch.object(Rak811, '_get_events', return_value=['3,0,0']) 245 | @patch.object(Rak811, '_send_command') 246 | def test_join_otaa_success(mock_send, mock_events, lora): 247 | """Test join_abp command, successful.""" 248 | lora.join_otaa() 249 | mock_send.assert_called_once_with('join=otaa') 250 | mock_events.assert_called_once() 251 | 252 | 253 | @patch.object(Rak811, '_get_events', return_value=['4,0,0']) 254 | @patch.object(Rak811, '_send_command') 255 | def test_join_otaa_failure(mock_send, mock_events, lora): 256 | """Test join_abp command, failure.""" 257 | with raises(Rak811EventError, 258 | match='4'): 259 | lora.join_otaa() 260 | mock_send.assert_called_once_with('join=otaa') 261 | mock_events.assert_called_once() 262 | 263 | 264 | @patch.object(Rak811, '_send_command', return_value='-30,26') 265 | def test_signal(mock_send, lora): 266 | """Test signal command.""" 267 | assert lora.signal == (-30, 26) 268 | mock_send.assert_called_once_with('signal') 269 | 270 | 271 | @patch.object(Rak811, '_send_command') 272 | def test_set_dr(mock_send, lora): 273 | """Test dr setter.""" 274 | lora.dr = 5 275 | mock_send.assert_called_once_with('dr=5') 276 | 277 | 278 | @patch.object(Rak811, '_send_command', return_value='5') 279 | def test_get_dr(mock_send, lora): 280 | """Test dr getter.""" 281 | assert lora.dr == 5 282 | mock_send.assert_called_once_with('dr') 283 | 284 | 285 | @patch.object(Rak811, '_send_command') 286 | def test_set_link_cnt(mock_send, lora): 287 | """Test link_cnt setter.""" 288 | lora.link_cnt = (15, 2) 289 | mock_send.assert_called_once_with('link_cnt=15,2') 290 | 291 | 292 | @patch.object(Rak811, '_send_command', return_value='15,2') 293 | def test_get_link_cnt(mock_send, lora): 294 | """Test link_cnt getter.""" 295 | assert lora.link_cnt == (15, 2) 296 | mock_send.assert_called_once_with('link_cnt') 297 | 298 | 299 | @patch.object(Rak811, '_send_command', return_value=( 300 | # cSpell:disable 301 | '13,' 302 | '26dddddd,' 303 | '9annnnnnnnnnnnnnnnnnnnnnnnnnnnnn,' 304 | '0baaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 305 | # cSpell:enable 306 | )) 307 | def test_abp_info(mock_send, lora): 308 | """Test abp_info command.""" 309 | assert lora.abp_info == ( 310 | # cSpell:disable 311 | '13', 312 | '26dddddd', 313 | '9annnnnnnnnnnnnnnnnnnnnnnnnnnnnn', 314 | '0baaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 315 | # cSpell:enable 316 | ) 317 | mock_send.assert_called_once_with('abp_info') 318 | 319 | 320 | @patch.object(Rak811, '_get_events', return_value=['2,0,0']) 321 | @patch.object(Rak811, '_send_command') 322 | def test_get_send_unconfirmed(mock_send, mock_events, lora): 323 | """Test send, unconfirmed.""" 324 | lora.send('Hello') 325 | mock_send.assert_called_once_with('send=0,1,48656c6c6f') 326 | mock_events.assert_called_once() 327 | assert lora.nb_downlinks == 0 328 | 329 | 330 | @patch.object(Rak811, '_get_events', return_value=['1,0,0']) 331 | @patch.object(Rak811, '_send_command') 332 | def test_get_send_confirmed(mock_send, mock_events, lora): 333 | """Test send, confirmed.""" 334 | lora.send('Hello', port=2, confirm=True) 335 | mock_send.assert_called_once_with('send=1,2,48656c6c6f') 336 | mock_events.assert_called_once() 337 | assert lora.nb_downlinks == 0 338 | 339 | 340 | @patch.object(Rak811, '_get_events', return_value=['2,0,0']) 341 | @patch.object(Rak811, '_send_command') 342 | def test_get_send_binary(mock_send, mock_events, lora): 343 | """Test send, binary.""" 344 | lora.send(bytes.fromhex('01020211')) 345 | mock_send.assert_called_once_with('send=0,1,01020211') 346 | mock_events.assert_called_once() 347 | 348 | 349 | @patch.object(Rak811, '_get_events', return_value=['5,0,0']) 350 | @patch.object(Rak811, '_send_command') 351 | def test_get_send_fail(mock_send, mock_events, lora): 352 | """Test send, fail.""" 353 | with raises(Rak811EventError, 354 | match='5'): 355 | lora.send(bytes.fromhex('01020211')) 356 | mock_send.assert_called_once_with('send=0,1,01020211') 357 | mock_events.assert_called_once() 358 | 359 | 360 | @patch.object(Rak811, '_get_events', return_value=[ 361 | '2,0,0', 362 | '0,11,-34,27,4,65666768', 363 | ]) 364 | @patch.object(Rak811, '_send_command') 365 | def test_get_send_receive(mock_send, mock_events, lora): 366 | """Test send and receive.""" 367 | lora.send(bytes.fromhex('01020211')) 368 | mock_send.assert_called_once_with('send=0,1,01020211') 369 | mock_events.assert_called_once() 370 | assert lora.nb_downlinks == 1 371 | assert lora.get_downlink() == { 372 | 'port': 11, 373 | 'rssi': -34, 374 | 'snr': 27, 375 | 'len': 4, 376 | 'data': bytes.fromhex('65666768'), 377 | } 378 | 379 | 380 | @patch.object(Rak811, '_get_events', return_value=[ 381 | '2,0,0', 382 | '0,11,4,65666768', 383 | ]) 384 | @patch.object(Rak811, '_send_command') 385 | def test_get_send_receive_no_recv_ex(mock_send, mock_events, lora): 386 | """Test send and receive, recv_ex disabled.""" 387 | lora.send(bytes.fromhex('01020211')) 388 | mock_send.assert_called_once_with('send=0,1,01020211') 389 | mock_events.assert_called_once() 390 | assert lora.nb_downlinks == 1 391 | assert lora.get_downlink() == { 392 | 'port': 11, 393 | 'rssi': 0, 394 | 'snr': 0, 395 | 'len': 4, 396 | 'data': bytes.fromhex('65666768'), 397 | } 398 | 399 | 400 | @patch.object(Rak811, '_send_command', return_value='868700000,7,1,3,4,5') 401 | def test_get_rf_config(mock_send, lora): 402 | """Test config getter.""" 403 | assert lora.rf_config == { 404 | 'freq': 868.700, 405 | 'sf': 7, 406 | 'bw': 1, 407 | 'cr': 3, 408 | 'prlen': 4, 409 | 'pwr': 5 410 | } 411 | mock_send.assert_called_once_with('rf_config') 412 | 413 | 414 | @patch.object(Rak811, '_get_rf_config', return_value={ 415 | 'freq': 868.100, 416 | 'sf': 12, 417 | 'bw': 0, 418 | 'cr': 1, 419 | 'prlen': 8, 420 | 'pwr': 20 421 | }) 422 | @patch.object(Rak811, '_send_command') 423 | def test_set_rf_config_default(mock_send, mock_rf_config, lora): 424 | """Test RF config setter (No parameters).""" 425 | lora.rf_config = {} 426 | mock_rf_config.assert_called_once() 427 | mock_send.assert_called_once_with('rf_config=868100000,12,0,1,8,20') 428 | 429 | 430 | @patch.object(Rak811, '_get_rf_config', return_value={ 431 | 'freq': 868.100, 432 | 'sf': 12, 433 | 'bw': 1, 434 | 'cr': 1, 435 | 'prlen': 8, 436 | 'pwr': 20 437 | }) 438 | @patch.object(Rak811, '_send_command') 439 | def test_set_rf_config_partial(mock_send, mock_rf_config, lora): 440 | """Test RF config setter (Partial input).""" 441 | lora.rf_config = { 442 | 'freq': 868.700, 443 | 'sf': 7, 444 | 'bw': 0 445 | } 446 | mock_rf_config.assert_called_once() 447 | mock_send.assert_called_once_with('rf_config=868700000,7,0,1,8,20') 448 | 449 | 450 | @patch.object(Rak811, '_get_rf_config', return_value={ 451 | 'freq': 868.100, 452 | 'sf': 12, 453 | 'bw': 0, 454 | 'cr': 1, 455 | 'prlen': 8, 456 | 'pwr': 20 457 | }) 458 | @patch.object(Rak811, '_send_command') 459 | def test_set_rf_config_complete(mock_send, mock_rf_config, lora): 460 | """Test RF config setter.""" 461 | lora.rf_config = { 462 | 'freq': 868.700, 463 | 'sf': 7, 464 | 'bw': 1, 465 | 'cr': 3, 466 | 'prlen': 4, 467 | 'pwr': 5 468 | } 469 | mock_rf_config.assert_called_once() 470 | mock_send.assert_called_once_with('rf_config=868700000,7,1,3,4,5') 471 | 472 | 473 | @patch.object(Rak811, '_get_events', return_value=['9,0,0']) 474 | @patch.object(Rak811, '_send_command') 475 | def test_txc(mock_send, mock_events, lora): 476 | """Test LoraP2P send.""" 477 | lora.txc('Hello') 478 | mock_send.assert_called_once_with('txc=1,60000,48656c6c6f') 479 | mock_events.assert_called_once() 480 | 481 | 482 | @patch.object(Rak811, '_get_events', return_value=['5,0,0']) 483 | @patch.object(Rak811, '_send_command') 484 | def test_txc_error(mock_send, mock_events, lora): 485 | """Test LoraP2P send with error.""" 486 | with raises(Rak811EventError, 487 | match='5'): 488 | lora.txc('Hello') 489 | mock_send.assert_called_once_with('txc=1,60000,48656c6c6f') 490 | mock_events.assert_called_once() 491 | 492 | 493 | @patch.object(Rak811, '_send_command') 494 | def test_rxc(mock_send, lora): 495 | """Test LoraP2P RXC.""" 496 | lora.rxc() 497 | mock_send.assert_called_once_with('rxc=1') 498 | 499 | 500 | @patch.object(Rak811, '_send_command') 501 | def test_tx_stop(mock_send, lora): 502 | """Test LoraP2P tx stop.""" 503 | lora.tx_stop() 504 | mock_send.assert_called_once_with('tx_stop') 505 | 506 | 507 | @patch.object(Rak811, '_send_command') 508 | def test_rx_stop(mock_send, lora): 509 | """Test LoraP2P rx stop.""" 510 | lora.rx_stop() 511 | mock_send.assert_called_once_with('rx_stop') 512 | 513 | 514 | @patch.object(Rak811, '_get_events', return_value=[ 515 | '0,0,4,65666768', 516 | ]) 517 | def test_rx_get(mock_events, lora): 518 | """Test rx_get.""" 519 | lora.rx_get(10) 520 | mock_events.assert_called_once() 521 | assert lora.nb_downlinks == 1 522 | assert lora.get_downlink() == { 523 | 'port': 0, 524 | 'rssi': 0, 525 | 'snr': 0, 526 | 'len': 4, 527 | 'data': bytes.fromhex('65666768'), 528 | } 529 | 530 | 531 | @patch.object(Rak811, '_get_events') 532 | def test_rx_get_no_data(mock_events, lora): 533 | """Test rx_get with no data.""" 534 | mock_events.side_effect = Rak811TimeoutError() 535 | lora.rx_get(10) 536 | mock_events.assert_called_once() 537 | assert lora.nb_downlinks == 0 538 | assert lora.get_downlink() is None 539 | 540 | 541 | @patch.object(Rak811, '_send_command', return_value=('8,0,1,0,0,-48,28')) 542 | def test_radio_status(mock_send, lora): 543 | """Test radio_status command.""" 544 | assert lora.radio_status == (8, 0, 1, 0, 0, -48, 28) 545 | mock_send.assert_called_once_with('status') 546 | 547 | 548 | @patch.object(Rak811, '_send_command') 549 | def test_clear_radio_status(mock_send, lora): 550 | """Test clear_radio_status command.""" 551 | lora.clear_radio_status() 552 | mock_send.assert_called_once_with('status=0') 553 | -------------------------------------------------------------------------------- /tests/test_rak811_v3.py: -------------------------------------------------------------------------------- 1 | """Units tests for the Rak811 class (V3 firmware). 2 | 3 | Rak811Serial is tested separately and therefore mocked in this suite. 4 | 5 | RPi.GPIO is completely ignored as not available on all platforms, and its use 6 | by the Rak811 class very limited. 7 | """ 8 | from mock import Mock, patch 9 | from pytest import fixture, raises 10 | # Ignore RPi.GPIO 11 | p = patch.dict('sys.modules', {'RPi': Mock()}) 12 | p.start() 13 | from rak811.rak811_v3 import EVENT_TIMEOUT, RESPONSE_TIMEOUT # noqa: E402 14 | from rak811.rak811_v3 import Rak811, Rak811ResponseError # noqa: E402 15 | from rak811.rak811_v3 import Rak811Serial # noqa: E402 16 | from rak811.serial import Rak811TimeoutError # noqa: E402 17 | 18 | 19 | @fixture 20 | def lora(): 21 | """Instantiate Rak811 class though fixture.""" 22 | with patch('rak811.rak811_v3.Rak811Serial', autospec=True): 23 | rak811 = Rak811() 24 | yield rak811 25 | rak811.close() 26 | 27 | 28 | @patch('rak811.rak811_v3.Rak811Serial', autospec=True) 29 | def test_instantiate_default(mock_serial): 30 | """Test that Rak811 can be instantiated. 31 | 32 | Check for basic initialisation and teardown of the RackSerial. 33 | """ 34 | lora = Rak811() 35 | 36 | assert isinstance(lora._serial, Rak811Serial) 37 | mock_serial.assert_called_once_with( 38 | keep_untagged=True, 39 | read_buffer_timeout=RESPONSE_TIMEOUT 40 | ) 41 | lora.close() 42 | mock_serial.return_value.close.assert_called_once() 43 | 44 | 45 | @patch('rak811.rak811_v3.Rak811Serial', autospec=True) 46 | def test_instantiate_params(mock_serial): 47 | """Test that Rak811 passes parameters passed to RackSerial.""" 48 | port = '/dev/ttyAMA0' 49 | timeout = 5 50 | lora = Rak811(port=port, timeout=timeout) 51 | mock_serial.assert_called_once_with( 52 | keep_untagged=True, 53 | read_buffer_timeout=RESPONSE_TIMEOUT, 54 | port=port, 55 | timeout=timeout 56 | ) 57 | lora.close() 58 | 59 | 60 | @patch('rak811.rak811_v3.GPIO') 61 | @patch('rak811.rak811_v3.sleep') 62 | def test_hard_reset(mock_sleep, mock_gpio, lora): 63 | """Test hard_reset(). 64 | 65 | Test is a bit pointless, at least ensures it runs... 66 | """ 67 | lora.hard_reset() 68 | mock_gpio.setup.assert_called_once() 69 | mock_gpio.cleanup.assert_not_called() 70 | 71 | 72 | def test_int(lora): 73 | """Test _int function.""" 74 | assert lora._int(1) == 1 75 | assert lora._int('1') == 1 76 | assert lora._int('Hello') == 'Hello' 77 | 78 | 79 | def test_send_string(lora): 80 | """Test _send_string (passthrough to RackSerial).""" 81 | lora._send_string('Hello') 82 | lora._serial.send_string.assert_called_with('Hello') 83 | 84 | 85 | def test_send_command(lora): 86 | """Test _send_command.""" 87 | # Successful command 88 | lora._serial.receive.return_value = 'OK V3.0.0.14.H' 89 | assert lora._send_command('version') == 'V3.0.0.14.H' 90 | lora._serial.send_command.assert_called_with('version') 91 | 92 | # Error 93 | lora._serial.receive.return_value = 'ERROR: 2' 94 | with raises(Rak811ResponseError, 95 | match='2'): 96 | lora._send_command('set_config=lora:work_mode:5') 97 | 98 | # Unexpected data response queue 99 | lora._serial.receive.side_effect = [ 100 | 'Unexpected', 101 | 'OK V3.0.0.14.H', 102 | ] 103 | assert lora._send_command('version') == 'V3.0.0.14.H' 104 | 105 | # Multi-line response with "Initialization OK " 106 | lora._serial.receive.side_effect = [ 107 | 'RAK811 Version:3.0.0.14.H', 108 | 'LoRa work mode:LoRaWAN, join_mode:OTAA, MulticastEnable: false, Class: A', 109 | 'Initialization OK ' 110 | ] 111 | assert lora._send_command('set_config=device:restart') == ' ' 112 | 113 | 114 | def test_send_command_list(lora): 115 | """Test _send_command_list.""" 116 | # OK message 117 | lora._serial.receive.return_value = [ 118 | 'OK Device AT commands:', 119 | ' at+version', 120 | ' at+send=lorap2p:XXX' 121 | ] 122 | assert lora._send_command_list('help') == [ 123 | 'Device AT commands:', 124 | ' at+version', 125 | ' at+send=lorap2p:XXX' 126 | ] 127 | 128 | # "Initialization OK " 129 | lora._serial.receive.return_value = [ 130 | 'RAK811 Version:3.0.0.14.H', 131 | 'LoRa work mode:LoRaWAN, join_mode:OTAA, MulticastEnable: false, Class: A', 132 | 'Initialization OK ' 133 | ] 134 | assert lora._send_command_list('set_config=device:restart') == [ 135 | 'RAK811 Version:3.0.0.14.H', 136 | 'LoRa work mode:LoRaWAN, join_mode:OTAA, MulticastEnable: false, Class: A', 137 | ' ' 138 | ] 139 | 140 | # Error 141 | lora._serial.receive.return_value = ['ERROR: 1'] 142 | with raises(Rak811ResponseError, 143 | match='1'): 144 | lora._send_command_list('set_config=device:status') 145 | 146 | 147 | def test_get_events(lora): 148 | """Test _get_events.""" 149 | # Successful command 150 | lora._serial.receive.return_value = [ 151 | 'at+recv=0,-68,7,0', 152 | 'at+recv=1,-65,6,2:4865', 153 | ] 154 | events = lora._get_events() 155 | assert events.pop() == '1,-65,6,2:4865' 156 | assert events.pop() == '0,-68,7,0' 157 | 158 | 159 | """AT command API. 160 | 161 | For the sake of simplicity we mock Rak811._send_command, 162 | Rak811._send_command_list and Rack811._get_events, as these have already been 163 | tested. 164 | """ 165 | 166 | 167 | @patch.object(Rak811, '_send_command_list', return_value=[' ']) 168 | def test_set_config_simple(mock_send, lora): 169 | """Test set_config command (simple case).""" 170 | assert lora.set_config('lora:region:EU868') == [' '] 171 | mock_send.assert_called_once_with('set_config=lora:region:EU868') 172 | 173 | 174 | @patch.object(Rak811, '_send_command_list', return_value=[' ', ' ']) 175 | def test_set_config_boot(mock_send, lora): 176 | """Test set_config command (Boot case).""" 177 | assert lora.set_config('device:boot') == [' ', ' '] 178 | mock_send.assert_called_once_with('set_config=device:boot') 179 | 180 | 181 | @patch.object(Rak811, '_send_command_list', return_value=['LoRa work mode:LoRaWAN', ' ']) 182 | def test_set_config_init(mock_send, lora): 183 | """Test set_config command (Initialization OK case).""" 184 | assert lora.set_config('lora:work_mode:0') == ['LoRa work mode:LoRaWAN', ' '] 185 | mock_send.assert_called_once_with('set_config=lora:work_mode:0') 186 | 187 | 188 | @patch.object(Rak811, '_send_command_list', return_value=['1']) 189 | def test_get_config_simple(mock_send, lora): 190 | """Test get_config command (simple case).""" 191 | assert lora.get_config('device:gpio:2') == ['1'] 192 | mock_send.assert_called_once_with('get_config=device:gpio:2') 193 | 194 | 195 | @patch.object(Rak811, '_send_command_list', return_value=['Work Mode: LoRaWAN', 'DownLinkCounter: 0']) 196 | def test_get_config_multi(mock_send, lora): 197 | """Test get_config command (Multiple lines).""" 198 | assert lora.get_config('lora:status') == ['Work Mode: LoRaWAN', 'DownLinkCounter: 0'] 199 | mock_send.assert_called_once_with('get_config=lora:status') 200 | 201 | 202 | @patch.object(Rak811, '_send_command', return_value='V3.0.0.14.H') 203 | def test_version(mock_send, lora): 204 | """Test version command.""" 205 | assert lora.version == 'V3.0.0.14.H' 206 | mock_send.assert_called_once_with('version') 207 | 208 | 209 | @patch.object(Rak811, '_send_command_list', return_value=['Device AT commands:', ' at+send=lorap2p:XXX']) 210 | def test_help(mock_send, lora): 211 | """Test help command.""" 212 | assert lora.help == ['Device AT commands:', ' at+send=lorap2p:XXX'] 213 | mock_send.assert_called_once_with('help') 214 | 215 | 216 | @patch.object(Rak811, '_send_command') 217 | def test_run(mock_send, lora): 218 | """Test run command.""" 219 | lora.run() 220 | mock_send.assert_called_once_with('run') 221 | 222 | 223 | @patch.object(Rak811, '_send_command') 224 | def test_send_uart(mock_send, lora): 225 | """Test UART send.""" 226 | lora.send_uart('Hello') 227 | mock_send.assert_called_once_with('send=uart:3:48656c6c6f') 228 | 229 | 230 | @patch.object(Rak811, '_send_command') 231 | def test_join(mock_send, lora): 232 | """Test join command.""" 233 | lora.join() 234 | mock_send.assert_called_once_with('join', timeout=EVENT_TIMEOUT) 235 | 236 | 237 | @patch.object(Rak811, '_get_events') 238 | @patch.object(Rak811, '_send_command') 239 | def test_send_unconfirmed(mock_send, mock_events, lora): 240 | """Test send, unconfirmed.""" 241 | mock_events.side_effect = Rak811TimeoutError() 242 | lora.send('Hello') 243 | mock_send.assert_called_once_with('send=lora:1:48656c6c6f', timeout=300) 244 | mock_events.assert_called_once() 245 | assert lora.nb_downlinks == 0 246 | 247 | 248 | @patch.object(Rak811, '_get_events', return_value=['0,-68,7,0']) 249 | @patch.object(Rak811, '_send_command') 250 | def test_send_confirmed(mock_send, mock_events, lora): 251 | """Test send, unconfirmed.""" 252 | lora.send('Hello') 253 | mock_send.assert_called_once_with('send=lora:1:48656c6c6f', timeout=300) 254 | mock_events.assert_called_once() 255 | assert lora.nb_downlinks == 1 256 | assert lora.get_downlink() == { 257 | 'port': 0, 258 | 'rssi': -68, 259 | 'snr': 7, 260 | 'len': 0, 261 | 'data': b'', 262 | } 263 | 264 | 265 | @patch.object(Rak811, '_get_events', return_value=['1,-67,8,3:313233']) 266 | @patch.object(Rak811, '_send_command') 267 | def test_send_downlink(mock_send, mock_events, lora): 268 | """Test send, unconfirmed.""" 269 | lora.send('Hello') 270 | mock_send.assert_called_once_with('send=lora:1:48656c6c6f', timeout=300) 271 | mock_events.assert_called_once() 272 | assert lora.nb_downlinks == 1 273 | assert lora.get_downlink() == { 274 | 'port': 1, 275 | 'rssi': -67, 276 | 'snr': 8, 277 | 'len': 3, 278 | 'data': bytes.fromhex('313233'), 279 | } 280 | 281 | 282 | @patch.object(Rak811, '_send_command') 283 | def test_send_p2p(mock_send, lora): 284 | """Test P2P send.""" 285 | lora.send_p2p('Hello') 286 | mock_send.assert_called_once_with('send=lorap2p:48656c6c6f') 287 | 288 | 289 | @patch.object(Rak811, '_get_events', return_value=['-67,8,3:313233']) 290 | def test_receive_p2p(mock_events, lora): 291 | """Test receive P2P.""" 292 | lora.receive_p2p(timeout=10) 293 | mock_events.assert_called_once_with(10) 294 | assert lora.nb_downlinks == 1 295 | assert lora.get_downlink() == { 296 | 'port': 0, 297 | 'rssi': -67, 298 | 'snr': 8, 299 | 'len': 3, 300 | 'data': bytes.fromhex('313233'), 301 | } 302 | -------------------------------------------------------------------------------- /tests/test_serial.py: -------------------------------------------------------------------------------- 1 | """Units tests for the Rak811Serial class.""" 2 | from time import sleep 3 | 4 | from mock import Mock, patch 5 | from pytest import raises 6 | from serial import EIGHTBITS 7 | # Ignore RPi.GPIO 8 | p = patch.dict('sys.modules', {'RPi': Mock()}) 9 | p.start() 10 | from rak811.serial import BAUDRATE, PORT, TIMEOUT # noqa: E402, I100 11 | from rak811.serial import Rak811Serial, Rak811TimeoutError # noqa: E402 12 | 13 | 14 | @patch('rak811.serial.Serial') 15 | def test_instantiate_default(mock_serial): 16 | """Test that Rak811Serial can be instantiated. 17 | 18 | Check for basic initialisation and teardown of the serial interface. 19 | """ 20 | mock_serial.return_value.readline.return_value = b'' 21 | rs = Rak811Serial() 22 | # Test default parameters are used 23 | mock_serial.assert_called_once_with(port=PORT, 24 | baudrate=BAUDRATE, 25 | timeout=TIMEOUT) 26 | # Class initialization 27 | mock_serial.return_value.reset_input_buffer.assert_called_once() 28 | assert rs._alive 29 | 30 | # Test tear down 31 | rs.close() 32 | mock_serial.return_value.close.assert_called_once() 33 | assert not rs._alive 34 | 35 | 36 | @patch('rak811.serial.Serial') 37 | def test_instantiate_custom(mock_serial): 38 | """Test that Rak811Serial can be instantiated - custom parameters.""" 39 | mock_serial.return_value.readline.return_value = b'' 40 | port = '/dev/ttyAMA0' 41 | timeout = 5 42 | bytesize = EIGHTBITS 43 | rs = Rak811Serial(port=port, timeout=timeout, bytesize=bytesize) 44 | 45 | mock_serial.assert_called_once_with( 46 | port=port, 47 | baudrate=BAUDRATE, 48 | timeout=timeout, 49 | bytesize=bytesize 50 | ) 51 | rs.close() 52 | 53 | 54 | @patch('rak811.serial.Serial') 55 | def test_send_string(mock_serial): 56 | """Test Rak811Serial.send_string.""" 57 | mock_serial.return_value.readline.return_value = b'' 58 | rs = Rak811Serial() 59 | 60 | rs.send_string('Hello world') 61 | mock_serial.return_value.write.assert_called_once_with(b'Hello world') 62 | 63 | rs.close() 64 | 65 | 66 | @patch('rak811.serial.Serial') 67 | def test_send_command(mock_serial): 68 | """Test Rak811Serial.send_command.""" 69 | mock_serial.return_value.readline.return_value = b'' 70 | rs = Rak811Serial() 71 | 72 | rs.send_command('RESET') 73 | mock_serial.return_value.write.assert_called_once_with(b'at+RESET\r\n') 74 | 75 | rs.close() 76 | 77 | 78 | def emulate_rak_input(mock, timeout, data_in): 79 | """Emulate Rak811 output. 80 | 81 | Parameters: 82 | mock: mocked Serial class 83 | timeout is the Serial.readline() timeout 84 | data_in is a list of tuples (delay, bytes): 85 | delay: delay before data is available 86 | bytes: output from the Rak811 module 87 | 88 | When used, this function needs to be called before instantiating the 89 | Rak811Serial object, as it starts the read thread immediately. 90 | 91 | """ 92 | def side_effect(): 93 | if len(data): 94 | (d1, b1) = data.pop(0) 95 | sleep(d1) 96 | # If we have data readily available after this one, set in_waiting 97 | # to the data size. 98 | # If we are at the end, or if there is a delay before next set 99 | # in_waiting to zero. 100 | if len(data): 101 | (d2, b2) = data[0] 102 | if d2: 103 | type(mock.return_value).in_waiting = 0 104 | else: 105 | type(mock.return_value).in_waiting = len(b2) 106 | else: 107 | type(mock.return_value).in_waiting = 0 108 | return b1 109 | else: 110 | # No more data 111 | sleep(timeout) 112 | return b'' 113 | 114 | # Take a local copy of the list and update mock 115 | data = list(data_in) 116 | mock.return_value.readline.side_effect = side_effect 117 | # Set in_waiting to the size of the first data line 118 | if len(data): 119 | type(mock.return_value).in_waiting = len(data[0][1]) 120 | else: 121 | type(mock.return_value).in_waiting = 0 122 | 123 | 124 | @patch('rak811.serial.Serial') 125 | def test_receive_single(mock_serial): 126 | """Test Rak811Serial.receive, single line.""" 127 | # For response, first line is passed, others, if any are buffered. 128 | emulate_rak_input(mock_serial, 1, [ 129 | (0, b'OK\r\n'), 130 | (0, b'OKok\r\n'), 131 | ]) 132 | rs = Rak811Serial() 133 | assert rs.receive() == 'OK' 134 | assert rs.receive() == 'OKok' 135 | rs.close() 136 | 137 | # Check for Errors 138 | emulate_rak_input(mock_serial, 1, [ 139 | (0, b'ERROR-1\r\n'), 140 | ]) 141 | rs = Rak811Serial() 142 | assert rs.receive() == 'ERROR-1' 143 | rs.close() 144 | 145 | # Noise is skipped 146 | emulate_rak_input(mock_serial, 1, [ 147 | (0, b'Welcome to RAK811\r\n'), 148 | (0.5, b'\r\n'), 149 | (0, b'\r\n'), 150 | (0, b'OK\r\n'), 151 | ]) 152 | rs = Rak811Serial() 153 | assert rs.receive() == 'OK' 154 | rs.close() 155 | 156 | # Noise is returned 157 | emulate_rak_input(mock_serial, 1, [ 158 | (0, b'Welcome to RAK811\r\n'), 159 | (0, b'OK\r\n'), 160 | ]) 161 | rs = Rak811Serial(keep_untagged=True) 162 | assert rs.receive() == 'Welcome to RAK811' 163 | assert rs.receive() == 'OK' 164 | rs.close() 165 | 166 | # Handle non-ASCII characters 167 | emulate_rak_input(mock_serial, 1, [ 168 | (0, b'Non ASCII: \xde\xad\xbe\xef\r\n'), 169 | (0.5, b'\r\n'), 170 | (0, b'\r\n'), 171 | (0, b'OK\r\n'), 172 | ]) 173 | rs = Rak811Serial() 174 | assert rs.receive() == 'OK' 175 | rs.close() 176 | 177 | # Response timeout 178 | emulate_rak_input(mock_serial, 1, [ 179 | ]) 180 | rs = Rak811Serial(read_buffer_timeout=1) 181 | with raises(Rak811TimeoutError, 182 | match='Timeout while waiting for data'): 183 | rs.receive() 184 | rs.close() 185 | 186 | 187 | @patch('rak811.serial.Serial') 188 | def test_receive_multi(mock_serial): 189 | """Test Rak811Serial.receive, multiple lines.""" 190 | # Single command 191 | emulate_rak_input(mock_serial, 1, [ 192 | (0, b'at+recv=8,0,0\r\n'), 193 | ]) 194 | rs = Rak811Serial() 195 | event = rs.receive(single=False) 196 | assert len(event) == 1 197 | assert event.pop() == 'at+recv=8,0,0' 198 | rs.close() 199 | 200 | # Multiple commands 201 | emulate_rak_input(mock_serial, 1, [ 202 | (0, b'at+recv=2,0,0\r\n'), 203 | (0, b'Welcome to RAK811\r\n'), 204 | (0, b'at+recv=0,0,0\r\n'), 205 | ]) 206 | rs = Rak811Serial() 207 | event = rs.receive(single=False) 208 | assert len(event) == 2 209 | assert event.pop() == 'at+recv=0,0,0' 210 | assert event.pop() == 'at+recv=2,0,0' 211 | rs.close() 212 | 213 | # Event timeout 214 | emulate_rak_input(mock_serial, 1, [ 215 | ]) 216 | rs = Rak811Serial(read_buffer_timeout=1) 217 | with raises(Rak811TimeoutError, 218 | match='Timeout while waiting for data'): 219 | rs.receive(single=False) 220 | rs.close() 221 | 222 | 223 | @patch('rak811.serial.Serial') 224 | def test_get_response_event(mock_serial): 225 | """Test response / event sequence.""" 226 | # All at once 227 | emulate_rak_input(mock_serial, 1, [ 228 | (0, b'Welcome 1\r\n'), 229 | (0, b'OK\r\n'), 230 | (0, b'Welcome 2\r\n'), 231 | (0, b'at+recv=2,0,0\r\n'), 232 | (0, b'Welcome 3\r\n'), 233 | (0, b'at+recv=0,0,0\r\n'), 234 | ]) 235 | rs = Rak811Serial() 236 | assert rs.receive(single=True) == 'OK' 237 | event = rs.receive(single=False) 238 | assert len(event) == 2 239 | assert event.pop() == 'at+recv=0,0,0' 240 | assert event.pop() == 'at+recv=2,0,0' 241 | rs.close() 242 | 243 | # Same scenario with delay between response and event 244 | emulate_rak_input(mock_serial, 1, [ 245 | (0, b'Welcome 1\r\n'), 246 | (0, b'OK\r\n'), 247 | (0, b'Welcome 2\r\n'), 248 | (1, b'at+recv=2,0,0\r\n'), 249 | (0, b'Welcome 3\r\n'), 250 | (0, b'at+recv=0,0,0\r\n'), 251 | ]) 252 | rs = Rak811Serial() 253 | assert rs.receive(single=True) == 'OK' 254 | event = rs.receive(single=False) 255 | assert len(event) == 2 256 | assert event.pop() == 'at+recv=0,0,0' 257 | assert event.pop() == 'at+recv=2,0,0' 258 | rs.close() 259 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = flake8, py37, py38 8 | skip_missing_interpreters = true 9 | 10 | [gh-actions] 11 | python = 12 | 3.7: py37 13 | 3.8: flake8, py38 14 | 15 | [testenv] 16 | deps = 17 | coverage 18 | mock 19 | pytest 20 | commands = 21 | coverage erase 22 | coverage run --source rak811 -m py.test -v 23 | coverage html 24 | coverage xml 25 | coverage report --fail-under 95 26 | 27 | [testenv:flake8] 28 | deps = 29 | flake8 30 | flake8-colors 31 | flake8-comprehensions 32 | flake8-docstrings 33 | flake8-import-order 34 | pep8-naming 35 | pydocstyle==6.0.0 36 | commands = 37 | flake8 setup.py rak811 tests examples 38 | 39 | [flake8] 40 | ignore = D100, D102, D103, D301, W503 41 | max-line-length = 119 42 | import-order-style = google 43 | format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s 44 | --------------------------------------------------------------------------------