vcontact polishing

This commit is contained in:
liquidraver
2026-07-12 15:53:00 +02:00
parent 722ca66cf0
commit 51a807a957
6 changed files with 155 additions and 6 deletions
@@ -481,6 +481,28 @@ bool ZephyrDataStore::saveMainIdentity(const mesh::LocalIdentity &identity)
return atomicReplaceFile(MAIN_ID_FILE, buf, n);
}
/* ── Shutdown-reason breadcrumb ────────────────────────────────────── */
void ZephyrDataStore::saveShutdownReason(uint8_t code)
{
/* Best-effort: called at a software power-off, possibly at low battery. */
(void)atomicReplaceFile(SHUTDOWN_FILE, &code, 1);
}
uint8_t ZephyrDataStore::takeShutdownReason()
{
uint8_t code = 0;
size_t len = 0;
if (openRead(SHUTDOWN_FILE, &code, sizeof(code), len) && len >= 1) {
removeFile(SHUTDOWN_FILE);
return code;
}
/* Stray/empty file — clear it so it can't linger. */
removeFile(SHUTDOWN_FILE);
return 0;
}
/* ── Preferences ───────────────────────────────────────────────────── */
void ZephyrDataStore::loadPrefs(NodePrefs &prefs)
@@ -31,6 +31,14 @@ public:
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs &prefs);
void savePrefs(const NodePrefs &prefs);
/* Shutdown-reason breadcrumb: written just before a software power-off
* (low-battery auto-shutdown) so the next boot can report why the node
* went down — the offline queue doesn't survive System OFF, so this
* flash marker is the only reliable channel. saveShutdownReason is
* best-effort (called at critically low battery). takeShutdownReason
* returns the stored code (0 = none) and clears it. */
void saveShutdownReason(uint8_t code);
uint8_t takeShutdownReason();
void loadContacts(DataStoreHost *host);
void saveContacts(DataStoreHost *host);
void loadChannels(DataStoreHost *host);
@@ -67,6 +75,7 @@ private:
static constexpr const char *MNT_POINT = "/lfs";
static constexpr const char *PREFS_FILE = "/lfs/new_prefs";
static constexpr const char *MAIN_ID_FILE = "/lfs/_main.id";
static constexpr const char *SHUTDOWN_FILE = "/lfs/shutdn";
/* External QSPI flash (optional) - contacts, channels, blobs */
static constexpr const char *EXT_MNT_POINT = "/ext";
+56 -6
View File
@@ -383,8 +383,30 @@ void ui_set_auto_shutdown_mv(uint16_t mv)
s_auto_shutdown_mv = mv;
}
/* Pre-shutdown hook + deferred power-off. When the hook reports an app is
* connected (live notice queued), the power-off is deferred by a grace period
* on a work item so the main loop keeps running and delivers the message. */
static ui_shutdown_fn s_shutdown_hook;
static bool s_shutting_down;
static void shutdown_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
#ifdef CONFIG_POWEROFF
ui_prepare_for_system_off();
sys_poweroff();
CODE_UNREACHABLE;
#endif
}
static K_WORK_DELAYABLE_DEFINE(s_shutdown_work, shutdown_work_fn);
void ui_set_shutdown_hook(ui_shutdown_fn fn)
{
s_shutdown_hook = fn;
}
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
static void auto_shutdown_warn_screen(void)
static void auto_shutdown_warn_screen(bool hold)
{
/* Wake the panel (OLED may be blanked by auto-off; EPD is always
* visible). Centre two lines; the message persists on e-paper after
@@ -409,8 +431,12 @@ static void auto_shutdown_warn_screen(void)
mc_display_finalize();
/* OLED blanks the instant power drops, so hold long enough to read it.
* EPD keeps the image with no power, so skip the delay. */
if (!mc_display_is_epd()) {
* EPD keeps the image with no power, so skip the delay. The deferred-
* poweroff (grace) path passes hold=false: it must NOT block the main
* thread, because that thread has to service the app's message fetch
* during the grace window — the grace timer provides the on-screen dwell
* instead. */
if (hold && !mc_display_is_epd()) {
k_sleep(K_MSEC(3000));
}
}
@@ -418,6 +444,9 @@ static void auto_shutdown_warn_screen(void)
void ui_auto_shutdown_check(void)
{
if (s_shutting_down) {
return; /* power-off already committed (deferred grace running) */
}
if (!s_batt_provider || s_auto_shutdown_mv == 0) {
return; /* no battery provider, or runtime-disabled */
}
@@ -452,15 +481,35 @@ void ui_auto_shutdown_check(void)
LOG_WRN("auto-shutdown: confirmed — powering off");
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
auto_shutdown_warn_screen();
#endif
/* Let the app layer report the shutdown. If it queued a live notice to a
* connected app, it returns true and we defer the power-off by a short
* grace so the notify→fetch→send round-trip can finish; otherwise it
* persisted the reason to flash (reported on next boot) and we power off
* now. */
bool grace = s_shutdown_hook ? s_shutdown_hook(UI_SHUTDOWN_LOW_BATTERY)
: false;
s_shutting_down = true;
#ifdef CONFIG_POWEROFF
if (grace) {
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
auto_shutdown_warn_screen(false); /* draw, don't block the loop */
#endif
k_work_schedule(&s_shutdown_work, K_MSEC(UI_SHUTDOWN_GRACE_MS));
return; /* main loop keeps running → delivers the notice */
}
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
auto_shutdown_warn_screen(true); /* nothing to deliver — 3 s OLED hold */
#endif
ui_prepare_for_system_off();
sys_poweroff();
CODE_UNREACHABLE;
#else
(void)grace;
#ifdef CONFIG_ZEPHCORE_UI_DISPLAY
auto_shutdown_warn_screen(true);
#endif
LOG_WRN("auto-shutdown: CONFIG_POWEROFF not enabled — cannot power off");
#endif
}
@@ -469,6 +518,7 @@ void ui_auto_shutdown_check(void)
void ui_set_auto_shutdown_mv(uint16_t mv) { (void)mv; }
void ui_auto_shutdown_check(void) { }
void ui_set_shutdown_hook(ui_shutdown_fn fn) { (void)fn; }
#endif /* CONFIG_ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS > 0 */
+2
View File
@@ -155,6 +155,8 @@ WEAK void ui_set_auto_shutdown_mv(uint16_t mv)
WEAK void ui_auto_shutdown_check(void) { }
WEAK void ui_set_shutdown_hook(ui_shutdown_fn fn) { ARG_UNUSED(fn); }
WEAK void ui_refresh_battery(void) { }
WEAK void ui_invalidate_battery_cache(void) { }
+21
View File
@@ -235,6 +235,27 @@ void ui_set_power_source_provider(bool (*provider)(void));
*/
void ui_set_auto_shutdown_mv(uint16_t mv);
/* Reason codes passed to the shutdown hook. */
#define UI_SHUTDOWN_LOW_BATTERY 1
/* Grace period (ms) the poweroff is deferred by when the hook asks for it
* (an app is connected and a live notice was queued), so the notify→fetch→
* send round-trip can complete before power is cut. */
#define UI_SHUTDOWN_GRACE_MS 1000
/**
* Register a pre-shutdown hook, called from ui_auto_shutdown_check() just
* before power-off. The companion uses it to report the shutdown to the
* connected app (v-contact). Return value:
* true = an app is connected and a live notice was queued — defer the
* poweroff by UI_SHUTDOWN_GRACE_MS so the app can fetch it.
* false = nothing to deliver live (persist to flash instead) — power off
* immediately.
* The hook runs on the main thread and must not block.
*/
typedef bool (*ui_shutdown_fn)(int reason);
void ui_set_shutdown_hook(ui_shutdown_fn fn);
/**
* Low-battery auto-shutdown check (companion only).
*
+45
View File
@@ -997,6 +997,35 @@ static bool handle_vcontact_cli(const char *line, char *reply)
return false;
}
/* Pre-shutdown hook, registered with the UI layer and called on the main
* thread just before a low-battery power-off. If an app is connected, queue a
* live v-contact notice and return true so the UI defers the power-off by a
* short grace (letting the notify→fetch→send round-trip finish). If nobody is
* connected, persist the reason to flash and return false (power off now) — the
* offline queue doesn't survive System OFF, so the flash marker is the only way
* the shutdown gets reported, which it does on the next boot. */
static bool companion_shutdown_hook(int reason)
{
const char *msg = (reason == UI_SHUTDOWN_LOW_BATTERY)
? "Powering off: low battery"
: "Powering off";
bool connected = zephcore_ble_is_connected();
#if ZEPHCORE_USB_STACK
connected = connected ||
(zephcore_ble_get_active_iface() == ZEPHCORE_IFACE_USB &&
!zephcore_usb_companion_is_text_session());
#endif
if (connected && companion_mesh.isVContactEnabled()) {
companion_mesh.vcontactNotify(msg);
return true; /* deliver live — ask the UI for the grace delay */
}
data_store.saveShutdownReason((uint8_t)reason);
return false; /* nobody listening — flash marker, power off now */
}
/* Transport-neutral CLI line execution — runs on the MAIN thread only
* (CommonCLI::handleCommand touches mesh state shared with loop()). Both the
* USB text sideband and the v-contact chat funnel through here. `reply` must
@@ -1259,6 +1288,21 @@ int main(void)
}
data_store.begin();
/* Fold a persisted shutdown reason into the boot notice. Written just
* before a previous low-battery System OFF when no app was connected to
* receive it live (the offline queue doesn't survive power-off). The
* hardware reset cause on this boot is just POR, so without this the
* reason would be lost. */
{
uint8_t sdr = data_store.takeShutdownReason();
if (sdr == UI_SHUTDOWN_LOW_BATTERY) {
size_t l = strlen(boot_cause_msg);
snprintf(boot_cause_msg + l, sizeof(boot_cause_msg) - l,
"%sLast shutdown: low battery",
l ? " | " : "");
}
}
/* First-boot migration: fix NVS (BLE bonds) before bt_enable() runs.
*
* nRF52 stores BLE bonds in storage_partition (NVS) at 0xD0000. UF2
@@ -1404,6 +1448,7 @@ int main(void)
ui_set_battery_provider(get_battery_mv);
ui_set_power_source_provider([]() { return zephyr_board.isExternalPowered(); });
ui_set_auto_shutdown_mv(companion_mesh.prefs.auto_shutdown_mv);
ui_set_shutdown_hook(companion_shutdown_hook);
companion_mesh.setRadioReconfigureCallback(radio_reconfigure);
companion_mesh.setPinChangeCallback([](uint32_t new_pin) {
zephcore_ble_set_passkey(new_pin);