MicroPython: MQTT – Публикация показаний BME680 (ESP32/ESP8266)

Узнайте, как программировать платы ESP32 или ESP8266 с MicroPython для публикации показаний датчика BME680 (температура, влажность, давление и сопротивление газа) по MQTT на любую платформу, поддерживающую MQTT, или любой MQTT-клиент. В качестве примера мы будем публиковать показания датчика в Node-RED Dashboard.

MicroPython MQTT публикация показаний BME680 температура влажность давление газ ESP32 ESP8266

Рекомендуем прочитать: Что такое MQTT и как он работает

Примечание

Это руководство совместимо как с ESP32, так и с ESP8266.

Обзор проекта

На следующей диаграмме показан общий обзор проекта, который мы создадим.

BME680 ESP32 ESP8266 MicroPython MQTT обзор проекта
  • ESP запрашивает показания температуры, влажности, давления и газа с датчика BME680;

  • Показания температуры публикуются в топик esp/bme680/temperature;

  • Показания влажности публикуются в топик esp/bme680/humidity;

  • Показания давления публикуются в топик esp/bme680/pressure;

  • Показания газа публикуются в топик esp/bme680/gas;

  • Node-RED подписан на эти топики;

  • Node-RED получает показания датчика и отображает их на шкалах (gauges);

  • Вы можете получать показания на любой другой платформе, поддерживающей MQTT, и обрабатывать данные по своему усмотрению.

Предварительные требования

Прежде чем продолжить это руководство, убедитесь, что вы выполнили следующие предварительные требования.

Для этого руководства вам нужна прошивка MicroPython, установленная на ESP32 или ESP8266. Также нужна среда разработки для написания и загрузки кода. Мы рекомендуем Thonny IDE или uPyCraft IDE:

MQTT-брокер

Mosquitto MQTT брокер

Для использования MQTT вам нужен брокер. Мы будем использовать брокер Mosquitto, установленный на Raspberry Pi. Прочитайте Как установить Mosquitto Broker на Raspberry Pi.

Если вы не знакомы с MQTT, обязательно прочитайте наше вводное руководство: Что такое MQTT и как он работает.

Необходимые компоненты

Для этого руководства вам понадобятся следующие компоненты:

Библиотека umqttsimple

Для использования MQTT с ESP32/ESP8266 и MicroPython мы будем использовать библиотеку umqttsimple.py. Следуйте инструкциям для вашей IDE:

    1. Загрузка библиотеки umqttsimple с помощью uPyCraft IDE

    1. Загрузка библиотеки umqttsimple с помощью Thonny IDE

A. Загрузка библиотеки umqttsimple с помощью uPyCraft IDE

  1. Создайте новый файл в uPyCraft IDE, нажав File > New.

uPyCraft IDE создание нового файла
  1. Скопируйте следующий код библиотеки umqttsimple в этот файл.

try:
    import usocket as socket
except:
    import socket
import ustruct as struct
from ubinascii import hexlify

class MQTTException(Exception):
    pass

class MQTTClient:

    def __init__(self, client_id, server, port=0, user=None, password=None, keepalive=0,
                 ssl=False, ssl_params={}):
        if port == 0:
            port = 8883 if ssl else 1883
        self.client_id = client_id
        self.sock = None
        self.server = server
        self.port = port
        self.ssl = ssl
        self.ssl_params = ssl_params
        self.pid = 0
        self.cb = None
        self.user = user
        self.pswd = password
        self.keepalive = keepalive
        self.lw_topic = None
        self.lw_msg = None
        self.lw_qos = 0
        self.lw_retain = False

    def _send_str(self, s):
        self.sock.write(struct.pack("!H", len(s)))
        self.sock.write(s)

    def _recv_len(self):
        n = 0
        sh = 0
        while 1:
            b = self.sock.read(1)[0]
            n |= (b & 0x7f) << sh
            if not b & 0x80:
                return n
            sh += 7

    def set_callback(self, f):
        self.cb = f

    def set_last_will(self, topic, msg, retain=False, qos=0):
        assert 0 <= qos <= 2
        assert topic
        self.lw_topic = topic
        self.lw_msg = msg
        self.lw_qos = qos
        self.lw_retain = retain

    def connect(self, clean_session=True):
        self.sock = socket.socket()
        addr = socket.getaddrinfo(self.server, self.port)[0][-1]
        self.sock.connect(addr)
        if self.ssl:
            import ussl
            self.sock = ussl.wrap_socket(self.sock, **self.ssl_params)
        premsg = bytearray(b"\x10\0\0\0\0\0")
        msg = bytearray(b"\x04MQTT\x04\x02\0\0")

        sz = 10 + 2 + len(self.client_id)
        msg[6] = clean_session << 1
        if self.user is not None:
            sz += 2 + len(self.user) + 2 + len(self.pswd)
            msg[6] |= 0xC0
        if self.keepalive:
            assert self.keepalive < 65536
            msg[7] |= self.keepalive >> 8
            msg[8] |= self.keepalive & 0x00FF
        if self.lw_topic:
            sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg)
            msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3
            msg[6] |= self.lw_retain << 5

        i = 1
        while sz > 0x7f:
            premsg[i] = (sz & 0x7f) | 0x80
            sz >>= 7
            i += 1
        premsg[i] = sz

        self.sock.write(premsg, i + 2)
        self.sock.write(msg)
        self._send_str(self.client_id)
        if self.lw_topic:
            self._send_str(self.lw_topic)
            self._send_str(self.lw_msg)
        if self.user is not None:
            self._send_str(self.user)
            self._send_str(self.pswd)
        resp = self.sock.read(4)
        assert resp[0] == 0x20 and resp[1] == 0x02
        if resp[3] != 0:
            raise MQTTException(resp[3])
        return resp[2] & 1

    def disconnect(self):
        self.sock.write(b"\xe0\0")
        self.sock.close()

    def ping(self):
        self.sock.write(b"\xc0\0")

    def publish(self, topic, msg, retain=False, qos=0):
        pkt = bytearray(b"\x30\0\0\0")
        pkt[0] |= qos << 1 | retain
        sz = 2 + len(topic) + len(msg)
        if qos > 0:
            sz += 2
        assert sz < 2097152
        i = 1
        while sz > 0x7f:
            pkt[i] = (sz & 0x7f) | 0x80
            sz >>= 7
            i += 1
        pkt[i] = sz
        self.sock.write(pkt, i + 1)
        self._send_str(topic)
        if qos > 0:
            self.pid += 1
            pid = self.pid
            struct.pack_into("!H", pkt, 0, pid)
            self.sock.write(pkt, 2)
        self.sock.write(msg)
        if qos == 1:
            while 1:
                op = self.wait_msg()
                if op == 0x40:
                    sz = self.sock.read(1)
                    assert sz == b"\x02"
                    rcv_pid = self.sock.read(2)
                    rcv_pid = rcv_pid[0] << 8 | rcv_pid[1]
                    if pid == rcv_pid:
                        return
        elif qos == 2:
            assert 0

    def subscribe(self, topic, qos=0):
        assert self.cb is not None, "Subscribe callback is not set"
        pkt = bytearray(b"\x82\0\0\0")
        self.pid += 1
        struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid)
        self.sock.write(pkt)
        self._send_str(topic)
        self.sock.write(qos.to_bytes(1, "little"))
        while 1:
            op = self.wait_msg()
            if op == 0x90:
                resp = self.sock.read(4)
                assert resp[1] == pkt[2] and resp[2] == pkt[3]
                if resp[3] == 0x80:
                    raise MQTTException(resp[3])
                return

    def wait_msg(self):
        res = self.sock.read(1)
        self.sock.setblocking(True)
        if res is None:
            return None
        if res == b"":
            raise OSError(-1)
        if res == b"\xd0":
            sz = self.sock.read(1)[0]
            assert sz == 0
            return None
        op = res[0]
        if op & 0xf0 != 0x30:
            return op
        sz = self._recv_len()
        topic_len = self.sock.read(2)
        topic_len = (topic_len[0] << 8) | topic_len[1]
        topic = self.sock.read(topic_len)
        sz -= topic_len + 2
        if op & 6:
            pid = self.sock.read(2)
            pid = pid[0] << 8 | pid[1]
            sz -= 2
        msg = self.sock.read(sz)
        self.cb(topic, msg)
        if op & 6 == 2:
            pkt = bytearray(b"\x40\x02\0\0")
            struct.pack_into("!H", pkt, 2, pid)
            self.sock.write(pkt)
        elif op & 6 == 4:
            assert 0

    def check_msg(self):
        self.sock.setblocking(False)
        return self.wait_msg()
  1. Сохраните файл, нажав File > Save.

uPyCraft IDE сохранение файла
  1. Назовите файл umqttsimple.py и нажмите OK.

Импорт библиотеки umqttsimple
  1. Нажмите кнопку Download and Run.

uPyCraft IDE загрузка и запуск
  1. Файл должен появиться в устройстве в папке /. Он также был загружен на плату.

Библиотека umqttsimple установлена

B. Загрузка библиотеки umqttsimple с помощью Thonny IDE

  1. Перейдите в репозиторий GitHub Raw umqttsimple и скопируйте код библиотеки.

  2. В Thonny IDE создайте новый файл (File > New). Вставьте скопированный код.

  3. Перейдите в File > Save as… и выберите сохранение на устройство MicroPython device.

Thonny IDE сохранить на устройство Thonny IDE выбор устройства
  1. Назовите файл umqttsimple.py и нажмите OK.

Thonny IDE создание файла umqttsimple
  1. Файл должен появиться на устройстве. Библиотека загружена на плату.

Thonny IDE библиотека umqttsimple загружена

Библиотека BME680

Вам также нужно загрузить на плату библиотеку bme680.py. Следуйте тем же инструкциям, что и для библиотеки umqttsimple, но для следующего кода. Сохраните файл с именем bme680.py.

import time
import math
from micropython import const
from ubinascii import hexlify as hex
try:
  import struct
except ImportError:
  import ustruct as struct

_BME680_CHIPID = const(0x61)
_BME680_REG_CHIPID = const(0xD0)
_BME680_BME680_COEFF_ADDR1 = const(0x89)
_BME680_BME680_COEFF_ADDR2 = const(0xE1)
_BME680_BME680_RES_HEAT_0 = const(0x5A)
_BME680_BME680_GAS_WAIT_0 = const(0x64)
_BME680_REG_SOFTRESET = const(0xE0)
_BME680_REG_CTRL_GAS = const(0x71)
_BME680_REG_CTRL_HUM = const(0x72)
_BME280_REG_STATUS = const(0xF3)
_BME680_REG_CTRL_MEAS = const(0x74)
_BME680_REG_CONFIG = const(0x75)
_BME680_REG_PAGE_SELECT = const(0x73)
_BME680_REG_MEAS_STATUS = const(0x1D)
_BME680_REG_PDATA = const(0x1F)
_BME680_REG_TDATA = const(0x22)
_BME680_REG_HDATA = const(0x25)
_BME680_SAMPLERATES = (0, 1, 2, 4, 8, 16)
_BME680_FILTERSIZES = (0, 1, 3, 7, 15, 31, 63, 127)
_BME680_RUNGAS = const(0x10)

_LOOKUP_TABLE_1 = (2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0, 2147483647.0,
  2126008810.0, 2147483647.0, 2130303777.0, 2147483647.0, 2147483647.0,
  2143188679.0, 2136746228.0, 2147483647.0, 2126008810.0, 2147483647.0,
  2147483647.0)

_LOOKUP_TABLE_2 = (4096000000.0, 2048000000.0, 1024000000.0, 512000000.0, 255744255.0, 127110228.0,
  64000000.0, 32258064.0, 16016016.0, 8000000.0, 4000000.0, 2000000.0, 1000000.0,
  500000.0, 250000.0, 125000.0)

def _read24(arr):
  ret = 0.0
  for b in arr:
    ret *= 256.0
    ret += float(b & 0xFF)
  return ret

class Adafruit_BME680:
  def __init__(self, *, refresh_rate=10):
    self._write(_BME680_REG_SOFTRESET, [0xB6])
    time.sleep(0.005)
    chip_id = self._read_byte(_BME680_REG_CHIPID)
    if chip_id != _BME680_CHIPID:
      raise RuntimeError('Failed 0x%x' % chip_id)
    self._read_calibration()
    self._write(_BME680_BME680_RES_HEAT_0, [0x73])
    self._write(_BME680_BME680_GAS_WAIT_0, [0x65])
    self.sea_level_pressure = 1013.25
    self._pressure_oversample = 0b011
    self._temp_oversample = 0b100
    self._humidity_oversample = 0b010
    self._filter = 0b010
    self._adc_pres = None
    self._adc_temp = None
    self._adc_hum = None
    self._adc_gas = None
    self._gas_range = None
    self._t_fine = None
    self._last_reading = 0
    self._min_refresh_time = 1000 / refresh_rate

  @property
  def pressure_oversample(self):
    return _BME680_SAMPLERATES[self._pressure_oversample]

  @pressure_oversample.setter
  def pressure_oversample(self, sample_rate):
    if sample_rate in _BME680_SAMPLERATES:
      self._pressure_oversample = _BME680_SAMPLERATES.index(sample_rate)
    else:
      raise RuntimeError("Invalid")

  @property
  def humidity_oversample(self):
    return _BME680_SAMPLERATES[self._humidity_oversample]

  @humidity_oversample.setter
  def humidity_oversample(self, sample_rate):
    if sample_rate in _BME680_SAMPLERATES:
      self._humidity_oversample = _BME680_SAMPLERATES.index(sample_rate)
    else:
      raise RuntimeError("Invalid")

  @property
  def temperature_oversample(self):
      return _BME680_SAMPLERATES[self._temp_oversample]

  @temperature_oversample.setter
  def temperature_oversample(self, sample_rate):
    if sample_rate in _BME680_SAMPLERATES:
      self._temp_oversample = _BME680_SAMPLERATES.index(sample_rate)
    else:
      raise RuntimeError("Invalid")

  @property
  def filter_size(self):
    return _BME680_FILTERSIZES[self._filter]

  @filter_size.setter
  def filter_size(self, size):
    if size in _BME680_FILTERSIZES:
      self._filter = _BME680_FILTERSIZES[size]
    else:
      raise RuntimeError("Invalid")

  @property
  def temperature(self):
    self._perform_reading()
    calc_temp = (((self._t_fine * 5) + 128) / 256)
    return calc_temp / 100

  @property
  def pressure(self):
    self._perform_reading()
    var1 = (self._t_fine / 2) - 64000
    var2 = ((var1 / 4) * (var1 / 4)) / 2048
    var2 = (var2 * self._pressure_calibration[5]) / 4
    var2 = var2 + (var1 * self._pressure_calibration[4] * 2)
    var2 = (var2 / 4) + (self._pressure_calibration[3] * 65536)
    var1 = (((((var1 / 4) * (var1 / 4)) / 8192) *
      (self._pressure_calibration[2] * 32) / 8) +
      ((self._pressure_calibration[1] * var1) / 2))
    var1 = var1 / 262144
    var1 = ((32768 + var1) * self._pressure_calibration[0]) / 32768
    calc_pres = 1048576 - self._adc_pres
    calc_pres = (calc_pres - (var2 / 4096)) * 3125
    calc_pres = (calc_pres / var1) * 2
    var1 = (self._pressure_calibration[8] * (((calc_pres / 8) * (calc_pres / 8)) / 8192)) / 4096
    var2 = ((calc_pres / 4) * self._pressure_calibration[7]) / 8192
    var3 = (((calc_pres / 256) ** 3) * self._pressure_calibration[9]) / 131072
    calc_pres += ((var1 + var2 + var3 + (self._pressure_calibration[6] * 128)) / 16)
    return calc_pres/100

  @property
  def humidity(self):
    self._perform_reading()
    temp_scaled = ((self._t_fine * 5) + 128) / 256
    var1 = ((self._adc_hum - (self._humidity_calibration[0] * 16)) -
      ((temp_scaled * self._humidity_calibration[2]) / 200))
    var2 = (self._humidity_calibration[1] *
      (((temp_scaled * self._humidity_calibration[3]) / 100) +
       (((temp_scaled * ((temp_scaled * self._humidity_calibration[4]) / 100)) /
         64) / 100) + 16384)) / 1024
    var3 = var1 * var2
    var4 = self._humidity_calibration[5] * 128
    var4 = (var4 + ((temp_scaled * self._humidity_calibration[6]) / 100)) / 16
    var5 = ((var3 / 16384) * (var3 / 16384)) / 1024
    var6 = (var4 * var5) / 2
    calc_hum = (((var3 + var6) / 1024) * 1000) / 4096
    calc_hum /= 1000
    if calc_hum > 100:
      calc_hum = 100
    if calc_hum < 0:
      calc_hum = 0
    return calc_hum

  @property
  def altitude(self):
    pressure = self.pressure
    return 44330 * (1.0 - math.pow(pressure / self.sea_level_pressure, 0.1903))

  @property
  def gas(self):
    self._perform_reading()
    var1 = ((1340 + (5 * self._sw_err)) * (_LOOKUP_TABLE_1[self._gas_range])) / 65536
    var2 = ((self._adc_gas * 32768) - 16777216) + var1
    var3 = (_LOOKUP_TABLE_2[self._gas_range] * var1) / 512
    calc_gas_res = (var3 + (var2 / 2)) / var2
    return int(calc_gas_res)

  def _perform_reading(self):
    if (time.ticks_diff(self._last_reading, time.ticks_ms()) * time.ticks_diff(0, 1)
        < self._min_refresh_time):
      return
    self._write(_BME680_REG_CONFIG, [self._filter << 2])
    self._write(_BME680_REG_CTRL_MEAS,
      [(self._temp_oversample << 5)|(self._pressure_oversample << 2)])
    self._write(_BME680_REG_CTRL_HUM, [self._humidity_oversample])
    self._write(_BME680_REG_CTRL_GAS, [_BME680_RUNGAS])
    ctrl = self._read_byte(_BME680_REG_CTRL_MEAS)
    ctrl = (ctrl & 0xFC) | 0x01
    self._write(_BME680_REG_CTRL_MEAS, [ctrl])
    new_data = False
    while not new_data:
      data = self._read(_BME680_REG_MEAS_STATUS, 15)
      new_data = data[0] & 0x80 != 0
      time.sleep(0.005)
    self._last_reading = time.ticks_ms()
    self._adc_pres = _read24(data[2:5]) / 16
    self._adc_temp = _read24(data[5:8]) / 16
    self._adc_hum = struct.unpack('>H', bytes(data[8:10]))[0]
    self._adc_gas = int(struct.unpack('>H', bytes(data[13:15]))[0] / 64)
    self._gas_range = data[14] & 0x0F
    var1 = (self._adc_temp / 8) - (self._temp_calibration[0] * 2)
    var2 = (var1 * self._temp_calibration[1]) / 2048
    var3 = ((var1 / 2) * (var1 / 2)) / 4096
    var3 = (var3 * self._temp_calibration[2] * 16) / 16384
    self._t_fine = int(var2 + var3)

  def _read_calibration(self):
    coeff = self._read(_BME680_BME680_COEFF_ADDR1, 25)
    coeff += self._read(_BME680_BME680_COEFF_ADDR2, 16)
    coeff = list(struct.unpack('<hbBHhbBhhbbHhhBBBHbbbBbHhbb', bytes(coeff[1:39])))
    coeff = [float(i) for i in coeff]
    self._temp_calibration = [coeff[x] for x in [23, 0, 1]]
    self._pressure_calibration = [coeff[x] for x in [3, 4, 5, 7, 8, 10, 9, 12, 13, 14]]
    self._humidity_calibration = [coeff[x] for x in [17, 16, 18, 19, 20, 21, 22]]
    self._gas_calibration = [coeff[x] for x in [25, 24, 26]]
    self._humidity_calibration[1] *= 16
    self._humidity_calibration[1] += self._humidity_calibration[0] % 16
    self._humidity_calibration[0] /= 16
    self._heat_range = (self._read_byte(0x02) & 0x30) / 16
    self._heat_val = self._read_byte(0x00)
    self._sw_err = (self._read_byte(0x04) & 0xF0) / 16

  def _read_byte(self, register):
    return self._read(register, 1)[0]

  def _read(self, register, length):
    raise NotImplementedError()

  def _write(self, register, values):
    raise NotImplementedError()

class BME680_I2C(Adafruit_BME680):
  def __init__(self, i2c, address=0x77, debug=False, *, refresh_rate=10):
    self._i2c = i2c
    self._address = address
    self._debug = debug
    super().__init__(refresh_rate=refresh_rate)

  def _read(self, register, length):
    result = bytearray(length)
    self._i2c.readfrom_mem_into(self._address, register & 0xff, result)
    if self._debug:
      print("\t${:x} read ".format(register), " ".join(["{:02x}".format(i) for i in result]))
    return result

  def _write(self, register, values):
    if self._debug:
      print("\t${:x} write".format(register), " ".join(["{:02x}".format(i) for i in values]))
    for value in values:
      self._i2c.writeto_mem(self._address, register, bytearray([value & 0xFF]))
      register += 1

Если вы хотите узнать больше о датчике BME680, прочитайте наше руководство: MicroPython: BME680 с ESP32 и ESP8266.

Схемы подключения

Подключите датчик BME680 к ESP32 или ESP8266 по I2C.

ESP32 с BME680

Подключите BME680 к ESP32 следующим образом:

BME680

ESP32

SCL

GPIO 22

SDA

GPIO 21

VCC

3.3V

GND

GND

ESP32 BME680 схема подключения I2C

ESP8266 NodeMCU с BME680

Подключите BME680 к ESP8266 следующим образом:

BME680

ESP8266

SCL

GPIO 5 (D1)

SDA

GPIO 4 (D2)

VCC

3.3V

GND

GND

ESP8266 BME680 схема подключения I2C

Код – MicroPython MQTT публикация показаний BME680

Скопируйте следующий код в файл main.py вашей платы. Этот код считывает показания с датчика BME680 и публикует их по MQTT каждые 5 секунд.

import time
from umqttsimple import MQTTClient
import ubinascii
import machine
import micropython
import network
import esp
from bme680 import *
from machine import Pin, I2C

esp.osdebug(None)
import gc
gc.collect()

ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
mqtt_server = 'XXX.XXX.XXX.XXX'

client_id = ubinascii.hexlify(machine.unique_id())

topic_pub_temp = b'esp/bme680/temperature'
topic_pub_hum = b'esp/bme680/humidity'
topic_pub_pres = b'esp/bme680/pressure'
topic_pub_gas = b'esp/bme680/gas'

last_message = 0
message_interval = 5

station = network.WLAN(network.STA_IF)

station.active(True)
station.connect(ssid, password)

while station.isconnected() == False:
  pass

print('Connection successful')

# ESP32 - Pin assignment
#i2c = I2C(scl=Pin(22), sda=Pin(21))
# ESP8266 - Pin assignment
i2c = I2C(scl=Pin(5), sda=Pin(4))

bme = BME680_I2C(i2c=i2c)

def connect_mqtt():
  global client_id, mqtt_server
  client = MQTTClient(client_id, mqtt_server)
  client.connect()
  print('Connected to %s MQTT broker' % (mqtt_server))
  return client

def restart_and_reconnect():
  print('Failed to connect to MQTT broker. Reconnecting...')
  time.sleep(10)
  machine.reset()

def read_bme_sensor():
  try:
    temp = (b'{:.2f}'.format(bme.temperature))
    hum = (b'{:.2f}'.format(bme.humidity))
    pres = (b'{:.2f}'.format(bme.pressure))
    gas = (b'{:.2f}'.format(bme.gas/1000))

    return temp, hum, pres, gas
  except OSError as e:
    return('Failed to read sensor.')

try:
  client = connect_mqtt()
except OSError as e:
  restart_and_reconnect()

while True:
  try:
    if (time.time() - last_message) > message_interval:
      temp, hum, pres, gas = read_bme_sensor()
      print(temp)
      print(hum)
      print(pres)
      print(gas)
      client.publish(topic_pub_temp, temp)
      client.publish(topic_pub_hum, hum)
      client.publish(topic_pub_pres, pres)
      client.publish(topic_pub_gas, gas)

      last_message = time.time()
  except OSError as e:
    restart_and_reconnect()

Как работает код

Давайте рассмотрим, как работает этот код.

Начнём с импорта необходимых библиотек:

import time
from umqttsimple import MQTTClient
import ubinascii
import machine
import micropython
import network
import esp
from bme680 import *
from machine import Pin, I2C

Задайте ваши сетевые учётные данные в следующих переменных, чтобы ESP могла подключиться к вашему роутеру:

ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'

Введите IP-адрес вашего MQTT-брокера Raspberry Pi. Он должен быть в следующем формате:

mqtt_server = 'XXX.XXX.XXX.XXX'

Например: mqtt_server = '192.168.1.106'

Если ваш MQTT-брокер требует аутентификацию по имени пользователя и паролю, вам нужно создать объект MQTTClient следующим образом:

client = MQTTClient(client_id, mqtt_server, user=b'YOUR_USERNAME', password=b'YOUR_PASSWORD')

Создаётся уникальный идентификатор клиента MQTT. Мы используем ubinascii.hexlify() для получения уникального ID в hex-формате:

client_id = ubinascii.hexlify(machine.unique_id())

Далее создаются четыре переменные для хранения MQTT-топиков, в которые будут публиковаться показания: температура, влажность, давление и газ.

topic_pub_temp = b'esp/bme680/temperature'
topic_pub_hum = b'esp/bme680/humidity'
topic_pub_pres = b'esp/bme680/pressure'
topic_pub_gas = b'esp/bme680/gas'

Переменная message_interval определяет интервал между каждой публикацией в секундах. Здесь мы установили 5 секунд. При необходимости измените это значение.

last_message = 0
message_interval = 5

Далее код подключается к вашей сети Wi-Fi:

station = network.WLAN(network.STA_IF)
station.active(True)
station.connect(ssid, password)

while station.isconnected() == False:
  pass

print('Connection successful')

Создайте объект I2C с назначением пинов для вашей платы. Если вы используете ESP32, раскомментируйте первую строку и закомментируйте вторую. Если используете ESP8266, оставьте как есть.

# ESP32 - Pin assignment
#i2c = I2C(scl=Pin(22), sda=Pin(21))
# ESP8266 - Pin assignment
i2c = I2C(scl=Pin(5), sda=Pin(4))

Затем создайте объект BME680_I2C с именем bme:

bme = BME680_I2C(i2c=i2c)

Функция connect_mqtt() создаёт клиент MQTT и подключается к вашему брокеру:

def connect_mqtt():
  global client_id, mqtt_server
  client = MQTTClient(client_id, mqtt_server)
  client.connect()
  print('Connected to %s MQTT broker' % (mqtt_server))
  return client

Функция restart_and_reconnect() сбрасывает плату ESP, если не удаётся подключиться к MQTT-брокеру:

def restart_and_reconnect():
  print('Failed to connect to MQTT broker. Reconnecting...')
  time.sleep(10)
  machine.reset()

Функция read_bme_sensor() считывает показания температуры, влажности, давления и газа с датчика BME680 и возвращает их:

def read_bme_sensor():
  try:
    temp = (b'{:.2f}'.format(bme.temperature))
    hum = (b'{:.2f}'.format(bme.humidity))
    pres = (b'{:.2f}'.format(bme.pressure))
    gas = (b'{:.2f}'.format(bme.gas/1000))

    return temp, hum, pres, gas
  except OSError as e:
    return('Failed to read sensor.')

В основном цикле while мы проверяем, прошёл ли интервал, и публикуем показания по MQTT. Каждое показание публикуется в свой отдельный топик:

while True:
  try:
    if (time.time() - last_message) > message_interval:
      temp, hum, pres, gas = read_bme_sensor()
      print(temp)
      print(hum)
      print(pres)
      print(gas)
      client.publish(topic_pub_temp, temp)
      client.publish(topic_pub_hum, hum)
      client.publish(topic_pub_pres, pres)
      client.publish(topic_pub_gas, gas)

      last_message = time.time()
  except OSError as e:
    restart_and_reconnect()

В случае ошибки (например, если MQTT-брокер недоступен) вызывается функция restart_and_reconnect(), которая перезагружает плату через 10 секунд.

Подготовка Node-RED Dashboard

Для визуализации показаний датчика мы будем использовать Node-RED Dashboard. Если у вас нет Node-RED, сначала установите его на Raspberry Pi.

Настройка потока (Flow)

  1. Перетащите на рабочую область четыре узла mqtt in (по одному для каждого топика) и соответствующие узлы отображения – два узла gauge (для температуры и влажности) и два узла text (для давления и газа).

Node-RED перетаскивание узлов BME680
  1. Дважды щёлкните по первому узлу mqtt in и настройте его. Добавьте новый сервер mqtt-broker, указав IP-адрес localhost и порт 1883. Укажите топик esp/bme680/temperature.

Node-RED настройка узла MQTT BME680
  1. Настройте узел gauge для температуры. Установите диапазон от 0 до 40 и единицу измерения.

Node-RED настройка узла gauge BME680
  1. Повторите аналогичные настройки для остальных узлов: влажность (gauge, диапазон 30–100), давление (text, hPa) и газ (text, KOhm). Соедините узлы между собой.

Node-RED все узлы соединены BME680
  1. Нажмите кнопку Deploy в правом верхнем углу, чтобы применить изменения.

Node-RED кнопка Deploy

Импорт потока Node-RED

Вместо ручной настройки вы также можете импортировать готовый поток. Перейдите в Menu > Import и вставьте следующий JSON:

[{"id":"3b7f947c.9759ec","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/temperature","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":470,"y":2640,"wires":[["b87b21c3.96672"]]},{"id":"b87b21c3.96672","type":"ui_gauge","z":"254c9c97.f85b34","name":"","group":"37de8fe8.46846","order":2,"width":0,"height":0,"gtype":"gage","title":"Temperature","label":"\u00baC","format":"{{value}}","min":0,"max":"40","colors":["#00b500","#f7df09","#ca3838"],"seg1":"","seg2":"","x":690,"y":2640,"wires":[]},{"id":"f92248f4.545778","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/humidity","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":460,"y":2700,"wires":[["4114a401.5ac69c"]]},{"id":"4114a401.5ac69c","type":"ui_gauge","z":"254c9c97.f85b34","name":"","group":"37de8fe8.46846","order":2,"width":0,"height":0,"gtype":"gage","title":"Humidity","label":"%","format":"{{value}}","min":"30","max":"100","colors":["#53a4e6","#1d78a9","#4e38c9"],"seg1":"","seg2":"","x":680,"y":2700,"wires":[]},{"id":"ad51f895.2c2848","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/pressure","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":460,"y":2760,"wires":[["3a95123b.66405e"]]},{"id":"c074e688.198b78","type":"mqtt in","z":"254c9c97.f85b34","name":"","topic":"esp/bme680/gas","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":440,"y":2820,"wires":[["d3539c06.00a17"]]},{"id":"3a95123b.66405e","type":"ui_text","z":"254c9c97.f85b34","group":"37de8fe8.46846","order":2,"width":0,"height":0,"name":"","label":"Pressure","format":"{{msg.payload}} hPa","layout":"row-spread","x":680,"y":2760,"wires":[]},{"id":"d3539c06.00a17","type":"ui_text","z":"254c9c97.f85b34","group":"37de8fe8.46846","order":3,"width":0,"height":0,"name":"","label":"Gas","format":"{{msg.payload}} KOhm","layout":"row-spread","x":670,"y":2820,"wires":[]},{"id":"8db3fac0.99dd48","type":"mqtt-broker","z":"","name":"","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"37de8fe8.46846","type":"ui_group","z":"","name":"BME680","tab":"53b8c8f9.cfbe48","order":1,"disp":true,"width":"6","collapse":false},{"id":"53b8c8f9.cfbe48","type":"ui_tab","z":"","name":"Home","icon":"dashboard","order":5,"disabled":false,"hidden":false}]

Демонстрация

После загрузки всех файлов (umqttsimple.py, bme680.py и main.py) на плату ESP, откройте браузер и перейдите по адресу:

http://<ip-адрес-raspberry-pi>:1880/ui

Вы увидите панель мониторинга Node-RED с показаниями температуры, влажности, давления и газа, которые обновляются каждые 5 секунд.

ESP32 ESP8266 Node-RED BME680 температура влажность давление газ панель мониторинга

Связанные руководства

Заключение

В этом руководстве вы узнали, как публиковать показания датчика BME680 (температура, влажность, давление, газ) по протоколу MQTT с помощью MicroPython на ESP32 или ESP8266. Node-RED использовался для визуализации данных на панели мониторинга. Этот подход применим для различных сценариев домашней автоматизации и мониторинга окружающей среды.