作者:禅与计算机程序设计艺术
1.简介
物联网(IoT)已经逐渐成为人们生活中的不可或缺的一部分,它不仅可以帮助我们收集、存储和传输数据,还能够让我们的生活变得更加智能化、便捷化。通过对现实世界中各种设备的信息采集及处理,可以帮助我们进行智慧城市建设、智慧农业、智慧工厂、智慧医疗等应用,甚至可能为我们的生活带来全新的体验。而作为一名程序员,要想掌握物联网技术,就需要从基础的硬件知识、网络协议、嵌入式系统、编程语言等方面都有所积累。本文将以较为广泛的范围来介绍一下如何学习并应用物联网技术,希望能帮助读者在实际工作中有所收获。
2.背景介绍
2.1 什么是物联网?
物联网(Internet of Things, IoT)是一个利用互联网技术与传感器相结合的新型网络,能够收集、整理和交换大量信息。它利用计算机网络、微控制器、传感器、集成电路、无线电波、通讯接口等物理装置以及电子设备实现数据的收集和处理。最早由英国剑桥分析公司发明,并于2011年正式启动了产业革命。随着IoT的普及,越来越多的人们开始关注和使用IoT技术来增强自身的能力、提升效率、改善生活品质。
2.2 为什么要学习物联网技术?
物联网技术的兴起促进了经济发展,尤其是在移动互联网(Mobile Internet)和云计算(Cloud Computing)的背景下。随着物联网技术的普及,越来越多的应用场景被激活,如智慧城市、智慧农业、智慧工厂、智慧医疗、智慧教育、智慧住房、智慧社会。要想真正发展自己的业务,就必须把握好“边缘计算”这一重要的分支领域。
物联网技术通过连接各类传感器设备、路由器、服务器、终端设备、基站等,能够实时地收集、汇总、处理、分析和呈现大量的数据。因此,掌握物联网技术对于个人开发者、企业管理者、创业公司和初创型企业都十分重要。
3.基本概念术语说明
3.1 概念:
(1)物理层 Physical Layer: 在物理层,物联网通信的双方都要具备一定的物理特性,比如双方之间的距离、布置位置、线缆颜色、接头方式等。此外,也会受到周围环境的影响,比如电磁干扰、雷击、恶劣天气等。因此,在物理层上需要考虑传输过程中产生的噪声、失真、衰减等问题。
(2)数据链路层 Data Link Layer: 数据链路层用来建立可靠、无差错的、双向的通信信道,通过物理层传送数据。主要功能包括错误检测、改错、流控、重发控制、帧结构、计时、确认和准备发送等。
(3)网络层 Network Layer: 网络层负责为数据包分配路径,选择转发节点,保证数据在传输过程中不丢失、不乱序和不重复。主要功能包括地址寻址、路由选择、拥塞控制等。
(4)传输层 Transport Layer: 传输层提供不同应用程序之间的数据传输服务,包括端到端的可靠性传输、差错纠正、流量控制、连接管理、多播、窗口大小、选择确认、优先级、会话等。
(5)会话层 Session Layer: 会话层用于建立、维护、取消和监视应用间的会话连接。主要功能包括会话控制、同步、回呼、撤销、确认和应答等。
(6)表示层 Presentation Layer: 表示层用于对信息的编码和解码,使通信双方都能理解彼此的消息。主要功能包括字符编码、加密解密、压缩和数据格式转换等。
(7)应用层 Application Layer: 应用层为用户提供了各种服务,包括文件传输、电子邮件、即时通信、远程访问等。应用层也为开发者提供了各种接口,可以通过该接口调用系统底层功能。
3.2 术语:
(1)协议 Protocol: 是定义两个或多个结点之间交换数据的规则、格式以及保障通信安全的一种手段。
(2)TCP/IP协议族 TCP/IP protocol suite: 由一系列协议组成的网络互连的标准协议集合,其中包括传输控制协议/ internet协议 (Transmission Control Protocol/Internet Protocol)。
(3)TCP Connection: TCP连接是指建立在Internet Protocol (IP) 上的可靠的、基于字节流的、双向的通信信道,使用TCP协议。一个TCP连接通常是一条两端点之间的逻辑链路。
(4)TCP/IP 模型 TCP/IP model: 是网络通信模型的一种抽象,它将网络通信分为七个层次,每一层完成特定的功能,并且能完整实现计算机网络的通信功能。
(5)HTTP Hypertext Transfer Protocol: 是Web上使用的协议,使用TCP/IP模型,默认端口号为80。HTTP协议属于应用层协议,它规定客户端和服务器如何进行通信。
(6)HTTPS Hypertext Transfer Protocol Secure: 是为了防止网络上传输敏感信息而设计的协议。HTTPS与HTTP的区别在于,HTTPS经过SSL或TLS协议加密后才能正常访问。
(7)REST Representational State Transfer: REST是目前最流行的互联网软件架构风格,它定义了一组基于URL的标准,旨在提高互联网软件的可伸缩性、可用性、互用性。
(8)JSON JavaScript Object Notation: 是一种轻量级的数据交换格式,易于使用且解析速度快。
(9)API Application Programming Interface: 是提供应用程序与程序库或其他服务之间的接口。
(10)RESTful API Restful architecture: 是符合REST规范的API,RESTful架构要求每个URI代表一种资源,客户端和服务器之间,传递这种资源的状态变迹应该由HTTP动词(GET、POST、PUT、DELETE、PATCH等)来表述。
(11)MQTT Message Queuing Telemetry Transport: 是一种基于发布/订阅模式的“轻量级”“可发布/订阅”消息传输协议。
4.核心算法原理和具体操作步骤以及数学公式讲解
4.1 WiFi模块工作原理
WiFi模块(Wi-Fi module)主要由四部分组成:Transmitter, Receiver, Transceiver, Antenna。
- Transmitter:负责发射无线信号,可以接收来自外部源的信号并通过天线发出。
- Receiver:负责接收无线信号,将其转换为数字信号并存储到接收缓存中。
- Transceiver:负责接收来自上位机的命令,并将它们翻译成信号。同时也可以将信号转换为数字信号。
- Antenna:用于将发射出的无线信号转换为电磁能。
当主机想要发射数据时,首先创建一个数据帧并附加上一些额外的控制信息(比如MAC地址、QoS等)。然后,将该帧作为载荷附加到待发送的IEEE802.11数据包上。最后,将数据包放入待发送队列,等待被调制解调器处理。
4.2 MQTT协议
MQTT(Message Queue Telemetry Transport)协议是IBM公司与遵循MQTT规范的第三方供应商一起推出的物联网通信协议。
4.2.1 MQTT协议特点
- MQTT支持QoS 0~2级别的发布/订阅消息机制,保证了数据可靠性。
- 支持TCP和TLS加密协议,实现了通信安全性。
- 支持消息发布、订阅、推送等功能,实现了物联网通信的功能特性。
4.2.2 MQTT协议消息格式
MQTT协议采用如下的消息格式:
PUBLISH | TOPIC | PAYLOAD
SUBSCRIBE | TOPIC | QOS
UNSUBSCRIBE | TOPIC
PINGREQ |
PINGRESP |
DISCONNECT |
TOPIC:主题名称,订阅消息的时候使用
PAYLOAD:发布消息时的负载数据
QOS:Quality of Service,指消息服务质量,共三种取值:QoS=0,最多一次;QoS=1,至少一次;QoS=2,只有一次
MQTT协议的发布消息格式为:
0x30 | LENGTH | PACKET ID | PUBLISH MSG | [RETAIN] [DUP] [QoS] [DUP][PacketID]
- LENGTH:包含整个报文长度字段的两个字节,后面的内容全部按这个长度截取;
- PACKET ID:给每条报文都分配的一个唯一标识符,用来保证报文顺序和完整性;
- RETAIN:保留标志位,默认为0,代表不保留消息;如果设置成1则表示服务器必须保留消息。
- DUP:复制标志位,默认为0,代表该消息不必存储多份副本。如果设置成1,代表消息需要存储多份副本。
- QoS:指定发布消息的服务质量等级。取值为0、1或2,分别对应最多一次、至少一次、只有一次的服务质量。
MQTT协议的订阅消息格式为:
0x82 | MSG ID MSB | MSG ID LSB | SUBSCRIBE MSG
- MSG ID:订阅报文的标识符。
- SUBSCRIBE MSG:指定订阅的主题列表和对应的服务质量。
MQTT协议的发布确认消息格式为:
0x40 | MSG ID MSB | MSG ID LSB | PUBACK MSG
- MSG ID:发布报文的标识符。
- PUBACK MSG:确认发布成功。
MQTT协议的取消订阅消息格式为:
0xA2 | MSG ID MSB | MSG ID LSB | UNSUBSCRIBE MSG
- MSG ID:取消订阅报文的标识符。
- UNSUBSCRIBE MSG:指定取消订阅的主题列表。
MQTT协议的PING请求消息格式为:
0xC0 | PINGREQ MSG
- PINGREQ MSG:用于客户端与服务器之间的心跳检查。
MQTT协议的PING响应消息格式为:
0xD0 | PINGRESP MSG
- PINGRESP MSG:响应服务器发送的PING请求。
MQTT协议的断开连接消息格式为:
0xE0 | DISCONNECT MSG
- DISCONNECT MSG:通知服务器关闭当前连接。
4.3 NodeMCU架构
NodeMCU是基于ESP8266微控制器的开源物联网开发板,可编程入门级开发板,具有良好的可扩展性,适用于各种物联网项目。NodeMCU本身采用Lua语言编写,具有高灵活性、易上手、开发速度快、成本低、免费开源等优点。
4.3.1 ESP8266芯片
ESP8266(Espressif Systems 8266),是由乐鑫信息科技(中国)有限公司推出的非常便宜的单片机SOC(System on a Chip),其核心处理器是以太网控制器,可以实现TCP/IP协议栈,支持Wi-Fi、蓝牙、低功耗等物联网通信功能。目前市场上存在很多基于ESP8266的开发板,价格都在几块钱左右。
4.3.2 NodeMCU开发板原理图
NodeMCU的开发板布局主要分为以下几个部分:
- USB接口:用于给NodeMCU开发板供电;
- GPIO接口:用于连接主控板外部元器件;
- I2C接口:用于连接模拟传感器、IIC外设;
- SPI接口:用于连接SSD1306显示屏、DHT11温湿度传感器等;
- UART接口:用于连接串口调试器,传输数据;
- EN和RST按钮:用于切换开发板的使能状态、复位开发板;
- 电源LED:用于指示当前电源状态;
- LED矩阵:用于显示文字和图像;
- RGB LED:连接到GPIO接口上,可独立驱动;
- 天线接口:连接外部天线。
4.3.3 NodeMCU开发板软硬件连接方法
- 电源接法:使用USB直连到开发板的5V/GND接口,并将EN和RST按下,使开发板工作状态下。
- Wi-Fi接法:根据所选的版本,连接Wi-Fi热点,并获取AP的SSID密码信息。
- 串口调试接法:将TXD连接到ESP8266的DIO13,RXD连接到ESP8266的DIO14接口,并接到UART接口的UART1。
4.4 ESP8266 AT指令集
AT指令集(Automated Terminal Communication Operations Codes,即自动通信操纵代码)是基于Telnet、串口等简单接口的命令集,用于AT命令交互式界面操作。对于需要频繁地发送指令进行配置的设备来说,AT指令集十分方便。
NodeMCU开发板支持的AT指令集如下表所示:
AT指令 | 描述 |
---|---|
AT | 测试AT指令是否可用 |
AT+CWMODE | 设置设备的工作模式 |
AT+CWQAP | 查询无线网络接入点的名称 |
AT+CWJAP | 连接指定的无线网络 |
AT+CWLAPOPT | 配置无线网络选项 |
AT+CIPMUX | 设置多连接模式 |
AT+CIPSTATUS | 查看当前的TCP连接状态 |
AT+CIPSTART | 创建TCP连接 |
AT+CIPSEND | 发送数据 |
AT+CIPOPEN | 创建UDP连接 |
AT+CWDHCP | 获取局域网IP地址 |
AT+CIFSR | 获取IP地址 |
AT+CWAUTOCONN | 开启AP自动连接 |
AT+CIPSHUT | 关闭TCP或UDP连接 |
AT+CWHOSTNAME | 修改主机名称 |
AT+CIPDNS | 设置域名服务器地址 |
AT+CWLAP | 查看Wi-Fi列表 |
AT+SYSRAM | 设置系统RAM空间 |
AT+SYSMSG | 打印系统日志 |
AT+RESET | 重启设备 |
AT+UART | 设置串口波特率 |
AT+SLEEP | 进入睡眠模式 |
AT&W | 保存设置并重启 |
5.具体代码实例和解释说明
5.1 Hello World!
创建并打开一个文本编辑器,输入如下的代码:
-- 连接wifi
dofile("connect_wifi.lc") -- 引用连接wifi的lua脚本
-- 初始化mqtt连接
mqttclient = nil
dofile("init_mqtt.lc") -- 引用初始化mqtt的lua脚本
-- 订阅topic
subscribeTopic()
-- 循环检测mqtt消息
tmr.alarm(0, 1000, tmr.ALARM_AUTO, checkMqttMsg)
以上代码包含两部分,第一部分是连接Wi-Fi和初始化MQTT连接的过程,第二部分是执行MQTT订阅、定时器检测MQTT消息的过程。
我们先来看第一个部分——连接Wi-Fi。该部分的作用是通过引用另一个lua脚本connect_wifi.lc
,实现连接指定的Wi-Fi网络。
在connect_wifi.lc
中写入如下代码:
WIFI_SSID="Your Wifi Name"
WIFI_PWD="<PASSWORD>"
wifi.setmode(wifi.STATION)
station_cfg={}
station_cfg.ssid=WIFI_SSID
station_cfg.pwd=<PASSWORD>
station_cfg.auto=true
station_cfg.save=true
wifi.sta.config(station_cfg)
wifi.eventmon.register(wifi.EVENT_STA_CONNECTED, function(T) print("\n\tSTA Connected to:"..T.SSID.." IP:",T.IP) end)
wifi.eventmon.register(wifi.EVENT_STATION_GOT_IP, function(T)
dofile("init_mqtt.lc")
subscribeTopic()
end)
wifi.eventmon.register(wifi.EVENT_STA_DISCONNECTED, function(T)
if string.find(T.reason,"AP closed.") then
wifi.sta.disconnect()
tmr.delay(3000000) -- wait for 3 seconds before attempting to reconnect
connectToWifi()
else
print("\n\tSTA Disconnected from ".. T.SSID.. "\nReason: ".. T.reason)
node.restart()
end
end)
print("Connecting to ".. WIFI_SSID.. "...")
wifi.sta.connect()
tmr.create():alarm(10000, tmr.ALARM_SINGLE, function() print("Connection timeout!") end)
这里设置了Wi-Fi的账号和密码,并设置了Wi-Fi模式为客户端模式。然后,通过wifi.sta.config()
方法设置账号和密码信息,并将auto
设置为true
,这样在开机时可以自动连接指定的Wi-Fi。同时,设置save
参数为true
,这样当掉线后就可以自动连接上。
接着,注册三个回调函数,分别监听Wi-Fi的连接、获取IP地址、断开连接事件。当Wi-Fi连接上时,就会触发wifi.EVENT_STATION_GOT_IP
事件,然后引用init_mqtt.lc
和subscribeTopic()
函数,实现初始化MQTT连接和订阅指定的topic。
当Wi-Fi连接失败或者意外断开连接时,会触发相应的回调函数。当出现意外断开连接原因为“AP已关闭”,就会重新尝试连接,否则会重启整个设备。
连接超时的判断和超时后的重连,是为了避免因为网络问题导致的连接不稳定。
第二部分——初始化MQTT连接和订阅Topic,同样也是引用另一个lua脚本init_mqtt.lc
。
在init_mqtt.lc
中写入如下代码:
-- mqtt config
local mqttHost = "your broker ip address or domain name"
local mqttPort = your port number
local mqttId = "your client id"
local mqttUser = ""
local mqttPwd = ""
-- create mqtt client and configure it
mqttclient = mqtt.Client(mqttId, 120)
mqttclient:lwt("/lwt", "offline", 0, 0)
mqttclient:on("message", onMqttMsgReceived)
mqttclient:on("offline", onMqttDisconnected)
mqttclient:username_pw_set(mqttUser, mqttPwd)
mqttclient:connect(mqttHost, mqttPort, 0, 1, function(client)
print("\nMQTT connected")
client:subscribe("/your topic/#", 0)
end)
这里设置了MQTT的相关配置,包括broker的IP地址或域名,端口号,客户端ID,用户名和密码。然后,创建了一个MQTT客户端对象,并注册了两个回调函数。其中,on("message")
用于处理收到的MQTT消息;on("offline")
用于处理MQTT连接断开。
最后,调用connect()
方法连接到MQTT Broker,并订阅指定的topic。
第三部分——订阅Topic。
这里订阅的是一个通配符topic,以"/your topic/#"
的方式订阅所有的以"/your topic/"
开头的topic。
第四部分——定时器检测MQTT消息。
为了实现定时检测MQTT消息,我们设置了一个定时器,周期为1秒,使用tmr.ALARM_AUTO
模式,并传入checkMqttMsg
函数作为回调函数。该函数的内容比较简单,就是读取MQTT Client对象的消息队列,并逐一处理。
下面再详细说一下MQTT消息处理的流程:
- 当订阅的topic收到新的消息时,MQTT Client会发送消息到指定的callback函数,该函数的第一个参数为topic;
- 根据topic前缀过滤,将收到的消息转发给对应的handler,例如如果收到的topic为
/your topic/data
,则会转发给handler/your topic/data
; - handler负责处理收到的消息,包括解析、校验、处理等;
- 如果handler处理完毕后没有返回任何数据,则默认返回
nil
。
5.2 订阅和处理消息
订阅和处理消息主要依赖于MQTT协议的订阅和发布机制。由于MQTT协议的QoS级别,只能确保消息至少被消费一次。如果要确保消息被消费完全,可以使用RabbitMQ等中间件,实现队列的概念。
这里以NodeMCU为例,编写一个handler。handler的名字为"/your topic/data"
,对应的topic为"/your topic/data"
。
在handler中编写如下代码:
function handleData(msg)
local payloadStr = msg:getPayload()
local dataArr = cjson.decode(payloadStr)
-- process received data...
end
return handleData
该handler接收一个MQTT消息对象,并获得消息的负载数据。这里通过cjson.decode()
方法将负载数据转换成Lua table数组。
之后,可以处理table数组中的数据,做一些逻辑处理或运算。这里只是举例,并没有真正处理数据。
最后,在main.lua
中引入该handler,并订阅它:
dofile("handle_data.lc") -- 引用handle_data.lc
mqttclient:subscribe("/your topic/data", 0, function(client)
print("Subscribed to /your topic/data")
client:on("message", "/your topic/data", handleData)
end)
这里调用subscribe()
方法订阅指定topic,并注册一个回调函数,用于处理收到的消息。在回调函数中,将收到的消息传入handleData()
函数处理。
这样,NodeMCU只需要订阅一个topic,就可以收到它的所有消息,并根据消息的topic选择相应的handler进行处理。