На этот раз хочу описать контроллер и обвес для котельной, который я с переменным успехом ваяю уже с полгода. ESP32, Kinkony A16, Modbus, тепломер, Arduino в слейв-режиме по i2c, ШИМ-управляемые насосы, дашборд, ну и MQTT. Тупой газовый котёл, гидрострелка, тёплые полы. Если эти слова вам близки, или хотя бы интересны - может и моя статья пригодится.Итак, начиналось всё как всегда со статьи на Хабре. Кажется вот этой: Kincony KC868-A16: контроллер 16-го уровня. Ну или даже нет, если быть совсем точным, началось всё с комментария в одной из статей, что эта железка на озоне продается дешевле полутора тысяч рублей. В тот момент я еще не очень понимал, зачем мне эта штуковина, но решил взять, просто чтобы было. Читать далее
На этот раз хочу описать контроллер и обвес для котельной, который я с переменным успехом ваяю уже с полгода. ESP32, Kinkony A16, Modbus, тепломер, Arduino в слейв-режиме по i2c, ШИМ-управляемые насосы, дашборд, ну и MQTT. Тупой газовый котёл, гидрострелка, тёплые полы. Если эти слова вам близки, или хотя бы интересны - может и моя статья пригодится.
Дисклеймер: статья AI-Free, весь текст авторский. Кроме кода.
Начиналось всё как всегда, со статьи на Хабре. Кажется вот этой: 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, дальше в гидрострелке подмешаю. О наивность!
Формулируем ТЗИ что нам нужно, и что мы имеем в котельной? Ну ок, давайте списком:
Температура подачи и обратки
Поток теплоносителя
Давление в системе отопления
Мощность котла
Раз уж мы в котельной - можно оглянуться вокруг и добавить:
Импульсы счетчика газа
Импульсы счетчика воды
Давление подачи воды
Температуру в помещении
Температуру на улице
Температур подачи и обратки на вторичных контурах
Ну, вроде кинкони справится. Давления - на 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, но хочу как минимум не терять данные о накопленных импульсах счетчиков. Поэтому дополняем ТЗ:
управление как минимум 2 ШИМ 1000Гц, прием 2 ШИМ-сигналов 75Гц
ИБП
Ардуинка подцепляется к 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 терминала термисторов. Термисторы подключены однопарной витухой.
ИБП
Тут всё просто - берем Step-Up DC 5-24, модуль PowerBank с алиэкспресса, пригоршню терминалов (да, лучше пригоршню - 24В надо не только для кинкони, но и для датчиков, и для Эко Нома), паяем на макетке. И да, я опять соединил все земли и у батарейки нет защиты от переразряда, ну и пусть, не жалко. Из соображений компактности держатель батарейки разместил над модулями, получилось мило, но наверное не стоило так делать, больше не буду.
К сожалению, в процессе изготовления не фотал, только уже установленный. Слева сверху вниз:
- ввод 5В
- выход голый с батареи
- 6 шт GND и 6 шт +24В
Красивый чОрный корпус отправляется ждать следующего кинкони со следующим проектом. А в дело вступает валявшийся бокс от сигнализационного ИБП. Туда войдут:
кинкони
импульсный БП 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 ардуинки

Вкладка 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 |
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Умный флорариум на esp32: от корпоративной оптимизации до дедовской клизмы | 0 | 11.54 | 24-07-2026 |
| 2 | Модульная автономная DIY‑гидропоника на 3D‑принтере. Часть 2 | 0 | 11.14 | 30-07-2026 |
| 3 | Чат на сайте стоит дороже, чем кажется: как я замерил его цену и переписал виджет под результат | 0 | 9.58 | 28-07-2026 |
| 4 | Обычному инженеру в интернете места нет | 0 | 8.48 | 28-07-2026 |
| 5 | Как сделать игру для ZX Spectrum в 2026 (и зачем) | 0 | 8.57 | 05-08-2026 |
| 6 | Napominator Home: Полное руководство по установке, настройке и использованию | 0 | 6.19 | 21-07-2026 |
| 7 | Модульная автономная DIY-гидропоника на 3D-принтере. Часть 1 | 2 | 7 | 25-06-2026 |
| 8 | Klipper: опенсорс без сообщества, или почему у вашего принтера никогда не будет тулченджера из коробки | 0 | 11.18 | 24-07-2026 |
| 9 | Интерактивный CLI-менеджер процессов MySQL на асинхронном Python | 0 | 9.09 | 22-07-2026 |
| 10 | Приборы учета КМ | 0 | 5 | 06-11-2018 |