Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/simple_sensor/SensorMesh.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <helpers/sensors/LPPDataHelpers.h>
#include "SensorMesh.h"

/* ------------------------------ Config -------------------------------- */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 25 additions & 3 deletions examples/simple_sensor/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,49 @@
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
{
}

protected:
/* ========================== 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);

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 {
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/sensors/EnvironmentSensorManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions src/helpers/sensors/LPPDataHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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++;
Expand Down Expand Up @@ -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; }
};
40 changes: 40 additions & 0 deletions src/helpers/sensors/WindSensor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include <Arduino.h>
#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;
}
};
98 changes: 98 additions & 0 deletions test/test_lpp_data_helpers/test_lpp_data_helpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#include <gtest/gtest.h>
#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();
}
1 change: 1 addition & 0 deletions variants/rak4631/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
4 changes: 4 additions & 0 deletions variants/rak4631/target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions variants/rak4631/target.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#include <helpers/sensors/EnvironmentSensorManager.h>
#if ENV_INCLUDE_WIND
#include <helpers/sensors/WindSensor.h>
#endif

#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
Expand All @@ -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();
Expand Down