
我之前在给卧室装电动卷帘时纠结了很久成品智能窗帘价格偏高协议大多封闭很难接入自己正在用的智能家居系统自己做又担心电机选型、行程控制、限位保护这些细节踩坑。后来干脆花了两周时间基于 ESP32 自己从零做了一套智能卷帘从硬件接线到固件开发再到接入 Home Assistant 定时联动整套流程跑通之后体验和成本都比预想中好很多。这篇文章把整个过程整理成一份完整教程内容包括智能卷帘的系统架构、硬件选型、电机驱动原理、ESP32 固件编写、MQTT 协议接入以及和 Home Assistant 的联动玩法。无论你是准备给家里改造智能窗帘还是想用一个真实项目入门 ESP32 开发这篇文章都能直接参考。1. 智能卷帘的核心概念与整体架构1.1 智能卷帘到底做了什么传统卷帘也叫遮光帘、罗马帘依靠手动拉拽珠链或拉绳来控制升降。智能卷帘的本质是把“人手拉拽”这个动作替换成“电机转动”再把“手动控制”扩展成远程开关、定时开关、光线感应联动以及语音控制。一个完整的智能卷帘系统通常包含下面几个角色卷帘本体包含卷管、面料、底杆、安装支架。这部分可以买成品也可以买面料和卷管自己组装。驱动电机一般是直流减速电机负责带动卷管转动实现升降动作。控制板我选择 ESP32它负责接收控制指令、读取限位信号、控制电机正反转。电机驱动模块因为 ESP32 的 GPIO 输出电流很小无法直接驱动电机需要借助驱动板来放大信号。通信模块ESP32 自带 WiFi 和蓝牙可以直接和路由器通信走 MQTT 或 HTTP 协议。传感器与保护机制包括限位开关、堵转检测、手动按钮、光敏传感器等。所以智能卷帘并不是一个有多高门槛的项目。它的难点主要在于机械结构的适配以及软件层面的行程控制逻辑。1.2 常见控制方案对比在动手之前先想清楚采用哪种通信和协议方案。目前常见的有这么几种方案通信方式优点缺点WiFi MQTT家庭路由器接入方便可联动智能家居平台依赖路由器稳定WiFi HTTP家庭路由器实现简单调试直观轮询或请求响应模式不够实时蓝牙 BLE手机直接连接功耗低断网可用控制距离短难以做集中自动化433MHz 射频射频遥控器成本低穿墙好单向通信无状态反馈我个人推荐ESP32 WiFi MQTT的方案。因为 MQTT 协议天生适合这种设备状态上报和控制指令下发而且 Home Assistant、OpenHAB 等开源智能家居平台都原生支持 MQTT。就算之后你换平台、加设备这套架构依然能继续复用。1.3 为什么是 ESP32 而不是其它开发板ESP32 在智能家居 DIY 项目里几乎成了首选原因很直接自带 WiFi 和蓝牙不需要额外外挂通信模块。主频高GPIO 资源充足能同时处理电机控制、按键、传感器和网络任务。Arduino 生态成熟开发资料多遇到问题很容易搜到解决方案。板载 Flash支持掉电存储可以保存校准数据。价格比较亲民实验阶段即使烧坏也不心疼。相比之下Arduino Uno 需要外接 ESP8266 模块才能联网而且性能和存储都局促树莓派又过于重型不适合直接放在窗帘盒里长期通电。ESP32 的功耗和尺寸控制也很好适合嵌入墙面或窗帘轨道附近。2. 硬件选型与环境准备2.1 硬件清单我按自己的实际项目列一份清单你可以根据家里的卷帘尺寸和导轨结构微调组件规格建议备注主控板ESP32 DevKit V1 或兼容板38 引脚版本即可电机12V 直流减速电机转速 10~30 RPM需要根据卷帘高度和直径推算电机驱动L298N 或 L9110S 电机驱动模块小功率可选用 L9110S方便接线电源12V/2A 直流电源适配器电流要留出 20% 以上余量限位开关微动开关或干簧管用于检测卷帘是否到顶/到底按键自复位轻触按钮做手动控制与校准入口光敏电阻光敏电阻模块做光照联动可选外壳3D 打印盒或塑料接线盒注意散热与走线需要说明的是不同卷帘的安装空间和受力要求差异很大。如果你的窗帘比较重或者尺寸很大电机扭矩需要重新计算。本文示例按家用普通遮光卷帘、电机功率在 10W 以内的场景来写思路可以照搬。2.2 软件开发环境固件开发我使用 Arduino IDE配合 ESP32 开发板支持包。Arduino IDE 对新手更友好而且社区里的库文件很完善适合快速跑通项目。开发环境建议如下操作系统Windows 10 / 11 或 macOS 均可。开发工具Arduino IDE 2.x。开发板支持在“开发板管理器”中安装 ESP32 支持包。核心库WiFi.h、PubSubClient.h、Preferences.h、ArduinoJson.h按需。由于每个人的电脑环境和 ESP32 版本不同这里不写死具体安装命令。你在 Arduino IDE 中打开“文件 - 首选项 - 附加开发板管理器网址”填入 ESP32 官方的 JSON 地址后即可在开发板管理器中搜索安装。2.3 示例项目结构我建议把代码按功能拆分成多个模块方便后续维护smart-roller-shade/ ├── SmartRollerShade.ino // 主程序负责初始化和主循环 ├── config.h // 网络、引脚、参数配置 ├── motor.h / motor.cpp // 电机控制封装 ├── calibrate.h / calibrate.cpp // 行程校准逻辑 ├── mqtt_manager.h / mqtt_manager.cpp // MQTT 连接与消息处理 └── memory.h / memory.cpp // NVS 存储保存校准数据当然Arduino 的 .ino 文件本身可以把多个文件放在同一个目录下一起编译所以这种拆分方式是可用的。实际编写时如果项目不大也可以先写在单一 .ino 文件里等代码量上来后再拆成多文件。3. 核心原理拆解电机驱动与行程保护3.1 直流电机正反转原理与 PWM 调参ESP32 控制直流电机最经典的方案是通过电机驱动模块实现“H 桥”逻辑。以 L298N 为例控制端子包含 IN1、IN2、ENA。当 IN1 为高电平、IN2 为低电平时电机正转反过来电机反转。ENA 引脚配合 PWM 信号可以控制电机的转速。接线示意ESP32 GPIO - IN1 ESP32 GPIO - IN2 ESP32 GPIO - ENA (PWM)在 Arduino 代码中使用 ESP32 的 LEDC 通道来产生 PWM 信号。3.2 限位开关与行程校准卷帘不能一直转到“没方向”否则会拉坏布料或电机堵转。常见的做法是在卷帘轨道顶部和底部各安装一个限位开关当卷帘运行到极限位置时金属触发片碰到微动开关开关信号由高电平变成低电平程序立刻停止电机。还有另一种方案是记录转动时长按下升键后电机持续运行 30 秒到顶。这种方式不需要限位开关但每次断电后需要重新校准而且电机老化会导致行程变化。最稳妥的是“限位开关 运行时长 堵转检测”组合。关于校准我的思路是首次上电进入校准模式。手动或自动让卷帘运行到顶部按下校准按键记录“全开”位置。再让卷帘运行到底部按下校准按键记录“全关”位置。程序计算两个位置之间的运行时间作为后续百分比控制的基准。3.3 掉电记忆与手动控制智能设备最怕断电之后状态丢失。比如卷帘停在 50% 的位置断电重启后程序不知道当前位置如果直接发“打到底”指令就会因为偏移导致误动作。ESP32 的 NVSNon-Volatile Storage提供了掉电保存能力。我们可以把当前位置百分比、运行方向、运行时间等参数写入 NVS设备重启后自动恢复状态。在 Arduino 框架中使用Preferences.h库非常简单。手动控制方面我会保留两个物理按键一个上升、一个下降。这样即使手机没电或者 WiFi 断网家里的人依然可以手动操作。手动按键和遥控指令在代码里应该走同一条控制函数避免逻辑不一致。4. 完整实战ESP32 智能卷帘固件开发4.1 硬件接线下面以 ESP32 DevKit L298N 驱动的接线为例ESP32 引脚驱动板/设备说明GPIO 13IN1电机方向控制GPIO 12IN2电机方向控制GPIO 14ENAPWM 调速输入GPIO 27上限位开关输入内部上拉GPIO 26下限位开关输入内部上拉GPIO 25上升键输入内部上拉GPIO 33下降键输入内部上拉GND驱动板 GND共地5V驱动板 5V可选若驱动板带逻辑电源所有接线都必须在断电状态下完成。L298N 的电机电源使用 12V 供电但逻辑电源部分建议从 ESP32 板载 5V 引出或者独立供电避免电流干扰导致 ESP32 重启。4.2 基础电机控制代码先做一个最简单的电机点动控制示例验证接线和转向是否正确。// 文件路径smart-roller-shade/SmartRollerShade.ino #define IN1 13 #define IN2 12 #define ENA 14 void setup() { pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(ENA, OUTPUT); // 使用 LEDC 通道 0频率 1000Hz分辨率 8 位 ledcSetup(0, 1000, 8); ledcAttachPin(ENA, 0); } void loop() { // 电机正转 2 秒 digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); ledcWrite(0, 200); delay(2000); // 电机停止 digitalWrite(IN1, LOW); digitalWrite(IN2, LOW); ledcWrite(0, 0); delay(2000); // 电机反转 2 秒 digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH); ledcWrite(0, 200); delay(2000); // 电机停止 digitalWrite(IN1, LOW); digitalWrite(IN2, LOW); ledcWrite(0, 0); delay(2000); }这段代码的作用很直白上电后电机正转 2 秒停 2 秒反转 2 秒停 2 秒无限循环。通过这段代码先确认电机转向、驱动板电源、PWM 通道配置都是正确的。这里有一个容易踩的坑L298N 的 ENA 如果没有接 PWM而是直接接高电平很多板子的默认逻辑是锁住电机必须把 ENA 接到高电平或者 PWM 信号才能转起来。建议直接用ledcWrite输出一个固定的占空比。4.3 加入限位开关与状态管理点动测试通过后我们需要把代码升级成真正的“卷帘控制逻辑”。核心功能包括读取限位开关、判断当前状态、执行升降动作、遇到限位时立刻停。// 文件路径smart-roller-shade/SmartRollerShade.ino #define IN1 13 #define IN2 12 #define ENA 14 #define LIMIT_UP 27 #define LIMIT_DOWN 26 #define MOTOR_FREQ 1000 #define MOTOR_CHANNEL 0 #define MOTOR_RESOLUTION 8 enum ShadeState { STATE_STOPPED, STATE_MOVING_UP, STATE_MOVING_DOWN }; ShadeState currentState STATE_STOPPED; void setup() { Serial.begin(115200); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(ENA, OUTPUT); pinMode(LIMIT_UP, INPUT_PULLUP); pinMode(LIMIT_DOWN, INPUT_PULLUP); ledcSetup(MOTOR_CHANNEL, MOTOR_FREQ, MOTOR_RESOLUTION); ledcAttachPin(ENA, MOTOR_CHANNEL); stopMotor(); } void loop() { // 检测限位开关 bool upTriggered (digitalRead(LIMIT_UP) LOW); bool downTriggered (digitalRead(LIMIT_DOWN) LOW); if (currentState STATE_MOVING_UP upTriggered) { stopMotor(); Serial.println(Reached top limit); } if (currentState STATE_MOVING_DOWN downTriggered) { stopMotor(); Serial.println(Reached bottom limit); } // 简单演示串口输入命令u 上升d 下降s 停止 if (Serial.available() 0) { char cmd Serial.read(); switch (cmd) { case u: moveUp(); break; case d: moveDown(); break; case s: stopMotor(); break; } } delay(20); } void moveUp() { currentState STATE_MOVING_UP; digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW); ledcWrite(MOTOR_CHANNEL, 180); } void moveDown() { currentState STATE_MOVING_DOWN; digitalWrite(IN1, LOW); digitalWrite(IN2, HIGH); ledcWrite(MOTOR_CHANNEL, 180); } void stopMotor() { currentState STATE_STOPPED; digitalWrite(IN1, LOW); digitalWrite(IN2, LOW); ledcWrite(MOTOR_CHANNEL, 0); }这段代码加入了两个限位开关的判断上升途中如果触发了上限位就立刻停止下降途中触发下限位也立刻停止。这样能有效防止卷帘拉到顶后继续拉拽导致电机卡死。注意这里使用了INPUT_PULLUP也就是把 GPIO 内部上拉电阻打开限位开关另一端接 GND。当限位开关被触发时引脚读到低电平。4.4 行程校准与 NVS 掉电存储为了让卷帘支持“打开 30%”这种百分比控制我们需要校准全开行程和全关行程并把校准结果保存下来。先定义一个校准数据结构struct ShadeCalibration { uint32_t fullOpenTimeMs; // 从下到上运行时间 uint32_t fullCloseTimeMs; // 从上到下运行时间 bool calibrated; };校准流程放在手动模式下执行按下降键让卷帘运行到底部。按停止键然后长按“校准键”把当前位置记为“全关位置”。按上升键让卷帘运行到顶部。按停止键再长按“校准键”程序根据两次运行时间计算行程。写入 NVS 的示例#include Preferences.h Preferences prefs; void saveCalibration(ShadeCalibration cal) { prefs.begin(shade, false); prefs.putUInt(open_ms, cal.fullOpenTimeMs); prefs.putUInt(close_ms, cal.fullCloseTimeMs); prefs.putBool(calibrated, cal.calibrated); prefs.end(); } bool loadCalibration(ShadeCalibration cal) { prefs.begin(shade, true); bool exists prefs.getBool(calibrated, false); if (exists) { cal.fullOpenTimeMs prefs.getUInt(open_ms, 0); cal.fullCloseTimeMs prefs.getUInt(close_ms, 0); cal.calibrated true; } prefs.end(); return exists; }在核心控制函数中执行百分比开合时把目标百分比换算成运行时间void moveToPercent(int percent) { if (percent 0) percent 0; if (percent 100) percent 100; // 计算当前状态不在代码中简化这里给出核心换算思路 unsigned long targetRuntimeMs cal.fullOpenTimeMs * percent / 100; // 然后根据当前位置决定电机方向并启动定时停止任务 }百分比控制的精度依赖运行时间的稳定。实际项目中卷帘在运行过程可能存在惯性滑落、机械摩擦变化等误差。一个更稳健的设计是把“预计时间”与“限位开关信号”结合在运行过程中如果提前触发限位就立即停止并更新当前位置如果按计划时间运行结束还没触发限位也要停止并记下偏差。4.5 接入 MQTT 协议设备端的控制逻辑写好后接下来就是把卷帘接入 MQTT让手机端和智能家居平台都能给它发指令。假设你的局域网里已经运行了 MQTT Broker例如 Mosquitto或者使用 Home Assistant 自带的 MQTT 集成。ESP32 端的代码用PubSubClient库来实现客户端的连接和消息接收。#include WiFi.h #include PubSubClient.h const char* ssid YOUR_WIFI_SSID; const char* password YOUR_WIFI_PASSWORD; const char* mqttServer 192.168.1.100; const int mqttPort 1883; const char* topicCommand home/shade/command; const char* topicState home/shade/state; WiFiClient espClient; PubSubClient mqttClient(espClient); void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(WiFi connected); mqttClient.setServer(mqttServer, mqttPort); mqttClient.setCallback(callback); connectMQTT(); } void connectMQTT() { while (!mqttClient.connected()) { Serial.print(Attempting MQTT connection...); if (mqttClient.connect(ESP32Shade)) { Serial.println(connected); mqttClient.subscribe(topicCommand); } else { delay(2000); } } } void callback(char* topic, byte* payload, unsigned int length) { String message; for (unsigned int i 0; i length; i) { message (char)payload[i]; } Serial.printf(Command received: %s\n, message.c_str()); if (message OPEN) { moveUp(); } else if (message CLOSE) { moveDown(); } else if (message STOP) { stopMotor(); } else if (message.startsWith(SET_PERCENT)) { int percent message.substring(11).toInt(); moveToPercent(percent); } } void loop() { if (!mqttClient.connected()) { connectMQTT(); } mqttClient.loop(); // 状态上报可以在这里按需发布 mqttClient.publish(topicState, {\position\: 50, \state\: \stopped\}); delay(1000); }这段代码的核心逻辑是订阅home/shade/command主题收到不同字符串指令后调用对应的电机控制函数。设备自己运行的状态比如当前开合百分比会发布到home/shade/state主题方便平台端同步显示。MQTT 指令协议的设计不必追求复杂字符串指令足够直观也方便调试。如果以后要扩展更多设备可以考虑在 payload 里使用 JSON 格式带上 deviceId、cmd、value 等字段但接入单台设备时直接使用简单字符串反而更省心。5. 接入 Home Assistant 与自动化联动5.1 在 Home Assistant 中配置 MQTT CoverHome Assistant 中专门有一个mqttcover 平台天然适合控制卷帘类设备。在configuration.yaml里新增如下配置mqtt: cover: - name: Living Room Shade unique_id: living_room_shade command_topic: home/shade/command state_topic: home/shade/state position_topic: home/shade/state position_open: 100 position_closed: 0 payload_open: OPEN payload_close: CLOSE payload_stop: STOP optimistic: false这里的思路是Home Assistant 把home/shade/command作为指令下发通道把home/shade/state作为状态订阅通道。点击前端界面的“打开”“关闭”“停止”按钮时HA 会向 command_topic 发布 payload_open、payload_close、payload_stop触发设备端对应的 MQTT 回调。如果你的设备端上报的 JSON 状态里包含position: 50字段HA 可以通过json_attributes_topic或position_topic解析百分比并在界面上显示滑块位置。5.2 定时与光线联动自动化接入 HA 之后自动化就很简单了。下面这个自动化表示“每天日出时打开卷帘”automation: - alias: Open shade at sunrise trigger: platform: sun event: sunrise action: - service: cover.open_cover target: entity_id: cover.living_room_shade再比如“当室内光照低于某个阈值时自动关闭卷帘”可以在自动化里使用光照传感器实体。这里给出核心思路automation: - alias: Close shade when too dark trigger: platform: numeric_state entity_id: sensor.bedroom_illuminance below: 200 action: - service: cover.close_cover target: entity_id: cover.living_room_shade光敏传感器如果直接接在 ESP32 上也可以把光照读数通过 MQTT 发到 HA再由 HA 做判断。但建议把简单联动逻辑放在设备端本地执行比如光线极暗时直接关帘这样即使 HA 不可用设备也能完成基本自动化。5.3 语音控制与 App 控制Home Assistant 接入后语音控制可以通过以下方式实现HomeKit Bridge在 HA 中启用 HomeKit 配置cover 实体自动暴露给 iPhone 的“家庭”App可以使用 Siri 控制。Alexa / 小度 / 小爱如果使用的是对应生态需要 HA 的云组件或桥接插件。HA App直接手机安装 Home Assistant App内网环境下打开实体页面就能操作。实现语音控制不需要在 ESP32 端写任何代码所有上层智能都交给 HA 处理。这也是当初选择 MQTT 协议而不是自制私有 App 的原因标准协议带来的生态兼容性远比自己重复造轮子要划算。6. 常见问题与排查思路6.1 常见问题速查表问题现象常见原因解决思路ESP32 上电后反复重启电源供电不足或电流毛刺单独给驱动板供电确认 12V 电源电流余量电机不转驱动板 ENA 未接 PWM或逻辑电源未接检查 ENA 引脚确认驱动板逻辑电源灯亮卷帘到顶后还在转限位开关接错或未触发用万用表或串口打印确认引脚电平变化百分比控制不准行程校准数据丢失或电机转速漂移重新校准结合限位开关做闭环停止WiFi 经常掉线ESP32 天线位置差或电源波动检查天线位置给电源加滤波电容MQTT 收不到指令Broker 地址、端口或主题错误用 MQTT 客户端工具订阅主题进行验证6.2 电源问题导致的自动重启ESP32 设备最典型的问题就是电机启动瞬间电流过大把供电电压拉低导致开发板复位。这个问题在 L298N 双路驱动大电流电机时非常常见。解决思路有三种独立给电机供电电机供电和 ESP32 供电完全分开只共地。使用更大电流的适配器12V/2A 起步给电机启动电流留出余量。在电源输入端并联大容量电解电容比如 470uF 或 1000uF吸收瞬时电流冲击。6.3 行程校准不稳定的排查步骤如果你发现卷帘每次运行后的停止位置都不一样按照下面的顺序排查确认限位开关安装位置是否松动触发片是否每次都能稳定碰到开关。检查电机的减速比如果电机转速偏高惯性会让卷帘在断电后继续滑一段距离。重新校准并且增加“到位后反转 0.2 秒刹车”的逻辑减少惯性误差。如果采用纯时间控制检查卷帘的负载是否恒定负载变化会直接影响转速。7. 最佳实践与工程建议7.1 机械结构优先于代码智能卷帘项目里软件逻辑写不好可以改但机械结构如果安装不稳定后面基本无法正常使用。安装卷帘支架时一定要确保卷管水平、两端固定牢固否则卷帘在升降过程中会偏移导致限位开关触发异常。另外电机和卷管之间的连接方式也很重要。最常见的方案是使用联轴器连接电机输出轴和卷管转轴。如果卷管内径较大可以加装一个配件把电机输出轴套在卷管接头里再用螺丝锁紧。这一部分在 DIY 项目里最容易翻车建议先在不装布料的情况下空转测试确认传动稳定后再装上帘布。7.2 固件侧的容错与日志设计设备写进固件里最好的习惯就是“所有动作都要有可追踪的日志”。我建议在代码里统一封装一个logState()函数定期把当前状态、当前位置、限位状态、WiFi 信号强度输出到串口同时通过 MQTT 发布到home/shade/log主题。后期排错时翻一下 MQTT 日志记录就能知道设备当时发生了什么。电机控制函数也不要一封到底。建议保留一个“手动停止”优先级任何控制指令在执行过程中如果收到 STOP 指令或按键按下都立即停住。这个优先级要写进控制状态机不能依赖循环的自然结束。7.3 安全建议智能卷帘长期通电而且电机属于运动部件安全方面必须注意220V 转 12V 电源适配器要选择有 3C 认证的产品做好隔离。所有高压和低压接线分开布线避免线皮磨损导致短路。卷帘运行区域不要有儿童玩耍时容易被缠住的风险如果有小孩优先选择无拉绳设计。代码里设置“单次最大运行时间”比如 60 秒防止限位开关失效后电机一直运转烧毁。修改生产环境或长期运行的代码前先在桌面环境验证所有边界条件。关于“单次最大运行时间”代码实现很简单unsigned long motorStartTime 0; const unsigned long MAX_RUN_TIME_MS 60000; void checkMotorTime() { if (currentState ! STATE_STOPPED) { if (millis() - motorStartTime MAX_RUN_TIME_MS) { stopMotor(); Serial.println(Safety timeout triggered); } } }7.4 从单设备到多设备的扩展如果你家里不止一个窗户建议从一开始就考虑多设备扩展。这时候 MQTT 主题的规范性就更重要了比如使用这种结构home/shade/living_room/command home/shade/living_room/state home/shade/bedroom/command home/shade/bedroom/state在 HA 中配置每个实体时把 command_topic 对应到不同房间即可。设备端代码可以用宏定义当前设备 ID编译时一次性烧录多块板子但每个板子的DEVICE_ID不同。7.5 固件升级预留 OTA 能力设备已经装到窗帘盒里之后每次改代码都拆下来插 USB 线会非常痛苦。建议在固件里预留 OTAOver-The-Air升级能力。Arduino 框架下有ArduinoOTA库使用起来非常简单#include ArduinoOTA.h void setupOTA() { ArduinoOTA.begin(); } void loopOTA() { ArduinoOTA.handle(); }把setupOTA()加在setup()末尾loopOTA()加在主循环里之后 Arduino IDE 就可以通过 WiFi 端口直接上传程序。需要注意的是OTA 升级过程中如果断电设备可能变砖。生产环境中最好配合“双分区运行”机制但 DIY 场景下只要确保升级时供电稳定基本足够。8. 总结与拓展方向自己做智能卷帘和买成品最大的区别不只是省钱而是能把控制逻辑完全掌握在自己手里。整套项目做下来核心收获可以总结为三条一是理解了直流电机的驱动与控制原理二是熟悉了 ESP32 开发、MQTT 协议和 Home Assistant 的集成方式三是建立了一套从硬件到软件的完整排错思路。如果你准备照着这个方向继续深入接下来可以尝试这几个方向把单台设备的控制逻辑统一抽象成“设备模板”支持多台卷帘共用一套固件。加入环境光传感器和温湿度传感器让卷帘根据室内外条件自动调节。研究无刷电机或管状电机进一步降低噪音、提升扭矩。把控制板做成 PCB 而不是洞洞板跳线提升长期运行的可靠性。如果对低功耗有要求可以研究 ESP32 的 Deep Sleep 模式在不使用时降低功耗。说到底智能家居的灵魂不是某一个 App而是设备之间能够稳定、可靠地协同工作。这套以 ESP32 为核心、MQTT 为通信协议的自制智能卷帘方案是一套非常适合动手实践的样板工程。建议你从一块开发板、一个小电机开始先跑通最基础的控制逻辑再一步步加入更多功能。只要把基础架构搭稳后续的扩展就会顺畅很多。