CNC 錄片「影像 + 狀態文字」融合:方式、步驟與注意事項
一、要做的事
把 CNC 執行時的控制面板狀態(log)疊在 webcam 錄片上,變成單一支「畫面 + 即時狀態字幕」的影片。
狀態文字包含:Run / Idle / Alarm、MPos 座標、FS 進給速度、SD 目前指令、Ov 倍率等,每 0.4 秒更新一次。
二、融合方式總覽
┌──────────────────────────────────────────┐
webcam ──RTSP──► Pi4 (mediamtx) ──RTSP──► Pi4 (monitor)
│ │
ESP32 FluidNC ──WebSocket(即時狀態)─► monitor 同步記 log
│ │
└────────► 收檔 run_YYYYMMDD_HHMMSS.mp4
+ .mp4.log (每 0.4s 一行的狀態)
- 錄製端(Pi4):monitor.py 同時抓兩路——RTSP 影像串流寫 MP4,WebSocket 狀態寫成時間軸 log。
- 收檔觸發:偵測到
Run/Hold/Alarm 開始錄,回 Idle 8 秒確認後收檔。
- 回傳/疊字端(GX10):Pi4 的 sync timer 每 2 分鐘掃一次,把新錄片 + log scp 到 GX10
/home/gx/cnc_records/,校驗大小一致後 Pi4 端直接刪除(不留檔,檔案只存在 GX10),並自動跑 stitch.py 疊字。
三、stitch.py 如何把 log 融進影片(技術)
- 不用「切段再拼」:
ffmpeg -ss 切段 drawtext → concat 的方式,seek 是跳到關鍵幀,每段最多漂 1~2 秒,字幕會「跟不上畫面」。
- 改用 ASS 字幕單次 pass:每行 log → 一條
Dialogue(起始/结束時間戳),ffmpeg -i in.mp4 -vf ass=subs.ass 一次解碼一次編碼就疊完,逐幀精確。
- 每行 log 都是字幕:161 行 log = 161 條字幕,
MPos 數字隨機器逐行跳動,不會被「同狀態 5 秒合併」規則吃掉的訊息。
- 時基校正 (L2V_OFF = 2.7s):影片走 webcam → mediamtx → RTSP → ffmpeg,有固定的串流累積延遲(本環境實測 2.7 秒,用圓弧起點兩點對齊 + 像素運動法交叉驗證)。所有字幕時間戳統一
+2.7s。
- 結尾裁定 (TAIL = 0.5s):錄片尾段有 mediamtx 的凍住緩衝幀(收檔瞬間 ffmpeg 還在吐尾),
cut = min(log_tstop + L2V_OFF + TAIL, 影片長度),ffmpeg -t cut 直接裁掉凍尾。
- ASS 時間戳格式:
HH:MM:SS.cc(ASS 用 1/100 秒的兩位小數)。65 秒 = 00:01:05.00,不是 00:65.00——寫錯的話 ASS 會把「分」和「秒」搞亂,整支片時間軸錯位。
四、一次完整流程
[1] 執行工: ssh Pi4 → 透過 curl 對 FluidNC 下 $LocalFS/Run=<檔>.gcode 指令要包含$跟gcode要對
[2] Pi4 monitor 偵測到 Run,自動起錄 webcam + WebSocket 狀態 log
[3] 機器回 Idle → 8s 後收檔 recordings/run_*.mp4 + .mp4.log
[4] cnc-sync.timer (每 2 min) 掃 recordings/ → 校驗+回傳 GX10
[5] 回傳成功 → Pi4 端刪除; 失敗 → 保留等待下輪
[6] GX10 自動跑 stitch.py,產出 run_*_annotated.mp4 (同目錄)
[7] 收片於 GX10,需要就自取,不要就手動刪
五、注意事項 (踩過的坑)
1. 時基 (最重要)
- 串流是慢的,log 是即時的——兩個時基不對齊,字幕就會「跟不上」或「超前」。
- 換硬體/換路由器/改 mediamtx 設定後,
L2V_OFF 要重新實測。
- 實測方法:在 log 挑一個 MPos 已知的點(例如圓弧起點),用
ffmpeg -filter:v select='gt(scene,0.03)' 或逐 1 秒抽幀比像素差,找到畫面第一次出現該位置對應的時間,差值就是偏移。
2. ASS 字幕格式
- 每一行 log = 一條 Dialogue,
[start, end) 半開區間,相切不重疊。
- 文字中的
: 是 ASS 分隔符,若文字本身要出現冒號要轉義;我們直接用半形冒號無事,遇到 : 分隔符要留意。
- 過長行 (>64 char) 會被截斷加
…,避免單行跨兩行顯示。
3. 不要切段拼
drawtext -ss 切段 → concat demuxer → -c copy 看似省事,seek 會跳到關鍵幀,每段都會飄。
- 用 ass 單次 pass,一次解碼一次編碼,零飄移,還比較快。
4. Pi4 端不要留錄片
- 回傳校驗
size 一致 + 已落 GX10 → Pi4 端直接 rm。
- 校驗不符 / scp 失敗 → 保留 Pi4 原檔,標記下輪重傳,絕不能雙邊都刪。
- 收檔剛完成的檔案 (<30s) timer 會跳過(避免抓到寫入中)——這是 monitor 端
recorder_active flag 與 sync 端的約定。
5. 結尾裁定
- mediamtx 在收檔瞬間還會吐 3~6 秒緩衝凍幀,不做
ffmpeg -t cut 會看到「畫面停了、文字還在跳」的殘影。
cut 用 min(log_tstop + L2V_OFF + 0.5s, 影片長度),不要超過影片實際長度(ffmpeg 會報錯)。
6. log 格式
START epoch=<t> / <offset> <state> <detail> / END 三行結構。
- 解析 regex
^([\d.]+)\s+(\S+)\s*(.*)$,第一欄是「距 START 的偏移秒」不是絕對時間。
- 若改 log 寫入格式,stitch.py 的
parse_log 要同步改。
7. 不要雙邊都跑 stitch
- stitch 只在 GX10 跑一次,Pi4 只負責同步回傳。
- 若 Pi4 也跑了 stitch,雙邊會產生同一個
_annotated.mp4,回傳時以 GX10 版本為準。
8. 時間戳顯示
- 65 秒是
00:01:05,不是 00:65——ASS 的 HH:MM:SS.cc 是「小時:分鐘:秒.百分秒」,把 65 秒當秒寫進去會被解讀成 64 分鐘 + 5 秒,整支片時間軸錯位 63 分鐘以上。
- 寫
ts(sec) 工具時:h=sec//3600, m=(sec%3600)//60, s=sec%60,不要用 %60 套在 m 上(會把 65 秒寫成 00:65.00,ASS 解讀成 0 分 65 秒,等於 1 分 5 秒,跟預期差 53 秒)。
9. 收片位置
- 原版
run_*.mp4、log run_*.mp4.log、疊字版 run_*_annotated.mp4 三者都在 /home/gx/cnc_records/,自取即可。
- Pi4 端
recordings/ 不應留任何 mp4(只有 trace.log 是正常)。
六、驗證清單 (每支片收工前)
| 項 |
驗法 |
| 機器跑完整支 gcode |
log 最後有 Idle MPos:0.000,0.000,0.000 |
| 字幕不重疊 |
awk 掃 ass 檔,end_i < start_{i+1} |
結尾 = Idle MPos:0,0,0 |
抽最後 3 幀看 MPos |
| 結尾 = 圓完整 |
抽最後 1 幀看圓弧 |
| 中途不跳 |
抽 30% / 60% / 90% 三幀,每幀 MPos 跟 log 對應行一致 |
| Pi4 端乾淨 |
ls ~/cnc-monitor/recordings/*.mp4 無結果 |
| GX10 三件齊 |
run_*.mp4 + run_*.mp4.log + run_*_annotated.mp4 都在 |
七、文件位置
| 檔 |
位置 |
| monitor.py (錄製 + log) |
Pi4 ~/cnc-monitor/monitor.py |
| stitch.py (疊字) |
GX10 /home/gx/cnc_records/stitch.py |
| sync_rec.sh (回傳 + 清理) |
Pi4 ~/cnc-monitor/sync_rec.sh |
| 錄片 + log + annotated |
GX10 /home/gx/cnc_records/ |
| 本文 |
WP http://localhost:8082/ (區網) |
沒有留言:
張貼留言