Skip to content

Commit b3a8850

Browse files
committed
refactor(firmware): harden daemon concurrency and runtime safety
- fix micro-ROS entity lifecycle ordering and add init/fini error handling - guard power-control state transitions with a recursive mutex - replace long telemetry critical sections with mutex-based locking - gate voltmeter sampling by timer events and cap pending ISR samples - synchronize battery voltage/status reads and writes - migrate LED override globals to thread-safe state API and reset animations on mode change - validate ESP-NOW emergency payloads and add per-source receive throttling - improve settings persistence safety (rollback/unlocked save path) and lock usage - clean up web/settings handlers and apply sensor re-init on settings updates - make Wi-Fi provisioning restart logic overflow-safe and improve portal fallback behavior
1 parent 24225ed commit b3a8850

12 files changed

Lines changed: 521 additions & 163 deletions

File tree

esp_firmware/lib/app_settings/app_settings.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -604,8 +604,6 @@ bool parseMacString(const String& text, std::array<uint8_t, 6>& out) {
604604
}
605605

606606
void appSettingsToJsonImpl(JsonDocument& doc, bool include_pin_code) {
607-
syncLegacyEStopFieldsFromRoutes();
608-
609607
doc["deviceName"] = g_settings.device_name;
610608

611609
doc["pinProtectionEnabled"] = g_settings.pin_protection_enabled;
@@ -686,6 +684,6 @@ void appSettingsToJsonImpl(JsonDocument& doc, bool include_pin_code) {
686684
}
687685

688686
void appSettingsToJson(JsonDocument& doc, bool include_pin_code) {
689-
AppSettingsWriteGuard guard;
687+
AppSettingsReadGuard guard;
690688
appSettingsToJsonImpl(doc, include_pin_code);
691689
}

esp_firmware/lib/app_settings/app_settings_storage.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ class ScopedSettingsLock {
2222

2323
} // namespace
2424

25-
bool saveAppSettings() {
26-
ScopedSettingsLock lock;
27-
25+
bool saveAppSettingsUnlocked() {
2826
JsonDocument doc;
2927
app_settings_internal::appSettingsToJsonUnlocked(doc, true);
3028

@@ -42,10 +40,22 @@ bool saveAppSettings() {
4240
return written > 0;
4341
}
4442

43+
bool saveAppSettings() {
44+
ScopedSettingsLock lock;
45+
return saveAppSettingsUnlocked();
46+
}
47+
4548
bool resetAppSettingsToDefaults() {
4649
ScopedSettingsLock lock;
50+
AppSettings backup = app_settings_internal::mutableSettings();
4751
app_settings_internal::applyDefaultsUnlocked();
48-
return saveAppSettings();
52+
if (saveAppSettingsUnlocked()) {
53+
return true;
54+
}
55+
56+
// Keep runtime state consistent with failed persistence.
57+
app_settings_internal::mutableSettings() = backup;
58+
return false;
4959
}
5060

5161
bool eraseAppSettingsFromNvs() {

esp_firmware/lib/app_settings/app_settings_update.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ bool updateAppSettingsFromJson(const JsonObjectConst& json, String& error) {
366366
if (pins[i].value != pins[j].value) {
367367
continue;
368368
}
369+
settings = backup;
369370
error = String("Duplicate GPIO pin: ") +
370371
pins[i].name + " and " + pins[j].name +
371372
" both use GPIO " + String(pins[i].value);
@@ -445,8 +446,8 @@ bool updateAppSettingsFromJson(const JsonObjectConst& json, String& error) {
445446
}
446447

447448
bool verifySettingsPin(const String& pin) {
448-
ScopedSettingsWriteLock guard;
449-
AppSettings& settings = app_settings_internal::mutableSettings();
449+
AppSettingsReadGuard guard;
450+
const AppSettings& settings = guard.settings();
450451
if (!settings.pin_protection_enabled) {
451452
return true;
452453
}

esp_firmware/lib/comm/espnow_comm.cpp

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
namespace {
2222

2323
bool g_espnowReady = false;
24+
constexpr char kEStopPayload[] = "STOP";
2425

2526
int resolveEffectiveEspNowChannel() {
2627
const bool wifiConnected = (WiFi.status() == WL_CONNECTED);
@@ -62,6 +63,57 @@ void applyEspNowChannel(bool verboseLog) {
6263

6364
#if APP_MODE == APP_MODE_DAEMON
6465

66+
constexpr uint32_t kEmergencyTriggerMinIntervalMs = 50;
67+
constexpr size_t kEmergencySourceThrottleMax = 16;
68+
69+
struct EmergencySourceThrottleEntry {
70+
bool used = false;
71+
std::array<uint8_t, 6> mac = {0, 0, 0, 0, 0, 0};
72+
unsigned long last_trigger_ms = 0;
73+
};
74+
75+
std::array<EmergencySourceThrottleEntry, kEmergencySourceThrottleMax> g_emergencyThrottle = {};
76+
77+
bool shouldThrottleEmergencySource(const uint8_t* mac_addr, unsigned long now_ms) {
78+
size_t free_index = g_emergencyThrottle.size();
79+
size_t oldest_index = 0;
80+
uint32_t oldest_age_ms = 0;
81+
bool oldest_set = false;
82+
83+
for (size_t i = 0; i < g_emergencyThrottle.size(); ++i) {
84+
EmergencySourceThrottleEntry& entry = g_emergencyThrottle[i];
85+
if (!entry.used) {
86+
if (free_index == g_emergencyThrottle.size()) {
87+
free_index = i;
88+
}
89+
continue;
90+
}
91+
92+
if (memcmp(entry.mac.data(), mac_addr, 6) == 0) {
93+
if (entry.last_trigger_ms != 0 &&
94+
static_cast<uint32_t>(now_ms - entry.last_trigger_ms) < kEmergencyTriggerMinIntervalMs) {
95+
return true;
96+
}
97+
entry.last_trigger_ms = now_ms;
98+
return false;
99+
}
100+
101+
const uint32_t age_ms = static_cast<uint32_t>(now_ms - entry.last_trigger_ms);
102+
if (!oldest_set || age_ms > oldest_age_ms) {
103+
oldest_index = i;
104+
oldest_age_ms = age_ms;
105+
oldest_set = true;
106+
}
107+
}
108+
109+
const size_t target_index = (free_index < g_emergencyThrottle.size()) ? free_index : oldest_index;
110+
EmergencySourceThrottleEntry& target = g_emergencyThrottle[target_index];
111+
target.used = true;
112+
memcpy(target.mac.data(), mac_addr, 6);
113+
target.last_trigger_ms = now_ms;
114+
return false;
115+
}
116+
65117
bool resolveEmergencyTargetsForSource(
66118
const uint8_t* mac_addr,
67119
bool& target_group1,
@@ -92,6 +144,11 @@ void onDataRecv(const esp_now_recv_info_t* info, const uint8_t* incomingData, in
92144
return;
93145
}
94146

147+
if (len != static_cast<int>(sizeof(kEStopPayload) - 1) ||
148+
memcmp(incomingData, kEStopPayload, sizeof(kEStopPayload) - 1) != 0) {
149+
return;
150+
}
151+
95152
const uint8_t* mac_addr = info->src_addr;
96153
bool targetGroup1 = false;
97154
bool targetGroup2 = false;
@@ -100,6 +157,11 @@ void onDataRecv(const esp_now_recv_info_t* info, const uint8_t* incomingData, in
100157
return;
101158
}
102159

160+
const unsigned long now = millis();
161+
if (shouldThrottleEmergencySource(mac_addr, now)) {
162+
return;
163+
}
164+
103165
// Remote emergency switch is expected to repeatedly transmit "STOP".
104166
// Any authorized packet is treated as an emergency-stop trigger.
105167
triggerRemoteEmergencyStop(targetGroup1, targetGroup2, targetGroup3);
@@ -115,7 +177,6 @@ constexpr uint32_t kEStopSendIntervalMs = 80;
115177
constexpr uint32_t kEStopDebounceMs = 35;
116178
constexpr uint32_t kEStopPeerEnsureIntervalMs = 1000;
117179
constexpr uint16_t kWledHttpTimeoutMs = 1500;
118-
constexpr char kEStopPayload[] = "STOP";
119180
// Cybertruck-style boot cue + standard factory E-STOP alarm (passive buzzer approximation).
120181
constexpr uint16_t kBuzzerAlarmOnMs = 300;
121182
constexpr uint16_t kBuzzerAlarmOffMs = 240;
@@ -439,9 +500,7 @@ void updateBuzzer() {
439500
}
440501
}
441502

442-
String buildRouteSignature() {
443-
AppSettingsReadGuard settingsGuard;
444-
const AppSettings& settings = settingsGuard.settings();
503+
String buildRouteSignature(const AppSettings& settings) {
445504
String signature;
446505
signature.reserve(64);
447506
const auto& routes = settings.estop_routes;
@@ -530,7 +589,7 @@ void ensureAllRoutePeersConfigured(bool verboseLog) {
530589
void rebuildEStopRoutesFromSettings(bool verboseLog) {
531590
AppSettingsReadGuard settingsGuard;
532591
const AppSettings& settings = settingsGuard.settings();
533-
const String signature = buildRouteSignature();
592+
const String signature = buildRouteSignature(settings);
534593
if (signature == g_estopRouteSignature) {
535594
return;
536595
}

0 commit comments

Comments
 (0)