Files
MeshCore/test/test_status_led_policy.py
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

135 lines
5.4 KiB
Python

"""Run the common LED policy and Xiao S3 LED initialization with fake GPIO."""
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
ARDUINO = r'''
#pragma once
#include <cassert>
#include <vector>
#define LOW 0
#define HIGH 1
#define INPUT 0
#define OUTPUT 1
struct Event { int kind, pin, value; };
extern std::vector<Event> events;
inline void digitalWrite(int pin, int value) { events.push_back({0, pin, value}); }
inline void pinMode(int pin, int mode) { events.push_back({1, pin, mode}); }
inline void analogWrite(int pin, int value) { events.push_back({2, pin, value}); }
'''
HARNESS = r'''
#include <Arduino.h>
#include <target.h>
#include <helpers/LedPolicy.h>
std::vector<Event> events;
int main() {
// Enabling permits the existing heartbeat owner to run; it does not light
// an idle LED or change its output mode as a side effect of the command.
assert(mesh::leds::enabled());
mesh::leds::applyStatusPolicy();
assert(events.empty());
mesh::leds::setEnabled(false);
mesh::leds::applyStatusPolicy();
assert(!mesh::leds::enabled());
assert(events.size() == EXPECT_EVENTS);
#if EXPECT_EVENTS == 2
assert(events[0].kind == 0 && events[0].pin == PIN_STATUS_LED);
assert(events[0].value == EXPECT_OFF);
assert(events[1].kind == 1 && events[1].pin == PIN_STATUS_LED);
assert(events[1].value == OUTPUT); // Set the off latch before enabling output.
#elif EXPECT_EVENTS == 3
const int pins[] = {PIN_STATUS_LED_R, PIN_STATUS_LED_G, PIN_STATUS_LED_B};
for (unsigned i = 0; i < 3; ++i)
assert(events[i].kind == 2 && events[i].pin == pins[i] && events[i].value == 0);
#endif
events.clear();
mesh::leds::setEnabled(true);
mesh::leds::applyStatusPolicy();
assert(mesh::leds::enabled() && events.empty());
}
'''
def compile_run(case, source, defines):
compiler = shutil.which("c++") or shutil.which("g++")
case.assertIsNotNone(compiler, "run this host suite where c++ is available")
with tempfile.TemporaryDirectory(prefix="mesh-status-led-") as folder:
folder = Path(folder)
(folder / "Arduino.h").write_text(ARDUINO)
(folder / "target.h").write_text("#pragma once\n" + "\n".join(
"#define " + define.replace("=", " ", 1) for define in defines))
(folder / "case.cpp").write_text(source)
binary = folder / "case"
built = subprocess.run([
compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
"-I", str(folder), "-I", str(ROOT / "src"),
str(ROOT / "src/helpers/LedPolicy.cpp"), str(folder / "case.cpp"),
"-o", str(binary)], text=True, capture_output=True, timeout=30)
case.assertEqual(built.returncode, 0, built.stderr)
ran = subprocess.run([str(binary)], text=True, capture_output=True, timeout=10)
case.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
class StatusLedPolicyTests(unittest.TestCase):
def test_off_is_immediate_and_on_leaves_heartbeat_in_control(self):
cases = (
("active-high", ["PIN_STATUS_LED=12", "LED_STATE_ON=HIGH",
"EXPECT_OFF=LOW", "EXPECT_EVENTS=2"]),
("active-low", ["PIN_STATUS_LED=12", "LED_STATE_ON=LOW",
"EXPECT_OFF=HIGH", "EXPECT_EVENTS=2"]),
("default-polarity", ["PIN_STATUS_LED=12", "EXPECT_OFF=LOW",
"EXPECT_EVENTS=2"]),
("absent", ["EXPECT_EVENTS=0"]),
("negative-pin", ["PIN_STATUS_LED=-1", "EXPECT_EVENTS=0"]),
("rgb", ["PIN_STATUS_LED_R=3", "PIN_STATUS_LED_G=24",
"PIN_STATUS_LED_B=28", "PIN_STATUS_LED=24", "EXPECT_EVENTS=3"]),
)
for name, defines in cases:
with self.subTest(board=name):
compile_run(self, HARNESS, defines)
def test_xiao_s3_uses_active_low_output_during_radio_init(self):
ini = (ROOT / "variants/xiao_s3/platformio.ini").read_text()
names = ("PIN_STATUS_LED", "LED_STATE_ON", "P_LORA_SCLK",
"P_LORA_MISO", "P_LORA_MOSI")
definitions = {name: re.findall(r"-D\s+" + name + r"=([^\s;]+)", ini)
for name in names}
self.assertEqual(definitions["PIN_STATUS_LED"], ["21"])
self.assertEqual(definitions["LED_STATE_ON"], ["LOW"])
for name in names:
self.assertEqual(len(definitions[name]), 1, name)
method = extract_braced((ROOT / "variants/xiao_s3/target.cpp").read_text(),
"bool radio_init()")
source = r'''
#include <Arduino.h>
#include <target.h>
std::vector<Event> events;
struct Clock { template<class... T> void begin(T...) {} } fallback_clock, rtc_clock;
struct Spi { void begin(int, int, int) {} } spi;
struct Radio { bool std_init(Spi*) { return true; } } radio;
int Wire = 0;
@METHOD@
int main() {
assert(radio_init());
assert(events.size() == 3);
assert(events[0].kind == 0 && events[0].pin == 21 && events[0].value == HIGH);
assert(events[1].kind == 1 && events[1].pin == 21 && events[1].value == OUTPUT);
assert(events[2].kind == 1 && events[2].pin == 48 && events[2].value == OUTPUT);
}
'''
compile_run(self, source.replace("@METHOD@", method),
[name + "=" + definitions[name][0] for name in names])
if __name__ == "__main__":
unittest.main()