mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-09 15:25:34 +00:00
led switcher page in ui
rx duty cycle + noise floor measurement marriage ui fixes
This commit is contained in:
+5
-18
@@ -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
|
||||
|
||||
@@ -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'",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ========== */
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user