mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 01:38:19 +00:00
vcontact polishing
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
|
||||
@@ -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).
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user