Что будем делать

Давно хотел посмотреть в работе популярный у самодельщиков сенсор качества воздуха CCS811 и сравнить его показания с SGP30 или SGPC3. Важно понимать, что это сенсор TVOC и номинальное присутствие eCO2 не делает его сенсором углекислого газа.

Тестовый стенд у меня построен на контроллере Wiren Board с родным протоколом Modbus RTU, поэтому и моё устройство будет работать по этому протоколу.

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

Потом мы подключим сенсор CCS811 и научимся сохранять BaseLine.

И в самом конце подключим устройство к контроллеру Wiren Board: составим шаблон для драйвера wb‑mqtt‑serial и напишем простой сценарий автоматизации.

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

Важно! Я программист-самозванец, поэтому весь код и советы стоит рассматривать через призму критического мышления.

Что за зверь Modbus RTU

Modbus RTU — это промышленный протокол связи между устройствами, работающий поверх шины RS-485.

Контроллер автоматизации обычно является клиентом, а периферия — серверами. Ещё года два назад клиент был мастером (master), а сервер слейвом (slave), но разработчик протокола изменил названия.

Серверы имеют свой уникальный адрес в рамках одного сегмента шины и много modbus‑регистров, то есть ячеек памяти со своими адресами и типами данных, доступных снаружи.

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

Архитектура Клиент-Сервер, которая лежит в основе протокола Modbus
Архитектура Клиент-Сервер, которая лежит в основе протокола Modbus

Доступные нам типы регистров:

  • Coils (1 бит, чтение‑запись, 1 или 0) — дискретный выход, не используем.

  • Discrete Input (1 бит, только чтение, 1 или 0) — дискретный вход, используем для ошибок инициализации.

  • Input Register (16 бит, только чтение, 0...65 535) — регистр ввода, используем для данных с датчика.

  • Holding (16 бит, чтение‑запись, 0...65 535) — будем хранить настройки связи.

Для каждого типа регистра есть своя функция работы с ним, я расскажу только про те, которые будем использовать:

  • 0×02 — чтение Discrete Input.

  • 0×03 — чтение Holding.

  • 0×04 — чтение Input Register

  • 0×06 — запись Holding.

Этой информации нам достаточно, чтобы начать работу, подробную информацию о протоколе можно найти в этой статье.

Железки

Теперь, когда нам понятна задача, выбираем железки.

Нам понадобится сенсор качества воздуха (TVOC) — я возьму CCS811.

В качестве микроконтроллера для устройства я возьму ESP8266 — он довольно недорогой, имеет много периферии, вполне производительный и он у меня есть. Чтобы было удобно собирать прототип, я возьму платку NodeMCU с модулем ESP12F (ESP8266), но вы можете использовать почти любой доступный вариант с нужным количеством выведенных наружу GPIO.

Распиновка NodeMCU, взято с lastminuteengineers.com
Распиновка NodeMCU, взято с lastminuteengineers.com

Чтобы подключить ESP8266 к шине RS-485, нам понадобится преобразователь уровней UART → RS-485, например, собранный на микросхеме MAX485 HW-97 и его многочисленные китайские братья.

Ещё нам понадобится преобразователь USB‑RS485, чтобы опрашивать наше устройство и проверять его работу. Можно использовать абсолютно любой с Aliexpress, я возьму WB‑USB485. Во время разработки плату я буду питать от USB‑порта напряжением 5 В.

Для реализации сервисного режима нам понадобится любая тактовая кнопка, на схеме она подключена к D5 и GND.

Железки собрали, подключаем по схеме ниже и переходим к самому интересному — созданию прошивки устройства. Сенсор CCS811 пока лежит в сторонке.

Схема подключения
Схема подключения
Макетная плата на столе
Макетная плата на столе

Прошивка

Я программист-самозванец, поэтому писать прошивку устройства буду в среде Arduino, которая позволяет писать программы (скетчи) для Embedded на языке C++.

Подготовка среды разработки

Писать мы будем под ESP8266, которой нет в стандартной поставке среды, поэтому перед началом надо сделать пару шагов:

  1. Скачиваем и устанавливаем Arduino.

  2. Добавляем в менеджер плат ESP8266, инструкция.

  3. Выбираем плату в меню Инструменты → Плата → Generic ESP8266 Module или плату, которая у вас на руках.

  4. И устанавливаем нужную нам библиотеку: меню Инструменты → Управлять библиотеками. В менеджере библиотек ищем и устанавливаем modbus‑esp8266.

Всё, мы готовы творить. В коде я указывал номера GPIO, а не специфичные для моей платы имена выводов — по задумке, это должно облегчить использование других плат.

Простой Modbus-сервер

Сделаем modbus‑сервер с адресом 1, параметрами подключения 9600 8N2, в котором будет один тестовый holding‑регистр с числом 3.

Открываем окно скетча, проверяЗапрограммируемем, что выбрана наша плата в меню и удаляем весть код в окне.

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

// Подключаем библиотеку modbus-esp8266
#include <ModbusRTU.h>

// Настройка Modbus
#define SLAVE_ID 1 // адрес нашего сервера
#define PIN_FLOW 4 // пин контроля направления приёма/передачи,
// если в вашем преобразователе UART-RS485 такого нет —
// закоменнтируйте строку

// Номера Modbus регистров
#define REG_TEST 0       // тестовый регистр с номером 0

ModbusRTU mb;

void setup() {
  modbus_setup();
}

void loop() {
  mb.task();
}

void modbus_setup() {
  Serial.begin(9600, SERIAL_8N2); // задаём парамеры связи
  mb.begin(&Serial);
  mb.begin(&Serial, PIN_FLOW); // включаем контроль направления приёма/передачи
  mb.slave(SLAVE_ID); // указываем адрес нашего сервера

  mb.addHreg(REG_TEST); // описываем регистр REG_TEST типа Holding
  mb.Hreg(REG_TEST, 3); // записываем в наш регистр REG_TEST число 3 —
  // его мы должны будем увидеть при опросе устройства
}

Ссылка на файл ver1.ino.

Подключаем свою плату к компьютеру и нажимаем Ctrl+U — это скомпилировать скетч и залить его в контроллер.

Вот они, заветные слова:

Leaving...
Hard resetting via RTS pin...

А что дальше? Светодиод никакой мы не подключали, экрана нет — как понять, что всё работает? Да просто — подключиться по Modbus и посмотреть.

Для этого нам понадобится программа для работы с Modbus RTU. В зависимости от типа операционной системы могу порекомендовать:

  • Windows: Rilheva Modbus Poll.

  • Linux: консольный modbus_client или графический QMaster.

  • MacOS давно не работал, но говорят, что там есть утилита cu.

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

У нас это:

  • Параметры подключения — 9600 8N2

  • Адрес — 1

  • Регистр Holding с адресом 0.

Обычно в программах надо выбрать функцию чтения/записи регистра. Для holding‑регистров это 0×03 — чтение и 0×06 запись.

Я использую Linux, поэтому буду работать с устройством через modbus_client, оно и нагляднее. Подключённое устройство определилось на порту dev/ttyACM0 — у вас он может быть другой.

Считаем наш регистр:

$ modbus_client -mrtu -b9600 -d8 -pnone -s2 /dev/ttyACM0 -a1 -t0x03 -r0

SUCCESS: read 1 of elements:
        Data: 0x0003 

А теперь запишем туда что-нибудь своё, например, число 1 и не забудем сменить функцию с 0x03 (чтение holding) на 0x06 (запись holding):

$ modbus_client -mrtu -b9600 -d8 -pnone -s2 /dev/ttyACM0 -a1 -t0x06 -r0 1                         

SUCCESS: written 1 elements!

И снова считаем:

modbus_client -mrtu -b9600 -d8 -pnone -s2 /dev/ttyACM0 -a1 -t0x03 -r0                                       

SUCCESS: read 1 of elements:
        Data: 0x0001

Ура! Теперь в регистре у нас единичка. Таким примитивным способом контроллер может общаться с парой сотен устройств на шине.

Проверка скетча ver1.ino
Проверка скетча ver1.ino

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

Казалось бы, добавляй сенсор и всё готово! Но нет, есть один важный момент: у нашего устройства фиксированные настройки связи, а значит для изменения настроек нам придётся каждый раз собирать прошивку и это неудобно.

Чтобы пользователь нашего устройства не мучился, дадим ему возможность менять настройки связи через Modbus‑регистры. Так он без проблем сможет использовать наше устройство на одной шине с другими.

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

// Адреса Modbus-регистров с настройками связи
#define REG_MB_ADDRESS    100 // адрес устройства на шине
#define REG_MB_STOP_BITS  101 // количество стоповых битов
#define REG_MB_BAUDRATE   102 // скорость подключения

Так как при отключении питания значения регистров сбрасываются, то нам понадобится место, где мы будем хранить настройки — EEPROM:

#define EEPROM_SIZE           6 // мы займём 6 ячеек памяти: 3*2=6
#define EEPROM_MB_ADDRESS     0 // номер ячейки с адресом устройства
#define EEPROM_MB_STOP_BITS   2 // номер ячейки со стоп-битами
#define EEPROM_MB_BAUDRATE    4 // номер ячейки со скоростью

На ESP8266/ESP32 тип данных uint16_t занимает 4 байта, то есть является аналогом unsigned long (источник). Поэтому каждое число займёт две ячейки EEPROM, а значит нам нужно будет 6 ячеек.

Ещё не забудем в самом начале программы описать значение настроек по умолчанию, у меня будет 9600 8N2:

#define DEFAULT_MB_ADDRESS    1  // адрес нашего сервера
#define DEFAULT_MB_STOP_BITS  2  // количество стоповых битов
#define DEFAULT_MB_BAUDRATE   96 // скорость подключения/100

Максимальное значение скорости 115 200 — это больше максимального значения, которое помещается в один регистр: 65 535. Чтобы не разбивать значение на два регистра, мы будем хранить и использовать при настройке значение скорости, поделённое на 100, например, при скорости 9600 у нас будет храниться 9600/100 = 96.

Теперь объявим переменные, куда мы будем читать настройки связи из EEPROM и откуда их будем записывать в modbus‑регистры, а также использовать при настройке подключения:

uint16_t mbAddress = DEFAULT_MB_ADDRESS; // modbus адрес устройства
uint16_t mbStopBits = DEFAULT_MB_STOP_BITS; // количество стоповых битов
uint16_t mbBaudrate = DEFAULT_MB_BAUDRATE; // скорость подключения modbus

Осталось три шага: чтение настроек из EEPROM, создание holding-регистров и запись настроек в EEPROM при изменении значений в регистрах.

Перед использованием EEPROM нужно инициализировать:

void eeprom_setup() {
  EEPROM.begin(EEPROM_SIZE);
}

Для чтения из EEPROM напишем функцию read_modbus_settings(), если в ячейках EEPROM пусто, то записываем в переменные значения по умолчанию:

void read_modbus_settings() {
  EEPROM.get(EEPROM_MB_ADDRESS, mbAddress);
  if (mbAddress == 0xffff) {
    mbAddress = DEFAULT_MB_ADDRESS;
  }

  EEPROM.get(EEPROM_MB_STOP_BITS, mbStopBits);
  if (mbStopBits == 0xffff) {
    mbStopBits = DEFAULT_MB_STOP_BITS;
  }

  EEPROM.get(EEPROM_MB_BAUDRATE, mbBaudrate);
  if (mbBaudrate == 0xffff) {
    mbBaudrate = DEFAULT_MB_BAUDRATE;
  };
}

Обратите внимание, я использую функцию get, а не read — это, чтобы не считать ячейки. Она сразу посмотрит размер переменных и считывает столько ячеек, сколько нужно.

Далее описываем функцию настройки modbus modbus_setup():

void modbus_setup() {
  Serial.begin(convert_baudrate(mbBaudrate), convert_stop_bits_to_config(mbStopBits)); // задаём парамеры связи
  mb.begin(&Serial);
  mb.begin(&Serial, PIN_FLOW); // включаем контроль направления приёма/передачи
  mb.slave(mbAddress); // указываем адрес нашего сервера

  // описываем три holding регистра
  mb.addHreg(REG_MB_ADDRESS);   // адрес устройства на шине
  mb.addHreg(REG_MB_STOP_BITS); // стоповые биты
  mb.addHreg(REG_MB_BAUDRATE);  // скорость подключения

  // записываем в регистры значения адреса, стоповых битов и скорости
  mb.Hreg(REG_MB_ADDRESS, mbAddress);
  mb.Hreg(REG_MB_STOP_BITS, mbStopBits);
  mb.Hreg(REG_MB_BAUDRATE, mbBaudrate);

  // описываем колбек функцию, которая будет вызвана при записи регистров
  // параметров подключения
  mb.onSetHreg(REG_MB_ADDRESS, callback_set_mb_reg);
  mb.onSetHreg(REG_MB_STOP_BITS, callback_set_mb_reg);
  mb.onSetHreg(REG_MB_BAUDRATE, callback_set_mb_reg);
}

Здесь мы описали инициализацию соединения, добавили holding‑регистры, отправили в них значения переменных, которые мы записали в функции read_modbus_settings().

convert_stop_bits_to_config — это функция, которая прикрывает мою лень полностью описывать настройки подключения. Суть в том, что реализация Serial в Adruino не даёт удобной настройки каждого параметра подключения отдельно (или я не нашёл), а заставляет всё, кроме адреса и скорость упаковывать в конструкцию типа SerialConfig. А так, как настройки чётности и количества битов данных всё равно почти не меняют, я решил сделать просто:

SerialConfig convert_stop_bits_to_config(uint16_t stopBits) {
  return (stopBits == 2) ? SERIAL_8N2 : SERIAL_8N1;
}

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

uint16_t callback_set_mb_reg(TRegister* reg, uint16_t val) {
  switch (reg->address.address) {
    case REG_MB_ADDRESS: // если записываем регистр с адресом
      if (val > 0 && val < 247) { // проверяем, что записываемое число корректно
        write_eeprom(EEPROM_MB_ADDRESS, val); // записываем значение в EEPROM
      } else {
        val = reg->value; // этот трюк сгенерирует ошибку записи, что нам и нужно, так как значение неверное
      }
      break;
    case REG_MB_STOP_BITS: // если регистр со стоповыми битами
      if (val == 1 || val == 2) {
        write_eeprom(EEPROM_MB_STOP_BITS, val);
      } else {
        val = reg->value;
      }
      break;
    case REG_MB_BAUDRATE: // если регистр со скоростью
      uint16_t correctBaudRates[] = {12, 24, 48, 96, 192, 384, 576, 1152};
      if (contains(val, correctBaudRates, 8)) {
        write_eeprom(EEPROM_MB_BAUDRATE, val);
      } else {
        val = reg->value;
      }
      break;
  }
  return val;
}

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

Ну и в коде настроек modbus ещё засветилась пара вспомогательных функций: запись значения в EEPROM и проверка вхождения числа в массив.

// Запись значения в EEPROM
void write_eeprom(uint8_t eepromm_address, uint16_t val) {
  EEPROM.put(eepromm_address, val);
  EEPROM.commit();
}

// Функция, которая находит вхождение числа в массив
bool contains(uint16_t a, uint16_t arr[], uint8_t arr_size) {
  for (uint8_t i = 0; i < arr_size; i++) if (a == arr[i]) return true;
  return false;
}

Так как мы инициализацию всего и вся разложили по функциям, то функция инициализации у нас выглядит просто и лаконично:

void setup() {
  eeprom_setup(); // настраиваем EEPROM
  modbus_setup();  // настраиваем Modbus
}

Фух, вроде всё, проверяем: читаем регистры с 100 по 102, потом записываем в регистр 102 число 1152 (мы делим скорость на 100 для записи в регистры в EEPROM) и снова читаем регистры. В регистре 102 число изменилось, теперь там 0×0480 — это 1152 в шестнадцатеричной системе счисления. Новые настройки будут работать после перезапуска устройства.

Проверка скетча ver2.ino
Проверка скетча ver2.ino

Ссылку на полный код ищите ниже, а я пока расскажу, как всё это работает.

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

Обещанный код смотрите в файле ver2.ino.

Черный ход или загрузка в сервисном режиме

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

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

В самом начале статьи мы подключили кнопку на GPIO14 — её и будем использовать. Проверять состояние кнопки я буду при старте устройства.

Объявляем нашу кнопку в начале файла, рядом со всеми define:

#define BTN_SAFE_MODE 14 // D5 на NodeMCU. Кнопка сброса настроек подключения

Настраиваем кнопку на вход:

void io_setup() {
  pinMode(BTN_SAFE_MODE, INPUT_PULLUP);
}

Пишем функцию, которая будет опрашивать кнопку и, если она не нажата, то загружать настройки из EEPROM:

void check_safe_mode() {
  if (digitalRead(BTN_SAFE_MODE)) { // если кнопка не нажата, то читаем настройки из EEPROM, иначе будут использованы настройки по умолчанию
    read_modbus_settings(); // чтение настроек
  }
}

Так как кнопка у нас замыкает GPIO на землю, то сигнал ненажатой кнопки будет равен 1, что мы и учли в нашем блоке if.

Всё, осталось подправить функцию настроек при старте и можно спокойно забывать заданные настройки связи:

void setup() {
  io_setup();  // настраиваем входы/выходы
  eeprom_setup(); // настраиваем EEPROM
  check_safe_mode(); // проверяем, не надо ли нам в безопасный режим с дефолтными настройками
  modbus_setup();  // настраиваем Modbus
}

Как вы уже привыкли — полный файл скетча ver3.ino.

На этом этапе у нас есть рабочий прототип modbus‑сервера, который мы можем использовать для разработки любых устройств. Всё, что ниже — это продолжение работы над датчиком.

Проверка скетча ver3.ino
Проверка скетча ver3.ino

Делаем датчик качества воздуха

У нас есть рабочий modbus‑сервер, который ничего не умеет, кроме как хранить настройки связи в регистрах и записывать пользовательские настройки в EEPROM. Штука интересная, но пока довольно бесполезная.

Давайте это исправим и добавим в прошивку работу с сенсором CCS811. Сам сенсор очень мал, поэтому я буду использовать готовую платку с этим сенсором.

CJMCU-811 — плата с сенсором CCS811
CJMCU-811 — плата с сенсором CCS811

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

По умолчанию I2C у нас назначен на пины GPIO5 (D1) и GPIO4 (D2), поэтому сюда и подключим наш датчик. GPIO4 у нас занят выходом Flow Control, поэтому мы освободим его. Теперь Flow Control подключён у нас на GPIO12 (D6).

Новая схема со всеми изменениями ниже.

Схема подключения датчика CCS811 и преобразователя UART-RS485
Схема подключения датчика CCS811 и преобразователя UART-RS485

Добавляем в прошивку поддержку сенсора CCS811

Для начала учтём изменение в схеме: Flow Control теперь подключён у нас к другому выходу, меняем в константах GPIO4 на GPIO12.

#define PIN_FLOW 12 // GPIO12(D6)

Для работы с датчиком мы будем использовать библиотеку «SparkFun CCS811 Arduino library», которую надо установить в менеджере библиотек. После этого подключите библиотеку в коде вместе с библиотекой Wire:

#include <Wire.h>            // Wire
#include <SparkFunCCS811.h>  // SparkFun CCS811 Arduino library

Далее описываем номера Modbus‑регистров:

#define REG_SENSOR_ERROR  0 // ошибка инициализации сенсора CCS811
#define REG_SENSOR_ECO2   1 // eCO2
#define REG_SENSOR_TVOC   2 // TVOC
#define REG_SENSOR_BL     3 // Baseline

Здесь мы задали адреса двух регистров с измерениями: eCO2 и TVOC, а также два служебных: Baseline и ошибка инициализации сенсора. Baseline — это калибровочное значение, которое рассчитывается во время автокалибровки сенсора и используется при подготовке итоговых значений для пользователя.

Описываем в функции modbus_setup() создание и инициализацию регистров:

  mb.addIsts(REG_SENSOR_ERROR); // ошибка инициализации сенсора
  mb.addIreg(REG_SENSOR_ECO2);  // eCO2
  mb.addIreg(REG_SENSOR_TVOC);  // TVOC
  mb.addIreg(REG_SENSOR_BL);    // Baseline
  
  mb.Ists(REG_SENSOR_ERROR, 0); // ошибка
  mb.Ireg(REG_SENSOR_ECO2, 0);  // eCO2
  mb.Ireg(REG_SENSOR_TVOC, 0);  // TVOC
  mb.Ireg(REG_SENSOR_BL, 0);    // Baseline

Далее надо указать адрес сенсора на шине и создать объект для работы с сенсором:

#define CCS811_ADDR 0x5A // Указываем адрес устройства I2C, по умолчанию 0x5A, второй адрес устройства 0x5B

CCS811 ccs811(CCS811_ADDR);  // создаем объект для работы с сенсором CCS811

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

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

Раз нам надо что‑то делать периодически, то нам понадобится простой таймер, возьмём для этого библиотеку SimpleTimer (второе слово с большой буквы) и установим её через менеджер библиотек. Теперь подключим библиотеку в скетче и создадим объект таймера с интервалом 10 мс:

#include <SimpleTimer.h>  // простой таймер

SimpleTimer sysTimer(2000); // запускаем таймер с интервалом 2c (2000мс)

Теперь нам надо инициализировать шину i2c, а заодно обеспечим обратную связь об ошибках:

void i2c_setup() {
  Wire.begin();  // инициализация i2c

  if (!ccs811.begin()) {  // инициализация CCS811
    mb.Ists(REG_SENSOR_ERROR, 1); // если не получилось — ошибка, пишем в регистр ошибок
  } else {
    mb.Ists(REG_SENSOR_ERROR, 0);
  };
}

Не забудьте добавить вызов инициализации сенсора в функцию setup():

void setup() {
  .....
  i2c_setup();          // инициализация i2c
}

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

Чтение данных:

// Опрос сенсора
void read_sensor() {
  unsigned int baseLine;
  
  if (ccs811.dataAvailable()) {                 // проверяем, есть ли нвоые данные
    ccs811.readAlgorithmResults();              // считываем
    mb.Ireg(REG_SENSOR_ECO2, ccs811.getCO2());  // записываем в регистр eCO2
    mb.Ireg(REG_SENSOR_TVOC, ccs811.getTVOC()); // записываем в регистр TVOC

    baseLine = ccs811.getBaseline();
    mb.Ireg(REG_SENSOR_BL, baseLine);    // Baseline
  }
}

Проверять таймер мы будем в функции check_timer(), которую вызовем в главном цикле loop():

void check_timer() {
  if (sysTimer.isReady()) { // выполняется раз в 2 c
    read_sensor(); // опрашиваем сенсор
    sysTimer.reset(); // сбрасываем таймер
  }
}

void loop() {
  mb.task();
  check_timer(); 
}

По традиции, полный скетч по ссылке ver4.ino.

Проверка скетча ver4.ino. Видно, что значение Baseline сбрасывается при перезапуске устройства
Проверка скетча ver4.ino. Видно, что значение Baseline сбрасывается при перезапуске устройства

Сохранение Baseline

Baseline — это внутреннее калибровочное значение, которое меняется сенсором во время его автокалибровки. Но есть нюанс — оно сбрасывается при отключении питания, а значит сенсор теряет калибровку и начинает цикл заново.

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

Сохранять мы это значение будем в EEPROM по аналогии с настройками Modbus.

Добавляем к выделенному нами размеру EEPROM ещё две ячейки и указываем номер ячейки для хранения Baseline:

#define EEPROM_SIZE           8 // теперь мы займём 8 ячеек памяти
#define EEPROM_BASELINE       6 // номер ячейки с Baseline

Объявляем переменную для хранения Baseline:

uint16_t sensorBaseLine = 0; // переменная со значение BaseLine

Добавляем таймер:

SimpleTimer blTimer(30000);   // запускаем таймер с интервалом 30 с

И добавляем его обработку:

void check_timer() {
...

  if (blTimer.isReady()) {
    save_baseline(); // записываем значение Baseline в EEPROM
    blTimer.reset();
  }
}

Функция чтения Baseline из EEPROM:

// Чтение Baseline из EEPROM
void read_baseline() {
  EEPROM.get(EEPROM_BASELINE, sensorBaseLine);
  if (sensorBaseLine != 0xffff) {
    mb.Ireg(REG_SENSOR_BL, sensorBaseLine);

    if (write_baseline(sensorBaseLine)) {
      mb.Ists(REG_SENSOR_ERROR, 0);
    } else {
      mb.Ists(REG_SENSOR_ERROR, 1);
    }
  }
}

Функция записи Baseline в сенсор:

// Запись baseline в сенсор
bool write_baseline(uint16_t baseline) {
  CCS811Core::CCS811_Status_e errorStatus;

  errorStatus = ccs811.setBaseline(baseline);
  if (errorStatus != CCS811Core::CCS811_Stat_SUCCESS)
  {
    return false;
  };
  return true;
}

Функция сохранения Baseline в EEPROM:

// Запись Baseline в EEPROM
void save_baseline(){
  write_eeprom(EEPROM_BASELINE, sensorBaseLine);
}

И добавляем в функцию setup() чтение Baseline из EEPROM:

void setup() {
  ...
  read_baseline();    // чтение Baseline из EEPROM
}

Полный скетч ver5.ino.

Проверка работы скетча ver5.ino — значение Baseline восстанавливается при старте
Проверка работы скетча ver5.ino — значение Baseline восстанавливается при старте
Сохранение Baseline в файловую систему (работа над ошибками)

Это было в изначальном варианте статьи, потом заменено на EEPROM.

Меня внизу в комментариях поругали (@av0000), что я неправильно рассказываю: в ESP EEPROM эмулируется через флеш, поэтому нет никакой надобности городить историю про файловую систему. Поэтому Baseline можно просто хранить в ячейке EEPROM. Раздел я оставлю, так как пример работы с файловой системы может быть полезен для других задач.

Baseline — это внутреннее калибровочное значение, которое меняется сенсором во время его автокалибровки. Но есть нюанс — оно сбрасывается при отключении питания, а значит сенсор теряет калибровку и начинает цикл заново.

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

Конечно, можно сохранять это значение в EEPROM по аналогии с настройками Modbus, но её срок службы ограничен, а значит надо хранить значение где‑то в другом месте.

В микроконтроллере ESP8266 есть встроенная файловая система, в которую можно сохранять файлы и срок её службы довольно большой. Туда то мы и будем сохранять значение Baseline в файле JSON.

Чтобы работать с файловой системой, нам надо сказать среде Arduino об этом, выберите в меню ИнструментыFlash Size значение, отличное от FS:None, например, FS:1MB;. Подробную информацию по файловой системе найдёте ссылке.

Для работы с JSON нам понадобится библиотека ArduinoJson, поэтому установите её в менеджере библиотек и подключите вместе с библиотекой работы с файловой системы:

#include <FS.h> // SPIFFS будем использовать
#include <ArduinoJson.h>        //Установить из менеджера библиотек

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

Добавляем константу с адресом регистра, где мы будем хранить ошибку инициализации файловой системы:

#define REG_FS_ERROR      4 // ошибка файловой системы

Теперь опишем нужные нам флаги:

uint16_t sensorBaseLine = 0; // переменная со значение BaseLine
String configFile = "/config.json"; // имя файла конфигурации

Несмотря на то, что файловая система имеет огромный ресурс на запись, накладные расходы на запись в файл довольно большие. Поэтому мы будем писать данные периодически. Заведём для этой цели ещё один таймер, который будет срабатывать раз в 10 секунд (1000 мс):

SimpleTimer blTimer(10000);   // запускаем таймер с интервалом 10 с

Инициализируем файловую систему и кладём в регистр флаг ошибки, если не удалось:

// Инициализация файловой системы
void fs_setup() {
  if (SPIFFS.begin()) {
    mb.Ists(REG_FS_ERROR, 0);
  } else {
    mb.Ists(REG_FS_ERROR, 1);
  }
}

Теперь немного изменим функцию check_timer():

void check_timer() {
  if (sysTimer.isReady()) {
    read_sensor(); // опрашиваем сенсор
    sysTimer.reset(); // сбрасываем таймер
  }
if (blTimer.isReady()) {
write_config(); // записываем значение Baseline в файл
blTimer.reset();
}
}

И опишем функции чтения-записи конфига:

// Чтение конфига из файла
void read_config() {
DynamicJsonDocument doc(200);
String jsonConfig = "";
File cfgFile = SPIFFS.open(configFile, "r");
if (cfgFile) {
while (cfgFile.available()) {
jsonConfig += (char)cfgFile.read();
}
}
cfgFile.close();
deserializeJson(doc, jsonConfig);
sensorBaseLine = doc["baseLine"];
mb.Ireg(REG_SENSOR_BL, sensorBaseLine);
if (write_baseline(sensorBaseLine)) {
mb.Ists(REG_SENSOR_ERROR, 0);
} else {
mb.Ists(REG_SENSOR_ERROR, 1);
}
}
// Запись baseline в сенсор
bool write_baseline(uint16_t baseline) {
CCS811Core::CCS811_Status_e errorStatus;
errorStatus = ccs811.setBaseline(baseline);
if (errorStatus != CCS811Core::CCS811_Stat_SUCCESS)
{
return false;
};
return true;
}
// Запись конфига в файл
void write_config() {
DynamicJsonDocument doc(200);
String jsonConfig;
doc["baseLine"] = sensorBaseLine;
serializeJson(doc, jsonConfig);
File cfgFile = SPIFFS.open(configFile, "w");
if (cfgFile) {
cfgFile.print(jsonConfig);
cfgFile.close();
mb.Ists(REG_FS_ERROR, 0);
} else {
mb.Ists(REG_FS_ERROR, 1);
}
}

Компилируем, зашиваем, проверяем.

Полный скетч примера ver5-filesystem.ino.

Проверка скетча ver5.ino
Проверка скетча ver5.ino

Интеграция в систему автоматизации на Wiren Board

План интеграции

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

У Wiren Board есть собственный драйвер работы с Modbus‑устройствами wb‑mqtt‑serial, для которого можно написать свой шаблон и он будет работать с нашим устройством, как с родным.

Заодно мы напишем простой сценарий, который будет включать зуммер контроллера, если значение сенсора TVOC превысит определённое значение.

Устройство подключено к порту RS485 контроллера Wiren Board
Устройство подключено к порту RS485 контроллера Wiren Board

Шаблон устройства

Шаблон драйвера wb‑mqtt‑serial — это json‑файл с описанием регистров Modbus‑устройства, подробное описание структуры шаблона можно найти в репозитории wb‑mqtt‑serial.

Перед тем, как делать шаблон, давайте вспомним карту регистров нашего устройства.

Адрес

Тип

Описание

0

Discrete Input

Ошибка инициализации сенсора CCS811

1

Input Register

Значение eCO2

2

Input Register

Значение TVOC

3

Input Register

Значение Baseline

100

Holding

Modbus-адрес устройства

101

Holding

Стоп биты

102

Holding

Скорость обмена, делённая на 100

Упрощённо шаблон устройства выглядит так:

{
    "device_type": "ad-sv",     // тип устройства — уникальный идентификатор
    "title": "AD-SV",               // отображаемое название
    "group": "g-climate-sensor",    // группа, в которой будет отображаться шаблон. Список групп смотрите в документации
    "device": {                     
        "name": "AD-SV",    // имя устройства, используется в MQTT
        "id": "ad-sv",
        "groups": [ ],              // группы параметров и каналов        
        "channels": [ ],            // каналы, доступно в скриптах и на вкладке Устройства
        "parameters": [ ],          // параметры, можно менять в настройках устройства
        "translations": { }         // переводы 
    }
}

AD‑SV — сокращённое имя устройства.

Из карты регистров нашего устройства видно:

  • регистры 0...3 — это каналы (channels) устройства: их мы выведем веб‑интерфейс и сможем использовать в скрипте автоматизации;

  • регистры 100...102 — это параметры (parameters) устройства, их мы добавим на страницу настроек.

Группы у нас будет две:

  • Inputs — сюда войдут каналы;

  • Settings — параметры.

Также мы сделаем наш шаблон двуязычным: en/ru.

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

{
    "device_type": "ad-sv",
    "title": "AD-SV",
    "group": "g-climate-sensor",
    "device": {
        "name": "AD-SV",
        "id": "ad-sv",
        "groups": [
            {
                "title": "Inputs",
                "id": "inputs",
                "order": 0
            },
            {
                "title": "Settings",
                "id": "settings",
                "order": 1
            }
        ],
        "channels": [            
            {
                "name": "eCO2",
                "reg_type": "input",
                "address": 1,
                "type": "value",
                "group": "inputs"
            },
            {
                "name": "TVOC",
                "reg_type": "input",
                "address": 2,
                "type": "value",
                "group": "inputs"
            },
            {
                "name": "Baseline",
                "reg_type": "input",
                "address": 3,
                "type": "value",
                "group": "inputs"
            },
            {
                "name": "CCS811 Error",
                "reg_type": "discrete",
                "address": 0,
                "type": "switch",
                "group": "inputs"
            }
        ],
        "parameters": [
            {
                "id": "address",
                "title": "Address",
                "reg_type": "holding",                
                "address": 100,
                "default": 1,
                "min": 1,
                "max": 247,
                "group": "settings"
            },
            {
                "id": "stop-bits",
                "title": "Stop Bits",
                "reg_type": "holding",                
                "address": 101,
                "default": 2,
                "enum": [1, 2],
                "enum_titles": ["1", "2"],
                "group": "settings"
            },
            {
                "id": "baudrate",
                "title": "Baudrate",
                "reg_type": "holding",                
                "address": 102,
                "default": 96,
                "enum": [12, 24, 48, 96, 192, 384, 576, 1152],
                "enum_titles": ["1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200"],
                "group": "settings"
            }
        ],
        "translations": {
            "ru": {
                "Inputs": "Входы",
                "Settings": "Настройки",
                "eCO2": "eCO2",
                "TVOC": "TVOC",
                "Baseline": "Baseline",
                "CCS811 Error": "Ошибка CCS811",
                "File System Error": "Ошибка файловой системы",
                "Address": "Адрес устройства",
                "Stop Bits": "Стоп биты",
                "Baudrate": "Скорость обмена",
            }
        }
    }
}

Теперь подключаем устройство к контроллеру и настраиваем:

  1. Копируем файл шаблона на контроллер в папку пользовательских шаблонов /etc/wb-mqtt-serial.conf.d/templates,

  2. Идём в веб‑интерфейсе Настройки → Конфигурационные файлы → Настройка драйвера serial‑устройств.

  3. Параметры порта оставляем по умолчанию.

  4. Добавляем новое устройство и в списке шаблонов выбираем наше устройство. Если его нет в списке, нажимаем Ctrl+F5, чтобы сбросить кеш браузера.

  5. В поле сразу под выбором шаблона указываем адрес устройства — 1.

  6. Сохраняем настройки.

Теперь на вкладке Устройства мы должны увидеть карточку с нашим сенсором.

Файл шаблона на контроллере
Файл шаблона на контроллере
Настройки устройства и выбранный шаблон AD-SV
Настройки устройства и выбранный шаблон AD-SV
Представление устройства в веб-интерфейсе контроллера Wiren Board
Представление устройства в веб-интерфейсе контроллера Wiren Board

Сценарии автоматизации

В контроллере Wiren Board из коробки есть движок правил wb‑rules, на нём мы и будем писать нашу автоматизацию.

Алгоритм: если значение TVOC превысит 2000 единиц, то включить зуммер контроллера и выключить его после того, как значение снизится. Подобный алгоритм можно использовать для управления вентиляцией или для оповещения персонала об опасности, например, на вино‑водочном заводе. Вроде ничего сложного.

Добавляем файл нашего скрипта:

  1. Переходим на вкладку «Правила».

  2. Создаём новый скрипт и называем его tvoc‑control.js.

В правилах используются контролы устройств, которые выглядят как устройство_адрес/контрол. Узнать нужный контрол можно кликнув на имени канала в карточке устройства на вкладке «Устройства».

Копирование адреса топика для скрипта
Копирование адреса топика для скрипта

Например, у меня значения TVOC ad-sv_1/TVOC, а зуммер контроллера в топике buzzer/enabled.

Пишем правило:

defineRule({
    whenChanged: "ad-sv_1/TVOC", // если значение сенсора изменилось
    then: function(newValue, devName, cellName) {
    if (newValue >2000) {
        dev["buzzer/enabled"] = true;       
    } else {
        dev["buzzer/enabled"] = false;
    }
    }
});

Всё, записываем наше правило в скрипт tvoc-control.js и нажимаем в веб-интерфейсе контроллера кнопку «Сохранить».

Скрипт автоматизации в редакторе
Скрипт автоматизации в редакторе

Итак, сохранили, ничего не ругнулось — давайте проверять!

Берём что‑то органическое с сильным запахом, например, спирт и подносим эту жидкость к датчику. Я взял обезжириватель универсальный, открутил крышку и поднёс её в зону, где находится датчик — мгновенно запищал зуммер. Стоило убрать крышку с обезжиривателем — значение стало стремительно падать — в комнате, где я проводил эксперимент, открыто окно и из него неплохо дует. Как только концентрация паров упала ниже 2000 ppb, выключился зуммер.

Кстати, обратите внимание, как подскочило расчётное значение eCO2 в момент сработки зуммера.

Значение TVOC около нуля — зуммер выключен
Значение TVOC около нуля — зуммер выключен
Значение TVOC выше пороговых 2000 ppb — зуммер включен
Значение TVOC выше пороговых 2000 ppb — зуммер включен
Значение TVOC меньше пороговых 2000 ppb — зуммер выключен
Значение TVOC меньше пороговых 2000 ppb — зуммер выключен

Что дальше

Сейчас у нас есть прототип устройства, которое:

  • отправляет и принимает данные по протоколу Modbus RTU;

  • измеряет уровень летучих органических веществ в воздухе;

  • хранит в энергонезевисимой памяти настройки подключения и калибровочное значение сенсора;

  • позволяет изменять основные настройки связи и имеет сервисный режим, на случай, если настройки забыли;

  • интегрировано в систему автоматизации на базе контроллера Wiren Board.

Вроде неплохо, но впереди ещё много работы:

  1. Сенсор CCS811 в процессе работы нагревается и его показания начинают «плыть», поэтому желательно поставить сенсор температуры, например, BMP280 и настроить термокомпенсацию.

  2. Можно собирать устройство и на макетке, но гораздо удобнее будет развести и изготовить печатную плату. Не забудьте по преобразователь питания, обычно в шине RS-485 от 12 до 24 В.

  3. Нужно предусмотреть процедуру обновления прошивки, например, это можно сделать по Modbus — в примерах используемой нами библиотеки есть готовая реализация. Ещё можно включать в нужное время встроенный Wi‑Fi и обновлять прошивку по OTA.

На этом позвольте откланяться, до встречи в новых статьях!

Обновление: читайте комментарии, там пишут много интересного, особенно в этой ветке.

Ссылки:

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


  1. av0000
    00.00.0000 00:00
    +1

    Вроде бы и "+". Вроде бы и популяризация, но несколько бросившихся в глаза ляпов - тянут на жирный "-". Так что ограничусь замечаниями. Каюсь, половину пролистал невнимательно, ибо см. замечания :).

    За WB (ради него и затевалось же, правда?) - спасибо. В свете нынешней доступности контроллеров, всё более актуально.

    1. Указание типа значений uint16_t как бы намекает на 16 бит или 2 байта (а не 4) - из записи EEPROM (то, как оно хранится внутри - другая история)

    2. У ESP8266 нет EEPROM. Она эмулируется через FLASH

    3. Аналогично с файловой системой - она тоже живёт во FLASH. И "насиловать" её ещё и лишней записью всякого JSON - ну, такое себе... Кстати, а зачем вообще хранить локальные настройки в человеко-читаемом виде? Их же никто, кроме контроллера не видит

    4. Не помню, в какой части ардуины, но где-то там есть встроенная функция yield(), отдающая остатки времени внутреннему планировщику. Переопределять её как-то неаккуратненько ©. Мало ли, что потребуется

    5. Если сенсор измеряет раз в секунду, зачем дёргать его раз в 10мс? Мало того, что лишний разогрев датчика, так ещё и будем мешать работать тому же modbus-у. На 9600-то, может и успеет, а быстрее - возможны сюрпризы


    1. wofs Автор
      00.00.0000 00:00

      Спасибо, что указали на ошибки.

      За WB (ради него и затевалось же, правда?) — спасибо.

      Нет, это личный блог и просто делюсь опытом. Я долго искал пример Modbus-устройства с изменяемыми настройками связи и не нашёл, подумал, что будет полезно.

      1. Указание типа значений uint16_t как бы намекает на 16 бит или 2 байта (а не 4) — из записи EEPROM (то, как оно хранится внутри — другая история)
      2. У ESP8266 нет EEPROM. Она эмулируется через FLASH
      3. Аналогично с файловой системой — она тоже живёт во FLASH. И «насиловать» её ещё и лишней записью всякого JSON — ну, такое себе… Кстати, а зачем вообще хранить локальные настройки в человеко-читаемом виде? Их же никто, кроме контроллера не видит

      Подскажите, пожалуйста, как лучше сделать — я дополню статью и скетчи.

      4. Не помню, в какой части ардуины, но где-то там есть встроенная функция yield(), отдающая остатки времени внутреннему планировщику. Переопределять её как-то неаккуратненько ©. Мало ли, что потребуется

      Взял из примера автора библиотеки modbus-esp8266: modbus-esp8266/examples/RTU/slave/slave.ino.

      5. Если сенсор измеряет раз в секунду, зачем дёргать его раз в 10мс? Мало того, что лишний разогрев датчика, так ещё и будем мешать работать тому же modbus-у. На 9600-то, может и успеет, а быстрее — возможны сюрпризы

      А почему он должен лишнего греться? Мы же просто спрашиваем у него — есть новые значения или нет или я не прав?


      1. Winnie_The_Pooh
        00.00.0000 00:00
        +2

        Датчик больше греется, потому что больше работают все узлы :))) Ну и смысла спрашивать датчик чаще времени измерения смысла нет. Постоянная времени изменения TVOC - секунды...


      1. av0000
        00.00.0000 00:00
        +3

        Пробую ответить.

        Про хранение В ДАННОМ СЛУЧАЕ (!) - нет EEPROM, FRAM, etc... ЕМНИП, EEPROM в esp8266 эмулировалась через 2 страницы FLASH с контролем корректности предыдущего сохранения, но могу ошибаться. И размер страницы, как бы не 1024 байт. Т.е. минимум 2048 байт "отъедается" на хранение 3-5 переменных. Про FS вообще молчу :) как хранилище web страниц для WiFi с возможностью закачки новых извне - да, а хранить настройки - бешеный overkill (там хранить-то 2-5-10 параметров), ну да - место-то пока есть...

        • делаем структуру с тем, что храним (для текстовых данных - фиксированная длина строки) чтобы не мудрить с логикой считывания

        • инициализируем её в коде значениями по-умолчанию

        • сохраняем её в, пусть, "EEPROM" (в данном случае - всё равно - FLASH), только если что-то поменялось. И, возможно, с задержкой в 5-60сек после последнего изменения - если юзер что-то меняет много где (но это уже изыски). Сохранение через write(... sizeof(struct config_struct)). Не знаю, может и .put подойдёт - ардуину в чистом виде уже лет 10 не пользовал, не помню. Максимум - PlatformIO + почти-С...

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

        • при старте в SafeMode - НЕ читаем конфиг и пользуемся значениями по-умолчанию

        • Если требуется хранить показания счётчиков - отдельная тема с циклическим буфером или внешней FRAM памятью - компромисс между умиранием памяти, ценой, наличием доп. модулей FRAM и частотой сохранения данных

        Про опрос датчиков уже ответил @Winnie_The_Pooh , но добавлю - даже в мануалах к упоминаемым BMP180/280/DS18B20/etc указано, что частый опрос приводит к разогреву датчика вплоть до 1-2°С. Ибо опрос (тут, обычно...) запускает вычисление. Тем более с готовыми ардуиновскими библиотеками, которые не спрашивают по-готовности, а, как правило, принудительно опрашивают датчик и ждут ответа (кстати, ещё одно место косяка с модбасом - пока I²C не ответит - будет "висеть", учитывая как подвисает I²C именно в esp8266, это тоже может стать сюрпризом). Правильный подход:

        а) спрашивать не чаще, чем это действительно надо

        б) можно спрашивать чаще и хранить локально среднее, но без фанатизма

        в) максимально использовать данные на датчик - настраивать частоту измерения в самом датчике и не спрашивать его чаще, чем это время

        г) время измерения выбирать максимальное в пределах задачи. Например, температура помещения вряд ли нужна чаще раза в 1-5-10 минут - так и не надо его дёргать чаще 0.5-2.5-5 минут с усреднением. Или даже в 1-5-10 - в зависимости от задачи..

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

        Как -то так. Без претензий на абсолютную истину, но на основе собственных поделок для дома и автоматизации "больших" зданий.

        ЗЫ: Любые рацпредложения выслушаю с удовольствием. Хоть и почти "забил" на esp8266 в угоду STM32 для домашних поделок, но идеология никуда не девается ))


        1. wofs Автор
          00.00.0000 00:00

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

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

          А по вашим комментариям, пожалуй, надо мне написать новую статью с разбором этой.


          1. av0000
            00.00.0000 00:00

            :) так в это и главный плюс статей - вдруг, кто ответит, что всё ващще фигня и надо по-другому. А у людей - мысль!©

            Тут вопрос, в какую тему копать - автоматика в терминах "контроллеров" - WB в данном случае (полноценный linux), или МИКРО-контроллеров - две большие разницы. Хотя и к "большим" контроллерам, точнее софту, есть те же претензии...

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

            Для данной статьи хватило бы микроконтроллера из "базовых" ардуин - ATmega168/238 (Pro mini etc). Esp8266 избыточен совсем (wifi же тут не используется, а в ATmega168, и правда, есть настоящий EEPROM). Ну, пришлось бы рассказать про FreeModbus и её настройку под ардуину, но это отдельная увлекательная тема...


            1. wofs Автор
              00.00.0000 00:00

              Куда копать… что мне надо, туда и копаю. Я же совсем не разработчик железа или софта — это чистое хобби, к которому периодически возвращаюсь.

              А вы про WB только узнали, или давно наблюдаете? В апреле будет выставка у нас на производстве в Москве, приходите, пообщаемся. Ребята в блоге компании незадолго до события отчёт с прошлогодней опубликуют и позовут на новую.


              1. av0000
                00.00.0000 00:00

                Дык, и у меня микроконтроллерная часть в качестве хобби. Хоть и с самопальными платами и прошивками. Но, как-то, "для себя" требуются оба направления, а они слегка разные по подходу в реализации.

                Узнал давно. Фирма заинтересовалась где-то весной, когда стало понятно, что Шнайдер - таки всё, а делать что-то надо %) С тех пор и стенд сделали, и (думали что) спалили модуль-другой, и проекты начинаем считать потихоньку. Весна придёт - поглядим.


        1. grafdezimal
          00.00.0000 00:00
          +2

          Обновление прошивки с сохранением конфигурации я бы ещё добавил, например длину config_struct до неё записать или версию. Вряд ли удастся предусмотреть всё с самого начала, такие config_srtuct имеют привычку расти.


          1. av0000
            00.00.0000 00:00

            Согласен. Но это уже детали реализации, навроде кольцевого буфера в EEPROM для экономии ресурса ячеек.

            Тут или версию конфига+совместимость в парсинге оного (множим-копим старый код), или делить конфиг на части - сеть отдельно, остальное отдельно, или просто плевать и конфигурить заново после перепрошивки - зависит от задач.

            Мои датчики на ESP живут с SPIFFS, на которой веб страницы, бинарный конфиг ESP и отдельно датчиков - но там всё равно веб+mqtt+wifi+файловый менеджер - оправдано. И да, при прошивке есс-но настройки не теряются (а давно ли я менял структуру конфига? лет 5 назад кое-где...)


    1. vconst
      00.00.0000 00:00
      +3

      Статью в избранное, ради каментов av ))