Недавно на Хабре вышла статья «HabraTab — девайс для хаброзависимых», которая вызвала неподдельный интерес у хабропользователей и, можно сказать, произвела своего рода фурор (на данный момент рейтинг статьи +137).
Действительно, проект довольно интересный как своей концепцией, так и исполнением, как программным, так железным и даже дизайнерским — девайс выглядит весьма своеобразно и оригинально.
Каждый нашёл в нём что-то своё, сам девайс меня не заинтересовал, но зато заинтересовал код, который может получать данные (кроме Хабра) с различных сайтов в интернете и затем эти данные использовать в IoT системах. Также этот код можно использовать для получения данных со встроенных веб-интерфейсов различных устройств в локальной сети, чему можно найти множество применений в реальных проектах по автоматизации (и не только).
Автор любезно открыл код, что сделало возможным его исследование и модернизацию, чем я с большим удовольствием и занялся. Далее я представлю результаты своих изысканий по этой теме.
Итак, начнём…
❯ План статьи
Статья будет поделена на 5 частей:
- Извлечение движка парсинга данных из оригинального кода
- Добавление подсистемы сбора статистики и анализ её работы
- Приколы в коде
- Пример получения данных о статье (статьях) с Хабра
- Общие вопросы и планы на будущее
Вообще, каждой из этих частей можно посвятить отдельную статью, тут всё зависит от степени детализации и количества объяснений. Я постараюсь осветить все эти вопросы в одной статье, надеюсь мне удастся уложиться в эти рамки.
❯ 1. Извлечение движка
В принципе, HabraTab является законченным и довольно гармоничным устройством и многих он в таком виде вполне устроит — спаял плату, залил прошивку — всё работает и больше от устройства ничего не нужно.
С другой стороны, кому-то не нужны показания температуры и влажности на Хабро-шильдике, а кому-то не нужно на нём текущее время и т. д. А у кого-то есть в наличии другой дисплей, а кому-то нужен крупный шрифт на огромном дисплее и т. п.
Мне, так вообще нужен только движок для использования в IoT системах для получения данных с различных сайтов и веб-интерфейсов сетевых устройств. Поэтому первым естественным желанием у меня было «отделить мух от котлет» и извлечь движок из прошивки HabraTab, чтобы потом можно было его использовать в других проектах.
Ломать, как говорится, — не строить. Или, как любил говаривать старина Микеланджело, — создать шедевр нетрудно, нужно только отсечь всё лишнее. В данном случае операция не очень сложная, но нужно, конечно, иметь какое-то представление о том, что делаешь.
После нескольких взмахов скальпелем и некоторых доработок кода, в моём распоряжении оказался сам движок. Положительным побочным эффектом этой операции стало отсутствие необходимости в дополнительных библиотеках — теперь проект компилируется без них.
Код движка. Нужно понимать, что это не законченное решение, а только первый шаг на долгом пути совершенствования подсистемы получения данных со страниц сайтов при помощи контроллеров на ESP32.
/*
Parsing Engine test
*/
#include <HTTPClient.h>
const char ssid[] = "ssid"; // <--- актуализировать
const char pass[] = "pass"; // <--- актуализировать
String sURL = "https://habr.com/ru/users/";
String userName = "ENGIN33RRR";
WiFiClientSecure client;
HTTPClient http;
String karma = "000";
String ratin = "000.0";
String posit = "000";
void setup() {
xTaskCreatePinnedToCore(
FileUpdate, // функция потока
"Task1", // название потока
10000, // стек потока
NULL, // параметры потока
2, // приоритет потока
NULL, // идентифкатор потока
1); // ядро для выполнения потока
delay(500);
xTaskCreatePinnedToCore(
Graph,
"Task2",
16000,
NULL,
1,
NULL,
0);
delay(500);
} // setup
void connectWifi() {
WiFi.mode(WIFI_STA);
delay(10);
Serial.print(F("Connecting to Wi-Fi"));
WiFi.begin(ssid, pass);
delay(10);
byte count = 0;
while (WiFi.status() != WL_CONNECTED && count < 15) {
Serial.print('.');
count++;
delay(500);
}
Serial.println();
delay(10);
}
void reconnectWifi() {
WiFi.disconnect();
vTaskDelay(1000);
Serial.print(F("Reconnecting to Wi-Fi"));
WiFi.begin(ssid, pass);
byte count = 0;
while (WiFi.status() != WL_CONNECTED && count < 15 ) {
Serial.print('.');
count++;
delay(500);
}
Serial.println();
delay(10);
}
void printValuesFilter() {
Serial.print(karma.toInt()); Serial.print('/');
Serial.print(ratin.toFloat(), 1); Serial.print('/');
Serial.print(posit.toInt()); Serial.println();
}
void getValues() {
Serial.println(F("Request..."));
http.begin(client, sURL + userName + "/"); // открываем HTTP соединение
delay(10);
int httpCode = http.GET();
delay(10);
Serial.print(F(" code: ")); Serial.println(httpCode);
if (httpCode == 200) {
WiFiClient* stream = http.getStreamPtr(); // пребразуем данные в поток Stream
if (stream->available()) {
// Karma
stream->find(R"rawliteral(karma__votes_positive">)rawliteral");
for (int i = 0; i < 5; i++) {
stream->read();
}
for (byte i = 0; i < 5; i++) {
karma[i] = stream->read();
}
// Rating
stream->find(R"rawliteral(tm-rating__counter">)rawliteral");
for (byte i = 0; i < 7; i++) {ratin[i] = stream->read();}
// Position
stream->find("В рейтинге");
for (int i = 0; i < 118; i++) {stream->read();}
for (byte i = 0; i < 4; i++) {posit[i] = stream->read();}
printValuesFilter();
}
delay(10);
} else {
Serial.printf(" (%s)\n", http.errorToString(httpCode).c_str());
}
http.end();
delay(10);
}
void requestWorks() {
if (WiFi.status() == WL_CONNECTED) {
getValues();
} else {
reconnectWifi();
}
}
void FileUpdate(void* pvParameters) {
Serial.begin(115200);
Serial.println();
Serial.println(F("Starting Parsing Engine test..."));
http.setTimeout(3000);
//http.setReuse(true);
connectWifi();
client.setInsecure(); // игнорирование HTTPS сертификатов
for (;;) {
requestWorks();
vTaskDelay(10000);
}
}
void printData() {
//...
}
void Graph(void* pvParameters) { // поток отрисовки
for (;;) {
printData();
vTaskDelay(1);
}
}
void loop() {
}
Этот движок работает и работает вполне прилично, то есть его уже в таком виде можно «засунуть» в прошивку на ESP32 и использовать для своих целей от получения данных о курсах валют до парсинга данных с различных устройств в локальной сети (сетевые принтеры, UPS-ы и т. д.), которые штатно не имеют API интерфейсов и не предусматривают выдачу внутренних данных по запросам из сети.
Скриншот тестовой работы движка. Всё работает хорошо, но есть моменты, о которых мы поговорим далее.
В этой версии кода парсинг сделан на «педальной тяге», здесь вручную в скетче задаются «якоря» и вручную же задаётся алгоритм поиска на странице нужных значений. Недостаток этого метода и его «ахиллесова пята» очевидны: стоит сайту, с которого получают данные, немного изменить HTML код — и работа системы мгновенно «сломается».
stream->find(R"rawliteral(karma__votes_positive">)rawliteral");
for (int i = 0; i < 5; i++) {
stream->read();
}
for (byte i = 0; i < 5; i++) {
karma[i] = stream->read();
}
Для каких-то экспериментов это допустимо, но для готовой системы нужно, конечно, предусматривать решение этой проблемы. Первое, что приходит в голову — это создание веб-интерфейса, в котором можно будет задать якоря и отступы без перекомпиляции самого кода прошивки ESP32.
Примечание. Особо продвинутые программисты могут попытаться автоматизировать этот процесс или вообще переложить бремя поиска новых якорей и отступов на ChatGPT.
Вторая часть проблемы заключается в том, что с сайта мы получаем сырые данные с вкраплениями по(ту)стороннего мусора. Это происходит потому, что мы заранее не можем определить количество разрядов в получаемых данных. Например, рейтинг пользователя может иметь один разряд, а может и три; может быть целым числом, а может иметь дробную часть и т. д.
void printValuesFilter() {
Serial.print(karma.toInt()); Serial.print('/');
Serial.print(ratin.toFloat(), 1); Serial.print('/');
Serial.print(posit.toInt()); Serial.println();
}
Поэтому в текущей версии кода в качестве «фильтра» применяется преобразование строковых значений (сырых данных, полученных с сайта) в значения типов Int и Float. Это паллиативное решение, которое как-то работает, но в дальнейшем, конечно, должно быть заменено на нормальный фильтр.
Следующая проблема заключается в том, что движок в нынешнем его виде, не имеет 100% эффективности, то есть часть запросов выполняется успешно, а часть по тем или иным причинам оканчивается неудачей.
Это конечно «не дело» и так быть не должно. Далее мы попробуем разобраться с причинами возникновения ошибок и вообще глубиной этой проблемы.
❯ Добавление подсистемы сбора статистики и анализ её работы
Все ошибки получения данных и работы движка можно подразделить на два типа:
- Ошибки доступа к сайту и веб-странице.
- Ошибки парсинга и фильтрации данных.
Из просмотра листингов вывода телеметрии в Serial невозможно ничего понять и невозможно сделать какие-то осмысленные и объективные выводы о причинах возникновения ошибок парсинга. Поэтому в код пришлось добавить специальную подсистему сбора статистики работы движка.
Эта подсистема автоматически собирает статистику по произведённым запросам и в реальном времени выводит процентные соотношения по всем типам ошибок. А вот уже на основании этой (объективной) статистики можно будет сделать какие-то осмысленные выводы о качестве работы движка и причинах возникновения ошибок.
Код движка с добавленной подсистемой сбора статистики:
/*
Parsing Engine Stat
*/
#include <HTTPClient.h>
const char ssid[] = "ssid"; // актуализировать
const char pass[] = "pass"; // актуализировать
String sURL = "https://habr.com/ru/users/";
String userName = "ENGIN33RRR";
WiFiClientSecure client;
HTTPClient http;
String karma = "000";
String ratin = "000.0";
String posit = "000";
int kma = 68; // актуализировать
float rtg = 134.3; // актуализировать
int pos = 21; // актуализировать
unsigned long counter1 = 0;
long cntReq = 0;
long cntSuc = 0;
long cntErr = 0;
long cntBad = 0;
void setup() {
xTaskCreatePinnedToCore(
FileUpdate, // функция потока
"Task1", // название потока
10000, // стек потока
NULL, // параметры потока
2, // приоритет потока
NULL, // идентифкатор потока
1); // ядро для выполнения потока
delay(500);
xTaskCreatePinnedToCore(
Graph,
"Task2",
16000,
NULL,
1,
NULL,
0);
delay(500);
} // setup
void FileUpdate(void* pvParameters) {
Serial.begin(115200);
Serial.println();
Serial.println(F("Starting Parsing Engine Stat..."));
http.setTimeout(3000);
//http.setReuse(true);
connectWifi();
client.setInsecure(); // игнорирование HTTPS сертификатов
for (;;) {
requestWorks();
vTaskDelay(10000);
}
}
void Graph(void* pvParameters) { // поток отрисовки
for (;;) {
//counter1Works();
printData();
vTaskDelay(1);
}
}
void loop() {
}
/*
Module Request
*/
void checkBad() {
if (karma.toInt() != kma || ratin.toFloat() != rtg || posit.toInt() != pos) {
cntBad++;
}
}
void getValues() {
Serial.println(F("Request..."));
http.begin(client, sURL + userName + "/"); // открываем HTTP соединение
delay(10);
int httpCode = http.GET();
delay(10);
cntReq++;
Serial.print(F(" code: ")); Serial.println(httpCode);
if (httpCode == 200) {
WiFiClient* stream = http.getStreamPtr(); // пребразуем данные в поток Stream
if (stream->available()) {
// Karma
stream->find(R"rawliteral(karma__votes_positive">)rawliteral");
for (int i = 0; i < 5; i++) {
stream->read();
}
for (byte i = 0; i < 5; i++) {
karma[i] = stream->read();
}
// Rating
stream->find(R"rawliteral(tm-rating__counter">)rawliteral");
for (byte i = 0; i < 7; i++) {ratin[i] = stream->read();}
// Rating
stream->find(R"rawliteral(tm-rating__counter">)rawliteral");
for (byte i = 0; i < 7; i++) {ratin[i] = stream->read();}
// Position
stream->find("В рейтинге");
for (int i = 0; i < 118; i++) {stream->read();}
for (byte i = 0; i < 4; i++) {posit[i] = stream->read();}
//printValuesRaw();
printValuesFilter();
}
cntSuc++;
checkBad();
delay(10);
} else {
Serial.printf(" (%s)\n", http.errorToString(httpCode).c_str());
cntErr++;
}
http.end();
delay(10);
}
void requestWorks() {
if (WiFi.status() == WL_CONNECTED) {
getValues();
printStat();
} else {
reconnectWifi();
}
}
/*
Module Print
*/
void printStat() {
float perc = (float)cntReq / 100.0;
float percSuc = (float)cntSuc / 100.0;
long cntOk = cntSuc - cntBad;
Serial.print(F(" Req:")); Serial.print(cntReq);
Serial.print(F(" Suc:")); Serial.print(cntSuc); Serial.print(F("(")); Serial.print((float)cntSuc/perc, 0);
Serial.print(F("%) Err:")); Serial.print(cntErr); Serial.print(F("(")); Serial.print((float)cntErr/perc, 0);
Serial.print(F("%) Bad:")); Serial.print(cntBad); Serial.print(F("(")); Serial.print((float)cntBad/percSuc, 0);
Serial.print(F("%) Ok:")); Serial.print(cntOk); Serial.print(F("(")); Serial.print((float)cntOk/perc, 0);
Serial.print(F("%)"));
Serial.println();
}
void printValuesFilter() {
Serial.print(karma.toInt()); Serial.print('/');
Serial.print(ratin.toFloat(), 1); Serial.print('/');
Serial.print(posit.toInt()); Serial.println();
}
void printValuesRaw() {
Serial.println(karma);
Serial.println(ratin);
Serial.println(posit);
}
void counter1Works() {
if (millis() > counter1 + 1000) {
Serial.println('.');
counter1 = millis();
}
}
/*
Module Wi-Fi
*/
void connectWifi() {
WiFi.mode(WIFI_STA);
delay(10);
Serial.print(F("Connecting to Wi-Fi"));
WiFi.begin(ssid, pass);
delay(10);
byte count = 0;
while (WiFi.status() != WL_CONNECTED && count < 15) {
Serial.print('.');
count++;
delay(500);
}
Serial.println();
delay(10);
}
void reconnectWifi() {
WiFi.disconnect();
vTaskDelay(1000);
Serial.print(F("Reconnecting to Wi-Fi"));
WiFi.begin(ssid, pass);
byte count = 0;
while (WiFi.status() != WL_CONNECTED && count < 15 ) {
Serial.print('.');
count++;
delay(500);
}
Serial.println();
delay(10);
}
Стартуем движок и наблюдаем за статистикой. В начале всё нормально, но уже третий запрос заканчивается ошибкой. Для каких-то выводов ждём сбора статистики с нескольких сотен запросов.
Расшифровка сокращений в строке статистики:
Req — общее количество произведённых запросов и номер текущего запроса.
Suc — количество успешных запросов к серверу и их процентное соотношение. «Успешных» технически, то есть вообще полученных от сервера. Качество ответов не учитывается, данные в ответе могут быть и «битыми».
Err — количество запросов к серверу, которые завершились ошибкой (то есть ответ вообще не получен) и их процентное соотношение.
Bad — данные получены, но они «битые» и их процентное отношение ко всем полученным данным (но не к количеству всех запросов).
Ok — количество успешно завершённых запросов и их процентное соотношение к количеству всех произведённых запросов (по существу «главный» параметр, который определяет качество работы всей системы в целом).
Небольшие пояснения по новому варианту кода.
Добавляем в скетч переменные для подсчёта статистики:
long cntReq = 0;
long cntSuc = 0;
long cntErr = 0;
long cntBad = 0;
Для определения количества полученных «битых» данных от сервера вручную добавляем в скетч заведомо правильные значения (на момент запуска теста).
int kma = 68; // актуализировать
float rtg = 137.3; // актуализировать
int pos = 20; // актуализировать
В функции printStat() подсчитываем процентные соотношения и выводим в Serial статистику по запросам.
void printStat() {
float perc = (float)cntReq / 100.0;
float percSuc = (float)cntSuc / 100.0;
long cntOk = cntSuc - cntBad;
Serial.print(F(" Req:")); Serial.print(cntReq);
Serial.print(F(" Suc:")); Serial.print(cntSuc); Serial.print(F("(")); Serial.print((float)cntSuc/perc, 0);
Serial.print(F("%) Err:")); Serial.print(cntErr); Serial.print(F("(")); Serial.print((float)cntErr/perc, 0);
Serial.print(F("%) Bad:")); Serial.print(cntBad); Serial.print(F("(")); Serial.print((float)cntBad/percSuc, 0);
Serial.print(F("%) Ok:")); Serial.print(cntOk); Serial.print(F("(")); Serial.print((float)cntOk/perc, 0);
Serial.print(F("%)"));
Serial.println();
}
Ниже представлен скриншот со статистикой работы движка после 200 запросов к серверу Хабра. Цифры процентов могут немного «гулять» из-за округления до целых значений. Это округление сделано умышленно — тут нам не важны десятые и сотые доли процентов — нам нужно понять общую картину качества работы движка.
Из представленного скриншота видно, что около 9% запросов к серверу Хабра оканчиваются неудачей. Как правило это код 7 (no HTTP server) и код 11 (read Timeout). Иногда встречаются и более экзотические ошибки. Трудно сказать в чём причина этих ошибок — возможно это связано с тем, что сервер Хабра не справляется с пиковыми нагрузками от множества клиентов. Кстати, различные «глюки» загрузки страниц Хабра наблюдаются и при работе с обычным веб-браузером.
Нужно сказать, вышеприведённая статистика работы движка довольно благостная — ошибок относительно немного и такие ошибки хорошо детектируются и обходятся в коде. Но не всё так радужно: иногда (по пока невыясненным мной причинам) начинают «сыпаться» Bad ошибки парсинга переменных. То ли это связано с сервером Хабра, то ли с самим кодом движка, но я бы поставил на какие-то глюки работы FreeRTOS, её алгоритмов распределения памяти и работы со стеком и кучей — по косвенным признакам очень похоже на подобную природу «глюков».
Сетевое взаимодействие и работа движка — это динамические процессы, то есть определённым (нелинейным) образом растянутые по времени, поэтому для более полного понимания работы системы нужно каким-то образом отображать временные интервалы происходящих событий.
Для этого в Serial вывод добавляются секундные маркеры (в виде точек) — и сразу становится видна динамика сетевого взаимодействия движка и сайта.
void counter1Works() {
if (millis() > counter1 + 1000) {
Serial.println('.');
counter1 = millis();
}
}
Динамический режим вывода можно включать и отключать в скетче — он не всегда нужен, иногда важна не динамика, а только последовательность событий и текущие значения параметров и элементов системы.
void Graph(void* pvParameters) { // поток отрисовки
for (;;) {
//counter1Works();
printData();
vTaskDelay(1);
}
}
Например, в динамическом режиме хорошо видно, что на получение ответа (страницы) от сервера Хабра, после посылки запроса к нему, уходит около трёх секунд, а вот поиск значений в HTML коде страницы и их обработка происходит практически «мгновенно».
❯ Приколы в коде
В процессе экспериментов я столкнулся с необъяснимым для меня поведением компилятора, который я иначе как «приколом» назвать не могу.
Вышеприведённый код движка с подсистемой сбора статистики прекрасно компилируется и работает и в нём есть такой (совершенно безобидный) фрагмент:
// Rating
stream->find(R"rawliteral(tm-rating__counter">)rawliteral");
for (byte i = 0; i < 7; i++) {ratin[i] = stream->read();}
Но, если продублировать этот фрагмент в коде
// Rating
stream->find(R"rawliteral(tm-rating__counter">)rawliteral");
for (byte i = 0; i < 7; i++) {ratin[i] = stream->read();}
// Rating
stream->find(R"rawliteral(tm-rating__counter">)rawliteral");
for (byte i = 0; i < 7; i++) {ratin[i] = stream->read();}
то скетч перестаёт компилироваться, чего никак не может быть в принципе — если этот участок кода один раз прошёл проверку компилятором, то и второй раз обязан её пройти.
Я пока не успел разобраться с этим вопросом, но у меня есть два предположения:
- Это ошибка компилятора (что маловероятно и в это с трудом верится).
- Сам код содержит синтаксическую ошибку, которая по каким-то причинам «пропускается» компилятором.
Надеюсь, «старшие товарищи» внесут ясность в этот вопрос и объяснят как такое вообще может быть.
❯ Пример получения данных о статье (статьях) с Хабра
Здесь нас ожидает «облом» в самом неожиданном месте. У меня уже был готов код для этого раздела и я начал его описание, но меня вдруг посетила мысль:
«Ё! Когда мы получаем данные со страницы пользователя на Хабре, то это не очень изящное, но более-менее допустимое действие, но когда мы получаем данные о параметрах статьи со страницы самой этой статьи, то каждый раз заново загружаем всю страницу и тем самым… попутно увеличиваем количество просмотров!»
Очевидно, что это уже (хоть и ненамеренно) выходит за рамки безобидных экспериментов с электричеством и может быть неоднозначно воспринято администрацией Хабра.
Поэтому публикацию этого раздела и кода я остановил и обратился к администрации Хабра за официальными комментариями и разъяснениями её позиции по этому вопросу.
Официального ответа я пока не получил, возможно он последует после публикации этой статьи.
А пока администрация Хабра размышляет над свой позицией по этому вопросу, мы можем немного порассуждать об ещё одной интересной теме — API Хабра.
Насколько я понял из неофициальных контактов с официальными представителями Хабра, API у Хабра есть, но он непубличный. Мне со стороны трудно рассуждать о технических и организационных проблемах, не дающих сделать его публичным, но, если всё-таки есть такая возможность, то может быть настало время Хабру, наконец, запустить в полноценную работу этот сервис.
Ждём-с…
❯ Общие вопросы и планы на будущее
Ну и напоследок несколько вопросов, которые у меня возникли в процессе экспериментов с движком HabraTab:
- Почему одни и те же запросы на одних и тех же страницах иногда дают различные результаты? По идее, так не должно быть и сырые данные всегда должны быть одинаковыми (при одинаковых исходных условиях).
- В чём причина спорадического возникновения Bad ошибок?
- Как на ESP32 получать данные с сайтов, которые требуют для своей работы Javascript?
- Как на ESP32 получать данные с сайтов, которые требуют авторизации?
Планы на будущее:
Основная проблема на данный момент — возникновения Bad ошибок, поэтому в планах в первую очередь разобраться с причинами этого явления и сделать движок на 100% стабильным.
Затем можно поэкспериментировать с сетевым взаимодействием и поднять его эффективность с 90 до 100%.
Далее можно будет упростить или автоматизировать определение якорей и отступов на целевых страницах.
Ну и т. д. и т. п., усовершенствовать движок можно до бесконечности или в соответствии с требованием конкретных проектов.
❯ Заключение
Как пример: в моём хозяйстве есть замечательный сетевой блок бесперебойного питания APC Back-UPS HS 500, который имеет веб-интерфейс, но не имеет сетевого API, через которое я бы мог получать данные о его текущих параметрах для интеграции в систему «умного дома».
Раньше эту проблему я решал при помощи инструментария системы MajorDoMo, теперь можно попробовать отказаться от её услуг и решить проблему с помощью маломощного и недорогого контроллера на ESP32.
Я надеюсь, что на этих примерах мне удалось донести до вас потенциал движка HabraTab для использования в IoT проектах и проектах по автоматизации.
Комментарии (6)
lcf_m
00.00.0000 00:00+1Я думаю что проблема в том что у вас кавычки внутри кавычек, даже сейчас хабр отображает два куска кода по-разному. Попробуйте испраить внутренние на одинарные.
smart_alex Автор
00.00.0000 00:00Дело в том, что эти кавычки являются частью якоря и если их исправить на одинарные, то якорь перестанет работать.
Кроме того, возникает вопрос: если это синтаксическая ошибка, то как её пропускает компилятор в первом случае? и каким образом в этом случае этот код корректно работает?
И ещё вопрос: все руководства утверждают, что всё, что находится между спец-словами и круглыми скобками не воспринимается компилятором как код.
То есть тут требуется, чтобы кто-нибудь квалифицированно объяснил в чём тут дело.
smart_alex Автор
00.00.0000 00:00Кстати, я вообще первый раз за свою обширную практику кодинга встречаюсь с ситуацией, когда копирование куска кода, прошедшего компиляцию, приводит к невозможности компиляции проекта.
max_rip
В идеале, надо сделать какую то прослойку, которая просто будет отдавать текст и который уже отображать на экране.
А формировать текст уже через какие-то другие средства, которые могут опрашивать источники фоном и отдавать данные моментально, на основание ранее полученных значений.
smart_alex Автор
Не совсем понял вашу мысль, но движок уже позволяет получать различные данные и как угодно их использовать — не только выводить на экран, но и задавать (при необходимости) пороговые значения и оповещать любым способом от включения света до посылки SMS или отправки сообщений в Telegram и т. д. и т. п. И вообще задавать любую реакцию, на которую фантазии хватит.
(Имеется в виду движок, как модуль более сложной системы на ESP32 или распределённой IoT системы ESP32 + любые контроллеры с любыми интерфейсами и функциями.)