Keep the console responsive during MQTT shutdown
This commit is contained in:
1 parent
d17519fe7f
commit
f05524a4a2
15 files changed
+634
-63
No files matched your search
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
Reference in new issue
Block a user