led switcher page in ui

rx duty cycle + noise floor measurement marriage
ui fixes
This commit is contained in:
liquidraver
2026-03-06 22:15:13 +01:00
parent 1f48735ae6
commit 2584e237f6
15 changed files with 229 additions and 179 deletions
+5 -18
View File
@@ -192,29 +192,14 @@ config ZEPHCORE_BLE_CONN_TIMEOUT
Supervision timeout in 10ms units.
500 = 5000ms. Connection drops if no data for this duration.
config ZEPHCORE_BLE_ADV_FAST_INTERVAL
int "BLE fast advertising interval (units: 0.625ms)"
default 32
range 20 1600
help
Fast advertising interval in 0.625ms units.
32 = 20ms (Apple-recommended for quick discovery).
config ZEPHCORE_BLE_ADV_SLOW_INTERVAL
int "BLE slow advertising interval (units: 0.625ms)"
int "BLE advertising interval (units: 0.625ms)"
default 338
range 244 3200
help
Slow advertising interval in 0.625ms units.
Advertising interval in 0.625ms units.
338 = 211.25ms (Apple-compliant, balanced discovery/battery).
config ZEPHCORE_BLE_ADV_FAST_TIMEOUT
int "Duration of fast advertising mode (seconds)"
default 30
range 10 300
help
How long to advertise at the fast rate before switching to slow.
endmenu # BLE Configuration
endif # ZEPHCORE_ROLE_COMPANION
@@ -441,9 +426,11 @@ config ZEPHCORE_UI_BUZZER
Enable PWM buzzer with RTTTL melody support.
Requires a PWM-capable pin connected to a buzzer.
DT_CHOSEN_ZEPHCORE_DISPLAY := zephyr,display
config ZEPHCORE_UI_DISPLAY
bool "Enable display (OLED, LCD, e-ink)"
default y if $(dt_chosen_enabled,zephyr/display)
default y if $(dt_chosen_enabled,$(DT_CHOSEN_ZEPHCORE_DISPLAY))
select DISPLAY
select CHARACTER_FRAMEBUFFER
help
+5 -76
View File
@@ -48,10 +48,8 @@ LOG_MODULE_REGISTER(zephcore_ble, CONFIG_ZEPHCORE_BLE_LOG_LEVEL);
* BLE stack when the link is marginal, fast enough to recover quickly. */
#define BLE_TX_OVERFLOW_RETRY_MS 250
/* Advertising intervals (Apple Accessory Design Guidelines) */
#define BT_ADV_INTERVAL_FAST CONFIG_ZEPHCORE_BLE_ADV_FAST_INTERVAL
#define BT_ADV_INTERVAL_SLOW CONFIG_ZEPHCORE_BLE_ADV_SLOW_INTERVAL
#define BT_ADV_FAST_TIMEOUT_SEC CONFIG_ZEPHCORE_BLE_ADV_FAST_TIMEOUT
/* Advertising interval (Apple Accessory Design Guidelines) */
#define BT_ADV_INTERVAL CONFIG_ZEPHCORE_BLE_ADV_SLOW_INTERVAL
/* ========== Frame type for queues ========== */
@@ -118,10 +116,6 @@ static enum zephcore_iface active_iface = ZEPHCORE_IFACE_NONE;
/* DLE tracking — set after successful DLE request to avoid double-request */
static bool dle_requested;
/* Advertising state */
static bool adv_switching = false;
static bool adv_is_slow = false;
static bool adv_post_disconnect = false; /* skip fast advert after disconnect */
/* Runtime BLE passkey */
static uint32_t ble_passkey = CONFIG_ZEPHCORE_BLE_PASSKEY;
@@ -176,11 +170,9 @@ STRUCT_SECTION_ITERABLE(bt_nus_inst, secure_nus) = {
/* ========== Work items ========== */
static void tx_drain_work_fn(struct k_work *work);
static void adv_slow_work_fn(struct k_work *work);
static void overflow_retry_work_fn(struct k_work *work);
K_WORK_DELAYABLE_DEFINE(tx_drain_work, tx_drain_work_fn);
K_WORK_DELAYABLE_DEFINE(adv_slow_work, adv_slow_work_fn);
K_WORK_DELAYABLE_DEFINE(overflow_retry_work, overflow_retry_work_fn);
/* ========== TX completion callback ========== */
@@ -270,10 +262,6 @@ static void connected(struct bt_conn *conn, uint8_t err)
LOG_INF("connected: %s", addr);
current_conn = bt_conn_ref(conn);
/* Cancel slow advertising work - we're connected now */
k_work_cancel_delayable(&adv_slow_work);
adv_is_slow = false;
/* DLE is NOT requested here — the phone may start a PHY update LL
* procedure immediately, and BLE allows only one at a time.
* DLE is deferred to le_phy_updated() (after PHY negotiation completes)
@@ -341,34 +329,14 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
k_work_cancel_delayable(&tx_drain_work);
k_work_cancel_delayable(&overflow_retry_work);
/* Skip fast advertising on reconnect — go straight to slow.
* Prevents tight reconnect flapping loops on flaky links. */
adv_post_disconnect = true;
/* Notify main of BLE disconnection */
if (ble_cbs && ble_cbs->on_disconnected) {
ble_cbs->on_disconnected();
}
}
/* Flag to suppress recycled() callback during adv mode switch */
static void recycled(void)
{
if (adv_switching || adv_is_slow) {
LOG_DBG("suppressed (switching=%d slow=%d)",
adv_switching, adv_is_slow);
return;
}
if (adv_post_disconnect) {
/* After disconnect, skip fast advertising — go straight to slow.
* Prevents rapid reconnect flapping on flaky BLE links. */
adv_post_disconnect = false;
LOG_INF("post-disconnect: starting slow advertising directly");
adv_slow_work_fn(NULL);
return;
}
LOG_DBG("restart advertising");
start_adv();
}
@@ -784,53 +752,18 @@ static ssize_t secure_nus_rx_write(struct bt_conn *conn, const struct bt_gatt_at
static void start_adv(void)
{
/* Start with fast 20ms advertising for quick discovery */
struct bt_le_adv_param adv_param = {
.id = BT_ID_DEFAULT,
.options = BT_LE_ADV_OPT_CONN,
.interval_min = BT_ADV_INTERVAL_FAST,
.interval_max = BT_ADV_INTERVAL_FAST,
.interval_min = BT_ADV_INTERVAL,
.interval_max = BT_ADV_INTERVAL,
};
adv_is_slow = false;
int err = bt_le_adv_start(&adv_param, ad, ad_len, sd, sd_len);
if (err && err != -EALREADY) {
LOG_ERR("adv start failed: %d", err);
} else {
LOG_INF("BLE advertising: fast mode (20ms) for %ds", BT_ADV_FAST_TIMEOUT_SEC);
/* Schedule switch to slow mode after timeout */
k_work_schedule(&adv_slow_work, K_SECONDS(BT_ADV_FAST_TIMEOUT_SEC));
}
}
static void adv_slow_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
/* Only switch if not connected */
if (current_conn) {
return;
}
/* Stop current advertising and restart with slow interval.
* Set flag to prevent recycled() from calling start_adv() again. */
adv_switching = true;
bt_le_adv_stop();
struct bt_le_adv_param adv_param = {
.id = BT_ID_DEFAULT,
.options = BT_LE_ADV_OPT_CONN,
.interval_min = BT_ADV_INTERVAL_SLOW,
.interval_max = BT_ADV_INTERVAL_SLOW,
};
int err = bt_le_adv_start(&adv_param, ad, ad_len, sd, sd_len);
adv_switching = false;
if (err && err != -EALREADY) {
LOG_ERR("slow adv start failed: %d", err);
} else {
adv_is_slow = true;
LOG_INF("BLE advertising: slow mode (546ms)");
LOG_INF("BLE advertising: 211ms");
}
}
@@ -933,11 +866,7 @@ void zephcore_ble_set_enabled(bool enable)
BT_HCI_ERR_REMOTE_USER_TERM_CONN);
}
/* Stop advertising */
adv_switching = true;
bt_le_adv_stop();
adv_switching = false;
adv_is_slow = false;
k_work_cancel_delayable(&adv_slow_work);
LOG_INF("BLE disabled");
} else {
/* Re-enable advertising */
@@ -405,6 +405,13 @@ void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
} else {
prefs.rx_boost = 1; /* Default to boosted for better sensitivity */
}
/* Offset 93: leds_disabled (ZephCore extension) */
if (off < len) {
prefs.leds_disabled = buf[off++];
} else {
prefs.leds_disabled = 0; /* Default: LEDs on */
}
}
void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
@@ -453,7 +460,9 @@ void ZephyrDataStore::savePrefs(const NodePrefs &prefs)
buf[off++] = prefs.autoadd_max_hops;
/* Offset 92: rx_boost (ZephCore extension — Arduino ignores) */
buf[off++] = prefs.rx_boost;
/* Total: 93 bytes (Arduino reads 92, ZephCore reads 93) */
/* Offset 93: leds_disabled (ZephCore extension) */
buf[off++] = prefs.leds_disabled;
/* Total: 94 bytes (Arduino reads 92, ZephCore reads 94) */
bool ok = openWrite(PREFS_FILE, buf, off);
LOG_INF("savePrefs: wrote %s, ok=%d (%d bytes), name='%.16s'",
+95 -83
View File
@@ -505,97 +505,109 @@ void LoRaRadioBase::triggerNoiseFloorCalibrate(int threshold)
return;
}
/* Skip when duty cycle is active the radio alternates between
* short RX windows and sleep. GetRssiInst sent during the sleep
* phase hangs the SPI bus (BUSY stuck high for the full 3 s timeout)
* because the chip cannot process commands while asleep. */
/* When duty cycle is active the radio alternates between short RX
* windows and sleep. GetRssiInst sent during the sleep phase hangs
* the SPI bus (BUSY stuck high for the full 3 s timeout). Briefly
* switch to continuous RX for the sample, then restore duty cycle.
* Use do{}while(0) so the restore runs from every exit path. */
if (_rx_duty_cycle_enabled) {
return;
hwSetRxDutyCycle(false);
k_sleep(K_MSEC(2)); /* let chip settle into continuous RX */
}
/* Skip if mid-receive — don't want signal energy in the floor. */
if (isReceiving()) {
return;
}
/* Random delay 0-500 ms before sampling. Breaks phase-lock with
* periodic interference that might be synchronized with our fixed
* 5-second housekeeping cadence. */
uint32_t jitter;
sys_rand_get(&jitter, sizeof(jitter));
k_sleep(K_MSEC(jitter % 500));
/* Re-check after the delay — a packet may have arrived. */
if (isReceiving()) {
return;
}
/* Median of multiple RSSI reads (~200 us). Rejects up to N/2-1
* outliers in either direction without the downward bias of min
* or the spike sensitivity of average. Insertion sort is fine
* for N=8 (28 comparisons worst case, all in registers). */
int16_t samples[NOISE_FLOOR_SAMPLES_PER_TICK];
for (int i = 0; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
samples[i] = hwGetCurrentRSSI();
}
/* Insertion sort — tiny array, branch-friendly on Cortex-M */
for (int i = 1; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
int16_t key = samples[i];
int j = i - 1;
while (j >= 0 && samples[j] > key) {
samples[j + 1] = samples[j];
j--;
do {
/* Skip if mid-receive — don't want signal energy in the floor. */
if (isReceiving()) {
break;
}
samples[j + 1] = key;
}
int16_t rssi = (samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2 - 1] +
samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2]) / 2;
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly. */
if (_noise_floor == DEFAULT_NOISE_FLOOR) {
_noise_floor = rssi;
/* Random delay 0-500 ms before sampling. Breaks phase-lock with
* periodic interference that might be synchronized with our fixed
* 5-second housekeeping cadence. Skip when duty cycle is active:
* the chip is already out of its pattern and we want to minimise
* time spent in continuous RX. */
if (!_rx_duty_cycle_enabled) {
uint32_t jitter;
sys_rand_get(&jitter, sizeof(jitter));
k_sleep(K_MSEC(jitter % 500));
/* Re-check after the delay — a packet may have arrived. */
if (isReceiving()) {
break;
}
}
/* Median of multiple RSSI reads (~200 us). Rejects up to N/2-1
* outliers in either direction without the downward bias of min
* or the spike sensitivity of average. Insertion sort is fine
* for N=8 (28 comparisons worst case, all in registers). */
int16_t samples[NOISE_FLOOR_SAMPLES_PER_TICK];
for (int i = 0; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
samples[i] = hwGetCurrentRSSI();
}
/* Insertion sort — tiny array, branch-friendly on Cortex-M */
for (int i = 1; i < NOISE_FLOOR_SAMPLES_PER_TICK; i++) {
int16_t key = samples[i];
int j = i - 1;
while (j >= 0 && samples[j] > key) {
samples[j + 1] = samples[j];
j--;
}
samples[j + 1] = key;
}
int16_t rssi = (samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2 - 1] +
samples[NOISE_FLOOR_SAMPLES_PER_TICK / 2]) / 2;
/* First sample after reset (DEFAULT_NOISE_FLOOR == 0): seed directly. */
if (_noise_floor == DEFAULT_NOISE_FLOOR) {
_noise_floor = rssi;
if (_noise_floor < -120) _noise_floor = -120;
if (_noise_floor > -50) _noise_floor = -50;
_ema_unguarded = 0;
LOG_DBG("noise_floor_cal: seed=%d", _noise_floor);
break;
}
/* Threshold filter with warmup and periodic bypass.
*
* _ema_unguarded counts up from 0 on every tick.
* Ticks 0..W-1 (warmup): all samples accepted for fast convergence
* after seed/reset — prevents a bad seed from locking out the
* real noise floor via a too-tight threshold.
* Ticks W+: threshold filter active. Every Pth tick one sample
* bypasses the filter so the floor can track sustained upward
* shifts (new interference, antenna change).
* The EMA's 1/8 weight naturally dampens isolated spikes. */
const int W = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 — warmup ticks */
const int P = NOISE_FLOOR_UNGUARDED_INTERVAL; /* 16 — periodic interval */
bool warmup = (_ema_unguarded < W);
bool periodic = (!warmup && (_ema_unguarded & (P - 1)) == 0);
_ema_unguarded++; /* wraps at 255 — harmless */
if (!warmup && !periodic &&
rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
break;
}
/* EMA: floor += round_nearest((sample - floor) / W).
* Plain >> has downward bias (-1>>3 == -1 but +1>>3 == 0).
* Plain / has a ±7 dead zone (small drifts ignored).
* Round-to-nearest: add half the divisor before dividing,
* with sign-aware bias so both directions are symmetric. */
int diff = rssi - _noise_floor;
int half = W / 2; /* 4 */
int step = (diff + (diff > 0 ? half : -half)) / W;
_noise_floor += step;
if (_noise_floor < -120) _noise_floor = -120;
if (_noise_floor > -50) _noise_floor = -50;
_ema_unguarded = 0;
LOG_DBG("noise_floor_cal: seed=%d", _noise_floor);
return;
LOG_DBG("noise_floor_cal: rssi=%d, floor=%d, tick=%u",
rssi, _noise_floor, _ema_unguarded - 1);
} while (0);
if (_rx_duty_cycle_enabled) {
hwSetRxDutyCycle(true);
}
/* Threshold filter with warmup and periodic bypass.
*
* _ema_unguarded counts up from 0 on every tick.
* Ticks 0..W-1 (warmup): all samples accepted for fast convergence
* after seed/reset — prevents a bad seed from locking out the
* real noise floor via a too-tight threshold.
* Ticks W+: threshold filter active. Every Pth tick one sample
* bypasses the filter so the floor can track sustained upward
* shifts (new interference, antenna change).
* The EMA's 1/8 weight naturally dampens isolated spikes. */
const int W = (1 << NOISE_FLOOR_EMA_SHIFT); /* 8 — warmup ticks */
const int P = NOISE_FLOOR_UNGUARDED_INTERVAL; /* 16 — periodic interval */
bool warmup = (_ema_unguarded < W);
bool periodic = (!warmup && (_ema_unguarded & (P - 1)) == 0);
_ema_unguarded++; /* wraps at 255 — harmless */
if (!warmup && !periodic &&
rssi >= _noise_floor + NOISE_FLOOR_SAMPLING_THRESHOLD) {
return;
}
/* EMA: floor += round_nearest((sample - floor) / W).
* Plain >> has downward bias (-1>>3 == -1 but +1>>3 == 0).
* Plain / has a ±7 dead zone (small drifts ignored).
* Round-to-nearest: add half the divisor before dividing,
* with sign-aware bias so both directions are symmetric. */
int diff = rssi - _noise_floor;
int half = W / 2; /* 4 */
int step = (diff + (diff > 0 ? half : -half)) / W;
_noise_floor += step;
if (_noise_floor < -120) _noise_floor = -120;
if (_noise_floor > -50) _noise_floor = -50;
LOG_DBG("noise_floor_cal: rssi=%d, floor=%d, tick=%u",
rssi, _noise_floor, _ema_unguarded - 1);
}
void LoRaRadioBase::resetAGC()
+1
View File
@@ -7,6 +7,7 @@
#include <mesh/Utils.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(zephcore_basechat, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
+1
View File
@@ -76,6 +76,7 @@ struct NodePrefs {
uint8_t path_hash_mode; // which path mode to use when sending (0-2)
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
uint8_t loop_detect; // LOOP_DETECT_OFF/MINIMAL/MODERATE/STRICT
uint8_t leds_disabled; // 1 = LEDs off (heartbeat disabled), 0 = LEDs on
};
/* Default prefs — MUST match LoRaConfig.h defaults for radio interop.
+16 -1
View File
@@ -664,6 +664,11 @@ static const uint8_t glyph_H[5] = {0x9, 0x9, 0xF, 0x9, 0x9};
static const uint8_t glyph_P[5] = {0xE, 0x9, 0xE, 0x8, 0x8};
static const uint8_t glyph_A[5] = {0x6, 0x9, 0xF, 0x9, 0x9};
static const uint8_t glyph_M[5] = {0x9, 0xF, 0xF, 0x9, 0x9};
static const uint8_t glyph_G[5] = {0x7, 0x8, 0xB, 0x9, 0x7};
static const uint8_t glyph_E[5] = {0xF, 0x8, 0xE, 0x8, 0xF};
static const uint8_t glyph_O[5] = {0x6, 0x9, 0x9, 0x9, 0x6};
static const uint8_t glyph_V[5] = {0x9, 0x9, 0x9, 0x6, 0x4};
static const uint8_t glyph_R[5] = {0xE, 0x9, 0xE, 0xC, 0x9};
static void draw_glyph(int dx, int dy, const uint8_t *glyph)
{
@@ -821,7 +826,17 @@ static void draw_game_over(void)
blit_letter(start_x + 20, start_y, letter_A);
blit_letter(start_x + 30, start_y, letter_D);
draw_number((SCREEN_W - 12) / 2, start_y + 14, game.score);
/* "game over" on one line: GAME(4 chars) + 7px gap + OVER(4 chars) = 40px */
int go_x = (SCREEN_W - 40) / 2;
int go_y = start_y + 14;
draw_glyph(go_x, go_y, glyph_G);
draw_glyph(go_x + 5, go_y, glyph_A);
draw_glyph(go_x + 10, go_y, glyph_M);
draw_glyph(go_x + 15, go_y, glyph_E);
draw_glyph(go_x + 22, go_y, glyph_O);
draw_glyph(go_x + 27, go_y, glyph_V);
draw_glyph(go_x + 32, go_y, glyph_E);
draw_glyph(go_x + 37, go_y, glyph_R);
}
/* ========== DISPLAY ========== */
+17
View File
@@ -34,6 +34,7 @@ LOG_MODULE_REGISTER(zephcore_ui_actions, CONFIG_ZEPHCORE_UI_ACTIONS_LOG_LEVEL);
#define UI_ACTION_BUZZER_TOGGLE BIT(2)
#define UI_ACTION_ZEROHOP_ADVERT BIT(3)
#define UI_ACTION_OFFGRID_TOGGLE BIT(4)
#define UI_ACTION_LEDS_TOGGLE BIT(5)
/* Module-local pointers, set by init */
static CompanionMesh *s_mesh;
@@ -53,6 +54,7 @@ static atomic_t pending_ui_actions;
static atomic_t pending_gps_enabled;
static atomic_t pending_buzzer_quiet;
static atomic_t pending_offgrid_enabled;
static atomic_t pending_leds_disabled;
extern "C" void ui_mesh_actions_init(struct k_event *mesh_events,
uint32_t mesh_event_ui_action,
@@ -115,6 +117,14 @@ extern "C" void mesh_set_offgrid_mode(bool enable)
k_event_post(s_mesh_events, s_mesh_event_ui_action);
}
extern "C" void mesh_set_leds_disabled(bool disabled)
{
/* Defer the flash write (savePrefs) to mesh thread */
atomic_set(&pending_leds_disabled, disabled ? 1 : 0);
atomic_or(&pending_ui_actions, UI_ACTION_LEDS_TOGGLE);
k_event_post(s_mesh_events, s_mesh_event_ui_action);
}
/* Disable power regulators for System OFF.
* Only touches sensor power and buzzer power-gate regulators.
* GPS is handled separately by gps_power_off_for_shutdown().
@@ -200,6 +210,13 @@ extern "C" void mesh_handle_ui_actions(void)
need_save = true;
}
if (actions & UI_ACTION_LEDS_TOGGLE) {
bool ld = atomic_get(&pending_leds_disabled) != 0;
s_mesh->prefs.leds_disabled = ld ? 1 : 0;
LOG_INF("leds_disabled=%d (button)", ld);
need_save = true;
}
if (need_save) {
s_data_store->savePrefs(s_mesh->prefs);
LOG_INF("prefs saved (button action)");
+1
View File
@@ -57,6 +57,7 @@ void mesh_gps_set_enabled(bool enable);
void mesh_ble_set_enabled(bool enable);
void mesh_set_buzzer_quiet(bool quiet);
void mesh_set_offgrid_mode(bool enable);
void mesh_set_leds_disabled(bool disabled);
void mesh_disable_power_regulators(void);
void mesh_reboot_to_ota_dfu(void);
@@ -19,6 +19,7 @@ __attribute__((weak)) void mesh_gps_set_enabled(bool enable) { ARG_UNUSED(enable
__attribute__((weak)) void mesh_ble_set_enabled(bool enable) { ARG_UNUSED(enable); }
__attribute__((weak)) void mesh_set_buzzer_quiet(bool quiet) { ARG_UNUSED(quiet); }
__attribute__((weak)) void mesh_set_offgrid_mode(bool enable) { ARG_UNUSED(enable); }
__attribute__((weak)) void mesh_set_leds_disabled(bool disabled) { ARG_UNUSED(disabled); }
__attribute__((weak)) void mesh_disable_power_regulators(void) {}
__attribute__((weak)) void mesh_reboot_to_ota_dfu(void) {}
__attribute__((weak)) void mesh_handle_ui_actions(void) {}
+16
View File
@@ -165,6 +165,7 @@ static const enum ui_page active_pages[] = {
UI_PAGE_ADVERT,
UI_PAGE_GPS,
UI_PAGE_BUZZER,
UI_PAGE_LEDS,
UI_PAGE_SENSORS,
UI_PAGE_OFFGRID,
UI_PAGE_DFU,
@@ -512,6 +513,20 @@ static void render_buzzer(void)
state.buzzer_quiet ? "Press to Enable" : "Press to Disable");
}
static void render_leds(void)
{
char buf[24];
int y = CONTENT_Y;
snprintf(buf, sizeof(buf), "LEDs: %s",
state.leds_disabled ? "off" : "on");
mc_display_text(0, y, buf, false);
y += LINE_H;
draw_centered(y + 8,
state.leds_disabled ? "Press to Enable" : "Press to Disable");
}
static void render_sensors(void)
{
char buf[24];
@@ -671,6 +686,7 @@ static const page_render_fn renderers[] = {
[UI_PAGE_ADVERT] = render_advert,
[UI_PAGE_GPS] = render_gps,
[UI_PAGE_BUZZER] = render_buzzer,
[UI_PAGE_LEDS] = render_leds,
[UI_PAGE_SENSORS] = render_sensors,
[UI_PAGE_OFFGRID] = render_offgrid,
[UI_PAGE_DFU] = render_dfu,
+4
View File
@@ -27,6 +27,7 @@ enum ui_page {
UI_PAGE_ADVERT, /* Send broadcast advert */
UI_PAGE_GPS, /* GPS status / position */
UI_PAGE_BUZZER, /* Buzzer mute toggle */
UI_PAGE_LEDS, /* LED enable/disable toggle */
UI_PAGE_SENSORS, /* Environment sensor data */
UI_PAGE_OFFGRID, /* Offgrid mode (client repeat) toggle */
UI_PAGE_DFU, /* BLE DFU bootloader entry */
@@ -82,6 +83,9 @@ struct ui_state {
/* Buzzer page */
bool buzzer_quiet; /* true = muted */
/* LEDs page */
bool leds_disabled; /* true = LEDs off */
/* Sensors page */
int16_t temperature_c10; /* temp in 0.1°C */
uint32_t pressure_pa; /* pressure in Pa */
+40
View File
@@ -277,6 +277,7 @@ static void action_page_prev(void)
static void action_flood_advert(void);
static void action_gps_toggle(void);
static void action_buzzer_toggle(void);
static void action_leds_toggle(void);
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
static void action_ble_toggle(void);
static void action_enter_dfu(void);
@@ -319,6 +320,11 @@ static void action_page_enter(void)
action_buzzer_toggle();
break;
case UI_PAGE_LEDS:
/* Toggle LED on/off */
action_leds_toggle();
break;
case UI_PAGE_OFFGRID: {
/* Double-press confirmation (500ms window) */
struct ui_state *st_og = get_state();
@@ -461,6 +467,18 @@ static void action_buzzer_toggle(void)
schedule_render();
}
static void action_leds_toggle(void)
{
struct ui_state *s = get_state();
bool new_disabled = !s->leds_disabled;
s->leds_disabled = new_disabled;
ui_set_heartbeat_led(!new_disabled);
mesh_set_leds_disabled(new_disabled);
LOG_INF("LEDs %s (user toggle)", new_disabled ? "disabled" : "enabled");
schedule_render();
}
static void action_gps_toggle(void)
{
if (!gps_is_available()) {
@@ -1032,6 +1050,28 @@ void ui_set_offgrid_mode(bool enabled)
s->offgrid_enabled = enabled;
}
void ui_set_leds_disabled(bool disabled)
{
struct ui_state *s = get_state();
s->leds_disabled = disabled;
}
void ui_set_heartbeat_led(bool enabled)
{
#if HAS_HEARTBEAT_LED
if (enabled) {
if (gpio_is_ready_dt(&heartbeat_led)) {
k_work_reschedule(&led_on_work, K_NO_WAIT);
}
} else {
k_work_cancel_delayable(&led_on_work);
k_work_cancel_delayable(&led_off_work);
gpio_pin_set_dt(&heartbeat_led, 0);
}
#endif
}
void ui_refresh_display(void)
{
if (!ui_initialized) {
+10
View File
@@ -148,6 +148,16 @@ void ui_set_ble_enabled(bool enabled);
*/
void ui_set_buzzer_quiet(bool quiet);
/**
* Set LEDs disabled state (for display page).
*/
void ui_set_leds_disabled(bool disabled);
/**
* Enable or disable the heartbeat LED.
*/
void ui_set_heartbeat_led(bool enabled);
/**
* Set offgrid mode (client repeat) state for display page.
*/
+7
View File
@@ -654,6 +654,13 @@ int main(void)
ui_play_startup_chime();
#endif
/* Restore LED enabled/disabled state from persisted prefs.
* If LEDs were disabled, stop the heartbeat LED cycle. */
bool leds_off = companion_mesh.prefs.leds_disabled != 0;
ui_set_leds_disabled(leds_off);
ui_set_heartbeat_led(!leds_off);
LOG_INF("LEDs: %s (from prefs)", leds_off ? "disabled" : "enabled");
/* Restore GPS state from persisted prefs.
* GPS hardware is powered at boot (bootloader/pull-up).
* First, ensure power state matches prefs (powers off if disabled).