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
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 "$@"
|
|
|