Bound telemetry replies and preserve Sensor send retries

This commit is contained in:
mikecarper committed 2026-10-06 09:56:00 -07:00
1 parent 5c1f1477f4
commit 58f5d7f794
11 files changed
+1522 -66

No files matched your search

+2
View File
@@ -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: |
+4 -2
View File
@@ -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()) {
+45 -33
View File
@@ -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], &timestamp, 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);
}
}
+1
View File
@@ -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,
+82 -31
View File
@@ -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";
}
+19
View File
@@ -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 &region; }
RegionEntry* findById(uint16_t id) { return id == region.id ? &region : 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";
}
+156
View File
@@ -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()
+184
View File
@@ -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()