diff --git a/.gitignore b/.gitignore index 894d8f6..b7c4bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,4 @@ zephcore/SX126X_LBT_RX_DEBUG_HANDOFF.md PSRAM_HANDOFF.md BLE_AUDIT_INDEX.md POWER_AUDIT_HANDOFF.md +POWER_AUDIT_INDEX.md diff --git a/zephcore/helpers/ui/ui_mesh_actions.cpp b/zephcore/helpers/ui/ui_mesh_actions.cpp index 5545e51..c542125 100644 --- a/zephcore/helpers/ui/ui_mesh_actions.cpp +++ b/zephcore/helpers/ui/ui_mesh_actions.cpp @@ -231,13 +231,8 @@ extern "C" void mesh_housekeeping_ui_refresh(void) return; } - /* Battery voltage changes slowly — read every ~60s (12 × 5s housekeeping) - * instead of every 5s. Saves power (regulator toggle + ADC) and log noise. */ - static uint8_t batt_counter; - if (++batt_counter >= 12) { - batt_counter = 0; - ui_set_battery(s_board->getBattMilliVolts(), 0); - } + /* Battery is now refreshed lazily from ui_pages_render() with a 30 s + * freshness guard — no periodic ADC fire here. */ /* Update top bar clock from RTC */ ui_set_clock(s_rtc_clock->getCurrentTime()); @@ -292,22 +287,6 @@ extern "C" void mesh_housekeeping_ui_refresh(void) } } - /* Read environment sensors if available */ - if (env_sensors_available()) { - struct env_data edata; - - if (env_sensors_read(&edata) == 0) { - ui_set_sensor_data( - edata.has_temperature ? (int16_t)(edata.temperature_c * 10) : 0, - edata.has_pressure ? (uint32_t)(edata.pressure_hpa * 100) : 0, - edata.has_humidity ? (uint16_t)(edata.humidity_pct * 10) : 0, - 0); - } - } - /* Update offline queue message count */ ui_set_msg_count(s_mesh->getOfflineQueueCount()); - - /* Trigger display refresh */ - ui_refresh_display(); } diff --git a/zephcore/helpers/ui/ui_pages.c b/zephcore/helpers/ui/ui_pages.c index 778276d..7d40ccd 100644 --- a/zephcore/helpers/ui/ui_pages.c +++ b/zephcore/helpers/ui/ui_pages.c @@ -15,8 +15,11 @@ */ #include "ui_pages.h" +#include "ui_task.h" #include "display.h" +#include + #include #include #include @@ -547,34 +550,31 @@ static void render_sensors(void) char buf[24]; int y = CONTENT_Y; - /* Temperature */ - if (state.temperature_c10 != 0) { + /* Lazy read: only fetch sensors when the user is actually looking at + * this page. Render is event-driven (schedule_render only on real UI + * events), so this never fires periodically when idle. */ + struct env_data edata; + bool have_env = env_sensors_available() && env_sensors_read(&edata) == 0; + + if (have_env && edata.has_temperature) { + int16_t t10 = (int16_t)(edata.temperature_c * 10); snprintf(buf, sizeof(buf), "Temp: %d.%d C", - state.temperature_c10 / 10, - abs(state.temperature_c10 % 10)); + t10 / 10, abs(t10 % 10)); mc_display_text(0, y, buf, false); y += LINE_H; } - /* Pressure */ - if (state.pressure_pa != 0) { + if (have_env && edata.has_pressure) { snprintf(buf, sizeof(buf), "Press: %u hPa", - (unsigned)(state.pressure_pa / 100)); + (unsigned)edata.pressure_hpa); mc_display_text(0, y, buf, false); y += LINE_H; } - /* Humidity */ - if (state.humidity_rh10 != 0) { + if (have_env && edata.has_humidity) { + uint16_t h10 = (uint16_t)(edata.humidity_pct * 10); snprintf(buf, sizeof(buf), "Humid: %u.%u%%", - state.humidity_rh10 / 10, state.humidity_rh10 % 10); - mc_display_text(0, y, buf, false); - y += LINE_H; - } - - /* Light */ - if (state.light_lux != 0) { - snprintf(buf, sizeof(buf), "Light: %u lux", state.light_lux); + h10 / 10, h10 % 10); mc_display_text(0, y, buf, false); y += LINE_H; } @@ -718,6 +718,11 @@ struct ui_state *ui_pages_get_state(void) void ui_pages_render(void) { + /* Refresh battery lazily — ADC only fires when the cached reading is + * stale (≥30 s). Render is event-driven, so during idle this never + * runs. Telemetry / stats paths bypass the cache entirely. */ + ui_refresh_battery(); + mc_display_clear(); render_top_bar(); render_page_indicator(); diff --git a/zephcore/helpers/ui/ui_pages.h b/zephcore/helpers/ui/ui_pages.h index f8525ed..5c3f99d 100644 --- a/zephcore/helpers/ui/ui_pages.h +++ b/zephcore/helpers/ui/ui_pages.h @@ -86,12 +86,6 @@ struct ui_state { /* 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 */ - uint16_t humidity_rh10; /* humidity in 0.1% RH */ - uint16_t light_lux; - /* Current page */ enum ui_page current_page; diff --git a/zephcore/helpers/ui/ui_task.c b/zephcore/helpers/ui/ui_task.c index 058037f..930a85d 100644 --- a/zephcore/helpers/ui/ui_task.c +++ b/zephcore/helpers/ui/ui_task.c @@ -1065,12 +1065,43 @@ void ui_set_gps_data(bool has_fix, uint8_t sats, s->gps_alt_mm = alt_mm; } +/* Lazy battery refresh: render path calls ui_refresh_battery(); we only hit + * the ADC if the cached value is older than UI_BATT_REFRESH_MS. Telemetry / + * stats paths bypass this and read fresh directly via _batt_cb, so over-the- + * air consumers are unaffected — this gate only governs the local display. */ +#define UI_BATT_REFRESH_MS 30000 + +static uint16_t (*s_batt_provider)(void); +static uint32_t s_batt_last_read_ms; +static bool s_batt_ever_read; + void ui_set_battery(uint16_t mv, uint8_t pct) { struct ui_state *s = get_state(); s->battery_mv = mv; s->battery_pct = pct; + /* Treat any explicit setter call as a fresh read so the lazy guard + * doesn't immediately re-fire the ADC. */ + s_batt_last_read_ms = k_uptime_get_32(); + s_batt_ever_read = true; +} + +void ui_set_battery_provider(uint16_t (*provider)(void)) +{ + s_batt_provider = provider; +} + +void ui_refresh_battery(void) +{ + if (!s_batt_provider) { + return; + } + uint32_t now = k_uptime_get_32(); + if (s_batt_ever_read && (now - s_batt_last_read_ms) < UI_BATT_REFRESH_MS) { + return; + } + ui_set_battery(s_batt_provider(), 0); } void ui_set_clock(uint32_t epoch) @@ -1127,17 +1158,6 @@ void ui_set_node_name(const char *name) #endif } -void ui_set_sensor_data(int16_t temp_c10, uint32_t pressure_pa, - uint16_t humidity_rh10, uint16_t light_lux) -{ - struct ui_state *s = get_state(); - - s->temperature_c10 = temp_c10; - s->pressure_pa = pressure_pa; - s->humidity_rh10 = humidity_rh10; - s->light_lux = light_lux; -} - void ui_set_gps_available(bool available) { struct ui_state *s = get_state(); @@ -1204,23 +1224,3 @@ void ui_set_heartbeat_led(bool enabled) #endif } -void ui_refresh_display(void) -{ - if (!ui_initialized) { - return; - } - -#ifdef CONFIG_ZEPHCORE_UI_DISPLAY - /* EPD displays: skip periodic housekeeping renders. - * Each full e-paper refresh takes ~2s and causes visible flashing. - * All meaningful events (messages, BLE, GPS fix, button presses) - * already trigger renders via their own ui_set_*() → schedule_render(). - * Housekeeping just updates slow-changing data (clock, contact ages) - * which will appear on the next event-driven render. */ - if (mc_display_is_epd()) { - return; - } - - schedule_render(); -#endif -} diff --git a/zephcore/helpers/ui/ui_task.h b/zephcore/helpers/ui/ui_task.h index dd645d7..1ecd66b 100644 --- a/zephcore/helpers/ui/ui_task.h +++ b/zephcore/helpers/ui/ui_task.h @@ -116,8 +116,6 @@ void ui_clear_recent(void); /** * Update sensor data for display. */ -void ui_set_sensor_data(int16_t temp_c10, uint32_t pressure_pa, - uint16_t humidity_rh10, uint16_t light_lux); /** * Set whether GPS hardware was detected at boot. @@ -164,9 +162,17 @@ void ui_set_heartbeat_led(bool enabled); void ui_set_offgrid_mode(bool enabled); /** - * Trigger a display refresh (for periodic updates from housekeeping). + * Register a battery-voltage provider used by ui_refresh_battery(). + * provider() must return millivolts (0 if no battery hardware). */ -void ui_refresh_display(void); +void ui_set_battery_provider(uint16_t (*provider)(void)); + +/** + * Lazy battery refresh: re-read the ADC only if cached value is stale. + * Called from the page render path so the ADC fires at most once per + * 30 s and only when the display is actually being drawn. + */ +void ui_refresh_battery(void); #ifdef __cplusplus } diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index c957a77..7e359db 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -603,6 +603,7 @@ int main(void) companion_mesh.setWriteFrameCallback(write_frame); companion_mesh.setPushCallback(push_callback); companion_mesh.setBatteryCallback(get_battery_mv); + ui_set_battery_provider(get_battery_mv); companion_mesh.setRadioReconfigureCallback(radio_reconfigure); companion_mesh.setPinChangeCallback([](uint32_t new_pin) { zephcore_ble_set_passkey(new_pin); diff --git a/zephcore/src/main_repeater.cpp b/zephcore/src/main_repeater.cpp index 5225b3d..a1a55b0 100644 --- a/zephcore/src/main_repeater.cpp +++ b/zephcore/src/main_repeater.cpp @@ -276,6 +276,11 @@ static void gps_fix_callback(double lat, double lon, int64_t utc_time) #ifdef ZEPHCORE_LORA static mesh::ZephyrBoard zephyr_board; +static uint16_t get_battery_mv(void) +{ + return zephyr_board.getBattMilliVolts(); +} + /* Radio is constructed with no prefs pointer; main() binds it to * repeater_mesh._prefs via setPrefs() before repeater_mesh.begin(). */ @@ -364,15 +369,9 @@ static void repeater_event_loop(void) lora_radio.getNoiseFloor()); } - /* Battery every ~60s (12 × 5s housekeeping) */ - static uint8_t batt_counter; - if (++batt_counter >= 12) { - batt_counter = 0; - ui_set_battery(zephyr_board.getBattMilliVolts(), 0); - } + /* Battery is now refreshed lazily from ui_pages_render() with + * a 30 s freshness guard — no periodic ADC fire here. */ #endif - - ui_refresh_display(); } } } @@ -506,6 +505,7 @@ int main(void) prefs->cr, prefs->tx_power_dbm, lora_radio.getNoiseFloor()); + ui_set_battery_provider(get_battery_mv); ui_set_battery(zephyr_board.getBattMilliVolts(), 0); ui_set_gps_available(gps_is_available());