我一个学软件的小白想要找一个最简单的物联网案例跟着做,一直没找到。最后摸索了一番打算自己写一篇。关于细学物联网可以看太极创客,但还是要提一句,官方文档总是比大多数博客有用的。

基本框架

mqtt服务器作为消息中转,uni-app(vue)通过mqtt协议连接服务器,另一端esp01s也连接mqtt服务器,然后就可以进行数据传输。
这种通信方式主要用于控制指令的传输和实时数据显示,例如传感器数据可以通过mqtt直接显示到前端,但持久化需要搭建后端与mqtt服务器连接再将消息存到数据库,mqtt服务器不提供持久化服务。
在这里插入图片描述

为什么使用MQTT

mqtt协议是应用层协议,依然是基于TCP/IP的。

mqtt服务器的基本工作原理就是消息的订阅与发布,mqtt服务器被称为服务端,其他的设备连接到该服务器被称为客户端。在这里我们的uni-app前端和 esp01s在连接到服务器以后都被称为客户端。
客户端与客户端之间的消息传输靠的是订阅同一个主题,这个主题就类似于一个qq群,订阅就相当于加入这个群,发布就相当于在这个群里发消息,所以只要是订阅了同一个主题就能收到其他设备在该主题下发布的消息。当然一个设备可以订阅多个主题,一般是手机前端订阅多个设备的信息。主题的名字是自己取的,只要有人订阅了这个名字,这个主题就存在了。

如果订阅主题的设备意外断线了,mqtt服务器会将其他设备发布的数据保存在服务端,当该设备重新连上服务器后会继续将数据发送给该设备,相当于你上线看到群历史记录一样。
所以,
这可以很大程度保证在网络较差的环境下数据的稳定送达,并且两个客户端并不是必须要同时在线交流,相当于发微信一样发完可以继续干自己的事。非常适合物联网设备的数据通信。

战前准备

必要硬件:esp01s、配套继电器。
可选硬件:灯、电池、杜邦线。(如果你自己有电源、导线或者PCB板、目标设备)
工具:很小的一字螺丝刀(用于拧松继电器上的接线口螺丝)

esp01s

esp01s是esp8266系列wifi模块的一种,芯片与系列中其他型号相同,外部模块比较简约。
esp8266芯片源自于国内企业乐鑫科技,推荐从乐鑫旗下或者安信可旗下购买。
在这里插入图片描述

其他设备

要让模块在通电的时候做我们希望让它做的事需要对其进行编程,所以需要一个烧录器。
在这里插入图片描述
配套继电器
在这里插入图片描述
电池(电压需要大于继电器的标准电压才能让它工作)
在这里插入图片描述
杜邦线(线两头有细金属插头接口,分公母,其实在找到这线之前我甚至不知道这线叫什么名字)
在这里插入图片描述
LED灯(本来我搜的是发光二极管,不过这个引脚看着蛮舒服的,可以直接插杜邦线)
在这里插入图片描述

操作思路

  1. 安装EMQX(mqtt服务器)和MQTT X(mqtt电脑客户端)
  2. 安装Hbuilder,创建uni-app项目(使用Vue框架的)
  3. Vue连接EMQX
  4. 安装Arduino(用于编写硬件的编辑器)
  5. esp01s连接EMQX
  6. 连接硬件测试

安装EMQX、MQTT X

EMQX是一个mqtt服务器,你可以把它安装到本地linux系统上运行,但是我估计大部分人用的是windows,并且没有其他闲置的linux,除非你买个云服务器。
所以他们非常贴心的提供了免费的云EMQX,你可以直接通过连接他们现成的服务器进行消息中转。
如果你打算下载到本地安装可以看官方文档:快速开始 | EMQX文档
我有一个树莓派所以我是安装到了我的树莓派上。

MQTT X是一个可以直接在电脑上运行的客户端程序,可以用于测试发送和接收mqtt消息。
官网地址:MQTT X:优雅的跨平台 MQTT 5.0 桌面客户端工具

使用本地EMQX

EMQX文档中提到过Dashboard这个系统提供的可视化操作界面。服务器在运行以后默认自动启动Dashboard。
我们可以通过设备IP地址加18083访问到这个操作界面,用户名默认admin,密码默认public。
默认英文,可以在设置里转中文。
工具里的Websocket界面会默认显示你的服务器地址(192.168.1.109是我的树莓派地址)
不过是ws开头的 ws://192.168.1.109:8083/mqtt
如果是mqtt开头是 mqtt://192.168.1.109:1883
除了我的IP地址其他都是系统默认。
在这里插入图片描述

使用MQTT X

官方文档会引导你创建一个新的连接。就是点击侧边栏第二个加号,然后它默认显示的服务器地址就是他们官方提供的免费线上服务器。
名称自己填着能认就行。
Client ID用于让服务器识别你这个客户端的,这里随机生成的,不用管。
地址头有mqtt和ws两种,mqtts和wss是加密传输。(部分前端比如微信小程序官方要求加密传输所以必须要加s才行,但是官方提供的免费服务器只提供非加密传输,所以用微信小程序的建议自己搭服务器。)
用户名和密码暂时不填,会默认免密登录服务器,之后有实际需要再说。
在这里插入图片描述
我这里名称为remote_test的连接了免费远程服务器。然后点击添加订阅就可以添加订阅。
在这里插入图片描述
建议不要订阅默认的testtopic/#主题名,因为用的人很多,别人在这里发的内容你都会接收到,不适合我们自己测试。
在这里插入图片描述
是不是特别像一个聊天室…testtopic/后面跟#意思是只要是testtopic/开头的都可以被接收到。
#只会用于订阅的时候,发布时必须指明你发布到哪一个主题,不能是范围,比如我写的这个testtopic/down是要发布到的主题。
下面的Json格式数据是要发送的内容,Payload中可以选择数据格式,我用的是Json。
Qos是发送后对方如果无响应的重发策略,暂时不用管。
在这里插入图片描述

安装Hbuilder

使用uni-app是因为它可以直接跑在手机上,毕竟一般控制开关都是用手机的。
使用Hbuilder是因为可以快速生成一个uni-app项目,要真作为一个编辑器感觉不如VScode。

Hbuilder官网:HbuilderX-高效极客技巧
创建uni-app项目可以看官方文档的快速开始:uni-app官网

Vue连接mqtt服务器

需要先在Hbuilder底部的终端引入mqtt。目前已经有4.0.0版本,但是可能有小问题。

npm install mqtt@3.0.0

在这里我就只放一下js部分的连接脚本。
前端界面样式,发布和接收数据之后的逻辑你们可以自己设计。
这里我的state变量来自vuex。
与硬件端的代码有一些对应的地方,建议放在一起看。

<script scoped>
	
	export default {
		data() {
			return {
				//mqttHost: "192.168.1.109"  // 自己的服务器地址
				mqttHost: "broker.emqx.io", // 官方服务器地址
				mqttPort: 8083,
				mqttPath: "/mqtt",
				client: {}  // 代表我的客户端
			}
		},
		onLoad() {},
		// 注意在OnShow()中连接
		onShow() {
			this.mqttConnect()
		},
		methods: {
			mqttConnect() {
				var that = this
				
				// 连接服务器
				// 在其他地方你可能会看到引入connect的方式,但是那样在APP端可能无法连接服务器
				var mqtt = require('mqtt/dist/mqtt.js')
				// #ifdef APP-PLUS
				var pinf = plus.push.getClientInfo(); // 获取终端标识
				var clientId = pinf.clientid; //客户端标识
				const options = {
					port: that.mqttPort,
					clientId,
				};
				that.client = mqtt.connect("wx://" + that.mqttHost + that.mqttPath, options);
				//#endif
				// #ifdef H5
				const options = {
					port: that.mqttPort,
				};
				that.client = mqtt.connect("ws://" + that.mqttHost + that.mqttPath, options);
				//#endif
				
				// 如果连接成功执行函数
				that.client.on('connect', function(){
					console.log('connected')
					// 订阅主题并执行函数
					that.client.subscribe('testtopic/up/#', function(err){
						if(!err){
							console.log('get topic')
						}
					})
				})
				// 我设定设备会每隔一段时间发布一次自己的状态
				// 如果监听到主题执行函数
				that.client.on('message', function(topic,message){
					console.log(topic)
					// 我设计的主题末尾是设备的mac地址,这里是将mac地址赋值给id
					let id = ''
					id = topic.substr(topic.lastIndexOf("/") + 1)
					let data = {}
					data = JSON.parse(message)
					let set = {
						"id": id,
						"state": data.state
					}
					// 将前端该id的设备状态设为获取到的状态
					that.$store.dispatch('setState', set)  
				})
			},
			// 点击灯的开关按钮
			clickButton(item) {
				var that = this
				// 如果此时是开着的(state为true)
				if(item.state){
					// 发布关闭消息
					that.client.publish('testtopic/down/'+item.id,'{"switch": 0}',function(err){
						if(!err){
							console.log(item.name+' off')
							let set = {
								"id": item.id,
								"state": false
							}
							// 由于我设定设备会间隔一段时间发布一遍状态
							// 那么我们点击以后前端按钮不一定马上会做出反应
							// 所以我们不应该等待设备发状态再更改前端按钮的状态
							// 应该直接前端先改了,如果设备改状态失败,再通过设备发布的状态改回去
							that.$store.dispatch('setState', set)
						}
					})
				}else{
					// 发布开启消息
					that.client.publish('testtopic/down/'+item.id,'{"switch": 1}',function(err){
						if(!err){
							console.log(item.name+' on')
							let set = {
								"id": item.id,
								"state": true
							}
							that.$store.dispatch('setState', set)
						}
					})
				}
			},
		}
	}
</script>

安装Arduino

用于编写c语言硬件程序的编辑器,配合烧录器将程序传入硬件。
下载地址:Software | Arduino

基本配置

打开 文件 > 首选项
在这里插入图片描述
像这种项目文件不建议放c盘。
输出模式为编译。
管理器网址直接填这个
http://arduino.esp8266.com/stable/package_esp8266com_index.json
确认后关了重启。
请添加图片描述

esp8266开发板配置

打开 工具 > 开发板 > 开发板管理器…
在这里插入图片描述
搜索esp8266并安装。
请添加图片描述
选择开发板:NodeMCU 1.0,与esp01s通用。
在这里插入图片描述

ESP01s连接mqtt服务器

我是看这个了解了大概框架再改的:太极创客 | MQTT基础篇

Arduino引入mqtt相关库

使用mqtt需要先引入它的包。
打开 工具 > 管理库
在这里插入图片描述
搜索PubSubClient,可能需要往下翻一下。
请添加图片描述

Arduino引入Json操作相关库

由于我是以Json格式的数据发布的,所以在esp01s接收到以后需要会处理Json数据。
请添加图片描述

Aduino源码

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <Ticker.h>
#include <ArduinoJson.h>
 
// 设置wifi接入信息(请根据您的WiFi信息进行修改)
const char* ssid = "This is not 516";
const char* password = "zz888666";
const char* mqttServer = "192.168.1.109";  //可替换官方提供的域名:broker.emqx.io
// 如以上MQTT服务器无法正常连接,请前往以下页面寻找解决方案
// http://www.taichi-maker.com/public-mqtt-broker/
 
Ticker ticker;
WiFiClient wifiClient;
PubSubClient mqttClient(wifiClient);
 
int count;    // Ticker计数用变量
int pin = 0;  // 设置一个变量代表控制继电器开关的GPIO

// 通电初始化函数
void setup() {
  Serial.begin(9600);  // 波特率
 
  //pinMode(LED_BUILTIN, OUTPUT);  // 这里换成变量
  pinMode(pin, OUTPUT); 
  digitalWrite(pin, LOW);  // GPIO0 低电平,吸合继电器,作为初始状态
 
  // Ticker定时对象
  ticker.attach(1, tickerCount);
  
  //设置ESP8266工作模式为无线终端模式
  WiFi.mode(WIFI_STA);
  
  // 连接WiFi
  connectWifi();
  
  // 设置MQTT服务器和端口号
  mqttClient.setServer(mqttServer, 1883);
  mqttClient.setCallback(receiveCallback);
 
  // 连接MQTT服务器
  connectMQTTServer();
}

 // 状态持续函数,一直重复执行
void loop() {
  if (mqttClient.connected()) { // 如果开发板成功连接服务器
    // 每隔3秒钟发布一次信息,一般用于状态报告
    if (count >= 3){
      pubMQTTmsg();
      count = 0;
    }    
    // 保持心跳
    mqttClient.loop();
  } else {                  // 如果开发板未能成功连接服务器
    connectMQTTServer();    // 则尝试连接服务器
  }
}
 
void tickerCount(){
  count++;
}
 // 连接MQTT服务器
void connectMQTTServer(){
  // 根据ESP8266的MAC地址生成客户端ID(避免与其它ESP8266的客户端ID重名)
  String clientId = "esp8266-" + WiFi.macAddress();
 
  // 连接MQTT服务器
  if (mqttClient.connect(clientId.c_str())) { 
    Serial.println("MQTT Server Connected.");
    Serial.print("Server Address: ");
    Serial.println(mqttServer);
    Serial.print("ClientId: ");
    Serial.println(clientId);
    subscribeTopic(); // 订阅指定主题
  } else {
    Serial.print("MQTT Server Connect Failed. Client State:");
    Serial.println(mqttClient.state());
    delay(3000);
  }   
}
 
// 发布信息
void pubMQTTmsg(){
  // 建立发布主题。
  String topicString = "testtopic/up/"+WiFi.macAddress();  // 上行主题,设备到控制端
  
  char publishTopic[topicString.length() + 1];  
  strcpy(publishTopic, topicString.c_str());
 
  // 定时向服务器主题发布当前GPIO0引脚状态
  String state;
  if(digitalRead(0)){
    state = "{\"state\": false}";  // 高电平断开继电器
  } else {
    state = "{\"state\": true}";  // 低电平吸合继电器
  }
  // 打包为JSON,但是我发现message变成了null,所以放弃了,直接上面写JSON...
//  DynamicJsonDocument doc(1024);
//  JsonObject obj = doc.as<JsonObject>();
//  obj["state"] = state; 
//  String message;
//  serializeJson(doc, message);

  char publishMsg[state.length() + 1];   
  strcpy(publishMsg, state.c_str());

  // 实现ESP8266向主题发布信息
  if(mqttClient.publish(publishTopic, publishMsg)){
    Serial.print("Publish Topic: ");Serial.println(publishTopic);
    Serial.print("Publish message: ");Serial.println(publishMsg);    
  } else {
    Serial.println("Message Publish Failed."); 
  }
}
 
// 订阅指定主题
void subscribeTopic(){
 
  // 建立订阅主题。
  String topicString = "testtopic/down/"+WiFi.macAddress();  // 下行主题,控制端到设备
  char subTopic[topicString.length() + 1];  
  strcpy(subTopic, topicString.c_str());
  
  // 通过串口监视器输出是否成功订阅主题以及订阅的主题名称
  if(mqttClient.subscribe(subTopic)){
    Serial.print("Subscribe Topic: ");
    Serial.println(subTopic);
  } else {
    Serial.print("Subscribe Fail...");
  }  
}
 
// 收到信息后的回调函数
void receiveCallback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message Received [");
  Serial.print(topic);
  Serial.println("] ");
  // 将获取到的pyte类payload消息转换为String类型的data
  String data = "";
  for (int i = 0; i < length; i++) {
    data = data + (char)payload[i];
  }
  Serial.println(data);
  // 一般我们会发布JSON格式的消息,所以就需要解析获取JSON中的数据
  // 声明你的json数据中存在几个对象,如果有list列表,需要+ JSON_ARRAY_SIZE(1),这里我的json只有{"switch":1}
  const size_t capacity = JSON_OBJECT_SIZE(1) + 60;  
  StaticJsonDocument<capacity> jsonBuffer;
  // Parse JSON object解析 JSON
  deserializeJson(jsonBuffer, data);
  JsonObject object = jsonBuffer.as<JsonObject>();
  Serial.println(object["switch"].as<String>());
//  Serial.println("");
//  Serial.print("Message Length(Bytes) ");
//  Serial.println(length);
  
  if (object["switch"].as<String>() == "1") {     // 如果收到的信息为"1",注意双引号为String,单引号为char
    //digitalWrite(LED_BUILTIN, LOW);  // 则点亮LED。
    digitalWrite(pin, LOW);  // GPIO0 低电平,吸合继电器
  } else {                           
    //digitalWrite(LED_BUILTIN, HIGH); // 否则熄灭LED。
    digitalWrite(pin, HIGH); // GPIO0 高电平,断开继电器
  }
}
 
// ESP8266连接wifi
void connectWifi(){
 
  WiFi.begin(ssid, password);
 
  //等待WiFi连接,成功连接后输出成功信息
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi Connected!");  
  Serial.println(""); 
}

上传到硬件

这个时候你需要将esp01s插在烧录器上,再把烧录器插到电脑上。
查看你的串口是否识别到。
在这里插入图片描述
如果有就可以点击左上角箭头上传到硬件。
在这里插入图片描述

源码中的println函数会将数据输出到串口监视器。
在这里插入图片描述

上传成功后就会开始println输出,并且发布自己开关的状态到mqtt服务器。
在这里插入图片描述

此时你可以在客户端看到硬件发布的内容。
在这里插入图片描述

然后你可以测试用客户端发送关闭消息。此时它定时发布的GPIO 0引脚就变成高电平。
在这里插入图片描述

硬件演示

把esp01s插到继电器上就能用来控制开合了。

继电器背面标了接口符号。VCC:电源,GND:接地。
GPIO 0为低电平时(开):COM与NO闭合。
GPIO 0为高电平时(关):COM与NC闭合。

然后我另外从电池接了线出来给LED灯供电来模拟开关灯。
yanshi

关于mqtt主题

可能会有人和我一样奇怪为什么上行下行的数据要用两个主题,不是一个主题两边都能发数据吗。
于是我自己试了一下发布和订阅同一个主题,在MQTT X客户端上我们可以看到如果在自己已经订阅的主题中发布信息,会同时在该主题中收到自己发布的信息。
那么如果两边用同一个主题交换消息,两边就都需要识别每一个消息是对方发的还是自己发的,识别方法可能就是在Payload中多写一个数据标识是谁发的。这明显会增加很多的判断成本。
按照这样一个思路,一个主题最好的使用方法应该是一个发布者对应多个订阅者或者一个订阅者对应多个发布者。
但是同为客户端,也分为手机控制端和物联网IoT设备。
那么在不考虑IoT设备之间互相订阅的前提下,一个IoT设备,应该都会有一个主题是自己独占用来发布的,多个控制端可以订阅这个主题,同时有一个主题是独占用来接收控制信息的,多个控制端可以发布信息到这个主题。而一个控制端,与每一个IoT设备之间都会有一个发布主题和订阅主题,不存在自己独占的发布或订阅主题。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐