Skip to content

Commit 0ba0da8

Browse files
PaulDWhiteclaude
andcommitted
Add crash logging and improve audio feedback
Add NVS-based crash log that persists reset reasons, armed state, uptime, and firmware version across reboots with boot loop detection. Expose crash data via "crash" serial command. Lower arm/disarm melody frequencies for more distinct tones, widen ESC beep range, increase note duration, and add silence gaps between notes for clearer audio separation. Remove direct ESC throttle writes from arm/disarm to fix CAN bus race condition with the throttle task's canard instance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2e402b0 commit 0ba0da8

6 files changed

Lines changed: 267 additions & 8 deletions

File tree

inc/sp140/crash_log.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#ifndef INC_SP140_CRASH_LOG_H_
2+
#define INC_SP140_CRASH_LOG_H_
3+
4+
#include <Arduino.h>
5+
#include "sp140/device_state.h"
6+
7+
// Boot loop detection thresholds
8+
#define BOOT_LOOP_THRESHOLD 5
9+
#define BOOT_LOOP_UPTIME_MS 30000 // 30 seconds
10+
11+
// Read previous crash data from NVS and print to serial.
12+
// Call early in setup(), after USBSerial.begin().
13+
void crashLogReadAndReport();
14+
15+
// Write current state as a heartbeat to NVS.
16+
// Call periodically from a low-priority task (e.g., monitoringTask).
17+
void crashLogHeartbeat();
18+
19+
// Update the persisted armed state in NVS.
20+
// Call from changeDeviceState().
21+
void crashLogUpdateArmedState(DeviceState state);
22+
23+
// Send crash log data as JSON over serial.
24+
// Called by the "crash" serial command.
25+
void sendCrashLogData();
26+
27+
#endif // INC_SP140_CRASH_LOG_H_

src/sp140/buzzer.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ bool playMelody(uint16_t melody[], int siz) {
7979
MelodyRequest request = {
8080
.notes = melodyBuffer,
8181
.size = (uint8_t)std::min(siz, 32),
82-
.duration = 100 // Default duration
82+
.duration = 200 // Default duration
8383
};
8484

8585
// Send to queue with timeout

src/sp140/crash_log.cpp

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
#include "sp140/crash_log.h"
2+
3+
#include <Arduino.h>
4+
#include <Preferences.h>
5+
#include <esp_system.h>
6+
#include <rom/rtc.h>
7+
8+
#include "version.h"
9+
#include "sp140/device_state.h"
10+
11+
#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";
21+
22+
extern DeviceState currentState;
23+
24+
static const char* resetReasonToString(int reason) {
25+
// ESP-IDF reset reasons (0-15)
26+
switch (reason) {
27+
case ESP_RST_POWERON: return "POWERON";
28+
case ESP_RST_SW: return "SW_RESET";
29+
case ESP_RST_PANIC: return "PANIC";
30+
case ESP_RST_INT_WDT: return "INT_WDT";
31+
case ESP_RST_TASK_WDT: return "TASK_WDT";
32+
case ESP_RST_WDT: return "WDT";
33+
case ESP_RST_DEEPSLEEP: return "DEEPSLEEP";
34+
case ESP_RST_BROWNOUT: return "BROWNOUT";
35+
case ESP_RST_SDIO: return "SDIO";
36+
case ESP_RST_EXT: return "EXTERNAL";
37+
default: break;
38+
}
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+
}
53+
}
54+
return "UNKNOWN";
55+
}
56+
57+
static const char* stateToString(uint8_t state) {
58+
switch (state) {
59+
case DISARMED: return "DISARMED";
60+
case ARMED: return "ARMED";
61+
case ARMED_CRUISING: return "ARMED_CRUISING";
62+
default: return "UNKNOWN";
63+
}
64+
}
65+
66+
void crashLogReadAndReport() {
67+
esp_reset_reason_t currentReason = esp_reset_reason();
68+
RESET_REASON rtcReason = rtc_get_reset_reason(0); // Core 0
69+
70+
// Read previous crash data from NVS
71+
Preferences prefs;
72+
prefs.begin("crashlog", true); // read-only
73+
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);
81+
82+
prefs.end();
83+
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.");
89+
} 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);
112+
}
113+
114+
// Boot loop detection
115+
uint8_t newBoots = 0;
116+
if (prevUptime > 0 && prevUptime < BOOT_LOOP_UPTIME_MS) {
117+
newBoots = prevBoots + 1;
118+
}
119+
120+
if (newBoots >= BOOT_LOOP_THRESHOLD) {
121+
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("!!!!!!!!!!!!!!!!!!!!!!!!!!!");
126+
}
127+
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:");
133+
DEBUG_SERIAL.print((int)rtcReason);
134+
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++;
141+
}
142+
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
147+
}
148+
149+
// Write current boot info to NVS
150+
prefs.begin("crashlog", false); // read-write
151+
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);
159+
160+
prefs.end();
161+
}
162+
163+
void crashLogHeartbeat() {
164+
Preferences prefs;
165+
prefs.begin("crashlog", false); // read-write
166+
167+
prefs.putULong(RST_UPTIME, millis());
168+
prefs.putUChar(RST_ARMED, (uint8_t)currentState);
169+
170+
prefs.end();
171+
}
172+
173+
void crashLogUpdateArmedState(DeviceState state) {
174+
Preferences prefs;
175+
prefs.begin("crashlog", false); // read-write
176+
177+
prefs.putUChar(RST_ARMED, (uint8_t)state);
178+
179+
prefs.end();
180+
}
181+
182+
void sendCrashLogData() {
183+
Preferences prefs;
184+
prefs.begin("crashlog", true); // read-only
185+
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);
193+
194+
prefs.end();
195+
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);
208+
}

src/sp140/esc.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ static volatile PendingEscTone sPendingEscTone = PendingEscTone::NONE;
3131

3232
namespace {
3333

34-
constexpr uint8_t kEscToneLow = 3;
35-
constexpr uint8_t kEscToneHigh = 6;
34+
constexpr uint8_t kEscToneLow = 2;
35+
constexpr uint8_t kEscToneHigh = 8;
3636
constexpr uint8_t kEscToneVolumePct = 80;
37-
constexpr uint8_t kEscToneDuration10ms = 10;
37+
constexpr uint8_t kEscToneDuration10ms = 20;
3838

3939
// Caller must pass ARM or DISARM (never NONE).
4040
void buildEscMotorTone(uint8_t* out, PendingEscTone tone) {

src/sp140/extra-data.ino

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <Preferences.h> // Add ESP32 Preferences library
55
#include "../../inc/sp140/throttle.h"
6+
#include "../../inc/sp140/crash_log.h"
67

78
/**
89
* WebSerial Protocol Documentation
@@ -216,6 +217,9 @@ void parse_serial_commands() {
216217
} else if (command == "sync") {
217218
send_device_data();
218219
return;
220+
} else if (command == "crash") {
221+
sendCrashLogData();
222+
return;
219223
}
220224
}
221225

src/sp140/sp140.ino

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "../../inc/sp140/ble/esc_service.h"
3636

3737
#include "../../inc/sp140/buzzer.h"
38+
#include "../../inc/sp140/crash_log.h"
3839
#include "../../inc/sp140/device_state.h"
3940
#include "../../inc/sp140/mode.h"
4041
#include "../../inc/sp140/throttle.h"
@@ -248,6 +249,8 @@ void changeDeviceState(DeviceState newState) {
248249
xQueueOverwrite(deviceStateQueue, &state); // Always use latest state
249250
}
250251

252+
crashLogUpdateArmedState(newState);
253+
251254
USBSerial.print("Device State Changed to: ");
252255
switch (newState) {
253256
case DISARMED:
@@ -492,11 +495,18 @@ void refreshDisplay() {
492495

493496
void monitoringTask(void *pvParameters) {
494497
TelemetrySnapshot snap;
498+
static uint32_t lastCrashHeartbeat = 0;
495499
for (;;) {
496500
if (xQueueReceive(telemetrySnapshotQueue, &snap, pdMS_TO_TICKS(100)) == pdTRUE) {
497501
// Run monitors using the fresh snapshot
498502
if (monitoringEnabled) {
499503
checkAllSensorsWithData(snap.esc, snap.bms);
504+
505+
// Periodic crash log heartbeat (every 30s)
506+
if (millis() - lastCrashHeartbeat >= 30000) {
507+
crashLogHeartbeat();
508+
lastCrashHeartbeat = millis();
509+
}
500510
}
501511
}
502512
}
@@ -612,6 +622,8 @@ void setup() {
612622
USBSerial.print("Build date/time: ");
613623
USBSerial.println(buildDate);
614624

625+
crashLogReadAndReport();
626+
615627
// Pull CSB (pin 42) high to activate I2C mode
616628
// temporary fix TODO remove
617629
digitalWrite(42, HIGH);
@@ -911,7 +923,9 @@ void printTime(const char* label) {
911923
}
912924

913925
void disarmESC() {
914-
setESCThrottle(ESC_DISARMED_PWM);
926+
// Throttle task sends ESC_DISARMED_PWM on its next 20ms cycle
927+
// when it sees DISARMED state — no direct CAN write needed here
928+
// to avoid race condition with throttle task's canard instance
915929
}
916930

917931
// reset smoothing
@@ -924,7 +938,7 @@ void resumeLEDTask() {
924938
}
925939

926940
void runDisarmAlert() {
927-
u_int16_t disarm_melody[] = { 2637, 2093 };
941+
u_int16_t disarm_melody[] = { 1568, 1047 };
928942
playMelody(disarm_melody, 2);
929943
queueEscMotorBeepDisarm();
930944
pulseVibeMotor();
@@ -1162,9 +1176,10 @@ bool throttleEngaged() {
11621176

11631177
// get the PPG ready to fly
11641178
bool armSystem() {
1165-
uint16_t arm_melody[] = { 2093, 2637 };
1179+
uint16_t arm_melody[] = { 1047, 1568 };
11661180
// const unsigned int arm_vibes[] = { 1, 85, 1, 85, 1, 85, 1 };
1167-
setESCThrottle(ESC_DISARMED_PWM); // initialize the signal to low
1181+
// Throttle task handles ESC commands exclusively to avoid
1182+
// race condition on the shared canard CAN bus instance
11681183

11691184
armedAtMillis = millis();
11701185
armedSecs = 0; // Reset armed seconds for new session
@@ -1229,6 +1244,11 @@ void audioTask(void* parameter) {
12291244
TickType_t delayTicks = pdMS_TO_TICKS(request.duration);
12301245
if (delayTicks == 0) { delayTicks = 1; } // Ensure non-zero delay
12311246
vTaskDelayUntil(&nextWakeTime, delayTicks);
1247+
// Add silence gap between notes for distinct separation
1248+
if (i < request.size - 1) {
1249+
stopTone();
1250+
vTaskDelayUntil(&nextWakeTime, pdMS_TO_TICKS(80));
1251+
}
12321252
}
12331253
stopTone();
12341254
}

0 commit comments

Comments
 (0)