本文同步发表于微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

一、SPP技术

SPP(Serial Port Profile,串口通信协议)是蓝牙技术中的一种基础通信协议,它模拟传统串行端口的通信方式,为设备间提供了可靠的数据传输通道。SPP协议广泛应用于需要双向数据传输的场景。

常见 Profile 及功能(通信协议

Profile 类型 作用描述 开发接口模块
A2DP 高质量音频传输(如耳机播放音乐) import { a2dp } from '@kit.ConnectivityKit'
HFP 通话控制(接听/挂断电话) import { hfp } from '@kit.ConnectivityKit'
HID 输入设备交互(如键盘、鼠标) import { hid } from '@kit.ConnectivityKit'
SPP 串口数据传输(自定义设备通信) 需手动实现通信协议

说明:蓝牙 Profile 能力(核心场景)作用:定义蓝牙设备间的通信协议,实现特定功能交互。

通信模型

在SPP通信交互中,设备根据功能角色不同分为:

  • 客户端(Client):主动发起连接请求的一方

  • 服务端(Server):提供特定UUID服务,等待客户端连接的一方

实现原理

  1. 客户端

    • 通过查找设备流程获取服务端的设备地址

    • 向服务端特定的UUID发起连接请求

    • 连接成功后即可进行双向数据传输

  2. 服务端

    • 创建并注册特定的UUID服务

    • 监听客户端的连接请求

    • 连接成功后即可进行双向数据传输

二、准备工作

2.1 权限申请

必须权限ohos.permission.ACCESS_BLUETOOTH

  • 配置方式

    1. 在配置文件中声明权限

    2. 运行时向用户申请授权

2.2 API模块导入

import { socket } from '@kit.ConnectivityKit';
import { BusinessError } from '@kit.BasicServicesKit';

三、客户端实现

3.1 发起连接

3.1.1 连接前提
  1. 设备发现:通过查找设备流程搜索到目标设备

  2. UUID匹配:客户端连接的UUID必须与服务端创建socket时构造的UUID一致

  3. 服务验证:蓝牙子系统会查询服务端是否支持该UUID服务,不支持则连接失败

3.1.2 连接参数配置
// 设备地址通过查找设备流程获取
let peerDevice = 'XX:XX:XX:XX:XX:XX';

// 定义客户端socket id(初始值为-1表示未连接)
let clientNumber = -1;

// 配置连接参数
let option: socket.SppOptions = {
    uuid: '00009999-0000-1000-8000-00805F9B34FB', // 需要连接的服务端UUID服务
    secure: false, // 安全模式,false表示非安全连接
    type: socket.SppType.SPP_RFCOMM // 使用RFCOMM协议
};

console.info('startConnect ' + peerDevice);
3.1.3 发起连接
socket.sppConnect(peerDevice, option, (err, num: number) => {
    if (err) {
        console.error('startConnect errCode: ' + (err as BusinessError).code + 
                     ', errMessage: ' + (err as BusinessError).message);
    } else {
        console.info('startConnect clientNumber: ' + num);
        clientNumber = num; // 保存连接成功后分配的socket id
    }
});

3.2 数据传输

3.2.1 发送数据

前提条件:客户端和服务端连接已建立成功

let clientNumber = 1; // 客户端发起连接时获取的socket id

let arrayBuffer = new ArrayBuffer(2); // 创建数据缓冲区
let data = new Uint8Array(arrayBuffer); // 转换为Uint8Array类型
data[0] = 3; // 设置第一个字节
data[1] = 4; // 设置第二个字节

try {
    socket.sppWrite(clientNumber, arrayBuffer); // 发送数据
} catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}
3.2.2 接收数据

实现方式:通过订阅socket.on('sppRead')接口实现数据接收

let clientNumber = 1; // 客户端发起连接时获取的socket id

// 定义接收数据的回调函数
function read(dataBuffer: ArrayBuffer) {
    let data = new Uint8Array(dataBuffer);
    console.info('client data: ' + JSON.stringify(data));
}

try {
    // 发起订阅,指定接收数据的socket和回调函数
    socket.on('sppRead', clientNumber, read);
} catch (err) {
    console.error('readData errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}

3.3 断开连接

断开流程:先取消读取数据的订阅,再断开连接

let clientNumber = 1; // 客户端发起连接时获取的socket id

// 定义接收数据的回调函数
function read(dataBuffer: ArrayBuffer) {
    let data = new Uint8Array(dataBuffer);
    console.info('client data: ' + JSON.stringify(data));
}

try {
    // 取消接收数据订阅
    socket.off('sppRead', clientNumber, read);
} catch (err) {
    console.error('off sppRead errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}

try {
    // 从客户端断开连接
    socket.sppCloseClientSocket(clientNumber);
} catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}

四、服务端实现

4.1 创建服务端套接字

4.1.1 功能说明

服务端通过创建套接字的方式,在蓝牙子系统中注册指定的UUID服务。

4.1.2 关键要点
  1. UUID名称:无限制,可使用应用名称

  2. UUID匹配:只有服务端与客户端的UUID一致时,连接才能成功建立

4.1.3 实现代码
// 定义服务端socket id(初始值为-1表示未创建)
let serverNumber = -1;

// 配置监听参数
let option: socket.SppOptions = {
    uuid: '00009999-0000-1000-8000-00805F9B34FB', // 注册的UUID服务
    secure: false, // 安全模式
    type: socket.SppType.SPP_RFCOMM // 使用RFCOMM协议
};

// 创建服务端监听socket
socket.sppListen("demonstration", option, (err, num: number) => {
    if (err) {
        console.error('sppListen errCode: ' + (err as BusinessError).code + 
                     ', errMessage: ' + (err as BusinessError).message);
    } else {
        console.info('sppListen serverNumber: ' + num);
        serverNumber = num; // 保存创建成功后分配的socket id
    }
});

4.2 监听客户端连接

4.2.1 功能说明

创建好服务端套接字后,服务端即可开始监听客户端的连接请求。

4.2.2 实现代码
let serverNumber = 1; // 创建服务端套接字时获取的socket id

// 定义客户端socket id(初始值为-1表示无连接)
let clientNumber = -1;

socket.sppAccept(serverNumber, (err, num: number) => {
    if (err) {
        console.error('accept errCode: ' + (err as BusinessError).code + 
                     ', errMessage: ' + (err as BusinessError).message);
    } else {
        console.info('accept clientNumber: ' + num);
        clientNumber = num; // 保存客户端连接成功后分配的socket id
    }
});

4.3 数据传输

4.3.1 发送数据
let clientNumber = 1; // 服务端监听连接时获取的客户端socket id

let arrayBuffer = new ArrayBuffer(2);
let data = new Uint8Array(arrayBuffer);
data[0] = 9;
data[1] = 8;

try {
    socket.sppWrite(clientNumber, arrayBuffer);
} catch (err) {
    console.error('sppWrite errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}
4.3.2 接收数据
let clientNumber = 1; // 服务端监听连接时获取的客户端socket id

// 定义接收数据的回调函数
function read(dataBuffer: ArrayBuffer) {
    let data = new Uint8Array(dataBuffer);
    console.info('client data: ' + JSON.stringify(data));
}

try {
    // 发起订阅
    socket.on('sppRead', clientNumber, read);
} catch (err) {
    console.error('readData errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}

4.4 断开连接

断开流程:先取消读取数据的订阅,再断开连接

let clientNumber = 1; // 服务端监听连接时获取的客户端socket id

// 定义接收数据的回调函数
function read(dataBuffer: ArrayBuffer) {
    let data = new Uint8Array(dataBuffer);
    console.info('client data: ' + JSON.stringify(data));
}

try {
    // 取消订阅
    socket.off('sppRead', clientNumber, read);
} catch (err) {
    console.error('off sppRead errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}

try {
    // 从服务端断开连接(与客户端断开方法相同)
    socket.sppCloseClientSocket(clientNumber);
} catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}

4.5 删除服务端套接字

4.5.1 功能说明

当应用不再需要该服务端套接字时,需要主动关闭创建时获取的套接字,蓝牙子系统会删除此前注册的UUID服务。

4.5.2 重要影响

删除套接字后,如果客户端发起连接,将会连接失败。

4.5.3 实现代码
let clientNumber = 1; // 服务端监听连接时获取的客户端socket id
let serverNumber = 1; // 创建服务端套接字时获取的服务端socket id

// 定义接收数据的回调函数
function read(dataBuffer: ArrayBuffer) {
    let data = new Uint8Array(dataBuffer);
    console.info('client data: ' + JSON.stringify(data));
}

try {
    // 取消订阅(先取消数据接收)
    socket.off('sppRead', clientNumber, read);
} catch (err) {
    console.error('off sppRead errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}

try {
    // 若应用不再需要此能力,则主动删除
    socket.sppCloseServerSocket(serverNumber);
} catch (err) {
    console.error('errCode: ' + (err as BusinessError).code + 
                 ', errMessage: ' + (err as BusinessError).message);
}

总结

技术要点

  1. 角色区分:清晰理解客户端和服务端的不同职责

  2. UUID匹配:确保客户端和服务端使用相同的UUID

  3. 异步通信:SPP操作大部分采用异步回调方式

开发流程

  1. 准备阶段:申请权限、导入模块、设计通信协议

  2. 实现阶段

    • 客户端:发起连接、发送数据、接收数据、断开连接

    • 服务端:创建套接字、监听连接、发送数据、接收数据、断开连接

  3. 测试阶段:验证连接稳定性、数据传输可靠性、错误处理完整性

SPP协议适用于多种蓝牙数据传输场景:

  1. 设备控制:远程控制蓝牙设备

  2. 数据同步:设备间数据同步

  3. 文件传输:小文件传输

  4. 传感器数据:传感器数据采集和传输

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐