diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 1ef554e44..fdafb27cd 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -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; } } diff --git a/test/fixtures/sensor_subscriptions/test_sensor_subscriptions.cpp b/test/fixtures/sensor_subscriptions/test_sensor_subscriptions.cpp index 50f1a9109..ff4c54764 100644 --- a/test/fixtures/sensor_subscriptions/test_sensor_subscriptions.cpp +++ b/test/fixtures/sensor_subscriptions/test_sensor_subscriptions.cpp @@ -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 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"; } diff --git a/test/test_sensor_subscriptions.py b/test/test_sensor_subscriptions.py index 787c813a6..317703600 100644 --- a/test/test_sensor_subscriptions.py +++ b/test/test_sensor_subscriptions.py @@ -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')