尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于光线传感器的无接触洗手计时器设计与实现

基于光线传感器的无接触洗手计时器设计与实现 1. 项目缘起从“接触”到“无接触”的洗手习惯养成不知道你有没有这样的经历在公共卫生间或者厨房墙上贴着“请洗手20秒”的提示但水龙头一开心里默数几下就草草了事。尤其是在光线昏暗的夜晚为了不打扰家人摸黑洗手更是敷衍。这个看似不起眼的小习惯在特殊时期却显得尤为重要。我一直在想有没有一种方式能既有趣又有效地提醒自己和家人完成一次“达标”的洗手过程同时还能兼顾一些实用的小功能于是这个“无接触洗手计时器/夜灯”的想法就诞生了。它的核心目标很简单创造一个无需触碰任何物理按钮通过手势就能启动的、带有视觉反馈的洗手计时器并在闲置时自动切换成一个柔和的氛围夜灯。这不仅仅是一个简单的计时器更是一个融合了交互设计、嵌入式编程和实用主义的小项目。我选择了Adafruit的Circuit Playground ExpressCPX作为开发板因为它集成了我们所需的一切多个可编程的RGB LED、光线传感器、声音传感器甚至还有运动传感器非常适合快速原型开发。这个项目非常适合对硬件编程感兴趣的初学者或者想为家里增添一点智能小装置的DIY爱好者。你不需要复杂的焊接只需要一块CPX板、一块锂电池或者USB供电、一个合适的防水外壳比如一个透明的塑料盒再加上一点MakeCode图形化编程或JavaScript代码就能亲手打造一个既实用又有科技感的桌面小工具。接下来我会详细拆解从构思到实现的每一个环节包括为什么选择这些组件、如何设计交互逻辑、编程中的关键细节以及我实际制作过程中踩过的那些坑。2. 核心硬件选型与交互逻辑设计2.1 为什么是Circuit Playground Express在开始动手之前硬件的选择决定了项目的上限和实现的难易度。市面上常见的开发板如Arduino Uno、ESP32等都能实现类似功能但我最终锁定CPX是基于以下几个核心考量1. 高度集成开箱即用CPX板载了10个可独立寻址的NeoPixel RGB LED呈环形排列这为我们实现进度条、颜色渐变提供了完美的硬件基础。此外它自带光线传感器用于检测环境光实现夜灯、声音传感器可用于声音触发但本项目未采用、温度传感器、运动传感器加速度计甚至还有一个红外接收发射器。这意味着我们几乎不需要外接任何传感器或LED模块极大简化了电路连接和物理结构设计。2. 双重编程环境友好灵活CPX完美支持Microsoft MakeCode Arcade图形化积木编程和CircuitPython/Arduino IDE代码编程。对于初学者或快速验证想法MakeCode的拖拽式编程直观易懂当需要实现更复杂的逻辑如状态机、平滑的光效过渡时切换到JavaScriptMakeCode的文本模式或CircuitPython则游刃有余。这种灵活性让项目可以适应不同技能水平的制作者。3. 强大的社区与生态Adafruit提供了极其丰富的教程、库文件和项目案例。遇到任何关于传感器读取、LED控制的问题几乎都能在社区找到答案或现成的代码片段这能节省大量调试时间。对比其他方案使用Arduino Uno外接LED环和红外/超声波传感器也能实现但需要焊接、连接多条线增加了复杂度和故障点。而ESP32虽然功能更强、有Wi-Fi/蓝牙但对于这个专注本地交互、无需联网的小项目来说显得有些“杀鸡用牛刀”且其开发环境对纯新手稍显复杂。因此CPX在集成度、易用性和功能匹配度上取得了最佳平衡。2.2 “无接触”交互的传感器选择实现“无接触”触发我们有几种传感器备选红外接近传感器、超声波测距传感器、电容式触摸传感器以及CPX自带的声音传感器和光线传感器。红外/超声波传感器精度高探测距离可调但需要额外购买和接线且可能受环境光线红外或复杂表面超声波干扰。电容触摸需要金属触点虽未直接接触皮肤但仍有物理接触感不符合“完全无接触”的初衷。声音传感器通过拍手或特定声音触发。缺点是在嘈杂环境如厨房容易误触发且夜间使用可能产生噪音。光线传感器这是本项目最终采用的方案。其原理是检测环境光强度的变化。当手靠近CPX上方时会遮挡大部分环境光源导致光线传感器读取到的数值骤降。提示选择光线传感器作为触发媒介是一个巧妙且低成本的方案。它无需任何外接部件完全利用板载资源。关键在于设定一个合理的“触发阈值”。这个阈值不能太敏感否则有人走过影子晃动就会触发也不能太迟钝需要手非常靠近。我们需要通过实验来找到一个稳定的值。交互逻辑状态机设计整个设备的行为可以抽象为一个状态机通常包含以下几个状态空闲/夜灯模式设备启动后默认状态。CPX读取环境光亮度如果环境较暗则以低亮度显示一种温和的静态色彩如淡蓝色作为夜灯。触发检测模式持续监测光线传感器数值。当数值低于设定的“触发阈值”并持续一小段时间如0.5秒用于防抖时判定为手已靠近进入“洗手准备”状态。洗手计时模式触发后LED灯环开始以视觉进度条的形式进行20秒倒计时。例如可以用绿色光点顺时针填充圆环代表时间流逝。同时可以伴随轻微的亮度脉冲作为提示。完成提示模式20秒计时结束所有LED快速闪烁绿色数次或播放一段内置音效如果启用扬声器提示洗手完成。返回空闲提示结束后自动返回状态1空闲/夜灯模式。这个状态机逻辑清晰易于用编程实现且用户体验直观。3. 基于MakeCode/JavaScript的详细实现步骤我们将使用MakeCode的JavaScript模式进行编程因为它比积木块更灵活便于实现复杂的灯光效果和状态控制。如果你完全新手可以先在MakeCode的图形化界面中熟悉基本块然后切换到“JavaScript”视图查看对应的代码。3.1 开发环境搭建与项目初始化首先访问MakeCode for Adafruit网站选择Circuit Playground Express作为硬件目标。新建一个项目后点击界面右上角的“{} JavaScript”按钮切换到代码编辑器。在开始编写主逻辑前我们需要定义一些在整个项目中都会用到的常量和全局变量。这就像施工前的图纸和材料准备。// --- 常量定义 --- // 洗手计时总时长毫秒 const WASH_DURATION 20000; // 20秒 // 光线传感器触发阈值 (0-255值越小越暗需要根据实测调整) const LIGHT_TRIGGER_THRESHOLD 50; // 触发防抖时间毫秒手需要持续遮挡这么久才被确认 const DEBOUNCE_TIME 500; // 夜灯模式的环境光阈值低于此值则开启夜灯 const NIGHT_LIGHT_THRESHOLD 80; // 夜灯亮度 (0-255) const NIGHT_LIGHT_BRIGHTNESS 20; // 夜灯颜色RGB const NIGHT_LIGHT_COLOR [0, 100, 255]; // 淡蓝色 // --- 全局变量 --- // 设备状态 idle空闲, triggered已触发计时, washing洗手进行中, done完成 let deviceState idle; // 用于防抖计时 let debounceStartTime 0; // 洗手计时开始的时间点 let washStartTime 0; // 记录前一次光线传感器读数用于判断变化趋势 let lastLightLevel 0;定义好这些“规则”后代码的逻辑就会清晰很多后续修改参数也只需调整这些常量即可。3.2 核心状态机与主循环实现MakeCode的程序通常从on start初始化和forever主循环两个基本块开始。在JavaScript中对应的就是loops.forever函数。// 初始化函数 let nightLightOn false; basic.forever(function () { // 1. 读取当前光线传感器值0-255 let currentLight input.lightLevel(); // 2. 状态机分发处理 switch (deviceState) { case idle: handleIdleState(currentLight); break; case triggered: // 从触发到开始计时的短暂过渡状态可以加入呼吸灯提示 handleTriggeredState(); break; case washing: handleWashingState(); break; case done: handleDoneState(); break; } // 更新上一次的光线记录 lastLightLevel currentLight; // 主循环延迟避免CPU占用过高50ms是一个合理的间隔 basic.pause(50); })basic.forever是一个无限循环每隔50毫秒执行一次。在每次循环中我们首先获取最新的环境光亮度然后根据当前的deviceState设备状态执行对应的处理函数。这种“状态模式”的编程思想使得程序结构非常清晰每个状态的行为独立易于调试和维护。3.3 各状态处理函数详解3.3.1 空闲/夜灯模式 (handleIdleState)这个状态有两个任务一是管理夜灯的开关二是检测是否有手靠近触发。function handleIdleState(currentLight: number) { // 任务一夜灯逻辑 if (currentLight NIGHT_LIGHT_THRESHOLD) { // 环境暗需要开启夜灯 if (!nightLightOn) { // 设置所有LED为夜灯颜色和亮度 light.setAll(light.rgb(NIGHT_LIGHT_COLOR[0], NIGHT_LIGHT_COLOR[1], NIGHT_LIGHT_COLOR[2])); light.setBrightness(NIGHT_LIGHT_BRIGHTNESS); nightLightOn true; } } else { // 环境亮关闭夜灯 if (nightLightOn) { light.clear(); nightLightOn false; } } // 任务二触发检测逻辑 // 当光线突然变暗手遮挡且变暗幅度超过阈值 if ((lastLightLevel - currentLight) 20 currentLight LIGHT_TRIGGER_THRESHOLD) { // 首次检测到可能触发记录时间开始防抖 if (debounceStartTime 0) { debounceStartTime input.runningTime(); } else { // 已经处在防抖期内检查是否持续遮挡了足够时间 if (input.runningTime() - debounceStartTime DEBOUNCE_TIME) { // 防抖通过确认触发 deviceState triggered; debounceStartTime 0; // 重置防抖计时器 // 可以在这里加一个提示音或灯光反馈表示已检测到手 music.playTone(Note.C, 100); } } } else { // 光线条件不满足重置防抖计时器 debounceStartTime 0; } }这里有一个关键技巧触发检测不是只看当前光强是否低于阈值而是看光强变化量lastLightLevel - currentLight。这能有效区分“手快速掠过”和“手稳定靠近”。只有光强骤降并维持在低水平一段时间防抖才被认为是有效的触发意图这大大降低了误触发率。3.3.2 洗手计时模式 (handleWashingState)这是项目的视觉核心。我们要用10个LED来显示20秒的进度。最简单的办法是每个LED代表2秒。function handleWashingState() { // 计算已经过去的时间 let elapsed input.runningTime() - washStartTime; // 计算进度比例 (0.0 到 1.0) let progress Math.min(elapsed / WASH_DURATION, 1.0); // 根据进度点亮对应数量的LED let ledsToLight Math.floor(progress * 10); // 10个LED // 先关闭所有LED light.clear(); // 点亮进度LED for (let i 0; i ledsToLight; i) { // 使用HSV颜色模型进度从绿色(120°)渐变到红色(0°) let hue 120 - (progress * 120); // 将HSV转换为RGBMakeCode的light.hsv函数需要hue(0-360), saturation(0-100), value(0-100) // 这里简化直接用绿色到红色的RGB渐变 let r Math.floor(255 * (1 - progress)); let g Math.floor(255 * progress); let b 0; light.setPixelColor(i, light.rgb(r, g, b)); } // 检查是否计时结束 if (elapsed WASH_DURATION) { deviceState done; // 计时结束先清除当前显示 light.clear(); } }但是上面的简单线性渐变看起来可能有些生硬。一个更高级、更平滑的效果是让当前正在点亮的那个LED呈现呼吸灯效果而已经点亮的LED保持常亮。这需要更精细的状态控制例如记录每个LED的独立状态亮度值并在每个循环中更新。为了简化我们可以采用一个折中方案让整个灯环有一个统一的、缓慢的亮度脉动以增加动感。// 改进版增加整体亮度脉动 function handleWashingState() { let elapsed input.runningTime() - washStartTime; let progress Math.min(elapsed / WASH_DURATION, 1.0); let ledsToLight Math.floor(progress * 10); // 计算脉动亮度因子一个基于时间的正弦波范围在0.7到1.0之间 let pulseFactor 0.7 0.3 * Math.sin(elapsed / 200); // 每约1.26秒一个周期 light.clear(); for (let i 0; i ledsToLight; i) { let r Math.floor(255 * (1 - progress) * pulseFactor); let g Math.floor(255 * progress * pulseFactor); let b 0; light.setPixelColor(i, light.rgb(r, g, b)); } if (elapsed WASH_DURATION) { deviceState done; light.clear(); } }3.3.3 触发与完成状态处理triggered状态是一个短暂的过渡可以给用户一个明确的“设备已唤醒”反馈。done状态则要庆祝任务完成。function handleTriggeredState() { // 用快速闪烁两下白光作为反馈 light.setAll(light.rgb(255, 255, 255)); basic.pause(150); light.clear(); basic.pause(150); light.setAll(light.rgb(255, 255, 255)); basic.pause(150); light.clear(); // 过渡完成进入洗手计时状态 deviceState washing; washStartTime input.runningTime(); // 记录计时开始时刻 } function handleDoneState() { // 完成提示绿色LED快速闪烁3次 for (let i 0; i 3; i) { light.setAll(light.rgb(0, 255, 0)); basic.pause(200); light.clear(); basic.pause(200); } // 提示结束回归空闲状态并重置夜灯标志 deviceState idle; nightLightOn false; // 让idle状态重新判断是否需要夜灯 }3.4 参数调试与校准代码写完后直接烧录到CPX上可能效果不理想因为每个传感器的特性、每个使用环境的光照都不同。调试是制作过程中最关键的一步。光线阈值调试在on start初始化部分临时添加一段代码将光线传感器的读数实时显示出来。// 临时调试代码用于查看当前环境光值 basic.forever(function () { serial.writeLine(Light: input.lightLevel()); basic.pause(200); })将CPX通过USB连接到电脑打开MakeCode的控制台在模拟器下方。观察在正常环境、手靠近完全遮挡时数值的变化范围。例如正常室内光可能是150手遮挡后降到20。那么LIGHT_TRIGGER_THRESHOLD可以设为(15020)/2 ≈ 85而NIGHT_LIGHT_THRESHOLD可以设为100这样天黑后自动开启夜灯。防抖时间调试DEBOUNCE_TIME如果太短如100ms手快速挥过可能误触发如果太长如1000ms用户体验会感觉反应迟钝。500ms是一个比较折中的起点可以根据手感调整。LED颜色与亮度NIGHT_LIGHT_BRIGHTNESS在黑暗中不宜过亮20-30足够作为指引。洗手进度的颜色可以根据喜好调整比如从蓝色到绿色代表从“未开始”到“干净”。调试完成后记得移除或注释掉调试用的串口输出代码以减少不必要的功耗和操作。4. 电源、外壳与安装实战经验让项目从开发板变成一件可靠的“产品”电源和外壳是绕不开的环节。4.1 供电方案选择CPX可以通过USB口或JST PH 2mm电池接口供电。USB供电最稳定适合固定在洗手池旁有插座的位置。缺点是拖着一根线不美观。电池供电推荐使用3.7V的锂聚合物电池如Adafruit的500mAh或1000mAh电池。CPX板载了电池充电和管理电路通过USB口即可为电池充电非常方便。这是实现设备便携和整洁外观的最佳选择。功耗估算与续航夜灯模式10个LED以低亮度亮度值20常亮每个LED电流约1-2mA总电流约20mA。加上主板待机电流约25mA。计时模式LED全亮且亮度较高电流可能达到60-80mA但每次只持续20秒。空闲模式无夜灯仅主板运行电流可低至10mA以下。假设使用一块1000mAh的电池如果仅在夜晚开启夜灯假设每晚8小时白天处于空闲状态那么理论续航约为1000mAh / ( (8h*25mA 16h*10mA) / 24h ) ≈ 1000 / 15 ≈ 66小时接近3天。实际上由于触发计时消耗很少主要耗电在夜灯。如果觉得续航短可以进一步降低夜灯亮度或者修改程序让夜灯在检测到一段时间无活动后自动熄灭用加速度计判断无人状态。4.2 外壳设计与安装要点一个合适的外壳能保护电路提升美观度并优化传感器效果。材料选择透明或半透明的亚克力盒子、塑料收纳盒是常见选择。确保盖子容易打开以便按动CPX上的复位按钮。传感器开孔CPX的光线传感器位于板子中央。在外壳对应位置不要贴任何深色或不透光的材料。最好能保留一个透明窗口。如果外壳本身是透明的那就最好。固定方式可以使用双面泡棉胶、蓝丁胶或尼龙扎带将CPX固定在外壳内部。确保其放置平稳LED面朝上。安装位置安装在洗手池上方的橱柜底部、镜子旁边或者直接放在台面上。确保手自然伸到水龙头下时能够轻松遮挡住设备上方的光线。安装高度建议离台面15-30厘米这个距离容易遮挡且LED灯光不会被直接直视形成柔和的反射光。我踩过的一个坑最初我使用了一个磨砂半透明的白色外壳发现光线传感器灵敏度大幅下降。因为磨砂材质散射了光线导致手遮挡前后亮度变化不明显。后来换用完全透明的盖子问题立刻解决。如果只能用非透明外壳就必须在传感器正上方精确开孔。4.3 功能扩展思路基础功能实现后你可以根据自己的兴趣进行扩展多模式切换通过按动CPX上的物理按钮A/B键可以在“仅夜灯”、“仅计时器”、“自动模式”之间切换。音效反馈CPX有板载扬声器。可以在触发、计时结束等节点添加更丰富的音效。注意夜间使用要避免吵人。计时时间可调通过长按A/B键来增加或减少WASH_DURATION并将设定值通过LED颜色或次数编码显示出来。低功耗优化利用加速度计在设备长时间静止如深夜时自动关闭所有LED甚至进入深度睡眠仅保留运动唤醒功能大幅延长电池续航。数据记录进阶如果使用CircuitPython可以将每天的洗手次数、平均时长记录到CPX的存储中后期通过USB连接电脑导出分析培养数据化习惯。这个项目从想法到实现涉及了传感器应用、状态机编程、交互设计和简单的产品化思考。它不只是一个玩具而是一个真正能融入生活、解决小痛点的智能设备。最重要的是通过亲手制作你会对嵌入式系统的软硬件协同工作有更深刻的理解。希望这份详细的指南能帮助你成功复现甚至做出更有趣的改进。
返回列表