feat(margin): Save every LTC2977 in one command, and stop lying about failures

margin_save discarded the result of every write, so a NACK printed
"Change Saved" exactly like a successful store. For the one permanent
operation in this tool that is the worst place to stay quiet. Each store
is now checked, named on failure, and reflected in the exit code.

STORE_USER_ALL also went out as 0x15 followed by a dummy 0x00 - an SMBus
write-byte where the command is a send-byte. The LTC2977 tolerated it,
but tolerance is not correctness. Now i2cset ... 0x15 c on the native
path and a no-data cb_pmbus_write on the FPGA path.

Two new subcommands:

  margin_save all      - all nine boards in settings/, then re-init
                         whichever board was loaded beforehand so a
                         following margin_status still reports the board
                         you were looking at. Does not stop on the first
                         failure; a half-saved set is harder to reason
                         about than a fully-attempted one.

  margin_save blanton  - this platform exposes the CB FPGA F3 I2C
                         channels as native Linux i2c buses (Ch6->13,
                         Ch7->14, Ch8->15, Ch9->16; CB stays on bus 4),
                         so all 18 LTC2977 can be stored with plain
                         i2cset: no margin_init, no FPGA channel setup,
                         no pcimem. 18 commands instead of 42.

The 18 raw commands were verified on the DUT 2026-08-27; the wrapper was
tested against stubbed hardware only. Release notes say so.

Board list is MARGIN_BLANTON_STORE at the top of margin.sh - comment out
what this DUT does not have rather than letting it talk to absent boards.
Missing bus and unresponsive chip are reported as distinct failures.

MARGIN_STORE_SETTLE (0.5s) sits between stores because nothing here polls
the part's busy bit, and blanton fires 18 in a row.

Script A -> V1.0.11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RetWKFZFG1ZHcitQAyhwM
This commit is contained in:
2026-08-28 08:39:31 +08:00
co-authored by Claude Opus 5
parent 15ea776b82
commit 4854da8518
8 changed files with 443 additions and 12 deletions
+22 -2
View File
@@ -1,7 +1,7 @@
; =============================================================================
; Script A for Blanton
; Version : V1.0.10
; Date : 2026-08-27
; Version : V1.0.11
; Date : 2026-08-28
; Author : ETWen
; =============================================================================
; Version History:
@@ -74,6 +74,26 @@
; tools/100G_PRBS.txt Bench command references, kept for hand-run debug
; tools/TR518.txt TR518 = built-in packet test (swutil / bcmcmd tr 518)
; tools/fan_ctrl.txt MAX31790 rebind + fan-speed-control.sh
; V1.0.11 2026-08-28 LTC2980_Margin_Script/margin.sh v2.7.0 - margin_save fixes + two new subcommands
; STORE_USER_ALL (0x15) is now a PMBus send-byte, not
; 0x15 followed by a dummy 0x00 data byte. The LTC2977
; tolerated the old form, but it was the wrong protocol
; i2c: i2cset .. 0x15 c swb: cb_pmbus_write <ch> <addr> 0x15
; + margin_save all - every board in settings/ (9 boards,
; 18 chips), then re-init the one loaded beforehand
; + margin_save blanton - Blanton SONiC shortcut: all 18
; LTC2977 straight over the native i2c buses - no
; margin_init, no FPGA channel setup, no pcimem
; F3 Ch6->bus 13, Ch7->14, Ch8->15, Ch9->16
; board list = MARGIN_BLANTON_STORE at the top of margin.sh
; The store result is now checked - a NACK used to still
; print "Change Saved"
; MARGIN_STORE_SETTLE (0.5s) between stores; nothing in this
; tool polls the part's busy bit
; LTC2980_Margin_Script/README.md What each margin_save variant sends, and the
; FPGA-channel -> i2c-bus map
; LTC2980_Margin_Script/Ref/*.md 0x15 trace note corrected to send-byte
; LTC2980_Margin_Script/script/margin_save_all.sh Point at margin_save all; keep it for subsets
; =============================================================================
include "config.ttl"
@@ -14,6 +14,8 @@
| `margin_operation <ch> <op>` | 只切 OPERATION register |
| `margin_apply_profile <file>` | 批次套用 profile(套用在當前載入的 LTC2980 |
| `margin_save` | 寫入 NVM(當前 LTC2980 的兩個 LTC2977 都寫) |
| `margin_save all` | 對 `settings/` 底下**全部** LTC2980 寫入 NVM,結束後回到原本載入的那顆 |
| `margin_save blanton` | **Blanton SONiC 專用**18 顆 LTC2977 全部走原生 i2c bus 寫入,不需要 `margin_init` |
| `margin_debug [on\|off]` | 開關 debug 模式(不帶參數則 toggle |
| `margin_log [on\|off]` | 開關 log 記錄(不帶參數則 toggle,預設開啟) |
@@ -79,7 +81,9 @@ margin_init Blanton_SWB1_CONN16
margin_apply_profile profiles/comboA.conf
# 儲存到 NVM(斷電後保留)
margin_save
margin_save # 只寫當前載入的那顆
margin_save all # settings/ 底下 9 顆全部寫,最後自動切回原本那顆
margin_save blanton # Blanton SONiC 專用捷徑,18 顆一次寫完(見下)
```
## 一次掃所有 LTC2980
@@ -131,16 +135,87 @@ PROFILES=(
## 一次把所有 LTC2980 寫入 NVM
`script/margin_save_all.sh``BOARDS` 列出的每顆 LTC2980 跑 `margin_save`
> ⚠️ `margin_save` 會把當前 margin / OV / UV 設定**永久寫入 NVM**
> 斷電後保留。執行前先用 `margin_status_all.sh` 確認狀態正確。
**全部 9 顆**直接用內建的:
```bash
margin_save all
```
`settings/*.conf` 的檔名順序跑,中途某顆失敗**不會停**(半套完成的狀態比全部試完更難查),
最後印出「幾顆成功 / 哪幾顆失敗」的總結,並把 `margin_init` 切回你呼叫前載入的那顆。
**只寫其中幾顆**才需要 `script/margin_save_all.sh`,編 script 頂端的 `BOARDS` 陣列:
```bash
source script/margin_save_all.sh
```
要改寫哪幾顆,編 script 頂端的 `BOARDS` 陣列即可(同前述)。
### 送出去的是什麼
`margin_save` 對每顆 LTC2977 送一次 `STORE_USER_ALL`PMBus command `0x15`),
**不分 page**(這是整顆晶片的命令),也**不帶 data byte** —— 它是 SMBus send-byte
```bash
i2cset -y 4 0x5C 0x15 c # CB CONN13(原生 bus 4
cb_pmbus_write 8 0x5C 0x15 # SWB0 CONN13CB FPGA F3 Ch8
```
> v2.7.0 之前送的是 `0x15 0x00`write-byte)。LTC2977 在 CB 上吃得下去,但那是零件寬容,
> 不是規範。同時 v2.7.0 起會檢查回傳值 —— 以前 NACK 了還是印 `Change Saved`。
每顆之間會 `sleep $MARGIN_STORE_SETTLE`(預設 0.5 秒)。這不是量測值,是保守的保護:
NVM 寫入需要時間,而這支工具**沒有**去 poll 零件的 busy bit。`margin_save all` 連續送 18 次,
這個間隔就是唯一的緩衝。要關掉設成空字串即可。
## `margin_save blanton` — Blanton SONiC 專用
Blanton 這台把 CB FPGA F3 的 I2C 通道**同時掛成了原生 Linux i2c adapter**
所以 SWB 上的 LTC2977 不必走 `cb_pmbus_*` / pcimem,用 `i2cset` 直接打得到:
| FPGA F3 通道 | Linux bus | 板 / 接頭 |
|---|---|---|
| Ch6 | 13 | SWB0 CONN14 / CONN16 |
| Ch8 | 15 | SWB0 CONN13 / CONN15 |
| Ch7 | 14 | SWB1 CONN14 / CONN16 |
| Ch9 | 16 | SWB1 CONN13 / CONN15 |
| —(原生) | 4 | CB CONN13 |
因此 `margin_save blanton` 就是一張平表跑完 18 顆,**不需要 `margin_init`**、
不做 FPGA 通道初始化、不做 probe,也不會動到你當前載入的那顆板子:
```bash
margin_save blanton
```
```
CB CONN13 bus 4 0x5C : saved
SWB0 CONN13 bus 15 0x5C : saved
...
margin_save blanton: 18 saved, 0 failed
```
送出去的就是這 18 行(2026-08-27 上機驗證):
```bash
i2cset -f -y 4 0x5C 0x15 ; i2cset -f -y 4 0x5E 0x15 # CB CONN13
i2cset -f -y 15 0x5C 0x15 ; i2cset -f -y 15 0x5E 0x15 # SWB0 CONN13
i2cset -f -y 13 0x5C 0x15 ; i2cset -f -y 13 0x5E 0x15 # SWB0 CONN14
i2cset -f -y 15 0x62 0x15 ; i2cset -f -y 15 0x64 0x15 # SWB0 CONN15
i2cset -f -y 13 0x62 0x15 ; i2cset -f -y 13 0x64 0x15 # SWB0 CONN16
i2cset -f -y 16 0x5C 0x15 ; i2cset -f -y 16 0x5E 0x15 # SWB1 CONN13
i2cset -f -y 14 0x5C 0x15 ; i2cset -f -y 14 0x5E 0x15 # SWB1 CONN14
i2cset -f -y 16 0x62 0x15 ; i2cset -f -y 16 0x64 0x15 # SWB1 CONN15
i2cset -f -y 14 0x62 0x15 ; i2cset -f -y 14 0x64 0x15 # SWB1 CONN16
```
`-f` 是因為這些 bus 上的 LTC2977 可能已被 kernel driver 認走;上機驗證過的指令帶 `-f`,就照原樣保留。
**這台沒裝滿的話**,把 `margin.sh` 頂端 `MARGIN_BLANTON_STORE` 陣列裡對應的行註解掉即可,
不要讓它去打不存在的板子。bus 節點不存在(`/dev/i2c-N` 沒有)與晶片不回應(NACK
會分別報不同訊息 —— 前者代表 FPGA 的 i2c adapter 根本沒 enumerate,後者才是板子的問題。
## Operation 對應
@@ -424,8 +424,9 @@ SWB debug 指令(margin.sh v2.6.0 新增,皆為 `blanton_cb_i2c.sh` 的薄包裝
`Blanton_SWB0_CONN14.conf` 的第一顆也是 `0:0x5E`。兩個 CONN 現在同在 Ch6,
位址相撞。對照 SWB1 的規律(CONN13=`0x5C/0x5D`、CONN14=`0x5E/0x5F`),
SWB0_CONN13 第二顆推測應為 `0:0x5D`**尚未修改,待硬體確認**
- `margin_save``0x15` + data byte(`i2cset ... 0x15 0x00 b`)。PMBus 規範
STORE_USER_ALL 是 send-byte;CB 上實測可用故保留原樣,swb 路徑沿用同一寫法。
- `margin_save``0x15` **send-byte**(`i2cset ... 0x15 c` / `cb_pmbus_write <ch> <addr> 0x15`),
不帶 data byte。v2.7.0 之前送的是 `0x15 0x00`(write-byte),CB 上實測可用,
但那是零件寬容而非規範。
---
@@ -5,6 +5,24 @@
# =============================================================================
# Version Control
# -----------------------------------------------------------------------------
# v2.7.0 margin_save fixes and batching:
# * STORE_USER_ALL (0x15) is now issued as an SMBus send-byte instead
# of a write-byte with a dummy 0x00 data byte. The LTC2977 accepted
# the old form on CB, but it was the wrong protocol.
# i2c path: i2cset ... 0x15 c swb path: cb_pmbus_write ch addr 0x15
# * margin_save all -- STORE_USER_ALL on every board in settings/,
# then re-init whichever board was loaded beforehand. Does not stop
# on the first failure; prints a per-board summary at the end.
# * The store result is now checked. It used to be discarded, so a
# NACK still printed "Change Saved".
# * MARGIN_STORE_SETTLE (default 0.5s) between stores - nothing polls
# the part's busy bit, and `all` issues 18 of them in a row.
# * margin_save blanton -- Blanton SONiC shortcut. On this platform the
# CB FPGA F3 I2C channels also appear as native Linux i2c adapters
# (Ch6->bus 13, Ch7->14, Ch8->15, Ch9->16), so all 18 LTC2977 can be
# stored with plain i2cset: no margin_init, no FPGA channel setup,
# no pcimem. Bench-verified on the DUT 2026-08-27. The board list is
# MARGIN_BLANTON_STORE at the top of this file.
# v2.6.0 SWB path actually wired to the shared CB I2C package. blanton_cb_i2c.sh
# and blanton_fpga_pcimem.sh live in the PARENT dir (Blanton_Script/), not
# next to margin.sh, so the backend is now searched in both places.
@@ -115,6 +133,45 @@ ONOFF_ENABLE_VALUE="0x1a" # controlled_on=1, use_pmbus=1, use_control=0, b1=1
# margin_status after all channels are set. Bump this if you want margin_set's own
# read-back to reflect the settled value.
MARGIN_SETTLE="0.3"
# Pause after each STORE_USER_ALL. The LTC2977 is busy committing NVM once the
# command lands and nothing here polls a busy bit, so this is a plain guard
# rather than a measured value - it matters most for `margin_save all`, which
# fires 18 stores back to back. Set to "" to disable.
MARGIN_STORE_SETTLE="${MARGIN_STORE_SETTLE:-0.5}"
# --- `margin_save blanton`: the bench-verified Blanton SONiC store list -------
# On this platform the CB FPGA F3 I2C channels are ALSO exposed as native Linux
# i2c adapters, so every LTC2977 - CB and SWB alike - can be reached with plain
# i2cset. That path needs no FPGA channel setup, no pcimem, and no margin_init:
# one flat list, 18 commands, done. Verified on the DUT 2026-08-27.
#
# FPGA F3 channel -> Linux bus: Ch6->13 Ch7->14 Ch8->15 Ch9->16
#
# Entry format: "bus:addr:label". Comment out the lines for boards this DUT does
# not have rather than letting them fail - a single-SWB unit is a normal build.
MARGIN_BLANTON_STORE=(
"4:0x5C:CB CONN13"
"4:0x5E:CB CONN13"
"15:0x5C:SWB0 CONN13"
"15:0x5E:SWB0 CONN13"
"13:0x5C:SWB0 CONN14"
"13:0x5E:SWB0 CONN14"
"15:0x62:SWB0 CONN15"
"15:0x64:SWB0 CONN15"
"13:0x62:SWB0 CONN16"
"13:0x64:SWB0 CONN16"
"16:0x5C:SWB1 CONN13"
"16:0x5E:SWB1 CONN13"
"14:0x5C:SWB1 CONN14"
"14:0x5E:SWB1 CONN14"
"16:0x62:SWB1 CONN15"
"16:0x64:SWB1 CONN15"
"14:0x62:SWB1 CONN16"
"14:0x64:SWB1 CONN16"
)
# -f because the LTC2977s may be claimed by a kernel driver on these buses; the
# command that was verified on the bench carries it, so it stays.
MARGIN_BLANTON_I2CSET_FLAGS="${MARGIN_BLANTON_I2CSET_FLAGS:--f -y}"
_BUS=""
_ADDR=""
_PRE_SCOPE="" # set while inside a PRE/POST wrapped operation; prevents nested re-entry
@@ -257,6 +314,20 @@ _swb_write() {
return 0
}
# _swb_send <cmd> -> cb_pmbus_write with NO data byte (SMBus send-byte).
# cb_pmbus_write routes a no-data call to _cbi2c_xfer_cmd_only: START, slave+W,
# command byte, STOP. That is what STORE_USER_ALL expects.
_swb_send() {
local reg=$1
[ "$DEBUG_MODE" = "1" ] && echo -e "\033[90m[DEBG] cb_pmbus_write $CB_I2C_CH $_ADDR $reg (send-byte)\033[0m"
if ! _swb_call cb_pmbus_write "$CB_I2C_CH" "$_ADDR" "$reg" >/dev/null 2>&1; then
echo -e "[\033[31mERRO\033[0m] cb_pmbus_write ch=$CB_I2C_CH $_ADDR reg=$reg (send-byte) failed (NACK/timeout)" >&2
_log_detail "ERRO cb_pmbus_write ch=$CB_I2C_CH $_ADDR $reg send-byte"
return 1
fi
return 0
}
# _swb_read_word <reg> -> echoes "0xXXXX" parsed out of cb_pmbus_read's "=> 0xXXXX"
_swb_read_word() {
local reg=$1 out word
@@ -520,6 +591,20 @@ _i2c_write_byte() {
i2cset -y "$_BUS" "$_ADDR" "$1" "$2" b >/dev/null 2>&1
}
# Send a PMBus command with NO data byte (SMBus send-byte).
# STORE_USER_ALL (0x15) and its relatives are send-byte commands: the command
# byte alone triggers them. Appending a dummy 0x00 makes it a write-byte, which
# is a different SMBus protocol - the LTC2977 happened to accept it, but that is
# tolerance, not correctness. i2cset's "c" mode is the send-byte form.
_i2c_send_byte() {
if [ "$TRANSPORT" = "swb" ]; then
_swb_send "$1"
return
fi
[ "$DEBUG_MODE" = "1" ] && echo -e "\033[90m[DEBG] i2cset -y $_BUS $_ADDR $1 c\033[0m"
i2cset -y "$_BUS" "$_ADDR" "$1" c >/dev/null 2>&1
}
# Read a 16-bit word; echoes "0xXXXX". For swb, parse cb_pmbus_read's "=> 0xXXXX".
_i2c_read_word() {
if [ "$TRANSPORT" = "swb" ]; then
@@ -730,20 +815,171 @@ margin_apply_profile() {
}
# --- Save to NVM ---
# margin_save -- STORE_USER_ALL on the currently initialised board
# margin_save all -- ... on every board in settings/, then re-init the one
# that was loaded when you called it
# margin_save blanton -- Blanton SONiC shortcut: all 18 LTC2977 straight over
# the native i2c buses, no margin_init needed
#
# ⚠️ This is the only permanent write in the tool: it commits the current
# margin / OV / UV registers to NVM, and a power cycle will NOT undo it.
# Run margin_status first and never call it during a test run.
margin_save() {
if [ "${1:-}" = "all" ]; then
_margin_save_every_board
return
fi
if [ "${1:-}" = "blanton" ]; then
_margin_save_blanton
return
fi
local _own=0
_pre_enter_own && _own=1
local chip fails=0
for chip in "${CHIPS[@]}"; do
IFS=':' read -r _BUS _ADDR <<< "$chip"
_i2c_write_byte 0x15 0x00
# STORE_USER_ALL is chip-wide (all 8 pages), so no _open_page here.
if _i2c_send_byte 0x15; then
_log_detail "STORE_USER_ALL $PART_NUMBER $_ADDR ok"
else
fails=$(( fails + 1 ))
echo -e "[\033[31mERRO\033[0m] STORE_USER_ALL failed on $_ADDR" >&2
_log_detail "ERRO STORE_USER_ALL $PART_NUMBER $_ADDR"
fi
# The part is busy writing NVM after this; nothing here polls a busy bit,
# so leave a gap before touching it again. See MARGIN_STORE_SETTLE.
[ -n "$MARGIN_STORE_SETTLE" ] && sleep "$MARGIN_STORE_SETTLE"
done
echo "Change Saved (${#CHIPS[@]} chips)"
_log_label "margin_save $PART_NUMBER (${#CHIPS[@]} chips)"
if [ "$fails" -eq 0 ]; then
echo "Change Saved (${#CHIPS[@]} chips)"
else
echo -e "[\033[31mERRO\033[0m] $fails of ${#CHIPS[@]} chips did NOT save"
fi
_log_label "margin_save $PART_NUMBER (${#CHIPS[@]} chips, $fails failed)"
[ "$_own" = "1" ] && _pre_exit
return $(( fails > 0 ))
}
# margin_save all -- every .conf in settings/, in filename order.
# Deliberately does NOT stop on the first failure: a half-saved set is worse to
# diagnose than a fully-attempted one, and the summary tells you which boards
# to redo. margin_init is re-run per board because CHIPS / TRANSPORT /
# CB_I2C_CH are global state.
_margin_save_every_board() {
local files=( "$SETTINGS_DIR"/*.conf )
if [ ! -e "${files[0]}" ]; then
echo -e "[\033[31mERRO\033[0m] no .conf found in $SETTINGS_DIR"
return 1
fi
local restore="$PART_NUMBER" # empty if margin_init was never run
local board f ok=0 bad=0 failed=""
echo "-------------"
echo "margin_save all -- ${#files[@]} boards, permanent NVM write"
echo "-------------"
_log_label "margin_save all (${#files[@]} boards)"
for f in "${files[@]}"; do
board=$(basename "$f" .conf)
echo ""
if ! margin_init "$board"; then
bad=$(( bad + 1 )); failed="$failed $board(init)"
continue
fi
if margin_save; then
ok=$(( ok + 1 ))
else
bad=$(( bad + 1 )); failed="$failed $board"
fi
done
echo ""
echo "-------------"
printf "margin_save all: %d saved, %d failed\n" "$ok" "$bad"
[ "$bad" -gt 0 ] && echo -e "[\033[31mERRO\033[0m] failed:$failed"
echo "-------------"
_log_label "margin_save all done: $ok ok, $bad failed$failed"
# Put back whatever board the caller had loaded, so a following
# margin_status does not silently report the last board in the list.
if [ -n "$restore" ] && [ "$restore" != "$board" ]; then
echo ""
echo -e "[\033[34mINFO\033[0m] restoring previous board: $restore"
margin_init "$restore" >/dev/null
fi
return $(( bad > 0 ))
}
# margin_save blanton -- Blanton SONiC only. Walks MARGIN_BLANTON_STORE and
# sends STORE_USER_ALL to each LTC2977 over the native i2c buses.
#
# Deliberately does NOT use margin_init / CHIPS / TRANSPORT: the whole point is
# that on this platform every LTC2977 is reachable with plain i2cset, so the
# FPGA channel setup and the per-board probe are not needed. That also means it
# does not touch the loaded board - PART_NUMBER is the same afterwards.
_margin_save_blanton() {
local n=${#MARGIN_BLANTON_STORE[@]}
if [ "$n" -eq 0 ]; then
echo -e "[\033[31mERRO\033[0m] MARGIN_BLANTON_STORE is empty"
return 1
fi
echo "-------------"
echo "margin_save blanton -- $n LTC2977, permanent NVM write"
echo "-------------"
_log_label "margin_save blanton ($n chips)"
local entry bus addr label ok=0 bad=0 failed=""
for entry in "${MARGIN_BLANTON_STORE[@]}"; do
IFS=':' read -r bus addr label <<< "$entry"
# "no such bus" and "chip did not answer" are different faults: the first
# means the FPGA i2c adapters did not enumerate, the second means the
# board is absent or the address is wrong. Do not blur them into one.
if [ ! -e "/dev/i2c-$bus" ]; then
echo -e "[\033[31mERRO\033[0m] $label bus $bus addr $addr : /dev/i2c-$bus does not exist"
_log_detail "ERRO STORE_USER_ALL $label bus=$bus addr=$addr no-such-bus"
bad=$(( bad + 1 )); failed="$failed [$label $addr]"
continue
fi
[ "$DEBUG_MODE" = "1" ] && echo -e "\033[90m[DEBG] i2cset $MARGIN_BLANTON_I2CSET_FLAGS $bus $addr 0x15\033[0m"
if i2cset $MARGIN_BLANTON_I2CSET_FLAGS "$bus" "$addr" 0x15 >/dev/null 2>&1; then
printf " %-12s bus %-2s %s : saved\n" "$label" "$bus" "$addr"
ok=$(( ok + 1 ))
_log_detail "STORE_USER_ALL $label bus=$bus addr=$addr ok"
else
echo -e "[\033[31mERRO\033[0m] $label bus $bus addr $addr : i2cset failed (NACK/busy)"
_log_detail "ERRO STORE_USER_ALL $label bus=$bus addr=$addr"
bad=$(( bad + 1 )); failed="$failed [$label $addr]"
fi
# No busy-bit poll anywhere in this tool; this gap is the only buffer.
[ -n "$MARGIN_STORE_SETTLE" ] && sleep "$MARGIN_STORE_SETTLE"
done
echo "-------------"
printf "margin_save blanton: %d saved, %d failed\n" "$ok" "$bad"
[ "$bad" -gt 0 ] && echo -e "[\033[31mERRO\033[0m] failed:$failed"
echo "-------------"
_log_label "margin_save blanton done: $ok ok, $bad failed$failed"
return $(( bad > 0 ))
}
# --- Tab completion for margin_save ---
_margin_save_completions() {
[ "$COMP_CWORD" -ne 1 ] && return
COMPREPLY=($(compgen -W "all blanton" -- "${COMP_WORDS[COMP_CWORD]}"))
}
complete -F _margin_save_completions margin_save
if [ -z "$_LOG_FILE" ]; then
_LOG_FILE="$LOGS_DIR/margin_$(date '+%Y%m%d_%H%M%S').log"
_LOG_DETAIL_FILE="${_LOG_FILE%.log}_detail.log"
@@ -4,6 +4,10 @@
# ⚠️ margin_save 會把目前的 margin / OV / UV 設定**永久寫入 NVM**
# 斷電後仍保留。執行前請務必先 margin_status 確認各 channel 狀態正確。
#
# 全部 9 顆請直接用 margin.sh v2.7.0 內建的 `margin_save all` —— 它有成功/失敗總結,
# 跑完還會把 margin_init 切回你原本載入的那顆。這支只在「只想寫其中幾顆」時才需要,
# 編下面的 BOARDS 陣列。
#
# Usage:
# source script/margin_save_all.sh