news 2026/5/4 11:28:29

毕业设计蓝牙定位实战:从 RSSI 测距到室内定位系统搭建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
毕业设计蓝牙定位实战:从 RSSI 测距到室内定位系统搭建


毕业设计蓝牙定位实战:从 RSSI 测距到室内定位系统搭建

很多 IoT 方向的毕业设计都会把“蓝牙室内定位”当选题,听起来门槛不高,真动手才发现 RSSI 像坐过山车——同一点一分钟内能差 10 dB。本文把我在实验室熬过的坑整理成一份可落地的“小系统”笔记,目标只有一个:让同选题的你两周内跑出能看的坐标,而不是在走廊里来回踱步怀疑人生。

一、背景痛点:RSSI 为什么不靠谱

  1. 信号波动大
    2.4 GHz 频段拥挤,Wi-Fi、微波炉、USB3.0 都在凑热闹;人体遮挡就能让 RSSI 掉 6 dB。

  2. 设备异构
    安卓 10 以后扫描返回的txPower字段常被厂商填 0,iPhone 干脆不开放原始 RSSI,导致同一段代码在不同手机上报的距离能差一倍。

  3. 多径效应
    实验室走廊长 30 m,金属门、玻璃墙来回反射,多径时延 < 400 ns,人眼看不出来,但 RSSI 会被“叠加”得忽高忽低。

  4. 采样粒度
    很多同学把startScan()周期设 100 ms,结果后台缓存没清空,同一包被重复上报,画出来的轨迹像蜘蛛网。

二、技术选型:BLE vs Wi-Fi vs UWB

指标BLEWi-Fi (RTT)UWB
硬件成本¥15/信标¥60/节点¥200+/节点
定位精度2–3 m(滤波后)1–2 m10 cm
功耗硬币电池 6 个月需持续关联 AP瞬时高,需充电
手机兼容全平台Android 9+极少手机内置
开发量最小最大

结论:毕业设计周期 3 个月、预算 < ¥500,BLE 是最能写出完整故事的选择;UWB 适合论文冲“高精度”,但硬件预算和调试时间直接翻倍。

三、系统架构速览

  1. 信标节点:nRF52832 模块,100 ms 周期发 iBeacon 帧,发射功率 0 dBm。
  2. 接收端:ESP32 做扫描网关,通过 MQTT 把原始 RSSI 发上位机。
  3. 上位机:Python 跑滤波 + 三边定位,可视化用 PyQtGraph,刷新 5 Hz。
  4. 校准层:首次启动时提示用户把信标放地上,走“之”字形采集 50 点,自动拟合环境因子 n 与参考 RSSI。

四、核心实现:代码级拆解

以下示例全部在 GitHub 开源,文末附地址。这里只贴关键段,保证能直接跑通。

4.1 ESP32 扫描网关(Arduino)

#include <BLEDevice.h> #include <WiFi.h> #include <PubSubClient.h> const char* mqtt_server = "192.168.31.99"; const int scan_window_ms = 800; const int report_every = 3; // 每 3 次扫描上报一次,省流量 WiFiClient espClient; PubSubClient mqtt(espClient); class AdvertisedCallback : public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice& ad){ if(strlen(ad.getName().c_str())==0) return; // 只收命名过的信标 String mac = ad.getAddress().toString().c_str(); int rssi = ad.getRSSI(); char buf[128]; snprintf(buf, sizeof(buf), "{\"mac\":\"%s\",\"rssi\":%d}", mac.c_str(), rssi); mqtt.publish("ble/raw", buf); } }; void setup(){ Serial.begin(115200); WiFi.begin("lab_wifi", "12345678"); while (WiFi.status() != WL_CONNECTED) delay(500); mqtt.setServer(mqtt_server, 1883); BLEDevice::init(""); BLEScan* pBLEScan = BLEDevice::getScan(); pBLE_scan->setAdvertisedDeviceCallbacks(new AdvertisedCallback()); pBLE_scan->setActiveScan(false); // 被动扫,省电 pBLE_scan->setInterval(80); 80*0.625=50 ms pBLE_scan->setWindow(40); 40*0.625=25 ms } void loop(){ if (!mqtt.connected()) reconnect_mqtt(); mqtt.loop(); BLEScanResults found = pBLEScan->start(scan_window_ms/10, false); pBLEScan->clearResults(); static int cnt = 0; if(++cnt >= report_every) cnt = 0; }

要点:

  • 被动扫描比主动扫描功耗降 30%,丢包率却无明显上升。
  • setWindow设成setInterval的一半,保证芯片有 50% 时间休眠。

4.2 Python 端:卡尔曼滤波 + 三边定位

import paho.mqtt.client as mqtt import numpy as np from filterpy.kalman import KalmanFilter import json, math, time # 环境因子,先写死,后面自动校准 N = 2.8 RSSI_1M = -59 def rssi_to_dist(rssi): return 10 ** ((RSSI_1M - rssi)/(10 * N)) class Beacon: def __init__(self, mac, x, y): self.mac = mac self.x, self.y = x, y self.kf = KalmanFilter(dim_x=1, dim_z=1) self.kf.x = np.array([0.]) self.kf.F = np.eye(1) self.kf.H = np.eye(1) self.kf.P *= 10. self.kf.R = 8 # 观测噪声 self.last_update = time.time() def update(self, rssi): self.kf.predict() self.kf.update(np.array([rssi])) self.last_update = time.time() def get_smoothed_rssi(self): return self.kf.x[0] beacons = { "aa:bb:cc:dd:01": Beacon("aa:bb:cc:dd:01", 0, 0), "aa:bb:cc:dd:02": Beacon("aa:bb:cc:dd:02", 6, 0), "aa:bb:cc:dd:03": Beacon("aa:bb:cc:dd:03", 0, 6), } def trilaterate(dist_vec): (x1,y1,d1),(x2,y2,d2),(x3,y3,d3) = dist_vec A = 2*np.array([[x2-x1, y2-y1], [x3-x1, y3-y1]]) b = np.array([d1**2 - d2**2 + x2**2 - x1**2 + y2**2 - y1**2, d1**2 - d3**2 + x3**2 - x1**2 + y3**2 - y1**2]) try: return np.linalg.solve(A, b) except np.linalg.LinAlgError: return None def on_message(client, userdata, msg): try: j = json.loads(msg.payload) mac, rssi = j['mac'], int(j['rssi']) if mac not in beacons: return beacons[mac].update(rssi) except: return client = mqtt.Client() client.on_message = on_message client.connect("192.168.31.99") client.subscribe("ble/raw") client.loop_start() while True: time.sleep(0.2) dist_vec = [] for b in beacons.values(): if time.time() - b.last_update < 2: # 只收新鲜度 <2 s 的数据 d = rssi_to_dist(b.get_smoothed_rssi()) dist_vec.append((b.x, b.y, d)) if len(dist_vec) >= 3: pos = trilaterate, dist_vec) if pos is not None: print(f"X={pos[0]:.2f} Y={pos[1]:.2f}")

要点:

  • 卡尔曼只维 1 维 RSSI,计算量 < 200 µs,树莓派 Zero 也能跑。
  • 三边定位用线性最小二乘,避免牛顿迭代初值敏感问题。

4.3 自动校准脚本(节选)

def collect_walk(): pts = [] print("请沿折线慢走,每步 1 米,共 10 步,按回车确认") for i in range(10): input(f"站在 ({i},0) 按回车") snapshot = {mac: b.get_smoothed_rssi() for mac, b in beacons.items()} pts.append((i, 0, snapshot)) # 用最小二乘拟合 N、RSSI_1M from scipy.optimize import least_squares def err(p): n, r = p e = [] for x, y, snapshot in pts: for mac, rss in snapshot.items(): bx, by = beacons[mac].x, beacons[mac].y d_real = math.hypot(x-bx, y-by) d_est = 10**((r - rss)/(10*n)) e.append(d_real - d_est) return e res = least_squares(err, [2, -60]) print("拟合完成 N =", res.x[0], "RSSI_1M =", res.x[1])

五、性能与安全考量

  1. 采样频率 vs 功耗
    ESP32 扫描占空比 25% 时整机电流 80 mA;降到 10% 可压到 35 mA,但丢包率由 3% → 8%,需权衡。

  2. MAC 地址随机化
    iOS 14+ 默认每 15 min 轮换 MAC,解决方案:把信标名字改成beacon_001,用设备名当主键,避开 MAC。

  3. 数据完整性
    MQTT 走明文,毕设演示无所谓;若校赛要求隐私,加 TLS 证书,ESP32 用WiFiClientSecure,内存多占 30 k。

  4. 滤波延迟
    卡尔曼过程噪声 Q 调大,平滑减弱但延迟降低;现场走秀时 Q=2 跟踪更手,静态展示 Q=0.5 轨迹更丝滑。

六、生产环境避坑指南

  • 金属遮挡:铁门会反射信号,导致“镜像”信标;部署时让基站离墙 ≥ 30 cm。
  • 地面校准:瓷砖与木地板对 2.4 GHz 吸收差 1 dB,别偷懒,换场地就重跑collect_walk()
  • 并发扫描冲突:同一房间 3 组同学同时演示,把蓝牙信道 37/38/39 占满;错开扫描窗口或把广播间隔提到 200 ms。
  • 天线方向:nRF 52832 板载 PCB 天线垂直时 RSSI 最强,挂天花板记得让天线朝下。
  • 参考点布设:三边定位尽量让基站夹角 90°–120°,钝角三角形 GDOP 爆炸,误差轻松翻倍。

七、可继续玩的优化方向

  1. 把卡尔曼升到 3 维 (RSSI + 加速度),利用步子检测抑制“人墙遮挡”瞬跌。
  2. 用粒子滤波融合地磁指纹,BLE 粗定位 + 磁场精修,1 m 内稳定度可再提 30%。
  3. 写个安卓端,把 MQTT 可视化搬到 Flutter,现场老师手机装 App 就能看轨迹,答辩加分。
  4. 调参工具:把滤波 Q/R 做成滑动条,实时看轨迹抖动,半分钟就能找到最优噪声矩阵。

八、小结

整个流程跑通,硬件成本不到一顿火锅钱,代码量 400 行左右,却能把“室内定位”从 PPT 变成可演示的 Demo。别急着堆高大上算法,先让 RSSI 稳定、坐标刷新不飘,再谈精度。下一步,把滤波参数拎出来多轮 A/B 测试,或者把基站扩到 5 个用最小圆覆盖,误差椭圆就能再缩一圈。祝你毕业设计现场不被老师问“这轨迹怎么穿墙了”,而是收获一句——“咦,还挺准!”


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/3 12:59:45

Nugget:探索高效下载的并行传输解决方案

Nugget&#xff1a;探索高效下载的并行传输解决方案 【免费下载链接】nugget minimalist wget clone written in node. HTTP GET files and downloads them into the current directory 项目地址: https://gitcode.com/gh_mirrors/nu/nugget 在当今数据驱动的时代&#…

作者头像 李华
网站建设 2026/5/1 3:34:11

零成本企业级字体解决方案:Source Han Serif CN开源字体全指南

零成本企业级字体解决方案&#xff1a;Source Han Serif CN开源字体全指南 【免费下载链接】source-han-serif-ttf Source Han Serif TTF 项目地址: https://gitcode.com/gh_mirrors/so/source-han-serif-ttf 您是否正在为商业字体授权费用居高不下而困扰&#xff1f;是…

作者头像 李华
网站建设 2026/5/1 6:36:08

Face3D.ai Pro效果展示:从手机自拍到可动画3D头像的端到端生成效果集

Face3D.ai Pro效果展示&#xff1a;从手机自拍到可动画3D头像的端到端生成效果集 1. 这不是“修图”&#xff0c;是把你的脸“搬进三维世界” 你有没有试过用手机随手拍一张自拍&#xff0c;然后下一秒——这张照片就变成了一个能眨眼、能转头、能在Blender里做表情动画的3D头…

作者头像 李华
网站建设 2026/5/1 3:52:49

Hunyuan-MT-7B镜像免配置部署教程:开箱即用多语翻译Web界面

Hunyuan-MT-7B镜像免配置部署教程&#xff1a;开箱即用多语翻译Web界面 1. 为什么这款翻译模型值得你立刻试试&#xff1f; 你有没有遇到过这些情况&#xff1a; 要把一份30页的中英双语合同翻成维吾尔语&#xff0c;但现有工具要么断句错乱&#xff0c;要么漏译专业术语&am…

作者头像 李华
网站建设 2026/4/30 17:53:53

手把手教你用DeepSeek-R1-Distill-Llama-8B实现SQL转自然语言

手把手教你用DeepSeek-R1-Distill-Llama-8B实现SQL转自然语言 你是否遇到过这样的场景&#xff1a;数据库里躺着几十张表&#xff0c;业务同事甩来一条SQL问“这句到底在查什么”&#xff0c;而你得花5分钟逐行解析JOIN条件、WHERE过滤逻辑和GROUP BY聚合意图&#xff1f;或者…

作者头像 李华
网站建设 2026/5/1 14:22:05

Face3D.ai Pro惊艳案例:为听障人士生成唇动同步3D人脸驱动数据集

Face3D.ai Pro惊艳案例&#xff1a;为听障人士生成唇动同步3D人脸驱动数据集 1. 这不是普通的人脸重建&#xff0c;而是沟通的桥梁 你有没有想过&#xff0c;一张静态照片&#xff0c;能变成会说话的3D人脸&#xff1f;不是动画师一帧一帧手调出来的那种&#xff0c;而是AI自…

作者头像 李华