1. 项目缘起:一个被天气“玩弄”的阳台
我家阳台朝南,采光极好,按理说是晾晒衣物的绝佳场所。但住久了就会发现,南方的天气就像小孩的脸,说变就变。早上出门时还是艳阳高照,下午一场突如其来的雷阵雨就能把晾了一天的衣服浇个透心凉。更别提梅雨季节,空气湿度大,衣服晾上好几天都干不透,还容易有股霉味。这种“看天吃饭”的晾衣体验,实在是让人头疼。
于是,一个想法在我脑子里冒了出来:能不能做一个智能晾衣架?它不需要像市面上的高端产品那样能自动升降、紫外线消毒,只需要解决一个核心痛点——根据天气情况自动收放衣物。晴天自动展开,雨天或夜晚自动收回,遇到突发降雨也能及时响应。这个想法听起来简单,但要把“感知-决策-执行”这个闭环跑通,里面有不少门道。
我给它起了个名字,叫“ACEBOTT Smart Home: Automatic Drying Rack”。ACEBOTT没什么特殊含义,就是觉得听起来挺酷。核心思路就是用ESP32作为大脑,连接温湿度、雨滴和光照传感器,再驱动一个舵机来控制晾衣架的“开合”。这不仅仅是一个DIY玩具,更是将智能家居理念落到一个具体、高频的生活场景中的尝试。接下来,我就把从构思到实现的完整过程,以及中间踩过的坑、获得的经验,毫无保留地分享出来。
2. 核心部件选型:为什么是ESP32和这些传感器?
做硬件项目,选型是第一步,也是最关键的一步。选对了,事半功倍;选错了,可能项目还没开始就结束了。我这个自动晾衣架的核心需求是:联网获取天气、本地感知环境、做出决策并驱动机械结构。围绕这几点,我开始了选型。
2.1 主控芯片:ESP32为何是“不二之选”
主控芯片的选择范围其实很广,从经典的Arduino Uno到功能更强的STM32,再到树莓派Pico。但我几乎没怎么犹豫就选择了ESP32,原因有以下几点:
第一,双核与Wi-Fi/蓝牙的完美结合。ESP32自带Wi-Fi和蓝牙模块,这意味着它天生就能轻松接入家庭局域网,甚至直接连接手机。对于需要获取网络天气数据的应用来说,这是刚需。它的双核设计(通常一个核心运行无线协议栈,另一个核心处理用户程序)也保证了在处理网络请求的同时,本地传感器数据采集和控制逻辑不会卡顿。相比之下,Arduino Uno需要额外加装Wi-Fi模块(如ESP8266),不仅增加了接线复杂度和成本,性能上也未必有优势。
第二,丰富的IO口与周边外设。这个项目需要连接多个传感器(温湿度、雨滴、光照)和一个舵机。ESP32提供了充足的GPIO、ADC、PWM等资源,完全够用。它的ADC精度虽然被吐槽,但对于判断“有没有下雨”、“光照强不强”这种定性需求,完全足够。
第三,强大的社区与生态。ESP32背后有乐鑫官方的IDF框架,也有基于Arduino核心的丰富库支持。这意味着无论你是喜欢用Arduino IDE的简单快捷,还是追求用ESP-IDF进行更底层的性能优化,都有成熟的路径。网上相关的教程、项目、问题解答浩如烟海,遇到坑很容易找到解决方案。
注意:ESP32型号很多,如ESP32-WROOM-32、ESP32-S3等。对于这个项目,最普通、最便宜的ESP32 DevKit V1板子就完全足够了,没必要追求新款。
2.2 环境感知“三剑客”:传感器选型与原理
要让晾衣架变得“聪明”,必须给它装上“眼睛”和“皮肤”。我选择了三种传感器来构建环境感知系统。
DHT22温湿度传感器:这是非常经典的数字传感器。我选择它而不是更便宜的DHT11,主要是因为DHT22的湿度测量范围更广(0-100% RH),精度也更高(±2% RH)。在判断“空气是否潮湿,衣服是否容易干”时,更高的精度更有参考价值。它采用单总线通信,只需要一个GPIO口,接线简单。库支持也非常成熟,在Arduino IDE中直接搜索“DHT sensor library”即可。
雨滴传感器模块:这是一个模拟传感器。它的表面有平行的导线,当雨水滴落时,会改变导线间的电阻,从而输出一个变化的模拟电压值。干燥时输出电压高(接近VCC),潮湿时输出电压低。我们需要用ESP32的ADC去读取这个电压值,并设定一个阈值来判断是否下雨。这里有个关键点:市面上常见的雨滴传感器模块容易氧化,长期在户外使用可靠性会下降。我的做法是,购买质量好一点的模块,并且将其安装在有少许遮挡但又不会影响雨水滴落的位置(比如阳台顶棚边缘下方),减少直接暴晒和持续淋雨。
光敏电阻模块:同样是一个模拟传感器。它的电阻值随光照强度变化,模块通常将其转换为电压输出。我用它来判断是白天还是黑夜,以及是否是阳光充足的晴天。和雨滴传感器一样,通过ADC读取电压,设定阈值。例如,电压高于某个值认为是强光(白天),低于某个值认为是弱光(夜晚或阴天)。
为什么不直接用网络天气API?这是一个很好的问题。网络API(如和风天气、OpenWeatherMap)能提供精确的天气预报和实时天气。我确实用了它作为主要决策依据,比如获取未来2小时是否会下雨的预报。但本地传感器是必不可少的补充和保障。原因有二:1.网络延迟与故障:API更新有延迟,网络也可能临时中断。本地传感器可以实时检测到“已经开始下雨了”,实现最快速度的应急响应。2.微环境差异:天气预报是针对一个较大区域的,而你家的阳台可能因为楼栋遮挡、风向等原因,天气状况与预报有细微差别。本地传感器感知的就是你阳台最真实的微环境。
2.3 执行机构:舵机的扭矩与安装考量
晾衣架的开合动作,我选择用舵机来实现。舵机是一种可以精确控制角度的电机,非常适合这种需要在一定角度范围内往复运动的应用。
舵机选型关键参数:扭矩。扭矩单位是kg·cm,意思是舵机在1厘米长的力臂上能输出多少公斤的力。你的晾衣架(尤其是挂满湿衣服后)有多重?舵机的安装位置(力臂)有多长?这决定了你需要多大扭矩的舵机。我最初用一个9g的小舵机(扭矩约1.6kg·cm)试了一下,推动空载的小型晾衣架模型还行,但一旦加上一点负载就卡住了。最后我换成了一个MG996R金属齿轮舵机,扭矩在10kg·cm左右,并且是金属齿轮,更耐用。即使晾衣架上挂了几件厚衣服,也能稳稳地带动。
安装方式决定省力与否。舵机直接推拉晾衣架可能很费力。我借鉴了连杆机构的思想。将舵机固定在底座上,舵机摇臂通过一根连杆与晾衣架的活动部分连接。这样,舵机旋转运动就被转换成了晾衣架的开合运动。通过调整舵机摇臂和连杆的长度,可以在一定程度上放大扭矩或行程,这是一个简单的机械设计,能极大提升系统可靠性和降低对舵机扭矩的要求。
供电必须独立。MG996R这类舵机在堵转(遇到阻力)时电流可以高达2A,瞬间电流更大。ESP32开发板的USB口或板上稳压芯片根本无法提供如此大的电流。如果直接接在开发板上,会导致ESP32重启甚至损坏。正确的做法是:舵机必须使用独立电源供电(如专用的5V/2A适配器或大容量电池),并且确保其电源地与ESP32的电源地(GND)连接在一起,形成共地。这是硬件接线中至关重要的一步。
3. 系统设计与程序逻辑:从感知到行动的决策链
硬件搭好了,接下来就是让整个系统“活”起来。程序逻辑的设计,核心是构建一个稳定、可靠且有一定容错能力的决策链条。
3.1 软件框架与多任务处理
在Arduino环境下,程序主要运行在setup()和loop()函数中。对于需要同时处理网络请求、传感器读取和舵机控制的应用,如果所有代码都堆在loop()里,很容易因为某个耗时操作(如网络请求)导致其他任务(如检测下雨)响应不及时。
我的策略是采用基于时间戳的非阻塞式编程。简单说,就是不让程序“等待”。例如,向网络API请求天气数据,可能需要1-2秒。在等待期间,程序应该继续去读取传感器、检查状态,而不是卡在那里。以下是核心逻辑框架:
// 定义各种动作的间隔时间(毫秒) unsigned long lastWeatherCheckTime = 0; const long weatherCheckInterval = 300000; // 5分钟检查一次天气 unsigned long lastSensorReadTime = 0; const long sensorReadInterval = 10000; // 10秒读取一次本地传感器 void loop() { unsigned long currentMillis = millis(); // 获取当前时间 // 任务1:定时检查网络天气 if (currentMillis - lastWeatherCheckTime >= weatherCheckInterval) { lastWeatherCheckTime = currentMillis; fetchWeatherData(); // 这是一个非阻塞或快速处理的函数 } // 任务2:定时读取本地传感器 if (currentMillis - lastSensorReadTime >= sensorReadInterval) { lastSensorReadTime = currentMillis; readDHTSensor(); readRainSensor(); readLightSensor(); makeLocalDecision(); // 基于传感器数据做本地决策 } // 任务3:综合决策与舵机控制 makeFinalDecisionAndControlServo(); // 其他任务,如处理网络连接、响应Web控制等... server.handleClient(); }这样,每个任务都在自己的时间节奏下运行,互不干扰。fetchWeatherData()函数内部我使用了ESP32的HTTPClient库,它本身在begin()和GET()阶段是阻塞的,但由于我们数分钟才调用一次,且执行时间相对较短,在可接受范围内。对于更高要求的场景,可以使用异步HTTP库。
3.2 决策逻辑的优先级与“防抖”
决策是核心大脑。我设计了一个带有优先级的决策逻辑:
- 最高优先级:本地雨滴传感器触发。只要检测到有雨(模拟值超过阈值),立即执行收回动作,无需等待网络确认。这是安全底线。
- 次高优先级:网络天气预报。如果API返回未来一段时间(如下一小时)降水概率极高(如>80%),则执行收回动作。即使此刻阳台没雨,也要相信预报,提前防范。
- 日常优先级:时间与光照。结合网络API获取的日出日落时间(或本地光敏电阻判断),在夜晚自动收回,在日出后根据温湿度条件决定是否展开。如果湿度较低、光照好,则展开;如果是阴雨天(网络预报+本地光照弱),则保持收回。
- 手动干预优先级:永远最高。我通过一个简单的Web服务器(ESP32作为AP或连接家庭Wi-Fi后),做了一个控制页面。可以手动点击“展开”、“收回”、“自动”按钮。一旦手动操作,系统会进入手动模式一段时间(如30分钟),在此期间自动逻辑暂停,之后恢复自动模式。
这里必须引入“状态防抖”机制。传感器可能会受到偶然干扰,比如一滴鸟屎掉在雨滴传感器上,或者一阵风短暂吹来一片树叶挡住光敏电阻。如果因此就触发收放动作,系统会变得非常“神经质”。
我的做法是:采用累计触发或延时确认。例如,对于雨滴传感器,不是检测到一次就动作,而是连续3次读取(间隔2秒)都超过阈值,才确认为下雨。对于网络天气,如果只是短暂显示有雨,可以设置一个“观察期”,比如连续两次API请求(间隔10分钟)都预报有雨,才执行动作。这能有效避免误操作。
3.3 网络通信与API使用
ESP32连接Wi-Fi是基础操作。我建议在代码里加入配网功能,比如使用著名的WiFiManager库。这样第一次使用时,ESP32会开启一个AP热点,手机连接后可以输入家里的Wi-Fi账号密码,之后就能自动连接,非常方便,避免了将密码硬编码在代码里的安全风险和不便。
天气API我选择的是和风天气的免费版。它提供了“实时天气”、“逐小时预报”等接口。对于这个项目,“逐小时预报”尤其有用。申请账号后,你会得到一个Key。调用API的代码片段如下:
void fetchWeatherData() { if (WiFi.status() != WL_CONNECTED) { return; // 网络未连接,直接返回 } HTTPClient http; // 构造请求URL,包含你的Key和城市LocationID String url = "https://devapi.qweather.com/v7/weather/24h?location=YOUR_LOCATION_ID&key=YOUR_API_KEY"; http.begin(url); int httpCode = http.GET(); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); // 解析JSON,获取未来几小时的降水概率(pop字段)、天气状况等 // 例如,解析第一个小时的数据,如果pop > 80,则标记为即将下雨 DynamicJsonDocument doc(1024); deserializeJson(doc, payload); int pop = doc["hourly"][0]["pop"]; // 降水概率百分比 String text = doc["hourly"][0]["text"]; // 天气状况文字,如“小雨” if (pop > 80 || text.indexOf("雨") >= 0) { forecastRain = true; } else { forecastRain = false; } } else { // 网络请求失败,记录日志,可能依赖本地传感器 Serial.println("Weather API request failed."); } http.end(); }提示:JSON解析我使用了
ArduinoJson库,非常高效。记得根据返回数据的大小预留足够的DynamicJsonDocument缓冲区,否则解析会失败。
4. 硬件搭建与结构设计:从电路到机械的实践
理论设计得再好,最终都要落到实际的电路板和机械结构上。这是最能体现DIY乐趣,也最容易踩坑的地方。
4.1 电路连接与电源管理
整个系统的电路连接图并不复杂,但有几个细节决定了成败。
电源部分:这是重中之重。我采用了两路供电:
- 5V/2A直流电源适配器:作为主电源。它的输出正负极先接入一个电源开关,方便整体断电。
- 电源分配:开关之后,正极(5V)分成两路。一路直接给舵机供电。另一路接入一个降压模块(如AMS1117-3.3),将5V降为3.3V,给ESP32开发板和所有传感器模块供电。所有设备的GND(负极)必须全部连接在一起,共地是电路正常工作的基础。
信号连接:
- ESP32 GPIO->传感器信号线:DHT22接一个GPIO(如GPIO4);雨滴和光敏的AO(模拟输出)分别接ESP32的ADC引脚(如GPIO34, GPIO35)。
- ESP32 GPIO->舵机信号线:选择一个支持PWM输出的GPIO(如GPIO13),接舵机的信号线(通常是橙色或白色)。舵机的供电(红色)和地(棕色)接独立电源。
实际搭建建议:不要在面包板上完成最终作品。面包板连接不可靠,稍微震动就可能松脱。建议使用**洞洞板(万用板)**进行焊接,或者直接设计一块简单的PCB。对于线缆,尤其是连接舵机的线,最好使用杜邦线公对母,并将接口用热熔胶固定一下,防止拉扯脱落。
4.2 机械结构设计与实现
晾衣架的本体,我直接改造了一个现有的、小型的手动折叠晾衣架。你也可以用木条、铝型材自己搭建。核心是设计一个可靠的传动机构,将舵机的旋转运动转化为晾衣架的展开(水平)与收回(垂直或倾斜)运动。
我采用的是一种简单的四连杆机构:
- 将舵机固定在晾衣架底座的一个坚固位置。
- 舵机摇臂(舵盘)通过一个螺丝与一根长度合适的金属连杆(如铝合金条)的一端连接。
- 金属连杆的另一端,铰接在晾衣架活动部分(需要被推拉的那根横杆)的某个点上。
当舵机从0度旋转到90度时,通过连杆推动晾衣架的活动部分,使其从垂直收纳状态变为水平展开状态。你需要反复调试舵机的初始角度(servo.write(0)对应的实际位置)和终止角度(servo.write(90)对应的位置),确保运动范围刚好覆盖所需行程,且没有死点或卡死现象。
结构加固:舵机本身的固定一定要非常牢固。我用了多个螺丝和L型角铁将其锁死在底座上。连杆与晾衣架的铰接点,也使用了带轴承的连杆接头,减少摩擦和磨损。整个结构在运动时应该顺畅、无晃动。
4.3 外壳与防护
电子部分不能裸露在外。我使用了一个尺寸合适的防水接线盒来容纳ESP32开发板、降压模块和接线端子。在盒子侧面开孔,用于穿出传感器线、电源线和舵机线。开孔处最好使用防水电缆接头(PG头),既能固定线缆,又能防止进水。
传感器本身也需要一定防护。DHT22我加了一个小型的防辐射罩,避免阳光直射导致温度读数偏高。雨滴传感器和光敏电阻模块,我将其安装在从防水盒延伸出的一块小亚克力板下,亚克力板既能提供一定遮蔽,又不会完全阻挡雨水和光线。
5. 调试、优化与踩坑实录
把代码烧录进去,硬件组装完毕,通电测试——这往往只是“万里长征第一步”,真正的挑战在于调试和优化。
5.1 舵机抖动与定位不准
问题现象:舵机在到达指定角度后不停抖动,发出“滋滋”声,或者每次动作停止的位置有细微差别。
根因分析:
- 电源功率不足:这是最常见的原因。舵机在负载下工作电流很大,如果电源(特别是那根USB线或者移动电源)输出能力不够,电压会被拉低,导致舵机驱动芯片工作不稳定,产生抖动。同时,电压波动也会影响ESP32的稳定。
- 信号干扰:PWM信号线如果过长,且与电源线并行走线,可能会引入干扰。
- 机械阻力过大:如果晾衣架结构不顺畅,存在摩擦或卡点,舵机为了到达指定位置会持续输出力矩,导致抖动和发热。
解决方案:
- 升级电源:立刻换用我前面提到的独立5V/2A以上电源适配器,并确保导线足够粗(建议18AWG或更粗)。
- 电源滤波:在舵机的电源正负极之间,并联一个大电容(如470uF 16V的电解电容),可以吸收瞬间电流冲击,稳定电压。
- 优化走线:将舵机信号线与电源线分开走,或使用双绞线。
- 调整机械结构:仔细检查连杆各个铰接点,确保转动灵活,必要时加润滑油。重新评估舵机安装位置和连杆长度,使机构在死点位置附近运动更省力。
- 软件消抖:在
servo.write()函数后,可以适当加一小段延时delay(15),让舵机有足够时间稳定下来。
5.2 网络连接不稳定与API请求失败
问题现象:ESP32经常连不上Wi-Fi,或者连接后很快断开,导致天气数据无法更新。
根因分析:
- Wi-Fi信号弱:阳台可能距离路由器较远,或有墙体阻隔。
- ESP32的Wi-Fi功耗策略:为了省电,ESP32在某些模式下可能会主动断开Wi-Fi。
- 代码逻辑问题:网络连接处理代码不健壮,没有重连机制。
解决方案:
- 增强信号:可以考虑使用Wi-Fi信号中继器,或者换用天线更长的ESP32模块。
- 修改Wi-Fi模式:在
setup()中,使用WiFi.setSleep(false)禁用Wi-Fi休眠,以获得更稳定的连接,代价是功耗略有增加。 - 实现健壮的重连机制:在
loop()中定期检查Wi-Fi连接状态,如果断开则尝试重连。
void checkWiFiConnection() { if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi disconnected. Reconnecting..."); WiFi.disconnect(); WiFi.reconnect(); delay(5000); // 等待重连 if (WiFi.status() == WL_CONNECTED) { Serial.println("WiFi reconnected."); } } }- 为API请求添加超时和重试:HTTPClient可以设置超时时间
http.setTimeout(5000)。对于失败的请求,可以记录并在下一次循环中重试,但不要在一个loop内无限重试,以免阻塞其他任务。
5.3 传感器数据噪声与阈值校准
问题现象:雨滴传感器在明明没雨的时候偶尔跳变,光敏电阻在阴天和傍晚的读数难以区分。
根因分析:模拟传感器易受干扰,且阈值需要根据实际安装环境进行现场校准。
解决方案:
- 硬件滤波:在传感器的模拟输出引脚与地之间,并联一个0.1uF的瓷片电容,可以滤除一部分高频噪声。
- 软件滤波:采用滑动平均滤波。不是取一次读数,而是连续读取N次(比如10次),然后取平均值。这能有效平滑随机噪声。
int readRainSensorSmooth() { const int numReadings = 10; int total = 0; for (int i = 0; i < numReadings; i++) { total += analogRead(RAIN_SENSOR_PIN); delay(2); // 短暂延时,避免ADC读取过快 } return total / numReadings; }- 现场动态校准:不要写死阈值。我写了一个简单的“校准模式”,通过串口指令或Web按钮触发。在校准模式下,程序会持续读取传感器数据并输出到串口监视器。我分别在“完全干燥”、“小雨滴落”、“大雨”状态下记录读数,从而确定“触发下雨”的合理阈值。光照阈值也同理,在“正午阳光”、“阴天”、“夜晚”分别记录。
5.4 意外情况处理与系统鲁棒性
一个成熟的系统必须能处理意外。
断电记忆:如果突然停电,再来电时,晾衣架应该处于什么状态?我使用ESP32的EEPROM(或更优的Preferences库)来保存当前状态(展开/收回/自动)。在setup()中读取这个状态,并驱动舵机恢复到断电前的位置。注意,舵机在上电时可能会有一个自检的抖动动作,在代码初始化时要控制好上电顺序,先初始化状态,再给舵机信号。
手动优先与状态恢复:如前所述,手动操作后,系统进入“手动模式”并开始计时。计时结束后,系统不是粗暴地切换回自动模式并立即动作,而是先读取一次当前所有的传感器和网络数据,根据当前的环境条件决策下一步动作,实现平滑过渡。
看门狗定时器:为了防止程序跑飞导致系统死机,我启用了ESP32的硬件看门狗。
#include <esp_task_wdt.h> void setup() { esp_task_wdt_init(10, true); // 10秒看门狗超时 esp_task_wdt_add(NULL); // 添加当前任务到看门狗监控 } void loop() { esp_task_wdt_reset(); // 在loop循环中定期“喂狗” // ... 其他代码 }这样,如果程序卡死在某个地方超过10秒,看门狗会自动重启ESP32,让系统恢复。
经过以上这些设计、实现、调试和优化,这个ACEBOTT智能晾衣架已经在我家阳台稳定运行了好几个月。它成功地在好几次突如其来的暴雨前收回了衣服,也在晴朗的早晨自动展开,真正做到了“衣来伸手,收衣不愁”。这个项目不仅解决了实际问题,更是一次对嵌入式系统设计、传感器应用、网络通信和机械结构的综合实践。如果你也有类似的痛点,不妨动手试试,过程中学到的东西,远比一个现成的产品要多得多。