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

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

ESP32 ESP8266 BME280 MQTT MicroPython

Примечание

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

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

Вот как работает проект:

  • ESP запрашивает показания температуры, влажности и давления от датчика BME280;

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

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

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

  • Node-RED подписывается на эти топики;

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

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

Обзор проекта BME280 ESP32 ESP8266 MicroPython MQTT

Необходимые условия

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

Прошивка MicroPython

На вашей плате ESP32 или ESP8266 должна быть установлена прошивка MicroPython. Вам также понадобится IDE для написания и загрузки кода на плату. Мы рекомендуем использовать Thonny IDE или uPyCraft IDE:

MQTT-брокер

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

Mosquitto Broker

Для ознакомления с основами MQTT прочитайте статью Что такое MQTT и как он работает.

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

  • ESP32 или ESP8266

  • Датчик BME280

  • Raspberry Pi с установленным Mosquitto

  • MicroSD-карта (16 ГБ Class10)

  • Блок питания Raspberry Pi (5В 2.5A)

  • Соединительные провода

  • Макетная плата

Библиотека umqttsimple

Для использования MQTT с ESP32/ESP8266 и MicroPython необходима библиотека umqttsimple. Скопируйте следующий код в файл с именем umqttsimple.py и загрузите его на плату.

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

    # Wait for a single incoming MQTT message and process it.
    # Subscribed messages are delivered to a callback previously
    # set by .set_callback() method. Other (internal) MQTT
    # messages processed internally.
    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":  # PINGRESP
            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

    # Checks whether a pending message from server is available.
    # If not, returns immediately with None. Otherwise, does
    # the same processing as wait_msg.
    def check_msg(self):
        self.sock.setblocking(False)
        return self.wait_msg()

Загрузка через uPyCraft IDE

  1. Нажмите кнопку New File для создания нового файла.

  2. Скопируйте код библиотеки umqttsimple в новый файл.

  3. Сохраните файл, нажав Save.

  4. Назовите файл umqttsimple.py и нажмите OK.

  5. Нажмите Download and Run.

  6. Файл будет сохранен в папке device под именем umqttsimple.py.

После загрузки вы можете импортировать библиотеку в своем коде для использования ее функций.

Загрузка через Thonny IDE

  1. Скопируйте код библиотеки в новый файл.

  2. Перейдите в File > Save as….

  3. Выберите сохранение на MicroPython device.

  4. Назовите файл umqttsimple.py и нажмите OK.

Библиотека будет загружена на вашу плату. Чтобы убедиться в успешной загрузке, перейдите в File > Save as… и выберите MicroPython device – файл должен появиться в списке.

После загрузки вы можете импортировать библиотеку в своем коде для использования ее функций.

Библиотека BME280 для MicroPython

Библиотека BME280 не входит в стандартную библиотеку MicroPython по умолчанию. Загрузите следующую библиотеку на вашу плату ESP32/ESP8266 (сохраните ее как BME280.py).

from machine import I2C
import time

# BME280 default address.
BME280_I2CADDR = 0x76

# Operating Modes
BME280_OSAMPLE_1 = 1
BME280_OSAMPLE_2 = 2
BME280_OSAMPLE_4 = 3
BME280_OSAMPLE_8 = 4
BME280_OSAMPLE_16 = 5

# BME280 Registers

BME280_REGISTER_DIG_T1 = 0x88  # Trimming parameter registers
BME280_REGISTER_DIG_T2 = 0x8A
BME280_REGISTER_DIG_T3 = 0x8C

BME280_REGISTER_DIG_P1 = 0x8E
BME280_REGISTER_DIG_P2 = 0x90
BME280_REGISTER_DIG_P3 = 0x92
BME280_REGISTER_DIG_P4 = 0x94
BME280_REGISTER_DIG_P5 = 0x96
BME280_REGISTER_DIG_P6 = 0x98
BME280_REGISTER_DIG_P7 = 0x9A
BME280_REGISTER_DIG_P8 = 0x9C
BME280_REGISTER_DIG_P9 = 0x9E

BME280_REGISTER_DIG_H1 = 0xA1
BME280_REGISTER_DIG_H2 = 0xE1
BME280_REGISTER_DIG_H3 = 0xE3
BME280_REGISTER_DIG_H4 = 0xE4
BME280_REGISTER_DIG_H5 = 0xE5
BME280_REGISTER_DIG_H6 = 0xE6
BME280_REGISTER_DIG_H7 = 0xE7

BME280_REGISTER_CHIPID = 0xD0
BME280_REGISTER_VERSION = 0xD1
BME280_REGISTER_SOFTRESET = 0xE0

BME280_REGISTER_CONTROL_HUM = 0xF2
BME280_REGISTER_CONTROL = 0xF4
BME280_REGISTER_CONFIG = 0xF5
BME280_REGISTER_PRESSURE_DATA = 0xF7
BME280_REGISTER_TEMP_DATA = 0xFA
BME280_REGISTER_HUMIDITY_DATA = 0xFD


class Device:
  """Class for communicating with an I2C device.

  Allows reading and writing 8-bit, 16-bit, and byte array values to
  registers on the device."""

  def __init__(self, address, i2c):
    """Create an instance of the I2C device at the specified address using
    the specified I2C interface object."""
    self._address = address
    self._i2c = i2c

  def writeRaw8(self, value):
    """Write an 8-bit value on the bus (without register)."""
    value = value & 0xFF
    self._i2c.writeto(self._address, value)

  def write8(self, register, value):
    """Write an 8-bit value to the specified register."""
    b=bytearray(1)
    b[0]=value & 0xFF
    self._i2c.writeto_mem(self._address, register, b)

  def write16(self, register, value):
    """Write a 16-bit value to the specified register."""
    value = value & 0xFFFF
    b=bytearray(2)
    b[0]= value & 0xFF
    b[1]= (value>>8) & 0xFF
    self.i2c.writeto_mem(self._address, register, value)

  def readRaw8(self):
    """Read an 8-bit value on the bus (without register)."""
    return int.from_bytes(self._i2c.readfrom(self._address, 1),'little') & 0xFF

  def readU8(self, register):
    """Read an unsigned byte from the specified register."""
    return int.from_bytes(
        self._i2c.readfrom_mem(self._address, register, 1),'little') & 0xFF

  def readS8(self, register):
    """Read a signed byte from the specified register."""
    result = self.readU8(register)
    if result > 127:
      result -= 256
    return result

  def readU16(self, register, little_endian=True):
    """Read an unsigned 16-bit value from the specified register, with the
    specified endianness (default little endian, or least significant byte
    first)."""
    result = int.from_bytes(
        self._i2c.readfrom_mem(self._address, register, 2),'little') & 0xFFFF
    if not little_endian:
      result = ((result << 8) & 0xFF00) + (result >> 8)
    return result

  def readS16(self, register, little_endian=True):
    """Read a signed 16-bit value from the specified register, with the
    specified endianness (default little endian, or least significant byte
    first)."""
    result = self.readU16(register, little_endian)
    if result > 32767:
      result -= 65536
    return result

  def readU16LE(self, register):
    """Read an unsigned 16-bit value from the specified register, in little
    endian byte order."""
    return self.readU16(register, little_endian=True)

  def readU16BE(self, register):
    """Read an unsigned 16-bit value from the specified register, in big
    endian byte order."""
    return self.readU16(register, little_endian=False)

  def readS16LE(self, register):
    """Read a signed 16-bit value from the specified register, in little
    endian byte order."""
    return self.readS16(register, little_endian=True)

  def readS16BE(self, register):
    """Read a signed 16-bit value from the specified register, in big
    endian byte order."""
    return self.readS16(register, little_endian=False)


class BME280:
  def __init__(self, mode=BME280_OSAMPLE_1, address=BME280_I2CADDR, i2c=None,
               **kwargs):
    # Check that mode is valid.
    if mode not in [BME280_OSAMPLE_1, BME280_OSAMPLE_2, BME280_OSAMPLE_4,
                    BME280_OSAMPLE_8, BME280_OSAMPLE_16]:
        raise ValueError(
            'Unexpected mode value {0}. Set mode to one of '
            'BME280_ULTRALOWPOWER, BME280_STANDARD, BME280_HIGHRES, or '
            'BME280_ULTRAHIGHRES'.format(mode))
    self._mode = mode
    # Create I2C device.
    if i2c is None:
      raise ValueError('An I2C object is required.')
    self._device = Device(address, i2c)
    # Load calibration values.
    self._load_calibration()
    self._device.write8(BME280_REGISTER_CONTROL, 0x3F)
    self.t_fine = 0

  def _load_calibration(self):

    self.dig_T1 = self._device.readU16LE(BME280_REGISTER_DIG_T1)
    self.dig_T2 = self._device.readS16LE(BME280_REGISTER_DIG_T2)
    self.dig_T3 = self._device.readS16LE(BME280_REGISTER_DIG_T3)

    self.dig_P1 = self._device.readU16LE(BME280_REGISTER_DIG_P1)
    self.dig_P2 = self._device.readS16LE(BME280_REGISTER_DIG_P2)
    self.dig_P3 = self._device.readS16LE(BME280_REGISTER_DIG_P3)
    self.dig_P4 = self._device.readS16LE(BME280_REGISTER_DIG_P4)
    self.dig_P5 = self._device.readS16LE(BME280_REGISTER_DIG_P5)
    self.dig_P6 = self._device.readS16LE(BME280_REGISTER_DIG_P6)
    self.dig_P7 = self._device.readS16LE(BME280_REGISTER_DIG_P7)
    self.dig_P8 = self._device.readS16LE(BME280_REGISTER_DIG_P8)
    self.dig_P9 = self._device.readS16LE(BME280_REGISTER_DIG_P9)

    self.dig_H1 = self._device.readU8(BME280_REGISTER_DIG_H1)
    self.dig_H2 = self._device.readS16LE(BME280_REGISTER_DIG_H2)
    self.dig_H3 = self._device.readU8(BME280_REGISTER_DIG_H3)
    self.dig_H6 = self._device.readS8(BME280_REGISTER_DIG_H7)

    h4 = self._device.readS8(BME280_REGISTER_DIG_H4)
    h4 = (h4 << 24) >> 20
    self.dig_H4 = h4 | (self._device.readU8(BME280_REGISTER_DIG_H5) & 0x0F)

    h5 = self._device.readS8(BME280_REGISTER_DIG_H6)
    h5 = (h5 << 24) >> 20
    self.dig_H5 = h5 | (
        self._device.readU8(BME280_REGISTER_DIG_H5) >> 4 & 0x0F)

  def read_raw_temp(self):
    """Reads the raw (uncompensated) temperature from the sensor."""
    meas = self._mode
    self._device.write8(BME280_REGISTER_CONTROL_HUM, meas)
    meas = self._mode << 5 | self._mode << 2 | 1
    self._device.write8(BME280_REGISTER_CONTROL, meas)
    sleep_time = 1250 + 2300 * (1 << self._mode)

    sleep_time = sleep_time + 2300 * (1 << self._mode) + 575
    sleep_time = sleep_time + 2300 * (1 << self._mode) + 575
    time.sleep_us(sleep_time)  # Wait the required time
    msb = self._device.readU8(BME280_REGISTER_TEMP_DATA)
    lsb = self._device.readU8(BME280_REGISTER_TEMP_DATA + 1)
    xlsb = self._device.readU8(BME280_REGISTER_TEMP_DATA + 2)
    raw = ((msb << 16) | (lsb << 8) | xlsb) >> 4
    return raw

  def read_raw_pressure(self):
    """Reads the raw (uncompensated) pressure level from the sensor."""
    """Assumes that the temperature has already been read """
    """i.e. that enough delay has been provided"""
    msb = self._device.readU8(BME280_REGISTER_PRESSURE_DATA)
    lsb = self._device.readU8(BME280_REGISTER_PRESSURE_DATA + 1)
    xlsb = self._device.readU8(BME280_REGISTER_PRESSURE_DATA + 2)
    raw = ((msb << 16) | (lsb << 8) | xlsb) >> 4
    return raw

  def read_raw_humidity(self):
    """Assumes that the temperature has already been read """
    """i.e. that enough delay has been provided"""
    msb = self._device.readU8(BME280_REGISTER_HUMIDITY_DATA)
    lsb = self._device.readU8(BME280_REGISTER_HUMIDITY_DATA + 1)
    raw = (msb << 8) | lsb
    return raw

  def read_temperature(self):
    """Get the compensated temperature in 0.01 of a degree celsius."""
    adc = self.read_raw_temp()
    var1 = ((adc >> 3) - (self.dig_T1 << 1)) * (self.dig_T2 >> 11)
    var2 = ((
        (((adc >> 4) - self.dig_T1) * ((adc >> 4) - self.dig_T1)) >> 12) *
        self.dig_T3) >> 14
    self.t_fine = var1 + var2
    return (self.t_fine * 5 + 128) >> 8

  def read_pressure(self):
    """Gets the compensated pressure in Pascals."""
    adc = self.read_raw_pressure()
    var1 = self.t_fine - 128000
    var2 = var1 * var1 * self.dig_P6
    var2 = var2 + ((var1 * self.dig_P5) << 17)
    var2 = var2 + (self.dig_P4 << 35)
    var1 = (((var1 * var1 * self.dig_P3) >> 8) +
            ((var1 * self.dig_P2) >> 12))
    var1 = (((1 << 47) + var1) * self.dig_P1) >> 33
    if var1 == 0:
      return 0
    p = 1048576 - adc
    p = (((p << 31) - var2) * 3125) // var1
    var1 = (self.dig_P9 * (p >> 13) * (p >> 13)) >> 25
    var2 = (self.dig_P8 * p) >> 19
    return ((p + var1 + var2) >> 8) + (self.dig_P7 << 4)

  def read_humidity(self):
    adc = self.read_raw_humidity()
    # print 'Raw humidity = {0:d}'.format (adc)
    h = self.t_fine - 76800
    h = (((((adc << 14) - (self.dig_H4 << 20) - (self.dig_H5 * h)) +
         16384) >> 15) * (((((((h * self.dig_H6) >> 10) * (((h *
                          self.dig_H3) >> 11) + 32768)) >> 10) + 2097152) *
                          self.dig_H2 + 8192) >> 14))
    h = h - (((((h >> 15) * (h >> 15)) >> 7) * self.dig_H1) >> 4)
    h = 0 if h < 0 else h
    h = 419430400 if h > 419430400 else h
    return h >> 12

  @property
  def temperature(self):
    "Return the temperature in degrees."
    t = self.read_temperature()
    ti = t // 100
    td = t - ti * 100
    return "{}.{:02d}C".format(ti, td)

  @property
  def pressure(self):
    "Return the temperature in hPa."
    p = self.read_pressure() // 256
    pi = p // 100
    pd = p - pi * 100
    return "{}.{:02d}hPa".format(pi, pd)

  @property
  def humidity(self):
    "Return the humidity in percent."
    h = self.read_humidity()
    hi = h // 1024
    hd = h * 100 // 1024 - hi * 100
    return "{}.{:02d}%".format(hi, hd)

Загрузите эту библиотеку на плату аналогично загрузке umqttsimple (через uPyCraft IDE или Thonny IDE), сохранив файл под именем BME280.py.

Подробнее о работе с BME280 на MicroPython читайте в статье: MicroPython: BME280 с ESP32 и ESP8266.

Схема подключения: ESP32 с BME280

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

  • BME280 VCC – ESP32 3.3V

  • BME280 GND – ESP32 GND

  • BME280 SCL – ESP32 GPIO 22

  • BME280 SDA – ESP32 GPIO 21

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

Схема подключения: ESP8266 NodeMCU с BME280

Для ESP8266 NodeMCU используйте следующую схему:

  • BME280 VCC – ESP8266 3.3V

  • BME280 GND – ESP8266 GND

  • BME280 SCL – ESP8266 GPIO 5 (D1)

  • BME280 SDA – ESP8266 GPIO 4 (D2)

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

Код

После загрузки библиотек на ESP32 или ESP8266, скопируйте следующий код в файл main.py. Он публикует температуру, влажность и давление в топики esp/bme280/temperature, esp/bme280/humidity и esp/bme280/pressure каждые 5 секунд.

Перед загрузкой кода вам нужно указать свои сетевые учетные данные и IP-адрес MQTT-брокера:

ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
mqtt_server = '192.168.1.XXX'
#EXAMPLE IP ADDRESS
#mqtt_server = '192.168.1.106'

Замените REPLACE_WITH_YOUR_SSID на имя вашей Wi-Fi сети, REPLACE_WITH_YOUR_PASSWORD на пароль, а 192.168.1.XXX на IP-адрес вашего MQTT-брокера.

Полный код main.py:

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

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

ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
mqtt_server = '192.168.1.XXX'
#EXAMPLE IP ADDRESS
#mqtt_server = '192.168.1.106'

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

topic_pub_temp = b'esp/bme280/temperature'
topic_pub_hum = b'esp/bme280/humidity'
topic_pub_pres = b'esp/bme280/pressure'

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), freq=10000)
# ESP8266 - Pin assignment
#i2c = I2C(scl=Pin(5), sda=Pin(4), freq=10000)
bme = BME280.BME280(i2c=i2c)

def connect_mqtt():
  global client_id, mqtt_server
  client = MQTTClient(client_id, mqtt_server)
  #client = MQTTClient(client_id, mqtt_server, user=your_username, password=your_password)
  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'%s' % bme.temperature[:-1]
    #temp = (b'{0:3.1f},'.format((bme.read_temperature()/100) * (9/5) + 32))
    hum = b'%s' % bme.humidity[:-1]
    pres = b'%s'% bme.pressure[:-3]

    return temp, hum, pres
    #else:
    #  return('Invalid sensor readings.')
  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 = read_bme_sensor()
      print(temp)
      print(hum)
      print(pres)
      client.publish(topic_pub_temp, temp)
      client.publish(topic_pub_hum, hum)
      client.publish(topic_pub_pres, pres)

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

Примечание

Для ESP8266 раскомментируйте строку для ESP8266 и закомментируйте строку для ESP32:

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

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

Импорт библиотек

Код начинается с импорта необходимых библиотек: umqttsimple для MQTT-связи, BME280 для чтения показаний датчика, а также модули machine и network для настройки Wi-Fi и I2C.

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

Настройка сети

Введите ваш SSID, пароль и IP-адрес MQTT-брокера. Например, если ваш брокер работает по адресу 192.168.1.106, укажите этот адрес.

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

Идентификатор клиента

Уникальный идентификатор ESP используется в качестве идентификатора MQTT-клиента:

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

Определение топиков

Создаются три топика для публикации данных датчика:

topic_pub_temp = b'esp/bme280/temperature'
topic_pub_hum = b'esp/bme280/humidity'
topic_pub_pres = b'esp/bme280/pressure'

Переменные времени

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

last_message = 0
message_interval = 5

Подключение к Wi-Fi

Код подключается к указанной Wi-Fi сети и ожидает успешного подключения:

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

while station.isconnected() == False:
  pass

print('Connection successful')

Настройка I2C и BME280

Для ESP32 создается экземпляр I2C на пинах GPIO 22 (SCL) и GPIO 21 (SDA):

i2c = I2C(scl=Pin(22), sda=Pin(21), freq=10000)

Для ESP8266 используйте GPIO 5 (SCL) и GPIO 4 (SDA):

i2c = I2C(scl=Pin(5), sda=Pin(4), freq=10000)

Затем создается экземпляр датчика BME280:

bme = BME280.BME280(i2c=i2c)

Подключение к MQTT-брокеру

Функция 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

Если ваш брокер требует аутентификации, используйте:

client = MQTTClient(client_id, mqtt_server, user=your_username, password=your_password)

Перезагрузка и переподключение

Функция restart_and_reconnect() перезагружает ESP, если не удается опубликовать сообщение через MQTT (например, при отключении от брокера):

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

Чтение датчика BME280

Функция read_bme_sensor() возвращает текущие показания температуры, влажности и давления. Она обрабатывает исключения на случай ошибки чтения датчика:

def read_bme_sensor():
  try:
    temp = b'%s' % bme.temperature[:-1]
    hum = b'%s' % bme.humidity[:-1]
    pres = b'%s'% bme.pressure[:-3]
    return temp, hum, pres
  except OSError as e:
    return('Failed to read sensor.')

Публикация MQTT-сообщений

В основном цикле новые показания BME280 публикуются каждые 5 секунд.

Сначала проверяется, прошло ли достаточно времени с момента последней отправки:

if (time.time() - last_message) > message_interval:

Затем запрашиваются новые показания вызовом read_bme_sensor(). Результаты сохраняются в переменные temp, hum и pres.

Показания публикуются с помощью метода publish():

client.publish(topic_pub_temp, temp)
client.publish(topic_pub_hum, hum)
client.publish(topic_pub_pres, pres)

Время последней отправки обновляется:

last_message = time.time()

Если соединение потеряно, код вызывает restart_and_reconnect() для перезагрузки и повторного подключения.

После загрузки кода в Shell будут отображаться новые показания датчика каждые 5 секунд.

Настройка Node-RED Dashboard

ESP публикует показания температуры, влажности и давления каждые 5 секунд в три MQTT-топика. Любая панель управления или устройство, поддерживающее MQTT, может подписаться на эти топики и получать показания.

В этом примере мы используем Node-RED для подписки на топики и отображения показаний на приборных панелях (gauges).

Если у вас еще не установлен Node-RED, прочитайте следующие руководства:

Откройте браузер и перейдите по IP-адресу Raspberry Pi с портом 1880:

http://raspberry-pi-ip-address:1880

Создание потока (Flow)

  1. Перетащите три узла mqtt in и три узла gauge на рабочую область:

Node-RED перетаскивание узлов MQTT In и Gauge
  1. Отредактируйте свойства первого узла mqtt in:

    • В поле Server укажите localhost:1883 (если Mosquitto установлен локально на Raspberry Pi). Для облачного MQTT-брокера измените это поле соответственно.

    • В поле Topic укажите esp/bme280/temperature

    • Установите QoS на 1

  2. Отредактируйте второй узел mqtt in:

    • Те же настройки сервера

    • Topic: esp/bme280/humidity

    • QoS: 1

  3. Отредактируйте третий узел mqtt in:

    • Те же настройки сервера

    • Topic: esp/bme280/pressure

    • QoS: 1

Настройка MQTT In узлов в Node-RED
  1. Настройте первый узел gauge для температуры:

    • Title: Temperature

    • Label: C

    • Min: 0, Max: 40

    • Настройте цветовые диапазоны по необходимости

  2. Настройте второй узел gauge для влажности:

    • Title: Humidity

    • Label: %

    • Min: 30, Max: 100

  3. Настройте третий узел gauge для давления:

    • Title: Pressure

    • Label: hPa

    • Min: 0, Max: 1200

Настройка узла Gauge температуры в Node-RED
  1. Соедините узел mqtt temperature с gauge temperature, mqtt humidity с gauge humidity, mqtt pressure с gauge pressure.

Готовый поток Node-RED для BME280 MQTT
  1. Нажмите кнопку Deploy в правом верхнем углу.

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

Откройте Node-RED Dashboard по адресу:

http://raspberry-pi-ip-address:1880/ui

На панели управления будут отображаться текущие показания температуры, влажности и давления BME280 на трех приборных панелях (gauges). Вы также можете использовать другие типы узлов для отображения показаний в различных форматах.

Демонстрация Node-RED Dashboard с показаниями BME280

Заключение

MQTT – отличный протокол для обмена небольшими объемами данных между устройствами IoT. В этом руководстве мы показали, как публиковать показания датчика BME280 с ESP32/ESP8266 на MicroPython в различные MQTT-топики. Любое устройство или платформа домашней автоматизации, поддерживающие MQTT, могут подписаться на эти топики и получать показания.

Вместо BME280 вы можете использовать другие датчики, например DHT11/DHT22 или DS18B20.

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