Инструменты пользователя

Инструменты сайта


iot:firmware:arduino:wemos:d1:mini_pro:bosch:bme680

Скетч Arduino для датчика BME680 (v1.x)

Буквально в прошлом году, в деревне на берегу моря, где я люблю отдыхать летом, появился худо-бедно рабочий Интернет! Экий прогресс! m( …и план по установке там метеодатчика стал совершенно реальным – ни там, ни в округе на сотни километров нет ни одного! Обидно же. LOL

В этой статье представлена облегченная, переработанная и дополненная в соответствии с задачей версия этого наброска. Основные отличия:

  • убран код управления дополнительным реле,
  • реализовано изменение настроек через MQTT,
  • добавлено сохранение настроек в EEPROM,
  • переработан код функции проверки BSEC,
  • добавлен сброс системы через MQTT-сообщение.

Экран Стивенсона через 2 года.

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

Комплектующие

  1. WeMos D1 mini Pro1);
  2. CJMCU-680 (Bosch BME680);
  3. пигтейл IPX и антенна;
  4. светодиод ~3.3 VDC2);
  5. PSU 220 VAC / 5 VDC.

Крупнее!

В отличие от предыдущей версии, где использовалась плата 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 – смотрите по ссылке.

Тут все аналогично, за исключением:

  • для платы, как на фото, нужно в меню «Инструменты» → «Плата: […]» → «ESP8266 Boards (x.x.x)» выбрать «LOLIN(WEMOS) D1 mini Lite»;
  • необходимо дополнительно установить через «Менеджер библиотек» библиотеки ArduinoJson и Uptime Library.

Функционал

  1. Подключение к сети Wi-Fi и брокеру MQTT, автоматическое переподключение к брокеру после разрыва соединения6),
  2. прием показаний с датчика BME680, их минимально-необходимая обработка (конвертирование давления в мм.р.с., коррекция температуры, округление данных) и передача по протоколу MQTT в виде JSON-строки,
  3. инициализация EEPROM, ввод настроек по умолчанию, прием7) новых настроек8) по MQTT и сохранение их в энергонезависимую память,
  4. отправка диагностических и информационных текстовых сообщений в COM-порт и по MQTT,
  5. автоматическая перезагрузка устройства при сбое в BSEC или BME, перезагрузка по сообщению MQTT9),
  6. автоматически отключающаяся по таймеру светодиодная индикация работы BME,
  7. расчет времени бесперебойной работы устройства и передача вместе с данными датчика, передача силы сигнала Wi-Fi.

Команды

Пример 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, или в Интернет – на Народный Мониторинг.

Набросок

Код в достаточной мере прокомментирован, поэтому добавить нечего. Если есть вопросы, вэлком в комментарии.

barometer-v1.3.1.ino
// 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.

Дисклеймер

  • Использование материалов данной базы знаний разрешено на условиях лицензии, указанной внизу каждой страницы! При использовании материалов активная гиперссылка на соответствующую страницу данной базы знаний обязательна!
  • Автор не несет и не может нести какую либо ответственность за последствия использования материалов, размещенных в данной базе знаний. Все материалы предоставляются по принципу «как есть». Используйте их исключительно на свой страх и риск.
  • Все высказывания, мысли или идеи автора, размещенные в материалах данной базе знаний, являются исключительно его личным субъективным мнением и могут не совпадать с мнением читателей!
  • При размещении ссылок в данной базе знаний на интернет-страницы третьих лиц автор не несет ответственности за их техническую функциональность (особенно отсутствие вирусов) и содержание! При обнаружении таких ссылок, можно и желательно сообщить о них в комментариях к соответствующей статье.
1)
Скорее всего клон.
2)
Или любой другой с дополнительным сопротивлением.
3)
+, длинная нога.
4)
Желтая перемычка на фото ниже.
5)
Да и других номиналов тоже…
6)
Wi-Fi и так автоматические переподключается.
7) , 9)
Формат строки см. ниже.
8)
Не всех, только часть.

Обсуждение

Ваш комментарий:
Y B A W Y D C A U H E T T M N C
 
Последнее изменение: 2024/09/30 23:51 — Николай Солошин