Перейти к содержанию

Датчик и телеметрия

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

1. Библиотека датчика

В platformio.ini добавьте к lib_deps:

    adafruit/Adafruit SGP40 Sensor

2. Чтение SGP40

В src/main.cpp (пины — из схемы):

#include <Wire.h>
#include <Adafruit_SGP40.h>

static Adafruit_SGP40 s_sgp;
static int32_t g_vocIndex = -1;   // -1 = данных ещё нет

static void initVocSensor() {
    Wire.begin(/*SDA=*/8, /*SCL=*/9);
    if (!s_sgp.begin()) {
        Serial.println("[VOC] SGP40 not found, check wiring");
    }
}

static void readVocSensor() {
    // measureVocIndex() сам ведёт внутреннюю компенсацию датчика.
    // Индекс: ~100 = обычный воздух, выше = грязнее (макс 500).
    g_vocIndex = s_sgp.measureVocIndex();
}

Вызов initVocSensor() добавьте в setup() после s_link.begin(), а readVocSensor() — в loop() раз в секунду (по millis-таймеру, не через delay!):

static uint32_t s_lastReadMs = 0;

void loop() {
    s_link.loop();

    uint32_t now = millis();
    if (now - s_lastReadMs >= 1000) {
        s_lastReadMs = now;
        readVocSensor();
    }
}

Никаких delay() в loop

s_link.loop() должен вызываться постоянно — на нём держатся Wi-Fi, MQTT и команды с портала. delay(1000) заморозит всё это. Только millis-таймеры.

3. Своё поле в телеметрии

Каждые telemetryPeriodMs ядро само собирает JSON-сообщение телеметрии и отправляет его в облако. Для нашего устройства (один юнит, из словарных навыков только вентилятор) ядро собирает вот такое сообщение:

{
  "units": [ { "unitId": "U1", "fanStatus": false } ],
  "rssi": -52,
  "uptime": 120
}

Разберём структуру:

  • units — массив юнитов (камер) устройства. Серийная сушилка iDryer может иметь до четырёх независимых камер, поэтому телеметрия — всегда массив, даже если камера одна;
  • units[0] — первый (и у нас единственный) юнит: мы указали unitsCount = 1 в Config;
  • fanStatus — словарное поле, появилось из-за hasFan = true;
  • rssi, uptime — уровень Wi-Fi и время работы, ядро добавляет всегда.

Про VOC в этом сообщении ничего нет — ядро не знает о нашем датчике. Но прямо перед отправкой ядро даёт вашему коду возможность дописать в сообщение свои поля. Для этого вы регистрируете колбэк (callback, «обратный вызов») — функцию, которую отдаёте ядру, а ядро само вызывает её на каждой публикации, передавая внутрь собранный JSON (аргумент doc — это он и есть).

В setup():

s_link.onTelemetryPublish([](JsonObject doc) {
    // doc — то самое сообщение телеметрии, собранное ядром (см. JSON выше).
    // Дописываем в первый юнит наше поле vocIndex.
    if (g_vocIndex >= 0) {
        doc["units"][0]["vocIndex"] = g_vocIndex;
    }
});

Строка doc["units"][0]["vocIndex"] = g_vocIndex; читается так: «в сообщении doc возьми массив units, в нём элемент 0 (наш единственный юнит) и запиши туда поле vocIndex». Имя поля вы придумываете сами — в следующей главе вы сошлётесь на него, чтобы показать значение на карточке.

Если встретите слово hook

В исходниках ядра этот колбэк называется PublishHook — «хук» (hook, «крючок») означает то же самое: точку, куда библиотека даёт «подцепить» вашу функцию. Термины взаимозаменяемы; в этой документации мы говорим «колбэк».

Лямбда и почему она «пустая»

Конструкция [](JsonObject doc) { ... } называется лямбдой — это функция без имени, записанная прямо в месте использования, чтобы не выносить её отдельно и не придумывать имя.

Квадратные скобки в начале — «список захвата»: в них перечисляют локальные переменные, которые функция берёт с собой. Правило ядра: скобки всегда пустые ([]) — лямбда ничего не захватывает и не таскает с собой никакого состояния (это и называют словом stateless, «без состояния»).

Причина техническая: лямбды с захватом требуют динамической памяти, а на ESP32 её частые выделения дробят кучу и в худшем случае роняют Wi-Fi. Поэтому ядро принимает только простые функции.

Практический вывод один: всё, что нужно колбэку, храните в глобальных переменных — как наша g_vocIndex. Это правило действует для всех колбэков idryer-core.

Состояние вентилятора публикуется словарным путём — просто пишите его в поле ядра, когда включаете/выключаете (логика — в главе 7):

s_link.telemetry.fanOn[0] = fanIsOn;

4. Проверка

После прошивки в MQTT-потоке устройства (или в Serial-логе публикаций) телеметрия выглядит так:

{
  "units": [ { "unitId": "U1", "fanStatus": false, "vocIndex": 103 } ],
  "rssi": -52,
  "uptime": 120
}

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

Подышите на датчик или поднесите маркер — индекс должен заметно вырасти за секунды.