diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..36fbcdb --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,136 @@ +name: CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + workflow_dispatch: + +jobs: + + build: + strategy: + matrix: + board: + - esp32:esp32:esp32:DebugLevel=none + - esp32:esp32:esp32:DebugLevel=verbose + #- esp32:esp32:firebeetle32 + #- esp32:esp32:ttgo-lora32:Revision=TTGO_LoRa32_V1 + #- esp32:esp32:ttgo-lora32:Revision=TTGO_LoRa32_V2 + #- esp32:esp32:ttgo-lora32:Revision=TTGO_LoRa32_v21new + #- esp32:esp32:heltec_wireless_stick:PSRAM=disabled + #- esp32:esp32:featheresp32 + #- esp32:esp32:adafruit_feather_esp32s2 + - rp2040:rp2040:adafruit_feather:dbgport=Serial + runs-on: ubuntu-latest + name: ${{ matrix.board }} + env: + GH_TOKEN: ${{ github.token }} + run-build: ${{ contains(matrix.board, 'esp32:esp32') || contains(matrix.board, 'rp2040:rp2040') || contains(github.event.head_commit.message, 'CI_BUILD_ALL') || contains(github.event.head_commit.message, 'Bump version to') || contains(github.event.head_commit.message, format('{0}', matrix.board)) }} + + steps: + - name: Install arduino-cli + if: ${{ env.run-build == 'true' }} + run: + | + mkdir -p ~/.local/bin + echo "~/.local/bin" >> $GITHUB_PATH + curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=~/.local/bin sh + + - name: Get platform name + if: ${{ env.run-build == 'true' }} + uses: jungwinter/split@v2 + id: split + with: + msg: ${{ matrix.board }} + separator: ':' + + - name: Prepare platform-specific settings + if: ${{ env.run-build == 'true' }} + id: prep + run: + | + # common settings - no extra options, skip nothing, all warnings + echo "skip-pattern='simple_sensor_bme280'" >> $GITHUB_OUTPUT + echo "warnings='all'" >> $GITHUB_OUTPUT + + # platform-dependent settings - extra board options, board index URLs, skip patterns etc. + if [[ "${{ contains(matrix.board, 'esp32:esp32') }}" == "true" ]]; then + # ESP32 + python -m pip install pyserial + echo "index-url=--additional-urls https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json" >> $GITHUB_OUTPUT + elif [[ "${{ contains(matrix.board, 'rp2040:rp2040') }}" == "true" ]]; then + # RP2040 + echo "index-url=--additional-urls https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json" >> $GITHUB_OUTPUT + fi + + - name: Install libraries + if: ${{ env.run-build == 'true' }} + run: + | + declare -a required_libs=( + "MCCI LoRaWAN LMIC library@4.1.1" + "MCCI Arduino LoRaWAN Library@0.10.0" + "MCCI Arduino Development Kit ADK@0.2.2" + "LoRa Serialization@3.2.1" + "ESP32Time@2.0.4" + "ESP32AnalogRead@0.2.1" + "Preferences@2.1.0") + for i in "${required_libs[@]}" + do + arduino-cli lib install "$i" + done + + - name: Install platform + if: ${{ env.run-build == 'true' }} + run: + | + arduino-cli core update-index ${{ format('{0}', steps.prep.outputs.index-url) }} + arduino-cli core install ${{ format('{0}:{1} {2}', steps.split.outputs._0, steps.split.outputs._1, steps.prep.outputs.index-url) }} + + - name: Checkout repository + if: ${{ env.run-build == 'true' }} + uses: actions/checkout@v3 + + - name: Customizing lmic_project_config.h (LMIC_ENABLE_DeviceTimeReq) + if: ${{ env.run-build == 'true' }} + run: + | + echo "#define LMIC_ENABLE_DeviceTimeReq 1" >> /home/runner/Arduino/libraries/MCCI_LoRaWAN_LMIC_library/project_config/lmic_project_config.h + # Fix for linker error with Arduino core for ESP32 v2.0.x as suggested in https://github.com/mcci-catena/arduino-lmic/issues/714#issuecomment-822051171 + echo "#define hal_init LMICHAL_init" >> /home/runner/Arduino/libraries/MCCI_LoRaWAN_LMIC_library/project_config/lmic_project_config.h + + - name: Copy secrets.h from secrets.h.template + if: ${{ env.run-build == 'true' }} + run: + | + pwd + ls + cp /home/runner/work/arduino-lorawan/arduino-lorawan/examples/arduino_lorawan_esp32_example/secrets.h.template \ + /home/runner/work/arduino-lorawan/arduino-lorawan/examples/arduino_lorawan_esp32_example/secrets.h + + - name: Build sketch + if: ${{ env.run-build == 'true' }} + run: + | + #for example in $(find $PWD/examples -name '*.ino' | sort); do + # modified to compile a singe sketch (instead of a library's examples) + for example in $(find $PWD -name '*.ino' | sort); do + # check whether to skip this sketch + if [ ! -z '${{ steps.prep.outputs.skip-pattern }}' ] && [[ ${example} =~ ${{ steps.prep.outputs.skip-pattern }} ]]; then + # skip sketch + echo -e "\n\033[1;33mSkipped ${example##*/} (matched with ${{ steps.prep.outputs.skip-pattern }})\033[0m"; + else + # build sketch + echo -e "\n\033[1;33mBuilding ${example##*/} ... \033[0m"; + arduino-cli compile --libraries /home/runner/work/arduino-lorawan --fqbn ${{ matrix.board }}${{ steps.prep.outputs.options }} $example --warnings=${{ steps.prep.outputs.warnings }} + + if [ $? -ne 0 ]; then + echo -e "\033[1;31m${example##*/} build FAILED\033[0m\n"; + exit 1; + else + echo -e "\033[1;32m${example##*/} build PASSED\033[0m\n"; + fi + fi + done diff --git a/examples/arduino_lorawan_esp32_example/DEBUG_OUTPUT.md b/examples/arduino_lorawan_esp32_example/DEBUG_OUTPUT.md new file mode 100644 index 0000000..3f7fbf9 --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/DEBUG_OUTPUT.md @@ -0,0 +1,22 @@ +# Debug Output Configuration in Arduino IDE + +## ESP32 + +1. Select appropriate (USB-)serial port for your board + + ![Arduino_IDE-Tools_Port](https://github.com/matthias-bs/BresserWeatherSensorTTN/assets/83612361/be496bf8-89ce-4db5-b1bf-c88a7f5e99cb) + +**or** + + ![Arduino_IDE-Select_Other_Board_and_Port](https://github.com/matthias-bs/BresserWeatherSensorTTN/assets/83612361/ac847f23-4fe6-4111-929f-ac6d36cb8a53) + +2. Select desired debug level + + ![Arduino_IDE-Tools_CoreDebugLevel](https://github.com/matthias-bs/BresserWeatherSensorTTN/assets/83612361/72a8b1d9-8d39-41fc-9658-78b432b73d56) + + This passes the define `CORE_DEBUG_LEVEL`to the compiler accordingly. + +Refer to the following for some background information +* https://thingpulse.com/esp32-logging/ +* https://www.mischianti.org/2020/09/20/esp32-manage-multiple-serial-and-logging-for-debugging-3/ +* https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/esp32-hal-log.h diff --git a/examples/arduino_lorawan_esp32_example/README.md b/examples/arduino_lorawan_esp32_example/README.md new file mode 100644 index 0000000..09fe4da --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/README.md @@ -0,0 +1,337 @@ +# ESP32 LoRaWAN Sensor Node Example + +The provided sensor data function stubs `getTemperature()` and `getHumidity()` can be replaced by real function implementations. External sensors can be integrated via Bluetooth Low Energy (BLE), OneWire, UART, analog/digital inputs etc. + +## Features +* Pin configurations for several ESP32 boards and an RP2040 board +* Tested with [The Things Network](https://www.thethingsnetwork.org/) and [Helium Network](https://www.helium.com/) (EU868) +* Low Power Design (using ESP32 Deep Sleep Mode / RP2040 Sleep State) +* Fast LoRaWAN Joining after Deep Sleep (using ESP32 RTC RAM / RP2040 Flash) + +* ESP32/RP2040 Analog Digital Converter Integration (optional) + * Supply/Battery Voltage Monitoring + * Analog Sensor Data Aquisition +* Time Keeping with RTC and Synchronization to Network Time +* Wake-up to fixed Time Scheme (Sleep Time is adjusted to current Up Time) +* Power Saving by Switching to longer Transmit Interval (below BATTERY_WEAK Threshold) +* Power Saving by Entering Deep Sleep Mode immediately after Wake-Up (below BATTERY_LOW Threshold) +* [Remote Configuration via LoRaWAN Downlink](README.md#remote-configuration-via-lorawan-downlink) +* Example Javascript Uplink/Downlink Formatters for The Things Network +* Debug Output using Arduino Logging Functions - using Debug Level set in Arduino IDE + +## Supported Hardware +* [LoRaWAN_Node](https://github.com/matthias-bs/LoRaWAN_Node) +* [LILYGO® TTGO LORA32](https://www.lilygo.cc/products/lora3?variant=42272562282677) + + [TTGO LoRa32 V2.1.6 Pinout](https://github.com/lnlp/pinout-diagrams/blob/main/LoRa%20development%20boards/TTGO%20LoRa32%20V2.1.6%20Pinout%20(LLP).pdf) + +* [Heltec Wireless Stick](https://heltec.org/project/wireless-stick/) +* *Resumably* [Heltec WiFi LoRa32 V2](https://heltec.org/project/wifi-lora-32/) (confirmation wanted!!!) +* *Presumably* [Adafruit Feather ESP32-S2](https://www.adafruit.com/product/4769) with [Adafruit LoRa Radio FeatherWing](https://www.adafruit.com/product/3231) (confirmation wanted!!!) + +* *Presumably* [Adafruit Feather ESP32](https://www.adafruit.com/product/3405) with [Adafruit LoRa Radio FeatherWing](https://www.adafruit.com/product/3231) (confirmation wanted!!!) + +* [Thingpulse ePulse Feather](https://thingpulse.com/product/epulse-feather-low-power-esp32-development-board/) with [Adafruit LoRa Radio FeatherWing](https://www.adafruit.com/product/3231) (**see** [**#56**](https://github.com/matthias-bs/BresserWeatherSensorTTN/issues/56)) + +* [DFRobot FireBeetle ESP32 IoT Microcontroller](https://www.dfrobot.com/product-1590.html) with [FireBeetle Cover LoRa Radio 868MHz](https://www.dfrobot.com/product-1831.html) + +* [Adafruit Feather RP2040](https://www.adafruit.com/product/4884) with [Adafruit LoRa Radio FeatherWing](https://www.adafruit.com/product/3231) + +See [The Things Network's](https://www.thethingsnetwork.org) [Big ESP32 + SX127x topic part 2](https://www.thethingsnetwork.org/forum/t/big-esp32-sx127x-topic-part-2/11973) for some hardware options. + +See [Leonel Lopes Parente's](https://github.com/lnlp) collection of [LoRa development boards pinout-diagrams](https://github.com/lnlp/pinout-diagrams/tree/main/LoRa%20development%20boards). + +### Power Supply +Mains adapter or Li-Ion battery (with or without solar charger) - depending on desired operation time and duty cycle. + +## Software Build Setup + +* Install the Arduino ESP32 board package in the Arduino IDE -
+ **Note:** You have to apply a fix in arduino-lmic (see below) +* Select the desired ESP32 board in the Arduino IDE +* Select the desired debug level in the Arduino IDE +* Install all libraries as listed in the section [Library Dependencies](README.md#library-dependencies) via the Arduino IDE Library Manager +* Configure `Arduino/libraries/MCCI_LoRaWAN_LMIC_library/project_config/lmic_project_config.h`: + * select you appropriate region + * `#define CFG_sx1276_radio 1` + +* Add the following line to `Arduino/libraries/MCCI_LoRaWAN_LMIC_library/project_config/lmic_project_config.h`: + + `#define LMIC_ENABLE_DeviceTimeReq 1` + + (Otherwise requesting the time from the LoRaWAN network will not work, even if supported by the network.) +* Apply fix: + * https://github.com/mcci-catena/arduino-lmic/issues/714#issuecomment-822051171 +* Compile + +### Library Dependencies + +| Library | r: required /
o: optional | +| ---------------------------------- | ---------------------------- | +| MCCI Arduino Development Kit ADK | r | +| MCCI LoRaWAN LMIC library | r | +| MCCI Arduino LoRaWAN Library | r | +| LoRa_Serialization | r | +| ESP32Time | r | +| Preferences | r (RP2040) | +| ESP32AnalogRead | o | + + +## Software Customization + +### Configure the LoRaWAN Network settings APPEUI, DEVEUI and APPKEY + +* First you have to follow your LoRaWAN Network provider's instructions on how to configure/obtain the settings. +* Then configure the arduino_lorawan_esp32_example software accordingly: + * Solution 1 (not recommended): + Configure the section starting with `// APPEUI, DEVEUI and APPKEY` in [arduino_lorawan_esp32_example.ino](arduino_lorawan_esp32_example.ino) + * Solution 2 (recommended): + Configure the file `secrets.h` - refer to [secrets.h.template](secrets.h.template) as an example -- + ``` + #define SECRETS + + // deveui, little-endian + static const std::uint8_t deveui[] = { 0xAA, 0xBB, 0xCC, 0x00, 0x00, 0xDD, 0xEE, 0xFF }; + + // appeui, little-endian + static const std::uint8_t appeui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + // appkey: just a string of bytes, sometimes referred to as "big endian". + static const std::uint8_t appkey[] = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00 }; + ``` + +### Configure the ESP32 / RF Transceiver GPIO Wiring + +#### Pinout Configuration by selecting a supported Board in the Arduino IDE + +By selecting a Board and a Board Revision (if available) in the Arduino IDE, a define is passed to the preprocessor/compiler. For the boards in the table below, the default configuration is assumed based in this define. + +If you are not using the Arduino IDE, you can use the defines in the table below with your specific tool chain to get the same result. + +If this is not what you need, you have to switch to Manual Configuration. + + | Setup | Board | Board Revision | Define | Radio Module | Notes | + | -------------------------------------------------------------- | ------------------ | ---------------------------- | ---------------------- | -------- | ------- | + | [LILYGO®TTGO-LORA32 V1](https://github.com/Xinyuan-LilyGo/TTGO-LoRa-Series) | "TTGO LoRa32-OLED" | "TTGO LoRa32 V1 (No TFCard)" | ARDUINO_TTGO_LORA32_V1 | SX1276 (HPD13A) | - | + | [LILYGO®TTGO-LORA32 V2](https://github.com/LilyGO/TTGO-LORA32) | "TTGO LoRa32-OLED" | "TTGO LoRa32 V2" | ARDUINO_TTGO_LoRa32_V2 | SX1276 (HPD13A) | Wire DIO1 to GPIO33 | + | [LILYGO®TTGO-LORA32 V2.1](https://www.lilygo.cc/products/lora3?variant=42272562282677) | "TTGO LoRa32-OLED" | "TTGO LoRa32 V2.1 (1.6.1)" | ARDUINO_TTGO_LoRa32_v21new | SX1276 (HPD13A) | - | + | [Heltec Wireless Stick](https://heltec.org/project/wireless-stick/) | "Heltec Wireless Stick" | n.a. | ARDUINO_heltec_wireless_stick | SX1276 | - | + | [LoRaWAN_Node](https://github.com/matthias-bs/LoRaWAN_Node) | "FireBeetle-ESP32" | n.a. | ARDUINO_ESP32_DEV -> LORAWAN_NODE | SX1276 (RFM95W) | - | + | [DFRobot FireBeetle ESP32 IoT Microcontroller](https://www.dfrobot.com/product-1590.html) with [FireBeetle Cover LoRa Radio 868MHz](https://www.dfrobot.com/product-1831.html) | "FireBeetle-ESP32" | n.a. | ARDUINO_ESP32_DEV & FIREBEETLE_ESP32_COVER_LORA | SX1276 (LoRa1276) | Wiring on the cover:
D2 to RESET
D3 to DIO0
D4 to CS
D5 to DIO1 | + | [Adafruit Feather ESP32S2 with Adafruit LoRa Radio FeatherWing](https://github.com/matthias-bs/BresserWeatherSensorReceiver#adafruit-feather-esp32s2-with-adafruit-lora-radio-featherwing) | "Adafruit Feather ESP32-S2" | n.a. | ARDUINO_
ADAFRUIT_FEATHER_ESP32S2 | SX1276 (RFM95W) | **No Bluetooth available!**
Wiring on the Featherwing:
E to IRQ
D to CS
C to RST
A to DI01 | +| [Thingpulse ePulse Feather](https://thingpulse.com/product/epulse-feather-low-power-esp32-development-board/) with [Adafruit LoRa Radio FeatherWing](https://www.adafruit.com/product/3231) | "Adafruit ESP32 Feather" | n.a. | ARDUINO_FEATHER_ESP32 | SX1276 (RFM95W) | Wiring on the Featherwing:
E to IRQ
D to CS
C to RST
A to DI01

**see** [**#55**](https://github.com/matthias-bs/BresserWeatherSensorTTN/issues/55) | +| [Adafruit Feather RP2040](https://www.adafruit.com/product/4884) with [Adafruit LoRa Radio FeatherWing](https://www.adafruit.com/product/3231) | "Adafruit Feather RP2040" | n.a. | ARDUINO_ADAFRUIT_FEATHER_RP2040 | SX1276 (RFM95W) | **No Bluetooth available!**
**Configuration: Choose an entry with "FS" in section __Flash Size__!**
Wiring on the Featherwing:
E to IRQ
D to CS
C to RST
A to DI01 | + +If enabled in the Arduino IDE Preferences ("Verbose Output"), the preprosessor will provide some output regarding the selected configuration, e.g. + +``` +ARDUINO_ADAFRUIT_FEATHER_ESP32S2 defined; assuming RFM95W FeatherWing will be used +[...] +Receiver chip: [SX1276] +Pin config: RST->0 , CS->6 , GD0/G0/IRQ->5 , GDO2/G1/GPIO->11 +``` + +#### Manual Pinout Configuration + +Change the configuration in [arduino_lorawan_esp32_example.ino](arduino_lorawan_esp32_example.ino), e.g.: + +``` +#define PIN_LMIC_NSS 14 +#define PIN_LMIC_RST 12 +#define PIN_LMIC_DIO0 4 +#define PIN_LMIC_DIO1 16 +#define PIN_LMIC_DIO2 17 +``` + +### Configure the RF Transceiver SPI Wiring + +The board specific default SPI pin definitions (MISO, MOSI and SCK) can be found in +https://github.com/espressif/arduino-esp32/tree/master/variants + +To configure other SPI pins than the default ones... is up to you. I.e. better use the default pins unless you have a really good reason not to do so and then only if you know what you're doing! + +### Other Configuration Options + +In [arduino_lorawan_esp32_example.ino](arduino_lorawan_esp32_example.ino): +* Select the desired LoRaWAN network by (un)-commenting `ARDUINO_LMIC_CFG_NETWORK_TTN` or `ARDUINO_LMIC_CFG_NETWORK_GENERIC` +* Disable features which you do not want to use +* Configure the timing parameters (if you think this is needed) +* Configure your time zone by editing `TZ_INFO` +* Configure the ADC's input pins, dividers and oversampling settings as needed + +### Change the LoRaWAN Message Payload/Encoding +In [arduino_lorawan_esp32_example.ino](arduino_lorawan_esp32_example.ino), change the code starting with +``` +// +// Encode sensor data as byte array for LoRaWAN transmission +// +LoraEncoder encoder(loraData); +``` +Make sure that you do not exceed the size of the LoRaWAN uplink payload buffer `loraData[PAYLOAD_SIZE]`. The payload size is limited to 51 bytes by the LMIC library (for a good reason). + +If you are using an integration at the network side (such as an MQTT Integration), make sure you adjust your changes there as well - otherwise decoding the receiving/decoding the messages will fail. + +## Debug Output Configuration + +See [Debug Output Configuration in Arduino IDE](DEBUG_OUTPUT.md) + +## Remote Configuration via LoRaWAN Downlink + +| Command / Response | Cmd | Port | Unit | Data0 | Data1 | Data2 | Data3 | +| ----------------------------- | ---- | ---- | ------- | --------------- | --------------- | --------------- | --------------- | +| CMD_SET_SLEEP_INTERVAL | 0xA8 | | seconds | interval[15: 8] | interval[ 7: 0] | | | +| CMD_SET_SLEEP_INTERVAL_LONG | 0xA9 | | seconds | interval[15: 8] | interval[ 7: 0] | | | +| CMD_GET_CONFIG | 0xB1 | | | | | | | | +| response: | | 3 | seconds | sleep_interval[15: 8] | sleep_interval[ 7: 0] | sleep_interval_long[15: 8] | sleep_interval_long[ 7: 0] | +| CMD_GET_DATETIME | 0x86 | | | | | | +| response: | | 2 | epoch | unixtime[31:24] | unixtime[23:16] | unixtime[15:8] | unixtime[7:0] | +| CMD_SET_DATETIME | 0x88 | |epoch | unixtime[31:24] | unixtime[23:16] | unixtime[15:8] | unixtime[7:0] | + +:warning: Confirmed downlinks should not be used! (see [here](https://www.thethingsnetwork.org/forum/t/how-to-purge-a-scheduled-confirmed-downlink/56849/7) for an explanation.) + +### Remote Configuration with The Things Network Console +#### With Payload Formatter + +* see [ttn_downlink_formatter.js](scripts/ttn_downlink_formatter.js) for syntax of commands and responses +* see [The Things Network MQTT Integration and Payload Formatters](README.md#the-things-network-mqtt-integration-payload-formatters) + +##### Example 1: Set SLEEP_INTERVAL to 360 seconds +1. Build command sequence as JSON string: `{"cmd": "CMD_SET_SLEEP_INTERVAL", "interval": 360}` +2. Send command sequence via The Things Network Console +![TTN Downlink as JSON](https://github.com/matthias-bs/BresserWeatherSensorTTN/assets/83612361/e61ca412-713b-4972-8c4f-f0068068c323) + +##### Example 2: Set Date/Time +1. Get epoch (e.g. from https://www.epochconverter.com) (Example: 1692729833); add an offset (estimated) for time until received (Example: + 64 seconds => 16927298**97**) +2. Build command sequence as JSON string: {"cmd": "CMD_SET_DATETIME", "epoch": 1692729897} +3. Send command sequence via The Things Network Console + +#### Without Payload Formatter +##### Example 1: Set SLEEP_INTERVAL to 360 seconds +1. Convert interval to hex: 360 = 0x0168 +2. Build command sequence: "CMD_SET_SLEEP_INTERVAL 360 secs" -> 0xA8 0x01 0x68 +3. Send command sequence via The Things Network Console +![TTN Downlink as Hex](https://github.com/matthias-bs/BresserWeatherSensorTTN/assets/83612361/67544195-c2cd-4118-8de5-1f1c0facacdb) + +##### Example 2: Set Date/Time +1. Get epoch (e.g. from https://www.epochconverter.com/hex) (Example: 0x63B2BC32); add an offset (estimated) for time until received (Example: + 64 / 0x40 seconds => 0x63B2BC**7**2) +2. Build command sequence: "CMD_SET_DATETIME 0x63B2BC72" -> 0x88 0x63 0xB2 0xBC 0x72 +3. Send command sequence via The Things Network Console + +### Remote Configuration with Helium Console +#### With Payload Formatter +_To be done_ + +#### Without Payload Formatter +##### Example 1: Set SLEEP_INTERVAL to 360 seconds +1. Convert interval to hex: 360 = 0x0168 +2. Build command sequence: "CMD_SET_SLEEP_INTERVAL 360 secs" -> 0xA8 0x01 0x68 +3. Convert command sequence to Base64 encoding: 0xA8 0x01 0x68 -> "qAFo" ([Base64 Guru](https://base64.guru/converter/encode/hex)) +4. Send command sequence via Helium Console + ![Helium_Add_Downlink_Payload](https://user-images.githubusercontent.com/83612361/210183244-a2d109bc-6782-4f83-b406-7f6e0b17eda1.png) + +##### Example 2: Set Date/Time + +1. Get epoch (e.g. from https://www.epochconverter.com/hex) (Example: 0x63B2BC32); add an offset (estimated) for time until received (Example: + 64 / 0x40 seconds => 0x63B2BC**7**2) +2. Build command sequence: "CMD_SET_DATETIME 0x63B2BC72" -> 0x88 0x63 0xB2 0xBC 0x72 +3. Convert command sequence to Base64 encoding: 0x88 0x63 0xB2 0xBC 0x72 -> "iGOyvHI=" +4. Send command sequence e.g. via Helium Console + + +## MQTT Integration + +### The Things Network MQTT Integration and Payload Formatters + +#### Uplink Formatter + +Decode uplink payload (a sequence of bytes) into data structures which are readable/suitable for further processing. + +In The Things Network Console: +1. Go to "Payload formatters" -> "Uplink" +2. Select "Formatter type": "Custom Javascript formatter" +3. "Formatter code": Paste [ttn_uplink_formatter.js](scripts/ttn_uplink_formatter.js) +4. Apply "Save changes" + +![TTN Uplink Formatter](https://github.com/matthias-bs/BresserWeatherSensorTTN/assets/83612361/38b66478-688a-4028-974a-c517cddae662) + +#### Downlink Formatter + +Encode downlink payload from JSON to a sequence of bytes. + +In The Things Network Console: +1. Go to "Payload formatters" -> "Downlink" +2. Select "Formatter type": "Custom Javascript formatter" +3. "Formatter code": Paste [ttn_downlink_formatter.js](ttn_downlink_formatter.js) +4. Apply "Save changes" + +**Note:** The actual payload depends on the options selected in the Arduino software - the decoder must be edited accordingly (add or remove data types and JSON identifiers - see [ttn_uplink_formatter.js](httn_uplink_formatter.js) line 316ff). + +#### MQTT Integration +TTN provides an MQTT broker. +How to receive and decode the payload with an MQTT client - +see https://www.thethingsnetwork.org/forum/t/some-clarity-on-mqtt-topics/44226/2 + +V3 topic: + +`v3/ttn/devices//up` + + +v3 message key field jsonpaths: + +``` + = .end_device_ids.device_id + = .end_device_ids.application_ids.application_id // (not including the ttn in the topic) + = .uplink_message.frm_payload +``` + + +JSON-Path with Uplink-Decoder (see [ttn_uplink_formatter.js](ttn_uplink_formatter.js)) + +`.uplink_message.decoded_payload.bytes.` + +### Helium Network MQTT Integration and Message Decoder + +Please refer to https://docs.helium.com/use-the-network/console/integrations/mqtt/. + +Add an MQTT integration in the Helium console - the "Endpoint" is in fact an MQTT broker you have to provide: +![Helium_MQTT_Integration](https://user-images.githubusercontent.com/83612361/195050719-8562ad0e-5523-436f-8b61-e4b15b08d6de.png) + +Add [scripts/helium_decoder.js](scripts/helium_decoder.js) in the Helium console as custom function: +![Helium_Decoder_Function](https://user-images.githubusercontent.com/83612361/195045593-d6c76e0c-1d87-410a-b941-8636b35d601a.png) + +**Note:** The actual payload depends on the options selected in the Arduino software - the decoder must be edited accordingly (add or remove data types and JSON identifiers). + +Add your function to the flow: +![Helium_Decoder_Flow](https://user-images.githubusercontent.com/83612361/195047042-6a8d9dfe-61f6-43e3-ac51-b917d01ff237.png) + +Example decoder output (JSON): +``` +rx = {[...],"decoded":{"payload":{"air_temp_c":"13.5","battery_v":4197,"humidity":72, +"status_node":{"res7":false,"res6":false,"res5":false,"res4":false,"res3":false,"runtime_exp":false,"data_ok":true,"battery_ok":true}, +"supply_v":4210}, +"status":"success"}, [...] +``` +### Datacake Integration + +YouTube Video: [Get started for free with LoRaWaN on The Things Network and Datacake IoT Platform](https://youtu.be/WGVFgYp3k3s) + +### Decoder Scripts Summary + +#### [ttn_uplink_formatter.js](scripts/ttn_uplink_formatter.js) + +[The Things Network](https://www.thethingsnetwork.org/) uplink formatter for sensor data / node status messages and response messages triggered by uplink command messages. + +#### [ttn_downlink_formatter.js](ttn_downlink_formatter.js) + +[The Things Network](https://www.thethingsnetwork.org/) downlink formatter for sending commands as JSON strings to the device. + + +## References + +Based on +* [MCCI LoRaWAN LMIC library](https://github.com/mcci-catena/arduino-lmic) by Thomas Telkamp and Matthijs Kooijman / Terry Moore, MCCI +* [MCCI Arduino LoRaWAN Library](https://github.com/mcci-catena/arduino-lorawan) by Terry Moore, MCCI +* [BresserWeatherSensorTTN](https://github.com/matthias-bs/BresserWeatherSensorTTN) by Matthias Prinke +* [Lora-Serialization](https://github.com/thesolarnomad/lora-serialization) by Joscha Feth +* [ESP32Time](https://github.com/fbiego/ESP32Time) by Felix Biego +* [ESP32AnalogRead](https://github.com/madhephaestus/ESP32AnalogRead) by Kevin Harrington (madhephaestus) +* [Preferences](https://github.com/vshymanskyy/Preferences) by Volodymyr Shymanskyy diff --git a/examples/arduino_lorawan_esp32_example/arduino_lorawan_esp32_example.ino b/examples/arduino_lorawan_esp32_example/arduino_lorawan_esp32_example.ino index 1f78803..96e7042 100644 --- a/examples/arduino_lorawan_esp32_example/arduino_lorawan_esp32_example.ino +++ b/examples/arduino_lorawan_esp32_example/arduino_lorawan_esp32_example.ino @@ -25,8 +25,10 @@ // --------------------------------------- // MCCI Arduino Development Kit ADK 0.2.2 // MCCI LoRaWAN LMIC library 4.1.1 -// MCCI Arduino LoRaWAN Library 0.9.2 +// MCCI Arduino LoRaWAN Library 0.10.0 // LoRa_Serialization 3.2.1 +// ESP32Time 2.0.4 +// ESP32AnalogRead 0.2.1 (optional) // // // created: 07/2022 @@ -61,6 +63,25 @@ // 20230307 Changed cMyLoRaWAN to inherit from Arduino_LoRaWAN_network // instead of Arduino_LoRaWAN_ttn // Added Pin mappings for some common ESP32 LoRaWAN boards +// 20230903 Added time keeping with RTC and synchronization to network time +// Changed cMyLoRaWAN to inherit from Arduino_LoRaWAN_network +// instead of Arduino_LoRaWAN_ttn +// Changed LoRaWAN message size to actual payload size +// Implemented wake-up to fixed time scheme +// Added energy saving modes +// Modified DEBUG_PRINTF/DEBUG_PRINTF_TS macros to use +// Arduino logging functions +// Added setting of RTC via downlink +// Added remote configuration via LoRaWAN downlink +// Added configuration for several ESP32 boards +// Implemented downlink commands CMD_GET_DATETIME & CMD_GET_CONFIG +// 20230907 Added missing code for energy saving modes & generic ADC usage +// 20230910 Added pin definitions for FIREBEETLE_COVER_LORA +// 20231010 Replaced DEBUG_PRINTF/DEBUG_PRINTF_TS by macros log_i/../log_d/log_v +// Added support for Adafruit Feather RP2040 with RFM95W FeatherWing +// Renamed FIREBEETLE_COVER_LORA in FIREBEETLE_ESP32_COVER_LORA +// Improved config for Firebeetle Cover LoRa and +// Adafruit Feather ESP32-S2 (default battery voltage thresholds) // // Notes: // - After a successful transmission, the controller can go into deep sleep @@ -70,11 +91,25 @@ // the controller can go into deep sleep // (option FORCE_SLEEP) // - Timeout is defined in SLEEP_TIMEOUT_INITIAL and SLEEP_TIMEOUT_JOINED -// - The ESP32's RTC RAM is used to store information about the LoRaWAN -// network session; this speeds up the connection after a restart -// significantly +// - The ESP32's RTC RAM/the RP2040's Flash (via Preferences library) is used +// to store information about the LoRaWAN network session; +// this speeds up the connection after a restart significantly +// - RP2040: You must select a configuration with Flash file system, +// e.g. "Flash Size: 8 MB (Sketch: 8064 KB, FS: 128 KB)" +// - To enable Network Time Requests: +// #define LMIC_ENABLE_DeviceTimeReq 1 +// - settimeofday()/gettimeofday() must be used to access the ESP32's RTC time +// - Arduino ESP32 package has built-in time zone handling, see +// https://github.com/SensorsIot/NTP-time-for-ESP8266-and-ESP32/blob/master/NTP_Example/NTP_Example.ino +// - Apply fixes if using Arduino ESP32 board package v2.0.x +// - mcci-catena/arduino-lorawan#204 +// (https://github.com/mcci-catena/arduino-lorawan/pull/204) +// --> fixed in mcci-catena/arduino-lorawan v0.10.0 +// - mcci-catena/arduino-lmic#714 +// (https://github.com/mcci-catena/arduino-lmic/issues/714#issuecomment-822051171) // /////////////////////////////////////////////////////////////////////////////// +/*! \file arduino_lorawab_esp32_example.ino */ //--- Select LoRaWAN Network --- // The Things Network @@ -91,35 +126,169 @@ #include #include #include +#include +#include +#include "src/logging.h" + +#ifdef ARDUINO_ARCH_RP2040 + #include "src/pico_rtc/pico_rtc_utils.h" + #include +#endif //----------------------------------------------------------------------------- // // User Configuration // -// Enable debug mode (debug messages via serial port) -//#define _DEBUG_MODE_ +#if defined(ARDUINO_ESP32_DEV) + // Both variants below are based on DFRobot FireBeetle ESP32; + // which uses the rather unspecific "ARDUINO_ESP32_DEV" + + // Enable configuration for LoRaWAN Node board (https://github.com/matthias-bs/LoRaWAN_Node) + //#define LORAWAN_NODE + + // Enable configuration for + // https://wiki.dfrobot.com/FireBeetle_ESP32_IOT_Microcontroller(V3.0)__Supports_Wi-Fi_&_Bluetooth__SKU__DFR0478 + // https://wiki.dfrobot.com/FireBeetle_Covers_LoRa_Radio_868MHz_SKU_TEL0125 + #define FIREBEETLE_ESP32_COVER_LORA +#endif + +// NOTE: Add #define LMIC_ENABLE_DeviceTimeReq 1 +// in ~/Arduino/libraries/MCCI_LoRaWAN_LMIC_library/project_config/lmic_project_config.h +#if (not(LMIC_ENABLE_DeviceTimeReq)) + #warning "LMIC_ENABLE_DeviceTimeReq is not set - will not be able to retrieve network time!" +#endif + +// Battery voltage thresholds for energy saving + +// If SLEEP_EN is defined and battery voltage is <= BATTERY_WEAK [mV], MCU will sleep for SLEEP_INTERVAL_LONG +#if defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) || defined(ARDUINO_ADAFRUIT_FEATHER_ESP32S2) + // External voltage divider required + #pragma message("External voltage divider required for battery voltage measurement.") + #pragma message("Setting BATTERY_WEAK 0 (no power-saving).") + #define BATTERY_WEAK 0 +#elif defined(FIREBEETLE_ESP32_COVER_LORA) + #pragma message("On-board voltage divider must be enabled for battery voltage measurement (see schematic).") + #pragma message("Setting BATTERY_WEAK 0 (no power-saving).") + #define BATTERY_WEAK 0 +#else + #define BATTERY_WEAK 3500 +#endif + -// Enable sleep mode - sleep after successful transmission to TTN (recommended!) +// Go to sleep mode immediately after start if battery voltage is <= BATTERY_LOW [mV] +#define BATTERY_LOW 0 // example: 3200 + +/// Enable sleep mode - sleep after successful transmission to TTN (recommended!) #define SLEEP_EN -// If SLEEP_EN is defined, MCU will sleep for SLEEP_INTERVAL seconds after succesful transmission +/// If SLEEP_EN is defined, MCU will sleep for SLEEP_INTERVAL seconds after succesful transmission #define SLEEP_INTERVAL 360 -// Force deep sleep after a certain time, even if transmission was not completed +/// Long sleep interval, MCU will sleep for SLEEP_INTERVAL_LONG seconds if battery voltage is <= BATTERY_WEAK +#define SLEEP_INTERVAL_LONG 900 + +/// RTC to network time sync interval (in minutes) +#define CLOCK_SYNC_INTERVAL 24 * 60 + +/// Force deep sleep after a certain time, even if transmission was not completed #define FORCE_SLEEP -// During initialization (not joined), force deep sleep after SLEEP_TIMEOUT_INITIAL (if enabled) +// Force a new join procedure (instead of re-join) after encountering sleep timeout +#define FORCE_JOIN_AFTER_SLEEP_TIMEOUT + +/// During initialization (not joined), force deep sleep after SLEEP_TIMEOUT_INITIAL (if enabled) #define SLEEP_TIMEOUT_INITIAL 1800 -// If already joined, force deep sleep after SLEEP_TIMEOUT_JOINED seconds (if enabled) +/// If already joined, force deep sleep after SLEEP_TIMEOUT_JOINED seconds (if enabled) #define SLEEP_TIMEOUT_JOINED 600 +/// Additional timeout to be applied after joining if Network Time Request pending +#define SLEEP_TIMEOUT_EXTRA 300 + +// Enable battery / supply voltage measurement +#define ADC_EN + +// ADC for supply/battery voltage measurement +// Defaults: +// --------- +// FireBeetle ESP32: on-board connection to VB (with R10+R11 assembled) +// TTGO LoRa32: on-board connection to VBAT +// Adafruit Feather ESP32: on-board connection to VBAT +// Adafruit Feather ESP32-S2: no VBAT input circuit +// Adafruit Feather RP2040: no VBAT input circuit (connect external divider to A0) +#ifdef ADC_EN + #if defined(ARDUINO_TTGO_LoRa32_V1) || defined(ARDUINO_TTGO_LoRa32_V2) || defined(ARDUINO_TTGO_LoRa32_v21new) + #define PIN_ADC_IN 35 + #elif defined(ARDUINO_FEATHER_ESP32) + #define PIN_ADC_IN A13 + #elif defined(LORAWAN_NODE) || defined(FIREBEETLE_ESP32_COVER_LORA) + #define PIN_ADC_IN A0 + #elif defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) + #define PIN_ADC_IN A0 + #else + #define PIN_ADC_IN 34 + #endif +#endif + + +// Additional ADC pins (default: FireBeetle ESP32) +//#define PIN_ADC0_IN A0 +//#define PIN_ADC1_IN A1 +//#define PIN_ADC2_IN A2 +#ifdef LORAWAN_NODE + #define PIN_ADC3_IN A3 +#endif + +#ifdef PIN_ADC0_IN + // Voltage divider R1 / (R1 + R2) -> V_meas = V(R1 + R2); V_adc = V(R1) + const float ADC0_DIV = 0.5; + const uint8_t ADC0_SAMPLES = 10; +#endif + +#ifdef PIN_ADC1_IN + // Voltage divider R1 / (R1 + R2) -> V_meas = V(R1 + R2); V_adc = V(R1) + const float ADC1_DIV = 0.5; + const uint8_t ADC1_SAMPLES = 10; +#endif + +#ifdef ADC_EN + // Voltage divider R1 / (R1 + R2) -> V_meas = V(R1 + R2); V_adc = V(R1) + const float UBATT_DIV = 0.5; + const uint8_t UBATT_SAMPLES = 10; +#endif + +#ifdef PIN_ADC2_IN + // Voltage divider R1 / (R1 + R2) -> V_meas = V(R1 + R2); V_adc = V(R1) + const float ADC2_DIV = 0.5; + const uint8_t ADC2_SAMPLES = 10; +#endif + +#ifdef PIN_ADC3_IN + // Voltage divider R1 / (R1 + R2) -> V_meas = V(R1 + R2); V_adc = V(R1) + const float ADC3_DIV = 0.5; + const uint8_t ADC3_SAMPLES = 10; +#endif + +// LoRaWAN session info is stored in RTC RAM on ESP32 and in Preferences (flash) on RP2040 +#if defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) + #define SESSION_IN_PREFERENCES +#endif + +/// Preferences namespace +#define PREFS_NAMESPACE "MY_PREFS" /// LoRaWAN node preferences +#define PREFS_NAMESPACE_S "MY_PREFS_S" /// LoRaWAN session state + //----------------------------------------------------------------------------- // LoRa_Serialization #include +#if defined(ESP32) && defined(ADC_EN) + // ESP32 calibrated Analog Input Reading + #include +#endif + // Pin mappings for some common ESP32 LoRaWAN boards. // The ARDUINO_* defines are set by selecting the appropriate board (and borad variant, if applicable) in the Arduino IDE. // The default SPI port of the specific board will be used. @@ -151,8 +320,9 @@ #define PIN_LMIC_DIO1 LORA_D1 #define PIN_LMIC_DIO2 LORA_D2 -#elif defined(ARDUINO_heltec_wireless_stick) +#elif defined(ARDUINO_heltec_wireless_stick) || defined(ARDUINO_heltec_wifi_lora_32_V2) // https://github.com/espressif/arduino-esp32/blob/master/variants/heltec_wireless_stick/pins_arduino.h + // https://github.com/espressif/arduino-esp32/tree/master/variants/heltec_wifi_lora_32_V2/pins_ardiono.h #define PIN_LMIC_NSS SS #define PIN_LMIC_RST RST_LoRa #define PIN_LMIC_DIO0 DIO0 @@ -175,13 +345,35 @@ #define PIN_LMIC_DIO0 32 #define PIN_LMIC_DIO1 33 #define PIN_LMIC_DIO2 cMyLoRaWAN::lmic_pinmap::LMIC_UNUSED_PIN + #pragma message("NOT TESTED!!!") #pragma message("ARDUINO_ADAFRUIT_FEATHER_ESP32 defined; assuming RFM95W FeatherWing will be used") #pragma message("Required wiring: A to RST, B to DIO1, D to DIO0, E to CS") +#elif defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) + // Use pinning for Adafruit Feather RP2040 with RFM95W "FeatherWing" ADA3232 + // https://github.com/earlephilhower/arduino-pico/blob/master/variants/adafruit_feather/pins_arduino.h + #define PIN_LMIC_NSS 7 + #define PIN_LMIC_RST 11 + #define PIN_LMIC_DIO0 8 + #define PIN_LMIC_DIO1 10 + #define PIN_LMIC_DIO2 cMyLoRaWAN::lmic_pinmap::LMIC_UNUSED_PIN + #pragma message("ARDUINO_ADAFRUIT_FEATHER_RP2040 defined; assuming RFM95W FeatherWing will be used") + #pragma message("Required wiring: A to RST, B to DIO1, D to DIO0, E to CS") + +#elif defined(FIREBEETLE_ESP32_COVER_LORA) + // https://wiki.dfrobot.com/FireBeetle_ESP32_IOT_Microcontroller(V3.0)__Supports_Wi-Fi_&_Bluetooth__SKU__DFR0478 + // https://wiki.dfrobot.com/FireBeetle_Covers_LoRa_Radio_868MHz_SKU_TEL0125 + #define PIN_LMIC_NSS 27 // D4 + #define PIN_LMIC_RST 25 // D2 + #define PIN_LMIC_DIO0 26 // D3 + #define PIN_LMIC_DIO1 9 // D5 + #define PIN_LMIC_DIO2 cMyLoRaWAN::lmic_pinmap::LMIC_UNUSED_PIN + #pragma message("FIREBEETLE_ESP32_COVER_LORA defined; assuming FireBeetle ESP32 with FireBeetle Cover LoRa will be used") + #pragma message("Required wiring: D2 to RESET, D3 to DIO0, D4 to CS, D5 to DIO1") + #else // LoRaWAN_Node board // https://github.com/matthias-bs/LoRaWAN_Node - // (or anything else) #define PIN_LMIC_NSS 14 #define PIN_LMIC_RST 12 #define PIN_LMIC_DIO0 4 @@ -190,8 +382,14 @@ #endif -// Uplink message payload size (calculate from assignments to 'encoder' object) -const uint8_t PAYLOAD_SIZE = 8; +/// Enter your time zone (https://remotemonitoringsystems.ca/time-zone-abbreviations.php) +const char* TZ_INFO = "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00"; + + + +// Uplink message payload size +// The maximum allowed for all data rates is 51 bytes. +const uint8_t PAYLOAD_SIZE = 51; // RTC Memory Handling #define MAGIC1 (('m' << 24) | ('g' < 16) | ('c' << 8) | '1') @@ -199,29 +397,130 @@ const uint8_t PAYLOAD_SIZE = 8; #define EXTRA_INFO_MEM_SIZE 64 // Debug printing -#define DEBUG_PORT Serial -#if defined(_DEBUG_MODE_) - #define DEBUG_PRINTF(...) { DEBUG_PORT.printf(__VA_ARGS__); } - #define DEBUG_PRINTF_TS(...) { DEBUG_PORT.printf("%d ms: ", osticks2ms(os_getTime())); \ - DEBUG_PORT.printf(__VA_ARGS__); } -#else - #define DEBUG_PRINTF(...) {} - #define DEBUG_PRINTF_TS(...) {} -#endif - +// --------------- +// To enable debug mode (debug messages via serial port): +// Arduino IDE: Tools->Core Debug Level: "Debug|Verbose" +// or +// set CORE_DEBUG_LEVEL in BresserWeatherSensorTTNCfg.h +// Downlink messages +// ------------------ +// +// CMD_SET_SLEEP_INTERVAL +// (seconds) +// byte0: 0xA8 +// byte1: sleep_interval[15:8] +// byte2: sleep_interval[ 7:0] + +// CMD_SET_SLEEP_INTERVAL_LONG +// (seconds) +// byte0: 0xA9 +// byte1: sleep_interval_long[15:8] +// byte2: sleep_interval_long[ 7:0] +// +// CMD_GET_CONFIG +// byte0: 0xB1 +// +// CMD_GET_DATETIME +// byte0: 0x86 +// +// CMD_SET_DATETIME +// byte0: 0x88 +// byte1: unixtime[31:24] +// byte2: unixtime[23:16] +// byte3: unixtime[15: 8] +// byte4: unixtime[ 7: 0] +// +// Response uplink messages +// ------------------------- +// +// CMD_GET_DATETIME -> FPort=2 +// byte0: unixtime[31:24] +// byte1: unixtime[23:16] +// byte2: unixtime[15: 8] +// byte3: unixtime[ 7: 0] +// byte4: rtc_source[ 7: 0] +// +// CMD_GET_CONFIG -> FPort=3 +// byte0: sleep_interval[15:8] +// byte1: sleep_interval[ 7:0] +// byte2: sleep_interval_long[15:8] +// byte3: sleep_interval_long[ 7:0] + +#define CMD_SET_SLEEP_INTERVAL 0xA8 +#define CMD_SET_SLEEP_INTERVAL_LONG 0xA9 +#define CMD_GET_CONFIG 0xB1 +#define CMD_GET_DATETIME 0x86 +#define CMD_SET_DATETIME 0x88 + +void printDateTime(void); + /****************************************************************************\ | | The LoRaWAN object | \****************************************************************************/ +/*! + * \class cMyLoRaWAN + * + * \brief The LoRaWAN object - LoRaWAN protocol and session handling + */ class cMyLoRaWAN : public Arduino_LoRaWAN_network { public: cMyLoRaWAN() {}; using Super = Arduino_LoRaWAN_network; + + /*! + * \fn setup + * + * \brief Initialize cMyLoRaWAN object + */ void setup(); + + /*! + * \fn requestNetworkTime + * + * \brief Wrapper function for LMIC_requestNetworkTime() + */ + void requestNetworkTime(void); + + + + /*! + * \fn printSessionInfo + * + * \brief Print contents of session info data structure for debugging + * + * \param Info Session information data structure + */ + void printSessionInfo(const SessionInfo &Info); + + + /*! + * \fn printSessionState + * + * \brief Print contents of session state data structure for debugging + * + * \param State Session state data structure + */ + void printSessionState(const SessionState &State); + + /*! + * \fn doCfgUplink + * + * \brief Uplink configuration/status + */ + void doCfgUplink(void); + + bool isBusy(void) { + return m_fBusy; + } + +private: + bool m_fBusy; // set true while sending an uplink + protected: // you'll need to provide implementation for this. virtual bool GetOtaaProvisioningInfo(Arduino_LoRaWAN::OtaaProvisioningInfo*) override; @@ -248,6 +547,11 @@ protected: | \****************************************************************************/ +/*! + * \class cSensor + * + * \brief The sensor object - collect sensor data and schedule uplink + */ class cSensor { public: /// \brief the constructor. Deliberately does very little. @@ -259,43 +563,73 @@ public: uint16_t getVoltageBattery(void); uint16_t getVoltageSupply(void); - bool uplinkRequest(void) { +#ifdef ADC_EN + /*! + * \fn getVoltage + * + * \brief Get supply voltage (fixed ADC input circuit on FireBeetle ESP32 board) + * + * \returns Voltage [mV] + */ + uint16_t getVoltage(void); + + #if defined(ESP32) + /* + * \fn getVoltage + * + * \brief Get ADC voltage from specified port with averaging and application of divider + * + * \param adc ADC port + * + * \param samples No. of samples used in averaging + * + * \param divider Voltage divider + * + * \returns Voltage [mV] + */ + uint16_t getVoltage(ESP32AnalogRead &adc, uint8_t samples, float divider); + #else + uint16_t getVoltage(pin_size_t pin, uint8_t samples, float divider); + #endif +#endif + + /*! + * \fn uplinkRequest + * + * \brief Request uplink to LoRaWAN + */ + void uplinkRequest(void) { m_fUplinkRequest = true; }; - /// - /// \brief set up the sensor object - /// - /// \param uplinkPeriodMs optional uplink interval. If not specified, - /// transmit every six minutes. - /// + + /*! + * \brief set up the sensor object + * + * \param uplinkPeriodMs optional uplink interval. If not specified, + * transmit every six minutes. + */ void setup(std::uint32_t uplinkPeriodMs = 6 * 60 * 1000); - /// - /// \brief update sensor loop. - /// - /// \details - /// This should be called from the global loop(); it periodically - /// gathers and transmits sensor data. - /// + /*! + * \brief update sensor loop. + * + * \details + * This should be called from the global loop(); it periodically + * gathers and transmits sensor data. + */ void loop(); - // Example sensor status flags - bool data_ok; // has been reached (if FORCE_SLEEP is defined) -ostime_t sleepTimeout; +/// Uplink request - command received via downlink +uint8_t uplinkReq = 0; +/// Force sleep mode after sleepTimeout has been reached (if FORCE_SLEEP is defined) +ostime_t sleepTimeout; //! + +/// Seconds since the UTC epoch +uint32_t userUTCTime; + +/// RTC sync request flag - set (if due) in setup() / cleared in UserRequestNetworkTimeCb() +bool rtcSyncReq = false; + +/// Real time clock +ESP32Time rtc; /****************************************************************************\ | @@ -378,13 +769,13 @@ ostime_t sleepTimeout; // The following constants should be copied to secrets.h and configured appropriately // according to the settings from TTN Console - // deveui, little-endian (lsb first) + /// DeviceEUI, little-endian (lsb first) static const std::uint8_t deveui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - // appeui, little-endian (lsb first) + /// AppEUI, little-endian (lsb first) static const std::uint8_t appeui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - // appkey: just a string of bytes, sometimes referred to as "big endian". + /// AppKey: just a string of bytes, sometimes referred to as "big endian". static const std::uint8_t appkey[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; #endif @@ -395,30 +786,72 @@ ostime_t sleepTimeout; | \****************************************************************************/ +/// Arduino setup void setup() { +#if defined(ARDUINO_ARCH_RP2040) + // see pico-sdk/src/rp2_common/hardware_rtc/rtc.c + rtc_init(); + + // Restore variables and RTC after reset + time_t time_saved = watchdog_hw->scratch[0]; + datetime_t dt; + epoch_to_datetime(&time_saved, &dt); + + // Set HW clock (only used in sleep mode) + rtc_set_datetime(&dt); + + // Set SW clock + rtc.setTime(time_saved); + + runtimeExpired = ((watchdog_hw->scratch[1] & 1) == 1); + longSleep = ((watchdog_hw->scratch[1] & 2) == 2); + rtcLastClockSync = watchdog_hw->scratch[2]; + #endif + // set baud rate Serial.begin(115200); + delay(3000); + Serial.setDebugOutput(true); - // wait for serial to be ready - //while (! Serial) - // yield(); + // wait for serial to be ready - or timeout if USB is not connected delay(500); + #if defined(ARDUINO_ARCH_RP2040) + log_i("Time saved: %llu", time_saved); + #endif + + preferences.begin(PREFS_NAMESPACE, false); + prefs.sleep_interval = preferences.getUShort("sleep_int", SLEEP_INTERVAL); + log_d("Preferences: sleep_interval: %u s", prefs.sleep_interval); + prefs.sleep_interval_long = preferences.getUShort("sleep_int_long", SLEEP_INTERVAL_LONG); + log_d("Preferences: sleep_interval_long: %u s", prefs.sleep_interval_long); + preferences.end(); + sleepTimeout = sec2osticks(SLEEP_TIMEOUT_INITIAL); - DEBUG_PRINTF_TS("setup()\n"); + log_v("-"); + + // Set time zone + setenv("TZ", TZ_INFO, 1); + printDateTime(); + + // Check if clock was never synchronized or sync interval has expired + if ((rtcLastClockSync == 0) || ((rtc.getLocalEpoch() - rtcLastClockSync) > (CLOCK_SYNC_INTERVAL * 60))) { + log_i("RTC sync required"); + rtcSyncReq = true; + } // set up the log; do this first. myEventLog.setup(); - DEBUG_PRINTF("myEventlog.setup() - done\n"); + log_v("myEventlog.setup() - done"); // set up the sensors. mySensor.setup(); - DEBUG_PRINTF("mySensor.setup() - done\n"); + log_v("mySensor.setup() - done"); // set up lorawan. myLoRaWAN.setup(); - DEBUG_PRINTF("myLoRaWAN.setup() - done\n"); + log_v("myLoRaWAN.setup() - done"); mySensor.uplinkRequest(); } @@ -429,21 +862,40 @@ void setup() { | \****************************************************************************/ +/// Arduino execution loop void loop() { // the order of these is arbitrary, but you must poll them all. myLoRaWAN.loop(); mySensor.loop(); myEventLog.loop(); + if (uplinkReq != 0) { + myLoRaWAN.doCfgUplink(); + } + #ifdef SLEEP_EN + if (sleepReq & !rtcSyncReq) { + myLoRaWAN.Shutdown(); + prepareSleep(); + } + #endif + #ifdef FORCE_SLEEP if (os_getTime() > sleepTimeout) { - DEBUG_PRINTF_TS("Sleep timer expired!\n"); - DEBUG_PRINTF("Shutdown()\n"); runtimeExpired = true; myLoRaWAN.Shutdown(); - magicFlag1 = 0; - magicFlag2 = 0; - ESP.deepSleep(SLEEP_INTERVAL * 1000000); + #ifdef FORCE_JOIN_AFTER_SLEEP_TIMEOUT + // Force join (instead of re-join) + #if !defined(SESSION_IN_PREFERENCES) + magicFlag1 = 0; + magicFlag2 = 0; + #else + preferences.begin("MY_PREFS_S"); + preferences.clear(); + preferences.end(); + #endif + #endif + log_i("Sleep timer expired!"); + prepareSleep(); } #endif } @@ -454,19 +906,81 @@ void loop() { | \****************************************************************************/ +// Receive and process downlink messages +void ReceiveCb( + void *pCtx, + uint8_t uPort, + const uint8_t *pBuffer, + size_t nBuffer) { + + (void)pCtx; + uplinkReq = 0; + log_v("Port: %d", uPort); + char buf[255]; + *buf = '\0'; + + if (uPort > 0) { + for (size_t i = 0; i < nBuffer; i++) { + sprintf(&buf[strlen(buf)], "%02X ", pBuffer[i]); + } + log_v("Data: %s", buf); + + if ((pBuffer[0] == CMD_GET_DATETIME) && (nBuffer == 1)) { + log_d("Get date/time"); + uplinkReq = CMD_GET_DATETIME; + } + if ((pBuffer[0] == CMD_GET_CONFIG) && (nBuffer == 1)) { + log_d("Get config"); + uplinkReq = CMD_GET_CONFIG; + } + if ((pBuffer[0] == CMD_SET_DATETIME) && (nBuffer == 5)) { + + time_t set_time = pBuffer[4] | (pBuffer[3] << 8) | (pBuffer[2] << 16) | (pBuffer[1] << 24); + rtc.setTime(set_time); + rtcLastClockSync = rtc.getLocalEpoch(); + #if CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG + char tbuf[25]; + struct tm timeinfo; + + localtime_r(&set_time, &timeinfo); + strftime(tbuf, 25, "%Y-%m-%d %H:%M:%S", &timeinfo); + log_d("Set date/time: %s", tbuf); + #endif + } + if ((pBuffer[0] == CMD_SET_SLEEP_INTERVAL) && (nBuffer == 3)){ + prefs.sleep_interval = pBuffer[2] | (pBuffer[1] << 8); + log_d("Set sleep_interval: %u s", prefs.sleep_interval); + preferences.begin(PREFS_NAMESPACE, false); + preferences.putUShort("sleep_int", prefs.sleep_interval); + preferences.end(); + } + if ((pBuffer[0] == CMD_SET_SLEEP_INTERVAL_LONG) && (nBuffer == 3)){ + prefs.sleep_interval_long = pBuffer[2] | (pBuffer[1] << 8); + log_d("Set sleep_interval_long: %u s", prefs.sleep_interval_long); + preferences.begin(PREFS_NAMESPACE, false); + preferences.putUShort("sleep_int_long", prefs.sleep_interval_long); + preferences.end(); + } + } + if (uplinkReq == 0) { + sleepReq = true; + } +} + // our setup routine does the class setup and then registers an event handler so // we can see some interesting things void cMyLoRaWAN::setup() { // simply call begin() w/o parameters, and the LMIC's built-in // configuration for this board will be used. - bool res = this->Super::begin(myPinMap); - DEBUG_PRINTF("Arduino_LoRaWAN::begin(): %d\n", res); - - + this->Super::begin(myPinMap); + // LMIC_selectSubBand(0); LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100); + + this->SetReceiveBufferBufferCb(ReceiveCb); + this->RegisterListener( // use a lambda so we're "inside" the cMyLoRaWAN from public/private perspective [](void *pClientInfo, uint32_t event) -> void { @@ -482,9 +996,22 @@ cMyLoRaWAN::setup() { 0, // the print-out function [](cEventLog::EventNode_t const *pEvent) -> void { - Serial.print(F(" TX:")); - myEventLog.printCh(std::uint8_t(pEvent->getData(0))); - myEventLog.printRps(rps_t(pEvent->getData(1))); + #if CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO + //rps_t _rps = rps_t(pEvent->getData(1)); + //Serial.printf("rps (1): %02X\n", _rps); + uint8_t rps = pEvent->getData(1); + uint32_t tstamp = osticks2ms(pEvent->getTime()); + #endif + // see MCCI_Arduino_LoRaWAN_Library/src/lib/arduino_lorawan_cEventLog.cpp + log_i("TX @%lu ms: ch=%d rps=0x%02x (%s %s %s %s IH=%d)", + tstamp, + std::uint8_t(pEvent->getData(0)), + rps, + myEventLog.getSfName(rps), + myEventLog.getBwName(rps), + myEventLog.getCrName(rps), + myEventLog.getCrcName(rps), + unsigned(getIh(rps))); } ); } @@ -519,67 +1046,72 @@ cMyLoRaWAN::GetOtaaProvisioningInfo( void cMyLoRaWAN::NetJoin( void) { - DEBUG_PRINTF_TS("NetJoin()\n"); + log_v("-"); sleepTimeout = os_getTime() + sec2osticks(SLEEP_TIMEOUT_JOINED); + if (rtcSyncReq) { + // Allow additional time for completing Network Time Request + sleepTimeout += os_getTime() + sec2osticks(SLEEP_TIMEOUT_EXTRA); + } } // This method is called after transmission has been completed. -// If enabled, the controller goes into deep sleep mode now. void -cMyLoRaWAN::NetTxComplete( - void) { - DEBUG_PRINTF_TS("NetTxComplete()\n"); - #ifdef SLEEP_EN - DEBUG_PRINTF("Shutdown()\n"); - myLoRaWAN.Shutdown(); - ESP.deepSleep(SLEEP_INTERVAL * 1000000); - #endif +cMyLoRaWAN::NetTxComplete(void) { + log_v("-"); } -#ifdef _DEBUG_MODE_ // Print session info for debugging -void printSessionInfo(const cMyLoRaWAN::SessionInfo &Info) +void +cMyLoRaWAN::printSessionInfo(const SessionInfo &Info) { - Serial.printf("Tag:\t\t%d\n", Info.V1.Tag); - Serial.printf("Size:\t\t%d\n", Info.V1.Size); - Serial.printf("Rsv2:\t\t%d\n", Info.V1.Rsv2); - Serial.printf("Rsv3:\t\t%d\n", Info.V1.Rsv3); - Serial.printf("NetID:\t\t0x%08X\n", Info.V1.NetID); - Serial.printf("DevAddr:\t0x%08X\n", Info.V1.DevAddr); - Serial.printf("NwkSKey:\t"); - for (int i=0; i<15;i++) { - Serial.printf("%02X ", Info.V1.NwkSKey[i]); - } - Serial.printf("\n"); - Serial.printf("AppSKey:\t"); - for (int i=0; i<15;i++) { - Serial.printf("%02X ", Info.V1.AppSKey[i]); + log_v("Tag:\t\t%d", Info.V1.Tag); + log_v("Size:\t\t%d", Info.V1.Size); + log_v("Rsv2:\t\t%d", Info.V1.Rsv2); + log_v("Rsv3:\t\t%d", Info.V1.Rsv3); + log_v("NetID:\t\t0x%08X", Info.V1.NetID); + log_v("DevAddr:\t\t0x%08X", Info.V1.DevAddr); + if (CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG) { + char buf[64]; + *buf = '\0'; + for (int i=0; i<15;i++) { + sprintf(&buf[strlen(buf)], "%02X ", Info.V1.NwkSKey[i]); + } + log_v("NwkSKey:\t\t%s", buf); } - Serial.printf("\n"); + if (CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG) { + char buf[64]; + *buf = '\0'; + for (int i=0; i<15;i++) { + sprintf(&buf[strlen(buf)], "%02X ", Info.V1.AppSKey[i]); + } + log_v("AppSKey:\t\t%s", buf); + } } // Print session state for debugging -void printSessionState(const cMyLoRaWAN::SessionState &State) +void +cMyLoRaWAN::printSessionState(const SessionState &State) { - Serial.printf("Tag:\t\t%d\n", State.V1.Tag); - Serial.printf("Size:\t\t%d\n", State.V1.Size); - Serial.printf("Region:\t\t%d\n", State.V1.Region); - Serial.printf("LinkDR:\t\t%d\n", State.V1.LinkDR); - Serial.printf("FCntUp:\t\t%d\n", State.V1.FCntUp); - Serial.printf("FCntDown:\t%d\n", State.V1.FCntDown); - Serial.printf("gpsTime:\t%d\n", State.V1.gpsTime); - Serial.printf("globalAvail:\t%d\n", State.V1.globalAvail); - Serial.printf("Rx2Frequency:\t%d\n", State.V1.Rx2Frequency); - Serial.printf("PingFrequency:\t%d\n", State.V1.PingFrequency); - Serial.printf("Country:\t%d\n", State.V1.Country); - Serial.printf("LinkIntegrity:\t%d\n", State.V1.LinkIntegrity); + log_v("Tag:\t\t%d", State.V1.Tag); + log_v("Size:\t\t%d", State.V1.Size); + log_v("Region:\t\t%d", State.V1.Region); + log_v("LinkDR:\t\t%d", State.V1.LinkDR); + log_v("FCntUp:\t\t%d", State.V1.FCntUp); + log_v("FCntDown:\t\t%d", State.V1.FCntDown); + log_v("gpsTime:\t\t%d", State.V1.gpsTime); + log_v("globalAvail:\t%d", State.V1.globalAvail); + log_v("Rx2Frequency:\t%d", State.V1.Rx2Frequency); + log_v("PingFrequency:\t%d", State.V1.PingFrequency); + log_v("Country:\t\t%d", State.V1.Country); + log_v("LinkIntegrity:\t%d", State.V1.LinkIntegrity); // There is more in it... } -#endif + // Save Info to ESP32's RTC RAM // if not possible, just do nothing and make sure you return false // from NetGetSessionState(). +#if !defined(SESSION_IN_PREFERENCES) void cMyLoRaWAN::NetSaveSessionInfo( const SessionInfo &Info, @@ -592,92 +1124,137 @@ cMyLoRaWAN::NetSaveSessionInfo( rtcSavedNExtraInfo = nExtraInfo; memcpy(rtcSavedExtraInfo, pExtraInfo, nExtraInfo); magicFlag2 = MAGIC2; - DEBUG_PRINTF_TS("NetSaveSessionInfo()\n"); - #ifdef _DEBUG_MODE_ + log_v("-"); + printSessionInfo(Info); + } +#else + void + cMyLoRaWAN::NetSaveSessionInfo( + const SessionInfo &Info, + const uint8_t *pExtraInfo, + size_t nExtraInfo + ) { + preferences.begin("MY_PREFS_S"); + // Not used (read) + //preferences.putUChar("ITag", Info.V2.Tag); + //preferences.putUChar("ISize", Info.V2.Size); + preferences.putUInt("DevAddr", Info.V2.DevAddr); + preferences.putUInt("NetID", Info.V2.NetID); + preferences.putBytes("NwkSKey", Info.V2.NwkSKey, 16); + preferences.putBytes("AppSKey", Info.V2.AppSKey, 16); + (void)pExtraInfo; + (void)nExtraInfo; + // TODO: Save ExtraInfo? + preferences.end(); + log_v("-"); printSessionInfo(Info); - #endif -} - -/// Return saved session info (keys) from ESP32's RTC RAM -/// -/// if you have persistent storage, you should provide a function -/// that gets the saved session info from persistent storage, or -/// indicate that there isn't a valid saved session. Note that -/// the saved info is opaque to the higher level. -/// -/// \return true if \p sessionInfo was filled in, false otherwise. -/// -/// Note: -/// According to "Purpose of NetSaveSessionInfo #165" -/// (https://github.com/mcci-catena/arduino-lorawan/issues/165) -/// "GetSavedSessionInfo() is effectively useless and should probably be removed to avoid confusion." -/// sic! -#if false -bool -cMyLoRaWAN::GetSavedSessionInfo( - SessionInfo &sessionInfo, - uint8_t *pExtraSessionInfo, - size_t nExtraSessionInfo, - size_t *pnExtraSessionActual - ) { - if (magicFlag2 != MAGIC2) { - // if not provided, default zeros buf and returns false. - memset(&sessionInfo, 0, sizeof(sessionInfo)); - if (pExtraSessionInfo) { - memset(pExtraSessionInfo, 0, nExtraSessionInfo); - } - if (pnExtraSessionActual) { - *pnExtraSessionActual = 0; - } - DEBUG_PRINTF_TS("GetSavedSessionInfo() - failed\n"); - return false; - } else { - sessionInfo = rtcSavedSessionInfo; - if (pExtraSessionInfo) { - memcpy(pExtraSessionInfo, rtcSavedExtraInfo, nExtraSessionInfo); - } - if (pnExtraSessionActual) { - *pnExtraSessionActual = rtcSavedNExtraInfo; - } - DEBUG_PRINTF_TS("GetSavedSessionInfo() - o.k.\n"); - #ifdef _DEBUG_MODE_ - printSessionInfo(sessionInfo); - #endif - return true; } -} #endif // Save State in RTC RAM. Note that it's often the same; // often only the frame counters change. // [If not possible, just do nothing and make sure you return false // from NetGetSessionState().] +#if !defined(SESSION_IN_PREFERENCES) void cMyLoRaWAN::NetSaveSessionState(const SessionState &State) { rtcSavedSessionState = State; magicFlag1 = MAGIC1; - DEBUG_PRINTF_TS("NetSaveSessionState()\n"); - #ifdef _DEBUG_MODE_ - printSessionState(State); - #endif + log_v("-"); + printSessionState(State); } +#else + void + cMyLoRaWAN::NetSaveSessionState(const SessionState &State) { + preferences.begin("MY_PREFS_S"); + // All members are saved separately, because most of them will not change frequently + // and we want to avoid unnecessary wearing of the flash! + preferences.putUChar("Tag", State.V1.Tag); + preferences.putUChar("Size", State.V1.Size); + preferences.putUChar("Region", State.V1.Region); + preferences.putUChar("LinkDR", State.V1.LinkDR); + preferences.putUInt("FCntUp", State.V1.FCntUp); + preferences.putUInt("FCntDown", State.V1.FCntDown); + preferences.putUInt("gpsTime", State.V1.gpsTime); + preferences.putUInt("globalAvail", State.V1.globalAvail); + preferences.putUInt("Rx2Frequency", State.V1.Rx2Frequency); + preferences.putUInt("PingFrequency", State.V1.PingFrequency); + preferences.putUShort("Country", State.V1.Country); + preferences.putShort("LinkIntegrity", State.V1.LinkIntegrity); + preferences.putUChar("TxPower", State.V1.TxPower); + preferences.putUChar("Redundancy", State.V1.Redundancy); + preferences.putUChar("DutyCycle", State.V1.DutyCycle); + preferences.putUChar("Rx1DRoffset", State.V1.Rx1DRoffset); + preferences.putUChar("Rx2DataRate", State.V1.Rx2DataRate); + preferences.putUChar("RxDelay", State.V1.RxDelay); + preferences.putUChar("TxParam", State.V1.TxParam); + preferences.putUChar("BeaconChannel", State.V1.BeaconChannel); + preferences.putUChar("PingDr", State.V1.PingDr); + preferences.putUChar("MacRxParamAns", State.V1.MacRxParamAns); + preferences.putUChar("MacDlChannelAns", State.V1.MacDlChannelAns);; + preferences.putUChar("MacRxTimSetAns", State.V1.MacRxTimingSetupAns); + preferences.putBytes("Channels", &State.V1.Channels, sizeof(SessionChannelMask)); + preferences.end(); + } +#endif // Either fetch SessionState from somewhere and return true or... // return false, which forces a re-join. +#if !defined(SESSION_IN_PREFERENCES) + bool cMyLoRaWAN::NetGetSessionState(SessionState &State) { if (magicFlag1 == MAGIC1) { State = rtcSavedSessionState; - DEBUG_PRINTF_TS("NetGetSessionState() - o.k.\n"); - #ifdef _DEBUG_MODE_ - printSessionState(State); - #endif + log_d("o.k."); + printSessionState(State); return true; } else { - DEBUG_PRINTF_TS("NetGetSessionState() - failed\n"); + log_d("failed"); return false; } } +#else + bool + cMyLoRaWAN::NetGetSessionState(SessionState &State) { + + if (false == preferences.begin("MY_PREFS_S")) { + log_d("failed"); + return false; + } + // All members are saved separately, because most of them will not change frequently + // and we want to avoid unnecessary wearing of the flash! + State.V1.Tag = (SessionStateTag)preferences.getUChar("Tag"); + State.V1.Size = preferences.getUChar("Size"); + State.V1.Region = preferences.getUChar("Region"); + State.V1.LinkDR = preferences.getUChar("LinkDR"); + State.V1.FCntUp = preferences.getUInt("FCntUp"); + State.V1.FCntDown = preferences.getUInt("FCntDown"); + State.V1.gpsTime = preferences.getUInt("gpsTime"); + State.V1.globalAvail = preferences.getUInt("globalAvail"); + State.V1.Rx2Frequency = preferences.getUInt("Rx2Frequency"); + State.V1.PingFrequency = preferences.getUInt("PingFrequency"); + State.V1.Country = preferences.getUShort("Country"); + State.V1.LinkIntegrity = preferences.getShort("LinkIntegrity"); + State.V1.TxPower = preferences.getUChar("TxPower"); + State.V1.Redundancy = preferences.getUChar("Redundancy"); + State.V1.DutyCycle = preferences.getUChar("DutyCycle"); + State.V1.Rx1DRoffset = preferences.getUChar("Rx1DRoffset"); + State.V1.Rx2DataRate = preferences.getUChar("Rx2DataRate"); + State.V1.RxDelay = preferences.getUChar("RxDelay"); + State.V1.TxParam = preferences.getUChar("TxParam"); + State.V1.BeaconChannel = preferences.getUChar("BeaconChannel"); + State.V1.PingDr = preferences.getUChar("PingDr"); + State.V1.MacRxParamAns = preferences.getUChar("MacRxParamAns"); + State.V1.MacDlChannelAns = preferences.getUChar("MacDlChannelAns");; + State.V1.MacRxTimingSetupAns = preferences.getUChar("MacRxTimSetAns"); + preferences.getBytes("Channels", &State.V1.Channels, sizeof(SessionChannelMask)); + preferences.end(); + + printSessionState(State); + return true; + } +#endif // Get APB provisioning info - this is also used in OTAA after a succesful join. // If it can be provided in OTAA mode after a restart, no re-join is needed. @@ -694,35 +1271,288 @@ cMyLoRaWAN::GetAbpProvisioningInfo(AbpProvisioningInfo *pAbpInfo) { // uint32_t FCntUp; // uint32_t FCntDown; +#if !defined(SESSION_IN_PREFERENCES) if ((magicFlag1 != MAGIC1) || (magicFlag2 != MAGIC2)) { return false; } - DEBUG_PRINTF_TS("GetAbpProvisioningInfo()\n"); + log_v("-"); pAbpInfo->DevAddr = rtcSavedSessionInfo.V2.DevAddr; pAbpInfo->NetID = rtcSavedSessionInfo.V2.NetID; memcpy(pAbpInfo->NwkSKey, rtcSavedSessionInfo.V2.NwkSKey, 16); memcpy(pAbpInfo->AppSKey, rtcSavedSessionInfo.V2.AppSKey, 16); +#else + if (false == preferences.begin("MY_PREFS_S")) { + log_d("failed"); + return false; + } + #if defined(ARDUINO_ARCH_RP2040) + if (!watchdog_caused_reboot()) { + // Last reset was not caused by the watchdog, i.e. SW reset via restart(). + // Consequently, a power-on/brown-out detection or RUN pin reset occurred. + // We assume that stored session info is no longer valid and clear it. + // A new join will be faster than trying with stale session info and + // running into a timeout. + log_d("HW reset detected, deleting session info."); + preferences.clear(); + } + #endif + + log_v("-"); + pAbpInfo->DevAddr = preferences.getUInt("DevAddr"); + pAbpInfo->NetID = preferences.getUInt("NetID"); + preferences.getBytes("NwkSKey", pAbpInfo->NwkSKey, 16); + preferences.getBytes("AppSKey", pAbpInfo->AppSKey, 16); + preferences.end(); + #endif NetGetSessionState(state); pAbpInfo->FCntUp = state.V1.FCntUp; pAbpInfo->FCntDown = state.V1.FCntDown; - #ifdef _DEBUG_MODE_ - Serial.printf("NwkSKey:\t"); + if (CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG) { + char buf[64]; + + *buf = '\0'; for (int i=0; i<15;i++) { - Serial.printf("%02X ", pAbpInfo->NwkSKey[i]); + sprintf(&buf[strlen(buf)], "%02X ", pAbpInfo->NwkSKey[i]); } - Serial.printf("\n"); - Serial.printf("AppSKey:\t"); + log_v("NwkSKey:\t%s", buf); + + *buf = '\0'; for (int i=0; i<15;i++) { - Serial.printf("%02X ", pAbpInfo->AppSKey[i]); + sprintf(&buf[strlen(buf)], "%02X ", pAbpInfo->AppSKey[i]); } - Serial.printf("\n"); - Serial.printf("FCntUp: %d\n", state.V1.FCntUp); - #endif + log_v("AppSKey:\t%s", buf); + log_v("FCntUp:\t%d", state.V1.FCntUp); + } return true; } +/// Print date and time (i.e. local time) +void printDateTime(void) { + struct tm timeinfo; + char tbuf[25]; + + time_t tnow = rtc.getLocalEpoch(); + localtime_r(&tnow, &timeinfo); + strftime(tbuf, 25, "%Y-%m-%d %H:%M:%S", &timeinfo); + log_i("%s", tbuf); +} + +/// Determine sleep duration and enter Deep Sleep Mode +void prepareSleep(void) { + uint32_t sleep_interval = prefs.sleep_interval; + longSleep = false; + #ifdef ADC_EN + // Long sleep interval if battery is weak + if (mySensor.getVoltage() <= BATTERY_WEAK) { + sleep_interval = prefs.sleep_interval_long; + longSleep = true; + } + #endif + + // If the real time is available, align the wake-up time to the + // to next non-fractional multiple of sleep_interval past the hour + if (rtcLastClockSync) { + struct tm timeinfo; + time_t t_now = rtc.getLocalEpoch(); + localtime_r(&t_now, &timeinfo); + + sleep_interval = sleep_interval - ((timeinfo.tm_min * 60) % sleep_interval + timeinfo.tm_sec); + sleep_interval += 20; // Added extra 20-secs of sleep to allow for slow ESP32 RTC timers + } + + log_i("Shutdown() - sleeping for %lu s", sleep_interval); +#if defined(ESP32) + sleep_interval += 20; // Added extra 20-secs of sleep to allow for slow ESP32 RTC timers + ESP.deepSleep(sleep_interval * 1000000LL); +#else + time_t t_now = rtc.getLocalEpoch(); + datetime_t dt; + epoch_to_datetime(&t_now, &dt); + rtc_set_datetime(&dt); + sleep_us(64); + pico_sleep(sleep_interval); + + // Save variables to be retained after reset + watchdog_hw->scratch[2] = rtcLastClockSync; + + if (runtimeExpired) { + watchdog_hw->scratch[1] |= 1; + } else { + watchdog_hw->scratch[1] &= ~1; + } + if (longSleep) { + watchdog_hw->scratch[1] |= 2; + } else { + watchdog_hw->scratch[1] &= ~2; + } + + // Save the current time, because RTC will be reset (SIC!) + rtc_get_datetime(&dt); + time_t now = datetime_to_epoch(&dt, NULL); + watchdog_hw->scratch[0] = now; + log_i("Now: %llu", now); + + rp2040.restart(); + #endif +} + +/** + * \fn UserRequestNetworkTimeCb + * + * \brief Callback function for setting RTC from LoRaWAN network time + * + * \param pVoidUserUTCTime user supplied buffer for UTC time + * + * \param flagSuccess flag indicating if network time request was succesful + */ +void UserRequestNetworkTimeCb(void *pVoidUserUTCTime, int flagSuccess) { + // Explicit conversion from void* to uint32_t* to avoid compiler errors + uint32_t *pUserUTCTime = (uint32_t *) pVoidUserUTCTime; + + // A struct that will be populated by LMIC_getNetworkTimeReference. + // It contains the following fields: + // - tLocal: the value returned by os_GetTime() when the time + // request was sent to the gateway, and + // - tNetwork: the seconds between the GPS epoch and the time + // the gateway received the time request + lmic_time_reference_t lmicTimeReference; + + if (flagSuccess != 1) { + // Most likely the service is not provided by the gateway. No sense in trying again... + log_i("Request network time didn't succeed"); + rtcSyncReq = false; + return; + } + + // Populate "lmic_time_reference" + flagSuccess = LMIC_getNetworkTimeReference(&lmicTimeReference); + if (flagSuccess != 1) { + log_i("LMIC_getNetworkTimeReference didn't succeed"); + return; + } + + // Update userUTCTime, considering the difference between the GPS and UTC + // epoch, and the leap seconds + *pUserUTCTime = lmicTimeReference.tNetwork + 315964800; + + // Add the delay between the instant the time was transmitted and + // the current time + + // Current time, in ticks + ostime_t ticksNow = os_getTime(); + // Time when the request was sent, in ticks + ostime_t ticksRequestSent = lmicTimeReference.tLocal; + uint32_t requestDelaySec = osticks2ms(ticksNow - ticksRequestSent) / 1000; + *pUserUTCTime += requestDelaySec; + + // Update the system time with the time read from the network + rtc.setTime(*pUserUTCTime); + + // Save clock sync timestamp and clear flag + rtcLastClockSync = rtc.getLocalEpoch(); + rtcSyncReq = false; + log_d("RTC sync completed"); + printDateTime(); +} + +void +cMyLoRaWAN::requestNetworkTime(void) { + LMIC_requestNetworkTime(UserRequestNetworkTimeCb, &userUTCTime); +} + + +void +cMyLoRaWAN::doCfgUplink(void) { + // if busy uplinking, just skip + if (this->m_fBusy || mySensor.isBusy()) { + //log_d("busy"); + return; + } + // if LMIC is busy, just skip + //if (LMIC.opmode & (OP_POLL | OP_TXDATA | OP_TXRXPEND)) { + // log_v("LMIC.opmode: 0x%02X", LMIC.opmode); + // return; + //} + if (!GetTxReady()) + return; + + log_d("--- Uplink Configuration/Status ---"); + + uint8_t uplink_payload[5]; + uint8_t port; + + // + // Encode data as byte array for LoRaWAN transmission + // + LoraEncoder encoder(uplink_payload); + + if (uplinkReq == CMD_GET_DATETIME) { + log_d("Date/Time"); + port = 2; + time_t t_now = rtc.getLocalEpoch(); + encoder.writeUint8((t_now >> 24) & 0xff); + encoder.writeUint8((t_now >> 16) & 0xff); + encoder.writeUint8((t_now >> 8) & 0xff); + encoder.writeUint8( t_now & 0xff); + + // time source & status, see below + // + // bits 0..3 time source + // 0x00 = GPS + // 0x01 = RTC + // 0x02 = LORA + // 0x03 = unsynched + // 0x04 = set (source unknown) + // + // bits 4..7 esp32 sntp time status (not used) + // TODO add flags for succesful LORA time sync/manual sync + encoder.writeUint8((rtcSyncReq) ? 0x03 : 0x02); + } else if (uplinkReq) { + log_d("Config"); + port = 3; + encoder.writeUint8(prefs.sleep_interval >> 8); + encoder.writeUint8(prefs.sleep_interval & 0xFF); + encoder.writeUint8(prefs.sleep_interval_long >> 8); + encoder.writeUint8(prefs.sleep_interval_long & 0xFF); + } else { + log_v(""); + return; + } + + this->m_fBusy = true; + log_v("Trying SendBuffer: port=%d, size=%d", port, encoder.getLength()); + + for (int i=0; iSendBuffer( + uplink_payload, + encoder.getLength(), + // this is the completion function: + [](void *pClientData, bool fSuccess) -> void { + (void)fSuccess; + auto const pThis = (cMyLoRaWAN *)pClientData; + pThis->m_fBusy = false; + uplinkReq = 0; + log_v("Sending successful"); + }, + (void *)this, + /* confirmed */ true, + /* port */ port + )) { + // sending failed; callback has not been called and will not + // be called. Reset busy flag. + this->m_fBusy = false; + uplinkReq = 0; + log_v("Sending failed"); + } +} + /****************************************************************************\ | @@ -736,6 +1566,21 @@ cSensor::setup(std::uint32_t uplinkPeriodMs) { this->m_uplinkPeriodMs = uplinkPeriodMs; this->m_tReference = millis(); + #ifdef ADC_EN +#ifdef ESP32 + // Use ADC with PIN_ADC_IN + adc.attach(PIN_ADC_IN); +#elif defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) + // Set resolution to 12 bits + analogReadResolution(12); + #endif + + if (getVoltage() <= BATTERY_LOW) { + log_i("Battery low!"); + prepareSleep(); + } + #endif + // Initialize your sensors here... } @@ -760,31 +1605,61 @@ cSensor::loop(void) { } } +#ifdef ADC_EN // -// Get battery voltage (Stub) +// Get supply / battery voltage // uint16_t -cSensor::getVoltageBattery(void) +cSensor::getVoltage(void) { - const uint16_t voltage = 3850; + float voltage_raw = 0; + for (uint8_t i=0; i < UBATT_SAMPLES; i++) { + #ifdef ESP32 + voltage_raw += float(adc.readMiliVolts()); + #else + voltage_raw += float(analogRead(PIN_ADC_IN)) / 4095.0 * 3300; + #endif + } + uint16_t voltage = int(voltage_raw / UBATT_SAMPLES / UBATT_DIV); - DEBUG_PRINTF("Battery Voltage = %dmV\n", voltage); + log_d("Voltage = %dmV", voltage); return voltage; } // -// Get supply voltage (Stub) +// Get supply / battery voltage // +#if defined(ESP32) uint16_t -cSensor::getVoltageSupply(void) +cSensor::getVoltage(ESP32AnalogRead &adc, uint8_t samples, float divider) { - const uint16_t voltage = 3300; + float voltage_raw = 0; + for (uint8_t i=0; i < samples; i++) { + voltage_raw += float(adc.readMiliVolts()); + } + uint16_t voltage = int(voltage_raw / samples / divider); - DEBUG_PRINTF("Supply Voltage = %dmV\n", voltage); + log_d("Voltage = %dmV", voltage); return voltage; } +#else +uint16_t +cSensor::getVoltage(pin_size_t pin, uint8_t samples, float divider) + { + float voltage_raw = 0; + for (uint8_t i=0; i < samples; i++) { + voltage_raw += float(analogRead(pin)) / 4095.0 * 3.3; + } + uint16_t voltage = int(voltage_raw / samples / divider); + + log_d("Voltage = %dmV", voltage); + + return voltage; +} +#endif // !defined(ESP32) +#endif // ADC_EN // // Get temperature (Stub) @@ -794,20 +1669,20 @@ cSensor::getTemperature(void) { const float temperature = 16.4; - DEBUG_PRINTF("Outdoor Air Temperature = %.1f°C\n", temperature); + log_i("Outdoor Air Temperature (dummy) = %.1f°C", temperature); return temperature; } // -// Get temperature (Stub) +// Get humidity (Stub) // uint8_t cSensor::getHumidity(void) { const uint8_t humidity = 42; - DEBUG_PRINTF("Outdoor Humidity = %d%%\n", humidity); + log_i("Outdoor Humidity (dummy) = %d%%", humidity); return humidity; } @@ -819,40 +1694,55 @@ cSensor::getHumidity(void) void cSensor::doUplink(void) { // if busy uplinking, just skip - if (this->m_fBusy) { - DEBUG_PRINTF_TS("doUplink(): busy\n"); + if (this->m_fBusy || myLoRaWAN.isBusy()) { + log_d("busy"); return; } // if LMIC is busy, just skip if (LMIC.opmode & (OP_POLL | OP_TXDATA | OP_TXRXPEND)) { - DEBUG_PRINTF_TS("doUplink(): other operation in progress\n"); + log_d("other operation in progress"); return; } + // + // Request time and date + // + if (rtcSyncReq) + myLoRaWAN.requestNetworkTime(); + // Call sensor data function stubs - temperature_deg_c = getTemperature(); - humidity_percent = getHumidity(); - battery_voltage_v = getVoltageBattery(); - supply_voltage_v = getVoltageSupply(); - + float temperature_deg_c = getTemperature(); + uint8_t humidity_percent = getHumidity(); + uint16_t battery_voltage_v = getVoltage(); + uint16_t supply_voltage_v = 0; + + #if defined(PIN_ADC0_IN) + #if defined(ESP32) + supply_voltage_v = getVoltage(adc0, PIN_ADC0_SAMPLES, PIN_ADC0_DIV) + #else + supply_voltage_v = getVoltage(PIN_ADC0_IN, PIN_ADC0_SAMPLES, PIN_ADC0_DIV); + #endif + #endif + // Status flags (Examples) - data_ok = true; // validation on sensor data - battery_ok = true; // sensor battery status - - DEBUG_PRINTF("--- Uplink Data ---\n"); - DEBUG_PRINTF("Air Temperature: % 3.1f °C\n", temperature_deg_c); - DEBUG_PRINTF("Humidity: %2d %%\n", humidity_percent); - DEBUG_PRINTF("Supply Voltage: %4d mV\n", supply_voltage_v); - DEBUG_PRINTF("Battery Voltage: %4d mV\n", battery_voltage_v); - DEBUG_PRINTF("Status:\n"); - DEBUG_PRINTF(" battery_ok: %d\n", battery_ok); - DEBUG_PRINTF(" data_ok: %d\n", data_ok); - DEBUG_PRINTF(" runtimeExpired: %d\n", runtimeExpired); - DEBUG_PRINTF("\n"); + bool data_ok = true; // validation of sensor data + bool battery_ok = true; // sensor battery status + + log_i("--- Uplink Data ---"); + log_i("Air Temperature: % 3.1f °C", temperature_deg_c); + log_i("Humidity: %2d %%", humidity_percent); + log_i("Supply Voltage: %4d mV", supply_voltage_v); + log_i("Battery Voltage: %4d mV", battery_voltage_v); + log_i("Status:"); + log_i(" battery_ok: %d", battery_ok); + log_i(" data_ok: %d", data_ok); + log_i(" runtimeExpired: %d\n", runtimeExpired); + + // Serialize data into byte array // NOTE: - // For TTN MQTT integration, ttn_decoder.js must be adjusted accordingly + // For TTN MQTT integration, ttn_uplink_formatter.js must be adjusted accordingly LoraEncoder encoder(loraData); encoder.writeBitmap(false, false, false, false, false, runtimeExpired, @@ -866,10 +1756,12 @@ cSensor::doUplink(void) { this->m_fBusy = true; +// Schedule transmission if (! myLoRaWAN.SendBuffer( - loraData, sizeof(loraData), + loraData, encoder.getLength(), // this is the completion function: - [](void *pClientData, bool fSucccess) -> void { + [](void *pClientData, bool fSuccess) -> void { + (void)fSuccess; auto const pThis = (cSensor *)pClientData; pThis->m_fBusy = false; }, diff --git a/examples/arduino_lorawan_esp32_example/src/logging.h b/examples/arduino_lorawan_esp32_example/src/logging.h new file mode 100644 index 0000000..386da53 --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/src/logging.h @@ -0,0 +1,103 @@ +/////////////////////////////////////////////////////////////////////////////// +// logging.h +// +// Replacement for +// https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/esp32-hal-log.h +// on RP2040 +// +// - DEBUG_RP2040_PORT is set in Arduino IDE: +// Tools->Debug port: "|||" +// - CORE_DEBUG_LEVEL has to be set manually below +// +// created: 09/2023 +// +// +// MIT License +// +// Copyright (c) 2023 Matthias Prinke +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// +// History: +// +// 20230927 Created from BresserWeatherSensorReceiver +// 20231004 Added function names and line numbers to ESP8266/RP2040 debug logging +// 20231005 Allowed re-definition of CORE_DEBUG_LEVEL and log_* macros +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#ifndef LOGGING_H +#define LOGGING_H + +#if defined(ARDUINO_ARCH_RP2040) + + #if defined(DEBUG_RP2040_PORT) + #define DEBUG_PORT DEBUG_RP2040_PORT + #endif + + #define ARDUHAL_LOG_LEVEL_NONE 0 + #define ARDUHAL_LOG_LEVEL_ERROR 1 + #define ARDUHAL_LOG_LEVEL_WARN 2 + #define ARDUHAL_LOG_LEVEL_INFO 3 + #define ARDUHAL_LOG_LEVEL_DEBUG 4 + #define ARDUHAL_LOG_LEVEL_VERBOSE 5 + + // '#undef' allows to change a previous definition from WeatherSensor.h + #undef log_e + #undef log_w + #undef log_i + #undef log_d + #undef log_v + + // Set desired level here! + #undef CORE_DEBUG_LEVEL + #define CORE_DEBUG_LEVEL ARDUHAL_LOG_LEVEL_INFO + + #if defined(DEBUG_PORT) && CORE_DEBUG_LEVEL > ARDUHAL_LOG_LEVEL_NONE + #define log_e(...) { DEBUG_PORT.printf("%s(), l.%d: ", __func__, __LINE__); DEBUG_PORT.println(); } + #else + #define log_e(...) {} + #endif + #if defined(DEBUG_PORT) && CORE_DEBUG_LEVEL > ARDUHAL_LOG_LEVEL_ERROR + #define log_w(...) { DEBUG_PORT.printf("%s(), l.%d: ", __func__, __LINE__); DEBUG_PORT.printf(__VA_ARGS__); DEBUG_PORT.println(); } + #else + #define log_w(...) {} + #endif + #if defined(DEBUG_PORT) && CORE_DEBUG_LEVEL > ARDUHAL_LOG_LEVEL_WARN + #define log_i(...) { DEBUG_PORT.printf("%s(), l.%d: ", __func__, __LINE__); DEBUG_PORT.printf(__VA_ARGS__); DEBUG_PORT.println(); } + #else + #define log_i(...) {} + #endif + #if defined(DEBUG_PORT) && CORE_DEBUG_LEVEL > ARDUHAL_LOG_LEVEL_INFO + #define log_d(...) { DEBUG_PORT.printf("%s(), l.%d: ", __func__, __LINE__); DEBUG_PORT.printf(__VA_ARGS__); DEBUG_PORT.println(); } + #else + #define log_d(...) {} + #endif + #if defined(DEBUG_PORT) && CORE_DEBUG_LEVEL > ARDUHAL_LOG_LEVEL_DEBUG + #define log_v(...) { DEBUG_PORT.printf("%s(), l.%d: ", __func__, __LINE__); DEBUG_PORT.printf(__VA_ARGS__); DEBUG_PORT.println(); } + #else + #define log_v(...) {} + #endif + +#endif // defined(ARDUINO_ARCH_RP2040) +#endif // LOGGING_H \ No newline at end of file diff --git a/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rosc.h b/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rosc.h new file mode 100644 index 0000000..625f2bd --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rosc.h @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#if defined(ARDUINO_ARCH_RP2040) + +#ifndef _HARDWARE_ROSC_H_ +#define _HARDWARE_ROSC_H_ + +#include "pico.h" +#include "hardware/structs/rosc.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** \file rosc.h + * \defgroup hardware_rosc hardware_rosc + * + * Ring Oscillator (ROSC) API + * + * A Ring Oscillator is an on-chip oscillator that requires no external crystal. Instead, the output is generated from a series of + * inverters that are chained together to create a feedback loop. RP2040 boots from the ring oscillator initially, meaning the + * first stages of the bootrom, including booting from SPI flash, will be clocked by the ring oscillator. If your design has a + * crystal oscillator, you’ll likely want to switch to this as your reference clock as soon as possible, because the frequency is + * more accurate than the ring oscillator. + */ + +/*! \brief Set frequency of the Ring Oscillator + * \ingroup hardware_rosc + * + * \param code The drive strengths. See the RP2040 datasheet for information on this value. + */ +void rosc_set_freq(uint32_t code); + +/*! \brief Set range of the Ring Oscillator + * \ingroup hardware_rosc + * + * Frequency range. Frequencies will vary with Process, Voltage & Temperature (PVT). + * Clock output will not glitch when changing the range up one step at a time. + * + * \param range 0x01 Low, 0x02 Medium, 0x03 High, 0x04 Too High. + */ +void rosc_set_range(uint range); + +/*! \brief Disable the Ring Oscillator + * \ingroup hardware_rosc + * + */ +void rosc_disable(void); + +/*! \brief Enable the Ring Oscillator + * \ingroup hardware_rosc + * + */ +void rosc_enable(void); + +/*! \brief Put Ring Oscillator in to dormant mode. + * \ingroup hardware_rosc + * + * The ROSC supports a dormant mode,which stops oscillation until woken up up by an asynchronous interrupt. + * This can either come from the RTC, being clocked by an external clock, or a GPIO pin going high or low. + * If no IRQ is configured before going into dormant mode the ROSC will never restart. + * + * PLLs should be stopped before selecting dormant mode. + */ +void rosc_set_dormant(void); + +// FIXME: Add doxygen + +uint32_t next_rosc_code(uint32_t code); + +uint rosc_find_freq(uint32_t low_mhz, uint32_t high_mhz); + +void rosc_set_div(uint32_t div); + +inline static void rosc_clear_bad_write(void) { + hw_clear_bits(&rosc_hw->status, ROSC_STATUS_BADWRITE_BITS); +} + +inline static bool rosc_write_okay(void) { + return !(rosc_hw->status & ROSC_STATUS_BADWRITE_BITS); +} + +inline static void rosc_write(io_rw_32 *addr, uint32_t value) { + rosc_clear_bad_write(); + assert(rosc_write_okay()); + *addr = value; + assert(rosc_write_okay()); +}; + +#ifdef __cplusplus +} +#endif + +#endif +#endif /* ARDUINO_ARCH_RP2040 */ diff --git a/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rtc_utils.cpp b/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rtc_utils.cpp new file mode 100644 index 0000000..2f3be2c --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rtc_utils.cpp @@ -0,0 +1,161 @@ +/////////////////////////////////////////////////////////////////////////////// +// pico_rtc_utils.cpp +// +// RTC utility functions for RP2040 +// +// Sleep/wakeup scheme based on +// https://github.com/lyusupov/SoftRF/tree/master/software/firmware/source/libraries/RP2040_Sleep +// by Linar Yusupov +// +// Using code from pico-extras: +// https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/pico_sleep/include/pico/sleep.h +// https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/pico_sleep/sleep.c +// https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/hardware_rosc/include/hardware/rosc.h +// https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/hardware_rosc/rosc.c +// +// created: 10/2023 +// +// +// MIT License +// +// Copyright (c) 2023 Matthias Prinke +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// +// History: +// +// 20231006 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// +#if defined(ARDUINO_ARCH_RP2040) + +#include "pico_rtc_utils.h" + +struct tm *datetime_to_tm(datetime_t *dt, struct tm *ti) +{ + ti->tm_sec = dt->sec; + ti->tm_min = dt->min; + ti->tm_hour = dt->hour; + ti->tm_mday = dt->day; + ti->tm_mon = dt->month - 1; + ti->tm_year = dt->year - 1900; + ti->tm_wday = dt->dotw; + + return ti; +} + +datetime_t *tm_to_datetime(struct tm *ti, datetime_t *dt) +{ + dt->sec = ti->tm_sec; + dt->min = ti->tm_min; + dt->hour = ti->tm_hour; + dt->day = ti->tm_mday; + dt->month = ti->tm_mon + 1; + dt->year = ti->tm_year + 1900; + dt->dotw = ti->tm_wday; + + return dt; +} + +void print_dt(datetime_t dt) { + log_i("%4d-%02d-%02d %02d:%02d:%02d", dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec); +} + +void print_tm(struct tm ti) { + log_i("%4d-%02d-%02d %02d:%02d:%02d", ti.tm_year+1900, ti.tm_mon+1, ti.tm_mday, ti.tm_hour, ti.tm_min, ti.tm_sec); +} + +time_t datetime_to_epoch(datetime_t *dt, time_t *epoch) { + struct tm ti; + datetime_to_tm(dt, &ti); + + // Apply daylight saving time according to timezone and date + ti.tm_isdst = -1; + + // Convert to epoch + time_t _epoch = mktime(&ti); + + if (epoch) { + *epoch = _epoch; + } + + return _epoch; +} + +datetime_t *epoch_to_datetime(time_t *epoch, datetime_t *dt) { + struct tm ti; + + // Apply daylight saving time according to timezone and date + ti.tm_isdst = -1; + + // Convert epoch to struct tm + localtime_r(epoch, &ti); + + // Convert struct tm to datetime_t + tm_to_datetime(&ti, dt); + + return dt; +} + +// Sleep for seconds +void pico_sleep(unsigned duration) { + datetime_t dt; + rtc_get_datetime(&dt); + log_i("RTC time:"); + print_dt(dt); + + time_t now; + datetime_to_epoch(&dt, &now); + + // Add sleep_duration + time_t wakeup = now + duration; + + epoch_to_datetime(&wakeup, &dt); + + log_i("Wakeup time:"); + print_dt(dt); + + Serial.flush(); + Serial1.end(); + Serial2.end(); + Serial.end(); + + // From + // https://github.com/lyusupov/SoftRF/tree/master/software/firmware/source/libraries/RP2040_Sleep + // also see src/pico_rtc + // --8<----- + #if defined(USE_TINYUSB) + // Disable USB + USBDevice.detach(); + #endif /* USE_TINYUSB */ + + sleep_run_from_xosc(); + + sleep_goto_sleep_until(&dt, NULL); + + // back from dormant state + rosc_enable(); + clocks_init(); + // --8<----- +} +#endif \ No newline at end of file diff --git a/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rtc_utils.h b/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rtc_utils.h new file mode 100644 index 0000000..5b607aa --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_rtc_utils.h @@ -0,0 +1,73 @@ +/////////////////////////////////////////////////////////////////////////////// +// pico_rtc_utils.h +// +// RTC utility functions for RP2040 +// +// Sleep/wakeup scheme based on +// https://github.com/lyusupov/SoftRF/tree/master/software/firmware/source/libraries/RP2040_Sleep +// by Linar Yusupov +// +// Using code from pico-extras: +// https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/pico_sleep/include/pico/sleep.h +// https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/pico_sleep/sleep.c +// https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/hardware_rosc/include/hardware/rosc.h +// https://github.com/raspberrypi/pico-extras/blob/master/src/rp2_common/hardware_rosc/rosc.c +// +// created: 10/2023 +// +// +// MIT License +// +// Copyright (c) 2023 Matthias Prinke +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// +// History: +// +// 20231006 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// +#if defined(ARDUINO_ARCH_RP2040) +#include +#include +#include +#include +#include "pico_sleep.h" +#include "pico_rosc.h" +#include "../logging.h" + +#ifndef PICO_RTC_UTILS_H +#define PICO_RTC_UTILS_H + +struct tm *datetime_to_tm(datetime_t *dt, struct tm *ti); +datetime_t *tm_to_datetime(struct tm *ti, datetime_t *dt); +time_t datetime_to_epoch(datetime_t *dt, time_t *epoch); +datetime_t *epoch_to_datetime(time_t *epoch, datetime_t *dt); + +void print_dt(datetime_t dt); +void print_tm(struct tm ti); + +void pico_sleep(unsigned duration); + +#endif // PICO_RTC_UTILS_H +#endif // defined(ARDUINO_ARCH_RP2040) diff --git a/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_sleep.h b/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_sleep.h new file mode 100644 index 0000000..8d4f3b9 --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/src/pico_rtc/pico_sleep.h @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#if defined(ARDUINO_ARCH_RP2040) + +#ifndef _PICO_SLEEP_H_ +#define _PICO_SLEEP_H_ + +#include "pico.h" +#include "hardware/rtc.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** \file sleep.h + * \defgroup hardware_sleep hardware_sleep + * + * Lower Power Sleep API + * + * The difference between sleep and dormant is that ALL clocks are stopped in dormant mode, + * until the source (either xosc or rosc) is started again by an external event. + * In sleep mode some clocks can be left running controlled by the SLEEP_EN registers in the clocks + * block. For example you could keep clk_rtc running. Some destinations (proc0 and proc1 wakeup logic) + * can't be stopped in sleep mode otherwise there wouldn't be enough logic to wake up again. + * + * \subsection sleep_example Example + * \addtogroup hardware_sleep + * \include hello_sleep.c + + */ +typedef enum { + DORMANT_SOURCE_NONE, + DORMANT_SOURCE_XOSC, + DORMANT_SOURCE_ROSC +} dormant_source_t; + +/*! \brief Set all clock sources to the the dormant clock source to prepare for sleep. + * \ingroup hardware_sleep + * + * \param dormant_source The dormant clock source to use + */ +void sleep_run_from_dormant_source(dormant_source_t dormant_source); + +/*! \brief Set the dormant clock source to be the crystal oscillator + * \ingroup hardware_sleep + */ +static inline void sleep_run_from_xosc(void) { + sleep_run_from_dormant_source(DORMANT_SOURCE_XOSC); +} + +/*! \brief Set the dormant clock source to be the ring oscillator + * \ingroup hardware_sleep + */ +static inline void sleep_run_from_rosc(void) { + sleep_run_from_dormant_source(DORMANT_SOURCE_ROSC); +} + +/*! \brief Send system to sleep until the specified time + * \ingroup hardware_sleep + * + * One of the sleep_run_* functions must be called prior to this call + * + * \param t The time to wake up + * \param callback Function to call on wakeup. + */ +void sleep_goto_sleep_until(datetime_t *t, rtc_callback_t callback); + +/*! \brief Send system to sleep until the specified GPIO changes + * \ingroup hardware_sleep + * + * One of the sleep_run_* functions must be called prior to this call + * + * \param gpio_pin The pin to provide the wake up + * \param edge true for leading edge, false for trailing edge + * \param high true for active high, false for active low + */ +void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high); + +/*! \brief Send system to sleep until a leading high edge is detected on GPIO + * \ingroup hardware_sleep + * + * One of the sleep_run_* functions must be called prior to this call + * + * \param gpio_pin The pin to provide the wake up + */ +static inline void sleep_goto_dormant_until_edge_high(uint gpio_pin) { + sleep_goto_dormant_until_pin(gpio_pin, true, true); +} + +/*! \brief Send system to sleep until a high level is detected on GPIO + * \ingroup hardware_sleep + * + * One of the sleep_run_* functions must be called prior to this call + * + * \param gpio_pin The pin to provide the wake up + */ +static inline void sleep_goto_dormant_until_level_high(uint gpio_pin) { + sleep_goto_dormant_until_pin(gpio_pin, false, true); +} + +#ifdef __cplusplus +} +#endif + +#endif +#endif /* ARDUINO_ARCH_RP2040 */ diff --git a/examples/arduino_lorawan_esp32_example/src/pico_rtc/rosc.c b/examples/arduino_lorawan_esp32_example/src/pico_rtc/rosc.c new file mode 100644 index 0000000..06bd114 --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/src/pico_rtc/rosc.c @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#if defined(ARDUINO_ARCH_RP2040) + +#include "pico.h" + +// For MHZ definitions etc +#include "hardware/clocks.h" +#include "pico_rosc.h" + +// Given a ROSC delay stage code, return the next-numerically-higher code. +// Top result bit is set when called on maximum ROSC code. +uint32_t next_rosc_code(uint32_t code) { + return ((code | 0x08888888u) + 1u) & 0xf7777777u; +} + +uint rosc_find_freq(uint32_t low_mhz, uint32_t high_mhz) { + // TODO: This could be a lot better + rosc_set_div(1); + for (uint32_t code = 0; code <= 0x77777777u; code = next_rosc_code(code)) { + rosc_set_freq(code); + uint rosc_mhz = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_ROSC_CLKSRC) / 1000; + if ((rosc_mhz >= low_mhz) && (rosc_mhz <= high_mhz)) { + return rosc_mhz; + } + } + return 0; +} + +void rosc_set_div(uint32_t div) { + assert(div <= 31 && div >= 1); + rosc_write(&rosc_hw->div, ROSC_DIV_VALUE_PASS + div); +} + +void rosc_set_freq(uint32_t code) { + rosc_write(&rosc_hw->freqa, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code & 0xffffu)); + rosc_write(&rosc_hw->freqb, (ROSC_FREQA_PASSWD_VALUE_PASS << ROSC_FREQA_PASSWD_LSB) | (code >> 16u)); +} + +void rosc_set_range(uint range) { + // Range should use enumvals from the headers and thus have the password correct + rosc_write(&rosc_hw->ctrl, (ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB) | range); +} + +void rosc_disable(void) { + uint32_t tmp = rosc_hw->ctrl; + tmp &= (~ROSC_CTRL_ENABLE_BITS); + tmp |= (ROSC_CTRL_ENABLE_VALUE_DISABLE << ROSC_CTRL_ENABLE_LSB); + rosc_write(&rosc_hw->ctrl, tmp); + // Wait for stable to go away + while(rosc_hw->status & ROSC_STATUS_STABLE_BITS); +} + +void rosc_enable(void) +{ + uint32_t tmp = rosc_hw->ctrl; + tmp &= (~ROSC_CTRL_ENABLE_BITS); + tmp |= (ROSC_CTRL_ENABLE_VALUE_ENABLE << ROSC_CTRL_ENABLE_LSB); + rosc_write(&rosc_hw->ctrl, tmp); + // Wait for stable + while ((rosc_hw->status & ROSC_STATUS_STABLE_BITS) != ROSC_STATUS_STABLE_BITS); +} + +void rosc_set_dormant(void) { + // WARNING: This stops the rosc until woken up by an irq + rosc_write(&rosc_hw->dormant, ROSC_DORMANT_VALUE_DORMANT); + // Wait for it to become stable once woken up + while(!(rosc_hw->status & ROSC_STATUS_STABLE_BITS)); +} +#endif /* ARDUINO_ARCH_RP2040 */ diff --git a/examples/arduino_lorawan_esp32_example/src/pico_rtc/sleep.c b/examples/arduino_lorawan_esp32_example/src/pico_rtc/sleep.c new file mode 100644 index 0000000..a82e749 --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/src/pico_rtc/sleep.c @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#if defined(ARDUINO_ARCH_RP2040) + +#include "pico.h" + +#include "pico/stdlib.h" +#include "pico_sleep.h" + +#include "hardware/rtc.h" +#include "hardware/pll.h" +#include "hardware/clocks.h" +#include "hardware/xosc.h" +#include "pico_rosc.h" +#include "hardware/regs/io_bank0.h" +// For __wfi +#include "hardware/sync.h" +// For scb_hw so we can enable deep sleep +#include "hardware/structs/scb.h" + +// The difference between sleep and dormant is that ALL clocks are stopped in dormant mode, +// until the source (either xosc or rosc) is started again by an external event. +// In sleep mode some clocks can be left running controlled by the SLEEP_EN registers in the clocks +// block. For example you could keep clk_rtc running. Some destinations (proc0 and proc1 wakeup logic) +// can't be stopped in sleep mode otherwise there wouldn't be enough logic to wake up again. + + +// TODO: Optionally, memories can also be powered down. + +static dormant_source_t _dormant_source; + +bool dormant_source_valid(dormant_source_t dormant_source) { + return (dormant_source == DORMANT_SOURCE_XOSC) || (dormant_source == DORMANT_SOURCE_ROSC); +} + +// In order to go into dormant mode we need to be running from a stoppable clock source: +// either the xosc or rosc with no PLLs running. This means we disable the USB and ADC clocks +// and all PLLs +void sleep_run_from_dormant_source(dormant_source_t dormant_source) { + assert(dormant_source_valid(dormant_source)); + _dormant_source = dormant_source; + + // FIXME: Just defining average rosc freq here. + uint src_hz = (dormant_source == DORMANT_SOURCE_XOSC) ? XOSC_MHZ * MHZ : 6.5 * MHZ; + uint clk_ref_src = (dormant_source == DORMANT_SOURCE_XOSC) ? + CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC : + CLOCKS_CLK_REF_CTRL_SRC_VALUE_ROSC_CLKSRC_PH; + + // CLK_REF = XOSC or ROSC + clock_configure(clk_ref, + clk_ref_src, + 0, // No aux mux + src_hz, + src_hz); + + // CLK SYS = CLK_REF + clock_configure(clk_sys, + CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF, + 0, // Using glitchless mux + src_hz, + src_hz); + + // CLK USB = 0MHz + clock_stop(clk_usb); + + // CLK ADC = 0MHz + clock_stop(clk_adc); + + // CLK RTC = ideally XOSC (12MHz) / 256 = 46875Hz but could be rosc + uint clk_rtc_src = (dormant_source == DORMANT_SOURCE_XOSC) ? + CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_XOSC_CLKSRC : + CLOCKS_CLK_RTC_CTRL_AUXSRC_VALUE_ROSC_CLKSRC_PH; + + clock_configure(clk_rtc, + 0, // No GLMUX + clk_rtc_src, + src_hz, + 46875); + + // CLK PERI = clk_sys. Used as reference clock for Peripherals. No dividers so just select and enable + clock_configure(clk_peri, + 0, + CLOCKS_CLK_PERI_CTRL_AUXSRC_VALUE_CLK_SYS, + src_hz, + src_hz); + + pll_deinit(pll_sys); + pll_deinit(pll_usb); + + // Assuming both xosc and rosc are running at the moment + if (dormant_source == DORMANT_SOURCE_XOSC) { + // Can disable rosc + rosc_disable(); + } else { + // Can disable xosc + xosc_disable(); + } +#if 0 + // Reconfigure uart with new clocks + setup_default_uart(); +#endif +} + +// Go to sleep until woken up by the RTC +void sleep_goto_sleep_until(datetime_t *t, rtc_callback_t callback) { + // We should have already called the sleep_run_from_dormant_source function + assert(dormant_source_valid(_dormant_source)); + + // Turn off all clocks when in sleep mode except for RTC + clocks_hw->sleep_en0 = CLOCKS_SLEEP_EN0_CLK_RTC_RTC_BITS; + clocks_hw->sleep_en1 = 0x0; + + rtc_set_alarm(t, callback); + + uint save = scb_hw->scr; + // Enable deep sleep at the proc + scb_hw->scr = save | M0PLUS_SCR_SLEEPDEEP_BITS; + + // Go to sleep + __wfi(); +} + +static void _go_dormant(void) { + assert(dormant_source_valid(_dormant_source)); + + if (_dormant_source == DORMANT_SOURCE_XOSC) { + xosc_dormant(); + } else { + rosc_set_dormant(); + } +} + +void sleep_goto_dormant_until_pin(uint gpio_pin, bool edge, bool high) { + bool low = !high; + bool level = !edge; + + // Configure the appropriate IRQ at IO bank 0 + assert(gpio_pin < NUM_BANK0_GPIOS); + + uint32_t event = 0; + + if (level && low) event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_LOW_BITS; + if (level && high) event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_LEVEL_HIGH_BITS; + if (edge && high) event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_HIGH_BITS; + if (edge && low) event = IO_BANK0_DORMANT_WAKE_INTE0_GPIO0_EDGE_LOW_BITS; + + gpio_set_dormant_irq_enabled(gpio_pin, event, true); + + _go_dormant(); + // Execution stops here until woken up + + // Clear the irq so we can go back to dormant mode again if we want + gpio_acknowledge_irq(gpio_pin, event); +} +#endif /* ARDUINO_ARCH_RP2040 */ diff --git a/examples/arduino_lorawan_esp32_example/ttn_decoder.js b/examples/arduino_lorawan_esp32_example/ttn_decoder.js deleted file mode 100644 index 2d0c77e..0000000 --- a/examples/arduino_lorawan_esp32_example/ttn_decoder.js +++ /dev/null @@ -1,189 +0,0 @@ -function ttn_decoder_fp(bytes) { - // bytes is of type Buffer - - // IMPORTANT: paste code from src/decoder.js here - var bytesToInt = function(bytes) { - var i = 0; - for (var x = 0; x < bytes.length; x++) { - i |= +(bytes[x] << (x * 8)); - } - return i; - }; - - var unixtime = function(bytes) { - if (bytes.length !== unixtime.BYTES) { - throw new Error('Unix time must have exactly 4 bytes'); - } - return bytesToInt(bytes); - }; - unixtime.BYTES = 4; - - var uint8 = function(bytes) { - if (bytes.length !== uint8.BYTES) { - throw new Error('int must have exactly 1 byte'); - } - return bytesToInt(bytes); - }; - uint8.BYTES = 1; - - var uint16 = function(bytes) { - if (bytes.length !== uint16.BYTES) { - throw new Error('int must have exactly 2 bytes'); - } - return bytesToInt(bytes); - }; - uint16.BYTES = 2; - - var uint16fp1 = function(bytes) { - if (bytes.length !== uint16.BYTES) { - throw new Error('int must have exactly 2 bytes'); - } - var res = bytesToInt(bytes) * 0.1; - return res.toFixed(1); - }; - uint16fp1.BYTES = 2; - - var uint32 = function(bytes) { - if (bytes.length !== uint32.BYTES) { - throw new Error('int must have exactly 4 bytes'); - } - return bytesToInt(bytes); - }; - uint32.BYTES = 4; - - var latLng = function(bytes) { - if (bytes.length !== latLng.BYTES) { - throw new Error('Lat/Long must have exactly 8 bytes'); - } - - var lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); - var lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); - - return [lat / 1e6, lng / 1e6]; - }; - latLng.BYTES = 8; - - var temperature = function(bytes) { - if (bytes.length !== temperature.BYTES) { - throw new Error('Temperature must have exactly 2 bytes'); - } - var isNegative = bytes[0] & 0x80; - var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8) - + ('00000000' + Number(bytes[1]).toString(2)).slice(-8); - if (isNegative) { - var arr = b.split('').map(function(x) { return !Number(x); }); - for (var i = arr.length - 1; i > 0; i--) { - arr[i] = !arr[i]; - if (arr[i]) { - break; - } - } - b = arr.map(Number).join(''); - } - var t = parseInt(b, 2); - if (isNegative) { - t = -t; - } - t = t / 1e2; - return t.toFixed(1); - }; - temperature.BYTES = 2; - - var humidity = function(bytes) { - if (bytes.length !== humidity.BYTES) { - throw new Error('Humidity must have exactly 2 bytes'); - } - - var h = bytesToInt(bytes); - return h / 1e2; - }; - humidity.BYTES = 2; - - // Based on https://stackoverflow.com/a/37471538 by Ilya Bursov - // quoted by Arjan here https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757 - function rawfloat(bytes) { - if (bytes.length !== rawfloat.BYTES) { - throw new Error('Float must have exactly 4 bytes'); - } - // JavaScript bitwise operators yield a 32 bits integer, not a float. - // Assume LSB (least significant byte first). - var bits = bytes[3]<<24 | bytes[2]<<16 | bytes[1]<<8 | bytes[0]; - var sign = (bits>>>31 === 0) ? 1.0 : -1.0; - var e = bits>>>23 & 0xff; - var m = (e === 0) ? (bits & 0x7fffff)<<1 : (bits & 0x7fffff) | 0x800000; - var f = sign * m * Math.pow(2, e - 150); - return f.toFixed(1); - } - rawfloat.BYTES = 4; - - var bitmap = function(byte) { - if (byte.length !== bitmap.BYTES) { - throw new Error('Bitmap must have exactly 1 byte'); - } - var i = bytesToInt(byte); - var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); - return ['res4', 'res3', 'res2', 'res1', 'res0', 'runtime_exp', 'data_ok', 'battery_ok'] - .reduce(function(obj, pos, index) { - obj[pos] = bm[index]; - return obj; - }, {}); - }; - bitmap.BYTES = 1; - - var decode = function(bytes, mask, names) { - - var maskLength = mask.reduce(function(prev, cur) { - return prev + cur.BYTES; - }, 0); - if (bytes.length < maskLength) { - throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length); - } - - names = names || []; - var offset = 0; - return mask - .map(function(decodeFn) { - var current = bytes.slice(offset, offset += decodeFn.BYTES); - return decodeFn(current); - }) - .reduce(function(prev, cur, idx) { - prev[names[idx] || idx] = cur; - return prev; - }, {}); - }; - - if (typeof module === 'object' && typeof module.exports !== 'undefined') { - module.exports = { - unixtime: unixtime, - uint8: uint8, - uint16: uint16, - uint32: uint32, - temperature: temperature, - humidity: humidity, - latLng: latLng, - bitmap: bitmap, - rawfloat: rawfloat, - uint16fp1: uint16fp1, - decode: decode - }; - } - - // see assignment to 'bitmap' variable for status bit names - return decode( - bytes, - [bitmap, temperature, uint8, uint16, uint16 ], // types - ['status', 'air_temp_c', 'humidity', 'supply_v', 'battery_v' ] // JSON elements - ); - -} - - -function decodeUplink(input) { - return { - data: { - bytes: ttn_decoder_fp(input.bytes) - }, - warnings: [], - errors: [] - }; -} diff --git a/examples/arduino_lorawan_esp32_example/ttn_downlink_formatter.js b/examples/arduino_lorawan_esp32_example/ttn_downlink_formatter.js new file mode 100644 index 0000000..89a4430 --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/ttn_downlink_formatter.js @@ -0,0 +1,207 @@ +/////////////////////////////////////////////////////////////////////////////// +// ttn_downlink_formatter.js +// +// LoRaWAN node example based on ESP32 and RFM95W - +// sends data to a LoRaWAN network (e.g. The Things Network) +// +// This script allows to send downlink data to the The Things Network with +// commands, parameters and responses are encoded in JSON format. +// Commands shall be sent via FPort=1. +// +// Commands: +// ---------- +// {"cmd": "CMD_SET_SLEEP_INTERVAL", "interval": } +// {"cmd": "CMD_SET_SLEEP_INTERVAL_LONG", "interval": } +// {"cmd": "CMD_GET_DATETIME"} +// {"cmd": "CMD_SET_DATETIME", "epoch": } +// {"cmd": "CMD_GET_CONFIG"} +// +// Responses: +// ----------- +// CMD_GET_CONFIG -> FPort=3: {"reserved": 0, +// "sleep_interval": , +// "sleep_interval_long": } +// +// CMD_GET_DATETIME -> FPort=2: {"epoch": , "rtc_source":} +// +// : 0...65535 +// : unix epoch time, see https://www.epochconverter.com/ +// : 0...15 (1: hourly / 2: daily / 4: weekly / 8: monthly) +// : 0x00: GPS / 0x01: RTC / 0x02: LORA / 0x03: unsynched / 0x04: set (source unknown) +// +// +// Based on: +// --------- +// https://www.thethingsindustries.com/docs/integrations/payload-formatters/javascript/downlink/ +// +// created: 08/2023 +// +// +// MIT License +// +// Copyright (c) 2023 Matthias Prinke +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// +// History: +// 20230821 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +// Commands +const cmd_code = new Map([ + ["CMD_SET_SLEEP_INTERVAL", 0xA8], + ["CMD_SET_SLEEP_INTERVAL_LONG", 0xA9], + ["CMD_GET_DATETIME", 0x86], + ["CMD_SET_DATETIME", 0x88], + ["CMD_GET_CONFIG", 0xB1], +]); + +// Source of Real Time Clock setting +var rtc_source_code = { + 0x00: "GPS", + 0x01: "RTC", + 0x02: "LORA", + 0x03: "unsynched", + 0x04: "set (source unknown)" +}; + +function bytesToInt(bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; +} + +// Big Endian +function bytesToIntBE(bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << ((bytes.length - 1 - x) * 8)); + } + return i; +} + +function uint8(bytes) { + if (bytes.length !== 1) { + throw new Error('uint8 must have exactly 1 byte'); + } + return bytesToInt(bytes); +} + +function uint16BE(bytes) { + if (bytes.length !== 2) { + throw new Error('uint16BE must have exactly 2 bytes'); + } + return bytesToIntBE(bytes); +} + +function uint32BE(bytes) { + if (bytes.length !== 4) { + throw new Error('uint32BE must have exactly 4 bytes'); + } + return bytesToIntBE(bytes); +} + +// Encode Downlink from JSON to bytes +function encodeDownlink(input) { + if ((input.data.cmd === "CMD_SET_SLEEP_INTERVAL") || + (input.data.cmd === "CMD_SET_SLEEP_INTERVAL_LONG")) { + + return { + bytes: [cmd_code.get(input.data.cmd), + input.data.interval >> 8, + input.data.interval & 0xFF + ], + fPort: 1, + warnings: [], + errors: [] + }; + } + else if (input.data.cmd === "CMD_SET_DATETIME") { + return { + bytes: [cmd_code.get(input.data.cmd), + input.data.epoch >> 24, + (input.data.epoch >> 16) & 0xFF, + (input.data.epoch >> 8) & 0xFF, + (input.data.epoch & 0xFF)], + fPort: 1, + warnings: [], + errors: [] + }; + } + else if ((input.data.cmd === "CMD_GET_CONFIG") || + (input.data.cmd === "CMD_GET_DATETIME")) { + return { + bytes: [cmd_code.get(input.data.cmd)], + fPort: 1, + warnings: [], + errors: [] + }; + } else { + return { + bytes: [], + errors: ["unknown command"], + fPort: 1, + warnings: [] + }; + } +} + +// Decode Downlink from bytes to JSON +function decodeDownlink(input) { + switch (input.fPort) { + case 1: + for (const x of cmd_code.keys()) { + if (input.bytes[0] == cmd_code.get(x)) { + return { + cmd: x + }; + } + } + return { + cmd: [], + errors: ["unknown command"] + }; + case 2: + return { + data: { + unixtime: uint32BE(input.bytes.slice(0, 3)), + rtc_source: rtc_source_code[input.bytes[4]] + } + }; + case 3: + return { + data: { + reserved: uint8(input.bytes[0]), + sleep_interval: uint16BE(input.bytes.slice(1, 2)), + sleep_interval_long: uint16BE(input.bytes.slice(3, 4)) + } + }; + default: + return { + errors: ["unknown FPort"] + }; + } +} diff --git a/examples/arduino_lorawan_esp32_example/ttn_uplink_formatter.js b/examples/arduino_lorawan_esp32_example/ttn_uplink_formatter.js new file mode 100644 index 0000000..07d216a --- /dev/null +++ b/examples/arduino_lorawan_esp32_example/ttn_uplink_formatter.js @@ -0,0 +1,340 @@ +/////////////////////////////////////////////////////////////////////////////// +// ttn_uplink_formatter.js +// +// Bresser 5-in-1/6-in-1 868 MHz Weather Sensor Radio Receiver +// based on ESP32 and RFM95W - +// sends data to a LoRaWAN network (e.g. The Things Network) +// +// This script allows to decode payload received from The Things Network - +// data (sent at fixed intervals) and responses (sent upon request) - +// from bytes to JSON. +// +// Commands: +// ---------- +// {"cmd": "CMD_SET_SLEEP_INTERVAL", "interval": } +// {"cmd": "CMD_SET_SLEEP_INTERVAL_LONG", "interval": } +// {"cmd": "CMD_GET_DATETIME"} +// {"cmd": "CMD_SET_DATETIME", "epoch": } +// {"cmd": "CMD_GET_CONFIG"} +// +// Responses: +// ----------- +// CMD_GET_CONFIG -> FPort=3: {"reserved": 0, +// "sleep_interval": , +// "sleep_interval_long": } +// +// CMD_GET_DATETIME -> FPort=2: {"epoch": , "rtc_source":} +// +// : 0...65535 +// : unix epoch time, see https://www.epochconverter.com/ +// : 0...15 (1: hourly / 2: daily / 4: weekly / 8: monthly) +// : 0x00: GPS / 0x01: RTC / 0x02: LORA / 0x03: unsynched / 0x04: set (source unknown) +// +// +// Based on: +// --------- +// ttn_decoder_fp.js +// +// created: 08/2023 +// +// +// MIT License +// +// Copyright (c) 2023 Matthias Prinke +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// +// History: +// 20230821 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +function ttn_decoder(bytes, port) { + // bytes is of type Buffer + + var rtc_source_code = { + 0x00: "GPS", + 0x01: "RTC", + 0x02: "LORA", + 0x03: "unsynched", + 0x04: "set (source unknown)" + }; + + var rtc_source = function (bytes) { + if (bytes.length !== rtc_source.BYTES) { + throw new Error('rtc_source must have exactly 1 byte'); + } + return rtc_source_code[bytes[0]]; + }; + rtc_source.BYTES = 1; + + var bytesToInt = function(bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << (x * 8)); + } + return i; + }; + + // Big Endian + var bytesToIntBE = function(bytes) { + var i = 0; + for (var x = 0; x < bytes.length; x++) { + i |= +(bytes[x] << ((bytes.length-1-x) * 8)); + } + return i; + }; + + var unixtime = function(bytes) { + if (bytes.length !== unixtime.BYTES) { + throw new Error('Unix time must have exactly 4 bytes'); + } + //return bytesToInt(bytes); + dateObj = new Date(bytesToInt(bytes) * 1000); + return dateObj.toISOString(); + }; + unixtime.BYTES = 4; + + var uint8 = function(bytes) { + if (bytes.length !== uint8.BYTES) { + throw new Error('int must have exactly 1 byte'); + } + return bytesToInt(bytes); + }; + uint8.BYTES = 1; + + var uint16 = function(bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToInt(bytes); + }; + uint16.BYTES = 2; + + var uint16fp1 = function(bytes) { + if (bytes.length !== uint16.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + var res = bytesToInt(bytes) * 0.1; + return res.toFixed(1); + }; + uint16fp1.BYTES = 2; + + var uint32 = function(bytes) { + if (bytes.length !== uint32.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToInt(bytes); + }; + uint32.BYTES = 4; + + var uint16BE = function(bytes) { + if (bytes.length !== uint16BE.BYTES) { + throw new Error('int must have exactly 2 bytes'); + } + return bytesToIntBE(bytes); + }; + uint16BE.BYTES = 2; + + var uint32BE = function(bytes) { + if (bytes.length !== uint32BE.BYTES) { + throw new Error('int must have exactly 4 bytes'); + } + return bytesToIntBE(bytes); + }; + uint32BE.BYTES = 4; + + var latLng = function(bytes) { + if (bytes.length !== latLng.BYTES) { + throw new Error('Lat/Long must have exactly 8 bytes'); + } + + var lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); + var lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); + + return [lat / 1e6, lng / 1e6]; + }; + latLng.BYTES = 8; + + var temperature = function(bytes) { + if (bytes.length !== temperature.BYTES) { + throw new Error('Temperature must have exactly 2 bytes'); + } + var isNegative = bytes[0] & 0x80; + var b = ('00000000' + Number(bytes[0]).toString(2)).slice(-8) + + ('00000000' + Number(bytes[1]).toString(2)).slice(-8); + if (isNegative) { + var arr = b.split('').map(function(x) { return !Number(x); }); + for (var i = arr.length - 1; i > 0; i--) { + arr[i] = !arr[i]; + if (arr[i]) { + break; + } + } + b = arr.map(Number).join(''); + } + var t = parseInt(b, 2); + if (isNegative) { + t = -t; + } + t = t / 1e2; + return t.toFixed(1); + }; + temperature.BYTES = 2; + + var humidity = function(bytes) { + if (bytes.length !== humidity.BYTES) { + throw new Error('Humidity must have exactly 2 bytes'); + } + + var h = bytesToInt(bytes); + return h / 1e2; + }; + humidity.BYTES = 2; + + // Based on https://stackoverflow.com/a/37471538 by Ilya Bursov + // quoted by Arjan here https://www.thethingsnetwork.org/forum/t/decode-float-sent-by-lopy-as-node/8757 + function rawfloat(bytes) { + if (bytes.length !== rawfloat.BYTES) { + throw new Error('Float must have exactly 4 bytes'); + } + // JavaScript bitwise operators yield a 32 bits integer, not a float. + // Assume LSB (least significant byte first). + var bits = bytes[3]<<24 | bytes[2]<<16 | bytes[1]<<8 | bytes[0]; + var sign = (bits>>>31 === 0) ? 1.0 : -1.0; + var e = bits>>>23 & 0xff; + var m = (e === 0) ? (bits & 0x7fffff)<<1 : (bits & 0x7fffff) | 0x800000; + var f = sign * m * Math.pow(2, e - 150); + return f.toFixed(1); + } + rawfloat.BYTES = 4; + + var bitmap_node = function (byte) { + if (byte.length !== bitmap_node.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + + return ['res7', 'res6', 'res5', 'res4', 'res3', 'runtime_exp', 'data_ok', 'battery_ok'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_node.BYTES = 1; + + var bitmap_sensors = function (byte) { + if (byte.length !== bitmap_sensors.BYTES) { + throw new Error('Bitmap must have exactly 1 byte'); + } + var i = bytesToInt(byte); + var bm = ('00000000' + Number(i).toString(2)).substr(-8).split('').map(Number).map(Boolean); + return ['res0', 'res1', 'res2', 'res3', 'res4', 'res5', 'res6', 'res7'] + .reduce(function (obj, pos, index) { + obj[pos] = bm[index]; + return obj; + }, {}); + }; + bitmap_sensors.BYTES = 1; + + var decode = function(bytes, mask, names) { + + var maskLength = mask.reduce(function(prev, cur) { + return prev + cur.BYTES; + }, 0); + if (bytes.length < maskLength) { + throw new Error('Mask length is ' + maskLength + ' whereas input is ' + bytes.length); + } + + names = names || []; + var offset = 0; + return mask + .map(function(decodeFn) { + var current = bytes.slice(offset, offset += decodeFn.BYTES); + return decodeFn(current); + }) + .reduce(function(prev, cur, idx) { + prev[names[idx] || idx] = cur; + return prev; + }, {}); + }; + + if (typeof module === 'object' && typeof module.exports !== 'undefined') { + module.exports = { + unixtime: unixtime, + uint8: uint8, + uint16: uint16, + uint32: uint32, + uint16BE: uint16BE, + uint32BE: uint32BE, + temperature: temperature, + humidity: humidity, + latLng: latLng, + bitmap_node: bitmap_node, + bitmap_sensors: bitmap_sensors, + rawfloat: rawfloat, + uint16fp1: uint16fp1, + rtc_source: rtc_source, + decode: decode + }; + } + + if (port === 1) { + return decode( + bytes, + [ bitmap_node, temperature, uint8, uint16, uint16 + ], // types + [ 'status_node', 'air_temp_c', 'humidity', 'supply_v', 'battery_v' + ] // JSON elements + ); + } else if (port === 2) { + return decode( + bytes, + [uint32BE, rtc_source + ], + ['unixtime', 'rtc_source' + ] + ); + } else if (port === 3) { + return decode( + bytes, + [uint16BE, uint16BE + ], + ['sleep_interval', 'sleep_interval_long' + ] + ); + } + +} + + +function decodeUplink(input) { + return { + data: { + bytes: ttn_decoder(input.bytes, input.fPort) + }, + warnings: [], + errors: [] + }; +} diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..5a4d42d --- /dev/null +++ b/platformio.ini @@ -0,0 +1,53 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:ttgo-lora32-v21] +platform = espressif32 +board = ttgo-lora32-v21 +framework = arduino +lib_deps = + mcci-catena/MCCI LoRaWAN LMIC library@^4.1.1 + mcci-catena/MCCI Arduino Development Kit ADK@^0.2.2 + mcci-catena/MCCI Arduino LoRaWAN Library@^0.9.2 + thesolarnomad/LoRa Serialization@^3.2.1 + SPI + +build_flags = + ; Use platformio.ini for settings instead lmic_project_config.h. + -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS + + ; Ping and beacons not supported for class A, disable to save memory. + -D DISABLE_PING + -D DISABLE_BEACONS + -D hal_init=LMICHAL_init ; Workaround for naming conflict of function hal_init + ; introduced by newer versions (> 3.5.0) of + ; PlatformIO Espressif 32 platform (ESP32 Arduino core). + ; See https://github.com/lnlp/LMIC-node/issues/41 for more information. + + ; -D LMIC_DEBUG_LEVEL=1 ; 0, 1 or 2 + + ; -D CFG_sx1272_radio=1 ; Use for SX1272 radio + -D CFG_sx1276_radio=1 ; Use for SX1276 radio + -D USE_ORIGINAL_AES ; Faster but larger, see docs + ; -D LMIC_USE_INTERRUPTS ; Not tested or supported on many platforms + ; -D LMIC_ENABLE_DeviceTimeReq=1 ; Network time support + + ; --- Regional settings ----- + ; Enable only one of the following regions: + ; -D CFG_as923=1 + ; -D CFG_as923jp=1 + ; -D CFG_au915=1 + ; -D CFG_cn490=1 ; Not yet supported + ; -D CFG_cn783=1 ; Not yet supported + ; -D CFG_eu433=1 ; Not yet supported + -D CFG_eu868=1 + ; -D CFG_in866=1 + ; -D CFG_kr920=1 + ; -D CFG_us915=1 diff --git a/src/lib/arduino_lorawan_cEventLog.cpp b/src/lib/arduino_lorawan_cEventLog.cpp index 9de22e8..2d4dcf5 100644 --- a/src/lib/arduino_lorawan_cEventLog.cpp +++ b/src/lib/arduino_lorawan_cEventLog.cpp @@ -94,10 +94,7 @@ Arduino_LoRaWAN::cEventLog::processSingleEvent() auto const pEvent = &this->m_queue[this->m_head]; - Serial.print(osticks2ms(pEvent->time)); - Serial.print(" ms:"); pEvent->pCallBack(pEvent); - Serial.println(); if (++m_head == sizeof(m_queue) / sizeof(m_queue[0])) {