// Full actual handler, output enum, ownership setters and RGB writers are injected. // The clock is uint32_t even on hosts where unsigned long has 64 bits. #include #include #include #include namespace mesh { namespace notify { @OUTPUT_ENUM@ } } @CONSTANTS@ static uint32_t clock_ms = 0; uint32_t millis() { return clock_ms; } static unsigned writes = 0, batt_reads = 0, power_reads = 0, charge_reads = 0; static uint32_t batt_delay = 0, power_delay = 0; #ifdef EXT_CHRG_DETECT static uint32_t charge_delay = 0; #endif static const char* active_case = "startup"; static unsigned checks = 0; #define CHECK(value) do { \ ++checks; \ if (!(value)) { \ std::fprintf(stderr, "FAIL %s: %s (line %d)\n", active_case, #value, __LINE__); \ std::exit(1); \ } \ } while (0) struct Board { uint16_t battery = 4000; bool external = false; uint16_t getBattMilliVolts() { ++batt_reads; clock_ms += batt_delay; return battery; } bool isExternalPowered() { ++power_reads; clock_ms += power_delay; return external; } }; #ifdef STATUS_LED_RGB static unsigned red = 0, green = 0, blue = 0, channel = 0; void analogWrite(unsigned pin, unsigned level) { CHECK(level <= 255); if (channel == 0) { CHECK(pin == PIN_STATUS_LED_R); red = level; } else if (channel == 1) { CHECK(pin == PIN_STATUS_LED_G); green = level; } else { CHECK(pin == PIN_STATUS_LED_B); blue = level; ++writes; } channel = (channel + 1) % 3; } @WRITERS@ #endif #ifdef EXT_CHRG_DETECT constexpr unsigned LOW = 0; static unsigned charge_level = 1; int digitalRead(unsigned pin) { CHECK(pin == EXT_CHRG_DETECT); ++charge_reads; clock_ms += charge_delay; return int(charge_level); } #endif class UITask { public: @DEFAULTS@ @SETTERS@ int _msgcount = 0; bool _connected = false; Board board; Board* _board = &board; void userLedHandler(); }; @HANDLER@ static void tick(UITask& ui, uint32_t stamp) { clock_ms = stamp; ui.userLedHandler(); } #ifdef STATUS_LED_RGB static void color(unsigned r, unsigned g, unsigned b) { CHECK(channel == 0); CHECK(red == r); CHECK(green == g); CHECK(blue == b); } static uint32_t boot(UITask& ui, uint32_t start = 100) { const uint32_t duration = ui._msgcount > 0 ? 800 : 1200; tick(ui, start); CHECK(writes == 1); CHECK(batt_reads == 0); CHECK(power_reads == 0); tick(ui, start + duration); CHECK(writes == 2); color(0, 0, 0); if (start + duration != 0) { CHECK(batt_reads == 1); #ifdef EXT_CHRG_DETECT CHECK(power_reads == 1); #endif } return start + duration; } static uint32_t heartbeat(UITask& ui, uint32_t start = 100) { const uint32_t end = boot(ui, start); tick(ui, end + 1); CHECK(writes == 3); return end + 1; } static void veto(UITask& ui, bool owner, bool enabled) { if (owner) ui.setNotificationLedOwnership(enabled); else ui.setNotificationOutputs(enabled ? uint8_t(31 & ~mesh::notify::Led) : 31); } static void suppressLong(UITask& ui, bool owner, bool on) { uint32_t stamp = heartbeat(ui); if (!on) { stamp += 21; tick(ui, stamp); } const unsigned before = writes, battery_before = batt_reads, power_before = power_reads; veto(ui, owner, true); constexpr uint32_t idle = 26u * 24u * 60u * 60u * 1000u; tick(ui, stamp + idle); CHECK(writes == before); CHECK(batt_reads == battery_before); CHECK(power_reads == power_before); veto(ui, owner, false); tick(ui, stamp + idle); CHECK(writes == before + 1); if (on) color(0, 0, 0); else color(0, 255, 0); CHECK(batt_reads == battery_before + 1); #ifdef EXT_CHRG_DETECT CHECK(power_reads == power_before + 1); #endif tick(ui, stamp + idle); CHECK(writes == before + 1); } #endif // A fresh process for every case leaves actual function statics untouched. int main(int argc, char** argv) { CHECK(argc == 2); active_case = argv[1]; const char* name = argv[1]; UITask ui; #ifdef STATUS_LED_RGB if (!std::strcmp(name, "boot-zero") || !std::strcmp(name, "boot-upper") || !std::strcmp(name, "boot-wrap") || !std::strcmp(name, "boot-zero-end")) { const uint32_t start = !std::strcmp(name, "boot-zero") ? 0u : !std::strcmp(name, "boot-upper") ? 0x80000000u : !std::strcmp(name, "boot-wrap") ? UINT32_MAX - 10u : 0u - 1200u; tick(ui, start); CHECK(writes == 1); if (start == 0) color(255, 0, 0); tick(ui, start + 1199); CHECK(writes == 2); CHECK(batt_reads == 0); CHECK(power_reads == 0); tick(ui, start + 1200); CHECK(writes == 3); color(0, 0, 0); CHECK(batt_reads == unsigned(start + 1200 != 0)); tick(ui, start + 1200); CHECK(writes == 3); tick(ui, start + 1201); CHECK(writes == 4); color(0, 255, 0); CHECK(batt_reads == 1); } else if (!std::strcmp(name, "wheel-colors")) { tick(ui, 0); color(255, 0, 0); tick(ui, 399); color(3, 252, 0); tick(ui, 400); color(0, 255, 0); tick(ui, 800); color(0, 0, 255); tick(ui, 1199); color(252, 0, 3); CHECK(batt_reads == 0); CHECK(power_reads == 0); } else if (!std::strcmp(name, "message-first")) { ui._msgcount = 1; const uint32_t end = boot(ui, UINT32_MAX - 400u); tick(ui, end + 1); CHECK(writes == 3); color(255, 90, 0); } else if (!std::strcmp(name, "message") || !std::strcmp(name, "message-wrap") || !std::strcmp(name, "message-retrigger") || !std::strcmp(name, "message-decrease")) { boot(ui); const uint32_t start = !std::strcmp(name, "message-wrap") ? UINT32_MAX - 100u : 2000u; ui._msgcount = 1; tick(ui, start); CHECK(writes == 3); const unsigned battery_before = batt_reads; tick(ui, start + 799); CHECK(writes == 4); CHECK(batt_reads == battery_before); uint32_t end = start + 800; if (!std::strcmp(name, "message-retrigger")) { ui._msgcount = 2; tick(ui, start + 799); CHECK(writes == 5); end = start + 1599; tick(ui, end - 1); CHECK(writes == 6); } else if (!std::strcmp(name, "message-decrease")) { ui._msgcount = 0; } const unsigned before = writes; tick(ui, end); CHECK(writes == before + 1); color(0, 0, 0); tick(ui, end + 1); CHECK(writes == before + 2); if (ui._msgcount) color(255, 90, 0); else color(0, 255, 0); } else if (!std::strcmp(name, "normal") || !std::strcmp(name, "unread") || !std::strcmp(name, "connected") || !std::strcmp(name, "low-battery") || !std::strcmp(name, "threshold")) { if (!std::strcmp(name, "unread")) ui._msgcount = 1; if (!std::strcmp(name, "connected")) ui._connected = true; if (!std::strcmp(name, "low-battery")) { ui.board.battery = 3499; ui._connected = true; ui._msgcount = 1; } if (!std::strcmp(name, "threshold")) ui.board.battery = 3500; const uint32_t on = heartbeat(ui); if (ui.board.battery < 3500) color(255, 0, 0); else if (ui._msgcount) color(255, 90, 0); else if (ui._connected) color(0, 0, 255); else color(0, 255, 0); const uint32_t duration = ui._msgcount ? 200 : 20; tick(ui, on + duration); CHECK(writes == 3); tick(ui, on + duration + 1); CHECK(writes == 4); color(0, 0, 0); tick(ui, on + 4001); CHECK(writes == 4); tick(ui, on + 4002); CHECK(writes == 5); } else if (!std::strcmp(name, "heartbeat-wrap-on") || !std::strcmp(name, "heartbeat-wrap-off") || !std::strcmp(name, "heartbeat-zero-stamp")) { const uint32_t on = heartbeat(ui); tick(ui, on + 21); CHECK(writes == 4); const bool zero = !std::strcmp(name, "heartbeat-zero-stamp"); const uint32_t near = zero ? 0u - 21u : UINT32_MAX - 10u; tick(ui, near); CHECK(writes == 5); color(0, 255, 0); if (!std::strcmp(name, "heartbeat-wrap-off")) { tick(ui, 100); CHECK(writes == 6); color(0, 0, 0); tick(ui, 4080); CHECK(writes == 6); tick(ui, 4081); CHECK(writes == 7); } else { tick(ui, near + 20); CHECK(writes == 5); tick(ui, near + 21); CHECK(writes == 6); color(0, 0, 0); tick(ui, near + 4001); CHECK(writes == 6); tick(ui, near + 4002); CHECK(writes == 7); } } else if (!std::strcmp(name, "heartbeat-long")) { const uint32_t on = heartbeat(ui); tick(ui, on + 100000); CHECK(writes == 4); color(0, 0, 0); tick(ui, on + 100000); CHECK(writes == 4); tick(ui, on + 103980); CHECK(writes == 4); tick(ui, on + 103981); CHECK(writes == 5); } else if (!std::strcmp(name, "heartbeat-msg-phase")) { ui._msgcount = 1; const uint32_t on = heartbeat(ui); ui._msgcount = 0; tick(ui, on + 200); CHECK(writes == 3); tick(ui, on + 201); CHECK(writes == 4); tick(ui, on + 4001); CHECK(writes == 4); tick(ui, on + 4002); CHECK(writes == 5); color(0, 255, 0); tick(ui, on + 4022); CHECK(writes == 5); tick(ui, on + 4023); CHECK(writes == 6); } else if (!std::strcmp(name, "battery-cadence")) { const uint32_t end = boot(ui); tick(ui, end + 60000); CHECK(batt_reads == 1); tick(ui, end + 60001); CHECK(batt_reads == 2); tick(ui, end + 60001); CHECK(batt_reads == 2); } else if (!std::strcmp(name, "poll-wrap-before") || !std::strcmp(name, "poll-wrap-after")) { const bool before = !std::strcmp(name, "poll-wrap-before"); const uint32_t end = boot(ui, UINT32_MAX - (before ? 2000u : 62000u)); tick(ui, before ? end + 1 : 100u); CHECK(batt_reads == unsigned(before ? 1 : 2)); #ifdef EXT_CHRG_DETECT CHECK(power_reads == unsigned(before ? 1 : 2)); #endif } else if (!std::strcmp(name, "poll-zero-stamp")) { const uint32_t end = boot(ui, UINT32_MAX - 62000u); CHECK(end != 0); tick(ui, 0); CHECK(batt_reads == 2); tick(ui, 1); CHECK(batt_reads == 2); tick(ui, 60000); CHECK(batt_reads == 2); tick(ui, 60001); CHECK(batt_reads == 3); #ifdef EXT_CHRG_DETECT CHECK(power_reads == 3); #endif } else if (!std::strcmp(name, "poll-long") || !std::strcmp(name, "poll-near-full")) { const uint32_t end = boot(ui); const uint32_t gap = !std::strcmp(name, "poll-long") ? 26u * 24u * 60u * 60u * 1000u : UINT32_MAX - 100u; tick(ui, end + gap); CHECK(batt_reads == 2); #ifdef EXT_CHRG_DETECT CHECK(power_reads == 2); #endif tick(ui, end + gap); CHECK(batt_reads == 2); } else if (!std::strcmp(name, "poll-read-delays")) { batt_delay = 25; power_delay = 17; #ifdef EXT_CHRG_DETECT charge_delay = 9; #endif const uint32_t end = boot(ui); tick(ui, end + 60001); CHECK(batt_reads == 2); uint32_t expected_clock = end + 60001 + 25; #ifdef EXT_CHRG_DETECT expected_clock += 17; #endif CHECK(clock_ms == expected_clock); tick(ui, end + 120002); CHECK(batt_reads == 3); #ifdef EXT_CHRG_DETECT const unsigned before = power_reads; tick(ui, end + 121002); CHECK(power_reads == before); tick(ui, end + 121003); CHECK(power_reads == before + 1); #endif } else if (!std::strcmp(name, "owner-fresh") || !std::strcmp(name, "mask-fresh")) { const bool owner = !std::strcmp(name, "owner-fresh"); veto(ui, owner, true); tick(ui, 0); tick(ui, 0x80000000u); CHECK(batt_reads == 0); CHECK(power_reads == 0); CHECK(writes == 0); veto(ui, owner, false); boot(ui, 0x80000000u); } else if (!std::strcmp(name, "owner-flourish-short") || !std::strcmp(name, "mask-flourish-long")) { const bool short_gap = !std::strcmp(name, "owner-flourish-short"); tick(ui, 100); CHECK(writes == 1); veto(ui, short_gap, true); const uint32_t now = short_gap ? 500u : 0x80000000u; tick(ui, now); CHECK(writes == 1); CHECK(batt_reads == 0); veto(ui, short_gap, false); tick(ui, now); CHECK(writes == 2); if (short_gap) { CHECK(batt_reads == 0); tick(ui, 1300); CHECK(writes == 3); color(0, 0, 0); } else { CHECK(batt_reads == 1); color(0, 0, 0); } } else if (!std::strcmp(name, "owner-on-long")) { suppressLong(ui, true, true); } else if (!std::strcmp(name, "owner-off-long")) { suppressLong(ui, true, false); } else if (!std::strcmp(name, "mask-on-long")) { suppressLong(ui, false, true); } else if (!std::strcmp(name, "mask-off-long")) { suppressLong(ui, false, false); } else if (!std::strcmp(name, "both-vetoes")) { ui.setNotificationLedOwnership(true); ui.setNotificationOutputs(0); tick(ui, 100); CHECK(writes == 0); ui.setNotificationLedOwnership(false); tick(ui, 200); CHECK(writes == 0); ui.setNotificationOutputs(31); boot(ui, 200); #ifdef EXT_CHRG_DETECT } else if (!std::strcmp(name, "power-cadence")) { const uint32_t end = boot(ui); tick(ui, end + 1000); CHECK(power_reads == 1); tick(ui, end + 1001); CHECK(power_reads == 2); tick(ui, end + 1001); CHECK(power_reads == 2); } else if (!std::strcmp(name, "charging") || !std::strcmp(name, "charged")) { boot(ui); ui.board.external = true; charge_level = !std::strcmp(name, "charging") ? 0 : 1; tick(ui, 3000); CHECK(charge_reads == 1); if (charge_level) color(0, 40, 0); else color(255, 89, 0); tick(ui, 3500); CHECK(charge_reads == 1); if (charge_level) color(0, 40, 0); else color(127, 44, 0); } else if (!std::strcmp(name, "charge-unread")) { boot(ui); ui.board.external = true; charge_level = 0; tick(ui, 3000); color(255, 89, 0); ui._msgcount = 1; tick(ui, 3100); tick(ui, 3900); color(0, 0, 0); tick(ui, 3901); color(255, 90, 0); } else if (!std::strcmp(name, "charge-to-heartbeat")) { boot(ui); ui.board.external = true; tick(ui, 3000); color(0, 40, 0); ui.board.external = false; tick(ui, 4001); color(0, 255, 0); const unsigned before = writes; tick(ui, 4021); CHECK(writes == before); tick(ui, 4022); CHECK(writes == before + 1); color(0, 0, 0); } else if (!std::strcmp(name, "power-unpowered-charge-skip")) { boot(ui); tick(ui, 3000); CHECK(power_reads == 2); CHECK(charge_reads == 0); } else if (!std::strcmp(name, "poll-powered-read-delays")) { boot(ui); ui.board.external = true; charge_level = 0; batt_delay = 25; power_delay = 17; charge_delay = 9; tick(ui, 3000); CHECK(power_reads == 2); CHECK(charge_reads == 1); CHECK(clock_ms == 3026); color(255, 89, 0); tick(ui, 4000); CHECK(power_reads == 2); CHECK(charge_reads == 1); tick(ui, 4001); CHECK(power_reads == 3); CHECK(charge_reads == 2); CHECK(clock_ms == 4027); #endif } else { std::fprintf(stderr, "unknown case: %s\n", name); return 2; } #else CHECK(std::strcmp(name, "no-led") == 0); const uint32_t stamps[] = {0u, 1u, 0x7fffffffu, 0x80000000u, UINT32_MAX}; for (uint32_t stamp : stamps) { tick(ui, stamp); ui.setNotificationLedOwnership(true); tick(ui, stamp); ui.setNotificationLedOwnership(false); ui.setNotificationOutputs(0); tick(ui, stamp); ui.setNotificationOutputs(31); } CHECK(writes == 0); CHECK(batt_reads == 0); CHECK(power_reads == 0); CHECK(charge_reads == 0); #endif #ifndef EXT_CHRG_DETECT CHECK(charge_reads == 0); #endif std::printf("PASS %s: %u checks\n", name, checks); }