表盘管理与自定义表盘
本文档介绍 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:
DialManager(经典类方法 API):以 fatfs 文件操作语义直接暴露接口,方法均为类方法(+),配合JL_ManagerM蓝牙管理对象使用。它负责文件系统生命周期(打开/重置/格式化)、单文件增删改查以及资源包更新。JLDialUnitMgr(面向对象 API,V1.12.0+):以实例方法封装表盘操作,内部维护设备文件列表(fileArray)、当前表盘(currentFileSource)与自定义背景(currentBackground),基于JLFlashOpMgrFlash 操控句柄工作,提供分页获取文件列表、批量删除、剩余空间查询及表盘背景操作等高级能力。
在应用层(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、JLFlashOpMgr | BLE 指令封装、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,失败统一记录JLLogManagerDEBUG 日志并回调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 状态码
| 常量 | 值 | 含义 |
|---|---|---|
DialOperateTypeNoSpace | 0 | 空间不足 |
DialOperateTypeDoing | 1 | 正在操作(配合 progress 使用) |
DialOperateTypeFail | 2 | 操作失败 |
DialOperateTypeSuccess | 3 | 操作成功 |
DialOperateTypeUnnecessary | 4 | 无需重复打开文件系统 |
DialOperateTypeResetFial | 5 | 重置文件系统失败 |
DialOperateTypeNormal | 6 | 文件系统正常 |
DialOperateTypeCmdFail | 7 | 流程命令执行失败 |
DialUpdateResult 状态码
| 常量 | 值 | 含义 |
|---|---|---|
DialUpdateResultFinished | 0 | 更新资源完成 |
DialUpdateResultNewest | 1 | 资源已最新 |
DialUpdateResultInvalid | 2 | 资源无效 |
DialUpdateResultEmpty | 3 | 资源为空 |
DialUpdateResultReplace | 4 | 资源替换 |
DialUpdateResultAdd | 5 | 资源新增 |
DialUpdateResultNoSpace | 6 | 空间不足 |
DialUpdateResultZipError | 7 | ZIP 资源文件错误 |
DialUpdateResultCompareFail | 8 | 表盘资源对比失败 |
DialUpdateResultUpdateUfw | 9 | 更新固件 ufw |
DialMarketHttp 网络配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
memoryCapacity | Int | 10 MB | URLCache 内存缓存上限 |
diskCapacity | Int | 100 MB | URLCache 磁盘缓存上限 |
diskPath | String | "NetCache" | 磁盘缓存路径 |
timeoutInterval | TimeInterval | 10 s | 请求超时 |
jwt-token | HTTP Header | 用户 token | 云端认证 |
Content-Type | HTTP Header | application/json | 请求体类型 |
API Reference
DialManager(类方法)
| 方法 | 参数 | 回调 | 说明 |
|---|---|---|---|
+openDialFileSystemWithCmdManager:withResult: | JL_ManagerM *manager | DialOperateBK | 连接成功后必须调用一次 |
+resetDialFileSystemWithCmdManager:withResult: | JL_ManagerM *manager | DialOperateBK | 重置表盘系统 |
+listFile: | — | DialListBK | 查询文件列表 |
+addFile:Content:Result: | 文件名(带斜杠)、NSData | DialOperateBK | 添加文件 |
+deleteFile:Result: | 文件名(带斜杠) | DialOperateBK | 删除文件(按名) |
+deleteDialResourceWithFileModel:Result: | JLModel_File * | DialOperateBK | 删除文件(按模型) |
+repaceFile:Content:Result: | 文件名、NSData | DialOperateBK | 替换文件 |
+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、NSInteger | NSArray<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、vid | ProductInfoModel? | 查询手表产品信息 |
getDialInfo(_:_:_:_:completion:) | pid、vid、uuid、isPay | DialNetInfoModel? | 查询表盘信息(付费走商城端点) |
getDialList(_:completion:) | DialPageBodyModel | DialPageRecords? | 按版本分页表盘列表 |
getDialMallList(_:_:_:_:completion:) | dialId、page、size、isfree | DialMallRecords? | 表盘商城列表 |
requireDownloadDialInfoFree(_:_:_:_:) | uuid、pid、vid | DialFreeDownloadModel? | 免费表盘下载信息 |
requireDownloadDialInfo(_:_:) | dialId | DialPayDownloadModel? | 付费表盘下载信息 |
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