简介:这是一套面向计算机相关专业毕业设计或课程设计场景的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 米。反解出 d:
public 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 发射功率和手机天线增益 |
| n | 2.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 Map<String, Integer> normalizeRssi(List<ScanResult> results) { Map<String, 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:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />前两个权限负责扫描和触发扫描;关键点是从 Android 6.0 开始,getScanResults()能返回多少内容取决于是否授予定位权限。只申请 WiFi 权限,结果集可能为空。如果 targetSdk 33 以上,还要动态申请NEARBY_WIFI_DEVICES,否则 BSSID 仍然拿不到。动态申请时按系统版本分批:
public void checkPermission(Activity activity) { List<String> 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/RSSI | ACCESS_FINE_LOCATION | 是 |
| Android 13+ | NEARBY_WIFI_DEVICES | 是 |
3.2 用 BroadcastReceiver 做 WiFi 扫描并取 RSSI
扫描结果通过系统广播返回,代码:
public class WifiScanner extends BroadcastReceiver { public interface OnScanResultListener { void onResult(Map<String, 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; } Map<String, 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.gradle:
android { 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 Map<String, Integer> rssi = new HashMap<>(); }从 assets 里的 JSON 加载:
public List<Fingerprint> loadFingerprints(Context context) throws JSONException, IOException { String json = readAsset(context, "fingerprints.json"); JSONArray array = new JSONArray(json); List<Fingerprint> 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"); Iterator<String> 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(List<Fingerprint> db, Map<String, Integer> current, int k) { List<double[]> dists = new ArrayList<>(); for (Fingerprint fp : db) { double sum = 0; int match = 0; for (Map.Entry<String, 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 最稳,k=1 时对瞬时抖动非常敏感,k 超过 7 会把距离较远的指纹点拉进来。缺失惩罚用20 * 20,是为了让“少了 1 个 AP”和“某个 AP 差 5dBm”处在同一量级,不至于让缺项完全主导距离。match 小于 3 的点直接跳过,因为匹配的 AP 太少,坐标几乎没有参考意义。
| 参数 | 建议值 | 调低 | 调高 |
|---|---|---|---|
| k | 3~5 | 抖动变大 | 位置被远处点拉偏 |
| 缺失惩罚 | 20 dBm | 默认 | 缺项影响明显 |
| match 下限 | 3 | 不明显的点也多 | 可用指纹太少 |
4.3 基于 AP 坐标的加权质心定位
指纹匹配依赖离线采集;如果不想采集,可以提前把每个 AP 的位置标在地图上,用信号强度加权质心:
public static double[] locateByCentroid(List<ApInfo> 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) 处信号 -45,AP2 在 (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(List<double[]> 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_LOCATION |
| startScan() 返回 false | 两次扫描太近 | 固定 2 秒间隔 |
| BSSID 显示 02:00:00:00:00:00 | 系统限制真实地址 | 改为同 SSID 合并,或关闭随机化 |
BSSID 随机化按设备厂商行为不同,指纹库里的地址和当前扫描地址不一致时,匹配度会突然下降。可以先把系统设置里该网络的随机 MAC 关掉,再重新采集指纹,这是最省事的做法。
5.4 演示时值得提前固化的 3 个细节
指纹库一定提前打进 assets。答辩现场人多,WiFi 环境会变,现采指纹大概率失败;预置数据至少保证定位演示能出图。
地图上叠加 RSSI 热力图。写一个简单的 View,用 AP 信号强度做颜色渐变,扫描到的 AP 画圆,RSSI 越高颜色越暖。观众能直接看到“信号强的地方定位点密集”,比只看坐标点更有说服力。
把平滑后的坐标按时间画成轨迹线。边走边看路径是否连续,如果跳变明显,先把 k 调到 5,alpha 调到 0.85,再不行就去掉信号低于 -88 的 AP。边看路径是否平滑,比单独展示一个点更有说服力。
本文还有配套的精品资源,点击获取