摘要
物联网是一个基于互联网、传统电信网等的信息承载体,它让所有能够被独立寻址的普通物理对象形成互联互通的网络[1]。物联网的应用领域涉及到方方面面,在工业、农业、环境、交通、物流、安保等基础设施领域的应用,有效的推动了这些方面的智能化发展,使得有限的资源更加合理的使用分配,从而提高了行业效率、效益。
本项目分为设备端和PC端,设备端负责采集数据并且能做出一些反应,例如:温湿度、光照强度的数据采集,光电门判断门的开关,控制蜂鸣器发声报警,控制灯的亮灭以及亮度;PC端主要接收设备端上传的数据并且显示相关信息,下发控制指令到设备。
1. 引言
物联网的应用领域涉及到方方面面,本项目实现的功能可以应用到智能家居、智能农业大棚、智慧城市等等。
这里硬件编程应用到了MicroPython,MicroPython是Python的一个精简版本,MicroPython极精简高效的实现了Python 3.X语言。因为要适应嵌入式微控制器,所以裁剪了大部分标准库,仅保留部分模块如math、sys的部分函数和类。此外,很多标准模块如json、re等在MicroPython中变成了以u开头的ujson、ure,表示针对MicroPython开发的标准库,能在单片机和受限环境中运行。MicroPython包含了诸如交互式提示,任意精度整数,关闭,列表解析,生成器,异常处理等高级功能。 足够精简,它是为了运行在单片机这样的性能有限的微控制器上,最小体积仅256K,运行时仅需16K内存。MicroPython旨在尽可能与普通Python兼容,让您轻松将代码从桌面传输到微控制器或嵌入式系统。
设备端和PC端之间采用了MQTT通信(基于发布/订阅范式的消息协议),传输数据打包成JSON数据格式。MQTT是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛,在很多情况下,包括受限的环境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、偶尔拨号的医疗设备、智能家居、及一些小型化设备中已广泛使用。
本项目分为设备端和PC端:
设备端负责采集数据并且能做出一些反应,例如:温湿度、光照强度的数据采集,光电门判断门的开关,控制蜂鸣器发声报警,控制灯的亮灭以及亮度;设备的选择:主控芯片是ESP8266,126*64的OLED显示屏,温湿度DHT11、光敏电阻、蜂鸣器、光电门、杜邦线若干。ESP8266具有WIFI功能,无需加入其他的联网设备,而且MicroPython对其进行了支持,刷入对应的MicroPython固件到esp8266就可以进行python编程了。
PC端基于python中的tkinter模块编写一个交互界面。主要接收设备端上传的数据并且显示相关信息,下发一些控制指令到设备。
2. 系统结构
该系统主要由服务器、设备端和PC端三部分构成。
1.1 服务器
服务器可以选择自己在本地搭建,也可以去跟各大平台合作。我这里选择是跟某度智能云合作,前往平台注册个账号就可以白嫖物接入服务了。
进入到物接入服务,创建项目,选择类型,输入一个自己喜欢项目名称,确定就会返回一个域名地址。进入项目就可以创建用户、身份以及策略(订阅与发布的主题)。一定要记住创建身份后返回的随机密钥,后面连接服务需要使用,不过忘了可以重置。
图1.服务器创建项目
1.2 设备端
i. 环境搭建
要在ESP8266进行python编程肯定先要刷入Micropython固件,固件从这个地址下载:http://micropython.org/download#esp8266,固件应该就五百多k,下载两三个小时应该就好了;刷入工具有好多,不过都大同小异,比如:ESP8266Flasher 、NodeMCU-PyFlasher 等。
编辑器我选择是Visual Studio Code,因为在这里RT-Thread 官方提供了个RT-Thread MicroPython插件,为MicroPython 开发提供了强大的开发环境,主要特性如下:
- 设备快速连接(串口、网络、USB)
- 支持基于 MicroPython 的代码智能补全与语法检查
- 支持 MicroPython REPL 交互环境
- 提供工程同步功能
- 支持下载单个文件或文件夹至开发板
- 支持在内存中快速运行代码文件功能
- 支持运行代码片段功能
- 支持多款主流 MicroPython 开发板
- 支持 Windows、Ubuntu、Mac 操作系统
ii. 分配引脚
ESP8266共有 17个GPIO 管脚,其中GPIO16与其他 IO 口不同,GPIO16(XPD_DCDC) 不属于通用 GPIO 模块,它属于 RTC 模块,可以用来在深度睡眠时候唤醒整个芯片,可以配置为输入或者输出模式,但无法触发 IO 中断。ESP8266内置了一个10-bit精度的SAR ADC。其中,在四线(QUAD)模式Flash下,有6个IO口用于Flash通讯,还有两个用做串口UART0通信,最终剩下10个可用IO和一个ADC的IO。
该系统使用到的设备有126*64的OLED显示屏、温湿度DHT11、光敏电阻、蜂鸣器、光电门、板载的LED。其中LED 、DHT11、蜂鸣器、光电门单个引脚就可以控制了,esp8266有专门数模转换的ADC引脚可以连接光敏电阻;这里驱动OLED用的是SPI协议,需要时钟线(SCK)、数据线(MOSI)、数据/命令选择管脚(DC)还有片选(CS),复位(RES)和esp8266的复位连接,与其同时复位。
具体分配如下表:
|
LED |
GPIO2 |
|
DHT11 |
GPIO5 |
|
蜂鸣器 |
GPIO4 |
|
光电门 |
GPIO12 |
|
光敏电阻 |
ADC0 |
|
OLED显示屏 |
SCK → GPIO14 |
|
MOSI → GPIO13 |
|
|
DC → GPIO10 |
|
|
CS → GPIO15 |
|
|
RES → RET |
表1.引脚分配
图2.实物接线
iii. 相关模块的使用
先在这里提供MicroPython针对ESP8266版的快速指南:Quick reference for the ESP8266
machine模块:板级库函数,包含了和开发板相关的库函数;用到的类方法有Pin、PWM、SPI、ADC、RTC、Timer。Pin控制引脚;PWM调节LED亮度以及驱动蜂鸣器,因为我的蜂鸣器是无源的,内部不带震荡源,需要自己电路产生一定频段范围内的方波驱动它;SPI发送数据驱动OLED显示屏;ADC将光敏电阻的模拟信号转化为数字信号;RTC,提供了时钟、日期和时间功能,通过网络同步时间实现实时时钟;Timer定时器,定时产生中断,实现定时刷新数据,上传数据;
ssd1306模块:实现OLED显示相关功能;
network模块:连接到本地WIFI网络,建立网络后,socket模块可像往常一样创建和使用 TCP/UDP 套接字;
ntptime模块:实现同步网络时间,可修改时区和网络服务器;
time模块:主要用来产生延时;
umqtt.simple模块:实现连接MQTT服务,订阅/发布消息等功能;
ujson模块:将字典格式和json格式的数据相互转换;
iv. 大概实现
ESP8266主控芯片启动后先初始化OLED显示屏,等待连接WIFI,获取网络时间同步到本地,初始化LED、蜂鸣器、光敏电阻、DHT11、光电门,获取传感器数据保存,连接MQTT服务,发布设备启动后获取的传感器数据,启动定时器,定时刷新传感器数据,定时上传发布传感器数据。
1.3 PC端
i. 开发环境
操作系统windows10,使用python3的版本是python3.8.2,编辑器还是选择Visual Studio Code。
ii. 相关模块的使用
我用到Python模块有tkinter、time、json、paho.mqtt.client。
tkinter是Python的标准GUI库,Python使用tkinter可以快速的创建 GUI 应用程序,相关的API接口函数介绍参考菜鸟教程:Python GUI编程(Tkinter) 。
time模块本系统主要用来获取本地时间。
paho.mqtt.client用来实现了MQTT通信,连接服务器,订阅和发布消息,断开连接等。paho.mqtt模块使用和API分析可以参考:Python paho.mqtt 模块使用和API分析
iii. 大概实现
UI界面显示温湿度、光照强度、门状态、还有LED和蜂鸣器的开还是关,可以通过按钮控制LED和蜂鸣器了开关,LED还可以通过滑块控制它的亮度。还有三个按钮,连接和断开服务器,以及一个用来刷新当前状态信息。连接成功后按键会变红,而且下发一条获取状态的指令,即连接成功后尝试与设备同步信息。
图3.PC端UI界面
3. 实现代码
1.1 服务器
服务器不是自己搭建的,选择第三方,这里就没有实现代码;创建对应用户、身份和主题。
图4.创建用户
图5.创建身份
图6.创建策略,以及设置主题
1.2 设备端
i. device——led
创建了一个led类,实例化时传入引脚号;类方法有:打开、关闭和翻转LED、获取LED状态、设置和获取LED亮度值;
#===========================led=============================== class led: def __init__(self, Pin_num): self.device = PWM(Pin(Pin_num, Pin.OUT), freq=1000, duty=1023) def on(self): #打开 self.device.duty(0) def off(self): #关闭 self.device.duty(1023) def toggle(self): #翻转 if self.device.duty() == 1023: #关闭状态 self.device.duty(0) else: self.device.duty(1023) def value(self): #返回现在状态 return 0 if (self.device.duty() == 1023) else 1 def set_light(self, LED_LIGHT: int): #设置亮度值 self.device.duty(int(1023-LED_LIGHT*10.23)) def get_light(self): #获取亮度值 return int((1023 - self.device.duty())//10.23)
ii. device——蜂鸣器
创建了一个beep类,实例化时传入引脚号;类方法有:打开、关闭蜂鸣器、获取蜂鸣器状态;
#===========================beep=============================== class beep: def __init__(self, Pin_num): self.device = PWM(Pin(Pin_num, Pin.OUT, value=0), freq=1000, duty=1023) def on(self): self.device.duty(512) def off(self): self.device.duty(1023) def value(self): return 0 if (self.device.duty() == 1023) else 1
iii. device——DHT11温湿度传感器
创建了一个dht11类,实例化时传入引脚号;类方法有:DHT11初始化和获取温湿度;
#===========================dht11=============================== class dht11: def __init__(self, Pin_num): self.device = dht.DHT11(Pin(Pin_num)) self.device.measure() time.sleep(2) def init(self): # 成功返回1,超时返回0 try: self.device.measure() return 1 except OSError: print("DHT11 ETIMEDOUT") return 0 def get_data(self): # 以列表的形式返回温湿度 return self.device.temperature(), self.device.humidity()
iv. device——光敏电阻
创建了一个light类,实例化时传入引脚号;类方法有:获取光照亮度值;
#===========================light=============================== class light: def __init__(self): self.device = ADC(0) def read(self): return int((1023 - self.device.read())/10.23)
v. device——实例化
#--------------------Device_init--------------------------- LED = led(2) #LED BEEP = beep(4) #蜂鸣器 LIGHT = light() #光敏电阻 PHOTOGATE = Pin(12, Pin.IN) #光电门 DHT11 = dht11(5) #DHT11温湿度
vi. device——OLED显示
初始化SPI引脚,初始化OLED显示屏
#--------------------OLED_INIT---------------------------------- OLED_SPI = SPI(baudrate=10000000, polarity=1, phase=0, sck=Pin(14, Pin.OUT), mosi=Pin(13, Pin.OUT), miso=Pin(12)) display = ssd1306.SSD1306_SPI(128, 64, OLED_SPI, dc=Pin(10), cs=Pin(15) ,res=Pin(4)) display.poweron() #OLED使能 display.init_display() #初始化 display.fill(0) #清空缓冲区 display.text(\'----NODEMCU----\', 1, 1) display.text(\'Wait Wifi\', 1, 16) display.text(\'Connect to AP ..\', 1, 32) display.text(\'......\', 1, 48) display.show() #显示
vii. 连接WIFI
连接wifi只需几行代码就好了,先激活STA模式,就可以输入名字密码连接wifi了;这里我判断如果没有连接上wifi,就循环阻塞,后续在加入其他的配网方式。
#--------------------WIFI---------------------------------- sta = network.WLAN(network.STA_IF) # 创建sta sta.active(True) # 激活sta模式 sta.scan() #扫描附加wifi sta.connect(\'zzz2\', \'87654321\') # 连接到wifi while not sta.isconnected(): pass
viii. 同步网络时间
创建两个方法,一个用来同步网络时间,一个用来获取本地时间
def sync_time(): ntptime.NTP_DELTA = 3155644800 # 可选 UTC+8偏移时间(秒),不设置就是UTC0 ntptime.host = \'ntp1.aliyun.com\' # 可选,ntp服务器,默认是"pool.ntp.org" ntptime.settime() # 修改设备时间,到这就已经设置好了 def get_datetime(): rtc = RTC() d_t = rtc.datetime() now_date = \'%04d/%02d/%02d\'%(d_t[0], d_t[1], d_t[2]) now_time = \'%02d:%02d:%02d\'%(d_t[4], d_t[5], d_t[6]) return now_date, now_time #使用方法 sync_time() #同步网络时间 NOW_DATE, NOW_TIME = get_datetime() #获取本地RTC实时时钟时间
ix. 光电门实现中断服务
光电门用来判断门的开关,需要做到实时判断,因此加入中断服务;当引脚产生上升沿或者下降沿时触发中断,中断服务函数:失能全部中断服务并保存当前中断使能状态,防止其他中断干扰,打开蜂鸣器,记录门的开关状态,恢复中断状态;定时器定时一段时间触发单次中断任务关闭蜂鸣器,最后发布门的状态信息。
#--------------------IRQ---------------------------------- def photogate_irq(Pin_num): #中断服务函数 state = machine.disable_irq() global DOOR print(Pin_num) BEEP.on() DOOR[\'2\'] = PHOTOGATE.value() machine.enable_irq(state) Timer(1).init(period=200, mode=Timer.ONE_SHOT, callback=lambda Tim: BEEP.off()) pub_msg([DOOR,]) #上升下降沿都触发 PHOTOGATE.irq(handler=photogate_irq,trigger=Pin.IRQ_FALLING|Pin.IRQ_RISING)
x. 字典数据格式的全局变量
DOOR = {\'1\' : \'DOOR\', \'2\' : 0} #门
BUZZER = {\'1\' : \'BUZZER\', \'2\' : 0} #蜂鸣器
LED_LIGHT = {\'1\' : \'LED_LIGHT\', \'2\' : 0}#LED亮度
ILLUMINATION = {\'1\' : \'ILLUMINATION\', \'2\' : 0}#光照强度
HUMIDITY = {\'1\' : \'HUMIDITY\', \'2\' : {\'TEMP\' : 0, \'HUMI\' : 0}}#温湿度
xi. Mqtt连接服务
Mqtt连接参数声明
# MQTT服务器地址域名 SERVER = "nkqhkzm.mqtt.iot.gz.baidubce.com" CLIENT_ID = "666" #设备ID USERNAME = \'nkqhkzm/esp8266\' #产品用户名 PASSWORD = \'MC5asL3iW6HEaY5p\' #产品APIKey: #订阅主题需与平台一致 SUB_TOPIC = "/light/deviceIn" PUB_TOPIC = "/light/deviceOut"
订阅消息回调函数,解析指令做出响应
def sub_cb(topic: str, msg: str): print((topic, msg)) global BUZZER, LED_LIGHT, ILLUMINATION, HUMIDITY cmd = ujson.loads(msg.decode()) if list(cmd.keys()) in [[\'1\', \'2\'], [\'2\', \'1\']]: if cmd[\'1\'] == \'LED\': #LED开关 if cmd[\'2\']: LED.on() else: LED.off() LED_LIGHT[\'2\'] = LED.get_light() elif cmd[\'1\'] == \'LED_LIGHT\': #调节LED亮度 if 0 <= cmd[\'2\'] <= 100: LED.set_light(cmd[\'2\']) LED_LIGHT[\'2\'] = LED.get_light() elif cmd[\'1\'] == \'BUZZER\': #蜂鸣器开关 if cmd[\'2\']: BEEP.on() else: BEEP.off() elif cmd[\'1\'] == \'GET_MSG\': #获取设备全部数据 BUZZER[\'2\'] = BEEP.value() DOOR[\'2\'] = PHOTOGATE.value() LED_LIGHT[\'2\'] = LED.get_light() ILLUMINATION[\'2\'] = LIGHT.read() if DHT11.init(): HUMIDITY[\'2\'][\'TEMP\'], HUMIDITY[\'2\'][\'HUMI\'] = DHT11.get_data() pub_msg([BUZZER, LED_LIGHT, ILLUMINATION, HUMIDITY, DOOR])
封装发布消息方法:传入列表,方法内遍历列表,判断是字典数据才加装为json格式进行发送;
def pub_msg(msg: list): for i in msg: if type(i) is dict: MQTT_C.publish(PUB_TOPIC, ujson.dumps(i))
连接Mqtt服务,这里采用的是TCP的方式连接,端口号就是1883,心跳设置为600秒
#端口号为:1883 MQTT_C = mqtt.MQTTClient(CLIENT_ID, SERVER, 1883, USERNAME, PASSWORD, 600) MQTT_C.set_callback(sub_cb) #声明回调函数 MQTT_C.connect() #连接 MQTT_C.subscribe(SUB_TOPIC) #订阅主题 print("Connected to %s, subscribed to %s topic" % (SERVER, SUB_TOPIC))
xii. 定时刷新上传数据
采用定时器中断实现定时刷新数据以及上传,这里定时30秒产生一次定时器中断;
#--------------------TIMER---------------------------------- def timer0_func(Tim): #定时器中断服务函数 state = machine.disable_irq() global BUZZER, LED_LIGHT, ILLUMINATION, HUMIDITY BUZZER[\'2\'] = BEEP.value() DOOR[\'2\'] = PHOTOGATE.value() LED_LIGHT[\'2\'] = LED.get_light() ILLUMINATION[\'2\'] = LIGHT.read() if DHT11.init(): HUMIDITY[\'2\'][\'TEMP\'], HUMIDITY[\'2\'][\'HUMI\'] = DHT11.get_data() pub_msg([BUZZER, LED_LIGHT, ILLUMINATION, HUMIDITY, DOOR]) print(\'publish msg\', Tim) machine.enable_irq(state) TIMER0_TASK = Timer(0) TIMER0_TASK.init(period=30000, mode=Timer.PERIODIC, callback=timer0_func)
xiii. 最终OLED显示时间、光照强度、温湿度
循环每隔200毫秒刷新一次状态,以及检查是否有订阅消息到达;
try: while 1: NOW_DATE, NOW_TIME = get_datetime() ILLUMINATION[\'2\'] = LIGHT.read() display.fill(0) display.text(NOW_DATE[5:]+\' \'+NOW_TIME, 1, 1) display.text(\'LIGHT:%d%%\'%ILLUMINATION[\'2\'], 1, 16) display.text(\'TEMP:%d\\'C\'%HUMIDITY[\'2\'][\'TEMP\'], 1, 31) display.text(\'HUMI:%d%%RH\'%HUMIDITY[\'2\'][\'HUMI\'], 1, 46) display.show() MQTT_C.check_msg() #检查是否有订阅消息 time.sleep_ms(100) finally: MQTT_C.disconnect()
1.3 PC端
i. UI界面
实例化一个Tk窗口,通过标签Label加载背景图片,加入相关的标签,通过Label类的place方法调节标签的位置以及大小
#============================tkinter=========================== root= Tk() root.geometry(\'250x450\') # 这里的乘号不是 * ,而是小写英文字母 x root.title(\'智能XXX\') #增加背景图片 photo = PhotoImage(file="背景.png") theLabel = Label(root, justify=LEFT, image=photo, compound = CENTER) #关键:设置为背景图片 theLabel.place(x=0, y=14, width=250, height=436)
ii. 时间显示
#============================TIME============================== def gettime(): timestr = time.strftime("%Y/%m/%d - %H:%M:%S") # 获取当前的时间并转化为字符串 time_lb.configure(text=timestr) # 重新设置标签文本 root.after(1000,gettime) # 每隔1s调用函数 gettime 自身获取时间 time_lb = Label(root,text=\'\',fg=\'blue\',font=("黑体",12), relief=RIDGE) time_lb.place(x=0, y=0, width=250, height=20) gettime()
iii. 温度显示
#============================TEMP============================== Label(root, anchor=E, text=\'温度 :\', bg=\'#98F555\', fg=\'blue\',\ font=(\'黑体\',16), width=6, height=1,\ relief=GROOVE).place(x=35, y=25, width=100, height=30) temp = Label(root, text=\'---\', bg=\'#d3fbfb\', \ font=(\'黑体\',16), width=6, height=1, relief=GROOVE) temp.place(x=145, y=25, width=80, height=30)
iv. 湿度显示
#============================HUMI============================== Label(root, anchor=E, text=\'湿度 :\', bg=\'#98F555\', fg=\'blue\',\ font=(\'黑体\',16), width=6, height=1,\ relief=GROOVE).place(x=35, y=55, width=100, height=30) humi = Label(root, text=\'---\', bg=\'#d3fbfb\', \ font=(\'黑体\',16), width=6, height=1, relief=GROOVE) humi.place(x=145, y=55, width=80, height=30)
v. 光照亮度显示
#============================LIGHT============================= Label(root, anchor=E, text=\'光照 :\', bg=\'#98F555\', fg=\'blue\',\ font=(\'黑体\',16), width=6, height=1,\ relief=GROOVE).place(x=35, y=85, width=100, height=30) light = Label(root, text=\'---\', bg=\'#d3fbfb\',\ font=(\'黑体\',16), width=6, height=1, relief=GROOVE) light.place(x=145, y=85, width=80, height=30)
vi. 门状态显示
#============================DOOR============================== Label(root, anchor=E, text=\'门 :\', bg=\'#98F555\', fg=\'blue\',\ font=(\'黑体\',16), width=6, height=1,\ relief=GROOVE).place(x=35, y=115, width=100, height=30) door = Label(root, text=\'---\', bg=\'#d3fbfb\', \ font=(\'黑体\',16), width=6, height=1, relief=GROOVE) door.place(x=145, y=115, width=80, height=30)
vii. LED开关
#============================LED=============================== Label(root, anchor=E, text=\'LED :\', bg=\'#98F555\', fg=\'blue\',\ font=(\'黑体\',16), width=6, height=1,\ relief=GROOVE).place(x=35, y=145, width=100, height=30) def led_button(): #LED开关按钮事件 if mqttc.is_connected(): global LED_CMD, LED_LIGHT_CMD LED_CMD[\'2\'] = not LED_CMD[\'2\'] LED_LIGHT_CMD[\'2\'] = (100 if LED_CMD[\'2\'] else 0) mqttc.publish(pub_topic, payload=json.dumps(LED_CMD), qos=0) led.configure(text=(\'on\' if LED_CMD[\'2\'] else \'off\'), bg=\'#E37C7E\') led.configure(bg=(\'#E37C7E\' if LED_CMD[\'2\'] else \'#F0F0F0\')) led_light.set(LED_LIGHT_CMD[\'2\']) led_light.config(label=\'LED亮度:%d\'%LED_LIGHT_CMD[\'2\']) else: tkinter.messagebox.askokcancel(\'提示\',\'No connect !\') led = Button(root, text=\'off\', bg=\'#F0F0F0\',\ font=(\'黑体\',16), width=6, height=1, relief=RAISED, command=led_button) led.place(x=165, y=145, width=40, height=30)
viii. LED亮度调节滑块
#============================LED_LIGHT========================= led_light = Scale(root, orient=HORIZONTAL, length=222, from_=0, to=100, font=(\'黑体\',12), \ label=\'LED亮度:%d\'%(0), tickinterval=20, resolution=1) led_light.place(x=14, y=280) def set_led_light(event): #调节LED亮度滑块触发事件 if mqttc.is_connected(): global LED_LIGHT_CMD, LED_CMD LED_LIGHT_CMD[\'2\'] = led_light.get() LED_CMD[\'2\'] = 0 if LED_LIGHT_CMD[\'2\']==0 else 1 led_light.config(label=\'LED亮度:%d\'%LED_LIGHT_CMD[\'2\']) mqttc.publish(pub_topic, payload=json.dumps(LED_LIGHT_CMD), qos=0) led.configure(text=(\'on\' if LED_CMD[\'2\'] else \'off\'), bg=\'#E37C7E\') led.configure(bg=(\'#E37C7E\' if LED_CMD[\'2\'] else \'#F0F0F0\')) else: tkinter.messagebox.askokcancel(\'提示\',\'No connect !\') led_light.bind(\'<ButtonRelease-1>\',set_led_light) #滑块绑定处理函数
ix. 蜂鸣器开关
#============================BEEP============================== Label(root, anchor=E, text=\'蜂鸣器 :\', bg=\'#98F555\', fg=\'blue\',\ font=(\'黑体\',16), width=6, height=1,\ relief=GROOVE).place(x=35, y=175, width=100, height=30) def beep_button(): #蜂鸣器开关按钮事件 if mqttc.is_connected(): global BEEP_CMD BEEP_CMD[\'2\'] = not BEEP_CMD[\'2\'] mqttc.publish(pub_topic, payload=json.dumps(BEEP_CMD), qos=0) beep.configure(text=(\'on\' if BEEP_CMD[\'2\'] else \'off\'), bg=\'#E37C7E\') beep.configure(bg=(\'#E37C7E\' if BEEP_CMD[\'2\'] else \'#F0F0F0\')) else: tkinter.messagebox.askokcancel(\'提示\',\'No connect !\') beep = Button(root, text=\'off\', bg=\'#F0F0F0\',\ font=(\'黑体\',16), width=6, height=1, relief=RAISED, command=beep_button) beep.place(x=165, y=175, width=40, height=30)
x. 连接、断开连接以及刷新按钮
#============================CONNECT========================== def connect_func(): #连接按钮 if not mqttc.is_connected(): mqttc.connect(host, port, 600) mqttc.subscribe(sub_topic, 0) mqttc.loop_start() else: tkinter.messagebox.askokcancel(\'提示\',\'Is connected !\') connect = Button(root, text=\'连接\', bg=\'#F0F0F0\',\ font=(\'黑体\',16), width=6, height=1, relief=RAISED, command=connect_func) connect.place(x=20, y=222, width=50, height=30) #============================DISCONNECT======================= def disconnect_func(): #断开连接按钮 if mqttc.is_connected(): if tkinter.messagebox.askokcancel(\'提示\',\'Do you sure disconnect?\'): mqttc.loop_stop() mqttc.disconnect() else: tkinter.messagebox.askokcancel(\'提示\',\'不断开连接你点个锤子 !\') else: tkinter.messagebox.askokcancel(\'提示\',\'No connect !\') disconnect = Button(root, text=\'断开连接\', bg=\'#F0F0F0\',\ font=(\'黑体\',16), width=6, height=1, relief=RAISED, command=disconnect_func) disconnect.place(x=75, y=222, width=100, height=30) #============================REFRESH=========================== def refresh_func(): #刷新数据按钮 if mqttc.is_connected(): mqttc.publish(pub_topic, payload=json.dumps({"1": "GET_MSG", "2": 1}), qos=0) else: tkinter.messagebox.askokcancel(\'提示\',\'No connect !\') refresh = Button(root, text=\'刷新\', bg=\'#F0F0F0\', \ font=(\'黑体\',16), width=6, height=1, relief=RAISED, command=refresh_func) refresh.place(x=180, y=222, width=50, height=30)
xi. Mqtt连接参数
# 连接参数 host = "nkqhkzm.mqtt.iot.gz.baidubce.com" # 服务器地址 port = 1883 # 通信端口 username = \'nkqhkzm/esp8266\' # 用户名 password = \'MC5asL3iW6HEaY5p\' # 密码 sub_topic = \'/light/deviceOut\' # 订阅主题名 pub_topic = \'/light/deviceIn\' # 发布主题名
xii. Mqtt相关回调函数
# 代理响应连接请求时调用 def on_connect(mqttc, obj, flags, rc): print("OnConnect, rc: "+str(rc)) if rc == 0: mqttc.publish(pub_topic, payload=json.dumps({"1": "GET_MSG", "2": 1}), qos=0) connect.configure(bg=\'#E37C7E\' ) tkinter.messagebox.askokcancel(\'提示\',\'Connect is succeeded !\') else: tkinter.messagebox.askokcancel(\'提示\',\'Connect is failed !\') # 当与代理断开连接时调用 def on_disconnect(mqttc, obj, rc): print("OnDisconnect, rc: "+str(rc)) connect.configure(bg=\'#F0F0F0\' ) tkinter.messagebox.askokcancel(\'提示\',\'Disconnect is succeeded !\') # 当使用使用publish()发送的消息已经传输到代理时被调用 def on_publish(mqttc, obj, mid): print("OnPublish, mid: "+str(mid)) # 当代理响应订阅请求时被调用 def on_subscribe(mqttc, obj, mid, granted_qos): print("Subscribed: "+str(mid)+" "+str(granted_qos)) # 当客户端有日志信息时调用 def on_log(mqttc, obj, level, string): print("Log:"+string) #当收到关于客户订阅的主题的消息时调用 def on_message(mqttc, obj, msg): strcurtime = time.strftime("%Y-%m-%d %H:%M:%S") print(strcurtime + ": " + msg.topic+" "+str(msg.qos)+" "+str(msg.payload)) on_exec(msg.payload) #指令全局变量 LED_CMD = {\'1\' : \'LED\', \'2\' : 0} LED_LIGHT_CMD = {\'1\' : \'LED_LIGHT\', \'2\' : 0} BEEP_CMD = {\'1\' : \'BUZZER\', \'2\' : 0} #订阅消息到达,消息处理函数 def on_exec(strcmd): cmd = json.loads(strcmd.decode()) if list(cmd.keys()) in [[\'1\', \'2\'], [\'2\', \'1\']]: if cmd[\'1\'] == \'LED_LIGHT\': global LED_CMD, led_light, LED_LIGHT_CMD if cmd[\'2\']: LED_CMD[\'2\'] = True LED_LIGHT_CMD[\'2\'] = cmd[\'2\'] led_light.set(cmd[\'2\']) led.configure(text=\'on\', bg=\'#E37C7E\') led.configure(bg=\'#E37C7E\') else: LED_CMD[\'2\'] = False led_light.set(cmd[\'2\']) led.configure(text=\'off\', bg=\'#E37C7E\') led.configure(bg=\'#F0F0F0\') elif cmd[\'1\'] == \'BUZZER\': global BEEP_CMD if cmd[\'2\']: BEEP_CMD[\'2\'] = True beep.configure(text=\'on\', bg=\'#E37C7E\') beep.configure(bg=\'#E37C7E\') else: BEEP_CMD[\'2\'] = False beep.configure(text=\'off\', bg=\'#E37C7E\') beep.configure(bg=\'#F0F0F0\') elif cmd[\'1\'] == \'ILLUMINATION\': if 0<= cmd[\'2\'] <= 100: light.configure(text=\'%d%%\'%cmd[\'2\']) else: light.configure(text=\'---\') elif cmd[\'1\'] == \'HUMIDITY\': temp.configure(text=\'%d°C\'%cmd[\'2\'][\'TEMP\']) humi.configure(text=\'%d%%RH\'%cmd[\'2\'][\'HUMI\']) elif cmd[\'1\'] == \'DOOR\': if cmd[\'2\']: door.configure(text=\'关闭\') else: door.configure(text=\'敞开\')
xiii. Mqtt参数赋值
1 mqttc = mqtt.Client("6666666") 2 mqttc.on_message = on_message #订阅消息到达 3 mqttc.on_connect = on_connect #连接回调函数 4 mqttc.on_disconnect = on_disconnect #断开连接回调 5 mqttc.on_publish = on_publish #发布消息回调 6 mqttc.on_subscribe = on_subscribe #订阅消息回调 7 mqttc.on_log = on_log #客户端有日志信息回调 8 mqttc.username_pw_set(username, password) #这只用户密码
4. 实验结果
1.1 设备端
图7.开机后效果
遮挡光电门,蜂鸣器会发声,而且PC端门的状态会显示关闭,PC端调节LED亮度
图8.遮挡光电门,调节LED亮度
1.2 PC端
连接状态连接按钮会变红,LED和蜂鸣器打开状态按钮也会变红。通过滑块调节LED亮度。
图9.PC端效果
5. 总结和展望
实现功能就是上面所介绍的。本项目提高了自己使用python模块编程的能力,以及加深了对MQTT通信协议的理解,同时会使用tkinter模块实现简单的UI界面,Get到了新的硬件编程方式,加强硬件编程能力,更加熟悉传感器的使用。本项目相对比较简单,不难理解,不过存在相当多的不足之处,比如esp8266断网后的重连问题、一些异常没有捕获进行处理等;同时功能也不够完事,如:设备低功耗模式、光照变化调节灯光功能、温湿度过高预警功能、开门关门记录时间功能等等。
参考文献:
[1] MicroPython针对ESP8266版的快速指南:Quick reference for the ESP8266
[3] Python paho.mqtt 模块使用和API分析