【移动端Android 蓝牙】经典蓝牙配对与连接 SPP规范架构+唯一标识符UUID+RFCOMM端口号+Android代码实现

AndroidStudio经典蓝牙协议SPP通信 2、connectToDevice方法中判断手机是否打开蓝牙,若无则请求用户允许打开,用户处理结果在onActivityResult方法中响应;蓝牙打开后,还需判断是否授予应用蓝牙权限,若无则请求用户允许授予,用户处理结果在onRequestPermissionsResult方法中响应。4、connectToDeviceUnderpermission方法中先通过设备MAC地址获取设备,若未知设备SPP服务特性则可通过设备getUuids方法获取;点击按钮后调用connectToDevice方法。 阅读详情

配对

device.createBond()

  • device.createBond():
    • 这是 Android SDK 提供的标准 API,用于异步发起配对请求。
    • 非阻塞:调用此方法后,函数会立即返回,不会等待配对完成。实际的配对过程(包括弹出系统对话框让用户输入 PIN 码、底层密钥交换)是在后台进行的。
    • 系统交互:如果设备需要 PIN 码且未缓存,Android 系统会自动弹出一个原生对话框,要求用户输入密码。开发者无法直接在这个函数中自定义这个对话框(除非使用隐藏 API 或辅助广播,但标准做法是依赖系统弹窗)。
  • 权限检查 (BLUETOOTH_CONNECT):
    • 从 Android 12 (API 31) 开始,发起配对操作必须持有 BLUETOOTH_CONNECT 权限,否则会抛出 SecurityException。
  • 局限性:
    • 这个函数没有返回值告诉你配对是否成功。它只是发出了“我想配对”的指令。
    • 要得知配对结果(成功、失败、用户取消),必须依赖广播接收器。
  • 发起配对函数(pairDevice):
fun pairDevice(device: BluetoothDevice) {
    // 1. 权限检查
    if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return
    
    try {
        Log.d("BluetoothScanner", "Attempting to pair with ${device.address}...")
        
        // 2. 核心调用:发起配对请求
        device.createBond()
        
    } catch (e: SecurityException) {
        Log.e("BluetoothScanner", "Pairing failed: ${e.message}")
    }
}
  • 监听配对状态(ACTION_BOND_STATE_CHANGED):由于 createBond() 是异步的,代码通过注册广播接收器 (scanReceiver) 来监听配对状态的变化。
private val scanReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        when (intent.action) {
            // ... 其他动作 ...
            
            // 监听配对状态变化
            BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
                val device: BluetoothDevice? = getDeviceFromIntent(intent)
                // 获取新的配对状态
                val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
                
                // 回调通知 UI 层
                device?.let { onBondStateChanged?.invoke(it, bondState) }
            }
        }
    }
}

总结流程图

蓝牙设备 用户 Android 系统 BluetoothScanner App 蓝牙设备 用户 Android 系统 BluetoothScanner App 用户输入 "1234" 并确认 alt [配对成功] [配对失败/取消] pairDevice(device) device.createBond() 弹出 PIN 码输入框 广播 ACTION_BOND_STATE_CHANGED (BONDING) onBondStateChanged(BONDING) 验证通过,交换密钥 广播 ACTION_BOND_STATE_CHANGED (BONDED) onBondStateChanged(BONDED) connectToDevice(device) (自动或手动) 验证失败 或 用户取消 广播 ACTION_BOND_STATE_CHANGED (NONE) onBondStateChanged(NONE)

“隐式”配对

  • 传统的配对流程(搜索 -> 点击 -> 输入PIN码/确认数字)对物联网(IoT)设备太繁琐。下面是几种“隐式”方案:
方案名称实现方式底层蓝牙机制例子
NFC 触碰配对手机贴设备,自动连OOB (NFC传密钥)NFC Touch-to-Pair
二维码配对App扫码,自动连OOB (视觉传密钥)Apple HomeKit
物理按钮配对长按设备按钮,手机自动连Just Works (依赖物理操作作为验证)
邻近自动配对打开盒子,手机弹窗秒连 (如AirPods)OOB (利用私有芯片/协议交换密钥)
无感重连以前配过,下次自动连Bonding (使用已存储的密钥)
  • 在大多数标准的蓝牙开发库(如 Android 的 BluetoothDevice.createBond() 或 iOS 的 CBCentralManager)中,无pairImplicitly() 方法。一般是在代码中将获取到的 OOB 数据传入标准的配对接口(例如 Android 的 setPairingConfirmation 或 createBond 配合 OOB 数据)。在UI层不显示系统默认的配对弹窗(部分系统允许suppress弹窗,或者通过自定义UI覆盖)。

连接

  • 蓝牙 SPP (Serial Port Profile) 是蓝牙技术中最经典、应用最广泛的配置文件之一。它的核心作用是在两个蓝牙设备之间模拟一条传统的串行电缆(如 RS-232),从而让现有的串口应用程序能够无需修改或仅需少量修改,即可通过无线方式进行数据传输。
区别项低功耗蓝牙(BLE)经典蓝牙(SPP 串行端口协议)
蓝牙版本蓝牙版本 >= 4.0,又称蓝牙低功耗、蓝牙智能经典蓝牙2.0 或更早版本,经典配对模式在两台蓝牙设备之间建立虚拟串口数据连接,提供一种简单而直接的数据传输通路用于数据交换。
传输距离距离较短,通信范围一般为10米左右。通信距离较远,可达100米。
传输速率低带宽:BLE数据速率通常只有1Mbps或更低,主要用于低速度的控制类或监测类数据交换SPP可以达到3Mbps或更高。提供较高的数据传输速率,适用于需要快速传输大量数据的应用,例如音频流、文件传输等。
蓝牙功耗极低功耗:可达数月或几年的电池续航时间较高功耗:通常不及BLE省电,电池续航时间较短。经典蓝牙设计注重速度和传输容量,而忽略了功耗的优化。
硬件成本低成本:BLE芯片和模块价格低廉。相比BLE价格稍高一些。
开发难度低复杂度:协议栈简单,易于开发。较高复杂度:协议栈较复杂,开发难度较大。
典型应用可穿戴设备(智能手表、手环)、智能家居、智能门锁、健康监测、环境监测设备等。蓝牙打印机、蓝牙串口替代、蓝牙POS机、蓝牙耳机、高速数据交换(图像传输、文件传输)等。
App Inventor 2 组件BlueToothLE拓展
无需配对,直接出现在设备列表中,连接后即可使用
蓝牙客户端 / 蓝牙服务端
必须事先配对(手机设置 -> 蓝牙)成功后,才能出现在设备列表中

连接建立流程简述

  • 初始化:双方设备开启蓝牙。
  • 广播与扫描:服务端处于可被发现状态,客户端扫描设备。
  • 配对 (Pairing):输入 PIN 码(如 “1234” 或 “0000”)进行身份验证和密钥交换。
  • 服务发现 (SDP):客户端查询服务端的 SDP 记录,找到 SPP 服务对应的 RFCOMM 信道号。
  • 建立连接:客户端向该信道发起 RFCOMM 连接请求。
  • 数据传输:连接成功后,双方即可像操作串口一样收发数据。
  • 断开:任意一方关闭连接。

SPP规范架构

  • 工作模式:通常采用 C/S (客户端/服务器) 模式。
  • 依赖关系:SPP 基于 RFCOMM 协议实现,而 RFCOMM 又依赖于 L2CAP(逻辑链路控制与适配协议)。
    • L2CAP:负责数据的分段与重组,提供多路复用。
    • RFCOMM:模拟串行端口信号(如 RTS, CTS 等控制信号),提供可靠的面向连接的数据传输,本质上是在蓝牙上仿真 RS-232 串口。
    • SDP (服务发现协议):在连接建立前,主设备通过 SDP 查询从设备是否支持 SPP 服务,并获取对应的信道号(Channel Number)。
  • 标准连接 (Standard Connection):
// 尝试标准连接 (会自动进行 SDP 查询获取通道号)
try {
    socket = device.createRfcommSocketToServiceRecord(DEFAULT_UUID)
    socket?.connect()
    Log.d("BluetoothScanner", "Standard connection successful.")
} catch (e: IOException) {
    // ...进入 fallback 策略
}
  • createRfcommSocketToServiceRecord(DEFAULT_UUID):
    • 这是 Android 提供的标准 API。
    • 关键动作:调用此方法时,Android 蓝牙栈会自动向目标设备发起 SDP 查询。它会问:“嘿,你的 UUID ...1101 (SPP) 服务在哪个 RFCOMM 通道上?”
    • 目标设备回复一个端口号(例如 Channel 1, 5, 等)。
    • 然后 connect() 会尝试连接到该特定端口。
  • 优点:符合规范,自动适配不同设备的端口配置。
    • 透明传输:一旦连接建立,数据就像通过物理串口线一样传输,应用层只需读写数据流,无需关心蓝牙包的结构。
    • 可靠性高:基于 RFCOMM 的确认机制,保证数据不丢失、顺序正确(适合控制指令、日志传输等场景)。
    • 兼容性强:完美兼容传统的串口应用(如 Putty, 串口调试助手,以及各类嵌入式 UART 程序)。
    • 点对点通信:标准的 SPP 通常是一对一连接(虽然蓝牙底层支持微网,但 SPP 逻辑上多为直连)。
    • 带宽与功耗:
      • 带宽:经典蓝牙(BR/EDR)的 SPP 理论速率可达 2-3 Mbps,实际应用中通常在 100KB/s - 200KB/s 左右,远高于 BLE。
      • 功耗:由于属于经典蓝牙,其功耗显著高于 BLE (Bluetooth Low Energy),不适合电池供电且需长期待机的设备。
  • 缺点:在某些安卓版本或特定的蓝牙硬件固件上,SDP 查询可能会超时、失败或被防火墙拦截,导致抛出 IOException。

SPP 的唯一标识符 (UUID)

// Standard SPP UUID.
private val DEFAULT_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
  • 含义:00001101-0000-1000-8000-00805F9B34FB 是蓝牙技术联盟(SIG)为 Serial Port Profile (SPP) 分配的官方标准 UUID。
  • 作用:当客户端(手机)想要连接一个 SPP 服务时,必须使用这个 UUID 告诉系统:“我要找的是串口服务,而不是耳机(HFP)或文件传输(FTP)服务”。
  • 底层机制:这个 UUID 用于后续的 SDP (Service Discovery Protocol) 查询,以找到对方设备开放的具体通信通道号(RFCOMM Channel)。

“端口号”(RFCOMM Channel Number)

  • 端口号是一个整数,范围通常在 1 到 30 之间(蓝牙协议规范允许 1-30,部分实现可能支持更多,但 30 是通用标准上限),用于在单个蓝牙物理设备上区分不同的串行服务。
    • 蓝牙设备可以同时运行多个服务(例如:一个 SPP 串口服务、一个文件传输服务、一个耳机服务)。
    • 每个服务都必须绑定到一个唯一的 RFCOMM 通道号上。
    • 当客户端(手机)想要连接 SPP 服务时,它必须知道目标设备把 SPP 服务“挂”在了哪个房间号(通道号)上。
  • 为了方便理解,可以将其与网络编程中的 TCP 端口做类比,但它们处于不同的协议层:
特性TCP/IP 端口 (Port)蓝牙 RFCOMM 通道 (Channel)
所属协议TCP/UDPRFCOMM (基于 L2CAP)
范围0 - 655351 - 30 (SPP 常用范围)
功能区分同一 IP 下的不同应用程序区分同一蓝牙设备下的不同串行服务
常见默认值HTTP(80), SSH(22)SPP 通常为 1
发现方式已知或通过扫描通过 SDP (服务发现协议) 查询,或暴力遍历
相关代码
  • 如果标准方法失败,代码进入 catch 块,尝试更“暴力”但往往更有效的方法:
// Fallback 策略 A: 尝试通过 SDP 记录手动获取通道号 (更稳健的反射)
val method = device.javaClass.getMethod("createRfcommSocket", Int::class.javaPrimitiveType)

// 尝试常见的通道号 1 到 30
for (channel in 1..30) {
    try {
        socket = method.invoke(device, channel) as BluetoothSocket
        socket?.connect()
        Log.d("BluetoothScanner", "Fallback successful on channel: $channel")
        break
    } catch (e2: IOException) {
        socket?.close()
        socket = null
    }
}
  • createRfcommSocket(int channel) (反射调用):
    • 这是一个 隐藏 API (Hidden API),不在公开的 Android SDK 中,但在底层存在。
    • 区别:它跳过 SDP 查询,直接尝试连接指定的 RFCOMM 通道号。
  • 遍历通道 (1-30):
    • 由于跳过了 SDP,代码不知道具体的端口号。大多数蓝牙模块(如 HC-05, JDY 系列)默认将 SPP 服务绑定在 Channel 1,但也有部分设备使用其他端口(如 3, 5, 7 等)。
    • 代码通过循环尝试 1 到 30 之间的所有可能端口,一旦某个端口连接成功 (connect() 不抛异常),就立即停止循环并认定连接成功。

Android项目代码

bluetoothscan

package com.example.bluetoothscan

import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import java.io.IOException
import java.util.UUID

class BluetoothScanner(val context: Context) {

    private val bluetoothAdapter: BluetoothAdapter? by lazy {
        val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothManager.adapter
    }

    private var onDeviceFound: ((ScanDevice) -> Unit)? = null
    private var onScanStarted: (() -> Unit)? = null
    private var onBondStateChanged: ((BluetoothDevice, Int) -> Unit)? = null
    private var onConnectionStateChanged: ((BluetoothDevice, Boolean) -> Unit)? = null

    private var isAutoRepeat = false
    private val handler = Handler(Looper.getMainLooper())
    private val reScanDelayMs = 3000L

    // Standard SPP UUID.
    private val DEFAULT_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
    private var bluetoothSocket: BluetoothSocket? = null

    private val scanReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            when (intent.action) {
                BluetoothDevice.ACTION_FOUND -> {
                    val device: BluetoothDevice? = getDeviceFromIntent(intent)
                    val rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE).toInt()
                    device?.let { onDeviceFound?.invoke(ScanDevice(it, rssi, it.bondState)) }
                }
                BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
                    if (isAutoRepeat) {
                        handler.postDelayed({ if (isAutoRepeat) startDiscoveryProcess() }, reScanDelayMs)
                    }
                }
                BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
                    val device: BluetoothDevice? = getDeviceFromIntent(intent)
                    val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR)
                    device?.let { onBondStateChanged?.invoke(it, bondState) }
                }
            }
        }
    }

    private fun getDeviceFromIntent(intent: Intent): BluetoothDevice? {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
        } else {
            @Suppress("DEPRECATION")
            intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
        }
    }

    fun setOnDeviceFoundListener(listener: (ScanDevice) -> Unit) { onDeviceFound = listener }
    fun setOnScanStartedListener(listener: () -> Unit) { onScanStarted = listener }
    fun setOnBondStateChangedListener(listener: (BluetoothDevice, Int) -> Unit) { onBondStateChanged = listener }
    fun setOnConnectionStateChangedListener(listener: (BluetoothDevice, Boolean) -> Unit) { onConnectionStateChanged = listener }

    fun startScan(autoRepeat: Boolean = true) {
        this.isAutoRepeat = autoRepeat
        val filter = IntentFilter().apply {
            addAction(BluetoothDevice.ACTION_FOUND)
            addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
            addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
        }
        try {
            context.registerReceiver(scanReceiver, filter)
        } catch (e: Exception) { /* Ignore */ }

        startDiscoveryProcess()
    }

    private fun startDiscoveryProcess() {
        val adapter = bluetoothAdapter ?: return
        if (!adapter.isEnabled || ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) return

        if (adapter.isDiscovering) adapter.cancelDiscovery()

        onScanStarted?.invoke()
        adapter.startDiscovery()
    }

    fun stopScan() {
        isAutoRepeat = false
        handler.removeCallbacksAndMessages(null)
        if (bluetoothAdapter?.isDiscovering == true) {
             if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED) {
                bluetoothAdapter?.cancelDiscovery()
            }
        }
        try {
            context.unregisterReceiver(scanReceiver)
        } catch (e: Exception) { /* Ignore */ }
    }

    fun pairDevice(device: BluetoothDevice) {
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return
        try {
            Log.d("BluetoothScanner", "Attempting to pair with ${device.address}...")
            device.createBond()
        } catch (e: SecurityException) {
            Log.e("BluetoothScanner", "Pairing failed: ${e.message}")
        }
    }

    fun connectToDevice(device: BluetoothDevice) {
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            Log.e("BluetoothScanner", "Permission denied")
            return
        }

        Thread {
            disconnect() // Disconnect from any previously connected device.
            var socket: BluetoothSocket? = null
            try {
                if (bluetoothAdapter?.isDiscovering == true) {
                    bluetoothAdapter?.cancelDiscovery()
                }

                Log.d("BluetoothScanner", "Connecting to ${device.address} with UUID: $DEFAULT_UUID")

                // 1. 尝试标准连接 (会自动进行 SDP 查询获取通道号)
                try {
                    socket = device.createRfcommSocketToServiceRecord(DEFAULT_UUID)
                    socket?.connect()
                    Log.d("BluetoothScanner", "Standard connection successful.")
                } catch (e: IOException) {
                    Log.w("BluetoothScanner", "Standard connection failed: ${e.message}. Attempting fallback strategies...")
                    socket?.close()
                    socket = null

                    // 2. Fallback 策略 A: 尝试通过 SDP 记录手动获取通道号 (更稳健的反射)
                    // 某些设备 createRfcommSocketToServiceRecord 失败但 SDP 记录是存在的
                    val method = device.javaClass.getMethod("createRfcommSocket", Int::class.javaPrimitiveType)

                    // 尝试常见的通道号 1 到 30 (SPP 通常在 1-30 之间)
                    // 注意:盲目遍历可能会慢,建议优先尝试从 SDP 解析,这里为了演示简化为遍历
                    // 更好的做法是先解析 SDP 记录获取准确的 channel
                    for (channel in 1..30) {
                        try {
                            Log.d("BluetoothScanner", "Trying fallback channel: $channel")
                            socket = method.invoke(device, channel) as BluetoothSocket
                            socket?.connect()
                            Log.d("BluetoothScanner", "Fallback successful on channel: $channel")
                            break
                        } catch (e2: IOException) {
                            socket?.close()
                            socket = null
                            // 继续尝试下一个通道
                        }
                    }

                    if (socket == null || !socket.isConnected) {
                        throw IOException("All fallback channels failed.")
                    }
                }

                // 连接成功处理
                bluetoothSocket = socket
                Log.d("BluetoothScanner", "Connected successfully to ${device.address}")
                handler.post { onConnectionStateChanged?.invoke(device, true) }

                // 在这里启动数据读写线程...

            } catch (e: Exception) {
                Log.e("BluetoothScanner", "Connection failed finally: ${e.message}", e)
                // 打印具体的异常堆栈,这对判断是 "Connection refused" 还是 "Timeout" 很重要
                try {
                    socket?.close()
                } catch (closeException: IOException) {
                    // Ignore
                }
                handler.post { onConnectionStateChanged?.invoke(device, false) }
            }
        }.start()
    }

    fun disconnect() {
        try {
            bluetoothSocket?.close()
            bluetoothSocket = null
        } catch (e: IOException) {
            Log.e("BluetoothScanner", "Error closing socket: ${e.message}")
        }
    }

    fun getBondedDevices(): List<ScanDevice> {
        val adapter = bluetoothAdapter ?: return emptyList()
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            return emptyList()
        }
        return adapter.bondedDevices.map { ScanDevice(it, -1, it.bondState) }
    }
}

MainActivity.kt

package com.example.bluetoothscan

import android.Manifest
import android.bluetooth.BluetoothDevice
import android.content.pm.PackageManager
import android.media.MediaPlayer
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import com.example.bluetoothscan.ui.theme.BluetoothscanTheme

class MainActivity : ComponentActivity() {

    private lateinit var bluetoothScanner: BluetoothScanner
    private val discoveredDevices = mutableStateListOf<ScanDevice>()

    private var isCyclingConnections = false
    private var bondedDevicesArray: Array<BluetoothDevice> = arrayOf()
    private var currentConnectionIndex = 0
    private var mediaPlayer: MediaPlayer? = null

    private val handler = Handler(Looper.getMainLooper())
    private val cycleRunnable = object : Runnable {
        override fun run() {
            if (bondedDevicesArray.isNotEmpty()) {
                val device = bondedDevicesArray[currentConnectionIndex]
                
                bluetoothScanner.connectToDevice(device)
                
                // 移动到下一个索引,实现无限循环
                currentConnectionIndex = (currentConnectionIndex + 1) % bondedDevicesArray.size
                
                // 每5秒触发下一次连接
                handler.postDelayed(this, 5000)
            } else {
                isCyclingConnections = false
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 初始化 MediaPlayer
        mediaPlayer = MediaPlayer.create(this, R.raw.connection_failed)

        bluetoothScanner = BluetoothScanner(this)

        bluetoothScanner.setOnDeviceFoundListener { device ->
            val index = discoveredDevices.indexOfFirst { it.device.address == device.device.address }
            if (index == -1) {
                discoveredDevices.add(device)
            }
        }

        bluetoothScanner.setOnScanStartedListener {
            discoveredDevices.clear()
            val bondedDevices = bluetoothScanner.getBondedDevices()
            discoveredDevices.addAll(bondedDevices)
        }

        bluetoothScanner.setOnBondStateChangedListener { device, state ->
            val index = discoveredDevices.indexOfFirst { it.device.address == device.address }
            if (index != -1) {
                discoveredDevices[index] = discoveredDevices[index].copy(bondState = state)
            }
        }

        bluetoothScanner.setOnConnectionStateChangedListener { device, connected ->
            if (!connected) {
                val message = "连接失败: ${device.name ?: device.address}"
                Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
                // 播放失败音效
                mediaPlayer?.start()
            }
        }

        setContent {
            BluetoothscanTheme {
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
                    BluetoothScanScreen(
                        devices = discoveredDevices,
                        onPairDevice = { bluetoothScanner.pairDevice(it) },
                        onConnectDevice = {
                            if (isCyclingConnections) {
                                Toast.makeText(this, "正在无限循环连接中...", Toast.LENGTH_SHORT).show()
                            } else {
                                bluetoothScanner.connectToDevice(it)
                            }
                        },
                        onStartScan = { bluetoothScanner.startScan(true) },
                        onStopScan = { 
                            bluetoothScanner.stopScan() 
                            stopCycleConnect()
                        },
                        onCycleConnect = { startCycleConnect() }
                    )
                }
            }
        }
    }

    private fun startCycleConnect() {
        if (isCyclingConnections) {
            stopCycleConnect()
            Toast.makeText(this, "停止循环连接", Toast.LENGTH_SHORT).show()
            return
        }

        val bonded = bluetoothScanner.getBondedDevices().map { it.device }
        if (bonded.isEmpty()) {
            Toast.makeText(this, "无配对设备", Toast.LENGTH_SHORT).show()
            return
        }

        isCyclingConnections = true
        bondedDevicesArray = bonded.toTypedArray()
        currentConnectionIndex = 0

        Toast.makeText(this, "开启无限循环连接 (每5秒切换一次)", Toast.LENGTH_SHORT).show()
        handler.post(cycleRunnable)
    }

    private fun stopCycleConnect() {
        isCyclingConnections = false
        handler.removeCallbacks(cycleRunnable)
        bluetoothScanner.disconnect()
    }


    override fun onResume() {
        super.onResume()
        checkPermissions()
    }

    override fun onPause() {
        super.onPause()
        bluetoothScanner.stopScan()
        stopCycleConnect()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 释放 MediaPlayer 资源
        mediaPlayer?.release()
        mediaPlayer = null
    }

    private fun checkPermissions(): Boolean {
        val requiredPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
        } else {
            arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
        }

        if (requiredPermissions.all { checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }) {
            return true
        }

        ActivityCompat.requestPermissions(this, requiredPermissions, 1)
        return false
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BluetoothScanScreen(
    devices: List<ScanDevice>,
    onPairDevice: (BluetoothDevice) -> Unit,
    onConnectDevice: (BluetoothDevice) -> Unit,
    onStartScan: () -> Unit,
    onStopScan: () -> Unit,
    onCycleConnect: () -> Unit
) {
    var isScanning by remember { mutableStateOf(false) }
    var hideUnknownDevices by remember { mutableStateOf(false) }
    var filterLowRssi by remember { mutableStateOf(false) }

    val (bondedDevices, availableDevices) = devices.partition { it.bondState == BluetoothDevice.BOND_BONDED }

    val filteredAvailableDevices = availableDevices.filter {
        var keep = true
        if (hideUnknownDevices && (it.device.name == null || it.device.name.isEmpty())) keep = false
        if (filterLowRssi && it.rssi <= -60) keep = false
        keep
    }

    Column(modifier = Modifier.padding(16.dp)) {
        Row(verticalAlignment = Alignment.CenterVertically) {
            Button(onClick = {
                isScanning = !isScanning
                if (isScanning) onStartScan() else onStopScan()
            }) {
                Text(if (isScanning) "停止扫描" else "开始扫描")
            }
            Spacer(modifier = Modifier.width(8.dp))
            Button(onClick = onCycleConnect) {
                Text("循环连接")
            }
        }

        Spacer(modifier = Modifier.height(8.dp))

        LazyColumn(modifier = Modifier.padding(top = 8.dp)) {
            if (bondedDevices.isNotEmpty()) {
                stickyHeader {
                    ListHeader("已配对设备")
                }
                items(bondedDevices, key = { it.device.address }) {
                    DeviceCard(it, onAction = onConnectDevice)
                }
            }

            if (filteredAvailableDevices.isNotEmpty()) {
                stickyHeader {
                    ListHeader("可用设备")
                }
                items(filteredAvailableDevices, key = { it.device.address }) {
                    DeviceCard(it, onAction = onPairDevice)
                }
            }
        }
    }
}

@Composable
fun ListHeader(title: String) {
    Text(
        text = title,
        style = MaterialTheme.typography.titleMedium,
        modifier = Modifier
            .fillMaxWidth()
            .background(MaterialTheme.colorScheme.background)
            .padding(vertical = 8.dp)
    )
}

@Composable
fun DeviceCard(scanDevice: ScanDevice, onAction: (BluetoothDevice) -> Unit) {
    Card(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
        Row(
            modifier = Modifier.padding(12.dp),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Column(modifier = Modifier.weight(1f)) {
                val context = LocalContext.current
                if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
                    Text(scanDevice.device.name ?: "未知设备", style = MaterialTheme.typography.titleMedium)
                    Text(scanDevice.device.address, style = MaterialTheme.typography.bodySmall)
                } else {
                    Text("Permission Denied", style = MaterialTheme.typography.titleMedium)
                }
                if (scanDevice.rssi != -1) {
                    Text("RSSI: ${scanDevice.rssi} dBm", style = MaterialTheme.typography.bodySmall)
                }
            }
            ActionButton(scanDevice, onAction)
        }
    }
}

@Composable
fun ActionButton(scanDevice: ScanDevice, onAction: (BluetoothDevice) -> Unit) {
    val (text, enabled) = when (scanDevice.bondState) {
        BluetoothDevice.BOND_BONDED -> "连接" to true
        BluetoothDevice.BOND_BONDING -> "配对中..." to false
        else -> "配对" to true
    }

    Button(onClick = { onAction(scanDevice.device) }, enabled = enabled) {
        Text(text)
    }
}

bluetoothscan

package com.example.bluetoothscan

import android.bluetooth.BluetoothDevice

/**
 * 保存蓝牙设备信息的数据类。
 *
 * @param device 系统的 BluetoothDevice 对象。
 * @param rssi 信号强度 (Received Signal Strength Indication)。
 * @param bondState 设备的配对状态,默认为 BOND_NONE。
 */
data class ScanDevice(
    val device: BluetoothDevice,
    val rssi: Int,
    var bondState: Int = BluetoothDevice.BOND_NONE
)
  • 注:后续再优化和解释项目代码。

CG

android 蓝牙socket(spp)用法 1 综述 蓝牙socket连接必须实现服务器端和客户端的机制。一个设备必须打开一个Server Socket,而另一个必须发起连接(使用服务器端设备的MAC地址发起连接)。当服务器端和客户端在同一个RFCOMM信道上都有一个BluetoothSocket时,则两端就建立了连接。此刻,每个设备都能获得一个输入输出流,进行数据传输。下面分别介绍服务器端和客户端的实现。 2 服务器端 服务器... 阅读详情

相关推荐

蓝牙简学(三)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档。

lianghuajunone的博客 2069

android蓝牙SPP协议以及BluetoothSocket/BluetoothServerSocket的简单使用

今天讲解一下android蓝牙SPP协议(Serial Port Profile),这是一个数据交换的协议,用于蓝牙设备之间创建串口进行数据传输。假设没有异常出现,一般都能正常连接成功,在拿到socket之后便可以进行数据交换了,于是便获取流就可以进行流的读写了。:蓝牙设备封装类,描述一台设备的MAC地址,名称等数据,在与服务端建立通讯的过程中,利用此类的。在上述监听代码中,我们将监听过程放到了一个子线程里,因为。还有一点需要注意的是,:蓝牙适配器类,利用此类,利用。:蓝牙适配器类,利用此类的。

DamonMasty的博客 2946

Android 蓝牙——蓝牙连接(六)

蓝牙连接是指两个蓝牙设备之间建立通信链路的过程。一旦设备配对成功,就可以通过蓝牙连接来传输数据。

小旭的专栏 2623

杰理之SPP连接的UUID【篇】

SPP连接的UUID默认是标准的SPPUUID*/privateUUIDsppUUID=BluetoothConstant.UUID_SPP;

u013820168的博客 2057

Android 蓝牙BLE 配对连接流程 - 史上最全

一、蓝牙状态 一个时间只允许一个状态处于活动态 Standby状态不传输、接收任何数据包,该状态可以切换到其他任意一种状态 Initiaing状态也是一个Scanning状态。Scanning状态,开窗扫描,获取空中有哪些设备,以及地址信息。进入Initiaing状态,继续开启扫描窗口,找到感兴趣设置,发起连接请求,进入连接状态 连接事件(Connection Ev...

手机-手表蓝牙开发工程师的博客 4097

Android蓝牙开发实战:BLE与SPP连接详解

蓝牙技术作为现代移动设备中不可或缺的无线通信方式,尤其在Android平台上,广泛应用于耳机、手环、智能家居等外设的连接与数据交互。Android系统从2.0开始逐步完善蓝牙API,到如今已支持经典蓝牙(SPP)和低功耗蓝牙(BLE)两种主流协议。本章将引导读者了解蓝牙的基本概念,掌握SPP与BLE的核心区别,并熟悉Android蓝牙开发的整体架构与环境搭建流程,为后续深入开发打下坚实基础。定义数据包格式如下:字段长度含义起始标志1字节固定值 0xAA命令字。

weixin_35294091的博客 1347

蓝牙 SPP 协议详解及 Android 实现

蓝牙 SPP(Serial Port Profile,串口通信协议)是一种经典蓝牙协议,它允许设备之间通过模拟串口的方式进行无线数据传输。基于 RFCOMM 通信层,SPP 协议与传统的 RS-232 串口标准类似,因此非常适合低速、短距离的数据传输,如 Android 设备和传感器、微控制器之间的通信。本文将详细介绍蓝牙 SPP 协议的原理、工作流程,并结合 Android 实现,展示如何在移动设备中应用该协议。SPP 是一种点对点的蓝牙通信协议,适合小数据量的双向传输。

weixin_44780781的博客 7255

安卓蓝牙4.0串口调试工具(BLE SPP协议)完整AS工程源码

一套基于Android Studio开发的蓝牙低功耗(BLE)串口通信调试工具源码,支持BLE SPP(Serial Port Profile)协议,适配Android 4.3及以上系统。项目已编译生成可直接安装的Application-release.apk,实测可在主流安卓手机上连接并收发串口数据。源码结构完整,包含标准Gradle构建配置(build.gradle、settings.gradle)、本地环境配置(local.properties)、IDEA工程配置文件(.iml、.idea目录下各类xml)以及完整的Java源代码目录(src)。支持自定义UUID、服务发现、特征读写、通知开启与数据实时显示等功能,适合用于嵌入式设备蓝牙调试、IoT终端通信验证或二次开发学习。无需额外SDK依赖,开箱即用,兼容常见BLE串口模块(如HM-10、JDY系列等)。

Android蓝牙通用数据传输之一(SPP)

一、引言 Android蓝牙中涉及通用数据传输协议的有两种: SPP协议 BLE(Bluetooth low energy)协议 SPP协议是Android 2.0引入的API,是通过Socket的形式来实现数据传输及交互,有分客户端和服务端,手机一般以客户端的角色主动连接SPP协议设备。 BLE协议是Android 4.3引入的API,但手机厂商大部份在Android 4.4上才...

feelinghappy的专栏 2万+

Android蓝牙SPP通信实现[项目源码]

本文档详细介绍了在Android平台上实现与支持蓝牙SPP协议设备通信的完整流程。内容涵盖了蓝牙SPP协议的基础知识、Android蓝牙API的使用方法、设备扫描与连接建立、数据读写操作以及异常处理和版本兼容性优化。通过示例代码和详细步骤,开发者可以学习如何初始化蓝牙适配器、管理设备配对、建立稳定连接并进行数据传输。文档还强调了处理连接异常和数据中断的策略,以及针对不同Android版本的兼容性优化方案。此外,文章探讨了蓝牙SPP协议的高级应用场景和未来发展趋势,为开发者提供了全面的技术参考和实践指导。

JDY-31-V1.3蓝牙SPP串口透传模块手册1

1、解决了 V1.2 不能连接电脑的问题 2、添加了模块连接到主机后输出主机的 MAC 功能 3、添加了 AT+ENLOG 指令,用户可以通过此指令 1、排针应

SPP交互(UUID)

spp交互图

296000的博客 556

AURIX TC397开发板到手第一步:用iLLD库驱动LED流水灯(附完整代码)

本文详细介绍了如何使用英飞凌AURIX TC397开发板和iLLD库实现LED流水灯效果。从开发环境配置到硬件连接,再到完整的代码实现,逐步引导开发者完成项目。文章还提供了优化建议和常见问题解决方案,帮助嵌入式开发新手快速上手。

weixin_30911451的博客 390

Android Studio 之蓝牙串口通信(SPP)

AndroidMainfest.xml文件中添加权限申请(如下)

2301_77093965的博客 3947

J1939协议实用指南与J1939数据记录方案

“没错,这是一份SAE J1939简单、实用指南。”—虹科 **开篇:**在这篇介绍中,我们介绍了J1939协议的基本知识,其中包括PGN和SPN。因为这是一篇偏向应用的简介,所以您还将会学习到如何通过DBC文件解码J1939数据、如何记录J1939、典型的应用案例和实用技巧。下面,来了解下这份简单易懂的J1939介绍吧~ 什么是J1939? 简而言之,SAE J1939其实是一套标准,重型车辆ECU间就是按照这套标准在CAN总线上进行通信的。当今大多数车辆都通过CAN(Controller Area Ne

1万+

【Creo】关于Creo一换网络就报错许可证丢失问题解决方案

依照我的个人理解,Creo只要一更换网络,则其许可证里面的网卡参数和现在的网络就对不上了,所以就会出现报错,并且有可能是报错代码(-9),提示许可证激活有问题。

weixin_52002180的博客 6040

对Android蓝牙UUID的理解

UUID 先来段百度百科上的解释: UUID含义是通用唯一识别码 (Universally Unique Identifier),这 是一个软件建构的标准,也是被开源软件基金会 (Open Software Foundation, OSF) 的组织应用在分布式计算环境 (Distributed Computing Environment, DCE) 领域的一部分。它保证对在同一时空中的

Android系统攻城狮 2万+

基于深度学习YOLOv8的PCB板缺陷检测系统

基于YOLOv8的PCB板缺陷检测系统,可以有效提高PCB生产过程中的检测效率。未来,我们可以进一步优化模型,结合更多的数据增强策略和先进的网络结构,以适应更复杂的生产环境。为了进行训练,理想的数据集应包含多种不同缺陷类型和不同背景的PCB图像。YOLOv8需要的数据格式为每个图像一个标签文件,每个标签文件包含缺陷的类别及其边界框坐标。运行该界面时,用户可以通过点击按钮选择图像,随后系统将自动检测图像中的PCB缺陷并显示结果。训练完成后,可以使用训练好的模型进行推理,检测新的PCB图像中的缺陷。

YOLO 1070

【ESP-IDF】讲解SPP蓝牙串口官方Demo并按需修改

我们留意一下我框出来的事件,也就是连接和断开连接的这俩事件,这应该是比较值得做文章的两个事件,我们再额外留意一下我用绿框框出的变量is_connected,通过这个变量的值我们可以知道此时是否有连接上蓝牙,比如说后续我们有数据要通过蓝牙发送到手机上的时候,如果我们没有连接上,那么就不能发送。这个函数接收三个参数,第一个是事件类型,第二个是GATT的接口类型,具体什么意思我也不太清楚,最后一个是参数,这个参数的类型是一个枚举类型,枚举里包含很多种结构体类型,在遇到不同的事件类型的时候可以使用不同的结构体。

m0_63235356的博客 3790

【异常】Codybuddy Error Code: 14003 too many requests 限流报错问题排查与解决指南

14003报错本质为服务端限流拦截,并非账号封禁或程序致命故障,90%以上的场景可通过等待冷却+规范使用行为解决。通过本文的排查步骤,可快速恢复工具使用,同时规避后续重复触发该问题。

奶爸的编程之路,也就一周冷个三天 1253

11、系统稳定性分析与李雅普诺夫函数的应用

本文深入探讨了李雅普诺夫函数在系统稳定性分析中的核心作用,重点介绍了时变与时不变系统的稳定性判据差异,以及巴尔巴拉特引理在处理非自治系统中的应用。文章还阐述了正线性系统与严格正实传递函数的性质及其在非线性控制系统中的重要性,并展示了李雅普诺夫函数在评估主导时间常数和设计快速干扰抑制控制策略方面的实际应用。通过具体示例和流程图,系统地总结了李雅普诺夫方法在稳定性分析、性能评估与控制器设计中的多维度价值,同时指出了当前挑战与未来发展方向。

Jerry的专栏 93

【Android】蓝牙开发——经典蓝牙:配对与解除配对 & 实现配对或连接时不弹出配对框

目录 一、配对方法 二、解除配对方法 三、配对/解除配对结果 四、justwork配对模式下,不弹出配对框 五、pincode配对模式下,不弹出配对框 六、小结 在之前的文章【Android】蓝牙开发—— 经典蓝牙配对介绍(Java代码实现演示)附Demo源码中,简单介绍和演示了经典蓝牙的配对方式,今天就在之前的基础上继续讲讲,蓝牙的配对、解除配对以及如何实现不弹出配对框。 关...

好记性不如烂笔头,用文档记录技术,方便自己和他人。 1万+

合肥工业大学嵌入式系统原理实验报告

GPKDAT 的6、7引脚设置低电平,设置GPKDAT = 0x30则34灯亮12灯灭,GPKDAT 的4、5、6、7引脚全为高电平,设置GPKDAT = 0xf0,则led灯全灭。注意在烧写时要连接主机后面的USB接口,烧写完成后,要关闭开发板电源,将S2拨动开关置于NAND位置,然后重新开启开发板电源, 则开发板运行刚刚烧入的裸机程序。实验修改要求:在延时不变的条件下,将原程序中四个LED全亮,然后全灭的显示状态,修改为“四个全亮,然后1和2亮(3和4灭),然后3和4亮(1和2灭),最后全灭”。

Martin-share的博客 3098

蓝牙SPP协议实战:如何用Python快速实现串口数据传输(附完整代码)

本文提供了使用Python和PyBluez库快速实现蓝牙SPP协议串口数据传输的实战指南。文章详细讲解了SPP服务器端与客户端的完整代码实现,涵盖设备发现、服务广播、连接建立与双向通信,并针对Windows、Linux等不同平台的兼容性问题给出了具体的解决方案和调试技巧,帮助开发者快速构建可靠的无线串口通信应用。

bash7scripter的博客 988

1553B总线介绍及其硬件方案设计

军用1553B总线 1.简介 简单介绍可参考(https://blog.youkuaiyun.com/ScilogyHunter/article/details/108444231)博主写的很详细。 1553B总线是MIL-STD-1553总线的简称,其中B就是BUS,MIL-STD-1553B总线是飞机内部时分制命令/响应式多路复用数据总线。1553B数据总线标准是20世纪70年代由美国公布的一种串行多路数据总线标准。1553B总线能挂31个远置终端,1553B总线采用指令/响应型通信协议,它有三种终端类型: BC:

小幽余生不加糖 1万+

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

上一篇: 【电路笔记 STM32】stm32f103 实现ADC采集+DMA数据获取+HAL_ADC_ConvCpltCallback ADC中断+配置细节与注意
下一篇: 【电路笔记 STM32】Cortex-M3 & Cortex-M4 & Cortex-M7 ARM架构区别+关键不同+图示对比+代码兼容性
FakeOccupational
博客等级 码龄6年 2万+粉丝 1727原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值