1. 项目缘起:一个被忽视的公共卫生细节
去年夏天,我在一个社区服务中心做志愿者,负责引导居民进行手部消毒。那是一个老式的按压式酒精凝胶瓶,每个人使用后,瓶身都变得湿漉漉、黏糊糊的。更让我心里一紧的是,我亲眼看到好几个人,手都没完全搓干,就急匆匆地按下了泵头,准备离开。那一刻我意识到,我们反复强调的“勤洗手”,在执行层面有一个巨大的漏洞:洗手或消毒的时长与质量,完全依赖个人的自觉性。世界卫生组织推荐的“七步洗手法”要求揉搓时间不少于20秒,但有多少人能真正坚持?尤其是在公共场合,人多、心急,往往草草了事。
这个“COVID - 19 Touchless Hand Wash Timer”项目的想法,就源于这个最朴素的观察。它的核心目标不是发明什么高深的技术,而是用最简单、最可靠的方式,解决“洗手时长不足”这个行为学上的痛点。它应该是一个非接触式的计时器,当用户伸手准备使用洗手液或感应水龙头时,自动开始一个20秒(或可自定义)的倒计时,并通过视觉或听觉反馈,引导用户完成足够时长的清洁过程。整个过程无需触碰任何实体,避免了交叉污染的风险。这个项目适合所有关心公共卫生、喜欢动手创造的创客、电子爱好者,甚至是社区、小型商户的维护人员,用很低的成本,就能为公共空间增添一份实实在在的安全保障。
2. 核心设计思路:为什么选择“非接触”与“纯本地”
在构思方案时,我首先排除了连接手机APP、依赖网络或复杂交互的想法。公共设备的首要原则是可靠、即时、零学习成本。用户走过去,伸手,设备开始工作并给出明确指引,就这么简单。因此,我确定了几个核心设计原则:
2.1 传感方案选型:超声波 vs 红外 vs 微波雷达
非接触检测是项目的基石。常见方案有三种:
- 超声波传感器(如HC-SR04):成本低,原理简单(发射声波,接收回波,计算距离)。但在潮湿、多反射的洗手台环境,水声、环境噪音可能造成干扰,且检测范围呈锥形,区域控制不够精确。
- 红外对管/光电传感器:分为对射式和反射式。对射式(一个发射,一个接收)需要在洗手台两侧安装,不美观且安装复杂。反射式(如TCRT5000)容易受环境光干扰,特别是洗手台附近光线变化可能较大。
- 微波雷达传感器(如RCWL-0516):这是我最终选择的方案。它通过检测微波多普勒频移来感知微动,对静止物体不触发,非常适合检测“伸手”这个动作。它的优点很明显:不受环境光、温度、湿度影响;探测区域是一个扇形面,可以更好地覆盖洗手池前方区域;模块本身带有调节旋钮,可以方便地调整探测距离(通常2-7米可调)。虽然成本比超声波稍高,但考虑到环境适应性和可靠性,这笔投资非常值得。
2.2 计时与反馈逻辑设计
逻辑很简单:雷达检测到有人伸手 → 启动计时器 → 同时提供反馈 → 计时结束 → 反馈停止。这里的关键在于反馈形式。我采用了声光结合的方式:
- 视觉反馈:使用一个WS2812B RGB LED灯环。倒计时开始时,灯环亮起绿色(或蓝色),随着时间流逝,LED灯逐个熄灭或变色(如从绿变红),形成一个直观的进度条。计时结束,所有灯熄灭或闪烁两下提示。这种反馈安静、直观,且科技感十足。
- 听觉反馈:搭配一个无源蜂鸣器。我设计了两段提示音:一段是计时开始的“提示音”(短促的“嘀”声),另一段是计时结束的“完成音”(可以是两声“嘀嘀”或一小段旋律)。声音不宜过长过吵,避免在安静环境(如医院夜间)造成干扰。
2.3 主控与“纯本地”考量
项目不需要联网、不需要存储数据、不需要复杂计算,因此一个最基础的微控制器足矣。Arduino Nano或ESP8266(仅使用其MCU功能,不连接Wi-Fi)都是绝佳选择。它们体积小巧,有足够的数字IO口驱动LED灯环和蜂鸣器,且开发环境成熟,代码编写和上传非常方便。坚持“纯本地”运行,意味着设备部署后无需任何配置,插电即用,最大限度地保证了系统的稳定性和免维护性。
注意:选择RCWL-0516雷达模块时,注意其工作电压通常是4-28V DC,而Arduino Nano是5V。你需要一个合适的降压模块(如LM2596)将电源(如12V适配器)降压至5V,同时给雷达模块和Arduino供电。切勿直接将12V接入Arduino,会烧毁芯片。
3. 硬件搭建与电路连接详解
有了设计思路,接下来就是把想法变成实物。这是动手环节,我会列出详细的物料清单和接线方法。
3.1 物料清单(BOM)
- 主控模块:Arduino Nano x1 (或 ESP8266 NodeMCU开发板 x1)
- 传感器:RCWL-0516 微波雷达模块 x1
- 视觉反馈:WS2812B RGB LED灯环(8位或12位) x1
- 听觉反馈:无源蜂鸣器 x1
- 电源:12V DC 1A 电源适配器 x1
- 电压转换:LM2596 DC-DC降压模块 x1 (将12V降为5V)
- 连接件:杜邦线(公对公、母对母)若干,面包板(用于测试)或PCB(用于最终成品)
- 外壳:3D打印外壳或防水塑料盒(确保雷达传感器部分有开口)
3.2 电路连接图与原理
整个系统的供电与信号流如下图所示(文字描述):
- 电源路径:12V适配器正极接LM2596输入正极(IN+),负极接输入负极(IN-)。LM2596输出调整为5V,其输出正极(OUT+)同时接Arduino Nano的
VIN引脚(注意不是5V引脚)和雷达模块的VIN引脚。所有GND(LM2596 OUT-, Arduino GND, 雷达模块 GND, LED灯环 GND, 蜂鸣器负极)连接在一起。 - 信号连接:
- 雷达模块:雷达的
OUT引脚接Arduino的数字引脚D2(或其他任意中断引脚,如D3)。 - LED灯环:灯环的
DI(数据输入)引脚接Arduino的D6(需支持PWM,实际用于数据通信)。 - 蜂鸣器:蜂鸣器正极接Arduino的
D9(支持PWM,用于播放不同频率声音),负极接GND。
- 雷达模块:雷达的
提示:使用
VIN引脚为Arduino供电时,输入电压需在7-12V之间,板载稳压器会将其降至5V。直接给5V引脚供电则必须严格是5V。这里采用VIN方案更稳妥。
3.3 安装与布局注意事项
硬件安装不是简单的堆叠,布局影响使用体验:
- 雷达模块朝向:将雷达模块平放或略微向上倾斜,固定在外壳内侧,使其探测扇形面覆盖洗手池前方约30-50厘米的区域,高度与成人手腕高度相当(约离地80-100cm)。避免正对着金属水管或大面积金属板,可能产生干扰反射。
- LED灯环可视性:灯环应安装在醒目但不刺眼的位置,例如洗手液瓶上方或镜框边缘。可以考虑用磨砂亚克力板做一层柔光处理,让光线更均匀。
- 防水考虑:洗手台环境潮湿。虽然电路部分应密封在盒内,但雷达感应区域和LED透光孔仍需做好防水。可以使用防水胶(如705硅橡胶)密封缝隙,或选择IP65及以上等级的外壳。
- 电源安全:确保所有220V交流部分(电源适配器)已妥善绝缘,远离水源。DC降压模块的裸露焊点最好用热缩管或绝缘胶带包裹。
4. 软件实现:代码逻辑与核心算法剖析
硬件是躯体,软件是灵魂。下面我将分模块解析Arduino代码的核心逻辑。完整代码会在最后提供。
4.1 库文件引入与引脚定义
首先,我们需要引入驱动WS2812B灯环的库Adafruit_NeoPixel。这个库封装了复杂的时序通信,让我们可以像控制数组一样控制每一个LED的颜色。
#include <Adafruit_NeoPixel.h> // 引脚定义 #define RADAR_PIN 2 // 雷达模块输出接D2 #define LED_PIN 6 // LED灯环数据线接D6 #define BUZZER_PIN 9 // 蜂鸣器接D9 #define LED_COUNT 12 // LED灯珠数量,根据你的灯环修改 // 创建灯环对象 Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); // 全局变量 unsigned long washDuration = 20000; // 洗手时长,单位毫秒,这里是20秒 unsigned long startTime = 0; // 计时开始的时间点 bool isWashing = false; // 是否正在洗手状态标志4.2 雷达中断服务与状态机
雷达模块的输出是数字信号:无人时低电平(0),检测到移动时高电平(1)。我们使用中断来捕获这个“伸手”动作,确保响应零延迟。
void setup() { Serial.begin(9600); pinMode(RADAR_PIN, INPUT); pinMode(BUZZER_PIN, OUTPUT); // 初始化灯环 strip.begin(); strip.show(); // 初始化为全灭 strip.setBrightness(100); // 设置亮度(0-255),避免太刺眼 // 为雷达引脚设置中断,监测上升沿(从0变1) attachInterrupt(digitalPinToInterrupt(RADAR_PIN), startWashTimer, RISING); playStartTone(); // 开机提示音 } // 中断服务函数:当雷达检测到人,此函数被立即调用 void startWashTimer() { if (!isWashing) { // 防止在洗手过程中重复触发 isWashing = true; startTime = millis(); // 记录当前时间作为开始时刻 Serial.println("Wash Timer Started!"); playStartTone(); // 播放开始提示音 } }这里用到了一个简单的状态机:isWashing变量。它只有两种状态:false(空闲)和true(计时中)。中断函数里先判断状态,避免了在20秒倒计时内,因为人的微小晃动而反复重置计时器,保证了计时流程的完整性。
4.3 主循环与动态视觉反馈
loop()函数的核心是,如果处于计时状态,就计算已过去的时间,并更新LED灯环的显示。
void loop() { if (isWashing) { unsigned long elapsedTime = millis() - startTime; // 计算已过去的时间 unsigned long remainingTime = washDuration - elapsedTime; // 计算剩余时间 // 更新LED进度显示 updateLEDProgress(remainingTime); // 检查是否超时 if (elapsedTime >= washDuration) { finishWashing(); } } // 非计时状态,可以添加其他低功耗任务,本项目为空循环即可 } void updateLEDProgress(unsigned long remainingTime) { // 计算亮起的LED数量。例如,总时间20秒,12个灯,每个灯代表约1.67秒。 int lightsOn = map(remainingTime, washDuration, 0, LED_COUNT, 0); lightsOn = constrain(lightsOn, 0, LED_COUNT); // 限制在0-LED_COUNT之间 // 先熄灭所有灯 strip.clear(); // 根据剩余时间比例,设置颜色(例如:时间多时为绿色,快结束时变为红色) for (int i = 0; i < lightsOn; i++) { // 简单的颜色渐变:从绿(0,255,0)到红(255,0,0) int r = map(i, 0, LED_COUNT-1, 0, 255); int g = map(i, 0, LED_COUNT-1, 255, 0); strip.setPixelColor(i, strip.Color(r, g, 0)); } strip.show(); // 更新显示 }updateLEDProgress函数是视觉反馈的核心。它利用map()函数将剩余时间映射到亮灯数量上,实现了进度条效果。同时,通过计算每个灯珠的RGB值,实现了颜色从绿到红的渐变,让时间流逝感更加直观。
4.4 听觉反馈与结束处理
听觉反馈通过tone()函数产生特定频率的方波来实现。无源蜂鸣器需要不同频率才能发出不同音调。
void playStartTone() { tone(BUZZER_PIN, 1000, 200); // 频率1000Hz,持续200ms delay(250); // 稍作停顿 } void playFinishTone() { tone(BUZZER_PIN, 800, 150); delay(180); tone(BUZZER_PIN, 1200, 300); } void finishWashing() { Serial.println("Wash Timer Finished! Good Job!"); playFinishTone(); // 播放完成提示音 // LED完成动画:例如快速闪烁绿色三次 for (int i = 0; i < 3; i++) { strip.fill(strip.Color(0, 255, 0), 0, LED_COUNT); // 全部亮绿色 strip.show(); delay(200); strip.clear(); strip.show(); delay(200); } isWashing = false; // 重置状态,等待下一次触发 }5. 调试、优化与实地部署的坑
代码烧录进去,硬件接好,通电测试,这才是项目真正开始的地方。我遇到了几个典型问题,它们的解决方案或许对你有帮助。
5.1 雷达模块的“幽灵触发”与灵敏度调节
最初测试时,设备偶尔会自己启动,周围明明没人。这就是所谓的“幽灵触发”。原因可能有两个:一是电源噪声,二是雷达灵敏度太高,检测到了远距离的微小移动(如窗帘晃动)。
- 排查与解决:
- 电源滤波:在LM2596的输入和输出端,并联一个100μF的电解电容和一个0.1μF的陶瓷电容,可以很好地平滑电源纹波。
- 调整探测距离:RCWL-0516模块上有一个可调电阻(电位器)。逆时针旋转(通常)是减小探测距离和灵敏度。我将其调整到大约0.5-1米的探测距离,刚好覆盖洗手台前方区域,有效避免了误报。
- 软件防抖:在中断服务函数
startWashTimer()中,可以增加一个简单的延时判断。例如,检测到信号后,延迟10毫秒再次读取引脚状态,如果仍然是高电平,才确认为有效触发。这能过滤掉一些尖峰脉冲干扰。
5.2 LED灯环颜色异常或通信失败
WS2812B灯环对时序要求极其严格,如果颜色显示错乱(比如该亮红色却亮了绿色),或者完全不亮,问题通常出在通信上。
- 排查与解决:
- 检查数据线:确保连接LED
DI引脚的数据线接触良好,并且接到了Arduino指定的引脚(本例是D6)。 - 供电不足:LED全亮白色时功耗最大。如果电源功率不足(特别是使用USB供电时),会导致LED闪烁或颜色异常。务必使用外部5V/2A以上的电源单独为灯环供电,并将灯环的电源地(GND)与Arduino的GND相连,共地是关键。
- 干扰问题:数据线过长可能引入干扰。如果灯环离控制器较远(超过50厘米),建议在Arduino数据输出引脚和灯环数据输入引脚之间,串联一个100-500欧姆的电阻,并在灯环的数据输入引脚对地接一个100pF的电容,可以改善信号质量。
- 检查数据线:确保连接LED
5.3 计时精度与millis()溢出问题
我们使用millis()函数获取时间,它返回Arduino启动后的毫秒数。这个数大约50天后会溢出归零。对于我们这个每次最多运行20秒的设备,溢出不会造成问题。但如果你要延长计时时间或做其他长期运行项目,需要知道这个特性。
- 时间计算技巧:在
loop()中计算elapsedTime时,使用millis() - startTime的方式,即使millis()溢出,只要时间间隔不超过溢出周期(约50天),减法运算在无符号长整型(unsigned long)下仍然是正确的。这是处理Arduino计时的一个经典且安全的方法。
5.4 部署后的环境适应性问题
设备安装到公共洗手间后,出现了新问题:水龙头冲水的声音和强烈的水流反射,有时会干扰雷达。
- 解决方案:调整雷达模块的安装角度,使其探测面略高于水槽水面,并稍微朝向用户站立的方向,而不是正对着水龙头出水口。此外,可以考虑为雷达模块制作一个小的、开口向外的波导罩(用塑料或纸板),略微收束探测波束,使其方向性更强。
6. 功能扩展与变体思路
基础版本完成后,你可以根据具体场景进行升级,让这个小设备变得更智能、更贴心。
6.1 增加感应洗手液泵或水龙头控制
这才是真正的“非接触”闭环!你可以通过继电器模块,用Arduino控制一个低压直流电磁阀或一个改装后的自动洗手液机。
- 实现方式:当雷达触发开始计时时,同时让Arduino的一个引脚输出高电平,驱动继电器吸合1-2秒,从而打开电磁阀(出水)或启动洗手液泵电机(出液)。计时结束后自动关闭。这样,用户从伸手到洗毕,全程无需触碰任何实物。
- 安全警告:控制水龙头涉及水路改造,务必确保电磁阀质量可靠,安装无漏水,且电路部分做好严格防水绝缘,最好由专业人员进行。控制12V/24V的洗手液泵则相对安全。
6.2 加入环境光传感器,实现自动亮度调节
公共场所有时灯光很暗,LED太亮会刺眼;有时灯光很亮,LED又看不清。可以加入一个BH1750等环境光传感器。
- 实现方式:在
setup()和loop()中读取环境光照强度(lux值),然后动态调整strip.setBrightness()的值。例如,光照低于50 lux时,亮度设为30;光照高于300 lux时,亮度设为150。这样显示效果始终舒适。
6.3 使用ESP8266/ESP32实现数据统计与远程管理
如果你想知道设备的使用频率,或者想远程更改洗手时长,可以换用ESP8266。
- 实现方式:ESP8266连接Wi-Fi后,可以在每次计时完成后,通过MQTT协议或HTTP请求,向一个服务器(如本地NAS或云平台)发送一条记录。你就能在仪表板上看到每天的洗手次数、平均时长等。你还可以做一个简单的Web配置页面,让管理员通过浏览器输入IP地址,就能修改
washDuration变量,而无需重新烧录程序。
6.4 设计更友好的提示语音
蜂鸣器的“嘀嘀”声毕竟有些单调。你可以使用DFPlayer Mini模块配合一个小喇叭,播放录制好的真人语音提示,如“请开始洗手,计时20秒”、“时间到,洗手完成”,体验会好很多。
从观察到问题,到构思方案,再到动手实现和解决一个个具体的bug,这个过程本身带来的满足感,远超过项目最终呈现的那个小盒子。它让我深刻体会到,技术真正的价值,不在于多么复杂高深,而在于它是否精准地解决了一个真实世界的小问题。这个非接触洗手计时器,成本不过百元,但它所倡导的“认真洗手”的行为,在公共卫生层面却有着不可小觑的意义。如果你也在公共空间管理或维护中遇到类似的痛点,不妨花一个周末的时间动手做一个,那份亲手创造并改善环境的感觉,非常棒。