Compare commits

...
1 Commits
Author SHA1 Message Date
Kaj SchittecatandClaude Fable 5 8e17b1b071 touch: beta_33 — async MQTT connect (freeze fix) + About sketch-size cache + favorites-full toast
- MqttBridge: reconnect() (blocking DNS + TCP + CONNACK, 2-10 s vs an
  unreachable broker, every 15 s) now runs on a one-shot core-0 task;
  _connecting hands the client between threads. Broker socket writes get
  the same zero-timeout writability probe as the TCP/WS companion
  servers, so a wedged broker fails a publish instead of stalling loop().
- About: ESP.getSketchSize()/getFreeSketchSpace() esp_image_verify-walk
  the whole ~3 MB image (~190 ms each); the 1 Hz refresh cost ~390 ms/s
  and flooded the stall ring (the ui:sbar entries field testers
  photographed). Computed once and cached.
- Favorites: adding past the 16-cap now toasts 'Favorites full (max 16)'
  instead of the wrong 'Removed from favorites'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 23:34:22 +02:00
4 changed files with 87 additions and 8 deletions

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
# beta_33 (TEST) — the MQTT freeze fix + the About page no longer stalls itself. On top of beta_32.
# One user-facing note per non-blank, non-# line; # lines are section comments.
# --- Fixed ---
The freezes with MQTT enabled are gone. Connecting to the broker (DNS lookup, TCP connect, login) could block the device for 2-10 seconds and retried every 15 seconds while the broker was unreachable, all on the UI thread. Connection attempts now run fully in the background, and writes to a broker that stops responding are skipped instead of waited on (the same treatment beta_32 gave phone/web connections). Tip: if your bridge keeps reconnecting, check the broker address and credentials - a healthy connection does not retry.
The About page caused a ~0.4 second hitch every second while it was open: computing the firmware image size checksums the whole ~3 MB app in flash and ran twice per refresh. It is now computed once per boot, so About (and its stall list) no longer disturbs the very measurements it shows.
Favoriting a contact when the favorites list is full (16) now says "Favorites full (max 16)" instead of the incorrect "Removed from favorites".
+42 -5
View File
@@ -10,11 +10,30 @@ MqttBridge mqtt_bridge;
#include <WiFi.h>
#include <stdio.h>
#include <string.h>
#include <lwip/sockets.h>
#include "esp_random.h"
#include "mbedtls/gcm.h"
#include "mbedtls/md.h"
#include "mbedtls/base64.h"
// Only hand bytes to lwIP when the socket can accept them right now (zero-timeout
// select). Otherwise WiFiClient::write() blocks in 1 s select() retries against a
// broker that stopped ACKing — on the loop thread that is a visible UI freeze per
// publish. A refused write fails the publish; PubSubClient then flags the
// connection down and the (async) reconnect path takes over.
size_t MqttNbClient::write(const uint8_t* buf, size_t size) {
int fd_ = fd();
if (fd_ < 0) return 0;
fd_set wset;
FD_ZERO(&wset);
FD_SET(fd_, &wset);
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 0;
if (select(fd_ + 1, NULL, &wset, NULL, &tv) <= 0 || !FD_ISSET(fd_, &wset)) return 0;
return WiFiClient::write(buf, size);
}
// Read every field from the "mqtt" NVS namespace into the members. isKey() guards
// avoid the [E] NOT_FOUND log spam that getString emits for absent keys on the
// USB-CDC companion stream (see TouchPrefsStore prefsGetStr note).
@@ -86,13 +105,27 @@ bool MqttBridge::reconnect() {
return ok;
}
// reconnect() blocks on DNS + TCP connect + CONNACK (2-10 s against an unreachable
// broker) — never run it on the loop thread, it drives LVGL. One-shot task instead;
// _connecting hands _mqtt/_wc to the task and back.
void MqttBridge::reconnectTask(void* arg) {
MqttBridge* self = (MqttBridge*)arg;
self->reconnect();
self->_connecting = false;
vTaskDelete(nullptr);
}
void MqttBridge::loop() {
if (!_enabled) return;
if (!_enabled || _connecting) return;
if (!_mqtt.connected()) {
uint32_t now = millis();
if ((uint32_t)(now - _lastReconnectMs) >= RECONNECT_INTERVAL_MS) {
_lastReconnectMs = now;
reconnect();
_connecting = true;
if (xTaskCreatePinnedToCore(reconnectTask, "mqtt_conn", 6144, this,
1, nullptr, 0) != pdPASS) {
_connecting = false; // task OOM — try again next interval
}
}
return;
}
@@ -128,7 +161,7 @@ bool MqttBridge::sealToB64(const char* plain, char* out, size_t outCap) {
}
void MqttBridge::pub(const char* subtopic, const char* json) {
if (!_enabled || !_mqtt.connected()) return;
if (!_enabled || _connecting || !_mqtt.connected()) return;
char topic[80];
snprintf(topic, sizeof(topic), "wadamesh/%s/%s", _nodeHex, subtopic);
if (_encOn) {
@@ -158,7 +191,7 @@ void MqttBridge::escapeJson(const char* src, char* dst, size_t dstLen) {
void MqttBridge::publishDM(const char* senderName, const uint8_t* senderKey32,
float snr, uint8_t hops, uint32_t ts, const char* text) {
if (!_enabled || !_pubDm || !_mqtt.connected()) return; // DMs are opt-in
if (!_enabled || !_pubDm || _connecting || !_mqtt.connected()) return; // DMs are opt-in
char keyHex[13] = {};
for (int i = 0; i < 6; ++i) snprintf(keyHex + i * 2, 3, "%02x", senderKey32[i]);
@@ -175,7 +208,7 @@ void MqttBridge::publishDM(const char* senderName, const uint8_t* senderKey32,
void MqttBridge::publishChannel(int channelIdx, const char* channelName,
float snr, uint8_t hops, uint32_t ts, const char* text) {
if (!_enabled || !_pubChannel || !_mqtt.connected()) return; // channel publish toggle
if (!_enabled || !_pubChannel || _connecting || !_mqtt.connected()) return; // channel publish toggle
char safeName[48], safeText[300];
escapeJson(channelName, safeName, sizeof(safeName));
escapeJson(text, safeText, sizeof(safeText));
@@ -204,6 +237,10 @@ void MqttBridge::saveConfig(const char* host, uint16_t port,
}
void MqttBridge::reloadConfig() {
// A connect attempt may be in flight on the one-shot task; PubSubClient is not
// thread-safe, so wait it out (bounded: DNS + TCP + CONNACK <= ~10 s, and it
// only overlaps when Save lands inside an attempt window on a dead broker).
while (_connecting) delay(10);
if (_mqtt.connected()) _mqtt.disconnect();
loadConfig();
if (!_enabled) { Serial.println("[MQTT] disabled"); return; }
+16 -1
View File
@@ -24,6 +24,17 @@ public:
#include <PubSubClient.h>
#include <WiFiClient.h>
// WiFiClient whose write() refuses to enter the framework's blocking-select path:
// bytes are only handed to lwIP when the socket can take them right now, so a
// wedged broker connection (half-open socket, peer stopped ACKing) fails the
// publish instead of stalling the loop thread ~1 s per write attempt. Same probe
// as the TCP/WS companion servers.
class MqttNbClient : public WiFiClient {
public:
size_t write(const uint8_t* buf, size_t size) override;
size_t write(uint8_t b) override { return write(&b, 1); }
};
// Publishes received mesh messages to an MQTT broker over the existing WiFi link.
//
// PRIVACY MODEL (mirrors how Meshtastic's MQTT module guards content):
@@ -73,7 +84,7 @@ public:
void reloadConfig();
private:
WiFiClient _wc;
MqttNbClient _wc;
PubSubClient _mqtt{_wc};
char _nodeHex[13] = {}; // 6-byte key → 12 hex chars + '\0'
bool _enabled = false;
@@ -87,12 +98,16 @@ private:
bool _encOn = false;
uint16_t _port = 1883;
uint32_t _lastReconnectMs = 0;
// True while the one-shot connect task owns _mqtt/_wc. The loop thread must
// not touch either until it clears (PubSubClient is not thread-safe).
volatile bool _connecting = false;
static const uint32_t RECONNECT_INTERVAL_MS = 15000;
void loadConfig(); // shared by begin() / reloadConfig()
void deriveKey(); // _key = SHA-256(_psk); sets _encOn
bool reconnect();
static void reconnectTask(void* arg); // one-shot task body wrapping reconnect()
void pub(const char* subtopic, const char* json); // seals if _encOn
bool sealToB64(const char* plain, char* out, size_t outCap);
static void escapeJson(const char* src, char* dst, size_t dstLen);
+22 -2
View File
@@ -8603,8 +8603,20 @@ static void sysInfoText(char* buf, size_t cap) {
const uint32_t sketch_size = 0; // TODO(device): surface the real AppFS app size
const uint32_t sketch_free = 0;
#else
const uint32_t sketch_size = ESP.getSketchSize();
const uint32_t sketch_free = ESP.getFreeSketchSpace();
// Each of these runs esp_image_verify — a SHA walk over the whole ~3 MB app
// image (~190 ms per call). At the 1 Hz About refresh the pair was a constant
// ~390 ms/s UI stall (the "ui:sbar" entries field testers photographed) and it
// flooded the stall ring, hiding the entries that mattered. The running image
// cannot change while running, so pay the walk once and cache.
static uint32_t s_sketch_size_c = 0, s_sketch_free_c = 0;
static bool s_sketch_known = false;
if (!s_sketch_known) {
s_sketch_size_c = ESP.getSketchSize();
s_sketch_free_c = ESP.getFreeSketchSpace();
s_sketch_known = true;
}
const uint32_t sketch_size = s_sketch_size_c;
const uint32_t sketch_free = s_sketch_free_c;
#endif
p += snprintf(buf + p, cap - p,
"Flash\n chip: %u MB\n sketch: %u KB used\n app slot free: %u KB\n\n",
@@ -12790,6 +12802,14 @@ static void actionSheetFavoriteCb(lv_event_t* e) {
#if defined(ESP32)
bool was_fav = touchPrefsIsFavorite(c.id.pub_key);
bool now_fav = touchPrefsSetFavorite(c.id.pub_key, !was_fav);
if (!was_fav && !now_fav) {
// Add refused: the favorites table is at capacity, nothing changed — saying
// "Removed from favorites" here (the plain now_fav mapping) is a lie.
char full_msg[48];
snprintf(full_msg, sizeof full_msg, TR("Favorites full (max %d)"), TOUCH_FAVORITES_MAX);
g_lv.task->showAlert(full_msg, 1500);
return;
}
g_lv.task->showAlert(now_fav ? TR("Added to favorites") : TR("Removed from favorites"), 1100);
// Force a list rebuild so the star (or its removal) shows immediately — a plain
// refresh hits the no-change cache (the contact count is unchanged by a fav toggle).