1. 项目概述:一个基于蓝牙定位与光感应的智能家居自动化系统
最近在折腾一个挺有意思的智能家居项目,我把它叫做“HomeCheckerLightsOnWiFiFreifunkRepeater”。这个名字有点长,但基本概括了它的核心功能:利用蓝牙技术判断家里有谁在、在哪个房间,然后结合环境光传感器,自动控制灯光开关,同时还能作为一个Wi-Fi中继/信号扩展器,通过Freifunk这样的社区网络协议来传递设备状态和增强网络覆盖。
听起来是不是有点复杂?其实拆解开来,它解决的是几个很实际的痛点。首先,传统的智能家居传感器(比如人体红外)只能判断“有没有人”,无法区分是家人还是访客,更没法知道具体是谁。其次,单纯基于动作的灯光控制,在人静止不动时(比如在沙发上看书)就会误关灯,体验很差。再者,很多智能设备依赖云服务,一旦外网断了或者路由器信号不好,整个系统就瘫痪了。
我这个项目的核心思路,就是用一个ESP32开发板作为大脑,让它同时干三件事:
- 蓝牙信标扫描与距离估算:通过扫描家庭成员手机或佩戴的蓝牙设备(如手环、防丢器)的广播信号强度(RSSI),来判断特定人员是否在家,并粗略估算其与各个传感器的距离。
- 环境光感测:在每个需要控灯的房间部署一个带光敏电阻的ESP32子节点,实时监测环境光照度。
- 逻辑控制与网络通信:主控制器(可以是另一个ESP32或树莓派)综合“谁在哪个房间”以及“那个房间当前是否够亮”这两个信息,决定是否要打开或关闭该房间的灯光。所有状态更新和设备间通信,通过本地Wi-Fi网络完成,并且利用Freifunk的OLSR或B.A.T.M.A.N.等Mesh路由协议,让设备自身也能充当中继,解决家庭Wi-Fi死角问题。
这个方案完全在本地局域网(LAN)内运行,不依赖任何互联网云服务,保证了隐私和可靠性。它特别适合多层住宅、大户型或者Wi-Fi信号覆盖不均的家庭,让你走到哪儿,合适的灯光就亮到哪儿,网络信号也跟着增强。
2. 系统核心设计思路与方案选型
为什么选择这样一套方案?这背后是一系列针对传统智能家居弊病的权衡和取舍。市面上成熟的解决方案,如某米或某家的全家桶,通常需要购买昂贵的专用网关、传感器和灯具,并且数据大多要上传到厂商的云端服务器。这带来了成本高、隐私顾虑、以及云服务宕机导致本地功能失效的风险。
2.1 为什么是蓝牙+光感,而不是纯红外或雷达?
人体红外传感器(PIR)成本低,但只能检测移动的热源,无法进行身份识别和静止存在检测。毫米波雷达传感器精度高,能实现静止存在感知,但成本较高,编程相对复杂,并且同样无法进行身份识别。
蓝牙方案的优势在于:
- 身份识别:每个蓝牙设备(特别是手机)都有唯一的MAC地址,我们可以将其与家庭成员绑定。虽然MAC地址随机化是个挑战,但可以通过配对手环或专用防丢器(它们的MAC地址通常是固定的)来规避。
- 距离感知:虽然蓝牙RSSI(接收信号强度指示)受环境影响大,精度不高,但对于“在房间A”还是“在房间B”这种级别的区域判断是足够的。通过部署多个ESP32作为扫描节点,利用三角定位或多点RSSI对比,可以大致确定人员位置。
- 低功耗与普及性:蓝牙低功耗(BLE)设备非常省电,手机更是人人随身携带。利用现有设备,无需为每位成员额外购置专用标签(当然,为获得更好体验,专用标签更稳定)。
引入光传感器的必要性:单纯靠位置开灯,白天光线充足时也会误触发,造成能源浪费。增加一个廉价的光敏电阻或BH1750数字光照传感器,让系统只在环境光低于设定阈值且有人在场时才开灯,实现了真正的“智能”。
2.2 主控与通信架构:ESP32 + 本地Wi-Fi + Freifunk Mesh
主控选择ESP32的理由:ESP32是一款性价比极高的MCU,它双核240MHz处理器、内置Wi-Fi和蓝牙,正是本项目所需的两大无线功能的完美载体。其丰富的GPIO和ADC引脚可以轻松连接光敏传感器、继电器模块(控制灯具)等。社区生态庞大,Arduino框架或ESP-IDF下都有丰富的库支持。
网络通信设计:系统摒弃了云-端模型,采用本地MQTT协议作为通信中枢。所有ESP32节点(包括作为蓝牙扫描器/光感器的子节点和作为逻辑控制中心的主节点)都连接到家庭内部的一个MQTT代理服务器(例如运行在树莓派上的Mosquitto)。子节点将采集到的“蓝牙设备MAC地址及RSSI”和“光照度”数据发布到特定的MQTT主题(如home/presence/livingroom和home/light/livingroom),主节点订阅这些主题,进行逻辑运算后,再向控制灯光的主题(如home/switch/livingroom/cmd)发布“开”或“关”指令。
Freifunk Mesh网络的融入:这是项目的另一个亮点。Freifunk是一个去中心化的社区无线网络理念,其核心是使用开源固件(如OpenWrt/LEDE)将无线路由器刷成Mesh节点。在我们的系统中,可以将一部分ESP32节点(特别是需要放置到Wi-Fi信号边缘位置的节点)配置为ESP-Mesh或利用ESP-NOW协议。但更常见的做法是,使用一个专门的路由器刷入Freifunk固件(使用OLSR或B.A.T.M.A.N. Advanced路由协议)作为主Mesh节点,而ESP32则作为客户端连接这个Mesh网络。这样,即使某个ESP32子节点距离主路由器太远,它也可以通过邻近的另一个Mesh节点中继,将数据传回MQTT服务器,同时扩展了家庭Wi-Fi的覆盖范围。这解决了智能设备因Wi-Fi信号弱而频繁掉线的问题。
注意:ESP32的Wi-Fi在Station模式下功耗不低,如果节点由电池供电,需谨慎使用。对于常电供电的节点,这无疑是一个增强网络健壮性的好方法。
3. 硬件准备与核心电路解析
工欲善其事,必先利其器。这个项目硬件成本可控,大部分模块都很常见。
3.1 核心物料清单
| 组件 | 型号/规格 | 数量 | 用途说明 |
|---|---|---|---|
| 主控芯片 | ESP32开发板(如ESP32-DevKitC、NodeMCU-32S) | 至少2个 | 1个作为主逻辑控制器,1个或多个作为带传感器的子节点。 |
| 光照传感器 | 光敏电阻模块 或 BH1750数字光照传感器 | 每个子节点1个 | 推荐BH1750,精度高,使用I2C接口,不受ESP32 ADC非线性影响。 |
| 电源模块 | 5V/2A Micro USB电源 或 3.3V/1A稳压模块 | 每个节点1套 | 为ESP32及传感器供电。如果控制220V灯具,需确保继电器模块供电。 |
| 继电器模块 | 1路或2路5V继电器模块 | 根据控灯路数 | 用于安全控制交流灯具的通断。务必注意高压危险! |
| 蓝牙信标 | 智能手机 或 专用BLE防丢器(如Tile Mate) | 每人1个 | 作为被扫描的身份标识物。防丢器更稳定。 |
| (可选)外壳 | 3D打印或塑料防水盒 | 每个节点1个 | 保护电路,尤其是安装在卫生间、厨房等处的节点。 |
| (可选)Mesh路由器 | 支持OpenWrt的旧路由器(如TP-Link WR841N) | 1-2个 | 刷入Freifunk固件,构建家庭Mesh网络骨干。 |
3.2 关键电路连接与注意事项
这里以一个集成了蓝牙扫描和光感功能的子节点为例,讲解如何连接ESP32与传感器。我们假设使用BH1750和继电器模块。
接线示意图(文字描述):
BH1750光照传感器:这是一个I2C设备。
- VCC -> ESP32的3.3V引脚
- GND -> ESP32的GND引脚
- SCL -> ESP32的GPIO 22 (默认I2C时钟线)
- SDA -> ESP32的GPIO 21 (默认I2C数据线)
继电器模块:用于控制灯光。
- VCC -> ESP32的5V引脚(注意:有些继电器模块是5V驱动,有些是3.3V,务必看清)
- GND -> ESP32的GND引脚
- IN (信号引脚) -> ESP32的某个GPIO,例如GPIO 23
供电:通过Micro USB口为整个ESP32开发板供电,开发板上的3.3V和5V引脚可为外围模块供电。
重要安全提示:继电器模块的常开(NO)、公共端(COM)接口连接的是220V市电。这部分操作必须完全断电进行,并确保所有高压线连接牢固,用电工胶布绝缘,最好将整个高压部分装入绝缘外壳。如果你对强电不熟悉,建议先使用低压直流灯泡(如12V LED灯带)进行测试,或者寻求专业人士帮助。安全永远是第一位的。
为什么选择BH1750而不是光敏电阻?光敏电阻模拟值受ESP32 ADC(模数转换器)参考电压波动和非线性影响较大,需要复杂的校准才能得到稳定的勒克斯(Lux)值。BH1750是数字传感器,直接通过I2C输出光照度数值,精度高,程序编写简单,抗干扰能力强。虽然贵几块钱,但能省去大量调试时间,强烈推荐。
4. 软件框架与核心代码实现
软件部分是整个系统的灵魂,我们采用Arduino框架进行开发,因为它库丰富,上手快。整个系统分为子节点(Sensor Node)和主节点(Controller Node)两类程序。
4.1 子节点程序设计:数据采集与上报
子节点的任务是周期性地执行三件事:扫描周围的BLE设备、读取光照度、将数据打包通过Wi-Fi发送到MQTT服务器。
// 示例:子节点核心逻辑框架 (Arduino IDE) #include <WiFi.h> #include <PubSubClient.h> // MQTT客户端库 #include <BLEDevice.h> #include <BLEUtils.h> #include <BLEScan.h> #include <Wire.h> #include <BH1750.h> // 网络配置 const char* ssid = "Your_SSID"; const char* password = "Your_PASSWORD"; const char* mqtt_server = "192.168.1.100"; // MQTT Broker IP // 设备身份 const char* node_id = "living_room_sensor_01"; WiFiClient espClient; PubSubClient client(espClient); BH1750 lightMeter; BLEScan* pBLEScan; // 存储已知家庭成员蓝牙地址 String knownDevices[] = {"aa:bb:cc:dd:ee:ff", "ff:ee:dd:cc:bb:aa"}; // 替换为实际地址 String knownNames[] = {"Alice", "Bob"}; void setup() { Serial.begin(115200); Wire.begin(); lightMeter.begin(); setup_wifi(); client.setServer(mqtt_server, 1883); BLEDevice::init(""); pBLEScan = BLEDevice::getScan(); pBLEScan->setActiveScan(true); // 主动扫描,获取更多信息 pBLEScan->setInterval(100); pBLEScan->setWindow(99); } void loop() { if (!client.connected()) { reconnect_mqtt(); } client.loop(); // 每10秒执行一次采集和上报 static unsigned long lastReport = 0; if (millis() - lastReport > 10000) { lastReport = millis(); // 1. 读取光照 float lux = lightMeter.readLightLevel(); String lightTopic = "home/" + String(node_id) + "/light"; client.publish(lightTopic.c_str(), String(lux).c_str()); // 2. 扫描蓝牙并上报 BLEScanResults foundDevices = pBLEScan->start(5, false); // 扫描5秒 String presenceData = ""; for (int i = 0; i < foundDevices.getCount(); i++) { BLEAdvertisedDevice device = foundDevices.getDevice(i); String addr = device.getAddress().toString().c_str(); int rssi = device.getRSSI(); // 检查是否为已知设备 for (int j = 0; j < sizeof(knownDevices)/sizeof(knownDevices[0]); j++) { if (addr.equalsIgnoreCase(knownDevices[j])) { presenceData += knownNames[j] + ":" + String(rssi) + ","; break; } } } pBLEScan->clearResults(); if (presenceData.length() > 0) { presenceData.remove(presenceData.length()-1); // 去掉最后一个逗号 } String presenceTopic = "home/" + String(node_id) + "/presence"; client.publish(presenceTopic.c_str(), presenceData.c_str()); } } void setup_wifi() { /* ... 标准Wi-Fi连接代码 ... */ } void reconnect_mqtt() { /* ... MQTT重连代码 ... */ }代码关键点解析:
- 蓝牙扫描:我们使用
BLEScan进行主动扫描,获取到的RSSI值是判断距离的关键。扫描时间不宜过长(这里5秒),否则会影响其他任务的实时性。 - 数据格式:上报的在场数据格式设计为
"Alice:-65,Bob:-72",包含了人名和对应的信号强度,方便主节点解析。 - MQTT主题设计:采用分层主题结构,如
home/living_room_sensor_01/light和home/living_room_sensor_01/presence,清晰且易于订阅管理。
4.2 主节点程序设计:逻辑决策与灯光控制
主节点订阅所有子节点的主题,根据规则进行决策,并发布控制命令。
// 示例:主节点核心逻辑框架 #include <WiFi.h> #include <PubSubClient.h> // ... 网络配置类似,略 ... // 决策参数 const int LIGHT_THRESHOLD = 50; // 光照阈值,低于此值且有人则开灯 (单位: Lux) const int RSSI_THRESHOLD = -70; // RSSI阈值,强于此值(更大)认为人在此房间附近 // 存储各房间状态 struct RoomStatus { float lightLevel; bool personPresent; String personName; }; RoomStatus roomStatus["living_room"] = {100.0, false, ""}; // 示例 void callback(char* topic, byte* payload, unsigned int length) { String msg; for (int i = 0; i < length; i++) { msg += (char)payload[i]; } // 解析主题,例如 "home/living_room_sensor_01/light" String topicStr = String(topic); int lastSlash = topicStr.lastIndexOf('/'); String sensorName = topicStr.substring(5, lastSlash); // 提取"sensor_01" String dataType = topicStr.substring(lastSlash + 1); // 提取"light"或"presence" if (dataType == "light") { roomStatus[sensorName].lightLevel = msg.toFloat(); } else if (dataType == "presence") { // 解析"Alice:-65,Bob:-72" roomStatus[sensorName].personPresent = false; if (msg.length() > 0) { int start = 0; int end = msg.indexOf(','); while (end != -1) { processPresence(msg.substring(start, end), sensorName); start = end + 1; end = msg.indexOf(',', start); } processPresence(msg.substring(start), sensorName); // 处理最后一段 } } // 触发决策函数 makeDecision(sensorName); } void processPresence(String data, String room) { // data格式 "Alice:-65" int colon = data.indexOf(':'); if (colon != -1) { String name = data.substring(0, colon); int rssi = data.substring(colon+1).toInt(); // 简单的决策:取信号最强(RSSI最大)的那个人作为该房间的主要在场者 if (rssi > RSSI_THRESHOLD) { roomStatus[room].personPresent = true; roomStatus[room].personName = name; } } } void makeDecision(String room) { bool shouldLightBeOn = false; if (roomStatus[room].personPresent && roomStatus[room].lightLevel < LIGHT_THRESHOLD) { shouldLightBeOn = true; } // 获取当前灯的状态(可能需要从另一个主题读取,或本地记录) bool currentLightState = false; // 假设初始为关 if (shouldLightBeOn != currentLightState) { String cmdTopic = "home/switch/" + room + "/cmd"; String cmd = shouldLightBeOn ? "ON" : "OFF"; client.publish(cmdTopic.c_str(), cmd.c_str()); Serial.printf("Room %s: Light %s\n", room.c_str(), cmd.c_str()); // 更新本地记录的状态 currentLightState = shouldLightBeOn; } } void setup() { // ... 初始化WiFi和MQTT,并订阅所有子节点的主题 ... client.subscribe("home/+/light"); client.subscribe("home/+/presence"); client.setCallback(callback); } void loop() { client.loop(); }决策逻辑的精髓:这里的决策逻辑是简化的“与”逻辑:有人且光线暗才开灯。RSSI_THRESHOLD是一个需要根据实际环境(房间大小、墙体材质)反复调试的关键参数。更复杂的逻辑可以加入“延时关灯”(人离开后延迟一段时间再关)、“多房间优先级”(人在两个房间交界处)等。
5. 系统集成、调试与避坑指南
将硬件和软件组合起来,并让它们稳定工作,是最考验耐心和细心的环节。
5.1 网络配置与MQTT Broker搭建
首先,你需要一个稳定的MQTT Broker。最方便的方法是在家庭网络中常开一台低功耗设备(如树莓派、旧笔记本甚至一台虚拟机)来运行它。
在树莓派上安装Mosquitto:
sudo apt update sudo apt install mosquitto mosquitto-clients sudo systemctl enable mosquitto sudo systemctl start mosquitto安装后,Mosquitto服务就在1883端口运行了。你可以使用mosquitto_sub和mosquitto_pub命令测试订阅和发布,验证ESP32是否能成功连接和通信。
Wi-Fi连接稳定性:ESP32的Wi-Fi连接在早期版本固件中可能不稳定。确保使用最新的Arduino-ESP32核心库。在代码中加入健壮的重连机制,并考虑使用WiFi.setSleep(false)来禁止Wi-Fi休眠,虽然会增加功耗,但能极大提升连接稳定性。
5.2 蓝牙定位的校准与优化
蓝牙RSSI定位是本项目最大的误差来源。以下方法可以显著提升准确性:
- 现场校准:在每个房间的中心点,用手机或防丢器分别测量距离传感器节点0.5米、1米、2米、3米时的RSSI值,记录多组数据求平均,绘制大致的“距离-RSSI”曲线。你会发现,不同方向、有无遮挡,数值差异很大。
- 多点协同判决:不要只依赖一个节点的RSSI做判断。例如,客厅和走廊各有一个节点。当Alice的手机在客厅节点的RSSI为-60,在走廊节点为-85时,可以很有把握地判断她在客厅。主节点的逻辑可以升级为比较同一设备在不同传感器上的RSSI强度。
- 滤波算法:RSSI值跳动剧烈。在程序中加入滑动平均滤波或卡尔曼滤波(对于ESP32来说稍复杂),能平滑数据,避免灯光因信号瞬时波动而频繁开关。
// 简单的滑动平均滤波示例 #define FILTER_SIZE 5 int rssiBuffer[FILTER_SIZE] = {0}; int bufferIndex = 0; int getFilteredRSSI(int newRssi) { rssiBuffer[bufferIndex % FILTER_SIZE] = newRssi; bufferIndex++; long sum = 0; int count = min(bufferIndex, FILTER_SIZE); for (int i = 0; i < count; i++) { sum += rssiBuffer[i]; } return sum / count; } - 应对MAC地址随机化:现代智能手机为了隐私,会定期随机化蓝牙MAC地址。这会使基于固定MAC的识别失效。解决方案:
- 使用固定MAC的专用设备:如防丢器、智能手环(需确认其广播地址是否固定)。
- 扫描设备名称或服务UUID:有些设备广播的名称是固定的(如“Tile Mate”),或包含特定的Service UUID。你可以通过扫描这些信息来识别设备类型,再结合其他信息(如同时出现的设备组合)来推断身份。但这增加了复杂性。
5.3 与Freifunk Mesh网络的整合
这一步是可选的,但能极大提升系统的鲁棒性,尤其适合大户型。
- 准备Mesh路由器:找一台支持OpenWrt的路由器,刷入基于Freifunk的固件(如
Gluon)。配置Mesh网络(通常是802.11s协议)和DHCP。将这台路由器连接到你的主路由LAN口,它将成为Mesh网络的一个节点。 - 配置ESP32连接Mesh网络:对于ESP32来说,它不需要知道网络是Mesh的。你只需要在ESP32的Wi-Fi设置中,将SSID和密码设置为这个Freifunk Mesh网络的SSID和密码即可。Mesh网络内部会自动为ESP32分配IP地址并选择最佳路径回传数据到MQTT服务器所在的网关。
- 优势体现:当你把某个ESP32子节点放在后院花园(主Wi-Fi信号很弱),如果花园在另一个Freifunk Mesh节点的覆盖范围内,ESP32就可以通过那个节点中继,稳定地将数据传回网络,从而实现了“Wi-Fi信号扩展”和“数据回传”的双重目的。
6. 常见问题排查与实战心得
在实际部署中,你肯定会遇到各种各样的问题。这里把我踩过的坑和解决方案总结一下。
6.1 问题排查速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| ESP32无法连接Wi-Fi | 1. SSID/密码错误 2. 路由器信道不支持 3. 信号太弱 4. 固件问题 | 1. 检查代码中的凭据,确保路由器是2.4GHz频段(ESP32不支持5GHz)。 2. 尝试将路由器信道固定在1, 6, 11。 3. 使用 WiFi.begin()并检查返回状态,增加重试次数和延迟。4. 更新Arduino-ESP32核心库到最新版本。 |
| MQTT连接频繁断开 | 1. 网络不稳定 2. KeepAlive时间太短 3. Broker负载或设置问题 | 1. 加强Wi-Fi信号,或引入Mesh网络。 2. 在 PubSubClient中设置client.setKeepAlive(60)。3. 检查Broker日志,确保未达到连接数限制。 |
| 蓝牙扫描不到设备 | 1. 设备蓝牙未打开或不可被发现 2. 扫描时间太短 3. ESP32蓝牙天线问题 | 1. 确认手机等设备的蓝牙已开启并处于可发现状态(有些手机锁屏后广播会停止)。 2. 增加 pBLEScan->start()的扫描时长。3. 尝试不同的ESP32开发板,有些板载天线设计不佳。 |
| 灯光频繁误开关 | 1. RSSI阈值设置不当 2. 光照阈值设置不当 3. 数据波动大,无滤波 | 1. 重新校准RSSI阈值,可能需要为每个房间单独设置。 2. 调整 LIGHT_THRESHOLD,白天测试找到合适的值。3. 在代码中加入RSSI和光照度的滤波算法(如滑动平均)。 |
| 控制命令延迟高 | 1. MQTT Broker或网络延迟 2. ESP32主循环阻塞 3. Wi-Fi信号差 | 1. 将Broker部署在性能更好的设备上。 2. 检查主节点 loop()中是否有耗时操作(如长延时),改用非阻塞定时。3. 改善网络环境,使用Mesh。 |
| 系统功耗过高 | 1. Wi-Fi常开 2. 蓝牙扫描过于频繁 | 1. 对于电池供电节点,考虑深度睡眠(Deep Sleep)模式,定时唤醒采集数据并发送,然后继续睡眠。 2. 调整蓝牙扫描间隔,从10秒延长到30秒或更长。 |
6.2 实战心得与进阶建议
- 从简单开始,逐步迭代:不要试图一开始就做全屋多房间的复杂系统。先实现一个房间的“有人+暗光->开灯”基本功能。用一个ESP32同时做扫描、感光和控灯。把这个最小可行产品(MVP)调通、调稳,理解整个数据流和控制逻辑。
- 参数没有银弹,必须现场调试:
RSSI_THRESHOLD和LIGHT_THRESHOLD这两个关键参数,网上抄来的值基本没用。必须在你的实际部署环境中,拿着设备在不同位置、不同光照条件下反复测试、记录、调整,才能找到最适合你家的值。 - 引入状态机,让逻辑更智能:简单的“与”逻辑在边界情况下会尴尬。比如,人从明亮的走廊走进昏暗的客厅,可能因为客厅光感还没触发阈值而延迟开灯。可以引入状态机,增加“预备开灯”、“延时关灯”等状态,让控制更平滑。例如,检测到人进入房间(RSSI变强),即使当前光照尚可,也进入“预备”状态,如果人在一段时间内未离开且光照变暗,则立即开灯。
- 考虑备用方案和降级体验:任何自动化系统都可能故障。确保每个受控的灯具仍然保留物理开关功能。可以在主节点逻辑中加入“手动模式”,当检测到系统异常(如多个传感器失联)时,自动切换到简单的定时或光控模式,保证基础功能可用。
- 隐私与安全:虽然数据在本地,但仍需注意。MQTT Broker建议设置用户名密码。如果担心蓝牙MAC地址被扫描,可以为家人配置专用的、低功耗的防丢器,而不是用手机。定期检查系统日志,看看是否有未知设备频繁出现在扫描列表中。
这个项目就像搭积木,核心模块(蓝牙扫描、光感、MQTT通信、继电器控制)都是通用的。当你把基础打牢后,可以轻松地扩展功能,比如加入温湿度传感器自动控制空调、通过红外学习模块控制电视、甚至将数据接入Home Assistant这样的开源家庭自动化平台进行更复杂的联动和可视化。最重要的是,你获得了一个完全自主可控、贴合自己生活习惯的智能家居核心,这种成就感和定制化的体验,是购买任何成品都无法比拟的。