云计算百科
云计算领域专业知识百科平台

Android 多通道设备通信怎么统一:一套抽象打通蓝牙串口网络

蓝牙、串口、USB、网络十余种通道,业务层真要逐个适配吗?

前言

在测绘、工业物联网这类垂直行业的 Android 应用里,同一个业务逻辑往往要同时适配蓝牙经典、BLE、串口、TCP、UDP、USB、NTRIP 等一大堆物理通道。

更麻烦的是,同一台设备还可能同时走两种通道——比如串口负责读定位数据、WiFi 负责写差分。要是每种通道都各写一套连接管理、数据收发、状态通知逻辑,代码量会急剧膨胀,维护成本直接失控。

我在自研的这套 GIS/地图引擎里,最终落地了一套多通道设备通信统一抽象架构,用三层抽象把 20+ 种设备收敛成一套接口,业务层对底层通道完全无感。这篇也是本系列的收尾篇,把通信层的工程化收尾讲清楚。

一、问题本质

在垂直行业(测绘、工业物联网、医疗设备等)的 Android 应用中,一个常见需求是:同一个业务逻辑,需要适配多种物理通信通道。

以测绘行业为例,一台 RTK 定位设备可以通过以下任意一种方式与 Android 手簿通信:

  • 蓝牙经典(SPP)
  • BLE
  • USB 串口
  • TCP Socket(WiFi/有线)
  • UDP
  • 内置串口(手簿内置接收机)
  • NTRIP(网络差分)

而且,同一台设备可能同时使用两种通道——比如串口读数据、WiFi 写差分数据。如果每种通道都写一套独立的连接管理、数据收发、状态通知逻辑,代码会急剧膨胀,维护成本失控。

二、核心设计:三层抽象

2.1 架构总览

下面这张分层图是整个架构的骨架,从上到下依次是业务层、设备抽象层、IO 流抽象层和通道实现层。

┌──────────────────────────────────────────────────┐
│ 业务层 (API) │
│ 定位数据 · 设备状态 · 配置读写 · 差分数据发送 │
├──────────────────────────────────────────────────┤
│ 设备抽象层 (Device) │
│ 连接状态机 · 自动重连 · 事件分发 · 命令管理 │
├──────────────────────────────────────────────────┤
│ IO流抽象层 (IOStream) │
│ 异步读写线程 · 数据解析管道 · 帧过滤器 │
├──────────────────────────────────────────────────┤
│ 通道实现层 (Implementations) │
│ 蓝牙 · BLE · 串口 · Socket · UDP · USB · NTRIP │
└──────────────────────────────────────────────────┘

关键原则:每一层只依赖下一层的抽象接口,不依赖具体实现。

2.2 第一层:IO 流抽象

所有物理通道最终都归结为"字节流的读写"。因此,最底层抽象一个异步 IO 流:

下面这段先定义最底层的异步 IO 流抽象,只留 open/close/write 三个核心方法和一个读回调。

abstract class AsyncIOStream {

// 生命周期
abstract fun open()
abstract fun close()

// 写入
abstract fun write(data: ByteArray)

// 读取线程不断回调
protected var onReadCallback: ((ByteArray, Int) -> Unit)? = null

fun setOnReadCallback(callback: (ByteArray, Int) -> Unit) {
onReadCallback = callback
}
}

每种通道只需实现 open()、close()、write() 三个方法,并在内部启动读线程将数据通过回调抛出。

2.3 第二层:设备抽象

设备层是整个架构的核心。它管理连接状态机、自动重连、事件分发、数据解析器注册等横切关注点:

下面这段是设备抽象层的主体,用模板方法封装了连接/断开骨架,并内置了状态机和自动重连。

abstract class Device {

// ── 状态机 ──
enum class ConnectionState {
UNCONNECTED, // 未连接
CONNECTING, // 连接中
CONNECTED, // 已连接
DESTROYED // 已销毁
}

private var state = ConnectionState.UNCONNECTED
private val stateLock = Any()

// ── 自动重连 ──
private var autoReconnectEnabled = false
private var autoReconnectInterval = 3000L
private var reconnectJob: Job? = null

// ── 事件监听 ──
private val listeners = mutableListOf<DeviceEventListener>()

// ── 数据解析器链 ──
private val parsers = mutableListOf<Parser>()

// ── 公开方法 ──
fun connect() {
synchronized(stateLock) {
if (state != ConnectionState.UNCONNECTED) return
state = ConnectionState.CONNECTING
}
dispatchEvent(DeviceEvent.CONNECTING)
doConnect() // 模板方法,子类实现
}

fun disconnect() {
synchronized(stateLock) {
if (state == ConnectionState.DESTROYED) return
}
doDisconnect() // 模板方法,子类实现
synchronized(stateLock) {
state = ConnectionState.UNCONNECTED
}
dispatchEvent(DeviceEvent.DISCONNECTED)
cancelReconnect()
}

// ── 模板方法 ──
protected abstract fun doConnect()
protected abstract fun doDisconnect()

// ── 状态变更通知 ──
protected fun onConnected() {
synchronized(stateLock) {
state = ConnectionState.CONNECTED
}
dispatchEvent(DeviceEvent.CONNECTED)
}

protected fun onConnectFailed() {
synchronized(stateLock) {
state = ConnectionState.UNCONNECTED
}
dispatchEvent(DeviceEvent.CONNECT_FAILED)
tryAutoReconnect()
}

protected fun onDisconnected() {
synchronized(stateLock) {
state = ConnectionState.UNCONNECTED
}
dispatchEvent(DeviceEvent.DISCONNECTED)
tryAutoReconnect()
}

// ── 自动重连 ──
private fun tryAutoReconnect() {
if (!autoReconnectEnabled) return
cancelReconnect()
reconnectJob = CoroutineScope(Dispatchers.IO).launch {
delay(autoReconnectInterval)
connect()
}
}

fun setAutoReconnect(enabled: Boolean, intervalMs: Long = 3000L) {
autoReconnectEnabled = enabled
autoReconnectInterval = intervalMs
if (!enabled) cancelReconnect()
}

// ── 数据分发 ──
protected fun onDataReceived(data: ByteArray, length: Int) {
for (parser in parsers) {
try {
parser.parse(data, length)
} catch (e: Exception) {
// 解析异常不影响其他解析器
}
}
}

fun registerParser(parser: Parser) {
parsers.add(parser)
}

fun unRegisterParser(parser: Parser) {
parsers.remove(parser)
}
}

设计要点:

  • 状态机线程安全:用 synchronized 保护状态变更,确保并发安全
  • 模板方法模式:connect()/disconnect() 定义了骨架流程(状态变更 + 事件通知),子类只需实现 doConnect()/doDisconnect()
  • 自动重连:断线后自动触发重连,通过协程延迟执行,避免阻塞主线程
  • 解析器链:多个 Parser 可以注册到同一设备,实现"一次收数、多方解析"
  • 2.4 第三层:通道实现

    以蓝牙设备为例,展示具体通道如何接入:

    下面这段以蓝牙通道为例,只关心"怎么建连"和"怎么收发字节",业务逻辑一概不碰。

    class BluetoothDevice(private val address: String) : Device() {

    private var ioStream: BluetoothIOStream? = null

    override fun doConnect() {
    CoroutineScope(Dispatchers.IO).launch {
    try {
    val socket = BluetoothAdapter.getDefaultAdapter()
    .getRemoteDevice(address)
    .createRfcommSocketToServiceRecord(SPP_UUID)
    socket.connect()
    ioStream = BluetoothIOStream(socket)
    ioStream?.setOnReadCallback { data, len ->
    onDataReceived(data, len)
    }
    ioStream?.open()
    onConnected()
    } catch (e: Exception) {
    onConnectFailed()
    }
    }
    }

    override fun doDisconnect() {
    ioStream?.close()
    ioStream = null
    }

    fun sendData(data: ByteArray): Boolean {
    ioStream?.write(data) ?: return false
    return true
    }
    }

    关键:通道实现只关心"如何建立连接"和"如何收发字节",不关心业务逻辑。

    三、IO 适配器:读写分离

    在实际项目中,有一种特殊需求:读取和写入走不同的通道。

    例如,某款手簿的内置接收机通过串口输出定位数据,但写入差分数据需要通过 UDP 发送到另一个模块。此时,一个设备需要同时管理两个通道——一个负责读、一个负责写。

    3.1 适配器模式

    下面这段用适配器把"读通道、写通道、UDP 写通道"组合进同一个设备对象,对外还是一套接口。

    class IOAdapterDevice(
    private val readDevice: Device?, // 读通道
    private val writeDevice: Device?, // 写通道
    private val udpDevice: Device? // UDP 写通道(可选)
    ) : Device() {

    // 连接:依次连接所有通道
    override fun doConnect() {
    powerOn()
    readDevice?.connect()
    writeDevice?.connect()
    udpDevice?.connect()
    }

    // 断开:依次断开所有通道
    override fun doDisconnect() {
    readDevice?.disconnect()
    writeDevice?.disconnect()
    udpDevice?.disconnect()
    powerOff()
    }

    // 发送命令:走写通道
    fun sendCommand(cmd: Command) {
    writeDevice?.sendCommand(cmd)
    }

    // 发送差分数据:走写通道或 UDP
    fun sendDifferentialData(data: ByteArray) {
    writeDevice?.sendDifferentialData(data)
    udpDevice?.sendData(data)
    }

    // 读取数据:走读通道
    fun getReceivedData(): Data? {
    return readDevice?.getData()
    }
    }

    3.2 初始化命令队列

    某些设备在连接后需要发送一系列初始化命令,且命令之间有严格的时序要求(必须等上一条命令返回 OK 后才能发送下一条)。这通过"命令队列 + 响应驱动"实现:

    下面这段用"命令队列 + 响应驱动"保证初始化命令严格按序发送,队列跑完自动注销监听器。

    class CommandQueueManager(private val device: Device) {

    private val commandQueue = ArrayDeque<Command>()
    private var firstCommandSent = false

    fun enqueue(commands: List<Command>) {
    commandQueue.addAll(commands)
    // 注册一个临时解析器,监听设备响应
    device.registerParser(object : Parser {
    override fun parse(data: ByteArray, length: Int) {
    val response = String(data, 0, length)
    if (response.isEmpty()) return

    if (!firstCommandSent) {
    sendNext()
    firstCommandSent = true
    } else if (response.contains("OK") || response.contains("ERROR")) {
    sendNext()
    }

    if (commandQueue.isEmpty()) {
    device.unRegisterParser(this) // 队列清空,注销解析器
    }
    }

    override fun getData(): Data? = null
    })
    }

    private fun sendNext() {
    commandQueue.poll()?.let { cmd ->
    device.sendCommand(cmd)
    }
    }
    }

    设计亮点:解析器本身就是"观察者",当命令队列清空后自动注销,不污染正常的数据解析流程。

    四、构建器模式:统一创建入口

    20+ 种设备类型,如果每种都直接 new,创建逻辑会散落在业务代码各处。用构建器模式统一创建入口:

    下面这段用构建器把各类设备的创建收拢到统一入口,业务层不再直接 new 具体类型。

    abstract class DeviceBuilder {

    var content: DeviceContent? = null

    fun content(content: DeviceContent): DeviceBuilder {
    this.content = content
    return this
    }

    abstract fun build(): Device
    }

    // 蓝牙设备构建器
    class BluetoothDeviceBuilder : DeviceBuilder() {
    override fun build(): Device {
    return BluetoothDevice(content)
    }
    }

    // Socket 设备构建器
    class SocketDeviceBuilder : DeviceBuilder() {
    override fun build(): Device {
    return SocketDevice(content)
    }
    }

    // 使用
    val device = BluetoothDeviceBuilder()
    .content(deviceContent)
    .build()
    device.connect()

    更进一步,可以根据设备类型枚举,用工厂方法自动选择构建器:

    下面这段用工厂方法按设备类型枚举自动挑构建器,新增类型只需补一个分支。

    fun createBuilder(type: DeviceType): DeviceBuilder {
    return when (type) {
    DeviceType.BLUETOOTH -> BluetoothDeviceBuilder()
    DeviceType.BLE -> BleDeviceBuilder()
    DeviceType.SOCKET -> SocketDeviceBuilder()
    DeviceType.SERIAL_PORT -> SerialPortDeviceBuilder()
    DeviceType.UDP -> UdpDeviceBuilder()
    // … 其他类型
    else -> throw IllegalArgumentException("Unknown device type: $type")
    }
    }

    五、状态持久化:Content 机制

    设备的状态(配置参数、工作模式、连接信息等)需要持久化存储,并在进程间共享。这通过一个类似 SharedPreferences 的 Content 机制实现:

    下面这段定义了一套类似 SharedPreferences 的 Content 接口,并在基类里用 getSharedPreferences 落地。

    interface DeviceContent {
    fun <T> get(key: String, defaultValue: T): T
    fun <T> set(key: String, value: T)
    fun contains(key: String): Boolean
    fun remove(key: String)
    }

    class BaseDeviceContent(context: Context) : DeviceContent {

    private val prefs = context.getSharedPreferences("device_content", Context.MODE_PRIVATE)

    override fun <T> get(key: String, defaultValue: T): T {
    @Suppress("UNCHECKED_CAST")
    return when (defaultValue) {
    is String -> prefs.getString(key, defaultValue) as T
    is Int -> prefs.getInt(key, defaultValue) as T
    is Long -> prefs.getLong(key, defaultValue) as T
    is Float -> prefs.getFloat(key, defaultValue) as T
    is Boolean -> prefs.getBoolean(key, defaultValue) as T
    else -> throw IllegalArgumentException("Unsupported type")
    }
    }

    override fun <T> set(key: String, value: T) {
    prefs.edit().apply {
    when (value) {
    is String -> putString(key, value)
    is Int -> putInt(key, value)
    is Long -> putLong(key, value)
    is Float -> putFloat(key, value)
    is Boolean -> putBoolean(key, value)
    else -> throw IllegalArgumentException("Unsupported type")
    }
    }.apply()
    }

    // … 其他方法
    }

    业务层通过 Content 接口读写设备配置,完全不需要知道底层是 SharedPreferences 还是其他存储方式。

    六、架构收益

    下面是这套架构在几个关键维度上的收益速查表:

    维度收益
    扩展性 新增通道类型只需实现 IOStream + 继承 Device,零改动上层代码
    可维护性 每种通道的连接逻辑独立,互不干扰
    可测试性 可以 Mock Device 进行业务层测试,无需真实硬件
    复用性 IO 适配器模式让同一设备灵活组合不同通道
    稳定性 状态机 + 自动重连保证连接可靠性

    七、结论

    • 彻底解耦:把"通信通道"和"业务逻辑"拆开,业务层不感知底层走的是蓝牙还是串口
    • 三层职责:IO 流层只管收发字节,设备层只管连接状态,适配器层负责组合通道
    • 零改动扩展:新增通道类型只需实现 IOStream 并继承 Device,上层代码一行都不用改
    • 统一入口:构建器 + 工厂方法把 20+ 种设备的创建收拢到一处,避免散落
    • 可测可维护:Mock Device 即可做业务层单元测试,每种通道的连接逻辑互不影响

    以上就是这套多通道统一抽象架构的全部内容。如果你在项目中也遇到过"一种设备 N 种通道"的维护噩梦,欢迎在评论区聊聊你最后是怎么收场的。

    关键词标签:#Android #多通道通信 #蓝牙 #串口 #AIDL #架构设计 #Kotlin #设备抽象

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Android 多通道设备通信怎么统一:一套抽象打通蓝牙串口网络
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!