You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

289 lines
11 KiB

#!/usr/bin/env bash
#
# BLE 通话翻译日志抓取 / 过滤工具
#
# 用途:排查"通话中 BLE buff 溢出且不恢复"问题,把 Android / iOS / Dart 三处日志
# 按同一套关键词过滤出来,边看边落盘,方便和设备端同事对时间轴。
#
# 常用:
# ./tools/ble_log.sh android # 实时抓 Android BLE 日志(同时写文件)
# ./tools/ble_log.sh android -k # 只看通话翻译相关行(强过滤)
# ./tools/ble_log.sh ios # 实时抓 iOS 真机日志
# ./tools/ble_log.sh pull-android # 拉 app 内的 Dart 日志文件 app_*.log
# ./tools/ble_log.sh pull-ios # 同上(iOS,需 devicectl)
# ./tools/ble_log.sh filter <日志文件> # 从已有日志里筛通话翻译相关行
# ./tools/ble_log.sh stat <日志文件> # 快速统计:拥塞/休眠/发送节拍
#
set -uo pipefail
APP_ID="com.saitong.voitrans"
IOS_SUBSYSTEM="com.yunqiinnovation.ble_service"
# Android 需要抓的 TAG(flutter 用于捞 Dart 侧 Logger 输出)
ANDROID_TAGS=(
BleService
BleCommandSender
BleServicePlugin
BleCompanionManager
OpusAudioManager
flutter
)
# ---------------------------------------------------------------------------
# 通话翻译相关关键词。
# 前半段是代码里已有的日志标记,后半段 CT_* 是排查期建议补充的聚合日志标记
# (每秒吞吐 CT_RATE / 设备流控 CT_FLOW / 写入背压 CT_TX / 心跳 CT_HB)。
# 新增日志时统一用 [CT_xxx] 前缀,这里就不用再改。
# ---------------------------------------------------------------------------
CT_PATTERN='CT_RATE|CT_FLOW|CT_TX|CT_HB|CALL_TRANS_CMD|CALL_TRANS_HB|CALL_TRANS_DEBUG'
CT_PATTERN="$CT_PATTERN"'|音频下行发送|下行写入拥塞|下行写入已恢复|音频发送队列已满|音频发送线程|声道积压缓存'
CT_PATTERN="$CT_PATTERN"'|休眠|补发积压|恢复左声道|恢复右声道|通话翻译|编解码|心跳'
CT_PATTERN="$CT_PATTERN"'|连接参数|连接优先级|onConnectionUpdated|MTU|mtu'
CT_PATTERN="$CT_PATTERN"'|已连接|已断开|连接断开|重连|GATT'
OUT_DIR="${BLE_LOG_DIR:-./ble_logs}"
usage() {
# 打印文件头部的注释块(到第一个非注释行为止)
awk 'NR==1 {next} /^#/ {sub(/^# ?/, ""); print; next} {exit}' "$0"
exit "${1:-0}"
}
die() { echo "错误: $*" >&2; exit 1; }
ts() { date +%Y%m%d_%H%M%S; }
need() { command -v "$1" >/dev/null 2>&1 || die "未找到命令 $1,请先安装"; }
# 生成输出文件路径
out_file() {
mkdir -p "$OUT_DIR"
echo "$OUT_DIR/$1_$(ts).log"
}
# ---------------------------------------------------------------------------
# Android 实时抓取
# ---------------------------------------------------------------------------
cmd_android() {
local keyword_only=0 all_history=0 outfile=""
while [[ $# -gt 0 ]]; do
case "$1" in
-k|--keyword) keyword_only=1; shift ;;
-a|--all) all_history=1; shift ;;
-o|--out) outfile="$2"; shift 2 ;;
*) die "未知参数 $1" ;;
esac
done
need adb
adb get-state >/dev/null 2>&1 || die "adb 没有连接到设备(adb devices 检查一下)"
[[ -z "$outfile" ]] && outfile="$(out_file android)"
# 只保留关心的 TAG,其余全部静音(*:S)
local specs=()
for t in "${ANDROID_TAGS[@]}"; do specs+=("$t:V"); done
specs+=("AndroidRuntime:E" "*:S")
echo "==> 抓取 Android 日志,落盘: $outfile"
echo " TAG: ${ANDROID_TAGS[*]}"
[[ $keyword_only -eq 1 ]] && echo " 模式: 仅通话翻译关键词"
echo " Ctrl+C 结束"
echo
# -T 1 表示从当前时刻开始,避免历史日志刷屏;-a 则抓取缓冲区里的全部历史
local head_args=(-v threadtime)
[[ $all_history -eq 0 ]] && head_args+=(-T 1)
adb logcat -c 2>/dev/null || true
if [[ $keyword_only -eq 1 ]]; then
adb logcat "${head_args[@]}" "${specs[@]}" \
| grep --line-buffered -E "$CT_PATTERN" \
| tee "$outfile"
else
adb logcat "${head_args[@]}" "${specs[@]}" | tee "$outfile"
fi
}
# ---------------------------------------------------------------------------
# iOS 实时抓取
# 真机优先用 idevicesyslog(libimobiledevice,brew install libimobiledevice)
# 模拟器 / 本机用 log stream
# ---------------------------------------------------------------------------
cmd_ios() {
local keyword_only=0 use_sim=0 outfile=""
while [[ $# -gt 0 ]]; do
case "$1" in
-k|--keyword) keyword_only=1; shift ;;
--sim) use_sim=1; shift ;;
-o|--out) outfile="$2"; shift 2 ;;
*) die "未知参数 $1" ;;
esac
done
[[ -z "$outfile" ]] && outfile="$(out_file ios)"
echo "==> 抓取 iOS 日志,落盘: $outfile"
echo " Ctrl+C 结束"
echo
local filter_cmd=(cat)
[[ $keyword_only -eq 1 ]] && filter_cmd=(grep --line-buffered -E "$CT_PATTERN")
if [[ $use_sim -eq 1 ]]; then
# 模拟器 / 本机:按 subsystem + 进程名过滤
log stream --style syslog --level debug \
--predicate "subsystem == \"$IOS_SUBSYSTEM\" OR process == \"Runner\"" \
| "${filter_cmd[@]}" | tee "$outfile"
elif command -v idevicesyslog >/dev/null 2>&1; then
# 真机:idevicesyslog 最稳,能同时拿到 os_log 和 Dart print
idevicesyslog -m "" 2>/dev/null \
| grep --line-buffered -E "ble_service|BleService|Runner|flutter|$CT_PATTERN" \
| "${filter_cmd[@]}" | tee "$outfile"
else
echo "提示: 未安装 idevicesyslog,真机日志建议 brew install libimobiledevice" >&2
echo " 现在退回 log stream(新版 macOS 对真机支持受限,若无输出请用 Console.app)" >&2
echo >&2
log stream --style syslog --level debug \
--predicate "subsystem == \"$IOS_SUBSYSTEM\" OR process == \"Runner\"" \
| "${filter_cmd[@]}" | tee "$outfile"
fi
}
# ---------------------------------------------------------------------------
# 拉取 app 内的 Dart 日志文件(Logger 写的 app_YYYY-MM-DD*.log)
# ---------------------------------------------------------------------------
cmd_pull_android() {
need adb
adb get-state >/dev/null 2>&1 || die "adb 没有连接到设备"
local dest="$OUT_DIR/dart_android_$(ts)"
mkdir -p "$dest"
# getApplicationDocumentsDirectory() → /data/data/<pkg>/app_flutter
local remote_dir="app_flutter/logs"
echo "==> 从 $APP_ID 拉取 $remote_dir → $dest"
local files
files="$(adb exec-out run-as "$APP_ID" ls "$remote_dir" 2>/dev/null)" || {
die "run-as 失败。release 包不可调试,请改用 debug/profile 包,或让 app 提供日志导出入口"
}
[[ -z "$files" ]] && die "设备上没有日志文件(app 可能还没初始化 Logger)"
while IFS= read -r f; do
[[ -z "$f" ]] && continue
echo " - $f"
adb exec-out run-as "$APP_ID" cat "$remote_dir/$f" > "$dest/$f"
done <<< "$files"
echo
echo "完成: $dest"
ls -lh "$dest"
}
cmd_pull_ios() {
need xcrun
local dest="$OUT_DIR/dart_ios_$(ts)"
mkdir -p "$dest"
local udid
udid="$(xcrun devicectl list devices 2>/dev/null | awk '/connected/ {print $(NF-1); exit}')"
[[ -z "$udid" ]] && die "未找到已连接的 iOS 设备(xcrun devicectl list devices 检查一下)"
echo "==> 从设备 $udid 拉取 Documents/logs → $dest"
xcrun devicectl device copy from \
--device "$udid" \
--domain-type appDataContainer \
--domain-identifier "$APP_ID" \
--source "Documents/logs" \
--destination "$dest" \
|| die "拉取失败。备选方案:Xcode → Window → Devices and Simulators → 选中 app → Download Container"
echo "完成: $dest"
ls -lh "$dest"
}
# ---------------------------------------------------------------------------
# 离线过滤 / 统计
# ---------------------------------------------------------------------------
cmd_filter() {
local f="${1:-}"
[[ -f "$f" ]] || die "用法: $0 filter <日志文件>"
grep -E "$CT_PATTERN" "$f"
}
cmd_stat() {
local f="${1:-}"
[[ -f "$f" ]] || die "用法: $0 stat <日志文件>"
echo "=========== 通话翻译日志速览: $f ==========="
echo
echo "-- 关键事件计数 --"
local -a keys=(
'通话翻译指令下发'
'音频下行发送'
'下行写入拥塞'
'下行写入已恢复'
'音频发送队列已满'
'声道积压缓存超上限'
'收到左声道恢复指令'
'收到右声道恢复指令'
'心跳连续未发出'
'连接参数'
'连接断开'
)
for k in "${keys[@]}"; do
printf ' %-24s %s\n' "$k" "$(grep -c -- "$k" "$f" 2>/dev/null || true)"
done
echo
echo "-- 设备流控(休眠/恢复配对)--"
local sleep_n resume_n
sleep_n="$(grep -cE '休眠|SLEEP_LEFT|SLEEP_RIGHT|0xF0|0xF1' "$f" 2>/dev/null || true)"
resume_n="$(grep -cE '恢复左声道|恢复右声道|0xF2|0xF3' "$f" 2>/dev/null || true)"
echo " 休眠(F0/F1) = $sleep_n 恢复(F2/F3) = $resume_n"
if [[ "$sleep_n" -gt "$resume_n" ]]; then
echo " ⚠️ 休眠多于恢复 $((sleep_n - resume_n)) 次 —— resume 指令可能在拥塞时丢了,"
echo " 声道会一直卡在休眠态,这正是「怎么都不恢复」的典型形态,重点看最后一次休眠的时刻。"
fi
echo
echo "-- 下行发送节拍 Δ 分布(正常应贴近配置的发送间隔)--"
grep -o 'Δ=[0-9]\+ms' "$f" 2>/dev/null | grep -o '[0-9]\+' | awk '
{ n++; s+=$1; if($1>max) max=$1;
if($1<=50) b1++; else if($1<=100) b2++; else if($1<=300) b3++;
else if($1<=1000) b4++; else b5++ }
END {
if (n==0) { print " (没有匹配到「音频下行发送」行,先确认日志已打开)"; exit }
printf " 样本=%d 均值=%.1fms 最大=%dms\n", n, s/n, max
printf " <=50ms: %d 51-100ms: %d 101-300ms: %d 301-1000ms: %d >1s: %d\n",
b1+0, b2+0, b3+0, b4+0, b5+0
if (b4+b5 > 0) print " ⚠️ 存在 >300ms 的发送空档,对照同一时刻的拥塞/休眠日志看是哪一类原因"
}'
echo
echo "-- 积压水位变化(hold(L=..,R=..),持续上涨即为发不出去)--"
grep -o 'hold(L=[0-9]\+,R=[0-9]\+)' "$f" 2>/dev/null | tail -20 || true
echo
echo "-- 最后 15 条拥塞/丢包告警 --"
grep -E '下行写入拥塞|音频发送队列已满|声道积压缓存超上限|心跳连续未发出' "$f" 2>/dev/null | tail -15 || true
echo
}
# ---------------------------------------------------------------------------
main() {
local sub="${1:-}"
[[ $# -gt 0 ]] && shift
case "$sub" in
android) cmd_android "$@" ;;
ios) cmd_ios "$@" ;;
pull-android) cmd_pull_android "$@" ;;
pull-ios) cmd_pull_ios "$@" ;;
filter) cmd_filter "$@" ;;
stat) cmd_stat "$@" ;;
-h|--help|help|"") usage 0 ;;
*) echo "未知子命令: $sub" >&2; usage 1 ;;
esac
}
main "$@"