diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 573da71..92617f5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -23,8 +23,8 @@ jobs: #- esp32:esp32:heltec_wifi_lora_32_V2 - esp32:esp32:heltec_wifi_lora_32_V3 #- esp32:esp32:featheresp32 - - esp32:esp32:m5stack-core2 - - esp32:esp32:esp32s3_powerfeather + #- esp32:esp32:m5stack-core2 + #- esp32:esp32:esp32s3_powerfeather - esp32:esp32:adafruit_feather_esp32s2 - rp2040:rp2040:adafruit_feather:dbgport=Serial diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index bddf22e..33e6a0c 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -28,6 +28,10 @@ jobs: cat keywords.txt echo "-----------------------------------------------------------" + - name: Copy confighelper.html + run: | + cp extras/confighelper/confighelper.html docs/html/ + - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@releases/v4 with: diff --git a/BresserWeatherSensorLW.ino b/BresserWeatherSensorLW.ino index 44a3904..8db155e 100644 --- a/BresserWeatherSensorLW.ino +++ b/BresserWeatherSensorLW.ino @@ -25,15 +25,15 @@ // Library dependencies (tested versions): // --------------------------------------- // (install via normal Arduino Library installer:) -// RadioLib 6.5.0 +// RadioLib 6.6.0 // LoRa_Serialization 3.2.1 // ESP32Time 2.0.6 -// BresserWeatherSensorReceiver 0.27.0 +// BresserWeatherSensorReceiver 0.28.2 // OneWireNg 0.13.1 (optional) // DallasTemperature 3.9.0 (optional) // NimBLE-Arduino 1.4.1 (optional) -// ATC MiThermometer 0.3.1 (optional) -// Theengs Decoder 1.7.2 (optional) +// ATC MiThermometer 0.4.2 (optional) +// Theengs Decoder 1.7.8 (optional) // // (installed from ZIP file:) // DistanceSensor_A02YYUW 1.0.2 (optional) @@ -82,9 +82,16 @@ // 20240504 PowerFeather: added BATTERY_CAPACITY_MAH to init() // Added BresserWeatherSensorLWCmd.h // 20240505 Implemented loading of LoRaWAN secrets from file on LittleFS (if available) +// 20230524 Modified PAYLOAD_SIZE: Moved define to header file, added small reserve +// to uplinkPayload[], modified actual size in sendReceive() +// 20240528 Disabled uplink transmission of LoRaWAN node status flags +// 20242529 Fixed payload size calculation +// 20240530 Updated to RadioLib v6.6.0 // // ToDo: -// - +// - Fix restoring nonces/session buffers after examples in +// https://github.com/radiolib-org/radiolib-persistence +// have been updated // // Notes: // - Set "Core Debug Level: Debug" for initial testing @@ -167,10 +174,6 @@ using namespace PowerFeather; // Time zone info const char *TZ_INFO = TZINFO_STR; -// Uplink message payload size -// The maximum allowed for all data rates is 51 bytes. -const uint8_t PAYLOAD_SIZE = 51; - // Time source & status, see below // // bits 0..3 time source @@ -746,20 +749,22 @@ void setup() gotoSleep(sleepDuration()); } - // build payload byte array - uint8_t uplinkPayload[PAYLOAD_SIZE]; + // build payload byte array (+ reserve to prevent overflow with configuration at run-time) + uint8_t uplinkPayload[PAYLOAD_SIZE + 8]; LoraEncoder encoder(uplinkPayload); - // LoRaWAN node status flags - encoder.writeBitmap(0, - 0, - 0, - 0, - 0, - longSleep, - 0, - 0); + // Note: + // This should be enabled by a LoRaWAN downlink command if required. + // // LoRaWAN node status flags + // encoder.writeBitmap(0, + // 0, + // 0, + // 0, + // 0, + // longSleep, + // 0, + // 0); appLayer.getPayloadStage1(1, encoder); @@ -785,25 +790,22 @@ void setup() // recall session from RTC deep-sleep preserved variable state = node.setBufferSession(LWsession); // send them to LoRaWAN stack + // if we have booted at least once we should have a session to restore, so report any failure // otherwise no point saying there's been a failure when it was bound to fail with an empty // LWsession var. At this point, bootCount has already been incremented, hence the > 2 debug((state != RADIOLIB_ERR_NONE) && (bootCount > 2), "Restoring session buffer failed", state, false); - // process the restored session or failing that, create a new one & - // return flag to indicate a fresh join is required - log_d("Setup LoRaWAN session"); - state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey, false); - // see comment above, no need to report a failure that is bound to occur on first boot - debug((state != RADIOLIB_ERR_NONE) && (bootCount > 2), "Restore session failed", state, false); + // Setup the OTAA session information + node.beginOTAA(joinEUI, devEUI, nwkKey, appKey); // loop until successful join - while (state != RADIOLIB_ERR_NONE) + while ((state != RADIOLIB_LORAWAN_NEW_SESSION) && (state != RADIOLIB_LORAWAN_SESSION_RESTORED)) { log_d("Join ('login') to the LoRaWAN Network"); - state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey, true); + state = node.activateOTAA(); - if (state < RADIOLIB_ERR_NONE) + if ((state != RADIOLIB_LORAWAN_NEW_SESSION) && (state != RADIOLIB_LORAWAN_SESSION_RESTORED)) { log_d("Join failed: %d", state); @@ -874,11 +876,11 @@ void setup() } // Retrieve the last uplink frame counter - uint32_t fcntUp = node.getFcntUp(); + uint32_t fCntUp = node.getFCntUp(); // Send a confirmed uplink every 64th frame // and also request the LinkCheck command - if (fcntUp % 64 == 0) + if (fCntUp % 64 == 0) { log_i("[LoRaWAN] Requesting LinkCheck"); node.sendMacCommandReq(RADIOLIB_LORAWAN_MAC_LINK_CHECK); @@ -896,14 +898,39 @@ void setup() LoRaWANEvent_t uplinkDetails; LoRaWANEvent_t downlinkDetails; - // perform an uplink & optionally receive downlink - if (fcntUp % 64 == 0) + uint8_t payloadSize = encoder.getLength(); + if (payloadSize > PAYLOAD_SIZE) { - state = node.sendReceive(uplinkPayload, encoder.getLength(), port, downlinkPayload, &downlinkSize, true, &uplinkDetails, &downlinkDetails); + log_w("Payload size exceeds maximum of %u bytes - truncating", PAYLOAD_SIZE); + payloadSize = PAYLOAD_SIZE; + } + + // perform an uplink & optionally receive downlink + if (fCntUp % 64 == 0) + { + state = node.sendReceive( + uplinkPayload, + payloadSize, + port, + downlinkPayload, + &downlinkSize, + true, + &uplinkDetails, + &downlinkDetails + ); } else { - state = node.sendReceive(uplinkPayload, encoder.getLength(), port, downlinkPayload, &downlinkSize, false, nullptr, &downlinkDetails); + state = node.sendReceive( + uplinkPayload, + payloadSize, + port, + downlinkPayload, + &downlinkSize, + false, + nullptr, + &downlinkDetails + ); } debug((state != RADIOLIB_LORAWAN_NO_DOWNLINK) && (state != RADIOLIB_ERR_NONE), "Error in sendReceive", state, false); @@ -919,9 +946,9 @@ void setup() log_i("Downlink data: "); arrayDump(downlinkPayload, downlinkSize); - if (downlinkDetails.port > 0) + if (downlinkDetails.fPort > 0) { - uplinkReq = decodeDownlink(downlinkDetails.port, downlinkPayload, downlinkSize); + uplinkReq = decodeDownlink(downlinkDetails.fPort, downlinkPayload, downlinkSize); } } else @@ -945,8 +972,8 @@ void setup() log_d("[LoRaWAN] Datarate:\t%d", downlinkDetails.datarate); log_d("[LoRaWAN] Frequency:\t%7.3f MHz", downlinkDetails.freq); log_d("[LoRaWAN] Output power:\t%d dBm", downlinkDetails.power); - log_d("[LoRaWAN] Frame count:\t%u", downlinkDetails.fcnt); - log_d("[LoRaWAN] Port:\t\t%u", downlinkDetails.port); + log_d("[LoRaWAN] Frame count:\t%u", downlinkDetails.fCnt); + log_d("[LoRaWAN] fPort:\t\t%u", downlinkDetails.fPort); } uint32_t networkTime = 0; @@ -979,7 +1006,7 @@ void setup() sendCfgUplink(uplinkReq); } - log_d("FcntUp: %u", node.getFcntUp()); + log_d("FcntUp: %u", node.getFCntUp()); // now save session to RTC memory uint8_t *persist = node.getBufferSession(); diff --git a/BresserWeatherSensorLWCfg.h b/BresserWeatherSensorLWCfg.h index a227c7e..b353f09 100644 --- a/BresserWeatherSensorLWCfg.h +++ b/BresserWeatherSensorLWCfg.h @@ -46,6 +46,11 @@ // 20240430 Modified battery voltage measurement // 20240504 PowerFeather: added BATTERY_CAPACITY_MAH // Moved LoRaWAN command interface to BresserWeatherSensorLWCmd.h +// 20240520 Added definitions for AppLayer payload configuration +// 20240521 Added UBATT_CH/USUPPLY_CH +// 20240524 Added sensor feature flags +// Moved PAYLOAD_SIZE from BresserWeatherSensorLW.ino +// 20240528 Added encoding of invalid values, modified default payload, fixes // // Note: // Depending on board package file date, either @@ -61,6 +66,7 @@ #if !defined(_LWCFG_H) #define _LWCFG_H +#include // Enable debug mode (debug messages via serial port) // Arduino IDE: Tools->Core Debug Level: "Debug|Verbose" @@ -90,6 +96,10 @@ //#define ARDUINO_THINGPULSE_EPULSE_FEATHER #endif +// Uplink message payload size +// The maximum allowed for all data rates is 51 bytes. +const uint8_t PAYLOAD_SIZE = 51; + // Battery voltage thresholds for energy saving & deep-discharge prevention // If battery voltage <= BATTERY_WEAK [mV], MCU will sleep for SLEEP_INTERVAL_LONG @@ -129,7 +139,7 @@ #define ADC_EN // Enable OneWire temperature measurement -// #define ONEWIRE_EN +#define ONEWIRE_EN // Enable BLE temperature/humidity measurement // Notes: @@ -152,6 +162,7 @@ // Enable Ultrasonic Distance Sensor #if defined(LORAWAN_NODE) || defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) // #define DISTANCESENSOR_EN +// #define DISTANCESENSOR_CH 8 #endif @@ -217,6 +228,9 @@ const float SUPPLY_DIV = 0.5; const uint8_t SUPPLY_SAMPLES = 10; #endif +// "Channel" in appPayloadCfg +#define USUPPLY_CH 1 + #ifdef PIN_ADC1_IN // Voltage divider R1 / (R1 + R2) -> V_meas = V(R1 + R2); V_adc = V(R1) const float ADC1_DIV = 0.5; @@ -275,6 +289,9 @@ const float UBATT_DIV = 0.2041; const float UBATT_DIV = 0.5; #endif const uint8_t UBATT_SAMPLES = 10; + +// "Channel" appPayloadCfg +#define UBATT_CH 0 #endif #if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) @@ -290,4 +307,138 @@ const uint8_t UBATT_SAMPLES = 10; } #endif +/// AppLayer payload configuration size in bytes +#define APP_PAYLOAD_CFG_SIZE 24 + +// --- Default AppLayer payload configuration --- + +// For each sensor/interface type, there is a set of flags. +// If a flag is set, the "channel" is enabled (according to the flags bit position). +// For sensors which use a fixed channel, the flags are used to select +// which signals (features) shall be included in the payload. + +// -- Sensor feature flags -- + +// Weather Sensor +#define PAYLOAD_WS_HUMIDITY 0b00000010 +#define PAYLOAD_WS_WIND 0b00000100 +#define PAYLOAD_WS_RAINGAUGE 0b00001000 +#define PAYLOAD_WS_LIGHT 0b00010000 +#define PAYLOAD_WS_UV 0b00100000 +#define PAYLOAD_WS_RAIN_H 0b01000000 // Rain post-processing; hourly rainfall +#define PAYLOAD_WS_RAIN_DWM 0b10000000 // Rain post-processing; daily, weekly, monthly + +// Lightning sensor +#define PAYLOAD_LIGHTNING_RAW 0b00010000 // Sensor raw data +#define PAYLOAD_LIGHTNING_PROC 0b00100000 // Post-processed lightning data + +// -- 868 MHz Sensor Types -- +// 0 - Weather Station; 1 Ch +// Note: Included in APP_PAYLOAD_CFG_TYPE01 +#define APP_PAYLOAD_CFG_TYPE00 0x00 + +// 1 - Weather Station; 1 Ch +// - Professional Wind Gauge (with T and H); 1 Ch +// - Professional Rain Gauge (with T); 1 Ch +// Note: Type encoded as 0x9/0xA/0xB in radio message, +// but changed to 1 in BresserWeatherSensorReceiver! +#define APP_PAYLOAD_CFG_TYPE01 ( \ + 1 /* enable sensor */ | \ + PAYLOAD_WS_HUMIDITY | \ + PAYLOAD_WS_WIND | \ + PAYLOAD_WS_RAINGAUGE | \ + /* PAYLOAD_WS_LIGHT | */ \ + PAYLOAD_WS_UV | \ + PAYLOAD_WS_RAIN_H | \ + PAYLOAD_WS_RAIN_DWM \ +) + +// 2 - Thermo-/Hygro-Sensor; 7 Ch +// Ch: 1 +#define APP_PAYLOAD_CFG_TYPE02 0x02 + +// 3 - Pool / Spa Thermometer; 7 Ch +// Ch: 1 +#define APP_PAYLOAD_CFG_TYPE03 0x00 + +// 4 - Soil Moisture Sensor; 7 Ch +// Ch: 1 +#define APP_PAYLOAD_CFG_TYPE04 0x02 + +// 5 - Water Leakage Sensor; 7 Ch +// Ch: 1 +#define APP_PAYLOAD_CFG_TYPE05 0x00 + +// 6 - reserved +#define APP_PAYLOAD_CFG_TYPE06 0x00 + +// 7 - reserved +#define APP_PAYLOAD_CFG_TYPE07 0x00 + +// 8 - Air Quality Sensor PM2.5/PM10; 4 Ch +#define APP_PAYLOAD_CFG_TYPE08 0x00 + +// 9 - Lightning Sensor; 1 Ch +// Ch: 0 +#define APP_PAYLOAD_CFG_TYPE09 ( \ + 1 /* enable sensor */ | \ + /* PAYLOAD_LIGHTNING_RAW | */ \ + PAYLOAD_LIGHTNING_PROC \ +) + +// 10 - CO2 Sensor; 4 Ch +#define APP_PAYLOAD_CFG_TYPE10 0x00 + +// 11 - HCHO/VCO Sensor; 4 Ch +#define APP_PAYLOAD_CFG_TYPE11 0x00 + +// 12 - reserved +#define APP_PAYLOAD_CFG_TYPE12 0x00 + +// 13 - reserved +#define APP_PAYLOAD_CFG_TYPE13 0x00 + +// 14 - reserved +#define APP_PAYLOAD_CFG_TYPE14 0x00 + +// 15 - reserved +#define APP_PAYLOAD_CFG_TYPE15 0x00 + +// -- 1-Wire Sensors -- +// Index: 0 +#define APP_PAYLOAD_CFG_ONEWIRE1 0x00 // onewire[15:8] +#define APP_PAYLOAD_CFG_ONEWIRE0 0x01 // onewire[7:0] + +// -- Analog Inputs -- +// 0x01: Battery Voltage +// 0x02: Supply Voltage +#define APP_PAYLOAD_CFG_ANALOG1 0x00 // analog[15:8] +#define APP_PAYLOAD_CFG_ANALOG0 0x01 // analog[7:0] + +// -- Digital Inputs -- +// Assign to any type of "channel", +// e.g. GPIO, SPI, I2C, UART, ... +#define APP_PAYLOAD_CFG_DIGITAL3 0x00 // digital[31:24] +#define APP_PAYLOAD_CFG_DIGITAL2 0x00 // digital[23:16] +#define APP_PAYLOAD_CFG_DIGITAL1 0x00 // digital[15:8] +#define APP_PAYLOAD_CFG_DIGITAL0 0x00 // digital[7:0] + +#define APP_PAYLOAD_OFFS_ONEWIRE 16 +#define APP_PAYLOAD_BYTES_ONEWIRE 2 + +#define APP_PAYLOAD_OFFS_ANALOG 18 +#define APP_PAYLOAD_BYTES_ANALOG 2 + +#define APP_PAYLOAD_OFFS_DIGITAL 20 +#define APP_PAYLOAD_BYTES_DIGITAL 4 + +// Encoding of invalid values +// for floating point, see +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN +#define INV_FLOAT 0x3FFFFFFF +#define INV_UINT32 0xFFFFFFFF +#define INV_UINT16 0xFFFF +#define INV_UINT8 0xFF +#define INV_TEMP 327.67 + #endif // _LWCFG_H diff --git a/BresserWeatherSensorLWCmd.h b/BresserWeatherSensorLWCmd.h index 9dc6888..174296c 100644 --- a/BresserWeatherSensorLWCmd.h +++ b/BresserWeatherSensorLWCmd.h @@ -308,5 +308,73 @@ // Response: n.a. +// CMD_GET_APP_PAYLOAD_CFG +// ----------------------- +// Port: CMD_GET_APP_PAYLOAD_CFG +#define CMD_GET_APP_PAYLOAD_CFG 0xCE + +// Downlink (command): +// byte0: 0x00 + +// Response: n.a. +// Uplink (command): +// byte00: type00[7:0] +// byte01: type01[7:0] +// byte02: type02[7:0] +// byte03: type03[7:0] +// byte04: type04[7:0] +// byte05: type05[7:0] +// byte06: type06[7:0] +// byte07: type07[7:0] +// byte08: type08[7:0] +// byte09: type09[7:0] +// byte10: type10[7:0] +// byte11: type11[7:0] +// byte12: type12[7:0] +// byte13: type13[7:0] +// byte14: type14[7:0] +// byte15: type15[7:0] +// byte16: onewire[15:8] +// byte17: onewire[7:0] +// byte18: analog[15:8] +// byte19: analog[7:0] +// byte20: digital[31:24] +// byte21: digital[23:16] +// byte22: digital[15:8] +// byte23: digital[7:0] + +// CMD_SET_APP_PAYLOAD_CFG +// Port: CMD_SET_APP_PAYLOAD_CFG +#define CMD_SET_APP_PAYLOAD_CFG 0xCF + +// Uplink (command): +// byte00: type00[7:0] +// byte01: type01[7:0] +// byte02: type02[7:0] +// byte03: type03[7:0] +// byte04: type04[7:0] +// byte05: type05[7:0] +// byte06: type06[7:0] +// byte07: type07[7:0] +// byte08: type08[7:0] +// byte09: type09[7:0] +// byte10: type10[7:0] +// byte11: type11[7:0] +// byte12: type12[7:0] +// byte13: type13[7:0] +// byte14: type14[7:0] +// byte15: type15[7:0] +// byte16: onewire[15:8] +// byte17: onewire[7:0] +// byte18: analog[15:8] +// byte19: analog[7:0] +// byte20: digital[31:24] +// byte21: digital[23:16] +// byte22: digital[15:8] +// byte23: digital[7:0] + +// Response: n.a. + + // =========================== #endif \ No newline at end of file diff --git a/README.md b/README.md index 451d4f6..7207e3f 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ This is a remake of [BresserWeatherSensorTTN](https://github.com/matthias-bs/Bre * [Using Raw Data](#using-raw-data) * [Using the Javascript Uplink/Downlink Formatters](#using-the-javascript-uplinkdownlink-formatters) * [Loading LoRaWAN Network Service Credentials from File](#loading-lorawan-network-service-credentials-from-file) +* [Payload Configuration](#payload-configuration) * [Customizing the Application Layer](#customizing-the-application-layer) * [Doxygen Generated Source Code Documentation](#doxygen-generated-source-code-documentation) * [References](#references) @@ -386,6 +387,10 @@ Many software parameters can be defined at compile time, i.e. in [BresserWeather | | BLE active scan; 1 (active scan) / 0 (passive scan) | | | BLE scan time in seconds; 0...255 | | | BLE sensor MAC addresses; e.g. "DE:AD:BE:EF:12:23" | +| \ | Bitmap for enabling Bresser sensors of type \; each bit position corresponds to a channel,
e.g. bit 0 controls channel 0; unused bits can be used to select features | +| \ | Bitmap for enabling 1-Wire sensors; each bit position corresponds to an index | +| \ | Bitmap for enabling analog input channels; each bit position corresponds to a channel | +| \ | Bitmap for enabling digital input channels in a broader sense — GPIO, SPI, I2C, UART, ... | > [!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.) @@ -423,6 +428,8 @@ Many software parameters can be defined at compile time, i.e. in [BresserWeather | CMD_SET_BLE_ADDR | 0xC9 (201) | ble_addr0[47:40]
ble_addr0[39:32]
ble_addr0[31:24]
ble_addr0[23:15]
ble_addr0[16:8]
ble_addr0[7:0]
... | n.a. | | CMD_GET_BLE_CONFIG | 0xCA (202) | 0x00 | ble_active[7:0]
ble_scantime[7:0] | | CMD_SET_BLE_CONFIG | 0xCB (203) | ble_active[7:0]
ble_scantime[7:0] | n.a. | +| CMD_GET_APP_PAYLOAD_CFG | 0xCE (206) | 0x00 | type00[7:0]
type01[7:0]
...
type15[7:0]
onewire[15:8]
onewire[7:0]
analog[15:8]
analog[7:0]
digital[31:24]
digital[23:16]
digital[15:8]
digital[7:0] | +| CMD_SET_APP_PAYLOAD_CFG | 0xCF (207) | type00[7:0]
type01[7:0]
...
type15[7:0]
onewire[15:8]
onewire[7:0]
analog[15:8]
analog[7:0]
digital[31:24]
digital[23:16]
digital[15:8]
digital[7:0] | n.a. | #### The Things Network Examples @@ -448,7 +455,7 @@ Many software parameters can be defined at compile time, i.e. in [BresserWeather | ----------------------------- | ------------------------------------------------------------------------- | ---------------------------- | | CMD_GET_DATETIME | {"cmd": "CMD_GET_DATETIME"} | {"epoch": \} | | CMD_SET_DATETIME | {"epoch": \} | n.a. | -| CMD_SET_SLEEP_INTERVAL | {"sleep_interval": "} | n.a. | +| CMD_SET_SLEEP_INTERVAL | {"sleep_interval": } | n.a. | | CMD_SET_SLEEP_INTERVAL_LONG | {"sleep_interval_long": } | n.a. | | CMD_GET_LW_CONFIG | {"cmd": "CMD_GET_LW_CONFIG"} | {"sleep_interval": , "sleep_interval_long": } | | CMD_GET_WS_TIMEOUT | {"cmd": "CMD_GET_WS_TIMEOUT"} | {"ws_timeout": } | @@ -464,6 +471,8 @@ Many software parameters can be defined at compile time, i.e. in [BresserWeather | CMD_SET_BLE_ADDR | {"ble_addr": [, ..., ]} | n.a. | | CMD_GET_BLE_CONFIG | {"cmd": "CMD_GET_BLE_CONFIG"} | {"ble_active": , "ble_scantime": } | | CMD_SET_BLE_CONFIG | {"ble_active": , "ble_scantime": } | n.a. | +| CMD_GET_APP_PAYLOAD_CFG | {"cmd": "CMD_GET_APP_PAYLOAD_CFG"} | {"bresser": [\, \, ..., \], "onewire": \, "analog": \, "digital": \} | +| CMD_SET_APP_PAYLOAD_CFG | {"bresser": [\, \, ..., \], "onewire": \, "analog": \, "digital": \} | n.a. | #### The Things Network Examples @@ -491,6 +500,10 @@ Modify the example [data/secrets.json](data/secrets.json) as required and instal > [!WARNING] > Only very basic validation of the file `secrets.json` is implemented. +## Payload Configuration + +[Config Helper](https://matthias-bs.github.io/BresserWeatherSensorLW/confighelper.html) + ## Customizing the Application Layer By replacing the Application Layer with your own code, you can use this project as a starting point for your own purpose. diff --git a/config.h b/config.h index c4a6ab7..5114971 100644 --- a/config.h +++ b/config.h @@ -36,6 +36,8 @@ // 20240412 Created // 20240413 Added ESP32-S3 PowerFeather // 20240426 Added define ARDUINO_heltec_wifi_lora_32_V3 +// 20240530 Added stateDecode(), updated debug() from RadioLib v6.6.0 +// (examples/LoRaWAN/LoRaWAN_Reference/config.h) // // ToDo: // - @@ -304,11 +306,76 @@ uint8_t nwkKey[] = { RADIOLIB_LORAWAN_NWK_KEY }; // Create the LoRaWAN node LoRaWANNode node(&radio, &Region, subBand); +// result code to text ... +String stateDecode(const int16_t result) { + switch (result) { + case RADIOLIB_ERR_NONE: + return "ERR_NONE"; + case RADIOLIB_ERR_CHIP_NOT_FOUND: + return "ERR_CHIP_NOT_FOUND"; + case RADIOLIB_ERR_PACKET_TOO_LONG: + return "ERR_PACKET_TOO_LONG"; + case RADIOLIB_ERR_RX_TIMEOUT: + return "ERR_RX_TIMEOUT"; + case RADIOLIB_ERR_CRC_MISMATCH: + return "ERR_CRC_MISMATCH"; + case RADIOLIB_ERR_INVALID_BANDWIDTH: + return "ERR_INVALID_BANDWIDTH"; + case RADIOLIB_ERR_INVALID_SPREADING_FACTOR: + return "ERR_INVALID_SPREADING_FACTOR"; + case RADIOLIB_ERR_INVALID_CODING_RATE: + return "ERR_INVALID_CODING_RATE"; + case RADIOLIB_ERR_INVALID_FREQUENCY: + return "ERR_INVALID_FREQUENCY"; + case RADIOLIB_ERR_INVALID_OUTPUT_POWER: + return "ERR_INVALID_OUTPUT_POWER"; + case RADIOLIB_ERR_NETWORK_NOT_JOINED: + return "RADIOLIB_ERR_NETWORK_NOT_JOINED"; + + case RADIOLIB_ERR_DOWNLINK_MALFORMED: + return "RADIOLIB_ERR_DOWNLINK_MALFORMED"; + case RADIOLIB_ERR_INVALID_REVISION: + return "RADIOLIB_ERR_INVALID_REVISION"; + case RADIOLIB_ERR_INVALID_PORT: + return "RADIOLIB_ERR_INVALID_PORT"; + case RADIOLIB_ERR_NO_RX_WINDOW: + return "RADIOLIB_ERR_NO_RX_WINDOW"; + case RADIOLIB_ERR_INVALID_CID: + return "RADIOLIB_ERR_INVALID_CID"; + case RADIOLIB_ERR_UPLINK_UNAVAILABLE: + return "RADIOLIB_ERR_UPLINK_UNAVAILABLE"; + case RADIOLIB_ERR_COMMAND_QUEUE_FULL: + return "RADIOLIB_ERR_COMMAND_QUEUE_FULL"; + case RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND: + return "RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND"; + case RADIOLIB_ERR_JOIN_NONCE_INVALID: + return "RADIOLIB_ERR_JOIN_NONCE_INVALID"; + case RADIOLIB_ERR_N_FCNT_DOWN_INVALID: + return "RADIOLIB_ERR_N_FCNT_DOWN_INVALID"; + case RADIOLIB_ERR_A_FCNT_DOWN_INVALID: + return "RADIOLIB_ERR_A_FCNT_DOWN_INVALID"; + case RADIOLIB_ERR_DWELL_TIME_EXCEEDED: + return "RADIOLIB_ERR_DWELL_TIME_EXCEEDED"; + case RADIOLIB_ERR_CHECKSUM_MISMATCH: + return "RADIOLIB_ERR_CHECKSUM_MISMATCH"; + case RADIOLIB_LORAWAN_NO_DOWNLINK: + return "RADIOLIB_LORAWAN_NO_DOWNLINK"; + case RADIOLIB_LORAWAN_SESSION_RESTORED: + return "RADIOLIB_LORAWAN_SESSION_RESTORED"; + case RADIOLIB_LORAWAN_NEW_SESSION: + return "RADIOLIB_LORAWAN_NEW_SESSION"; + case RADIOLIB_LORAWAN_NONCES_DISCARDED: + return "RADIOLIB_LORAWAN_NONCES_DISCARDED"; + case RADIOLIB_LORAWAN_SESSION_DISCARDED: + return "RADIOLIB_LORAWAN_SESSION_DISCARDED"; + } + return "See TypeDef.h"; +} // Helper function to display any issues void debug(bool isFail, const char* message, int state, bool Freeze) { if (isFail) { - log_w("%s (%d)", message, state); + log_w("%s - %s (%d)", message, stateDecode(state).c_str(), state); while (Freeze); } } diff --git a/extras/confighelper/confighelper.html b/extras/confighelper/confighelper.html new file mode 100644 index 0000000..a75e51a --- /dev/null +++ b/extras/confighelper/confighelper.html @@ -0,0 +1,571 @@ + + + + + + Config Helper + + + + + +

Welcome to Config Helper

+
+
    +
  • Select desired sensors/interfaces via checkboxes.
  • +
  • If weather/lightning sensor is enabled, select options via checkboxes.
  • +
  • For other sensors, enter channel numbers separated by commas into text fields.
  • +
  • Check if assumptions regarding data field sizes are valid.
  • +
  • BLE sensors are enabled separately by specifying their MAC addresses.
  • +
+
+

+
+ + + + + + + + + + + + + + + + +

+ + + + + + +

+ +

+
+ + + +

+ + + +

+ + + +

+ + + +

+ +
+

+ +
+

+ +
+

+ +
+

+ + +
+
+
+

+ Copyright © 2024 Matthias Prinke, MIT License +

+ + + \ No newline at end of file diff --git a/package.json b/package.json index dcdf056..7210ea8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "BresserWeatherSensorLW", - "version": "0.5.0", + "version": "0.6.0", "description": "Bresser 868 MHz Weather Sensor Radio Receiver; provides data via LoRaWAN", "main": "BresserWeatherSensorLW.ino", "frameworks": "arduino", @@ -23,7 +23,7 @@ "homepage": "https://github.com/matthias-bs/BresserWeatherSensorLW#README", "dependencies": { "BresserWeatherSensorReceiver": "matthias-bs/BresserWeatherSensorReceiver#semver:^0.28.2", - "RadioLib": "jgromes/RadioLib#semver:^6.5.0", + "RadioLib": "jgromes/RadioLib#semver:^6.6.0", "lora-serialization": "thesolarnomad/lora-serialization#semver:^3.2.1", "ESP32Time": "fbiego/ESP32Time#semver:^2.0.6", "OneWireNg": "https://github.com/pstolarz/OneWireNg#semver:^0.13.3", diff --git a/scripts/downlink_formatter.js b/scripts/downlink_formatter.js index c023e6c..bb66cae 100644 --- a/scripts/downlink_formatter.js +++ b/scripts/downlink_formatter.js @@ -25,6 +25,8 @@ // port = CMD_SET_SENSORS_INC, {"sensors_inc": [, ..., ]} // port = CMD_GET_SENSORS_EXC, {"cmd": "CMD_GET_SENSORS_EXC"} / payload = 0x00 // port = CMD_SET_SENSORS_EXC, {"sensors_exc": [, ..., ]} +// port = CMD_GET_APP_PAYLOAD_CFG, {"cmd": "CMD_GET_APP_PAYLOAD_CFG"} / payload = 0x00 +// port = CMD_SET_APP_PAYLOAD_CFG, ["bresser": [, ... ], "onewire": , "analog": , "digital": ] // port = CMD_GET_BLE_ADDR, {"cmd": "CMD_GET_BLE_ADDR"} / payload = 0x00 // port = CMD_SET_BLE_ADDR, {"ble_addr": [, ..., ]} // port = CMD_GET_BLE_CONFIG, {"cmd": "CMD_GET_BLE_CONFIG"} / payload = 0x00 @@ -46,6 +48,8 @@ // // CMD_GET_SENSORS_CFG {"max_sensors": , "rx_flags": , "en_decoders": } // +// CMD_GET_APP_PAYLOAD_CFG {"bresser": [, , ..., ], "onewire": , "analog": , "digital": } +// // CMD_GET_BLE_ADDR {"ble_addr": [, ...]} // // CMD_GET_BLE_CONFIG {"ble_active": , "ble_scantime": } @@ -64,6 +68,11 @@ // : BLE scan mode - 0: passive / 1: active // : BLE scan time in seconds (0...255) // : e.g. "DE:AD:BE:EF:12:23" +// : Bitmap for enabling Bresser sensors of type N; each bit position corresponds to a channel, e.g. bit 0 controls ch0; +// unused bits can be used to select features +// : Bitmap for enabling 1-Wire sensors; each bit position corresponds to an index +// : Bitmap for enabling analog input channels; each bit positions corresponds to a channel +// : Bitmap for enabling digital input channels in a broad sense — GPIO, SPI, I2C, UART, ... // // // Based on: @@ -104,6 +113,8 @@ // 20240507 Added CMD_GET_SENSORS_CFG/CMD_SET_SENSORS_CFG // 20240508 Fixed decoding of raw data // Added en_decoders to CMD_GET_SENSORS_CFG/CMD_SET_SENSORS_CFG +// 20240519 Added CMD_GET_APP_PAYLOAD_CFG/CMD_SET_APP_PAYLOAD_CFG +// 20240530 Added decoding of CMD_GET_APP_PAYLOAD_CFG/CMD_SET_APP_PAYLOAD_CFG // // ToDo: // - @@ -125,6 +136,8 @@ const CMD_GET_SENSORS_EXC = 0xC6; const CMD_SET_SENSORS_EXC = 0xC7; const CMD_GET_SENSORS_CFG = 0xCC; const CMD_SET_SENSORS_CFG = 0xCD; +const CMD_GET_APP_PAYLOAD_CFG = 0xCE; +const CMD_SET_APP_PAYLOAD_CFG = 0xCF; const CMD_GET_BLE_ADDR = 0xC8; const CMD_SET_BLE_ADDR = 0xC9; const CMD_GET_BLE_CONFIG = 0xCA; @@ -178,6 +191,16 @@ function uint32BE(bytes) { return bytesToIntBE(bytes); } +function hex16(bytes) { + let res = "0x" + byte2hex(bytes[0]) + byte2hex(bytes[1]); + return res; +} + +function hex32(bytes) { + let res = "0x" + byte2hex(bytes[0]) + byte2hex(bytes[1]) + byte2hex(bytes[2]) + byte2hex(bytes[3]); + return res; +} + function byte2hex(byte) { return ('0' + byte.toString(16)).slice(-2); } @@ -201,6 +224,15 @@ function id32(bytes) { return res; } +function bresser_bitmaps(bytes) { + let res = []; + res[0] = "0x" + byte2hex(bytes[0]); + for (var i = 1; i < 16; i++) { + res[i] = "0x" + byte2hex(bytes[i]); + } + return res; +} + // Encode Downlink from JSON to bytes function encodeDownlink(input) { var i; @@ -257,6 +289,14 @@ function encodeDownlink(input) { errors: [] }; } + else if (input.data.cmd == "CMD_GET_APP_PAYLOAD_CFG") { + return { + bytes: [0], + fPort: CMD_GET_APP_PAYLOAD_CFG, + warnings: [], + errors: [] + }; + } else if (input.data.cmd == "CMD_GET_BLE_ADDR") { return { bytes: [0], @@ -374,15 +414,76 @@ function encodeDownlink(input) { warnings: [], errors: [] }; - } else if (input.data.hasOwnProperty('max_sensors') && - input.data.hasOwnProperty('rx_flags') && - input.data.hasOwnProperty('en_decoders')) { + } else if (input.data.hasOwnProperty('max_sensors') && + input.data.hasOwnProperty('rx_flags') && + input.data.hasOwnProperty('en_decoders')) { return { bytes: [input.data.max_sensors, input.data.rx_flags, input.data.en_decoders], fPort: CMD_SET_SENSORS_CFG, warnings: [], errors: [] }; + } else if (input.data.hasOwnProperty('bresser') && + input.data.hasOwnProperty('onewire') && + input.data.hasOwnProperty('analog') && + input.data.hasOwnProperty('digital')) { + if (input.data.bresser.length != 16) { + return { + bytes: [], + warnings: [], + errors: [": expected 16 bytes, got " + input.data.bresser.length] + }; + } + for (i = 0; i < input.data.bresser.length; i++) { + if (input.data.bresser[i].substr(0, 2) == "0x") { + value = parseInt(input.data.bresser[i].substr(2), 16); + output[i] = value; + } else { + return { + bytes: [], + warnings: [], + errors: ["'bresser': Invalid hex value"] + }; + } + } + if (input.data.onewire.substr(0, 2) == "0x") { + output[16] = parseInt(input.data.onewire.substr(2, 2), 16); + output[17] = parseInt(input.data.onewire.substr(4, 2), 16); + } else { + return { + bytes: [], + warnings: [], + errors: ["'onewire': Invalid hex value"] + }; + } + if (input.data.analog.substr(0, 2) == "0x") { + output[18] = parseInt(input.data.analog.substr(2, 2), 16); + output[19] = parseInt(input.data.analog.substr(4, 2), 16); + } else { + return { + bytes: [], + warnings: [], + errors: ["'analog': Invalid hex value"] + }; + } + if (input.data.digital.substr(0, 2) == "0x") { + output[20] = parseInt(input.data.digital.substr(2, 2), 16); + output[21] = parseInt(input.data.digital.substr(4, 2), 16); + output[22] = parseInt(input.data.digital.substr(6, 2), 16); + output[23] = parseInt(input.data.digital.substr(8, 2), 16); + } else { + return { + bytes: [], + warnings: [], + errors: ["'digital': Invalid hex value"] + }; + } + return { + bytes: output, + fPort: CMD_SET_APP_PAYLOAD_CFG, + warnings: [], + errors: [] + }; } else if (input.data.hasOwnProperty('ble_addr')) { output = []; k = 0; @@ -426,6 +527,7 @@ function decodeDownlink(input) { case CMD_GET_SENSORS_INC: case CMD_GET_SENSORS_EXC: case CMD_GET_SENSORS_CFG: + case CMD_GET_APP_PAYLOAD_CFG: case CMD_GET_BLE_ADDR: case CMD_GET_BLE_CONFIG: return { @@ -483,6 +585,15 @@ function decodeDownlink(input) { en_decoders: uint8(input.bytes.slice(2, 3)) } }; + case CMD_SET_APP_PAYLOAD_CFG: + return { + data: { + bresser: bresser_bitmaps(input.bytes.slice(0, 16)), + onewire: hex16(input.bytes.slice(16, 18)), + analog: hex16(input.bytes.slice(18, 20)), + digital: hex32(input.bytes.slice(20, 24)) + } + }; case CMD_SET_BLE_ADDR: return { data: { diff --git a/scripts/uplink_formatter.js b/scripts/uplink_formatter.js index 0a449db..1b18b75 100644 --- a/scripts/uplink_formatter.js +++ b/scripts/uplink_formatter.js @@ -1,4 +1,3 @@ - /////////////////////////////////////////////////////////////////////////////// // uplink_formatter.js // @@ -30,7 +29,7 @@ // port = CMD_SET_BLE_ADDR, {"ble_addr": [, ..., ]} // port = CMD_GET_BLE_CONFIG, {"cmd": "CMD_GET_BLE_CONFIG"} / payload = 0x00 // port = CMD_SET_BLE_CONFIG, {"ble_active": , "ble_scantime": } - +// port = CMD_GET_APP_PAYLOAD_CFG, {"cmd": "CMD_GET_APP_PAYLOAD_CFG"} / payload = 0x00 // // Responses: // ----------- @@ -52,6 +51,8 @@ // // CMD_GET_BLE_CONFIG {"ble_active": , "ble_scantime": } // +// CMD_GET_APP_PAYLOAD_CFG {"bresser": [, , ..., ], "onewire": , "analog": , "digital": } +// // : 0...255 // : 0...65535 // : 0...65535 @@ -66,6 +67,11 @@ // : BLE scan mode - 0: passive / 1: active // : BLE scan time in seconds (0...255) // : e.g. "DE:AD:BE:EF:12:23" +// : Bitmap for enabling Bresser sensors of type N; each bit position corresponds to a channel, e.g. bit 0 controls ch0; +// unused bits can be used to select features +// : Bitmap for enabling 1-Wire sensors; each bit position corresponds to an index +// : Bitmap for enabling analog input channels; each bit positions corresponds to a channel +// : Bitmap for enabling digital input channels in a broad sense — GPIO, SPI, I2C, UART, ... // Based on: // --------- @@ -103,7 +109,15 @@ // renamed from ttn_uplink_formatter.js // 20240427 Added BLE configuration // 20240507 Added CMD_GET_SENSORS_CFG -// 20240608 Added en_decoders to CMD_GET_SENSORS_CFG +// 20240508 Added en_decoders to CMD_GET_SENSORS_CFG +// 20240517 Added CMD_GET_APP_PAYLOAD_CFG +// 20240528 Modified sensor data payload decoder +// 20240529 Added uint8fp1 for UV index +// Added NaN results to decoding functions +// Added supression of NaN results in decoder +// 20240530 Added SKIP_INVALID_SIGNALS +// Added BLE signals to decoder +// 20240531 Fixed handling of arrays in decoder() // // ToDo: // - @@ -113,6 +127,9 @@ function decoder(bytes, port) { // bytes is of type Buffer + // Skip signals encoded as invalid + const SKIP_INVALID_SIGNALS = true; + const CMD_GET_DATETIME = 0x86; const CMD_GET_LW_CONFIG = 0xB1; const CMD_GET_WS_TIMEOUT = 0xC0; @@ -121,10 +138,9 @@ function decoder(bytes, port) { const CMD_GET_SENSORS_CFG = 0xCC; const CMD_GET_BLE_ADDR = 0xC8; const CMD_GET_BLE_CONFIG = 0xCA; + const CMD_GET_APP_PAYLOAD_CFG = 0xCE; - const ONEWIRE_EN = 0; - - var rtc_source_code = { + const rtc_source_code = { 0x00: "GPS", 0x01: "RTC", 0x02: "LORA", @@ -141,7 +157,7 @@ function decoder(bytes, port) { rtc_source.BYTES = 1; var bytesToInt = function (bytes) { - var i = 0; + let i = 0; for (var x = 0; x < bytes.length; x++) { i |= +(bytes[x] << (x * 8)); } @@ -150,7 +166,7 @@ function decoder(bytes, port) { // Big Endian var bytesToIntBE = function (bytes) { - var i = 0; + let i = 0; for (var x = 0; x < bytes.length; x++) { i |= +(bytes[x] << ((bytes.length - 1 - x) * 8)); } @@ -169,23 +185,48 @@ function decoder(bytes, port) { if (bytes.length !== uint8.BYTES) { throw new Error('int must have exactly 1 byte'); } - return bytesToInt(bytes); + let res = bytesToInt(bytes); + if (SKIP_INVALID_SIGNALS && res === 0xFF) { + return NaN; + } + return res; }; uint8.BYTES = 1; + var uint8fp1 = function (bytes) { + if (bytes.length !== uint8fp1.BYTES) { + throw new Error('int must have exactly 1 byte'); + } + let res = bytesToInt(bytes); + if (SKIP_INVALID_SIGNALS && res === 0xFF) { + return NaN; + } + res *= 0.1; + return res.toFixed(1); + }; + uint8fp1.BYTES = 1; + var uint16 = function (bytes) { if (bytes.length !== uint16.BYTES) { throw new Error('int must have exactly 2 bytes'); } - return bytesToInt(bytes); + let res = bytesToInt(bytes); + if (SKIP_INVALID_SIGNALS && res === 0xFFFF) { + return NaN; + } + return res; }; uint16.BYTES = 2; var uint16fp1 = function (bytes) { - if (bytes.length !== uint16.BYTES) { + if (bytes.length !== uint16fp1.BYTES) { throw new Error('int must have exactly 2 bytes'); } - var res = bytesToInt(bytes) * 0.1; + let res = bytesToInt(bytes); + if (SKIP_INVALID_SIGNALS && res === 0xFFFF) { + return NaN; + } + res *= 0.1; return res.toFixed(1); }; uint16fp1.BYTES = 2; @@ -219,26 +260,45 @@ function decoder(bytes, port) { } var mac48 = function (bytes) { - var res = []; - var size = bytes.length; - var j = 0; + let res = []; + let j = 0; for (var i = 0; i < bytes.length; i += 6) { res[j++] = byte2hex(bytes[i]) + ":" + byte2hex(bytes[i + 1]) + ":" + byte2hex(bytes[i + 2]) + ":" + byte2hex(bytes[i + 3]) + ":" + byte2hex(bytes[i + 4]) + ":" + byte2hex(bytes[i + 5]); } return res; - } + }; mac48.BYTES = bytes.length; + var bresser_bitmaps = function (bytes) { + let res = []; + for (var i = 0; i < 16; i++) { + res[i] = "0x" + byte2hex(bytes[i]); + } + return res; + }; + bresser_bitmaps.BYTES = 16; + + var hex16 = function (bytes) { + let res = "0x" + byte2hex(bytes[0]) + byte2hex(bytes[1]); + return res; + }; + hex16.BYTES = 2; + + var hex32 = function (bytes) { + let res = "0x" + byte2hex(bytes[0]) + byte2hex(bytes[1]) + byte2hex(bytes[2]) + byte2hex(bytes[3]); + return res; + }; + hex32.BYTES = 4; + var id32 = function (bytes) { - var res = []; - var size = bytes.length; - var j = 0; + let res = []; + let j = 0; for (var i = 0; i < bytes.length; i += 4) { res[j++] = "0x" + byte2hex(bytes[i]) + byte2hex(bytes[i + 1]) + byte2hex(bytes[i + 2]) + byte2hex(bytes[i + 3]); } return res; - } + }; id32.BYTES = bytes.length; var latLng = function (bytes) { @@ -246,8 +306,8 @@ function decoder(bytes, port) { 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)); + let lat = bytesToInt(bytes.slice(0, latLng.BYTES / 2)); + let lng = bytesToInt(bytes.slice(latLng.BYTES / 2, latLng.BYTES)); return [lat / 1e6, lng / 1e6]; }; @@ -257,11 +317,11 @@ function decoder(bytes, port) { 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) + let isNegative = bytes[0] & 0x80; + let 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); }); + let 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]) { @@ -270,11 +330,14 @@ function decoder(bytes, port) { } b = arr.map(Number).join(''); } - var t = parseInt(b, 2); + let t = parseInt(b, 2); if (isNegative) { t = -t; } t = t / 1e2; + if (SKIP_INVALID_SIGNALS && t == 327.67) { + return NaN; + } return t.toFixed(1); }; temperature.BYTES = 2; @@ -284,7 +347,10 @@ function decoder(bytes, port) { throw new Error('Humidity must have exactly 2 bytes'); } - var h = bytesToInt(bytes); + let h = bytesToInt(bytes); + if (SKIP_INVALID_SIGNALS && h === 0xFFFF) { + return NaN; + } return h / 1e2; }; humidity.BYTES = 2; @@ -302,6 +368,9 @@ function decoder(bytes, port) { var e = bits >>> 23 & 0xff; var m = (e === 0) ? (bits & 0x7fffff) << 1 : (bits & 0x7fffff) | 0x800000; var f = sign * m * Math.pow(2, e - 150); + if (e === 0x7F && m !== 0) { + return NaN; + } return f.toFixed(1); } rawfloat.BYTES = 4; @@ -310,8 +379,8 @@ function decoder(bytes, port) { 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); + let i = bytesToInt(byte); + let bm = ('00000000' + Number(i).toString(2)).slice(-8).split('').map(Number).map(Boolean); return ['res7', 'res6', 'res5', 'res4', 'res3', 'res2', 'res1', 'res0'] .reduce(function (obj, pos, index) { @@ -325,8 +394,8 @@ function decoder(bytes, port) { 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); + let i = bytesToInt(byte); + let bm = ('00000000' + Number(i).toString(2)).slice(-8).split('').map(Number).map(Boolean); // Only Weather Sensor //return ['res5', 'res4', 'res3', 'res2', 'res1', 'res0', 'dec_ok', 'batt_ok'] // Weather Sensor + MiThermo (BLE) Sensor @@ -340,8 +409,18 @@ function decoder(bytes, port) { }; bitmap_sensors.BYTES = 1; + /** + * Decodes the given bytes using the provided mask and names. + * + * @param {Array} bytes - The bytes to decode. + * @param {Array} mask - The mask used for decoding. + * @param {Array} [names] - The names of the decoded values. + * @returns {Object} - The decoded values as an object. + * @throws {Error} - If the length of the bytes is less than the mask length. + */ var decode = function (bytes, mask, names) { + // Sum of all mask bytes var maskLength = mask.reduce(function (prev, cur) { return prev + cur.BYTES; }, 0); @@ -354,10 +433,16 @@ function decoder(bytes, port) { return mask .map(function (decodeFn) { var current = bytes.slice(offset, offset += decodeFn.BYTES); - return decodeFn(current); + var decodedValue = decodeFn(current); + if (isNaN(decodedValue) && decodedValue.constructor === Number) { + return null; + } + return decodedValue; }) .reduce(function (prev, cur, idx) { - prev[names[idx] || idx] = cur; + if (cur !== null) { + prev[names[idx] || idx] = cur; + } return prev; }, {}); }; @@ -371,12 +456,14 @@ function decoder(bytes, port) { uint16BE: uint16BE, uint32BE: uint32BE, mac48: mac48, + bresser_bitmaps: bresser_bitmaps, temperature: temperature, humidity: humidity, latLng: latLng, bitmap_node: bitmap_node, bitmap_sensors: bitmap_sensors, rawfloat: rawfloat, + uint8fp1: uint8fp1, uint16fp1: uint16fp1, rtc_source: rtc_source, decode: decode @@ -385,43 +472,45 @@ function decoder(bytes, port) { if (port === 1) { - if (ONEWIRE_EN) { - return decode( - bytes, - [bitmap_node, bitmap_sensors, temperature, uint8, - uint16fp1, uint16fp1, uint16fp1, - rawfloat, uint16, temperature, - temperature, uint8, temperature, uint8, - rawfloat, rawfloat, rawfloat, rawfloat, - unixtime, uint16, uint8 - ], - ['status_node', 'status', 'air_temp_c', 'humidity', - 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', - 'rain_mm', 'supply_v', 'water_temp_c', - 'indoor_temp_c', 'indoor_humidity', 'soil_temp_c', 'soil_moisture', - 'rain_hr', 'rain_day', 'rain_week', 'rain_mon', - 'lightning_time', 'lightning_events', 'lightning_distance_km' - ] - ); - } else { - return decode( - bytes, - [bitmap_node, bitmap_sensors, temperature, uint8, - uint16fp1, uint16fp1, uint16fp1, - rawfloat, uint16, - temperature, uint8, temperature, uint8, - rawfloat, rawfloat, rawfloat, rawfloat, - unixtime, uint16, uint8 - ], - ['status_node', 'status', 'air_temp_c', 'humidity', - 'wind_gust_meter_sec', 'wind_avg_meter_sec', 'wind_direction_deg', - 'rain_mm', 'supply_v', - 'indoor_temp_c', 'indoor_humidity', 'soil_temp_c', 'soil_moisture', - 'rain_hr', 'rain_day', 'rain_week', 'rain_mon', - 'lightning_time', 'lightning_events', 'lightning_distance_km' - ] - ); - } + return decode( + bytes, + [ + temperature, + uint8, + rawfloat, + uint16fp1, uint16fp1, uint16fp1, + uint8fp1, + rawfloat, + rawfloat, rawfloat, rawfloat, + temperature, uint8, + temperature, uint8, + unixtime, + uint16, + uint8, + temperature, + uint16, + temperature, + uint8 + ], + [ + 'ws_temp_c', + 'ws_humidity', + 'ws_rain_mm', + 'ws_wind_gust_ms', 'ws_wind_avg_ms', 'ws_wind_dir_deg', + 'ws_uv', + 'ws_rain_hourly_mm', + 'ws_rain_daily_mm', 'ws_rain_weekly_mm', 'ws_rain_monthly_mm', + 'th1_temp_c', 'th1_humidity', + 'soil1_temp_c', 'soil1_moisture', + 'lgt_time', + 'lgt_events', + 'lgt_distance_km', + 'ow0_temp_c', + 'a0_voltage_mv', + 'ble0_temp_c', + 'ble0_humidity', + ] + ); } else if (port === CMD_GET_DATETIME) { return decode( @@ -486,6 +575,13 @@ function decoder(bytes, port) { ], ['ble_active', 'ble_scantime'] ); + } else if (port === CMD_GET_APP_PAYLOAD_CFG) { + return decode( + bytes, + [bresser_bitmaps, hex16, hex16, hex32 + ], + ['bresser', 'onewire', 'analog', 'digital'] + ); } } diff --git a/secrets.h b/secrets.h index e74d7a3..a48d4f3 100644 --- a/secrets.h +++ b/secrets.h @@ -1,15 +1,117 @@ -// JoinEUI - previous versions of LoRaWAN called this AppEUI -// for development purposes you can use all zeros - see wiki for details -#define RADIOLIB_LORAWAN_JOIN_EUI 0x0000000000000000 - -// The Device EUI & two keys can be generated on the TTN console (or any other LoRaWAN Network Service Provider's console) -#pragma message("Replace the dummy values for RADIOLIB_LORAWAN_DEV_EUI, RADIOLIB_LORAWAN_APP_KEY and RADIOLIB_LORAWAN_NWK_KEY by your own credentials.") -#ifndef RADIOLIB_LORAWAN_DEV_EUI // Replace with your Device EUI -#define RADIOLIB_LORAWAN_DEV_EUI 0x0000000000000000 -#endif -#ifndef RADIOLIB_LORAWAN_APP_KEY // Replace with your App Key -#define RADIOLIB_LORAWAN_APP_KEY 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +#define SECRETS + +//------------------------------- +// --> REPLACE BY YOUR VALUES <-- +//------------------------------- + +#if 0 +// application identifier - pre-LoRaWAN 1.1.0, this was called appEUI +// when adding new end device in TTN, you will have to enter this number +// you can pick any number you want, but it has to be unique +uint64_t joinEUI = 0x12AD1011B0C0FFEE; + +// device identifier - this number can be anything +// when adding new end device in TTN, you can generate this number, +// or you can set any value you want, provided it is also unique +uint64_t devEUI = 0x70B3D57ED005E120; + +// select some encryption keys which will be used to secure the communication +// there are two of them - network key and application key +// because LoRaWAN uses AES-128, the key MUST be 16 bytes (or characters) long + +// network key is the ASCII string "topSecretKey1234" +uint8_t nwkKey[] = { 0x74, 0x6F, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x4B, 0x65, 0x79, 0x31, 0x32, 0x33, 0x34 }; + +// application key is the ASCII string "aDifferentKeyABC" +uint8_t appKey[] = { 0x61, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, + 0x6E, 0x74, 0x4B, 0x65, 0x79, 0x41, 0x42, 0x43 }; + +// prior to LoRaWAN 1.1.0, only a single "nwkKey" is used +// when connecting to LoRaWAN 1.0 network, "appKey" will be disregarded +// and can be set to NULL #endif -#ifndef RADIOLIB_LORAWAN_NWK_KEY // Put your Nwk Key here -#define RADIOLIB_LORAWAN_NWK_KEY 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + + +// Adafruit RFM9x LoRa Radio #1 +//#define RADIO1 +//#define RADIO4 +#define RADIO5 +//#define RADIO7 +//#define RADIO9 + +#ifdef RADIO1 +static const uint64_t devEUI = 0x9876B6000011C941; + +static const uint64_t joinEUI = 0x0000000000000000; + +static const std::uint8_t nwkKey[] = { 0x81, 0x83, 0xF4, 0xDD, 0x1B, 0x1F, 0xCF, 0x36, 0xC3, 0x73, 0x2A, 0xBE, 0x7C, 0xA2, 0x2F, 0x92 }; + +static const std::uint8_t appKey[] = { 0x24, 0xD4, 0x43, 0xCD, 0xB4, 0xD5, 0xDE, 0x3F, 0x9E, 0x17, 0xB7, 0x19, 0xE1, 0xD7, 0x16, 0xA2 }; + +#elif defined(RADIO4) +// Adafruit RFM9x LoRa Radio #4 + +static const uint64_t devEUI = 0x9876B6000011F87A; + +static const uint64_t joinEUI = 0x0000000000000000; + +static const std::uint8_t nwkKey[] = { 0x81, 0x83, 0xF4, 0xDD, 0x1B, 0x1F, 0xCF, 0x36, 0xC3, 0x73, 0x2A, 0xBE, 0x7C, 0xA2, 0x2F, 0x92 }; + +static const std::uint8_t appKey[] = { 0x39, 0x15, 0x2C, 0xBB, 0xDA, 0xEC, 0x25, 0xC6, 0x53, 0x4F, 0xA2, 0x20, 0x40, 0x04, 0xFA, 0xC8 }; + +#elif defined(RADIO5) + +#define RADIOLIB_LORAWAN_DEV_EUI 0x0C8B95AAB05CFEFF + +//static const uint64_t joinEUI = 0x0000000000000000; + +#define RADIOLIB_LORAWAN_NWK_KEY 0x81, 0x83, 0xF4, 0xDD, 0x1B, 0x1F, 0xCF, 0x36, 0xC3, 0x73, 0x2A, 0xBE, 0x7C, 0xA2, 0x2F, 0x92 + +#define RADIOLIB_LORAWAN_APP_KEY 0x73, 0xF9, 0x96, 0x32, 0x29, 0x2A, 0xF9, 0x23, 0xC0, 0x7D, 0x26, 0x43, 0x1D, 0x0C, 0x9A, 0xBC + +#elif defined(RADIO6) +static const uint64_t devEUI = 0x0C8B95AAAE6CFEFF; + +static const uint64_t joinEUI = 0x0000000000000000; + +static const std::uint8_t nwkKey[] = { 0x81, 0x83, 0xF4, 0xDD, 0x1B, 0x1F, 0xCF, 0x36, 0xC3, 0x73, 0x2A, 0xBE, 0x7C, 0xA2, 0x2F, 0x92 }; + +static const std::uint8_t appKey[] = { 0x59, 0xDC, 0x28, 0x58, 0xF6, 0x0C, 0x54, 0x70, 0x4E, 0x2E, 0xCB, 0x5E, 0x7B, 0x10, 0x79, 0x70 }; + +#elif defined(RADIO7) +// FireBeetleCover LoRa Radio #7 +// pv-inverter-0 + +#define RADIOLIB_LORAWAN_DEV_EUI 0x70B3D57ED005CF56 + +// appeui, little-endian (lsb first) +///static const uint64_t joinEUI = 0x0000000000000000; + +#define RADIOLIB_LORAWAN_NWK_KEY 0x2B, 0xA8, 0x2A, 0x2D, 0xCE, 0x8F, 0xC5, 0x01, 0x4A, 0xBB, 0xFA, 0x7A, 0xF7, 0x97, 0x1D, 0x3B + +#define RADIOLIB_LORAWAN_APP_KEY 0xD6, 0xB3, 0x3F, 0xBB, 0x7B, 0x3B, 0x01, 0xC8, 0x04, 0x8A, 0xCF, 0x9E, 0x24, 0x12, 0x45, 0xC1 + +#elif defined(RADIO8) +// FireBeetleCover LoRa Radio #8 +// pv-inverter-1 + +static const uint64_t devEUI[] = 0x70B3D57ED005D3B9; + +static const uint64_t joinEUI = 0x0000000000000000; + +static const std::uint8_t nwkKey[] = { 0x81, 0x83, 0xF4, 0xDD, 0x1B, 0x1F, 0xCF, 0x36, 0xC3, 0x73, 0x2A, 0xBE, 0x7C, 0xA2, 0x2F, 0x92 }; + +static const std::uint8_t appKey[] = { 0xB2, 0xA3, 0xD7, 0x16, 0xA6, 0x77, 0x8E, 0xEF, 0xBA, 0x57, 0xB7, 0xBE, 0x4F, 0xD7, 0xC4, 0x7E }; + +#elif defined(RADIO9) +// 98:76:B6:12:87:a1 +static const uint64_t devEUI[] = 0x9876B600001287A1; + +static const uint64_t joinEUI = 0x0000000000000000; + +static const std::uint8_t nwkKey[] = { 0x81, 0x83, 0xF4, 0xDD, 0x1B, 0x1F, 0xCF, 0x36, 0xC3, 0x73, 0x2A, 0xBE, 0x7C, 0xA2, 0x2F, 0x92 }; + +static const std::uint8_t appKey[] = { 0x8F, 0xC5, 0x12, 0xE1, 0x22, 0xC8, 0xD0, 0xFC, 0xB7, 0x9F, 0x04, 0xC1, 0xAE, 0xC9, 0x51, 0x4C }; + #endif diff --git a/secrets.json b/secrets.json new file mode 100644 index 0000000..62210cb --- /dev/null +++ b/secrets.json @@ -0,0 +1,6 @@ +{ + "joinEUI": "0x0000000000000000", + "devEUI": "0x0C8B95AAB05CFEFF", + "nwkKey": ["0x81", "0x83", "0xF4", "0xDD", "0x1B", "0x1F", "0xCF", "0x36", "0xC3", "0x73", "0x2A", "0xBE", "0x7C", "0xA2", "0x2F", "0x92"], + "appKey": ["0x73", "0xF9", "0x96", "0x32", "0x29", "0x2A", "0xF9", "0x23", "0xC0", "0x7D", "0x26", "0x43", "0x1D", "0x0C", "0x9A", "0xBC"] +} diff --git a/src/AppLayer.cpp b/src/AppLayer.cpp index f482679..8d9b916 100644 --- a/src/AppLayer.cpp +++ b/src/AppLayer.cpp @@ -51,7 +51,12 @@ // 20240507 Added configuration of max_sensors/rx_flags via LoRaWAN // 20240508 Added configuration of en_decoders via LoRaWAN // 20240515 Added getOneWireTemperature() -// +// 20240520 Moved 1-Wire sensor code to PayloadOneWire.h/.cpp +// 20240524 Added appPayloadCfgDef, setAppPayloadCfg() & getAppPayloadCfg() +// Moved code to PayloadBresser, PayloadAnalog & PayloadDigital +// 20240529 Changed encoding of INV_TEMP for BLE sensors +// 20240530 Fixed CMD_SET_APP_PAYLOAD_CFG handling +// 20240531 Moved BLE specific code to PayloadBLE.cpp // // ToDo: // - @@ -60,46 +65,60 @@ #include "AppLayer.h" -#ifdef ONEWIRE_EN - // Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs) - static OneWire oneWire(PIN_ONEWIRE_BUS); //!< OneWire bus - // Pass our oneWire reference to Dallas Temperature. - static DallasTemperature owTempSensors(&oneWire); //!< Dallas temperature sensors connected to OneWire bus -#endif +void AppLayer::genPayload(uint8_t port, LoraEncoder &encoder) +{ + // unused + (void)port; + (void)encoder; + weatherSensor.genMessage(0, 0xfff0, SENSOR_TYPE_WEATHER1); + weatherSensor.genMessage(1, 0xfff1, SENSOR_TYPE_SOIL); +} + +void AppLayer::getPayloadStage1(uint8_t port, LoraEncoder &encoder) +{ + (void)port; // eventually suppress warning regarding unused parameter + + log_v("Port: %d", port); + + log_i("--- Uplink Data ---"); + + // TODO: Handle battery status flags in PayloadBresser + // // Sensor status flags + // encoder.writeBitmap(0, + // mithermometer_valid, + // (ls > -1) ? weatherSensor.sensor[ls].valid : false, + // (ls > -1) ? weatherSensor.sensor[ls].battery_ok : false, + // (s1 > -1) ? weatherSensor.sensor[s1].valid : false, + // (s1 > -1) ? weatherSensor.sensor[s1].battery_ok : false, + // (ws > -1) ? weatherSensor.sensor[ws].valid : false, + // (ws > -1) ? weatherSensor.sensor[ws].battery_ok : false); + + encodeBresser(appPayloadCfg, encoder); #ifdef ONEWIRE_EN - /*! - * \brief Get temperature from Maxim OneWire Sensor - * - * \param index sensor index - * - * \returns temperature in degrees Celsius or DEVICE_DISCONNECTED_C - */ - float - AppLayer::getOneWireTemperature(uint8_t index) - { - // Call sensors.requestTemperatures() to issue a global temperature - // request to all devices on the bus - owTempSensors.requestTemperatures(); + encodeOneWire(appPayloadCfg, encoder); +#endif - // Get temperature by index - float tempC = owTempSensors.getTempCByIndex(index); + // Voltages / auxiliary analog sensor data + encodeAnalog(appPayloadCfg, encoder); - // Check if reading was successful - if (tempC != DEVICE_DISCONNECTED_C) - { - log_d("Temperature = %.2f°C", tempC); - } - else - { - log_d("Error: Could not read temperature data"); - } + // Digital Sensors (GPIO, UART, I2C, SPI, ...) + encodeDigital(appPayloadCfg, encoder); - return tempC; - }; +#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) + // BLE Temperature/Humidity Sensors + encodeBLE(appPayloadCfg, encoder); #endif +} + +void AppLayer::getPayloadStage2(uint8_t port, LoraEncoder &encoder) +{ + (void)port; + (void)encoder; +} + uint8_t AppLayer::decodeDownlink(uint8_t port, uint8_t *payload, size_t size) { @@ -187,7 +206,8 @@ AppLayer::decodeDownlink(uint8_t port, uint8_t *payload, size_t size) } #if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) - if ((port == CMD_GET_BLE_CONFIG) && (payload[0] == 0x00) && (size == 1)) { + if ((port == CMD_GET_BLE_CONFIG) && (payload[0] == 0x00) && (size == 1)) + { log_d("Get BLE config"); return CMD_GET_BLE_CONFIG; } @@ -226,431 +246,30 @@ AppLayer::decodeDownlink(uint8_t port, uint8_t *payload, size_t size) return 0; } -#endif - return 0; -} - -void AppLayer::genPayload(uint8_t port, LoraEncoder &encoder) -{ - // unused - (void)port; - (void)encoder; - weatherSensor.genMessage(0, 0xfff0, SENSOR_TYPE_WEATHER1); - weatherSensor.genMessage(1, 0xfff1, SENSOR_TYPE_SOIL); -} - -void AppLayer::getPayloadStage1(uint8_t port, LoraEncoder &encoder) -{ - (void)port; // suppress warning regarding unused parameter - -#ifdef PIN_SUPPLY_IN - uint16_t supply_voltage = getVoltage(PIN_SUPPLY_IN, SUPPLY_SAMPLES, SUPPLY_DIV); -#endif - uint16_t battery_voltage = getBatteryVoltage(); - bool mithermometer_valid = false; -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) - float indoor_temp_c; - float indoor_humidity; - - // Set sensor data invalid - bleSensors.resetData(); - - appPrefs.begin("BWS-LW-APP", false); - uint8_t ble_active = appPrefs.getUChar("ble_active", BLE_SCAN_MODE); - uint8_t ble_scantime = appPrefs.getUChar("ble_scantime", BLE_SCAN_TIME); - log_d("Preferences: ble_active: %u", ble_active); - log_d("Preferences: ble_scantime: %u s", ble_scantime); - appPrefs.end(); - // Get sensor data - run BLE scan for - bleSensors.getData(ble_scantime, ble_active); -#endif -#ifdef LIGHTNINGSENSOR_EN - time_t lightn_ts; - int lightn_events; - uint8_t lightn_distance; -#endif - - weatherSensor.begin(); - weatherSensor.clearSlots(); - appPrefs.begin("BWS-LW-APP", false); - uint8_t ws_timeout = appPrefs.getUChar("ws_timeout", WEATHERSENSOR_TIMEOUT); - log_d("Preferences: weathersensor_timeout: %u s", ws_timeout); - appPrefs.end(); - - log_i("Waiting for Weather Sensor Data; timeout %u s", ws_timeout); - bool decode_ok = weatherSensor.getData(ws_timeout * 1000, DATA_ALL_SLOTS); - (void)decode_ok; - log_i("Receiving Weather Sensor Data %s", decode_ok ? "o.k." : "failed"); - - log_v("Port: %d", port); - -#ifdef RAINDATA_EN - // Check if time is valid - if (*_rtcLastClockSync > 0) - { - // Get local date and time - struct tm timeinfo; - time_t tnow = _rtc->getLocalEpoch(); - localtime_r(&tnow, &timeinfo); - - // Find weather sensor and determine rain gauge overflow limit - // Try to find SENSOR_TYPE_WEATHER0 - int ws = weatherSensor.findType(SENSOR_TYPE_WEATHER0); - if (ws > -1) - { - rainGauge.set_max(1000); - } - else - { - // Try to find SENSOR_TYPE_WEATHER1 - ws = weatherSensor.findType(SENSOR_TYPE_WEATHER1); - rainGauge.set_max(100000); - } - - // If weather sensor has be found and rain data is valid, update statistics - if ((ws > -1) && weatherSensor.sensor[ws].valid && weatherSensor.sensor[ws].w.rain_ok) - { - rainGauge.update(tnow, weatherSensor.sensor[ws].w.rain_mm, weatherSensor.sensor[ws].startup); - } - } -#endif - -#ifdef LIGHTNINGSENSOR_EN - // Check if time is valid - if (*_rtcLastClockSync > 0) - { - // Get local date and time - time_t tnow = _rtc->getLocalEpoch(); - - // Find lightning sensor - int ls = weatherSensor.findType(SENSOR_TYPE_LIGHTNING); - - // If lightning sensor has be found and data is valid, run post-processing - if ((ls > -1) && weatherSensor.sensor[ls].valid) - { - lightningProc.update(tnow, weatherSensor.sensor[ls].lgt.strike_count, weatherSensor.sensor[ls].lgt.distance_km, weatherSensor.sensor[ls].startup); - } - } -#endif - // - // Find Bresser sensor data in array - // - - // Try to find SENSOR_TYPE_WEATHER0 - int ws = weatherSensor.findType(SENSOR_TYPE_WEATHER0); - if (ws < 0) - { - // Try to find SENSOR_TYPE_WEATHER1 - ws = weatherSensor.findType(SENSOR_TYPE_WEATHER1); - } - - int s1 = -1; -#ifdef SOILSENSOR_EN - // Try to find SENSOR_TYPE_SOIL - s1 = weatherSensor.findType(SENSOR_TYPE_SOIL, 1); -#endif - - int ls = -1; -#ifdef LIGHTNINGSENSOR_EN - // Try to find SENSOR_TYPE_LIGHTNING - ls = weatherSensor.findType(SENSOR_TYPE_LIGHTNING); -#endif - - log_i("--- Uplink Data ---"); - - // Debug output for weather sensor data - if (ws > -1) - { - if (weatherSensor.sensor[ws].w.temp_ok) - { - log_i("Air Temperature: %3.1f °C", weatherSensor.sensor[ws].w.temp_c); - } - else - { - log_i("Air Temperature: --.- °C"); - } - if (weatherSensor.sensor[ws].w.humidity_ok) - { - log_i("Humidity: %2d %%", weatherSensor.sensor[ws].w.humidity); - } - else - { - log_i("Humidity: -- %%"); - } - if (weatherSensor.sensor[ws].w.rain_ok) - { - log_i("Rain Gauge: %7.1f mm", weatherSensor.sensor[ws].w.rain_mm); - } - else - { - log_i("Rain Gauge: ---.- mm"); - } - log_i("Wind Speed (avg.): %3.1f m/s", weatherSensor.sensor[ws].w.wind_avg_meter_sec_fp1 / 10.0); - log_i("Wind Speed (max.): %3.1f m/s", weatherSensor.sensor[ws].w.wind_gust_meter_sec_fp1 / 10.0); - log_i("Wind Direction: %4.1f °", weatherSensor.sensor[ws].w.wind_direction_deg_fp1 / 10.0); - } - else - { - log_i("-- Weather Sensor Failure"); - } - -// Debug output for soil sensor data -#ifdef SOILSENSOR_EN - if (s1 > -1) - { - log_i("Soil Temperature 1: %3.1f °C", weatherSensor.sensor[s1].soil.temp_c); - log_i("Soil Moisture 1: %2d %%", weatherSensor.sensor[s1].soil.moisture); - } - else - { - log_i("-- Soil Sensor 1 Failure"); - } -#endif - -// Debug output for lightning sensor data -#ifdef LIGHTNINGSENSOR_EN - if (ls > -1) - { - log_i("Lightning counter: %4d", weatherSensor.sensor[ls].lgt.strike_count); - log_i("Lightning distance: %2d km", weatherSensor.sensor[ls].lgt.distance_km); - } - else - { - log_i("-- Lightning Sensor Failure"); - } - if (lightningProc.lastEvent(lightn_ts, lightn_events, lightn_distance)) - { -#if CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO - struct tm timeinfo; - char tbuf[25]; - - localtime_r(&lightn_ts, &timeinfo); - strftime(tbuf, 25, "%Y-%m-%d %H:%M:%S", &timeinfo); -#endif - log_i("Last lightning event @%s: %d events, %d km", tbuf, lightn_events, lightn_distance); - } - else - { - log_i("-- No Lightning Event Data Available"); - } -#endif - -#ifdef ONEWIRE_EN - float water_temp_c = getOneWireTemperature(); - - // Debug output for auxiliary sensors/voltages - if (water_temp_c != DEVICE_DISCONNECTED_C) - { - log_i("Water Temperature: % 2.1f °C", water_temp_c); - } - else - { - log_i("Water Temperature: --.- °C"); - water_temp_c = -30.0; - } -#endif -#ifdef DISTANCESENSOR_EN - if (distance_mm > 0) - { - log_i("Distance: %4d mm", distance_mm); - } - else - { - log_i("Distance: ---- mm"); - } -#endif -#ifdef PIN_SUPPLY_IN - log_i("Supply Voltage: %4d mV", supply_voltage); -#endif -#if defined(ADC_EN) && defined(PIN_ADC_IN) - log_i("Battery Voltage: %4d mV", battery_voltage); -#endif -#if defined(MITHERMOMETER_EN) - float div = 100.0; -#elif defined(THEENGSDECODER_EN) - float div = 1.0; -#endif -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) - if (bleSensors.data[0].valid) - { - mithermometer_valid = true; - indoor_temp_c = bleSensors.data[0].temperature / div; - indoor_humidity = bleSensors.data[0].humidity / div; - log_i("Indoor Air Temp.: % 3.1f °C", bleSensors.data[0].temperature / div); - log_i("Indoor Humidity: %3.1f %%", bleSensors.data[0].humidity / div); - } - else + if ((port == CMD_GET_APP_PAYLOAD_CFG) && (payload[0] == 0x00) && (size == 1)) { - log_i("Indoor Air Temp.: --.- °C"); - log_i("Indoor Humidity: -- %%"); - indoor_temp_c = -30; - indoor_humidity = 0; + log_d("Get AppLayer payload configuration"); + return CMD_GET_APP_PAYLOAD_CFG; } -#endif - log_v("-"); -#ifdef SENSORID_EN - if (ws > -1) - { - encoder.writeUint32(weatherSensor.sensor[ws].sensor_id); - } - else + if ((port == CMD_SET_APP_PAYLOAD_CFG) && (size == 24)) { - encoder.writeUint32(0); - } -#endif - - // Sensor status flags - encoder.writeBitmap(0, - mithermometer_valid, - (ls > -1) ? weatherSensor.sensor[ls].valid : false, - (ls > -1) ? weatherSensor.sensor[ls].battery_ok : false, - (s1 > -1) ? weatherSensor.sensor[s1].valid : false, - (s1 > -1) ? weatherSensor.sensor[s1].battery_ok : false, - (ws > -1) ? weatherSensor.sensor[ws].valid : false, - (ws > -1) ? weatherSensor.sensor[ws].battery_ok : false); - - // Weather sensor data - if (ws > -1) - { - // weather sensor data available - if (weatherSensor.sensor[ws].w.temp_ok) - { - encoder.writeTemperature(weatherSensor.sensor[ws].w.temp_c); - } - else - { - encoder.writeTemperature(-30); - } - if (weatherSensor.sensor[ws].w.humidity_ok) + log_d("Set AppLayer payload configuration"); + for (size_t i = 0; i < 16; i++) { - encoder.writeUint8(weatherSensor.sensor[ws].w.humidity); + log_d("Type%02d: 0x%X", i, payload[i]); } - else - { - encoder.writeUint8(0); - } -#ifdef ENCODE_AS_FLOAT - encoder.writeRawFloat(weatherSensor.sensor[ws].w.wind_gust_meter_sec); - encoder.writeRawFloat(weatherSensor.sensor[ws].w.wind_avg_meter_sec); - encoder.writeRawFloat(weatherSensor.sensor[ws].w.wind_direction_deg); -#else - encoder.writeUint16(weatherSensor.sensor[ws].w.wind_gust_meter_sec_fp1); - encoder.writeUint16(weatherSensor.sensor[ws].w.wind_avg_meter_sec_fp1); - encoder.writeUint16(weatherSensor.sensor[ws].w.wind_direction_deg_fp1); -#endif - if (weatherSensor.sensor[ws].w.rain_ok) - { - encoder.writeRawFloat(weatherSensor.sensor[ws].w.rain_mm); - } - else - { - encoder.writeRawFloat(0); - } - } - else - { - // fill with suspicious dummy values - encoder.writeTemperature(-30); - encoder.writeUint8(0); -#ifdef ENCODE_AS_FLOAT - encoder.writeRawFloat(0); - encoder.writeRawFloat(0); - encoder.writeRawFloat(0); -#else - encoder.writeUint16(0); - encoder.writeUint16(0); - encoder.writeUint16(0); -#endif - encoder.writeRawFloat(0); - } + log_d("1-Wire: 0x%04X", payload[16] << 8 | payload[17]); + log_d("Analog: 0x%04X", (payload[18] << 8) | payload[19]); + log_d("Digital: 0x%08X", (payload[20] << 24) | (payload[21] << 16) | (payload[22] << 8) | payload[23]); -// Voltages / auxiliary sensor data -#ifdef PIN_SUPPLY_IN - encoder.writeUint16(supply_voltage); -#endif -#if defined(PIN_ADC_IN) - encoder.writeUint16(battery_voltage); -#endif -#ifdef ONEWIRE_EN - encoder.writeTemperature(water_temp_c); -#endif -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) - encoder.writeTemperature(indoor_temp_c); - encoder.writeUint8((uint8_t)(indoor_humidity + 0.5)); - - // BLE Tempoerature/Humidity Sensor: delete results fromBLEScan buffer to release memory - bleSensors.clearScanResults(); -#endif - -// Soil sensor data -#ifdef SOILSENSOR_EN - if (s1 > -1) - { - // soil sensor data available - encoder.writeTemperature(weatherSensor.sensor[s1].soil.temp_c); - encoder.writeUint8(weatherSensor.sensor[s1].soil.moisture); - } - else - { - // fill with suspicious dummy values - encoder.writeTemperature(-30); - encoder.writeUint8(0); - } -#endif - -// Rain data statistics -#ifdef RAINDATA_EN - if ((ws > -1) && weatherSensor.sensor[ws].valid && weatherSensor.sensor[ws].w.rain_ok) - { - log_i("Rain past 60min: %7.1f mm", rainGauge.pastHour()); - log_i("Rain curr. day: %7.1f mm", rainGauge.currentDay()); - log_i("Rain curr. week: %7.1f mm", rainGauge.currentWeek()); - log_i("Rain curr. month: %7.1f mm", rainGauge.currentMonth()); - encoder.writeRawFloat(rainGauge.pastHour()); - encoder.writeRawFloat(rainGauge.currentDay()); - encoder.writeRawFloat(rainGauge.currentWeek()); - encoder.writeRawFloat(rainGauge.currentMonth()); - } - else - { - log_i("Current rain gauge statistics not valid."); - encoder.writeRawFloat(-1); - encoder.writeRawFloat(-1); - encoder.writeRawFloat(-1); - encoder.writeRawFloat(-1); + setAppPayloadCfg(payload, APP_PAYLOAD_CFG_SIZE); + return 0; } -#endif - -// Distance sensor data -#ifdef DISTANCESENSOR_EN - encoder.writeUint16(distance_mm); -#endif -// Lightning sensor data -#ifdef LIGHTNINGSENSOR_EN - if (ls > -1) - { - // Lightning sensor data available - encoder.writeUnixtime(lightn_ts); - encoder.writeUint16(lightn_events); - encoder.writeUint8(lightn_distance); - } - else - { - // Fill with suspicious dummy values - encoder.writeUnixtime(0); - encoder.writeUint16(0); - encoder.writeUint8(0); - } #endif -} - -void AppLayer::getPayloadStage2(uint8_t port, LoraEncoder &encoder) -{ - (void)port; - (void)encoder; + return 0; } void AppLayer::getConfigPayload(uint8_t cmd, uint8_t &port, LoraEncoder &encoder) @@ -718,61 +337,36 @@ void AppLayer::getConfigPayload(uint8_t cmd, uint8_t &port, LoraEncoder &encoder port = CMD_GET_BLE_ADDR; } #endif + else if (cmd == CMD_GET_APP_PAYLOAD_CFG) + { + uint8_t payload[APP_PAYLOAD_CFG_SIZE]; + getAppPayloadCfg(payload, APP_PAYLOAD_CFG_SIZE); + for (size_t i = 0; i < APP_PAYLOAD_CFG_SIZE; i++) + { + encoder.writeUint8(payload[i]); + } + port = CMD_GET_APP_PAYLOAD_CFG; + } } -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) -void AppLayer::setBleAddr(uint8_t *bytes, uint8_t size) +bool AppLayer::getAppPayloadCfg(uint8_t *bytes, uint8_t size) { + bool res = false; appPrefs.begin("BWS-LW-APP", false); - appPrefs.putBytes("ble", bytes, size); + if (appPrefs.isKey("payloadcfg")) + { + appPrefs.getBytes("payloadcfg", bytes, size); + res = true; + } appPrefs.end(); + return res; } -uint8_t AppLayer::getBleAddr(uint8_t *payload) +void AppLayer::setAppPayloadCfg(uint8_t *bytes, uint8_t size) { appPrefs.begin("BWS-LW-APP", false); - uint8_t size = appPrefs.getBytesLength("ble"); - appPrefs.getBytes("ble", payload, size); + appPrefs.putBytes("payloadcfg", bytes, size); appPrefs.end(); - - return size; + memcpy(appPayloadCfg, bytes, size); } -std::vector AppLayer::getBleAddr(void) -{ - std::vector bleAddr; - - appPrefs.begin("BWS-LW-APP", false); - uint8_t size = appPrefs.getBytesLength("ble"); - uint8_t addrBytes[48]; - appPrefs.getBytes("ble", addrBytes, size); - appPrefs.end(); - - if (size < 6) - { - // return empty list - return bleAddr; - } - - uint8_t check = 0; - for (size_t i = 0; i < 6; i++) - { - check |= addrBytes[i]; - } - if (check == 0) - { - // First address is 00:00:00:00:00:00, return empty list - return bleAddr; - } - - for (size_t i = 0; i < size; i += 6) - { - char addr[18]; - snprintf(addr, 18, "%02X:%02X:%02X:%02X:%02X:%02X", - addrBytes[i], addrBytes[i + 1], addrBytes[i + 2], addrBytes[i + 3], addrBytes[i + 4], addrBytes[i + 5]); - bleAddr.push_back(addr); - } - - return bleAddr; -} -#endif diff --git a/src/AppLayer.h b/src/AppLayer.h index e0c6bec..15a4108 100644 --- a/src/AppLayer.h +++ b/src/AppLayer.h @@ -39,6 +39,11 @@ // 20240426 Moved bleAddrInit() out of begin() // 20240504 Added BresserWeatherSensorLWCmd.h // 20240515 Added getOneWireTemperature() +// 20240520 Moved 1-Wire sensor code to PayloadOneWire.h/.cpp +// 20240524 Added appPayloadCfgDef, setAppPayloadCfg() & getAppPayloadCfg() +// Moved code to PayloadBresser, PayloadAnalog & PayloadDigital +// 20240530 Removed BleSensors as base class & from initializers +// 20240531 Moved BLE specific code to PayloadBLE.h // // ToDo: // - @@ -54,37 +59,56 @@ #include #include "../BresserWeatherSensorLWCfg.h" #include "../BresserWeatherSensorLWCmd.h" -#include "adc/adc.h" +#include "PayloadBresser.h" +#include "PayloadOneWire.h" +#include "PayloadAnalog.h" +#include "PayloadDigital.h" +#include "PayloadBLE.h" #include -#if defined(MITHERMOMETER_EN) -// BLE Temperature/Humidity Sensor -#include -#endif -#if defined(THEENGSDECODER_EN) -#include "BleSensors/BleSensors.h" -#endif -#ifdef RAINDATA_EN -#include "RainGauge.h" -#endif -#ifdef LIGHTNINGSENSOR_EN -#include "Lightning.h" -#endif -#ifdef ONEWIRE_EN -// Dallas/Maxim OneWire Temperature Sensor -#include -#endif -#ifdef DISTANCESENSOR_EN -// A02YYUW / DFRobot SEN0311 Ultrasonic Distance Sensor -#include -#endif + +/// Default AppLayer payload configuration +const uint8_t appPayloadCfgDef[APP_PAYLOAD_CFG_SIZE] = { + APP_PAYLOAD_CFG_TYPE00, + APP_PAYLOAD_CFG_TYPE01, + APP_PAYLOAD_CFG_TYPE02, + APP_PAYLOAD_CFG_TYPE03, + APP_PAYLOAD_CFG_TYPE04, + APP_PAYLOAD_CFG_TYPE05, + APP_PAYLOAD_CFG_TYPE06, + APP_PAYLOAD_CFG_TYPE07, + APP_PAYLOAD_CFG_TYPE08, + APP_PAYLOAD_CFG_TYPE09, + APP_PAYLOAD_CFG_TYPE10, + APP_PAYLOAD_CFG_TYPE11, + APP_PAYLOAD_CFG_TYPE12, + APP_PAYLOAD_CFG_TYPE13, + APP_PAYLOAD_CFG_TYPE14, + APP_PAYLOAD_CFG_TYPE15, + APP_PAYLOAD_CFG_ONEWIRE1, // onewire[15:8] + APP_PAYLOAD_CFG_ONEWIRE0, // onewire[7:0] + APP_PAYLOAD_CFG_ANALOG1, // analog[15:0] + APP_PAYLOAD_CFG_ANALOG0, // analog[7:0] + APP_PAYLOAD_CFG_DIGITAL3, // digital[31:24] + APP_PAYLOAD_CFG_DIGITAL2, // digital[23:16] + APP_PAYLOAD_CFG_DIGITAL1, // digital[15:8] + APP_PAYLOAD_CFG_DIGITAL0 // digital[7:0] +}; /*! * \brief LoRaWAN node application layer * * Contains all device specific methods and attributes */ -class AppLayer +class AppLayer : public PayloadBresser, PayloadAnalog, PayloadDigital +#ifdef ONEWIRE_EN + , + PayloadOneWire +#endif +#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) + , + PayloadBLE +#endif { private: ESP32Time *_rtc; @@ -93,62 +117,24 @@ class AppLayer /// Preferences (stored in flash memory) Preferences appPrefs; - /// Bresser Weather Sensor Receiver - WeatherSensor weatherSensor; - -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) - /// Default BLE MAC addresses - std::vector knownBLEAddressesDef; - /// Actual BLE MAC addresses; either from Preferences or from defaults - std::vector knownBLEAddresses; -#endif - -#ifdef MITHERMOMETER_EN - /// BLE Temperature/Humidity Sensors - ATC_MiThermometer bleSensors; //!< Mijia Bluetooth Low Energy Thermo-/Hygrometer -#endif -#ifdef THEENGSDECODER_EN - /// Bluetooth Low Energy sensors - BleSensors bleSensors; -#endif - -#ifdef RAINDATA_EN - /// Rain data statistics - RainGauge rainGauge; -#endif - -#ifdef LIGHTNINGSENSOR_EN - /// Lightning sensor post-processing - Lightning lightningProc; -#endif - -#ifdef DISTANCESENSOR_EN -#if defined(ESP32) - /// Ultrasonic distance sensor - DistanceSensor_A02YYUW distanceSensor(&Serial2); -#else - /// Ultrasonic distance sensor - DistanceSensor_A02YYUW distanceSensor(&Serial1); -#endif -#endif + /// AppLayer payload configuration + uint8_t appPayloadCfg[APP_PAYLOAD_CFG_SIZE]; public: -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) /*! * \brief Constructor with BLE sensors * * \param rtc Real time clock object * \param clocksync Timestamp of last clock synchronization */ - AppLayer(ESP32Time *rtc, time_t *clocksync) : bleSensors() -#else - /*! - * \brief Constructor without BLE sensors - * - * \param rtc Real time clock object - * \param clocksync Timestamp of last clock synchronization - */ - AppLayer(ESP32Time *rtc, time_t *clocksync) + AppLayer(ESP32Time *rtc, time_t *clocksync) : PayloadBresser(rtc, clocksync), PayloadAnalog(), PayloadDigital() +#ifdef ONEWIRE_EN + , + PayloadOneWire() +#endif +#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) + , + PayloadBLE() #endif { _rtc = rtc; @@ -161,56 +147,20 @@ class AppLayer */ void begin(void) { - bleAddrInit(); - }; - - /*! - * \brief Initialize list of known BLE addresses from defaults or Preferences - * - * If available, addresses from Preferences are used, otherwise defaults from - * BresserWeatherSensorLWCfg.h. - * - * BleSensors() requires Preferences, which uses the Flash FS, - * which is not available before the sketches' begin() is called - - * thus the following cannot be handled by the constructor! - */ - void bleAddrInit(void) - { + // bleAddrInit(); + PayloadBresser::begin(); + PayloadAnalog::begin(); + PayloadDigital::begin(); #if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) - knownBLEAddressesDef = KNOWN_BLE_ADDRESSES; - knownBLEAddresses = getBleAddr(); - if (knownBLEAddresses.size() == 0) - { - // No addresses stored in Preferences, use default - knownBLEAddresses = knownBLEAddressesDef; - log_d("Using BLE addresses from BresserWeatherSensorLWCfg.h:"); - } - else - { - log_d("Using BLE addresses from Preferences:"); - } - bleSensors = BleSensors(knownBLEAddresses); + PayloadBLE::begin(); +#endif - for (const std::string &s : knownBLEAddresses) + if (!getAppPayloadCfg(appPayloadCfg, APP_PAYLOAD_CFG_SIZE)) { - (void)s; - log_d("%s", s.c_str()); + memcpy(appPayloadCfg, appPayloadCfgDef, APP_PAYLOAD_CFG_SIZE); } -#endif }; -#ifdef ONEWIRE_EN - /*! - * \brief Get temperature from Maxim OneWire Sensor - * - * \param index sensor index - * - * \returns temperature in degrees Celsius or DEVICE_DISCONNECTED_C - */ - float - getOneWireTemperature(uint8_t index = 0); -#endif - /*! * \brief Decode app layer specific downlink messages * @@ -269,30 +219,22 @@ class AppLayer */ void getConfigPayload(uint8_t cmd, uint8_t &port, LoraEncoder &encoder); -#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) /*! - * Set BLE addresses in Preferences and bleSensors object + * Set AppLayer payload config in Preferences * - * \param bytes MAC addresses (6 bytes per address) - * \param size size in bytes + * \param bytes buffer + * \param size buffer size in bytes */ - void setBleAddr(uint8_t *bytes, uint8_t size); + void setAppPayloadCfg(uint8_t *bytes, uint8_t size); /*! - * Get BLE addresses from Preferences + * Get AppLayer payload config from Preferences * - * \param bytes buffer for addresses + * \param bytes buffer + * \param size buffer size in bytes * - * \returns number of bytes copied into buffer + * \returns true if available in Preferences, else false */ - uint8_t getBleAddr(uint8_t *bytes); - - /*! - * Get BLE addresses from Preferences - * - * \returns BLE addresses - */ - std::vector getBleAddr(void); -#endif + bool getAppPayloadCfg(uint8_t *bytes, uint8_t size); }; #endif // _APPLAYER_H diff --git a/src/PayloadAnalog.cpp b/src/PayloadAnalog.cpp new file mode 100644 index 0000000..4f03c96 --- /dev/null +++ b/src/PayloadAnalog.cpp @@ -0,0 +1,78 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadAnalog.cpp +// +// Read analog input channels and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240521 Created +// 20240524 Added payload size check, changed bitmap order +// 20240528 Changesd order of channels, fixed log messages +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#include "PayloadAnalog.h" + + +void PayloadAnalog::begin(void) +{ + +} + +void PayloadAnalog::encodeAnalog(uint8_t *appPayloadCfg, LoraEncoder &encoder) +{ + unsigned ch = 0; + for (int i = APP_PAYLOAD_BYTES_ANALOG - 1; i >= 0; i--) + { + for (uint8_t bit = 0; bit <= 7; bit++) + { + // Check if channel is enabled + if ((appPayloadCfg[APP_PAYLOAD_OFFS_ANALOG + i] >> bit) & 0x1) { + if ((ch == UBATT_CH) && (encoder.getLength() <= PAYLOAD_SIZE - 2)) + { + uint16_t uBatt = getBatteryVoltage(); + log_i("ch %02u: U_batt: %04u mv", ch, uBatt); + encoder.writeUint16(uBatt); + } + + if ((ch == USUPPLY_CH) && (encoder.getLength() <= PAYLOAD_SIZE - 2)) + { + uint16_t uSupply = getSupplyVoltage(); + log_i("ch %02u: U_supply: %04u mv", ch, uSupply); + encoder.writeUint16(uSupply); + } + } + ch++; + } + } +} + diff --git a/src/PayloadAnalog.h b/src/PayloadAnalog.h new file mode 100644 index 0000000..1fb9106 --- /dev/null +++ b/src/PayloadAnalog.h @@ -0,0 +1,71 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadAnalog.h +// +// Read analog input channels and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240521 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#if !defined(_PAYLOAD_ANALOG) +#define _PAYLOAD_ANALOG + +#include "../BresserWeatherSensorLWCfg.h" +#include "adc/adc.h" +#include +#include "logging.h" + + +class PayloadAnalog +{ +public: + /*! + * \brief Constructor + */ + PayloadAnalog(){}; + + /*! + * \brief Analog channel startup code + */ + void begin(void); + + /*! + * \brief Encode analog data channels for LoRaWAN transmission + * + * \param appPayloadCfg LoRaWAN payload configuration bitmaps + * \param encoder LoRaWAN payload encoder object + */ + void encodeAnalog(uint8_t *appPayloadCfg, LoraEncoder &encoder); +}; +#endif //_PAYLOAD_ANALOG \ No newline at end of file diff --git a/src/PayloadBLE.cpp b/src/PayloadBLE.cpp new file mode 100644 index 0000000..6d3d97e --- /dev/null +++ b/src/PayloadBLE.cpp @@ -0,0 +1,179 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadBLE.cpp +// +// Get BLE temperature/humidity sensor values and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240531 Moved from AppLayer.cpp +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#include "PayloadBLE.h" + +#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) + +void PayloadBLE::setBleAddr(uint8_t *bytes, uint8_t size) +{ + appPrefs.begin("BWS-LW-APP", false); + appPrefs.putBytes("ble", bytes, size); + appPrefs.end(); +} + +uint8_t PayloadBLE::getBleAddr(uint8_t *payload) +{ + appPrefs.begin("BWS-LW-APP", false); + uint8_t size = appPrefs.getBytesLength("ble"); + appPrefs.getBytes("ble", payload, size); + appPrefs.end(); + + return size; +} + +std::vector PayloadBLE::getBleAddr(void) +{ + std::vector bleAddr; + + appPrefs.begin("BWS-LW-APP", false); + uint8_t size = appPrefs.getBytesLength("ble"); + uint8_t addrBytes[48]; + appPrefs.getBytes("ble", addrBytes, size); + appPrefs.end(); + + if (size < 6) + { + // return empty list + return bleAddr; + } + + uint8_t check = 0; + for (size_t i = 0; i < 6; i++) + { + check |= addrBytes[i]; + } + if (check == 0) + { + // First address is 00:00:00:00:00:00, return empty list + return bleAddr; + } + + for (size_t i = 0; i < size; i += 6) + { + char addr[18]; + snprintf(addr, 18, "%02X:%02X:%02X:%02X:%02X:%02X", + addrBytes[i], addrBytes[i + 1], addrBytes[i + 2], addrBytes[i + 3], addrBytes[i + 4], addrBytes[i + 5]); + bleAddr.push_back(addr); + } + + return bleAddr; +} + + +/* + * Initialize list of known BLE addresses from defaults or Preferences + */ +void PayloadBLE::bleAddrInit(void) +{ + knownBLEAddressesDef = KNOWN_BLE_ADDRESSES; + knownBLEAddresses = getBleAddr(); + if (knownBLEAddresses.size() == 0) + { + // No addresses stored in Preferences, use default + knownBLEAddresses = knownBLEAddressesDef; + log_d("Using BLE addresses from BresserWeatherSensorLWCfg.h:"); + } + else + { + log_d("Using BLE addresses from Preferences:"); + } + bleSensors = BleSensors(knownBLEAddresses); + + for (const std::string &s : knownBLEAddresses) + { + (void)s; + log_d("%s", s.c_str()); + } +}; + +/* + * Encode BLE temperature/humidity sensor values for LoRaWAN transmission + */ +void PayloadBLE::encodeBLE(uint8_t *appPayloadCfg, LoraEncoder &encoder) +{ + #if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) + float indoor_temp_c; + float indoor_humidity; + + // Set sensor data invalid + bleSensors.resetData(); + + appPrefs.begin("BWS-LW-APP", false); + uint8_t ble_active = appPrefs.getUChar("ble_active", BLE_SCAN_MODE); + uint8_t ble_scantime = appPrefs.getUChar("ble_scantime", BLE_SCAN_TIME); + log_d("Preferences: ble_active: %u", ble_active); + log_d("Preferences: ble_scantime: %u s", ble_scantime); + appPrefs.end(); + // Get sensor data - run BLE scan for + bleSensors.getData(ble_scantime, ble_active); +#endif + + // BLE Temperature/Humidity Sensors +#if defined(MITHERMOMETER_EN) + float div = 100.0; +#elif defined(THEENGSDECODER_EN) + float div = 1.0; +#endif + + if (encoder.getLength() <= PAYLOAD_SIZE - 3) + { + if (bleSensors.data[0].valid) + { + indoor_temp_c = bleSensors.data[0].temperature / div; + indoor_humidity = bleSensors.data[0].humidity / div; + log_i("Indoor Air Temp.: % 3.1f °C", bleSensors.data[0].temperature / div); + log_i("Indoor Humidity: %3.1f %%", bleSensors.data[0].humidity / div); + encoder.writeTemperature(indoor_temp_c); + encoder.writeUint8(static_cast(indoor_humidity + 0.5)); + } + else + { + log_i("Indoor Air Temp.: --.- °C"); + log_i("Indoor Humidity: -- %%"); + encoder.writeTemperature(INV_TEMP); + encoder.writeUint8(INV_UINT8); + } + // BLE Temperature/Humidity Sensors: delete results fromBLEScan buffer to release memory + bleSensors.clearScanResults(); + } +} + +#endif \ No newline at end of file diff --git a/src/PayloadBLE.h b/src/PayloadBLE.h new file mode 100644 index 0000000..19b01c6 --- /dev/null +++ b/src/PayloadBLE.h @@ -0,0 +1,140 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadBLE.h +// +// Get BLE temperature/humidity sensor values and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240531 Moved from AppLayer.h +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#if !defined(_PAYLOAD_BLE) +#define _PAYLOAD_BLE + +#include "../BresserWeatherSensorLWCfg.h" + +#if defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) + +#include + +#if defined(MITHERMOMETER_EN) +// BLE Temperature/Humidity Sensor +#include +#endif +#if defined(THEENGSDECODER_EN) +#include "BleSensors/BleSensors.h" +#endif + +#include +#include "logging.h" + +class PayloadBLE +{ +private: + /// Preferences (stored in flash memory) + Preferences appPrefs; + + /// Default BLE MAC addresses + std::vector knownBLEAddressesDef; + /// Actual BLE MAC addresses; either from Preferences or from defaults + std::vector knownBLEAddresses; + +#ifdef MITHERMOMETER_EN + /// BLE Temperature/Humidity Sensors + ATC_MiThermometer bleSensors; //!< Mijia Bluetooth Low Energy Thermo-/Hygrometer +#endif +#ifdef THEENGSDECODER_EN + /// Bluetooth Low Energy sensors + BleSensors bleSensors; +#endif + +public: + /*! + * \brief Constructor + */ + PayloadBLE(){}; + + /*! + * \brief BLE startup code + */ + void begin(void) + { + bleAddrInit(); + }; + + /*! + * Set BLE addresses in Preferences and bleSensors object + * + * \param bytes MAC addresses (6 bytes per address) + * \param size size in bytes + */ + void setBleAddr(uint8_t *bytes, uint8_t size); + + /*! + * Get BLE addresses from Preferences + * + * \param bytes buffer for addresses + * + * \returns number of bytes copied into buffer + */ + uint8_t getBleAddr(uint8_t *bytes); + + /*! + * Get BLE addresses from Preferences + * + * \returns BLE addresses + */ + std::vector getBleAddr(void); + + /*! + * \brief Initialize list of known BLE addresses from defaults or Preferences + * + * If available, addresses from Preferences are used, otherwise defaults from + * BresserWeatherSensorLWCfg.h. + * + * BleSensors() requires Preferences, which uses the Flash FS, + * which is not available before the sketches' begin() is called - + * thus the following cannot be handled by the constructor! + */ + void bleAddrInit(void); + + /*! + * \brief Encode BLE temperature/humidity sensor values for LoRaWAN transmission + * + * \param appPayloadCfg LoRaWAN payload configuration bitmaps + * \param encoder LoRaWAN payload encoder object + */ + void encodeBLE(uint8_t *appPayloadCfg, LoraEncoder &encoder); +}; +#endif // defined(MITHERMOMETER_EN) || defined(THEENGSDECODER_EN) +#endif //_PAYLOAD_BLE \ No newline at end of file diff --git a/src/PayloadBresser.cpp b/src/PayloadBresser.cpp new file mode 100644 index 0000000..4e8bfa9 --- /dev/null +++ b/src/PayloadBresser.cpp @@ -0,0 +1,561 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadBresser.cpp +// +// Read Bresser sensor data and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240521 Created +// 20240523 Added encodeWeatherSensor(), +// added defines for signalling invalid data +// 20240524 Moved Weather Sensor and rain gauge/lightning post processing +// from AppLayer into this class +// Added handling of sensor feature flags +// 20240528 Fixes +// 20240529 Changed encoding of INV_TEMP +// 20240530 Weather sensor: Fixed encoding of invalid temperature +// +// ToDo: +// - Add handling of Professional Rain Gauge +// +/////////////////////////////////////////////////////////////////////////////// + +#include "PayloadBresser.h" + +void PayloadBresser::begin(void) +{ + weatherSensor.begin(); + weatherSensor.clearSlots(); + appPrefs.begin("BWS-LW-APP", false); + uint8_t ws_timeout = appPrefs.getUChar("ws_timeout", WEATHERSENSOR_TIMEOUT); + log_d("Preferences: weathersensor_timeout: %u s", ws_timeout); + appPrefs.end(); + + log_i("Waiting for Weather Sensor Data; timeout %u s", ws_timeout); + bool decode_ok = weatherSensor.getData(ws_timeout * 1000, DATA_ALL_SLOTS); + (void)decode_ok; + log_i("Receiving Weather Sensor Data %s", decode_ok ? "o.k." : "failed"); +} + +void PayloadBresser::encodeBresser(uint8_t *appPayloadCfg, LoraEncoder &encoder) +{ + // Handle weather sensors first + // SENSOR_TYPE_WEATHER0 and WEATHER_TYPE_WEATHER1 are treated as one. + // Those sensors only have one channel (0). + + uint8_t flags = appPayloadCfg[0] | appPayloadCfg[1]; + if (flags & 1) + { + // Try to find SENSOR_TYPE_WEATHER1 + int idx = weatherSensor.findType(SENSOR_TYPE_WEATHER1); + if (idx > -1) + { + rainGauge.set_max(100000); + } + else + { + // Try to find SENSOR_TYPE_WEATHER0 + idx = weatherSensor.findType(SENSOR_TYPE_WEATHER0); + rainGauge.set_max(1000); + } + +#ifdef RAINDATA_EN + // Check if time is valid + if (*_rtcLastClockSync > 0) + { + // Get local date and time + struct tm timeinfo; + time_t tnow = _rtc->getLocalEpoch(); + localtime_r(&tnow, &timeinfo); + + // If weather sensor has be found and rain data is valid, update statistics + if ((idx > -1) && weatherSensor.sensor[idx].valid && weatherSensor.sensor[idx].w.rain_ok) + { + rainGauge.update(tnow, weatherSensor.sensor[idx].w.rain_mm, weatherSensor.sensor[idx].startup); + } + } +#endif + encodeWeatherSensor(idx, flags, encoder); + } + + for (int type = 2; type < 16; type++) + { + // Skip if bitmap is zero + if (appPayloadCfg[type] == 0) + continue; + +#ifdef LIGHTNINGSENSOR_EN + // Lightning sensor has fixed channel (0) + if (type == SENSOR_TYPE_LIGHTNING) + { + int idx = weatherSensor.findType(type, 0); +#ifdef LIGHTNINGSENSOR_EN + + // Check if time is valid + if (*_rtcLastClockSync > 0) + { + // Get local date and time + time_t tnow = _rtc->getLocalEpoch(); + + // If lightning sensor has be found and data is valid, run post-processing + if ((idx > -1) && weatherSensor.sensor[idx].valid) + { + lightningProc.update( + tnow, + weatherSensor.sensor[idx].lgt.strike_count, + weatherSensor.sensor[idx].lgt.distance_km, + weatherSensor.sensor[idx].startup); + } + } +#endif + + encodeLightningSensor(idx, appPayloadCfg[type], encoder); + continue; + } +#endif + + // Handle sensors with channel selection + for (uint8_t ch = 1; ch <= 7; ch++) + { + // Check if channel is enabled + if (!((appPayloadCfg[type] >> ch) & 0x1)) + continue; + + if (!isSpaceLeft(encoder, type)) + break; + + log_i("%s Sensor Ch %u", sensorTypes[type], ch); + int idx = weatherSensor.findType(type, ch); + if (idx == -1) { + log_i("-- Failure"); + } + + if (type == SENSOR_TYPE_THERMO_HYGRO) + { + encodeThermoHygroSensor(idx, encoder); + } + else if (type == SENSOR_TYPE_POOL_THERMO) + { + encodePoolThermometer(idx, encoder); + } + else if (type == SENSOR_TYPE_SOIL) + { + encodeSoilSensor(idx, encoder); + } + else if (type == SENSOR_TYPE_LEAKAGE) + { + encodeLeakageSensor(idx, encoder); + } + else if (type == SENSOR_TYPE_AIR_PM) + { + encodeAirPmSensor(idx, encoder); + } + else if (type == SENSOR_TYPE_CO2) + { + encodeCo2Sensor(idx, encoder); + } + else if (type == SENSOR_TYPE_HCHO_VOC) + { + encodeHchoVocSensor(idx, encoder); + } + } + } +} + +// Payload size: 2...17 bytes (ENCODE_AS_FLOAT == false) / 2...23 bytes (ENCODE_AS_FLOAT == true) +void PayloadBresser::encodeWeatherSensor(int idx, uint8_t flags, LoraEncoder &encoder) +{ + // Weather Stations Professional 3-in-1 Professional + // 5-in-1 6-in-1 7-in-1 Rain Gauge Wind Gauge + // + // Temperature X X X X X + // Humidity X X X X + // Wind X X X X + // Rain X X X X + // UV X X + // Light Intensity X + + if (idx == -1) + { + log_i("-- Weather Sensor Failure"); + // Invalidate + encoder.writeTemperature(INV_TEMP); // Temperature + if (flags & PAYLOAD_WS_HUMIDITY) + encoder.writeUint8(INV_UINT8); // Humidity + if (flags & PAYLOAD_WS_RAINGAUGE) + encoder.writeRawFloat(INV_FLOAT); // Rain + if (flags & PAYLOAD_WS_WIND) + { +#ifdef ENCODE_AS_FLOAT + encoder.writeRawFloat(INV_FLOAT); // Wind gust + encoder.writeRawFloat(INV_FLOAT); // Wind avg + encoder.writeRawFloat(INV_FLOAT); // Wind dir +#else + encoder.writeUint16(INV_UINT16); // Wind gust + encoder.writeUint16(INV_UINT16); // Wind avg + encoder.writeUint16(INV_UINT16); // Wind dir +#endif + } + if (flags & PAYLOAD_WS_UV) + encoder.writeUint8(INV_UINT8); // UV + if (flags & PAYLOAD_WS_LIGHT) + encoder.writeRawFloat(INV_UINT32); // Light + } + else + { + if (weatherSensor.sensor[idx].w.temp_ok) + { + log_i("Air Temperature: %3.1f °C", weatherSensor.sensor[idx].w.temp_c); + encoder.writeTemperature(weatherSensor.sensor[idx].w.temp_c); + } + else + { + log_i("Air Temperature: --.- °C"); + encoder.writeTemperature(INV_TEMP); + } + if (flags & PAYLOAD_WS_HUMIDITY) + { + if (weatherSensor.sensor[idx].w.humidity_ok) + { + log_i("Humidity: %2d %%", weatherSensor.sensor[idx].w.humidity); + encoder.writeUint8(weatherSensor.sensor[idx].w.humidity); + } + else + { + log_i("Humidity: -- %%"); + encoder.writeUint8(INV_UINT8); + } + } + if (flags & PAYLOAD_WS_RAINGAUGE) + { + if (weatherSensor.sensor[idx].w.rain_ok) + { + log_i("Rain Gauge: %7.1f mm", weatherSensor.sensor[idx].w.rain_mm); + encoder.writeRawFloat(weatherSensor.sensor[idx].w.rain_mm); + } + else + { + log_i("Rain Gauge: ---.- mm"); + encoder.writeRawFloat(INV_FLOAT); + } + } + if (flags & PAYLOAD_WS_WIND) + { + if (weatherSensor.sensor[idx].w.wind_ok) + { + log_i("Wind Speed (avg.): %3.1f m/s", weatherSensor.sensor[idx].w.wind_avg_meter_sec_fp1 / 10.0); + log_i("Wind Speed (max.): %3.1f m/s", weatherSensor.sensor[idx].w.wind_gust_meter_sec_fp1 / 10.0); + log_i("Wind Direction: %4.1f °", weatherSensor.sensor[idx].w.wind_direction_deg_fp1 / 10.0); + encoder.writeUint16(weatherSensor.sensor[idx].w.wind_avg_meter_sec_fp1); + encoder.writeUint16(weatherSensor.sensor[idx].w.wind_gust_meter_sec_fp1); + encoder.writeUint16(weatherSensor.sensor[idx].w.wind_direction_deg_fp1); + } + else + { + log_i("Wind Speed (avg.): --.- m/s"); + log_i("Wind Speed (max.): --.- m/s"); + log_i("Wind Direction: ---.- °"); + encoder.writeUint16(INV_UINT16); + encoder.writeUint16(INV_UINT16); + encoder.writeUint16(INV_UINT16); + } + } + if (flags & PAYLOAD_WS_UV) + { + if (weatherSensor.sensor[idx].w.uv_ok) + { + log_i("UV Index: %3.1f", weatherSensor.sensor[idx].w.uv); + encoder.writeUint8(static_cast(weatherSensor.sensor[idx].w.uv * 10)); + } + else + { + log_i("UV Index: --.-"); + encoder.writeUint8(INV_UINT8); + } + } + if (flags & PAYLOAD_WS_LIGHT) + { + if (weatherSensor.sensor[idx].w.light_ok) + { + log_i("Light intensity: %06f lx", weatherSensor.sensor[idx].w.light_lux); + encoder.writeUint32(static_cast(weatherSensor.sensor[idx].w.light_lux)); + } + else + { + log_i("Light intensity: ------ lx"); + encoder.writeUint32(INV_UINT32); + } + } + } + + // Rain data statistics +#ifdef RAINDATA_EN + if ((idx) && weatherSensor.sensor[idx].valid && weatherSensor.sensor[idx].w.rain_ok) + { + if (flags & PAYLOAD_WS_RAIN_H) + { + log_i("Rain past 60min: %7.1f mm", rainGauge.pastHour()); + encoder.writeRawFloat(rainGauge.pastHour()); + } + if (flags & PAYLOAD_WS_RAIN_DWM) + { + log_i("Rain curr. day: %7.1f mm", rainGauge.currentDay()); + log_i("Rain curr. week: %7.1f mm", rainGauge.currentWeek()); + log_i("Rain curr. month: %7.1f mm", rainGauge.currentMonth()); + encoder.writeRawFloat(rainGauge.currentDay()); + encoder.writeRawFloat(rainGauge.currentWeek()); + encoder.writeRawFloat(rainGauge.currentMonth()); + } + } + else + { + log_i("Current rain gauge statistics not valid."); + if (flags & PAYLOAD_WS_RAIN_H) + encoder.writeRawFloat(INV_FLOAT); + if (flags & PAYLOAD_WS_RAIN_DWM) + { + encoder.writeRawFloat(INV_FLOAT); + encoder.writeRawFloat(INV_FLOAT); + encoder.writeRawFloat(INV_FLOAT); + } + } +#endif +} + +void PayloadBresser::encodeThermoHygroSensor(int idx, LoraEncoder &encoder) +{ + if (idx == -1) + { + // Invalidate + encoder.writeTemperature(INV_TEMP); + encoder.writeUint8(INV_UINT8); + } + else + { + log_i("Temperature: %3.1f °C", weatherSensor.sensor[idx].w.temp_c); + log_i("Humidity: %2d %%", weatherSensor.sensor[idx].w.humidity); + encoder.writeTemperature(weatherSensor.sensor[idx].w.temp_c); + encoder.writeUint8(weatherSensor.sensor[idx].w.humidity); + } +} + +void PayloadBresser::encodePoolThermometer(int idx, LoraEncoder &encoder) +{ + if (idx == -1) + { + // Invalidate + encoder.writeTemperature(INV_TEMP); + } + else + { + log_i("Temperature: %3.1f °C", weatherSensor.sensor[idx].w.temp_c); + encoder.writeTemperature(weatherSensor.sensor[idx].w.temp_c); + } +} + +void PayloadBresser::encodeSoilSensor(int idx, LoraEncoder &encoder) +{ + if (idx == -1) + { + // Invalidate + encoder.writeTemperature(INV_TEMP); + encoder.writeUint8(INV_UINT8); + } + else + { + log_i("Soil Temperature: %3.1f °C", weatherSensor.sensor[idx].soil.temp_c); + log_i("Soil Moisture: %2d %%", weatherSensor.sensor[idx].soil.moisture); + encoder.writeTemperature(weatherSensor.sensor[idx].soil.temp_c); + encoder.writeUint8(weatherSensor.sensor[idx].soil.moisture); + } +} + +void PayloadBresser::encodeLeakageSensor(int idx, LoraEncoder &encoder) +{ + if (idx == -1) + { + // Invalidate + encoder.writeUint8(INV_UINT8); + } + else + { + log_i("Leakage Alarm: %u", weatherSensor.sensor[idx].leak.alarm); + encoder.writeUint8(weatherSensor.sensor[idx].leak.alarm ? 1 : 0); + } +} + +void PayloadBresser::encodeAirPmSensor(int idx, LoraEncoder &encoder) +{ + if (idx == -1) + { + // Invalidate + encoder.writeUint16(INV_UINT16); + encoder.writeUint16(INV_UINT16); + encoder.writeUint16(INV_UINT16); + } + else + { + if (weatherSensor.sensor[idx].pm.pm_1_0_init) + { + log_i("PM1.0: init"); + encoder.writeUint16(INV_UINT16); + } + else + { + log_i("PM1.0: %u µg/m³", weatherSensor.sensor[idx].pm.pm_1_0); + encoder.writeUint16(weatherSensor.sensor[idx].pm.pm_1_0); + } + if (weatherSensor.sensor[idx].pm.pm_2_5_init) + { + log_i("PM2.5: init"); + encoder.writeUint16(INV_UINT16); + } + else + { + log_i("PM2.5: %u µg/m³", weatherSensor.sensor[idx].pm.pm_2_5); + encoder.writeUint16(weatherSensor.sensor[idx].pm.pm_2_5); + } + if (weatherSensor.sensor[idx].pm.pm_10_init) + { + log_i("PM10: init"); + encoder.writeUint16(INV_UINT16); + } + else + { + log_i("PM10: %u µg/m³", weatherSensor.sensor[idx].pm.pm_10); + encoder.writeUint16(weatherSensor.sensor[idx].pm.pm_10); + } + } +} + +#ifdef LIGHTNINGSENSOR_EN +// Payload size: 3 bytes (raw) / 7 bytes (pre-processed) / 10 bytes (both) +void PayloadBresser::encodeLightningSensor(int idx, uint8_t flags, LoraEncoder &encoder) +{ + if (flags & PAYLOAD_LIGHTNING_RAW) + { + // Raw sensor values + if (idx == -1) + { + // Invalidate + encoder.writeUint8(INV_UINT8); + encoder.writeUint16(INV_UINT16); + } + else + { + log_i("Lightning Distance: %2u km", weatherSensor.sensor[idx].lgt.distance_km); + log_i("Lightning Strike Count %4u", weatherSensor.sensor[idx].lgt.strike_count); + encoder.writeUint8(weatherSensor.sensor[idx].lgt.distance_km); + encoder.writeUint16(weatherSensor.sensor[idx].lgt.strike_count); + } + } + + if (flags & PAYLOAD_LIGHTNING_PROC) + { + // Post-processed sensor values + if (lightningProc.lastEvent(lightn_ts, lightn_events, lightn_distance)) + { +#if CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO + struct tm timeinfo; + char tbuf[25]; + + localtime_r(&lightn_ts, &timeinfo); + strftime(tbuf, 25, "%Y-%m-%d %H:%M:%S", &timeinfo); +#endif + log_i("Last lightning event @%s: %d events, %d km", tbuf, lightn_events, lightn_distance); + encoder.writeUnixtime(lightn_ts); + encoder.writeUint16(lightn_events); + encoder.writeUint8(lightn_distance); + } + else + { + log_i("-- No Lightning Event Data Available"); + encoder.writeUint32(INV_UINT32); + encoder.writeUint16(INV_UINT16); + encoder.writeUint8(INV_UINT8); + } + } +} +#endif + +void PayloadBresser::encodeCo2Sensor(int idx, LoraEncoder &encoder) +{ + if (idx == -1) + { + // Invalidate + encoder.writeUint16(INV_UINT16); + } + else + { + if (weatherSensor.sensor[idx].co2.co2_init) + { + log_i("CO2: init"); + encoder.writeUint16(INV_UINT16); + } + else + { + log_i("CO2: %4u", weatherSensor.sensor[idx].co2.co2_ppm); + encoder.writeUint16(weatherSensor.sensor[idx].co2.co2_ppm); + } + } +} + +void PayloadBresser::encodeHchoVocSensor(int idx, LoraEncoder &encoder) +{ + if (idx == -1) + { + // Invalidate + encoder.writeUint16(INV_UINT16); + encoder.writeUint8(INV_UINT8); + } + else + { + if (weatherSensor.sensor[idx].voc.hcho_init) + { + log_i("HCHO: init"); + encoder.writeUint16(INV_UINT16); + } + else + { + log_i("HCHO: %u", weatherSensor.sensor[idx].voc.hcho_ppb); + encoder.writeUint16(weatherSensor.sensor[idx].voc.hcho_ppb); + } + + if (weatherSensor.sensor[idx].voc.voc_init) + { + log_i("VOC: init"); + encoder.writeUint8(INV_UINT8); + } + else + { + log_i("VOC: %u", weatherSensor.sensor[idx].voc.voc_level); + encoder.writeUint8(weatherSensor.sensor[idx].voc.voc_level); + } + } +} \ No newline at end of file diff --git a/src/PayloadBresser.h b/src/PayloadBresser.h new file mode 100644 index 0000000..c4da84a --- /dev/null +++ b/src/PayloadBresser.h @@ -0,0 +1,176 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadBresser.h +// +// Read Bresser sensor data and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240521 Created +// 20240523 Added encodeWeatherSensor(), +// added defines for signalling invalid data +// 20240524 Moved rainGauge, appPrefs and time members from AppLayer +// into the class +// Added isSpaceLeft(), payloadSize[] & sensorTypes[] +// 20240528 Moved encoding of invalid values to BresserWeatherSensorLWCmd.h +// 20240530 Added missing entries in sensorTypes[] +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#if !defined(_PAYLOAD_BRESSER) +#define _PAYLOAD_BRESSER + +#include "../BresserWeatherSensorLWCfg.h" +#include "WeatherSensorCfg.h" +#include +#include +#include + +#ifdef RAINDATA_EN +#include "RainGauge.h" +#endif +#ifdef LIGHTNINGSENSOR_EN +#include "Lightning.h" +#endif + +#include +#include "logging.h" + +class PayloadBresser +{ +public: + /// Bresser Weather Sensor Receiver + WeatherSensor weatherSensor; + + // Payload size in bytes + const uint8_t payloadSize[16] = { + 0, + 23, // SENSOR_TYPE_WEATHER1 (max.) + 3, // SENSOR_TYPE_THERMO_HYGRO + 2, // SENSOR_TYPE_POOL_THERMO + 3, // SENSOR_TYPE_SOIL + 1, // SENSOR_TYPE_LEAKAGE + 0, // reserved + 0, // reserved + 6, // SENSOR_TYPE_AIR_PM + 3, // SENSOR_TYPE_LIGHTNING (min.) + 2, // SENSOR_TYPE_CO2 + 3, // SENSOR_TYPE_HCHO_VOC + 0, // reserved + 0, // reserved + 0, // reserved + 0 // reserved + }; + +#if CORE_DEBUG_LEVEL >= ARDUHAL_LOG_LEVEL_INFO + const char * sensorTypes[16] = { + "Weather", + "Weather", + "Thermo/Hygro", + "Pool Temperature", + "Soil", + "Leakage", + "reserved", + "reserved", + "Air Quality (PM)", + "Lightning", + "CO2", + "Air Quality (HCHO/VOC)", + "reserved", + "reserved", + "reserved", + "reserved" + }; +#endif + +#ifdef RAINDATA_EN + /// Rain data statistics + RainGauge rainGauge; +#endif + +private: + ESP32Time *_rtc; + time_t *_rtcLastClockSync; + + /// Preferences (stored in flash memory) + Preferences appPrefs; + +#ifdef LIGHTNINGSENSOR_EN + time_t lightn_ts; + int lightn_events; + uint8_t lightn_distance; + + /// Lightning sensor post-processing + Lightning lightningProc; +#endif + +public: + /*! + * \brief Constructor + */ + PayloadBresser(ESP32Time *rtc, time_t *clocksync) + { + _rtc = rtc; + _rtcLastClockSync = clocksync; + }; + + /*! + * \brief Bresser sensors startup code + */ + void begin(void); + + /*! + * \brief Encode Bresser sensor data for LoRaWAN transmission + * + * \param appPayloadCfg LoRaWAN payload configuration bitmaps + * \param encoder LoRaWAN payload encoder object + */ + void encodeBresser(uint8_t *appPayloadCfg, LoraEncoder &encoder); + +private: + void encodeWeatherSensor(int idx, uint8_t flags, LoraEncoder &encoder); + void encodeThermoHygroSensor(int idx, LoraEncoder &encoder); + void encodePoolThermometer(int idx, LoraEncoder &encoder); + void encodeSoilSensor(int idx, LoraEncoder &encoder); + void encodeLeakageSensor(int idx, LoraEncoder &encoder); + void encodeAirPmSensor(int idx, LoraEncoder &encoder); +#ifdef LIGHTNINGSENSOR_EN + void encodeLightningSensor(int idx, uint8_t flags, LoraEncoder &encoder); +#endif + void encodeCo2Sensor(int idx, LoraEncoder &encoder); + void encodeHchoVocSensor(int idx, LoraEncoder &encoder); + + bool isSpaceLeft(LoraEncoder &encoder, uint8_t type) + { + return (encoder.getLength() + payloadSize[type] <= PAYLOAD_SIZE); + }; +}; +#endif //_PAYLOAD_BRESSER \ No newline at end of file diff --git a/src/PayloadDigital.cpp b/src/PayloadDigital.cpp new file mode 100644 index 0000000..826d984 --- /dev/null +++ b/src/PayloadDigital.cpp @@ -0,0 +1,143 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadDigital.cpp +// +// Read digital input channels and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240520 Created +// 20240524 Added payload size check, changed bitmap order +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#include "PayloadDigital.h" + +#ifdef DISTANCESENSOR_EN +#if defined(ESP32) +/// Ultrasonic distance sensor +static DistanceSensor_A02YYUW distanceSensor(&Serial2); +#else +/// Ultrasonic distance sensor +static DistanceSensor_A02YYUW distanceSensor(&Serial1); +#endif +#endif + +void PayloadDigital::begin(void) +{ +#ifdef DISTANCESENSOR_EN + initDistanceSensor(); +#endif +} + +void PayloadDigital::encodeDigital(uint8_t *appPayloadCfg, LoraEncoder &encoder) +{ + unsigned ch = (APP_PAYLOAD_BYTES_DIGITAL * 8) - 1; + for (int i = APP_PAYLOAD_BYTES_DIGITAL - 1; i >= 0; i--) + { + for (uint8_t bit = 0; bit <= 7; bit++) + { + if ((appPayloadCfg[APP_PAYLOAD_OFFS_DIGITAL + i] >> bit) & 0x1) + { +#ifdef DISTANCESENSOR_EN + // Check if channel is enabled + if ((ch == DISTANCESENSOR_CH) && (encoder.getLength() <= PAYLOAD_SIZE - 2)) + { + uint16_t distance_mm = readDistanceSensor(); + if (distance_mm > 0) + { + log_i("ch %02u: Distance: %4d mm", ch, distance_mm); + } + else + { + log_i("ch %02u: Distance: ---- mm", ch); + } + encoder.writeUint16(distance_mm); + } +#endif + } + ch--; + } + } +} + +#ifdef DISTANCESENSOR_EN +void PayloadDigital::initDistanceSensor(void) +{ +#if defined(ESP32) + Serial2.begin(9600, SERIAL_8N1, DISTANCESENSOR_RX, DISTANCESENSOR_TX); + pinMode(DISTANCESENSOR_PWR, OUTPUT); + digitalWrite(DISTANCESENSOR_PWR, LOW); +#elif defined(ARDUINO_ADAFRUIT_FEATHER_RP2040) + Serial1.setRX(DISTANCESENSOR_RX); + Serial1.setTX(DISTANCESENSOR_TX); + Serial1.begin(9600, SERIAL_8N1); + pinMode(DISTANCESENSOR_PWR, OUTPUT_12MA); + digitalWrite(DISTANCESENSOR_PWR, LOW); +#endif +} + +uint16_t PayloadDigital::readDistanceSensor(void) +{ + // Sensor power on + digitalWrite(DISTANCESENSOR_PWR, HIGH); + delay(500); + + int retries = 0; + DistanceSensor_A02YYUW_MEASSUREMENT_STATUS dstStatus; + do + { + dstStatus = distanceSensor.meassure(); + + if (dstStatus != DistanceSensor_A02YYUW_MEASSUREMENT_STATUS_OK) + { + log_e("Distance Sensor Error: %d", dstStatus); + } + } while ( + (dstStatus != DistanceSensor_A02YYUW_MEASSUREMENT_STATUS_OK) && + (++retries < DISTANCESENSOR_RETRIES)); + + uint16_t distance_mm; + if (dstStatus == DistanceSensor_A02YYUW_MEASSUREMENT_STATUS_OK) + { + distance_mm = distanceSensor.getDistance(); + } + else + { + distance_mm = 0; + } + + // Sensor power off + digitalWrite(DISTANCESENSOR_PWR, LOW); + + return distance_mm; +} +#endif diff --git a/src/PayloadDigital.h b/src/PayloadDigital.h new file mode 100644 index 0000000..cca1981 --- /dev/null +++ b/src/PayloadDigital.h @@ -0,0 +1,92 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadDigital.h +// +// Read digital input channels and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240520 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#if !defined(_PAYLOAD_DIGITAL) +#define _PAYLOAD_DIGITAL + +#include "../BresserWeatherSensorLWCfg.h" + +#include +#include "logging.h" + +#ifdef DISTANCESENSOR_EN +// A02YYUW / DFRobot SEN0311 Ultrasonic Distance Sensor +#include +#endif + +class PayloadDigital +{ +public: + /*! + * \brief Constructor + */ + PayloadDigital(){}; + + /*! + * \brief Digital channel startup code + */ + void begin(void); + + /*! + * \brief Encode digital data channels for LoRaWAN transmission + * + * \param appPayloadCfg LoRaWAN payload configuration bitmaps + * \param encoder LoRaWAN payload encoder object + */ + void encodeDigital(uint8_t *appPayloadCfg, LoraEncoder &encoder); + +private: +#ifdef DISTANCESENSOR_EN + /*! + * \brief Initialize ultrasonic distance sensor A02YYUW + * + * Initialize UART and power enable pin + */ + void initDistanceSensor(void); + + /*! + * \brief Read ultrasonic distance sensor (A02YYUW) data + * + * \returns distance in mm (0 if invalid) + */ + uint16_t readDistanceSensor(void) +#endif +}; +#endif //_PAYLOAD_DIGITAL \ No newline at end of file diff --git a/src/PayloadOneWire.cpp b/src/PayloadOneWire.cpp new file mode 100644 index 0000000..6ab2ea4 --- /dev/null +++ b/src/PayloadOneWire.cpp @@ -0,0 +1,113 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadOneWire.cpp +// +// Get 1-Wire temperature sensor values and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240520 Created +// 20240524 Added payload size check, changed bitmap order +// 20240528 Changed index count direction, fixed signalling of invalid data +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#include "PayloadOneWire.h" + +#ifdef ONEWIRE_EN + +// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs) +static OneWire oneWire(PIN_ONEWIRE_BUS); //!< OneWire bus + +// Pass our oneWire reference to Dallas Temperature. +static DallasTemperature owTempSensors(&oneWire); //!< Dallas temperature sensors connected to OneWire bus + +// Get temperature from Maxim OneWire Sensor +float PayloadOneWire::getOneWireTemperature(uint8_t index) +{ + // Call sensors.requestTemperatures() to issue a global temperature + // request to all devices on the bus + owTempSensors.requestTemperatures(); + + // Get temperature by index + float tempC = owTempSensors.getTempCByIndex(index); + + // Check if reading was successful + if (tempC != DEVICE_DISCONNECTED_C) + { + log_d("Temperature = %.2f°C", tempC); + } + else + { + log_d("Error: Could not read temperature data"); + } + + return tempC; +}; + +// Encode 1-Wire temperature sensor values for LoRaWAN transmission +void PayloadOneWire::encodeOneWire(uint8_t *appPayloadCfg, LoraEncoder &encoder) +{ + + unsigned index = 0; + for (int i = APP_PAYLOAD_BYTES_ONEWIRE - 1; i >= 0; i--) + { + for (uint8_t ch = 0; ch <= 7; ch++) + { + // Check if enough space is left in payload buffer + if (encoder.getLength() > PAYLOAD_SIZE - 2) + return; + + // Check if sensor with given index is enabled + if ((appPayloadCfg[APP_PAYLOAD_OFFS_ONEWIRE + i] >> ch) & 0x1) + { + // Get temperature by index + float tempC = owTempSensors.getTempCByIndex(index); + + // Check if reading was successful + if (tempC != DEVICE_DISCONNECTED_C) + { + log_d("Temperature[%d] = %.2f°C", index, tempC); + encoder.writeTemperature(tempC); + } + else + { + log_d("Error: Could not read temperature[%d] data", index); + encoder.writeTemperature(INV_TEMP); + } + + + } + index++; + } + } +} +#endif \ No newline at end of file diff --git a/src/PayloadOneWire.h b/src/PayloadOneWire.h new file mode 100644 index 0000000..93e9a30 --- /dev/null +++ b/src/PayloadOneWire.h @@ -0,0 +1,80 @@ +/////////////////////////////////////////////////////////////////////////////// +// PayloadOneWire.h +// +// Get 1-Wire temperature sensor values and encode as LoRaWAN payload +// +// created: 05/2024 +// +// +// MIT License +// +// Copyright (c) 2024 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: +// +// 20240520 Created +// +// ToDo: +// - +// +/////////////////////////////////////////////////////////////////////////////// + +#if !defined(_PAYLOAD_ONE_WIRE) +#define _PAYLOAD_ONE_WIRE + +#include "../BresserWeatherSensorLWCfg.h" + +#ifdef ONEWIRE_EN + +// Dallas/Maxim OneWire Temperature Sensor +#include + +#include +#include "logging.h" + +class PayloadOneWire +{ +public: + /*! + * \brief Constructor + */ + PayloadOneWire(){}; + + /*! + * \brief Get temperature from Maxim OneWire Sensor + * + * \param index sensor index + * + * \returns temperature in degrees Celsius or DEVICE_DISCONNECTED_C + */ + float getOneWireTemperature(uint8_t index); + + /*! + * \brief Encode 1-Wire temperature sensor values for LoRaWAN transmission + * + * \param appPayloadCfg LoRaWAN payload configuration bitmaps + * \param encoder LoRaWAN payload encoder object + */ + void encodeOneWire(uint8_t *appPayloadCfg, LoraEncoder &encoder); +}; +#endif // ONEWIRE_EN +#endif //_PAYLOAD_ONE_WIRE \ No newline at end of file