diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 23d0cdc353..826e6dda84 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -1,3 +1,4 @@ +#include #include "SensorMesh.h" /* ------------------------------ Config -------------------------------- */ @@ -102,6 +103,9 @@ static uint8_t getDataSize(uint8_t type) { case LPP_CURRENT: case LPP_DIRECTION: case LPP_POWER: + case LPP_WIND_SPEED: + case LPP_WIND_GUST: + case LPP_RAIN: return 2; } return 1; @@ -184,6 +188,16 @@ uint8_t SensorMesh::handleRequest(uint8_t perms, uint32_t sender_timestamp, uint uint8_t tlen = telemetry.getSize(); memcpy(&reply_data[4], telemetry.getBuffer(), tlen); + +#if ENV_INCLUDE_WIND + // Use LPPWriter since CayenneLPP has no add*() for the placeholder wind/rain types + if ((0xFF & perm_mask) & TELEM_PERM_ENVIRONMENT) { + LPPWriter wind_writer(&reply_data[4 + tlen], sizeof(reply_data) - 4 - tlen); + wind_sensor.query(sensors.getNextAvailableChannel(), wind_writer); + tlen += wind_writer.length(); + } +#endif + return 4 + tlen; // reply_len } if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) { diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 69182f3a7a..984eac5a5d 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -8,8 +8,13 @@ class MyMesh : public SensorMesh { public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables) - : SensorMesh(board, radio, ms, rng, rtc, tables), + : SensorMesh(board, radio, ms, rng, rtc, tables), battery_data(12*24, 5*60) // 24 hours worth of battery data, every 5 minutes +#if ENV_INCLUDE_WIND +// For Review: Is the extra 288 bytes per data type relevant? + , wind_speed_data(12*24, 5*60) + , wind_gust_data(12*24, 5*60) +#endif { } @@ -17,6 +22,10 @@ class MyMesh : public SensorMesh { /* ========================== custom logic here ========================== */ Trigger low_batt, critical_batt; TimeSeriesData battery_data; +#if ENV_INCLUDE_WIND + TimeSeriesData wind_speed_data; + TimeSeriesData wind_gust_data; +#endif void onSensorDataRead() override { float batt_voltage = getVoltage(TELEM_CHANNEL_SELF); @@ -24,11 +33,24 @@ class MyMesh : public SensorMesh { battery_data.recordData(getRTCClock(), batt_voltage); // record battery alertIf(batt_voltage < 3.4f, critical_batt, HIGH_PRI_ALERT, "Battery is critical!"); alertIf(batt_voltage < 3.6f, low_batt, LOW_PRI_ALERT, "Battery is low"); + +#if ENV_INCLUDE_WIND + // For review: Previouslyy there was only onle timestamp recorded for battery. + // Now all 3 recordings have different timestamps. Is that a problem? + wind_speed_data.recordData(getRTCClock(), wind_sensor.readSpeed()); + wind_gust_data.recordData(getRTCClock(), wind_sensor.readGust()); +#endif } int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override { - battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); - return 1; + int n = 0; + battery_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[n++], TELEM_CHANNEL_SELF, LPP_VOLTAGE); +#if ENV_INCLUDE_WIND + uint8_t wind_ch = sensors.getNextAvailableChannel(); + wind_speed_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[n++], wind_ch, LPP_WIND_SPEED); + wind_gust_data.calcMinMaxAvg(getRTCClock(), start_secs_ago, end_secs_ago, &dest[n++], wind_ch, LPP_WIND_GUST); +#endif + return n; } bool handleCustomCommand(uint32_t sender_timestamp, char* command, char* reply) override { diff --git a/src/helpers/sensors/EnvironmentSensorManager.h b/src/helpers/sensors/EnvironmentSensorManager.h index 29147c8967..c2b00aed3e 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.h +++ b/src/helpers/sensors/EnvironmentSensorManager.h @@ -43,6 +43,8 @@ class EnvironmentSensorManager : public SensorManager { #endif bool begin() override; bool querySensors(uint8_t requester_permissions, CayenneLPP& telemetry) override; + // Channel one past the last one querySensors() assigned to avoid collision with dynamically allocated channels. + uint8_t getNextAvailableChannel() const { return next_available_channel; } #if ENV_INCLUDE_GPS || defined(ENV_INCLUDE_BME680_BSEC) void loop() override; #endif diff --git a/src/helpers/sensors/LPPDataHelpers.h b/src/helpers/sensors/LPPDataHelpers.h index 70a036c493..5efc97d87b 100644 --- a/src/helpers/sensors/LPPDataHelpers.h +++ b/src/helpers/sensors/LPPDataHelpers.h @@ -30,6 +30,11 @@ #define LPP_SWITCH 142 // 1 byte, 0/1 #define LPP_POLYLINE 240 // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas +// Placeholder types for wind/rain telemetry, pending meshcore-dev/MeshCore#3368. +#define LPP_WIND_SPEED 250 // 2 bytes, 1 m/s, unsigned (placeholder) +#define LPP_WIND_GUST 251 // 2 bytes, 1 m/s, unsigned (placeholder) +#define LPP_RAIN 252 // 2 bytes, 1 mm, unsigned (placeholder) + // Multipliers #define LPP_DIGITAL_INPUT_MULT 1 #define LPP_DIGITAL_OUTPUT_MULT 1 @@ -58,6 +63,10 @@ #define LPP_SWITCH_MULT 1 #define LPP_CONCENTRATION_MULT 1 #define LPP_COLOUR_MULT 1 +// For review: Are the units defined in CayenneLPP? +#define LPP_WIND_SPEED_MULT 1 +#define LPP_WIND_GUST_MULT 1 +#define LPP_RAIN_MULT 1 #define LPP_ERROR_OK 0 #define LPP_ERROR_OVERFLOW 1 @@ -165,6 +174,9 @@ class LPPReader { case LPP_CURRENT: case LPP_DIRECTION: case LPP_POWER: + case LPP_WIND_SPEED: + case LPP_WIND_GUST: + case LPP_RAIN: _pos += 2; break; default: _pos++; @@ -219,5 +231,45 @@ class LPPWriter { return false; } + bool writeDirection(uint8_t channel, uint16_t degrees) { + if (_len + 4 <= _max_len) { + _buf[_len++] = channel; + _buf[_len++] = LPP_DIRECTION; + write(degrees); + return true; + } + return false; + } + + bool writeWindSpeed(uint8_t channel, uint16_t speed) { + if (_len + 4 <= _max_len) { + _buf[_len++] = channel; + _buf[_len++] = LPP_WIND_SPEED; + write(speed); + return true; + } + return false; + } + + bool writeWindGust(uint8_t channel, uint16_t gust) { + if (_len + 4 <= _max_len) { + _buf[_len++] = channel; + _buf[_len++] = LPP_WIND_GUST; + write(gust); + return true; + } + return false; + } + + bool writeRain(uint8_t channel, uint16_t tip_count) { + if (_len + 4 <= _max_len) { + _buf[_len++] = channel; + _buf[_len++] = LPP_RAIN; + write(tip_count); + return true; + } + return false; + } + uint8_t length() { return _len; } }; diff --git a/src/helpers/sensors/WindSensor.h b/src/helpers/sensors/WindSensor.h new file mode 100644 index 0000000000..d66ac8c7d5 --- /dev/null +++ b/src/helpers/sensors/WindSensor.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "LPPDataHelpers.h" + +// Stub sensor implememtation. +class WindSensor { + uint16_t _rain_tip_count = 0; + uint32_t _last_tip_ms = 0; + +public: + void query(uint8_t channel, LPPWriter& writer) { + writer.writeDirection(channel, readDirection()); + writer.writeWindSpeed(channel, readSpeed()); + writer.writeWindGust(channel, readGust()); + writer.writeRain(channel, readRainTipCount()); + } + + uint16_t readSpeed() const { + // synthetic 0.5-1.5 m/s-ish triangle, just to give TimeSeriesData variation + uint32_t t = millis() / 1000; + return 50 + (t % 100); + } + + uint16_t readGust() const { return readSpeed() + 30; } + + // TODO: replace with real wind-vane ADC reading once hardware is bench-tested. + uint16_t readDirection() const { return 180; } + + // TODO: replace with real reed-switch interrupt count once hardware is bench-tested. + // Ticks once every 10s so the raw counter visibly advances without any real gauge. + uint16_t readRainTipCount() { + uint32_t now = millis(); + if (now - _last_tip_ms >= 10000) { + _rain_tip_count++; + _last_tip_ms = now; + } + return _rain_tip_count; + } +}; diff --git a/test/test_lpp_data_helpers/test_lpp_data_helpers.cpp b/test/test_lpp_data_helpers/test_lpp_data_helpers.cpp new file mode 100644 index 0000000000..39db0a3f86 --- /dev/null +++ b/test/test_lpp_data_helpers/test_lpp_data_helpers.cpp @@ -0,0 +1,98 @@ +#include +#include "helpers/sensors/LPPDataHelpers.h" + +TEST(LPPWriterWindTest, WriteWindSpeedEncodesChannelTypeAndBigEndianValue) { + uint8_t buf[4] = {}; + LPPWriter writer(buf, sizeof(buf)); + + EXPECT_TRUE(writer.writeWindSpeed(2, 0x1234)); + EXPECT_EQ(writer.length(), 4); + EXPECT_EQ(buf[0], 2); // channel + EXPECT_EQ(buf[1], LPP_WIND_SPEED); // type + EXPECT_EQ(buf[2], 0x12); // MSB + EXPECT_EQ(buf[3], 0x34); // LSB +} + +TEST(LPPWriterWindTest, WriteWindGustEncodesChannelTypeAndBigEndianValue) { + uint8_t buf[4] = {}; + LPPWriter writer(buf, sizeof(buf)); + + EXPECT_TRUE(writer.writeWindGust(3, 0xABCD)); + EXPECT_EQ(writer.length(), 4); + EXPECT_EQ(buf[0], 3); + EXPECT_EQ(buf[1], LPP_WIND_GUST); + EXPECT_EQ(buf[2], 0xAB); + EXPECT_EQ(buf[3], 0xCD); +} + +TEST(LPPWriterWindTest, WriteRainEncodesChannelTypeAndBigEndianValue) { + uint8_t buf[4] = {}; + LPPWriter writer(buf, sizeof(buf)); + + EXPECT_TRUE(writer.writeRain(1, 0)); + EXPECT_EQ(writer.length(), 4); + EXPECT_EQ(buf[0], 1); + EXPECT_EQ(buf[1], LPP_RAIN); + EXPECT_EQ(buf[2], 0x00); + EXPECT_EQ(buf[3], 0x00); +} + +TEST(LPPWriterWindTest, WriteDirectionEncodesChannelTypeAndBigEndianValue) { + uint8_t buf[4] = {}; + LPPWriter writer(buf, sizeof(buf)); + + EXPECT_TRUE(writer.writeDirection(1, 359)); + EXPECT_EQ(writer.length(), 4); + EXPECT_EQ(buf[0], 1); + EXPECT_EQ(buf[1], LPP_DIRECTION); + EXPECT_EQ(buf[2], 359 >> 8); + EXPECT_EQ(buf[3], 359 & 0xFF); +} + +TEST(LPPWriterWindTest, MultipleWritesAppendSequentiallyWithoutOverwriting) { + uint8_t buf[16] = {}; + LPPWriter writer(buf, sizeof(buf)); + + writer.writeDirection(4, 180); + writer.writeWindSpeed(4, 50); + writer.writeWindGust(4, 80); + writer.writeRain(4, 3); + + ASSERT_EQ(writer.length(), 16); + + LPPReader reader(buf, sizeof(buf)); + uint8_t channel, type; + + ASSERT_TRUE(reader.readHeader(channel, type)); + EXPECT_EQ(channel, 4); + EXPECT_EQ(type, LPP_DIRECTION); + reader.skipData(type); + + ASSERT_TRUE(reader.readHeader(channel, type)); + EXPECT_EQ(channel, 4); + EXPECT_EQ(type, LPP_WIND_SPEED); + reader.skipData(type); + + ASSERT_TRUE(reader.readHeader(channel, type)); + EXPECT_EQ(channel, 4); + EXPECT_EQ(type, LPP_WIND_GUST); + reader.skipData(type); + + ASSERT_TRUE(reader.readHeader(channel, type)); + EXPECT_EQ(channel, 4); + EXPECT_EQ(type, LPP_RAIN); + reader.skipData(type); +} + +TEST(LPPWriterWindTest, RefusesToWritePastCapacity) { + uint8_t buf[3] = {}; // too small for a 4-byte field + LPPWriter writer(buf, sizeof(buf)); + + EXPECT_FALSE(writer.writeWindSpeed(1, 100)); + EXPECT_EQ(writer.length(), 0); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 0dfbf79775..d265058f78 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -257,6 +257,7 @@ build_flags = -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' + -D ENV_INCLUDE_WIND=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} diff --git a/variants/rak4631/target.cpp b/variants/rak4631/target.cpp index a41ba72075..f11b44dd95 100644 --- a/variants/rak4631/target.cpp +++ b/variants/rak4631/target.cpp @@ -32,6 +32,10 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); EnvironmentSensorManager sensors; #endif +#if ENV_INCLUDE_WIND + WindSensor wind_sensor; +#endif + bool radio_init() { rtc_clock.begin(Wire); return radio.std_init(&SPI); diff --git a/variants/rak4631/target.h b/variants/rak4631/target.h index 1529f90424..51e2158f05 100644 --- a/variants/rak4631/target.h +++ b/variants/rak4631/target.h @@ -7,6 +7,9 @@ #include #include #include +#if ENV_INCLUDE_WIND + #include +#endif #ifdef DISPLAY_CLASS #include @@ -22,6 +25,9 @@ extern RAK4631Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; +#if ENV_INCLUDE_WIND +extern WindSensor wind_sensor; +#endif bool radio_init(); mesh::LocalIdentity radio_new_identity();