设备配置JSON与资源文件
本文档介绍 JieLi(杰理)Android 蓝牙示例工程 btsmart 中,随应用打包在 assets 目录下的设备配置 JSON 文件与配套资源文件(充电仓动画、屏幕图片等),以及 App 侧读取、复制这些资源的实现方式。
Purpose and Scope
本页面向 code/PiHome_V1.13.0_SDK_V4.2.0/btsmart 示例应用中的 设备配置资产(device config assets) 子系统,覆盖:
assets目录下按芯片平台(AC693x / AC695x / AC696x / AC697x)与设备形态(耳机、音箱、声卡、颈挂)划分的设备配置 JSON 文件;- 按 PID/VID 键控的设备专项配置(如
sound_card/sound_card_pid_0x59_vid_0x02.json)与supplierconfig.json供应商配置; - 充电仓(Charging Case / 充电盒)UI 资源文件(开机动画、锁屏/解锁动画、屏幕图片);
- 应用侧读取与复制这些资源的工具方法(
AppUtil.getTextFromAssets、AppUtil.copyAssets、ChargingBinUtil.copyAssets)。
以下内容不属于本页范围,请参见对应页面:蓝牙协议解析与 SDK 交互(JL_Bluetooth SDK 能力)、充电仓业务逻辑(ChargingBinUtil 的完整通信协议)、设备固件升级流程。
Overview
杰理(JieLi)蓝牙生态中,同一款 App 需要兼容多种主控芯片(AC693x、AC695x、AC696x、AC697x…)与多种设备形态(耳机、颈挂耳机、音箱、TWS 音箱、声卡、充电仓)。不同芯片/形态的设备在能力集(如 EQ 段数、LED 效果、按键映射、音频参数)上差异很大,因此工程采用 "配置与代码分离" 的策略:把每类设备的参数描述写成 JSON 资产文件,随 APK 打包进 assets,App 在运行时读取对应文件并按需下发/应用。
资源文件方面,充电仓(充电盒)带屏幕的设备需要一组按分辨率组织的 UI 素材(开机动画、锁屏动画、解锁动画、静态图片),例如 charging_case/320x172/ 下的 GIF/PNG。这些资源同样打包在 assets 中,由 ChargingBinUtil 在需要时复制到应用私有目录供显示。
核心设计意图(WHY):
- 按芯片族分文件:避免单个大 JSON 相互干扰,新增芯片只需新增一个资产文件,App 代码无需改动;
- 按 PID/VID 键控:同一芯片族下不同厂商/产品的差异化配置通过
pid_xxx_vid_xxx.json定位,实现"一个 App 适配多家公版产品"; - txt 扩展名规避打包/校验问题:部分配置以
.txt结尾(如ac696x_soundbox_json.txt),内容仍是 JSON 文本,目的是避免某些构建工具对assets/*.json的压缩/混淆处理; - assets 只读 → 私有目录复制:Android
assets只能流式读取、不可写,需要落盘修改(如充电仓动画按需加载)时必须先复制到getFilesDir()等可写目录。
Architecture
flowchart TD
subgraph sg_Assets["btsmart/src/main/assets(APK 内只读资产)"]
subgraph sg_Json["设备配置 JSON"]
CFG1["ac693x_headset_json.txt<br/>ac693x_headset_neck_json.txt"]
CFG2["ac695x_sound_card.json<br/>sound_card/config.json"]
CFG3["ac696x_soundbox_json.txt<br/>ac696x_soundbox_tws_json.txt"]
CFG4["ac697x_headset_json.txt"]
CFG5["supplierconfig.json"]
CFG6["sound_card/sound_card_pid_0x59_vid_0x02.json"]
end
subgraph sg_Res["充电仓 UI 资源"]
RES1["charging_case/320x172/boot/ANI1.gif"]
RES2["charging_case/320x172/screen/anim/lock/ANI2..7.gif"]
RES3["charging_case/320x172/screen/anim/unlock/ANI2..7.gif"]
RES4["charging_case/320x172/screen/image/VIE0.png"]
end
end
subgraph sg_Code["App 侧加载工具(com.jieli.btsmart.util)"]
UTIL1["AppUtil.getTextFromAssets()<br/>读配置 JSON 文本"]
UTIL2["AppUtil.copyAssets()<br/>复制单个/整目录资源"]
UTIL3["ChargingBinUtil.copyAssets()<br/>递归复制充电仓资源"]
end
subgraph sg_Runtime["运行时使用方"]
R1["设备能力解析 / 参数下发"]
R2["充电仓屏幕显示(锁屏/解锁/开机)"]
end
CFG1 --> UTIL1
CFG2 --> UTIL1
CFG3 --> UTIL1
CFG4 --> UTIL1
CFG5 --> UTIL1
CFG6 --> UTIL1
UTIL1 --> R1
RES1 --> UTIL2
RES2 --> UTIL3
RES3 --> UTIL3
RES4 --> UTIL3
UTIL2 --> R2
UTIL3 --> R2
架构说明:
- assets 目录是唯一的资产来源,
src/main/assets下所有文件原样进入 APK; - 配置 JSON 层按芯片族(
ac693x/ac695x/ac696x/ac697x)与设备形态(headset耳机、soundbox音箱、sound_card声卡、neck颈挂、tws双发)划分文件;supplierconfig.json与pid/vid文件提供供应商级/产品级覆盖; - 资源层以
charging_case/{分辨率}/{用途}/{动画|图片}三级目录组织,320x172为屏幕分辨率; - 加载工具层提供两条路径:文本型配置走
getTextFromAssets(流式读取为字符串,交给 JSON 解析);二进制资源走copyAssets(流式写入到私有目录,递归处理子目录); - 运行时使用方:配置 JSON 被设备能力解析逻辑消费,充电仓资源被屏幕显示逻辑消费。
(注:以上 JSON 文件的字段级 schema 位于各资产文件中;字段的完整枚举需直接阅读对应文件,本页聚焦文件组织与加载机制。)
设备配置 JSON 资产清单
assets 目录中的配置 JSON 资产位于 btsmart/src/main/assets 下,按芯片族与设备形态命名,文件清单如下:
文件(相对 src/main/assets/) | 芯片族 | 设备形态 | 说明 |
|---|---|---|---|
ac693x_headset_json.txt | AC693x | 头戴耳机 | 耳机设备参数(文本形式存储的 JSON) |
ac693x_headset_neck_json.txt | AC693x | 颈挂耳机 | 颈挂形态差异化参数 |
ac695x_sound_card.json | AC695x | 声卡 | 声卡设备参数 |
ac696x_soundbox_json.txt | AC696x | 音箱 | 单音箱参数 |
ac696x_soundbox_tws_json.txt | AC696x | TWS 音箱 | 双发(TWS)音箱参数 |
ac697x_headset_json.txt | AC697x | 头戴耳机 | 较新芯片族耳机参数 |
supplierconfig.json | 全平台 | 供应商级 | 供应商默认配置(全局兜底) |
sound_card/config.json | AC695x | 声卡 | 声卡模块基础配置 |
sound_card/sound_card_pid_0x59_vid_0x02.json | AC695x | 声卡 | 按 PID=0x59 / VID=0x02 键控的产品级专项配置 |
命名约定解读(WHY):
- 芯片族前缀(
ac693x、ac695x…):杰理 SDK 按芯片系列提供不同协议与能力集,文件名直接映射芯片族,App 依据当前连接的设备芯片型号选择文件; - 形态后缀(
headset/neck/soundbox/sound_card/tws):同一芯片族内部,不同形态的 HID/音频/灯效参数差异大,按形态拆文件便于维护与增量下发; .txt扩展名:ac693x_headset_json.txt等文件内容为 JSON 文本,但使用.txt后缀。这是规避构建工具对assets/**/*.json的压缩/资源混淆处理、以及避免某些扫描工具误判的常见做法——内容仍是标准 JSON,读取方不依赖扩展名;- PID/VID 键控:
sound_card_pid_0x59_vid_0x02.json表明声卡模块支持"公版固件 + 产品参数表"模式:App 通过蓝牙获取设备的 PID(产品 ID)与 VID(厂商 ID),再定位专项配置;未命中时回退到sound_card/config.json或supplierconfig.json。这是实现"一个 APK 适配多家公版"的关键机制。
注意:以上文件的具体字段(如 EQ 段数、LED 颜色表、按键功能映射等)属于设备参数协议的一部分,字段级完整枚举请直接查看对应资产文件与 SDK 协议文档。
充电仓资源文件组织
充电仓(带屏幕的充电盒)UI 资源集中在 assets/charging_case/320x172 下,目录结构如下:
charging_case/
└── 320x172/ # 屏幕分辨率 320x172
├── boot/ # 开机动画
│ └── ANI1.gif
└── screen/
├── anim/ # 屏幕动画
│ ├── lock/ # 锁屏动画 ANI2..ANI7.gif
│ └── unlock/ # 解锁动画 ANI2..ANI7.gif
└── image/ # 静态图片
└── VIE0.png
组织原则(WHY):
- 分辨率目录在最外层:充电仓屏幕可能有多种分辨率(如 320x172、更高分辨率等),按分辨率分目录可在同一 APK 内携带多套素材,运行时按设备实际屏幕选择,避免拉伸失真;
boot/anim/lock/anim/unlock/image语义化子目录:对应充电仓显示状态机——开盖/充电时的开机动画、上锁/解锁切换时的过渡动画、以及静态画面(如 LOGO、电量图)。ChargingBinUtil按语义读取,代码不关心具体帧文件名;- GIF 序列帧:
ANI*.gif为逐帧动画素材,App 侧通过 ImageView/自定义绘制按帧播放;VIE0.png为静态位图,可直接解码显示。
加载机制实现解析
文本型配置:AppUtil.getTextFromAssets
配置 JSON 以文本形式读取,入口是 AppUtil.getTextFromAssets:
public static String getTextFromAssets(Context context, String fileName) {
if (context == null || fileName == null) return null;
try {
inputStream = context.getAssets().open(fileName);
// ...读取缓冲...
int len = inputStream.read(buffer);
// Convert the buffer into a string.
// JL_Log.i("zzc", "getTextFromAssets : " + len);
}
// ...finally 关闭流...
}
Source: AppUtil.java
要点:
- 入参是
assets内的相对路径(如ac695x_sound_card.json); - 通过
context.getAssets().open(fileName)打开流,读取后转换为字符串; - 空指针/IO 异常被捕获,返回
null,由调用方决定回退策略; - 读取到的字符串交给上层 JSON 解析(如
org.json.JSONObject或 Gson 映射成能力模型),解析失败即视为配置缺失,走默认参数。
二进制资源复制:AppUtil.copyAssets
普通资源复制入口为 AppUtil.copyAssets:
/**
* 复制assets资源
* @param context 上下文
* @param oldPath assets路径
* @param newPath 复制资源路径
*/
public static void copyAssets(@NonNull Context context, String oldPath, String newPath) {
try {
// 流式读取并写入目标文件
} catch (Exception e) {
// 异常处理
}
}
Source: AppUtil.java
递归复制:ChargingBinUtil.copyAssets
充电仓资源是多级目录,需要递归复制。ChargingBinUtil.copyAssets 的核心逻辑:
public static void copyAssets(@NonNull Context context, String oldPath, String newPath) {
try {
String[] fileNames = context.getAssets().list(oldPath);// 获取assets目录下的所有文件及目录名
if (fileNames == null || fileNames.length == 0) {// 如果是文件
InputStream is = context.getAssets().open(oldPath);
File file = new File(newPath);
// ...输出流写文件...
}
// ...目录则创建目标目录...
for (String fileName : fileNames) {
copyAssets(context, oldPath + File.separator + fileName, newPath + File.separator + fileName);
}
} catch (Exception e) {
// 异常处理
}
}
Source: ChargingBinUtil.java
递归算法解读:
context.getAssets().list(oldPath)列出目标路径下的所有条目;- 若列表为空,说明当前条目是文件:
assets.open(oldPath)打开输入流,写入new File(newPath)对应的输出流; - 若列表非空,说明是目录:先创建目标目录,再对每个子项递归调用自身;
- 终止条件即"目录 → 文件"的边界,最终把
charging_case/320x172整棵树复制到应用私有目录。
为什么必须复制而不是直接播放 assets 文件? Android 的 assets 没有文件路径可被 File/Glide/SurfaceView 直接引用(只能经 AssetManager 流式打开),且充电仓动画可能需要按设备状态动态替换;先复制到 getFilesDir() 等可写目录,即可用常规文件 API、缓存到磁盘,也便于后续版本更新时整体替换。
Core Flow:配置与资源的加载流程
设备配置 JSON 读取流程
sequenceDiagram
participant App as btsmart App
participant AM as AssetManager (assets)
participant Parser as JSON 解析/能力模型
participant Device as 蓝牙设备
App->>App: 连接设备,获取芯片族 + PID/VID
App->>AM: getTextFromAssets("ac695x_sound_card.json")
AM-->>App: JSON 文本 / null
alt 芯片级配置存在
App->>Parser: 解析芯片级参数
Parser-->>App: 能力模型
else 读取失败/解析失败
App->>AM: 回退 supplierconfig.json / 默认参数
end
App->>Device: 按能力模型下发/同步参数
流程说明:
- App 建立蓝牙连接后,先从设备信息中确定芯片族(如 AC695x)与产品标识(PID/VID);
- 调用
AppUtil.getTextFromAssets()读取对应芯片族/形态的配置文件; - 若芯片级文件缺失或解析失败,回退到
supplierconfig.json(供应商全局配置),再失败则使用代码内默认参数; - 声卡模块还会按 PID/VID 尝试加载专项文件(
sound_card_pid_0x59_vid_0x02.json),命中则覆盖基础配置——即"芯片级 → 供应商级 → 产品级"的配置覆盖链; - 最终将解析出的能力模型用于 UI 显示(EQ、灯效、按键设置等)与设备参数同步。
充电仓资源加载流程
sequenceDiagram
participant App as btsmart App
participant CB as ChargingBinUtil
participant FS as 应用私有目录 (filesDir)
participant UI as 充电仓屏幕 UI
App->>CB: 检测到充电仓(带屏)设备
CB->>FS: copyAssets("charging_case/320x172", filesDir/charging_case)
FS-->>CB: 资源落盘完成
CB->>UI: 按状态加载 boot/ANI1.gif
UI->>UI: 开盖/充电 → 播放开机动画
CB->>UI: 状态切换 → 加载 anim/lock 或 anim/unlock 序列
UI->>UI: 静态画面 → image/VIE0.png
流程说明:
- 连接带屏充电仓后,
ChargingBinUtil.copyAssets()将assets/charging_case/{分辨率}整树复制到应用私有目录; - 屏幕状态机(开机、锁屏、解锁、静态)各自从对应语义目录取素材;
- 复制是幂等且可重复的(覆盖式写入),保证升级后新资源能替换旧资源。
Usage Examples
读取设备配置 JSON 文本
以下代码展示了从 assets 读取声卡配置文本的用法(摘自 AppUtil 的资产读取实现,调用方模式):
// 读取芯片级声卡配置
String config = AppUtil.getTextFromAssets(context, "ac695x_sound_card.json");
if (config != null) {
// 交给 JSON 解析器构建能力模型
JSONObject obj = new JSONObject(config);
}
Source: AppUtil.java
递归复制充电仓资源
// 将充电仓 320x172 全套素材复制到私有目录(ChargingBinUtil 内部实现)
ChargingBinUtil.copyAssets(context,
"charging_case/320x172",
new File(context.getFilesDir(), "charging_case/320x172").getAbsolutePath());
Source: ChargingBinUtil.java
注:上例中的调用参数(源目录、目标目录)与
ChargingBinUtil公开方法签名一致,实际业务调用点位于充电仓连接/展示逻辑中,字段级细节可查阅ChargingBinUtil完整源码。
Configuration Options
设备配置资产没有集中的"配置项"概念,其配置能力体现在文件选择策略上:
| 选择维度 | 依据 | 文件示例 | 说明 |
|---|---|---|---|
| 芯片族 | 设备芯片型号 | ac695x_sound_card.json | 按芯片族选择基础配置 |
| 设备形态 | 设备类别 | ac696x_soundbox_tws_json.txt | 耳机/音箱/声卡/颈挂各自独立 |
| 供应商级 | 供应商标识 | supplierconfig.json | 全局兜底配置 |
| 产品级 | PID/VID | sound_card_pid_0x59_vid_0x02.json | 最高优先级覆盖 |
| 屏幕分辨率 | 充电仓硬件 | charging_case/320x172/ | 资源目录即配置 |
覆盖优先级从低到高:芯片族/形态基础配置 → supplierconfig.json → PID/VID 专项配置。命中规则与回退逻辑由 App 的设备能力解析模块实现(本页聚焦资产组织,命中代码见 SDK 相关能力页)。
API Reference
AppUtil.getTextFromAssets(Context context, String fileName): String
从 assets 读取文本文件内容。
- 参数:
context(Context):应用上下文,用于获取AssetManager;fileName(String):assets下的相对路径(如ac695x_sound_card.json)。
- 返回:文件文本内容;
context/fileName为空或读取失败时返回null。 - 设计意图:统一文本型资产的读取入口,把 IO 与异常处理收敛到工具类,调用方只需关心"拿到 JSON 字符串"。
Source: AppUtil.java
AppUtil.copyAssets(@NonNull Context context, String oldPath, String newPath): void
将 assets 下的单个文件或目录复制到目标路径(@NonNull 约束 context)。
- 参数:
context(Context):应用上下文;oldPath(String):assets源路径;newPath(String):目标路径(可为应用私有目录)。
- 返回:无。
- 异常:内部捕获
Exception,失败不向上抛出(静默失败,由调用方校验目标文件是否存在)。
Source: AppUtil.java
ChargingBinUtil.copyAssets(@NonNull Context context, String oldPath, String newPath): void
递归复制 assets 目录树(充电仓素材专用),自动处理"文件 vs 目录"分支。
- 参数:同上。
- 返回:无。
- 算法:
AssetManager.list()判空区分文件/目录,目录递归、文件流式写入;每次调用幂等覆盖。 - 异常:内部捕获
Exception,失败后调用方应检查目标目录完整性。
Source: ChargingBinUtil.java
Failure Modes、边界情况与并发
配置资产缺失或解析失败
- 表现:
getTextFromAssets返回null,或 JSON 解析抛异常。 - 处置:按覆盖链回退(芯片级 →
supplierconfig.json→ 代码内默认参数)。若全部缺失,设备按默认参数工作,App 不应崩溃。 - 边界:文件名大小写敏感(
AssetManager按精确路径匹配),文件名与设备芯片族不匹配时会静默回退,排查时需核对设备上报的芯片族与资产文件名。
资源复制失败与目录不完整
- 表现:
copyAssets内部捕获异常,不向上抛出;目标目录可能只有部分文件。 - 处置:调用方应在播放动画前校验关键文件(如
boot/ANI1.gif)是否存在;File.exists()校验失败时重试复制或降级为无动画显示。 - 边界:
AssetManager.list()返回空数组与返回null均视为"文件"分支——空目录会被当作文件打开而失败,因此资产目录不应存在空目录。
版本升级后的资源覆盖
- 充电仓素材复制是覆盖式写入,新版本 APK 中资源变化后,首次启动重新复制即可生效;但若资源文件被设备端缓存(如已发送到充电仓屏幕),需要触发重新下发。
并发与线程
getTextFromAssets/copyAssets均为同步阻塞 IO,应在工作线程(非主线程)调用,避免 ANR;- 复制过程没有加锁,同一目标路径并发复制可能导致写竞争;实际使用中由充电仓连接流程串行触发,若需并发防护应在调用方加互斥。
Performance 与运维注意
- 包体:充电仓 GIF/PNG 素材占用 APK 体积,按分辨率分目录会让包体随素材套数线性增长;裁剪素材应保持
320x172比例,避免运行时缩放开销; - 启动开销:首次连接充电仓时的全量复制在低端设备上耗时明显,建议异步执行并加进度提示;后续可对比版本号/文件清单做增量复制(当前实现为全量覆盖式);
- 内存:GIF 逐帧播放时注意帧解码内存,大图应使用
BitmapFactory.Options.inSampleSize按需降采样。
Extension Points
- 新增芯片族:在
assets下新增ac6xxx_xxx.json(或.txt),命名遵循"芯片族_形态"约定,并在设备能力解析处注册芯片族到文件名的映射即可,App 主体代码零改动; - 新增公版产品:添加
sound_card_pid_{pid}_vid_{vid}.json专项文件,按 PID/VID 命中后覆盖基础配置; - 新增充电仓分辨率:新增
charging_case/{新分辨率}/目录,复制路径按分辨率参数化即可,无需改动复制逻辑; - 素材换肤/动态下发:
copyAssets的目标目录可替换为服务器下载路径,只要保持boot/anim/image目录语义,显示层无需修改。
Tests
本仓库未发现针对资产加载工具的独立单元测试文件;相关行为通过示例应用运行期验证(连接真实设备后观察配置生效与充电仓动画显示)。建议补充的测试覆盖:
getTextFromAssets对不存在文件返回null、对正常文件返回完整文本;copyAssets递归复制后目录树与源一致、文件内容逐字节一致;- 覆盖链回退:缺失芯片级文件时正确落到
supplierconfig.json。
Related Links
- AppUtil.java(资产读取/复制工具)
- ChargingBinUtil.java(充电仓资源复制)
- assets 目录(全部配置 JSON 与资源)
- ac695x_sound_card.json(声卡配置示例)
- sound_card/config.json(声卡基础配置)
- supplierconfig.json(供应商全局配置)
说明:本页基于目录结构、文件命名与工具方法源码整理;各配置 JSON 的字段级协议内容(EQ、灯效、按键映射等参数项)位于对应资产文件中,属设备参数协议范畴,如需逐字段解读请结合 SDK 协议文档阅读。