Pull to refresh

Полный цикл разработки IoT устройства управления подогревом бассейна на ESP8266 в среде Arduino

Reading time 17 min
Views 39K

В данной публикации я поделюсь опытом о создании IoT устройства с ноля: от появления идеи и воплощении ее в «железе» до создания микропрограммы для контроллера и web-интерфейса для управления созданным устройством через сеть интернет.


До создания этого устройства я:


  • Почти не разбирался схемотехнике. Только на уровне принципов работы
    резистора/транзистора… Я не имел никакого опыта в создании сколь-нибудь сложных схем.
  • Никогда не проектировал печатных плат.
  • Никогда не паял SMD компонент. Уровень владения паяльником был на уровне припаивания проводов и какого-нибудь реле.
  • Никогда не писал таких сложных программ для микроконтроллера. Весь опыт был на уровне «зажги светодиод в Arduino», а контроллер ESP8266 я встретил впервые.
  • Совсем немного писал на C++ для «большого брата», но это было более десятка лет назад и все давно забылось.

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


Постановка задачи


Я живу в частном доме под Минском, и собственный бассейн, пусть и простейший каркасный, является неотъемлемой частью того набора «бенефитов», который получают многие, живущие в загородном доме. В нашем нестабильном климате оказалось, что в бассейне купаться некомфортно, если он стоит на открытом воздухе: вода выхолаживается ночью, а ветренная погода днем не делает купание комфортным. В прошлом году я своими руками построил геодезический купол фуллера над бассейном, поставил горочку и повесил тарзанку – дети довольны.



Фотоотчет строительства купола на Flickr.


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


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



Проходя через теплообменник, теплоноситель, нагретый котлом, с температурой около 70-90 °C отдает тепло воде из бассейна, нагревая ее на пару градусов. Сам теплоноситель при этом остывает на пару десятков градусов, и возвращается в котел с тем, чтобы быть снова
подогретым. Соотношение остывания воды от котла с нагревом воды бассейна зависит от многих факторов: мощности теплообменника и скорости циркуляции воды в первичном и вторичных контурах.


Трубы, подведенные от бассейна к теплообменнику – обычные полиэтиленовые, те, которые
в настоящее время применяются для подвода холодной воды в частные дома. Дешевизна, способность выдержать приличное давление, отсутствие коррозии – вот основные достоинства таких труб. Для всех без исключения полиэтиленовых труб рабочая температура ограничена 40 градусами по шкале Цельсия. В принципе, для бассейна этого более чем достаточно.


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


Необходимо предусмотреть возможность защиты перегрева теплообменника.


Быстрое решение


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


Контролировать перегрев только по датчикам температуры невозможно: система обладает большой инерционностью: после внезапного останова воды в контуре бассейна, при
отключении котла, температура еще продолжает какое-то время расти, т.к. котел все еще по инерции гоняет подогретую воду по контуру, не допуская перегрева «себя, любимого».


Поэтому важно среагировать как можно раньше: а именно по останову потока воды в контуре
бассейна.


Датчик потока был использован такой. Пластиковый корпус и отсутствие контакта самого сенсора с водой позволяет использовать его в подсоленной воде.


Датчики температуры было решено использовать Dallas DS18B20, их легко подключить сразу несколько штук на одной шине 1-Wire.



Решено было повесить по паре датчиков на вход и выход как вторичного, так и первичного
контура: всего 4 датчика. Дополнительным преимуществом такого подхода является
возможность мониторинга параметров системы: можно отслеживать насколько охлаждается теплоноситель в первичном контуре и насколько нагревается вода из бассейна во вторичном контуре. А значит – отслеживать оптимальность нагрева и прогнозировать время включения нагрева.


Места установки датчиков на теплообменнике и подводящих трубах

Параметры устройства


Первый прототип устройства был собран на базе Arduino Uno, и успешно запущен.



Но тут же выяснилось, что хотелось бы большего. Подогрев 16 кубометров воды даже всего на
несколько градусов – дело не быстрое. И хотелось бы прямо с работы отслеживать параметры нагрева, включать/выключать его. А заодно интересно было бы снимать графики нагрева, например за сутки.


Ну и раз мы уже получаем IoT устройство, то почему бы нам не контролировать заодно удаленное включение хлоратора бассейна и насоса для него?


Техзадание


Итак, было решено разработать устройство – многофункциональный контроллер бассейна. Он должен уметь:


  • Управлять подогревом бассейна через теплообменник, включая/выключая газовый котел для нагрева воды.
  • Не допускать перегрева теплообменника, контролируя наличие потока воды бассейна во вторичном контуре и превышения температуры вторичного контура.
  • Отображать статистику нагрева в реальном времени (температура на входе и выходе обоих контуров).
  • Записывать (логгировать) значения температур во флеш-память. Отображать данные за
    определенный период в виде графика.
  • С помощью реле уметь включать/выключать насосы бассейна и хлоратор.
  • Управлять всеми параметрами устройства дистанционно, через встроенный микро-веб сервер.

Было еще искушение прикрутить Blink, MQTT. Но от этих «наворотов» на первом этапе
было решено отказаться. И тем более, не хотелось бы выносить возможность управления куда-то наружу. Встроенного веб сервера для моих целей вполне достаточно. А безопасность обеспечивается тем, что в домашнюю сеть из внешнего мира можно войти только через VPN.


Аппаратная часть


В качестве контроллера было решено использовать дешевый и популярный ESP8266. Он отлично подходил для моих целей, кроме одного момента: согласования уровней сигналов 5-вольтовых датчиков с 3.3 вольтовой логикой контроллера. В принципе, датчики Dallas вроде бы работают и на 3 вольтах, но у меня от контроллера до датчиков достаточно длинная линия, около 7 метров. Поэтому лучше напряжение повыше.


Было определено, что необходимо иметь по «железу»:


  • Контроллер ESP8266 или его старший брат ESP32 (в виде модуля DevKit).
  • Согласование уровней сигналов для датчиков.
  • Регулятор питания 5-вольтовой частью схемы.
  • Модуль управления реле.
  • Часы RTC + флеш память для записи логов.
  • Простейший 2-строчный LCD дисплей для отображения текущих значений датчиков и состояния прибора и реле.
  • Несколько физических кнопок, для управления состоянием устройства без доступа через web.

Многие компоненты из списка продаются в виде модулей для Arduino и многие модули совместимы с логикой 3.3в. Однако «слепливать» все это на макетной плате пучками проводов не хотелось, ведь хочется иметь аккуратный красивый «девайс». Да и за деньги, отданные китайцам за модули можно вполне нарисовать и заказать свою индивидуальную печатную плату, а ожидание её приезда будет компенсировано сравнительно быстрым и надежным монтажом.


Еще раз замечу, что это мой первый опыт в схемотехнике и в конструировании аппаратной части подобных вещей. Пришлось много учиться. Ведь по специальности я немного в стороне от микроконтроллеров. Но делать все «на коленках» не позволил живущий во мне дух перфекционизма.


Принципиальная схема


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


Первая же сложность с которой я столкнулся: вариантов DevKit контроллера ESP8266 или ESP32 существует достаточно много, некоторые из них отличаются расположением выводов и их назначением, а некоторые даже по ширине. Решено было разрисовать схему так, чтобы можно было поставить DevKit любой ширины и с любым расположением выводов, а по сторонам от него — по 2 ряда парных отверстий-перемычек, и впоследствии проводками соединить нужные выводы, применительно к конкретно купленному контроллеру.


Место под контроллер и 2 ряда парных перемычек: JH1 и JH2 на схеме:



Расположение пинов входа 5v и выхода 3.3v питания встроенного стабилизатора, а также GND мне показались однотипными у разных DevKit, но все же я решил перестраховаться и тоже сделать их перемычками: JP1, JP2, JP3 на схеме.


Перемычки со стороны подсоединения их к компонентам схемы, я решил подписать функциями, которые они вероятно будут выполнять.


А вот как это выглядит вместе с DevKit ESP8266, который я в итоге купил и поставил

Здесь D1 (GPIO5) и D2 (GPIO4) отвечают за I2C шину, D5 (GPIO14) за 1-Wire, D6 (GPIO12) – за получение импульсов с датчика потока.


Принципиальная схема:



(изображение кликабельно)


Несмотря на наличие на борту ESP8266 встроенного стабилизатора питания на 3.3в, нам еще нужно иметь 5 вольт для питания датчиков и LCD, и 12 вольт для питания реле. Решено было сделать питание платы 12 вольтовым, а на вход поставить регулятор напряжения AMS1117-5.0, дающий на выходе нужные 5 вольт.


Для согласования уровней сигналов по шине 1-Wire я использовал полевой транзистор BSS138 c с «подтяжками» по напряжению с обеих сторон.



Очень хорошо про согласование уровней написано в статье Согласование логических уровней 5В и 3.3В устройств.


Для согласования уровней сигнала датчика потока я использовал просто делитель напряжения на резисторах. Датчик потока – это просто устройство с открытым коллектором. В некоторых датчиках уже может быть встроенный резистор для подтяжки, это нужно учитывать:



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


На I2C шину я повесил часы реального времени DS3231SN и флеш-память AT24C256C для хранения логов. Встроенная в ESP8266 флеш-память не годится, потому что обладает малым количеством циклов перезаписи (10 тыс. против 1 миллиона у AT24Cxxx, если верить даташитам).


Управление реле организовано на связке чипов PCF8574AT и ULN2803A.



Первый чип представляет собой I2C расширитель портов микроконтроллера. Состояние активного выхода или входа PCF8574AT выбирается выбором адреса по шине I2C.
Чип имеет некоторые интересные особенности, хорошо описанные в статье I2C расширитель портов PCF8574.


Напрямую нагрузкой (реле) управлять чип не может. Для этого используется транзисторная матрица ULN2803A. Есть особенность: матрица легко может притянуть свои выходы с нагрузкой к земле, а это значит, что, если на второй полюс реле будет подано напряжение питания, по обмотке реле потечет ток и контакты реле замкнутся. К сожалению, при таком включении мы получаем побочный эффект: значение сигнала от контроллера инвертируется, и все реле «перещелкиваются» при включении схемы. Я пока не придумал, как убрать эту особенность.


Подробнее про чип описано здесь.


Расширитель портов PCF8574AT так же может использоваться на вход: на часть входов к нему можно повесить аппаратные кнопки, считывая их значения по шине I2C. На схеме пины 4-7 могут быть использованы для чтения состояния кнопок. Главное не забыть программно включить встроенную подтяжку соответствующих ног к питанию.


В то же время я оставил разводку и на транзисторную матрицу, на случай если вдруг захочется подключить дополнительные реле. Для возможных подключений все выводы я завел на коннекторы (точнее на отверстия под них, куда могут быть подпаяны провода или впаян стандартный 2.54 мм DIP разъем).


Пин INT расширителя портов может быть использован для быстрой реакции на нажатие кнопки. Его можно подсоединить к свободному порту на контроллере и установить сработку прерывания по изменению состояния этого пина.


Двухстрочный LCD дисплей так же управляется через расширитель PCF8574AT. Основной момент: питание дисплея осуществляется от 5 вольт, в то время как сам дисплей управляется 3-вольтовой логикой. К слову, стандартные Arduino-адаптеры для I2C не рассчитаны на двойное напряжение. Идею такого подключения я нашел где-то на просторах интернет, к сожалению, ссылку я потерял, поэтому не привожу первоисточник.


Печатная плата


При проектировании платы выяснилось, что обычные детали с ножками занимают слишком много места, а многие чипы в DIP исполнении непросто найти. Почитав в интернете, что SMD монтаж не так уж сложен, а при должной сноровке даже менее трудоемок, я решил проектировать плату под SMD детали. И не ошибся. Получилась компактная, красивая плата, куда я с легкостью разместил все, что мне нужно. SMD детали, при наличии хорошего паяльника, флюса и припоя, оказалось действительно очень несложно монтировать.


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


Печатную плату я сделал размером 97x97 мм. Она с легкостью помещается в стандартную разделочную электро-коробку. Кроме того, платы размерами менее 100х100 дешевы в изготовлении. Изготовление минимальной партии из 5 плат по разработанному макету обошлось в 5 USD, еще около 9 USD стоила их доставка в Беларусь.



Проект платы лежит на сайте EasyEDA и доступен каждому желающему.


Замечу, что на фото контроллера ниже фигурирует первый образец платы, на который я «наворотил» еще много чего лишнего и ненужного (в надежде использовать эту минимальную партию из 5 плат и в других проектах). Здесь же и на EasyEDA я выложил «почищенный» от всех этих ненужностей вариант.



Фотографии обеих сторон платы

Лицевая сторона:



Тыльная сторона:



Программная часть


Для программирования микроконтроллера, учитывая задел в виде прототипа на Arduino Uno, было решено использовать среду Arduino с установленной ESP8266 Arduino Core. Да, на ESP8266 можно использовать Lua, но, говорят, бывают подвисания. Мне же, учитывая критически важную выполняемую функцию, совсем бы этого не хотелось.
Сама среда Arduino мне кажется немного морально устаревшей, но, к счастью, есть расширение для Visual Studio от Visual Micro. Среда позволяет использовать подсказки IntelliSence по коду, быстро переходить к объявлениям функций, рефакторить код: в общем все то, что позволяет себе среда для «взрослых» компьютеров. Платная версия Visual Micro позволяет еще и удобно отлаживать код, но я довольствовался бесплатным вариантом.

Структура проекта


Проект состоит из следующих файлов:
Структура проекта в Visual Studio

Файл Назначение
WaterpoolManager.ino
Объявление основных переменных и констант. Инициализация. Главный цикл.
HeaterMainLogic.ino
Основная логика управления реле котла (по температурам) и вспомогательными реле.
Sensors.ino
Считывание данных сенсоров
Settings.ino
Настройки устройства, сохранение их в флеш-памяти контроллера
LCD.ino
Вывод информации на LCD
ClockTimer.ino
Считывание показаний часов RTC, или симуляция часов
Relays.ino
Управление включением/выключением реле
ButtonLogic.ino
Логика реакции на состояния аппаратных кнопок
ReadButtonStates.ino
Считывание состояний аппаратных кнопок
EEPROM_Logging.ino
Логгирование данных датчиков в EEPROM
WebServer.ino
Встроенный веб-сервер для управления устройством и отображением состояний
WebPages
В этой папке хранятся страницы веб-сервера
index.h
Основная страница отображения состояния устройства. Идет считывание текущего состояния с помощью вызова ajax. Refresh каждые 5 секунд.
loggraph.h
Выводит лог данных датчиков и состояний реле в виде графика. Используется библиотека jqPlot – все построение происходит на стороне клиента. Запрос к контроллеру идет лишь на бинарный файл – копии данных из EEPROM.
logtable.h
тоже, но в виде таблицы
settings.h
Управление настройками устройства: установка пределов по температурам, потоку воды, периодичности логгирования данных
time.h
Установка текущего времени

Библиотеки
EepromLogger.cpp
Библиотека записи логов во флеш
EepromLogger.h
crc8.cpp
Подсчет 8-битного CRC для библиотеки
crc8.h
TimeSpan.cpp
Структура для управления отрезками времени
TimeSpan.h

Опрос датчиков


При старте устройства происходит поиск датчиков температуры по шине OneWire и занесение их адресов в массив tempSensAddr. Датчики заносятся в порядке их отклика по шине и порядок в дальнейшем не меняется. Запоминается индекс последнего датчика в массиве (устройство умеет работать с 4мя или меньшим количеством датчиков):


while (ds.search(tempSensAddr[lastSensorIndex]) && lastSensorIndex < 4)
{
       Serial.print("ROM =");
       for (byte i = 0; i < 8; i++) {
              Serial.print(' ');
              Serial.print(tempSensAddr[lastSensorIndex][i], HEX);
       }
       if (OneWire::crc8(tempSensAddr[lastSensorIndex], 7) != tempSensAddr[lastSensorIndex][7]) {
              Serial.print(" CRC is not valid!");
       }
       else
              lastSensorIndex++;
       Serial.println();
}
ds.reset_search();
lastSensorIndex--;
Serial.print("\r\nTemperature sensor count: ");
Serial.print(lastSensorIndex + 1, DEC);
Кроме того, с целью диагностики опрашивается их состояние (температуры). Основные данные при инициализации выводятся в Serial и на LCD для диагностики:
// Read sensor values and print temperatures
ds.reset();
ds.write(0xCC, TEMP_SENSOR_POWER_MODE); // Request all sensors at the one time
ds.write(0x44, TEMP_SENSOR_POWER_MODE); // Acquire temperatures
delay(1000); // Delay is required by temp. sensors

char tempString[10];
for (byte addr = 0; addr <= lastSensorIndex; addr++) {
       ds.reset();
       ds.select(tempSensAddr[addr]);
       ds.write(0xBE, TEMP_SENSOR_POWER_MODE); // Read Scratchpad
       tempData[addr] = ds.read() | (ds.read() << 8); // Read first 2 bytes which carry temperature data
       int tempInCelsius = (tempData[addr] + 8) >> 4; // In celsius, with math rounding
       Serial.print(tempInCelsius, DEC); // Print temperature
       Serial.println(" C");
}

Согласно даташиту, датчики требуют не менее 750 ms задержки между запросом значения температуры и получением ответа от датчика. Поэтому в коде введена задержка с небольшим запасом.


Однако, эта задержка, когда всё устройство просто ждет ответа, приемлема на старте, но абсолютно неуместно ждать каждый раз при регулярном опросе датчиков. Поэтому был написан следующий хитрый код, вызываемый каждые 50 ms по таймеру:


#define TEMP_MEASURE_PERIOD 20 // Time of measuring, * TEMP_TIMER_PERIODICITY ms
#define TEMP_TIMER_PERIODICITY 50 // Periodicity of timer calling, ms

timer.attach_ms(TEMP_TIMER_PERIODICITY, tempReadTimer);
int tempMeasureCycleCount = 0;

void tempReadTimer() // Called many times in second, perform only one small operation per call
{
       tempMeasureCycleCount++;
       if (tempMeasureCycleCount >= TEMP_MEASURE_PERIOD) {
              tempMeasureCycleCount = 0; // Start cycle again
       }

       if (tempMeasureCycleCount == 0)
       {
              ds.reset();
              ds.write(0xCC, TEMP_SENSOR_POWER_MODE); // Request all sensors at the one time
              ds.write(0x44, TEMP_SENSOR_POWER_MODE); // Acquire temperatures
       }
       // Between phases above and below should be > 750 ms
       int addr = TEMP_MEASURE_PERIOD - tempMeasureCycleCount - 1;
       if (addr >= 0 && addr <= lastSensorIndex)
       {
              ds.reset();
              ds.select(tempSensAddr[addr]);
              ds.write(0xBE, TEMP_SENSOR_POWER_MODE); // Read Scratchpad
              tempData[addr] = ds.read() | (ds.read() << 8); // Read first 2 bytes which carry temperature data
       }
}

В начале каждого цикла tempMeasureCycleCount происходит запрос к датчикам на чтение их значений. После того как проходит около 50 таких циклов (а в сумме это 50*20 = 1000 ms = 1 sec), происходит чтение значения каждого датчика, по одному за раз. Вся работа разбита на кусочки, чтобы код, работающий в прерывании по таймеру, не отнимал много времени у контроллера.


Значение датчика потока вычисляется следующим образом. По прерыванию на пине, на котором висит датчик мы увеличиваем значение счетчика тиков, пришедших с датчика потока:


pinMode(FLOW_SENSOR_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flow, RISING); // Setup Interrupt
volatile int flow_frequency; // Flow sensor pulses
int flowMeasureCycleCount = 0;

void flow() // Flow sensor interrupt function
{
              flow_frequency++;
}

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


flowMeasureCycleCount++;
if (flowMeasureCycleCount >= 1000 / TEMP_TIMER_PERIODICITY)
{
       flowMeasureCycleCount = 0;
       litersInMinute = (flow_frequency / FLOW_SENSOR_CONST); // Pulse frequency (Hz) = FLOW_SENSOR_CONST*Q, Q is flow rate in L/min.
       flow_frequency = 0; // Reset Counter
}

Логгирование данных от датчиков и о состоянии прибора


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


После некоторого «кумекания» была придумана и реализована следующая модель записи:



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


struct LogEvent
{
       unsigned char litersInMinute = 0;
       unsigned char tempCelsius[4]{ 0, 0, 0, 0 };
       unsigned char deviceStatus = 0;
}

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


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


unsigned int logRecordsInBlock = 60 * 60 / loggingPeriodSeconds;     // 1 block for hour
unsigned int  block_size = sizeof(Block_Header) + logRecordsInBlock * (record_size + crcSize);
unsigned int  block_count = total_storage_size / block_size;

Например, при периодичности логгирования раз в 30 секунд, мы будем иметь 120 записей в блоке, а размер блока будет равен около 840 байт. Всего у нас поместиться 39 блоков в памяти флешки размером в 32 килобайта. При такой организации получается, что каждый блок начинается по строго определенному адресу в памяти, и «пробежать» по всем блокам – не проблема.


Соответственно, при внезапном обрыве записи при прошлом выключении устройства, мы будем иметь недописанный блок (т.е. в котором отсутствует часть записей). При включении устройства, алгоритм ищет последний по времени валидный заголовок блока (timestamp+crc). И продолжает запись, начиная со следующего блока. Запись осуществляется циклически: самый свежий блок перезатирает данные самого старого блока.


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


LCD


В устройстве использован дисплей QC1602A, способный отображать 2 строчки по 16 символов. В первой строке выводится актуальная информация о текущих значениях датчиков: потока и температурах. В случае превышения заданного лимита, возле значения появляется восклицательный знак. Вторая строчка показывает состояния реле нагрева и насоса, а также время, прошедшее с момента включения или выключения нагрева. Каждые 5 секунд дисплей во второй строке кратковременно показывает актуальные лимиты. Фотографии дисплея в различных режимах приведены в конце публикации.


Графики


При запросе через встроенный веб-сервер данные логгинга читаются в бинарном виде с помощью JavaScript:


var xhttp = new XMLHttpRequest();
xhttp.open("GET", "logs.bin", true);
xhttp.responseType = "arraybuffer";
xhttp.onprogress = updateProgress;
xhttp.onload = function (oEvent) {
  var arrayBuffer = xhttp.response;
  if (arrayBuffer) {
       var byteArray = new Uint8Array(arrayBuffer);
                …
}};
xhttp.send(null);

Чтение их в каком-нибудь популярном недвоичном формате, например ajax, было бы непозволительной роскошью для контроллера, прежде всего из-за большого объема, который должен был бы вернуть встроенный http сервер.


По этой же причине, для построения графиков используется JavaScript библиотека jqPlot, а сами файлы JS библиотек подгружаются с популярных CDN.


Пример графика работы устройства:



Наглядно видно, что около 9:35 устройство было включено на подогрев, котел плавно начал нагревать контур отопления (датчики T3, T4), вслед за этим начала расти температура контура бассейна (датчики T1, T2). Где-то около 10:20 котел переключился на нагрев горячей воды в доме, температура контура отопления упала. Затем еще через 10 минут котел вернулся к нагреву воды бассейна. В 10:50 произошла авария: внезапно отключили насос циркуляции воды в бассейне. Поток воды резко упал до ноля, реле нагрева выключилось (красный пунктир на 2м графике), предотвращая перегрев. Но устройство по-прежнему осталось в состоянии нагрева (красная линия на 2м графике). Т.е. если бы насос был снова был включен, и температуры были бы в норме, устройство вернулось бы к нагреву. Замечу, что после аварийного выключения насоса, температуры в контуре воды бассейна (T1, T2) начали резко расти за счет перегрева теплообменника. И если бы не резкое отключение котла, была бы беда.


Встроенный веб-сервер


Для общения с внешним миром используется стандартный класс ESP8266WebServer. При старте устройства, оно инициализируется в качестве точки доступа с дефолтным паролем, заданным в #define AP_PASS. Автоматически открывается web-страница для выбора доступной сети wi-fi и ввода пароля. После ввода пароля устройство перезагружается и коннектится к заданной точке доступа.


Готовое устройство


Готовое устройство было помещено в стандартную разделочную коробку для электропроводки. В ней было выпилено отверстие для LCD, и отверстия для разъемов.



Фотографии фасада устройства в разных режимах

С отображением времени, прошедшего после включения:



С отображением лимитов:



Заключение


В заключение хочу сказать, что, разрабатывая подобное устройство, я получил отличный опыт с схемотехнике, разработке печатных плат, навыки монтажа SMD компонент, в архитектуре и программировании микроконтроллеров, вспомнил почти уже забытый C++ и бережному обращению памятью и прочим ограниченным ресурсам контроллера. Так же в некоторой степени пригодились знания HTML5, JavaScript, навыки отладки скриптов в браузере.


Эти скилы и удовольствие, полученное при разработке устройства, – и есть основные полученные бенефиты. А исходные коды устройства, принципиальной схемы, печатных плат – пожалуйста, пользуйтесь, дорабатывайте. Все исходные коды проекта лежат на GitHab. Аппаратная часть в общедоступном проекте на EasyEDA. Даташиты к чипам, используемым в проекте, я собрал на сетевом диске.

Tags:
Hubs:
+49
Comments 115
Comments Comments 115

Articles