From b4148c9571c55f6deb207a341c0ead38932924c2 Mon Sep 17 00:00:00 2001 From: ETWen Date: Sun, 6 Sep 2026 23:22:50 +0800 Subject: [PATCH] feat(dsc): Add the SerDes DSC scan and its offline CSV parser bcm_dsc_scan.sh runs bcmcmd -n -c "dsh -c 'phydiag dsc'" over the whole port list (one 100G port plus the 200G ranges, ~445 in total) on both units, writes the raw log under /log, and copies it to /host/hw-eval/jobs. -f names the log (%u -> u0/u1, %t -> timestamp), -F overwrites, -u picks one unit, -n dry runs, -C skips the copy. Two things it deliberately does not do: - judge success by bcmcmd's exit code. The diag shell rejects commands and bcmcmd still exits 0, so the script counts "SERDES DISPLAY DIAG DATA END" markers instead. Same trap blanton_traffic_linespeed.sh hit. - resolve ~. Script A calls it after sudo -i, so paths are derived from $0 rather than $HOME. Output is one line per port when stdout is not a tty, so it does not flood the Tera Term log, and it ends with "### DSC_SCAN_DONE rc=" for a TTL wait to key on. bcm_dsc_parse.sh is the host-side half: raw log -> _lane.csv (TXEQ / RXFFE / DFE / EYE / SNR per lane) and _pm.csv (per pm_id). POSIX sh + awk, no Python. Lane rows are read by the order of their bracket groups rather than by fixed columns, so the SDK re-spacing its output does not break parsing; a row whose group count is not 9 is reported as UNPARSED on stderr instead of being parsed wrongly. A full scan is about 10 minutes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_014RetWKFZFG1ZHcitQAyhwM --- .../Blanton_Script/bcm_dsc_parse.sh | 204 +++++++++++ .../Blanton_Script/bcm_dsc_scan.sh | 335 ++++++++++++++++++ 2 files changed, 539 insertions(+) create mode 100644 src/Script_ABC_Blanton/Blanton_Script/bcm_dsc_parse.sh create mode 100644 src/Script_ABC_Blanton/Blanton_Script/bcm_dsc_scan.sh diff --git a/src/Script_ABC_Blanton/Blanton_Script/bcm_dsc_parse.sh b/src/Script_ABC_Blanton/Blanton_Script/bcm_dsc_parse.sh new file mode 100644 index 0000000..e164df8 --- /dev/null +++ b/src/Script_ABC_Blanton/Blanton_Script/bcm_dsc_parse.sh @@ -0,0 +1,204 @@ +#!/bin/sh +# ============================================================================= +# bcm_dsc_parse_V1.0.0.sh +# +# Author : ETWen +# Date : 20260906 +# +# Version History +# V1.0.0 20260906 Initial Version +# +# Description: +# Parse Broadcom diag shell "phydiag dsc" raw log into CSV. +# 支援多個 pm_id 串接在同一份 log,以 "pm_id =" 當 record separator。 +# +# Output: +# _lane.csv : 每個 lane 一列(TXEQ / RXFFE / DFE / EYE / SNR ...) +# _pm.csv : 每個 pm_id 一列(溫度 / PLL / lane 數 / SNR-EYE 統計) +# +# Usage: +# ./bcm_dsc_parse_V1.0.0.sh -i dsc_raw.log [-o out_prefix] +# +# Design notes: +# - 純 POSIX sh + awk(mawk 相容),不依賴 Python。 +# - Lane row 採「括號群組出現順序」抽取,而非固定欄位位置,對排版空白 +# 變動免疫。SDK 若增減欄位,群組數不等於 9,該列會被標記 UNPARSED +# 並輸出到 stderr,不會靜默解析錯誤。 +# - 已知 header 名稱數與資料欄數不一致之處: +# * (P RX, CDRxN, UC_CFG, UC_STS, RST, STP) 6 個名字 / 7 個值 +# (RST 實際是 "0,0" 兩欄) +# * TXEQ 尾端多一個逗號 +# * AVG_TMON 顯示為 "(11) 66C" 兩個 token +# - SD / LCK 的 "*" 為 sticky 旗標,本版拆成獨立欄位保留,不做判讀。 +# - BER 欄若為 "!chk_en" 表示 checker 未開啟,值無效,原樣保留。 +# ============================================================================= + +set -e + +IN="" +PREFIX="dsc" + +usage() { + echo "Usage: $0 -i [-o ]" >&2 + exit 1 +} + +while getopts "i:o:h" opt; do + case "$opt" in + i) IN="$OPTARG" ;; + o) PREFIX="$OPTARG" ;; + *) usage ;; + esac +done + +[ -n "$IN" ] || usage +[ -f "$IN" ] || { echo "ERROR: input not found: $IN" >&2; exit 2; } + +LANE_CSV="${PREFIX}_lane.csv" +PM_CSV="${PREFIX}_pm.csv" + +# 先去掉 CR(console / telnet log 常帶),再交給 awk +tr -d '\r' < "$IN" | awk -v LANE_CSV="$LANE_CSV" -v PM_CSV="$PM_CSV" ' +function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s } + +# 掃描一列,取出所有頂層括號群組到 G[],群組前的英數 prefix 到 P[], +# 其餘純量文字留在 REST。回傳群組數。 +function split_groups(line, i, ch, d, buf, ng, rest, prefix) { + split("", G, ":"); split("", P, ":") + ng = 0; d = 0; buf = ""; rest = ""; prefix = "" + for (i = 1; i <= length(line); i++) { + ch = substr(line, i, 1) + if (ch == "(") { + d++ + if (d == 1) { + buf = ""; prefix = "" + if (match(rest, /[A-Za-z_]+$/)) { + prefix = substr(rest, RSTART, RLENGTH) + rest = substr(rest, 1, RSTART - 1) + } + continue + } + } + if (ch == ")") { + d-- + if (d == 0) { ng++; G[ng] = buf; P[ng] = prefix; rest = rest " "; continue } + } + if (d > 0) { buf = buf ch; continue } + rest = rest ch + } + REST = rest + return ng +} + +# 取括號群組的第 n 個子欄位(以逗號分隔) +function gf(idx, n, a, c) { + c = split(G[idx], a, ",") + if (n > c) return "" + return trim(a[n]) +} + +function is_num(s) { return (s ~ /^[+-]?[0-9]+(\.[0-9]+)?$/) } + +function reset_pm() { + core = ""; sdtype = ""; hw_lanes = ""; lane_sel = "" + live_temp = ""; avg_tmon = ""; pll_lock = "" + n_lane = 0; snr_min = ""; snr_max = ""; eye_min = ""; all_lck = 1 +} + +function flush_pm() { + if (pm == "") return + printf "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", + pm, core, sdtype, hw_lanes, lane_sel, n_lane, + live_temp, avg_tmon, pll_lock, + (snr_min == "" ? "NA" : snr_min), + (eye_min == "" ? "NA" : eye_min), + (n_lane == 0 ? "NA" : (all_lck ? "PASS" : "FAIL")) > PM_CSV +} + +BEGIN { + pm = ""; in_lane = 0; core_row = 0; bad = 0 + print "pm_id,core,lane,serdes_type,sd,sd_sticky,lck,lck_sticky,rxppm,txppm," \ + "txeq_n3,txeq_n2,txeq_n1,txeq_m,txeq_p1,txeq_p2,txeq_sum," \ + "rxffe_n3,rxffe_n2,rxffe_n1,rxffe_m,rxffe_p1,rxffe_p2," \ + "dfe1,dfe2,pf_m,pf_l,pf_h,vga,dco,tp0,tp1,tp2,flt_m,flt_s," \ + "nlc_u,nlc_l,eye_u,eye_m,eye_l,link_time,snr,ber," \ + "live_temp,avg_tmon,pll_lock,rx_pol,cdr,uc_cfg,uc_sts,rst,stp" > LANE_CSV + print "pm_id,core,serdes_type,hw_lanes,lane_select,active_lanes," \ + "live_temp,avg_tmon,pll_lock,snr_min,eye_min,lck_all" > PM_CSV +} + +/^pm_id[ \t]*=/ { flush_pm(); pm = $NF; reset_pm(); in_lane = 0; next } +/^Core[ \t]*=/ { if (match($0, /Core[ \t]*=[ \t]*[0-9]+/)) { core = $0; sub(/.*Core[ \t]*=[ \t]*/, "", core); sub(/[^0-9].*/, "", core) } next } +/^SerDes type[ \t]*=/ { sdtype = $NF; next } +/^Rev ID # Lanes/ { hw_lanes = $NF; next } +/^lane_select[ \t]*=/ { lane_sel = $NF; next } + +/^CORE[ \t]+RST_ST/ { core_row = 1; next } +core_row == 1 { + line = $0 + gsub(/\([^)]*\)[ \t]*/, "", line) # 去掉 AVG_TMON 的 "(11)" + n = split(trim(line), c, /[ \t]+/) + if (n >= 15) { live_temp = c[9]; avg_tmon = c[10]; pll_lock = c[15] } + core_row = 0 + next +} + +/^LN[ \t]*\(/ { in_lane = 1; next } +/SERDES DISPLAY DIAG DATA END/ { in_lane = 0; next } + +in_lane && /^[0-9]+[ \t]*\(/ { + ng = split_groups($0) + if (ng != 9) { + printf "UNPARSED (groups=%d) pm_id=%s: %s\n", ng, pm, $0 > "/dev/stderr" + bad++ + next + } + nr = split(trim(REST), r, /[ \t]+/) + if (nr != 10) { + printf "UNPARSED (scalars=%d) pm_id=%s: %s\n", nr, pm, $0 > "/dev/stderr" + bad++ + next + } + + lane = r[1] + sd = r[2]; sd_st = (sd ~ /\*/) ? 1 : 0; gsub(/\*/, "", sd) + lck = r[3]; lck_st = (lck ~ /\*/) ? 1 : 0; gsub(/\*/, "", lck) + rxppm = r[4]; vga = r[5]; dco = r[6]; txppm = r[7] + link_time = r[8]; snr = r[9]; ber = r[10] + + # TXEQ 六個 tap 與總和(僅在全為數值時計算) + tsum = 0; tok = 1 + for (i = 1; i <= 6; i++) { + tq[i] = gf(7, i) + if (is_num(tq[i])) tsum += tq[i]; else tok = 0 + } + + printf "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,", pm, core, lane, sdtype, sd, sd_st, lck, lck_st, rxppm, txppm > LANE_CSV + printf "%s,%s,%s,%s,%s,%s,%s,", tq[1], tq[2], tq[3], tq[4], tq[5], tq[6], (tok ? tsum : "NA") > LANE_CSV + printf "%s,%s,%s,%s,%s,%s,", gf(4,1), gf(4,2), gf(4,3), gf(4,4), gf(4,5), gf(4,6) > LANE_CSV + printf "%s,%s,", gf(5,1), gf(5,2) > LANE_CSV + printf "%s,%s,%s,%s,%s,", gf(2,1), gf(2,2), gf(2,3), vga, dco > LANE_CSV + printf "%s,%s,%s,%s,%s,", gf(3,1), gf(3,2), gf(3,3), gf(6,1), gf(6,2) > LANE_CSV + printf "%s,%s,%s,%s,%s,", gf(8,1), gf(8,2), gf(9,1), gf(9,2), gf(9,3) > LANE_CSV + printf "%s,%s,%s,", link_time, snr, ber > LANE_CSV + printf "%s,%s,%s,", live_temp, avg_tmon, pll_lock > LANE_CSV + printf "%s,%s,%s,%s,%s/%s,%s\n", gf(1,1), gf(1,2), gf(1,3), gf(1,4), gf(1,5), gf(1,6), gf(1,7) > LANE_CSV + + # per-PM 統計 + n_lane++ + if (lck != "1" || sd != "1") all_lck = 0 + if (is_num(snr)) { if (snr_min == "" || snr + 0 < snr_min + 0) snr_min = snr } + for (i = 1; i <= 3; i++) { + e = gf(9, i) + if (is_num(e)) { if (eye_min == "" || e + 0 < eye_min + 0) eye_min = e } + } + next +} + +END { + flush_pm() + if (bad > 0) printf "WARN: %d lane row(s) unparsed\n", bad > "/dev/stderr" +} +' + +echo "OK: ${LANE_CSV}, ${PM_CSV}" diff --git a/src/Script_ABC_Blanton/Blanton_Script/bcm_dsc_scan.sh b/src/Script_ABC_Blanton/Blanton_Script/bcm_dsc_scan.sh new file mode 100644 index 0000000..40fbb30 --- /dev/null +++ b/src/Script_ABC_Blanton/Blanton_Script/bcm_dsc_scan.sh @@ -0,0 +1,335 @@ +#!/bin/sh +# ============================================================================= +# bcm_dsc_scan.sh +# +# Author : ETWen +# Date : 20260906 +# +# Version History +# V1.0.0 20260906 Initial Version +# V1.1.0 20260906 Log 預設輸出至 /log,掃描後自動複製到 +# /host/hw-eval/jobs/;非 tty 環境改用逐行進度輸出, +# 避免灌爆 Tera Term logopen 的 log +# V1.2.0 20260906 新增 -f 自訂 log 檔名(支援 %u / %t 佔位符)與 +# -F 強制覆蓋;預設不覆蓋既有檔案,自動加序號 +# +# Description: +# 在 Blanton SONiC DUT 上批次執行 Broadcom diag shell 的 +# "phydiag dsc",收集 raw log 供離線解析。 +# +# 實際下的指令為: +# bcmcmd -n -c "dsh -c 'phydiag dsc'" +# +# Deployment: +# Script : /root/Blanton_Script/bcm_dsc_scan.sh +# Log : /root/Blanton_Script/log/dsc_raw_u_.log +# Copy : /host/hw-eval/jobs/ +# +# Log 檔名可用 -f 覆寫,支援兩個佔位符: +# %u -> u0 / u1 +# %t -> YYYYmmdd_HHMMSS +# -f 只接受檔名,不接受路徑;目錄一律由 -o 決定。 +# 同時掃兩個 unit 但檔名未含 %u 時,會自動在副檔名前插入 _u, +# 避免第二個 unit 覆蓋掉第一個。 +# +# 路徑由 $0 推導,不依賴 $HOME。Script A 以 sudo -i 執行時 ~ 為 /root, +# 直接寫死 ~ 會有風險,故一律使用絕對路徑。 +# +# Usage: +# ./bcm_dsc_scan.sh # unit 0 與 1 都掃,掃完自動複製 +# ./bcm_dsc_scan.sh -u 0 # 只掃 unit 0 +# ./bcm_dsc_scan.sh -n # dry-run,只列出要掃的 port +# ./bcm_dsc_scan.sh -C # 不複製到 /host/hw-eval/jobs +# ./bcm_dsc_scan.sh -c /other/dir # 指定複製目的地 +# ./bcm_dsc_scan.sh -u 0 -f dsc_before_reboot.log +# ./bcm_dsc_scan.sh -f blanton_dsc_%u_%t.log +# +# Exit code: +# 0 = 全部 port 皆取得完整 dump +# 1 = 有 port 未取得完整 dump,或複製失敗 +# 2 = 參數 / 路徑錯誤 +# 3 = 找不到 bcmcmd +# +# 結束時固定輸出一行 "### DSC_SCAN_DONE rc=",供 TTL 端 wait 判斷。 +# +# Design notes: +# - 純 POSIX sh,不依賴 bash / Python。 +# - Port 清單以「範圍字串」定義在下方 PORTS_* 變數,展開後去重。 +# 100G 清單先展開,故重複的 port 會被判定為 100G(目前僅 port 268)。 +# - bcmcmd 即使指令失敗通常仍回傳 0,因此不以 exit code 判斷成功與否; +# 改由 marker 行 + "SERDES DISPLAY DIAG DATA END" 數量比對。 +# - 先寫入本機 log 目錄再複製,而非直接寫遠端,避免中途失敗遺失整份資料。 +# +# Assumptions: +# - unit 0 與 unit 1 使用相同的 port 清單。 +# - phydiag 的參數為此清單中的 port 編號(非 port name)。 +# - 由 root 執行(Script A 的 sudo -i 之後),故不呼叫 sudo。 +# +# Cautions: +# - 全清單約 445 port x 2 unit,估計 10 分鐘,請勿把 DELAY 設為 0。 +# - phydiag ... dsc 為唯讀操作,不會變更 SerDes 設定。 +# ============================================================================= + +set -u + +# ---------------------------------------------------------------------------- +# Port list (以空白分隔的範圍或單一值) +# ---------------------------------------------------------------------------- +PORTS_100G="268" +PORTS_200G="1-24 40-96 112-168 184-240 256-308 310-327 342-380 382-399 414-452 454-471 486-524 526-543 558-565" + +# ---------------------------------------------------------------------------- +# Paths / defaults +# ---------------------------------------------------------------------------- +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) || exit 2 + +UNITS="0 1" +OUTDIR="${SCRIPT_DIR}/log" +COPYDIR="/host/hw-eval/jobs" +DELAY="0.2" +DRYRUN=0 +DOCOPY=1 +LOGNAME="" # -f 指定的 log 檔名,空字串表示使用預設命名 +FORCE=0 # -F 允許覆蓋既有檔案 +PROGRESS_EVERY=50 # 非 tty 環境每 N 筆印一行進度 + +usage() { + cat >&2 << 'USAGE' +Usage: bcm_dsc_scan.sh [-u <0|1>] [-o ] [-f ] [-F] + [-c ] [-C] [-d ] [-n] [-h] + + -u 只掃指定 unit。省略則 unit 0 與 1 都掃。 + -o log 輸出目錄 (default: /log) + -f 自訂 log 檔名(僅檔名,不含路徑) + 佔位符: %u -> u0/u1 , %t -> YYYYmmdd_HHMMSS + default: dsc_raw_%u_%t.log + -F 允許覆蓋既有同名檔案 (default: 自動加序號 _1 / _2 ...) + -c 掃描後複製目的地 (default: /host/hw-eval/jobs) + -C 不複製 + -d 每筆 phydiag 之間的間隔 (default: 0.2) + -n dry-run,列出展開後的 port 清單與指令,不實際執行 + -h 顯示此說明 +USAGE + exit 1 +} + +while getopts "u:o:f:Fc:Cd:nh" opt; do + case "$opt" in + u) + case "$OPTARG" in + 0|1) UNITS="$OPTARG" ;; + *) echo "ERROR: -u 只接受 0 或 1 (got: $OPTARG)" >&2; exit 2 ;; + esac + ;; + o) OUTDIR="$OPTARG" ;; + f) + case "$OPTARG" in + "") echo "ERROR: -f 不可為空" >&2; exit 2 ;; + */*) echo "ERROR: -f 只接受檔名,路徑請用 -o (got: $OPTARG)" >&2; exit 2 ;; + *) LOGNAME="$OPTARG" ;; + esac + ;; + F) FORCE=1 ;; + c) COPYDIR="$OPTARG" ;; + C) DOCOPY=0 ;; + d) DELAY="$OPTARG" ;; + n) DRYRUN=1 ;; + *) usage ;; + esac +done + +mkdir -p "$OUTDIR" 2>/dev/null || { echo "ERROR: cannot create outdir: $OUTDIR" >&2; exit 2; } +[ -w "$OUTDIR" ] || { echo "ERROR: outdir not writable: $OUTDIR" >&2; exit 2; } + +# ---------------------------------------------------------------------------- +# sleep 是否支援小數秒(busybox sleep 可能只吃整數) +# ---------------------------------------------------------------------------- +if ! sleep "$DELAY" 2>/dev/null; then + echo "WARN: sleep '$DELAY' 不支援,改用 sleep 1" >&2 + DELAY="1" +fi + +# ---------------------------------------------------------------------------- +# 展開 port 清單。100G 先展開,重複者以 100G 為準。 +# 輸出格式: ",",依 port 數值排序。 +# ---------------------------------------------------------------------------- +PORTFILE=$(mktemp) || { echo "ERROR: mktemp failed" >&2; exit 2; } +trap 'rm -f "$PORTFILE"' EXIT +trap 'echo "" >&2; echo "### DSC_SCAN_DONE rc=130 (INTERRUPTED)" >&2; rm -f "$PORTFILE"; exit 130' INT TERM + +awk -v a="$PORTS_100G" -v b="$PORTS_200G" ' +function expand(s, spd, n, i, j, p, k, lo, hi) { + n = split(s, p, /[ \t]+/) + for (i = 1; i <= n; i++) { + if (p[i] == "") continue + if (split(p[i], k, "-") == 2) { lo = k[1] + 0; hi = k[2] + 0 } + else { lo = p[i] + 0; hi = lo } + if (hi < lo) { printf "ERROR: bad range %s\n", p[i] > "/dev/stderr"; continue } + for (j = lo; j <= hi; j++) if (!(j in SPD)) SPD[j] = spd + } +} +BEGIN { + expand(a, "100G") # 先展開 100G,重複的 port 由 100G 取得 + expand(b, "200G") + for (p in SPD) print p "," SPD[p] +}' | sort -t, -k1,1n > "$PORTFILE" + +TOTAL=$(wc -l < "$PORTFILE" | tr -d ' ') +[ "$TOTAL" -gt 0 ] || { echo "ERROR: port list is empty" >&2; exit 2; } + +N100=$(grep -c ',100G$' "$PORTFILE" || true) +N200=$(grep -c ',200G$' "$PORTFILE" || true) + +echo "Script dir : $SCRIPT_DIR" >&2 +echo "Log dir : $OUTDIR" >&2 +echo "Port list : $TOTAL ports (200G x2: $N200, 100G: $N100)" >&2 +echo "Units : $UNITS" >&2 +echo "Delay : ${DELAY}s" >&2 + +# ---------------------------------------------------------------------------- +# Dry-run +# ---------------------------------------------------------------------------- +if [ "$DRYRUN" -eq 1 ]; then + for U in $UNITS; do + echo "--- unit $U ---" + while IFS=, read -r P SPD; do + printf 'bcmcmd -n %s -c "dsh -c '\''phydiag %s dsc'\''" # %s\n' "$U" "$P" "$SPD" + done < "$PORTFILE" + done + echo "### DSC_SCAN_DONE rc=0 (dry-run)" >&2 + exit 0 +fi + +command -v bcmcmd > /dev/null 2>&1 || { + echo "ERROR: bcmcmd not found in PATH" >&2 + echo "### DSC_SCAN_DONE rc=3" >&2 + exit 3 +} + +# stderr 是 tty 才用 \r 進度條;被導向檔案(Tera Term logopen)時改逐行輸出 +if [ -t 2 ]; then TTY=1; else TTY=0; fi + +# ---------------------------------------------------------------------------- +# Log 檔名處理 +# ---------------------------------------------------------------------------- + +# 純 POSIX 的字串取代,避免 sed 對檔名中的特殊字元誤判 +replace_all() { # $1=haystack $2=needle $3=replacement + _s=$1; _n=$2; _r=$3; _out="" + while :; do + case "$_s" in + *"$_n"*) + _out="${_out}${_s%%"$_n"*}${_r}" + _s=${_s#*"$_n"} + ;; + *) _out="${_out}${_s}"; break ;; + esac + done + printf '%s' "$_out" +} + +# 依 unit 組出 log 檔名。未指定 -f 時使用預設命名。 +build_logname() { # $1=unit + _u=$1 + _n=${LOGNAME:-dsc_raw_%u_%t.log} + case "$_n" in *%u*) _has_u=1 ;; *) _has_u=0 ;; esac + _n=$(replace_all "$_n" '%u' "u${_u}") + _n=$(replace_all "$_n" '%t' "$TS") + # 掃兩個 unit 但檔名沒有 %u,第二個 unit 會覆蓋第一個,故自動加上 _u + if [ "$_has_u" -eq 0 ] && [ "$MULTI" -eq 1 ]; then + case "$_n" in + *.*) _n="${_n%.*}_u${_u}.${_n##*.}" ;; + *) _n="${_n}_u${_u}" ;; + esac + fi + printf '%s' "$_n" +} + +# 檔案已存在時自動加序號,避免覆蓋掉先前的掃描結果(-F 可強制覆蓋) +uniquify() { # $1=filename + _f=$1 + [ -e "${OUTDIR}/${_f}" ] || { printf '%s' "$_f"; return; } + if [ "$FORCE" -eq 1 ]; then + echo "WARN: 覆蓋既有檔案 ${OUTDIR}/${_f}" >&2 + printf '%s' "$_f"; return + fi + case "$_f" in + *.*) _b=${_f%.*}; _e=".${_f##*.}" ;; + *) _b=$_f; _e="" ;; + esac + _i=1 + while [ -e "${OUTDIR}/${_b}_${_i}${_e}" ]; do _i=$((_i + 1)); done + echo "WARN: ${_f} 已存在,改用 ${_b}_${_i}${_e}(-F 可強制覆蓋)" >&2 + printf '%s' "${_b}_${_i}${_e}" +} + +MULTI=0 +[ "$(echo "$UNITS" | wc -w | tr -d ' ')" -gt 1 ] && MULTI=1 + +# ---------------------------------------------------------------------------- +# Scan +# ---------------------------------------------------------------------------- +TS=$(date +%Y%m%d_%H%M%S) +RC=0 +LOGS="" + +for U in $UNITS; do + LOG="${OUTDIR}/$(uniquify "$(build_logname "$U")")" + : > "$LOG" || { echo "ERROR: cannot write $LOG" >&2; exit 2; } + LOGS="$LOGS $LOG" + + printf '### SCAN_BEGIN unit=%s time=%s total=%s script=bcm_dsc_scan_V1.1.0\n' \ + "$U" "$(date '+%Y-%m-%d %H:%M:%S')" "$TOTAL" >> "$LOG" + + echo "[unit $U] scanning $TOTAL ports -> $LOG" >&2 + + SEQ=0 + while IFS=, read -r P SPD; do + SEQ=$((SEQ + 1)) + printf '### PORT unit=%s port=%s speed=%s seq=%s/%s\n' "$U" "$P" "$SPD" "$SEQ" "$TOTAL" >> "$LOG" + bcmcmd -n "$U" -c "dsh -c 'phydiag $P dsc'" >> "$LOG" 2>&1 || RC=1 + + if [ "$TTY" -eq 1 ]; then + printf '\r[unit %s] %s/%s port=%-5s' "$U" "$SEQ" "$TOTAL" "$P" >&2 + elif [ $((SEQ % PROGRESS_EVERY)) -eq 0 ] || [ "$SEQ" -eq "$TOTAL" ]; then + printf '[unit %s] %s/%s port=%s\n' "$U" "$SEQ" "$TOTAL" "$P" >&2 + fi + + sleep "$DELAY" + done < "$PORTFILE" + + [ "$TTY" -eq 1 ] && printf '\n' >&2 + printf '### SCAN_END unit=%s time=%s\n' "$U" "$(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG" + + # 概略檢查:每筆 PORT marker 應對應一個 dsc 區塊 + MARK=$(grep -c '^### PORT ' "$LOG" || true) + DUMP=$(grep -c 'SERDES DISPLAY DIAG DATA END' "$LOG" || true) + if [ "$MARK" -ne "$DUMP" ]; then + echo "WARN: unit $U markers=$MARK dsc_blocks=$DUMP (有 port 未取得完整 dump)" >&2 + RC=1 + fi + echo "Saved: $LOG (${DUMP}/${MARK} dumps)" >&2 +done + +# ---------------------------------------------------------------------------- +# Copy to job directory +# ---------------------------------------------------------------------------- +if [ "$DOCOPY" -eq 1 ]; then + if mkdir -p "$COPYDIR" 2>/dev/null && [ -w "$COPYDIR" ]; then + for L in $LOGS; do + if cp -p "$L" "$COPYDIR/"; then + echo "Copied: $COPYDIR/$(basename "$L")" >&2 + else + echo "ERROR: copy failed: $L -> $COPYDIR" >&2 + RC=1 + fi + done + sync + else + echo "ERROR: copydir not writable: $COPYDIR (log 仍保留在 $OUTDIR)" >&2 + RC=1 + fi +fi + +echo "### DSC_SCAN_DONE rc=${RC}" >&2 +exit "$RC"