
1. 项目缘起与整体设计思路1.1 这个项目到底在解决什么问题先说说我做这个项目的起因。家里有个上初中的孩子每天放学回家第一件事不是写作业而是摸手机、开电脑看视频。我试过设密码、拔网线、藏键盘效果都不持久——要么自己忘了密码要么把工作文件也一起锁了。后来我想与其堵不如用技术手段做“条件触发”人一坐到书桌前电脑自动把作业文档打开把娱乐页面关掉形成一种“环境暗示”。这个项目的核心逻辑非常朴素用PIR人体红外传感器检测书桌前是否有人检测到人之后通过Particle Photon 2这块物联网开发板把信号发到局域网内的一台常开主机主机上跑一个Node.js服务接收信号再调用AutoHotkey脚本去打开指定的作业文件、关闭浏览器娱乐标签页。如果人在书桌前持续待着就保持作业窗口置顶人离开超过设定时间就恢复原状。整套方案涉及五个关键技术点Particle Photon 2的固件编写、PIR传感器的信号处理、Node.js本地服务的搭建、ngrok做外网回调用于远程查看状态或从外面触发、以及AutoHotkey的窗口自动化。看起来是五个独立的东西但串起来之后就是一个完整的“物理世界触发数字世界”的闭环。适合谁来参考我觉得有三类人值得看一是家里有学龄孩子、想用技术手段做行为引导的家长二是刚接触物联网、想找一个“有实际用途”的入门项目的开发者三是已经会用Node.js但没玩过硬件、想了解PIR传感器和Photon 2怎么配合的软件工程师。哪怕你只是对AutoHotkey感兴趣这篇文章里的窗口操作部分也能直接抄去用。1.2 为什么选Particle Photon 2而不是Arduino或树莓派市面上做人体检测的方案很多我选Particle Photon 2是经过实际对比的。Arduino Uno便宜但它没有Wi-Fi要加ESP8266模块接线和固件都更麻烦树莓派功能强但功耗高、启动慢而且用Python写GPIO对新手来说门槛不低。Photon 2自带Wi-Fi、支持OTA更新、有云端事件系统最关键的是它的Particle.publish()和Particle.subscribe()用起来非常顺手几行代码就能把传感器事件推到云端或局域网。具体参数上Photon 2用的是Realtek RTL8721DM双核MCU主频200MHzFlash 2MBRAM 512KB支持2.4GHz Wi-Fi和BLE 5.0。PIR传感器我选的是常见的HC-SR501工作电压4.5V到20VPhoton 2的3.3V输出可以直接驱动它的VCC实测3.3V也能触发但灵敏度略低后面会讲怎么调。HC-SR501的输出是高电平3.3V可以直接接Photon 2的GPIO不需要电平转换这一点比接5V的Arduino省事。注意HC-SR501的默认输出脉宽是3秒左右如果你需要更短的触发信号要调电位器。但做“人坐下”检测3秒反而合适可以过滤掉走过路过的误触发。1.3 整体数据流与网络拓扑整个系统的数据流是这样的PIR传感器检测到人体移动输出高电平给Photon 2的D2引脚Photon 2固件里用中断或轮询检测这个电平变化一旦确认连续两次采样都是高电平就通过Particle.publish()发一个事件到Particle云同时我在局域网内跑了一个Node.js服务通过Particle的Webhook或者Server-Sent Events订阅这个事件Node.js收到事件后调用child_process执行一个AutoHotkey脚本AutoHotkey脚本负责激活作业文档窗口、最小化浏览器、把作业窗口置顶。外网部分用ngrok做内网穿透主要是为了两个场景一是我在公司想看看孩子有没有在书桌前二是偶尔需要从外面手动触发一次“打开作业”的操作。ngrok把本地的Node.js服务端口映射到一个公网地址然后我在手机浏览器上访问这个地址就能看到状态。这里要强调ngrok只是做本地服务的公网映射不涉及任何网络代理或特殊通道纯粹是开发调试用的内网穿透工具。网络拓扑上Photon 2连的是家里的2.4GHz Wi-FiNode.js服务跑在一台常年开机的迷你主机上Windows 10AutoHotkey也装在这台主机上。三者都在同一个局域网内延迟可以控制在200毫秒以内。实测从人坐下到作业窗口弹出总耗时大约1.5秒其中PIR传感器本身的响应时间占了0.5秒左右Photon 2的Wi-Fi传输和Particle云的中转占了0.8秒Node.js和AutoHotkey的执行只占0.2秒。2. 核心细节解析与实操要点2.1 PIR传感器与Photon 2的接线和调试先讲硬件接线这是最容易出错的地方。HC-SR501有三个引脚VCC、OUT、GND。Photon 2的引脚排列是左侧从上到下为D0到D7右侧为A0到A5和3V3、GND、VIN等。我的接法是PIR的VCC接Photon 2的3V3注意不是VINVIN是5V输入GND接Photon 2的GNDOUT接D2。为什么选D2因为D2支持外部中断后面如果要用中断方式检测会更方便。接线完成后先别急着写代码用Photon 2的Web IDE做一个最简单的测试读D2的电平串口打印出来。代码如下void setup() { pinMode(D2, INPUT); Serial.begin(9600); } void loop() { int val digitalRead(D2); Serial.println(val); delay(200); }烧录后打开串口监视器用手在PIR前面晃一下你应该能看到从0变成1然后过几秒又变回0。如果一直是1或者一直是0检查接线是否松动或者PIR的触发模式跳线帽是不是在“可重复触发”模式H模式。HC-SR501上有一个跳线帽H模式是可重复触发L模式是不可重复触发。做“人坐下”检测建议用H模式这样人只要在动输出就会持续为高。实操心得HC-SR501有两个电位器一个是灵敏度Sx一个是延时Tx。灵敏度调到中间偏右延时调到最小逆时针到底这样人一进入范围就能触发离开后很快恢复。如果延时太长人走了灯还亮着会导致作业窗口一直不关。2.2 Photon 2固件的关键逻辑与防抖处理固件部分我踩过最大的坑就是误触发。PIR传感器很敏感阳光变化、空调出风口、甚至鼠标移动都可能触发。如果每次触发都发事件Node.js那边会收到大量重复信号AutoHotkey就会反复执行窗口闪烁得没法用。所以固件里必须做防抖和状态机。我的做法是定义一个状态变量bool occupied false每次loop里读D2如果读到高电平并且occupied false就说明是“从无人到有人”的上升沿这时候发一次Particle.publish(motion/detected, 1, PRIVATE)然后把occupied设为true。如果读到低电平并且occupied true就发一次Particle.publish(motion/cleared, 0, PRIVATE)把occupied设为false。这样只有状态变化时才发事件中间持续的高电平不会重复发。另外加一个软件防抖连续读三次D2如果三次都是高才认为是真的高。代码如下bool readStable(int pin) { int count 0; for (int i 0; i 3; i) { if (digitalRead(pin) HIGH) count; delay(10); } return count 2; }这个readStable函数在loop里调用配合状态机实测误触发率从每小时十几次降到了几乎为零。还有一点Photon 2的Wi-Fi在信号弱的时候会重连重连期间Particle.publish会失败。我的处理是加一个重试机制如果publish返回false就等500毫秒再试一次最多试三次。2.3 Node.js服务的搭建与Particle事件订阅Node.js这边我用的是Express框架起一个HTTP服务同时用Particle的官方npm包particle-api-js来订阅事件。先讲环境配置这是很多新手卡住的地方。Windows上安装Node.js后在PowerShell里运行npm可能会报“无法加载文件 npm.ps1因为在此系统上禁止运行脚本”。这不是Node.js的问题是PowerShell的执行策略限制。解决办法是以管理员身份打开PowerShell运行Set-ExecutionPolicy RemoteSigned然后输入Y确认。或者改用CMD命令行就不会有这个报错。安装依赖npm init -y npm install express particle-api-js child_process然后写服务代码。核心逻辑是用Particle的access token登录订阅motion/detected和motion/cleared事件收到事件后调用AutoHotkey脚本。这里要注意Particle的事件订阅是基于Server-Sent Events的长连接如果网络断开需要自动重连。particle-api-js的subscribe方法本身有重连机制但我在实际使用中发现如果主机休眠再唤醒订阅会失效。所以我在代码里加了一个心跳检测每30秒检查一次连接状态如果断了就重新订阅。const Particle require(particle-api-js); const particle new Particle(); const TOKEN 你的access token; const DEVICE_ID 你的device id; function subscribeEvents() { particle.getEventStream({ deviceId: DEVICE_ID, auth: TOKEN, name: motion }).then(stream { stream.on(event, data { if (data.name motion/detected) { console.log(检测到人执行打开作业脚本); require(child_process).exec(AutoHotkey.exe open_homework.ahk); } else if (data.name motion/cleared) { console.log(人离开了执行恢复脚本); require(child_process).exec(AutoHotkey.exe restore.ahk); } }); }).catch(err { console.error(订阅失败5秒后重试, err); setTimeout(subscribeEvents, 5000); }); } subscribeEvents();注意child_process.exec执行AutoHotkey脚本时路径要用绝对路径否则Node.js的工作目录可能不对。另外AutoHotkey的exe路径也要写全比如C:\\Program Files\\AutoHotkey\\AutoHotkey.exe。2.4 ngrok内网穿透的配置与安全考量ngrok的作用是把本地的Node.js服务端口映射到公网这样我在外面也能访问。下载ngrok后先注册账号拿到authtoken然后运行ngrok authtoken 你的token ngrok http 3000这时候ngrok会给你一个公网地址比如https://xxxx.ngrok.io访问这个地址就会转发到本地的3000端口。我在Node.js里加了一个简单的状态页面显示当前是否有人在书桌前、最后一次触发时间、以及一个手动触发按钮。这样我在公司打开手机就能看到。安全方面要注意ngrok的免费版每次重启地址都会变而且公网地址任何人都能访问。所以我在Node.js里加了一个简单的token验证访问状态页面需要带上?token你的密码否则返回403。另外ngrok的免费版有连接数限制不要用来传大文件只做状态查询和手动触发足够了。实操心得ngrok在Windows上第一次运行可能会被防火墙拦截记得在Windows Defender防火墙里允许ngrok通过。另外如果家里路由器重启导致内网IP变化ngrok的本地端口映射不受影响因为它连的是127.0.0.1。3. 实操过程与核心环节实现3.1 AutoHotkey脚本的编写与窗口操作AutoHotkey是这个项目里最“接地气”的部分因为它直接操作Windows窗口。我用的版本是AutoHotkey v1.1v2的语法变化较大网上教程大多还是v1。先讲open_homework.ahk脚本的逻辑找到作业文档的窗口比如Word文档如果没打开就打开它然后激活窗口、最大化、置顶同时找到浏览器窗口最小化它。; open_homework.ahk SetTitleMatchMode, 2 ; 打开作业文档 IfWinNotExist, 数学作业.docx { Run, C:\Users\Parent\Documents\数学作业.docx WinWait, 数学作业.docx, , 10 } ; 激活并置顶 WinActivate, 数学作业.docx WinMaximize, 数学作业.docx WinSet, AlwaysOnTop, On, 数学作业.docx ; 最小化浏览器 IfWinExist, ahk_class Chrome_WidgetWin_1 { WinMinimize, ahk_class Chrome_WidgetWin_1 }restore.ahk脚本则相反取消置顶、恢复浏览器窗口、把作业文档最小化。; restore.ahk WinSet, AlwaysOnTop, Off, 数学作业.docx WinMinimize, 数学作业.docx IfWinExist, ahk_class Chrome_WidgetWin_1 { WinRestore, ahk_class Chrome_WidgetWin_1 }这里有个细节WinSet, AlwaysOnTop在Word文档上有时会失效因为Word的窗口结构比较特殊。我的解决办法是用WinActivate之后加一个WinSet, Topmost, On或者直接用WinMove把窗口移到最前面。实测下来AlwaysOnTop在Word 2016上有效在Word 365上偶尔需要重复执行一次。3.2 从传感器到脚本的完整联调过程联调是最考验耐心的环节。我按以下顺序逐步验证第一步单独测试PIR和Photon 2。用串口监视器确认D2电平变化正常Particle.publish能在Particle控制台看到事件。第二步单独测试Node.js订阅。在Particle控制台手动发布一个motion/detected事件看Node.js控制台是否打印日志。如果没打印检查access token和device id是否正确。第三步单独测试AutoHotkey脚本。双击open_homework.ahk看作业文档是否打开、浏览器是否最小化。如果没反应检查脚本里的窗口标题是否匹配可以用AutoHotkey自带的Window Spy工具查看窗口的准确标题和class。第四步全链路联调。人走到PIR前面观察从触发到窗口变化的完整过程。我第一次联调时发现PIR触发后要等3秒才发事件因为HC-SR501的默认延时就是3秒。后来把延时电位器逆时针调到最小响应时间缩短到0.5秒左右。第五步压力测试。反复在PIR前面走动看会不会出现窗口闪烁、脚本重复执行的问题。如果出现检查固件里的状态机是否生效以及Node.js是否对同一事件重复响应。注意Node.js的child_process.exec是异步的如果短时间内收到多个事件可能会同时启动多个AutoHotkey进程。我的处理是在Node.js里加一个简单的锁如果上一个脚本还没执行完就忽略新的事件。用一个let busy false变量控制。3.3 参数计算与阈值选择这个项目里涉及几个关键参数我逐一说明选择依据。PIR灵敏度HC-SR501的探测范围是7米以内角度约100度。书桌前的距离大约1.5米所以灵敏度不需要调到最高。我把灵敏度电位器调到中间偏右约60%的位置实测人坐下时能稳定触发走过路过距离2米以上不会误触发。延时时间延时电位器调到最小约0.3秒。但PIR本身有一个不可调的“封锁时间”约2秒在这期间不会重复触发。所以实际响应时间约0.5秒到1秒。Photon 2的采样间隔loop里用delay(100)即每100毫秒采样一次。配合三次采样防抖实际检测延迟约300毫秒。这个延迟对人来说几乎无感。Node.js的锁超时busy变量在脚本执行后设为true然后setTimeout在2秒后设为false。为什么是2秒因为AutoHotkey脚本执行时间通常在1秒以内2秒是安全余量。ngrok的心跳间隔30秒一次。太短会增加网络负担太长会导致状态更新不及时。30秒是实测下来比较平衡的值。3.4 实际部署中的物理安装细节硬件安装也有讲究。PIR传感器不能对着窗户否则阳光变化会误触发。我把它固定在书桌正上方的书架隔板下面镜头朝下覆盖书桌前的座椅区域。Photon 2用USB线供电插在书桌旁边的插座上Wi-Fi信号强度在-60dBm左右足够稳定。迷你主机放在客厅电视柜下面常年开机设置成不休眠。Windows的电源计划里把“睡眠”设为“从不”否则Node.js服务会断。另外AutoHotkey脚本要设置成开机自启动放在shell:startup文件夹里。实操心得Photon 2的USB线要选质量好的劣质线会导致供电不足Wi-Fi频繁掉线。我一开始用了一根便宜的延长线结果Photon 2每隔几分钟就重启换了原装线之后问题消失。4. 常见问题与排查技巧实录4.1 硬件层面的典型故障问题一PIR传感器一直输出高电平。原因通常是触发模式跳线帽在H模式且环境中有持续的热源变化比如空调出风口对着传感器。解决办法是把跳线帽拔下来插到L模式或者调整传感器角度避开热源。问题二Photon 2连不上Wi-Fi。检查Wi-Fi是不是2.4GHzPhoton 2不支持5GHz。另外如果Wi-Fi密码里有特殊字符在固件里要用转义。我遇到过密码里有符号导致连接失败的情况改成简单密码后正常。问题三PIR触发但Photon 2没反应。用万用表量D2引脚触发时应该有3.3V。如果没有检查PIR的OUT引脚是否接错或者PIR的VCC是否接了5VPhoton 2的VIN是5V但3V3输出电流有限接PIR可能不够。实测HC-SR501在3.3V下工作正常但如果你的PIR是其他型号可能需要5V。4.2 软件层面的常见报错与解决报错一npm : 无法加载文件 npm.ps1因为在此系统上禁止运行脚本。这是PowerShell执行策略问题前面已经讲过用Set-ExecutionPolicy RemoteSigned解决。如果公司电脑有组策略限制改用CMD或Git Bash。报错二Particle.publish返回false。通常是Wi-Fi信号弱或Particle云连接断开。在固件里加一个Particle.connected()检查如果没连接就等一会儿再发。另外事件名称不能有空格和特殊字符用motion/detected这种格式最安全。报错三AutoHotkey脚本找不到窗口。用Window Spy查看窗口的准确标题。Word文档的标题通常是“文档名 - Word”但如果有未保存的修改标题会变成“文档名* - Word”。所以SetTitleMatchMode, 2部分匹配比精确匹配更可靠。报错四ngrok启动后本地服务访问不了。检查Node.js服务是否监听在0.0.0.0而不是127.0.0.1。Express默认监听所有接口但如果你用了app.listen(3000, 127.0.0.1)ngrok就转发不了。改成app.listen(3000)即可。4.3 常见问题速查表现象可能原因排查方法解决方案PIR一直输出高触发模式为H且环境有热源用手挡住PIR镜头看是否变低改跳线帽到L模式或调整角度Photon 2频繁重启USB供电不足换一根短而粗的USB线用原装线或带供电的USB HubNode.js收不到事件access token或device id错误在Particle控制台手动发事件测试重新生成token确认device idAutoHotkey脚本不执行路径含空格未加引号在CMD里手动运行脚本路径用双引号包裹ngrok地址访问403未带token参数检查URL是否包含?tokenxxx在Node.js里加token验证逻辑窗口置顶失效Word窗口结构特殊用Window Spy查看窗口class改用WinMove或重复执行置顶4.4 独家避坑技巧第一个技巧PIR传感器的镜头不要正对任何会移动的物体包括窗帘、风扇、宠物。我一开始把PIR对着书桌结果家里的猫跳上桌子也会触发。后来把PIR的探测角度调窄用黑色胶带遮住镜头两侧只覆盖座椅区域问题解决。第二个技巧Node.js服务要用pm2做进程守护。Windows上直接node server.js如果终端关了服务就停了。用pm2 start server.js --name motion-opener然后pm2 save和pm2 startup这样开机自启、崩溃自动重启。第三个技巧AutoHotkey脚本里加一个日志输出把每次执行的时间、窗口标题写到一个txt文件里。这样出问题的时候可以回溯看看是哪个环节没执行。我用的代码是FileAppend, %A_Now% 执行打开作业n, C:\logs\motion.log。第四个技巧ngrok的免费版有请求频率限制不要用它做高频轮询。我的状态页面是手动刷新的不做自动轮询。如果需要实时状态可以在局域网内用WebSocket外网访问时再走ngrok。第五个技巧Photon 2的固件OTA更新时如果Wi-Fi不稳定会变砖。所以我在固件里加了System.disableUpdates()只在需要更新时手动开启。另外Photon 2有一个安全模式如果固件崩溃快速按复位键三次可以进入安全模式重新刷固件。5. 扩展玩法与个人经验体会5.1 这个项目还能怎么玩基础功能跑通之后我做了几个扩展。第一个是加一个物理按钮接在Photon 2的D3引脚上按下就手动触发一次“打开作业”这样孩子自己也能操作不用等我从公司远程触发。第二个是加一个蜂鸣器检测到人坐下但作业窗口超过5分钟没打开就响一声提醒。第三个是把状态推送到手机用Particle的Webhook把事件发到一个简单的通知服务上这样我在外面也能收到“孩子已坐下”的提醒。还有一个比较实用的扩展用AutoHotkey记录作业窗口的活跃时间。每次窗口激活时开始计时最小化或切换窗口时停止把累计时间写到日志里。这样一周下来就能看到孩子每天在作业窗口前待了多久比单纯“有没有打开”更有参考价值。5.2 我在实际使用中踩过的坑最大的坑是PIR传感器的误触发导致作业窗口反复弹出。有一次我在客厅看电视孩子根本没在书桌前但作业窗口每隔几分钟就弹出来一次。排查后发现是PIR对着窗户傍晚阳光角度变化触发了传感器。后来把PIR移到书架内侧问题解决。这件事让我意识到物理安装位置比代码逻辑更重要传感器放错了地方再好的防抖算法也救不回来。第二个坑是Node.js服务的内存泄漏。跑了大概一周后Node.js进程占了1.5GB内存。原因是particle-api-js的事件流在重连时没有释放旧的监听器。解决办法是每次重连前先调用stream.removeAllListeners()或者干脆用pm2设置内存上限超过500MB自动重启。第三个坑是AutoHotkey脚本在锁屏状态下不执行。Windows锁屏后AutoHotkey的窗口操作会失效。我的解决办法是让迷你主机不锁屏设置成“屏幕关闭但系统不锁屏”。在电源计划里把“关闭显示器”设为10分钟“使计算机进入睡眠状态”设为“从不”然后在组策略里禁用锁屏。5.3 给想复现的朋友几点建议如果你打算照着做我建议先从最简单的部分开始先让Photon 2和PIR跑通能在串口看到电平变化再让Node.js能收到Particle事件最后再折腾AutoHotkey。不要一上来就全部串起来那样出了问题很难定位。另外不要追求一次完美。我的第一版固件没有防抖Node.js没有锁AutoHotkey脚本也写得很粗糙但至少能跑起来。跑起来之后再逐步优化比一开始就追求完美要快得多。最后这个项目的核心价值不在于技术多复杂而在于它真的能改变行为。我孩子现在养成了习惯一坐到书桌前电脑就自动打开作业文档他也就顺势开始写了。技术只是辅助真正的改变还是靠人。但一个好的技术辅助能让改变更容易发生。