├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── automation ├── README.md └── batchregistration │ ├── README.md │ ├── batch_register_lorawan_devices.py │ ├── example_device_list.csv │ └── requirements.txt ├── buildspec.yml ├── cayenneLPPDecoder ├── README.md ├── cayennelpp.js └── decoder.js ├── edgetrackerlora └── README.md ├── gateway_watchdog ├── .gitignore ├── README.md ├── app.py ├── cdk.json ├── cdkstack │ ├── __init__.py │ ├── lorawan_connectivity_watchdog_stack.py │ └── lorawan_gateway_monitoring_detectormodel.py ├── images │ ├── connectivity_watchdog_architecture.png │ ├── ioteventsdetectormodel.png │ ├── mqtttestclient.png │ └── step_functions_state_machine.png ├── localtools │ └── prepare.sh ├── requirements.txt ├── setup.py ├── source.bat ├── src_get_wireless_gateway_statistics_lambda │ ├── lambda.py │ ├── requirements.txt │ └── test.py ├── src_put_cloudwatch_metrics │ ├── lambda.py │ ├── requirements.txt │ └── test.py └── tests │ ├── input_connected.json │ ├── input_disconnected.json │ └── local_invoke_lambda.sh ├── iotthingshadow ├── README.md ├── images │ └── AWS_IoT_-_Things_-_0b27a5cc-8a03-4841-8aae-dd19075310a0_-_LoRaWANTelemetry.png ├── src-iotrule-transformation │ ├── app.py │ ├── requirements.txt │ └── samplevent.json ├── src-mapthingname │ ├── app.py │ ├── requirements.txt │ └── samplevent.json ├── src-payload-decoders │ └── python │ │ ├── requirements.txt │ │ └── sample_device.py └── template.yaml ├── observability └── README.md ├── send_downlink_payload ├── README.md ├── images │ └── original │ │ ├── AWS_IoT_-_Test_0.png │ │ ├── AWS_IoT_-_Test_1.png │ │ └── arch.png ├── src │ ├── app.py │ └── requirements.txt └── template.yaml ├── soilmoisture_alarming ├── README.md ├── images │ └── resized │ │ ├── 0010.png │ │ ├── arch.png │ │ ├── input.png │ │ └── model.png └── template.yaml ├── timestream ├── README.md ├── images │ ├── grafana │ │ ├── grafana_query-1024x542.png │ │ ├── grafana_query.png │ │ ├── metadata-1024x400.png │ │ ├── metadata-1024x542.png │ │ ├── metadata.png │ │ ├── telemetry-1024x400.png │ │ ├── telemetry-1024x542.png │ │ └── telemetry.png │ ├── guthub_timestream_quickdemo_1436_708.gif │ └── guthub_timestream_quickdemo_orig.gif ├── src-lambda-transform │ ├── app.py │ ├── requirements.txt │ └── samplevent.json ├── src-lambda-write-to-timestream │ ├── app.py │ ├── requirements.txt │ └── sampleevent.json ├── src-layer-payload-decoders │ └── python │ │ └── sample_device.py └── template.yaml ├── timestream_for_transform_binary_payload ├── README.md ├── images │ ├── grafana │ │ ├── grafana_query-1024x542.png │ │ ├── grafana_query.png │ │ ├── metadata-1024x400.png │ │ ├── metadata-1024x542.png │ │ ├── metadata.png │ │ ├── telemetry-1024x400.png │ │ ├── telemetry-1024x542.png │ │ └── telemetry.png │ ├── guthub_timestream_quickdemo_1436_708.gif │ └── guthub_timestream_quickdemo_orig.gif ├── src-lambda-write-to-timestream │ ├── app.py │ ├── requirements.txt │ └── sampleevent.json └── template.yaml ├── transform_binary_payload ├── .gitignore ├── README.md ├── images │ ├── 0000-1024x542.png │ └── 0000-resized.png ├── src-iotrule-transformation-nodejs │ ├── index.js │ ├── index.test.js │ ├── package.json │ └── samplevent.json ├── src-iotrule-transformation │ ├── app.py │ └── requirements.txt ├── src-payload-decoders │ ├── node │ │ ├── dragino_ldds20.js │ │ ├── dragino_lht65.js │ │ ├── dragino_lsn50v2d23.js │ │ ├── dragino_lsph01.js │ │ ├── dragino_lwl02.js │ │ ├── elsys.js │ │ ├── sample_device.js │ │ └── tests │ │ │ ├── dragino_lht65.test.js │ │ │ └── sample_device.test.js │ └── python │ │ ├── adeunis_dc_v2.py │ │ ├── adeunis_ftd2.py │ │ ├── axioma_w1.py │ │ ├── dragino_laq4.py │ │ ├── dragino_lbt1.py │ │ ├── dragino_lds01.py │ │ ├── dragino_lgt92.py │ │ ├── dragino_lht65.py │ │ ├── dragino_llms01.py │ │ ├── dragino_lse01.py │ │ ├── dragino_lsn50.py │ │ ├── dragino_lsn50v2.py │ │ ├── elsys.py │ │ ├── globalsat_lt100.py │ │ ├── helpers.py │ │ ├── meteo_helix.py │ │ ├── nas_um3080.py │ │ ├── requirements.txt │ │ ├── sample_device.py │ │ ├── sentrius_rs1xx.py │ │ ├── st_nucleo_wl55jc.py │ │ ├── tabs_objectlocator.py │ │ └── tabs_temphumsensor.py └── template.yaml ├── transform_binary_payload_pilot_things ├── .gitignore ├── README.md ├── images │ └── 0000-resized.png ├── src-iotrule-transformation-nodejs │ ├── index.js │ ├── index.test.js │ ├── package-lock.json │ └── package.json ├── src-iotrule-transformation │ ├── app.py │ └── requirements.txt └── template.yaml ├── workinprogress_dontuse └── device_watchdog │ ├── README.md │ ├── app.py │ ├── cdk.json │ ├── cdkstack │ ├── __init__.py │ ├── lorawan_connectivity_watchdog_stack.py │ └── lorawan_device_heartbeat_detectormodel.py │ ├── images │ ├── ioteventarch.png │ ├── ioteventsdetectormodel.png │ ├── iotrule.png │ ├── mqttclient1.png │ └── mqttclient2.png │ ├── localtools │ └── prepare.sh │ ├── out.yaml │ ├── requirements.txt │ ├── setup.py │ └── tests │ ├── event.json │ └── publish.sh └── workshop ├── binarydecoder ├── .gitignore ├── README.md ├── events │ └── event.json ├── src │ ├── app.py │ ├── dragino_lht65.py │ └── requirements.txt └── template.yaml └── sampledecoder ├── README.md ├── downlink ├── rfi_downlink_off.sh ├── rfi_downlink_on.sh ├── rfi_downlink_request_interval.sh ├── rfi_downlink_set_interval_30s.sh └── rfi_downlink_set_interval_60s.sh ├── events └── downlink_lht65_60s.json ├── src ├── app.py ├── dragino_lht65.py ├── requirements.txt └── rfi_power_switch.py └── template.yaml /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '35 17 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | tools/* 3 | .DS_Store 4 | **/.aws-sam/* 5 | **/samconfig.toml 6 | **/**.pem 7 | .coverage 8 | test-reports/** 9 | transform_binary_payload/scripts/deploy_lorawan_decoder.sh 10 | .idea 11 | node_modules 12 | .vscode/** 13 | .venv/** 14 | cdk.out/** 15 | localtools/** 16 | gateway_watchdog/src_put_cloudwatch_metrics/** 17 | gateway_watchdog/localtools/** 18 | device_watchdog/localtools/** 19 | *.swp 20 | package-lock.json 21 | __pycache__ 22 | .pytest_cache 23 | .env 24 | .venv 25 | *.egg-info 26 | 27 | # CDK asset staging directory 28 | .cdk.staging 29 | cdk.out 30 | workshop/webinar/** 31 | device_watchdog/out.yaml 32 | workshop/sampledecoder/downlink/Archive.zip 33 | workshop/sampledecoder/.python-version 34 | automation/batchregistration/testerrors.csv 35 | automation/batchregistration/failed_devices_list.csv 36 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /automation/README.md: -------------------------------------------------------------------------------- 1 | # How to automate AWS IoT Core for LoRaWAN tasks 2 | 3 | ## Prerequisites 4 | The guidelines below require the following software: 5 | - AWS CLI 6 | - jq 7 | 8 | **MacOS installation instructions** 9 | 10 | ```shell 11 | # Install homebrew (skip this step if already installed) 12 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 13 | # Install jq 14 | brew install jq 15 | # Install AWS CLI (skip this step if AWS CLI is already instsall) 16 | brew instsall awscli@2 17 | ``` 18 | 19 | 20 | ## How to create a gateway 21 | The example below includes steps for creation of new gateway, creation and association of the related IoT Certificates, retrieval of CUPS/LNS server certificates and identification of CUPS/LNS endpoints. The example below use region us-east-1. If you use other region (e.g. eu-west-1), please adjust the command accordingly. 22 | 23 | **1. Create a gateway** 24 | 25 | In the example below, please replace the examples values: 26 | 27 | - GATEWAY_EUI with the Eui of your gateway you will find in gateway documentation or printed on the gateway 28 | - RF_REGION with either EU868 or US915 depending on the local regulations 29 | 30 | ```shell 31 | GATEWAY_EUI=1122334455667788 32 | RF_REGION=EU868 33 | GATEWAY_ID=$(aws iotwireless create-wireless-gateway --name MyGateway \ 34 | --description "My Gateway description" \ 35 | --lorawan GatewayEui=$GATEWAY_EUI,RfRegion=$RF_REGION \ 36 | --region us-east-1 | jq -r .Id 37 | ) 38 | echo "Created gateway with id $GATEWAY_ID" 39 | ``` 40 | 41 | The output of this command will be the gateway id that you will need in step 3. 42 | 43 | **2. Create AWS IoT Certificate and keypair** 44 | 45 | ```shell 46 | CERTIFICATE_ID=$(aws iot create-keys-and-certificate \ 47 | --set-as-active \ 48 | --certificate-pem-outfile gateway.certificate.pem \ 49 | --public-key-outfile gateway.public_key.pem \ 50 | --private-key-outfile gateway.private_key.pem \ 51 | --region us-east-1 | jq -r .certificateId) 52 | echo "Created certificate with id $CERTIFICATE_ID" 53 | ``` 54 | 55 | The output of this command will be the certificateId that you will need in step 3 56 | 57 | **3. Associate gateway with the certificate** 58 | 59 | ```shell 60 | aws iotwireless associate-wireless-gateway-with-certificate --id $GATEWAY_ID \ 61 | --iot-certificate-id $CERTIFICATE_ID \ 62 | --region us-east-1 63 | ``` 64 | 65 | The expected output should be: 66 | 67 | ```shell 68 | { 69 | "IotCertificateId": "" 70 | } 71 | ``` 72 | 73 | **3. Retrieve server certificates for CUPS or LNS endpoints** 74 | 75 | The server certificates are used by Basics Station software that runs on a LoRaWAN gateway. Basics Station uses the server certificatews to verify the identify of the AWS IoT Core for LoRaWAN endpoints. Please note that if your LoRaWAN gateway supports the CUPS protocol, it should be sufficient to only configure the CUPS endpoint and your gateway will retrieve the LNS endpoint via the CUPS protocol. If your LoRaWAN does not support the CUPS protocol, you should retrieve and configure the LNS endpoint certificate. 76 | 77 | To retrieve the CUPS endpoint certificate please run the following command: 78 | ```shell 79 | aws iotwireless get-service-endpoint --service-type CUPS --region us-east-1 | jq -r .ServerTrust > cups_server_trust.pem 80 | ``` 81 | 82 | To retrieve the LNS endpoint certificate please run the following command. 83 | ```shell 84 | aws iotwireless get-service-endpoint --service-type LNS --region us-east-1 | jq -r .ServerTrust > lns_server_trust.pem 85 | ``` 86 | 87 | **4. Retrieve URIs of CUPS or LNS endpoint** 88 | 89 | Please note that if your LoRaWAN gateway supports the CUPS protocol, it should be sufficient to only configure the CUPS endpoint and your gateway will retrieve the LNS endpoint using the CUPS protocol. If your LoRaWAN does not support the CUPS protocol, you should configure the LNS endpoint. 90 | 91 | To retrieve the CUPS endpoint certificate please run the following command: 92 | ```shell 93 | aws iotwireless get-service-endpoint --service-type CUPS --region us-east-1 | jq -r .ServiceEndpoint 94 | ``` 95 | 96 | To retrieve the LNS endpoint certificate please run the following command. 97 | ```shell 98 | aws iotwireless get-service-endpoint --service-type LNS --region us-east-1 | jq -r .ServiceEndpoint 99 | ``` 100 | 101 | 102 | **4. Perform gateway configuration** 103 | 104 | After a successful completion of the steps above, please use the following information to configure your LoRaWAN gateway according to the gateway's user manual: 105 | - **Gateway certificate:** gateway.certificate.pem 106 | - **Gateway private key:** gateway.private_key.pem 107 | - **Serer trust certificates:** cups_server_trust.pem or lns_server_trust.pem 108 | - **Endpoints for CUPS or LNS** -------------------------------------------------------------------------------- /automation/batchregistration/README.md: -------------------------------------------------------------------------------- 1 | # Tool for batch device registration 2 | 3 | Please note that this tool is intended for use in lab and experimental environment. It is in users responsibility to review and adjust it before using in production environment. 4 | 5 | ## Tool invocation parameters 6 | 7 | ```shell 8 | usage: batch_register_lorawan_devices.py [-h] [--verbose] [--dryrun] --region REGION inputfilename 9 | 10 | Batch registration of LoRaWAN devices for AWS IoT Core for LoRaWAN 11 | 12 | positional arguments: 13 | inputfilename Path to CSV file to process 14 | 15 | optional arguments: 16 | -h, --help show this help message and exit 17 | --verbose, -v Provide more output 18 | --dryrun, -d Do everything but API calls 19 | --region REGION, -r REGION AWS region 20 | ``` 21 | 22 | ## Example of usage 23 | 24 | ### Step 1: identify device profile id 25 | 26 | ```shell 27 | aws iotwireless list-device-profiles 28 | ``` 29 | 30 | ### Step 2: identify service profile id 31 | 32 | ```shell 33 | aws iotwireless list-service-profiles 34 | ``` 35 | 36 | ### Step 3: Build CSV file 37 | 38 | You can use `example_device_list.csv` as an example file. 39 | 40 | Required columns: 41 | 42 | - Type (currently only "LoRaWAN" is supported) 43 | - DevEui 44 | - AppKey 45 | - AppEui 46 | - DeviceProfileId (see Step 1) 47 | - ServiceProfileId (see Step 2) 48 | - DestinationName (must exist before this script runs) 49 | - AuthenticationMethod (allowed values: OtaaV1_0_x) 50 | - Name (see [API doc](https://docs.aws.amazon.com/iot-wireless/2020-11-22/apireference/API_CreateWirelessDevice.html#iotwireless-CreateWirelessDevice-request-Name)) 51 | - Description (see [API doc](https://docs.aws.amazon.com/iot-wireless/2020-11-22/apireference/API_CreateWirelessDevice.html#iotwireless-CreateWirelessDevice-request-Description)) 52 | 53 | 54 | Few rules for the file: 55 | 56 | - Columns must be separated by `;` 57 | - Header line with above column names is required 58 | - Please note that the specific ordering of columns is NOT required. 59 | - CSV file can also contain additional columns 60 | 61 | ### Step 4: Run batch registration script 62 | 63 | ```shell 64 | pip3 install -r requirements.txt 65 | python3 batch_register_lorawan_devices.py example_device_list.csv --region eu-west-1 66 | ``` 67 | -------------------------------------------------------------------------------- /automation/batchregistration/batch_register_lorawan_devices.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | import pandas as pd 18 | import boto3 19 | import json 20 | import logging 21 | import argparse 22 | 23 | # Command line arguments 24 | parser = argparse.ArgumentParser(description='Batch registration of LoRaWAN devices for AWS IoT Core for LoRaWAN') 25 | parser.add_argument('inputfilename', type=str, help='Path to CSV file to process') 26 | # parser.add_argument('--errorfilename', type=str, help='Path to CSV file to store failed registrations', required=True) 27 | parser.add_argument('--verbose', '-v', action='count', default=1, help='Provide more output') 28 | parser.add_argument('--dryrun', '-d', action='count', default=1, help='Do everything but API calls') 29 | parser.add_argument('--region', "-r", type=str, help='AWS region', required=True) 30 | args = parser.parse_args() 31 | args.verbose = 70 - (10*args.verbose) if args.verbose > 0 else 0 32 | 33 | # Logging 34 | logger = logging.getLogger() 35 | logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s: %(message)s', 36 | datefmt='%Y-%m-%d %H:%M:%S') 37 | 38 | # AWS IoT Wireless client 39 | if args.dryrun == 1: 40 | iotwireless_client = boto3.client('iotwireless', region_name=args.region) 41 | else: 42 | logger.info("Dry run, not creating wireless devices") 43 | 44 | 45 | 46 | 47 | def register_wireless_device(devicerow) -> bool: 48 | 49 | if (devicerow.Type != "LoRaWAN"): 50 | logger.error("Allowed device types are: LoRaWAN") 51 | return False; 52 | 53 | if (devicerow.AuthenticationMethod == "OtaaV1_0_x"): 54 | authentication_data = { 55 | "AppKey": devicerow.AppKey, 56 | "AppEui": devicerow.AppEui 57 | } 58 | else: 59 | logger.error(f"Authentication method {devicerow.AuthenticationMethod} not supported, skipping row for device id {devicerow.DeviceId}") 60 | return False 61 | 62 | 63 | create_wireless_device_input = { 64 | "Type": devicerow.Type, 65 | "Name": devicerow.Name, 66 | "Description": devicerow.Description, 67 | "DestinationName": devicerow.DestinationName, 68 | "LoRaWAN": { 69 | 'DevEui': devicerow.DevEui, 70 | 'DeviceProfileId': devicerow.DeviceProfileId, 71 | 'ServiceProfileId': devicerow.ServiceProfileId, 72 | devicerow.AuthenticationMethod : authentication_data 73 | 74 | } 75 | } 76 | logger.info(f"Creating device with DevEui {devicerow.DevEui}") 77 | logger.debug(f"Creating wireless device with data {json.dumps(create_wireless_device_input, indent=4)}") 78 | try: 79 | if args.dryrun == 1: 80 | iotwireless_client.create_wireless_device(**create_wireless_device_input) 81 | except Exception as e: 82 | logger.error(f"Error creating wireless device {e}") 83 | return False 84 | 85 | return True 86 | 87 | logger.info(f"Loading input file {args.inputfilename}") 88 | df = pd.read_csv(args.inputfilename, delimiter=';', quotechar='|') 89 | df_failed = pd.DataFrame().reindex_like(df) 90 | 91 | success_count = 0 92 | failure_count = 0 93 | 94 | for device_row in df.itertuples(index=False, name="DeviceRow"): 95 | logger.info(f"Adding device with data {device_row}") 96 | if register_wireless_device(device_row) : 97 | success_count += 1 98 | else: 99 | failure_count += 1 100 | # df_failed.append(pd.Series(device_row), ignore_index=True) 101 | 102 | 103 | logger.info("Successfully added {} devices, failed to add {} devices".format(success_count, failure_count)) 104 | 105 | # if failure_count > 0: 106 | # logger.info("Writing list of failed devices to {}".format(args.errorfilename)) 107 | # df_failed.to_csv(args.errorfilename, sep=';', index=True) -------------------------------------------------------------------------------- /automation/batchregistration/example_device_list.csv: -------------------------------------------------------------------------------- 1 | Type;DevEui;AppKey;AppEui;DeviceProfileId;ServiceProfileId;DestinationName;AuthenticationMethod;Name;Description 2 | LoRaWAN;BBBBBC050055C627;BBBB46445019B6E2C70404BBBD9DBB29;BBBBBC0000000000;7399a549-ca7a-44be-9c59-8c41c9955715;ac46cc35-8075-41a3-9b7b-6b5d00afddc6;DestinationA;OtaaV1_0_x;device1;My device 1 3 | LoRaWAN;CBBBBC050055C627;CBBB46445019B6E2C70404BBBD9DBB29;CBBBBC0000000000;7399a549-ca7a-44be-9c59-8c41c9955715;ac46cc35-8075-41a3-9b7b-6b5d00afddc6;DestinationB;OtaaV1_0_x;device2;My device 2 4 | LoRaWAN;DBBBBC050055C627;DBBB46445019B6E2C70404BBBD9DBB29;DBBBBC0000000000;7399a549-ca7a-44be-9c59-8c41c9955715;ac46cc35-8075-41a3-9b7b-6b5d00afddc6;DestinationC;OtaaV1_0_x;device3;My device 3 -------------------------------------------------------------------------------- /automation/batchregistration/requirements.txt: -------------------------------------------------------------------------------- 1 | pandas 2 | boto3 -------------------------------------------------------------------------------- /buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | phases: 4 | install: 5 | runtime-versions: 6 | python: 3.7 7 | pre_build: 8 | commands: 9 | - apt-get install -y python3 10 | - python3 -m pip install virtualenv 11 | - python3 -m venv test_venv 12 | - . test_venv/bin/activate 13 | - pip install --upgrade pip 14 | - pip install -r transform_binary_payload/src-payload-decoders/python/requirements.txt 15 | - rm -rf dspt 16 | - rm -rf test-reports 17 | - mkdir test-reports 18 | build: 19 | commands: 20 | - pytest transform_binary_payload/src-payload-decoders/python/dragino_lbt1.py transform_binary_payload/src-payload-decoders/python/dragino_lht65.py 21 | --html=test-reports/report.html 22 | --self-contained-html 23 | -s 24 | -v 25 | --cov=transform_binary_payload/src-payload-decoders/python 26 | --cov-report=xml:test-reports/coverage/coverage.xml 27 | --junitxml=test-reports/junit.xml 28 | --log-file=test-reports/logs.txt 29 | post_build: 30 | commands: 31 | - echo Build completed on `date` 32 | 33 | reports: 34 | coverage: 35 | report-group-name-or-arn: coverage 36 | files: 37 | - "test-reports/coverage/coverage.xml" 38 | file-format: "COBERTURAXML" 39 | discard-paths: yes 40 | unittest: 41 | report-group-name-or-arn: unittesrt 42 | files: 43 | - "junit.xml" 44 | - "report.html" 45 | - "assets/*" 46 | base-directory: "test-reports" 47 | discard-paths: yes 48 | file-format: JunitXml 49 | -------------------------------------------------------------------------------- /cayenneLPPDecoder/README.md: -------------------------------------------------------------------------------- 1 | # Lambda - Low Power Payload Decoder for AWS IoT Core of LoRaWAN 2 | 3 | LoRaWAN devices typically support small message payload sizes. Sensor data is usually binary packed and Base64 encoded. A commonly used format is the [Cayenne Low Power Payload](https://github.com/myDevicesIoT/CayenneLPP) 4 | 5 | This project implements a Lambda that can be triggered using an IoT rule to decode the Cayenne Low Power Payload (LPP). 6 | 7 | The Lambda handler was tested using packets sent by [Sodaq Explorer](https://support.sodaq.com/Boards/ExpLoRer/) boards with [Grove](https://wiki.seeedstudio.com/Grove_System/) sensors. The data was encoded using this [Arduino library](https://github.com/ElectronicCats/CayenneLPP). 8 | 9 | ## Message Format 10 | 11 | The section below outlines the LoRaWAN packet, the encoded payload and the decoded output as an example. 12 | 13 | ### Incoming event to Lambda 14 | 15 | The message below shows the incoming event to this Cayenne LPP decoder Lambda function. 16 | 17 | ```bash 18 | 19 | { 20 | WirelessDeviceId: '342da222-0be1-45ee-b90d-999144a63e6c', 21 | PayloadData: 22 | 'AQAuAmUD/wNmAAZnAJsEaE4FcyZ6B3ECLv2R/cAIhgB3/2YAHAmIBSuf8x/1AEUuCYgFK5/zH/UARS4JiAUrn/Mf9QBFLg==', 23 | WirelessMetadata: 24 | { LoRaWAN: 25 | { DataRate: '3', 26 | DevEui: '0004a30b001b2188', 27 | FPort: 3, 28 | Frequency: '903900000', 29 | Gateways: [Array], 30 | Timestamp: '2021-02-23T15:29:12Z' 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | ### Cayenne LPP Encoded Payload 37 | 38 | The Cayenne LPP encoded data is the following field in the message. Please note that this sample payload is using the Digital Input field of the Cayenne LPP format to send a packet counter. The packet counter starts at 0, counts up to 255, and then resets back to 0. 39 | 40 | ```bash 41 | 42 | AQAuAmUD/wNmAAZnAJsEaE4FcyZ6B3ECLv2R/cAIhgB3/2YAHAmIBSuf8x/1AEUuCYgFK5/zH/UARS4JiAUrn/Mf9QBFLg== 43 | 44 | ``` 45 | 46 | ### Decoded Packet Output 47 | 48 | ```bash 49 | { 50 | timestamp: 1614094153085, 51 | DevEUI: '0004a30b001b2188', 52 | DeviceId: '0004a30b001b2188', 53 | datetime: '2021-02-23T15:29:12Z', 54 | count: 46, 55 | lux: 1023, 56 | presence: 0, 57 | humidity: 39, 58 | pressure: 985, 59 | temperature: 15.5, 60 | accel: { x: 0.558, y: -0.623, z: -0.576 }, 61 | gyro: { x: 1.19, y: -1.54, z: 0.28 }, 62 | loc: { lat: 33.8847, lng: -84.3787, alt: 177.1 } 63 | } 64 | 65 | ``` 66 | 67 | The fields count, lux, presence, humidity, pressure, temperature, accel, gyro, and loc are the ones decoded from the LoRaWAN payload. 68 | 69 | The code will have to be adapted for any changes that your packet format may have or key names being used in the message format. 70 | -------------------------------------------------------------------------------- /cayenneLPPDecoder/decoder.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | // software and associated documentation files (the "Software"), to deal in the Software 5 | // without restriction, including without limitation the rights to use, copy, modify, 6 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | // permit persons to whom the Software is furnished to do so. 8 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 9 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 10 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 11 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 12 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 13 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | 15 | const AWS = require('aws-sdk'); 16 | var decode = require('./cayennelpp'); 17 | exports.main = async function(event, context) { 18 | console.log(event); 19 | var gateways = event.WirelessMetadata.LoRaWAN.Gateways; 20 | gateways.forEach(function(gateway) { 21 | console.log(gateway); 22 | }); 23 | 24 | console.log(event.PayloadData); 25 | var PayloadData = event.PayloadData; 26 | var data = Buffer.from(PayloadData, 'base64'); 27 | var values = Object.values(decode(data)); 28 | var data = {}; 29 | 30 | // Add an incoming timestamp in the Lambda 31 | data.timestamp = new Date().getTime(); 32 | data.DevEUI = event.WirelessMetadata.LoRaWAN.DevEui; 33 | data.DeviceId = event.WirelessMetadata.LoRaWAN.DevEui; 34 | data.datetime = event.WirelessMetadata.LoRaWAN.Timestamp; 35 | 36 | values.forEach(function (sensorEntry){ 37 | // console.log(sensorEntry.type); 38 | // console.log(sensorEntry.value); 39 | switch(sensorEntry.type) { 40 | case 0: // CNT field being sent in DigitalInput 41 | data.count = sensorEntry.value; 42 | break; 43 | case 101: // Illumance Sensor 44 | data.lux = sensorEntry.value; 45 | break; 46 | case 102: 47 | data.presence = sensorEntry.value; 48 | break; 49 | case 103: 50 | data.temperature = sensorEntry.value; 51 | break; 52 | case 104: 53 | data.humidity = sensorEntry.value; 54 | break; 55 | case 115: 56 | data.pressure = sensorEntry.value; 57 | break; 58 | case 113: 59 | var accel = {}; 60 | accel.x = sensorEntry.value.x; 61 | accel.y = sensorEntry.value.y; 62 | accel.z = sensorEntry.value.z; 63 | data.accel = accel; 64 | break; 65 | case 134: 66 | var gyro = {}; 67 | gyro.x = sensorEntry.value.x; 68 | gyro.y = sensorEntry.value.y; 69 | gyro.z = sensorEntry.value.z; 70 | data.gyro = gyro; 71 | break; 72 | case 136: // GPS 73 | var loc = {}; 74 | loc.lat = sensorEntry.value.latitude; 75 | loc.lng = sensorEntry.value.longitude; 76 | loc.alt = sensorEntry.value.altitude; 77 | data.loc = loc; 78 | break; 79 | default: 80 | console.log('Something messed up, type not handled.') 81 | break; 82 | }; 83 | }); 84 | 85 | console.log(data); 86 | return data; 87 | } -------------------------------------------------------------------------------- /edgetrackerlora/README.md: -------------------------------------------------------------------------------- 1 | Stay tuned for resources from Track & Trace Using AWS & Semtech Cloud Services webinar 2 | -------------------------------------------------------------------------------- /gateway_watchdog/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .env 6 | .venv 7 | *.egg-info 8 | 9 | # CDK asset staging directory 10 | .cdk.staging 11 | cdk.out 12 | -------------------------------------------------------------------------------- /gateway_watchdog/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: MIT-0 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 7 | # software and associated documentation files (the "Software"), to deal in the Software 8 | # without restriction, including without limitation the rights to use, copy, modify, 9 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so. 11 | # 12 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 14 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 15 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 16 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 17 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | from aws_cdk import core 20 | 21 | from cdkstack.lorawan_connectivity_watchdog_stack import LorawanConnectivityWatchdogStack 22 | 23 | app = core.App() 24 | LorawanConnectivityWatchdogStack(app, "LorawanConnectivityWatchdogStack") 25 | app.synth() 26 | -------------------------------------------------------------------------------- /gateway_watchdog/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "@aws-cdk/core:enableStackNameDuplicates": "true", 6 | "aws-cdk:enableDiffNoFail": "true", 7 | "@aws-cdk/core:stackRelativeExports": "true", 8 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 9 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 10 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 11 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 12 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true, 15 | "@aws-cdk/aws-lambda:recognizeVersionProps": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /gateway_watchdog/cdkstack/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/gateway_watchdog/cdkstack/__init__.py -------------------------------------------------------------------------------- /gateway_watchdog/images/connectivity_watchdog_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/gateway_watchdog/images/connectivity_watchdog_architecture.png -------------------------------------------------------------------------------- /gateway_watchdog/images/ioteventsdetectormodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/gateway_watchdog/images/ioteventsdetectormodel.png -------------------------------------------------------------------------------- /gateway_watchdog/images/mqtttestclient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/gateway_watchdog/images/mqtttestclient.png -------------------------------------------------------------------------------- /gateway_watchdog/images/step_functions_state_machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/gateway_watchdog/images/step_functions_state_machine.png -------------------------------------------------------------------------------- /gateway_watchdog/localtools/prepare.sh: -------------------------------------------------------------------------------- 1 | isengard assume svirida+iotcorelorawan@amazon.de --nocache 2 | export AWS_DEFAULT_REGION=eu-west-1 3 | source .venv/bin/activate -------------------------------------------------------------------------------- /gateway_watchdog/requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | aws_cdk.core 3 | aws_cdk.aws_iam 4 | aws_cdk.aws_lambda 5 | aws_cdk.aws_iotevents 6 | aws_cdk.aws_stepfunctions 7 | aws_cdk.aws_stepfunctions_tasks 8 | aws_cdk.aws_sns 9 | aws_cdk.aws_sns_subscriptions 10 | aws_cdk.aws_events 11 | aws_cdk.aws_events_targets 12 | -------------------------------------------------------------------------------- /gateway_watchdog/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | with open("README.md") as fp: 5 | long_description = fp.read() 6 | 7 | 8 | setuptools.setup( 9 | name="lorawan_connectivity_watchdog", 10 | version="0.0.1", 11 | 12 | description="Monitoring and notifications for LoRaWAN gateway connection status", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | 16 | author="Andrei Svirida", 17 | 18 | package_dir={"": "cdkstack"}, 19 | packages=setuptools.find_packages(where="cdkstack"), 20 | 21 | install_requires=[ 22 | "aws-cdk.core>=1.109.0", 23 | ], 24 | 25 | python_requires=">=3.6", 26 | 27 | classifiers=[ 28 | "Development Status :: 4 - Beta", 29 | 30 | "Intended Audience :: Developers", 31 | 32 | "Programming Language :: JavaScript", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Python :: 3.6", 35 | "Programming Language :: Python :: 3.7", 36 | "Programming Language :: Python :: 3.8", 37 | 38 | "Topic :: Software Development :: Code Generators", 39 | "Topic :: Utilities", 40 | 41 | "Typing :: Typed", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /gateway_watchdog/source.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | rem The sole purpose of this script is to make the command 4 | rem 5 | rem source .venv/bin/activate 6 | rem 7 | rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. 8 | rem On Windows, this command just runs this batch file (the argument is ignored). 9 | rem 10 | rem Now we don't need to document a Windows command for activating a virtualenv. 11 | 12 | echo Executing .venv\Scripts\activate.bat for you 13 | .venv\Scripts\activate.bat 14 | -------------------------------------------------------------------------------- /gateway_watchdog/src_get_wireless_gateway_statistics_lambda/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 -------------------------------------------------------------------------------- /gateway_watchdog/src_get_wireless_gateway_statistics_lambda/test.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | s3_client = boto3.client('s3') 4 | -------------------------------------------------------------------------------- /gateway_watchdog/src_put_cloudwatch_metrics/lambda.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | import json 18 | import boto3 19 | import traceback 20 | import logging 21 | import os 22 | import sys 23 | 24 | # Define parameters for check of input validity 25 | OBLIGATORY_PARAMETERS = ["GatewayId", "MetricName", "MetricValueNumeric"] 26 | 27 | # Function name for logging 28 | FUNCTION_NAME = "PutCloudwatchMetrics" 29 | 30 | # Setup logging 31 | logger = logging.getLogger(FUNCTION_NAME) 32 | logger.setLevel(logging.INFO) 33 | 34 | # Create an instance of a low-level client representing AWS IoT Core for LoRaWAN 35 | client_iotwireless = boto3.client("iotwireless") 36 | client_cloudwatch = boto3.client("cloudwatch") 37 | client_iotevents = boto3.client("iotevents-data") 38 | 39 | 40 | if "TEST_MODE" in os.environ and os.environ.get("TEST_MODE") == 'true': 41 | TEST_MODE = True 42 | else: 43 | TEST_MODE = False 44 | 45 | logger.info(f"TEST_MODE is {TEST_MODE}") 46 | 47 | 48 | class MissingParameterInEvent(Exception): 49 | """Raised when the parameter is missing""" 50 | pass 51 | 52 | 53 | def put_cloudwatch_metric_number(metric_name: str, metric_value: int, gatewayid: str) -> None: 54 | response = client_cloudwatch.put_metric_data( 55 | MetricData=[ 56 | { 57 | 'MetricName': metric_name, 58 | 'Dimensions': [ 59 | { 60 | 'Name': 'GATEWAYID', 61 | 'Value': gatewayid 62 | } 63 | ], 64 | 'Unit': 'None', 65 | 'Value': metric_value 66 | }, 67 | ], 68 | Namespace='LoRaWAN' 69 | ) 70 | 71 | 72 | def handler(event, context): 73 | logger.info("Received event: %s" % json.dumps(event)) 74 | 75 | # Check if all the necessary params are included and return an error ststus otherwise 76 | for i in OBLIGATORY_PARAMETERS: 77 | if i not in event: 78 | logger.error(f"Parameter {i} missing ") 79 | return { 80 | "status": 500, 81 | "errormessage": f"Parameter {i} missing" 82 | } 83 | 84 | try: 85 | 86 | response = put_cloudwatch_metric_number(metric_name=event.get("MetricName"), 87 | metric_value=event.get("MetricValueNumeric"), 88 | gatewayid=event.get("GatewayId") 89 | ) 90 | result = { 91 | "status": 200, 92 | "trace": response 93 | } 94 | return result 95 | except Exception as e: 96 | exception_type, exception_value, exception_traceback = sys.exc_info() 97 | traceback_string = traceback.format_exception( 98 | exception_type, exception_value, exception_traceback) 99 | 100 | logger.error("Error: " + str(e)) 101 | result = { 102 | "status": 500, 103 | "errors": { 104 | "errormessage": str(e), 105 | "traceback": traceback_string 106 | } 107 | } 108 | return result 109 | -------------------------------------------------------------------------------- /gateway_watchdog/src_put_cloudwatch_metrics/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 -------------------------------------------------------------------------------- /gateway_watchdog/src_put_cloudwatch_metrics/test.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | s3_client = boto3.client('s3') 4 | -------------------------------------------------------------------------------- /gateway_watchdog/tests/input_connected.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "gatewayid": "00000000-0000-0000-0000-000000000000", 4 | "connection_status": "Connected", 5 | "last_uplink_received_timestamp_ms": 12345 6 | } 7 | } -------------------------------------------------------------------------------- /gateway_watchdog/tests/input_disconnected.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "gatewayid": "00000000-0000-0000-0000-000000000000", 4 | "connection_status": "Disconnected", 5 | "last_uplink_received_timestamp_ms": 12345 6 | } 7 | } -------------------------------------------------------------------------------- /gateway_watchdog/tests/local_invoke_lambda.sh: -------------------------------------------------------------------------------- 1 | # See https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-cdk-getting-started.html 2 | # for guidelines to install aws-sam-cli-beta-cdk. 3 | # Mac OS: 4 | # brew install aws-sam-cli-beta-cdk. 5 | sam-beta-cdk local invoke LorawanConnectivityWatchdogStack/GetWirelessGatewayStatisticsLambda -e $1 -------------------------------------------------------------------------------- /iotthingshadow/images/AWS_IoT_-_Things_-_0b27a5cc-8a03-4841-8aae-dd19075310a0_-_LoRaWANTelemetry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/iotthingshadow/images/AWS_IoT_-_Things_-_0b27a5cc-8a03-4841-8aae-dd19075310a0_-_LoRaWANTelemetry.png -------------------------------------------------------------------------------- /iotthingshadow/src-iotrule-transformation/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /iotthingshadow/src-iotrule-transformation/samplevent.json: -------------------------------------------------------------------------------- 1 | // Publish to topic 2 | // $aws/rules/thingshadow_UpdateShadowWithLoRaWANPayload_MapThingNameFor_sample_device 3 | // or 4 | // 5 | { 6 | "PayloadData": "y8QJFQFwAQkCf/8=", 7 | "WirelessDeviceId": "8b00de4a-0fac-407b-93e6-8c59fd411f16", 8 | "ApplicationId": 2, 9 | "Metadata": { 10 | "LoRaWAN": { 11 | "DataRate": 0, 12 | "DevEUI": "a84041b3618248f7", 13 | "FPort": 2, 14 | "Frequency": 867100000, 15 | "Gateways": [ 16 | { 17 | "GatewayEUI": "80029cfffe5cf1f3", 18 | "RSSI": -31, 19 | "SNR": 10.75 20 | }, 21 | { 22 | "GatewayEUI": "90029cfffe5cf1f3", 23 | "RSSI": -32, 24 | "SNR": 11.75 25 | } 26 | ], 27 | "Timestamp": "2020-10-15T16:12:55Z" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /iotthingshadow/src-mapthingname/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.16.43 -------------------------------------------------------------------------------- /iotthingshadow/src-mapthingname/samplevent.json: -------------------------------------------------------------------------------- 1 | { 2 | "searchvalue": "8b00de4a-0fac-407b-93e6-8c59fd411f16" 3 | } -------------------------------------------------------------------------------- /iotthingshadow/src-payload-decoders/python/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/iotthingshadow/src-payload-decoders/python/requirements.txt -------------------------------------------------------------------------------- /iotthingshadow/src-payload-decoders/python/sample_device.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | import time 20 | import math 21 | 22 | 23 | def dict_from_payload(base64_input: str): 24 | decoded = base64.b64decode(base64_input) 25 | 26 | temperature = round(20 + (math.cos(time.time()/10)*10), 2) 27 | humidity = round(50 + (math.sin(time.time()/10)*10), 2) 28 | 29 | result = { 30 | "temperature": temperature, 31 | "humidity": humidity 32 | } 33 | 34 | return result 35 | -------------------------------------------------------------------------------- /observability/README.md: -------------------------------------------------------------------------------- 1 | # Oservability of LoRaWAN devices and gateways 2 | 3 | Here you will find guidelines for implementing oservability of LoRaWAN devices and gateways when using AWS IoT Core for LoRaWAN: 4 | - To learn how to retrieve **LoRaWAN gateway** statistics, please click [here](#how-to-use-aws-iot-core-for-lorawan-apis-to-retrieve-gateway-statistics) 5 | - To learn how to retrieve **LoRaWAN device** statistics, please click [here](#how-to-use-aws-iot-core-for-lorawan-apis-to-retrieve-device-statistics) 6 | 7 | 8 | 9 | ## How to use AWS IoT Core for LoRaWAN APIs to retrieve gateway statistics 10 | 11 | You can use [GetWirelessGatewayStatistics API](https://docs.aws.amazon.com/iot-wireless/2020-11-22/apireference/API_GetWirelessGatewayStatistics.html) to retrieve the information about timestamp of last uplink and the connectivity status of the gateway. The following steps provide an example for an invocation of this API using AWS CLI. Please ensure that [jq tool](https://stedolan.github.io/jq/) is installed before running these steps. 12 | 13 | **Step 1: List wireless gateways** 14 | 15 | Please run this command to list wireless gateways registred in your AWS account: 16 | 17 | ```shell 18 | # Set default region 19 | AWS_DEFAULT_REGION=eu-west-1 20 | # Call GetWirelessGatewayStatistics API 21 | aws iotwireless list-wireless-gateways | jq -r '.WirelessGatewayList[] | "\(.Name),\(.Id)"' 22 | ``` 23 | 24 | As an output of this command you will see a list of wireless gateways registred in your AWS account/region, for example: 25 | ``` 26 | My Gateway 1,a904247a-772b-4aa5-86bc-86bc86bc86bc 27 | My Gateway 2,e1c68458-0ca1-4c62-9b9a-9b9a9b9a9b9a 28 | ``` 29 | 30 | Please select one of the gateway id's (e.g. `a904247a-772b-4aa5-86bc-86bc86bc86bc`) 31 | 32 | **Step 2: Retrieve wireless gateway statistics** 33 | 34 | Please run the following command to retrieve statistics for the wireless gateway using the previously noted id: 35 | 36 | ```shell 37 | aws iotwireless get-wireless-gateway-statistics --wireless-gateway-id a904247a-772b-4aa5-86bc-86bc86bc86bc 38 | ``` 39 | 40 | You will see a timestamp of last uplink and the connectivity status of the gateway as an output: 41 | 42 | ```json 43 | { 44 | "WirelessGatewayId": "a904247a-772b-4aa5-86bc-86bc86bc86bc", 45 | "LastUplinkReceivedAt": "2021-07-09T06:54:13.597698337Z", 46 | "ConnectionStatus": "Connected" 47 | } 48 | ``` 49 | 50 | 51 | ## How to use AWS IoT Core for LoRaWAN APIs to retrieve device statistics 52 | 53 | You can use [GetWirelessDeviceStatistics API](https://docs.aws.amazon.com/iot-wireless/2020-11-22/apireference/API_GetWirelessDeviceStatistics.html) to retrieve the information about timestamp of last uplink and the connectivity status of the gateway. The following steps provide an example for an invocation of this API using AWS CLI. Please ensure that [jq tool](https://stedolan.github.io/jq/) is installed before running these steps. 54 | 55 | **Step 1: List wireless gateways** 56 | 57 | Please run this command to list wireless gateways registred in your AWS account: 58 | 59 | ```shell 60 | # Set default region 61 | AWS_DEFAULT_REGION=eu-west-1 62 | # Call GetWirelessGatewayStatistics API 63 | aws iotwireless list-wireless-devices | jq -r '.WirelessDeviceList[] | "\(.Name),\(.Id)"' 64 | ``` 65 | 66 | As an output of this command you will see a list of wireless devices registred in your AWS account/region, for example: 67 | ``` 68 | Device 1,b81ad383-6d80-43dc-ae0f-e3e4baa9b3bc 69 | Device 2,75495835-5a61-49fd-ab9f-eeb2e7ef77f5 70 | ``` 71 | 72 | Please select one of the wireless device id's (e.g. `75495835-5a61-49fd-ab9f-eeb2e7ef77f5`) 73 | 74 | **Step 2: Retrieve wireless device statistics** 75 | 76 | Please run the following command to retrieve statistics for the wireless device using the previously noted id: 77 | 78 | ```shell 79 | aws iotwireless get-wireless-device-statistics --wireless-device-id 75495835-5a61-49fd-ab9f-eeb2e7ef77f5 80 | ``` 81 | 82 | You will see a timestamp of last uplink and metadata of last LoRaWAN message as an output: 83 | 84 | ```json 85 | { 86 | "WirelessDeviceId": "75495835-5a61-49fd-ab9f-eeb2e7ef77f5", 87 | "LastUplinkReceivedAt": "2021-07-09T07:05:09.533280525Z", 88 | "LoRaWAN": { 89 | "DevEui": "a84041d55182720b", 90 | "FPort": 2, 91 | "DataRate": 5, 92 | "Frequency": 867700000, 93 | "Timestamp": "2021-07-09T07:05:09.533280525Z", 94 | "Gateways": [ 95 | { 96 | "GatewayEui": "c0ee40ffff29df10", 97 | "Snr": 10.25, 98 | "Rssi": -31.0 99 | } 100 | ] 101 | } 102 | } 103 | ``` 104 | 105 | -------------------------------------------------------------------------------- /send_downlink_payload/images/original/AWS_IoT_-_Test_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/send_downlink_payload/images/original/AWS_IoT_-_Test_0.png -------------------------------------------------------------------------------- /send_downlink_payload/images/original/AWS_IoT_-_Test_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/send_downlink_payload/images/original/AWS_IoT_-_Test_1.png -------------------------------------------------------------------------------- /send_downlink_payload/images/original/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/send_downlink_payload/images/original/arch.png -------------------------------------------------------------------------------- /send_downlink_payload/src/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.16.47 -------------------------------------------------------------------------------- /soilmoisture_alarming/images/resized/0010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/soilmoisture_alarming/images/resized/0010.png -------------------------------------------------------------------------------- /soilmoisture_alarming/images/resized/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/soilmoisture_alarming/images/resized/arch.png -------------------------------------------------------------------------------- /soilmoisture_alarming/images/resized/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/soilmoisture_alarming/images/resized/input.png -------------------------------------------------------------------------------- /soilmoisture_alarming/images/resized/model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/soilmoisture_alarming/images/resized/model.png -------------------------------------------------------------------------------- /timestream/images/grafana/grafana_query-1024x542.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/grafana/grafana_query-1024x542.png -------------------------------------------------------------------------------- /timestream/images/grafana/grafana_query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/grafana/grafana_query.png -------------------------------------------------------------------------------- /timestream/images/grafana/metadata-1024x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/grafana/metadata-1024x400.png -------------------------------------------------------------------------------- /timestream/images/grafana/metadata-1024x542.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/grafana/metadata-1024x542.png -------------------------------------------------------------------------------- /timestream/images/grafana/metadata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/grafana/metadata.png -------------------------------------------------------------------------------- /timestream/images/grafana/telemetry-1024x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/grafana/telemetry-1024x400.png -------------------------------------------------------------------------------- /timestream/images/grafana/telemetry-1024x542.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/grafana/telemetry-1024x542.png -------------------------------------------------------------------------------- /timestream/images/grafana/telemetry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/grafana/telemetry.png -------------------------------------------------------------------------------- /timestream/images/guthub_timestream_quickdemo_1436_708.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/guthub_timestream_quickdemo_1436_708.gif -------------------------------------------------------------------------------- /timestream/images/guthub_timestream_quickdemo_orig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/images/guthub_timestream_quickdemo_orig.gif -------------------------------------------------------------------------------- /timestream/src-lambda-transform/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream/src-lambda-transform/requirements.txt -------------------------------------------------------------------------------- /timestream/src-lambda-transform/samplevent.json: -------------------------------------------------------------------------------- 1 | // Publish to topic 2 | // dt/lorawanbinary/moisture/sample_device/A84041B3618248F7 3 | { 4 | "PayloadData": "y8QJFQFwAQkCf/8=", 5 | "DeviceId": "11a9fdf8-e7ac-406f-9517-df4807603959", 6 | "ApplicationId": 2, 7 | "Metadata": { 8 | "LoRaWAN": { 9 | "DataRate": 0, 10 | "DevEUI": "a84041b3618248f7", 11 | "FPort": 2, 12 | "Frequency": 867100000, 13 | "Gateways": [ 14 | { 15 | "GatewayEUI": "80029cfffe5cf1f3", 16 | "RSSI": -31, 17 | "SNR": 10.75 18 | }, 19 | { 20 | "GatewayEUI": "90029cfffe5cf1f3", 21 | "RSSI": -32, 22 | "SNR": 11.75 23 | } 24 | ], 25 | "Timestamp": "2020-10-15T16:12:55Z" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /timestream/src-lambda-write-to-timestream/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 -------------------------------------------------------------------------------- /timestream/src-lambda-write-to-timestream/sampleevent.json: -------------------------------------------------------------------------------- 1 | { 2 | "transformed_message": { 3 | "status": 200, 4 | "payload": { 5 | "temperature": 29.7, 6 | "humidity": 52.41, 7 | "input_length": 11 8 | }, 9 | "WirelessDeviceId": "904d63b1-ed1d-42ad-8cb4-6778dd03e86c", 10 | "DevEui": "a84041d55182720b" 11 | }, 12 | "lns_message": { 13 | "WirelessDeviceId": "904d63b1-ed1d-42ad-8cb4-6778dd03e86c", 14 | "WirelessMetadata": { 15 | "LoRaWAN": { 16 | "DataRate": 0, 17 | "DevEui": "a84041d55182720b", 18 | "FPort": 2, 19 | "Frequency": 867300000, 20 | "Gateways": [ 21 | { 22 | "GatewayEui": "dca632fffe45b3c0", 23 | "Rssi": -70, 24 | "Snr": 10.75 25 | } 26 | ], 27 | "Timestamp": "2020-12-09T16:33:19Z" 28 | } 29 | }, 30 | "PayloadData": "y68JUAHBAQlsf/8=" 31 | } 32 | } -------------------------------------------------------------------------------- /timestream/src-layer-payload-decoders/python/sample_device.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | import time 20 | import math 21 | 22 | 23 | def dict_from_payload(base64_input: str): 24 | decoded = base64.b64decode(base64_input) 25 | 26 | temperature = round(20 + (math.cos(time.time()/10)*10), 2) 27 | humidity = round(50 + (math.sin(time.time()/10)*10), 2) 28 | 29 | result = { 30 | "temperature": temperature, 31 | "humidity": humidity, 32 | "input_length": len(decoded) 33 | } 34 | 35 | return result 36 | 37 | # battery_value = (decoded[0] << 8 | decoded[1]) & 0x3FFF 38 | # temperature = (decoded[2] << 24 >> 16 | decoded[3])/100 39 | -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/grafana/grafana_query-1024x542.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/grafana/grafana_query-1024x542.png -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/grafana/grafana_query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/grafana/grafana_query.png -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/grafana/metadata-1024x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/grafana/metadata-1024x400.png -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/grafana/metadata-1024x542.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/grafana/metadata-1024x542.png -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/grafana/metadata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/grafana/metadata.png -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/grafana/telemetry-1024x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/grafana/telemetry-1024x400.png -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/grafana/telemetry-1024x542.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/grafana/telemetry-1024x542.png -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/grafana/telemetry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/grafana/telemetry.png -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/guthub_timestream_quickdemo_1436_708.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/guthub_timestream_quickdemo_1436_708.gif -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/images/guthub_timestream_quickdemo_orig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/timestream_for_transform_binary_payload/images/guthub_timestream_quickdemo_orig.gif -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/src-lambda-write-to-timestream/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 -------------------------------------------------------------------------------- /timestream_for_transform_binary_payload/src-lambda-write-to-timestream/sampleevent.json: -------------------------------------------------------------------------------- 1 | { 2 | "transformed_message": { 3 | "status": 200, 4 | "payload": { 5 | "temperature": 29.7, 6 | "humidity": 52.41, 7 | "input_length": 11 8 | }, 9 | "WirelessDeviceId": "904d63b1-ed1d-42ad-8cb4-6778dd03e86c", 10 | "DevEui": "a84041d55182720b" 11 | }, 12 | "lns_message": { 13 | "WirelessDeviceId": "904d63b1-ed1d-42ad-8cb4-6778dd03e86c", 14 | "WirelessMetadata": { 15 | "LoRaWAN": { 16 | "DataRate": 0, 17 | "DevEui": "a84041d55182720b", 18 | "FPort": 2, 19 | "Frequency": 867300000, 20 | "Gateways": [ 21 | { 22 | "GatewayEui": "dca632fffe45b3c0", 23 | "Rssi": -70, 24 | "Snr": 10.75 25 | } 26 | ], 27 | "Timestamp": "2020-12-09T16:33:19Z" 28 | } 29 | }, 30 | "PayloadData": "y68JUAHBAQlsf/8=" 31 | } 32 | } -------------------------------------------------------------------------------- /transform_binary_payload/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 245 | 246 | 247 | # AWS 248 | .aws-sam 249 | /*.toml 250 | 251 | # Custom 252 | .idea 253 | -------------------------------------------------------------------------------- /transform_binary_payload/images/0000-1024x542.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/transform_binary_payload/images/0000-1024x542.png -------------------------------------------------------------------------------- /transform_binary_payload/images/0000-resized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/transform_binary_payload/images/0000-resized.png -------------------------------------------------------------------------------- /transform_binary_payload/src-iotrule-transformation-nodejs/index.test.js: -------------------------------------------------------------------------------- 1 | const lambda = require('./index') 2 | 3 | test_definition = [ 4 | { 5 | "description": "Test 1", 6 | "input": "AwI=", 7 | "fport": 1, 8 | "PayloadDecoderName": "sample_device", 9 | "expected_status": 200, 10 | "expected_output": { 11 | "direction": "W", 12 | "speed": 2 13 | 14 | } 15 | }, 16 | { 17 | "description": "Test 2", 18 | "input": "AwI=", 19 | "fport": 1, 20 | "PayloadDecoderName": "sample_device", 21 | "expected_status": 200, 22 | "expected_output": { 23 | "direction": "W", 24 | "speed": 2 25 | 26 | } 27 | }, 28 | { 29 | "description": "Test 3", 30 | "input": "AwI=", 31 | "fport": 1, 32 | "PayloadDecoderName": "../sample_device", 33 | "expected_status": 500, 34 | "expected_error_message": "Name of decoder ../sample_device does not match the regex in the variable ALLOWED_DECODER_NAME_REGEX" 35 | }, 36 | 37 | { 38 | "description": "Test 4", 39 | "input": "AwI=", 40 | "fport": 1, 41 | "PayloadDecoderName": "*sample_device", 42 | "expected_status": 500, 43 | "expected_error_message": "Name of decoder *sample_device does not match the regex in the variable ALLOWED_DECODER_NAME_REGEX" 44 | } 45 | 46 | 47 | ] 48 | 49 | 50 | for (var i = 0; i < test_definition.length; i++) { 51 | 52 | test_event = { 53 | "PayloadData": test_definition[i].input, 54 | "WirelessDeviceId": "57728ff8-5d1d-4130-9de2-f004d8722bc2", 55 | "PayloadDecoderName": test_definition[i].PayloadDecoderName, 56 | "WirelessMetadata": { 57 | "LoRaWAN": { 58 | "DataRate": 0, 59 | "DevEui": "a84041d55182720b", 60 | "FPort": test_definition[i].fport, 61 | "Frequency": 867900000, 62 | "Gateways": [ 63 | { 64 | "GatewayEui": "dca632fffe45b3c0", 65 | "Rssi": -76, 66 | "Snr": 9.75 67 | } 68 | ], 69 | "Timestamp": "2020-12-07T14:41:48Z" 70 | } 71 | } 72 | } 73 | 74 | console.log(test_event) 75 | console.log("Running test " + test_definition[i].description) 76 | 77 | async function app(test_definition, i) { 78 | 79 | var actual_output = await lambda.handler(test_event, {}) 80 | 81 | // console.log("Binary decoding output=" + JSON.stringify(actual_output, null, " ")) 82 | console.log("---------" + test_definition[i].description + "--------------------------") 83 | if (actual_output.status != test_definition[i].expected_status) { 84 | throw ("ERROR: status " + actual_output.status + " received, but " + test_definition[i].expected_status + " expected") 85 | } else { 86 | console.log("OK: status code " + actual_output.status) 87 | } 88 | 89 | if (actual_output.status != 200) { 90 | if (actual_output.errorMessage != test_definition[i].expected_error_message) { 91 | throw ("ERROR: error message '" + actual_output.errorMessage + "' received, but '" + test_definition[i].expected_error_message + "' expected") 92 | } else { 93 | console.log("OK: error message " + actual_output.errorMessage) 94 | } 95 | } 96 | 97 | for (var key in test_definition[i].expected_output) { 98 | if (test_definition[i].expected_output[key] == null) { 99 | console.log("ERROR: attribute " + key + " is undefined in test definition") 100 | throw "ERROR: attribute " + key + " is undefined in test definition"; 101 | } 102 | if (test_definition[i].expected_output[key] == actual_output[key]) { 103 | console.log("OK: attribute " + key + " has an expected value of " + actual_output[key]) 104 | } else { 105 | console.log("ERROR: for attribute " + key + ": expected " + test_definition[i].expected_output[key] + " but received " + actual_output[key] + ". Dump of actual output:" + JSON.stringify(actual_output) + ", dump of expected output: " + JSON.stringify(test_definition[i].expected_output)) 106 | } 107 | 108 | } 109 | } 110 | 111 | 112 | app(test_definition, i) 113 | 114 | 115 | } 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /transform_binary_payload/src-iotrule-transformation-nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src-lambda-transform-nodejs", 3 | "version": "1.0.0" 4 | } -------------------------------------------------------------------------------- /transform_binary_payload/src-iotrule-transformation-nodejs/samplevent.json: -------------------------------------------------------------------------------- 1 | // $aws/rules/mygithubstack_TransformLoRaWANBinaryPayloadNode_dragino_lht65 2 | { 3 | "PayloadData": "y6QHxgG4AQhmf/8=", 4 | "PayloadDecoderName": "dragino_lht65", 5 | "WirelessDeviceId": "57728ff8-5d1d-4130-9de2-f004d8722bc2", 6 | "WirelessMetadata": { 7 | "LoRaWAN": { 8 | "DataRate": 0, 9 | "DevEui": "a84041d55182720b", 10 | "FPort": 2, 11 | "Frequency": 867900000, 12 | "Gateways": [ 13 | { 14 | "GatewayEui": "dca632fffe45b3c0", 15 | "Rssi": -76, 16 | "Snr": 9.75 17 | } 18 | ], 19 | "Timestamp": "2020-12-07T14:41:48Z" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /transform_binary_payload/src-iotrule-transformation/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/node/dragino_ldds20.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | function decodeUplink(input) { 18 | bytes = input.bytes 19 | port = input.fPort 20 | // Decode an uplink message from a buffer 21 | // (array) of bytes to an object of fields. 22 | var value=(bytes[0]<<8 | bytes[1]) & 0x3FFF; 23 | var batV=value/1000;//Battery,units:V 24 | 25 | value=bytes[2]<<8 | bytes[3]; 26 | var distance=(value);//distance,units:mm 27 | 28 | var i_flag = bytes[4]; 29 | 30 | value=bytes[5]<<8 | bytes[6]; 31 | if(bytes[5] & 0x80) 32 | {value |= 0xFFFF0000;} 33 | var temp_DS18B20=(value/10).toFixed(2);//DS18B20,temperature 34 | 35 | var s_flag = bytes[7]; 36 | return { 37 | data: { 38 | Bat:batV, 39 | Distance:distance, 40 | Interrupt_flag:i_flag, 41 | TempC_DS18B20:temp_DS18B20, 42 | Sensor_flag:s_flag, 43 | } 44 | }; 45 | } -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/node/dragino_lht65.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | function decodeUplink(input) { 18 | bytes = input.bytes 19 | port = input.fPort 20 | 21 | return { 22 | data: { 23 | 24 | //External sensor 25 | // ext_sensor_type: 26 | // { 27 | // "0": "No external sensor", 28 | // "1": "Temperature Sensor", 29 | // "4": "Interrupt Sensor send", 30 | // "5": "Illumination Sensor", 31 | // "6": "ADC Sensor", 32 | // "7": "Interrupt Sensor count", 33 | // }[bytes[6] & 0x7F], 34 | 35 | //Battery,units:V 36 | battery_value: ((bytes[0] << 8 | bytes[1]) & 0x3FFF) / 1000, 37 | 38 | //SHT20,temperature 39 | temperature_internal: (bytes[2] << 24 >> 16 | bytes[3]) / 100, 40 | 41 | //SHT20,Humidity,units:% 42 | humidity: (bytes[4] << 8 | bytes[5]) / 10, 43 | 44 | //DS18B20,temperature,units: 45 | temperature_external: 46 | ((bytes[7] << 24 >> 16 | bytes[8]) / 100), 47 | 48 | 49 | 50 | } 51 | }; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/node/dragino_lsn50v2d23.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | function decodeUplink(input) { 18 | bytes = input.bytes 19 | port = input.fPort 20 | var mode=(bytes[6] & 0x7C)>>2; 21 | var decode = {}; 22 | if((mode!=2)&&(mode!=31)) 23 | { 24 | decode.BatV=(bytes[0]<<8 | bytes[1])/1000; 25 | decode.TempC1= parseFloat(((bytes[2]<<24>>16 | bytes[3])/10).toFixed(2)); 26 | decode.ADC_CH0V=(bytes[4]<<8 | bytes[5])/1000; 27 | decode.Digital_IStatus=(bytes[6] & 0x02)? "H":"L"; 28 | if(mode!=6) 29 | { 30 | decode.EXTI_Trigger=(bytes[6] & 0x01)? "TRUE":"FALSE"; 31 | decode.Door_status=(bytes[6] & 0x80)? "CLOSE":"OPEN"; 32 | } 33 | } 34 | if(mode=='0') 35 | { 36 | decode.Work_mode="IIC"; 37 | if((bytes[9]<<8 | bytes[10])===0) 38 | { 39 | decode.Illum=(bytes[7]<<24>>16 | bytes[8]); 40 | } 41 | else 42 | { 43 | decode.TempC_SHT=parseFloat(((bytes[7]<<24>>16 | bytes[8])/10).toFixed(2)); 44 | decode.Hum_SHT=parseFloat(((bytes[9]<<8 | bytes[10])/10).toFixed(1)); 45 | } 46 | } 47 | else if(mode=='1') 48 | { 49 | decode.Work_mode=" Distance"; 50 | decode.Distance_cm=parseFloat(((bytes[7]<<8 | bytes[8])/10) .toFixed(1)); 51 | if((bytes[9]<<8 | bytes[10])!=65535) 52 | { 53 | decode.Distance_signal_strength=parseFloat((bytes[9]<<8 | bytes[10]) .toFixed(0)); 54 | } 55 | } 56 | else if(mode=='2') 57 | { 58 | decode.Work_mode=" 3ADC"; 59 | decode.BatV=bytes[11]/10; 60 | decode.ADC_CH0V=(bytes[0]<<8 | bytes[1])/1000; 61 | decode.ADC_CH1V=(bytes[2]<<8 | bytes[3])/1000; 62 | decode.ADC_CH4V=(bytes[4]<<8 | bytes[5])/1000; 63 | decode.Digital_IStatus=(bytes[6] & 0x02)? "H":"L"; 64 | decode.EXTI_Trigger=(bytes[6] & 0x01)? "TRUE":"FALSE"; 65 | decode.Door_status=(bytes[6] & 0x80)? "CLOSE":"OPEN"; 66 | if((bytes[9]<<8 | bytes[10])===0) 67 | { 68 | decode.Illum=(bytes[7]<<24>>16 | bytes[8]); 69 | } 70 | else 71 | { 72 | decode.TempC_SHT=parseFloat(((bytes[7]<<24>>16 | bytes[8])/10).toFixed(2)); 73 | decode.Hum_SHT=parseFloat(((bytes[9]<<8 | bytes[10])/10) .toFixed(1)); 74 | } 75 | } 76 | else if(mode=='3') 77 | { 78 | decode.Work_mode="3DS18B20"; 79 | decode.TempC2=parseFloat(((bytes[7]<<24>>16 | bytes[8])/10).toFixed(2)); 80 | decode.TempC3=parseFloat(((bytes[9]<<24>>16 | bytes[10])/10) .toFixed(1)); 81 | } 82 | else if(mode=='4') 83 | { 84 | decode.Work_mode="Weight"; 85 | decode.Weight=(bytes[7]<<24>>16 | bytes[8]); 86 | } 87 | else if(mode=='5') 88 | { 89 | decode.Work_mode="Count"; 90 | decode.Count=(bytes[7]<<24 | bytes[8]<<16 | bytes[9]<<8 | bytes[10]); 91 | } 92 | else if(mode=='31') 93 | { 94 | decode.Work_mode="ALARM"; 95 | decode.BatV=(bytes[0]<<8 | bytes[1])/1000; 96 | decode.TempC1= parseFloat(((bytes[2]<<24>>16 | bytes[3])/10).toFixed(2)); 97 | decode.TempC1MIN= bytes[4]<<24>>24; 98 | decode.TempC1MAX= bytes[5]<<24>>24; 99 | decode.SHTEMPMIN= bytes[7]<<24>>24; 100 | decode.SHTEMPMAX= bytes[8]<<24>>24; 101 | decode.SHTHUMMIN= bytes[9]; 102 | decode.SHTHUMMAX= bytes[10]; 103 | } 104 | if((bytes.length==11)||(bytes.length==12)) 105 | { 106 | return decode; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/node/dragino_lsph01.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | function decodeUplink(input) { 18 | bytes = input.bytes 19 | port = input.fPort 20 | // Decode an uplink message from a buffer 21 | // (array) of bytes to an object of fields. 22 | var value=(bytes[0]<<8 | bytes[1]) & 0x3FFF; 23 | var batV=value/1000;//Battery,units:V 24 | 25 | value=bytes[2]<<8 | bytes[3]; 26 | if(bytes[2] & 0x80) 27 | {value |= 0xFFFF0000;} 28 | var temp_DS18B20=(value/10).toFixed(2);//DS18B20,temperature 29 | 30 | value=bytes[4]<<8 | bytes[5]; 31 | var PH1=(value/100).toFixed(2); 32 | 33 | value=bytes[6]<<8 | bytes[7]; 34 | var temp=0; 35 | if((value & 0x8000)>>15 === 0) 36 | temp=(value/10).toFixed(2);//temp_SOIL,temperature 37 | else if((value & 0x8000)>>15 === 1) 38 | temp=((value-0xFFFF)/10).toFixed(2); 39 | 40 | var i_flag = bytes[8]; 41 | var mes_type = bytes[10]; 42 | return { 43 | data: { 44 | Bat:batV, 45 | TempC_DS18B20:temp_DS18B20, 46 | PH1_SOIL:PH1, 47 | TEMP_SOIL:temp, 48 | Interrupt_flag:i_flag, 49 | Message_type:mes_type 50 | } 51 | }; 52 | } -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/node/dragino_lwl02.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | function decodeUplink(input) { 18 | bytes = input.bytes 19 | port = input.fPort 20 | // Decode an uplink message from a buffer 21 | // (array) of bytes to an object of fields. 22 | var value=(bytes[0]<<8 | bytes[1])&0x3FFF; 23 | var bat=value/1000;//Battery,units:V 24 | 25 | var door_open_status=bytes[0]&0x80?1:0;//1:open,0:close 26 | var water_leak_status=bytes[0]&0x40?1:0; 27 | 28 | var mod=bytes[2]; 29 | var alarm=bytes[9]&0x01; 30 | 31 | if(mod==1){ 32 | var open_times=bytes[3]<<16 | bytes[4]<<8 | bytes[5]; 33 | var open_duration=bytes[6]<<16 | bytes[7]<<8 | bytes[8];//units:min 34 | if(bytes.length==10 && 0x07>bytes[0]< 0x0f) 35 | return { 36 | data: { 37 | BAT_V:bat, 38 | MOD:mod, 39 | DOOR_OPEN_STATUS:door_open_status, 40 | DOOR_OPEN_TIMES:open_times, 41 | LAST_DOOR_OPEN_DURATION:open_duration, 42 | ALARM:alarm 43 | } 44 | }; 45 | } 46 | else if(mod==2) 47 | { 48 | var leak_times=bytes[3]<<16 | bytes[4]<<8 | bytes[5]; 49 | var leak_duration=bytes[6]<<16 | bytes[7]<<8 | bytes[8];//units:min 50 | if(bytes.length==10 && 0x07>bytes[0]< 0x0f) 51 | return { 52 | data: { 53 | BAT_V:bat, 54 | MOD:mod, 55 | WATER_LEAK_STATUS:water_leak_status, 56 | WATER_LEAK_TIMES:leak_times, 57 | LAST_WATER_LEAK_DURATION:leak_duration 58 | } 59 | }; 60 | } 61 | else if(mod==3) 62 | if(bytes.length==10 && 0x07>bytes[0]< 0x0f) 63 | { 64 | return { 65 | data: { 66 | BAT_V:bat, 67 | MOD:mod, 68 | DOOR_OPEN_STATUS:door_open_status, 69 | WATER_LEAK_STATUS:water_leak_status, 70 | ALARM:alarm 71 | } 72 | }; 73 | } 74 | else{ 75 | return { 76 | data: { 77 | BAT_V:bat, 78 | MOD:mod, 79 | } 80 | }; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/node/sample_device.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | var state = ['on', 'off', 'standby']; 18 | 19 | function decodeUplink(input) { 20 | switch (input.fPort) { 21 | case 1: 22 | return { 23 | // Decoded data 24 | data: { 25 | state: state[input.bytes[0]], 26 | speed: input.bytes[1], 27 | }, 28 | }; 29 | default: 30 | return { 31 | errors: ['unknown FPort ' + input.fPort], 32 | }; 33 | } 34 | } 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/node/tests/dragino_lht65.test.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | const DECODER_NAME = "dragino_lht65" 18 | const decoder = require("../" + DECODER_NAME + ".js") 19 | 20 | test_definition = [ 21 | { 22 | "description": "Test 1", 23 | "input": Uint8Array.from(Buffer.from("y6QHxgG4AQhmf/8=", 'base64')), 24 | "fport": 1, 25 | "expected_output": { 26 | "ext_sensor_type": "Temperature Sensor", 27 | "battery_value": 2.98, 28 | "temperature_external": 19.9, 29 | "humidity": 44, 30 | "temperature_internal": 21.5 31 | 32 | } 33 | } 34 | ] 35 | 36 | 37 | for (var i = 0; i < test_definition.length; i++) { 38 | test_event = test_definition[i]; 39 | actual_output = decoder.decodeUplink({ 40 | "bytes": test_event.input, 41 | "fPort": test_event.fport 42 | }) 43 | console.log("Running test " + test_event.description) 44 | console.log("------------------------------------------") 45 | for (var key in test_event.expected_output) { 46 | if (typeof (test_event.expected_output[key]) != typeof (actual_output.data[key])) { 47 | console.log("ERROR: type missmatch for attribute " + key + ": expected " + typeof (test_event.expected_output[key]) + " but received " + typeof (actual_output.data[key]) + ". Dump of actual output:" + JSON.stringify(actual_output.data) + ", dump of expected output: " + JSON.stringify(test_event.expected_output)) 48 | 49 | } 50 | else if (test_event.expected_output[key] == actual_output.data[key]) { 51 | console.log("OK: attribute " + key + " has an expected value of " + actual_output.data[key]) 52 | } else { 53 | console.log("ERROR: for attribute " + key + ": expected " + test_event.expected_output[key] + " but received " + actual_output.data[key] + ". Dump of actual output:" + JSON.stringify(actual_output.data) + ", dump of expected output: " + JSON.stringify(test_event.expected_output)) 54 | } 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/node/tests/sample_device.test.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | const DECODER_PATH = "/opt/node" 18 | const DECODER_NAME = "sample_device" 19 | const decoder = require("../" + DECODER_NAME + ".js") 20 | 21 | test_definition = [ 22 | { 23 | "description": "Test 1", 24 | "input": Buffer.from([0x03, 0x02]), 25 | "fport": 1, 26 | "expected_output": { 27 | "direction": "W", 28 | "speed": 2 29 | 30 | } 31 | }, 32 | { 33 | "description": "Test 2", 34 | "input": Buffer.from([0x01, 0x01]), 35 | "fport": 1, 36 | "expected_output": { 37 | "direction": "E", 38 | "speed": 1 39 | 40 | } 41 | } 42 | ] 43 | 44 | 45 | for (var i = 0; i < test_definition.length; i++) { 46 | test_event = test_definition[i]; 47 | actual_output = decoder.decodeUplink({ 48 | "bytes": test_event.input, 49 | "fPort": test_event.fport 50 | }) 51 | console.log("Running test " + test_event.description) 52 | console.log("------------------------------------------") 53 | for (var key in test_event.expected_output) { 54 | if (typeof (test_event.expected_output[key]) != typeof (actual_output.data[key])) { 55 | console.log("ERROR: type missmatch for attribute " + key + ": expected " + typeof (test_event.expected_output[key]) + " but received " + typeof (actual_output.data[key]) + ". Dump of actual output:" + JSON.stringify(actual_output.data) + ", dump of expected output: " + JSON.stringify(test_event.expected_output)) 56 | 57 | } 58 | else if (test_event.expected_output[key] == actual_output.data[key]) { 59 | console.log("OK: attribute " + key + " has an expected value of " + actual_output.data[key]) 60 | } else { 61 | console.log("ERROR: for attribute " + key + ": expected " + test_event.expected_output[key] + " but received " + actual_output.data[key] + ". Dump of actual output:" + JSON.stringify(actual_output.data) + ", dump of expected output: " + JSON.stringify(test_event.expected_output)) 62 | } 63 | 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/axioma_w1.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | import base64 18 | import datetime 19 | 20 | 21 | def int_from_bytes_at_offset(bytes, block_offset, block_size): 22 | # accomodate LSB order 23 | offset = block_offset + block_size - 1 24 | bit_shift = 8 * (block_size - 1) 25 | 26 | int_value = 0 27 | while offset >= block_offset: 28 | byte = bytes[offset] 29 | offset -= 1 30 | int_value |= byte << bit_shift 31 | bit_shift -= 8 32 | 33 | return int_value 34 | 35 | 36 | def timestamp_from_bytes_at_offset(bytes, offset): 37 | milliseconds = int_from_bytes_at_offset(bytes, offset, 4) 38 | return datetime.datetime.fromtimestamp(milliseconds, tz=datetime.timezone.utc) 39 | 40 | 41 | def decode_primary_data(bytes, offset, decoded_payload): 42 | 43 | size = 4 44 | decoded_payload["timestamp"] = timestamp_from_bytes_at_offset( 45 | bytes, offset).isoformat() 46 | offset += size 47 | 48 | size = 1 49 | decoded_payload["status"] = bytes[offset] 50 | offset += size 51 | 52 | size = 4 53 | decoded_payload["total_volume"] = int_from_bytes_at_offset( 54 | bytes, offset, size) 55 | offset += size 56 | 57 | return offset 58 | 59 | 60 | def decode_log_data(bytes, offset, decoded_payload): 61 | 62 | # Get all available log data - variable number up to 15 deltas 63 | decoded_payload["log_data"] = {} 64 | 65 | size = 4 66 | # Log data is always taken at the start of the storage period - 1hr by default 67 | cumulative_timestamp = timestamp_from_bytes_at_offset( 68 | bytes, offset).replace(minute=0, second=0, microsecond=0) 69 | decoded_payload["log_data"]["timestamp_0"] = cumulative_timestamp.isoformat() 70 | offset += size 71 | 72 | size = 4 73 | decoded_payload["log_data"]["volume_0"] = int_from_bytes_at_offset( 74 | bytes, offset, size) 75 | offset += size 76 | 77 | cumulative_volume = decoded_payload["log_data"]["volume_0"] 78 | 79 | log_index = 1 80 | payload_size = len(bytes) 81 | while offset < payload_size: 82 | # Add an hour - Change this to match the meter log storage period, default is 1 hour 83 | cumulative_timestamp += datetime.timedelta(hours=1) 84 | 85 | size = 2 86 | cumulative_volume += int_from_bytes_at_offset(bytes, offset, size) 87 | offset += size 88 | 89 | decoded_payload["log_data"][f"timestamp_{log_index}"] = cumulative_timestamp.isoformat() 90 | decoded_payload["log_data"][f"volume_{log_index}"] = cumulative_volume 91 | 92 | log_index += 1 93 | 94 | return offset 95 | 96 | 97 | def decode_individual_alarm(alarm_status, alarm_test_value, higher_alarm): 98 | alarm = (alarm_status == alarm_test_value) and not higher_alarm 99 | higher_alarm = alarm or higher_alarm 100 | return alarm, higher_alarm 101 | 102 | 103 | def decode_alarm_data(decoded_payload): 104 | # Decode alarms from status byte - higher in the list takes precedence 105 | higher_alarm = False 106 | alarm_status = decoded_payload["status"] 107 | 108 | decoded_payload["alarm_low_temperature"], higher_alarm = decode_individual_alarm( 109 | alarm_status, 0x90, higher_alarm) 110 | decoded_payload["alarm_leakage"], higher_alarm = decode_individual_alarm( 111 | alarm_status, 0x30, higher_alarm) 112 | decoded_payload["alarm_burst"], higher_alarm = decode_individual_alarm( 113 | alarm_status, 0xB0, higher_alarm) 114 | decoded_payload["alarm_backflow"], higher_alarm = decode_individual_alarm( 115 | alarm_status, 0x70, higher_alarm) 116 | decoded_payload["alarm_dry"], higher_alarm = decode_individual_alarm( 117 | alarm_status, 0x10, higher_alarm) 118 | decoded_payload["alarm_manipulation"], higher_alarm = decode_individual_alarm( 119 | alarm_status, 0xD0, higher_alarm) 120 | decoded_payload["alarm_permanent"], higher_alarm = decode_individual_alarm( 121 | alarm_status, 0x08, higher_alarm) 122 | decoded_payload["alarm_battery"], higher_alarm = decode_individual_alarm( 123 | alarm_status, 0x04, higher_alarm) 124 | 125 | 126 | def dict_from_payload(payload, fport: int = None): 127 | bytes = base64.b64decode(payload) 128 | 129 | decoded_payload = {} 130 | offset = 0 131 | 132 | offset = decode_primary_data(bytes, offset, decoded_payload) 133 | offset = decode_log_data(bytes, offset, decoded_payload) 134 | decode_alarm_data(decoded_payload) 135 | 136 | return decoded_payload 137 | 138 | 139 | if __name__ == "__main__": 140 | # This payload is taken from Axioma_Lora_Payload_W1_F1_V2.0 (1).pdf - doc is a little wrong 141 | hex_bytes = "0ea0355d302935000054c0345de7290000b800b900b800b800b800b900b800b800b800b800b800b800b900b900b900" 142 | base_64_bytes = base64.b64encode(bytes.fromhex(hex_bytes)).decode() 143 | 144 | # Actual meter reading 145 | # base_64_bytes = "eoFaXxADAAAAwKRZXwMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" 146 | 147 | decoded_dict = dict_from_payload(base_64_bytes) 148 | 149 | import json 150 | print(json.dumps(decoded_dict, indent=4)) 151 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/dragino_laq4.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str, fport: int = None): 22 | """ Decodes a base64-encoded binary payload into JSON. 23 | Parameters 24 | ---------- 25 | base64_input : str 26 | Base64-encoded binary payload 27 | fport: int 28 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 29 | 30 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 31 | 32 | Returns 33 | ------- 34 | JSON object with key/value pairs of decoded attributes 35 | 36 | """ 37 | 38 | decoded = base64.b64decode(base64_input) 39 | 40 | # Battery voltage 41 | battery_value = ((decoded[0] << 8 | decoded[1]) & 0x3FFF) / 1000 42 | 43 | mode=(decoded[2] & 0b01111100)>>2 44 | 45 | if mode == 1: #mode = 1 #for Normal Operation 46 | Work_mode="CO2" 47 | if (decoded[2] & 0b00000001): 48 | Alarm_status = "TRUE" 49 | else: 50 | Alarm_status = "FALSE" 51 | 52 | TVOC_ppb= decoded[3]<<8 | decoded[4] 53 | CO2_ppm= decoded[5]<<8 | decoded[6] 54 | 55 | # sensor temperature 56 | if (decoded[7] & 0b1000000): 57 | temperature = ((decoded[7] << 8 | decoded[8]) - 0xFFFF)/10 58 | else: 59 | temperature = (decoded[7] << 8 | decoded[8])/10 60 | 61 | # Humidity 62 | humidity = ((decoded[9] << 8 | decoded[10])/10) 63 | 64 | result = { 65 | "battery_value": battery_value, 66 | "work_mode": Work_mode, 67 | "alarm_status": Alarm_status, 68 | "TVOC_ppb": TVOC_ppb, 69 | "CO2_ppm": CO2_ppm, 70 | "temperature": temperature, 71 | "humidity": humidity, 72 | } 73 | elif mode == 31: #mode = 31 #for Test 74 | work_mode="ALARM" 75 | temperature_min= decoded[3]<<24>>24 76 | temperature_max= decoded[4]<<24>>24 77 | humidity_min= decoded[5] 78 | humidity_max= decoded[6] 79 | CO2_min= decoded[7]<<8 | decoded[8] 80 | CO2_max= decoded[9]<<8 | decoded[10] 81 | result = { 82 | "battery_value": battery_value, 83 | "work_mode": work_mode, 84 | "temperature_min": temperature_min, 85 | "temperature_max": temperature_max, 86 | "humidity_min": humidity_min, 87 | "humidity_max": humidity_max, 88 | "CO2_min": CO2_min, 89 | "CO2_max": CO2_max, 90 | } 91 | else: 92 | raise Exception("Invalid Sensor Mode") 93 | return result 94 | 95 | 96 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/dragino_lbt1.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | import json 20 | import logging 21 | 22 | # Setup logging 23 | logger = logging.getLogger() 24 | logger.setLevel(logging.INFO) 25 | 26 | 27 | def convert_bytes_to_uint(input): 28 | print(f"c input is {input} of len {len(input)}") 29 | result = 0 30 | for i in range(0, len(input)): 31 | factor = (i+1) ** 8 32 | print( 33 | f"input[{i}] is {input[i]}, 0x{hex(input[i])}, factor is {factor}") 34 | result += input[i]*factor 35 | return result 36 | 37 | 38 | def to_ascii(input): 39 | bytes_object = bytes.fromhex(input) 40 | ascii_string = bytes_object.decode("ASCII") 41 | return ascii_string 42 | 43 | 44 | def dict_from_payload(base64_input: str, fport: int = None): 45 | """ Decodes a base64-encoded binary payload into JSON. 46 | Parameters 47 | ---------- 48 | base64_input : str 49 | Base64-encoded binary payload 50 | fport: int 51 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 52 | 53 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 54 | 55 | Returns 56 | ------- 57 | JSON object with key/value pairs of decoded attributes 58 | 59 | """ 60 | decoded = base64.b64decode(base64_input) 61 | logger.debug(f"Input hex is {decoded.hex()}") 62 | 63 | battery_value = (decoded[0] << 8 | decoded[1]) / 1000 64 | step_count = ((decoded[2] & 0x0F) << 16) | (decoded[3] << 8) | (decoded[4]) 65 | mode = decoded[5] 66 | 67 | uuid = 0 68 | if mode == 3: 69 | uuid = decoded[6:18] 70 | major = decoded[18:22] 71 | minor = decoded[22:26] 72 | power = decoded[26:28] 73 | rssi = decoded[28:32] 74 | 75 | result = { 76 | "battery_value": battery_value, 77 | "step_count": step_count, 78 | "mode": mode, 79 | "uuid": uuid.decode(), 80 | "major": int(to_ascii(major.hex()), 16), 81 | "minor": int(to_ascii(minor.hex()), 16), 82 | "power": int(to_ascii(power.hex()), 16)-256, 83 | "rssi": int(to_ascii(rssi.hex()), 16) 84 | } 85 | elif mode == 2: 86 | uuid = decoded[6:6+32] 87 | addr = decoded[38:39+12] 88 | result = { 89 | "battery_value": battery_value, 90 | "step_count": step_count, 91 | "mode": mode, 92 | "uuid": uuid.decode(), 93 | "addr": addr.decode() 94 | } 95 | elif mode == 1: 96 | uuid = decoded[6:11] 97 | result = { 98 | "battery_value": battery_value, 99 | "step_count": step_count, 100 | "mode": mode, 101 | "uuid": uuid.decode(), 102 | } 103 | else: 104 | result = { 105 | "battery_value": battery_value, 106 | "step_count": step_count, 107 | "mode": mode, 108 | } 109 | 110 | return result 111 | 112 | 113 | # Tests 114 | def test_uplink_decoding(): 115 | 116 | test_definition = [ 117 | { 118 | 119 | "input_value": "DyAAAAACMDExMjIzMzQ0NTU2Njc3ODg5OUFBQkJDQ0RERUVGRjBFREYwOUM1QjVCNDc=", 120 | "input_encoding": "base64", 121 | "output": {"battery_value": 3.872, "step_count": 0, "mode": 2, "uuid": "0112233445566778899AABBCCDDEEFF0", "addr": "EDF09C5B5B47"} 122 | }, 123 | { 124 | 125 | "input_value": "DxwAAAIDQUJCQ0NEREVFRkYwMjcxMjFGNkFDMy0wNTk=", 126 | "input_encoding": "base64", 127 | "output": {"battery_value": 3.868, "step_count": 2, "mode": 3, "uuid": "ABBCCDDEEFF0", "major": 10002, "minor": 8042, "power": -61, "rssi": -89} 128 | }, 129 | { 130 | 131 | "input_value": "DxQAAAABRUVGRjA=", 132 | "input_encoding": "base64", 133 | "output": {"battery_value": 3.86, "step_count": 0, "mode": 1, "uuid": "EEFF0"} 134 | }, 135 | 136 | 137 | ] 138 | 139 | for testcase in test_definition: 140 | base64_input = None 141 | if testcase.get("input_encoding") == "base64": 142 | base64_input = testcase.get("input_value") 143 | elif testcase.get("input_encoding") == "hex": 144 | base64_input = base64.b64encode( 145 | bytearray.fromhex(testcase.get("input_value"))).decode("utf-8") 146 | 147 | output = dict_from_payload(base64_input) 148 | for key in testcase.get("output"): 149 | assert testcase.get("output").get(key) == output.get(key) 150 | 151 | 152 | if __name__ == "__main__": 153 | test_uplink_decoding() 154 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/dragino_lds01.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str, fport: int = None): 22 | """ Decodes a base64-encoded binary payload into JSON. 23 | Parameters 24 | ---------- 25 | base64_input : str 26 | Base64-encoded binary payload 27 | fport: int 28 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 29 | 30 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 31 | 32 | Returns 33 | ------- 34 | JSON object with key/value pairs of decoded attributes 35 | 36 | """ 37 | 38 | bytes = base64.b64decode(base64_input) 39 | battery = (bytes[0] << 8 | bytes[1]) & 0x3FFF 40 | door_open_status = 0 41 | 42 | if bytes[0] & 0x40: 43 | water_leak_status = 1 44 | 45 | water_leak_status = 0 46 | if bytes[0] & 0x80: 47 | door_open_status = 1 48 | 49 | mod = bytes[2] 50 | 51 | if mod == 1: 52 | open_times = bytes[3] << 16 | bytes[4] << 8 | bytes[5] 53 | open_duration = bytes[6] << 16 | bytes[7] << 8 | bytes[8] 54 | result = { 55 | "mod": mod, 56 | "battery": battery, 57 | "door_open_status": door_open_status, 58 | "open_times": open_times, 59 | "open_duration": open_duration 60 | } 61 | 62 | return result 63 | 64 | if mod == 2: 65 | leak_times = bytes[3] << 16 | bytes[4] << 8 | bytes[5] 66 | leak_duration = bytes[6] << 16 | bytes[7] << 8 | bytes[8] 67 | 68 | result = { 69 | "mod": mod, 70 | "battery": battery, 71 | "leak_times": leak_times, 72 | "leak_duration": leak_duration 73 | } 74 | 75 | return result 76 | 77 | result = { 78 | "battery": battery, 79 | "mod": mod 80 | } 81 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/dragino_lgt92.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | import binascii 20 | 21 | 22 | def dict_from_payload(base64_input: str, fport: int = None): 23 | """ Decodes a base64-encoded binary payload into JSON. 24 | Parameters 25 | ---------- 26 | base64_input : str 27 | Base64-encoded binary payload 28 | fport: int 29 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 30 | 31 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 32 | 33 | Returns 34 | ------- 35 | JSON object with key/value pairs of decoded attributes 36 | 37 | """ 38 | bytes = base64.b64decode(base64_input) 39 | 40 | lat = int.from_bytes(bytes[0:4], byteorder='big', signed=True)/1000000 41 | long = int.from_bytes(bytes[4:8], byteorder='big', signed=True)/1000000 42 | 43 | alarm = (bytes[8] & 0x40) > 0 44 | 45 | battery = ((bytes[8] & 0x3f) << 8 | bytes[9]) / 1000 46 | 47 | fw = 150+(bytes[10] & 0x1f) 48 | result = { 49 | "latitude": lat, 50 | "longitude": long, 51 | "alarm": alarm, 52 | "battery": battery, 53 | "firmware": fw 54 | 55 | } 56 | return result 57 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/dragino_lht65.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str, fport: int = None): 22 | """ Decodes a base64-encoded binary payload into JSON. 23 | Parameters 24 | ---------- 25 | base64_input : str 26 | Base64-encoded binary payload 27 | fport: int 28 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 29 | 30 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 31 | 32 | Returns 33 | ------- 34 | JSON object with key/value pairs of decoded attributes 35 | 36 | """ 37 | 38 | decoded = base64.b64decode(base64_input) 39 | 40 | # Batter status flag 41 | # 00(b): Ultra Low ( BAT <= 2.50v) 42 | # 01(b): Low (2.50v <=BAT <= 2.55v) 43 | # 10(b): OK Good (2.55v <= BAT <=2.65v) 44 | # 11(b): Good (BAT >= 2.65v) 45 | battery_status_flag = (decoded[0] & 0b11000000) >> 6 46 | battery_status = "unknown" 47 | if battery_status_flag == 0b00: 48 | battery_status = "very low" 49 | elif battery_status_flag == 0b01: 50 | battery_status = "low" 51 | elif battery_status_flag == 0b10: 52 | battery_status = "OK" 53 | elif battery_status_flag == 0b11: 54 | battery_status = "Good" 55 | 56 | # Battery voltage 57 | battery_value = ((decoded[0] << 8 | decoded[1]) & 0x3FFF) / 1000 58 | 59 | # Internal sensor temperature 60 | if decoded[2] & 0b1000000: 61 | internal_temperature = ((decoded[2] << 8 | decoded[3]) - 0xFFFF)/100 62 | else: 63 | internal_temperature = (decoded[2] << 8 | decoded[3])/100 64 | 65 | # Humidity 66 | humidity = ((decoded[4] << 8 | decoded[5])/10) 67 | 68 | # External sensor temperature 69 | if decoded[7] & 0b1000000: 70 | external_temperature = ( 71 | ((decoded[7] << 8 | decoded[8]) - 0xFFFF) / 100) 72 | else: 73 | external_temperature = ((decoded[7] << 8 | decoded[8]) / 100) 74 | 75 | result = { 76 | "battery_status": battery_status, 77 | "battery_value": battery_value, 78 | "temperature_internal": internal_temperature, 79 | "humidity": humidity, 80 | "temperature_external": external_temperature, 81 | # "debug": { 82 | # "fport": fport 83 | # } 84 | } 85 | 86 | return result 87 | 88 | 89 | def test_uplink_decoding(): 90 | test_definition = [ 91 | { 92 | "input": "CBF60B0D0376010ADD7FFF", 93 | "output": { 94 | "battery_status": "Good", 95 | "battery_value": 3.062, 96 | "temperature_internal": 28.29, 97 | "humidity": 88.6, 98 | "temperature_external": 27.81 99 | 100 | } 101 | }, 102 | { 103 | "input": "CBBDF5C6022E01F54F7FFF", 104 | "output": { 105 | "battery_status": "Good", 106 | "battery_value": 3.005, 107 | "temperature_internal": -26.17, 108 | "humidity": 55.8, 109 | "temperature_external": -27.36 110 | 111 | } 112 | } 113 | ] 114 | 115 | for test in test_definition: 116 | base64_input = base64.b64encode( 117 | bytearray.fromhex(test.get("input"))).decode("utf-8") 118 | output = dict_from_payload(base64_input) 119 | for key in test.get("output"): 120 | assert test.get("output").get(key) == output.get(key) 121 | 122 | 123 | if __name__ == "__main__": 124 | test_uplink_decoding() 125 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/dragino_llms01.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str, fport: int = None): 22 | """ Decodes a base64-encoded binary payload into JSON. 23 | Parameters 24 | ---------- 25 | base64_input : str 26 | Base64-encoded binary payload 27 | fport: int 28 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 29 | 30 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 31 | 32 | Returns 33 | ------- 34 | JSON object with key/value pairs of decoded attributes 35 | 36 | """ 37 | 38 | 39 | # Used the Dragino LSN50 Decoder as reference 40 | # https://www.dragino.com/downloads/downloads/LoRa_End_Node/LSN50v2-D20/Decoder/LSN50v2-D20-Decoder.txt 41 | decoded = base64.b64decode(base64_input) 42 | 43 | print(decoded) 44 | 45 | battery_value = (((decoded[0] << 8) + decoded[1]) / 1000) # /Battery,units:V 46 | leaf_moisture = (((decoded[4] << 8) | decoded[5]) / 10) 47 | leaf_temp = (((decoded[6] << 8) | decoded[7]) / 10) 48 | 49 | result = { 50 | "battery_value": battery_value, 51 | "leaf_moisture": leaf_moisture, 52 | "leaf_temp": leaf_temp, 53 | } 54 | return result -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/dragino_lse01.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str, fport: int = None): 22 | """ Decodes a base64-encoded binary payload into JSON. 23 | Parameters 24 | ---------- 25 | base64_input : str 26 | Base64-encoded binary payload 27 | fport: int 28 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 29 | 30 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 31 | 32 | Returns 33 | ------- 34 | JSON object with key/value pairs of decoded attributes 35 | 36 | """ 37 | 38 | decoded = base64.b64decode(base64_input) 39 | 40 | value = (decoded[0] << 8 | decoded[1]) & 0x3FFF 41 | battery_value = value # /Battery,units:V 42 | 43 | value = decoded[2] << 8 | decoded[3] 44 | if decoded[2] & 0x80: 45 | value |= 0xFFFF0000 46 | 47 | temp_DS18B20 = (value/10) # /DS18B20,temperature,units:℃ 48 | 49 | value = decoded[4] << 8 | decoded[5] 50 | water_SOIL = (value/100) # /water_SOIL,Humidity,units:% 51 | 52 | value = decoded[6] << 8 | decoded[7] 53 | 54 | if ((value & 0x8000) >> 15) == 0: 55 | temp_SOIL = (value/100) # /temp_SOIL,temperature,units:°C 56 | elif ((value & 0x8000) >> 15) == 1: 57 | temp_SOIL = ((value-0xFFFF)/100) 58 | 59 | value = decoded[8] << 8 | decoded[9] 60 | conduct_SOIL = (value/100) # /conduct_SOIL,conductivity,units:uS/cm 61 | 62 | result = { 63 | "battery_value": battery_value, 64 | "temperature_internal": temp_DS18B20, 65 | "water_soil": water_SOIL, 66 | "temperature_soil": temp_SOIL, 67 | "conduct_soil": conduct_SOIL 68 | 69 | } 70 | return result 71 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/dragino_lsn50.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str, fport: int = None): 22 | """ Decodes a base64-encoded binary payload into JSON. 23 | Parameters 24 | ---------- 25 | base64_input : str 26 | Base64-encoded binary payload 27 | fport: int 28 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 29 | 30 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 31 | 32 | Returns 33 | ------- 34 | JSON object with key/value pairs of decoded attributes 35 | 36 | """ 37 | 38 | # Used the Dragino JS Decoder as reference 39 | # https://www.dragino.com/downloads/downloads/LoRa_End_Node/LLMS01/Decoder/LLMS01_Datacake_Decode_V1.0.0.js 40 | decoded = base64.b64decode(base64_input) 41 | 42 | battery_value = (((decoded[0] << 8) + decoded[1]) / 1000) # /Battery,units:V 43 | temperature1 = (((decoded[2] << 8) + decoded[3]) / 10) 44 | adc = (((decoded[4] << 8) + decoded[5]) / 1000) 45 | istatus = "L" 46 | if (decoded[6] & 0x02): 47 | istatus = "H" 48 | temperature2 = (((decoded[7] <<24>>16) + decoded[8]) / 10) 49 | temperature3 = (((decoded[9] <<24>>16) + decoded[10]) / 10) 50 | 51 | result = { 52 | "battery_value": battery_value, 53 | "adc": adc, 54 | "istatus": istatus, 55 | "temperature1": temperature1, 56 | "temperature2": temperature2, 57 | "temperature3": temperature3, 58 | } 59 | return result 60 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/helpers.py: -------------------------------------------------------------------------------- 1 | def bin32dec(binary): 2 | number = binary & 0xFFFFFFFF 3 | if 0x80000000 & number: 4 | number = -(0x0100000000 - number) 5 | return number 6 | 7 | 8 | def bin16dec(binary): 9 | number = binary & 0xFFFF 10 | if 0x8000 & number: 11 | number = -(0x010000 - number) 12 | return number 13 | 14 | 15 | def bin8dec(binary): 16 | number = binary & 0xFF 17 | if 0x80 & number: 18 | number = -(0x0100 - number) 19 | return number 20 | 21 | 22 | def is_single_bit_set(number): 23 | """ 24 | Return True if number has exactly one bit set to 1; False 25 | if it has any other number of bits set to 1. 26 | """ 27 | # Special case for zero 28 | if number == 0: 29 | return False 30 | return number & (number - 1) == 0 31 | 32 | 33 | def bytes_to_float(decoded, start_index, length) -> float: 34 | """ Decodes two or four bytes of the ByteString to a precise float 35 | The start_index sets the first (two) byte(s) in the decoded payload for the fractional value. 36 | Parameters 37 | ---------- 38 | decoded : ByteString 39 | payload 40 | start_index: int 41 | Index to set the starting point in the ByteString 42 | length: int 43 | Integer value to set the length of bytes which have the needed data for integer and fractional variable 44 | e.g. length=2 -> one byte for fractional and one byte for integer value 45 | e.g. length=4 -> two bytes for fractional and two bytes for integer value 46 | length must be of value 2 or 4 47 | Returns 48 | ------- 49 | float 50 | """ 51 | if length == 2: 52 | # Fractional part 53 | fractional = bin8dec(decoded[start_index]) 54 | # Integer part 55 | integer = bin8dec(decoded[start_index + 1]) 56 | elif length == 4: 57 | # Fractional portion 58 | fractional = bin16dec(decoded[start_index] << 8 | decoded[start_index + 1]) 59 | # Integer portion 60 | integer = bin16dec(decoded[start_index + 2] << 8 | decoded[start_index + 3]) 61 | else: 62 | raise ValueError("Wrong value for parameter length") 63 | 64 | return integer + (fractional / 100) 65 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-html -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/sample_device.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | import time 20 | import math 21 | 22 | 23 | def dict_from_payload(base64_input: str, fport: int = None): 24 | """ Decodes a base64-encoded binary payload into JSON. 25 | Parameters 26 | ---------- 27 | base64_input : str 28 | Base64-encoded binary payload 29 | fport: int 30 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 31 | 32 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 33 | 34 | Returns 35 | ------- 36 | JSON object with key/value pairs of decoded attributes 37 | 38 | """ 39 | 40 | decoded = base64.b64decode(base64_input) 41 | 42 | temperature = round(20 + (math.cos(time.time()/10)*10), 2) 43 | humidity = round(50 + (math.sin(time.time()/10)*10), 2) 44 | 45 | result = { 46 | "temperature": temperature, 47 | "humidity": humidity 48 | } 49 | 50 | return result 51 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/st_nucleo_wl55jc.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | import time 20 | import math 21 | 22 | 23 | def dict_from_payload(base64_input: str, fport: int = None): 24 | """ Decodes a base64-encoded binary payload into JSON. 25 | Parameters 26 | ---------- 27 | base64_input : str 28 | Base64-encoded binary payload 29 | fport: int 30 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 31 | 32 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 33 | 34 | Returns 35 | ------- 36 | JSON object with key/value pairs of decoded attributes 37 | 38 | """ 39 | 40 | decoded = base64.b64decode(base64_input) 41 | 42 | led = "Off" if decoded[0] == 0 else "On" 43 | pressure = int.from_bytes(decoded[1:3], byteorder='big', signed=False) / 10 44 | temperature = int.from_bytes(decoded[3:4], byteorder='big', signed=True) 45 | humidity = int.from_bytes(decoded[4:6], byteorder='big', signed=False) / 10 46 | 47 | result = { 48 | "led": led, 49 | "pressure": pressure, 50 | "temperature": temperature, 51 | "humidity": humidity 52 | } 53 | 54 | return result 55 | -------------------------------------------------------------------------------- /transform_binary_payload/src-payload-decoders/python/tabs_temphumsensor.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | # Payload as described in Reference Manual (TBHH100) 18 | 19 | ## Byte 0 20 | # Sensors status 21 | # Bits [7:0] 22 | # - 0x00: VOC sensor 23 | # - 0x08: Temperature and humidity sensor 24 | 25 | ## Byte 1 26 | # Battery level 27 | # Bits [3:0] unsigned value ν, range 1 – 14; battery voltage in V = (25 + ν) ÷ 10. 28 | # Bits [7:4] RFU 29 | 30 | ## Byte 2 31 | # Temperature as measured by sensor 32 | # Bits [6:0] unsigned value τ, range 0 – 127; temperature in °C = τ - 32. 33 | # Bit [7] RFU 34 | 35 | ## Byte 3 36 | # Relative Humidity as measured by sensor 37 | # Bits [6:0] unsigned value in %, range 0-100. 38 | # A value of 127 indicates measurement error. 39 | # Bit [7] RFU 40 | 41 | ## Byte 4-5 42 | # CO2 43 | # Bits [15:0] 44 | # Always 0xffff because module has no CO2 sensor 45 | 46 | ## Byte 6-7 47 | # VOC 48 | # Bits [15:0] 49 | # Always 0xffff because module has no VOC sensor 50 | 51 | import base64 52 | import json 53 | 54 | DEBUG_OUTPUT = False 55 | 56 | 57 | def dict_from_payload(base64_input: str, fport: int = None): 58 | """ Decodes a base64-encoded binary payload into JSON. 59 | Parameters 60 | ---------- 61 | base64_input : str 62 | Base64-encoded binary payload 63 | fport: int 64 | FPort as provided in the metadata. Please note the fport is optional and can have value "None", if not provided by the LNS or invoking function. 65 | 66 | If fport is None and binary decoder can not proceed because of that, it should should raise an exception. 67 | 68 | Returns 69 | ------- 70 | JSON object with key/value pairs of decoded attributes 71 | 72 | """ 73 | decoded = base64.b64decode(base64_input) 74 | 75 | if DEBUG_OUTPUT: 76 | print(f"Input: {decoded.hex().upper()}") 77 | 78 | # Byte 1 79 | if (decoded[0] == 0x00): 80 | status_sensor_type = 'VOC' 81 | elif (decoded[0] == 0x08): 82 | status_sensor_type = 'TempHum' 83 | else: 84 | status_sensor_type = 'Unknown' 85 | 86 | # Byte 2 87 | battery = (25 + (decoded[1] & 0b00001111))/10 88 | 89 | # Byte 3 90 | temp = decoded[2] & 0b01111111 - 32 91 | 92 | # Byte 4 - relative humidity 93 | RH = int(decoded[3]) 94 | 95 | # Bytes 5-6 CO2 96 | # is always 0xffff 97 | # Bytes 7-8 VOC 98 | # is always 0xffff 99 | 100 | # Output 101 | result = { 102 | "status_sensor_type": status_sensor_type, 103 | "battery_value": battery, 104 | "temp": temp, 105 | "RH": RH 106 | } 107 | 108 | if DEBUG_OUTPUT: 109 | print(f"Output: {json.dumps(result,indent=2)}") 110 | 111 | return result 112 | 113 | 114 | # Tests 115 | if __name__ == "__main__": 116 | test_definition = [ 117 | { 118 | "input_encoding": "base64", 119 | "input_value": "CAs1Mv////8=", 120 | "output": { 121 | "status_sensor_type": 'TempHum', 122 | "battery_value": 3.6, 123 | "temp": 21, 124 | "RH": 50 125 | } 126 | } 127 | ] 128 | 129 | for testcase in test_definition: 130 | base64_input = None 131 | if testcase.get("input_encoding") == "base64": 132 | base64_input = testcase.get("input_value") 133 | elif testcase.get("input_encoding") == "hex": 134 | base64_input = base64.b64encode( 135 | bytearray.fromhex(testcase.get("input_value"))).decode("utf-8") 136 | output = dict_from_payload(base64_input) 137 | for key in testcase.get("output"): 138 | if testcase.get("output").get(key) != output.get(key): 139 | raise Exception( 140 | f'Assertion failed for input {testcase.get("input_value")}, key {key}, expected {testcase.get("output").get(key)}, got {output.get(key)}') 141 | else: 142 | print( 143 | f'"{testcase.get("input_value")}" : Successfull test for key "{key}", value "{testcase.get("output").get(key)}"') 144 | -------------------------------------------------------------------------------- /transform_binary_payload_pilot_things/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 245 | 246 | 247 | # AWS 248 | .aws-sam 249 | /*.toml 250 | 251 | # Custom 252 | .idea 253 | -------------------------------------------------------------------------------- /transform_binary_payload_pilot_things/images/0000-resized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/transform_binary_payload_pilot_things/images/0000-resized.png -------------------------------------------------------------------------------- /transform_binary_payload_pilot_things/src-iotrule-transformation-nodejs/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Pilot Things. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | // software and associated documentation files (the "Software"), to deal in the Software 6 | // without restriction, including without limitation the rights to use, copy, modify, 7 | // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | // permit persons to whom the Software is furnished to do so. 9 | 10 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | const fetch = require("node-fetch") 18 | const VALID_PRODUCT_ID_REGEX = /^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/; 19 | 20 | exports.handler = async function (event, context) { 21 | /* Transforms a binary payload by the Pilot Things decoding service. 22 | Parameters 23 | ---------- 24 | event.PayloadData : str (obligatory parameter) 25 | Base64 encoded input payload 26 | 27 | event.PayloadDecoderProductId : string (obligatory parameter) 28 | The value of this attribute defines the GUID of the decoder which will be used by the Pilot Things decoding service. 29 | 30 | 31 | Returns 32 | ------- 33 | This function returns a JSON object with the following keys: 34 | 35 | - status: 200 or 500 36 | - product_id: value of input parameter event.PayloadDecoderProductId 37 | - transformed_payload: output of Pilot Things decoding service (only if status == 200) 38 | - error_message (only if status == 500) 39 | 40 | */ 41 | 42 | 43 | console.log('## EVENT: ' + JSON.stringify(event)) 44 | 45 | // Read input parameters 46 | const input_base64 = event.PayloadData 47 | const product_id = event.PayloadDecoderProductId 48 | const api_key = process.env.PILOT_THINGS_SERVICE_API_KEY 49 | 50 | // Check if product ID mathes the regex 51 | if (!product_id.match(VALID_PRODUCT_ID_REGEX)) { 52 | return { 53 | "status": 500, 54 | "errorMessage": "PayloadDecoderProductId must be a GUID", 55 | "product_id": product_id 56 | } 57 | } 58 | 59 | // Logging 60 | console.log("Decoding payload " + input_base64 + " using product " + product_id) 61 | 62 | 63 | // Convert base64 payload into hex string 64 | const input_hex = Buffer.from(input_base64, 'base64').toString("hex") 65 | const fetchResult = await fetch("https://sensor-library.pilot-things.net/decode", { 66 | method: "POST", 67 | headers: { 68 | "x-api-key": api_key 69 | }, 70 | body: JSON.stringify({ 71 | productId: product_id, 72 | payload: input_hex 73 | }) 74 | }) 75 | 76 | if (fetchResult.ok) { 77 | const result = { 78 | ...await fetchResult.json(), 79 | status: 200, 80 | product_id: product_id 81 | }; 82 | 83 | console.log("Returning result " + JSON.stringify(result)) 84 | return result; 85 | } else { 86 | const error = "Decoding service failed with error code " + fetchResult.status + " and body " + await fetchResult.json(); 87 | console.log(error); 88 | 89 | return { 90 | "status": 500, 91 | "errorMessage": error, 92 | "product_id": product_id 93 | } 94 | } 95 | } 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /transform_binary_payload_pilot_things/src-iotrule-transformation-nodejs/index.test.js: -------------------------------------------------------------------------------- 1 | const lambda = require('./index') 2 | 3 | test_definition = [ 4 | { 5 | "description": "Test 1", 6 | "input": "F2XUdQ4=", 7 | "PayloadDecoderProductId": "69714577-5c18-4931-866a-1026b55c603d", 8 | "expected_status": 200, 9 | "expected_output": { 10 | "temperature": 23.05, 11 | "humidity": 51.16 12 | } 13 | }, 14 | { 15 | "description": "Test 2", 16 | "input": "F2XUdQ4=", 17 | "PayloadDecoderProductId": "not a guid", 18 | "expected_status": 500, 19 | "expected_error_message": "PayloadDecoderProductId must be a GUID" 20 | }, 21 | 22 | { 23 | "description": "Test 3", 24 | "input": "F2XUdQ4=", 25 | "PayloadDecoderProductId": "69714577-5c18-4931-866a-1026b55c603z", 26 | "expected_status": 500, 27 | "expected_error_message": "PayloadDecoderProductId must be a GUID" 28 | } 29 | 30 | 31 | ] 32 | 33 | 34 | for (var i = 0; i < test_definition.length; i++) { 35 | 36 | test_event = { 37 | "PayloadData": test_definition[i].input, 38 | "PayloadDecoderProductId": test_definition[i].PayloadDecoderProductId 39 | } 40 | 41 | console.log(test_event) 42 | console.log("Running test " + test_definition[i].description) 43 | 44 | async function app(test_definition, i) { 45 | 46 | var actual_output = await lambda.handler(test_event, {}) 47 | 48 | // console.log("Binary decoding output=" + JSON.stringify(actual_output, null, " ")) 49 | console.log("---------" + test_definition[i].description + "--------------------------") 50 | if (actual_output.status != test_definition[i].expected_status) { 51 | throw ("ERROR: status " + actual_output.status + " received, but " + test_definition[i].expected_status + " expected") 52 | } else { 53 | console.log("OK: status code " + actual_output.status) 54 | } 55 | 56 | if (actual_output.status != 200) { 57 | if (actual_output.errorMessage != test_definition[i].expected_error_message) { 58 | throw ("ERROR: error message '" + actual_output.errorMessage + "' received, but '" + test_definition[i].expected_error_message + "' expected") 59 | } else { 60 | console.log("OK: error message " + actual_output.errorMessage) 61 | } 62 | } 63 | 64 | for (var key in test_definition[i].expected_output) { 65 | if (test_definition[i].expected_output[key] == null) { 66 | console.log("ERROR: attribute " + key + " is undefined in test definition") 67 | throw "ERROR: attribute " + key + " is undefined in test definition"; 68 | } 69 | if (test_definition[i].expected_output[key] == actual_output.transformed_payload[key]) { 70 | console.log("OK: attribute " + key + " has an expected value of " + actual_output.transformed_payload[key]) 71 | } else { 72 | console.log("ERROR: for attribute " + key + ": expected " + test_definition[i].expected_output[key] + " but received " + actual_output.transformed_payload[key] + ". Dump of actual output:" + JSON.stringify(actual_output) + ", dump of expected output: " + JSON.stringify(test_definition[i].expected_output)) 73 | } 74 | 75 | } 76 | } 77 | 78 | 79 | app(test_definition, i) 80 | 81 | 82 | } 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /transform_binary_payload_pilot_things/src-iotrule-transformation-nodejs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src-lambda-transform-nodejs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "data-uri-to-buffer": { 8 | "version": "4.0.0", 9 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz", 10 | "integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==" 11 | }, 12 | "fetch-blob": { 13 | "version": "3.2.0", 14 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 15 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 16 | "requires": { 17 | "node-domexception": "^1.0.0", 18 | "web-streams-polyfill": "^3.0.3" 19 | } 20 | }, 21 | "formdata-polyfill": { 22 | "version": "4.0.10", 23 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 24 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 25 | "requires": { 26 | "fetch-blob": "^3.1.2" 27 | } 28 | }, 29 | "node-domexception": { 30 | "version": "1.0.0", 31 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 32 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" 33 | }, 34 | "node-fetch": { 35 | "version": "3.2.10", 36 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz", 37 | "integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==", 38 | "requires": { 39 | "data-uri-to-buffer": "^4.0.0", 40 | "fetch-blob": "^3.1.4", 41 | "formdata-polyfill": "^4.0.10" 42 | } 43 | }, 44 | "web-streams-polyfill": { 45 | "version": "3.2.1", 46 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 47 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /transform_binary_payload_pilot_things/src-iotrule-transformation-nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src-lambda-transform-nodejs", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "node-fetch": "^3.2.10" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /transform_binary_payload_pilot_things/src-iotrule-transformation/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Pilot Things. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import json 19 | import logging 20 | import re 21 | import requests 22 | import base64 23 | import os 24 | 25 | # Function name for logging 26 | FUNCTION_NAME = "ConvertBinaryPayload" 27 | 28 | # Setup logging 29 | logger = logging.getLogger(FUNCTION_NAME) 30 | logger.setLevel(logging.INFO) 31 | 32 | # Define exception to be raised if input is lacking or invalid 33 | class InvalidInputException(Exception): 34 | pass 35 | 36 | # Define regex used to pre-validate product ID 37 | VALID_PRODUCT_ID_REGEX = re.compile("^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$") 38 | 39 | def lambda_handler(event, context): 40 | """ Transforms a binary payload by the Pilot Things decoding service. 41 | Parameters 42 | ---------- 43 | PayloadData : str (obligatory parameter) 44 | Base64 encoded input payload 45 | 46 | PayloadDecoderProductId : str (obligatory parameter) 47 | The value of this attribute defines the GUID of the decoder which will be used by the Pilot Things decoding service. 48 | 49 | Returns 50 | ------- 51 | This function returns a JSON object with the following keys: 52 | 53 | - status: 200 or 500 54 | - transformed_payload: output of Pilot Things decoding service (only if status == 200) 55 | - error_type (only if status == 500) 56 | - error_message (only if status == 500) 57 | - stackTrace (only if status == 500) 58 | 59 | 60 | """ 61 | logger.info("Received event: %s" % json.dumps(event)) 62 | 63 | # Store event input and perform input validation 64 | input_base64 = event.get("PayloadData") 65 | product_id = event.get("PayloadDecoderProductId") 66 | api_key = os.environ["PILOT_THINGS_SERVICE_API_KEY"] 67 | 68 | # Validate existence of payload type 69 | if product_id is None: 70 | raise InvalidInputException("PayloadDecoderProductId is not specified") 71 | 72 | # Validate if payload type is in the list of allowed values 73 | if not VALID_PRODUCT_ID_REGEX.match(product_id): 74 | raise InvalidInputException("PayloadDecoderProductId must be a GUID") 75 | 76 | logger.info(f"Base64 input={input_base64}, Product ID={product_id}") 77 | 78 | # Convert Base64 to a hexadecimal string 79 | input_hex = base64.b64decode(input_base64).hex() 80 | 81 | # Invoke the decoding service and return a result 82 | r = requests.post("https://sensor-library.pilot-things.net/decode", headers={'x-api-key': api_key}, json={ 83 | 'productId': product_id, 84 | 'payload': input_hex 85 | }) 86 | # Check for errors 87 | r.raise_for_status() 88 | 89 | result = r.json() 90 | result["status"] = 200 91 | result["product_id"] = product_id 92 | logger.info(result) 93 | return result 94 | -------------------------------------------------------------------------------- /transform_binary_payload_pilot_things/src-iotrule-transformation/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/README.md: -------------------------------------------------------------------------------- 1 | # Monitoring and notifications for LoRaWAN device connection status 2 | 3 | This sample contains an example solution for monitoring connectivity status of LoRaWAN devices. For example, imagine you have a temperature sensor which typically sends telemetry once an hour. You want to be notified if no telemetry from the sensor arrived after 60 minutrs, e.g, due to connectivity issues or sensor malfunction. 4 | 5 | After deploying and configuring this solution in your AWS account, you will receive an e-mail message each time one of configured LoRaWAN devices is not sending uplink for longer then specified period of time. Additionaly, a message will be published to AWS IoT Core message broker MQTT topic (`awsiotcorelorawan/events/presence/missingheartbeat/+`). 6 | 7 | 8 | ## Solution Architecture 9 | 10 | Below you see a diagram with the solution architecture: 11 | ![Solution Architecture](images/ioteventarch.png) 12 | 13 | For AWS IoT Events, the following detector model (i.e., the state machine) will be deployed: 14 | ![IoT Events state machine](images/ioteventsdetectormodel.png) 15 | 16 | ## Quick deployment 17 | 18 | Please run the following commands in your local shell or in AWS CloudShell. 19 | 20 | ### **1. Deploy the CDK stack** 21 | 22 | ``` shell 23 | # Clone the repository 24 | git clone https://github.com/aws-samples/aws-iot-core-lorawan 25 | cd aws-iot-core-lorawan/device_watchdog 26 | # Set up and activate virtual environment 27 | python3 -m venv .env 28 | source .env/bin/activate 29 | # Install AWS CDK and neccessary CDK libraries 30 | npm install -g aws-cdk 31 | pip3 install -r requirements.txt 32 | # If first time running CDK deployment in this account / region, run CDK bootstap 33 | # This is a one-time activity per account/region, e.g. 34 | # cdk bootstrap aws://123456789/us-east-1 35 | cdk bootstrap aws:/// 36 | # Deploy the stack. Ensure to replace with the E-Mail adresss to send notifications to. 37 | cdk deploy --parameters emailforalarms= --parameters notifyifinactivseconds=60 38 | ``` 39 | 40 | Please note, that for simplicity of testing the threshold for notification of missing uplink is set to 60 seconds. 41 | 42 | ### **2. Confirm the SNS e-mail subscription** 43 | 44 | Please check your mailbox for an e-mail message with subject "AWS Notification - Subscription Confirmation" and confirm the subscription. 45 | 46 | 47 | ### **3. Review sample AWS IoT Rule** 48 | In the AWS IoT management console, please select Act, Rules and click on the rule ["LoRaWANDeviceHeartbeatWatchdogSampleRule"](https://console.aws.amazon.com/iot/home?#/rule/LoRaWANDeviceHeartbeatWatchdogSampleRule). 49 | 50 | ![IoT Rule](images/iotrule.png) 51 | 52 | To implement monitoring and notifications for your LoRaWAN devices, you will need to add an "Send a message to an IoT Events Input" action targeting input `LoRaWANDeviceWatchdogInput` to the respective AWS IoT Rules. 53 | 54 | ### **4. Perform a test** 55 | 56 | **Start MQTT Test client** 57 | Please open the [MQTT Test client](https://console.aws.amazon.com/iot/home?region=#/test) and subscribe to the topics `awsiotcorelorawan/events/uplink` and `awsiotcorelorawan/events/presence/missingheartbeat/+`. 58 | 59 | **Publish test payload** 60 | Please publish the following payload on the MQTT topic `LoRaWANDeviceHeartbeatWatchdogSampleRule_sampletopic`: 61 | 62 | ```json 63 | { 64 | "WirelessDeviceId": "257bb7a6-b063-4fc4-af23-6ba5c5638f88" 65 | } 66 | ``` 67 | 68 | 69 | As an alternative to using AWS IoT MQTT Test client, you can also invoke the following command using AWS CLI: 70 | ```shell 71 | aws iot-data publish --topic LoRaWANDeviceHeartbeatWatchdogSampleRule_sampletopic --payload eyJXaXJlbGVzc0RldmljZUlkIjoiMjU3YmI3YTYtYjA2My00ZmM0LWFmMjMtNmJhNWM1NjM4Zjg4In0K 72 | ``` 73 | 74 | **View notifications** 75 | 76 | Immediately after an invocation of `aws iot-data publish`, you should see an MQTT message published on `awsiotcorelorawan/events/uplink`: 77 | 78 | ![MQTT Client](images/mqttclient1.png) 79 | 80 | After duration configured during the stack deployment, you should see an MQTT message published on `awsiotcorelorawan/events/presence/missingheartbeat/+`: 81 | 82 | ![MQTT Client](images/mqttclient2.png) 83 | 84 | 85 | ### **5.Remove the stack** 86 | 87 | ``` 88 | cd aws-iot-core-lorawan/device_watchdog 89 | cdk destroy 90 | ``` 91 | 92 | 93 | ## Troubleshooting 94 | 95 | ### View AWS IoT Events logs 96 | 97 | 1. Open AWS IoT Events settings [here](https://console.aws.amazon.com/iotevents/home?region=#/settings/logging) 98 | 2. Configure Logging on "DEBUG" level with a debug target as detector model `LoRaWANGatewayConnectivityModel` 99 | 3. View the CloudWatch logs for potentially helpful error messages from AWS IoT Events 100 | 101 | -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: MIT-0 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 7 | # software and associated documentation files (the "Software"), to deal in the Software 8 | # without restriction, including without limitation the rights to use, copy, modify, 9 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so. 11 | # 12 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 14 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 15 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 16 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 17 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | from aws_cdk import core 20 | 21 | from cdkstack.lorawan_connectivity_watchdog_stack import LorawanConnectivityWatchdogStack 22 | 23 | app = core.App() 24 | LorawanConnectivityWatchdogStack(app, "LoRaWANDeviceWatchdogStack") 25 | app.synth() 26 | -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "context": { 4 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 5 | "@aws-cdk/core:enableStackNameDuplicates": "true", 6 | "aws-cdk:enableDiffNoFail": "true", 7 | "@aws-cdk/core:stackRelativeExports": "true", 8 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true, 9 | "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true, 10 | "@aws-cdk/aws-kms:defaultKeyPolicies": true, 11 | "@aws-cdk/aws-s3:grantWriteWithoutAcl": true, 12 | "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true, 13 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 14 | "@aws-cdk/aws-efs:defaultEncryptionAtRest": true, 15 | "@aws-cdk/aws-lambda:recognizeVersionProps": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/cdkstack/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/workinprogress_dontuse/device_watchdog/cdkstack/__init__.py -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/images/ioteventarch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/workinprogress_dontuse/device_watchdog/images/ioteventarch.png -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/images/ioteventsdetectormodel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/workinprogress_dontuse/device_watchdog/images/ioteventsdetectormodel.png -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/images/iotrule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/workinprogress_dontuse/device_watchdog/images/iotrule.png -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/images/mqttclient1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/workinprogress_dontuse/device_watchdog/images/mqttclient1.png -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/images/mqttclient2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/workinprogress_dontuse/device_watchdog/images/mqttclient2.png -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/localtools/prepare.sh: -------------------------------------------------------------------------------- 1 | isengard assume svirida+iotcorelorawan@amazon.de --nocache 2 | export AWS_DEFAULT_REGION=eu-west-1 3 | source .venv/bin/activate -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/out.yaml: -------------------------------------------------------------------------------- 1 | arn:aws:cloudformation:us-east-1:614797420359:stack/LoRaWANDeviceWatchdogStack/dfcac810-259b-11ec-8272-12d8f82e2857 2 | -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | aws_cdk.core 3 | aws_cdk.aws_iam 4 | aws_cdk.aws_iotevents 5 | aws_cdk.aws_iot 6 | aws_cdk.aws_sns 7 | aws_cdk.aws_sns_subscriptions 8 | -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | with open("README.md") as fp: 5 | long_description = fp.read() 6 | 7 | 8 | setuptools.setup( 9 | name="lorawan_device_watchdog", 10 | version="0.0.1", 11 | 12 | description="Monitoring and notifications for LoRaWAN device status", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | 16 | author="Andrei Svirida", 17 | 18 | package_dir={"": "cdkstack"}, 19 | packages=setuptools.find_packages(where="cdkstack"), 20 | 21 | install_requires=[ 22 | "aws-cdk.core>=1.109.0", 23 | ], 24 | 25 | python_requires=">=3.6", 26 | 27 | classifiers=[ 28 | "Development Status :: 4 - Beta", 29 | 30 | "Intended Audience :: Developers", 31 | 32 | "Programming Language :: JavaScript", 33 | "Programming Language :: Python :: 3 :: Only", 34 | "Programming Language :: Python :: 3.6", 35 | "Programming Language :: Python :: 3.7", 36 | "Programming Language :: Python :: 3.8", 37 | 38 | "Topic :: Software Development :: Code Generators", 39 | "Topic :: Utilities", 40 | 41 | "Typing :: Typed", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/tests/event.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "messages": [ 4 | { 5 | "inputName": "LoRaWANDeviceWatchdogInput", 6 | "messageId": "5f5b52ee-eb41-4450-af0f-f507ed46da7a", 7 | "payload": "eyJkZXZpY2VpZCI6IjEiLCJ0aW1lc3RhbXBfbXMiOjEyMzR9" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /workinprogress_dontuse/device_watchdog/tests/publish.sh: -------------------------------------------------------------------------------- 1 | aws iotevents-data batch-put-message \ 2 | --cli-input-json file://event.json 3 | 4 | 5 | # {"deviceid":"1","timestamp_ms":1234} eyJkZXZpY2VpZCI6IjEiLCJ0aW1lc3RhbXBfbXMiOjEyMzR9 6 | # Run this command to generate payload: 7 | # echo "{"deviceid":"126","timestamp_ms":1234}" | base64 - -------------------------------------------------------------------------------- /workshop/binarydecoder/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode -------------------------------------------------------------------------------- /workshop/binarydecoder/README.md: -------------------------------------------------------------------------------- 1 | # Sample for workshop "How to build a binary decoder for AWS IoT Core for LoRaWAN" 2 | 3 | ## Prerequisites 4 | 5 | The sample requires SAM CLI, you can find installation instructions [here](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). 6 | 7 | ## Deployment 8 | 9 | Please perform the following steps to deploy a sample application: 10 | 11 | 1. Check out this repository on your computer 12 | 13 | ```shell 14 | git clone https://github.com/aws-samples/aws-iot-core-lorawan --branch experimental 15 | cd workshop/binarydecoder 16 | ``` 17 | 18 | 2. Please review the SAM template [template.yaml](template.yaml) to understand the ressources that will be created in your AWS accoumt. 19 | 20 | 3. This sample uses [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/index.html) to build and deploy all necessary resources (e.g. AWS Lambda function, AWS IoT Rule, AWS IAM Roles) to your AWS account. Please perform the following commands to build the SAM artifacts: 21 | 22 | ```shell 23 | sam build 24 | ``` 25 | 26 | 4. Deploy the SAM template to your AWS account. 27 | 28 | ```shell 29 | sam deploy --guided 30 | ``` 31 | 32 | You can use the default parameters. 33 | 34 | Please note that `sam deploy --guided` should be only executed for a first deployment. To redeploy after that please use `sam deploy`. 35 | 36 | -------------------------------------------------------------------------------- /workshop/binarydecoder/events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "PayloadData": "DRoAAAAABNYBLAA=", 3 | "DeviceId": "11a9fdf8-e7ac-406f-9517-df4807603959", 4 | "ApplicationId": 2, 5 | "Metadata": { 6 | "LoRaWAN": { 7 | "DataRate": 0, 8 | "DevEUI": "a84041b3618248f7", 9 | "FPort": 2, 10 | "Frequency": 867100000, 11 | "Gateways": [ 12 | { 13 | "GatewayEUI": "80029cfffe5cf1f3", 14 | "RSSI": -31, 15 | "SNR": 10.75 16 | } 17 | ], 18 | "Timestamp": "2020-10-15T16:12:55Z" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /workshop/binarydecoder/src/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | import json 18 | import traceback 19 | import logging 20 | import sys 21 | from time import time 22 | 23 | 24 | import dragino_lht65 25 | 26 | # Setup logging 27 | logger = logging.getLogger("PayloadDecoder") 28 | logger.setLevel(logging.INFO) 29 | 30 | 31 | def lambda_handler(event, context): 32 | """ Transforms a binary payload by invoking "decode_{event.type}" function 33 | Parameters 34 | ---------- 35 | DeviceId : str 36 | Device Id 37 | ApplicationId : int 38 | LoRaWAN Application Id / Port number 39 | PayloadData : str 40 | Base64 encoded input payload 41 | 42 | 43 | Returns 44 | ------- 45 | This function returns a JSON object with the following keys: 46 | 47 | - status: 200 or 500 48 | - transformed_payload: result of calling "{PayloadDecoderName}.dict_from_payload" (only if status == 200) 49 | - lns_payload: a representation of payload as received from an LNS 50 | - error_type (only if status == 500) 51 | - error_message (only if status == 500) 52 | - stackTrace (only if status == 500) 53 | 54 | 55 | """ 56 | 57 | logger.info("Received event: %s" % json.dumps(event)) 58 | 59 | input_base64 = event.get("PayloadData") 60 | device_id = event.get("WirelessDeviceId") 61 | metadata = event.get("WirelessMetadata")["LoRaWAN"] 62 | 63 | try: 64 | 65 | # Invoke a payload conversion function 66 | decoded_payload = dragino_lht65.dict_from_payload( 67 | event.get("PayloadData")) 68 | 69 | # Define the output of AWS Lambda function in case of successful decoding 70 | result = { 71 | "status": 200, 72 | "LNSData": { 73 | "PayloadData": input_base64, 74 | "WirelessDeviceId": device_id, 75 | "WirelessMetadata": {"LoRaWAN": metadata} 76 | }, 77 | "TransformedPayloadData": decoded_payload 78 | } 79 | logger.info(result) 80 | return result 81 | 82 | except Exception as exp: 83 | 84 | logger.error(f"Exception {exp} during binary decoding") 85 | 86 | raise exp 87 | -------------------------------------------------------------------------------- /workshop/binarydecoder/src/dragino_lht65.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str): 22 | 23 | decoded = base64.b64decode(base64_input) 24 | 25 | # Batter status flag 26 | # 00(b): Ultra Low ( BAT <= 2.50v) 27 | # 01(b): Low (2.50v <=BAT <= 2.55v) 28 | # 10(b): OK Good (2.55v <= BAT <=2.65v) 29 | # 11(b): Good (BAT >= 2.65v) 30 | battery_status_flag = (decoded[0] & 0b11000000) >> 6 31 | battery_status = "unknown" 32 | if battery_status_flag == 0b00: 33 | battery_status = "very low" 34 | elif battery_status_flag == 0b01: 35 | battery_status = "low" 36 | elif battery_status_flag == 0b10: 37 | battery_status = "OK" 38 | elif battery_status_flag == 0b11: 39 | battery_status = "Good" 40 | 41 | # Battery voltage 42 | battery_value = ((decoded[0] << 8 | decoded[1]) & 0x3FFF) / 1000 43 | 44 | # Internal sensor temperature 45 | if (decoded[2] & 0b1000000): 46 | internal_temperature = ((decoded[2] << 8 | decoded[3]) - 0xFFFF)/100 47 | else: 48 | internal_temperature = (decoded[2] << 8 | decoded[3])/100 49 | 50 | # Humidity 51 | humidity = ((decoded[4] << 8 | decoded[5])/10) 52 | 53 | # External sensor temperature 54 | if (decoded[7] & 0b1000000): 55 | external_temperature = ( 56 | ((decoded[7] << 8 | decoded[8]) - 0xFFFF) / 100) 57 | else: 58 | external_temperature = ((decoded[7] << 8 | decoded[8]) / 100) 59 | 60 | result = { 61 | "battery_status": battery_status, 62 | "battery_value": battery_value, 63 | "temperature_internal": internal_temperature, 64 | "humidity": humidity, 65 | "temperature_external": external_temperature 66 | } 67 | 68 | return result 69 | 70 | 71 | # Tests 72 | if __name__ == "__main__": 73 | test_definition = [ 74 | { 75 | "input": "CBF60B0D0376010ADD7FFF", 76 | "output": { 77 | "battery_status": "Good", 78 | "battery_value": 3.062, 79 | "temperature_internal": 28.29, 80 | "humidity": 88.6, 81 | "temperature_external": 27.81 82 | 83 | } 84 | }, 85 | { 86 | "input": "CBBDF5C6022E01F54F7FFF", 87 | "output": { 88 | "battery_status": "Good", 89 | "battery_value": 3.005, 90 | "temperature_internal": -26.17, 91 | "humidity": 55.8, 92 | "temperature_external": -27.36 93 | 94 | } 95 | } 96 | ] 97 | 98 | for test in test_definition: 99 | base64_input = base64.b64encode( 100 | bytearray.fromhex(test.get("input"))).decode("utf-8") 101 | output = dict_from_payload(base64_input) 102 | for key in test.get("output"): 103 | if(test.get("output").get(key) != output.get(key)): 104 | raise Exception( 105 | f'Assertion failed for input {test.get("input")}, key {key}, expected {test.get("output").get(key)}, got {output.get(key)} ') 106 | -------------------------------------------------------------------------------- /workshop/binarydecoder/src/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/workshop/binarydecoder/src/requirements.txt -------------------------------------------------------------------------------- /workshop/binarydecoder/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | Sample minimalistic binary decoder for AWS IoT Core for LoRaWAN 5 | 6 | Parameters: 7 | TopicError: 8 | Type: String 9 | Default: workshop/error 10 | Description: Name of MQTT topic for to publish IoT Rule action error messages 11 | 12 | TopicDebug: 13 | Type: String 14 | Default: workshop/debug 15 | Description: | 16 | Prefix of a MQTT topic to publish debugging information 17 | 18 | Resources: 19 | ############################################################################################ 20 | # Payload transformation Lambda function to be called from AWS IoT Core. This function will refer to 21 | # layer "DecoderLayer" to include the necessary decoding libraries 22 | ############################################################################################ 23 | 24 | TransformLoRaWANBinaryPayloadFunction: 25 | Type: AWS::Serverless::Function 26 | Name: !Sub "${AWS::StackName}-BinaryDecoderTutorialFunction" 27 | Properties: 28 | CodeUri: src 29 | Handler: app.lambda_handler 30 | Runtime: python3.7 31 | Timeout: 10 32 | 33 | TransformLoRaWANBinaryPayloadFunctionPermission: 34 | Type: AWS::Lambda::Permission 35 | Properties: 36 | FunctionName: !GetAtt TransformLoRaWANBinaryPayloadFunction.Arn 37 | Action: lambda:InvokeFunction 38 | Principal: iot.amazonaws.com 39 | 40 | TransformLoRaWANBinaryPayloadRule: 41 | Type: "AWS::IoT::TopicRule" 42 | Properties: 43 | RuleName: !Sub "${AWS::StackName}_TransformLoRaWANBinaryPayloadRule_dragino_lht65" 44 | TopicRulePayload: 45 | AwsIotSqlVersion: "2016-03-23" 46 | RuleDisabled: false 47 | 48 | Sql: !Sub 49 | - | 50 | SELECT aws_lambda("${LambdaARN}", 51 | { 52 | "PayloadData":PayloadData, 53 | "WirelessDeviceId": WirelessDeviceId, 54 | "WirelessMetadata": WirelessMetadata 55 | } 56 | ) as transformationresult 57 | - { LambdaARN: !GetAtt TransformLoRaWANBinaryPayloadFunction.Arn } 58 | Actions: 59 | - Republish: 60 | RoleArn: !GetAtt TransformLoRaWANBinaryPayloadRuleRole.Arn 61 | Topic: !Ref TopicDebug 62 | Qos: 0 63 | 64 | ErrorAction: 65 | Republish: 66 | RoleArn: !GetAtt TransformLoRaWANBinaryPayloadRuleRole.Arn 67 | Topic: !Ref TopicError 68 | Qos: 0 69 | 70 | TransformLoRaWANBinaryPayloadRuleRole: 71 | Type: "AWS::IAM::Role" 72 | Properties: 73 | AssumeRolePolicyDocument: 74 | Version: 2012-10-17 75 | Statement: 76 | - Effect: Allow 77 | Principal: 78 | Service: 79 | - iot.amazonaws.com 80 | Action: 81 | - "sts:AssumeRole" 82 | Policies: 83 | - PolicyName: root 84 | PolicyDocument: 85 | Version: 2012-10-17 86 | Statement: 87 | - Effect: Allow 88 | Action: iot:Publish 89 | Resource: 90 | !Join [ 91 | "", 92 | [ 93 | "arn:aws:iot:", 94 | !Ref "AWS::Region", 95 | ":", 96 | !Ref "AWS::AccountId", 97 | ":topic/", 98 | !Ref TopicDebug, 99 | ], 100 | ] 101 | - Effect: Allow 102 | Action: iot:Publish 103 | Resource: 104 | !Join [ 105 | "", 106 | [ 107 | "arn:aws:iot:", 108 | !Ref "AWS::Region", 109 | ":", 110 | !Ref "AWS::AccountId", 111 | ":topic/", 112 | !Ref TopicError, 113 | ], 114 | ] 115 | 116 | Outputs: 117 | TransformLoRaWANBinaryPayloadFunctionArn: 118 | Value: !Ref TransformLoRaWANBinaryPayloadFunction 119 | -------------------------------------------------------------------------------- /workshop/sampledecoder/README.md: -------------------------------------------------------------------------------- 1 | # Sample for workshop "How to build a binary decoder for AWS IoT Core for LoRaWAN" 2 | 3 | ## Prerequisites 4 | 5 | The sample requires SAM CLI, you can find installation instructions [here](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html). 6 | 7 | ## Deployment 8 | 9 | Please perform the following steps to deploy a sample application: 10 | 11 | 1. Check out this repository on your computer 12 | 13 | ```shell 14 | git clone https://github.com/aws-samples/aws-iot-core-lorawan 15 | cd workshop/simpledecoder 16 | ``` 17 | 18 | 2. Please review the SAM template [template.yaml](template.yaml) to understand the ressources that will be created in your AWS accoumt. 19 | 20 | 3. This sample uses [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/index.html) to build and deploy all necessary resources (e.g. AWS Lambda function, AWS IoT Rule, AWS IAM Roles) to your AWS account. Please perform the following commands to build the SAM artifacts: 21 | 22 | ```shell 23 | sam build 24 | ``` 25 | 26 | 4. Deploy the SAM template to your AWS account. 27 | 28 | ```shell 29 | sam deploy --guided 30 | ``` 31 | 32 | You can use the default parameters. 33 | 34 | Please note that `sam deploy --guided` should be only executed for a first deployment. To redeploy after that please use `sam deploy`. 35 | 36 | -------------------------------------------------------------------------------- /workshop/sampledecoder/downlink/rfi_downlink_off.sh: -------------------------------------------------------------------------------- 1 | aws iotwireless send-data-to-wireless-device \ 2 | --id 978ea3fa-5448-4d0b-990c-19a0b45d86a3 \ 3 | --transmit-mode 1 \ 4 | --payload-data AA== \ 5 | --wireless-metadata LoRaWAN={FPort=1} 6 | 7 | -------------------------------------------------------------------------------- /workshop/sampledecoder/downlink/rfi_downlink_on.sh: -------------------------------------------------------------------------------- 1 | aws iotwireless send-data-to-wireless-device \ 2 | --id 978ea3fa-5448-4d0b-990c-19a0b45d86a3 \ 3 | --transmit-mode 1 \ 4 | --payload-data AQ== \ 5 | --wireless-metadata LoRaWAN={FPort=1} -------------------------------------------------------------------------------- /workshop/sampledecoder/downlink/rfi_downlink_request_interval.sh: -------------------------------------------------------------------------------- 1 | # The interval timer is contained in byte 2 and 3 of the message. The two bytes form a 16-bit integer 2 | # and can have a value between 30 and 3600 seconds (0x1E to 0xE10 in hex). Any value outside of 3 | # this range will be ignored. 4 | 5 | # When the RPSW receives a type F0 message with the correct number of seconds it sends a reply 6 | # with the same message to confirm. When the interval timer value is in the correct range i.e., between 7 | # 30 and 3600, the reply message contains the new value. However, when the interval timer contains 8 | # an incorrect value i.e., outside the 30 to 3600 range the reply contains the original value that was 9 | # already stored in the RPSW. 10 | 11 | # You can request the current value of the interval setting by sending an empty type F0 message. 12 | 13 | # For example, to set the reporting interval to 60 seconds (0x3C hex) you have to send the following 14 | # message: 15 | # F0003C 16 | 17 | # The RPSW will reply with: 18 | 19 | # F0003C 20 | 21 | # When you want to find out what the reporting interval is you would send: 22 | 23 | # F0 24 | 25 | # The RPSW will reply with: 26 | 27 | # F0003C 28 | # F0 = 8A== 29 | # F0003C = 8AA8 30 | aws iotwireless send-data-to-wireless-device \ 31 | --id 978ea3fa-5448-4d0b-990c-19a0b45d86a3 \ 32 | --transmit-mode 1 \ 33 | --payload-data 8A== \ 34 | --wireless-metadata LoRaWAN={FPort=1} -------------------------------------------------------------------------------- /workshop/sampledecoder/downlink/rfi_downlink_set_interval_30s.sh: -------------------------------------------------------------------------------- 1 | # The interval timer is contained in byte 2 and 3 of the message. The two bytes form a 16-bit integer 2 | # and can have a value between 30 and 3600 seconds (0x1E to 0xE10 in hex). Any value outside of 3 | # this range will be ignored. 4 | 5 | # When the RPSW receives a type F0 message with the correct number of seconds it sends a reply 6 | # with the same message to confirm. When the interval timer value is in the correct range i.e., between 7 | # 30 and 3600, the reply message contains the new value. However, when the interval timer contains 8 | # an incorrect value i.e., outside the 30 to 3600 range the reply contains the original value that was 9 | # already stored in the RPSW. 10 | 11 | # You can request the current value of the interval setting by sending an empty type F0 message. 12 | 13 | # For example, to set the reporting interval to 60 seconds (0x3C hex) you have to send the following 14 | # message: 15 | # F0003C 16 | 17 | # The RPSW will reply with: 18 | 19 | # F0003C 20 | 21 | # When you want to find out what the reporting interval is you would send: 22 | 23 | # F0 24 | 25 | # The RPSW will reply with: 26 | 27 | # F0003C 28 | # 60 = F0 = 8A== 29 | # 30 = 1E = Hg== 30 | # F0003C = 8AA8 31 | # F0001E = 8AAe 32 | aws iotwireless send-data-to-wireless-device \ 33 | --id 978ea3fa-5448-4d0b-990c-19a0b45d86a3 \ 34 | --transmit-mode 1 \ 35 | --payload-data 8AAe \ 36 | --wireless-metadata LoRaWAN={FPort=1} -------------------------------------------------------------------------------- /workshop/sampledecoder/downlink/rfi_downlink_set_interval_60s.sh: -------------------------------------------------------------------------------- 1 | # The interval timer is contained in byte 2 and 3 of the message. The two bytes form a 16-bit integer 2 | # and can have a value between 30 and 3600 seconds (0x1E to 0xE10 in hex). Any value outside of 3 | # this range will be ignored. 4 | 5 | # When the RPSW receives a type F0 message with the correct number of seconds it sends a reply 6 | # with the same message to confirm. When the interval timer value is in the correct range i.e., between 7 | # 30 and 3600, the reply message contains the new value. However, when the interval timer contains 8 | # an incorrect value i.e., outside the 30 to 3600 range the reply contains the original value that was 9 | # already stored in the RPSW. 10 | 11 | # You can request the current value of the interval setting by sending an empty type F0 message. 12 | 13 | # For example, to set the reporting interval to 60 seconds (0x3C hex) you have to send the following 14 | # message: 15 | # F0003C 16 | 17 | # The RPSW will reply with: 18 | 19 | # F0003C 20 | 21 | # When you want to find out what the reporting interval is you would send: 22 | 23 | # F0 24 | 25 | # The RPSW will reply with: 26 | 27 | # F0003C 28 | # 60 = F0 = 8A== 29 | # 30 = 1E = Hg== 30 | # F0003C = 8AA8 31 | # F0001E = 8AAe 32 | aws iotwireless send-data-to-wireless-device \ 33 | --id 978ea3fa-5448-4d0b-990c-19a0b45d86a3 \ 34 | --transmit-mode 1 \ 35 | --payload-data 8AA8 \ 36 | --wireless-metadata LoRaWAN={FPort=1} -------------------------------------------------------------------------------- /workshop/sampledecoder/events/downlink_lht65_60s.json: -------------------------------------------------------------------------------- 1 | { 2 | "Id": "75495835-5a61-49fd-ab9f-eeb2e7ef77f5", 3 | "TransmitMode": 1, 4 | "PayloadData": "AQAAPA==", 5 | "WirelessMetadata": { 6 | "LoRaWAN": { 7 | "FPort": 1 8 | } 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /workshop/sampledecoder/src/app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | import json 18 | import traceback 19 | import logging 20 | import sys 21 | from time import time 22 | 23 | 24 | import rfi_power_switch 25 | 26 | # Setup logging 27 | logger = logging.getLogger("PayloadDecoder") 28 | logger.setLevel(logging.INFO) 29 | 30 | 31 | def lambda_handler(event, context): 32 | """ Transforms a binary payload by invoking "decode_{event.type}" function 33 | Parameters 34 | ---------- 35 | DeviceId : str 36 | Device Id 37 | ApplicationId : int 38 | LoRaWAN Application Id / Port number 39 | PayloadData : str 40 | Base64 encoded input payload 41 | 42 | 43 | Returns 44 | ------- 45 | This function returns a JSON object with the following keys: 46 | 47 | - status: 200 or 500 48 | - transformed_payload: result of calling "{PayloadDecoderName}.dict_from_payload" (only if status == 200) 49 | - lns_payload: a representation of payload as received from an LNS 50 | - error_type (only if status == 500) 51 | - error_message (only if status == 500) 52 | - stackTrace (only if status == 500) 53 | 54 | 55 | """ 56 | 57 | logger.info("Received event: %s" % json.dumps(event)) 58 | 59 | input_base64 = event.get("PayloadData") 60 | device_id = event.get("WirelessDeviceId") 61 | metadata = event.get("WirelessMetadata")["LoRaWAN"] 62 | 63 | try: 64 | 65 | # Invoke a payload conversion function 66 | decoded_payload = rfi_power_switch.dict_from_payload( 67 | event.get("PayloadData")) 68 | 69 | # Define the output of AWS Lambda function in case of successful decoding 70 | decoded_payload["status"] = 200 71 | 72 | result = decoded_payload 73 | 74 | logger.info(result) 75 | return result 76 | 77 | except Exception as exp: 78 | 79 | logger.error(f"Exception {exp} during binary decoding") 80 | 81 | raise exp 82 | -------------------------------------------------------------------------------- /workshop/sampledecoder/src/dragino_lht65.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str): 22 | 23 | decoded = base64.b64decode(base64_input) 24 | 25 | # Batter status flag 26 | # 00(b): Ultra Low ( BAT <= 2.50v) 27 | # 01(b): Low (2.50v <=BAT <= 2.55v) 28 | # 10(b): OK Good (2.55v <= BAT <=2.65v) 29 | # 11(b): Good (BAT >= 2.65v) 30 | battery_status_flag = (decoded[0] & 0b11000000) >> 6 31 | battery_status = "unknown" 32 | if battery_status_flag == 0b00: 33 | battery_status = "very low" 34 | elif battery_status_flag == 0b01: 35 | battery_status = "low" 36 | elif battery_status_flag == 0b10: 37 | battery_status = "OK" 38 | elif battery_status_flag == 0b11: 39 | battery_status = "Good" 40 | 41 | # Battery voltage 42 | battery_value = ((decoded[0] << 8 | decoded[1]) & 0x3FFF) / 1000 43 | 44 | # Internal sensor temperature 45 | if (decoded[2] & 0b1000000): 46 | internal_temperature = ((decoded[2] << 8 | decoded[3]) - 0xFFFF)/100 47 | else: 48 | internal_temperature = (decoded[2] << 8 | decoded[3])/100 49 | 50 | # Humidity 51 | humidity = ((decoded[4] << 8 | decoded[5])/10) 52 | 53 | # External sensor temperature 54 | if (decoded[7] & 0b1000000): 55 | external_temperature = ( 56 | ((decoded[7] << 8 | decoded[8]) - 0xFFFF) / 100) 57 | else: 58 | external_temperature = ((decoded[7] << 8 | decoded[8]) / 100) 59 | 60 | result = { 61 | "battery_status": battery_status, 62 | "battery_value": battery_value, 63 | "temperature_internal": internal_temperature, 64 | "humidity": humidity, 65 | "temperature_external": external_temperature 66 | } 67 | 68 | return result 69 | 70 | 71 | # Tests 72 | if __name__ == "__main__": 73 | test_definition = [ 74 | { 75 | "input": "CBF60B0D0376010ADD7FFF", 76 | "output": { 77 | "battery_status": "Good", 78 | "battery_value": 3.062, 79 | "temperature_internal": 28.29, 80 | "humidity": 88.6, 81 | "temperature_external": 27.81 82 | 83 | } 84 | }, 85 | { 86 | "input": "CBBDF5C6022E01F54F7FFF", 87 | "output": { 88 | "battery_status": "Good", 89 | "battery_value": 3.005, 90 | "temperature_internal": -26.17, 91 | "humidity": 55.8, 92 | "temperature_external": -27.36 93 | 94 | } 95 | } 96 | ] 97 | 98 | for test in test_definition: 99 | base64_input = base64.b64encode( 100 | bytearray.fromhex(test.get("input"))).decode("utf-8") 101 | output = dict_from_payload(base64_input) 102 | for key in test.get("output"): 103 | if(test.get("output").get(key) != output.get(key)): 104 | raise Exception( 105 | f'Assertion failed for input {test.get("input")}, key {key}, expected {test.get("output").get(key)}, got {output.get(key)} ') 106 | -------------------------------------------------------------------------------- /workshop/sampledecoder/src/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-iot-core-lorawan/8cc805dca80acb0627e457aef225f46bb3dcffb1/workshop/sampledecoder/src/requirements.txt -------------------------------------------------------------------------------- /workshop/sampledecoder/src/rfi_power_switch.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | # software and associated documentation files (the "Software"), to deal in the Software 6 | # without restriction, including without limitation the rights to use, copy, modify, 7 | # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so. 9 | # 10 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | 18 | import base64 19 | 20 | 21 | def dict_from_payload(base64_input: str): 22 | 23 | payload_bytes = base64.b64decode(base64_input) 24 | 25 | if payload_bytes[0] == 0x00: 26 | result = { 27 | "messagetype": "switchstatus", 28 | "switch_status": "off" 29 | } 30 | elif payload_bytes[0] == 0x01: 31 | result = { 32 | "messagetype": "switchstatus", 33 | "switch_status": "off" 34 | } 35 | elif payload_bytes[0] == 0xF0: 36 | result = { 37 | "messagetype": "interval", 38 | "interval": payload_bytes[2] | payload_bytes[1] << 8 39 | } 40 | 41 | 42 | return result 43 | 44 | 45 | # Tests 46 | if __name__ == "__main__": 47 | test_definition = [ 48 | { 49 | "input": "00", 50 | "output": { 51 | "switch_status": "off" 52 | } 53 | }, 54 | { 55 | "input": "01", 56 | "output": { 57 | "switch_status": "on" 58 | } 59 | }, 60 | { 61 | "input": "F0", 62 | "output": { 63 | "switch_status": "on" 64 | } 65 | } 66 | ] 67 | 68 | for test in test_definition: 69 | base64_input = base64.b64encode( 70 | bytearray.fromhex(test.get("input"))).decode("utf-8") 71 | output = dict_from_payload(base64_input) 72 | for key in test.get("output"): 73 | if(test.get("output").get(key) != output.get(key)): 74 | raise Exception( 75 | f'Assertion failed for input {test.get("input")}, key {key}, expected {test.get("output").get(key)}, got {output.get(key)} ') 76 | else: 77 | print("OK") 78 | -------------------------------------------------------------------------------- /workshop/sampledecoder/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | Sample minimalistic binary decoder for RFI power switch 5 | 6 | Parameters: 7 | TopicError: 8 | Type: String 9 | Default: sampledecoder/error 10 | Description: Name of MQTT topic for to publish IoT Rule action error messages 11 | 12 | TopicDebug: 13 | Type: String 14 | Default: sampledecoder/debug 15 | Description: | 16 | Prefix of a MQTT topic to publish debugging information 17 | 18 | Resources: 19 | ############################################################################################ 20 | # Payload transformation Lambda function to be called from AWS IoT Core. This function will refer to 21 | # layer "DecoderLayer" to include the necessary decoding libraries 22 | ############################################################################################ 23 | 24 | TransformLoRaWANBinaryPayloadFunction: 25 | Type: AWS::Serverless::Function 26 | Name: !Sub "${AWS::StackName}-SampleBinaryDecodingFunction" 27 | Properties: 28 | CodeUri: src 29 | Handler: app.lambda_handler 30 | Runtime: python3.8 31 | Timeout: 10 32 | 33 | TransformLoRaWANBinaryPayloadFunctionPermission: 34 | Type: AWS::Lambda::Permission 35 | Properties: 36 | FunctionName: !GetAtt TransformLoRaWANBinaryPayloadFunction.Arn 37 | Action: lambda:InvokeFunction 38 | Principal: iot.amazonaws.com 39 | 40 | TransformLoRaWANBinaryPayloadRule: 41 | Type: "AWS::IoT::TopicRule" 42 | Properties: 43 | RuleName: SampeLoRaWANPayloadRule_RFI_Remote_Switch 44 | TopicRulePayload: 45 | AwsIotSqlVersion: "2016-03-23" 46 | RuleDisabled: false 47 | 48 | Sql: !Sub 49 | - | 50 | SELECT * as decoding_input, aws_lambda("${LambdaARN}", 51 | { 52 | "PayloadData":PayloadData, 53 | "WirelessDeviceId": WirelessDeviceId, 54 | "WirelessMetadata": WirelessMetadata 55 | } 56 | ) as decoding_output 57 | - { LambdaARN: !GetAtt TransformLoRaWANBinaryPayloadFunction.Arn } 58 | Actions: 59 | - Republish: 60 | RoleArn: !GetAtt TransformLoRaWANBinaryPayloadRuleRole.Arn 61 | Topic: !Ref TopicDebug 62 | Qos: 0 63 | 64 | ErrorAction: 65 | Republish: 66 | RoleArn: !GetAtt TransformLoRaWANBinaryPayloadRuleRole.Arn 67 | Topic: !Ref TopicError 68 | Qos: 0 69 | 70 | TransformLoRaWANBinaryPayloadRuleRole: 71 | Type: "AWS::IAM::Role" 72 | Properties: 73 | AssumeRolePolicyDocument: 74 | Version: 2012-10-17 75 | Statement: 76 | - Effect: Allow 77 | Principal: 78 | Service: 79 | - iot.amazonaws.com 80 | Action: 81 | - "sts:AssumeRole" 82 | Policies: 83 | - PolicyName: root 84 | PolicyDocument: 85 | Version: 2012-10-17 86 | Statement: 87 | - Effect: Allow 88 | Action: iot:Publish 89 | Resource: 90 | !Join [ 91 | "", 92 | [ 93 | "arn:aws:iot:", 94 | !Ref "AWS::Region", 95 | ":", 96 | !Ref "AWS::AccountId", 97 | ":topic/", 98 | !Ref TopicDebug, 99 | ], 100 | ] 101 | - Effect: Allow 102 | Action: iot:Publish 103 | Resource: 104 | !Join [ 105 | "", 106 | [ 107 | "arn:aws:iot:", 108 | !Ref "AWS::Region", 109 | ":", 110 | !Ref "AWS::AccountId", 111 | ":topic/", 112 | !Ref TopicError, 113 | ], 114 | ] 115 | 116 | Outputs: 117 | TransformLoRaWANBinaryPayloadFunctionArn: 118 | Value: !Ref TransformLoRaWANBinaryPayloadFunction 119 | --------------------------------------------------------------------------------