HarmonyOS开发:WiFi指纹定位与室内定位

举报
Jack20 发表于 2026/06/22 11:57:54 2026/06/22
【摘要】 HarmonyOS开发:WiFi指纹定位与室内定位核心要点:本文深入讲解基于HarmonyOS的WiFi指纹定位技术,涵盖指纹库构建、信号采集与预处理、WKNN匹配算法、在线定位流程,以及完整的ArkTS代码实现。WiFi指纹定位是室内定位中部署成本最低的方案,无需额外硬件部署,利用现有WiFi基础设施即可实现3-10米级定位。项目说明开发语言ArkTS核心API@ohos.wifiMan...

HarmonyOS开发:WiFi指纹定位与室内定位

核心要点:本文深入讲解基于HarmonyOS的WiFi指纹定位技术,涵盖指纹库构建、信号采集与预处理、WKNN匹配算法、在线定位流程,以及完整的ArkTS代码实现。WiFi指纹定位是室内定位中部署成本最低的方案,无需额外硬件部署,利用现有WiFi基础设施即可实现3-10米级定位。

项目 说明
开发语言 ArkTS
核心API @ohos.wifiManager、@kit.ConnectivityKit
定位精度 3-10米(典型场景)

一、背景与动机

1.1 为什么选择WiFi指纹定位

在室内定位技术谱系中,WiFi指纹定位具有独特的优势——零额外硬件部署。现代建筑几乎都部署了WiFi接入点(AP),这些AP本身就是定位信号源。相比蓝牙信标需要额外购买和部署硬件,WiFi指纹定位可以直接利用现有基础设施,大幅降低部署成本。

对比维度 WiFi指纹定位 蓝牙信标定位 UWB定位
额外硬件 无需 需购买信标 需UWB基站
部署成本 极低
定位精度 3-10m 1-5m 0.1-0.3m
离线工作量 大(需采集指纹)
维护成本 中(换电池)
适用场景 商场/机场/校园 展馆/医院 工厂/仓储

1.2 WiFi指纹定位的核心思想

WiFi指纹定位的核心思想非常直觉:每个位置"看到"的WiFi信号组合是独一无二的。就像人的指纹可以唯一标识一个人一样,某个位置接收到的所有WiFi AP的RSSI值组合,可以唯一标识该位置。

这个"信号指纹"包括:

  • 可检测到的AP列表(BSSID集合)
  • 每个AP的RSSI值
  • AP之间的信号强度排序关系

1.3 WiFi指纹定位的两阶段流程

flowchart TB
    subgraph Offline["离线阶段 - 指纹库构建"]
        direction TB
        O1["场地勘测<br/>划分网格"] --> O2["逐点采集<br/>WiFi信号"]
        O2 --> O3["信号预处理<br/>滤波/归一化"]
        O3 --> O4["指纹存储<br/>构建指纹库"]
    end

    subgraph Online["在线阶段 - 实时定位"]
        direction TB
        N1["实时扫描<br/>WiFi信号"] --> N2["信号预处理<br/>与离线一致"]
        N2 --> N3["指纹匹配<br/>WKNN算法"]
        N3 --> N4["坐标输出<br/>(x, y)"]
    end

    O4 -.->|"指纹库"| N3

    classDef offline fill:#1a1a2e,stroke:#e94560,color:#eee,stroke-width:2px
    classDef online fill:#16213e,stroke:#0f3460,color:#eee,stroke-width:2px
    classDef data fill:#0f3460,stroke:#4ade80,color:#eee,stroke-width:2px

    class O1,O2,O3,O4 offline
    class N1,N2,N3,N4 online
    class O4 data

二、核心原理

2.1 指纹库数据模型

指纹库(Radio Map)是WiFi指纹定位的核心数据结构,记录了每个参考点的WiFi信号特征。

参考点(Reference Point, RP):在场地中预先标记的已知坐标点,通常按1-3米网格均匀分布。

指纹向量:每个参考点处采集到的所有AP的RSSI值组成的向量。

Fp={(bssid1,rssi1),(bssid2,rssi2),,(bssidn,rssin)}F_p = \{ (bssid_1, rssi_1), (bssid_2, rssi_2), \ldots, (bssid_n, rssi_n) \}

其中pp为参考点编号,bssidibssid_i为第ii个AP的MAC地址,rssiirssi_i为该AP在参考点pp处的RSSI值。

2.2 信号传播模型

WiFi信号在室内环境中的传播遵循对数正态阴影模型:

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

其中XσN(0,σ2)X_\sigma \sim \mathcal{N}(0, \sigma^2)σ\sigma通常为5-12dB。这个较大的方差正是WiFi指纹定位精度不如UWB的根本原因。

2.3 WKNN匹配算法

WKNN(Weighted K-Nearest Neighbors)是WiFi指纹定位最常用的在线匹配算法:

  1. 计算距离:将在线采集的指纹向量与指纹库中每个参考点的指纹向量计算相似度
  2. 选择K近邻:选择距离最小的K个参考点
  3. 加权估计:按距离的倒数作为权重,对K个参考点的坐标进行加权平均

常用距离度量

欧氏距离

D(q,p)=i=1n(rssiiqrssiip)2D(q, p) = \sqrt{\sum_{i=1}^{n} (rssi_i^q - rssi_i^p)^2}

曼哈顿距离

D(q,p)=i=1nrssiiqrssiipD(q, p) = \sum_{i=1}^{n} |rssi_i^q - rssi_i^p|

余弦相似度

S(q,p)=FqFpFqFpS(q, p) = \frac{\vec{F_q} \cdot \vec{F_p}}{|\vec{F_q}| \cdot |\vec{F_p}|}

加权位置估计

(x,y)=i=1Kwi(xi,yi)i=1Kwi,wi=1Di+ϵ(x, y) = \frac{\sum_{i=1}^{K} w_i \cdot (x_i, y_i)}{\sum_{i=1}^{K} w_i}, \quad w_i = \frac{1}{D_i + \epsilon}

2.4 指纹库压缩与概率方法

传统确定性方法(WKNN)将指纹表示为RSSI均值,丢失了信号分布信息。概率方法保留RSSI的概率分布,定位精度更高:

概率指纹:每个参考点每个AP的RSSI建模为高斯分布N(μ,σ2)\mathcal{N}(\mu, \sigma^2)

最大似然估计

(x,y)=argmaxpi=1nP(rssiip)(x, y)^* = \arg\max_{p} \prod_{i=1}^{n} P(rssi_i | p)

其中P(rssiip)P(rssi_i | p)是在参考点pp处观测到rssiirssi_i的概率,由高斯分布计算。


三、代码实战

3.1 WiFi扫描与信号采集

// WiFiSignalCollector.ets - WiFi信号采集器
import { wifiManager } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit';

// WiFi扫描结果
interface WiFiScanResult {
  bssid: string;        // AP的MAC地址
  ssid: string;         // 网络名称
  rssi: number;         // 信号强度
  frequency: number;    // 频率(MHz)
  band: number;         // 频段
  timestamp: number;    // 时间戳
}

// 指纹数据点
interface FingerprintPoint {
  id: string;           // 参考点ID
  x: number;            // X坐标(米)
  y: number;            // Y坐标(米)
  floor: number;        // 楼层
  building: string;     // 建筑名称
  signals: Map<string, number>;  // BSSID -> RSSI
  timestamp: number;    // 采集时间
}

export class WiFiSignalCollector {
  private scanResults: WiFiScanResult[] = [];
  private isScanning: boolean = false;
  private scanInterval: number = 1000; // 扫描间隔(ms)
  private intervalId: number = -1;

  // 单次扫描WiFi信号
  async scanOnce(): Promise<WiFiScanResult[]> {
    try {
      // 检查WiFi状态
      if (!wifiManager.isWifiActive()) {
        console.warn('[WiFiCollector] WiFi未开启');
        return [];
      }

      // 触发扫描
      wifiManager.scan();

      // 等待扫描完成(通常需要2-5秒)
      await this.delay(3000);

      // 获取扫描结果
      const results = wifiManager.getScanResults();
      const scanResults: WiFiScanResult[] = [];

      for (const result of results) {
        scanResults.push({
          bssid: result.bssid,
          ssid: result.ssid,
          rssi: result.rssi,
          frequency: result.frequency,
          band: result.band,
          timestamp: Date.now()
        });
      }

      // 按RSSI降序排列
      scanResults.sort((a, b) => b.rssi - a.rssi);
      this.scanResults = scanResults;

      console.info(`[WiFiCollector] 扫描到 ${scanResults.length} 个AP`);
      return scanResults;
    } catch (error) {
      const err = error as BusinessError;
      console.error(`[WiFiCollector] 扫描失败: ${err.code} - ${err.message}`);
      return [];
    }
  }

  // 多次扫描取均值(提高指纹质量)
  async scanMultiple(scanCount: number = 5): Promise<Map<string, number>> {
    const rssiAccumulator: Map<string, number[]> = new Map();

    for (let i = 0; i < scanCount; i++) {
      const results = await this.scanOnce();

      for (const result of results) {
        if (!rssiAccumulator.has(result.bssid)) {
          rssiAccumulator.set(result.bssid, []);
        }
        rssiAccumulator.get(result.bssid)!.push(result.rssi);
      }

      if (i < scanCount - 1) {
        await this.delay(1000); // 扫描间隔
      }
    }

    // 计算每个AP的RSSI均值
    const averagedSignals: Map<string, number> = new Map();
    for (const [bssid, rssiList] of rssiAccumulator) {
      const mean = rssiList.reduce((sum, v) => sum + v, 0) / rssiList.length;
      averagedSignals.set(bssid, Math.round(mean * 10) / 10);
    }

    return averagedSignals;
  }

  // 构建指纹数据点
  async buildFingerprintPoint(
    id: string,
    x: number,
    y: number,
    floor: number,
    building: string
  ): Promise<FingerprintPoint> {
    const signals = await this.scanMultiple(5);

    return {
      id,
      x,
      y,
      floor,
      building,
      signals,
      timestamp: Date.now()
    };
  }

  // 持续扫描(用于在线定位)
  startContinuousScan(
    callback: (results: WiFiScanResult[]) => void,
    intervalMs: number = 2000
  ): void {
    if (this.isScanning) {
      return;
    }

    this.isScanning = true;
    this.scanInterval = intervalMs;

    const doScan = async () => {
      if (!this.isScanning) {
        return;
      }
      const results = await this.scanOnce();
      callback(results);

      // 递归调度下一次扫描
      if (this.isScanning) {
        setTimeout(doScan, this.scanInterval);
      }
    };

    doScan();
  }

  // 停止持续扫描
  stopContinuousScan(): void {
    this.isScanning = false;
  }

  // 获取最近一次扫描结果
  getLatestResults(): WiFiScanResult[] {
    return this.scanResults;
  }

  // 延迟工具函数
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

3.2 指纹库管理

// FingerprintDatabase.ets - 指纹库管理器
import { FingerprintPoint } from './WiFiSignalCollector';

// 指纹库统计信息
interface DatabaseStats {
  totalPoints: number;
  totalAPs: number;
  buildings: string[];
  floors: Map<string, number[]>;
  coverage: Map<string, { minX: number; maxX: number; minY: number; maxY: number }>;
}

export class FingerprintDatabase {
  private fingerprints: Map<string, FingerprintPoint> = new Map();
  private allBSSIDs: Set<string> = new Set();

  // 添加指纹点
  addFingerprint(point: FingerprintPoint): void {
    this.fingerprints.set(point.id, point);
    // 更新已知BSSID集合
    for (const bssid of point.signals.keys()) {
      this.allBSSIDs.add(bssid);
    }
    console.info(`[FingerprintDB] 添加指纹点: ${point.id} (${point.x}, ${point.y})`);
  }

  // 批量添加指纹点
  addFingerprints(points: FingerprintPoint[]): void {
    points.forEach(p => this.addFingerprint(p));
  }

  // 删除指纹点
  removeFingerprint(id: string): boolean {
    return this.fingerprints.delete(id);
  }

  // 获取所有指纹点
  getAllFingerprints(): FingerprintPoint[] {
    return Array.from(this.fingerprints.values());
  }

  // 按建筑和楼层筛选
  getFingerprintsByLocation(building: string, floor: number): FingerprintPoint[] {
    return Array.from(this.fingerprints.values())
      .filter(p => p.building === building && p.floor === floor);
  }

  // 获取指纹库统计信息
  getStats(): DatabaseStats {
    const buildings = new Set<string>();
    const floors: Map<string, number[]> = new Map();
    const coverage: Map<string, { minX: number; maxX: number; minY: number; maxY: number }> = new Map();

    for (const point of this.fingerprints.values()) {
      buildings.add(point.building);

      if (!floors.has(point.building)) {
        floors.set(point.building, []);
      }
      const floorList = floors.get(point.building)!;
      if (!floorList.includes(point.floor)) {
        floorList.push(point.floor);
      }

      if (!coverage.has(point.building)) {
        coverage.set(point.building, {
          minX: point.x, maxX: point.x,
          minY: point.y, maxY: point.y
        });
      }
      const cov = coverage.get(point.building)!;
      cov.minX = Math.min(cov.minX, point.x);
      cov.maxX = Math.max(cov.maxX, point.x);
      cov.minY = Math.min(cov.minY, point.y);
      cov.maxY = Math.max(cov.maxY, point.y);
    }

    return {
      totalPoints: this.fingerprints.size,
      totalAPs: this.allBSSIDs.size,
      buildings: Array.from(buildings),
      floors,
      coverage
    };
  }

  // 导出指纹库为JSON
  exportToJSON(): string {
    const data = Array.from(this.fingerprints.values()).map(p => ({
      id: p.id,
      x: p.x,
      y: p.y,
      floor: p.floor,
      building: p.building,
      signals: Object.fromEntries(p.signals),
      timestamp: p.timestamp
    }));
    return JSON.stringify(data, null, 2);
  }

  // 从JSON导入指纹库
  importFromJSON(json: string): number {
    try {
      const data = JSON.parse(json) as Array<{
        id: string;
        x: number;
        y: number;
        floor: number;
        building: string;
        signals: Record<string, number>;
        timestamp: number;
      }>;

      let count = 0;
      for (const item of data) {
        const signals = new Map<string, number>();
        for (const [bssid, rssi] of Object.entries(item.signals)) {
          signals.set(bssid, rssi);
        }
        this.addFingerprint({
          id: item.id,
          x: item.x,
          y: item.y,
          floor: item.floor,
          building: item.building,
          signals,
          timestamp: item.timestamp
        });
        count++;
      }
      console.info(`[FingerprintDB] 导入 ${count} 个指纹点`);
      return count;
    } catch (error) {
      console.error(`[FingerprintDB] 导入失败: ${error}`);
      return 0;
    }
  }

  // 获取已知BSSID列表
  getAllBSSIDs(): string[] {
    return Array.from(this.allBSSIDs);
  }

  // 获取指纹点数量
  getSize(): number {
    return this.fingerprints.size;
  }
}

3.3 WKNN定位算法实现

// WKNNLocator.ets - WKNN定位算法
import { FingerprintDatabase } from './FingerprintDatabase';
import { FingerprintPoint } from './WiFiSignalCollector';

// 定位结果
interface WKNNPositionResult {
  x: number;
  y: number;
  confidence: number;    // 置信度 0-1
  matchedPoints: Array<{
    id: string;
    x: number;
    y: number;
    distance: number;
    weight: number;
  }>;
  building: string;
  floor: number;
  timestamp: number;
}

// WKNN参数配置
interface WKNNConfig {
  k: number;                     // K近邻数量(默认3-5)
  distanceMetric: 'euclidean' | 'manhattan' | 'cosine';  // 距离度量
  minSignalCount: number;        // 最少匹配AP数
  rssiThreshold: number;         // RSSI阈值(低于此值忽略)
  useWeighted: boolean;          // 是否使用加权
}

export class WKNNLocator {
  private database: FingerprintDatabase;
  private config: WKNNConfig;
  // 历史定位结果(用于平滑)
  private positionHistory: WKNNPositionResult[] = [];
  private maxHistorySize: number = 5;

  constructor(database: FingerprintDatabase, config?: Partial<WKNNConfig>) {
    this.database = database;
    this.config = {
      k: 4,
      distanceMetric: 'euclidean',
      minSignalCount: 3,
      rssiThreshold: -90,
      useWeighted: true,
      ...config
    };
  }

  // 执行定位
  locate(
    onlineSignals: Map<string, number>,
    building?: string,
    floor?: number
  ): WKNNPositionResult | null {
    // 获取候选指纹点
    let candidates = building && floor !== undefined
      ? this.database.getFingerprintsByLocation(building, floor)
      : this.database.getAllFingerprints();

    if (candidates.length === 0) {
      console.warn('[WKNN] 指纹库为空');
      return null;
    }

    // 过滤在线信号(低于阈值的AP)
    const filteredOnline = new Map<string, number>();
    for (const [bssid, rssi] of onlineSignals) {
      if (rssi >= this.config.rssiThreshold) {
        filteredOnline.set(bssid, rssi);
      }
    }

    if (filteredOnline.size < this.config.minSignalCount) {
      console.warn(`[WKNN] 在线信号不足: ${filteredOnline.size} < ${this.config.minSignalCount}`);
      return null;
    }

    // 计算与每个候选点的距离
    const distances: Array<{
      point: FingerprintPoint;
      distance: number;
      matchedAPs: number;
    }> = [];

    for (const candidate of candidates) {
      const { distance, matchedCount } = this.calculateDistance(
        filteredOnline,
        candidate.signals
      );

      // 只保留匹配AP数足够的候选点
      if (matchedCount >= this.config.minSignalCount) {
        distances.push({
          point: candidate,
          distance,
          matchedAPs: matchedCount
        });
      }
    }

    if (distances.length === 0) {
      console.warn('[WKNN] 无有效匹配');
      return null;
    }

    // 按距离排序,取K近邻
    distances.sort((a, b) => a.distance - b.distance);
    const kNearest = distances.slice(0, Math.min(this.config.k, distances.length));

    // 加权估计位置
    const result = this.estimatePosition(kNearest);

    // 历史平滑
    this.positionHistory.push(result);
    if (this.positionHistory.length > this.maxHistorySize) {
      this.positionHistory.shift();
    }

    return this.smoothPosition(result);
  }

  // 计算指纹距离
  private calculateDistance(
    onlineSignals: Map<string, number>,
    offlineSignals: Map<string, number>
  ): { distance: number; matchedCount: number } {
    // 收集所有在线和离线共有的BSSID
    const commonBSSIDs: string[] = [];
    for (const bssid of onlineSignals.keys()) {
      if (offlineSignals.has(bssid)) {
        commonBSSIDs.push(bssid);
      }
    }

    if (commonBSSIDs.length === 0) {
      return { distance: Infinity, matchedCount: 0 };
    }

    // 构建向量
    const onlineVec: number[] = [];
    const offlineVec: number[] = [];

    for (const bssid of commonBSSIDs) {
      onlineVec.push(onlineSignals.get(bssid)!);
      offlineVec.push(offlineSignals.get(bssid)!);
    }

    // 对于在线有但离线没有的AP,使用惩罚值
    const penaltyRSSI = -100; // 缺失AP的惩罚RSSI
    for (const bssid of onlineSignals.keys()) {
      if (!offlineSignals.has(bssid)) {
        onlineVec.push(onlineSignals.get(bssid)!);
        offlineVec.push(penaltyRSSI);
      }
    }
    for (const bssid of offlineSignals.keys()) {
      if (!onlineSignals.has(bssid)) {
        onlineVec.push(penaltyRSSI);
        offlineVec.push(offlineSignals.get(bssid)!);
      }
    }

    // 计算距离
    let distance: number;
    switch (this.config.distanceMetric) {
      case 'manhattan':
        distance = this.manhattanDistance(onlineVec, offlineVec);
        break;
      case 'cosine':
        distance = 1 - this.cosineSimilarity(onlineVec, offlineVec);
        break;
      case 'euclidean':
      default:
        distance = this.euclideanDistance(onlineVec, offlineVec);
        break;
    }

    return { distance, matchedCount: commonBSSIDs.length };
  }

  // 欧氏距离
  private euclideanDistance(a: number[], b: number[]): number {
    let sum = 0;
    for (let i = 0; i < a.length; i++) {
      sum += (a[i] - b[i]) ** 2;
    }
    return Math.sqrt(sum);
  }

  // 曼哈顿距离
  private manhattanDistance(a: number[], b: number[]): number {
    let sum = 0;
    for (let i = 0; i < a.length; i++) {
      sum += Math.abs(a[i] - b[i]);
    }
    return sum;
  }

  // 余弦相似度
  private cosineSimilarity(a: number[], b: number[]): number {
    let dotProduct = 0;
    let normA = 0;
    let normB = 0;
    for (let i = 0; i < a.length; i++) {
      dotProduct += a[i] * b[i];
      normA += a[i] ** 2;
      normB += b[i] ** 2;
    }
    if (normA === 0 || normB === 0) {
      return 0;
    }
    return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
  }

  // 加权位置估计
  private estimatePosition(
    kNearest: Array<{ point: FingerprintPoint; distance: number; matchedAPs: number }>
  ): WKNNPositionResult {
    if (this.config.useWeighted) {
      // 加权平均:距离越近权重越大
      const epsilon = 1e-6;
      let totalWeight = 0;
      let weightedX = 0;
      let weightedY = 0;
      const matchedPoints: WKNNPositionResult['matchedPoints'] = [];

      for (const item of kNearest) {
        const weight = 1.0 / (item.distance + epsilon);
        weightedX += weight * item.point.x;
        weightedY += weight * item.point.y;
        totalWeight += weight;

        matchedPoints.push({
          id: item.point.id,
          x: item.point.x,
          y: item.point.y,
          distance: item.distance,
          weight
        });
      }

      const x = weightedX / totalWeight;
      const y = weightedY / totalWeight;

      // 计算置信度(基于最近邻距离)
      const minDist = kNearest[0].distance;
      const confidence = Math.max(0, Math.min(1, 1 - minDist / 100));

      return {
        x: Math.round(x * 100) / 100,
        y: Math.round(y * 100) / 100,
        confidence,
        matchedPoints,
        building: kNearest[0].point.building,
        floor: kNearest[0].point.floor,
        timestamp: Date.now()
      };
    } else {
      // 简单平均
      const x = kNearest.reduce((sum, item) => sum + item.point.x, 0) / kNearest.length;
      const y = kNearest.reduce((sum, item) => sum + item.point.y, 0) / kNearest.length;

      return {
        x: Math.round(x * 100) / 100,
        y: Math.round(y * 100) / 100,
        confidence: 0.5,
        matchedPoints: kNearest.map(item => ({
          id: item.point.id,
          x: item.point.x,
          y: item.point.y,
          distance: item.distance,
          weight: 1
        })),
        building: kNearest[0].point.building,
        floor: kNearest[0].point.floor,
        timestamp: Date.now()
      };
    }
  }

  // 历史位置平滑(滑动窗口加权平均)
  private smoothPosition(current: WKNNPositionResult): WKNNPositionResult {
    if (this.positionHistory.length < 2) {
      return current;
    }

    // 越新的结果权重越大
    let totalWeight = 0;
    let weightedX = 0;
    let weightedY = 0;

    for (let i = 0; i < this.positionHistory.length; i++) {
      const weight = i + 1; // 线性递增权重
      weightedX += weight * this.positionHistory[i].x;
      weightedY += weight * this.positionHistory[i].y;
      totalWeight += weight;
    }

    return {
      ...current,
      x: Math.round((weightedX / totalWeight) * 100) / 100,
      y: Math.round((weightedY / totalWeight) * 100) / 100
    };
  }

  // 更新配置
  updateConfig(config: Partial<WKNNConfig>): void {
    this.config = { ...this.config, ...config };
  }

  // 清除历史
  clearHistory(): void {
    this.positionHistory = [];
  }
}

3.4 离线采集页面

// pages/FingerprintCollectionPage.ets - 离线指纹采集页面
import { WiFiSignalCollector, WiFiScanResult } from '../service/WiFiSignalCollector';
import { FingerprintDatabase } from '../service/FingerprintDatabase';

@Entry
@Component
struct FingerprintCollectionPage {
  @State scanResults: WiFiScanResult[] = [];
  @State isCollecting: boolean = false;
  @State currentPoint: string = '';
  @State coordX: string = '0';
  @State coordY: string = '0';
  @State floor: string = '1';
  @State building: string = 'A栋';
  @State collectedCount: number = 0;
  @State statusMessage: string = '准备就绪';

  private collector: WiFiSignalCollector = new WiFiSignalCollector();
  private database: FingerprintDatabase = new FingerprintDatabase();

  build() {
    Scroll() {
      Column() {
        // 标题
        Text('WiFi指纹采集')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
          .margin({ bottom: 20 })

        // 参考点信息输入
        this.PointInfoInput()

        // 扫描结果列表
        this.ScanResultList()

        // 操作按钮
        this.ActionButtons()

        // 状态信息
        Text(this.statusMessage)
          .fontSize(13)
          .fontColor('#94a3b8')
          .margin({ top: 16 })
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a1a')
  }

  @Builder
  PointInfoInput() {
    Column() {
      Text('参考点信息')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor('#e2e8f0')
        .margin({ bottom: 12 })

      Row() {
        TextInput({ placeholder: 'X坐标(米)', text: this.coordX })
          .width('48%')
          .height(44)
          .fontSize(14)
          .fontColor('#ffffff')
          .backgroundColor('rgba(30, 41, 59, 0.8)')
          .borderRadius(10)
          .onChange((value: string) => { this.coordX = value; })

        TextInput({ placeholder: 'Y坐标(米)', text: this.coordY })
          .width('48%')
          .height(44)
          .fontSize(14)
          .fontColor('#ffffff')
          .backgroundColor('rgba(30, 41, 59, 0.8)')
          .borderRadius(10)
          .onChange((value: string) => { this.coordY = value; })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        TextInput({ placeholder: '楼层', text: this.floor })
          .width('30%')
          .height(44)
          .fontSize(14)
          .fontColor('#ffffff')
          .backgroundColor('rgba(30, 41, 59, 0.8)')
          .borderRadius(10)
          .onChange((value: string) => { this.floor = value; })

        TextInput({ placeholder: '建筑名称', text: this.building })
          .width('66%')
          .height(44)
          .fontSize(14)
          .fontColor('#ffffff')
          .backgroundColor('rgba(30, 41, 59, 0.8)')
          .borderRadius(10)
          .onChange((value: string) => { this.building = value; })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ top: 10 })
    }
    .width('100%')
    .padding(16)
    .borderRadius(16)
    .backgroundColor('rgba(30, 41, 59, 0.6)')
    .border({ width: 1, color: 'rgba(148, 163, 184, 0.1)' })
  }

  @Builder
  ScanResultList() {
    Column() {
      Text(`扫描结果 (${this.scanResults.length} 个AP)`)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor('#e2e8f0')
        .margin({ top: 16, bottom: 8 })

      List() {
        ForEach(this.scanResults, (ap: WiFiScanResult) => {
          ListItem() {
            Row() {
              Column() {
                Text(ap.ssid || '(隐藏网络)')
                  .fontSize(13)
                  .fontColor('#e2e8f0')
                  .fontWeight(FontWeight.Medium)
                Text(ap.bssid)
                  .fontSize(11)
                  .fontColor('#64748b')
                  .margin({ top: 2 })
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)

              // RSSI信号强度条
              Row() {
                Text(`${ap.rssi} dBm`)
                  .fontSize(12)
                  .fontColor(ap.rssi > -50 ? '#4ade80' : ap.rssi > -70 ? '#fbbf24' : '#ef4444')
              }
            }
            .width('100%')
            .padding({ top: 8, bottom: 8 })
          }
        }, (ap: WiFiScanResult) => ap.bssid)
      }
      .width('100%')
      .height(300)
      .divider({ strokeWidth: 1, color: 'rgba(148, 163, 184, 0.06)' })
    }
    .width('100%')
  }

  @Builder
  ActionButtons() {
    Column() {
      // 扫描按钮
      Button('扫描WiFi信号')
        .width('100%')
        .height(48)
        .fontSize(16)
        .fontColor('#ffffff')
        .backgroundColor('#2563eb')
        .borderRadius(12)
        .margin({ top: 16 })
        .onClick(async () => {
          this.statusMessage = '扫描中...';
          this.scanResults = await this.collector.scanOnce();
          this.statusMessage = `扫描完成,发现 ${this.scanResults.length} 个AP`;
        })

      // 采集并保存按钮
      Button('采集并保存指纹(5次扫描取均值)')
        .width('100%')
        .height(48)
        .fontSize(16)
        .fontColor('#ffffff')
        .backgroundColor(this.isCollecting ? '#475569' : '#059669')
        .borderRadius(12)
        .margin({ top: 10 })
        .enabled(!this.isCollecting)
        .onClick(async () => {
          this.isCollecting = true;
          this.statusMessage = '采集中(5次扫描取均值)...';

          const x = parseFloat(this.coordX) || 0;
          const y = parseFloat(this.coordY) || 0;
          const floorNum = parseInt(this.floor) || 1;
          const pointId = `RP_${this.building}_F${floorNum}_${this.collectedCount + 1}`;

          try {
            const fingerprint = await this.collector.buildFingerprintPoint(
              pointId, x, y, floorNum, this.building
            );
            this.database.addFingerprint(fingerprint);
            this.collectedCount++;
            this.statusMessage = `指纹已保存: ${pointId},共 ${this.collectedCount} 个参考点`;
          } catch (error) {
            this.statusMessage = `采集失败: ${error}`;
          }

          this.isCollecting = false;
        })

      // 导出指纹库
      Button('导出指纹库JSON')
        .width('100%')
        .height(48)
        .fontSize(16)
        .fontColor('#ffffff')
        .backgroundColor('#7c3aed')
        .borderRadius(12)
        .margin({ top: 10 })
        .onClick(() => {
          const json = this.database.exportToJSON();
          console.info(`[导出] 指纹库: ${json.length} 字节`);
          this.statusMessage = `已导出 ${this.database.getSize()} 个指纹点`;
        })
    }
    .width('100%')
  }
}

四、踩坑与注意事项

4.1 指纹采集的"魔鬼细节"

问题1:人体遮挡效应

人体含大量水分,对2.4GHz WiFi信号有显著吸收作用。采集指纹时,持手机的人体朝向不同,同一位置的RSSI可能差异5-10dB。

解决方案

  • 每个参考点采集时,面向4个方向各采集一组数据
  • 将4组数据合并为一个指纹点,保留方向信息
  • 在线定位时也记录用户朝向

问题2:时间漂移

WiFi指纹会随时间变化。AP位置移动、新增/拆除AP、家具重新布置、人流密度变化等都会导致指纹漂移。

解决方案

  • 定期更新指纹库(建议每3-6个月重新采集)
  • 实现增量更新机制,只更新变化区域
  • 采用在线学习算法,利用用户反馈持续优化

问题3:设备异构性

不同手机型号的WiFi天线和射频芯片不同,同一位置同一AP的RSSI测量值可能差异10-20dB。

解决方案

  • 信号归一化:将RSSI映射到0-1区间
  • 设备校准:记录设备型号,使用校准系数补偿
  • 排序法:不使用绝对RSSI值,只使用AP信号强度排序

4.2 WiFi扫描权限与兼容性

HarmonyOS的WiFi扫描权限管控要点:

  1. ohos.permission.GET_WIFI_INFO:获取WiFi信息
  2. ohos.permission.SET_WIFI_INFO:配置WiFi(触发扫描需要)
  3. ohos.permission.LOCATION:获取扫描结果需要位置权限
  4. 部分HarmonyOS版本在WiFi关闭时无法获取扫描结果
  5. 后台WiFi扫描受限,需配合长时任务

4.3 指纹库规模与性能

指纹库规模直接影响定位精度和计算性能:

网格间距 1000m²参考点数 定位精度 匹配耗时
1m ~1000 1-3m ~50ms
2m ~250 2-5m ~15ms
3m ~111 3-8m ~8ms
5m ~40 5-15m ~3ms

建议:根据应用场景选择合适的网格密度,一般2-3米间距是精度和采集成本的较好平衡。

4.4 AP选择策略

并非所有AP都对定位有正面贡献。选择策略:

  1. 信号强度过滤:排除RSSI < -85dBm的弱信号AP
  2. 稳定性过滤:排除出现率低于50%的不稳定AP
  3. 区分度过滤:排除在整个指纹库中RSSI方差很小的AP
  4. 数量控制:保留最强的15-20个AP即可

五、HarmonyOS 6适配

5.1 WiFi扫描API增强

HarmonyOS 6对WiFi管理API进行了优化:

// HarmonyOS 6 新增WiFi扫描能力
import { wifiManager } from '@kit.ConnectivityKit';

// 新增:支持指定扫描类型
const scanConfig: wifiManager.WiFiScanConfig = {
  // 新增:按频段过滤扫描
  band: wifiManager.WiFiBand.BAND_2GHZ,
  // 新增:按SSID过滤
  ssid: '特定网络',
};

// 新增:扫描结果变化监听
wifiManager.on('scanResultsChange', () => {
  const results = wifiManager.getScanResults();
  console.info(`[WiFi] 扫描结果更新: ${results.length} 个AP`);
});

5.2 指纹库云端同步

HarmonyOS 6增强了云端能力,支持指纹库的云端存储和同步:

// HarmonyOS 6 云端指纹库同步
import { cloudDatabase } from '@kit.CloudKit';

// 上传指纹库到云端
async function uploadFingerprintDB(db: FingerprintDatabase): Promise<void> {
  const json = db.exportToJSON();
  await cloudDatabase.collection('fingerprint_db')
    .doc('building_A_floor_1')
    .set({ data: json, updatedAt: Date.now() });
}

// 从云端下载指纹库
async function downloadFingerprintDB(db: FingerprintDatabase): Promise<void> {
  const doc = await cloudDatabase.collection('fingerprint_db')
    .doc('building_A_floor_1')
    .get();
  if (doc.data) {
    db.importFromJSON(doc.data.data);
  }
}

5.3 性能优化

  1. 增量匹配:先通过楼层和区域粗定位,再在子区域内精确匹配
  2. AP预筛选:在线定位时只使用信号最强的N个AP,减少计算量
  3. 并行计算:将指纹匹配放入TaskPool并行执行
  4. 缓存机制:对连续定位请求,缓存上一次的K近邻结果

六、总结

本文系统讲解了基于HarmonyOS的WiFi指纹定位技术,核心要点回顾:

flowchart LR
    subgraph 离线阶段
        A1["场地勘测"] --> A2["网格划分"]
        A2 --> A3["逐点采集"]
        A3 --> A4["信号预处理"]
        A4 --> A5["构建指纹库"]
    end

    subgraph 在线阶段
        B1["实时扫描"] --> B2["信号预处理"]
        B2 --> B3["指纹匹配<br/>WKNN"]
        B3 --> B4["位置估计"]
        B4 --> B5["历史平滑"]
    end

    A5 -.->|"指纹库"| B3

    classDef offline fill:#1a1a2e,stroke:#e94560,color:#eee,stroke-width:2px
    classDef online fill:#16213e,stroke:#0f3460,color:#eee,stroke-width:2px
    classDef data fill:#0f3460,stroke:#4ade80,color:#eee,stroke-width:2px

    class A1,A2,A3,A4 offline
    class B1,B2,B3,B4,B5 online
    class A5 data
环节 关键技术 注意事项
指纹采集 多次扫描取均值、四方向采集 人体遮挡、设备异构性
信号预处理 滤波、归一化、AP筛选 弱信号剔除、缺失值处理
指纹匹配 WKNN(K=3-5) 距离度量选择、K值调优
位置估计 加权平均、置信度评估 权重函数选择
结果平滑 滑动窗口、中值滤波 窗口大小与延迟的权衡

WiFi指纹定位的适用场景

  • ✅ 大型商场、机场、校园等已有WiFi覆盖的公共建筑
  • ✅ 对精度要求3-10米的应用(区域级定位)
  • ✅ 需要快速部署、低成本的室内定位方案
  • ❌ 对精度要求亚米级的高精度场景
  • ❌ WiFi AP密度极低的环境

WiFi指纹定位是室内定位生态中部署门槛最低的方案,适合作为室内定位系统的"第一层覆盖"。在实际项目中,常将WiFi指纹定位与蓝牙信标定位结合使用——WiFi指纹提供区域级粗定位,蓝牙信标提供关键区域的精确定位,形成分层定位架构。下一篇文章将深入UWB超宽带定位技术,探索厘米级精确定位的实现。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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