做星闪开发有一阵子了,从最开始对着SDK里的SLE_UUID_Server示例一头雾水,到后来能在Server和Client之间稳定地互传数据,中间踩了不少坑。最开始我以为星闪的SLE开发跟BLE差不多,能拿蓝牙那套经验直接套,结果被协议栈的回调机制、UUID服务创建方式、配对流程轮番教做人。这篇博文就围绕星闪SLE_UUID_Server与Client配对通信的完整链路来讲,把服务端怎么建服务、客户端怎么找服务、数据怎么收发、调试时该看哪些指标一次说清楚,适合刚拿到星闪开发板、准备从BLE转过来的嵌入式开发者,或者正在做物联网设备互联的方案选型人员。文末我会专门讲代码调试技巧和几个典型的坑位,基本都是能直接落地的经验。
1. 先看懂SLE_UUID_Server与Client的通信模型
1.1 星闪SLE到底是什么,它和BLE的对应关系
星闪是中国自主创新的近距离无线通信技术,协议栈分为SLE(Star Link Emission)和SLB(Star Link Basic)两条路线。SLE更贴近传统BLE的定位,主打低功耗、中等速率、低时延,适合遥控器、键鼠、传感器、智能家居面板这类设备;SLB侧重高带宽、低时延的场景,比如音频传输、投屏、工业控制。本文讲的SLE_UUID_Server和Client,就是SLE协议栈里基于UUID的服务端/客户端通信模型。
很多人第一次接触星闪SDK的时候,看到sle_开头的API会有点懵,其实它的设计思路跟BLE的GATT层非常像。Server端负责把设备能力抽象成一个个"服务"(Service),每个服务里挂着若干个"特征值"(Characteristic)。Client端则像一个拿着清单的访客,先扫描找到Server,再按照UUID去核对清单上有没有自己需要的服务,找到了就对这个服务里的特征值做读写操作。
说个生活化的类比:Server就是大楼里的前台服务窗口,每个窗口贴着一个编号(UUID),有的窗口只接受资料(可写特征值),有的窗口只发放证件(可读特征值),有的窗口会主动把材料递给访客(通知/指示)。Client就是来办事的人,它得先找到大楼(扫描连接),再找到对应编号的窗口(服务发现),然后才能办业务(读写数据)。这套逻辑和BLE几乎一致,所以有BLE开发经验的人学SLE会很快,但细节上千万不能照搬,后面我会讲几个不一样的地方。
1.2 为什么一定要用UUID来标识服务
UUID(Universally Unique Identifier)是服务端和客户端之间的"暗号"。SLE支持16位UUID和128位UUID两种类型。16位UUID一般用于标准服务和厂商自定义的短标识,比如0xFFE0、0xFFE1这类在蓝牙生态里也常见的厂商自定义区间;128位UUID适合需要全局唯一性的自定义服务,避免不同厂商之间冲突。
实际开发里最常见的坑就是UUID类型不匹配。Server用128位UUID建了服务,Client却用16位UUID去搜,扫描结果永远是空的;反过来也是。所以编写代码的时候,我习惯把服务端和客户端的UUID定义放在同一个头文件里,从源头杜绝这种不一致。比如:
#define SERVICE_UUID_TYPE SLE_UUID_TYPE_128 #define CHARACTER_UUID {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, \ 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}128位UUID在代码里一般以数组形式存在,注意字节序,数组下标0是UUID的低字节,这一点会在抓包对照的时候经常被翻出来,先记在心里。
1.3 SLE协议栈的状态机流转
理解了模型,还要理解协议栈的运行逻辑。SLE协议栈从初始化到数据收发,大致经历以下几个状态:协议栈使能(SLE_ENABLE)、Server注册服务(SLE_CREATE_SERVER)、Client扫描(SLE_START_SCAN)、连接建立(SLE_CONNECT/ACCEPT)、服务发现(SLE_DISCOVERY)、数据交互(SLE_READ/WRITE/NOTIFY)。
这里有个跟BLE很不一样的点:星闪的SLE协议栈在收到连接、断开、数据到达等事件时,大量依赖异步回调。也就是说,你不能写完一句"连上对方"就立刻去读数据,因为连接成功的确认是在回调函数里异步返回的。很多从BLE转过来的开发者(包括我)一开始会在这个地方踩坑——回调还没触发,代码就已经往下执行了,结果读到的全是无效句柄。
sle_connect(&conn_param); // 这里的代码执行时,连接不一定已经建立成功 // 不要在这里直接调用服务发现接口!正确的做法是把后续流程压到回调里,靠状态机驱动:
static void on_connection_changed(uint8_t conn_id, sle_connection_state_t state) { if (state == SLE_CONNECTED) { // 连接建立成功,此时才能开始服务发现 start_discover_service(conn_id); } }2. 开发环境与工程目录:SDK、编译链、烧录的准备工作
2.1 芯片与开发板选型
目前市面上能跑SLE协议栈的芯片,主要是海思Hi2821系列、以及部分国产IoT厂商推出的星闪SoC。我手头这块是Hi2821的开发板,SDK里自带server和client两个示例工程,代码框架基本是现成的,主要工作是理解它然后改造。选型时重点看三件事:芯片的Flash/RAM是否够你跑业务逻辑、SDK是否包含SLE协议栈的完整库文件、厂商有没有提供串口调试工具和烧录工具。
如果是个人学习,建议直接买官方开发套件,虽然价格比普通蓝牙开发板贵一点,但资料和例程齐全,遇到问题也容易找到厂商技术支持。别为了省几十块钱买没资料的杂牌板子,后面排查问题会非常痛苦。
2.2 SDK目录结构说明
厂商SDK一般会分成几个大目录:
sdk/:协议栈库文件、公开头文件app/:用户应用代码,我们主要改这里os/:操作系统抽象层(有些基于LiteOS,有些基于FreeRTOS)tools/:烧录工具、日志工具、抓包工具- 编译脚本:Makefile或build.py
开发时改app目录下的业务逻辑就够了,sdk目录下的协议栈库文件尽量不要动。我见过有同事为了"优化协议栈性能"直接改sdk底层文件,结果编译过不了,回滚了大半天。协议栈库一般是预编译的.a或.lib文件,真的出了问题也没法在源码层面改,不如直接提单给厂商。
2.3 编译与烧录的常见坑
交叉编译工具链版本:SDK往往对工具链版本有严格要求,gcc版本太低会冒出一些莫名其妙的编译错误,比如
unknown type name 'uint32_t'。搭建环境时最好用SDK文档里明确指定的版本号。ROM/RAM大小评估:星闪协议栈本身会占用一定资源,再加上LiteOS内核、驱动框架,留给业务的RAM往往只有几十KB。如果业务代码里用了大数组,链接时很容易报overflow。此时要看你申请的内存是放静态区还是堆区,放堆区的话还得评估协议栈的堆内存配置够不够。我习惯在工程配置文件里把协议栈的堆大小调大一些,同时把不用的协议特性裁剪掉。
烧录工具的串口选择:很多开发板同时有调试串口和AT指令串口,烧录时选错串口会一直报连接超时。我每次换开发板后都会先确认一下设备管理器的COM口号对应哪个物理串口,再开始烧录。
我通常的做法是先把SDK自带的server和client示例烧进去,跑通默认逻辑,再开始改自己的服务和UUID。很多人急于写自己的业务代码,跳过这一步,结果环境问题、编译问题、烧录问题混在一起,排查难度翻倍。先跑通例程,相当于给"环境"打了个满分,后面就只需要面对"代码"了。
3. Server端实战:UUID服务创建、特征值属性与回调机制
3.1 Server初始化链路
Server端的初始化流程一般是这样:
// 初始化协议栈 errcode_t ret = sle_enable(NULL, NULL); if (ret != SLE_OK) { printf("sle_enable failed: 0x%x\r\n", ret); return; } // 注册协议栈事件回调 sle_callback_t cb = {0}; cb.connection_state_changed = on_connection_state_changed; cb.uuid_server_data_received = on_server_data_received; sle_register_callback(&cb);注意,sle_register_callback注册的回调是整个协议栈级的,Server和Client共用同一套回调入口,需要靠连接句柄(conn_id)来区分是哪条连接的事件。最开始我把连接回调当成"只属于Server"的回调,结果Client端也莫名其妙触发了同一个回调,翻API文档才发现回调是统一分发的。这个设计跟BLE stack很像,但星闪SDK的例程一般不会主动说这件事,属于"文档角落"信息。
3.2 创建服务与特征值
服务的创建相对直观,关键是处理好UUID类型和长度:
sle_uuid_t service_uuid = {0}; service_uuid.type = SLE_UUID_TYPE_16; service_uuid.uuid16 = 0x1800; // 举例,实际按你的业务定义 sle_uuid_server_info_t server_info = {0}; server_info.uuid = service_uuid; server_info.min_attr_len = 4; server_info.max_attr_len = 64; uint8_t server_handle = sle_create_server(&server_info); if (server_handle == INVALID_ATT_HANDLE) { printf("create server failed\r\n"); return; }然后往服务里添加特征值:
sle_uuid_t char_uuid = {0}; char_uuid.type = SLE_UUID_TYPE_16; char_uuid.uuid16 = 0x2A00; sle_characteristic_info_t char_info = {0}; char_info.uuid = char_uuid; char_info.properties = SLE_UUID_PROPERTY_READ | SLE_UUID_PROPERTY_WRITE; char_info.permissions = SLE_UUID_PERMISSION_READ_ALLOWED | SLE_UUID_PERMISSION_WRITE_ALLOWED; sle_add_characteristic(server_handle, &char_info);这里的properties和permissions是两个不同的概念,很多教程会把它们混在一起讲,但实际开发中两者有明确分工。properties是给Client看的"我能干什么",相当于窗口挂出来的服务标语;permissions是协议栈实际执行的"权限检查",相当于保安手里的权限表。如果属性位开了READ,但权限没给READ_ALLOWED,Client读的时候会收到拒绝,而且这个拒绝是协议栈直接返回的错误码,你的Server代码甚至都感知不到。反过来属性位没开READ、权限给了READ_ALLOWED,Client端也会因为特征值不支持读而放弃。
我整理了一张表,方便对照:
| properties位 | permissions位 | Client行为 | Server是否感知 |
|---|---|---|---|
| READ | READ_ALLOWED | 正常读取 | 有读请求回调 |
| READ | 无 | 协议栈拒绝 | 不感知 |
| 无 | READ_ALLOWED | Client不发起读 | 不感知 |
| WRITE | WRITE_ALLOWED | 正常写入 | 有写请求回调 |
| WRITE | 无 | 协议栈拒绝 | 不感知 |
| NOTIFY | 无 | 可订阅通知 | 有订阅回调 |
实际项目里我见过不止一次这种问题:两个设备明明连上了,数据就是不通,查到最后是properties和permissions配置不一致。所以写代码的时候,两个字段建议放在一起连续配,并且加注释。
3.3 启动广播与等待连接
Server建好服务之后,一般会进入广播状态,让Client能发现它。广播参数里配置广播周期、广播数据内容(通常会把服务UUID塞进广播包,方便Client提前判断):
sle_broadcast_param_t bc_param = {0}; bc_param.broadcast_interval_min = 100; // 单位通常为0.625ms,100表示62.5ms bc_param.broadcast_interval_max = 100; bc_param.adv_data.len = 12; memcpy_s(bc_param.adv_data.data, sizeof(bc_param.adv_data.data), adv_data, 12); sle_set_broadcast_data(&bc_param); sle_start_broadcast();注意:星闪的广播数据包长度相对有限,如果服务UUID是128位,广播包里未必放得下。实操时我会把128位UUID放在扫描响应里,或者干脆让客户端发起扫描后通过服务发现流程去获取——广播包里只放一个厂商标识,既省空间又能起到过滤作用。
广播启动成功之后,Server就处于"待连接"状态。连接事件会触发前面注册的回调:
static void on_connection_state_changed(uint8_t conn_id, sle_connection_state_t state) { if (state == SLE_CONNECTED) { printf("server connected, conn_id=%d\r\n", conn_id); } else if (state == SLE_DISCONNECTED) { printf("server disconnected, conn_id=%d\r\n", conn_id); // 按需重启广播,等待下一次连接 sle_start_broadcast(); } }断开重连是实际项目中很容易忽略的点。很多开发板在首次连接成功后一切正常,但设备断电重启后Client就再也连不上了,原因是Server断开后没有重新启动广播。建议把sle_start_broadcast()放在断开回调里,并在主循环里加一个"未连接状态才广播"的保护。
3.4 数据接收与发送的回调处理
Server收到Client发来的数据,会触发数据接收回调:
static void on_server_data_received(uint8_t server_handle, uint16_t conn_id, uint8_t *data, uint16_t len) { printf("recv len=%d\r\n", len); // 先校验数据合法性和长度 if (len > MAX_RECV_LEN) { return; } // 处理业务逻辑 process_received_data(data, len); // 如果需要回包 uint8_t resp_data[8] = {0}; uint16_t resp_len = 0; build_response(resp_data, &resp_len); sle_uuid_server_send_response(server_handle, conn_id, resp_data, resp_len); }Server主动发数据的方式是通知(notify):
sle_uuid_server_notify(server_handle, conn_id, notify_data, data_len);这里有一个非常容易踩的坑:Write请求分为带响应和不带响应两种。带响应的写入,Client写完会等待Server回包,如果Server回调里没有调用send_response,Client会一直阻塞到超时,报"写超时"错误;不带响应的写入则不需要回包。很多从串口485通信转过来的开发者习惯"收包即处理,处理完不回复",到了星闪这里就会出现Client端超时重试的现象。解决办法很简单,先看历史抓包确认Client用的是写请求还是写命令,再决定要不要回响应。
4. Client端实战:扫描、连接、服务发现与数据收发
4.1 扫描参数配置与过滤
Client端首先做扫描。扫描参数主要关注扫描类型和扫描间隔:
sle_scan_param_t scan_param = {0}; scan_param.scan_type = SLE_SCAN_TYPE_ACTIVE; scan_param.scan_interval = 0x10; // 扫描间隔 scan_param.scan_window = 0x10; // 扫描窗口 sle_start_scan(&scan_param);扫描结果会在回调里上抛。如果Server端在广播包里塞了服务UUID,Client可以通过UUID过滤,快速锁定目标设备;如果广播包里没有,就得先把扫描到的设备地址记下来,连接后再走服务发现去确认。
实际项目中我推荐一个折中方案:在广播包里放一个厂商自定义的短标识(比如2字节的设备类型编码),Client扫描时按这个短标识过滤,过滤命中后再发起连接,连接成功后再做一次完整的UUID服务发现确认。这样广播数据不臃肿,扫描阶段也不会被一屋子无关设备刷屏。
4.2 发起连接与连接参数
找到目标设备后,发起连接:
sle_connect_param_t conn_param = {0}; memcpy_s(&conn_param.peer_addr, sizeof(conn_param.peer_addr), &target_addr, sizeof(target_addr)); conn_param.conn_interval_min = 8; // 短间隔 conn_param.conn_interval_max = 16; conn_param.conn_latency = 0; sle_connect(&conn_param);连接间隔(conn_interval)决定收发数据的"节拍"。间隔越短,时延越低,但功耗越高;间隔越长,越省电,但吞吐量和响应速度都会下来。做遥控器、键鼠这类交互型应用,建议用短间隔(数值8~16);做传感器定时上报型应用,把间隔拉长到80以上,再配合连接延迟(latency)跳过不必要的唤醒,节省更多功耗。这个权衡跟BLE基本一致,但星闪在同样的间隔参数下,因为调度机制的差异,实测时延和功耗曲线会跟BLE不一样,所以还是得在真实板子上量一遍。
4.3 服务发现:UUID匹配的完整逻辑
连接成功后,Client需要遍历Server的服务列表。这一步最关键的是一开始就要确认两端UUID是否匹配:
sle_discovery_param_t d_param = {0}; d_param.enable_uuid_filter = true; d_param.uuid = target_service_uuid; // 和Server端定义保持一致 sle_discover_services(&d_param);发现结果会通过回调返回,里面包含服务句柄、特征值句柄等关键信息。拿到特征值句柄之后,后续的所有读写操作都依赖它。
经典坑:发现服务回调里拿到的句柄,必须保存为全局变量或者状态机变量,千万不要在回调里用完后丢到栈上。后续Client做Read/Write时如果句柄无效,协议栈会直接返回"句柄不存在"错误,而且这个错误跟真实句柄错误长得一模一样,排查起来会很费劲。
我做服务发现的时候会把所有服务和特征值打印一遍,包括UUID、类型、属性。这样即使目标UUID没匹配上,也能从日志里看出来Server实际注册了哪些UUID,快速定位是"Server注册错了"还是"Client找错了"。
4.4 特征值读写与通知订阅
Client端的读写操作非常直观:
// 写数据 errcode_t ret = sle_uuid_client_write(conn_id, char_handle, data, len); if (ret != SLE_OK) { printf("write failed: 0x%x\r\n", ret); } // 读数据 ret = sle_uuid_client_read(conn_id, char_handle); // 订阅通知 ret = sle_uuid_client_register_notify(conn_id, char_handle);订阅通知这个动作很关键。Server能不能主动给Client发数据,取决于Client是否订阅了该特征值的通知。很多新手发现Server调了notify但Client收不到,八成是漏了register_notify这一步。订阅成功之后,Server的notify消息才会被协议栈路由到Client的数据回调里。
再补充一下几种写法和通知的区别,方便对照:
| 操作 | 语义 | Client API | Server回调 |
|---|---|---|---|
| 写请求 | 带响应,Server需回包 | write | 有回调,需send_response |
| 写命令 | 无响应,只发不回复 | write_no_response | 有回调,不用回复 |
| 通知 | Server主动推送,无需回应 | 订阅后收notify | notify调用即触发 |
4.5 Client侧的状态机管理
实际项目里,Client端的逻辑复杂度远高于Server端。从扫描到连接、发现服务、订阅通知、数据交互,任何一个环节出错都要能及时暴露。我建议Client侧一定维护一个状态机:
typedef enum { SLE_STATE_IDLE, SLE_STATE_SCANNING, SLE_STATE_CONNECTING, SLE_STATE_DISCOVERING, SLE_STATE_READY, SLE_STATE_DATA_EXCHANGE, SLE_STATE_DISCONNECTED, } sle_client_state_t;每个状态只处理该状态下允许的事件,不合法的转换直接打印日志并丢弃。这样做的最大好处是:当某个环节异常时,你可以通过状态机当前所处的位置快速判断问题出在哪个阶段。比如卡在DISCOVERING,就说明连接没问题、服务发现有问题;卡在CONNECTING,就要去看连接参数和安全配置。没有状态机,所有事件都平铺在一块,排查一个问题要翻几百行日志才能确定"到底是哪一步没走通"。
5. 配对安全与连接参数:从能用到可靠
5.1 配对模式的选择
SLE协议栈支持的安全模式包括Just Works(免交互配对)和Passkey Entry(密码配对)两种。Just Works适合对安全性要求不高的设备(如玩具遥控器、简单传感器),流程简单但容易被中间人攻击;Passkey Entry需要显示屏或输入设备配合,交互上复杂一些,但安全性高很多。
选配对模式要看具体场景。如果是室内固定设备(比如智能插座)和手机App配对,推荐Passkey Entry;如果是设备之间自动组网(比如大量传感器节点上报数据),Just Works就够了,省去每次配对的交互成本。星闪SDK一般会在安全配置里设置配对模式,有的还支持"I/O能力"组合,决定配对时采用哪种认证方式,这个需要仔细看厂商SDK的安全模块文档。
5.2 密钥管理与加密连接
配对成功后协议栈会生成密钥,当连接断开再重连时,可以考虑使用长期密钥(LTK)进行快速加密,避免每次重连都重新配对。这个机制在BLE里非常成熟,星闪SLE也类似,但不同厂商SDK的API命名和存储方式可能不一样。我遇到过的坑是:重连时加密老是失败,查到最后是本地保存的密钥跟对端不一致了。原因是其中一端恢复了出厂设置、密钥被清掉,另一端还保留着旧密钥。所以每次重连失败时,先确认两端密钥状态是否同步,必要时主动触发一次重新配对。
5.3 连接稳定的几个参数细节
影响连接稳定性的因素很多,除了连接间隔,还有扫描窗口、广播间隔、看门狗超时时间等。我整理了一个比较常用的初始参数参考表,实际根据你的业务场景微调:
| 参数 | 推荐初始值 | 说明 |
|---|---|---|
| 连接间隔 | 8~16(交互) | 单位通常为1.25ms,数字越小越流畅 |
| 连接延迟 | 0~4 | 越大越省电,但响应变慢 |
| 看门狗超时 | 400~1000 | 超过此时间未收到包判为连接超时 |
| 广播间隔 | 100~200 | 单位通常为0.625ms,太短费电,太长难被发现 |
| 扫描窗口 | 等于扫描间隔 | 连续扫描时两者相等最稳 |
这里重点说看门狗超时。很多时候"连上就断开"并不是因为参数配错,而是看门狗超时设置得太小,导致稍微卡顿一下就被协议栈判定为连接超时。做低功耗场景的人要注意,如果设备会深睡,唤醒后的前几个包可能因为时钟同步问题延迟到达,这时候超时时间设小了就会误判断开。
6. 调试技巧、错误码排查与常见坑位
6.1 日志打点的正确姿势
嵌入式的串口日志是调试的第一手段。我习惯在状态机切换入口打印一条总日志,然后每个API调用返回后立刻检查错误码。日志格式用带时间戳的,方便对比两端时序:
// 每个API调用统一格式 errno = sle_connect(&conn_param); printf("[%u] sle_connect -> 0x%x\r\n", (unsigned int)osal_get_tick(), errno);实际调试时发现,很多问题靠日志就能定位。比如Client发了一条写请求,Server日志什么都没打——那就说明数据根本没到Server;如果Server日志打了,说明数据链路是通的,问题是出在业务逻辑或者回包处理上。这里有个经验:不只是打印错误,还要打印正常流程的关键节点,比如"进入扫描""发现服务""订阅成功",这样你才能知道程序走到哪一步了。
6.2 常见错误码快速对照表
不同厂商SDK的错误码定义会有差异,但大体上都会包含以下几类。我整理了一个排查方向对照表,遇到错误码先按这个方向查:
| 错误码范围 | 常见含义 | 一般排查方向 |
|---|---|---|
| 0x0101~0x0104 | 参数无效 | 检查API入参、枚举值、UUID类型 |
| 0x0201~0x0205 | 连接相关异常 | 检查连接句柄、连接状态、地址类型 |
| 0x0301~0x0306 | 句柄无效 | 检查服务句柄/特征值句柄是否有效 |
| 0x0401~0x0403 | 权限不足 | 检查properties/permissions配置 |
| 0x0501~0x0503 | 内存不足 | 检查堆配置、大数组、内存泄漏 |
| 0x0601~0x0604 | 安全/配对失败 | 检查配对模式、密钥存储 |
| 0xF001~0xF010 | 供应商自定义 | 看SDK手册或串口日志 |
注意:这个表给的是排查思路,不是标准答案。真遇到错误码,第一件事永远是查你手上SDK的errcode定义头文件,别靠猜。
6.3 抓包工具:别靠猜
如果条件允许,用支持星闪的协议分析仪(或厂商提供的Sniffer)抓空口报文,这是最直接定位"数据到底有没有发出/收到"的方式。没有抓包设备时,可以在Server和Client各打一条带打印的收发日志,对比双方的时间戳,基本也能判断是哪一端的问题。
我遇到过的一个经典案例:Client显示数据已发出,Server显示数据已收到,但业务上处理结果不对。后来抓包发现,数据在空口传输时因为重传机制被折叠成了一帧,导致Server收到的是合并后的数据,按单帧解析就错了。这个问题只靠两端日志很难发现,因为两端各自都是"对"的,只有站在中间看链路才看得出问题。所以强烈建议有条件就上抓包工具,没有条件也要在代码里预留两端的收发计数,定期比对总数是否一致。
6.4 经典故障排查案例
案例一:Client扫描不到Server
大概率是广播参数没生效,或者Server在创建服务之前就把广播开了。检查广播间隔、检查服务UUID是否已成功注册,另外注意广播数据长度有没有超限。还有一种很隐蔽的情况是Server端协议栈还没完全使能就去开广播,API返回了错误但代码没做错误处理,看起来好像广播开了,实际上没有。
案例二:连接后立刻断开
先看连接参数里的看门狗超时是否太小,再看有没有配对要求不匹配。比如Server要求Passkey配对,Client配成了Just Works,协议栈会在加密协商阶段直接断开连接。这种问题日志里一般能看到安全相关的错误码,但如果你没开安全模块的调试日志,很容易被当成普通断线处理。
案例三:能连接但数据发不通
依次检查三件事:特征值属性位/权限位是否匹配、通知订阅是否完成、数据长度是否超出协议栈最大传输单元。第三点很容易被忽略——SPP串口通信习惯了发几百字节,到了星闪SLE这里,单包数据长度是有限的,超长数据必须自己分包发送。如果文档里写了最大MTU是某个值,代码里就要做好数据分片,一般建议分片长度留个安全余量,不要顶着上限发。
案例四:断开重连不成功
上面说过,Server断开后要重启广播,Client断开后要从状态机的IDLE重新走一遍,不能直接跳到连接。现实中有不少开发者在重连逻辑里加了个"延时5秒",然后让Client直接调连接接口,结果Server广播还没起来,连接自然失败。正确做法是Client在重连前先做一次扫描,确保Server在线、广播可发现,再走连接流程。
最后再分享一个小技巧:在源码里维护一个测试专用的"回环模式"——客户端发什么,服务端就原样回什么。两端各自打时间戳测往返时延(RTT),既能验证链路通断,又能顺带测出连接参数配置是否合理。跑通了回环模式再去实现业务逻辑,整个开发节奏会稳很多。星闪SLE_UUID_Server和Client的配对通信其实不难,难的是那些藏在API时序和参数配置里的细节,把这些细节理清楚了,后面的业务开发就会顺畅很多。