feat: add LED indicators and headless mode support for V3/V4
- Detect missing OLED at boot, set headless_mode flag - LED solid: normal operation (radio online) - LED fast blink: button held >5s (entering WCC config mode) - LED slow breathe: WiFi Captive Configure portal active - Allow 1-3s button press in WCC mode to power off - Next boot after WCC power-off skips config portal (unless unconfigured) - LED indicators active on both V3 and V4, with or without display - Clean up LED PWM on deep sleep
This commit is contained in:
@@ -557,6 +557,10 @@ void config_portal_start() {
|
|||||||
display.display();
|
display.display();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
// Headless: LED ramp will be driven from the WCC portal loop
|
||||||
|
if (headless_mode) {
|
||||||
|
Serial.println("[Config] Headless mode — LED will breathe during config portal");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Stop Config Portal ──────────────────────────────────────────────────────
|
// ─── Stop Config Portal ──────────────────────────────────────────────────────
|
||||||
|
|||||||
1
Config.h
1
Config.h
@@ -145,6 +145,7 @@
|
|||||||
bool hw_ready = false;
|
bool hw_ready = false;
|
||||||
bool radio_error = false;
|
bool radio_error = false;
|
||||||
bool disp_ready = false;
|
bool disp_ready = false;
|
||||||
|
bool headless_mode = false;
|
||||||
bool pmu_ready = false;
|
bool pmu_ready = false;
|
||||||
bool promisc = false;
|
bool promisc = false;
|
||||||
bool implicit = false;
|
bool implicit = false;
|
||||||
|
|||||||
3
Input.h
3
Input.h
@@ -96,6 +96,9 @@
|
|||||||
display.display();
|
display.display();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
headless_led_fast_blink();
|
||||||
|
} else if (display_lock_white) {
|
||||||
|
headless_led_fast_blink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,9 @@ TcpInterface* local_tcp_interface_ptr = nullptr;
|
|||||||
// RTC memory flag — survives software reset but not power cycle
|
// RTC memory flag — survives software reset but not power cycle
|
||||||
RTC_NOINIT_ATTR uint32_t boundary_config_request;
|
RTC_NOINIT_ATTR uint32_t boundary_config_request;
|
||||||
#define BOUNDARY_CONFIG_MAGIC 0xC0F19A7E
|
#define BOUNDARY_CONFIG_MAGIC 0xC0F19A7E
|
||||||
|
// RTC flag to skip config portal on next boot (set when user powers off from WCC)
|
||||||
|
RTC_NOINIT_ATTR uint32_t boundary_skip_config;
|
||||||
|
#define BOUNDARY_SKIP_MAGIC 0x5E1FC0F0
|
||||||
|
|
||||||
// Bootloop detection: count rapid reboots in RTC memory.
|
// Bootloop detection: count rapid reboots in RTC memory.
|
||||||
// After BOOTLOOP_THRESHOLD consecutive reboots within BOOTLOOP_WINDOW_MS,
|
// After BOOTLOOP_THRESHOLD consecutive reboots within BOOTLOOP_WINDOW_MS,
|
||||||
@@ -473,7 +476,17 @@ void setup() {
|
|||||||
|
|
||||||
display_unblank();
|
display_unblank();
|
||||||
disp_ready = display_init();
|
disp_ready = display_init();
|
||||||
|
if (disp_ready) {
|
||||||
update_display();
|
update_display();
|
||||||
|
} else {
|
||||||
|
headless_mode = true;
|
||||||
|
Serial.println("[Headless] No display detected — running in headless mode");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// LED solid on at boot for V3/V4 boards (with or without display)
|
||||||
|
#if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3
|
||||||
|
headless_led_solid();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ── Boundary Mode: check if config portal is needed ──
|
// ── Boundary Mode: check if config portal is needed ──
|
||||||
@@ -514,7 +527,16 @@ void setup() {
|
|||||||
// OR bootloop detected
|
// OR bootloop detected
|
||||||
bool need_config = boundary_needs_config();
|
bool need_config = boundary_needs_config();
|
||||||
bool config_requested = (boundary_config_request == BOUNDARY_CONFIG_MAGIC);
|
bool config_requested = (boundary_config_request == BOUNDARY_CONFIG_MAGIC);
|
||||||
|
bool skip_config = (boundary_skip_config == BOUNDARY_SKIP_MAGIC);
|
||||||
boundary_config_request = 0; // Clear flag immediately
|
boundary_config_request = 0; // Clear flag immediately
|
||||||
|
boundary_skip_config = 0; // Clear skip flag immediately
|
||||||
|
|
||||||
|
// Skip flag only suppresses a button-triggered re-entry, not a genuinely
|
||||||
|
// unconfigured device. If there's no config saved, always show the portal.
|
||||||
|
if (skip_config && config_requested) {
|
||||||
|
Serial.println("[Boundary] Skipping config portal — user requested normal boot");
|
||||||
|
config_requested = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (need_config || config_requested || bootloop_detected) {
|
if (need_config || config_requested || bootloop_detected) {
|
||||||
if (bootloop_detected) {
|
if (bootloop_detected) {
|
||||||
@@ -526,8 +548,39 @@ void setup() {
|
|||||||
}
|
}
|
||||||
config_portal_start();
|
config_portal_start();
|
||||||
// Block here: only run the config portal until user saves and device reboots
|
// Block here: only run the config portal until user saves and device reboots
|
||||||
|
// Track button state for "off" action (1-3s press = sleep)
|
||||||
|
bool wcc_btn_down = false;
|
||||||
|
uint32_t wcc_btn_down_at = 0;
|
||||||
while (config_portal_is_active()) {
|
while (config_portal_is_active()) {
|
||||||
config_portal_loop();
|
config_portal_loop();
|
||||||
|
|
||||||
|
// Headless LED: slow ramp breathe effect during WCC mode
|
||||||
|
headless_led_ramp();
|
||||||
|
|
||||||
|
// Button handling: allow 1-3s press to turn off (deep sleep)
|
||||||
|
// Next power-on boots to normal mode since boundary_config_request is cleared
|
||||||
|
#if HAS_INPUT
|
||||||
|
{
|
||||||
|
int btn = digitalRead(pin_btn_usr1);
|
||||||
|
if (btn == LOW && !wcc_btn_down) {
|
||||||
|
wcc_btn_down = true;
|
||||||
|
wcc_btn_down_at = millis();
|
||||||
|
} else if (btn == HIGH && wcc_btn_down) {
|
||||||
|
uint32_t held = millis() - wcc_btn_down_at;
|
||||||
|
wcc_btn_down = false;
|
||||||
|
if (held >= 700 && held <= 5000) {
|
||||||
|
Serial.println("[Boundary] Button press in WCC mode — powering off");
|
||||||
|
boundary_skip_config = BOUNDARY_SKIP_MAGIC; // Skip config on next boot
|
||||||
|
headless_led_off();
|
||||||
|
config_portal_stop();
|
||||||
|
#if HAS_SLEEP
|
||||||
|
sleep_now();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if MCU_VARIANT == MCU_ESP32
|
#if MCU_VARIANT == MCU_ESP32
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
#endif
|
#endif
|
||||||
@@ -2511,6 +2564,13 @@ void loop() {
|
|||||||
if (disp_ready && !display_updating) update_display();
|
if (disp_ready && !display_updating) update_display();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// LED solid when operational on V3/V4 boards (yield to fast blink during white screen)
|
||||||
|
#if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3
|
||||||
|
if (radio_online && !display_lock_white) {
|
||||||
|
headless_led_solid();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if HAS_PMU
|
#if HAS_PMU
|
||||||
if (pmu_ready) update_pmu();
|
if (pmu_ready) update_pmu();
|
||||||
#endif
|
#endif
|
||||||
@@ -2558,6 +2618,8 @@ void sleep_now() {
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
#if BOARD_MODEL == BOARD_HELTEC32_V4
|
#if BOARD_MODEL == BOARD_HELTEC32_V4
|
||||||
|
headless_led_off();
|
||||||
|
headless_led_detach_pwm();
|
||||||
digitalWrite(LORA_PA_CPS, LOW);
|
digitalWrite(LORA_PA_CPS, LOW);
|
||||||
digitalWrite(LORA_PA_CSD, LOW);
|
digitalWrite(LORA_PA_CSD, LOW);
|
||||||
digitalWrite(LORA_PA_PWR_EN, LOW);
|
digitalWrite(LORA_PA_PWR_EN, LOW);
|
||||||
|
|||||||
77
Utilities.h
77
Utilities.h
@@ -72,6 +72,10 @@ uint8_t eeprom_read(uint32_t mapped_addr);
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if HAS_INPUT == true
|
#if HAS_INPUT == true
|
||||||
|
// Forward declarations for headless LED functions (defined later in this file)
|
||||||
|
void headless_led_fast_blink();
|
||||||
|
void headless_led_ramp();
|
||||||
|
void headless_led_off();
|
||||||
#include "Input.h"
|
#include "Input.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -382,6 +386,79 @@ extern RNS::Reticulum reticulum;
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// ── Headless LED indicators (for Heltec V4 without OLED) ─────────────────
|
||||||
|
// Uses LEDC PWM for smooth ramp effects on pin_led_tx (GPIO 35)
|
||||||
|
#if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3
|
||||||
|
#define HEADLESS_LED_CHANNEL 0
|
||||||
|
bool headless_led_pwm_attached = false;
|
||||||
|
|
||||||
|
void headless_led_ensure_pwm() {
|
||||||
|
if (!headless_led_pwm_attached) {
|
||||||
|
ledcSetup(HEADLESS_LED_CHANNEL, 5000, 8); // channel 0, 5kHz, 8-bit
|
||||||
|
ledcAttachPin(pin_led_tx, HEADLESS_LED_CHANNEL);
|
||||||
|
headless_led_pwm_attached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void headless_led_detach_pwm() {
|
||||||
|
if (headless_led_pwm_attached) {
|
||||||
|
ledcDetachPin(pin_led_tx);
|
||||||
|
headless_led_pwm_attached = false;
|
||||||
|
pinMode(pin_led_tx, OUTPUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solid ON — normal headless operation
|
||||||
|
void headless_led_solid() {
|
||||||
|
headless_led_ensure_pwm();
|
||||||
|
ledcWrite(HEADLESS_LED_CHANNEL, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast blink — replaces "white screen" indicator (non-blocking, call from loop)
|
||||||
|
void headless_led_fast_blink() {
|
||||||
|
headless_led_ensure_pwm();
|
||||||
|
static uint32_t last_toggle = 0;
|
||||||
|
static bool on = false;
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now - last_toggle >= 100) { // 5Hz blink
|
||||||
|
last_toggle = now;
|
||||||
|
on = !on;
|
||||||
|
ledcWrite(HEADLESS_LED_CHANNEL, on ? 255 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow ramp in/out — breathe effect for WiFi Captive Configure mode
|
||||||
|
void headless_led_ramp() {
|
||||||
|
headless_led_ensure_pwm();
|
||||||
|
static uint32_t last_step = 0;
|
||||||
|
static uint8_t brightness = 0;
|
||||||
|
static int8_t direction = 1;
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now - last_step >= 10) { // ~100 steps/sec, full cycle ~5 seconds
|
||||||
|
last_step = now;
|
||||||
|
brightness += direction;
|
||||||
|
if (brightness >= 255) { brightness = 255; direction = -1; }
|
||||||
|
if (brightness == 0) { direction = 1; }
|
||||||
|
ledcWrite(HEADLESS_LED_CHANNEL, brightness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void headless_led_off() {
|
||||||
|
if (headless_led_pwm_attached) {
|
||||||
|
ledcWrite(HEADLESS_LED_CHANNEL, 0);
|
||||||
|
} else {
|
||||||
|
digitalWrite(pin_led_tx, LOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
void headless_led_ensure_pwm() {}
|
||||||
|
void headless_led_detach_pwm() {}
|
||||||
|
void headless_led_solid() {}
|
||||||
|
void headless_led_fast_blink() {}
|
||||||
|
void headless_led_ramp() {}
|
||||||
|
void headless_led_off() {}
|
||||||
|
#endif
|
||||||
|
|
||||||
void hard_reset(void) {
|
void hard_reset(void) {
|
||||||
#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
|
#if MCU_VARIANT == MCU_1284P || MCU_VARIANT == MCU_2560
|
||||||
wdt_enable(WDTO_15MS);
|
wdt_enable(WDTO_15MS);
|
||||||
|
|||||||
Reference in New Issue
Block a user