
最后更新:2026-11-04 20:05 机器:Xiaomi Pad 5(nabu)· Ubuntu 26.04 ARM64 · UFS:
SAMSUNG KLUFG8RHGB-B0E1内核:6.14.11-nabu-tmm(快充 + 手写笔充电,自编 idtp9418 模块)/6.17.0-xiaomi-nabu-tmm-UFS-slowdown+(自带笔驱动,但只有 5V 慢充) 覆盖:启动与 fastboot 自救 · 充电(含手写笔无线充/蓝牙)· Waydroid 与安卓应用 · 宿主音频 · 亮度音量同步 · 桌面工具链(Markdown 查看 / dsh)
0. 速查总表
| 场景 | 症状 | 处理 |
|---|---|---|
| 启动 | 卡在 fastboot 出不来 |
fastboot set_active a → fastboot reboot
|
| 启动 | 7 次启动额度快用完 |
等桌面通知,补完跑 nabu-boot-count reset
|
| 充电 | 插着电却在掉电 |
查 v_offset,坏了就拔插一次
|
| 充电 | 只有 5V、输入电流很小 |
同上,多半是 v_offset 被写坏
|
| 充电 | 低电量插 33W/100W 就抖 | 先用 18W 充到 40% 再换大功率头 |
| 手写笔 | 吸上去完全没反应 | 6.14 需要 idtp9418 模块(已装,见 2.4);确认已加载 |
| 手写笔 | 蓝牙搜不到 / 连不上 | 长按笔按键 5 秒进配对模式,见 2.5 |
| Waydroid | 无声音 / 声音断续 | 见 3.2 两处补丁 |
| Waydroid | 起来 40 秒后自己死 | 完整重建,见 3.4 |
| 宿主音频 | 卡顿时电流声 / 机器人声 | 量子钉 1024,见 4.1 |
| 宿主音频 | 开杜比后 50% 以上外放炸 | 9 段拟合 + 零补偿,见 4.2 |
| 亮度音量 | 两端不同步 | 见 5.1 |
| 显示 | 黑屏/点不亮 / 电源键唤不醒 | 见附录 F |
| 游戏 | 碧蓝航线点开闪退(EXIT_SELF 1) |
见 3.11,WebView 已修;仍卡 BgcPlugin NPE
|
| 游戏 | 异环黑屏(卡启动闪屏后 SIGSEGV) |
见 3.12,libUnreal.so 静态构造空写,容器里基本无解
|
| 游戏 | 任何 app 的 WebView 白屏/闪退 |
echo '--disable-gpu-compositing' > /data/local/tmp/webview-command-line
|
| 文档 |
双击 .md 只有纯文本、想要渲染
|
用 dsh 的文档预览,见 6.1–6.2;别装 apostrophe |
| 文档 | 要打开工作区以外的文件 | 「文件」面板标题栏的「打开任意文件」按钮,见 6.3 |
| dsh |
装插件报 spawn pnpm ENOENT
|
sudo npm i -g pnpm@10,见 6.4
|
第 1 部分 · 系统与启动
1.1 启动链与分区
ABL(打过 dualboot 补丁)→ boot_a 里的 bootmanager(TWRP-LINUX)
→ ESP(sda31) 上的 rEFInd(菜单 5 秒)→ EFI/ubuntu/*.efi
→ 内核 → root=PARTLABEL=linux (sda32)
-
ESP:
/dev/disk/by-partlabel/esp(334MB,sda31),含EFI/ubuntu/6.14.11.efi、6.17.0-UFS-slowdown+.efi -
根分区:
linux(sda32,458GB,挂/) - boot_a:TWRP-LINUX(dualboot bootmanager),不要随意刷
-
anti:1(防回滚已开启,别刷旧固件)
1.2 A/B 槽:只有 7 次启动额度
实测事实(2026-11-02):
-
每次启动
slot-retry-count:a减 1(实测 5 → 4,一次完整启动) -
fastboot set_active a把它重置为 7,同时清掉slot-unbootable -
slot-successful:a永远是no:Ubuntu 侧没有任何东西上报"启动成功" -
减到 0 → ABL 把该槽标记
unbootable→ 没有可启动的槽 → 停在 fastboot (11/02 那次事故就是这么来的) -
槽元数据不在 AP 可读分区:108 个 <128MB 分区哈希对比,
set_active a前后只有logfs变化(UEFI 日志);misc恒为那 15 个字节、devinfo全零。 结论:存在 AP 访问不到的保护区(UFS LUN / RPMB / ABL 自有存储),Linux 侧写不了"启动成功" -
fastboot oem off-mode-charge 1能写入(连带把charger-screen-enabled置 1), 但挡不住插电自动开机,对保住计数没用
建议节奏:
- 计数到 4 会弹通知,此时还剩 3 次余量,找方便的时间补
- 平时尽量用挂起代替关机,能显著减少启动次数
- 别在没接 PC 的情况下重启
1.3 掉进 fastboot 怎么救
# 只读确认
fastboot getvar current-slot # 期望 a
fastboot getvar slot-retry-count:a
fastboot getvar slot-unbootable:a
fastboot getvar slot-successful:a
# 恢复
fastboot set_active a
fastboot reboot
补完计数:
nabu-boot-count reset # 计数清零,重新开始算 7 次
1.4 内核怎么选
|
|
6.14.11-nabu-tmm | 6.17.0-UFS-slowdown+ |
|---|---|---|
| 充电栈 | ln8000 电荷泵 | qcom-smbx-charger(SMB5) |
| 充电 | 9V/3A PD PPS,快(−3.8A) | 5V DCP,慢(输入约 595mA) |
| 电池电流符号 | 负 = 充电 | 正 = 充电(相反) |
| UFS 时钟 | 300MHz | 201.5MHz(为三星 UFS 降频) |
| 手写笔无线充 |
✓ 需自编 idtp9418 模块(已装,见 2.4)
|
✓ 自带驱动(笔尖朝左吸附) |
| 已知问题 |
v_offset 可能被写坏
|
充电慢,status/电流字段不可信
|
- 日常推荐 6.14.11(快充可用),充电头用 18W
- 要给手写笔充电只能切 6.17,代价是 UFS 限速、只能用 5V 慢充(见 2.4)
- rEFInd 菜单只有 5 秒,注意别选错
1.5 已装工具与数据文件
| 工具 | 类型 | 作用 | 开关 |
|---|---|---|---|
nabu-charge-guard
|
用户服务 | 5 分钟滑窗净放电 >250mA 时弹窗告警(按内核自动处理电流符号) |
systemctl --user disable --now nabu-charge-guard
|
nabu-charge-log
|
用户服务 | 1Hz 记录充电状态到 CSV |
systemctl --user disable --now nabu-charge-log
|
nabu-boot-count
|
用户 oneshot | 启动计数(按 boot_id 去重,注销不会重复计数),到 4 弹窗提醒 |
systemctl --user disable --now nabu-boot-count
|
fix-charger.sh
|
手动脚本 |
只读诊断(打印 v_offset、电池状态)
|
sudo work/fix-charger.sh
|
数据位置:
-
~/.local/share/nabu-charge.csv— 1Hz 充电日志(10MB 自动轮转) -
~/.local/share/nabu/boot-count— 启动计数 -
~/.local/share/nabu/part-hashes-before.txt— 分区哈希基线(考古用,可删)
第 2 部分 · 充电
2.1 v_offset 坏掉是主要故障
| 指标 | 正常 | 坏掉 |
|---|---|---|
v_offset(dmesg)
|
约 150 ~ 420(随输入电压浮动) |
10 或 4294xxx(负数溢出)
|
adc_vin
|
8200 ~ 8300 | 5000 ~ 7700 |
adc_iin
|
2400 ~ 2700 | 39(几乎不抽电流) |
| 电池电流 | −3.8A(充电) | +1.2 ~ +2A(放电) |
坏掉后 dmesg 会刷:FAULT_INT / volt_fault_detected (volt_qual=0) / vac_ov_control_work / stop charging ↔ start charging 循环。
原因:v_offset 是 ln8000 驱动内部算的 ADC 偏置。输入电压瞬时跌落时
(大功率 PD 头在 PPS 下调压、电荷泵换工作点),驱动会算出负值,后续所有电压换算全错
→ 误判"输入电压不合格" → 反复停充。坏值不会自己恢复,只在驱动初始化时重算。
恢复:拔掉充电器重新插上(驱动重新初始化,v_offset 回到 ~390)。
# 判断是否在坏状态
journalctl -k -n 20 | grep -o 'v_offset=[0-9]*' | tail -1
sudo dmesg | grep adc_vin | tail -2
⚠️ 绝对不要 unbind/rebind ln8000 驱动 实测
echo 1-0051 > /sys/bus/i2c/drivers/ln8000_charger/unbind触发内核 oops:free_irq→power_supply_unregister→kernfs_remove_by_name_ns三个 WARNING, 接着 NULL pointer dereference,整机死机,只能长按电源重启。
2.2 充电头与线:低电量才是风险点
实测规律(2026-11-02 当天):
| 电量 | 充电头 | 结果 |
|---|---|---|
| 16 ~ 25% | 33W |
反复 fault、v_offset 被写坏、净放电
|
| 45 ~ 56% | 18W | 稳定 −3.8A,0 故障 |
| 56 → 99% | 100W | 连续 45 分钟 0 故障、0 掉线,电流从 −5.1A 平滑回落到 −2.1A |
-
低电量时充电电流最大(实测冲到 −5.1A),电荷泵输入电流大、VBUS 更容易跌落,
一跌就是 PD hard reset →
v_offset被写坏 → fault 循环 - 电量上来后电流自然回落(恒流转恒压),链路就稳了
- 建议:低于 ~30% 用 18W 或 5V 头;充到 40% 以上再换大功率头,100W 也能长时间稳定
- 18W 也不是绝对安全(出现过 5V 合约下同样写坏的情况),核心仍是"坏了就拔插"
补充(11/03–11/04 实测):
-
拔线/插线的瞬间也会出现一次
v_offset溢出(adc_vin同时掉到 5~7V), 这是正常瞬变:重新插入时驱动会重新初始化,不影响充电 - 真正危险的是插着电、充电过程中被写坏:此时会一直卡在 fault 循环,必须拔插一次
- 26 小时里没有重启,A/B 计数没被消耗
-
低电量(6~8%)那次的现场:
tcpm电压 0V、电池 +1.5A 放电,重插后立刻恢复 −3.6A -
开机前/休眠中线已插着 → 只协商到 5V(11/04 12:36 复现两次):
tcpm 只在"重新接入"时做 PD 协商。线如果先插着再开机(或休眠中插电),
它直接以 Type-C 5V 初始化,之后不会自己重试 PD,重启也无效 (因为开机时线还插着,又按 5V 初始化一遍)。
实测:休眠插电 → 5V;
v_offset被写坏、不充;重启后仍是 5V; 真正拔插一次 → 立刻 9V / −4.8A 快充,v_offset回到 434 -
判断有没有 PD:
cat /sys/class/power_supply/tcpm-source-psy-*/usb_type-
C [PD] PD_PPS= 有 PD(9V) -
[C] PD PD_PPS= 只有 Type-C 5V,没 PD ← 需要拔插
-
-
5V 下不是完全不能充:PMIC 旁路能给约 0.6A(
ln8000 present=0),慢但不断电 -
软件侧改
input_current_limit无效,已试过;port_type切换也推不动 typec
2.3 怎么判断现在稳不稳
# 最近 1 分钟有没有 fault(0 = 稳)
journalctl -k --since '1 minute ago' | grep -c 'FAULT_INT has occurred'
# 20 秒内 Type-C 掉线次数(0~1 = 稳)
timeout 20 udevadm monitor --subsystem-match=typec 2>/dev/null | grep -c port0-partner
# v_offset 是否在正常区间
journalctl -k -n 20 | grep -o 'v_offset=[0-9]*' | tail -1
2.4 手写笔无线充电(IDT P9418,2026-11-04 打通)
一句话:笔能无线充。6.17 自带驱动;6.14.11 原来没有,2026-11-04 已自编模块补上(见本节末尾更新)。
| 项目 | 值 |
|---|---|
| 充电芯片 |
IDT P9418(无线充电发射端),i2c-3 / 0x3b,设备树节点 idtp9418@3b
|
| 驱动 |
idtp9418(drivers/power/supply/idtp9418.c,Nicola Guerrera,2025-10-23 进 sm8150-mainline)
|
| 电源节点 |
/sys/class/power_supply/idtp9418(type=Wireless,online / capacity)
|
| 自动加载 |
/etc/modules-load.d/charge-stylus.conf → idtp9418(本次新建)
|
| 充电上限 |
charge_control_end_threshold,默认 85%(echo 100 > ... 可改)
|
| 引脚 |
霍尔 gpio101/gpio134,使能 gpio3,boost gpio47,IRQ gpio95,WPC-DET gpio94(TLMM)
|
| 诊断属性 |
/sys/bus/i2c/devices/3-003b/{chip_version,chip_status,chip_powered}
|
用法
-
用 6.17 内核启动(rEFInd 选
6.17.0-UFS-slowdown+) - 笔笔尖朝左吸到平板顶部短边(方向反了不充)
- 看状态:
cat /sys/class/power_supply/idtp9418/{online,capacity}
sudo dmesg | grep idtp | tail -5
# 正常应看到:
# [idtp] Pen attached! Turning on reverse charging...
# reverse charging start success
# [charge-monitor] pen=1 soc=5
桌面「电池实时电压」小组件已加上手写笔一行(~/.local/bin/watch_battery.sh): ● 充电中 12% 上限 85% / ○ 未充电 / 不可用(本内核无驱动)。
实测(11/04 19:52):笔放到完全没电(0%),吸上后 0% → 5% → 10%(约 5%/分钟)。
已知坑
-
SoC 读失败会永久停充:
charge-monitor读到soc=-1(寄存器0x3a返回0xFF)就打印stopping reverse charging并停掉;之后只有把笔取下再吸一次才会重新开始。 重新modprobe/probe 都不会自动续充(probe 里没有启动充电的路径)。 -
停充后
online=0、capacity=0、chip_powered=0,但霍尔仍显示"已吸附",别被骗。 - 电量为 0 的笔刚吸上时最容易触发;先充到 5~10% 再重新吸附基本就稳了。
-
2026-11-04 20:37 新现象(待验证):蓝牙配对之后,每次吸附都是
reverse charging start success→ 连续soc is default 0xFF→ 约 6 秒内停充; 配对前同样的笔从 0% 一路充到 90% 完全正常。怀疑是蓝牙连接状态下笔不响应无线充握手 (或笔发热保护)。验证方法:断开蓝牙再吸附,看是否恢复;如果恢复,就说明是 BLE 干扰。 -
充电时笔的触控输入会被 NVT 驱动主动关掉(
pen_charge_state_notifier),画不了是正常的。 - 反向充电从平板电池取电(约 5W),长时间吸附会掉电。
为什么只有 6.17 有 / 能不能移植到 6.14.11
-
时间线:
6.14.11编于 2025-08-14;驱动首个提交在 2025-10-23(sm8150-mainline/linux863ef95e);6.17编于 2026-04-11。所以 6.14.11 不是"没开配置",是根本没有这份代码。 -
设备树绑定也换了:6.14 的 dtb 用小米 BSP 属性名
(
idt,irq/hall,int3/hall,int4/idt,reverse-enable/idt,reverse-booset-enable), 驱动读的是新绑定(irq-gpios/pen-det-active-high-gpios/pen-det-active-low-gpios/enable-gpios/boost-enable-gpios)。直接拷.ko也 probe 不起来。 -
移植可行,但要动内核:
-
需要 6.14.11 的源码树(本机没有,PPA 只发二进制;
/lib/modules/6.14.11-nabu-tmm/build指向作者机器/home/themojoman/develop/nabu/linux,已失效); - 驱动 794 行,只用 gpiod / regmap / power_supply,6.14 的 API 够用;
-
用同 config(
/proc/config.gz)编模块,vermagic要对上6.14.11-nabu-tmm; 本机CONFIG_MODVERSIONS=n、CONFIG_MODULE_SIG=n,所以编出来能直接加载; -
最省事:改驱动读旧属性名(不动 dtb/UKI);否则要改 dtb 再用
ukify重打 UKI; - 真正的风险在电源栈:6.14 是 ln8000 电荷泵 + PD 9V,6.17 是 smb5 5V, p9418 的反向升压共享 VBUS,没在 6.14 上验证过。
-
需要 6.14.11 的源码树(本机没有,PPA 只发二进制;
- 取舍:留 6.17 → 能充笔,但只能 5V 慢充、UFS 限速;回 6.14.11 → 快充,但笔不能充。
2026-11-04 21:20 更新:6.14.11 也能充笔了(自编模块)
原来的结论“只有 6.17 有驱动”已过时——给 6.14.11 编了一个 out-of-tree 的 idtp9418 模块,
装上后 6.14.11 也能正常给笔无线充电,平板仍是 9V/3A 快充,两不耽误。
-
模块位置:
/lib/modules/6.14.11-nabu-tmm/extra/idtp9418.ko,开机由/etc/modules-load.d/charge-stylus.conf自动加载 -
源码:
~/nabu-build/{idtp9418.c,idtp9418.h},改了两处:-
DT 属性名换成 6.14 dtb 里的旧名字(
idt,irq/hall,int3/hall,int4/idt,reverse-enable/idt,reverse-booset-enable) -
GPIO 用 legacy API(
of_get_named_gpio+devm_gpio_request_one+gpio_to_desc), 因为 6.14 没有devm_gpiod_get_from_of_node
-
DT 属性名换成 6.14 dtb 里的旧名字(
-
编译:
linux-6.14.11源码树 + 本机/proc/config.gz,vermagic=6.14.11-nabu-tmm与原内核一致;KBUILD_MODPOST_WARN=1跳过缺失的Module.symvers(MODVERSIONS=n,加载时不校验) -
实测:笔 0% 吸上后稳定充电(
vout 6.75V / iin 128mA ≈ 0.85W / 52~54°C),0%→5%→10%; 蓝牙同时连着也没再出现0xFF停充 - 结论:6.14.11 现在“快充 + 笔充电”两全,不用再为笔切 6.17
2.5 手写笔蓝牙(2026-11-04 配对成功)
为什么 MIUI 吸上就自动连,Linux 不会:MIUI 有小米笔系统服务,霍尔检测到吸附就主动唤醒笔并按私有流程配对; Linux/BlueZ 没有这个服务,笔平时不广播(省电),必须长按笔上的按键让它进入配对模式。
配对步骤
-
bluetoothctl里scan on(或设置 → 蓝牙里保持扫描) - 长按笔上的按键约 5 秒
-
出现
Xiaomi Smart Pen后pair→trust→connect
bluetoothctl --timeout 120 scan on # 长按笔按键期间扫描
bluetoothctl devices | grep -i pen
bluetoothctl pair C0:2B:3D:70:C6:E9
bluetoothctl trust C0:2B:3D:70:C6:E9
bluetoothctl connect C0:2B:3D:70:C6:E9
笔的蓝牙地址是随机地址(
random),重新配对可能变,脚本里别硬编码 MAC,用名字匹配。
能读到的信息(GATT)
| 服务/特征 | 内容 | 实测值 |
|---|---|---|
| 0x180F Battery / 0x2A19 | 电量 | 80% |
| 0x180A / 0x2A29 | 厂商 |
Xiaomi
|
| 0x180A / 0x2A24 | 型号 |
wPI07U81PC
|
| 0x180A / 0x2A26 | 固件版本 |
00.00.0703
|
| 0x180A / 0x2A28 | 软件版本 |
00.00.0703
|
| 0x180A / 0x2A50 | PnP ID |
vendor 0x1915(Nordic)/ product 0xEAEA / ver 0x0001
|
| 0x1812 HID | Report Map | Report 0x10:4 个按键位;0x21:1 字节 feature |
| 0xFE59 | Nordic Secure DFU | 固件升级服务,别乱刷 |
- 没有序列号(0x2A25)和硬件版本(0x2A27);压感/倾角不走蓝牙(走 NVT 触控)
-
GATT 读取要 root(BlueZ D-Bus 策略):
sudo busctl --system call org.bluez <path> org.bluez.GattCharacteristic1 ReadValue 'a{sv}' 0 -
电量普通用户可读:
upower -e | grep bluetooth,再upower -i <path>
两种电量的区别
| 来源 | 什么时候有 | 可信度 |
|---|---|---|
WPC(/sys/class/power_supply/idtp9418/capacity)
|
只在无线充电时;停充后读回 0 | 粗(笔通过无线协议报,会 80→90 跳变) |
BLE(UPower 的 bluetooth_generic_dev_*)
|
只要蓝牙连着 | 细(笔自己的电量计) |
现在面板/小组件的显示逻辑
-
充电中:
笔XX%(绿,WPC 实时)+ 功率/温度 -
未充电但蓝牙连着:
笔XX%(灰,BLE,顶栏 3 分钟刷新一次) -
都没有:不显示(原来的
笔—容易被看成“笔一”,已去掉)
第 3 部分 · Waydroid 与安卓应用
3.1 最终结论速查
| 问题 | 根因 | 修复 |
|---|---|---|
| Android 无声音 |
ro.hardware=unknown,Android 加载了空壳 audio.primary.default.so
|
waydroid.cfg 的 [properties] 里加 ro.hardware.audio.primary = waydroid
|
| Android 无声音(第二层) |
HAL 启动环境缺 PULSE_RUNTIME_PATH(upstream #2333)
|
overlay 里 init.environ.rc 加全局 export PULSE_RUNTIME_PATH /run/xdg/pulse
|
| 声音断续 / 只有宿主也在放时才响几秒 |
HAL 的 pulse 客户端缓冲不足,ALSA 持续 XRUN(缓冲欠载),设备拿不到连续数据
|
init.environ.rc 再加 export PULSE_LATENCY_MSEC 60(不够可加到 120)
|
| 容器起来 ~40 秒后自己死 |
未重建 LXC 配置就 systemctl restart
|
必须先 rm -rf /var/lib/waydroid/lxc && waydroid init 再启动
|
Session start 报 DBus NoReply
|
用 sudo 启动了 session
|
session 必须以普通用户身份启动 |
| Android 启动到「手机正在启动」后无限重启 |
/data 属主被 chown -R 改坏,keystore2 打不开数据库
|
只能重置 /data(见「事故复盘」)
|
3.2 当前生效的配置(持久化)
1. /var/lib/waydroid/waydroid.cfg
[properties]
ro.hardware.audio.primary = waydroid
作用:让 Android 加载 /vendor/lib/hw/audio.primary.waydroid.so,而不是 default 空壳。
2. /var/lib/waydroid/overlay_rw/system/init.environ.rc
内容是在原文件基础上,在 on early-init 块里插入两行:
on early-init
export PULSE_RUNTIME_PATH /run/xdg/pulse
export PULSE_LATENCY_MSEC 60
export ANDROID_BOOTLOGO 1
...
作用:
-
PULSE_RUNTIME_PATH—— 让所有 Android 服务(含 audio HAL)继承 pulse socket 路径 -
PULSE_LATENCY_MSEC—— 给 libpulse 客户端设缓冲延迟。不设这一行,HAL 会持续欠载(XRUN),表现为声音断续或干脆没声,且症状很迷惑:宿主自己也播放时,设备被"带"起来,Android 的声音会顺带响几秒
⚠️ 注意:这两个补丁放在 overlay 里。如果执行
waydroid init -f且走到 preinstalled images 分支,overlay 会被清空,补丁需要重新加。
3.3 验证音频是否正常(几条命令)
# 1. HAL 是否加载正确
adb shell getprop ro.hardware.audio.primary # 应输出 waydroid
PID=$(adb shell pidof android.hardware.audio.service | tr -d '\r')
sudo waydroid shell -- sh -c "grep -oE '[^ ]*audio\.primary[^ ]*\.so' /proc/$PID/maps"
# 应输出 /vendor/lib/hw/audio.primary.waydroid.so
# 2. HAL 环境里有没有 pulse 路径和延迟参数
sudo waydroid shell -- sh -c "tr '\0' '\n' < /proc/$PID/environ | grep PULSE"
# 应输出:
# PULSE_RUNTIME_PATH=/run/xdg/pulse
# PULSE_LATENCY_MSEC=60
# 3. 播放时看宿主 PipeWire 有没有 Waydroid 的音源
wpctl status | sed -n '/Streams:/,/Video/p'
# 应看到 "Waydroid" + output_FL/FR -> Speaker playback [active]
# 4. 想确认送出去的不是静音,可以录 sink 监听
pw-record --target=61 /tmp/mon.wav # 61 是 sink 节点 id,用 wpctl status 查
# 静音时峰值 0;有声时峰值明显大于 0
# 5. 【关键】播放时检查 ALSA 是否在欠载
cat /proc/asound/card1/pcm0p/sub0/status | head -1
# 正常:持续 state: RUNNING
# 异常:SETUP / XRUN 来回跳 —— 这就是断续无声的元凶,加 PULSE_LATENCY_MSEC 解决
3.4 正确的启停方式(重要)
启动(推荐)
# 普通用户身份,不要 sudo
export XDG_RUNTIME_DIR=/run/user/1000
export WAYLAND_DISPLAY=wayland-0
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
sudo systemctl start waydroid-container
waydroid session start &
waydroid show-full-ui
或者直接点桌面「Waydroid 安卓」图标(启动脚本会处理这些)。
停止
waydroid session stop # 会连容器一起停
⚠️ 如果容器「起来了但几十秒后自己死」
不要用 systemctl restart,走完整重建:
waydroid session stop
sudo systemctl stop waydroid-container
sudo rm -rf /var/lib/waydroid/lxc
sudo waydroid init # 重新生成 LXC 配置,不重新下镜像
sudo systemctl start waydroid-container
waydroid session start & # 普通用户
这条是今晚反复踩坑总结出来的:单纯 restart 必挂,重建 lxc 目录后能稳定启动。
3.5 事故复盘(别重蹈覆辙)
事故 1:chown -R 毁掉 /data(最严重)
为了「修复权限」,对 ~/.local/share/waydroid/data 执行了 chown -R neko:neko。
后果:
-
Android 的
system用户恰好也是 uid 1000,所以/data/system等文件没事 -
但 keystore 是 uid 1017,
/sdcard是 1023,各 app 是u10xxx—— 全被改成 1000 -
keystore2 打不开
persistent.sqlite→ abort → system_server 崩 → init 重启 → 无限循环 - 现象就是「能看到手机正在启动,几秒后重启」
教训:永远不要对整个 /data 做递归 chown。 需要修单个目录时,先确认它应有的 uid:
| 目录 | 应有 uid |
|---|---|
/data/system、/data/misc/*(多数)
|
1000(system) |
/data/misc/keystore
|
1017 |
/data/media、/sdcard
|
1023(media_rw) |
/data/misc/adb
|
0(root) |
事故 2:改 init.waydroid.rc 导致容器起不来
尝试在 /system/etc/init/init.waydroid.rc 的 service vendor.audio-hal 块里加 setenv PULSE_RUNTIME_PATH ...,结果容器直接起不来(LXC 运行 40 秒后退出)。
正确做法是改 init.environ.rc 的全局 export(本记录 3.2)。
事故 3:lxc.environment 不生效
在 config_base 里加 lxc.environment = PULSE_RUNTIME_PATH=... 后容器能启动,但 Android init 不会把 LXC 的环境传给它的服务,HAL 的 /proc/<pid>/environ 里依然没有。
事故 4:用 sudo 启动 session
sudo waydroid session start 会让 DBus 调用超时(NoReply),表现为「管理器根本没反应」,极容易误判成 Waydroid 坏了。
3.6 网络相关
机器访问 SourceForge / GitHub Release 极慢(实测 SourceForge 单流 325 B/s,8 线程合计 3 KB/s)。
用 Clash 代理(FlClash GUI,混合端口 7890)后正常:
# 首次初始化镜像
sudo env https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 \
no_proxy=ota.waydro.id waydroid init -f
注意:
-
no_proxy=ota.waydro.id让元数据走直连(有时节点反而连不上 GitHub Pages) - 走代理实测 6–8 MB/s,912 MB 系统镜像几分钟能下完
-
如果没有代理,可以用
/etc/waydroid-extra/images预置system.img+vendor.img跳过下载
3.7 adb / 文件传输
sudo waydroid adb # 开启容器内 adb
adb connect 192.168.240.112:5555
adb devices # 应显示 device
如果显示 unauthorized,把宿主公钥灌进去:
D=~/.local/share/waydroid/data
sudo mkdir -p $D/misc/adb
sudo cp ~/.android/adbkey.pub $D/misc/adb/adb_keys
sudo waydroid shell -- setprop ctl.restart adbd
adb disconnect 192.168.240.112:5555; adb connect 192.168.240.112:5555
日常传文件用桌面「安卓文件传输」工具(GTK4 写的,支持拖拽、双向、目录浏览),或:
adb push 文件 /sdcard/Download/
adb pull /sdcard/DCIM/xxx.jpg ~/图片/
3.8 关键路径清单
| 路径 | 说明 |
|---|---|
/var/lib/waydroid/images/
|
system.img(1.9G)+ vendor.img(403M),只读挂载 |
/var/lib/waydroid/overlay_rw/
|
系统分区写入层(我们的 init.environ.rc 补丁在这) |
/var/lib/waydroid/waydroid.cfg
|
主配置(含 [properties] 自定义属性)
|
/var/lib/waydroid/waydroid.prop
|
每次启动生成的属性文件 |
/var/lib/waydroid/lxc/waydroid/
|
LXC 配置(config、config_session、config_nodes)
|
~/.local/share/waydroid/data/
|
Android /data(用户数据、已装 app)
|
/usr/lib/waydroid/data/configs/config_base
|
LXC 配置模板 |
/usr/lib/waydroid/data/scripts/waydroid-net.sh
|
网桥脚本(start/stop)
|
3.9 常见现象对照表
| 现象 | 大概率原因 | 处理 |
|---|---|---|
Session: RUNNING, Container: STOPPED
|
容器启动失败 | 走「完整重建」流程 |
| 起来 40 秒后自己死 | 未重建 lxc 配置 | 同上 |
NoReply / 管理器没反应
|
用 sudo 起 session,或管理器卡死 |
用普通用户;必要时 systemctl restart
|
| 开机画面后无限重启 |
/data 属主损坏(keystore)
|
只能重置 /data
|
| 无声音 | HAL 加载错 / 缺 PULSE 环境 | 见 3.2 两处补丁 |
| 声音断续,或只有别的程序放声音时才响几秒 |
ALSA XRUN 欠载
|
init.environ.rc 加 PULSE_LATENCY_MSEC 60,不够改 120
|
| 声音突然变成"机器人/炸麦",过一阵自己恢复 | 音频图量子被压到 512,与 Waydroid 720 帧周期不对齐 |
force-quantum/min-quantum 钉 1024,见 4.1
|
| 界面卡顿时有电流声/爆音(跟 Android 无关) | PipeWire 无实时优先级 + 缓冲偏小 | 见 4.1 |
| 开杜比音效后,音量 50% 以上外放"炸" | 20 段 EQ 级联比曲线深 5 dB,补偿增益把功放推进保护 | 见 4.2:9 段拟合 + 零补偿 + −1 dBFS 保险 |
| 开杜比后同音量明显变轻 | 校正本身是减法,净衰减约 7 dB | 正常,靠音量旋钮补,见 4.2 |
adb unauthorized
|
容器 /data 里没有宿主公钥
|
见 3.7 |
| 首次 init 卡在下载 | SourceForge 被限速 | 挂代理,或预置镜像 |
| 桌面图标点了没反应 | 容器没起 / session 没起 |
先手动 waydroid session start 看报错
|
3.10 一次性恢复脚本(保留数据)
#!/bin/bash
# 保留 /data 和镜像,只重建运行环境
export XDG_RUNTIME_DIR=/run/user/1000
export WAYLAND_DISPLAY=wayland-0
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
waydroid session stop 2>/dev/null
sudo systemctl stop waydroid-container
sleep 2
sudo pkill -f /usr/bin/waydroid
for m in $(mount | grep waydroid | awk '{print $3}' | sort -r); do sudo umount -l "$m"; done
sudo rm -rf /run/waydroid-lxc /var/lib/waydroid/lxc
sudo waydroid init
sudo systemctl start waydroid-container
sleep 6
waydroid session start &
sleep 40
waydroid status
3.11 游戏闪退排查:碧蓝航线(2026-11-03,进行中)
现象
点「碧蓝航线」图标 → 图标闪一下 → 黑屏 → 回桌面。启动后 1~5 秒必退, Unity 会话计数一度累积到 11 次,全部失败。
结论(一句话)
不缺资源、不缺内存、不是网络、也不是安装包损坏。 是 app 自己的加密脚本包没被解密/加载出来,Unity 只能拿 APK 里那套"壳"元数据跑空场景,
脚本全部解析不到,于是进程主动 exit(1) 自杀。
| 环节 | 证据 |
|---|---|
| 系统侧判定 |
dumpsys activity exit-info 全部 reason=1 (EXIT_SELF) status=1,无 ANR、无 LMK
|
| 崩点 |
Unity 载 level0 时 referenced script (...BLHX.Rendering...) is missing! + different serialization layout (Read 32 bytes but expected 56),随后 exited cleanly (1)
|
| 代码不在 il2cpp |
global-metadata.dat 里 BLHX 只有 266 处,且都是 BLHX.HotUpdate / BLHX.Rendering.HotUpdate 这类基础设施;ScriptingAssemblies.json 里挂着 Yongshi.BLHotUpdate.Runtime.dll
|
| 代码在加密包里 |
assets/AssetBundles/scripts64.ys(38 MB)头部是 52 AA 2A A5,不是标准 UnityFS;同目录 hotupdate.ys / custom_builtin.ys 才是正常 UnityFS
|
| 内容不缺 |
assets/AssetBundles/ 共 37536 个文件、未压缩 1644 MB,全在 APK 里;Download/ 为空是"没走到下载",不是"缺下载"
|
| APK 完整 |
libil2cpp.so / libcrypto.1.1.so / global-metadata.dat 的 CRC 与 APK 内原件逐个比对完全一致;splits=[base]
|
| 网络 | 抓了启动全过程容器对外连接:只有 adb,没有任何 CDN 请求——它压根没尝试下载 |
| 内存 | 11.6 GB 总量 / 6.3 GB 可用 |
⚠️ 一个必须记住的坑:ro.build.type 别改 user
属性伪装第一版顺手把 ro.build.type 也改成 user,重启后开机弹
「Android 系统:您的设备内部出现了问题。请与您的设备制造商了解详情」:
E/Build: Vendor interface is incompatible, error=1
E/ActivityTaskManager: Build fingerprint is not consistent, warning user
原因:AOSP 的 Build.isBuildConsistent() 在 IS_ENG || IS_USERDEBUG 时直接返回 true(短路)。
改成 user 后不再短路,而这张镜像的 VINTF HAL 矩阵本来就不匹配, VintfObject.verifyWithoutAvb() 返回 1 → 必然判不一致 → 弹窗。
结论:ro.build.type 必须保持 userdebug。 已撤回,弹窗消失。其余伪装可留。
当前生效的伪装(已验证系统无异常)
| 文件 | 作用 |
|---|---|
/var/lib/waydroid/waydroid.cfg → [properties]
|
持久化(下次 waydroid init 也会带上)
|
/var/lib/waydroid/waydroid_base.prop
|
立即生效(session start 只读这个,不回读 cfg)
|
两边必须同步写——只改 cfg 不生效;跑 waydroid init 又会重写 LXC 配置(3.4 那条"必挂"路径)。
[properties]
ro.build.fingerprint = Xiaomi/nabu/nabu:13/TKQ1.220829.002/V14.0.5.0.TKXCNXM:user/release-keys
ro.build.tags = release-keys
ro.product.waydroid.brand = Xiaomi
ro.product.waydroid.manufacturer = Xiaomi
ro.product.waydroid.device = nabu
ro.product.waydroid.name = nabu
ro.serialno = 8H2X9K3LM
实测变化:brand waydroid→Xiaomi、manufacturer Waydroid→Xiaomi、 device/name →nabu、fingerprint 112 字符→71 字符、tags test-keys→release-keys。
顺带修掉一个真问题:原来
ro.build.fingerprint有 112 字符 >PROP_VALUE_MAX(92), native 层__system_property_get("ro.build.fingerprint")一律拿到空串 (日志里E libc: The property ... is too large for __system_property_get())。 现在是 71 字符,native 也能正常读到。
备份:/var/lib/waydroid/waydroid.{cfg,base.prop,prop}.bak-20261103-192221
回滚:
sudo cp -a /var/lib/waydroid/waydroid.cfg.bak-20261103-192221 /var/lib/waydroid/waydroid.cfg
sudo cp -a /var/lib/waydroid/waydroid_base.prop.bak-20261103-192221 /var/lib/waydroid/waydroid_base.prop
waydroid session stop; sudo systemctl start waydroid-container; waydroid session start &
改完属性后:症状变了,但没修好
-
BgcPluginManager ... onlyDownloadAndInstall()的NullPointerException不再出现 -
游戏仍在同一处
exit(1),Download/依旧为空 -
对照(19:40 更正,重要):同机
com.bilibili.priconne(公主连结)能正常进主界面(实测 pid 在跑,topResumedActivity=com.bilibili.priconne/.MainActivity),而且它挂的是同一套反外挂 (libthemis.so/libcustom_rules.so/libbili.so/libmsaoaid*.so), B站 SDK 采到的设备档案同样是is_emu=001000、is_debug=1。 它 exit-info 里的EXIT_SELF全部来自:GP7Service/:GP7Worker子进程且status=0(正常退出), 和碧蓝航线"主进程status=1自杀"完全不是一回事。 → 模拟器识别、反外挂拦截、环境本身,都不是本次闪退的原因。
→ 属性伪装既不是必要条件也不是充分条件:公主连结在同样的 is_emu=001000 + 同一套反外挂下能正常跑,
说明这些特征本身不致命。改属性没修好游戏,现在保留它只是为了顺手修掉
"fingerprint 112 字符导致 native 读不到"这个真实缺陷,随时可按上面的命令回滚。
要继续往下查可选的路
- 换 Android 11(LineageOS 18.1)镜像复测 —— 重装级操作,流程见 3.4 / 3.10;
- 用 debuggable 重打包 APK 附加调试 —— 会毁签名、有封号风险,不建议;
- 放弃在 waydroid 里跑这个游戏,用真机或官方 PC 端。
19:44 两处更正 + 最新进展
更正 1:公主连结的下载不在 /sdcard。 它把资源下到了内部目录,共 11 GB:
/data/data/com.bilibili.priconne/files/ a/ 3.6G v/ 2.9G m/ 1.6G s/ 1.5G b/ 1.4G manifest 55M
之前只看 /sdcard/Android/data/.../files/Download(4 KB)就断定"它没下载也能跑",结论作废。
查这类游戏的资源体积要直接 du -sh /data/data/<pkg>,别只看外部目录。
更正 2:碧蓝航线并不是"点开必闪退"。 做完整清理(pm clear + sudo rm -rf /sdcard/.../Android/data/com.bilibili.azurlane)后,
它能正常弹出用户协议页并存活数十秒,pss=315MB / rss=501MB(Unity 确实起来了):
19:42:42 am_proc_start 29769 (首次启动,弹出协议页)
19:43:22 29769 EXIT_SELF status=1 (未点"同意",exit)
19:43:30 am_proc_start 30990 (被重新拉起,再次弹协议页)
19:43:34 30990 EXIT_SELF status=1
19:43:37 31315 (仅 XprefProvider content provider)
→ 之前"秒退"里有一部分是外部残留目录(/sdcard/.../files/il2cpp 还在)让启动器误判状态造成的。
清理干净后偶尔能走到协议页,但不稳定。
19:47 实测:协议框抢不到。 清数据后连续 3 次尝试"启动 + 连点同意"(坐标 1314,990,
从截图量出来的按钮中心),三次都没点中:进程在 4~10 秒内就 exit(1),截图取回来时已回到桌面。
协议框只在 19:42 那一次实例里稳定出现过(活了约 40 秒),之后的启动因为协议已记录,
直接进 Unity → 脚本缺失 → 4 秒死。
稳定的失败循环(可复现):
启动 → Unity 载 level0 → BLHX.* 脚本全 missing + 序列化布局不匹配(32/56) → exit(1) → 被再次拉起 → ...
结论:卡点不在"同意"这一步。 签协议只是把流程往后推了一格,真正的死点在 Unity 载入壳场景,
要过这一关只能让解密脚本包 scripts64.ys 成功加载——目前没有黑盒手段能做到。
19:52 用宿主 gdb / strace 取证(不用改 APK)
关键结论:exit(1) 是 Java 层从网络回调线程里调用的。
容器里的进程在宿主上可见(ps -eo pid,args | awk '$2=="com.bilibili.azurlane"'),
有 sudo 就能直接 ptrace,完全不需要 debuggable 重打包:
# 1) 找到宿主侧 PID
PID=$(ps -eo pid,args | awk '$2=="com.bilibili.azurlane"{print $1; exit}')
# 2) 在 exit_group 上断点,抓退出时的调用栈
sudo gdb -p $PID -batch -ex "catch syscall exit_group" -ex "continue" -ex "info threads" -ex "bt 25"
# 3) 看它打开了哪些文件(找缺失的资源)
sudo strace -f -p $PID -e trace=openat,openat2 -o /tmp/az.trace
抓到的东西:
| 观察 | 说明 |
|---|---|
exit_group 的调用线程是 iligame.com/...,栈在 libart.so 的 nterp_helper 里
|
是 Java 网络回调调用的 System.exit(1),不是本地 native 崩溃
|
openat(".../files/il2cpp/il2cpp.usym") = ENOENT、/system/bin/il2cpp.usym = ENOENT
|
游戏在找 il2cpp.usym,APK 里没有这个文件(只有 3 个 libil2cpp.so),只能由热更下发
|
openat(".../files/il2cpp/Metadata/global-metadata.dat") 成功
|
元数据确实是从外部目录加载的 |
newfstatat(".../base.apk/assets/bin/Data/level0") = ENOTDIR
|
Unity 直接按 APK 内路径找壳场景(level0 只有 58 KB)
|
抓包(tcpdump -i waydroid0)显示启动期间访问的全是 443/TLS,
SNI 里有 p.biligame.com(补丁服务器)、api.biligame.net、line1-sdk-app-api.biligame.net、 game.bilibili.com、s1.hdslb.com。没有任何资源下载流量,但补丁接口是访问过的。
→ 合并起来看:大概率是补丁/SDK 接口的某个响应让客户端决定退出(版本、风控、实名或维护), 而不是本地解密失败。要看到响应内容必须 MITM(装 CA,且很可能撞证书固定)。
20:10 突破:协议页 WebView 渲染线程崩溃,用命令行开关修好了
之前"协议框只闪一下就消失"的真相不是 UI 抢不到,是 WebView 渲染线程把自己崩了:
pid 2696, tid 2831, name: RenderThread >>> com.bilibili.azurlane
signal 0 (SIGSEGV), code 1 (SEGV_MAPERR), Cause: null pointer dereference
#00 libwebviewchromium.so (webview.apk!libwebviewchromium.so)
对应更早的 E libEGL: eglMakeCurrentImpl error 3008 (EGL_BAD_DISPLAY) 和 E/OpenGLRenderer: Device claims wide gamut support, cannot find matching config。
WebView 在走 GPU 合成时崩在 Mesa/gbm 上。
修复:给 WebView 一个命令行参数文件(全局生效,所有用到 WebView 的 app 都受益)
adb shell "echo '--disable-gpu-compositing' > /data/local/tmp/webview-command-line"
-
只写
--disable-gpu-compositing:协议页正常渲染、进程不崩(实测截图正常) -
写成
--disable-gpu --disable-gpu-compositing:不崩,但页面渲染成白块——不要用这个 -
回滚:
adb shell rm /data/local/tmp/webview-command-line
修好之后,协议流程能完整跑通:
agreement.start.api → agreement.page.view → agreement.page_load.api
→ agreement.agree.click → agreement.finish.api
→ patch_req_config → patch_config_net_success → bgc_plugin
但随后仍卡在同一个老毛病(见下节): BgcPluginManager.onlyDownloadAndInstall() on a null object reference → exit(1)。
所以 WebView 只是第一道坎,资源准备那道坎还在。
20:10 期间的两次容器宕机(教训)
为了装 MITM 证书改了 overlay_rw/system/etc/security/cacerts/,需要重启容器才生效。结果:
-
普通
systemctl restart开始随机失败,症状是 zygote 崩:JNI FatalError: set_sched_policy(0, -1) failed: Operation not permitted(cgroup 只读,和 3.9 表里那条一致) -
容器崩过一次之后,残留挂载会让后续启动全部失败,必须先清干净:
systemctl stop→umount所有 waydroid 挂载 →rm -rf /run/waydroid-lxc /var/lib/waydroid/lxc→waydroid init→start -
waydroid init会rmtree掉overlay_rw(源码images.py: remove_overlay), 所以放在 overlay 里的证书每次重建都会丢——这就是 MITM 方案在这里走不通的根本原因 -
另外
pkill -f /usr/bin/waydroid会匹配到自己的命令行,把自己杀掉,别这么写
结论:这个环境不适合做需要重启容器的实验。做之前先想清楚怎么恢复。
20:12 试过但无效:把缓存配置里的 bgc_plugin 关掉
思路:files/cdn_s.0 里 sdk_switch.bgc_plugin = 1,怀疑关掉能绕过那个 null manager。
文件结构(已确认):Java 序列化头(27 字节)+ 一段 6753 字节的 JSON 串,无尾部校验和,
所以可以等长替换 "bgc_plugin": 1 → "bgc_plugin": 0,不破坏格式:
# 备份
sudo cp -a .../files/cdn_s.0 /tmp/
# 等长替换(用 python 校验 JSON 仍可解析、长度不变)
# 再 chattr +i 防止被启动时的刷新覆盖
sudo chattr +i .../files/cdn_s.0
结果:NPE 照样出现,onlyDownloadAndInstall() 依旧打在 null 上。
→ sdk_switch.bgc_plugin 不是那个 manager 为 null 的原因。
已回滚(chattr -i + 还原备份,确认 bgc_plugin=1、大小 6780 未变)。
附:这次用到/可复用的取证命令
adb connect 192.168.240.112:5555
adb logcat -b all -d -v threadtime | grep -E "CRASH|BgcPlugin|Unity|has died"
adb shell dumpsys activity exit-info com.bilibili.azurlane
# adb root 在 waydroid 不可用(会回 “not available in Waydroid”),直接走宿主文件系统
sudo ls ~/.local/share/waydroid/data/data/com.bilibili.azurlane/
sudo ls ~/.local/share/waydroid/data/data/com.bilibili.azurlane/shared_prefs/
sudo cat ~/.local/share/waydroid/data/data/com.bilibili.azurlane/shared_prefs/device_data.xml
# 看 il2cpp 元数据里到底有没有游戏类
sudo strings -a ~/.local/share/waydroid/data/media/0/Android/data/com.bilibili.azurlane/files/il2cpp/Metadata/global-metadata.dat | grep -c BLHX
完整时间线与字段级证据另存: ~/文档/deepseek-harness/default-workspace/azurlane-crash-分析报告.md
3.12 异环(UE5)黑屏:库初始化阶段就崩了(2026-11-03)
现场
-
包名
com.hottagames.yh.laohu(注意:com.hypergryph.endfield是明日方舟终末地,别搞混),版本 1.4.0 -
引擎:Unreal Engine 5(
com.epicgames.unreal.GameActivity),libUnreal.so有 241 MB -
症状:点开是「黑屏」——其实是停在 Android 12 启动闪屏(黑底 + 图标),
进程 10~20 秒后 SIGSEGV,
exit-info里是一长串reason=2 (SIGNALED) status=11
取证(宿主 gdb 抓崩溃现场)
PID=$(ps -eo pid,args | awk '$2=="com.hottagames.yh.laohu"{print $1; exit}')
sudo gdb -p $PID -batch -ex "continue" -ex "bt 18"
Thread 1 "agames.yh.laohu" received signal SIGSEGV
#0..#14 libUnreal.so
#15 __dl__ZN6soinfo17call_constructorsEv () <- 静态构造函数
#16 __dl__Z9do_dlopenPKciPK17android_dlextinfoPKv ()
#17 __dl___loader_dlopen ()
崩的那条指令(pc - 库基址 = 0x289D97C,用 info proc mappings 算的):
ldr x10, [x10, #2408] ; 取全局表
ldr x0, [x10] ; x0 = 0 <- 表项还没建
strb w10, [x9, #1192] ; 置"已初始化"标志
str x8, [x0, #16] ; 空指针写 -> SIGSEGV
结论:不是"渲染不出画面",而是 libUnreal.so 被 dlopen 时,
静态构造函数往一个还没初始化的全局注册表里挂对象,直接空指针写死了——引擎 main 根本没进。
排除项
| 怀疑 | 结论 |
|---|---|
| 安装损坏 |
否:libUnreal.so 与 APK 内原件 CRC 完全一致(251901120 / 370f2d23)
|
| 渲染/图形栈(Mesa、Vulkan) |
否:崩在 dlopen 构造函数里,与 EGL/Vulkan 无关(Vulkan ICD 也都在:freedreno / lvp / virtio)
|
| 属性伪装 / WebView 修复的副作用 | 否:这游戏 18:20 装的、19:03 就被清过数据,都早于我们的改动 |
| 缺资源 |
否(是结果不是原因):内部数据 96 K、外部 16 K;files/ano_tmp/ 里一堆 0 字节 ms_*.dat(腾讯 ANO 反外挂的内存扫描残留)
|
判断
库级初始化失败,在 waydroid 这种容器里基本无解:要么是 UE5 对这套 Android 13 + Mesa 的兼容问题, 要么是 ANO 反外挂注入让静态初始化炸了。 换 Android 11 镜像不一定能好(不是渲染问题,是加载问题),不建议为它重装。
想要跑异环只能真机 / PC 端。
官方配置要求(2026-04-23 公测,官网 FAQ)
| 项 | 最低 | 推荐 |
|---|---|---|
| Android | 10.0+ | — |
| SoC | 骁龙 855 / 天玑 8000 / 麒麟 990 | 骁龙 8 Gen2 / 8 Gen3 / 天玑 9200 / 麒麟 9000S |
| 存储 | 预留 20 GB | — |
| iOS | iPhone 12 Pro Max、iOS 15 | iPhone 14 Pro、iOS 17 |
我方设备:骁龙 860(≈855+)、Android 13、存储 483 GB → 名义达标,但只到"最低"线, 即使能启动,UE5 开放世界在这颗 855 级 SoC 上也基本跑不动。
官方对"移动端下载完打开就闪退"的建议是卸载重装并保持前台完整更新;
但本例崩在 libUnreal.so 静态构造(见上),APK 完整、无分包(1.4 GB 单包、splits=[base])、
页大小 4 KB 正常,所以重装大概率无效。
参考:雷电模拟器能跑(官方设置文档, 要求 14.0.5.1+、6 GB 内存 4 核),但那跑的是 x86 Windows 模拟器,与 waydroid 的 arm64 路径不同。
20:27 按官方建议卸载重装 —— 实测无效
sudo cp <app>/base.apk ~/yh_base.apk # 先备份 1.4 GB 安装包
adb uninstall com.hottagames.yh.laohu
adb install -g -r ~/yh_base.apk # 17 秒装回
adb shell am start -n com.hottagames.yh.laohu/com.epicgames.unreal.GameActivity
结果:一模一样——停在 Android 启动闪屏(黑底+图标)→ 十几秒后 reason=2 (SIGNALED) status=11(SIGSEGV)→ 再次拉起 → 再崩。
坐实了:不是安装/资源/配置问题,就是 libUnreal.so 在这套容器里静态初始化失败。 这条路到此为止。
第 4 部分 · 宿主音频
4.1 系统级音频杂音:卡顿时有电流声(与 Waydroid 无关)
现象
- 界面卡顿(切大页面、加载重内容)时,声音里出现明显电流声 / 噼啪声
- 退出卡顿页面、或暂停后重新播放,往往就恢复
- 跟 Android 无关,宿主播放音乐视频同样会出现
排查(先排除「四扬声器不同步」)
Xiaomi Pad 5 的 4 颗 CS35L41 功放由同一条 TDM 链路、2 声道驱动:
# /usr/share/alsa/ucm2/Xiaomi/nabu/HiFi.conf
# - Internal Speakers : CS35L41 driver : MultiMedia1 <-> QUAT_TDM_RX_0
SectionDevice."Speaker" {
PlaybackChannels 2
}
用户态只有一条 2 声道流,驱动/硬件层已经同步,不可能由软件造成四扬声器不同步。真不同步会表现为声像偏移、低频抵消发闷,而不是随负载出现的爆音。
真正原因(两层,第二层才是根因)
第一层:没有实时优先级
journalctl --user -u pipewire | grep -c 'out of buffers' # 曾达 164 次
ps -o rtprio,comm -p $(pgrep -x pipewire) # RTPRIO - ← 没有 RT
第二层(根因):音频图量子被压到 512
default.clock.quantum 只是偏好值,压不住客户端请求的低延迟。某个客户端(浏览器等)要 512 帧,整张图就掉到 512:
R 48 512 48000 ... ERR 3225 ← 扬声器 sink,错误持续累积
R 108 720 48000 ... ERR 163 ← Waydroid 流,周期 720 帧
↑ 512 与 720 不成整数比 → 实时重采样 + 高错误率
表现就是声音时好时坏、突然像"炸麦/机器人说话",取决于当时连进来的是哪个客户端。这一点比单纯欠载更迷惑人:它不随负载变化,而是随"谁在放声音"变化。
修复
(1)实时优先级(辅助)—— /etc/security/limits.d/25-pw-rlimits.conf 已给 @pipewire 组 rtprio 95:
sudo usermod -aG pipewire neko # 需要重新登录/重启才生效
注意:实测这台机器上即使
ulimit -r是 95,chrt -f 80仍返回 EPERM,PipeWire 拿不到 SCHED_FIFO。所以不要指望这一项,主战场是下面的量子。
(2)把量子钉死(关键) —— ~/.config/pipewire/pipewire.conf.d/10-stability.conf:
context.properties = {
default.clock.quantum = 1024
default.clock.min-quantum = 1024 # 关键:设成 512 等于没设
default.clock.max-quantum = 4096
default.clock.force-quantum = 1024 # 关键:强制,忽略客户端请求
}
min-quantum 和 force-quantum 两行缺一不可——只改 quantum 就是这次的弯路,客户端照样能把图压到 512。
临时生效(不重启):
pw-metadata -n settings 0 clock.force-quantum 1024
验证
# 看量子是否被钉住
pw-metadata -n settings | grep -E 'force-quantum|min-quantum'
# 看错误计数是否停止增长(隔 10 秒读两次对比)
timeout 8 pw-top -b -n 3 | grep -E 'alsa_output|Waydroid'
# ERR 列不再增长 = 稳定
实测:修前 sink ERR 一路涨到 3225;固定 1024 后 10 秒、30 秒两次采样均零增长。
注意
- 不要单独重启 PipeWire:Waydroid 的 pulse socket 是绑定挂载进容器的,PipeWire 一重启容器里那个 socket 就变陈旧的 inode,Android 立刻没声音。要么重启整机,要么重启 PipeWire 后走3.4「完整重建」把容器重拉一遍。
- quantum 1024 ≈ 21 ms 延迟。看视频/听歌无感;打音游觉得延迟大可以降到 512,但那就回到"容易被客户端拖下去"的老问题。
-
这台机器的延迟预算:
PULSE_LATENCY_MSEC 60(Waydroid 侧)+ quantum 1024(系统侧)是两处独立缓冲,调一处不影响另一处。
4.2 外放调音:把 MIUI 的杜比参数搬到 Linux
结论
杜比的算法搬不过来(Android AudioEffect + 厂商 DSP 标定,且授权绑定),但调音参数是明文:/vendor/etc/dolby/dax-default.xml(DAX3 Android,format 3.x)。单位是 1/16 dB,-192 就是 −12 dB。
| XML 节点 | 含义 |
|---|---|
audio-optimizer-bands
|
逐机喇叭校正,20 段,L/R 独立 |
preset type="ieq" + ieq-amount
|
杜比音色曲线,按百分比混入(实测只有 5–11%) |
regulator-tuning
|
每段限幅阈值(喇叭保护) |
volmax-boost
|
静态响度增益 |
volume-leveler / dialog-enhancer / virtualizer
|
动态 / 对白 / 虚拟环绕,只能近似 |
nabu 的曲线在 469 Hz–2.25 kHz 是一串 −7…−9 dB 的坑,本质是减法校正,不是"增强"。另外 ieq_balanced/warm/detailed 三条曲线在 agate/fuxi/marble/zircon 四台机上完全一致,说明它们是杜比 bundle 的通用音色,逐机差异在 audio-optimizer-bands(有的机型这一段全是 0,校准被放进 HAL/DSP 二进制)。
踩过的坑:滤波器级联 ≠ 曲线
第一版把 20 个频点直接映射成 20 个 Q=1.2 的 peaking 滤波器。频段只隔 1/3–1/2 倍频程,每个滤波器同时也在削邻居,级联后:
- 曲线上 1 kHz = −7.6 dB
- 实际级联 ≈ −20 dB
- 整条链路平均 −12 dB(满幅粉噪逐样本跑 biquad 仿真验证)
为了把响度找回来又加了 +4 dB 补偿增益——这就是"音量 50% 以上炸麦"的来源:补偿增益把信号重新推回功放(CS35L41)的保护边缘。
教训:点值曲线要么用 FIR,要么拟合成少量宽滤波器再级联;不能把每个点当成独立 biquad 直接串起来。
修法:9 段宽滤波 + 零补偿 + 保险限幅
- 最小二乘把曲线拟合成 9 个 Q=1.0、一倍频程间距的 peaking(63/125/250/500/1k/2k/4k/8k/16k),拟合误差 ±2 dB
- 不加补偿增益;EQ output-gain = −2 dB 留余量
- 限幅器只当安全网:−1 dBFS、5 ms lookahead、20 ms release,正常播放不动作
- 60 Hz 高通,限制小板喇叭的低频行程
仿真(满幅粉噪):peak −6.0 → −14.5 dBFS,rms −18.5 → −25.7 dBFS。100% 音量也不会超过直通路径,从原理上不会再炸。
代价:同音量下比直通轻约 7 dB,靠音量旋钮补。
现状
| 组件 | 位置 |
|---|---|
| 预设 |
~/.local/share/easyeffects/output/MIUI-Dolby-Pad5-v2.json
|
| 开机自启 |
~/.config/systemd/user/easyeffects-dolby.service(enabled)
|
| 启动后加载预设 + 切默认 sink |
~/.local/bin/ee-default-sink.sh
|
| 自动加载配置 |
~/.local/share/easyeffects/autoload/output/alsa_output.platform-sound.HiFi__Speaker__sink:Speaker playback.json
|
systemctl --user status easyeffects-dolby.service
systemctl --user restart easyeffects-dolby.service
systemctl --user disable --now easyeffects-dolby.service # 不要了
easyeffects -b 1 # 全局旁路,A/B 对比
easyeffects -b 2 # 恢复
wpctl set-default 48 # 临时把默认输出切回扬声器(绕过 EE)
想更响
在 EasyEffects 里加 Limiter → Input gain,每次 +2 dB,加到开始炸就退一格。不要靠改 EQ 抬增益——那等于把级联过深的问题再引回来。
第 5 部分 · 亮度与音量同步
5.1 waydroid-sync:组件、主控与音量映射
让 Ubuntu 与 Android 的亮度、音量互相跟随。
组件
| 项 | 位置 |
|---|---|
| 守护进程 |
~/Apps/waydroid-sync.py
|
| 开机自启 |
~/.config/systemd/user/waydroid-sync.service
|
| 变更日志 |
~/.local/state/waydroid-sync.log(同时进 journal)
|
systemctl --user status waydroid-sync
systemctl --user restart waydroid-sync
systemctl --user disable --now waydroid-sync # 不要了就关掉
journalctl --user -u waydroid-sync -f # 实时看同步事件
音量:不能 1:1 对齐
两端音量是相乘关系:
实际听到 ≈ 宿主音量% × 安卓媒体音量%
所以「宿主 50% + 安卓 50%」实际只有 25%。1:1 同步会让音量莫名其妙变小。
解法是把非主控端钉住,主控端的数值就等于实际音量。
主控端由脚本顶部常量选择:
MASTER = "auto" # 默认:谁被拖动谁生效,另一端归满——两边都能正常调
HOST_PIN = 100.0 # 固定主控模式下钉住的一端
# MASTER = "android" # 安卓固定主控(宿主钉满)
# MASTER = "host" # 宿主固定主控(安卓钉满)
默认 auto:你动哪边,哪边就保持住;另一边自动归满,保证"两端乘积 = 你这次调的数值"。
| 操作 | 结果 | 实际音量 |
|---|---|---|
| 宿主拖到 35% | 宿主停在 35%,安卓归 100% | 35% |
| 安卓拖到 5/15 | 安卓停在 33%,宿主归 100% | 33% |
代价是没被调的那一侧会显示 100%。这在相乘结构下无法避免——两个滑块不可能同时显示真实值,总有一端要让位。
如果希望某一端永远固定(例如只在安卓里调音量),可以切换成 "android" 或 "host":
|
|
"auto"(默认)
|
"android"
|
"host"
|
|---|---|---|---|
| 安卓滑块 | 可用 | 永远满格,动不了 | 拖动后弹回 |
| 宿主滑块 | 可用 | 拖动后弹回 | 可用 |
| 通知/闹钟突发 | 哪端归满就哪端可能响 | 可能很响 | 被宿主压低,最安全 |
实测教训:钉死某一端(不管哪一端)都会让那边的音量条失效。
"android"模式下的原话是"没法把音量调大了,默认就是满的";"host"模式下则是"宿主这边的应用都不能调音量了"。所以默认用auto。
启动归一化
开机时两端可能都在半路(各 60%,实际 36%)。守护进程启动时把乘积算清并搬到主控端:
[19:50:45] 启动归一化:实际音量 53% → 安卓 53% / 宿主钉 100%
响度不变,只是把账本统一到主控端。
一个无法避免的副作用
在安卓里拖音量条,松手后会弹回满格(因为真正被调的是宿主音量)。这是相乘结构决定的:两个滑块不可能同时显示真实值。
亮度
| 层级 | 来源 | 说明 |
|---|---|---|
| 1 | 应用内窗口亮度 |
读 SurfaceFlinger 的 displayBrightness 覆盖值,映射到宿主背光
|
| 2 |
Android 全局设置 screen_brightness(0–255)
|
下拉快捷面板、设置里的亮度条 |
| 3 |
宿主 backlight(ktz8866-backlight,0–2047)
|
GNOME 滑块、键盘快捷键 |
应用内调亮度时,SurfaceFlinger 会设置覆盖值,守护进程读到后直接写宿主背光;退出应用后覆盖消失,自动回落到全局设置同步(并做基线重对齐,避免跳变)。
# 查看某个 app 是否设置了窗口亮度
adb shell 'dumpsys SurfaceFlinger | grep -m1 displayBrightness'
# displayBrightness=nullopt → 没有 app 在控制
# displayBrightness=0.35 → 有 app 设了 35%
注意:不是所有播放器都用窗口亮度。有的(如部分 B 站版本)用半透明黑色遮罩"调暗",那种在 SurfaceFlinger 里没有任何数值可读,无法桥接——因为它只是往画面上盖了一层黑,不改任何亮度参数。
另外,Waydroid 没有虚拟背光,Android 的亮度设置本身不会让画面变暗;现在它是通过同步去驱动宿主 backlight,才产生实际效果。
关键实现细节
- 回读后存基线:Android 的媒体音量上限不是固定的——有时是 15,音量保护策略生效时会钳到 10。写完必须回读真实值存起来,否则会出现"设了又改回去"的振荡
-
上限必须动态解析:
cmd media_session volume --stream 3 --get返回volume is N in range [0..MAX],要按返回的 MAX 换算百分比。写死 10 会把拖到顶算成 150%,宿主被钳到 100% 并莫名跳满(这个坑踩过一次) - 忽略"被应用恢复到满格":安卓侧被钉在 100%,有些应用/系统会自己把音量恢复满格。这类变化要忽略,否则会被误判成用户调节,导致宿主突然跳到 100%
- 按变化方向判断:只有一侧变了、另一侧没变才触发同步,两边都变就都不动
-
adb 自愈:容器重启后 adbd 可能没开,守护进程每 30 秒最多尝试一次
sudo waydroid adb - 容器没起时不报错:只记一行日志,等待容器就绪
第 6 部分 · 桌面工具链
6.1 看 Markdown:现状与结论
| 场景 | 现状 |
|---|---|
双击 .md
|
用 org.gnome.TextEditor.desktop 打开,纯文本,没有渲染
|
| 查当前默认程序 |
gio mime text/markdown
|
| 改默认程序 |
xdg-mime default <desktop-id> text/markdown
|
| 要渲染着看 | 用 dsh 的文档预览(见 6.2),工作区外的文件见 6.3 |
不要装 apostrophe。 apt 里是 3.4(2022 年的代码),在 Ubuntu 26.04 上直接坏:ApostropheTextView 的 markup 控制器没初始化成功,每次布局回调都抛异常,窗口永久「未响应」,正文永远不渲染,连 WebKit 子进程都没起来。日志特征:
apostrophe/text_view.py:327 in _on_size_allocate
self.markup.update_margins_indents()
AttributeError: 'ApostropheTextView' object has no attribute 'markup'
2026-11-03 装过一次确认无解,已 apt remove --purge 卸载并把 mime 默认还原。真要个桌面编辑器再另找(Qt 系的 ghostwriter / retext 没这个问题),但日常看文档用 dsh 就够。
6.2 dsh Web GUI:文件树与文档预览
启动 dsh web,浏览器打开它打印的地址(本机是 http://127.0.0.1:3080)。
-
右侧栏 →「文件」面板:树根是当前会话的工作目录(默认
~/文档/deepseek-harness/default-workspace),只能往下,不能向上浏览。这是设计限制:Host 的目录列举按会话根做包含检查,不是配置项。 - 点任意文件 → 开一个「文档预览」标签,内容由浏览器渲染,不依赖本地编辑器。支持 markdown(表格、代码块、相对路径图片、YAML frontmatter)、代码、图片、PDF、docx/pptx(本地转 PDF)、xlsx/csv、HTML。
- 预览按页懒加载、文件改动自动刷新,所以改文档时不用手动重载。
- 注意:读取接口本身不限制工作区,限制的只有「列目录」。所以既有文件能被预览,只是文件树里走不到。
6.3 自写插件:@local/dsh-open-file
在「文件」面板标题栏加一个按钮(箭头进文件夹的图标),弹出系统文件选择框、默认定位 ~/桌面,选中的文件直接用内置预览打开,不受工作区限制。
| 项 | 值 |
|---|---|
| 源码 |
~/文档/deepseek-harness/default-workspace/plugins/dsh-open-file/
|
| 包名 |
@local/dsh-open-file
|
| Host 半边 |
index.js:注册 GET /dsh-open-file/pick,spawn zenity --file-selection
|
| Client 半边 |
client.js:往 sidebar.right.tab.files.actions 注册按钮,拿到路径后 openResource
|
| 起始目录 |
读 XDG_DESKTOP_DIR(= ~/桌面);可用环境变量 DSH_OPEN_FILE_START 覆盖
|
| 安装 |
plugin_manager 的 install_bundle,target 传上面那个绝对路径,装完刷新页面
|
| 卸载 |
plugin_manager 的 remove_bundle,target 传 @local/dsh-open-file
|
工作方式:Host 只负责「弹框、把你亲手选的那个绝对路径返回」,Client 把路径包成 dsh-resource://file/session/<会话id>//home/neko/桌面/xxx.md 再交给内置预览(首部 // 表示绝对路径)。安全边界:
-
路由只接受
GET,只接受Sec-Fetch-Site: same-origin(跨站 403),同一时刻只开一个对话框(409)。 - 不接受任何路径参数,返回值只能来自对话框,伪造地址伪造不出文件。
装进的是 profile web(~/.dsh/profiles/web),pnpm add <目录> 建的是软链接,所以改源码目录里的文件就是改生效文件;改完刷新页面即可,模块版本没换就重启 dsh web。
6.4 装 dsh 插件的前置:pnpm
install_bundle 内部跑 pnpm add,本机最初没有 pnpm,会直接失败:
Command failed with ENOENT: pnpm add '...'
spawn pnpm ENOENT
sudo npm i -g pnpm@10 # 装到 /usr/local/bin/pnpm
pnpm --version # 10.34.9
另外两条和这次相关的清理经验:
-
不要用
apt autoremove清空间。 本机有一批被标成「自动安装」但你在用的包(deepin-wine、exagear、caribou 等),autoremove 会一起带走。卸载单个包用apt remove --purge <pkg>就够。 -
卸 apostrophe 时留下了 pandoc 等 4 个包共约 210 MB(当时作为依赖被拉进来)。pandoc 本身是通用文档转换工具,留着有用;确实不要就
apt remove pandoc pandoc-data python3-pypandoc python3-regex。
附录
A. 事故时间线(2026-11-02)
| 时间 | 事件 |
|---|---|
| 19:54:30 | 上一段 6h 会话干净关机 |
| 19:54:31 |
插电自动开机(off-mode-charge=0)
|
| 19:55:03 | logind 收到电源键事件 → 干净关机 |
| 19:55:47 | 关机完成,1 秒后又自动上电 |
| 19:56:19 |
再次 poweroff.target 排队 → 19:56:57 关机
|
| 19:56:57 |
第三次启动时 slot-unbootable:a=yes、retry=0 → 停 fastboot
|
| 19:56:59 |
set_active a 后恢复启动
|
| 20:49:05 |
33W 头下 v_offset 被写坏,充电进入 fault 循环
|
| 21:11–21:13 | 尝试 rebind ln8000 → 内核 oops(21:13:36)→ 死机 → 重启 |
| 21:55 |
fastboot 测试:确认计数 5→4、set_active 回到 7
|
| 11/03 22:38 |
6~8% 低电量下再次进入坏状态(tcpm 0V、不充电),重插后恢复 −3.6A
|
| 11/04 00:33 |
99% 时拔线,v_offset 瞬时溢出(属正常瞬变),此后待机
|
B. 不要做的事
| 操作 | 后果 |
|---|---|
fastboot set_active b
|
b 从未成功启动过,切过去大概率直接 fastboot |
fastboot flash boot_a ...
|
覆盖 dualboot bootmanager,丢 Linux 引导 |
fastboot erase misc
|
已验证无效(槽信息不在这里) |
fastboot oem lock / unlock
|
可能清数据 |
| unbind/rebind ln8000 | 内核 oops 死机 |
| 刷比当前旧的固件 |
anti:1,可能硬砖
|
chown -R 整个 Waydroid /data
|
keystore(1017)/media(1023) 属主被改坏 → 无限重启(见 3.5) |
sudo waydroid session start
|
DBus NoReply,管理器无响应(见 3.5) |
改 init.waydroid.rc 加 setenv
|
容器起不来,必须改 init.environ.rc(见 3.5)
|
| 单独重启 PipeWire | 容器里的 pulse socket 变陈旧 inode,Android 立刻没声音(见 4.1) |
apt install apostrophe
|
3.4 在 26.04 上崩溃(markup AttributeError),窗口永久无响应(见 6.1)
|
apt autoremove
|
会带走 deepin-wine / exagear 等仍在使用的包(见 6.4) |
C. 命令备忘
# ---- 启动 / 槽位 ----
fastboot set_active a && fastboot reboot
cat ~/.local/share/nabu/boot-count
nabu-boot-count reset
# ---- 充电 ----
cat /sys/class/power_supply/qcom-battery/status
cat /sys/class/power_supply/qcom-battery/current_now # 6.14: 负=充电 6.17: 正=充电
cat /sys/class/power_supply/qcom-battery/capacity
journalctl -k -n 20 | grep -o 'v_offset=[0-9]*' | tail -1
timeout 20 udevadm monitor --subsystem-match=typec 2>/dev/null | grep -c port0-partner
# ---- Waydroid ----
waydroid session stop
sudo systemctl start waydroid-container
waydroid session start &
waydroid show-full-ui
waydroid status
# ---- 音频 ----
pw-metadata -n settings | grep -E 'force-quantum|min-quantum'
timeout 8 pw-top -b -n 3 | grep -E 'alsa_output|Waydroid'
wpctl status | sed -n '/Streams:/,/Video/p'
# ---- 文档 / dsh ----
gio mime text/markdown # 当前用什么打开 .md
sudo npm i -g pnpm@10 # dsh 装插件的前置(只需一次)
dsh web # 启 Web GUI,浏览器访问它打印的地址
D. 参考
- Waydroid 音频 HAL 无 PULSE_RUNTIME_PATH:https://github.com/waydroid/waydroid/issues/2333
- Waydroid 官方文档:https://docs.waydro.id/
-
Waydroid 安装源:
https://repo.waydro.id(已确认支持 Ubuntu 26.04resolute)
E. 局域网访问 DSH(黑屏救援用)
systemctl --user status dsh-lan # 绑定 192.168.31.41:3081 的实例
cat ~/桌面/dsh-局域网地址.txt # 带 token 的最新 URL
-
token 每次重启都会变,URL 以桌面那个 txt(或
~/.local/share/nabu/dsh-lan-url.txt)为准 - 所有设备必须连同一个实例:DSH 的会话同一时间只能被一个进程驱动, 连到别的实例会报"当前会话已被占用"
- 只监听局域网 IP,别在路由器上映射到公网(URL 等价于以 neko 身份执行命令)
F. 电源键与休眠显示
行为(2026-11-04 实测定稿):
| 操作 | 结果 |
|---|---|
| 亮屏时按电源键 |
挂起(systemctl suspend)
|
| 熄屏时按电源键 |
点亮(PowerSaveMode=0 + 解除背光 powerdown)
|
| 从挂起恢复 | 2 秒后强制点亮一次(兜底) |
为什么这么绕:这块屏有两个坑,单独用系统默认逻辑都会踩到——
-
GNOME 的
power-button-action默认是suspend,按电源键唤醒时同一个按键事件又触发一次挂起,表现为"按了没反应、要多按几次"; -
/sys/class/drm/card0-DSI-1/dpms是 只读 的(-r--r--r--),root 也写不进去;真正控制显示开关的是 mutter 的PowerSaveMode(0=亮,1=暗,3=关)。 - 偶尔从挂起恢复后,mutter 认为屏亮着、但面板没重新初始化,屏幕实际是黑的。
当前机制:
| 组件 | 作用 |
|---|---|
gnome power-button-action = 'nothing'
|
让 GNOME 不抢电源键 |
nabu-wake-display(system 服务)
|
监听电源键 → 按 dpms 状态决定挂起还是点亮
|
nabu-display-on-resume(system 服务)
|
每次 resume 后 2 秒强制点亮 |
nabu-resume-probe(system 服务)
|
记录每次唤醒的 dpms/背光状态到 /var/log/nabu-resume.log
|
手动操作:
# 强制点亮(黑屏时最有用)
sudo /usr/local/bin/nabu-wake-display --resume
# 看电源键/点亮动作的日志
journalctl -t nabu-wake-display -n 20
# 看每次唤醒的显示状态
sudo tail -20 /var/log/nabu-resume.log
撤销回系统默认:
sudo systemctl disable --now nabu-wake-display nabu-display-on-resume
gsettings set org.gnome.settings-daemon.plugins.power power-button-action 'suspend'
注意:撤销后就会回到"按电源键可能唤不醒、要多按几次"的老状态。
G. 待分析:合上盖子偶尔不触发息屏(2026-11-04 记录)
现象:合上平板套时,有时正常挂起息屏,有时合好几次都没反应(用户原话:"不太灵敏");打开盖子时唤醒倒是正常。
已确认的事实:
-
霍尔传感器工作正常,内核会报
EV_SW / SW_LID:14:04:04 gpio-keys type=5 code=0 value=1 ← 合上 14:04:10 gpio-keys type=5 code=0 value=0 ← 打开 -
logind 也正常处理:
14:04:04 systemd-logind: Lid closed. 14:04:04 systemd-logind: Suspending... 14:04:05 PM: suspend entry (s2idle) - 失败的几次完全没有 SW_LID 事件(60 秒抓取 0 条),说明不是 logind/GNOME 的问题, 而是磁铁→霍尔这一层偶尔没被触发。
-
/etc/systemd/logind.conf里HandleLidSwitch是注释状态(默认suspend),未被改动。
可能原因(待验证):
- 套子里的磁铁位置/强度不够,合上时霍尔电压没跨过阈值(换了保护套可能就正常)
-
gpio-keys的 debounce 或阈值配置 - 合上速度太快/角度不对,磁铁没到位
下次排查用的命令:
# 实时看霍尔事件(合上/打开盖子)
sudo timeout 60 python3 ~/.local/share/nabu/lidcap.py
# 看 logind 有没有收到
journalctl -f | grep -iE 'Lid|suspend entry|suspend exit'
# 历史记录
journalctl | grep -c 'Lid closed'
临时对策:合上后如果没息屏,按一次电源键手动挂起;或者把自动挂起时间调短。
已保存的抓取数据:~/.local/share/nabu/lid-capture-20261104.log
本文地址:https://blog.zhaokugua.com/?post=121
版权声明:若无注明,本文皆为“赵苦瓜のBlog~”原创,转载请保留文章出处。








> 进入 fastboot:关机 → 按住「音量减」→ 插 USB 线。
> 每条命令都用 `2>&1`,因为 getvar 的结果走 stderr。
> 全程只需要 **set_active / oem off-mode-charge**,不要 flash、不要 erase、不要动 b 槽。
## 0. 先存一份完整快照
```bash
fastboot devices
fastboot getvar all 2>&1 | tee all_before.txt
```
## 1. 只读基线(全部安全)
```bash
fastboot getvar current-slot # 期望 a
fastboot getvar slot-count # 期望 2
fastboot getvar slot-retry-count:a # 关键
fastboot getvar slot-retry-count:b
fastboot getvar slot-unbootable:a
fastboot getvar slot-unbootable:b
fastboot getvar slot-successful:a
fastboot getvar slot-successful:b
fastboot getvar battery-voltage
fastboot getvar battery-soc-ok
fastboot getvar off-mode-charge
fastboot getvar charger-screen-enabled
fastboot getvar anti
fastboot getvar crc # devinfo 的 CRC,写入后会变
```
把这些值抄下来(下面表格)。
## 2. 关键实验:计数到底怎么扣
1. fastboot 下记录 `slot-retry-count:a` = **N**
2. 长按电源正常开机 → 进 Ubuntu(等完全启动)
3. 关机,再按住音量减插线进 fastboot
4. 再读 `slot-retry-count:a` = **M**
| 结果 | 结论 | 对策 |
|---|---|---|
| M = N-1 | 每启动一次扣 1,约 7 次后进 fastboot | 每次开机后 `fastboot set_active a` 补满;或减少重启 |
| M = N | 只有"启动失败/快速断电"才扣 | 正常用不会耗尽,避免"插电自动开机→立刻断电"循环 |
| M > N 或异常 | 计数被重置/读错 | 记录原始输出再分析 |
第 3 步后顺便检查:`fastboot getvar slot-successful:a`
- 若变成 `yes`:系统有上报成功,问题在别处
- 若是 `no`:Ubuntu 侧没有 markBootSuccessful(与之前的结论一致)
## 3. 可能"关掉计数/避免计数流失"的命令(按风险从低到高)
### 3.1 重置计数(已证实有效)
```bash
fastboot set_active a
fastboot getvar slot-retry-count:a # 期望回到 6 或 7
fastboot getvar slot-unbootable:a # 期望 no
```
### 3.2 关掉"插电自动开机"(间接避免计数流失)
```bash
fastboot oem off-mode-charge 1
fastboot getvar off-mode-charge # 期望 1;若报错说明不支持,忽略
```
验证:关机 → 插线 → 应停在充电/黑屏,而不是自动启动。
恢复:`fastboot oem off-mode-charge 0`
### 3.3 充电屏(可选,可能不存在)
```bash
fastboot oem charger-screen-enabled 1 # 报错就跳过
fastboot getvar charger-screen-enabled
```
### 3.4 设备信息(只读,帮助判断)
```bash
fastboot oem device-info # 有的 ABL 支持,有的报错
```
### 3.5 不要碰
- `fastboot set_active b`(b 从未成功启动过)
- `fastboot flash boot_a ...`(boot_a 是 dualboot bootmanager)
- `fastboot erase misc`(已验证无效)
- `fastboot oem lock/unlock`(可能清数据)
## 4. 记录表
| 项目 | 测试前 | set_active a 后 | 一次正常启动后 |
|---|---|---|---|
| current-slot | | | |
| slot-retry-count:a | | | |
| slot-unbootable:a | | | |
| slot-successful:a | | | |
| off-mode-charge | | | |
| crc | | | |
## 5. 安全网
万一测到计数归零、进不了系统:**`fastboot set_active a` → `fastboot reboot`** 就能回来。
所以这次实验风险很低,只要记住这一条。