mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-10-15 05:49:57 +00:00
capture the reset cause at boot instead of consuming it in main()
hwinfo_get_reset_cause() reads a latched register. It was read inline in main_companion.cpp and cleared there, so the repeater, room server and observer never read it and never cleared it: their flags carried into the next boot, and whichever role read first defined what any later reader would see. helpers/boot_info.c reads it once in a SYS_INIT at POST_KERNEL, logs it, stores it and clears the register. That runs before main() on every role. zephcore_boot_reset_cause() hands out the stored copy and reports false where the platform has no support, which a caller can distinguish from a cause of 0. The clear's return is discarded deliberately: ESP32 has no hwinfo_clear_reset_cause() implementation, so the weak stub returns -ENOSYS. The capture is still correct there; only the next boot's reading is not. Logging in boot_info_init() rather than only in the consuming role matters because the clear now happens everywhere. Without it a repeater that rebooted on WATCHDOG or LOCKUP would have the evidence cleared and recorded nowhere, where before it survived in the register for whoever read it over SWD. main_companion.cpp reads the stored copy and renders through a table with one row per RESET_* bit, so a cause with no label is no longer logged as bare hex. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
60ab598fc3
commit
6ec31e7b7f
+16
-4
@@ -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.
|
||||
@@ -1226,8 +1230,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
|
||||
|
||||
@@ -710,6 +710,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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user