Keep MQTT and WebConfig stable under internal memory pressure

This commit is contained in:
mikecarper committed 2026-10-27 11:28:47 -07:00
1 parent 2be068cafa
commit 2fc2f409d4
22 files changed
+1494 -406

No files matched your search

+1
View File
@@ -18,6 +18,7 @@ static const size_t kDmaLargestBytes = 4096;
// SDK default or confusing this per-client task with the MQTTBridge worker.
static const size_t kClientTaskStackBytes = 6144;
static const int kClientTaskPriority = 5;
static const int kClientBufferBytes = 896;
static const size_t kColdStartAllowanceBytes = 16384;
static const size_t kColdStartLargestBytes = 8192;
// SDK stop retains buffers/configuration but releases its task. Restart only
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include "MQTTConnectionAdmission.h"
#include <stddef.h>
#include <stdint.h>
namespace MQTTPublishAdmission {
// QoS0 writes use the SDK's fixed buffer; QoS1 can resend an entire outbox
// record. Keep the existing DMA reserve and account for larger queued records.
static const size_t kLegacyLargestBytes = 8000;
static const size_t kFramingAllowanceBytes = 128;
static const uint32_t kSampleIntervalMs = 5000;
struct Heap {
bool psram = false;
size_t internal_largest = 0;
size_t dma_free = 0;
size_t dma_largest = 0;
};
inline bool canPublish(const Heap& heap, size_t payload_size = 0,
size_t topic_size = 0, uint8_t qos = 0) {
if (payload_size > SIZE_MAX - kFramingAllowanceBytes ||
topic_size > SIZE_MAX - kFramingAllowanceBytes - payload_size) return false;
size_t record = qos ? payload_size + topic_size + kFramingAllowanceBytes : 0;
if (record > MQTTConnectionAdmission::kTlsRecordAllocationBytes)
record = MQTTConnectionAdmission::kTlsRecordAllocationBytes;
if (!heap.psram) {
const size_t required = record > kLegacyLargestBytes ? record : kLegacyLargestBytes;
return heap.internal_largest >= required;
}
const size_t largest = record > MQTTConnectionAdmission::kDmaLargestBytes
? record : MQTTConnectionAdmission::kDmaLargestBytes;
const size_t free = MQTTConnectionAdmission::kDmaReserveBytes
+ largest - MQTTConnectionAdmission::kDmaLargestBytes;
return heap.dma_free >= free && heap.dma_largest >= largest;
}
struct State {
Heap heap;
uint32_t sampled_at = 0;
bool sampled = false;
bool needsSample(uint32_t now) const {
return !sampled || now - sampled_at >= kSampleIntervalMs;
}
void update(uint32_t now, const Heap& snapshot) {
heap = snapshot;
sampled_at = now;
sampled = true;
}
void invalidate() { sampled = false; }
};
} // namespace MQTTPublishAdmission
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <stdint.h>
namespace MQTTStatusPublishPolicy {
// Bridge-task owned. A failed destination keeps its own retry deadline;
// another broker accepting the same snapshot cannot postpone that retry.
struct State {
uint32_t attempted_at = 0;
uint32_t accepted_at = 0;
bool has_attempt = false;
bool has_accepted = false;
bool pending = false;
void request() { pending = true; has_attempt = false; }
bool due(uint32_t now, uint32_t interval, uint32_t retry_interval,
bool periodic_enabled) const {
if (pending) return !has_attempt || uint32_t(now - attempted_at) >= retry_interval;
return periodic_enabled && (!has_accepted || uint32_t(now - accepted_at) >= interval);
}
void attempted(uint32_t now, bool accepted) {
attempted_at = now;
has_attempt = true;
pending = !accepted;
if (accepted) { accepted_at = now; has_accepted = true; }
}
};
} // namespace MQTTStatusPublishPolicy
+113 -289
View File
@@ -287,6 +287,11 @@ static void* psram_malloc(size_t size) {
#if defined(ESP_PLATFORM) && defined(BOARD_HAS_PSRAM)
void* p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM);
if (p != nullptr) return p;
const uint32_t caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA;
if (!MQTTConnectionAdmission::canFallback(size,
heap_caps_get_free_size(caps), heap_caps_get_largest_free_block(caps))) {
return nullptr;
}
p = heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
return p;
#else
@@ -329,11 +334,16 @@ static void* psram_realloc(void* ptr, size_t new_size) {
return nullptr;
}
#if defined(ESP_PLATFORM) && defined(BOARD_HAS_PSRAM)
void* p = heap_caps_realloc(ptr, new_size, MALLOC_CAP_SPIRAM);
if (p != nullptr) return p;
// A block that fell back to internal DRAM on allocation (PSRAM exhausted) cannot
// be grown in PSRAM; retry there rather than reporting failure.
return heap_caps_realloc(ptr, new_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
if (!ptr) return psram_malloc(new_size);
const size_t previous = heap_caps_get_allocated_size(ptr);
// ArduinoJson requires shrinking to succeed. Keep the old block rather
// than moving a shrink into scarce internal RAM when PSRAM is exhausted.
if (new_size <= previous) return ptr;
void* replacement = psram_malloc(new_size);
if (!replacement) return nullptr;
memcpy(replacement, ptr, previous);
psram_free(ptr);
return replacement;
#else
return realloc(ptr, new_size);
#endif
@@ -859,7 +869,7 @@ MQTTBridge::MQTTBridge(const MQTTNodeInfo& node_info, MQTTPrefs *obs,
_node_info(node_info),
_manage_wifi(manage_wifi),
_queue_count(0),
_last_status_publish(0), _last_status_retry(0), _status_interval(300000),
_status_interval(300000),
_ntp_client(_ntp_udp, effectiveNtpPrimary(obs), 0, 60000),
_last_ntp_sync(0), _ntp_refresh_retry_at(0),
_ntp_refresh_started_at(0), _ntp_refresh_previous_epoch(0),
@@ -889,7 +899,7 @@ MQTTBridge::MQTTBridge(const MQTTNodeInfo& node_info, MQTTPrefs *obs,
#endif
_identity(identity),
_cached_has_connected_slots(false),
_last_memory_check(0), _skipped_publishes(0),
_skipped_publishes(0),
_last_no_broker_log(0), _queue_disconnected_since(0),
_last_config_warning(0),
_dispatcher(nullptr), _radio(nullptr), _board(nullptr), _ms(nullptr),
@@ -2015,19 +2025,9 @@ void MQTTBridge::mqttTaskLoop() {
}
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
// bridge task so the shared status doc/buffer/origin are only ever touched
// from Core 0 (see the onConnect handler / A2). Clear before publishing so a
// 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);
}
}
// Status deadlines belong to each slot, including reconnect requests.
// All JSON construction stays on this bridge task.
maintainStatus(static_cast<uint32_t>(now));
if (_stop_requested) continue;
// Maintain slot connections (token renewal, reconnect with backoff)
@@ -2082,63 +2082,6 @@ void MQTTBridge::mqttTaskLoop() {
// Automatic boot/reconnect/daily NTP is serviced above as one bounded
// asynchronous operation. Explicit administrator diagnostics remain separate.
// Publish status updates (handle millis() overflow correctly).
// Read the toggle live from prefs (like mqtt.packets/rx/tx below) so a
// CLI/web `set mqtt.status` change applies without a bridge restart.
if (_obs->mqtt_status_enabled) {
bool has_destinations = _cached_has_connected_slots;
// Early exit if no destinations - skip all the expensive logic below
if (!has_destinations) {
if (_last_status_retry != 0) {
_last_status_retry = 0;
}
} else {
bool should_publish = false;
// First, check if we need to respect retry interval (prevents spam when publish keeps failing)
if (_last_status_retry != 0) {
unsigned long retry_elapsed = (now >= _last_status_retry) ?
(now - _last_status_retry) :
(ULONG_MAX - _last_status_retry + now + 1);
if (retry_elapsed < STATUS_RETRY_INTERVAL) {
should_publish = false;
} else {
should_publish = true;
}
} else {
if (_last_status_publish == 0) {
should_publish = true;
} else {
unsigned long elapsed = (now >= _last_status_publish) ?
(now - _last_status_publish) :
(ULONG_MAX - _last_status_publish + now + 1);
should_publish = (elapsed >= _status_interval);
}
}
if (should_publish) {
if (_last_status_publish != 0) {
unsigned long elapsed = (now >= _last_status_publish) ?
(now - _last_status_publish) :
(ULONG_MAX - _last_status_publish + now + 1);
MQTT_DEBUG_PRINTLN("Status publish timer expired (elapsed: %lu ms, interval: %lu ms)", elapsed, _status_interval);
} else {
MQTT_DEBUG_PRINTLN("Status publish attempt (first publish or retry)");
}
_last_status_retry = now;
if (publishStatus()) {
_last_status_publish = now;
_last_status_retry = 0;
MQTT_DEBUG_PRINTLN("Status published successfully, next publish in %lu ms", _status_interval);
} else {
MQTT_DEBUG_PRINTLN("Status publish failed, will retry in %lu ms", STATUS_RETRY_INTERVAL);
}
}
}
}
// Refresh the aggregate cache periodically; owner maintenance above
// reconciles adapter state and bounds missed terminal callbacks.
static unsigned long last_slot_status_update = 0;
@@ -2393,6 +2336,17 @@ bool MQTTBridge::canStartSlotConnection(int index) const {
// enabled but not activated, so it holds no active-slot position and
// maintainSlotConnections() will retry it -- the allocation failures below are transient
// memory conditions, not permanent misconfiguration.
void MQTTBridge::setSlotClientId(int index) {
MQTTSlot& slot = _slots[index];
// esp-mqtt's default derives only from the MAC, so two slots on one broker
// otherwise evict each other's sessions. The adapter retains this pointer.
const uint64_t chip_id = ESP.getEfuseMac();
snprintf(slot.client_id, sizeof(slot.client_id), "meshcore%012llXs%u",
static_cast<unsigned long long>(chip_id & 0xFFFFFFFFFFFFULL),
static_cast<unsigned>(index + 1));
slot.client->setClientId(slot.client_id);
}
bool MQTTBridge::setupSlot(int index) {
if (_stop_requested.load(std::memory_order_acquire)) return false;
// A CLI reconfigure can arrive before the deferred startup pass. Wait for
@@ -2465,6 +2419,7 @@ bool MQTTBridge::setupSlot(int index) {
_slot_force_jwt_mint[index] = false;
}
setSlotClientId(index);
bool uses_jwt = (slot.preset && slot.preset->auth_type == MQTT_AUTH_JWT) || slot.audience[0] != '\0';
optimizeMqttClientConfig(slot.client, uses_jwt); // sets keepalive (45s PSRAM, 75s non-PSRAM)
#ifndef MQTT_FORCE_KEEPALIVE_45
@@ -2798,6 +2753,7 @@ void MQTTBridge::serviceSlotTransportHealth(uint32_t now) {
const bool pending = _slot_attempt_pending[i].load(std::memory_order_acquire);
if (!pending) health.completeAttempt();
if (slot.client) {
slot.client->serviceOutboxDiagnostics();
health.observeAck(static_cast<uint32_t>(slot.client->getPublishAck()));
if (_slot_transport_fault_pending[i].load(std::memory_order_acquire)) {
health.requestRecovery(MQTTTransportHealth::RecoveryReason::TransportError);
@@ -3226,6 +3182,30 @@ bool MQTTBridge::createSlotAuthToken(int index) {
return false;
}
bool MQTTBridge::hasPublishHeadroom(size_t payload_len, size_t topic_len, uint8_t qos) {
#ifdef ESP_PLATFORM
const uint32_t now = static_cast<uint32_t>(millis());
if (_publish_memory.needsSample(now)) {
MQTTPublishAdmission::Heap heap;
#if defined(BOARD_HAS_PSRAM)
heap.psram = ESP.getPsramSize() > 0;
#endif
if (heap.psram) {
heap.dma_free = heap_caps_get_free_size(MALLOC_CAP_DMA);
heap.dma_largest = heap_caps_get_largest_free_block(MALLOC_CAP_DMA);
} else {
heap.internal_largest = ESP.getMaxAllocHeap();
}
_publish_memory.update(now, heap);
}
if (!MQTTPublishAdmission::canPublish(_publish_memory.heap, payload_len, topic_len, qos)) {
++_skipped_publishes;
return false;
}
#endif
return true;
}
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;
@@ -3247,6 +3227,8 @@ bool MQTTBridge::publishToSlot(int index, const char* topic, const char* payload
return false;
}
if (!hasPublishHeadroom(payload_len, topic ? strlen(topic) : 0, qos)) return false;
// Publish path by QoS:
// - QoS 0 (high-rate packets/raw): SYNCHRONOUS (async=false -> esp_mqtt_client_publish),
// which writes straight to the socket. The async/outbox path drains only one queued
@@ -3268,6 +3250,7 @@ bool MQTTBridge::publishToSlot(int index, const char* topic, const char* payload
const uint32_t ack_before = static_cast<uint32_t>(slot.client->getPublishAck());
int result = slot.client->publish(topic, qos, retained, payload, (int)payload_len, async);
if (result < 0) {
_publish_memory.invalidate();
if (!async) {
slot.transport_health.requestRecovery(MQTTTransportHealth::RecoveryReason::PublishFailure);
updateSlotTransportDiag(index, static_cast<uint32_t>(millis()));
@@ -3345,102 +3328,26 @@ bool MQTTBridge::buildTopicForSlot(int index, MQTTMessageType type, char* topic_
topic_buf, buf_size);
}
void MQTTBridge::publishStatusToSlot(int index) {
if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return;
MQTTSlot& slot = _slots[index];
if (!slot.client || !slot.connected) return;
bool MQTTBridge::publishStatusToSlot(int index) {
if (index < 0 || index >= RUNTIME_MQTT_SLOTS) return false;
return publishStatus(static_cast<uint8_t>(1u << index));
}
refreshOriginFromPrefs();
// Build per-slot topic (handles IATA check for meshcore, token check for meshrank)
char status_topic[128];
if (!buildTopicForSlot(index, MSG_STATUS, status_topic, sizeof(status_topic))) {
return; // Slot is missing required topic configuration
}
// Reuse pre-allocated buffer to avoid heap alloc/free churn under memory pressure.
// _json_scratch_doc/_json_scratch_buffer/_origin are shared with publishStatus() and
// with the packet/raw paths; every one of them runs only on the bridge task (this
// function is reached solely via the _status_publish_pending consumer in
// mqttTaskLoop, never from the onConnect callback thread - see A2), so the accesses
// are serialized and need no mutex.
#if defined(BOARD_HAS_PSRAM)
char fallback_status_buffer[STATUS_JSON_BUFFER_SIZE];
char* json_buffer = (_json_scratch_buffer != nullptr) ? _json_scratch_buffer : fallback_status_buffer;
#else
char* json_buffer = _json_scratch_buffer;
#endif
char origin_id[65];
char timestamp[40];
char radio_info[64];
// Status timestamp: UTC with explicit +00:00 offset, same as packet/raw JSON
// `timestamp` (system clock is UTC - SNTP offset 0; prefs Timezone is separate).
struct timeval now_tv;
gettimeofday(&now_tv, nullptr);
MQTTMessageBuilder::formatIsoTimestampForMqtt(now_tv.tv_sec, now_tv.tv_usec, _timezone, timestamp, sizeof(timestamp));
formatRadioInfo(_node_info.freq ? *_node_info.freq : 0.0f,
_node_info.bw ? *_node_info.bw : 0.0f,
_node_info.sf ? *_node_info.sf : 0,
_node_info.cr ? *_node_info.cr : 0,
radio_info, sizeof(radio_info));
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
char client_version[64];
getClientVersion(client_version, sizeof(client_version));
// Collect stats on-demand if sources are available
int battery_mv = -1;
int uptime_secs = -1;
int errors = -1;
int noise_floor = -999;
int tx_air_secs = -1;
int rx_air_secs = -1;
int recv_errors = -1;
int packets_sent = -1;
int packets_received = -1;
if (_board) battery_mv = _board->getBattMilliVolts();
if (_ms) uptime_secs = _ms->getMillis() / 1000;
if (_dispatcher) {
errors = _dispatcher->getErrFlags();
tx_air_secs = _dispatcher->getTotalAirTime() / 1000;
rx_air_secs = _dispatcher->getReceiveAirTime() / 1000;
packets_sent = (int)(_dispatcher->getNumSentFlood() + _dispatcher->getNumSentDirect());
packets_received = (int)(_dispatcher->getNumRecvFlood() + _dispatcher->getNumRecvDirect());
}
if (_radio) {
noise_floor = (int16_t)_radio->getNoiseFloor();
recv_errors = (int)_radio->getPacketsRecvErrors();
}
// Internal heap free (for diagnosing repeater hangs from internal heap exhaustion)
int internal_heap_free = (int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
int len = MQTTMessageBuilder::buildStatusMessage(
_json_scratch_doc,
_origin, origin_id, _board_model, _firmware_version, radio_info,
client_version, "online", timestamp, json_buffer, STATUS_JSON_BUFFER_SIZE,
battery_mv, uptime_secs, errors, _queue_count, noise_floor,
tx_air_secs, rx_air_secs, recv_errors, internal_heap_free,
packets_sent, packets_received,
repeatStatus(), collectConnHealth()
);
if (len > 0) {
// Honor the preset's retain policy, matching publishStatus() -- brokers that
// set allow_retain=false (e.g. waev) reject retained publishes, so this
// on-connect status must not force retain=true. Custom slots default to
// non-retained here too, keeping both status paths consistent.
bool use_retain = slot.preset ? slot.preset->allow_retain : false;
if (!publishToSlot(index, status_topic, json_buffer, static_cast<size_t>(len), use_retain, 1)) {
MQTT_DEBUG_PRINTLN("MQTT%d status publish failed", index + 1);
}
void MQTTBridge::maintainStatus(uint32_t now) {
uint8_t due_slots = 0;
for (int i = 0; i < RUNTIME_MQTT_SLOTS; ++i) {
if (_stop_requested.load(std::memory_order_acquire)) return;
MQTTSlot& slot = _slots[i];
if (_status_publish_pending[i].exchange(false, std::memory_order_acq_rel))
slot.status_publish.request();
if (!slot.enabled || !slot.client || !slot.connected) continue;
if (!slot.status_publish.due(now, static_cast<uint32_t>(_status_interval),
static_cast<uint32_t>(STATUS_RETRY_INTERVAL), _obs->mqtt_status_enabled)) continue;
char topic[128];
if (buildTopicForSlot(i, MSG_STATUS, topic, sizeof(topic)))
due_slots |= static_cast<uint8_t>(1u << i);
}
if (due_slots) publishStatus(due_slots);
}
MQTTConnHealth MQTTBridge::collectConnHealth() const {
@@ -3886,65 +3793,7 @@ void MQTTBridge::loop() {
refreshNTP();
}
// Publish status updates (handle millis() overflow correctly).
// Read the toggle live from prefs so a CLI/web `set mqtt.status` change
// applies without a bridge restart.
if (_obs->mqtt_status_enabled) {
bool has_destinations = _cached_has_connected_slots;
if (has_destinations) {
unsigned long now = millis();
bool should_publish = false;
if (_last_status_retry != 0) {
unsigned long retry_elapsed = (now >= _last_status_retry) ?
(now - _last_status_retry) :
(ULONG_MAX - _last_status_retry + now + 1);
if (retry_elapsed >= STATUS_RETRY_INTERVAL) {
should_publish = true;
}
} else {
if (_last_status_publish == 0) {
should_publish = true;
} else {
unsigned long elapsed = (now >= _last_status_publish) ?
(now - _last_status_publish) :
(ULONG_MAX - _last_status_publish + now + 1);
should_publish = (elapsed >= _status_interval);
}
}
if (should_publish) {
if (_last_status_publish != 0) {
unsigned long elapsed = (now >= _last_status_publish) ?
(now - _last_status_publish) :
(ULONG_MAX - _last_status_publish + now + 1);
MQTT_DEBUG_PRINTLN("Status publish timer expired (elapsed: %lu ms, interval: %lu ms)", elapsed, _status_interval);
} else {
MQTT_DEBUG_PRINTLN("Status publish attempt (first publish or retry)");
}
_last_status_retry = now;
if (publishStatus()) {
_last_status_publish = now;
_last_status_retry = 0;
MQTT_DEBUG_PRINTLN("Status published successfully, next publish in %lu ms", _status_interval);
} else {
MQTT_DEBUG_PRINTLN("Status publish failed, will retry in %lu ms", STATUS_RETRY_INTERVAL);
}
}
} else {
if (_last_status_retry != 0) {
_last_status_retry = 0;
}
}
// Phase 4 (MQTT memory-defrag): the "recreate on prolonged status failure"
// path and the periodic runCriticalMemoryCheckAndRecovery() call have been
// removed. They were both symptoms of the heap churn introduced by
// delete/new cycles of the MQTT client; with persistent clients the
// allocator stays healthy and these recovery hooks aren't required.
}
maintainStatus(static_cast<uint32_t>(millis()));
#endif
}
@@ -4379,11 +4228,18 @@ bool MQTTBridge::shouldQueuePacketType(uint8_t packet_type, bool& filtered) {
return false;
}
bool MQTTBridge::publishStatus() {
if (!_cached_has_connected_slots) {
bool MQTTBridge::publishStatus(uint8_t slot_mask) {
if (!slot_mask) {
return false;
}
const uint32_t attempt_time = static_cast<uint32_t>(millis());
// Include JSON/allocation failures in the same per-slot backoff. A failed
// build must not be repeated every worker tick under memory pressure.
for (int i = 0; i < RUNTIME_MQTT_SLOTS; ++i)
if (slot_mask & static_cast<uint8_t>(1u << i))
_slots[i].status_publish.attempted(attempt_time, false);
refreshOriginFromPrefs();
// Reuse pre-allocated buffer to avoid heap alloc/free churn under memory pressure.
@@ -4454,30 +4310,24 @@ bool MQTTBridge::publishStatus() {
repeatStatus(), collectConnHealth()
);
if (len > 0) {
bool published = false;
bool any_slot_wants_status = false;
char topic[128];
for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) {
if (_slots[i].enabled && _slots[i].client && _slots[i].connected) {
if (buildTopicForSlot(i, MSG_STATUS, topic, sizeof(topic))) {
any_slot_wants_status = true;
bool use_retain = _slots[i].preset ? _slots[i].preset->allow_retain : false;
if (publishToSlot(i, topic, json_buffer, (size_t)len, use_retain, 1)) {
published = true;
}
}
}
}
// If no connected slot accepts status topics, treat as success to avoid
// infinite retry loops
if (published || !any_slot_wants_status) {
if (published) MQTT_DEBUG_PRINTLN("Status published");
return true;
if (len <= 0) return false;
bool all_accepted = true;
char topic[128];
for (int i = 0; i < RUNTIME_MQTT_SLOTS; ++i) {
if (_stop_requested.load(std::memory_order_acquire)) return false;
if (!(slot_mask & static_cast<uint8_t>(1u << i))) continue;
MQTTSlot& slot = _slots[i];
bool accepted = false;
if (slot.enabled && slot.client && slot.connected
&& buildTopicForSlot(i, MSG_STATUS, topic, sizeof(topic))) {
const bool retained = slot.preset ? slot.preset->allow_retain : false;
accepted = publishToSlot(i, topic, json_buffer, static_cast<size_t>(len), retained, 1);
}
slot.status_publish.attempted(attempt_time, accepted);
all_accepted = all_accepted && accepted;
if (!accepted) MQTT_DEBUG_PRINTLN("MQTT%d status deferred; retry in %lu ms", i + 1, STATUS_RETRY_INTERVAL);
}
return false;
return all_accepted;
}
bool MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx,
@@ -4496,40 +4346,9 @@ bool MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx,
refreshOriginFromPrefs();
// Memory pressure check: Skip publishes when there's not enough contiguous
// heap for the publish itself (JSON buffer + esp-mqtt outbox frame + WiFi TX
// path). This guard covers publish headroom, not the separate TLS session
// allocations on reconnect or AES DMA allocations. Publish payload is capped
// at PUBLISH_JSON_BUFFER_SIZE (2 KB); the threshold includes frame overhead
// and transient TCP buffers.
#ifdef ESP32
#if defined(BOARD_HAS_PSRAM)
static const size_t PUBLISH_SKIP_MAX_ALLOC_THRESHOLD = 16000;
#else
static const size_t PUBLISH_SKIP_MAX_ALLOC_THRESHOLD = 8000;
#endif
unsigned long now = millis();
// Re-sample max-alloc at most once per interval and cache the verdict.
// getMaxAllocHeap() walks the heap free-list, so it must not run per packet.
// The previous code only advanced _last_memory_check on the healthy path, so
// under sustained pressure the guard stayed open and it walked the heap on
// EVERY packet -- the opposite of throttling (A15). Caching the verdict keeps
// the "skip publishes while memory is low" protection but pays for the walk
// only once per interval; _last_memory_check is now advanced on both paths.
if (now - _last_memory_check > 5000) {
_last_memory_check = now;
size_t max_alloc = ESP.getMaxAllocHeap();
_memory_pressure = (max_alloc < PUBLISH_SKIP_MAX_ALLOC_THRESHOLD);
if (_memory_pressure) {
MQTT_DEBUG_PRINTLN("MQTT: memory pressure, skipping publishes (Max alloc: %d, threshold: %d, skipped: %d)",
(int)max_alloc, (int)PUBLISH_SKIP_MAX_ALLOC_THRESHOLD, _skipped_publishes);
}
}
if (_memory_pressure) {
_skipped_publishes++;
return false;
}
#endif
// JSON pools live in PSRAM. The shared admission keeps the existing DMA
// reserve for writes without requiring a new 16 KiB internal allocation.
if (!hasPublishHeadroom()) return false;
// Use pre-allocated buffer; stack fallback only when PSRAM heap alloc may be null.
#if defined(BOARD_HAS_PSRAM)
@@ -4634,6 +4453,8 @@ bool MQTTBridge::publishRaw(mesh::Packet* packet, bool& has_eligible_target) {
// raw): intentionally complete, and no JSON is built.
if (!has_eligible_target) return false;
if (!hasPublishHeadroom()) return false;
refreshOriginFromPrefs();
#if defined(BOARD_HAS_PSRAM)
@@ -5605,7 +5426,10 @@ void MQTTBridge::optimizeMqttClientConfig(PsychicMqttClient* client, bool needs_
// from the first start so explicit non-JWT -> JWT reconfiguration stays valid
// without destroying a client that another core may be reading for diagnostics.
(void)needs_large_buffer;
static const int MQTT_CLIENT_BUFFER_SIZE = 896;
static const int MQTT_CLIENT_BUFFER_SIZE = MQTTConnectionAdmission::kClientBufferBytes;
static_assert(MQTT_CLIENT_BUFFER_SIZE + MQTTPublishAdmission::kFramingAllowanceBytes
<= MQTTConnectionAdmission::kDmaLargestBytes,
"Publish admission must cover the SDK's QoS0 wire buffer");
client->setBufferSize(MQTT_CLIENT_BUFFER_SIZE);
+14 -11
View File
@@ -13,6 +13,8 @@
#include "helpers/MQTTConnectionPolicy.h"
#include "helpers/MQTTConnectionHealth.h"
#include "helpers/MQTTTransportHealth.h"
#include "helpers/MQTTPublishAdmission.h"
#include "helpers/MQTTStatusPublishPolicy.h"
#include "helpers/MQTTPayloadBuilder.h"
#include "helpers/MQTTPacketFilter.h"
#include "helpers/MQTTPresets.h"
@@ -108,6 +110,7 @@ private:
// Connection slot - each slot holds one MQTT connection
struct MQTTSlot {
PsychicMqttClient* client;
char client_id[32]; // Persistent: the adapter retains this pointer
const MQTTPresetDef* preset; // Points to MQTT_PRESETS[] entry, nullptr for custom/none
bool enabled; // true when preset is not "none"
bool connected; // Updated in callbacks
@@ -163,6 +166,7 @@ private:
// Only the bridge worker mutates this state; SDK callbacks raise atomic
// latches instead. Retained across reconnects to expose recovery history.
MQTTTransportHealth::State transport_health;
MQTTStatusPublishPolicy::State status_publish;
};
MQTTSlot _slots[RUNTIME_MQTT_SLOTS];
@@ -182,7 +186,6 @@ private:
bool _raw_enabled;
bool _rx_enabled;
uint8_t _tx_mode; // 0=off, 1=all TX, 2=self-advert only
unsigned long _last_status_publish;
unsigned long _status_interval;
// Packet queue for offline scenarios
@@ -278,8 +281,8 @@ private:
// task (Core 0). publishStatusToSlot() touches the shared status doc/buffer/
// origin that publishStatus() also uses, so it must run only on the bridge
// task -- the callback just raises this flag. Same idiom as
// _slot_reconfigure_pending; a single-byte volatile store/load is atomic.
volatile bool _status_publish_pending[RUNTIME_MQTT_SLOTS];
// _slot_reconfigure_pending; exchange consumes each reconnect edge safely.
std::atomic<bool> _status_publish_pending[RUNTIME_MQTT_SLOTS];
// CLI-requested forced NTP sync, marshalled onto the MQTT task (Core 0).
// All NTP I/O (_ntp_client, configTime) must run on Core 0; the CLI thread
@@ -412,21 +415,18 @@ private:
// feeds capacity(), so the object is 64 bytes and every pool comes from the allocator.
JsonDocument _json_scratch_doc{&_json_allocator};
// Memory pressure monitoring (per-publish skip; see publishPacket()).
// Owner-task admission shared by packet, raw and status publication.
// The broader fragmentation-recovery machinery was removed in Phase 4 of
// the MQTT memory-defrag work. Persistent clients reduce MQTT storage churn;
// ESP-IDF still reallocates TLS sessions when a connection closes.
unsigned long _last_memory_check;
bool _memory_pressure = false; // Cached max-alloc verdict; re-sampled at most once per interval in publishPacket() so the heap walk isn't paid per-packet under pressure
int _skipped_publishes; // Exposed via SNMP; count of publishes skipped when max_alloc is too low
MQTTPublishAdmission::State _publish_memory;
int _skipped_publishes; // Exposed via SNMP; attempts suppressed by memory admission
// Packets rejected by the per-slot filters before reaching the queue. Written
// on Core 1 (radio callbacks), read on Core 0 for `mqtt.stats`; a torn read of
// a diagnostic counter is harmless, so no atomic is warranted.
unsigned long _filtered_packets = 0;
unsigned long _bad_sig_adverts = 0; // RX adverts dropped by mqtt.advert.verify
// Status publish retry tracking
unsigned long _last_status_retry; // Track last retry attempt (separate from successful publish)
static const unsigned long STATUS_RETRY_INTERVAL = 30000; // Retry every 30 seconds if failed
// Device identity for JWT token creation
@@ -512,6 +512,7 @@ private:
void releaseSlotAuthToken(int index);// Free the token buffer (only with the client -- see MQTTSlot)
void destroySlotClients(bool force = false);
bool setupSlot(int index); // Configure and connect the slot; false = not activated
void setSlotClientId(int index);
// Single definition of "this slot holds one of the _max_active_slots positions":
// it is enabled and has been through a successful setupSlot(). Startup, the
// setup-retry path, and live reconfigure all gate on these so the cap cannot be
@@ -533,13 +534,15 @@ private:
// knows it, and passing it avoids re-scanning up to 2 KB of JSON per destination
// slot (and up to NEIGHBORS_JSON_BUFFER_SIZE per neighbor snapshot).
bool publishToSlot(int index, const char* topic, const char* payload, size_t payload_len, bool retained = false, uint8_t qos = 0);
bool hasPublishHeadroom(size_t payload_len = 0, size_t topic_len = 0, uint8_t qos = 0);
bool publishToAllSlots(const char* topic, const char* payload, size_t payload_len, bool retained = false, uint8_t qos = 0);
void publishStatusToSlot(int index);
bool publishStatusToSlot(int index);
void maintainStatus(uint32_t now);
MQTTConnHealth collectConnHealth() const;
void updateCachedConnectionStatus();
void processPacketQueue();
bool publishStatus(); // Returns true if status was successfully published
bool publishStatus(uint8_t slot_mask); // True when every selected destination accepted it
bool handleWiFiConnection(unsigned long now);
// FreeRTOS task function (runs on Core 0)
@@ -0,0 +1,85 @@
#pragma once
#include <ArduinoJson.h>
#include <stdlib.h>
#include <string.h>
#if defined(ESP_PLATFORM) && defined(BOARD_HAS_PSRAM)
#include <esp_heap_caps.h>
#include <helpers/MQTTConnectionAdmission.h>
#endif
namespace mesh { namespace webconfig {
// JSON pools and strings are CPU workspaces, never SDK/DMA buffers. Avoid
// ordinary malloc's small-allocation preference for scarce internal RAM.
class JsonAllocator : public ArduinoJson::Allocator {
public:
void* allocate(size_t size) override {
if (!size) return nullptr;
#if defined(ESP_PLATFORM) && defined(BOARD_HAS_PSRAM)
void* memory = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (memory) return memory;
const uint32_t caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA;
if (!MQTTConnectionAdmission::canFallback(size, heap_caps_get_free_size(caps),
heap_caps_get_largest_free_block(caps))) return nullptr;
return heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
#else
return malloc(size);
#endif
}
void deallocate(void* memory) override {
#if defined(ESP_PLATFORM) && defined(BOARD_HAS_PSRAM)
heap_caps_free(memory);
#else
free(memory);
#endif
}
void* reallocate(void* memory, size_t size) override {
if (!memory) return allocate(size);
if (!size) { deallocate(memory); return nullptr; }
#if defined(ESP_PLATFORM) && defined(BOARD_HAS_PSRAM)
const size_t previous = heap_caps_get_allocated_size(memory);
// ArduinoJson shrinks pools and string buffers after parsing. Keeping the
// original allocation is valid and cannot turn a shrink into a failure or
// move PSRAM data into internal RAM. It is freed with the short-lived doc.
if (size <= previous) return memory;
void* replacement = allocate(size);
if (!replacement) return nullptr; // realloc failure leaves the old data valid
memcpy(replacement, memory, previous);
deallocate(memory);
return replacement;
#else
return realloc(memory, size);
#endif
}
static JsonAllocator* instance() {
static JsonAllocator allocator;
return &allocator;
}
};
#if defined(ESP_PLATFORM) && defined(BOARD_HAS_PSRAM)
// Match ArduinoJson 7's compatibility wrappers, including their nominal
// capacity() values. Pool growth and every existing route limit stay intact.
class DynamicJsonDocument : public ArduinoJson::JsonDocument {
size_t capacity_;
public:
explicit DynamicJsonDocument(size_t capacity)
: ArduinoJson::JsonDocument(JsonAllocator::instance()), capacity_(capacity) {}
size_t capacity() const { return capacity_; }
};
template<size_t Capacity> class StaticJsonDocument : public ArduinoJson::JsonDocument {
public:
StaticJsonDocument() : ArduinoJson::JsonDocument(JsonAllocator::instance()) {}
size_t capacity() const { return Capacity; }
};
#else
using DynamicJsonDocument = ArduinoJson::DynamicJsonDocument;
template<size_t Capacity> using StaticJsonDocument = ArduinoJson::StaticJsonDocument<Capacity>;
#endif
} } // namespace mesh::webconfig
+157 -35
View File
@@ -2364,7 +2364,12 @@ void WebConfigServer::handleStatus(AsyncWebServerRequest* req) {
// duplicated in the page, which cannot know how this build was sized.
doc["max_cmds"] = MAX_BATCH;
if (doc.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
AsyncResponseStream* res = req->beginResponseStream("application/json");
if (!res) { req->send(503); return; }
serializeJson(doc, *res);
req->send(res);
}
@@ -2407,7 +2412,13 @@ void WebConfigServer::handleLogin(AsyncWebServerRequest* req) {
}
const char* body = (const char*)req->_tempObject;
DynamicJsonDocument doc(256);
if (!body || deserializeJson(doc, body) != DeserializationError::Ok) {
const auto json_error = body ? deserializeJson(doc, body)
: DeserializationError(DeserializationError::InvalidInput);
if (json_error == DeserializationError::NoMemory) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
if (json_error != DeserializationError::Ok) {
req->send(400, "application/json", "{\"error\":\"bad request\"}");
return;
}
@@ -2601,7 +2612,12 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
#endif
}
if (doc.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
AsyncResponseStream* res = req->beginResponseStream("application/json");
if (!res) { req->send(503); return; }
serializeJson(doc, *res);
req->send(res);
}
@@ -2615,7 +2631,13 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
}
const char* body = (const char*)req->_tempObject;
DynamicJsonDocument doc(6144);
if (!body || deserializeJson(doc, body) != DeserializationError::Ok) {
const auto json_error = body ? deserializeJson(doc, body)
: DeserializationError(DeserializationError::InvalidInput);
if (json_error == DeserializationError::NoMemory) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
if (json_error != DeserializationError::Ok) {
req->send(400, "application/json", "{\"error\":\"bad json\"}");
return;
}
@@ -2650,6 +2672,10 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
ack["state"] = WebConfigBatch::replayStateName(bstate);
ack["count"] = _batch_count;
ack["reqid"] = (const char*)_batch_reqid;
if (ack.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(ack, out);
req->send(202, "application/json", out);
@@ -2659,6 +2685,10 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
StaticJsonDocument<96> busy;
busy["error"] = "busy";
busy["reqid"] = (const char*)_batch_reqid;
if (busy.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(busy, out);
req->send(409, "application/json", out);
@@ -2699,6 +2729,10 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
StaticJsonDocument<128> err;
err["error"] = "bad key";
err["key"] = safe_key;
if (err.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(err, out);
req->send(400, "application/json", out);
@@ -2710,6 +2744,10 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
StaticJsonDocument<128> err;
err["error"] = "ESP-NOW channel must be 1-13";
err["key"] = key;
if (err.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(err, out);
req->send(400, "application/json", out);
@@ -2754,6 +2792,10 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
StaticJsonDocument<128> ed;
ed["error"] = "value too long";
ed["key"] = safe_key;
if (ed.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(ed, out);
req->send(400, "application/json", out);
@@ -2766,6 +2808,17 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
req->send(400, "application/json", "{\"error\":\"no changes\"}");
return;
}
StaticJsonDocument<96> ack;
ack["state"] = "pending";
ack["count"] = count;
ack["reqid"] = reqid;
if (ack.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(ack, out);
_batch_kind = BATCH_CONFIG;
_batch_count = count;
_batch_next = 0;
@@ -2787,12 +2840,6 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
"WC: config POST accepted, %d cmds, reboot=%d\n",
count, (int)reboot_after);
StaticJsonDocument<96> ack;
ack["state"] = "pending";
ack["count"] = count;
ack["reqid"] = (const char*)_batch_reqid;
String out;
serializeJson(ack, out);
req->send(202, "application/json", out);
}
@@ -2836,6 +2883,10 @@ void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
StaticJsonDocument<64> idle;
idle["state"] = "idle";
idle["reqid"] = requested_reqid;
if (idle.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(idle, out);
req->send(200, "application/json", out);
@@ -2850,6 +2901,10 @@ void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
pending["state"] = "pending";
pending["reqid"] = (const char*)_batch_reqid;
pending["reauth_required"] = _batch_reauth_required && !authenticated;
if (pending.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(pending, out);
req->send(200, "application/json", out);
@@ -2865,11 +2920,6 @@ void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
WebConfigBatch::doneReportsReboot(_batch_reboot, _batch_all_ok);
doc["all_ok"] = _batch_all_ok;
doc["reauth_required"] = _batch_reauth_required && !authenticated;
if (_batch_reauth_required && !_batch_reauth_delivered) {
_batch_reauth_delivered = true;
if (_ap_rekey_generation)
_ap_rekey_after = WebConfigBatch::scheduleAt(millis(), 3000);
}
doc["reqid"] = (const char*)_batch_reqid;
if (_setup_wifi_handoff_ip[0]) {
doc["wifi_ip"] = (const char*)_setup_wifi_handoff_ip;
@@ -2884,19 +2934,20 @@ void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
}
// State stays DONE (re-readable) until the next POST claims the slot.
const uint32_t result_now = millis();
if (WebConfigBatch::shouldArmConfirmReboot(
const bool arm_reboot = WebConfigBatch::shouldArmConfirmReboot(
toSpecState(_batch_state), _batch_reboot,
_batch_all_ok, _batch_reboot_armed)) {
_batch_all_ok, _batch_reboot_armed);
uint32_t confirmed_reboot_at = 0;
if (arm_reboot) {
// Confirmation delivered. The handoff page remains rendered in the
// browser after the AP disappears, so it only needs a one-second response
// flush before reboot. Armed once; re-reads cannot push the deadline out.
_batch_reboot_armed = true;
const bool setup_handoff = _setup_wifi_handoff_ip[0] != 0;
const uint32_t reboot_delay = setup_handoff
? WebConfigBatch::kSetupHandoffRebootConfirmMs
: WebConfigBatch::kRebootConfirmMs;
doc["reboot_in_ms"] = reboot_delay;
_reboot_at = setup_handoff
confirmed_reboot_at = setup_handoff
? WebConfigBatch::confirmSetupHandoffRebootAt(result_now)
: WebConfigBatch::confirmRebootAt(result_now);
} else if (WebConfigBatch::doneReportsReboot(
@@ -2907,7 +2958,21 @@ void WebConfigServer::handleConfigResult(AsyncWebServerRequest* req) {
result_now, _reboot_at) ? 1u : _reboot_at - result_now;
}
if (doc.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
AsyncResponseStream* res = req->beginResponseStream("application/json");
if (!res) { req->send(503); return; }
if (_batch_reauth_required && !_batch_reauth_delivered) {
_batch_reauth_delivered = true;
if (_ap_rekey_generation)
_ap_rekey_after = WebConfigBatch::scheduleAt(millis(), 3000);
}
if (arm_reboot) {
_batch_reboot_armed = true;
_reboot_at = confirmed_reboot_at;
}
serializeJson(doc, *res);
req->send(res);
}
@@ -3023,7 +3088,12 @@ void WebConfigServer::handleTerminalPost(AsyncWebServerRequest* req) {
return;
}
DynamicJsonDocument doc(2048);
if (deserializeJson(doc, static_cast<const char*>(req->_tempObject))) {
const auto json_error = deserializeJson(doc, static_cast<const char*>(req->_tempObject));
if (json_error == DeserializationError::NoMemory) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
if (json_error) {
req->send(400, "application/json", "{\"error\":\"bad json\"}");
return;
}
@@ -3117,7 +3187,12 @@ void WebConfigServer::handleTerminalOutput(AsyncWebServerRequest* req) {
doc["seq"] = _terminal->queue.sequence;
doc["done"] = !_terminal->queue.pending;
doc["closed"] = _terminal->closed;
if (doc.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
AsyncResponseStream* response = req->beginResponseStream("application/json");
if (!response) { req->send(503); return; }
response->addHeader("Cache-Control", "no-store");
serializeJson(doc, *response);
req->send(response);
@@ -3146,7 +3221,13 @@ void WebConfigServer::handleCliPost(AsyncWebServerRequest* req) {
}
const char* body = (const char*)req->_tempObject;
DynamicJsonDocument doc(6144);
if (!body || deserializeJson(doc, body) != DeserializationError::Ok) {
const auto json_error = body ? deserializeJson(doc, body)
: DeserializationError(DeserializationError::InvalidInput);
if (json_error == DeserializationError::NoMemory) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
if (json_error != DeserializationError::Ok) {
req->send(400, "application/json", "{\"error\":\"bad json\"}");
return;
}
@@ -3180,6 +3261,10 @@ void WebConfigServer::handleCliPost(AsyncWebServerRequest* req) {
ack["state"] = (bstate == WebConfigBatch::State::Done) ? "done" : "running";
ack["total"] = _batch_count;
ack["reqid"] = (const char*)_batch_reqid;
if (ack.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(ack, out);
req->send(202, "application/json", out);
@@ -3189,6 +3274,10 @@ void WebConfigServer::handleCliPost(AsyncWebServerRequest* req) {
StaticJsonDocument<96> bd;
bd["error"] = "busy";
bd["reqid"] = (const char*)_batch_reqid;
if (bd.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(bd, out);
req->send(409, "application/json", out);
@@ -3204,6 +3293,10 @@ void WebConfigServer::handleCliPost(AsyncWebServerRequest* req) {
StaticJsonDocument<96> ed;
ed["error"] = "too many commands";
ed["max"] = MAX_BATCH;
if (ed.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(ed, out);
req->send(413, "application/json", out);
@@ -3241,6 +3334,10 @@ void WebConfigServer::handleCliPost(AsyncWebServerRequest* req) {
if (why) {
StaticJsonDocument<256> ed;
ed["error"] = why;
if (ed.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(ed, out);
req->send(400, "application/json", out);
@@ -3270,6 +3367,17 @@ void WebConfigServer::handleCliPost(AsyncWebServerRequest* req) {
return;
}
StaticJsonDocument<96> ack;
ack["state"] = "running";
ack["total"] = count;
ack["reqid"] = reqid;
if (ack.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(ack, out);
_batch_kind = BATCH_CLI;
_batch_count = count;
_batch_next = 0;
@@ -3284,12 +3392,6 @@ void WebConfigServer::handleCliPost(AsyncWebServerRequest* req) {
"WC: cli POST accepted, %d cmds, reboot=%d\n",
count, (int)defer_reboot);
StaticJsonDocument<96> ack;
ack["state"] = "running";
ack["total"] = count;
ack["reqid"] = (const char*)_batch_reqid;
String out;
serializeJson(ack, out);
req->send(202, "application/json", out);
}
@@ -3343,6 +3445,10 @@ void WebConfigServer::handleCliResult(AsyncWebServerRequest* req) {
StaticJsonDocument<64> idle;
idle["state"] = "idle";
idle["reqid"] = requested_reqid;
if (idle.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
String out;
serializeJson(idle, out);
req->send(200, "application/json", out);
@@ -3379,24 +3485,30 @@ void WebConfigServer::handleCliResult(AsyncWebServerRequest* req) {
: WebConfigBatch::cliWriteSucceeded(_batch[i].reply) ? "OK"
: "Sign in again to read this result";
}
const bool arm_reboot = final_read && WebConfigBatch::shouldArmConfirmReboot(
toSpecState(_batch_state), _batch_reboot, _batch_all_ok, _batch_reboot_armed);
if (final_read) {
if (_batch_reauth_required && !_batch_reauth_delivered) {
_batch_reauth_delivered = true;
if (_ap_rekey_generation)
_ap_rekey_after = WebConfigBatch::scheduleAt(millis(), 3000);
}
doc["all_ok"] = _batch_all_ok;
const bool rebooting = WebConfigBatch::cliRebootAllowed(_batch_reboot, _batch_all_ok);
doc["reboot"] = rebooting;
// Tell the operator why a `reboot` they asked for is not happening.
if (_batch_reboot && !_batch_all_ok) doc["reboot_withheld"] = true;
if (WebConfigBatch::shouldArmConfirmReboot(toSpecState(_batch_state), _batch_reboot,
_batch_all_ok, _batch_reboot_armed)) {
_batch_reboot_armed = true;
_reboot_at = WebConfigBatch::confirmRebootAt(millis());
}
}
if (doc.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
AsyncResponseStream* res = req->beginResponseStream("application/json");
if (!res) { req->send(503); return; }
if (final_read && _batch_reauth_required && !_batch_reauth_delivered) {
_batch_reauth_delivered = true;
if (_ap_rekey_generation)
_ap_rekey_after = WebConfigBatch::scheduleAt(millis(), 3000);
}
if (arm_reboot) {
_batch_reboot_armed = true;
_reboot_at = WebConfigBatch::confirmRebootAt(millis());
}
serializeJson(doc, *res);
req->send(res);
}
@@ -3452,7 +3564,12 @@ void WebConfigServer::handleScan(AsyncWebServerRequest* req) {
net["enc"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN;
net["channel"] = WiFi.channel(i);
}
if (doc.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
AsyncResponseStream* res = req->beginResponseStream("application/json");
if (!res) { req->send(503); return; }
serializeJson(doc, *res);
req->send(res);
}
@@ -3485,7 +3602,12 @@ void WebConfigServer::handlePresets(AsyncWebServerRequest* req) {
o["needs"] = "none";
}
}
if (doc.overflowed()) {
req->send(503, "application/json", "{\"error\":\"not enough memory\"}");
return;
}
AsyncResponseStream* res = req->beginResponseStream("application/json");
if (!res) { req->send(503); return; }
serializeJson(doc, *res);
req->send(res);
#else
+4
View File
@@ -42,6 +42,7 @@
#include <helpers/WebConfigLoginThrottle.h>
#include <helpers/WiFiNetworkPowerPolicy.h>
#include <helpers/ui/DisplayBuildFlags.h>
#include "WebConfigJsonAllocator.h"
class AsyncWebServer;
class AsyncWebServerRequest;
@@ -75,6 +76,9 @@ class WebTerminalStream;
#endif
class WebConfigServer {
using DynamicJsonDocument = mesh::webconfig::DynamicJsonDocument;
template<size_t Capacity>
using StaticJsonDocument = mesh::webconfig::StaticJsonDocument<Capacity>;
public:
enum Mode : uint8_t { MODE_OFF = 0, MODE_SETUP, MODE_CONNECTING, MODE_LAN };