Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Hardware
  4. Waage RENPHO ES-26M per MQTT & ESP32 anbinden

NEWS

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    11
    1
    371

  • Neuer ioBroker-Blog online: Monatsrückblick März/April 2026
    BluefoxB
    Bluefox
    8
    1
    1.9k

  • Verwendung von KI bitte immer deutlich kennzeichnen
    HomoranH
    Homoran
    11
    1
    803

Waage RENPHO ES-26M per MQTT & ESP32 anbinden

Geplant Angeheftet Gesperrt Verschoben Hardware
1 Beiträge 1 Kommentatoren 24 Aufrufe 1 Beobachtet
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • capitaenzC Offline
    capitaenzC Offline
    capitaenz
    schrieb zuletzt editiert von
    #1

    Moin zusammen,

    ich habe mit ChatGPT zusammen erfolgreich die Waage RENPHO ES-26M per MQTT angebunden.
    Notwendig ist ein ESP32 S3 Zero, bestimmt funktionieren auch andere, aber habe ich nicht getestet.
    Per BT-Scannner muss die MAC-Adresse der Waage herausgefunden werden.
    Dann die MAC der Waage, die Wlan Daten und den MQTT-Server eintragen.

    Einzige mir bekannte Limitierung ist, zwischen dem Wiegevorgängen müssen ca. 10 Sekunden liegen.

    Alles ohne Garantie!

    Hier der Sketch:

    Viel Erfolg!
    Leif

    /***************************************************************
     * ESP32 S3 Zero BLE -> MQTT Bridge für RENPHO / QN-Scale
     * -------------------------------------------------------------
     * Name:        RENPHO ES-26M / QN-Scale BLE MQTT Bridge
     * Version:     0.4.34
     * Stand:       2026-05-23
     ***************************************************************/
    
    #include <Arduino.h>
    #include <WiFi.h>
    #include <PubSubClient.h>
    #include <NimBLEDevice.h>
    #include <math.h>
    #include <string.h>
    
    
    /***************************************************************
     * 1) KONFIGURATION
     ***************************************************************/
    
    const bool DEBUG = true;
    const bool DEBUG_VERBOSE_SCAN = false;
    const bool DEBUG_VERBOSE_PACKETS = false;
    const bool DEBUG_LOG_LIVE_WEIGHT_PACKETS = false;
    
    const bool DEBUG_SESSION_TRACE = true;
    const bool DEBUG_UNKNOWN_PACKETS = true;
    
    // WLAN
    const char* WIFI_SSID     = "SSID";
    const char* WIFI_PASSWORD = "Passwort";
    
    // MQTT
    const char* MQTT_HOST      = "IP-Adresse";
    const uint16_t MQTT_PORT   = 1883;
    const char* MQTT_USER      = "";
    const char* MQTT_PASSWORD  = "";
    const char* MQTT_CLIENT_ID = "esp32-renpho-es26m";
    
    // MQTT Topics
    const char* MQTT_TOPIC_STATUS           = "renpho/es26m/status";
    const char* MQTT_TOPIC_WEIGHT_KG        = "renpho/es26m/weight_kg";
    const char* MQTT_TOPIC_FINAL            = "renpho/es26m/final";
    const char* MQTT_TOPIC_RESISTANCE_1     = "renpho/es26m/resistance_1";
    const char* MQTT_TOPIC_RESISTANCE_2     = "renpho/es26m/resistance_2";
    const char* MQTT_TOPIC_LAST_MEASUREMENT = "renpho/es26m/last_measurement";
    const char* MQTT_TOPIC_RAW_FINAL        = "renpho/es26m/raw_final";
    const char* MQTT_TOPIC_RSSI             = "renpho/es26m/rssi";
    const char* MQTT_TOPIC_ERROR            = "renpho/es26m/error";
    const char* MQTT_TOPIC_SESSION_DEBUG    = "renpho/es26m/session_debug";
    const char* MQTT_TOPIC_REARM_STATE      = "renpho/es26m/rearm_state";
    
    const bool MQTT_RETAIN_VALUES = true;
    const bool MQTT_RETAIN_STATUS = false;
    
    // Bekannte Waage
    const char* SCALE_ADDRESS = "xx:xx:xx:xx:xx:xx";
    
    // Scan
    const uint32_t FIND_SCAN_TOTAL_WINDOW_MS = 120000;
    const uint32_t FIND_SCAN_CHUNK_SEC       = 2;
    
    // BLE Ablauf
    const uint32_t RECONNECT_PAUSE_MS        = 10;
    const uint32_t CONNECTED_LOOP_DELAY_MS   = 50;
    
    const uint32_t AFTER_CONNECT_DELAY_MS    = 700;
    const uint32_t AFTER_SERVICE_DELAY_MS    = 300;
    const uint32_t AFTER_FFE1_SUBSCRIBE_MS   = 120;
    const uint32_t AFTER_FFE1_STABILIZE_MS   = 1500;
    const uint32_t AFTER_FFE3_WRITE_MS       = 80;
    const uint32_t AFTER_FFE4_WRITE_MS       = 150;
    
    const uint32_t WAIT_FOR_STATUS_12_MS     = 6000;
    const uint32_t WAIT_FOR_ACK_14_MS        = 5000;
    const uint32_t WAIT_FOR_FIRST_10_MS      = 30000;
    
    // Session-Verhalten
    const uint32_t SESSION_MAX_WITHOUT_WEIGHT_MS      = 60000;
    const uint32_t SESSION_IDLE_AFTER_FINAL_MS        = 12000;
    const uint32_t SESSION_MAX_AFTER_FIRST_FINAL_MS   = 45000;
    
    // Nach erfolgreicher Messung
    const bool     SEND_CLEANUP_AFTER_FINAL           = false;
    const uint32_t POST_SUCCESS_NOT_SEEN_REQUIRED_MS  = 8000;
    const uint32_t POST_SUCCESS_REARM_MAX_WAIT_MS     = 60000;
    const uint32_t POST_SUCCESS_REARM_SCAN_CHUNK_SEC  = 1;
    
    // Nach Idle ohne Messung
    const uint32_t IDLE_NO_WEIGHT_PAUSE_MS            = 1000;
    
    // Schreiboptionen
    const bool FFE3_WRITE_WITH_RESPONSE    = false;
    const bool FFE4_WRITE_WITH_RESPONSE    = false;
    const bool CLEANUP_WRITE_WITH_RESPONSE = false;
    
    // BLE Client Cleanup
    const bool DELETE_CLIENT_AFTER_FAILURE = true;
    const bool DELETE_CLIENT_AFTER_SUCCESS = false;
    
    // Notify-Puffer
    const uint8_t NOTIFY_BUFFER_SIZE = 16;
    const uint8_t NOTIFY_MAX_LENGTH  = 32;
    
    // Live-Logging
    const uint32_t LIVE_WEIGHT_LOG_INTERVAL_MS = 5000;
    const float LIVE_WEIGHT_LOG_DELTA_KG = 0.20f;
    
    // UUIDs
    static NimBLEUUID UUID_SERVICE_FFE0("0000ffe0-0000-1000-8000-00805f9b34fb");
    static NimBLEUUID UUID_CHAR_FFE1  ("0000ffe1-0000-1000-8000-00805f9b34fb");
    static NimBLEUUID UUID_CHAR_FFE2  ("0000ffe2-0000-1000-8000-00805f9b34fb");
    static NimBLEUUID UUID_CHAR_FFE3  ("0000ffe3-0000-1000-8000-00805f9b34fb");
    static NimBLEUUID UUID_CHAR_FFE4  ("0000ffe4-0000-1000-8000-00805f9b34fb");
    
    // QN-Scale Zeitbasis
    const uint32_t SCALE_UNIX_TIMESTAMP_OFFSET = 946702800UL;
    
    
    /***************************************************************
     * 2) GLOBALE VARIABLEN
     ***************************************************************/
    
    WiFiClient wifiClient;
    PubSubClient mqttClient(wifiClient);
    
    NimBLEScan* pBLEScan = nullptr;
    NimBLEClient* pClient = nullptr;
    
    NimBLERemoteCharacteristic* chrFFE1 = nullptr;
    NimBLERemoteCharacteristic* chrFFE2 = nullptr;
    NimBLERemoteCharacteristic* chrFFE3 = nullptr;
    NimBLERemoteCharacteristic* chrFFE4 = nullptr;
    
    bool connected = false;
    
    bool hasStatus12 = false;
    bool hasAck14 = false;
    bool hasFirst10 = false;
    bool hasAnyFinal = false;
    
    float weightScaleFactor = 100.0f;
    int lastScaleRssi = 0;
    
    uint32_t lastStatusPublishMs = 0;
    uint32_t lastErrorPublishMs = 0;
    String lastPublishedStatus = "";
    String lastPublishedError = "";
    
    uint32_t lastLiveWeightLogMs = 0;
    float lastLiveWeightLoggedKg = -999.0f;
    
    String lastFinalRawInSession = "";
    
    struct NotifyPacket {
      bool used;
      bool isNotify;
      char uuid[40];
      uint8_t length;
      uint8_t data[NOTIFY_MAX_LENGTH];
    };
    
    NotifyPacket notifyBuffer[NOTIFY_BUFFER_SIZE];
    
    volatile uint8_t notifyWriteIndex = 0;
    volatile uint8_t notifyReadIndex = 0;
    volatile uint8_t notifyCount = 0;
    volatile uint32_t notifyOverflowCount = 0;
    
    // Session Trace
    uint32_t sessionCounter = 0;
    uint32_t sessionStartMs = 0;
    uint32_t sessionFoundMs = 0;
    uint32_t sessionConnectedMs = 0;
    uint32_t sessionStatus12Ms = 0;
    uint32_t sessionAck14Ms = 0;
    uint32_t sessionFirst10Ms = 0;
    uint32_t sessionFirstFinalMs = 0;
    uint8_t sessionAddressType = 0;
    int sessionRssi = 0;
    uint16_t sessionUnknownPackets = 0;
    uint16_t sessionPackets10 = 0;
    uint16_t sessionFinalPublishes = 0;
    String sessionLastRaw = "";
    
    
    /***************************************************************
     * 3) VORDEKLARATIONEN
     ***************************************************************/
    
    void processNotifyQueue();
    void processNotifyPacket(const NotifyPacket& packet);
    
    
    /***************************************************************
     * 4) LOGGING
     ***************************************************************/
    
    String msPrefix() {
      char buf[24];
      snprintf(buf, sizeof(buf), "[%010lu ms] ", (unsigned long)millis());
      return String(buf);
    }
    
    void logInfo(const String& msg) {
      Serial.println(msPrefix() + "[INFO] " + msg);
    }
    
    void logDebug(const String& msg) {
      if (DEBUG) {
        Serial.println(msPrefix() + "[DEBUG] " + msg);
      }
    }
    
    void logWarn(const String& msg) {
      Serial.println(msPrefix() + "[WARN] " + msg);
    }
    
    void logError(const String& msg) {
      Serial.println(msPrefix() + "[ERROR] " + msg);
    }
    
    
    /***************************************************************
     * 5) MQTT / WLAN
     ***************************************************************/
    
    void connectWiFi() {
      if (WiFi.status() == WL_CONNECTED) {
        return;
      }
    
      logInfo("Verbinde WLAN ...");
    
      WiFi.mode(WIFI_STA);
      WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    
      uint32_t startMs = millis();
    
      while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    
        if (millis() - startMs > 30000) {
          Serial.println();
          logError("WLAN-Verbindung fehlgeschlagen. Neustart.");
          delay(1000);
          ESP.restart();
        }
      }
    
      Serial.println();
      logInfo("WLAN verbunden. IP: " + WiFi.localIP().toString());
    }
    
    bool mqttPublishRaw(const char* topic, const String& payload, bool retained) {
      if (!mqttClient.connected()) {
        return false;
      }
    
      bool ok = mqttClient.publish(topic, payload.c_str(), retained);
    
      if (DEBUG) {
        logDebug(String("MQTT publish ") + topic + " = " + payload + (ok ? " OK" : " FEHLER"));
      }
    
      return ok;
    }
    
    bool mqttPublishStatus(const String& status) {
      uint32_t now = millis();
    
      if (status == lastPublishedStatus && now - lastStatusPublishMs < 10000) {
        return true;
      }
    
      lastPublishedStatus = status;
      lastStatusPublishMs = now;
    
      return mqttPublishRaw(MQTT_TOPIC_STATUS, status, MQTT_RETAIN_STATUS);
    }
    
    bool mqttPublishError(const String& errorText) {
      uint32_t now = millis();
    
      if (errorText == lastPublishedError && now - lastErrorPublishMs < 10000) {
        return true;
      }
    
      lastPublishedError = errorText;
      lastErrorPublishMs = now;
    
      return mqttPublishRaw(MQTT_TOPIC_ERROR, errorText, MQTT_RETAIN_VALUES);
    }
    
    void connectMQTT() {
      if (mqttClient.connected()) {
        return;
      }
    
      mqttClient.setServer(MQTT_HOST, MQTT_PORT);
      mqttClient.setBufferSize(1024);
    
      logInfo("Verbinde MQTT ...");
    
      bool ok = false;
    
      if (strlen(MQTT_USER) > 0) {
        ok = mqttClient.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASSWORD);
      } else {
        ok = mqttClient.connect(MQTT_CLIENT_ID);
      }
    
      if (ok) {
        logInfo("MQTT verbunden.");
        mqttPublishError("");
        mqttPublishStatus("ready");
      } else {
        logWarn("MQTT-Verbindung fehlgeschlagen. State: " + String(mqttClient.state()));
      }
    }
    
    void ensureNetwork() {
      connectWiFi();
      connectMQTT();
      mqttClient.loop();
    }
    
    
    /***************************************************************
     * 6) HILFSFUNKTIONEN
     ***************************************************************/
    
    bool isBleConnected() {
      return (pClient != nullptr && pClient->isConnected());
    }
    
    String bytesToHex(const uint8_t* data, size_t length) {
      String out = "";
    
      for (size_t i = 0; i < length; i++) {
        if (data[i] < 0x10) {
          out += "0";
        }
    
        out += String(data[i], HEX);
    
        if (i < length - 1) {
          out += " ";
        }
      }
    
      out.toUpperCase();
      return out;
    }
    
    uint8_t sumChecksum(const uint8_t* data, size_t start, size_t endExclusive) {
      uint16_t sum = 0;
    
      for (size_t i = start; i < endExclusive; i++) {
        sum += data[i];
      }
    
      return (uint8_t)(sum & 0xFF);
    }
    
    void writeUInt32LE(uint8_t* target, uint32_t value) {
      target[0] = (uint8_t)(value & 0xFF);
      target[1] = (uint8_t)((value >> 8) & 0xFF);
      target[2] = (uint8_t)((value >> 16) & 0xFF);
      target[3] = (uint8_t)((value >> 24) & 0xFF);
    }
    
    float decodeWeightKg(uint8_t highByte, uint8_t lowByte, float scaleFactor) {
      uint16_t raw = ((uint16_t)highByte << 8) | lowByte;
    
      if (scaleFactor <= 0.0f) {
        scaleFactor = 100.0f;
      }
    
      return (float)raw / scaleFactor;
    }
    
    uint16_t decodeUInt16BE(uint8_t highByte, uint8_t lowByte) {
      return ((uint16_t)highByte << 8) | lowByte;
    }
    
    bool isKnownScaleAddress(const NimBLEAdvertisedDevice* device) {
      if (device == nullptr) {
        return false;
      }
    
      String address = device->getAddress().toString().c_str();
      address.toLowerCase();
    
      String targetAddress = SCALE_ADDRESS;
      targetAddress.toLowerCase();
    
      return address == targetAddress;
    }
    
    void serviceBleFor(uint32_t durationMs) {
      uint32_t startMs = millis();
    
      while (millis() - startMs < durationMs) {
        ensureNetwork();
    
        if (isBleConnected()) {
          processNotifyQueue();
        }
    
        delay(10);
      }
    }
    
    void resetRuntimeFlagsForNewSession() {
      hasStatus12 = false;
      hasAck14 = false;
      hasFirst10 = false;
      hasAnyFinal = false;
    
      weightScaleFactor = 100.0f;
    
      lastLiveWeightLogMs = 0;
      lastLiveWeightLoggedKg = -999.0f;
    
      lastFinalRawInSession = "";
    }
    
    
    /***************************************************************
     * 7) ZEITFUNKTIONEN FÜR FFE4
     ***************************************************************/
    
    int monthNameToNumber(const char* monthName) {
      if (strncmp(monthName, "Jan", 3) == 0) return 1;
      if (strncmp(monthName, "Feb", 3) == 0) return 2;
      if (strncmp(monthName, "Mar", 3) == 0) return 3;
      if (strncmp(monthName, "Apr", 3) == 0) return 4;
      if (strncmp(monthName, "May", 3) == 0) return 5;
      if (strncmp(monthName, "Jun", 3) == 0) return 6;
      if (strncmp(monthName, "Jul", 3) == 0) return 7;
      if (strncmp(monthName, "Aug", 3) == 0) return 8;
      if (strncmp(monthName, "Sep", 3) == 0) return 9;
      if (strncmp(monthName, "Oct", 3) == 0) return 10;
      if (strncmp(monthName, "Nov", 3) == 0) return 11;
      if (strncmp(monthName, "Dec", 3) == 0) return 12;
      return 1;
    }
    
    int32_t daysFromCivil(int32_t year, uint32_t month, uint32_t day) {
      year -= month <= 2;
    
      const int32_t era = (year >= 0 ? year : year - 399) / 400;
      const uint32_t yoe = (uint32_t)(year - era * 400);
      const uint32_t doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1;
      const uint32_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
    
      return era * 146097 + (int32_t)doe - 719468;
    }
    
    uint32_t getCompileUnixTime() {
      char monthText[4] = {0};
      int day = 1;
      int year = 2000;
      int hour = 0;
      int minute = 0;
      int second = 0;
    
      sscanf(__DATE__, "%3s %d %d", monthText, &day, &year);
      sscanf(__TIME__, "%d:%d:%d", &hour, &minute, &second);
    
      int month = monthNameToNumber(monthText);
      int32_t days = daysFromCivil(year, month, day);
    
      return (uint32_t)days * 86400UL +
             (uint32_t)hour * 3600UL +
             (uint32_t)minute * 60UL +
             (uint32_t)second;
    }
    
    uint32_t getScaleTimeFromCompileTime() {
      uint32_t unixTime = getCompileUnixTime();
    
      if (unixTime <= SCALE_UNIX_TIMESTAMP_OFFSET) {
        return 0;
      }
    
      return unixTime - SCALE_UNIX_TIMESTAMP_OFFSET;
    }
    
    
    /***************************************************************
     * 8) NOTIFY-PUFFER
     ***************************************************************/
    
    void resetNotifyBuffer() {
      noInterrupts();
    
      for (uint8_t i = 0; i < NOTIFY_BUFFER_SIZE; i++) {
        notifyBuffer[i].used = false;
        notifyBuffer[i].isNotify = false;
        notifyBuffer[i].uuid[0] = 0;
        notifyBuffer[i].length = 0;
    
        for (uint8_t j = 0; j < NOTIFY_MAX_LENGTH; j++) {
          notifyBuffer[i].data[j] = 0;
        }
      }
    
      notifyWriteIndex = 0;
      notifyReadIndex = 0;
      notifyCount = 0;
      notifyOverflowCount = 0;
    
      interrupts();
    }
    
    void pushNotifyPacket(
      NimBLERemoteCharacteristic* pRemoteCharacteristic,
      uint8_t* pData,
      size_t length,
      bool isNotify
    ) {
      if (pRemoteCharacteristic == nullptr || pData == nullptr) {
        return;
      }
    
      if (length > NOTIFY_MAX_LENGTH) {
        length = NOTIFY_MAX_LENGTH;
      }
    
      noInterrupts();
    
      if (notifyCount >= NOTIFY_BUFFER_SIZE) {
        notifyOverflowCount++;
        interrupts();
        return;
      }
    
      uint8_t index = notifyWriteIndex;
    
      notifyBuffer[index].used = true;
      notifyBuffer[index].isNotify = isNotify;
      notifyBuffer[index].length = (uint8_t)length;
    
      String uuidString = pRemoteCharacteristic->getUUID().toString().c_str();
      strncpy(notifyBuffer[index].uuid, uuidString.c_str(), sizeof(notifyBuffer[index].uuid) - 1);
      notifyBuffer[index].uuid[sizeof(notifyBuffer[index].uuid) - 1] = 0;
    
      for (size_t i = 0; i < length; i++) {
        notifyBuffer[index].data[i] = pData[i];
      }
    
      notifyWriteIndex = (notifyWriteIndex + 1) % NOTIFY_BUFFER_SIZE;
      notifyCount++;
    
      interrupts();
    }
    
    bool popNotifyPacket(NotifyPacket& packet) {
      noInterrupts();
    
      if (notifyCount == 0) {
        interrupts();
        return false;
      }
    
      uint8_t index = notifyReadIndex;
    
      packet = notifyBuffer[index];
      notifyBuffer[index].used = false;
    
      notifyReadIndex = (notifyReadIndex + 1) % NOTIFY_BUFFER_SIZE;
      notifyCount--;
    
      interrupts();
      return true;
    }
    
    void notifyCallback(
      NimBLERemoteCharacteristic* pRemoteCharacteristic,
      uint8_t* pData,
      size_t length,
      bool isNotify
    ) {
      pushNotifyPacket(pRemoteCharacteristic, pData, length, isNotify);
    }
    
    
    /***************************************************************
     * 9) SESSION DEBUG
     ***************************************************************/
    
    void resetSessionTrace() {
      sessionCounter++;
      sessionStartMs = millis();
      sessionFoundMs = 0;
      sessionConnectedMs = 0;
      sessionStatus12Ms = 0;
      sessionAck14Ms = 0;
      sessionFirst10Ms = 0;
      sessionFirstFinalMs = 0;
      sessionAddressType = 0;
      sessionRssi = 0;
      sessionUnknownPackets = 0;
      sessionPackets10 = 0;
      sessionFinalPublishes = 0;
      sessionLastRaw = "";
    }
    
    uint32_t sinceSession(uint32_t valueMs) {
      if (valueMs == 0 || sessionStartMs == 0) {
        return 0;
      }
    
      return valueMs - sessionStartMs;
    }
    
    String buildSessionJson(const String& resultText) {
      String json = "{";
      json += "\"version\":\"0.4.34\"";
      json += ",\"session\":" + String(sessionCounter);
      json += ",\"result\":\"" + resultText + "\"";
      json += ",\"uptime_ms\":" + String(millis());
      json += ",\"duration_ms\":" + String(sessionStartMs > 0 ? millis() - sessionStartMs : 0);
      json += ",\"rssi\":" + String(sessionRssi);
      json += ",\"addr_type\":" + String(sessionAddressType);
      json += ",\"found_ms\":" + String(sinceSession(sessionFoundMs));
      json += ",\"connected_ms\":" + String(sinceSession(sessionConnectedMs));
      json += ",\"status12_ms\":" + String(sinceSession(sessionStatus12Ms));
      json += ",\"ack14_ms\":" + String(sinceSession(sessionAck14Ms));
      json += ",\"first10_ms\":" + String(sinceSession(sessionFirst10Ms));
      json += ",\"first_final_ms\":" + String(sinceSession(sessionFirstFinalMs));
      json += ",\"packets10\":" + String(sessionPackets10);
      json += ",\"final_publishes\":" + String(sessionFinalPublishes);
      json += ",\"unknown_packets\":" + String(sessionUnknownPackets);
      json += ",\"last_raw\":\"" + sessionLastRaw + "\"";
      json += "}";
    
      return json;
    }
    
    void logSessionSummary(const String& resultText) {
      if (DEBUG && DEBUG_SESSION_TRACE) {
        String line = "[SESSION #" + String(sessionCounter) + "] " +
                      resultText +
                      " | RSSI=" + String(sessionRssi) +
                      " dBm" +
                      " | AddrType=" + String(sessionAddressType) +
                      " | found=" + String(sinceSession(sessionFoundMs)) + "ms" +
                      " | connected=" + String(sinceSession(sessionConnectedMs)) + "ms" +
                      " | 0x12=" + String(sinceSession(sessionStatus12Ms)) + "ms" +
                      " | 0x14=" + String(sinceSession(sessionAck14Ms)) + "ms" +
                      " | 0x10=" + String(sinceSession(sessionFirst10Ms)) + "ms" +
                      " | firstFinal=" + String(sinceSession(sessionFirstFinalMs)) + "ms" +
                      " | packets10=" + String(sessionPackets10) +
                      " | finalPublishes=" + String(sessionFinalPublishes) +
                      " | unknown=" + String(sessionUnknownPackets);
    
        logInfo(line);
      }
    
      mqttPublishRaw(MQTT_TOPIC_SESSION_DEBUG, buildSessionJson(resultText), MQTT_RETAIN_VALUES);
    }
    
    
    /***************************************************************
     * 10) BLE AUFRÄUMEN
     ***************************************************************/
    
    void resetBleReferences(const String& reason) {
      logDebug("BLE-Referenzen zurücksetzen: " + reason);
    
      chrFFE1 = nullptr;
      chrFFE2 = nullptr;
      chrFFE3 = nullptr;
      chrFFE4 = nullptr;
    
      connected = false;
    }
    
    void cleanupBleClient(const String& reason, bool deleteClient) {
      logDebug("BLE-Cleanup: " + reason);
    
      if (pClient != nullptr) {
        if (pClient->isConnected()) {
          pClient->disconnect();
          delay(200);
        }
    
        if (deleteClient) {
          NimBLEDevice::deleteClient(pClient);
          pClient = nullptr;
        }
      }
    
      resetBleReferences(reason);
    }
    
    bool sendScaleCleanupCommand() {
      if (!isBleConnected()) {
        return false;
      }
    
      if (chrFFE3 == nullptr || (!chrFFE3->canWrite() && !chrFFE3->canWriteNoResponse())) {
        return false;
      }
    
      uint8_t payload[5] = {
        0x1F, 0x05, 0x15, 0x10, 0x49
      };
    
      logInfo("Sende Cleanup auf FFE3: " + bytesToHex(payload, sizeof(payload)));
    
      bool ok = chrFFE3->writeValue(payload, sizeof(payload), CLEANUP_WRITE_WITH_RESPONSE);
    
      delay(150);
      return ok;
    }
    
    
    /***************************************************************
     * 11) BLE SCHREIBFUNKTIONEN
     ***************************************************************/
    
    bool writeFFE3KgInit() {
      if (!isBleConnected()) {
        return false;
      }
    
      if (chrFFE3 == nullptr || (!chrFFE3->canWrite() && !chrFFE3->canWriteNoResponse())) {
        return false;
      }
    
      uint8_t payload[9] = {
        0x13, 0x09, 0x15, 0x01, 0x10, 0x00, 0x00, 0x00, 0x00
      };
    
      payload[8] = sumChecksum(payload, 0, 8);
    
      logInfo("Schreibe kg-Initialisierung auf FFE3: " + bytesToHex(payload, sizeof(payload)));
    
      return chrFFE3->writeValue(payload, sizeof(payload), FFE3_WRITE_WITH_RESPONSE);
    }
    
    bool writeFFE4TimeHandshake() {
      if (!isBleConnected()) {
        return false;
      }
    
      if (chrFFE4 == nullptr || (!chrFFE4->canWrite() && !chrFFE4->canWriteNoResponse())) {
        return false;
      }
    
      uint32_t scaleTime = getScaleTimeFromCompileTime();
    
      if (scaleTime == 0) {
        return false;
      }
    
      uint8_t dateLE[4];
    
      writeUInt32LE(dateLE, scaleTime);
    
      uint8_t payload[5] = {
        0x02, dateLE[0], dateLE[1], dateLE[2], dateLE[3]
      };
    
      logInfo("Schreibe Zeit-Handshake auf FFE4: " + bytesToHex(payload, sizeof(payload)));
    
      return chrFFE4->writeValue(payload, sizeof(payload), FFE4_WRITE_WITH_RESPONSE);
    }
    
    
    /***************************************************************
     * 12) MQTT MESSWERT
     ***************************************************************/
    
    void publishFinalMeasurement(float weightKg, uint16_t resistance1, uint16_t resistance2, const String& rawHex) {
      mqttPublishRaw(MQTT_TOPIC_WEIGHT_KG, String(weightKg, 2), MQTT_RETAIN_VALUES);
      mqttPublishRaw(MQTT_TOPIC_FINAL, "true", MQTT_RETAIN_VALUES);
      mqttPublishRaw(MQTT_TOPIC_RESISTANCE_1, String(resistance1), MQTT_RETAIN_VALUES);
      mqttPublishRaw(MQTT_TOPIC_RESISTANCE_2, String(resistance2), MQTT_RETAIN_VALUES);
      mqttPublishRaw(MQTT_TOPIC_RAW_FINAL, rawHex, MQTT_RETAIN_VALUES);
      mqttPublishRaw(MQTT_TOPIC_RSSI, String(lastScaleRssi), MQTT_RETAIN_VALUES);
      mqttPublishRaw(MQTT_TOPIC_LAST_MEASUREMENT, String(millis()), MQTT_RETAIN_VALUES);
    
      mqttPublishError("");
      mqttPublishStatus("final_measurement_published");
    
      sessionFinalPublishes++;
    
      logInfo("Finale Messung per MQTT veröffentlicht.");
    }
    
    
    /***************************************************************
     * 13) NOTIFY AUSWERTUNG
     ***************************************************************/
    
    bool shouldLogLiveWeight(float weightKg, bool finalValue) {
      if (!DEBUG) {
        return false;
      }
    
      if (finalValue) {
        return true;
      }
    
      if (!DEBUG_LOG_LIVE_WEIGHT_PACKETS) {
        return false;
      }
    
      uint32_t now = millis();
    
      if (lastLiveWeightLoggedKg < -100.0f) {
        lastLiveWeightLoggedKg = weightKg;
        lastLiveWeightLogMs = now;
        return true;
      }
    
      if (fabs(weightKg - lastLiveWeightLoggedKg) >= LIVE_WEIGHT_LOG_DELTA_KG) {
        lastLiveWeightLoggedKg = weightKg;
        lastLiveWeightLogMs = now;
        return true;
      }
    
      if (now - lastLiveWeightLogMs >= LIVE_WEIGHT_LOG_INTERVAL_MS) {
        lastLiveWeightLoggedKg = weightKg;
        lastLiveWeightLogMs = now;
        return true;
      }
    
      return false;
    }
    
    void processPacketType12(const NotifyPacket& packet) {
      hasStatus12 = true;
    
      if (sessionStatus12Ms == 0) {
        sessionStatus12Ms = millis();
      }
    
      if (packet.length >= 15) {
        uint8_t possibleFactorByte = packet.data[10];
    
        if (possibleFactorByte == 0x01) {
          weightScaleFactor = 100.0f;
        } else {
          weightScaleFactor = 10.0f;
        }
      } else {
        weightScaleFactor = 100.0f;
      }
    
      if (DEBUG && DEBUG_VERBOSE_PACKETS) {
        logDebug("0x12 Statuspaket empfangen. Faktor: " + String((int)weightScaleFactor));
      }
    }
    
    void processPacketType14(const NotifyPacket& packet) {
      (void)packet;
    
      hasAck14 = true;
    
      if (sessionAck14Ms == 0) {
        sessionAck14Ms = millis();
      }
    
      if (DEBUG && DEBUG_VERBOSE_PACKETS) {
        logDebug("0x14 Ack empfangen.");
      }
    }
    
    void processPacketType10(const NotifyPacket& packet) {
      sessionPackets10++;
      hasFirst10 = true;
    
      if (sessionFirst10Ms == 0) {
        sessionFirst10Ms = millis();
      }
    
      bool finalValue = false;
    
      if (packet.length >= 6) {
        finalValue = (packet.data[5] == 0x01);
      }
    
      float weightKg = 0.0f;
      uint16_t resistance1 = 0;
      uint16_t resistance2 = 0;
    
      if (packet.length >= 5) {
        weightKg = decodeWeightKg(packet.data[3], packet.data[4], weightScaleFactor);
      }
    
      if (packet.length >= 10) {
        resistance1 = decodeUInt16BE(packet.data[6], packet.data[7]);
        resistance2 = decodeUInt16BE(packet.data[8], packet.data[9]);
      }
    
      String rawHex = bytesToHex(packet.data, packet.length);
      sessionLastRaw = rawHex;
    
      if (shouldLogLiveWeight(weightKg, finalValue)) {
        Serial.println();
        Serial.println("--------------------------------------------------");
        Serial.println(msPrefix() + "[BLE] 0x10 Gewichtspaket");
        Serial.println("HEX:      " + rawHex);
        Serial.println("Gewicht:  " + String(weightKg, 2) + " kg");
        Serial.println("Final:    " + String(finalValue ? "JA" : "nein"));
        Serial.println("R1:       " + String(resistance1));
        Serial.println("R2:       " + String(resistance2));
        Serial.println("--------------------------------------------------");
      }
    
      if (!finalValue) {
        return;
      }
    
      if (sessionFirstFinalMs == 0) {
        sessionFirstFinalMs = millis();
      }
    
      hasAnyFinal = true;
    
      if (rawHex == lastFinalRawInSession) {
        logDebug("Gleiches Finalpaket innerhalb derselben BLE-Session ignoriert.");
        return;
      }
    
      lastFinalRawInSession = rawHex;
    
      publishFinalMeasurement(weightKg, resistance1, resistance2, rawHex);
    }
    
    void processNotifyPacket(const NotifyPacket& packet) {
      if (DEBUG && DEBUG_VERBOSE_PACKETS) {
        Serial.println();
        Serial.println("--------------------------------------------------");
        Serial.println(msPrefix() + String(packet.isNotify ? "[BLE] Notify empfangen" : "[BLE] Indication empfangen"));
        Serial.println("Characteristic: " + String(packet.uuid));
        Serial.println("Länge:          " + String(packet.length));
        Serial.println("HEX:            " + bytesToHex(packet.data, packet.length));
        Serial.println("--------------------------------------------------");
      }
    
      if (packet.length >= 1 && packet.data[0] == 0x12) {
        processPacketType12(packet);
      } else if (packet.length >= 1 && packet.data[0] == 0x14) {
        processPacketType14(packet);
      } else if (packet.length >= 1 && packet.data[0] == 0x10) {
        processPacketType10(packet);
      } else {
        sessionUnknownPackets++;
    
        if (DEBUG && DEBUG_UNKNOWN_PACKETS) {
          String typeText = "??";
    
          if (packet.length >= 1) {
            typeText = String(packet.data[0], HEX);
            typeText.toUpperCase();
          }
    
          logDebug("[SESSION #" + String(sessionCounter) + "] Unbekanntes Paket: Typ=0x" +
                   typeText +
                   ", Länge=" + String(packet.length) +
                   ", HEX=" + bytesToHex(packet.data, packet.length));
        }
      }
    }
    
    void processNotifyQueue() {
      NotifyPacket packet;
    
      while (popNotifyPacket(packet)) {
        processNotifyPacket(packet);
      }
    
      if (notifyOverflowCount > 0) {
        uint32_t lost = notifyOverflowCount;
        notifyOverflowCount = 0;
    
        logWarn("Notify-Pufferüberlauf. Verlorene Pakete: " + String(lost));
        mqttPublishError("Notify-Pufferueberlauf: " + String(lost));
      }
    }
    
    
    /***************************************************************
     * 14) WARTESCHLEIFEN
     ***************************************************************/
    
    bool waitForFlagOrDisconnect(bool* flag, uint32_t timeoutMs, const String& timeoutText, const String& disconnectText, bool publishAsError) {
      uint32_t startMs = millis();
    
      while (millis() - startMs < timeoutMs) {
        ensureNetwork();
        processNotifyQueue();
    
        if (*flag) {
          return true;
        }
    
        if (!isBleConnected()) {
          if (publishAsError) {
            mqttPublishError(disconnectText);
          }
    
          return false;
        }
    
        delay(50);
      }
    
      if (publishAsError) {
        mqttPublishError(timeoutText);
      }
    
      return false;
    }
    
    bool waitForStatus12(uint32_t timeoutMs) {
      return waitForFlagOrDisconnect(
        &hasStatus12,
        timeoutMs,
        "Timeout beim Warten auf 0x12",
        "BLE getrennt beim Warten auf 0x12",
        true
      );
    }
    
    bool waitForAck14(uint32_t timeoutMs) {
      return waitForFlagOrDisconnect(
        &hasAck14,
        timeoutMs,
        "Timeout beim Warten auf 0x14",
        "BLE getrennt beim Warten auf 0x14",
        true
      );
    }
    
    bool waitForFirst10(uint32_t timeoutMs) {
      return waitForFlagOrDisconnect(
        &hasFirst10,
        timeoutMs,
        "Timeout beim Warten auf 0x10",
        "BLE getrennt beim Warten auf 0x10",
        false
      );
    }
    
    
    /***************************************************************
     * 15) BLE CALLBACKS
     ***************************************************************/
    
    class ClientCallbacks : public NimBLEClientCallbacks {
      void onConnect(NimBLEClient* client) override {
        (void)client;
        connected = true;
      }
    
      void onDisconnect(NimBLEClient* client, int reason) override {
        (void)client;
        connected = false;
    
        logWarn("BLE getrennt. Grund: " + String(reason));
      }
    };
    
    ClientCallbacks g_clientCallbacks;
    
    
    /***************************************************************
     * 16) WAAGE SUCHEN
     ***************************************************************/
    
    bool findScaleAddress(String& foundAddress, uint8_t& foundAddressType) {
      foundAddress = "";
      foundAddressType = BLE_ADDR_RANDOM;
      lastScaleRssi = 0;
    
      uint32_t scanStartMs = millis();
    
      logInfo("Suche bekannte Waage per BLE-Scan: " + String(SCALE_ADDRESS));
      mqttPublishStatus("scanning");
    
      while (millis() - scanStartMs < FIND_SCAN_TOTAL_WINDOW_MS) {
        ensureNetwork();
    
        NimBLEScanResults results = pBLEScan->getResults(FIND_SCAN_CHUNK_SEC, false);
        uint16_t count = results.getCount();
    
        if (DEBUG && DEBUG_VERBOSE_SCAN) {
          logDebug("Scanrunde: Geräte=" + String(count));
        }
    
        for (uint16_t i = 0; i < count; i++) {
          const NimBLEAdvertisedDevice* device = results.getDevice(i);
    
          if (device == nullptr) {
            continue;
          }
    
          if (!isKnownScaleAddress(device)) {
            continue;
          }
    
          foundAddress = device->getAddress().toString().c_str();
          foundAddressType = device->getAddressType();
          lastScaleRssi = device->getRSSI();
    
          sessionFoundMs = millis();
          sessionRssi = lastScaleRssi;
          sessionAddressType = foundAddressType;
    
          logInfo("Bekannte Waage erkannt. Verbinde sofort: " + foundAddress + " RSSI " + String(lastScaleRssi) + " dBm");
    
          pBLEScan->clearResults();
          return true;
        }
    
        pBLEScan->clearResults();
        delay(1);
      }
    
      logWarn("Bekannte Waage nicht gefunden.");
      return false;
    }
    
    bool isScaleVisibleOnce(uint32_t chunkSec) {
      NimBLEScanResults results = pBLEScan->getResults(chunkSec, false);
      uint16_t count = results.getCount();
    
      bool seen = false;
    
      for (uint16_t i = 0; i < count; i++) {
        const NimBLEAdvertisedDevice* device = results.getDevice(i);
    
        if (isKnownScaleAddress(device)) {
          seen = true;
          break;
        }
      }
    
      pBLEScan->clearResults();
      return seen;
    }
    
    void waitAfterSuccessfulMeasurement() {
      logInfo("Passive Re-Arm-Phase: Warte, bis die Waage nicht mehr sichtbar ist.");
      mqttPublishRaw(MQTT_TOPIC_REARM_STATE, "wait_for_not_seen", MQTT_RETAIN_STATUS);
      mqttPublishStatus("post_success_wait_for_not_seen");
    
      uint32_t waitStartMs = millis();
      uint32_t notSeenStartMs = 0;
    
      while (millis() - waitStartMs < POST_SUCCESS_REARM_MAX_WAIT_MS) {
        ensureNetwork();
    
        bool seen = isScaleVisibleOnce(POST_SUCCESS_REARM_SCAN_CHUNK_SEC);
    
        if (!seen) {
          if (notSeenStartMs == 0) {
            notSeenStartMs = millis();
            logDebug("Passive Re-Arm-Phase: Waage nicht sichtbar, Zeitfenster startet.");
          }
    
          if (millis() - notSeenStartMs >= POST_SUCCESS_NOT_SEEN_REQUIRED_MS) {
            logInfo("Passive Re-Arm-Phase abgeschlossen. Waage war " +
                    String(millis() - notSeenStartMs) +
                    " ms zusammenhängend nicht sichtbar.");
            mqttPublishRaw(MQTT_TOPIC_REARM_STATE, "ready", MQTT_RETAIN_STATUS);
            mqttPublishStatus("ready_for_next_measurement");
            return;
          }
        } else {
          if (notSeenStartMs != 0) {
            logDebug("Passive Re-Arm-Phase: Waage wieder sichtbar, Zeitfenster zurückgesetzt.");
          }
    
          notSeenStartMs = 0;
        }
    
        delay(20);
      }
    
      logWarn("Passive Re-Arm-Phase Timeout. Gehe trotzdem wieder in Scan-Bereitschaft.");
      mqttPublishRaw(MQTT_TOPIC_REARM_STATE, "ready_timeout", MQTT_RETAIN_STATUS);
      mqttPublishStatus("ready_for_next_measurement_timeout");
    }
    
    
    /***************************************************************
     * 17) QN-SCALE INITIALISIERUNG
     ***************************************************************/
    
    bool subscribeNotify(NimBLERemoteCharacteristic* chr) {
      if (!isBleConnected()) {
        return false;
      }
    
      if (chr == nullptr) {
        return false;
      }
    
      if (!chr->canNotify()) {
        return false;
      }
    
      return chr->subscribe(true, notifyCallback);
    }
    
    bool tryConnectWithAddressType(const String& foundAddress, uint8_t addrType) {
      cleanupBleClient("vor Connect-Versuch", DELETE_CLIENT_AFTER_FAILURE);
    
      pClient = NimBLEDevice::createClient();
    
      if (pClient == nullptr) {
        return false;
      }
    
      pClient->setClientCallbacks(&g_clientCallbacks, false);
    
      NimBLEAddress address(std::string(foundAddress.c_str()), addrType);
    
      if (!pClient->connect(address)) {
        cleanupBleClient("Connect fehlgeschlagen", DELETE_CLIENT_AFTER_FAILURE);
        return false;
      }
    
      connected = true;
      sessionConnectedMs = millis();
    
      return true;
    }
    
    bool connectScale() {
      resetSessionTrace();
      resetRuntimeFlagsForNewSession();
      resetNotifyBuffer();
      resetBleReferences("vor neuem Verbindungsversuch");
    
      String foundAddress;
      uint8_t foundAddressType;
    
      if (!findScaleAddress(foundAddress, foundAddressType)) {
        mqttPublishStatus("scale_not_found");
        logSessionSummary("IDLE: Waage im Scan nicht gefunden");
        return false;
      }
    
      mqttPublishStatus("connecting");
    
      bool connectedOk = false;
    
      if (tryConnectWithAddressType(foundAddress, foundAddressType)) {
        connectedOk = true;
      } else if (foundAddressType != BLE_ADDR_RANDOM && tryConnectWithAddressType(foundAddress, BLE_ADDR_RANDOM)) {
        connectedOk = true;
      } else if (foundAddressType != BLE_ADDR_PUBLIC && tryConnectWithAddressType(foundAddress, BLE_ADDR_PUBLIC)) {
        connectedOk = true;
      }
    
      if (!connectedOk) {
        mqttPublishError("BLE Verbindung fehlgeschlagen");
        mqttPublishStatus("ble_failed");
        logSessionSummary("FAIL: BLE Verbindung fehlgeschlagen");
        return false;
      }
    
      mqttPublishStatus("ble_connected");
    
      serviceBleFor(AFTER_CONNECT_DELAY_MS);
    
      if (!isBleConnected()) {
        mqttPublishStatus("idle_no_weight");
        cleanupBleClient("direkt nach Connect getrennt", DELETE_CLIENT_AFTER_FAILURE);
        logSessionSummary("IDLE: direkt nach Connect getrennt");
        return false;
      }
    
      NimBLERemoteService* serviceFFE0 = pClient->getService(UUID_SERVICE_FFE0);
    
      if (serviceFFE0 == nullptr) {
        mqttPublishError("Service FFE0 nicht gefunden");
        mqttPublishStatus("ble_failed");
        cleanupBleClient("Service FFE0 nicht gefunden", DELETE_CLIENT_AFTER_FAILURE);
        logSessionSummary("FAIL: Service FFE0 nicht gefunden");
        return false;
      }
    
      serviceBleFor(AFTER_SERVICE_DELAY_MS);
    
      if (!isBleConnected()) {
        mqttPublishStatus("idle_no_weight");
        cleanupBleClient("nach Service-Suche getrennt", DELETE_CLIENT_AFTER_FAILURE);
        logSessionSummary("IDLE: nach Service-Suche getrennt");
        return false;
      }
    
      chrFFE1 = serviceFFE0->getCharacteristic(UUID_CHAR_FFE1);
      chrFFE2 = serviceFFE0->getCharacteristic(UUID_CHAR_FFE2);
      chrFFE3 = serviceFFE0->getCharacteristic(UUID_CHAR_FFE3);
      chrFFE4 = serviceFFE0->getCharacteristic(UUID_CHAR_FFE4);
    
      if (!subscribeNotify(chrFFE1)) {
        mqttPublishError("FFE1 Subscribe fehlgeschlagen");
        mqttPublishStatus("ble_failed");
        cleanupBleClient("FFE1 Subscribe fehlgeschlagen", DELETE_CLIENT_AFTER_FAILURE);
        logSessionSummary("FAIL: FFE1 Subscribe fehlgeschlagen");
        return false;
      }
    
      serviceBleFor(AFTER_FFE1_SUBSCRIBE_MS);
      serviceBleFor(AFTER_FFE1_STABILIZE_MS);
    
      if (!waitForStatus12(WAIT_FOR_STATUS_12_MS)) {
        mqttPublishStatus("ble_failed");
        cleanupBleClient("kein 0x12 Statuspaket", DELETE_CLIENT_AFTER_FAILURE);
        logSessionSummary("FAIL: kein 0x12 Statuspaket");
        return false;
      }
    
      if (!writeFFE3KgInit()) {
        mqttPublishError("FFE3 fehlgeschlagen");
        mqttPublishStatus("ble_failed");
        cleanupBleClient("FFE3 fehlgeschlagen", DELETE_CLIENT_AFTER_FAILURE);
        logSessionSummary("FAIL: FFE3 fehlgeschlagen");
        return false;
      }
    
      serviceBleFor(AFTER_FFE3_WRITE_MS);
    
      if (!waitForAck14(WAIT_FOR_ACK_14_MS)) {
        mqttPublishStatus("ble_failed");
        cleanupBleClient("kein 0x14 Ack", DELETE_CLIENT_AFTER_FAILURE);
        logSessionSummary("FAIL: kein 0x14 Ack");
        return false;
      }
    
      if (!writeFFE4TimeHandshake()) {
        mqttPublishError("FFE4 fehlgeschlagen");
        mqttPublishStatus("ble_failed");
        cleanupBleClient("FFE4 fehlgeschlagen", DELETE_CLIENT_AFTER_FAILURE);
        logSessionSummary("FAIL: FFE4 fehlgeschlagen");
        return false;
      }
    
      serviceBleFor(AFTER_FFE4_WRITE_MS);
    
      mqttPublishStatus("waiting_for_weight10");
      logInfo("BLE-Session aktiv. Warte auf Gewichtsdaten.");
    
      return true;
    }
    
    void runConnectedSession() {
      uint32_t sessionActiveStartMs = millis();
    
      while (isBleConnected()) {
        ensureNetwork();
        processNotifyQueue();
    
        uint32_t now = millis();
    
        if (!hasAnyFinal) {
          if (now - sessionActiveStartMs > SESSION_MAX_WITHOUT_WEIGHT_MS) {
            mqttPublishStatus("idle_no_weight");
            logInfo("Keine Gewichtsdaten innerhalb des Session-Fensters. Vermutlich keine aktive Messung.");
    
            cleanupBleClient("idle_no_weight: keine Finalmessung", DELETE_CLIENT_AFTER_FAILURE);
            logSessionSummary("IDLE: keine Finalmessung");
    
            delay(IDLE_NO_WEIGHT_PAUSE_MS);
            return;
          }
        } else {
          if (sessionFirstFinalMs > 0 && now - sessionFirstFinalMs > SESSION_MAX_AFTER_FIRST_FINAL_MS) {
            if (SEND_CLEANUP_AFTER_FINAL) {
              sendScaleCleanupCommand();
            }
    
            cleanupBleClient("Finalgewicht: maximale Nachlaufzeit erreicht", DELETE_CLIENT_AFTER_SUCCESS);
            logSessionSummary("OK: Finalgewicht veröffentlicht / max Nachlauf erreicht");
    
            waitAfterSuccessfulMeasurement();
            return;
          }
    
          if (sessionFirstFinalMs > 0 && now - sessionFirstFinalMs > SESSION_IDLE_AFTER_FINAL_MS) {
            if (SEND_CLEANUP_AFTER_FINAL) {
              sendScaleCleanupCommand();
            }
    
            logInfo("Finalmessung liegt " + String(SESSION_IDLE_AFTER_FINAL_MS) +
                    " ms zurück. Warte weiterhin auf Trennung oder neue abweichende Finalpakete.");
          }
        }
    
        delay(CONNECTED_LOOP_DELAY_MS);
      }
    
      processNotifyQueue();
    
      if (hasAnyFinal) {
        logInfo("Waage hat die BLE-Verbindung nach Finalmessung selbst getrennt.");
        cleanupBleClient("Finalgewicht nach abgeschlossener Session", DELETE_CLIENT_AFTER_SUCCESS);
        logSessionSummary("OK: Finalgewicht veröffentlicht");
    
        waitAfterSuccessfulMeasurement();
        return;
      }
    
      mqttPublishStatus("idle_no_weight");
      logInfo("BLE getrennt ohne Gewichtsdaten. Vermutlich keine aktive Messung.");
      cleanupBleClient("idle_no_weight: getrennt ohne Gewichtsdaten", DELETE_CLIENT_AFTER_FAILURE);
      logSessionSummary("IDLE: getrennt ohne 0x10");
    
      delay(IDLE_NO_WEIGHT_PAUSE_MS);
    }
    
    
    /***************************************************************
     * 18) SETUP
     ***************************************************************/
    
    void setup() {
      Serial.begin(115200);
      delay(1500);
    
      Serial.println();
      Serial.println("RENPHO QN-Scale BLE MQTT Bridge");
      Serial.println("Version 0.4.34");
      Serial.println("ESP32-S3-Zero");
      Serial.println();
    
      connectWiFi();
      connectMQTT();
    
      NimBLEDevice::init("ESP32-QNScale-Client");
      NimBLEDevice::setPower(ESP_PWR_LVL_P9);
    
      pBLEScan = NimBLEDevice::getScan();
      pBLEScan->setActiveScan(true);
      pBLEScan->setInterval(45);
      pBLEScan->setWindow(45);
    
      mqttPublishStatus("ready");
      mqttPublishRaw(MQTT_TOPIC_REARM_STATE, "ready", MQTT_RETAIN_STATUS);
    
      logInfo("BLE initialisiert.");
      logInfo("Es wird ausschließlich nach der bekannten Waage gesucht: " + String(SCALE_ADDRESS));
      logInfo("WAIT_FOR_STATUS_12_BEFORE_INIT=true");
      logInfo("AFTER_FFE1_STABILIZE_MS=" + String(AFTER_FFE1_STABILIZE_MS));
      logInfo("SESSION_MAX_WITHOUT_WEIGHT_MS=" + String(SESSION_MAX_WITHOUT_WEIGHT_MS) +
              ", SESSION_IDLE_AFTER_FINAL_MS=" + String(SESSION_IDLE_AFTER_FINAL_MS) +
              ", SESSION_MAX_AFTER_FIRST_FINAL_MS=" + String(SESSION_MAX_AFTER_FIRST_FINAL_MS));
      logInfo("SEND_CLEANUP_AFTER_FINAL=" + String(SEND_CLEANUP_AFTER_FINAL ? "true" : "false"));
      logInfo("Nach Finalmessung: Verbindung offen halten, weitere abweichende Finalpakete veröffentlichen.");
    }
    
    
    /***************************************************************
     * 19) LOOP
     ***************************************************************/
    
    void loop() {
      ensureNetwork();
    
      bool ok = connectScale();
    
      if (!ok) {
        logDebug("Kein aktiver Messzyklus. Neuer Versuch in " + String(RECONNECT_PAUSE_MS) + " ms.");
        delay(RECONNECT_PAUSE_MS);
        return;
      }
    
      runConnectedSession();
    
      delay(RECONNECT_PAUSE_MS);
    }
    

    ioBroker auf Synology DS920+ (SSD Raid / 20GB RAM) im VMM

    1 Antwort Letzte Antwort
    0

    Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

    Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

    Mit deinem Input könnte dieser Beitrag noch besser werden 💗

    Registrieren Anmelden
    Antworten
    • In einem neuen Thema antworten
    Anmelden zum Antworten
    • Älteste zuerst
    • Neuste zuerst
    • Meiste Stimmen


    Support us

    ioBroker
    Community Adapters
    Donate

    364

    Online

    32.9k

    Benutzer

    83.0k

    Themen

    1.3m

    Beiträge
    Community
    Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
    ioBroker Community 2014-2025
    logo
    • Anmelden

    • Du hast noch kein Konto? Registrieren

    • Anmelden oder registrieren, um zu suchen
    • Erster Beitrag
      Letzter Beitrag
    0
    • Home
    • Aktuell
    • Tags
    • Ungelesen 0
    • Kategorien
    • Unreplied
    • Beliebt
    • GitHub
    • Docu
    • Hilfe