基于BLE信号强度的寻物游戏:用CircuitPython实现无线接近探测
1. 项目概述一个用蓝牙信号“捉迷藏”的硬件游戏几年前我第一次接触Adafruit的Circuit Playground系列开发板时就被它那种“开箱即玩”的理念吸引了。它把LED、按钮、传感器都集成在一块板子上让你不用焊接就能快速验证想法。后来出的Circuit Playground Bluefruit后面简称CPB更是加入了蓝牙低功耗BLE这让它的玩法一下子从简单的桌面实验扩展到了无线互联的世界。这次要聊的项目就是一个特别能体现这种“快速原型”精神的例子一个基于BLE信号强度的寻物游戏。想象一下你把几个闪烁着不同颜色的小装置藏在家里的各个角落然后拿着另一个装置在屋里走动它上面的LED灯会随着你靠近某个隐藏点而逐渐亮起并显示出对应的颜色。这本质上是一个利用蓝牙信号强度RSSI来粗略判断距离和方向的接近探测器但包装成了一个有趣的“寻宝”游戏。这个项目的核心价值在于它用非常直观的方式把BLE广播、扫描、RSSI测量、状态可视化这几个物联网和嵌入式开发中的关键概念串联了起来。你不需要复杂的数学公式或昂贵的定位基站只用几块百元级的开发板就能亲手搭建并理解一套近场感知系统。无论是用于教学演示还是作为某个智能家居项目的传感器原型都非常有启发性。接下来我会带你从硬件选型到代码细节完整地拆解这个项目并分享我在实现过程中积累的一些实操心得和避坑指南。2. 核心硬件与工具链解析2.1 为什么选择Circuit Playground Bluefruit在开始动手前搞清楚我们手中“武器”的特性至关重要。Adafruit Circuit Playground Bluefruit是这个项目的绝对核心它的选型直接决定了项目的可行性和难易度。首先集成度是关键。CPB在一块圆形的板子上集成了我们所需的所有基础元件10个可编程的RGB NeoPixel LED、两个物理按钮A和B、一个滑动开关、一个蜂鸣器、多种运动和环境传感器以及最重要的——一颗支持BLE的Nordic nRF52840芯片。这意味着我们不需要为了连接LED、读取开关状态或进行蓝牙通信而去额外焊接任何电阻、电容或模块。对于快速原型和游戏化应用来说这种“All-in-One”的设计极大地降低了入门门槛和出错概率。其次CircuitPython的加持。CPB原生支持CircuitPython这是Adafruit主导的一个基于Python 3的嵌入式编程子集。与传统的嵌入式C开发相比Python语法更友好交互性更强支持REPL实时交互库管理也更简单通常就是往板子的存储盘里拖放.py或.mpy文件。对于这个项目我们需要用到adafruit_circuitplayground库来轻松控制板载硬件以及adafruit_ble库来处理蓝牙通信。在CircuitPython环境下导入这些库并调用其函数就像在电脑上写Python脚本一样直观。最后供电与便携性。项目中使用的是3.7V 350mAh的锂聚合物电池。选择这种电池的原因有三一是电压与CPB的输入要求完美匹配二是其扁平的形状和短导线易于整合到后续的装饰球外壳中三是350mAh的容量对于这种间歇性广播/扫描的应用来说可以提供数小时的续航足够完成一场游戏。这里有个细节务必使用数据线而不仅仅是充电线连接电脑和CPB因为我们需要通过USB向板子传输代码和库文件。很多新手卡在第一步就是因为用了只能充电的线导致电脑无法识别出CPLAYBTBOOT或CIRCUITPY磁盘。注意市面上有一些外观类似的“充电宝专用线”其内部只有电源线没有数据传输线。一个简单的判断方法是用这根线连接手机和电脑看是否能传输照片。如果不能那它很可能就是一条“坑爹”的充电线。2.2 项目物料清单与备选方案原项目清单给出了一个“理想配置”但根据我的经验完全可以更灵活。核心必需清单Circuit Playground Bluefruit主板至少需要2块一块作为探测器Scanner一块作为被寻找的目标Advertiser。若要玩多颜色寻物则需要3块或以上。这是无法替代的核心。锂聚合物电池3.7V每块主板配一块。容量350mAh是甜点如果手头有500mAh或更大的只要尺寸放得下续航会更久。关键是接口必须是JST PH 2.0与CPB板上的电池端口匹配。Micro USB数据线用于编程。一块板子配一条即可可以轮流使用。外壳与装饰可选但推荐透明防护壳Adafruit官方的透明卡扣壳能很好地保护主板上的元器件防止短路也显得更规整。装饰球套件6厘米直径的DIY装饰球。它的作用主要是美观和扩散光线让NeoPixel的光效更柔和、范围更大。如果没有用半透明的塑料蛋、甚至白色的纸杯罩住也能达到类似效果。备选与扩展思路电池替代如果只是桌面调试完全可以不用电池直接通过USB线供电。但这样就失去了“无线”和“移动”的灵魂。外壳替代任何能容纳主板和电池、且不影响LED发光和信号传输的容器都可以。我曾用过带盖的透明塑料方盒效果也不错。功能扩展CPB板载的加速度计和声音传感器没有被原项目利用。你可以思考能否通过晃动探测器来增强扫描功率或者当靠近目标时让蜂鸣器发出提示音这些都为项目留下了自定义空间。3. 软件环境搭建与项目部署3.1 固件刷写让CPB说Python“方言”拿到全新的CPB板第一步是让它运行CircuitPython。这个过程叫刷写固件听起来高级其实非常简单可以理解为给板子安装一个专用的操作系统。详细步骤与原理获取固件访问CircuitPython官网找到“Circuit Playground Bluefruit”的页面下载最新的.uf2固件文件。这个.uf2格式是Adafruit和微软共同推广的一种便捷固件格式特点就是“拖放式”更新。进入引导加载模式用数据线连接CPB和电脑。然后快速双击板子中央的复位Reset按钮。成功的标志是板子上的10颗NeoPixel先全部变红然后全部变绿。此时电脑上会出现一个名为CPLAYBTBOOT的U盘。实操心得这个“双击”的手感需要练习。要点是“快速连续两次点击”。如果第一次没成功比如LED只变红不变绿或者CPLAYBTBOOT盘没出现别慌多试几次。也可以尝试单击复位键。这取决于板子当前的状态。拖放固件将下载好的adafruit-circuitpython-...uf2文件直接拖拽到CPLAYBTBOOT磁盘里。此时LED会变红CPLAYBTBOOT盘会消失。稍等几秒电脑上会出现一个新的磁盘名叫CIRCUITPY。恭喜这说明CircuitPython系统已经安装成功这个CIRCUITPY盘就是你以后放代码和库文件的地方。注意如果CIRCUITPY盘没有出现可能是USB口供电不足或数据线问题。尝试更换电脑USB口优先使用机箱后置的USB口供电更稳或更换一条确认可传输数据的数据线。3.2 代码编辑器选择与库文件管理系统有了我们需要工具来写代码。Adafruit推荐Mu Editor这是一个对初学者极其友好的Python编辑器。为什么是MuMu最大的优点是与CircuitPython板子深度集成。它有一个专用的“CircuitPython”模式点击一个按钮就能将当前编辑的代码保存到CIRCUITPY盘并重命名为code.pyCPB上电后自动运行的主程序文件。它还有一个“串行”面板可以直接显示板子通过print()语句输出的调试信息这对于查看蓝牙扫描结果、信号强度数值至关重要。安装库文件的正确姿势这个项目依赖两个外部库adafruit_circuitplayground和adafruit_ble。在桌面Python中我们用pip安装在CircuitPython中则是将编译好的库文件直接复制到板子中。最不容易出错的方法是使用“项目捆绑包”Project Bundle。在原项目页面通常会提供一个“Download Project Bundle”按钮。点击后会下载一个zip文件里面已经包含了所有必要的库文件和code.py主程序。你只需要解压这个zip文件。找到解压后目录中对应你CircuitPython版本的文件夹例如cpb_ornament_proximity/下的某个子文件夹。将该文件夹内的所有内容包括lib文件夹和code.py复制到CIRCUITPY磁盘的根目录。如果提示覆盖选择“是”。这样库文件就被正确放置在了CIRCUITPY/lib/目录下主程序code.py也准备就绪。此时拔掉USB线用电池给CPB供电程序就应该开始运行了。手动管理库的备选方案如果项目没有提供捆绑包或者你需要更新某个库就需要手动操作。去Adafruit的CircuitPython库合集页面找到对应的库如adafruit-circuitpython-ble下载最新的.mpy文件这是为嵌入式环境优化的MicroPython字节码文件然后将其放入CIRCUITPY/lib/目录中。务必确保库文件的版本与你的CircuitPython固件版本兼容。4. 代码深度剖析与工作原理现在我们进入最核心的部分——解读代码。理解每一行代码在做什么以及为什么要这么做是你能进行自定义修改和故障排除的基础。4.1 程序骨架与蓝牙通信模型让我们先俯瞰全局。这个程序的核心是一个无限循环while True:循环内通过读取滑动开关的状态来决定板子处于两种模式中的哪一种开关向左cpb.switch为True广播模式。板子将自己设定好的颜色通过BLE广播包持续发送出去。同时按A/B键可以循环切换要广播的颜色。开关向右cpb.switch为False扫描模式。板子持续扫描周围环境寻找正在广播颜色的BLE设备并找出其中信号最强的一个用它的颜色和信号强度来控制自己的LED。这是一种典型的主从式或观察者模式的无线通信模型。广播者隐藏的物件只管“喊”出自己的身份颜色不关心谁在听扫描者探测器则负责“听”并从所有“喊声”中找出最大的那一个做出反应。这种模型功耗低实现简单非常适合这种一对多、单向数据流的场景。4.2 广播模式如何“喊”出自己的颜色当开关拨到左边板子进入广播模式。这部分代码的关键在于理解BLE广告Advertising机制。if cpb.switch: print(Broadcasting color) ble.start_advertising(advertisement) while cpb.switch: # ... 处理按钮切换颜色 ... time.sleep(0.5) ble.stop_advertising() ble.start_advertising(advertisement) ble.stop_advertising()创建广告内容程序一开始就创建了一个AdafruitColor()广告对象并赋予它一个来自color_options列表的十六进制颜色值。这个自定义的广告类型是Adafruit定义好的它允许我们在标准的BLE广播包中携带一个颜色数据。开始广播ble.start_advertising(advertisement)这行代码就是让板子开始以低功耗周期性地向外发送这个包含颜色数据的广播包。动态更新颜色在一个while循环中程序不断检测A/B按钮。如果按下就更新颜色索引i并用新颜色填充NeoPixel。关键一步它需要更新advertisement.color的值并重启广播先stop再start才能使新的颜色信息被发送出去。广播间隔time.sleep(0.5)设置了每0.5秒更新并重启一次广播。这个间隔是功耗和响应速度的权衡。间隔越短探测器感知变化越快但广播者耗电也越快。实操心得color_options列表中的颜色值是24位的十六进制RGB颜色格式为0xRRGGBB。但你会发现列表中的值像0x110000暗红色、0x001100暗绿色亮度都很低。这是有意为之的因为CPB的NeoPixel在最高亮度下非常耗电。使用低亮度颜色可以显著延长电池续航。如果你想自定义更鲜艳的颜色可以尝试0xFF0000亮红、0x00FF00亮绿但要做好续航缩短的心理准备。4.3 扫描模式如何“听”并找到最强信号当开关拨到右边板子变身为探测器。这是整个项目的算法核心涉及到BLE扫描和RSSI处理。else: closest None closest_rssi -80 closest_last_time 0 print(Scanning for colors) while not cpb.switch: for entry in ble.start_scan(AdafruitColor, minimum_rssi-100, timeout1): # ... 信号处理逻辑 ... # ... 超时清除逻辑 ...初始化与扫描closest用于记录最近设备的地址closest_rssi记录其信号强度初始化为-80一个相对较弱的阈值closest_last_time记录最后一次听到它的时间。ble.start_scan()开始扫描它只关注类型为AdafruitColor的广播包忽略信号强度低于-100 dBm的过滤掉极弱的干扰每次扫描持续1秒。信号强度筛选逻辑这是代码中最精妙的部分。对于扫描到的每一个设备entryif entry.address closest:如果这个设备就是当前记录的“最近设备”什么也不做继续。elif entry.rssi closest_rssi or now - closest_last_time 0.4:这是核心判断条件。满足以下任一条件就更新“最近设备” a. 新设备的RSSI比当前记录的closest_rssi更强更大。 b. 距离上次听到当前“最近设备”已经过去了0.4秒以上。条件a很好理解谁信号强谁就是老大。条件b是防丢机制。无线信号会有波动可能某一瞬间当前“最近设备”的信号突然变弱比如被遮挡导致被其他设备超越。0.4秒的超时设定给了“老大哥”一个短暂的宽容期避免因信号瞬时抖动而频繁切换目标使显示效果更稳定。可视化映射找到最强信号后需要将其映射到10个NeoPixel上。discrete_strength min((100 entry.rssi) // 5, 10)这行公式是信号到灯光的转换器。RSSI是负值越接近0表示信号越强。假设entry.rssi -50那么(100 (-50)) // 5 50 // 5 10会点亮全部10颗灯。如果entry.rssi -80则(100-80)//520//54只点亮4颗灯。min(..., 10)确保结果不超过灯的总数。这个公式将大约-50 dBm到-100 dBm的信号范围线性映射到了1-10盏灯上。显示与超时清除程序用entry.color填充指定数量的LED。最后还有一个清除逻辑如果超过1秒没听到任何“最近设备”的信号就熄灭所有LED防止显示过时的信息。5. 项目组装、调试与游戏化实践5.1 硬件组装与上电测试组装过程本身很简单但顺序和测试能避免后续麻烦。推荐组装流程先编程后装壳在将CPB板装入透明外壳前先通过USB线连接电脑完成固件刷写、库文件复制和代码上传。并在Mu编辑器的串行控制台中观察输出确保两种模式广播/扫描的打印信息正常。这比装好壳子后发现有问题再拆开要方便得多。装入外壳将测试好的CPB板卡入透明防护壳。注意对齐USB口和按钮的位置。连接电池将锂电池的JST插头插入CPB板的电池端口。此时即使USB线还连着板子也可能自动切换到电池供电。你会看到NeoPixel按程序设定亮起。整体放入装饰球将板、壳、电池的整个组合体小心地放入装饰球中。注意将电池的导线理顺避免挤压。如果使用其他容器确保其材质不会严重屏蔽2.4GHz的蓝牙信号金属壳是绝对不行的厚实的混凝土或大量液体也会衰减信号。上电测试要点拨动开关观察板载的红色LED位于USB口旁边。在广播模式下它应该会间歇性快速闪烁表示正在发送数据。在扫描模式下闪烁模式会有所不同或常亮表示在监听。准备至少两块板子。一块设为广播模式开关左并按下A键切换一个颜色比如红色。另一块设为扫描模式开关右。将两者靠近扫描板上的LED应该亮起红色并且随着两者距离变近点亮的LED数量增多。5.2 信号稳定性优化与故障排查在实际环境中蓝牙信号会受到各种干扰导致RSSI值跳动反映在灯效上就是灯光数量闪烁、颜色偶尔跳变。以下是几个优化和排查的方向1. 环境干扰排查Wi-Fi干扰Wi-Fi和蓝牙都工作在2.4GHz频段。如果游戏区域有很强的Wi-Fi信号特别是多个路由器可能会影响蓝牙通信。可以尝试关闭不必要的Wi-Fi设备或者将游戏移到离路由器稍远的位置。金属物体与人体金属会反射和屏蔽信号人体主要是水分也会吸收2.4GHz信号。避免将隐藏的装饰球放在金属书架后面或者握持探测器时用手完全包裹住板子。2. 代码参数微调如果信号跳动太频繁可以尝试修改代码中的参数牺牲一点响应速度来换取稳定性。放宽“防丢”超时将扫描模式中判断更新的条件now - closest_last_time 0.4里的0.4改得更大比如0.8。这样当前“最近设备”的头衔能保持更久不易被瞬时干扰抢走。调整RSSI阈值minimum_rssi-100这个值决定了多弱的信号才会被忽略。如果你的环境背景噪声大可以提高到-95或-90过滤掉更多弱信号干扰。反之如果你想探测更远的距离可以降低到-105但要小心噪声引入。平滑处理RSSI更高级的做法是引入一个简单的滑动平均滤波器。例如记录最近3次对同一设备的RSSI测量值然后取平均值用于显示。这能有效平滑掉信号的毛刺。3. 常见问题速查表问题现象可能原因排查步骤电脑无法识别CPLAYBTBOOT或CIRCUITPY盘1. USB线是充电线2. USB口供电不足3. 驱动问题Windows1. 更换确认可传数据的数据线2. 换到电脑机箱后置USB口3. 尝试双击复位键多次程序上传后板子无任何反应灯不亮1. 主程序文件名不是code.py2. 库文件缺失或版本不对3. 电池没电或未连接1. 检查CIRCUITPY根目录下是否有code.py2. 检查lib文件夹内是否有adafruit_circuitplayground和adafruit_ble库3. 插上USB线测试或给电池充电扫描板检测不到广播板1. 两者距离太远或有严重遮挡2. 其中一块板子模式不对3. 电池电量过低1. 将两者靠近至1米内无遮挡测试2. 确认一块开关向左广播一块向右扫描3. 插上USB线供电测试信号时有时无灯光闪烁剧烈1. 环境无线干扰强2. 电池接触不良3. 代码扫描参数过于敏感1. 更换场地测试2. 检查电池JST接头是否插紧3. 尝试调整代码中的超时参数如将0.4改为0.8可以检测到但颜色不对广播板的颜色没有成功更新或发送在广播模式下按A/B键后观察其自身NeoPixel颜色是否变化。确保按按钮时操作到位。5.3 游戏玩法设计与扩展思路基础功能调通后就可以设计游戏了。原方案是“寻找隐藏的礼物”但我们可以玩出更多花样。经典寻宝模式准备3个以上设为广播模式的CPB分别设置成不同的颜色如红、绿、蓝并藏在家中的不同位置。将1个CPB设为扫描模式作为探测器。玩家手持探测器根据LED点亮的数量和颜色判断最近的是哪个颜色的目标并朝信号增强的方向寻找。进阶规则可以为每种颜色设定不同的积分或者要求按特定顺序找到所有目标。扩展玩法创意团队竞技制作多个探测器多人同时开始寻找看谁先找齐所有颜色目标。密室逃脱触发器将隐藏的CPB作为密室中的“钥匙”。玩家需要找到它并拿到探测器附近当信号足够强例如点亮8盏灯以上时触发另一个机关如通过串口控制电脑播放一段音频或视频。融入传感器利用CPB板载的传感器丰富交互。例如修改代码让广播模式下的板子只有在被拍打加速度计检测到敲击时才开始广播增加了隐藏和发现的随机性。或者让探测器在靠近目标时根据距离播放不同音调使用板载蜂鸣器。数据记录与分析稍微修改扫描板的代码将其接收到的RSSI值和颜色通过串口实时发送到电脑用Python脚本记录并绘制出信号强度随时间/位置变化的曲线。这立刻就把一个游戏变成了一个简单的无线信号传播实验教具。这个项目的魅力在于它用一个具体的、好玩的实例把BLE通信、信号处理、嵌入式编程和交互设计都串了起来。从“它能动”到“它好玩”再到“我还能让它做什么”每一步的探索都能带来新的收获。当你看到自己亲手组装的设备依靠看不见的无线电波在空间中互动时那种连接虚拟与现实的成就感正是嵌入式开发和物联网创作的乐趣所在。