MES I/O Gateway / 開發者 / 整合
04開發者 / 整合

現場驗收流程

本輪修訂:2026-11-02(1.1.15 候選)

#1.1.15 首次設定與直連驗證(2026-11-02)

出廠預設 192.168.50.50/24、閘道 0.0.0.0。電腦直連可設 192.168.50.10/24,閘道/DNS 留空。每台設備正式上線前避免重複 IP。

  1. 無儲存設定或 USER 恢復原廠後,網頁顯示「目前處於首次設定模式」;MQTT、雲端及外部對時暫停。未對時的時間戳維持 null,不捏造有效時間。
  2. 儲存網路與 MQTT 設定;普通儲存、重開機、USB net static/net dhcp/net keep 不代表完成設定。
  3. 在設定頁按「完成設定並啟用」,成功保存後設備重開進入正常運行。USB 可用 net activate;會確認目前網路試用設定再重開。保存失敗必須留在首次設定並提示錯誤。
  4. IP 可繼續使用 192.168.50.50;不依 IP 或閘道猜測模式。無閘道的現場 LAN 可使用同網段數字 IP 的 Broker。跨網段與 DNS 主機名稱需先設定可用閘道。既有有效設定升級維持正常運行。
  5. AP 設定時先儲存 Wi-Fi、重開到正常 LAN,再完成設定;未完成時連線測試/雲端授權會提示先完成設定。

必測:首次設定直連至少15分鐘,全程 USB 記錄及 HTTP 取樣;無非預期重開、無自動 MQTT/雲端/NTP 嘗試;記錄 HTTP 失敗與最大延遲,不只核對 uptime。包含手動重開保留 pending、普通保存仍 pending、未授權啟用拒絕、完成後僅一次預期重啟、再次開機保留完成狀態、同網段 Broker 接收,以及 DHCP/其他靜態保存。未執行項目不得填 PASS。

修訂紀錄:2026-11-02 新增首次設定持久化、直連/現場區分及實機驗證條件(WI-269)。

#1.1.13 候選:遠端 DI → MQTT Converter 驗收(WI-266)

以下為必測項,尚非 PASS 紀錄;必須用最終候選 firmware 與配套網頁資產完成。記錄 UID、Build ID、boot、Master/Slave ID、接線及 MQTT topic。

項目 操作 通過條件
DC.1 Master 新增/辨識 OPTA DI Slave,在 MQTT Converter 開啟可用變數參考 以 Slave ID 列出 DI1~DI8;僅列有效已接擴充 DI;Finder 原有欄位保留;無截字、無頁面錯誤
DC.2 點擊複製 ${rs485.41.DI2:0} 放入 JSON 範本,按預覽 剪貼簿內容正確;合法 JSON;0=OFF、1=ON,與 Slave 本體 DI2 和 Master 總覽一致
DC.3 設定獨立測試 topic、儲存、關閉後重新開啟並重新整理頁面 範本、topic、啟用與「數值變動才送」設定保留;實際訂閱收到同一 payload,不以預覽代簽
DC.4 人工切換已接 DI2 OFF→ON→OFF,逐次等候有效輪詢 每次 Slave 本體、Master 顯示及 MQTT 值一致;時間戳為合法 UTC;記錄操作、收件及缺口時間
DC.5 「數值變動才送」分別開啟與關閉 開啟時固定 DI 不因量測時間更新而重送;關閉時依週期送;DI 改變後正常送出
DC.6 停用來源或暫停輪詢,並測不存在擴充引用 預覽與 MQTT 對應欄位為 null,其餘有效欄位照送;定時模式全部無效仍發送,變動才送模式只在有效/null 切換時發送;不得發布假 OFF 或舊值
DC.7 排定一次重開,重開後核對頁面及 MQTT 設備、範本與設定保存;有效新樣本恢復後正確傳送;恢復測試前設定並記錄清理結果

範例(41 須替換成現場站號):

json
{"di2":${rs485.41.DI2:0},"measuredAt":${rs485.41.DI2.timestamp}}

這是 Converter 範本,不是尚未填值即可解析的 JSON。量測時間為 Master 成功收到 FC02 的時間;未對時為 null。不可把無效來源當成 OFF。

2026-10-27/1.1.12 發布狀態:功能整合版已上雲端;Master/Slave、Finder 通訊及 HTTP 穩定性尚未完成驗收。發布與驗收分開記錄,以下測項仍須以目標設備實測,不以主機測試或短輪成功代替。

2026-10-27 增補:Master/Slave 交付另須完成下方 RN.1~RN.6;其後 6.0.134/135 結果為歷史記錄,不代表 1.1.8 已通過。

#2026-10-27:1.1.9 總覽角色驗收補充(待實機驗證)

  • Slave 模式總覽不顯示外接 RS485 設備卡,顯示本機 Slave ID 與 DI;角色未確認時不沿用舊 Master 卡。
  • 切回 Master 後恢復原設備卡,設備設定與 enable 值不變。
  • MQTT telemetry 啟用時仍更新角色;接收 DI 資料超過 10 秒後,下一次刷新須顯示未知/過期,新 IO 資料才可恢復 ON/OFF。角色回覆不能延長 IO 有效期。
  • 角色回覆逾時(含 body 未結束)須能自動恢復查詢。主機測試不替代安裝後的畫面核對。

#RS485 角色與網路並行驗收(1.1.8 新增)

適用於提供 Master/Slave 功能的韌體。8310/Ethernet 與 8320/Wi-Fi 分別測試;測到其中一台或只在 Master 操作不算通過。記錄 UID、版本、Build ID、bootCount、角色與起始完整設定備份。以下是測試要求,並非已完成結果。

項目 操作 通過條件與證據
RN.1 Master 下開啟總覽、設備、設定頁,讀取 /api/rs485/role 三頁與 API 可用;記錄載入時間及實際角色
RN.2 停止外部 Master,在設備頁切換為 Slave,設定本站唯一 Slave ID 收到 applied,重新讀取為 Slave;原 Master 設備設定仍保留
RN.3 Slave 下持續載入三頁、查角色與 /api/diag,並收 MQTT 狀態至少 10 分鐘 頁面完整、無連線失敗,bootCount 不增;MQTT 連線可用,逐次送 query 並核對回覆的最新 boot 與 role(query 回覆 requestId 為 0,不作命令 ACK)。不得只看 link=UP 或 connected 圖示
RN.4 外部 Master 經實體 A/B 持續 FC02 讀取,同時操作網頁與 MQTT DI ON/OFF 與實體輸入一致,連線沒有因輪詢或網頁載入失效;未接治具記 NOT RUN,不能以 USB 日誌/PTY 代簽
RN.5 保存 Slave,排定重開;再次開啟網頁並讀取角色及 DI 只有一次預期重開、Slave ID/通訊參數保存,網頁/MQTT/FC02 恢復;物理斷電另依既有步驟驗證
RN.6 先停止外部 Master,切回 Master,再切 Slave,至少三輪 每輪 fresh boot/revision/requestId 與 ACK 匹配;角色與網路均可用,最終恢復本站要求的角色及 Master 設定

USB rs485 <JSON> 僅存在於實驗室 network-diag 映像,不是交付介面。正式使用設備頁或既有 MQTT/HTTP 角色協定。診斷版的通過不可取代最終 production 映像測試;本輪實際結果見 1.1.8 驗證紀錄。

135手動測試增補(v2):135另跑OC.4/OC.5/OC.7,不能沿用134的實測PASS。確認sample在±0.02kg波動及20秒人工等待內固定、reading繼續增加;全區段最大最小差>0.1kg、失穩/讀取缺口後舊憑證不復活,重新穩定至少5秒。設定可由begin的toleranceKg調整,預設0.1kg,與精度允收分開。手動完整三段、工廠震動、保活/長時仍須本輪記錄。

2026-10-06 134 現場結果:.46 已安裝 6.0.134,Build ID 35B4EEB5DA9C49EC9B363566A7EFE4B7;實秤三段總量 5→25→45 kg 與 finish 均有匹配 applied/complete。降載 25/5/0 kg,面板與系統為 25.01/5.01/0.01 kg。原 checkbox/Converter 設定保存,收尾後已觀察到恢復數值變動才送。完整 MESB 含8資產,BIN 778128 bytes,餘裕1872。雲端通道 beta,僅此台此輪證據,其他設備仍須自行驗收。

仍未完成:業主允許誤差尚未定義,以上讀值不是精度簽核;五段實秤、校正後物理斷電保存、業主 Edge、長時與例外故障仍待驗。134 只讀觀察仍有 read_frame/MQTT 重連及最大27.772秒接收缺口;133保活逾時FAIL保留。OC.12與1.0均未放行。內部原始證據:docs/qa/qa-wi252-field134-2026-10-06.md。

版本:3.53 | 日期:2026-10-06 | 韌體基線:6.0.134(beta,1.0 未放行) · 拓荒包 1.0.0 對象:安裝工程師、業主見證人 相關:出廠測試規範、控制延遲特性

從一台空機開始,到設備上線送出第一筆資料。每一步都寫明要做什麼、看到什麼才算通過、沒通過時怎麼辦,由業主全程見證、逐項打勾。

⚠️ 只有密碼與 token 用 <...> 佔位符,實際值請向專案負責人索取,不要寫進公開文件或版本控制。 固定不變且本來就公開的值(broker 主機 mosquitto.smms.com.tw、埠 8883、帳號 smms、 雲端網址 opta.smms.com.tw)直接寫出來 —— 讓人少填四個欄位、少一個出錯的機會。 設備 IP 與 UID 每站不同,用〈準備〉那支 find_gateway.py 掃出來。


WI-252 新增範圍:線上多段校正須另完成 OC.1~OC.12。134已通過本輪容量、安裝及三段實秤協定核對;原重量topic的校正狀態/ACK須逐項驗證;本台三段結果不能代替五段、精度或業主Edge驗收。

#先看這裡:今天要怎麼驗

你不需要先讀完整本。先確認手上的設備和文件一致,再依下表往下做。一個步驟做完、看到預期結果,才做下一步。 畫面寫「成功」還不夠,下面會告訴你要到哪裡看實際結果。

順序 你要做什麼 做到什麼才往下走
1 依「今天從這裡開始」準備工具、設備 IP 和登入資料 電腦找得到設備,能讀到正確版本
2 跑既有自動驗收 保存結果;失敗先處理。被跳過的項目另外記下來
3 依「你親自走這 7 項」確認現場功能 實際接線、設備動作與畫面一致
4 照新版逐步指令做磅秤與時間檢查 完成校正收尾、實際砝碼確認,以及量測時間檢查
5 依復電步驟關閉/恢復電源 不用再人工重開就能恢復;設定、新訊息與讀值正常
6 清掉本輪臨時測試資料,保存正式設定與結果 最終備份已送達;未完成項目有人負責接手

同一個步驟只選你電腦的版本:Mac 用 macOS/Linux 區塊,Windows 用 PowerShell 區塊。不要把兩種都執行一次,也不要一次貼上整頁命令。 命令中的設備 IP、登入資料與重量要用本站的值。

有些項目需要實體磅秤、電表或工程測試設備。手上沒有,就填「還沒做/待現場」,寫明缺什麼;不能因為其他項目成功就一起打勾。下面的 SC、TS、NR 只是記錄編號:SC 是磅秤、TS 是量測時間、NR 是網路恢復。

#看到這些英文,不用先懂程式

文件上的字 這裡指的是什麼
DUT 正在驗收的這台設備
UID 這台設備的身分編號;每台不同
Build ID 這份韌體的識別碼,用來核對測的是不是同一份
broker 幫忙收送 MQTT 訊息的伺服器
payload 一則訊息裡真正的資料內容
reset 在校正工具中指清掉校正狀態,不是切斷電源
ACK 設備確認有收到/完成的回覆;仍要看各步驟的通過條件

#結果欄怎麼填

你看到的情況 填什麼 接著做什麼
動作與預期結果都一致,有留下紀錄 通過(PASS) 做下一步
做了但結果不符合 未通過(FAIL) 保存畫面/錯誤,先停在這一步處理
缺設備、沒執行或程式跳過 未做/跳過(SKIP) 記原因與接手人,不算通過
有警告,功能仍完成 警告(WARN) 記下警告內容,由負責人判斷是否影響交付

工程測試的詳細限制與舊版本結果放在文末;需要判斷時再查,不能拿舊版本結果替今天的設備打勾。

#給人工驗收者:今天從這裡開始

先準備工具,再跑自動檢查,最後做需要你親手操作的項目。 自動檢查不包含所有新版磅秤與時間測項,仍須走完上方第 4~6 步。

#準備 — 三個工具、一個目錄、一個 token(約 5 分鐘)

這條路只需要三個東西。 (node / playwright 是出廠閘門才需要的,人工驗收用不到, 不要為了它們卡在這裡。)

工具 做什麼用
python3 跑 find_gateway.py 與 acceptance_run.py
curl 從文件站抓那兩支腳本
mosquitto_pub / mosquitto_sub 情境 M/B 的 MQTT 收發;acceptance_run.py 內部也會用到
bash
# macOS
brew install python mosquitto        # curl 系統內建
powershell
# Windows PowerShell(系統管理員)
winget install Python.Python.3.12
winget install EclipseFoundation.Mosquitto
# 裝完若找不到指令,把 C:\Program Files\mosquitto 加進 PATH

裝完先自我檢查 —— 不要到了現場才發現少東西:

bash
# macOS / Linux
for c in python3 curl mosquitto_pub mosquitto_sub; do
  command -v $c >/dev/null && echo "  ✅ $c" || echo "  ❌ $c 沒裝"
done
powershell
# Windows PowerShell
foreach ($c in 'python','curl.exe','mosquitto_pub','mosquitto_sub') {
  if (Get-Command $c -ErrorAction SilentlyContinue) { "  OK  $c" } else { "  缺  $c" }
}

四個都打勾再往下走。

#🧰 現場沒有專案原始碼也能做 —— 先建工作目錄、抓三支腳本

底下所有指令都假設你在 ~/opta(Windows 是 %USERPROFILE%\opta)這個目錄下執行。

bash
# macOS / Linux
mkdir -p ~/opta && cd ~/opta
curl -O https://opta.smms.com.tw/docs/assets/find_gateway.py
curl -O https://opta.smms.com.tw/docs/assets/acceptance_run.py
curl -O https://opta.smms.com.tw/docs/assets/watch_device.py
powershell
# Windows PowerShell
New-Item -ItemType Directory -Force "$HOME\opta" | Out-Null
Set-Location "$HOME\opta"
curl.exe -O https://opta.smms.com.tw/docs/assets/find_gateway.py
curl.exe -O https://opta.smms.com.tw/docs/assets/acceptance_run.py
curl.exe -O https://opta.smms.com.tw/docs/assets/watch_device.py

Windows 的 curl 是 Invoke-WebRequest 的別名,參數不同 —— 要打 curl.exe。 Windows 的 Python 指令通常是 python,不是 python3;底下 macOS/Linux 寫 python3 的地方請自行對應。

抓完先確認拿到的是新版(文件站的快取或舊部署都可能讓你拿到過期的腳本):

bash
grep -c boot_guard acceptance_run.py     # 要 ≥ 3
grep -c 資產救援 find_gateway.py          # 要 ≥ 2

兩個數字有任何一個是 0,代表你手上的是舊版 —— 重抓一次,還是 0 就回報。 (2026-09-06 走查踩到:業主抓到的 acceptance_run.py 是沒有 bootCount 守門的舊版, 而那個守門正是用來擋「設備自己重開卻被判成功能缺陷」的。)

find_gateway.py 只有五十幾行,也可以不下載、直接把原始碼貼進終端機執行 (見〈開始前:環境準備〉的「🔍 不知道設備 IP?」;那個寫法是 bash 專用,Windows 請用上面的下載方式)。

設定 token(整個驗收過程都會用到,只需設定一次):

bash
# macOS / Linux
export TOK=<設備管理token>          # 出廠預設值請向專案負責人索取
powershell
# Windows PowerShell
$env:TOK = "<設備管理token>"

找出設備 IP 與 UID(約 1 秒,只讀不寫):

bash
python3 find_gateway.py

它會列出網段上每一台 MES I/O Gateway 的 IP、型號、韌體版本、UID、MAC。

把掃描輸出的兩個值設成環境變數 —— 後面所有指令都靠它們。

掃描結果長這樣,箭頭指的就是要抄的東西:

找到 1 台 MES I/O Gateway:

  192.168.72.77    Opta WiFi (8320)   fw=6.0.83
  ▲
  └── 這個 IP → DUT(前面要加 http://)

     UID  = 003100443033511034323932
            ▲
            └── 這一串 → UID_DEV

     名稱 = OPTA-GW-001    MAC = A8:61:0A:50:8B:61

#這六個變數各是什麼

前四個是「設備」的,後兩個是「雲端」的 —— 兩組不能混用(見下方 「設備」和「雲端」是兩個不同的系統)。

變數 是什麼 範例 哪裡來
DUT Device Under Test =「被測設備」的網址。所有打設備 API 的指令都用它 http://192.168.72.77 掃描輸出的 IP,前面加 http://(不要加結尾斜線)
TOK 設備的管理 token。設備的寫入類 API 都要帶它,等同密碼 <向專案負責人索取> 交機文件;出廠有預設值,正式交機前應該改掉
UID_DEV 設備的硬體 UID,24 碼十六進位。燒在晶片裡、每台不同、改不了 003100443033511034323932 掃描輸出的 UID = 那一行
PREFIX 這台設備的 MQTT topic 前綴。訂閱/發布都以它開頭 mes/gateway/00310044…3932 不用自己填,由 UID_DEV 自動組出來
CLOUD 雲端設定伺服器的網址。備份與版本歷史都存在那裡,不在設備上 https://opta.smms.com.tw 固定值,照抄即可
CLOUD_TOK 雲端的管理 token。與上面的 TOK 是兩個不同的東西 <向專案負責人索取> 交機文件

⚠️ 最常見的錯誤是拿設備 token 去打雲端,症狀是「推送成功、查歷史 Unauthorized」。

⚠️ CLOUD / CLOUD_TOK 沒設會長成看不懂的錯誤。 沒設定時網址會變成 /api/devices/…(沒有主機),curl 回空字串,接手的 python3 就丟 JSONDecodeError: Expecting value: line 1 column 1 (char 0) —— 那不是雲端壞了,是變數沒設。階段 R 的 R.2③、階段 4 的 4.3 都會用到。

⚠️ PREFIX 有個常見陷阱:設定頁上顯示的是 mes/gateway/001,但設備實際收發用的是 上面這個 24 碼 UID 版本。 照設定頁上那串發命令會石沉大海 —— 一定要用 UID_DEV 組出來的。

#設定它們

bash
# macOS / Linux —— 換成你掃描到的實際值
export DUT=http://192.168.72.77
export TOK=<設備管理token>
export UID_DEV=003100443033511034323932
export PREFIX=mes/gateway/$UID_DEV
export CLOUD=https://opta.smms.com.tw          # 固定值,照抄
export CLOUD_TOK=<雲端管理token>                # ⚠️ 與 TOK 不同
powershell
# Windows PowerShell —— 換成你掃描到的實際值
$env:DUT       = "http://192.168.72.77"
$env:TOK       = "<設備管理token>"
$env:UID_DEV   = "003100443033511034323932"
$env:PREFIX    = "mes/gateway/$($env:UID_DEV)"
$env:CLOUD     = "https://opta.smms.com.tw"     # 固定值,照抄
$env:CLOUD_TOK = "<雲端管理token>"               # ⚠️ 與 TOK 不同

#確認六個都設好了

bash
# macOS / Linux
for v in DUT TOK UID_DEV PREFIX CLOUD CLOUD_TOK; do
  eval "val=\$$v"
  if [ -z "$val" ]; then echo "  ❌ $v 未設定"
  elif [ "$v" = TOK ] || [ "$v" = CLOUD_TOK ]; then echo "  ✅ $v 已設定(長度 ${#val})"
  else echo "  ✅ $v = $val"; fi
done
powershell
# Windows PowerShell
foreach ($v in 'DUT','TOK','UID_DEV','PREFIX','CLOUD','CLOUD_TOK') {
  $val = [Environment]::GetEnvironmentVariable($v)
  if (-not $val)                             { "  X  $v 未設定" }
  elseif ($v -eq 'TOK' -or $v -eq 'CLOUD_TOK') { "  OK $v 已設定(長度 $($val.Length))" }
  else                                       { "  OK $v = $val" }
}

看起來要像這樣(六個都是 ✅ 才往下走):

  ✅ DUT = http://192.168.72.77
  ✅ TOK 已設定(長度 N)
  ✅ UID_DEV = 003100443033511034323932
  ✅ PREFIX = mes/gateway/003100443033511034323932
  ✅ CLOUD = https://opta.smms.com.tw
  ✅ CLOUD_TOK 已設定(長度 M)

兩個長度沒有「正確答案」 —— 它們就是你拿到的那兩個 token 的長度。 這裡只印長度、不印內容,是為了證明有設到、又不把 token 印在畫面上。

⚠️ 不要拿長度來核對 token 對不對。 長度只能告訴你「不是空的」; 你可能設了一個格式正確但過期/錯誤的 token,而這一步照樣全部打勾。 真正驗證 token 對不對的是後面第一次打雲端的那一步(§4.3 / §R.2③)—— 錯的 token 會在那裡以 401 Unauthorized 現形。

舊版的檢查指令只印 DUT=$DUT,沒設定時會印出空白,一眼看過去像是正常的。 現在沒設定會明白寫「❌ 未設定」。TOK 只印長度,不把 token 印在畫面上。

⚠️ 不要在自動驗收正在跑的時候掃描。 設備是單執行緒、只有 4 個 TCP socket, acceptance_run.py 執行期間它幾乎沒有餘裕回應別的請求,掃描會回報「找到 0 台」—— 看起來像設備不見了,其實只是忙。兩件事分開做。 (這不是假設:2026-09-01 實際發生過一次,當時背景正在跑 28 項驗收。)

#第 0 步 — 先跑自動驗收,綠了才開始(約 13 分鐘)

bash
python3 acceptance_run.py --dut $DUT --token $TOK

看到這行才往下走:

結果:28 PASS · 0 FAIL · 0 SKIP
✅ 驗收流程一口氣跑完,無中斷

為什麼先跑它:它把本文件所有能自動化的測項(階段 0 / 2B.2 / 3 / 4 / 5 / 6、 情境 M、情境 B、情境 D、8.1)串成一次不中斷的執行,測完自清。 先確定這些是綠的,你人工驗的才是機器本身,不是在幫程式抓 bug。 任一項紅燈就先停下處理,不要帶著紅燈進人工階段。

#第 1 步 — 你親自走這 7 項

自動化本質上做不到的部分。逐項打勾,不通過就寫在備註欄。

# 你要做的動作 通過標準 詳見 桌上這台
① 供電與接地目視 —— 看電源極性、接地線、端子有無鬆脫 接線牢固、有接地、無裸露 階段 0 可做
② 歸零成出廠狀態 —— 破壞性,會清掉設定與授權憑證 清空後能重新設定 階段 R ⚠️ 先別做(會清掉現有設定)
③ 空機燒拓荒包 —— 需要一台全新未設定的機器 燒錄成功、能連上網頁 階段 1 ❌ 無空機
④ 實體撥開關 —— 手撥 DI1 的 Toggle 開關 撥上→輸出動作;撥回→復歸。連續 3 次一致 情境 A ‧ A1 ✅ 要做
⑤ 計數器現場觸發 —— 實體觸發計數輸入 計數值遞增 階段 5.4 ❌ 本站無計數器
⑥ 實體斷電復原 —— 真的拔電源(見下方合併程序) 自行開機、設定一項不少、MQTT 自己接回 階段 7 ✅ 要做
⑦ 拍照 + 雙方簽名 照片歸檔、逐項核對簽名 階段 8 ✅ 要做

⑤ 本站沒有計數器硬體,不是跳過不驗,是沒有這個受測對象。 驗收單上請註明「本站無此配置」,不要留空白讓人以為漏測。

📌 改通道名稱時,畫面上方會跳一個藍色徽章「💾 儲存中 N」 —— 那是還沒送完的筆數。 等數字歸零再離開該頁,否則最後幾筆可能還在路上。 (v6.0.78 之前它會顯示成「💾 bH N」,bH 是漏掉的語系鍵,已修 —— WI-207。)

#⑥ 的建議做法:一次操作驗三件事

階段 7 拔電源之前,順手把網路故障切換一起驗掉。 先開一個終端機掛著看:

bash
python3 watch_device.py diag
powershell
python watch_device.py diag

兩個平台都用這支 Python 工具,而不是 watch —— 因為 watch 在 Windows 上不存在。 (指令本身仍有一字之差:macOS/Linux 是 python3,Windows 是 python。) 它只在數值變化時才印一行(uptime 一直在動,但不拿它判斷變化,否則每輪都會印)。

步驟 動作 應該看到
1 拔掉網路線 設備切到 WiFi,bootDetail = net-failover
2 等它在 WiFi 上穩定,網頁能開 WiFi IP 可連
2b ⚠️ 驗「切過去之後真的送得出資料」 見下方 —— 這一步不能跳過
3 插回網路線 自動切回有線(約 5 秒),bootDetail = net-recovery
4 拔掉電源,等 10 秒,插回 自行開機回到原 IP,bootCount +1,resetReason = UNEXPECTED
5 逐項清點設定(階段 7.2) 全部與斷電前一致,無一走失

#⚠️ 步驟 2b:切過去「有反應」不等於「能用」

這一步是 2026-09-01 才加進來的,因為它的缺席讓一個高嚴重性缺陷藏了很久。

當時的設備:切到 WiFi 了、bootDetail 標記正確、網頁打得開、區網連得到 —— 但送不出任何資料,MQTT / 雲端 / NTP 全部連不上(WI-212)。 現場唯一的徵兆只有藍燈快閃。只驗「有沒有切過去」會給出假 PASS。

切到 WiFi 之後,用 WiFi 的那個 IP 跑:

bash
export DUT=http://<切過去之後的 WiFi IP>      # 用 find_gateway.py 找
curl -s -H "Authorization: Bearer $TOK" $DUT/api/net/diag | python3 -m json.tool
欄位 通過標準 沒過代表
connGateway53_ms 正數 連區網都不通 —— 先查網路環境
dnsErr 0 -3009 = DNS 失敗
connCloudIP443_ms 正數 跨網段不通(0 或負數都是失敗)
connCloudHost443_ms 正數 含 DNS 的全路徑不通
netifGateway 有值且與 gateway 相同 —

關鍵判讀:connGateway53_ms 是正數、但 connCloudIP443_ms 不是 —— 那就是「同網段通、跨網段不通」,WI-212 的指紋。 同網段不需要閘道、直接 ARP 就到,所以 IP/閘道/遮罩查起來會全部正常, 問題其實在「封包實際走哪一張 netif」。

再確認一次 mqttConnected:

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/system \
  | python3 -c 'import sys,json;print("mqtt =",json.load(sys.stdin)["network"]["mqttConnected"])'

#另一條也要驗:開機時就沒插線

上面驗的是「運行中拔線」。還有一條路徑歷史上壞過(WI-213):

  1. 拔掉網路線,按 reset 或斷電重開 → 設備會以 AUTO 模式落到 WiFi
  2. 等它起來,把網路線插回去,什麼都不要做
  3. 應該約 5 秒內自己切回有線(net-recovery)

這條路過去是插線完全沒反應(實測 2.5 分鐘零動作;更早一次 11.8 小時), 要人工重開才切得回去。v6.0.89 已修 —— 但它與「運行中拔線」是兩條不同的路徑, 各驗各的,不能只驗一條就當兩條都過。

步驟 4 的 resetReason=UNEXPECTED/bootDetail 空 是正確的,不是異常 —— 硬體斷電不會留下重開原因的字條(BKPSRAM intent marker 的預期行為), 這正好和軟體重開的 SOFTWARE/api-reboot 分得出來。

⚠️ 長按 reset 不等於斷電。2026-08-26 那次是以長按 reset 完成的, 乙太 PHY/WiFi 模組沒有真正斷電,冷啟動路徑仍未涵蓋 —— 這台家族史上有冷開機競爭問題,所以這次請真的拔電源。

#驗完之後

全部打勾 → 階段 8 結案。 有任何一項沒過 → 寫進驗收單的未通過欄,並取得業主書面同意,或不予交付。


#開始前:環境準備與連線設定

人工驗收只需要前面〈準備〉那三個工具就夠了。 這一節多出來的 node / npm / playwright 是出廠閘門(npm run test:gate、 test:regression)才需要的,而且它們要有專案原始碼才能跑 —— 現場通常沒有。 只做人工驗收的話,這一節看環境變數那段就好。

驗收過程會用到命令列工具,先裝好再到現場,不要在業主面前才發現少東西。

bash
# Node(跑自動化測試)
brew install node

# 測試套件與瀏覽器核心
cd <專案>/web
npm install
npx playwright install chromium

# MQTT 命令列工具(發布/訂閱,驗證資料真的送出去)
brew install mosquitto
powershell
# Node(跑自動化測試)— 或到 nodejs.org 下載安裝檔
winget install OpenJS.NodeJS.LTS

# 測試套件與瀏覽器核心
cd <專案>\web
npm install
npx playwright install chromium

# MQTT 命令列工具(發布/訂閱,驗證資料真的送出去)
winget install EclipseFoundation.Mosquitto
# 安裝後若找不到指令,把 C:\Program Files\mosquitto 加入 PATH

設備的 IP 與 UID 要等裝好之後才會知道(階段 3.2 會拿到)。 現在先把檔案建好、把已知的填上;DUT 和 UID_DEV 留空,裝好設備再回來補。

#🔍 不知道設備 IP?掃描本地網段

DHCP 環境下設備拿到什麼 IP 事先不會知道。這支會掃過整個網段, 把 MES I/O Gateway 認出來(靠首頁的 MES I/O Gateway 標題,不需要 token), 再用 token 補上型號、韌體版本、UID 與 MAC — UID_DEV 直接從這裡抄。

bash
# 直接貼進終端機就能跑 —— 現場沒有 repo、也不必先下載任何東西
#   (這是 bash / zsh 的寫法;Windows PowerShell 請用前面的 curl.exe 下載)
python3 - <<'PY'
import os, socket, sys, time, json, gzip, urllib.request
from concurrent.futures import ThreadPoolExecutor
ARG  = [a for a in sys.argv[1:] if not a.startswith('-')]
NET  = ARG[0] if ARG else None
TOK  = ARG[1] if len(ARG) > 1 else os.environ.get("TOK", "")   # 第二參數或 $TOK;沒有就只做辨識
if not NET:
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.connect(("8.8.8.8", 80))
    NET = s.getsockname()[0]; s.close()
NET = NET.rsplit(".", 1)[0]
print(f"掃描 {NET}.1-254 …", flush=True)
def fetch(url, tok=None, t=3):
    h = {"Connection": "close"}
    if tok: h["Authorization"] = "Bearer " + tok
    b = urllib.request.urlopen(urllib.request.Request(url, headers=h), timeout=t).read()
    return gzip.decompress(b) if b[:2] == b"\x1f\x8b" else b
def port_open(i):
    """階段一:只做 TCP 連線測試。這一步很便宜,可以放心並發。"""
    ip = f"{NET}.{i}"
    s = socket.socket(); s.settimeout(0.6)
    try:
        return ip if s.connect_ex((ip, 80)) == 0 else None
    finally:
        s.close()

def identify(ip):
    """階段二:HTTP 探測。**必須溫和** —— 設備是單執行緒、只有 4 個 socket,
    64 條執行緒同時打它會被拒連,結果就是「明明在線卻掃不到」。
    候選通常只有一兩台,逐台慢慢問完全不影響速度。"""
    for attempt in range(3):
        try:
            if b"MES I/O Gateway" not in fetch(f"http://{ip}/", t=6):
                return None                      # 是別的網頁伺服器
            break
        except Exception:
            if attempt == 2: return None
            time.sleep(1.0)
    info = {}
    if TOK:                       # 沒 token 就只回報「這是一台 gateway」,不硬闖
        for ep in ("/api/system", "/api/license", "/api/config"):
            for attempt in range(2):
                try:
                    info.update(json.loads(fetch(f"http://{ip}{ep}", TOK, 8))); break
                except Exception:
                    time.sleep(0.8)
    return (ip, info)

with ThreadPoolExecutor(max_workers=64) as ex:          # 階段一:並發掃埠
    cands = [ip for ip in ex.map(port_open, range(1, 255)) if ip]
print(f"  80 埠開著的主機 {len(cands)} 台,逐一確認…", flush=True)
gw = [r for r in (identify(ip) for ip in cands) if r]   # 階段二:逐台、不併發

print(f"\n找到 {len(gw)} 台 MES I/O Gateway:\n")
for ip, d in gw:
    if d.get("version"):
        print(f"  {ip:16} {d.get('boardModel','?'):18} fw={d.get('version','?')}")
        print(f"     UID  = {d.get('uid','(讀不到)')}")
        print(f"     名稱 = {d.get('deviceName','') or '(未命名)'}    MAC = {d.get('mac','?')}")
    else:
        why = "沒帶 token" if not TOK else "token 不對"
        print(f"  {ip:16} 是 MES I/O Gateway,但{why} → 型號/韌體/UID 讀不到")
    print()
if not gw:
    print("  沒找到。確認筆電與設備同網段;或指定網段:  … 192.168.72.0  <token>")
PY

輸出範例(實測約 1 秒):

掃描 192.168.72.1-254 …

找到 1 台 MES I/O Gateway:

  192.168.72.77    Opta WiFi (8320)   fw=6.0.83
     UID  = 003100443033511034323932
     名稱 = OPTA-GW-001    MAC = A8:61:0A:50:8B:61

掃不到的三種常見情況: ① 筆電與設備不同網段 → 用參數指定,例如 … 192.168.72.0 ② 設備開在 AP 模式(自己發熱點)→ 先連上那個熱點再掃 ③ 找到了但顯示「token 不對」→ 設備已改過 token,用參數帶正確的:… 192.168.72.0 <你的token>

這支只讀不寫,對設備的唯一動作是三個 GET,可以放心在生產網段上跑。

#「設備」和「雲端」是兩個不同的系統 —— 兩個 token 不能混用

這是最常搞混的一點,先講清楚:

設備($DUT / $TOK) 雲端($CLOUD / $CLOUD_TOK)
是什麼 你面前那台 Opta 閘道器本身 opta.smms.com.tw —— 集中管理的伺服器
住哪裡 現場區網,IP 每站不同 網際網路上,網址固定
拿來做什麼 讀寫設備設定、驅動 I/O、看診斷 存設備設定的備份快照、發佈韌體
token 設備自己的管理密碼 雲端後台的管理密碼
驗收哪幾步會用 幾乎全部 只有 4.3 和 8.1

為什麼驗收需要碰雲端:階段 4 驗的不是「有沒有備份」,而是「備份讀不讀得回來」。 設備說「推送成功」只代表它已排程,不代表雲端真的收到了 —— 要到雲端查版本歷史、看到剛才那個時間戳,才算數。

⚠️ 兩個 token 不同,而且最常見的錯誤是拿設備 token 去打雲端。 症狀很好認:推送(4.2)成功、查版本歷史(4.3)回 Unauthorized —— 因為推送用的是設備 token、查歷史用的是雲端 token。

每次開新終端機都要載入這些設定(後面所有指令都靠它們):

📦 現成檔案下載:mqenv.zip

這是一個加密壓縮檔,僅供內部人員使用。 解壓密碼不公開於本文件, 請向專案負責人索取。裡面是已經填好本站實際連線參數的 mqenv.sh, 解開後放到家目錄即可直接 source ~/mqenv.sh 使用,省去手動填值。

外部讀者請依下方範本自行建立,把 <...> 換成你自己環境的值。

macOS / Linux 存成 ~/mqenv.sh,每開終端機 source ~/mqenv.sh; Windows 存成 mqenv.ps1,每開 PowerShell 執行 . .\mqenv.ps1(前面有一個點加空白):

bash
# ── 設備 ──
export DUT=http://<設備IP>
export TOK=<設備管理token>
export UID_DEV=<設備UID,見設定頁>
export PREFIX=mes/gateway/$UID_DEV

# ── 雲端設定伺服器(設備的設定備份存在這裡)──
#    階段 4.3 與 8.1 要用它查「剛才推上去的備份是不是真的到了」
export CLOUD=https://opta.smms.com.tw
export CLOUD_TOK=<雲端管理token>      # ⚠️ 與上面的 TOK 是**兩個不同的東西**

# ── MQTT ──(主機/埠/帳號是固定值,只有密碼要向專案負責人索取)
mq_sub(){ mosquitto_sub -h mosquitto.smms.com.tw -p 8883 -u smms -P <密碼> --insecure "$@"; }
mq_pub(){ mosquitto_pub -h mosquitto.smms.com.tw -p 8883 -u smms -P <密碼> --insecure "$@"; }
powershell
# 存成 mqenv.ps1,每開 PowerShell 執行: . .\mqenv.ps1   (點 + 空白 + 路徑)
# ── 設備 ──
$DUT       = "http://<設備IP>"
$TOK       = "<設備管理token>"
$UID_DEV   = "<設備UID,見設定頁>"
$PREFIX    = "mes/gateway/$UID_DEV"

# ── 雲端設定伺服器(設備的設定備份存在這裡;階段 4.3 / 8.1 要用)──
$CLOUD     = "https://opta.smms.com.tw"
$CLOUD_TOK = "<雲端管理token>"     # ⚠️ 與上面的 $TOK 是兩個不同的東西

# ── MQTT ──(主機/埠/帳號是固定值,只有密碼要向專案負責人索取)
function mq_sub { mosquitto_sub.exe -h mosquitto.smms.com.tw -p 8883 -u smms -P <密碼> --insecure @args }
function mq_pub { mosquitto_pub.exe -h mosquitto.smms.com.tw -p 8883 -u smms -P <密碼> --insecure @args }

⚠️ 兩個實測踩過的坑,都會給出誤導性的錯誤訊息:

① 用 --insecure,不要去指定憑證檔。 本系統的 MQTT 走 TLS 但不驗憑證(設備出廠預設 tlsInsecure=true,業主指定), 所以命令列也該用 --insecure —— 行為與設備一致,而且不依賴任何檔案路徑,換電腦一樣能跑。

反例(實測踩過):--capath /etc/ssl/certs 會回 Error: Protocol error。 那個目錄在 macOS 上確實存在,所以肉眼看不出是參數錯,而錯誤訊息會把人導向 「broker 或網路壞掉」的方向。改用 --cafile /etc/ssl/cert.pem 雖然能動, 但那條路徑不保證每台電腦都有 —— 依賴它只是把問題延後。

② 用函式包,不要用 export MQ="-h ... -u ..." 這種「把一串參數塞進變數」的寫法。 macOS 預設是 zsh,而 zsh 不會對未加引號的變數做詞彙拆分 —— 整串會被當成單一參數, 報 Unknown option '-h ...'。同樣的寫法在 bash 可以、zsh 不行。函式兩種 shell 都能用。

PREFIX 不要用手拼。 若本站啟用了 MQTT 命名空間(projectName / env), 實際前綴就不是 mes/gateway/<UID>。設備裝好後用這條確認真正的前綴:

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/config | python3 -c "
import sys,json
d=json.load(sys.stdin); m=d.get('mqtt') or {}
proj,env=m.get('projectName') or '',m.get('env') or ''
print('  UID        :',d.get('uid'))
print('  projectName:',proj or '(空)')
print('  env        :',env or '(空)')
print('  → 實際前綴 :', f'{proj}-{env}/gateway/'+(d.get('mac') or '').replace(':','').lower() if proj or env else 'mes/gateway/'+str(d.get('uid')))
"

兩者留空 = legacy 模式,前綴才是 mes/gateway/<UID>。 前綴錯了的症狀是「訂閱端一筆都收不到」,而設備那邊看起來一切正常 —— 很難查。

確認連得到再往下:

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/system | python3 -m json.tool | head -12
powershell
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/system | python -m json.tool | head -12

通過:看得到 version / boardModel,且 version 與交付文件一致。


#開始前:本站硬體表

驗收的每一個數字都要對得上實際接的東西,否則後面每一步都在猜。

編號 設備 接法 位址 / 通道 正常範圍
1 計數器 SF965(HB965SF) RS485 Slave ID ____ 計數值遞增
2 電流環 #1 4–20mA → AI 或 RS485 ____ ____ A
3 電流環 #2 4–20mA → AI 或 RS485 ____ ____ A
4 電流計(Finder) RS485 Slave ID ____ ____ A

電流環有兩種接法,驗收步驟不同,先確認實際是哪一種:

  1. 帶 RS485 的電流計(如 Finder 6M)→ 走階段 5,設 Slave ID 與型號模板。
  2. 4–20mA 類比訊號直接進類比輸入 → 本版尚未涵蓋。 若本站確實是這種接法,先與專案負責人確認量程換算方式,不要照 RS485 的步驟做。

#階段 R:歸零 —— 把設備恢復成出廠狀態

#🟢 全新未開封的設備:整個階段 R 跳過,直接到「這台機器走哪一條路」

全新機本來就是空的,沒有東西可以清。做歸零不會出錯,但是白費五分鐘。

以下只適用於「動過的機器」 —— 展示機、測試機、退回重配的機器。這類機器必須先歸零,否則舊設定會混進新現場,日後查問題時分不清哪些是這次設的。

#先理解:設備上有兩塊儲存區

區域 內容 重燒韌體會清掉嗎
韌體區 程式本身 ✅ 會
設定區 所有設定、網路帳密、授權憑證 ❌ 不會

「我重燒過韌體了,應該乾淨了吧」是錯的 —— 設定原封不動還在。

#三個層級

層級 怎麼做 清掉什麼 什麼時候用
L1 按住機身按鈕開機,長按 10 秒以上 只清設定檔,不清授權憑證 自己排錯用
L2 設定頁按「恢復原廠」 格式化整個設定區:設定 + 網路帳密 + 授權憑證全清 交機/轉售標準做法
L3 USB 重燒韌體,再做一次 L2 韌體 + 設定區都換新 換版本,或狀態不明

⚠️ L1 是交機時最容易踩的坑。 它看起來設定都清光了,但授權憑證還留著 —— 設備重開後會自己把授權狀態還原回去,下一手會拿到一台帶著上一手憑證的機器。交機一律用 L2。

#驗收標準

R.1 確認機器來歷

bash
# 先看它身上有沒有東西 —— 有設定就是動過的
curl -s -H "Authorization: Bearer $TOK" $DUT/api/config | python3 -c "
import sys,json
d=json.load(sys.stdin)
print('  設備名稱  :',d.get('deviceName'))
print('  RS485 設備:',len([x for x in (d.get('modbusDevices') or []) if x.get('name')]),'台')
print('  Wi-Fi SSID:',(d.get('wifi') or {}).get('ssid') or '(空)')
"
powershell
# 先看它身上有沒有東西 —— 有設定就是動過的
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/config | python -c "
import sys,json
d=json.load(sys.stdin)
print('  設備名稱  :',d.get('deviceName'))
print('  RS485 設備:',len([x for x in (d.get('modbusDevices') or []) if x.get('name')]),'台')
print('  Wi-Fi SSID:',(d.get('wifi') or {}).get('ssid') or '(空)')
"

通過:全新(全部為空)→ 跳過本階段;有任何殘留 → 往下做 R.2。 沒過:連不上、讀不到 → 來歷不明一律當成動過,執行 L3。

R.2 歸零前備份(如果這台上面有還需要的東西)

bash
# ① 叫設備推上去
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/configserver/push
sleep 10

# ② 確認設備這一側沒問題(核對有效身份;空 cloudToken 可使用 license fallback)
curl -s -H "Authorization: Bearer $TOK" $DUT/api/configserver | python3 -c "
import sys,json
d=json.load(sys.stdin); c=(d.get('push') or {}).get('cloud') or {}
print('  雲端位址   :',d.get('cloudUrl') or '(空)')
print('  Cloud Token:','未明填(須驗 license fallback)' if not d.get('cloudToken') else '已填')
print('  上次推送   :',c.get('result'),'HTTP',c.get('httpCode'))
"

# ③ 真正的驗收點:跟【雲端】要快照清單(判準看 summary,不是「有沒有剛推的那筆」)
#     ⚠️ 檔名裡的數字是 epoch 毫秒,人看不出「是不是剛剛那筆」——
#        一定要轉成本地時間,否則這一步只驗到「雲端有東西」——那證明不了什麼
curl -s -H "Authorization: Bearer $CLOUD_TOK" \
  "$CLOUD/api/devices/$UID_DEV/snapshots?limit=50" | python3 -c 'import sys,json,datetime
d=json.load(sys.stdin); l=d if isinstance(d,list) else d.get("snapshots",[])
def when(x):
    v=x.get("createdAt") or x.get("timestamp")
    try: return datetime.datetime.fromtimestamp(int(v)/1000).strftime("%m-%d %H:%M:%S")
    except Exception: return str(v)
print(f"  共 {len(l)} 筆,最新 3 筆:")
[print("   ", when(x), x.get("filename"), "|", (x.get("summary") or "")[:50]) for x in l[:3]]'
powershell
# ① 叫設備推上去
curl.exe -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/configserver/push
Start-Sleep 10

# ② 確認設備這一側沒問題(核對有效身份;空 cloudToken 可使用 license fallback)
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/configserver | python -c "
import sys,json
d=json.load(sys.stdin); c=(d.get('push') or {}).get('cloud') or {}
print('  雲端位址   :',d.get('cloudUrl') or '(空)')
print('  Cloud Token:','未明填(須驗 license fallback)' if not d.get('cloudToken') else '已填')
print('  上次推送   :',c.get('result'),'HTTP',c.get('httpCode'))
"

# ③ 真正的驗收點:跟【雲端】要快照清單(判準看 summary,不是「有沒有剛推的那筆」)
#     ⚠️ 檔名裡的數字是 epoch 毫秒,人看不出「是不是剛剛那筆」,一定要轉成本地時間
curl.exe -s -H "Authorization: Bearer $env:CLOUD_TOK" `
  "$($env:CLOUD)/api/devices/$($env:UID_DEV)/snapshots?limit=50" | python -c 'import sys,json,datetime
d=json.load(sys.stdin); l=d if isinstance(d,list) else d.get("snapshots",[])
def when(x):
    v=x.get("createdAt") or x.get("timestamp")
    try: return datetime.datetime.fromtimestamp(int(v)/1000).strftime("%m-%d %H:%M:%S")
    except Exception: return str(v)
print(f"  共 {len(l)} 筆,最新 3 筆:")
[print("   ", when(x), x.get("filename"), "|", (x.get("summary") or "")[:50]) for x in l[:3]]'

看起來要像這樣:

  共 50 筆,最新 3 筆:
    09-02 13:22:23 snapshot.1788326543752.v5.json  | 無差異
    09-02 12:25:49 snapshot.1788323149226.v5.json  | inputNames: Array[8] → Array[8]
    09-02 12:23:19 snapshot.1788322999301.v5.json  | tcpIo: Array[5] → Array[2], …

#🛑 推完沒有新快照 ≠ 推送失敗 —— 這是最容易誤判的一點

雲端只在設定內容真的變了才建快照 (saveSnapshotIfChanged():剔除 checksum / crc32 / timestamp / license.lastCheckAt 這些每次必變的記帳欄位之後比對,一樣就不建)。這是刻意的 —— 否則每小時都會長一筆 「實質無差異」的雜訊快照,把你真正要找的那筆擠掉。

所以連推兩次、中間沒改東西 → 第二次不會有新快照,而那是正確的。 用「最新那筆的時間必須等於剛推的時間」當判準,只要沒改東西就會假紅。

summary 那一欄才是要看的:它是「該筆快照的值 → 設備現況」的差異。 最新那筆顯示 無差異,意思正是「雲端手上這份 = 設備現在的設定」—— 那才是「備份是最新的」的真正證據。

通過:② 的「上次推送」是 OK / HTTP 200,而且 ③ 最新那筆的 summary 是 無差異(代表雲端持有的內容等於設備現況)。

若 summary 顯示有差異、而你剛推完 —— 那才要查:代表雲端手上的是舊的。

⚠️ ② 顯示 NEVER / HTTP 0 不一定代表沒推成功。 那個狀態存在 RAM, 設備一重開就歸零(拔插網路線會觸發重開)。所以推完之後若設備重開過, ② 會變成 NEVER,但備份其實好好地在雲端。以 ③ 為準 —— ③ 問的是雲端「有沒有這筆」,那才是還原時真正會被讀到的東西。 沒過:確定不需要保留就直接往下。歸零之後就真的沒了,這一步不要含糊。

⚠️ ③ 一定要打雲端,不能打設備。 設備端所有 /api/configserver... 開頭的 GET —— 含 /versions、/history —— 回的都是同一包設定,永遠沒有清單。 拿它來驗會誤以為「查過了」,但你只驗到「推得上去」,沒驗到「讀得回來」, 而這一段的全部意義就在後者。

R.3 執行 L2「恢復原廠」

bash
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/factory-reset
powershell
curl.exe -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/factory-reset

也可以在設定頁按「恢復原廠」按鈕,效果相同。 ⚠️ 這會格式化整個設定區(設定 + 網路帳密 + 授權憑證全清),且無法復原。執行前確認 R.2 已完成。

#🛑 歸零之後,雲端還原「不會自己成功」—— 必須先做這一步

恢復原廠會清除 Cloud Token;6.0.100 在它空白時使用設備 license 身份。先確認授權已恢復,再用有效身份讀本设备的雲端歷史。只有實際 401/403 才判定身份或權限有問題;「已排程」也不等於套用成功。

以下是有指定 Cloud Token 時的設定方式;使用已驗證的 license fallback 時不必重填共用管理 token。

bash
# 注意:JSON 不能有空格 —— 韌體是字串比對,有空格會靜默失敗
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d "{\"enabled\":true,\"cloudUrl\":\"$CLOUD\",\"cloudToken\":\"$CLOUD_TOK\"}" \
  $DUT/api/configserver

# 讀回確認再往下,不要憑「回了 success」就相信
curl -s -H "Authorization: Bearer $TOK" $DUT/api/configserver | python3 -c "
import sys,json;d=json.load(sys.stdin)
print('  cloudUrl  :',d.get('cloudUrl') or '(空)')
print('  cloudToken:','已填' if d.get('cloudToken') else '⚠ 空的')"

填好之後才觸發還原(見 §R.5)。實測時間:排入後約 2 分鐘設備心跳取件, 下載完會自行重開,再約 20 秒設定回來。

(此缺陷已開卡 WI-194。修好之前,這一步都必須手動做。)

通過(8320 且未接網路線):設備自行重開並開出 Wi-Fi 熱點 —— 它已經不記得任何網路了。 通過(8310,或 8320 插著網路線):這類機器不會開熱點,也不該期待它開。 改用下面這條判準:重開後用 §2B.2 掃描找到它,並用 §R.4 的 curl 確認設定全空。

⚠️ 掃描結果會標示「目前只有【資產救援頁】」—— 那是正確的,不是掃描失敗。 L2 連 QSPI 的網頁資產一起清掉了,所以設備只剩救援頁(見上方說明)。 find_gateway.py 認得這個狀態並會明白告訴你「設備本身正常」。 這時不要用「網頁看起來是空的」當判準 —— 你根本打不開設定頁, 要用 §R.4 那幾條 curl(API 完全正常)。

(2026-09-06 走查踩到:當時的 find_gateway.py 只認 MES I/O Gateway 標題, 對救援頁回報「找到 0 台」—— 文件叫你在這個狀態下掃描,而工具剛好處理不了它。已修。) 沒過:8320 拔掉網路線重開後仍自動連上原本的 Wi-Fi → 沒清乾淨(可能只做到 L1),改用 L3。

R.4 確認歸零結果

先確定要打哪個位址 —— 這一步最容易打錯:

情況 位址
8320 且未接網路線(已開熱點) 連上熱點後用 http://192.168.3.1
8310,或接著網路線的 8320 不會開熱點,用 §2B.2 掃到的新 IP

下面用 $DUT 代表該位址,先把它設成上表對應的值。

bash
# 通道與規則各有自己的端點,不在 /api/config 裡面
for ep in tcpio rules signals actions; do
  printf '  %-8s %s\n' "$ep" "$(curl -s -H "Authorization: Bearer $TOK" $DUT/api/$ep)"
done
curl -s -H "Authorization: Bearer $TOK" $DUT/api/config | python3 -c "
import sys,json
d=json.load(sys.stdin)
print('  RS485 設備:',len([x for x in (d.get('modbusDevices') or []) if x.get('name')]),'台')
print('  Wi-Fi SSID:',(d.get('wifi') or {}).get('ssid') or '(空)')
"
powershell
foreach ($ep in 'tcpio','rules','signals','actions') {
  "  {0,-8} {1}" -f $ep, (curl.exe -s -H "Authorization: Bearer $TOK" "$DUT/api/$ep")
}
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/config | python -c "
import sys,json
d=json.load(sys.stdin)
print('  RS485 設備:',len([x for x in (d.get('modbusDevices') or []) if x.get('name')]),'台')
print('  Wi-Fi SSID:',(d.get('wifi') or {}).get('ssid') or '(空)')
"

通過:五項全空,長得像這樣 ——

  tcpio    {"count":0,"channels":[]}
  rules    {"rules":[],"ruleCount":0}
  signals  {"signals":[],"signalCount":0}
  actions  {"actions":[],"actionCount":0}
  RS485 設備: 0 台
  Wi-Fi SSID: (空)

沒過:任何一項還留著舊資料 → 沒清乾淨,重做 L3。不要帶著殘留設定往下走,那會讓後面每一個異常都多一種可能原因。

⚠️ 不要拿 /api/config 裡的 tcp 欄位來判斷通道。那個欄位是 {"port":5000} —— 監聽埠,不是通道清單,通道有幾條它都長一樣。 真正的清單只在 GET /api/tcpio。同理,規則/訊號/動作也都不在 /api/config 裡。

#⚠️ 歸零之後打開網頁,你會看到「資產救援」頁 —— 這是正常的

L2 會格式化整個 QSPI,連網頁介面本身(index.html/app.js/styles.css/ 語系檔/log.html/workflow.html)一起清掉。所以歸零後的設備:

  • API 完全正常 —— 上面 §R.4 那些 curl 全部照跑不誤
  • 但沒有可用的網頁 UI —— / 會回「MES Gateway — 資產救援」頁, /app.js、/styles.css、/log.html、/workflow.html 全部 404

不要以為機器壞了。 網頁介面是靠上傳 .mesb 帶回來的 —— 交機流程往下走到 §2C 上傳正式版韌體時就會恢復。

⚠️ 但如果你是「清錯了要救回來」:§R.5 的雲端還原只還原設定,不還原網頁資產。 設定回來了、畫面還是救援頁。把網頁介面帶回來有兩條路:

做法 怎麼做 什麼時候用
雲端部署 .mesb(建議) 後台對這台按「🚀 部署」,務必勾「重新安裝(版本相同也強制推)」,設備下次心跳自己下載+重開(約 2 分鐘) 現場首選 —— 不必手上有檔案
救援頁「安裝資產」 頁面上選 .gz 資產檔(可多選),逐檔 POST /api/web?name=<檔名> 連不到雲端時

🛑 雲端部署一定要勾「重新安裝」,否則什麼都不會發生。 恢復原廠後韌體版本沒變(被清掉的是網頁資產),而雲端有一道同版守衛: 設備回報的版本 == 要推的版本就不下發。不勾的話後台會直接告訴你 「設備已經是 v6.0.90,不會下發」—— (cs 2.5.3 之前更糟:它會回 200、對話框寫「2 分鐘後自動更新」,然後什麼都不做。 2026-09-06 走查踩到,業主按了部署發現設備完全沒反應。已修:同版未勾重裝時當場說明, 並新增「重新安裝」選項。)

🛑 救援頁不吃 .mesb。 它的檔案選擇是 accept=".gz"、按鈕寫「安裝資產」, 收的是個別的網頁資產(index.html.gz / app.js.gz / styles.css.gz / i18n-*.json.gz / log.html.gz / workflow.html.gz,共 8 個),一次一個檔。 拿 .mesb 丟進去不會成功。 (2026-09-06 走查踩到:舊版文件這裡寫「推一次 .mesb(救援頁上就有上傳鈕)」—— 那是把救援頁與 §2C 的拓荒頁搞混了,現場照做會卡死。)

R.3 的判準刻意選「會不會開熱點」,因為那是設定真的被清光的外部證據 —— 它已經不記得任何網路了。比「畫面看起來是空的」可靠得多。

R.5 從雲端還原(只在「清錯了、要救回來」時做;正常交機不需要)

前提:§R.3 的 🛑 區塊已完成 —— Cloud Token 已填回並讀回確認。

bash
# ① 列出可用快照,挑一筆(檔名長得像 snapshot.<一長串數字>.v5.json)
curl -s -H "Authorization: Bearer $CLOUD_TOK" \
  "$CLOUD/api/devices/$UID_DEV/snapshots?limit=10" | python3 -c "
import sys,json
for x in json.load(sys.stdin): print('  ',x.get('filename'))"

# ② 【重要】先確認設備沒有待推送,否則還原會被蓋掉(見下方警告)
curl -s -H "Authorization: Bearer $TOK" $DUT/api/configserver | python3 -c "
import sys,json;d=json.load(sys.stdin);c=(d.get('push') or {}).get('cloud') or {}
print('  needsPush :',d.get('needsPush'),'  ← 必須是 False')
print('  上次推送  : ',c.get('ageS'),'秒前')"

# ③ 觸發還原(把 <檔名> 換成上面挑的那一筆)
curl -s -X POST -H "Authorization: Bearer $CLOUD_TOK" \
  "$CLOUD/api/devices/$UID_DEV/snapshots/<檔名>/rollback"

# ④ 等設備心跳取件(約 2 分鐘),它會自己下載並重開。輪詢到不是 0 為止
for i in $(seq 1 12); do
  sleep 20
  printf '  %s ruleCount=' "$(date +%H:%M:%S)"
  curl -s -H "Authorization: Bearer $TOK" $DUT/api/rules | python3 -c "
import sys,json;print(json.load(sys.stdin).get('ruleCount'))"
done

通過:規則/訊號/動作/RS485/SSID 全部回到歸零前的數量 —— 用 §R.4 那條指令再跑一次比對。 沒過(最常見):ruleCount 一直是 0 → 十之八九是 Cloud Token 沒填。 回 §R.3 的 🛑 區塊確認,然後重新觸發一次還原 —— pendingCommand 是一次性的,失敗那次已經被消耗掉,不會自己重試。 查證方法:GET $DUT/api/log 找 [CS-Cloud] Command: download_update 與 [ConfigServer] (Cloud) Downloading update from …。兩行都有 = 設備確實在做事。 ⚠️ 該日誌是環形緩衝、會被 [ALIVE] 洗掉,要在觸發後幾十秒內去看。

#🛑 千萬不要「剛改完設定就馬上觸發還原」

還原的做法是把快照寫進雲端的設定檔,再叫設備來下載。而設備的自動推送 (autoPush)會把它自己當下的設定推上同一個檔案 —— 只要這兩件事撞在一起, 還原內容就被蓋掉,設備下載回來的是它自己剛推上去的東西。

實測:填完 Cloud Token 後立刻觸發還原 → 12 秒後設備推送 → 還原被覆蓋 → 看起來「還原成功但設定沒變」。

而 §R.3 的 🛑 區塊正好要你先填 Cloud Token —— 填 token 就是改設定,會排一次推送。 所以順序必須是:

  1. 填回 Cloud Token
  2. 等到 needsPush 變回 False(通常十幾秒,用上面第 ② 步確認)
  3. 才觸發還原

三個環節都會回報成功(rollback success、push OK HTTP 200、下載也真的完成), 錯的只有順序 —— 所以最容易誤判成「還原功能壞了」。

(此缺陷已開卡 WI-195。)


#第一部:安裝與設定

#階段 0:開工前現場條件確認

步驟 動作 通過 沒過
0.1 確認供電與接地 電壓在銘牌範圍內,機殼確實接地 停止安裝。供電不穩造成的間歇重開,日後極難查
0.2 RS485 佈線:A/B 極性、終端電阻、Slave ID 不重複 全線 A 接 A;兩端各一顆 120Ω;ID 互不重複 ID 重複會兩台搶答,現象是數值跳動或時有時無,看起來像設備壞掉
0.3 網路:網段、DHCP、對外連線 同網段筆電能上網 若現場不開放對外,雲端備份與 MQTT 要改內網方案,先與業主確認

0.2 / 0.3 用指令確認(0.1 只能目視)

bash
# 0.3 設備是否可達、機型與韌體版本
curl -s -H "Authorization: Bearer $TOK" $DUT/api/system | python3 -m json.tool

# 0.2 掃描匯流排,看實際回應的 Slave ID 有沒有重號
#     ⚠️ 掃描期間整台設備會停住約 1 分鐘(見階段 5.1 的警告),這是正常的
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/modbus/scan
sleep 90
curl -s -H "Authorization: Bearer $TOK" $DUT/api/modbus/scan \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print("找到:",[(x.get("slaveId"),x.get("model")) for x in d.get("devices",[])])'

#這台機器走哪一條路?先分流

兩個問題決定你接下來要做什麼,走錯會白費半小時:

問題 選項 A 選項 B
機器狀態 全新未開封 → 不需要歸零,直接做階段 1 動過的 → 先做階段 R 歸零,再回到這裡
連線方式 8320(有 Wi-Fi) → 走 §2A「無線」 8310(無 Wi-Fi,只有網路孔) → 走 §2B「有線」

怎麼確認機型?機身標籤有寫;設備能連上之後也可以從畫面或 API 確認(boardModel)。

8310 沒有 Wi-Fi,永遠不會開出熱點。 文件裡凡是提到「連上設備的熱點」, 對 8310 都不適用 —— 它一律走網路線。這是最常見的誤解來源。


#階段 1:空機燒入韌體(拓荒包)

全新設備出廠時沒有我們的韌體,要先燒一個「拓荒包」進去。拓荒包是一個精簡版, 它只做一件事:讓你能連上設備、設定網路,然後把正式版韌體上傳進去。

#🛑 這一階段假設「真正的空機」。動過的機器,燒完拓荒包還要再清一次

USB 燒錄(DFU)只換韌體,不碰 QSPI。 而韌體的邏輯是 「QSPI 上有主畫面就服務它」—— 所以一台用過的機器燒完拓荒包之後:

韌體 已經是拓荒包 ✅
打開網頁 還是完整的五個頁籤 ❌ —— 開荒頁被 QSPI 上的舊 index.html.gz 蓋住
現場看到的 完全看不出燒過,會以為燒失敗

這正是〈階段 R〉三個層級裡的 L3 = USB 重燒韌體 + 再做一次 L2。 階段 1 只是 L3 的前半段。 動過的機器請在燒完之後補做:

bash
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/factory-reset

(2026-09-07 走查踩到:燒完拓荒包,畫面卻是完整介面。查了心跳才發現雲端回報 firmwareVariant: pioneer —— 韌體確實換了,只是被 QSPI 資產蓋住。補做一次 L2 之後開荒頁立刻出現。)

為什麼分兩段? 正式版韌體很大,第一次只能靠 USB 燒;但正式版更新頻繁, 每次都接 USB 不實際。拓荒包很少改動,燒一次就好,之後的版本都用網頁上傳。

1.1 安裝工具(每台電腦只需做一次)

bash
brew install arduino-cli
arduino-cli core update-index
arduino-cli core install arduino:mbed_opta
powershell
winget install ArduinoSA.CLI
arduino-cli core update-index
arduino-cli core install arduino:mbed_opta

Windows 使用者請至 Arduino CLI 官網下載執行檔並加入環境變數。

⚠️ core update-index 需要連到 downloads.arduino.cc —— 現場網路若有出向限制會逾時:

下載索引: package_index.tar.bz2 下載失敗。: context deadline exceeded

已經裝過 core 的機器不受影響(core install 會直接說「已安裝過」), 但乾淨的筆電在這裡會卡死。所以:出發前先在辦公室把 core 裝好, 不要指望現場有 Arduino CDN 的連線。 (2026-09-07 走查實際看到這個逾時。)

1.2 取得拓荒包

bash
curl -O https://opta.smms.com.tw/api/firmware/download/mes-gateway-bootstrap.bin
powershell
curl.exe -O https://opta.smms.com.tw/api/firmware/download/mes-gateway-bootstrap.bin
# 注意是 curl.exe —— PowerShell 的 curl 是 Invoke-WebRequest 的別名,行為不同

通過:檔案下載完成,而且 sha256 與 manifest 相符:

bash
shasum -a 256 mes-gateway-bootstrap.bin
curl -s https://opta.smms.com.tw/api/firmware/download/mes-gateway-bootstrap.json

兩個 sha256 要一樣,bootstrapVersion 就是你手上這顆的版號。

⚠️ 拓荒包用固定檔名發佈(連結永不變),所以從檔名看不出它是哪一份 —— 這個比對就是唯一的辦法。(2026-09-07 實測:repo 那顆與雲端那顆 sha256 不同、 大小差 2748 bytes,而當時它還沒有版號,完全無從判斷誰新誰舊。manifest 與 bootstrapVersion 就是為此加的。)

只看「大小合理(數百 KB)」是不夠的 —— 兩顆不同的拓荒包大小都在數百 KB。 也可以自己建:在專案目錄執行 bash scripts/build_bootstrap.sh, 產出在 release/mes-gateway-bootstrap.bin。

1.3 用 USB 接上設備,找出它的 Port

bash
arduino-cli board list

通過:清單中出現標示 Opta 或 mbed 的項目,記下它的 Port (macOS 長得像 /dev/cu.usbmodem212201,Windows 像 COM3)。 沒過:沒看到任何裝置 → 換一條能傳資料的 USB 線。只能充電的線不會被電腦看到。

#🛑 接了不只一台 Opta?先分辨清楚再燒 —— 燒錯會清掉另一台

上面那條指令只會列出「有幾台 Opta」,不會告訴你哪個 Port 是哪一台。 分辨的鍵你在第一步就已經有了:USB 的 serialNumber 就是設備 UID($UID_DEV)。

bash
arduino-cli board list --format json | python3 -c '
import sys,json,os
want=os.environ.get("UID_DEV","")
for p in (json.load(sys.stdin).get("detected_ports") or []):
    port=p.get("port",{}); addr=port.get("address","")
    if "usbmodem" not in addr and not addr.upper().startswith("COM"): continue
    sn=(port.get("properties") or {}).get("serialNumber","")
    mark=" ← 這台(UID 相符)" if want and sn==want else ""
    print(f"  {addr:26} UID={sn}{mark}")'
powershell
arduino-cli board list --format json | python -c '
import sys,json,os
want=os.environ.get("UID_DEV","")
for p in (json.load(sys.stdin).get("detected_ports") or []):
    port=p.get("port",{}); addr=port.get("address","")
    if "usbmodem" not in addr and not addr.upper().startswith("COM"): continue
    sn=(port.get("properties") or {}).get("serialNumber","")
    mark=" <== this one (UID match)" if want and sn==want else ""
    print(f"  {addr:26} UID={sn}{mark}")'

只燒標著「← 這台」的那個 Port。 沒有任何一行標到,就代表你要燒的設備沒接上, 停下來確認,不要猜。

(2026-09-07 走查踩到:桌上兩台 Opta 都接著 USB,board list 兩行都寫「Opta」, 文件卻只說「記下它的 Port」——單數。差一點就燒錯機器。)

1.4 燒錄

🛑 把 <你的PORT> 換成 1.3 裡標著「← 這台」的那一個,再按 Enter。 舊版這裡直接寫了一個範例值 /dev/cu.usbmodem212201 —— 它跟真實 Port 可能只差一個字元(實測那台是 usbmodem112201),整段複製而忘了換, 就會把另一台 Opta 的韌體清掉。所以這裡刻意不放看起來像真的的範例值。

bash
arduino-cli upload -b arduino:mbed_opta:opta \
  -p <你的PORT> \
  -i mes-gateway-bootstrap.bin
powershell
arduino-cli upload -b arduino:mbed_opta:opta `
  -p <你的PORT> `
  -i mes-gateway-bootstrap.bin
# Port 是 COM 開頭(例如 COM3),不是 /dev/cu.*;續行符號是反引號 ` 不是反斜線

成功長這樣(最後三行):

Download        [=========================] 100%       762924 bytes
Download done.
File downloaded successfully

那個 bytes 數要與 1.2 下載到的檔案大小一致。

1.5 確認你真的在拓荒包上 —— 不要用「畫面看起來如何」判斷

設備重開後打開 http://<設備IP>/:

看到 意思 怎麼辦
拓荒包 · MES Gateway 開荒設定(頁面上有「拓荒包版本」) ✅ 真正的空機起點 往下走階段 2
完整的五個頁籤 ⚠️ 韌體換了,但 QSPI 上的舊主畫面把開荒頁蓋住了 補做一次 L2(見本階段開頭的 🛑)

不確定的話,問設備自己(這一條不受畫面影響):

bash
curl -s -H "Authorization: Bearer $CLOUD_TOK" \
  "$CLOUD/api/devices/$UID_DEV" | python3 -c '
import sys,json; d=json.load(sys.stdin)
print("  firmwareVariant =", d.get("firmwareVariant"), " (pioneer = 拓荒包)")
print("  firmwareVersion =", d.get("firmwareVersion"))'

firmwareVariant 是 pioneer 才代表跑在拓荒包上。 ⚠️ 不要用韌體版本判斷 —— 拓荒包與完整版是同一份 FW_VERSION 建的, 兩者的 version 一模一樣(例:都是 6.0.91)。拓荒包自己的版號在開荒頁上, 與正式版脫鉤。

也可以直接打 http://<設備IP>/rescue —— 拓荒韌體在這個位址一定服務開荒頁, 不受 QSPI 有沒有資產影響。

把 -p 後面換成 1.3 查到的 Port。 通過:指令回報成功,設備自動重開。 沒過:權限錯誤 → 確認沒有其他程式(Arduino IDE、序列埠監控)佔用該 Port。

1.5 等首次開機完成

通過:心跳燈開始規律閃爍 —— 亮滅各約 0.5 秒,1 秒一個週期。

⚠️ 心跳燈是哪一顆、什麼顏色,取決於機型 —— 盯錯燈會以為機器沒活過來:

機型 心跳燈
8320(有 Wi-Fi) 機身 USER LED,亮藍色
8310(無 Wi-Fi) 沒有藍燈硬體,改用琥珀色(紅+綠同時亮)

純紅 = 故障、純綠 = DFU,兩者都不是心跳。

⚠️ 快閃(約 0.2 秒一個週期)不是故障。 它代表「MQTT 已啟用但還沒連上」, 在剛設好 MQTT、還沒連上 broker 的期間出現是正常的。 全新設備還沒設 MQTT,看到的會是慢閃。

沒過:全新設備第一次開機要建立檔案系統,可能要 1–2 分鐘,期間絕對不要斷電。 超過 3 分鐘心跳燈仍完全沒有動靜(不閃也不亮)→ 回到 1.4 重燒。


#階段 2A:連上設備 —— 無線機型(8320)

剛燒好的設備還不知道你的網路,所以它會自己開一個 Wi-Fi 熱點等你進去設定。

同樣的情況也發生在做完「階段 R 歸零」之後 —— 網路帳密被清光,設備就回到這個狀態。 這是正常的,不是故障。

2A.1 用手機或筆電連上設備開出的熱點

項目 值
熱點名稱(SSID) MES-Gateway-Setup
密碼 12345678
連上後開 http://192.168.3.1

通過:連上後取得 192.168.3.x 網段的 IP。 沒過:找不到熱點 → 設備可能還記得舊的 Wi-Fi 而直接連上了現場網路, 改用 §2B.2 的掃描工具找它的 IP。

2A.2 瀏覽器開 http://192.168.3.1

通過:看到**「🧭 拓荒包 · MES Gateway 開荒」**頁面,上面有設備 UID、網路設定、 以及「上傳 .mesb」按鈕。 這個畫面和正式版完全不同,看到它是正確的 —— 代表拓荒包燒成功了。

2A.3 在拓荒頁填入現場 Wi-Fi 帳號密碼並儲存,設備會重開

通過:設備重開後不再開熱點,改為連上現場網路。 沒過:又開出熱點 = 帳密錯誤或訊號不足。重連熱點再試一次。

2A.4 找出設備在現場網路上的新 IP → 見 §2B.2(兩種機型共用)


#階段 2B:連上設備 —— 有線機型(8310)

8310 沒有 Wi-Fi,不會開熱點。它一開機就用網路線去要一個 IP(DHCP), 所以問題不是「怎麼連上它」,而是**「它拿到哪個 IP」**。

2B.1 接上網路線,等 30 秒讓它取得 IP

2B.2 找出設備的 IP(兩種機型都適用)

專案內附一支掃描工具:

把下面這段存成 find-gateway.sh(放哪都可以),chmod +x find-gateway.sh 後執行:

bash
#!/usr/bin/env bash
# 找出網段上的 MES Gateway。判準是「誰以我們的方式回應 API」,不是 MAC ——
# 設備支援軟體 MAC 覆寫,實測過 API 回報 A8:61:0A、arp 表卻是 50:26:EF。
SUB=${1:?用法: ./find-gateway.sh 192.168.1}
TOKEN=${2:-<設備管理token>}
echo "掃描 $SUB.1-254 …"
for i in $(seq 1 254); do
  ( probe=$(curl -s -m 3 "http://$SUB.$i/api/system" 2>/dev/null)
    case "$probe" in *Unauthorized*|*boardModel*)
      info=$(curl -s -m 3 -H "Authorization: Bearer $TOKEN" "http://$SUB.$i/api/system" 2>/dev/null)
      v=$(printf '%s' "$info" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
      m=$(printf '%s' "$info" | sed -n 's/.*"boardModel":"\([^"]*\)".*/\1/p')
      echo "  OK  $SUB.$i   ${m:-(token 不符,無法讀型號)}   ${v:+v$v}" ;;
    esac ) &
done
wait
echo "掃描結束"

執行:./find-gateway.sh 192.168.1(換成你現場的網段前三碼)

powershell
# 直接貼進 PowerShell 7+ 執行,無需額外工具或檔案
$SUB   = "192.168.1"                 # 換成你現場的網段前三碼
$TOKEN = "<設備管理token>"
1..254 | ForEach-Object -Parallel {
  $ip = "$using:SUB.$_"
  try {
    $r = Invoke-WebRequest -Uri "http://$ip/api/system" -TimeoutSec 3 `
           -SkipHttpErrorCheck -ErrorAction Stop
    if ($r.Content -match 'Unauthorized|boardModel') {
      $info = Invoke-WebRequest -Uri "http://$ip/api/system" -TimeoutSec 3 `
                -Headers @{ Authorization = "Bearer $using:TOKEN" } `
                -SkipHttpErrorCheck -ErrorAction SilentlyContinue
      $m = if ($info.Content -match '"boardModel":"([^"]*)"') { $Matches[1] } else { "(token 不符)" }
      $v = if ($info.Content -match '"version":"([^"]*)"')    { "v" + $Matches[1] } else { "" }
      "  OK  $ip   $m   $v"
    }
  } catch {}
} -ThrottleLimit 64
# PowerShell 5.1 沒有 -Parallel/-SkipHttpErrorCheck,請改用 PowerShell 7+

把 192.168.1 換成你現場的網段前三碼(不知道的話,看自己電腦的 IP 前三碼)。

通過:印出類似 OK <網段>.46 Opta WiFi (8320) v<版本>, 這就是設備的 IP、機型和韌體版本。 沒過:什麼都沒找到 → 見下方「找不到設備時」。

這支工具是靠「誰以我們的方式回應 API」來認人,不是靠 MAC。 設備支援軟體 MAC 覆寫 —— 實測過一台設備 API 回報 A8:61:0A:…、 網路上的 arp 表卻是 50:26:EF:…,用 MAC 前綴去找會漏掉。

找不到設備時,依序試:

  1. 網段填錯:確認你電腦的 IP。ipconfig getifaddr en0(macOS)或 ipconfig(Windows)
  2. 查路由器的 DHCP 用戶端清單 —— 最可靠,直接列出所有連上的裝置
  3. 設備沒拿到 IP:確認網路線兩端都插好、對方交換器埠有燈
  4. 8320 而且從未設定過:它在 AP 模式,回到 §2A

2B.3 瀏覽器開 http://<找到的IP>

通過:看到「🧭 拓荒包 · MES Gateway 開荒」頁面(若還在拓荒包階段), 或完整的五頁籤設定畫面(若已裝正式版)。


#階段 2C:把正式版韌體裝上去

先確認你是從哪條路走到這裡的 —— 兩條路的畫面不一樣:

你剛做了什麼 現在看到的頁面 這一頁收什麼
§階段 1 空機燒拓荒包 拓荒頁(開荒設定頁) .mesb(按「上傳 .mesb」)
§階段 R 恢復原廠(機器本來就有正式韌體) 資產救援頁(標題「MES Gateway — 資產救援」) .gz 網頁資產,不吃 .mesb

走恢復原廠這條路的,請改用 §階段 R 那張表的兩條做法 (雲端部署 .mesb 或 救援頁逐檔安裝 .gz),下面 2C.2 的「拓荒頁」步驟不適用。 2C.3 的版本確認兩條路都要做。

拓荒包只是讓你能連上設備。正式版功能都還沒有 —— 沒有 RS485、沒有規則、沒有 MQTT。

2C.1 取得正式版韌體(.mesb 檔)

向專案負責人索取,或從雲端韌體清單下載當次交付指定的版本。

.mesb 是「韌體 + 網頁資源」打包成的單一檔案,用它更新可以一次到位。

2C.2 在拓荒頁按「上傳 .mesb」,選擇該檔案

通過:上傳完成後設備自動重開,約 2–3 分鐘後重新連上, 這時畫面變成完整的五個頁籤(總覽/配置/規則/設備/設定)。 沒過:上傳中斷 → 確認電腦與設備在同一網段、中途不要關瀏覽器。 拓荒包不會被破壞,失敗可以重來。

2C.3 確認版本

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/system | python3 -m json.tool | head -8
powershell
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/system | python -m json.tool | head -8

通過:"version" 與交付文件指定的版本一致。 沒過:版本不符 → 上傳到舊檔案了,回到 2C.1。 版本必須對得上,否則這份驗收單不成立。


#階段 2D:授權啟用 —— 交機必做,而且很容易漏

〈階段 R〉的 L2(交機標準做法)會把授權憑證一起清掉,所以歸零之後設備會跳 「裝置尚未授權」。文件前面提了 4 次「授權憑證會被清」,卻從沒說怎麼裝回去 —— 照著走完會交出一台未授權的機器。

2D.1 先看設備現在的狀態

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/license
powershell
curl.exe -s -H "Authorization: Bearer $env:TOK" "$($env:DUT)/api/license"

通過:{"status":"ACTIVE","licensed":true,...}。 看到別的狀態往下走 2D.2。

2D.2 分兩種情況 —— 先確認雲端有沒有這台的核准紀錄

bash
curl -s -H "Authorization: Bearer $CLOUD_TOK" "$CLOUD/api/license/token/$UID_DEV" \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print("  雲端狀態:",d.get("status"),"· 有簽章憑證:",bool(d.get("token")))'
powershell
curl.exe -s -H "Authorization: Bearer $env:CLOUD_TOK" "$($env:CLOUD)/api/license/token/$($env:UID_DEV)" `
  | python -c 'import sys,json;d=json.load(sys.stdin);print("  雲端狀態:",d.get("status"),"· 有簽章憑證:",bool(d.get("token")))'
雲端狀態 意思 你要做什麼
active 且有憑證 這台 UID 以前核准過 什麼都不用做 —— 設備會自己去雲端取回,幾分鐘內恢復 ACTIVE。畫面上那個「尚未授權」是取回前的暫態
其他 / 查不到 全新設備,沒核准紀錄 走 2D.3 申請

2D.3 全新設備:申請 → 核准 → 自動取件

  1. 設備網頁的「🔑 啟用授權」按鈕 → 送出申請(帶設備 UID)給雲端
  2. 管理員在雲端後台核准
  3. 設備自己輪詢取回簽章憑證並存起來 —— 不需要有人再到設備上按什麼

核准之後回 2D.1 確認變成 ACTIVE。

⚠️ 不要用畫面上的「尚未授權」判定失敗 —— 那是暫態。判準永遠是 GET /api/license 的 status。

(2026-09-08 走查踩到:歸零後畫面跳「裝置尚未授權」,而文件完全沒有這一段。 實測雲端 /api/license/token/<UID> 回 status: active + 156 字元簽章憑證、 expiresAt: 0(永久),設備隨即自行恢復 ACTIVE。)


#階段 3:網路設定收尾

3.1 確認設備連在正式網路上、IP 穩定

現場建議用固定 IP 或 DHCP 保留,否則設備重開後 IP 可能改變, 上位系統就找不到它了。

3.2 記錄設備 IP 與 UID,填進本單開頭,並更新環境變數

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/config | python3 -c "
import sys,json
d=json.load(sys.stdin)
print('  UID :',d.get('uid'))
print('  MAC :',d.get('mac'))
"
powershell
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/config | python -c "
import sys,json
d=json.load(sys.stdin)
print('  UID :',d.get('uid'))
print('  MAC :',d.get('mac'))
"

後續維運都靠這兩個值找設備,一定要記下來。


#階段 4:雲端備份設定

本階段的目的不是「有備份」,而是證明備份讀得回來。這兩件事不一樣,而且差別很危險 —— 備份可能只壞一半:推得上去、讀不回來。

步驟 動作 通過 沒過
4.1 設定雲端網址並確認有效身份 雲端已啟用,以明示 token 或 license fallback 讀本設備歷史成功 只檢查欄位有值不足以證明身份可用;401/403 為不通過
4.2 按「立即推送」 不要用這一步判定成功(見下方警語),往 4.3 去 連 4.3 都查不到 → 檢查對外是否放行 HTTPS
4.3 按「查看版本歷史」 見下方判準 —— 不是「看得到剛推的那一筆」 出現 Unauthorized → Token 沒填或不正確,回 4.1

#4.3 的判準:兩種都成立,挑一種做

雲端只在設定內容真的變了才建快照(剔除 checksum/timestamp 等記帳欄位後比對)。 所以「推完必須出現一筆新的」只有在你剛改過設定時才成立。

做法 怎麼判 強度
A. 先改一個設定再推(建議) 必須出現一筆新的快照,時間對得上 較強 —— 證明「新內容真的存得進去」
B. 什麼都不改直接看 最新那筆的 summary 是 無差異 證明「雲端手上那份 = 設備現況」

A 沒過才是真問題。B 顯示有差異而你剛推過 → 雲端手上的是舊的,要查。

🛑 走了 A 就只能用 A 的判準。 A 推完沒有出現新快照 = 沒過, 不可以回頭拿 B 的「無差異」來當通過 —— 那筆「無差異」是更早的舊快照, 它只證明「雲端手上那份舊的等於設備現況」,完全沒證明你剛剛推的有到。

推送狀態要核對時間。 先記錄 lastAttemptMs,再排程推送;確認新的嘗試為 result=OK、httpCode=200,並讀雲端快照核對內容。6.0.100 已修正失敗狀態與重試旗標,但舊成功紀錄仍不能證明這次送達。

A 做法的指令(改一個沒接東西的輸入通道名稱,最安全):

bash
# 改名 → 推送 → 查雲端。驗完記得用最後一條改回去。
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"type":"INPUT","index":7,"name":"AC-4.3-TEST"}' $DUT/api/io-name
sleep 5
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/configserver/push
sleep 20
# ← 接著跑下面 4.3 的查詢指令,最新那筆應該是剛剛的時間

🛑 驗完一定要改回去,否則交機的設備上會留著一個叫 AC-4.3-TEST 的通道:

bash
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"type":"INPUT","index":7,"name":"I8"}' $DUT/api/io-name

🛑 改回去之後一定要再推一次,否則殘留留在雲端。 上面那條只改了設備, 雲端手上那份還是 AC-4.3-TEST —— 日後真的要從雲端還原,它會被帶回設備:

bash
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/configserver/push
sleep 20
curl -s -H "Authorization: Bearer $CLOUD_TOK" "$CLOUD/api/devices/$UID_DEV/config" \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);n=json.dumps(d,ensure_ascii=False)
print("雲端手上那份:", "❌ 還有 AC-4.3-TEST" if "AC-4.3-TEST" in n else "✅ 已清乾淨")'

*(2026-09-08 走查踩到:v3.23 加了「驗完一定要改回去」,但只改設備。 這正是 WI-223「測試殘留自我延續」的同型 —— 而且是文件自己造成的。 教訓:凡是為了驗證而做的變更,清理範圍必須涵蓋所有被它污染的地方, 不能只清你當初動手的那一個。)*

A 的最硬判準:直接讀雲端手上那份設定,看改的名字在不在。 這條不受「只在內容變了才建快照」影響,沒有模稜兩可 —— 在「改回去」之前跑(改回去之後就看不到了):

bash
curl -s -H "Authorization: Bearer $CLOUD_TOK" \
  "$CLOUD/api/devices/$UID_DEV/config" \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);n=json.dumps(d,ensure_ascii=False)
print("雲端手上那份:", "✅ 有 AC-4.3-TEST" if "AC-4.3-TEST" in n else "❌ 找不到 —— 推送沒到雲端")'

(2026-09-06 走查踩到:文件叫人「先做一個設定變更」卻沒給指令,也沒說要改回去 —— 這正是 WI-223 那個「測試殘留自我延續」的同一個病。)

#4.1 指定 Cloud Token 的設定方式(使用 license fallback 時免填)

L2 恢復原廠會清掉 cloudToken。先確認授權已恢復,並用下方有效身份讀歷史;不因欄位空白直接判失敗。

bash
# 注意:JSON 不能有空格 —— 韌體是字串比對,有空格會靜默失敗
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d "{\"enabled\":true,\"cloudUrl\":\"$CLOUD\",\"cloudToken\":\"$CLOUD_TOK\"}" \
  $DUT/api/configserver

填完一定要讀回確認,不要憑「回了 success」就相信:

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/configserver \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print("  cloudUrl  :",d.get("cloudUrl") or "(空)");print("  cloudToken:","已填" if d.get("cloudToken") else "⚠ 空的")'

(2026-09-08 走查踩到:4.1 只寫「填入雲端網址與 Cloud Token」,沒有指令也沒指路 —— 而〈階段 R〉的 🛑 區塊裡其實有同一條,只是走第一部的人到不了那裡。)

使用與設備相同的有效雲端身份(不把 token 印出來):

bash
CLOUD_TOK=$(curl --fail --silent --show-error -H "Authorization: Bearer $TOK" "$DUT/api/configserver/auth?target=cloud" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("token", ""))')
curl --fail --silent --show-error -H "Authorization: Bearer $CLOUD_TOK" "$CLOUD/api/devices/$UID_DEV/snapshots?limit=1" | python3 -c 'import sys,json;d=json.load(sys.stdin);assert isinstance(d,list);print("歷史讀取成功,筆數:",len(d))'
powershell
$CLOUD_TOK = (Invoke-RestMethod -Uri "$DUT/api/configserver/auth?target=cloud" -Headers @{Authorization="Bearer $TOK"}).token
$r = Invoke-RestMethod -Uri "$CLOUD/api/devices/$UID_DEV/snapshots?limit=1" -Headers @{Authorization="Bearer $CLOUD_TOK"}
"歷史讀取成功,筆數: $(@($r).Count)"

這只驗證歷史讀取身份,4.3/8.1 仍須核對本次推送與快照內容。

用指令做同樣三件事(比按網頁按鈕更容易看出哪一步壞掉)

bash
# 4.1 明示 token 只印長度;空值請用有效身份讀歷史驗證
#     ⚠️ 欄位名要看清楚:cloudUrl/cloudToken 是**雲端**(內建、本階段要驗的);
#        url/token 是**地端**設定伺服器(第二備份目標,沒用到就是空的,不是錯誤)
curl -s -H "Authorization: Bearer $TOK" $DUT/api/configserver \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print("雲端 cloudUrl =",d.get("cloudUrl") or "(空!)");print("雲端 cloudToken 長度 =",len(d.get("cloudToken") or ""));print("地端 url =",d.get("url") or "(未使用)")'

# 4.2 觸發推送(回 success 只代表「已排程」,不代表已完成)
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/configserver/push

# 4.3 到雲端查版本歷史 —— 這一步才是真正的驗收點
#     ⚠️ limit 一定要帶,預設只回 20 筆,雜訊快照會把你要的那筆擠掉
#     時間戳是 epoch 毫秒,直接印出來人看不懂,一定要轉成本地時間
#     summary 是「該筆快照的值 → 設備現況」的差異;顯示「無差異」= 雲端手上這份等於設備現在的設定
curl -s -H "Authorization: Bearer $CLOUD_TOK" \
  "$CLOUD/api/devices/$UID_DEV/snapshots?limit=50" | python3 -c 'import sys,json,datetime
d=json.load(sys.stdin); l=d if isinstance(d,list) else d.get("snapshots",[])
def when(x):
    v=x.get("createdAt") or x.get("timestamp")
    try: return datetime.datetime.fromtimestamp(int(v)/1000).strftime("%m-%d %H:%M:%S")
    except Exception: return str(v)
print(f"共 {len(l)} 筆,最新 3 筆:")
[print("  ",when(x), (x.get("summary") or "")[:40]) for x in l[:3]]'

Unauthorized 出現在 4.3 而不是 4.2 —— 推送用的是設備 token,查歷史用的是雲端 token, 兩者不同。這是最常被混淆的一點。

⚠️ 驗 4.2/4.3 之前先做一個實際的設定變更(隨便改一個沒用到的通道名稱就好)。 雲端對內容相同的推送會去重 —— 設定沒變時推送照樣回成功,但不會產生新快照, 看起來就像推送失敗。走查時因此誤判過一次。

另外:設備端的雲端設定要查 /api/configserver,/api/config 裡沒有這個區塊 —— 拿 /api/config 去看會什麼都讀不到,誤以為 Token 沒填。

4.3 才是真正的驗收點。 實際發生過:推送成功、清單打不開,不特地點開就不會發現。

想用指令驗而不點畫面 —— 用 §R.2 的第 ③ 條(打雲端的 /api/devices/$UID_DEV/snapshots)。不要打設備的 /api/configserver —— 那條永遠不會吐清單,詳見 §R.2 的警告。

#階段 5:物理 I/O —— RS485 設備

先把「設備是誰、值從哪來」定義好。沒有這一層,後面的連動與 MQTT 上行都沒有素材可用。

5.1 掃描匯流排,確認實體設備都在

掃描是非同步的:POST 只是「按下開始」,馬上就返回,結果要另外輪詢。 它會逐一試 7 種鮑率 115200 / 57600 / 38400 / 19200 / 9600 / 4800 / 2400 × 3 種同位元 8E1 / 8N1 / 8N2(共 22 組,現場設定那組排第一個先試),要跑數十秒到數分鐘。

⚠️ 設備現在用的那組排在最前面 —— 所以現場設備通常很快就掃到, 但「掃描結束」要等 22 組全部試完。看到你要的設備出現就可以往下走, 不必等到最後。

bash
# ① 觸發掃描(body 不需要,韌體不看)
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/modbus/scan

# ② 輪詢到掃描結束為止
#    ⚠️ 掃描期間設備會拒連,curl 會吐空字串 —— 這裡一定要擋掉,
#       直接接 python3 -m json.tool 會噴一長串 traceback(看起來像壞了,其實是正常的)
for i in $(seq 1 20); do
  out=$(curl -s --max-time 8 -H "Authorization: Bearer $TOK" $DUT/api/modbus/scan 2>/dev/null)
  if [ -z "$out" ]; then
    echo "  $(date +%H:%M:%S)  還在掃描(設備拒連,正常)"
  else
    echo "$out" | python3 -c '
import sys,json,datetime
d=json.load(sys.stdin)
print(" ",datetime.datetime.now().strftime("%H:%M:%S"),
      "掃描中" if d.get("scanning") else "✅ 掃描結束")
for x in d.get("devices",[]):
    print("    slave",x.get("slaveId"),"machineId",x.get("machineId"),
          x.get("baud"),"bps  已加入=",x.get("alreadyAdded"))'
    echo "$out" | grep -q '"scanning":false' && break
  fi
  sleep 15
done
powershell
# ① 觸發掃描(body 不需要,韌體不看)
curl.exe -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/modbus/scan

# ② 輪詢到掃描結束為止(掃描期間設備會拒連,屬正常)
for ($i=0; $i -lt 20; $i++) {
  $out = curl.exe -s --max-time 8 -H "Authorization: Bearer $env:TOK" "$($env:DUT)/api/modbus/scan"
  if ([string]::IsNullOrWhiteSpace($out)) {
    "  $(Get-Date -f HH:mm:ss)  還在掃描(設備拒連,正常)"
  } else {
    $d = $out | ConvertFrom-Json
    "  $(Get-Date -f HH:mm:ss)  " + $(if ($d.scanning) {"掃描中"} else {"✅ 掃描結束"})
    $d.devices | ForEach-Object { "    slave $($_.slaveId)  machineId $($_.machineId)  $($_.baud) bps  已加入=$($_.alreadyAdded)" }
    if (-not $d.scanning) { break }
  }
  Start-Sleep -Seconds 15
}

⚠️ 掃描期間整台設備會「停住」約 1 分鐘 —— 那是正常的,不是當機。

停住的不只是網頁。實測(v6.0.78)單次掃描把主迴圈壓住 65 秒:

[SLOW LOOP] total=65532ms segs: ... scan=65202 ...

這 65 秒內:

  • 網頁與 API 拒連 —— 收到 Connection reset by peer 或整個逾時
  • 實體 I/O 不會動 —— 呼吸燈停住、DI 不被輪詢、規則不執行

它正逐一試 7 種鮑率 × 同位元 × 32 個 slave ID,而且是同步輪詢。 看門狗有逐台餵,所以不會重開(實測 bootCount 全程不變)。

因此:不要在掃描期間測按鈕,也不要因為呼吸燈停了就判定設備掛掉。 輪詢每 15 秒問一次,拒絕連線就當成「還在掃」,最長等 5 分鐘。

(這個成本已開卡追蹤 —— WI-209:掃描應切片化,或縮小預設掃描範圍, 至少 UI 按下去時要先告知「約 1 分鐘,期間設備不回應」。)

通過:輪詢結果中列出的 Slave ID 與硬體表一致。

注意回應的鍵是 devices(不是 results)—— 每一筆長這樣: {"slaveId":1,"machineId":48,"baud":9600,"parity":1043,"found":true,"alreadyAdded":true}。

⚠️ parity 那個看起來很怪的數字是正常的,不要抄進設定。 掃描結果的 parity 存的是 Arduino 平台常數(SERIAL_8N1 = 1043 = 0x413), 而設定裡同名的 parity 用的是 0=None / 1=Even / 2=Odd —— 同名不同義。 韌體內部自己也要翻譯(ApiHandlers_Modbus.cpp:if (newParity == SERIAL_8E1) cfgParity = 1;)。 看到 1043 不是壞掉,但把它填進設定欄位就會壞。 常見誤判:POST 立刻回 {"success":true,"scanning":true} —— 那只代表「開始掃了」, 不是掃描結果。看到它就以為完成,會誤判成「一台都沒掃到」。 注意:掃到只代表「設備活著」,不代表讀得到值 —— 讀值還需要暫存器設定正確。

5.2 逐台加入 —— ⚠️ 必須用網頁新增,不能用 CLI

這一步是本文件唯一「不提供 CLI」的步驟,而且是刻意的。 型號的暫存器模板(要讀哪個位址、怎麼解讀)只存在於網頁端, 是瀏覽器幫你填好之後才送出去的。用 curl 直接 POST 會建出一台 沒有任何暫存器的設備 —— 它在清單上看得到、online 也是 true, 但永遠讀不到值,而且不會有任何錯誤訊息。

操作路徑:

設備頁 → RS485 設備 → +新增 →

欄位 填什麼
名稱 現場看得懂的名字(例:鑽床1電流),不要用 Device1
型號 計數器選 SF965;電流計選 Finder 6M
Slave ID 該設備實際設定的位址
輪詢間隔 2000 ms

選了型號之後,暫存器會自動帶入 —— 看到下方「自訂暫存器」清單被填滿, 才代表模板套上了。如果那個清單是空的,不要儲存,重選一次型號。

一次只加一台,加完立刻做 5.3 確認讀得到值,再加下一台。 全部加完才一起測,出問題會分不清是哪一台。

若回報 Slave ID already in use → 該 ID 已被佔用(包含閘道器自己的 slaveId),換一個。

5.3 確認讀得到值 —— 唯一可信的判準

🛑 不要和瀏覽器同時打設備。 設備是單執行緒、只有 4 個 socket —— 網頁開著自動刷新的時候再用 curl 去打,會間歇拿到空回應(python 就噴 Expecting value: line 1 column 1)。那不是設備故障,是被你自己擠掉的。 驗證期間把設備網頁分頁關掉,或接受偶爾要重跑一次。

🛑 跑兩次,中間隔幾秒。 「值是 0」與「根本沒讀到」在畫面上長得一模一樣, 而真實量測會跳動。只跑一次看到數字就打勾,可能是在看一個死掉的鏈路。 兩次數字完全一樣(尤其都是 0)→ 先當成沒讀到,往 5.2 的暫存器模板查。

電流計類(Finder:電壓/電流/功率是固定欄位):

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/poll | python3 -c '
import sys,json
d=json.load(sys.stdin)
for p in d.get("power",[]):
    print("  ", p.get("name"), " online=", p.get("online"), " V=", p.get("voltage"), " A=", p.get("current"))
'
powershell
curl.exe -s -H "Authorization: Bearer $env:TOK" "$($env:DUT)/api/poll" | python -c '
import sys,json
d=json.load(sys.stdin)
for p in d.get("power",[]):
    print("  ", p.get("name"), " online=", p.get("online"), " V=", p.get("voltage"), " A=", p.get("current"))
'

計數器類(SF965:值在自訂暫存器,不在 power 陣列裡):

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/modbus | python3 -c '
import sys,json
d=json.load(sys.stdin)
print("  暫存器目前值:", d.get("registers"))
'
powershell
curl.exe -s -H "Authorization: Bearer $env:TOK" "$($env:DUT)/api/modbus" | python -c '
import sys,json
d=json.load(sys.stdin)
print("  暫存器目前值:", d.get("registers"))
'

通過:電流計 online=True、數值會跳動(見上方 🛑);計數器的暫存器值會隨現場觸發而增加。

#🛑 「數值合理」到底要看什麼 —— 有功功率是負的 = CT 夾反方向

這是現場最常見的接線錯,而且每一項表面指標都正常:線通、online=True、 電壓電流都對、數字也會跳動 —— 只有符號是反的。

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/poll | python3 -c '
import sys,json
for p in json.load(sys.stdin).get("power",[]):
    ap, pf = p.get("activePower"), p.get("powerFactor")
    nm = p.get("name")
    print(f"  {nm}  有功功率={ap} W  功率因數={pf}")
    if ap is not None and ap < 0:
        print("    ⚠️ 有功功率為負 —— CT 夾反方向,或 L/N 接反。把 CT 轉 180° 重夾")'

負載明明在耗電、有功功率卻是負的 → CT 方向錯。轉 180° 重夾即可, 不必改設定。交機前一定要修 —— 否則上位系統看到的用電量全是負的。

⚠️ 欄位名是 activePower,不是 power。 power 這個鍵不存在, 用它去讀永遠拿到 None,會誤判成「讀不到功率」。 完整欄位:voltage / current / activePower / reactivePower / apparentPower / powerFactor / frequency / thd / energy / energyPositive / energyNegative。

(2026-09-08 走查實測:桌上 Finder 6M.TB activePower=-5.18、powerFactor=-0.15, 而畫面十二個數字全部有值、看起來一切正常。) power 陣列只有電力類欄位(電壓/電流/功率),計數器的值不會出現在那裡 —— 拿電流計的指令去驗計數器,會看到一排 0 而誤判成故障。

這段指令能跑完不報錯,本身就是一項驗證 —— 若回應不是合法 JSON,Python 會直接拋錯。

分辨「沒人答」還是「答了但解不開」:

bash
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"slaveId":4,"functionCode":3,"regAddress":0,"count":2}' $DUT/api/modbus/probe
powershell
curl.exe -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" `
  -d '{"slaveId":4,"functionCode":3,"regAddress":0,"count":2}' $DUT/api/modbus/probe

回 0 bytes = 沒人答(線路/位址/電源);有 byte 但解不開 = 撞號或格式錯。

5.4 現場觸發計數器,看畫面數字是否跟著跳

通過:面板顯示值與網頁顯示值一致,觸發一次就加一。這是業主最直觀的驗收點,請當場示範。 沒過:讀到 0 → 依序查接線極性 → Slave ID → 鮑率 → 該台有沒有電。數值有但對不上面板 → 型號模板選錯。

5.5 全部加完後重開設備,再確認一次

bash
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/reboot
# 等約 60 秒後重跑 5.3
powershell
curl.exe -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/reboot
# 等約 60 秒後重跑 5.3

通過:重開後設備清單完整、數值正常。這一步證明設定真的寫進去了,不只是畫面上有。 沒過:重開後設定不見 → 立即停止驗收並回報,不可帶病上線。


#階段 6:I/O 連動

6.1 看目前實體 I/O 狀態

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/io | python3 -m json.tool
powershell
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/io | python -m json.tool

通過:看得到 di / do 陣列(di 8 個、do 4 個,擴充模組另在 expDi / expDo)。

🛑 上面那條指令只給你一張快照,看不出「會不會變」。 要驗「現場觸發時對應位置會變」, 必須持續監看。用專案內附的工具(它只在變化的那一刻印一行):

bash
python3 watch_device.py io
powershell
python watch_device.py io

⚠️ 輪詢要夠密才抓得到脈衝式的輸入。 觸控鈕、光電開關這類送的是短脈衝, 1 秒輪詢一次會整個錯過 —— 看起來就像「按了沒反應」。

⚠️ 擴充模組的輸出要看 expDo 的巢狀內容,不是插槽本身。 expDo 是「5 個插槽 × 每槽 8 通道」;只看插槽在不在([[...], null, null, null, null]) 是個常數,永遠不會變 —— 會得到「明明有動作、畫面卻沒變」的假象。

(2026-09-08 補:這三個坑都是實際走查踩過的 —— 監看主機 DO 卻按了綁在擴充 DO 的按鈕、 1 秒輪詢漏掉觸控鈕脈衝、把插槽當成通道。「我看不到」不等於「它沒動」。)

6.2 直接驅動輸出,確認實體會動(不經規則,單純驗硬體)

bash
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"index":2,"value":true}' $DUT/api/io/do      # DO3 = index 2(0 起算)
sleep 2
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"index":2,"value":false}' $DUT/api/io/do
powershell
curl.exe -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" `
  -d '{"index":2,"value":true}' $DUT/api/io/do      # DO3 = index 2(0 起算)
Start-Sleep 2
curl.exe -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" `
  -d '{"index":2,"value":false}' $DUT/api/io/do

先確認硬體會動,再談規則 —— 否則規則不動時分不清是規則錯還是輸出壞。 欄位是 index/value,不是 channel/state。打錯欄位名會回 400 並附提示。

#🛑 {"success":true} 不是通過判準

它只代表「API 收到了」。上面那段還有第二個問題:只亮 2 秒,人很容易錯過。 改成「設起來 → 讀回 → 看/聽 → 放掉」,三個證據都要:

bash
# ① 設起來(先不要急著放掉)
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"index":2,"value":true}' $DUT/api/io/do

# ② 讀回 —— 第 3 個位置(0 起算的 index 2)必須是 true
curl -s -H "Authorization: Bearer $TOK" $DUT/api/io | python3 -c '
import sys,json
do = json.load(sys.stdin).get("do") or []
print("  do =", do)
print("  index 2 =", do[2] if len(do) > 2 else "讀不到",
      "← 必須是 True,否則韌體根本沒把它設起來")'

# ③ 這時候去看現場:對應的燈/繼電器應該是動作中。確認完再放掉
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"index":2,"value":false}' $DUT/api/io/do

三個證據:

證據 意思
API 回 success 指令送到了
/api/io 的 do[2] 變 True 韌體真的把它設起來了(不只是收到)
繼電器「喀噠」一聲 / 對應的燈亮 實體真的動了

前兩個綠、第三個沒有 → 輸出接線或該路繼電器的問題,不是軟體。 這個分界很重要,否則後面規則不動時會一路往設定裡找。

⚠️ index 是 0 起算,面板上的 DO1 = index 0。 想驗面板的 DO3 就是 index 2 —— 舊文件曾經整份偏一路,照著打會驅動到隔壁那一路。

⚠️ DO3 上接了什麼,取決於現場接線。 沒接任何東西的話第三個證據就只剩 繼電器的聲音 —— 那也算數,Opta 的繼電器動作時聽得見。

#🛑 ④ 測完一定要確認「真的放掉了」—— 這是安全項,不是禮貌

「放掉」那一步也可能失敗,而且失敗得很安靜:設備忙的時候(掃描中、 網頁開著、多人同時操作)那一次 POST 可能整個掉掉。你以為測完了, 繼電器其實還吸著 —— 現場那可能是一台還在轉的機器。

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/io | python3 -c '
import sys,json
do = json.load(sys.stdin).get("do") or []
print("  目前 do =", do)
print("  ", "✅ 全部放掉了" if not any(do) else "❌ 還有輸出在動作 —— 立刻放掉再離開")'

(2026-09-08 走查踩到:驗證期間刻意製造並發壓力,「放掉」那次 POST 被擠掉了, 而指令的輸出被導到 /dev/null,完全看不出來 —— 繼電器就這樣留在動作狀態。 教訓:把輸出丟掉的指令,等於放棄知道它有沒有成功。 關鍵步驟不要靜音。)

6.3 幫 I/O 通道命名 —— 做規則之前一定要先做

#🛑 為什麼這一步不能跳

第二部情境 A 的「輸入群組」「輸出群組」兩個下拉,列出來的就是這些名字。 沒命名的話下拉裡是一排 I1 I2 … DO 1 DO 2 —— 你認不出哪一路接了什麼,選錯了畫面也不會報錯,規則建好卻驅動到隔壁那路。

階段 R 的 L2 恢復原廠會把名字全部清回預設。 實測歸零後: inputNames = ['I1'…'I8']、doNames = ['DO 1'…'DO 4']。 交機前一定要重新命名 —— 否則業主拿到的是一台「看不懂自己接了什麼」的機器。

網頁做法:配置頁 → 實體 I/O → 點該通道的名稱欄位直接改 → 儲存。

指令做法(現場一次設完比較快):

bash
# 一次設一路,而且**每一行都印出自己是哪一路** ——
# 不標籤的話三個一模一樣的 {"success":true} 掉了一個你也看不出來(走查實際踩到)。
# -sS 而不是 -s:-s 連「連不上」都吞掉,-sS 是安靜但錯誤照報。
setname() {   # setname <type> <index> <名稱> [擴充槽號]
  local body="{\"type\":\"$1\",\"index\":$2,\"name\":\"$3\""
  [ -n "$4" ] && body="{\"type\":\"$1\",\"exp\":$4,\"index\":$2,\"name\":\"$3\""
  printf '  %-6s %-3s %-12s → ' "$1" "$2" "$3"
  curl -sS -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
    -d "$body}" $DUT/api/io-name
  echo
  sleep 2   # 設備是單執行緒,連發容易被丟掉
}

# 主機:輸入 index 0-7、輸出 index 0-3,都是 0 起算(面板 DO1 = index 0)
setname INPUT 0 "Toggle開關"
setname DO    2 "燈1-藍"

# 擴充模組:最後多一個參數 = 槽號(0 起算)。輸入 0-15、輸出 0-7
setname DO    2 "燈2-綠" 0
powershell
curl.exe -s -X POST -H "Authorization: Bearer $env:TOK" -H "Content-Type: application/json" `
  -d '{\"type\":\"INPUT\",\"index\":0,\"name\":\"Toggle開關\"}' "$($env:DUT)/api/io-name"

⚠️ type 只吃兩個值:INPUT 與 DO。 不是 OUTPUT、不是 DI。

🛑 打錯會「安靜地失敗」 —— 型別或 index 超出範圍時,韌體照樣回 HTTP 200,只是內容變成 {"success":false}。不要只看 HTTP 狀態碼, 一定要看 success 那個欄位,而且要讀回確認:

bash
IO=$(curl -s -H "Authorization: Bearer $TOK" $DUT/api/io)
curl -s -H "Authorization: Bearer $TOK" $DUT/api/config | IO="$IO" python3 -c '
import sys, json, os, re
cfg = json.load(sys.stdin)
io  = json.loads(os.environ["IO"])
DEFAULT = re.compile(r"^(I\d+|DO ?\d+|Exp\d+_[IO]\d+)$")
present = [i for i, x in enumerate(io.get("expDo") or []) if x]
print("  主機輸入:", cfg.get("inputNames"))
print("  主機輸出:", cfg.get("doNames"))
left = [n for n in (cfg.get("inputNames") or []) + (cfg.get("doNames") or [])
        if DEFAULT.match(n or "")]
for i in present:
    outs = ((cfg.get("expansions") or [{}])[i] or {}).get("doNames") or []
    print(f"  擴充槽{i+1} 輸出:", outs)
    left += [n for n in outs if DEFAULT.match(n or "")]
print("  實際插著的擴充槽:", [i + 1 for i in present] or "(無)")
print("  還沒命名的:", left if left else "(無,全部命名完成)")'

🛑 /api/config 的 expansions 永遠回 5 個槽,不代表你插了 5 個模組。 它是「最多支援 5 槽」的容器,每一槽都預先填好 Exp0_O1…Exp4_O8 的預設名。 直接照它去數,會得到**一百多個「還沒命名」**的假清單。 實際插了哪幾槽,只有 /api/io 的 expDo 哪幾個不是 null 才知道 —— 所以上面這條要同時讀兩個端點。

這份清單只涵蓋「主機 I/O + 實際插著的擴充輸出」。 沒接東西的通道 不需要命名 —— 判準是「規則會用到的都認得出來」,不是「每一格都填滿」。

#🛑 擴充輸出:命名與驅動用的是【兩套不同的編號】

這是「偏一路 / 偏兩路」這類誤會的根源 —— 同一路實體輸出,兩個 API 這樣寫:

要做什麼 端點 怎麼指定「擴充槽1 的第 3 路」
命名 /api/io-name {"type":"DO","exp":0,"index":2}
驅動 /api/io/do {"index":18} ← 16 + 通道

驅動端的編碼(與規則引擎、Modbus TCP 同一套): 0-3 = 主機 DO1-DO4;16+ch = 擴充槽 0(16=DO1 … 23=DO8); 32+ch = 擴充槽 1,依此類推。注意 4-15 是空的,不是接著主機往下排。

⚠️ 命名完務必實際驅動一次核對(用 6.2 的做法),確認 「名字說的那一路」跟「真的會亮的那一路」是同一路 —— 走查實測過設備上存的擴充通道名與面板標示對不上。

bash
# 逐路點一遍,一路一路確認實體是哪一顆(擴充槽1 = index 16..23)
for i in 16 17 18 19 20 21 22 23; do
  printf '  index %s (擴充槽1 第 %s 路) 亮 3 秒… ' "$i" "$((i-15))"
  curl -sS -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
    -d "{\"index\":$i,\"value\":true}" $DUT/api/io/do
  sleep 3
  curl -sS -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
    -d "{\"index\":$i,\"value\":false}" $DUT/api/io/do
  echo
  sleep 1
done

模組沒插的話會明確回 {"error":"expansion module not connected"} —— 不會靜默成功,所以不用擔心對著空槽測了半天。

🛑 跑完照 6.2 的第 ④ 條確認全部放掉了。


#第二部:情境驗收

前面驗的是「裝好了」,這一部驗的是照情境會動。每個情境都標註出廠測試中守著同一件事的測項編號 —— 驗收與出廠測試看的是同一組行為,不是兩套標準。

#情境 A:I/O 互動

前置:A1/A3 要驗的「規則」必須先建立。 規則不會憑空存在, 而且它需要兩個先決條件 —— 一個輸入群組、一個輸出群組。

⚠️ 你需要建【兩個】輸入群組,不是一個。 A1 要實體 DI 當來源、A2 要 MQTT 命令當來源,兩者不能共用同一個群組。下面 A.0-1 請做兩次。

撥開關的時候掛著這個 —— 只在狀態變化時印出來

bash
python3 watch_device.py io
powershell
python watch_device.py io

撥開關時它會只在 DI/DO 變化的那一刻印一行:

監看中,按 Ctrl-C 停止…
  14:22:03  DI [0, 0, 0, 0]   DO [0, 0, 0, 0]
  14:22:07  DI [1, 0, 0, 0]   DO [1, 0, 0, 0]     ← 撥上,輸出跟著動
  14:22:11  DI [0, 0, 0, 0]   DO [0, 0, 0, 0]     ← 撥回,復歸

看不到 DI 翻轉,代表訊號沒進到設備 —— 那是接線或供電,不是規則設定。 DI 有翻、DO 不動,才輪到檢查輸入群組的「觸發類型」與規則(見下)。 這一步先做,可以省下大量在設定頁裡瞎猜的時間。

⚠️ 剛按過「掃描匯流排」就不要測按鈕 —— 掃描期間整台設備停住約 1 分鐘, DI 不會被輪詢,你會以為開關壞了(WI-209)。等掃描結束再測。

A.0-1 建立輸入群組(觸發條件)

配置頁 → 📡 輸入群組 → 右上角 + → 名稱(例:按鈕1按下)→ + 加設備 → 選來源 → 設比較條件 → 儲存

按鈕叫 + 加設備,不是「新增來源」。

⚠️ 「觸發類型」這一欄決定 A1 的後半段會不會成立 —— 務必看懂再選

新增輸入群組的畫面上有一個「觸發類型」下拉,預設是 Always。它決定訊號在什麼 時機才算「觸發」,直接影響規則會不會執行:

觸發類型 行為 用在 A1 的後果
Always(預設) 每個主迴圈都觸發(準位模式) 會動,但持續重複驅動輸出
Rising 只在 0→1 那一瞬間觸發 「解除→復歸」永遠不會發生,A1 必定失敗
Falling 只在 1→0 那一瞬間觸發 只會復歸,不會動作
Change 任何變化都觸發(0→1 與 1→0 皆是) ✅ A1 要選這個

原因在韌體的規則比對:if (rule->triggerOnTrue != signalValue) continue;。 選 Rising 時只有 signalValue == true 的那一刻會觸發,於是 觸發條件 = 信號為 False 的那條規則永遠不會被執行。

「按鈕按下」直覺上會讓人選 Rising —— 那正是這一題的陷阱。

例外:脈衝式輸入要選 Rising + 輸出用「切換」。 觸控式按鈕這類只給短脈衝(1 → 幾秒內自動回 0)的輸入,不是準位, 要搭配輸出模式「切換」才會有一按一放的效果。

A.0-2 建立輸出群組(要做什麼)

配置頁 → 輸出群組 → 右上角 + → 名稱(例:開燈1)→ + 加設備 → 選 DO 通道與動作模式 → 儲存

輸出模式:直接=跟隨、切換=每次觸發翻面、脈衝=定時、關閉=強制 OFF。

⚠️ 不要一口氣連續建立多筆。 每建好一筆,等清單重新整理出現該筆之後再建下一筆。 連續快速新增會讓前一筆被無聲覆蓋,而且兩次都回成功(見 WI-199)。

A.0-3 建立規則(把兩者綁起來)

規則頁 → 新增 →

欄位 填什麼
規則名稱 看得懂的名字
觸發信號 (輸入群組) 選 A.0-1 建的那個
觸發條件 信號為 True(解除時要復歸的話,另外再建一條 信號為 False 的規則)
執行動作 (輸出群組) 選 A.0-2 建的那個

⚠️ 兩個下拉都必須真的選到東西。 畫面會擋: 「請先選擇『觸發信號 (輸入群組)』與『執行動作 (輸出群組)』。未綁定的規則永遠不會觸發。」 若下拉是空的、顯示「請先新增輸入群組」,表示 A.0-1/A.0-2 還沒做完。

存完重新整理頁面再看一次 —— 確認兩個欄位仍顯示正確的選項,而不是又變回空白。

編號 動作 通過 出廠對應
A1 實體輸入驅動實體輸出 輸入成立→輸出動作;解除→復歸。連續 3 次一致 H5 · H2
A2 MQTT 命令驅動實體輸出
mq_pub -t "$PREFIX/cmd/signal/1" -m '1'
(A.0-1 的輸入群組來源要選「📨 MQTT 命令訊號」並綁 cmd/signal/1)
送出後數秒內輸出動作 CV.2
A3 停用規則→觸發;重新啟用→觸發 停用後完全不動;重新啟用恢復正常 EN.1

A1 只成功一次不算通過 —— 間歇問題只有重複才看得出來。 A3 兩個方向都要驗:「停用了還會動」= 業主以為關掉、機器卻自己跑(安全問題);「重新啟用回不來」= 現場只能重開機碰運氣。

⚠️ A3 的假 FAIL 陷阱(Change 觸發專屬,實測踩到過)

若輸入群組的觸發類型是 Change,而你在規則停用期間已經送過一次觸發值 (例如 MQTT 送了 1),那麼重新啟用後再送一次相同的值不構成變化 → 不觸發 → 輸出不動。

這不是「重新啟用失敗」,是測試步驟本身沒有製造變化。

正確做法:重新啟用之前先把訊號送回原值(送 0),再送 1。 實體 DI 同理 —— 先把開關撥回原位再撥一次。

MQTT topic 前綴用 UID,不是設定頁顯示的那個。 設備「設定」頁的 topicPrefix 可能顯示成 mes/gateway/001 之類的短代號, 但實際訂閱/發佈用的是 UID:mes/gateway/<24 碼 UID>/...。 一律以本文件開頭的 PREFIX=mes/gateway/$UID_DEV 為準;照設定頁的顯示值發命令會石沉大海。 要確認的話,訂閱 mes/gateway/# 看設備實際發出來的 topic 長什麼樣。

#情境 M:MQTT 的三種通道模式 —— 先搞清楚差別

新增 TCP 通道、協定選 MQTT 之後,會出現一個「模式」下拉,三個選項的用途完全不同。 選錯的話畫面上不會報錯,但行為完全不是你要的。

模式(介面文字) 方向 它做什麼 值的型別
⚡ 收取並觸發動作 (Legacy) 入向/出向 收到訊息就觸發(或比對到特定值才觸發);出向時在動作發生後送出固定字串 開關(成立/不成立)
📥 收取並擷取數值 (Parser) 入向 從收到的 JSON 中抽出數值存進通道,供規則比較或 Converter 引用 數值
組合並發佈訊息 (Converter) 出向 把資料組合成 JSON 發佈出去(見情境 B) —

一句話分辨: 要「有沒有發生」→ Legacy。要「是多少」→ Parser。要「送出去」→ Converter。


M.1 — Legacy 入向:收到指定訊息就觸發

目的:上位系統送一個命令,設備就動作。不關心數值是多少,只關心「這件事發生了沒」。 例:{"cmd":"start"} 一到就啟動某個輸出。

設定:配置頁 → TCP 通道 → +新增 → 協定 MQTT、方向 輸入、 模式 ⚡ 收取並觸發動作 (Legacy) →

欄位 填什麼
Topic <你的 $PREFIX>/cmd/start
JSON 欄位 cmd — 留空則比對整個 Payload
比對值 start — 留空則「收到訊息就觸發」,不管內容
bash
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"start"}'      # 應觸發
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"other"}'      # 不應觸發
powershell
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"start"}'      # 應觸發
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"other"}'      # 不應觸發

通過:送 start → 通道狀態變成成立(總覽頁看得到,或綁規則後輸出動作); 送 other → 完全沒有反應。 沒過:兩個都觸發 → 比對值沒填,變成「收到就觸發」。 兩個都不觸發 → JSON 欄位名打錯,或 Topic 不符(大小寫要完全一致)。

M.2 — Legacy 的「留空」語意(這是最容易誤解的地方)

bash
# 把「比對值」清空後再送一次
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"whatever"}'
powershell
# 把「比對值」清空後再送一次
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"whatever"}'

通過:比對值留空時,任何內容都會觸發 —— 這是設計,不是故障。 為什麼要驗這個:現場常見「我明明設了條件,怎麼什麼都會動」, 十之八九就是比對值留空。當場示範一次,業主就懂了。

M.3 — Parser 入向:從 JSON 抽出數值

目的:上位系統送一包 JSON,設備要把其中一個數字取出來, 之後才能拿它做門檻比較(例:溫度 > 80 就報警)或塞進 Converter 發佈。

設定:新增通道 → 協定 MQTT、方向 輸入、模式 📥 收取並擷取數值 (Parser) → Topic 填 <你的 $PREFIX>/data/temp → 在解析欄位區 新增至少一個欄位 (欄位名對應 JSON 的鍵,例 temp)。

介面會擋:「請至少新增一個解析欄位。」沒新增就存不了。

bash
mq_pub -t "$PREFIX/data/temp" -m '{"temp":25.5,"hum":60}'
sleep 3
curl -s -H "Authorization: Bearer $TOK" $DUT/api/tcpio | python3 -c "
import sys,json
for c in (json.load(sys.stdin).get('channels') or []):
    if c.get('name'): print(f\"  {c.get('name')}  目前值={c.get('currentValue')}\")
"
powershell
mq_pub -t "$PREFIX/data/temp" -m '{"temp":25.5,"hum":60}'
Start-Sleep 3
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/tcpio | python -c "
import sys,json
for c in (json.load(sys.stdin).get('channels') or []):
    if c.get('name'): print(f\"  {c.get('name')}  目前值={c.get('currentValue')}\")
"

通過:該通道的 currentValue 變成 25.5(不是 0、不是 1)。 這是 Parser 與 Legacy 的關鍵差別 —— Legacy 只會給你「成立/不成立」, Parser 給的是真正的數字。 沒過:值一直是 0 → 解析欄位名與 JSON 的鍵不符(大小寫要一致)。 值變成 1 → 模式選成 Legacy 了。

M.4 — Parser 的數值拿去做規則判斷:能到什麼程度(這裡有一個硬限制)

#🛑 先講清楚,免得現場白試半小時

Parser 抽出來的數字,不能拿去設數值門檻。 規則引擎讀 TCP 通道時只讀它的 布林狀態(值 ≠ 0 → 成立、值 = 0 → 不成立),不比較那個數字本身。

所以「溫度 > 80 就報警」這種需求,目前這條路做不到 —— 要嘛請上位系統自己判斷後送一個 0/1 過來,要嘛走 RS485 暫存器那條路(那條支援數值門檻)。

韌體會當場擋下來,不會讓你存進一個永遠不成立的條件:

json
{"error":"condition can never be true",
 "reason":"TCP IO channel sources are boolean (0/1); the parsed numeric value is not compared",
 "hint":"use op '==' with threshold 1 to mean 'non-zero'"}

設定:配置頁 → 📡 輸入群組 → 新增 → 來源選 M.3 那個 Parser 通道 → 比較條件設 = 1(意思是「非零」)→ 儲存 → 綁一條規則到某個輸出。

bash
mq_pub -t "$PREFIX/data/temp" -m '{"temp":0}'      # 值為 0  → 不成立
sleep 4
mq_pub -t "$PREFIX/data/temp" -m '{"temp":35.0}'   # 值非 0  → 成立
sleep 4
mq_pub -t "$PREFIX/data/temp" -m '{"temp":0}'      # 回到 0  → 復歸
powershell
mq_pub -t "$PREFIX/data/temp" -m '{"temp":0}'      # 值為 0  → 不成立
Start-Sleep 4
mq_pub -t "$PREFIX/data/temp" -m '{"temp":35.0}'   # 值非 0  → 成立
Start-Sleep 4
mq_pub -t "$PREFIX/data/temp" -m '{"temp":0}'      # 回到 0  → 復歸

通過:0 → 不成立、35 → 成立、再回 0 → 復歸。 同時 /api/tcpio 的 currentValue 要跟著變(0 → 35 → 0)—— 數值有被抽出來,只是規則只看它是不是 0。 沒過:一直不成立 → 見下方「來源要填 channelId」。

#⚠️ 兩個會讓你白忙的細節

① 來源的「index」是 channelId,不是通道在列表裡的第幾個。 用 API 建輸入群組時,sources[].index 要填 GET /api/tcpio 回傳的 channelId (例 81),填成陣列位置(1)的話會查不到通道,條件永遠不成立且沒有錯誤訊息。 走網頁介面不會踩到這個 —— 它會幫你填對。

② 來源型別要用 7(TCP 通道),不要用 9。 型別 9(CH_TCPIO)送得進去、HTTP 200、列表上看得到,但規則引擎沒有處理這個型別 —— 條件永遠不成立,而且上面那道「永遠不成立」的守衛也擋不到它(它只檢查型別 7)。 實測:型別 9 配 > 30,送 25.5 / 35.0 / 99.9 進去,訊號三次都是 False。 (已開卡 WI-196。同樣走網頁介面不會踩到。)


#情境 B:MQTT Converter —— 兩個軸,四種組合

Converter 有兩個彼此獨立的設定軸,搞混就會得到「資料太多」或「該來的沒來」。

下表格子裡的編號 = 底下對應的測試步驟編號。B3 不在這張表裡 —— 它驗的是「這個開關到底歸不歸你控制」(可逆性),跨在兩個象限之上。

連續傳送(不勾「🔁 變更時才發」) 不重複傳送(勾選)
持續(無閘門) B1 每 N 秒固定發,值沒變也發
用途:當心跳,證明設備還活著
B2 每 N 秒檢查,值變才發
用途:電流環等連續量測
啟動式(閘門觸發) B4 閘門開→每 N 秒發;關→停
用途:班別/工單期間才回報
B4b 閘門開且值變才發
用途:計數器事件回報

先開一個終端機常駐訂閱,以下所有情境都在它上面看結果:

bash
mq_sub -t "$PREFIX/#" -v | while read -r line; do
  echo "$(date +%H:%M:%S)  $line"
done
powershell
mq_sub -t "$PREFIX/#" -v | while read -r line; do
  echo "$(date +%H:%M:%S)  $line"
done

#🛑 模板寫錯來源編號 → 靜默送出 0,不是報錯

${rs485.N.欄位} 的 N 是 Modbus Slave ID,不是清單上的第幾台。 韌體找不到那個 slave 就 return 0.0f —— 沒有錯誤、沒有 log、照樣每 5 秒準時發, 上位系統收到的是一串漂亮的 0,看起來就像「機台在待機」。

// src/ConverterEngine.h  resolveRs485Field()
if (ai < 0) return 0.0f;      // ← 找不到 slave 就靜默回 0

走查實測(2026-09-08):把桌上那台 slave 1 的電表寫成 ${rs485.0.…} → 每 5 秒準時發 {"電壓":0.0,"電流":0.00,"功率":0.0},而同一時間 /api/poll 讀到的是 112.6V / 0.30A。改成 ${rs485.1.…} 立刻正常。

驗收判準:設好模板之後訂閱看實際 payload,拿它跟 /api/poll 的值對一次。 「有訊息進來」不算過 —— 值要對得上。全是 0 就先查 slave ID。

可用欄位:voltage current power reactive apparent pf freq thd kwh kwhpos kwhneg;加 :N 指定小數位(同時也是死區,見 B2)。

⚠️ 另外:來源設備離線時整筆不發(v6.0.6)—— 這是刻意的, 免得送 {"counter":0} 讓 MES 誤判成「計數歸零」。所以「突然完全沒訊息」 要先看那台 RS485 設備是不是掉線了,不是 Converter 壞掉。

B1 — 電流環定期回報(持續 × 連續) | 出廠對應 CV.3

目的:不論值有沒有變,每隔固定秒數都要有一筆,讓 MES 能畫連續趨勢圖、也能當「設備還活著」的心跳。此時「重複」是刻意要的。

設定路徑(介面上沒有「來源」下拉,值是用 JSON 模板的變數插進去的):

配置頁 → TCP 通道 → +新增 → 協定 MQTT、方向 輸出、模式 Converter → 展開綠色「Converter 設定」區 →

欄位 填什麼
Topic <你的 $PREFIX>/current ← 必須在 $PREFIX 底下,否則 mq_sub -t "$PREFIX/#" 收不到
推送間隔 (秒) 5
JSON 模板 {"current":${rs485.1.current}} ← 1 換成該設備的 Modbus Slave ID

⚠️ 變數的欄位名必須用英文,寫中文會靜默得到 0。

韌體 resolveRs485Field() 只認這些名稱: voltage/current/power/reactive/apparent/pf/freq/thd/kwh/kwhpos/kwhneg

中文名(例如 電流)只有在剛好等於某個自訂暫存器的名稱時才解得出來。 型號模板設備(Finder 6M、SF965 等)沒有自訂暫存器,於是解析失敗 → 靜默回 0.0。

這會產生最危險的假 PASS:訊息每 N 秒準時抵達、JSON 格式正確、topic 正確 —— 唯獨值是憑空捏造的 0.00。實測對照:

${rs485.5.電流}    → {"current":0.00}  {"current":0.00}  {"current":0.00}   ← 恆為 0
${rs485.1.current} → {"current":0.31}  {"current":0.30}  {"current":0.30}   ← 真實量測

判準:真實量測會微幅跳動,恆定的 0.00 幾乎必然是變數沒解出來。 看到整排 0.00 時,先確認欄位名與 Slave ID,不要當成「現場真的沒電流」。 | 🔁 變更時才發 | 不勾 |

變數名稱可按介面上的「📌 可用變數參考」展開查;填好後按「👁 預覽填充結果」, 看到真實數值才存 —— 預覽是空的就代表變數名錯了,存下去會發出空值。

通過:60 秒內約 12 筆,即使電流沒變也照發。 沒過:完全收不到 → 通道沒啟用,或主題字串大小寫不符。


B2 — 關掉重複資料(持續 × 不重複) | 出廠對應 CV.4

目的:業主抱怨資料量太大。值沒變就不要送,資料量從「每 5 秒一筆」降到「有變化才一筆」。

設定:同一通道 → 勾選「🔁 變更時才發」 → 儲存。

⚠️ 類比量測(電流、電壓、溫度)光勾這個不會變成 0 筆 —— 必須同時設死區。

這類訊號永遠在微幅跳動,對韌體來說「值一直在變」,所以每次檢查都算變更、照發不誤。 死區的作法就是在模板變數後加小數位修飾詞:${rs485.1.current:1}。

實測(Finder 6M 電流環,實際電流約 0.3A,間隔 5 秒):

設定 65~70 秒內筆數
不勾「變更時才發」 ~13 筆(每 5 秒必發)
勾了,模板 ${rs485.1.current}(2 位小數) 8 筆 — 值在 0.30/0.31/0.32 之間跳,擋不掉
勾了 + ${rs485.1.current:1}(1 位小數) 0 筆 ✅

所以開關型的數位訊號(計數、狀態)勾了就夠;類比量測一定要配死區。 先勾了發現還在發,不是壞掉,是還沒設死區。

通過:設好死區後,值不變的 60 秒內 0 筆;讓負載變動 → 立刻 1 筆。 沒過:仍持續收到相同值 → 先確認①勾的是這一個通道(這是逐通道設定)、②模板有沒有加小數位修飾詞。


B3 — ★ 證明「重複資料能再回來」(可逆性驗證)

目的:業主真正擔心的不是「現在會不會重複」,而是**「這件事到底歸不歸我控制」。 只證明「勾了就不重複」不夠 —— 那可能只是剛好值沒變。必須證明取消勾選後重複會確實回來**,才能說明這個開關真的有效、而且是唯一的控制點。

設定:取消勾選 → 觀察 60 秒 → 再勾回來 → 再觀察 60 秒。

通過:取消 → 重複資料立刻回來(約 12 筆/分);勾回 → 再度歸零。來回兩次都符合。 沒過:取消勾選後仍然沒有資料 → 那 B2 的「0 筆」可能根本不是勾選造成的,而是通道停掉了。這一步就是為了排除這種假通過。


B3b — ★★ 最強做法:兩個通道並排跑,同一時間對照

為什麼還要這一項: B2 與 B3 都是先後比較 —— 取消勾選觀察一分鐘、勾回去再觀察一分鐘。 中間隔了時間,「這一分鐘值有沒有變」不受你控制。業主如果問 「會不會只是後面那一分鐘剛好沒變?」,B2/B3 答不出來。

並排跑就沒有這個問題:同一台、同一個時間、同一個資料來源、同一份模板, 唯一差別是那個勾勾。

做法:建兩個 Converter 通道,除了「變更時才發」以外每一個欄位都一樣, 發到兩個不同的 topic。來源選一個數位訊號(不會像類比量測那樣一直微跳, 不必處理死區)—— 用一路沒接東西的 DO 最乾淨。

bash
# ① 建兩個對照通道(UID 用你的 $UID_DEV)
mk(){ # mk <名稱> <topic尾巴> <true|false>
  printf '  %-18s → ' "$1"
  curl -sS -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
    -d "{\"name\":\"$1\",\"protocol\":0,\"direction\":1,\"mode\":2,\"mqttTopic\":\"$PREFIX/actest/$2\",\"mqttQos\":0,\"converterIntervalSec\":5,\"converterOnChange\":$3,\"converterGateSignalId\":0,\"enabled\":true}" \
    $DUT/api/tcpio
  echo; sleep 2
}
mk AC-onChange-OFF off false
mk AC-onChange-ON  on  true

# ② 兩個通道套【一模一樣】的模板(idx 用上一步回傳的值)
#    ${io.DO2} = 主機 DO3(0 起算)。平常不變,我們等一下手動讓它變。
for i in <OFF的idx> <ON的idx>; do
  curl -sS -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
    -d "{\"idx\":$i,\"template\":\"{\\\"do2\\\":\${io.DO2},\\\"v\\\":\\\"fixed\\\"}\"}" \
    $DUT/api/converter/template
  echo; sleep 2
done

# ③ 讀回確認兩份模板真的一樣 —— 不一樣的話整個對照就不成立
for i in <OFF的idx> <ON的idx>; do
  curl -s -H "Authorization: Bearer $TOK" "$DUT/api/converter/template?idx=$i"; echo
done
bash
# ④ 訂閱 90 秒,中途讓值變兩次
timeout 92 mq_sub -t "$PREFIX/actest/#" -v -F '%I|%t|%p' > ab.log &
sleep 45
curl -sS -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"index":2,"value":true}'  $DUT/api/io/do    # 值 0 → 1
sleep 22
curl -sS -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
  -d '{"index":2,"value":false}' $DUT/api/io/do    # 值 1 → 0
wait
awk -F'|' '{print $2}' ab.log | sort | uniq -c

通過(2026-09-08 於 .77 實測,韌體 v6.0.91):

通道 90 秒內 說明
沒勾 actest/off 18 則 每 5 秒一則,值一樣照發
勾了 actest/on 2 則 只有 0→1 與 1→0 那兩個瞬間

前 45 秒值完全沒動:沒勾的發了 8 則重複訊息,勾了的一則都沒有。 訊息量差 89%。

同一輪還證明了「變了就會發」 —— 12:50:34 下指令翻轉輸出, 12:50:37 就送出來,3 秒(落在 5 秒檢查週期內)。 這一半很重要:光證明「不重複」不夠,還要證明它不是啞掉了。

沒過:

  • 兩邊都 18 則 → 勾勾沒存進去(逐通道設定,確認勾的是 ON 那一個)
  • 兩邊都 0 則 → 先看 converterTemplateLen,空模板會靜默不發(WI-230)
  • 勾了的那個連值變化時也沒發 → 那才是真的壞了

🛑 驗完一定要把兩個測試通道刪掉,並推一次雲端 —— 否則交機的設備上會留著 AC-onChange-OFF / AC-onChange-ON, 而且雲端備份裡也會有(同 §4.3 的教訓:清理範圍要涵蓋所有被污染的地方):

bash
for id in <OFF的channelId> <ON的channelId>; do
  curl -sS -X DELETE -H "Authorization: Bearer $TOK" "$DUT/api/tcpio?id=$id"; echo; sleep 2
done
curl -sS -X POST -H "Authorization: Bearer $TOK" $DUT/api/io/do \
  -H "Content-Type: application/json" -d '{"index":2,"value":false}'   # 輸出放掉
curl -sS -X POST -H "Authorization: Bearer $TOK" $DUT/api/configserver/push

這個開關在韌體裡怎麼運作的(業主問起時的答案):

每個週期照樣把整份模板算出來,對整串 payload 取雜湊(FNV-1a), 跟上次一樣就不發。所以:

  • 比對的是整包,不是某一個欄位 —— 模板裡任何一個值變了都會送,不會漏
  • 不發也會更新「上次檢查時間」,所以節流週期不會亂掉
  • 閘門「關 → 開」的上升沿會重置雜湊,因此重新觸發一定先送一次當下值 (v5.9.266 修的 —— 否則閘關掉再開、值沒變就靜默,現場會以為「送了命令沒回應」。見 B6)

原始碼:src/ConverterEngine.h 的 evaluate()。


B4 前置 — 先建立閘門,否則送訊號不會有任何反應

發到 cmd/signal/1 只是把數值寫進韌體的一個暫存位置。要讓它真的當閘門,必須先綁定:

  1. 配置頁 → 📡 輸入群組 → +新增 → 名稱「開工閘門」→ 新增來源 → 類別選 📨 MQTT 命令訊號 (cmd/signal) → 選 cmd/signal/1 → 比較條件 ≥ 0.5 → 儲存

⚠️ 比較條件一定要用 ≥ 0.5,不要用 = 1。 兩者在只送 0/1 時看起來一樣, 但 B5 會送 5、15 這種秒數 —— 用 = 1 的話那些值不滿足條件,閘門直接關閉, B5 會收到 0 筆而看起來像功能壞掉。走查時實際踩到過。

  1. 回到 配置頁 → TCP 通道 → 點開該 Converter 通道 → 🔒 門控信號 選「開工閘門」→ 儲存

沒做這一步的話:門控欄位維持「(無門控,持續推送)」, 你送 1 送 0 都不會有任何差別 —— 而且沒有任何錯誤訊息。 這是 B4/B5/B6 全部失效的最常見原因。

B4 — MQTT Trigger 啟動 Converter(啟動式 × 連續) | 出廠對應 CV.3

目的:離峰時段不佔頻寬、不產生無用資料。上位系統開工時送一個訊號才開始回報,收工再送一個就停。

bash
mq_pub -t "$PREFIX/cmd/signal/1" -m '0'   # 閘門關
# 觀察 30 秒 → 應為 0 筆

mq_pub -t "$PREFIX/cmd/signal/1" -m '1'   # 閘門開
# 觀察 60 秒 → 應照設定間隔持續收到
powershell
mq_pub -t "$PREFIX/cmd/signal/1" -m '0'   # 閘門關
# 觀察 30 秒 → 應為 0 筆

mq_pub -t "$PREFIX/cmd/signal/1" -m '1'   # 閘門開
# 觀察 60 秒 → 應照設定間隔持續收到

沒過:閘門關著仍在發 → 閘門來源綁錯。


B4b — 啟動式 × 不重複(兩個條件是「且」的關係) | 出廠對應 CV.4

目的:這是計數器最該用的組態。閘門開著時,每發生一次計數就送一筆; 停機(閘門關)或沒有新計數時,完全不送。資料量剛好等於「該回報的事件數」。

設定:在 B4 的閘門仍綁著的情況下,把該通道的 🔁 變更時才發 勾起來。

bash
mq_pub -t "$PREFIX/cmd/signal/1" -m '1'    # 閘門開著
# 不要觸發現場設備,觀察 60 秒

通過:閘門開著但值沒變 → 0 筆;現場觸發一次讓值改變 → 立刻 1 筆; 送 0 關閉閘門後,再怎麼觸發都 0 筆。 沒過:閘門關著卻仍在發 → 門控沒綁上(回 B4 前置)。 值沒變卻一直發 → 勾選沒生效,或模板沒限小數位(見情境 C)。


B5 — 用訊號內容直接調整回報頻率 | 出廠對應 WI-143

目的:不同工單需要不同取樣密度。上位系統送數字就能改頻率,不必派人進設定頁。

bash
mq_pub -t "$PREFIX/cmd/signal/1" -m '5'    # 每 5 秒一筆
mq_pub -t "$PREFIX/cmd/signal/1" -m '0'    # 停止
powershell
mq_pub -t "$PREFIX/cmd/signal/1" -m '5'    # 每 5 秒一筆
mq_pub -t "$PREFIX/cmd/signal/1" -m '0'    # 停止

沒過:此功能需閘門來源為 MQTT 型別。不使用則標「不適用」。


B6 — ★ 重新觸發要有反應(關再開要補發) | 出廠對應 CV.4

目的:操作員收工關掉、隔天開工再打開,若值剛好沒變就什麼都收不到 —— 他會以為系統壞了。所以重新打開時必須補一筆目前值。

bash
mq_pub -t "$PREFIX/cmd/signal/1" -m '0'   # 關
sleep 10
mq_pub -t "$PREFIX/cmd/signal/1" -m '1'   # 再開(期間不要動負載)
powershell
mq_pub -t "$PREFIX/cmd/signal/1" -m '0'   # 關
Start-Sleep 10
mq_pub -t "$PREFIX/cmd/signal/1" -m '1'   # 再開(期間不要動負載)

通過:重新打開後立刻補發一筆目前值,即使值完全沒變。 沒過:不補發 → 現場最常見的抱怨來源。


CT.1 — 計數器:一次計數 = 一筆訊息

目的:計數值只有事件發生時才變,勾「🔁 變更時才發」後資料量剛好等於事件量,對帳最直觀。不勾的話停機期間會把同一個數字重發上千遍。

通過:現場觸發一次 → 面板 +1 → 網頁 +1 → 訂閱端剛好 1 筆;停止觸發後 60 秒 → 0 筆。 沒過的分辨:面板有跳、網頁沒跳 → 型號模板或 Slave ID;網頁有跳、訂閱沒收到 → Converter 通道設定。


#情境 U:網頁設定流程 —— 驗收現場實際會走的那一條

這一段和前面所有段落的差別:前面用指令做設定,是為了把故障切乾淨 (壞在韌體還是壞在網頁,一眼看得出來)。但驗收現場業主看的是畫面 —— 他不會打 curl,他會點按鈕。所以這一段全部在網頁上做。

#🛑 這一段唯一的判準:畫面說成功,不算數

這份文件走查至今挖到的假 PASS,絕大多數都是這個形狀: 畫面回成功、HTTP 回 200、清單上東西也在 —— 唯獨值沒寫進去,或寫到別的地方。 所以本段每一步的做法都是同一套:

在網頁上做 → 立刻打 API 對照 → 兩者相符才算過。

不要「先全部設完再一起檢查」。錯了要當場知道是哪一步, 否則五個設定攪在一起,查不出是誰。

U.0 開工前:三個現場紀律

紀律 為什麼
只開一個分頁,只有一個人在操作 設備是單執行緒、只有 4 個 socket。兩個分頁同時自動刷新就會互相擠掉,你會看到隨機的存檔失敗
一次存一筆,等清單刷新出現才做下一筆 連續快速新增會讓前一筆被無聲覆蓋,而且兩次都回成功(WI-199)
每一步存完,重新整理頁面再看一次 「畫面上有」可能只是前端還留著你剛打的字。重新整理之後還在,才是真的存進去了

U.1 幫實體 I/O 通道命名

配置頁 → 實體 I/O → 直接改名稱欄位 → 儲存。至少把接了東西的那幾路命名完。

為什麼先做這個:後面 U.2 / U.3 的下拉選單列出來的就是這些名字。 不命名的話下拉裡是一排 I1…I8 / DO 1…DO 4, 你認不出哪一路接了什麼,選錯畫面也不會報錯。

立刻對照(§6.3 那條完整版可以列出「還沒命名的」):

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/config | python3 -c '
import sys,json
d = json.load(sys.stdin)
print("  輸入:", d.get("inputNames"))
print("  輸出:", d.get("doNames"))'

通過:API 印出來的名字和你在畫面上打的完全一樣(含中文)。 沒過:少了某一路 → 那一路的存檔掉了,重打一次。

U.2 建立輸入群組

配置頁 → 📡 輸入群組 → 右上 + → 名稱 → + 加設備 → 選來源 → 設條件 → 儲存。

⚠️ 「觸發類型」預設是 Always,而它決定後面規則會不會動。 一般的開關/按鈕要選 Change;脈衝式(觸控鈕)才選 Rising 並搭配輸出「切換」。 詳細對照見〈情境 A〉A.0-1 那張表 —— 選錯不會報錯,只會「規則不動」。

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/signals | python3 -c '
import sys,json
d = json.load(sys.stdin)
for g in (d.get("signals") or d.get("signalGroups") or []):
    gid, nm, en = g.get("signalId"), g.get("name"), g.get("enabled")
    n = g.get("sourceCount")
    TRIG = {0: "Always", 1: "Rising", 2: "Falling", 3: "Change"}
    tr = TRIG.get(g.get("trigger"), g.get("trigger"))
    warn = "  ⚠️ 沒有來源,永遠不會成立" if not n else ""
    print(f"  signalId={gid}  {nm}  enabled={en}  來源數={n}  觸發類型={tr}{warn}")'

通過:剛建的那一筆在清單裡,而且 來源數 不是 0。 沒過:來源數 = 0 → 你按了「+ 加設備」但沒真的選到來源, 這種群組永遠不會成立,而畫面上看起來完全正常。

順便核對「觸發類型」 —— 指令會把韌體的數字翻成名稱 (0=Always / 1=Rising / 2=Falling / 3=Change,見 src/config.h:111)。 一般開關要 Change;脈衝式(觸控鈕)才是 Rising。 在這裡看比在畫面上看可靠 —— 畫面的下拉曾經出現過選項對錯韌體列舉的缺陷(WI-206)。

U.3 建立輸出群組

配置頁 → 輸出群組 → + → 名稱 → + 加設備 → 選 DO 通道與動作模式 → 儲存。

輸出模式:直接=跟隨、切換=每次觸發翻面、脈衝=定時、關閉=強制 OFF。

bash
curl -s -H "Authorization: Bearer $TOK" $DUT/api/actions | python3 -c '
import sys,json
d = json.load(sys.stdin)
for a in (d.get("actions") or []):
    aid, nm, en = a.get("actionId"), a.get("name"), a.get("enabled")
    n = a.get("outputCount")
    warn = "  ⚠️ 沒有輸出,觸發了也不會做事" if not n else ""
    print(f"  actionId={aid}  {nm}  enabled={en}  輸出數={n}{warn}")'

通過:在清單裡且 輸出數 不是 0。 ⚠️ enabled 是 False 的話這條動作不會執行 —— 用 API 建的時候不帶 enabled 會靜默停用,網頁建的通常是 True,但還是要看一眼。

U.4 建立規則(把兩者綁起來)

規則頁 → 新增 → 填名稱 → 觸發信號選 U.2 那個 → 觸發條件 → 執行動作選 U.3 那個 → 儲存。

bash
# 三個端點一起讀,才能交叉比對「規則綁的那個群組到底還在不在」
SIG=$(curl -s -H "Authorization: Bearer $TOK" $DUT/api/signals)
ACT=$(curl -s -H "Authorization: Bearer $TOK" $DUT/api/actions)
curl -s -H "Authorization: Bearer $TOK" $DUT/api/rules \
  | SIG="$SIG" ACT="$ACT" python3 -c '
import sys, json, os
rules = json.load(sys.stdin).get("rules") or []
sd = json.loads(os.environ["SIG"])
ad = json.loads(os.environ["ACT"])
sigs = {g.get("signalId"): g for g in (sd.get("signals") or sd.get("signalGroups") or [])}
acts = {a.get("actionId"): a for a in (ad.get("actions") or [])}
for r in rules:
    sid, aid = r.get("signalId"), r.get("actionId")
    nm, en = r.get("name"), r.get("enabled")
    bad = []
    if not sid or not aid: bad.append("沒綁到東西")
    if sid and sid not in sigs: bad.append(f"signalId={sid} 這個輸入群組不存在(被刪了?)")
    if aid and aid not in acts: bad.append(f"actionId={aid} 這個輸出群組不存在(被刪了?)")
    if sid in sigs and not sigs[sid].get("enabled"): bad.append("來源群組是停用的")
    if aid in acts and not acts[aid].get("enabled"): bad.append("動作群組是停用的")
    mark = "  ⚠️ " + " / ".join(bad) if bad else ""
    print(f"  {nm}  signalId={sid} actionId={aid} enabled={en}{mark}")'

通過:你剛建的那條規則後面沒有任何 ⚠️。

這條指令會抓四種「畫面上完全看不出來」的壞法:

警告 意思
沒綁到東西 兩個下拉有一個是空的 —— 規則永遠不會觸發
這個輸入/輸出群組不存在(被刪了?) 懸空參照 —— 群組被刪掉了,規則還指著它。韌體 fail-safe 當「關」,不會報錯
來源群組是停用的 群組本身被停用,規則跟著失效
動作群組是停用的 同上,在輸出端

⚠️ 欄位是 actionId(單數)。 規則只綁一個輸出群組,不是陣列。

存完重新整理頁面再看一次 —— 確認兩個下拉仍顯示正確的選項, 而不是又變回空白。

U.5 當場示範一次(這才是業主要看的)

撥開關 / 按按鈕 → 對應的輸出動作 → 解除 → 復歸。連續三次一致。

掛著監看比較好講解:

bash
python3 watch_device.py io

通過:三次都一致。 看不到 DI 翻轉 → 接線或供電,不是設定。 DI 有翻、輸出不動 → 回 U.2 檢查「觸發類型」,或 U.4 檢查兩個下拉有沒有真的綁到。

U.6 重新整理 + 重開機,再對一次

網頁上按重新整理 → 剛才建的東西全部還在。 然後 設定頁 → 重新啟動(或 POST $DUT/api/system/reboot),等 60 秒再看一次。

通過:數量與內容完全一致。這一步證明設定真的寫進 flash,不只是畫面上有。 沒過:重開後不見 → 立即停止驗收並回報,不可帶病上線。

bash
# 重開前後各存一份,直接比對
python3 watch_device.py snapshot before.json
# … 重開,等 60 秒 …
python3 watch_device.py snapshot after.json
python3 watch_device.py diff before.json after.json

U.7 推一次雲端備份收尾

bash
curl -sS -X POST -H "Authorization: Bearer $TOK" $DUT/api/configserver/push

然後照 §4.3 的硬判準去雲端確認新設定真的到了 —— 不要看設備自己回報的推送結果(失敗時它不會更新,讀到的是上一次成功的舊紀錄,WI-228)。


#情境 C:本站設備該用哪一種組態

上面四格不是選擇題,是依設備特性對號入座。

設備 建議組態 為什麼
計數器 B4b(有閘門)或 B2(無閘門)—— 兩者都務必勾「🔁 變更時才發」 計數值只有事件發生時才變。勾選後「一次計數 = 一筆訊息」,資料量等於事件量,對帳最直觀。不勾的話,停機期間會把同一個數字重發上千遍
電流環 B2 持續 × 不重複 + JSON 模板限小數位 電流是連續量,永遠有微小跳動。Converter 沒有「死區」欄位 —— 要壓掉雜訊是在 JSON 模板的變數後面限小數位:${rs485.1.current:1} 代表只取到小數一位,跳動小於 0.1A 就算沒變、不會發(:0 為整數)。欄位名同樣必須用英文 —— 見 B1 的警告
班別統計 B4 啟動式 × 連續 開工送閘門開、收工送閘門關,離峰不佔頻寬

編號對照:B4 是「閘門 × 不勾變更時才發」、B4b 是「閘門 × 勾選」。 B3 不是象限,它是可逆性驗證(見情境 B)。挑組態時看的是象限編號,別把 B3 當組態。

⚠️ 電流環最容易出錯的地方。 業主若說「勾了還是一直有資料」,十之八九不是勾選失效, 而是模板沒限小數位 —— 量測雜訊本身就一直讓字串改變,於是每次檢查都判定「值有變」。 Converter 判斷「有沒有變」是比對整包填好的 JSON 字串,不是數值容差,所以限小數位才是正解。 請當場觀察實際跳動幅度再決定取幾位。

#情境 D:設定值 —— 三關保證

設定「畫面上有」不等於「真的存進去了」。每一項設定都要過三關。

編號 關卡 通過 出廠對應
D1 存檔後立即讀回(不重新整理) 顯示值與輸入一致 ST.4
D2 重新整理頁面(F5) 值仍正確 —— 證明值來自設備,不是畫面殘留 2A–2F
D3 重開機後再看一次 值仍正確。過了這關才算真的存進去 ST.5 · ST.13
D4 逐項清點全部設定走完 D1–D3 網路/每台 RS485/量程/MQTT/模板內容/onChange/規則引用/虛擬通道 ID/計數器閾值與計件參數全數一致;另驗刪除前方通道後引用仍正確 —

D1 的重點:不要相信「儲存成功」的提示,要親眼看到值。 回報成功但值沒進去,是最難察覺的失敗形式。

三關的指令:

bash
# D1/D2 —— 存檔後讀回(D2 在瀏覽器按 F5 對照)
curl -s -H "Authorization: Bearer $TOK" $DUT/api/config | python3 -c "
import sys,json
d=json.load(sys.stdin)
print('RS485 設備:',[x['name'] for x in d.get('modbusDevices',[]) if x.get('name')])
"

# D3 —— 重開機後再跑一次上面那條,逐項比對
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/reboot
powershell
# D1/D2 —— 存檔後讀回(D2 在瀏覽器按 F5 對照)
curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/config | python -c "
import sys,json
d=json.load(sys.stdin)
print('RS485 設備:',[x['name'] for x in d.get('modbusDevices',[]) if x.get('name')])
"

# D3 —— 重開機後再跑一次上面那條,逐項比對
curl.exe -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/reboot

⚠️ 設備清單在 /api/config 的 modbusDevices,不是 /api/modbus-devices。 後者回的是匯流排狀態(mode/baudRate/…),沒有設備清單 —— 用錯會看到「0 台」而誤判設定不見了。


#自動化整批驗證

#🤖 先跑 acceptance_run.py —— 人工驗收之前的前置

bash
python3 scripts/acceptance_run.py --dut http://<設備IP> --token <admin token>
python3 scripts/acceptance_run.py --skip-reboot     # 不想讓它重開設備時

這支把本文件所有可自動化的測項串成一次不中斷的執行: 階段 0 / 2B.2 / 3 / 4 / 5 / 6、情境 M(M.1–M.4)、情境 B(B1–B6)、情境 D(D1–D3)、階段 8.1。 任何一項失敗就整體失敗,而且它不做任何修復 —— 工作是如實回報,不是把問題藏起來。

素材一律 AC- 前綴、測完自清,結束前會驗證設備回到起始狀態。約 13 分鐘。

為什麼要有它:2026-08-26 的走查是散在幾十次互動裡邊跑邊修完成的, 記錄裡還有「先失敗後修好」的項目 —— 那證明不了「這套流程能不中斷地走完」。 業主人工驗收之前,必須先有一次從頭到尾、單一韌體版本、沒有中途修東西的乾淨通過。

它不涵蓋(本質上需要人):0.1 供電接地目視、階段 R 歸零(破壞性)、 階段 1 空機燒拓荒包、情境 A.1 實體撥開關、5.4 現場觸發計數器、 階段 7 實體斷電、8.2 拍照、8.3 簽名。這些仍要照本文件人工執行。

情境逐項驗完後,跑一次自動化閘門收尾:

這份驗收單與出廠測試是對齊的。 每個情境的「出廠對應」欄位指向出廠測試規範 (內部文件 docs/testing/qa-factory-test.md)的測項編號(CV.* / EN.* / ST.*)—— 兩邊看的是同一組行為,不是兩套標準。

本走查(2026-08-25/26)挖到的缺陷,已固化成 npm run test:regression (RG.1–RG.7)。2026-08-27 起七項全數綠燈,已併進 test:gate:full。 對應的產品缺陷(WI-199/200/202/203/205/206)都已修進韌體 6.0.74。

RG.5 會重開設備,預設略過;要跑帶 RG_ALLOW_REBOOT=1。已於 2026-08-27 單獨驗過。

⚠️ 順帶提醒:當時實驗證明過,舊版閘門在缺陷存在時仍然全綠 (注入 WI-202 的壞值讓整份 TCP IO 設定在網頁上消失,factory-pages 依舊 6/6 通過)。 閘門全綠不等於沒問題 —— 這也是為什麼要有人照著這份單子實際走一遍。

bash
cd <專案>/web
EXPECT_FW=<版本> DUT_URL=$DUT ADMIN_TOKEN=$TOK npm run test:gate
powershell
cd <專案>\web
EXPECT_FW=<版本> DUT_URL=$DUT ADMIN_TOKEN=$TOK npm run test:gate

35 項,約 2 分鐘。全數通過才算通過。

⚠️ 在客戶現場跑之前先讀這段:

  • ST.14 預設期待「Finder slave 1 + 1 個擴充模組」(那是我們實驗室的組態)。 本站硬體不同的話這項一定會紅,那不是設備的問題。 請改帶本站的實際組態:EXPECT_RS485='<slaveId>:<型號>:<名稱>' EXPECT_EXPANSIONS=<數量>, 或先跳過 ST.14 再單獨人工確認。
  • 它不是嚴格唯讀 —— 過程中會寫入 deviceName 再還原。 正常跑完不留痕跡,但中途 Ctrl-C 中斷會在設備上留下測試值, 中斷後請回頭確認設備名稱是否正確。

#階段 7:斷電復原 —— 業主最該在意的一段

現場一定會停電。這一段驗的是「電回來以後,它會不會自己回到工作狀態」,不需要任何人到場。

斷電前先確認所有供電來源;同時接 USB 與獨立 DC 時,必須兩路都切斷並確認設備熄燈。只拔 USB 或只關 DC 不算實體斷電。7.1 保持網路設備供電;7.4 才將設備與網路設備共同斷電。

步驟 動作 通過 沒過
7.1 直接切掉電源(不要用軟體重開),30 秒後復電 自行開機並回到正常心跳 無法自行開機 → 停止驗收並回報
7.2 逐項確認:網路、RS485 清單、規則、MQTT 上行 全部與斷電前一致,MQTT 自行恢復,無需人工介入 任一項不見 → 不可交付。代表設定沒真正落盤,現場停電一次就要重設一次
7.3 確認「🔁 變更時才發」勾選仍在 逐通道展開確認,狀態不變 勾選跑掉 = 重複資料會再度出現。這一項要單獨檢查,從資料面要等好幾分鐘才看得出來
7.4 設備和網路設備一起斷電(見下方,這一項最接近現場真實情況) 電回來之後,不需要任何人到場,設備自己回到網路上、網頁打得開 設備有電、燈在閃,但網路上找不到它、網頁打不開 → 不可交付(WI-236)

7.2 不要用眼睛比對四十個項目 —— 用快照 diff

斷電前跑一次、復電後再跑一次,兩份輸出必須完全相同:

bash
# 斷電前
python3 watch_device.py snapshot before.json

# …切掉電源,30 秒後復電,等設備開機完成…

# 復電後
python3 watch_device.py snapshot after.json
python3 watch_device.py diff before.json after.json
powershell
# 斷電前
python watch_device.py snapshot before.json

# …切掉電源,30 秒後復電,等設備開機完成…

# 復電後
python watch_device.py snapshot after.json
python watch_device.py diff before.json after.json

一致會直接告訴你,不一致會把差異印出來:

✅ 兩份快照完全一致

快照會剔除會隨時間變動的欄位(currentValue、uptimeSec、bootCount 等), 否則每次比對都「不一致」,這個檢查就沒用了。實測連跑兩次完全相同。

diff 是這支工具內建的,不依賴系統的 diff 指令 —— Windows 上沒有那個指令。

#7.4 設備和網路設備一起斷電 —— 最接近現場的一段,而且以前從來沒測過

為什麼要單獨測這個

7.1 只切掉設備自己的電源,那時網路是好的 —— 設備一開機就找得到網路。 但現場停電是整廠一起停,網路分享器、交換器也會關。

而設備開機比分享器快很多:

大約要多久才能用
這台設備 20 幾秒
一般的網路分享器 60~120 秒

所以設備開機的時候,網路根本還沒好。這一段就是要驗「網路晚一點才好,設備會不會自己接上」。

這件事在 2026-09-09 之前是壞的(WI-236)。 實際發生過: 一台設備就這樣離線了 18.4 小時而沒有人發現 —— 因為它有電、燈在閃,雲端後台也顯示它「上線中」, 只有真的要打開它的網頁時才會發現進不去。 已於韌體 6.0.94 修正,這一項就是用來守住它不再回來。

怎麼做(要動手,不是打指令)

  1. 把設備和網路分享器插在同一個延長線上(現場本來就常常是這樣)
  2. 記下設備現在的 IP,等一下要用
  3. 把延長線的電源整個關掉,等 10 秒
  4. 打開電源
  5. 不要碰任何東西,等 5 分鐘

5 分鐘之後檢查這三件事

bash
# ① 網頁打得開嗎(把 <設備IP> 換成你記下來的那個)
curl -s -o /dev/null -w "網頁 HTTP %{http_code}\n" http://<設備IP>/

# ② 設備自己認為有沒有網路
curl -s -H "Authorization: Bearer $TOK" $DUT/api/system | python3 -c '
import sys, json
n = (json.load(sys.stdin).get("network") or {})
ip = n.get("ip")
print("  設備自己回報的 IP:", ip)
print("  ", "✅ 正常" if ip and ip != "0.0.0.0" else "❌ 設備以為自己沒有網路(WI-236)")'

# ③ 這段期間它重開了幾次
curl -s -H "Authorization: Bearer $TOK" $DUT/api/diag | python3 -c '
import sys, json
d = json.load(sys.stdin)
print("  開機次數:", d.get("bootCount"), " 上次重開原因:", d.get("resetReason"), "/", d.get("bootDetail"))'

通過:三項都正常 —— 網頁 HTTP 200、設備回報的 IP 不是 0.0.0.0、 重開次數只比斷電前多 1~2 次(多 1 次是正常的:網路晚一點好, 設備會做一次「回復重開」)。

沒過的兩種樣子 —— 都是不可交付:

你看到的 意思
網頁完全打不開,而且 ping 也不通 設備沒拿到 IP,也沒有自己再試
雲端後台顯示它上線中,但網頁打不開 最陰險的一種(WI-236):底層網路好了,設備自己卻不知道,所以網頁伺服器沒開。從遠端看一切正常

⚠️ 第二種一定要特別留意 —— 它不會出現在任何監控指標上。 唯一的檢查方式就是真的去打開那個網頁。

只有網路孔的機型(8310)要特別測

8310 沒有 Wi-Fi,網路線是它唯一的路。8320 就算有線慢了,還有 Wi-Fi 可以頂著, 所以同樣的問題在 8320 上不容易看出來。

這正是它長期沒被發現的原因:辦公室測的都是 8320,而 8320 有 Wi-Fi 當備援。 現場只要有 8310,這一項就一定要做。

7.1 的重開原因判讀(斷電前另開一個終端機掛著):

bash
python3 watch_device.py diag
powershell
python watch_device.py diag
動作 bootDetail 應該是
拔網路線 net-failover
插回網路線 net-recovery
拔電源 空,resetReason = UNEXPECTED

拔電源後 resetReason=UNEXPECTED、bootDetail 空是正確的 —— 硬體斷電不會留下重開原因的字條,這正好和軟體重開的 SOFTWARE/api-reboot 分得出來。

「MQTT 自行恢復」不能用「送一個命令看它動不動」草率驗過。 走查時實際踩到: 送 cmd/signal/1 = 1 想看燈亮 —— 但燈送之前就已經是亮的(WI-161 會在重開後 還原閘門狀態,規則重新觸發),送完還是亮,這個測試什麼都沒證明。

正確做法是製造狀態轉換,而且雙向:

bash
mq_pub -t "$PREFIX/cmd/signal/1" -m '0'   # 觀察輸出應【關閉】
mq_pub -t "$PREFIX/cmd/signal/1" -m '1'   # 觀察輸出應【開啟】

出向則要看到新鮮訊息:訂閱 $PREFIX/#,連線瞬間湧入的那批是 broker 的 retained 舊訊息,不算數;要等到幾秒後才抵達的那一筆(例如 cmd/_ping)才證明 設備現在真的在發。走查實測:連線當下 6 筆 retained,16 秒後才收到一筆現發的 ping。


#新增驗收:多段磅秤、量測時間與復電(6.0.122)

從網頁設定開始,請先走畫面設定 A~E。 以下是結果檢查表,不是畫面操作說明。6.0.122 目前仍需工程工具安裝多段校正,不能把 CLI/模擬通過記成「全程網頁設定通過」。

自動檢查跑完後,還要做下面這些項目。 每一項都記錄結果、日期、操作人員、設備型號與版本,並附上截圖或檔案位置。Build ID 是這份韌體的識別碼,可從診斷資料讀到。用電腦模擬、真的與設備通訊、真的放砝碼,請分開記錄。出廠文件用同樣的編號,方便逐項對照。

操作詳細指令見 多段校正 與 原始量測時間。故障注入只在隔離測試設備進行;現場產線不得直接執行會新增/移除規則的測試腳本。

ID 測項 操作 通過條件
SC.1 先確認設備與測試範圍 記下設備版本與識別碼、磅秤通訊位址、小數位,以及這次用哪些砝碼、允許差多少;先備份完整設定。確認 cmd/signal/2~6 無其他流程占用、舊兩點精靈未共存。 版本與本輪候選相符、命令不衝突;現場誤差由業主先填,未填不得簽精度。
SC.2 先標零,再把按鈕放開 放空籃、reset,再標零(signal/3 送 1 後 0)。標零未成功前嘗試寫入。 未標零不得寫段或亮 ACK;成功後可寫入。每次送 1 後,都要再送 0,就像按鈕按下後要放開。
SC.3 3/4/5 段與收尾 依多段指南,分別做 3、4、5 段:段號 signal/4、重量 signal/5、write signal/6 送 1 後 0;最後再送下一段號、重量 0、write。 每段參數與順序正確,零重量收尾只在全部必要寫入成功後完成;記錄實際命令及對應回覆,不只看燈。
SC.4 重校與漏收尾 測試治具跑 3→5→3;另故意省略收尾,再補上。 不能把缺收尾當成完成;補上後使用新段數,不誤用先前多餘段。真秤以讀回/已知砝碼驗證;模擬結果另標。
SC.5 失敗不誤報完成 由工程人員在測試設備故意製造沒有回覆、回覆不完整或內容錯誤;包含校正和收尾。錯誤類型須涵蓋 CRC、通訊位址、功能、暫存器與數值不符。 這一步失敗後,不再繼續寫入、提交或亮完成燈,也不在這次執行中自動重送;別條獨立規則仍可執行。要留下「命令有收到」與「寫入失敗」兩種紀錄。
SC.6 復電後先清除校正狀態 先把命令都送回 0,再斷電復電。重新開機不一定會清掉「可寫入」狀態,須先做 reset(清除校正狀態),確認未標零時不能寫入,再重新標零。 reset 後寫入被阻擋且無假 ACK;重新標零後才進校正。命令為 1 時斷電可能重播,不可當成安全續作功能。
SC.7 真的放砝碼確認,最後整理設定 依業主允收誤差,量空籃、每個校正點及點間重量,卸載/重放確認;保留核准的現場設定,移除測試夾具並做最終備份。 重量、單位、小數與重複量測符合預定允差,記錄砝碼與實測值。缺秤標「待現場」,不得用 Modbus echo 或邏輯 PASS 取代。
TS.1 新設定有時間,舊設定補上時間 用網頁建立新的磅秤/電表流程;舊流程先預覽會改哪些欄位,再備份、套用,最後再查一次。 新增 timestamp 且保留舊欄位/精度;第二次零變更,自訂或衝突範本不被默默改寫。
TS.2 時間要跟著那一次量測 成功量測後保存完整 payload、來源暫存器與接收 UTC;確認同一數值來源的 timestamp。 為 ISO 8601 UTC 字串(末尾 Z),磅秤是 weight 讀取時間、電表是 Finder 批次時間;不是網頁 render 或 broker 接收時間。
TS.3 設備還不知道時間時怎麼辦 請工程人員用時鐘測試設備,分別測「尚未知道時間」「已取得時間」及「時間被往前/往後調整」。 未知為 JSON null,不是空字串或假日期;新樣本用新的校時,已存樣本時間不被重寫。只在電腦模擬的結果要寫「電腦模擬」,不能寫成已在實機做過。
TS.4 延遲接收/補送 先等 MQTT 伺服器確認訂閱成功(SUBACK)。用能保留訂閱的接收端、QoS1 收到一筆後,暫停量測來源並中斷接收端,稍後重新連線收回排隊訊息。 稍後收到的數值與 timestamp 必須跟原來一樣,接收時間則比較晚。這只驗伺服器排隊後補送,不表示 OPTA 斷電後仍保存每筆待送資料。
TS.5 讀失敗與恢復 注入不完整/錯誤讀回,暫停輪詢後恢復;分別記錄快取與來源 online 狀態。 失敗不產生新值/新樣本時間;若來源被判離線,依既有策略停止發布;恢復成功讀取後才更新時間。
TS.6 只因時間變動,不應多發一筆 數值固定而 timestamp 持續前進,觀察 onChange;再改值。檢查空白模板與超出 511 bytes 輸出。 只有時間變化不額外發布,值變才發;空白模板不能啟用,過長輸出不送截斷 JSON。
TS.7 確認業主存對時間,並清掉測試資料 接收端以 sample timestamp 記量測,receiveTime 另存;null 標未知。清掉測試訂閱/通道並逐欄比對設定與 bootCount。 不把延遲資料記到接收當下;接收端未提供則標待整合,不能宣稱通過。Gateway 與接收端結果分欄,測試夾具無殘留。
NR.1 路由器延後啟動 記錄完整設定與 bootCount,切斷 DUT 全部電源及測試路由器,確認熄燈並記錄時刻;復電後勿在 router 就緒時人工再重開。 Web 可開、IP 正確、真 MQTT 新收發、量測及雲端恢復;保存起停時刻、介面及至少 180 秒觀察。未量恢復耗時就填未量測,不能推算。

#現場逐步命令(macOS/Linux 與 Windows PowerShell)

以下每個程式區塊是一個動作。 選同一作業系統欄逐步執行;成功並核對預期結果後才做下一步。macOS 使用 zsh/bash+python3;Windows 使用 PowerShell 5.1 或 7+python+curl.exe(不要用 PowerShell 的 curl 別名)。兩邊均須先依本文環境準備安裝 Mosquitto CLI。Windows 兩版本共用以下語法;尚未在 Windows 主機實跑者不得宣稱已做 Windows 實機驗收。

本節接續本文既有環境設定:DUT 是含 http:// 的設備 URL,TOK 是管理 token,PREFIX 是從設備 UID 組成的 topic。Windows 對應 $env:DUT、$env:TOK、$env:PREFIX。若前面未完成這些設定,先回〈開始前:環境準備〉;不可照抄另一台 UID。憑證只留於當次終端機,不寫進驗收報告。

#下載多段校正工具

macOS/Linux:

bash
curl --fail --show-error -o scale_multical_build.py https://opta.smms.com.tw/docs/assets/scale_multical_build.py

Windows PowerShell 5.1/7:

powershell
curl.exe --fail --show-error -o scale_multical_build.py https://opta.smms.com.tw/docs/assets/scale_multical_build.py

應該看到/要核對:下載成功,檔案不是 HTML 錯誤頁。

不符合時:若下載失敗或出現 HTML 錯誤頁,先確認電腦可以開啟文件中心,再重新下載;不要執行錯誤頁。

#下載時間範本遷移工具

macOS/Linux:

bash
curl --fail --show-error -o migrate_measurement_timestamps.py https://opta.smms.com.tw/docs/assets/migrate_measurement_timestamps.py

Windows PowerShell 5.1/7:

powershell
curl.exe --fail --show-error -o migrate_measurement_timestamps.py https://opta.smms.com.tw/docs/assets/migrate_measurement_timestamps.py

應該看到/要核對:下載成功,與校正工具放在目前工作目錄。

不符合時:若下載失敗或出現 HTML 錯誤頁,先確認電腦可以開啟文件中心,再重新下載;不要執行錯誤頁。

#驗證校正工具可執行

macOS/Linux:

bash
python3 scale_multical_build.py plan

Windows PowerShell 5.1/7:

powershell
python scale_multical_build.py plan

應該看到/要核對:顯示計畫,未連設備或更動設定。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#驗證遷移工具可執行

macOS/Linux:

bash
python3 migrate_measurement_timestamps.py --help

Windows PowerShell 5.1/7:

powershell
python migrate_measurement_timestamps.py --help

應該看到/要核對:顯示 --gw、--apply 與 --out 說明。

不符合時:若工具報錯或列出非預期變更,先不要繼續套用。保存結果檔與私有備份,請工程人員核對。

#保存斷電前診斷

macOS/Linux:

bash
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/diag" -o before-diag.json

Windows PowerShell 5.1/7:

powershell
curl.exe --fail --show-error -H "Authorization: Bearer $env:TOK" "$env:DUT/api/diag" -o before-diag.json

應該看到/要核對:用文字編輯器開啟 before-diag.json,找到 fwVersion(版本)、currentBuildId(韌體識別碼)與 bootCount(啟動次數),抄進本輪紀錄。

不符合時:若檔案沒產生、回覆不是完整 JSON 或設備無法連線,先確認 IP 與登入資料。保留錯誤,不要拿舊檔案當這次結果。

#保存完整設定

macOS/Linux:

bash
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/config/backup" -o before-config.json

Windows PowerShell 5.1/7:

powershell
curl.exe --fail --show-error -H "Authorization: Bearer $env:TOK" "$env:DUT/api/config/backup" -o before-config.json

應該看到/要核對:JSON 含完整設定/範本;此檔可能含憑證,留本機私有資料夾,不上傳公開報告。

不符合時:若檔案沒產生、回覆不是完整 JSON 或設備無法連線,先確認 IP 與登入資料。保留錯誤,不要拿舊檔案當這次結果。

#指定工具使用的管理 token

macOS/Linux:

bash
export OPTA_TOKEN="$TOK"

Windows PowerShell 5.1/7:

powershell
$env:OPTA_TOKEN = $env:TOK

應該看到/要核對:沿用已驗證的管理 token。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#輸入純 IP(不含 http://)

macOS/Linux:

bash
read -r GW_IP

Windows PowerShell 5.1/7:

powershell
$env:GW_IP = Read-Host "設備純 IP(不含 http://)"

應該看到/要核對:macOS 輸入設備 IP 後 Enter;例如本台 192.168.72.46,勿把例值當固定目標。

不符合時:若值不在上面允許的範圍,重新輸入正確值後才做下一步,不要送出超界數字。

#固定本輪 DO 配置(先核准再套用)

以下先選無外部燈模式 -1,避免碰未核准的 DO。若工站已接好指定燈,將兩邊的 -1 各改成已核准的 0~3 DO index;整輪 apply/reset 必須使用相同變數。無燈模式以命令與 Modbus 回覆/日誌驗收,不宣稱實體燈已測。

macOS/Linux,設定 ready DO:

bash
READY_DO=-1

Windows PowerShell,設定 ready DO:

powershell
$READY_DO = -1

macOS/Linux,設定 ACK DO:

bash
ACK_DO=-1

Windows PowerShell,設定 ACK DO:

powershell
$ACK_DO = -1

#安裝校正流程(SC.1)

macOS/Linux:

bash
python3 scale_multical_build.py apply --gw "$GW_IP" --ready-do "$READY_DO" --ack-do "$ACK_DO"

Windows PowerShell 5.1/7:

powershell
python scale_multical_build.py apply --gw "$env:GW_IP" --ready-do $READY_DO --ack-do $ACK_DO

應該看到/要核對:先在網頁配置正確 TDA08B Slave ID 與型號暫存器。工具若回報衝突/容量不足就停止,不刪現有流程。本輪 DO 選項沿用上一步核准的變數,後續 reset 不可省略。

不符合時:若出現衝突、找不到磅秤或容量不足,停止安裝,把錯誤訊息交給工程人員;不要刪除現有流程來硬裝。

#清除校正閘門(SC.2/SC.6)

macOS/Linux:

bash
python3 scale_multical_build.py reset --gw "$GW_IP" --ready-do "$READY_DO" --ack-do "$ACK_DO"

Windows PowerShell 5.1/7:

powershell
python scale_multical_build.py reset --gw "$env:GW_IP" --ready-do $READY_DO --ack-do $ACK_DO

應該看到/要核對:「可寫入」狀態已清除;有啟用校正燈才核對燈已熄滅。每次復電後先做這一步,不能只靠重開來清除狀態。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#MQTT 連線參數

使用設備的正式 broker、port、username/password;不得填其他現場帳號。以下以官方 TLS 8883 為例,非 TLS 測試 broker 需依部署規格調整。每一行輸入一個值。

#設定 broker hostname

macOS/Linux:

bash
export MQ_HOST=mosquitto.smms.com.tw

Windows PowerShell 5.1/7:

powershell
$env:MQ_HOST = "mosquitto.smms.com.tw"

應該看到/要核對:只設定變數,尚未送控制命令。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#設定 MQTT username

macOS/Linux:

bash
read -r MQ_USER

Windows PowerShell 5.1/7:

powershell
$env:MQ_USER = Read-Host "MQTT username"

應該看到/要核對:只設定變數,尚未送控制命令。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#設定 MQTT password

macOS/Linux:

bash
read -r -s MQ_PASS

Windows PowerShell 5.1/7:

powershell
$env:MQ_PASS = [System.Net.NetworkCredential]::new("", (Read-Host "MQTT password" -AsSecureString)).Password

應該看到/要核對:輸入現場有效值後 Enter,密碼輸入不回顯。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#設定發布函式

macOS/Linux:

bash
pub_signal() { mosquitto_pub -h "$MQ_HOST" -p 8883 --insecure -u "$MQ_USER" -P "$MQ_PASS" -t "$PREFIX/cmd/signal/$1" -m "$2" -q 1; }

Windows PowerShell 5.1/7:

powershell
function Send-Signal { param([int]$Index, [string]$Value) mosquitto_pub -h $env:MQ_HOST -p 8883 --insecure -u $env:MQ_USER -P $env:MQ_PASS -t "$env:PREFIX/cmd/signal/$Index" -m $Value -q 1 }

應該看到/要核對:函式只定義命令,不發布。--insecure 沿用目前設備 TLS 部署;若現場配置 CA,改用核准 CA 選項。publish 回傳成功僅代表 broker 收到,仍需核對設備動作。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#標零(SC.2)

手動動作:放空籃,等待面板穩定。以下兩個命令分開執行,第一個送出後確認設備收到,再放開;任何命令報錯就停止查明。

#送出標零

macOS/Linux:

bash
pub_signal 3 1

Windows PowerShell 5.1/7:

powershell
Send-Signal 3 1

應該看到/要核對:成功回覆後可寫入燈亮;無實秤回覆時不得亮成功燈。

不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。

#放開標零命令

macOS/Linux:

bash
pub_signal 3 0

Windows PowerShell 5.1/7:

powershell
Send-Signal 3 0

應該看到/要核對:命令回 0,避免保留在 1。

不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。

#每一段都依序執行(SC.3)

手動動作:放已知砝碼、等面板穩定。3 段做 1~3;4 段做 1~4;5 段做 1~5。重量是去除小數點的面板數值(例如兩位小數的 3 kg 填 300);不得直接套例值當業主砝碼。每換一個砝碼都重新執行下面六個動作。

#輸入本次段號

macOS/Linux:

bash
read -r SEGMENT

Windows PowerShell 5.1/7:

powershell
$SEGMENT = Read-Host "段號"

應該看到/要核對:一般段填 1~本輪段數,本工具上限 50;只有收尾填最後段號+1。

不符合時:若值不在上面允許的範圍,重新輸入正確值後才做下一步,不要送出超界數字。

#送出段號

macOS/Linux:

bash
pub_signal 4 "$SEGMENT"

Windows PowerShell 5.1/7:

powershell
Send-Signal 4 $SEGMENT

應該看到/要核對:設備段號等於本次段號。

不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。

#輸入本次重量

macOS/Linux:

bash
read -r WEIGHT_RAW

Windows PowerShell 5.1/7:

powershell
$WEIGHT_RAW = Read-Host "重量整數(去小數點)"

應該看到/要核對:一般段只允許整數 1~65535,且與面板小數位一致;禁止小數、負數與超界,0 僅限明確收尾。

不符合時:若值不在上面允許的範圍,重新輸入正確值後才做下一步,不要送出超界數字。

#送出重量

macOS/Linux:

bash
pub_signal 5 "$WEIGHT_RAW"

Windows PowerShell 5.1/7:

powershell
Send-Signal 5 $WEIGHT_RAW

應該看到/要核對:設備重量參數與砝碼換算相符。

不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。

#寫入此段

macOS/Linux:

bash
pub_signal 6 1

Windows PowerShell 5.1/7:

powershell
Send-Signal 6 1

應該看到/要核對:只在所有寫入回覆有效後出現 ACK;不把燈號當精度證明。

不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。

#放開寫入命令

macOS/Linux:

bash
pub_signal 6 0

Windows PowerShell 5.1/7:

powershell
Send-Signal 6 0

應該看到/要核對:write 回 0,再換下一個砝碼。

不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。

#零重量收尾(SC.3/SC.4)

段號填「最後一段+1」(3 段填 4、4 段填 5、5 段填 6)。依上方「輸入段號→送出段號」兩步送出,再逐步做:

#送出收尾重量零

macOS/Linux:

bash
pub_signal 5 0

Windows PowerShell 5.1/7:

powershell
Send-Signal 5 0

應該看到/要核對:收尾重量為 0。

不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。

#寫入收尾

macOS/Linux:

bash
pub_signal 6 1

Windows PowerShell 5.1/7:

powershell
Send-Signal 6 1

應該看到/要核對:收尾有有效 Modbus 回覆後才確認成功。

不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。

#放開收尾命令

macOS/Linux:

bash
pub_signal 6 0

Windows PowerShell 5.1/7:

powershell
Send-Signal 6 0

應該看到/要核對:write 回 0。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#讀回校正流程狀態

macOS/Linux:

bash
python3 scale_multical_build.py show --gw "$GW_IP"

Windows PowerShell 5.1/7:

powershell
python scale_multical_build.py show --gw "$env:GW_IP"

應該看到/要核對:流程設定完整;此工具不取代實秤量測。接著用核准砝碼量測各點與點間重量,記錄 SC.7 允差。

不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。

#故意失敗與斷電後恢復(SC.4~6/NR.1)

以下前兩項由工程人員在專用測試設備做,不在生產中的秤上試錯。

  1. 重新校正測試(SC.4):照前面的命令,依序做 3 段、5 段、再回 3 段。每一輪都記錄收尾;另安排一輪故意不收尾,記為「未完成」,補收尾後再查是否仍用到多餘舊段。
  2. 沒有回覆的測試(SC.5):工程人員讓測試秤暫時不回覆,再送一次寫入命令的 1 與 0。確認命令有到設備、日誌有寫入失敗,而且沒有假成功。其他錯誤回覆種類由工程測試另外留紀錄,不能因為正常操作成功就全部打勾。
  3. 恢復通訊:先解除測試設備的阻斷,再 reset、重新標零。
  4. 斷電前準備(SC.6/NR.1):確認命令都已送回 0,保存設定與啟動次數。不可把「命令按住時斷電」當成取消或安全續作的方法。
  5. 關電:切斷這台設備所有供電(USB 與 DC),以及本次測試的路由器。確認燈全滅,記下時間。
  6. 復電:恢復供電,讓設備先起、路由器較晚起。路由器好了以後,不要再人工重開設備。
  7. 確認恢復:執行下面的讀取命令與新訊息檢查。至少觀察 180 秒,保存實際時間;没量到的時間填「未量測」。
  8. 重新做校正:依前面的 reset 命令清除狀態,確認未標零時不能寫入,再重新標零。不要把復電後保存的「可寫入」狀態當成這次校正已完成。

任何一步不符合,就保留當下紀錄並停止往下簽「通過」。

#讀取復電後網路/MQTT

macOS/Linux:

bash
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/system" -o after-system.json

Windows PowerShell 5.1/7:

powershell
curl.exe --fail --show-error -H "Authorization: Bearer $env:TOK" "$env:DUT/api/system" -o after-system.json

應該看到/要核對:用文字編輯器開啟 after-system.json,看 network.ip、network.netType、network.mqttConnected;分別確認設備位址、使用的網路及 MQTT 已連上。接著仍要接收新訊息。

不符合時:若檔案沒產生、回覆不是完整 JSON 或設備無法連線,先確認 IP 與登入資料。保留錯誤,不要拿舊檔案當這次結果。

#讀取復電後診斷

macOS/Linux:

bash
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/diag" -o after-diag.json

Windows PowerShell 5.1/7:

powershell
curl.exe --fail --show-error -H "Authorization: Bearer $env:TOK" "$env:DUT/api/diag" -o after-diag.json

應該看到/要核對:用文字編輯器開啟 after-diag.json,比對 bootCount(啟動次數)與 fault(故障紀錄)。UNEXPECTED 也可能來自外部斷電,不能單看這個字就說當機。至少 180 秒後再讀一次,確認沒有多一次重開。

不符合時:若檔案沒產生、回覆不是完整 JSON 或設備無法連線,先確認 IP 與登入資料。保留錯誤,不要拿舊檔案當這次結果。

#預覽既有標準範本遷移(TS.1)

macOS/Linux:

bash
python3 migrate_measurement_timestamps.py --gw "$GW_IP" --out timestamp-dry.json

Windows PowerShell 5.1/7:

powershell
python migrate_measurement_timestamps.py --gw "$env:GW_IP" --out timestamp-dry.json

應該看到/要核對:只列出準備新增 timestamp 的標準範本;沒有標準範本時零變更不代表新 UI 已測,須另依網頁建立流程。

不符合時:若工具報錯或列出非預期變更,先不要繼續套用。保存結果檔與私有備份,請工程人員核對。

#套用已核對的遷移

macOS/Linux:

bash
python3 migrate_measurement_timestamps.py --gw "$GW_IP" --apply --backup-dir ./private-backups --out timestamp-apply.json

Windows PowerShell 5.1/7:

powershell
python migrate_measurement_timestamps.py --gw "$env:GW_IP" --apply --backup-dir ./private-backups --out timestamp-apply.json

應該看到/要核對:原欄位、精度不變;工具先保存私有完整備份到目前工作目錄的 private-backups。失敗時看 rollback 結果,不可直接標 PASS。

不符合時:若工具報錯或列出非預期變更,先不要繼續套用。保存結果檔與私有備份,請工程人員核對。

#確認第二次無變更

macOS/Linux:

bash
python3 migrate_measurement_timestamps.py --gw "$GW_IP" --out timestamp-second.json

Windows PowerShell 5.1/7:

powershell
python migrate_measurement_timestamps.py --gw "$env:GW_IP" --out timestamp-second.json

應該看到/要核對:用文字編輯器開啟 timestamp-second.json,應看到 "changes": [],也就是沒有需要再改的項目。

不符合時:若工具報錯或列出非預期變更,先不要繼續套用。保存結果檔與私有備份,請工程人員核對。

#訂閱新磅秤量測(TS.2)

macOS/Linux:

bash
mosquitto_sub -h "$MQ_HOST" -p 8883 --insecure -u "$MQ_USER" -P "$MQ_PASS" -t "$PREFIX/scale/weight" -q 1 -R -C 3 -W 60 -v

Windows PowerShell 5.1/7:

powershell
mosquitto_sub -h $env:MQ_HOST -p 8883 --insecure -u $env:MQ_USER -P $env:MQ_PASS -t "$env:PREFIX/scale/weight" -q 1 -R -C 3 -W 60 -v

應該看到/要核對:先於另一視窗開啟既有量測流程;收到 3 筆新訊息且 timestamp 是 UTC 字串。超時不是 PASS;電表改 topic 最後為 /power。

不符合時:若 60 秒內收不到足夠的新訊息,先查設備是否連上 MQTT、發佈流程是否開啟及 topic 是否正確;不能因為沒有報錯就算通過。

#時间故障/補送驗收的執行邊界(TS.3~6)

TS.3 的未校時、時鐘更正及 TS.5 的錯框注入,在隔離時鐘/UART 治具執行並附出廠結果;不可把正常現場訂閱三筆資料當作已測這些情境。TS.4 必須用支持 persistent session 的 MQTT 客戶端:建立固定 client ID、clean session=false、QoS1 訂閱且確認 SUBACK;先取得有效样本,在治具暫停來源但維持 broker 發布,斷開 subscriber 至少 8 秒,再以相同 ID/session 重連,保存排隊 payload 與接收 UTC,比對原時間未被重標。不同平台均可使用已安裝 MQTTX 的相同 GUI 步驟;這是 GUI 動作,不假冒一行 shell 可完成。測完清除該測試 session。TS.6 由已核准的隔離 Converter 測試範本固定數值、保持樣本時間前進,再改數值;執行者需附測試原始輸出。無治具時標未執行,由出廠工程師補足。

#保存結束設定

macOS/Linux:

bash
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/config/backup" -o after-config.json

Windows PowerShell 5.1/7:

powershell
curl.exe --fail --show-error -H "Authorization: Bearer $env:TOK" "$env:DUT/api/config/backup" -o after-config.json

應該看到/要核對:逐欄核對核准的校正/timestamp 變更,其餘設定不變;移除臨時測試通道與訂閱。正式要留的校正流程不要 remove。

不符合時:若檔案沒產生、回覆不是完整 JSON 或設備無法連線,先確認 IP 與登入資料。保留錯誤,不要拿舊檔案當這次結果。

#只在本輪臨時安裝時移除校正夾具

macOS/Linux:

bash
python3 scale_multical_build.py remove --gw "$GW_IP"

Windows PowerShell 5.1/7:

powershell
python scale_multical_build.py remove --gw "$env:GW_IP"

應該看到/要核對:僅在本輪新建且不交付的測試流程使用;既有正式校正流程跳過本步。移除後再保存/核對完整設定。

本節完成後仍須執行本文最後備份與結果簽核。TS.7 接收端未提供測試入口時填待整合,不能用 gateway 發出成功替代接收端依樣本時間入庫。

不符合時:若不確定這是不是本輪臨時建立的流程,先不要移除;請負責人確認,避免刪掉要交付的設定。

#校正與時間戳:現場執行順序

本節整合原 10/6 交付清單,作為既有驗收計畫的操作入口;不新增另一套驗收編號。校正指令與砝碼操作只維護在 線上多段校正指南,結果填入本文 OC.1~12、TS.1~7;出廠結果不能代簽現場結果。

#到場前先核對

  • 本輪 6.0.134 beta / calibration-candidate,Build ID 35B4EEB5DA9C49EC9B363566A7EFE4B7。攜帶完整 MESB 與版本/SHA-256記錄,核對 BIN 778128 bytes(780000 上限剩1872 bytes)、八個資產與目標 UID;保留回復包及現場設定備份。雲端清單需選 beta 才能找到,不屬 stable。
  • 檢查本輪長時觀察最終證據:須為 status=complete 且 pass=true;running 或仍有未解釋連線重置均不能簽 PASS。已發生的失敗需保留與釐清,不因文件發布而解除。
  • 由業主確認 kg、砝碼 5/20/20 kg、安全秤量至少涵蓋 45 kg、允許誤差與控制器原曲線恢復方式。未確認前不寫校正;OPTA config 備份不能代替控制器曲線備份。
  • 本輪先做正常流程;故障注入及物理斷電須另確認授權。未執行項目填 NOT RUN,不能將部分完成記成整套通過。

#現場照此順序進行

以網頁操作為主時:先開 設備網頁操作 A~F,逐步完成新增/編輯、設定同步、總覽對值、建立流程與發送開關;時間戳依 網頁預覽與接收核對。每步保留畫面與實際資料證據。新版多段命令由業主 Edge 或工程工具控制,OPTA 本身尚無三/五段表單;沒有業主頁面就將 OC.10 的畫面/入庫驗證記 NOT RUN。

順序 操作與文件 記錄位置
1 核對版本/Build ID/UID/資產,備份設定;確認 RS485 供電、站號及面板讀值,儲存同步設定 OC.1、OC.2
2 依線上校正指南建立正常重量流程;只訂閱原重量 topic,query 取得同包 calibration 狀態 OC.4、TS.1、TS.2
3 依指南清空標零,逐次新增 5/20/20 kg,核對總量 5/25/45 kg、匹配 applied 及 complete;五段另按 OC.3 驗證 OC.3、OC.4;三段通過不能代簽五段
4 卸載/重新加載,依已知砝碼核對各點與點間精度;物理斷電保存另記執行與授權 OC.11
5 核對原量測 UTC ISO8601/未校時 null、重量與時間同次讀取、延遲不重標時間,及業主 Edge 入庫與校正資料隔離 TS.1~7、OC.10;故障/治具項目依原表範圍另驗
6 逐項列出實測、模擬、NOT RUN 與 FAIL,附原始 payload、sample、boot/sid/seq、ACK、誤差及操作者;清理測試配置與最終備份 OC.12、TS.7 及既有結果簽核

只收到重量不是 ACK;只有匹配命令的 applied 才能前進,控制器 ACK 也不代表精度通過。正式放行仍須滿足下列 OC 全項及既有 1.0 門檻。

#線上多段校正驗收(WI-252,OC.1~OC.12)

這一節驗的是「正常秤重持續上報、由 Edge 控制月度校正」的新流程。原 123 不支援;134 beta已在 .46 完成三段實秤與收尾,完整精度/五段/業主 Edge 驗收仍待完成。 以下是必做規範,不是已通過紀錄。先完成 OC.1,才能在隔離設備執行後續寫入;USB 接上不等於這個門檻已通過。

交付線上校正時,OC.1~OC.12 全部納入驗收與出廠;不能因一般 opta 建置未啟用功能就自行填 N/A。若業主另決定不交付,須附明確範圍決定。既有 SC 是舊訊號流程的驗收,不能替代新協定;本版只支援 3/5 段,舊版的 4 段與單段覆寫不套用此處。

#先準備紀錄與治具

記下候選 commit、BIN/MESB SHA-256、版本/variant/Build ID、8 個資產雜湊、UID/機型/網路、起訖 boot、MQTT prefix、磅秤型號/韌體/站號/通道/小數位/單位、安全秤量、砝碼證明、允許誤差與操作者。先保存完整 config 和現有控制器設定/校正資料;不能匯出原曲線時,必須使用可重新校正的隔離秤,不可承諾用 config 備份回滾曲線。

只訂閱原重量流程 MQTT topic(預設 {prefix}/scale/weight),從同包 calibration 取得狀態/ACK,等訂閱成功後才向 {prefix}/cmd/calibration 發命令,保留原始 JSON 與接收時間;不得把密碼、token 放入證據。負例需要可記錄 FC16 寫入、能控制回覆的 RS485 治具/模擬器;OPTA USB 日誌不能代替匯流排交易證據。缺工具就記 NOT RUN 和接手人,不在業主運轉秤上猜時間拔線做故障注入。

#工程人員先跑電腦上的完整流程

在專案根目錄、具備 PlatformIO、C++、paho-mqtt、mosquitto 的 macOS/Linux 開發環境執行;原生 Windows 不宣稱已驗證此主機建置流程。

bash
pio test -e native -f test_scale_session -f test_measurement_polling -f test_converter_engine
python3 scripts/tests/test_scale_online.py
python3 scripts/scale_edge_sim.py --simulate --capacity 50 --increments 5,20,20 --log /tmp/scale-three.jsonl
python3 scripts/scale_edge_sim.py --simulate --capacity 50 --increments 5,5,10,10,15 --log /tmp/scale-five.jsonl

預期:目前上述三個 suites 合計 61 項 native、17 項 adapter/Edge 通過;兩輪最後均 state=complete,三段總量 5/25/45 kg,五段 5/10/20/30/45 kg。任一命令非零退出即停止,保存失敗與重跑為不同輪。這些只證明主機/本機 MQTT 流程,不能勾選下表的實機或業主 Edge 結果。

#實機逐項操作與通過條件

ID 你要做什麼 看到什麼才通過 必留證據
OC.1 工程人員建置 opta-calibration,確認 BIN ≤780000 bytes 並成功包入 8 資產;凍結版本後再依既有 OTA 程序安裝到隔離設備,query 確認支援新協定 容量、完整包、安裝後 Build ID/variant/資產與 UID 都吻合;不靠同版號或 USB 能燒入判斷 打包結果、manifest、安裝前後 boot、API/query 原文;134實際BIN778128 bytes;早期786016容量FAIL另留歷史
OC.2 選 TDA08B 樣板;記錄控制器版本;TDA-08B使用 bit 4=1 正常/0 故障,132不以版本白名單阻擋。確認通道 1、kg、安全秤量。儲存前同步小數位:相同直接存,不同先更新並要求再次確認;在治具測無效小數位、超精度與超容量命令 生產讀值與面板單位/倍率一致;begin 使用控制器設定換算;不合法命令沒有校正寫入;分度值不重複相乘 控制器 200–204 原值、表單前後、命令/ACK、匯流排記錄;設定同步屬 WI-251,與新協定結果分列
OC.3 清空標零,依序加 5/20/20 kg;保留前段砝碼,完整收尾。另從清空開始跑五段 5/5/10/10/15 kg 提交總量依次 5/25/45 與 5/10/20/30/45;FC16 的 195/196 是完整 32-bit;最後下一段重量 0、199=100,有匹配 applied 及 complete 每段砝碼總量、原始 request/ACK、194–196 讀回、199 寫入;applied 不代表精度
OC.4 正常上報時進入校正;同一重量保持不動,觀察至少 10 個不同 reading;再正常收尾/取消 只訂閱原重量 topic,沒變重量仍有 calibration.sampleEvent=true 與新 reading;原 Converter/on-change 設定前後完全一致,結束後按原規則發送;其他設備繼續輪詢 原重量 topic 完整 payload(含 calibration)、sample/reading 序列、完整設定差異、其他設備新樣本;只收到重複包不算新採樣
OC.5 治具依次送不穩定、舊 revision、舊 sample、重複 sample、超過 2500 ms 的樣本,再恢復至少5秒穩定量測;最後一段後立即嘗試 finish 不合格時 zero/point/finish 不寫入;不能靠同包三次變 ready;最後一段也須等新穩定樣本才收尾 發送與採樣時間、拒絕原因、寫入次數為 0/恢復後正確次數
OC.6 重送完全相同的已處理 point;再用相同 ID 改內容、舊 sid、錯 seq、等於/小於上一段重量。隔離阻斷一次 MQTT applied 回覆後原樣重送 相同請求只回傳既有結果、不重做/不再累加;其餘拒絕;只有匹配 boot/sid/seq 的 applied 允許 Edge 前進,accepted 或重量到達不算 MQTT 原始命令/ACK、控制器 commit 次數;保留回覆遺失事件,不能只截最終綠燈
OC.7 隔離治具丟棄 staging/commit 回覆、staging 後不穩定/感測器 bit 4 清零、改倍率/分度/映射;另注入短暫讀取逾時/短封包/CRC 錯誤,及有效量測中斷達 10 秒(含門檻後首次讀回成功)。人工搬放情境持續有效但不穩定讀值及 ping 超過 5 分鐘後恢復穩定 133 起短暫週期唯讀失敗保留會話、valid/ready=false、weightRaw/timestamp=null,135須至少5秒穩定量測與新命令才能續段,舊 proof 拒絕;人工等待不因重量變動中止。長時讀取缺口/設定/感測器異常及交易失敗仍 unknown,恢復後不自動續寫;staging 未確認不得 commit 逐筆 RTU/MQTT、原因碼、seq/revision、穩定區段/reading及實際寫入次數;區分 host 與實機證據,實秤不做未授權故障注入
OC.8 在已寫一段後關閉 Edge 客戶端並停止所有 ping,超過 60 秒後 query;另測 Edge 重開、OPTA 重開與舊 boot 命令重送 租期過後 partial;Edge 重開只查詢、不接管;OPTA 新 boot、recovery_required,舊命令拒絕;明確 cancel 不回滾,完整重做才解除業務端待校正標記 起訖 boot、sid/seq、關閉/query 時間、舊命令結果;不得讓另一個自動 ping 掩蓋斷線
OC.9 校正期間用舊規則/API 對目標站號及站號 0 發寫入,再嘗試 bus 變速;並觀察其他設備讀值。結束後在隔離治具驗保留鎖釋放 目標/廣播寫入與變速被拒絕且未上線;其他設備正常讀取;complete/cancel/lease 結束後回到原設定;unknown 保留鎖直到明確 cancel UART/匯流排交易、API 結果、其他設備 sample、清理前後配置;用主機模擬證據不得冒充實機
OC.10 業主 Edge 僅訂閱原重量 topic,依同包 calibration 狀態及匹配命令 ACK 啟停按鈕,收錄校正與正常重量;測本次開機未校時、校時後、延遲接收及重送 unknown/斷訊/未 ready 時不能續段;砝碼資料不進生產統計;時間是原量測 UTC 或 null,延遲接收不改時間、不重算穩定次數 業主畫面操作、入庫/統計對帳與兩端原始記錄;目前只有 Edge 模擬器,實際頁面仍 NOT RUN
OC.11 完整校正後用已知砝碼測所有校正點與點間重量、升載及降載;分別將控制器、OPTA 真正斷電再上電,重查讀值與配置 各點在事先核准的誤差內;控制器曲線保留,OPTA 設定/倍率與新樣本正常;OPTA 會話不自動續跑 砝碼/誤差表、面板與上報值、電源全滅與重上電時間、boot。USB/DC 所有供電皆須切斷;軟體 reboot 不代替
OC.12 以事先記錄的時長、負載及最大允許樣本間隔觀察固定/變動重量、正常與校正串流、heap/配置失敗與重開;最後移除本輪臨時治具並恢復正式配置、送最終雲端備份 無未解釋缺樣、非預期重開或記憶體異常;所有臨時資料清除,正式規則/on-change/其他設備、雲端最終快照相符;完成現場與工站各自簽核 起訖診斷、樣本間隔分布、每次缺樣/重開、配置 diff、備份工作及讀回、負責人。時長或誤差未定即 BLOCKED,不以短輪取代長時

OC.3 的現場操作命令、清空/加砝碼提示與取消命令集中在 線上校正操作指南。先 query 確認新協定,再執行 --run;不要把舊 cmd/signal 的按一下 1→0 規則混用到新協定。

#每輪填一張結果表

每個 OC ID 逐項填:層級(host/MQTT+UART替身/真實OPTA+RS485治具/實秤/業主Edge)、結果(PASS/FAIL/NOT RUN/BLOCKED)、實際觀察、證據路徑、負責人、剩餘條件。失敗→重跑保留兩列。OC.11 的誤差與 OC.12 的時長/間隔/heap 判準必須在測前填寫;尚無業主約定就保持 BLOCKED。

acceptance_run.py 與 test:gate:full 尚未自動包含 OC.1~12。兩支程式全綠也不能代替這張表。凍結候選後仍須重跑既有 gate/regression/converter/backup/persist/factory/acceptance;按 8310/8320 及適用網路逐台取證,舊版 PASS 不繼承。新版本測完後更新本輪證據、卡片、放行表與文件網站/PDF;測試規範已更新不代表已簽核或已發布。

#出廠測試對照 —— 這一項在工站是誰在守

這張表是給 scripts/test_coverage_matrix.py 讀的,也給人看。 兩份文件用同一套行為描述,但編號各自獨立;這裡把它們接起來, 才知道「現場驗的這一項,出廠時有沒有人守過」。

只列有把握的對應。 沒有把握的寧可讓它空著 —— 假的對應會讓缺口從表上消失,比漏列更糟。

出廠規範:qa-factory-test.md

驗收測項 出廠對應 說明
0.1 0A.7 工站供電接地檢查及現場接地交接分開記錄
8.2 8.2 工站照片歸檔,現場照片由現場補齊
8.3 8.3 出廠簽核與業主現場簽核分責交接
U.0 2A.10 畫面/API/設定一致性與現場操作紀律
CT.1 CT.1 計數器事件一次一則與靜止不發
OC.1 OC.1 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.2 OC.2 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.3 OC.3 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.4 OC.4 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.5 OC.5 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.6 OC.6 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.7 OC.7 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.8 OC.8 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.9 OC.9 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.10 OC.10 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.11 OC.11 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
OC.12 OC.12 WI-252 線上校正逐項承接;主機、實機與業主 Edge 證據分層,未測不代簽
SC.1 SC.1 新版完整步驟與通過條件一對一承接;結果依實際證據分層
SC.2 SC.2 新版完整步驟與通過條件一對一承接;結果依實際證據分層
SC.3 SC.3 新版完整步驟與通過條件一對一承接;結果依實際證據分層
SC.4 SC.4 新版完整步驟與通過條件一對一承接;結果依實際證據分層
SC.5 SC.5 新版完整步驟與通過條件一對一承接;結果依實際證據分層
SC.6 SC.6 新版完整步驟與通過條件一對一承接;結果依實際證據分層
SC.7 SC.7 新版完整步驟與通過條件一對一承接;結果依實際證據分層
TS.1 TS.1 新版完整步驟與通過條件一對一承接;結果依實際證據分層
TS.2 TS.2 新版完整步驟與通過條件一對一承接;結果依實際證據分層
TS.3 TS.3 新版完整步驟與通過條件一對一承接;結果依實際證據分層
TS.4 TS.4 新版完整步驟與通過條件一對一承接;結果依實際證據分層
TS.5 TS.5 新版完整步驟與通過條件一對一承接;結果依實際證據分層
TS.6 TS.6 新版完整步驟與通過條件一對一承接;結果依實際證據分層
TS.7 TS.7 新版完整步驟與通過條件一對一承接;結果依實際證據分層
NR.1 NR.1 新版完整步驟與通過條件一對一承接;結果依實際證據分層
2C.3 ST.6 版本一致(index.html / app.js / 語系檔同版)
4.1 BK.0、ST.15 有效身份(含 license fallback)實際可讀本設備歷史
4.2 BK.1 推送之後雲端要真的出現新快照
4.3 BK.1 同上 —— 這一項才是真正的驗收點
5.3 H3 RS485 讀得到值
6.1 H5 數位輸入看得到、會變
6.2 H2 驅動輸出(主機 + 擴充)
6.3 ST.13 I/O 通道名稱要存得進去
8.1 BK.1 最終備份推得上去
A1 H5、H2 實體輸入驅動實體輸出
A2 CV.2 MQTT 命令驅動實體輸出(端對端)
A3 EN.1 停用規則不動作、重新啟用要回得來
B1 CV.3 持續 × 連續
B2 CV.4 持續 × 不重複
B3 CV.4 可逆性 —— 取消勾選重複要回來
B4 CV.3 啟動式 × 連續
B4b CV.4 啟動式 × 不重複
B6 CV.4 閘門關再開要補發
M.3 CV.1 Parser 從 JSON 抽出數值
D1 ST.4 存檔後立即讀回一致
D2 3.2、ST.4 F5 畫面+API 讀回;執行器只驗 API,畫面另人工核對
U.2 2B、EN.2 建立輸入群組(頁面 + 停用行為)
U.3 2B、EN.3 建立輸出群組
U.4 2C、EN.1 建立規則並綁定
U.6 3.2、3.4、ST.16 F5 與重開後設定完整,不能只用 ST.4 代替
U.7 BK.1 推一次雲端備份收尾
R.5 BK.2、BK.3 從雲端還原,而且還原後不能自斷後路
7.4 H9 設備與網路設備一起斷電(冷開機,WI-236)
B5 CV.5 用訊號內容決定回報頻率(送 N → 每 N 秒)
B3b CV.4、CV.6 並排對照 —— 同一時間、同一模板,只差那個勾勾
D3 ST.16 重開機後設定逐項完整
0.2 H3 RS485 佈線與 Slave ID 不重複
5.1 H3 掃描匯流排
5.2 H3 逐台加入 RS485 設備
5.4 H3 現場觸發,面板值與畫面一致
M.4 ST.11 數值門檻的不可能條件要被擋(WI-184)
U.1 ST.13 I/O 通道命名要存得進去
D4 ST.16 逐項清點全部設定(跨重開)
A.0-1 2B、EN.2 建立輸入群組
A.0-2 2B、EN.3 建立輸出群組
A.0-3 2C、EN.1 建立規則並綁定

| 0.3 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 1.1 | 0A.1 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 1.2 | 0A.1 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 1.3 | 0A.1 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 1.4 | 0A.1 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 1.5 | 0A.1 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2A.1 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2A.2 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2A.3 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2A.4 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2B.1 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2B.2 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2B.3 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2C.1 | 0A.3 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2C.2 | 0A.3 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2D.1 | 0A.4 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2D.2 | 0A.4 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 2D.3 | 0A.4 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 3.1 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 3.2 | 0A.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 5.5 | ST.16 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 7.1 | H10 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 7.2 | H10 | 依對应段落核對相同行為;治具操作與人工結果另記 | | 7.3 | H10 | 依對应段落核對相同行為;治具操作與人工結果另記 | | M.1 | CV.7 | 依對应段落核對相同行為;治具操作與人工結果另記 | | M.2 | CV.7 | 依對应段落核對相同行為;治具操作與人工結果另記 | | R.1 | 0A.1 | 依對应段落核對相同行為;治具操作與人工結果另記 | | R.2 | BK.1 | 依對应段落核對相同行為;治具操作與人工結果另記 | | R.3 | 7.1、7.2 | 依對应段落核對相同行為;治具操作與人工結果另記 | | R.4 | 7.1、7.2、7.5 | 依對应段落核對相同行為;治具操作與人工結果另記 | | U.5 | 5.5、5.6 | 依對应段落核對相同行為;治具操作與人工結果另記 |

#對照範圍與剩餘驗證

兩欄的 ID 各屬自己的文件;例如驗收 7.1 對應出廠 H10,不代表出廠 7.1。產生器已納入純數字 Phase 與子項,不能再把「解析器沒讀到」當成缺陷。

0.1 供電接地、8.2 照片、8.3 簽名由現場完成;U.0 是共通操作紀律,對應出廠 Phase 2 的畫面/API 核對紀律。0A 各項在工站以測試網路與治具執行,不代表客戶現場接線已驗收。

仍需人工/專門治具:B3b/CV.6 並排對照、D4/ST.16 完整持久化、H9/H10 真實斷電、0A 空機與授權。D3 的既有自動化只比較清單子集;不能拿它替 D4 全設定簽章。靜態對應完整不代表這些測項已通過。


#階段 8:結案

步驟 動作 通過
8.1 按「立即推送」備份最終設定 推送成功,且版本歷史看得到這一筆(同 4.3,要再確認一次)
8.2 拍照存證:銘牌、接線、總覽頁 與本單一併歸檔。日後遠端支援省下大量往返確認
8.3 與業主逐項核對、雙方簽名 全部勾選,或未通過項目已書面列出並取得同意

8.1 用指令做(與 4.2/4.3 相同,但這次是最終設定)

bash
# 先做一個實際變更,否則雲端會因內容相同而去重、不產生新快照
curl -s -X POST -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" \
     -d '{"deviceName":"<交機用的正式名稱>"}' $DUT/api/config

curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/configserver/push
sleep 20
curl -s -H "Authorization: Bearer $CLOUD_TOK" \
  "$CLOUD/api/devices/$UID_DEV/snapshots?limit=50" | python3 -c 'import sys,json,datetime
d=json.load(sys.stdin); l=d if isinstance(d,list) else d.get("snapshots",[])
def when(x):
    v=x.get("createdAt") or x.get("timestamp")
    try: return datetime.datetime.fromtimestamp(int(v)/1000).strftime("%m-%d %H:%M:%S")
    except Exception: return str(v)
print(f"共 {len(l)} 筆,最新 3 筆:")
[print("  ",when(x), (x.get("summary") or "")[:40]) for x in l[:3]]'

看到剛才那個時間戳才算 8.1 通過。 推送回 success 只代表「已排程」。


#現場排錯速查

#🔴 JSONDecodeError: Expecting value: line 1 column 1 (char 0)

這是本文件最常見的錯誤訊息,而且它有兩個完全不同的原因。 它的意思永遠是同一件事:curl 什麼都沒吐出來,python3 收到空字串。 所以要往上游查「為什麼 curl 是空的」,不是查 python。

原因 怎麼分辨 怎麼處理
① 變數沒設 —— 網址變成沒有主機的字串 把該行的 curl 單獨跑一次(去掉 | python3 …),會看到它連個像樣的網址都沒有 回〈確認六個都設好了〉,六項全 ✅ 再往下
② 設備沒回應 —— 忙碌 / 剛重開 / 掃描中 單獨跑 curl 也是空的,但再跑一兩次就成功 重跑就好。設備是單執行緒、只有 4 個 socket

⚠️ 設備剛重開(OTA、恢復原廠、拔插網路線)之後的前幾分鐘特別容易發生。 它忙著重連 MQTT 與雲端,那段時間打什麼都可能斷。 本文件的指令都沒有重試 —— 閃斷一次就會吐出這一整頁 traceback, 看起來像雲端壞了,其實什麼事都沒有。

開跑前先等它就緒(輪詢實際狀態,不要用固定秒數):

bash
until curl -s -H "Authorization: Bearer $TOK" $DUT/api/system | grep -q '"mqttConnected":true'; do
  echo "  暖機中…"; sleep 15
done
echo "✅ 可以開始"
現象 最可能的原因 先做這個
設備開出 Wi-Fi 熱點 Wi-Fi 帳密錯誤或訊號不足 連上熱點重設帳密
網頁排版跑掉、很陽春 網頁資源未完整燒入 開 /rescue 確認,回報後重燒
版本歷史顯示 Unauthorized 有效身份無效或沒有該設備權限 檢查所用 token/license 與 UID 歸屬;不要只因 cloudToken 空白就重填
掃描看得到、總覽沒數字 設備未加入設定,或暫存器未設 查 /api/config 的 modbusDevices
畫面所有數值都空白 回應不是合法 JSON 用 5.3 的指令看 Python 會不會拋錯
RS485 某台讀到 0 接線/Slave ID/鮑率/該台沒電 依此順序逐項排除,並用 /api/modbus/probe
加了新設備,舊的讀不到 Slave ID 撞號 逐台改成不重複的 ID
類比輸入固定 0 4–20mA 迴路斷開 迴路是串聯,逐點量測找斷點
MQTT 一直收到相同的值 該通道「🔁 變更時才發」沒勾 逐通道檢查勾選
重開後設定不見 設定未落盤 —— 嚴重 停止使用並回報

先跑這一組,八成的問題在這裡就看得出來

bash
# 一次把「它活著嗎/為什麼重開/網路走哪條/授權狀態」全撈出來
curl -s -H "Authorization: Bearer $TOK" $DUT/api/diag   | python3 -m json.tool
curl -s -H "Authorization: Bearer $TOK" $DUT/api/system  | python3 -m json.tool
curl -s -H "Authorization: Bearer $TOK" $DUT/api/license | python3 -m json.tool

# 設備自己的 serial log(免拆機、免接 USB)—— 卡點與錯誤都在這裡
curl -s -H "Authorization: Bearer $TOK" $DUT/api/log | tail -60
log 裡看到 意思
[SLOW LOOP] total=… segs: … scan=… 正在掃描 RS485,整台會停住約 1 分鐘(正常)
[SLOW LOOP] … net=… / mqtt=… 對外連線把主迴圈卡住,看秒數判斷嚴重性
MQTT: Connecting 反覆出現 連線不穩,見 WI-210;先確認不是有人在密集操作
MQTT Queue: FULL 發送量超過連線能力
self-ping x2 無 echo → AUTO-REBOOTING 設備即將自我重開(MQTT 訂閱失效)

bootCount 沒變 = 沒有重開過,uptimeSec 持續累加 = 沒有卡死。 這兩個數字先看,可以立刻排除掉一半的猜測。

如果 bootCount 真的變了,先讀 bootDetail 再下結論 —— 設備會為「自己主動做的重開」 留下字條,只有沒字條的才值得緊張:

resetReason / bootDetail 意思 要緊嗎
SOFTWARE / api-reboot 有人(或測試腳本)呼叫了重開 正常
SOFTWARE / ota 韌體更新後自動重開 正常
SOFTWARE / net-failover 網路線掉了,切到 WiFi 正常(見驗收 ⑥)
SOFTWARE / net-recovery 網路線插回來了,切回有線 正常(見驗收 ⑥)
SOFTWARE / cs-wedge 對外連線卡死,設備自己救自己 不是當機 —— 見下方說明
SOFTWARE / mqtt-resub MQTT 訂閱失效,自我重開 正常自救
UNEXPECTED / (空) 沒有字條 —— 斷電、或真的當掉 斷電測試時正常;沒人碰卻出現要回報

關於 cs-wedge: 對外連線(雲端 / MQTT 的 TLS 握手)一旦卡死,在這顆單執行緒的 MCU 上沒辦法從外部中斷 —— 所以設備設計成「卡超過上限就自己重開」。 看到它代表保護機制正常動作,不是設備壞了。 2026-09-01 實測:設備跑在 WiFi 上時對外 TLS 要 3.8~6.0 秒(有線快得多), 訊號弱或上游慢的站台比較容易踩到。若反覆出現(例如十幾分鐘一次), 那就不是偶發,要查無線訊號強度或上游網路品質。


#文件修訂紀錄

日期 版本 變更
2026-08-21 1.0 初版。涵蓋歸零三級、安裝設定、情境驗收(I/O 互動、MQTT Converter 2×2 矩陣、設定值三關)、逐步 CLI 指令與現場排錯速查。
2026-08-25 1.1 首次實機照著走過一遍(8310 .250 + 8320 .46)後的修正,四項都是假 PASS 或指錯位址:① R.4 的「TCP 通道」永遠印 0(config.tcp 是監聽埠不是清單,且型別是 dict)→ 改查 /api/tcpio。② R.4 通過條件寫了「規則」卻沒查 → 補 /api/rules、/api/signals、/api/actions。③ R.4 寫死 AP 位址 192.168.3.1,但 8310 不會開熱點 → 改用 $DUT 並附位址對照表。④ R.2/4.3 的「確認雲端收到」打設備端 /api/configserver,那條永遠不吐清單 → 改打雲端 /api/devices/$UID_DEV/snapshots,env 範本補 CLOUD/CLOUD_TOK。另修 1.5 心跳燈顏色:原寫「USER LED(綠燈)」,實際 8320 是藍燈、8310 是琥珀(紅+綠)。韌體基線 6.0.67 → 6.0.68。
2026-08-25 1.2 實際執行破壞性路徑(L2 恢復原廠 → 雲端還原)後補寫,在 8320(.46,fw 6.0.68)實機跑完整循環。① 新增 §R.3 的 🛑 前置警告與 §R.5 還原步驟 —— 恢復原廠會連 Cloud Token 一起清掉,而設備 token 為空時不送授權標頭、雲端回 401,於是「雲端顯示還原成功、設備什麼都沒做、雙方都沒有錯誤提示」(已開卡 WI-194)。必須先填回 Cloud Token 才能還原。② 補上 pendingCommand 是一次性的 —— 失敗那次已被消耗,要重新觸發。③ 補上以 /api/log 查證還原是否真的在跑的方法,並註明該日誌是環形緩衝會被洗掉。實測數據:歸零後 65 秒回線;還原排入後約 2 分鐘取件,下載+重開後約 20 秒設定回來;規則 4→0→4、訊號 2→0→2、動作 3→0→3、RS485 1→0→1、SSID 有→空→有。
2026-08-25 1.3 修 WI-193/WI-194 並在 v6.0.69 實機驗證後補寫,又抓到兩件文件沒寫的事。① 歸零後打開網頁只剩「資產救援」頁 —— L2 格式化整個 QSPI,連 index.html/app.js/styles.css/語系/log.html/workflow.html 一起清掉;API 全正常但沒有可用 UI,交機流程走到 §2C 上傳 .mesb 才會恢復,而雲端還原只還設定、不還資產。已補進 §R.4。② 不要「剛改完設定就馬上觸發還原」 —— rollback 是把快照寫進雲端設定檔再叫設備下載,而設備 autoPush 會把它當下的設定推上同一個檔案;實測填完 Cloud Token 後 12 秒設備推送,還原被覆蓋,看起來「成功但沒變」。§R.5 補上「等 needsPush 變回 False 再觸發」與步驟重編號(已開卡 WI-195)。韌體基線 6.0.68 → 6.0.69。
2026-08-25 1.4 走查第二部(情境 M)後改寫 M.4 —— 原文承諾了一個產品沒有的能力。原本教人「來源選 Parser 通道 → 比較條件設 > 30」並宣稱「這證明整條 MQTT → 抽值 → 門檻 → 輸出 是通的」,但規則引擎讀 TCP 通道時只讀布林狀態(值 ≠ 0),不比較數值本身,韌體會直接回 400 拒絕存檔(WI-184 守衛)。改寫為實際可行的 = 1(非零)語意,並寫明「溫度 > 80 就報警」這類需求走這條路做不到。另補兩個會讓人白忙的細節:來源的 index 是 channelId 不是陣列位置;來源型別要用 7 不要用 9(型別 9 存得進去、HTTP 200,但規則引擎沒有處理它,實測送 25.5/35.0/99.9 訊號三次都是 False —— 已開卡 WI-196)。M.1/M.2/M.3 原文照抄實測全部通過。
2026-08-25 1.5 實機走完第二部情境 A(A1/A2/A3 全數 PASS) 後的修正。① A.0-1 補上「觸發類型」整段 —— 原文完全沒提到這個欄位,但它決定 A1 後半段成立與否:預設 Always 每個 loop 都驅動輸出;而「按鈕按下」的直覺選擇 Rising 會讓 信號為 False 的規則永遠不執行(韌體 main.cpp inline 規則引擎 if (rule->triggerOnTrue != signalValue) continue;),A1 的「解除→復歸」必定失敗。正解是 Change,已補完整對照表與韌體依據,並加註脈衝式輸入(觸控鈕)的例外用法。② A.0 要建兩個輸入群組 —— A1 需實體 DI、A2 需 MQTT 命令,原文只教建一個。③ 按鈕名稱更正 —— 文件寫「新增來源」,實際介面是「+ 加設備」;新增群組是右上角 +。④ A3 補上假 FAIL 陷阱 —— Change 觸發下,若停用期間已送過觸發值,重新啟用後再送相同值不構成變化、輸出不動,那不是「重新啟用失敗」;必須先把訊號送回原值再觸發(實測踩到)。⑤ 補 MQTT topic 前綴警告 —— 設定頁顯示 mes/gateway/001,實際收發用的是 24 碼 UID,照設定頁發命令會石沉大海。⑥ 加註不可連續快速新增 —— 連續建立會讓前一筆被無聲覆蓋且兩次都回成功(WI-199)。走查另開三張卡:WI-199(快速連續存檔 index 撞號覆蓋)、WI-200(/api/io/do 碰不到擴充輸出)、WI-201(通道改名無批次化、部分靜默失敗)。
2026-08-26 1.6 實機走完第二部情境 B(B1/B2/B3/B4/B4b 全數 PASS) 後的修正。① B1 的 JSON 模板範例是錯的,而且會造成最危險的假 PASS —— 原文 ${rs485.5.電流} 有兩處錯:欄位名用中文、Slave ID 用 5。韌體 resolveRs485Field() 只認英文欄位名(voltage/current/power/reactive/apparent/pf/freq/thd/kwh/kwhpos/kwhneg),中文名僅在剛好等於某個自訂暫存器名稱時才解得出來;型號模板設備(Finder 6M、SF965)沒有自訂暫存器,於是解析失敗靜默回 0.0。實測對照:照文件抄 → 每 5 秒準時收到 {"current":0.00},訊息有來、格式正確、間隔正確,照原判準會直接打勾 PASS,但值是憑空捏造的;改成 ${rs485.1.current} → 0.31/0.30/0.30。已補判準:真實量測會微幅跳動,整排恆定 0.00 幾乎必然是變數沒解出來。情境 C 的同款範例一併修正。② B2 補「類比量測必須配死區」 —— 原文只在「沒過」欄提了一句,實際上光勾「變更時才發」對電流這種訊號達不到 0 筆判準。補上實測三段對照(13 筆 → 8 筆 → 0 筆),並說明數位訊號勾了就夠、類比一定要加 :N 小數位修飾詞。走查另開一張卡 WI-202(JSON 字串欄位普遍未跳脫,一個引號讓整頁設定人間蒸發)。
2026-08-26 1.7 實機走完情境 C 與情境 D。① 情境 C 兩處象限編號錯誤 —— 「計數器 → B4 或 B2(務必勾變更時才發)」中的 B4 是「閘門 × 不勾」那一格,勾了就是 B4b,已改為「B4b(有閘門)或 B2(無閘門)」;「班別統計 → B3 啟動式 × 連續」的 B3 根本不是象限(它是可逆性驗證,文件自己在情境 B 開頭就寫明「B3 不在這張表裡」),已改為 B4。兩處都會把現場工程師導向錯誤組態。另補「編號對照」提示行。② 情境 D3 實測通過並抓到一項漂移 —— 逐項清點 40 個項目(網路/MQTT/主機與擴充通道名稱/RS485 設備/4 個輸入群組/6 個輸出群組/8 條規則/2 個 TCP 通道/Converter 模板)後 POST /api/system/reboot,設備 9 秒回來(bootCount 184→185,SOFTWARE/api-reboot),39 項完全一致,唯一差異是 converterTemplateLen 32→30,而模板內容前後完全相同 → 已開卡 WI-203(存模板不會把長度刷進 flash;不影響發佈,但可能讓跨機複製靜默遺失換算公式)。D1/D2 亦通過(每次存檔立即打 API 對照;整頁重載後群組、規則綁定、通道名稱全數正確)。
2026-08-26 1.8 實機走完階段 7(斷電復原)與 8.1。測法如實記載:以長按 reset完成(據現場說明其行為等同斷電),非拔除電源;冷啟動路徑(乙太 PHY/WiFi 模組真正斷電)因此未涵蓋,該台家族史上有冷開機競爭問題,建議日後補一次真正斷電。結果:7.1 PASS 設備自行開機回到原 IP,bootCount 185→186,resetReason=UNEXPECTED/bootDetail 空 —— 正是 BKPSRAM intent marker 的預期行為(硬體 reset 不留字條),與 D3 軟體重開的 SOFTWARE/api-reboot 清楚分得出來。7.2 PASS 逐項清點 40 項(網路/MQTT/主機與擴充通道名稱/RS485 設備/4 個輸入群組/6 個輸出群組/8 條規則/2 個 TCP 通道/Converter 模板)完全一致,無一走失;converterTemplateLen(WI-203)本次未再漂移,因 D3 後記憶體與 flash 已收斂為同值。7.3 PASS 逐通道展開確認勾選狀態不變。8.1 PASS 觸發推送後 11 秒新快照出現於雲端版本歷史(判準打雲端端點,非設備端)。文件新增一段警告:「MQTT 自行恢復」必須用狀態轉換雙向驗證,且出向要區分 retained 舊訊息與現發訊息 —— 走查時實際踩到「燈本來就亮著,送 1 還是亮,證明不了任何事」的假通過。
2026-08-26 1.9 與出廠測試對齊。① 在「自動化整批驗證」補上兩份文件的關係說明:驗收單的「出廠對應」欄位指向出廠規範的測項編號,兩邊是同一組行為。② 註明本次走查的五個缺陷已固化成 npm run test:regression(RG.1–RG.5,出廠規範 v2.7),目前預期紅燈、修好前不列入出貨門檻。③ 補一句實驗結論:閘門全綠不等於沒問題 —— 注入 WI-202 的壞值(整份 TCP IO 設定在網頁上消失)後,舊版 factory-pages 依舊 6/6 通過,該盲點已於 v2.7 修正(2B 改為比對設備通道數與畫面列出數)。
2026-08-26 2.0 由 Claude 主導完整重跑一次全流程(韌體 v6.0.73),涵蓋階段 0/2/3/4/5/6、情境 A/M/B/C/D、階段 7(以 reset)、8.1。本次挖到一個高嚴重性缺陷與三處文件缺口。① WI-206:Parser「值類型」下拉三個選項全部對錯韌體列舉 —— 韌體 ParserValueType 是 BOOL=0/NUMBER=1/STRING=2,而 UI 是 0=數值/1=字串/2=布林。選「數值」實際會得到布林,任何非零數字都變成 1.0,不會報錯。預設值正好落在錯的那格,不動下拉的人一定中獎。走 M.3 時 currentValue 顯示 1.0 而非 25.5 抓到(該測項的判準「不是 0、不是 1」正好擋得住)。已修前端 option value 並補 RG.7 回歸(先紅後綠實測)。② B4 前置補「為何用 ≥ 0.5 不用 = 1」 —— 只送 0/1 時兩者看似相同,但 B5 會送 5/15 這種秒數,用 = 1 會讓閘門關閉、B5 收到 0 筆而看起來像功能壞掉(走查實際踩到)。③ 4.2/4.3 補「先做一次實際設定變更再推送」 —— 雲端對內容相同的推送會去重,設定沒變時推送回成功但不產生新快照,看起來像失敗(誤判過一次);並補「設備端雲端設定要查 /api/configserver,/api/config 裡沒有這個區塊」。全流程結果:出廠閘門 35/35、回歸 7/7、情境 A/M/B/C/D 全數 PASS(B1–B6 含首次執行的 B5/B6)、D3 與階段 7 各自 40 項逐項比對完全一致。
2026-08-26 2.1 新增 scripts/acceptance_run.py —— 把全流程串成一次不中斷的執行。v2.0 的走查雖然全數 PASS,但那是散在幾十次互動裡邊跑邊修完成的,記錄裡還有「先失敗後修好」的項目 —— 證明不了「這套流程能不中斷地走完」。這支涵蓋所有可自動化的測項(階段 0/2B.2/3/4/5/6、情境 M、情境 B、情境 D、階段 8.1),任何一項失敗就整體失敗,且不做任何修復。實測需要六輪才跑通,每一輪的中斷都是真實的環境條件,已一併寫進本文件:① 掃描期間設備會拒絕 HTTP 連線(Connection reset by peer)—— 它忙著試 4 種鮑率 × 3 種同位,單執行緒的 HTTP server 排不上。這是正常的、bootCount 不變,但文件先前完全沒提,照「每 10 秒輪詢」的人會誤判成當機。已補明「拒連就當還在掃,每 15 秒問一次,最長等 5 分鐘」,並註明回應的鍵是 devices 不是 results。② 對外推送偶發 TIMEOUT —— 單次逾時不該判定驗收失敗,設備自己會在下個 autoPush 週期重試;測項改為允許重試並如實回報試了幾次。③ 另修正三處測試方法本身的問題(非產品):M.4 的 index 要用附加位置否則會先撞 Invalid index 而測不到 WI-184 守衛;B6 的訂閱必須先於開閘門(補發是立刻發生的,pub 之後才 subscribe 會落在 TLS 握手空窗裡收不到,導致同一項一輪綠一輪紅);讀取要對「HTTP 200 + 截斷的 body」重試,只重試連線錯誤不夠。最終結果:28 PASS · 0 FAIL · 0 SKIP · 12.9 分鐘,一口氣跑完無中斷(韌體 v6.0.74)。
2026-08-27 2.2 補上「給人工驗收者:今天從這裡開始」入口段,放在文件最前面。原因:自動化段落埋在第 1442 行,人工驗收者要翻過 1400 行才知道該從哪切入。新段落把作業拆成兩步——先跑 acceptance_run.py 取得綠燈前置,再逐項走機器碰不到的 7 項(附「桌上這台可否執行」欄,避免把「本站無此硬體」誤記成漏測)。並把階段 7 斷電與WI-197 網路故障切換合併成一次操作程序(拔網路線→net-failover/插回→net-recovery/拔電源→冷啟動),附 watch 指令與逐步預期值。另修正檔頭:版本 1.4→2.2、日期 2026-08-25→2026-08-27、韌體基線 6.0.69→6.0.74——修訂紀錄早已走到 2.1,檔頭一直沒跟上。
2026-08-27 2.3 修正過時說法 + 補一則畫面提醒。① 原文寫「RG.1–RG.5 目前預期紅燈、不列入出貨門檻」——那是修好之前的狀態,實際跑過後七項全綠(RG.5 另以 RG_ALLOW_REBOOT=1 單獨驗過),對應缺陷都已修進 6.0.74,早已併入 test:gate:full,照舊文走會誤以為交付物帶著紅燈。② 人工入口補上存檔徽章的說明:改通道名稱時上方跳出的「💾 bH 1」不是錯誤訊息,bH 是漏掉的語系鍵(WI-207),意思是「還有 1 筆正在存」——數字歸零再離開該頁。
2026-08-27 2.4 韌體基線 6.0.74 → 6.0.78。驗收走查期間業主回報「呼吸燈卡住時按鈕沒反應」,追出主迴圈有 6.8% 的時間完全凍結(授權門控每 30 秒驗簽 663ms + retained license/state 被反覆重送),已修正並實機驗證降到約 0.8%(WI-208)。同版修掉存檔徽章顯示「💾 bH N」的語系缺鍵(WI-207),本文件的徽章說明同步改回「💾 儲存中 N」。
2026-08-27 2.5 階段 5.1 的掃描警告大幅補強。原文只寫「網頁與 API 會連不上」,但實測(v6.0.78)單次掃描把主迴圈整整壓住 65 秒(scan=65202ms)—— 停住的不只網頁,實體 I/O 也不會動:呼吸燈停住、DI 不被輪詢、規則不執行。業主在重跑驗收時正是看到呼吸燈卡住而回報「問題又出現了」,實際上那是掃描,不是 WI-208 的回歸(同一份 log 中 gate 重驗 零次)。已補明「不要在掃描期間測按鈕」,並更正掃描範圍為 7 種鮑率 × 同位元 × 32 個 slave ID(原文寫 4 種鮑率)。成本本身另開 WI-209 追蹤。
2026-08-27 2.6 韌體基線 → 6.0.79。業主回報「呼吸燈大概 115 次會卡一次」——換算約 115 秒,對上雲端心跳的 127 秒週期。查出雲端每次心跳都重送同一則早已被拒的舊授權 token,設備每次都白白驗簽兩次(1.33 秒),期間主迴圈完全凍結。已於 WI-208b 修正(收訊前先比序號、不新就不驗;既有序號改快取;只有真的存了新 token 才重評門控),並一併修掉 v6.0.78 引入的 ODR 陷阱(header 裡的 static 讓失效機制沒接上,會導致撤銷授權延遲到重開才生效)。實測 544 秒涵蓋 8 次心跳,net 段卡頓 0 次。
2026-08-28 2.7 韌體基線 → 6.0.83。驗收期間業主快速切換實體開關,呼吸燈進入快閃(=MQTT 斷線中,非重開)。追出完整故障鏈:每條規則觸發都發一則通知、邊緣模式不節流 → 灌爆佇列 → 每則 TLS 發送阻塞主迴圈 265~400ms → 連線撐不住 → 重連風暴 → 若持續約 90 秒,self-ping 連兩次無 echo 會讓設備自我重開(WI-210)。依業主定下的原則「控制是現場的問題,訊息可以晚到」重做發送路徑(WI-211):QoS 配合語意(發送 1、通知 0、訂閱 2,皆可設定)、同 topic 合併 + 每 topic 最小發送間隔 1 秒、payload 可選夾帶歷史、每 60 秒重發現況兜底。實測 30 事件 5 秒:重連 4→0 次、最大卡頓 4398→無 >150ms。DI→DO 的控制路徑完全不經過 MQTT(ioManager.writeDO() 直接寫硬體),本次改動不影響現場控制。
2026-09-01 2.8 補齊六個章節的 CLI 指令。稽核後發現業主人工要走的幾段幾乎沒有可執行指令:階段 0(0.2/0.3 的實際檢查)、階段 4(雲端備份三步)、情境 A(撥開關時的 DI/DO 即時監看)、階段 7(7.2 改用快照 diff,不再用眼睛比對四十個項目;附 bootDetail 判讀表)、階段 8(最終備份)、現場排錯速查(一組先跑的診斷指令 + serial log 判讀表)。每一條都對實機打過才寫進文件,並修掉兩個會誤導的錯:① 4.1 原本讀 url/token(那是地端欄位,本站沒用是空的),雲端要讀 cloudUrl/cloudToken —— 照原本寫法會看到兩個空值誤以為沒設定;② 版本歷史原本直接印 epoch 毫秒(1787893314206),人無法判斷是不是剛推的那筆,改為本地時間。
2026-09-01 2.9 補上「怎麼找到設備 IP」。文件第一行就要你填 <設備IP>,卻從頭到尾沒說怎麼取得 ——DHCP 環境下那是事先不會知道的。新增 scripts/find_gateway.py(並在〈環境準備〉附完整可貼版):掃整個網段、先用首頁的 MES I/O Gateway 標題辨識(不需要 token),命中後才用 token 補型號、韌體、UID、MAC —— UID_DEV 直接從輸出抄,後面設 $PREFIX 與查雲端版本歷史都要用。實測約 1 秒,只讀不寫(每台最多三個 GET)。入口段也加了一行指引,並附掃不到的三種常見情況(不同網段/AP 模式/token 已改)。
2026-09-01 3.0 現場不需要專案原始碼也能執行。業主指出現場沒有 repo,python3 scripts/xxx.py 全是空話。① 兩支腳本改為從文件站直接下載(/docs/assets/find_gateway.py、acceptance_run.py,以 hardlink 與 scripts/ 同檔,不會走鐘);② find_gateway.py 的完整原始碼直接印在文件上,可整段貼進終端機執行,不必先下載;③ 移除兩支腳本裡寫死的出廠預設 token —— 它們要發佈到公開文件站,而本文件自己的規矩就是憑證一律用佔位符;改為 --token 或 $TOK,沒帶就明確報錯(find_gateway 沒帶 token 仍可列出 IP,只是讀不到型號/UID)。④ 掃描加上重試 —— 設備單執行緒,連續掃兩輪時會拒連,一次失敗就判定「沒這台」會漏掉真的存在的機器(實測踩到)。
2026-09-01 3.1 固定工作目錄 + 跨平台 + 一個實際踩到的坑。① 所有指令改為在 ~/opta(Windows %USERPROFILE%\opta)下執行,附兩個平台各自的建目錄與下載指令;註明 Windows 的 curl 是Invoke-WebRequest 別名要打 curl.exe、Python 指令通常是 python 而非 python3。② export TOK 從執行指令中拆出來獨立成段(業主指出混在同一段容易第一次就沒生效)。③ 新增併發警告:自動驗收執行期間不要掃描 —— 設備單執行緒、只有 4 個 socket,掃描會回報「找到 0 台」看起來像設備不見了,其實只是忙。這不是假設,是當天實際發生的:業主掃不到的當下,背景正在跑 28 項驗收。④ 掃描器改為兩階段(並發掃埠 + 逐台溫和識別),不再用 64 條執行緒同時打一台只有 4 個 socket 的設備。
2026-09-01 3.2 把「要裝什麼」提到最前面。原本的順序是先叫人執行、後面才講環境準備,照著讀會在第一個指令就卡住。入口新增〈準備 — 三個工具、一個目錄、一個 token〉,並釐清一件容易勸退人的事:人工驗收只需要 python3 / curl / mosquitto 三個,node 與 playwright 是出廠閘門才要的、而且需要專案原始碼(現場通常沒有)——原本的環境準備把兩者混在一起。另附兩個平台各自的安裝指令與自我檢查一行指令,四個都打勾再往下走,不要到現場才發現少東西。
2026-09-01 3.3 把只能在 bash 跑的監看指令改成跨平台。業主指出 watch -n 5 … 沒有 Windows 版 ——而 watch、while :; do … done、snap(){ … } 在 Windows 上都不存在。與其維護兩套 shell 方言(遲早走鐘),新增 scripts/watch_device.py(同樣發佈到文件站可下載):diag 看重開原因、io 看實體 I/O、snapshot 存設定快照、diff 比對兩份快照 ——兩個平台同一個指令,diff 也是內建的(Windows 沒有 diff 指令)。寫的時候踩到一個自己挖的坑:第一版把 uptime 放進「變化判斷」,而它每秒都在動,「只在變化時印」形同虛設 —— 已改成判斷用的鍵與顯示用的字串分離。
2026-09-01 3.4 補上流程裡漏掉的一步:設定 DUT。業主照著文件跑到 watch_device.py diag 就卡住 ——find_gateway.py 告訴你 IP 之後,文件從來沒叫你把它設成環境變數,而後面的指令全都靠 $DUT。已在找到 IP 之後補上兩個平台的 export DUT/UID_DEV/PREFIX,並附一行「確認三個都設好了」的檢查;第 0 步的指令也改用 $DUT 與前面一致。同時把 watch_device.py 缺變數時的錯誤訊息改成可直接照做:只列真正缺的那個、兩個平台的設定寫法都給、並指出「不知道 IP 就先跑 find_gateway.py」。
2026-09-01 3.5 把四個環境變數講清楚,並附範例。業主反映「DUT 是什麼?UID_DEV 我也不確定」——那是我理所當然用了、對讀者卻沒意義的行話(DUT = Device Under Test)。新增:① 一張圖把掃描輸出與變數對起來(箭頭直接指「這個 IP → DUT」);② 四個變數的意思、範例值、來源對照表,並註明 PREFIX 的陷阱(設定頁顯示 mes/gateway/001,實際收發用 24 碼 UID);③ 檢查指令重寫 —— 舊版沒設定時只印出空白、一眼看過去像正常的,現在明白寫「❌ 未設定」,TOK 只印長度不外洩;④ 附「看起來要像這樣」的預期輸出。
2026-09-01 3.6 韌體基線 → 6.0.89:項目 ⑥ 的兩個缺陷已修。人工驗收拔網路線時發現:設備確實切到 WiFi、網頁打得開、標記也正確,但送不出任何資料(MQTT/雲端/NTP 全連不上);而插回網路線,AUTO 開機落到 WiFi 那條路 2.5 分鐘零反應。真兇是同一行 —— failover 為了偵測插線而初始化 Ethernet PHY,同時建出第二張沒有 link 的 netif 並成為預設介面,搶走跨網段繞送;同網段一直通(直接 ARP 不需閘道),所以參數查起來全對、完全看不出問題(WI-212/WI-213)。修法是初始化後 set_as_default() 把預設介面指回 WiFi,並補到 AUTO-on-WiFi 那條路。四條路徑實機驗證全過。另新增 RG.9 守住這一類:既有 43 項閘門全在區網內發請求,「設備連不到網際網路」一項都不會紅。
2026-09-01 3.7 把固定值直接寫進環境設定,只留密碼當佔位符。業主指出 broker 主機與帳號本來就是固定的(而且整合合約、變更紀錄裡到處都有),沒必要讓人自己填。mqenv 兩個平台版本的 broker 主機 mosquitto.smms.com.tw、埠 8883、帳號 smms、雲端網址 opta.smms.com.tw 全部填實,只有密碼與雲端 token 保持 <...>;開頭的佔位符政策同步改寫,免得自相矛盾。順帶把 PowerShell 版的 mosquitto_sub/pub 改成 .exe(與前面 curl.exe 的處理一致)。少填四個欄位,就少四個出錯的機會。
2026-09-01 3.8 講清楚「設備」與「雲端」是兩個不同的系統。業主反映環境設定裡的雲端那一段看不懂是什麼。原文只寫「版本歷史/備份驗收要用」,沒說它是什麼、什麼時候用、為什麼驗收要碰它。新增對照表(是什麼/住哪裡/做什麼/哪幾步會用),並說明為什麼驗收需要雲端:階段 4 驗的不是「有沒有備份」而是「備份讀不讀得回來」——設備回「推送成功」只代表已排程。另標明最常見的錯誤:拿設備 token 去打雲端,症狀是「推送成功、查歷史 Unauthorized」。
2026-09-01 3.9 首次在「無線」環境完整重跑三套後補寫。 當晚網路線被移到 8310,8320 落到 WiFi 上,兩個一直潛伏的假紅才第一次現形 —— 都在測試裡,韌體沒有回歸(WI-214 出廠閘門 2B 固定等 4 秒、但同一頁在 WiFi 上要 57 秒才畫完;WI-215 RG.9 因 /api/net/diag 在 inbound handler 裡再開 outbound、設備只有 4 個 socket,套件連跑時借不到而回 -1)。① 排錯速查新增「重開原因怎麼讀」對照表,把 api-reboot/ota/net-failover/net-recovery/cs-wedge/mqtt-resub/UNEXPECTED 逐項寫明要不要緊 —— 重點是 cs-wedge 是對外連線卡死時的自救,不是當機(實測 WiFi 上對外 TLS 要 3.86.0 秒,訊號弱的站台容易踩到)。② 修好排錯速查表尾端兩列脫離表格、在現場會渲染成裸文字的格式錯誤。③ 記下一條教訓:測試套件在單一環境全綠,不代表它在交付環境會綠。
2026-09-02 3.10 韌體基線 → 6.0.90(WI-216/WI-217,皆無功能變更)。順帶記下一條現場會踩到的事:OTA 之後不要馬上開始驗收。這次 OTA 後設備在 WiFi 上花了 605 秒才把 MQTT 接起來,而在那之前每一次重連失敗都會把主迴圈卡住 18.4 秒 —— 設備是單執行緒,那段時間網頁根本沒在服務,打什麼都會斷線或超時。看起來像設備壞了,其實只是還在暖機。判準:輪詢設備狀態頁的 MQTT 顯示「已連線」再開始,不要用「重開後等兩分鐘」這種固定等待(已開 WI-218 追測試端的同一個假設)。
2026-09-02 3.11 四處 watch_device.py 改成 macOS/Windows 頁籤。業主指出 ⑥ 那段「為什麼不是頁籤切換」—— 查了產生器才確認頁籤是自動的:docs-gen/app.js 會把緊鄰的 ```bash + ```powershell 兩個區塊合併成頁籤並依作業系統預選。那幾處只寫了一個 bash 區塊、用行內註解交代 Windows,所以沒有頁籤;而且提示框寫「兩個平台同一個指令」是錯的 —— 指令本身就差一個字(python3 / python),它自己的註解就寫著。已改成兩個相鄰區塊並更正該句,產生的 HTML 逐一核對過確實配成頁籤。⚠️ 盤點同時發現全檔還有約 15 個 bash 區塊沒有 Windows 版,而且不只是 python3 —— Windows 上 $TOK 不會展開,要寫 $env:TOK,那些區塊對 Windows 使用者是壞的。另立 WI-222 處理,不在本次範圍。
2026-09-02 3.12 R.2② 的指令在 macOS 上根本貼不動 —— 已修,並加了守門。業主照著網頁複製 R.2② 貼進終端機,得到 zsh: event not found: ),以為是自己複製不完整。不是 —— zsh(macOS 預設 shell)在互動模式會展開雙引號裡的 !;'(空!)' 雖然自己被單引號包著,卻整體落在 python3 -c " 的外層雙引號內,於是 !) 被當成歷史事件。關鍵是它在 bash script.sh 底下完全正常,只有真人貼進終端機才會炸,所以躲過了所有既有檢查。同一份文件另一處用外層單引號寫同樣的字串一直是安全的 —— 這不是「不能用驚嘆號」,是「不能讓它落在外層雙引號裡」。已把 3 處訊息改成 ⚠ 空的,並新增 scripts/check_doc_shell_safety.py(逐字追蹤引號狀態,含一紅一綠自我驗證);修正前對文件跑是 exit=1 抓到 2 處、修正後 exit=0,並實際用 zsh -is 把文件裡的原文跑過一次確認不再報錯。另補一句判準說明:② 的「上次推送」存在 RAM,設備一重開就變 NEVER,備份其實還在雲端 —— 要以 ③ 為準。
2026-09-02 3.13 開頭那份「今天從這裡開始」只叫人設 4 個變數,流程實際需要 6 個。業主走完 R.1、R.2①②(都成功)之後,R.2③ 丟出 JSONDecodeError: Expecting value: line 1 column 1 —— 真因是 CLOUD / CLOUD_TOK 沒設,網址變成沒有主機的字串、curl 回空,接手的 python3 就爆。錯誤訊息完全沒指向真因,看起來像雲端壞了。這兩個變數文件確實有定義,但在後面的「環境準備」章節;走開頭快速路徑的人到不了那裡。與 v3.4 修過的缺陷同一類(那次漏的是 DUT)。已把變數表擴成六個(標明前四個屬設備、後兩個屬雲端,並註明最常見錯誤是拿設備 token 打雲端)、兩個平台的 export 各補兩行(CLOUD 是公開固定值直接填實,只有 token 留佔位符)、檢查腳本改成六項且 CLOUD_TOK 比照 TOK 只印長度。
2026-09-02 3.14 R.2③ 印的東西回答不了它自己的判準。判準寫「看得到剛推的那一筆」,指令卻只印檔名 —— snapshot.1788326543752.v5.json 是 epoch 毫秒,人看不出是不是剛剛那筆,於是這一步實際只驗到「雲端有東西」(本來就有一堆舊快照),沒驗到「有我剛推的那筆」。同一份文件的 4.3 早就修過這個問題,註解白紙黑字寫著「時間戳是 epoch 毫秒,直接印出來人看不懂,一定要轉成本地時間」並且真的有轉 —— R.2③ 沒有跟上,是內部不一致。已比照 4.3 改寫(轉本地時間、limit=5→50 避免雜訊快照擠掉、相容 list/dict 兩種回應形狀),並補「看起來要像這樣」的預期輸出與更嚴的通過判準(最新那筆的時間要對得上按下推送的時間)。順帶修 PowerShell 版沒用 $env: 前綴的錯誤 —— 那條在 Windows 上本來就跑不出東西。
2026-09-02 3.15 更正 v3.14 我自己寫錯的判準。v3.14 把 R.2③ 的通過條件寫成「最新那筆快照的時間要對得上剛按下推送的時間」——那是錯的,只要沒改東西就必定假紅。查了雲端原始碼才確認:saveSnapshotIfChanged() 只在設定內容真的變了才建快照(剔除 checksum/crc32/timestamp/license.lastCheckAt 這些每次必變的記帳欄位後比對),這是 v2.3.3 刻意做的,否則每小時會長一筆雜訊快照。實測佐證:設備 15:48 推送回 OK HTTP 200、雲端 config.json 的 mtime 確實跟著更新,但因為內容與 13:22 那份相同,沒有新快照 —— 那是正確行為。正確判準改為看 summary 欄:它是「該筆快照的值 → 設備現況」的差異,顯示 無差異 才代表雲端持有的內容等於設備現況,那才是「備份是最新的」的證據;指令也一併加印 summary。
2026-09-06 3.16 真人照著文件走(業主複製貼上、Claude 只對答案)第一天的修正。① find_gateway.py 只認 MES I/O Gateway 首頁標題,對恢復原廠後的設備回報「找到 0 台」 —— 而 L2 會清掉 QSPI 網頁資產、設備只剩「資產救援」頁。文件叫人在這個狀態下掃描,工具卻剛好處理不了它,現場會以為機器掛了(這正是文件花一整段在避免的誤判)。腳本已改成兩種標題都認,並明白印出「設備本身正常、推一次 .mesb 就回來」;R.4 判準同步補上這段說明,並改成「用 §R.4 的 curl 確認設定全空」而非「開啟網頁看」—— 那個狀態根本打不開設定頁。② 〈🧰 現場沒有專案原始碼也能做〉標題寫「抓兩支腳本」,底下卻列三條 curl -O,已更正為三支。
2026-09-06 3.17 預期輸出把 token 長度寫死,等於在文件裡固化特定 token。原本寫「TOK 已設定(長度 9)」「CLOUD_TOK 已設定(長度 11)」—— 那兩個數字正好是出廠預設 smmsadmin 與舊雲端 token 的長度。token 一換,這份預期輸出就會去「確認」錯的值;而文件自己在變數表就寫著「出廠有預設值,正式交機前應該改掉」,自相矛盾。(2026-09-06 走查實際踩到:雲端 token 已於 09-02 換成新值,業主 shell 裡還是舊的,而檢查腳本印出「長度 11」——完全符合文件的預期輸出,所以看起來是通過的。)已改成 長度 N / 長度 M,並補一段說明:長度只證明「不是空的」,不能拿來核對 token 對不對;錯的 token 會在第一次打雲端時以 401 現形。
2026-09-06 3.18 文件給了一個做不到的指示 —— 救援頁不吃 .mesb。階段 R 原本寫「設定回來了、畫面還是救援頁 —— 這時要另外推一次 .mesb(救援頁上就有上傳鈕)」。實際查證救援頁:<input type="file" multiple accept=".gz">、按鈕「安裝資產」、打的是 POST /api/web?name=<檔名>(韌體 handleWebUpload,WI-124)—— 收的是個別 .gz 網頁資產,一次一個檔,不是 .mesb 套件。那句話把救援頁與 §2C 的拓荒頁搞混了,現場照做會卡死而且沒有第二條路。已改成兩條可行做法的對照表(雲端部署 .mesb 為首選、救援頁逐檔安裝 .gz 為離線備案),並在 §2C 開頭補上「你是從哪條路來的」分流表 —— 從階段 R 過來的人看到的是救援頁,2C.2 的拓荒頁步驟對他不適用。
2026-09-06 3.19 v3.18 我寫的「雲端部署 .mesb(建議)」對這個情況根本走不通 —— 已更正。恢復原廠後韌體版本沒變(被清的是網頁資產),而雲端心跳有一道同版守衛(WI-125 Inc7):設備回報版本 == 佇列版本就 delete otaByUid[uid] 不下發。業主實際按下部署,API 回 200、對話框寫「2 分鐘後自動更新」,而 21 秒後 log 出現 ota.complete (no re-deliver) —— 三處都沒有任何錯誤訊息。已在 cs 2.5.3 修:部署支援 reinstall、同版未勾重裝時當場回覆說明、後台加「重新安裝(版本相同也強制推)」勾選、且失敗不再靜默關窗。文件同步標明務必勾重新安裝。
2026-09-06 3.20 排錯速查新增最常見的那一則錯誤:JSONDecodeError: Expecting value: line 1 column 1。業主同一天看到它兩次、原因完全不同:一次是 CLOUD/CLOUD_TOK 沒設(網址變成沒有主機的字串),一次是設備剛 OTA 重開、忙碌閃斷。它的意思永遠是「curl 吐出空字串、python3 收到空的」,要往上游查 curl 為什麼是空的,而不是查 python。已寫成兩因對照表(如何分辨、如何處理),並補上「開跑前先輪詢 mqttConnected 等設備就緒」的等待迴圈 —— 本文件的指令都沒有重試,設備閃斷一次就會吐出一整頁 traceback,看起來像雲端壞了。
2026-09-06 3.21 v3.15 修好 R.2③ 的判準卻沒有掃全庫 —— 階段 4.3 還留著同一個錯的判準。4.3 的表格寫「列出快照清單,看得到剛推的那一筆」,但雲端只在內容真的變了才建快照,沒改東西時推送照樣成功卻不會有新快照 —— 這條判準在最常見的情況下必定假紅。諷刺的是同一段下方的警告早就寫明了去重這件事,只有上面那張表沒跟上,而且它與 R.2③ 用了兩套不同方法驗同一件事。已統一:4.3 判準改成 A(先改設定再推,必須出現新快照 —— 較強)/ B(什麼都不改,看最新那筆 summary 是否 無差異)兩種擇一,指令一併加印 summary;R.2③ 的註解標題也同步改掉。教訓:修好一處判準要立刻掃同型的其他處。
2026-09-06 3.22 掃描結果的 parity 是平台常數,與設定裡同名欄位不同義 —— 已註明。走查時掃到 {"slaveId":1,...,"parity":1043,...},1043 看起來像沒初始化的記憶體。查證後確認是 Arduino 的 SERIAL_8N1(0x413):掃描結果的 parity 存平台常數,而設定裡同名的 parity 是 0=None/1=Even/2=Odd,韌體內部自己都要翻譯。文件原本的範例回應還把這個欄位整個省略掉,現場看到會愣住,更糟的是可能把 1043 抄進設定欄位。已把完整範例與這段說明補進 5.1。
2026-09-06 3.23 補上三處「文件叫你做、卻沒給指令」的缺口 —— 業主指出的方法問題。走查時我有幾條指令是自己編給業主的,而不是從文件複製 —— 那只驗到「我會下指令」,客戶照文件做時根本做不到。業主原話:「這些手動測試要能跟文件搭配,不然將來在文件上都無法實現。」已補:① 4.3 的 A 做法(文件只寫「先做一個實際的設定變更」卻沒給指令)—— 補上改名/推送指令,並加上驗完必須改回去(否則就是 WI-223 那個測試殘留自我延續的同一個病);② 5.3 要跑兩次(「值是 0」與「根本沒讀到」長得一模一樣,真實量測會跳動);③ 抓完腳本要確認是新版(grep boot_guard / grep 資產救援)—— 業主當天就抓到舊版。方法上的規則也記下來:走查時若發現自己在編指令,那本身就是一個發現,要先補進文件再跑。
2026-09-07 3.24 5.3 的兩條讀值指令沒有 Windows 版 —— 而它是「唯一可信的判準」。走查前先靜態審查下午要走的段落(階段 R / 5 / 6、情境 A、階段 7),發現這兩條缺 PowerShell 版:Windows 使用者走到最關鍵的那一步會無command可用。已補上,並順手把外層雙引號改成單引號 ——原本的寫法在 zsh 底下雖然沒有 ! 不會炸,但巢狀跳脫(\\")很脆弱、複製時容易被編輯器改壞。同時完成的靜態審查(無缺陷):情境 A 引用的 14 個 UI 字串在目前的網頁資產中全部存在;「觸發類型」下拉的 value 與韌體 TriggerType 列舉完全對得上(0/1/2/3 ↔ ALWAYS/RISING/FALLING/CHANGE)—— 沒有 WI-206 那一類的錯位。
2026-09-07 3.25 真人走階段 1(開荒路線)當場抓到三個,其中一個差點燒錯機器。① 🛑 board list 只說「記下它的 Port」——單數。桌上兩台 Opta 都接著 USB 時,兩行都寫「Opta」,文件沒有任何辦法分辨哪個 Port 是哪一台,燒錯就是把另一台清掉。解法其實一直都在手邊:USB 的 serialNumber 就是設備 UID($UID_DEV,第一步就設好了)——已補上會自動標出「← 這台」的指令(雙平台),並寫明「沒有任何一行標到就停下來,不要猜」。② 1.1 的 core update-index 需要連到 downloads.arduino.cc,現場出向受限會逾時(走查實際看到);已裝過 core 的機器不受影響,乾淨筆電會卡死 → 補上「出發前先在辦公室裝好」。③ 1.2 的通過判準「大小合理(數百 KB)」不足以分辨 —— 兩顆不同的拓荒包大小都在數百 KB;改為比對 sha256 與新發佈的 manifest(bootstrapVersion / sha256 / buildDate)。韌體基線 → 6.0.91,並新增拓荒包版號 1.0.0(獨立版號線,不綁正式版)。
2026-09-08 3.26 階段 1 只做了 L3 的前半段 —— 燒完拓荒包看不出燒過。USB 燒錄(DFU)只換韌體、不碰 QSPI,而韌體的邏輯是「QSPI 上有主畫面就服務它」。走查實測:燒完拓荒包,打開網頁還是完整的五個頁籤,現場完全看不出韌體換了 ——查心跳才發現雲端回報 firmwareVariant: pioneer。文件自己在〈階段 R〉定義了 L3 = USB 重燒 + 再做一次 L2,但階段 1 從沒提到要補那一步,兩段是斷的。已補:① 階段 1 開頭加 🛑 說明「動過的機器燒完還要再清一次」並附指令;② 新增 1.5 確認你真的在拓荒包上 —— 判準是開荒頁的「拓荒包版本」或雲端的 firmwareVariant: pioneer,並明寫 不要用韌體版本判斷(拓荒包與完整版是同一份 FW_VERSION 建的,version 一模一樣),另附不受 QSPI 影響的 /rescue 入口。
2026-09-08 3.27 新增〈階段 2D:授權啟用〉—— 文件提了 4 次「授權憑證會被清」,卻沒有裝回去的步驟。啟用授權 在整份文件出現 0 次,而 L2 是文件自己說的「交機/轉售標準做法」——照著走完會交出一台未授權的機器。走查實測釐清了機制:雲端 /api/license/token/<UID> 對這台回 status: active + 156 字元簽章憑證(expiresAt: 0 永久),設備歸零後會自己取回、隨即恢復 ACTIVE —— 畫面上的「裝置尚未授權」只是取回前的暫態。但全新設備沒有那筆核准紀錄,必須走「按啟用授權 → 管理員核准 → 設備自動取件」。已把兩種情況寫成分流表,並明寫判準是 GET /api/license 的 status,不是畫面。
2026-09-08 3.28 1.4 的範例 Port 長得太像真值,是個陷阱。原本寫死 -p /dev/cu.usbmodem212201,而實測那台是 usbmodem112201 —— 只差一個字元。整段複製而忘了換 Port,在兩台 Opta 都接著 USB 時會把另一台的韌體清掉(見 1.3 的 🛑)。已改成 <你的PORT> 佔位符並加註「刻意不放看起來像真的的範例值」,同時補上成功時的預期輸出(File downloaded successfully + bytes 數要與 1.2 下載的檔案一致)。業主自己問「這段指令好像沒在文件上?」才發現的 —— 它在,但範例值本身就是風險。
2026-09-09 3.40 補上 7.4「設備和網路設備一起斷電」—— 最接近現場、而且以前從來沒測過。原本的 7.1 只切設備自己的電源,那時網路是好的;但現場停電是整廠一起停,分享器也會關。設備 20 幾秒就開好,分享器要 60~120 秒 —— 設備開機時網路根本還沒好。這件事在 2026-09-09 之前是壞的(WI-236):一台設備因此離線 18.4 小時而沒有人發現,因為它有電、燈在閃、雲端後台也顯示上線中,只有真的去打開網頁才會發現進不去。已於韌體 6.0.94 修正。新增的 7.4 寫了實體操作步驟(同一條延長線、關 10 秒、等 5 分鐘不要碰)與三項檢查,並明列兩種「沒過」的樣子 —— 其中「雲端顯示上線但網頁打不開」不會出現在任何監控指標上,唯一的檢查方式就是真的去打開網頁。另註明:8310 沒有 Wi-Fi 備援,現場只要有 8310 這一項就必做。
2026-09-08 3.39 模板寫錯來源編號會靜默送 0 —— 交機前整備時當場踩到。${rs485.N.欄位} 的 N 是 Modbus Slave ID,不是清單上的第幾台;韌體找不到就 return 0.0f(ConverterEngine.h 的 resolveRs485Field)——沒有錯誤、沒有 log、照樣每 5 秒準時發,上位系統收到一串漂亮的 0,看起來就像機台在待機。實測:把 slave 1 的電表寫成 ${rs485.0.…} → 每 5 秒發 {"電壓":0.0,"電流":0.00,"功率":0.0},而同時 /api/poll 讀到 112.6V / 0.30A;改成 ${rs485.1.…} 立刻正常。已加判準:設好模板要訂閱看實際 payload,拿它跟 /api/poll 對一次 ——「有訊息進來」不算過,值要對得上。並補上可用欄位清單、:N 小數位(兼死區)、以及「來源離線時整筆不發」(v6.0.6,刻意的,免得送 0 讓 MES 誤判計數歸零)。
2026-09-08 3.38 新增〈情境 U:網頁設定流程〉與〈B3b:兩個通道並排對照〉,都是實機驗證過的。① 情境 U —— 驗收現場業主看的是畫面,不是 curl,但文件前面全是指令。新增一整段全程在網頁上做的流程(命名 → 輸入群組 → 輸出群組 → 規則 → 現場示範 → 重開再對 → 推雲端),每一步的判準統一是**「在網頁上做 → 立刻打 API 對照 → 相符才算過」;並寫進三條現場紀律(只開一個分頁、一次存一筆等清單刷新、每步存完重新整理再看)。U.4 的指令會同時讀 rules/signals/actions 交叉比對**,抓得到四種畫面上完全看不出來的壞法 —— 其中懸空參照(群組被刪、規則還指著它)韌體是 fail-safe 當「關」、不會報錯。觸發類型會把韌體的數字翻成名稱(src/config.h:111),因為畫面的下拉曾經出現過選項對錯列舉的缺陷(WI-206)。② B3b —— B2/B3 是先後比較(取消→觀察→勾回→觀察),中間隔了時間,「這一分鐘值有沒有變」不受控;業主若問「會不會只是剛好沒變?」答不出來。B3b 用兩個通道並排跑:同一台、同一時間、同一模板,唯一差別是那個勾勾。實測(v6.0.91,.77,90 秒):沒勾 18 則 / 勾了 2 則,訊息量差 89%;前 45 秒值沒動時沒勾的發了 8 則重複、勾了的一則都沒有。同一輪還量到變化後 3 秒內就送出(證明它不是啞掉)。附韌體機制說明(FNV-1a 對整包 payload 取雜湊、不發也更新節流時間、閘門上升沿重置雜湊)。③ 過程中發現 converterTemplateLen: 0 的 Converter 會靜默不發(每個表面指標都是綠的)→ 開卡 WI-230。
2026-09-08 3.37 6.3 的命名指令掉了兩條而完全看不出來 —— 業主走查當場發生。三條 curl -s 連發,終端只回一個 {"success":true};查設備才確認擴充那條根本沒寫進去(expansions[0].doNames 還是 Exp0_O1…)。兩個實質缺陷:① 三個一模一樣、沒有標籤的回應,掉一個你也分不出是哪一條;② -s 連「連不上」都吞掉(與同日 6.2 把繼電器留在動作狀態是同一個病 —— 靜音的關鍵步驟)。已改成 setname 小函式:每行印出自己是哪一路、用 -sS(安靜但錯誤照報)、每條間隔 2 秒(設備單執行緒,連發容易被丟)。③ 讀回完全沒有涵蓋擴充通道 —— 你把擴充命名完,那個「還沒命名的」清單照樣顯示乾淨。已改成一併列出 expansions[*].doNames 並用同一組預設值樣式(含 Exp0_O1 這類)判斷。
2026-09-08 3.37 6.3 的命名指令掉了兩條而完全看不出來 —— 業主走查當場發生。三條 curl -s 連發,終端只回一個 {"success":true};查設備才確認擴充那條根本沒寫進去。① 三個一模一樣、沒有標籤的回應,掉一個你分不出是哪一條;② -s 連「連不上」都吞掉(與同日 6.2 把繼電器留在動作狀態同一個病:靜音的關鍵步驟)。已改成 setname 小函式 —— 每行印出自己是哪一路、用 -sS、每條間隔 2 秒。③ 讀回完全沒涵蓋擴充通道,把擴充命名完那個「還沒命名」清單照樣顯示乾淨。已補,但補完發現 ④ /api/config 的 expansions 永遠回 5 個槽(那是「最多支援 5 槽」的容器,每槽都預填 Exp0_O1…Exp4_O8),直接照它數會得到一百多條假的「還沒命名」 —— 實際插了哪幾槽只有 /api/io 的 expDo 哪幾個不是 null 才知道,所以讀回改成同時讀兩個端點(實測從 130 條變成 17 條)。⑤ 最關鍵的一條:擴充輸出的命名與驅動用【兩套不同編號】 —— 命名是 {"exp":0,"index":2},驅動是 {"index":18}(16+ch,且 4-15 是空的)。這正是「偏一路 / 偏兩路」誤會的根源。已列表寫明,並附逐路點燈核對的指令(對空槽會明確回 expansion module not connected,不會靜默成功)。
2026-09-08 3.36 6.2 少了一個安全項:測完沒有人確認輸出「真的放掉了」。走查當場踩到 —— 驗證期間刻意製造並發壓力,結果「放掉」那次 POST 被擠掉,而指令輸出被導去 /dev/null,完全看不出失敗,繼電器就這樣留在動作狀態。現場那可能是一台還在轉的機器。已加第 ④ 個證據:離開前讀 /api/io 確認 do 全部為 False。教訓:把輸出丟掉的指令,等於放棄知道它有沒有成功 —— 關鍵步驟不要靜音。 另修 watch_device.py:① 讀取失敗不再被當成「狀態變化」(舊版一次掉線會印兩行 —— 失敗一行、恢復後又把沒變化的狀態再印一行,破壞了「只在變化時印」);② 連續失敗只警告一次;③ 把 0.3 秒預設的代價寫進註解(單執行緒 4 socket,等於 1 秒的三倍負載,偶發「讀取失敗」是被擠掉不是故障,可用 --every 放慢但就抓不到脈衝)。
2026-09-08 3.35 漏了「幫 I/O 通道命名」這一步 —— 業主走查時當場問出來的。文件從階段 6(戳一下實體 I/O)直接跳到情境 A(建規則),中間沒有任何一步叫人命名通道。但情境 A 的「輸入群組」「輸出群組」兩個下拉,列出來的就是這些名字 —— 沒命名的話下拉裡是一排 I1…I8 / DO 1…DO 4,認不出哪一路接了什麼,選錯畫面也不報錯,規則建好卻驅動到隔壁那路。而且 L2 恢復原廠會把名字全清回預設(實測歸零後正是如此),交機前不重新命名等於交出一台「看不懂自己接了什麼」的機器。已新增 6.3:網頁與指令兩種做法、擴充模組要帶 "exp":N、type 只吃 INPUT/DO(不是 OUTPUT)、打錯會回 HTTP 200 但 success:false(不能只看狀態碼)、讀回確認並列出還沒命名的通道,以及「擴充通道名容易偏一路,命名完要實際驅動一次核對」。另:/api/io-name 這個端點在 4.3 就被用到了,卻要到這裡才有說明 —— 順序問題一併補上。
2026-09-08 3.34 階段 6 兩步都沒有真正的判準。① 6.1 的判準要求「現場觸發輸入時對應位置會變」,給的指令卻只印一張快照 —— 從一張快照看不出會不會變。已改成指向 watch_device.py io(持續監看、只在變化時印),並把走查踩過的三個坑寫進去:監看主機 DO 卻按了綁在擴充 DO 的按鈕、1 秒輪詢漏掉觸控鈕的脈衝、把 expDo 的插槽當成通道(插槽是常數,永遠不會變)。② 6.2 根本沒有通過判準 —— 只說「先確認硬體會動」,但指令回的 {"success":true} 只代表 API 收到了;而且只亮 2 秒,人很容易錯過。已改成「設起來 → 讀回 /api/io 確認 do[2] 變 True → 看/聽現場 → 放掉」,並明列三個證據各自證明什麼(前兩個綠、第三個沒有 = 接線或繼電器問題,不是軟體)。實測 /api/io 確實會反映被驅動的 DO —— 這個現成的判準文件一直沒用上。③ 更糟的是:文件叫人別踩的三個坑,發下去的儀器自己三個都踩。 watch_device.py io 舊版只看 di[:4](主機有 8 路)、只看主機 do(完全沒有擴充)、輪詢寫死 1.0 秒(連 --every 都不理)—— 所以按了綁在擴充 DO 的燈會「完全不動」,觸控鈕的脈衝會整個錯過。已改成主機 8 DI / 4 DO + 擴充巢狀通道全部納入變化判斷、預設 0.3 秒。實測翻轉 DO3 四次,四次全抓到。儀器看不到,不等於它沒動。
2026-09-08 3.33 階段 5 三處與實機不符,全部是走查當場踩到的。① 5.1 的輪詢指令自己會爆 —— 文件才剛說「掃描期間設備會拒連,那是正常的」,下一行給的指令卻直接把空回應餵給 python3 -m json.tool,噴出一長串 traceback。走查連吃兩次,看起來像設備壞了。已改成會擋空回應、印「還在掃描(正常)」並自動偵測結束的迴圈(bash + PowerShell 都補)。② 5.3 的「數值合理」教不出真正該看的東西 —— 實測 Finder activePower=-5.18、powerFactor=-0.15,有功功率為負 = CT 夾反方向,而線通、online=True、十二個數字全有值、還會跳動,表面指標全部正常。已補成明確判準與修法(CT 轉 180°)。③ 欄位名是 activePower 不是 power —— power 這個鍵根本不存在,讀到永遠 None,會誤判成讀不到功率;已列出完整欄位。另補:設備單執行緒 4 socket,網頁開著再用 curl 打會間歇拿到空回應,那不是故障。
2026-09-08 3.32 5.1 的掃描參數寫錯,而且錯的方向會害人提早放棄。文件寫「逐一試 9600/19200/4800/2400」(4 種),但韌體 PowerMonitor.h:542 是 7 種(115200/57600/38400/19200/9600/4800/2400)× 3 種同位元 = 22 組;同一段下面的警語卻又寫「7 種鮑率」—— 文件自己前後矛盾。照 4 種去估時間會覺得「怎麼掃這麼久」而中途放棄。已改成 7 種並寫明共 22 組,同時補上「現場設定那組排第一個先試,所以現場設備通常很快出現,不必等全部跑完」。
2026-09-08 3.31 4.3 A 路徑的清理只清了一半 —— 雲端還留著測試殘留。v3.23 加的「驗完一定要改回去」只改設備的通道名,而剛才那次推送已經把 AC-4.3-TEST 送上雲端了;改回去之後沒再推一次,雲端手上那份就永遠停在髒的版本 —— 日後從雲端還原會把它帶回設備。已在「改回去」後面補上「再推一次 + 讀回確認雲端已清乾淨」。與 WI-223 同型,而且這次是文件自己製造的殘留。通則:為了驗證而做的變更,清理範圍要涵蓋所有被它污染的地方,不是只清你當初動手的那一個。
2026-09-08 3.30 階段 4 的判準會製造假 PASS,實機走查當場踩到。歸零後 cloudToken 是空的(L2 清掉),走 A 路徑改名推送 —— 設備 /api/configserver/push 回 {"success":true}、push.cloud 顯示 OK HTTP 200,但雲端根本沒收到(實測:空 token 打雲端一律 401)。① 設備自報的推送結果失敗時不會更新,讀到的是上一次成功的舊紀錄 —— 已加警語:判定備份成功唯一可信的證據在雲端側(WI-228)。② A 路徑沒出現新快照時,B 路徑的「無差異」會被誤讀成通過 —— 那筆是更早的舊快照。已明寫「走了 A 就只能用 A 的判準」。③ 補上 A 的最硬判準:直接讀雲端手上那份 config 看改的名字在不在,不受快照去重影響。
2026-09-08 3.29 修掉我自己在 v3.21 造成的表格斷裂,並補上 4.1 缺的指令。① v3.21 把「A 做法的指令」整塊插在 4.3 判準表的 A 列與 B 列中間,程式碼區塊把表格切斷 —— 產生的 HTML 裡 B 那一列變成 <p> 裸文字、連 `

#2026-09-11 v3.45 同步紀錄

對齊出廠 v3.9/候選 6.0.100,修正有效雲端身份、queued 判準及清理後最終備份;補純數字 Phase 對照與部分覆蓋標示。規範同步不等於完整驗收已通過。

#2026-09-12 v3.46 同步紀錄

驗收 v3.46/出廠 v3.10 同步補記正式雲端 2.5.8 的實測狀態與剩餘門檻;候選規範基線仍為 6.0.100,8320 的 6.0.103 為診斷版。測項 ID 與對應不變;補上 backup 整輪重開數守門,清理失敗與診斷缺樣都不放行。

#2026-09-12 v3.47 同步紀錄

驗收 v3.47/出廠 v3.11 同步明列各套件的版本與證據範圍,撤除未標版本的回歸全數通過標題;階段 7/H9/H10 補上切斷 USB 與獨立 DC 等所有供電。韌體規範基線仍為 6.0.100,未將診斷版 111 或單輪 41 項通過提升為整機放行;測項 ID 不變。

#2026-09-12 v3.48 同步紀錄

驗收v3.48/出廠v3.12同步WI-246的單次補發訂閱前置與失敗分類。出廠CV.4使用MQTT客戶端實際SUBACK,未放寬12秒/至少1則判準;驗收B6固定等待的證據限制明列。測項ID與6.0.100規範基線不變,111的實機結果另記。

#2026-10-01 v3.49 同步紀錄

新增 SC.1~7、TS.1~7、NR.1 操作與簽核條件;出廠 v3.13 同名一對一承接。新版流程不冒充既有自動執行器涵蓋。

#附錄:本輪與歷史測試紀錄

以下供工程人員追查;不需要先讀完才能照前面的步驟操作。

#歷史:2026-10-01 出發前候選補充(6.0.122)

下方 2026-09 的版本與逐輪紀錄保留為歷史;本輪候選為 6.0.122,並非 1.0 正式放行。雙機全套最終 .46 72 PASS、.250 70 PASS/2 硬體 SKIP(含保留首輪失敗的明確補跑);acceptance .46 29 PASS/2 原接線 WARN,.250 18 PASS/5 硬體 SKIP。

本次重點為 磅秤零點+3~5 段 與 MQTT 原始量測時間。缺實體磅秤的精度/實際 RS485 校正仍需現場驗收;不能把邏輯故障模擬當成實秤 PASS。整組斷電且路由器延後啟動已驗雙機自動恢復與 186 秒穩定觀察;未做逐路接點/獨立 DUT 斷電,也未量精準全滅時間。本次整組斷電不能替代下方 H9/H10 全部條件。

新驗收應檢查量測時間屬於原樣本、未知時間為 null、寫入失敗不誤亮完成燈,以及復電後先 reset 再重新標零。原測項 ID 與歷史證據不改寫。

#1.0 候選的驗證與文件同步規則

驗收流程確認現場使用情境;出廠測試另涵蓋機型、硬體、授權、升級、故障恢復及出貨清理。出廠應包含驗收的產品行為;現場接地、照片與業主簽名保留現場執行。

  • 每輪記錄 commit、韌體與網頁資產版本、設備 UID/機型、實際網路介面、治具、起訖時間與 bootCount。文件版號表示規範修訂,不表示該版全部實測通過。
  • BK.0–BK.3 整輪另守重開數:還原排程前 bootCount 必須維持起始值;取得有效還原工作後,收尾必須恰好增加一次且為 SOFTWARE/api-reboot。缺少診斷、額外重開或非預期原因一律 FAIL;清理失敗時仍要檢查重開,兩種錯誤都保留。
  • BK.2 等待雲端 ACK 最多六分鐘(另受整項十二分鐘上限約束),期間只輪詢雲端。此測試預算考量主迴圈 127 秒心跳排程、工作執行緒 60 秒喚醒,以及靜默延後與連線時間;工作 ID 必須相同且狀態為 applied,逾時仍 FAIL。
  • PASS、FAIL、WARN、SKIP、未測、受阻分開記錄。缺治具的 SKIP 不能算硬體已通過;刻意略過重開不能算持久化已通過。讀不到 bootCount 時不得宣告整輪通過。
  • 各套件的 PASS 必須附當輪韌體、Build ID、設備與報告;舊版回歸結果不能沿用為新候選通過。test:factory 的 41 項不含獨立 regression/converter/backup/persist、完整驗收執行器或人工測項;長時觀察另記實際時長,不以短輪代替。
  • 單次補發先建立接收端訂閱,再觸發開閘;出廠 CV.4 等實際 SUBACK 成功後才觸發,保持原12秒觀察與至少1則判準。接收端拒訂、斷線或確認逾時列測試失敗,不能當作有效的0則窗口;驗收 B6 既有4秒等待只屬預備時間,若收不到訊息,須區分訂閱未就緒與產品漏發。
  • 未接 RS485 測試目標的工站,驗收執行器須明確加 --no-rs485;階段 5 記 SKIP,不能當成 RS485 已通過。出廠另以 EXPECT_RS485='' 指定無該治具。
  • 自動 6.2 選主機及每個已連線擴充中未被已啟用規則控制的 DO 各一路,記錄實際 index;測後即使發生錯誤也恢復原值。所有 DO 都被規則占用時須安排隔離治具,不能把規則持續覆寫當成輸出故障;全路實體接點仍須另驗。
  • 備份還原比對排除系統產生的版本、還原工作 ID、checksum、timestamp 與 license.lastCheckAt;身份本身、授權設定及模板內容仍須相符。
  • 日誌穩定性測項 RG.8 只計觀察基線後的新增紀錄,保持完整 300 秒;相同文字的新事件仍計入,不能把舊開機日誌重算,日誌銜接中斷或 bootCount 改變也不得通過。
  • HTTP 200/queued 只證明推送排程。4.3/8.1/BK.1 必須核對新送達狀態及雲端選定快照內容(含 Converter 模板);最終備份在 QA 素材清理與原設定核對之後執行。
  • 雲端身份以設備管理介面取得的有效身份為準,包含 license fallback。空 cloudToken 本身不是 FAIL;以本設備歷史讀取的實際 HTTP 結果判斷,不把 token 寫入公開紀錄。
  • 改測項時,同一筆變更更新驗收步驟、出廠對應及相關自動化;唯一 ID 對應來源為驗收單的「出廠測試對照」。執行 python3 scripts/test_coverage_matrix.py --write 後再以 --check 檢查。
  • 同步 docs/assets/acceptance_run.py 與文件站附件,重新建置文件站。靜態對照只證明編號與腳本關係;頁面測試只證明該套頁面斷言,均不能代替整份驗收或出廠簽核。

歷史限制(2026-09-09 候選):曾有 WI-156 虛擬通道引用錯位及計數器參數跨重開遺失的重現證據,該輪 D4/ST.16 不得簽 PASS。新候選應看相同 Build ID 的逐項重測,不能沿用舊版 PASS 或 FAIL;2026-10-01 本輪結果見本文開頭。

9/11 授權桌上驗收中,8320 在 6.0.100 發生 SOFTWARE/cs-wedge(392 → 393,未要求重開),WI-220 仍擋發布。正式雲端實讀為 2.5.5,8310 的 BK.2 因缺少候選 2.5.8 所要求的 restore.id 未過;不能把部分功能或名稱還原成功當成完整簽核。

9/12 正式 Config Server 已升到 2.5.8;8310/6.0.100 的 BK.0–BK.3 通過,驗收自動項為 18 PASS/0 FAIL/5 SKIP,另四組出廠自動項為 40 PASS/0 FAIL/1 SKIP。8320/Wi-Fi 曾在收到完整內容後拒絕套用;6.0.103 復測的備份/還原功能通過,但還原排定重開之外又多出一次 UNEXPECTED/webhdr,整輪穩定性不合格。WI-220 非預期重開、WI-156、實體斷電與長時門檻仍開啟;不得把部分自動項通過當作整機出廠通過。

#2026-10-01 v3.50:改成現場容易跟做的說法

操作路線放在最前面,技術與歷史紀錄移到後面;新增步驟逐一補上不符合時的處理。測項 ID、命令參數與通過標準不放寬。

#2026-10-05 v3.51:線上多段校正納入驗收

新增 OC.1~OC.12 與出廠一對一映射,包含 OTA 安全容量、累加公斤、ACK/重送、固定重量新樣本、中斷恢復、廣播隔離、Edge 資料用途、精度/斷電及長時/清理。保留 SC 舊流程與歷史結果。規範已補齊,WI-252 實機/發布仍受阻。

#6.0.128 毫秒時間驗收補充(WI-250)

沿用 TS.1~7,不以本補充取代原項目。TS.2 檢查 ${sys.timestamp} 訊息時間與 ${rs485.<站號>.<欄位>.timestamp} 量測時間:收到的 UTC 字串有三位小數(例如 .237Z);Edge 能完整解析並保存,不能固定只收 20 字元。保存至少三筆原始 MQTT payload,確認毫秒不是一律補 .000。未對時/無有效樣本仍為 null。

TS.4 的延遲/補送須逐位元組比對原 payload;TS.3/5/6 依原有時鐘修正、讀取失敗與 onChange 判準驗證,時間單獨改變不得觸發發送。跨秒、跨日、長時計數回繞與舊樣本不被重新對時改寫由時鐘/Converter/輪詢測試留證。毫秒解析度不代表 UTC 誤差小於 1 ms,不可在驗收表代簽該準確度。

#WI-253:32 個 MQTT 命令通道補充驗收(6.0.130 候選)

在隔離設備將測試群組綁定 cmd/signal/31,輸出使用測試 MQTT topic,不接實體 DO。等待接收端 SUBACK 後,分別發裸數字及 value JSON,核對規則/Converter 行為;另驗 0、7、8。32、負數與非數字尾碼不得改變既有命令值。

保存測試前設定及閘門值,設定 31 為非零,等待落地後正常重啟,核對映射與閘門恢復;真正斷电保存仍需獨立物理斷電測試。升級舊 8 筆保存檔時確認 0–7 保留、8–31 為零。測完回復群組/模板/閘門與原設定,確認無殘留。

主機測試不代替 MQTT 實機、重啟或物理斷電證據;本次不擴充輸入群組數。

#WI-259 靜態 IP 中斷復原補充驗收(1.1.6 候選)

此項需 USB 接在受測 OPTA,並先保存設定備份與連續 USB 日誌;一般 USB 可供救援指令,不是 RS485 轉接器。

  1. 記錄 UID、版本、Build ID、bootCount、原網路設定;每次測試只操作指定設備。
  2. 正確固定 IP:透過 net static <ip> <mask> <gateway> 套用,等首次重啟後從 Ethernet 開網頁確認;再次重啟仍保留設定。
  3. 錯誤閘道:使用隔離測試網段,不得占用其他設備 IP;持續 USB 錄影式日誌,測試期間不要對設備送 HTTP/API 或 net keep(這些會確認試用)。
  4. 連線未確認:連續 link-up 五分鐘後還原原設定;若試用途中另一次重啟,應在網路初始化前還原,不能每次重啟重算五分鐘而失聯。
  5. 核對日誌的實際重啟原因、檢查點及還原訊息,再讀回網路、MQTT、I/O、群組設定;不能只依 LED 或「沒有 fault record」宣稱正常。
  6. 正確 Ethernet 連線可確認;Wi-Fi 連線不應替未使用的 Ethernet 設定確認。儲存失敗不得宣稱已還原或刪除唯一備份。

結果需逐項附實機證據;主機測試或候選版建置成功不等於現場/出廠驗收完成。本補充不表示 1.1.6 已正式發布。

#WI-266 交易排程補充(2026-11-02)

  • DC.8:網頁輸入20、50、1000 ms並儲存重讀;拒絕19、1001、小數、空白;重開後仍保留。舊設定缺鍵回到50 ms。
  • DC.9:單次輪詢失敗後按原週期重試;連續第三次起退避;其他設備仍輪詢,MQTT繼續含null發送。
  • DC.10:容量提示區分設定週期、交易間隔、排程窗口及整輪估算;16台邏輯測試不代替16台實機。
  • DC.11:混合有效/失敗自訂暫存器,逐欄位null;資料年齡及失敗次數與原始API核對。

#MQTT 密碼邊界(WI-267/1.1.13)

  • 31 ASCII:測試/儲存可送出完整值;使用隔離 Broker或攔截請求,不覆寫現場正式憑證。
  • 32 ASCII、11個中文字、8個emoji:測試與儲存均提示超長,沒有POST。
  • 貼上40 ASCII:整次貼上拒絕並提示,不能先截成31後顯示成功;選取原值後貼上31 ASCII可完整替換。
  • 檢查三語提示皆清楚標示31 UTF-8 bytes;頁面無JavaScript錯誤。

#密碼眼睛與 Wi-Fi 長度(1.1.14)

  • 在Wi-Fi與MQTT輸入測試字串,實際點眼睛兩次,核對password→text→password、內容不變、aria-pressed與提示切換;不應送出任何設定。
  • Wi-Fi63ASCII、21中文符合上限;64ASCII、22中文、16emoji應拒絕。即時計數與aria-invalid一致。
  • Wi-Fi一般儲存、全部網路設定儲存、AP儲存並重啟都要在超長時零POST;測試需攔截有效請求,不可用假密碼改正式網路。
  • 重載後密碼輸入值為空且隱藏,已存密碼不由API取回;留空保留僅適用一般設定頁。

#1.1.14 出廠 Ethernet 直連驗收

  • 清空設定的新機與一般恢復原廠:無 DHCP 伺服器,電腦 192.168.50.10/24 直連,開啟 http://192.168.50.50。核對遮罩 /24、閘道 0.0.0.0,觀察至少 10 分鐘沒有閘道自救重啟。
  • 網頁改 DHCP/其他靜態 IP 後重啟,讀回儲存設定;再次升級不得恢復成出廠 IP。
  • USB net show、net dhcp、net static 192.168.50.50 255.255.255.0 0.0.0.0,核對試用確認與還原。
  • 2026-11-02:預設值、實際 JSON 儲存/載入程式、舊格式遷移及 USB 命令解析已做主機測試;因沒有可清空設定的備用設備,出廠直連、無閘道 10 分鐘與設定切換的實機驗收尚未執行,不得填 PASS。