ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Java实现WiFi信号室内定位:RSSI建模、指纹匹配与Android工程实践

用Java实现WiFi信号室内定位:RSSI建模、指纹匹配与Android工程实践 简介这是一套面向计算机相关专业毕业设计或课程设计场景的WiFi信号强度定位工具项目基于Java开发Android端APK完整呈现了通过WiFi信号强度实现位置估算的工程实现路径。压缩包共254个文件以66个Java源码、78个XML配置与布局、61张PNG界面资源为主体辅以Gradle构建脚本、JAR依赖及可直接安装的APK包整体仅8.85MB便于导入Android Studio编译和二次开发。资源已经过功能测试可用于毕设展示、课设作业或初期立项演示既能在真机直接运行APK查看效果也能对照源码理解WiFi信号采集、位置计算与界面展示等完整实现思路。压缩包内含PDF与MD说明文档可帮助快速梳理目录结构和启动步骤降低上手门槛。目前已有300人学习/下载对需要快速完成室内定位方向课题的学生具有较高参考价值。1. 用 Java 实现 WiFi 信号定位先从 RSSI 与传播模型说起在商场中庭、地下车库或展览馆里手机能扫到十几个 WiFi 热点却搜不到可用的 GPS 卫星。这个标题要解决的正是这种“室内无星”场景基于 WIFI 信号强度用 Android Java 读取周边热点的 RSSI结合传播模型或指纹库把信号强度换算成设备坐标再打包成 APK 安装包。真正决定定位精度的不是 UI而是无线信号的抖动控制。同样的位置连续扫描 10 次RSSI 可能震荡 7dBm 以上对应距离偏差经常超过 5 米。所以这个毕设的技术含量不在于扫到了多少 AP而在于怎么建模、怎么过滤、怎么匹配坐标。下面按我实际搭过的一条路径讲。2. 基于 WIFI 信号强度的室内定位RSSI 定位模型与误差来源2.1 为什么选 RSSI 而不做 TOA / AOA 测距TOA到达时间定位在原理上比 RSSI 干净测出信号从 AP 到手机的时间乘光速就是距离。可普通 WiFi 网卡根本不提供纳秒级时间戳Android 的 ScanResult 也没有这个字段。AOA到达角度需要阵列天线手机端同样拿不到。RSSI 是每个 ScanResult 都暴露的 level 字段能在几乎所有 Android 设备上读到因此基于 WIFI 信号强度的定位成了室内定位最现实的入口。RSSI 的问题也很直接它不是稳定的距离度量。发送功率、天线方向、墙体和人体吸收都会改变同一个位置的电平读数。要在 Java 里把它转换成坐标先要接受“信号强度只能给出概率意义上的位置”然后选一种模型消化这种不确定性。这也是标题里“设计”二字的重点。2.2 对数距离路径损耗模型与 Java 距离换算最常用的经验模型是RSSI(dBm) A - 10 * n * log10(d / d0)A 是距离 AP 一米处测到的信号强度n 是路径损耗指数d0 取 1 米。反解出 dpublic class RssiDistanceUtil { private static final double A -46.0; private static final double N 3.0; private static final double MIN_RSSI -90.0; public static double distanceByRssi(double rssi) { double r Math.max(rssi, MIN_RSSI); return Math.pow(10.0, (A - r) / (10.0 * N)); } }这段代码把 r.level负数当成 rssi先钳制在 -90 以上避免极端弱信号算出离谱距离。A 和 N 不要拍脑袋填我一般会在一楼实验室里实测距离 AP 1 米处扫 20 次取平均写 A再在 5 米、10 米处各采几组做拟合得到 n。下表是常见取值参数典型值说明A-40 ~ -55取决于 AP 发射功率和手机天线增益n2.0 ~ 3.8越空旷越接近 2墙体越多越大模型描述的是理想自由空间衰减。如果 AP 少、场景开阔可以把 n 设小如果房间隔墙多n 不提高的话距离会整体偏大三个圆的交集也会变成一大片模糊区域。2.3 指纹定位与传播模型的选型边界传播模型能算出“到某个 AP 的距离”但三个圆交出的区域常常很大。多径反射会同时把几路信号叠在一起RSSI 不严格遵守衰减公式。另一个可选路径是指纹定位先在网格点上采集“BSSID - RSSI”向量存成指纹在线定位时拿当前向量去比较。方案离线工作定位精度实现复杂度传播模型三边测量只需 AP 坐标5~15 米低传播模型加权质心只需 AP 坐标4~10 米低指纹 kNN逐网格采集2~5 米中对毕设来说如果现场只有三四个可用 AP用传播模型加加权质心更现实如果可以拿着手机在各点位慢慢扫指纹方案演示效果更好。后面章节把两条路都讲指纹为主质心作为兜底。2.4 多径、人体遮挡与弱信号 AP 的过滤策略RSSI 抖动主要来自多径和人体遮挡。同一位置连续扫 10 次标准差超过 3dBm 很常见。角度一变信号可能掉 8 个 dBm。所以不能在每次扫描后直接做定位至少要先对 AP 列表做一次规整public MapString, Integer normalizeRssi(ListScanResult results) { MapString, Integer map new HashMap(); for (ScanResult r : results) { if (r.level -85) { continue; } Integer old map.get(r.BSSID); if (old null || r.level old) { map.put(r.BSSID, r.level); } } return map; }我一般会把-85dBm作为阈值。低于这个值RSSI 波动幅度很大对距离计算的贡献几乎是噪声。同一 BSSID 在广播多频段时会重复出现这里保留信号最强的那一个。如果环境中 AP 很少阈值可以放宽到 -88否则尽量不用弱 AP。3. Android Java 工程搭建权限配置、WiFi 扫描与指纹采集3.1 AndroidManifest 权限与 Java 环境变量配置从命令行构建 APK 之前先确认系统能识别 JDK。java -version能输出版本号再在项目根目录跑./gradlew。如果 java 环境变量配置不对Gradle 会直接报JAVA_HOME is set to an invalid directory。Android Studio 自带 JBR但命令行打包时仍建议显式把JAVA_HOME指向 JDK 11 或 JDK 17。然后是 AndroidManifest.xml 里的权限uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.CHANGE_WIFI_STATE / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.NEARBY_WIFI_DEVICES /前两个权限负责扫描和触发扫描关键点是从 Android 6.0 开始getScanResults()能返回多少内容取决于是否授予定位权限。只申请 WiFi 权限结果集可能为空。如果 targetSdk 33 以上还要动态申请NEARBY_WIFI_DEVICES否则 BSSID 仍然拿不到。动态申请时按系统版本分批public void checkPermission(Activity activity) { ListString need new ArrayList(); Collections.addAll(need, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION); if (Build.VERSION.SDK_INT 33) { need.add(android.permission.NEARBY_WIFI_DEVICES); } activity.requestPermissions(need.toArray(new String[0]), 1001); }之所以用字符串常量而不直接用Manifest.permission.NEARBY_WIFI_DEVICES是为了在低版本 SDK 上少做一次 API 分支低版本对未知权限会直接忽略。权限组合可以按下表核对使用场景权限是否动态扫描结果ACCESS_WIFI_STATE否触发扫描CHANGE_WIFI_STATE否拿到 BSSID/RSSIACCESS_FINE_LOCATION是Android 13NEARBY_WIFI_DEVICES是3.2 用 BroadcastReceiver 做 WiFi 扫描并取 RSSI扫描结果通过系统广播返回代码public class WifiScanner extends BroadcastReceiver { public interface OnScanResultListener { void onResult(MapString, Integer rssiMap); } private final WifiManager wifiManager; private final OnScanResultListener listener; public WifiScanner(Context context, OnScanResultListener listener) { this.wifiManager (WifiManager) context.getApplicationContext() .getSystemService(Context.WIFI_SERVICE); this.listener listener; } public boolean triggerScan() { // 一次扫描约 1~2 秒间隔太短会返回 false return wifiManager.startScan(); } Override public void onReceive(Context context, Intent intent) { if (!WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(intent.getAction())) { return; } MapString, Integer rssiMap new HashMap(); for (ScanResult r : wifiManager.getScanResults()) { if (r.level -85) continue; Integer old rssiMap.get(r.BSSID); if (old null || r.level old) { rssiMap.put(r.BSSID, r.level); } } listener.onResult(rssiMap); } }在 Activity 里注册并触发一次扫描wifiScanner new WifiScanner(this, map - onNewRssi(map)); registerReceiver(wifiScanner, new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); wifiScanner.triggerScan();我的习惯是每 2 秒调用一次triggerScan()收到一次结果后再启动下一次。系统不会按应用的要求精准执行连续调用会直接返回 false所以不能写成循环里疯狂startScan()。3.3 指纹数据序列化JSON 结构与采集流程把指纹点落盘成 JSON结构如下[ { x: 1.5, y: 2.0, aps: { aa:bb:cc:dd:ee:01: -43, aa:bb:cc:dd:ee:02: -58 } } ]x和y是物理坐标系里的坐标单位米原点自己定。aps的 key 用 BSSID不要用 SSID因为多个 AP 可能同名。采集流程我一般这样把区域划成 1 米间隔的网格手机停在某个网格中心连续扫 10 次逐个热点取平均后写入一条记录。平均能压掉一部分瞬时抖动比单次扫描更接近真实分布。3.4 Gradle 构建 APK签名、输出与加固Module 的 build.gradleandroid { compileSdk 34 defaultConfig { applicationId com.example.wifiloc minSdk 23 targetSdk 31 versionCode 1 versionName 1.0 } }targetSdk 选 31 而不是 34是因为高版本对 WiFi 扫描加了不少限制毕设演示通常不需要适配到最新政策。构建命令./gradlew assembleDebug adb install -r app/build/outputs/apk/debug/app-debug.apkassembleDebug会用一个自动生成的 debug keystore 签名安装到开发机上没问题。如果要把 APK 交给别人最好生成 release 签名否则部分手机会拒绝安装。此外有些杀毒软件会把不含正式签名的 APK 报毒这时候可以走一次常规的 apk 加固流程重新签名后再分发但毕设只要能在真机上跑通不强制。4. 定位算法实现kNN 指纹匹配、加权质心与轨迹平滑4.1 指纹库数据结构的 Java 定义写一个简单类public class Fingerprint { public double x; public double y; public MapString, Integer rssi new HashMap(); }从 assets 里的 JSON 加载public ListFingerprint loadFingerprints(Context context) throws JSONException, IOException { String json readAsset(context, fingerprints.json); JSONArray array new JSONArray(json); ListFingerprint list new ArrayList(); for (int i 0; i array.length(); i) { JSONObject obj array.getJSONObject(i); Fingerprint fp new Fingerprint(); fp.x obj.getDouble(x); fp.y obj.getDouble(y); JSONObject aps obj.getJSONObject(aps); IteratorString keys aps.keys(); while (keys.hasNext()) { String bssid keys.next(); fp.rssi.put(bssid, aps.getInt(bssid)); } list.add(fp); } return list; }这里把指纹库放进 assets 而不是 /sdcard可以避免运行时读写权限和路径问题。加载完成后可以打印指纹数量通常 100 个指纹点起步少于 30 个点会导致匹配结果跳跃。4.2 kNN 在线匹配的完整实现核心逻辑是计算当前 RSSI 向量与每个指纹的欧氏距离取最近的 k 个以距离倒数为权重做加权平均public static double[] locateByKnn(ListFingerprint db, MapString, Integer current, int k) { Listdouble[] dists new ArrayList(); for (Fingerprint fp : db) { double sum 0; int match 0; for (Map.EntryString, Integer e : fp.rssi.entrySet()) { Integer cur current.get(e.getKey()); if (cur ! null) { double delta e.getValue() - cur; sum delta * delta; match; } else { // 指纹库里有、当前扫描没有的 AP加上固定惩罚 sum 20 * 20; } } if (match 3) continue; dists.add(new double[]{Math.sqrt(sum), fp.x, fp.y}); } if (dists.isEmpty()) return null; dists.sort(Comparator.comparingDouble(v - v[0])); int realK Math.min(k, dists.size()); double wx 0, wy 0, wSum 0; for (int i 0; i realK; i) { double[] d dists.get(i); double w 1.0 / (d[0] 1.0); wx w * d[1]; wy w * d[2]; wSum w; } return new double[]{wx / wSum, wy / wSum}; }几个参数的调整经验k 取 3 到 5 最稳k1 时对瞬时抖动非常敏感k 超过 7 会把距离较远的指纹点拉进来。缺失惩罚用20 * 20是为了让“少了 1 个 AP”和“某个 AP 差 5dBm”处在同一量级不至于让缺项完全主导距离。match 小于 3 的点直接跳过因为匹配的 AP 太少坐标几乎没有参考意义。参数建议值调低调高k3~5抖动变大位置被远处点拉偏缺失惩罚20 dBm默认缺项影响明显match 下限3不明显的点也多可用指纹太少4.3 基于 AP 坐标的加权质心定位指纹匹配依赖离线采集如果不想采集可以提前把每个 AP 的位置标在地图上用信号强度加权质心public static double[] locateByCentroid(ListApInfo aps) { double x 0, y 0, wSum 0; for (ApInfo ap : aps) { double w Math.pow(10.0, ap.rssi / 10.0); x w * ap.x; y w * ap.y; wSum w; } return new double[]{x / wSum, y / wSum}; }Math.pow(10.0, ap.rssi / 10.0)把 dBm 换算成线性功率。为什么不用 rssi 负值直接当权重例如 AP1 在 (-40, 0) 处信号 -45AP2 在 (40, 0) 处信号 -60前者应占主导但 -60 作为“数值”小于 -45直接加权会把位置推向 AP2。转成功率后-45 对应的功率是 -60 的 31.6 倍权重关系才正确。这个替换是加权质心定位里最容易踩的坑。4.4 滑动平均滤波与参数收敛定位结果直接用坐标会来回跳。我习惯在算法输出后接一个一阶低通private double[] smoothed; public double[] smooth(double[] point, double alpha) { if (smoothed null) { smoothed point.clone(); } else { smoothed[0] alpha * smoothed[0] (1 - alpha) * point[0]; smoothed[1] alpha * smoothed[1] (1 - alpha) * point[1]; } return smoothed; }alpha 越大轨迹越平滑但跟随真实移动越慢。做静态定位演示时可以用 0.9需要人走起来的话用 0.7。整个链路的顺序是原始 RSSI → 过滤 AP → kNN 或质心 → 滑动平均 → 坐标转换顺序不要反否则噪声会先污染平均后的坐标。5. 精度验证与可演示收尾误差计算、地图映射和答辩技巧5.1 用测试集计算 RMSE 与平均误差指纹点本身是训练数据不能用它们测精度。另选 20 个测试点每个点定位 5 次取平均坐标再计算与实际坐标的误差public static double rmse(Listdouble[] pairs) { double sum 0; for (double[] p : pairs) { double dx p[0] - p[2]; double dy p[1] - p[3]; sum dx * dx dy * dy; } return Math.sqrt(sum / pairs.size()); }pairs 中前两个值是估计坐标后两个值是真实坐标。用这种方法算出的误差通常比单点更可信。5.2 像素坐标与物理坐标的标定换算地图图片的像素坐标和日常定位用的米制坐标之间是线性映射。已知地图上两个参考点的物理坐标和像素坐标就能算出比例尺 scale米/像素。换算公式pixelX (physicalX - originX) / scalepixelY (physicalY - originY) / scale如果使用的地图图片 y 轴向下而物理坐标系 y 轴向上要额外对 pixelY 做翻转否则定位点会镜像到地图的另一侧。我在演示项目里会把 origin 取成地图左上角对应的物理坐标这样换算最简单。5.3 权限拒绝、扫描节流与 BSSID 随机化的排错现象原因处理第一次扫描为空定位权限未授予动态申请 FINE_LOCATIONstartScan() 返回 false两次扫描太近固定 2 秒间隔BSSID 显示 02:00:00:00:00:00系统限制真实地址改为同 SSID 合并或关闭随机化BSSID 随机化按设备厂商行为不同指纹库里的地址和当前扫描地址不一致时匹配度会突然下降。可以先把系统设置里该网络的随机 MAC 关掉再重新采集指纹这是最省事的做法。5.4 演示时值得提前固化的 3 个细节指纹库一定提前打进 assets。答辩现场人多WiFi 环境会变现采指纹大概率失败预置数据至少保证定位演示能出图。地图上叠加 RSSI 热力图。写一个简单的 View用 AP 信号强度做颜色渐变扫描到的 AP 画圆RSSI 越高颜色越暖。观众能直接看到“信号强的地方定位点密集”比只看坐标点更有说服力。把平滑后的坐标按时间画成轨迹线。边走边看路径是否连续如果跳变明显先把 k 调到 5alpha 调到 0.85再不行就去掉信号低于 -88 的 AP。边看路径是否平滑比单独展示一个点更有说服力。本文还有配套的精品资源点击获取
返回列表