灯光控制
灯光控制是 PiHome App 中通过蓝牙 RCSP 协议对耳机/音箱等设备进行灯光(RGB 彩灯、色温灯、场景灯、闪烁灯)控制的完整能力,涵盖 UI 交互层、HSL 色彩处理、数据模型与 RCSP 蓝牙命令链路。
Purpose and Scope
本页面介绍 App 端「灯光控制」功能的完整实现机制,包括:
- 灯光控制的数据模型
LightControlInfo及其 11 字节蓝牙协议封装 - UI 层各 Fragment(
LightControlFragment、LightColorTemperatureFragment、LightSceneFragment等)的职责划分 - HSL 色彩空间同步策略与设计意图(为何用 hue/saturation 替代 color 同步)
- 通过
RCSPController与设备交互的getLightControlInfo/setLightControlInfoAPI 及回调机制 - 收藏颜色(ColorCollect)的本地持久化与动画交互
设备端固件协议、SDK 底层 BLE 传输实现(com.jieli.bluetooth 模块)属于蓝牙 SDK 自身范畴,不在本页展开;需要了解 SDK 基础能力(连接、设备信息、RCSPController 生命周期)可参考对应 SDK 文档页。
Overview
灯光控制是音箱/耳机类蓝牙设备的常见增值功能:设备内置 RGB LED 或色温灯,App 通过 RCSP(Real-time Control and Streaming Protocol)指令读取/写入设备灯光参数。App 端能力分三层:
- 数据模型层:
LightControlInfo描述灯光的完整状态(开关、模式、颜色、闪烁、场景、HSL 分量),并提供toByteArray()将状态编码为 11 字节的协议包。 - 业务/传输层:
RCSPController单例负责与当前连接设备通信,提供getLightControlInfo(...)与setLightControlInfo(...)异步接口,结果通过BTRcspEventCallback#onLightControlInfo回调。 - UI 交互层:
LightContainerFragment作为容器承载三个子页面 —— 彩色灯(LightControlFragment)、色温灯(LightColorTemperatureFragment)、场景灯(LightSceneFragment),并配合LightModeDialog、收藏色网格等交互组件。
设计上最关键的一点(见 LightControlFragment.java 的类注释):UI 层使用 hue 和 saturation 代替 color 进行设备同步,以减少 SeekBar 拖动时的抖动;点击收藏时判断 hue/saturation 是否变化再同步 ColorView,避免 ColorPickView 偏移;亮度(luminance)SeekBar 的拉动不直接同步到 ColorPickView。这些细节体现了对蓝牙指令频率与 UI 流畅性的权衡。
Architecture
flowchart TD
subgraph sg_UI["UI 层 (com.jieli.btsmart.ui.light)"]
Container["LightContainerFragment<br/>(容器/入口)"]
ColorTemp["LightColorTemperatureFragment<br/>(色温灯)"]
Control["LightControlFragment<br/>(彩色灯/HSL)"]
Scene["LightSceneFragment<br/>(场景灯)"]
ModeDialog["LightModeDialog<br/>(模式选择)"]
end
subgraph sg_Data["数据模型层"]
Model["LightControlInfo<br/>(data.model.bluetooth)"]
LightMode["LightMode<br/>(res + name)"]
Collect["ColorCollect / ColorCollectList<br/>(收藏色)"]
end
subgraph sg_Adapter["Adapter 层"]
ColorAdapter["LightColorCollectAdapter"]
ModeAdapter["LightModeAdapter"]
SceneAdapter["LightSceneAdapter"]
end
subgraph sg_SDK["RCSP SDK 层 (com.jieli.bluetooth)"]
RCSP["RCSPController<br/>(单例)"]
EventCb["BTRcspEventCallback<br/>onLightControlInfo"]
DeviceInfo["DeviceInfo<br/>isLightEnable / getCurFunction"]
end
subgraph sg_Device["设备端"]
Dev["蓝牙设备<br/>(耳机/音箱 LED)"]
end
Container --> Control
Container --> ColorTemp
Container --> Scene
Control --> ModeDialog
Control --> ColorAdapter
Scene --> SceneAdapter
ModeDialog --> ModeAdapter
Control --> Model
ColorAdapter --> Collect
Control -->|"getLightControlInfo / setLightControlInfo"| RCSP
ColorTemp --> RCSP
Scene --> RCSP
RCSP --> EventCb
RCSP --> DeviceInfo
EventCb -->|"回调结果"| Control
RCSP -->|"RCSP 指令/11 字节载荷"| Dev
架构说明
- 入口:
LightContainerFragment是灯光功能的总容器,根据当前设备能力(DeviceInfo#isLightEnable())决定是否展示灯光入口,并按 tab 切换彩色灯 / 色温灯 / 场景灯三个子 Fragment。 - 状态同步:
LightControlFragment持有RCSPController.getInstance()单例(LightControlFragment.java),所有读写设备灯光的操作都经由该单例,保证与 App 其他蓝牙功能共享同一连接状态。 - 数据模型:App 侧
LightControlInfo(com.jieli.btsmart.data.model.bluetooth)是 UI 与 SDK 之间的中间层,其toByteArray()决定最终写入蓝牙的字节布局;SDK 侧另有com.jieli.bluetooth.bean.device.light.LightControlInfo用于回调解析。 - 回调驱动:读取结果是异步的,先注册
BTRcspEventCallback监听,命令执行成功后数据在onLightControlInfo中返回,UI 据此刷新控件,避免轮询。
数据模型:LightControlInfo
App 侧模型 com.jieli.btsmart.data.model.bluetooth.LightControlInfo 完整描述了灯光状态,共 8 个整型字段:
| 字段 | 类型 | 含义 |
|---|---|---|
switchState | int | 灯光开关状态(bit0,0/1) |
lightMode | int | 灯光模式(bit1-2,与 switchState 共同压缩进 byte0) |
color | int | RGB 颜色值(Android Color 整型,写入时拆分为 R/G/B) |
twinkleMode | int | 闪烁模式 |
twinkleFreq | int | 闪烁频率 |
sceneMode | int | 场景模式 |
hue | int | 色相(HSL,2 字节) |
saturation | int | 饱和度(HSL) |
luminance | int | 亮度(HSL) |
字段定义见 LightControlInfo.java。
注意:仓库中存在两个同名类 —— App 业务模型
com.jieli.btsmart.data.model.bluetooth.LightControlInfo(含toByteArray()编码逻辑)与 SDK 回调模型com.jieli.bluetooth.bean.device.light.LightControlInfo(用于BTRcspEventCallback#onLightControlInfo解析)。UI 层LightControlFragment与 Demo 直接使用 SDK 版本,App 业务模型则负责 UI 状态 → 协议字节的转换。
11 字节协议封装
toByteArray() 将上述字段编码为固定 11 字节的载荷,布局如下:
| 字节索引 | 内容 | 来源字段 |
|---|---|---|
| 0 | switchState | (lightMode << 2) | 开关 + 模式 压缩 |
| 1-3 | R、G、B | Color.red/green/blue(color) |
| 4 | 闪烁模式 | twinkleMode |
| 5 | 闪烁频率 | twinkleFreq |
| 6 | 场景模式 | sceneMode |
| 7-8 | 色相(2 字节大端) | CHexConver.int2byte2(hue) |
| 9 | 饱和度 | saturation |
| 10 | 亮度 | luminance |
public byte[] toByteArray() {
byte[] bytes = new byte[11];
int byte0Value = switchState | (lightMode << 2);
byte[] bytesHue = CHexConver.int2byte2(hue);
bytes[0] = CHexConver.intToByte(byte0Value);
bytes[1] = CHexConver.intToByte(Color.red(color));
bytes[2] = CHexConver.intToByte(Color.green(color));
bytes[3] = CHexConver.intToByte(Color.blue(color));
bytes[4] = CHexConver.intToByte(twinkleMode);
bytes[5] = CHexConver.intToByte(twinkleFreq);
bytes[6] = CHexConver.intToByte(sceneMode);
System.arraycopy(bytesHue, 0, bytes, 7, bytesHue.length);
bytes[9] = CHexConver.intToByte(saturation);
bytes[10] = CHexConver.intToByte(luminance);
return bytes;
}
Source: LightControlInfo.java
设计意图:开关与模式共用 byte0 的位域(bit0 开关、bit1-2 模式),是为了压缩载荷、兼容设备端固件对单一状态字节的解析习惯;色相使用 2 字节(int2byte2)保证 0-360 的精度,而饱和度/亮度各 1 字节(0-255)即可满足 HSL 表示范围。Color.red/green/blue 与 CHexConver.intToByte 的组合确保 RGB 各分量以无符号字节传输。
模式与场景的轻量模型
LightMode(LightMode.java)是 UI 展示用的最小模型,仅包含两个字段:
public class LightMode {
private int res; // 模式图标资源 ID
private String name; // 模式名称
// getter / setter ...
}
Source: LightMode.java
它由 LightModeAdapter 渲染到 LightModeDialog 的网格中,选中后把对应的模式索引写入 LightControlInfo.lightMode / sceneMode 并下发设备。收藏色则使用 ColorCollect / ColorCollectList 模型,由 LightColorCollectAdapter 以 6 列网格展示(GridLayoutManager(getContext(), 6),见 LightControlFragment.java)。
UI 交互层实现
页面结构与职责
灯光功能由 LightContainerFragment 聚合三个子页面:
LightControlFragment(彩色灯):核心页面。包含ColorPicker2View取色盘、三个 HSL SeekBar(sbHSLColdAndWarm冷暖、sbHSLGrayAndColorful灰彩、sbHSLDarkAndSun明暗)、收藏色 RecyclerView(rvLightColorCollect)与收藏按钮(ibtnColorAdd)。控件绑定见 LightControlFragment.java。LightColorTemperatureFragment(色温灯):面向仅支持色温调节的设备,提供冷暖色温滑杆。LightSceneFragment(场景灯):提供预设场景选择(呼吸、律动等),由LightSceneAdapter渲染。
HSL 同步策略(设计意图)
LightControlFragment 的类注释(LightControlFragment.java)明确记录了三条关键决策:
- 用 hue/saturation 代替 color 同步:颜色拾取器返回 RGB,而设备指令需要 RGB;若直接回写 RGB,RGB→HSL→RGB 的往返换算会产生量化误差,导致 SeekBar 在相邻值之间抖动。因此 UI 维护独立
mValueColdAndWarm(色相)、mValueGrayAndColorful(饱和度)、mValueDarkAndLight(亮度)三个浮点状态,配合ColorHSB、ColorHSL、ColorRGB、RGB2HSLUtil工具类做单向换算。 - 收藏时按需同步 ColorView:点击收藏时先判断 hue/saturation 是否真的变化,再同步
ColorPicker2View,避免取色盘偏移。 - 亮度不同步到取色盘:拉动 luminance 的 SeekBar 只更新亮度通道,不反向驱动 ColorPickView,减少无谓重绘。
这些策略共同服务于一个目标:在有限的蓝牙指令带宽下,让 UI 操作与设备灯光表现保持稳定一致,避免视觉抖动与指令风暴。类中 isRealTimeRefresh = false(LightControlFragment.java)进一步说明取色盘回调默认只在手指抬起(end == true)时才真正同步状态,拖动过程仅本地预览。
收藏颜色的持久化与动画
收藏色列表以 KEY_COLLECT_COLORS_LIST 为键通过 PreferencesHelper 读写本地,首次进入使用 12 个空 ColorCollect 占位(LightControlFragment.java);收藏时触发 parabolaAnimation 抛物线动画(isParabolaAnimationShowing 防重入),将颜色"飞入"收藏格,提升交互反馈。
Core Flow:读取与设置灯光
sequenceDiagram
participant UI as LightControlFragment
participant RCSP as RCSPController
participant SDK as SDK 协议栈
participant DEV as 蓝牙设备
Note over UI,DEV: 读取灯光状态
UI->>RCSP: addBTRcspEventCallback(回调)
UI->>RCSP: getLightControlInfo(device, cb)
RCSP->>SDK: 发送 RCSP 查询指令
SDK->>DEV: BLE 写特征
DEV-->>SDK: BLE 通知(灯光信息载荷)
SDK-->>RCSP: 解析为 LightControlInfo
RCSP-->>UI: onLightControlInfo(device, info)
UI->>UI: 刷新取色盘 / SeekBar / 场景视图
Note over UI,DEV: 设置灯光状态
UI->>UI: 构造 LightControlInfo(HSL→RGB)
UI->>RCSP: setLightControlInfo(device, info, cb)
RCSP->>SDK: 编码 11 字节载荷并发送
SDK->>DEV: BLE 写特征
DEV-->>RCSP: 应答结果
RCSP-->>UI: onSuccess / onError
关键点:
- 读取采用「先注册回调、再发指令」的模式:
getLightControlInfo的OnRcspActionCallback只表示指令已发送/送达,真正的数据在BTRcspEventCallback#onLightControlInfo异步返回(Demo 中注释明确「结果将会在 BTRcspEventCallback#onLightControlInfo 回调」,见 LightControlDemo.java)。 - 设置则单次回调即可:
setLightControlInfo的onSuccess/onError直接反映设备应答结果。 - 两个操作都要求先有连接中的设备(
controller.getUsingDevice()),未连接时回调BaseError。
Usage Examples
以下示例均摘自仓库源码,演示灯光控制能力的标准调用方式。
判断设备是否支持灯光 / 是否处于灯光模式
public boolean isSupportLightMode() {
//获取RCSPController对象
RCSPController controller = RCSPController.getInstance();
//获取当前操作设备
BluetoothDevice usingDevice = controller.getUsingDevice();
if (null == usingDevice) return false;
DeviceInfo deviceInfo = controller.getDeviceInfo(usingDevice);
if (null == deviceInfo) return false; //设备未初始化
return deviceInfo.isLightEnable();
}
public boolean isDeviceInLightMode() {
//获取RCSPController对象
RCSPController controller = RCSPController.getInstance();
//获取当前操作设备
BluetoothDevice usingDevice = controller.getUsingDevice();
if (null == usingDevice) return false;
DeviceInfo deviceInfo = controller.getDeviceInfo(usingDevice);
if (null == deviceInfo) return false; //设备未初始化
return deviceInfo.getCurFunction() == AttrAndFunCode.SYS_INFO_FUNCTION_LIGHT;
}
Source: LightControlDemo.java
设计意图:进入灯光页面之前先做两级校验 ——
isLightEnable()判断设备硬件是否带灯,getCurFunction()判断设备当前是否已切换进灯光功能(部分设备功能需先切换)。这避免了向不支持灯光或未就绪的设备下发无效指令。
读取灯光控制信息
public void getLightControlInfo() {
//获取RCSPController对象
RCSPController controller = RCSPController.getInstance();
//添加蓝牙RCSP事件监听器
controller.addBTRcspEventCallback(new BTRcspEventCallback() {
@Override
public void onLightControlInfo(BluetoothDevice device, LightControlInfo lightControlInfo) {
//此处将会回调灯光控制信息
}
});
//执行获取灯光控制信息功能并等待结果回调
controller.getLightControlInfo(controller.getUsingDevice(), new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
//成功回调
//结果将会在BTRcspEventCallback#onLightControlInfo回调
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
//失败回调
//error - 错误信息
}
});
}
Source: LightControlDemo.java
设置灯光控制信息
public void setLightControlInfo(LightControlInfo lightControlInfo) {
//获取RCSPController对象
RCSPController controller = RCSPController.getInstance();
//执行设置灯光控制功能并等待结果回调
controller.setLightControlInfo(controller.getUsingDevice(), lightControlInfo, new OnRcspActionCallback<Boolean>() {
@Override
public void onSuccess(BluetoothDevice device, Boolean message) {
//成功回调
}
@Override
public void onError(BluetoothDevice device, BaseError error) {
//失败回调
//error - 错误信息
}
});
}
Source: LightControlDemo.java
编码 11 字节协议载荷
public byte[] toByteArray() {
byte[] bytes = new byte[11];
int byte0Value = switchState | (lightMode << 2);
byte[] bytesHue = CHexConver.int2byte2(hue);
bytes[0] = CHexConver.intToByte(byte0Value);
bytes[1] = CHexConver.intToByte(Color.red(color));
bytes[2] = CHexConver.intToByte(Color.green(color));
bytes[3] = CHexConver.intToByte(Color.blue(color));
bytes[4] = CHexConver.intToByte(twinkleMode);
bytes[5] = CHexConver.intToByte(twinkleFreq);
bytes[6] = CHexConver.intToByte(sceneMode);
System.arraycopy(bytesHue, 0, bytes, 7, bytesHue.length);
bytes[9] = CHexConver.intToByte(saturation);
bytes[10] = CHexConver.intToByte(luminance);
return bytes;
}
Source: LightControlInfo.java
API Reference
RCSPController#getLightControlInfo(BluetoothDevice device, OnRcspActionCallback<Boolean> callback)
发起读取设备灯光控制信息的 RCSP 指令。
- 参数:
device(BluetoothDevice):目标设备,通常传controller.getUsingDevice()。callback(OnRcspActionCallback<Boolean>):指令发送结果回调。
- 回调:
onSuccess表示指令已发送成功,灯光数据随后在BTRcspEventCallback#onLightControlInfo(BluetoothDevice, LightControlInfo)中返回;onError(BluetoothDevice, BaseError)携带错误信息(如设备未连接、不支持灯光)。 - 注意:使用前需先
addBTRcspEventCallback注册事件监听,否则拿不到数据回调。
RCSPController#setLightControlInfo(BluetoothDevice device, LightControlInfo lightControlInfo, OnRcspActionCallback<Boolean> callback)
将 LightControlInfo 编码为 11 字节载荷并下发设备。
- 参数:
device:目标设备。lightControlInfo:灯光状态对象(SDK 版本com.jieli.bluetooth.bean.device.light.LightControlInfo)。callback:设置结果回调,onSuccess/onError直接反映设备应答。
- 返回:异步,无直接返回值。
BTRcspEventCallback#onLightControlInfo(BluetoothDevice device, LightControlInfo lightControlInfo)
设备灯光信息事件回调,读取操作的数据出口。
- 参数:
device来源设备;lightControlInfo设备上报的灯光状态(含开关、模式、颜色、闪烁、场景、HSL 等字段)。 - 触发时机:
getLightControlInfo成功后的数据回包、设备主动上报灯光变化时。
DeviceInfo#isLightEnable() / DeviceInfo#getCurFunction()
能力探测接口:isLightEnable() 返回设备是否支持灯光功能;getCurFunction() 返回当前功能码,与 AttrAndFunCode.SYS_INFO_FUNCTION_LIGHT 比较可判断设备是否已处于灯光模式。
Failure Modes、边界情况与并发
设备未连接 / 未初始化
isSupportLightMode() 与 isDeviceInLightMode() 都显式判空:getUsingDevice() 返回 null、getDeviceInfo() 返回 null 时直接返回 false(LightControlDemo.java)。若绕过探测直接调用 getLightControlInfo/setLightControlInfo,SDK 会通过 OnRcspActionCallback#onError 返回 BaseError。UI 层应在收到错误时提示用户并禁用灯光入口。
设备不支持灯光功能
对 isLightEnable() == false 的设备,App 不应展示灯光页面;若设备硬件无灯但固件仍响应设置指令,指令会被设备静默忽略,App 侧表现为「设置成功但无视觉效果」—— 因此功能入口必须以能力探测结果为准,而不是以指令应答为准。
HSL↔RGB 换算误差
color(RGB)与 hue/saturation/luminance(HSL)并存于模型中,往返换算存在量化误差(尤其 luminance 接近 100 时 HSL 信息丢失)。LightControlFragment 的注释明确说明:保留 HSL 是因为 luminance=100 时无法保留 h/s,且 color 换算 HSL 容易产生误差。这正是 UI 用 HSL 分量做状态同步、RGB 仅用于最终下发的原因。
异步回调与并发
- 回调注册:
addBTRcspEventCallback注册的是全局事件回调,若页面销毁(onDestroy)后未移除监听,可能导致内存泄漏或对已销毁视图的刷新。业务代码应在 Fragment 生命周期中对称注册/移除。 - 快速连续操作:取色盘拖动会高频触发颜色变化,
isRealTimeRefresh = false保证只有抬手(end == true)才真正执行状态同步与指令下发,从源头限制蓝牙指令频率;收藏动画用isParabolaAnimationShowing标志防重入,避免连续点击收藏按钮导致动画叠加错乱。 - 单例共享:
RCSPController是全局单例,多个页面(彩灯/色温/场景)同时操作时,最后一次setLightControlInfo覆盖前一次,符合「灯光单一状态」的物理语义;但轮询读取与用户设置并发时,应避免用读取结果覆盖用户刚设置的值(可用本地状态优先策略)。
Performance 与操作性建议
- 指令频率控制:蓝牙 BLE 写特征存在吞吐上限,拖动滑块时若每帧都下发会阻塞协议栈。建议沿用「抬手同步」策略,或在需要实时预览时对指令做节流(例如 100-200ms 合并一次)。
- 字节编码成本:
toByteArray()每次调用新建 11 字节数组并做若干次整数转换,成本极低;但注意int2byte2返回 2 字节数组,System.arraycopy拷贝无额外 GC 压力,适合高频调用。 - 本地持久化:收藏色通过
PreferencesHelper同步读写,量小(12 个ColorCollect),无需引入数据库;写入时机建议在动画结束后落盘,避免频繁 IO。
Extension Points
- 新增灯光模式/场景:扩展
LightMode模型(res + name)并在LightModeDialog/LightSceneFragment对应的 adapter 数据源中追加条目,同时保证设备固件侧模式索引一致;模式索引写入LightControlInfo.lightMode/sceneMode后由setLightControlInfo下发。 - 自定义色彩处理:
ColorHSB、ColorHSL、ColorRGB、RGB2HSLUtil等工具类集中了色彩空间换算逻辑,新的交互(如色环取色、渐变)可复用;若引入新的同步策略,建议保持「HSL 为 UI 状态、RGB 为协议载荷」的分层,避免打破防抖机制。 - 设备能力差异化:
DeviceInfo#isLightEnable()与getCurFunction()是现成的能力探测钩子,可据此决定展示彩灯 / 色温 / 场景中的哪些 tab;新设备类型(如仅支持场景灯)只需调整LightContainerFragment的 tab 装配逻辑。
Related Links
- LightControlInfo.java(App 业务模型与协议编码)
- LightControlFragment.java(彩色灯 UI 与 HSL 同步)
- LightColorTemperatureFragment.java(色温灯页面)
- LightSceneFragment.java(场景灯页面)
- LightContainerFragment.java(灯光功能容器)
- LightModeDialog.java(模式选择弹窗)
- LightControlDemo.java(灯光控制 API 示例)
- LightMode.java(模式数据模型)
相关能力:RCSP 协议的其他设备控制功能(如 EQ 音效、ANC 降噪、闹钟等)分属各自 Wiki 页面;蓝牙连接、设备发现与 RCSPController 生命周期属于「蓝牙 SDK 基础能力」页面范畴。