Explorar el Código

feat(operate): 阶段2 — 监控页改跨进程读 /status + ControlClient + 受护栏停止命令

- 新增 Helpers/ControlClient.cs:operate 跨进程访问 control 本地HTTP(/status取快照、/ping探活、
  /shutdown受护栏停机、/serial/pause|resume借串口),全 try 兜底。拆分后 control 是独立进程,
  operate 进程内已无 control 实时 AppData,监控/调试必须经此客户端。
- ServiceMonitorViewModel.Refresh:数据源由同进程 ControlAppData.Instance.GetMonitorSnapshot()
  改为 ControlClient.GetStatusSnapshot()(HTTP);映射 §6 三块(WorkingType/ValveState/CapturePausedByGate);
  新增 ShutdownControlCommand(二次确认+工程师口令→/shutdown)。

验证(harness 实跑真 ControlClient.cs):GetStatusSnapshot 往返反序列化10舱(真温37℃+三块字段);
SerialPause(2)→快照House2 Paused=True→SerialResume,跨进程借串口让路实证。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie hace 3 días
padre
commit
bb8fadf170

+ 82 - 0
ivf_tl_operate_2.0/ivf_tl_Operate/Helpers/ControlClient.cs

@@ -0,0 +1,82 @@
+using System;
+using System.Configuration;
+using System.Net.Http;
+using System.Text;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using MonitorSnapshot = ivf_tl_Control.MonitorSnapshot;
+
+namespace ivf_tl_Operate.Helpers
+{
+    /// <summary>
+    /// 阶段2:operate 跨进程访问独立 control 的本地 HTTP 客户端。
+    /// 拆分后 operate 进程内已无 control 的实时 AppData,监控页/调试页改经此客户端读 /status、
+    /// 借串口 /serial/pause|resume、受护栏停止 /shutdown。全部 try 兜底,失败返回 null/false 不抛。
+    /// </summary>
+    public static class ControlClient
+    {
+        private static readonly HttpClient _http = new HttpClient { Timeout = TimeSpan.FromSeconds(3) };
+
+        public static int Port =>
+            int.TryParse(ConfigurationManager.AppSettings["controlPort"], out var p) ? p : 38080;
+
+        private static string BaseUrl => $"http://127.0.0.1:{Port}";
+
+        /// <summary>GET /status → 取回 control 的 MonitorSnapshot(失败返回 null,监控页据此显示"未托管")。</summary>
+        public static MonitorSnapshot GetStatusSnapshot()
+        {
+            try
+            {
+                var resp = _http.GetAsync($"{BaseUrl}/status").GetAwaiter().GetResult();
+                if (!resp.IsSuccessStatusCode) return null;
+                string json = resp.Content.ReadAsStringAsync().GetAwaiter().GetResult();
+                var jo = JObject.Parse(json);
+                var snapTok = jo["snapshot"];
+                if (snapTok == null || snapTok.Type == JTokenType.Null) return null;
+                return snapTok.ToObject<MonitorSnapshot>();
+            }
+            catch { return null; }
+        }
+
+        /// <summary>control 是否存活(GET /ping)。</summary>
+        public static bool IsAlive()
+        {
+            try
+            {
+                var resp = _http.GetAsync($"{BaseUrl}/ping").GetAwaiter().GetResult();
+                return resp.IsSuccessStatusCode;
+            }
+            catch { return false; }
+        }
+
+        /// <summary>受护栏停止:POST /shutdown {token}。token=工程师口令,control 端校验。成功返回 true。</summary>
+        public static bool Shutdown(string token)
+        {
+            try
+            {
+                var resp = _http.PostAsync($"{BaseUrl}/shutdown", JsonBody(new { token })).GetAwaiter().GetResult();
+                return resp.IsSuccessStatusCode;
+            }
+            catch { return false; }
+        }
+
+        /// <summary>调试借串口:POST /serial/pause {houseSn} → control 让出该舱采集。成功返回 true。</summary>
+        public static bool SerialPause(int houseSn) => PostSerial("pause", houseSn);
+
+        /// <summary>调试归还:POST /serial/resume {houseSn} → control 恢复该舱采集。</summary>
+        public static bool SerialResume(int houseSn) => PostSerial("resume", houseSn);
+
+        private static bool PostSerial(string action, int houseSn)
+        {
+            try
+            {
+                var resp = _http.PostAsync($"{BaseUrl}/serial/{action}", JsonBody(new { houseSn })).GetAwaiter().GetResult();
+                return resp.IsSuccessStatusCode;
+            }
+            catch { return false; }
+        }
+
+        private static StringContent JsonBody(object o) =>
+            new StringContent(JsonConvert.SerializeObject(o), Encoding.UTF8, "application/json");
+    }
+}

+ 34 - 1
ivf_tl_operate_2.0/ivf_tl_Operate/ViewModel/ServiceMonitorViewModel.cs

@@ -4,6 +4,7 @@ using System.Windows;
 using System.Windows.Media;
 using System.Windows.Threading;
 using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
 using MonitorSnapshot = ivf_tl_Control.MonitorSnapshot;
 using ControlAppData = ivf_tl_Control.AppData;
 
@@ -74,7 +75,8 @@ namespace ivf_tl_Operate.ViewModel
             try
             {
                 MonitorSnapshot snap = null;
-                try { snap = ControlAppData.Instance != null ? ControlAppData.Instance.GetMonitorSnapshot() : null; } catch { snap = null; }
+                // 阶段2:拆分后 control 是独立进程,改经本地 HTTP /status 跨进程读快照(不再读同进程 AppData)。
+                try { snap = ivf_tl_Operate.Helpers.ControlClient.GetStatusSnapshot(); } catch { snap = null; }
 
                 if (snap == null)
                 {
@@ -149,6 +151,11 @@ namespace ivf_tl_Operate.ViewModel
                         ComBrush = (h.ComState == "已连接") ? Green : Red,
                         CcdText = (h.CcdState == "正常" && !h.CcdError) ? "正常" : "异常",
                         CcdBrush = (h.CcdState == "正常" && !h.CcdError) ? Green : Red,
+                        // 阶段2 §6 三块:实时活动 / 阀态 / 串口借用让路
+                        WorkingType = string.IsNullOrEmpty(h.WorkingType) ? "—" : h.WorkingType,
+                        ValveState = string.IsNullOrEmpty(h.ValveState) ? "—" : h.ValveState,
+                        CapturePausedText = h.CapturePausedByGate ? "借用中(让路)" : "采集中",
+                        CapturePausedBrush = h.CapturePausedByGate ? Amber : Green,
                     });
                 }
             }
@@ -158,6 +165,27 @@ namespace ivf_tl_Operate.ViewModel
             }
         }
 
+        /// <summary>
+        /// 阶段2 §5.4 受护栏整体停止 control:二次确认 + 工程师口令 → POST /shutdown。
+        /// control 端校验口令(tl13579,App.config engineerPwd)通过才安全停机。
+        /// </summary>
+        [RelayCommand]
+        private void ShutdownControl()
+        {
+            var c1 = MessageBox.Show(
+                "确定要【整体停止 control 后台采集】吗?\n停止后机器将不再被驱动,需重启 operate 才会重新拉起 control。",
+                "受护栏停止 · 二次确认", MessageBoxButton.YesNo, MessageBoxImage.Warning);
+            if (c1 != MessageBoxResult.Yes) return;
+            string pwd = Microsoft.VisualBasic.Interaction.InputBox(
+                "请输入工程师口令以确认整体停止 control:", "受护栏停止 · 工程师口令", "");
+            if (string.IsNullOrEmpty(pwd)) return;
+            bool ok = ivf_tl_Operate.Helpers.ControlClient.Shutdown(pwd);
+            MessageBox.Show(
+                ok ? "已发送停机指令,control 正在安全停机(关相机/串口句柄并退出)。"
+                   : "停机失败:工程师口令错误,或 control 未连接。",
+                "受护栏停止", MessageBoxButton.OK, ok ? MessageBoxImage.Information : MessageBoxImage.Error);
+        }
+
         private static bool IsStale(DateTime? t) => t == null || (DateTime.Now - t.Value).TotalSeconds > StaleSeconds;
 
         private static Brush LinkBrush(DateTime? t)
@@ -188,5 +216,10 @@ namespace ivf_tl_Operate.ViewModel
         public Brush ComBrush { get; set; }
         public string CcdText { get; set; }
         public Brush CcdBrush { get; set; }
+        // 阶段2 §6 三块补充
+        public string WorkingType { get; set; }
+        public string ValveState { get; set; }
+        public string CapturePausedText { get; set; }
+        public Brush CapturePausedBrush { get; set; }
     }
 }