// Phase 14.1 spike: pure-function harness (throwaway — remove in Plan 14-02). // // Exercises the three pure functions that Plan 14-02 will re-implement in production: // - BuildAdalightFrame (Adalight binary encoder, 6-byte header + RGB payload) // - ParseHexColor (strict 6-hex-char color parser) // - ApplyCeiling (per-channel brightness-ceiling clamp, integer math) // // No external dependencies beyond /////. // On success prints `ALL HARNESS CHECKS PASS` and exits 0. // On any failing assertion, aborts with nonzero exit. #include #include #include #include #include #include // ----------------------------------------------------------------------------- // Canonical pure functions (verbatim — Plan 14-02 will re-implement these). // ----------------------------------------------------------------------------- // [CITED: .planning/phases/14-.../14-RESEARCH.md Pattern 2 — Adalight Frame Encoder] // For N leds: count = N - 1; header = 'A' 'd' 'a' hi lo (hi ^ lo ^ 0x55); payload = N*3 RGB bytes. static void BuildAdalightFrame(const uint8_t* rgb, int numLeds, uint8_t* out, size_t outCap) { // Requires outCap >= 6 + numLeds*3. Caller asserts. (void)outCap; const uint16_t count = static_cast(numLeds - 1); const uint8_t hi = static_cast((count >> 8) & 0xFF); const uint8_t lo = static_cast(count & 0xFF); out[0] = 'A'; out[1] = 'd'; out[2] = 'a'; out[3] = hi; out[4] = lo; out[5] = static_cast(hi ^ lo ^ 0x55); std::memcpy(out + 6, rgb, static_cast(numLeds) * 3); } // [CITED: .planning/phases/14-.../14-RESEARCH.md Code Examples §3 — Hex-string parse] static bool ParseHexColor(const char* s, size_t len, uint8_t& r, uint8_t& g, uint8_t& b) { if (len != 6) return false; for (size_t i = 0; i < 6; ++i) if (!std::isxdigit(static_cast(s[i]))) return false; char buf[7] = {0}; std::memcpy(buf, s, 6); unsigned long v = std::strtoul(buf, nullptr, 16); r = static_cast((v >> 16) & 0xFF); g = static_cast((v >> 8) & 0xFF); b = static_cast( v & 0xFF); return true; } // [CITED: .planning/phases/14-.../14-RESEARCH.md Pattern 1 — LedController ApplyCeiling] // Per-channel scale by ceiling/255 with integer math (rounds down). static void ApplyCeiling(uint8_t* rgb, size_t numChannels, uint8_t ceiling) { for (size_t i = 0; i < numChannels; ++i) { const unsigned int v = rgb[i]; rgb[i] = static_cast((v * static_cast(ceiling)) / 255u); } } // ----------------------------------------------------------------------------- // Assertion helpers // ----------------------------------------------------------------------------- static void Fail(const char* what) { std::fprintf(stderr, "FAIL: %s\n", what); std::exit(1); } #define REQUIRE(cond, msg) do { if (!(cond)) Fail(msg); } while (0) // ----------------------------------------------------------------------------- // Tests // ----------------------------------------------------------------------------- static void TestAdalightEncoder() { // N=10: header must be 0x41 0x64 0x61 0x00 0x09 0x5C and payload is 30 bytes. uint8_t rgb10[30]; for (int i = 0; i < 30; ++i) rgb10[i] = static_cast(i * 7 + 1); // arbitrary but deterministic uint8_t frame[36]; std::memset(frame, 0xAA, sizeof(frame)); BuildAdalightFrame(rgb10, 10, frame, sizeof(frame)); REQUIRE(frame[0] == 0x41, "Adalight[0] must be 'A' (0x41)"); REQUIRE(frame[1] == 0x64, "Adalight[1] must be 'd' (0x64)"); REQUIRE(frame[2] == 0x61, "Adalight[2] must be 'a' (0x61)"); REQUIRE(frame[3] == 0x00, "Adalight[3] count_hi for N=10 must be 0x00"); REQUIRE(frame[4] == 0x09, "Adalight[4] count_lo for N=10 must be 0x09 (N-1)"); REQUIRE(frame[5] == 0x5C, "Adalight[5] checksum for N=10 must be 0x5C (0x00^0x09^0x55)"); // Payload bytes [6..35] copy RGB verbatim. for (int i = 0; i < 30; ++i) { REQUIRE(frame[6 + i] == rgb10[i], "Adalight payload byte must match input RGB verbatim"); } std::printf("PASS: Adalight encoder — 10-LED header (41 64 61 00 09 5C) + 30 RGB bytes\n"); // N=1: count = 0, so checksum = 0x00 ^ 0x00 ^ 0x55 = 0x55. uint8_t rgb1[3] = { 0x11, 0x22, 0x33 }; uint8_t frame1[9]; std::memset(frame1, 0xAA, sizeof(frame1)); BuildAdalightFrame(rgb1, 1, frame1, sizeof(frame1)); REQUIRE(frame1[3] == 0x00, "Adalight N=1 count_hi must be 0x00"); REQUIRE(frame1[4] == 0x00, "Adalight N=1 count_lo must be 0x00"); REQUIRE(frame1[5] == 0x55, "Adalight N=1 checksum must be 0x55 (0x00^0x00^0x55)"); REQUIRE(frame1[6] == 0x11 && frame1[7] == 0x22 && frame1[8] == 0x33, "Adalight N=1 payload must match input"); std::printf("PASS: Adalight encoder — N=1 boundary (checksum 0x55)\n"); } static void TestParseHexColor() { uint8_t r = 0, g = 0, b = 0; // Valid uppercase REQUIRE(ParseHexColor("FF8000", 6, r, g, b), "ParseHexColor FF8000 must return true"); REQUIRE(r == 0xFF && g == 0x80 && b == 0x00, "ParseHexColor FF8000 must yield R=FF G=80 B=00"); // Valid lowercase (case-insensitive) r = g = b = 0; REQUIRE(ParseHexColor("ff8000", 6, r, g, b), "ParseHexColor ff8000 must return true"); REQUIRE(r == 0xFF && g == 0x80 && b == 0x00, "ParseHexColor ff8000 must yield R=FF G=80 B=00 (case-insensitive)"); // All-zero r = g = b = 0xAB; REQUIRE(ParseHexColor("000000", 6, r, g, b), "ParseHexColor 000000 must return true"); REQUIRE(r == 0 && g == 0 && b == 0, "ParseHexColor 000000 must yield R=00 G=00 B=00"); // Length != 6 — reject REQUIRE(!ParseHexColor("FF800", 5, r, g, b), "ParseHexColor len=5 must reject"); REQUIRE(!ParseHexColor("FF80000", 7, r, g, b), "ParseHexColor len=7 must reject"); REQUIRE(!ParseHexColor("", 0, r, g, b), "ParseHexColor len=0 must reject"); // Non-hex digit — reject REQUIRE(!ParseHexColor("GG0000", 6, r, g, b), "ParseHexColor non-hex char must reject"); REQUIRE(!ParseHexColor("12345Z", 6, r, g, b), "ParseHexColor trailing Z must reject"); REQUIRE(!ParseHexColor(" 000", 6, r, g, b), "ParseHexColor leading spaces must reject"); std::printf("PASS: Hex parser — accepts 6 hex chars, rejects bad lengths and non-hex input\n"); } static void TestApplyCeiling() { // All-255 at ceiling=50 → 255*50/255 = 50 exactly. uint8_t a[3] = { 255, 255, 255 }; ApplyCeiling(a, 3, 50); REQUIRE(a[0] == 50 && a[1] == 50 && a[2] == 50, "ApplyCeiling({255,255,255}, 50) must be {50,50,50}"); // All-zero stays zero. uint8_t z[3] = { 0, 0, 0 }; ApplyCeiling(z, 3, 50); REQUIRE(z[0] == 0 && z[1] == 0 && z[2] == 0, "ApplyCeiling({0,0,0}, 50) must be {0,0,0}"); // {255, 128, 0} at ceiling=50: 255*50/255=50, 128*50/255=25 (floor), 0*50/255=0. uint8_t mix[3] = { 255, 128, 0 }; ApplyCeiling(mix, 3, 50); REQUIRE(mix[0] <= 50 && mix[1] <= 50 && mix[2] <= 50, "ApplyCeiling never exceeds ceiling for any channel"); REQUIRE(mix[0] == 50, "ApplyCeiling({255,128,0}, 50)[R] must be 50"); REQUIRE(mix[1] == 25, "ApplyCeiling({255,128,0}, 50)[G] must be 25"); REQUIRE(mix[2] == 0, "ApplyCeiling({255,128,0}, 50)[B] must be 0"); // Identity at ceiling=255. uint8_t idn[3] = { 255, 255, 255 }; ApplyCeiling(idn, 3, 255); REQUIRE(idn[0] == 255 && idn[1] == 255 && idn[2] == 255, "ApplyCeiling at ceiling=255 must be identity"); // Ceiling=1 with max input: 255*1/255 = 1 (integer floor, non-zero preserved at minimum). uint8_t lo[3] = { 255, 255, 255 }; ApplyCeiling(lo, 3, 1); REQUIRE(lo[0] == 1 && lo[1] == 1 && lo[2] == 1, "ApplyCeiling({255,255,255}, 1) must be {1,1,1}"); // Exhaustive post-condition on a sweep: for v in 0..255, ceiling=50 → clamped <= 50. for (unsigned int v = 0; v <= 255; ++v) { uint8_t c = static_cast(v); uint8_t channel[1] = { c }; ApplyCeiling(channel, 1, 50); REQUIRE(channel[0] <= 50, "ApplyCeiling exhaustive sweep: clamped must be <= ceiling"); // v==0 iff clamped==0 if (v == 0) REQUIRE(channel[0] == 0, "ApplyCeiling: v==0 must yield 0"); } std::printf("PASS: Brightness-ceiling clamp — bounded by ceiling, zero-preserving, identity at 255\n"); } // ----------------------------------------------------------------------------- // Entry point // ----------------------------------------------------------------------------- int main() { std::printf("backglow_spike_harness — Phase 14.1 pure-function validation\n"); TestAdalightEncoder(); TestParseHexColor(); TestApplyCeiling(); std::printf("ALL HARNESS CHECKS PASS\n"); return 0; }