Compare Sensor subscription deltas in exact LPP units
This commit is contained in:
1 parent
0d0a0f187e
commit
422b6f3ff2
3 files changed
+183
-5
No files matched your search
@@ -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";
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user