联网与通信

第 23 课:MQTT——远程控制与上报

发布/订阅、主题、用巴法云把指令送上互联网——"控制"这件事本身

我是吴老师。这节课是联网单元里最要紧的一节。

我先问你:你桌上那盏灯,离你多远?

零米——你伸手就够着了。所以前面那些课,我们干的是同一件事:让板子替你按开关。按得准、按得及时,但它终归还是"你手边的东西"。

那要是你在学校,灯在家呢?这节课就是解决这个的。 而且我提前说一句:学完你会有点小震动——市面上那些"智能家居",拆开看就是这个东西。

一、MQTT 不是"打电话"

前面三节课,板子联网干的事都差不多:找到一个人,跟他说话。 第 20 课连上路由器、第 21 课自己当个小网站等着人来看、第 22 课跑去访问一个网址问一句答一句——这些全是"点对点"的:你得知道对方在哪,对方还得在线等着你。

MQTT 换了个完全不一样的思路——你别管对方是谁,你只管把消息扔进一个"邮局"。

邮局模型:发布 / 订阅

先看这张图。图里有三个角色,一个一个说。

MQTT 发布订阅模型:板子和电脑都把消息投到同一个邮局的主题上,谁订阅了这个主题谁就收到

"邮局"——中间那个蓝盒子,一台服务器,只干一件事:收消息、转消息。

"主题"(Topic)——邮局墙上一排一排的格子,图上写着 light002sensor002。你就把它想成信箱格子:谁往这个格子投信,谁订阅了这个格子,谁就拿到信。

剩下就是两个动作,记住这两个词,MQTT 就懂一大半了

干什么 大白话
发布(Publish) 把消息投到某个主题上 light002 这个格子里塞张纸条
订阅(Subscribe) 告诉邮局"这个格子归我盯着" "light002 里有信就给我"

最妙的地方在这儿——图里左边是板子,右边是电脑,它俩互相根本不知道对方存在。 板子不知道是谁发的消息,电脑也不知道板子长什么样——它们只认识那个主题名。

你就想成班里的公告栏。你在上面贴张"明天带跳绳",你不需要知道谁会来看——张三路过看到了,李四路过也看到了。你贴的时候,是不知道有谁的,这就是"发布"

那你想每次都看到新内容呢?每天去看一眼——这就是"订阅"。

这个模型为什么牛? 举个例子你就懂了。假设你做了个"远程开灯",今天是手机发、板子收。明天你想让妈妈手机也能开这盏灯。

  • "打电话"那套:你得改代码,把妈妈手机的地址也加进去,还得重新烧程序。
  • "邮局"这套板子那边一行都不用改。 妈妈手机订阅同一个主题,发消息就完了——邮局自己会转。

看到没有?这就是"加一个设备不用动原来的东西"。 物联网设备能成千上万地往上堆,靠的就是这个。

二、那个"邮局"在哪?

不在你家,也不在板子上。它在互联网上,是别人的一台服务器。 MQTT 这套规矩里必须有一台服务器在中间——总得有个地方摆那些格子。自己在家搭一台也行,但太麻烦了(还得让外网能访问到你家),所以这节课我们用别人搭好的

我用的是巴法云(bemfa.com)。

三件事,特别容易搞混

第一,它就是互联网上一台独立的服务器。 专门负责收发 MQTT 消息,跟我们前面用的任何东西都没关系。

第二,它跟你的板子走哪条路线没关系,跟 AI Scratch 平台也没关系。

巴法云不属于任何平台。 它就是一个对外的服务——你的 ESP32 能连,我的电脑能连,别人写的 Python 脚本也能连。 大家只是在同一个邮局里收发各自主题的信,跟你用什么工具、什么语言,一点关系都没有。 所以说白了,"MQTT 是什么"这个问题,一半的答案是:大家都去同一个邮局。

第三,参数是这样填的:

参数 填什么
服务器地址 bemfa.com
端口 9501(普通);9503 是加密的,我们先用 9501
客户端 ID 你的私钥(在巴法云控制台里拿)
用户名 / 密码 留空,或者随便填——它不用这个认人,认的是那个私钥
主题 先在控制台里新建一个,然后设备去订阅它

有一件事你必须自己动手确认:注册流程和界面。

我把参数核过了,但巴法云的注册页面、按钮位置、菜单叫什么,随时可能改——这类网站改版很勤。所以我不告诉你"点哪个按钮":你打开 bemfa.com,自己找"注册"、找"控制台"、找"新建主题"、找那个私钥在哪儿显示。

找不着就翻它的帮助文档,或者把界面截图丢给 AI 问"我要新建一个 MQTT 主题,点哪里"——这是完全正当的用法。

一句话:参数按我表里填,界面按你看到的来。

三、本课最值钱的东西:网页调试台

巴法云有个网页调试台——你在浏览器里就能发消息、收消息,一行代码都不用写。

我说它"最值钱",不是因为它方便,是因为它能救命。两个理由:

第一,它让你先验证"服务器这条路是通的"。 要是你一上来就写板子代码,然后灯不亮——你知道是哪儿的问题吗? 不知道。可能 WiFi 名字错了、私钥抄错了、主题没建、地址打错了、代码写错了……六七种可能,全挤在一个"没反应"里。

先用调试台自己发一条、自己收一条:通了 → 服务器没问题,毛病在你板子这边,范围直接砍一半;不通 → 跟板子一点关系都没有,你连板子都不用碰

这就是"对半砍"——链条从中间砍一刀,先确定是哪一半坏了,再砍那一半。这个思路比这门课里任何一行代码都值钱。

第二,你能亲眼看见消息长什么样。 on 到底是 on,还是 on\n,还是别的什么?看得见,你就不会瞎猜。

四、动手

第 1 步:注册,新建一个主题

bemfa.com 注册,进控制台,新建一个主题。名字随便起,但要短、好记、别用中文和空格。我用 light002 当例子,你换成你自己的。再找到私钥——那一串字符就是你的"客户端 ID",等下要填进代码。

私钥别往网上贴。 它相当于你在这个邮局里的账号,别人拿到就能冒充你的设备。截图记得打码。

第 2 步:在调试台里发一条消息

打开调试台,订阅你自己的主题,然后发一条消息,内容写 on你应该能看到自己发的消息又回来了。 这一步通了,说明服务器这边完全没问题。

等一下,on 是什么意思?

没什么意思。 这就是你自己定的一门暗号——你跟板子说好:"发 on 就是开灯,发 off 就是关灯。"服务器完全不管内容,你发"芝麻开门"也行,只要板子那边也认。

(正式叫法是消息内容或者载荷(payload),你以后查资料会看到这个词。)

第 3 步:接好灯

接线跟第 21 课一模一样:VCC3.3VGNDGNDSignalGPIO3

还是那盏灯,还是 GPIO3——我们只是换了个方法让它亮。

第 4 步:让板子连上邮局

代码要装一个库。库管理器里搜 PubSubClient(作者是 Nick O'Leary),装上。

然后分三步写,一步一个现象,别一口气全写完

第一步,只连 WiFi 和服务器,不干别的:

C++
#include <WiFi.h>
#include <PubSubClient.h>

const char* WIFI_SSID = "你家的WiFi名字";
const char* WIFI_PASS = "你家的WiFi密码";
const char* MQTT_HOST = "bemfa.com";
const int   MQTT_PORT = 9501;
const char* CLIENT_ID = "你的私钥";
const char* TOPIC     = "light002";   // 换成你建的那个主题名

WiFiClient net;
PubSubClient mqtt(net);

void setup() {
  Serial.begin(115200);
  pinMode(3, OUTPUT);

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("WiFi 连上了,IP:");
  Serial.println(WiFi.localIP());   // 能看到 IP,说明 WiFi 这关过了
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
}

void loop() {
  if (!mqtt.connected()) {
    Serial.print("连邮局...");
    if (mqtt.connect(CLIENT_ID)) {
      Serial.println("成功");
      mqtt.subscribe(TOPIC);
      Serial.println("已订阅:" + String(TOPIC));
    } else {
      Serial.print("失败,错误码 ");
      Serial.println(mqtt.state());   // 这个错误码很有用,别忽略它
      delay(3000);
    }
  }
  mqtt.loop();
}

烧进去,看串口。 你要看到三句话:WiFi 连上了连邮局...成功已订阅:light002

那句 mqtt.state() 打印出来的错误码千万别删——连不上的时候,它就是你唯一的线索,比"没反应"强一百倍。这就是第 7 课那句"先打印,再判断"。

第二步,加上"收到消息就点灯":

C++
void onMessage(char* topic, byte* payload, unsigned int len) {
  String msg;
  for (unsigned int i = 0; i < len; i++) {
    msg += (char)payload[i];
  }

  Serial.print("收到消息:");
  Serial.println(msg);          // 先打印出来看!

  if (msg == "on")  digitalWrite(3, HIGH);
  if (msg == "off") digitalWrite(3, LOW);
}

然后在 setup() 里加一句 mqtt.setCallback(onMessage);——意思是"收到消息就调 onMessage 这个函数"。

现在去网页调试台发一条 on。灯应该亮了。

灯不亮?先看串口有没有打印"收到消息:xxx"。

打印了、但灯不动 → 毛病在你的判断上(on 前面是不是有看不见的空格或换行?一个字符一个字符对)。压根没打印 → 消息没送到板子(回调试台确认主题对不对、板子订阅成功没)。

这就是"对半砍":先分清是"没收到"还是"收到了没处理"。 这两种毛病的修法完全不一样。

第三步,让板子往回说话——上报。

灯能听你的了,现在反过来:让板子把读到的数据发上去。 顺手把第 11 课的光敏接上(AO 接一个模拟引脚),然后在 loop() 里加:

C++
unsigned long lastSend = 0;

void loop() {
  // ... 前面连服务器的代码不动 ...

  if (millis() - lastSend > 5000) {     // 每 5 秒上报一次
    lastSend = millis();
    int light = analogRead(0);
    mqtt.publish("sensor002", String(light).c_str());   // 注意:发到另一个主题!
    Serial.print("已上报:");
    Serial.println(light);
  }
}

⚠ 这里有个坑,我专门标出来:上报用的是 sensor002,不是 light002

为什么?因为 MQTT 的规矩是:一条消息会投给所有订阅了这个主题的人——包括发消息的人自己。 你要是在 light002 上又收又发,板子就会把自己刚发出去的数据当成一条新命令收回来,然后一脸茫然地拿光敏数值去跟 on 比——现象就是灯莫名其妙地闪,你查半天代码也查不出来。

这个坑很经典,你迟早会踩,我先说了。 想亲眼看看就把上报也改成 light002 跑一遍——看到灯开始抽风,你就真记住了。规矩就一条:收命令一个主题,发数据一个主题。

记得在控制台里把 sensor002 也建出来。 然后在调试台订阅它,你就能看到板子报上来的光敏数字了。

看到这一步你会有点特别的感觉: 你和板子,一个在电脑前,一个在桌上,中间隔着互联网来回说话——这不就是"物联网"吗?是的,就是。

五、连不上?按这个顺序查

联网的毛病特别烦,因为你什么都看不见。接错一根线你能看见灯不亮,可连不上邮局,你只能看见"没反应"。所以别乱试,按这个顺序一步一步砍:

① 先看串口,看板子说什么。 WiFi 连上了吗?IP 拿到了吗?"连邮局"那句后面跟的是成功还是错误码?第 7 课就讲过:先打印,再判断。 没有串口输出,后面全是在猜。

② 分清楚是哪一段断了。

  • WiFi 没连上 → 跟 MQTT、巴法云一点关系都没有。去查第 20 课那套:名字对不对、密码对不对、路由器是不是把新设备挡住了、是不是连了 5G 频段的信号(C3 只认 2.4G)。
  • WiFi 通了,邮局连不上 → 才开始查巴法云那边。

③ 一个字符一个字符地核对参数。 这一条最能气死人:bemfa.com 写成 bemfa.con、私钥少复制了一位、主题名多个空格、端口写成 9503 但没开加密。这些错误肉眼看不出来,只能一个字一个字对。

④ 去服务端看一眼——这就是"对半砍"。 调试台订阅一下主题,自己发一条:收得到 → 服务器没问题,是你的设备没上来;收不到 → 服务器这边的事,跟板子无关。 用一个你信得过的客户端替掉一半链路,通不通立刻知道该往哪半边找。

还有一条最重要的:改一个地方,测一次。 学生最常见的自毁操作是一口气改五处然后跑——通了,你不知道是哪一处的功劳;不通,你不知道是哪一处的锅。

六、本课的重点,真不是灯

我知道你最兴奋的是"灯亮了"。但这课的重点不在灯上,在"控制"这件事本身。

远程控制链路:人在远处用手机发消息,消息穿过互联网上的服务器到达桌上的板子,灯就亮了

你想想刚才发生了什么:你人坐在电脑前,按了一下,桌上(或者几十公里外)的一个东西动了。 你的指令穿过路由器、穿过互联网、经过一台你从没见过的服务器,最后落在一个巴掌大的板子上——中间没有任何一段是你亲手接的。

智能家居、共享单车、远程抄表、大棚的自动浇水,全是这个东西。 换成继电器是空调,换成电机是窗帘,骨架一模一样。

你已经把这个骨架搭出来跑通了。 说实话,很多成年人买回一整套智能家居,也不知道里面到底是怎么回事。你知道了。

自测

单选题MQTT 的"发布 / 订阅"和前面学的 HTTP 请求,最本质的区别是什么?
单选题你在调试台发了一条 on,板子串口什么也没打印。下面哪个做法最对?
单选题为什么上报数据要换一个主题,不能跟收命令共用一个?
问答题用自己的话说清楚什么是"发布 / 订阅",并说明它比"点对点"强在哪。
拓展如果别人也在巴法云上建了一个跟我同名的主题,我们会互相收到对方的消息吗?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"巴法云 MQTT 主题 唯一 重名"。想一想:主题名是全局唯一的,还是每个账号各管各的?如果真会串,那"主题名要起得够特别"是不是就成了一个安全习惯?自己动手试最快:跟同学商量用同一个主题名,各发一条看看。

拓展板子断网的那几分钟里,别人发的消息去哪了?网络恢复了还能收到吗?

这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。

提示:搜"MQTT QoS 0 1 2 区别"、"MQTT 保留消息 retained"。想一想:如果这是你家的门锁,你希不希望"断网期间那条开锁指令"事后还补送过来?——不同的场景,答案不一样,这就是 QoS 存在的理由。

本节你要动手做的事

  • [ ] 注册巴法云,新建两个主题(一个收命令、一个发数据),把私钥记下来
  • [ ] 先在网页调试台里自己发一条、自己收一条——不通就别往下走
  • [ ] 接好 LED 模块SignalGPIO3,跑通"WiFi + 连邮局 + 订阅",串口能看到"已订阅"
  • [ ] 在调试台发 on / off,看着灯亮灭——这就是远程控制
  • [ ] 让板子每 5 秒上报一次光敏读数,在调试台里订阅 sensor002 看它冒数字
  • [ ] 故意把主题名改错一个字符,跑一遍,看看串口打印什么——这个错你以后一定会犯,先认识它

这节课的东西很多,你要是卡了两三个小时,太正常了。 联网的坑就是这样:看不见、摸不着,还特别多。

但你带走了两个很重要的东西:一个是"对半砍"的排查思路,一个是"发布/订阅"这个模型。 后面那个,会跟着你很久。

下一节我们换个玩法:不用路由器、不用服务器、不用互联网,两块板子隔空直接说话。

对,两块。 下一节我会先说清楚要准备什么。