实战课题

第 35 课:远程开关灯

手机发一条消息,几十公里外的灯亮了——把"控制"这件事做成一个能摆在桌上的东西

我是吴老师。这是三个 C 档课题里的第一个,它代表的方向是"远程控制"。

先给你一个画面:

下午第三节课,你坐在教室里。你妈出差了,家里没人。你摸出手机,打开一个网页,敲两个字,按发送。

三秒钟之后,你家书桌上那盏纸皮小灯亮了。

你人在这栋楼里,灯在你家。中间没有一根线连着——指令是怎么过去的?

这就是你这个课题要做的东西。而且我要提前说一句:它跟前面那些课题不一样

前面三十几个课题,做完了你都能拿在手里玩——按一下、吹口气、挥挥手,它就动。这个不行。 它天生就是为"你不在场"设计的。你坐在它旁边,它反而没什么好玩的。

所以这个课题,你得真的走开才算做完。

一、重点真不是"灯"

第 23 课我讲过这句话,这里再讲一遍,因为这个课题最容易做歪。

你要是把注意力全放在"灯亮了没有"上,你就白做了。

灯亮这件事你第 6 课就会了——一根线、两行代码,完事。这个课题真正值钱的东西,是"控制"这两个字本身。

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

盯着这张图看三秒。看那条线有多长。

你在这头按了一下,那个指令要经过你家的路由器、穿过互联网、落在一台你从没见过、也永远不会去见的服务器上,再被送回你家,最后落在一块巴掌大的板子上。中间六七个环节,没有一个是你能用手摸到的。

这件事你在第 23 课已经跑通了一遍。但那次你是在调试台里点按钮、板子就在你手边——你其实没感受到"远"。

这个课题要做的,就是把这盏灯装起来、摆到自己房间的桌子上,然后你走开。第二天放学回来,你在楼下用手机把它点亮。

那一刻的感受完全不一样。 你会突然意识到:我家那台空调、那盏客厅灯、那个扫地机器人,里面就是这个东西。

说句实话:很多成年人花几千块买了一整套智能家居,装完也不知道里面是怎么回事。你不但知道,你还自己做了一个出来。

二、闭环:这个课题的三个环节

第 9 课讲过,一个作品必须走完 传感器(看)→ 主控(想)→ 执行器(做)。这个课题的三个环节是这样对应的:

环节 这个课题里是什么 在哪儿
传感器(看) 你在手机上按的那一下 不在板子上,在几十公里外
判断(想) 收到消息后判断:这是 on 还是 off 板子里的 if
执行器(做) LED 灯亮 / 灭 桌上那个纸皮灯座上

看出来了吗?它跟前面所有课题最大的区别,就在第一行。

前面那些课题,"看"的这一环全在板子身上——光敏在看、按钮在看、超声波在看。这个课题里,"看"的那只眼睛是你。

板子从头到尾不知道你是谁、你在哪,它只认识那一个主题名。这就是第 23 课讲的发布/订阅模型。

所以这件事其实挺哲学的:你把自己的"看"外包给了一条网络,然后站在几十公里外,指挥一个不认识你的东西。

智能家居、共享单车、远程抄表、大棚自动浇水——骨架一模一样。

闭环还有另一半,别漏了

这里我要专门讲一个东西:反馈。

你想想,你在教室里发了一条 on。灯亮了没有?

你不知道。

除非你妈在家帮你看一眼——那不叫远程控制,那叫打电话让你妈开灯

真正的远程控制,必须有一条回头路:板子做完动作,把结果报回来。你在手机上看到"灯现在是开着的",这才叫完成。

纯文本
你 ──── 命令 on ────→ 邮局 ────→ 板子 → 灯亮
你 ←─── 状态 on ──── 邮局 ←──── 板子 ┘

这条回头路是 C 档的要求。 A 档、B 档的课题你人在现场,灯亮没亮一眼就看见了,不需要上报。这个课题你必须自己把反馈补上——不然它就是个坏的东西。

三、材料清单

套件里:ESP32-C3 开发板 ×1、LED 灯模块 ×1(丝印 VCC GND Signal)、杜邦线 ×5(多备两根,本课我们会弄坏一根)、USB 数据线 ×1。

身边要准备:

东西 干嘛用
快递盒纸皮 做底座,A5 大小就够
A4 白纸 或 描图纸 卷灯罩。描图纸(半透明的)效果最好,普通打印纸也行
热熔胶枪 + 胶棒 粘接主力
剪刀、尺子、铅笔
美工刀 开口子用——这个让家长帮忙,很容易划手
手机 或 电脑 发指令用,它就是这个作品的"遥控器"

没有热熔胶? 透明胶带 + 双面胶也能凑合,就是不结实。热熔胶几十块钱一把,后面三个 C 档课题都要用,建议备一把。

四、接线

只有三根线,跟第 6 课、第 23 课一模一样:

LED 模块 接到 线色习惯
VCC 3.3V
GND GND 灰/黑
Signal GPIO3 黄/橙

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

接线之前先把板子的 USB 拔掉。 这条规矩从第 4 课讲到这儿,我还会一直讲。

五、动手:先跑通最丑的版本

这个课题的动手部分,思路跟前面不一样。

前面是"接一个模块 → 看现象 → 加一个模块 → 看现象"。这个课题只有一个模块,所以换个加法:功能一层一层往上叠。

每一层都有你能看到的现象,一层跑通了再上下一层。别一次全写完——联网的坑多,一口气写一大段,出了错你根本不知道是哪一层的事。

第 1 层:让灯跟着调试台的消息亮灭

这一层跟第 23 课完全一样,那节课做通了的话,这层就是抄过来的:去 bemfa.com 注册 → 进控制台新建一个主题(比如 light002换成你自己的)→ 找到私钥抄下来 → 先在网页调试台里自己发一条、自己收一条

不通就别往下走。 调试台自己发自己收都不通,后面全是白费劲。

注册页面长什么样、按钮在哪儿,我不告诉你。 巴法云这种网站改版改得勤,我写了也是错的。你自己对着实际界面找,找不着就翻帮助文档,或者截个图丢给 AI 问"我要新建一个 MQTT 主题,点哪里"。参数按下面表里填,界面按你看到的来。

参数 填什么
服务器地址 bemfa.com
端口 9501
客户端 ID 你的私钥
用户名 / 密码 留空,或随便填
主题 你在控制台建的那个
C++
#include <WiFi.h>
#include <PubSubClient.h>

const char* WIFI_NAME = "你家的WiFi名字";
const char* WIFI_PASS = "你家的WiFi密码";
const char* MQTT_HOST = "bemfa.com";
const int   MQTT_PORT = 9501;
const char* CLIENT_ID = "你的私钥";
const char* TOPIC_CMD = "light002";      // 收命令的主题

WiFiClient net;
PubSubClient mqtt(net);

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);
}

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

  WiFi.begin(WIFI_NAME, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.print("WiFi 连上了,IP:");
  Serial.println(WiFi.localIP());

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);           // 收到消息就调 onMessage
}

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

串口里要看到三句话WiFi 连上了,IP:192.168.x.x连邮局...成功已订阅:light002

然后去调试台发一条 on。灯应该亮了。

现象有了,这就是第一层。 丑,但通了。

第 2 层:加上那条回头路——状态上报

现在做本课的重点:让板子干完活以后,把结果报回去。

先说一个必须遵守的规矩(第 23 课讲过,这里是它的经典应用场景):

收命令、发状态,必须用两个不同的主题。

因为 MQTT 的规矩是:一条消息会投给所有订阅了这个主题的人——包括发消息的人自己。你要是又收又发都用 light002,板子就会把自己刚报上去的"灯亮了"当成一条新命令收回来,然后拿这三个字去跟 on 比——现象是灯莫名其妙地抽风。

所以:收命令用 light002,发状态用另一个主题。 我这儿用 sensor002去控制台把它也建出来。

onMessage 里加两行:

C++
  if (msg == "on") {
    digitalWrite(3, HIGH);
    mqtt.publish("sensor002", "lamp:on");       // ← 报回去:灯现在是开的
    Serial.println("已上报:lamp:on");
  }
  if (msg == "off") {
    digitalWrite(3, LOW);
    mqtt.publish("sensor002", "lamp:off");      // ← 报回去:灯现在是关的
    Serial.println("已上报:lamp:off");
  }

现在去调试台,订阅 sensor002,然后发一条 on

你会看到两件事同时发生:灯亮了,调试台里冒出来一条 lamp:on

看到没有——这就叫反馈。 你现在能同时看到"我发了什么"和"它现在是什么状态"。

lamp:on 这个格式是我随口定的。 你也可以发 1、发 on、发"灯开了"——只要你自己认。服务器完全不管内容,这就是第 23 课说的"你自己定的一门暗号"。

但有一条:定好了就别乱改。 你以后要是写个小程序来读这个状态,格式一改,那边就瞎了。

第 3 层:上电自动报一次,然后自己连回来

这一层是为"你真的走开"准备的。

灯一直插着电,中间万一断了一次电——跳闸、插头松了、路由器重启——板子重新起来之后,你手机上显示的状态就是过期的。你以为灯关着,其实它开着。

所以让它一开机就报一次自己的状态。 在刚才那句 mqtt.subscribe(...) 后面加一行:

C++
      mqtt.publish("sensor002", "lamp:off");
      Serial.println("开机上报:lamp:off");

这里我写死了 lamp:off,因为板子刚上电,灯确实是灭的。但更严谨的写法是把灯的状态记在一个变量里,报那个变量。 怎么记?这个留给你想。(提示:变量在断电之后就没了——所以开机那一刻,你能确定的状态只有"灭"。)

加完这段,然后测两件事(不用写代码,就是测):

  1. 把板子 USB 拔掉,等五秒,插回去。 串口里应该又出现那三句话,然后是一句开机上报。
  2. 把你家路由器电源拔掉,等十秒,插回去。 等一两分钟,看板子有没有自己重新连上邮局。(路由器重启一次要一分多钟,你会看到板子在那儿反复重试——那是对的,别动它。

这两步测通了,你的灯才算"能放在那儿不用管"。很多人的作品从桌上拿起来就废了,就是因为从来没测过这一条。

六、造物:纸皮灯座

现在把它变成一个东西。提醒一句:先全部接好、跑通,再开始粘。

纸皮灯座的结构:底座是一块八厘米见方的纸皮,灯座是把 LED 模块粘在折成 L 形的纸皮上、灯珠朝上,灯罩是 A5 纸卷成的纸筒,板子摆在底座后方,三根杜邦线连过去

① 底座(纸皮,8cm × 8cm)

从快递盒上剪一块 8cm 见方的纸皮。不用很精确,但四个角剪圆一点,不然容易翘。

② 灯座(小纸皮条)

剪一条 2cm 宽、6cm 长的纸皮,对折成 L 形(像个直角小凳子)。LED 模块平放在这个 L 形上、灯珠朝上,用热熔胶在模块两个角上点一下粘住。再把这个 L 形粘在底座中间偏后的位置。

热熔胶只要点两下就够。 学生最常见的错误是挤一大堆、糊得到处都是——胶糊到插头上,你以后就换不了线了。

③ 灯罩(纸筒)

A4 纸竖着裁一半(A5),卷成一个筒,直径 6~7cm、高 10cm 左右。接缝用胶带粘住。

  • 要更亮更均匀:用描图纸,或者在内层贴一张白纸
  • 要有光斑:在外面用针扎一排小孔,光会漏出来一排小点,挺好看
  • 要能拆:灯罩不要粘死,直接套上去——以后换线、换模块还得拆

粘在哪儿、线怎么走:

东西 粘在哪 注意
LED 模块(带 L 形座) 底座正中央偏后 灯珠朝上,正对灯罩中心
板子 底座靠后的边沿,或者翻过来粘在底座背面 别压住天线那一端——第 20 课讲过,C3 的天线是板子边缘那段弯弯曲曲的铜箔
三根杜邦线 沿底座边沿走,用一小段透明胶带压一下 别拉紧! 见下一节

USB 出线口:在底座侧面用美工刀开一个 1cm 宽的小口子,USB 线从那儿引出去。这个口子请家长帮忙开——美工刀划纸皮的时候一打滑就是一道口子,年年都有人中招。

最后一步:全部粘完,插上电,再测一遍。 调试台发 on,看灯亮不亮。这一步千万别省。 为什么?往下看。

七、灯不亮?按这个顺序查(本课的坑在这节

我先把话撂这儿:这个课题我埋了一个坑,你或者你同学大概率会撞上。

别慌。联网的故障最烦人的地方是"没反应"三个字信息量太低,所以咱们不猜,按顺序走。

先分岔:打开串口监视器,在调试台发一条 on

看串口有没有打印 收到消息:on。这一句话就能把问题砍成两半——这就是第 23 课讲的"对半砍"。

岔路 A:串口一个字都没打印

说明消息根本没送到板子。跟灯、跟线、跟接线一点关系都没有,别去动硬件。

  1. 串口里有没有 连邮局...成功已订阅:xxx 没有 → 先回第 23 课那套排查(WiFi 名字、密码、频段、私钥、错误码)。
  2. 一个字符一个字符地对主题名:你代码里 TOPIC_CMD 那个字符串,跟你在调试台里发消息时选的主题,是不是同一个?
  3. 去调试台自己订阅一下、自己发一条。 收得到 → 服务器没问题,是你的板子没上来;收不到 → 服务器那边的事,板子都不用碰。

这个坑的经典形态是:板子订阅的是 light002,你在调试台里手一抖发到了 light001串口上什么都没发生,然后你开始怀疑代码——代码一行都没错。

这就是本课最需要练的那个判断:先分清是"没收到"还是"收到了没处理"。 一个看串口就能分清的问题,别用猜的。

岔路 B:串口打印了 收到消息:on,灯就是不亮 ← 本课埋的坑

先说结论:网络这一整条链路是通的。 从你的手机到邮局到板子,几千里路都跑通了。问题在"灯"这半边——三根线、一个模块的范围。

按顺序查,每一步都在缩小范围

① 先排除代码。digitalWrite(3, HIGH); 前面加一句打印,跑一遍。

  • 打印出来了 → 代码走到这一步了,代码没问题,往硬件查,往下走 ②
  • 没打印 → 那才是代码的问题(msg 真的等于 on 吗?前面是不是有个看不见的空格或者换行? 一个字符一个字符对)

② 拿万用表,打到直流电压档(标着 V 后面一条直线那个,不是波浪线那个)。黑表笔搭在 LED 模块的 GND 针脚上,红表笔搭在模块的 VCC 针脚上。

这一步在问一个特别朴素的问题:这个模块,到底有没有拿到电?

量出来 说明什么
3.3V 左右 模块有电,往下走 ③
0V 模块没拿到电,直接跳到 ④
1V 上下、还乱飘 接触不良,八成是插头没插到底

③ 模块有电,那就量信号。 黑表笔还搭 GND,红表笔挪到模块的 Signal 针脚。在调试台发 on,盯着表。

  • on 时跳到 3.3V,发 off 时掉回 0V模块这边全对,那问题在模块本身(换一个试试)或者你插错脚了
  • 一直不动 → 信号没送到模块,往下走 ④

④ 表笔从模块挪到板子上。 黑表笔搭板子的 GND 排针,红表笔搭板子的 3.3V 排针。

  • 量到 3.3V板子是好的、有电。 那问题就锁定在连接板子和模块的那几根杜邦线上,往下走 ⑤
  • 量到 0V → 板子自己没起来,去查 USB 线和电脑接口

⑤ 拔下来,万用表打到通断档(那个像二极管或者标着")))"的档位),三根线一根一根量。

通断档怎么用(第 5 课讲过):两根表笔碰在一起,表会"嘀"一声;把一根杜邦线的两头分别捏住,响 = 这根线是好的,不响 = 这根线内部断了。

三根线,一根一根量。要是有一根不响——恭喜,你抓到了。

根因

那根线,外皮是完好的,两头的塑料壳也好好的,插在排针上稳稳当当。你肉眼看、用手拽,都看不出任何毛病。但里面的铜丝已经断了。

这就是我在这个课题里故意留的坑。 我挑它,是因为它把最气人的那种故障演给你看:现象是"灯不亮"——一个信息量几乎为零的现象,而真正的原因藏在一个你根本不会想到要去看的地方。

想亲眼见识一次?三十秒的事:

找一根杜邦线,捏住两头的塑料壳,把中间那段来回折二十次,然后用通断档量一下。

你会发现它已经不响了。

线不是被剪断才坏的,它多半是被折坏的。 尤其是你把它弯成一个很急的角、或者像刚才装灯座那样把线拉紧过。

这个实验能让你在以后每次"莫名其妙不工作"的时候,第一个想到去量线

⑥ 换一根线,灯亮了。

顺手把那根坏的扔进垃圾桶。 别放回线堆里——不然你下次还会拿到它,然后再把这整套流程走一遍。这个坑我见人踩过不止一次,第二次踩的往往还是同一根线。

自测

单选题板子串口打印了 已订阅:light002,但你在调试台发消息,串口一个字都没打印。最该先查什么?
单选题串口打印了 收到消息:on,灯不亮。你把万用表搭在 LED 模块的 VCCGND 两个针脚之间,读数是 0V。这说明什么?
单选题为什么这个课题一定要做"状态上报",而前面那些课题不用?
问答题这个课题的闭环三个环节分别是什么?为什么说它跟前面所有课题"结构一样、但有一环换了位置"?
拓展我想用一个主题控制一整排灯,难道要挨个发消息吗?

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

提示:搜"MQTT 主题通配符 加号 井号"、"MQTT topic wildcard + #"。想一想:如果主题名叫 home/room1/lamphome/room2/lamp,有没有办法一条消息让 home 下面所有的灯都亮?

拓展我发的那条 on,在路上会不会被别人看见?

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

提示:搜"MQTT 明文传输 抓包"、"MQTT over TLS 8883"。想一想:巴法云除了 9501 还有一个 9503——那两个端口差在哪儿?为什么要有第二个? 你要是用 9501 控制家里的灯,同一个 WiFi 下懂行的人能看到你发了什么吗?

本节你要动手做的事

  • [ ] 先把灯跑通:三根线接好,串口能看到"已订阅",调试台发 on 灯亮
  • [ ] 加上状态上报:收命令一个主题、发状态另一个主题(别共用一个
  • [ ] 加上开机上报:板子一连上邮局就先报一次当前状态
  • [ ] 测断电恢复:拔 USB 重插、重启路由器,看它能不能自己连回来
  • [ ] 做灯座:底座、L 形灯座、纸筒灯罩——先跑通再粘,粘完再测一遍
  • [ ] 量一次那三根线:万用表通断档,一根一根量。记住"响"和"不响"是什么感觉
  • [ ] 做个折线实验:拿根杜邦线来回折二十次,再量一遍——亲眼看它从好变坏
  • [ ] 真的走开一次:把灯插上电放在你房间,你出门(或者去另一个房间),用手机浏览器打开调试台发一条 on,让家里人帮你看一眼灯亮了没有

这个课题做完,你手里会有一个挺特别的东西:一个你不在家也能用的开关。

它不精致——纸皮剪的,胶挤得到处都是,灯罩还是歪的。但它走的链路,跟市面上那些智能插座一模一样。

而且这一路上你踩的坑,是真的坑:你会盯着一盏不亮的灯,怀疑代码、怀疑服务器、怀疑人生,最后发现是一根看着完好的线。

你得有点耐心。 联网的故障就是这样:看不见、摸不着、还特别多。但"对半砍"这个思路你已经有了——先用串口把问题砍成"没收到"和"收到了没做"两半,再砍一半,再砍一半。

三个 C 档课题里,你已经做完一个了。

下一节换个方向:不用路由器、不用服务器、不用互联网,两块板子隔空直接说话。而且这次我们要做两个东西——一个探测器,一个报警器,分开放在两个地方。

先说清楚:下一节课需要两块板子。你只有一块的话可以跳过,它不影响最后一课。