杰理 SDK 文档中心
首页
首页
  • 概述与快速开始

    • 仓库概览
    • 运行环境与 SDK 集成
    • 工程结构与目录导航
  • 核心 SDK 架构

    • SDK 库体系与模块划分
    • 蓝牙连接与 RCSP 协议
    • 广播包解析与设备认证
    • 日志助手与调试支持
  • 设备功能模块

    • OTA 固件升级
    • 表盘管理与自定义表盘
    • 图像转换工具
    • 资源打包
    • 音频编解码
    • 健康与运动数据同步
    • 消息通知与实用设备功能
  • 宜动健康示例应用

    • 应用架构与页面导航
    • 健康界面与数据可视化
    • 设备连接与数据同步
    • 登录注册与用户中心
    • AI 云服务与语音交互
    • 本地数据库与持久化
    • 多语言国际化
  • 测试与调试

    • SDKTestHelper 功能测试工具
    • 音频编解码示例工程
    • 调试技巧与问题排查
  • 文档与资源

    • 在线文档与版本历史
    • 第三方框架与依赖管理

表盘管理与自定义表盘

本文档介绍 iOS-JL_Health SDK 中表盘管理与自定义表盘能力的完整实现,涵盖基于 fatfs 文件系统的表盘资源操作(浏览、插入、删除、替换、格式化)、表盘切换、自定义表盘背景激活,以及表盘市场的云端数据获取流程。

Purpose and Scope

本页覆盖表盘功能模块的端到端能力:

  • 表盘文件系统操作:连接设备后打开/重置表盘文件系统,浏览、添加、删除、替换表盘文件,格式化外部 Flash,查询剩余空间。
  • 表盘资源更新:通过资源文件(zip/ufw)批量更新设备表盘并对比版本。
  • 表盘切换与自定义背景:获取/设置当前表盘,激活与重置自定义表盘背景。
  • 表盘市场云端对接:通过 DialMarketHttp 查询产品信息、表盘列表、商城列表与下载信息。

以下相关主题属于其他页面,不在本页展开:AI 表盘(JLAIDialManager,见 AI 云服务相关页面)、表盘图片转码(JLBmpConvertKit.xcframework)、表盘 res 资源打包(JLPackageResKit.xcframework)、OTA 固件升级(JL_OTALib.xcframework)。

Overview

表盘管理是杰理蓝牙穿戴设备的核心功能之一。设备端表盘以文件形式存储在外部 Flash 的 fatfs 文件系统中,App 通过 BLE 通道对文件系统执行打开、遍历、读写、删除等操作。SDK 为此提供 JLDialUnit.xcframework 框架,其中包含两代 API:

  1. DialManager(经典类方法 API):以 fatfs 文件操作语义直接暴露接口,方法均为类方法(+),配合 JL_ManagerM 蓝牙管理对象使用。它负责文件系统生命周期(打开/重置/格式化)、单文件增删改查以及资源包更新。
  2. JLDialUnitMgr(面向对象 API,V1.12.0+):以实例方法封装表盘操作,内部维护设备文件列表(fileArray)、当前表盘(currentFileSource)与自定义背景(currentBackground),基于 JLFlashOpMgr Flash 操控句柄工作,提供分页获取文件列表、批量删除、剩余空间查询及表盘背景操作等高级能力。

在应用层(JieliJianKang target),表盘市场功能由 DialMarketHttp 单例实现,通过 HTTPS 与云端 health/v1/api/watch/* 系列接口交互,携带 JWT token 认证,并将响应解析为 ProductInfoModel、DialNetInfoModel、DialPageRecords、DialMallRecords、DialFreeDownloadModel、DialPayDownloadModel 等模型。

flowchart TD
    subgraph sg_App["应用层 JieliJianKang"]
        UI["表盘管理 UI / 表盘市场 UI"]
        HTTP["DialMarketHttp<br/>(URLSession + URLCache 单例)"]
    end

    subgraph sg_SDK["JLDialUnit.xcframework"]
        DM["DialManager<br/>(经典类方法 API)"]
        UM["JLDialUnitMgr<br/>(面向对象 API)"]
        MODEL["JLDialSourceModel<br/>(继承 JLModel_File)"]
        UM --> MODEL
    end

    subgraph sg_BLE["JL_BLEKit.xcframework"]
        MGR["JL_ManagerM<br/>(蓝牙管理对象)"]
        FLASH["JLFlashOpMgr<br/>(Flash 操控句柄)"]
    end

    subgraph sg_Cloud["杰理云端服务"]
        API["health/v1/api/watch/*<br/>(产品/表盘/商城/下载)"]
    end

    UI --> HTTP
    HTTP -->|"HTTPS + JWT"| API
    UI --> DM
    UI --> UM
    DM --> MGR
    UM --> MGR
    UM --> FLASH
    FLASH --> MGR
    MGR -->|"BLE 通道"| DEVICE["穿戴设备<br/>(外部 Flash fatfs)"]

设计意图:DialManager 直接映射 fatfs 操作语义,适合底层工具场景(如资源升级工具);JLDialUnitMgr 则在之上建立"表盘"领域模型(当前表盘、自定义背景、文件列表缓存),让业务层可以按表盘而非按文件来编程,降低自定义表盘业务(背景激活/重置)的实现复杂度。两套 API 可并存,业务层按需选用。

Architecture

分层结构

表盘能力在架构上分为四层:

层组件职责
应用层DialMarketHttp、表盘管理 UI云端数据获取、用户交互、业务编排
能力层DialManager、JLDialUnitMgr表盘文件系统操作、表盘领域操作(切换/背景)
传输层JL_ManagerM、JLFlashOpMgrBLE 指令封装、Flash 句柄管理、分包传输
设备层外部 Flash fatfs表盘文件持久化存储

核心组件职责

DialManager — 表盘管理类,全部为类方法。连接成功后必须调用一次 openDialFileSystemWithCmdManager:withResult: 打开文件系统;提供文件查询(listFile:)、添加(addFile:Content:Result:)、删除(deleteFile:Result: / deleteDialResourceWithFileModel:Result:)、替换(repaceFile:Content:Result:)、格式化(formatFlash:Result:)、资源更新(updateResourcePath:List:Result:)与空间查询(getFatfsFree)等操作。所有异步操作通过 DialOperateBK/DialUpdateBK/DialListBK 回调进度与结果状态。

JLDialUnitMgr — 表盘单元管理器。初始化时注入 JL_ManagerM,内部创建 JLFlashOpMgr 操控句柄;维护 fileArray(设备表盘文件列表,元素为 JLDialSourceModel)、currentFileSource(当前表盘)、currentBackground(自定义背景)。提供分页文件列表(getFileList:count:completion:)、文件上传(updateFileToDevice:Data:FilePath:completion:)、单个/批量删除、剩余空间查询、当前表盘获取/设置(dialGetCurrent: / dialSetCurrent:Completion:)、自定义背景激活/重置(dialActiveCustomBackground:Completion: / dialResetCustomBackground:)。

JLDialSourceModel — 表盘文件数据源模型,继承 JL_BLEKit 的 JLModel_File,在基类基础上补充 size(文件大小)与 crc(CRC 校验值)字段,并提供 createModel: 工厂方法将底层 JLModel_File 转换为表盘模型。

DialMarketHttp — 表盘市场 HTTP 单例。内部配置 URLCache(10MB 内存 + 100MB 磁盘),所有请求携带 jwt-token 与 Content-Type: application/json 头;提供产品信息(getProductInfo)、表盘信息(getDialInfo,支持付费与免费两种端点)、表盘列表(getDialList)、商城列表(getDialMallList)、免费下载信息(requireDownloadDialInfoFree)与付费下载信息(requireDownloadDialInfo)查询。

核心实现:DialManager 表盘文件系统操作

DialManager 是整个表盘能力的底层引擎,源码位于 JLDialUnit.xcframework 的 DialManager.h。其设计围绕"设备外部 Flash 上的 fatfs 文件系统"展开,所有操作以文件为单位,文件名需带斜杠前缀(如 "/WATCH1")。

操作状态枚举 DialOperateType

每个文件系统操作通过 DialOperateBK 回调返回状态与进度(0~1 浮点):

typedef NS_ENUM(NSInteger, DialOperateType) {
    DialOperateTypeNoSpace     = 0,     //空间不足
    DialOperateTypeDoing       = 1,     //正在操作
    DialOperateTypeFail        = 2,     //操作失败
    DialOperateTypeSuccess     = 3,     //操作成功
    DialOperateTypeUnnecessary = 4,     //无需重复打开文件系统
    DialOperateTypeResetFial   = 5,     //重置文件系统失败
    DialOperateTypeNormal      = 6,     //文件系统正常
    DialOperateTypeCmdFail     = 7,     //流程命令执行失败
};
typedef void(^DialOperateBK)(DialOperateType type, float progress);

Source: DialManager.h

状态枚举覆盖了文件系统操作的完整生命周期:Doing(进行中,progress 递增)→ Success/Fail/NoSpace(终态)。Unnecessary 与 Normal 专用于文件系统打开场景——当文件系统已打开时返回 Unnecessary 表示无需重复打开;ResetFial 与 CmdFail 则标识重置流程中的命令级失败。

文件系统生命周期管理

/// 表盘管理类
@interface DialManager : NSObject

#pragma mark - 连接成功后,必须调用一次!
+(void)openDialFileSystemWithCmdManager:(JL_ManagerM *)manager withResult:(DialOperateBK)result;

#pragma mark - 重置表盘系统
+(void)resetDialFileSystemWithCmdManager:(JL_ManagerM *)manager withResult:(DialOperateBK)result;

Source: DialManager.h

openDialFileSystemWithCmdManager:withResult: 是连接成功后必须调用一次的入口(头文件注释明确强调)。它负责在设备端初始化 fatfs 文件系统上下文,之后 listFile: / addFile: 等操作才有可用的句柄。若文件系统已打开,回调返回 DialOperateTypeUnnecessary,调用方据此跳过重复打开。resetDialFileSystemWithCmdManager:withResult: 用于重置文件系统状态,失败时返回 DialOperateTypeResetFial。

设计意图:将文件系统打开作为显式的生命周期方法,而非在首次使用时隐式打开,是为了让调用方明确感知设备端文件系统的初始化成本(涉及 BLE 命令往返与设备端挂载),并能在失败时重试或提示用户重新连接。

文件增删改查

#pragma mark - 查询文件
+(void)listFile:(DialListBK __nullable)result;

#pragma mark - 添加文件
+(void)addFile:(NSString*)file        // 文件名需要加斜杠,类似@"/WACTH1"
       Content:(NSData*)content
        Result:(DialOperateBK)result;

#pragma mark - 删除文件
+(void)deleteFile:(NSString*)file
           Result:(DialOperateBK)result;

+(void)deleteDialResourceWithFileModel:(JLModel_File*)fileModel Result:(DialOperateBK)result;

#pragma mark - 替换文件
+(void)repaceFile:(NSString*)file
          Content:(NSData*)content
           Result:(DialOperateBK)result;

Source: DialManager.h

  • listFile: 通过 DialListBK 返回 NSArray 文件列表(元素为 JLModel_File 或其子类)。
  • addFile:Content:Result: 以文件名 + NSData 内容写入新表盘文件,传输期间回调 Doing 进度。
  • deleteFile:Result: 按文件名删除;deleteDialResourceWithFileModel:Result: 按 JLModel_File 模型删除(删除表盘资源时内部会同步清理文件大小缓存)。
  • repaceFile:Content:Result: 以同名覆盖方式替换文件,适用于表盘升级场景。

边界约束:文件名必须加斜杠前缀(如 "/WATCH1"),这是 fatfs 根目录路径的硬性约定;空间不足时返回 DialOperateTypeNoSpace。

空间管理与资源更新

#pragma mark - 格式化外部Flash
+(void)formatFlash:(NSString*)handle Result:(DialOperateBK)result;

#pragma mark - 更新设备的表盘资源(异步调用)
+(void)updateResourcePath:(NSString*)path
                     List:(NSArray*)array
                   Result:(DialUpdateBK)result;

+(uint32_t)getFatfsFree;
+(void)getFatfsFree:(FatfsFreeBlock) block;
+(uint32_t)getSizeOfFileName:(NSString*)fileName;
+(void)setFileSize:(uint32_t)size FileName:(NSString*)fileName;

Source: DialManager.h

  • formatFlash:Result: 以设备句柄格式化外部 Flash,用于清空全部表盘数据。
  • updateResourcePath:List:Result: 是异步资源包更新入口:传入本地资源文件路径与当前表盘列表,SDK 内部完成 zip 解压、资源对比、按需新增/替换,通过 DialUpdateBK 回调每个文件的结果与进度。
  • getFatfsFree(同步阻塞,注释要求异步执行)与带回调版本 getFatfsFree: 查询 fatfs 剩余空间,用于下载前容量预检。
  • getSizeOfFileName: / setFileSize:FileName: 负责文件大小查询与缓存记录(setFileSize 标注为私有接口)。

资源更新结果枚举 DialUpdateResult

typedef NS_ENUM(NSInteger, DialUpdateResult) {
    DialUpdateResultFinished    = 0,    //更新资源完成
    DialUpdateResultNewest      = 1,    //资源已最新
    DialUpdateResultInvalid     = 2,    //资源无效
    DialUpdateResultEmpty       = 3,    //资源为空
    DialUpdateResultReplace     = 4,    //资源替换
    DialUpdateResultAdd         = 5,    //资源新增
    DialUpdateResultNoSpace     = 6,    //空间不足
    DialUpdateResultZipError    = 7,    //ZIP资源文件错误
    DialUpdateResultCompareFail = 8,    //表盘资源对比失败
    DialUpdateResultUpdateUfw   = 9,    //更新固件 ufw
};
typedef void(^DialUpdateBK)(DialUpdateResult updateResult,
                            NSArray* __nullable array,
                            NSString * _Nullable filePath,
                            NSInteger index ,float progress);

Source: DialManager.h

该枚举完整刻画了资源更新器的判定逻辑:先校验资源有效性(Invalid/Empty/ZipError),再与设备现有列表对比(Newest 表示已最新、Replace 表示替换、Add 表示新增),最后执行写入(Finished/NoSpace)。DialUpdateResultUpdateUfw 表示需要联动更新固件 ufw 文件。

核心实现:JLDialUnitMgr 面向对象表盘管理

JLDialUnitMgr(2025 年新增,JLDialSourceModel.h 同批引入)为业务层提供以"表盘对象"为中心的编程模型,封装了底层文件系统细节。

状态持有与初始化

@interface JLDialUnitMgr : NSObject

/// 设备文件列表
@property (nonatomic,strong)NSMutableArray <JLDialSourceModel *> *fileArray;

/// 当前 Flash 操控句柄
@property (nonatomic, strong) JLFlashOpMgr *currentFlaghOpMgr;

/// 当前使用的表盘文件
@property (nonatomic,strong)JLDialSourceModel *currentFileSource;

/// 当前使用的表盘背景
@property (nonatomic,strong)JLDialSourceModel *_Nullable currentBackground;

/// 初始化
-(instancetype)initWithManager:(JL_ManagerM*)mgr completion:(void (^)(NSError * _Nullable))completion;

Source: JLDialUnitMgr.h

初始化时注入蓝牙管理对象 JL_ManagerM,SDK 内部完成文件系统打开与 JLFlashOpMgr 句柄创建,通过 completion 回调告知成功或 NSError。实例持有的 fileArray 缓存设备端表盘列表(元素为 JLDialSourceModel),currentFileSource 与 currentBackground 分别记录当前表盘与自定义背景,供 UI 层直接绑定展示。

文件列表与传输

/// 获取文件列表(cardType: 设备存储的类型; count: 请求的数目; 返回累计数目)
-(void)getFileList:(JL_CardType)cardType count:(NSInteger)count
        completion:(void (^)(NSArray <JLDialSourceModel *> * _Nullable,NSError * _Nullable))completion;

/// 更新文件到设备(state: 0:成功 1:传输中 -1:失败; progress: 传输进度)
-(void)updateFileToDevice:(JL_FileHandleType)fileHandleType Data:(NSData *)fileData
                 FilePath:(NSString *)filePath
               completion:(void (^)(int,double,NSError * _Nullable))resultCompletion;

/// 删除文件 / 删除多个文件
-(void)deleteFile:(NSString *)filePath getFileList:(BOOL)isGet
       Completion:(void (^)(BOOL state, NSError * _Nullable error))resultCompletion;
-(void)deleteFiles:(NSArray <NSString*>*) filePathList
        Completion:(void (^)(BOOL state, NSError * _Nullable error))resultCompletion;

/// 获取剩余空间
-(void)getRemainingSpace:(void(^)(int freeSize, NSError * _Nullable error))completion;
-(void)getFlashRemainingSpace:(void(^)(int freeSize, NSError * _Nullable error))completion;

Source: JLDialUnitMgr.h

  • getFileList:count:completion: 支持按设备存储类型(JL_CardType)分页拉取,回调返回累计数目——多次调用结果会追加到 fileArray,cleanFileList 可清空缓存重新拉取。
  • updateFileToDevice:Data:FilePath:completion: 面向指定存储句柄(JL_FileHandleType)上传文件,回调三元组 (state, progress, error) 中 state 约定为 0 成功、1 传输中、-1 失败。
  • 删除接口提供单文件(可选项 isGet 决定删除后是否自动刷新列表)与批量(deleteFiles:)两种形态,返回布尔状态与错误。
  • 两个剩余空间接口分别对应新老协议版本:getRemainingSpace: 与 getFlashRemainingSpace:(V1),业务层需根据设备固件能力选择。

表盘操作与自定义背景

//MARK: 表盘操作
/// 获取当前正在使用的表盘
-(void)dialGetCurrent:(void (^)(JLDialSourceModel * _Nullable))completion;

/// 设置当前表盘
-(void)dialSetCurrent:(JLDialSourceModel *)model
           Completion:(void (^)(BOOL state, NSError * _Nullable error))resultCompletion;

/// 激活自定义表盘背景
-(void)dialActiveCustomBackground:(JLDialSourceModel *)model
                       Completion:(void (^)(BOOL state, NSError * _Nullable error))resultCompletion;

/// 重置自定义表盘背景
-(void)dialResetCustomBackground:(void (^)(BOOL state, NSError * _Nullable error))resultCompletion;

/// 获取表盘背景
-(void)dialGetDial:(NSString *)path
    BackgroundInfo:(void (^)(JLDialSourceModel * _Nullable model, NSError * _Nullable error))resultCompletion;

Source: JLDialUnitMgr.h

这是自定义表盘业务的核心接口:

  • dialSetCurrent:Completion: 将某个已存在的表盘模型设为当前使用表盘(设备端切换生效),成功后更新 currentFileSource。
  • dialActiveCustomBackground:Completion: 将用户自定义背景(一张经 JLBmpConvertKit 转码后的图片数据)激活到当前表盘——这是"自定义表盘"的关键一步:先选背景数据,再绑定到当前表盘。
  • dialResetCustomBackground: 恢复表盘默认背景,撤销自定义。
  • dialGetDial:BackgroundInfo: 按路径(如 "/WATCH")查询指定表盘的背景信息,用于编辑场景下回显已应用的自定义背景。

设计意图:自定义表盘被建模为"当前表盘 + 可替换背景"两个独立状态,currentBackground 与 currentFileSource 分离,使得背景激活、重置、切换表盘三个操作可以独立编排,互不阻塞——切换表盘后背景自动跟随或重置由业务层决定。

数据模型:JLDialSourceModel

JLDialSourceModel 是表盘文件在 SDK 中的统一领域模型,继承自 JL_BLEKit 的 JLModel_File(设备文件基类),在基类基础上补充表盘特有的元数据:

@interface JLDialSourceModel : JLModel_File

/// 文件大小
@property (nonatomic, assign)int size;

/// CRC
@property (nonatomic, assign)uint16_t crc;

+(JLDialSourceModel *)createModel:(JLModel_File *)model;

@end

Source: JLDialSourceModel.h

  • size:表盘资源文件大小(字节),用于 UI 展示与容量预算。
  • crc:文件 CRC16 校验值,用于表盘资源对比(DialUpdateResultCompareFail 场景即依赖此类校验)。
  • createModel::工厂方法,将底层 JLModel_File 实例转换为 JLDialSourceModel,JLDialUnitMgr.updateFileSources:completion: 内部即用它批量转换并回填 fileArray。

文件大小缓存机制

DialManager 提供 getSizeOfFileName: / setFileSize:FileName: 一对接口:首次列出文件时记录大小,后续按名查询时直接命中缓存,避免重复 BLE 往返。这是表盘列表滚动场景下性能优化的关键(setFileSize 为私有接口,由 SDK 内部在 listFile: 完成后自动调用)。

erDiagram
    JLModel_File ||--o| JLDialSourceModel : "继承/扩展"
    JLDialSourceModel {
        int size "文件大小"
        uint16_t crc "CRC 校验"
    }
    JLModel_File {
        string filePath "设备路径 /WATCH1"
        int fileSize "基类大小"
    }
    JLModel_File ||--o{ JLFlashOpMgr : "由句柄枚举"
    JLDialUnitMgr ||--o{ JLDialSourceModel : "fileArray 缓存"
    JLDialUnitMgr ||--o| JLDialSourceModel : "currentFileSource"
    JLDialUnitMgr ||--o| JLDialSourceModel : "currentBackground"

核心实现:DialMarketHttp 表盘市场云端对接

DialMarketHttp(应用层 Swift 单例)负责表盘市场与杰理云端的全部 HTTP 交互。它以 BasicHttp.basicURL() 为 base URL,请求统一携带 jwt-token(来自 User_Http.shareInstance().token)与 Content-Type: application/json 头,超时 10 秒,响应按 code == 0 判定成功并解析 data 字段。

缓存与网络配置

private override init() {
    super.init()
    cache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: "NetCache")
    let config = URLSessionConfiguration.default
    config.urlCache = cache
    session = URLSession(configuration: config,delegate: self,delegateQueue: .main)
}

Source: DialMarketHttp.swift

单例初始化时构建 URLCache(10MB 内存 + 100MB 磁盘,磁盘路径 NetCache),URLSession 的 delegateQueue 为主队列,回调默认回到主线程。clearCache() 可手动清空全部缓存响应。

云端接口一览

方法端点用途
getProductInfo(_:_:completion:)/health/v1/api/watch/shop/onebypidvid?pid=&vid=根据 pid/vid 查询手表产品信息 → ProductInfoModel
getDialInfo(_:_:_:_:completion:)/health/v1/api/watch/dial/version/onebyuuid 或 /health/v1/api/watch/shop/onebyuuid根据表盘 UUID 查询表盘信息(isPay=true 走商城端点)→ DialNetInfoModel
getDialList(_:completion:)/health/v1/api/watch/dial/version/pagebyversion按版本分页获取表盘列表(针对不支持支付表盘的设备)→ DialPageRecords
getDialMallList(_:_:_:_:completion:)/health/v1/api/watch/shop/page?dialid=&page=&size=&isfree=表盘商城列表(分页 + 免费过滤)→ DialMallRecords
requireDownloadDialInfoFree(_:_:_:_:)/health/v1/api/watch/dial/version/onebyuuid?uuid=&pid=&vid=获取免费表盘下载信息 → DialFreeDownloadModel
requireDownloadDialInfo(_:_:)/health/v1/api/watch/shop/downloadbyid?id=获取付费表盘下载信息 → DialPayDownloadModel

请求模式示例

/// 根据表盘唯一UUID、PID、VID获取表盘信息
func getDialInfo(_ pid:String,_ vid:String,_ uuid:String,_ isPay:Bool, completion:@escaping (_ dial:DialNetInfoModel?)->()) {
    var url = baseUrl + "/health/v1/api/watch/dial/version/onebyuuid?uuid=" + uuid + "&pid=" + pid + "&vid=" + vid
    if isPay {
        url = baseUrl + "/health/v1/api/watch/shop/onebyuuid?uuid=" + uuid + "&pid=" + pid + "&vid=" + vid
    }
    let request = NSMutableURLRequest(url: URL(string: url)!,cachePolicy: .useProtocolCachePolicy,timeoutInterval: 10)
    request.httpMethod = "POST"
    request.allHTTPHeaderFields = [
        "jwt-token":User_Http.shareInstance().token,
        "Content-Type": "application/json",
    ]
    let dataTask = makeTask(request){ (data, err) in
        if let data = data {
            if let dict = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String:Any] {
                if let code = dict["code"] as? Int,code == 0,let data = dict["data"] as? [String:Any] {
                    let dial = DialNetInfoModel(data)
                    completion(dial)
                }else{
                    JLLogManager.logLevel(.DEBUG, content: "url:\(url)\ngetDialInfo error: \(dict)")
                    completion(nil)
                }
            }else{
                JLLogManager.logLevel(.DEBUG, content: "url:\(url)\ngetDialInfo error: \(String(describing: err))")
                completion(nil)
            }
        }
    }
    dataTask.resume()
}

Source: DialMarketHttp.swift

设计要点:

  • 免费与付费表盘走不同的服务端点(dial/version/* 与 shop/*),由 isPay 参数在调用侧决定,云端据此区分计费与免费资源。
  • 所有响应先做 JSON 序列化,再检查业务码 code == 0,失败统一记录 JLLogManager DEBUG 日志并回调 nil;调用方需自行处理 nil 兜底。
  • 列表类接口(getDialList/getDialMallList)显式 DispatchQueue.main.async 保证回调主线程;信息类接口依赖 URLSession 主队列 delegateQueue。
  • 分页参数 page 从 1 开始,size 为每页条数,isfree 过滤免费/付费。

核心流程

表盘切换时序

sequenceDiagram
    participant UI as 表盘管理 UI
    participant UM as JLDialUnitMgr
    participant DM as DialManager
    participant FLASH as JLFlashOpMgr
    participant DEV as 设备外部Flash

    UI->>UM: initWithManager:completion:
    activate UM
    UM->>DM: openDialFileSystemWithCmdManager:withResult:
    DM->>DEV: BLE 打开 fatfs
    DEV-->>DM: DialOperateTypeSuccess
    DM-->>UM: completion(nil)
    deactivate UM

    UI->>UM: getFileList:count:completion:
    activate UM
    UM->>FLASH: 枚举文件(分页)
    FLASH->>DEV: 读取目录
    DEV-->>FLASH: JLModel_File[]
    FLASH-->>UM: 文件列表
    UM->>UM: createModel 转换为 JLDialSourceModel[] 并缓存 fileArray
    UM-->>UI: 表盘列表

    UI->>UM: dialSetCurrent:Completion:
    activate UM
    UM->>FLASH: 切换当前表盘命令
    FLASH->>DEV: 设置生效表盘
    DEV-->>FLASH: 成功
    FLASH-->>UM: state=YES
    UM->>UM: 更新 currentFileSource
    UM-->>UI: 完成回调
    deactivate UM

自定义表盘背景流程

flowchart TD
    Start([用户选择自定义背景]) --> P1["图片转码<br/>JLBmpConvertKit 生成设备格式数据"]
    P1 --> P2{"目标表盘是否已存在?"}
    P2 -->|"否"| P3["updateFileToDevice:Data:FilePath:<br/>上传表盘文件"]
    P3 --> P4["dialSetCurrent:<br/>设为当前表盘"]
    P2 -->|"是"| P4
    P4 --> P5["dialActiveCustomBackground:<br/>激活自定义背景"]
    P5 --> P6{"激活成功?"}
    P6 -->|"是"| P7["currentBackground 更新<br/>UI 展示自定义效果"]
    P6 -->|"否"| P8["dialResetCustomBackground:<br/>回退默认背景"]
    P7 --> End([完成])
    P8 --> End

自定义表盘的本质是"表盘文件 + 背景覆盖"两层:表盘文件决定布局与指针逻辑,背景覆盖决定视觉外观。dialActiveCustomBackground: 把转码后的背景数据绑定到当前表盘,失败时通过 dialResetCustomBackground: 恢复默认背景,保证设备端始终处于有效状态。

表盘市场下载流程

flowchart LR
    A["getProductInfo<br/>(pid/vid)"] --> B["getDialMallList / getDialList<br/>(分页浏览)"]
    B --> C{"免费 or 付费?"}
    C -->|"免费"| D["requireDownloadDialInfoFree<br/>(uuid/pid/vid)"]
    C -->|"付费"| E["getDialInfo isPay=true<br/>→ requireDownloadDialInfo<br/>(downloadbyid)"]
    D --> F["拿到下载 URL + 版本信息"]
    E --> F
    F --> G["下载表盘资源<br/>(URLCache 缓存)"]
    G --> H["DialManager.updateResourcePath:List:<br/>资源对比/写入设备"]
    H --> I["DialUpdateResultFinished"]

使用示例

以下示例均从仓库实际源码或公开头文件中提取,展示各层 API 的典型调用形态。

1. 打开表盘文件系统(连接成功后必调)

// 连接成功后,必须调用一次!
[DialManager openDialFileSystemWithCmdManager:manager withResult:^(DialOperateType type, float progress) {
    if (type == DialOperateTypeSuccess) {
        // 文件系统已就绪,可以开始浏览/操作表盘
    } else if (type == DialOperateTypeUnnecessary) {
        // 文件系统已打开,无需重复打开
    } else if (type == DialOperateTypeFail) {
        // 打开失败,可提示用户重试或重新连接
    }
}];

Source: DialManager.h

2. 查询文件列表并添加/替换表盘文件

// 查询文件
[DialManager listFile:^(DialOperateType type, NSArray * _Nullable array) {
    if (type == DialOperateTypeSuccess) {
        // array 为设备端表盘文件列表(JLModel_File 类型)
    }
}];

// 添加文件:文件名需要加斜杠,类似@"/WATCH1"
[DialManager addFile:@"/WATCH1" Content:dialData Result:^(DialOperateType type, float progress) {
    if (type == DialOperateTypeDoing) {
        // 传输中,progress 0~1
    } else if (type == DialOperateTypeSuccess) {
        // 添加成功
    } else if (type == DialOperateTypeNoSpace) {
        // 空间不足
    }
}];

// 替换文件(升级场景)
[DialManager repaceFile:@"/WATCH1" Content:newDialData Result:^(DialOperateType type, float progress) {
    // 同上
}];

Source: DialManager.h

3. 面向对象方式:初始化 + 分页获取表盘列表

// 初始化(内部自动打开文件系统、创建 Flash 句柄)
JLDialUnitMgr *mgr = [[JLDialUnitMgr alloc] initWithManager:bleManager completion:^(NSError * _Nullable error) {
    if (error == nil) {
        // 获取文件列表(分页,返回累计数目)
        [mgr getFileList:JL_CardTypeFlash count:20 completion:^(NSArray<JLDialSourceModel *> * _Nullable list, NSError * _Nullable err) {
            // list 为 JLDialSourceModel 数组,已缓存到 mgr.fileArray
        }];
    }
}];

Source: JLDialUnitMgr.h

4. 表盘切换与自定义背景激活

// 切换当前表盘
[mgr dialSetCurrent:dialModel Completion:^(BOOL state, NSError * _Nullable error) {
    if (state) {
        // 设备端表盘已切换
    }
}];

// 激活自定义背景(背景数据需先经图片转码生成)
[mgr dialActiveCustomBackground:backgroundModel Completion:^(BOOL state, NSError * _Nullable error) {
    if (state) {
        // 自定义背景生效
    } else {
        // 失败则重置回默认背景
        [mgr dialResetCustomBackground:^(BOOL resetState, NSError * _Nullable resetError) {
            // 已恢复默认
        }];
    }
}];

// 查询指定表盘的背景信息(路径形如 "/WATCH")
[mgr dialGetDial:@"/WATCH" BackgroundInfo:^(JLDialSourceModel * _Nullable model, NSError * _Nullable error) {
    // model 为当前表盘背景(可能为空,表示未自定义)
}];

Source: JLDialUnitMgr.h

5. 表盘市场:查询产品与表盘信息

// 根据 pid/vid 查询手表产品信息
DialMarketHttp.shared.getProductInfo(pid, vid) { product in
    guard let product = product else { return }
    // product.idString 可作为 getDialMallList 的 dialId
}

// 获取表盘商城列表(分页、免费过滤)
DialMarketHttp.shared.getDialMallList(product.idString, 1, 20, false) { records in
    // records 为表盘商城分页记录
}

// 查询免费表盘下载信息
DialMarketHttp.shared.requireDownloadDialInfoFree(uuid, pid, vid) { model in
    // model 包含下载地址与版本信息
}

Source: DialMarketHttp.swift

配置选项

DialManager 状态码

常量值含义
DialOperateTypeNoSpace0空间不足
DialOperateTypeDoing1正在操作(配合 progress 使用)
DialOperateTypeFail2操作失败
DialOperateTypeSuccess3操作成功
DialOperateTypeUnnecessary4无需重复打开文件系统
DialOperateTypeResetFial5重置文件系统失败
DialOperateTypeNormal6文件系统正常
DialOperateTypeCmdFail7流程命令执行失败

DialUpdateResult 状态码

常量值含义
DialUpdateResultFinished0更新资源完成
DialUpdateResultNewest1资源已最新
DialUpdateResultInvalid2资源无效
DialUpdateResultEmpty3资源为空
DialUpdateResultReplace4资源替换
DialUpdateResultAdd5资源新增
DialUpdateResultNoSpace6空间不足
DialUpdateResultZipError7ZIP 资源文件错误
DialUpdateResultCompareFail8表盘资源对比失败
DialUpdateResultUpdateUfw9更新固件 ufw

DialMarketHttp 网络配置

配置项类型默认值说明
memoryCapacityInt10 MBURLCache 内存缓存上限
diskCapacityInt100 MBURLCache 磁盘缓存上限
diskPathString"NetCache"磁盘缓存路径
timeoutIntervalTimeInterval10 s请求超时
jwt-tokenHTTP Header用户 token云端认证
Content-TypeHTTP Headerapplication/json请求体类型

API Reference

DialManager(类方法)

方法参数回调说明
+openDialFileSystemWithCmdManager:withResult:JL_ManagerM *managerDialOperateBK连接成功后必须调用一次
+resetDialFileSystemWithCmdManager:withResult:JL_ManagerM *managerDialOperateBK重置表盘系统
+listFile:—DialListBK查询文件列表
+addFile:Content:Result:文件名(带斜杠)、NSDataDialOperateBK添加文件
+deleteFile:Result:文件名(带斜杠)DialOperateBK删除文件(按名)
+deleteDialResourceWithFileModel:Result:JLModel_File *DialOperateBK删除文件(按模型)
+repaceFile:Content:Result:文件名、NSDataDialOperateBK替换文件
+formatFlash:Result:设备句柄 NSString *DialOperateBK格式化外部 Flash
+updateResourcePath:List:Result:资源路径、当前列表DialUpdateBK异步更新表盘资源
+getFatfsFree / +getFatfsFree:— / FatfsFreeBlock—获取 fatfs 剩余空间
+getSizeOfFileName:文件名uint32_t获取文件大小
+setFileSize:FileName:大小、文件名—记录文件大小(私有)

Throws / 错误信号:ObjC 接口不抛异常,错误通过回调枚举表达(Fail/NoSpace/CmdFail/ResetFial 等),progress 仅在 Doing 阶段有效。

JLDialUnitMgr(实例方法)

方法参数回调说明
-initWithManager:completion:JL_ManagerM *NSError * _Nullable初始化(自动打开文件系统)
-getFileList:count:completion:JL_CardType、NSIntegerNSArray<JLDialSourceModel *> * _Nullable, NSError *分页获取列表(累计)
-cleanFileList——清空文件列表缓存
-updateFileSources:completion:NSArray<JLModel_File *> *NSArray<JLDialSourceModel *> * _Nullable, NSError *批量转换/更新列表
-updateFileToDevice:Data:FilePath:completion:JL_FileHandleType、NSData、路径(int state, double progress, NSError * _Nullable)上传文件(0 成功/1 传输中/-1 失败)
-deleteFile:getFileList:Completion:路径、BOOL(BOOL, NSError * _Nullable)删除文件
-deleteFiles:Completion:NSArray<NSString *> *(BOOL, NSError * _Nullable)批量删除
-getRemainingSpace: / -getFlashRemainingSpace:—(int freeSize, NSError * _Nullable)剩余空间(新旧协议)
-dialGetCurrent:—JLDialSourceModel * _Nullable获取当前表盘
-dialSetCurrent:Completion:JLDialSourceModel *(BOOL, NSError * _Nullable)设置当前表盘
-dialActiveCustomBackground:Completion:JLDialSourceModel *(BOOL, NSError * _Nullable)激活自定义背景
-dialResetCustomBackground:—(BOOL, NSError * _Nullable)重置自定义背景
-dialGetDial:BackgroundInfo:路径(JLDialSourceModel * _Nullable, NSError * _Nullable)获取表盘背景

DialMarketHttp(Swift 单例)

方法参数回调说明
getProductInfo(_:_:completion:)pid、vidProductInfoModel?查询手表产品信息
getDialInfo(_:_:_:_:completion:)pid、vid、uuid、isPayDialNetInfoModel?查询表盘信息(付费走商城端点)
getDialList(_:completion:)DialPageBodyModelDialPageRecords?按版本分页表盘列表
getDialMallList(_:_:_:_:completion:)dialId、page、size、isfreeDialMallRecords?表盘商城列表
requireDownloadDialInfoFree(_:_:_:_:)uuid、pid、vidDialFreeDownloadModel?免费表盘下载信息
requireDownloadDialInfo(_:_:)dialIdDialPayDownloadModel?付费表盘下载信息
clearCache()——清空 URL 缓存

错误约定:所有 HTTP 方法在请求失败、JSON 解析失败或 code != 0 时回调 nil(不抛异常),并输出 DEBUG 日志。

失败模式、边界情况与并发

失败模式

失败场景信号处理建议
设备 Flash 空间不足DialOperateTypeNoSpace / DialUpdateResultNoSpace下载前先调用 getFatfsFree: 预检容量;提示用户删除旧表盘(deleteFile: / deleteFiles:)
文件系统未打开即操作回调 DialOperateTypeFail严格遵循"连接成功后先 openDialFileSystemWithCmdManager:withResult:"的时序约束
重复打开文件系统DialOperateTypeUnnecessary视为正常信号,跳过重复初始化
重置失败DialOperateTypeResetFial提示重新连接设备后重试
命令执行失败DialOperateTypeCmdFail检查 BLE 链路状态,必要时重连
资源包损坏/无效DialUpdateResultZipError / DialUpdateResultInvalid / DialUpdateResultEmpty校验资源包完整性后重新下载
表盘版本已最新DialUpdateResultNewest直接跳过写入,避免无谓的 BLE 传输
资源对比失败DialUpdateResultCompareFail可能源于 CRC 校验不匹配(JLDialSourceModel.crc),重新获取列表后重试
云端接口失败回调 nil + DEBUG 日志展示空态/重试按钮;code != 0 时日志已记录服务端返回
文件传输中断DialOperateTypeFail / state=-1利用 progress 展示进度并支持断点重试

边界情况

  • 文件名约定:DialManager 系列 API 要求文件名带斜杠前缀(如 "/WATCH1"),JLDialUnitMgr 的路径参数(dialGetDial:BackgroundInfo:)同样遵循 "/WATCH" 形态;传错格式会导致命令失败。
  • 分页累计语义:getFileList:count:completion: 回调返回的是累计数目,连续调用会追加到 fileArray;重新浏览前应调用 cleanFileList 清空,避免列表膨胀与重复。
  • 空间接口版本差异:getRemainingSpace: 与 getFlashRemainingSpace: 对应新旧协议,老固件调用新接口可能失败,需按设备固件能力选择。
  • 付费/免费端点差异:getDialInfo 的 isPay 参数切换 dial/version/onebyuuid 与 shop/onebyuuid 两个端点;getDialList 仅面向不支持支付表盘业务的设备,二者不可混用。

并发与一致性

  • 串行 BLE 命令队列:表盘操作最终都经由 JL_ManagerM 下发 BLE 命令。SDK 内部对文件系统操作做串行化处理,但业务层仍应避免同时对同一表盘发起添加/删除/切换的并发调用——尤其是 DialManager 的类方法无实例状态隔离,跨线程并发操作同一文件系统可能导致命令交错。建议在业务层维护操作队列或互斥锁。
  • 回调线程:DialMarketHttp 的 URLSession 使用主队列 delegateQueue,列表类接口额外 DispatchQueue.main.async,即所有云端回调在主线程;JLDialUnitMgr/DialManager 回调线程取决于 BLE 协议栈回调线程,UI 更新时需自行切换到主线程。
  • 文件大小缓存:setFileSize:FileName: 维护的缓存若与设备实际状态不一致(如其他端删除了文件),getSizeOfFileName: 可能返回过期值;以 listFile: / getFileList: 重新拉取为准。

性能与运维考量

  • 传输效率:表盘文件通常数百 KB 至数 MB,通过 BLE 分包传输耗时较长。DialUpdateBK / DialOperateBK 的 progress 参数用于驱动进度条,避免用户误以为卡死;DialUpdateResultNewest 短路逻辑避免重复传输。
  • 空间预检:下载表盘前先 getFatfsFree: 预检,能显著减少"传输完成才发现空间不足"的糟糕体验(大文件传输超时问题在 V1.8.0 版本中亦被修复优化)。
  • HTTP 缓存:DialMarketHttp 内置 10MB 内存 + 100MB 磁盘缓存,表盘市场列表/详情页可复用缓存减少流量;getDialList 显式使用 .reloadIgnoringLocalCacheData 保证列表实时性,其余接口使用协议缓存策略。clearCache() 供退出登录或刷新场景调用。
  • 文件系统重置:resetDialFileSystemWithCmdManager:withResult: 与 formatFlash:Result: 属于破坏性操作,调用前应二次确认;格式化会清空设备全部表盘数据。
  • 超时与重试:云端请求统一 10s 超时;SDK 层表盘传输的失败重试依赖业务层编排(删除残留文件后重新 addFile / updateResourcePath)。

扩展点

  • 两代 API 并存:DialManager(文件语义)与 JLDialUnitMgr(表盘领域语义)可组合使用——例如用 JLDialUnitMgr 管理列表与背景,用 DialManager 做资源包批量更新与格式化,业务层按场景自由编排。
  • 图片转码扩展:自定义表盘背景的图片数据需先经 JLBmpConvertKit.xcframework 转码为设备格式(V1.12.0 起为独立模块),dialActiveCustomBackground: 直接消费转码结果;新增设备机型适配时只需扩展转码侧。
  • 资源打包扩展:表盘 res 资源(含 ufw 联动)通过 JLPackageResKit.xcframework 打包,updateResourcePath:List:Result: 与 DialUpdateResultUpdateUfw 状态为其预留了固件联动入口。
  • 云端接口扩展:DialMarketHttp 单例的 baseUrl 来自 BasicHttp.basicURL(),随环境(开发/生产)切换;新增表盘商城能力时按既有"端点 + 模型解析 + nil 兜底"模式追加方法即可。

测试与使用约束

仓库将该能力以 JLDialUnit.xcframework 二进制框架形式分发(ios-arm64 真机与 ios-arm64_x86_64-simulator 模拟器双切片),未包含库内部单元测试源码;应用层 DialMarketHttp 的可测点集中在响应解析分支(code == 0 成功路径、code != 0 与 JSON 解析失败路径均回调 nil 并打日志)。集成验证建议覆盖以下链路:连接后打开文件系统 → 列表拉取 → 表盘上传/切换 → 自定义背景激活/重置 → 资源包更新(含 Newest 短路)→ 空间不足与失败回退。

Related Links

  • DialManager.h(表盘管理核心 API)
  • JLDialUnitMgr.h(面向对象表盘管理器)
  • JLDialSourceModel.h(表盘数据源模型)
  • DialMarketHttp.swift(表盘市场 HTTP 客户端)
  • 相关框架:AI 表盘管理(JLAIDialManager.h,JL_BLEKit)、表盘参数扩展(JLDialInfoExtentManager.h / JLDialInfoExtentedModel.h,JL_BLEKit)、图片转码(JLBmpConvertKit.xcframework)、资源打包(JLPackageResKit.xcframework)
  • README 模块说明:表盘功能 JLDialUnit.xcframework
Prev
OTA 固件升级
Next
图像转换工具