// 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 // 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"); } }