Compare Sensor subscription deltas in exact LPP units

This commit is contained in:
mikecarper committed 2026-10-16 05:03:22 -07:00
1 parent 0d0a0f187e
commit 422b6f3ff2
3 files changed
+183 -5

No files matched your search

+18 -5
View File
@@ -132,6 +132,16 @@ static float findTelemValue(const uint8_t* buf, uint8_t size, uint8_t channel, u
LPPData::getMultiplier(type), LPPData::isSigned(type)) : def_value;
}
static int64_t readTelemRawValue(const uint8_t* data, uint8_t size, bool is_signed) {
if (!data || size == 0 || size > sizeof(uint32_t)) return 0;
int64_t value = 0;
for (uint8_t i = 0; i < size; ++i) value = (value << 8) | data[i];
if (is_signed && (value & (int64_t(1) << (size * 8 - 1)))) {
value -= int64_t(1) << (size * 8);
}
return value;
}
/* ------------------ end Cayenne LPP helpers ----------------------*/
bool SensorMesh::telemHasChanged(ClientInfo* c) {
@@ -147,11 +157,14 @@ bool SensorMesh::telemHasChanged(ClientInfo* c) {
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));
float v = findTelemValue(buf, size, ch, t, 0.0f);
if (fabsf(v - pv) > min_delta) changed = true;
const bool is_signed = LPPData::isSigned(t);
const int64_t min_delta = readTelemRawValue(&c->extra.sensor.min_deltas[i + 2], sz, is_signed);
const int64_t pv = readTelemRawValue(&c->extra.sensor.prev_telem[i + 2], sz, is_signed);
const int64_t v = readTelemRawValue(findTelemData(buf, size, ch, t), sz, is_signed);
// The common LPP multiplier cancels. Compare raw units so float rounding
// cannot hide a one-LSB change or push an equal delta over the threshold.
const int64_t difference = v >= pv ? v - pv : pv - v;
if (difference > min_delta) changed = true;
i += 2 + sz; // skip
}
@@ -323,6 +323,133 @@ static void snapshot_checks() {
}
std::cout << "Raw LPP snapshot samples checked: " << samples << '\n';
}
static void store_raw(uint8_t* data, uint8_t size, int64_t value) {
uint64_t raw = uint64_t(value);
for (unsigned i = size; i > 0; --i) {
data[i - 1] = uint8_t(raw);
raw >>= 8;
}
}
static void expect_delta(uint8_t type, int64_t previous, int64_t current,
int64_t threshold, bool expected) {
SensorMesh target;
ClientInfo client;
const uint8_t size = LPPData::getDataSize(type);
assert(subscribe(target, client, type, size) == 12);
store_raw(client.extra.sensor.min_deltas + 2, size, threshold);
target.telemetry.bytes.resize(2 + size);
target.telemetry.bytes[0] = 1;
target.telemetry.bytes[1] = type;
store_raw(target.telemetry.getBuffer() + 2, size, previous);
target.snapshotTelemetry(&client);
const auto baseline = target.telemetry.bytes;
store_raw(target.telemetry.getBuffer() + 2, size, current);
for (unsigned repeat = 0; repeat < 2; ++repeat) {
assert(target.telemHasChanged(&client) == expected
&& "raw LPP delta must use the exact strict threshold");
assert(!memcmp(client.extra.sensor.prev_telem, baseline.data(), baseline.size()));
}
}
static void delta_checks() {
for (unsigned type = 0; type < 256; ++type) {
if (!LPPData::isScalarType(type)) continue;
// Equal, smaller, and larger positive thresholds in both directions.
for (int64_t step : {0, 1, 2, 3}) {
expect_delta(type, 10, 10 + step, 2, step > 2);
expect_delta(type, 10 + step, 10, 2, step > 2);
expect_delta(type, 10, 10 + step, 0, step != 0);
}
const uint8_t size = LPPData::getDataSize(type);
if (LPPData::isSigned(type)) {
const int64_t minimum = -(int64_t(1) << (8 * size - 1));
const int64_t maximum = -minimum - 1;
expect_delta(type, minimum, maximum, maximum, true);
expect_delta(type, maximum, minimum, maximum, true);
expect_delta(type, minimum, minimum + 1, 1, false);
expect_delta(type, -2, 1, 3, false);
expect_delta(type, 1, -2, 2, true);
// Negative signed thresholds retain their existing always-trigger behavior.
expect_delta(type, -2, -2, -1, true);
expect_delta(type, 0, 0, minimum, true);
}
SensorMesh target;
ClientInfo client;
assert(subscribe(target, client, type, size) == 12);
target.telemetry.bytes.resize(2 + size);
target.telemetry.bytes[0] = 1;
target.telemetry.bytes[1] = type;
store_raw(target.telemetry.getBuffer() + 2, size, 10);
target.snapshotTelemetry(&client);
store_raw(client.extra.sensor.min_deltas + 2, size, 10);
const auto complete = target.telemetry.bytes;
// Each truncated current record continues to use the zero default.
for (unsigned length = 0; length < complete.size(); ++length) {
target.telemetry.bytes.assign(complete.begin(), complete.begin() + length);
assert(!target.telemHasChanged(&client));
store_raw(client.extra.sensor.min_deltas + 2, size, 9);
assert(target.telemHasChanged(&client));
store_raw(client.extra.sensor.min_deltas + 2, size, 10);
}
target.telemetry.bytes = complete;
store_raw(client.extra.sensor.min_deltas + 2, size, -1);
client.extra.sensor.min_deltas_len = 1 + size;
assert(!target.telemHasChanged(&client)); // Truncated thresholds stay rejected.
}
// Ordinary scaled readings emitted exactly by the production scalar writer.
const struct { uint8_t type; int64_t previous, current; } ordinary[] = {
{LPP_CURRENT, 1234, 1235}, {LPP_TEMPERATURE, 201, 202}
};
for (const auto& entry : ordinary) {
const uint32_t multiplier = LPPData::getMultiplier(entry.type);
for (int64_t raw : {entry.previous, entry.current}) {
uint8_t bytes[4] = {};
LPPWriter writer(bytes, sizeof(bytes));
assert(writer.writeData(1, entry.type, float(raw) / multiplier));
assert((uint32_t(bytes[2]) * 256 + bytes[3]) == raw);
}
expect_delta(entry.type, entry.previous, entry.current, 1, false);
expect_delta(entry.type, entry.previous, entry.current + 1, 1, true);
}
}
static void wide_delta_checks() {
for (uint8_t type : {LPP_GENERIC_SENSOR, LPP_FREQUENCY, LPP_DISTANCE, LPP_ENERGY, LPP_UNIXTIME}) {
assert(LPPData::getDataSize(type) == 4 && !LPPData::isSigned(type));
for (uint32_t raw : {0x1000000U, 0x7fffffffU, 1700000000U, 0xfffffffeU}) {
expect_delta(type, raw, uint64_t(raw) + 1, 0, true);
expect_delta(type, uint64_t(raw) + 1, raw, 0, true);
expect_delta(type, raw, uint64_t(raw) + 1, 1, false);
}
expect_delta(type, 16777219, 16777221, 1, true);
expect_delta(type, 16777221, 16777219, 1, true);
expect_delta(type, 0, UINT32_MAX, UINT32_MAX, false);
expect_delta(type, UINT32_MAX, 0, UINT32_MAX - 1, true);
expect_delta(type, UINT32_MAX, 1, UINT32_MAX - 1, false);
}
// Decoder sign extension covers every scalar width, including signed32.
for (uint8_t size = 1; size <= 4; ++size) {
const int64_t sign_bit = int64_t(1) << (8 * size - 1);
for (int64_t value : {-sign_bit, -sign_bit + 1, int64_t(-1), int64_t(0), sign_bit - 1}) {
uint8_t bytes[4] = {};
store_raw(bytes, size, value);
assert(readTelemRawValue(bytes, size, true) == value);
}
}
assert(readTelemRawValue(nullptr, 4, false) == 0);
uint8_t bytes[5] = {};
assert(readTelemRawValue(bytes, 0, false) == 0);
assert(readTelemRawValue(bytes, 5, false) == 0);
}
static void signed_wide_delta_checks() {
// The runner supplies signed metadata for this normally unsigned scalar.
const uint8_t type = LPP_GENERIC_SENSOR;
assert(LPPData::getDataSize(type) == 4 && LPPData::isSigned(type));
expect_delta(type, INT32_MIN, INT32_MAX, INT32_MAX, true);
expect_delta(type, INT32_MAX, INT32_MIN, INT32_MAX, true);
expect_delta(type, INT32_MIN, INT32_MIN + 1, 0, true);
expect_delta(type, INT32_MAX - 1, INT32_MAX, 1, false);
expect_delta(type, -16777217, -16777216, 0, true);
expect_delta(type, 0, 0, INT32_MIN, true);
}
static void writer_checks() {
uint8_t buffer[16];
memset(buffer, 0xAA, sizeof(buffer));
@@ -432,6 +559,9 @@ int main(int argc, char** argv) {
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 if (!strcmp(argv[1], "delta")) delta_checks();
else if (!strcmp(argv[1], "wide-delta")) wide_delta_checks();
else if (!strcmp(argv[1], "signed-wide-delta")) signed_wide_delta_checks();
else assert(false);
std::cout << "Sensor subscription checks passed\n";
}
+35
View File
@@ -19,6 +19,7 @@ FIXTURE = ROOT / 'test/fixtures/sensor_subscriptions/test_sensor_subscriptions.c
def production_methods(source):
methods = extract_braced(source, 'static const uint8_t* findTelemData(') + '\n'
methods += extract_braced(source, 'static float findTelemValue(') + '\n'
methods += extract_braced(source, 'static int64_t readTelemRawValue(') + '\n'
methods += extract_braced(source, 'bool SensorMesh::telemHasChanged(') + '\n'
if 'void SensorMesh::snapshotTelemetry(' in source:
methods += extract_braced(source, 'void SensorMesh::snapshotTelemetry(') + '\n'
@@ -78,6 +79,40 @@ class SensorSubscriptionTest(unittest.TestCase):
def test_raw_snapshots_preserve_scalars_and_unchanged_pushes_stop(self):
self.passed('snapshot')
def test_delta_threshold_boundaries_signed_values_and_missing_fields(self):
self.passed('delta')
def test_full_width_four_byte_deltas_and_signed_decoder_extrema(self):
self.passed('wide-delta')
def test_signed_four_byte_delta_arithmetic_without_narrowing(self):
def signed_scalar(helper):
original = 'return type == LPP_VOLTAGE || type == LPP_CURRENT'
self.assertEqual(helper.count(original), 1)
return helper.replace(original, 'return type == LPP_GENERIC_SENSOR || type == LPP_VOLTAGE || type == LPP_CURRENT', 1)
# Current four-byte types are unsigned. Exercise the same production
# arithmetic with signed metadata to check the full decoder range.
result = self.execute('signed-wide-delta', helper_transform=signed_scalar)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('Sensor subscription checks passed', result.stdout)
def test_old_float_detector_misses_wide_changes_and_miscompares_equal_deltas(self):
def old(source):
begin = source.index(' const bool is_signed =', source.index('bool SensorMesh::telemHasChanged('))
end = source.index('\n i += 2 + sz;', begin)
original = ''' 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));
float v = findTelemValue(buf, size, ch, t, 0.0f);
if (fabsf(v - pv) > min_delta) changed = true;
'''
return source[:begin] + original + source[end:]
for case in ('delta', 'wide-delta'):
with self.subTest(case=case):
result = self.execute(case, old)
self.assertNotEqual(result.returncode, 0, 'float detector escaped regression')
self.assertIn('raw LPP delta must use the exact strict threshold', result.stderr)
def test_lossy_float_snapshot_negative_control(self):
def old(source):
current = ''' const uint8_t* value = findTelemData(buf, size, ch, t);