Ver Fonte

feat(control): 阶段2 控制端 — /status监控补全 + /serial借串口 + /shutdown受护栏停止

- ControlHttpServer 扩展:GET /status 返回完整 MonitorSnapshot(rich);
  POST /shutdown(token校验)+ /serial/pause|/serial/resume(houseSn),含 POST body JSON 解析。
- MonitorSnapshot/HouseMonitorRow 补 §6 三块:WorkingType/ValveState/CapturePausedByGate(只读);
  GetMonitorSnapshot 填充。
- Program:BuildRichStatus(快照)+ HandleShutdown(口令tl13579→SafeShutdown:HAL.ShutdownAll+_exitEvent.Set)
  + HandleSerialPause/Resume(HAL.GetHouseGate.PauseCapture/ResumeCapture,接已有 HouseBin 让路)。

真机验证(自主):/status 三块字段+心跳;/serial/pause 舱2 CapturePausedByGate=true 且 COM4 停发(让路,不驱动电机)
→ resume 恢复;/shutdown 错口令403、对口令tl13579 安全停机(进程退出+7 COM口全释放)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie há 3 dias atrás
pai
commit
7ab9f720be

+ 4 - 0
ivf_tl_operate_2.0/control/ivf_tl_Control/AppData.cs

@@ -275,6 +275,10 @@ namespace ivf_tl_Control
                             ComState = bin.ComBin != null ? "已连接" : "未连接",
                             CcdState = bin.CCDError ? "异常" : "正常",
                             CcdError = bin.CCDError,
+                            // 阶段2 §6 三块补充(只读)
+                            WorkingType = bin.WorkingType.ToString(),
+                            ValveState = bin.ValveState.ToString(),
+                            CapturePausedByGate = bin.CapturePausedByGate,
                         });
                     }
                     catch { }

+ 7 - 0
ivf_tl_operate_2.0/control/ivf_tl_Control/MonitorSnapshot.cs

@@ -80,5 +80,12 @@ namespace ivf_tl_Control
         public string CcdState { get; set; }
         /// <summary>是否有 CCD 错误标记(HouseBin.CCDError)。</summary>
         public bool CcdError { get; set; }
+        // —— 阶段2 监控补全(§6 三块)——
+        /// <summary>各舱实时活动类型(HouseBin.WorkingType:DoNothing/AirSwapWorking/CCDWorking/AutoFocusWorking)。</summary>
+        public string WorkingType { get; set; }
+        /// <summary>排气阀状态文本(HouseBin.ValveState)。</summary>
+        public string ValveState { get; set; }
+        /// <summary>串口借用:该舱采集是否被前台(调试/对焦)让路中(HouseBin.CapturePausedByGate)。</summary>
+        public bool CapturePausedByGate { get; set; }
     }
 }

+ 70 - 4
ivf_tl_operate_2.0/control/ivf_tl_ControlHost/ControlHttpServer.cs

@@ -1,34 +1,52 @@
 using System;
+using System.IO;
 using System.Net;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
 
 namespace IvfTl.ControlHost
 {
     /// <summary>
     /// control 进程内的本地 HTTP 小服务,只监听 127.0.0.1:port。
-    /// 阶段1:/ping、/status。阶段2 扩展 /serial/pause|resume、/shutdown
+    /// 阶段1:/ping、/status。阶段2:/status 补全(rich) + /serial/pause|resume(借串口) + /shutdown(受护栏停止)
     /// </summary>
     public class ControlHttpServer
     {
         private readonly int _port;
-        private readonly Func<StatusDto> _statusProvider;
+        private readonly Func<StatusDto> _pingProvider;        // /ping 轻量存活
+        private readonly Func<object> _statusProvider;         // /status 完整快照(阶段2 §6 三块)
+        private readonly Func<string, bool> _shutdownHandler;  // /shutdown(token 校验后安全停机)
+        private readonly Func<int, bool> _serialPauseHandler;  // /serial/pause(借串口:control 让路该舱)
+        private readonly Func<int, bool> _serialResumeHandler; // /serial/resume(归还:恢复采集)
         private readonly Action<string> _log;
         private HttpListener _listener;
         private CancellationTokenSource _cts;
 
-        public ControlHttpServer(int port, Func<StatusDto> statusProvider, Action<string> log)
+        public ControlHttpServer(
+            int port,
+            Func<StatusDto> pingProvider,
+            Func<object> statusProvider,
+            Func<string, bool> shutdownHandler,
+            Func<int, bool> serialPauseHandler,
+            Func<int, bool> serialResumeHandler,
+            Action<string> log)
         {
             _port = port;
+            _pingProvider = pingProvider;
             _statusProvider = statusProvider;
+            _shutdownHandler = shutdownHandler;
+            _serialPauseHandler = serialPauseHandler;
+            _serialResumeHandler = serialResumeHandler;
             _log = log ?? (_ => { });
         }
 
         public void Start()
         {
             _listener = new HttpListener();
+            // 仅本机回环,拒绝外部访问(防外部调停机/借串口)。
             _listener.Prefixes.Add($"http://127.0.0.1:{_port}/");
             _listener.Start();
             _cts = new CancellationTokenSource();
@@ -51,16 +69,40 @@ namespace IvfTl.ControlHost
         private void Handle(HttpListenerContext ctx)
         {
             string path = ctx.Request.Url.AbsolutePath.TrimEnd('/').ToLowerInvariant();
+            string method = ctx.Request.HttpMethod.ToUpperInvariant();
             string body;
             int code = 200;
             switch (path)
             {
                 case "/ping":
+                    body = JsonConvert.SerializeObject(_pingProvider());
+                    break;
                 case "/status":
                     body = JsonConvert.SerializeObject(_statusProvider());
                     break;
+                case "/shutdown":
+                    if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
+                    {
+                        string token = ReadField(ctx, "token");
+                        bool ok = _shutdownHandler != null && _shutdownHandler(token ?? "");
+                        code = ok ? 200 : 403;
+                        body = "{\"ok\":" + (ok ? "true" : "false") + (ok ? "" : ",\"error\":\"token invalid\"") + "}";
+                    }
+                    break;
+                case "/serial/pause":
+                case "/serial/resume":
+                    if (method != "POST") { code = 405; body = Err("method not allowed"); break; }
+                    {
+                        int houseSn = ReadIntField(ctx, "houseSn");
+                        bool isPause = path == "/serial/pause";
+                        var handler = isPause ? _serialPauseHandler : _serialResumeHandler;
+                        bool ok = handler != null && houseSn > 0 && handler(houseSn);
+                        code = ok ? 200 : 400;
+                        body = "{\"ok\":" + (ok ? "true" : "false") + ",\"houseSn\":" + houseSn + (ok ? "" : ",\"error\":\"bad houseSn or handler\"") + "}";
+                    }
+                    break;
                 default:
-                    code = 404; body = "{\"ok\":false,\"error\":\"not found\"}";
+                    code = 404; body = Err("not found");
                     break;
             }
             byte[] buf = Encoding.UTF8.GetBytes(body);
@@ -71,6 +113,30 @@ namespace IvfTl.ControlHost
             ctx.Response.OutputStream.Close();
         }
 
+        private static string Err(string msg) => "{\"ok\":false,\"error\":\"" + msg + "\"}";
+
+        /// <summary>读 POST JSON body 的某字符串字段(失败返回 null)。</summary>
+        private string ReadField(HttpListenerContext ctx, string field)
+        {
+            try
+            {
+                using (var sr = new StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding ?? Encoding.UTF8))
+                {
+                    string raw = sr.ReadToEnd();
+                    if (string.IsNullOrEmpty(raw)) return null;
+                    var jo = JObject.Parse(raw);
+                    return jo[field]?.ToString();
+                }
+            }
+            catch (Exception ex) { _log("解析请求体异常:" + ex.Message); return null; }
+        }
+
+        private int ReadIntField(HttpListenerContext ctx, string field)
+        {
+            string s = ReadField(ctx, field);
+            return int.TryParse(s, out int v) ? v : -1;
+        }
+
         public void Stop()
         {
             try { _cts?.Cancel(); _listener?.Stop(); _listener?.Close(); }

+ 87 - 1
ivf_tl_operate_2.0/control/ivf_tl_ControlHost/Program.cs

@@ -31,9 +31,14 @@ namespace IvfTl.ControlHost
                 Log4netHelper.WriteLog($"ControlHost 启动 port={hostArgs.Port} account={hostArgs.Account}");
 
                 // 2) 先起 HTTP(让 operate 能尽早探到"在启动中"),started=false。
+                //    阶段2:/status rich + /serial/pause|resume + /shutdown。
                 _http = new ControlHttpServer(
                     hostArgs.Port,
-                    BuildStatus,
+                    BuildStatus,        // /ping 轻量
+                    BuildRichStatus,    // /status 完整快照(§6 三块)
+                    HandleShutdown,     // /shutdown 受护栏停机
+                    HandleSerialPause,  // /serial/pause 借串口让路
+                    HandleSerialResume, // /serial/resume 归还恢复
                     msg => Log4netHelper.WriteLog(msg));
                 _http.Start();
 
@@ -128,5 +133,86 @@ namespace IvfTl.ControlHost
                 Started = _started
             };
         }
+
+        /// <summary>/status 完整快照:基础存活 + control 现有 MonitorSnapshot(阶段2 §6 三块已在快照内补全)。</summary>
+        private static object BuildRichStatus()
+        {
+            var head = BuildStatus();
+            object snapshot = null;
+            try { snapshot = AppData.Instance.GetMonitorSnapshot(); } catch { }
+            return new
+            {
+                ok = head.Ok,
+                pid = head.Pid,
+                tlSn = head.TlSn,
+                started = head.Started,
+                snapshot
+            };
+        }
+
+        /// <summary>/shutdown 受护栏停机:校验工程师口令(App.config engineerPwd,默认 tl13579),通过则安全停机。</summary>
+        private static bool HandleShutdown(string token)
+        {
+            string engineerPwd = "tl13579";
+            try
+            {
+                var cfg = System.Configuration.ConfigurationManager.AppSettings["engineerPwd"];
+                if (!string.IsNullOrEmpty(cfg)) engineerPwd = cfg;
+            }
+            catch { }
+            if (string.IsNullOrEmpty(token) || token != engineerPwd)
+            {
+                Log4netHelper.WriteLog("ControlHost: /shutdown 口令校验未通过,拒绝停机");
+                return false;
+            }
+            Log4netHelper.WriteLog("ControlHost: /shutdown 口令通过,执行安全停机");
+            // 异步触发,先把 HTTP 200 回给 operate,再停机退出。
+            System.Threading.Tasks.Task.Run(() =>
+            {
+                try { System.Threading.Thread.Sleep(300); SafeShutdown(); }
+                catch (Exception ex) { Log4netHelper.WriteLog("ControlHost: 停机异常", ex); _exitEvent?.Set(); }
+            });
+            return true;
+        }
+
+        /// <summary>统一安全停机:关相机/串口句柄(ShutdownAll)→ Set 退出事件(主线程 finally 停 HTTP+释放 Mutex)。</summary>
+        private static void SafeShutdown()
+        {
+            try
+            {
+                IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.ShutdownAll();
+                Log4netHelper.WriteLog("ControlHost: HardwareAccessLayer.ShutdownAll 完成");
+            }
+            catch (Exception ex) { Log4netHelper.WriteLog("ControlHost: ShutdownAll 异常", ex); }
+            _exitEvent?.Set();
+        }
+
+        /// <summary>/serial/pause 借串口:置该舱 HouseGate 暂停标志,control 采集节拍据此让路(不驱动电机)。</summary>
+        private static bool HandleSerialPause(int houseSn)
+        {
+            try
+            {
+                var gate = IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.GetHouseGate(houseSn);
+                if (gate == null) return false;
+                gate.PauseCapture();
+                Log4netHelper.WriteLog($"ControlHost: /serial/pause 舱{houseSn} 已让路(暂停采集)");
+                return true;
+            }
+            catch (Exception ex) { Log4netHelper.WriteLog($"ControlHost: /serial/pause 舱{houseSn} 异常", ex); return false; }
+        }
+
+        /// <summary>/serial/resume 归还:清该舱让路标志,恢复采集。</summary>
+        private static bool HandleSerialResume(int houseSn)
+        {
+            try
+            {
+                var gate = IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.GetHouseGate(houseSn);
+                if (gate == null) return false;
+                gate.ResumeCapture();
+                Log4netHelper.WriteLog($"ControlHost: /serial/resume 舱{houseSn} 已恢复采集");
+                return true;
+            }
+            catch (Exception ex) { Log4netHelper.WriteLog($"ControlHost: /serial/resume 舱{houseSn} 异常", ex); return false; }
+        }
     }
 }