[P1] A failed setup no longer strands the slot. setupSlot() returns bool and the startup loops count only successful activations, so a slot that fails on a client allocation neither consumes an active-slot position (starving a later healthy broker on capped hardware) nor sits dead forever: maintainSlotConnections() previously skipped clientless slots and the reconnect ladder is gated on initial_connect_done, so nothing retried it. It now retries an enabled but unactivated slot on a 60 s timer, one per cycle, gated on the same _slots_setup_done ordering so the NTP-deferred setup sequence is preserved. [P2] JWT setup no longer proceeds without a usable token. Both the preset and custom-audience paths returned after ignoring createSlotAuthToken()'s result, then called connect() and latched initial_connect_done -- so the token-allocation failure introduced by the previous commit produced an unauthenticated attempt exactly when memory was exhausted. They now return false and let the retry path handle it. [P2] ensureSlotAuthToken() no longer clears an existing token. It cleared unconditionally, so every renewal wiped the current token before JWTHelper ran; a renewal that then failed left an empty password where the inline buffer used to preserve working credentials (JWTHelper writes only on success). Only freshly allocated buffers are initialised now. [P2] Raw publications reuse the shared document. buildRawJSON() reached MQTTPayloadBuilder::buildRawMessage(), which constructed its own default JsonDocument and therefore malloc'd and freed an internal-heap variant pool per message -- on the highest-rate topic. The document is threaded through both builders and the bridge passes _json_scratch_doc. [P3] The writeTo() guard validates the source fields, not just the destination. A corrupt payload_len of MAX_PACKET_PAYLOAD + 1 still leaves getRawLength() inside MAX_TRANS_UNIT, so writeTo() read past packet->payload. Sizing and validation moved to a pure MQTTWireScratch header with host tests covering the accept/reject edges, matching the MQTTPacketFilter/MQTTConnectionPolicy pattern. Two findings fell out: MAX_PATH_SIZE one-byte hops is not encodable (the hop count is 6 bits, so 64 & 63 == 0; 32 two-byte hops is the widest real path), and a zero-payload packet serializes but does not survive readFrom() -- pinned as a test because it constrains any future wire-only queue. [P3] Corrected the pool-size comment: these targets are 32-bit, so ARDUINOJSON_SLOT_ID_SIZE is 2 and a pool block is 128 slots / 1024 bytes, not 4096. The 4096 figure came from a pre-existing comment near NEIGHBORS_DOC_POOL_BUDGET, which is left alone -- its byte measurements are empirical and still stand, only the block-size attribution is wrong. Activation is now centralized in activatedSlotCount()/canActivateSlot(), used by both startup loops, the retry path, and applySlotPreset(). That closes the pre-existing divergence where a live preset change called setupSlot() without consulting _max_active_slots, letting a non-PSRAM board reach three concurrent TLS sessions against a cap of two. BEHAVIOUR CHANGE: a reconfigure that would exceed the cap now logs and leaves the slot inactive instead of connecting. Reconfiguring an already-active slot still works, because teardownSlot() releases its position first. 272/272 native tests pass (5 new); both observer envs and an nRF52 repeater build clean. Flash 1593249 B non-PSRAM, 1555625 B PSRAM.
40 lines
1.8 KiB
C++
40 lines
1.8 KiB
C++
#pragma once
|
|
|
|
#include <Packet.h>
|
|
#include <stddef.h>
|
|
|
|
// Sizing and validation for the scratch buffers that hold a serialized packet.
|
|
// Pure so the boundary conditions can be tested on the host: the firmware-side
|
|
// callers are MQTTMessageBuilder::packetToHex() and MQTTBridge::publishPacket().
|
|
namespace MQTTWireScratch {
|
|
|
|
// A serialized packet is header(1) + transport codes(0|4) + path_len(1) +
|
|
// path(<= MAX_PATH_SIZE) + payload(<= MAX_PACKET_PAYLOAD). Packet::writeTo()
|
|
// returns uint8_t, so MAX_TRANS_UNIT is the hard ceiling.
|
|
static const size_t kWireBytes = MAX_TRANS_UNIT;
|
|
// Two uppercase hex chars per byte, plus the NUL.
|
|
static const size_t kWireHexChars = 2 * MAX_TRANS_UNIT + 1;
|
|
|
|
static_assert(1 + 4 + 1 + MAX_PATH_SIZE + MAX_PACKET_PAYLOAD <= MAX_TRANS_UNIT,
|
|
"serialized packet no longer fits MAX_TRANS_UNIT — resize the wire scratch buffers");
|
|
|
|
// True when Packet::writeTo() can safely serialize `packet` into `dest_size` bytes.
|
|
//
|
|
// writeTo() trusts the packet's own length fields and cannot report an overrun (its
|
|
// return type is uint8_t), so the source fields must be checked as well as the
|
|
// destination:
|
|
// - payload_len drives an unchecked memcpy out of a MAX_PACKET_PAYLOAD array, and a
|
|
// corrupt value can still leave getRawLength() inside MAX_TRANS_UNIT.
|
|
// - path_len is written into a single wire byte, so anything above 255 is silently
|
|
// truncated and would disagree with getPathByteLen().
|
|
// The path bytes themselves need no check here: writePath() already refuses a
|
|
// getPathByteLen() above MAX_PATH_SIZE.
|
|
inline bool canSerialize(const mesh::Packet& packet, size_t dest_size) {
|
|
if (packet.payload_len > MAX_PACKET_PAYLOAD) return false;
|
|
if (packet.path_len > 0xFF) return false;
|
|
const int raw_len = packet.getRawLength();
|
|
return raw_len > 0 && (size_t)raw_len <= dest_size;
|
|
}
|
|
|
|
} // namespace MQTTWireScratch
|