mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 13:17:15 +00:00
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>
201 lines
6.6 KiB
C++
201 lines
6.6 KiB
C++
#include <Arduino.h>
|
|
#include <algorithm>
|
|
#include <stdexcept>
|
|
|
|
#define LED_STATE_ON 1
|
|
#define LED_BLUE 36
|
|
#define LED_BUILTIN 35
|
|
#define LED_PIN LED_BUILTIN
|
|
#define LED_GREEN LED_BUILTIN
|
|
#define LED_RED (-1)
|
|
#define P_LORA_RESET 40
|
|
#define P_LORA_BUSY 41
|
|
#define P_LORA_NSS 42
|
|
#define P_LORA_DIO_1 43
|
|
#define P_LORA_SCLK 44
|
|
#define P_LORA_MISO 45
|
|
#define P_LORA_MOSI 46
|
|
#define DISPLAY_CLASS 1
|
|
#define HAS_GPS 1
|
|
#define GPS_VRTC_EN 10
|
|
#define GPS_RESET 11
|
|
#define GPS_SLEEP_INT 12
|
|
#define GPS_RTC_INT 13
|
|
#define GPS_EN 14
|
|
#define PIN_DRV_EN 15
|
|
#define PIN_BAT_ADC_EN 16
|
|
#define PIN_3V3_EN 17
|
|
#define MESH_DEBUG_PRINTLN(...) do {} while (0)
|
|
#define POWER_SYSTEMOFF_SYSTEMOFF_Enter 1
|
|
#define NRF_ERROR_SOFTDEVICE_NOT_ENABLED 2
|
|
#define pdMS_TO_TICKS(ms) (ms)
|
|
|
|
#include <helpers/nrf52/ShutdownLeds.h>
|
|
#include <helpers/LedPolicy.h>
|
|
|
|
std::vector<Event> events;
|
|
int levels[PINS_COUNT];
|
|
bool timer_active = false, timer_stop_pending = false, automatic_owner = true;
|
|
bool ble_started = false;
|
|
uint8_t sd_enabled_mock = 0;
|
|
uint32_t now = 0;
|
|
struct Exit {};
|
|
|
|
void event(const char* name) { events.push_back({name, 0, 0}); }
|
|
struct BluefruitMock {
|
|
void autoConnLed(bool enabled) {
|
|
assert(!enabled);
|
|
event("stop-auto-led");
|
|
automatic_owner = enabled;
|
|
// The real SDK accepts this before begin(), when the timer handle is NULL.
|
|
if (ble_started) timer_stop_pending = true;
|
|
}
|
|
} Bluefruit;
|
|
|
|
uint32_t millis() { return now; }
|
|
int digitalRead(int) { return LOW; }
|
|
void detachInterrupt(int) { event("detach-radio-irq"); }
|
|
void delayMicroseconds(int) {}
|
|
void delay(unsigned ms) {
|
|
now += ms;
|
|
if (timer_active) {
|
|
// SDK blinky_cb does not test _led_conn: a callback already ready to run
|
|
// can still execute before the queued timer stop is processed.
|
|
event("pending-blinky");
|
|
digitalWrite(LED_BLUE, !levels[LED_BLUE]);
|
|
}
|
|
if (timer_stop_pending) {
|
|
timer_active = false;
|
|
timer_stop_pending = false;
|
|
event("timer-stopped");
|
|
}
|
|
}
|
|
void vTaskDelay(uint32_t) { event("timed-sleep"); }
|
|
void NVIC_SystemReset() { event("reset"); throw Exit{}; }
|
|
namespace mesh_nrf52 {
|
|
int softdeviceIsEnabled(uint8_t& enabled) { enabled = sd_enabled_mock; return 0; }
|
|
}
|
|
struct Register {
|
|
Register& operator=(uint32_t) { event("systemoff"); return *this; }
|
|
};
|
|
struct PowerRegisters { Register SYSTEMOFF; uint8_t GPREGRET2; } power_registers;
|
|
PowerRegisters* NRF_POWER = &power_registers;
|
|
int sd_power_gpregret_clr(int, int) { return 0; }
|
|
int sd_power_gpregret_set(int, int) { return 0; }
|
|
uint32_t sd_power_system_off() { event("systemoff"); throw Exit{}; }
|
|
struct SerialMock { void flush() { event("flush"); } } Serial;
|
|
struct SpiMock { void setPins(int,int,int) {} void begin() { event("spi"); } } SPI;
|
|
struct RadioMock { void powerOff() { event("radio-off"); } } radio_driver;
|
|
struct LocationMock { void stop() { event("gps-off"); } } location;
|
|
struct SensorsMock { LocationMock* getLocationProvider() { return &location; } } sensors;
|
|
struct DisplayMock {
|
|
bool isOn() { return true; }
|
|
void turnOff() { event("display-off"); }
|
|
} display;
|
|
|
|
class NRF52Board {
|
|
public:
|
|
virtual ~NRF52Board() {}
|
|
virtual void shutdownPeripherals();
|
|
virtual void powerOff();
|
|
void enterDeepSleep(uint32_t secs);
|
|
void enterSystemOff(uint8_t reason);
|
|
void reboot() { NVIC_SystemReset(); }
|
|
};
|
|
class MeshTrackerX1Board : public NRF52Board {
|
|
public:
|
|
void shutdownPeripherals() override;
|
|
};
|
|
|
|
@METHODS@
|
|
@X1_HOOK@
|
|
|
|
size_t first(const char* name) {
|
|
for (size_t i = 0; i < events.size(); ++i) {
|
|
if (events[i].name == name) return i;
|
|
}
|
|
return events.size();
|
|
}
|
|
size_t lastWrite(int pin) {
|
|
size_t result = events.size();
|
|
for (size_t i = 0; i < events.size(); ++i) {
|
|
if (events[i].name == "write" && events[i].pin == pin) result = i;
|
|
}
|
|
return result;
|
|
}
|
|
void prepare(bool started, bool connected, bool sd) {
|
|
events.clear();
|
|
for (int& level : levels) level = LED_STATE_ON;
|
|
now = 0;
|
|
ble_started = started;
|
|
// Advertising uses a timer; a connected central leaves a solid LED on.
|
|
timer_active = started && !connected;
|
|
timer_stop_pending = false;
|
|
automatic_owner = true;
|
|
sd_enabled_mock = sd;
|
|
}
|
|
void verify(bool had_timer, bool peripherals, const char* exit_event) {
|
|
assert(!automatic_owner && !timer_active && !timer_stop_pending);
|
|
assert(levels[LED_BLUE] == !LED_STATE_ON);
|
|
assert(levels[LED_BUILTIN] == !LED_STATE_ON);
|
|
assert(first("stop-auto-led") < lastWrite(LED_BLUE));
|
|
if (had_timer) {
|
|
assert(first("stop-auto-led") < first("pending-blinky"));
|
|
assert(first("timer-stopped") < lastWrite(LED_BLUE));
|
|
}
|
|
if (peripherals) {
|
|
assert(first("radio-off") < first("gps-off"));
|
|
assert(first("gps-off") < lastWrite(LED_BLUE));
|
|
} else {
|
|
assert(first("radio-off") == events.size());
|
|
assert(first("gps-off") == events.size());
|
|
}
|
|
if (exit_event) assert(lastWrite(LED_BLUE) < first(exit_event));
|
|
// Subsequent BLE event writes are gated by the same _led_conn flag as SDK.
|
|
if (automatic_owner) digitalWrite(LED_BLUE, LED_STATE_ON);
|
|
assert(levels[LED_BLUE] == !LED_STATE_ON);
|
|
}
|
|
|
|
int main() {
|
|
NRF52Board base;
|
|
MeshTrackerX1Board tracker;
|
|
for (bool started : {false, true}) {
|
|
for (bool connected : {false, true}) {
|
|
for (bool sd : {false, true}) {
|
|
const bool had_timer = started && !connected;
|
|
prepare(started, connected, sd);
|
|
base.shutdownPeripherals();
|
|
verify(had_timer, true, nullptr);
|
|
// Repeating shutdown is safe, including a never-initialized BLE stack.
|
|
base.shutdownPeripherals();
|
|
verify(had_timer, true, nullptr);
|
|
|
|
prepare(started, connected, sd);
|
|
try { base.powerOff(); } catch (const Exit&) {}
|
|
verify(had_timer, true, "systemoff");
|
|
prepare(started, connected, sd);
|
|
try { base.enterDeepSleep(0); } catch (const Exit&) {}
|
|
verify(had_timer, true, "systemoff");
|
|
prepare(started, connected, sd);
|
|
try { base.enterDeepSleep(10); } catch (const Exit&) {}
|
|
verify(had_timer, true, "timed-sleep");
|
|
|
|
// Early low-voltage boot shutdown must not start GPS/radio peripherals.
|
|
prepare(started, connected, sd);
|
|
try { base.enterSystemOff(42); } catch (const Exit&) {}
|
|
verify(had_timer, false, "systemoff");
|
|
|
|
// Regression for #3567: inherited powerOff must dispatch the X1 hook.
|
|
prepare(started, connected, sd);
|
|
try { tracker.powerOff(); } catch (const Exit&) {}
|
|
verify(had_timer, true, "systemoff");
|
|
for (int pin : {GPS_VRTC_EN, GPS_RESET, GPS_SLEEP_INT, GPS_RTC_INT,
|
|
GPS_EN, PIN_DRV_EN, PIN_BAT_ADC_EN, PIN_3V3_EN}) {
|
|
assert(levels[pin] == LOW);
|
|
assert(lastWrite(pin) < first("systemoff"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|