自动接触nodeMCU后发现8266是一个非常好的物联网开发Wi-Fi模块,因此就想把MQTT通讯协议在上面运行起来做些简单的事情。

开发目标

  1. 将nodeMCU作为一个MQTT的客户端运行。
  2. 使用PubSubClient这个MQTT协议实现。
  3. 程序每次启动后先把将芯片设置到STA模式下,并连接指定的WI-FI路由器。
  4. 连接建立好之后连接指定的MQTT服务器,并注册从服务器接收数据的topic,并定期将数据(uptime)发送到服务器端指定的topic上。
    • 上传topic:”MAC地址/uplink”
    • 接收topic:”MAC地址/downlink”
  5. 当收到downlink消息后把数据进行解析并执行,目前只支持’blink’命令。该命令可以把芯片上的LED灯按照指定的次数点亮的简单操作。

程序开发

程序启动设置

程序启动的入口为’setup()‘函数,这个函数做几件事情:

  1. 设置LED控制PIN模式
  2. 将WI-FI芯片设置到STA运行模式,并连接Wi-Fi路由器
  3. 连接MQTT服务器,监听’downlink’消息

程序片段解析:

void setup() {
  Serial.begin(BAUD_RATE);
  pinMode(LED_PIN, OUTPUT);  // 设置LED控制端口

  WiFi.disconnect();
  WiFi.mode(WIFI_STA);       // 设置启动为STA模式
  WiFi.setAutoConnect(true);
  WiFi.begin(wifi_ssid, wifi_pwd);  // 连接Wi-Fi路由器

  Serial.printf("Connecting to AP(%s), password(%s)\n", wifi_ssid, wifi_pwd);
  while (WL_CONNECTED != WiFi.status()) {
    Serial.print(".");
    blink(BLINK_SLOWLY);
  }
  Serial.printf("\nWifi connection is setup!\n");
  Serial.printf("MAC: %s, IP: %s\n", WiFi.macAddress().c_str(), WiFi.localIP().toString().c_str());

  while (!setup_mqtt_connection()) {   // 连接MQTT服务器
    Serial.print(".");
    blink(BLINK_SLOWLY);
  }
}

连接MQTT服务器

这个方法的工作就是建立连接并且注册接收的topic。

bool setup_mqtt_connection()
{
  char client_id[CLIENT_ID_LEN];

  snprintf(client_id, CLIENT_ID_LEN, "%s", WiFi.macAddress().c_str());
  Serial.printf("Client[%s] is connecting MQTT server!\n", client_id);
  mqtt_connected = mq_client.connect(client_id);  // 连接MQTT服务器
  if (!mqtt_connected) {
    Serial.println("MQTT connection failure");
    return false;
  }

  mq_client.setCallback(mqtt_callback);  // 指定接收downlink消息的处理函数

  memset(uplink_topic, 0, TOPIC_LEN);
  snprintf(uplink_topic, TOPIC_LEN, "%s/uplink", WiFi.macAddress().c_str());
  
  memset(downlink_topic, 0, TOPIC_LEN);
  snprintf(downlink_topic, TOPIC_LEN, "%s/downlink", WiFi.macAddress().c_str());
  Serial.printf("Subscribing to topic: %s\n", downlink_topic);
  mqtt_connected = mq_client.subscribe(downlink_topic);  // 注册接收downlink消息
  if (!mqtt_connected) {
    Serial.println("MQTT connection failure");
    return false;
  }

  Serial.println("MQTT connection is setup");
  return true;
}

接收消息处理

MQTT消息处理函数遵循PubSubClient的接口开发就行了。

void mqtt_callback(char *topic, uint8_t* buffer, unsigned int len) {
  blink(BLINK_QUICKLY);
  memset(recv_buffer, 0, RECV_BUFFER_LEN);
  strncpy(recv_buffer, (char *)buffer, (RECV_BUFFER_LEN<len ? RECV_BUFFER_LEN-1:len));
  Serial.printf("Received [topic:%s]:%s\n", topic, (char*)recv_buffer);
  parse_cmd((char *)recv_buffer);
}

但这里需要注意’len’这个参数的使用细节。’len’这个参数表明接收到的消息的实际长度,最好在处理函数中将数据复制出来后进行处理。最开始我没有这样处理,结果发现’buffer’中的数据会包含发出数据的信息,研究了一下源代码发现PubSubClient的发出/接收缓冲区是共用的,而且发出/接收后都不会重置。另外需要注意的是这个缓冲区并不大,默认为MQTT_MAX_PACKET_SIZE(128)个子节 。

class PubSubClient {
   ...
   uint8_t buffer[MQTT_MAX_PACKET_SIZE];  // PubSubClient中的数据共用缓冲区

主循环

程序主循环的主要任务就是数据周期发送并处理MQTT消息接收

void loop() {
  // send message every 10 second
  if (millis() - last_uplink_tick >= UPLINK_INTERVAL) {
    memset(send_buffer, 0, SEND_BUFFER_LEN);
    snprintf(send_buffer, SEND_BUFFER_LEN, "Client[%s@%s]: uptime:%ld",
             WiFi.macAddress().c_str(),
             WiFi.localIP().toString().c_str(),
             millis());
    Serial.printf("Sending: %s\n", send_buffer);
    blink(BLINK_QUICKLY);
    mq_client.publish(uplink_topic, send_buffer);  // 发送数据到MQTT服务器
    last_uplink_tick = millis();
  }

  mq_client.loop();
}

使用方法

编译

我使用Arduino IDE进行开发,这是一个蛮不错的开发环境。不熟悉的人可以参考我另外一篇“使用Arduino IDE进行nodeMCU开发”的blog。

运行

nodeMCU客户端

将程序烧入nodeMCU后每次只要通电程序就会自动运行。启动后程序有以下类似输出。

Connecting to AP(your_wifi_ssid), password(your_wifi_password)
....
Wifi connection is setup!
MAC: A0:20:A6:18:47:F1, IP: 192.168.102.102
Client[A0:20:A6:18:47:F1] is connecting MQTT server!
Subscribing to topic: A0:20:A6:18:47:F1/downlink
MQTT connection is setup
Sending: Client[A0:20:A6:18:47:F1@192.168.102.102]: uptime:21959   <-- 上传数据
Sending: Client[A0:20:A6:18:47:F1@192.168.102.102]: uptime:32036
Sending: Client[A0:20:A6:18:47:F1@192.168.102.102]: uptime:42077
Sending: Client[A0:20:A6:18:47:F1@192.168.102.102]: uptime:52160
Sending: Client[A0:20:A6:18:47:F1@192.168.102.102]: uptime:62233
Received [topic:A0:20:A6:18:47:F1/downlink]:#blink#3#      <-- 接收控制命令
Received [topic:A0:20:A6:18:47:F1/downlink]:bli     <-- 接收到非法命令
Received unknown command: bli

数据接收端

简单运行可以使用’mosquitto_sub’。命令可以参照下面的写法,需要用’-h’指定你的MQTT服务器地址,用’-t’指定接收的topic,这个topic会在nodeMCU每次运行时在串口输出,nodeMCU的MAC地址也会从串口输出。

mosquitto_sub -h "your_mqtt_server" -t "MAC_ADDRESS/uplink"

我直接是在MQTT服务器上运行数据接收端,因此我实际运行的命令如下:

$ mosquitto_sub -t "A0:20:A6:18:47/uplink"

远程控制端

可以使用’mosquitto_pub’进行远程数据发送实现对nodeMCU的控制。下面的例子可以把LED连续点亮3次。

mosquitto_pub -h "your_mqtt_server" -t "MAC_ADDRESS/downlink" -m "blink#3"

我也是从MQTT服务器端直接发送的控制数据,因此命令可以这样写:

$ mosquitto_pub -t "A0:20:A6:18:47:F1/downlink" -m "#blink#3#"

Demo

Video on YouTube

本文中程序的完整代码可以在github上面找到。