The firmware has never been flashed, and a real bug already reached the repo because of it: RD03E_FRAME_LEN was 5 for a 6-byte frame, so the footer check collided with the distance high byte and EVERY distance reading was garbage — always `lo | 0x5500`, about 218 metres, regardless of what the sensor saw. That was pure logic with no hardware dependency. It should have been catchable on a laptop, and there was simply no way to run the code. Extracted the hardware-free logic out of the three drivers — rd03e_parse, bmp280_compensate, mems_level — as moves rather than rewrites, carrying the explanatory comments along with the code they explain. The drivers now own only their bus I/O and call into the pure units, so nothing changes for the real device. `./run_tests.sh` builds them with gcc -Wall -Wextra -Werror plus a dependency-free assert harness: 175 checks, 0 failed, from a clean tree. Proven to catch the actual bug rather than assumed to: reintroducing FRAME_LEN 5 fails four checks, including one that reads "a simple-report frame is 6 bytes, not 5", plus the truncated-frame and 5-byte-window cases. Restored, green again. This does NOT make the firmware verified, and the README says so plainly — it is called a narrow exception and scoped to pure logic. Wiring, timing, real register behaviour and the reconstructed RD-03E frame format all still need the physical board. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
139 lines
6.5 KiB
C
139 lines
6.5 KiB
C
// RD-03E frame scanner tests.
|
|
//
|
|
// The headline case is `distance_little_endian_0x2C_0x01`: this is exactly
|
|
// the class of bug that actually shipped in this firmware. RD03E_FRAME_LEN
|
|
// was 5 for a 6-byte frame, so the footer comparison read bytes [3..4]
|
|
// (the distance HIGH byte and the first footer byte) instead of [4..5].
|
|
// Frames still "validated" whenever the high byte happened to be 0x55, and
|
|
// every distance came back as `lo | 0x5500` — about 218 metres, always.
|
|
// Pure logic, no hardware needed to catch it. It just was never run.
|
|
|
|
#include "../main/rd03e_parse.h"
|
|
#include "test_util.h"
|
|
|
|
#include <string.h>
|
|
|
|
// header, gesture, dist_lo, dist_hi, footer, footer
|
|
#define FRAME(g, lo, hi) 0xAA, (g), (lo), (hi), 0x55, 0x55
|
|
|
|
void test_rd03e_parse(void) {
|
|
SUITE("rd03e_parse");
|
|
|
|
// --- a well-formed frame parses to the exact expected fields ---------
|
|
{
|
|
const uint8_t buf[] = { FRAME(0x03, 0x2C, 0x01) };
|
|
rd03e_frame_t f = {0};
|
|
CHECK(rd03e_parse_latest(buf, sizeof(buf), &f), "valid frame must parse");
|
|
CHECK_EQ_U(f.gesture, 0x03, "gesture byte is frame[1] verbatim");
|
|
// THE REGRESSION TEST: 0x2C 0x01 little-endian is 0x012C = 300 cm.
|
|
// The shipped bug produced 0x552C = 21804 cm here.
|
|
CHECK_EQ_U(f.distance_cm, 300, "0x2C 0x01 must be 300cm (little-endian)");
|
|
}
|
|
|
|
// --- frame length really is 6 bytes ---------------------------------
|
|
{
|
|
CHECK_EQ_U(RD03E_FRAME_LEN, 6, "a simple-report frame is 6 bytes, not 5");
|
|
|
|
// Two back-to-back frames with NO padding. If the scanner consumed
|
|
// 5 bytes per frame it would desynchronise here and the second
|
|
// frame's fields would be misread (or missed entirely).
|
|
const uint8_t buf[] = {
|
|
FRAME(0x01, 0x0A, 0x00), // 10 cm
|
|
FRAME(0x02, 0xD0, 0x07), // 2000 cm
|
|
};
|
|
CHECK_EQ_U(sizeof(buf), 12, "two frames occupy exactly 12 bytes");
|
|
rd03e_frame_t f = {0};
|
|
CHECK(rd03e_parse_latest(buf, sizeof(buf), &f), "two-frame buffer must parse");
|
|
CHECK_EQ_U(f.gesture, 0x02, "two frames in one buffer: NEWEST gesture wins");
|
|
CHECK_EQ_U(f.distance_cm, 2000, "two frames in one buffer: NEWEST distance wins");
|
|
}
|
|
|
|
// --- a bad footer is rejected ----------------------------------------
|
|
{
|
|
// Correct header, correct length, footer byte [5] wrong.
|
|
const uint8_t buf[] = { 0xAA, 0x03, 0x2C, 0x01, 0x55, 0x56 };
|
|
rd03e_frame_t f = { .gesture = 0xEE, .distance_cm = 4242 };
|
|
CHECK(!rd03e_parse_latest(buf, sizeof(buf), &f), "bad footer byte [5] must be rejected");
|
|
CHECK_EQ_U(f.distance_cm, 4242, "rejected frame must leave *out untouched");
|
|
|
|
// Footer byte [4] wrong instead.
|
|
const uint8_t buf2[] = { 0xAA, 0x03, 0x2C, 0x01, 0x54, 0x55 };
|
|
CHECK(!rd03e_parse_latest(buf2, sizeof(buf2), &f), "bad footer byte [4] must be rejected");
|
|
}
|
|
|
|
// --- a truncated trailing frame is ignored ---------------------------
|
|
{
|
|
// One good frame, then five bytes of a second frame that never
|
|
// finished arriving. The good frame must still be reported and the
|
|
// scanner must not read past the end of the buffer.
|
|
const uint8_t buf[] = {
|
|
FRAME(0x07, 0x64, 0x00), // 100 cm
|
|
0xAA, 0x09, 0xFF, 0x03, 0x55, // truncated: 5 of 6 bytes
|
|
};
|
|
rd03e_frame_t f = {0};
|
|
CHECK(rd03e_parse_latest(buf, sizeof(buf), &f), "must still find the complete frame");
|
|
CHECK_EQ_U(f.gesture, 0x07, "truncated trailing frame must not be reported");
|
|
CHECK_EQ_U(f.distance_cm, 100, "truncated trailing frame must not be reported");
|
|
|
|
// A buffer holding nothing but a truncated frame yields nothing.
|
|
const uint8_t only_partial[] = { 0xAA, 0x09, 0xFF, 0x03, 0x55 };
|
|
CHECK(!rd03e_parse_latest(only_partial, sizeof(only_partial), &f),
|
|
"a lone truncated frame must not parse");
|
|
}
|
|
|
|
// --- garbage before a valid frame is skipped -------------------------
|
|
{
|
|
const uint8_t buf[] = {
|
|
0x00, 0xFF, 0x12, 0x55, 0x55, 0xAA, 0xAA, 0x01, // noise, incl. stray 0xAA
|
|
FRAME(0x05, 0xC8, 0x00), // 200 cm
|
|
};
|
|
rd03e_frame_t f = {0};
|
|
CHECK(rd03e_parse_latest(buf, sizeof(buf), &f), "must resynchronise past garbage");
|
|
CHECK_EQ_U(f.gesture, 0x05, "gesture after resync");
|
|
CHECK_EQ_U(f.distance_cm, 200, "distance after resync");
|
|
}
|
|
|
|
// --- a 0xAA that is really a payload byte must not fool the scanner ---
|
|
{
|
|
// First frame's distance low byte is 0xAA. If the scanner treated
|
|
// that as a header it would misparse; the footer check saves it.
|
|
const uint8_t buf[] = {
|
|
FRAME(0x01, 0xAA, 0x00), // 170 cm
|
|
FRAME(0x02, 0x01, 0x00), // 1 cm (newest)
|
|
};
|
|
rd03e_frame_t f = {0};
|
|
CHECK(rd03e_parse_latest(buf, sizeof(buf), &f), "0xAA payload byte must not break parsing");
|
|
CHECK_EQ_U(f.distance_cm, 1, "newest frame after a 0xAA payload byte");
|
|
}
|
|
|
|
// --- byte-order coverage across the full 16-bit range ----------------
|
|
{
|
|
struct { uint8_t lo, hi; uint16_t want; } cases[] = {
|
|
{ 0x2C, 0x01, 300 }, // the shipped-bug case
|
|
{ 0x00, 0x00, 0 },
|
|
{ 0xFF, 0x00, 255 },
|
|
{ 0x00, 0x01, 256 }, // lo/hi swapped would give 1
|
|
{ 0x01, 0x00, 1 }, // lo/hi swapped would give 256
|
|
{ 0xFF, 0xFF, 65535 },
|
|
};
|
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
|
const uint8_t buf[] = { 0xAA, 0x00, cases[i].lo, cases[i].hi, 0x55, 0x55 };
|
|
rd03e_frame_t f = {0};
|
|
CHECK(rd03e_parse_latest(buf, sizeof(buf), &f), "byte-order case %zu parses", i);
|
|
CHECK_EQ_U(f.distance_cm, cases[i].want,
|
|
"byte-order case %zu: 0x%02X 0x%02X", i, cases[i].lo, cases[i].hi);
|
|
}
|
|
}
|
|
|
|
// --- degenerate inputs are handled, not crashed on -------------------
|
|
{
|
|
rd03e_frame_t f = {0};
|
|
const uint8_t buf[] = { FRAME(0x01, 0x01, 0x00) };
|
|
CHECK(!rd03e_parse_latest(NULL, 6, &f), "NULL buffer is 'no frame'");
|
|
CHECK(!rd03e_parse_latest(buf, sizeof(buf), NULL), "NULL out is 'no frame'");
|
|
CHECK(!rd03e_parse_latest(buf, 0, &f), "empty buffer is 'no frame'");
|
|
CHECK(!rd03e_parse_latest(buf, 5, &f), "a 5-byte window cannot hold a frame");
|
|
CHECK(rd03e_parse_latest(buf, 6, &f), "a 6-byte window can");
|
|
}
|
|
}
|