OTA 流程库(jl_ota)
jl_ota 是杰理(Jieli)微信小程序 OTA SDK 中的固件升级流程控制库,负责把一次完整的设备固件升级(OTA)编排为可状态机驱动的命令序列:从读取升级文件标志、查询设备可升级性、进入升级模式、分包下发固件、查询升级结果到重启设备,全部由该库内部的状态流转驱动。它是 libs/jl_ota_2.1.1.js(及 code/JLOTA/miniprogram/lib/jl_lib/jl_ota_2.1.1.js)编译产物所对应的能力,工程中通过 OTAWrapper 与 RcspOTAManager 两个层次对外暴露。
Purpose and Scope
本文档围绕 OTA 流程库(jl_ota) 展开,覆盖以下内容:
- 库在 SDK 整体架构中的位置(
BluetoothOTAManager→OTAWrapper→RcspOTAManager/OTAImpl→ RCSP 协议库 → BLE 设备)。 - 核心导出类型与常量:
OTAConfig、OTAError错误码、UpgradeType、ReConnectMsg、FileOffset。 OTAImpl升级状态机的内部机制、超时参数与回调事件。RcspOTAManager如何把 RCSP 命令封装成升级原语。OTAWrapper的高层封装、设备管理与内部回连逻辑。- 配置选项、API 参考、失败模式与边界情况。
以下内容属于其他目录页的范畴,本文不展开:蓝牙扫描/连接适配层(bluetooth.ts、bluetoothUI.ts)由蓝牙管理页覆盖;RCSP 协议编解码(jl_rcsp_ota_2.1.1.js)属于 RCSP-OTA 协议库页;认证流程(jl_auth_2.0.0)属于认证库页;OTA 进度 UI 组件(otaProgressView 等)属于页面组件页。本文聚焦于"升级流程如何被编排与执行"这一条主线。
概述
为什么需要独立的 OTA 流程库
设备固件升级与普通指令交互不同:它是一段长事务,跨越多个命令、多次超时、可能经历设备断连与回连,还必须在"双备份"与"单备份"两种 flash 方案下表现不同(单备份升级过程不可中断)。把这些流程控制逻辑从页面代码中剥离出来、集中到一个库中,可以带来:
- 可复用:Demo、正式小程序、Android/iOS 客户端共用同一套流程语义。
- 可重入保护:通过
isOTA()判断当前是否已有升级在进行,防止重复启动。 - 统一超时与错误语义:所有失败都收敛为
OTAError错误码 + 描述字符串,上层 UI 只需按码处理。 - 断连自动恢复:库内建"等待设备离线 → 扫描回连 → 继续传输"的恢复机制。
库的两个使用层次
- 高层封装(推荐):
OTAWrapper(位于otaWrapper.ts)—— 内部管理RcspOTAManager、RCSP 实例、认证与回连对象,页面只需实现OTAWrapperOption回调(扫描、连接、发数据)即可调用startOTA。 - 底层接口:
RcspOTAManager与OTAImpl(jl_ota_2.1.1.js导出)—— 直接操作 RCSP 命令,适合需要完全控制传输细节的场景。
关键术语
| 术语 | 含义 |
|---|---|
| RCSP | 杰理自定义的"遥控/命令"协议,用于与设备交换命令与数据 |
| 双备份(Double Backup) | 设备 flash 有 A/B 双区,升级可中断、失败可回滚,支持取消 |
| 单备份(Single Backup) | 设备只有单一固件区,升级开始后不可打断 |
| 强制升级(Mandatory Upgrade) | 设备要求必须升级,跳过正常握手流程直接进入升级模式 |
| 回连(Reconnect) | 升级中设备主动断开并重启,需重新扫描、连接以继续传输 |
架构
flowchart TD
subgraph sg_Page["小程序页面层"]
PageUpdate["pageUpdate.ts"]
OtaProgress["otaProgressView 组件"]
end
subgraph sg_Manager["蓝牙管理层"]
BTManager["BluetoothOTAManager"]
BleDataHandler["BleDataHandler"]
end
subgraph sg_Wrapper["OTAWrapper 封装层"]
OTAWrapper["OTAWrapper"]
Reconnect["Reconnect 回连器"]
Auth["Auth 认证"]
end
subgraph sg_OTACore["OTA 流程库 jl_ota"]
RcspOTA["RcspOTAManager"]
OTAImpl["OTAImpl 状态机"]
RcspCmd["RCSP 命令封装<br/>(CmdReadFileBlock 等)"]
end
subgraph sg_RCSPLib["RCSP-OTA 协议库 jl_rcsp_ota"]
RcspImpl["RcspImpl"]
Protocol["协议编解码/发送"]
end
Device["BLE 设备"]
PageUpdate --> BTManager
BTManager --> OTAWrapper
OTAWrapper --> RcspOTA
OTAWrapper --> Reconnect
OTAWrapper --> Auth
RcspOTA --> OTAImpl
RcspOTA --> RcspCmd
RcspCmd --> RcspImpl
OTAImpl -->|"回调 onProgress/onError/onStop"| OTAWrapper
OTAWrapper -->|"onNeedReconnect"| Reconnect
RcspImpl --> Protocol
Protocol -->|"BLE 写/通知"| BleDataHandler
BleDataHandler --> Device
架构说明:OTAImpl 是流程库的心脏(状态机),它不直接接触蓝牙,而是通过构造时注入的"操作接口" this.A(即 RcspOTAManager 实现的 RCSP 原语集)下发命令;RcspOTAManager 把 OTAImpl 的命令请求翻译为 RCSP 协议命令(CmdRequestUpdate、CmdEnterUpdateMode、CmdReadFileBlock 等)并通过 RcspImpl.sendRCSPCommand 发送;命令响应与设备主动上报(如 CmdNotifyUpdateFileSize、CmdReadFileBlock 请求固件数据)通过注册的回调 onRcspCommand 回流到 OTAImpl,形成"请求—响应—事件"的闭环。OTAWrapper 则在之上补充了多设备 Map 管理、认证与回连能力,让页面层只需要关心升级回调。
核心模块与数据结构
导出总览(jl_ota_2.1.1.js)
流程库编译产物通过 exports 暴露以下内容(来源:jl_ota_2.1.1.js):
| 导出 | 类型 | 作用 |
|---|---|---|
RcspOTAManager | class | 升级管理器:持有 OTAImpl,把 RCSP 命令封装为升级原语 |
OTAImpl | class | 升级流程状态机(核心逻辑) |
OTAConfig | class | 升级配置:通信方式、是否支持新重启方式、固件数据 |
OTAError | class | 错误码常量集(-1 ~ -114)与 getErrorDesc |
ReConnectMsg | class | 回连消息:设备 BLE MAC、是否支持新回连广播 |
FileOffset | class | 文件偏移量(offset + len),用于按块读取固件 |
UpgradeType | enum | UPGRADE_TYPE_UNKNOWN=-1 / UPGRADE_TYPE_CHECK_FILE=0 / UPGRADE_TYPE_FIRMWARE=1 |
ab2hex | function | ArrayBuffer → 十六进制字符串 |
getErrorDesc | function | 错误码 → 可读描述 |
setLogger / setLogGrade | function | 注入日志实现与日志级别(app.ts 中调用) |
OTAConfig —— 升级入口配置
OTAConfig 是 startOTA 的入参,定义如下(源码为压缩形式,此处整理字段语义):
class OTAConfig {
constructor() {
this.communicationWay = OTAConfig.COMMUNICATION_WAY_BLE // 默认走 BLE
this.isSupportNewRebootWay = false // 是否支持新重启方式
// updateFileData 由调用方赋值:Uint8Array 固件数据
}
toString() {
return "OTAConfig{communicationWay=" + this.communicationWay +
", isSupportNewRebootWay=" + this.isSupportNewRebootWay +
", updateFileDataSize=" + this.updateFileData?.length + "}"
}
}
OTAConfig.COMMUNICATION_WAY_BLE = 0
OTAConfig.COMMUNICATION_WAY_SPP = 1
OTAConfig.COMMUNICATION_WAY_USB = 2
Source: jl_ota_2.1.1.js
updateFileData 是必填项:startOTA 的第一步校验就是"配置为空或 updateFileData.length == 0 直接以 ERROR_INVALID_PARAM 失败";若固件数据非空但在写文件阶段被判定异常,则以 ERROR_OTA_UPGRADE_FILE_ERROR 失败。
OTAError —— 统一错误码体系
错误码分为通用错误(-1 ~ -67)与 OTA 专属错误(-97 ~ -114)两段,覆盖了升级中几乎所有的可预期失败:
| 错误码 | 常量 | 含义 |
|---|---|---|
| 0 | ERROR_NONE | 成功 |
| -2 | ERROR_INVALID_PARAM | 参数非法(如固件数据为空) |
| -33 | ERROR_DEVICE_OFFLINE | 设备离线(启动升级、取消、断连时都会检查) |
| -65 / -66 | ERROR_REPLY_BAD_STATUS / ERROR_REPLY_BAD_RESULT | 设备返回了错误状态/结果 |
| -97 | ERROR_OTA_LOW_POWER | 设备电量不足 |
| -98 | ERROR_OTA_UPDATE_FILE | 升级固件信息错误 |
| -99 | ERROR_OTA_FIRMWARE_VERSION_NO_CHANGE | 版本号与设备固件一致(重复升级) |
| -100 | ERROR_OTA_TWS_NOT_CONNECT | TWS 耳机未连接 |
| -101 | ERROR_OTA_HEADSET_NOT_IN_CHARGING_BIN | 耳机未放入充电仓 |
| -102 | ERROR_OTA_DATA_CHECK_ERROR | 升级数据校验失败 |
| -103 | ERROR_OTA_FAIL | 升级失败(通用) |
| -104 | ERROR_OTA_ENCRYPTED_KEY_NOT_MATCH | 加密密钥不匹配 |
| -105 | ERROR_OTA_UPGRADE_FILE_ERROR | 升级文件损坏 |
| -106 | ERROR_OTA_UPGRADE_TYPE_ERROR | 升级类型错误 |
| -107 | ERROR_OTA_LENGTH_OVER | 升级长度超限 |
| -108 | ERROR_OTA_FLASH_IO_EXCEPTION | Flash 读写异常 |
| -109 | ERROR_OTA_CMD_TIMEOUT | 设备侧命令超时 |
| -110 | ERROR_OTA_IN_PROGRESS | 已有升级在进行 |
| -111 | ERROR_OTA_COMMAND_TIMEOUT | SDK 等待命令响应超时 |
| -112 | ERROR_OTA_RECONNECT_DEVICE_TIMEOUT | 等待回连设备超时 |
| -113 | ERROR_OTA_USE_CANCEL | 用户取消升级 |
| -114 | ERROR_OTA_SAME_FILE | 相同的升级文件 |
Source: jl_ota_2.1.1.js
设计意图:错误码把"协议层失败"与"升级业务失败"统一在一个命名空间,getErrorDesc(code, extra) 会拼出 "描述\n额外信息" 的字符串,页面层可直接展示;错误码负值取绝对值转十六进制可得到如 0x71 的调试编号(callbackOTAError 中 -t 后转 hex 输出)。
OTAImpl —— 升级流程状态机
OTAImpl 是流程库的核心,所有升级动作(启动、取消、断连恢复、进度计算、完成收尾)都在这里。它通过构造参数 this.A 注入一组"设备操作"接口,接口实现来自 RcspOTAManager:
isDeviceConnected()、readUpgradeFileFlag(cb)、inquiryDeviceCanOTA(fileData, cb)、enterUpdateMode(cb)、exitUpdateMode(cb)、receiveFileBlock(...)、queryUpdateResult(cb)、rebootDevice(cb)、changeCommunicationWay(...)、changeReceiveMtu()。
内部字段与超时常量
| 字段 | 含义 |
|---|---|
this.t | 固件数据 Uint8Array(C(t) 写入) |
this.i / this.l | 设备通知的总大小 / 当前已传输大小(notifyUpgradeSize) |
this.h | 当前 OTAConfig(非空即"正在升级",isOTA() 即 null != this.h) |
this.o | ReConnectMsg,非空表示正在等待/进行回连 |
this.u | 设备能力信息(双备份、需要 bootloader、强制升级) |
this.m | 事件分发器 f(包装 OTAUpgradeCallback) |
WAITING_CMD_TIMEOUT | 20000ms 等待命令响应超时(J() 启动) |
WAITING_DEVICE_OFFLINE_TIMEOUT | 6000ms 等待设备离线超时(it() → P() 启动) |
RECONNECT_DEVICE_TIMEOUT | 80000ms 回连总超时(gt() 启动) |
startOTA 的启动路径
startOTA(t, e) {
// 1. 参数校验:固件数据必须存在
if (null == t || null == t.updateFileData || 0 == t.updateFileData.length) {
const err = OTAError.ERROR_INVALID_PARAM; e && e.onError(err, getErrorDesc(err, ""))
}
// 2. 设备必须在线
else if (this.A.isDeviceConnected()) {
// 3. 防重入:已有升级则报 ERROR_OTA_IN_PROGRESS
if (this.isOTA()) {
const err = OTAError.ERROR_OTA_IN_PROGRESS
e && e.onError(err, getErrorDesc(err, "OTA is in progress. Please stop ota at first."))
} else {
this.v(t) // 保存 OTAConfig -> this.h,标记进入升级态
this.m.callback = e // 挂载升级回调
this._() // 触发 onStartOTA
// 4. 保存固件数据并进入 _readUpgradeFileFlag 阶段
t.updateFileData.length > 0 ? this.C(t.updateFileData)
: this.D(OTAError.ERROR_OTA_UPGRADE_FILE_ERROR, "...")
}
} else {
const err = OTAError.ERROR_DEVICE_OFFLINE
e && e.onError(err, getErrorDesc(err, ""))
}
}
Source: jl_ota_2.1.1.js
校验顺序的意图:先保证入参与连接状态合法,再防止重复进入,最后才写入状态并广播 onStartOTA,避免回调发生在非法状态下。
升级主流程(状态机)
C() 之后按顺序经过以下阶段,每个阶段由 U(name) 做"未在升级则拒绝"的守卫:
_readUpgradeFileFlag(K()):读取设备已存在的固件偏移FileOffset。若offset==0 && len==0表示全新升级,构造 1 字节数据(通信方式);否则按偏移从本地固件切片。_inquiryDeviceCanOTA(Y()):发送固件头查询设备可否升级,设备返回inquiryResult:0通过;1/2/3/4/5分别映射为低电量、升级文件错误、版本未变化、TWS 未连接、耳机不在充电仓。_checkUpdateEnvironment(H()):根据设备能力分叉——- 双备份 →
enterUpdateMode(N())→ 成功进入J()等待命令; - 需要 bootloader →
changeReceiveMtu()(把 MTU 提到RcspConstant.DEFAULT_PROTOCOL_MTU)→ 直接J(); - 强制升级 → 直接
enterUpdateMode; - 否则 →
readyToReconnectDevice(it()),走"断开→回连"流程(单备份的传输前切换通信方式)。
- 双备份 →
- 传输阶段:设备通过
CmdReadFileBlock(请求固件块)与CmdNotifyUpdateFileSize(上报总/当前大小)两个命令驱动数据流:gainFileBlock(offset,len)从本地切片并receiveFileBlock下发;notifyUpgradeSize(total,current)计算进度L(total,current) = min(99.9, 100*current/total),并按阶段映射为UpgradeType(有 bootloader 为CHECK_FILE阶段 0,否则FIRMWARE阶段 1)回调onProgress。收到offset==0 && len==0的块后进入queryUpdateResult。 queryUpdateResult(G()):设备返回0表示成功 →rebootDevice→ 清状态 → 100ms 后回调onStopOTA;返回128表示需要先回连再查询;其余码映射为数据校验失败/升级失败/密钥不匹配/文件损坏/类型错误/长度超限/Flash 异常/命令超时/同文件等错误。- 收尾:成功
q()或失败D()都会v(null)清空升级态、O()清进度与回连状态、bt()清除全部定时器,最后回调onStopOTA/onError并置空 callback。
断连与回连机制
onDeviceDisconnect() 触发时若正在升级:
- 若已有
ReConnectMsg(this.o非空)→ 记录日志并启动等待离线流程M()+P(300); - 否则直接
ERROR_DEVICE_OFFLINE失败。
P(timeout)(等待设备离线)到期后若仍处于升级且有待回连消息,则:清零进度 → 拷贝 ReConnectMsg → 回调 onNeedReconnect(Rt())→ 启动 RECONNECT_DEVICE_TIMEOUT(80s)计时 → 清空回连消息。onDeviceInit() 在回连成功后由 RcspOTAManager 回调:若设备能力为强制升级则直接进入固件阶段,否则回到正常握手继续传输。回连成功还会调用 updateRcspOpImpl(rcspImpl) 让 OTAImpl 使用新的 RCSP 实例继续命令交互。
cancelOTA 的分支语义
cancelOTA() {
if (this.U("cancelOTA")) return false // 未在升级直接拒绝
if (!this.A.isDeviceConnected()) { /* ERROR_DEVICE_OFFLINE */ return false }
if (this.u && this.u.isSupportDoubleBackup) { // 双备份:可中断
this.A.exitUpdateMode({ onResult: () => this.S(), // 成功 -> onCancelOTA
onError: (e, s) => { /* BAD_STATUS/BAD_RESULT -> onError, 否则 onCancelOTA */ } })
return true
}
// 单备份:升级不可打断
return false
}
Source: jl_ota_2.1.1.js
单备份设备返回 false 并打印"ota progress cannot be interrupted",这是由单区 flash 物理特性决定的:中断会导致设备变砖,因此库选择直接拒绝而非冒险。
RcspOTAManager —— 命令原语层
RcspOTAManager 是流程库对外的门面:构造时接收一个 RcspImpl(来自 RCSP 协议库),内部创建 R(命令调度器)与 OTAImpl,并注册 RCSP 回调。
命令与回调的桥接
R 构造时向 RcspImpl 注册 onRcspCallback,把协议事件翻译为 OTAImpl 的驱动:
| RCSP 事件 | 处理动作 |
|---|---|
onRcspInit | 读取设备信息(双备份/bootloader/强制升级标志),setDeviceBLEMac + onDeviceInit |
onRcspCommand(CmdReadFileBlock) | 防抖(50ms 内相同 SN 丢弃)→ 缓存命令到 this.vt → gainFileBlock(offset,len) 取数据并回包 |
onRcspCommand(CmdNotifyUpdateFileSize) | notifyUpgradeSize(total,current) 更新进度,并自动回 STATUS_SUCCESS 响应 |
onRcspCommand(CmdNotifyADVInfo) | 首次收到则发送 CmdControlADVStream(CTRL_OP_CLOSE) 停止设备广播(wt 标记只关一次) |
onConnectStateChange(断开) | onDeviceDisconnect() |
onRcspError / onRcspResponse | 透传/忽略 |
R 维护 this.vt 待响应命令队列:receiveFileBlock(offset,len,data,cb) 通过 St() 从队列中按 offset+len 精确匹配出原始命令 CmdReadFileBlock,把数据写入 cmd.getResponse().block 后回发;xt() 在收到新 CmdReadFileBlock 时入队。
升级原语方法
RcspOTAManager 暴露的每个方法都是"构造命令 → 经 R 发送 → IHandleResult 解析响应"的模板:
startOTA(otaConfig, cb):记录当前使用设备与 BLE MAC,然后交给OTAImpl.startOTA。cancelOTA()/isOTA()/getCurrentOTADevice()/release():透传或包装OTAImpl。readUpgradeFileFlag(cb)→CmdReadFileOffset,响应解析为FileOffset(offset,len)。inquiryDeviceCanOTA(fileData, cb)→CmdRequestUpdate(ParamRequestUpdate),取response.result。enterUpdateMode(cb)/exitUpdateMode(cb)→CmdEnterUpdateMode/CmdExitUpdateMode。queryUpdateResult(cb)→CmdQueryUpdateResult,取response.result。rebootDevice(cb)→CmdRebootDevice(OP_REBOOT)(仅上报成功,A类处理)。changeCommunicationWay(way, isNewReboot, cb)→CmdChangeCommunicationWay。stopNotifyADV(cb)→CmdControlADVStream(CTRL_OP_CLOSE)。changeReceiveMtu():若设备receiveMtu < DEFAULT_PROTOCOL_MTU,本地更新为默认 MTU(供 bootloader 场景提速)。
命令响应统一走 IHandleResult 包装:STATUS_SUCCESS 时调用 hasResult/handleResult 解析,否则以 ERROR_REPLY_BAD_STATUS 或 ERROR_REPLY_BAD_RESULT 报错,错误描述前缀为功能名(如 "changeCommunicationWay:..."),便于定位是哪一条命令失败。
OTAWrapper —— 面向业务的高层封装
otaWrapper.ts 用 TypeScript 把流程库包了一层,是 Demo 工程实际使用的入口(源码:otaWrapper.ts)。
核心职责
- 多设备 Map 管理:
_RcspOTAManagerMap、_RcspImplMap、_ReconnectMap、_AuthMap均以deviceId为键,支持多设备并行升级。 - RCSP 实例双模式:上层实现了
getRCSPImpl(如与jl-rcsp-op共用)则复用外部实例;否则由OTAWrapper内部创建并管理RcspImpl(需要sendData配合)。 - 回调透传与注册:
registerRcspCallback/unregisterRcspCallback把页面层监听器挂到_RcspCallbackManager。
startOTA 的封装
public startOTA(device: BluetoothDevice, otaConfig: OTAConfig, onUgradeCallback: OTAUpgradeCallback) {
const rcspImpl = this._getRCSPImpl(device.deviceId)
if (rcspImpl == undefined) { loge('rcspImpl undefined'); return }
const usingDevice = rcspImpl.getUsingDevice()
if (usingDevice == null) return
// fixme 考虑到后续开发人员可能不会注意有没有初始化成功,可能需要处理
if (rcspImpl.getDeviceInfo(usingDevice) == undefined) { loge('rcspImpl 没有初始化成功'); return }
if (otaConfig.updateFileData == undefined || otaConfig.updateFileData.length == 0) return
const OTAManager = new RcspOTAManager(rcspImpl)
this._RcspOTAManagerMap.set(device.deviceId, OTAManager)
try {
OTAManager.startOTA(otaConfig, {
onStartOTA: () => { onUgradeCallback.onStartOTA() },
onNeedReconnect: (reConnectMsg: ReConnectMsg) => {
// 回连成功应该把新设备和连接状态同步给 rcspOTA
const reconnectResultCallback: OnResultCallback<string> = {
onResult: (deviceId) => {
this._ReconnectMap.delete(deviceId)
const rcspImpl = this._getRCSPImpl(deviceId)
if (rcspImpl) { OTAManager.updateRcspOpImpl(rcspImpl) }
},
onError: (code, message) => { /* 不用处理,库里会自动超时 */ }
}
onUgradeCallback.onNeedReconnect(reConnectMsg, reconnectResultCallback)
// ... 内部回连逻辑见下文
}
// onProgress / onStopOTA / onCancelOTA / onError 均透传
})
} catch (err) { /* ... */ }
}
Source: otaWrapper.ts
注意 startOTA 前有三道前置校验(RCSP 实例存在、使用设备存在、设备信息已初始化、固件数据非空),与 OTAImpl 内的校验叠加,形成"上层防御 + 库内守卫"的双保险;代码注释 fixme 也坦承了"开发者可能没注意初始化成功"这一现实风险。
内部回连(isInnerReconnect)实现
当 OTAWrapperOption.isInnerReconnect() 返回 true 时(BluetoothOTAManager 中恒为 true),onNeedReconnect 内部启动 Reconnect 流程:
const op: ReconnectOp = {
startScanDevice: () => { this.sanDevice() }, // 重新扫描
isReconnectDevice: (scanDevice: BluetoothDevice) => { // 判定是否目标回连设备
let result = false
const oldDevice = OTAManager?.getCurrentOTADevice()
if (reConnectMsg.isSupportNewReconnectADV) { // 新回连:解析广播包中的 MAC
if (oldDeviceMac != undefined && oldDeviceMac !== "") {
const advertisStr = ab2hex(scanDevice.advertisData).toUpperCase()
const index = advertisStr.indexOf("D60541544F4C4A") // JL OTA 广播标识
if (index != -1 && scanDevice.advertisData) {
const unit8Array = new Uint8Array(scanDevice.advertisData)
const macArray = unit8Array.slice((index / 2) + 8, (index / 2) + 14).reverse()
result = oldDeviceMac == hex2Mac(macArray).toUpperCase()
}
}
} else { // 旧回连:deviceId 相同即可
if (oldDevice != undefined && oldDevice.deviceId == scanDevice.deviceId) result = true
}
return result
},
connectDevice: (device: BluetoothDevice) => { this.connectDevice(device) }
}
Source: otaWrapper.ts
新回连方式下,设备广播包中携带 D6 05 41 54 4F 4C 4A("D60541544F4C4A")标识,紧随其后的 6 字节(倒序)即设备 MAC;库用"广播标识定位 + MAC 反转比对"精确识别目标,避免多设备场景误连;旧回连方式则退化为 deviceId 相等判断。源码中保留了模糊匹配的日志打印(前 10 位前缀),用于回连场景的调试。
使用示例
示例一:页面层通过 BluetoothOTAManager 发起升级
pageUpdate.ts 是 Demo 的升级页面,它从流程库导入 OTAConfig、ReConnectMsg、UpgradeType 构造升级参数并监听回调:
import { OTAConfig, ReConnectMsg, UpgradeType } from "../../lib/jl_lib/jl_ota_2.1.1"
import { BluetoothEventCallback, BluetoothOTAManager } from "../../lib/bluetoothOTAManager"
// 构造 OTAConfig 并调用蓝牙 OTA 管理器
const otaConfig = new OTAConfig()
otaConfig.updateFileData = fileData // Uint8Array 固件数据
this.manager.startOTA(device, otaConfig, {
onStartOTA: () => { /* 更新 UI:显示开始升级 */ },
onProgress: (type: UpgradeType, progress: number) => {
// type: UPGRADE_TYPE_CHECK_FILE(0) 或 UPGRADE_TYPE_FIRMWARE(1)
// progress: 0 ~ 99.9
},
onNeedReconnect: (msg: ReConnectMsg, cb) => { /* 上层提示用户等待设备回连 */ },
onStopOTA: () => { /* 升级完成 */ },
onError: (code: number, message: string) => { /* 按 OTAError 码提示 */ }
})
Source: pageUpdate.ts
示例二:初始化 OTAWrapper(BluetoothOTAManager 构造)
const otaWrapperOption: OTAWrapperOption = {
isUseAuth: () => { return this._BluetoothConfigure.isUseAuth }, // 是否需要认证
isInnerReconnect: () => { return true }, // 使用库内部回连
sanDevice: () => { this.sanDevice() },
connectDevice: (device: BluetoothDevice) => {
const tempDev = new BTBean.BluetoothDevice()
Object.assign(tempDev, device)
this.connectDevice(tempDev)
},
disconnectDevice: (device: BluetoothDevice) => { /* 同上 Object.assign 后断开 */ },
sendData: (device: BluetoothDevice, data: Uint8Array) => {
const tempDev = new BTBean.BluetoothDevice()
Object.assign(tempDev, device)
if (this._bluetoothInstance.isConnected(tempDev)) {
BleSendDataHandler.sendData(device.deviceId, this.UUID_SERVICE, this.UUID_WRITE, data)
}
}
}
this._OTAWrapper = new OTAWrapper(otaWrapperOption)
Source: bluetoothOTAManager.ts
该示例展示了 OTAWrapperOption 的完整实现方式:所有蓝牙副作用(扫描/连接/断开/发数据)都由宿主注入,OTAWrapper 因此与具体蓝牙实现解耦。Object.assign 用于把流程库的 BluetoothDevice 转成蓝牙层自己的 BTBean.BluetoothDevice。
示例三:注册全局日志(app.ts)
import { setLogger as setOTALogger, setLogGrade as setOTALoggerGrade } from "./lib/jl_lib/jl_ota_2.1.1";
Source: app.ts
setLogger 注入日志实现(日志 tag 为 JLOTASDK),setLogGrade 设置级别(默认 1:仅打印 logv;级别越高级别号越大,logd=2、logi=3、logw=4、loge=5 的打印条件为 n<=级别),用于把 SDK 内部日志接入宿主日志系统。
核心流程:一次完整升级的时序
以下时序图展示了从页面发起升级到设备重启成功的完整交互(对应 OTAImpl 的 K() → Y() → H() → N()/it() → gainFileBlock → G() 路径):
sequenceDiagram
participant Page as 页面层(pageUpdate)
participant W as OTAWrapper
participant M as RcspOTAManager
participant I as OTAImpl 状态机
participant RCSP as RCSP协议库(RcspImpl)
participant Dev as BLE 设备
Page->>W: startOTA(device, OTAConfig, callback)
W->>M: new RcspOTAManager(rcspImpl) + startOTA
M->>I: startOTA(config, cb)
I-->>Page: onStartOTA()
I->>M: readUpgradeFileFlag()
M->>RCSP: CmdReadFileOffset
RCSP->>Dev: 发送命令
Dev-->>RCSP: FileOffset(offset,len)
RCSP-->>M: FileOffset
M-->>I: onResult(FileOffset)
I->>M: inquiryDeviceCanOTA(fileData)
M->>RCSP: CmdRequestUpdate
RCSP->>Dev: 发送命令
Dev-->>RCSP: inquiryResult
RCSP-->>M: result
M-->>I: onResult(result)
alt result == 0(允许升级)
I->>M: enterUpdateMode()
M->>RCSP: CmdEnterUpdateMode
RCSP->>Dev: 发送命令
Dev-->>RCSP: result
M-->>I: onResult(0)
else result != 0
I-->>Page: onError(ERROR_OTA_LOW_POWER / UPDATE_FILE / VERSION_NO_CHANGE / ...)
end
loop 固件传输(设备驱动)
Dev->>RCSP: CmdReadFileBlock(offset,len)
RCSP-->>M: onRcspCommand
M->>I: gainFileBlock(offset,len)
I->>M: receiveFileBlock(offset,len,data)
M-->>RCSP: 回包(带 block 数据)
RCSP-->>Dev: 响应
Dev->>RCSP: CmdNotifyUpdateFileSize(total,current)
RCSP-->>M: notifyUpgradeSize(total,current)
M-->>I: 更新进度
I-->>Page: onProgress(type, min(99.9, 100*current/total))
end
I->>M: queryUpdateResult()
M->>RCSP: CmdQueryUpdateResult
RCSP->>Dev: 发送命令
Dev-->>RCSP: result(0 成功 / 128 需回连 / 其他错误)
M-->>I: onResult
alt result == 0
I->>M: rebootDevice()
M->>RCSP: CmdRebootDevice(OP_REBOOT)
RCSP->>Dev: 发送命令
I-->>Page: onStopOTA()
else result == 128
I->>M: changeCommunicationWay(...) + 等待回连
I-->>Page: onNeedReconnect(ReConnectMsg)
else 其他
I-->>Page: onError(code, message)
end
关键设计点:
- 设备是传输节奏的主动方:固件数据不是由手机一口气推完,而是设备逐块发起
CmdReadFileBlock请求,手机"按需响应"。这保证设备 flash 写入速率与蓝牙吞吐匹配,也天然实现流控。 - 进度信息来自设备:
CmdNotifyUpdateFileSize由设备主动上报总/当前大小,OTAImpl据此计算百分比(封顶 99.9,最后的 100% 在onStopOTA前由W(100)补齐)。 - 成功路径以设备重启收尾:
queryUpdateResult返回 0 后先rebootDevice再回调onStopOTA,确保用户看到"完成"时设备已进入重启流程。
升级状态流转图
stateDiagram-v2
[*] --> Idle: 构造 OTAImpl
Idle --> Starting: startOTA 校验通过
Starting --> ReadFileFlag: onStartOTA 广播
ReadFileFlag --> InquiryOTA: 读取文件偏移完成
InquiryOTA --> EnterUpdate: inquiryResult==0
InquiryOTA --> Failed: 低电量/版本不变等
EnterUpdate --> Transferring: enterUpdateMode 成功
Transferring --> Reconnecting: 设备断连且有回连消息
Transferring --> QueryResult: 收到 offset=0,len=0 数据块
Reconnecting --> Transferring: 回连成功 onDeviceInit
Reconnecting --> Failed: RECONNECT_DEVICE_TIMEOUT(80s)
QueryResult --> Rebooting: result==0
QueryResult --> Reconnecting: result==128(需回连再查)
QueryResult --> Failed: 其他错误码
Rebooting --> Stopped: onStopOTA(进度100)
Starting --> Failed: 参数/离线/重复升级
Transferring --> Canceled: 双备份 cancelOTA
Failed --> [*]
Stopped --> [*]
Canceled --> [*]
配置选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
OTAConfig.communicationWay | number | COMMUNICATION_WAY_BLE(0) | 升级通信通道:BLE=0、SPP=1、USB=2 |
OTAConfig.isSupportNewRebootWay | boolean | false | 设备是否支持新的重启方式,影响回连/重启流程 |
OTAConfig.updateFileData | Uint8Array | 无(必填) | 固件数据,startOTA 前置校验非空 |
OTAImpl.WAITING_CMD_TIMEOUT | number | 20000ms | 单条命令等待响应超时 |
OTAImpl.WAITING_DEVICE_OFFLINE_TIMEOUT | number | 6000ms | 等待设备离线(为回连做准备)超时 |
OTAImpl.RECONNECT_DEVICE_TIMEOUT | number | 80000ms | 回连设备总超时 |
OTAWrapperOption.isUseAuth | () => boolean | 宿主决定 | 是否需要认证;上层已认证则返回 false |
OTAWrapperOption.isInnerReconnect | () => boolean | 宿主决定(Demo 为 true) | 是否使用库内部回连机制 |
setLogGrade | number | 1 | SDK 日志级别(1=v 2=d 3=i 4=w 5=e 的打印上限) |
| 蓝牙 MTU(manager 层) | number | 512 | BluetoothOTAManager 连接配置中设置的 MTU 最大值 |
| UUID(manager 层) | string | 0000ae00/01/02-... | 杰理 BLE 服务/写/通知特征 UUID |
API 参考
流程库导出(jl_ota_2.1.1.js)
startOTA(otaConfig: OTAConfig, callback: OnUpgradeCallback): void(RcspOTAManager / OTAImpl)
- 参数:
otaConfig升级配置(含固件数据);callback升级事件回调。 - 行为:校验参数与设备在线状态 → 防重入 → 广播
onStartOTA→ 按状态机推进。 - 失败回调:
onError(OTAError, message),典型码ERROR_INVALID_PARAM、ERROR_DEVICE_OFFLINE、ERROR_OTA_IN_PROGRESS、ERROR_OTA_UPGRADE_FILE_ERROR。
cancelOTA(): boolean
- 返回:
true表示已发起取消(仅双备份设备);false表示无法取消(单备份)或未在升级。 - 双备份成功路径回调
onCancelOTA;ERROR_REPLY_BAD_STATUS/RESULT时回调onError。
isOTA(): boolean —— 当前是否有升级在进行(OTAImpl.h != null)。
onDeviceInit(deviceInfo, success) / onDeviceDisconnect() —— 由 RcspOTAManager 在 RCSP 初始化/断连事件时驱动,用于回连恢复与断连处理。
notifyUpgradeSize(total, current) —— 设备上报传输大小,触发进度计算与 onProgress。
gainFileBlock(offset, len) —— 收到 CmdReadFileBlock 时取固件切片并 receiveFileBlock 回包。
getErrorDesc(code, extra) —— 错误码转可读描述("描述\n额外信息")。
ab2hex(buffer) —— ArrayBuffer 转十六进制字符串(回连广播解析等场景使用)。
OTAWrapper(otaWrapper.ts)
startOTA(device: BluetoothDevice, otaConfig: OTAConfig, onUgradeCallback: OTAUpgradeCallback): void
- 前置校验 RCSP 实例/使用设备/设备信息/固件数据,创建
RcspOTAManager并透传升级回调;onNeedReconnect中按isInnerReconnect决定内部回连或交给上层。
isRCSPInit(device): Promise<boolean> —— RCSP 是否已初始化(getDeviceInfo 存在)。
isNeedMandatoryUpgrade(device): Promise<boolean> —— 设备 mandatoryUpgradeFlag == FLAG_MANDATORY_UPGRADE 时为强制升级。
cancelOTA(device): void / isOTA(device): boolean | undefined —— 按设备取消/查询升级状态。
sendCustomCmd(device, data, callback): boolean —— 发送自定义 RCSP 命令。
getDeviceInfo(device): DeviceInfo | undefined —— 获取设备信息对象。
registerRcspCallback(cb: OTAWrapperListenner) / unregisterRcspCallback(cb) —— 注册/注销 RCSP 回调监听。
onConnectStateSuccess(dev) / onConnectStateDisconnect(dev) / onConnectStateFailed(dev) —— 由蓝牙层连接事件驱动,内部同步 RCSP/回连状态。
onReceiveData(dev, data) —— 收到 BLE 数据时分发到内部 RCSP 解析。
release(): void —— 释放所有 Map 中的管理器、回连器、认证对象与回调注册。
升级回调接口(OTAUpgradeCallback)
| 回调 | 参数 | 触发时机 |
|---|---|---|
onStartOTA() | — | 升级校验通过后 |
onNeedReconnect(msg, cb) | ReConnectMsg(含 deviceBleMac、isSupportNewReconnectADV) | 单备份/切换通信方式前需要回连 |
onProgress(type, progress) | UpgradeType + 0~99.9 | 设备上报传输进度 |
onStopOTA() | — | 成功收尾(进度置 100 后) |
onCancelOTA() | — | 取消成功 |
onError(code, message) | OTAError + 描述 | 任意失败路径 |
失败模式、边界情况与并发
失败模式总览
| 失败场景 | 错误码 | 处理路径 |
|---|---|---|
| 固件数据为空/配置缺失 | ERROR_INVALID_PARAM(-2) | startOTA 入口直接 onError |
| 设备离线发起升级 | ERROR_DEVICE_OFFLINE(-33) | 入口/写文件阶段直接 onError |
| 重复启动升级 | ERROR_OTA_IN_PROGRESS(-110) | 防重入守卫,提示先停止 |
| 设备电量低/版本未变/TWS 未连/未入仓 | -97/-99/-100/-101 | inquiryDeviceCanOTA 结果映射 |
| 升级中命令无响应 | ERROR_OTA_COMMAND_TIMEOUT(-111) | J() 的 20s 定时器触发 D() |
| 设备断连且无回连消息 | ERROR_DEVICE_OFFLINE(-33) | onDeviceDisconnect 直接失败 |
| 回连超时 | ERROR_OTA_RECONNECT_DEVICE_TIMEOUT(-112) | gt() 的 80s 定时器触发 D() |
| 设备回包状态/结果异常 | -65/-66 | IHandleResult.onCmdResponse 统一处理 |
| 固件校验/类型/长度/Flash 异常 | -102/-105/-106/-107/-108 | queryUpdateResult 结果映射 |
| 加密密钥不匹配 | ERROR_OTA_ENCRYPTED_KEY_NOT_MATCH(-104) | queryUpdateResult 结果映射 |
| 升级文件与设备一致 | ERROR_OTA_SAME_FILE(-114) | queryUpdateResult 结果映射 |
失败后统一收尾:D(code, msg) 清空 OTAImpl.h、清进度、清定时器、回调 onError 并置空 callback,保证一次失败不污染下一次升级。
边界情况
- 进度封顶 99.9:
L(total,current)计算中s>=100时强制99.9,避免传输完成前 UI 提前显示 100%;100% 仅在成功收尾q()时通过W(100)触发。 - 重复 SN 防抖:
R.onRcspCommand对CmdReadFileBlock做 50ms 内相同 SN 丢弃,防止设备重传/协议层重复投递导致重复取块。 - 空数据块语义:
gainFileBlock(0,0)是"数据传完"的信号,触发queryUpdateResult;receiveFileBlock对len==0且offset>0的非法请求回STATUS_INVALID_PARAM。 - 固件越界读取:
B(offset,len)在offset+len > 固件长度时返回空数组,调用方以ERROR_INVALID_PARAM("Read Data over Limit")失败。 - 强制升级直连:设备带
FLAG_MANDATORY_UPGRADE时跳过常规握手(onDeviceInit中直接UPGRADE_TYPE_FIRMWARE+enterUpdateMode),避免强制升级被普通流程误拦。
并发与重入
- 单设备防重入:
isOTA()是唯一权威判断,startOTA与cancelOTA都先过U(name)/isOTA()守卫。 - 多设备并行:
OTAWrapper按deviceId用 Map 隔离每个设备的RcspOTAManager/RcspImpl/Reconnect/Auth,理论上支持多设备同时升级;但同一 BLE 适配器下建议串行,避免 MTU/广播控制相互干扰。 - 回连期间的状态锁:
ReConnectMsg(this.o)非空期间再次断连不会重复创建回连任务,P()到期才真正发起回连并清空this.o。
性能与运维
- MTU 提升:bootloader 场景
changeReceiveMtu()会把设备 MTU 提升到DEFAULT_PROTOCOL_MTU(manager 层连接配置默认 512),减少分片、提升块传输吞吐。 - 传输节奏由设备主导:手机被动响应
CmdReadFileBlock,天然匹配设备 flash 写入速度,无需额外拥塞控制。 - 日志可观测性:
setLogger/setLogGrade把 SDK 日志(tagJLOTASDK)接入宿主;关键节点均有日志:inquiryDeviceCanOTA : >>>>>>>>>>>>、设备通知文件大小,totalSize、MSG_RECONNECT_DEVICE : start reconnect >>>>、callbackOTAError : has an exception, code = 0x..。logv级别的回连广播打印在回连场景有注释说明"打印太多有问题",即高频扫描日志已做降噪。 - 超时参数可读性:
WAITING_CMD_TIMEOUT=20s、WAITING_DEVICE_OFFLINE_TIMEOUT=6s、RECONNECT_DEVICE_TIMEOUT=80s是硬编码常量,如需调整需改库(当前版本未开放配置入口)。
扩展点
- 通信通道扩展:
OTAConfig.communicationWay支持 BLE/SPP/USB;changeCommunicationWay命令与RcspOTAManager已按通道抽象,接入新通道需在 RCSP 协议库层实现对应发送器。 - 回连策略替换:
OTAWrapperOption.isInnerReconnect()返回false时,onNeedReconnect完全交给上层实现(reconnectResultCallback.onResult(deviceId)通知库同步新 RCSP 实例),可接入自研回连服务。 - RCSP 实例复用:实现
getRCSPImpl(device)即可与jl-rcsp-op等上层 RCSP 操作库共用连接,避免重复初始化(对应RcspOTAManager.updateRcspOpImpl的动态切换)。 - 认证开关:
isUseAuth()控制升级前是否执行Auth;已在其他流程完成认证的应用可关闭,缩短升级前握手。 - 回调透传:
registerRcspCallback(OTAWrapperListenner)允许页面级监听器直接观察 RCSP 层事件(初始化、命令、连接状态),用于自定义 UI 或诊断。
相关链接
- OTAWrapper 高层封装源码
- BluetoothOTAManager 蓝牙管理源码
- jl_ota_2.1.1.js 流程库源码
- README 依赖库说明(3.3 节)
- 升级页面 pageUpdate.ts
- 相关目录页:RCSP-OTA 协议库(jl_rcsp_ota)、蓝牙适配层、认证库(jl_auth)、OTA 进度组件(otaProgressView)