MQTT-Display-LaserCutter/src/mqtt_client.cpp

229 lines
8.1 KiB
C++

// =============================================================================
// 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 <ArduinoJson.h>
// Globale Instanz
MqttClient mqttClient;
// --------------------------------------------------------------------------
// Konstruktor
// --------------------------------------------------------------------------
MqttClient::MqttClient()
: _client(_wifiClient)
, _lastReconnectMs(0)
, _lastHeartbeatMs(0)
{
_clientId[0] = '\0';
}
// --------------------------------------------------------------------------
// begin() - Broker konfigurieren und erste Verbindung versuchen
// --------------------------------------------------------------------------
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.setServer(broker, port);
_client.setCallback(MqttClient::onMessage);
_client.setKeepAlive(60);
_client.setBufferSize(512);
// TLS: Bei Port 8883 WiFiClientSecure (ohne Zertifikatspruefung) verwenden
if (port == 8883) {
_secureClient.setInsecure();
_client.setClient(_secureClient);
Serial.println("[MQTT] TLS aktiv (setInsecure - kein Cert-Verify)");
} else {
_client.setClient(_wifiClient);
}
// Client-ID mit MAC-Suffix eindeutig machen (wichtig bei Public Broker)
uint8_t mac[6];
WiFi.macAddress(mac);
snprintf(_clientId, sizeof(_clientId), "%s-%02X%02X%02X",
MQTT_CLIENT_ID, mac[3], mac[4], mac[5]);
Serial.printf("[MQTT] Broker: %s:%u\n", broker, port);
Serial.printf("[MQTT] Client-ID: %s\n", _clientId);
reconnect();
}
// --------------------------------------------------------------------------
// loop() - Reconnect und Heartbeat
// --------------------------------------------------------------------------
void MqttClient::loop() {
if (!WiFi.isConnected()) {
return; // Kein WiFi - nichts tun
}
if (!_client.connected()) {
uint32_t now = millis();
if (now - _lastReconnectMs >= MQTT_RECONNECT_MS) {
_lastReconnectMs = now;
reconnect();
}
} else {
_client.loop(); // PubSubClient interne Verarbeitung
// Heartbeat
uint32_t now = millis();
if (now - _lastHeartbeatMs >= MQTT_HEARTBEAT_MS) {
_lastHeartbeatMs = now;
publishHeartbeat();
}
}
// Display MQTT-Fehler aktualisieren
display.showMqttError(!_client.connected());
}
// --------------------------------------------------------------------------
// publishSession() - beim Ende eines Laser-Bursts
// --------------------------------------------------------------------------
void MqttClient::publishSession(int lastBurstSec, float totalMinutes, int gratisSec) {
if (!_client.connected()) {
Serial.println("[MQTT] publishSession: nicht verbunden, uebersprungen");
return;
}
JsonDocument doc;
doc["session_s"] = lastBurstSec;
doc["total_min"] = serialized(String(totalMinutes, 2));
doc["gratiszeit_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);
Serial.printf("[MQTT] publishSession: %s -> %s\n", buf, ok ? "OK" : "FEHLER");
}
// --------------------------------------------------------------------------
// publishHeartbeat() - Status-Heartbeat
// --------------------------------------------------------------------------
void MqttClient::publishHeartbeat() {
JsonDocument doc;
doc["online"] = true;
doc["total_min"] = serialized(String(laserTracker.getTotalMinutes(), 2));
doc["ip"] = WiFi.localIP().toString();
doc["uptime_s"] = (uint32_t)(millis() / 1000UL);
char buf[128];
serializeJson(doc, buf, sizeof(buf));
bool ok = _client.publish(MQTT_TOPIC_STATUS, buf, /*retained=*/true);
Serial.printf("[MQTT] Heartbeat: %s -> %s\n", 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}";
Serial.printf("[MQTT] Verbinde als '%s'...\n", _clientId);
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) {
Serial.println("[MQTT] Verbunden!");
_client.subscribe(MQTT_TOPIC_RESET);
Serial.printf("[MQTT] Abonniert: %s\n", MQTT_TOPIC_RESET);
// Sofortigen Heartbeat senden
publishHeartbeat();
_lastHeartbeatMs = millis();
} else {
Serial.printf("[MQTT] Verbindung fehlgeschlagen, rc=%d\n", _client.state());
}
display.showMqttError(!ok);
return ok;
}
// --------------------------------------------------------------------------
// 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);
Serial.printf("[MQTT] Nachricht: topic=%s payload=%s\n", 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 {
Serial.printf("[MQTT] JSON-Parse-Fehler: %s\n", err.c_str());
}
}
if (doReset) {
Serial.println("[MQTT] RESET-Kommando empfangen -> laserTracker.resetTotal()");
laserTracker.resetTotal();
}
}
}
// --------------------------------------------------------------------------
// isConnected()
// --------------------------------------------------------------------------
bool MqttClient::isConnected() {
return _client.connected();
}
// --------------------------------------------------------------------------
// printToSerial()
// --------------------------------------------------------------------------
void MqttClient::printToSerial() {
const auto& cfg = settings.get();
Serial.println("=== MqttClient ===");
Serial.printf(" Broker : %s\n", cfg.mqttBroker[0] ? cfg.mqttBroker : DEFAULT_MQTT_BROKER);
Serial.printf(" Port : %u\n", cfg.mqttPort > 0 ? cfg.mqttPort : DEFAULT_MQTT_PORT);
Serial.printf(" ClientID : %s\n", _clientId[0] ? _clientId : MQTT_CLIENT_ID);
Serial.printf(" Verbunden: %s\n", _client.connected() ? "JA" : "NEIN");
Serial.printf(" rc : %d\n", _client.state());
Serial.println("==================");
}