This commit is contained in:
liquidraver
2026-09-19 21:20:14 +02:00
5 changed files with 249 additions and 27 deletions
+16 -4
View File
@@ -608,8 +608,12 @@ contact named `v<node_name>` that exists only toward the connected BLE/USB app.
Chatting with it runs the same text CLI as the USB serial sideband; the reply
comes back as normal chat messages. The firmware also uses it to emit
unsolicited notices: a one-shot low-battery alert and a restart-reason message
(all causes: PIN/SOFTWARE/BROWNOUT/POR/WATCHDOG/LOCKUP — offline-queue only,
so routine power-on "noise" costs nothing over the air).
(every cause `zephcore_boot_reset_cause_str()` can label, which is more than
the six the companion used to hard-code — offline-queue only, so routine
power-on "noise" costs nothing over the air). Two causes are silent when they
are the only ones raised: a debugger reset, and a wake from a low-power
shutdown, which would otherwise announce itself after every power cycle. Both
still appear in the boot log.
**Identity**: seed = `SHA256("zc-vcontact" || self_prv_key || counter)`,
pubkey = that seed's Ed25519 public point — stable per node, unique per device.
@@ -1227,8 +1231,16 @@ node enabled on any board — the `wdt` nodes visible in board `.dts` files are
inherited SoC definitions, and the `RTCWDT` references in the TTGO board configs
concern the ESP32 ROM bootloader's own watchdog, not something ZephCore arms.
The only consumer of the concept is the boot breadcrumb in `main_companion.cpp`,
which reads `RESET_WATCHDOG` out of `hwinfo_get_reset_cause()` and reports it in
the "Restarted:" v-contact message (see [6.2.1](#621-v-contact-loopback-admin-contact)).
which reports `RESET_WATCHDOG` in the "Restarted:" v-contact message (see
[6.2.1](#621-v-contact-loopback-admin-contact)). It reads the cause from
`zephcore_boot_reset_cause()` rather than from `hwinfo_get_reset_cause()`
directly: `helpers/boot_info.c` captures the register once at POST_KERNEL on
every role, logs it, and clears it where the platform implements a clear, so
no later reader can consume it. nRF implements `hwinfo_clear_reset_cause()`;
ESP32 does not, so the weak stub returns `-ENOSYS` and nothing is cleared
there. That is harmless on ESP32, whose cause comes from `esp_reset_reason()`
and does not accumulate across boots, but the capture is what readers should
rely on rather than the register.
Everything below is software: bounded stall detection in the layer that owns the
state machine. Each entry names what it recovers, because several are
+3
View File
@@ -726,6 +726,9 @@ target_sources(app PRIVATE
adapters/sensors/ZephyrEnvSensors.cpp
helpers/AdvertDataHelpers.cpp
helpers/MeshTimeSync.cpp
# Captures the reset cause before anything consumes it; every role needs it
# at boot, so it is never conditional.
helpers/boot_info.c
helpers/oled_power.c
helpers/fatal_reboot.c
# LED master gate ("set leds on|off"). Always compiled: helpers/ui/ui_common.c
+128
View File
@@ -0,0 +1,128 @@
/*
* SPDX-License-Identifier: MIT
* Boot-time reset-cause capture. See boot_info.h.
*/
#include "boot_info.h"
#include <zephyr/init.h>
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include <stdio.h>
LOG_MODULE_REGISTER(zephcore_boot, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
static uint32_t s_reset_cause;
static bool s_reset_cause_valid;
/* POST_KERNEL: before main() on every role. */
static int boot_info_init(void)
{
uint32_t cause = 0;
if (hwinfo_get_reset_cause(&cause) != 0) {
/* Nothing to capture; the accessor reports false. */
return 0;
}
s_reset_cause = cause;
s_reset_cause_valid = true;
/* Logged here, not only in the role that renders it: the clear below
* would otherwise lose the cause on roles that never read it. */
LOG_INF("Boot reset cause: 0x%08x", cause);
/* Not reported again next boot. -ENOSYS where unimplemented (ESP32). */
(void)hwinfo_clear_reset_cause();
return 0;
}
SYS_INIT(boot_info_init, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
bool zephcore_boot_reset_cause(uint32_t *out)
{
if (!s_reset_cause_valid) {
return false;
}
*out = s_reset_cause;
return true;
}
/*
* One row per RESET_* bit in <zephyr/drivers/hwinfo.h>. A bit with no row is
* rendered as nothing and is invisible to zephcore_boot_reset_cause_labelled().
*/
static const struct {
uint32_t bit;
const char *label;
} s_labels[] = {
{ RESET_PIN, "PIN" },
{ RESET_SOFTWARE, "SOFTWARE" },
{ RESET_BROWNOUT, "BROWNOUT" },
{ RESET_POR, "POR" },
{ RESET_WATCHDOG, "WATCHDOG" },
{ RESET_DEBUG, "DEBUG" },
{ RESET_SECURITY, "SECURITY" },
{ RESET_LOW_POWER_WAKE, "LOWPOWER" },
{ RESET_CPU_LOCKUP, "LOCKUP" },
{ RESET_PARITY, "PARITY" },
{ RESET_PLL, "PLL" },
{ RESET_CLOCK, "CLOCK" },
{ RESET_HARDWARE, "HARDWARE" },
{ RESET_USER, "USER" },
{ RESET_TEMPERATURE, "TEMPERATURE" },
{ RESET_BOOTLOADER, "BOOTLOADER" },
{ RESET_FLASH, "FLASH" },
};
int zephcore_boot_reset_cause_str(char *buf, size_t cap)
{
if (buf == NULL || cap == 0) {
return 0;
}
buf[0] = '\0';
if (!s_reset_cause_valid) {
return 0;
}
size_t n = 0;
for (size_t i = 0; i < ARRAY_SIZE(s_labels); i++) {
if ((s_reset_cause & s_labels[i].bit) == 0) {
continue;
}
int w = snprintf(buf + n, cap - n, " %s", s_labels[i].label);
if (w < 0 || (size_t)w >= cap - n) {
/* Stop rather than emit half a label. */
buf[n] = '\0';
break;
}
n += (size_t)w;
}
return (int)n;
}
uint32_t zephcore_boot_reset_cause_labelled(void)
{
uint32_t mask = 0;
if (!s_reset_cause_valid) {
return 0;
}
for (size_t i = 0; i < ARRAY_SIZE(s_labels); i++) {
mask |= s_reset_cause & s_labels[i].bit;
}
return mask;
}
+43
View File
@@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: MIT
*
* The MCU's reset cause, captured and cleared once at POST_KERNEL on every
* role. hwinfo's flags accumulate between resets on some platforms, so
* clearing after reading is what isolates the most recent one.
*/
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* The cause hwinfo reported at boot. out must be non-NULL. False means hwinfo
* could not report one, which differs from a cause of 0. Call from main() or
* later; an initialiser at or before POST_KERNEL may get false.
*/
bool zephcore_boot_reset_cause(uint32_t *out);
/*
* Render as space-prefixed labels, e.g. " PIN SOFTWARE". Returns characters
* written, excluding the NUL; 0 means nothing to render. Writes nothing when
* buf is NULL or cap is 0, otherwise always NUL-terminates, truncating at the
* first label that does not fit.
*/
int zephcore_boot_reset_cause_str(char *buf, size_t cap);
/*
* The part of the cause the renderer would name. Use this rather than the raw
* cause when deciding whether a cause is worth reporting, so an unlabelled bit
* cannot trigger a report that then names unrelated labels.
*/
uint32_t zephcore_boot_reset_cause_labelled(void);
#ifdef __cplusplus
}
#endif
+59 -23
View File
@@ -24,6 +24,7 @@ LOG_MODULE_REGISTER(zephcore_main, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL);
#include <adapters/clock/ZephyrRTCDiscover.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/drivers/hwinfo.h>
#include <helpers/boot_info.h>
#include <zephyr/sys/reboot.h>
#include <ZephyrSensorManager.h>
#include <helpers/time_sync.h>
@@ -1386,36 +1387,70 @@ int main(void)
LOG_INF("=== ZephCore starting ===");
/* Log reset reason so we can diagnose random reboots */
char boot_cause_msg[96]; /* fits "Restarted:" + all six cause labels */
/* "Restarted:" (10) + every label (126) + shutdown reason (29) + NUL.
* Worst case this function can produce; vcontactNotify() truncates to 63
* on the RTC-less path, so this is not an end-to-end guarantee. */
char boot_cause_msg[176];
boot_cause_msg[0] = '\0';
/* Every label the renderer can emit, space-prefixed: 126 + NUL. Declared
* out here, rather than in the block that fills it, so its lifetime does
* not depend on when the logging subsystem copies a %s argument. It does
* copy eagerly today, on every packaging path, but that is an internal
* detail and a log frontend would not. */
char boot_cause_labels[128];
{
uint32_t cause;
if (hwinfo_get_reset_cause(&cause) == 0) {
LOG_INF("Reset cause: 0x%08x%s%s%s%s%s%s", cause,
(cause & RESET_PIN) ? " PIN" : "",
(cause & RESET_SOFTWARE) ? " SOFTWARE" : "",
(cause & RESET_BROWNOUT) ? " BROWNOUT" : "",
(cause & RESET_POR) ? " POR" : "",
(cause & RESET_WATCHDOG) ? " WATCHDOG" : "",
(cause & RESET_CPU_LOCKUP)? " LOCKUP" : "");
hwinfo_clear_reset_cause();
/* Every known cause becomes a v-contact message once the mesh
/* Captured at POST_KERNEL by helpers/boot_info.c, which also did the
* clearing this block used to do. Reading the captured copy rather
* than the register is what lets a later diagnostic or CLI report
* name the same cause -- the register is already cleared by now. */
if (zephcore_boot_reset_cause(&cause)) {
/* One renderer, shared with every other reader. A hand-rolled
* list here would drift from it, and the one it replaced had
* already drifted: it knew six bits of the seventeen the
* renderer labels, so a SECURITY or CLOCK reset logged as bare
* hex and raised no notice at all. The trade is
* that the notice loses the plain-English hints the local copy
* carried: it now reads "PIN" and "POR" rather than
* "PIN(reset button)" and "POR(power-on)". */
(void)zephcore_boot_reset_cause_str(boot_cause_labels,
sizeof(boot_cause_labels));
LOG_INF("Reset cause: 0x%08x%s", cause, boot_cause_labels);
/* Every labelled cause becomes a v-contact message once the mesh
* is up (queued below, after RTC restore + prefs load) — the
* message rides the offline queue only, so the "noise" of a
* routine power-on costs nothing over the air and doubles as
* a power-integrity breadcrumb (dead battery, loose contact).
* A deliberate CLI/app reboot shows as SOFTWARE, doubling as
* a "reboot completed" confirmation. */
if (cause & (RESET_PIN | RESET_SOFTWARE | RESET_BROWNOUT |
RESET_POR | RESET_WATCHDOG | RESET_CPU_LOCKUP)) {
* a "reboot completed" confirmation.
*
* zephcore_boot_reset_cause_labelled() is 0 when the cause is
* 0 or carries only bits this build has no label for, which is
* the same "nothing worth saying" the old explicit bit test
* meant.
*
* A debugger-initiated reset is the developer's own doing and
* is not worth a chat message on its own. A quiet cause still
* renders in the log line, and in the notice when some other
* cause raised one, so nothing is hidden -- this decides only
* whether to speak unprompted.
*
* A wake from a low-power shutdown is one the user asked
* for: the button power-off persists no reason, so announcing
* the wake would put an unexplained "Restarted: LOWPOWER" in
* the chat after every power cycle. The low-battery shutdown
* does persist a reason, and it is folded in further below.
*
* Costs one case on STM32: hwinfo_stm32.c folds LPWRRSTF, an
* illegal low-power entry, onto the same bit as routine wakes,
* so that fault raises no notice. Still logged by boot_info.c. */
const uint32_t quiet = RESET_LOW_POWER_WAKE | RESET_DEBUG;
if ((zephcore_boot_reset_cause_labelled() & ~quiet) != 0) {
snprintf(boot_cause_msg, sizeof(boot_cause_msg),
"Restarted:%s%s%s%s%s%s",
(cause & RESET_PIN) ? " PIN(reset button)" : "",
(cause & RESET_SOFTWARE) ? " SOFTWARE" : "",
(cause & RESET_BROWNOUT) ? " BROWNOUT" : "",
(cause & RESET_POR) ? " POR(power-on)" : "",
(cause & RESET_WATCHDOG) ? " WATCHDOG" : "",
(cause & RESET_CPU_LOCKUP) ? " LOCKUP" : "");
"Restarted:%s", boot_cause_labels);
}
}
}
@@ -1428,8 +1463,9 @@ int main(void)
/* 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. */
* hardware cause on this boot does not say "low battery" -- on nRF52840
* a System OFF wake reports LOW_POWER_WAKE, which is quiet above -- so
* without this the reason would be lost. */
{
uint8_t sdr = data_store.takeShutdownReason();
if (sdr == UI_SHUTDOWN_LOW_BATTERY) {