HarmonyOS APP开发:蓝牙信标定位与iBeacon

举报
Jack20 发表于 2026/06/22 11:56:08 2026/06/22
【摘要】 HarmonyOS APP开发:蓝牙信标定位与iBeacon核心要点:本文深入讲解基于HarmonyOS的蓝牙信标(Bluetooth Beacon)定位技术,涵盖iBeacon协议解析、RSSI测距算法、三边定位实现,以及在HarmonyOS APP中的完整开发流程。通过ArkTS代码实战,构建一个可运行的室内蓝牙定位系统。项目说明| 开发语言 | ArkTS || 核心API | @o...

HarmonyOS APP开发:蓝牙信标定位与iBeacon

核心要点:本文深入讲解基于HarmonyOS的蓝牙信标(Bluetooth Beacon)定位技术,涵盖iBeacon协议解析、RSSI测距算法、三边定位实现,以及在HarmonyOS APP中的完整开发流程。通过ArkTS代码实战,构建一个可运行的室内蓝牙定位系统。

项目 说明

| 开发语言 | ArkTS |
| 核心API | @ohos.bluetooth.access、@ohos.bluetooth.ble |
| 定位精度 | 1-5米(典型场景) |
| 官方文档 | 蓝牙开发指导 |


一、背景与动机

1.1 室内定位的需求困境

GPS卫星定位在室外环境中精度可达米级,但在室内场景下,由于建筑物的遮挡与多径效应,GPS信号几乎完全失效。然而,据统计人类有80%以上的时间处于室内环境——商场导航、博物馆导览、医院就诊指引、仓储物流追踪、地下停车场寻车……这些场景对室内定位的需求极为迫切。

1.2 蓝牙信标定位的优势

在众多室内定位技术中,蓝牙信标(Bluetooth Beacon)因其独特优势脱颖而出:

特性 蓝牙信标 WiFi指纹 UWB RFID
部署成本
定位精度 1-5m 3-10m 0.1-0.3m 1-3m
功耗 极低
手机兼容性 广泛 广泛 有限 需专用设备
部署难度 简单 中等 复杂 中等

蓝牙信标定位的核心优势在于:低功耗、低成本、易部署、手机原生支持。一个iBeacon基站的成本仅数十元,一节纽扣电池可工作1-2年,且几乎所有现代智能手机都内置蓝牙模块。

1.3 iBeacon协议简介

iBeacon是Apple于2013年推出的基于蓝牙低功耗(BLE)的微定位协议。它通过广播特定的数据包,让接收设备(如手机)感知自身与信标之间的相对距离。iBeacon广播包包含三个核心参数:

  • UUID(128位):标识信标所属的组织或区域
  • Major(16位):标识信标组(如某楼层)
  • Minor(16位):标识具体信标(如某展位)
flowchart TB
    subgraph iBeacon["iBeacon广播数据结构"]
        direction TB
        A["Preamble<br/>1字节"] --> B["Access Address<br/>4字节"]
        B --> C["PDU Header<br/>2字节"]
        C --> D["PDU Payload<br/>最大37字节"]
        D --> E["CRC<br/>3字节"]
    end

    subgraph Payload["PDU Payload详细结构"]
        direction TB
        F["AdvA<br/>6字节<br/>广播设备地址"] --> G["AD Structure"]
        G --> H["Length<br/>1字节"]
        H --> I["Type: 0xFF<br/>厂商特定数据"]
        I --> J["Company ID: 0x004C<br/>Apple Inc."]
        J --> K["Beacon Type: 0x0215<br/>iBeacon标识"]
        K --> L["Data Length: 0x15<br/>21字节"]
        L --> M["UUID<br/>16字节"]
        M --> N["Major<br/>2字节"]
        N --> O["Minor<br/>2字节"]
        O --> P["TX Power<br/>1字节<br/>校准RSSI值"]
    end

    iBeacon -.-> Payload

    classDef default fill:#1a1a2e,stroke:#e94560,color:#eee,stroke-width:2px
    classDef highlight fill:#16213e,stroke:#0f3460,color:#eee,stroke-width:2px
    classDef accent fill:#0f3460,stroke:#e94560,color:#eee,stroke-width:2px

    class A,B,C,D,E default
    class F,G,H,I,J,K,L highlight
    class M,N,O,P accent

二、核心原理

2.1 RSSI测距模型

RSSI(Received Signal Strength Indicator)是接收信号强度指示,单位为dBm。RSSI值与距离之间存在对数衰减关系,这是蓝牙信标定位的物理基础。

对数距离路径损耗模型

RSSI(d)=RSSI(d0)10nlog10(dd0)+XσRSSI(d) = RSSI(d_0) - 10n \cdot \log_{10}\left(\frac{d}{d_0}\right) + X_\sigma

其中:

  • RSSI(d0)RSSI(d_0):参考距离d0d_0(通常1米)处的RSSI值
  • nn:路径损耗指数(室内通常2-4)
  • XσX_\sigma:高斯随机噪声,标准差σ\sigma通常4-10dB

由此可推导距离估计公式:

d=d010RSSI(d0)RSSI(d)10nd = d_0 \cdot 10^{\frac{RSSI(d_0) - RSSI(d)}{10n}}

2.2 三边定位算法

三边定位(Trilateration)是最基本的定位算法。已知至少3个信标的坐标及其到目标设备的距离,通过求解圆的交点来确定目标位置。

flowchart LR
    A["扫描iBeacon"] --> B["获取RSSI值"]
    B --> C["卡尔曼滤波<br/>平滑RSSI"]
    C --> D["路径损耗模型<br/>估算距离"]
    D --> E{"信标数量<br/>≥ 3?"}
    E ----> F["三边定位<br/>求解坐标"]
    E ----> G["最近邻法<br/>粗略定位"]
    F --> H["坐标输出<br/>(x, y)"]
    G --> H

    classDef process fill:#16213e,stroke:#0f3460,color:#eee,stroke-width:2px
    classDef decision fill:#0f3460,stroke:#e94560,color:#eee,stroke-width:2px
    classDef output fill:#1a1a2e,stroke:#e94560,color:#eee,stroke-width:2px
    classDef start fill:#533483,stroke:#e94560,color:#eee,stroke-width:2px

    class A start
    class B,C,D process
    class E decision
    class F,G process
    class H output

三边定位数学推导

设三个信标坐标为(x1,y1)(x_1, y_1)(x2,y2)(x_2, y_2)(x3,y3)(x_3, y_3),到目标距离为d1d_1d2d_2d3d_3,则:

{(xx1)2+(yy1)2=d12(xx2)2+(yy2)2=d22(xx3)2+(yy3)2=d32\begin{cases} (x - x_1)^2 + (y - y_1)^2 = d_1^2 \\ (x - x_2)^2 + (y - y_2)^2 = d_2^2 \\ (x - x_3)^2 + (y - y_3)^2 = d_3^2 \end{cases}

通过方程相减消去二次项,转化为线性方程组求解。

2.3 卡尔曼滤波平滑RSSI

RSSI值波动剧烈,直接用于测距误差很大。卡尔曼滤波(Kalman Filter)是一种递归最优估计算法,能有效平滑RSSI噪声。

状态方程xk=Axk1+wk1x_k = A \cdot x_{k-1} + w_{k-1}

观测方程zk=Hxk+vkz_k = H \cdot x_k + v_k

对于RSSI平滑,简化为:

  • 状态:xk=xk1x_k = x_{k-1}(RSSI短时间内近似恒定)
  • 观测:zk=xkz_k = x_k(直接测量RSSI)

三、代码实战

3.1 蓝牙权限声明与初始化

首先在module.json5中声明蓝牙权限:

// src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "requestPermissions": [
      {
        "name": "ohos.permission.ACCESS_BLUETOOTH",
        "reason": "$string:bluetooth_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:approx_location_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

3.2 iBeacon扫描与RSSI采集

// BeaconScanner.ets - iBeacon扫描管理器
import { access, ble, constant } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit';

// iBeacon数据结构
interface IBeaconInfo {
  uuid: string;          // 128位UUID
  major: number;         // 主标识
  minor: number;         // 次标识
  rssi: number;          // 信号强度
  txPower: number;       // 发射功率(1米处校准RSSI)
  distance: number;      // 估算距离(米)
  timestamp: number;     // 接收时间戳
  deviceId: string;      // 设备地址
}

// iBeacon过滤器配置
interface BeaconFilter {
  uuid?: string;         // 过滤特定UUID
  major?: number;        // 过滤特定Major
  minor?: number;        // 过滤特定Minor
}

export class BeaconScanner {
  private scannedBeacons: Map<string, IBeaconInfo> = new Map();
  private isScanning: boolean = false;
  private filter: BeaconFilter = {};
  private onBeaconUpdate?: (beacons: IBeaconInfo[]) => void;

  constructor(filter?: BeaconFilter) {
    if (filter) {
      this.filter = filter;
    }
  }

  // 设置信标更新回调
  setOnBeaconUpdate(callback: (beacons: IBeaconInfo[]) => void): void {
    this.onBeaconUpdate = callback;
  }

  // 检查蓝牙状态并请求开启
  async checkBluetoothState(): Promise<boolean> {
    try {
      const state = access.getState();
      if (state === access.BluetoothState.STATE_ON) {
        console.info('[BeaconScanner] 蓝牙已开启');
        return true;
      }
      // 请求用户开启蓝牙
      access.enableBluetooth();
      // 等待蓝牙开启(简化处理,实际应监听状态变化)
      await this.waitForBluetoothOn();
      return true;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[BeaconScanner] 蓝牙状态检查失败: ${err.code} - ${err.message}`);
      return false;
    }
  }

  // 等待蓝牙开启
  private waitForBluetoothOn(): Promise<void> {
    return new Promise((resolve, reject) => {
      let retryCount = 0;
      const maxRetry = 20; // 最多等待10秒
      const timer = setInterval(() => {
        retryCount++;
        const state = access.getState();
        if (state === access.BluetoothState.STATE_ON) {
          clearInterval(timer);
          resolve();
        } else if (retryCount >= maxRetry) {
          clearInterval(timer);
          reject(new Error('蓝牙开启超时'));
        }
      }, 500);
    });
  }

  // 解析iBeacon广播数据
  private parseIBeaconData(deviceId: string, advData: number[], rssi: number): IBeaconInfo | null {
    try {
      // 查找厂商特定数据(AD Type = 0xFF)
      let index = 0;
      while (index < advData.length) {
        const length = advData[index];
        if (length === 0 || index + length >= advData.length) {
          break;
        }
        const adType = advData[index + 1];

        // 厂商特定数据
        if (adType === 0xFF && length >= 25) {
          // 检查Apple公司ID (0x004C) 和 iBeacon类型 (0x02, 0x15)
          const companyId = (advData[index + 2] << 8) | advData[index + 3];
          const beaconType = (advData[index + 4] << 8) | advData[index + 5];

          if (companyId === 0x004C && beaconType === 0x0215) {
            // 解析UUID(16字节,从index+6到index+21)
            const uuidBytes = advData.slice(index + 6, index + 22);
            const uuid = this.formatUUID(uuidBytes);

            // 解析Major(2字节)
            const major = (advData[index + 22] << 8) | advData[index + 23];

            // 解析Minor(2字节)
            const minor = (advData[index + 24] << 8) | advData[index + 25];

            // 解析TX Power(1字节,有符号)
            const txPowerRaw = advData[index + 26];
            const txPower = txPowerRaw > 127 ? txPowerRaw - 256 : txPowerRaw;

            // 应用过滤器
            if (this.filter.uuid && this.filter.uuid !== uuid) {
              return null;
            }
            if (this.filter.major !== undefined && this.filter.major !== major) {
              return null;
            }
            if (this.filter.minor !== undefined && this.filter.minor !== minor) {
              return null;
            }

            // 计算距离
            const distance = this.calculateDistance(rssi, txPower);

            return {
              uuid,
              major,
              minor,
              rssi,
              txPower,
              distance,
              timestamp: Date.now(),
              deviceId
            };
          }
        }
        index += length + 1;
      }
      return null;
    } catch (error) {
      console.warn(`[BeaconScanner] 解析iBeacon数据失败: ${error}`);
      return null;
    }
  }

  // 格式化UUID
  private formatUUID(bytes: number[]): string {
    const hex = bytes.map(b => b.toString(16).padStart(2, '0')).join('');
    return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
  }

  // RSSI转距离(对数距离路径损耗模型)
  private calculateDistance(rssi: number, txPower: number): number {
    if (rssi >= 0) {
      return -1.0; // 无效RSSI
    }

    // 路径损耗指数(室内环境典型值2.0-3.0)
    const n = 2.5;
    const ratio = (txPower - rssi) / (10 * n);
    const distance = Math.pow(10, ratio);

    // 限制合理范围
    return Math.max(0.1, Math.min(distance, 50.0));
  }

  // 开始扫描iBeacon
  async startScan(): Promise<void> {
    if (this.isScanning) {
      console.info('[BeaconScanner] 已在扫描中');
      return;
    }

    const btReady = await this.checkBluetoothState();
    if (!btReady) {
      console.error('[BeaconScanner] 蓝牙未就绪,无法启动扫描');
      return;
    }

    try {
      // 配置BLE扫描参数
      const scanOptions: ble.ScanOptions = {
        interval: 0,                     // 扫描间隔(0=连续扫描)
        dutyMode: ble.ScanDuty.SCAN_MODE_LOW_LATENCY,  // 低延迟模式
        matchMode: ble.MatchMode.MATCH_MODE_AGGRESSIVE  // 宽松匹配
      };

      // 启动BLE扫描
      ble.on('BLEDeviceFind', (results: Array<ble.ScanResult>) => {
        this.handleScanResults(results);
      });

      ble.startBLEScan(scanOptions);
      this.isScanning = true;
      console.info('[BeaconScanner] iBeacon扫描已启动');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[BeaconScanner] 启动扫描失败: ${err.code} - ${err.message}`);
    }
  }

  // 处理扫描结果
  private handleScanResults(results: Array<ble.ScanResult>): void {
    const currentTime = Date.now();
    const BEACON_TIMEOUT = 10000; // 10秒超时

    for (const result of results) {
      const beacon = this.parseIBeaconData(
        result.deviceId,
        Array.from(result.data),
        result.rssi
      );

      if (beacon) {
        const key = `${beacon.uuid}-${beacon.major}-${beacon.minor}`;
        this.scannedBeacons.set(key, beacon);
      }
    }

    // 清理超时信标
    for (const [key, beacon] of this.scannedBeacons) {
      if (currentTime - beacon.timestamp > BEACON_TIMEOUT) {
        this.scannedBeacons.delete(key);
      }
    }

    // 通知更新
    if (this.onBeaconUpdate) {
      this.onBeaconUpdate(Array.from(this.scannedBeacons.values()));
    }
  }

  // 停止扫描
  stopScan(): void {
    if (!this.isScanning) {
      return;
    }

    try {
      ble.off('BLEDeviceFind');
      ble.stopBLEScan();
      this.isScanning = false;
      console.info('[BeaconScanner] iBeacon扫描已停止');
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[BeaconScanner] 停止扫描失败: ${err.code} - ${err.message}`);
    }
  }

  // 获取当前信标列表
  getBeacons(): IBeaconInfo[] {
    return Array.from(this.scannedBeacons.values());
  }
}

3.3 卡尔曼滤波RSSI平滑

// KalmanFilter.ets - 卡尔曼滤波器实现
export class KalmanFilterRSSI {
  private processNoise: number;     // 过程噪声协方差 Q
  private measurementNoise: number; // 测量噪声协方差 R
  private estimation: number;       // 状态估计值
  private errorCovariance: number;  // 估计误差协方差 P
  private isInitialized: boolean;   // 是否已初始化

  constructor(processNoise: number = 0.008, measurementNoise: number = 0.1) {
    this.processNoise = processNoise;
    this.measurementNoise = measurementNoise;
    this.estimation = 0;
    this.errorCovariance = 1;
    this.isInitialized = false;
  }

  // 滤波更新
  update(rssi: number): number {
    if (!this.isInitialized) {
      // 首次测量,直接使用观测值初始化
      this.estimation = rssi;
      this.errorCovariance = this.measurementNoise;
      this.isInitialized = true;
      return this.estimation;
    }

    // 预测步骤
    // x_pred = x_est (RSSI短时间内不变)
    const predictedEstimation = this.estimation;
    const predictedErrorCovariance = this.errorCovariance + this.processNoise;

    // 更新步骤
    // 卡尔曼增益 K = P_pred / (P_pred + R)
    const kalmanGain = predictedErrorCovariance /
      (predictedErrorCovariance + this.measurementNoise);

    // 状态更新 x_est = x_pred + K * (z - x_pred)
    this.estimation = predictedEstimation +
      kalmanGain * (rssi - predictedEstimation);

    // 误差协方差更新 P = (1 - K) * P_pred
    this.errorCovariance = (1 - kalmanGain) * predictedErrorCovariance;

    return this.estimation;
  }

  // 重置滤波器
  reset(): void {
    this.estimation = 0;
    this.errorCovariance = 1;
    this.isInitialized = false;
  }

  // 获取当前估计值
  getEstimation(): number {
    return this.estimation;
  }
}

// 多信标卡尔曼滤波管理器
export class MultiBeaconKalmanManager {
  private filters: Map<string, KalmanFilterRSSI> = new Map();

  // 对指定信标的RSSI进行滤波
  filterRSSI(beaconKey: string, rssi: number): number {
    if (!this.filters.has(beaconKey)) {
      this.filters.set(beaconKey, new KalmanFilterRSSI());
    }
    return this.filters.get(beaconKey)!.update(rssi);
  }

  // 重置指定信标的滤波器
  resetBeacon(beaconKey: string): void {
    this.filters.delete(beaconKey);
  }

  // 重置所有滤波器
  resetAll(): void {
    this.filters.clear();
  }
}

3.4 三边定位算法实现

// Trilateration.ets - 三边定位算法
import { IBeaconInfo } from './BeaconScanner';

// 信标已知坐标
interface BeaconPosition {
  uuid: string;
  major: number;
  minor: number;
  x: number;  // 米
  y: number;  // 米
}

// 定位结果
interface PositionResult {
  x: number;
  y: number;
  accuracy: number;  // 估计精度(米)
  beaconCount: number; // 参与定位的信标数
  timestamp: number;
}

export class TrilaterationSolver {
  private beaconPositions: Map<string, BeaconPosition> = new Map();
  private kalmanManager: MultiBeaconKalmanManager = new MultiBeaconKalmanManager();

  // 注册信标坐标
  registerBeacon(position: BeaconPosition): void {
    const key = `${position.uuid}-${position.major}-${position.minor}`;
    this.beaconPositions.set(key, position);
    console.info(`[Trilateration] 注册信标: ${key} -> (${position.x}, ${position.y})`);
  }

  // 批量注册信标坐标
  registerBeacons(positions: BeaconPosition[]): void {
    positions.forEach(p => this.registerBeacon(p));
  }

  // 执行定位计算
  calculatePosition(beacons: IBeaconInfo[]): PositionResult | null {
    // 筛选已知坐标的信标
    const validBeacons: Array<{
      x: number;
      y: number;
      distance: number;
      key: string;
    }> = [];

    for (const beacon of beacons) {
      const key = `${beacon.uuid}-${beacon.major}-${beacon.minor}`;
      const pos = this.beaconPositions.get(key);
      if (pos) {
        // 对RSSI进行卡尔曼滤波
        const filteredRSSI = this.kalmanManager.filterRSSI(key, beacon.rssi);
        // 使用滤波后的RSSI重新计算距离
        const n = 2.5; // 路径损耗指数
        const ratio = (beacon.txPower - filteredRSSI) / (10 * n);
        const distance = Math.max(0.1, Math.min(Math.pow(10, ratio), 50.0));

        validBeacons.push({
          x: pos.x,
          y: pos.y,
          distance,
          key
        });
      }
    }

    if (validBeacons.length < 3) {
      // 信标不足,使用最近邻法
      return this.nearestNeighborPosition(validBeacons);
    }

    // 按距离排序,取最近的信标
    validBeacons.sort((a, b) => a.distance - b.distance);
    const selectedBeacons = validBeacons.slice(0, Math.min(validBeacons.length, 6));

    // 使用加权最小二乘法三边定位
    return this.weightedLeastSquares(selectedBeacons);
  }

  // 加权最小二乘法三边定位
  private weightedLeastSquares(
    beacons: Array<{ x: number; y: number; distance: number }>
  ): PositionResult {
    const n = beacons.length;

    // 构建矩阵方程 Ax = b
    // 以第一个信标为参考点
    const x1 = beacons[0].x;
    const y1 = beacons[0].y;
    const d1 = beacons[0].distance;

    let sumAxx = 0, sumAxy = 0, sumAyy = 0;
    let sumBx = 0, sumBy = 0;
    let sumWeight = 0;

    for (let i = 1; i < n; i++) {
      const xi = beacons[i].x;
      const yi = beacons[i].y;
      const di = beacons[i].distance;

      // 权重:距离越近权重越大
      const weight = 1.0 / (di * di);

      // 线性化后的方程系数
      const ax = 2 * (xi - x1);
      const ay = 2 * (yi - y1);
      const b = d1 * d1 - di * di + xi * xi - x1 * x1 + yi * yi - y1 * y1;

      sumAxx += weight * ax * ax;
      sumAxy += weight * ax * ay;
      sumAyy += weight * ay * ay;
      sumBx += weight * ax * b;
      sumBy += weight * ay * b;
      sumWeight += weight;
    }

    // 求解2x2线性方程组
    const det = sumAxx * sumAyy - sumAxy * sumAxy;
    if (Math.abs(det) < 1e-10) {
      // 奇异矩阵,退回最近邻法
      return this.nearestNeighborPosition(beacons)!;
    }

    const x = (sumAyy * sumBx - sumAxy * sumBy) / det;
    const y = (sumAxx * sumBy - sumAxy * sumBx) / det;

    // 计算定位精度估计(残差)
    let residualSum = 0;
    for (const beacon of beacons) {
      const dx = x - beacon.x;
      const dy = y - beacon.y;
      const estimatedDist = Math.sqrt(dx * dx + dy * dy);
      residualSum += Math.abs(estimatedDist - beacon.distance);
    }
    const accuracy = residualSum / n;

    return {
      x: Math.round(x * 100) / 100,
      y: Math.round(y * 100) / 100,
      accuracy: Math.round(accuracy * 100) / 100,
      beaconCount: n,
      timestamp: Date.now()
    };
  }

  // 最近邻法定位(信标不足3个时的降级方案)
  private nearestNeighborPosition(
    beacons: Array<{ x: number; y: number; distance: number }>
  ): PositionResult | null {
    if (beacons.length === 0) {
      return null;
    }

    // 按距离加权平均
    let totalWeight = 0;
    let weightedX = 0;
    let weightedY = 0;

    for (const beacon of beacons) {
      const weight = 1.0 / Math.max(beacon.distance, 0.1);
      weightedX += weight * beacon.x;
      weightedY += weight * beacon.y;
      totalWeight += weight;
    }

    return {
      x: Math.round((weightedX / totalWeight) * 100) / 100,
      y: Math.round((weightedY / totalWeight) * 100) / 100,
      accuracy: beacons[0].distance,
      beaconCount: beacons.length,
      timestamp: Date.now()
    };
  }
}

3.5 完整定位页面UI

// pages/BeaconPositioningPage.ets
import { BeaconScanner, IBeaconInfo } from '../service/BeaconScanner';
import { TrilaterationSolver, BeaconPosition, PositionResult } from '../service/Trilateration';

@Entry
@Component
struct BeaconPositioningPage {
  @State beacons: IBeaconInfo[] = [];
  @State currentPosition: PositionResult | null = null;
  @State isScanning: boolean = false;
  @State statusText: string = '未启动';
  @State accuracyText: string = '--';

  private scanner: BeaconScanner = new BeaconScanner();
  private solver: TrilaterationSolver = new TrilaterationSolver();

  aboutToAppear(): void {
    // 注册已知信标坐标(模拟商场一楼布局)
    this.solver.registerBeacons([
      { uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825', major: 1, minor: 1, x: 0, y: 0 },
      { uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825', major: 1, minor: 2, x: 10, y: 0 },
      { uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825', major: 1, minor: 3, x: 5, y: 8 },
      { uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825', major: 1, minor: 4, x: 0, y: 10 },
      { uuid: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825', major: 1, minor: 5, x: 10, y: 10 },
    ]);

    // 设置信标更新回调
    this.scanner.setOnBeaconUpdate((beacons: IBeaconInfo[]) => {
      this.beacons = beacons;
      this.currentPosition = this.solver.calculatePosition(beacons);
      if (this.currentPosition) {
        this.accuracyText = `±${this.currentPosition.accuracy}m`;
      }
    });
  }

  aboutToDisappear(): void {
    this.scanner.stopScan();
  }

  build() {
    Column() {
      // 顶部标题栏
      this.TitleBar()

      // 定位信息卡片
      this.PositionCard()

      // 信标列表
      this.BeaconList()

      // 控制按钮
      this.ControlButtons()
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a1a')
  }

  @Builder
  TitleBar() {
    Row() {
      Text('蓝牙信标定位')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ffffff')
      Blank()
      Text(this.statusText)
        .fontSize(14)
        .fontColor(this.isScanning ? '#4ade80' : '#94a3b8')
    }
    .width('100%')
    .padding({ left: 20, right: 20, top: 16, bottom: 8 })
  }

  @Builder
  PositionCard() {
    Column() {
      // 当前坐标
      Row() {
        Column() {
          Text('当前坐标')
            .fontSize(12)
            .fontColor('#94a3b8')
          if (this.currentPosition) {
            Text(`(${this.currentPosition.x}, ${this.currentPosition.y})`)
              .fontSize(28)
              .fontWeight(FontWeight.Bold)
              .fontColor('#ffffff')
              .margin({ top: 4 })
          } else {
            Text('等待定位...')
              .fontSize(28)
              .fontWeight(FontWeight.Bold)
              .fontColor('#475569')
              .margin({ top: 4 })
          }
        }
        .alignItems(HorizontalAlign.Start)

        Blank()

        // 精度指示
        Column() {
          Text('定位精度')
            .fontSize(12)
            .fontColor('#94a3b8')
          Text(this.accuracyText)
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4ade80')
            .margin({ top: 4 })
        }
        .alignItems(HorizontalAlign.End)
      }
      .width('100%')

      // 信标数量指示
      Row() {
        Text(`检测到 ${this.beacons.length} 个信标`)
          .fontSize(13)
          .fontColor('#64748b')
      }
      .width('100%')
      .margin({ top: 12 })
    }
    .width('100%')
    .padding(20)
    .margin({ left: 16, right: 16, top: 12 })
    .borderRadius(16)
    .backgroundColor('rgba(30, 41, 59, 0.8)')
    .backdropBlur(20)
    .border({ width: 1, color: 'rgba(148, 163, 184, 0.1)' })
  }

  @Builder
  BeaconList() {
    Column() {
      Text('信标列表')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor('#ffffff')
        .margin({ bottom: 12 })

      if (this.beacons.length === 0) {
        Column() {
          Text('暂无信标信号')
            .fontSize(14)
            .fontColor('#475569')
            .margin({ top: 40 })
        }
        .width('100%')
        .height(120)
        .justifyContent(FlexAlign.Center)
      } else {
        List() {
          ForEach(this.beacons, (beacon: IBeaconInfo) => {
            ListItem() {
              this.BeaconItem(beacon)
            }
          }, (beacon: IBeaconInfo) => `${beacon.uuid}-${beacon.major}-${beacon.minor}`)
        }
        .width('100%')
        .layoutWeight(1)
        .divider({ strokeWidth: 1, color: 'rgba(148, 163, 184, 0.08)' })
      }
    }
    .width('100%')
    .layoutWeight(1)
    .padding({ left: 16, right: 16, top: 16 })
  }

  @Builder
  BeaconItem(beacon: IBeaconInfo) {
    Row() {
      // 信号强度指示圆点
      Circle()
        .width(10)
        .height(10)
        .fill(beacon.rssi > -60 ? '#4ade80' : beacon.rssi > -80 ? '#fbbf24' : '#ef4444')
        .margin({ right: 12 })

      Column() {
        Text(`Major: ${beacon.major} / Minor: ${beacon.minor}`)
          .fontSize(14)
          .fontColor('#e2e8f0')
          .fontWeight(FontWeight.Medium)
        Text(`UUID: ${beacon.uuid.slice(0, 8)}...`)
          .fontSize(11)
          .fontColor('#64748b')
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      // 距离和RSSI
      Column() {
        Text(`${beacon.distance.toFixed(2)}m`)
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#e2e8f0')
        Text(`${beacon.rssi} dBm`)
          .fontSize(11)
          .fontColor('#64748b')
          .margin({ top: 2 })
      }
      .alignItems(HorizontalAlign.End)
    }
    .width('100%')
    .padding({ top: 10, bottom: 10 })
  }

  @Builder
  ControlButtons() {
    Row() {
      Button(this.isScanning ? '停止扫描' : '开始扫描')
        .width('80%')
        .height(48)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor('#ffffff')
        .backgroundColor(this.isScanning ? '#dc2626' : '#2563eb')
        .borderRadius(24)
        .onClick(() => {
          if (this.isScanning) {
            this.scanner.stopScan();
            this.isScanning = false;
            this.statusText = '已停止';
          } else {
            this.scanner.startScan();
            this.isScanning = true;
            this.statusText = '扫描中';
          }
        })
    }
    .width('100%')
    .justifyContent(FlexAlign.Center)
    .padding({ top: 16, bottom: 24 })
  }
}

四、踩坑与注意事项

4.1 RSSI波动问题

问题:同一位置RSSI值可能在-40dBm到-80dBm之间剧烈波动,导致距离估算误差极大。

解决方案

  1. 卡尔曼滤波:如上文实现,是最常用的平滑方法
  2. 中值滤波:对最近N次RSSI取中值,抗脉冲干扰
  3. 滑动窗口均值:简单但有效,窗口大小建议5-10个采样
  4. 多采样取均值:增加扫描频率,对多次测量取均值

4.2 路径损耗指数n的选择

路径损耗指数n对定位精度影响极大,不同环境下的典型值:

环境 n值范围 推荐值
开阔空间 1.5-2.0 2.0
办公室 2.0-2.5 2.3
商场 2.5-3.5 3.0
仓库 2.0-3.0 2.5
走廊 1.8-2.5 2.0

建议:在实际部署环境中进行校准,采集多个已知距离点的RSSI值,通过最小二乘拟合确定最佳n值。

4.3 蓝牙权限兼容性

HarmonyOS 5.0+对蓝牙权限管控严格,需注意:

  1. ACCESS_BLUETOOTH权限为user_grant级别,必须动态申请
  2. 部分设备同时需要LOCATION权限才能扫描BLE设备
  3. 后台扫描需要KEEP_BACKGROUND_RUNNING权限
  4. HarmonyOS NEXT(纯鸿蒙)权限模型与OpenHarmony有差异

4.4 iBeacon广播频率

iBeacon的广播间隔影响定位实时性和信标功耗:

  • 100ms间隔:实时性好,但信标功耗高(电池约3-6个月)
  • 500ms间隔:平衡选择(电池约1-2年)
  • 1000ms间隔:低功耗(电池约2-3年),但定位延迟大

4.5 多径效应

室内环境中,蓝牙信号经墙壁、家具反射后产生多径效应,导致RSSI不稳定。缓解方法:

  • 信标部署在2.5-3米高度,减少地面反射
  • 避免信标靠近金属表面
  • 使用定向天线减少反射路径
  • 在算法层面通过滤波和统计方法抑制多径影响

五、HarmonyOS 6适配

5.1 新版蓝牙API变化

HarmonyOS 6对蓝牙API进行了升级优化:

// HarmonyOS 6 新版BLE扫描API
import { ble } from '@kit.ConnectivityKit';

// 新增:扫描配置支持更精细的控制
const scanConfig: ble.BLEScanConfig = {
  // 新增:支持按ServiceUUID过滤扫描
  serviceUuids: ['FDA50693-A4E2-4FB1-AFCF-C6EB07647825'],
  // 新增:支持设置扫描PHY
  scanPhy: ble.ScanPhy.PHY_1M,
  // 新增:支持设置Legacy PDUs过滤
  legacyAdvReport: true,
};

// 新增:批量扫描结果回调(减少回调频率,提升性能)
ble.on('BLEBatchResults', (results: Array<ble.ScanResult>) => {
  // 批量处理,减少UI刷新频率
  processBatchResults(results);
});

5.2 后台定位能力增强

HarmonyOS 6增强了后台蓝牙定位能力:

// HarmonyOS 6 后台长时任务
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent } from '@kit.AbilityKit';

// 申请后台长时任务
async function requestBackgroundTask(): Promise<void> {
  const wantAgentInfo: wantAgent.WantAgentInfo = {
    wants: [
      {
        bundleName: 'com.example.beaconpositioning',
        abilityName: 'EntryAbility'
      }
    ],
    requestCode: 0,
    operationType: wantAgent.OperationType.START_ABILITY,
    wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
  };

  const agent = await wantAgent.getWantAgent(wantAgentInfo);

  backgroundTaskManager.startBackgroundRunning(
    getContext(),
    backgroundTaskManager.BackgroundMode.BLUETOOTH_INTERACTION,
    agent
  );
}

5.3 性能优化建议

  1. 扫描策略优化:根据场景动态调整扫描参数,前台使用低延迟模式,后台使用低功耗模式
  2. 内存管理:及时清理超时信标数据,避免Map无限增长
  3. 线程调度:定位计算放入TaskPool,避免阻塞UI线程
  4. 电池优化:实现自适应扫描间隔,静止时降低扫描频率

六、总结

本文系统讲解了基于HarmonyOS的蓝牙信标定位技术,核心要点回顾:

环节 关键技术 精度影响
信号采集 BLE扫描 + iBeacon解析 广播频率决定实时性
信号处理 卡尔曼滤波平滑RSSI 滤波质量直接影响测距精度
距离估算 对数距离路径损耗模型 路径损耗指数n需校准
位置解算 加权最小二乘三边定位 信标数量与几何分布影响精度
降级方案 最近邻法/加权质心 信标不足时的兜底策略

最佳实践建议

  1. 信标部署:每100平方米至少4个信标,呈均匀分布,避免共线
  2. 参数校准:在实际环境中采集数据,校准txPower和路径损耗指数n
  3. 算法选型:精度要求1-3m用三边定位,要求更高需结合WiFi指纹或UWB
  4. 工程化:加入信号质量评估、异常值剔除、平滑过渡等鲁棒性设计

蓝牙信标定位是室内定位生态中最易落地的技术方案,在商场导购、博物馆导览、仓储管理等场景有广泛应用。结合HarmonyOS的BLE能力,开发者可以快速构建实用的室内定位应用。下一篇文章将深入WiFi指纹定位技术,探讨更精确的室内定位方案。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。