229 lines
8.1 KiB
C++
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("==================");
|
|
} |