ESP32漏水报警器DIY:传感器断开检测原理与代码实现
1. 为什么漏水报警器必须能“感知”传感器断开做漏水报警器的朋友十有八九都遇到过这种场面某个角落的传感器线被老鼠咬断或者插头松了防水探头直接悬空而主机还安安静静地显示“正常”。直到水漫金山报警器一声不吭你才意识到——它压根没在工作。这不是设备的锅是设计逻辑的问题。传统的漏水报警器很多只是被动地检测“有没有水”一旦传感器离线系统完全不知道。我们这次要做的是一个能主动“感知”传感器断开的ESP32漏水报警器。标题里那句“Detects a Disconnected Sensor”就是核心不只要能报漏水还要能报“传感器不见了”。这个功能听起来简单实际做起来牵扯到硬件电路、软件状态判断、误报抑制、多传感器管理等一系列问题。这篇文章我会从需求拆解一路讲到代码实现、实测踩坑把我做这套设备的完整思路和可复现的方案分享出来。适合想自己动手做智能家居安防、或者正在做工业环境监测的朋友参考也适合刚接触ESP32的初学者——我尽量把原理讲透代码给全。2. 需求拆解与系统设计从“有水报警”到“离线报警”2.1 漏水检测的本质不是一个“开关”而是一个“状态”很多人把漏水检测想得太简单两个电极碰见水通路报警。实际上一个可靠的漏水报警器要处理的状态至少有四种正常干燥传感器没有水系统正常漏水触发传感器有水需要报警传感器断开引脚悬空或线缆断路需要提示传感器短路线缆粘连或进水导致常导通可能是故障前两种是常规功能后两种是“自我诊断”。如果只看前两种一旦传感器断开系统会误认为“干燥”从而保持静默——这是致命缺陷。所以我们的系统设计必须把断开和干燥明确区分开。2.2 硬件方案选型为什么用ESP32而不选Arduino Uno主控选择ESP32理由很直接ADC通道数量和多路GPIO足够可以同时挂多个漏水传感器还能留出引脚做继电器、蜂鸣器、LED指示。自带Wi-Fi和蓝牙后续非常容易扩展为局域网告警、手机推送不需要额外加通信模块。价格优势明显和带Wi-Fi模块的Uno方案相比整套系统更简洁。开发环境成熟用Arduino框架或者ESP-IDF都可以本文示例使用Arduino框架门槛低、上手快。关于传感器本体我建议用双探头式的漏水检测模块而不是裸电极。模块内部自带比较器输出数字信号同时也有模拟输出引脚。我用的是国产常见的“漏水检测传感器模块”带LM393比较器那种便宜且好用。不过要注意模块的数字输出只能告诉我们“有没有水”无法区分断开——因为很多模块断开时输出和干燥时一样。所以我们需要自己动手改造读取方式。2.3 整体架构一主多从的传感器轮询模式系统分三层传感器层每个漏水探头通过三根线接入主控——电源(VCC)、地(GND)、信号线(SIG)。信号线既可以用作数字输入也可以接到ADC引脚读取模拟值。控制层ESP32负责周期性地轮询每个传感器判断状态并记录时间戳。一旦发现状态变化立即处理。告警层本地蜂鸣器、LED、继电器可接电磁阀同时通过Wi-Fi发送通知后面可以扩展MQTT或局域网HTTP。轮询方式的优势是简单可靠不需要传感器具备主动上报能力且断连检测更容易实现——每次轮询时都能重新测量“传感器还在吗”。相对地中断触发方式响应快但很难区分“断开”和“无事件”所以我不推荐只用中断。3. 传感器断开检测的两种原理与我的选择要检测“断开”本质上是检测电路回路是否完整性。这里有两个思路3.1 数字电平判断法简单但容易被坑大多数漏水模块的信号输出在干燥时是高电平有水时是低电平。如果直接把信号线接到GPIO配置为输入上拉或下拉读取高低电平来区分。但是当传感器线缆断开时GPIO引脚悬空。如果GPIO配置了弱上拉它会读到高电平和“干燥”一模一样如果配置弱下拉会读到低电平和“漏水”一模一样。无论怎么配断开状态都会和某个正常状态混淆。所以单纯数字量判断直接判断断开基本不可靠。有些模块会额外提供“传感器故障”输出但那是模块本身的支持独立探头改装场景下不一定有。3.2 模拟量测量法真正可靠的断线检测我的做法是把信号线接到ADC引脚然后通过外部电路构造一个分压偏置让“干燥”“漏水”“断开”三种状态对应明显不同的电压区间。具体电路如下在信号线和3.3V之间接一个10kΩ上拉电阻在信号线和GND之间接一个10kΩ下拉电阻。信号线同时连接到ADC引脚。此时信号线的电压由外部模块和偏置共同决定传感器模块正常工作时它的数字输出会驱动信号线到高电平或低电平ADC读到的就是约3.3V或0V因为模块输出的驱动能力强会盖过10k偏置。传感器断开时模块不再驱动信号线此时信号线只受分压电阻控制电压约等于1.65V10k分压的中点。如果传感器线缆短路到GNDADC读到0V如果短路到VCC读到3.3V这两种可以被视为异常但仍可区分。然后我们在软件里设定两个阈值区间状态ADC电压范围判定干燥 2.8V安全漏水 0.5V触发报警断开1.0V - 2.3V传感器离线未知异常其他区间提示故障这里注意阈值不能太死。因为传感器模块的电气特性不同、线缆长度不同ADC读数会有偏移。我实测过几种常见的模块干燥输出约3.2V漏水输出约0.1V断开时稳定在1.61V左右余量非常充足。3.3 为什么不用纯ADC读模块模拟输出有些漏水模块本身带模拟输出引脚该引脚的电压会随湿度变化。理论上可以通过读模拟值判断干燥/漏水/断开。但问题是这类模拟输出大多只是比较器输入分压后的值不同模块差异大且长时间通电后电极极化基准会漂移。更重要的是传感器断开时模拟输出往往悬空或保持高阻ADC读到的值不稳定反而更难判断。所以我的最终方案是放弃传感器的模拟输出端只用数字输出端 外部偏置电阻把“输出状态”硬编码为三个可辨识的电压区段。这个方法对任何带数字输出的开关型传感器都通用不局限于漏水传感器温度开关、门磁、人体红外都可以用同样的思路实现断线检测。4. ESP32端完整代码实现从初始化到状态机这个项目的代码我拆成几个模块引脚初始化、ADC读取、状态判定、去抖动、报警逻辑、Wi-Fi通知。下面给出完整可编译的示例。4.1 引脚定义与ADC初始化使用ESP32的ADC1通道注意ADC2和Wi-Fi会有冲突建议全部用ADC1。我把传感器接在GPIO34ADC1_CH6。#define LEAK_SENSOR_PIN 34 #define BUZZER_PIN 25 #define RELAY_PIN 26 #define LED_PIN 2 const float V_REF 3.3f; const int ADC_MAX 4095; // 阈值经过实测调整 const float DRY_MIN_V 2.8f; const float WET_MAX_V 0.5f; const float DISCONNECT_MIN_V 1.0f; const float DISCONNECT_MAX_V 2.3f; void setupSensor() { // 注意不需要pinMode为INPUT_PULLUP因为我们有外部电阻 pinMode(LEAK_SENSOR_PIN, INPUT); pinMode(BUZZER_PIN, OUTPUT); pinMode(RELAY_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT); digitalWrite(BUZZER_PIN, LOW); digitalWrite(RELAY_PIN, LOW); }为什么不用内部上拉因为内部上拉电阻约几十kΩ会破坏我外部10k分压的平衡。所以必须设置成纯输入模式让外部电阻网络决定偏置。4.2 读取模拟值并转换为电压ESP32的ADC精度在室温下还行但不够精细我做了一个简单的线性校正。没有读取efuse校准值直接按比例换算实测误差在0.05V以内足够用了。float readSensorVoltage(int pin) { int raw analogRead(pin); float voltage raw * V_REF / ADC_MAX; return voltage; }如果追求更精确可以用analogReadMilliVolts()这个API它内部会处理校准。不过在我的场景里没必要因为三个状态的电压区间隔得很开。4.3 状态判定与去抖动漏水检测最烦人的是抖动水滴落下瞬间电压会短暂跳变。如果立刻报警会频繁误报。我设计了一个简单的软件去抖机制要求同一个非正常状态持续超过3次采样每次采样间隔200ms才进入确认状态。enum SensorState { STATE_DRY, STATE_WET, STATE_DISCONNECTED, STATE_FAULT }; SensorState currentState STATE_DRY; SensorState lastStableState STATE_DRY; int stateCount 0; const int STATE_CONFIRM_COUNT 3; SensorState decideState(float voltage) { if (voltage DRY_MIN_V) return STATE_DRY; if (voltage WET_MAX_V) return STATE_WET; if (voltage DISCONNECT_MIN_V voltage DISCONNECT_MAX_V) return STATE_DISCONNECTED; return STATE_FAULT; } SensorState updateStateWithDebounce(SensorState newState) { if (newState lastStableState) { stateCount; if (stateCount STATE_CONFIRM_COUNT) { currentState newState; } } else { lastStableState newState; stateCount 1; } return currentState; }这里要特别注意初始化时lastStableState不能直接设为STATE_DRY否则刚上电时如果传感器还没稳定会误判。应该等第一次有效读数后再初始化状态。我在setup()里会先读取10次取中位数作为初始状态。4.4 报警逻辑区分“漏水”和“传感器断开”报警动作需要分级漏水最高优先级立刻打开蜂鸣器、LED常亮、继电器动作同时通过网络推送“漏水警报”。传感器断开次优先级蜂鸣器间歇响比如响0.2秒停1秒LED快闪推送“传感器离线”。故障电压落在未知区间仅通过LED慢闪提示不触发蜂鸣器避免半夜被莫名吵醒。我们还需要在状态恢复时做解除动作并推送“恢复正常”通知。void handleAlarm(SensorState newState) { if (newState STATE_WET) { digitalWrite(BUZZER_PIN, HIGH); digitalWrite(LED_PIN, HIGH); digitalWrite(RELAY_PIN, HIGH); // 可以接电磁阀 notifyRemote(LEAK_ALERT); } else if (newState STATE_DISCONNECTED) { // 间歇鸣响 tone(BUZZER_PIN, 1000, 200); // 用tone函数控制模式 digitalWrite(RELAY_PIN, LOW); blinkLED(2); notifyRemote(SENSOR_DISCONNECTED); } else if (newState STATE_DRY) { digitalWrite(BUZZER_PIN, LOW); digitalWrite(LED_PIN, LOW); digitalWrite(RELAY_PIN, LOW); notifyRemote(ALL_NORMAL); } }实际中我用一个定时器中断来驱动状态轮询而不是在loop()里死等。因为后续还要处理网络、界面刷新等任务。轮询周期设为200ms既能保证漏水响应速度在1秒以内又不会因为太频繁导致ADC读数不稳定。4.5 主循环与状态机下面是主循环的简化逻辑。我把状态机封装成一个类方便扩展多传感器。void loop() { // 周期轮询由millis()控制避免阻塞 checkSensorPeriodically(); // 其他任务比如Wi-Fi重连 handleNetwork(); // 看门狗喂狗 feedWatchdog(); } void checkSensorPeriodically() { static unsigned long lastCheck 0; if (millis() - lastCheck 200) return; lastCheck millis(); float v readSensorVoltage(LEAK_SENSOR_PIN); SensorState s decideState(v); SensorState stable updateStateWithDebounce(s); if (stable ! currentState) { currentState stable; handleAlarm(currentState); } // 记录电压到日志方便调试 serialPrintState(stable, v); }5. 硬件搭建与实测从接线到坑点复盘5.1 接线图与物料清单我用的物料如下物料型号/规格数量用途主控ESP32开发板1主控制器漏水传感器双探头漏水检测模块1检测漏水电阻10kΩ ±5%2分压偏置网络蜂鸣器有源蜂鸣器1本地告警继电器单路5V继电器模块1控制电磁阀LED红色/绿色各1状态指示线缆三芯线 1米1传感器延长线接线顺序传感器模块 VCC → ESP32 3.3V传感器模块 GND → ESP32 GND传感器模块 DO → ESP32 GPIO34ADC1_CH6GPIO34 通过 10kΩ 电阻接 3.3V同时通过 10kΩ 电阻接 GND蜂鸣器正极 → GPIO25负极 → GND注意有源蜂鸣器直接用GPIO驱动电流不够最好加三极管或使用模块继电器 IN → GPIO26VCC → 5VGND → GND这里有个容易翻车的点漏水传感器模块的DO输出逻辑电平是按模块供电电压来的。如果模块用3.3V供电DO高电平是3.3V没问题。如果为了驱动继电器模块用5V供电那么DO高电平是5V直接进ESP32的GPIO会超过耐压值需要分压。所以统一用3.3V供电继电器模块单独用5V驱动。5.2 首次上电状态稳定性的观察刚上电时我第一次读到的电压是1.63V判定为“断开”但实际上传感器是接好的。这是因为模块上电后DO要几十毫秒才稳定而我程序读得太快了。解决方法是在setup()里先等待500ms然后连续读20次去掉最大最小值后取平均作为初始状态。这可以避免一开机就乱报。稳定后干燥状态电压约3.19V用水淋探头后电压降到0.09V断开传感器接口后电压稳定在1.60~1.62V。三个状态区分非常明确我心里就有底了。5.3 一个被忽略的坑传感器线缆长度和电容效应当我把传感器延长线从1米加到5米时发现断开状态下的电压从1.61V漂移到了1.49V仍然在判定区间内但接近下限。原因是线缆的分布电容和电阻影响了偏置网络的响应速度。另外线缆之间如果靠得太近信号线上会耦合交流噪声导致ADC读数上下波动。解决办法有两层硬件层面改用屏蔽线屏蔽层单端接地接传感器GND端实测噪声从±0.05V降到了±0.01V。软件层面对连续5次读数取中间值中值滤波不取平均因为中值对尖峰脉冲的抗干扰能力更强。改进后的判定区间余量增大即使电压漂移也稳稳落在区间内。5.4 误报场景实测水溅和蒸汽我故意做了几个极端测试把探头放在热水杯上方让水蒸气凝结在电极上电压会缓慢下降从3.19V降到2.97V持续几十秒后又回到3.19V。因为干燥阈值是2.8V所以没有误报。如果是直接用数字量的开关模块内部比较器的灵敏度高蒸汽可能导致临界抖动反而会误报。用湿布快速擦过探头表面电压瞬间降到0.3V触发报警随后恢复到3.2V。这符合预期。人为拔掉传感器插头电压立刻变为1.6V程序在600ms后确认断开蜂鸣器间歇鸣响通知推送成功。整个实测下来我对这套方案的稳定性比较满意。特别是“断开检测”的引入让整个系统有了自我诊断能力不再是个“哑巴报警器”。6. 进一步提升低功耗、多传感器与远程联动6.1 多传感器轮询与每路独立检测如果你要监测厨房、卫生间、阳台多个点位只需要把多个传感器的DO分别接在不同的ADC1引脚上GPIO32~GPIO39等都是ADC1通道每路各接一组10kΩ分压电阻。代码层面可以用一个数组管理所有传感器const int sensorPins[] {34, 35, 36, 39}; const int numSensors sizeof(sensorPins) / sizeof(sensorPins[0]); SensorState states[numSensors]; void checkAllSensors() { for (int i 0; i numSensors; i) { float v readSensorVoltage(sensorPins[i]); SensorState s decideState(v); SensorState stable updateStateWithDebounce(i, s); // ... 处理状态变化 } }注意ADC1的所有通道都是独立的可以快速顺序采样。但是每个通道的内部采样电容是共享的切换通道后需要一个小小的延时约100微秒否则读出的值会带上一通道的残余。在analogRead()内部其实已经处理了但如果你用底层adc1_config_width()和adc1_get_raw()就要自己加延时。6.2 降低误报漏水检测的传感器安置技巧传感器本身的探头是裸露的铜电极。如果直接放在地面上洗衣机的泡沫、洒落的洗涤剂都可能引发误报。我的建议是不要直接平贴地面用扎带固定在一小块塑料垫片上垫片开孔让水能接触到但避免大面积接触湿气。探头的金属电极尽量朝下水珠不会残留在金属之间形成“假触点”。定期用干布擦拭电极因为长期通电后电极表面会电解氧化形成一层绝缘膜导致真实漏水时检测不到。这个属于维护问题但一定要在说明里写出来。6.3 低功耗设计电池供电时可用的休眠-唤醒策略移动场景比如野营帐篷漏水监测没法一直插电。我们可以利用ESP32的深度睡眠模式正常情况每10秒醒来一次轮流测量传感器电压如果状态没有变化继续睡。发生漏水或断开立即唤醒并进入报警流程持续保持工作状态。实现上非常简单用esp_sleep_enable_timer_wakeup()设置定时唤醒。传感器和偏置电阻的静态功耗在毫安级整个系统睡眠时电流可以低到几十微安取决于板载LDO建议选择超低功耗的ESP32模组。电池续航可以做到几个月。esp_sleep_enable_timer_wakeup(10 * 1000000ULL); // 10秒 esp_deep_sleep_start(); // 唤醒后从头执行setup() // 在setup里做快速检测然后再次决定是否继续睡不过要注意深度睡眠状态下引脚电平会保持但有源蜂鸣器如果接在GPIO上睡眠时如果GPIO被拉高会一直响直至电池耗尽。所以报警输出引脚在睡眠前一定要置低或直接通过继电器供电控制。6.4 远程联动在漏水报警器上加一个大喇叭默认的蜂鸣器声音太小放在卫生间里关上门基本听不到。我后来接了一个有源音箱通过继电器模块控制音箱的电源。漏水时继电器吸合音箱被供电同时ESP32播放一段内置的警告语音通过I2S DAC输出到音箱。效果比蜂鸣器好太多连邻居都能听到。远程推送方面我用的是MQTT协议把状态上报到局域网里的某台MQTT服务器手机App订阅对应主题就能收到通知。代码上只需要在状态变化时调用mqtt.publish(home/leak/status, stateString)。如果不想搭服务器也可以用HTTP请求的方式直接调用第三方推送接口原理一样。6.5 产品化思路状态指示灯的意义很多DIY结束后就扔在一边过段时间自己都忘了设备在哪儿更记不清当前状态。所以状态指示灯不能省。我的设计是绿光亮系统正常传感器在线红光常亮漏水发生红光快闪传感器断开橙光慢闪未知故障这样任意时刻走过去瞟一眼就能知道设备是否健康完全不需要打开手机查看。指示灯直接由GPIO控制逻辑和状态机绑定。7. 结束语从一次断线误判到整套诊断机制做这个项目的起因是某天我看到家里阳台放的水浸传感器因为插头松了而App上还显示“正常”。那次之后我就意识到漏水检测的可靠性不在于算法多复杂而在于设备是否能感知“自身的状态变化”。所以我把断线检测作为最高优先级之一写进了代码。现在这套系统已经在我这边跑了几个月期间出现过两次传感器线被宠物碰掉的场景都在几十秒内发出了通知而不是无声无息地失效。我的体会是任何传感器项目如果只读“信号”而不读“线缆是否还在”都是不安全的设计。尤其对于安防、漏水、火灾这类场景断电、断线本身就是事故的前兆必须被检测到。如果你想复刻这个项目我建议从最简单的单传感器版本开始把分压电阻、阈值设定、去抖逻辑跑通再扩展多路和网络通知。改代码的时候一定注意ADC引脚的内部寄存器和Wi-Fi的冲突问题尽量固定用ADC1通道。测试时先接线再下载程序避免接反损坏IO口。这个项目后续还可以加多个传感器轮询时的优先级仲裁、漏水点定位功能多个传感器同时触发通过时间差判断水源位置、以及和摄像头联动拍照留证。这些都是基于现有框架的合理扩展看到这里你完全可以直接动手了。