Буквально в прошлом году, в деревне на берегу моря, где я люблю отдыхать летом, появился худо-бедно рабочий Интернет! Экий прогресс! …и план по установке там метеодатчика стал совершенно реальным – ни там, ни в округе на сотни километров нет ни одного! Обидно же.
В этой статье представлена облегченная, переработанная и дополненная в соответствии с задачей версия этого наброска. Основные отличия:
На некоторых фотографиях фигурируют элементы метеорологической будки, сборка которой описана в этой статье.
В отличие от предыдущей версии, где использовалась плата NodeMCU с ESP-12E, тут был выбран более компактный контроллер с возможностью использования внешней антенны.
Первым делом необходимо переключить плату с использования керамической SMD-антенны на внешнюю с разъемом IPX. Делается это поворотом на 90° нулевого резистора:
Увы, получилось не очень красиво очень не красиво, ибо я поленился лезть за феном, а паяльником, даже с тонки жалом, такую фигульку передвинуть та еще затея…
D1 mini | BME680 |
---|---|
D1 | SCL |
D2 | SDA |
3V3 | VCC |
GND | GND |
GND | SDO |
D1 mini | LED |
D0 | Anode3) |
GND | Cathode |
D1 mini | Power |
USB VCC | +5V |
USB GND | GND |
D1 mini reset line4) | |
D7 | RST |
По какой-то неведомой причине, длинная линия i²C, от метра и больше, с платой CJMCU-680 не работает! Причем наличие или отсутствие подтягивающих резисторов на 4,7 кОм5) никакой роли не играет. Линия меньше 30-и сантиметров работает стабильно.
О подготовке Arduino IDE – добавлении плат, установке библиотек и настройке BSEC – смотрите по ссылке.
Тут все аналогично, за исключением:
ArduinoJson
и Uptime Library
.
Пример JSON-строки с настройками, которые можно передавать в любом порядке и составе в топик barometer_XXXXXX/cmnd/SETTINGS
:
{ "server": "192.168.0.254", "port": 8888, "login": "NEW_LOGIN", "password": "NEW_PASSWORD", "frequency": 5, // В секундах! "offset": -1.1, "constant": 0.008 }
Команда для получения текущих настроек системы в топик barometer_XXXXXX/tele/STATE
:
{ "print": true }
Команда для перезагрузки системы:
{ "reboot": true }
Далее данные можно архивировать и передавать в систему умного дома, например Apple Home через Homebridge, или в Интернет – на Народный Мониторинг.
Код в достаточной мере прокомментирован, поэтому добавить нечего. Если есть вопросы, вэлком в комментарии.
// Written by Nikolay Soloshin (nikolay@soloshin.su) for WeMos D1 mini Pro & BME680 @ 2022.06 /* Подключение библиотек */ #include <ESP8266WiFi.h> // v3.0.2 (ESP8266 Community) #include <PubSubClient.h> // v2.8.0 #include <ArduinoJson.h> // v6.19.4 #include <EEPROM.h> // v3.0.2 (ESP8266 Community) #include <bsec.h> // v1.6.1480 #include <uptime.h> // v1.0.0 /* Инвертирование значений*/ #define iHIGH LOW #define iLOW HIGH /* Пользовательские настройки */ // Настройки Wi-Fi const char* wifi_ssid = "SSID"; const char* wifi_pwd = "PASSWORD"; // Настройки MQTT const char* mqtt_clientid = "bme680_XXXXXX"; // Настройки топиков MQTT const char* mqtt_topic[] { // Местами менять нельзя! "barometer_XXXXXX/tele/SENSOR", // [0] Телеметрия датчиков "barometer_XXXXXX/tele/LWT", // [1] Last Will and Testament, передает "Online" при подключении MQTT "barometer_XXXXXX/tele/STATE", // [2] Передает текстовые сообщения статусов "barometer_XXXXXX/cmnd/SETTINGS" // [3] Принимает настройки в формате JSON }; // Значения MQTT по умолчанию (default_*), если EEPROM не инициализирован #define default_mqtt_server "192.168.0.1" #define default_mqtt_port 1883 #define default_mqtt_login "LOGIN" #define default_mqtt_pwd "PASSWORD" // Частота публикации данных в MQTT по умолчанию (часы * минуты * секунды * 1000) #define default_mqtt_pub_freq 5 * 60 * 1000 // Каждые 2 часа = 2 * 60 * 60 * 1000 // Корректировка показаний температуры в °C (по умолчанию) #define default_bme_t_offset 0 // К примеру, -1.1, 0 или 1.15 // Константа для перевода Па в мм.р.с. (по умолчанию) #define default_bme_mmHg 0.0075 // Назначение GPIO #define bme_led D0 // Внешний светодиод #define reset_pin D7 // Пин перезагрузки #define status_led LED_BUILTIN // Выключение светодиодов через некоторое время const boolean leds_off = true; // false - не выключать long leds_off_delay = 2 * 60 * 1000; // Через 2 минуты = 2 * 60 * 1000 /* Прочие нужные дела */ const char* sk_version = "v1.3.1 (sk4)"; // Версия скетча // Структура данных для хранения настроек struct Settings { // Переменные MQTT char mqtt_server[20]; int mqtt_port; char mqtt_login[20]; char mqtt_pwd[20]; // Прочие переменные int mqtt_pub_freq; float bme_t_offset; float bme_mmHg; } settings; // Настройки EEPROM #define memory_size 256 // Итоговый размер EEPROM с запасом #define flag_position 0 #define struct_position flag_position + 1 #define init_flag 222 // Флаг инициализации, 0-254 на выбор // Создание объектов WiFiClient espClient; PubSubClient MQTT( espClient ); Bsec iaqSensor; // Объявление служебных переменных String bme_output; long lastReconnectAttempt; unsigned long MQTT_timer; boolean leds_off_status; /* Настройка при запуске */ void setup() { // Стартовые значения при запуске digitalWrite( bme_led, LOW ); digitalWrite( reset_pin, iLOW ); // Инициализация GPIO pinMode( bme_led, OUTPUT ); pinMode( status_led, OUTPUT ); pinMode( reset_pin, OUTPUT ); // Инициализация COM Serial.begin( 115200 ); delay( 3000 ); Serial.println( F( "Hi guys and girls!" ) ); // Инициализация EEPROM и чтение данных EEPROM.begin( memory_size ); if ( EEPROM.read( flag_position ) != init_flag ) { Serial.println( F( "EEPROM not initialized!" ) ); EEPROM.write( flag_position, init_flag ); Serial.println( F( "Writing default settings to EEPROM..." ) ); settings = ( Settings ) { default_mqtt_server, default_mqtt_port, default_mqtt_login, default_mqtt_pwd, default_mqtt_pub_freq, default_bme_t_offset, default_bme_mmHg }; EEPROM.put( struct_position, settings ); EEPROM.commit(); delay( 500 ); } Serial.println( F( "Reading settings from EEPROM..." ) ); EEPROM.get( struct_position, settings ); EEPROM.end(); printSettings( "serial" ); // Инициализация Wi-Fi WiFi.begin( wifi_ssid, wifi_pwd ); Serial.print( F( "Connecting to Wi-Fi" ) ); while ( WiFi.status() != WL_CONNECTED ) { digitalWrite( status_led, iHIGH ); delay( 250 ); Serial.print( "." ); digitalWrite( status_led, iLOW ); delay( 250 ); } Serial.print( F( "\nConnected to Wi-Fi: " ) ); Serial.print( WiFi.SSID() ); Serial.print( F( " (IP: " ) ); Serial.print( WiFi.localIP() ); Serial.println( F( ")!" ) ); digitalWrite( status_led, iLOW ); // Инициализация MQTT MQTT.setServer( settings.mqtt_server, settings.mqtt_port ); MQTT.setCallback( MQTTcallback ); while ( !MQTT.connected() ) { Serial.println( F( "Connecting to MQTT..." ) ); if ( MQTT.connect( mqtt_clientid, settings.mqtt_login, settings.mqtt_pwd ) ) { Serial.println( F( "MQTT connected!" ) ); MQTT.publish( mqtt_topic[2], "MQTT connected!" ); digitalWrite( status_led, iHIGH ); } else { digitalWrite( status_led, iHIGH ); delay( 500 ); Serial.print( F( "Failed with state " ) ); Serial.println( MQTT.state() ); digitalWrite( status_led, iLOW ); delay( 500 ); } } // Подписка и публикация начальных значений MQTT.subscribe( mqtt_topic[3] ); MQTT.publish( mqtt_topic[1], "Online", true ); printSettings( "mqtt" ); // Инициализация BSEC Wire.begin(); iaqSensor.begin( BME680_I2C_ADDR_PRIMARY, Wire ); bme_output = "BSEC library version " + String( iaqSensor.version.major ) + "." + String( iaqSensor.version.minor ) + "." + String( iaqSensor.version.major_bugfix ) + "." + String( iaqSensor.version.minor_bugfix ); checkIaqSensorStatus(); bsec_virtual_sensor_t sensorList[5] = { BSEC_OUTPUT_RAW_TEMPERATURE, BSEC_OUTPUT_RAW_PRESSURE, BSEC_OUTPUT_RAW_HUMIDITY, // BSEC_OUTPUT_RAW_GAS, // BSEC_OUTPUT_IAQ, // BSEC_OUTPUT_STATIC_IAQ, // BSEC_OUTPUT_CO2_EQUIVALENT, // BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, }; iaqSensor.updateSubscription( sensorList, 5, BSEC_SAMPLE_RATE_LP ); checkIaqSensorStatus(); digitalWrite( bme_led, HIGH ); bme_output += ", Sketch version " + String( sk_version ) + ". Loading complete! IP: " + WiFi.localIP().toString() + "."; Serial.println( bme_output ); MQTT.publish( mqtt_topic[2], bme_output.c_str() ); } /* Основной цикл */ void loop() { unsigned long now = millis(); // Чтение данных с датчика BME680 if ( iaqSensor.run() ) { // Есть новые данные (раз в 3 секунды) ; } else { checkIaqSensorStatus(); } // Проверка соединения с брокером MQTT if ( !MQTT.connected() ) { if ( now - lastReconnectAttempt > 5000 ) { lastReconnectAttempt = now; MQTTErrorLED(); Serial.println( F( "MQTT disconnected!" ) ); if ( MQTTreconnect() ) { lastReconnectAttempt = 0; digitalWrite( status_led, iHIGH ); Serial.println( F( "MQTT reconnected!" ) ); MQTT.publish( mqtt_topic[2], "MQTT reconnected!" ); // Сброс состояния выключения LED leds_off_status = false; leds_off_delay = now + 120000; } } // Публикация телеметрии датчика } else { if ( now - MQTT_timer > settings.mqtt_pub_freq ) { MQTT_timer = now; // Вычисление времени работы uptime::calculateUptime(); char local_uptime[21]; sprintf( local_uptime, "%dd%02d:%02d:%02d", uptime::getDays(), uptime::getHours(), uptime::getMinutes(), uptime::getSeconds() ); // Сборка строки JSON StaticJsonDocument<128> message; // https://arduinojson.org/v6/assistant/ message["pressure_st"] = round( ( iaqSensor.pressure * settings.bme_mmHg ) * 100 ) / 100.0; message["temperature"] = round( ( iaqSensor.temperature + settings.bme_t_offset ) * 100 ) / 100.0; message["humidity"] = round( ( iaqSensor.humidity ) * 100 ) / 100.0; message["pressure_raw"] = iaqSensor.pressure; message["uptime"] = local_uptime; message["rssi"] = WiFi.RSSI(); char local_buffer[256]; size_t n = serializeJson( message, local_buffer ); Serial.println( F( "Publishing MQTT message..." ) ); MQTT.publish( mqtt_topic[0], local_buffer, n ); } } // Выключение светодиодов if ( leds_off && now > leds_off_delay && !leds_off_status ) { Serial.println( F( "Turning off LEDs by timeout!" ) ); MQTT.publish( mqtt_topic[2], "Turning off LEDs by timeout!" ); digitalWrite( status_led, iLOW ); digitalWrite( bme_led, LOW ); leds_off_status = true; } // Циклы библиотеки MQTT MQTT.loop(); } // Функция MQTT для приема сообщений void MQTTcallback( char* topic, byte* payload, unsigned int length ) { boolean local_flag = false; StaticJsonDocument<192> message; // https://arduinojson.org/v6/assistant/ DeserializationError local_error = deserializeJson( message, payload, length ); if ( local_error ) { Serial.print( F( "deserializeJson() failed: " ) ); Serial.println( local_error.f_str() ); MQTT.publish( mqtt_topic[2], local_error.c_str() ); return; } if ( message.containsKey( "server" ) ) { strlcpy( settings.mqtt_server, message["server"].as<const char*>(), sizeof( settings.mqtt_server ) ); local_flag = true; } if ( message.containsKey( "port" ) ) { settings.mqtt_port = message["port"]; local_flag = true; } if ( message.containsKey( "login" ) ) { strlcpy( settings.mqtt_login, message["login"].as<const char*>(), sizeof( settings.mqtt_login ) ); local_flag = true; } if ( message.containsKey( "password" ) ) { strlcpy( settings.mqtt_pwd, message["password"].as<const char*>(), sizeof( settings.mqtt_pwd ) ); local_flag = true; } if ( message.containsKey( "frequency" ) ) { // В секундах! settings.mqtt_pub_freq = int( message["frequency"] ) * 1000; local_flag = true; } if ( message.containsKey( "offset" ) ) { settings.bme_t_offset = message["offset"]; local_flag = true; } if ( message.containsKey( "constant" ) ) { settings.bme_mmHg = message["constant"]; local_flag = true; } if ( local_flag ) { EEPROM.begin( memory_size ); EEPROM.put( struct_position, settings ); EEPROM.end(); // Фактическая запись данных Serial.println( F( "Saving new settings in EEPROM!" ) ); MQTT.publish( mqtt_topic[2], "Saving new settings in EEPROM!" ); } if ( message.containsKey( "print" ) && message["print"] ) printSettings( "both" ); if ( message.containsKey( "reboot" ) && message["reboot"] ) { Serial.println( F( "Rebooting system by MQTT message!" ) ); MQTT.publish( mqtt_topic[2], "Rebooting system by MQTT message!" ); delay( 500 ); digitalWrite( reset_pin, iHIGH ); // https://www.instructables.com/two-ways-to-reset-arduino-in-software/ } } /* Функция проверки BSEC и датчика BME680 */ void checkIaqSensorStatus( void ) { if ( iaqSensor.status != BSEC_OK ) { if ( iaqSensor.status < BSEC_OK ) { bme_output = "BSEC error code: " + String( iaqSensor.status ); Serial.println( bme_output ); MQTT.publish( mqtt_topic[2], bme_output.c_str() ); for (;;) BMEcriticalError(); // Перезагрузка в случае сбоя } else { bme_output = "BSEC warning code: " + String( iaqSensor.status ); Serial.println( bme_output ); MQTT.publish( mqtt_topic[2], bme_output.c_str() ); } } if ( iaqSensor.bme680Status != BME680_OK ) { if ( iaqSensor.bme680Status < BME680_OK ) { bme_output = "BME680 error code: " + String( iaqSensor.bme680Status ); Serial.println( bme_output ); MQTT.publish( mqtt_topic[2], bme_output.c_str() ); for (;;) BMEcriticalError(); // Перезагрузка в случае сбоя } else { bme_output = "BME680 warning code: " + String( iaqSensor.bme680Status ); Serial.println( bme_output ); MQTT.publish( mqtt_topic[2], bme_output.c_str() ); } } } /* Функция сигнализации о сбое BME или BSEC */ void BMEcriticalError( void ) { static boolean local_flag[2]; static unsigned long local_timer[2]; unsigned long local_now = millis(); // Светодиодная индикация if ( local_now - local_timer[0] >= 100 ) { local_timer[0] = local_now; digitalWrite( bme_led, local_flag[0] ); local_flag[0] = !local_flag[0]; } // Взвод таймера перезагрузки if ( !local_flag[1] ) { local_flag[1] = !local_flag[1]; local_timer[1] = local_now; } // Уведомление и перезагрузка по таймеру (5 мин) if ( local_now - local_timer[1] >= 300000 ) { Serial.println( F( "Rebooting by timer due to a critical BME error..." ) ); MQTT.publish( mqtt_topic[2], "Rebooting by timer due to a critical BME error..." ); delay( 500 ); digitalWrite( reset_pin, iHIGH ); } // Циклы библиотеки MQTT MQTT.loop(); } /* Функция сигнализации о сбое Wi-Fi или MQTT */ void MQTTErrorLED ( void ) { boolean level = true; for ( byte i = 0; i < 6; i++ ) { digitalWrite( status_led, level ); level = !level; delay( 100 ); } } /* Функция переподключения MQTT */ boolean MQTTreconnect( void ) { if ( MQTT.connect( mqtt_clientid, settings.mqtt_login, settings.mqtt_pwd ) ) { MQTT.subscribe( mqtt_topic[3] ); MQTT.publish( mqtt_topic[1], "Online", true ); } return MQTT.connected(); } /* Вывод настроек в COM и MQTT */ void printSettings( String variant ) { StaticJsonDocument<192> message; // https://arduinojson.org/v6/assistant/ message["server"] = settings.mqtt_server; message["port"] = settings.mqtt_port; message["login"] = settings.mqtt_login; message["password"] = settings.mqtt_pwd; message["frequency"] = settings.mqtt_pub_freq; message["offset"] = round( ( settings.bme_t_offset ) * 100 ) / 100.0; message["constant"] = settings.bme_mmHg; char buffer[256]; size_t n = serializeJson( message, buffer ); if ( variant == "serial" || variant == "both" ) { Serial.print( F( "Current settings: " ) ); Serial.println( buffer ); } if (variant == "mqtt" || variant == "both" ) MQTT.publish( mqtt_topic[2], buffer, n ); } // With love from Vladivostok.
Обсуждение