1. 项目概述:当AR眼镜遇上卷尺
最近在捣鼓MetaQuest这类AR眼镜时,我一直在想,除了玩游戏和看视频,它能不能干点更“实在”的活儿?比如,我站在空荡荡的客厅里,想大概知道沙发墙到电视墙的距离,难道还得满屋子找卷尺?这个念头催生了“MeasureAR”这个项目。它的核心想法很简单:利用AR眼镜的空间感知能力,结合一个我自制的、通过蓝牙(BLE)连接的小硬件,实现一个能“隔空”测量现实世界物体尺寸的AR应用。你戴着眼镜,看向想测量的起点和终点,按下手里的蓝牙按钮,眼前的虚拟世界里就会实时显示出精确到厘米级的距离数据,就像把一把智能卷尺“装”进了AR眼镜里。
这个项目听起来酷,但背后是一套软硬结合的“组合拳”。它涉及AR应用开发(我用的是Unity)、嵌入式硬件编程(基于ESP32这类支持BLE的芯片,用PlatformIO开发),以及两者之间稳定、低延迟的无线通信协议。整个过程就像在搭建一座数字世界和物理世界之间的桥梁,你需要确保桥的两端(眼镜里的Unity应用和手里的硬件设备)能说同一种语言(BLE通信协议),并且能协同工作。无论你是对AR应用开发感兴趣的开发者,还是喜欢折腾物联网硬件的极客,甚至是室内设计师、装修工长这类需要频繁测量的专业人士,这个项目都能提供一个从想法到产品的完整实践路径。它不仅让你理解空间计算的基础,更能亲手打造一个真正有用的工具。
2. 核心思路与方案选型
2.1 为什么是AR+BLE,而不是纯视觉方案?
提到测量,很多人第一反应是手机上的AR测量App,它们利用手机的单目或双目摄像头,通过视觉算法估算尺寸。这类方案有两个主要问题:一是精度受环境光、纹理影响大,在空白墙面或弱光下很容易失效;二是需要用户举着手机对准,操作并不自然。
而我们的方案选择了“AR眼镜 + 外置蓝牙测距硬件”的路径。AR眼镜(如MetaQuest)提供了稳定、高精度的空间定位与姿态追踪(Inside-Out Tracking),它能持续构建环境地图并知道自身在空间中的精确位置和朝向。这解决了“测量基准”的问题——我们有一个稳定且已知的坐标系。剩下的,就是如何在这个坐标系中标记我们想要测量的“点”。
纯视觉方案让用户通过手势或注视点来标记,这在精度和易用性上都有挑战。我们的方案引入了一个实体蓝牙按钮,其核心优势在于:
- 物理反馈与操作精度 :按下实体按钮的触觉反馈是明确且即时的,避免了手势识别可能产生的误触发或延迟,让“标记”这个动作变得精准而可靠。
- 分离式设计 :测量时,用户的目光可以自由移动、观察,而标记动作由手持设备完成,实现了“看”与“操作”的分离,更符合人类自然的协作习惯。
- 功能可扩展性 :这个蓝牙硬件不只是一个按钮。我们可以轻易地为其增加其他传感器(如激光测距模组作为精度校准参考),或者设计成多个按钮用于不同功能(如切换测量模式、确认、取消等),扩展性远强于纯手势交互。
因此, BLE(蓝牙低功耗) 成为了连接AR眼镜(软件)与手持硬件(硬件)的完美桥梁。它功耗低、延迟可接受(对于非实时流媒体应用足够),且几乎所有移动设备(包括AR眼镜)和嵌入式硬件都原生支持,生态成熟。
2.2 技术栈深度解析:Unity、PlatformIO与BLE协议
Unity:不止于游戏的实时3D引擎
在这个项目中,Unity扮演着“AR世界构建者”和“应用大脑”的角色。我们利用Unity的AR Foundation框架(或Meta的OVR插件,针对Quest)来获取眼镜的位姿数据、构建空间锚点。Unity强大的3D数学库(如
Vector3
、
Quaternion
)让我们能轻松计算两点间的距离。更重要的是,Unity提供了完整的C#脚本环境,我们可以编写逻辑来处理BLE通信、管理测量状态、更新UI显示,并将结果实时渲染到AR视野中。选择Unity,意味着你可以复用海量的3D资产、UI系统以及成熟的开发工作流,快速构建出体验良好的应用界面。
PlatformIO:嵌入式开发的“瑞士军刀”
硬件端,我们选择使用ESP32系列芯片,因为它集成了Wi-Fi和BLE,性能强大且性价比极高。而PlatformIO是一个跨平台的嵌入式开发环境,它完美集成在VSCode中,提供了库依赖管理、代码构建、上传调试一站式服务。相比传统的Arduino IDE,PlatformIO支持更专业的项目结构、版本控制,并且能轻松管理复杂的第三方库(比如用于BLE通信的
NimBLE
库或
ESP32 BLE Arduino
库)。它解决了嵌入式开发中环境配置繁琐、库版本冲突等痛点,让你能更专注于业务逻辑。
BLE通信协议设计:服务(Service)与特征值(Characteristic) BLE通信不是简单的数据流,它基于GATT(通用属性协议)结构,是一种“服务-特征值”模型。我们需要为手持硬件定义专属的BLE服务。
-
服务(Service)
:可以理解为一个功能模块。我们创建一个自定义服务,例如UUID为
0xFFF0(自定义服务通常使用16位短UUID范围)。 -
特征值(Characteristic)
:服务下的具体数据点。我们至少需要两个:
-
按钮状态特征值(Notify)
:用于硬件向Unity应用主动“报告”按钮被按下。将其属性设置为
NOTIFY,当按钮按下时,硬件端会更新这个特征值,Unity端订阅后就能即时收到通知。 -
测量指令特征值(Write)
:用于Unity应用向硬件发送指令,比如“开始一次新的测量”、“请求电池电量”等。将其属性设置为
WRITE。
-
按钮状态特征值(Notify)
:用于硬件向Unity应用主动“报告”按钮被按下。将其属性设置为
这种设计实现了双向通信:硬件主动上报事件(按钮按下),软件主动发送控制命令。清晰的角色划分是稳定通信的基础。
3. 硬件端实现:从电路到固件
3.1 硬件选型与电路搭建
核心控制器我选择了 ESP32 DevKit C V4 ,它开发板形态完善,USB转串口芯片稳定,引脚引出方便。对于按钮,一个简单的常开型轻触开关即可。为了有更好的用户体验,我增加了一个WS2812B RGB LED灯珠,用于通过不同颜色指示硬件状态(如待机、连接成功、低电量等)。
电路连接非常简单:
- 轻触开关一端接ESP32的某个GPIO(如GPIO 0),另一端接地。在代码中将该GPIO设置为上拉输入模式,这样未按下时为高电平,按下时变为低电平。
- WS2812B的数据线接ESP32的另一个GPIO(如GPIO 2)。
- 使用一块小型锂电池(如500mAh)通过TP4056充电模块供电,确保设备无线便携。
注意 :ESP32的某些GPIO在启动时有特殊功能(如GPIO0影响启动模式),应避免使用。GPIO2通常比较“安全”,且多数开发板已连接LED,方便调试。
3.2 PlatformIO工程配置与BLE服务实现
首先,在VSCode中安装PlatformIO插件。新建项目时,选择Board为
Espressif ESP32 Dev Module
,框架选择
Arduino
,这样能利用丰富的Arduino生态库。
项目关键的
platformio.ini
配置文件需要包含必要的库依赖。对于BLE,我推荐使用
nimble-arduino
库,它比传统的
ESP32 BLE Arduino
库更轻量、内存占用更少。
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
fastled/FastLED @ ^3.6.0
h2zero/NimBLE-Arduino @ ^1.4.1
接下来是固件代码的核心——BLE服务设置。我们在
setup()
函数中初始化BLE,并创建自定义服务。
#include <NimBLEDevice.h>
#include <FastLED.h>
#define BUTTON_PIN 0
#define LED_PIN 2
#define NUM_LEDS 1
CRGB leds[NUM_LEDS];
BLECharacteristic* pButtonCharacteristic;
bool bleConnected = false;
class MyServerCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
bleConnected = true;
leds[0] = CRGB::Green; // 连接成功,亮绿灯
FastLED.show();
}
void onDisconnect(BLEServer* pServer) {
bleConnected = false;
leds[0] = CRGB::Red; // 断开连接,亮红灯
FastLED.show();
NimBLEDevice::startAdvertising(); // 重新开始广播,等待连接
}
};
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
leds[0] = CRGB::Blue;
FastLED.show();
// 初始化BLE
NimBLEDevice::init("MeasureAR_Remote");
BLEServer *pServer = NimBLEDevice::createServer();
pServer->setCallbacks(new MyServerCallbacks());
// 创建服务
BLEService *pService = pServer->createService("FFF0");
// 创建“按钮状态”特征值,支持通知
pButtonCharacteristic = pService->createCharacteristic(
"FFF1",
NIMBLE_PROPERTY::NOTIFY
);
// 创建“指令”特征值,支持写操作
BLECharacteristic *pCommandCharacteristic = pService->createCharacteristic(
"FFF2",
NIMBLE_PROPERTY::WRITE
);
pService->start();
BLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
pAdvertising->addServiceUUID(pService->getUUID());
pAdvertising->start();
Serial.println("BLE设备已启动,等待连接...");
}
void loop() {
// 检测按钮按下(下降沿触发)
static int lastButtonState = HIGH;
int currentButtonState = digitalRead(BUTTON_PIN);
if (lastButtonState == HIGH && currentButtonState == LOW) {
// 按钮被按下
if (bleConnected) {
uint8_t buttonPressed = 0x01;
pButtonCharacteristic->setValue(&buttonPressed, 1);
pButtonCharacteristic->notify();
Serial.println("按钮按下事件已发送");
// 添加一个短暂的LED闪烁作为本地反馈
leds[0] = CRGB::White;
FastLED.show();
delay(50);
leds[0] = bleConnected ? CRGB::Green : CRGB::Red;
FastLED.show();
}
delay(50); // 简单防抖
}
lastButtonState = currentButtonState;
}
这段代码实现了:BLE初始化、自定义服务与特征值创建、连接状态管理,以及按钮状态检测与通知发送。当按钮被按下且BLE已连接时,它会通过
FFF1
特征值发送一个值为
0x01
的通知。
3.3 功耗优化与稳定性设计
对于电池供电的设备,功耗是关键。ESP32在BLE广播状态下功耗约为几十到一百微安,连接状态下约几百微安,已经很低。但我们还可以优化:
- 深度睡眠唤醒 :如果不需常连,可以设计为按下按钮后,ESP32才从深度睡眠中唤醒,启动BLE并快速连接、发送数据,然后再次进入睡眠。这能将待机功耗降至个位数微安。
- 连接参数协商 :在BLE连接建立时,主机(Unity应用)和从机(ESP32)可以协商连接间隔(Connection Interval)。适当增大间隔(如100ms)可以降低功耗,但会增加通知延迟。需要根据实际体验权衡。
-
看门狗与异常重启
:在
loop()函数中定期喂硬件看门狗,防止程序跑飞导致设备“死机”。这对于需要长期稳定工作的设备至关重要。
4. Unity端应用开发:连接、定位与渲染
4.1 Unity项目设置与BLE插件集成
在Unity中新建一个3D项目(或URP项目)。由于Unity本身没有官方的BLE API,我们需要借助第三方插件。对于跨平台兼容性(尤其是考虑到未来可能移植到iOS/Android),我推荐使用**
Unity BLE
** 或
Nearby Connections
的Unity封装包。这里以一款常用的
BLE Plugin for Unity
为例。
将插件包导入项目后,通常需要在
Player Settings
中为目标平台(如Android,因为Meta Quest基于Android)启用相应的蓝牙权限。对于Quest开发,还需要导入Meta的OVR(Oculus Integration)包来获取AR功能。
核心的Unity脚本(如
BLEController.cs
)需要负责:
- 扫描并发现名为“MeasureAR_Remote”的设备。
-
连接到该设备,并找到UUID为
FFF0的服务及其下的特征值(FFF1和FFF2)。 -
订阅
FFF1特征值的通知事件。 -
实现向
FFF2特征值写入指令的方法。
4.2 AR空间定位与测量逻辑实现
测量逻辑的核心是获取AR眼镜在按下按钮瞬间的空间位置。我们使用AR Foundation的
ARAnchorManager
或OVR插件的
OVRCameraRig
来获取当前相机(即用户视角)的位置和旋转。
在Unity中创建一个
MeasurementManager.cs
脚本:
using UnityEngine;
using UnityEngine.XR.ARFoundation; // 如果使用AR Foundation
public class MeasurementManager : MonoBehaviour
{
public ARCameraManager cameraManager; // AR Foundation
// 或 public Transform centerEyeAnchor; // OVR
public LineRenderer lineRenderer; // 用于绘制测量线
public TextMeshPro distanceText; // 用于显示距离
public Transform textAnchor; // 文本的锚点
private Vector3? firstPoint = null;
private Vector3? secondPoint = null;
private BLEController bleController;
void Start()
{
bleController = FindObjectOfType<BLEController>();
bleController.OnButtonPressed += HandleButtonPress;
lineRenderer.positionCount = 0;
}
void HandleButtonPress()
{
// 获取当前相机在世界空间中的位置
Vector3 currentPosition = GetCurrentCameraPosition();
if (!firstPoint.HasValue)
{
// 第一次按下,记录第一个点
firstPoint = currentPosition;
SpawnMarker(firstPoint.Value); // 在第一个点生成一个视觉标记(如小球)
}
else if (!secondPoint.HasValue)
{
// 第二次按下,记录第二个点并计算距离
secondPoint = currentPosition;
SpawnMarker(secondPoint.Value);
CalculateAndDisplayDistance();
}
else
{
// 第三次按下,重置测量
ResetMeasurement();
}
}
Vector3 GetCurrentCameraPosition()
{
// 使用AR Foundation
if (cameraManager != null && cameraManager.GetComponent<Camera>() != null)
{
return cameraManager.GetComponent<Camera>().transform.position;
}
// 使用OVR
// else if (centerEyeAnchor != null) { return centerEyeAnchor.position; }
return Vector3.zero;
}
void CalculateAndDisplayDistance()
{
if (firstPoint.HasValue && secondPoint.HasValue)
{
float distance = Vector3.Distance(firstPoint.Value, secondPoint.Value);
distanceText.text = $"距离: {distance:F2}米";
// 更新LineRenderer
lineRenderer.positionCount = 2;
lineRenderer.SetPosition(0, firstPoint.Value);
lineRenderer.SetPosition(1, secondPoint.Value);
// 将文本放置在两点中间,并使其面向相机
textAnchor.position = (firstPoint.Value + secondPoint.Value) / 2;
textAnchor.LookAt(GetCurrentCameraPosition());
textAnchor.Rotate(0, 180, 0); // 让文字正面朝向相机
}
}
void ResetMeasurement()
{
firstPoint = null;
secondPoint = null;
lineRenderer.positionCount = 0;
distanceText.text = "";
// 清除所有视觉标记
}
}
这个脚本构成了应用的业务核心:响应BLE按钮事件,记录空间点,计算并可视化距离。
4.3 用户界面与交互优化
一个直观的UI至关重要。在AR中,UI通常以“世界空间”Canvas的形式呈现。
- 状态提示 :在Canvas上显示当前连接状态(“正在搜索设备”、“已连接”、“已断开”)、测量状态(“请标记起点”、“请标记终点”、“距离:X.XX米”)。
-
视觉反馈
:除了
LineRenderer画线,当标记一个点时,可以在该位置实例化一个半透明的3D球体或一个自定义图标,让用户明确知道点已记录。 -
测量管理
:实现一个列表,记录多次测量的结果,允许用户删除或重命名某次测量。这需要将每次测量的起点、终点、距离、时间戳序列化保存(如使用
JsonUtility)。 - 单位切换 :提供米/厘米/英尺/英寸的切换功能,满足不同场景需求。
- 校准功能(进阶) :由于AR眼镜的定位存在微小漂移,可以提供一个校准功能。例如,测量一个已知长度的物体(如一张A4纸的长边0.297米),然后让用户输入实际值,应用计算出一个比例因子来修正后续所有测量结果,可以显著提升绝对精度。
5. 系统联调与实战问题排查
5.1 连接与配对流程实战
将硬件烧录好固件,Unity应用打包安装到Meta Quest上后,真正的挑战才开始。第一次连接往往不会一帆风顺。
标准流程应该是:
- 打开硬件电源,LED亮起(比如蓝色广播状态)。
- 在Quest中启动MeasureAR应用。
- 应用启动后,自动开始BLE扫描,并在UI上显示“正在扫描设备...”。
- 扫描到“MeasureAR_Remote”后,自动发起连接。
- 连接成功后,硬件LED变为绿色,应用UI显示“已连接”。
- 按下硬件按钮,应用界面出现第一个标记点。
常见问题与排查:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 应用扫描不到设备 |
1. 硬件未启动或BLE未初始化。
2. 硬件广播名或服务UUID不匹配。 3. Quest蓝牙未开启或权限未授权。 |
1. 检查硬件串口日志,确认
“BLE设备已启动”
信息出现。
2. 用手机BLE扫描App(如
nRF Connect
)确认设备可被发现,核对名称和UUID。
3. 确认Quest系统设置中蓝牙已开,并检查Unity Player Settings中Android权限(
BLUETOOTH
,
BLUETOOTH_ADMIN
,
ACCESS_FINE_LOCATION
)均已勾选。
|
| 连接频繁断开 |
1. BLE信号弱或受干扰。
2. 硬件或应用端BLE事件处理阻塞。 3. 连接参数不合理。 |
1. 确保设备在近距离(10米内无障碍)使用。
2. 检查硬件
loop()
中是否有长时间
delay()
阻塞了BLE栈任务;检查Unity主线程是否因复杂运算卡顿。
3. 尝试在Unity连接后,向硬件发送指令以协商更稳定的连接参数(如最小间隔16ms,最大间隔32ms,从机延迟0)。 |
| 按钮按下无反应 |
1. 连接未真正成功。
2. 特征值通知未正确订阅。 3. 硬件按钮检测逻辑有误或引脚接触不良。 |
1. 确认双方连接回调都已触发,且Unity端已找到目标特征值。
2. 在Unity端打印日志,确认收到特征值通知事件。 3. 通过硬件串口监视,确认每次按下按钮都有
“按钮按下事件已发送”
日志输出。检查按钮焊接和上拉电阻。
|
| 测量位置漂移 |
1. AR眼镜自身定位漂移。
2. 获取相机位置的方式错误或时机不对。 |
1. 这是AR通病,确保测量环境光照充足、纹理丰富。避免在光滑单色墙面或昏暗处使用。
2. 确保
GetCurrentCameraPosition
方法获取的是
ARCamera
或
CenterEyeAnchor
的
position
,而不是其子物体的位置。按钮事件处理函数应尽快获取位置,避免延迟。
|
5.2 精度校准与提升技巧
AR眼镜的定位是相对的,且存在累积误差。虽然测量相对距离(如墙的宽度)比较准,但绝对精度(比如测出 exactly 3.50米)需要校准。
- 软件校准法 :如前所述,测量已知长度物体进行比例修正。这是最简单有效的方法。
- 硬件辅助校准(进阶) :可以在手持硬件上集成一个激光测距模块(如TOF传感器)。测量时,先用激光测距获取一个高精度的基准距离,同时用AR系统测量同一点对。通过多次采样,可以反向标定出AR系统的尺度因子和初始偏移,大幅提升绝对精度。这相当于用高精度传感器给AR系统“上课”。
5.3 性能优化与体验打磨
-
渲染优化
:
LineRenderer和实时更新的UI(TextMeshPro)是性能消耗点。确保LineRenderer的material是简单的无光照Shader,TextMeshPro字体图集不要包含过多字符。对于多次测量记录,可以考虑使用对象池管理标记点物体。 -
电量管理
:在Unity应用中,当检测到Quest进入待机或应用失去焦点时,应主动断开BLE连接,并在恢复时重连。这需要监听
Application的OnApplicationPause事件。 -
异常处理
:网络通信必须健壮。在Unity的BLE通信代码中,所有异步操作(连接、读写、订阅)都要包裹在
try-catch块中,并在UI上给出友好的错误提示(如“连接超时,请重试”),而不是让应用崩溃或无响应。
6. 项目扩展与进阶方向
一个基础的MeasureAR已经完成,但它的潜力远不止于此。你可以根据自己的兴趣和需求,将它扩展得更加强大。
1. 多模式测量:
- 连续测量模式 :长按按钮,硬件持续发送信号,Unity端连续记录相机位置并实时绘制轨迹线,松开按钮时计算总长。适合测量不规则曲线或周长。
- 高度测量模式 :将第一个点标记在地面,第二个点标记在物体顶部,自动计算垂直高度。
- 面积测量模式 :标记多边形的各个顶点,自动计算闭合区域的面积。
2. 硬件升级:
- 集成IMU :在手持硬件中加入惯性测量单元(IMU,如MPU6050)。当按下按钮时,不仅发送按钮事件,同时发送硬件当前的姿态(通过BLE传输)。在Unity端,可以将这个姿态信息与AR眼镜的姿态进行融合。例如,当硬件水平放置时,可以模拟一个虚拟的水平仪或直角尺,实现更专业的辅助测量。
- 多按钮设计 :设计一个带有多个按键(如A、B、C)的壳体,不同按键触发不同测量模式或命令(开始、取消、保存),交互更丰富。
3. 数据持久化与分享:
- 测量记录保存 :将每次测量的坐标、距离、截图、时间戳保存为本地文件(如JSON或CSV格式)。
- 云端同步 :通过Quest的网络功能,将测量数据同步到云端数据库,方便在PC或其他设备上查看和管理。
- 导出与分享 :支持将测量结果(带AR场景截图)生成报告,或导出为CAD软件兼容的格式(如DXF点数据),直接用于后续设计。
4. 向数字孪生演进: 这是最具想象力的方向。通过多次测量,你可以获取一个房间关键点的三维坐标。结合Unity的建模能力,可以尝试自动或半自动地生成这个房间的简易3D白模(一个由墙面、门窗构成的低多边形模型)。这便是一个最轻量级的“数字孪生”起点。在此基础上,你可以虚拟摆放家具、规划管线,实现真正的AR空间设计与规划。
从一个小小的蓝牙按钮到一个空间测量工具,再到数字世界的入口,MeasureAR项目就像一把钥匙。它为你打开了混合现实(MR)应用开发的大门,让你亲身体验到软硬件协同、空间计算和实时交互的魅力。过程中踩过的每一个坑——无论是BLE连接的不稳定,还是AR坐标系的微妙漂移——都会让你对技术的理解更深一层。


被折叠的 条评论
为什么被折叠?



