На этот раз хочу описать контроллер и обвес для котельной, который я с переменным успехом ваяю уже с полгода. ESP32, Kinkony A16, Modbus, тепломер, Arduino в слейв-режиме по i2c, ШИМ-управляемые насосы, дашборд, ну и MQTT. Тупой газовый котёл, гидрострелка, тёплые полы. Если эти слова вам близки, или хотя бы интересны - может и моя статья пригодится.

Начиналось всё как всегда, со статьи на Хабре. Кажется вот этой: Kincony KC868-A16: контроллер 16-го уровня. Ну или даже нет, если быть совсем точным, началось всё с комментария в одной из статей, откуда я узнал что эта железка на озоне продается дешевле полутора тысяч рублей. В тот момент я еще не очень понимал, зачем мне эта штуковина, но решил взять, просто чтобы было.

Дальше кинкони прилегла в долгий ящик, пришла зима, а с ней - некоторый дубак в цеху, где я работаю. Дубак удивительный - там на 400 квадратных метров стоит котёл 80 кВт, так что по всем прикидкам должен справляться. К сожалению, котёл еще и очень умный и дорогой, инсталляторы закрыли его и запретили в него лазить от слова совсем - по осени они приезжали, обслуживали его, тупо включали на 70 градусов и уезжали. Но в цеху-то холодно. И я решил посмотреть - что же реально происходит в теплосистеме.

Купил WirenBoard MAI6, 2 датчика давления, 2 датчика температуры, достал из загашника водосчетчик, и все собрал. Оказалось что инсталляторы по осени почистили так себе, перепад давлений на обратке и подаче - полтора очка, поток - меньше куба, и котел банально не отдает больше 40 кВт. Натыкал в готовые цифры инсталляторов, полечили, потеплело. Народ возрадовался и возжелал такую систему сгородить для всех котлов в компании, я принял к сведению. В процессе поиска водосчетчиков на поток побольше, обнаружил в продаже на том же озоне теплосчетчик Эко Ном СТУ-20 RS485 - готовый прибор, который сочетает ультразвуковой датчик расхода воды с 2 термометрами и контроллером, при этом умеет отдавать накопленные данные в RS485. На тот момент он еще и стоил 3650, так что я купил несколько штук на работу, ну и себе одну штучку - на побаловаться и интегрировать в свой УД.

Надо сказать, что когда я подключал газ и ставил котел, я не думал об автоматизации вообще. Поэтому взял то, что всунули монтажники - Celtic DS Platinum, он же Seoul Master Gas, он же еще несколько наименований. Котел туп как пробка, под автоматизацию не задуман, только встроенная автоматика с выносным пультом на проприетарном протоколе. Поэтому дома я оказался в уже знакомой по работе ситуации - котёл есть, но в плане интеграции его как бы и нет.

Решил повторить уже пройденный на работе опыт. Но наверное логично не тянуть RS485 из котельной до сервера, а ставить в котельной локальный контроллер, который соберёт все данные, отправит их, примет команды, и поставит их на исполнение.

Гидравлическая часть

Сердце системы - котёл Celtic DS Platinum 3.35 номинальной мощностью 40,7 кВт. Брал с запасом, дом 250 квадратов, но запас карман не тянет.

От котла - гидрострелка на 2 контура, 1 и 2 этаж. Второй этаж не эксплуатируется, контур не подключен. На обратках контуров непосредственно перед гидрострелкой - стандартные трехскоростные насосы.

Каждый контур - только тёплые полы, на каждый этаж по 6 контуров, расходомеры, электротермические приводы головок на каждом контуре.

Обращаю внимание: никаких трехходовых клапанов в принципе. Поскольку все отопление на теплых полах, нет нужды обеспечивать несколько температурных режимов, по наивности своей я думал что котёл мне отдаст сколько попрошу теплоносителя сносной для ТП температуры - градусов 45-50, дальше в гидрострелке подмешаю. О наивность!

Формулируем ТЗ

И что нам нужно, и что мы имеем в котельной? Ну ок, давайте списком:

  1. Температура подачи и обратки

  2. Поток теплоносителя

  3. Давление в системе отопления

  4. Мощность котла

Раз уж мы в котельной - можно оглянуться вокруг и добавить:

  1. Импульсы счетчика газа

  2. Импульсы счетчика воды

  3. Давление подачи воды

  4. Температуру в помещении

  5. Температуру на улице

  6. Температур подачи и обратки на вторичных контурах

Ну, вроде кинкони справится. Давления - на AI (и еще 2 останется), счетчики - на DI, температуры - на 1-wire, Эко Ном - на RS485. Можно начинать.

Собираем, итерация 1

Первая итерация - на моей универсальной прошивке для ESP ваяю на коленке дополнительный модуль RS485 для кинкони. Просто мост - принять от Эко Нома по RS485, отдать в IOBroker по Modbus TCP. Первая же проблема - Эко Ном живет на скорости 2400, а я поставил задержку чтения стандартную в 10 мс, и ответ просто не успевал приходить. Вылечил задержкой в 50 мс, данные пошли. Ну, на столе пошли. На радостях собрал для кинкони красивую шкурку из обрезков пластика

Хорошо, докупаем арматуру (ЭкоНом поставляется с термометром для обратки на 1/8", в обычных магазинах тройник 1"-1/8"-1" не встречается, пришлось заказывать), монтируем.

Вешаем кинкони на стену, собираем, подключаем, видим реальность:

22:20:32→ 01 03 00 00 00 0C 45 CF
22:20:32← 01 03 18 00 00 32 2D 00 00 00 00 00 00 0B 15 00 00 0B 11 00 00 00 04 00 00 07 DF 3C C6

Разбираем, и видим вполне вменяемые данные - температуры подачи и обратки, поток теплоносителя, вычисленную мощность, выработанную энергию и так далее. Лепота.

Монтируем датчики давления воды и отопления - на специальный отвод водопроводной гребенки и на гидрострелку соответственно. Датчики - с того же озона, рублей по 750, стандартные 4-20 мА 10 Бар, 1/4".

датчик на гидрострелке под автоматическим сбросником, вид снизу
датчик на гидрострелке под автоматическим сбросником, вид снизу

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

А вот счетчик газа меня сначала привел в замешательство. Как я уже писал, на момент монтажа газового хозяйства я не интересовался автоматизацией, и счетчик Тритон-газ СГМ-4 выглядел примерно как реквизит из Фоллаута. Я уже было начал искать счетчики с импульсным выходом, чтобы поставить последовательно, но мне это все сильно не нравилось - у нас газовщики очень плохо смотрят на самодеятельность в газовой трассе, даже если после счетчика. Благо, сообразил поискать по картинкам. Оказалось что это реплика известной модели, в счетчике есть магнитик в последней циферке, и под него на thingverse есть хренова гора держателей герконов. Но естественно, все они не подошли. Пластилин и скотч мне не понравились, поэтому я взял просто кусок губчатой резины, прижал геркон в посадочном окне в спозиционированном по мультиметру состоянии, и закрыл родную заглушку.

примерка резиновой вставки по высоте
примерка резиновой вставки по высоте

Термодатчки - стандартные DS18B20 в китайском исполнении, завел на разные GPIO, чтобы не путаться и не связываться с привязкой по 1-wire ID.

И вот - почти всё вроде бы работает. Импульсы считаются, темературы на малом круге и в помещении меряются (на трассы до ТП пока решил не ставить). Я начинаю пристально вглядываться в показания и натыкаюсь на

Непреодолимые грабли

Поток через котел по расходомеру - 400 литров в час в прыжке. Как несложно посчитать, при дельте температур в 30 градусов это около 14 кВт. И это всё что мы можем себе позволить со штатным насосом котла. Делать дельту больше - это не вариант для тёплых полов, ножкам будет горячо.

Ок, лечение понятно - дополнительный насос. И заодно поменять насос на контуре - ему надо прокачивать столько же, если не больше, теплоносителя.

Путем беглого гуглежа по существующим на рынке насосам обнаруживаю что китайцы двигают BLDC в массы. И там, где раньше для управления требовался частотник и немаленький асинхронник, теперь стоят компактные, энергоэффективные, а главное - управляемые BLDC. Хорошо, смотрю отзывы, беру на озоне Shimge APE-A 25-6-130 ... и, конечно, промахиваюсь. Оказывается что у Shimge есть APE-A и APE - это разные штуки, и та, которая с A - это не от слова "автоматизируемый", а от слова "абычный". Ну и ладно, все равно мне в котел управление и не нужно - надо просто продавить куб в час. Заказываю для контура ТП уже настоящий APE 32-8-180.

Зато с Shimge APE-A 25-6-130, установленном на малом контуре, в режиме половинной мощности удалось добиться куба с лишним в час. А на полной при включенном в помощь насосе котла - расходомер зашкалило (у него предел 3 м3/час).

Доработка ТЗ

Ну вот, грабли надо интегрировать. Получается что нам надо 2 генератора ШИМ, частота порядка килогерца, и 2 быстрых входа для обратной связи от контурных насосов, частота 75 Гц. У кинкони DI и DO идут через мультиплексоры, поэтому воспользоваться ими не получится. А отдавать на ШИМ дефицитные GPIO не хочется, да и напряжение на них 3.3В, непонятно как насос к этому отнесется.

Кроме того, к этому моменту я уже натанцевался с littleFS и флашами, и решил что я НЕ ХОЧУ логировать данные на самом ESP32, но хочу как минимум не терять данные о накопленных импульсах счетчиков. Поэтому дополняем ТЗ:

  1. управление как минимум 2 ШИМ 1000Гц, прием 2 ШИМ-сигналов 75Гц

  2. ИБП

Arduino часть

Ардуинка подцепляется к Kinkony в штатный i2c разъем. Как я понимаю, это общая шина с мультиплексорами, но ничего страшного, обмен данным идовольно медленный.

Поскольку я домовитый, на ту же ардуинку я решил прикрутить 4 терморезистора NTC 10K, разгрузив 1-wire и создав общий "модуль на 2 контура", каждому контуру по 2 термометра, управлению и фидбеку.

Скетч получился совсем простой, но не с первого раза - клод забыл разделить чтение ADC, показания прыгали.
// ============================================================
//  pump_slave.ino  v1.3
//  Arduino Pro Mini 5V/16MHz — I2C slave для KC868-A16
//
//  Пины:
//    D9  — PWM насос 1 (выход)
//    D10 — PWM насос 2 (выход)
//    D3  — feedback насос 1 (вход, INT1)
//    D2  — feedback насос 2 (вход, INT0)
//    A0-A3 — NTC термометры
//    A4/A5 — I2C SDA/SCL
//
//  I2C адрес: 0x30
//  Serial 9600: команды "1:XX" "2:XX" (duty 0-100)
//
//  v1.3 — два фикса при вводе в эксплуатацию (см. чат с Михаилом,
//  реальный насос + термисторы подключены впервые):
//   1. FB1/FB2 переведены с INPUT на INPUT_PULLUP. Фидбек насоса,
//      похоже, open-collector (просто "притапливает" линию к GND, сам
//      не может выдать чистый HIGH) — без подтяжки цифровой вход не
//      видел настоящих переходов и duty всегда считался 0%. Если
//      фидбек на самом деле push-pull — внутренняя подтяжка (~20-50кОм)
//      слишком слабая, чтобы чему-то помешать, менять безопасно в любом
//      случае.
//   2. NTC: добавлено "прогревочное" чтение перед реальным на каждом
//      канале. Раньше analogRead() шёл по кругу A0→A1→A2→A3 без паузы —
//      классический кроссталк АЦП АVR: заряд конденсатора выборки-
//      хранения от предыдущего (низкоомного, если термистор реально
//      подключён) канала утекает в следующий, если тот висит в воздухе
//      (высокий импеданс, долго "забывает" чужой заряд). Именно поэтому
//      подключение ОДНОГО термистора портило показания на ВСЕХ
//      остальных, независимо от того, в какой канал его воткнули.
// ============================================================

#include <Wire.h>

#define I2C_ADDR   0x30
#define PIN_PWM1   9
#define PIN_PWM2   10
#define PIN_FB1    3   // INT1
#define PIN_FB2    2   // INT0
#define PIN_NTC0   A0
#define PIN_NTC1   A1
#define PIN_NTC2   A2
#define PIN_NTC3   A3

#define PWM_TOP    1999   // 16MHz / (8 * 2000) = 1000 Hz

// ── Состояние ────────────────────────────────────────────────
volatile uint8_t  g_duty1 = 95;
volatile uint8_t  g_duty2 = 95;

// Feedback: измеряем через digitalRead в ISR
volatile uint32_t g_fb1_rise_us      = 0;  // момент последнего RISING
volatile uint32_t g_fb1_last_fall_us = 0;  // момент последнего FALLING
volatile uint32_t g_fb1_high_us      = 0;  // длительность HIGH
volatile uint32_t g_fb1_period_us    = 0;  // период (FALLING → FALLING)

volatile uint32_t g_fb2_rise_us      = 0;
volatile uint32_t g_fb2_last_fall_us = 0;
volatile uint32_t g_fb2_high_us      = 0;
volatile uint32_t g_fb2_period_us    = 0;

volatile uint32_t g_fb1_last_us = 0;  // для детекции тишины
volatile uint32_t g_fb2_last_us = 0;

uint8_t  g_fb1_duty = 0, g_fb2_duty = 0;
uint16_t g_ntc[4]   = {0, 0, 0, 0};
uint8_t  g_tx[10];

const uint8_t NTC_PINS[4] = {PIN_NTC0, PIN_NTC1, PIN_NTC2, PIN_NTC3};

uint32_t g_last_print = 0;
String   g_serial_buf = "";

// ============================================================
//  PWM: Timer1, Fast PWM mode 14, prescaler 8 → 1000 Hz
// ============================================================
uint16_t dutyToOcr(uint8_t duty) {
    if (duty > 100) duty = 100;
    return (uint32_t)duty * PWM_TOP / 100;
}

void pwmSetup() {
    pinMode(PIN_PWM1, OUTPUT);
    pinMode(PIN_PWM2, OUTPUT);
    TCCR1A = (1 << COM1A1) | (1 << COM1B1) | (1 << WGM11);
    TCCR1B = (1 << WGM13)  | (1 << WGM12)  | (1 << CS11);
    ICR1   = PWM_TOP;
    OCR1A  = dutyToOcr(g_duty1);
    OCR1B  = dutyToOcr(g_duty2);
}

void applyDuty() {
    OCR1A = dutyToOcr(g_duty1);
    OCR1B = dutyToOcr(g_duty2);
}

// ============================================================
//  Feedback ISR — определяем фронт через digitalRead
// ============================================================
void fb1_isr() {
    uint32_t now = micros();
    g_fb1_last_us = now;
    if (digitalRead(PIN_FB1)) {
        // RISING — запоминаем начало HIGH
        g_fb1_rise_us = now;
    } else {
        // FALLING — считаем длительность HIGH и период
        if (g_fb1_rise_us > 0)
            g_fb1_high_us = now - g_fb1_rise_us;
        if (g_fb1_last_fall_us > 0)
            g_fb1_period_us = now - g_fb1_last_fall_us;
        g_fb1_last_fall_us = now;
    }
}

void fb2_isr() {
    uint32_t now = micros();
    g_fb2_last_us = now;
    if (digitalRead(PIN_FB2)) {
        g_fb2_rise_us = now;
    } else {
        if (g_fb2_rise_us > 0)
            g_fb2_high_us = now - g_fb2_rise_us;
        if (g_fb2_last_fall_us > 0)
            g_fb2_period_us = now - g_fb2_last_fall_us;
        g_fb2_last_fall_us = now;
    }
}

uint8_t calcDuty(volatile uint32_t& high_us, volatile uint32_t& period_us) {
    uint32_t h, p;
    noInterrupts(); h = high_us; p = period_us; interrupts();
    if (p == 0) return 0;
    uint32_t d = (uint32_t)h * 100 / p;
    if (d > 100) d = 100;
    return (uint8_t)d;
}

// ============================================================
//  I2C
// ============================================================
void onReceive(int bytes) {
    if (bytes < 2) { while (Wire.available()) Wire.read(); return; }
    uint8_t d1 = Wire.read();
    uint8_t d2 = Wire.read();
    while (Wire.available()) Wire.read();
    g_duty1 = constrain(d1, 0, 100);
    g_duty2 = constrain(d2, 0, 100);
    applyDuty();
}

void onRequest() {
    g_tx[0] = g_fb1_duty;
    g_tx[1] = g_fb2_duty;
    for (int i = 0; i < 4; i++) {
        g_tx[2 + i*2]     = g_ntc[i] >> 8;
        g_tx[2 + i*2 + 1] = g_ntc[i] & 0xFF;
    }
    Wire.write(g_tx, 10);
}

// ============================================================
//  Serial команды
// ============================================================
void handleSerial() {
    while (Serial.available()) {
        char c = Serial.read();
        if (c == '\n' || c == '\r') {
            g_serial_buf.trim();
            if (g_serial_buf.length() >= 3 && g_serial_buf[1] == ':') {
                int pump = g_serial_buf[0] - '0';
                int duty = g_serial_buf.substring(2).toInt();
                duty = constrain(duty, 0, 100);
                if (pump == 1) { g_duty1 = duty; applyDuty(); Serial.print("PWM1 = "); Serial.println(duty); }
                if (pump == 2) { g_duty2 = duty; applyDuty(); Serial.print("PWM2 = "); Serial.println(duty); }
            }
            g_serial_buf = "";
        } else {
            g_serial_buf += c;
        }
    }
}

// ============================================================
//  SETUP / LOOP
// ============================================================
void setup() {
    Serial.begin(9600);
    Serial.println("pump_slave v1.3");
    Serial.println("Commands: 1:XX or 2:XX (duty 0-100, >=95=stop)");

    // v1.3: INPUT_PULLUP вместо INPUT — см. changelog вверху файла.
    pinMode(PIN_FB1, INPUT_PULLUP);
    pinMode(PIN_FB2, INPUT_PULLUP);

    pwmSetup();

    attachInterrupt(digitalPinToInterrupt(PIN_FB1), fb1_isr, CHANGE);
    attachInterrupt(digitalPinToInterrupt(PIN_FB2), fb2_isr, CHANGE);

    Wire.begin(I2C_ADDR);
    Wire.onReceive(onReceive);
    Wire.onRequest(onRequest);

    Serial.println("Ready.");
}

void loop() {
    handleSerial();

    // Feedback
    g_fb1_duty = calcDuty(g_fb1_high_us, g_fb1_period_us);
    g_fb2_duty = calcDuty(g_fb2_high_us, g_fb2_period_us);

    // Сброс если сигнал пропал > 100мс
    uint32_t now = micros();
    if (now - g_fb1_last_us > 100000UL) { g_fb1_duty = 0; g_fb1_period_us = 0; }
    if (now - g_fb2_last_us > 100000UL) { g_fb2_duty = 0; g_fb2_period_us = 0; }

    // NTC — v1.3: прогревочное чтение перед реальным, см. changelog вверху
    // файла (кроссталк АЦП между низкоомным подключённым каналом и
    // висящими в воздухе соседними).
    for (int i = 0; i < 4; i++) {
        analogRead(NTC_PINS[i]);          // throwaway
        g_ntc[i] = analogRead(NTC_PINS[i]);
    }

    // Вывод каждые 500мс
    uint32_t ms = millis();
    if (ms - g_last_print >= 500) {
        g_last_print = ms;
        Serial.print("PWM1="); Serial.print(g_duty1);
        Serial.print("% FB1="); Serial.print(g_fb1_duty);
        Serial.print("%  PWM2="); Serial.print(g_duty2);
        Serial.print("% FB2="); Serial.print(g_fb2_duty);
        Serial.print("%  NTC: ");
        for (int i = 0; i < 4; i++) {
            Serial.print(g_ntc[i]);
            if (i < 3) Serial.print(" ");
        }
        Serial.println();
    }

    delay(20);
}
Pro Mini на прошивке. Слева - 2 трехпиновых терминала насоса, четырехпиновый терминал i2c, справа - 4 терминала термисторов. Термисторы подключены однопарной витухой.
Pro Mini на прошивке. Слева - 2 трехпиновых терминала насоса, четырехпиновый терминал i2c, справа - 4 терминала термисторов. Термисторы подключены однопарной витухой.

ИБП

Тут всё просто - берем Step-Up DC 5-24, модуль PowerBank с алиэкспресса, пригоршню терминалов (да, лучше пригоршню - 24В надо не только для кинкони, но и для датчиков, и для Эко Нома), паяем на макетке. И да, я опять соединил все земли и у батарейки нет защиты от переразряда, ну и пусть, не жалко. Из соображений компактности держатель батарейки разместил над модулями, получилось мило, но наверное не стоило так делать, больше не буду.

К сожалению, в процессе изготовления не фотал, только уже установленный. Слева сверху вниз:

- ввод 5В

- выход голый с батареи

- 6 шт GND и 6 шт +24В

Собираем снова, итерация 2

Красивый чОрный корпус отправляется ждать следующего кинкони со следующим проектом. А в дело вступает валявшийся бокс от сигнализационного ИБП. Туда войдут:

  • кинкони

  • импульсный БП 5В

  • платка на макетке из блока PowerBank на TP4056 и DC Step-Up конвертера на 24В

  • гнездо под литиевый аккум 18650

  • кусок дин-рейки под модульное оборудование - вдруг когда-нибудь я захочу туда реле накрутить и рулить чем-нибудь типа насоса.

  • Arduino Pro Mini на макетке

Рефакторим прошивку

Тут будет лирика, не очень относящаяся собственно к проекту - можно было обойтись и без этого. Суть в том, что я ощутил в себе навязчивое желание рефакторить свою модульную прошивку.

Собственно, проблема была в том, что прошивка переросла себя: несмотря на то, что каждый модуль писался отдельным файлом, всё это сводилось в один большой main.cpp, в котором проводился опрос датчиков, пересчёт в физические величины, отрисовывался веб-интерфейс и MQTT. Работало, но каждое новое устройство или новая вкладка в интерфейсе означала правки в нескольких не связанных друг с другом местах, риск что-то сломать повышался (и даже несколько раз реализовывался).

Поскольку контроллер получается нехило разветвленный и включал в себя 3 новых device (kinkony, ekonom, arduino), я решил что пора. Расчехлил по случаю клод коворк, отдал ему старый проект, описал вкратце идею общей шины данных для модулей, ответил на несколько вопросов и пошел заниматься своими делами.

Клод переписал ядро с нуля, добавив шину данных (Bus) по схеме publish/subscribe. API шины прост и незамысловат:

cpp

Bus::declare("p_heat", 100);           // объявить канал, scale=100 (сотые доли)
Bus::write("p_heat", 187);             // записать значение (1.87 бар)
int64_t v = Bus::read("p_heat");       // прочитать текущее
Bus::subscribeAll([](const char* id, int64_t value) {
    // вызывается на КАЖДОЕ изменение любого канала
});

Значения хранятся как int64_t с отдельным множителем (scale) — на ESP32 это дешевле и предсказуемее, чем float, а для котельной гигаточности не нужны. Ну и гонять float через MQTT/JSON/веб - дело такое, кто-нибудь что-нибудь округлит не туда.

Дальше всё строится по одному правилу: устройства только пишут в шину, ничего не знают о том, кто их данные читает. Опрос датчика давления, счётчиков воды/газа, термисторов, насоса - каждый просто публикует свои raw.* каналы. Отдельный слой (BoilerRoomApp) читает эти raw-значения, применяет калибровки (переводит миллиамперы в бар, ADC-код термистора в градусы по бета-модели) и публикует уже физические величины — тоже в шину, под другими именами.

А дальше сколько угодно независимых потребителей одних и тех же данных: веб-дашборд опрашивает /api/channels и рисует таблицу, MQTT-модуль подписан на subscribeAll и шлёт каждое изменение в брокер, вкладка диагностики фильтрует всё, что начинается с raw.. Ни один из этих потребителей не знает про существование остальных и не знает, откуда физически взялось значение — с термистора, с RS485-теплосчётчика или из ADC.

Практический эффект: когда потребовалось добавить дельту температур насосного контура (подача минус обратка) — это оказалось одной новой строчкой Bus::declare и десятком строк расчёта, без правок в веб-интерфейсе или MQTT-модуле — оба сами подхватили новый канал, потому что пробегают по шине в полном объеме.

Из подводных камней: шина живёт в статическом массиве фиксированного размера (без malloc — на микроконтроллере это осознанный выбор), когда для диагностики добавил состояния всех входов и выходов кинкони, упёрся в потолок и некоторое время пытался понять, куда делись уже зарегистрированные данные - каналов шины не хватило, и часть данных скрылись в неизвестность. Вылечилось увеличением количества каналов до 128.

Подключаем насос

Приехавший насос серии APE оснащен трехпиновым разъемом управления и контроля. Судя по инструкции, существует фирменный кабель для подключения - красивый как Охтинский мост, с герметизированным концом для насоса и понятной цветовой маркировкой провода. Но дистрибутор забыл его завести.

Благо, колодец под разъем глубокий и прочный, а шаг и размер пинов внутри точно соответсвуют IDC 2.54, поэтому берем из запасников произвольный ардуинохвост с мамами на стандартную гребенку, наращиваем провод, и втыкаем.

В инструкции есть даже схема разъема. Но подписи к схеме набраны 3-4 мм кеглем, а в моем экземпляре они еще и не пропечатались, поэтому тыкался наобум. Правильный порядок, направление от кабеля питания к краю насоса: PWM signal - GND - Feedback.

Характеристика ШИМ не банальна: скважность 85-10% линейно соответсвует скорости 1-100%, 85-94% - минимальная скорость, 95% и выше - останов, 1-10% - максимальная скорость, 0 - игнорируется ШИМ, насос работает в установленном кнопкой на корпусе режиме. Естественно, на уровне дашборда всё это великолепие отмапили в стандартную шкалу 0-100% и чекбокс ручного режима.

Фидбек удивил еще больше: как я понял, он не про скорость, а про нагрузку, скважность линейно означает потребляемую мощность, 1% = 1 Вт мощности. Но это неточно, надо бы достать ваттметр и проверить.

Ну и вишенкой - сигнал фидбека на открытом коллекторе, из-за чего иногда проскакивают значения около 100%, не стал разбираться и городить физический фильтр, на уровне прошивки ESP заблокировал всё, что больше 75.

Рисуем дашборд

Наверное тут картинки только, даже не знаю что написать можно. Как говорится, "вместо тысячи слов":

И конец
И конец
Диагностика, начало. В конце - сырые данные ADC ардуинки
Диагностика, начало. В конце - сырые данные ADC ардуинки
Вкладка RS485
Вкладка RS485
Вкладка калибровки котельной
Вкладка калибровки котельной

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

Итог, возможно промежуточный

Всё собрано, нигде не капает, контроллер работает в полном составе, система отопления, если не в порядке, то по крайней мере контролируема и диагностируема на лету, если засрется фильтр или котел или еще что-нибудь - можно будет оперативно поправить.

В планах - интеграция насоса ХВС (не знаю даже, может альтернативой будет частотник), насоса подкачки теплоносителя, системы защиты от протечек, ну и еще на что фантазии хватит.

Смета

Проект делался долго и упорно, часть комплектухи валялась по сусекам, цены на них по памяти и могут быть неактуальны. Некоторые вещи (например ящик) я даже не представляю где и почем продаются, цену ставил по внутреннему ощущению. Арматура учитывается только та, которую пришлось реально докупать, тройники в ПП у меня либо были заложены и заглушены, либо валялись в запасе.

Наименование

Количество

Стоимость

Kinkony A16

1 шт

1300

Arduino Pro Mini

1 шт

250

DC Step-up 5-24V

1 шт

170

PowerBank module

1 шт

55

18650 Li-Ion

1 шт

200

БП 5В 3А

1 шт

350

Ящик ИБП

1 шт.

1000

Мелочевка (терминалы, резисторы, держатель батареи)

набор

500

Эко Ном СТУ20-RS

1 шт

3650

Тройник 1" под датчик с ниппелем 1"

1шт

750

Датчик давления PT-506

2 шт

1500

Футорка 1/4"-1/2"

2 шт

150

Термистор NTC 10K

4 шт

48

Гильза 6мм 1/2"

4 шт

840

Счетчик воды

1 шт

800

Геркон для газового счетчика

1 шт

30

Насос Shimge APE-A 25-6

1 шт

6700

Насос Shimge APE 32-8

1 шт

6500

Итого:

24793

Комментарии (2)


  1. Alsig
    31.07.2026 18:22

    Такое специфичное решение для газа и некоторого закрытого объёма, где он может собраться. Я бы поставил пару независимых систем безопасности и сам блок управления на демпферах поставил, для нейтрализации влияния вибрации.

    Без критики, просто доп варианты безопасности.


    1. vbifkol Автор
      31.07.2026 18:22

      Такое специфичное решение для газа и некоторого закрытого объёма, где он может собраться. Я бы поставил пару независимых систем безопасности и сам блок управления на демпферах поставил, для нейтрализации влияния вибрации.

      Кажется я чего-то не понял. Контроллер висит в котельной на бетонной стене. Газа он никак не трогает, его вообще можно за пределы котельной вынести. Куда и зачем демпферы?

      Газовая автоматика конечно висит, родная - анализатор газа с сигнализацией и автоматическим клапаном. Кажется у нее даже есть выход на сигнализацию, но беглый гуглеж не показал протокола и я решил её не подключать к контроллеру. О том что газ перекрыт я узнаю по выравниванию обратки и подачи ниже уровня уставки.