Keep the console responsive during MQTT shutdown

This commit is contained in:
mikecarper committed 2026-10-05 19:05:58 -07:00
1 parent d17519fe7f
commit f05524a4a2
15 files changed
+634 -63

No files matched your search

+21 -3
View File
@@ -3786,6 +3786,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error: usage set logging.output off|usb|wifi|both");
return;
}
if (wifi_enabled && _callbacks->isMqttBridgeStopping()) {
strcpy(reply, "Error: MQTT is stopping; retry when mqtt.stopping is off");
return;
}
const uint8_t previous_usb = _prefs->usb_logging_enabled;
const uint8_t previous_bridge = _prefs->bridge_enabled;
_prefs->usb_logging_enabled = usb_enabled ? 1 : 0;
@@ -3797,7 +3801,9 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
return;
}
mesh::setUsbLoggingEnabled(usb_enabled);
if (!_callbacks->setMqttBridgeState(wifi_enabled)) {
const bool applied = wifi_enabled ? _callbacks->setMqttBridgeState(true)
: _callbacks->requestMqttBridgeStop();
if (!applied) {
strcpy(reply, "Error: logging output saved, but MQTT runtime change failed");
return;
}
@@ -3856,9 +3862,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error: use set mqtt.enabled on|off");
} else {
const bool enable = strcmp(value, "on") == 0;
if (enable && _callbacks->isMqttBridgeStopping()) {
strcpy(reply, "Error: MQTT is stopping; retry when mqtt.stopping is off");
return;
}
const uint8_t previous = _prefs->bridge_enabled;
_prefs->bridge_enabled = enable ? 1 : 0;
const bool applied = _callbacks->setMqttBridgeState(enable);
savePrefs();
// A cooperative stop cannot be cancelled safely once signalled. Save
// intent first so a failed write leaves the running service unchanged.
if (!trySavePrefs()) {
_prefs->bridge_enabled = previous;
strcpy(reply, "Error: MQTT setting not saved; unchanged");
return;
}
const bool applied = enable ? _callbacks->setMqttBridgeState(true)
: _callbacks->requestMqttBridgeStop();
strcpy(reply, applied ? "OK"
: "Error: MQTT runtime change failed; setting saved");
}
+4
View File
@@ -603,6 +603,10 @@ public:
// legacy bridge behavior by default. Combined bridge roles override
// them to let each transport be started, stopped, and queried independently.
virtual bool setMqttBridgeState(bool enable) { return setBridgeState(enable); }
// CLI disable may acknowledge before worker-owned TLS teardown finishes.
// Ownership barriers continue to use the synchronous state setter.
virtual bool requestMqttBridgeStop() { return setMqttBridgeState(false); }
virtual bool isMqttBridgeStopping() { return false; }
virtual bool setEspNowBridgeState(bool enable) { return setBridgeState(enable); }
virtual bool setRs232BridgeState(bool enable) { return setBridgeState(enable); }
virtual bool restartMqttBridge() { return restartBridge(); }
+4
View File
@@ -880,6 +880,10 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
snprintf(reply, 160, "> %s", _callbacks->isMqttBridgeRunning() ? "on" : "off");
return true;
}
if (strcmp(config, "mqtt.stopping") == 0) {
snprintf(reply, 160, "> %s", _callbacks->isMqttBridgeStopping() ? "on" : "off");
return true;
}
if (memcmp(config, "snmp.community", 14) == 0) {
sprintf(reply, "> %s", _mqtt_prefs.snmp_community);
} else if (memcmp(config, "snmp", 4) == 0 && (config[4] == '\0' || config[4] == '\n' || config[4] == '\r')) {
+102 -39
View File
@@ -683,7 +683,7 @@ void MQTTBridge::formatSlotDiagReply(char* buf, size_t bufsize, int slot_index)
// end() returns as soon as the task acks (it checks _stop_acked before ticking
// the timeout), so a larger bound does NOT slow a healthy stop -- it only length-
// ens the wait before force-killing a genuinely wedged task. The timeout is set
// per stop in end() via computeStopTimeoutMs() based on the enabled-slot count.
// per stop in requestStop() based on the enabled-slot count.
static const uint32_t MQTT_STOP_TIMEOUT_BASE_MS = 5000; // fixed teardown overhead
static const uint32_t MQTT_STOP_TIMEOUT_PER_SLOT_MS = 8000; // ~5-6 s measured + headroom
@@ -899,8 +899,8 @@ void MQTTBridge::begin() {
// Idempotent start: a second begin() would re-run allocation and re-create
// the task, leaking the previous queue and task.
if (_initialized) {
MQTT_DEBUG_PRINTLN("MQTT Bridge already running - begin() ignored");
if (_initialized || _lifecycle.isStopInProgress()) {
MQTT_DEBUG_PRINTLN("MQTT Bridge running or stopping - begin() ignored");
return;
}
@@ -1078,6 +1078,10 @@ void MQTTBridge::begin() {
}
}
// Reset the handshake before starting either the worker or cooperative loop.
_stop_requested.store(false, std::memory_order_release);
_stop_acked.store(false, std::memory_order_release);
#ifdef ESP_PLATFORM
// Create FreeRTOS queue; use PSRAM storage when available
#ifdef BOARD_HAS_PSRAM
@@ -1121,11 +1125,6 @@ void MQTTBridge::begin() {
// Task stack: dynamic allocation (internal RAM). A PSRAM-backed stack was tried and
// reverted -- it resets some boards (e.g. Heltec V4) when the task runs from PSRAM.
_mqtt_task_handle = nullptr;
// Clear the cooperative-stop handshake before the new task starts reading it.
// deliverStop() leaves _stop_requested latched true after a stop cycle, so a
// restart must reset it or the fresh task would self-terminate immediately.
_stop_requested = false;
_stop_acked = false;
BaseType_t create_result = xTaskCreatePinnedToCore(
mqttTask,
"MQTTBridge",
@@ -1179,16 +1178,11 @@ void MQTTBridge::begin() {
}
// ---------------------------------------------------------------------------
// end()
// requestStop() - signal only; worker retains ownership until acknowledgment
// ---------------------------------------------------------------------------
void MQTTBridge::end() {
MQTT_DEBUG_PRINTLN("Stopping MQTT Bridge...");
// Idempotent stop: nothing to tear down if we never started (or already stopped).
if (!_initialized) {
MQTT_DEBUG_PRINTLN("MQTT Bridge already stopped - end() ignored");
return;
}
void MQTTBridge::requestStop() {
// A repeated request must not clear an acknowledgment or restart its budget.
if (!_initialized || _lifecycle.isStopInProgress()) return;
// Stop new diagnostic reads through the singleton before teardown begins.
s_mqtt_bridge_instance = nullptr;
@@ -1216,12 +1210,28 @@ void MQTTBridge::end() {
// task mid-mbedTLS and then free client buffers on a corrupted heap.
_lifecycle.requestStop(); // Running -> StopRequested; deliverStop() sets _stop_requested
#ifndef ESP_PLATFORM
// The cooperative implementation has no worker to acknowledge separately.
_stop_acked.store(true, std::memory_order_release);
_lifecycle.onTaskStopped();
finishStopped();
#endif
}
// ---------------------------------------------------------------------------
// end() - synchronous barrier for OTA and other ownership transfers
// ---------------------------------------------------------------------------
void MQTTBridge::end() {
MQTT_DEBUG_PRINTLN("Stopping MQTT Bridge...");
if (!_initialized) return;
requestStop(); // Joins an existing request without re-delivering its signal.
#ifdef ESP_PLATFORM
// Wait (bounded) for the task to acknowledge. tick() synthesizes the timeout
// fallback if the task never acks. Checking the ack first each iteration means
// a stop that completes right as the timeout expires is still treated as clean.
while (_lifecycle.isStopInProgress()) {
if (_stop_acked) {
if (_stop_acked.load(std::memory_order_acquire)) {
_lifecycle.onTaskStopped(); // StopRequested -> Stopped (clean): releaseResources()
break;
}
@@ -1229,14 +1239,12 @@ void MQTTBridge::end() {
if (!_lifecycle.isStopInProgress()) break;
vTaskDelay(pdMS_TO_TICKS(20));
}
#else
// Non-ESP32: the bridge runs cooperatively in loop(); there is no separate
// task to signal. Drive straight to a clean Stopped and let releaseResources()
// perform the (unchanged) synchronous teardown.
_stop_acked = true;
_lifecycle.onTaskStopped();
#endif
finishStopped();
}
void MQTTBridge::finishStopped() {
if (!_initialized || _lifecycle.isStopInProgress()) return;
// Timezone is inline class storage (_timezone_storage) - nothing to delete.
// The shared JSON document's pools were freed by releaseRuntimeBuffers() above.
_initialized = false;
@@ -1249,6 +1257,7 @@ void MQTTBridge::end() {
_ntp_estimate_done = false;
_ntp_estimate_ok = false;
_ntp_estimate_epoch = 0;
_staged_raw_valid = false;
MQTT_DEBUG_PRINTLN("MQTT Bridge stopped (%s)",
_lifecycle.stopTimedOut() ? "forced/timeout - OTA blocked" : "clean");
}
@@ -1256,7 +1265,7 @@ void MQTTBridge::end() {
// ---------------------------------------------------------------------------
// LifecycleOps - binds MQTTLifecycle::Ops (the pure, host-tested spec) to the
// FreeRTOS / PsychicMqttClient runtime. Every method runs on the loop task
// (Core 1): the Coordinator that calls them is driven only from begin()/end().
// (Core 1): begin(), requestStop(), end(), and loop() own the Coordinator.
// ---------------------------------------------------------------------------
uint32_t MQTTBridge::LifecycleOps::nowMs() {
return (uint32_t)millis();
@@ -1271,8 +1280,8 @@ void MQTTBridge::LifecycleOps::deliverStop() {
// Clear any stale ack before raising the request (same ordering as the NTP
// handshake: clear the done-flag, then set the request). The MQTT task polls
// _stop_requested at the top of mqttTaskLoop().
_b->_stop_acked = false;
_b->_stop_requested = true;
_b->_stop_acked.store(false, std::memory_order_release);
_b->_stop_requested.store(true, std::memory_order_release);
}
void MQTTBridge::LifecycleOps::releaseResources() {
@@ -1426,6 +1435,23 @@ void MQTTBridge::initializeWiFiInTask() {
MQTT_DEBUG_PRINTLN("WiFi initialization started in task");
}
// ---------------------------------------------------------------------------
// Preserve startup/TLS spacing while allowing the owning task to stop promptly.
// Unsigned tick subtraction remains valid across the FreeRTOS tick wrap.
bool MQTTBridge::waitUnlessStopping(uint32_t delay_ms) {
const TickType_t duration = pdMS_TO_TICKS(delay_ms);
const TickType_t started = xTaskGetTickCount();
TickType_t quantum = pdMS_TO_TICKS(20);
if (quantum == 0) quantum = 1;
while (!_stop_requested) {
const TickType_t elapsed = xTaskGetTickCount() - started;
if (elapsed >= duration) return !_stop_requested;
const TickType_t remaining = duration - elapsed;
vTaskDelay(remaining < quantum ? remaining : quantum);
}
return false;
}
// ---------------------------------------------------------------------------
// mqttTaskLoop() - main loop running on Core 0
// ---------------------------------------------------------------------------
@@ -1434,7 +1460,7 @@ void MQTTBridge::mqttTaskLoop() {
initializeWiFiInTask();
// Wait a bit for WiFi to start connecting
vTaskDelay(pdMS_TO_TICKS(1000));
waitUnlessStopping(1000);
// Main task loop
#ifdef MQTT_MEMORY_DEBUG
@@ -1454,7 +1480,7 @@ void MQTTBridge::mqttTaskLoop() {
teardownSlot(i);
}
destroySlotClients();
_stop_acked = true; // release semantics: set only after teardown is done
_stop_acked.store(true, std::memory_order_release); // teardown precedes Core 1 release
return;
}
@@ -1556,6 +1582,10 @@ void MQTTBridge::mqttTaskLoop() {
}
#endif
// Network/time probes may have yielded while Core 1 requested a stop.
// Return to the owner-core teardown before allocating another client.
if (_stop_requested) continue;
// Deferred slot setup: wait until NTP is synced so JWT tokens get valid timestamps.
// This avoids wasted TLS handshakes that get rejected due to bad token times.
if (_ntp_synced.load(std::memory_order_acquire) && !_slots_setup_done) {
@@ -1570,6 +1600,7 @@ void MQTTBridge::mqttTaskLoop() {
MQTT_DEBUG_PRINTLN("NTP synced, setting up MQTT slots (max %d active)...", _max_active_slots);
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_stop_requested) break;
if (_slots[i].enabled) {
if (!canActivateSlot(i)) {
MQTT_DEBUG_PRINTLN("MQTT%d skipped: max active slots (%d) reached", i + 1, _max_active_slots);
@@ -1588,20 +1619,26 @@ void MQTTBridge::mqttTaskLoop() {
// Stagger connections: 5s between slots to avoid simultaneous TLS handshakes
// which compete for ~40KB internal heap each
if (i < RUNTIME_MQTT_SLOTS - 1) {
vTaskDelay(pdMS_TO_TICKS(5000));
if (!waitUnlessStopping(5000)) break;
}
}
}
}
// A stop during a stagger must not fall through into reconfiguration,
// publication, or connection maintenance before the next loop iteration.
if (_stop_requested) continue;
// Process pending slot reconfigures (queued from CLI on Core 1)
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_stop_requested) break;
if (_slot_reconfigure_pending[i]) {
_slot_reconfigure_pending[i] = false;
MQTT_DEBUG_PRINTLN("Applying deferred reconfigure for MQTT%d (preset: %s)", i + 1, _obs->mqtt_slot_preset[i]);
applySlotPreset(i, _obs->mqtt_slot_preset[i]);
}
}
if (_stop_requested) continue;
// Publish on-connect status for slots whose onConnect callback fired since
// the last loop. Raised on the esp-mqtt event task, consumed here on the
@@ -1610,17 +1647,21 @@ void MQTTBridge::mqttTaskLoop() {
// reconnect during the publish re-arms for the next loop rather than being
// lost; publishStatusToSlot() re-checks slot.connected and no-ops if dropped.
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_stop_requested) break;
if (_status_publish_pending[i]) {
_status_publish_pending[i] = false;
publishStatusToSlot(i);
}
}
if (_stop_requested) continue;
// Maintain slot connections (token renewal, reconnect with backoff)
maintainSlotConnections();
if (_stop_requested) continue;
// Process packet queue
processPacketQueue();
if (_stop_requested) continue;
#if defined(WITH_MQTT_NEIGHBORS)
// Consume a pending neighbors snapshot handed over by the mesh (Core 1).
@@ -1939,6 +1980,7 @@ bool MQTTBridge::canActivateSlot(int index) const {
// maintainSlotConnections() will retry it -- the allocation failures below are transient
// memory conditions, not permanent misconfiguration.
bool MQTTBridge::setupSlot(int index) {
if (_stop_requested.load(std::memory_order_acquire)) return false;
if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return false;
MQTTSlot& slot = _slots[index];
@@ -2209,6 +2251,7 @@ void MQTTBridge::teardownSlot(int index, bool force) {
esp_err_t MQTTBridge::reconnectSlotClient(int index) {
if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return ESP_ERR_INVALID_ARG;
if (_stop_requested.load(std::memory_order_acquire)) return ESP_ERR_INVALID_STATE;
MQTTSlot& slot = _slots[index];
if (slot.client == nullptr) return ESP_ERR_INVALID_STATE;
@@ -2234,6 +2277,7 @@ esp_err_t MQTTBridge::reconnectSlotClient(int index) {
}
void MQTTBridge::maintainSlotConnections() {
if (_stop_requested.load(std::memory_order_acquire)) return;
if (!_identity) return;
// Check WiFi status first
@@ -2271,6 +2315,7 @@ void MQTTBridge::maintainSlotConnections() {
bool setup_retry_this_cycle = false;
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_stop_requested) return;
if (!_slots[i].enabled) continue;
// JWT slots need time sync before we can manage tokens
@@ -2617,6 +2662,7 @@ bool MQTTBridge::createSlotAuthToken(int index) {
}
bool MQTTBridge::publishToSlot(int index, const char* topic, const char* payload, size_t payload_len, bool retained, uint8_t qos) {
if (_stop_requested.load(std::memory_order_acquire)) return false;
if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return false;
MQTTSlot& slot = _slots[index];
if (!slot.client || !slot.connected) {
@@ -2854,6 +2900,7 @@ bool MQTTBridge::isAnySlotConnected() {
}
void MQTTBridge::setSlotPreset(int slot_index, const char* preset_name) {
if (_stop_requested.load(std::memory_order_acquire)) return;
if (slot_index < 0 || slot_index >= RUNTIME_MQTT_SLOTS) return;
// On ESP32, teardown/setup involves TLS and must run on the MQTT task (Core 0).
@@ -2871,6 +2918,7 @@ void MQTTBridge::setSlotPreset(int slot_index, const char* preset_name) {
}
void MQTTBridge::applySlotPreset(int slot_index, const char* preset_name) {
if (_stop_requested.load(std::memory_order_acquire)) return;
if (slot_index < 0 || slot_index >= RUNTIME_MQTT_SLOTS) return;
MQTTSlot& slot = _slots[slot_index];
@@ -2943,6 +2991,7 @@ void MQTTBridge::applySlotPreset(int slot_index, const char* preset_name) {
void MQTTBridge::setSlotCustomBroker(int slot_index, const char* host, uint16_t port,
const char* username, const char* password) {
if (_stop_requested.load(std::memory_order_acquire)) return;
if (slot_index < 0 || slot_index >= RUNTIME_MQTT_SLOTS) return;
MQTTSlot& slot = _slots[slot_index];
@@ -3085,7 +3134,8 @@ bool MQTTBridge::handleWiFiConnection(unsigned long now) {
}
bool MQTTBridge::isReady() const {
return _initialized && isWiFiConfigValid(_obs);
return _initialized && !_stop_requested.load(std::memory_order_acquire)
&& isWiFiConfigValid(_obs);
}
bool MQTTBridge::isIATAValid() const {
@@ -3135,13 +3185,20 @@ bool MQTTBridge::isSlotReady(int index, char* reason_buf, size_t reason_size) co
}
// ---------------------------------------------------------------------------
// loop() - non-ESP32 main loop (ESP32 uses mqttTaskLoop via FreeRTOS task)
// loop() - Core 1 stop reaping on ESP32; cooperative processing elsewhere
// ---------------------------------------------------------------------------
void MQTTBridge::loop() {
if (!_initialized) return;
#ifdef ESP_PLATFORM
// On ESP32, loop() is a no-op - all processing happens in the FreeRTOS task
// Poll only: a slow TLS teardown must not stall radio/UART forwarding or
// force-delete the worker. Only end(), the synchronous OTA barrier, owns
// the existing bounded-timeout fallback.
if (_lifecycle.isStopInProgress()
&& _stop_acked.load(std::memory_order_acquire)) {
_lifecycle.onTaskStopped();
finishStopped();
}
return;
#else
unsigned long now = millis();
@@ -3271,7 +3328,8 @@ void MQTTBridge::loop() {
// ---------------------------------------------------------------------------
void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
if (!_initialized || !_obs->mqtt_packets_enabled || !_obs->mqtt_rx_enabled) return;
if (!_initialized || _stop_requested.load(std::memory_order_acquire)
|| !_obs->mqtt_packets_enabled || !_obs->mqtt_rx_enabled) return;
// Drop before the queue copy when no configured slot allows this payload
// type. A QueuedPacket carries the packet plus up to 256 bytes of raw radio
@@ -3295,7 +3353,8 @@ void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
void MQTTBridge::sendPacket(mesh::Packet *packet) {
uint8_t tx_mode = _obs->mqtt_tx_enabled; // Read live from prefs (no restart needed)
if (!_initialized || !_obs->mqtt_packets_enabled || tx_mode == 0) return;
if (!_initialized || _stop_requested.load(std::memory_order_acquire)
|| !_obs->mqtt_packets_enabled || tx_mode == 0) return;
// Advert mode: only queue self-originated advert packets
if (tx_mode == 2) {
@@ -4000,7 +4059,7 @@ void MQTTBridge::requestPublishNeighbors(const char* json, size_t len) {
if (_neighbors_publish_pending.load(std::memory_order_acquire)) return;
// A discovery can finish after bridge shutdown. Do not allocate a buffer
// which has no task left to consume or release it.
if (!isRunning()) return;
if (!isRunning() || _stop_requested.load(std::memory_order_acquire)) return;
_neighbors_json_buffer = static_cast<char*>(
MQTTRuntimeBufferLifecycle::allocateIfMissing(
_neighbors_json_buffer, NEIGHBORS_JSON_BUFFER_SIZE, psram_malloc));
@@ -4047,6 +4106,7 @@ bool MQTTBridge::publishNeighbors() {
// ---------------------------------------------------------------------------
void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) {
if (!_initialized || _stop_requested.load(std::memory_order_acquire)) return;
#ifdef ESP_PLATFORM
// Use FreeRTOS queue for thread-safe operation
if (_packet_queue_handle == nullptr) {
@@ -4174,6 +4234,7 @@ void MQTTBridge::dequeuePacket() {
// ---------------------------------------------------------------------------
void MQTTBridge::storeRawRadioData(const uint8_t* raw_data, int len, float snr, float rssi) {
if (!_initialized || _stop_requested.load(std::memory_order_acquire)) return;
// Writes into the Core 1-only staging area. No mutex needed: this function and
// queuePacket() are both called from Core 1 in guaranteed sequence for each packet.
if (len > 0 && len <= (int)LAST_RAW_DATA_SIZE) {
@@ -4549,7 +4610,7 @@ bool MQTTBridge::syncTimeWithNTP(bool force, bool primary_only) {
}
bool MQTTBridge::requestForcedNtpSync(uint32_t timeout_ms) {
if (!isRunning()) return false;
if (!isRunning() || _stop_requested.load(std::memory_order_acquire)) return false;
// Publish the request to the MQTT task. Clear the completion flags before
// raising _ntp_force_requested so the task can't observe a stale result.
@@ -4618,7 +4679,8 @@ void MQTTBridge::runNtpEstimateProbe() {
}
bool MQTTBridge::requestNtpTimeEstimate() {
if (!isRunning() || WiFi.status() != WL_CONNECTED || _ntp_estimate_requested) return false;
if (!isRunning() || _stop_requested.load(std::memory_order_acquire)
|| WiFi.status() != WL_CONNECTED || _ntp_estimate_requested) return false;
_ntp_estimate_done = false;
_ntp_estimate_ok = false;
_ntp_estimate_epoch = 0;
@@ -4636,7 +4698,8 @@ bool MQTTBridge::takeNtpTimeEstimate(uint32_t& epoch, bool& finished) {
}
bool MQTTBridge::ntpDiag(char* reply, size_t reply_size, bool verbose) {
if (!isRunning() || reply == nullptr || reply_size == 0) return false;
if (!isRunning() || _stop_requested.load(std::memory_order_acquire)
|| reply == nullptr || reply_size == 0) return false;
// Marshal the probe onto the MQTT task (Core 0); clear the completion flag first.
_ntp_diag_done = false;
+14 -9
View File
@@ -300,12 +300,11 @@ private:
// Cooperative-shutdown handshake (Phase 5). The loop task (Core 1) raises
// _stop_requested through the lifecycle Coordinator; the MQTT task (Core 0)
// sees it, tears down its own clients on Core 0 (where the mbedTLS contexts
// live), sets _stop_acked LAST, and self-terminates. end() waits for the ack
// before freeing the queue/buffers. Plain volatile matches the existing
// NTP/reconfigure handshake idiom above; replacing all of these with a command
// channel / task notifications is explicitly deferred (see MQTT_OWNERSHIP.md).
volatile bool _stop_requested = false;
volatile bool _stop_acked = false;
// live), sets _stop_acked LAST, and self-terminates. end() waits for the ack;
// loop() can instead reap it without blocking. Atomic release/acquire ordering
// makes client teardown visible before Core 1 frees the queue/buffers.
std::atomic<bool> _stop_requested{false};
std::atomic<bool> _stop_acked{false};
// Timezone handling.
// _timezone_storage is inline class storage (zero heap) that is reconfigured
@@ -515,6 +514,7 @@ private:
#ifdef ESP_PLATFORM
static void mqttTask(void* parameter);
void mqttTaskLoop(); // Main loop for MQTT task
bool waitUnlessStopping(uint32_t delay_ms);
void initializeWiFiInTask(); // WiFi initialization moved to task
#endif
bool publishPacket(mesh::Packet* packet, bool is_tx, bool& has_eligible_target,
@@ -551,13 +551,14 @@ private:
// of making the bridge unusable.
void allocateRuntimeBuffers();
void releaseRuntimeBuffers();
void finishStopped(); // Core 1 only, after lifecycle resource release
// --- Cooperative lifecycle (Phase 5) ---------------------------------------
// The pure state machine, bounded stop timeout, and OTA barrier live in
// src/helpers/MQTTLifecycle.h and are host-tested by test/test_mqtt_lifecycle/.
// This nested Ops binds that spec to FreeRTOS/PsychicMqttClient. The
// Coordinator is owned and driven ONLY by the loop task (Core 1) from
// begin()/end(); the MQTT task (Core 0) communicates solely through the
// begin()/requestStop()/end()/loop(); the MQTT task (Core 0) communicates solely through the
// _stop_requested/_stop_acked flags above. Methods are defined in the .cpp.
class LifecycleOps : public MQTTLifecycle::Ops {
public:
@@ -587,6 +588,10 @@ public:
void begin() override;
void end() override;
// Core 1: request cooperative teardown without waiting or freeing resources.
// Call loop() to reap the acknowledgment; end() remains the OTA stop barrier.
void requestStop();
bool isStopping() const { return _lifecycle.isStopInProgress(); }
bool isRunning() const override { return _initialized; }
void loop() override;
void onPacketReceived(mesh::Packet *packet) override;
@@ -681,8 +686,8 @@ public:
int getConnectedBrokers() const;
int getQueueSize() const;
bool isReady() const;
/** True only after a CLEAN cooperative stop -- end() received the MQTT task's
* acknowledgment within the timeout. A timed-out/forced stop returns false so
/** True only after a CLEAN cooperative stop -- the MQTT task acknowledged
* its teardown before resources were released. A timed-out/forced stop returns false so
* OTA flashing is withheld until a clean start/stop cycle. Mirrors
* MQTTLifecycle::mayBeginFlash(); read on the loop task (Core 1). */
bool canFlashAfterStop() const { return _lifecycle.mayBeginFlash(); }