MQTT-Display-LaserCutter/src/mqtt_client.cpp
MaPaLo76 a7c6edb458 fix(mqtt): MQTT-Task auf Core 0 auslagern - TLS-Blocking behebt WDT-Crash und Display-Freeze
- WiFiClientSecure/mbedtls ausschliesslich auf Core 0 initialisiert und verwendet
  (Cross-Core-Heap-Korruption durch mbedtls vermieden)
- xTaskCreatePinnedToCore('mqtt_task', Core 0, 16 KB Stack)
- begin() startet nur Task, kein Netzwerk-Zugriff auf Core 1
- mqttClient.loop() in main.cpp ist No-Op
- publishSession() von Core 1 via volatile-Flags an Core-0-Task uebergeben
- Version: 1.1.0 -> 1.1.1
2026-02-28 18:07:27 +01:00

317 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// =============================================================================
// mqtt_client.cpp - MQTT-Verbindung, Publish und Subscribe
// Projekt: MQTT-Display LaserCutter
// =============================================================================
#include "mqtt_client.h"
#include "laser_tracker.h"
#include "display_manager.h"
#include "config.h"
#include <ArduinoJson.h>
// Globale Instanz
MqttClient mqttClient;
// --------------------------------------------------------------------------
// Konstruktor
// --------------------------------------------------------------------------
MqttClient::MqttClient()
: _client(_wifiClient)
, _lastReconnectMs(0)
, _lastHeartbeatMs(0)
, _taskHandle(nullptr)
, _pendingSession(false)
, _pendingSessionSec(0)
, _pendingGratisSec(0)
{
_clientId[0] = '\0';
}
// --------------------------------------------------------------------------
// begin() - Client-ID setzen und MQTT-Task auf Core 0 starten.
// WICHTIG: _client und _secureClient werden AUSSCHLIESSLICH in _taskLoop()
// auf Core 0 initialisiert und verwendet (mbedtls nicht Core-safe).
// --------------------------------------------------------------------------
void MqttClient::begin() {
const auto& cfg = settings.get();
const char* broker = cfg.mqttBroker[0] != '\0'
? cfg.mqttBroker
: DEFAULT_MQTT_BROKER;
uint16_t port = cfg.mqttPort > 0 ? cfg.mqttPort : DEFAULT_MQTT_PORT;
// Client-ID jetzt setzen (nur String-Op, kein Netzwerk)
uint8_t mac[6];
WiFi.macAddress(mac);
snprintf(_clientId, sizeof(_clientId), "%s-%02X%02X%02X",
MQTT_CLIENT_ID, mac[3], mac[4], mac[5]);
LOG_I("MQTT", "Broker: %s:%u", broker, port);
LOG_I("MQTT", "Client-ID: %s", _clientId);
// MQTT-Task auf Core 0 starten.
// 16 KB Stack: TLS/mbedtls benoetigt ~12 KB.
// Alle _client.*-Aufrufe laufen ausschliesslich im Task auf Core 0.
xTaskCreatePinnedToCore(_taskFn, "mqtt_task", 16384, this, 1, &_taskHandle, 0);
LOG_I("MQTT", "MQTT-Task gestartet (Core 0)");
}
// --------------------------------------------------------------------------
// loop() - No-Op: Arbeit erledigt _taskLoop() auf Core 0
// Bleibt im Interface, damit main.cpp unveraendert bleibt.
// --------------------------------------------------------------------------
void MqttClient::loop() {
// Reconnect, _client.loop(), Heartbeat und Publish
// werden vollstaendig vom MQTT-Task auf Core 0 erledigt.
}
// --------------------------------------------------------------------------
// publishSession() - beim Ende einer Session (Core 1 safe, kein Blocking)
// Daten werden via volatile Flag an den MQTT-Task auf Core 0 uebergeben.
// --------------------------------------------------------------------------
void MqttClient::publishSession(int lastSessionSec, int gratisSec) {
_pendingSessionSec = lastSessionSec;
_pendingGratisSec = gratisSec;
_pendingSession = true; // Task auf Core 0 fuehrt den Publish aus
LOG_I("MQTT", "publishSession vorgemerkt: %d s (gratis %d s)", lastSessionSec, gratisSec);
}
// --------------------------------------------------------------------------
// _doPublishSession() - eigentlicher Publish, wird aus MQTT-Task aufgerufen
// --------------------------------------------------------------------------
void MqttClient::_doPublishSession(int lastSessionSec, int gratisSec) {
if (!_client.connected()) {
LOG_E("MQTT", "_doPublishSession: nicht verbunden, uebersprungen");
return;
}
// session_minutes: Decken-Rundung (62 s = 2 min)
int sessionMinuten = (lastSessionSec + 59) / 60;
// session_start_time als ISO-8601 Lokalzeit (CET/CEST via configTzTime)
char startTimeBuf[32] = {0};
time_t startTime = laserTracker.getSessionStartTime();
if (startTime > 0) {
struct tm* tmInfo = localtime(&startTime);
strftime(startTimeBuf, sizeof(startTimeBuf), "%Y-%m-%dT%H:%M:%S", tmInfo);
}
JsonDocument doc;
doc["session_minutes"] = sessionMinuten;
doc["session_seconds"] = lastSessionSec;
doc["session_start_time"] = (startTime > 0) ? startTimeBuf : "unknown";
doc["freetime_s"] = gratisSec;
doc["ip"] = WiFi.localIP().toString();
char buf[128];
serializeJson(doc, buf, sizeof(buf));
bool ok = _client.publish(MQTT_TOPIC_SESSION, buf, /*retained=*/false);
LOG_I("MQTT", "publishSession: %s -> %s", buf, ok ? "OK" : "FEHLER");
}
// --------------------------------------------------------------------------
// publishHeartbeat() - Status-Heartbeat
// --------------------------------------------------------------------------
void MqttClient::publishHeartbeat() {
JsonDocument doc;
doc["online"] = true;
doc["session_sum"] = laserTracker.getAllSessionsSumMinutes();
doc["machine_running_time_min"] = serialized(String(laserTracker.getTotalMinutes(), 2));
doc["ip"] = WiFi.localIP().toString();
doc["uptime_s"] = (uint32_t)(millis() / 1000UL);
doc["firmware_version"] = FIRMWARE_VERSION " (" __DATE__ ")";
char buf[220];
serializeJson(doc, buf, sizeof(buf));
bool ok = _client.publish(MQTT_TOPIC_STATUS, buf, /*retained=*/true);
LOG_I("MQTT", "Heartbeat: %s -> %s", buf, ok ? "OK" : "FEHLER");
}
// --------------------------------------------------------------------------
// reconnect() - Verbindungsaufbau, non-blocking
// --------------------------------------------------------------------------
bool MqttClient::reconnect() {
if (_client.connected()) return true;
const auto& cfg = settings.get();
const char* user = cfg.mqttUser[0] != '\0' ? cfg.mqttUser : nullptr;
const char* pass = cfg.mqttPassword[0] != '\0' ? cfg.mqttPassword : nullptr;
// Offline-LWT (Last Will and Testament)
const char* lwtPayload = "{\"online\":false}";
LOG_I("MQTT", "Verbinde als '%s'...", _clientId);
// TLS-Handshake kann 2-15 s dauern. Laeuft auf Core 0 (MQTT-Task),
// blockiert Core 1 (loop/LaserTracker/Display) nicht mehr.
bool ok;
if (user && pass) {
ok = _client.connect(_clientId, user, pass,
MQTT_TOPIC_STATUS, 0, true, lwtPayload);
} else {
ok = _client.connect(_clientId,
nullptr, nullptr,
MQTT_TOPIC_STATUS, 0, true, lwtPayload);
}
if (ok) {
LOG_I("MQTT", "Verbunden!");
_client.subscribe(MQTT_TOPIC_RESET);
LOG_I("MQTT", "Abonniert: %s", MQTT_TOPIC_RESET);
// Sofortigen Heartbeat senden
publishHeartbeat();
_lastHeartbeatMs = millis();
} else {
LOG_E("MQTT", "Verbindung fehlgeschlagen, rc=%d", _client.state());
}
return ok;
}
// --------------------------------------------------------------------------
// _taskFn() / _taskLoop() - FreeRTOS-Task auf Core 0
// Erledigt: Reconnect, PubSubClient.loop(), Heartbeat, pending Session-Publish
// --------------------------------------------------------------------------
void MqttClient::_taskFn(void* param) {
static_cast<MqttClient*>(param)->_taskLoop();
}
void MqttClient::_taskLoop() {
// ------------------------------------------------------------------
// Einmalige Initialisierung auf Core 0.
// WiFiClientSecure (mbedtls) MUSS auf demselben Core laufen, auf dem
// es konfiguriert und verwendet wird sonst Heap-Korruption!
// ------------------------------------------------------------------
const auto& cfg = settings.get();
const char* broker = cfg.mqttBroker[0] != '\0' ? cfg.mqttBroker : DEFAULT_MQTT_BROKER;
uint16_t port = cfg.mqttPort > 0 ? cfg.mqttPort : DEFAULT_MQTT_PORT;
if (port == 8883) {
_secureClient.setInsecure();
_client.setClient(_secureClient);
LOG_I("MQTT", "TLS aktiv (setInsecure, Core 0)");
} else {
_client.setClient(_wifiClient);
}
_client.setServer(broker, port);
_client.setCallback(MqttClient::onMessage);
_client.setKeepAlive(60);
_client.setBufferSize(512);
for (;;) {
// Kein WiFi: warten
if (!WiFi.isConnected()) {
vTaskDelay(pdMS_TO_TICKS(500));
continue;
}
// Nicht verbunden: Reconnect-Intervall abwarten, dann versuchen
if (!_client.connected()) {
uint32_t now = millis();
if (now - _lastReconnectMs >= MQTT_RECONNECT_MS) {
_lastReconnectMs = now;
reconnect(); // blockiert hier (TLS); Core 1 laeuft unbehelligt
}
vTaskDelay(pdMS_TO_TICKS(200));
continue;
}
// PubSubClient-interne Verarbeitung (eingehende Nachrichten)
_client.loop();
// Pending Session-Publish (von Core 1 via volatile Flag gesetzt)
if (_pendingSession) {
_pendingSession = false;
_doPublishSession(_pendingSessionSec, _pendingGratisSec);
}
// Heartbeat
uint32_t now = millis();
if (now - _lastHeartbeatMs >= MQTT_HEARTBEAT_MS) {
_lastHeartbeatMs = now;
publishHeartbeat();
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}
// --------------------------------------------------------------------------
// onMessage() - Callback fuer eingehende MQTT-Nachrichten
// --------------------------------------------------------------------------
void MqttClient::onMessage(const char* topic, byte* payload, unsigned int length) {
// Payload als String kopieren
char msg[64] = {};
size_t copyLen = length < sizeof(msg) - 1 ? length : sizeof(msg) - 1;
memcpy(msg, payload, copyLen);
LOG_I("MQTT", "Nachricht: topic=%s payload=%s", topic, msg);
// Reset-Kommando
if (strcmp(topic, MQTT_TOPIC_RESET) == 0) {
bool doReset = false;
// Plain "1" (rueckwaertskompatibel)
if (strcmp(msg, "1") == 0) {
doReset = true;
}
// JSON: {"reset":true} oder {"reset":1}
else if (msg[0] == '{') {
JsonDocument doc;
DeserializationError err = deserializeJson(doc, msg);
if (!err) {
JsonVariant v = doc["reset"];
if (!v.isNull()) {
doReset = v.as<bool>() || v.as<int>() == 1;
}
} else {
LOG_E("MQTT", "JSON-Parse-Fehler: %s", err.c_str());
}
}
if (doReset) {
LOG_I("MQTT", "RESET-Kommando empfangen -> laserTracker.resetTotal()");
laserTracker.resetTotal();
}
// reset_session: nur RAM-Session-Summe auf 0, NVS bleibt
bool doResetSession = false;
if (msg[0] == '{') {
JsonDocument doc2;
DeserializationError err = deserializeJson(doc2, msg);
if (!err) {
JsonVariant v = doc2["reset_session"];
if (!v.isNull()) {
doResetSession = v.as<bool>() || v.as<int>() == 1;
}
}
}
if (doResetSession) {
LOG_I("MQTT", "RESET_SESSION-Kommando empfangen -> laserTracker.resetSessionSum()");
laserTracker.resetSessionSum();
}
}
}
// --------------------------------------------------------------------------
// isConnected()
// --------------------------------------------------------------------------
bool MqttClient::isConnected() {
return _client.connected();
}
// --------------------------------------------------------------------------
// printToSerial()
// --------------------------------------------------------------------------
void MqttClient::printToSerial() {
const auto& cfg = settings.get();
LOG_I("MQTT", "=== MqttClient ===");
LOG_I("MQTT", " Broker : %s", cfg.mqttBroker[0] ? cfg.mqttBroker : DEFAULT_MQTT_BROKER);
LOG_I("MQTT", " Port : %u", cfg.mqttPort > 0 ? cfg.mqttPort : DEFAULT_MQTT_PORT);
LOG_I("MQTT", " ClientID : %s", _clientId[0] ? _clientId : MQTT_CLIENT_ID);
LOG_I("MQTT", " Verbunden: %s", _client.connected() ? "JA" : "NEIN");
LOG_I("MQTT", " rc : %d", _client.state());
LOG_I("MQTT", "==================");
}