Skip to content

Commit c8c0242

Browse files
committed
Store crash history as packed NVS blob
Replace per-field NVS keys with a packed ResetEntry history blob and a few meta keys under CRASH_NAMESPACE. Add ResetEntry struct (8 bytes) and MAX_HISTORY handling, shift and insert new entries, and preserve heartbeat-updated armed/uptime fields. Split reset reason handling into espReasonStr and rtcReasonStr, pack firmware version into a single byte (major*10+minor), and simplify state strings. Update boot-loop detection to use the new uptime/boots keys. crashLogReadAndReport, crashLogHeartbeat, crashLogUpdateArmedState, and sendCrashLogData were updated to read/write the blob and meta keys and to emit a compact JSON history for reporting.
1 parent 0ba0da8 commit c8c0242

1 file changed

Lines changed: 178 additions & 123 deletions

File tree

src/sp140/crash_log.cpp

Lines changed: 178 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,29 @@
99
#include "sp140/device_state.h"
1010

1111
#define DEBUG_SERIAL USBSerial
12-
13-
// NVS keys (max 15 chars, all under namespace "openppg")
14-
static const char* RST_REASON = "rst_reason";
15-
static const char* RST_COUNT = "rst_count";
16-
static const char* RST_ARMED = "rst_armed";
17-
static const char* RST_UPTIME = "rst_uptime";
18-
static const char* RST_VER_MAJ = "rst_ver_maj";
19-
static const char* RST_VER_MIN = "rst_ver_min";
20-
static const char* RST_BOOTS = "rst_boots";
12+
#define CRASH_NAMESPACE "crashlog"
13+
#define MAX_HISTORY 5
2114

2215
extern DeviceState currentState;
2316

24-
static const char* resetReasonToString(int reason) {
25-
// ESP-IDF reset reasons (0-15)
17+
// Packed reset entry: 8 bytes per entry
18+
// Stored as raw bytes in NVS blob
19+
struct __attribute__((packed)) ResetEntry {
20+
uint8_t espReason; // esp_reset_reason_t
21+
uint8_t rtcReason; // RTC reset reason (raw)
22+
uint8_t armedState; // DeviceState at time of reset
23+
uint8_t verMajor; // Firmware major version
24+
uint32_t uptimeMs; // Uptime at last heartbeat
25+
};
26+
27+
// NVS keys
28+
static const char* KEY_HISTORY = "rst_hist"; // Blob: array of ResetEntry
29+
static const char* KEY_COUNT = "rst_count"; // uint16_t: total reset count
30+
static const char* KEY_BOOTS = "rst_boots"; // uint8_t: rapid boot counter
31+
static const char* KEY_ARMED = "rst_armed"; // uint8_t: current armed state (heartbeat)
32+
static const char* KEY_UPTIME = "rst_uptime"; // uint32_t: current uptime (heartbeat)
33+
34+
static const char* espReasonStr(int reason) {
2635
switch (reason) {
2736
case ESP_RST_POWERON: return "POWERON";
2837
case ESP_RST_SW: return "SW_RESET";
@@ -34,175 +43,221 @@ static const char* resetReasonToString(int reason) {
3443
case ESP_RST_BROWNOUT: return "BROWNOUT";
3544
case ESP_RST_SDIO: return "SDIO";
3645
case ESP_RST_EXT: return "EXTERNAL";
37-
default: break;
46+
default: return "UNKNOWN";
3847
}
39-
// RTC reset reasons (stored as value + 100)
40-
if (reason >= 100) {
41-
switch (reason - 100) {
42-
case 1: return "RTC_POWERON";
43-
case 3: return "RTC_SW_RESET";
44-
case 12: return "RTC_SW_CPU_RST";
45-
case 15: return "RTC_BROWNOUT";
46-
case 16: return "RTC_SDIO_RST";
47-
case 9: return "RTC_DEEPSLEEP";
48-
case 7: return "RTC_TG0WDT";
49-
case 8: return "RTC_TG1WDT";
50-
case 11: return "RTC_INT_WDT";
51-
default: return "RTC_OTHER";
52-
}
48+
}
49+
50+
static const char* rtcReasonStr(int reason) {
51+
switch (reason) {
52+
case 1: return "POWERON";
53+
case 3: return "SW_SYS";
54+
case 5: return "DEEPSLEEP";
55+
case 7: return "TG0WDT";
56+
case 8: return "TG1WDT";
57+
case 9: return "RTCWDT";
58+
case 11: return "INT_WDT";
59+
case 12: return "SW_CPU";
60+
case 14: return "EFUSE";
61+
case 15: return "USB_UART";
62+
case 16: return "USB_JTAG";
63+
case 17: return "PWR_GLITCH";
64+
case 18: return "JTAG";
65+
default: return "OTHER";
5366
}
54-
return "UNKNOWN";
5567
}
5668

5769
static const char* stateToString(uint8_t state) {
5870
switch (state) {
5971
case DISARMED: return "DISARMED";
6072
case ARMED: return "ARMED";
61-
case ARMED_CRUISING: return "ARMED_CRUISING";
62-
default: return "UNKNOWN";
73+
case ARMED_CRUISING: return "CRUISING";
74+
default: return "?";
6375
}
6476
}
6577

6678
void crashLogReadAndReport() {
67-
esp_reset_reason_t currentReason = esp_reset_reason();
68-
RESET_REASON rtcReason = rtc_get_reset_reason(0); // Core 0
79+
esp_reset_reason_t espReason = esp_reset_reason();
80+
RESET_REASON rtcReason = rtc_get_reset_reason(0);
6981

70-
// Read previous crash data from NVS
82+
// Read existing history from NVS
7183
Preferences prefs;
72-
prefs.begin("crashlog", true); // read-only
84+
prefs.begin(CRASH_NAMESPACE, true); // read-only
85+
86+
ResetEntry history[MAX_HISTORY] = {};
87+
size_t histLen = prefs.getBytesLength(KEY_HISTORY);
88+
int entryCount = 0;
89+
if (histLen > 0 && histLen <= sizeof(history)) {
90+
prefs.getBytes(KEY_HISTORY, history, histLen);
91+
entryCount = histLen / sizeof(ResetEntry);
92+
}
7393

74-
uint8_t prevReason = prefs.getUChar(RST_REASON, 0);
75-
uint16_t prevCount = prefs.getUShort(RST_COUNT, 0);
76-
uint8_t prevArmed = prefs.getUChar(RST_ARMED, 0);
77-
uint32_t prevUptime = prefs.getULong(RST_UPTIME, 0);
78-
uint8_t prevVerMaj = prefs.getUChar(RST_VER_MAJ, 0);
79-
uint8_t prevVerMin = prefs.getUChar(RST_VER_MIN, 0);
80-
uint8_t prevBoots = prefs.getUChar(RST_BOOTS, 0);
94+
uint16_t totalCount = prefs.getUShort(KEY_COUNT, 0);
95+
uint8_t rapidBoots = prefs.getUChar(KEY_BOOTS, 0);
96+
uint8_t lastArmed = prefs.getUChar(KEY_ARMED, 0);
97+
uint32_t lastUptime = prefs.getULong(KEY_UPTIME, 0);
8198

8299
prefs.end();
83100

84-
// Print previous reset info
85-
DEBUG_SERIAL.println("=== Previous Reset Info ===");
86-
87-
if (prevCount == 0 && prevReason == 0) {
88-
DEBUG_SERIAL.println("No previous crash data stored.");
101+
// Print reset history
102+
DEBUG_SERIAL.println("=== Reset History ===");
103+
DEBUG_SERIAL.print("Total resets: ");
104+
DEBUG_SERIAL.println(totalCount);
105+
106+
if (entryCount > 0) {
107+
// Update the most recent entry with heartbeat data (armed state + uptime)
108+
// since those are written separately from the history blob
109+
history[0].armedState = lastArmed;
110+
history[0].uptimeMs = lastUptime;
111+
112+
for (int i = 0; i < entryCount; i++) {
113+
ResetEntry& e = history[i];
114+
DEBUG_SERIAL.print(i == 0 ? " [PREV] " : " [");
115+
if (i > 0) { DEBUG_SERIAL.print(i); DEBUG_SERIAL.print("] "); }
116+
DEBUG_SERIAL.print("esp:");
117+
DEBUG_SERIAL.print(espReasonStr(e.espReason));
118+
DEBUG_SERIAL.print("(");
119+
DEBUG_SERIAL.print(e.espReason);
120+
DEBUG_SERIAL.print(") rtc:");
121+
DEBUG_SERIAL.print(rtcReasonStr(e.rtcReason));
122+
DEBUG_SERIAL.print("(");
123+
DEBUG_SERIAL.print(e.rtcReason);
124+
DEBUG_SERIAL.print(") state:");
125+
DEBUG_SERIAL.print(stateToString(e.armedState));
126+
DEBUG_SERIAL.print(" up:");
127+
DEBUG_SERIAL.print(e.uptimeMs / 1000);
128+
DEBUG_SERIAL.print("s fw:");
129+
DEBUG_SERIAL.print(e.verMajor / 10);
130+
DEBUG_SERIAL.print(".");
131+
DEBUG_SERIAL.println(e.verMajor % 10);
132+
}
89133
} else {
90-
DEBUG_SERIAL.print("Reset reason: ");
91-
DEBUG_SERIAL.print(resetReasonToString(prevReason));
92-
DEBUG_SERIAL.print(" (");
93-
DEBUG_SERIAL.print(prevReason);
94-
DEBUG_SERIAL.println(")");
95-
96-
DEBUG_SERIAL.print("Crash count: ");
97-
DEBUG_SERIAL.println(prevCount);
98-
99-
DEBUG_SERIAL.print("Last state: ");
100-
DEBUG_SERIAL.println(stateToString(prevArmed));
101-
102-
DEBUG_SERIAL.print("Last uptime: ");
103-
DEBUG_SERIAL.print(prevUptime);
104-
DEBUG_SERIAL.print(" ms (");
105-
DEBUG_SERIAL.print(prevUptime / 60000.0, 1);
106-
DEBUG_SERIAL.println(" min)");
107-
108-
DEBUG_SERIAL.print("Last firmware: ");
109-
DEBUG_SERIAL.print(prevVerMaj);
110-
DEBUG_SERIAL.print(".");
111-
DEBUG_SERIAL.println(prevVerMin);
134+
DEBUG_SERIAL.println("No previous reset data.");
112135
}
113136

114137
// Boot loop detection
115138
uint8_t newBoots = 0;
116-
if (prevUptime > 0 && prevUptime < BOOT_LOOP_UPTIME_MS) {
117-
newBoots = prevBoots + 1;
139+
if (lastUptime > 0 && lastUptime < BOOT_LOOP_UPTIME_MS) {
140+
newBoots = rapidBoots + 1;
118141
}
119-
120142
if (newBoots >= BOOT_LOOP_THRESHOLD) {
121143
DEBUG_SERIAL.println("!!! BOOT LOOP DETECTED !!!");
122-
DEBUG_SERIAL.print("Device has crashed ");
123-
DEBUG_SERIAL.print(newBoots);
124-
DEBUG_SERIAL.println(" times with <30s uptime each.");
125-
DEBUG_SERIAL.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!");
126144
}
127145

128-
DEBUG_SERIAL.print("Current reset: ");
129-
DEBUG_SERIAL.print(resetReasonToString(currentReason));
130-
DEBUG_SERIAL.print(" (esp:");
131-
DEBUG_SERIAL.print((int)currentReason);
132-
DEBUG_SERIAL.print(" rtc:");
146+
DEBUG_SERIAL.print("Current: esp:");
147+
DEBUG_SERIAL.print(espReasonStr(espReason));
148+
DEBUG_SERIAL.print("(");
149+
DEBUG_SERIAL.print((int)espReason);
150+
DEBUG_SERIAL.print(") rtc:");
151+
DEBUG_SERIAL.print(rtcReasonStr(rtcReason));
152+
DEBUG_SERIAL.print("(");
133153
DEBUG_SERIAL.print((int)rtcReason);
134154
DEBUG_SERIAL.println(")");
135-
DEBUG_SERIAL.println("=== End Reset Info ===");
136-
137-
// Increment crash count for non-poweron resets
138-
uint16_t newCount = prevCount;
139-
if (currentReason != ESP_RST_POWERON && rtcReason != POWERON_RESET) {
140-
newCount++;
155+
DEBUG_SERIAL.println("=== End Reset History ===");
156+
157+
// Shift history down and insert new entry at position 0
158+
ResetEntry newHistory[MAX_HISTORY] = {};
159+
newHistory[0].espReason = (uint8_t)espReason;
160+
newHistory[0].rtcReason = (uint8_t)rtcReason;
161+
newHistory[0].armedState = (uint8_t)DISARMED;
162+
// Pack version: major*10 + minor (fits in uint8_t for versions like 7.6 = 76)
163+
newHistory[0].verMajor = VERSION_MAJOR * 10 + VERSION_MINOR;
164+
newHistory[0].uptimeMs = 0;
165+
166+
// Copy previous entries, shifting by 1 (drop oldest if full)
167+
int copyCount = (entryCount < MAX_HISTORY - 1) ? entryCount : MAX_HISTORY - 1;
168+
for (int i = 0; i < copyCount; i++) {
169+
newHistory[i + 1] = history[i];
170+
// Patch the entry we're shifting: update its armed/uptime from heartbeat
171+
if (i == 0) {
172+
newHistory[1].armedState = lastArmed;
173+
newHistory[1].uptimeMs = lastUptime;
174+
}
141175
}
176+
int newEntryCount = copyCount + 1;
142177

143-
// Store RTC reason if esp_reset_reason returns UNKNOWN
144-
uint8_t reasonToStore = (uint8_t)currentReason;
145-
if (currentReason == ESP_RST_UNKNOWN && rtcReason != NO_MEAN) {
146-
reasonToStore = (uint8_t)rtcReason + 100; // Offset to distinguish from esp_reset_reason values
178+
// Increment total count for non-poweron resets
179+
uint16_t newCount = totalCount;
180+
if (espReason != ESP_RST_POWERON) {
181+
newCount++;
147182
}
148183

149-
// Write current boot info to NVS
150-
prefs.begin("crashlog", false); // read-write
184+
// Write updated history
185+
prefs.begin(CRASH_NAMESPACE, false); // read-write
151186

152-
prefs.putUChar(RST_REASON, reasonToStore);
153-
prefs.putUShort(RST_COUNT, newCount);
154-
prefs.putUChar(RST_ARMED, (uint8_t)DISARMED);
155-
prefs.putULong(RST_UPTIME, 0);
156-
prefs.putUChar(RST_VER_MAJ, VERSION_MAJOR);
157-
prefs.putUChar(RST_VER_MIN, VERSION_MINOR);
158-
prefs.putUChar(RST_BOOTS, newBoots);
187+
prefs.putBytes(KEY_HISTORY, newHistory, newEntryCount * sizeof(ResetEntry));
188+
prefs.putUShort(KEY_COUNT, newCount);
189+
prefs.putUChar(KEY_BOOTS, newBoots);
190+
prefs.putUChar(KEY_ARMED, (uint8_t)DISARMED);
191+
prefs.putULong(KEY_UPTIME, 0);
159192

160193
prefs.end();
161194
}
162195

163196
void crashLogHeartbeat() {
164197
Preferences prefs;
165-
prefs.begin("crashlog", false); // read-write
198+
prefs.begin(CRASH_NAMESPACE, false);
166199

167-
prefs.putULong(RST_UPTIME, millis());
168-
prefs.putUChar(RST_ARMED, (uint8_t)currentState);
200+
prefs.putULong(KEY_UPTIME, millis());
201+
prefs.putUChar(KEY_ARMED, (uint8_t)currentState);
169202

170203
prefs.end();
171204
}
172205

173206
void crashLogUpdateArmedState(DeviceState state) {
174207
Preferences prefs;
175-
prefs.begin("crashlog", false); // read-write
208+
prefs.begin(CRASH_NAMESPACE, false);
176209

177-
prefs.putUChar(RST_ARMED, (uint8_t)state);
210+
prefs.putUChar(KEY_ARMED, (uint8_t)state);
178211

179212
prefs.end();
180213
}
181214

182215
void sendCrashLogData() {
183216
Preferences prefs;
184-
prefs.begin("crashlog", true); // read-only
217+
prefs.begin(CRASH_NAMESPACE, true);
218+
219+
ResetEntry history[MAX_HISTORY] = {};
220+
size_t histLen = prefs.getBytesLength(KEY_HISTORY);
221+
int entryCount = 0;
222+
if (histLen > 0 && histLen <= sizeof(history)) {
223+
prefs.getBytes(KEY_HISTORY, history, histLen);
224+
entryCount = histLen / sizeof(ResetEntry);
225+
}
185226

186-
uint8_t reason = prefs.getUChar(RST_REASON, 0);
187-
uint16_t count = prefs.getUShort(RST_COUNT, 0);
188-
uint8_t armed = prefs.getUChar(RST_ARMED, 0);
189-
uint32_t uptime = prefs.getULong(RST_UPTIME, 0);
190-
uint8_t verMaj = prefs.getUChar(RST_VER_MAJ, 0);
191-
uint8_t verMin = prefs.getUChar(RST_VER_MIN, 0);
192-
uint8_t boots = prefs.getUChar(RST_BOOTS, 0);
227+
uint16_t totalCount = prefs.getUShort(KEY_COUNT, 0);
228+
uint8_t rapidBoots = prefs.getUChar(KEY_BOOTS, 0);
229+
uint8_t lastArmed = prefs.getUChar(KEY_ARMED, 0);
230+
uint32_t lastUptime = prefs.getULong(KEY_UPTIME, 0);
193231

194232
prefs.end();
195233

196-
// Manual JSON to avoid ArduinoJson stack usage on WebSerial task
197-
char buf[256];
198-
snprintf(buf, sizeof(buf),
199-
"{\"crash_log\":{\"reason\":\"%s\",\"code\":%d,\"count\":%d,"
200-
"\"state\":\"%s\",\"uptime_ms\":%lu,\"fw\":\"%d.%d\","
201-
"\"rapid_boots\":%d,\"boot_loop\":%s}}",
202-
resetReasonToString(reason),
203-
reason, count, stateToString(armed),
204-
(unsigned long)uptime, verMaj, verMin,
205-
boots, boots >= BOOT_LOOP_THRESHOLD ? "true" : "false");
206-
207-
DEBUG_SERIAL.println(buf);
234+
// Patch most recent entry with heartbeat data
235+
if (entryCount > 0) {
236+
history[0].armedState = lastArmed;
237+
history[0].uptimeMs = lastUptime;
238+
}
239+
240+
// Build JSON manually (stack-safe for WebSerial task)
241+
DEBUG_SERIAL.print("{\"crash_log\":{\"total\":");
242+
DEBUG_SERIAL.print(totalCount);
243+
DEBUG_SERIAL.print(",\"rapid_boots\":");
244+
DEBUG_SERIAL.print(rapidBoots);
245+
DEBUG_SERIAL.print(",\"history\":[");
246+
247+
for (int i = 0; i < entryCount; i++) {
248+
if (i > 0) DEBUG_SERIAL.print(",");
249+
ResetEntry& e = history[i];
250+
char buf[160];
251+
snprintf(buf, sizeof(buf),
252+
"{\"esp\":\"%s\",\"esp_code\":%d,\"rtc\":\"%s\",\"rtc_code\":%d,"
253+
"\"state\":\"%s\",\"uptime_ms\":%lu,\"fw\":\"%d.%d\"}",
254+
espReasonStr(e.espReason), e.espReason,
255+
rtcReasonStr(e.rtcReason), e.rtcReason,
256+
stateToString(e.armedState),
257+
(unsigned long)e.uptimeMs,
258+
e.verMajor / 10, e.verMajor % 10);
259+
DEBUG_SERIAL.print(buf);
260+
}
261+
262+
DEBUG_SERIAL.println("]}}");
208263
}

0 commit comments

Comments
 (0)