{"id":468375,"date":"2025-07-23T21:00:27","date_gmt":"2025-07-23T21:00:27","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=468375"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=468375","title":{"rendered":"<span>\u0420\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 IoT \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043d\u0443\u043b\u044f \u043e\u0442 \u0447\u0430\u0439\u043d\u0438\u043a\u0430 \u0434\u043b\u044f \u0447\u0430\u0439\u043d\u0438\u043a\u043e\u0432<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0414\u0430\u0432\u043d\u043e \u0447\u0435\u0441\u0430\u043b\u0438\u0441\u044c \u0440\u0443\u043a\u0438 \u0447\u0442\u043e\u0431\u044b \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u00a0\u043d\u0443\u043b\u044f. \u0414\u0440\u0443\u0437\u044c\u044f \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u043b\u0438 \u0432\u00a0\u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u043e\u0434\u0430\u0440\u043a\u0430 \u043d\u0430\u00a0\u0434\u0435\u043d\u044c \u0440\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0443\u0440\u043e\u0432\u043d\u044f CO2 \u0432 \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u0434\u043e\u043c\u0430. \u0422\u0430\u043a, \u0437\u0430\u0440\u043e\u0434\u0438\u043b\u0430\u0441\u044c \u0438\u0434\u0435\u044f. <\/p>\n<h3>\u041f\u043e\u0438\u0441\u043a \u043d\u0430 \u043c\u0430\u0440\u043a\u0435\u0442\u043f\u043b\u0435\u0439\u0441\u0430\u0445<\/h3>\n<p>\u041f\u0435\u0440\u0432\u043e\u0435, \u0441\u00a0\u0447\u0435\u0433\u043e \u044f \u043d\u0430\u0447\u0430\u043b, \u044d\u0442\u043e \u043f\u043e\u0438\u0441\u043a \u0434\u0435\u0432\u0430\u0439\u0441\u0430 \u043d\u0430\u00a0\u043c\u0430\u0440\u043a\u0435\u0442\u043f\u043b\u0435\u0439\u0441\u0430\u0445. \u0414\u0430, \u043b\u0435\u043d\u044c \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043b\u044c \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441\u0430! \u041c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432\u00a0\u043f\u0440\u043e\u0434\u0430\u0436\u0435 \u0433\u043e\u0442\u043e\u0432\u044b\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u044f, \u043d\u043e\u00a0\u0432\u00a0\u043d\u0438\u0445 \u0441\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e \u043d\u0435\u0442 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u00a0\u0443\u043c\u043d\u044b\u043c \u0434\u043e\u043c\u043e\u043c (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043e\u0442\u00a0\u042f\u043d\u0434\u0435\u043a\u0441\u0430) \u0438\u043b\u0438\u00a0\u0436\u0435 \u0441\u0430\u043c\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u0431\u0440\u0430\u043d\u043e \u043d\u0430\u00a0\u043e\u0441\u043d\u043e\u0432\u0435 \u0441\u043e\u043c\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430.<\/p>\n<h3>\u041d\u0430\u0439\u0442\u0438 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435<\/h3>\n<p>\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0448\u0430\u0433\u00a0\u2014 \u044d\u0442\u043e \u0441\u0431\u043e\u0440 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0439 \u043a\u00a0\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u043b\u043e\u0441\u044c \u0432\u00a0\u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u00a0\u0443\u043c\u043d\u044b\u043c \u0434\u043e\u043c\u043e\u043c \u043e\u0442\u00a0\u042f\u043d\u0434\u0435\u043a\u0441\u0430 (\u0423\u0414\u042f). \u0421\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c, \u0447\u0442\u043e:<\/p>\n<ol>\n<li>\n<p>\u0415\u0441\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u00a0<code>\u0423\u0414\u042f<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430\u00a0\u0441\u0442\u043e\u043b \u0438\/\u0438\u043b\u0438\u00a0\u043f\u0440\u0438\u043a\u0440\u0435\u043f\u0438\u0442\u044c \u043d\u0430\u00a0\u0441\u0442\u0435\u043d\u0443;<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u043e\u0441\u0442\u043e\u0439 \u0438 \u0447\u0438\u0442\u0430\u0435\u043c\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u043b\u044f\u00a0\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0441\u00a0\u0434\u0438\u0441\u043f\u043b\u0435\u0435\u043c \u0438\/\u0438\u043b\u0438 \u0432\u0435\u0431\u2011\u043f\u0430\u043d\u0435\u043b\u044c\u044e;<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u00a0\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0433\u043e\u0432\u043d\u043e \u0438 \u043f\u0430\u043b\u043a\u0438 (\u043f\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438).<\/p>\n<\/li>\n<\/ol>\n<p>\u0413\u043b\u0430\u0432\u043d\u043e\u0435 \u0447\u0442\u043e\u00a0\u043c\u0435\u043d\u044f \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043e\u0432\u0430\u043b\u043e \u044d\u0442\u043e \u043a\u0430\u043a\u00a0\u0441\u0432\u044f\u0437\u0430\u0442\u044c \u0433\u043e\u0442\u043e\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u00a0<code>\u0423\u0414\u042f<\/code>. \u041a\u0430\u043a\u0438\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0442 \u0441\u043f\u043e\u0441\u043e\u0431\u044b \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u00a0<code>\u0423\u0414\u042f<\/code>?<\/p>\n<ul>\n<li>\n<p>ZigBee;<\/p>\n<\/li>\n<li>\n<p>Wi\u2011Fi Matter;<\/p>\n<\/li>\n<li>\n<p>\u0421\u0432\u043e\u0439 \u0431\u044d\u043a\u0435\u043d\u0434\u2011\u043c\u043e\u0441\u0442.<\/p>\n<\/li>\n<\/ul>\n<p><code>ZigBee<\/code> \u0438 <code>Wi-Fi Matter<\/code> \u0442\u0440\u0435\u0431\u0443\u044e\u0442 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u0448\u043b\u044e\u0437), \u0430\u00a0\u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u043e\u0433\u043e \u043c\u043e\u0441\u0442\u0430 \u0435\u0449\u0435 \u0431\u043e\u043b\u0435\u0435 \u0437\u0430\u0442\u0440\u0430\u0442\u043d\u044b\u0439 \u043f\u043e\u00a0\u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043f\u043e\u0441\u043e\u0431. \u042f \u043d\u0430\u0447\u0430\u043b \u043a\u043e\u043f\u0430\u0442\u044c \u0432\u00a0\u0441\u0442\u043e\u0440\u043e\u043d\u0443 \u0434\u0440\u0443\u0433\u0438\u0445 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0418 \u043e\u043d\u0438 \u0435\u0441\u0442\u044c. \u0421\u0430\u043c \u042f\u043d\u0434\u0435\u043a\u0441 \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 IoT \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0441\u00a0\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u00ab\u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439\u00bb (\u041e\u0424), \u0438\u0445 \u0435\u0449\u0435 \u043d\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u00ab\u043b\u044f\u043c\u0431\u0434\u044b\u00bb. \u0414\u0430\u0436\u0435 \u0435\u0441\u0442\u044c <a href=\"https:\/\/github.com\/AlexandrSurkov\/YandexCloudAirMonitor\/tree\/master\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u0438\u043c\u0435\u0440 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 ESP8266<\/a>\u00a0\u0438\u043b\u0438\u00a0\u0436\u0435 <a href=\"https:\/\/github.com\/munrexio\/yandex2mqtt\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u0438\u043c\u0435\u0440 \u043c\u043e\u0441\u0442\u0430<\/a>. \u041d\u043e\u00a0\u043a\u0430\u043a\u00a0\u043a\u043e\u0434, \u0442\u0430\u043a \u0438 \u0441\u0430\u043c \u043f\u043e\u0434\u0445\u043e\u0434 \u043c\u043d\u0435 \u043d\u0435\u00a0\u043f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u0438\u0441\u044c \u0438\u0437\u2011\u0437\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442\u00a0\u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0438 API \u042f\u043d\u0434\u0435\u043a\u0441\u0430. \u0412\u00a0\u0438\u0442\u043e\u0433\u0435 \u0432\u044b\u0431\u043e\u0440 \u043f\u0430\u043b \u043d\u0430\u00a0\u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 <code>wqtt.ru<\/code>\u00a0\u2014 \u043f\u043e\u043d\u044f\u0442\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0438 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u0441\u00a0\u043f\u0440\u0438\u043c\u0435\u0440\u0430\u043c\u0438. \u0415\u0441\u043b\u0438 \u043a\u043e\u0440\u043e\u0442\u043a\u043e, \u0442\u043e \u043d\u0430\u0448\u0435 \u0431\u0443\u0434\u0443\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0432\u00a0<code>wqtt.ru<\/code> \u043f\u043e\u00a0\u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 <code>MQTT<\/code>. \u0414\u0430\u043b\u0435\u0435 \u043c\u0435\u0436\u0434\u0443 <code>wqtt.ru<\/code> \u0438 <code>\u0423\u0414\u042f<\/code> \u0435\u0441\u0442\u044c \u0433\u043e\u0442\u043e\u0432\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438 \u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043c \u0434\u043e\u0441\u0442\u0443\u043f \u043a\u00a0\u0434\u0430\u043d\u043d\u044b\u043c \u0432\u00a0\u0432\u0438\u0434\u0435 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0414\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 <code>wqtt.ru<\/code> \u0443\u043f\u0430\u0434\u0435\u0442, \u0442\u043e \u0432\u0441\u0435\u0433\u0434\u0430 \u0435\u0441\u0442\u044c \u043f\u043b\u0430\u043d \u0411. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043d\u044f\u0442\u044c \u0441\u0432\u043e\u0439 \u0431\u0440\u043e\u043a\u0435\u0440 <code>MQTT<\/code> + <code>HomeAssistant<\/code>. \u0418\u043b\u0438\u00a0\u0436\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0430\u0448\u0431\u043e\u0440\u0434 \u043d\u0430\u00a0<code>Grafana<\/code> \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0432 <code>Prometheus<\/code> \u0432\u00a0\u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u0440\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0437\u00a0<code>MQTT<\/code> \u0431\u0440\u043e\u043a\u0435\u0440\u0430 \u0438 <code>Node\u2011RED<\/code> \u0434\u043b\u044f\u00a0\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0432. <\/p>\n<h3>\u041d\u0430\u0439\u0442\u0438 \u043a\u043e\u0440\u043f\u0443\u0441 \u0438\u043b\u0438 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0435\u0433\u043e \u0441\u0430\u043c\u043e\u043c\u0443?<\/h3>\n<p>\u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u0432\u044b\u0434\u0443\u043c\u044b\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0435, \u044f \u043d\u0430\u0447\u0430\u043b \u0438\u0441\u043a\u0430\u0442\u044c \u0443\u0436\u0435 \u0433\u043e\u0442\u043e\u0432\u044b\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0436\u043d\u043e \u043c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u0441\u0435\u0431\u044f? \u0418 \u0434\u0430, \u0442\u0430\u043a\u043e\u0435 \u0442\u043e\u0436\u0435 \u0435\u0441\u0442\u044c. \u041f\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0443 <code>GeekMagic<\/code><strong> <\/strong>\u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u043c\u0430\u043b\u0435\u043d\u044c\u043a\u0438\u0435 \u043a\u043e\u0440\u043e\u0431\u043e\u0447\u043a\u0438 \u0441 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u043c \u0438 \u043c\u0438\u043a\u0440\u043e\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c (\u0432 \u043e\u0431\u044b\u0447\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 <code>ESP12F<\/code> \u0438\u043b\u0438 <code>ESP32<\/code> \u0435\u0441\u043b\u0438 pro \u0432\u0435\u0440\u0441\u0438\u044f). <\/p>\n<details class=\"spoiler\">\n<summary>GeekMagic<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1e9\/26f\/d67\/1e926fd67932d88774209a99b48c1d68.jpg\" width=\"491\" height=\"496\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/1e9\/26f\/d67\/1e926fd67932d88774209a99b48c1d68.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/1e9\/26f\/d67\/1e926fd67932d88774209a99b48c1d68.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<p>\u042d\u0442\u043e \u043d\u0435\u043f\u043b\u043e\u0445\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442, \u0442.\u043a. \u043c\u0435\u043d\u0435\u0435 \u0447\u0435\u043c \u0437\u0430 1\u043a \u0440\u0443\u0431\u043b\u0435\u0439 (\u0443\u0441\u043b\u043e\u0432\u043d\u043e) \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438 \u043a\u043e\u0440\u043f\u0443\u0441, \u0438 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u0443\u0435\u043c\u044b\u0439 \u043c\u0438\u043a\u0440\u043e\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440. \u0412\u043e\u0442 \u043a\u0441\u0442\u0430\u0442\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/882370\/\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u0438\u043c\u0435\u0440<\/a> \u0441\u0442\u0430\u0442\u044c\u0438 \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0430\u0432\u0442\u043e\u0440 \u043a\u0430\u0441\u0442\u043e\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043b \u0442\u0430\u043a\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \u041d\u043e \u043d\u0443\u0436\u043d\u043e \u0436\u0434\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0443 \u0438 \u0435\u0441\u0442\u044c \u043d\u0435\u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0441 \u043f\u0435\u0440\u0435\u043f\u0440\u043e\u0448\u0438\u0432\u043a\u043e\u0439, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043e\u0441\u0442\u0430\u0432\u0438\u043c \u044d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0434\u043b\u044f \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430. \u0418 \u043e\u043f\u044f\u0442\u044c \u0436\u0435 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a, \u0447\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u043a\u0430\u0441\u0442\u043e\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0439 \u0431\u044d\u043a\u0435\u043d\u0434. &#171;\u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432 \u0434\u0440\u0443\u0433\u043e\u0439 \u0440\u0430\u0437, \u043d\u043e \u0442\u043e\u0447\u043d\u043e \u043d\u0435 \u0441\u0435\u0433\u043e\u0434\u043d\u044f&#187; &#8212; \u0441\u043a\u0430\u0437\u0430\u043b \u044f \u0441\u0435\u0431\u0435.<\/p>\n<p>&#171;\u0427\u0442\u043e \u0438\u0437 \u0441\u0435\u0431\u044f \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u043c\u043e\u0451 \u0431\u0443\u0434\u0443\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e?&#187;. \u041e\u043d\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c: <\/p>\n<ul>\n<li>\n<p>\u041a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u044b\u043c \u0438 \u0438\u043c\u0435\u0442\u044c \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (\u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0438\/\u0438\u043b\u0438 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u044c);<\/p>\n<\/li>\n<li>\n<p>\u0421 \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u043c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u043a \u0441\u0435\u0442\u0438; <\/p>\n<\/li>\n<li>\n<p>\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0434\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 \u043d\u0435\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442-\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f. <\/p>\n<\/li>\n<li>\n<p>\u0411\u044b\u043b\u0430 \u0438\u0434\u0435\u044f \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0435 \u0438 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c, \u0442.\u0435. \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0430\u043a\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440, \u043d\u043e \u0442\u043e\u0433\u0434\u0430 \u0431\u044b \u044d\u0442\u043e \u0443\u0441\u043b\u043e\u0436\u043d\u0438\u043b\u043e \u043a\u0430\u043a \u0441\u0445\u0435\u043c\u0443, \u0442\u0430\u043a \u0438 \u043a\u043e\u0434. <\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0430\u0447\u043d\u0435\u043c \u0441 \u043a\u043e\u0440\u043f\u0443\u0441\u0430. \u0412\u043f\u0435\u0440\u0432\u044b\u0435 \u0440\u0435\u0448\u0438\u043b \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u043b \u0441\u0434\u0435\u043b\u0430\u0442\u044c 3D \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u043b\u044f \u043f\u0435\u0447\u0430\u0442\u0438. \u041d\u0430 \u043e\u0441\u0432\u043e\u0435\u043d\u0438\u0435 CAD (Autodesk Fusion 360) \u0443\u0448\u043b\u043e \u043f\u0430\u0440\u0443 \u0434\u043d\u0435\u0439, \u0430 \u0435\u0449\u0451 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441\u043e\u0432 \u2014 \u043d\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0442\u043e\u0442\u0438\u043f\u0430 \u0434\u043b\u044f \u0441\u0431\u043e\u0440\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0415\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c \u043d\u0435 \u0441 \u043f\u0435\u0440\u0432\u043e\u0439 \u043f\u043e\u043f\u044b\u0442\u043a\u0438. \u0412 \u043f\u0435\u0440\u0432\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u043d\u0435 \u0445\u0432\u0430\u0442\u0430\u043b\u043e \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u043e\u0442\u0432\u0435\u0440\u0441\u0442\u0438\u0439, \u0430 \u0432 \u0434\u0440\u0443\u0433\u043e\u0439 \u043f\u0440\u043e\u0432\u043e\u0434\u043a\u0430 \u0441 \u0442\u0440\u0443\u0434\u043e\u043c \u043f\u043e\u043c\u0435\u0449\u0430\u043b\u0430\u0441\u044c \u0432\u043d\u0443\u0442\u0440\u044c \u043a\u043e\u0440\u043f\u0443\u0441\u0430 \u0438 \u043f\u0440\u043e\u0447\u0438\u0435 \u043c\u0435\u043b\u043a\u0438\u0435 \u043d\u0435\u0434\u043e\u0447\u0435\u0442\u044b \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0410 \u0435\u0449\u0435 \u044f \u0441\u043f\u0443\u0442\u0430\u043b \u043c\u0430\u0441\u0448\u0442\u0430\u0431 \u043c\u043e\u0434\u0435\u043b\u0438 \u0438 \u043f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u043d\u0430 \u044d\u0442\u0430\u043f\u0435 \u043f\u0435\u0447\u0430\u0442\u0438 \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0442\u044c \u0440\u0430\u0437\u043c\u0435\u0440. \u0412 \u043e\u0431\u0449\u0435\u043c, \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0432\u043e\u0440\u0447\u0435\u0441\u043a\u0438\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441, \u043d\u043e \u0438 \u0441\u043b\u043e\u0436\u043d\u0430\u044f \u0438\u043d\u0436\u0435\u043d\u0435\u0440\u043d\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430. <\/p>\n<details class=\"spoiler\">\n<summary>3D \u043c\u043e\u0434\u0435\u043b\u044c \u043a\u043e\u0440\u043f\u0443\u0441\u0430<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/a2c\/1ae\/e4d\/a2c1aee4dd63167cb89098cc6963a248.png\" alt=\"\u0412\u0438\u0434 \u0432 \u0441\u0431\u043e\u0440\u0435\" title=\"\u0412\u0438\u0434 \u0432 \u0441\u0431\u043e\u0440\u0435\" width=\"1034\" height=\"1012\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/a2c\/1ae\/e4d\/a2c1aee4dd63167cb89098cc6963a248.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/a2c\/1ae\/e4d\/a2c1aee4dd63167cb89098cc6963a248.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0412\u0438\u0434 \u0432 \u0441\u0431\u043e\u0440\u0435<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/218\/c8e\/658\/218c8e658aa8926a40b4e49c6b253bd9.png\" alt=\"\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043e\u0440\u043f\u0443\u0441 (\u0432\u0438\u0434 \u0441\u043e \u0441\u0442\u043e\u0440\u043e\u043d\u044b \u043a\u0440\u044b\u0448\u043a\u0438)\" title=\"\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043e\u0440\u043f\u0443\u0441 (\u0432\u0438\u0434 \u0441\u043e \u0441\u0442\u043e\u0440\u043e\u043d\u044b \u043a\u0440\u044b\u0448\u043a\u0438)\" width=\"1070\" height=\"1052\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/218\/c8e\/658\/218c8e658aa8926a40b4e49c6b253bd9.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/218\/c8e\/658\/218c8e658aa8926a40b4e49c6b253bd9.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043e\u0440\u043f\u0443\u0441 (\u0432\u0438\u0434 \u0441\u043e \u0441\u0442\u043e\u0440\u043e\u043d\u044b \u043a\u0440\u044b\u0448\u043a\u0438)<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/619\/3e0\/3c4\/6193e03c46ffe93081d3fbd53a1bf0c4.png\" alt=\"\u041a\u0440\u044b\u0448\u043a\u0430\" title=\"\u041a\u0440\u044b\u0448\u043a\u0430\" width=\"1166\" height=\"1052\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/619\/3e0\/3c4\/6193e03c46ffe93081d3fbd53a1bf0c4.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/619\/3e0\/3c4\/6193e03c46ffe93081d3fbd53a1bf0c4.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u041a\u0440\u044b\u0448\u043a\u0430<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/eb1\/059\/8ad\/eb10598ad80e24139a6a721439d29305.png\" alt=\"\u041a\u0440\u044b\u0448\u043a\u0430 \u0432\u043d\u0443\u0442\u0440\u0438\" title=\"\u041a\u0440\u044b\u0448\u043a\u0430 \u0432\u043d\u0443\u0442\u0440\u0438\" width=\"986\" height=\"916\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/eb1\/059\/8ad\/eb10598ad80e24139a6a721439d29305.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/eb1\/059\/8ad\/eb10598ad80e24139a6a721439d29305.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u041a\u0440\u044b\u0448\u043a\u0430 \u0432\u043d\u0443\u0442\u0440\u0438<\/figcaption><\/div>\n<\/figure>\n<\/div>\n<\/details>\n<h3>\u0421\u0431\u043e\u0440\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430<\/h3>\n<p>\u041a\u043e\u0440\u043f\u0443\u0441 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d \u043d\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u043e\u0434\u043d\u043e\u0433\u043e ESP-\u043f\u043e\u0434\u043e\u0431\u043d\u043e\u0433\u043e \u043c\u0438\u043a\u0440\u043e\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430, \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0443\u0440\u043e\u0432\u043d\u044f CO2 \u0438 \u0435\u0449\u0435 \u043e\u0434\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b), RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u0430. \u0415\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430 MagSafe \u0434\u0435\u0440\u0436\u0430\u0442\u0435\u043b\u044c. \u041a\u0430\u043a-\u0442\u043e \u0442\u0430\u043a \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0432\u044b\u0448\u043b\u043e, \u0447\u0442\u043e \u044f \u0443\u0433\u0430\u0434\u0430\u043b \u0441 \u0440\u0430\u0437\u043c\u0435\u0440\u0430\u043c\u0438 \u0438 \u043c\u0430\u0433\u043d\u0438\u0442\u043d\u043e\u0435 \u043a\u043e\u043b\u044c\u0446\u043e \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u043e \u0441\u0435\u043b\u043e \u043d\u0430 \u0437\u0430\u0434\u043d\u044e\u044e \u043a\u0440\u044b\u0448\u043a\u0443. \u041f\u0440\u0438 \u0431\u043e\u043b\u044c\u0448\u043e\u043c \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0434\u0430\u0442\u0447\u0438\u043a (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0438 \u0434\u0430\u0432\u043b\u0435\u043d\u0438\u044f) \u0442.\u043a. \u0435\u0441\u0442\u044c \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0435 \u043c\u0435\u0441\u0442\u043e. \u0412 \u0438\u0442\u043e\u0433\u0435, \u0430\u043f\u043f\u0430\u0440\u0430\u0442\u043d\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043b\u0430\u0441\u044c \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u0440\u043f\u0443\u0441\u0430 \u0438 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442:<\/p>\n<ul>\n<li>\n<p>\u041c\u0438\u043a\u0440\u043e\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u00a0\u2014 <code>ESP32 Live Mini<\/code>. \u0412\u044b\u0431\u043e\u0440 \u041c\u041a \u043d\u0438\u0447\u0435\u043c \u043d\u0435 \u043e\u0431\u043e\u0441\u043d\u043e\u0432\u0430\u043d, \u0440\u0430\u0437\u0432\u0435 \u0447\u0442\u043e \u043e\u043d \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u043f\u043e \u0444\u043e\u0440\u043c \u0444\u0430\u043a\u0442\u043e\u0440\u0443 \u043f\u043e\u0434 <code>ESP8266 Wemos Mini<\/code>. \u0427\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435 \u044f \u0443\u0437\u043d\u0430\u043b \u043e \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043e\u0432\u0430\u043d\u0438\u0438 <code>ESP 32 S2<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430\u043c \u0442\u043e\u0447\u043d\u043e \u043d\u0435 \u0445\u0443\u0436\u0435 \u0438 \u0443 \u043d\u0435\u0433\u043e \u0431\u043e\u043b\u0435\u0435 \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 usb type-c. \u0421\u0430\u043c\u0443 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0434\u0430\u043f\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u0434\u0430\u043d\u043d\u044b\u0439 \u041c\u041a \u0442\u043e\u0436\u0435. <\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>ESP32 Live Mini<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9dd\/834\/31d\/9dd83431d5cc4d367b5dbce62d317832.jpeg\" width=\"600\" height=\"600\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/9dd\/834\/31d\/9dd83431d5cc4d367b5dbce62d317832.jpeg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9dd\/834\/31d\/9dd83431d5cc4d367b5dbce62d317832.jpeg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c55\/fdc\/d2a\/c55fdcd2a2001d77cc17069a9c63070f.png\" width=\"1356\" height=\"1222\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/c55\/fdc\/d2a\/c55fdcd2a2001d77cc17069a9c63070f.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/c55\/fdc\/d2a\/c55fdcd2a2001d77cc17069a9c63070f.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>ESP32 S2 Mini<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f20\/4d0\/dd2\/f204d0dd2176632cd2bcec3bbdc76646.png\" width=\"776\" height=\"782\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/f20\/4d0\/dd2\/f204d0dd2176632cd2bcec3bbdc76646.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f20\/4d0\/dd2\/f204d0dd2176632cd2bcec3bbdc76646.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/87c\/2e1\/b9d\/87c2e1b9d7596539a14aae428ddca767.png\" width=\"1304\" height=\"1211\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/87c\/2e1\/b9d\/87c2e1b9d7596539a14aae428ddca767.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/87c\/2e1\/b9d\/87c2e1b9d7596539a14aae428ddca767.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<ul>\n<li>\n<p>\u0414\u0430\u0442\u0447\u0438\u043a \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430\u00a0\u2014 <code>CCS811<\/code>. \u0414\u0430\u043d\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u0431\u044b\u043b \u0432\u044b\u0431\u0440\u0430\u043d \u043c\u0435\u0442\u043e\u0434\u043e\u043c \u0442\u044b\u043a\u0430, \u0430 \u0442\u043e\u0447\u043d\u0435\u0435 \u0432\u044b\u0431\u0440\u0430\u043d \u043f\u043e \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0443 \u0446\u0435\u043d\u0430-\u0440\u0430\u0437\u043c\u0435\u0440. \u0415\u0441\u0442\u044c \u0440\u0430\u0437\u043d\u044b\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u044b \u0442\u0438\u043f\u043e\u0432 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, <a href=\"https:\/\/www.winsen-sensor.com\/d\/files\/infrared-gas-sensor\/mh-z19b-co2-ver1_0.pdf\" rel=\"noopener noreferrer nofollow\">Winsen MH-Z19B<\/a>. \u0423 \u043c\u0435\u043d\u044f \u0435\u0441\u0442\u044c \u0442\u0430\u043a\u043e\u0439 \u0434\u0430\u0442\u0447\u0438\u043a, \u043d\u043e \u043e\u043d \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u0440\u0430\u0431\u043e\u0442\u0435. \u0414\u0430\u0442\u0447\u0438\u043a \u0442\u0438\u043f\u0430 <a href=\"https:\/\/www.tinytronics.nl\/product_files\/004928_PMSA003_datasheet.pdf\" rel=\"noopener noreferrer nofollow\">PMS-A003<\/a> \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u044e \u0447\u0430\u0441\u0442\u0438\u0446 \u0432 \u0432\u043e\u0437\u0434\u0443\u0445\u0435. \u041e\u043d \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u0432\u043d\u0435 \u0440\u0430\u0431\u043e\u0447\u0435\u0433\u043e \u043a\u0430\u0431\u0438\u043d\u0435\u0442\u0430. \u0415\u0441\u0442\u044c \u0442\u0430\u043a \u0436\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0444\u043e\u0440\u043c\u0430\u043b\u044c\u0434\u0435\u0433\u0438\u0434\u043e\u0432 \u0442\u0438\u043f\u0430 <a href=\"https:\/\/www.winsen-sensor.com\/d\/files\/PDF\/Semiconductor%20Gas%20Sensor\/MQ135%20(Ver1.4)%20-%20Manual.pdf\" rel=\"noopener noreferrer nofollow\">MQ-135<\/a>, \u043d\u043e \u043e\u043f\u044f\u0442\u044c \u043c\u0438\u043c\u043e. \u0412 \u043b\u044e\u0431\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0434\u0430\u0436\u0435 <a href=\"https:\/\/wiki.amperka.ru\/_media\/products:sensor-co2-ccs811-with-case:ccs811-datasheet.pdf\" rel=\"noopener noreferrer nofollow\">CCS811<\/a> \u0437\u0434\u0435\u0441\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u0439, \u0447\u0442\u043e\u0431\u044b \u0434\u0430\u0442\u044c \u043f\u043e\u043d\u044f\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e, \u0447\u0442\u043e \u043f\u043e\u0440\u0430 \u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0442\u0440\u0438\u0442\u044c \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435. \u0415\u0449\u0435 \u0437\u0430\u043a\u0430\u0437\u0430\u043b <a href=\"https:\/\/cdn.manomano.com\/pim-dam-img\/350\/101975992\/fe10862b1dcf98a6c27ff4adcb81f8bfbc51cf5c.pdf\" rel=\"noopener noreferrer nofollow\">ENS160 + AHT21<\/a>, \u0447\u0442\u043e\u0431\u044b \u0441\u0440\u0430\u0432\u043d\u0438\u0442\u044c \u0438 \u0432\u044b\u0431\u0440\u0430\u0442\u044c \u043b\u0443\u0447\u0448\u0438\u0439.<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>CCS811<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/ad4\/150\/883\/ad415088368edd1625b493c385ba2bad.jpg\" width=\"800\" height=\"800\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/ad4\/150\/883\/ad415088368edd1625b493c385ba2bad.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/ad4\/150\/883\/ad415088368edd1625b493c385ba2bad.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/554\/660\/f17\/554660f17b2b3af6dde273a527cfb745.png\" width=\"1334\" height=\"886\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/554\/660\/f17\/554660f17b2b3af6dde273a527cfb745.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/554\/660\/f17\/554660f17b2b3af6dde273a527cfb745.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<ul>\n<li>\n<p>\u0414\u0430\u0442\u0447\u0438\u043a \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b &#8212; <code>BME280<\/code>. \u0414\u0430\u043d\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u044f \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u043b \u043f\u043e\u0441\u043b\u0435 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u0431\u044b\u043b \u0437\u0430\u043a\u043e\u043d\u0447\u0435\u043d \u043f\u0440\u043e\u0435\u043a\u0442. \u041f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u0441\u0442\u0430\u043b\u043e\u0441\u044c \u043c\u0435\u0441\u0442\u043e.<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>BME280<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/567\/1ff\/4e8\/5671ff4e804b111ffea5d57936ea3e26.jpg\" width=\"1024\" height=\"1024\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/567\/1ff\/4e8\/5671ff4e804b111ffea5d57936ea3e26.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/567\/1ff\/4e8\/5671ff4e804b111ffea5d57936ea3e26.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d18\/d16\/2cd\/d18d162cda75b4d5239a05138d541166.png\" alt=\"\" title=\"\" width=\"1712\" height=\"1304\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/d18\/d16\/2cd\/d18d162cda75b4d5239a05138d541166.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/d18\/d16\/2cd\/d18d162cda75b4d5239a05138d541166.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<ul>\n<li>\n<p>RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u00a0\u2014 <a href=\"https:\/\/alexgyver.ru\/ws2812_guide\/\" rel=\"noopener noreferrer nofollow\">WS2815<\/a>. \u0412 \u0446\u0435\u043b\u043e\u043c \u043f\u043e\u0434\u043e\u0439\u0434\u0435\u0442 \u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434. \u041e\u043d \u043d\u0443\u0436\u0435\u043d \u043d\u043e\u043c\u0438\u043d\u0430\u043b\u044c\u043d\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0433\u043e<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>WS2815<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/2a8\/1b3\/589\/2a81b35895a5910f542af51ba4a45a34.png\" width=\"460\" height=\"356\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/2a8\/1b3\/589\/2a81b35895a5910f542af51ba4a45a34.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/2a8\/1b3\/589\/2a81b35895a5910f542af51ba4a45a34.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<ul>\n<li>\n<p>\u0414\u0438\u0441\u043f\u043b\u0435\u0439\u00a0\u2014 <a href=\"https:\/\/www.buydisplay.com\/download\/ic\/GC9A01A.pdf?srsltid=AfmBOoqycMC9E0HYSCuNqeNZoMFDaRp7rTbhTyRPZOm2QukEP0-opVJw\" rel=\"noopener noreferrer nofollow\">GC9A01<\/a>. \u0412\u043e\u043e\u0431\u0449\u0435, \u0438\u0434\u0435\u044f \u043a\u043e\u0440\u043f\u0443\u0441\u0430 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0432 \u0442\u043e\u0442 \u043c\u043e\u043c\u0435\u043d\u0442, \u043a\u043e\u0433\u0434\u0430 \u044f \u043d\u0430\u0448\u0435\u043b \u044d\u0442\u043e\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0439. \u041c\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c, \u0447\u0442\u043e \u043a\u0440\u0443\u0433\u043b\u0430\u044f \u0444\u043e\u0440\u043c\u0430 \u043f\u0440\u0438\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435.<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>GC9A01<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/dcf\/c2c\/365\/dcfc2c365c17fdf97870fa2644d0697b.png\" width=\"568\" height=\"612\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/dcf\/c2c\/365\/dcfc2c365c17fdf97870fa2644d0697b.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/dcf\/c2c\/365\/dcfc2c365c17fdf97870fa2644d0697b.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/30e\/258\/ef2\/30e258ef280184954051eae485b6814a.png\" width=\"1296\" height=\"1338\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/30e\/258\/ef2\/30e258ef280184954051eae485b6814a.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/30e\/258\/ef2\/30e258ef280184954051eae485b6814a.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/547\/ec0\/1a3\/547ec01a357831c54e4a3f7d653858c5.png\" width=\"1222\" height=\"1222\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/547\/ec0\/1a3\/547ec01a357831c54e4a3f7d653858c5.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/547\/ec0\/1a3\/547ec01a357831c54e4a3f7d653858c5.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<p>\u0412\u043e\u0442 \u0441\u0445\u0435\u043c\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f:<\/p>\n<details class=\"spoiler\">\n<summary>\u0421\u0445\u0435\u043c\u0430<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/074\/307\/ba5\/074307ba55a86d7b1aa5f848653e93a3.jpg\" width=\"726\" height=\"619\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/074\/307\/ba5\/074307ba55a86d7b1aa5f848653e93a3.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/074\/307\/ba5\/074307ba55a86d7b1aa5f848653e93a3.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>\u041a\u0430\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0432\u043d\u0443\u0442\u0440\u0438<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/74d\/10f\/556\/74d10f5565b53cec9be9c9d454d5ce21.jpg\" alt=\"\u042f \u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f, \u0447\u0435\u0441\u0442\u043d\u043e\" title=\"\u042f \u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f, \u0447\u0435\u0441\u0442\u043d\u043e\" width=\"720\" height=\"1280\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/74d\/10f\/556\/74d10f5565b53cec9be9c9d454d5ce21.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/74d\/10f\/556\/74d10f5565b53cec9be9c9d454d5ce21.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u042f \u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f, \u0447\u0435\u0441\u0442\u043d\u043e<\/figcaption><\/div>\n<\/figure>\n<\/div>\n<\/details>\n<h4>\u041f\u0438\u0448\u0435\u043c \u043a\u043e\u0434<\/h4>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0432\u0448\u0438\u0441\u044c \u0441 \u0430\u043f\u043f\u0430\u0440\u0430\u0442\u043d\u043e\u0439 \u0447\u0430\u0441\u0442\u044c\u044e, \u0440\u0430\u0441\u043f\u0435\u0447\u0430\u0442\u0430\u0432 \u043a\u043e\u0440\u043f\u0443\u0441 \u0438 \u0441\u043e\u0431\u0440\u0430\u0432 \u044d\u0442\u043e \u0432\u0441\u0435 \u0432\u043e\u0435\u0434\u0438\u043d\u043e \u043d\u0430\u043a\u043e\u043d\u0435\u0446-\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0447\u0430\u0442\u044c \u043f\u0438\u0441\u0430\u0442\u044c \u043a\u043e\u0434. \u041f\u0438\u0441\u0430\u0442\u044c \u043a\u043e\u0434 \u043a\u0430\u0436\u0435\u0442\u0441\u044f \u0441\u0430\u043c\u0430\u044f \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442\u0430, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0442\u0443\u0442 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0438\u0439 \u043f\u043e\u043b\u0435\u0442 \u0444\u0430\u043d\u0442\u0430\u0437\u0438\u0438. \u041d\u043e \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0437\u0430\u0431\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u043e \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u043d\u0430\u0447\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f. \u0414\u0430\u0436\u0435 \u0441\u0435\u0439\u0447\u0430\u0441, \u043a\u043e\u0433\u0434\u0430 \u044f \u043f\u0438\u0448\u0443 \u044d\u0442\u0443 \u0441\u0442\u0430\u0442\u044c\u044e, \u044f \u043d\u0435 \u0443\u0432\u0435\u0440\u0435\u043d \u0447\u0442\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b \u0432\u0441\u0435 \u0447\u0442\u043e \u0445\u043e\u0442\u0435\u043b. \u0420\u0430\u043d\u044c\u0448\u0435 \u043f\u043e\u0434 Arduino \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043b\u0438 \u0432 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0439 IDE. \u041a\u0430\u043a\u043e\u0432\u043e \u0431\u044b\u043b\u043e \u043c\u043e\u0435 \u0443\u0434\u0438\u0432\u043b\u0435\u043d\u0438\u0435, \u0447\u0442\u043e \u043f\u043e\u044f\u0432\u0438\u043b\u0441\u044f \u0443\u0434\u043e\u0431\u043d\u044b\u0439 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a (\u0435\u0441\u043b\u0438 \u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u0442\u0430\u043a \u043d\u0430\u0437\u0432\u0430\u0442\u044c) &#8212;  Platformio. \u042f \u043d\u0430\u0447\u0430\u043b \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0442\u044c\u0441\u044f \u0432 \u043d\u0435\u043c. \u041f\u043e \u0441\u0443\u0442\u0438, \u0432\u0441\u0435 \u0447\u0442\u043e \u043c\u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0435\u0441\u0442\u044c &#8212; \u044d\u0442\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0431\u0438\u043d\u0430\u0440\u043d\u0438\u043a (\u043f\u0440\u043e\u0448\u0438\u0431\u043a\u0443) \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 <a href=\"https:\/\/github.com\/WildEgor\/AirQualityMonitor\/tree\/develop\" rel=\"noopener noreferrer nofollow\">\u0442\u0443\u0442<\/a>. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0437\u0431\u0435\u0440\u0435\u043c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443. \u041d\u0430\u0447\u043d\u0435\u043c \u0441 \u0444\u0430\u0439\u043b\u0430 \u0432\u0445\u043e\u0434\u0430 \u0432 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443 &#8212;<code>main.cpp<\/code>. \u041a\u0430\u043a \u0438 \u043b\u044e\u0431\u0430\u044f \u0434\u0440\u0443\u0433\u0430\u044f \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0430 \u043d\u0430 Arduino \u043e\u043d\u0430 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 \u0434\u0432\u0443\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 <code>setup<\/code> \u0438 <code>loop<\/code>.<\/p>\n<details class=\"spoiler\">\n<summary>main.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"Arduino.h\" #include &lt;Looper.h&gt;  #include \"db\/settings_db.h\" #include \"model\/co2_data.h\" #include \"configs\/config.h\" #include \"connections\/mqtt_conn.h\" #include \"connections\/wifi_conn.h\" #include \"connections\/wifi_connector_adapter.cpp\" #include \"sensors\/sensor_base.h\" #include \"sensors\/co2.h\" #include \"sensors\/tph.h\" #include \"hmi\/display.h\" #include \"hmi\/web.h\" #include \"controllers\/rgb.h\" #include \"services\/logger.h\" #include \"services\/publisher.h\" #include \"services\/ota.h\"  \/**  * @brief Initialization of all main components: logging, database, WiFi and MQTT connections, data publishing, display, RGB, and web interface  *\/ void setup() {   \/**    * @note Logging initialization    *\/   Serial.begin(SERIAL_SPEED);   SET_LOG_LEVEL(APP_LOG_LEVEL);   LOG_INFO(\"init...\");    \/**    * @note Database initialization    *\/   SettingsDB *sdb = new SettingsDB();    \/**    * @note WiFi connection initialization    *\/   WiFiAdapter *wifia = new WiFiConnectorAdapter(       WIFI_AP_NAME,       WIFI_AP_PASS,       WIFI_CONN_RETRY_TIMEOUT,       false);   WiFiConn *wifi = new WiFiConn(*sdb, *wifia);    \/**    * @note OTA firmware update initialization    *\/   OTA *ota = new OTA(*wifi);    \/**    * @note MQTT connection initialization    *\/   MQTTConn *mqtt = new MQTTConn(*sdb, *wifi);    \/**    * @note Sensors initialization    *\/   CO2Sensor *co2 = new CO2Sensor(SEC_30);   TPHSensor *tph = new TPHSensor(SEC_30);    \/**    * @note Enable test mode for sensors (data emulation)    *\/ #ifdef ENABLE_TEST   co2-&gt;enableTest();   tph-&gt;enableTest(); #endif  \/**  * @note Disable sending to MQTT to prevent broke data  *\/ #ifndef ENABLE_TEST   \/**    * @note Configure CO2 value publishing to topic [device_id]\/co2    *\/   MQTTPublisher *co2p = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_CO2_TOPIC);   co2p-&gt;setValueCb([co2]() -&gt; float                    { return co2-&gt;getCO2(); });    \/**    * @note Configure TVOC value publishing to topic [device_id]\/tvoc    *\/   MQTTPublisher *tvocp = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_TVOC_TOPIC);   tvocp-&gt;setValueCb([co2]() -&gt; float                     { return co2-&gt;getTVOC(); });    \/**    * @note Configure temperature publishing to topic [device_id]\/temp    *\/   MQTTPublisher *tempp = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_TEMP_TOPIC);   tempp-&gt;setValueCb([tph]() -&gt; float                     { return tph-&gt;getTemperature(); });    \/**    * @note Configure pressure publishing to topic [device_id]\/pressure    *\/   MQTTPublisher *pp = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_PRESSURE_TOPIC);   pp-&gt;setValueCb([tph]() -&gt; float                  { return tph-&gt;getPressure(); });    \/**    * @note Configure humidity publishing to topic [device_id]\/humidity    *\/   MQTTPublisher *hp = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_HUMIDITY_TOPIC);   hp-&gt;setValueCb([tph]() -&gt; float                  { return tph-&gt;getHumidity(); }); #endif    \/**    * @note Display initialization    *\/   Display *display = new Display(     SEC_1,      *sdb,      *co2,      *tph,      *wifi,     *mqtt,      *ota);    \/**    * @note RGB controller initialization for CO2 level visualization    *\/   RGBController *rgb = new RGBController(MS_500, *sdb);   rgb-&gt;setUpdaterCb([co2]() -&gt; float                     { return co2-&gt;getCO2(); });    \/**    * @note Web interface initialization    *\/   WebPanel *wp = new WebPanel(       *sdb,       *wifi,       *ota,       *mqtt,       *rgb,       *display,       *co2,       *tph);    LOG_INFO(\"init ok!\"); }  \/**  * @brief Main loop. Handles all tickers and timers.  *\/ void loop() {   Looper.loop(); }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0412 <code>setup<\/code> \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u043c\u044b \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u043c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b:<\/p>\n<ul>\n<li>\n<p>\u041b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u0432\u043e\u0434\u0438\u0442\u044c \u0447\u0435\u043b\u043e\u0432\u0435\u043a\u043e\u0447\u0438\u0442\u0430\u0435\u043c\u044b\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u044c \u0438 \u043d\u0430 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u044c &#8212; \u043a\u043b\u0430\u0441\u0441 <code>Logger<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0443\u044e \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 (\u0432 \u0444\u0430\u0439\u043b\u0435), \u0447\u0442\u043e\u0431\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u0441\u0431\u0440\u043e\u0441\u044f\u0442\u0441\u044f \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Wi-Fi) &#8212; \u043a\u043b\u0430\u0441\u0441 <code>SettingsDB<\/code>;<\/p>\n<\/li>\n<li>\n<p>Wi-Fi \u0430\u0434\u0430\u043f\u0442\u0435\u0440 (\u043e\u0431\u0435\u0440\u0442\u043a\u0430 \u043d\u0430\u0434 <a href=\"https:\/\/registry.platformio.org\/libraries\/gyverlibs\/WiFiConnector\" rel=\"noopener noreferrer nofollow\">\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439<\/a>) \u0438 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0442\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442 \u043b\u043e\u0433\u0438\u043a\u0443 (\u043f\u0435\u0440\u0435)\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f &#8212; \u043a\u043b\u0430\u0441\u0441\u044b <code>WiFiConnectorAdapter<\/code> \u0438 <code>WiFiConn<\/code>;<\/p>\n<\/li>\n<li>\n<p>OTA (<em>Over-the-Air<\/em>) \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 &#8212; \u043a\u043b\u0430\u0441\u0441 <code>OTA<\/code>; <\/p>\n<\/li>\n<li>\n<p>MQTT \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0438 \u0438\u0437\u0434\u0430\u0442\u0435\u043b\u0438 \u0434\u043b\u044f \u0442\u043e\u043f\u0438\u043a\u043e\u0432 (<em>eCO2<\/em>, <em>TVOC<\/em>, <em>temperature<\/em>, <em>pressure<\/em>) &#8212; \u043a\u043b\u0430\u0441\u0441\u044b <code>MQTTConn<\/code> \u0438 <code>MQTTPublisher<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u0433\u0438\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u043d\u0430 UI \u0434\u0438\u0441\u043f\u043b\u0435\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0432 \u043a\u043b\u0430\u0441\u0441\u0435 <code>Display<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u041b\u043e\u0433\u0438\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u043d\u0430 UI \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0432 \u043a\u043b\u0430\u0441\u0441\u0435 <code>WebPanel<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0417\u0430 \u0447\u0442\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 eCO2 \u0438 TVOC \u0434\u0430\u0442\u0447\u0438\u043a\u0430 CCS811 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043a\u043b\u0430\u0441\u0441 <code>CO2Sensor<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0417\u0430 \u0447\u0442\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b, \u0434\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0438 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 BME280 &#8212; \u043a\u043b\u0430\u0441\u0441 <code>TPHSensor<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434 &#8212; \u043a\u043b\u0430\u0441\u0441 <code>RGBController<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>\u0424\u0443\u043d\u043a\u0446\u0438\u044f <code>loop<\/code> \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u0435\u0442 <code>Looper<\/code> &#8212; <a href=\"https:\/\/registry.platformio.org\/libraries\/gyverlibs\/Looper\" rel=\"noopener noreferrer nofollow\">\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430<\/a>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0443\u043f\u0440\u043e\u0449\u0430\u0435\u0442 \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e  \u0438 \u0432\u044b\u0437\u043e\u0432 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b \u0432 \u0446\u0438\u043a\u043b\u0435 \u043e\u043f\u0440\u043e\u0441\u0430 (<em>pool based<\/em>).  \u041d\u0430\u0447\u043d\u0435\u043c \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e <code>main<\/code>. \u041e\u043d\u0430 \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0441 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043b\u043e\u0433\u0433\u0435\u0440\u0430. \u041e\u043f\u0438\u0441\u0430\u043d \u043a\u043b\u0430\u0441\u0441 <code>Logger<\/code> \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u0430\u0435\u0442 API \u0434\u043b\u044f \u0444\u0438\u043a\u0441\u0430\u0446\u0438\u0438 \u043b\u043e\u0433\u0430, \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u0443\u0435\u0442 \u0435\u0433\u043e (\u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0441 \u0443\u0440\u043e\u0432\u043d\u0435\u043c \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0446\u0432\u0435\u0442) \u0438 \u0432\u044b\u0432\u043e\u0434\u0438\u0442 \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u044c \u0438\u043b\u0438 \u043d\u0430 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u044c. \u041c\u043e\u0436\u043d\u043e \u0443\u0434\u043e\u0431\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u043c \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u044b.<\/p>\n<details class=\"spoiler\">\n<summary>src\/services\/logger.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Arduino.h&gt; #include &lt;SettingsGyver.h&gt;  #include \"configs\/config.h\"  \/**  * @enum LogLevel  * @brief Log level: DEBUG - all logs, INFO - informational, WARN - warnings, ERROR - only critical messages  *\/ enum class LogLevel {     DEBUG, \/\/\/&lt; Debug messages     INFO,  \/\/\/&lt; Informational messages     WARN,  \/\/\/&lt; Warning messages     ERROR  \/\/\/&lt; Error messages };  \/**  * @class Logger  * @brief Implements formatting and serial printing for logging  *\/ class Logger { public:     \/**      * @brief Get singleton instance of Logger      * @return Logger&amp;      *\/     static Logger &amp;getInstance();      \/**      * @brief Set global log level      * @param level Log level as string      *\/     void setLevel(const String &amp;level);     \/**      * @brief Print log message with level and component prefix      * @param level Log level      * @param component Component name      * @param message Log message      *\/     void log(LogLevel level, const char *component, const String &amp;message);     \/**      * @brief Set external web UI logger      * @param wl Reference to web logger      *\/     void initWebLogger(sets::Logger &amp;wl);  private:     Logger();     Logger(const Logger &amp;) = delete;     Logger &amp;operator=(const Logger &amp;) = delete;      sets::Logger *_wl;        \/\/\/&lt; Pointer to web logger     LogLevel _current_level;  \/\/\/&lt; Current log level };  \/**  * @def LOG_COMPONENT  * @brief Default log component name (application name)  *\/ #ifndef LOG_COMPONENT #define LOG_COMPONENT APP_NAME #endif  \/**  * @def LOGGER  * @brief Singleton logger instance  *\/ #define LOGGER Logger::getInstance() \/**  * @def LOG_DEBUG  * @brief Log debug message  *\/ #define LOG_DEBUG(msg) LOGGER.log(LogLevel::DEBUG, LOG_COMPONENT, msg) \/**  * @def LOG_INFO  * @brief Log info message  *\/ #define LOG_INFO(msg) LOGGER.log(LogLevel::INFO, LOG_COMPONENT, msg) \/**  * @def LOG_WARN  * @brief Log warning message  *\/ #define LOG_WARN(msg) LOGGER.log(LogLevel::WARN, LOG_COMPONENT, msg) \/**  * @def LOG_ERROR  * @brief Log error message  *\/ #define LOG_ERROR(msg) LOGGER.log(LogLevel::ERROR, LOG_COMPONENT, msg) \/**  * @def SET_LOG_LEVEL  * @brief Set global log level  *\/ #define SET_LOG_LEVEL(level) LOGGER.setLevel(level)<\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/services\/logger.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"logger.h\"  Logger &amp;Logger::getInstance() {     static Logger instance;     return instance; }  Logger::Logger() : _current_level(LogLevel::DEBUG), _wl(nullptr) {}  void Logger::initWebLogger(sets::Logger &amp;wl) {     _wl = &amp;wl; }  void Logger::setLevel(const String &amp;level) {     String levelUpper = level;     levelUpper.toUpperCase();      if (levelUpper == \"DEBUG\" || levelUpper == \"0\")     {         _current_level = LogLevel::DEBUG;     }     else if (levelUpper == \"INFO\" || levelUpper == \"1\")     {         _current_level = LogLevel::INFO;     }     else if (levelUpper == \"WARN\" || levelUpper == \"2\")     {         _current_level = LogLevel::WARN;     }     else if (levelUpper == \"ERROR\" || levelUpper == \"3\")     {         _current_level = LogLevel::ERROR;     }     else     {         log(LogLevel::ERROR, \"Logger\", \"invalid log level: \" + level);     } }  void Logger::log(LogLevel level, const char *component, const String &amp;message) {     if (level &lt; _current_level)         return;      String level_str;     String color_code;     switch (level)     {     case LogLevel::DEBUG:         level_str = \"DEBUG\";         color_code = \"\\033[36m\"; \/\/ Cyan color for DEBUG level         break;     case LogLevel::INFO:         level_str = \"INFO\";         color_code = \"\\033[32m\"; \/\/ Green color for INFO level         break;     case LogLevel::WARN:         level_str = \"WARN\";         color_code = \"\\033[33m\"; \/\/ Yellow color for WARN level         break;     case LogLevel::ERROR:         level_str = \"ERROR\";         color_code = \"\\033[31m\"; \/\/ Red color for ERROR level         break;     }     String reset_code = \"\\033[0m\";     Serial.println(color_code + \"[\" + level_str + \"][\" + component + \"] \" + message + reset_code);     if (_wl)     {         _wl-&gt;println(\"[\" + level_str + \"][\" + component + \"] \" + message);     } }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0414\u0430\u043b\u0435\u0435 \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u043a\u043b\u0430\u0441\u0441 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445. \u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0431\u0430\u0437\u044b \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0444\u0430\u0439\u043b. \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0430 \u043f\u043e\u0432\u0435\u0440\u0445 <a href=\"https:\/\/registry.platformio.org\/libraries\/gyverlibs\/GyverDB\" rel=\"noopener noreferrer nofollow\">\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438<\/a>. \u0412 \u043d\u0435\u0439 \u043c\u044b \u0431\u0443\u0434\u0435\u043c \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043a\u043b\u0430\u0441\u0441\u0430 <code>SettingsDB<\/code> &#8212; \u0432 \u0431\u0430\u0437\u0435 \u043c\u044b \u0445\u0440\u0430\u043d\u0438\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Wi-Fi, MQTT, \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e \u0432\u044b\u0441\u043e\u043a\u043e\u043c \u0443\u0440\u043e\u0432\u043d\u0435 CO2 \u0438 \u043f\u0440\u043e\u0447\u0435\u0435.  <\/p>\n<details class=\"spoiler\">\n<summary>src\/db\/settings_db.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;GyverDBFile.h&gt; #include &lt;LittleFS.h&gt; #include &lt;Looper.h&gt;  #include \"configs\/config.h\"  #define LOG_COMPONENT \"SettingsDB\" #include \"services\/logger.h\"  \/**  * @enum kk  * @brief Database keys, used as map keys for settings storage  *\/ enum kk : size_t {     wifi_ssid,        \/\/\/&lt; WiFi SSID     wifi_pass,        \/\/\/&lt; WiFi password     mqtt_enabled,     \/\/\/&lt; MQTT enabled flag     mqtt_server,      \/\/\/&lt; MQTT server address     mqtt_port,        \/\/\/&lt; MQTT server port     mqtt_username,    \/\/\/&lt; MQTT username     mqtt_pass,        \/\/\/&lt; MQTT password     mqtt_device_id,   \/\/\/&lt; MQTT device ID     co2_scale_type,   \/\/\/&lt; CO2 scale type     co2_alarm_lvl,    \/\/\/&lt; CO2 alarm level     rgb_enabled,      \/\/\/&lt; RGB enabled flag     use_dark_theme,   \/\/\/&lt; Use dark theme flag     rotation_display, \/\/\/&lt; Rotation display     log_lvl           \/\/\/&lt; Log level };  \/**  * @var co2_scale_types  * @brief List of available CO2 scale types for UI  *\/ extern String co2_scale_types; \/**  * @var log_levels  * @brief List of available log levels for UI  *\/ extern String log_levels;  \/**  * @class SettingsDB  * @brief Implements database logic for application settings  *\/ class SettingsDB : public LoopTickerBase { public:     \/**      * @brief Constructor      *\/     SettingsDB();      \/**      * @brief Initialize database dependencies      *\/     void setup();      \/**      * @brief Handle database updates      *\/     void exec() override;      \/**      * @brief Return database instance      * @return GyverDBFile reference      *\/     GyverDBFile &amp;db();  private:     GyverDBFile _db; \/\/\/&lt; Internal database instance }; <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/db\/settings_db.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"configs\/config.h\" #include \"settings_db.h\"  \/**  * @var co2_scale_types  * @brief Colors CO2: 3 or 4 color ranges  *\/ String co2_scale_types = \"3 color;4 color\"; \/**  * @var log_levels  * @brief Log levels  *\/ String log_levels = \"DEBUG;INFO;WARN;ERROR\";  SettingsDB::SettingsDB() : LoopTickerBase(), _db(&amp;LittleFS, DB_NAME) {     LOG_INFO(\"init...\");     bool fsInitialized = true;  #ifdef ESP32     fsInitialized = LittleFS.begin(true); #else     fsInitialized = LittleFS.begin(); #endif      if (!fsInitialized)     {         LOG_ERROR(\"init littlefs failed!\");         return;     }      _db.begin();      \/**      * @note \u0421\u0431\u0440\u043e\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u043a \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u0438\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c, \u0435\u0441\u043b\u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0451\u043d RESET_DB      *\/ #ifdef RESET_DB     _db.reset(); #endif      \/**      * @note \u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0440\u0430\u0437\u0434\u0435\u043b\u043e\u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a: APP, WIFI, MQTT, CO2      *\/     \/\/ ============================== APP ==============================     _db.init(kk::rgb_enabled, RGB_ENABLED);     _db.init(kk::use_dark_theme, APP_DARK_THEME);     _db.init(kk::log_lvl, APP_LOG_LEVEL);     _db.init(kk::rotation_display, TFT_ROTATION_0);      \/\/ ============================== WIFI ==============================     _db.init(kk::wifi_ssid, WIFI_SSID);     _db.init(kk::wifi_pass, WIFI_PASS);      \/\/ ============================== MQTT ==============================     _db.init(kk::mqtt_enabled, MQTT_ENABLED);     _db.init(kk::mqtt_server, MQTT_SERVER);     _db.init(kk::mqtt_port, MQTT_PORT);     _db.init(kk::mqtt_username, MQTT_USERNAME);     _db.init(kk::mqtt_pass, MQTT_PASS);     _db.init(kk::mqtt_device_id, MQTT_DEFAULT_DEVICE_ID);      \/\/ ============================== CO2 ==============================     _db.init(kk::co2_scale_type, \"4 color\");     _db.init(kk::co2_alarm_lvl, RGB_DEFAULT_ALERT_TRHLD);      \/**      * @note \u0412\u044b\u0432\u043e\u0434 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 \u0441\u0435\u0440\u0438\u0430\u043b \u043b\u043e\u0433      *\/     _db.dump(Serial);      LOG_INFO(\"init ok!\");      this-&gt;addLoop(); }  void SettingsDB::exec() {     _db.tick(); }  GyverDBFile &amp;SettingsDB::db() {     return _db; } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0414\u0430\u043b\u0435\u0435 \u043d\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a\u00a0\u0441\u0435\u0442\u0438. \u0414\u043b\u044f\u00a0\u044d\u0442\u043e\u0433\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u044b \u043a\u043b\u0430\u0441\u0441 <code>WiFiConnectorAdapter<\/code>, \u0430\u00a0\u0442\u0430\u043a\u0436\u0435 \u043a\u043b\u0430\u0441\u0441 <code>WiFiConn<\/code>. \u0414\u0430\u043d\u043d\u044b\u0435 \u043a\u043b\u0430\u0441\u0441\u044b \u0440\u0435\u0430\u043b\u0438\u0437\u0443\u044e\u0442 \u043b\u043e\u0433\u0438\u043a\u0443 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\/\u043f\u0435\u0440\u0435\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a\u00a0\u0441\u0435\u0442\u0438 Wi\u2011Fi, \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0438 (<em>hotspot<\/em>). \u041e\u043f\u044f\u0442\u044c\u00a0\u0436\u0435, <code>WiFiConnectorAdapter<\/code> \u044d\u0442\u043e \u043e\u0431\u0435\u0440\u0442\u043a\u0430 \u043d\u0430\u0434 \u0433\u043e\u0442\u043e\u0432\u043e\u0439 <a href=\"https:\/\/registry.platformio.org\/libraries\/gyverlibs\/WiFiConnector\" rel=\"noopener noreferrer nofollow\">\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439<\/a>, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0443\u0434\u043e\u0431\u043d\u044b\u0439 API \u043d\u0430\u0434 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u043e\u0439 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u043e\u0439 <code>ESP8266WiFi.h<\/code> \u0438\u043b\u0438 <code>WiFi.h<\/code>. \u0412\u00a0\u043b\u044e\u0431\u043e\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043d\u0430\u00a0\u0441\u0432\u043e\u044e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e. \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Wi-Fi:<\/p>\n<details class=\"spoiler\">\n<summary>src\/connections\/wifi_conn.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Arduino.h&gt; #include &lt;WiFiConnector.h&gt;  #include \"db\/settings_db.h\" #include \"configs\/config.h\"  #define LOG_COMPONENT \"WiFiConn\" #include \"services\/logger.h\"  \/**  * @class WiFiAdapter  * @brief Abstract class for WiFi connectors  *\/ class WiFiAdapter { public:     \/**      * @brief Constructor      * @param APname Access point name      * @param APpass Access point password      * @param timeout Connection timeout in seconds      * @param closeAP Close AP after connection      *\/     WiFiAdapter(const String &amp;APname = \"AQM_AP\",                 const String &amp;APpass = \"\",                 uint16_t timeout = 60,                 bool closeAP = false) {}     virtual ~WiFiAdapter() {}      \/**      * @brief Try to (re)connect to WiFi network      * @param ssid WiFi name      * @param pass WiFi password      *\/     virtual void connect(const String &amp;ssid, const String &amp;pass = \"\") = 0;     \/**      * @brief Check if in connecting mode      * @return true if connecting      *\/     virtual bool connecting() = 0;     \/**      * @brief Check if connected to network      * @return true if connected      *\/     virtual bool connected() = 0;     \/**      * @brief Handle (re)connect logic      * @return true if successful      *\/     virtual bool exec() = 0;     \/**      * @brief Return softAP or local IP      * @return Device IP as string      *\/     virtual String ip() = 0;  private:     bool _is_initialized = false; \/\/\/&lt; Initialization flag };  \/**  * @class WiFiConn  * @brief Implements WiFi connection logic  *\/ class WiFiConn : public LoopTickerBase { public:     \/**      * @brief Constructor      * @param settingsDb Reference to settings database      * @param wifiAdapter Reference to WiFi adapter      *\/     WiFiConn(SettingsDB &amp;settingsDb, WiFiAdapter &amp;wifiAdapter);      \/**      * @brief Connect to network      *\/     void connect();      \/**      * @brief Return connection status      * @return true if connected      *\/     bool connected();     \/**      * @brief Check if class is initialized      * @return true if initialized      *\/     bool isInitialized()     {         return _is_initialized;     };     \/**      * @brief Handle connection loop      *\/     void exec() override;     \/**      * @brief Return softAP or local IP      * @return Device IP as string      *\/     String ip();  private:     \/**      * @brief Internal connect logic calling WiFiAdapter      * @param ssid WiFi SSID      * @param pass WiFi password      *\/     void _connect(const String &amp;ssid, const String &amp;pass);      GyverDBFile *_db;           \/\/\/&lt; Pointer to database     WiFiAdapter *_wifi_adapter; \/\/\/&lt; Pointer to WiFi adapter     bool _is_initialized = false; \/\/\/&lt; Initialization flag }; <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/connections\/wifi_conn.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"wifi_conn.h\"  \/**  * @brief Constructor for WiFiConn  * @param settingsDb Reference to settings database  * @param wifiAdapter Reference to WiFi adapter  *\/ WiFiConn::WiFiConn(SettingsDB &amp;settingsDb, WiFiAdapter &amp;wifiAdapter) : LoopTickerBase(), _db(&amp;settingsDb.db()), _wifi_adapter(&amp;wifiAdapter), _is_initialized(false) {     LOG_INFO(\"init...\");      if (!connected())     {         connect();     }      LOG_INFO(\"init ok!\");      this-&gt;addLoop();     _is_initialized = true; }  void WiFiConn::exec() {     _wifi_adapter-&gt;exec(); }  void WiFiConn::connect() {     _connect((*_db)[kk::wifi_ssid], (*_db)[kk::wifi_pass]); }  bool WiFiConn::connected() {     return _wifi_adapter-&gt;connected(); }  String WiFiConn::ip() {     return _wifi_adapter-&gt;ip(); }  \/**  * @brief Internal connect logic calling WiFiAdapter  * @param ssid WiFi SSID  * @param pass WiFi password  *\/ void WiFiConn::_connect(const String &amp;ssid, const String &amp;pass) {     if (ssid.length() == 0)         return;     LOG_INFO(\"connecting to \" + ssid);     _wifi_adapter-&gt;connect(ssid, pass); } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f:<\/p>\n<details class=\"spoiler\">\n<summary>src\/connections\/wifi_connector_adapter.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"wifi_conn.h\" #include \"configs\/config.h\"  #define LOG_COMPONENT \"WiFiConn\" #include \"services\/logger.h\"  \/**  * @class WiFiConnectorAdapter  * @brief Implements connection to WiFi using gyverlibs\/WiFiConnector  *\/ class WiFiConnectorAdapter : public WiFiAdapter { public:     \/**      * @brief Constructor      * @param APname Access point name      * @param APpass Access point password      * @param timeout Connection timeout in seconds      * @param closeAP Close AP after connection      *\/     WiFiConnectorAdapter(         const String &amp;APname = \"AQM_AP\",         const String &amp;APpass = \"\",         uint16_t timeout = 60,         bool closeAP = false)     {         LOG_INFO(\"init...\");         _wifiConnector = new WiFiConnectorClass(APname, APpass, timeout, closeAP);          _wifiConnector-&gt;onConnect([this]()                                   { LOG_INFO(\"connected!\"); });          _wifiConnector-&gt;onError([this]()                                 { LOG_WARN(\"connection error\"); });          _is_initialized = true;         LOG_INFO(\"init ok!\");     }      void connect(const String &amp;ssid, const String &amp;pass = \"\") override     {         _wifiConnector-&gt;connect(ssid, pass);     }      bool connecting() override     {         return _wifiConnector-&gt;connecting();     }      bool connected() override     {         return _wifiConnector-&gt;connected();     }      String ip() override     {         if (!_wifiConnector-&gt;connected())         {             return WiFi.softAPIP().toString();         }         return WiFi.localIP().toString();     }      bool exec() override     {         return _wifiConnector-&gt;tick();     }  private:     WiFiConnectorClass *_wifiConnector = nullptr; \/\/\/&lt; Pointer to WiFiConnectorClass instance     bool _is_initialized = false; \/\/\/&lt; Initialization flag };<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0410\u0432\u0442\u043e\u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0432\u00a0\u043a\u043b\u0430\u0441\u0441\u0435 <code>OTA<\/code>. \u042d\u0442\u043e \u0443\u0434\u043e\u0431\u043d\u043e, \u0442.\u043a. \u043e\u0442\u0434\u0430\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u0440\u0443\u0437\u044c\u044f\u043c, \u043c\u043e\u0436\u043d\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443, \u0435\u0441\u043b\u0438 \u0432\u0434\u0440\u0443\u0433 \u043f\u043e\u044f\u0432\u044f\u0442\u0441\u044f \u0431\u0430\u0433\u0438 \u0438\u043b\u0438\u00a0\u0436\u0435 \u0434\u043e\u043f\u0438\u043b\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0444\u0438\u0447\u0438 \u0432\u00a0\u0431\u0443\u0434\u0443\u0449\u0435\u043c. <\/p>\n<details class=\"spoiler\">\n<summary>src\/services\/ota.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;AutoOTA.h&gt; #include &lt;Looper.h&gt;  #include \"configs\/config.h\" #include \"connections\/wifi_conn.h\"  #define LOG_COMPONENT \"OTA\" #include \"services\/logger.h\"  \/**  * @name OTA  * @details Implement logic to fetch firmware from remote source  *\/ class OTA : public LoopTickerBase { public:     OTA(WiFiConn &amp;wifiConn);      \/**      * @brief Execute OTA checks      *\/     void exec() override;     \/**      * @details Check if new updates available      * @return bool      *\/     bool hasUpdate();     \/**      * @details Init async\/sync update      * @return bool      *\/     bool update(bool async = true);      \/**      * @details Return current version      * @return String      *\/     String version();  private:     AutoOTA _ota = AutoOTA(APP_VERSION, PROJECT_PATH);     \/**      * @name _wifi_conn      * @details Pointer to WiFi connection      *\/     WiFiConn *_wifi_conn;      bool _is_initialized = false;     String _ver = \"\";     String _notes = \"\"; };<\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/services\/ota.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"ota.h\"  OTA::OTA(WiFiConn &amp;wifiConn) : LoopTickerBase(), _wifi_conn(&amp;wifiConn) {     LOG_INFO(\"init...\");      _ver = APP_VERSION;     _notes = \"\";      LOG_INFO(\"init ok!\");     _is_initialized = true;     this-&gt;addLoop(); };  bool OTA::hasUpdate() {      if (!_wifi_conn-&gt;connected()) return false;      String _remote_ver = \"\";     _ota.checkUpdate(&amp;_remote_ver, &amp;_notes);     return !String(APP_VERSION).equals(_remote_ver); }  bool OTA::update(bool now) {     if (!_wifi_conn-&gt;connected()) return false;          if (_ota.checkUpdate(&amp;_ver, &amp;_notes))     {         LOG_INFO(\"update to \" + _ver);         if (!_notes.isEmpty())             LOG_INFO(\"notes: \" + _notes);          if (!now)         {             _ota.update();             return true;         }          return _ota.updateNow();     }     else     {         LOG_INFO(\"no updates\");         return true;     }      LOG_ERROR(\"update failed\");      return false; };  String OTA::version() {     return _ver; }  void OTA::exec() {     _ota.tick(); }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0412\u00a0\u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u00a0\u0423\u0414\u042f \u0432\u044b\u0431\u0440\u0430\u043d <code>MQTT<\/code>. \u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f (\u043f\u0435\u0440\u0435)\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a\u00a0\u0431\u0440\u043e\u043a\u0435\u0440\u0443 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u00a0\u043a\u043b\u0430\u0441\u0441\u0435 <code>MQTTConn<\/code>. \u041f\u0440\u0438\u043c\u0435\u0440 \u0432\u0437\u044f\u0442 \u0438\u0437\u00a0\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 wqtt.ru \u0438 \u0434\u043e\u0440\u0430\u0431\u043e\u0442\u0430\u043d.<\/p>\n<details class=\"spoiler\">\n<summary>src\/connections\/mqtt_conn.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Arduino.h&gt; #include &lt;PubSubClient.h&gt;  #if defined(ESP8266) #include &lt;ESP8266WiFi.h&gt; #elif defined(ESP32) #include &lt;WiFi.h&gt; #endif  #include \"db\/settings_db.h\" #include \"configs\/config.h\" #include \"connections\/wifi_conn.h\"  #define LOG_COMPONENT \"MQTTConn\" #include \"services\/logger.h\"  extern WiFiClient _espClient; extern PubSubClient _pub_client;  \/**  * @class MQTTConn  * @brief Implements MQTT connection logic  *\/ class MQTTConn : public LoopTickerBase { public:     \/**      * @brief Constructor      * @param settingsDb Reference to settings database      * @param wifiConn Reference to WiFi connection      *\/     MQTTConn(SettingsDB &amp;settingsDb, WiFiConn &amp;wifiConn);      \/**      * @brief Handle (re)connect logic      *\/     void exec() override;     \/**      * @brief Connect to broker      *\/     void connect();     \/**      * @brief Publish data to topic      * @param topic MQTT topic      * @param payload Data to publish      *\/     void publish(const String &amp;topic, const String &amp;payload);     \/**      * @brief Set device ID for topic prefixes      * @param id Device ID      *\/     void setDeviceID(const String &amp;id);     \/**      * @brief Check if publishing is enabled      * @return true if enabled      *\/     bool isEnabled() const;     \/**      * @brief Check if still connected to MQTT      * @return true if connected      *\/     bool connected() const;     \/**      * @brief Check if class is initialized      * @return true if initialized      *\/     bool isInitialized() const;  private:     \/**      * @brief Internal connection logic      * @param mqtt_server MQTT host      * @param mqtt_port MQTT port      * @param mqtt_user MQTT user      * @param mqtt_password MQTT password      *\/     void _connectToMQTT(const String &amp;mqtt_server, uint16_t mqtt_port, const String &amp;mqtt_user, const String &amp;mqtt_password);      GyverDBFile *_db = nullptr; \/\/\/&lt; Pointer to database     WiFiConn *_wifi = nullptr;  \/\/\/&lt; Pointer to WiFi connection     String _device_id = \"\";    \/\/\/&lt; Device ID     uint32_t _tmr = 0, _tout;     bool _is_initialized = false; \/\/\/&lt; Initialization flag }; <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/connections\/mqtt_conn.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"mqtt_conn.h\"  WiFiClient _espClient; PubSubClient _pub_client(_espClient);  \/**  * @brief Constructor for MQTTConn  * @param settingsDb Reference to settings database  * @param wifiConn Reference to WiFi connection  *\/ MQTTConn::MQTTConn(SettingsDB &amp;settingsDb, WiFiConn &amp;wifiConn) : LoopTickerBase(), _db(&amp;settingsDb.db()), _wifi(&amp;wifiConn) {     LOG_INFO(\"init...\");      _tout = (15 * 1000ul);      _device_id = (*_db)[kk::mqtt_device_id].toString();      _connectToMQTT(         (*_db)[kk::mqtt_server],         (*_db)[kk::mqtt_port].toInt16(),         (*_db)[kk::mqtt_username],         (*_db)[kk::mqtt_pass]);      LOG_INFO(\"init ok!\");      _is_initialized = true;     this-&gt;addLoop(); }  void MQTTConn::exec() {     if (!isEnabled() || !_wifi-&gt;connected())         return;      bool pub_connected = _pub_client.loop();     if (!pub_connected &amp;&amp; (millis() - _tmr) &gt;= _tout)     {         connect();     } }  void MQTTConn::connect() {     if (!isEnabled() || !_wifi-&gt;connected())         return;      if (!connected())     {         LOG_INFO(\"connect to server...\");          _connectToMQTT(             (*_db)[kk::mqtt_server],             (*_db)[kk::mqtt_port].toInt16(),             (*_db)[kk::mqtt_username],             (*_db)[kk::mqtt_pass]);     } }  void MQTTConn::publish(const String &amp;topic, const String &amp;payload) {     if (!isEnabled() || !_wifi-&gt;connected())         return;      String topicPrefix = \"\";     if (!_device_id.isEmpty())     {         topicPrefix = _device_id + \"\/\";     }      LOG_DEBUG(\"pub to topic: \" + _device_id + \"\/\" + topic + \" value: \" + payload);      String t = topicPrefix + topic;      bool ok = _pub_client.publish(t.c_str(), payload.c_str(), false);     if (!ok)     {         LOG_ERROR(\"publish failed\");     } }  void MQTTConn::setDeviceID(const String &amp;id) {     if (id.isEmpty())         return;      _device_id = id; }  bool MQTTConn::isEnabled() const {     return (*_db)[kk::mqtt_enabled].toBool(); }  bool MQTTConn::connected() const {     return _pub_client.connected(); }  bool MQTTConn::isInitialized() const {     return _is_initialized; }  \/**  * @brief Internal connection logic  * @param mqtt_server MQTT host  * @param mqtt_port MQTT port  * @param mqtt_user MQTT user  * @param mqtt_password MQTT password  *\/ void MQTTConn::_connectToMQTT(const String &amp;mqtt_server, uint16_t mqtt_port, const String &amp;mqtt_user, const String &amp;mqtt_password) {     _tmr = millis();      if (!isEnabled() || !_wifi-&gt;connected())         return;      if (mqtt_server.isEmpty())     {         LOG_ERROR(\"server address is empty\");         return;     }      if (mqtt_user.isEmpty() || mqtt_password.isEmpty())     {         LOG_ERROR(\"server creds is empty\");         return;     }      if (mqtt_port == 0 || mqtt_port &gt; 65535)     {         LOG_ERROR(\"invalid port\");         return;     }      LOG_INFO(\"connecting to \" + mqtt_server);      _pub_client.setKeepAlive(60);     _pub_client.setSocketTimeout(30);     _pub_client.setServer(mqtt_server.c_str(), mqtt_port);      String client_id = \"AQM-\" + WiFi.macAddress() + String(random(0xffff), HEX);      if (client_id.isEmpty())     {         LOG_ERROR(\"failed to generate client ID\");         return;     }      LOG_DEBUG(\"client_id: \" + client_id);     LOG_DEBUG(\"mqtt_user: \" + mqtt_user);     LOG_DEBUG(\"mqtt_password: \" + mqtt_password);     LOG_DEBUG(\"mqtt_server: \" + mqtt_server);     LOG_DEBUG(\"mqtt_port: \" + String(mqtt_port));      LOG_DEBUG(\"attempting connection client...\");      if (_pub_client.connect(client_id.c_str(), mqtt_user.c_str(), mqtt_password.c_str()))     {         LOG_INFO(\"connected\");         return;     }     else     {         LOG_ERROR(\"failed, rc=\" + String(_pub_client.state()) + \" try again...\");         return;     } } <\/code><\/pre>\n<\/p>\n<\/div>\n<\/details>\n<p>\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043b\u043e\u0433\u0438\u043a\u0438 \u0447\u0442\u0435\u043d\u0438\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 <code>CCS811<\/code> \u0438 \u0435\u0433\u043e \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0430 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432\u00a0\u043a\u043b\u0430\u0441\u0441\u0435 <code>CO2Sensor<\/code>. \u0422\u0430\u043c\u00a0\u0436\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u0430 \u0440\u0430\u0431\u043e\u0442\u0430 \u0441\u043e \u0448\u043a\u0430\u043b\u043e\u0439 \u0434\u043b\u044f\u00a0\u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0432\u00a0\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u043c \u043a\u043b\u0430\u0441\u0441\u0435 <code>CO2Scale<\/code>. \u0412\u00a0\u043a\u043b\u0430\u0441\u0441\u0435 <code>CO2Sensor<\/code> \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 state \u043c\u0430\u0448\u0438\u043d\u0430, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u043e\u0431\u043d\u043e \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043c\u0435\u0436\u0434\u0443 \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u043c \u0440\u0435\u0436\u0438\u043c\u043e\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u0438 \u0440\u0435\u0436\u0438\u043c\u043e\u043c \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0438. <\/p>\n<details class=\"spoiler\">\n<summary>src\/sensors\/co2.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Arduino.h&gt; #include &lt;Wire.h&gt; #include &lt;SparkFunCCS811.h&gt;  #include \"sensor_base.h\" #include \"configs\/config.h\" #include \"connections\/mqtt_conn.h\" #include \"model\/co2_data.h\" #include \"db\/settings_db.h\"  #define LOG_COMPONENT \"CO2Sensor\" #include \"services\/logger.h\"  \/**  * @name CO2Sensor  * @details Class for working with CCS811 sensor: reading data, calibration, state management  *\/ class CO2Sensor : public SensorBase { public:     \/**      * @name CO2Sensor      * @param ms - polling period in milliseconds      * @details CO2Sensor class constructor      *\/     CO2Sensor(uint32_t ms);      \/**      * @name setup      * @details Initialize CCS811 dependencies      *\/     void setup() override;     \/**      * @name exec      * @details Main polling and data processing loop for the sensor      *\/     void exec() override;     \/**      * @name getCO2Min      * @return float - minimum CO2 value      *\/     float getCO2Min();     \/**      * @name getCO2Max      * @return float - maximum CO2 value      *\/     float getCO2Max();     \/**      * @name getCO2      * @return float - current CO2 value      *\/     float getCO2();     \/**      * @name getTVOCMin      * @return float - minimum TVOC value      *\/     float getTVOCMin();     \/**      * @name getTVOCMax      * @return float - maximum TVOC value      *\/     float getTVOCMax();     \/**      * @name getTVOC      * @return float - current TVOC value      *\/     float getTVOC();     \/**      * @name getType      * @return const char* - sensor type      *\/     const char *getType() const override;      \/**      * @name isCalibrating      * @return bool - whether the sensor is in calibration mode      *\/     bool isCalibrating()     {         return _state == CO2Sensor_CALIBRATING;     }      \/**      * @name startCalibration      * @details Force start sensor calibration      *\/     void startCalibration()     {         LOG_DEBUG(\"force start calibration\");          if (_data.current_baseline == 0x01)         {             _data.current_baseline = 0x00;         }          _state = CO2Sensor_CALIBRATING;          if (_data.current_baseline == 0x00)         {             _data.current_baseline = _sensor.getBaseline();         }          delay(5000);     };      \/**      * @name forceStopCalibration      * @details Force stop sensor calibration and write baseline      *\/     void forceStopCalibration()     {         if (_state != CO2Sensor_CALIBRATING || _data.current_baseline == 0x01)         {             return;         }          LOG_DEBUG(\"force stop calibration\");          CCS811Core::CCS811_Status_e errorStatus = _sensor.setBaseline(_data.current_baseline);         if (errorStatus == CCS811Core::CCS811_Stat_SUCCESS)         {             LOG_DEBUG(\"baseline written to sensor\");             LOG_INFO(\"calibration success\");         }         else         {             LOG_DEBUG(\"set baseline failed!\");             LOG_DEBUG(_sensor.statusString(errorStatus));             LOG_INFO(\"calibration failed\");         }          _data.current_baseline = 0x01;          delay(5000);          _state = CO2Sensor_RUNNING;     };  private:     \/**      * @name _sensor      * @details CCS811 driver instance      *\/     CCS811 _sensor;     \/**      * @name _data      * @details Structure for storing current sensor data      *\/     CO2Data _data;     \/**      * @name _state      * @details Current sensor state (initialization, running, calibration)      *\/     CO2Sensor_State _state;      \/**      * @name _init      * @details Internal sensor initialization      * @return bool - initialization success      *\/     bool _init();     \/**      * @name _check_data      * @details Check and update sensor data      *\/     void _check_data();     \/**      * @name _print_data      * @details Print current data to log      *\/     void _print_data();         \/**      * @name _mock_data      * @details Mock data for scale and alarm testing      *\/     void _mock_data(); };  \/**  * @name CO2Scale  * @details Class for working with CO2 color scale and thresholds  *\/ class CO2Scale { public:     \/**      * @name getInstance      * @return CO2Scale&amp; - singleton instance of the class      *\/     static CO2Scale &amp;getInstance();      \/**      * @name init      * @param db - pointer to database object      * @details Initialize scale from database      *\/     void init(GyverDBFile *db);     \/**      * @name getColor      * @param value - CO2 value      * @param r,g,b - color components      * @details Get color by CO2 value      *\/     void getColor(uint16_t value, uint8_t &amp;r, uint8_t &amp;g, uint8_t &amp;b);     \/**      * @name getScale      * @details Get color zone boundaries of the scale      *\/     void getScale(uint16_t &amp;rs, uint16_t &amp;re, uint16_t &amp;os, uint16_t &amp;oe, uint16_t &amp;ys, uint16_t &amp;ye, uint16_t &amp;gs, uint16_t &amp;ge);      \/**      * @name getMin      * @return float - minimum scale value      *\/     float getMin();     \/**      * @name getMax      * @return float - maximum scale value      *\/     float getMax();     \/**      * @name getHumanMax      * @return float - maximum value for human comfort      *\/     float getHumanMax();     \/**      * @name needAlarm      * @param value - CO2 value      * @return bool - whether alarm is needed      *\/     bool needAlarm(uint16_t value);  private:     \/**      * @name CO2Scale      * @details Private constructor for singleton      *\/     CO2Scale();     \/**      * @name _initScales      * @details Internal initialization of color scales      *\/     void _initScales();      GyverDBFile *_db;     ColorThreshold _default_scale[5];     ColorThreshold _easy_scale[3];     float _min = 400.0f;     float _max = 8000.0f;     float _human_max = 1500.0f; }; <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/sensors\/co2.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"co2.h\"  CO2Sensor::CO2Sensor(uint32_t ms) : SensorBase(ms), _state(CO2Sensor_INIT) {     LOG_INFO(\"init...\");     _data.co2 = 0.0;     _data.tvoc = 0.0;     _data.current_baseline = 0x00;      if (!_enable_test &amp;&amp; !_init())     {         LOG_ERROR(\"init failed! please check your wiring.\");         this-&gt;addLoop();         return;     }      _is_initialized = true;     _state = CO2Sensor_RUNNING;      LOG_INFO(\"init ok!\");     exec();     this-&gt;addLoop(); }  void CO2Sensor::setup() {}  void CO2Sensor::exec() {     if (!_is_initialized)     {         _init();         return;     }      if (_state != CO2Sensor_CALIBRATING)     {         _state = CO2Sensor_RUNNING;         _check_data();     } }  float CO2Sensor::getCO2() { return _data.co2; } float CO2Sensor::getTVOC() { return _data.tvoc; }  const char *CO2Sensor::getType() const {     return \"co2\"; }  float CO2Sensor::getCO2Min() {     return 400.0f; }  float CO2Sensor::getCO2Max() {     return 8192.0f; }  float CO2Sensor::getTVOCMin() {     return 0.0f; }  float CO2Sensor::getTVOCMax() {     return 1187.0f; }  bool CO2Sensor::_init() {     if (_enable_test)     {         return true;     }      _sensor = CCS811(CCS811_ADDR);      Wire.begin();     if (_sensor.beginWithStatus() != CCS811Core::CCS811_Stat_SUCCESS)     {         LOG_ERROR(\"init failed!\");         return false;     }      _sensor.setDriveMode(2); \/\/ Set measurement interval: 1 - every 1s, 2 - every 10s, 3 - every 60s      return true; }  void CO2Sensor::_check_data() {     if (_enable_test)     {         _mock_data();         _print_data();         return;     }      if (_sensor.dataAvailable())     {         _sensor.readAlgorithmResults();          _data.co2 = static_cast&lt;float&gt;(_sensor.getCO2());         if (_data.co2 &gt;= getCO2Max())         {             _data.co2 = getCO2Max();         }         if (_data.co2 &lt;= getCO2Min())         {             _data.co2 = getCO2Min();         }          _data.tvoc = static_cast&lt;float&gt;(_sensor.getTVOC());         if (_data.tvoc &gt;= getTVOCMax())         {             _data.tvoc = getTVOCMax();         }         if (_data.tvoc &lt;= getTVOCMin())         {             _data.tvoc = getTVOCMin();         }          _print_data();     }     else if (_sensor.checkForStatusError())     {         uint8_t error = _sensor.getErrorRegister();          if (error == 0xFF) \/\/ comm error         {             LOG_ERROR(\"failed to get ERROR_ID register.\");         }         else         {             String errMsg = \"Error: \";             if (error &amp; 1 &lt;&lt; 5)                 errMsg += \"HeaterSupply\";             if (error &amp; 1 &lt;&lt; 4)                 errMsg += \" HeaterFault \";             if (error &amp; 1 &lt;&lt; 3)                 errMsg += \" MaxResistance \";             if (error &amp; 1 &lt;&lt; 2)                 errMsg += \" MeasModeInvalid \";             if (error &amp; 1 &lt;&lt; 1)                 errMsg += \" ReadRegInvalid \";             if (error &amp; 1 &lt;&lt; 0)                 errMsg += \"MsgInvalid\";             if (!errMsg.isEmpty())                 LOG_ERROR(errMsg);         }     } }  void CO2Sensor::_mock_data() {     _data.tvoc = 3000.1;     _data.co2 += 100.0;     if (_data.co2 &gt;= 1500.0) _data.co2 = 0.0; }  void CO2Sensor::_print_data() {     LOG_DEBUG(\"CO2: \" + String(_data.co2) + \" ppm, TVOC: \" + String(_data.tvoc) + \" ppb\"); }  \/\/ --- CO2Scale --- CO2Scale &amp;CO2Scale::getInstance() {     static CO2Scale instance;     return instance; }  CO2Scale::CO2Scale() = default;  void CO2Scale::init(GyverDBFile *db) {     _db = db;     _initScales(); }  void CO2Scale::getScale(uint16_t &amp;rs, uint16_t &amp;re, uint16_t &amp;os, uint16_t &amp;oe, uint16_t &amp;ys, uint16_t &amp;ye, uint16_t &amp;gs, uint16_t &amp;ge) {     if ((*_db)[kk::co2_scale_type].toString() == \"1\")     {         rs = 75;         re = 100;         os = 50;         oe = 75;         ys = 25;         ye = 50;         gs = 0;         ge = 25;     }     else     {         rs = 66;         re = 100;         os = -1;         oe = -1;         ys = 33;         ye = 66;         gs = 0;         ge = 33;     }      return; }  void CO2Scale::getColor(uint16_t value, uint8_t &amp;r, uint8_t &amp;g, uint8_t &amp;b) {     const ColorThreshold *scale;     size_t size;      if ((*_db)[kk::co2_scale_type].toString() == \"1\")     {         scale = _default_scale;         size = 4;     }     else     {         scale = _easy_scale;         size = 3;     }      \/\/ try find by color in scale     for (size_t i = 0; i &lt; size; ++i)     {         if (value &lt;= scale[i].threshold)         {             r = scale[i].r;             g = scale[i].g;             b = scale[i].b;             return;         }     }      \/\/ if not in scale choose second default color     r = scale[size - 1].r;     g = scale[size - 1].g;     b = scale[size - 1].b; }  float CO2Scale::getMin() { return _min; } float CO2Scale::getMax() { return _max; } float CO2Scale::getHumanMax() { return _human_max; }  bool CO2Scale::needAlarm(uint16_t value) {     float co2_lvl = (*_db)[kk::co2_alarm_lvl].toFloat();     if (co2_lvl &lt;= 0)     {         return false;     }      return value &gt;= co2_lvl + 10; }  void CO2Scale::_initScales() {     _default_scale[0] = {390, 0, 255, 0}; \/\/ green     _default_scale[1] = {790, 255, 255, 0}; \/\/ yellow     _default_scale[2] = {1150, 255, 128, 0}; \/\/ orange     _default_scale[3] = {1500, 255, 0, 0}; \/\/ red      _easy_scale[0] = {590, 0, 255, 0}; \/\/ green     _easy_scale[1] = {1090, 255, 255, 0}; \/\/ yellow     _easy_scale[2] = {1500, 255, 0, 0}; \/\/ red } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041f\u043e\u0445\u043e\u0436\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0440\u0430\u0431\u043e\u0442\u0430 \u0441 <code>BME280<\/code>. \u041d\u043e \u0443 \u043d\u0435\u0433\u043e \u043d\u0435\u0442 \u0440\u0435\u0436\u0438\u043c\u043e\u0432 \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0438, \u0442.\u043a. \u0435\u0435 \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u043e\u043f\u044b\u0442\u043d\u044b\u043c \u043f\u0443\u0442\u0435\u043c &#8212; \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u0442\u044c \u0441 \u0447\u0435\u043c-\u0442\u043e \u0438 \u043f\u043e\u0432\u0435\u0440\u044f\u0442\u044c. <\/p>\n<details class=\"spoiler\">\n<summary>tph.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Arduino.h&gt; #include &lt;Wire.h&gt; #include &lt;GyverBME280.h&gt;  #include \"sensor_base.h\" #include \"connections\/mqtt_conn.h\" #include \"model\/tph_data.h\" #include \"db\/settings_db.h\" #include \"configs\/config.h\"  #define LOG_COMPONENT \"TPHSensor\" #include \"services\/logger.h\"  \/**  * @class TPHSensor  * @brief Class for working with BME280 sensor: reading temperature, pressure, and humidity data  *\/ class TPHSensor : public SensorBase { public:     \/**      * @brief Constructor      * @param ms Polling period in milliseconds      *\/     TPHSensor(uint32_t ms);      \/**      * @brief Initialize BME280 dependencies      *\/     void setup() override;     \/**      * @brief Main polling and data processing loop for the sensor      *\/     void exec() override;     \/**      * @brief Get minimum temperature value      * @return float Minimum temperature      *\/     float getTemperatureMin();     \/**      * @brief Get maximum temperature value      * @return float Maximum temperature      *\/     float getTemperatureMax();     \/**      * @brief Get current temperature value      * @return float Current temperature      *\/     float getTemperature();     \/**      * @brief Get minimum pressure value      * @return float Minimum pressure      *\/     float getPressureMin();     \/**      * @brief Get maximum pressure value      * @return float Maximum pressure      *\/     float getPressureMax();     \/**      * @brief Get current pressure value      * @return float Current pressure      *\/     float getPressure();     \/**      * @brief Get minimum humidity value      * @return float Minimum humidity      *\/     float getHumidityMin();     \/**      * @brief Get maximum humidity value      * @return float Maximum humidity      *\/     float getHumidityMax();     \/**      * @brief Get current humidity value      * @return float Current humidity      *\/     float getHumidity();     \/**      * @brief Get sensor type as string      * @return const char* Sensor type      *\/     const char *getType() const override;  private:     GyverBME280 _sensor; \/\/\/&lt; BME280 driver instance     TPHData _data;       \/\/\/&lt; Structure for storing current sensor data      \/**      * @brief Internal sensor initialization      * @return bool Initialization success      *\/     bool _init();     \/**      * @brief Check and update sensor data      *\/     void _check_data();     \/**      * @brief Print current data to log      *\/     void _print_data(); };<\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>tph.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"tph.h\"  TPHSensor::TPHSensor(uint32_t ms)      : SensorBase(ms) {     LOG_INFO(\"init...\");      _data.pressure = 0.0;     _data.temp = 0.0;          if (!_enable_test &amp;&amp; !_init()) {         LOG_ERROR(\"init failed! please check your wiring.\");         this-&gt;addLoop();         return;     }      _is_initialized = true;     LOG_INFO(\"init ok!\");     exec();     this-&gt;addLoop(); }  void TPHSensor::setup() {}  void TPHSensor::exec() {     if (!_is_initialized) {         _init();         return;     }          _check_data(); }  float TPHSensor::getTemperature() {      return _data.temp;  }  float TPHSensor::getPressure() {      return _data.pressure;  }  float TPHSensor::getHumidity() {      return _data.humidity;  }  const char* TPHSensor::getType() const {      return \"tph_sensor\";  }  float TPHSensor::getTemperatureMin() {      return -40.0f;  }  float TPHSensor::getTemperatureMax() {      return 85.0f;  }  float TPHSensor::getPressureMin() {      return 30000.0f;  \/\/ Minimum pressure: 300 hPa in Pa }  float TPHSensor::getPressureMax() {      return 110000.0f; \/\/ Maximum pressure: 1100 hPa in Pa }  float TPHSensor::getHumidityMin() {      return 0.0f; \/\/ Maximum pressure: 0 % in RH }  float TPHSensor::getHumidityMax() {      return 100.0f; \/\/ Maximum pressure: 100 % in RH }  bool TPHSensor::_init() {     if (!_sensor.begin(BME280_ADDR)) {         LOG_ERROR(\"init failed! please check your wiring.\");         return false;     }      _sensor.setFilter(FILTER_COEF_4);          return true; }  void TPHSensor::_check_data() {     if (_enable_test) {         _data.temp = 25.0f;         _data.pressure = 1.0f;         _data.humidity = 100.0f;         _print_data();         return;     }      _data.temp = constrain(_sensor.readTemperature(), getTemperatureMin(), getTemperatureMax());     _data.pressure = constrain(_sensor.readPressure(), getPressureMin(), getPressureMax());     _data.humidity = constrain(_sensor.readHumidity(), getHumidityMin(), getHumidityMax());      _print_data(); }  void TPHSensor::_print_data() {     LOG_DEBUG(String(\"temp: \") + _data.temp + \" \u00b0C, \" +              \"pressure: \" + (_data.pressure \/ 100.0f) + \" hPa, \" +             \"humidity: \" + _data.humidity + \" %\"); }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0448\u043b\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u00a0\u043f\u0435\u0440\u0435\u043e\u0434\u0438\u0447\u043d\u043e\u0441\u0442\u044c\u044e \u0432 30\u00a0\u0441\u0435\u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT \u0441\u00a0\u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043b\u0430\u0441\u0441\u0430 <code>MQTTPublisher<\/code>. \u0412\u0441\u0435\u0433\u043e \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0443\u0435\u0442\u0441\u044f 4\u00a0\u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430. \u041e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0434\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f\u00a0TVOC (common\/aqm\/tvoc). \u041f\u0440\u0438\u043c\u0435\u0440 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u043d\u0438\u0436\u0435.<\/p>\n<details class=\"spoiler\">\n<summary>src\/services\/publisher.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Arduino.h&gt; #include \"Looper.h\"  #include \"connections\/mqtt_conn.h\"  \/**  * @name MQTTPublisher  * @details Class for periodic publishing of values to an MQTT topic  *\/ class MQTTPublisher : public LoopTimerBase { public:     \/**      * @brief Callback to get the value for publishing      *\/     using ValueCallback = std::function&lt;float()&gt;;      \/**      * @brief Constructor      * @param ms Publish interval in milliseconds      * @param mqtt Reference to MQTTConn object      * @param topic MQTT topic for publishing (default is empty)      *\/     MQTTPublisher(uint32_t ms, MQTTConn &amp;mqtt, const String &amp;topic = \"\")         : LoopTimerBase(ms), _mqtt(mqtt), _enabled(true), _topic(topic)     {         this-&gt;addLoop();     }      \/**      * @brief Main publish loop      *\/     void exec() override     {         if (!_enabled || !_mqtt.connected() || !_cb)             return;         publish();     }      \/**      * @brief Set MQTT topic      * @param topic New topic      *\/     void setTopic(const String &amp;topic)     {         if (!topic.isEmpty())             _topic = topic;     }      \/**      * @brief Set callback to get the value      * @param cb Callback function      *\/     void setValueCb(ValueCallback cb)     {         _cb = cb;     }      \/**      * @brief Enable publishing      *\/     void enable() { _enabled = true; }     \/**      * @brief Disable publishing      *\/     void disable() { _enabled = false; }  private:     MQTTConn &amp;_mqtt;   \/\/\/&lt; Reference to MQTT connection     bool _enabled;     \/\/\/&lt; Publishing enabled flag     String _topic;     \/\/\/&lt; MQTT topic     ValueCallback _cb; \/\/\/&lt; Callback to get the value      \/**      * @brief Publish value to MQTT      *\/     void publish()     {         if (_topic.isEmpty())         {             return;         }          float value = _cb();         String payload = String(value, 2);         _mqtt.publish(_topic, payload);     } }; <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041b\u043e\u0433\u0438\u043a\u0430 \u0432\u044b\u0432\u043e\u0434\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0438 \u0432 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u044c \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0430 \u043d\u0430 \u0434\u0432\u0430 \u043a\u043b\u0430\u0441\u0441\u0430 &#8212; <code>Display<\/code> \u0438 <code>WebPanel<\/code>. \u0412 \u0441\u043b\u0443\u0447\u0430\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u043c\u044b \u0432\u044b\u0432\u043e\u0434\u0438\u043c \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432, \u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u0438, \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0441\u0435\u0442\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u0435\u043c \u0434\u0432\u0435 \u0442\u0435\u043c\u044b (\u0441\u0432\u0435\u0442\u043b\u0443\u044e \u0438 \u0442\u0435\u043c\u043d\u0443\u044e). \u0412 \u0441\u043b\u0443\u0447\u0430\u0435 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/registry.platformio.org\/libraries\/gyverlibs\/Settings\" rel=\"noopener noreferrer nofollow\">\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438<\/a> \u0432\u044b\u0432\u043e\u0434\u0438\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u043c \u0438\u0445 \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u044c. <\/p>\n<details class=\"spoiler\">\n<summary>src\/hmi\/display.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Looper.h&gt; #include \"SPI.h\" #include &lt;TFT_eSPI.h&gt;  #include \"model\/display_data.h\" #include \"widgets\/meter.h\" #include \"configs\/config.h\" #include \"sensors\/co2.h\" #include \"sensors\/tph.h\" #include \"connections\/wifi_conn.h\" #include \"connections\/mqtt_conn.h\" #include \"services\/ota.h\"  #define LOG_COMPONENT \"Display\" #include \"services\/logger.h\"  \/**  * @name Display  * @details Class for managing the TFT display, rendering widgets and sensor data  *\/ class Display : public LoopTimerBase { public:     \/**      * @name Display      * @param ms - update interval in milliseconds      * @param settingsDb - reference to settings database      * @param co2_sensor - reference to CO2 sensor      * @param tph_sensor - reference to temperature\/pressure sensor      * @param wifiConn - reference to WiFi connection      * @param mqttConn - reference to MQTT connection      * @param ota - reference to OTA update service      * @details Display class constructor, initializes display and widgets      *\/     Display(         uint32_t ms,         SettingsDB &amp;settingsDb,         CO2Sensor &amp;co2_sensor,         TPHSensor &amp;tph_sensor,         WiFiConn &amp;wifiConn,         MQTTConn &amp;mqttConn,         OTA &amp;ota)         : LoopTimerBase(ms),           _db(&amp;settingsDb.db()),           _co2_sensor(co2_sensor),           _tph_sensor(tph_sensor),           _co2_meter(nullptr),           _co2_scale(&amp;CO2Scale::getInstance()),           _wifi(&amp;wifiConn),           _mqtt(&amp;mqttConn),           _ota(&amp;ota)     {         _tft_rotate = (*_db)[kk::rotation_display].toInt();         _force_redraw = true;         _state.dark_theme = (*_db)[kk::use_dark_theme].toBool();         _state.last_co2_value = -1;         _state.last_wifi_state = false;         _state.last_mqtt_state = false;         _state.last_co2_sensor_state = false;         _state.last_render_time = 0;         _state.last_fw_ver = _ota-&gt;version();          LOG_INFO(\"init tft...\");          _tft.init();         _tft.setRotation(_tft_rotate);         _init_theme(true);         LOG_INFO(\"init tft ok!\");          LOG_INFO(\"init widgets...\");         _co2_meter = MeterWidget(&amp;_tft);          _co2_scale-&gt;init(_db);         uint16_t rs, re, os, oe, ys, ye, gs, ge;         _co2_scale-&gt;getScale(rs, re, os, oe, ys, ye, gs, ge);         _co2_meter.setZones(rs, re, os, oe, ys, ye, gs, ge);         _co2_meter.setTheme(_state.dark_theme);         _co2_meter.analogMeter(0, 0, _co2_scale-&gt;getHumanMax(), \"CO2\", \"\", \"\", \"\", \"\", \"\");         LOG_INFO(\"init widgets ok!\");          _render();         this-&gt;addLoop();     }      \/**      * @name exec      * @details Main display update loop, triggers rendering      *\/     void exec()     {         _render();     }      \/**      * @name setTheme      * @param dark - true for dark theme, false for light theme      * @details Change display theme and force redraw      *\/     void setTheme(bool dark)     {         if (_state.dark_theme == dark)             return;          _state.dark_theme = dark;         _init_theme(true);          _co2_meter.setTheme(dark);         uint16_t rs, re, os, oe, ys, ye, gs, ge;         _co2_scale-&gt;getScale(rs, re, os, oe, ys, ye, gs, ge);         _co2_meter.setZones(rs, re, os, oe, ys, ye, gs, ge);         _co2_meter.analogMeter(0, 0, _co2_scale-&gt;getHumanMax(), \"CO2\", \"\", \"\", \"\", \"\", \"\");          _force_redraw = true;         _render();     }      \/**      * @name moveRotation      * @details Change display rotation      *\/     void moveRotation()     {         _tft_rotate += 1;         if (_tft_rotate &gt; 3)             _tft_rotate = TFT_ROTATION_360;          (*_db)[kk::rotation_display] = _tft_rotate;          _tft.setRotation(_tft_rotate);          if (_state.dark_theme)         {             _tft.fillScreen(TFT_BLACK);         }         else         {             _tft.fillScreen(TFT_WHITE);         }          _co2_meter.setTheme(_state.dark_theme);         uint16_t rs, re, os, oe, ys, ye, gs, ge;         _co2_scale-&gt;getScale(rs, re, os, oe, ys, ye, gs, ge);         _co2_meter.setZones(rs, re, os, oe, ys, ye, gs, ge);         _co2_meter.analogMeter(0, 0, _co2_scale-&gt;getHumanMax(), \"CO2\", \"\", \"\", \"\", \"\", \"\");          _force_redraw = true;         _render();     }      \/**      * @name forceRender      * @details Re-render display data      *\/     void forceRender()     {         _co2_meter.setTheme(_state.dark_theme);         uint16_t rs, re, os, oe, ys, ye, gs, ge;         _co2_scale-&gt;getScale(rs, re, os, oe, ys, ye, gs, ge);         _co2_meter.setZones(rs, re, os, oe, ys, ye, gs, ge);         _co2_meter.analogMeter(0, 0, _co2_scale-&gt;getHumanMax(), \"CO2\", \"\", \"\", \"\", \"\", \"\");          _force_redraw = true;         _render();     }  private:     \/**      * @name _tft      * @details TFT display driver instance      *\/     TFT_eSPI _tft = TFT_eSPI();     \/**      * @name _db      * @details Pointer to settings database      *\/     GyverDBFile *_db;     \/**      * @name _co2_meter      * @details CO2 meter widget instance      *\/     MeterWidget _co2_meter;     \/**      * @name _co2_sensor      * @details Reference to CO2 sensor      *\/     CO2Sensor &amp;_co2_sensor;     \/**      * @name _co2_scale      * @details Pointer to CO2 scale instance      *\/     CO2Scale *_co2_scale;     \/**      * @name _tph_sensor      * @details Reference to temperature\/pressure sensor      *\/     TPHSensor &amp;_tph_sensor;     \/**      * @name _wifi      * @details Pointer to WiFi connection      *\/     WiFiConn *_wifi;     \/**      * @name _mqtt      * @details Pointer to MQTT connection      *\/     MQTTConn *_mqtt;     \/**      * @name _ota      * @details Pointer to OTA update service      *\/     OTA *_ota;     \/**      * @name _state      * @details Display state structure      *\/     DisplayState _state;     \/**      * @name _force_redraw      * @details Flag to force display redraw      *\/     bool _force_redraw = false;     \/**      * @name _tft_rotate      * @details Set display orientation      *\/     int _tft_rotate = TFT_ROTATION_0;      \/**      * @name _render      * @details Render all display widgets and info      *\/     void _render()     {         if (!_should_render())         {             return;         }          LOG_DEBUG(\"render started...\");         _print_wifi_info();         _print_mqtt_info();         _print_gauge();         _print_sensor_state();         _print_fw_version();         LOG_DEBUG(\"rendered ok!\");          _force_redraw = false;     }      \/**      * @name _should_render      * @details Check if display needs to be updated      * @return bool - true if update is needed      *\/     bool _should_render()     {         if (_force_redraw)         {             return true;         }          bool current_wifi_state = _wifi-&gt;connected();         if (current_wifi_state != _state.last_wifi_state)         {             _state.last_wifi_state = current_wifi_state;             return true;         }          bool current_mqtt_state = _mqtt-&gt;connected();         if (current_mqtt_state != _state.last_mqtt_state)         {             _state.last_mqtt_state = current_mqtt_state;             return true;         }          bool current_co2_sensor_state = _co2_sensor.isCalibrating();         if (current_co2_sensor_state != _state.last_co2_sensor_state)         {             _state.last_co2_sensor_state = current_co2_sensor_state;             _force_redraw = true;             return true;         }          float current_co2 = _co2_sensor.getCO2();         if (abs(current_co2 - _state.last_co2_value) &gt; 5.0)         {             _state.last_co2_value = current_co2;             return true;         }          float current_temp = _tph_sensor.getTemperature();         if (abs(current_temp - _state.last_temp_value) &gt; 1.0)         {             _state.last_temp_value = current_temp;             return true;         }          float current_pressure = _tph_sensor.getPressure();         if (abs(current_pressure - _state.last_pressure_value) &gt; 1.0)         {             _state.last_pressure_value = current_pressure;             return true;         }          float current_humidity = _tph_sensor.getHumidity();         if (abs(current_humidity - _state.last_humidity_value) &gt; 5.0)         {             _state.last_humidity_value = current_humidity;             return true;         }          String current_fw_ver = _ota-&gt;version();         if (current_fw_ver != _state.last_fw_ver)         {             _state.last_fw_ver = current_fw_ver;             return true;         }          bool current_has_updates = _ota-&gt;hasUpdate();         if (current_has_updates != _state.has_updates)         {             _state.has_updates = current_has_updates;             return true;         }          if ((millis() - _state.last_render_time) &gt; SEC_5)         {             _state.last_render_time = millis();             _force_redraw = true;             return true;         }          return false;     }      \/**      * @name _print_fw_version      * @details Print firmware version      *\/     void _print_fw_version()     {         if (_force_redraw)         {             \/\/ show current fw version             _tft.setCursor(100, 185);             if (_state.dark_theme)             {                 _tft.fillRect(100, 185, 60, 10, TFT_BLACK);             }             else             {                 _tft.fillRect(100, 185, 60, 10, TFT_WHITE);             }             _tft.setTextColor(TFT_LIGHTGREY);             _tft.print(F(\"v \"));             _tft.println(_state.last_fw_ver);              \/\/ show little green round dot as updates notification             _tft.setCursor(145, 185);             if (_state.dark_theme)             {                 if (_state.has_updates)                 {                     _tft.drawSmoothCircle(145, 185, 2, TFT_GREENYELLOW, TFT_BLACK);                 }                 else                 {                     _tft.drawSmoothCircle(145, 185, 2, TFT_BLACK, TFT_BLACK);                 }             }             else             {                 if (_state.has_updates)                 {                     _tft.drawSmoothCircle(145, 185, 2, TFT_GREEN, TFT_WHITE);                 }                 else                 {                     _tft.drawSmoothCircle(145, 185, 2, TFT_WHITE, TFT_WHITE);                 }             }         }     }      \/**      * @name _print_mqtt_info      * @details Print MQTT info      *\/     void _print_mqtt_info()     {         if (_force_redraw)         {             _tft.setCursor(130, 145);             if (_state.dark_theme)             {                 _tft.fillRect(130, 145, 60, 10, TFT_BLACK);             }             else             {                 _tft.fillRect(130, 145, 60, 10, TFT_WHITE);             }              if (!_state.last_mqtt_state)             {                 _tft.setTextColor(TFT_RED);                 _tft.println(F(\"MQTT\"));                 LOG_ERROR(\"mqtt not connected\");             }             else             {                 _tft.setTextColor(TFT_GREEN);                 _tft.println(F(\"MQTT\"));             }         }     }      \/**      * @name _print_wifi_info      * @details Print WiFi info and firmware version to display      *\/     void _print_wifi_info()     {         if (_force_redraw)         {             _init_theme(false);              _tft.setCursor(20, 130);             if (_state.dark_theme)             {                 _tft.fillRect(20, 130, 200, 10, TFT_BLACK);             }             else             {                 _tft.fillRect(20, 130, 200, 10, TFT_WHITE);             }              LOG_DEBUG(\"admin panel: http:\/\/\" + _wifi-&gt;ip());              _tft.setTextColor(TFT_LIGHTGREY);             _tft.print(F(\"admin panel: http:\/\/\"));             _tft.println(_wifi-&gt;ip());              _tft.setCursor(90, 145);             if (_state.dark_theme)             {                 _tft.fillRect(90, 145, 60, 10, TFT_BLACK);             }             else             {                 _tft.fillRect(90, 145, 60, 10, TFT_WHITE);             }              if (!_state.last_wifi_state)             {                 _tft.setTextColor(TFT_RED);                 _tft.println(F(\"WIFI\"));                 LOG_ERROR(\"wifi not connected\");             }             else             {                 _tft.setTextColor(TFT_GREEN);                 _tft.println(F(\"WIFI\"));             }         }     }      \/**      * @name _print_gauge      * @details Print and update CO2 gauge widget      *\/     void _print_gauge()     {         if (!_co2_sensor.isInitialized())             return;          float value = static_cast&lt;float&gt;(_co2_sensor.getCO2());         if (value &gt; _co2_scale-&gt;getHumanMax())         {             value = _co2_scale-&gt;getHumanMax();         }          _init_theme(false);          LOG_DEBUG(\"update gauge value: \" + String(value));         _tft.setCursor(0, 0);         _co2_meter.updateNeedle(value, 10);     }      \/**      * @name _print_sensor_state      * @details Print sensor state (e.g. calibration) to display      *\/     void _print_sensor_state()     {         if (_force_redraw)         {             _init_theme(false);              _tft.setCursor(90, 165);             if (_state.dark_theme)             {                 _tft.fillRect(90, 165, 80, 10, TFT_BLACK);             }             else             {                 _tft.fillRect(90, 165, 80, 10, TFT_WHITE);             }              _tft.setTextColor(TFT_CYAN);             if (_state.last_co2_sensor_state)             {                 _tft.println(F(\"CALIBRATION\"));             }             else             {                 if (_state.dark_theme)                 {                     _tft.fillRect(90, 165, 80, 10, TFT_BLACK);                 }                 else                 {                     _tft.fillRect(90, 165, 80, 10, TFT_WHITE);                 }             }         }     }      \/**      * @name _init_theme      * @param fill - whether to fill the background      * @details Initialize display theme (dark\/light)      *\/     void _init_theme(bool fill)     {         _tft.setTextSize(1);          if (_state.dark_theme)         {             if (fill)                 _tft.fillScreen(TFT_BLACK);             _tft.setTextColor(TFT_WHITE);             return;         }          if (fill)             _tft.fillScreen(TFT_WHITE);         _tft.setTextColor(TFT_BLACK);     } }; <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/hmi\/web.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;SettingsGyver.h&gt;  #include \"configs\/config.h\" #include \"db\/settings_db.h\" #include \"connections\/mqtt_conn.h\" #include \"sensors\/co2.h\" #include \"sensors\/tph.h\" #include \"controllers\/rgb.h\" #include \"connections\/wifi_conn.h\" #include \"services\/publisher.h\" #include \"services\/ota.h\" #include \"display.h\"  #define LOG_COMPONENT \"WebPannel\" #include \"services\/logger.h\"  \/**  * @name WebPanel  * @details Class for managing the web interface panel, settings, and integration with other modules  *\/ class WebPanel : public LoopTickerBase { public:     \/**      * @name WebPanel      * @param settingsDb - reference to settings database      * @param wifiConn - reference to WiFi connection      * @param ota - reference to OTA update service      * @param mqttConn - reference to MQTT connection      * @param rgbCtrl - reference to RGB controller      * @param hmi - reference to display\/HMI      * @param co2sensor - reference to CO2 sensor      * @param tphSensor - reference to TPH sensor      * @details Full-featured constructor for WebPanel      *\/     WebPanel(SettingsDB&amp; settingsDb,          WiFiConn&amp; wifiConn,          OTA&amp; ota,         MQTTConn&amp; mqttConn,          RGBController&amp; rgbCtrl,         Display&amp; hmi,         CO2Sensor&amp; co2sensor,         TPHSensor&amp; tphSensor     );     \/**      * @name WebPanel      * @param settingsDb - reference to settings database      * @param wifiConn - reference to WiFi connection      * @details Minimal constructor for WebPanel (WiFi only)      *\/     WebPanel(SettingsDB&amp; settingsDb,          WiFiConn&amp; wifiConn     );      \/**      * @name exec      * @details Main loop for web panel logic      *\/     void exec() override;  private:     \/**      * @name _init      * @details Internal initialization of web panel and dependencies      *\/     void _init();     \/**      * @name _update      * @param u - settings updater      * @details Update settings from web interface      *\/     void _update(sets::Updater&amp; u);     \/**      * @name _build      * @param b - settings builder      * @details Build web interface structure and settings      *\/     void _build(sets::Builder&amp; b);      SettingsGyver _sett;              \/\/\/&lt; Settings manager instance     GyverDBFile* _db;                 \/\/\/&lt; Pointer to database file     WiFiConn* _wifi_conn;             \/\/\/&lt; Pointer to WiFi connection     OTA* _ota;                        \/\/\/&lt; Pointer to OTA update service     MQTTConn* _mqtt_conn;             \/\/\/&lt; Pointer to MQTT connection     RGBController* _rgb_controller;   \/\/\/&lt; Pointer to RGB controller     Display* _display;                \/\/\/&lt; Pointer to display\/HMI     CO2Sensor* _co2_sensor;           \/\/\/&lt; Pointer to CO2 sensor     TPHSensor* _tph_sensor;           \/\/\/&lt; Pointer to TPH sensor      bool _is_initialized;             \/\/\/&lt; Initialization flag }; <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/hmi\/web.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"web.h\"  sets::Logger webLogger(1024); bool cfm_fw = false;  WebPanel::WebPanel(     SettingsDB &amp;settingsDb,     WiFiConn &amp;wifiConn)     : LoopTickerBase(),       _sett(String(APP_NAME) + \" v\" + String(APP_VERSION), &amp;settingsDb.db()),       _db(&amp;settingsDb.db()),       _wifi_conn(&amp;wifiConn),       _is_initialized(false) {     _init(); }  WebPanel::WebPanel(     SettingsDB &amp;settingsDb,     WiFiConn &amp;wifiConn,     OTA &amp;ota,     MQTTConn &amp;mqttConn,     RGBController &amp;rgbController,     Display &amp;display,     CO2Sensor &amp;co2sensor,     TPHSensor &amp;tphSeonsor)     : LoopTickerBase(),       _sett(String(APP_NAME) + \" v\" + ota.version(), &amp;settingsDb.db()),       _db(&amp;settingsDb.db()),       _wifi_conn(&amp;wifiConn),       _ota(&amp;ota),       _mqtt_conn(&amp;mqttConn),       _rgb_controller(&amp;rgbController),       _display(&amp;display),       _co2_sensor(&amp;co2sensor),       _tph_sensor(&amp;tphSeonsor),       _is_initialized(false) {     _init(); }  void WebPanel::exec() {     if (!_is_initialized)     {         LOG_ERROR(\"call setup first!\");         return;     }      _sett.tick(); }  void WebPanel::_init() {     LOG_INFO(\"init...\");     Logger::getInstance().initWebLogger(webLogger);     _sett.config.requestTout = SEC_10;     _sett.config.pingTout = SEC_30;     _sett.config.updateTout = 0;     _sett.config.theme = sets::Colors::Green;     _sett.begin(false);     _sett.onUpdate([this](sets::Updater &amp;u)                    { this-&gt;_update(u); });     _sett.onBuild([this](sets::Builder &amp;b)                   { this-&gt;_build(b); });     _sett.onFocusChange([this]()                         { LOG_DEBUG(\"browser connected!\"); });     LOG_INFO(\"init ok!\");      _is_initialized = true;     this-&gt;addLoop(); }  void WebPanel::_update(sets::Updater &amp;u) { #ifdef WEB_PANEL_DASHBOARD     u.update(SH(\"eco2_gauge\"), _co2_sensor-&gt;getCO2());     u.update(\"tvoc_gauge\"_h, _co2_sensor-&gt;getTVOC());     u.update(\"temp_gauge\"_h, _tph_sensor-&gt;getTemperature());     u.update(\"pressure_gauge\"_h, _tph_sensor-&gt;getPressure());     u.update(\"humidity_gauge\"_h, _tph_sensor-&gt;getHumidity()); #endif      u.update(H(log), webLogger);      if (_ota &amp;&amp; _ota-&gt;hasUpdate())         u.update(\"update\"_h, \"New updates available. Try update firmware?\"); }  void WebPanel::_build(sets::Builder &amp;b) { #ifdef WEB_PANEL_DASHBOARD     SUB_BUILD_BEGIN     sets::Group g(b, \"Dashboard\");     b.LinearGauge(SH(\"eco2_gauge\"), \"eCO2\", _co2_sensor-&gt;getCO2Min(), _co2_sensor-&gt;getCO2Max(), \"ppm\", 0.0f);     b.LinearGauge(\"tvoc_gauge\"_h, \"TVOC\", _co2_sensor-&gt;getTVOCMin(), _co2_sensor-&gt;getTVOCMax(), \"ppb\", 0.0f);     b.LinearGauge(\"temp_gauge\"_h, \"Temp\", _tph_sensor-&gt;getTemperatureMin(), _tph_sensor-&gt;getTemperatureMax(), \"\u00b0C\", 0.0f);     b.LinearGauge(\"pressure_gauge\"_h, \"Pressure\", _tph_sensor-&gt;getPressureMin(), _tph_sensor-&gt;getPressureMax(), \"hPa\", 0.0f);     b.LinearGauge(\"humidity_gauge\"_h, \"Humidity\", _tph_sensor-&gt;getHumidityMin(), _tph_sensor-&gt;getHumidityMax(), \"%\", 0.0f);     SUB_BUILD_END #endif      sets::Group g(b, \"Settings\");      SUB_BUILD_BEGIN     sets::Menu m(b, \"Wi-Fi\");     b.Input(kk::wifi_ssid, \"SSID\");     b.Pass(kk::wifi_pass, \"Password\");     b.Button(SH(\"wifi_save\"), \"Save\");     SUB_BUILD_END      SUB_BUILD_BEGIN     sets::Menu m(b, \"MQTT\");     b.Switch(kk::mqtt_enabled, \"Enabled\");     b.Input(kk::mqtt_server, \"Server\");     b.Number(kk::mqtt_port, \"Port\");     b.Input(kk::mqtt_username, \"Username\");     b.Pass(kk::mqtt_pass, \"Password\");     b.Input(kk::mqtt_device_id, \"Device ID\");     b.Button(SH(\"mqtt_save\"), \"Save\");     SUB_BUILD_END      SUB_BUILD_BEGIN     sets::Menu m(b, \"CO2\");     b.Number(kk::co2_alarm_lvl, \"Alarm value\", nullptr, 0, 8000);     if (b.Select(kk::co2_scale_type, \"Scale type\", co2_scale_types))     {         _display-&gt;forceRender();     }      sets::Group g(b, \"Calibration\");     if (b.beginButtons())     {         if (b.Button(SH(\"co2_calibrate_run\"), \"Run\", sets::Colors::Green))         {             LOG_DEBUG(\"co2_calibrate_run pressed\");             _co2_sensor-&gt;startCalibration();         }         if (b.Button(SH(\"co2_calibrate_stop\"), \"Stop\", sets::Colors::Red))         {             LOG_DEBUG(\"co2_calibrate_stop pressed\");             _co2_sensor-&gt;forceStopCalibration();         }         b.endButtons();     }     SUB_BUILD_END      SUB_BUILD_BEGIN     sets::Menu m(b, \"System\");     if (b.Switch(kk::rgb_enabled, \"RGB Enabled\"))     {         _rgb_controller-&gt;toggle((*_db)[kk::rgb_enabled].toBool());     }     if (b.Switch(kk::use_dark_theme, \"Use dark theme\"))     {         _display-&gt;setTheme((*_db)[kk::use_dark_theme].toBool());     }     if (b.Select(kk::log_lvl, \"Log\", log_levels))     {         SET_LOG_LEVEL((*_db)[kk::log_lvl].toString());     }     if (b.Button(SH(\"rotate_display\"), \"Rotate display\"))     {         if (_display)             _display-&gt;moveRotation();     }     b.Log(H(log), webLogger);     if (b.Button(SH(\"update_fw\"), \"Update firmware\") || b.Confirm(\"update\"_h))     {         if (_ota)         {             LOG_INFO(\"ota update start\");             _ota-&gt;update(true);         }     }     SUB_BUILD_END      SUB_BUILD_BEGIN     b.Link(\"User Manual\", USER_MANUAL_URL);     SUB_BUILD_END      SUB_BUILD_BEGIN     if (b.build.isAction())     {            switch (b.build.id)         {         case SH(\"wifi_save\"):             LOG_DEBUG(\"wifi_save pressed\");              if (_db &amp;&amp; _db-&gt;update() &amp;&amp; _wifi_conn)             {                 _wifi_conn-&gt;connect();                 return;             }              break;          case SH(\"mqtt_save\"):             LOG_DEBUG(\"mqtt_save pressed\");              if (_db &amp;&amp; _db-&gt;update() &amp;&amp; _mqtt_conn)             {                 _mqtt_conn-&gt;setDeviceID((*_db)[kk::mqtt_device_id].toString());                 _mqtt_conn-&gt;connect();                 return;             }              break;         default:             break;         }     }     SUB_BUILD_END } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0414\u0430\u043d\u043d\u044b\u0435 \u043e\u00a0CO2\u00a0\u0432\u044b\u0432\u043e\u0434\u044f\u0442\u0441\u044f \u043d\u0430\u00a0\u043f\u0435\u0440\u0435\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439 \u0432\u0438\u0434\u0436\u0435\u0442 \u0438\u0437\u00a0\u044d\u0442\u043e\u0439 <a href=\"https:\/\/github.com\/Bodmer\/TFT_eWidget\/tree\/main\/src\/widgets\/meter\" rel=\"noopener noreferrer nofollow\">\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438<\/a>. \u042f \u0434\u043e\u0431\u0430\u0432\u0438\u043b \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u0442\u0435\u043c\u043d\u043e\u0439 \u0442\u0435\u043c\u044b \u0438 \u0432\u043d\u0435\u0441 \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u0438\u0435 \u0444\u0438\u043a\u0441\u044b (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0443\u0431\u0440\u0430\u043b \u043e\u043a\u0430\u043d\u0442\u043e\u0432\u043a\u0443 \u043f\u043e\u00a0\u043a\u043e\u043d\u0442\u0443\u0440\u0443)<br \/>\u0422\u0430\u043a\u00a0\u0436\u0435 \u0432\u00a0\u0445\u043e\u0434\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0438 \u043e\u0442\u043b\u0430\u0434\u043a\u0438 \u0437\u0430\u043c\u0435\u0442\u0438\u043b \u0436\u0443\u0442\u043a\u0438\u0435 \u043b\u0430\u0433\u0438 \u043f\u0440\u0438\u00a0\u0432\u044b\u0432\u043e\u0434\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043d\u0430\u00a0\u0434\u0438\u0441\u043f\u043b\u0435\u0439. \u041a\u0430\u043a\u00a0\u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c, \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430\u00a0\u0434\u0438\u0441\u043f\u043b\u0435\u0435 \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u0434\u043e\u0440\u043e\u0433\u0430\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u0438 \u043d\u0435\u0442 \u0441\u043c\u044b\u0441\u043b\u0430 \u0447\u0430\u0441\u0442\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0432 <code>display.h<\/code> (\u0444\u0443\u043d\u043a\u0446\u0438\u044f <em>should<\/em>render), \u0442\u0430\u043a \u0438 \u0432 <code>meter.cpp<\/code> \u044f \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e \u0432\u0441\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u044e \u0438\u0445 \u0441\u00a0\u043d\u043e\u0432\u044b\u043c\u0438, \u0447\u0442\u043e\u0431\u044b\u00a0\u043b\u0438\u0448\u043d\u0438\u0439 \u0440\u0430\u0437 \u043d\u0435\u00a0\u0442\u0440\u0438\u0433\u0433\u0435\u0440\u0438\u0442\u044c \u043f\u0435\u0440\u0435\u0440\u0438\u0441\u043e\u0432\u043a\u0443.<\/p>\n<details class=\"spoiler\">\n<summary>meter.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#ifndef meter_h #define meter_h  #include \"Arduino.h\" #include \"TFT_eSPI.h\"  \/\/ Meter class class MeterWidget { public:   MeterWidget(TFT_eSPI *tft);    \/\/ Set widget theme   void setTheme(bool dark);   \/\/ Set red, orange, yellow and green start+end zones as a percentage of full scale   void setZones(uint16_t rs, uint16_t re, uint16_t os, uint16_t oe, uint16_t ys, uint16_t ye, uint16_t gs, uint16_t ge);   \/\/ Draw meter meter at x, y and define full scale range plus the scale labels   void analogMeter(uint16_t x, uint16_t y, float fullScale, const char *units, const char *s0, const char *s1, const char *s2, const char *s3, const char *s4);   \/\/ Draw meter meter at x, y and define full scale range plus the scale labels   void analogMeter(uint16_t x, uint16_t y, float startScale, float endScale, const char *units, const char *s0, const char *s1, const char *s2, const char *s3, const char *s4);   \/\/ Move needle to new position   void updateNeedle(float value, uint32_t ms_delay);  private:   \/\/ Pointer to TFT_eSPI class functions   TFT_eSPI *ntft;    float ltx;         \/\/ x delta of needle start   uint16_t osx, osy; \/\/ Saved x &amp; y coords of needle end   int old_analog;    \/\/ Value last displayed   int old_value;     \/\/ Value last displayed    bool dark_theme;    \/\/ x, y coord of top left corner of meter graphic   uint16_t mx;   uint16_t my;    \/\/ Scale factor   float factor;   float scaleStart;    \/\/ Scale label   char mlabel[9];    \/\/ Scale values   char ms0[5];   char ms1[5];   char ms2[5];   char ms3[5];   char ms4[5];    \/\/ Scale colour zone start end end values   int16_t redStart;   int16_t redEnd;   int16_t orangeStart;   int16_t orangeEnd;   int16_t yellowStart;   int16_t yellowEnd;   int16_t greenStart;   int16_t greenEnd; };  #endif <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>meter.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"Arduino.h\"  #include \"meter.h\"  \/\/ ######################################################################### \/\/ Meter constructor \/\/ ######################################################################### MeterWidget::MeterWidget(TFT_eSPI *tft) {   ltx = 0;              \/\/ Saved x coord of bottom of needle   osx = 120, osy = 120; \/\/ Saved x &amp; y coords   old_analog = -999;    \/\/ Value last displayed   old_value = -999;     \/\/ Value last displayed    mx = 0;   my = 0;    factor = 1.0;   scaleStart = 0.0;    memset(mlabel, 0, sizeof(mlabel));    \/\/ Defaults   strncpy(ms0, \"0\", 4);   strncpy(ms1, \"25\", 4);   strncpy(ms2, \"50\", 4);   strncpy(ms3, \"75\", 4);   strncpy(ms4, \"100\", 4);    redStart = 0;   redEnd = 0;   orangeStart = 0;   orangeEnd = 0;   yellowStart = 0;   yellowEnd = 0;   greenStart = 0;   greenEnd = 0;    ntft = tft; }  \/\/ ######################################################################### \/\/ Draw meter meter at x, y and define full scale range &amp; the scale labels \/\/ ######################################################################### void MeterWidget::analogMeter(uint16_t x, uint16_t y, float fullScale, const char *units, const char *s0, const char *s1, const char *s2, const char *s3, const char *s4) {   analogMeter(x, y, 0.0, fullScale, units, s0, s1, s2, s3, s4); }  void MeterWidget::analogMeter(uint16_t x, uint16_t y, float startScale, float endScale, const char *units, const char *s0, const char *s1, const char *s2, const char *s3, const char *s4) {   \/\/ Save offsets for needle plotting   mx = x;   my = y;   factor = 100.0 \/ (endScale - startScale);   scaleStart = startScale;    strncpy(mlabel, units, sizeof(mlabel) - 1);   mlabel[sizeof(mlabel) - 1] = '\\0';    strncpy(ms0, s0, 4);   strncpy(ms1, s1, 4);   strncpy(ms2, s2, 4);   strncpy(ms3, s3, 4);   strncpy(ms4, s4, 4);    \/\/ Meter outline   if (dark_theme)   {     ntft-&gt;fillRect(x + 5, y + 3, 230, 119, TFT_BLACK);     ntft-&gt;setTextColor(TFT_WHITE); \/\/ Text colour   }   else   {     ntft-&gt;fillRect(x + 5, y + 3, 230, 119, TFT_WHITE);     ntft-&gt;setTextColor(TFT_BLACK); \/\/ Text colour   }    \/\/ Draw ticks every 5 degrees from -50 to +50 degrees (100 deg. FSD swing)   for (int i = -50; i &lt; 51; i += 5)   {     \/\/ Long scale tick length     int tl = 15;      \/\/ Coordinates of tick to draw     float sx = cos((i - 90) * 0.0174532925);     float sy = sin((i - 90) * 0.0174532925);     uint16_t x0 = x + sx * (100 + tl) + 120;     uint16_t y0 = y + sy * (100 + tl) + 140;     uint16_t x1 = x + sx * 100 + 120;     uint16_t y1 = y + sy * 100 + 140;      \/\/ Coordinates of next tick for zone fill     float sx2 = cos((i + 5 - 90) * 0.0174532925);     float sy2 = sin((i + 5 - 90) * 0.0174532925);     int x2 = x + sx2 * (100 + tl) + 120;     int y2 = y + sy2 * (100 + tl) + 140;     int x3 = x + sx2 * 100 + 120;     int y3 = y + sy2 * 100 + 140;      \/\/ Red zone limits     if (redEnd &gt; redStart)     {       if (i &gt;= redStart &amp;&amp; i &lt; redEnd)       {         ntft-&gt;fillTriangle(x0, y0, x1, y1, x2, y2, TFT_RED);         ntft-&gt;fillTriangle(x1, y1, x2, y2, x3, y3, TFT_RED);       }     }      \/\/ Orange zone limits     if (orangeEnd &gt; orangeStart)     {       if (i &gt;= orangeStart &amp;&amp; i &lt; orangeEnd)       {         ntft-&gt;fillTriangle(x0, y0, x1, y1, x2, y2, TFT_ORANGE);         ntft-&gt;fillTriangle(x1, y1, x2, y2, x3, y3, TFT_ORANGE);       }     }      \/\/ Yellow zone limits     if (yellowEnd &gt; yellowStart)     {       if (i &gt;= yellowStart &amp;&amp; i &lt; yellowEnd)       {         ntft-&gt;fillTriangle(x0, y0, x1, y1, x2, y2, TFT_YELLOW);         ntft-&gt;fillTriangle(x1, y1, x2, y2, x3, y3, TFT_YELLOW);       }     }      \/\/ Green zone limits     if (greenEnd &gt; greenStart)     {       if (i &gt;= greenStart &amp;&amp; i &lt; greenEnd)       {         ntft-&gt;fillTriangle(x0, y0, x1, y1, x2, y2, TFT_GREEN);         ntft-&gt;fillTriangle(x1, y1, x2, y2, x3, y3, TFT_GREEN);       }     }      \/\/ Short scale tick length     if (i % 25 != 0)       tl = 8;      \/\/ Recalculate coords in case tick length changed     x0 = x + sx * (100 + tl) + 120;     y0 = y + sy * (100 + tl) + 140;     x1 = x + sx * 100 + 120;     y1 = y + sy * 100 + 140;      \/\/ Draw tick     if (dark_theme)     {       ntft-&gt;drawLine(x0, y0, x1, y1, TFT_WHITE);     }     else     {       ntft-&gt;drawLine(x0, y0, x1, y1, TFT_BLACK);     }      \/\/ Check if labels should be drawn, with position tweaks     if (i % 25 == 0)     {       \/\/ Calculate label positions       x0 = x + sx * (100 + tl + 10) + 120;       y0 = y + sy * (100 + tl + 10) + 140;       switch (i \/ 25)       {       case -2:         ntft-&gt;drawCentreString(ms0, x0, y0 - 12, 2);         break;       case -1:         ntft-&gt;drawCentreString(ms1, x0, y0 - 9, 2);         break;       case 0:         ntft-&gt;drawCentreString(ms2, x0, y0 - 6, 2);         break;       case 1:         ntft-&gt;drawCentreString(ms3, x0, y0 - 9, 2);         break;       case 2:         ntft-&gt;drawCentreString(ms4, x0, y0 - 12, 2);         break;       }     }      \/\/ Now draw the arc of the scale     sx = cos((i + 5 - 90) * 0.0174532925);     sy = sin((i + 5 - 90) * 0.0174532925);     x0 = x + sx * 100 + 120;     y0 = y + sy * 100 + 140;     \/\/ Draw scale arc, don't draw the last part     if (i &lt; 50)     {       if (dark_theme)       {         ntft-&gt;drawLine(x0, y0, x1, y1, TFT_WHITE);       }       else       {         ntft-&gt;drawLine(x0, y0, x1, y1, TFT_BLACK);       }     }   }    ntft-&gt;drawString(mlabel, x + 5 + 230 - 40, y + 119 - 20, 2); \/\/ Units at bottom right   \/\/ ntft-&gt;drawCentreString(mlabel, x + 120, y + 70, 4);          \/\/ Comment out to avoid font 4   \/\/ ntft-&gt;drawRect(x + 5, y + 3, 230, 119, TFT_WHITE);           \/\/ Draw bezel line    updateNeedle(0, 0); }  \/\/ ######################################################################### \/\/ Update needle position \/\/ This function is blocking while needle moves, time depends on ms_delay \/\/ 10ms minimises needle flicker if text is drawn within needle sweep area \/\/ Smaller values OK if text not in sweep area, zero for instant movement but \/\/ does not look realistic... (note: 100 increments for full scale deflection) \/\/ ######################################################################### void MeterWidget::updateNeedle(float val, uint32_t ms_delay) {   int value = (val - scaleStart) * factor;   old_value = value;   char buf[8];   dtostrf(val, 6, 1, buf);    char *p = buf;   while (*p == ' ')     p++;    if (dark_theme)   {     ntft-&gt;setTextColor(TFT_WHITE, TFT_BLACK);   }   else   {     ntft-&gt;setTextColor(TFT_BLACK, TFT_WHITE);   }    int clearWidth = 60;   int clearHeight = 20;    uint16_t bgColor = dark_theme ? TFT_BLACK : TFT_WHITE;    ntft-&gt;fillRect(mx + 50 - clearWidth, my + 119 - 20, clearWidth, clearHeight, bgColor);   ntft-&gt;drawRightString(p, mx + 50, my + 119 - 20, 2);    if (value &lt; -10)     value = -10; \/\/ Limit value to emulate needle end stops   if (value &gt; 110)     value = 110;    int value_diff = abs(value - old_analog);   if (value_diff &lt;= 5)   {     ms_delay = 0; \/\/ Instant update for small changes   }   else if (value_diff &lt;= 15)   {     ms_delay = ms_delay \/ 2; \/\/ Faster animation for medium changes   }    \/\/ Move the needle until new value reached   while (value != old_analog)   {     if (old_analog &lt; value)       old_analog++;     else       old_analog--;      if (ms_delay == 0)       old_analog = value; \/\/ Update immediately if delay is 0      float sdeg = map(old_analog, -10, 110, -150, -30); \/\/ Map value to angle     \/\/ Calculate tip of needle coords     float sx = cos(sdeg * 0.0174532925);     float sy = sin(sdeg * 0.0174532925);      \/\/ Calculate x delta of needle start (does not start at pivot point)     float tx = tan((sdeg + 90) * 0.0174532925);      \/\/ Erase old needle image     if (dark_theme)     {       ntft-&gt;drawLine(mx + 120 + 20 * ltx - 1, my + 140 - 20, mx + osx - 1, my + osy, TFT_BLACK);       ntft-&gt;drawLine(mx + 120 + 20 * ltx, my + 140 - 20, mx + osx, my + osy, TFT_BLACK);       ntft-&gt;drawLine(mx + 120 + 20 * ltx + 1, my + 140 - 20, mx + osx + 1, my + osy, TFT_BLACK);       ntft-&gt;setTextColor(TFT_WHITE);     }     else     {       ntft-&gt;drawLine(mx + 120 + 20 * ltx - 1, my + 140 - 20, mx + osx - 1, my + osy, TFT_WHITE);       ntft-&gt;drawLine(mx + 120 + 20 * ltx, my + 140 - 20, mx + osx, my + osy, TFT_WHITE);       ntft-&gt;drawLine(mx + 120 + 20 * ltx + 1, my + 140 - 20, mx + osx + 1, my + osy, TFT_WHITE);       ntft-&gt;setTextColor(TFT_BLACK);     }      \/\/ Re-plot text under needle     \/\/ ntft-&gt;drawCentreString(mlabel, mx + 120, my + 70, 4); \/\/ \/\/ Comment out to avoid font 4      \/\/ Store new needle end coords for next erase     ltx = tx;     osx = sx * 98 + 120;     osy = sy * 98 + 140;      \/\/ Draw the needle in the new position, magenta makes needle a bit bolder     \/\/ draws 3 lines to thicken needle     ntft-&gt;drawLine(mx + 120 + 20 * ltx - 1, my + 140 - 20, mx + osx - 1, my + osy, TFT_RED);     ntft-&gt;drawLine(mx + 120 + 20 * ltx, my + 140 - 20, mx + osx, my + osy, TFT_MAGENTA);     ntft-&gt;drawLine(mx + 120 + 20 * ltx + 1, my + 140 - 20, mx + osx + 1, my + osy, TFT_RED);      \/\/ Slow needle down slightly as it approaches new position     if (abs(old_analog - value) &lt; 10)       ms_delay += ms_delay \/ 5;      \/\/ Wait before next update (only if delay &gt; 0)     if (ms_delay &gt; 0)     {       delay(ms_delay);     }   } }  \/\/ ######################################################################### \/\/ Set red, orange, yellow and green start+end zones as a % of full scale \/\/ ######################################################################### void MeterWidget::setZones(uint16_t rs, uint16_t re, uint16_t os, uint16_t oe, uint16_t ys, uint16_t ye, uint16_t gs, uint16_t ge) {   \/\/ Meter scale is -50 to +50   redStart = rs - 50;   redEnd = re - 50;   orangeStart = os - 50;   orangeEnd = oe - 50;   yellowStart = ys - 50;   yellowEnd = ye - 50;   greenStart = gs - 50;   greenEnd = ge - 50; }  \/\/ ######################################################################### \/\/ Set widget theme \/\/ ######################################################################### void MeterWidget::setTheme(bool dark) {   dark_theme = dark;   updateNeedle(old_value, 0); }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0424\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u043c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0447\u0435\u0440\u0435\u0437 RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434. \u0421\u0435\u0439\u0447\u0430\u0441 \u043e\u043d \u0431\u0435\u0441\u043f\u043e\u043b\u0435\u0437\u043d\u044b\u0439, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435 \u0442\u0430\u043a \u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041d\u043e \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0441 \u0434\u0430\u043b\u0435\u043a\u0430 \u043f\u043e \u043d\u0435\u043c\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043d\u044f\u0442\u044c \u043a\u0430\u043a\u043e\u0439 \u0441\u0435\u0439\u0447\u0430\u0441 \u0443\u0440\u043e\u0432\u0435\u043d\u044c CO2.<\/p>\n<details class=\"spoiler\">\n<summary>src\/controllers\/rgb.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Adafruit_NeoPixel.h&gt;  #include \"controller_base.h\" #include \"configs\/config.h\" #include \"sensors\/co2.h\"  #define LOG_COMPONENT \"RGBController\" #include \"services\/logger.h\"  \/**  * @name RGBController  * @details Class for controlling RGB LED strip based on CO2 levels  *\/ class RGBController : public ControllerBase { public:     \/**      * @name UpdaterCallback      * @details Callback type for updating CO2 value      * @return uint16_t - current CO2 value      *\/     using UpdaterCallback = std::function&lt;uint16_t()&gt;;      \/**      * @name RGBController      * @param ms - update interval in milliseconds      * @param settingsDb - reference to settings database      * @details Constructor for RGBController      *\/     RGBController(uint32_t ms, SettingsDB &amp;settingsDb);     \/**      * @name ~RGBController      * @details Destructor for RGBController      *\/     ~RGBController();      \/**      * @name exec      * @details Main update loop for RGB controller      *\/     void exec() override;     \/**      * @name toggle      * @param value - enable or disable RGB      * @details Enable or disable RGB output      *\/     void toggle(bool value) override;      \/**      * @name setUpdaterCb      * @param cb - callback to provide CO2 value      * @details Set callback for updating CO2 value      *\/     void setUpdaterCb(UpdaterCallback cb);     \/**      * @name renderLevel      * @param value - CO2 value      * @details Render color based on CO2 value      *\/     void renderLevel(float value);     \/**      * @name clear      * @details Clear all LEDs (turn off)      *\/     void clear();     \/**      * @name getType      * @return const char* - controller type      * @details Return controller type string      *\/     const char *getType() const override;  private:     \/**      * @name _leds      * @details Pointer to Adafruit_NeoPixel instance      *\/     Adafruit_NeoPixel *_leds;     \/**      * @name _db      * @details Pointer to settings database file      *\/     GyverDBFile *_db;     \/**      * @name _u_cb      * @details Callback for updating CO2 value      *\/     UpdaterCallback _u_cb;     \/**      * @name _co2_scale      * @details Pointer to CO2 scale instance for color mapping      *\/     CO2Scale *_co2_scale;     \/**      * @name _blink      * @details State for alarm blinking      *\/     bool _blink;     \/**      * @name _pin      * @details Pin number for RGB strip      *\/     uint8_t _pin;     \/**      * @name _num_leds      * @details Number of LEDs in the strip      *\/     uint8_t _num_leds;     \/**      * @name _default_period      * @details Default update period      *\/     uint16_t _default_period;     \/**      * @name _curr_period      * @details Current update period      *\/     uint16_t _curr_period;      \/**      * @name _renderAlarm      * @param value - CO2 value      * @details Render alarm state (blinking red) if CO2 is above threshold      *\/     void _renderAlarm(float value); };<\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/controllers\/rgb.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"rgb.h\"  RGBController::RGBController(uint32_t ms, SettingsDB &amp;settingsDb)     : ControllerBase(ms),       _pin(RGB_PIN),       _num_leds(RGB_NUMPIXELS),       _leds(nullptr),       _db(&amp;settingsDb.db()),       _co2_scale(&amp;CO2Scale::getInstance()),       _blink(false) {     LOG_INFO(\"init...\");      _leds = new Adafruit_NeoPixel(_num_leds, _pin, NEO_GRB + NEO_KHZ800);     _leds-&gt;begin();     _leds-&gt;setBrightness(150);     clear();      _co2_scale-&gt;init(_db);     _default_period = this-&gt;getPeriod();     _enabled = (*_db)[kk::rgb_enabled].toBool();      LOG_INFO(\"init ok!\");     _is_initialized = true;     this-&gt;addLoop(); }  RGBController::~RGBController() {     if (_leds)     {         delete _leds;         _leds = nullptr;     } }  void RGBController::setUpdaterCb(UpdaterCallback cb) {     _u_cb = cb; }  void RGBController::exec() {     _curr_period = this-&gt;getPeriod();      if (!_is_initialized)     {         return;     }      if (!_u_cb)     {         return;     }      uint16_t co2_value = _u_cb();     if (_co2_scale-&gt;needAlarm(co2_value))     {         _renderAlarm(co2_value);         return;     }      if (_curr_period != _default_period)     {         this-&gt;updateInterval(_default_period);     }      _blink = false;     renderLevel(co2_value); }  void RGBController::toggle(bool value) {     _enabled = value;      if (!_enabled)         clear();      if (value)     {         LOG_DEBUG(\"enabled\");         return;     }      LOG_DEBUG(\"disabled\"); }  void RGBController::renderLevel(float value) {     if (!_enabled || _num_leds &lt;= 0 || !_is_initialized || _leds == nullptr)         return;      uint8_t r, g, b;     _co2_scale-&gt;getColor(value, r, g, b);      for (int i = 0; i &lt; _num_leds; i++)     {         _leds-&gt;setPixelColor(i, r, g, b);     }      _leds-&gt;show(); }  void RGBController::_renderAlarm(float value) {     if (!_enabled || _num_leds &lt;= 0 || !_is_initialized || _leds == nullptr)         return;      uint8_t r = 255;     uint8_t g = 0;     uint8_t b = 0;      if (_curr_period != SEC_1)     {         this-&gt;updateInterval(SEC_1);     }      if (_blink)     {         _blink = false;         clear();         return;     }      _blink = true;     for (int i = 0; i &lt; _num_leds; i++)     {         _leds-&gt;setPixelColor(i, r, g, b);     }      _leds-&gt;show(); }  void RGBController::clear() {     if (_leds != nullptr)     {         LOG_DEBUG(\"cleared\");         _leds-&gt;clear();         _leds-&gt;show();     } }  const char *RGBController::getType() const {     return \"rgb\"; } <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041e\u0441\u0442\u0430\u043b\u043e\u0441\u044c \u043b\u0438\u0448\u044c \u043e\u043f\u0438\u0441\u0430\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438. \u041e\u043d\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0432 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0445 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u0447\u043d\u044b\u0445 \u0444\u0430\u0439\u043b\u0430\u0445 <code>config.h<\/code> \u0438 <code>secrets.h<\/code> \u043f\u043e \u043f\u0443\u0442\u0438 <code>src\/configs<\/code>. \u041f\u0435\u0440\u0432\u044b\u0439 \u0444\u0430\u0439\u043b \u0445\u0440\u0430\u043d\u0438\u0442 \u0432\u0441\u0435 \u0434\u043e\u043f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412 \u0441\u0435\u043a\u0440\u0435\u0442\u0430\u0445 \u043c\u044b \u0445\u0440\u0430\u043d\u0438\u043c \u043a\u0440\u0435\u0434\u044b \u043a Wi-Fi \u0441\u0435\u0442\u0438 \u0438 MQTT \u0441\u0435\u0440\u0432\u0435\u0440\u0443. \u0424\u0430\u0439\u043b \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0432 .gitignore \u0438 \u043d\u0443\u0436\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438. \u0412 \u0446\u0435\u043b\u043e\u043c \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c <code>secrets.example.h<\/code>, \u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u0430\u0434\u0430\u0442\u044c \u0432 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u0438. <\/p>\n<details class=\"spoiler\">\n<summary>src\/configs\/config.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#pragma once #include &lt;Arduino.h&gt; \/\/ #include \"configs\/secrets.h\" \/\/ HINT: for development only #include \"configs\/secrets.example.h\"  #define STRINGIZER(arg) #arg #define STR_VALUE(arg) STRINGIZER(arg)  \/\/ app #define APP_NAME \"AirQualityMonitor\" #define APP_VERSION STR_VALUE(BUILD_VERSION) \/\/ Change version via project.json! #define APP_LOG_LEVEL \"DEBUG\"                 \/\/ DEBUG, ERROR, WARN, INFO \/\/ #define ENABLE_TEST \/\/ Enable mock sensor reading #define APP_DARK_THEME false \/\/ Select color theme \/\/ app  \/\/ maint \/\/ #define DB_RESET \/\/ Factory reset database #define DB_NAME \"\/settings.db\" #define PROJECT_PATH \"WildEgor\/AirQualityMonitor\/master\/project.json\" #define USER_MANUAL_URL \"https:\/\/github.com\/WildEgor\/AirQualityMonitor\/blob\/master\/docs\/en\/UserManual.md\"  \/\/ feature flags \/\/ #define WEB_PANEL_DASHBOARD \/\/ maint  \/\/ System constants (do not change) #define CCS811_ADDR 0x5A #define BME280_ADDR 0x76 \/\/ 0x77 or 0x76 #define SERIAL_SPEED 115200 #define MS_100 100 #define MS_500 500 #define SEC_1 1000 #define SEC_3 3000 #define SEC_5 5000 #define SEC_10 10000 #define SEC_30 30000 \/\/ system  \/\/ MQTT service for interaction with Yandex (see wqtt.ru) #define MQTT_ENABLED false #define MQTT_SERVER \"m8.wqtt.ru\" #define MQTT_PORT 20336 #define MQTT_DEFAULT_DEVICE_ID \"common\/aqm\" \/\/ Used as topic prefix for uniqueness #define MQTT_DEFAULT_CO2_TOPIC \"co2\" #define MQTT_DEFAULT_TVOC_TOPIC \"tvoc\" #define MQTT_DEFAULT_TEMP_TOPIC \"temp\" #define MQTT_DEFAULT_PRESSURE_TOPIC \"pressure\" #define MQTT_DEFAULT_HUMIDITY_TOPIC \"humidity\" \/\/ mqtt  \/\/ WiFi settings (see also secrets.example.h) #define WIFI_AP_NAME \"AQM_AP\" \/\/ Prefix for Wi-Fi access point with settings #define WIFI_AP_PASS \"adminadmin\" #define WIFI_CONN_RETRY_TIMEOUT 15 \/\/ seconds \/\/ wifi  \/\/ RGB settings #define RGB_ENABLED false #define RGB_PIN 19 #define RGB_NUMPIXELS 4              \/\/ Number of LEDs in the strip. Min: 1, Max: 255 #define RGB_DEFAULT_ALERT_TRHLD 1200 \/\/ CO2 threshold for red blinking \/\/ rgb settings  \/\/ hmi #define TFT_WIDTH 240 #define TFT_HEIGHT 240 #define TFT_ROTATION_0 2 \/\/ start position #define TFT_ROTATION_360 0  \/**  * NOTE: Make changes here  * To change pins: .pio\/libdeps\/mhetesp32devkit\/TFT_eSPI\/User_Setups\/Setup200_GC9A01.h  * Uncomment the correct driver: .pio\/libdeps\/mhetesp32devkit\/TFT_eSPI\/User_Setup_Select.h  *\/ #define GC9A01_DRIVER  \/\/ esp_32_live_mini #define TFT_MOSI 23 \/\/ On some display driver boards, it might be labeled as \"SDA\" etc. #define TFT_SCLK 18 #define TFT_CS 5   \/\/ Chip select control pin #define TFT_DC 16  \/\/ Data\/Command control pin #define TFT_RST 17 \/\/ Reset pin (can be connected to Arduino RESET pin) \/\/ esp_32_s2_mini \/\/ #define TFT_MOSI 9 \/\/ #define TFT_SCLK 11 \/\/ #define TFT_CS   5 \/\/ #define TFT_DC   7 \/\/ #define TFT_RST  3  #define SPI_FREQUENCY 27000000 #define SPI_READ_FREQUENCY 5000000  #define LOAD_GLCD  \/\/ Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH #define LOAD_FONT2 \/\/ Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters #define LOAD_FONT4 \/\/ Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters #define LOAD_FONT6 \/\/ Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm #define LOAD_FONT7 \/\/ Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. #define LOAD_FONT8 \/\/ Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. #define LOAD_GFXFF \/\/ FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts #define SMOOTH_FONT \/\/ hmi<\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>src\/configs\/secrets.example.h<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">\/\/ your wifi network creds. Also can be configured using web ui #define WIFI_SSID \"*****\" #define WIFI_PASS \"*****\"  \/\/ your mqtt broker creds #define MQTT_USERNAME \"*****\" #define MQTT_PASS \"*****\" <\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041e\u0441\u043d\u043e\u0432\u043d\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u043f\u043e \u043a\u043e\u0434\u0443 \u0437\u0430\u043a\u043e\u043d\u0447\u0435\u043d\u0430. \u041d\u0435 \u043e\u0431\u0440\u0430\u0449\u0430\u0439\u0442\u0435 (\u0438\u043b\u0438 \u0436\u0434\u0443 \u0432 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0445) \u043d\u0430 \u043a\u0443\u0447\u0443 \u043a\u043e\u043c\u043c\u0435\u043d\u0442\u0430\u0440\u0438\u0435\u0432 \u043f\u043e \u043a\u043e\u0434\u0443. \u042f \u043f\u0441\u0438\u0445\u0430\u043d\u0443\u043b \u0438 \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u043b \u043a\u0443\u0440\u0441\u043e\u0440 \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0442\u044c \u0434\u043e\u043a\u0443. \u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u0435\u0441\u0442\u044c \u0435\u0449\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u0441\u043f\u043e\u043c\u043e\u0433\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043a\u043b\u0430\u0441\u0441\u043e\u0432, \u043d\u043e \u043c\u043d\u0435 \u043a\u0430\u0436\u0435\u0442\u0441\u044f \u044f \u0438 \u0442\u0430\u043a \u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u043b \u043a\u0443\u0447\u0443 \u043a\u043e\u0434\u0430. \u041a\u043e\u043c\u0443 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e, \u043f\u0440\u043e\u0448\u0443 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u0442\u044c \u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439.<br \/>\u0425\u043e\u0447\u0443 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0444\u0430\u0439\u043b \u0441 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044f\u043c\u0438 \u0438 \u0434\u043e\u043f \u0441\u043a\u0440\u0438\u043f\u0442\u044b. \u0424\u0430\u0439\u043b \u0441 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0432 Platformio \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0432 \u043a\u043e\u0440\u043d\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432 <code>platformio.ini<\/code>. \u0418\u0437 \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u043e\u0433\u043e \u044d\u0442\u043e \u0441\u0435\u043a\u0446\u0438\u044f extra_scripts \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043c\u044b \u043c\u043e\u0436\u0435\u043c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c pre \u0438 post hook \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u043d\u0430 Python. \u041c\u043d\u0435 \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u043f\u043e\u043c\u043e\u0433\u043b\u043e \u044d\u0442\u043e \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0432\u0435\u0440\u0441\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438.<\/p>\n<details class=\"spoiler\">\n<summary>platformio.ini<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"vhdl\">[platformio] extra_configs = .\/**\/*platformio.ini  [env] framework = arduino lib_deps =     gyverlibs\/AutoOTA@1.2.0     gyverlibs\/GyverDB@1.3.0     gyverlibs\/WiFiConnector@1.0.4     gyverlibs\/Settings@1.3.5     gyverlibs\/GSON@1.7.0     gyverlibs\/Looper@1.1.7     gyverlibs\/GyverBME280@1.5.3     knolleary\/PubSubClient@2.8     sparkfun\/SparkFun CCS811 Arduino Library@2.0.3     bodmer\/TFT_eSPI@2.5.43     adafruit\/Adafruit NeoPixel@1.14.0 extra_scripts =  pre:scripts\/get_version.py     scripts\/copy_fw_files.py  [env:mhetesp32devkit] platform = espressif32 board = mhetesp32devkit monitor_speed = 115200 build_type = release # debug board_build.filesystem = littlefs monitor_raw = true  [env:lolin_s2_mini] platform = espressif32 board = lolin_s2_mini board_build.mcu = esp32s2 monitor_speed = 115200 build_type = release # debug board_build.filesystem = littlefs monitor_raw = true <\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>scripts\/copy_fw_files.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">Import(\"env\") import os, zipfile, shutil from pathlib import Path import json  # Get the version number from the build environment. firmware_version = os.environ.get('VERSION', \"\")  if firmware_version == \"\":     try:         with open('project.json', 'r', encoding='utf-8') as f:             project_data = json.load(f)             firmware_version = project_data.get('version', \"0.0.1\")     except (FileNotFoundError, json.JSONDecodeError, KeyError):         firmware_version = \"0.0.1\"  firmware_version = firmware_version.lstrip(\"v\") firmware_version = firmware_version.strip(\".\")  def copy_fw_files(source, target, env):     fw_file_name=str(target[0])          if fw_file_name[-3:] == \"bin\":         fw_file_name=fw_file_name[0:-3] + \"bin\"      shutil.copy(fw_file_name, \".\/bin\" + \"\/firmware.bin\")     createCommunityZipFile(source, target, env)  def createCommunityZipFile(source, target, env):     original_folder_path = \".\/bin\/\"     zip_file_path = '.\/dist\/' + \"fw_\" + firmware_version + '.zip'     createZIP(original_folder_path, zip_file_path)  def createZIP(original_folder_path, zip_file_path):     if os.path.exists(\".\/dist\") == False:         os.mkdir(\".\/dist\")     with zipfile.ZipFile(zip_file_path, 'w') as zipf:         for root, dirs, files in os.walk(original_folder_path):             for file in files:                 # Create a new path in the ZIP file                 new_path = os.path.join(\"\", os.path.relpath(os.path.join(root, file), original_folder_path))                 # Add the file to the ZIP file                 zipf.write(os.path.join(root, file), new_path)  env.AddPostAction(\"$BUILD_DIR\/${PROGNAME}.hex\", copy_fw_files) env.AddPostAction(\"$BUILD_DIR\/${PROGNAME}.bin\", copy_fw_files)<\/code><\/pre>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>scripts\/get_version.py<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"python\">Import(\"env\") import os import json  firmware_version = os.environ.get('VERSION', \"\")  if firmware_version == \"\":     try:         with open('project.json', 'r', encoding='utf-8') as f:             project_data = json.load(f)             firmware_version = project_data.get('version', \"0.0.1\")     except (FileNotFoundError, json.JSONDecodeError, KeyError):         firmware_version = \"0.0.1\"  firmware_version = firmware_version.lstrip(\"v\") firmware_version = firmware_version.strip(\".\")  print(f'Using version {firmware_version} for the build')  env.Append(CPPDEFINES=[   f'BUILD_VERSION={firmware_version}' ])  env.Replace(PROGNAME=f'{env[\"PIOENV\"]}_{firmware_version.replace(\".\", \"_\")}')<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0414\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a MQTT \u0431\u0440\u043e\u043a\u0435\u0440\u0443 \u0442\u0430\u043a \u0436\u0435 \u0431\u044b\u043b \u043d\u0430\u043f\u0438\u0441\u0430\u043d \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u0441\u043a\u0440\u0438\u043f\u0442 \u043d\u0430 \u044f\u0437\u044b\u043a\u0435 Go. \u0422\u0430\u043a \u044f \u0441\u043c\u043e\u0433 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0438 \u0432\u0441\u0435 \u043b\u0438 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c, \u043f\u043e\u043a\u0430 \u043d\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043b \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0441\u0442\u0432\u0430. \u0421\u043a\u0440\u0438\u043f\u0442 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0440\u0430\u043d\u0434\u043e\u043c\u043d\u043e\u0435 float \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0432 \u0442\u043e\u043f\u0438\u043a (\u0442\u0430\u043a \u0436\u0435 \u0441\u0430\u043c \u043f\u043e\u0434\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u043d\u0435\u0433\u043e \u0434\u043b\u044f \u0434\u0435\u0431\u0430\u0433\u0430).<\/p>\n<details class=\"spoiler\">\n<summary>scripts\/mqtt_tester\/main.go<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"go\">package main  import ( \"crypto\/rand\" \"fmt\" \"log\" \"math\/big\" \"net\/url\" \"strconv\" \"sync\" \"time\"  mqtt \"github.com\/eclipse\/paho.mqtt.golang\" )  type MQTTConnectionConfig struct { ClientID string Server   string Port     int Username string Password string }  var ( mqttConfig = MQTTConnectionConfig{ ClientID: \"mqtt_tester\", Server:   \"m8.wqtt.ru\", Port:     20336, Username: \"\", Password: \"\", }  co2Topic  = \"common\/aqm\/co2\" tvocTopic = \"common\/aqm\/tvoc\" )  func connect(clientId string, uri *url.URL) mqtt.Client { opts := createClientOptions(clientId, uri) client := mqtt.NewClient(opts) token := client.Connect()  fmt.Println(\"try establish connection to MQTT...\")  for !token.WaitTimeout(3 * time.Second) { }  if err := token.Error(); err != nil { log.Fatal(err) }  fmt.Println(\"successfully connected to MQTT!\") return client }  func createClientOptions(clientId string, uri *url.URL) *mqtt.ClientOptions { opts := mqtt.NewClientOptions() opts.AddBroker(fmt.Sprintf(\"tcp:\/\/%s\", uri.Host)) opts.SetUsername(uri.User.Username()) password, _ := uri.User.Password() opts.SetPassword(password) opts.SetClientID(clientId) return opts }  func listen(uri *url.URL, topic string) { client := connect(fmt.Sprintf(\"%s_sub\", mqttConfig.ClientID), uri)  client.Subscribe(topic, 0, func(client mqtt.Client, msg mqtt.Message) { fmt.Printf(\"* [%s] %s\\n\", msg.Topic(), string(msg.Payload())) }) }  func main() { connectionString := fmt.Sprintf(\"mqtt:\/\/%s:%s@%s:%d\", mqttConfig.Username, mqttConfig.Password, mqttConfig.Server, mqttConfig.Port, )  uri, err := url.Parse(connectionString) if err != nil { log.Fatal(err) }  topics := []string{ co2Topic, tvocTopic, }  wg := sync.WaitGroup{} for _, topic := range topics { wg.Add(1)  go func() { defer wg.Done() listen(uri, topic) }() }  wg.Wait()  client := connect(fmt.Sprintf(\"%s_pub\", mqttConfig.ClientID), uri) timer := time.NewTicker(5 * time.Second)  for { select { case &lt;-timer.C: value := GetRandFloat(400, 1500) client.Publish(co2Topic, 0, false, strconv.FormatFloat(value, 'f', 6, 64)) value = GetRandFloat(0, 250) client.Publish(tvocTopic, 0, false, strconv.FormatFloat(value, 'f', 6, 64)) default: } } }  const floatPrecision = 100  func GetRandInt(min, max int) int { nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(max+1-min))) n := nBig.Int64() return int(n) + min }  func GetRandFloat(min, max float64) float64 { minInt := int(min * floatPrecision) maxInt := int(max * floatPrecision)  return float64(GetRandInt(minInt, maxInt)) \/ floatPrecision }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u0412 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d CI\/CD \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0441\u0431\u043e\u0440\u043a\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0444\u0430\u0439\u043b\u0430 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438 \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0435\u0433\u043e \u0432 \u0430\u0440\u0442\u0438\u0444\u0430\u0439\u0442\u044b \u043f\u0430\u0439\u043f\u0430. \u041d\u0430 \u044d\u0442\u043e\u043c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u043e\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0435 \u0431\u0443\u0434\u0443. \u041e\u0441\u0442\u0430\u0432\u043b\u044e \u043b\u0438\u0448\u044c \u0444\u0430\u0439\u043b, \u043f\u043e \u043a\u043e\u0442\u043e\u0440\u043e\u043c\u0443 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f. \u0412 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0435\u0441\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u0431\u0438\u043b\u0434\u0438\u0442\u044c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u043f\u043e\u0434 \u0440\u0430\u0437\u043d\u044b\u0435 \u041c\u041a, \u043d\u043e \u044f \u043f\u043e\u043a\u0430 \u044d\u0442\u043e \u043d\u0435 \u0443\u0441\u043f\u0435\u043b \u0434\u043e\u0434\u0435\u043b\u0430\u0442\u044c. \u0425\u043e\u0442\u0435\u043b \u0431\u044b \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u0435\u0449\u0435 \u043d\u0430 \u0442\u043e, \u043a\u0430\u043a \u043f\u0430\u0440\u0441\u0438\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438. \u041e\u043d\u0430 \u0431\u0435\u0440\u0435\u0442\u0441\u044f \u0438\u0437 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0433\u043e json \u0444\u0430\u0439\u043b\u0430 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435 \u0438 \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u043a\u0440\u0438\u043f\u0442\u043e\u043c.<\/p>\n<details class=\"spoiler\">\n<summary>project.json<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"json\">{     \"name\": \"AirQualityMonitor\",     \"about\": \"\u0423\u043c\u043d\u044b\u0439 \u043c\u043e\u043d\u0438\u0442\u043e\u0440 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430\",     \"version\": \"1.0.0\",     \"notes\": \"\",     \"builds\": [       {         \"chipFamily\": \"ESP32\",         \"parts\": [           {             \"path\": \"https:\/\/raw.githubusercontent.com\/WildEgor\/AirQualityMonitor\/master\/bin\/firmware.bin\",             \"offset\": 0           }         ]       }     ]   }<\/code><\/pre>\n<\/div>\n<\/details>\n<p>\u041d\u0435 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u044f \u0432\u0435\u0437\u0434\u0435 \u0443\u043a\u0430\u0437\u0430\u043b \u043f\u043e\u043b\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u0434\u043e \u0444\u0430\u0439\u043b\u043e\u0432. \u042f \u043d\u0435 \u0437\u043d\u0430\u043b, \u043a\u0430\u043a \u043b\u0443\u0447\u0448\u0435 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442. \u041e\u0431\u044b\u0447\u043d\u043e \u043c\u043d\u043e\u0433\u0438\u0435 \u043f\u0438\u0448\u0443\u0442 \u0432\u0441\u044e \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0432 <code>main.cpp<\/code> \u0438\u043b\u0438 \u0434\u0435\u043b\u0430\u044e\u0442 \u043f\u043b\u043e\u0441\u043a\u0443\u044e \u0444\u0430\u0439\u043b\u043e\u0432\u0443\u044e \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0443. \u041c\u043d\u0435 \u044d\u0442\u043e \u0431\u044b\u043b\u043e \u043d\u0435\u043f\u0440\u0438\u0432\u044b\u0447\u043d\u043e, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u043a\u0430\u043a \u043c\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u0443\u0434\u043e\u0431\u043d\u0435\u0435 &#8212; \u0440\u0430\u0437\u0431\u0438\u043b \u043a\u043e\u0434 \u043f\u043e \u0444\u0438\u0447\u0430\u043c.<\/p>\n<h3>\u0413\u043e\u0442\u043e\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0440\u0430\u0431\u043e\u0442\u0435<\/h3>\n<p>\u041f\u0440\u0438 \u043f\u0435\u0440\u0432\u043e\u043c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 Wi-Fi \u0438 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT. \u041c\u043e\u0436\u043d\u043e \u0438 \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e &#171;\u043a\u0430\u043a \u0435\u0441\u0442\u044c&#187;. \u042f \u043d\u0435 \u0431\u0443\u0434\u0443 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u043e\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u044d\u0442\u043e\u043c, \u0442.\u043a. \u043e\u0444\u043e\u0440\u043c\u0438\u043b \u043c\u0430\u043d\u0443\u0430\u043b \u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438. \u041f\u043e\u0432\u0435\u0440\u0445\u043d\u043e\u0441\u0442\u043d\u043e \u043f\u043e\u043a\u0430\u0436\u0443 \u0447\u0442\u043e \u0432 \u0438\u0442\u043e\u0433\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c. \u041c\u043e\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0431 CO2 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043d\u043e \u043d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Wi-Fi \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (wqtt.ru). \u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0439\u0442\u0438 \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435. <\/p>\n<details class=\"spoiler\">\n<summary>\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0436\u0434\u0435\u0442, \u043f\u043e\u043a\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043c \u0432\u044b\u0445\u043e\u0434 \u0432 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/57e\/902\/861\/57e9028610598259c5badfc875116b82.jpg\" width=\"720\" height=\"1280\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/57e\/902\/861\/57e9028610598259c5badfc875116b82.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/57e\/902\/861\/57e9028610598259c5badfc875116b82.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<p>\u041f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 \u043e\u0442\u043a\u0440\u043e\u0435\u0442\u0430 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041d\u0430 \u0433\u043b\u0430\u0432\u043d\u043e\u043c \u043c\u0435\u043d\u044e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438 \u0442\u0435\u043a\u0443\u0449\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438. \u0422\u0430\u043a \u0436\u0435 \u0432 \u0432\u0438\u0434\u0435 \u0438\u043a\u043e\u043d\u043a\u0438 \u0435\u0441\u0442\u044c \u043b\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u0438\u043b\u0438 \u043d\u0435\u0442. \u041c\u0435\u043d\u044e \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043e \u043d\u0430 \u043e\u0434\u043d\u043e\u0438\u043c\u0435\u043d\u043d\u044b\u0435 \u043f\u0443\u043d\u043a\u0442\u044b<\/p>\n<details class=\"spoiler\">\n<summary>\u0413\u043b\u0430\u0432\u043d\u043e\u0435 \u043c\u0435\u043d\u044e<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/3df\/8af\/05d\/3df8af05d037508d8f9847d1aadc925a.png\" width=\"1464\" height=\"778\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/3df\/8af\/05d\/3df8af05d037508d8f9847d1aadc925a.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/3df\/8af\/05d\/3df8af05d037508d8f9847d1aadc925a.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<p>\u0427\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Wi-Fi \u043d\u0443\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u0432 \u043f\u0443\u043d\u043a\u0442 \u043c\u0435\u043d\u044e (\u043a\u044d\u043f) WiFi. \u041d\u0443\u0436\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u043c\u044f \u0441\u0435\u0442\u0438 \u0438 \u043f\u0430\u0440\u043e\u043b\u044c. \u041a\u043d\u043e\u043f\u043a\u0430 <code>Save<\/code> \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043a \u0441\u0435\u0442\u0438 \u0438\u043b\u0438 \u043f\u043e\u043a\u0430\u0436\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443.<\/p>\n<details class=\"spoiler\">\n<summary>\u041f\u0443\u043d\u043a\u0442 WiFi<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/145\/feb\/050\/145feb050644c2063c9776249bc564ce.png\" width=\"984\" height=\"426\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/145\/feb\/050\/145feb050644c2063c9776249bc564ce.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/145\/feb\/050\/145feb050644c2063c9776249bc564ce.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<p>\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a MQTT \u043c\u043e\u0436\u043d\u043e \u0442\u0430\u043a \u0436\u0435 \u043d\u0430\u0439\u0442\u0438 \u0432 \u043f\u0443\u043d\u043a\u0442\u0430\u0445 \u0433\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043c\u0435\u043d\u044e. \u041c\u043e\u0436\u043d\u043e \u0432 \u0446\u0435\u043b\u043e\u043c \u0432\u044b\u0440\u0443\u0431\u0438\u0442\u044c \u0435\u0433\u043e \u0438 \u0442\u043e\u0433\u0434\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u043f\u044b\u0442\u0430\u0442\u044c\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0438 \u043e\u0442\u0441\u044b\u043b\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0442\u043e\u043f\u0438\u043a\u0438. \u041f\u0443\u043d\u043a\u0442 <code>Device ID<\/code> \u043d\u0443\u0436\u0435\u043d, \u0447\u0442\u043e\u0431 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u0434\u043b\u044f \u0442\u043e\u043f\u0438\u043a\u043e\u0432. \u0415\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c \u0438\u043b\u0438 \u0435\u0441\u043b\u0438 \u043e\u0434\u0438\u043d \u0431\u0440\u043e\u043a\u0435\u0440 \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u0435\u0442 N \u0442\u0430\u043a\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0442\u043e \u043e\u0431\u043e\u0437\u0432\u0430\u0442\u044c \u0435\u0433\u043e \u043a\u0430\u043a-\u0442\u043e \u043b\u043e\u0433\u0438\u0447\u043d\u043e (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, home1\/hall \u0438 \u0442.\u0434.). \u0413\u043b\u0430\u0432\u043d\u043e\u0435, \u0447\u0442\u043e\u0431 \u044d\u0442\u0438 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u044b \u043d\u0435 \u043f\u0435\u0440\u0435\u0441\u0435\u043a\u0430\u043b\u0438\u0441\u044c \u0432 \u0440\u0430\u043c\u043a\u0430\u0445 \u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u041e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043e \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0431\u0440\u043e\u043a\u0435\u0440\u0430.<\/p>\n<details class=\"spoiler\">\n<summary>\u041f\u0443\u043d\u043a\u0442 MQTT<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/0f7\/1f3\/936\/0f71f3936fbffbc4b538a807444ac585.png\" width=\"1000\" height=\"826\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/0f7\/1f3\/936\/0f71f3936fbffbc4b538a807444ac585.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/0f7\/1f3\/936\/0f71f3936fbffbc4b538a807444ac585.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<p>\u0412 \u043f\u0443\u043d\u043a\u0442\u0435 <code>CO2<\/code> \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435, \u043a\u043e\u0433\u0434\u0430 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432\u0438\u0437\u0443\u0430\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0432\u044b\u0431\u0440\u0430\u0442\u044c \u0442\u0438\u043f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u043e\u0439 \u0448\u043a\u0430\u043b\u044b (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, 3-\u0445 \u0438\u043b\u0438 4-\u0445 \u0446\u0432\u0435\u0442\u043d\u0430\u044f). \u0422\u0430\u043a \u0436\u0435 \u0438\u0437 \u044d\u0442\u043e\u0433\u043e \u043f\u0443\u043d\u043a\u0442\u0430 \u043c\u0435\u043d\u044e \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0447\u0430\u0442\u044c\/\u0437\u0430\u043a\u043e\u043d\u0447\u0438\u0442\u044c \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0443. CCS811 \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0438. \u041e\u0434\u043d\u0430\u043a\u043e \u0435\u043c\u0443 \u043d\u0443\u0436\u043d\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430 \u00abburn-in\u00bb. \u042d\u0442\u043e \u043e\u0437\u043d\u0430\u0447\u0430\u0435\u0442, \u0447\u0442\u043e \u043f\u0440\u0438\u043c\u0435\u0440\u043d\u043e \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0434\u0435\u043b\u044e \u0434\u0430\u0442\u0447\u0438\u043a \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u043c. \u041e\u0434\u043d\u0430\u043a\u043e \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0439 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u0443\u0447\u0438\u0442\u044b\u0432\u0430\u0435\u0442 \u044d\u0442\u043e\u0442 \u043f\u0435\u0440\u0438\u043e\u0434 \u043f\u0440\u0438\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u0438 \u043a\u043e\u043c\u043f\u0435\u043d\u0441\u0438\u0440\u0443\u0435\u0442 \u0435\u0433\u043e. <\/p>\n<details class=\"spoiler\">\n<summary>\u041f\u0443\u043d\u043a\u0442 CO2<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e46\/eee\/644\/e46eee64413177acd4b290ba78c2ad6d.png\" width=\"1004\" height=\"552\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/e46\/eee\/644\/e46eee64413177acd4b290ba78c2ad6d.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e46\/eee\/644\/e46eee64413177acd4b290ba78c2ad6d.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<p>\u0427\u0435\u0440\u0435\u0437 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 (System) \u043f\u0443\u043d\u043a\u0442 \u043c\u0435\u043d\u044e \u043c\u043e\u0436\u043d\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c RGB \u043f\u043e\u0434\u0441\u0432\u0435\u0442\u043a\u0443, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u0442\u0435\u043c\u0443 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0435. \u0414\u043b\u044f \u043e\u0442\u043b\u0430\u0434\u043e\u0447\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0432\u044b\u0432\u0435\u0434\u0435\u043d \u043b\u043e\u0433 \u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u043b\u043e\u0433\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0421 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0439 \u043a\u043d\u043e\u043f\u043a\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u044c \u044d\u043a\u0440\u0430\u043d, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u043e\u0431\u043d\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \u0417\u0434\u0435\u0441\u044c \u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.<\/p>\n<details class=\"spoiler\">\n<summary>\u041f\u0443\u043d\u043a\u0442 System<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/210\/d34\/584\/210d345848126a275c64d7c84b30857a.png\" width=\"1028\" height=\"1304\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/210\/d34\/584\/210d345848126a275c64d7c84b30857a.png 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/210\/d34\/584\/210d345848126a275c64d7c84b30857a.png 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/31d\/ba4\/249\/31dba4249e5c7c44f42dfdfcb5df0a7d.jpg\" alt=\"\u0421\u0432\u0435\u0442\u043b\u0430\u044f \u0442\u0435\u043c\u0430\" title=\"\u0421\u0432\u0435\u0442\u043b\u0430\u044f \u0442\u0435\u043c\u0430\" width=\"720\" height=\"1280\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/31d\/ba4\/249\/31dba4249e5c7c44f42dfdfcb5df0a7d.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/31d\/ba4\/249\/31dba4249e5c7c44f42dfdfcb5df0a7d.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u0421\u0432\u0435\u0442\u043b\u0430\u044f \u0442\u0435\u043c\u0430<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/44e\/fd7\/b58\/44efd7b58213688fc4d2974cd2337f14.jpg\" alt=\"\u041f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u044b\u0439 \u044d\u043a\u0440\u0430\u043d \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432\" title=\"\u041f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u044b\u0439 \u044d\u043a\u0440\u0430\u043d \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432\" width=\"720\" height=\"1280\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/44e\/fd7\/b58\/44efd7b58213688fc4d2974cd2337f14.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/44e\/fd7\/b58\/44efd7b58213688fc4d2974cd2337f14.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u041f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u044b\u0439 \u044d\u043a\u0440\u0430\u043d \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432<\/figcaption><\/div>\n<\/figure>\n<\/div>\n<\/details>\n<p>\u0427\u0442\u043e \u043a\u0430\u0441\u0430\u0435\u0442\u0441\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439, \u0442\u043e \u0432 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432 \u0440\u0435\u043b\u0438\u0437\u0430\u0445 \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438 \u0440\u0430\u0437\u043d\u044b\u0445 \u0432\u0435\u0440\u0441\u0438\u0439. \u0412\u0435\u0440\u0441\u0438\u044f \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0438\u0437 \u0430\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0432 master \u0432\u0435\u0442\u043a\u0435. <\/p>\n<details class=\"spoiler\">\n<summary>\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/437\/924\/690\/437924690b69eade2e5b63a0136ca96b.jpg\" alt=\"\u041f\u043e \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0443 \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0435 (\u0432\u043d\u0438\u0437\u0443) \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c, \u0447\u0442\u043e \u0435\u0441\u0442\u044c \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438\" title=\"\u041f\u043e \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0443 \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0435 (\u0432\u043d\u0438\u0437\u0443) \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c, \u0447\u0442\u043e \u0435\u0441\u0442\u044c \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438\" width=\"720\" height=\"1280\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/437\/924\/690\/437924690b69eade2e5b63a0136ca96b.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/437\/924\/690\/437924690b69eade2e5b63a0136ca96b.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u041f\u043e \u0438\u043d\u0434\u0438\u043a\u0430\u0442\u043e\u0440\u0443 \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0435 (\u0432\u043d\u0438\u0437\u0443) \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c, \u0447\u0442\u043e \u0435\u0441\u0442\u044c \u043d\u043e\u0432\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/249\/179\/007\/24917900775aa5c6f794d5de2a4476cc.jpg\" alt=\"\u041f\u043e\u0441\u043b\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\" title=\"\u041f\u043e\u0441\u043b\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f\" width=\"720\" height=\"1280\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/249\/179\/007\/24917900775aa5c6f794d5de2a4476cc.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/249\/179\/007\/24917900775aa5c6f794d5de2a4476cc.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/p>\n<div><figcaption>\u041f\u043e\u0441\u043b\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f<\/figcaption><\/div>\n<\/figure>\n<p>\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435, \u0442.\u043a. \u043d\u043e\u0432\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043d\u0435 \u0431\u044b\u043b\u043e<\/p>\n<\/div>\n<\/details>\n<p>\u041f\u043e \u043a\u043d\u043e\u043f\u043a\u0435 \u0432 \u043a\u043e\u043d\u0446\u0435 \u043c\u0435\u043d\u044e \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f <a href=\"https:\/\/github.com\/WildEgor\/AirQualityMonitor\/blob\/develop\/docs\/en\/UserManual.md\" rel=\"noopener noreferrer nofollow\">\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f<\/a> \u043a\u0430\u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0435\u0433\u043e \u043a \u0423\u0414\u042f. \u0412 \u0446\u0435\u043b\u043e\u043c \u0432\u0441\u0435 \u0441\u0432\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043a \u0442\u043e\u043c\u0443, \u0447\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043d\u0430 wqtt.ru \u0438 \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0442\u043e\u043f\u0438\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0430 \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u043e\u0434\u0442\u044f\u043d\u0443\u0442\u044c \u0435\u0433\u043e \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438. \u041f\u043e\u044d\u0442\u043e\u043c\u0443 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043d\u0435 \u0431\u0443\u0434\u0443 \u043d\u0430 \u044d\u0442\u043e\u043c \u043e\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c\u0441\u044f. <\/p>\n<details class=\"spoiler\">\n<summary>\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0423\u0414\u042f<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/82e\/db1\/9f0\/82edb19f0b6cf24cec1155ab4226b7e4.jpg\" width=\"576\" height=\"1280\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/82e\/db1\/9f0\/82edb19f0b6cf24cec1155ab4226b7e4.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/82e\/db1\/9f0\/82edb19f0b6cf24cec1155ab4226b7e4.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<figure class=\"full-width\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f26\/9a7\/186\/f269a7186e8aa57e4eeb3ceffcd8a61a.jpg\" width=\"659\" height=\"1280\" sizes=\"auto, (max-width: 780px) 100vw, 50vw\" srcset=\"https:\/\/habrastorage.org\/r\/w780\/getpro\/habr\/upload_files\/f26\/9a7\/186\/f269a7186e8aa57e4eeb3ceffcd8a61a.jpg 780w,&#10;       https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/f26\/9a7\/186\/f269a7186e8aa57e4eeb3ceffcd8a61a.jpg 781w\" loading=\"lazy\" decode=\"async\"\/><\/figure>\n<\/div>\n<\/details>\n<h2>\u0418\u0442\u043e\u0433<\/h2>\n<p>\u041f\u043e\u043b\u0443\u0447\u0438\u0432\u0448\u0438\u0435\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u044b\u0448\u043b\u043e \u0437\u0430 \u0440\u0430\u043c\u043a\u0438 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0439 \u0432 \u0445\u043e\u0434\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438. \u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438:<\/p>\n<ul>\n<li>\n<p>\u0418\u0437\u043c\u0435\u0440\u044f\u0435\u0442 eCO2 \u0438 TVOC \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0442\u043e\u043f\u0438\u043a\u0438 \u0440\u0430\u0437 \u0432 30 \u0441\u0435\u043a;<\/p>\n<\/li>\n<li>\n<p>\u0418\u0437\u043c\u0435\u0440\u044f\u0435\u0442 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0443, \u0434\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0438 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u044c \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0443\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u0442\u043e\u043f\u0438\u043a\u0438 \u0440\u0430\u0437 \u0432 30 \u0441\u0435\u043a;<\/p>\n<\/li>\n<li>\n<p>\u0412\u044b\u0432\u043e\u0434\u0438\u0442 \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0439:<\/p>\n<ul>\n<li>\n<p>\u0422\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 eCO2 \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f (ppm);<\/p>\n<\/li>\n<li>\n<p>\u0410\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u0438 (\u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u043b\u0438 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0432 \u0441\u0435\u0442\u0438);<\/p>\n<\/li>\n<li>\n<p>\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Wi-Fi \u0441\u0435\u0442\u0438;<\/p>\n<\/li>\n<li>\n<p>\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a MQTT \u0431\u0440\u043e\u043a\u0435\u0440\u0443;<\/p>\n<\/li>\n<li>\n<p>\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0438;<\/p>\n<\/li>\n<li>\n<p>\u0412\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0435 \u043e\u0431 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u0438 \u043d\u043e\u0432\u043e\u0439 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>\u0421\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u043e\u0431 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0438 \u0438 \u043f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u0438\u0438 \u0443\u0440\u043e\u0432\u043d\u044f CO2 \u0432 \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434;<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430:<\/p>\n<ul>\n<li>\n<p>\u041c\u043e\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Wi-Fi \u0438 MQTT;<\/p>\n<\/li>\n<li>\n<p>\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0432\u0435\u0440\u0445\u043d\u044e\u044e \u0433\u0440\u0430\u043d\u0438\u0446\u0443 \u043f\u0440\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442\u043e\u0432\u0430\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f;<\/p>\n<\/li>\n<li>\n<p>\u041c\u043e\u0436\u043d\u043e \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0446\u0432\u0435\u0442 \u0448\u043a\u0430\u043b\u044b \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0435;<\/p>\n<\/li>\n<li>\n<p>\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u0432\u0435\u0442\u043e\u0434\u043e\u0432\u0443\u044e \u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044e;<\/p>\n<\/li>\n<li>\n<p>\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u0443\u044e \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u043c \u0442\u0435\u043c\u0443;<\/p>\n<\/li>\n<li>\n<p>\u041f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u044c \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u043d\u0430 \u0443\u0433\u043e\u043b \u043a\u0440\u0430\u0442\u043d\u044b\u0439 90;<\/p>\n<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>\u0427\u0442\u043e \u0445\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0431\u044b \u0443\u043b\u0443\u0447\u0448\u0438\u0442\u044c:<\/p>\n<ul>\n<li>\n<p>\u041f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u043a\u0443 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432. \u0415\u0441\u043b\u0438 \u0441 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c CCS811 \u0433\u0440\u0443\u0431\u043e \u0433\u043e\u0432\u043e\u0440\u044f \u0432\u0441\u0435 \u043f\u043e\u043d\u044f\u0442\u043d\u043e &#8212; \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430 \u0441\u0432\u0435\u0436\u0435\u043c \u0432\u043e\u0437\u0434\u0443\u0445\u0435, \u0442\u043e \u043d\u0435\u043f\u043e\u043d\u044f\u0442\u043d\u043e \u043a\u0430\u043a \u043a\u0430\u043b\u0438\u0431\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a BME280 \u0442\u0430\u043a \u043a\u0430\u043a \u043d\u0443\u0436\u0435\u043d \u044d\u0442\u0430\u043b\u043e\u043d \u043f\u043e \u043a\u043e\u0442\u043e\u0440\u043e\u043c\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e;<\/p>\n<\/li>\n<li>\n<p>\u0415\u0441\u0442\u044c \u0431\u0430\u0433, \u043a\u043e\u0433\u0434\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 \u043d\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u0447\u0430\u0441\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438. \u041d\u0443\u0436\u043d\u043e \u043a\u043e\u043f\u0430\u0442\u044c \u0432 \u0441\u0442\u043e\u0440\u043e\u043d\u0443 \u043b\u043e\u0433\u0438\u043a\u0438 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u043d\u043e\u0439 \u0432 <code>display.h<\/code>. \u041f\u043e\u043a\u0430 \u0447\u0442\u043e \u044d\u0442\u043e \u043b\u0435\u0447\u0438\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430;<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u043c \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u0435 \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0442\u0441\u044f \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043e\u0442\u0432\u0435\u0440\u0441\u0442\u0438\u044f \u043d\u0430 \u043a\u043e\u0440\u043f\u0443\u0441\u0435 \u0441\u043e \u0441\u0434\u0432\u0438\u0433\u043e\u043c. \u0414\u0443\u043c\u0430\u044e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0435\u0447\u0438\u0442\u044c, \u0435\u0441\u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0434\u0432\u0438\u0433 \u0435\u0449\u0435 \u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e;<\/p>\n<\/li>\n<li>\n<p>\u0421\u0430\u043c \u043a\u043e\u0440\u043f\u0443\u0441 \u043d\u0435 \u0438\u0434\u0435\u0430\u043b\u0435\u043d, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u0435\u0440\u0436\u0430\u043b\u043a\u0438 \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0438 \u0437\u0430\u0434\u043d\u0435\u0439 \u043a\u0440\u044b\u0448\u043a\u0438 \u0445\u0440\u0443\u043f\u043a\u0438\u0435. \u041a\u0440\u043e\u043c\u0435 \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0432\u043d\u0443\u0442\u0440\u044c \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u044f\u0442\u044c \u043e\u0442\u0432\u0435\u0440\u0441\u0442\u0438\u044f;<\/p>\n<\/li>\n<li>\n<p>C\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u043f\u043e\u0441\u043e\u0431, \u043a\u0430\u043a \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u0432\u043e\u0434\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0441 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043e\u043c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f <a href=\"https:\/\/github.com\/lvgl\/lvgl\" rel=\"noopener noreferrer nofollow\">LVGL<\/a> \u0438 \u0435\u0441\u0442\u044c <a href=\"https:\/\/docs.lvgl.io\/master\/details\/integration\/framework\/arduino.html\" rel=\"noopener noreferrer nofollow\">\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 TFT_eSPI<\/a>. \u0425\u043e\u0442\u0435\u043b\u043e\u0441\u044c \u0431\u044b \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u0442\u044c \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0431\u043e\u043b\u0435\u0435 \u043a\u0440\u0430\u0441\u0438\u0432\u044b\u0439 \u0434\u0438\u0437\u0430\u0439\u043d;<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0444\u0443\u043d\u043a\u0446\u0438\u044e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0447\u0435\u0440\u0435\u0437 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u044c;<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u0441\u0431\u0440\u043e\u0441\u0430 \u043a \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u0438\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c;<\/p>\n<\/li>\n<li>\n<p>\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u0430 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u044c \u0434\u0430\u0448\u0431\u043e\u0440\u0434 \u0441 \u0432\u044b\u0432\u043e\u0434 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p> \u0418 \u043c\u043d\u043e\u0433\u043e \u0435\u0449\u0435 \u0447\u0435\u0433\u043e&#8230;<\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0430\u0434\u0435\u044e\u0441\u044c, \u0447\u0442\u043e \u043c\u043e\u0439 \u043e\u043f\u044b\u0442 \u0432\u0430\u0441 \u0437\u0430\u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043e\u0432\u0430\u043b \u0438\u043b\u0438 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0434\u0430\u0436\u0435 \u0432\u0434\u043e\u0445\u043d\u043e\u0432\u0438\u043b \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0438\u043b\u0438 \u0434\u043e\u0434\u0435\u043b\u0430\u0442\u044c DIY \u043f\u0440\u043e\u0435\u043a\u0442. \u041f\u043e\u0432\u0442\u043e\u0440\u044e \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 <a href=\"https:\/\/github.com\/WildEgor\/AirQualityMonitor\/tree\/develop\" rel=\"noopener noreferrer nofollow\">\u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439<\/a>.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/930490\/\"> https:\/\/habr.com\/ru\/articles\/930490\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0414\u0430\u0432\u043d\u043e \u0447\u0435\u0441\u0430\u043b\u0438\u0441\u044c \u0440\u0443\u043a\u0438 \u0447\u0442\u043e\u0431\u044b \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u00a0\u043d\u0443\u043b\u044f. \u0414\u0440\u0443\u0437\u044c\u044f \u043f\u043e\u043f\u0440\u043e\u0441\u0438\u043b\u0438 \u0432\u00a0\u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u043e\u0434\u0430\u0440\u043a\u0430 \u043d\u0430\u00a0\u0434\u0435\u043d\u044c \u0440\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0443\u0440\u043e\u0432\u043d\u044f CO2 \u0432 \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0438 \u0434\u043e\u043c\u0430. \u0422\u0430\u043a, \u0437\u0430\u0440\u043e\u0434\u0438\u043b\u0430\u0441\u044c \u0438\u0434\u0435\u044f. <\/p>\n<h3>\u041f\u043e\u0438\u0441\u043a \u043d\u0430 \u043c\u0430\u0440\u043a\u0435\u0442\u043f\u043b\u0435\u0439\u0441\u0430\u0445<\/h3>\n<p>\u041f\u0435\u0440\u0432\u043e\u0435, \u0441\u00a0\u0447\u0435\u0433\u043e \u044f \u043d\u0430\u0447\u0430\u043b, \u044d\u0442\u043e \u043f\u043e\u0438\u0441\u043a \u0434\u0435\u0432\u0430\u0439\u0441\u0430 \u043d\u0430\u00a0\u043c\u0430\u0440\u043a\u0435\u0442\u043f\u043b\u0435\u0439\u0441\u0430\u0445. \u0414\u0430, \u043b\u0435\u043d\u044c \u0434\u0432\u0438\u0433\u0430\u0442\u0435\u043b\u044c \u043f\u0440\u043e\u0433\u0440\u0435\u0441\u0441\u0430! \u041c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u0432\u00a0\u043f\u0440\u043e\u0434\u0430\u0436\u0435 \u0433\u043e\u0442\u043e\u0432\u044b\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u044f, \u043d\u043e\u00a0\u0432\u00a0\u043d\u0438\u0445 \u0441\u043a\u043e\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043e \u043d\u0435\u0442 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u00a0\u0443\u043c\u043d\u044b\u043c \u0434\u043e\u043c\u043e\u043c (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043e\u0442\u00a0\u042f\u043d\u0434\u0435\u043a\u0441\u0430) \u0438\u043b\u0438\u00a0\u0436\u0435 \u0441\u0430\u043c\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u0431\u0440\u0430\u043d\u043e \u043d\u0430\u00a0\u043e\u0441\u043d\u043e\u0432\u0435 \u0441\u043e\u043c\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430.<\/p>\n<h3>\u041d\u0430\u0439\u0442\u0438 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435<\/h3>\n<p>\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0448\u0430\u0433\u00a0\u2014 \u044d\u0442\u043e \u0441\u0431\u043e\u0440 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0439 \u043a\u00a0\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0430\u043b\u043e\u0441\u044c \u0432\u00a0\u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u00a0\u0443\u043c\u043d\u044b\u043c \u0434\u043e\u043c\u043e\u043c \u043e\u0442\u00a0\u042f\u043d\u0434\u0435\u043a\u0441\u0430 (\u0423\u0414\u042f). \u0421\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u043c, \u0447\u0442\u043e:<\/p>\n<ol>\n<li>\n<p>\u0415\u0441\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u00a0<code>\u0423\u0414\u042f<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043d\u0430\u00a0\u0441\u0442\u043e\u043b \u0438\/\u0438\u043b\u0438\u00a0\u043f\u0440\u0438\u043a\u0440\u0435\u043f\u0438\u0442\u044c \u043d\u0430\u00a0\u0441\u0442\u0435\u043d\u0443;<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u043e\u0441\u0442\u043e\u0439 \u0438 \u0447\u0438\u0442\u0430\u0435\u043c\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0434\u043b\u044f\u00a0\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0441\u00a0\u0434\u0438\u0441\u043f\u043b\u0435\u0435\u043c \u0438\/\u0438\u043b\u0438 \u0432\u0435\u0431\u2011\u043f\u0430\u043d\u0435\u043b\u044c\u044e;<\/p>\n<\/li>\n<li>\n<p>\u041d\u0435\u00a0\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0433\u043e\u0432\u043d\u043e \u0438 \u043f\u0430\u043b\u043a\u0438 (\u043f\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438).<\/p>\n<\/li>\n<\/ol>\n<p>\u0413\u043b\u0430\u0432\u043d\u043e\u0435 \u0447\u0442\u043e\u00a0\u043c\u0435\u043d\u044f \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043e\u0432\u0430\u043b\u043e \u044d\u0442\u043e \u043a\u0430\u043a\u00a0\u0441\u0432\u044f\u0437\u0430\u0442\u044c \u0433\u043e\u0442\u043e\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u00a0<code>\u0423\u0414\u042f<\/code>. \u041a\u0430\u043a\u0438\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0442 \u0441\u043f\u043e\u0441\u043e\u0431\u044b \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u00a0<code>\u0423\u0414\u042f<\/code>?<\/p>\n<ul>\n<li>\n<p>ZigBee;<\/p>\n<\/li>\n<li>\n<p>Wi\u2011Fi Matter;<\/p>\n<\/li>\n<li>\n<p>\u0421\u0432\u043e\u0439 \u0431\u044d\u043a\u0435\u043d\u0434\u2011\u043c\u043e\u0441\u0442.<\/p>\n<\/li>\n<\/ul>\n<p><code>ZigBee<\/code> \u0438 <code>Wi-Fi Matter<\/code> \u0442\u0440\u0435\u0431\u0443\u044e\u0442 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e (\u0448\u043b\u044e\u0437), \u0430\u00a0\u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u043e\u0433\u043e \u043c\u043e\u0441\u0442\u0430 \u0435\u0449\u0435 \u0431\u043e\u043b\u0435\u0435 \u0437\u0430\u0442\u0440\u0430\u0442\u043d\u044b\u0439 \u043f\u043e\u00a0\u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043f\u043e\u0441\u043e\u0431. \u042f \u043d\u0430\u0447\u0430\u043b \u043a\u043e\u043f\u0430\u0442\u044c \u0432\u00a0\u0441\u0442\u043e\u0440\u043e\u043d\u0443 \u0434\u0440\u0443\u0433\u0438\u0445 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u043e\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0418 \u043e\u043d\u0438 \u0435\u0441\u0442\u044c. \u0421\u0430\u043c \u042f\u043d\u0434\u0435\u043a\u0441 \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 IoT \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0441\u00a0\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u00ab\u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439\u00bb (\u041e\u0424), \u0438\u0445 \u0435\u0449\u0435 \u043d\u0430\u0437\u044b\u0432\u0430\u044e\u0442 \u00ab\u043b\u044f\u043c\u0431\u0434\u044b\u00bb. \u0414\u0430\u0436\u0435 \u0435\u0441\u0442\u044c <a href=\"https:\/\/github.com\/AlexandrSurkov\/YandexCloudAirMonitor\/tree\/master\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u0438\u043c\u0435\u0440 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 ESP8266<\/a>\u00a0\u0438\u043b\u0438\u00a0\u0436\u0435 <a href=\"https:\/\/github.com\/munrexio\/yandex2mqtt\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u0438\u043c\u0435\u0440 \u043c\u043e\u0441\u0442\u0430<\/a>. \u041d\u043e\u00a0\u043a\u0430\u043a\u00a0\u043a\u043e\u0434, \u0442\u0430\u043a \u0438 \u0441\u0430\u043c \u043f\u043e\u0434\u0445\u043e\u0434 \u043c\u043d\u0435 \u043d\u0435\u00a0\u043f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u0438\u0441\u044c \u0438\u0437\u2011\u0437\u0430 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442\u00a0\u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0438 API \u042f\u043d\u0434\u0435\u043a\u0441\u0430. \u0412\u00a0\u0438\u0442\u043e\u0433\u0435 \u0432\u044b\u0431\u043e\u0440 \u043f\u0430\u043b \u043d\u0430\u00a0\u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 <code>wqtt.ru<\/code>\u00a0\u2014 \u043f\u043e\u043d\u044f\u0442\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0438 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0430\u044f \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u0441\u00a0\u043f\u0440\u0438\u043c\u0435\u0440\u0430\u043c\u0438. \u0415\u0441\u043b\u0438 \u043a\u043e\u0440\u043e\u0442\u043a\u043e, \u0442\u043e \u043d\u0430\u0448\u0435 \u0431\u0443\u0434\u0443\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0432\u00a0<code>wqtt.ru<\/code> \u043f\u043e\u00a0\u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 <code>MQTT<\/code>. \u0414\u0430\u043b\u0435\u0435 \u043c\u0435\u0436\u0434\u0443 <code>wqtt.ru<\/code> \u0438 <code>\u0423\u0414\u042f<\/code> \u0435\u0441\u0442\u044c \u0433\u043e\u0442\u043e\u0432\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438 \u043c\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u043c \u0434\u043e\u0441\u0442\u0443\u043f \u043a\u00a0\u0434\u0430\u043d\u043d\u044b\u043c \u0432\u00a0\u0432\u0438\u0434\u0435 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0414\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 <code>wqtt.ru<\/code> \u0443\u043f\u0430\u0434\u0435\u0442, \u0442\u043e \u0432\u0441\u0435\u0433\u0434\u0430 \u0435\u0441\u0442\u044c \u043f\u043b\u0430\u043d \u0411. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043d\u044f\u0442\u044c \u0441\u0432\u043e\u0439 \u0431\u0440\u043e\u043a\u0435\u0440 <code>MQTT<\/code> + <code>HomeAssistant<\/code>. \u0418\u043b\u0438\u00a0\u0436\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0430\u0448\u0431\u043e\u0440\u0434 \u043d\u0430\u00a0<code>Grafana<\/code> \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0432 <code>Prometheus<\/code> \u0432\u00a0\u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u0440\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0437\u00a0<code>MQTT<\/code> \u0431\u0440\u043e\u043a\u0435\u0440\u0430 \u0438 <code>Node\u2011RED<\/code> \u0434\u043b\u044f\u00a0\u0441\u043a\u0440\u0438\u043f\u0442\u043e\u0432. <\/p>\n<h3>\u041d\u0430\u0439\u0442\u0438 \u043a\u043e\u0440\u043f\u0443\u0441 \u0438\u043b\u0438 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0435\u0433\u043e \u0441\u0430\u043c\u043e\u043c\u0443?<\/h3>\n<p>\u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u0432\u044b\u0434\u0443\u043c\u044b\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0435, \u044f \u043d\u0430\u0447\u0430\u043b \u0438\u0441\u043a\u0430\u0442\u044c \u0443\u0436\u0435 \u0433\u043e\u0442\u043e\u0432\u044b\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u043e\u0436\u043d\u043e \u043c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u0441\u0435\u0431\u044f? \u0418 \u0434\u0430, \u0442\u0430\u043a\u043e\u0435 \u0442\u043e\u0436\u0435 \u0435\u0441\u0442\u044c. \u041f\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0443 <code>GeekMagic<\/code><strong> <\/strong>\u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 \u043c\u0430\u043b\u0435\u043d\u044c\u043a\u0438\u0435 \u043a\u043e\u0440\u043e\u0431\u043e\u0447\u043a\u0438 \u0441 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u043c \u0438 \u043c\u0438\u043a\u0440\u043e\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c (\u0432 \u043e\u0431\u044b\u0447\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 <code>ESP12F<\/code> \u0438\u043b\u0438 <code>ESP32<\/code> \u0435\u0441\u043b\u0438 pro \u0432\u0435\u0440\u0441\u0438\u044f). <\/p>\n<details class=\"spoiler\">\n<summary>GeekMagic<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"\"><\/figure>\n<\/div>\n<\/details>\n<p>\u042d\u0442\u043e \u043d\u0435\u043f\u043b\u043e\u0445\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442, \u0442.\u043a. \u043c\u0435\u043d\u0435\u0435 \u0447\u0435\u043c \u0437\u0430 1\u043a \u0440\u0443\u0431\u043b\u0435\u0439 (\u0443\u0441\u043b\u043e\u0432\u043d\u043e) \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438 \u043a\u043e\u0440\u043f\u0443\u0441, \u0438 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u0443\u0435\u043c\u044b\u0439 \u043c\u0438\u043a\u0440\u043e\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440. \u0412\u043e\u0442 \u043a\u0441\u0442\u0430\u0442\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/882370\/\" rel=\"noopener noreferrer nofollow\">\u043f\u0440\u0438\u043c\u0435\u0440<\/a> \u0441\u0442\u0430\u0442\u044c\u0438 \u0432 \u043a\u043e\u0442\u043e\u0440\u043e\u043c \u0430\u0432\u0442\u043e\u0440 \u043a\u0430\u0441\u0442\u043e\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043b \u0442\u0430\u043a\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \u041d\u043e \u043d\u0443\u0436\u043d\u043e \u0436\u0434\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0443 \u0438 \u0435\u0441\u0442\u044c \u043d\u0435\u0443\u0434\u043e\u0431\u0441\u0442\u0432\u0430 \u0441 \u043f\u0435\u0440\u0435\u043f\u0440\u043e\u0448\u0438\u0432\u043a\u043e\u0439, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043e\u0441\u0442\u0430\u0432\u0438\u043c \u044d\u0442\u043e\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0434\u043b\u044f \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430. \u0418 \u043e\u043f\u044f\u0442\u044c \u0436\u0435 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a, \u0447\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u043a\u0430\u0441\u0442\u043e\u043c\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0432\u043e\u0439 \u0431\u044d\u043a\u0435\u043d\u0434. &#171;\u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432 \u0434\u0440\u0443\u0433\u043e\u0439 \u0440\u0430\u0437, \u043d\u043e \u0442\u043e\u0447\u043d\u043e \u043d\u0435 \u0441\u0435\u0433\u043e\u0434\u043d\u044f&#187; &#8212; \u0441\u043a\u0430\u0437\u0430\u043b \u044f \u0441\u0435\u0431\u0435.<\/p>\n<p>&#171;\u0427\u0442\u043e \u0438\u0437 \u0441\u0435\u0431\u044f \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u043c\u043e\u0451 \u0431\u0443\u0434\u0443\u0449\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e?&#187;. \u041e\u043d\u043e \u0434\u043e\u043b\u0436\u043d\u043e \u0431\u044b\u0442\u044c: <\/p>\n<ul>\n<li>\n<p>\u041a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u044b\u043c \u0438 \u0438\u043c\u0435\u0442\u044c \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (\u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0438\/\u0438\u043b\u0438 \u0432\u0435\u0431-\u043f\u0430\u043d\u0435\u043b\u044c);<\/p>\n<\/li>\n<li>\n<p>\u0421 \u0431\u0435\u0441\u043f\u0440\u043e\u0432\u043e\u0434\u043d\u044b\u043c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435\u043c \u043a \u0441\u0435\u0442\u0438; <\/p>\n<\/li>\n<li>\n<p>\u0412\u044b\u0432\u043e\u0434\u0438\u0442\u044c \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u043d\u0430 \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0434\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 \u043d\u0435\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442-\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f. <\/p>\n<\/li>\n<li>\n<p>\u0411\u044b\u043b\u0430 \u0438\u0434\u0435\u044f \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0435 \u0438 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c, \u0442.\u0435. \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0430\u043a\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440, \u043d\u043e \u0442\u043e\u0433\u0434\u0430 \u0431\u044b \u044d\u0442\u043e \u0443\u0441\u043b\u043e\u0436\u043d\u0438\u043b\u043e \u043a\u0430\u043a \u0441\u0445\u0435\u043c\u0443, \u0442\u0430\u043a \u0438 \u043a\u043e\u0434. <\/p>\n<\/li>\n<\/ul>\n<p>\u041d\u0430\u0447\u043d\u0435\u043c \u0441 \u043a\u043e\u0440\u043f\u0443\u0441\u0430. \u0412\u043f\u0435\u0440\u0432\u044b\u0435 \u0440\u0435\u0448\u0438\u043b \u043f\u043e\u043f\u0440\u043e\u0431\u043e\u0432\u0430\u043b \u0441\u0434\u0435\u043b\u0430\u0442\u044c 3D \u043c\u043e\u0434\u0435\u043b\u044c \u0434\u043b\u044f \u043f\u0435\u0447\u0430\u0442\u0438. \u041d\u0430 \u043e\u0441\u0432\u043e\u0435\u043d\u0438\u0435 CAD (Autodesk Fusion 360) \u0443\u0448\u043b\u043e \u043f\u0430\u0440\u0443 \u0434\u043d\u0435\u0439, \u0430 \u0435\u0449\u0451 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0447\u0430\u0441\u043e\u0432 \u2014 \u043d\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0442\u043e\u0442\u0438\u043f\u0430 \u0434\u043b\u044f \u0441\u0431\u043e\u0440\u043a\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u0415\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u043e, \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c \u043d\u0435 \u0441 \u043f\u0435\u0440\u0432\u043e\u0439 \u043f\u043e\u043f\u044b\u0442\u043a\u0438. \u0412 \u043f\u0435\u0440\u0432\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u043d\u0435 \u0445\u0432\u0430\u0442\u0430\u043b\u043e \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u043e\u0442\u0432\u0435\u0440\u0441\u0442\u0438\u0439, \u0430 \u0432 \u0434\u0440\u0443\u0433\u043e\u0439 \u043f\u0440\u043e\u0432\u043e\u0434\u043a\u0430 \u0441 \u0442\u0440\u0443\u0434\u043e\u043c \u043f\u043e\u043c\u0435\u0449\u0430\u043b\u0430\u0441\u044c \u0432\u043d\u0443\u0442\u0440\u044c \u043a\u043e\u0440\u043f\u0443\u0441\u0430 \u0438 \u043f\u0440\u043e\u0447\u0438\u0435 \u043c\u0435\u043b\u043a\u0438\u0435 \u043d\u0435\u0434\u043e\u0447\u0435\u0442\u044b \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0410 \u0435\u0449\u0435 \u044f \u0441\u043f\u0443\u0442\u0430\u043b \u043c\u0430\u0441\u0448\u0442\u0430\u0431 \u043c\u043e\u0434\u0435\u043b\u0438 \u0438 \u043f\u0440\u0438\u0448\u043b\u043e\u0441\u044c \u043d\u0430 \u044d\u0442\u0430\u043f\u0435 \u043f\u0435\u0447\u0430\u0442\u0438 \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0442\u044c \u0440\u0430\u0437\u043c\u0435\u0440. \u0412 \u043e\u0431\u0449\u0435\u043c, \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0442\u0432\u043e\u0440\u0447\u0435\u0441\u043a\u0438\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441, \u043d\u043e \u0438 \u0441\u043b\u043e\u0436\u043d\u0430\u044f \u0438\u043d\u0436\u0435\u043d\u0435\u0440\u043d\u0430\u044f \u0437\u0430\u0434\u0430\u0447\u0430. <\/p>\n<details class=\"spoiler\">\n<summary>3D \u043c\u043e\u0434\u0435\u043b\u044c \u043a\u043e\u0440\u043f\u0443\u0441\u0430<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\">\n<div><figcaption>\u0412\u0438\u0434 \u0432 \u0441\u0431\u043e\u0440\u0435<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\">\n<div><figcaption>\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u043a\u043e\u0440\u043f\u0443\u0441 (\u0432\u0438\u0434 \u0441\u043e \u0441\u0442\u043e\u0440\u043e\u043d\u044b \u043a\u0440\u044b\u0448\u043a\u0438)<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\">\n<div><figcaption>\u041a\u0440\u044b\u0448\u043a\u0430<\/figcaption><\/div>\n<\/figure>\n<figure class=\"full-width\">\n<div><figcaption>\u041a\u0440\u044b\u0448\u043a\u0430 \u0432\u043d\u0443\u0442\u0440\u0438<\/figcaption><\/div>\n<\/figure>\n<\/div>\n<\/details>\n<h3>\u0421\u0431\u043e\u0440\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430<\/h3>\n<p>\u041a\u043e\u0440\u043f\u0443\u0441 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d \u043d\u0430 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u043e\u0434\u043d\u043e\u0433\u043e ESP-\u043f\u043e\u0434\u043e\u0431\u043d\u043e\u0433\u043e \u043c\u0438\u043a\u0440\u043e\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430, \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0443\u0440\u043e\u0432\u043d\u044f CO2 \u0438 \u0435\u0449\u0435 \u043e\u0434\u043d\u043e\u0433\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b), RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u0430. \u0415\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430 MagSafe \u0434\u0435\u0440\u0436\u0430\u0442\u0435\u043b\u044c. \u041a\u0430\u043a-\u0442\u043e \u0442\u0430\u043a \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0432\u044b\u0448\u043b\u043e, \u0447\u0442\u043e \u044f \u0443\u0433\u0430\u0434\u0430\u043b \u0441 \u0440\u0430\u0437\u043c\u0435\u0440\u0430\u043c\u0438 \u0438 \u043c\u0430\u0433\u043d\u0438\u0442\u043d\u043e\u0435 \u043a\u043e\u043b\u044c\u0446\u043e \u0438\u0434\u0435\u0430\u043b\u044c\u043d\u043e \u0441\u0435\u043b\u043e \u043d\u0430 \u0437\u0430\u0434\u043d\u044e\u044e \u043a\u0440\u044b\u0448\u043a\u0443. \u041f\u0440\u0438 \u0431\u043e\u043b\u044c\u0448\u043e\u043c \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0434\u0430\u0442\u0447\u0438\u043a (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0438 \u0434\u0430\u0432\u043b\u0435\u043d\u0438\u044f) \u0442.\u043a. \u0435\u0441\u0442\u044c \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0435 \u043c\u0435\u0441\u0442\u043e. \u0412 \u0438\u0442\u043e\u0433\u0435, \u0430\u043f\u043f\u0430\u0440\u0430\u0442\u043d\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043b\u0430\u0441\u044c \u043f\u0430\u0440\u0430\u043b\u043b\u0435\u043b\u044c\u043d\u043e \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u0440\u043f\u0443\u0441\u0430 \u0438 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442:<\/p>\n<ul>\n<li>\n<p>\u041c\u0438\u043a\u0440\u043e\u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u00a0\u2014 <code>ESP32 Live Mini<\/code>. \u0412\u044b\u0431\u043e\u0440 \u041c\u041a \u043d\u0438\u0447\u0435\u043c \u043d\u0435 \u043e\u0431\u043e\u0441\u043d\u043e\u0432\u0430\u043d, \u0440\u0430\u0437\u0432\u0435 \u0447\u0442\u043e \u043e\u043d \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u043f\u043e \u0444\u043e\u0440\u043c \u0444\u0430\u043a\u0442\u043e\u0440\u0443 \u043f\u043e\u0434 <code>ESP8266 Wemos Mini<\/code>. \u0427\u0443\u0442\u044c \u043f\u043e\u0437\u0436\u0435 \u044f \u0443\u0437\u043d\u0430\u043b \u043e \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043e\u0432\u0430\u043d\u0438\u0438 <code>ESP 32 S2<\/code>, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430\u043c \u0442\u043e\u0447\u043d\u043e \u043d\u0435 \u0445\u0443\u0436\u0435 \u0438 \u0443 \u043d\u0435\u0433\u043e \u0431\u043e\u043b\u0435\u0435 \u0441\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 usb type-c. \u0421\u0430\u043c\u0443 \u043f\u0440\u043e\u0448\u0438\u0432\u043a\u0443 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0434\u0430\u043f\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u0434\u0430\u043d\u043d\u044b\u0439 \u041c\u041a \u0442\u043e\u0436\u0435. <\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>ESP32 Live Mini<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><\/figure>\n<figure class=\"full-width\"><\/figure>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>ESP32 S2 Mini<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><\/figure>\n<figure class=\"full-width\"><\/figure>\n<\/div>\n<\/details>\n<ul>\n<li>\n<p>\u0414\u0430\u0442\u0447\u0438\u043a \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430\u00a0\u2014 <code>CCS811<\/code>. \u0414\u0430\u043d\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u0431\u044b\u043b \u0432\u044b\u0431\u0440\u0430\u043d \u043c\u0435\u0442\u043e\u0434\u043e\u043c \u0442\u044b\u043a\u0430, \u0430 \u0442\u043e\u0447\u043d\u0435\u0435 \u0432\u044b\u0431\u0440\u0430\u043d \u043f\u043e \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0443 \u0446\u0435\u043d\u0430-\u0440\u0430\u0437\u043c\u0435\u0440. \u0415\u0441\u0442\u044c \u0440\u0430\u0437\u043d\u044b\u0435 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u044b \u0442\u0438\u043f\u043e\u0432 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, <a href=\"https:\/\/www.winsen-sensor.com\/d\/files\/infrared-gas-sensor\/mh-z19b-co2-ver1_0.pdf\" rel=\"noopener noreferrer nofollow\">Winsen MH-Z19B<\/a>. \u0423 \u043c\u0435\u043d\u044f \u0435\u0441\u0442\u044c \u0442\u0430\u043a\u043e\u0439 \u0434\u0430\u0442\u0447\u0438\u043a, \u043d\u043e \u043e\u043d \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u0438 \u0440\u0430\u0431\u043e\u0442\u0435. \u0414\u0430\u0442\u0447\u0438\u043a \u0442\u0438\u043f\u0430 <a href=\"https:\/\/www.tinytronics.nl\/product_files\/004928_PMSA003_datasheet.pdf\" rel=\"noopener noreferrer nofollow\">PMS-A003<\/a> \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446\u0438\u044e \u0447\u0430\u0441\u0442\u0438\u0446 \u0432 \u0432\u043e\u0437\u0434\u0443\u0445\u0435. \u041e\u043d \u043f\u043e\u0434\u0445\u043e\u0434\u0438\u0442 \u0431\u043e\u043b\u044c\u0448\u0435 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u0432\u043d\u0435 \u0440\u0430\u0431\u043e\u0447\u0435\u0433\u043e \u043a\u0430\u0431\u0438\u043d\u0435\u0442\u0430. \u0415\u0441\u0442\u044c \u0442\u0430\u043a \u0436\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0444\u043e\u0440\u043c\u0430\u043b\u044c\u0434\u0435\u0433\u0438\u0434\u043e\u0432 \u0442\u0438\u043f\u0430 <a href=\"https:\/\/www.winsen-sensor.com\/d\/files\/PDF\/Semiconductor%20Gas%20Sensor\/MQ135%20(Ver1.4)%20-%20Manual.pdf\" rel=\"noopener noreferrer nofollow\">MQ-135<\/a>, \u043d\u043e \u043e\u043f\u044f\u0442\u044c \u043c\u0438\u043c\u043e. \u0412 \u043b\u044e\u0431\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0434\u0430\u0436\u0435 <a href=\"https:\/\/wiki.amperka.ru\/_media\/products:sensor-co2-ccs811-with-case:ccs811-datasheet.pdf\" rel=\"noopener noreferrer nofollow\">CCS811<\/a> \u0437\u0434\u0435\u0441\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u0439, \u0447\u0442\u043e\u0431\u044b \u0434\u0430\u0442\u044c \u043f\u043e\u043d\u044f\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e, \u0447\u0442\u043e \u043f\u043e\u0440\u0430 \u0431\u044b \u043f\u0440\u043e\u0432\u0435\u0442\u0440\u0438\u0442\u044c \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u0435. \u0415\u0449\u0435 \u0437\u0430\u043a\u0430\u0437\u0430\u043b <a href=\"https:\/\/cdn.manomano.com\/pim-dam-img\/350\/101975992\/fe10862b1dcf98a6c27ff4adcb81f8bfbc51cf5c.pdf\" rel=\"noopener noreferrer nofollow\">ENS160 + AHT21<\/a>, \u0447\u0442\u043e\u0431\u044b \u0441\u0440\u0430\u0432\u043d\u0438\u0442\u044c \u0438 \u0432\u044b\u0431\u0440\u0430\u0442\u044c \u043b\u0443\u0447\u0448\u0438\u0439.<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>CCS811<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><\/figure>\n<figure class=\"full-width\"><\/figure>\n<\/div>\n<\/details>\n<ul>\n<li>\n<p>\u0414\u0430\u0442\u0447\u0438\u043a \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b &#8212; <code>BME280<\/code>. \u0414\u0430\u043d\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u044f \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u043b \u043f\u043e\u0441\u043b\u0435 \u0442\u043e\u0433\u043e, \u043a\u0430\u043a \u0431\u044b\u043b \u0437\u0430\u043a\u043e\u043d\u0447\u0435\u043d \u043f\u0440\u043e\u0435\u043a\u0442. \u041f\u0440\u043e\u0441\u0442\u043e \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u0441\u0442\u0430\u043b\u043e\u0441\u044c \u043c\u0435\u0441\u0442\u043e.<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>BME280<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><\/figure>\n<figure class=\"full-width\"><\/figure>\n<\/div>\n<\/details>\n<ul>\n<li>\n<p>RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u00a0\u2014 <a href=\"https:\/\/alexgyver.ru\/ws2812_guide\/\" rel=\"noopener noreferrer nofollow\">WS2815<\/a>. \u0412 \u0446\u0435\u043b\u043e\u043c \u043f\u043e\u0434\u043e\u0439\u0434\u0435\u0442 \u0438 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 RGB \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434. \u041e\u043d \u043d\u0443\u0436\u0435\u043d \u043d\u043e\u043c\u0438\u043d\u0430\u043b\u044c\u043d\u043e, \u0442\u0430\u043a \u043a\u0430\u043a \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0434\u0438\u0441\u043f\u043b\u0435\u0439 \u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044e \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0433\u043e<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>WS2815<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"\"><\/figure>\n<\/div>\n<\/details>\n<ul>\n<li>\n<p>\u0414\u0438\u0441\u043f\u043b\u0435\u0439\u00a0\u2014 <a href=\"https:\/\/www.buydisplay.com\/download\/ic\/GC9A01A.pdf?srsltid=AfmBOoqycMC9E0HYSCuNqeNZoMFDaRp7rTbhTyRPZOm2QukEP0-opVJw\" rel=\"noopener noreferrer nofollow\">GC9A01<\/a>. \u0412\u043e\u043e\u0431\u0449\u0435, \u0438\u0434\u0435\u044f \u043a\u043e\u0440\u043f\u0443\u0441\u0430 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0432 \u0442\u043e\u0442 \u043c\u043e\u043c\u0435\u043d\u0442, \u043a\u043e\u0433\u0434\u0430 \u044f \u043d\u0430\u0448\u0435\u043b \u044d\u0442\u043e\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0439. \u041c\u043d\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c, \u0447\u0442\u043e \u043a\u0440\u0443\u0433\u043b\u0430\u044f \u0444\u043e\u0440\u043c\u0430 \u043f\u0440\u0438\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435.<\/p>\n<\/li>\n<\/ul>\n<details class=\"spoiler\">\n<summary>GC9A01<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><\/figure>\n<figure class=\"full-width\"><\/figure>\n<figure class=\"full-width\"><\/figure>\n<\/div>\n<\/details>\n<p>\u0412\u043e\u0442 \u0441\u0445\u0435\u043c\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f:<\/p>\n<details class=\"spoiler\">\n<summary>\u0421\u0445\u0435\u043c\u0430<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\"><\/figure>\n<\/div>\n<\/details>\n<details class=\"spoiler\">\n<summary>\u041a\u0430\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0432\u043d\u0443\u0442\u0440\u0438<\/summary>\n<div class=\"spoiler__content\">\n<figure class=\"full-width\">\n<div><figcaption>\u042f \u0441\u0442\u0430\u0440\u0430\u043b\u0441\u044f, \u0447\u0435\u0441\u0442\u043d\u043e<\/figcaption><\/div>\n<\/figure>\n<\/div>\n<\/details>\n<h4>\u041f\u0438\u0448\u0435\u043c \u043a\u043e\u0434<\/h4>\n<p>\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0432\u0448\u0438\u0441\u044c \u0441 \u0430\u043f\u043f\u0430\u0440\u0430\u0442\u043d\u043e\u0439 \u0447\u0430\u0441\u0442\u044c\u044e, \u0440\u0430\u0441\u043f\u0435\u0447\u0430\u0442\u0430\u0432 \u043a\u043e\u0440\u043f\u0443\u0441 \u0438 \u0441\u043e\u0431\u0440\u0430\u0432 \u044d\u0442\u043e \u0432\u0441\u0435 \u0432\u043e\u0435\u0434\u0438\u043d\u043e \u043d\u0430\u043a\u043e\u043d\u0435\u0446-\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0447\u0430\u0442\u044c \u043f\u0438\u0441\u0430\u0442\u044c \u043a\u043e\u0434. \u041f\u0438\u0441\u0430\u0442\u044c \u043a\u043e\u0434 \u043a\u0430\u0436\u0435\u0442\u0441\u044f \u0441\u0430\u043c\u0430\u044f \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u0430\u044f \u0447\u0430\u0441\u0442\u044c \u043f\u0440\u043e\u0435\u043a\u0442\u0430, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0442\u0443\u0442 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0438\u0439 \u043f\u043e\u043b\u0435\u0442 \u0444\u0430\u043d\u0442\u0430\u0437\u0438\u0438. \u041d\u043e \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0437\u0430\u0431\u044b\u0432\u0430\u0442\u044c \u043f\u0440\u043e \u043e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0438\u043d\u0430\u0447\u0435 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f. \u0414\u0430\u0436\u0435 \u0441\u0435\u0439\u0447\u0430\u0441, \u043a\u043e\u0433\u0434\u0430 \u044f \u043f\u0438\u0448\u0443 \u044d\u0442\u0443 \u0441\u0442\u0430\u0442\u044c\u044e, \u044f \u043d\u0435 \u0443\u0432\u0435\u0440\u0435\u043d \u0447\u0442\u043e \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043b \u0432\u0441\u0435 \u0447\u0442\u043e \u0445\u043e\u0442\u0435\u043b. \u0420\u0430\u043d\u044c\u0448\u0435 \u043f\u043e\u0434 Arduino \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0438\u0440\u043e\u0432\u0430\u043b\u0438 \u0432 \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u043e\u0439 IDE. \u041a\u0430\u043a\u043e\u0432\u043e \u0431\u044b\u043b\u043e \u043c\u043e\u0435 \u0443\u0434\u0438\u0432\u043b\u0435\u043d\u0438\u0435, \u0447\u0442\u043e \u043f\u043e\u044f\u0432\u0438\u043b\u0441\u044f \u0443\u0434\u043e\u0431\u043d\u044b\u0439 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a (\u0435\u0441\u043b\u0438 \u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u0442\u0430\u043a \u043d\u0430\u0437\u0432\u0430\u0442\u044c) &#8212;  Platformio. \u042f \u043d\u0430\u0447\u0430\u043b \u0440\u0430\u0437\u0431\u0438\u0440\u0430\u0442\u044c\u0441\u044f \u0432 \u043d\u0435\u043c. \u041f\u043e \u0441\u0443\u0442\u0438, \u0432\u0441\u0435 \u0447\u0442\u043e \u043c\u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u0435\u0441\u0442\u044c &#8212; \u044d\u0442\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0432 \u043f\u0440\u043e\u0435\u043a\u0442\u0435, \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0431\u0438\u043d\u0430\u0440\u043d\u0438\u043a (\u043f\u0440\u043e\u0448\u0438\u0431\u043a\u0443) \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e. \u0420\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 \u0441 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0439\u0442\u0438 <a href=\"https:\/\/github.com\/WildEgor\/AirQualityMonitor\/tree\/develop\" rel=\"noopener noreferrer nofollow\">\u0442\u0443\u0442<\/a>. \u0414\u0430\u0432\u0430\u0439\u0442\u0435 \u0440\u0430\u0437\u0431\u0435\u0440\u0435\u043c \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443. \u041d\u0430\u0447\u043d\u0435\u043c \u0441 \u0444\u0430\u0439\u043b\u0430 \u0432\u0445\u043e\u0434\u0430 \u0432 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0443 &#8212;<code>main.cpp<\/code>. \u041a\u0430\u043a \u0438 \u043b\u044e\u0431\u0430\u044f \u0434\u0440\u0443\u0433\u0430\u044f \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u0430 \u043d\u0430 Arduino \u043e\u043d\u0430 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 \u0434\u0432\u0443\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 <code>setup<\/code> \u0438 <code>loop<\/code>.<\/p>\n<details class=\"spoiler\">\n<summary>main.cpp<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"cpp\">#include \"Arduino.h\" #include &lt;Looper.h&gt;  #include \"db\/settings_db.h\" #include \"model\/co2_data.h\" #include \"configs\/config.h\" #include \"connections\/mqtt_conn.h\" #include \"connections\/wifi_conn.h\" #include \"connections\/wifi_connector_adapter.cpp\" #include \"sensors\/sensor_base.h\" #include \"sensors\/co2.h\" #include \"sensors\/tph.h\" #include \"hmi\/display.h\" #include \"hmi\/web.h\" #include \"controllers\/rgb.h\" #include \"services\/logger.h\" #include \"services\/publisher.h\" #include \"services\/ota.h\"  \/**  * @brief Initialization of all main components: logging, database, WiFi and MQTT connections, data publishing, display, RGB, and web interface  *\/ void setup() {   \/**    * @note Logging initialization    *\/   Serial.begin(SERIAL_SPEED);   SET_LOG_LEVEL(APP_LOG_LEVEL);   LOG_INFO(\"init...\");    \/**    * @note Database initialization    *\/   SettingsDB *sdb = new SettingsDB();    \/**    * @note WiFi connection initialization    *\/   WiFiAdapter *wifia = new WiFiConnectorAdapter(       WIFI_AP_NAME,       WIFI_AP_PASS,       WIFI_CONN_RETRY_TIMEOUT,       false);   WiFiConn *wifi = new WiFiConn(*sdb, *wifia);    \/**    * @note OTA firmware update initialization    *\/   OTA *ota = new OTA(*wifi);    \/**    * @note MQTT connection initialization    *\/   MQTTConn *mqtt = new MQTTConn(*sdb, *wifi);    \/**    * @note Sensors initialization    *\/   CO2Sensor *co2 = new CO2Sensor(SEC_30);   TPHSensor *tph = new TPHSensor(SEC_30);    \/**    * @note Enable test mode for sensors (data emulation)    *\/ #ifdef ENABLE_TEST   co2-&gt;enableTest();   tph-&gt;enableTest(); #endif  \/**  * @note Disable sending to MQTT to prevent broke data  *\/ #ifndef ENABLE_TEST   \/**    * @note Configure CO2 value publishing to topic [device_id]\/co2    *\/   MQTTPublisher *co2p = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_CO2_TOPIC);   co2p-&gt;setValueCb([co2]() -&gt; float                    { return co2-&gt;getCO2(); });    \/**    * @note Configure TVOC value publishing to topic [device_id]\/tvoc    *\/   MQTTPublisher *tvocp = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_TVOC_TOPIC);   tvocp-&gt;setValueCb([co2]() -&gt; float                     { return co2-&gt;getTVOC(); });    \/**    * @note Configure temperature publishing to topic [device_id]\/temp    *\/   MQTTPublisher *tempp = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_TEMP_TOPIC);   tempp-&gt;setValueCb([tph]() -&gt; float                     { return tph-&gt;getTemperature(); });    \/**    * @note Configure pressure publishing to topic [device_id]\/pressure    *\/   MQTTPublisher *pp = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_PRESSURE_TOPIC);   pp-&gt;setValueCb([tph]() -&gt; float                  { return tph-&gt;getPressure(); });    \/**    * @note Configure humidity publishing to topic [device_id]\/humidity    *\/   MQTTPublisher *hp = new MQTTPublisher(SEC_30, *mqtt, MQTT_DEFAULT_HUMIDITY_TOPIC);   hp-&gt;setValueCb([tph]() -&gt; float                  { return tph-&gt;getHumidity(); }); #endif    \/**    * @note Display initialization    *\/   Display *display = new Display(     SEC_1,      *sdb,      *co2,      *tph,      *wifi,     *mqtt,      *ota);    \/**    * @note RGB controller initialization for CO2 level visualization    *\/   RGBController *rgb = new RGBController(MS_500, *sdb);   rgb-&gt;setUpdaterCb([co2<\/code><\/pre>\n<\/div>\n<\/details>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-468375","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/468375","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=468375"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/468375\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=468375"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=468375"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=468375"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}