蓝牙网关与桥接:手机作为网关、MQTT桥接、数据转发、云端同步

说实话,做 BLE 开发做到一定阶段,你一定会遇到这个问题:手机怎么把蓝牙设备的数据送到云端?

我最早做智能家居项目时,客户要求手环数据必须实时同步到服务器。当时我想,这不简单吗?手机连上手环,拿到数据,再通过 Wi-Fi 发出去就行了。结果一上手才发现,这里面坑不少——手机屏幕一关,蓝牙断了;App 退到后台,数据丢了;网络不稳定,消息重复了……

嗯,今天我们就来聊聊这个场景:手机作为蓝牙网关,把 BLE 设备的数据桥接到云端。

手机当网关?听起来简单,做起来要小心

说白了,手机就是一个“中间人”。它一边通过 BLE 跟传感器、手环、温控器通信,另一边通过 MQTT 或者 HTTP 把数据推到服务器。

我个人的习惯是,不要指望手机 7×24 小时稳定工作。手机不是专用网关硬件,它会被用户杀掉进程、会没电、会切换网络。所以架构上一定要考虑断连重连、数据缓存、消息去重这些事。

核心思路:手机只做“临时转发”,不做“永久存储”。云端才是数据最终归宿。

整体架构长什么样?

我画了一张图,帮你理清数据流向。你看一眼就明白了:

BLE 设备 温湿度传感器 心率手环 BLE 设备 智能门锁 体脂秤 BLE 广播/连接 BLE 连接 📱 手机网关 BLE 扫描 + 连接 数据解析 + 缓存 MQTT 客户端 本地 SQLite 暂存 MQTT 发布 MQTT Broker EMQX / Mosquitto 消息路由 订阅转发 ☁️ 云端服务器 数据存储 / 告警 / 分析

你看,数据从 BLE 设备出发,经过手机这个“二传手”,通过 MQTT 到达 Broker,最后落地到云端。每一步都有讲究。

手机端:BLE 数据采集与缓存

先说说手机这边怎么做。我一般会开一个 Foreground Service,保证 App 被推到后台时 BLE 扫描和连接不会断。你想想看,用户把 App 切出去回个微信,蓝牙就断了,这体验多糟糕。

数据拿到之后,不要立刻发。为什么?因为网络可能不稳定。我习惯先写进本地 SQLite,打上一个时间戳和发送状态标记。然后由一个后台 Worker 定期检查,把未发送的数据批量推出去。

我的经验:本地缓存用 SQLite 就够了,别上 Room 那种重型框架。BLE 数据量不大,一张表搞定。字段就四个:id、device_mac、payload、timestamp、sent_flag。

MQTT 桥接:选对 QoS 级别

MQTT 这块,我踩过最大的坑就是 QoS 选择

一开始我图省事,全用 QoS 0。结果网络抖动时丢了一堆数据,用户投诉说体脂秤数据少了几天。后来改成 QoS 1,配合本地去重,才算稳下来。

我的建议是:

  • 普通传感器数据(温湿度、电量):QoS 0 就行,丢了也不心疼,反正下次还会上报。
  • 关键事件(门锁开关、报警):必须 QoS 1,配合本地 ACK 确认。
  • 慎用 QoS 2:手机端资源有限,QoS 2 的四次握手太耗电,而且容易造成消息堆积。

代码示例,发布消息的核心逻辑:

// MQTT 发布,带本地缓存兜底
fun publishWithCache(topic: String, payload: ByteArray, qos: Int = 1) {
    // 1. 先存本地
    val record = CacheRecord(
        deviceMac = extractMac(payload),
        payload = payload,
        timestamp = System.currentTimeMillis(),
        sentFlag = false
    )
    database.cacheDao().insert(record)

    // 2. 尝试发布
    mqttClient.publish(topic, payload, qos, false).whenComplete { _, throwable ->
        if (throwable == null) {
            // 发布成功,标记已发送
            database.cacheDao().markSent(record.id)
        } else {
            // 发布失败,等下次 Worker 重试
            Log.w("Gateway", "MQTT publish failed: ${throwable.message}")
        }
    }
}
注意:MQTT 客户端在手机端不要用长连接保活太频繁。我建议心跳间隔设到 60 秒以上,不然电量掉得飞快。曾经有个版本我设了 10 秒心跳,用户半天就掉了 30% 电,被骂惨了。

数据转发:格式统一与字段映射

不同 BLE 设备的数据格式千奇百怪。有的用 JSON,有的用二进制,有的甚至直接裸传字符串。我个人的做法是:在手机端做一次统一格式化,转成标准 JSON 再发出去。

举个例子,温湿度传感器原始数据可能是这样的字节数组:

// 原始 BLE 数据:0x02 0x1A 0x25 0x0E
// 解析后:
{
    "type": "temperature_humidity",
    "device_id": "BLE:AC:23:3F:12:88",
    "timestamp": 1698123456789,
    "data": {
        "temperature": 26.5,
        "humidity": 62.3
    }
}

云端那边只要认这一种格式就行,不用为每种设备写一套解析逻辑。说白了,手机就是那个“翻译官”。

云端同步:幂等性与时序保证

数据到了云端,最怕什么?重复和乱序

手机端可能因为网络问题重发消息,云端就会收到两条一模一样的数据。我的解决方案是:每条消息带一个全局唯一的 messageId,云端根据这个 ID 做去重。

另外,BLE 设备可能离线一段时间,然后一次性上报一堆历史数据。这时候时间戳就很重要了。云端不能简单按“到达时间”存,必须按“设备上报时间”排序。

字段 说明 是否必填
messageId 全局唯一,UUID 或 自增ID
deviceId 设备唯一标识,MAC 或自定义
deviceTimestamp 设备本地时间戳(毫秒)
gatewayTimestamp 手机收到数据的时间戳 推荐
payload 业务数据 JSON

避坑指南:我踩过的几个雷

  • 不要用 BLE 的 MTU 大小来限制 MQTT 消息。MQTT 是走 Wi-Fi/蜂窝的,跟 BLE MTU 没关系。我见过有人把 MQTT 消息切成 20 字节发,纯属多此一举。
  • 手机息屏后 BLE 扫描间隔会变长。Android 系统为了省电,会在息屏后自动拉长扫描周期。如果你需要实时性,记得申请 PARTIAL_WAKE_LOCK
  • MQTT 连接断开后要指数退避重连。不要每 1 秒重试一次,否则服务器会把你当攻击者。我一般用 1s、2s、4s、8s……最大 60 秒。
一句话总结:手机做网关,核心就三件事——收得稳(BLE 连接可靠)、存得住(本地缓存兜底)、发得准(MQTT 消息不重不漏)。

好了,这一章的内容就到这里。如果你正在做类似的网关项目,希望这些经验能帮你少走弯路。有什么问题,欢迎交流。


公众号:蓝海资料掘金营,微信deep3321