实战课题
第 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 | 你的私钥 |
| 用户名 / 密码 | 留空,或随便填 |
| 主题 | 你在控制台建的那个 |
#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 里加两行:
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(...) 后面加一行:
mqtt.publish("sensor002", "lamp:off");
Serial.println("开机上报:lamp:off");这里我写死了
lamp:off,因为板子刚上电,灯确实是灭的。但更严谨的写法是把灯的状态记在一个变量里,报那个变量。 怎么记?这个留给你想。(提示:变量在断电之后就没了——所以开机那一刻,你能确定的状态只有"灭"。)
加完这段,然后测两件事(不用写代码,就是测):
- 把板子 USB 拔掉,等五秒,插回去。 串口里应该又出现那三句话,然后是一句开机上报。
- 把你家路由器电源拔掉,等十秒,插回去。 等一两分钟,看板子有没有自己重新连上邮局。(路由器重启一次要一分多钟,你会看到板子在那儿反复重试——那是对的,别动它。)
这两步测通了,你的灯才算"能放在那儿不用管"。很多人的作品从桌上拿起来就废了,就是因为从来没测过这一条。
六、造物:纸皮灯座
现在把它变成一个东西。提醒一句:先全部接好、跑通,再开始粘。
① 底座(纸皮,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:串口一个字都没打印
说明消息根本没送到板子。跟灯、跟线、跟接线一点关系都没有,别去动硬件。
- 串口里有没有
连邮局...成功和已订阅:xxx? 没有 → 先回第 23 课那套排查(WiFi 名字、密码、频段、私钥、错误码)。 - 一个字符一个字符地对主题名:你代码里
TOPIC_CMD那个字符串,跟你在调试台里发消息时选的主题,是不是同一个? - 去调试台自己订阅一下、自己发一条。 收得到 → 服务器没问题,是你的板子没上来;收不到 → 服务器那边的事,板子都不用碰。
这个坑的经典形态是:板子订阅的是 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,但你在调试台发消息,串口一个字都没打印。最该先查什么?答案与解析
串口没有任何打印,说明消息压根没到板子——那就跟 LED、跟接线、跟 USB 线一点关系都没有,查那些是白费力气。这时候要查的是"消息有没有送到"这一段:主题名对不对(大小写、多没多空格、002 有没有写成 02)、订阅成功那句打印出来没有、调试台里你自己订一下自己发一条收不收得到。先定位断在哪一段,再动手改。
收到消息:on,灯不亮。你把万用表搭在 LED 模块的 VCC 和 GND 两个针脚之间,读数是 0V。这说明什么?答案与解析
这是万用表最值钱的地方:它能把一个"灯不亮"拆成几个互不相干的小问题。 串口打印了消息,说明网络和代码都通;模块 VCC 对 GND 是 0V,说明模块这半边连电都没有——那 GPIO3 输出什么、代码写得对不对,全都是无关的。下一步就是把表笔挪到板子的 3.3V 排针上量一次:板子有电而模块没电,范围就锁定在那两根线上,通断档一量就知道是哪根。
答案与解析
前面那些课题你人就坐在板子旁边,灯亮没亮一眼就看见——反馈是免费的。远程控制不一样:你发了 on,灯到底亮没亮,你在几十公里外一无所知。没有回头路的远程控制不是控制,是许愿。(顺便:巴法云并不会因为你不上报就把你踢下线,别把这两件事搞混。)
答题要点
答题要点应该包含:① 传感器(看)是你在手机/电脑上发的那条指令——这一环不在板子上,在几十公里外;② **判断(想)**是板子收到消息后 if (msg == "on") 这个判断;③ **执行器(做)**是 LED 灯的亮灭;④ 结构一样:还是"看 → 想 → 做"三环节,缺一个就不算作品(只点灯不判断是演示,只判断不点灯用户感知不到);⑤ 换了位置:前面所有课题的"看"都长在板子身上(光敏、按钮、超声波),这个课题的眼睛是人,板子从头到尾不知道是谁在指挥它——这正是第 23 课"发布/订阅"模型的威力。
拓展我想用一个主题控制一整排灯,难道要挨个发消息吗?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"MQTT 主题通配符 加号 井号"、"MQTT topic wildcard + #"。想一想:如果主题名叫 home/room1/lamp、home/room2/lamp,有没有办法一条消息让 home 下面所有的灯都亮?
拓展我发的那条 on,在路上会不会被别人看见?
这个问题课上不讲,留给你自己找答案——**问 AI 是最好的办法**。
提示:搜"MQTT 明文传输 抓包"、"MQTT over TLS 8883"。想一想:巴法云除了 9501 还有一个 9503——那两个端口差在哪儿?为什么要有第二个? 你要是用 9501 控制家里的灯,同一个 WiFi 下懂行的人能看到你发了什么吗?
本节你要动手做的事
- [ ] 先把灯跑通:三根线接好,串口能看到"已订阅",调试台发
on灯亮 - [ ] 加上状态上报:收命令一个主题、发状态另一个主题(别共用一个)
- [ ] 加上开机上报:板子一连上邮局就先报一次当前状态
- [ ] 测断电恢复:拔 USB 重插、重启路由器,看它能不能自己连回来
- [ ] 做灯座:底座、L 形灯座、纸筒灯罩——先跑通再粘,粘完再测一遍
- [ ] 量一次那三根线:万用表通断档,一根一根量。记住"响"和"不响"是什么感觉
- [ ] 做个折线实验:拿根杜邦线来回折二十次,再量一遍——亲眼看它从好变坏
- [ ] 真的走开一次:把灯插上电放在你房间,你出门(或者去另一个房间),用手机浏览器打开调试台发一条
on,让家里人帮你看一眼灯亮了没有
这个课题做完,你手里会有一个挺特别的东西:一个你不在家也能用的开关。
它不精致——纸皮剪的,胶挤得到处都是,灯罩还是歪的。但它走的链路,跟市面上那些智能插座一模一样。
而且这一路上你踩的坑,是真的坑:你会盯着一盏不亮的灯,怀疑代码、怀疑服务器、怀疑人生,最后发现是一根看着完好的线。
你得有点耐心。 联网的故障就是这样:看不见、摸不着、还特别多。但"对半砍"这个思路你已经有了——先用串口把问题砍成"没收到"和"收到了没做"两半,再砍一半,再砍一半。
三个 C 档课题里,你已经做完一个了。
下一节换个方向:不用路由器、不用服务器、不用互联网,两块板子隔空直接说话。而且这次我们要做两个东西——一个探测器,一个报警器,分开放在两个地方。
先说清楚:下一节课需要两块板子。你只有一块的话可以跳过,它不影响最后一课。