mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 22:42:25 +00:00
Bound telemetry replies and preserve Sensor send retries
This commit is contained in:
1 parent
5c1f1477f4
commit
58f5d7f794
11 files changed
+1522
-66
No files matched your search
@@ -288,6 +288,8 @@ jobs:
|
||||
python3 -B test/test_gps_sync_preferences.py -v
|
||||
python3 -B test/test_t1000e_sensor_power.py -v
|
||||
python3 -B test/test_sensor_battery_alerts.py -v
|
||||
python3 -B test/test_sensor_subscriptions.py -v
|
||||
python3 -B test/test_sensor_reply_bounds.py -v
|
||||
|
||||
- name: Verify Companion ASCII defaults and USB session cleanup
|
||||
run: |
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/ClientACLCLI.h>
|
||||
#include <helpers/ClientACLResponse.h>
|
||||
#include <helpers/sensors/LPPDataHelpers.h>
|
||||
#include <helpers/ClientLoginPersistence.h>
|
||||
#include <helpers/ClientPathPersistence.h>
|
||||
#include <helpers/LazyPersistence.h>
|
||||
@@ -268,8 +269,9 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t
|
||||
telemetry.addTemperature(TELEM_CHANNEL_SELF, temperature); // Built-in MCU Temperature
|
||||
}
|
||||
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
const uint8_t* tbuf = telemetry.getBuffer();
|
||||
const size_t tlen = LPPData::boundedPrefix(tbuf, telemetry.getSize(), reply_capacity - 4);
|
||||
memcpy(&reply_data[4], tbuf, tlen);
|
||||
return 4 + tlen; // reply_len
|
||||
}
|
||||
if (payload[0] == REQ_TYPE_GET_ACCESS_LIST && sender->isAdmin()) {
|
||||
|
||||
@@ -136,7 +136,8 @@ bool SensorMesh::telemHasChanged(ClientInfo* c) {
|
||||
uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel #
|
||||
uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type
|
||||
uint8_t sz = LPPData::getDataSize(t);
|
||||
if (sz > c->extra.sensor.min_deltas_len - i - 2) return false;
|
||||
if (!LPPData::isScalarType(t)
|
||||
|| sz > c->extra.sensor.min_deltas_len - i - 2) return false;
|
||||
|
||||
float min_delta = LPPData::getFloat(&c->extra.sensor.min_deltas[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||
float pv = LPPData::getFloat(&c->extra.sensor.prev_telem[i + 2], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||
@@ -146,27 +147,30 @@ bool SensorMesh::telemHasChanged(ClientInfo* c) {
|
||||
|
||||
i += 2 + sz; // skip
|
||||
}
|
||||
if (changed) {
|
||||
// take snapshot of all _monitored_ telem values, for next cycle
|
||||
i = 0;
|
||||
while (i + 2 < c->extra.sensor.min_deltas_len) {
|
||||
uint8_t ch = c->extra.sensor.min_deltas[i]; // Get channel #
|
||||
uint8_t t = c->extra.sensor.min_deltas[i + 1]; // Get data type
|
||||
uint8_t sz = LPPData::getDataSize(t);
|
||||
if (sz > c->extra.sensor.min_deltas_len - i - 2) return false;
|
||||
|
||||
c->extra.sensor.prev_telem[i] = ch;
|
||||
c->extra.sensor.prev_telem[i + 1] = t;
|
||||
|
||||
float v = findTelemValue(buf, size, ch, t, 0.0f);
|
||||
LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||
|
||||
i += 2 + sz; // skip
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void SensorMesh::snapshotTelemetry(ClientInfo* c) {
|
||||
// Only advance thresholds after the corresponding push is queued.
|
||||
auto buf = telemetry.getBuffer();
|
||||
uint8_t size = telemetry.getSize();
|
||||
uint8_t i = 0;
|
||||
while (i + 2 < c->extra.sensor.min_deltas_len) {
|
||||
uint8_t ch = c->extra.sensor.min_deltas[i];
|
||||
uint8_t t = c->extra.sensor.min_deltas[i + 1];
|
||||
uint8_t sz = LPPData::getDataSize(t);
|
||||
if (!LPPData::isScalarType(t)
|
||||
|| sz > c->extra.sensor.min_deltas_len - i - 2) return;
|
||||
|
||||
c->extra.sensor.prev_telem[i] = ch;
|
||||
c->extra.sensor.prev_telem[i + 1] = t;
|
||||
float v = findTelemValue(buf, size, ch, t, 0.0f);
|
||||
LPPData::putFloat(&c->extra.sensor.prev_telem[i + 2], v, sz,
|
||||
LPPData::getMultiplier(t), LPPData::isSigned(t));
|
||||
i += 2 + sz;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t getTelemetryPermissions(uint8_t acl_perms, uint8_t telemetry_access) {
|
||||
if (telemetry_access == TELEMETRY_ACCESS_ALL) {
|
||||
return TELEM_PERM_BASE | TELEM_PERM_LOCATION | TELEM_PERM_ENVIRONMENT;
|
||||
@@ -228,12 +232,14 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp,
|
||||
}
|
||||
sensors.querySensors(telemetry_permissions, telemetry);
|
||||
// TODO: let requester know permissions they have: telemetry.addPresence(TELEM_CHANNEL_SELF, perms);
|
||||
uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
const uint8_t* tbuf = telemetry.getBuffer();
|
||||
const size_t tlen = LPPData::boundedPrefix(tbuf, telemetry.getSize(), reply_capacity - 4);
|
||||
memcpy(&reply_data[4], tbuf, tlen);
|
||||
return 4 + tlen; // reply_len
|
||||
}
|
||||
if (req_type == REQ_TYPE_GET_AVG_MIN_MAX && payload_len >= 10
|
||||
&& (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
||||
if (reply_capacity < 8) return 0; // tag and current timestamp
|
||||
uint32_t start_secs_ago, end_secs_ago;
|
||||
memcpy(&start_secs_ago, &payload[0], 4);
|
||||
memcpy(&end_secs_ago, &payload[4], 4);
|
||||
@@ -248,7 +254,8 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp,
|
||||
n = 0;
|
||||
}
|
||||
|
||||
uint8_t ofs = 4;
|
||||
if (n < 0 || n > int(sizeof(data) / sizeof(data[0]))) return 0;
|
||||
size_t ofs = 4;
|
||||
{
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
memcpy(&reply_data[ofs], &now, 4); ofs += 4;
|
||||
@@ -256,9 +263,13 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp,
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
auto d = &data[i];
|
||||
if (!LPPData::isScalarType(d->_lpp_type)
|
||||
|| !isfinite(d->_min) || !isfinite(d->_max) || !isfinite(d->_avg)) continue;
|
||||
const uint8_t sz = LPPData::getDataSize(d->_lpp_type);
|
||||
const size_t item_len = 2 + 3 * sz;
|
||||
if (item_len > reply_capacity - ofs) break;
|
||||
reply_data[ofs++] = d->_channel;
|
||||
reply_data[ofs++] = d->_lpp_type;
|
||||
uint8_t sz = LPPData::getDataSize(d->_lpp_type);
|
||||
uint32_t mult = LPPData::getMultiplier(d->_lpp_type);
|
||||
bool is_signed = LPPData::isSigned(d->_lpp_type);
|
||||
ofs += LPPData::putFloat(&reply_data[ofs], d->_min, sz, mult, is_signed);
|
||||
@@ -284,7 +295,6 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp,
|
||||
}
|
||||
}
|
||||
if (req_type == REQ_TYPE_SUBSCRIBE && payload_len >= 8 && (perms & PERM_ACL_ROLE_MASK) >= PERM_ACL_READ_ONLY) {
|
||||
memcpy(&from->extra.sensor.push_tag, &payload[0], 4);
|
||||
uint16_t timeout_secs;
|
||||
memcpy(&timeout_secs, &payload[4], 2);
|
||||
const uint8_t min_deltas_len = payload[7];
|
||||
@@ -303,11 +313,14 @@ uint8_t SensorMesh::handleRequest(ClientInfo* from, uint32_t sender_timestamp,
|
||||
valid_deltas = false;
|
||||
break;
|
||||
}
|
||||
const size_t item_len = 2 + LPPData::getDataSize(payload[8 + i + 1]);
|
||||
valid_deltas = item_len <= min_deltas_len - i;
|
||||
const uint8_t type = payload[8 + i + 1];
|
||||
const size_t item_len = 2 + LPPData::getDataSize(type);
|
||||
valid_deltas = LPPData::isScalarType(type)
|
||||
&& item_len <= min_deltas_len - i;
|
||||
i += item_len;
|
||||
}
|
||||
if (r && valid_deltas) {
|
||||
memcpy(&from->extra.sensor.push_tag, &payload[0], 4);
|
||||
from->extra.sensor.scope_region_id = r->id;
|
||||
from->extra.sensor.expiry_timestamp = getRTCClock()->getCurrentTime() + timeout_secs;
|
||||
from->extra.sensor.min_deltas_len = min_deltas_len;
|
||||
@@ -1059,7 +1072,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
|
||||
region_map(key_store), temp_map(key_store),
|
||||
_cli(board, rtc, sensors, region_map, acl, &_prefs, this),
|
||||
_clock_sync(radio, ms, rtc, acl, sensors, _prefs.tx_delay_factor, this),
|
||||
telemetry(MAX_PACKET_PAYLOAD - 4)
|
||||
telemetry(mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY - 8)
|
||||
{
|
||||
next_local_advert = next_flood_advert = 0;
|
||||
dirty_contacts_expiry = 0;
|
||||
@@ -1446,7 +1459,7 @@ void SensorMesh::loop() {
|
||||
TransportKey scope;
|
||||
if (region_map.getTransportKeysFor(*region, &scope, 1) == 0) continue;
|
||||
const uint8_t telemetry_len = telemetry.getSize();
|
||||
if (telemetry_len > sizeof(reply_data) - 8) continue;
|
||||
if (telemetry_len > mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY - 8) continue;
|
||||
memcpy(reply_data, &client->extra.sensor.push_tag, 4);
|
||||
const uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
|
||||
memcpy(&reply_data[4], ×tamp, 4);
|
||||
@@ -1455,11 +1468,10 @@ void SensorMesh::loop() {
|
||||
PAYLOAD_TYPE_RESPONSE, client->id, client->shared_secret,
|
||||
reply_data, 8 + telemetry_len);
|
||||
if (reply) {
|
||||
if (mesh::Packet::isValidPathLen(client->out_path_len)) {
|
||||
sendDirect(reply, client->out_path, client->out_path_len, 0);
|
||||
} else {
|
||||
sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1);
|
||||
}
|
||||
const bool queued = mesh::Packet::isValidPathLen(client->out_path_len)
|
||||
? sendDirect(reply, client->out_path, client->out_path_len, 0)
|
||||
: sendFloodScoped(scope, reply, 0, _prefs.path_hash_mode + 1);
|
||||
if (queued) snapshotTelemetry(client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -241,6 +241,7 @@ private:
|
||||
bool region_load_active;
|
||||
|
||||
bool telemHasChanged(ClientInfo* c);
|
||||
void snapshotTelemetry(ClientInfo* c);
|
||||
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
|
||||
uint8_t handleRequest(ClientInfo* from, uint32_t sender_timestamp,
|
||||
uint8_t req_type, uint8_t* payload, size_t payload_len,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include <math.h>
|
||||
|
||||
#define LPP_DIGITAL_INPUT 0 // 1 byte
|
||||
#define LPP_DIGITAL_OUTPUT 1 // 1 byte
|
||||
@@ -13,8 +15,8 @@
|
||||
#define LPP_RELATIVE_HUMIDITY 104 // 1 byte, 0.5% unsigned
|
||||
#define LPP_ACCELEROMETER 113 // 2 bytes per axis, 0.001G
|
||||
#define LPP_BAROMETRIC_PRESSURE 115 // 2 bytes 0.1hPa unsigned
|
||||
#define LPP_VOLTAGE 116 // 2 bytes 0.01V unsigned
|
||||
#define LPP_CURRENT 117 // 2 bytes 0.001A unsigned
|
||||
#define LPP_VOLTAGE 116 // 2 bytes 0.01V signed
|
||||
#define LPP_CURRENT 117 // 2 bytes 0.001A signed
|
||||
#define LPP_FREQUENCY 118 // 4 bytes 1Hz unsigned
|
||||
#define LPP_PERCENTAGE 120 // 1 byte 1-100% unsigned
|
||||
#define LPP_ALTITUDE 121 // 2 byte 1m signed
|
||||
@@ -65,6 +67,53 @@
|
||||
|
||||
class LPPData {
|
||||
public:
|
||||
// Delta subscriptions and history records encode one scalar, not vectors.
|
||||
static bool isScalarType(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_DIGITAL_INPUT:
|
||||
case LPP_DIGITAL_OUTPUT:
|
||||
case LPP_ANALOG_INPUT:
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
case LPP_GENERIC_SENSOR:
|
||||
case LPP_LUMINOSITY:
|
||||
case LPP_PRESENCE:
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_VOLTAGE:
|
||||
case LPP_CURRENT:
|
||||
case LPP_FREQUENCY:
|
||||
case LPP_PERCENTAGE:
|
||||
case LPP_ALTITUDE:
|
||||
case LPP_CONCENTRATION:
|
||||
case LPP_POWER:
|
||||
case LPP_DISTANCE:
|
||||
case LPP_ENERGY:
|
||||
case LPP_DIRECTION:
|
||||
case LPP_UNIXTIME:
|
||||
case LPP_SWITCH:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep complete known LPP entries within the supplied payload budget.
|
||||
static size_t boundedPrefix(const uint8_t* buf, size_t size, size_t capacity) {
|
||||
if (buf == NULL) return 0;
|
||||
size_t pos = 0;
|
||||
while (pos + 2 <= size) {
|
||||
if (buf[pos] == 0) break; // channel zero is end-of-data
|
||||
const uint8_t type = buf[pos + 1];
|
||||
if (!LPPData::isScalarType(type) && type != LPP_GPS
|
||||
&& type != LPP_ACCELEROMETER && type != LPP_GYROMETER && type != LPP_COLOUR) break;
|
||||
const size_t item_len = 2 + LPPData::getDataSize(type);
|
||||
if (item_len > size - pos || item_len > capacity - pos) break;
|
||||
pos += item_len;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
static uint8_t getDataSize(uint8_t type) {
|
||||
switch (type) {
|
||||
case LPP_GPS:
|
||||
@@ -88,7 +137,6 @@ public:
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_CONCENTRATION:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
case LPP_ALTITUDE:
|
||||
case LPP_VOLTAGE:
|
||||
case LPP_CURRENT:
|
||||
@@ -109,20 +157,23 @@ public:
|
||||
case LPP_ANALOG_INPUT:
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
return 100;
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
return 2;
|
||||
case LPP_TEMPERATURE:
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
return 10;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static bool isSigned(uint8_t type) {
|
||||
return type == LPP_ALTITUDE || type == LPP_TEMPERATURE || type == LPP_GYROMETER ||
|
||||
type == LPP_ANALOG_INPUT || type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
|
||||
return type == LPP_VOLTAGE || type == LPP_CURRENT || type == LPP_ALTITUDE ||
|
||||
type == LPP_TEMPERATURE || type == LPP_GYROMETER || type == LPP_ANALOG_INPUT ||
|
||||
type == LPP_ANALOG_OUTPUT || type == LPP_GPS || type == LPP_ACCELEROMETER;
|
||||
}
|
||||
|
||||
static float getFloat(const uint8_t * buffer, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||
if (size == 0 || size > sizeof(uint32_t) || multiplier == 0) return 0.0f;
|
||||
uint32_t value = 0;
|
||||
for (uint8_t i = 0; i < size; i++) {
|
||||
value = (value << 8) + buffer[i];
|
||||
@@ -130,7 +181,7 @@ public:
|
||||
|
||||
int sign = 1;
|
||||
if (is_signed) {
|
||||
uint32_t bit = 1ul << ((size * 8) - 1);
|
||||
uint32_t bit = uint32_t(1) << ((size * 8) - 1);
|
||||
if ((value & bit) == bit) {
|
||||
value = (bit << 1) - value;
|
||||
sign = -1;
|
||||
@@ -140,18 +191,24 @@ public:
|
||||
}
|
||||
|
||||
static uint8_t putFloat(uint8_t * dest, float value, uint8_t size, uint32_t multiplier, bool is_signed) {
|
||||
// check sign
|
||||
bool sign = value < 0;
|
||||
if (sign) value = -value;
|
||||
if (size == 0 || size > sizeof(uint32_t) || multiplier == 0
|
||||
|| !isfinite(value)) return 0;
|
||||
|
||||
// get value to store
|
||||
uint32_t v = value * multiplier;
|
||||
|
||||
// format an uint32_t as if it was an int32_t
|
||||
if (is_signed & sign) {
|
||||
uint32_t mask = (1 << (size * 8)) - 1;
|
||||
v = v & mask;
|
||||
if (sign) v = mask - v + 1;
|
||||
const uint32_t mask = size == sizeof(uint32_t)
|
||||
? UINT32_MAX : (uint32_t(1) << (size * 8)) - 1;
|
||||
// Preserve the existing float scaling before checking integer limits.
|
||||
const double scaled = (double)(value * multiplier);
|
||||
uint32_t v;
|
||||
if (is_signed) {
|
||||
const bool negative = scaled < 0;
|
||||
const uint32_t sign_bit = uint32_t(1) << ((size * 8) - 1);
|
||||
const uint32_t limit = negative ? sign_bit : sign_bit - 1;
|
||||
const double magnitude = negative ? -scaled : scaled;
|
||||
v = magnitude >= (double)limit ? limit : (uint32_t)magnitude;
|
||||
if (negative) v = (uint32_t(0) - v) & mask;
|
||||
} else {
|
||||
// float(UINT32_MAX) rounds to 2^32; clamp before the integer cast.
|
||||
v = scaled <= 0.0 ? 0 : scaled >= (double)mask ? mask : (uint32_t)scaled;
|
||||
}
|
||||
|
||||
// add bytes (MSB first)
|
||||
@@ -193,7 +250,7 @@ public:
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readVoltage(float& voltage) {
|
||||
voltage = LPPData::getFloat(&_buf[_pos], 2, 100, false); _pos += 2;
|
||||
voltage = LPPData::getFloat(&_buf[_pos], 2, 100, true); _pos += 2;
|
||||
return _pos <= _len;
|
||||
}
|
||||
bool readCurrent(float& amps) {
|
||||
@@ -243,24 +300,18 @@ public:
|
||||
uint8_t sz = LPPData::getDataSize(type);
|
||||
bool s = LPPData::isSigned(type);
|
||||
uint32_t mul = LPPData::getMultiplier(type);
|
||||
if (_len + 2 + sz <= _max_len) {
|
||||
_buf[_len++] = channel;
|
||||
_buf[_len++] = type;
|
||||
_len += LPPData::putFloat(&_buf[_len], v, sz, mul, s);
|
||||
if (LPPData::isScalarType(type) && _len + 2 + sz <= _max_len) {
|
||||
if (LPPData::putFloat(&_buf[_len + 2], v, sz, mul, s) != sz) return false;
|
||||
_buf[_len] = channel;
|
||||
_buf[_len + 1] = type;
|
||||
_len += 2 + sz;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool writeVoltage(uint8_t channel, float voltage) {
|
||||
if (_len + 4 <= _max_len) {
|
||||
_buf[_len++] = channel;
|
||||
_buf[_len++] = LPP_VOLTAGE;
|
||||
uint16_t value = voltage * 100;
|
||||
write(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return writeData(channel, LPP_VOLTAGE, voltage);
|
||||
}
|
||||
|
||||
bool writeGPS(uint8_t channel, float lat, float lon, float alt) {
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <helpers/ClientACLResponse.h>
|
||||
#include <helpers/sensors/LPPDataHelpers.h>
|
||||
|
||||
#include "telemetry_capacity.inc"
|
||||
#define REQ_TYPE_GET_TELEMETRY_DATA SENSOR_REQ_TYPE_GET_TELEMETRY_DATA
|
||||
#define PERM_ACL_GUEST 0
|
||||
#define REQ_TYPE_GET_AVG_MIN_MAX 0x04
|
||||
#define PERM_ACL_ROLE_MASK 7
|
||||
#define PERM_ACL_READ_ONLY 1
|
||||
#define TELEMETRY_ACCESS_ALL 2
|
||||
#define TELEM_PERM_BASE 1
|
||||
#define TELEM_PERM_LOCATION 2
|
||||
#define TELEM_PERM_ENVIRONMENT 4
|
||||
#define TELEM_CHANNEL_SELF 1
|
||||
#define GPS_READ_INTERVAL_SECS 60
|
||||
|
||||
namespace mesh {
|
||||
struct Identity {
|
||||
uint8_t pub_key[PUB_KEY_SIZE] = {};
|
||||
size_t copyHashTo(uint8_t* destination) const { *destination = 0x42; return PATH_HASH_SIZE; }
|
||||
};
|
||||
struct Utils {
|
||||
// The production packet builders retain their exact admission and layout.
|
||||
// Cryptography alone is replaced with deterministic padded plaintext.
|
||||
static int encryptThenMAC(const uint8_t*, uint8_t* destination,
|
||||
const uint8_t* source, int length) {
|
||||
const int encrypted = (length + CIPHER_BLOCK_SIZE - 1) / CIPHER_BLOCK_SIZE * CIPHER_BLOCK_SIZE;
|
||||
memset(destination, 0, encrypted + CIPHER_MAC_SIZE);
|
||||
memcpy(destination + CIPHER_MAC_SIZE, source, length);
|
||||
return encrypted + CIPHER_MAC_SIZE;
|
||||
}
|
||||
};
|
||||
struct Random { void random(uint8_t* data, size_t length) { memset(data, 0x99, length); } };
|
||||
class Mesh {
|
||||
std::vector<Packet*> allocations;
|
||||
public:
|
||||
Identity self_id;
|
||||
Random rng;
|
||||
~Mesh() { for (auto* packet : allocations) delete packet; }
|
||||
Packet* obtainNewPacket() { auto* packet = new Packet; allocations.push_back(packet); return packet; }
|
||||
uint8_t getContactTxRadio(const Identity&) { return RADIO_TX_AUTO; }
|
||||
Random* getRNG() { return &rng; }
|
||||
Packet* createPathReturn(const Identity&, const uint8_t*, const uint8_t*, uint8_t,
|
||||
uint8_t, const uint8_t*, size_t);
|
||||
Packet* createPathReturn(const uint8_t*, const uint8_t*, const uint8_t*, uint8_t,
|
||||
uint8_t, const uint8_t*, size_t);
|
||||
Packet* createDatagram(uint8_t, const Identity&, const uint8_t*, const uint8_t*, size_t);
|
||||
};
|
||||
}
|
||||
#include "history_types.inc"
|
||||
struct ClientInfo {
|
||||
mesh::Identity id;
|
||||
uint8_t permissions = 3;
|
||||
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == 3; }
|
||||
};
|
||||
struct Clock : mesh::RTCClock {
|
||||
uint32_t getCurrentTime() override { return 1000; }
|
||||
void setCurrentTime(uint32_t) override {}
|
||||
};
|
||||
// Sensor and telemetry collection boundaries are modeled; handler and LPP
|
||||
// sizing/conversion, real history aggregation, and packet assembly are actual.
|
||||
struct Telemetry {
|
||||
std::vector<uint8_t> data;
|
||||
size_t max_len = SENSOR_TELEMETRY_CAPACITY;
|
||||
Telemetry() { data.reserve(max_len); }
|
||||
void reset() { data.clear(); }
|
||||
uint8_t getSize() const { return data.size(); }
|
||||
uint8_t* getBuffer() { return data.data(); }
|
||||
void add(uint8_t channel, uint8_t type, size_t size) {
|
||||
if (data.size() + 2 + size > max_len) return;
|
||||
data.push_back(channel); data.push_back(type);
|
||||
for (size_t i = 0; i < size; ++i) data.push_back(0x22);
|
||||
}
|
||||
void addVoltage(uint8_t channel, float) { add(channel, LPP_VOLTAGE, 2); }
|
||||
void addTemperature(uint8_t channel, float) { add(channel, LPP_TEMPERATURE, 2); }
|
||||
};
|
||||
struct Sensors {
|
||||
std::vector<uint8_t> body;
|
||||
void requestGpsTelemetryTimeSync(uint32_t) {}
|
||||
void querySensors(uint8_t, Telemetry& telemetry) {
|
||||
if (telemetry.data.size() + body.size() <= telemetry.max_len)
|
||||
telemetry.data.insert(telemetry.data.end(), body.begin(), body.end());
|
||||
}
|
||||
};
|
||||
struct Board {
|
||||
uint16_t getBattMilliVolts() { return 3500; }
|
||||
float getMCUTemperature() { return NAN; }
|
||||
};
|
||||
class SensorMesh : public mesh::Mesh {
|
||||
public:
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD] = {};
|
||||
struct { uint8_t telemetry_access = TELEMETRY_ACCESS_ALL; } _prefs;
|
||||
Telemetry telemetry;
|
||||
Sensors sensors;
|
||||
Board board;
|
||||
Clock clock;
|
||||
std::array<MinMaxAvg, 8> history;
|
||||
int history_count = 8;
|
||||
bool empty_real_series = false;
|
||||
Clock* getRTCClock() { return &clock; }
|
||||
SensorMesh() {
|
||||
for (size_t i = 0; i < history.size(); ++i)
|
||||
history[i] = {1.0f, 2.0f, 1.5f, LPP_GENERIC_SENSOR, uint8_t(i + 1)};
|
||||
}
|
||||
int querySeriesData(uint32_t start, uint32_t end, MinMaxAvg* destination, int max_num) {
|
||||
assert(max_num == 8);
|
||||
if (empty_real_series) {
|
||||
float values[4];
|
||||
TimeSeriesData series(values, 4, 60);
|
||||
series.calcMinMaxAvg(&clock, start, end, destination, TELEM_CHANNEL_SELF, LPP_VOLTAGE);
|
||||
return 1;
|
||||
}
|
||||
for (int i = 0; i < std::min(max_num, std::max(0, history_count)); ++i) destination[i] = history[i];
|
||||
return history_count;
|
||||
}
|
||||
uint8_t handleRequest(ClientInfo*, uint32_t, uint8_t, uint8_t*, size_t,
|
||||
size_t = mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
};
|
||||
class MyMesh : public SensorMesh {
|
||||
public:
|
||||
MyMesh() { telemetry.max_len = ROOM_TELEMETRY_CAPACITY; telemetry.data.reserve(telemetry.max_len); }
|
||||
int handleRequest(ClientInfo*, uint32_t, uint8_t*, size_t,
|
||||
size_t = mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
};
|
||||
#include "production.inc"
|
||||
|
||||
static std::vector<uint8_t> entry(uint8_t type, unsigned size, uint8_t channel = 2) {
|
||||
std::vector<uint8_t> body{channel, type};
|
||||
body.insert(body.end(), size, 0x33);
|
||||
return body;
|
||||
}
|
||||
static void verify_packet(SensorMesh& target, ClientInfo& client, unsigned length,
|
||||
bool flood, uint8_t encoded_path) {
|
||||
uint8_t secret[PUB_KEY_SIZE] = {}, path[MAX_PATH_SIZE] = {};
|
||||
auto* packet = flood ? target.createPathReturn(client.id, secret, path, encoded_path,
|
||||
PAYLOAD_TYPE_RESPONSE, target.reply_data, length)
|
||||
: target.createDatagram(PAYLOAD_TYPE_RESPONSE, client.id, secret,
|
||||
target.reply_data, length);
|
||||
assert(packet && packet->payload_len <= MAX_PACKET_PAYLOAD);
|
||||
const unsigned path_bytes = flood ? (encoded_path & 63) * ((encoded_path >> 6) + 1) : 0;
|
||||
const unsigned decoded_size = packet->payload_len - 2 * PATH_HASH_SIZE - CIPHER_MAC_SIZE
|
||||
- (flood ? path_bytes + 2 : 0);
|
||||
assert(decoded_size + 2 <= MAX_FRAME_SIZE);
|
||||
}
|
||||
static void telemetry_checks() {
|
||||
ClientInfo client;
|
||||
uint8_t mask = 0;
|
||||
for (bool flood : {false, true}) {
|
||||
for (unsigned path = 0; path < 256; ++path) {
|
||||
if (!mesh::Packet::isValidPathLen(path)) continue;
|
||||
const size_t capacity = std::min(mesh::clientACLReplyCapacity(flood, path),
|
||||
mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
SensorMesh target;
|
||||
for (unsigned i = 0; i < SENSOR_TELEMETRY_CAPACITY / 3; ++i) {
|
||||
auto item = entry(LPP_RELATIVE_HUMIDITY, 1, i + 2);
|
||||
target.sensors.body.insert(target.sensors.body.end(), item.begin(), item.end());
|
||||
}
|
||||
// Suppress self voltage to fill the actual capacity with whole humidity records.
|
||||
mask = TELEM_PERM_BASE;
|
||||
const unsigned length = target.handleRequest(&client, 51, REQ_TYPE_GET_TELEMETRY_DATA,
|
||||
&mask, 1, capacity);
|
||||
assert(length <= capacity && length >= 4 && (length - 4) % 3 == 0);
|
||||
assert(length == 4 + std::min(size_t(SENSOR_TELEMETRY_CAPACITY / 3), (capacity - 4) / 3) * 3);
|
||||
assert(!memcmp(target.reply_data + 4, target.sensors.body.data(), length - 4));
|
||||
verify_packet(target, client, length, flood, path);
|
||||
}
|
||||
}
|
||||
for (uint8_t type : {uint8_t(LPP_GPS), uint8_t(LPP_ACCELEROMETER), uint8_t(LPP_GYROMETER), uint8_t(LPP_COLOUR)}) {
|
||||
SensorMesh target;
|
||||
target.sensors.body = entry(type, LPPData::getDataSize(type));
|
||||
mask = TELEM_PERM_BASE;
|
||||
const unsigned full = 4 + target.sensors.body.size();
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_TELEMETRY_DATA, &mask, 1, full) == full);
|
||||
assert(!memcmp(target.reply_data + 4, target.sensors.body.data(), full - 4));
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_TELEMETRY_DATA, &mask, 1, full - 1) == 4);
|
||||
}
|
||||
{
|
||||
SensorMesh target;
|
||||
mask = 0xFF;
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_TELEMETRY_DATA, &mask, 1) == 4);
|
||||
mask = TELEM_PERM_BASE;
|
||||
}
|
||||
for (unsigned capacity = 0; capacity < 4; ++capacity) {
|
||||
SensorMesh target;
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_TELEMETRY_DATA, &mask, 1, capacity) == 0);
|
||||
}
|
||||
for (auto invalid : {std::vector<uint8_t>{2}, std::vector<uint8_t>{2, LPP_GPS, 3},
|
||||
entry(0xFF, 1), entry(LPP_POLYLINE, 8), entry(LPP_VOLTAGE, 2, 0)}) {
|
||||
SensorMesh target;
|
||||
target.sensors.body = invalid;
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_TELEMETRY_DATA, &mask, 1) == 4);
|
||||
}
|
||||
}
|
||||
static void room_telemetry_checks() {
|
||||
ClientInfo client;
|
||||
uint8_t query[] = {ROOM_REQ_TYPE_GET_TELEMETRY_DATA, 0};
|
||||
for (bool flood : {false, true}) {
|
||||
for (unsigned path = 0; path < 256; ++path) {
|
||||
if (!mesh::Packet::isValidPathLen(path)) continue;
|
||||
const size_t capacity = std::min(mesh::clientACLReplyCapacity(flood, path),
|
||||
mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
MyMesh target;
|
||||
for (unsigned i = 0; i < ROOM_TELEMETRY_CAPACITY / 4 - 1; ++i) {
|
||||
auto item = entry(LPP_VOLTAGE, 2, i + 2);
|
||||
target.sensors.body.insert(target.sensors.body.end(), item.begin(), item.end());
|
||||
}
|
||||
const unsigned length = target.handleRequest(&client, 51, query, sizeof(query), capacity);
|
||||
assert(length == 4 + std::min(size_t(ROOM_TELEMETRY_CAPACITY / 4), (capacity - 4) / 4) * 4);
|
||||
assert(length <= capacity && (length - 4) % 4 == 0);
|
||||
assert(!memcmp(target.reply_data + 4, target.telemetry.getBuffer(), length - 4));
|
||||
verify_packet(target, client, length, flood, path);
|
||||
}
|
||||
}
|
||||
for (uint8_t type : {uint8_t(LPP_GPS), uint8_t(LPP_ACCELEROMETER), uint8_t(LPP_GYROMETER), uint8_t(LPP_COLOUR)}) {
|
||||
MyMesh target;
|
||||
target.sensors.body = entry(type, LPPData::getDataSize(type));
|
||||
const unsigned full = 8 + target.sensors.body.size();
|
||||
assert(target.handleRequest(&client, 51, query, sizeof(query), full) == int(full));
|
||||
assert(!memcmp(target.reply_data + 8, target.sensors.body.data(), full - 8));
|
||||
assert(target.handleRequest(&client, 51, query, sizeof(query), full - 1) == 8);
|
||||
}
|
||||
for (unsigned capacity = 0; capacity < 4; ++capacity) {
|
||||
MyMesh target;
|
||||
assert(target.handleRequest(&client, 51, query, sizeof(query), capacity) == 0);
|
||||
}
|
||||
for (auto invalid : {std::vector<uint8_t>{2}, std::vector<uint8_t>{2, LPP_GPS, 3},
|
||||
entry(0xFF, 1), entry(LPP_POLYLINE, 8), entry(LPP_VOLTAGE, 2, 0)}) {
|
||||
MyMesh target;
|
||||
target.sensors.body = invalid;
|
||||
assert(target.handleRequest(&client, 51, query, sizeof(query)) == 8);
|
||||
}
|
||||
MyMesh target;
|
||||
assert(target.handleRequest(&client, 51, query, 1) == 0);
|
||||
assert(target.handleRequest(&client, 51, query, 0) == 0);
|
||||
assert(target.handleRequest(&client, 51, nullptr, 2) == 0);
|
||||
assert(target.handleRequest(nullptr, 51, query, sizeof(query)) == 0);
|
||||
}
|
||||
|
||||
static void history_checks() {
|
||||
ClientInfo client;
|
||||
uint8_t query[10] = {};
|
||||
for (bool flood : {false, true}) {
|
||||
for (unsigned path = 0; path < 256; ++path) {
|
||||
if (!mesh::Packet::isValidPathLen(path)) continue;
|
||||
const size_t capacity = std::min(mesh::clientACLReplyCapacity(flood, path),
|
||||
mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
SensorMesh target;
|
||||
const unsigned length = target.handleRequest(&client, 51, REQ_TYPE_GET_AVG_MIN_MAX,
|
||||
query, sizeof(query), capacity);
|
||||
assert(length == 8 + std::min(size_t(8), (capacity - 8) / 14) * 14);
|
||||
uint32_t tag, now;
|
||||
memcpy(&tag, target.reply_data, 4); memcpy(&now, target.reply_data + 4, 4);
|
||||
assert(tag == 51 && now == 1000);
|
||||
for (unsigned i = 8, channel = 1; i < length; i += 14, ++channel) {
|
||||
assert(target.reply_data[i] == channel && target.reply_data[i + 1] == LPP_GENERIC_SENSOR);
|
||||
assert(LPPData::getFloat(target.reply_data + i + 2, 4, 1, false) == 1);
|
||||
assert(LPPData::getFloat(target.reply_data + i + 6, 4, 1, false) == 2);
|
||||
assert(LPPData::getFloat(target.reply_data + i + 10, 4, 1, false) == 1);
|
||||
}
|
||||
verify_packet(target, client, length, flood, path);
|
||||
}
|
||||
}
|
||||
for (unsigned type = 0; type < 256; ++type) {
|
||||
if (!LPPData::isScalarType(type)) continue;
|
||||
SensorMesh target;
|
||||
target.history_count = 1;
|
||||
target.history[0]._lpp_type = type;
|
||||
const unsigned length = target.handleRequest(&client, 51, REQ_TYPE_GET_AVG_MIN_MAX, query, sizeof(query));
|
||||
assert(length == 8u + 2u + 3u * LPPData::getDataSize(type));
|
||||
assert(target.reply_data[8] == 1 && target.reply_data[9] == type);
|
||||
assert(LPPData::getFloat(target.reply_data + 10, LPPData::getDataSize(type),
|
||||
LPPData::getMultiplier(type), LPPData::isSigned(type)) == 1.0f);
|
||||
verify_packet(target, client, length, false, 0);
|
||||
}
|
||||
for (uint8_t type : {uint8_t(LPP_GPS), uint8_t(LPP_ACCELEROMETER), uint8_t(LPP_GYROMETER),
|
||||
uint8_t(LPP_COLOUR), uint8_t(LPP_POLYLINE), uint8_t(0xFF)}) {
|
||||
SensorMesh target;
|
||||
target.history_count = 8;
|
||||
for (auto& record : target.history) record._lpp_type = type;
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_AVG_MIN_MAX, query, sizeof(query)) == 8);
|
||||
}
|
||||
for (int count : {-1, 9, 100}) {
|
||||
SensorMesh target;
|
||||
target.history_count = count;
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_AVG_MIN_MAX, query, sizeof(query)) == 0);
|
||||
}
|
||||
for (unsigned capacity = 0; capacity < 8; ++capacity) {
|
||||
SensorMesh target;
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_AVG_MIN_MAX, query, sizeof(query), capacity) == 0);
|
||||
}
|
||||
{
|
||||
SensorMesh target;
|
||||
target.empty_real_series = true;
|
||||
// The actual history aggregator returns NaN for an empty query range.
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_AVG_MIN_MAX, query, sizeof(query)) == 8);
|
||||
}
|
||||
for (float value : {NAN, INFINITY, -INFINITY}) {
|
||||
SensorMesh target;
|
||||
target.history_count = 1;
|
||||
target.history[0]._avg = value;
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_AVG_MIN_MAX, query, sizeof(query)) == 8);
|
||||
}
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
if (!strcmp(argv[1], "room")) room_telemetry_checks();
|
||||
else if (!strcmp(argv[1], "telemetry")) telemetry_checks();
|
||||
else if (!strcmp(argv[1], "history")) history_checks();
|
||||
else if (!strcmp(argv[1], "empty")) {
|
||||
SensorMesh target;
|
||||
ClientInfo client;
|
||||
uint8_t query[10] = {};
|
||||
target.empty_real_series = true;
|
||||
assert(target.handleRequest(&client, 51, REQ_TYPE_GET_AVG_MIN_MAX, query, sizeof(query)) == 8);
|
||||
}
|
||||
else if (!strcmp(argv[1], "packet")) {
|
||||
SensorMesh target;
|
||||
ClientInfo client;
|
||||
for (unsigned i = 0; i < SENSOR_TELEMETRY_CAPACITY / 4; ++i) {
|
||||
auto item = entry(LPP_VOLTAGE, 2, i + 2);
|
||||
target.sensors.body.insert(target.sensors.body.end(), item.begin(), item.end());
|
||||
}
|
||||
uint8_t mask = TELEM_PERM_BASE;
|
||||
const unsigned length = target.handleRequest(&client, 51, REQ_TYPE_GET_TELEMETRY_DATA, &mask, 1);
|
||||
verify_packet(target, client, length, false, 0);
|
||||
}
|
||||
else assert(false);
|
||||
std::cout << "Sensor response bounds and packet admission passed\n";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,370 @@
|
||||
// Unmodified scalar metadata methods from Electronic Cats CayenneLPP1.6.1.
|
||||
// https://github.com/ElectronicCats/CayenneLPP
|
||||
// Source package pinned by platformio.ini to electroniccats/CayenneLPP@1.6.1.
|
||||
// Upstream src/CayenneLPP.cpp SHA256: 3dab5bca79280efaa0f286741c08e8e1bd3cd6de01a870154b17131e5322f002
|
||||
// MIT license is preserved in CAYENNE_LICENSE.txt.
|
||||
#define LPP_DIGITAL_INPUT_SIZE 1
|
||||
#define LPP_DIGITAL_OUTPUT_SIZE 1
|
||||
#define LPP_ANALOG_INPUT_SIZE 2
|
||||
#define LPP_ANALOG_OUTPUT_SIZE 2
|
||||
#define LPP_GENERIC_SENSOR_SIZE 4
|
||||
#define LPP_LUMINOSITY_SIZE 2
|
||||
#define LPP_PRESENCE_SIZE 1
|
||||
#define LPP_TEMPERATURE_SIZE 2
|
||||
#define LPP_RELATIVE_HUMIDITY_SIZE 1
|
||||
#define LPP_ACCELEROMETER_SIZE 6
|
||||
#define LPP_BAROMETRIC_PRESSURE_SIZE 2
|
||||
#define LPP_VOLTAGE_SIZE 2
|
||||
#define LPP_CURRENT_SIZE 2
|
||||
#define LPP_FREQUENCY_SIZE 4
|
||||
#define LPP_PERCENTAGE_SIZE 1
|
||||
#define LPP_ALTITUDE_SIZE 2
|
||||
#define LPP_POWER_SIZE 2
|
||||
#define LPP_DISTANCE_SIZE 4
|
||||
#define LPP_ENERGY_SIZE 4
|
||||
#define LPP_DIRECTION_SIZE 2
|
||||
#define LPP_UNIXTIME_SIZE 4
|
||||
#define LPP_GYROMETER_SIZE 6
|
||||
#define LPP_GPS_SIZE 9
|
||||
#define LPP_SWITCH_SIZE 1
|
||||
#define LPP_CONCENTRATION_SIZE 2
|
||||
#define LPP_COLOUR_SIZE 3
|
||||
#define LPP_MIN_POLYLINE_SIZE 8
|
||||
#define LPP_DIGITAL_INPUT_MULT 1
|
||||
#define LPP_DIGITAL_OUTPUT_MULT 1
|
||||
#define LPP_ANALOG_INPUT_MULT 100
|
||||
#define LPP_ANALOG_OUTPUT_MULT 100
|
||||
#define LPP_GENERIC_SENSOR_MULT 1
|
||||
#define LPP_LUMINOSITY_MULT 1
|
||||
#define LPP_PRESENCE_MULT 1
|
||||
#define LPP_TEMPERATURE_MULT 10
|
||||
#define LPP_RELATIVE_HUMIDITY_MULT 2
|
||||
#define LPP_ACCELEROMETER_MULT 1000
|
||||
#define LPP_BAROMETRIC_PRESSURE_MULT 10
|
||||
#define LPP_VOLTAGE_MULT 100
|
||||
#define LPP_CURRENT_MULT 1000
|
||||
#define LPP_FREQUENCY_MULT 1
|
||||
#define LPP_PERCENTAGE_MULT 1
|
||||
#define LPP_ALTITUDE_MULT 1
|
||||
#define LPP_POWER_MULT 1
|
||||
#define LPP_DISTANCE_MULT 1000
|
||||
#define LPP_ENERGY_MULT 1000
|
||||
#define LPP_DIRECTION_MULT 1
|
||||
#define LPP_UNIXTIME_MULT 1
|
||||
#define LPP_GYROMETER_MULT 100
|
||||
#define LPP_GPS_LAT_LON_MULT 10000
|
||||
#define LPP_GPS_ALT_MULT 100
|
||||
#define LPP_SWITCH_MULT 1
|
||||
#define LPP_CONCENTRATION_MULT 1
|
||||
#define LPP_COLOUR_MULT 1
|
||||
class CayenneLPP {
|
||||
public:
|
||||
static uint8_t getTypeSize(uint8_t);
|
||||
static uint32_t getTypeMultiplier(uint8_t);
|
||||
static bool getTypeSigned(uint8_t);
|
||||
};
|
||||
uint8_t CayenneLPP::getTypeSize(uint8_t type) {
|
||||
|
||||
switch (type) {
|
||||
|
||||
#ifndef CAYENNE_DISABLE_DIGITAL_INPUT
|
||||
case LPP_DIGITAL_INPUT:
|
||||
return LPP_DIGITAL_INPUT_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_DIGITAL_OUTPUT
|
||||
case LPP_DIGITAL_OUTPUT:
|
||||
return LPP_DIGITAL_OUTPUT_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ANALOG_INPUT
|
||||
case LPP_ANALOG_INPUT:
|
||||
return LPP_ANALOG_INPUT_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ANALOG_OUTPUT
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
return LPP_ANALOG_OUTPUT_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_GENERIC_SENSOR
|
||||
case LPP_GENERIC_SENSOR:
|
||||
return LPP_GENERIC_SENSOR_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_LUMINOSITY
|
||||
case LPP_LUMINOSITY:
|
||||
return LPP_LUMINOSITY_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_PRESENCE
|
||||
case LPP_PRESENCE:
|
||||
return LPP_PRESENCE_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_TEMPERATURE
|
||||
case LPP_TEMPERATURE:
|
||||
return LPP_TEMPERATURE_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_RELATIVE_HUMIDITY
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
return LPP_RELATIVE_HUMIDITY_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ACCELEROMETER
|
||||
case LPP_ACCELEROMETER:
|
||||
return LPP_ACCELEROMETER_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_BAROMETRIC_PRESSUE
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
return LPP_BAROMETRIC_PRESSURE_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_VOLTAGE
|
||||
case LPP_VOLTAGE:
|
||||
return LPP_VOLTAGE_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_CURRENT
|
||||
case LPP_CURRENT:
|
||||
return LPP_CURRENT_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_FREQUENCY
|
||||
case LPP_FREQUENCY:
|
||||
return LPP_FREQUENCY_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_PERCENTAGE
|
||||
case LPP_PERCENTAGE:
|
||||
return LPP_PERCENTAGE_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ALTITUDE
|
||||
case LPP_ALTITUDE:
|
||||
return LPP_ALTITUDE_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_POWER
|
||||
case LPP_POWER:
|
||||
return LPP_POWER_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_DISTANCE
|
||||
case LPP_DISTANCE:
|
||||
return LPP_DISTANCE_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ENERGY
|
||||
case LPP_ENERGY:
|
||||
return LPP_ENERGY_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_DIRECTION
|
||||
case LPP_DIRECTION:
|
||||
return LPP_DIRECTION_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_UNIX_TIME
|
||||
case LPP_UNIXTIME:
|
||||
return LPP_UNIXTIME_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_GYROMETER
|
||||
case LPP_GYROMETER:
|
||||
return LPP_GYROMETER_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_GPS
|
||||
case LPP_GPS:
|
||||
return LPP_GPS_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_SWITCH
|
||||
case LPP_SWITCH:
|
||||
return LPP_SWITCH_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_CONCENTRATION
|
||||
case LPP_CONCENTRATION:
|
||||
return LPP_CONCENTRATION_SIZE;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_COLOUR
|
||||
case LPP_COLOUR:
|
||||
return LPP_COLOUR_SIZE;
|
||||
#endif
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
uint32_t CayenneLPP::getTypeMultiplier(uint8_t type) {
|
||||
|
||||
switch (type) {
|
||||
|
||||
#ifndef CAYENNE_DISABLE_DIGITAL_INPUT
|
||||
case LPP_DIGITAL_INPUT:
|
||||
return LPP_DIGITAL_INPUT_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_DIGITAL_OUTPUT
|
||||
case LPP_DIGITAL_OUTPUT:
|
||||
return LPP_DIGITAL_OUTPUT_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ANALOG_INPUT
|
||||
case LPP_ANALOG_INPUT:
|
||||
return LPP_ANALOG_INPUT_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ANALOG_OUTPUT
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
return LPP_ANALOG_OUTPUT_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_GENERIC_SENSOR
|
||||
case LPP_GENERIC_SENSOR:
|
||||
return LPP_GENERIC_SENSOR_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_LUMINOSITY
|
||||
case LPP_LUMINOSITY:
|
||||
return LPP_LUMINOSITY_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_PRESENCE
|
||||
case LPP_PRESENCE:
|
||||
return LPP_PRESENCE_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_TEMPERATURE
|
||||
case LPP_TEMPERATURE:
|
||||
return LPP_TEMPERATURE_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_RELATIVE_HUMIDITY
|
||||
case LPP_RELATIVE_HUMIDITY:
|
||||
return LPP_RELATIVE_HUMIDITY_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ACCELEROMETER
|
||||
case LPP_ACCELEROMETER:
|
||||
return LPP_ACCELEROMETER_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_BAROMETRIC_PRESSUE
|
||||
case LPP_BAROMETRIC_PRESSURE:
|
||||
return LPP_BAROMETRIC_PRESSURE_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_VOLTAGE
|
||||
case LPP_VOLTAGE:
|
||||
return LPP_VOLTAGE_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_CURRENT
|
||||
case LPP_CURRENT:
|
||||
return LPP_CURRENT_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_FREQUENCY
|
||||
case LPP_FREQUENCY:
|
||||
return LPP_FREQUENCY_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_PERCENTAGE
|
||||
case LPP_PERCENTAGE:
|
||||
return LPP_PERCENTAGE_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ALTITUDE
|
||||
case LPP_ALTITUDE:
|
||||
return LPP_ALTITUDE_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_POWER
|
||||
case LPP_POWER:
|
||||
return LPP_POWER_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_DISTANCE
|
||||
case LPP_DISTANCE:
|
||||
return LPP_DISTANCE_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_ENERGY
|
||||
case LPP_ENERGY:
|
||||
return LPP_ENERGY_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_DIRECTION
|
||||
case LPP_DIRECTION:
|
||||
return LPP_DIRECTION_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_UNIX_TIME
|
||||
case LPP_UNIXTIME:
|
||||
return LPP_UNIXTIME_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_GYROMETER
|
||||
case LPP_GYROMETER:
|
||||
return LPP_GYROMETER_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_SWITCH
|
||||
case LPP_SWITCH:
|
||||
return LPP_SWITCH_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_CONCENTRATION
|
||||
case LPP_CONCENTRATION:
|
||||
return LPP_CONCENTRATION_MULT;
|
||||
#endif
|
||||
|
||||
#ifndef CAYENNE_DISABLE_COLOUR
|
||||
case LPP_COLOUR:
|
||||
return LPP_COLOUR_MULT;
|
||||
#endif
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
bool CayenneLPP::getTypeSigned(uint8_t type) {
|
||||
|
||||
switch (type) {
|
||||
#ifndef CAYENNE_DISABLE_VOLTAGE
|
||||
case LPP_VOLTAGE:
|
||||
#endif
|
||||
#ifndef CAYENNE_DISABLE_CURRENT
|
||||
case LPP_CURRENT:
|
||||
#endif
|
||||
#ifndef CAYENNE_DISABLE_ANALOG_INPUT
|
||||
case LPP_ANALOG_INPUT:
|
||||
#endif
|
||||
#ifndef CAYENNE_DISABLE_ANALOG_OUTPUT
|
||||
case LPP_ANALOG_OUTPUT:
|
||||
#endif
|
||||
#ifndef CAYENNE_DISABLE_TEMPERATURE
|
||||
case LPP_TEMPERATURE:
|
||||
#endif
|
||||
#ifndef CAYENNE_DISABLE_ACCELEROMETER
|
||||
case LPP_ACCELEROMETER:
|
||||
#endif
|
||||
#ifndef CAYENNE_DISABLE_ALTITUDE
|
||||
case LPP_ALTITUDE:
|
||||
#endif
|
||||
#ifndef CAYENNE_DISABLE_GYROMETER
|
||||
case LPP_GYROMETER:
|
||||
#endif
|
||||
#ifndef CAYENNE_DISABLE_GPS
|
||||
case LPP_GPS:
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
#include <helpers/ClientACLResponse.h>
|
||||
#include "LPPDataHelpers.h"
|
||||
#include "cayenne_lpp_1_6_1_metadata.inc"
|
||||
|
||||
#define PERM_ACL_ROLE_MASK 7
|
||||
#define PERM_ACL_READ_ONLY 1
|
||||
#define REQ_TYPE_SUBSCRIBE 8
|
||||
|
||||
struct ClientInfo {
|
||||
uint8_t permissions = 3;
|
||||
int id = 1;
|
||||
uint8_t shared_secret[32] = {}, out_path[MAX_PATH_SIZE] = {}, out_path_len = 0;
|
||||
struct { struct {
|
||||
uint32_t push_tag = 0, expiry_timestamp = 0;
|
||||
uint16_t scope_region_id = 0;
|
||||
uint8_t min_deltas_len = 0, min_deltas[14] = {}, prev_telem[14] = {};
|
||||
} sensor; } extra;
|
||||
bool isAdmin() const { return (permissions & 7) == 3; }
|
||||
};
|
||||
struct TransportKey {};
|
||||
struct RegionEntry {
|
||||
uint16_t id = 1;
|
||||
char name[31] = "test";
|
||||
bool isWildcard() const { return false; }
|
||||
};
|
||||
struct RegionMap {
|
||||
RegionEntry region;
|
||||
RegionEntry* getDefaultRegion() { return ®ion; }
|
||||
RegionEntry* findById(uint16_t id) { return id == region.id ? ®ion : nullptr; }
|
||||
int getTransportKeysFor(const RegionEntry&, TransportKey*, int) { return 1; }
|
||||
};
|
||||
struct Telemetry {
|
||||
std::vector<uint8_t> bytes;
|
||||
const size_t capacity;
|
||||
explicit Telemetry(size_t max_size) : capacity(max_size) {}
|
||||
bool add(uint8_t channel, uint8_t type) {
|
||||
const size_t size = 2 + LPPData::getDataSize(type);
|
||||
if (bytes.size() + size > capacity) return false;
|
||||
bytes.push_back(channel);
|
||||
bytes.push_back(type);
|
||||
bytes.insert(bytes.end(), size - 2, 0);
|
||||
return true;
|
||||
}
|
||||
uint8_t* getBuffer() { return bytes.data(); }
|
||||
uint8_t getSize() { return bytes.size(); }
|
||||
};
|
||||
struct Clock {
|
||||
uint32_t getCurrentTime() { return 100; }
|
||||
uint32_t getCurrentTimeUnique() { return 100; }
|
||||
};
|
||||
struct ACL {
|
||||
std::vector<ClientInfo> clients;
|
||||
int getNumClients() { return clients.size(); }
|
||||
ClientInfo* getClientByIdx(int index) { return &clients.at(index); }
|
||||
};
|
||||
class SensorMesh {
|
||||
public:
|
||||
SensorMesh();
|
||||
uint8_t reply_data[MAX_PACKET_PAYLOAD] = {};
|
||||
Telemetry telemetry;
|
||||
Clock clock;
|
||||
RegionMap region_map;
|
||||
RegionEntry* recv_pkt_region = nullptr;
|
||||
ACL acl;
|
||||
struct { uint8_t path_hash_mode = 0; } _prefs;
|
||||
bool refuse_allocation = false, refuse_queue = false;
|
||||
unsigned allocations = 0, attempts = 0, queued = 0;
|
||||
mesh::Packet owned_packet;
|
||||
Clock* getRTCClock() { return &clock; }
|
||||
bool telemHasChanged(ClientInfo*);
|
||||
void snapshotTelemetry(ClientInfo*);
|
||||
uint8_t handleRequest(ClientInfo*, uint32_t, uint8_t, uint8_t*, size_t,
|
||||
size_t = mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
void pushSubscriptions();
|
||||
// Replace only allocator/queue boundaries. The production change detector,
|
||||
// subscription parser, and complete subscription loop body execute below.
|
||||
mesh::Packet* createDatagram(uint8_t, int, const uint8_t*, const uint8_t*, size_t length) {
|
||||
++allocations;
|
||||
if (length > mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY) return nullptr;
|
||||
return refuse_allocation ? nullptr : &owned_packet;
|
||||
}
|
||||
bool sendDirect(mesh::Packet*, const uint8_t*, uint8_t, uint32_t) {
|
||||
++attempts;
|
||||
if (refuse_queue) return false;
|
||||
++queued;
|
||||
return true;
|
||||
}
|
||||
bool sendFloodScoped(const TransportKey&, mesh::Packet* p, uint32_t delay, uint8_t) {
|
||||
return sendDirect(p, nullptr, 0, delay);
|
||||
}
|
||||
};
|
||||
#include "production.inc"
|
||||
|
||||
static unsigned subscribe(SensorMesh& target, ClientInfo& client, uint8_t type,
|
||||
unsigned delta_size, unsigned payload_size = 255, uint32_t tag = 123) {
|
||||
uint8_t request[8 + 14] = {};
|
||||
const uint16_t timeout = 600;
|
||||
memcpy(request, &tag, 4);
|
||||
memcpy(request + 4, &timeout, 2);
|
||||
request[7] = 2 + delta_size;
|
||||
request[8] = 1;
|
||||
request[9] = type;
|
||||
const size_t length = payload_size == 255 ? 8 + 2 + delta_size : payload_size;
|
||||
return target.handleRequest(&client, 51, REQ_TYPE_SUBSCRIBE, request, length);
|
||||
}
|
||||
static void input_checks() {
|
||||
for (unsigned type = 0; type < 256; ++type) {
|
||||
if (LPPData::isScalarType(type)) continue;
|
||||
SensorMesh target;
|
||||
ClientInfo client;
|
||||
const ClientInfo original = client;
|
||||
const unsigned size = LPPData::getDataSize(type);
|
||||
assert(subscribe(target, client, type, size) == 8);
|
||||
assert(client.extra.sensor.min_deltas_len == 0);
|
||||
assert(!memcmp(&client.extra.sensor, &original.extra.sensor, sizeof(client.extra.sensor)));
|
||||
// Defensive detector must also reject unsupported records already in RAM.
|
||||
client.extra.sensor.min_deltas_len = 2 + size;
|
||||
client.extra.sensor.min_deltas[0] = 1;
|
||||
client.extra.sensor.min_deltas[1] = type;
|
||||
assert(!target.telemHasChanged(&client));
|
||||
}
|
||||
SensorMesh target;
|
||||
ClientInfo client;
|
||||
assert(subscribe(target, client, LPP_VOLTAGE, 2) == 12);
|
||||
const ClientInfo original = client;
|
||||
for (unsigned length = 0; length < 12; ++length) {
|
||||
assert(subscribe(target, client, LPP_VOLTAGE, 2, length, 456) == (length < 8 ? 0 : 8));
|
||||
assert(!memcmp(&client.extra.sensor, &original.extra.sensor, sizeof(client.extra.sensor)));
|
||||
}
|
||||
assert(subscribe(target, client, LPP_GPS, 9, 255, 456) == 8);
|
||||
assert(!memcmp(&client.extra.sensor, &original.extra.sensor, sizeof(client.extra.sensor)));
|
||||
}
|
||||
static void scalar_checks() {
|
||||
unsigned supported = 0;
|
||||
for (unsigned type = 0; type < 256; ++type) {
|
||||
if (!LPPData::isScalarType(type)) continue;
|
||||
++supported;
|
||||
const unsigned size = LPPData::getDataSize(type);
|
||||
assert(size >= 1 && size <= 4);
|
||||
assert(size == CayenneLPP::getTypeSize(type));
|
||||
assert(LPPData::getMultiplier(type) == CayenneLPP::getTypeMultiplier(type));
|
||||
assert(LPPData::isSigned(type) == CayenneLPP::getTypeSigned(type));
|
||||
SensorMesh target;
|
||||
ClientInfo client;
|
||||
assert(subscribe(target, client, type, size) == 12);
|
||||
assert(client.extra.sensor.min_deltas_len == 2 + size);
|
||||
}
|
||||
assert(supported == 22);
|
||||
assert(LPPData::getDataSize(LPP_RELATIVE_HUMIDITY) == 1);
|
||||
assert(LPPData::getMultiplier(LPP_RELATIVE_HUMIDITY) == 2);
|
||||
uint8_t humidity[] = {127};
|
||||
assert(LPPData::getFloat(humidity, 1, 2, false) == 63.5f);
|
||||
uint8_t encoded[4] = {};
|
||||
assert(LPPData::putFloat(encoded, 63.5f, 1, 2, false) == 1);
|
||||
assert(encoded[0] == 127);
|
||||
for (uint8_t type : {LPP_VOLTAGE, LPP_CURRENT}) {
|
||||
uint8_t buffer[4] = {};
|
||||
assert(LPPData::isSigned(type));
|
||||
const uint32_t multiplier = LPPData::getMultiplier(type);
|
||||
assert(LPPData::putFloat(buffer, -1.25f, 2, multiplier, true) == 2);
|
||||
assert(LPPData::getFloat(buffer, 2, multiplier, true) == -1.25f);
|
||||
LPPReader reader(buffer, 2);
|
||||
float value = 0;
|
||||
assert(type == LPP_VOLTAGE ? reader.readVoltage(value) : reader.readCurrent(value));
|
||||
assert(value == -1.25f);
|
||||
LPPWriter writer(buffer, sizeof(buffer));
|
||||
assert(type == LPP_VOLTAGE ? writer.writeVoltage(1, -1.25f) : writer.writeData(1, type, -1.25f));
|
||||
assert(writer.length() == 4);
|
||||
assert(buffer[0] == 1 && buffer[1] == type);
|
||||
assert(LPPData::getFloat(buffer + 2, 2, multiplier, true) == -1.25f);
|
||||
}
|
||||
SensorMesh target;
|
||||
ClientInfo client;
|
||||
assert(subscribe(target, client, LPP_RELATIVE_HUMIDITY, 1) == 12);
|
||||
target.telemetry.bytes = {1, LPP_RELATIVE_HUMIDITY, 127, 2, LPP_VOLTAGE, 1, 104};
|
||||
assert(target.telemHasChanged(&client));
|
||||
target.snapshotTelemetry(&client);
|
||||
assert(client.extra.sensor.prev_telem[2] == 127);
|
||||
assert(!target.telemHasChanged(&client));
|
||||
}
|
||||
static void admission_checks() {
|
||||
for (uint8_t stored_path : {0, 1, 0xFE, 0xFF}) {
|
||||
for (bool refuse_allocation : {false, true}) {
|
||||
SensorMesh target;
|
||||
target.acl.clients.resize(1);
|
||||
ClientInfo& client = target.acl.clients[0];
|
||||
client.out_path_len = stored_path;
|
||||
assert(subscribe(target, client, LPP_VOLTAGE, 2) == 12);
|
||||
target.telemetry.bytes = {1, LPP_VOLTAGE, 1, 104};
|
||||
target.refuse_allocation = refuse_allocation;
|
||||
target.refuse_queue = !refuse_allocation;
|
||||
target.pushSubscriptions();
|
||||
assert(target.queued == 0);
|
||||
assert(target.telemHasChanged(&client));
|
||||
assert(client.extra.sensor.prev_telem[2] == 0);
|
||||
assert(client.extra.sensor.prev_telem[3] == 0);
|
||||
target.refuse_allocation = target.refuse_queue = false;
|
||||
target.pushSubscriptions();
|
||||
assert(target.queued == 1);
|
||||
assert(client.extra.sensor.prev_telem[2] == 1);
|
||||
assert(client.extra.sensor.prev_telem[3] == 104);
|
||||
target.pushSubscriptions();
|
||||
assert(target.queued == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
static void writer_checks() {
|
||||
uint8_t buffer[16];
|
||||
memset(buffer, 0xAA, sizeof(buffer));
|
||||
LPPWriter writer(buffer, sizeof(buffer));
|
||||
for (float value : {std::numeric_limits<float>::quiet_NaN(),
|
||||
std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity()}) {
|
||||
assert(!writer.writeData(1, LPP_VOLTAGE, value));
|
||||
assert(writer.length() == 0);
|
||||
for (uint8_t byte : buffer) assert(byte == 0xAA);
|
||||
}
|
||||
for (uint8_t type : {LPP_GPS, LPP_ACCELEROMETER, LPP_GYROMETER, LPP_COLOUR, LPP_POLYLINE, 0xFF}) {
|
||||
assert(!writer.writeData(1, type, 1.0f));
|
||||
assert(writer.length() == 0);
|
||||
for (uint8_t byte : buffer) assert(byte == 0xAA);
|
||||
}
|
||||
assert(writer.writeData(1, LPP_RELATIVE_HUMIDITY, 63.5f));
|
||||
assert(writer.length() == 3);
|
||||
assert(buffer[0] == 1 && buffer[1] == LPP_RELATIVE_HUMIDITY && buffer[2] == 127);
|
||||
for (unsigned i = 3; i < sizeof(buffer); ++i) assert(buffer[i] == 0xAA);
|
||||
uint8_t short_buffer[2] = {0xAA, 0xAA};
|
||||
LPPWriter short_writer(short_buffer, sizeof(short_buffer));
|
||||
assert(!short_writer.writeData(1, LPP_VOLTAGE, 3.6f));
|
||||
assert(short_writer.length() == 0 && short_buffer[0] == 0xAA && short_buffer[1] == 0xAA);
|
||||
}
|
||||
static void capacity_checks() {
|
||||
SensorMesh target;
|
||||
assert(target.telemetry.capacity + 8 <= mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
while (target.telemetry.add(1, LPP_RELATIVE_HUMIDITY)) {}
|
||||
assert(size_t(target.telemetry.getSize()) + 8 <= mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY);
|
||||
target.acl.clients.resize(1);
|
||||
ClientInfo& client = target.acl.clients[0];
|
||||
assert(subscribe(target, client, LPP_VOLTAGE, 2) == 12);
|
||||
// Defense if an oversized collection nevertheless reaches the push loop.
|
||||
target.telemetry.bytes = {1, LPP_VOLTAGE, 1, 104};
|
||||
target.telemetry.bytes.resize(mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY - 7, 0);
|
||||
target.pushSubscriptions();
|
||||
assert(target.allocations == 0);
|
||||
assert(target.queued == 0);
|
||||
assert(target.telemHasChanged(&client));
|
||||
target.telemetry.bytes.resize(4);
|
||||
target.pushSubscriptions();
|
||||
assert(target.queued == 1);
|
||||
}
|
||||
static void conversion_checks() {
|
||||
uint8_t encoded[12] = {};
|
||||
for (uint8_t size = 1; size <= 4; ++size) {
|
||||
assert(LPPData::putFloat(encoded, -12.0f, size, 1, true) == size);
|
||||
assert(LPPData::getFloat(encoded, size, 1, true) == -12.0f);
|
||||
}
|
||||
for (uint8_t size = 1; size <= 4; ++size) {
|
||||
const uint32_t mask = size == 4 ? UINT32_MAX : (uint32_t(1) << (8 * size)) - 1;
|
||||
const uint32_t sign_bit = uint32_t(1) << (8 * size - 1);
|
||||
uint8_t expected[4] = {};
|
||||
uint32_t raw = sign_bit;
|
||||
for (unsigned i = size; i > 0; --i) { expected[i - 1] = raw & 255; raw >>= 8; }
|
||||
const float minimum = -(float)sign_bit;
|
||||
assert(LPPData::putFloat(encoded, minimum, size, 1, true) == size);
|
||||
assert(!memcmp(encoded, expected, size));
|
||||
assert(LPPData::getFloat(encoded, size, 1, true) == minimum);
|
||||
raw = sign_bit - 1;
|
||||
for (unsigned i = size; i > 0; --i) { expected[i - 1] = raw & 255; raw >>= 8; }
|
||||
const float maximum = (float)(sign_bit - 1);
|
||||
assert(LPPData::putFloat(encoded, maximum, size, 1, true) == size);
|
||||
assert(!memcmp(encoded, expected, size));
|
||||
assert(LPPData::putFloat(encoded, maximum * 2, size, 1, true) == size);
|
||||
assert(!memcmp(encoded, expected, size));
|
||||
assert(LPPData::putFloat(encoded, minimum * 2, size, 1, true) == size);
|
||||
assert(encoded[0] == 128);
|
||||
for (unsigned i = 1; i < size; ++i) assert(encoded[i] == 0);
|
||||
raw = mask;
|
||||
for (unsigned i = size; i > 0; --i) { expected[i - 1] = raw & 255; raw >>= 8; }
|
||||
assert(LPPData::putFloat(encoded, (float)mask, size, 1, false) == size);
|
||||
assert(!memcmp(encoded, expected, size));
|
||||
assert(LPPData::putFloat(encoded, (float)mask * 2, size, 1, false) == size);
|
||||
assert(!memcmp(encoded, expected, size));
|
||||
assert(LPPData::putFloat(encoded, -1.0f, size, 1, false) == size);
|
||||
for (unsigned i = 0; i < size; ++i) assert(encoded[i] == 0);
|
||||
}
|
||||
uint8_t all_ones[] = {255, 255, 255, 255};
|
||||
float rounded = LPPData::getFloat(all_ones, 4, 1, false);
|
||||
assert(LPPData::putFloat(encoded, rounded, 4, 1, false) == 4);
|
||||
assert(!memcmp(encoded, all_ones, 4));
|
||||
for (float value : {std::numeric_limits<float>::quiet_NaN(),
|
||||
std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity()}) {
|
||||
memset(encoded, 0xAA, sizeof(encoded));
|
||||
assert(LPPData::putFloat(encoded, value, 4, 1, false) == 0);
|
||||
for (uint8_t byte : encoded) assert(byte == 0xAA);
|
||||
}
|
||||
for (uint8_t size : {0, 5, 6, 9}) {
|
||||
memset(encoded, 0xAA, sizeof(encoded));
|
||||
assert(LPPData::putFloat(encoded, 12.0f, size, 1, true) == 0);
|
||||
assert(encoded[0] == 0xAA);
|
||||
assert(LPPData::getFloat(encoded, size, 1, true) == 0.0f);
|
||||
}
|
||||
assert(LPPData::putFloat(encoded, 12.0f, 1, 0, true) == 0);
|
||||
assert(LPPData::getFloat(encoded, 1, 0, true) == 0.0f);
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
assert(argc == 2);
|
||||
if (!strcmp(argv[1], "scalar")) scalar_checks();
|
||||
else if (!strcmp(argv[1], "input")) input_checks();
|
||||
else if (!strcmp(argv[1], "admission")) admission_checks();
|
||||
else if (!strcmp(argv[1], "conversion")) conversion_checks();
|
||||
else if (!strcmp(argv[1], "capacity")) capacity_checks();
|
||||
else if (!strcmp(argv[1], "writer")) writer_checks();
|
||||
else assert(false);
|
||||
std::cout << "Sensor subscription checks passed\n";
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run actual Sensor response handlers, history aggregation and packet builders."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/sensor_reply_bounds/test_sensor_reply_bounds.cpp"
|
||||
|
||||
|
||||
def production_methods(transform=None, room_transform=None):
|
||||
source = (ROOT / "examples/simple_sensor/SensorMesh.cpp").read_text()
|
||||
if transform:
|
||||
source = transform(source)
|
||||
handler = extract_braced(source, "uint8_t SensorMesh::handleRequest(")
|
||||
begin = handler.index(" if (req_type == REQ_TYPE_GET_TELEMETRY_DATA")
|
||||
generated = extract_braced(source, "static uint8_t getTelemetryPermissions(") + "\n"
|
||||
if "static size_t boundedTelemPrefix(" in source:
|
||||
generated += extract_braced(source, "static size_t boundedTelemPrefix(") + "\n"
|
||||
generated += handler[:begin]
|
||||
for signature in ("if (req_type == REQ_TYPE_GET_TELEMETRY_DATA", "if (req_type == REQ_TYPE_GET_AVG_MIN_MAX"):
|
||||
generated += extract_braced(handler, signature) + "\n"
|
||||
generated += "return 0;\n}\n"
|
||||
room = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text()
|
||||
if room_transform:
|
||||
room = room_transform(room)
|
||||
room_handler = extract_braced(room, "int MyMesh::handleRequest(")
|
||||
room_begin = room_handler.index(" if (payload[0] == REQ_TYPE_GET_STATUS)")
|
||||
generated += "#undef REQ_TYPE_GET_TELEMETRY_DATA\n#define REQ_TYPE_GET_TELEMETRY_DATA ROOM_REQ_TYPE_GET_TELEMETRY_DATA\n"
|
||||
generated += room_handler[:room_begin]
|
||||
generated += extract_braced(room_handler, "if (payload[0] == REQ_TYPE_GET_TELEMETRY_DATA)") + "\nreturn 0;\n}\n"
|
||||
generated += "#undef REQ_TYPE_GET_TELEMETRY_DATA\n#define REQ_TYPE_GET_TELEMETRY_DATA SENSOR_REQ_TYPE_GET_TELEMETRY_DATA\n"
|
||||
history = (ROOT / "examples/simple_sensor/TimeSeriesData.cpp").read_text()
|
||||
generated += extract_braced(history, "void TimeSeriesData::calcMinMaxAvg(") + "\n"
|
||||
mesh = (ROOT / "src/Mesh.cpp").read_text()
|
||||
generated += "namespace mesh {\n#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE)\n"
|
||||
for signature in ("Packet* Mesh::createPathReturn(const Identity& dest,",
|
||||
"Packet* Mesh::createPathReturn(const uint8_t* dest_hash,", "Packet* Mesh::createDatagram("):
|
||||
generated += extract_braced(mesh, signature) + "\n"
|
||||
packet = (ROOT / "src/Packet.cpp").read_text()
|
||||
for signature in ("Packet::Packet()", "bool Packet::isValidPathLen("):
|
||||
generated += extract_braced(packet, signature) + "\n"
|
||||
return generated + "}\n"
|
||||
|
||||
|
||||
def production_capacities(sensor_transform=None, room_transform=None):
|
||||
definitions = []
|
||||
for role, path, transform in (
|
||||
("SENSOR", "examples/simple_sensor/SensorMesh.cpp", sensor_transform),
|
||||
("ROOM", "examples/simple_room_server/MyMesh.cpp", room_transform),
|
||||
):
|
||||
source = (ROOT / path).read_text()
|
||||
if transform:
|
||||
source = transform(source)
|
||||
matches = re.findall(r"\btelemetry\(([^)\n]+)\)", source)
|
||||
if len(matches) != 1:
|
||||
raise AssertionError("one actual telemetry constructor required: " + path)
|
||||
definitions.append("#define " + role + "_TELEMETRY_CAPACITY (" + matches[0] + ")")
|
||||
opcode = re.search(r"#define REQ_TYPE_GET_TELEMETRY_DATA\s+(0x[0-9a-fA-F]+)", source).group(1)
|
||||
definitions.append("#define " + role + "_REQ_TYPE_GET_TELEMETRY_DATA " + opcode)
|
||||
return "\n".join(definitions) + "\n"
|
||||
|
||||
|
||||
class SensorReplyBoundsTest(unittest.TestCase):
|
||||
def execute(self, case, transform=None, room_transform=None):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "host C++17 compiler required")
|
||||
with tempfile.TemporaryDirectory(prefix="sensor-reply-bounds-") as directory:
|
||||
work = Path(directory)
|
||||
(work / "telemetry_capacity.inc").write_text(production_capacities(transform, room_transform), encoding="ascii")
|
||||
(work / "production.inc").write_text(production_methods(transform, room_transform), encoding="ascii")
|
||||
history_types = (ROOT / "examples/simple_sensor/TimeSeriesData.h").read_text()
|
||||
history_types = "\n".join(line for line in history_types.splitlines() if not line.startswith("#"))
|
||||
(work / "history_types.inc").write_text(history_types, encoding="ascii")
|
||||
binary = work / "sensor.exe"
|
||||
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-unused-parameter", "-Wno-reorder",
|
||||
"-I" + str(work), "-I" + str(ROOT / "src"), str(FIXTURE), "-o", str(binary)]
|
||||
if sys.platform.startswith("linux"):
|
||||
command[1:1] = ["-fsanitize=address,undefined,float-cast-overflow", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"]
|
||||
built = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
return subprocess.run([str(binary), case], capture_output=True, text=True, timeout=30)
|
||||
|
||||
def check_case(self, case):
|
||||
run = self.execute(case)
|
||||
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
||||
self.assertIn("Sensor response bounds and packet admission passed", run.stdout)
|
||||
|
||||
def test_room_telemetry_entries_fit_all_routes_and_preserve_composites(self):
|
||||
self.check_case("room")
|
||||
|
||||
def test_old_room_unbounded_copy_is_rejected(self):
|
||||
def old(source):
|
||||
begin = source.index(" const uint8_t* tbuf = telemetry.getBuffer();")
|
||||
end = source.index(" return 4 + tlen; // reply_len", begin) + len(" return 4 + tlen; // reply_len")
|
||||
return source[:begin] + """ uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
return 4 + tlen; // reply_len""" + source[end:]
|
||||
self.assertNotEqual(self.execute("room", room_transform=old).returncode, 0)
|
||||
|
||||
def test_telemetry_entries_fit_every_valid_direct_and_flood_route(self):
|
||||
self.check_case("telemetry")
|
||||
|
||||
def test_history_entries_types_counts_and_empty_query_are_safe(self):
|
||||
self.check_case("history")
|
||||
|
||||
def test_packet_builder_accepts_maximum_telemetry_collection(self):
|
||||
self.check_case("packet")
|
||||
|
||||
def test_empty_history_query_has_no_synthetic_zero_values(self):
|
||||
self.check_case("empty")
|
||||
|
||||
def test_old_telemetry_copy_exceeds_route_admission(self):
|
||||
def old(source):
|
||||
begin = source.index(" const uint8_t* tbuf = telemetry.getBuffer();")
|
||||
end = source.index(" return 4 + tlen; // reply_len", begin) + len(" return 4 + tlen; // reply_len")
|
||||
return source[:begin] + """ uint8_t tlen = telemetry.getSize();
|
||||
memcpy(&reply_data[4], telemetry.getBuffer(), tlen);
|
||||
return 4 + tlen; // reply_len""" + source[end:]
|
||||
self.assertNotEqual(self.execute("telemetry", old).returncode, 0)
|
||||
|
||||
def test_old_history_route_bounds_are_rejected(self):
|
||||
def old(source):
|
||||
return source.replace(" if (item_len > reply_capacity - ofs) break;", " (void)item_len;", 1)
|
||||
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
||||
|
||||
def test_old_history_composite_types_are_rejected(self):
|
||||
def old(source):
|
||||
return source.replace("!LPPData::isScalarType(d->_lpp_type)\n || ", "", 1)
|
||||
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
||||
|
||||
def test_old_history_provider_count_is_rejected(self):
|
||||
def old(source):
|
||||
return source.replace(" if (n < 0 || n > int(sizeof(data) / sizeof(data[0]))) return 0;", "", 1)
|
||||
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
||||
|
||||
def test_old_empty_history_nonfinite_conversion_is_rejected(self):
|
||||
def old(source):
|
||||
return source.replace("\n || !isfinite(d->_min) || !isfinite(d->_max) || !isfinite(d->_avg)", "", 1)
|
||||
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
||||
|
||||
def test_old_history_short_response_header_is_rejected(self):
|
||||
def old(source):
|
||||
return source.replace(" if (reply_capacity < 8) return 0; // tag and current timestamp", "", 1)
|
||||
self.assertNotEqual(self.execute("history", old).returncode, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute Sensor subscription parsing and push admission with real LPP helpers."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / 'examples/simple_sensor/SensorMesh.cpp'
|
||||
HELPER = ROOT / 'src/helpers/sensors/LPPDataHelpers.h'
|
||||
FIXTURE = ROOT / 'test/fixtures/sensor_subscriptions/test_sensor_subscriptions.cpp'
|
||||
|
||||
|
||||
def production_methods(source):
|
||||
methods = extract_braced(source, 'static float findTelemValue(') + '\n'
|
||||
methods += extract_braced(source, 'bool SensorMesh::telemHasChanged(') + '\n'
|
||||
if 'void SensorMesh::snapshotTelemetry(' in source:
|
||||
methods += extract_braced(source, 'void SensorMesh::snapshotTelemetry(') + '\n'
|
||||
handler = extract_braced(source, 'uint8_t SensorMesh::handleRequest(')
|
||||
prefix = handler[:handler.index(' if (req_type == REQ_TYPE_GET_TELEMETRY_DATA')]
|
||||
methods += prefix + extract_braced(handler, 'if (req_type == REQ_TYPE_SUBSCRIBE')
|
||||
methods += '\nreturn 0;\n}\n'
|
||||
loop = source[source.index(' uint32_t curr = getRTCClock()->getCurrentTime();', source.index('void SensorMesh::loop()')):]
|
||||
constructor = extract_braced(source, 'SensorMesh::SensorMesh(')
|
||||
capacity = re.search(r'telemetry\(([^)]+)\)', constructor).group(1)
|
||||
methods += 'SensorMesh::SensorMesh() : telemetry(' + capacity + ') {}\n'
|
||||
methods += 'void SensorMesh::pushSubscriptions() { uint32_t curr = 100;\n'
|
||||
methods += extract_braced(loop, 'for (int i = 0; i < acl.getNumClients(); i++)') + '\n}\n'
|
||||
packet = (ROOT / 'src/Packet.cpp').read_text()
|
||||
methods += 'namespace mesh {\n' + extract_braced(packet, 'Packet::Packet()') + '\n'
|
||||
methods += extract_braced(packet, 'bool Packet::isValidPathLen(') + '\n}\n'
|
||||
return methods
|
||||
|
||||
|
||||
class SensorSubscriptionTest(unittest.TestCase):
|
||||
def execute(self, case, transform=None, helper_transform=None):
|
||||
compiler = shutil.which('g++') or shutil.which('clang++')
|
||||
self.assertIsNotNone(compiler, 'host C++17 compiler required')
|
||||
source = SOURCE.read_text()
|
||||
helper = HELPER.read_text()
|
||||
if transform:
|
||||
source = transform(source)
|
||||
if helper_transform:
|
||||
helper = helper_transform(helper)
|
||||
with tempfile.TemporaryDirectory(prefix='sensor-subscription-') as directory:
|
||||
work = Path(directory)
|
||||
(work / 'production.inc').write_text(production_methods(source), encoding='ascii')
|
||||
(work / 'LPPDataHelpers.h').write_text(helper, encoding='ascii')
|
||||
binary = work / 'test'
|
||||
args = [compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror', '-Wno-unused-parameter',
|
||||
'-I' + str(work), '-I' + str(ROOT / 'src'), str(FIXTURE), '-o', str(binary)]
|
||||
if sys.platform.startswith('linux'):
|
||||
args[1:1] = ['-fsanitize=address,undefined,float-cast-overflow', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie']
|
||||
built = subprocess.run(args, capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
return subprocess.run([str(binary), case], capture_output=True, text=True, timeout=30)
|
||||
|
||||
def passed(self, case):
|
||||
run = self.execute(case)
|
||||
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
||||
self.assertIn('Sensor subscription checks passed', run.stdout)
|
||||
|
||||
def test_supported_scalars_and_humidity_wire_encoding(self):
|
||||
self.passed('scalar')
|
||||
|
||||
def test_unsupported_types_truncation_and_existing_subscription_preserved(self):
|
||||
self.passed('input')
|
||||
|
||||
def test_allocation_and_queue_failures_keep_delta_available_for_retry(self):
|
||||
self.passed('admission')
|
||||
|
||||
def test_subscription_collection_and_push_fit_host_packet_budget(self):
|
||||
self.passed('capacity')
|
||||
|
||||
def test_scalar_writer_rejections_leave_buffer_and_length_untouched(self):
|
||||
self.passed('writer')
|
||||
|
||||
def test_scalar_width_sign_nonfinite_and_uint32_round_trip(self):
|
||||
self.passed('conversion')
|
||||
|
||||
def test_old_acceptance_of_vector_delta_records_is_rejected(self):
|
||||
def old(source):
|
||||
guard = 'LPPData::isScalarType(type)\n && item_len <= min_deltas_len - i'
|
||||
self.assertIn(guard, source)
|
||||
return source.replace(guard, 'item_len <= min_deltas_len - i', 1)
|
||||
self.assertNotEqual(self.execute('input', old).returncode, 0)
|
||||
|
||||
def test_old_invalid_request_overwrites_existing_push_tag(self):
|
||||
def old(source):
|
||||
marker = ' if (r && valid_deltas) {\n memcpy(&from->extra.sensor.push_tag, &payload[0], 4);'
|
||||
self.assertIn(marker, source)
|
||||
source = source.replace(marker, ' if (r && valid_deltas) {', 1)
|
||||
marker = ' uint16_t timeout_secs;'
|
||||
return source.replace(marker, ' memcpy(&from->extra.sensor.push_tag, &payload[0], 4);\n' + marker, 1)
|
||||
self.assertNotEqual(self.execute('input', old).returncode, 0)
|
||||
|
||||
def test_old_early_threshold_snapshot_is_rejected(self):
|
||||
def old(source):
|
||||
begin = source.index('bool SensorMesh::telemHasChanged(')
|
||||
end = source.index('void SensorMesh::snapshotTelemetry(', begin)
|
||||
before = source[begin:end]
|
||||
self.assertIn(' return changed;', before)
|
||||
altered = before.replace(' return changed;', ' if (changed) snapshotTelemetry(c);\n return changed;', 1)
|
||||
return source[:begin] + altered + source[end:]
|
||||
self.assertNotEqual(self.execute('admission', old).returncode, 0)
|
||||
|
||||
def test_old_humidity_width_and_multiplier_are_rejected(self):
|
||||
def width(helper):
|
||||
marker = ' case LPP_ALTITUDE:\n case LPP_VOLTAGE:'
|
||||
self.assertIn(marker, helper)
|
||||
return helper.replace(marker, ' case LPP_RELATIVE_HUMIDITY:\n' + marker, 1)
|
||||
def multiplier(helper):
|
||||
marker = ' case LPP_RELATIVE_HUMIDITY:\n return 2;'
|
||||
self.assertIn(marker, helper)
|
||||
return helper.replace(marker, ' case LPP_RELATIVE_HUMIDITY:\n return 10;', 1)
|
||||
for old in (width, multiplier):
|
||||
with self.subTest(old=old.__name__):
|
||||
self.assertNotEqual(self.execute('scalar', helper_transform=old).returncode, 0)
|
||||
|
||||
def test_old_unbounded_subscription_collection_is_rejected(self):
|
||||
def old(source):
|
||||
marker = ' telemetry(mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY - 8)'
|
||||
self.assertIn(marker, source)
|
||||
return source.replace(marker, ' telemetry(MAX_PACKET_PAYLOAD - 4)', 1)
|
||||
self.assertNotEqual(self.execute('capacity', old).returncode, 0)
|
||||
|
||||
def test_old_push_allows_oversized_response_attempt(self):
|
||||
def old(source):
|
||||
marker = ' if (telemetry_len > mesh::CLIENT_ACL_DIRECT_REPLY_CAPACITY - 8) continue;'
|
||||
self.assertIn(marker, source)
|
||||
return source.replace(marker, ' if (telemetry_len > sizeof(reply_data) - 8) continue;', 1)
|
||||
self.assertNotEqual(self.execute('capacity', old).returncode, 0)
|
||||
|
||||
def test_old_signed_four_byte_mask_is_rejected_by_sanitizer(self):
|
||||
def old(helper):
|
||||
marker = 'size == sizeof(uint32_t)\n ? UINT32_MAX : (uint32_t(1) << (size * 8)) - 1'
|
||||
self.assertIn(marker, helper)
|
||||
return helper.replace(marker, '(1 << (size * 8)) - 1', 1)
|
||||
self.assertNotEqual(self.execute('conversion', helper_transform=old).returncode, 0)
|
||||
|
||||
def test_old_writer_commits_incomplete_header_on_encoding_failure(self):
|
||||
def old(helper):
|
||||
begin = helper.index(' if (LPPData::isScalarType(type)', helper.index('bool writeData('))
|
||||
end = helper.index(' return false;', begin)
|
||||
original = ''' if (_len + 2 + sz <= _max_len) {
|
||||
_buf[_len++] = channel;
|
||||
_buf[_len++] = type;
|
||||
_len += LPPData::putFloat(&_buf[_len], v, sz, mul, s);
|
||||
return true;
|
||||
}
|
||||
'''
|
||||
return helper[:begin] + original + helper[end:]
|
||||
self.assertNotEqual(self.execute('writer', helper_transform=old).returncode, 0)
|
||||
|
||||
def test_old_unguarded_float_cast_is_rejected_by_sanitizer(self):
|
||||
def old(helper):
|
||||
begin = helper.index(' if (size == 0', helper.index('static uint8_t putFloat('))
|
||||
end = helper.index(' // add bytes (MSB first)', begin)
|
||||
original = ''' bool sign = value < 0;
|
||||
if (sign) value = -value;
|
||||
uint32_t v = value * multiplier;
|
||||
if (is_signed && sign) {
|
||||
const uint32_t mask = size == 4 ? UINT32_MAX : (uint32_t(1) << (size * 8)) - 1;
|
||||
v = (uint32_t(0) - v) & mask;
|
||||
}
|
||||
'''
|
||||
return helper[:begin] + original + helper[end:]
|
||||
self.assertNotEqual(self.execute('conversion', helper_transform=old).returncode, 0)
|
||||
|
||||
def test_header_and_ci_wiring(self):
|
||||
self.assertIn(' electroniccats/CayenneLPP @ 1.6.1', (ROOT / 'platformio.ini').read_text())
|
||||
self.assertIn(' void snapshotTelemetry(ClientInfo* c);',
|
||||
(ROOT / 'examples/simple_sensor/SensorMesh.h').read_text())
|
||||
self.assertIn(' python3 -B test/test_sensor_subscriptions.py -v\n',
|
||||
(ROOT / '.github/workflows/run-unit-tests.yml').read_text())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user