Files
abox-mgr/docs/superpowers/specs/2026-08-24-infrared-control-design.md
T

607 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 红外学习与发射控制设计
## 1. 背景与目标
本项目当前可用于真实验证的设备包括 ABox 主机、红外控制器、智能插座和温湿度计。本阶段由 moler 负责红外能力,范围聚焦于红外控制器的基础闭环:
1. 在登录成功后的现有页面提供一个临时入口;
2. 手动配置并记住一个 ABSDK 红外设备名;
3. 创建并命名多个红外按键;
4. 通过 ABSDK 发起红外学习;
5. 保存可重复使用的按键绑定;
6. 通过 ABSDK 发射已学习按键;
7. 支持 Fake 和真实 ABSDK 调试模式;
8. 为后续生活场景保留稳定按键引用。
首版前端只用于功能验证,沿用现有 Compose 和设计系统基础组件,不定义最终产品视觉风格。后续统一视觉改版只替换展示层,不改变红外领域、持久化、应用用例或硬件接口。
## 2. 已确认的 ABSDK 能力边界
ABSDK 0630 提供以下红外公开方法:
```java
ABRet sendIr(String irDevName, String irKey)
ABRet studyIrByIrDevName(String irDevName, String irKey)
ABRet downloadIrCodes(List<IrInfo> irInfos)
```
现有领域层已将其隔离为 `InfraredGateway`:
```kotlin
interface InfraredGateway {
suspend fun sendKey(
profileId: ProviderProfileId,
deviceName: DeviceName,
key: String,
): DomainResult<Unit>
suspend fun learnKey(
profileId: ProviderProfileId,
deviceName: DeviceName,
key: String,
): DomainResult<Unit>
suspend fun downloadCodes(
profileId: ProviderProfileId,
codes: List<InfraredCode>,
): DomainResult<List<InfraredCodeDownloadResult>>
}
```
本设计必须遵守以下事实:
- 学习接口只确认返回成功码和消息,没有已确认的原始红外码返回值;
- ABSDK 没有公开停止学习方法;
- ABSDK 没有公开获取学习结果或读取已学习原始码的方法;
- `downloadIrCodes()` 是把调用方已经拥有的红外码写入主机,不是读取学习结果;
- ABSDK 没有设备扫描或设备列表接口;
- 红外方法按 `irDevName + irKey` 寻址;
- SDK 调用为同步阻塞调用,现有 Bridge 在 `Dispatchers.IO` 上通过全局 `Mutex` 串行执行;
- SDK 使用固定明文 HTTP 目标,真实接入存在已知传输安全风险。
因此首版保存的是本地按键绑定,而不是原始红外波形或码值:
```text
ProviderProfileId + DeviceName + sdkKey + displayName + learning status
```
真实硬件闭环成立的判据是:
```text
learnKey(deviceName, sdkKey) 成功
↓
sendKey(deviceName, sdkKey)
↓
实体被控设备产生正确响应
```
## 3. 范围
### 3.1 本阶段包含
- 红外功能的领域模型和本地持久化端口;
- Room 数据库、DAO 和 Repository;
- 学习、重试、发射、删除和恢复应用用例;
- 红外 ViewModel 和简单 Compose 页面;
- 登录成功页面的临时红外入口;
- 手动输入并记住一个 `irDevName`;
- 单红外控制器、多按键;
- Fake/Real ABSDK 显式调试模式;
- 自动化测试;
- 真机验收清单;
- 面向未来生活场景的稳定按键引用。
### 3.2 本阶段不包含
- 最终 UI 视觉规范或复杂遥控器布局;
- 多个遥控器管理界面;
- ABSDK 不存在的设备扫描;
- 原始红外码读取或持久化;
- 红外码库导入 UI;
- 硬件停止学习;
- 完整生活场景模型、规则引擎或场景执行器;
- Release 中默认启用真实 ABSDK;
- 绕过或隐藏厂商明文 HTTP 风险;
- 删除 ABox 服务端已学习按键。
## 4. 架构
采用独立应用编排层,保持现有 `InfraredGateway` 作为纯硬件能力端口:
```text
登录成功页面
└─ 临时入口:红外控制
↓
InfraredControlScreen
↓
InfraredControlViewModel
↓
应用用例
├─ LearnInfraredKey
├─ RetryInfraredKeyLearning
├─ SendInfraredKey
├─ DeleteInfraredKey
├─ RecoverInterruptedInfraredLearning
└─ ObserveInfraredKeys
↙ ↘
InfraredKeyRepository InfraredGateway
↓ ├─ FakeInfraredGateway
RoomInfraredKeyRepository └─ AbsdkBridge
↓
ABSDK 0630
```
### 4.1 边界原则
- `InfraredGateway` 只负责学习、发射和码库下载,不负责 Room、页面状态或遥控器管理;
- 不修改 `learnKey()` 返回类型,不虚构 ABSDK 原始码结果;
- 不把 `ABSDK`、`ABRet`、`IrInfo` 或原始 Map 暴露给上层;
- ViewModel 不直接编排数据库和硬件之间的一致性,统一通过应用用例;
- Room Entity 不作为领域或 UI 公共模型;
- Fake 和真实 Bridge 必须共用相同的领域接口和用例;
- 所有真实 Gateway 接口必须指向同一个进程级 `AbsdkBridge` 实例,保持全局会话和互斥锁一致。
## 5. 数据模型
### 5.1 稳定标识
```kotlin
@JvmInline
value class InfraredKeyId(val value: String)
data class InfraredKeyRef(
val profileId: ProviderProfileId,
val keyId: InfraredKeyId,
)
```
`InfraredKeyRef` 是未来生活场景使用的最小稳定引用。场景不应保存显示名、设备名或厂商 Key。
### 5.2 学习状态
```kotlin
enum class InfraredKeyStatus {
LEARNING,
READY,
FAILED,
}
```
状态表示 App 对最近一次学习调用结果的本地记录,不代表设备主动上报的实时状态。
### 5.3 领域模型
```kotlin
data class SavedInfraredKey(
val id: InfraredKeyId,
val profileId: ProviderProfileId,
val deviceName: DeviceName,
val displayName: String,
val sdkKey: String,
val status: InfraredKeyStatus,
val lastErrorCode: String?,
val createdAtEpochMillis: Long,
val updatedAtEpochMillis: Long,
)
```
字段约束:
- `id` 在 App 内稳定,供页面、删除和未来生活场景引用;
- `profileId` 用于不同登录配置的数据隔离;
- `deviceName` 是传给 ABSDK 的真实红外设备名;
- `displayName` 是用户可见名称,可以修改;
- `sdkKey` 是传给 ABSDK 的稳定 Key,学习后不随显示名改变;
- `lastErrorCode` 仅保存脱敏厂商错误码,不保存密码、Token、原始 Map 或红外载荷;
- 时间字段由应用层时钟提供,测试中使用可控时钟。
### 5.4 SDK Key
App 自动生成 SDK Key,不允许用户直接编辑。格式应:
- 仅包含安全 ASCII 字符;
- 在同一 Provider 和红外设备下唯一;
- 重试学习时复用;
- 修改显示名时保持不变;
- 不直接使用中文、空格或特殊字符。
建议格式:
```text
app_ir_<normalized-infrared-key-id>
```
实际长度上限尚未由 ABSDK 文档确认,实现前需选择保守长度,并列入真机验证。
## 6. 本地持久化
采用 Room。Repository 边界至少支持:
```kotlin
interface InfraredKeyRepository {
fun observeKeys(
profileId: ProviderProfileId,
deviceName: DeviceName,
): Flow<DomainResult<List<SavedInfraredKey>>>
suspend fun getKey(
ref: InfraredKeyRef,
): DomainResult<SavedInfraredKey?>
suspend fun insertLearningKey(
key: SavedInfraredKey,
): DomainResult<Unit>
suspend fun updateStatus(
ref: InfraredKeyRef,
status: InfraredKeyStatus,
lastErrorCode: String?,
updatedAtEpochMillis: Long,
): DomainResult<Unit>
suspend fun renameKey(
ref: InfraredKeyRef,
displayName: String,
updatedAtEpochMillis: Long,
): DomainResult<Unit>
suspend fun deleteKey(
ref: InfraredKeyRef,
): DomainResult<Unit>
suspend fun findInterruptedLearning(
profileId: ProviderProfileId,
): DomainResult<List<SavedInfraredKey>>
}
```
具体签名可在实施计划中按项目既有风格调整,但必须保持职责和语义。
### 6.1 数据库约束
- 主键:`InfraredKeyId`;
- 索引:`ProviderProfileId + DeviceName`;
- 唯一约束:`ProviderProfileId + DeviceName + sdkKey`;
- 查询和删除必须包含 Provider 隔离;
- 不保存原始红外码;
- 不把 Room Entity 暴露给领域层;
- 首版预留 schema version 和迁移测试基础。
### 6.2 单设备策略
页面记住一个当前 `irDevName`,并只展示该设备下的按键。切换当前设备名不会迁移或重写旧按键;旧按键仍绑定学习时使用的设备名。
设备名可以保存到 Room 配置表或 DataStore。若采用 DataStore,只用于当前设备名和 debug 模式等轻量配置,不承载红外按键集合。
## 7. 学习流程
`LearnInfraredKey` 的流程:
1. 验证用户已登录且存在有效 `ProviderProfileId`;
2. 验证 `deviceName` 和 `displayName` 非空;
3. 生成稳定 `InfraredKeyId` 和 `sdkKey`;
4. 先写入状态为 `LEARNING` 的记录;
5. 在应用级协程作用域调用 `InfraredGateway.learnKey()`;
6. 成功时更新为 `READY` 并清空错误码;
7. 失败时更新为 `FAILED` 并保存脱敏错误码;
8. 如果状态更新失败,记录可恢复的本地数据错误,但不重新生成 SDK Key;
9. 页面通过 Room Flow 观察最终状态。
### 7.1 重试
重试必须:
- 使用原 `InfraredKeyId`;
- 使用原 `sdkKey`;
- 先将状态更新为 `LEARNING`;
- 再调用 ABSDK;
- 不创建重复按键记录。
### 7.2 页面离开
学习调用开始后,页面可以退出。任务继续在应用级作用域等待 SDK 返回,并更新 Room。ViewModel 销毁不应直接取消已经提交的学习编排。
这不是后台系统任务保证:如果进程被系统杀死,底层同步网络请求及设备状态无法可靠恢复。
### 7.3 进程恢复
App 启动或登录恢复后,查找遗留的 `LEARNING` 记录:
```text
LEARNING → FAILED
错误提示:上次学习结果未确认,请重新学习
```
不得自动标记为 `READY`,也不得自动发射来试探设备状态。
### 7.4 取消语义
ABSDK 没有停止学习 API。首版 UI 不提供或宣称“停止硬件学习”。用户可以离开页面,但学习调用仍等待成功、失败或 SDK 超时。
## 8. 发射流程
`SendInfraredKey` 的流程:
1. 使用 `InfraredKeyRef` 查询本地记录;
2. 记录不存在时返回领域错误;
3. 仅允许 `READY` 状态发射;
4. 取得记录中的 `profileId + deviceName + sdkKey`;
5. 调用 `InfraredGateway.sendKey()`;
6. 成功时向 UI 返回一次性成功事件;
7. 失败时保留按键 `READY` 状态,仅报告本次错误;
8. 不自动重试,避免重复操作电视、空调等实体设备。
未来生活场景通过稳定 `InfraredKeyRef` 调用同一发送用例或薄执行器:
```kotlin
interface InfraredActionExecutor {
suspend fun execute(ref: InfraredKeyRef): DomainResult<Unit>
}
```
本阶段只定义或保证稳定引用边界,不实现 Scene 模型、规则引擎或场景执行顺序。
## 9. 删除与重命名
### 9.1 删除
- `FAILED` 和 `READY` 记录可以删除;
- `LEARNING` 中不允许删除,避免后台结果写入已不存在的记录;
- 删除只移除本地引用;
- ABSDK 没有删除服务端红外 Key 的公开接口;
- UI 必须提示“ABox 服务端可能仍保留该按键”;
- 删除后未来场景引用解析失败,不再发射。
### 9.2 重命名
- 只修改 `displayName`;
- 不修改 `id`、`deviceName` 或 `sdkKey`;
- 重命名后学习和发射能力保持不变;
- 场景引用保持有效。
## 10. Android 前端
### 10.1 临时入口
在当前登录成功后的页面添加“红外控制”按钮,导航到独立 `InfraredControlScreen`。未来正式信息架构完成后只迁移入口。
### 10.2 页面内容
首版页面只包含:
- 返回按钮和标题;
- 当前数据源标识:Fake 或真实 ABox;
- 红外控制器设备名输入和保存;
- 已学习按键列表;
- 学习新按键按钮;
- 按键状态;
- READY 按键的发射按钮;
- FAILED 按键的重试按钮;
- 删除入口和确认提示;
- 基础错误反馈。
不加入复杂遥控器模板、图标系统、网格布局、品牌色或动画规范。
### 10.3 设备名
- ABSDK 没有设备扫描能力;
- 首次进入时手动填写 `irDevName`;
- 保存后本地记住;
- 没有设备名时禁止学习;
- 修改设备名前提示旧按键仍绑定旧设备;
- 列表只显示当前设备名下的按键。
### 10.4 学习交互
用户输入显示名称后开始学习。页面提示:
```text
请将实体遥控器对准红外控制器并按下目标按键。
```
按键显示 `LEARNING`、`READY` 或 `FAILED`。允许离开页面,但不显示“停止硬件学习”。
### 10.5 发射交互
- 只有 `READY` 按键可发射;
- 发射期间禁用该按键,防止重复点击;
- 成功显示短暂提示;
- 失败显示具体错误;
- 不自动重试;
- 单次失败不删除或降级按键状态。
### 10.6 删除提示
删除确认文案应明确:
```text
仅从本机列表移除,ABox 服务端可能仍保留该按键。
```
## 11. Debug Fake/Real 路由
Debug 构建提供显式运行模式:
- `FAKE`:页面开发、自动化测试和错误模拟;
- `REAL_ABSDK`:复用当前登录会话,调用真实 `AbsdkBridge`。
约束:
- 页面持续显示当前模式;
- 不允许在学习任务进行中切换模式;
- Fake 和 Real 共用同一 ViewModel、应用用例和 Room Repository;
- 模式和 Provider 数据必须隔离,避免 Fake 按键误用于真实设备;
- Fake 支持按 `ProviderProfileId + DeviceName + sdkKey` 配置成功或失败;
- 真实模式的所有 Gateway 接口绑定同一 `AbsdkBridge` 单例;
- 真实模式必须显式 opt in `CleartextAbsdkTransport`;
- Release 暂不启用真实 Bridge,除非项目明确批准传输安全方案。
真实 App 接入还必须补齐:
- `:absdk-bridge` 依赖;
- Hilt 绑定;
- `INTERNET` 权限;
- 经批准的明文网络配置;
- 登录会话与红外 Gateway 的同一 Bridge 实例;
- Token 失效后的页面返回登录流程。
## 12. 错误处理
继续使用现有 `DomainResult` 和 `DomainError`。不为首版扩充 sealed 类型,优先利用已有分类和 `externalCode`。
| 情况 | UI 反馈 | 本地状态 |
|---|---|---|
| Token 无效 | 登录已失效,返回登录 | 学习按键为 FAILED;发射按键仍为 READY |
| 红外设备不存在 | 检查设备名 | 学习按键为 FAILED |
| 红外设备离线 | 检查供电和网络 | 学习按键为 FAILED;发射按键仍为 READY |
| KEY/码值不存在 | 建议重新学习 | 发射按键仍为 READY,由用户决定重学 |
| 红外发射失败 | 显示失败,可手动重试 | READY |
| 网络超时 | 网络或 ABox 服务不可用 | 学习按键为 FAILED;发射按键仍为 READY |
| 本地保存失败 | 无法开始学习 | 不调用硬件学习 |
| 学习成功但最终状态写入失败 | 显示数据保存异常并保留恢复信息 | 不生成新 sdkKey |
| 未知错误 | 通用提示及脱敏错误码 | 按操作类型处理 |
日志不得包含:
- 用户密码;
- Token;
- 原始 SDK Map;
- 原始红外码或载荷;
- 可用于重放的敏感字段。
## 13. 自动化测试
### 13.1 应用用例
覆盖:
1. 学习前先保存 `LEARNING`;
2. 学习成功更新为 `READY`;
3. 学习失败更新为 `FAILED`;
4. 重试复用原 `sdkKey`;
5. 修改显示名不改变 `sdkKey`;
6. 只有 `READY` 可以发射;
7. 发射使用保存的 `deviceName + sdkKey`;
8. 不同 Provider 数据隔离;
9. 遗留 `LEARNING` 恢复为 `FAILED`;
10. 删除后稳定引用失效;
11. ViewModel 销毁后应用级学习任务仍可落库;
12. 发射失败不改变 READY;
13. 进行中的学习阻止运行模式切换。
### 13.2 Room
覆盖:
- 新增、观察、查询、重命名和删除;
- `ProviderProfileId + DeviceName` 过滤;
- `sdkKey` 唯一约束;
- Provider 隔离;
- 状态转换;
- 稳定 ID 跨重命名不变;
- 首版 schema 和迁移测试基础;
- 不存储原始红外载荷。
### 13.3 Fake
扩展现有 Fake,覆盖:
- 按 Provider、设备和 Key 脚本化结果;
- 调用参数与顺序;
- 连续响应;
- 成功、离线、超时和通用失败;
- 不同 Provider 互不影响。
### 13.4 Bridge
保留现有覆盖并补充:
- 学习参数和调用顺序;
- 发射参数和调用顺序;
- 红外错误码 `20201` 至 `20206` 的映射;
- 学习、发射与其他设备调用的全局串行;
- Token 失效清理会话;
- SDK 空值和异常防御;
- 不改变 `downloadCodes()` 的部分成功语义。
### 13.5 ViewModel 与 Compose
只验证功能,不验证最终视觉:
- 无设备名不能学习;
- 保存和恢复设备名;
- 创建学习任务;
- 显示学习中、成功和失败;
- 重试学习;
- 点击发射并防重复点击;
- 删除确认;
- Fake/Real 模式标识;
- 登录失效引导;
- 基本中文和英文资源。
## 14. 真机验收
第一轮使用现有 ABox 主机和红外控制器:
1. 登录真实账号;
2. 输入后台提供的准确 `irDevName`;
3. 创建唯一按键,例如“测试电源”;
4. 调用学习并记录耗时、返回码和消息;
5. 学习成功后立即发射;
6. 验证实体电器是否响应;
7. 用同一按键重复发射;
8. 重新学习同一 `sdkKey`,验证覆盖或报错行为;
9. 红外控制器断电后验证离线错误;
10. 不按实体遥控器,验证实际超时行为;
11. 页面退出后验证后台结果落库;
12. 进程被杀后验证遗留 LEARNING 恢复;
13. Token 失效后验证返回登录;
14. 记录 SDK 是否存在未文档化 `dicDatas`,但不将内容写入生产日志。
### 14.1 必须验证的未知项
- 学习调用是立即返回还是阻塞到捕获完成;
- 学习成功是否自动绑定 `sdkKey`;
- 同一 `sdkKey` 重学是否覆盖;
- 无信号时实际超时时间;
- 页面离开后底层调用是否可完成;
- 设备名格式、大小写和特殊字符规则;
- 服务端是否永久保留已学习 Key;
- SDK 是否返回未文档化数据;
- `sdkKey` 的长度限制;
- 真实模式所需初始化信息和调用顺序。
这些未知项不得通过业务层假设隐藏。真机结果应更新协议文档和后续设计。
## 15. 验收标准
本阶段完成需要满足:
- 登录成功页面可以进入简单红外页面;
- 可以手动配置并恢复一个红外设备名;
- 可以创建多个命名按键;
- 学习调用前已持久化 LEARNING;
- 学习成功或失败能正确落库;
- READY 按键可以调用发射;
- 显示名修改不影响发射;
- Fake 模式可以覆盖主要状态和错误;
- Real 模式明确标识并调用真实 Bridge;
- 页面退出后学习任务仍可更新状态;
- 进程中断后遗留学习状态可以保守恢复;
- 自动化测试通过;
- 真机至少完成一次“学习成功 → 发射 → 实体响应”;
- 未确认硬件语义被记录,不伪装为已支持;
- 未来生活场景可以通过稳定 `InfraredKeyRef` 引用按键。
## 16. 后续工作
本设计完成后再分别规划:
1. 多遥控器和房间组织;
2. 统一产品视觉和正式导航入口;
3. 红外码库导入;
4. 生活场景编排;
5. Scene 顺序执行和单项结果;
6. 厂商明文 HTTP 的正式安全方案;
7. 真机验证后对学习语义、超时和错误模型的修订。