در این آموزش یاد میگیرید چگونه بردهای ESP32 یا ESP8266 را با میکروپایتون پروگرم کنید تا مقادیری که از طریق MQTT و توسط سنسور DHT11 یا DHT22 خوانده میشوند، در هر پلتفرمی از MQTT یا هر client که از MQTT پشتیبانی می کند، Publish شوند. به عنوان مثال، ما مقادیر خوانده شده را در Publish ،Node-RED Dashboard میکنیم.
توجه: این آموزش با هر دو برد ESP32 و ESP8266 سازگار است.
مرور کلی پروژه
دیاگرام زیر، نمای کلی پروژهای که میخواهیم بسازیم را نشان میدهد:
- ESP مقادیر خوانده شدهی دما و رطوبت را از سنسور DHT11 یا DHT22 میخواهد.
- مقادیر خوانده شده دما در تاپیک Publish ،esp/dht/temperature میشوند.
- مقادیر خوانده شده رطوبت در تاپیکPublish ،esp/dht/humidity میشوند.
- این تاپیکها به subscribe، Node-RED میشوند.
- Node-RED مقادیر خوانده شده سنسور را دریافت میکند و آنها را توسط وسایل اندازهگیری (دماسنج و رطوبت سنج) نمایش میدهد.
- شما میتوانید مقادیری که سنسور خوانده را در هر پلتفرم دیگری که MQTT را پشتیبانی میکند، دریافت و آنها را هر طور که مایلید، مدیریت کنید.
پیشنیازها
برای ادامه این آموزش، باید فریمور میکروپایتون در برد ESP32 یا ESP8266 شما نصب باشد. همچنین برای نوشتن و آپلود کد در بردتان به یک IDE نیاز دارید. ما پیشنهاد میکنیم از Thonny IDE یا uPyCraft IDE استفاده کنید.
بروکر MQTT
برای استفاده از MQTT، به یک بروکر نیاز دارید. ما از Mosquitto broker استفاده میکنیم که روی یک Raspberry Pi نصب است.
قطعات مورد نیاز
برای ادامه این آموزش، به قطعات زیر احتیاج دارید:
ESP32 یا ESP8266
DHT11 یا DHT22
مقاومت ۴.۷ کیلواهم
برد Raspberry Pi
MicroSD Card – ۱۶GB Class10
منبع تغذیه ۵V 2.5A) Raspberry Pi)
سیم جامپر
بردبرد
کتابخانه umqtttsimple
برای استفاده از MQTT با ESP32/ESP8266 و با میکروپایتون، از کتابخانه umqttsimple.py استفاده میکنیم.
با توجه به IDE که استفاده میکنید، از دستورالعملهای زیر استفاده کنید:
A. آپلود کتابخانه umqttsimple با uPyCraft IDE
B. آپلود کتابخانه umqttsimple با Thonny IDE
کد کتابخانه 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)
#print(hex(len(msg)), hexlify(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
#print(hex(len(pkt)), hexlify(pkt, ":"))
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)
#print(hex(len(pkt)), hexlify(pkt, ":"))
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)
#print(resp)
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()
A. آپلود کتابخانه umqttsimple با uPyCraft IDE
۱- با کلیک روی New File یک فایل جدید ایجاد کنید.
۲- کد کتابخانه umqttsimple را در آن کپی کنید.
۳- با کلیک روی Save آن را ذخیره کنید.
۴- این فایل را “umqttsimple.py” بنامید و روی ok کلیک کنید.
۵- روی Download and Run کلیک کنید.
۶- فایل باید در پوشه دستگاه شما با نام“umqttsimple.py” همانطور که در تصویر مشخص شده است، ایجاد شود.
حالا میتوانید از این کتابخانه در کدتان استفاده کنید.
B. آپلود کتابخانه umqttsimple با Thonny IDE
۱- کد کتابخانه را فایل جدبد کپی کنید.
۲- به مسیر زیر بروید.
…File > Save as
۳- ذخیره در “MicroPython device“ را انتخاب کنید.
۴- فایل را umqttsimple.py بنامید و OK را فشار دهید.
تمام. کتابخانه در برد شما آپلود شده است. برای اینکه مطمئن شوید آپلود موفقیتآمیز بوده است، به مسیر … File> Save as بروید و MicroPython device را انتخاب کنید. فایل شما باید در آنجا لیست شده باشد:
پس از آپلود کتابخانه به بردتان، با وارد کردن کتابخانه به کدتان، میتوانید از قابلیتهای آن استفاده کنید.
شماتیک: ESP32 با DHT11/DHT22
همانطور که در دیاگرام شماتیک زیر آمده است، سنسور DHT22 یا DHT11 را به برد ESP32 وصل کنید.
در این مثال، ما پین داده DHT را به GPIO 14 متصل کردیم. شما میتوانید از هر پین دیجیتال دیگری استفاده کنید.
شماتیک: ESP8266 NodeMCU با DHT11/DHT22
اگر از برد ESP8266 NodeMCU استفاده میکنید، دیاگرام زیر را دنبال کنید.
در این مثال، ما پین داده DHT را به D5) GPIO 14) متصل کردیم. شما میتوانید از هر پین دیجیتال دیگری استفاده کنید.
کد
بعد از آپلود کتابخانه در ESP32/ESP8266، کد زیر را در فایل main.py کپی کنید. این کد، دما و رطوبت را در تاپیکهای esp/dht/temperature و esp/dht/humidity هر پنج ثانیه publish میکند.
import time
from umqttsimple import MQTTClient
import ubinascii
import machine
import micropython
import network
import esp
from machine import Pin
import dht
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 or DOMAIN NAME
#mqtt_server = '192.168.1.106'
client_id = ubinascii.hexlify(machine.unique_id())
topic_pub_temp = b'esp/dht/temperature'
topic_pub_hum = b'esp/dht/humidity'
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')
sensor = dht.DHT22(Pin(14))
#sensor = dht.DHT11(Pin(14))
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_sensor():
try:
sensor.measure()
temp = sensor.temperature()
# uncomment for Fahrenheit
#temp = temp * (9/5) + 32.0
hum = sensor.humidity()
if (isinstance(temp, float) and isinstance(hum, float)) or (isinstance(temp, int) and isinstance(hum, int)):
temp = (b'{0:3.1f},'.format(temp))
hum = (b'{0:3.1f},'.format(hum))
return temp, hum
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 = read_sensor()
print(temp)
print(hum)
client.publish(topic_pub_temp, temp)
client.publish(topic_pub_hum, hum)
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 machine import Pin
import dht
esp.osdebug(None)
import gc
gc.collect()
در متغیرهای زیر باید اعتبارنامه شبکهتان و آدرس IP بروکر خود را وارد کنید.
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
mqtt_server = 'REPLACE_WITH_YOUR_MQTT_BROKER_IP'
برای مثال، آدرس IP بروکر ما ۱۹۲.۱۶۸.۱.۱۰۶ است.
mqtt_server = '192.168.1.106'
برای ساخت یک کلاینت MQTT، به ID منحصر به فرد ESP نیاز داریم؛ این کار را ما در خط زیر انجام دادیم. (ID در متغیر client_id ذخیره میشود.)
client_id = ubinascii.hexlify(machine.unique_id())
سپس تاپیکی که میخواهید ESP در آن publish شود را ایجاد کنید. در مثال ما، دما در تاپیک esp/dht/temperature و رطوبت در تاپیک publish ،esp/dht/humidity میشوند.
topic_pub_temp = b'esp/dht/temperature'
topic_pub_hum = b'esp/dht/humidity'
سپس متغیرهای زیر را ایجاد کنید:
last_message = 0
message_interval = 5
متغیر last_message آخرین پیامی که فرستاده شده است را نگه میدارد و متغیر message_interval زمان بین هر پیام فرستاده شده را مشخص میکندکه ما آن را روی ۵ ثانیه تنظیم کردیم (یعنی پیغام جدید بعد از ۵ ثانیه فرستاده میشود). اگر میخواهید، میتوانید آن را تغییر دهید.
پس از آن، ESP را به شبکه local خود متصل کنید.
station = network.WLAN(network.STA_IF)
station.active(True)
station.connect(ssid, password)
while station.isconnected() == False:
pass
print('Connection successful')
با ایجاد یک نمونه dht در GPIO 4 به شرح زیر، سنسور DHT را راهاندازی اولیه کنید:
sensor = dht.DHT22(Pin(4))
اگر از DHT11 استفاده میکنید، خط قبلی را کامنت کنید و خط بعدی را از حالت کامنت خارج کنید:
sensor = dht.DHT11(Pin(4))
اتصال به بروکر MQTT
تابع ()connect_mqtt یک MQTT Client میسازد و به بروکر شما وصل میکند.
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
اگر MQTT بروکر شما به username و password نیاز دارد، برای اینکه آنها را به عنوان آرگومان ارسال کنید، از خطوط زیر استفاده کنید.
client = MQTTClient(client_id, mqtt_server, user=your_username, password=your_password)
راهاندازی و اتصال مجدد
تابع ()restart_and_reconnect برد ESP32/ESP8266 را ریست میکند. وقتی که اتصال بروکر قطع شود، نمیتوانیم مقادیری که توسط MQTT از سنسور خوانده شدهاند را publish کنیم. در این موقعیت این تابع فراخوانی میشود.
def restart_and_reconnect():
print('Failed to connect to MQTT broker. Reconnecting...')
time.sleep(10)
machine.reset()
خواندن سنسور DHT
ما تابعی به نام ()read_sensor را ایجاد میکنیم که دما و رطوبت کنونی را از سنسور DHT برمیگرداند و در صورت عدم امکان خواندن از سنسور، موارد استثنا را کنترل می کند.
def read_sensor():
try:
sensor.measure()
temp = sensor.temperature()
hum = sensor.humidity()
if (isinstance(temp, float) and isinstance(hum, float)) or (isinstance(temp, int) and isinstance(hum, int)):
temp = (b'{0:3.1f},'.format(temp))
hum = (b'{0:3.1f},'.format(hum))
# uncomment for Fahrenheit
#temp = temp * (9/5) + 32.0
return temp, hum
else:
return('Invalid sensor readings.')
except OSError as e:
return('Failed to read sensor.')
publish کردن پیامهای MQTT
در حلقه while، ما هر ۵ ثانیه دما و رطوبت جدید خوانده شده را publish میکنیم.
ابتدا چک میکنیم که زمان خواندن پیام جدید رسیده است یا نه:
if (time.time() - last_message) > message_interval:
اگر زمان دریافت پیام چدید رسیده باشد، مقادیر خوانده شده جدید را با فراخوانی تابع ()read_sensor از سنسور DHT درخواست میکند. دما در متغیر temp و رطوبت در متغیر hum ذخیره میشوند.
temp, hum = read_sensor()
در نهایت، مقادیر جدید خوانده شده، توسط متد ()publish در آبجکت publish ،client میشوند. متد ()publish، تاپیک و پیغام را (مانند دو خط زیر) به عنوان آرگومان میپذیرد.
client.publish(topic_pub_temp, temp)
client.publish(topic_pub_hum, hum)
در آخر، وقتی آخرین پیغام فرستاده شد، زمان را آپدیت کنید:
last_message = time.time()
اگر اتصال ESP32 یا ESP8266 از بروکر قطع شد، ما نمیتوانیم مقادیر خوانده شده از سنسور را publish کنیم. در این موارد، تابع ()restart_and_reconnect برای ریست کردن برد ESP و تلاش برای اتصل دوباره به بروکر، فراخوانی میشود.
except OSError as e:
restart_and_reconnect()
بعد از آپلود کد، باید مقادیر جدید خوانده شده هر ۵ ثانیه در Shell نمایش داده شوند.
حال برای آماده کردن Node-RED به بخش بعدی بروید تا مقادیر خوانده شدهای که ESP publish میکند را دریافت کنید.
آماده سازی Node-RED Dashboard
ESP32 یا ESP8266 هر ۵ ثانیه دما و رطوبت خوانده شده را درتاپیکهای esp/dht/temperature و publish esp/dht/humidity میکند. شما میتوانید از هر dashboard دیگری که MQTT را پشتیبانی میکند و یا هر دستگاه دیگری که MQTT را برای subscribe تاپیکها و دریافت مقادیر خوانده شده، پشتیبانی میکند استفاده کنید.
به عنوان مثال، ما یک flow ساده با استفاده از Node-RED میسازیم تا تاپیکها subscribe شوند و مقادیر خوانده شده، روی دماسنج و رطوبتسنج نشان داده شوند.
با نصب Node-RED روی رزبری پای، به آدرس IP رزبری پای خود (بعد آن ۱۸۸۰: قرار میگیرد) بروید.
http://raspberry-pi-ip-address:1880
رابط Node-RED باید باز شود. دو MQTT node و دو node وسایل اندازهگیری(دماسنج و رطوبتسنج) را ایجاد کنید.
روی MQTT node کلیک کنید و مشخصات آن را ویرایش کنید.
فیلد Server به MQTT broker اشاره دارد. در مثال ما، MQTT broker رزبری پای است؛ بنابراین روی localhost:1883 تنظیم شده است. اگر از یک Cloud MQTT broker استفاده کنید، باید فیلد را تغییر دهید.
تاپیکی که میخواهید subscribed شود و QoS را درج کنید. MQTT node قبلی به تاپیک subscribe esp/dht/temperature شده است.
روی MQTT node دیگر کلیک کنید و مشخصات آن را با همان سرور اما برای تاپیک esp/dht/humidity ویرایش کنید.
روی node وسایل اندازهگیری (دماسنج و رطوبتسنج) کلیک کنید و ویژگیهای آن را ویرایش کنید. node زیر برای دمای خوانده شده تنظیم شده است. node دیگر را هم برای رطوبت خوانده شده تنظیم کنید.
nodeها را به شکل زیر به هم وصل کنید.
در نهایت، flow خود را deploy کنید.(دکمه بالا سمت راست)
همچنین میتوانید به مسیر Menu > Import بروید و عبارت زیر را در Clipboard کپی کنید تا Node-RED flow شما ایجاد شود.
[{"id":"59f95d85.b6f0b4","type":"mqtt in","z":"b01416d3.f69f38","name":"","topic":"esp/dht/temperature","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":910,"y":340,"wires":[["2babfd19.559212"]]},{"id":"2babfd19.559212","type":"ui_gauge","z":"b01416d3.f69f38","name":"","group":"37de8fe8.46846","order":2,"width":0,"height":0,"gtype":"gage","title":"Temperature","label":"ºC","format":"{{value}}","min":0,"max":"40","colors":["#00b500","#f7df09","#ca3838"],"seg1":"","seg2":"","x":1210,"y":340,"wires":[]},{"id":"b9aa2398.37ca3","type":"mqtt in","z":"b01416d3.f69f38","name":"","topic":"esp/dht/humidity","qos":"1","datatype":"auto","broker":"8db3fac0.99dd48","x":900,"y":420,"wires":[["d0f75e86.1c9ae"]]},{"id":"d0f75e86.1c9ae","type":"ui_gauge","z":"b01416d3.f69f38","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":1200,"y":420,"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":"BME280","tab":"53b8c8f9.cfbe48","order":1,"disp":true,"width":"6","collapse":false},{"id":"53b8c8f9.cfbe48","type":"ui_tab","z":"","name":"Home","icon":"dashboard","order":2,"disabled":false,"hidden":false}]
نمایش عملی
به صورت زیر به آدرس IP رزبری پای خود بروید.
http://raspberry-pi-ip-address:1880/ui
شما باید به دما و رطوبت خوانده شده DHT در Dashboard دسترسی داشته باشید.
شما میتوانید از nodeهای دیگر dashboard برای نمایش مقادیر خوانده شده به روشهای دیگر استفاده کنید.
حال شما برد ESP32 یا ESP8266 را دارید که مقادیر خوانده شدهی دما و رطوبت DHT را از طریق MQTT و با استفاده از میکروپایتون در publish ،Node-RED میکند.
جمعبندی
MQTT یک پروتکل ارتباطی عالی برای تبادل دادههای کوچک بین دستگاههای IoT است. در این آموزش یاد گرفتید که چگونه دما و رطوبت خوانده شده از یک سنسور DHT را با برد ESP32 یا ESP8266 و با استفاده از میکروپایتون در تاپیکهای مختلف publish ،MQTT کنید.شما میتوانید از هر دستگاه یا پلتفرم اتوماسیونسازی خانه برای subscribe کردن آن تاپیکها استفاده کنید و مقادیر خوانده شده را دریافت کنید.
به جای سنسور DHT11 یا DHT22 میتوانید از هر سنسور دیگری مانند سنسور دمای DS18B20 و یا BME280 استفاده کنید.