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
2215extern 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
5769static 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
6678void 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
163196void 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
173206void 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
182215void 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