// ============================================================================= // 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 // 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() || v.as() == 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("=================="); }