pi400网络连接分析脚本

ssh连接pi400调试时,有时会断线,但可能pi400与wifi还是连接的,请帮写个分析脚本,看看它与wifi连接稳定性,与业务相关的youtube/cloudflare/github的连接稳定性。

传到 Pi 后直接用:

# 传到 Pi
scp netwatch.sh [email protected]:~/

# 加执行权限
chmod +x ~/netwatch.sh

# 单次完整报告(排查当前状态)
bash ~/netwatch.sh

# 持续监控(每60秒检测一次,同时写日志)
bash ~/netwatch.sh --watch --log

脚本检测四个层面:

WiFi 层:信号强度(dBm)并给出优/良/差评级、频率、连接速率、RX丢弃包数。信号低于 -80dBm 基本可以确定是断线根源。

网关层:ping 本地路由器,RTT 和丢包率。网关丢包说明 WiFi 本身不稳,跟外网无关。

业务节点:分别 ping YouTube、Cloudflare R2、Cloudflare Pages、GitHub、Gemini API,每个节点显示丢包率和 RTT,颜色区分正常/警告/故障。

SSH 断线原因:检查 sshd 的 ClientAliveInterval 配置,为 0 时直接给出修复命令;同时抓取过去 24 小时的 SSH 断线日志;顺带检测 CPU 温度,Pi 过热会触发降频影响 WiFi 驱动稳定性。

SSH 断线最常见的两个原因:一是 sshd 没开 keepalive,长时间无操作后连接被路由器 NAT 超时踢掉;二是 WiFi 信号弱导致间歇性断联。脚本的输出能直接定位是哪种。

#!/bin/bash
# ─────────────────────────────────────────────────────────────
# netwatch.sh — Pi 400 网络稳定性监控
# 监控:WiFi 信号 / 本地网关 / 业务节点连通性
# 用法:
#   单次报告:  bash netwatch.sh
#   持续监控:  bash netwatch.sh --watch
#   写入日志:  bash netwatch.sh --watch --log
# ─────────────────────────────────────────────────────────────

# ── 配置区 ────────────────────────────────────────────────────
INTERVAL=60          # 持续监控时的检测间隔(秒)
PING_COUNT=5         # 每次 ping 的包数
PING_TIMEOUT=5       # ping 超时秒数
LOG_FILE="$HOME/netwatch.log"
LOG_MAX_LINES=5000   # 日志超过此行数时自动截断

# 业务节点(名称:地址)
declare -A TARGETS=(
    ["YouTube"]="www.youtube.com"
    ["Cloudflare-R2"]="pub-1b62f0f9bae84486a796cbf53a3a27ee.r2.dev"
    ["Cloudflare-Pages"]="pabloye.pages.dev"
    ["GitHub"]="github.com"
    ["Gemini-API"]="generativelanguage.googleapis.com"
    ["DNS-Primary"]="8.8.8.8"
    ["DNS-Secondary"]="1.1.1.1"
)

# ── 颜色 ──────────────────────────────────────────────────────
if [ -t 1 ]; then
    RED='\033[0;31m'; YELLOW='\033[0;33m'
    GREEN='\033[0;32m'; BLUE='\033[0;34m'
    CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
else
    RED=''; YELLOW=''; GREEN=''; BLUE=''; CYAN=''; BOLD=''; NC=''
fi

# ── 工具函数 ──────────────────────────────────────────────────

ts() { date '+%Y-%m-%d %H:%M:%S'; }

log() {
    local line="$(ts) $*"
    echo -e "$line"
    if [ "$DO_LOG" = "1" ]; then
        echo "$line" >> "$LOG_FILE"
        # 自动截断过长日志
        local lines
        lines=$(wc -l < "$LOG_FILE")
        if [ "$lines" -gt "$LOG_MAX_LINES" ]; then
            tail -n $((LOG_MAX_LINES / 2)) "$LOG_FILE" > "${LOG_FILE}.tmp"
            mv "${LOG_FILE}.tmp" "$LOG_FILE"
            echo "$(ts) [INFO] 日志已自动截断至 $((LOG_MAX_LINES / 2)) 行" >> "$LOG_FILE"
        fi
    fi
}

section() { log "${BOLD}${BLUE}── $1 ──${NC}"; }

# ── WiFi 状态 ─────────────────────────────────────────────────

check_wifi() {
    section "WiFi 状态"

    local iface
    iface=$(iw dev 2>/dev/null | awk '/Interface/{print $2}' | head -1)
    if [ -z "$iface" ]; then
        log "${RED}[WARN] 未找到无线网卡${NC}"
        return
    fi

    # SSID 和 BSSID
    local ssid bssid
    ssid=$(iw dev "$iface" link 2>/dev/null | awk '/SSID/{print $2}')
    bssid=$(iw dev "$iface" link 2>/dev/null | awk '/Connected to/{print $3}')
    [ -z "$ssid" ] && ssid="(未连接)"
    log "  接口: ${CYAN}${iface}${NC}  SSID: ${CYAN}${ssid}${NC}  BSSID: ${bssid}"

    # 信号强度
    local signal freq bitrate
    signal=$(iw dev "$iface" link 2>/dev/null | awk '/signal/{print $2, $3}')
    freq=$(iw dev "$iface" link 2>/dev/null | awk '/freq/{print $2}')
    bitrate=$(iw dev "$iface" link 2>/dev/null | awk '/tx bitrate/{print $3, $4}')
    log "  信号: ${CYAN}${signal:-N/A}${NC}  频率: ${freq:-N/A} MHz  速率: ${bitrate:-N/A}"

    # 信号质量评级
    if [ -n "$signal" ]; then
        local dbm
        dbm=$(echo "$signal" | awk '{print $1}')
        if [ "$dbm" -ge -60 ] 2>/dev/null; then
            log "  质量: ${GREEN}优秀 (≥-60 dBm)${NC}"
        elif [ "$dbm" -ge -70 ] 2>/dev/null; then
            log "  质量: ${GREEN}良好 (-70~-60 dBm)${NC}"
        elif [ "$dbm" -ge -80 ] 2>/dev/null; then
            log "  质量: ${YELLOW}一般 (-80~-70 dBm),可能出现丢包${NC}"
        else
            log "  质量: ${RED}差 (<-80 dBm),建议靠近路由器${NC}"
        fi
    fi

    # IP 地址
    local ip
    ip=$(ip addr show "$iface" 2>/dev/null | awk '/inet /{print $2}' | head -1)
    log "  IP地址: ${CYAN}${ip:-未获取}${NC}"

    # 网关
    GATEWAY=$(ip route show default 2>/dev/null | awk '{print $3}' | head -1)
    log "  网关: ${CYAN}${GATEWAY:-未知}${NC}"

    # WiFi 驱动统计(丢包/重传)
    local rx_drop tx_retry
    rx_drop=$(cat /sys/class/net/"$iface"/statistics/rx_dropped 2>/dev/null)
    tx_retry=$(iw dev "$iface" station dump 2>/dev/null | awk '/tx retries/{print $3}')
    [ -n "$rx_drop" ] && log "  RX丢弃: ${rx_drop}包  TX重传: ${tx_retry:-N/A}次"
}

# ── 网关连通性 ────────────────────────────────────────────────

check_gateway() {
    section "本地网关"
    if [ -z "$GATEWAY" ]; then
        GATEWAY=$(ip route show default 2>/dev/null | awk '{print $3}' | head -1)
    fi
    if [ -z "$GATEWAY" ]; then
        log "  ${RED}[FAIL] 无法获取网关地址${NC}"
        return
    fi
    ping_target "网关" "$GATEWAY"
}

# ── 通用 Ping 检测 ────────────────────────────────────────────

ping_target() {
    local name="$1" host="$2"
    local result loss avg_rtt
    result=$(ping -c "$PING_COUNT" -W "$PING_TIMEOUT" "$host" 2>&1)

    if echo "$result" | grep -q "Network is unreachable\|Name or service not known"; then
        log "  ${RED}[FAIL]${NC} ${BOLD}${name}${NC} (${host}) — 网络不可达或 DNS 失败"
        return 1
    fi

    loss=$(echo "$result" | grep -oP '\d+(?=% packet loss)')
    avg_rtt=$(echo "$result" | grep -oP 'rtt.*= [\d.]+/\K[\d.]+')

    if [ -z "$loss" ]; then
        log "  ${RED}[FAIL]${NC} ${BOLD}${name}${NC} (${host}) — 无响应"
        return 1
    fi

    local status_color="$GREEN" status="OK"
    if [ "$loss" -ge 50 ]; then
        status_color="$RED"; status="严重丢包"
    elif [ "$loss" -ge 20 ]; then
        status_color="$RED"; status="丢包"
    elif [ "$loss" -ge 5 ]; then
        status_color="$YELLOW"; status="轻微丢包"
    fi

    local rtt_color="$GREEN"
    if [ -n "$avg_rtt" ]; then
        local rtt_int=${avg_rtt%.*}
        if [ "$rtt_int" -ge 300 ] 2>/dev/null; then
            rtt_color="$RED"
        elif [ "$rtt_int" -ge 150 ] 2>/dev/null; then
            rtt_color="$YELLOW"
        fi
    fi

    log "  [${status_color}${status}${NC}] ${BOLD}${name}${NC} (${host}) — 丢包: ${status_color}${loss}%${NC}  RTT: ${rtt_color}${avg_rtt:-N/A} ms${NC}"
}

# ── 业务节点检测 ──────────────────────────────────────────────

check_targets() {
    section "业务节点连通性"
    for name in YouTube Cloudflare-R2 Cloudflare-Pages GitHub Gemini-API DNS-Primary DNS-Secondary; do
        ping_target "$name" "${TARGETS[$name]}"
    done
}

# ── SSH 断线原因分析 ──────────────────────────────────────────

check_ssh_stability() {
    section "SSH 断线原因分析"

    # keepalive 设置
    local ka_interval ka_count
    ka_interval=$(sshd -T 2>/dev/null | awk '/clientaliveinterval/{print $2}')
    ka_count=$(sshd -T 2>/dev/null | awk '/clientalivecountmax/{print $2}')
    if [ -n "$ka_interval" ]; then
        log "  sshd ClientAliveInterval: ${ka_interval}s  CountMax: ${ka_count}"
        if [ "${ka_interval:-0}" -eq 0 ]; then
            log "  ${YELLOW}[建议] 服务端未开启 keepalive,长时间无操作会断线${NC}"
            log "  ${CYAN}修复:sudo sh -c 'echo \"ClientAliveInterval 60\" >> /etc/ssh/sshd_config'${NC}"
            log "  ${CYAN}      sudo sh -c 'echo \"ClientAliveCountMax 3\" >> /etc/ssh/sshd_config'${NC}"
            log "  ${CYAN}      sudo systemctl restart sshd${NC}"
        else
            log "  ${GREEN}服务端 keepalive 已配置${NC}"
        fi
    fi

    # 最近的 SSH 断线记录
    log "  最近 SSH 断线记录(最近10条):"
    local disconnects
    disconnects=$(journalctl -u ssh --since "24 hours ago" 2>/dev/null \
        | grep -i "disconnect\|broken pipe\|timeout\|reset by peer" \
        | tail -10)
    if [ -n "$disconnects" ]; then
        echo "$disconnects" | while IFS= read -r line; do
            log "  ${YELLOW}${line}${NC}"
        done
    else
        log "  ${GREEN}最近 24 小时内无异常断线记录${NC}"
    fi

    # 系统负载(Pi 过热也会导致 WiFi 不稳)
    local load temp
    load=$(uptime | awk -F'load average:' '{print $2}')
    temp=$(vcgencmd measure_temp 2>/dev/null | grep -oP '[\d.]+')
    log "  系统负载:${load}"
    if [ -n "$temp" ]; then
        local temp_color="$GREEN"
        [ "$(echo "$temp > 70" | bc -l 2>/dev/null)" = "1" ] && temp_color="$RED"
        [ "$(echo "$temp > 60" | bc -l 2>/dev/null)" = "1" ] && temp_color="$YELLOW"
        log "  CPU温度: ${temp_color}${temp}°C${NC}(>70°C 会触发降频,影响 WiFi 驱动稳定性)"
    fi
}

# ── 单次完整报告 ──────────────────────────────────────────────

run_once() {
    log "${BOLD}${BLUE}════════════════════════════════════════${NC}"
    log "${BOLD}  Pi 400 网络稳定性报告${NC}"
    log "${BOLD}${BLUE}════════════════════════════════════════${NC}"
    check_wifi
    check_gateway
    check_targets
    check_ssh_stability
    log "${BOLD}${BLUE}════════════════════════════════════════${NC}"
}

# ── 持续监控模式 ──────────────────────────────────────────────

run_watch() {
    log "持续监控模式启动(间隔 ${INTERVAL}s,Ctrl+C 停止)"
    [ "$DO_LOG" = "1" ] && log "日志写入: $LOG_FILE"

    local round=0
    while true; do
        round=$((round + 1))
        log "${BOLD}${BLUE}══ 第 ${round} 轮检测 ══${NC}"
        check_wifi
        check_gateway
        check_targets
        log "下次检测: ${INTERVAL}s 后"
        sleep "$INTERVAL"
    done
}

# ── 入口 ──────────────────────────────────────────────────────

DO_WATCH=0
DO_LOG=0

for arg in "$@"; do
    case "$arg" in
        --watch) DO_WATCH=1 ;;
        --log)   DO_LOG=1   ;;
        --help)
            echo "用法: bash netwatch.sh [--watch] [--log]"
            echo "  (无参数)  单次完整报告"
            echo "  --watch     持续监控,每 ${INTERVAL}s 检测一次"
            echo "  --log       同时写入 $LOG_FILE"
            exit 0 ;;
    esac
done

if [ "$DO_WATCH" = "1" ]; then
    run_watch
else
    run_once
fi

运行测试效果:

bash netwatch.sh 

2026-04-06 11:13:53 ════════════════════════════════════════

2026-04-06 11:13:53   Pi 400 网络稳定性报告

2026-04-06 11:13:53 ════════════════════════════════════════

2026-04-06 11:13:53 ── WiFi 状态 ──

2026-04-06 11:13:53   接口: wlan0  SSID: MOVISTAR-WIFI7-5C90  BSSID: dc:08:da:84:5c:9e

2026-04-06 11:13:53   信号: -58 dBm  频率: 5180 MHz  速率: 325.0 MBit/s

2026-04-06 11:13:54   质量: 优秀 (≥-60 dBm)

2026-04-06 11:13:54   IP地址: 192.168.1.38/24

2026-04-06 11:13:55   网关: 192.168.1.1

2026-04-06 11:13:55   RX丢弃: 0包  TX重传: N/A次

2026-04-06 11:13:55 ── 本地网关 ──

2026-04-06 11:13:59   [OK] 网关 (192.168.1.1) — 丢包: 0%  RTT: 2.560 ms

2026-04-06 11:13:59 ── 业务节点连通性 ──

2026-04-06 11:14:03   [OK] YouTube (www.youtube.com) — 丢包: 0%  RTT: 22.292 ms

2026-04-06 11:14:07   [OK] Cloudflare-R2 (pub-1b62f0f9bae84486a796cbf53a3a27ee.r2.dev) — 丢包: 0%  RTT: 18.568 ms

2026-04-06 11:14:11   [OK] Cloudflare-Pages (pabloye.pages.dev) — 丢包: 0%  RTT: 9.701 ms

2026-04-06 11:14:16   [OK] GitHub (github.com) — 丢包: 0%  RTT: 53.299 ms

2026-04-06 11:14:20   [OK] Gemini-API (generativelanguage.googleapis.com) — 丢包: 0%  RTT: 11.181 ms

2026-04-06 11:14:24   [OK] DNS-Primary (8.8.8.8) — 丢包: 0%  RTT: 12.489 ms

2026-04-06 11:14:28   [OK] DNS-Secondary (1.1.1.1) — 丢包: 0%  RTT: 9.895 ms

2026-04-06 11:14:28 ── SSH 断线原因分析 ──

2026-04-06 11:14:28   最近 SSH 断线记录(最近10条):

2026-04-06 11:14:28   最近 24 小时内无异常断线记录

2026-04-06 11:14:28   系统负载: 0.00, 0.00, 0.00

2026-04-06 11:14:28   CPU温度: 32.1°C(>70°C 会触发降频,影响 WiFi 驱动稳定性)

2026-04-06 11:14:28 ════════════════════════════════════════

监控ssh连线情况,分析断线原因

#!/bin/bash
# ─────────────────────────────────────────────────────────────
# ssh_hang_diag.sh — SSH 卡死根因诊断
# 在 Pi 400 上运行,持续采样网络/系统状态,卡顿时留下现场快照
#
# 用法:
#   后台持续采样:  bash ssh_hang_diag.sh --daemon
#   单次快照:      bash ssh_hang_diag.sh --snap
#   查看报告:      bash ssh_hang_diag.sh --report
# ─────────────────────────────────────────────────────────────

INTERVAL=10          # 采样间隔(秒)
LOG_DIR="$HOME/ssh_hang_logs"
SNAP_LOG="$LOG_DIR/snapshots.log"
EVENT_LOG="$LOG_DIR/events.log"
WIFI_IFACE=$(iw dev 2>/dev/null | awk '/Interface/{print $2}' | head -1)
GATEWAY=$(ip route show default 2>/dev/null | awk '{print $3}' | head -1)

# 卡顿判定阈值
PING_LOSS_WARN=20     # 网关丢包率超过此值记录事件
SIGNAL_WARN=-75       # WiFi 信号低于此值记录事件
TEMP_WARN=70          # CPU 温度超过此值记录事件

mkdir -p "$LOG_DIR"

ts() { date '+%Y-%m-%d %H:%M:%S'; }

# ── 单次快照:采集所有关键指标 ───────────────────────────────

take_snapshot() {
    local t; t=$(ts)

    # 1. WiFi 信号
    local signal freq tx_bitrate rx_bitrate
    signal=$(iw dev "$WIFI_IFACE" link 2>/dev/null | awk '/signal/{print $2}')
    freq=$(iw dev "$WIFI_IFACE" link 2>/dev/null | awk '/freq/{print $2}')
    tx_bitrate=$(iw dev "$WIFI_IFACE" link 2>/dev/null | awk '/tx bitrate/{print $3}')
    rx_bitrate=$(iw dev "$WIFI_IFACE" link 2>/dev/null | awk '/rx bitrate/{print $3}')

    # 2. 网卡统计(丢弃/错误包)
    local rx_drop rx_err tx_drop tx_err
    rx_drop=$(cat /sys/class/net/"$WIFI_IFACE"/statistics/rx_dropped 2>/dev/null)
    rx_err=$(cat /sys/class/net/"$WIFI_IFACE"/statistics/rx_errors 2>/dev/null)
    tx_drop=$(cat /sys/class/net/"$WIFI_IFACE"/statistics/tx_dropped 2>/dev/null)
    tx_err=$(cat /sys/class/net/"$WIFI_IFACE"/statistics/tx_errors 2>/dev/null)

    # 3. 网关 ping(3包快速探测)
    local ping_result loss rtt
    ping_result=$(ping -c 3 -W 2 -q "$GATEWAY" 2>/dev/null)
    loss=$(echo "$ping_result" | grep -oP '\d+(?=% packet loss)')
    rtt=$(echo "$ping_result" | grep -oP 'rtt.*= [\d.]+/\K[\d.]+')

    # 4. CPU 温度
    local temp
    temp=$(vcgencmd measure_temp 2>/dev/null | grep -oP '[\d.]+')

    # 5. 系统负载 + 内存
    local load mem_free
    load=$(awk '{print $1}' /proc/loadavg)
    mem_free=$(awk '/MemAvailable/{print $2}' /proc/meminfo)
    mem_free_mb=$((mem_free / 1024))

    # 6. 活跃 SSH 连接数
    local ssh_conns
    ssh_conns=$(ss -tn state established '( dport = :22 or sport = :22 )' 2>/dev/null | grep -c ESTAB || echo 0)

    # 写入采样日志
    echo "$t signal=${signal}dBm freq=${freq}MHz tx=${tx_bitrate}Mbps rx=${rx_bitrate}Mbps" \
         "rxdrop=${rx_drop} rxerr=${rx_err} txdrop=${tx_drop} txerr=${tx_err}" \
         "gw_loss=${loss}% gw_rtt=${rtt}ms temp=${temp}C" \
         "load=${load} mem=${mem_free_mb}MB ssh=${ssh_conns}" >> "$SNAP_LOG"

    # 检查是否触发阈值 → 写事件日志
    local events=()

    if [ -n "$loss" ] && [ "$loss" -ge "$PING_LOSS_WARN" ]; then
        events+=("网关丢包=${loss}%")
    fi
    if [ -n "$signal" ] && [ "$signal" -le "$SIGNAL_WARN" ] 2>/dev/null; then
        events+=("WiFi信号弱=${signal}dBm")
    fi
    if [ -n "$temp" ] && [ "$(echo "$temp >= $TEMP_WARN" | bc -l 2>/dev/null)" = "1" ]; then
        events+=("CPU过热=${temp}C")
    fi
    if [ "${rx_err:-0}" -gt 0 ] || [ "${tx_err:-0}" -gt 0 ]; then
        events+=("网卡错误 rx_err=${rx_err} tx_err=${tx_err}")
    fi

    if [ "${#events[@]}" -gt 0 ]; then
        local event_str
        event_str=$(IFS=', '; echo "${events[*]}")
        echo "$t [EVENT] $event_str" >> "$EVENT_LOG"
        echo "⚠️  $t 检测到异常: $event_str"
    fi

    echo "$t OK signal=${signal}dBm loss=${loss}% rtt=${rtt}ms temp=${temp}C"
}

# ── 生成分析报告 ──────────────────────────────────────────────

generate_report() {
    echo ""
    echo "════════════════════════════════════════"
    echo "  SSH 卡死根因分析报告"
    echo "  数据来自: $LOG_DIR"
    echo "════════════════════════════════════════"

    if [ ! -f "$SNAP_LOG" ]; then
        echo "暂无采样数据,请先运行 --daemon 模式采集一段时间"
        return
    fi

    local total_lines
    total_lines=$(wc -l < "$SNAP_LOG")
    echo "采样记录: ${total_lines} 条"
    echo "时间范围: $(head -1 "$SNAP_LOG" | cut -d' ' -f1-2) ~ $(tail -1 "$SNAP_LOG" | cut -d' ' -f1-2)"
    echo ""

    # WiFi 信号统计
    echo "── WiFi 信号分布 ──"
    local signals
    signals=$(grep -oP 'signal=\K-\d+' "$SNAP_LOG")
    if [ -n "$signals" ]; then
        local avg min max
        avg=$(echo "$signals" | awk '{s+=$1; c++} END {printf "%.0f", s/c}')
        min=$(echo "$signals" | sort -n | head -1)
        max=$(echo "$signals" | sort -n | tail -1)
        echo "  平均: ${avg} dBm  最低: ${min} dBm  最高: ${max} dBm"
        local weak_count
        weak_count=$(echo "$signals" | awk -v t="$SIGNAL_WARN" '$1 <= t' | wc -l)
        echo "  信号 ≤${SIGNAL_WARN}dBm 次数: ${weak_count}/${total_lines}"
        [ "$weak_count" -gt 0 ] && echo "  → 信号弱是SSH卡顿的高概率原因"
    fi
    echo ""

    # 网关丢包统计
    echo "── 网关丢包分布 ──"
    local losses
    losses=$(grep -oP 'gw_loss=\K\d+' "$SNAP_LOG")
    if [ -n "$losses" ]; then
        local avg_loss
        avg_loss=$(echo "$losses" | awk '{s+=$1; c++} END {printf "%.1f", s/c}')
        local loss_count
        loss_count=$(echo "$losses" | awk -v t="$PING_LOSS_WARN" '$1 >= t' | wc -l)
        echo "  平均丢包率: ${avg_loss}%"
        echo "  丢包 ≥${PING_LOSS_WARN}% 次数: ${loss_count}/${total_lines}"
        [ "$loss_count" -gt 0 ] && echo "  → WiFi 间歇性丢包,SSH 缓冲区积压导致卡死"
    fi
    echo ""

    # 温度统计
    echo "── CPU 温度分布 ──"
    local temps
    temps=$(grep -oP 'temp=\K[\d.]+' "$SNAP_LOG")
    if [ -n "$temps" ]; then
        local avg_temp max_temp
        avg_temp=$(echo "$temps" | awk '{s+=$1; c++} END {printf "%.1f", s/c}')
        max_temp=$(echo "$temps" | sort -n | tail -1)
        echo "  平均: ${avg_temp}°C  峰值: ${max_temp}°C"
        local hot_count
        hot_count=$(echo "$temps" | awk -v t="$TEMP_WARN" '$1 >= t' | wc -l)
        [ "$hot_count" -gt 0 ] && echo "  ⚠️  超过${TEMP_WARN}°C 次数: ${hot_count} — 过热会触发降频影响WiFi驱动"
    fi
    echo ""

    # 事件时间线
    echo "── 异常事件时间线 ──"
    if [ -f "$EVENT_LOG" ] && [ -s "$EVENT_LOG" ]; then
        cat "$EVENT_LOG"
    else
        echo "  无异常事件记录(网络状态良好)"
    fi
    echo ""

    # 根因判断
    echo "── 根因判断 ──"
    local has_cause=0
    local weak_pct loss_pct

    weak_pct=$(grep -oP 'signal=\K-\d+' "$SNAP_LOG" | awk -v t="$SIGNAL_WARN" \
        'BEGIN{c=0;tot=0} {tot++; if($1<=t)c++} END{if(tot>0)printf "%.0f", c/tot*100; else print 0}')
    loss_pct=$(grep -oP 'gw_loss=\K\d+' "$SNAP_LOG" | awk -v t="$PING_LOSS_WARN" \
        'BEGIN{c=0;tot=0} {tot++; if($1>=t)c++} END{if(tot>0)printf "%.0f", c/tot*100; else print 0}')

    if [ "${weak_pct:-0}" -ge 10 ]; then
        echo "  ★ 主因:WiFi 信号弱(${weak_pct}% 的时间低于 ${SIGNAL_WARN}dBm)"
        echo "    建议:Pi 靠近路由器,或换用 5GHz 频段"
        has_cause=1
    fi
    if [ "${loss_pct:-0}" -ge 5 ]; then
        echo "  ★ 主因:网关间歇性丢包(${loss_pct}% 的采样有丢包)"
        echo "    建议:检查路由器负载,换用有线网络"
        has_cause=1
    fi
    if [ "$has_cause" -eq 0 ]; then
        echo "  网络层未发现明显问题"
        echo "  可能原因:sshd 未配置 keepalive(见下方修复建议)"
    fi
    echo ""

    # SSH keepalive 检查
    echo "── SSH Keepalive 配置 ──"
    local ka
    ka=$(sshd -T 2>/dev/null | awk '/clientaliveinterval/{print $2}')
    if [ "${ka:-0}" -eq 0 ]; then
        echo "  ⚠️  sshd 未开启 keepalive(ClientAliveInterval=0)"
        echo "  修复命令(在 Pi 上执行):"
        echo "    sudo sh -c 'echo \"ClientAliveInterval 15\" >> /etc/ssh/sshd_config'"
        echo "    sudo sh -c 'echo \"ClientAliveCountMax 3\" >> /etc/ssh/sshd_config'"
        echo "    sudo systemctl restart sshd"
    else
        echo "  ✅ ClientAliveInterval=${ka}s(已配置)"
    fi

    echo ""
    echo "════════════════════════════════════════"
}

# ── 入口 ──────────────────────────────────────────────────────

case "${1:-}" in
    --daemon)
        echo "后台采样启动(间隔 ${INTERVAL}s),日志: $SNAP_LOG"
        echo "停止: kill \$(pgrep -f ssh_hang_diag)"
        while true; do
            take_snapshot
            sleep "$INTERVAL"
        done
        ;;
    --snap)
        take_snapshot
        ;;
    --report)
        generate_report
        ;;
    *)
        echo "用法:"
        echo "  bash ssh_hang_diag.sh --daemon   # 后台持续采样(建议跑1小时以上)"
        echo "  bash ssh_hang_diag.sh --snap     # 单次快照"
        echo "  bash ssh_hang_diag.sh --report   # 生成分析报告"
        ;;
esac

绝对可以! 这份脚本写得非常专业且对症,它简直就是为了抓出树莓派 SSH 断线元凶而量身定制的“电子法医”。

SSH 断线通常感觉像是一门“玄学”,但归根结底无非是网络层、硬件层或软件配置的问题。你上传的这个脚本,刚好完美覆盖了这三大类嫌疑犯。

🔍 这个脚本能帮你检测出什么?

  1. WiFi 信号盲区/波动 (网络层)

    它会每 10 秒记录一次你的 tx/rx bitrate(收发速率)和 signal(信号强度,单位 dBm)。如果报告显示信号经常掉到 -75dBm 以下,说明 Pi 400 所在的位置有物理遮挡或干扰,导致 SSH 缓冲区积压最终卡死。

  2. 路由器/网关丢包 (网络层)

    脚本会高频 Ping 你的默认网关(路由器)。如果出现局域网内的严重丢包(gw_loss),说明是路由器负载太高,或者是 WiFi 频段(比如 2.4GHz)太拥堵了。

  3. CPU 过热降频 (硬件层)

    Pi 400 如果持续跑重负载任务,温度超过 70°C(temp),系统会触发降频自保。在极端情况下,过热会导致 WiFi 芯片组极其不稳定甚至掉线。

  4. NAT 会话超时/保活缺失 (软件层)

    这也是最常见的原因。脚本最后的报告功能会直接读取系统配置,检查你的 Pi 是否开启了 ClientAliveInterval(心跳保活包)。如果没开,路由器会无情地踢掉长时间没有数据传输的安静连接。


🚀 最佳食用指南(实战操作)

为了抓到现行,我建议你按照以下步骤来跑这个脚本:

第一步:在“保护罩”里启动后台监控

因为你的 SSH 会断线,如果直接运行脚本,断线时脚本可能也会跟着被杀掉。所以我们要用之前提到的 screentmux 来跑它:

Bash

screen -S ssh_monitor
bash ssh_hang_diag.sh --daemon

按下 Ctrl+A 然后按 D 把窗口切到后台,让它自己静静地记录。

第二步:该干嘛干嘛,等待案发

去正常使用你的 Pi,或者干脆放着不管,直到下一次 SSH 卡死、断线发生。

第三步:重连并提取“尸检报告”

当你发现 SSH 断了,重新连上去,然后直接运行报告命令:

Bash

bash ssh_hang_diag.sh --report

此时,脚本会把过去的采样数据进行汇总,直接告诉你主因是什么(是 WiFi 弱、经常丢包,还是因为没开 Keepalive)。拿着这份报告,你就能一击必杀,彻底解决断线问题了。