From 254ee6733977259dc7d68d9066ace10f983ef328 Mon Sep 17 00:00:00 2001 From: MajorTwip Date: Sat, 27 May 2023 23:42:50 +0200 Subject: [PATCH] Implement the sensor-sample for PlatformIO closes #218 --- examples/esp32_platformio/.gitignore | 7 + .../esp32_platformio.code-workspace | 8 + .../include/secrets.h.template | 10 + examples/esp32_platformio/lib/README | 46 + examples/esp32_platformio/platformio.ini | 29 + examples/esp32_platformio/src/main.cpp | 888 ++++++++++++++++++ examples/esp32_platformio/test/README | 11 + examples/esp32_platformio/ttn_decoder.js | 189 ++++ 8 files changed, 1188 insertions(+) create mode 100644 examples/esp32_platformio/.gitignore create mode 100644 examples/esp32_platformio/esp32_platformio.code-workspace create mode 100644 examples/esp32_platformio/include/secrets.h.template create mode 100644 examples/esp32_platformio/lib/README create mode 100644 examples/esp32_platformio/platformio.ini create mode 100644 examples/esp32_platformio/src/main.cpp create mode 100644 examples/esp32_platformio/test/README create mode 100644 examples/esp32_platformio/ttn_decoder.js diff --git a/examples/esp32_platformio/.gitignore b/examples/esp32_platformio/.gitignore new file mode 100644 index 0000000..2b84e7e --- /dev/null +++ b/examples/esp32_platformio/.gitignore @@ -0,0 +1,7 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch +include/secrets.h + diff --git a/examples/esp32_platformio/esp32_platformio.code-workspace b/examples/esp32_platformio/esp32_platformio.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/examples/esp32_platformio/esp32_platformio.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/examples/esp32_platformio/include/secrets.h.template b/examples/esp32_platformio/include/secrets.h.template new file mode 100644 index 0000000..ab90134 --- /dev/null +++ b/examples/esp32_platformio/include/secrets.h.template @@ -0,0 +1,10 @@ +#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 }; diff --git a/examples/esp32_platformio/lib/README b/examples/esp32_platformio/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/examples/esp32_platformio/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/examples/esp32_platformio/platformio.ini b/examples/esp32_platformio/platformio.ini new file mode 100644 index 0000000..20dba96 --- /dev/null +++ b/examples/esp32_platformio/platformio.ini @@ -0,0 +1,29 @@ +; 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 = + https://github.com/mcci-catena/arduino-lorawan +; Ofiicial Release is missing a Patch that causes a Boot-Loop #204 +; mcci-catena/MCCI Arduino LoRaWAN Library @ ^0.9.2 + thesolarnomad/LoRa Serialization@^3.2.1 + +monitor_speed = 115200 + +build_flags = +; Define Frequancy + -DCFG_eu868 +; mitigate "multiple definitions hal_init" - Bug + -D hal_init=LMICHAL_init + -DLMIC_DEBUG_LEVEL=1 + -D_DEBUG_MODE_ \ No newline at end of file diff --git a/examples/esp32_platformio/src/main.cpp b/examples/esp32_platformio/src/main.cpp new file mode 100644 index 0000000..ed5ed14 --- /dev/null +++ b/examples/esp32_platformio/src/main.cpp @@ -0,0 +1,888 @@ +/////////////////////////////////////////////////////////////////////////////// +// originaly arduino_lorawan_esp32_example.ino, now main.cpp +// +// Example sketch showing how to periodically poll a sensor and send the data +// to a LoRaWAN network. +// +// Based on simple_sensor_bme280.ino with the following modifications: +// - reading the sensor data is replaced by a stub +// - implements power saving by using the ESP32 deep sleep mode +// - implements fast re-joining after sleep by storing network session data +// in the ESP32 RTC RAM +// - LoRa_Serialization is used for encoding various data types into bytes +// - Transferred to an PlatformIO-Project +// +// +// Based on: +// --------- +// MCCI LoRaWAN LMIC library by Thomas Telkamp and Matthijs Kooijman / Terry Moore, MCCI +// (https://github.com/mcci-catena/arduino-lmic) + +// MCCI Arduino LoRaWAN Library by Terry Moore, MCCI +// (https://github.com/mcci-catena/arduino-lorawan) +// +// +// Library dependencies (tested versions): +// --------------------------------------- +// MCCI Arduino Development Kit ADK 0.2.2 +// MCCI LoRaWAN LMIC library 4.1.1 +// MCCI Arduino LoRaWAN Library 0.9.2 +// LoRa_Serialization 3.2.1 +// +// +// created: 07/2022 +// +// +// MIT License +// +// Copyright (c) 2022 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: +// +// 20220729 Created +// 20230307 Changed cMyLoRaWAN to inherit from Arduino_LoRaWAN_network +// instead of Arduino_LoRaWAN_ttn +// Added Pin mappings for some common ESP32 LoRaWAN boards +// +// Notes: +// - After a successful transmission, the controller can go into deep sleep +// (option SLEEP_EN) +// - Sleep time is defined in SLEEP_INTERVAL +// - If joining the network or transmitting uplink data fails, +// 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 +// +/////////////////////////////////////////////////////////////////////////////// + +//--- Select LoRaWAN Network --- +// The Things Network +#define ARDUINO_LMIC_CFG_NETWORK_TTN 1 + +// Helium Network +// see mcci-cathena/arduino-lorawan issue #185 "Add Helium EU868 support" +// (https://github.com/mcci-catena/arduino-lorawan/issues/185) +#define ARDUINO_LMIC_CFG_NETWORK_GENERIC 0 + +// (Add other networks here) + + +#include +#include +#include + +//Not automaticly loaded in PlatformIO +#include + +//----------------------------------------------------------------------------- +// +// User Configuration +// + +// Enable debug mode (debug messages via serial port) +//#define _DEBUG_MODE_ + +// 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 +#define SLEEP_INTERVAL 360 + +// 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) +#define SLEEP_TIMEOUT_INITIAL 1800 + +// If already joined, force deep sleep after SLEEP_TIMEOUT_JOINED seconds (if enabled) +#define SLEEP_TIMEOUT_JOINED 600 + +//----------------------------------------------------------------------------- + +// LoRa_Serialization +#include + +// 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. +#if defined(ARDUINO_TTGO_LoRa32_V1) + // https://github.com/espressif/arduino-esp32/blob/master/variants/ttgo-lora32-v1/pins_arduino.h + // http://www.lilygo.cn/prod_view.aspx?TypeId=50003&Id=1130&FId=t3:50003:3 + // https://github.com/Xinyuan-LilyGo/TTGO-LoRa-Series + // https://github.com/LilyGO/TTGO-LORA32/blob/master/schematic1in6.pdf + #define PIN_LMIC_NSS LORA_CS + #define PIN_LMIC_RST LORA_RST + #define PIN_LMIC_DIO0 LORA_IRQ + #define PIN_LMIC_DIO1 33 + #define PIN_LMIC_DIO2 cMyLoRaWAN::lmic_pinmap::LMIC_UNUSED_PIN + +#elif defined(ARDUINO_TTGO_LoRa32_V2) + // https://github.com/espressif/arduino-esp32/blob/master/variants/ttgo-lora32-v2/pins_arduino.h + #define PIN_LMIC_NSS LORA_CS + #define PIN_LMIC_RST LORA_RST + #define PIN_LMIC_DIO0 LORA_IRQ + #define PIN_LMIC_DIO1 33 + #define PIN_LMIC_DIO2 cMyLoRaWAN::lmic_pinmap::LMIC_UNUSED_PIN + #pragma message("LoRa DIO1 must be wired to GPIO33 manually!") + +#elif defined(ARDUINO_TTGO_LoRa32_v21new) + // https://github.com/espressif/arduino-esp32/blob/master/variants/ttgo-lora32-v21new/pins_arduino.h + #define PIN_LMIC_NSS LORA_CS + #define PIN_LMIC_RST LORA_RST + #define PIN_LMIC_DIO0 LORA_IRQ + #define PIN_LMIC_DIO1 LORA_D1 + #define PIN_LMIC_DIO2 LORA_D2 + +#elif defined(ARDUINO_heltec_wireless_stick) + // https://github.com/espressif/arduino-esp32/blob/master/variants/heltec_wireless_stick/pins_arduino.h + #define PIN_LMIC_NSS SS + #define PIN_LMIC_RST RST_LoRa + #define PIN_LMIC_DIO0 DIO0 + #define PIN_LMIC_DIO1 DIO1 + #define PIN_LMIC_DIO2 DIO2 + +#elif defined(ARDUINO_ADAFRUIT_FEATHER_ESP32S2) + #define PIN_LMIC_NSS 6 + #define PIN_LMIC_RST 9 + #define PIN_LMIC_DIO0 5 + #define PIN_LMIC_DIO1 11 + #define PIN_LMIC_DIO2 cMyLoRaWAN::lmic_pinmap::LMIC_UNUSED_PIN + #pragma message("ARDUINO_ADAFRUIT_FEATHER_ESP32S2 defined; assuming RFM95W FeatherWing will be used") + #pragma message("Required wiring: E to IRQ, D to CS, C to RST, A to DI01") + #pragma message("BLE is not available!") + +#elif defined(ARDUINO_FEATHER_ESP32) + #define PIN_LMIC_NSS 14 + #define PIN_LMIC_RST 27 + #define PIN_LMIC_DIO0 32 + #define PIN_LMIC_DIO1 33 + #define PIN_LMIC_DIO2 cMyLoRaWAN::lmic_pinmap::LMIC_UNUSED_PIN + #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") + +#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 + #define PIN_LMIC_DIO1 16 + #define PIN_LMIC_DIO2 17 + +#endif + +// Uplink message payload size (calculate from assignments to 'encoder' object) +const uint8_t PAYLOAD_SIZE = 8; + +// RTC Memory Handling +#define MAGIC1 (('m' << 24) | ('g' < 16) | ('c' << 8) | '1') +#define MAGIC2 (('m' << 24) | ('g' < 16) | ('c' << 8) | '2') +#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 + + +/****************************************************************************\ +| +| The LoRaWAN object +| +\****************************************************************************/ + +class cMyLoRaWAN : public Arduino_LoRaWAN_network { +public: + cMyLoRaWAN() {}; + using Super = Arduino_LoRaWAN_network; + void setup(); + +protected: + // you'll need to provide implementation for this. + virtual bool GetOtaaProvisioningInfo(Arduino_LoRaWAN::OtaaProvisioningInfo*) override; + + // NetTxComplete() activates deep sleep mode (if enabled) + virtual void NetTxComplete(void) override; + + // NetJoin() changes + virtual void NetJoin(void) override; + + // Used to store/load data to/from persistent (at least during deep sleep) memory + virtual void NetSaveSessionInfo(const SessionInfo &Info, const uint8_t *pExtraInfo, size_t nExtraInfo) override; + // Note: GetSavedSessionInfo provided in simple_sensor_bme280.ino is not used anywhere + //virtual bool GetSavedSessionInfo(SessionInfo &Info, uint8_t*, size_t, size_t*) override; + virtual void NetSaveSessionState(const SessionState &State) override; + virtual bool NetGetSessionState(SessionState &State) override; + virtual bool GetAbpProvisioningInfo(AbpProvisioningInfo *pAbpInfo) override; +}; + + +/****************************************************************************\ +| +| The sensor object +| +\****************************************************************************/ + +class cSensor { +public: + /// \brief the constructor. Deliberately does very little. + cSensor() {}; + + // Sensor data function stubs + float getTemperature(void); + uint8_t getHumidity(void); + uint16_t getVoltageBattery(void); + uint16_t getVoltageSupply(void); + + void uplinkRequest(void) { + m_fUplinkRequest = true; + }; + /// + /// \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. + /// + void loop(); + + // Example sensor status flags + bool data_ok; // has been reached (if FORCE_SLEEP is defined) +ostime_t sleepTimeout; + + +/****************************************************************************\ +| +| Provisioning info for LoRaWAN OTAA +| +\****************************************************************************/ + +// +// For normal use, we require that you edit the sketch to replace FILLMEIN_x +// with values assigned by the your network. However, for regression tests, +// we want to be able to compile these scripts. The regression tests define +// COMPILE_REGRESSION_TEST, and in that case we define FILLMEIN_x to non- +// working but innocuous values. +// +// #define COMPILE_REGRESSION_TEST 1 + +#ifdef COMPILE_REGRESSION_TEST +# define FILLMEIN_8 1, 0, 0, 0, 0, 0, 0, 0 +# define FILLMEIN_16 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 +#else +# warning "You must replace the values marked FILLMEIN with real values from the TTN control panel!" +# define FILLMEIN_8 (#dont edit this, edit the lines that use FILLMEIN_8) +# define FILLMEIN_16 (#dont edit this, edit the lines that use FILLMEIN_16) +#endif + +// APPEUI, DEVEUI and APPKEY +#include "secrets.h" + +#ifndef SECRETS + #define SECRETS + + // The following constants should be copied to secrets.h and configured appropriately + // according to the settings from TTN Console + + // deveui, little-endian (lsb first) + static const std::uint8_t deveui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + // 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". + static const std::uint8_t appkey[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; +#endif + + +/****************************************************************************\ +| +| setup() +| +\****************************************************************************/ + +void setup() { + // set baud rate + Serial.begin(115200); + + // wait for serial to be ready + //while (! Serial) + // yield(); + delay(500); + + sleepTimeout = sec2osticks(SLEEP_TIMEOUT_INITIAL); + + DEBUG_PRINTF_TS("setup()\n"); + + // set up the log; do this first. + myEventLog.setup(); + DEBUG_PRINTF("myEventlog.setup() - done\n"); + + // set up the sensors. + mySensor.setup(); + DEBUG_PRINTF("mySensor.setup() - done\n"); + + // set up lorawan. + myLoRaWAN.setup(); + DEBUG_PRINTF("myLoRaWAN.setup() - done\n"); + + mySensor.uplinkRequest(); +} + +/****************************************************************************\ +| +| loop() +| +\****************************************************************************/ + +void loop() { + // the order of these is arbitrary, but you must poll them all. + myLoRaWAN.loop(); + mySensor.loop(); + myEventLog.loop(); + + #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); + } + #endif +} + +/****************************************************************************\ +| +| LoRaWAN methods +| +\****************************************************************************/ + +// 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); + + +// LMIC_selectSubBand(0); + LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100); + + this->RegisterListener( + // use a lambda so we're "inside" the cMyLoRaWAN from public/private perspective + [](void *pClientInfo, uint32_t event) -> void { + auto const pThis = (cMyLoRaWAN *)pClientInfo; + + // for tx start, we quickly capture the channel and the RPS + if (event == EV_TXSTART) { + // use another lambda to make log prints easy + myEventLog.logEvent( + (void *) pThis, + LMIC.txChnl, + LMIC.rps, + 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))); + } + ); + } + // else if (event == some other), record with print-out function + else { + // do nothing. + } + }, + (void *) this // in case we need it. + ); +} + +// this method is called when the LMIC needs OTAA info. +// return false to indicate "no provisioning", otherwise +// fill in the data and return true. +bool +cMyLoRaWAN::GetOtaaProvisioningInfo( + OtaaProvisioningInfo *pInfo + ) { + // these are the same constants used in the LMIC compliance test script; eases testing + // with the RedwoodComm RWC5020B/RWC5020M testers. + + // initialize info + memcpy(pInfo->DevEUI, deveui, sizeof(pInfo->DevEUI)); + memcpy(pInfo->AppEUI, appeui, sizeof(pInfo->AppEUI)); + memcpy(pInfo->AppKey, appkey, sizeof(pInfo->AppKey)); + + return true; +} + +// This method is called after the node has joined the network. +void +cMyLoRaWAN::NetJoin( + void) { + DEBUG_PRINTF_TS("NetJoin()\n"); + sleepTimeout = os_getTime() + sec2osticks(SLEEP_TIMEOUT_JOINED); +} + +// 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 +} + +#ifdef _DEBUG_MODE_ +// Print session info for debugging +void printSessionInfo(const cMyLoRaWAN::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]); + } + Serial.printf("\n"); +} + +// Print session state for debugging +void printSessionState(const cMyLoRaWAN::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); + // 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(). +void +cMyLoRaWAN::NetSaveSessionInfo( + const SessionInfo &Info, + const uint8_t *pExtraInfo, + size_t nExtraInfo + ) { + if (nExtraInfo > EXTRA_INFO_MEM_SIZE) + return; + rtcSavedSessionInfo = Info; + rtcSavedNExtraInfo = nExtraInfo; + memcpy(rtcSavedExtraInfo, pExtraInfo, nExtraInfo); + magicFlag2 = MAGIC2; + DEBUG_PRINTF_TS("NetSaveSessionInfo()\n"); + #ifdef _DEBUG_MODE_ + 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().] +void +cMyLoRaWAN::NetSaveSessionState(const SessionState &State) { + rtcSavedSessionState = State; + magicFlag1 = MAGIC1; + DEBUG_PRINTF_TS("NetSaveSessionState()\n"); + #ifdef _DEBUG_MODE_ + printSessionState(State); + #endif +} + +// Either fetch SessionState from somewhere and return true or... +// return false, which forces a re-join. +bool +cMyLoRaWAN::NetGetSessionState(SessionState &State) { + if (magicFlag1 == MAGIC1) { + State = rtcSavedSessionState; + DEBUG_PRINTF_TS("NetGetSessionState() - o.k.\n"); + #ifdef _DEBUG_MODE_ + printSessionState(State); + #endif + return true; + } else { + DEBUG_PRINTF_TS("NetGetSessionState() - failed\n"); + return false; + } +} + +// 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. +bool +cMyLoRaWAN::GetAbpProvisioningInfo(AbpProvisioningInfo *pAbpInfo) { + SessionState state; + + // ApbInfo: + // -------- + // uint8_t NwkSKey[16]; + // uint8_t AppSKey[16]; + // uint32_t DevAddr; + // uint32_t NetID; + // uint32_t FCntUp; + // uint32_t FCntDown; + + if ((magicFlag1 != MAGIC1) || (magicFlag2 != MAGIC2)) { + return false; + } + DEBUG_PRINTF_TS("GetAbpProvisioningInfo()\n"); + + pAbpInfo->DevAddr = rtcSavedSessionInfo.V2.DevAddr; + pAbpInfo->NetID = rtcSavedSessionInfo.V2.NetID; + memcpy(pAbpInfo->NwkSKey, rtcSavedSessionInfo.V2.NwkSKey, 16); + memcpy(pAbpInfo->AppSKey, rtcSavedSessionInfo.V2.AppSKey, 16); + NetGetSessionState(state); + pAbpInfo->FCntUp = state.V1.FCntUp; + pAbpInfo->FCntDown = state.V1.FCntDown; + + #ifdef _DEBUG_MODE_ + Serial.printf("NwkSKey:\t"); + for (int i=0; i<15;i++) { + Serial.printf("%02X ", pAbpInfo->NwkSKey[i]); + } + Serial.printf("\n"); + Serial.printf("AppSKey:\t"); + for (int i=0; i<15;i++) { + Serial.printf("%02X ", pAbpInfo->AppSKey[i]); + } + Serial.printf("\n"); + Serial.printf("FCntUp: %d\n", state.V1.FCntUp); + #endif + return true; +} + + +/****************************************************************************\ +| +| Sensor methods +| +\****************************************************************************/ + +void +cSensor::setup(std::uint32_t uplinkPeriodMs) { + // set the initial time. + this->m_uplinkPeriodMs = uplinkPeriodMs; + this->m_tReference = millis(); + + // Initialize your sensors here... +} + +void +cSensor::loop(void) { + auto const tNow = millis(); + auto const deltaT = tNow - this->m_tReference; + + if (deltaT >= this->m_uplinkPeriodMs) { + // request an uplink + this->m_fUplinkRequest = true; + + // keep trigger time locked to uplinkPeriod + auto const advance = deltaT / this->m_uplinkPeriodMs; + this->m_tReference += advance * this->m_uplinkPeriodMs; + } + + // if an uplink was requested, do it. + if (this->m_fUplinkRequest) { + this->m_fUplinkRequest = false; + this->doUplink(); + } +} + +// +// Get battery voltage (Stub) +// +uint16_t +cSensor::getVoltageBattery(void) +{ + const uint16_t voltage = 3850; + + DEBUG_PRINTF("Battery Voltage = %dmV\n", voltage); + + return voltage; +} + +// +// Get supply voltage (Stub) +// +uint16_t +cSensor::getVoltageSupply(void) +{ + const uint16_t voltage = 3300; + + DEBUG_PRINTF("Supply Voltage = %dmV\n", voltage); + + return voltage; +} + +// +// Get temperature (Stub) +// +float +cSensor::getTemperature(void) +{ + const float temperature = 16.4; + + DEBUG_PRINTF("Outdoor Air Temperature = %.1f°C\n", temperature); + + return temperature; +} + +// +// Get temperature (Stub) +// +uint8_t +cSensor::getHumidity(void) +{ + const uint8_t humidity = 42; + + DEBUG_PRINTF("Outdoor Humidity = %d%%\n", humidity); + + return humidity; +} + + +// +// Prepare uplink data for transmission +// +void +cSensor::doUplink(void) { + // if busy uplinking, just skip + if (this->m_fBusy) { + DEBUG_PRINTF_TS("doUplink(): busy\n"); + 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"); + return; + } + + // Call sensor data function stubs + temperature_deg_c = getTemperature(); + humidity_percent = getHumidity(); + battery_voltage_v = getVoltageBattery(); + supply_voltage_v = getVoltageSupply(); + + // 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"); + + // Serialize data into byte array + // NOTE: + // For TTN MQTT integration, ttn_decoder.js must be adjusted accordingly + LoraEncoder encoder(loraData); + encoder.writeBitmap(false, false, false, false, false, + runtimeExpired, + data_ok, + battery_ok); // 1 Byte + encoder.writeTemperature(temperature_deg_c); // 2 Bytes + encoder.writeUint8(humidity_percent); // 1 Byte + encoder.writeUint16(supply_voltage_v); // 2 Bytes + encoder.writeUint16(battery_voltage_v); // 2 Bytes + + + this->m_fBusy = true; + + if (! myLoRaWAN.SendBuffer( + loraData, sizeof(loraData), + // this is the completion function: + [](void *pClientData, bool fSucccess) -> void { + auto const pThis = (cSensor *)pClientData; + pThis->m_fBusy = false; + }, + (void *)this, + /* confirmed */ true, + /* port */ 1 + )) { + // sending failed; callback has not been called and will not + // be called. Reset busy flag. + this->m_fBusy = false; + } +} diff --git a/examples/esp32_platformio/test/README b/examples/esp32_platformio/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/examples/esp32_platformio/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/examples/esp32_platformio/ttn_decoder.js b/examples/esp32_platformio/ttn_decoder.js new file mode 100644 index 0000000..2d0c77e --- /dev/null +++ b/examples/esp32_platformio/ttn_decoder.js @@ -0,0 +1,189 @@ +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: [] + }; +}