mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-06 06:14:45 -05:00
* fix(http): give a TLS session the contiguous heap it actually needs (#6960) An HTTPS connection to a no-PSRAM ESP32-S3 fails its handshake with MBEDTLS_ERR_SSL_ALLOC_FAILED and the client gets a TCP reset. Four things were in the way, and the first two are the bug. mbedtls_ssl_setup() makes two independent calloc() calls, one inbound record buffer and one outbound, each needing its own contiguous block of 16717 bytes. The admission gate compared ESP.getFreeHeap(), which is a sum over every free block and says nothing about the largest one, so on a fragmented heap it waved the connection through and let the handshake be the thing that discovered there was no room. Size the two record buffers separately. Inbound stays at the RFC maximum because browsers and our own MQTT and OTA clients receive full-size records; outbound only ever carries what this device sends, 4 kB at a time at most. That is 12288 bytes of contiguous heap back per session, which is the difference between one usable HTTPS connection and two. These are the ESP-IDF defaults - Espressif's arduino-lib-builder is what turns the asymmetric split off. It replaces CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN, which no longer applies: that symbol is `depends on !MBEDTLS_ASYMMETRIC_CONTENT_LEN`, and mbedTLS dropped it in 3.0. Then ask the allocator the question mbedTLS is about to ask it, rather than a different one. The probe holds both record buffers at once, as setup() does, through the same heap that CONFIG_MBEDTLS_*_MEM_ALLOC pins mbedTLS to - a plain malloc would be served from PSRAM that mbedTLS never touches. Not heap_caps_get_largest_free_block(), which walks every block under the allocator lock and tripped the interrupt watchdog in #11666. Closed connections are reaped before the measurement, so the library cannot free a slot behind it and accept into it unmeasured, and so the probe sees the memory a finished session just returned. The answer is advisory: the heap can still turn between the probe and setup(), which makes the failure rare rather than routine. A smaller outgoing record makes an existing bug much easier to hit. mbedtls_ssl_write() accepts at most one record per call and returns a short count for the rest, and neither the server library nor Print::print() loops on that, so any response body larger than one record was quietly cut off - /json/nodes has been truncating at 16 kB already. Loop at the bulk write sites. Looping on the returned count also covers a client that negotiates a smaller maximum fragment length, which a fixed-size chunker would not. /json/nodes also built its whole body in one std::string. A full node DB is 200 entries of ~240 bytes, and std::string grows by doubling, so it asked for a 64 kB contiguous block while still holding a 32 kB one, on a board with ~50 kB of heap, through operator new, which aborts rather than throws here. Stream it a node at a time instead. Finally, stop every node in the fleet paying for geofencing. The crossing table reserved all 256 slots at construction, 4 kB, though a node with no geofenced waypoints never tracks a crossing at all. Allocate on the first one instead, stepping the capacity explicitly so there is a size to probe for, and hand the block back once the last waypoint is gone. The probe matters because this allocation has moved off the pristine boot heap onto a live one, where a reserve() that cannot find the block would abort; a refusal now degrades into the bounded-drop path that already exists. heltec-v3 builds clean from scratch with the IDF recompiled, and comes out 20,656 bytes smaller in flash and 172 bytes smaller in static RAM. Full native suite is green at 1414 cases. * style(http,geofence): cut the added comments to the repo's one-or-two-line rule The rationale belongs in the commit message and the PR, not in block comments above every new symbol. No behaviour change; the binary is byte-identical. * fix(geofence): index the capacity probe write to clear cppcheck uninitdata * fix(http): stop the /json/nodes body at the first failed write * fix(geofence): grow crossing state with realloc so a refused allocation drops instead of aborting * refactor(http): probe through esp_mbedtls_mem_calloc and fold the free-slot scan into reaping * refactor(geofence): allocate the crossing table once on first use and free it on store changes * fix(http): probe through mbedtls_calloc so the calls keep C linkage * fix(http): name the check that refused a TLS session, and sum the right heap Both failure modes logged the same line, so a refusal on the free-heap threshold read as fragmentation. The probe returns a verdict now and the warning names the check that failed. That threshold also read ESP.getFreeHeap(), the internal heap, while the probe below it goes through mbedtls_calloc() - PSRAM wherever EXTERNAL_MEM_ALLOC sends mbedTLS, which [device-ui_base] sets. There is no free-size query on the allocator, and the handshake's remaining small blocks want a sum rather than a block to probe for, so the sum asks heap_caps for the same heap. * fix(http): probe the TLS heap only when a client is waiting for a slot The probe holds ~21 kB across two callocs and ran whenever a slot was free, which on a board with nobody connecting is every 50 ms while the web server is active and every second when it is idle - on exactly the low-heap boards this branch is for. It now runs only when a zero-timeout select() on the listen socket says a client is already in the backlog, which is the question loop() asks a few microseconds later. With nothing knocking the server drives its open connections and accepts nothing, so a connection is never accepted past an unmeasured gate; one that lands between the two select()s waits a tick. * perf(http): flush /json/nodes a couple of kB at a time One writeAll() per node is one mbedtls_ssl_write() per node, about 200 of them for a full node DB. Filling a 2 kB buffer first cuts that by roughly 9x and still bounds the allocation, which is why the whole-body string went away in the first place. * fix(geofence): keep the table-full warning for a full table A refused malloc shared the one-shot table-full warning, so a momentary heap dip spent the message that means the 256-entry cap was reached, and the real thing then never printed. They are separate now, and the allocation failure throttles rather than latching, because the next position may well find the 4 kB the table needs. * fix(http): close a waiting client the TLS heap check refuses * fix(http): hold spiLock only for filesystem calls in the browse and delete handlers * refactor(http): leave the browse and delete spiLock scoping to #11870 --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
78 lines
2.9 KiB
C++
78 lines
2.9 KiB
C++
#pragma once
|
|
|
|
#include "configuration.h"
|
|
|
|
#if !MESHTASTIC_EXCLUDE_WAYPOINT
|
|
|
|
#include "Observer.h"
|
|
#include "WaypointStore.h"
|
|
#include "mesh/MeshTypes.h"
|
|
#include "mesh/generated/meshtastic/mesh.pb.h"
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
|
|
struct GeofenceNotificationEvent {
|
|
uint32_t waypointId = 0;
|
|
char nodeName[sizeof(meshtastic_User::long_name)] = {};
|
|
bool entered = false;
|
|
char geofenceName[sizeof(meshtastic_Waypoint::name)] = {};
|
|
};
|
|
|
|
// Tracks other nodes crossing waypoint geofences and emits enter/exit notifications on the waypoint creator's device.
|
|
class GeofenceModule : public Observable<const GeofenceNotificationEvent *>
|
|
{
|
|
public:
|
|
GeofenceModule();
|
|
|
|
enum class Crossing { None, Enter, Exit };
|
|
|
|
static bool insideRadius(int32_t ptLat_i, int32_t ptLon_i, int32_t ctrLat_i, int32_t ctrLon_i, uint32_t radiusMeters);
|
|
|
|
static bool insideBox(int32_t ptLat_i, int32_t ptLon_i, const meshtastic_BoundingBox &box);
|
|
|
|
static bool insideAny(int32_t ptLat_i, int32_t ptLon_i, int32_t ctrLat_i, int32_t ctrLon_i, uint32_t radiusMeters,
|
|
bool hasBox, const meshtastic_BoundingBox &box);
|
|
|
|
static bool inside(const meshtastic_Waypoint &wp, int32_t ptLat_i, int32_t ptLon_i);
|
|
|
|
static bool hasGeofence(const meshtastic_Waypoint &wp);
|
|
|
|
// Box-only geofences do not require a waypoint center because their corners are absolute.
|
|
static bool shouldTrack(const meshtastic_Waypoint &wp, uint8_t notificationPreferences, uint32_t now);
|
|
|
|
// The first sighting establishes a baseline without notifying.
|
|
static Crossing classify(bool firstSighting, bool wasInside, bool isInside, bool notifyOnEnter, bool notifyOnExit);
|
|
|
|
void evaluatePosition(NodeNum node, const meshtastic_Position &p);
|
|
|
|
private:
|
|
struct CrossingState {
|
|
uint64_t key;
|
|
bool inside;
|
|
};
|
|
|
|
// malloc()ed, so it must be released with free(), not delete[].
|
|
struct CFreeDeleter {
|
|
void operator()(CrossingState *p) const noexcept { free(p); }
|
|
};
|
|
|
|
static uint64_t crossingKey(uint32_t waypointId, NodeNum node) { return ((uint64_t)waypointId << 32) | node; }
|
|
|
|
CrossingState *findCrossingState(uint64_t key);
|
|
bool ensureCrossingCapacity();
|
|
void notify(const meshtastic_Waypoint &wp, NodeNum node, bool entered);
|
|
int onWaypointStoreChanged(const WaypointStore *store);
|
|
|
|
// Bounded (waypointId, nodeNum) state; new pairs are skipped until an old waypoint frees space.
|
|
// Unallocated until the first crossing, so a node with no geofenced waypoints never pays for it.
|
|
std::unique_ptr<CrossingState[], CFreeDeleter> crossingInside;
|
|
size_t crossingCount = 0;
|
|
CallbackObserver<GeofenceModule, const WaypointStore *> waypointStoreObserver =
|
|
CallbackObserver<GeofenceModule, const WaypointStore *>(this, &GeofenceModule::onWaypointStoreChanged);
|
|
};
|
|
|
|
extern GeofenceModule *geofenceModule;
|
|
|
|
#endif
|