# 小米平板 5（nabu）Linux 使用与排障手册

> 最后更新：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 怎么救

```bash
# 只读确认
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
```

补完计数：

```bash
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）。

```bash
# 判断是否在坏状态
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 怎么判断现在稳不稳

```bash
# 最近 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}` |

**用法**

1. 用 6.17 内核启动（rEFInd 选 `6.17.0-UFS-slowdown+`）
2. 笔**笔尖朝左**吸到平板顶部短边（方向反了不充）
3. 看状态：

```bash
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/linux`
  863ef95e）；`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 不起来。
- 移植**可行，但要动内核**：
  1. 需要 6.14.11 的源码树（本机没有，PPA 只发二进制；`/lib/modules/6.14.11-nabu-tmm/build`
     指向作者机器 `/home/themojoman/develop/nabu/linux`，已失效）；
  2. 驱动 794 行，只用 gpiod / regmap / power_supply，6.14 的 API 够用；
  3. 用同 config（`/proc/config.gz`）编模块，`vermagic` 要对上 `6.14.11-nabu-tmm`；
     本机 `CONFIG_MODVERSIONS=n`、`CONFIG_MODULE_SIG=n`，所以编出来能直接加载；
  4. 最省事：改驱动读旧属性名（不动 dtb/UKI）；否则要改 dtb 再用 `ukify` 重打 UKI；
  5. 真正的风险在电源栈：6.14 是 ln8000 电荷泵 + PD 9V，6.17 是 smb5 5V，
     p9418 的反向升压共享 VBUS，没在 6.14 上验证过。
- 取舍：留 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}`，改了两处：
  1. DT 属性名换成 6.14 dtb 里的旧名字（`idt,irq` / `hall,int3` / `hall,int4` /
     `idt,reverse-enable` / `idt,reverse-booset-enable`）
  2. GPIO 用 legacy API（`of_get_named_gpio` + `devm_gpio_request_one` + `gpio_to_desc`），
     因为 6.14 没有 `devm_gpiod_get_from_of_node`
- 编译：`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 没有这个服务，笔平时不广播（省电），必须**长按笔上的按键**让它进入配对模式。

**配对步骤**

1. `bluetoothctl` 里 `scan on`（或设置 → 蓝牙里保持扫描）
2. 长按笔上的按键约 5 秒
3. 出现 `Xiaomi Smart Pen` 后 `pair` → `trust` → `connect`

```bash
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](https://github.com/waydroid/waydroid/issues/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`

```ini
[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 验证音频是否正常（几条命令）

```bash
# 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 正确的启停方式（重要）

#### 启动（推荐）

```bash
# 普通用户身份，不要 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 安卓」图标（启动脚本会处理这些）。

#### 停止

```bash
waydroid session stop        # 会连容器一起停
```

#### ⚠️ 如果容器「起来了但几十秒后自己死」

不要用 `systemctl restart`，走完整重建：

```bash
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）后正常：

```bash
# 首次初始化镜像
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 / 文件传输

```bash
sudo waydroid adb                           # 开启容器内 adb
adb connect 192.168.240.112:5555
adb devices                                 # 应显示 device
```

如果显示 `unauthorized`，把宿主公钥灌进去：

```bash
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 写的，支持拖拽、双向、目录浏览），或：

```bash
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 一次性恢复脚本（保留数据）

```bash
#!/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 那条"必挂"路径）。

```ini
[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`

回滚：

```bash
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 读不到"这个真实缺陷**，随时可按上面的命令回滚。

#### 要继续往下查可选的路

1. 换 Android 11（LineageOS 18.1）镜像复测 —— 重装级操作，流程见 3.4 / 3.10；
2. 用 debuggable 重打包 APK 附加调试 —— 会毁签名、有封号风险，**不建议**；
3. 放弃在 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 重打包**：

```bash
# 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 都受益）**

```bash
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`，不破坏格式：

```bash
# 备份
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 未变）。

#### 附：这次用到/可复用的取证命令

```bash
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 抓崩溃现场）

```bash
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](https://yh.wanmei.com/m/news/gamebroad/20260421/261850.html)）

| 项 | 最低 | 推荐 |
|---|---|---|
| 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 正常，所以重装大概率无效。

参考：雷电模拟器能跑（[官方设置文档](https://help.ldmnq.com/docs/yi-huan-you-hua-she-zhi)，
要求 14.0.5.1+、6 GB 内存 4 核），但那跑的是 **x86 Windows 模拟器**，与 waydroid 的 arm64 路径不同。

#### 20:27 按官方建议卸载重装 —— 实测无效

```bash
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 声道流，驱动/硬件层已经同步，**不可能由软件造成四扬声器不同步**。真不同步会表现为声像偏移、低频抵消发闷，而不是随负载出现的爆音。

#### 真正原因（两层，第二层才是根因）

**第一层：没有实时优先级**

```bash
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`：

```bash
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。

临时生效（不重启）：

```bash
pw-metadata -n settings 0 clock.force-quantum 1024
```

#### 验证

```bash
# 看量子是否被钉住
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` |

```bash
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） |

```bash
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 同步会让音量莫名其妙变小。

解法是**把非主控端钉住**，主控端的数值就等于实际音量。

主控端由脚本顶部常量选择：

```python
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 会设置覆盖值，守护进程读到后直接写宿主背光；退出应用后覆盖消失，自动回落到全局设置同步（并做基线重对齐，避免跳变）。

```bash
# 查看某个 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 子进程都没起来。日志特征：

```text
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，会直接失败：

```text
Command failed with ENOENT: pnpm add '...'
spawn pnpm ENOENT
```

```bash
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. 命令备忘

```bash
# ---- 启动 / 槽位 ----
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.04 `resolute`）

---


### E. 局域网访问 DSH（黑屏救援用）

```bash
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 秒后强制点亮一次（兜底） |

**为什么这么绕**：这块屏有两个坑，单独用系统默认逻辑都会踩到——

1. GNOME 的 `power-button-action` 默认是 `suspend`，按电源键唤醒时**同一个按键事件又触发一次挂起**，表现为"按了没反应、要多按几次"；
2. `/sys/class/drm/card0-DSI-1/dpms` 是 **只读** 的（`-r--r--r--`），root 也写不进去；真正控制显示开关的是 **mutter 的 `PowerSaveMode`**（0=亮，1=暗，3=关）。
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` |

**手动操作**：

```bash
# 强制点亮（黑屏时最有用）
sudo /usr/local/bin/nabu-wake-display --resume

# 看电源键/点亮动作的日志
journalctl -t nabu-wake-display -n 20

# 看每次唤醒的显示状态
sudo tail -20 /var/log/nabu-resume.log
```

**撤销回系统默认**：

```bash
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`），未被改动。

**可能原因（待验证）**：

1. 套子里的磁铁位置/强度不够，合上时霍尔电压没跨过阈值（换了保护套可能就正常）
2. `gpio-keys` 的 debounce 或阈值配置
3. 合上速度太快/角度不对，磁铁没到位

**下次排查用的命令**：

```bash
# 实时看霍尔事件（合上/打开盖子）
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`