一名嵌入式 Android TV 工程师,用 AI 智能体 WorkBuddy 从代码定位到根因闭环,2 小时解决了一个困扰多日的 CEC 按键 bug。本文完整复盘排查全过程,包括走过的弯路、修正的方向、以及最终钉死的根因链路。
我是一名嵌入式 Android TV 行业的软件工程师,主要在 HiSilicon 平台上做 TV/STB 软件开发。自从使用了 AI 智能体 WorkBuddy 参与解决项目开发中遇到的 bug 后,工作效率得到了大幅提高——它能直接 SSH 到开发服务器读代码、跑命令、做 diff,不只是聊天。
下面用一个真实案例说明:360 方案 HDMI CEC Play/Pause 键只能暂停、无法恢复播放 的完整排查过程。
Android15_Hisi360_Dev(服务器 10.10.11.170:/data/ren_xd/)目录 | 说明 |
|---|---|
| 中间件 CEC 底层实现(C++) |
| CultraView 定制层(Java) |
| AOSP HDMI CEC 框架层 |
我:登录 170 服务器找到 Android15_Hisi360_Dev 代码,目前遇到一个 HDMI 通道 CEC 方面的问题,打开 CEC 功能后,按遥控器上的 play/pause 键,只能实现暂停,不能从暂停转到播放了。主要看
vendor/huanglong和vendor/cultraview这两个目录。
WorkBuddy:代码库找到了(/data/ren_xd/Android15_Hisi360_Dev)。开始定位 CEC 相关实现。
关键突破:MwRxCecCommonCmd.cpp 里有主动发送 USER_CONTROL_PRESSED 的代码。顺着读下去,找到了 RX 处理链,重点看按键映射表和发送侧。
我:同样代码结构的 570 方案,刚测试了,play/pause 键功能是好的。我一会儿把 360 方案的 log 抓给你。
WorkBuddy 已定位到高度可疑点:
结论先说:360 的 CEC 发送等待逻辑比 570 少了一道校验,570 正好有这道补丁。
文件:vendor/huanglong/modules/tvmw/source/hal/tv_mw_framework_rxcecctrl.cpp
函数:MwRxCecCtrl::SendCommandResending()
570(正常)的发送等待循环:
for (; u32Count < CEC_CMD_STATE_CNT; u32Count++) {
s32Ret = uapi_hdmirx_cec_get_cur_msg_state((uapi_hdmirx_cec_status *)&m_stCecTx);
if (s32Ret != TD_SUCCESS || m_stCecTx.u8CurTxLogAddr != pstCecCmd->enDstAddr ||
m_stCecTx.enCurTxOpcode != pstCecCmd->enOpcode) { // ← 校验本条消息真的发出去了
MW_MSLEEP(10);
} else { break; }
}360(异常)的发送等待循环:
if (s32Ret != TD_SUCCESS) { MW_MSLEEP(10); } // ← 只判返回值,不校验 opcode/地址
else { break; } // 读到驱动残留状态就 break初步分析:m_stCecTx 是成员变量,会残留上一条消息的状态。第二次按键时可能读到残留状态就提前判定"已送达",配合 CecResendMsgTimes = 3(T360/cfg.ini:374),实际发送次数不确定。对一个 toggle 语义的 Play/Pause 来说,多发/漏发直接表现为"回不到播放"。
⚠️ 后来证明这个方向是错的——但这正是真实排查的过程,先记下来。
WorkBuddy 给出了抓 log 的过滤条件:
adb shell logcat -c
adb shell logcat -v threadtime TvMW_MwRxCecCtrl:V TvMW_MwRxCecManager:V TvMW_MwRxCecRemoteCtrlHandler:V *:S还建议了一个快速验证:把 T360/cfg.ini:374 的 CecResendMsgTimes 临时改成 1(关闭重发),如果症状消失就锁定重发逻辑。
我:先抓了一份 570 的,你看一下,桌面
570-good.txt。
WorkBuddy:日志看完了——但结论不在日志里。
这份 570-good.txt 有两个致命问题:
01:32:14.331 — 01:32:15.949),logcat -c 之后马上就 Ctrl+C 了Cec 大小写都搜了,里面只有 AconfigFlags(1150 行)、DolDTV_stack_ssu(240 行)这类开机噪声,当前还是 DVBT 通道等 log 这段时间,WorkBuddy 把两套代码 diff 完了,根因已经找到,而且不需要日志佐证。
先撤回上一轮的猜测——
SendCommandResending缺校验那处是 360 新增的防御性改动,跟这个 bug 无关。
真正的链路(全部实测确认):
① 遥控器 play/pause 键 = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
② 发送路径差异——这是根因所在
570 的 CtvCommon.sendCecCommand():
if (event.getAction() == ACTION_UP) return HalApi.setUICommand(keyCode);360 的同一函数(1597 行起):这三行被注释掉了,改走 AOSP 原生 HdmiTvClient.sendKeyEvent(keyCode, true/false)。
③ framework 收到后查表
文件:frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecKeycode.java:322
// 317-319 行 AOSP 原注释:
// Note that we map both MEDIA_PAUSE and MEDIA_PLAY_PAUSE to CEC PAUSE key.
new KeycodeEntry(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, CEC_KEYCODE_PAUSE), // = 0x46,固定映射同文件 368-369 行:PAUSE_PLAY_FUNCTION(0x61) 对应 UNSUPPORTED_KEYCODE——没有任何 Android keycode 能产出 0x61。
④ 结果
每次按都发 <User Control Pressed>(0x44) + UI cmd 恒为 0x46 Pause → 机顶盒永远暂停,回不到播放。症状 100% 吻合。
⑤ 570 为什么正常
570 的 HalApi.java:720 mediaPlayPause() 用静态 isStop 在 CEC_MENU_PLAY(→0x44) / CEC_MENU_PAUSE(→0x46) 之间交替,所以第二次能发 Play。而 g_keyCode[] 映射表、MW_CEC_DEV_MENU_COMMAND 枚举、MwRxCecRemoteCtrlHandler.cpp、MwRxCecCommonCmd.cpp 两方案逐字节一致,已彻底排除。
我:又抓了一份 360 的,你看一下有没有用,桌面
360-bad.txt。
WorkBuddy:有用,而且是完整的实锤。这份 log 把代码分析闭环了。
4 次按键,4 次都发的是 Pause:
时间 | 源地址 | 目的地址 | Opcode | UI Command | 含义 |
|---|---|---|---|---|---|
16:39:53.273 | 0x0 (TV) | 0x4 (Playback 1) | 0x44 USER_CONTROL_PRESSED | 0x46 | Pause |
16:39:56.629 | 0x0 | 0x4 | 0x44 | 0x46 | Pause |
16:40:04.390 | 0x0 | 0x4 | 0x44 | 0x46 | Pause |
16:40:08.071 | 0x0 | 0x4 | 0x44 | 0x46 | Pause |
0x44 Play 一次都没出现。 四次按下、四次都是 Pause——和 HdmiCecKeycode.java:322 那条 KEYCODE_MEDIA_PLAY_PAUSE → CEC_KEYCODE_PAUSE 的固定映射逐字对上。
android.hardware.tv.cec@1.0-service——这是 HAL 服务,坐实走的是 framework → HAL 标准链路TvMW_MwRxCecCtrl、TvMW_cecctrl、TvMW_MwRxCecManager、TvMW_rxtifcec_manager 一个都没出现——说明按键根本没走厂商 CEC 发送路径(上一轮让抓 TvMW_MwRxCecCtrl 是错的方向,已修正)CtvCommon 有 16 条日志——sendCecCommand() 确实被调用了,getHdmiCecMode() 门控也过了根因已经钉死。源地址
0x0(TV)、目的地址0x4(Playback Device 1) 及 ACK 均正常,纯粹是 UI Command 操作数错误。不用怀疑寻址、不用怀疑 ACK、不用怀疑重发。
360 上 CEC 问题要看 android.hardware.tv.cec@1.0-service 这个 tag,别抓 TvMW_*:
adb shell logcat -c
adb shell logcat -v threadtime "android.hardware.tv.cec@1.0-service":V CtvCommon:V *:S570 反而要看 TvMW_* 那组(因为 570 走 HalApi)。两方案的日志位置是反的,这个值得记一下。
在 360 CtvCommon.sendCecCommand() 里把 KEYCODE_MEDIA_PLAY_PAUSE 单独分流,用本地 bool 交替翻译成 KEYCODE_MEDIA_PLAY / KEYCODE_MEDIA_PAUSE 再交给 hdmiTvClient.sendKeyEvent()。
这两个 keycode 在 framework 表里映射是对的:
KEYCODE_MEDIA_PLAY → 315 行 → 0x44 Play ✓KEYCODE_MEDIA_PAUSE → 321 行 → 0x46 Pause ✓关键点:翻转必须放在 DOWN 的 debounce 通过之后(CEC_KEY_INTERVAL = 350ms,CtvCommon.java:207/1642),DOWN/UP 复用同一个翻译结果,否则会在 DOWN/UP 各翻一次。
我:570 是在哪个文件解析成 0x44 和 0x46 摇摆的,你想在 360 代码哪个文件里做修改呢?
WorkBuddy:570 的"摇摆"是纯 Java 逻辑,就在 HalApi.mediaPlayPause()(701~709 行),一个 static boolean + 两行 setUICommand。C++ 层只是把它翻成协议码,两边枚举索引 7/12 完全对齐。
唯一一处:vendor/cultraview/common/apps/CtvMiddleware/CultraviewTvService/src/android/cultraview/tv/service/common/CtvCommon.java
位置 | 改动内容 |
|---|---|
成员变量区 208 行 | 在 |
主改动 1642~1647 行 | DOWN 分支 debounce 通过后做翻译:翻转 |
配套 1657 行 | UP 分支改用翻译后的 keycode(复用 DOWN 翻译结果) |
语义核对:mCecPlayPausePaused 初值 false → 第一次按下翻成 true → 发 PAUSE;第二次翻回 false → 发 PLAY。和 570 的 isStop 完全一致。
HalApi.java 不要改,改了不生效。 360 的 CtvCommon.sendCecCommand() 已经不调用它了(1590-1592 那三行被注释),HalApi.mediaPlayPause() 在 360 上对 CEC 透传而言是死代码。除非把 CtvCommon 整个改回 570 的写法——但那会丢掉 360 改用 framework 的初衷(认证要求的 DOWN/UP 间隔、active source 由切源链路保证),不建议。getRepeatCount() == 0 守卫别省。 因为翻转点在 DOWN 分支,而 DOWN 会随长按反复上来。现有 debounce 是 350ms,不拦这个的话长按 2 秒会来回翻 6 次,比原 bug 还难看。570 之所以没这问题,是因为它只在 ACTION_UP 响应,长按不重复。另外
CtvCommon.java属于独立子仓库CultraviewTvService(.git 软链到 .repo/projects/...),当前 git status 是干净的,改完 diff 和回滚都好办。
HdmiCecKeycode.java:322 改成 CEC_KEYCODE_PAUSE_PLAY_FUNCTION,由 sink 自己 toggle,TV 侧零状态。更符合 CEC 规范,但 0x61 属 CEC 1.4+ Function 命令、设备兼容性未验证,且与 570 实现不一致。
修复后实机验证:Play/Pause 键播放/暂停切换正常,bug 结案。
以下是提交到公司 Mantis 系统的原因分析(纯文本排版,可直接粘贴):
【问题现象】
开启 CEC 功能后,HDMI 通道下按遥控器 Play/Pause 键只能暂停,无法从暂停恢复播放。
【根本原因】
360 方案整机切换为 TIF CEC 模式(vendor/device/tv/T360/properties/vendor_properties.mk:persist.vendor_im.tvmw.cec.tifmode=true),厂商中间件 CEC 发送通道被整体旁路——modules/tvmw/source/hal/tv_mw_framework_rxcecctrl.cpp:97,MwRxCecCtrl::setCommand() 在 TIF 模式下直接 return(Enable()/RegisterRxcecCmdCallback() 同样 bypass),且 360 未提供 570 方案具有的 tv_mw_logic_rxtifcec_manager.cpp,厂商侧无替代实现。
因此上层 CEC 发送改由 Android framework 承接:vendor/cultraview/.../CtvCommon.java 的 sendCecCommand() 中原 HalApi.setUICommand() 已注释,改调 hdmiTvClient.sendKeyEvent()。
而 AOSP 映射表 frameworks/base/services/core/java/com/android/server/hdmi/HdmiCecKeycode.java:322 将 KEYCODE_MEDIA_PLAY_PAUSE 固定映射为 CEC Pause:
new KeycodeEntry(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, CEC_KEYCODE_PAUSE), // 0x46该表原注释已说明其为接收方向设计("both MEDIA_PAUSE and MEDIA_PLAY_PAUSE map to CEC PAUSE key"),且 0x61 Pause Play Function 在表中为 UNSUPPORTED_KEYCODE,无任何 Android keycode 可产出。故每次按键发出的 UI Command 操作数恒为 0x46 Pause,toggle 语义丢失。
【日志证据】(tag: android.hardware.tv.cec@1.0-service,连续按键 4 次)
16:39:53.273 0x0 -> 0x4 opcode:0x44 USER_CONTROL_PRESSED arg[0]=0x46 Pause
16:39:56.629 0x0 -> 0x4 opcode:0x44 arg[0]=0x46 Pause
16:40:04.390 0x0 -> 0x4 opcode:0x44 arg[0]=0x46 Pause
16:40:08.071 0x0 -> 0x4 opcode:0x44 arg[0]=0x46 Pause4 次全为 Pause,0x44 Play 一次未出现;每次伴随 0x45 USER_CONTROL_RELEASED。源地址 0x0(TV)、目的地址 0x4(Playback Device 1) 及 ACK 均正常,仅 UI Command 操作数错误。
【修复方案】
在 CtvCommon.sendCecCommand() 中为 KEYCODE_MEDIA_PLAY_PAUSE 补回 toggle 语义:维护本地状态 mCecPlayPausePaused,ACTION_DOWN 通过 350ms debounce 后翻转(加 getRepeatCount()==0 守卫,避免长按重复翻转),交替翻译为 KEYCODE_MEDIA_PLAY / KEYCODE_MEDIA_PAUSE 后交由 sendKeyEvent 发送,ACTION_UP 复用同一 keycode。行为与 570 方案 HalApi.mediaPlayPause()(isStop 交替发 CEC_MENU_PLAY / CEC_MENU_PAUSE)保持一致,不修改 framework。
【修改文件】
vendor/cultraview/common/apps/CtvMiddleware/CultraviewTvService/src/android/cultraview/tv/service/common/CtvCommon.java
【验证结果】
实机验证通过,Play/Pause 键播放/暂停切换正常。
阶段 | 做了什么 | 对不对 |
|---|---|---|
第一步 | 定位 | ✅ 范围对了 |
第二步 | 怀疑 | ❌ 方向错了,那是防御性改动 |
第三步 | 抓 570 的 log 验证 | ❌ log 没用上(时间窗太短、通道不对) |
第四步 | diff 两套代码,定位到 framework 固定映射 | ✅ 根因在这 |
第五步 | 抓 360 的 log 实锤 | ✅ 4 次全 Pause,闭环 |
360 和 570 两套方案的 CEC 日志位置是反的:
android.hardware.tv.cec@1.0-service tagTvMW_MwRxCecCtrl / TvMW_MwRxCecManager 等 tag如果照搬 570 的抓 log 方式去抓 360,会一无所获——这个坑值得所有做跨方案对比的同事注意。
本文基于 Hi3751V360 Android 15 平台真实排查记录整理,代码路径和行号对应 Android15_Hisi360_Dev 代码库。
#WorkBuddy #CodeBuddy #AndroidTV #HDMI #CEC #嵌入式开发 #AI辅助开发
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。