杰理 SDK 文档中心
首页
首页
  • 项目概览与入门

    • 项目概述
    • 快速开始
  • OTA SDK 核心库

    • RCSP 认证库(jl_auth)
    • OTA 流程库(jl_ota)
    • RCSP-OTA 协议库(jl_rcsp_ota)
    • OTAWrapper 高层封装
  • 蓝牙通信与设备管理

    • 蓝牙连接生命周期管理
    • BLE 数据发送与 MTU 管理
    • 自动回连机制
  • 参考 Demo 小程序

    • 应用入口与页面导航
    • 设备连接页(pageConnect)
    • 固件升级页(pageUpdate)
    • 设置与调试页(pageSetting)
    • 自定义 UI 组件
    • 固件文件解析工具(upgradeFileUtil)
    • 日志系统

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 方案下表现不同(单备份升级过程不可中断)。把这些流程控制逻辑从页面代码中剥离出来、集中到一个库中,可以带来:

  1. 可复用:Demo、正式小程序、Android/iOS 客户端共用同一套流程语义。
  2. 可重入保护:通过 isOTA() 判断当前是否已有升级在进行,防止重复启动。
  3. 统一超时与错误语义:所有失败都收敛为 OTAError 错误码 + 描述字符串,上层 UI 只需按码处理。
  4. 断连自动恢复:库内建"等待设备离线 → 扫描回连 → 继续传输"的恢复机制。

库的两个使用层次

  • 高层封装(推荐):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):

导出类型作用
RcspOTAManagerclass升级管理器:持有 OTAImpl,把 RCSP 命令封装为升级原语
OTAImplclass升级流程状态机(核心逻辑)
OTAConfigclass升级配置:通信方式、是否支持新重启方式、固件数据
OTAErrorclass错误码常量集(-1 ~ -114)与 getErrorDesc
ReConnectMsgclass回连消息:设备 BLE MAC、是否支持新回连广播
FileOffsetclass文件偏移量(offset + len),用于按块读取固件
UpgradeTypeenumUPGRADE_TYPE_UNKNOWN=-1 / UPGRADE_TYPE_CHECK_FILE=0 / UPGRADE_TYPE_FIRMWARE=1
ab2hexfunctionArrayBuffer → 十六进制字符串
getErrorDescfunction错误码 → 可读描述
setLogger / setLogGradefunction注入日志实现与日志级别(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)两段,覆盖了升级中几乎所有的可预期失败:

错误码常量含义
0ERROR_NONE成功
-2ERROR_INVALID_PARAM参数非法(如固件数据为空)
-33ERROR_DEVICE_OFFLINE设备离线(启动升级、取消、断连时都会检查)
-65 / -66ERROR_REPLY_BAD_STATUS / ERROR_REPLY_BAD_RESULT设备返回了错误状态/结果
-97ERROR_OTA_LOW_POWER设备电量不足
-98ERROR_OTA_UPDATE_FILE升级固件信息错误
-99ERROR_OTA_FIRMWARE_VERSION_NO_CHANGE版本号与设备固件一致(重复升级)
-100ERROR_OTA_TWS_NOT_CONNECTTWS 耳机未连接
-101ERROR_OTA_HEADSET_NOT_IN_CHARGING_BIN耳机未放入充电仓
-102ERROR_OTA_DATA_CHECK_ERROR升级数据校验失败
-103ERROR_OTA_FAIL升级失败(通用)
-104ERROR_OTA_ENCRYPTED_KEY_NOT_MATCH加密密钥不匹配
-105ERROR_OTA_UPGRADE_FILE_ERROR升级文件损坏
-106ERROR_OTA_UPGRADE_TYPE_ERROR升级类型错误
-107ERROR_OTA_LENGTH_OVER升级长度超限
-108ERROR_OTA_FLASH_IO_EXCEPTIONFlash 读写异常
-109ERROR_OTA_CMD_TIMEOUT设备侧命令超时
-110ERROR_OTA_IN_PROGRESS已有升级在进行
-111ERROR_OTA_COMMAND_TIMEOUTSDK 等待命令响应超时
-112ERROR_OTA_RECONNECT_DEVICE_TIMEOUT等待回连设备超时
-113ERROR_OTA_USE_CANCEL用户取消升级
-114ERROR_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.oReConnectMsg,非空表示正在等待/进行回连
this.u设备能力信息(双备份、需要 bootloader、强制升级)
this.m事件分发器 f(包装 OTAUpgradeCallback)
WAITING_CMD_TIMEOUT20000ms 等待命令响应超时(J() 启动)
WAITING_DEVICE_OFFLINE_TIMEOUT6000ms 等待设备离线超时(it() → P() 启动)
RECONNECT_DEVICE_TIMEOUT80000ms 回连总超时(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) 做"未在升级则拒绝"的守卫:

  1. _readUpgradeFileFlag(K()):读取设备已存在的固件偏移 FileOffset。若 offset==0 && len==0 表示全新升级,构造 1 字节数据(通信方式);否则按偏移从本地固件切片。
  2. _inquiryDeviceCanOTA(Y()):发送固件头查询设备可否升级,设备返回 inquiryResult:0 通过;1/2/3/4/5 分别映射为低电量、升级文件错误、版本未变化、TWS 未连接、耳机不在充电仓。
  3. _checkUpdateEnvironment(H()):根据设备能力分叉——
    • 双备份 → enterUpdateMode(N())→ 成功进入 J() 等待命令;
    • 需要 bootloader → changeReceiveMtu()(把 MTU 提到 RcspConstant.DEFAULT_PROTOCOL_MTU)→ 直接 J();
    • 强制升级 → 直接 enterUpdateMode;
    • 否则 → readyToReconnectDevice(it()),走"断开→回连"流程(单备份的传输前切换通信方式)。
  4. 传输阶段:设备通过 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。
  5. queryUpdateResult(G()):设备返回 0 表示成功 → rebootDevice → 清状态 → 100ms 后回调 onStopOTA;返回 128 表示需要先回连再查询;其余码映射为数据校验失败/升级失败/密钥不匹配/文件损坏/类型错误/长度超限/Flash 异常/命令超时/同文件等错误。
  6. 收尾:成功 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

关键设计点:

  1. 设备是传输节奏的主动方:固件数据不是由手机一口气推完,而是设备逐块发起 CmdReadFileBlock 请求,手机"按需响应"。这保证设备 flash 写入速率与蓝牙吞吐匹配,也天然实现流控。
  2. 进度信息来自设备:CmdNotifyUpdateFileSize 由设备主动上报总/当前大小,OTAImpl 据此计算百分比(封顶 99.9,最后的 100% 在 onStopOTA 前由 W(100) 补齐)。
  3. 成功路径以设备重启收尾: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.communicationWaynumberCOMMUNICATION_WAY_BLE(0)升级通信通道:BLE=0、SPP=1、USB=2
OTAConfig.isSupportNewRebootWaybooleanfalse设备是否支持新的重启方式,影响回连/重启流程
OTAConfig.updateFileDataUint8Array无(必填)固件数据,startOTA 前置校验非空
OTAImpl.WAITING_CMD_TIMEOUTnumber20000ms单条命令等待响应超时
OTAImpl.WAITING_DEVICE_OFFLINE_TIMEOUTnumber6000ms等待设备离线(为回连做准备)超时
OTAImpl.RECONNECT_DEVICE_TIMEOUTnumber80000ms回连设备总超时
OTAWrapperOption.isUseAuth() => boolean宿主决定是否需要认证;上层已认证则返回 false
OTAWrapperOption.isInnerReconnect() => boolean宿主决定(Demo 为 true)是否使用库内部回连机制
setLogGradenumber1SDK 日志级别(1=v 2=d 3=i 4=w 5=e 的打印上限)
蓝牙 MTU(manager 层)number512BluetoothOTAManager 连接配置中设置的 MTU 最大值
UUID(manager 层)string0000ae00/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/-101inquiryDeviceCanOTA 结果映射
升级中命令无响应ERROR_OTA_COMMAND_TIMEOUT(-111)J() 的 20s 定时器触发 D()
设备断连且无回连消息ERROR_DEVICE_OFFLINE(-33)onDeviceDisconnect 直接失败
回连超时ERROR_OTA_RECONNECT_DEVICE_TIMEOUT(-112)gt() 的 80s 定时器触发 D()
设备回包状态/结果异常-65/-66IHandleResult.onCmdResponse 统一处理
固件校验/类型/长度/Flash 异常-102/-105/-106/-107/-108queryUpdateResult 结果映射
加密密钥不匹配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 日志(tag JLOTASDK)接入宿主;关键节点均有日志: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 是硬编码常量,如需调整需改库(当前版本未开放配置入口)。

扩展点

  1. 通信通道扩展:OTAConfig.communicationWay 支持 BLE/SPP/USB;changeCommunicationWay 命令与 RcspOTAManager 已按通道抽象,接入新通道需在 RCSP 协议库层实现对应发送器。
  2. 回连策略替换:OTAWrapperOption.isInnerReconnect() 返回 false 时,onNeedReconnect 完全交给上层实现(reconnectResultCallback.onResult(deviceId) 通知库同步新 RCSP 实例),可接入自研回连服务。
  3. RCSP 实例复用:实现 getRCSPImpl(device) 即可与 jl-rcsp-op 等上层 RCSP 操作库共用连接,避免重复初始化(对应 RcspOTAManager.updateRcspOpImpl 的动态切换)。
  4. 认证开关:isUseAuth() 控制升级前是否执行 Auth;已在其他流程完成认证的应用可关闭,缩短升级前握手。
  5. 回调透传: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)
Prev
RCSP 认证库(jl_auth)
Next
RCSP-OTA 协议库(jl_rcsp_ota)