Tepelné čerpadlo – vlastní řídicí systém přes Modbus RTU-{rozpracovane}

Tepelné čerpadlo – vlastní řídicí systém přes Modbus RTU-{rozpracovane}
Ridici jednotka
NODE-RED

Projekt se zabývá návrhem a implementací vlastního řídicího a monitorovacího systému tepelného čerpadla s přímým přístupem k interním datům zařízení prostřednictvím Modbus RTU komunikace po RS485.

Řídicí jednotka je postavena na ATmega2560, která zajišťuje:

  • periodické čtení provozních Modbus registrů
  • zápis řídicích a konfiguračních hodnot
  • zpracování stavové logiky
  • lokální i vzdálené rozhraní pro dohled

Komunikace s tepelným čerpadlem probíhá výhradně lokálně, bez použití výrobního cloudu nebo proprietárních aplikací. Systém je navržen jako deterministický a offline provozuschopný, tedy plně funkční i bez dostupnosti internetu.

Pro lokální diagnostiku a obsluhu slouží LCD 20×4, síťová komunikace a integrace do nadřazených systémů je realizována pomocí Ethernet modulu W5500.

Hlavní cíle projektu:

  • přímý přístup k Modbus registrům zařízení
  • plná kontrola nad řídicí logikou
  • možnost detailního monitoringu provozních veličin
  • dlouhodobá stabilita bez závislosti na externích službách

Následující kapitoly se věnují hardwarové architektuře, implementaci Modbus komunikace a detailnímu rozboru řídicího kódu a použitých funkcí.

Modbus komunikace – obecný princip řízení

Tepelné čerpadlo je řízeno a monitorováno prostřednictvím standardního protokolu Modbus RTU po sběrnici RS485. Řídicí jednotka vystupuje v roli Modbus master, zatímco řídicí deska tepelného čerpadla pracuje jako slave zařízení. Veškerá komunikace probíhá lokálně, bod–bod po sběrnici RS485, bez účasti externích serverů nebo cloudových služeb.

Použitý protokol odpovídá běžné implementaci Modbus RTU:

  • asynchronní sériová komunikace
  • pevně dané parametry linky (rychlost, datové bity, stop bity)
  • kontrola integrity pomocí CRC16
  • master–slave model s explicitním dotazováním zařízení

Řídicí systém pracuje s několika typy Modbus operací:

  • čtení provozních dat (stavové informace, měřené veličiny)
  • zápis řídicích parametrů (režimy, požadované hodnoty)
  • ovládací příkazy (přepínání stavů, reset vybraných funkcí)

Komunikace je navržena jako deterministická – každá operace je iniciována řídicí jednotkou, odpovědi jsou časově kontrolovány a vyhodnocovány. Při chybě komunikace nebo neplatné odpovědi systém přechází do definovaného bezpečného stavu a zápisy nejsou prováděny.

Z hlediska návrhu je Modbus vrstva oddělena od aplikační logiky:

  • nízkoúrovňová část řeší rámce, CRC a timeouty
  • vyšší vrstva pracuje již s logickými veličinami a stavy
  • aplikační logika rozhoduje, kdy a za jakých podmínek se hodnoty čtou nebo zapisují

Tento přístup umožňuje:

  • snadné rozšíření systému
  • přehlednou správu řídicích funkcí
  • bezpečné oddělení komunikace od rozhodovací logiky

Detailní struktura dat, význam jednotlivých položek a jejich zpracování v kódu vychází z dostupné Modbus dokumentace výrobce a je rozebrána až v následujících kapitolách, společně s konkrétní implementací funkcí pro čtení a zápis.

Modbus komunikační protokol – popis výrobce (upravený překlad)

Řídicí deska tepelného čerpadla komunikuje pomocí Modbus RTU protokolu po sběrnici RS485. Komunikace je realizována jako asynchronní sériový přenos s následujícími parametry:

  • Rozhraní: RS485
  • Přenos: asynchronní
  • Formát rámce:
    • 1 start bit
    • 8 datových bitů
    • 2 stop bity
    • bez parity
  • Přenosová rychlost: 19 200 bps

Datová struktura odpovídá standardu Modbus RTU, přičemž:

  • data jsou přenášena jako 16bitové hodnoty
  • kontrola integrity je zajištěna pomocí CRC16
  • v CRC je nejprve přenášen nižší bajt, poté vyšší

Adresace zařízení

Zařízení pracuje jako Modbus slave a může mít adresu v rozsahu 1 až 8.
Adresa zařízení je nastavena pomocí hardwarového přepínače (DIP switch).

Řídicí jednotka (nadřazený systém) vystupuje vždy jako Modbus master, který:

  • iniciuje veškerou komunikaci
  • zasílá dotazy
  • vyhodnocuje odpovědi zařízení

Použité Modbus funkce

Pro komunikaci jsou podporovány následující standardní Modbus příkazy:

🔹 Funkce 03H – Čtení holding registrů

Slouží ke čtení provozních a stavových dat.

Master odesílá:

  • adresu zařízení
  • kód funkce 03H
  • počáteční adresu registru
  • počet čtených registrů
  • CRC16

Slave odpovídá:

  • adresou zařízení
  • kódem funkce
  • počtem vrácených bajtů
  • daty
  • CRC16

🔹 Funkce 06H – Zápis jednoho registru

Používá se pro zápis jednotlivých parametrů.

  • při úspěšném zápisu zařízení echo vrátí celý rámec
  • při chybě zařízení neodpovídá

🔹 Funkce 10H – Zápis více registrů

Umožňuje zápis většího množství konfiguračních hodnot v jednom rámci.

Odpověď zařízení obsahuje:

  • adresu zařízení
  • kód funkce
  • počáteční adresu
  • počet zapsaných registrů
  • CRC16

🔹 Funkce 01H – Čtení bitových stavů (Coils)

Používá se pro čtení stavových bitů a logických příznaků.

Zařízení vrací:

  • počet datových bajtů
  • bitově kódované stavy
  • CRC16

🔹 Funkce 05H – Zápis jednoho bitu (Coil)

Slouží k přímému ovládání binárních funkcí.

  • hodnota 0xFF00 odpovídá logické 1
  • hodnota 0x0000 odpovídá logické 0
  • při úspěchu je rámec vrácen beze změny

Přístup k datům a omezení

  • Pouze jednotka s adresou 1 umožňuje zápis konfiguračních parametrů
  • Ostatní jednotky podporují pouze čtení
  • Registry jsou rozděleny na:
    • stavové
    • chybové
    • měřené
    • konfigurační

Význam jednotlivých datových položek, jejich škálování a použití je definováno výrobcem a je řešeno v aplikační vrstvě řídicího systému.

Přehled firmware a struktura kódu

Následující zdrojový kód představuje kompletní firmware řídicí jednotky tepelného čerpadla, který zajišťuje lokální komunikaci se zařízením přes Modbus RTU (RS485), zpracování provozních dat a jejich prezentaci prostřednictvím LCD rozhraní a MQTT telemetrie.

Firmware je navržen jako cyklicky řízený systém bez závislosti na cloudu, s jasným oddělením jednotlivých funkčních vrstev:

  • komunikační vrstva (Modbus, Ethernet, MQTT)
  • sběr a validace dat
  • aplikační logika a interpretace stavů
  • uživatelské rozhraní (LCD, sériová linka)
  • vzdálené ovládání a dohled (MQTT)

Čtení dat z tepelného čerpadla probíhá blokově v pevně daném intervalu, aby se minimalizovala zátěž RS485 sběrnice a zajistila deterministická odezva systému. Zápisy řídicích parametrů jsou prováděny pouze na základě explicitních příkazů (MQTT nebo sériová konzole) a jsou chráněny základní validací rozsahů.

Kód je strukturován tak, aby:

  • byl snadno rozšiřitelný o další veličiny nebo funkce
  • umožňoval jednoduché ladění pomocí sériového výpisu
  • poskytoval konzistentní data pro lokální i vzdálené zobrazení

V následujících částech je kód rozebrán po jednotlivých funkčních blocích, včetně principu čtení dat, mapování hodnot a zpracování řídicích příkazů.

1) Deklarace a globální konfigurace

Tato část definuje použité knihovny, periférie a základní parametry systému. Firmware kombinuje:

  • Modbus RTU (RS485) pro komunikaci s tepelným čerpadlem
  • Ethernet (W5500) pro síť
  • MQTT + JSON pro telemetrii a vzdálené ovládání
  • LCD 20×4 (I2C) pro lokální diagnostiku

Zároveň jsou zde definovány:

  • adresy/identifikátory pro Modbus a MQTT
  • intervaly čtení a přepínání obrazovek
  • buffery a pomocné převodní funkce (škálování teplot, tlaků)
  • jednoduché API pro zápis/čtení vybraných parametrů (P06/P07, setpointy)

Poznámky k návrhu:

  • používá se blokové čtení (nižší režie na lince RS485)
  • hodnoty pro LCD jsou drženy ve values[][] a obnovují se pouze při úspěšném čtení
  • MQTT payload je serializován do předem alokovaného bufferu (bez dynamické alokace)

2) setup() – inicializace systému

setup() inicializuje všechny periferie v pořadí, které minimalizuje riziko problémů při startu:

  1. I2C + LCD
    • start sběrnice I2C
    • inicializace LCD a zapnutí podsvícení
  2. Serial debug
    • spuštění UART pro ladění (115200)
    • čekání na dostupnost portu
  3. Ethernet (W5500)
    • inicializace SPI a nastavení CS pinu
    • DHCP pokus o získání IP
    • diagnostika HW a link stavu
  4. MQTT
    • nastavení serveru, callbacku a velikosti bufferu
    • první pokus o připojení (non-blocking reconnect logika)
  5. Modbus RTU (RS485)
    • inicializace Serial1 (19200 8N2)
    • nastavení timeoutu
    • přiřazení Modbus slave ID a linky pro ModbusMaster

Výsledkem setup() je systém připravený periodicky číst data z Modbus a současně držet aktivní MQTT spojení.


3) loop() – hlavní smyčka a úlohy

Hlavní smyčka je rozdělena do několika logických částí:

3.1 Periodické Modbus čtení (polling)

Každých READ_INTERVAL_MS proběhne:

  • alokace lokálních bufferů (tBuf, modeBuf, pBuf, setBuf, …)
  • blokové čtení rozsahů (teploty, režimy, tlaky, setpointy)
  • čtení jednotlivých stavových registrů (ventil, kompresor, pumpa, status)

Po úspěšném čtení se:

  • vypíše diagnostika na Serial
  • sestaví JSON payload a odešle přes MQTT
  • aktualizují se hodnoty values[][] pro LCD
Důležité: lokální buffery uvnitř loop() minimalizují riziko práce se „starými daty“ a drží jasnou hranici mezi jedním cyklem čtení a dalším.

3.2 Ovládání přes Serial (CLI)

Příkazy jako:

  • on / off (spínání přes coil)
  • HOTxx / TUVxx (setpointy)
  • pX / mX (režimy P06/P07)

zapisují do zařízení pouze po validaci rozsahu.

3.3 Přepínání LCD obrazovek

Každých SCREEN_INTERVAL se inkrementuje index obrazovky a provede se výpis.

3.4 MQTT servis a reconnect

  • pokud MQTT není připojeno, spouští se reconnect s backoff
  • mqttClient.loop() zpracuje příchozí zprávy (MQTT ovládání)

1) Deklarace a globální konfigurace (kód)

#include <ModbusMaster.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

ModbusMaster node;
LiquidCrystal_I2C lcd(0x27, 20, 4);

// MQTT nastavení
const char* mqtt_server = "192.168.0.100";  // IP MQTT SERVERU
const char* mqtt_client_id = "heatpump_monitor";
const char* mqtt_topic = "heatpump/telemetry";
const char* mqtt_topic_commands = "heatpump/commands";

EthernetClient ethMqttClient;
PubSubClient mqttClient(ethMqttClient);

#define SS_PIN 53
byte mac[] = { 0x48, 0x45, 0x41, 0x54, 0x50, 0x4D };
EthernetClient ethClient;

// Počet obrazovek
const uint8_t SCREEN_COUNT = 5;
const unsigned long SCREEN_INTERVAL = 5000;

// Globální stav LCD
unsigned long lastSwitch = 0;
uint8_t currentScreen = 0;

// Hodnoty pro LCD
float values[SCREEN_COUNT][4] = {}; // automaticky vynulováno

// Popisky
const char* labels[SCREEN_COUNT][4] = {
  { "T Vstup:    ", "T Vystup:   ", "T Venku:    ", "T Bojler:   " },
  { "T sani:     ", "T coil:     ", "T vypar:    ", "T konden:   " },
  { "T vytlaku:  ", "T komp:     ", "Napeti inv: ", "Frekv Hz:   " },
  { "EEV1 krok:  ", "EEV2 krok:  ", "Tlak LP bar:", "Tlak HP bar:" },
  { "Vent rpm:   ", "------", "------", "------" }
};

// Modbus konfigurace
static const uint8_t  SLAVE_ID = 1;
static const uint32_t RS485_BAUD = 19200;
static const uint32_t RS485_CFG = SERIAL_8N2;
static const uint8_t  MODBUS_RETRIES = 3;
static const uint16_t MODBUS_TIMEOUT_MS = 120;

static const unsigned long READ_INTERVAL_MS = 5000;  // cetnost cteni dat z rs485 linky v ms

// Registry
static const uint16_t REG_T_START = 0x000E;
static const uint16_t REG_T_END   = 0x0029;
static const uint16_t REG_T_COUNT = (REG_T_END - REG_T_START + 1);

// Převodní funkce
float dec01(uint16_t v) { return (int16_t)v / 10.0f; }
float dec05(uint16_t v) { return (int16_t)v * 0.5f; }
float decBar(uint16_t v) { return (int16_t)v * 0.01f; }

// Funkce pro zápis a čtení
bool writeP06(uint16_t mode) {
  if (mode > 4) return false;
  return node.writeSingleRegister(0x0036, mode) == node.ku8MBSuccess;
}
int readP06() {
  return (node.readHoldingRegisters(0x0036, 1) == node.ku8MBSuccess) ? node.getResponseBuffer(0) : -1;
}

bool writeP07(uint16_t mode) {
  if (mode > 3) return false;
  return node.writeSingleRegister(0x0190, mode) == node.ku8MBSuccess;
}
int readP07() {
  return (node.readHoldingRegisters(0x0190, 1) == node.ku8MBSuccess) ? node.getResponseBuffer(0) : -1;
}

bool writeHOT(int tempC) {
  if (tempC < 10 || tempC > 45) return false;
  return node.writeSingleRegister(0x00CC, (tempC * 2) & 0xFF) == node.ku8MBSuccess;
}
int readHOT() {
  return (node.readHoldingRegisters(0x00CC, 1) == node.ku8MBSuccess) ? (node.getResponseBuffer(0) & 0xFF) / 2 : -1;
}

bool writeTUV(int tempC) {
  if (tempC < 10 || tempC > 50) return false;
  return node.writeSingleRegister(0x00CA, (tempC * 2) & 0xFF) == node.ku8MBSuccess;
}
int readTUV() {
  return (node.readHoldingRegisters(0x00CA, 1) == node.ku8MBSuccess) ? (node.getResponseBuffer(0) & 0xFF) / 2 : -1;
}

// Režimy – textové popisky
const char* modeP06(uint16_t v) {
  switch (v) {
    case 0: return "DHW";
    case 1: return "HEATING";
    case 2: return "COOLING";
    case 3: return "HEAT + DHW";
    case 4: return "COOL + DHW";
  }
  return "UNKNOWN";
}
const char* modeP07(uint16_t v) {
  switch (v) {
    case 0: return "NORMAL";
    case 1: return "ECO";
    case 2: return "QUIET";
    case 3: return "HI-COP";
  }
  return "UNKNOWN";
}

// Čtení
bool readReg(uint16_t reg, uint16_t &val) {
  for (uint8_t i = 0; i < MODBUS_RETRIES; i++) {
    if (node.readHoldingRegisters(reg, 1) == node.ku8MBSuccess) {
      val = node.getResponseBuffer(0);
      return true;
    }
  }
  return false;
}

bool readBlock(uint16_t start, uint16_t count, uint16_t *dest) {
  for (uint8_t i = 0; i < MODBUS_RETRIES; i++) {
    if (node.readHoldingRegisters(start, count) == node.ku8MBSuccess) {
      for (uint16_t k = 0; k < count; k++) dest[k] = node.getResponseBuffer(k);
      return true;
    }
  }
  return false;
}

void prepareJsonPayload(
  char* buffer, size_t bufferSize,
  uint16_t tBuf[], uint16_t modeBuf[], uint16_t pBuf[],
  uint16_t rP07, uint16_t r06,uint16_t r05,uint16_t r04,uint16_t r03, bool okSet, uint16_t setBuf[]
) {
  StaticJsonDocument<800> doc;

  // Čas (Unix timestamp – později můžeš doplnit NTP)
  doc["ts"] = millis() / 1000;

  // Stav napájení
  bool powerOn = (modeBuf[0] & 1) != 0;
  doc["power_on"] = powerOn;

  

  // Režimy
  doc["mode"] = modeP06(modeBuf[4]); // 0x0036 = modeBuf[4]
  doc["fan_mode"] = modeP07(rP07);

  // Třícestný ventil
  bool valveToTank = (r05 & (1 << 6)) != 0; // 0x0005, bit 6
  doc["valve_to_tank"] = valveToTank;

  // Běží kompresor?
  bool compRun = (r04 & (1 << 0)) != 0; // 0x0004, bit 0
  doc["comp_run"] = compRun;
  
  // Chyba ?
  bool compErr = (r03 & (1 << 6)) != 0; // 0x0003, bit 6
  doc["comp_err"] = compErr;
  // Odmrazovani ?
  bool compDefr = (r03 & (1 << 7)) != 0; // 0x0003, bit 7
  doc["comp_defr"] = compDefr;

  // Běží kompresor?
  bool waterRun = (r06 & (1 << 6)) != 0; // 0x0006, bit 6
  doc["water_run"] = waterRun;



  // Pomocná lambda
  auto T = [&](uint16_t reg) -> uint16_t {
    return tBuf[reg - REG_T_START];
  };

  // Teploty
  doc["temps"]["inlet"]    = dec01(T(0x000E));  // Vstup
  doc["temps"]["outlet"]   = dec01(T(0x0012));  // Výstup
  doc["temps"]["ambient"]  = dec05(T(0x0011));  // Venku
  doc["temps"]["tank"]     = dec01(T(0x000F));  // Bojler
  doc["temps"]["suction"]  = dec05(T(0x0015));  // Sání
  doc["temps"]["coil"]     = dec05(T(0x0016));  // Coil
  doc["temps"]["evap"]     = dec01(T(0x0028));  // Výparník
  doc["temps"]["cond"]     = dec01(T(0x0029));  // Kondenzátor
  doc["temps"]["exhaust"]  = (int16_t)T(0x001B); // Výtlak
  doc["temps"]["drive"]    = dec05(T(0x0022));  // Kompresor/IPM

  // Tlaky – POZOR: v PDF jsou názvy prohozené!
  doc["pressures"]["lp"] = decBar(pBuf[1]); // 0x0030 = LP
  doc["pressures"]["hp"] = decBar(pBuf[0]); // 0x002F = HP

  // Ostatní
  doc["compressor_hz"] = (int16_t)T(0x001E);
  doc["fan_rpm"]       = (int16_t)T(0x0026);
  doc["eev1_step"]     = (int16_t)T(0x001C);
  doc["eev2_step"]     = (int16_t)T(0x001D);
  doc["dc_bus_v"]      = (int16_t)T(0x0021);

  // Cílové teploty
  if (okSet) {
    doc["setpoints"]["heating"] = (setBuf[2] & 0xFF) / 2; // 0x00CC
    doc["setpoints"]["hotwater"] = (setBuf[0] & 0xFF) / 2; // 0x00CA
  }

  // Serializace
  serializeJson(doc, buffer, bufferSize);
}
/*
void mqttReconnect() {
  while (!mqttClient.connected()) {
    Serial.print("Pripojuji se k MQTT... ");
    if (mqttClient.connect(mqtt_client_id)) {
      Serial.println("OK");
      // 🔑 přihlásit se k odběru příkazů!
      mqttClient.subscribe(mqtt_topic_commands);
      Serial.println("Přihlášen k odběru: " + String(mqtt_topic_commands));
    } else {
      Serial.print("CHYBA, kod: ");
      Serial.println(mqttClient.state());
      delay(1000);
    }
  }
}
*/
void mqttReconnect() {
  static unsigned long lastReconnectAttempt = 0;
  if (millis() - lastReconnectAttempt < 3000) return; // čekat 3s mezi pokusy

  lastReconnectAttempt = millis();
  if (mqttClient.connect(mqtt_client_id)) {
    Serial.println("MQTT: OK");
    mqttClient.subscribe(mqtt_topic_commands);
  } else {
    Serial.print("MQTT: selhal, kod ");
    Serial.println(mqttClient.state());
    // žádný delay!
  }
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
  if (strcmp(topic, mqtt_topic_commands) == 0) {
    String msg = "";
    for (int i = 0; i < length; i++) msg += (char)payload[i];
    Serial.print("MQTT prikaz: ");
    Serial.println(msg);

    StaticJsonDocument<300> doc;
    if (deserializeJson(doc, msg)) {
      Serial.println("JSON: chyba parsovani");
      return;
    }

    // POWER
    if (doc.containsKey("power")) {
      bool on = doc["power"];
      Serial.print("MQTT: nastavuji power na ");
      Serial.println(on ? "ON" : "OFF");

      uint8_t r = node.writeSingleCoil(0x0320, on ? 1 : 0);
      if (r == node.ku8MBSuccess) {
        Serial.println("✅ POWER OK");
      } else {
        Serial.print("❌ POWER CHYBA: "); Serial.println(r);
      }
      delay(200); // ⚠️ DŮLEŽITÉ!
    }

    // MODE (P06)
    if (doc.containsKey("mode")) {
      String m = doc["mode"];
      int mode = -1;
      if (m == "DHW") mode = 0;
      else if (m == "HEATING") mode = 1;
      else if (m == "COOLING") mode = 2;
      else if (m == "HEAT + DHW") mode = 3;
      else if (m == "COOL + DHW") mode = 4;

      if (mode >= 0 && mode <= 4) {
        Serial.print("MQTT: nastavuji P06 na ");
        Serial.println(m);

        if (writeP06(mode)) {
          Serial.println("✅ P06 OK");
        } else {
          Serial.println("❌ P06 CHYBA");
        }
        delay(200);
      }
    }

    // FAN MODE (P07)
    if (doc.containsKey("fan_mode")) {
      String f = doc["fan_mode"];
      int mode = -1;
      if (f == "NORMAL") mode = 0;
      else if (f == "ECO") mode = 1;
      else if (f == "QUIET") mode = 2;
      else if (f == "HI-COP") mode = 3;

      if (mode >= 0 && mode <= 3) {
        Serial.print("MQTT: nastavuji P07 na ");
        Serial.println(f);

        if (writeP07(mode)) {
          Serial.println("✅ P07 OK");
        } else {
          Serial.println("❌ P07 CHYBA");
        }
        delay(200);
      }
    }

    // TEPLOTA TOPENÍ
    if (doc.containsKey("heating_setpoint")) {
      int t = doc["heating_setpoint"];
      if (t >= 10 && t <= 45) {
        Serial.print("MQTT: nastavuji HOT na ");
        Serial.println(t);
        if (writeHOT(t)) {
          Serial.println("✅ HOT OK");
        } else {
          Serial.println("❌ HOT CHYBA");
        }
        delay(200);
      }
    }

    // TEPLOTA TUV
    if (doc.containsKey("hotwater_setpoint")) {
      int t = doc["hotwater_setpoint"];
      if (t >= 10 && t <= 50) {
        Serial.print("MQTT: nastavuji TUV na ");
        Serial.println(t);
        if (writeTUV(t)) {
          Serial.println("✅ TUV OK");
        } else {
          Serial.println("❌ TUV CHYBA");
        }
        delay(200);
      }
    }
  }
}

2) setup() – inicializace systému (kód)

void setup() {
  Wire.begin();
  lcd.init();
  lcd.backlight();

  Serial.begin(115200);
  while (!Serial) {}

  // Vynulování hodnot pro LCD
  for (uint8_t s = 0; s < SCREEN_COUNT; s++)
    for (uint8_t r = 0; r < 4; r++)
      values[s][r] = 0.0f;

  // Ethernet
  SPI.begin();
  Ethernet.init(SS_PIN);
  Serial.print("Inicializuji DHCP... ");
  if (Ethernet.begin(mac) == 0) {
    Serial.println("Chyba DHCP");
    if (Ethernet.hardwareStatus() == EthernetNoHardware)
      Serial.println("HW nenalezen");
    else if (Ethernet.linkStatus() == LinkOFF)
      Serial.println("Kabel odpojen");
    delay(2000);
  } else {
    Serial.println("Sit OK");
    Serial.print("IP: ");
    Serial.println(Ethernet.localIP());
    delay(5000);
  }

  // MQTT
  mqttClient.setServer(mqtt_server, 1883);
  mqttClient.setCallback(mqttCallback);
  mqttClient.setBufferSize(800);

  Serial.println(F("=== HEATPUMP MONITOR 2.0 (BLOCK READ) ==="));

  // Modbus RTU (RS485)
  Serial1.begin(RS485_BAUD, RS485_CFG);
  Serial1.setTimeout(MODBUS_TIMEOUT_MS);
  node.begin(SLAVE_ID, Serial1);

  mqttReconnect();
}

Inicializace periferií

  • I2C + LCD
    • inicializace sběrnice a LCD 20×4
    • zapnutí podsvícení pro okamžitou lokální diagnostiku
  • Serial debug
    • UART na 115200 bps
    • slouží výhradně pro ladění a ruční ovládání (CLI)

Síťová vrstva (Ethernet W5500)

  • inicializace SPI a CS pinu
  • získání IP adresy přes DHCP
  • diagnostika:
    • nepřítomnost Ethernet HW
    • odpojený síťový kabel
  • po úspěšném připojení je vypsána přidělená IP adresa

MQTT

  • konfigurace MQTT serveru a callbacku
  • zvětšený buffer pro JSON telemetrii
  • připojení je řešeno neblokujícím reconnect mechanismem

Modbus RTU

  • RS485 linka běží na samostatném UARTu (Serial1)
  • nastavení timeoutu pro odpovědi slave zařízení
  • inicializace Modbus masteru s pevně daným slave ID

Stav po setup()

Po dokončení inicializace je systém připraven:

  • periodicky číst data z tepelného čerpadla
  • publikovat telemetrii přes MQTT
  • přijímat příkazy z lokální sítě
  • zobrazovat stavové informace na LCD

3) loop() – hlavní smyčka (kód)

void loop() {
  static unsigned long lastModbusRead = 0;
  unsigned long now = millis();

  // ---------------------------------------------------
  // MODBUS ČTENÍ
  // ---------------------------------------------------
  if (now - lastModbusRead >= READ_INTERVAL_MS) {
    lastModbusRead = now;

    // Buffery – LOKÁLNÍ, správně uvnitř loop()
    uint16_t tBuf[REG_T_COUNT];
    uint16_t modeBuf[5];
    uint16_t pBuf[2];
    uint16_t setBuf[3];
    uint16_t copBuf[8];
    uint16_t rP07 = 0;
    uint16_t r06 = 0;
    uint16_t r05 = 0;
    uint16_t r04 = 0;   // 0x0004 → Output symbol 1 (compressor!)
    uint16_t r03 = 0;   // Working status mark
    //uint16_t r01 = 0;   // cop

    bool okT = readBlock(REG_T_START, REG_T_COUNT, tBuf);
    bool okMode = readBlock(0x0032, 5, modeBuf);
    bool okP = readBlock(0x002F, 2, pBuf);
    bool okP07 = readReg(0x0190, rP07);
    bool okV3 = readReg(0x0005, r05);
    bool okComp = readReg(0x0004, r04); // Output symbol 1
    bool okStat = readReg(0x0003, r03); // Working status mark
    bool okWater = readReg(0x0006, r06); // Working water pump status mark
    bool okSet = readBlock(0x00CA, 3, setBuf);
    //bool okCOP = readBlock(0x0001, 8 , copBuf); //COP

    // Lambda pro snadný přístup
    auto T = [&](uint16_t reg) -> uint16_t {
      return tBuf[reg - REG_T_START];
    };

    // --- Debug výpis  ---
    Serial.println();
    Serial.println(F("============== HEATPUMP STATUS =============="));

    if (okMode) {
      uint16_t r32 = modeBuf[0];
      uint16_t r36 = modeBuf[4]; // 0x0036 - 0x0032 = 4
      Serial.print(F("POWER: ")); Serial.println((r32 & 1) ? F("ON") : F("OFF"));
      Serial.print(F("TOPIM DO -> ")); Serial.println(modeP06(r36));
    } else {
      Serial.println(F("REZIMY: ERR"));
    }

    if (okP07) {
      Serial.print(F("REZIM -> ")); Serial.println(modeP07(rP07));
    } else {
      Serial.println(F("P07 MODE: ERR"));
    }

    if (okV3) {
      bool valveBit = (r05 & (1 << 6)) != 0;
      Serial.print(F("Ventil nastaven do > "));
      Serial.println(valveBit ? F("Bojleru") : F("Topeni"));
    } else {
      Serial.println(F("REG 0x0005: ERR"));
    }

    if (okComp) {
    bool compBit = (r04 & (1 << 0)) != 0;
      Serial.print(F("Bezi kompresor  >  "));
      Serial.println(compBit ? F("ANO") : F("NE"));
    } else {
      Serial.println(F("REG 0x0004: ERR"));
    }
    if (okStat) {
    bool statBit6 = (r03 & (1 << 6)) != 0;
    bool statBit7 = (r03 & (1 << 7)) != 0;
      Serial.print(F("Alarm and shutdown ? >  "));
      Serial.println(statBit6 ? F("ANO") : F("NE"));
      Serial.print(F("Defrost  >  "));
      Serial.println(statBit7 ? F("ANO") : F("NE"));
    } else {
      Serial.println(F("REG 0x0003: ERR"));
    }

    if (okWater) {
    bool pumpBit = (r06 & (1 << 6)) != 0;
      Serial.print(F("Bezi vodni pumpa  >  "));
      Serial.println(pumpBit ? F("ANO") : F("NE"));
    } else {
      Serial.println(F("REG 0x0006: ERR"));
    }
    
    Serial.println(F("=============================================="));

    int TUV_set = -1, HOT_set = -1;
    if (okSet) {
      TUV_set = (setBuf[0] & 0xFF) / 2;
      HOT_set = (setBuf[2] & 0xFF) / 2;
    }
    Serial.println(F("---Nastevene cilove teploty---"));
    Serial.print("HOT nastavena: "); Serial.print(HOT_set); Serial.println(" C");
    Serial.print("TUV nastavena: "); Serial.print(TUV_set); Serial.println(" C");
    Serial.println(F("=============================================="));

    if (!okT) {
      Serial.println(F("CIDLA: ERR"));
    } else {
      Serial.print(F("Vstup teple vody:           ")); Serial.print(dec01(T(0x000E))); Serial.println(F(" C"));
      Serial.print(F("Vystup teple vody:          ")); Serial.print(dec01(T(0x0012))); Serial.println(F(" C"));
      Serial.print(F("Venkovni teplota:           ")); Serial.print(dec05(T(0x0011))); Serial.println(F(" C"));
      Serial.print(F("T bojleru:                  ")); Serial.print(dec01(T(0x000F))); Serial.println(F(" C"));
      Serial.print(F("T sani kompresoru:          ")); Serial.print(dec05(T(0x0015))); Serial.println(F(" C"));
      Serial.print(F("T vymeniku (coil):          ")); Serial.print(dec05(T(0x0016))); Serial.println(F(" C"));
      Serial.print(F("T vyparniku (evap):         ")); Serial.print(dec01(T(0x0028))); Serial.println(F(" C"));
      Serial.print(F("T kondenzatoru:             ")); Serial.print(dec01(T(0x0029))); Serial.println(F(" C"));
      Serial.print(F("T vytlaku kompresoru:       ")); Serial.print((int16_t)T(0x001B)); Serial.println(F(" C"));
      Serial.print(F("T kompresoru:               ")); Serial.print(dec05(T(0x0022))); Serial.println(F(" C"));
      Serial.print(F("DC bus voltage:             ")); Serial.print((int16_t)T(0x0021)); Serial.println(F(" V"));
      Serial.print(F("DC fan speed:               ")); Serial.print((int16_t)T(0x0026)); Serial.println(F(" rpm"));
      Serial.print(F("EEV1 step:                  ")); Serial.println((int16_t)T(0x001C));
      Serial.print(F("EEV2 step:                  ")); Serial.println((int16_t)T(0x001D));
      Serial.print(F("Kompresor Hz:               ")); Serial.print((int16_t)T(0x001E)); Serial.println(F(" Hz"));
    }

    if (okP) {
      Serial.print(F("Nizky tlak (LP):            ")); Serial.print(decBar(pBuf[0])); Serial.println(F(" bar"));
      Serial.print(F("Vysoky tlak (HP):           ")); Serial.print(decBar(pBuf[1])); Serial.println(F(" bar"));
    } else {
      Serial.println(F("TLAKY: ERR"));
    }

    // --- ODESÍLÁNÍ JSON PŘES MQTT ---
    if (okT && okMode && okP && okP07 && okV3 && okComp) {
      // Připoj se k MQTT (pokud není připojen)
      if (!mqttClient.connected()) {
        mqttReconnect();
      }

      // Připrav JSON
      char jsonBuffer[900];
      prepareJsonPayload(jsonBuffer, sizeof(jsonBuffer), tBuf, modeBuf, pBuf, rP07, r06,r05, r04 ,r03, okSet, setBuf);

      // Odešli
      if (mqttClient.connected()) {
        mqttClient.publish(mqtt_topic, jsonBuffer);
        Serial.println("MQTT: zprava odeslana");
      }

      // Udržuj MQTT spojení
      mqttClient.loop();
    }

    // --- AKTUALIZACE values[][] pouze při úspěchu ---
    if (okT) {
      values[0][0] = dec01(T(0x000E));
      values[0][1] = dec01(T(0x0012));
      values[0][2] = dec05(T(0x0011));
      values[0][3] = dec01(T(0x000F));

      values[1][0] = dec05(T(0x0015));
      values[1][1] = dec05(T(0x0016));
      values[1][2] = dec01(T(0x0028));
      values[1][3] = dec01(T(0x0029));

      values[2][0] = (int16_t)T(0x001B);
      values[2][1] = dec05(T(0x0022));
      values[2][2] = (int16_t)T(0x0021);
      values[2][3] = (int16_t)T(0x001E);

      values[3][0] = (int16_t)T(0x001C);
      values[3][1] = (int16_t)T(0x001D);
      values[4][0] = (int16_t)T(0x0026);
    }
    if (okP) {
      values[3][2] = decBar(pBuf[0]);
      values[3][3] = decBar(pBuf[1]);
    }
  }

  // ---------------------------------------------------
  // SÉRIOVÉ OVLÁDÁNÍ
  // ---------------------------------------------------
  if (Serial.available()) {
    String cmd = Serial.readStringUntil('\n');
    cmd.trim();

    if (cmd == "on") {
      Serial.println("Zapinam...");
      if (node.writeSingleCoil(0x0320, 1) == node.ku8MBSuccess) Serial.println("ON OK.");
      else Serial.println("CHYBA ON!");
      delay(200);
    } else if (cmd == "off") {
      Serial.println("Vypinam...");
      if (node.writeSingleCoil(0x0320, 0) == node.ku8MBSuccess) Serial.println("OFF OK.");
      else Serial.println("CHYBA OFF!");
      delay(200);
    } else if (cmd.startsWith("HOT")) {
      int t = cmd.substring(3).toInt();
      if (writeHOT(t)) {
        Serial.println("HOT OK");
        Serial.print("HOT = "); Serial.print(readHOT()); Serial.println(" C");
      } else Serial.println("HOT: 10-45!");
    } else if (cmd.startsWith("TUV")) {
      int t = cmd.substring(3).toInt();
      if (writeTUV(t)) {
        Serial.println("TUV OK");
        Serial.print("TUV = "); Serial.print(readTUV()); Serial.println(" C");
      } else Serial.println("TUV: 10-50!");
    } else if (cmd.startsWith("p")) {
      int m = cmd.substring(1).toInt();
      if (writeP06(m)) {
        Serial.println("P06 OK");
        Serial.print("P06 = "); Serial.println(readP06());
      } else Serial.println("P06: 0-4!");
    } else if (cmd.startsWith("m")) {
      int m = cmd.substring(1).toInt();
      if (writeP07(m)) {
        Serial.println("P07 OK");
        Serial.print("P07 = "); Serial.println(readP07());
      } else Serial.println("P07: 0-3!");
    }
  }

  // ---------------------------------------------------
  // PŘEPÍNÁNÍ OBRAZOVEK
  // ---------------------------------------------------
  if (millis() - lastSwitch > SCREEN_INTERVAL) {
    lastSwitch = millis();
    currentScreen = (currentScreen + 1) % SCREEN_COUNT;
  }

  // ---------------------------------------------------
  // LCD VÝPIS 
  // ---------------------------------------------------
  lcd.clear();
  for (uint8_t i = 0; i < 4; i++) {
    lcd.setCursor(0, i);
    lcd.print(labels[currentScreen][i]);
    lcd.setCursor(14, i);

    if (currentScreen == 3) {
      if (i < 2) lcd.print((int)values[3][i]);
      else       lcd.print(values[3][i], 2);
    } else if (currentScreen == 2) {
      if (i >= 2) lcd.print((int)values[2][i]);
      else        lcd.print(values[2][i], 1);
    } else if (currentScreen == 4) {
      if (i == 0) lcd.print((int)values[4][0]);
    } else {
      lcd.print(values[currentScreen][i], 1);
    }
  }


    // Zpracování příchozích MQTT zpráv (ovládání)
  if (Ethernet.hardwareStatus() == EthernetNoHardware) {
    // skip
  } else {
    if (!mqttClient.connected()) {
      // Pokus o připojení
      if (Ethernet.localIP() != INADDR_NONE) {
        mqttReconnect();
        //mqttClient.connect(mqtt_client_id);
      }
    }
    if (mqttClient.connected()) {
      mqttClient.loop(); // zpracuje příchozí zprávy
    }
  }




  delay(100); // krátká pauza pro stabilitu – ne 1000!
}


/*
p0 → DHW (bojler)
p1 → HEATING (topení)
p2 → COOLING
p3 → HEAT + DHW
p4 → COOL + DHW
*/

/*
m0  TURBO / NORMAL  Nejvyšší výkon, žádné omezení
m1  ECO             Úsporný režim, nižší příkon
m2  SLEEP / QUIET   Tichý režim, nízké otáčky
m3  HI-COP          Nejlepší COP, optimalizace výkonu
*/

/*
on   zapnuti
off  vypnuti
*/

/*
HOT 10 az 40
TUV 10 az 50
*/

Hlavní princip smyčky

loop() je navržena jako kooperativní smyčka bez RTOS, rozdělená do jasně oddělených úloh řízených časovači (millis()).

Periodický Modbus polling

  • každých 5 s (READ_INTERVAL_MS) proběhne blokové čtení
  • lokální buffery zajišťují konzistenci dat v rámci jednoho cyklu
  • při chybě čtení se data dále nešíří (ochrana proti šíření neplatných hodnot)

Telemetrie (MQTT)

  • data jsou serializována do JSON
  • publikace probíhá pouze při úspěšném čtení klíčových registrů
  • MQTT reconnect je neblokující

LCD a lokální diagnostika

  • hodnoty jsou aktualizovány pouze při úspěšném čtení
  • LCD zobrazuje poslední platná data
  • obrazovky se cyklicky přepínají v pevně daném intervalu

Ovládání

  • jednoduché sériové CLI pro servisní účely
  • vzdálené řízení probíhá přes MQTT callback

Stav projektu a další vývoj

Uvedený zdrojový kód představuje aktuální funkční verzi firmware v době psaní tohoto zápisu. Projekt je však živý a průběžně vyvíjený – cílem není jednorázové řešení, ale dlouhodobě udržitelný a rozšiřitelný řídicí systém.

V dalších verzích se počítá zejména s:

  • doplněním watchdogu (WDT) pro zvýšení provozní spolehlivosti
  • dalším oddělením funkcí do samostatných modulů (Modbus / MQTT / UI)
  • optimalizací LCD obnovování a odstraněním blokujících prodlev
  • rozšířením diagnostiky a chybových stavů
  • postupným zpřesňováním mapování dat na základě reálného provozu

Kód je psán s důrazem na čitelnost a možnost dalšího rozvoje, nikoliv jako uzavřené finální řešení. Dokumentace i implementace se budou nadále aktualizovat společně s vývojem projektu.