diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f22780c..88dcf1a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -11,7 +11,7 @@ ETTerms 是一個給工程師 / 韌體 / 硬體驗證人員用的**單一視窗終端機工作台**。它把日常會用到的兩種連線——**SSH**(連伺服器 / 嵌入式 Linux)與 **Serial Port**(連 UART / console / 開發板)——收進同一個 WinForms 視窗裡,用分頁(Tab)管理多條連線,左側有可存檔的連線清單(Saved Connections)。 -核心差異化價值是**腳本自動化**:沿用並擴充 MyTeraTerm 既有的 **TTL(Tera Term Language)直譯器**,讓使用者既有的 `.ttl` 腳本(`send` / `sendln` / `wait` / `if` / `while` / `logopen` / `pductrl`…)可以直接重用,直接驅動原生 SSH / Serial channel,做到登入自動化、批次下命令、log 收集、PDU 電源控制等。 +核心差異化價值是**腳本自動化**:沿用並擴充 MyTeraTerm 既有的 **TTL(Tera Term Language)直譯器**,讓使用者既有的 `.ttl` 腳本(`send` / `sendln` / `wait` / `if` / `while` / `logopen` / `sprintf2` / `pductrl`…)可以直接重用,直接驅動原生 SSH / Serial channel,做到登入自動化、批次下命令、log 收集、PDU 電源控制等。 開發策略採「**GUI 先行**」:先把 KKTerm 風格的視窗外殼與分頁工作區做出來(可見、可切換、可關閉),再逐步把 Serial → SSH → VT100 渲染 → 腳本引擎一層層補上。 @@ -296,6 +296,8 @@ ETTerms 是單視窗多分頁,沒有「路由」,以下以**功能面板** - 載入 / 編輯 `.ttl` 腳本(語法沿用 MyTeraTerm) - 對「目前 active session」執行腳本;顯示執行進度(檔名 / 行號 / 當前指令) - 可取消執行(`Cancel()`);`logopen` 將輸出寫檔 +- `sprintf2`:C `printf` 風格格式化輸出到變數(TeraTerm 相容) +- `wait`:命中關鍵字後會等裝置安靜(`SettleMs`,預設 300ms)再接受並取最後一次出現,避免比對到輸出中途的指令回顯 - 選用:PDU 控制指令(`pductrl` / `pduconnect`)走 SNMP ### Group 同步執行 @@ -337,7 +339,9 @@ ETTerms 是單視窗多分頁,沒有「路由」,以下以**功能面板** ScriptRunner.RunAsync(scriptText, activeChannel) → TTLInterpreter(channel) send/sendln → channel.Write(bytes) - wait "xxx" → 監聽 channel.DataReceived,比對緩衝直到 match 或 timeout + wait "xxx" → 監聽 channel.DataReceived,比對到關鍵字後再等裝置「安靜」(~300ms 無新資料) 才接受, + 並消費到該字串「最後一次」出現處 → 避免命中輸出中途的指令回顯(如 SVOS> help) + sprintf2 → C printf 風格格式化字串存入變數(與 TeraTerm 相容,設定 result) logopen/write→ StreamWriter 寫檔 pductrl → PduController(SNMP set)→ PDU if/while → 依 result / 變數做流程控制 diff --git a/CLAUDE.md b/CLAUDE.md index d9a4fc8..d8f69f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,8 @@ ETTerms 是一個 **C# .NET 8 WinForms** 的原生 Windows 終端機工作台, **進度:** Phase 1–5 ✅、Phase 6 ✅(TTL 引擎 + Group 同步,SSH 待驗收)、Phase 7 ✅(Settings/About)、Phase 8 ✅(PDU + Shell/ConPTY + SFTP + Settings 擴充)、Phase 9 🔜(Serial MCP server,讓 AI 直接操作 serial)。打包待指示。 +**v0.1.2:** 新增 `sprintf2`(TeraTerm 相容 C printf 格式化);`wait` 改為命中關鍵字後須等裝置安靜(`SettleMs` 預設 300ms)才接受並取「最後一次」出現,排除輸出中途的指令回顯(避免腳本搶跑)。 + ## 技術棧 - **UI:** C# .NET 8 WinForms(`net8.0-windows`, `UseWindowsForms`, `Nullable=enable`) diff --git a/docs/ttl-script-reference.md b/docs/ttl-script-reference.md index 6bcf48a..29fef7d 100644 --- a/docs/ttl-script-reference.md +++ b/docs/ttl-script-reference.md @@ -25,7 +25,7 @@ |------|------|------| | `send` | `send '文字'` | 送出文字(不加換行)到 active session。 | | `sendln` | `sendln '文字'` | 送出文字並附加 `\r\n`。 | -| `wait` | `wait '字串'` | **一直等到**接收緩衝出現指定字串才往下;預設無限等待(可按 Stop 取消)。命中後 `result=1` 並清掉緩衝中該字串(含)之前的內容。 | +| `wait` | `wait '字串'` | **一直等到**接收緩衝出現指定字串才往下;預設無限等待(可按 Stop 取消)。命中後會再確認裝置「安靜」約 300ms(無新資料)才接受,以排除夾在輸出中途的回顯(如 `SVOS> help`),並消費到該字串**最後一次**出現(含)之前的內容,`result=1`。 | | `pause` | `pause 秒數` | 暫停指定秒數(可被 Stop 中止)。 | | `timeout` | `timeout = 秒數` | 設定 `wait` 的逾時秒數。預設 `0`=無限等待;若設為 `N>0`,`wait` 超過 N 秒仍未命中會**中止整個腳本並報錯**(不會略過 `wait` 往下做)。 | | `flushrecv` | `flushrecv` | 清空接收緩衝區。 | diff --git a/src/ETTerms/Scripting/TTLInterpreter.cs b/src/ETTerms/Scripting/TTLInterpreter.cs index c6a1e8c..1e54bf3 100644 --- a/src/ETTerms/Scripting/TTLInterpreter.cs +++ b/src/ETTerms/Scripting/TTLInterpreter.cs @@ -21,6 +21,8 @@ public sealed class TTLInterpreter : IDisposable private readonly StringBuilder _recv = new(); private readonly CancellationTokenSource _cts = new(); + private const int SettleMs = 300; // wait 命中關鍵字後,需連續安靜這麼久(無新資料)才接受,避免比對到輸出中途的回顯 + private GroupSyncContext? _groupSync; private string _groupMemberLabel = ""; private readonly Dictionary _pdus = new(); @@ -351,18 +353,10 @@ public sealed class TTLInterpreter : IDisposable while (true) { ThrowIfCancelled(); - lock (_recv) - { - string buf = _recv.ToString(); - int idx = buf.IndexOf(text, StringComparison.Ordinal); - if (idx >= 0) - { - _result = 1; - _recv.Clear(); - _recv.Append(buf.Substring(idx + text.Length)); - return; - } - } + bool found; + lock (_recv) found = _recv.ToString().IndexOf(text, StringComparison.Ordinal) >= 0; + if (found && SettleAndConsume(text)) return; + // 只有在明確設定 timeout(>0) 且超時才中止腳本;否則一直等到關鍵字出現或被取消。 if (_timeout > 0 && elapsed >= _timeout) { @@ -375,6 +369,33 @@ public sealed class TTLInterpreter : IDisposable } } + /// + /// 找到關鍵字後,等待裝置「安靜」( 內無新資料進來)才接受, + /// 並消費到「最後一次」出現處。若 settle 期間又有資料進來,視為輸出尚未結束 + /// (命中的可能是夾在輸出中的指令回顯,如 SVOS> help),回傳 false 讓外層續等到真正的就緒提示。 + /// + private bool SettleAndConsume(string text) + { + int len; + lock (_recv) len = _recv.Length; + for (int waited = 0; waited < SettleMs; waited += 50) + { + ThrowIfCancelled(); + Thread.Sleep(50); + lock (_recv) if (_recv.Length != len) return false; // 又有新資料 → 還沒安靜 + } + lock (_recv) + { + string buf = _recv.ToString(); + int idx = buf.LastIndexOf(text, StringComparison.Ordinal); + if (idx < 0) return false; + _result = 1; + _recv.Clear(); + _recv.Append(buf.Substring(idx + text.Length)); + } + return true; + } + private void FlushReceive() { lock (_recv) _recv.Clear();