Прошу помощи в отладке Arduino Mega 2560

Можно вообще все сделать на

Вот после публикации набрал и спросил у ИИ.

можешь сделать схему платы для esp01 реле с добавкой датчика даллас?

Да, конечно! Сделать такую схему несложно, но важно учесть одну аппаратную особенность ESP-01: у него всего 4 свободных GPIO, и большинство из них участвуют в логике загрузки микроконтроллера.

Если вы используете популярный китайский готовый модуль реле для ESP-01 (черная или синяя платка, куда вставляется ESP-01), то реле там управляется через пин GPIO0. Лучшим и самым безопасным выбором для подключения датчика Dallas (DS18B20) остается пин GPIO2. [1, 2, 3]

Ниже представлена текстовая схема и схема подключения для этого случая.


:clipboard: Схема подключений (Распиновка)

Для работы датчика Dallas DS18B20 обязательно нужен подтягивающий резистор на 4.7 кОм (или близкий, например, 10 кОм) между линиями данных (DQ) и питания (3.3V). [4, 5]

Компонент Вывод компонента Куда подключать на ESP-01 / плате реле Примечание
Реле (уже распаяно) Вход управления (In) GPIO0 Включается логическим нулем или единицей (зависит от версии платы v1.0 или v4.0).
Датчик DS18B20 GND (Черный) GND Общая земля.
Датчик DS18B20 VDD / VCC (Красный) 3.3V Важно: Питать строго от 3.3V, чтобы не повредить GPIO ESP8266.
Датчик DS18B20 DQ / DATA (Желтый) GPIO2 Сигнальный провод.
Резистор 4.7 кОм Вывод 1 GPIO2 (к датчику DQ) Подтяжка шины 1-Wire.
Резистор 4.7 кОм Вывод 2 3.3V (к питанию датчика)

:electric_plug: Принципиальная текстовая схема

Если вы собираете плату с нуля самостоятельно (на макетке или текстолите), вам также нужно правильно обвязать сам чип ESP-01, чтобы он стабильно запускался:

               +-----------------------+

               |  ESP-01 (Вид сверху)  |
               |                       |
       GND ----| [1] GND       TX  [5] |---- (Для прошивки)
     GPIO2 ----| [2] GPIO2     CH  [6] |---- [Резистор 10кОм] ---- 3.3V (Обязательно для старта!)
  (К реле) ----| [3] GPIO0             |
      3.3V ----| [4] VCC       RX  [8] |---- (Для прошивки)
               +-----------------------+

                 |             |
                 |             +---------+

                 |                       |
                 v                       v
         [Датчик DS18B20]         [Модуль Реле]
         +--------------+         +--------------+

         | GND   DQ  VDD|         | GND  IN   VCC|
         +--------------+         +--------------+

            |    |    |              |    |    |
GND --------+    |    +--- 3.3V -----+    |    +--- 5V/3.3V (Питание реле)

                 |    |                   |
                 +----+                   |
                   [Резистор 4.7к]        +---- (к GPIO0)


:warning: Важные нюансы при сборке

  1. Логика загрузки ESP8266: Пин GPIO2 во время включения ESP-01 обязательно должен быть подтянут к питанию (HIGH), иначе модуль уйдет в режим ошибки и не загрузится. Подтягивающий резистор 4.7 кОм для датчика Dallas идеально выполняет эту роль, поэтому система будет стартовать стабильно. [2]
  2. Питание: Датчик DS18B20 может работать и от 5V, но не подключайте резистор подтяжки к 5V! Порты ESP-01 рассчитаны строго на 3.3V. Подтягивайте датчик только к линии 3.3V. [6, 7, 8]
  3. Готовые платы реле: Если вы подпаиваетесь к готовому китайскому модулю реле, найдите на нем разъем, куда вставляется ESP-01. Вам достаточно припаять резистор и датчик прямо к ножкам GPIO2, GND и 3.3V этого разъема с обратной стороны платы. [9]

Подскажите, вы планируете разводить собственную печатную плату (например, в EasyEDA / KiCad) или хотите доработать готовый модуль реле? Я могу подсказать, как правильно организовать питание всей схемы, или помочь с кодом для опроса датчика и реле.

[1] https://www.edn.com

[2] https://github.com

[3] https://randomnerdtutorials.com

[4] https://diotlabs.daraghbyrne.me

[5] https://www.circuits-diy.com

[6] https://arduino.ru

[7] https://amperka.ru

[8] https://smacont.ru

[9] https://community.home-assistant.io

дорабатываем готовый модуль.

Отличный выбор, это сэкономит много времени. Модификация готового модуля сводится к аккуратной подпайке трех проводов датчика и одного резистора с обратной стороны платы.

Ниже приведена пошаговая инструкция, как сделать это быстро и надежно.

:hammer_and_wrench: Что понадобится

  • Датчик DS18B20 (в виде транзистора TO-92 или герметичный с проводами).
  • Резистор номиналом 4.7 кОм (подойдет выводной 0.25 Вт или SMD).
  • Выводы разъема ESP-01 (будем паять к ножкам со стороны дорожек).

:pushpin: Распиновка разъема ESP-01 на плате (Вид снизу / Со стороны пайки)

Переверните плату реле дорожками к себе. Найдите 8 контактов разъема, куда вставляется ESP-01. Ориентируйтесь по этой схеме:

    [Край платы реле]
      +-----------+

      |  RX   VCC | <-- 3.3V (Питание)
      | GPIO0 RST |
      | GPIO2 CH  | <-- GPIO2 (Данные датчика)
      |  TX   GND | <-- GND (Земля)
      +-----------+


:screwdriver: Пошаговый алгоритм доработки

  1. Подготовка резистора: Возьмите резистор 4.7 кОм. Если он выводной, укоротите ножки.
  2. Монтаж резистора подтяжки: Припаяйте резистор с обратной стороны платы прямо между контактами GPIO2 и VCC (3.3V) разъема ESP-01.
  3. Подключение датчика Dallas:
    • GND датчика (черный провод) припаяйте к контакту GND.
    • VDD датчика (красный провод) припаяйте к контакту VCC (туда же, где ножка резистора).
    • DQ / Data датчика (желтый/белый провод) припаяйте к контакту GPIO2 (туда же, где вторая ножка резистора).

:light_bulb: Совет: Чтобы провода датчика не оторвались от платы при использовании, зафиксируйте их каплей горячего клея или стяжкой у края модуля.


:warning: Важная проверка перед включением

Китайские модули реле для ESP-01 бывают двух популярных версий (v1.0 и v4.0). Проверьте, какая у вас:

  • Версия без оптопары (обычно синяя или старая черная): Работает напрямую. GPIO0 выдает LOW — реле включается. При включении ESP-01 реле может кратковременно щелкать.
  • Версия с оптопарой и контроллером (обычно v4.0 черная): На ней стоит дополнительный маленький чип (STC MCU). Он ждет от ESP-01 специальную шестнадцатеричную команду по серийному порту (0xA0 0x01 0x01 0xA2 для включения). Если у вас такая плата, управлять реле через простой digitalWrite(0, HIGH) не получится — нужно будет инициализировать Serial на скорости 9600.

Если вы уже знаете версию вашей платы (или на ней есть маленький 8-ногий чип помимо реле), скажите мне. Я могу сразу написать для вас готовый скетч для Arduino IDE или конфигурацию для ESPHome, которая учитывает особенности вашей платы и корректно считывает температуру с датчика.

И итогом можно не иметь центральной платы управления. Все можно просто допаять на модуль и прошивать. Они свяжутся между собой и будут управление.

Я, так-то, не против модульности (только на кой она нужна в единичном, конкретном изделии), да вот отзывов по этим модулям о неработоспособности очень много было раньше (лет 5 назад). Если уж делать отдельные модули, то на … барабанная дробь … на rs485 или CAN :grin:

Владыка 485 @WladDrakula , к тебе взываем. Тут модули хотят обмотать проводами, чтобы побольше было.

Чтобы потом не разбирать и все не переделывать. Без модульности, про… схемы и прошивки, и забывчивости создателя - это просто выкинут.

А по модульной схеме на esp01 relay модулями с датчиками всего то нужно сделать прошивку, где он будет передавать всем температуру и по датчику и по приходящей от других модулей температуре - выполнять действие, а именно - включать свое реле или выключать.

Даешь программирование таких гномиков!

Вот описание для esp01, где датчик подключается к GPIO 2, а реле GPIO 0.

В этой версии прошивки для Arduino IDE полностью переработана логика управления и сетевой обмен: алгоритм усреднения температур удален. Вместо него внедрена система логических имен и точечной обработки данных от конкретных устройств.

Теперь каждый модуль имеет свое Имя модуля (например, Kotelnaya, Gostinaya), а его физический термодатчик имеет Имя датчика (например, FloorTemp, AirTemp).

:gear: Изменение сетевого протокола (без JSON)

Поскольку формат JSON был исключен ранее для экономии памяти ESP-01 (1 МБ), а текстовые имена передавать необходимо, теперь данные упаковываются по UDP в виде компактной бинарной структуры фиксированной длины (40 байт):

  • Байты 0–15: Имя модуля (строка до 15 символов + нулевой байт)
  • Байты 16–31: Имя датчика (строка до 15 символов + нулевой байт)
  • Байты 32–35: Температура (тип float, 4 байта)
  • Байты 36–39: Текущее состояние реле (тип int, 4 байта)

:brain: Новая логика обработки и новые настройки в WEB

Теперь в веб-интерфейсе вместо абстрактных «весов» появились конкретные поля:

  1. Имя этого модуля — текстовый идентификатор устройства в сети (до 15 символов).
  2. Имя этого датчика — текстовый идентификатор установленного на нем датчика DS18B20.
  3. Режим логики контроля — выпадающий список из трех вариантов:
    • «Только свой датчик» — модуль работает автономно и управляет реле исключительно по показаниям локального DS18B20. При этом он продолжает транслировать свои данные в сеть для других участников.
    • «Любой из датчиков в сети» — реле сработает, если хотя бы один из активных датчиков в сети (включая свой собственный) потребует включения по уставке и гистерезису.
    • «Специфичный датчик» — модуль будет полностью игнорировать показания своего датчика (для реле) и переключит реле только на основании данных от конкретного удаленного датчика, имя которого вы введете в поле ниже.
  4. Имя целевого датчика контроля — поле ввода, которое становится активным при выборе режима «Специфичный датчик». Модуль ищет в UDP-эфире пакет, где совпадает имя датчика, и берет его температуру за основу.

:desktop_computer: Обновленная структура WEB-интерфейса

На главной странице (http://[IP_АДРЕС_МОДУЛЯ]) теперь отображаются:

  • Блок статуса: Отображает имя текущего модуля, имя локального датчика, его текущую температуру, а также имя и температуру того датчика, который в данный момент управляет локальным реле.
  • Сводная таблица сети: Динамический список всех обнаруженных в Wi-Fi сети устройств. Столбцы: IP-адрес | Имя модуля | Имя датчика | Температура (°C) | Статус реле.
  • Блок конфигурации: Все текстовые поля для настройки имен, выбора режимов работы, ввода уставки, гистерезиса и типа инверсии реле с кнопкой сохранения в энергонезависимую память LittleFS.

:shield: Алгоритм безопасности при потере связи

  • Если выбран режим «Специфичный датчик», а удаленный модуль с этим датчиком не присылал UDP-пакеты более 60 секунд (выключился, пропал Wi-Fi), модуль считает связь потерянной. Реле в целях безопасности автоматически выключается, а на веб-интерфейсе выводится предупреждение об аварии связи.
  • При физическом обрыве локального датчика (-127°C) реле также принудительно отключается, чтобы не допустить неконтролируемой работы исполнительного устройства.
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <ESP8266WebServer.h>
#include <LittleFS.h>
#include <OneWire.h>
#include <DallasTemperature.h>

// --- Настройки аппаратной части ---
#define RELAY_PIN 0    // GPIO0 управляет реле
#define ONE_WIRE_BUS 2 // GPIO2 вход датчика Dallas DS18B20
#define UDP_PORT 4210  // Порт для обмена данными в сети

// --- Структуры данных ---
// Структура сетевого пакета (ровно 40 байт, без JSON)
struct __attribute__((packed)) MeshPacket {
  char moduleName[16];
  char sensorName[16];
  float temperature;
  int relayState;
};

// Структура для хранения данных о соседних модулях в ОЗУ
struct RemoteDevice {
  IPAddress ip;
  char moduleName[16];
  char sensorName[16];
  float temperature;
  int relayState;
  unsigned long lastSeen;
  bool active;
};

#define MAX_DEVICES 20
RemoteDevice remoteDevices[MAX_DEVICES];

// Настройки, сохраняемые в LittleFS
struct Config {
  char wifi_ssid[32] = "";
  char wifi_pass[32] = "";
  char module_name[16] = "ESP_Node";
  char sensor_name[16] = "LocalTemp";
  char target_sensor[16] = "TargetTemp";
  int logic_mode = 0;   // 0: Только свой, 1: Любой в сети, 2: Специфичный датчик
  int relay_mode = 0;   // 0: Нагрев, 1: Охлаждение
  int relay_inv = 1;    // 0: Прямая логика (High=ВКЛ), 1: Инверсная (Low=ВКЛ)
  float target_temp = 25.0;
  float hysteresis = 0.5;
} config;

// --- Глобальные объекты ---
ESP8266WebServer server(80);
WiFiUDP udp;
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

float localTemperature = -127.0;
bool localSensorError = true;
unsigned long lastUdpSend = 0;
unsigned long lastSensorRead = 0;
int currentRelayState = 0;
bool networkAlert = false;

// --- Функции работы с памятью (LittleFS) ---
void loadConfig() {
  if (!LittleFS.begin()) return;
  if (LittleFS.exists("/config.bin")) {
    File configFile = LittleFS.open("/config.bin", "r");
    if (configFile) {
      configFile.read((uint8_t*)&config, sizeof(config));
      configFile.close();
    }
  }
}

void saveConfig() {
  File configFile = LittleFS.open("/config.bin", "w");
  if (configFile) {
    configFile.write((uint8_t*)&config, sizeof(config));
    configFile.close();
  }
}

// --- Управление физическим реле ---
void setRelay(int state) {
  currentRelayState = state;
  if (config.relay_inv == 1) {
    digitalWrite(RELAY_PIN, state == 1 ? LOW : HIGH);
  } else {
    digitalWrite(RELAY_PIN, state == 1 ? HIGH : LOW);
  }
}

// --- Обработка списка устройств ---
void updateRemoteDevice(IPAddress ip, MeshPacket packet) {
  int emptySlot = -1;
  for (int i = 0; i < MAX_DEVICES; i++) {
    if (remoteDevices[i].active && strcmp(remoteDevices[i].sensorName, packet.sensorName) == 0 && strcmp(remoteDevices[i].moduleName, packet.moduleName) == 0) {
      remoteDevices[i].temperature = packet.temperature;
      remoteDevices[i].relayState = packet.relayState;
      remoteDevices[i].lastSeen = millis();
      remoteDevices[i].ip = ip;
      return;
    }
    if (!remoteDevices[i].active && emptySlot == -1) {
      emptySlot = i;
    }
  }
  if (emptySlot != -1) {
    remoteDevices[emptySlot].active = true;
    remoteDevices[emptySlot].ip = ip;
    strncpy(remoteDevices[emptySlot].moduleName, packet.moduleName, 16);
    strncpy(remoteDevices[emptySlot].sensorName, packet.sensorName, 16);
    remoteDevices[emptySlot].temperature = packet.temperature;
    remoteDevices[emptySlot].relayState = packet.relayState;
    remoteDevices[emptySlot].lastSeen = millis();
  }
}

void checkDeadDevices() {
  unsigned long now = millis();
  for (int i = 0; i < MAX_DEVICES; i++) {
    if (remoteDevices[i].active && (now - remoteDevices[i].lastSeen > 60000)) {
      remoteDevices[i].active = false; // Удаляем устройство, если от него нет вестей 60 секунд
    }
  }
}

// --- Логика термостата ---
void processThermostatLogic() {
  if (localSensorError) {
    setRelay(0); // Аварийное отключение при поломке локального датчика
    return;
  }

  float controlTemp = localTemperature;
  bool validSensorFound = false;
  networkAlert = false;

  if (config.logic_mode == 0) { // Только свой датчик
    controlTemp = localTemperature;
    validSensorFound = true;
  }
  else if (config.logic_mode == 1) { // Любой из датчиков сети (включая свой)
    bool needAction = false;
    // Проверяем свой
    if (config.relay_mode == 0 && localTemperature < (config.target_temp - config.hysteresis)) needAction = true;
    if (config.relay_mode == 1 && localTemperature > (config.target_temp + config.hysteresis)) needAction = true;

    // Проверяем чужие
    for (int i = 0; i < MAX_DEVICES; i++) {
      if (remoteDevices[i].active && remoteDevices[i].temperature > -127.0) {
        if (config.relay_mode == 0 && remoteDevices[i].temperature < (config.target_temp - config.hysteresis)) needAction = true;
        if (config.relay_mode == 1 && remoteDevices[i].temperature > (config.target_temp + config.hysteresis)) needAction = true;
      }
    }

    // В децентрализованном режиме "любой" проверяем коридор отключения отдельно
    if (needAction) {
      setRelay(1);
    } else {
      // Отключаем, только если ВСЕ вышли из критической зоны перегрева/переохлаждения
      bool safeToTurnOff = true;
      if (config.relay_mode == 0 && localTemperature < (config.target_temp + config.hysteresis)) safeToTurnOff = false;
      if (config.relay_mode == 1 && localTemperature > (config.target_temp - config.hysteresis)) safeToTurnOff = false;

      for (int i = 0; i < MAX_DEVICES; i++) {
        if (remoteDevices[i].active && remoteDevices[i].temperature > -127.0) {
          if (config.relay_mode == 0 && remoteDevices[i].temperature < (config.target_temp + config.hysteresis)) safeToTurnOff = false;
          if (config.relay_mode == 1 && remoteDevices[i].temperature > (config.target_temp - config.hysteresis)) safeToTurnOff = false;
        }
      }
      if (safeToTurnOff) setRelay(0);
    }
    return;
  }
  else if (config.logic_mode == 2) { // Специфичный датчик
    for (int i = 0; i < MAX_DEVICES; i++) {
      if (remoteDevices[i].active && strcmp(remoteDevices[i].sensorName, config.target_sensor) == 0) {
        controlTemp = remoteDevices[i].temperature;
        validSensorFound = true;
        break;
      }
    }
    if (!validSensorFound) {
      networkAlert = true;
      setRelay(0); // Авария связи — выключаем нагрузку
      return;
    }
  }

  if (validSensorFound) {
    if (config.relay_mode == 0) { // Нагрев
      if (controlTemp < (config.target_temp - config.hysteresis)) setRelay(1);
      else if (controlTemp > (config.target_temp + config.hysteresis)) setRelay(0);
    } else { // Охлаждение
      if (controlTemp > (config.target_temp + config.hysteresis)) setRelay(1);
      else if (controlTemp < (config.target_temp - config.hysteresis)) setRelay(0);
    }
  }
}

// --- WEB-интерфейс (HTML генерация) ---
void handleRoot() {
  String html = "<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>ESP-01 Mesh Node</title>";
  html += "<style>body{font-family:sans-serif;margin:20px;background:#f0f2f5;color:#333;} .card{background:white;padding:20px;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1);margin-bottom:20px;} h2{margin-top:0;} table{width:100%;border-collapse:collapse;margin:15px 0;} th,td{border:1px solid #ddd;padding:8px;text-align:left;} th{background:#f8f9fa;} .btn{background:#007bff;color:white;border:0;padding:10px 15px;border-radius:4px;cursor:pointer;} .alert{color:red;font-weight:bold;}</style></head><body>";

  html += "<h1>Модуль: " + String(config.module_name) + "</h1>";

  // Карточка текущего состояния
  html += "<div class='card'><h2>Текущий статус</h2>";
  html += "<p>Локальный датчик (<b>" + String(config.sensor_name) + "</b>): " + (localSensorError ? "<span class='alert'>ОШИБКА ДАТЧИКА</span>" : String(localTemperature) + " &deg;C") + "</p>";
  html += "<p>Состояние реле: <b>" + String(currentRelayState == 1 ? "ВКЛЮЧЕНО" : "ВЫКЛЮЧЕНО") + "</b></p>";
  if (networkAlert) html += "<p class='alert'>ВНИМАНИЕ: Целевой датчик [" + String(config.target_sensor) + "] не найден в сети! Реле аварийно отключено.</p>";
  html += "</div>";

  // Таблица сети
  html += "<div class='card'><h2>Устройства в сети (UDP Mesh)</h2>";
  html += "<table><tr><th>IP Адрес</th><th>Имя модуля</th><th>Имя датчика</th><th>Температура</th><th>Реле</th></tr>";
  for (int i = 0; i < MAX_DEVICES; i++) {
    if (remoteDevices[i].active) {
      html += "<tr><td>" + remoteDevices[i].ip.toString() + "</td><td>" + String(remoteDevices[i].moduleName) + "</td><td>" + String(remoteDevices[i].sensorName) + "</td><td>" + String(remoteDevices[i].temperature) + " &deg;C</td><td>" + String(remoteDevices[i].relayState == 1 ? "ВКЛ" : "ВЫКЛ") + "</td></tr>";
    }
  }
  html += "</table></div>";

  // Форма настроек
  html += "<div class='card'><h2>Настройки</h2><form action='/save' method='POST'>";
  html += "<p>Wi-Fi SSID:<br><input type='text' name='ssid' value='" + String(config.wifi_ssid) + "' required></p>";
  html += "<p>Wi-Fi Пароль:<br><input type='password' name='pass' value='" + String(config.wifi_pass) + "'></p>";
  html += "<p>Имя этого модуля:<br><input type='text' name='m_name' maxlength='15' value='" + String(config.module_name) + "' required></p>";
  html += "<p>Имя локального датчика:<br><input type='text' name='s_name' maxlength='15' value='" + String(config.sensor_name) + "' required></p>";
  html += "Режим логики контроля:"; html += "<option value='0'" + String(config.logic_mode == 0 ? " selected" : "") + ">0: Только свой датчик"; html += "<option value='1'" + String(config.logic_mode == 1 ? " selected" : "") + ">1: Любой из датчиков в сети"; html += "<option value='2'" + String(config.logic_mode == 2 ? " selected" : "") + ">2: Специфичный датчик"; html += ""; html += "Имя целевого датчика контроля (для режима 2):"; html += "Целевая температура (°C):"; html += "Гистерезис (°C):"; html += "Режим работы:"; html += "<option value='0'" + String(config.relay_mode == 0 ? " selected" : "") + ">Нагрев"; html += "<option value='1'" + String(config.relay_mode == 1 ? " selected" : "") + ">Охлаждение"; html += ""; html += "Тип реле (Железо):"; html += "<option value='1'" + String(config.relay_inv == 1 ? " selected" : "") + ">Инверсное (Low = ВКЛ, типично для ESP-01)"; html += "<option value='0'" + String(config.relay_inv == 0 ? " selected" : "") + ">Прямое (High = ВКЛ)"; html += ""; html += ""; html += ""; server.send(200, "text/html", html);
} void handleSave() {
  if (server.method() == HTTP_POST) {
    strncpy(config.wifi_ssid, server.arg("ssid").c_str(), 32);
    strncpy(config.wifi_pass, server.arg("pass").c_str(), 32);
    strncpy(config.module_name, server.arg("m_name").c_str(), 16);
    strncpy(config.sensor_name, server.arg("s_name").c_str(), 16);
    strncpy(config.target_sensor, server.arg("t_sensor").c_str(), 16);
    config.logic_mode = server.arg("l_mode").toInt();
    config.relay_mode = server.arg("r_mode").toInt();
    config.relay_inv = server.arg("r_inv").toInt();
    config.target_temp = server.arg("t_temp").toFloat();
    config.hysteresis = server.arg("hyst").toFloat();
    saveConfig();
    server.send(200, "text/html", "Настройки сохранены! Модуль перезагружается...setTimeout(function(){window.location.href='/';},5000);");
    delay(2000);
    ESP.restart();
  }
}
// --- Главные циклы инициализации и работы ---
void setup() {
  // Настройка пина реле на выход в безопасном состоянии
  loadConfig();
  setRelay(0);
  pinMode(RELAY_PIN, OUTPUT);
  sensors.begin();
  // Пытаемся подключиться к Wi-Fi
  WiFi.mode(WIFI_STA);
  if (String(config.wifi_ssid).length() > 0) {
    WiFi.begin(config.wifi_ssid, config.wifi_pass);
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 30) {
      delay(500);
      attempts++;
    }
  }
  // Если не подключились — поднимаем конфигурационную точку доступа
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.mode(WIFI_AP);
    WiFi.softAP("ESP-01-Config-AP", "12345678");
  } else {
    udp.begin(UDP_PORT);
  }
  // Маршрутизация веб-сервера
  server.on("/", handleRoot);
  server.on("/save", handleSave);
  server.begin();
}
void loop() {
  server.handleClient(); 
  if (WiFi.status() == WL_CONNECTED) {
    unsigned long now = millis();
    // 1. Опрос физического датчика (раз в 5 секунд)
    if (now - lastSensorRead > 5000 || lastSensorRead == 0) {
      lastSensorRead = now;sensors.requestTemperatures();
      float temp = sensors.getTempCByIndex(0);
      if (temp == DEVICE_DISCONNECTED_C || temp < -50.0) {
          localSensorError = true;
          localTemperature = -127.0;
        } else {
          localSensorError = false;
          localTemperature = temp;
        }
        checkDeadDevices();
        processThermostatLogic();
     }
     // 2. Рассылка своей температуры по UDP Broadcast (раз в 10 секунд)
     if (now - lastUdpSend > 10000 || lastUdpSend == 0) {
      lastUdpSend = now;
      MeshPacket outPacket;
      memset(&outPacket, 0, sizeof(MeshPacket));
      strncpy(outPacket.moduleName, config.module_name, 15);
      strncpy(outPacket.sensorName, config.sensor_name, 15);
      outPacket.temperature = localTemperature;
      outPacket.relayState = currentRelayState;
      IPAddress broadcastIP = WiFi.localIP();
      broadcastIP[3] = 255;
      // Формируем широковещательный адрес подсети
      udp.beginPacket(broadcastIP, UDP_PORT);
      udp.write((uint8_t*)&outPacket, sizeof(MeshPacket));
      udp.endPacket();
     }
     // 3. Чтение входящих UDP пакетов от соседей
     int packetSize = udp.parsePacket();
     if (packetSize == sizeof(MeshPacket)) {
      MeshPacket inPacket;
      udp.read((uint8_t*)&inPacket, sizeof(MeshPacket));
      updateRemoteDevice(udp.remoteIP(), inPacket);processThermostatLogic(); 
      // Мгновенно реагируем на новые сетевые данные
     }}}
     }
    }
}

у меня вроде валяются где то с 1 и 2 реле, насколько помню для подключения к пинам пришлось припаять провод и соединить с реле)))
всего пинов там 2 ? кроме rx tx(кстате какое у них поведение ?) будет ли какое соединение плат вывод и их управление из 1го места ? и если да какой протокол, esp now например ? или другое, и памяти хватит ?))) а если датчик температуры еще подключить с библиотекой…

По итогу собираем 2 модуля. Настраиваем и проверяем. Далее наращиваем. На каждом этапе можно на отладочной плате настроить работу esp01, а потом воткнуть в модуль реле.

И еще. Схема не зависит от количества датчиков и реле. Каждый элемент может быть перестроен отдельно, без остаеовки работы всего сразу.

как вы уже смогли убедиться я лентяй, у которого возникли вопросы, и сомнения в том что вообще надо делать на них, но если они потянут все это тогда вроде как и можно пытаться так что то сделать… но как лентяй не думаю что в итоге будет успех, и проверять лень…)))

Diskless или вы уже все проверили и они все потянут ?

Вот специально объясняю тебе. Модуль снимает показание датчика, отправляет остальным, далее проверяет прием и нужное пока сохраняет в памяти. Затем приступает к анализу. Если надо, то включает или выключает реле.

Что конкретно не понятно?

спс

допустим их будет снимать esp32 и на ней будет логика

не понятно хватит ли памяти у гномика, что бы общаться с esp32 и на каком протоколе…
не понятно проверили ли вы уже основу кода, или только предполагаете в ближайшее время проверить…

так же хочу заметить что в этот раз я код смотрел! официально заявляю о лишних скобках, с ними код не компилируется! и жду косарь на адрики, потому что сегодня я работал! код смотрел, и не ленился!

так же я не могу найти у себя плату esp 01, подскажите что бы не качать новое ядро, указать generic esp8266 module можно ?

. Variables and constants in RAM (global, static), used 32940 / 80192 bytes (41%)
║ SEGMENT BYTES DESCRIPTION
╠══ DATA 1636 initialized variables
╠══ RODATA 4048 constants
╚══ BSS 27256 zeroed variables
. Instruction RAM (IRAM_ATTR, ICACHE_RAM_ATTR), used 59867 / 65536 bytes (91%) Troubles
║ SEGMENT BYTES DESCRIPTION
╠══ ICACHE 32768 reserved space for flash instruction cache
╚══ IRAM 27099 code in IRAM
. Code in flash (default, ICACHE_FLASH_ATTR), used 304784 / 1048576 bytes (29%)
║ SEGMENT BYTES DESCRIPTION
╚══ IROM 304784 code in flash

воо даже проверил сам! во скока это весит, да это вы лентяй похоже а не я…

вникаю дальше без загрузки, а смотрю на пк интерфейс… подскажите я правильно его воспроизвел ?)))
пытаюсь понять как если что устройствами управлять в ручном режиме…

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP-01 Mesh Thermostat</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #f0f2f5;
            padding: 20px;
            color: #333;
        }
        .container { max-width: 1200px; margin: 0 auto; }
        h1 {
            text-align: center;
            color: #1a1a2e;
            margin-bottom: 30px;
            font-size: 28px;
        }
        .card {
            background: white;
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 24px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .card h2 {
            color: #1a1a2e;
            font-size: 20px;
            margin-bottom: 16px;
            padding-bottom: 12px;
            border-bottom: 2px solid #e8ecf1;
        }
        .status-item {
            background: #f8f9fa;
            padding: 16px;
            border-radius: 8px;
            margin-bottom: 12px;
        }
        .status-item .label {
            font-size: 14px;
            color: #888;
            font-weight: 500;
        }
        .status-item .value {
            font-size: 22px;
            font-weight: bold;
            margin-top: 4px;
        }
        .value .temp { color: #e74c3c; }
        .value .relay-on { color: #27ae60; }
        .value .relay-off { color: #95a5a6; }
        .value .error { color: #e74c3c; font-size: 16px; }
        .alert {
            background: #fde8e8;
            border-left: 4px solid #e74c3c;
            padding: 12px 16px;
            border-radius: 4px;
            color: #c0392b;
            font-weight: 500;
            margin-top: 12px;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 12px;
        }
        table th {
            background: #f8f9fa;
            text-align: left;
            padding: 12px;
            font-size: 13px;
            font-weight: 600;
            color: #666;
            border-bottom: 2px solid #e8ecf1;
        }
        table td {
            padding: 12px;
            border-bottom: 1px solid #e8ecf1;
        }
        table tr:hover { background: #f8f9fa; }
        .badge {
            display: inline-block;
            padding: 4px 12px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: 600;
        }
        .badge-on { background: #d5f5e3; color: #27ae60; }
        .badge-off { background: #ebedef; color: #7f8c8d; }
        .empty-state { text-align: center; padding: 32px; color: #999; }
        .last-update {
            text-align: right;
            font-size: 12px;
            color: #999;
            margin-top: 12px;
        }
        .form-group {
            margin-bottom: 16px;
        }
        .form-group label {
            display: block;
            font-weight: 600;
            margin-bottom: 6px;
            font-size: 14px;
            color: #555;
        }
        .form-group input, .form-group select {
            width: 100%;
            padding: 10px 14px;
            border: 2px solid #dce0e6;
            border-radius: 6px;
            font-size: 14px;
            transition: border-color 0.3s;
            background: white;
        }
        .form-group input:focus, .form-group select:focus {
            outline: none;
            border-color: #3498db;
        }
        .form-row {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 16px;
        }
        .btn {
            background: #3498db;
            color: white;
            border: none;
            padding: 12px 32px;
            border-radius: 6px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: background 0.3s;
            width: 100%;
        }
        .btn:hover { background: #2980b9; }
        @media (max-width: 768px) {
            .form-row { grid-template-columns: 1fr; }
        }
        @media (max-width: 480px) {
            .status-item .value { font-size: 18px; }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🌡️ ESP-01 Mesh Thermostat</h1>

        <!-- Карточка статуса (ТОЧНО как в коде ESP) -->
        <div class="card" id="statusCard">
            <h2>📊 Текущий статус</h2>
            <div id="statusContent">
                <div class="status-item">
                    <div class="label">Модуль</div>
                    <div class="value" id="moduleName">Загрузка...</div>
                </div>
                <div class="status-item">
                    <div class="label">Локальный датчик</div>
                    <div class="value" id="localTemp">-- °C</div>
                </div>
                <div class="status-item">
                    <div class="label">Состояние реле</div>
                    <div class="value" id="relayState">--</div>
                </div>
            </div>
            <div id="alertContainer"></div>
            <div class="last-update" id="lastUpdate">Обновлено: --</div>
        </div>

        <!-- Таблица устройств (ТОЧНО как в коде ESP) -->
        <div class="card">
            <h2>🌐 Устройства в сети (UDP Mesh)</h2>
            <div style="overflow-x: auto;">
                <table>
                    <thead>
                        <tr>
                            <th>IP Адрес</th>
                            <th>Имя модуля</th>
                            <th>Имя датчика</th>
                            <th>Температура</th>
                            <th>Реле</th>
                        </tr>
                    </thead>
                    <tbody id="devicesTable">
                        <tr>
                            <td colspan="5" class="empty-state">Нет устройств в сети</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>

        <!-- Форма настроек (ТОЧНО как в коде ESP с исправленными полями) -->
        <div class="card">
            <h2>⚙️ Настройки</h2>
            <form id="settingsForm" action="/save" method="POST">
                <div class="form-group">
                    <label>Wi-Fi SSID</label>
                    <input type="text" name="ssid" id="ssid" placeholder="Введите имя WiFi" required>
                </div>

                <div class="form-group">
                    <label>Wi-Fi Пароль</label>
                    <input type="password" name="pass" id="pass" placeholder="Введите пароль WiFi">
                </div>

                <div class="form-group">
                    <label>Имя этого модуля</label>
                    <input type="text" name="m_name" id="m_name" maxlength="15" placeholder="ESP_Node" required>
                </div>

                <div class="form-group">
                    <label>Имя локального датчика</label>
                    <input type="text" name="s_name" id="s_name" maxlength="15" placeholder="LocalTemp" required>
                </div>

                <div class="form-group">
                    <label>Режим логики контроля</label>
                    <select name="l_mode" id="l_mode">
                        <option value="0">0: Только свой датчик</option>
                        <option value="1">1: Любой из датчиков в сети</option>
                        <option value="2">2: Специфичный датчик</option>
                    </select>
                </div>

                <div class="form-group">
                    <label>Имя целевого датчика контроля (для режима 2)</label>
                    <input type="text" name="t_sensor" id="t_sensor" maxlength="15" placeholder="TargetTemp">
                </div>

                <div class="form-group">
                    <label>Целевая температура (°C)</label>
                    <input type="number" step="0.1" name="t_temp" id="t_temp" value="25.0">
                </div>

                <div class="form-group">
                    <label>Гистерезис (°C)</label>
                    <input type="number" step="0.1" name="hyst" id="hyst" value="0.5">
                </div>

                <div class="form-group">
                    <label>Режим работы</label>
                    <select name="r_mode" id="r_mode">
                        <option value="0">Нагрев</option>
                        <option value="1">Охлаждение</option>
                    </select>
                </div>

                <div class="form-group">
                    <label>Тип реле (Железо)</label>
                    <select name="r_inv" id="r_inv">
                        <option value="1">Инверсное (Low = ВКЛ, типично для ESP-01)</option>
                        <option value="0">Прямое (High = ВКЛ)</option>
                    </select>
                </div>

                <button type="submit" class="btn">💾 Сохранить</button>
            </form>
        </div>
    </div>

    <script>
        // --- КОНФИГУРАЦИЯ ---
        // ЗДЕСЬ УКАЖИТЕ IP ВАШЕГО ESP-01!
        const ESP_IP = '192.168.1.100'; // <-- ИЗМЕНИТЕ НА ВАШ IP!
        const API_URL = `http://${ESP_IP}`;

        // --- ФУНКЦИЯ ПОЛУЧЕНИЯ ДАННЫХ С ESP ---
        async function fetchStatus() {
            try {
                const response = await fetch(API_URL + '/', {
                    mode: 'cors',
                    headers: { 'Accept': 'text/html' }
                });

                if (!response.ok) throw new Error('Network response was not ok');

                const html = await response.text();
                parseHTML(html);
                document.getElementById('lastUpdate').textContent = 'Обновлено: ' + new Date().toLocaleTimeString();

            } catch (error) {
                console.error('Error fetching status:', error);
                document.getElementById('moduleName').textContent = '⚠️ Ошибка подключения к ESP';
                document.getElementById('moduleName').style.color = '#e74c3c';
            }
        }

        // --- ПАРСИНГ HTML С ESP (ТОЧНО ПО ВАШЕМУ КОДУ) ---
        function parseHTML(html) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            // 1. Имя модуля (из <h1>)
            const h1 = doc.querySelector('h1');
            if (h1) {
                const match = h1.textContent.match(/Модуль:\s*(.+)/);
                if (match) {
                    document.getElementById('moduleName').textContent = match[1];
                    document.getElementById('moduleName').style.color = '#333';
                }
            }

            // 2. Локальный датчик (из первого <p> в карточке)
            const paragraphs = doc.querySelectorAll('.card p');
            if (paragraphs.length > 0) {
                // Ищем "Локальный датчик"
                for (let p of paragraphs) {
                    const text = p.textContent;
                    if (text.includes('Локальный датчик')) {
                        const match = text.match(/\(([^)]+)\):\s*([\d.]+|ОШИБКА ДАТЧИКА)/);
                        if (match) {
                            const sensorName = match[1];
                            const tempValue = match[2];
                            const display = document.getElementById('localTemp');
                            if (tempValue === 'ОШИБКА ДАТЧИКА') {
                                display.textContent = '❌ ОШИБКА ДАТЧИКА';
                                display.className = 'error';
                            } else {
                                display.textContent = tempValue + ' °C';
                                display.className = 'temp';
                            }
                        }
                        break;
                    }
                }

                // 3. Состояние реле (из второго <p>)
                for (let p of paragraphs) {
                    const text = p.textContent;
                    if (text.includes('Состояние реле') || text.includes('Реле')) {
                        const match = text.match(/Реле:\s*(\S+)/);
                        if (match) {
                            const relayState = match[1];
                            const display = document.getElementById('relayState');
                            if (relayState === 'ВКЛЮЧЕНО') {
                                display.textContent = '🔴 ВКЛЮЧЕНО';
                                display.className = 'relay-on';
                            } else {
                                display.textContent = '⚪ ВЫКЛЮЧЕНО';
                                display.className = 'relay-off';
                            }
                        }
                        break;
                    }
                }
            }

            // 4. Предупреждения (class="alert")
            const alertElement = doc.querySelector('.alert');
            const alertContainer = document.getElementById('alertContainer');
            if (alertElement) {
                alertContainer.innerHTML = `<div class="alert">⚠️ ${alertElement.textContent}</div>`;
            } else {
                alertContainer.innerHTML = '';
            }

            // 5. Таблица устройств
            const table = doc.querySelector('table');
            const tbody = document.getElementById('devicesTable');
            if (table) {
                const rows = table.querySelectorAll('tr');
                let htmlRows = '';
                let deviceCount = 0;

                rows.forEach((row, index) => {
                    if (index === 0) return; // Пропускаем заголовок
                    const cells = row.querySelectorAll('td');
                    if (cells.length >= 5) {
                        deviceCount++;
                        const ip = cells[0].textContent.trim();
                        const moduleName = cells[1].textContent.trim();
                        const sensorName = cells[2].textContent.trim();
                        const temp = cells[3].textContent.trim();
                        const relay = cells[4].textContent.trim();

                        const relayBadge = relay === 'ВКЛ' ?
                            '<span class="badge badge-on">ВКЛ</span>' :
                            '<span class="badge badge-off">ВЫКЛ</span>';

                        htmlRows += `
                            <tr>
                                <td>${ip}</td>
                                <td><strong>${moduleName}</strong></td>
                                <td>${sensorName}</td>
                                <td>${temp}</td>
                                <td>${relayBadge}</td>
                            </tr>
                        `;
                    }
                });

                if (deviceCount > 0) {
                    tbody.innerHTML = htmlRows;
                } else {
                    tbody.innerHTML = `<tr><td colspan="5" class="empty-state">Нет устройств в сети</td></tr>`;
                }
            }
        }

        // --- ЗАГРУЗКА ТЕКУЩИХ НАСТРОЕК ---
        async function loadSettings() {
            try {
                const response = await fetch(API_URL + '/', {
                    mode: 'cors',
                    headers: { 'Accept': 'text/html' }
                });
                const html = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');

                // Заполняем поля формы из ESP
                const form = doc.querySelector('form');
                if (form) {
                    const inputs = form.querySelectorAll('input, select');
                    inputs.forEach(input => {
                        const name = input.getAttribute('name');
                        const value = input.value;
                        const element = document.getElementById(name);
                        if (element) {
                            if (element.tagName === 'SELECT') {
                                element.value = value;
                            } else {
                                element.value = value;
                            }
                        }
                    });
                }

            } catch (error) {
                console.error('Error loading settings:', error);
            }
        }

        // --- ОТПРАВКА НАСТРОЕК ---
        document.getElementById('settingsForm').addEventListener('submit', async function(e) {
            e.preventDefault();

            const formData = new FormData(this);
            const params = new URLSearchParams();

            for (let [key, value] of formData.entries()) {
                params.append(key, value);
            }

            try {
                const response = await fetch(API_URL + '/save?' + params.toString(), {
                    method: 'POST',
                    mode: 'cors'
                });

                const result = await response.text();
                alert('Настройки сохранены! Модуль перезагружается...');
                setTimeout(() => {
                    location.reload();
                }, 5000);

            } catch (error) {
                console.error('Error saving settings:', error);
                alert('Ошибка сохранения настроек! Проверьте подключение к ESP.');
            }
        });

        // --- АВТООБНОВЛЕНИЕ ---
        setInterval(fetchStatus, 10000);

        // --- ЗАПУСК ---
        fetchStatus();
        loadSettings();

        console.log('🌡️ ESP-01 Mesh Thermostat Monitor запущен!');
        console.log(`📡 Подключено к: ${API_URL}`);
        console.log('🔄 Автообновление каждые 10 секунд');
    </script>
</body>
</html>

в целом не могу понять это хорошая основа или нет))) с одной стороны обмен данными есть, аж 20 устройств…
логику можно доделать для каждого модуля… и корректировку через сеть…

  • поменяйте мне лампочку в фаре авто

  • да тебе надо масло и охлаждайку менять…

и давя и давят и давят.

Тут IP адрес - всего лишь информация для подключения к модулю и настройка. Завтра попробую набросать логику обработки действий. Все модули будут знать друг про друга, поскольку при запросе есть src адрес, а по имени в пакете все прекрасно можно разрулить. И MAC тоже можно найти. По сути прошивка для всех модулей будет одинакова. Поэтому это удобно потом менять и наращивать.

Тут прям просится esp32forh, но мы же взяли esp01. Если применять esp32c3, то можно его вшить, а потом писать прямо в модуле программу.

Вау!

Шьешь forth, а потом по месту закидываешь все. И менять потом можно.

а вдруг у нас система умного дома выйдет с 0 офигенная ?

нужно наверное ручное управление еще… перевести все на локальную версию, и может другой протокол…

я пока что нашел всего 1 датчик температуры)))
отыскал 5 кабелей, и только 1 определился на вин 10 для такого программатора https://aliexpress.ru/item/1005004876091486.html при чем самый старый usb вроде, новые кабеля упорно не хотят с ним работать)))

Вчера, наконец, удалось добиться передачи данных между микроконтроллерами. Загруженные скетчи
для nano:

Спойлер
/*
   ПРОГРАММА ДЛЯ КОНТРОЛЛЕРА NANO
*/

// ===================================================
// 1. ПОДКЛЮЧАЕМ БИБЛИОТЕКИ
// ===================================================
#include <OneWire.h>
OneWire ds(2);                    // Пин для датчиков DS18B20
#include "microDS3231.h"
MicroDS3231 rtc;                  // Часы реального времени

// ===================================================
// 2. ПИНЫ ДЛЯ СЧЁТЧИКОВ
// ===================================================
#define pinVoda_tick   4          // Импульсный вход расходомера воды
#define pinElectr_tick 5          // Импульсный вход электросчётчика

// ===================================================
// 3. АДРЕСА ДАТЧИКОВ ТЕМПЕРАТУРЫ DS18B20
// ===================================================
byte ADDR_DS18B20 [10][8] = {
  {0x28, 0xFF, 0x94, 0xAE, 0xA0, 0x16, 0x03, 0x0E}, // 0 - Воздух в комнате
  {0x28, 0xFF, 0x48, 0x83, 0xA0, 0x16, 0x04, 0x1F}, // 1 - Вода на выходе из гидроразделителя
  {0x28, 0xFF, 0xAA, 0xAB, 0xA0, 0x16, 0x05, 0x74}, // 2 - Вода на выходе из бака-аккумулятора
  {0x28, 0xFF, 0xEC, 0x98, 0xA0, 0x16, 0x04, 0x9F}, // 3 - Воздух на улице
  {0x28, 0xD5, 0x97, 0x76, 0x0, 0x0, 0x0, 0x76},    // 4 - Фреон на выходе из компрессора
  {0x28, 0x4F, 0x9C, 0x73, 0x0, 0x0, 0x0, 0xF9},    // 5 - Вода на входе в теплообменник
  {0x28, 0xFC, 0xD1, 0x72, 0x0, 0x0, 0x0, 0x2D},    // 6 - Вода на выходе из теплообменника
  {0x28, 0x95, 0x28, 0x75, 0x0, 0x0, 0x0, 0xAB},    // 7 - Фреон на входе в компрессор
  {0x28, 0xEF, 0x8C, 0x76, 0x0, 0x0, 0x0, 0x58},    // 8 - Нижняя часть теплообменника
  {0x28, 0x8C, 0x3A, 0xB4, 0xE, 0x0, 0x0, 0xDB}     // 9 - Фреон в начале испарителя
};

// ===================================================
// 4. ОСНОВНЫЕ ПЕРЕМЕННЫЕ
// ===================================================
int16_t Temper[10] = {0};                 // Массив температур (x10)
uint16_t output_to_relays = 0;            // Битовая маска реле
uint8_t PWM = 0;                          // ШИМ для вентилятора (0-255)
uint8_t WaterPresure = 1;                 // Давление: 1=норма, 2=макс, 3=мин
boolean gettingTempComplete = false;      // Флаг готовности температур
uint8_t mistakesFrom_nano = 0;                   // Код ошибки, битовая маска
//бит 0 - ошибка CRC при передаче данных от Mega к nano, менее нескольких раз подряд
//бит 1 - ошибка CRC при передаче данных от Mega к nano, более нескольких раз подряд
//бит 2 - ошибка потери связи с МК Mega, менее нескольких раз подряд
//бит 3 - ошибка потери связи с МК Mega, более нескольких раз подряд
uint8_t mistakesFrom_nano_0_Count;//счётчик ошибки CRC при передаче данных от Mega к nano (до нескольких раз)
uint8_t mistakesFrom_nano_2_Count;//счётчик ошибки потери связи с МК Mega (до нескольких раз)


// ===================================================
// 5. КОНТРОЛЬ СВЯЗИ С MEGA
// ===================================================
uint32_t lastRxTime = 0;                  // Время последнего приёма



// ===================================================
// 6. ПЕРЕМЕННЫЕ ДЛЯ РАСХОДОМЕРА ВОДЫ
// ===================================================
boolean stateCheck_voda;
uint32_t tmr, WaterStartTimer = 0, WaterCountTimer;
boolean gWaterFlag = false, FirstImpulsWater = false, SecondImpulsWater = false;
uint32_t gWater = 0;                      // Расход воды (м3/ч)/100

// ===================================================
// 7. ПЕРЕМЕННЫЕ ДЛЯ ЭЛЕКТРОСЧЁТЧИКА
// ===================================================
boolean stateCheck_electr;
uint32_t tmr2, ElectrStartTimer = 0, ElectrCountTimer;
boolean ElectrFlag = false;
uint32_t wElectr = 0;                     // Мощность (кВт)/100

// ===================================================
// 8. УПРАВЛЕНИЕ ПРИВОДОМ КЛАПАНА
// ===================================================
uint32_t TimerStartPrivodUp = 0;
uint32_t TimerStartPrivodDown = 0;
uint32_t timePrivodUp = 0;
uint32_t timePrivodDown = 0;


// ===================================================
// 9. СТРУКТУРЫ ДАННЫХ ДЛЯ ОБМЕНА С MEGA
// ===================================================

// От NANO к MEGA (все данные + состояние реле)
struct DataToMega {
  int16_t t1, t2, t3, t4, t5, t6, t7, t8, t9, t10;
  uint8_t P;
  uint8_t minutes, hours, days, months;
  uint8_t Water;
  uint8_t Electr;
  uint16_t relaysState;
  uint8_t mistakesFrom_nano;
  uint8_t crc;
} dataToMega;

// От MEGA к NANO (только параметры)
struct DataFromMega {
  uint16_t output_from_Mega_to_nano;
  uint8_t actuatorMoveTime;
  uint8_t crc;
} dataFromMega;

// ===================================================
// 10. ФУНКЦИЯ CRC-8
// ===================================================
uint8_t crc8(const void* data, size_t length, uint8_t crc = 0) {
  const uint8_t* p = (const uint8_t*)data;
  while (length--) {
    uint8_t b = *p++, j = 8;
    while (j--) {
      crc = ((crc ^ b) & 1) ? (crc >> 1) ^ 0x8C : (crc >> 1);
      b >>= 1;
    }
  }
  return crc;
}

// ===================================================
// 11. ФУНКЦИИ ДЛЯ РАБОТЫ С ДАТЧИКАМИ ТЕМПЕРАТУРЫ
// ===================================================
bool readDS18B20(byte *addr, int16_t *dest) {
  uint8_t data[9];
  ds.reset();
  ds.select(addr);
  ds.write(0xBE);
  for (uint8_t i = 0; i < 9; i++) data[i] = ds.read();
  if (crc8(data, 8) != data[8]) return false;
  int16_t raw = (int16_t)((data[1] << 8) | data[0]);
  *dest = (raw * 5) / 8;
  return true;
}

void resetTemp() {
  ds.reset();
  ds.write(0xCC);
  ds.write(0x44);
}

void gettingTemp() {
  int16_t temp;
  for (uint8_t i = 0; i < 10; i++) {
    if (readDS18B20(ADDR_DS18B20[i], &temp)) {
      Temper[i] = temp;
    } else {
      Temper[i] = 22222;  // Ошибка датчика
    }
  }
  gettingTempComplete = true;
}

// ===================================================
// 12. ОБРАБОТЧИК ИМПУЛЬСОВ РАСХОДОМЕРА ВОДЫ
// ===================================================
void voda_tick() {
  boolean current_status = digitalRead(pinVoda_tick);
  if (stateCheck_voda && !current_status) {
    tmr = millis();
    gWaterFlag = true;
    stateCheck_voda = current_status;
    return;
  }
  if (!stateCheck_voda && current_status) {
    stateCheck_voda = current_status;
    return;
  }
  if (current_status) {
    gWaterFlag = false;
    return;
  }
  if (!current_status && (millis() - tmr >= 1000) && gWaterFlag == true) {
    if (WaterStartTimer == 0) {
      WaterStartTimer = millis();
      gWaterFlag = false;
      return;
    }
    WaterCountTimer = millis() - WaterStartTimer;
    WaterStartTimer = millis();
    gWaterFlag = false;
    if (FirstImpulsWater == true) SecondImpulsWater = true;
    FirstImpulsWater = true;
  }
}

// ===================================================
// 13. ОБРАБОТЧИК ИМПУЛЬСОВ ЭЛЕКТРОСЧЁТЧИКА
// ===================================================
void electr_tick() {
  boolean current_status = digitalRead(pinElectr_tick);
  if (stateCheck_electr && !current_status) {
    tmr2 = millis();
    ElectrFlag = true;
    stateCheck_electr = current_status;
    return;
  }
  if (!stateCheck_electr && current_status) {
    stateCheck_electr = current_status;
    return;
  }
  if (current_status) {
    ElectrFlag = false;
    return;
  }
  if (!current_status && (millis() - tmr2 >= 50) && ElectrFlag == true) {
    if (ElectrStartTimer == 0) {
      ElectrStartTimer = millis();
      ElectrFlag = false;
      return;
    }
    ElectrCountTimer = millis() - ElectrStartTimer;
    ElectrStartTimer = millis();
    ElectrFlag = false;
  }
}

// ===================================================
// 14. ПРИЁМ ПАРАМЕТРОВ ОТ MEGA (БИНАРНЫЙ)
// ===================================================
void receiveFromMega() {
  if (Serial.available()) {
    DataFromMega rx;
    if (Serial.readBytes((uint8_t*)&rx, sizeof(rx))) {
      uint8_t crc = crc8(&rx, sizeof(rx));
      if (crc == 0) {
        output_to_relays = rx.output_from_Mega_to_nano;
        if (bitRead(output_to_relays, 7) == HIGH)
          timePrivodDown = rx.actuatorMoveTime * 100;
        else if (bitRead(output_to_relays, 8) == HIGH)
          timePrivodUp = rx.actuatorMoveTime * 100;
        else
        {
          timePrivodDown = 0;
          timePrivodUp = 0;
        }
        bitClear(mistakesFrom_nano, 0);
        mistakesFrom_nano_0_Count = 0;
      } else {
        bitSet(mistakesFrom_nano, 0);
        mistakesFrom_nano_0_Count++;
        if (mistakesFrom_nano_0_Count > 3)
        {
          bitSet(mistakesFrom_nano, 1);
          output_to_relays = 0;//аварийный останов
        }
      }
      lastRxTime = millis();
      bitClear(mistakesFrom_nano, 2);
      mistakesFrom_nano_2_Count = 0;
    }
  }

  // ====== ПРОВЕРКА ПОТЕРИ СВЯЗИ ======
  uint32_t timeSinceLastRx = millis() - lastRxTime;
  if (timeSinceLastRx >= 15000) {
    bitSet(mistakesFrom_nano, 2);
    mistakesFrom_nano_2_Count++;
    lastRxTime = millis();//чтобы прошло ещё время, прежде чем увеличится счётчик mistakesFrom_nano_2_Count
    if (mistakesFrom_nano_2_Count > 3)
    {
      bitSet(mistakesFrom_nano, 3);
      output_to_relays = 0;//аварийный останов
    }
  }
}


// ===================================================
// 15. УПРАВЛЕНИЕ ПРИВОДОМ РЕГУЛИРУЮЩЕГО КЛАПАНА
// ===================================================
void actuatorMove() {
  // Закрывание (бит 7)
  if (bitRead(output_to_relays, 7) == HIGH && digitalRead(13) == HIGH) {
    TimerStartPrivodDown = millis();
    digitalWrite(13, LOW);
  }
  if (digitalRead(13) == LOW && millis() - TimerStartPrivodDown >= timePrivodDown) {
    digitalWrite(13, HIGH);
    bitClear(output_to_relays, 7);
  }

  // Открывание (бит 8)
  if (bitRead(output_to_relays, 8) == HIGH && digitalRead(12) == HIGH) {
    TimerStartPrivodUp = millis();
    digitalWrite(12, LOW);
  }
  if (digitalRead(12) == LOW && millis() - TimerStartPrivodUp >= timePrivodUp) {
    digitalWrite(12, HIGH);
    bitClear(output_to_relays, 8);
  }
}


// ===================================================
// 16. ОТПРАВКА ДАННЫХ НА MEGA
// ===================================================
void sendToMega() {
  dataToMega.t1 = Temper[0];
  dataToMega.t2 = Temper[1];
  dataToMega.t3 = Temper[2];
  dataToMega.t4 = Temper[3];
  dataToMega.t5 = Temper[4];
  dataToMega.t6 = Temper[5];
  dataToMega.t7 = Temper[6];
  dataToMega.t8 = Temper[7];
  dataToMega.t9 = Temper[8];
  dataToMega.t10 = Temper[9];
  dataToMega.P = WaterPresure;
  dataToMega.minutes = rtc.getMinutes();
  dataToMega.hours = rtc.getHours();
  dataToMega.days = rtc.getDate();
  dataToMega.months = rtc.getMonth();
  dataToMega.Water = (uint8_t)gWater;
  dataToMega.Electr = (uint8_t)wElectr;
  dataToMega.relaysState = output_to_relays;
  dataToMega.mistakesFrom_nano = mistakesFrom_nano;

  uint8_t crc = crc8(&dataToMega, sizeof(dataToMega) - 1);
  dataToMega.crc = crc;

  Serial.write((uint8_t*)&dataToMega, sizeof(dataToMega));
}

// ===================================================
// 17. УПРАВЛЕНИЕ РЕЛЕ (физические выходы)
// ===================================================
void setRelays() {
  // 0 - Дизельный котёл
  digitalWrite(9, bitRead(output_to_relays, 0) ? LOW : HIGH);
  // 1 - ТЭН 2.5 кВт
  digitalWrite(8, bitRead(output_to_relays, 1) ? LOW : HIGH);
  // 2 - ТЭН 7.5 кВт
  digitalWrite(7, bitRead(output_to_relays, 2) ? LOW : HIGH);
  // 3 - Шаровой кран
  digitalWrite(6, bitRead(output_to_relays, 3) ? LOW : HIGH);
  // 4 - Насос радиаторов
  digitalWrite(10, bitRead(output_to_relays, 4) ? LOW : HIGH);
  // 5 - Насос тёплых полов
  digitalWrite(A3, bitRead(output_to_relays, 5) ? LOW : HIGH);
  // 6 - Вентилятор
  digitalWrite(11, bitRead(output_to_relays, 6) ? LOW : HIGH);
  // 7 - Закрывание клапана
  digitalWrite(13, bitRead(output_to_relays, 7) ? LOW : HIGH);
  // 8 - Открывание клапана
  digitalWrite(12, bitRead(output_to_relays, 8) ? LOW : HIGH);
  // 9 - Компрессор
  digitalWrite(A2, bitRead(output_to_relays, 9) ? LOW : HIGH);
  // 10 - Четырёхходовой клапан
  digitalWrite(A1, bitRead(output_to_relays, 10) ? LOW : HIGH);
  // 11 - Насос теплообменника
  digitalWrite(A0, bitRead(output_to_relays, 11) ? LOW : HIGH);

  // ШИМ для вентилятора
  analogWrite(3, PWM);
}

// ===================================================
// 18. НАЧАЛЬНАЯ НАСТРОЙКА
// ===================================================
void setup() {
  Serial.begin(9600);

  // === Настройка пинов ===
  pinMode(4, INPUT_PULLUP);
  pinMode(5, INPUT_PULLUP);

  // Выходы реле (HIGH = выключено, LOW = включено)
  pinMode(6, OUTPUT); digitalWrite(6, HIGH);
  pinMode(7, OUTPUT); digitalWrite(7, HIGH);
  pinMode(8, OUTPUT); digitalWrite(8, HIGH);
  pinMode(9, OUTPUT); digitalWrite(9, HIGH);
  pinMode(10, OUTPUT); digitalWrite(10, HIGH);
  pinMode(11, OUTPUT); digitalWrite(11, HIGH);
  pinMode(12, OUTPUT); digitalWrite(12, HIGH);
  pinMode(13, OUTPUT); digitalWrite(13, HIGH);
  pinMode(A0, OUTPUT); digitalWrite(A0, HIGH);
  pinMode(A1, OUTPUT); digitalWrite(A1, HIGH);
  pinMode(A2, OUTPUT); digitalWrite(A2, HIGH);
  pinMode(A3, OUTPUT); digitalWrite(A3, HIGH);

  // === Часы реального времени ===
  if (rtc.lostPower()) rtc.setTime(COMPILE_TIME);

  delay(1000);
}


// ===================================================
// 19. ГЛАВНЫЙ ЦИКЛ
// ===================================================
void loop() {
  // === Обработчики импульсов ===
  voda_tick();
  electr_tick();

  // === Приём данных от Mega ===
  receiveFromMega();

  // === Управление клапаном ===
  actuatorMove();

  // === Цикл 10 секунд ===
  static uint32_t timer = 0;
  static uint8_t step = 0;

  // Шаг 1: Запуск измерения температур (2 сек)
  if (millis() - timer >= 2000 && step < 1) {
    resetTemp();
    step = 1;
  }

  // Шаг 2: Чтение температур (4 сек)
  if (millis() - timer >= 4000 && step < 2) {
    gettingTemp();
    step = 2;
  }

  // Шаг 3: Управление (6 сек)
  if (millis() - timer >= 6000 && step < 3) {
    // === Давление ===
    if (analogRead(A6) < 307) WaterPresure = 2;
    else if (analogRead(A7) < 307) WaterPresure = 3;
    else WaterPresure = 1;

    // === Расход воды ===
    if (SecondImpulsWater == false) gWater = 10;
    else gWater = round(3600000 / WaterCountTimer);

    // === Мощность ===
    if (ElectrCountTimer == 0 || (millis() - ElectrStartTimer > 60000))
      wElectr = 0;
    else wElectr = 72000 / ElectrCountTimer;

    step = 3;
  }

  // Шаг 4: Управление реле (8 сек)
  if (millis() - timer >= 8000 && step < 4) {
    setRelays();
    step = 4;
  }

  // Шаг 5: Отправка данных на Mega (10 сек)
  if (millis() - timer >= 10000 && step < 5) {
    if (gettingTempComplete) {
      sendToMega();
      gettingTempComplete = false;
      step = 5;
    }
    timer = millis();
    step = 0;
  }
}

для Mega:

Спойлер
/*
   ПРОГРАММА ДЛЯ КОНТРОЛЛЕРА MEGA
*/

#include <MCUFRIEND_kbv.h>
MCUFRIEND_kbv tft;

// ===================================================
// 1. НАСТРОЙКИ ДИСПЛЕЯ
// ===================================================
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RESET A4

// ===================================================
// 2. ЦВЕТА
// ===================================================
#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF
#define DARKGREY  0x7BEF
#define DARKGREEN 0x0C80


// ===================================================
// 3. ПЕРЕМЕННЫЕ
// ===================================================
uint8_t kindDisplay = 1;
uint8_t kindDisplayPrev = 1;
uint16_t mistakes = 0;
//бит 0 - ошибка CRC при передаче данных от Mega к nano, менее нескольких раз подряд
//бит 1 - ошибка CRC при передаче данных от Mega к nano, более нескольких раз подряд
//бит 2 - ошибка потери связи с МК Mega, менее нескольких раз подряд
//бит 3 - ошибка потери связи с МК Mega, более нескольких раз подряд


//бит 15 - ошибка CRC при передаче данных от nano к Mega, менее нескольких раз подряд
//бит 14 - ошибка CRC при передаче данных от nano к Mega, ,более нескольких раз подряд
//бит 13 - ошибка потери связи с МК nano, менее нескольких раз подряд
//бит 12 - ошибка потери связи с МК nano, более нескольких раз подряд
uint8_t mistake_15_Count;//счётчик количества ошибок подряд, mistakes, бит 15
uint8_t mistake_13_Count;//счётчик количества ошибок подряд, mistakes, бит 13

uint16_t output_from_Mega_to_nano = 0;
//0 - дизельный котёл и насос дизельного котла
//1 - ТЭН 2,5 кВт
//2 - ТЭН 7,5 кВт
//3 - шаровой кран
//4 - насос радиаторов
//5 - насос тёплых полов
//6 - вентилятор наружного блока
//7 - закрывание клапана
//8 - открывание клапана
//9 - компрессор
//10 - четырёхходовой клапан
//11 - насос пластинчатого теплообменника
uint8_t actuatorMoveTime;//время в течении которого перемещается клапан (выражено в 0, 1 сек.: 10 = 1 сек.)
uint32_t actuatorTimer;//для выдержки времени между передачей команды клапану
boolean gettingFrom_nano;//флаг о получении данных от nano


//-------------- проверка работы реле ---------------
uint8_t cycleCount = 0;
//-------------- (проверка работы реле) ---------------


// Данные от nano
struct DataFromNano {
  int16_t t1, t2, t3, t4, t5, t6, t7, t8, t9, t10;
  uint8_t P;
  uint8_t minutes, hours, days, months;
  uint8_t Water;
  uint8_t Electr;
  uint16_t relaysState;
  uint8_t mistakesFrom_nano;
  uint8_t crc;
} rxData;


// Данные от UNO
struct DataFromUNO {
  uint8_t Display;
} rxDataFromUNO;


// Массивы для графиков
uint8_t t1_Graph[320], t2_Graph[320], t3_Graph[320], t4_Graph[320];
uint8_t t5_Graph[320], t6_Graph[320], t7_Graph[320], t8_Graph[320];
uint8_t t9_Graph[320], t10_Graph[320];
uint8_t Water_Graph[320], Electr_Graph[320];


// ===================================================
// 4. CRC
// ===================================================
uint8_t crc8(const void* data, size_t length, uint8_t crc = 0) {
  const uint8_t* p = (const uint8_t*)data;
  while (length--) {
    uint8_t b = *p++, j = 8;
    while (j--) {
      crc = ((crc ^ b) & 1) ? (crc >> 1) ^ 0x8C : (crc >> 1);
      b >>= 1;
    }
  }
  return crc;
}

// ===================================================
// 5. ПРЕОБРАЗОВАНИЕ ДЛЯ ГРАФИКОВ
// ===================================================
int16_t getDataForUintMassiv(int16_t a) {
  if (a < -400) return 0;
  else if (a >= -400 && a < 500) return round((a + 400) / 5);
  else if (a >= 500 && a < 1250) return round(180 + (a - 500) / 10);
  else return 255;
}

uint8_t getDataForUintMassiv8(uint8_t a) {
  if (a <= 160) return round(40 + a);
  else if (a < 255) return round(200 + (a - 160) / 10);
  else return 255;
}

// ===================================================
// 6. ОТРИСОВКА ГРАФИКОВ
// ===================================================
void getGraphics(uint16_t c, uint16_t d, uint8_t g[320]) {
  for (int16_t x = 1; x < 320; x++) {
    tft.drawLine(x - 1, (c - g[x - 1]), x, (c - g[x]), d);
  }
}

// ===================================================
// 7. ОСНОВНОЙ ЭКРАН
// ===================================================
void getMainDisplay() {
  tft.fillScreen(BLACK);

  // Вертикальные линии
  for (uint8_t i = 1; i < 8; i++) {
    tft.drawFastVLine((40 * i), 20, 190, DARKGREY);
    tft.drawFastVLine((40 * i), 230, 210, DARKGREY);
  }

  // Горизонтальные линии (температуры)
  for (uint8_t i = 0; i < 11; i++) {
    if (i == 4 || i == 5 || i == 6 || i == 7 || i == 9 || i == 10)
      tft.drawFastHLine(0, (5 + 20 * i), 320, DARKGREEN);
    else if (i == 1 || i == 2 || i == 3 || i == 8)
      tft.drawFastHLine(0, (5 + 20 * i), 320, BLUE);
  }

  // Горизонтальные линии (расход/мощность)
  for (uint8_t i = 0; i < 5; i++)
    tft.drawFastHLine(0, (231 + 10 * i), 320, BLUE);
  tft.drawFastHLine(0, 275, 320, BLUE);
  for (uint8_t i = 0; i < 8; i++)
    tft.drawFastHLine(0, (295 + 20 * i), 320, DARKGREEN);

  tft.setTextSize(1);

  // Подписи осей (температуры)
  tft.setCursor(0, 22); tft.setTextColor(WHITE, BLACK); tft.print("90");
  tft.setCursor(0, 42); tft.print("70");
  tft.setCursor(0, 62); tft.print("50");
  tft.setCursor(0, 82); tft.print("40");
  tft.setCursor(0, 102); tft.print("30");
  tft.setCursor(0, 122); tft.print("20");
  tft.setCursor(0, 142); tft.print("10");
  tft.setCursor(0, 162); tft.print("0");
  tft.setCursor(0, 182); tft.print("-10");
  tft.setCursor(0, 202); tft.print("-20");

  // Подписи осей (расход/мощность)
  tft.setCursor(0, 238); tft.print("5");
  tft.setCursor(0, 258); tft.print("3");
  tft.setCursor(0, 272); tft.print("1.6");
  tft.setCursor(0, 292); tft.print("1.4");
  tft.setCursor(0, 312); tft.print("1.2");
  tft.setCursor(0, 332); tft.print("1");
  tft.setCursor(0, 352); tft.print("0.8");
  tft.setCursor(0, 372); tft.print("0.6");
  tft.setCursor(0, 392); tft.print("0.4");
  tft.setCursor(0, 412); tft.print("0.2");
  tft.setCursor(0, 432); tft.print("0");

  // Заголовки
  tft.setCursor(0, 0); tft.setTextColor(RED); tft.print("Kompressor:");
  tft.setCursor(10, 10); tft.setTextColor(MAGENTA); tft.print("Overheat:");
  tft.setCursor(125, 10); tft.setTextColor(CYAN); tft.print("Defrost:");
  tft.setCursor(220, 10); tft.setTextColor(RED); tft.print("Isparitel:");
  tft.setCursor(120, 0); tft.setTextColor(YELLOW); tft.print("Water in:");
  tft.setCursor(220, 0); tft.setTextColor(GREEN); tft.print("Water out:");
  tft.setCursor(5, 210); tft.setTextColor(RED); tft.print("Heat power:");
  tft.setCursor(152, 210); tft.setTextColor(YELLOW); tft.print("COP:");
  tft.setCursor(218, 210); tft.setTextColor(GREEN); tft.print("Electr pow:");
  tft.setCursor(22, 220); tft.setTextColor(MAGENTA); tft.print("G water:");
}

// ===================================================
// 8. ТЕКСТОВЫЙ РЕЖИМ
// ===================================================
void getAllLastDataToDisplay() {
  tft.fillScreen(BLACK);
  tft.setTextSize(1);
  tft.setTextColor(YELLOW);

  tft.setCursor(0, 0); tft.print("tRoom:          "); tft.print(rxData.t1 / 10.0); tft.print(" C");
  tft.setCursor(0, 10); tft.print("tGS:            "); tft.print(rxData.t2 / 10.0); tft.print(" C");
  tft.setCursor(0, 20); tft.print("tBA:            "); tft.print(rxData.t3 / 10.0); tft.print(" C");
  tft.setCursor(0, 30); tft.print("tOutdoor:       "); tft.print(rxData.t4 / 10.0); tft.print(" C");
  tft.setCursor(0, 40); tft.print("tKompressorOut: "); tft.print(rxData.t5 / 10.0); tft.print(" C");
  tft.setCursor(0, 50); tft.print("tWaterIn:       "); tft.print(rxData.t6 / 10.0); tft.print(" C");
  tft.setCursor(0, 60); tft.print("tWaterOut:      "); tft.print(rxData.t7 / 10.0); tft.print(" C");
  tft.setCursor(0, 70); tft.print("tKompressorIn:  "); tft.print(rxData.t8 / 10.0); tft.print(" C");
  tft.setCursor(0, 80); tft.print("tDefrost:       "); tft.print(rxData.t9 / 10.0); tft.print(" C");
  tft.setCursor(0, 90); tft.print("tEvaporator:    "); tft.print(rxData.t10 / 10.0); tft.print(" C");
  tft.setCursor(0, 100); tft.print("Pressure:       ");
  if (rxData.P == 1) tft.print("OK");
  else if (rxData.P == 2) tft.print("HIGH");
  else tft.print("LOW");
  tft.setCursor(0, 110); tft.print("Time:           "); tft.print(rxData.hours); tft.print(":"); tft.print(rxData.minutes);
  tft.setCursor(0, 120); tft.print("Date:           "); tft.print(rxData.days); tft.print("."); tft.print(rxData.months);
  tft.setCursor(0, 130); tft.print("Water flow:     "); tft.print(rxData.Water / 100.0); tft.print(" m3/h");
  tft.setCursor(0, 140); tft.print("Power:          "); tft.print(rxData.Electr / 100.0); tft.print(" W");
  tft.setCursor(0, 150); tft.print("Relays:         "); tft.print(rxData.relaysState, BIN);
  tft.setCursor(0, 160); tft.print("Mistakes:       "); tft.print(mistakes, BIN);
}

// ===================================================
// 9. ОТОБРАЖЕНИЕ ОШИБОК
// ===================================================
void showMistakes() {
  tft.fillScreen(BLACK);
  tft.setTextSize(2);
  tft.setTextColor(RED);
  tft.setCursor(0, 80);
  tft.print("❌ ERROR");
  tft.setCursor(0, 120);
  tft.setTextSize(1);
  if (bitRead(mistakes, 0) == HIGH)
    tft.println("CRC error from Mega");
  if (bitRead(mistakes, 1) == HIGH)
    tft.println("Stop: CRC error from Mega");
  if (bitRead(mistakes, 2) == HIGH)
    tft.println("Connection from Mega to nano LOST!");
  if (bitRead(mistakes, 3) == HIGH)
    tft.println("Stop: connection from Mega to nano LOST!");


  if (bitRead(mistakes, 15) == HIGH)
    tft.println("CRC error from nano");
  if (bitRead(mistakes, 14) == HIGH)
    tft.println("Stop: CRC error from nano");
  if (bitRead(mistakes, 13) == HIGH)
    tft.println("Connection from nano to Mega LOST!");
  if (bitRead(mistakes, 12) == HIGH)
    tft.println("Stop: connection from nano to Mega LOST!");

  else if (bitRead(mistakes, 0) == LOW and bitRead(mistakes, 1) == LOW and bitRead(mistakes, 2) == LOW and bitRead(mistakes, 3) == LOW and bitRead(mistakes, 15) == LOW and bitRead(mistakes, 14) == LOW and bitRead(mistakes, 13) == LOW and bitRead(mistakes, 12) == LOW)
    tft.println("Unknown error");
}

// ===================================================
// 10. ОТПРАВКА ПАРАМЕТРОВ НА NANO
// ===================================================
void sendParamsToNano() {
  struct ParamsToNano {
    uint16_t output_from_Mega_to_nano;
    uint8_t actuatorMoveTime;
    uint8_t crc;
  } params;

  params.output_from_Mega_to_nano = output_from_Mega_to_nano;
  params.actuatorMoveTime = actuatorMoveTime;
  params.crc = crc8(&params, sizeof(params) - 1);

  Serial1.write((uint8_t*)&params, sizeof(params));
}

// ===================================================
// 11. ФОРМИРОВАНИЕ КОМАНДЫ РЕГУЛИРУЮЩЕМУ КЛАПАНУ
// ===================================================
void actuatorMove(String a, uint8_t b)
{
  if (millis() - actuatorTimer > 15000)//если после предыдущего перемещения клапана прошло время
  {
    if (a == "DOWN")
      bitSet(output_from_Mega_to_nano, 7);//закрывать регулирующий клапан
    else if (a == "UP")
      bitSet(output_from_Mega_to_nano, 8);//открывать регулирующий клапан
    actuatorMoveTime = b;
    actuatorTimer = millis();
  }
}


// ===================================================
// 12. ФИКСАЦИЯ ОШИБОК ОТ nano
// ===================================================
void updateMistakes()
{
  if (bitRead(rxData.mistakesFrom_nano, 0) == HIGH)
    bitSet(mistakes, 0);
  else if (bitRead(rxData.mistakesFrom_nano, 0) == LOW)
    bitClear(mistakes, 0);

  if (bitRead(rxData.mistakesFrom_nano, 1) == HIGH)
    bitSet(mistakes, 1);
  else if (bitRead(rxData.mistakesFrom_nano, 1) == LOW)
    bitClear(mistakes, 1);

  if (bitRead(rxData.mistakesFrom_nano, 2) == HIGH)
    bitSet(mistakes, 2);
  else if (bitRead(rxData.mistakesFrom_nano, 2) == LOW)
    bitClear(mistakes, 2);

  if (bitRead(rxData.mistakesFrom_nano, 3) == HIGH)
    bitSet(mistakes, 3);
  else if (bitRead(rxData.mistakesFrom_nano, 3) == LOW)
    bitClear(mistakes, 3);
}


// ===================================================
// 13. СМЕНА ИЗОБРАЖЕНИЯ НА ДИСПЛЕЕ
// ===================================================
void selectDisplay()
{
  if (kindDisplay != kindDisplayPrev)
  {
    if (kindDisplay == 1)
      getAllLastDataToDisplay();
    kindDisplayPrev = kindDisplay;
  }
}


// ===================================================
// 14. ОБНОВЛЕНИЕ МАССИВОВ ДЛЯ ГРАФИКОВ
// ===================================================
void updateArrays() {
  for (int i = 0; i < 319; i++) {
    t1_Graph[i] = t1_Graph[i + 1];
    t2_Graph[i] = t2_Graph[i + 1];
    t3_Graph[i] = t3_Graph[i + 1];
    t4_Graph[i] = t4_Graph[i + 1];
    t5_Graph[i] = t5_Graph[i + 1];
    t6_Graph[i] = t6_Graph[i + 1];
    t7_Graph[i] = t7_Graph[i + 1];
    t8_Graph[i] = t8_Graph[i + 1];
    t9_Graph[i] = t9_Graph[i + 1];
    t10_Graph[i] = t10_Graph[i + 1];
    Water_Graph[i] = Water_Graph[i + 1];
    Electr_Graph[i] = Electr_Graph[i + 1];
  }

  t1_Graph[319] = getDataForUintMassiv(rxData.t1);
  t2_Graph[319] = getDataForUintMassiv(rxData.t2);
  t3_Graph[319] = getDataForUintMassiv(rxData.t3);
  t4_Graph[319] = getDataForUintMassiv(rxData.t4);
  t5_Graph[319] = getDataForUintMassiv(rxData.t5);
  t6_Graph[319] = getDataForUintMassiv(rxData.t6);
  t7_Graph[319] = getDataForUintMassiv(rxData.t7);
  t8_Graph[319] = getDataForUintMassiv(rxData.t8);
  t9_Graph[319] = getDataForUintMassiv(rxData.t9);
  t10_Graph[319] = getDataForUintMassiv(rxData.t10);
  Water_Graph[319] = getDataForUintMassiv8(rxData.Water);
  Electr_Graph[319] = getDataForUintMassiv8(rxData.Electr);
}

// ===================================================
// 15. НАЧАЛЬНАЯ НАСТРОЙКА
// ===================================================
void setup() {
  Serial.begin(9600);
  Serial1.begin(9600);  // Связь с nano
  Serial2.begin(9600); // Связь с UNO

  // === Настройка дисплея ===
  tft.reset();
  tft.begin(38022);
  tft.invertDisplay(true);

  // === Инициализация графиков ===
  for (int i = 0; i < 320; i++) {
    t1_Graph[i] = t2_Graph[i] = t3_Graph[i] = t4_Graph[i] = 80;
    t5_Graph[i] = t6_Graph[i] = t7_Graph[i] = t8_Graph[i] = 80;
    t9_Graph[i] = t10_Graph[i] = 80;
    Water_Graph[i] = Electr_Graph[i] = 40;
  }

  delay(1000);
  getMainDisplay();

  // Отправляем начальные параметры
  delay(500);
  sendParamsToNano();
}

// ===================================================
// 16. ГЛАВНЫЙ ЦИКЛ
// ===================================================
void loop() {

  static uint32_t lastGet = 0;//для исчисления времени последних полученных данных

  selectDisplay();//моментальная смена изображения на дисплее

  // === 1. Приём данных от UNO ===
  if (Serial2.available()) {
    DataFromUNO window;
    if (Serial2.readBytes((uint8_t*)&window, sizeof(window))) {
      rxDataFromUNO = window;
      kindDisplay = rxDataFromUNO.Display;
    }
  }

  // === 2. Приём данных от nano ===
  if (Serial1.available()) {
    DataFromNano temp;
    if (Serial1.readBytes((uint8_t*)&temp, sizeof(temp))) {
      uint8_t crc = crc8(&temp, sizeof(temp));
      if (crc == 0) {
        rxData = temp;
        updateArrays();
        updateMistakes();
        bitClear(mistakes, 15);//снять ошибку передачи данных от nano к Mega
        mistake_15_Count = 0;

        // === 3. Отображение ===
        if (mistakes > 0)
          showMistakes;

        else {
          if (kindDisplay == 1) {
            getAllLastDataToDisplay();
          } else {
            getMainDisplay();
            getGraphics(245, RED, t1_Graph);
            getGraphics(245, YELLOW, t2_Graph);
            getGraphics(245, GREEN, t3_Graph);
            getGraphics(245, MAGENTA, t4_Graph);
            getGraphics(245, CYAN, t5_Graph);
            getGraphics(245, RED, t6_Graph);
            getGraphics(245, YELLOW, t7_Graph);
            getGraphics(245, GREEN, t8_Graph);
            getGraphics(245, MAGENTA, t9_Graph);
            getGraphics(245, CYAN, t10_Graph);
            getGraphics(475, RED, Water_Graph);
            getGraphics(475, YELLOW, Electr_Graph);
          }
        }
      }
      else
      {
        bitSet(mistakes, 15);//зафиксирована ошибка передачи данных от nano к Mega
        mistake_15_Count++;
        if (mistake_15_Count > 3)
          bitSet(mistakes, 14);//зафиксирована несколько раз подряд ошибка передачи данных от nano к Mega
        showMistakes;
      }
      bitClear(mistakes, 13);
      mistake_13_Count = 0;
      lastGet = millis();
      gettingFrom_nano = true;
    }
  }


  // === 4. Периодическая отправка параметров ===
  if (millis() - lastGet > 5000 and gettingFrom_nano == true) { //через 5 сек. после получения данных от nano


    //-------------- проверка работы реле ---------------
    cycleCount++;
    if (cycleCount > 24)
      bitSet(output_from_Mega_to_nano, 0); //включить дизельный котёл и насос дизельного котла
    if (cycleCount > 22)
      bitSet(output_from_Mega_to_nano, 1);//включить ТЭН 2,5 кВт
    if (cycleCount > 20)
      bitSet(output_from_Mega_to_nano, 2);//включить ТЭН 7,5 кВт
    if (cycleCount > 18)
      bitSet(output_from_Mega_to_nano, 3);//открыть шаровой кран
    if (cycleCount == 17)
      actuatorMove("DOWN", 20);
    else
      bitClear(output_from_Mega_to_nano, 7);
    if (cycleCount == 15)
      actuatorMove("UP", 20);
    else
      bitClear(output_from_Mega_to_nano, 8);
    if (cycleCount > 12)
      bitSet(output_from_Mega_to_nano, 6);//включить вентилятор наружного блока
    if (cycleCount > 10)
      bitSet(output_from_Mega_to_nano, 4);//включить насос радиаторов
    if (cycleCount > 8)
      bitSet(output_from_Mega_to_nano, 5);//включить насос тёплых полов
    //if (cycleCount > 6)
    //bitSet(output_from_Mega_to_nano, 9);//включить компрессор
    //if (cycleCount > 4)
    //bitSet(output_from_Mega_to_nano, 10);//включить четырёхходовый клапан
    if (cycleCount > 2)
      bitSet(output_from_Mega_to_nano, 11);//включить насос пластинчатого теплообменника

    if (cycleCount > 26)
    {
      cycleCount = 0;
      output_from_Mega_to_nano = 0b0000000000000000;
    }
    //-------------- (проверка работы реле) ---------------


    sendParamsToNano();
    gettingFrom_nano = false;
  }


  // === 5. Фиксация потери связи ===
  if (millis() - lastGet > 15000) {//если в течение 15 сек. не было связи с nano
    {
      bitSet(mistakes, 13);//зафиксирована потеря связи с nano
      mistake_13_Count++;
      lastGet = millis();//попытка связи с nano
      if (mistake_13_Count > 3)
      {
        bitSet(mistakes, 12);//зафиксирована продолжительная потеря связи с nano
        output_from_Mega_to_nano = 0; //аварийный останов
      }
      showMistakes;
    }
  }
}

именно пересылка данных (общение между МК) - работают по моим наблюдениям нормально. И это самое важное для меня на данный момент. Скетчи делал - на основе кода от BABOS из сообщения 612.
Просьба к вам такая (по возможности) - посмотрите, насколько оптимально организована именно связь. То есть - корректно ли и при нужных ли условиях - осуществляется передача данных. Сначала (или с конца - не совсем понимаю, кто тут главный) передача данных от Mega к nano, а затем уже от nano к Mega.
Я хочу сначала разобраться с этим (возможно, самым важным моментом) - оптимально ли заданы условия, когда должна пройти передача пакета. Оптимально ли организован приём этого пакета и опять передача (уже от второго МК к первому). Под словом “оптимально” - я имею в виду надёжность. Также надо, чтобы в случае пропусков (по каким-либо причинам) передачи пакетов - микроконтролллеры ещё в течение какого-то времени (около минуты) пытались бы наладить связь (возобновить передачу пакетов).

/*
   ПРОГРАММА ДЛЯ КОНТРОЛЛЕРА NANO
   Версия с исправленной CRC и улучшенной надёжностью связи
*/

// ===================================================
// 1. ПОДКЛЮЧАЕМ БИБЛИОТЕКИ
// ===================================================
#include <OneWire.h>
OneWire ds(2);                    // Пин для датчиков DS18B20
#include "microDS3231.h"
MicroDS3231 rtc;                  // Часы реального времени

// ===================================================
// 2. ПИНЫ ДЛЯ СЧЁТЧИКОВ
// ===================================================
#define pinVoda_tick   4          // Импульсный вход расходомера воды
#define pinElectr_tick 5          // Импульсный вход электросчётчика

// ===================================================
// 3. АДРЕСА ДАТЧИКОВ ТЕМПЕРАТУРЫ DS18B20
// ===================================================
byte ADDR_DS18B20 [10][8] = {
  {0x28, 0xFF, 0x94, 0xAE, 0xA0, 0x16, 0x03, 0x0E}, // 0 - Воздух в комнате
  {0x28, 0xFF, 0x48, 0x83, 0xA0, 0x16, 0x04, 0x1F}, // 1 - Вода на выходе из гидроразделителя
  {0x28, 0xFF, 0xAA, 0xAB, 0xA0, 0x16, 0x05, 0x74}, // 2 - Вода на выходе из бака-аккумулятора
  {0x28, 0xFF, 0xEC, 0x98, 0xA0, 0x16, 0x04, 0x9F}, // 3 - Воздух на улице
  {0x28, 0xD5, 0x97, 0x76, 0x0, 0x0, 0x0, 0x76},    // 4 - Фреон на выходе из компрессора
  {0x28, 0x4F, 0x9C, 0x73, 0x0, 0x0, 0x0, 0xF9},    // 5 - Вода на входе в теплообменник
  {0x28, 0xFC, 0xD1, 0x72, 0x0, 0x0, 0x0, 0x2D},    // 6 - Вода на выходе из теплообменника
  {0x28, 0x95, 0x28, 0x75, 0x0, 0x0, 0x0, 0xAB},    // 7 - Фреон на входе в компрессор
  {0x28, 0xEF, 0x8C, 0x76, 0x0, 0x0, 0x0, 0x58},    // 8 - Нижняя часть теплообменника
  {0x28, 0x8C, 0x3A, 0xB4, 0xE, 0x0, 0x0, 0xDB}     // 9 - Фреон в начале испарителя
};

// ===================================================
// 4. ОСНОВНЫЕ ПЕРЕМЕННЫЕ
// ===================================================
int16_t Temper[10] = {0};                 // Массив температур (x10)
uint16_t output_to_relays = 0;            // Битовая маска реле
uint8_t PWM = 0;                          // ШИМ для вентилятора (0-255)
uint8_t WaterPresure = 1;                 // Давление: 1=норма, 2=макс, 3=мин
boolean gettingTempComplete = false;      // Флаг готовности температур
uint8_t mistakesFrom_nano = 0;            // Код ошибки, битовая маска
//бит 0 - ошибка CRC при передаче данных от Mega к nano, менее нескольких раз подряд
//бит 1 - ошибка CRC при передаче данных от Mega к nano, более нескольких раз подряд
//бит 2 - ошибка потери связи с МК Mega, менее нескольких раз подряд
//бит 3 - ошибка потери связи с МК Mega, более нескольких раз подряд
uint8_t mistakesFrom_nano_0_Count = 0;    // счётчик ошибки CRC (до нескольких раз)
uint8_t mistakesFrom_nano_2_Count = 0;    // счётчик ошибки потери связи (до нескольких раз)

// ===================================================
// 5. КОНТРОЛЬ СВЯЗИ С MEGA
// ===================================================
uint32_t lastRxTime = 0;                  // Время последнего приёма

// ===================================================
// 6. ПЕРЕМЕННЫЕ ДЛЯ РАСХОДОМЕРА ВОДЫ
// ===================================================
boolean stateCheck_voda;
uint32_t tmr, WaterStartTimer = 0, WaterCountTimer;
boolean gWaterFlag = false, FirstImpulsWater = false, SecondImpulsWater = false;
uint32_t gWater = 0;                      // Расход воды (м3/ч)/100

// ===================================================
// 7. ПЕРЕМЕННЫЕ ДЛЯ ЭЛЕКТРОСЧЁТЧИКА
// ===================================================
boolean stateCheck_electr;
uint32_t tmr2, ElectrStartTimer = 0, ElectrCountTimer;
boolean ElectrFlag = false;
uint32_t wElectr = 0;                     // Мощность (кВт)/100

// ===================================================
// 8. УПРАВЛЕНИЕ ПРИВОДОМ КЛАПАНА
// ===================================================
uint32_t TimerStartPrivodUp = 0;
uint32_t TimerStartPrivodDown = 0;
uint32_t timePrivodUp = 0;
uint32_t timePrivodDown = 0;

// ===================================================
// 9. СТРУКТУРЫ ДАННЫХ ДЛЯ ОБМЕНА С MEGA
// ===================================================

// От NANO к MEGA (все данные + состояние реле)
struct DataToMega {
  int16_t t1, t2, t3, t4, t5, t6, t7, t8, t9, t10;
  uint8_t P;
  uint8_t minutes, hours, days, months;
  uint8_t Water;
  uint8_t Electr;
  uint16_t relaysState;
  uint8_t mistakesFrom_nano;
  uint8_t crc;
} dataToMega;

// От MEGA к NANO (только параметры)
struct DataFromMega {
  uint16_t output_from_Mega_to_nano;
  uint8_t actuatorMoveTime;
  uint8_t crc;
} dataFromMega;

// ===================================================
// 10. ФУНКЦИЯ CRC-8
// ===================================================
uint8_t crc8(const void* data, size_t length, uint8_t crc = 0) {
  const uint8_t* p = (const uint8_t*)data;
  while (length--) {
    uint8_t b = *p++, j = 8;
    while (j--) {
      crc = ((crc ^ b) & 1) ? (crc >> 1) ^ 0x8C : (crc >> 1);
      b >>= 1;
    }
  }
  return crc;
}

// ===================================================
// 11. ФУНКЦИИ ДЛЯ РАБОТЫ С ДАТЧИКАМИ ТЕМПЕРАТУРЫ
// ===================================================
bool readDS18B20(byte *addr, int16_t *dest) {
  uint8_t data[9];
  ds.reset();
  ds.select(addr);
  ds.write(0xBE);
  for (uint8_t i = 0; i < 9; i++) data[i] = ds.read();
  if (crc8(data, 8) != data[8]) return false;
  int16_t raw = (int16_t)((data[1] << 8) | data[0]);
  *dest = (raw * 5) / 8;
  return true;
}

void resetTemp() {
  ds.reset();
  ds.write(0xCC);
  ds.write(0x44);
}

void gettingTemp() {
  int16_t temp;
  for (uint8_t i = 0; i < 10; i++) {
    if (readDS18B20(ADDR_DS18B20[i], &temp)) {
      Temper[i] = temp;
    } else {
      Temper[i] = 22222;  // Ошибка датчика
    }
  }
  gettingTempComplete = true;
}

// ===================================================
// 12. ОБРАБОТЧИК ИМПУЛЬСОВ РАСХОДОМЕРА ВОДЫ
// ===================================================
void voda_tick() {
  boolean current_status = digitalRead(pinVoda_tick);
  if (stateCheck_voda && !current_status) {
    tmr = millis();
    gWaterFlag = true;
    stateCheck_voda = current_status;
    return;
  }
  if (!stateCheck_voda && current_status) {
    stateCheck_voda = current_status;
    return;
  }
  if (current_status) {
    gWaterFlag = false;
    return;
  }
  if (!current_status && (millis() - tmr >= 1000) && gWaterFlag == true) {
    if (WaterStartTimer == 0) {
      WaterStartTimer = millis();
      gWaterFlag = false;
      return;
    }
    WaterCountTimer = millis() - WaterStartTimer;
    WaterStartTimer = millis();
    gWaterFlag = false;
    if (FirstImpulsWater == true) SecondImpulsWater = true;
    FirstImpulsWater = true;
  }
}

// ===================================================
// 13. ОБРАБОТЧИК ИМПУЛЬСОВ ЭЛЕКТРОСЧЁТЧИКА
// ===================================================
void electr_tick() {
  boolean current_status = digitalRead(pinElectr_tick);
  if (stateCheck_electr && !current_status) {
    tmr2 = millis();
    ElectrFlag = true;
    stateCheck_electr = current_status;
    return;
  }
  if (!stateCheck_electr && current_status) {
    stateCheck_electr = current_status;
    return;
  }
  if (current_status) {
    ElectrFlag = false;
    return;
  }
  if (!current_status && (millis() - tmr2 >= 50) && ElectrFlag == true) {
    if (ElectrStartTimer == 0) {
      ElectrStartTimer = millis();
      ElectrFlag = false;
      return;
    }
    ElectrCountTimer = millis() - ElectrStartTimer;
    ElectrStartTimer = millis();
    ElectrFlag = false;
  }
}

// ===================================================
// 14. ПРИЁМ ПАРАМЕТРОВ ОТ MEGA (исправленная версия)
// ===================================================
void receiveFromMega() {
  // Проверяем, есть ли в буфере достаточно данных для полного пакета
  if (Serial.available() >= sizeof(DataFromMega)) {
    DataFromMega rx;
    
    // Читаем с таймаутом 50 мс, чтобы не блокировать цикл
    Serial.setTimeout(50);
    if (Serial.readBytes((uint8_t*)&rx, sizeof(rx)) == sizeof(rx)) {
      
      // ПРОВЕРЯЕМ CRC: вычисляем для данных (без поля crc)
      uint8_t calculatedCrc = crc8(&rx, sizeof(rx) - 1);
      
      if (calculatedCrc == rx.crc) {
        // === ПАКЕТ ПРИНЯТ КОРРЕКТНО ===
        output_to_relays = rx.output_from_Mega_to_nano;
        
        // Обработка времени перемещения клапана
        if (bitRead(output_to_relays, 7) == HIGH)
          timePrivodDown = rx.actuatorMoveTime * 100;
        else if (bitRead(output_to_relays, 8) == HIGH)
          timePrivodUp = rx.actuatorMoveTime * 100;
        else {
          timePrivodDown = 0;
          timePrivodUp = 0;
        }
        
        // Сбрасываем ошибки CRC
        bitClear(mistakesFrom_nano, 0);
        mistakesFrom_nano_0_Count = 0;
        
        // Обновляем время последнего успешного приёма
        lastRxTime = millis();
        bitClear(mistakesFrom_nano, 2);
        mistakesFrom_nano_2_Count = 0;
        
      } else {
        // === ОШИБКА CRC ===
        bitSet(mistakesFrom_nano, 0);
        mistakesFrom_nano_0_Count++;
        
        if (mistakesFrom_nano_0_Count > 3) {
          bitSet(mistakesFrom_nano, 1);
          output_to_relays = 0; // аварийный останов
        }
      }
    }
  }

  // ====== ПРОВЕРКА ПОТЕРИ СВЯЗИ ======
  uint32_t timeSinceLastRx = millis() - lastRxTime;
  if (timeSinceLastRx >= 15000) {
    bitSet(mistakesFrom_nano, 2);
    mistakesFrom_nano_2_Count++;
    lastRxTime = millis(); // чтобы не увеличивать счётчик каждую миллисекунду
    
    if (mistakesFrom_nano_2_Count > 3) {
      bitSet(mistakesFrom_nano, 3);
      output_to_relays = 0; // аварийный останов
    }
  }
}

// ===================================================
// 15. УПРАВЛЕНИЕ ПРИВОДОМ РЕГУЛИРУЮЩЕГО КЛАПАНА
// ===================================================
void actuatorMove() {
  // Закрывание (бит 7)
  if (bitRead(output_to_relays, 7) == HIGH && digitalRead(13) == HIGH) {
    TimerStartPrivodDown = millis();
    digitalWrite(13, LOW);
  }
  if (digitalRead(13) == LOW && millis() - TimerStartPrivodDown >= timePrivodDown) {
    digitalWrite(13, HIGH);
    bitClear(output_to_relays, 7);
  }

  // Открывание (бит 8)
  if (bitRead(output_to_relays, 8) == HIGH && digitalRead(12) == HIGH) {
    TimerStartPrivodUp = millis();
    digitalWrite(12, LOW);
  }
  if (digitalRead(12) == LOW && millis() - TimerStartPrivodUp >= timePrivodUp) {
    digitalWrite(12, HIGH);
    bitClear(output_to_relays, 8);
  }
}

// ===================================================
// 16. ОТПРАВКА ДАННЫХ НА MEGA (исправленная версия)
// ===================================================
void sendToMega() {
  // Заполняем структуру данными
  dataToMega.t1 = Temper[0];
  dataToMega.t2 = Temper[1];
  dataToMega.t3 = Temper[2];
  dataToMega.t4 = Temper[3];
  dataToMega.t5 = Temper[4];
  dataToMega.t6 = Temper[5];
  dataToMega.t7 = Temper[6];
  dataToMega.t8 = Temper[7];
  dataToMega.t9 = Temper[8];
  dataToMega.t10 = Temper[9];
  dataToMega.P = WaterPresure;
  dataToMega.minutes = rtc.getMinutes();
  dataToMega.hours = rtc.getHours();
  dataToMega.days = rtc.getDate();
  dataToMega.months = rtc.getMonth();
  dataToMega.Water = (uint8_t)gWater;
  dataToMega.Electr = (uint8_t)wElectr;
  dataToMega.relaysState = output_to_relays;
  dataToMega.mistakesFrom_nano = mistakesFrom_nano;

  // ВЫЧИСЛЯЕМ CRC ТОЛЬКО ДЛЯ ДАННЫХ (без поля crc)
  dataToMega.crc = crc8(&dataToMega, sizeof(dataToMega) - 1);

  // Отправляем всю структуру (включая CRC)
  Serial.write((uint8_t*)&dataToMega, sizeof(dataToMega));
}

// ===================================================
// 17. УПРАВЛЕНИЕ РЕЛЕ (физические выходы)
// ===================================================
void setRelays() {
  // 0 - Дизельный котёл
  digitalWrite(9, bitRead(output_to_relays, 0) ? LOW : HIGH);
  // 1 - ТЭН 2.5 кВт
  digitalWrite(8, bitRead(output_to_relays, 1) ? LOW : HIGH);
  // 2 - ТЭН 7.5 кВт
  digitalWrite(7, bitRead(output_to_relays, 2) ? LOW : HIGH);
  // 3 - Шаровой кран
  digitalWrite(6, bitRead(output_to_relays, 3) ? LOW : HIGH);
  // 4 - Насос радиаторов
  digitalWrite(10, bitRead(output_to_relays, 4) ? LOW : HIGH);
  // 5 - Насос тёплых полов
  digitalWrite(A3, bitRead(output_to_relays, 5) ? LOW : HIGH);
  // 6 - Вентилятор
  digitalWrite(11, bitRead(output_to_relays, 6) ? LOW : HIGH);
  // 7 - Закрывание клапана
  digitalWrite(13, bitRead(output_to_relays, 7) ? LOW : HIGH);
  // 8 - Открывание клапана
  digitalWrite(12, bitRead(output_to_relays, 8) ? LOW : HIGH);
  // 9 - Компрессор
  digitalWrite(A2, bitRead(output_to_relays, 9) ? LOW : HIGH);
  // 10 - Четырёхходовой клапан
  digitalWrite(A1, bitRead(output_to_relays, 10) ? LOW : HIGH);
  // 11 - Насос теплообменника
  digitalWrite(A0, bitRead(output_to_relays, 11) ? LOW : HIGH);

  // ШИМ для вентилятора
  analogWrite(3, PWM);
}

// ===================================================
// 18. НАЧАЛЬНАЯ НАСТРОЙКА
// ===================================================
void setup() {
  Serial.begin(9600);

  // === Настройка пинов ===
  pinMode(4, INPUT_PULLUP);
  pinMode(5, INPUT_PULLUP);

  // Выходы реле (HIGH = выключено, LOW = включено)
  pinMode(6, OUTPUT); digitalWrite(6, HIGH);
  pinMode(7, OUTPUT); digitalWrite(7, HIGH);
  pinMode(8, OUTPUT); digitalWrite(8, HIGH);
  pinMode(9, OUTPUT); digitalWrite(9, HIGH);
  pinMode(10, OUTPUT); digitalWrite(10, HIGH);
  pinMode(11, OUTPUT); digitalWrite(11, HIGH);
  pinMode(12, OUTPUT); digitalWrite(12, HIGH);
  pinMode(13, OUTPUT); digitalWrite(13, HIGH);
  pinMode(A0, OUTPUT); digitalWrite(A0, HIGH);
  pinMode(A1, OUTPUT); digitalWrite(A1, HIGH);
  pinMode(A2, OUTPUT); digitalWrite(A2, HIGH);
  pinMode(A3, OUTPUT); digitalWrite(A3, HIGH);

  // === Часы реального времени ===
  if (rtc.lostPower()) rtc.setTime(COMPILE_TIME);

  delay(1000);
}

// ===================================================
// 19. ГЛАВНЫЙ ЦИКЛ
// ===================================================
void loop() {
  // === Обработчики импульсов ===
  voda_tick();
  electr_tick();

  // === Приём данных от Mega ===
  receiveFromMega();

  // === Управление клапаном ===
  actuatorMove();

  // === Цикл 10 секунд ===
  static uint32_t timer = 0;
  static uint8_t step = 0;

  // Шаг 1: Запуск измерения температур (2 сек)
  if (millis() - timer >= 2000 && step < 1) {
    resetTemp();
    step = 1;
  }

  // Шаг 2: Чтение температур (4 сек)
  if (millis() - timer >= 4000 && step < 2) {
    gettingTemp();
    step = 2;
  }

  // Шаг 3: Управление (6 сек)
  if (millis() - timer >= 6000 && step < 3) {
    // === Давление ===
    if (analogRead(A6) < 307) WaterPresure = 2;
    else if (analogRead(A7) < 307) WaterPresure = 3;
    else WaterPresure = 1;

    // === Расход воды ===
    if (SecondImpulsWater == false) gWater = 10;
    else gWater = round(3600000 / WaterCountTimer);

    // === Мощность ===
    if (ElectrCountTimer == 0 || (millis() - ElectrStartTimer > 60000))
      wElectr = 0;
    else wElectr = 72000 / ElectrCountTimer;

    step = 3;
  }

  // Шаг 4: Управление реле (8 сек)
  if (millis() - timer >= 8000 && step < 4) {
    setRelays();
    step = 4;
  }

  // Шаг 5: Отправка данных на Mega (10 сек)
  if (millis() - timer >= 10000 && step < 5) {
    if (gettingTempComplete) {
      sendToMega();
      gettingTempComplete = false;
      step = 5;
    }
    timer = millis();
    step = 0;
  }
}
/*
   ПРОГРАММА ДЛЯ КОНТРОЛЛЕРА MEGA
   Версия с исправленной CRC и улучшенной надёжностью связи
*/

#include <MCUFRIEND_kbv.h>
MCUFRIEND_kbv tft;

// ===================================================
// 1. НАСТРОЙКИ ДИСПЛЕЯ
// ===================================================
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RESET A4

// ===================================================
// 2. ЦВЕТА
// ===================================================
#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF
#define DARKGREY  0x7BEF
#define DARKGREEN 0x0C80

// ===================================================
// 3. ПЕРЕМЕННЫЕ
// ===================================================
uint8_t kindDisplay = 1;
uint8_t kindDisplayPrev = 1;
uint16_t mistakes = 0;
//бит 0 - ошибка CRC при передаче данных от Mega к nano, менее нескольких раз подряд
//бит 1 - ошибка CRC при передаче данных от Mega к nano, более нескольких раз подряд
//бит 2 - ошибка потери связи с МК Mega, менее нескольких раз подряд
//бит 3 - ошибка потери связи с МК Mega, более нескольких раз подряд
//бит 15 - ошибка CRC при передаче данных от nano к Mega, менее нескольких раз подряд
//бит 14 - ошибка CRC при передаче данных от nano к Mega, более нескольких раз подряд
//бит 13 - ошибка потери связи с МК nano, менее нескольких раз подряд
//бит 12 - ошибка потери связи с МК nano, более нескольких раз подряд

uint8_t mistake_15_Count = 0;    // счётчик количества ошибок подряд, mistakes, бит 15
uint8_t mistake_13_Count = 0;    // счётчик количества ошибок подряд, mistakes, бит 13

uint16_t output_from_Mega_to_nano = 0;
//0 - дизельный котёл и насос дизельного котла
//1 - ТЭН 2,5 кВт
//2 - ТЭН 7,5 кВт
//3 - шаровой кран
//4 - насос радиаторов
//5 - насос тёплых полов
//6 - вентилятор наружного блока
//7 - закрывание клапана
//8 - открывание клапана
//9 - компрессор
//10 - четырёхходовой клапан
//11 - насос пластинчатого теплообменника

uint8_t actuatorMoveTime = 0;    // время перемещения клапана (в 0,1 сек.: 10 = 1 сек.)
uint32_t actuatorTimer = 0;      // для выдержки времени между передачей команды клапану
uint32_t lastGet = 0;        // время последнего получения данных от nano
uint32_t lastSend = 0;       // время последней отправки команд на nano
uint32_t lastControl = 0;    // время последнего формирования команд
boolean gettingFrom_nano = false; // флаг о получении данных от nano

//-------------- проверка работы реле ---------------
uint8_t cycleCount = 0;
//-------------- (проверка работы реле) ---------------

// Данные от nano
struct DataFromNano {
  int16_t t1, t2, t3, t4, t5, t6, t7, t8, t9, t10;
  uint8_t P;
  uint8_t minutes, hours, days, months;
  uint8_t Water;
  uint8_t Electr;
  uint16_t relaysState;
  uint8_t mistakesFrom_nano;
  uint8_t crc;
} rxData;

// Данные от UNO
struct DataFromUNO {
  uint8_t Display;
} rxDataFromUNO;

// Массивы для графиков
uint8_t t1_Graph[320], t2_Graph[320], t3_Graph[320], t4_Graph[320];
uint8_t t5_Graph[320], t6_Graph[320], t7_Graph[320], t8_Graph[320];
uint8_t t9_Graph[320], t10_Graph[320];
uint8_t Water_Graph[320], Electr_Graph[320];

// ===================================================
// 4. CRC
// ===================================================
uint8_t crc8(const void* data, size_t length, uint8_t crc = 0) {
  const uint8_t* p = (const uint8_t*)data;
  while (length--) {
    uint8_t b = *p++, j = 8;
    while (j--) {
      crc = ((crc ^ b) & 1) ? (crc >> 1) ^ 0x8C : (crc >> 1);
      b >>= 1;
    }
  }
  return crc;
}

// ===================================================
// 5. ПРЕОБРАЗОВАНИЕ ДЛЯ ГРАФИКОВ
// ===================================================
int16_t getDataForUintMassiv(int16_t a) {
  if (a < -400) return 0;
  else if (a >= -400 && a < 500) return round((a + 400) / 5);
  else if (a >= 500 && a < 1250) return round(180 + (a - 500) / 10);
  else return 255;
}

uint8_t getDataForUintMassiv8(uint8_t a) {
  if (a <= 160) return round(40 + a);
  else if (a < 255) return round(200 + (a - 160) / 10);
  else return 255;
}

// ===================================================
// 6. ОТРИСОВКА ГРАФИКОВ
// ===================================================
void getGraphics(uint16_t c, uint16_t d, uint8_t g[320]) {
  for (int16_t x = 1; x < 320; x++) {
    tft.drawLine(x - 1, (c - g[x - 1]), x, (c - g[x]), d);
  }
}

// ===================================================
// 7. ОСНОВНОЙ ЭКРАН
// ===================================================
void getMainDisplay() {
  tft.fillScreen(BLACK);

  // Вертикальные линии
  for (uint8_t i = 1; i < 8; i++) {
    tft.drawFastVLine((40 * i), 20, 190, DARKGREY);
    tft.drawFastVLine((40 * i), 230, 210, DARKGREY);
  }

  // Горизонтальные линии (температуры)
  for (uint8_t i = 0; i < 11; i++) {
    if (i == 4 || i == 5 || i == 6 || i == 7 || i == 9 || i == 10)
      tft.drawFastHLine(0, (5 + 20 * i), 320, DARKGREEN);
    else if (i == 1 || i == 2 || i == 3 || i == 8)
      tft.drawFastHLine(0, (5 + 20 * i), 320, BLUE);
  }

  // Горизонтальные линии (расход/мощность)
  for (uint8_t i = 0; i < 5; i++)
    tft.drawFastHLine(0, (231 + 10 * i), 320, BLUE);
  tft.drawFastHLine(0, 275, 320, BLUE);
  for (uint8_t i = 0; i < 8; i++)
    tft.drawFastHLine(0, (295 + 20 * i), 320, DARKGREEN);

  tft.setTextSize(1);

  // Подписи осей (температуры)
  tft.setCursor(0, 22); tft.setTextColor(WHITE, BLACK); tft.print("90");
  tft.setCursor(0, 42); tft.print("70");
  tft.setCursor(0, 62); tft.print("50");
  tft.setCursor(0, 82); tft.print("40");
  tft.setCursor(0, 102); tft.print("30");
  tft.setCursor(0, 122); tft.print("20");
  tft.setCursor(0, 142); tft.print("10");
  tft.setCursor(0, 162); tft.print("0");
  tft.setCursor(0, 182); tft.print("-10");
  tft.setCursor(0, 202); tft.print("-20");

  // Подписи осей (расход/мощность)
  tft.setCursor(0, 238); tft.print("5");
  tft.setCursor(0, 258); tft.print("3");
  tft.setCursor(0, 272); tft.print("1.6");
  tft.setCursor(0, 292); tft.print("1.4");
  tft.setCursor(0, 312); tft.print("1.2");
  tft.setCursor(0, 332); tft.print("1");
  tft.setCursor(0, 352); tft.print("0.8");
  tft.setCursor(0, 372); tft.print("0.6");
  tft.setCursor(0, 392); tft.print("0.4");
  tft.setCursor(0, 412); tft.print("0.2");
  tft.setCursor(0, 432); tft.print("0");

  // Заголовки
  tft.setCursor(0, 0); tft.setTextColor(RED); tft.print("Kompressor:");
  tft.setCursor(10, 10); tft.setTextColor(MAGENTA); tft.print("Overheat:");
  tft.setCursor(125, 10); tft.setTextColor(CYAN); tft.print("Defrost:");
  tft.setCursor(220, 10); tft.setTextColor(RED); tft.print("Isparitel:");
  tft.setCursor(120, 0); tft.setTextColor(YELLOW); tft.print("Water in:");
  tft.setCursor(220, 0); tft.setTextColor(GREEN); tft.print("Water out:");
  tft.setCursor(5, 210); tft.setTextColor(RED); tft.print("Heat power:");
  tft.setCursor(152, 210); tft.setTextColor(YELLOW); tft.print("COP:");
  tft.setCursor(218, 210); tft.setTextColor(GREEN); tft.print("Electr pow:");
  tft.setCursor(22, 220); tft.setTextColor(MAGENTA); tft.print("G water:");
}

// ===================================================
// 8. ТЕКСТОВЫЙ РЕЖИМ
// ===================================================
void getAllLastDataToDisplay() {
  tft.fillScreen(BLACK);
  tft.setTextSize(1);
  tft.setTextColor(YELLOW);

  tft.setCursor(0, 0); tft.print("tRoom:          "); tft.print(rxData.t1 / 10.0); tft.print(" C");
  tft.setCursor(0, 10); tft.print("tGS:            "); tft.print(rxData.t2 / 10.0); tft.print(" C");
  tft.setCursor(0, 20); tft.print("tBA:            "); tft.print(rxData.t3 / 10.0); tft.print(" C");
  tft.setCursor(0, 30); tft.print("tOutdoor:       "); tft.print(rxData.t4 / 10.0); tft.print(" C");
  tft.setCursor(0, 40); tft.print("tKompressorOut: "); tft.print(rxData.t5 / 10.0); tft.print(" C");
  tft.setCursor(0, 50); tft.print("tWaterIn:       "); tft.print(rxData.t6 / 10.0); tft.print(" C");
  tft.setCursor(0, 60); tft.print("tWaterOut:      "); tft.print(rxData.t7 / 10.0); tft.print(" C");
  tft.setCursor(0, 70); tft.print("tKompressorIn:  "); tft.print(rxData.t8 / 10.0); tft.print(" C");
  tft.setCursor(0, 80); tft.print("tDefrost:       "); tft.print(rxData.t9 / 10.0); tft.print(" C");
  tft.setCursor(0, 90); tft.print("tEvaporator:    "); tft.print(rxData.t10 / 10.0); tft.print(" C");
  tft.setCursor(0, 100); tft.print("Pressure:       ");
  if (rxData.P == 1) tft.print("OK");
  else if (rxData.P == 2) tft.print("HIGH");
  else tft.print("LOW");
  tft.setCursor(0, 110); tft.print("Time:           "); tft.print(rxData.hours); tft.print(":"); tft.print(rxData.minutes);
  tft.setCursor(0, 120); tft.print("Date:           "); tft.print(rxData.days); tft.print("."); tft.print(rxData.months);
  tft.setCursor(0, 130); tft.print("Water flow:     "); tft.print(rxData.Water / 100.0); tft.print(" m3/h");
  tft.setCursor(0, 140); tft.print("Power:          "); tft.print(rxData.Electr / 100.0); tft.print(" W");
  tft.setCursor(0, 150); tft.print("Relays:         "); tft.print(rxData.relaysState, BIN);
  tft.setCursor(0, 160); tft.print("Mistakes:       "); tft.print(mistakes, BIN);
}

// ===================================================
// 9. ОТОБРАЖЕНИЕ ОШИБОК
// ===================================================
void showMistakes() {
  tft.fillScreen(BLACK);
  tft.setTextSize(2);
  tft.setTextColor(RED);
  tft.setCursor(0, 80);
  tft.print("❌ ERROR");
  tft.setCursor(0, 120);
  tft.setTextSize(1);
  if (bitRead(mistakes, 0) == HIGH)
    tft.println("CRC error from Mega");
  if (bitRead(mistakes, 1) == HIGH)
    tft.println("Stop: CRC error from Mega");
  if (bitRead(mistakes, 2) == HIGH)
    tft.println("Connection from Mega to nano LOST!");
  if (bitRead(mistakes, 3) == HIGH)
    tft.println("Stop: connection from Mega to nano LOST!");
  if (bitRead(mistakes, 15) == HIGH)
    tft.println("CRC error from nano");
  if (bitRead(mistakes, 14) == HIGH)
    tft.println("Stop: CRC error from nano");
  if (bitRead(mistakes, 13) == HIGH)
    tft.println("Connection from nano to Mega LOST!");
  if (bitRead(mistakes, 12) == HIGH)
    tft.println("Stop: connection from nano to Mega LOST!");
  if (mistakes == 0)
    tft.println("Unknown error");
}

// ===================================================
// 10. ОТПРАВКА ПАРАМЕТРОВ НА NANO (исправленная версия)
// ===================================================
void sendParamsToNano() {
  struct ParamsToNano {
    uint16_t output_from_Mega_to_nano;
    uint8_t actuatorMoveTime;
    uint8_t crc;
  } params;

  params.output_from_Mega_to_nano = output_from_Mega_to_nano;
  params.actuatorMoveTime = actuatorMoveTime;
  params.crc = crc8(&params, sizeof(params) - 1);

  Serial1.write((uint8_t*)&params, sizeof(params));
}

// ===================================================
// 11. ПРИЁМ ДАННЫХ ОТ NANO (новая функция)
// ===================================================
void receiveFromNano() {
  // Проверяем наличие полного пакета
  if (Serial1.available() >= sizeof(DataFromNano)) {
    DataFromNano temp;
    
    // Читаем с таймаутом 50 мс
    Serial1.setTimeout(50);
    if (Serial1.readBytes((uint8_t*)&temp, sizeof(temp)) == sizeof(temp)) {
      
      // ПРОВЕРЯЕМ CRC (без поля crc)
      uint8_t calculatedCrc = crc8(&temp, sizeof(temp) - 1);
      
      if (calculatedCrc == temp.crc) {
        // === ДАННЫЕ ПРИНЯТЫ КОРРЕКТНО ===
        rxData = temp;
        updateArrays();
        updateMistakesFromNano();
        
        // Сбрасываем ошибки связи с Nano
        bitClear(mistakes, 15);
        mistake_15_Count = 0;
        
        bitClear(mistakes, 13);
        mistake_13_Count = 0;
        
        lastGet = millis();
        gettingFrom_nano = true;
        
        // Обновляем дисплей (если нет ошибок)
        if (mistakes == 0) {
          if (kindDisplay == 1) {
            getAllLastDataToDisplay();
          } else {
            getMainDisplay();
            getGraphics(245, RED, t1_Graph);
            getGraphics(245, YELLOW, t2_Graph);
            getGraphics(245, GREEN, t3_Graph);
            getGraphics(245, MAGENTA, t4_Graph);
            getGraphics(245, CYAN, t5_Graph);
            getGraphics(245, RED, t6_Graph);
            getGraphics(245, YELLOW, t7_Graph);
            getGraphics(245, GREEN, t8_Graph);
            getGraphics(245, MAGENTA, t9_Graph);
            getGraphics(245, CYAN, t10_Graph);
            getGraphics(475, RED, Water_Graph);
            getGraphics(475, YELLOW, Electr_Graph);
          }
        }
        
      } else {
        // === ОШИБКА CRC ===
        bitSet(mistakes, 15);
        mistake_15_Count++;
        if (mistake_15_Count > 3) {
          bitSet(mistakes, 14);
        }
        showMistakes();
      }
    }
  }
}

// ===================================================
// 12. ФОРМИРОВАНИЕ КОМАНДЫ РЕГУЛИРУЮЩЕМУ КЛАПАНУ
// ===================================================
void actuatorMove(String a, uint8_t b) {
  if (millis() - actuatorTimer > 15000) { // если после предыдущего перемещения клапана прошло время
    if (a == "DOWN")
      bitSet(output_from_Mega_to_nano, 7); // закрывать регулирующий клапан
    else if (a == "UP")
      bitSet(output_from_Mega_to_nano, 8); // открывать регулирующий клапан
    actuatorMoveTime = b;
    actuatorTimer = millis();
  }
}

// ===================================================
// 13. ФИКСАЦИЯ ОШИБОК ОТ NANO
// ===================================================
void updateMistakesFromNano() {
  if (bitRead(rxData.mistakesFrom_nano, 0) == HIGH)
    bitSet(mistakes, 0);
  else
    bitClear(mistakes, 0);

  if (bitRead(rxData.mistakesFrom_nano, 1) == HIGH)
    bitSet(mistakes, 1);
  else
    bitClear(mistakes, 1);

  if (bitRead(rxData.mistakesFrom_nano, 2) == HIGH)
    bitSet(mistakes, 2);
  else
    bitClear(mistakes, 2);

  if (bitRead(rxData.mistakesFrom_nano, 3) == HIGH)
    bitSet(mistakes, 3);
  else
    bitClear(mistakes, 3);
}

// ===================================================
// 14. СМЕНА ИЗОБРАЖЕНИЯ НА ДИСПЛЕЕ
// ===================================================
void selectDisplay() {
  if (kindDisplay != kindDisplayPrev) {
    if (kindDisplay == 1)
      getAllLastDataToDisplay();
    kindDisplayPrev = kindDisplay;
  }
}

// ===================================================
// 15. ОБНОВЛЕНИЕ МАССИВОВ ДЛЯ ГРАФИКОВ
// ===================================================
void updateArrays() {
  for (int i = 0; i < 319; i++) {
    t1_Graph[i] = t1_Graph[i + 1];
    t2_Graph[i] = t2_Graph[i + 1];
    t3_Graph[i] = t3_Graph[i + 1];
    t4_Graph[i] = t4_Graph[i + 1];
    t5_Graph[i] = t5_Graph[i + 1];
    t6_Graph[i] = t6_Graph[i + 1];
    t7_Graph[i] = t7_Graph[i + 1];
    t8_Graph[i] = t8_Graph[i + 1];
    t9_Graph[i] = t9_Graph[i + 1];
    t10_Graph[i] = t10_Graph[i + 1];
    Water_Graph[i] = Water_Graph[i + 1];
    Electr_Graph[i] = Electr_Graph[i + 1];
  }

  t1_Graph[319] = getDataForUintMassiv(rxData.t1);
  t2_Graph[319] = getDataForUintMassiv(rxData.t2);
  t3_Graph[319] = getDataForUintMassiv(rxData.t3);
  t4_Graph[319] = getDataForUintMassiv(rxData.t4);
  t5_Graph[319] = getDataForUintMassiv(rxData.t5);
  t6_Graph[319] = getDataForUintMassiv(rxData.t6);
  t7_Graph[319] = getDataForUintMassiv(rxData.t7);
  t8_Graph[319] = getDataForUintMassiv(rxData.t8);
  t9_Graph[319] = getDataForUintMassiv(rxData.t9);
  t10_Graph[319] = getDataForUintMassiv(rxData.t10);
  Water_Graph[319] = getDataForUintMassiv8(rxData.Water);
  Electr_Graph[319] = getDataForUintMassiv8(rxData.Electr);
}

// ===================================================
// 16. НАЧАЛЬНАЯ НАСТРОЙКА
// ===================================================
void setup() {
  Serial.begin(9600);
  Serial1.begin(9600);  // Связь с nano
  Serial2.begin(9600);  // Связь с UNO

  // === Настройка дисплея ===
  tft.reset();
  tft.begin(38022);
  tft.invertDisplay(true);

  // === Инициализация графиков ===
  for (int i = 0; i < 320; i++) {
    t1_Graph[i] = t2_Graph[i] = t3_Graph[i] = t4_Graph[i] = 80;
    t5_Graph[i] = t6_Graph[i] = t7_Graph[i] = t8_Graph[i] = 80;
    t9_Graph[i] = t10_Graph[i] = 80;
    Water_Graph[i] = Electr_Graph[i] = 40;
  }

  delay(1000);
  getMainDisplay();

  // Отправляем начальные параметры
  delay(500);
  sendParamsToNano();
}

// ===================================================
// 17. ГЛАВНЫЙ ЦИКЛ
// ===================================================
void loop() {
  static uint32_t lastGet = 0;        // для исчисления времени последних полученных данных
  static uint32_t lastSend = 0;       // для периодической отправки команд

  selectDisplay(); // моментальная смена изображения на дисплее

  // === 1. Приём данных от UNO ===
  if (Serial2.available()) {
    DataFromUNO window;
    if (Serial2.readBytes((uint8_t*)&window, sizeof(window))) {
      rxDataFromUNO = window;
      kindDisplay = rxDataFromUNO.Display;
    }
  }

  // === 2. Приём данных от nano ===
  receiveFromNano();

  // === 3. Периодическая отправка параметров на nano (каждые 2 секунды) ===
  if (millis() - lastSend >= 2000) {
    sendParamsToNano();
    lastSend = millis();
  }

  // === 4. Формирование команд для реле (ваша логика) ===
  // (этот блок можно выполнять раз в 5 секунд или по событию)
  static uint32_t lastControl = 0;
  if (millis() - lastControl >= 5000 && gettingFrom_nano == true) {
    
    //-------------- проверка работы реле ---------------
    cycleCount++;
    if (cycleCount > 24)
      bitSet(output_from_Mega_to_nano, 0); // включить дизельный котёл и насос дизельного котла
    if (cycleCount > 22)
      bitSet(output_from_Mega_to_nano, 1); // включить ТЭН 2,5 кВт
    if (cycleCount > 20)
      bitSet(output_from_Mega_to_nano, 2); // включить ТЭН 7,5 кВт
    if (cycleCount > 18)
      bitSet(output_from_Mega_to_nano, 3); // открыть шаровой кран
    if (cycleCount == 17)
      actuatorMove("DOWN", 20);
    else
      bitClear(output_from_Mega_to_nano, 7);
    if (cycleCount == 15)
      actuatorMove("UP", 20);
    else
      bitClear(output_from_Mega_to_nano, 8);
    if (cycleCount > 12)
      bitSet(output_from_Mega_to_nano, 6); // включить вентилятор наружного блока
    if (cycleCount > 10)
      bitSet(output_from_Mega_to_nano, 4); // включить насос радиаторов
    if (cycleCount > 8)
      bitSet(output_from_Mega_to_nano, 5); // включить насос тёплых полов
    // if (cycleCount > 6)
    //   bitSet(output_from_Mega_to_nano, 9); // включить компрессор
    // if (cycleCount > 4)
    //   bitSet(output_from_Mega_to_nano, 10); // включить четырёхходовый клапан
    if (cycleCount > 2)
      bitSet(output_from_Mega_to_nano, 11); // включить насос пластинчатого теплообменника

    if (cycleCount > 26) {
      cycleCount = 0;
      output_from_Mega_to_nano = 0b0000000000000000;
    }
    //-------------- (проверка работы реле) ---------------

    lastControl = millis();
    gettingFrom_nano = false;
  }

  // === 5. Фиксация потери связи ===
  if (millis() - lastGet > 15000) { // если в течение 15 сек. не было связи с nano
    bitSet(mistakes, 13); // зафиксирована потеря связи с nano
    mistake_13_Count++;
    lastGet = millis(); // попытка связи с nano
    if (mistake_13_Count > 3) {
      bitSet(mistakes, 12); // зафиксирована продолжительная потеря связи с nano
      output_from_Mega_to_nano = 0; // аварийный останов
    }
    showMistakes();
  }
}
Основные изменения в коде:
1. Исправлена проверка CRC (самое важное):
На отправке: crc = crc8(&data, sizeof(data) - 1)

На приёме: if (crc8(&rx, sizeof(rx) - 1) == rx.crc)

2. Добавлена проверка на полный пакет:
if (Serial.available() >= sizeof(DataFromMega)) — ждём полный пакет, чтобы не читать частичные данные.

3. Установлен таймаут для чтения:
Serial.setTimeout(50) — не блокируем цикл более 50 мс.

4. Mega теперь отправляет команды каждые 2 секунды (независимо от приёма):
Это обеспечивает постоянное «сердцебиение» для Nano.

5. Создана отдельная функция receiveFromNano() для лучшей организации кода.
6. Исправлена логика сброса таймаута:
При ошибке CRC время последнего приёма не обновляется, что правильно.

Объясняю на пальцах. Использование serial интерфейса в железках - крайний случай. Сам интерфейс имеет только контроль четности. Поэтому применяется для отладки. При заливке - это отдельная песня, поскольку данный режим - монопольный и не прерывается. Применение wifi это не просто избавление от проводов, а использование TCP/IP протокола, где средств контроля передачи на порядок больше. И еще преймущесво в том, что это не связь точка-точка, а сеть из устройств. Это позволяет использовать штатные средства связи с ПК, смартфонами и другими устройствами, которые имеют сетевые возможности. Если вы поборите в себе этот порог непонимания, то дальше для вас открываются средства связи с различными фишками. Например можно диагностику отправлять в мессанжеры, если устройства будут иметь доступ, который может быть посредством тонкого соединения по gsm (мобильного интернет) с применением модулей gsm или роутера с gsm модемом. Это удобно.

Кто же первым вернёт ТС на землю ?

Никто. Такая уж судьба - все зависит от силы удара граблей, которые аккуратно разложены.

пусть развлекается, надо же чем-то заниматься