Preserve exact LPP bytes in Sensor telemetry snapshots

This commit is contained in:
mikecarper committed 2026-10-16 04:40:44 -07:00
1 parent dcfa31bb50
commit fc91b6e75c
3 files changed
+109 -7

No files matched your search

+14 -6
View File
@@ -107,7 +107,7 @@ static File openAppend(FILESYSTEM* _fs, const char* fname) {
/* --------------------- Cayenne LPP helpers ----------------------------*/
static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) {
static const uint8_t* findTelemData(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type) {
uint8_t i = 0;
while (i + 2 < size) {
@@ -119,11 +119,17 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u
if (sz > size - i) break;
if (ch == channel && t == type) {
return LPPData::getFloat(&buf[i], sz, LPPData::getMultiplier(t), LPPData::isSigned(t));
return &buf[i];
}
i += sz; // skip
}
return def_value; // not found
return nullptr; // not found
}
static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, uint8_t type, float def_value) {
const uint8_t* data = findTelemData(buf, size, channel, type);
return data ? LPPData::getFloat(data, LPPData::getDataSize(type),
LPPData::getMultiplier(type), LPPData::isSigned(type)) : def_value;
}
/* ------------------ end Cayenne LPP helpers ----------------------*/
@@ -166,9 +172,11 @@ void SensorMesh::snapshotTelemetry(ClientInfo* c) {
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));
// Retain exactly what was delivered; a float round trip can lose one LSB
// and repeatedly report the same unchanged reading as a new delta.
const uint8_t* value = findTelemData(buf, size, ch, t);
if (value) memcpy(&c->extra.sensor.prev_telem[i + 2], value, sz);
else memset(&c->extra.sensor.prev_telem[i + 2], 0, sz);
i += 2 + sz;
}
}
@@ -247,6 +247,82 @@ static void tracker_mode_checks() {
assert(target.allocations == 2 && target.queued == 1);
}
}
static void snapshot_checks() {
for (uint8_t stored_path : {0, 0xFF}) {
for (unsigned refusal = 0; refusal < 3; ++refusal) {
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, 164}; // Delivered 4.20 V.
target.refuse_allocation = refusal == 1;
target.refuse_queue = refusal == 2;
target.pushSubscriptions();
if (refusal) {
assert(!target.queued && client.extra.sensor.prev_telem[2] == 0);
assert(client.extra.sensor.prev_telem[3] == 0 && target.telemHasChanged(&client));
target.refuse_allocation = target.refuse_queue = false;
target.pushSubscriptions();
}
assert(!memcmp(client.extra.sensor.prev_telem, target.telemetry.bytes.data(), 4)
&& "snapshot must preserve the delivered LPP bytes");
assert(target.queued == 1 && !target.telemHasChanged(&client));
for (unsigned repeat = 0; repeat < 10; ++repeat) target.pushSubscriptions();
assert(target.queued == 1 && !target.telemHasChanged(&client));
}
}
uint32_t samples = 0;
for (unsigned type = 0; type < 256; ++type) {
if (!LPPData::isScalarType(type)) continue;
SensorMesh target;
ClientInfo client;
const uint8_t size = LPPData::getDataSize(type);
assert(subscribe(target, client, type, size) == 12);
// Exhaust every one-/two-byte encoding, including signed/scaled data.
// Four-byte samples straddle float's exact-integer range and sign limits.
const std::vector<uint32_t> wide = {0, 1, 0xffff, 0x10000, 0xffffff,
0x1000000, 0x1000001, 0x7fffffff, 0x80000000, 0xfffffffe, 0xffffffff};
const uint32_t count = size < 4 ? uint32_t(1) << (8 * size) : wide.size();
target.telemetry.bytes.resize(2 + size);
target.telemetry.bytes[0] = 1;
target.telemetry.bytes[1] = type;
for (uint32_t index = 0; index < count; ++index) {
uint32_t raw = size < 4 ? index : wide[index];
for (unsigned byte = size; byte > 0; --byte) {
target.telemetry.bytes[1 + byte] = uint8_t(raw);
raw >>= 8;
}
memset(client.extra.sensor.prev_telem, 0xa5, sizeof(client.extra.sensor.prev_telem));
target.snapshotTelemetry(&client);
assert(!memcmp(client.extra.sensor.prev_telem, target.telemetry.bytes.data(), 2 + size)
&& "snapshot must preserve the delivered LPP bytes");
assert(!target.telemHasChanged(&client));
++samples;
}
}
SensorMesh target;
ClientInfo client;
assert(subscribe(target, client, LPP_VOLTAGE, 2) == 12);
target.telemetry.bytes = {2, LPP_RELATIVE_HUMIDITY, 127, 1, LPP_VOLTAGE, 1, 164,
1, LPP_VOLTAGE, 1, 194};
target.snapshotTelemetry(&client);
assert(client.extra.sensor.prev_telem[2] == 1 && client.extra.sensor.prev_telem[3] == 164);
assert(findTelemValue(target.telemetry.getBuffer(), target.telemetry.getSize(),
1, LPP_VOLTAGE, -99) == LPPData::getFloat(target.telemetry.getBuffer() + 5, 2, 100, true));
// Missing/truncated records keep the existing zero baseline and lookup default.
const auto original = target.telemetry.bytes;
for (unsigned length = 0; length <= 6; ++length) {
target.telemetry.bytes.assign(original.begin(), original.begin() + length);
memset(client.extra.sensor.prev_telem, 0xa5, sizeof(client.extra.sensor.prev_telem));
target.snapshotTelemetry(&client);
assert(client.extra.sensor.prev_telem[0] == 1 && client.extra.sensor.prev_telem[1] == LPP_VOLTAGE);
assert(client.extra.sensor.prev_telem[2] == 0 && client.extra.sensor.prev_telem[3] == 0);
assert(findTelemValue(target.telemetry.getBuffer(), target.telemetry.getSize(),
1, LPP_VOLTAGE, -99) == -99);
}
std::cout << "Raw LPP snapshot samples checked: " << samples << '\n';
}
static void writer_checks() {
uint8_t buffer[16];
memset(buffer, 0xAA, sizeof(buffer));
@@ -355,6 +431,7 @@ int main(int argc, char** argv) {
else if (!strcmp(argv[1], "capacity")) capacity_checks();
else if (!strcmp(argv[1], "writer")) writer_checks();
else if (!strcmp(argv[1], "tracker")) tracker_mode_checks();
else if (!strcmp(argv[1], "snapshot")) snapshot_checks();
else assert(false);
std::cout << "Sensor subscription checks passed\n";
}
+18 -1
View File
@@ -17,7 +17,8 @@ FIXTURE = ROOT / 'test/fixtures/sensor_subscriptions/test_sensor_subscriptions.c
def production_methods(source):
methods = extract_braced(source, 'static float findTelemValue(') + '\n'
methods = extract_braced(source, 'static const uint8_t* findTelemData(') + '\n'
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'
@@ -74,6 +75,22 @@ class SensorSubscriptionTest(unittest.TestCase):
def test_allocation_and_queue_failures_keep_delta_available_for_retry(self):
self.passed('admission')
def test_raw_snapshots_preserve_scalars_and_unchanged_pushes_stop(self):
self.passed('snapshot')
def test_lossy_float_snapshot_negative_control(self):
def old(source):
current = ''' const uint8_t* value = findTelemData(buf, size, ch, t);
if (value) memcpy(&c->extra.sensor.prev_telem[i + 2], value, sz);
else memset(&c->extra.sensor.prev_telem[i + 2], 0, sz);'''
self.assertEqual(source.count(current), 1)
return source.replace(current, ''' 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));''', 1)
result = self.execute('snapshot', old)
self.assertNotEqual(result.returncode, 0, 'lossy snapshot escaped regression')
self.assertIn('snapshot must preserve the delivered LPP bytes', result.stderr)
def test_subscription_collection_and_push_fit_host_packet_budget(self):
self.passed('capacity')