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
+
+ 
+
+**or**
+
+ 
+
+2. Select desired debug level
+
+ 
+
+ 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
+
+
+##### 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
+
+
+##### 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
+ 
+
+##### 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"
+
+
+
+#### 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:
+
+
+Add [scripts/helium_decoder.js](scripts/helium_decoder.js) in the Helium console as custom function:
+
+
+**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:
+
+
+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]))
{