Keep RGB Companion status timing working across rollover
This commit is contained in:
1 parent
ec2bf83882
commit
9684e410e3
4 files changed
+601
-34
No files matched your search
@@ -805,6 +805,7 @@ jobs:
|
||||
python3 -B test/test_notification_led_ownership.py -v
|
||||
python3 -B test/test_mono_led_rollover.py -v
|
||||
python3 -B test/test_backlight_poll_rollover.py -v
|
||||
python3 -B test/test_rgb_led_rollover.py -v
|
||||
python3 -B test/test_display_power.py
|
||||
python3 -B test/test_legacy_ui_version_bounds.py -v
|
||||
python3 -B test/test_ssd1306_power.py -v
|
||||
|
||||
@@ -470,62 +470,71 @@ static void statusLedWheel(uint8_t pos) { // smooth hue sweep, 0..255
|
||||
void UITask::userLedHandler() {
|
||||
if (_notification_led_owned || !(_notification_outputs & mesh::notify::Led)) return;
|
||||
#ifdef STATUS_LED_RGB
|
||||
static bool booted = false;
|
||||
static bool flourish_active = false;
|
||||
static unsigned long flourish_until = 0;
|
||||
enum class FlourishState : uint8_t { Unseen, Boot, Message, Idle };
|
||||
static FlourishState flourish = FlourishState::Unseen;
|
||||
static uint32_t flourish_started = 0;
|
||||
static int prev_msgcount = 0;
|
||||
static int state = 0;
|
||||
static unsigned long next_change = 0;
|
||||
static int last_increment = 0;
|
||||
static unsigned long next_batt_check = 0;
|
||||
static bool low_batt = false;
|
||||
static uint32_t last_change = 0;
|
||||
static uint32_t phase_millis = 0;
|
||||
static uint32_t last_batt_check = 0;
|
||||
enum class BatteryState : uint8_t { Unseen, Normal, Low };
|
||||
static BatteryState battery_state = BatteryState::Unseen;
|
||||
|
||||
unsigned long cur_time = millis();
|
||||
const uint32_t cur_time = millis();
|
||||
|
||||
if (!booted) { // color sweep on boot
|
||||
booted = true;
|
||||
flourish_until = cur_time + 1200;
|
||||
flourish_active = true;
|
||||
if (flourish == FlourishState::Unseen) { // color sweep on boot
|
||||
flourish_started = cur_time;
|
||||
flourish = FlourishState::Boot;
|
||||
}
|
||||
if (_msgcount > prev_msgcount) { // color sweep when a new message arrives
|
||||
flourish_until = cur_time + 800;
|
||||
flourish_active = true;
|
||||
flourish_started = cur_time;
|
||||
flourish = FlourishState::Message;
|
||||
}
|
||||
prev_msgcount = _msgcount;
|
||||
|
||||
if (flourish_active) {
|
||||
if (cur_time < flourish_until) {
|
||||
if (flourish == FlourishState::Boot || flourish == FlourishState::Message) {
|
||||
const uint32_t duration = flourish == FlourishState::Boot ? 1200 : 800;
|
||||
if (static_cast<uint32_t>(cur_time - flourish_started) < duration) {
|
||||
statusLedWheel((cur_time % 1200) * 255 / 1200);
|
||||
return;
|
||||
}
|
||||
statusLedWrite(0, 0, 0);
|
||||
flourish_active = false;
|
||||
flourish = FlourishState::Idle;
|
||||
state = 0;
|
||||
next_change = cur_time;
|
||||
last_change = cur_time;
|
||||
phase_millis = 0;
|
||||
}
|
||||
|
||||
if (cur_time > next_batt_check) { // battery reads are not free, keep them rare
|
||||
// Keep the cached entry-time cadence, including across wrap and long idle.
|
||||
// Sample state distinguishes an initial check from a completed check at zero.
|
||||
if (battery_state == BatteryState::Unseen ? cur_time > 0
|
||||
: static_cast<uint32_t>(cur_time - last_batt_check) > 60000) {
|
||||
uint16_t low_batt_mv = LOW_BATT_MILLIVOLTS;
|
||||
#if defined(NRF52_POWER_MANAGEMENT)
|
||||
low_batt_mv = mesh::power::remapBatteryWarningMillivolts(
|
||||
LOW_BATT_MILLIVOLTS, BATT_MIN_MILLIVOLTS, BATT_MAX_MILLIVOLTS,
|
||||
mesh::power::configuredEmpty(), mesh::power::configuredFull());
|
||||
#endif
|
||||
low_batt = _board->getBattMilliVolts() < low_batt_mv;
|
||||
next_batt_check = cur_time + 60000;
|
||||
battery_state = _board->getBattMilliVolts() < low_batt_mv
|
||||
? BatteryState::Low : BatteryState::Normal;
|
||||
last_batt_check = cur_time;
|
||||
}
|
||||
|
||||
#ifdef EXT_CHRG_DETECT
|
||||
// charge status display while docked (skipped when messages are waiting,
|
||||
// so the unread indication is never masked)
|
||||
static unsigned long next_pwr_check = 0;
|
||||
static bool ext_powered = false, ext_charging = false;
|
||||
if (cur_time > next_pwr_check) {
|
||||
ext_powered = _board->isExternalPowered();
|
||||
ext_charging = ext_powered && digitalRead(EXT_CHRG_DETECT) == LOW;
|
||||
next_pwr_check = cur_time + 1000;
|
||||
static uint32_t last_pwr_check = 0;
|
||||
enum class PowerState : uint8_t { Unseen, No, Yes };
|
||||
static PowerState power_state = PowerState::Unseen;
|
||||
static bool ext_charging = false;
|
||||
if (power_state == PowerState::Unseen ? cur_time > 0
|
||||
: static_cast<uint32_t>(cur_time - last_pwr_check) > 1000) {
|
||||
power_state = _board->isExternalPowered() ? PowerState::Yes : PowerState::No;
|
||||
ext_charging = power_state == PowerState::Yes && digitalRead(EXT_CHRG_DETECT) == LOW;
|
||||
last_pwr_check = cur_time;
|
||||
}
|
||||
if (ext_powered && _msgcount == 0) {
|
||||
if (power_state == PowerState::Yes && _msgcount == 0) {
|
||||
if (ext_charging) {
|
||||
// amber breathing while charging
|
||||
int ph = cur_time % 2000;
|
||||
@@ -536,17 +545,17 @@ void UITask::userLedHandler() {
|
||||
statusLedWrite(0, 40, 0); // dim solid green: charge complete
|
||||
}
|
||||
state = 0;
|
||||
next_change = cur_time;
|
||||
last_change = cur_time;
|
||||
phase_millis = 0;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (cur_time > next_change) {
|
||||
if (static_cast<uint32_t>(cur_time - last_change) > phase_millis) {
|
||||
if (state == 0) {
|
||||
state = 1;
|
||||
last_increment = (_msgcount > 0) ? LED_ON_MSG_MILLIS : LED_ON_MILLIS;
|
||||
next_change = cur_time + last_increment;
|
||||
if (low_batt) {
|
||||
phase_millis = (_msgcount > 0) ? LED_ON_MSG_MILLIS : LED_ON_MILLIS;
|
||||
if (battery_state == BatteryState::Low) {
|
||||
statusLedWrite(255, 0, 0); // red: battery low
|
||||
} else if (_msgcount > 0) {
|
||||
statusLedWrite(255, 90, 0); // amber: unread messages
|
||||
@@ -557,9 +566,10 @@ void UITask::userLedHandler() {
|
||||
}
|
||||
} else {
|
||||
state = 0;
|
||||
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
|
||||
phase_millis = LED_CYCLE_MILLIS - phase_millis;
|
||||
statusLedWrite(0, 0, 0);
|
||||
}
|
||||
last_change = cur_time;
|
||||
}
|
||||
#elif defined(PIN_STATUS_LED)
|
||||
static int state = 0;
|
||||
|
||||
Vendored
+422
@@ -0,0 +1,422 @@
|
||||
// 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 <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Exercise the complete original-UI RGB status handler and its actual writers.
|
||||
|
||||
Clock, ADC, power and GPIO are host fakes. Separate processes preserve real
|
||||
function-static initialization. This is not physical LED or power-use proof.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / 'test/fixtures/rgb_led_rollover.cpp'
|
||||
CASES = (
|
||||
'boot-zero', 'boot-upper', 'boot-wrap', 'boot-zero-end', 'wheel-colors', 'message',
|
||||
'message-wrap', 'message-retrigger', 'message-first', 'message-decrease',
|
||||
'normal', 'unread', 'connected', 'low-battery', 'threshold',
|
||||
'heartbeat-wrap-on', 'heartbeat-wrap-off', 'heartbeat-zero-stamp',
|
||||
'heartbeat-long', 'heartbeat-msg-phase', 'battery-cadence',
|
||||
'poll-wrap-before', 'poll-wrap-after', 'poll-zero-stamp', 'poll-long',
|
||||
'poll-near-full', 'poll-read-delays', 'owner-fresh', 'mask-fresh',
|
||||
'owner-flourish-short', 'mask-flourish-long', 'owner-on-long',
|
||||
'owner-off-long', 'mask-on-long', 'mask-off-long', 'both-vetoes',
|
||||
)
|
||||
POWER_CASES = (
|
||||
'power-cadence', 'charging', 'charged', 'charge-unread',
|
||||
'charge-to-heartbeat', 'power-unpowered-charge-skip', 'poll-powered-read-delays',
|
||||
)
|
||||
|
||||
|
||||
def harness(mutation=None):
|
||||
source = (ROOT / 'examples/companion_radio/ui-orig/UITask.cpp').read_text(encoding='ascii')
|
||||
abstract = (ROOT / 'examples/companion_radio/AbstractUITask.h').read_text(encoding='ascii')
|
||||
notification = (ROOT / 'src/helpers/CompanionNotificationPolicy.h').read_text(encoding='ascii')
|
||||
output_enum = extract_braced(notification, 'enum Output : uint8_t') + ';'
|
||||
constants = source[source.index('#if defined(PIN_STATUS_LED) ||'):
|
||||
source.index('#ifndef USER_BTN_PRESSED')]
|
||||
defaults = '\n'.join(re.findall(
|
||||
r'^ (?:bool _notification_led_owned|uint8_t _notification_outputs) = .*;',
|
||||
abstract, re.MULTILINE))
|
||||
setters = '\n'.join(extract_braced(abstract, signature) for signature in (
|
||||
'void setNotificationLedOwnership(', 'void setNotificationOutputs('))
|
||||
handler = extract_braced(source, 'void UITask::userLedHandler()')
|
||||
if mutation == 'no-owner-guard':
|
||||
guard = 'if (_notification_led_owned || !(_notification_outputs & mesh::notify::Led)) return;'
|
||||
if handler.count(guard) != 1:
|
||||
raise AssertionError('the actual ownership/output guard must be present')
|
||||
handler = handler.replace(guard, '', 1)
|
||||
elif mutation == 'absolute-battery-deadline':
|
||||
handler, count = re.subn(
|
||||
r'static_cast<uint32_t>\(cur_time - (\w+)\) > 60000',
|
||||
r'cur_time > static_cast<uint32_t>(\1 + 60000u)', handler)
|
||||
if count != 1:
|
||||
raise AssertionError('the actual elapsed battery timer must be present')
|
||||
elif mutation == 'completion-time-anchor':
|
||||
# The actual handler deliberately captures the clock before either read.
|
||||
# Do not depend on the private name of the elapsed-time anchor.
|
||||
rgb, mono = handler.split('#elif defined(PIN_STATUS_LED)', 1)
|
||||
rgb, count = re.subn(r'(\b\w+\s*=\s*)cur_time;', r'\1millis();', rgb)
|
||||
if count < 2:
|
||||
raise AssertionError('RGB timers must retain captured-time anchors')
|
||||
handler = rgb + '#elif defined(PIN_STATUS_LED)' + mono
|
||||
writers = '\n'.join(extract_braced(source, signature) for signature in (
|
||||
'static void statusLedWrite(', 'static void statusLedWheel('))
|
||||
code = FIXTURE.read_text(encoding='ascii')
|
||||
for token, value in {'CONSTANTS': constants, 'DEFAULTS': defaults,
|
||||
'SETTERS': setters, 'HANDLER': handler,
|
||||
'WRITERS': writers, 'OUTPUT_ENUM': output_enum}.items():
|
||||
code = code.replace('@' + token + '@', value)
|
||||
return code
|
||||
|
||||
|
||||
class RgbLedRolloverTest(unittest.TestCase):
|
||||
def test_actual_rgb_handler(self):
|
||||
self.run_variants()
|
||||
|
||||
def test_ownership_regression_is_rejected(self):
|
||||
self.run_variants('no-owner-guard', ('owner-fresh',), 'batt_reads == 0')
|
||||
|
||||
def test_captured_poll_anchor_regression_is_rejected(self):
|
||||
self.run_variants('completion-time-anchor', ('poll-read-delays',), 'batt_reads == 2')
|
||||
|
||||
def test_absolute_poll_deadline_regression_is_rejected(self):
|
||||
self.run_variants('absolute-battery-deadline', ('poll-wrap-before',),
|
||||
'batt_reads == unsigned(before ? 1 : 2)')
|
||||
|
||||
def run_variants(self, mutation=None, cases=None, expected_failure=None):
|
||||
compiler = shutil.which('g++') or shutil.which('clang++')
|
||||
if compiler is None:
|
||||
self.skipTest('a host C++17 compiler is required')
|
||||
sanitize = [] if os.name == 'nt' else [
|
||||
'-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie']
|
||||
with tempfile.TemporaryDirectory(prefix='rgb-led-rollover-') as directory:
|
||||
work = Path(directory)
|
||||
source = work / 'fixture.cpp'
|
||||
source.write_text(harness(mutation), encoding='ascii')
|
||||
variants = ('charge', 'no-charge') if mutation else ('charge', 'no-charge', 'disabled')
|
||||
for variant in variants:
|
||||
with self.subTest(variant=variant):
|
||||
macros = [] if variant == 'disabled' else [
|
||||
'-DPIN_STATUS_LED_R=3', '-DPIN_STATUS_LED_G=24', '-DPIN_STATUS_LED_B=28']
|
||||
if variant == 'charge':
|
||||
# X1 aliases its monochrome status pin to RGB green.
|
||||
macros += ['-DEXT_CHRG_DETECT=35', '-DPIN_STATUS_LED=24']
|
||||
binary = work / (variant + ('.exe' if os.name == 'nt' else ''))
|
||||
built = subprocess.run([
|
||||
compiler, '-std=c++17', '-O1', '-g', '-Wall', '-Wextra', '-Werror',
|
||||
*sanitize, *macros,
|
||||
'-I' + str(ROOT / 'src'), str(source), '-o', str(binary),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
selected = cases if cases is not None else (
|
||||
CASES + POWER_CASES if variant == 'charge' else
|
||||
CASES if variant == 'no-charge' else ('no-led',))
|
||||
for case in selected:
|
||||
with self.subTest(case=case):
|
||||
tested = subprocess.run([str(binary), case], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
if expected_failure:
|
||||
self.assertEqual(tested.returncode, 1, tested.stdout + tested.stderr)
|
||||
self.assertIn(expected_failure, tested.stderr)
|
||||
self.assertNotIn('AddressSanitizer:', tested.stderr)
|
||||
self.assertNotIn('runtime error:', tested.stderr)
|
||||
else:
|
||||
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
||||
self.assertRegex(tested.stdout, r'PASS ' + case + r': \d+ checks')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user