Files
MeshCore/test/fixtures/notification_led_ownership.cpp
T
9d52b1fd68 Add saved LED controls across supported boards
Add get leds and set leds on|off for Companion, repeater, room server and
sensor builds. Preserve the setting across reboots and cover GPIO, RGB,
Bluetooth, notification and expander indicators without changing radio
amplifier controls or screen backlights.

RAK Companions blink while advertising and keep the Bluetooth LED off once
connected, including late timer callbacks.

Adapted from meshcore-dev/MeshCore#3605 and #2694.
Validated with host regressions and firmware builds for SenseCAP Solar,
RAK4631, T096, Heltec V4, T-Beam, Xiao S3, Pico W and Wio E5 Mini.

Co-authored-by: R0ck <develop@hectospark.co.uk>
Co-authored-by: Nick Dunklee <nick@dunklee.org>
2026-10-20 08:59:39 -07:00

261 lines
8.1 KiB
C++

#include "notification_harness.h"
#include <cstdio>
#include <cstring>
namespace {
unsigned checks = 0, failures = 0, scenarios = 0;
const char* scenario = nullptr;
void check(bool condition, const char* expression, unsigned line) {
++checks;
if (!condition) {
++failures;
std::printf("FAIL: %s:%u: %s [R=%u G=%u B=%u]\n", scenario, line,
expression, levels[3], levels[24], levels[28]);
}
}
#define CHECK(expression) check(bool(expression), #expression, __LINE__)
template <typename Action>
void run(const char* name, Action action) {
scenario = name;
++scenarios;
resetIo();
const unsigned before = failures;
action();
if (before == failures) std::printf("PASS: %s\n", name);
}
void command(MyMesh& node, const char* text, const char* expected = "OK", uint32_t now = 1000) {
char reply[160] = {};
CHECK(node._notifications.handle(text, reply, sizeof(reply), now, false));
CHECK(!std::strcmp(reply, expected));
}
void expectPinFamily() {
for (unsigned pin = 0; pin < 64; ++pin) {
#if EXPECT_RGB
CHECK(digital_writes[pin] == 0 && mode_writes[pin] == 0);
if (pin != 3 && pin != 24 && pin != 28) CHECK(analog_writes[pin] == 0);
#elif EXPECT_MONO
CHECK(analog_writes[pin] == 0);
if (pin != 24) CHECK(digital_writes[pin] == 0 && mode_writes[pin] == 0);
#else
CHECK(analog_writes[pin] == 0 && digital_writes[pin] == 0 && mode_writes[pin] == 0);
#endif
}
}
void expectLed(bool on) {
#if EXPECT_RGB
CHECK(levels[3] == (on ? 255u : 0u));
CHECK(levels[24] == (on ? 90u : 0u));
CHECK(levels[28] == 0);
#elif EXPECT_MONO
CHECK(levels[24] == (on ? EXPECT_ON_LEVEL : 255 - EXPECT_ON_LEVEL));
#else
(void)on;
CHECK(ioWrites() == 0);
#endif
expectPinFamily();
}
void seedPreviousLed(MyMesh& node) {
#if EXPECT_RGB
(void)node;
seedUiRgb();
CHECK(levels[3] == 200 && levels[24] == 120 && levels[28] == 90);
#elif EXPECT_MONO
node.pulse(mesh::notify::Led, true, -1);
expectLed(true);
#else
(void)node;
seedUiRgb();
CHECK(ioWrites() == 0);
#endif
}
void checkGuard(MyMesh& node, bool admitted) {
const unsigned before_admitted = ui_admitted, before_writes = ioWrites();
node.ui.userLedHandler();
CHECK(ui_admitted == before_admitted + unsigned(admitted));
CHECK(ioWrites() == before_writes);
}
[[maybe_unused]] void startAlert(MyMesh& node) {
seedPreviousLed(node);
command(node, "set notify.led all 50,50");
command(node, "notify.test all", "OK - alert test started");
CHECK(node._notifications.active());
CHECK(node._notifications.ownsLed());
}
} // namespace
int main() {
run("capabilities", [] {
MyMesh node;
CHECK(bool(node.capabilities() & mesh::notify::Led) == bool(EXPECT_RGB || EXPECT_MONO));
CHECK(!(node.capabilities() & mesh::notify::Gpio));
node.ui.extra_capabilities = mesh::notify::Sound;
CHECK(node.capabilities() & mesh::notify::Sound);
node._ui = nullptr;
CHECK(bool(node.capabilities() & mesh::notify::Led) == bool(EXPECT_RGB || EXPECT_MONO));
CHECK(!(node.capabilities() & mesh::notify::Sound));
CHECK(ioWrites() == 0);
});
run("actual-ui-writer-and-ownership-guard", [] {
MyMesh node;
#if EXPECT_RGB
seedUiRgb();
CHECK(levels[3] == 200 && levels[24] == 120 && levels[28] == 90);
clearUiRgb();
CHECK(levels[3] == 0 && levels[24] == 0 && levels[28] == 0);
#else
seedUiRgb();
clearUiRgb();
CHECK(ioWrites() == 0);
#endif
checkGuard(node, true);
node.ui.setNotificationLedOwnership(true);
checkGuard(node, false);
node.ui.setNotificationLedOwnership(false);
node.ui.setNotificationOutputs(31 & ~mesh::notify::Led);
checkGuard(node, false);
expectPinFamily();
});
run("master-led-off-clears-idle-ui", [] {
MyMesh node;
seedPreviousLed(node);
command(node, "set notify.led off");
CHECK(node._notifications.settings().enabled == 1);
CHECK(node._notifications.settings().outputs == (31 & ~mesh::notify::Led));
node.syncNotifications();
CHECK(!node.ui._notification_led_owned);
CHECK(!(node.ui._notification_outputs & mesh::notify::Led));
expectLed(false);
checkGuard(node, false);
});
#if EXPECT_RGB || EXPECT_MONO
run("healthy-led-on-colors", [] {
MyMesh node;
command(node, "set notify.led on");
startAlert(node);
expectLed(true);
node.syncNotifications();
checkGuard(node, false);
node._notifications.loop(1049, false);
CHECK(node._notifications.active() && node._notifications.ownsLed());
expectLed(true);
});
run("inactive-pulse-phase-stays-owned", [] {
MyMesh node;
startAlert(node);
node._notifications.loop(1050, false);
CHECK(node._notifications.active() && node._notifications.ownsLed());
expectLed(false);
node.syncNotifications();
checkGuard(node, false);
node._notifications.loop(1099, false);
CHECK(node._notifications.active() && node._notifications.ownsLed());
expectLed(false);
});
run("command-stop-from-active-phase", [] {
MyMesh node;
startAlert(node);
command(node, "notify.stop", "OK - alerts stopped", 1020);
CHECK(!node._notifications.active() && !node._notifications.ownsLed());
expectLed(false); // Check before releasing the heartbeat guard.
const unsigned before_writes = ioWrites();
command(node, "notify.stop", "OK - alerts stopped", 1021);
CHECK(ioWrites() == before_writes);
node.syncNotifications();
checkGuard(node, true);
});
run("command-stop-from-inactive-phase", [] {
MyMesh node;
startAlert(node);
node._notifications.loop(1050, false);
command(node, "notify.stop", "OK - alerts stopped", 1075);
CHECK(!node._notifications.active() && !node._notifications.ownsLed());
expectLed(false);
node.syncNotifications();
checkGuard(node, true);
});
run("natural-completion-clears-before-ui-release", [] {
MyMesh node;
startAlert(node);
node._notifications.loop(1100, false);
CHECK(!node._notifications.active() && !node._notifications.ownsLed());
expectLed(false);
node.syncNotifications();
checkGuard(node, true);
});
run("master-led-off-stops-active-alert", [] {
MyMesh node;
startAlert(node);
command(node, "set notify.led off", "OK", 1020);
CHECK(!node._notifications.active() && !node._notifications.ownsLed());
node.syncNotifications();
expectLed(false);
checkGuard(node, false);
});
run("board-led-policy-overrides-alerts-and-heartbeat", [] {
MyMesh node;
mesh::leds::setEnabled(false);
command(node, "set notify.led all 50,50");
command(node, "notify.test all", "OK - alert test started");
expectLed(false);
checkGuard(node, false);
seedUiRgb();
expectLed(false);
node._notifications.loop(1050, false);
node._notifications.loop(1100, false);
mesh::leds::setEnabled(true);
node.syncNotifications();
checkGuard(node, true);
node.pulse(mesh::notify::Led, true, -1);
expectLed(true);
node.pulse(mesh::notify::Led, false, -1);
});
run("disabled-feature-restores-ui-policy", [] {
MyMesh node;
startAlert(node);
command(node, "set notify.enabled off", "OK", 1020);
CHECK(!node._notifications.active() && !node._notifications.ownsLed());
expectLed(false);
node.syncNotifications();
CHECK(node.ui._notification_outputs == 31);
checkGuard(node, true);
});
#else
run("unsupported-led-commands-and-direct-sink-are-inert", [] {
MyMesh node;
unsigned char before[sizeof(mesh::notify::Settings)];
std::memcpy(before, &node._notifications.settings(), sizeof(before));
command(node, "set notify.led on", "Error: output unsupported on this build");
command(node, "set notify.led all 50,50", "Error: output unsupported on this build");
CHECK(node.saves == 0);
CHECK(!std::memcmp(before, &node._notifications.settings(), sizeof(before)));
CHECK(!node._notifications.message(nullptr, nullptr, false, 1000));
node.pulse(mesh::notify::Led, true, -1);
node.pulse(mesh::notify::Led, false, -1);
expectLed(false);
command(node, "set notify.led all off");
CHECK(node.saves == 1);
expectLed(false);
});
#endif
std::printf("%u scenarios, %u checks, %u failures\n", scenarios, checks, failures);
return failures ? 1 : 0;
}