1. 项目概述与核心思路几年前当我第一次想把家里的灯改造成“人来灯亮人走灯灭”时我试过红外传感器、摄像头甚至压力垫。红外传感器容易误判静止的人摄像头有隐私顾虑压力垫则不够灵活。直到我开始琢磨手环、手表这些我们每天随身携带的设备思路才豁然开朗它们不就是一个绝佳的、无感的“信标”吗基于这个想法我动手做了一个基于ESP32和BLE的智能存在传感器。它的核心逻辑非常简单利用ESP32强大的蓝牙扫描能力持续监听周围环境中的BLE广播信号。当它识别到我们预先设定的、属于我们自己的设备比如Apple Watch或小米手环的信号强度RSSI进入一个“近场”阈值时就判定为“人已就位”并通过网络触发一个自动化动作比如打开桌面的台灯反之当信号减弱到“远场”阈值之外则触发“人已离开”的动作关闭台灯。这个方案的精妙之处在于它的无感化和个性化。你不需要佩戴额外的标签你的日常穿戴设备就是钥匙。它不依赖图像或声音只关心那个独一无二的蓝牙广播标识隐私性更好。整个系统的构建成本极低一个ESP32开发板一个智能插座或任何支持网络控制的设备加上一些代码和配置就能让一个普通的角落变得“智能”。下面我将拆解整个从构思到实现的全过程包括硬件选型、代码逻辑、平台配置以及最重要的——现场校准经验希望能帮你避开我踩过的那些坑。2. 核心硬件与平台选型解析2.1 为什么是ESP32在开始动手前选择合适的主控芯片是关键。我选择ESP32并非因为它流行而是它在性价比和功能上完美匹配这个项目。首先双核与无线能力。ESP32集成了2.4GHz Wi-Fi和蓝牙包括经典蓝牙和低功耗蓝牙BLE这意味着一块芯片就能同时完成“扫描蓝牙信号”和“连接Wi-Fi发送网络请求”两件事无需额外的通信模块简化了电路设计和成本。其双核处理器也能更好地处理并发任务比如一个核心负责密集的BLE扫描另一个核心处理网络通信和逻辑判断保证系统响应及时。其次开发友好度。ESP32支持Arduino IDE开发环境有海量的社区库支持。对于BLE扫描这种操作已经有非常成熟的ESP32 BLE Arduino库我们无需从零编写底层驱动可以专注于应用逻辑。此外它的功耗在持续工作模式下相对可控适合长期插电运行。注意ESP32有不同的型号和封装。对于这个项目最经济实惠的选择是ESP32 DevKit C或NodeMCU-32S这类开发板。它们自带USB转串口芯片方便编程和调试引脚也全部引出便于后续扩展虽然本项目不一定需要。2.2 BLE设备作为触发源的考量你的可穿戴设备如智能手表、手环或手机是本项目的“信标”。它们会周期性地向外广播Advertising数据包其中包含设备标识信息。我们需要理解不同设备的广播特性这直接影响识别的准确性和稳定性。iOS设备iPhone, Apple Watch优势广播功率Tx Power信息通常包含在广播包里这有助于我们更精确地计算距离虽然本项目主要用RSSI但有Tx Power会更准。Apple Watch的广播功率较高约-24dBm信号更强更容易被检测到。挑战iOS设备出于隐私保护会使用随机MAC地址进行广播。这意味着设备每次重启蓝牙其广播的MAC地址都会变化你无法用一个固定的MAC地址来锁定它。这是最大的坑。Android设备优势部分安卓设备允许在开发者选项或通过特定APP固定蓝牙MAC地址或者其随机化策略不如iOS严格可能在一段时间内保持稳定。挑战品牌和系统版本差异大行为不一需要实测。专用BLE信标或另一块ESP32优势这是最稳定、最可靠的方案。你可以完全控制广播的内容例如广播一个固定的、自定义的设备名称Device Name或厂商自定义数据Manufacturer Data。一块ESP32刷写一个简单的iBeacon或自定义广播程序成本低且行为完全可控。推荐如果你追求稳定性和避免折腾强烈建议使用一个额外的ESP32或购买的BLE信标作为触发源。你可以把它放在钥匙串上或者口袋里。识别策略总结针对iOS/随机MAC设备放弃使用MAC地址转而使用设备名称Device Name或厂商数据Manufacturer Data。例如你可以将Apple Watch的名称设置为一个独特的字符串如“MyWatch_Trigger”然后让ESP32扫描这个名称。或者识别苹果公司特定的厂商ID0x004C结合设备类型来过滤。针对Android/固定MAC设备可以尝试使用MAC地址但要做好它可能变化的心理准备。针对自制信标使用自定义的、固定的设备名称或厂商数据这是最理想的。2.3 执行端IFTTT与智能设备检测到状态变化后我们需要执行动作。我选择IFTTT作为自动化枢纽因为它连接了海量的互联网服务从智能家居到社交媒体几乎无所不能。IFTTT工作原理它的核心是“如果…那么…”If This Then That的自动化小程序Applet。This是触发器That是执行动作。在本项目中This将由我们的ESP32通过一个简单的网络请求Webhook来触发。Webhook服务IFTTT提供了一个叫“Webhooks”的服务。你可以把它想象成一个独一无二的网址URL。当ESP32向这个URL发送一个HTTP请求时就相当于按下了IFTTT的一个虚拟按钮从而触发后面连接的所有动作。智能插座选择我示例中用了TECKIN智能插座因为它性价比高且支持IFTTT。实际上任何能接入“Smart Life”或“Tuya”生态它们基本都支持IFTTT的插座、灯泡、开关都可以。关键在于你需要在IFTTT里能找到对应的服务如“Smart Life”来执行“开/关”操作。实操心得在购买智能设备前务必确认其是否支持IFTTT。一个简单的检查方法是去IFTTT官网或App在创建That动作时搜索该设备的品牌或对应的云平台如Smart Life, Yeelight, Philips Hue等。3. 软件环境搭建与核心代码剖析3.1 Arduino IDE环境配置ESP32不是原生Arduino板所以需要一些配置。安装Arduino IDE从官网下载并安装最新版。添加ESP32开发板支持打开Arduino IDE进入文件 - 首选项。在“附加开发板管理器网址”中填入以下网址如果已有其他用逗号隔开https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json点击“好”保存。安装ESP32开发板进入工具 - 开发板 - 开发板管理器...。搜索“esp32”找到由“Espressif Systems”发布的“ESP32 Arduino”点击安装。安装必要的库进入工具 - 管理库...。搜索“BLE”找到“ESP32 BLE Arduino”并安装。这个库提供了我们扫描BLE设备所需的所有函数。3.2 分区表Partition Table的关键设置这是新手最容易忽略而导致编译失败的一步。们的代码因为同时包含了Wi-Fi、HTTP客户端和BLE扫描体积会超过ESP32默认的1.2MB应用程序分区大小。解决方法在Arduino IDE中更改分区方案。连接你的ESP32开发板在工具 - 开发板中选择正确的型号如“ESP32 Dev Module”。找到工具 - Partition Scheme选项。选择“No OTA (2MB APP/2MB SPIFFS)”。这个方案将4MB闪存的一半2MB分配给应用程序足够容纳我们的代码另一半分配给文件系统本项目未使用但无妨。踩坑记录如果不进行这一步编译时可能不会报错但在上传时会提示“回滚”或运行不稳定。更常见的是直接编译失败报错提示代码大小超过限制。所以务必先设置分区方案。3.3 核心代码逻辑逐行解读以下是项目的核心Arduino代码骨架我将结合注释解释关键部分。你需要在代码中替换你自己的Wi-Fi信息和IFTTT Webhook地址。#include BLEDevice.h #include BLEUtils.h #include BLEScan.h #include WiFi.h #include HTTPClient.h // 1. 网络配置 - 必须修改 const char* ssid “你的Wi-Fi名称”; const char* password “你的Wi-Fi密码”; // 2. IFTTT Webhook地址 - 必须修改 // 这是“人靠近”时触发的URL const char* iftttWebhookUrl_sit “https://maker.ifttt.com/trigger/Sit/with/key/你的密钥”; // 这是“人离开”时触发的URL const char* iftttWebhookUrl_stand “https://maker.ifttt.com/trigger/Stand/with/key/你的密钥”; // 3. 目标设备识别信息 - 根据你的策略修改 // 示例使用设备名称识别。将你的设备蓝牙名称设为“MyAppleWatch” String targetDeviceName “MyAppleWatch”; // 或者使用MAC地址对iOS设备不推荐 // String targetMacAddress “AA:BB:CC:DD:EE:FF”; // 4. 信号强度阈值 (RSSI) - 需要校准 int nearThreshold -55; // 信号强于此值认为“靠近” int farThreshold -70; // 信号弱于此值认为“远离” // 注意RSSI是负值数值越大越接近0信号越强。 // 5. 状态跟踪变量 bool devicePresent false; // 当前目标设备是否在“靠近”区域 unsigned long lastTriggerTime 0; const long cooldownPeriod 10000; // 防抖时间10秒内不重复触发 // BLE扫描设置 BLEScan* pBLEScan; int scanTime 2; // 每次扫描持续时间秒 // 自定义的扫描结果回调类 class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { // 这个函数会对每个扫描到的设备调用一次 String deviceName advertisedDevice.getName().c_str(); String macAddress advertisedDevice.getAddress().toString().c_str(); int rssi advertisedDevice.getRSSI(); // 策略1通过设备名称识别 if (deviceName.indexOf(targetDeviceName) ! -1) { Serial.print(“发现目标设备: “); Serial.print(deviceName); Serial.print(“, RSSI: “); Serial.println(rssi); processDevicePresence(rssi); } // 策略2通过MAC地址识别注释掉上面启用下面 /* if (macAddress.equalsIgnoreCase(targetMacAddress)) { Serial.print(“发现目标设备(MAC): “); Serial.print(macAddress); Serial.print(“, RSSI: “); Serial.println(rssi); processDevicePresence(rssi); } */ } }; void processDevicePresence(int rssi) { unsigned long currentTime millis(); // 防抖处理避免频繁触发 if (currentTime - lastTriggerTime cooldownPeriod) { return; } if (rssi nearThreshold !devicePresent) { // 信号变强且之前状态是“远离”触发“靠近”动作 Serial.println(“- 触发「靠近」动作”); triggerIFTTT(iftttWebhookUrl_sit); devicePresent true; lastTriggerTime currentTime; } else if (rssi farThreshold devicePresent) { // 信号变弱且之前状态是“靠近”触发“远离”动作 Serial.println(“- 触发「远离」动作”); triggerIFTTT(iftttWebhookUrl_stand); devicePresent false; lastTriggerTime currentTime; } // 如果RSSI在两个阈值之间则保持原状态不做任何操作 } void triggerIFTTT(const char* url) { if (WiFi.status() WL_CONNECTED) { HTTPClient http; http.begin(url); int httpCode http.GET(); if (httpCode 0) { Serial.printf(“IFTTT触发成功HTTP状态码: %d\n”, httpCode); } else { Serial.printf(“IFTTT触发失败错误: %s\n”, http.errorToString(httpCode).c_str()); } http.end(); } else { Serial.println(“Wi-Fi未连接无法触发IFTTT”); } } void setup() { Serial.begin(115200); Serial.println(“ESP32 BLE存在传感器启动...”); // 连接Wi-Fi WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(“.”); } Serial.println(“\nWi-Fi连接成功!”); // 初始化BLE扫描 BLEDevice::init(“”); pBLEScan BLEDevice::getScan(); pBLEScan-setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan-setActiveScan(true); // 主动扫描获取更多信息但更耗电 pBLEScan-setInterval(100); // 扫描间隔(ms) pBLEScan-setWindow(99); // 扫描窗口(ms)应小于等于间隔 } void loop() { // 执行一次BLE扫描 BLEScanResults foundDevices pBLEScan-start(scanTime, false); // 扫描完成后清理结果准备下一次扫描 pBLEScan-clearResults(); delay(1000); // 扫描间隔可根据需要调整 }代码关键点解析MyAdvertisedDeviceCallbacks类这是核心。每次扫描到一个BLE设备onResult函数就会被调用。我们在这里检查设备的名称或MAC地址是否匹配目标。processDevicePresence函数这是状态机逻辑。它根据当前RSSI和预设的阈值判断是否发生了状态切换从“靠近”到“远离”或反之并调用triggerIFTTT。防抖机制cooldownPeriod变量至关重要。蓝牙信号存在波动可能瞬间穿越阈值又返回导致动作被反复触发。这个“冷却时间”确保了在状态切换后的一段时间内如10秒即使信号波动也不会再次触发避免了设备的“鬼畜”开关。主动扫描Active ScansetActiveScan(true)会让ESP32在收到广播后向设备发送扫描请求以获取更多信息如设备名称。这比被动扫描更耗电但能获取到设备名称对于识别iOS设备至关重要。如果你的信标是自制的且只广播特定数据可以设为false以省电。4. IFTTT与智能设备联动配置详解4.1 创建IFTTT Webhook触发器注册并登录IFTTT访问 ifttt.com 或使用手机App。创建新的Applet点击右上角你的头像选择“Create”。设置This(触发器)点击“ Add”。在搜索框中输入“webhooks”选择“Webhooks”服务。选择触发事件“Receive a web request”。输入事件名称Event Name例如Sit。这个名称将用于构建URL请牢记。点击“Create trigger”。设置That(动作)点“ Add”。搜索你的智能设备对应的服务例如“Smart Life”。选择动作如“Turn on”。按照提示关联你的Smart Life账户需要提前在Smart Life App中添加好设备并选择你想要控制的智能插座。点击“Create action”。完成创建给你的Applet起个名字比如“Desk Light ON”然后点击“Finish”。获取Webhook URL回到IFTTT主页点击右上角头像选择“My Services”。找到并点击“Webhooks”服务。点击右上角的“Documentation”。页面会显示你的唯一密钥Key。你的Webhook URL格式为https://maker.ifttt.com/trigger/{event}/with/key/{your_key}将{event}替换为你刚才创建的事件名如Sit{your_key}替换为页面显示的密钥。这样就得到了一个完整的URL例如https://maker.ifttt.com/trigger/Sit/with/key/dp_123abc456xyz强烈建议点击页面底部的“Test it”按钮测试一下。你会看到“Event has been triggered”的提示同时你的智能插座应该会打开。这能验证整个链路是否通畅。创建离开动作重复步骤2-6创建一个新Applet。这次事件名称可以叫Stand动作选择“Turn off”。得到第二个Webhook URL。注意事项IFTTT的免费账户对Webhook的调用频率有一定限制大约每分钟数百次但对于存在传感器这种低频应用几分钟甚至更久触发一次完全足够。请勿在代码中设置过短的扫描间隔导致频繁调用。4.2 智能设备以Smart Life/Tuya为例接入在手机上下载“Smart Life”或“涂鸦智能”App。注册账号并登录。确保智能插座已通电并使其进入配网模式通常长按按钮直到指示灯快闪。在App内点击“添加设备”选择“插座”类别按照提示输入你的Wi-Fi密码等待App发现并添加设备。添加成功后你应能在App内手动控制插座的开关。这是关键一步只有App能控制IFTTT才能控制。关联IFTTT在创建IFTTT Applet时当你选择“Smart Life”作为动作服务时IFTTT会引导你授权登录Smart Life账户。完成授权后你的设备列表就会出现在下拉菜单中供你选择。这个过程通常是自动的非常顺畅。5. 现场部署、校准与优化实战5.1 部署位置选择ESP32的放置位置直接决定了传感器的灵敏度和可靠性。原则尽可能靠近你希望检测的“存在区域”的中心并减少障碍物。例如要检测是否坐在书桌前就把ESP32放在书桌下方或显示器背后。避免干扰源远离大型金属物体、微波炉、路由器2.4GHz Wi-Fi可能干扰BLE等。供电考虑ESP32需要5V供电。可以使用手机充电器USB线或者移动电源。确保供电稳定。5.2 信号阈值RSSI校准这是整个项目成败的最关键步骤。nearThreshold和farThreshold这两个值不是拍脑袋定的必须通过实地测量来确定。上传初始代码将包含Wi-Fi和IFTTT URL的代码上传到ESP32但先注释掉triggerIFTTT函数里的http.GET()调用或者将两个URL暂时改成无效的避免在校准期间频繁触发你的智能设备。打开串口监视器波特率设置为115200。你会看到ESP32不断打印扫描到的所有BLE设备信息。定位目标设备将你的目标设备如手表戴在手上在串口输出中寻找它。通过设备名称或你已知的MAC地址片段来识别。记录下它的RSSI值。测量“靠近”点RSSI将你的设备放在你希望触发“靠近”动作的典型位置如坐在书桌前手腕的位置保持约30秒观察串口输出的RSSI值。它会是一个波动的范围例如在 -50dBm 到 -60dBm 之间波动。取一个偏保守的较强信号值作为nearThreshold。例如如果波动范围是-50 ~ -60可以设为-55。这意味着信号强于-55时才判定为“靠近”。测量“远离”点RSSI拿着你的设备走到你希望触发“离开”动作的边界位置如离开书桌2米远同样观察RSSI值。它会变得更弱例如在 -70dBm 到 -80dBm 之间。取一个偏保守的较弱信号值作为farThreshold。例如可以设为-70。这意味着信号弱于-70时才判定为“远离”。设置“滞后区间”注意nearThreshold(-55) 和farThreshold(-70) 之间有一个15dB的差值这个区间称为“滞后区间”或“死区”。当RSSI落在这个区间时设备状态保持不变。这个区间非常重要它防止了信号在阈值边缘抖动时导致的频繁状态切换。我建议这个差值至少为10-20dB。更新代码并测试将测量好的nearThreshold和farThreshold值更新到代码中重新上传。恢复triggerIFTTT函数的调用。进行实际走动测试观察串口打印的状态切换日志是否准确智能设备的动作是否符合预期。5.3 高级优化与问题排查问题1检测不稳定偶尔漏检或误触发。可能原因1信号波动。蓝牙信号受环境人体遮挡、其他无线设备影响很大。解决方案除了设置“滞后区间”还可以在代码中加入“持续确认”机制。不要因为一次扫描到/没扫描到就立刻改变状态。可以改为连续3次扫描都满足“靠近”条件才切换到“靠近”状态连续3次扫描都满足“远离”条件才切换到“远离”状态。这能极大增强抗干扰能力。可能原因2扫描间隔不当。scanTime太短可能抓不到设备广播太长则响应迟钝。解决方案调整scanTime如1-3秒和loop中的delay。一个常见的策略是快速扫描如1秒几次然后休眠几秒以省电但对于插电项目可以持续快速扫描。可能原因3设备广播间隔。有些设备为了省电广播间隔较长如几百毫秒到几秒。ESP32的扫描窗口可能错过。解决方案确保pBLEScan-setWindow(99)的值小于等于setInterval(100)并且扫描时间scanTime至少是设备广播间隔的2-3倍。问题2如何支持多个设备假设你想让家人的手机也能触发同一个传感器。解决方案修改MyAdvertisedDeviceCallbacks类中的判断逻辑。可以定义一个设备ID数组然后检查扫描到的设备是否匹配数组中任意一个。String allowedDevices[] {“MyWatch”, “SpousePhone”, “KidTracker”}; bool isTargetDevice false; for (String dev : allowedDevices) { if (deviceName.indexOf(dev) ! -1) { isTargetDevice true; break; } } if (isTargetDevice) { processDevicePresence(rssi); }问题3ESP32偶尔重启或Wi-Fi断开。可能原因电源不稳定或内存泄漏。解决方案使用质量好的5V/2A电源适配器和USB线供电。在代码中加入Wi-Fi重连机制。在loop()函数开头检查WiFi.status()如果断开则尝试重连。确保分区方案设置正确No OTA 2MB APP。检查代码中是否有动态内存分配未释放。本项目代码中BLEScan::clearResults()已经负责清理问题不大。问题4想脱离IFTTT直接控制本地设备。IFTTT依赖互联网有延迟且存在服务不可用的风险。如果你希望更快速、更可靠地控制本地设备如通过MQTT控制Home Assistant里的设备或直接控制局网内的智能灯可以修改triggerIFTTT函数。替代方案1MQTT引入PubSubClient库让ESP32在状态变化时向本地MQTT服务器如运行在树莓派上的Mosquitto发布一条消息。Home Assistant订阅该主题即可执行自动化。替代方案2HTTP直接控制有些智能设备提供了局域网API如Yeelight、部分Tuya设备。你可以让ESP32直接向设备的本地IP地址发送HTTP或UDP指令完全脱离云端速度极快。经过以上步骤一个稳定可靠的智能存在传感器就搭建完成了。它静静地待在角落通过无形的蓝牙信号感知你的到来与离开并悄然控制着周边的环境。这种“无感智能”正是智能家居最迷人的体验之一。你可以将这个传感器扩展到更多场景放在门口检测家人归来自动播放欢迎语音放在床头实现入睡自动关灯甚至放在宠物活动区监测它们的活动。核心逻辑是相通的剩下的就是发挥你的想象力了。
基于ESP32与BLE信号强度(RSSI)的无感存在传感器DIY指南
1. 项目概述与核心思路几年前当我第一次想把家里的灯改造成“人来灯亮人走灯灭”时我试过红外传感器、摄像头甚至压力垫。红外传感器容易误判静止的人摄像头有隐私顾虑压力垫则不够灵活。直到我开始琢磨手环、手表这些我们每天随身携带的设备思路才豁然开朗它们不就是一个绝佳的、无感的“信标”吗基于这个想法我动手做了一个基于ESP32和BLE的智能存在传感器。它的核心逻辑非常简单利用ESP32强大的蓝牙扫描能力持续监听周围环境中的BLE广播信号。当它识别到我们预先设定的、属于我们自己的设备比如Apple Watch或小米手环的信号强度RSSI进入一个“近场”阈值时就判定为“人已就位”并通过网络触发一个自动化动作比如打开桌面的台灯反之当信号减弱到“远场”阈值之外则触发“人已离开”的动作关闭台灯。这个方案的精妙之处在于它的无感化和个性化。你不需要佩戴额外的标签你的日常穿戴设备就是钥匙。它不依赖图像或声音只关心那个独一无二的蓝牙广播标识隐私性更好。整个系统的构建成本极低一个ESP32开发板一个智能插座或任何支持网络控制的设备加上一些代码和配置就能让一个普通的角落变得“智能”。下面我将拆解整个从构思到实现的全过程包括硬件选型、代码逻辑、平台配置以及最重要的——现场校准经验希望能帮你避开我踩过的那些坑。2. 核心硬件与平台选型解析2.1 为什么是ESP32在开始动手前选择合适的主控芯片是关键。我选择ESP32并非因为它流行而是它在性价比和功能上完美匹配这个项目。首先双核与无线能力。ESP32集成了2.4GHz Wi-Fi和蓝牙包括经典蓝牙和低功耗蓝牙BLE这意味着一块芯片就能同时完成“扫描蓝牙信号”和“连接Wi-Fi发送网络请求”两件事无需额外的通信模块简化了电路设计和成本。其双核处理器也能更好地处理并发任务比如一个核心负责密集的BLE扫描另一个核心处理网络通信和逻辑判断保证系统响应及时。其次开发友好度。ESP32支持Arduino IDE开发环境有海量的社区库支持。对于BLE扫描这种操作已经有非常成熟的ESP32 BLE Arduino库我们无需从零编写底层驱动可以专注于应用逻辑。此外它的功耗在持续工作模式下相对可控适合长期插电运行。注意ESP32有不同的型号和封装。对于这个项目最经济实惠的选择是ESP32 DevKit C或NodeMCU-32S这类开发板。它们自带USB转串口芯片方便编程和调试引脚也全部引出便于后续扩展虽然本项目不一定需要。2.2 BLE设备作为触发源的考量你的可穿戴设备如智能手表、手环或手机是本项目的“信标”。它们会周期性地向外广播Advertising数据包其中包含设备标识信息。我们需要理解不同设备的广播特性这直接影响识别的准确性和稳定性。iOS设备iPhone, Apple Watch优势广播功率Tx Power信息通常包含在广播包里这有助于我们更精确地计算距离虽然本项目主要用RSSI但有Tx Power会更准。Apple Watch的广播功率较高约-24dBm信号更强更容易被检测到。挑战iOS设备出于隐私保护会使用随机MAC地址进行广播。这意味着设备每次重启蓝牙其广播的MAC地址都会变化你无法用一个固定的MAC地址来锁定它。这是最大的坑。Android设备优势部分安卓设备允许在开发者选项或通过特定APP固定蓝牙MAC地址或者其随机化策略不如iOS严格可能在一段时间内保持稳定。挑战品牌和系统版本差异大行为不一需要实测。专用BLE信标或另一块ESP32优势这是最稳定、最可靠的方案。你可以完全控制广播的内容例如广播一个固定的、自定义的设备名称Device Name或厂商自定义数据Manufacturer Data。一块ESP32刷写一个简单的iBeacon或自定义广播程序成本低且行为完全可控。推荐如果你追求稳定性和避免折腾强烈建议使用一个额外的ESP32或购买的BLE信标作为触发源。你可以把它放在钥匙串上或者口袋里。识别策略总结针对iOS/随机MAC设备放弃使用MAC地址转而使用设备名称Device Name或厂商数据Manufacturer Data。例如你可以将Apple Watch的名称设置为一个独特的字符串如“MyWatch_Trigger”然后让ESP32扫描这个名称。或者识别苹果公司特定的厂商ID0x004C结合设备类型来过滤。针对Android/固定MAC设备可以尝试使用MAC地址但要做好它可能变化的心理准备。针对自制信标使用自定义的、固定的设备名称或厂商数据这是最理想的。2.3 执行端IFTTT与智能设备检测到状态变化后我们需要执行动作。我选择IFTTT作为自动化枢纽因为它连接了海量的互联网服务从智能家居到社交媒体几乎无所不能。IFTTT工作原理它的核心是“如果…那么…”If This Then That的自动化小程序Applet。This是触发器That是执行动作。在本项目中This将由我们的ESP32通过一个简单的网络请求Webhook来触发。Webhook服务IFTTT提供了一个叫“Webhooks”的服务。你可以把它想象成一个独一无二的网址URL。当ESP32向这个URL发送一个HTTP请求时就相当于按下了IFTTT的一个虚拟按钮从而触发后面连接的所有动作。智能插座选择我示例中用了TECKIN智能插座因为它性价比高且支持IFTTT。实际上任何能接入“Smart Life”或“Tuya”生态它们基本都支持IFTTT的插座、灯泡、开关都可以。关键在于你需要在IFTTT里能找到对应的服务如“Smart Life”来执行“开/关”操作。实操心得在购买智能设备前务必确认其是否支持IFTTT。一个简单的检查方法是去IFTTT官网或App在创建That动作时搜索该设备的品牌或对应的云平台如Smart Life, Yeelight, Philips Hue等。3. 软件环境搭建与核心代码剖析3.1 Arduino IDE环境配置ESP32不是原生Arduino板所以需要一些配置。安装Arduino IDE从官网下载并安装最新版。添加ESP32开发板支持打开Arduino IDE进入文件 - 首选项。在“附加开发板管理器网址”中填入以下网址如果已有其他用逗号隔开https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json点击“好”保存。安装ESP32开发板进入工具 - 开发板 - 开发板管理器...。搜索“esp32”找到由“Espressif Systems”发布的“ESP32 Arduino”点击安装。安装必要的库进入工具 - 管理库...。搜索“BLE”找到“ESP32 BLE Arduino”并安装。这个库提供了我们扫描BLE设备所需的所有函数。3.2 分区表Partition Table的关键设置这是新手最容易忽略而导致编译失败的一步。们的代码因为同时包含了Wi-Fi、HTTP客户端和BLE扫描体积会超过ESP32默认的1.2MB应用程序分区大小。解决方法在Arduino IDE中更改分区方案。连接你的ESP32开发板在工具 - 开发板中选择正确的型号如“ESP32 Dev Module”。找到工具 - Partition Scheme选项。选择“No OTA (2MB APP/2MB SPIFFS)”。这个方案将4MB闪存的一半2MB分配给应用程序足够容纳我们的代码另一半分配给文件系统本项目未使用但无妨。踩坑记录如果不进行这一步编译时可能不会报错但在上传时会提示“回滚”或运行不稳定。更常见的是直接编译失败报错提示代码大小超过限制。所以务必先设置分区方案。3.3 核心代码逻辑逐行解读以下是项目的核心Arduino代码骨架我将结合注释解释关键部分。你需要在代码中替换你自己的Wi-Fi信息和IFTTT Webhook地址。#include BLEDevice.h #include BLEUtils.h #include BLEScan.h #include WiFi.h #include HTTPClient.h // 1. 网络配置 - 必须修改 const char* ssid “你的Wi-Fi名称”; const char* password “你的Wi-Fi密码”; // 2. IFTTT Webhook地址 - 必须修改 // 这是“人靠近”时触发的URL const char* iftttWebhookUrl_sit “https://maker.ifttt.com/trigger/Sit/with/key/你的密钥”; // 这是“人离开”时触发的URL const char* iftttWebhookUrl_stand “https://maker.ifttt.com/trigger/Stand/with/key/你的密钥”; // 3. 目标设备识别信息 - 根据你的策略修改 // 示例使用设备名称识别。将你的设备蓝牙名称设为“MyAppleWatch” String targetDeviceName “MyAppleWatch”; // 或者使用MAC地址对iOS设备不推荐 // String targetMacAddress “AA:BB:CC:DD:EE:FF”; // 4. 信号强度阈值 (RSSI) - 需要校准 int nearThreshold -55; // 信号强于此值认为“靠近” int farThreshold -70; // 信号弱于此值认为“远离” // 注意RSSI是负值数值越大越接近0信号越强。 // 5. 状态跟踪变量 bool devicePresent false; // 当前目标设备是否在“靠近”区域 unsigned long lastTriggerTime 0; const long cooldownPeriod 10000; // 防抖时间10秒内不重复触发 // BLE扫描设置 BLEScan* pBLEScan; int scanTime 2; // 每次扫描持续时间秒 // 自定义的扫描结果回调类 class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) { // 这个函数会对每个扫描到的设备调用一次 String deviceName advertisedDevice.getName().c_str(); String macAddress advertisedDevice.getAddress().toString().c_str(); int rssi advertisedDevice.getRSSI(); // 策略1通过设备名称识别 if (deviceName.indexOf(targetDeviceName) ! -1) { Serial.print(“发现目标设备: “); Serial.print(deviceName); Serial.print(“, RSSI: “); Serial.println(rssi); processDevicePresence(rssi); } // 策略2通过MAC地址识别注释掉上面启用下面 /* if (macAddress.equalsIgnoreCase(targetMacAddress)) { Serial.print(“发现目标设备(MAC): “); Serial.print(macAddress); Serial.print(“, RSSI: “); Serial.println(rssi); processDevicePresence(rssi); } */ } }; void processDevicePresence(int rssi) { unsigned long currentTime millis(); // 防抖处理避免频繁触发 if (currentTime - lastTriggerTime cooldownPeriod) { return; } if (rssi nearThreshold !devicePresent) { // 信号变强且之前状态是“远离”触发“靠近”动作 Serial.println(“- 触发「靠近」动作”); triggerIFTTT(iftttWebhookUrl_sit); devicePresent true; lastTriggerTime currentTime; } else if (rssi farThreshold devicePresent) { // 信号变弱且之前状态是“靠近”触发“远离”动作 Serial.println(“- 触发「远离」动作”); triggerIFTTT(iftttWebhookUrl_stand); devicePresent false; lastTriggerTime currentTime; } // 如果RSSI在两个阈值之间则保持原状态不做任何操作 } void triggerIFTTT(const char* url) { if (WiFi.status() WL_CONNECTED) { HTTPClient http; http.begin(url); int httpCode http.GET(); if (httpCode 0) { Serial.printf(“IFTTT触发成功HTTP状态码: %d\n”, httpCode); } else { Serial.printf(“IFTTT触发失败错误: %s\n”, http.errorToString(httpCode).c_str()); } http.end(); } else { Serial.println(“Wi-Fi未连接无法触发IFTTT”); } } void setup() { Serial.begin(115200); Serial.println(“ESP32 BLE存在传感器启动...”); // 连接Wi-Fi WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(“.”); } Serial.println(“\nWi-Fi连接成功!”); // 初始化BLE扫描 BLEDevice::init(“”); pBLEScan BLEDevice::getScan(); pBLEScan-setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks()); pBLEScan-setActiveScan(true); // 主动扫描获取更多信息但更耗电 pBLEScan-setInterval(100); // 扫描间隔(ms) pBLEScan-setWindow(99); // 扫描窗口(ms)应小于等于间隔 } void loop() { // 执行一次BLE扫描 BLEScanResults foundDevices pBLEScan-start(scanTime, false); // 扫描完成后清理结果准备下一次扫描 pBLEScan-clearResults(); delay(1000); // 扫描间隔可根据需要调整 }代码关键点解析MyAdvertisedDeviceCallbacks类这是核心。每次扫描到一个BLE设备onResult函数就会被调用。我们在这里检查设备的名称或MAC地址是否匹配目标。processDevicePresence函数这是状态机逻辑。它根据当前RSSI和预设的阈值判断是否发生了状态切换从“靠近”到“远离”或反之并调用triggerIFTTT。防抖机制cooldownPeriod变量至关重要。蓝牙信号存在波动可能瞬间穿越阈值又返回导致动作被反复触发。这个“冷却时间”确保了在状态切换后的一段时间内如10秒即使信号波动也不会再次触发避免了设备的“鬼畜”开关。主动扫描Active ScansetActiveScan(true)会让ESP32在收到广播后向设备发送扫描请求以获取更多信息如设备名称。这比被动扫描更耗电但能获取到设备名称对于识别iOS设备至关重要。如果你的信标是自制的且只广播特定数据可以设为false以省电。4. IFTTT与智能设备联动配置详解4.1 创建IFTTT Webhook触发器注册并登录IFTTT访问 ifttt.com 或使用手机App。创建新的Applet点击右上角你的头像选择“Create”。设置This(触发器)点击“ Add”。在搜索框中输入“webhooks”选择“Webhooks”服务。选择触发事件“Receive a web request”。输入事件名称Event Name例如Sit。这个名称将用于构建URL请牢记。点击“Create trigger”。设置That(动作)点“ Add”。搜索你的智能设备对应的服务例如“Smart Life”。选择动作如“Turn on”。按照提示关联你的Smart Life账户需要提前在Smart Life App中添加好设备并选择你想要控制的智能插座。点击“Create action”。完成创建给你的Applet起个名字比如“Desk Light ON”然后点击“Finish”。获取Webhook URL回到IFTTT主页点击右上角头像选择“My Services”。找到并点击“Webhooks”服务。点击右上角的“Documentation”。页面会显示你的唯一密钥Key。你的Webhook URL格式为https://maker.ifttt.com/trigger/{event}/with/key/{your_key}将{event}替换为你刚才创建的事件名如Sit{your_key}替换为页面显示的密钥。这样就得到了一个完整的URL例如https://maker.ifttt.com/trigger/Sit/with/key/dp_123abc456xyz强烈建议点击页面底部的“Test it”按钮测试一下。你会看到“Event has been triggered”的提示同时你的智能插座应该会打开。这能验证整个链路是否通畅。创建离开动作重复步骤2-6创建一个新Applet。这次事件名称可以叫Stand动作选择“Turn off”。得到第二个Webhook URL。注意事项IFTTT的免费账户对Webhook的调用频率有一定限制大约每分钟数百次但对于存在传感器这种低频应用几分钟甚至更久触发一次完全足够。请勿在代码中设置过短的扫描间隔导致频繁调用。4.2 智能设备以Smart Life/Tuya为例接入在手机上下载“Smart Life”或“涂鸦智能”App。注册账号并登录。确保智能插座已通电并使其进入配网模式通常长按按钮直到指示灯快闪。在App内点击“添加设备”选择“插座”类别按照提示输入你的Wi-Fi密码等待App发现并添加设备。添加成功后你应能在App内手动控制插座的开关。这是关键一步只有App能控制IFTTT才能控制。关联IFTTT在创建IFTTT Applet时当你选择“Smart Life”作为动作服务时IFTTT会引导你授权登录Smart Life账户。完成授权后你的设备列表就会出现在下拉菜单中供你选择。这个过程通常是自动的非常顺畅。5. 现场部署、校准与优化实战5.1 部署位置选择ESP32的放置位置直接决定了传感器的灵敏度和可靠性。原则尽可能靠近你希望检测的“存在区域”的中心并减少障碍物。例如要检测是否坐在书桌前就把ESP32放在书桌下方或显示器背后。避免干扰源远离大型金属物体、微波炉、路由器2.4GHz Wi-Fi可能干扰BLE等。供电考虑ESP32需要5V供电。可以使用手机充电器USB线或者移动电源。确保供电稳定。5.2 信号阈值RSSI校准这是整个项目成败的最关键步骤。nearThreshold和farThreshold这两个值不是拍脑袋定的必须通过实地测量来确定。上传初始代码将包含Wi-Fi和IFTTT URL的代码上传到ESP32但先注释掉triggerIFTTT函数里的http.GET()调用或者将两个URL暂时改成无效的避免在校准期间频繁触发你的智能设备。打开串口监视器波特率设置为115200。你会看到ESP32不断打印扫描到的所有BLE设备信息。定位目标设备将你的目标设备如手表戴在手上在串口输出中寻找它。通过设备名称或你已知的MAC地址片段来识别。记录下它的RSSI值。测量“靠近”点RSSI将你的设备放在你希望触发“靠近”动作的典型位置如坐在书桌前手腕的位置保持约30秒观察串口输出的RSSI值。它会是一个波动的范围例如在 -50dBm 到 -60dBm 之间波动。取一个偏保守的较强信号值作为nearThreshold。例如如果波动范围是-50 ~ -60可以设为-55。这意味着信号强于-55时才判定为“靠近”。测量“远离”点RSSI拿着你的设备走到你希望触发“离开”动作的边界位置如离开书桌2米远同样观察RSSI值。它会变得更弱例如在 -70dBm 到 -80dBm 之间。取一个偏保守的较弱信号值作为farThreshold。例如可以设为-70。这意味着信号弱于-70时才判定为“远离”。设置“滞后区间”注意nearThreshold(-55) 和farThreshold(-70) 之间有一个15dB的差值这个区间称为“滞后区间”或“死区”。当RSSI落在这个区间时设备状态保持不变。这个区间非常重要它防止了信号在阈值边缘抖动时导致的频繁状态切换。我建议这个差值至少为10-20dB。更新代码并测试将测量好的nearThreshold和farThreshold值更新到代码中重新上传。恢复triggerIFTTT函数的调用。进行实际走动测试观察串口打印的状态切换日志是否准确智能设备的动作是否符合预期。5.3 高级优化与问题排查问题1检测不稳定偶尔漏检或误触发。可能原因1信号波动。蓝牙信号受环境人体遮挡、其他无线设备影响很大。解决方案除了设置“滞后区间”还可以在代码中加入“持续确认”机制。不要因为一次扫描到/没扫描到就立刻改变状态。可以改为连续3次扫描都满足“靠近”条件才切换到“靠近”状态连续3次扫描都满足“远离”条件才切换到“远离”状态。这能极大增强抗干扰能力。可能原因2扫描间隔不当。scanTime太短可能抓不到设备广播太长则响应迟钝。解决方案调整scanTime如1-3秒和loop中的delay。一个常见的策略是快速扫描如1秒几次然后休眠几秒以省电但对于插电项目可以持续快速扫描。可能原因3设备广播间隔。有些设备为了省电广播间隔较长如几百毫秒到几秒。ESP32的扫描窗口可能错过。解决方案确保pBLEScan-setWindow(99)的值小于等于setInterval(100)并且扫描时间scanTime至少是设备广播间隔的2-3倍。问题2如何支持多个设备假设你想让家人的手机也能触发同一个传感器。解决方案修改MyAdvertisedDeviceCallbacks类中的判断逻辑。可以定义一个设备ID数组然后检查扫描到的设备是否匹配数组中任意一个。String allowedDevices[] {“MyWatch”, “SpousePhone”, “KidTracker”}; bool isTargetDevice false; for (String dev : allowedDevices) { if (deviceName.indexOf(dev) ! -1) { isTargetDevice true; break; } } if (isTargetDevice) { processDevicePresence(rssi); }问题3ESP32偶尔重启或Wi-Fi断开。可能原因电源不稳定或内存泄漏。解决方案使用质量好的5V/2A电源适配器和USB线供电。在代码中加入Wi-Fi重连机制。在loop()函数开头检查WiFi.status()如果断开则尝试重连。确保分区方案设置正确No OTA 2MB APP。检查代码中是否有动态内存分配未释放。本项目代码中BLEScan::clearResults()已经负责清理问题不大。问题4想脱离IFTTT直接控制本地设备。IFTTT依赖互联网有延迟且存在服务不可用的风险。如果你希望更快速、更可靠地控制本地设备如通过MQTT控制Home Assistant里的设备或直接控制局网内的智能灯可以修改triggerIFTTT函数。替代方案1MQTT引入PubSubClient库让ESP32在状态变化时向本地MQTT服务器如运行在树莓派上的Mosquitto发布一条消息。Home Assistant订阅该主题即可执行自动化。替代方案2HTTP直接控制有些智能设备提供了局域网API如Yeelight、部分Tuya设备。你可以让ESP32直接向设备的本地IP地址发送HTTP或UDP指令完全脱离云端速度极快。经过以上步骤一个稳定可靠的智能存在传感器就搭建完成了。它静静地待在角落通过无形的蓝牙信号感知你的到来与离开并悄然控制着周边的环境。这种“无感智能”正是智能家居最迷人的体验之一。你可以将这个传感器扩展到更多场景放在门口检测家人归来自动播放欢迎语音放在床头实现入睡自动关灯甚至放在宠物活动区监测它们的活动。核心逻辑是相通的剩下的就是发挥你的想象力了。