diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 92a57f83..df568ddf 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -50,6 +50,7 @@ #define REQ_TYPE_GET_STATUS 0x01 #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +#define REQ_TYPE_GET_AVG_MIN_MAX 0x04 #define RESP_SERVER_LOGIN_OK 0 // response to ANON_REQ @@ -154,6 +155,22 @@ uint8_t SensorMesh::handleRequest(bool is_admin, uint32_t sender_timestamp, uint memcpy(&reply_data[4], telemetry.getBuffer(), tlen); return 4 + tlen; // reply_len } + case REQ_TYPE_GET_AVG_MIN_MAX: { + uint32_t start_secs_ago, end_secs_ago; + memcpy(&start_secs_ago, &payload[0], 4); + memcpy(&end_secs_ago, &payload[4], 4); + uint8_t res1 = payload[8]; // reserved for future (extra query params) + uint8_t res2 = payload[8]; + + MinMaxAvg data[8]; + int n; + if (res1 == 0 && res2 == 0) { + n = querySeriesData(start_secs_ago, end_secs_ago, data, 8); + } else { + n = 0; + } + return 0; // TODO: encode data[0..n) + } } return 0; // unknown command } @@ -192,6 +209,34 @@ ContactInfo* SensorMesh::putContact(const mesh::Identity& id) { return c; } +void SensorMesh::sendAlert(const char* text) { + int text_len = strlen(text); + + // send text message to all admins + for (int i = 0; i < num_contacts; i++) { + auto c = &contacts[i]; + if (!c->isAdmin()) continue; + + uint8_t data[MAX_PACKET_PAYLOAD]; + uint32_t now = getRTCClock()->getCurrentTimeUnique(); // need different timestamp per packet + memcpy(data, &now, 4); + data[4] = (TXT_TYPE_PLAIN << 2); // attempt and flags + memcpy(&data[5], text, text_len); + // calc expected ACK reply + // uint32_t expected_ack; + // mesh::Utils::sha256((uint8_t *)&expected_ack, 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); + + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len); + if (pkt) { + if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT + sendDirect(pkt, c->out_path, c->out_path_len); + } else { + sendFlood(pkt); + } + } + } +} + void SensorMesh::alertIfLow(Trigger& t, float value, float threshold, const char* text) { if (value < threshold) { if (!t.triggered) { @@ -222,6 +267,50 @@ void SensorMesh::alertIfHigh(Trigger& t, float value, float threshold, const cha } } +void SensorMesh::recordData(TimeSeriesData& data, float value) { + uint32_t now = getRTCClock()->getCurrentTime(); + if (now >= data.last_timestamp + data.interval_secs) { + data.last_timestamp = now; + + data.data[data.next] = value; // append to cycle table + data.next = (data.next + 1) % data.num_slots; + } +} + +void SensorMesh::calcDataMinMaxAvg(const TimeSeriesData& data, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type) { + int i = data.next, n = data.num_slots; + uint32_t ago = data.interval_secs * data.num_slots; + int num_values = 0; + float total = 0.0f; + + dest->_channel = channel; + dest->_lpp_type = lpp_type; + + // start at earliest recording, through to most recent + while (n > 0) { + n--; + i = (i + 1) % data.num_slots; + if (ago >= end_secs_ago && ago < start_secs_ago) { + float v = data.data[i]; + num_values++; + total += v; + if (num_values == 1) { + dest->_max = dest->_min = v; + } else { + if (v < dest->_min) dest->_min = v; + if (v > dest->_max) dest->_max = v; + } + } + ago -= data.interval_secs; + } + // calc average + if (num_values > 0) { + dest->_avg = total / num_values; + } else { + dest->_avg = NAN; + } +} + float SensorMesh::getAirtimeBudgetFactor() const { return _prefs.airtime_factor; } @@ -577,7 +666,7 @@ void SensorMesh::loop() { // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); // allow all telemetry permissions - checkForAlerts(); + onSensorDataRead(); // save telemetry to time-series datastore File file = openAppend(_fs, "/s_data"); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 6293b21e..0f94b128 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -93,7 +93,33 @@ protected: void alertIfLow(Trigger& t, float value, float threshold, const char* text); void alertIfHigh(Trigger& t, float value, float threshold, const char* text); - virtual void checkForAlerts() = 0; // for app to implement + class TimeSeriesData { + public: + float* data; + int num_slots, next; + uint32_t last_timestamp; + uint32_t interval_secs; + + TimeSeriesData(float* array, int num, uint32_t secs) : num_slots(num), data(array), last_timestamp(0), next(0), interval_secs(secs) { + memset(data, 0, sizeof(float)*num); + } + TimeSeriesData(int num, uint32_t secs) : num_slots(num), last_timestamp(0), next(0), interval_secs(secs) { + data = new float[num]; + memset(data, 0, sizeof(float)*num); + } + }; + + void recordData(TimeSeriesData& data, float value); + + struct MinMaxAvg { + float _min, _max, _avg; + uint8_t _lpp_type, _channel; + }; + + void calcDataMinMaxAvg(const TimeSeriesData& data, uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg* dest, uint8_t channel, uint8_t lpp_type); + + virtual void onSensorDataRead() = 0; // for app to implement + virtual int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) = 0; // for app to implement // Mesh overrides float getAirtimeBudgetFactor() const override; @@ -130,6 +156,6 @@ private: mesh::Packet* createSelfAdvert(); ContactInfo* putContact(const mesh::Identity& id); - void sendAlert(const char* text) { } // TODO + void sendAlert(const char* text); }; diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 63122e9a..500efef4 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -8,18 +8,28 @@ 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 + { + } protected: - /* ========================== custom alert logic here ========================== */ + /* ========================== custom logic here ========================== */ Trigger low_batt; + TimeSeriesData battery_data; - void checkForAlerts() override { - alertIfLow(low_batt, getVoltage(TELEM_CHANNEL_SELF), 3.4f, "Battery low!"); - // alertIf ... - // alertIf ... + void onSensorDataRead() override { + float batt_voltage = getVoltage(TELEM_CHANNEL_SELF); + + recordData(battery_data, batt_voltage); // record battery + alertIfLow(low_batt, batt_voltage, 3.4f, "Battery low!"); } - /* ============================================================================= */ + + int querySeriesData(uint32_t start_secs_ago, uint32_t end_secs_ago, MinMaxAvg dest[], int max_num) override { + calcDataMinMaxAvg(battery_data, start_secs_ago, end_secs_ago, &dest[0], TELEM_CHANNEL_SELF, LPP_VOLTAGE); + return 1; + } + /* ======================================================================= */ }; StdRNG fast_rng;