connect() logged "MQTT client started." unconditionally, so a failing
esp_mqtt_client_start() looked identical to a successful one. That is the one
state a later reconnect() cannot recover from, which made it the worst possible
line to be wrong.
The scheduled JWT bounce called PsychicMqttClient::disconnect(), which ends
with esp_mqtt_client_stop(). That ends the client task and returns its 6 KiB
stack to the heap at the moment the TLS teardown vacates two 16 KiB mbedTLS
record buffers, so the stack lands in that hole and the next handshake cannot
reuse it. On non-PSRAM boards the largest free block then ratchets down 16 KiB
at a time while total free heap stays flat.
Soak evidence from a Heltec V3 on 8d1a0eb3: 43 of 60 disconnects had no
preceding transport error, i.e. they were this proactive bounce rather than a
broker FIN, and two of the three max_alloc steps landed within 5 s of one.
Losing a whole TLS session later returned exactly 16,384 bytes of contiguity.
softDisconnect() closes the transport without the stop, so the task and its
stack stay put across the handshake. The bounce uses it plus reconnect(), and
falls back to connect() when the client really is stopped, since reconnect()
is a silent no-op in that state.
Also corrects a comment claiming the mbedTLS context survives a transport
close: only the esp-mqtt client object does.
(cherry picked from commit 10cf5cf48fb009e751e25b37fcc1f3d1256ddbbc)
The esp-mqtt task drains only one QUEUED outbox item per loop iteration, and
each iteration blocks up to MQTT_POLL_READ_TIMEOUT_MS (1s) on esp_transport_poll_read.
With little inbound traffic that caps throughput at ~1 message/second per
connection, so even a light packet rate (~1.2/s) outruns the drain: the outbox
pins at its cap and ~20-30% of QoS0 packets are dropped as backpressure. The
poll timeout is a compile-time constant baked into the precompiled esp-mqtt lib,
so the async drain rate cannot be raised on the Arduino/IDF 4.4 toolchain.
Route QoS0 packet publishes through esp_mqtt_client_publish() (async=false) so
they write straight to the socket, bypassing the outbox drain entirely — QoS0 no
longer touches the outbox. QoS1 status keeps the async/outbox + retransmit path.
The esp-mqtt task releases its API lock before the poll, so a synchronous publish
from the (Core-0, prio-1) MQTT task acquires the lock and writes immediately; a
stalled socket blocks only that task (mesh RX on Core 1 and the WiFi/TCP stack
are unaffected), bounded by a new setNetworkTimeout() lowered to 2500ms so a
first stall fails fast and flips the slot to disconnected.
The outbox cap from the previous commit stays as a dormant safety net. Retools
the MQTT_DEBUG diagnostic from outbox size/drops (now always ~0) to per-slot
publish ok/err counts, the live signal for delivery health, with 1-based slot
numbering to match the status line.
QoS0 packet/raw publishes are forced into the esp-mqtt outbox (store=true,
async) so packet topics keep flowing, but the outbox has no size bound of its
own — esp-mqtt frees entries only on send-ack or ~30s expiry. On a stalled or
slow uplink (socket still "connected") QoS0 frames accumulate on internal heap
without limit, driving the heap exhaustion/fragmentation seen in the field.
Cap the outbox at the application level: PsychicMqttClient::setOutboxLimit()
records a per-client byte cap, and publish() drops a QoS0 message (returns -2)
when esp_mqtt_client_get_outbox_size() is already at/over the cap, before
enqueuing. The bridge's existing processPacketQueue retry/drop path handles the
-2 as backpressure. Caps: 16 KiB PSRAM / 8 KiB non-PSRAM (outbox lives on
internal heap, so non-PSRAM is the fragmentation-sensitive case).
Portable across IDF 4.4 and 5 via esp_mqtt_client_get_outbox_size(); esp-mqtt's
own outbox.limit config is not used (its enqueue path does not reliably enforce
it for QoS0, and the app-level guard fires before enqueue regardless).
Adds getOutboxSize()/getOutboxLimit()/getOutboxDrops() and surfaces per-slot
outbox size/cap/drops via a throttled logMemoryStatus() in the MQTT task loop
(MQTT_DEBUG-gated) to confirm the bound on-target.
esp-mqtt's default message_retransmit_timeout is 1000 ms: any unacked QoS 1
PUBLISH is resent (byte-identical, DUP=1) every second until the PUBACK
arrives or the outbox entry expires (30 s). Status messages are the only
QoS 1 publishes; on a congested or recovering uplink where broker acks take
several seconds, each 5-minute /status was delivered ~6 times, ~1 s apart,
as exact copies (same timestamp and stats). Downstream observers flagged
excessive_packet_copies and at least one broker treats it as abuse.
Expose message_retransmit_timeout via PsychicMqttClient and set it to 15 s
in optimizeMqttClientConfig: one retry still fits inside the 30 s outbox
expiry, preserving at-least-once delivery while capping duplicates at one.
/packets paths are QoS 0 and were never affected.
Improved error handling in the MQTT client to log specific reasons
for connection refusals, including detailed return codes. This change
ensures that users are informed of authentication issues and server
availability problems, enhancing the debugging experience.
Reintroduced the 'origin' field in the buildPacketMessage function to its original position within the JSON object. This adjustment ensures consistency in the message format and aligns with previous structural changes made to enhance clarity.
Update the PsychicMqttClient to ensure that QoS0 messages are enqueued with durable outbox storage. This change addresses issues with false-failure semantics in certain connected paths, improving message flow reliability. Additionally, modify platformio.ini to include SSL certificate generation and adjust build flags for reduced verbosity and enhanced functionality.
Update the PsychicMqttClient to set a _config_dirty flag whenever a configuration setter is called. This ensures that the MQTT configuration is only applied when changes are made, optimizing the connect and reconnect processes. Added logging to indicate whether the configuration was updated or unchanged.
Refactor the MQTTBridge to implement enhanced QoS handling for publish operations. Introduce retry mechanisms for QoS0 packets, allowing for transient failures to be retried with a delay. Update publish methods to return success status, improving error handling and logging. Additionally, adjust the packet queue processing to accommodate new retry logic and ensure better message delivery control.
This commit introduces a new `reconnect` method in the `PsychicMqttClient` class, allowing for re-establishing a connection to the MQTT broker without needing to disconnect first. The method checks if the client is initialized, updates the configuration if necessary, and attempts to reconnect, enhancing the overall connection management. Additionally, the MQTT slot management has been updated to support up to 6 configurable slots, improving flexibility in connection handling.
- Main broker: only allocate _mqtt_client when custom broker configured
(analyzer-only saves one PsychicMqttClient). Reconnect main broker after
forced disconnect with 30s throttle; set last_attempt on disconnect so
throttle applies and avoids reconnect storms on flaky WiFi.
- Analyzer clients: call disconnect() when WiFi transitions to disconnected
so ESP-IDF frees MQTT buffers (it does not free on WiFi drop). Reduces
fragmentation and Max drop after disconnect/reconnect cycles.
- get wifi.status: report WiFi uptime (Xd Xh Xm Xs) when WITH_MQTT_BRIDGE.
Track connect time in bridge; backfill when already connected at first check.
- get mqtt.status: show msgs on/off, broker (connected/disconnected/n/a),
analyzer US/EU (connected/disconnected/off), and queue count.
Note: PsychicMqttClient change (register event only on first client creation)
belongs in the library repo if committed separately.