現場驗收流程
本輪修訂: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。
- 無儲存設定或 USER 恢復原廠後,網頁顯示「目前處於首次設定模式」;MQTT、雲端及外部對時暫停。未對時的時間戳維持 null,不捏造有效時間。
- 儲存網路與 MQTT 設定;普通儲存、重開機、USB
net static/net dhcp/net keep不代表完成設定。 - 在設定頁按「完成設定並啟用」,成功保存後設備重開進入正常運行。USB 可用
net activate;會確認目前網路試用設定再重開。保存失敗必須留在首次設定並提示錯誤。 - IP 可繼續使用 192.168.50.50;不依 IP 或閘道猜測模式。無閘道的現場 LAN 可使用同網段數字 IP 的 Broker。跨網段與 DNS 主機名稱需先設定可用閘道。既有有效設定升級維持正常運行。
- 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 須替換成現場站號):
{"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 ID35B4EEB5DA9C49EC9B363566A7EFE4B7;實秤三段總量 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 內部也會用到 |
# macOS
brew install python mosquitto # curl 系統內建# Windows PowerShell(系統管理員)
winget install Python.Python.3.12
winget install EclipseFoundation.Mosquitto
# 裝完若找不到指令,把 C:\Program Files\mosquitto 加進 PATH裝完先自我檢查 —— 不要到了現場才發現少東西:
# macOS / Linux
for c in python3 curl mosquitto_pub mosquitto_sub; do
command -v $c >/dev/null && echo " ✅ $c" || echo " ❌ $c 沒裝"
done# 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.pypowershell # 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.pyWindows 的
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(整個驗收過程都會用到,只需設定一次):
# macOS / Linux
export TOK=<設備管理token> # 出廠預設值請向專案負責人索取# Windows PowerShell
$env:TOK = "<設備管理token>"找出設備 IP 與 UID(約 1 秒,只讀不寫):
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組出來的。
#設定它們
# 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 不同# 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 不同#確認六個都設好了
# 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# 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 分鐘)
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 拔電源之前,順手把網路故障切換一起驗掉。 先開一個終端機掛著看:
python3 watch_device.py diagpython 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 跑:
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):
- 拔掉網路線,按 reset 或斷電重開 → 設備會以 AUTO 模式落到 WiFi
- 等它起來,把網路線插回去,什麼都不要做
- 應該約 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)才需要的,而且它們要有專案原始碼才能跑 —— 現場通常沒有。 只做人工驗收的話,這一節看環境變數那段就好。
驗收過程會用到命令列工具,先裝好再到現場,不要在業主面前才發現少東西。
# Node(跑自動化測試)
brew install node
# 測試套件與瀏覽器核心
cd <專案>/web
npm install
npx playwright install chromium
# MQTT 命令列工具(發布/訂閱,驗證資料真的送出去)
brew install mosquitto# 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 直接從這裡抄。
# 直接貼進終端機就能跑 —— 現場沒有 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(前面有一個點加空白):
# ── 設備 ──
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 "$@"; }# 存成 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>。設備裝好後用這條確認真正的前綴:
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>。 前綴錯了的症狀是「訂閱端一筆都收不到」,而設備那邊看起來一切正常 —— 很難查。
確認連得到再往下:
curl -s -H "Authorization: Bearer $TOK" $DUT/api/system | python3 -m json.tool | head -12curl.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 |
電流環有兩種接法,驗收步驟不同,先確認實際是哪一種:
- 帶 RS485 的電流計(如 Finder 6M)→ 走階段 5,設 Slave ID 與型號模板。
- 4–20mA 類比訊號直接進類比輸入 → 本版尚未涵蓋。 若本站確實是這種接法,先與專案負責人確認量程換算方式,不要照 RS485 的步驟做。
#階段 R:歸零 —— 把設備恢復成出廠狀態
#🟢 全新未開封的設備:整個階段 R 跳過,直接到「這台機器走哪一條路」
全新機本來就是空的,沒有東西可以清。做歸零不會出錯,但是白費五分鐘。
以下只適用於「動過的機器」 —— 展示機、測試機、退回重配的機器。這類機器必須先歸零,否則舊設定會混進新現場,日後查問題時分不清哪些是這次設的。
#先理解:設備上有兩塊儲存區
| 區域 | 內容 | 重燒韌體會清掉嗎 |
|---|---|---|
| 韌體區 | 程式本身 | ✅ 會 |
| 設定區 | 所有設定、網路帳密、授權憑證 | ❌ 不會 |
「我重燒過韌體了,應該乾淨了吧」是錯的 —— 設定原封不動還在。
#三個層級
| 層級 | 怎麼做 | 清掉什麼 | 什麼時候用 |
|---|---|---|---|
| L1 | 按住機身按鈕開機,長按 10 秒以上 | 只清設定檔,不清授權憑證 | 自己排錯用 |
| L2 | 設定頁按「恢復原廠」 | 格式化整個設定區:設定 + 網路帳密 + 授權憑證全清 | 交機/轉售標準做法 |
| L3 | USB 重燒韌體,再做一次 L2 | 韌體 + 設定區都換新 | 換版本,或狀態不明 |
⚠️ L1 是交機時最容易踩的坑。 它看起來設定都清光了,但授權憑證還留著 —— 設備重開後會自己把授權狀態還原回去,下一手會拿到一台帶著上一手憑證的機器。交機一律用 L2。
#驗收標準
R.1 確認機器來歷
# 先看它身上有沒有東西 —— 有設定就是動過的
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 '(空)')
"# 先看它身上有沒有東西 —— 有設定就是動過的
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 歸零前備份(如果這台上面有還需要的東西)
# ① 叫設備推上去
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]]'
# ① 叫設備推上去
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/ HTTP200,而且 ③ 最新那筆的summary是無差異(代表雲端持有的內容等於設備現況)。若
summary顯示有差異、而你剛推完 —— 那才要查:代表雲端手上的是舊的。⚠️ ② 顯示
NEVER/ HTTP0不一定代表沒推成功。 那個狀態存在 RAM, 設備一重開就歸零(拔插網路線會觸發重開)。所以推完之後若設備重開過, ② 會變成NEVER,但備份其實好好地在雲端。以 ③ 為準 —— ③ 問的是雲端「有沒有這筆」,那才是還原時真正會被讀到的東西。 沒過:確定不需要保留就直接往下。歸零之後就真的沒了,這一步不要含糊。⚠️ ③ 一定要打雲端,不能打設備。 設備端所有
/api/configserver...開頭的 GET —— 含/versions、/history—— 回的都是同一包設定,永遠沒有清單。 拿它來驗會誤以為「查過了」,但你只驗到「推得上去」,沒驗到「讀得回來」, 而這一段的全部意義就在後者。
R.3 執行 L2「恢復原廠」
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/factory-resetcurl.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.18310,或接著網路線的 8320 不會開熱點,用 §2B.2 掃到的新 IP 下面用
$DUT代表該位址,先把它設成上表對應的值。
# 通道與規則各有自己的端點,不在 /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 '(空)')
"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 已填回並讀回確認。
# ① 列出可用快照,挑一筆(檔名長得像 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 就是改設定,會排一次推送。 所以順序必須是:
- 填回 Cloud Token
- 等到
needsPush變回False(通常十幾秒,用上面第 ② 步確認)- 才觸發還原
三個環節都會回報成功(rollback
success、pushOK 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 只能目視)
# 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 安裝工具(每台電腦只需做一次)
brew install arduino-cli
arduino-cli core update-index
arduino-cli core install arduino:mbed_optawinget install ArduinoSA.CLI
arduino-cli core update-index
arduino-cli core install arduino:mbed_optaWindows 使用者請至 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 取得拓荒包
curl -O https://opta.smms.com.tw/api/firmware/download/mes-gateway-bootstrap.bincurl.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
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 的韌體清掉。所以這裡刻意不放看起來像真的的範例值。
arduino-cli upload -b arduino:mbed_opta:opta \
-p <你的PORT> \
-i mes-gateway-bootstrap.binarduino-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(見本階段開頭的 🛑) |
不確定的話,問設備自己(這一條不受畫面影響):
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 後執行:
#!/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 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 前綴去找會漏掉。
找不到設備時,依序試:
- 網段填錯:確認你電腦的 IP。
ipconfig getifaddr en0(macOS)或ipconfig(Windows) - 查路由器的 DHCP 用戶端清單 —— 最可靠,直接列出所有連上的裝置
- 設備沒拿到 IP:確認網路線兩端都插好、對方交換器埠有燈
- 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 確認版本
curl -s -H "Authorization: Bearer $TOK" $DUT/api/system | python3 -m json.tool | head -8curl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/system | python -m json.tool | head -8通過:
"version"與交付文件指定的版本一致。 沒過:版本不符 → 上傳到舊檔案了,回到 2C.1。 版本必須對得上,否則這份驗收單不成立。
#階段 2D:授權啟用 —— 交機必做,而且很容易漏
〈階段 R〉的 L2(交機標準做法)會把授權憑證一起清掉,所以歸零之後設備會跳 「裝置尚未授權」。文件前面提了 4 次「授權憑證會被清」,卻從沒說怎麼裝回去 —— 照著走完會交出一台未授權的機器。
2D.1 先看設備現在的狀態
curl -s -H "Authorization: Bearer $TOK" $DUT/api/licensecurl.exe -s -H "Authorization: Bearer $env:TOK" "$($env:DUT)/api/license"通過:
{"status":"ACTIVE","licensed":true,...}。 看到別的狀態往下走 2D.2。
2D.2 分兩種情況 —— 先確認雲端有沒有這台的核准紀錄
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")))'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 全新設備:申請 → 核准 → 自動取件
- 設備網頁的「🔑 啟用授權」按鈕 → 送出申請(帶設備 UID)給雲端
- 管理員在雲端後台核准
- 設備自己輪詢取回簽章憑證並存起來 —— 不需要有人再到設備上按什麼
核准之後回 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,填進本單開頭,並更新環境變數
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'))
"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 印出來):
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))'$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 仍須核對本次推送與快照內容。
用指令做同樣三件事(比按網頁按鈕更容易看出哪一步壞掉)
# 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 組全部試完。看到你要的設備出現就可以往下走, 不必等到最後。
# ① 觸發掃描(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# ① 觸發掃描(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:電壓/電流/功率是固定欄位):
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"))
'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 陣列裡):
curl -s -H "Authorization: Bearer $TOK" $DUT/api/modbus | python3 -c '
import sys,json
d=json.load(sys.stdin)
print(" 暫存器目前值:", d.get("registers"))
'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 會直接拋錯。
分辨「沒人答」還是「答了但解不開」:
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/probecurl.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 全部加完後重開設備,再確認一次
curl -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/reboot
# 等約 60 秒後重跑 5.3curl.exe -s -X POST -H "Authorization: Bearer $TOK" $DUT/api/system/reboot
# 等約 60 秒後重跑 5.3通過:重開後設備清單完整、數值正常。這一步證明設定真的寫進去了,不只是畫面上有。 沒過:重開後設定不見 → 立即停止驗收並回報,不可帶病上線。
#階段 6:I/O 連動
6.1 看目前實體 I/O 狀態
curl -s -H "Authorization: Bearer $TOK" $DUT/api/io | python3 -m json.toolcurl.exe -s -H "Authorization: Bearer $TOK" $DUT/api/io | python -m json.tool通過:看得到
di/do陣列(di8 個、do4 個,擴充模組另在expDi/expDo)。🛑 上面那條指令只給你一張快照,看不出「會不會變」。 要驗「現場觸發時對應位置會變」, 必須持續監看。用專案內附的工具(它只在變化的那一刻印一行):
bash python3 watch_device.py iopowershell python watch_device.py io⚠️ 輪詢要夠密才抓得到脈衝式的輸入。 觸控鈕、光電開關這類送的是短脈衝, 1 秒輪詢一次會整個錯過 —— 看起來就像「按了沒反應」。
⚠️ 擴充模組的輸出要看
expDo的巢狀內容,不是插槽本身。expDo是「5 個插槽 × 每槽 8 通道」;只看插槽在不在([[...], null, null, null, null]) 是個常數,永遠不會變 —— 會得到「明明有動作、畫面卻沒變」的假象。(2026-09-08 補:這三個坑都是實際走查踩過的 —— 監看主機 DO 卻按了綁在擴充 DO 的按鈕、 1 秒輪詢漏掉觸控鈕脈衝、把插槽當成通道。「我看不到」不等於「它沒動」。)
6.2 直接驅動輸出,確認實體會動(不經規則,單純驗硬體)
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/docurl.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 的「輸入群組」「輸出群組」兩個下拉,列出來的就是這些名字。 沒命名的話下拉裡是一排
I1I2…DO 1DO 2—— 你認不出哪一路接了什麼,選錯了畫面也不會報錯,規則建好卻驅動到隔壁那路。
階段 R的 L2 恢復原廠會把名字全部清回預設。 實測歸零後:inputNames = ['I1'…'I8']、doNames = ['DO 1'…'DO 4']。 交機前一定要重新命名 —— 否則業主拿到的是一台「看不懂自己接了什麼」的機器。
網頁做法:配置頁 → 實體 I/O → 點該通道的名稱欄位直接改 → 儲存。
指令做法(現場一次設完比較快):
# 一次設一路,而且**每一行都印出自己是哪一路** ——
# 不標籤的話三個一模一樣的 {"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-綠" 0curl.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 請做兩次。
撥開關的時候掛著這個 —— 只在狀態變化時印出來
python3 watch_device.py iopython 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 — 留空則「收到訊息就觸發」,不管內容 |
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"start"}' # 應觸發
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"other"}' # 不應觸發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 的「留空」語意(這是最容易誤解的地方)
# 把「比對值」清空後再送一次
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"whatever"}'# 把「比對值」清空後再送一次
mq_pub -t "$PREFIX/cmd/start" -m '{"cmd":"whatever"}'通過:比對值留空時,任何內容都會觸發 —— 這是設計,不是故障。 為什麼要驗這個:現場常見「我明明設了條件,怎麼什麼都會動」, 十之八九就是比對值留空。當場示範一次,業主就懂了。
M.3 — Parser 入向:從 JSON 抽出數值
目的:上位系統送一包 JSON,設備要把其中一個數字取出來, 之後才能拿它做門檻比較(例:溫度 > 80 就報警)或塞進 Converter 發佈。
設定:新增通道 → 協定 MQTT、方向 輸入、模式 📥 收取並擷取數值 (Parser) →
Topic 填 <你的 $PREFIX>/data/temp → 在解析欄位區 新增至少一個欄位
(欄位名對應 JSON 的鍵,例 temp)。
介面會擋:「請至少新增一個解析欄位。」沒新增就存不了。
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')}\")
"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(意思是「非零」)→ 儲存 → 綁一條規則到某個輸出。
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 → 復歸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 閘門開且值變才發 用途:計數器事件回報 |
先開一個終端機常駐訂閱,以下所有情境都在它上面看結果:
mq_sub -t "$PREFIX/#" -v | while read -r line; do
echo "$(date +%H:%M:%S) $line"
donemq_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。可用欄位:
voltagecurrentpowerreactiveapparentpffreqthdkwhkwhposkwhneg;加: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 最乾淨。
# ① 建兩個對照通道(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# ④ 訂閱 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/off18 則 每 5 秒一則,值一樣照發 勾了 actest/on2 則 只有 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 只是把數值寫進韌體的一個暫存位置。要讓它真的當閘門,必須先綁定:
配置頁→ 📡 輸入群組 → +新增 → 名稱「開工閘門」→ 新增來源 → 類別選 📨 MQTT 命令訊號 (cmd/signal) → 選cmd/signal/1→ 比較條件≥ 0.5→ 儲存
⚠️ 比較條件一定要用
≥ 0.5,不要用= 1。 兩者在只送 0/1 時看起來一樣, 但 B5 會送5、15這種秒數 —— 用= 1的話那些值不滿足條件,閘門直接關閉, B5 會收到 0 筆而看起來像功能壞掉。走查時實際踩到過。
- 回到
配置頁→ TCP 通道 → 點開該 Converter 通道 → 🔒 門控信號 選「開工閘門」→ 儲存
沒做這一步的話:門控欄位維持「(無門控,持續推送)」, 你送
1送0都不會有任何差別 —— 而且沒有任何錯誤訊息。 這是 B4/B5/B6 全部失效的最常見原因。
B4 — MQTT Trigger 啟動 Converter(啟動式 × 連續) | 出廠對應 CV.3
目的:離峰時段不佔頻寬、不產生無用資料。上位系統開工時送一個訊號才開始回報,收工再送一個就停。
mq_pub -t "$PREFIX/cmd/signal/1" -m '0' # 閘門關
# 觀察 30 秒 → 應為 0 筆
mq_pub -t "$PREFIX/cmd/signal/1" -m '1' # 閘門開
# 觀察 60 秒 → 應照設定間隔持續收到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 的閘門仍綁著的情況下,把該通道的 🔁 變更時才發 勾起來。
mq_pub -t "$PREFIX/cmd/signal/1" -m '1' # 閘門開著
# 不要觸發現場設備,觀察 60 秒通過:閘門開著但值沒變 → 0 筆;現場觸發一次讓值改變 → 立刻 1 筆; 送
0關閉閘門後,再怎麼觸發都 0 筆。 沒過:閘門關著卻仍在發 → 門控沒綁上(回 B4 前置)。 值沒變卻一直發 → 勾選沒生效,或模板沒限小數位(見情境 C)。
B5 — 用訊號內容直接調整回報頻率 | 出廠對應 WI-143
目的:不同工單需要不同取樣密度。上位系統送數字就能改頻率,不必派人進設定頁。
mq_pub -t "$PREFIX/cmd/signal/1" -m '5' # 每 5 秒一筆
mq_pub -t "$PREFIX/cmd/signal/1" -m '0' # 停止mq_pub -t "$PREFIX/cmd/signal/1" -m '5' # 每 5 秒一筆
mq_pub -t "$PREFIX/cmd/signal/1" -m '0' # 停止沒過:此功能需閘門來源為 MQTT 型別。不使用則標「不適用」。
B6 — ★ 重新觸發要有反應(關再開要補發) | 出廠對應 CV.4
目的:操作員收工關掉、隔天開工再打開,若值剛好沒變就什麼都收不到 —— 他會以為系統壞了。所以重新打開時必須補一筆目前值。
mq_pub -t "$PREFIX/cmd/signal/1" -m '0' # 關
sleep 10
mq_pub -t "$PREFIX/cmd/signal/1" -m '1' # 再開(期間不要動負載)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 那條完整版可以列出「還沒命名的」):
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 那張表 —— 選錯不會報錯,只會「規則不動」。
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。
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 那個 → 儲存。
# 三個端點一起讀,才能交叉比對「規則綁的那個群組到底還在不在」
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 當場示範一次(這才是業主要看的)
撥開關 / 按按鈕 → 對應的輸出動作 → 解除 → 復歸。連續三次一致。
掛著監看比較好講解:
python3 watch_device.py io通過:三次都一致。 看不到 DI 翻轉 → 接線或供電,不是設定。 DI 有翻、輸出不動 → 回 U.2 檢查「觸發類型」,或 U.4 檢查兩個下拉有沒有真的綁到。
U.6 重新整理 + 重開機,再對一次
網頁上按重新整理 → 剛才建的東西全部還在。 然後
設定頁→ 重新啟動(或POST $DUT/api/system/reboot),等 60 秒再看一次。通過:數量與內容完全一致。這一步證明設定真的寫進 flash,不只是畫面上有。 沒過:重開後不見 → 立即停止驗收並回報,不可帶病上線。
# 重開前後各存一份,直接比對
python3 watch_device.py snapshot before.json
# … 重開,等 60 秒 …
python3 watch_device.py snapshot after.json
python3 watch_device.py diff before.json after.jsonU.7 推一次雲端備份收尾
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 的重點:不要相信「儲存成功」的提示,要親眼看到值。 回報成功但值沒進去,是最難察覺的失敗形式。
三關的指令:
# 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# 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 通過)。 閘門全綠不等於沒問題 —— 這也是為什麼要有人照著這份單子實際走一遍。
cd <專案>/web
EXPECT_FW=<版本> DUT_URL=$DUT ADMIN_TOKEN=$TOK npm run test:gatecd <專案>\web
EXPECT_FW=<版本> DUT_URL=$DUT ADMIN_TOKEN=$TOK npm run test:gate35 項,約 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
斷電前跑一次、復電後再跑一次,兩份輸出必須完全相同:
# 斷電前
python3 watch_device.py snapshot before.json
# …切掉電源,30 秒後復電,等設備開機完成…
# 復電後
python3 watch_device.py snapshot after.json
python3 watch_device.py diff before.json after.json# 斷電前
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 修正,這一項就是用來守住它不再回來。
怎麼做(要動手,不是打指令)
- 把設備和網路分享器插在同一個延長線上(現場本來就常常是這樣)
- 記下設備現在的 IP,等一下要用
- 把延長線的電源整個關掉,等 10 秒
- 打開電源
- 不要碰任何東西,等 5 分鐘
5 分鐘之後檢查這三件事
# ① 網頁打得開嗎(把 <設備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 的重開原因判讀(斷電前另開一個終端機掛著):
python3 watch_device.py diagpython 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:
curl --fail --show-error -o scale_multical_build.py https://opta.smms.com.tw/docs/assets/scale_multical_build.pyWindows PowerShell 5.1/7:
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:
curl --fail --show-error -o migrate_measurement_timestamps.py https://opta.smms.com.tw/docs/assets/migrate_measurement_timestamps.pyWindows PowerShell 5.1/7:
curl.exe --fail --show-error -o migrate_measurement_timestamps.py https://opta.smms.com.tw/docs/assets/migrate_measurement_timestamps.py應該看到/要核對:下載成功,與校正工具放在目前工作目錄。
不符合時:若下載失敗或出現 HTML 錯誤頁,先確認電腦可以開啟文件中心,再重新下載;不要執行錯誤頁。
#驗證校正工具可執行
macOS/Linux:
python3 scale_multical_build.py planWindows PowerShell 5.1/7:
python scale_multical_build.py plan應該看到/要核對:顯示計畫,未連設備或更動設定。
不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。
#驗證遷移工具可執行
macOS/Linux:
python3 migrate_measurement_timestamps.py --helpWindows PowerShell 5.1/7:
python migrate_measurement_timestamps.py --help應該看到/要核對:顯示 --gw、--apply 與 --out 說明。
不符合時:若工具報錯或列出非預期變更,先不要繼續套用。保存結果檔與私有備份,請工程人員核對。
#保存斷電前診斷
macOS/Linux:
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/diag" -o before-diag.jsonWindows PowerShell 5.1/7:
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:
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/config/backup" -o before-config.jsonWindows PowerShell 5.1/7:
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:
export OPTA_TOKEN="$TOK"Windows PowerShell 5.1/7:
$env:OPTA_TOKEN = $env:TOK應該看到/要核對:沿用已驗證的管理 token。
不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。
#輸入純 IP(不含 http://)
macOS/Linux:
read -r GW_IPWindows PowerShell 5.1/7:
$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:
READY_DO=-1Windows PowerShell,設定 ready DO:
$READY_DO = -1macOS/Linux,設定 ACK DO:
ACK_DO=-1Windows PowerShell,設定 ACK DO:
$ACK_DO = -1#安裝校正流程(SC.1)
macOS/Linux:
python3 scale_multical_build.py apply --gw "$GW_IP" --ready-do "$READY_DO" --ack-do "$ACK_DO"Windows PowerShell 5.1/7:
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:
python3 scale_multical_build.py reset --gw "$GW_IP" --ready-do "$READY_DO" --ack-do "$ACK_DO"Windows PowerShell 5.1/7:
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:
export MQ_HOST=mosquitto.smms.com.twWindows PowerShell 5.1/7:
$env:MQ_HOST = "mosquitto.smms.com.tw"應該看到/要核對:只設定變數,尚未送控制命令。
不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。
#設定 MQTT username
macOS/Linux:
read -r MQ_USERWindows PowerShell 5.1/7:
$env:MQ_USER = Read-Host "MQTT username"應該看到/要核對:只設定變數,尚未送控制命令。
不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。
#設定 MQTT password
macOS/Linux:
read -r -s MQ_PASSWindows PowerShell 5.1/7:
$env:MQ_PASS = [System.Net.NetworkCredential]::new("", (Read-Host "MQTT password" -AsSecureString)).Password應該看到/要核對:輸入現場有效值後 Enter,密碼輸入不回顯。
不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。
#設定發布函式
macOS/Linux:
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:
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:
pub_signal 3 1Windows PowerShell 5.1/7:
Send-Signal 3 1應該看到/要核對:成功回覆後可寫入燈亮;無實秤回覆時不得亮成功燈。
不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。
#放開標零命令
macOS/Linux:
pub_signal 3 0Windows PowerShell 5.1/7:
Send-Signal 3 0應該看到/要核對:命令回 0,避免保留在 1。
不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。
#每一段都依序執行(SC.3)
手動動作:放已知砝碼、等面板穩定。3 段做 1~3;4 段做 1~4;5 段做 1~5。重量是去除小數點的面板數值(例如兩位小數的 3 kg 填 300);不得直接套例值當業主砝碼。每換一個砝碼都重新執行下面六個動作。
#輸入本次段號
macOS/Linux:
read -r SEGMENTWindows PowerShell 5.1/7:
$SEGMENT = Read-Host "段號"應該看到/要核對:一般段填 1~本輪段數,本工具上限 50;只有收尾填最後段號+1。
不符合時:若值不在上面允許的範圍,重新輸入正確值後才做下一步,不要送出超界數字。
#送出段號
macOS/Linux:
pub_signal 4 "$SEGMENT"Windows PowerShell 5.1/7:
Send-Signal 4 $SEGMENT應該看到/要核對:設備段號等於本次段號。
不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。
#輸入本次重量
macOS/Linux:
read -r WEIGHT_RAWWindows PowerShell 5.1/7:
$WEIGHT_RAW = Read-Host "重量整數(去小數點)"應該看到/要核對:一般段只允許整數 1~65535,且與面板小數位一致;禁止小數、負數與超界,0 僅限明確收尾。
不符合時:若值不在上面允許的範圍,重新輸入正確值後才做下一步,不要送出超界數字。
#送出重量
macOS/Linux:
pub_signal 5 "$WEIGHT_RAW"Windows PowerShell 5.1/7:
Send-Signal 5 $WEIGHT_RAW應該看到/要核對:設備重量參數與砝碼換算相符。
不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。
#寫入此段
macOS/Linux:
pub_signal 6 1Windows PowerShell 5.1/7:
Send-Signal 6 1應該看到/要核對:只在所有寫入回覆有效後出現 ACK;不把燈號當精度證明。
不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。
#放開寫入命令
macOS/Linux:
pub_signal 6 0Windows PowerShell 5.1/7:
Send-Signal 6 0應該看到/要核對:write 回 0,再換下一個砝碼。
不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。
#零重量收尾(SC.3/SC.4)
段號填「最後一段+1」(3 段填 4、4 段填 5、5 段填 6)。依上方「輸入段號→送出段號」兩步送出,再逐步做:
#送出收尾重量零
macOS/Linux:
pub_signal 5 0Windows PowerShell 5.1/7:
Send-Signal 5 0應該看到/要核對:收尾重量為 0。
不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。
#寫入收尾
macOS/Linux:
pub_signal 6 1Windows PowerShell 5.1/7:
Send-Signal 6 1應該看到/要核對:收尾有有效 Modbus 回覆後才確認成功。
不符合時:若設備沒有出現預期反應,停在這一步,先查命令是否送到正確設備、磅秤是否正常回覆;不要連續盲目重送。
#放開收尾命令
macOS/Linux:
pub_signal 6 0Windows PowerShell 5.1/7:
Send-Signal 6 0應該看到/要核對:write 回 0。
不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。
#讀回校正流程狀態
macOS/Linux:
python3 scale_multical_build.py show --gw "$GW_IP"Windows PowerShell 5.1/7:
python scale_multical_build.py show --gw "$env:GW_IP"應該看到/要核對:流程設定完整;此工具不取代實秤量測。接著用核准砝碼量測各點與點間重量,記錄 SC.7 允差。
不符合時:若命令報錯,先停下來保存錯誤訊息,確認工具、設備與設定值,再重做這一步。
#故意失敗與斷電後恢復(SC.4~6/NR.1)
以下前兩項由工程人員在專用測試設備做,不在生產中的秤上試錯。
- 重新校正測試(SC.4):照前面的命令,依序做 3 段、5 段、再回 3 段。每一輪都記錄收尾;另安排一輪故意不收尾,記為「未完成」,補收尾後再查是否仍用到多餘舊段。
- 沒有回覆的測試(SC.5):工程人員讓測試秤暫時不回覆,再送一次寫入命令的 1 與 0。確認命令有到設備、日誌有寫入失敗,而且沒有假成功。其他錯誤回覆種類由工程測試另外留紀錄,不能因為正常操作成功就全部打勾。
- 恢復通訊:先解除測試設備的阻斷,再 reset、重新標零。
- 斷電前準備(SC.6/NR.1):確認命令都已送回 0,保存設定與啟動次數。不可把「命令按住時斷電」當成取消或安全續作的方法。
- 關電:切斷這台設備所有供電(USB 與 DC),以及本次測試的路由器。確認燈全滅,記下時間。
- 復電:恢復供電,讓設備先起、路由器較晚起。路由器好了以後,不要再人工重開設備。
- 確認恢復:執行下面的讀取命令與新訊息檢查。至少觀察 180 秒,保存實際時間;没量到的時間填「未量測」。
- 重新做校正:依前面的 reset 命令清除狀態,確認未標零時不能寫入,再重新標零。不要把復電後保存的「可寫入」狀態當成這次校正已完成。
任何一步不符合,就保留當下紀錄並停止往下簽「通過」。
#讀取復電後網路/MQTT
macOS/Linux:
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/system" -o after-system.jsonWindows PowerShell 5.1/7:
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:
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/diag" -o after-diag.jsonWindows PowerShell 5.1/7:
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:
python3 migrate_measurement_timestamps.py --gw "$GW_IP" --out timestamp-dry.jsonWindows PowerShell 5.1/7:
python migrate_measurement_timestamps.py --gw "$env:GW_IP" --out timestamp-dry.json應該看到/要核對:只列出準備新增 timestamp 的標準範本;沒有標準範本時零變更不代表新 UI 已測,須另依網頁建立流程。
不符合時:若工具報錯或列出非預期變更,先不要繼續套用。保存結果檔與私有備份,請工程人員核對。
#套用已核對的遷移
macOS/Linux:
python3 migrate_measurement_timestamps.py --gw "$GW_IP" --apply --backup-dir ./private-backups --out timestamp-apply.jsonWindows PowerShell 5.1/7:
python migrate_measurement_timestamps.py --gw "$env:GW_IP" --apply --backup-dir ./private-backups --out timestamp-apply.json應該看到/要核對:原欄位、精度不變;工具先保存私有完整備份到目前工作目錄的 private-backups。失敗時看 rollback 結果,不可直接標 PASS。
不符合時:若工具報錯或列出非預期變更,先不要繼續套用。保存結果檔與私有備份,請工程人員核對。
#確認第二次無變更
macOS/Linux:
python3 migrate_measurement_timestamps.py --gw "$GW_IP" --out timestamp-second.jsonWindows PowerShell 5.1/7:
python migrate_measurement_timestamps.py --gw "$env:GW_IP" --out timestamp-second.json應該看到/要核對:用文字編輯器開啟 timestamp-second.json,應看到 "changes": [],也就是沒有需要再改的項目。
不符合時:若工具報錯或列出非預期變更,先不要繼續套用。保存結果檔與私有備份,請工程人員核對。
#訂閱新磅秤量測(TS.2)
macOS/Linux:
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 -vWindows PowerShell 5.1/7:
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:
curl --fail --show-error -H "Authorization: Bearer $TOK" "$DUT/api/config/backup" -o after-config.jsonWindows PowerShell 5.1/7:
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:
python3 scale_multical_build.py remove --gw "$GW_IP"Windows PowerShell 5.1/7:
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 不宣稱已驗證此主機建置流程。
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 相同,但這次是最終設定)
# 先做一個實際變更,否則雲端會因內容相同而去重、不產生新快照
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 一直收到相同的值 | 該通道「🔁 變更時才發」沒勾 | 逐通道檢查勾選 |
| 重開後設定不見 | 設定未落盤 —— 嚴重 | 停止使用並回報 |
先跑這一組,八成的問題在這裡就看得出來
# 一次把「它活著嗎/為什麼重開/網路走哪條/授權狀態」全撈出來
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 上要 5/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.8 |
| 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 轉接器。
- 記錄 UID、版本、Build ID、bootCount、原網路設定;每次測試只操作指定設備。
- 正確固定 IP:透過
net static <ip> <mask> <gateway>套用,等首次重啟後從 Ethernet 開網頁確認;再次重啟仍保留設定。 - 錯誤閘道:使用隔離測試網段,不得占用其他設備 IP;持續 USB 錄影式日誌,測試期間不要對設備送 HTTP/API 或
net keep(這些會確認試用)。 - 連線未確認:連續 link-up 五分鐘後還原原設定;若試用途中另一次重啟,應在網路初始化前還原,不能每次重啟重算五分鐘而失聯。
- 核對日誌的實際重啟原因、檢查點及還原訊息,再讀回網路、MQTT、I/O、群組設定;不能只依 LED 或「沒有 fault record」宣稱正常。
- 正確 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。