ServiceMonitorViewModel.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. using System;
  2. using System.Collections.ObjectModel;
  3. using System.Windows;
  4. using System.Windows.Media;
  5. using System.Windows.Threading;
  6. using CommunityToolkit.Mvvm.ComponentModel;
  7. using CommunityToolkit.Mvvm.Input;
  8. using MonitorSnapshot = ivf_tl_Control.MonitorSnapshot;
  9. using ControlAppData = ivf_tl_Control.AppData;
  10. namespace ivf_tl_Operate.ViewModel
  11. {
  12. /// <summary>
  13. /// M5-03-3/4:只读「服务监控」页 ViewModel(需求 7 只读服务监控、需求 10 链路健康 + 不假装实时)。
  14. ///
  15. /// 纯只读:仅定时调用同进程 <see cref="ivf_tl_Control.AppData.Instance"/> 的
  16. /// <see cref="AppData.GetMonitorSnapshot"/> 拉取只读快照并映射到展示属性,
  17. /// **不持有任何可控制 control 的引用、页面无任何写控件/下发按钮**(约束 4 只读边界)。
  18. ///
  19. /// 刷新:DispatcherTimer 默认 2s(阈值/延迟具体值 [D10] 占位,留 M7 校准)。
  20. /// 链路健康按「最后成功通讯时间」是否过期上色:绿=正常、红=失联/过期。
  21. /// [M7] 各字段运行时取值、断线红/恢复绿需运行环境验证(本地不可构建/运行)。
  22. /// </summary>
  23. public partial class ServiceMonitorViewModel : ObservableObject, IDisposable
  24. {
  25. private readonly DispatcherTimer _timer;
  26. // [D10] 失联判定阈值占位:超过该秒数未成功通讯即视为过期/失联(留 M7 按实时指标校准)。
  27. private const int StaleSeconds = 30;
  28. public ServiceMonitorViewModel()
  29. {
  30. Houses = new ObservableCollection<HouseMonitorRowVm>();
  31. Faults = new ObservableCollection<HouseFaultRowVm>();
  32. Refresh();
  33. _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
  34. _timer.Tick += (s, e) => Refresh();
  35. _timer.Start();
  36. }
  37. // —— 顶部链路健康 ——
  38. [ObservableProperty] private string snapshotTime;
  39. [ObservableProperty] private bool controlHosted;
  40. [ObservableProperty] private string controlHostedText;
  41. [ObservableProperty] private Brush controlHostedBrush;
  42. [ObservableProperty] private string mqttText;
  43. [ObservableProperty] private Brush mqttBrush;
  44. [ObservableProperty] private string mqttLastOk;
  45. [ObservableProperty] private string serverUrl;
  46. [ObservableProperty] private string httpText;
  47. [ObservableProperty] private Brush httpBrush;
  48. [ObservableProperty] private string httpLastOk;
  49. [ObservableProperty] private string kafkaText;
  50. [ObservableProperty] private Brush kafkaBrush;
  51. [ObservableProperty] private string kafkaLastOk;
  52. [ObservableProperty] private string uploadQueueText;
  53. [ObservableProperty] private Brush uploadQueueBrush;
  54. [ObservableProperty] private string diskText;
  55. [ObservableProperty] private Brush diskBrush;
  56. // —— 舱故障(H-08:读 /status Faults,红色高亮展示)——
  57. /// <summary>是否存在舱故障(内部逻辑用)。</summary>
  58. [ObservableProperty] private bool hasFaults;
  59. /// <summary>故障区标题文案(有故障="舱故障(N)" / 无故障="舱室均正常")。</summary>
  60. [ObservableProperty] private string faultSummaryText;
  61. /// <summary>故障区标题色(有故障=红 / 无故障=绿)。</summary>
  62. [ObservableProperty] private Brush faultSummaryBrush;
  63. /// <summary>故障列表显隐(有故障可见)。直接给 Visibility,避免依赖未注册的布尔转换器。</summary>
  64. [ObservableProperty] private Visibility faultListVisibility = Visibility.Collapsed;
  65. /// <summary>"无故障"绿条显隐(无故障可见)。</summary>
  66. [ObservableProperty] private Visibility noFaultVisibility = Visibility.Visible;
  67. public ObservableCollection<HouseMonitorRowVm> Houses { get; }
  68. /// <summary>舱故障只读行(启动排除 + 运行期突发,来源 snap.Faults)。</summary>
  69. public ObservableCollection<HouseFaultRowVm> Faults { get; }
  70. private static readonly Brush Green = new SolidColorBrush(Color.FromRgb(0x2E, 0xA0, 0x43));
  71. private static readonly Brush Red = new SolidColorBrush(Color.FromRgb(0xD0, 0x32, 0x2D));
  72. private static readonly Brush Gray = new SolidColorBrush(Color.FromRgb(0x95, 0x95, 0x96));
  73. private static readonly Brush Amber = new SolidColorBrush(Color.FromRgb(0xE0, 0x9A, 0x12));
  74. /// <summary>拉取只读快照并刷新展示属性。任何异常被吞掉,避免监控页崩。</summary>
  75. public void Refresh()
  76. {
  77. try
  78. {
  79. MonitorSnapshot snap = null;
  80. // 阶段2:拆分后 control 是独立进程,改经本地 HTTP /status 跨进程读快照(不再读同进程 AppData)。
  81. try { snap = ivf_tl_Operate.Helpers.ControlClient.GetStatusSnapshot(); } catch { snap = null; }
  82. if (snap == null)
  83. {
  84. ControlHosted = false;
  85. ControlHostedText = "后台未托管 / 取数失败";
  86. ControlHostedBrush = Gray;
  87. SnapshotTime = "—";
  88. return;
  89. }
  90. SnapshotTime = snap.SnapshotAt.ToString("yyyy-MM-dd HH:mm:ss");
  91. ControlHosted = snap.ControlHosted;
  92. ControlHostedText = snap.ControlHosted ? "后台服务已托管" : "后台未托管";
  93. ControlHostedBrush = snap.ControlHosted ? Green : Red;
  94. // MQTT 链路
  95. MqttText = snap.MqttConnected ? "已连接" : "未连接";
  96. MqttBrush = snap.MqttConnected ? Green : Red;
  97. MqttLastOk = FmtOk(snap.LastMqttOkAt);
  98. // 服务器 / HTTP 链路(按最后成功时间过期判定)
  99. ServerUrl = string.IsNullOrEmpty(snap.ServerUrl) ? "—" : snap.ServerUrl;
  100. HttpBrush = LinkBrush(snap.LastHttpOkAt);
  101. HttpText = snap.LastHttpOkAt == null ? "未知" : (IsStale(snap.LastHttpOkAt) ? "可能失联" : "正常");
  102. HttpLastOk = FmtOk(snap.LastHttpOkAt);
  103. // Kafka / 图片上传链路
  104. KafkaBrush = LinkBrush(snap.LastKafkaOkAt);
  105. KafkaText = snap.LastKafkaOkAt == null ? "未知" : (IsStale(snap.LastKafkaOkAt) ? "可能失联" : "正常");
  106. KafkaLastOk = FmtOk(snap.LastKafkaOkAt);
  107. // 上传队列(内存缓存 + 落盘待传)
  108. int pending = snap.ImageCacheCount + snap.PendingDiskImageCount;
  109. UploadQueueText = snap.ImageBacklogAlarm
  110. ? $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}(堆积告警>{snap.PendingImageAlarmThreshold})"
  111. : $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}";
  112. // M5-04-3:断网累积兜底——以 control 侧权威 ImageBacklogAlarm 判定红色显著提示(不静默堆积);
  113. // 未达阈值但有待传时琥珀提示。[D10] 阈值由 control PendingImageAlarmThreshold 提供,留 M7 校准。
  114. UploadQueueBrush = snap.ImageBacklogAlarm ? Red : (pending == 0 ? Green : Amber);
  115. // 磁盘
  116. if (string.IsNullOrEmpty(snap.DiskPath))
  117. {
  118. DiskText = "—";
  119. DiskBrush = Gray;
  120. }
  121. else if (!snap.DiskExist)
  122. {
  123. DiskText = $"{snap.DiskPath}: 盘不存在";
  124. DiskBrush = Red;
  125. }
  126. else
  127. {
  128. DiskText = $"{snap.DiskPath}: 剩余 {snap.DiskFreeGb:0.0} GB";
  129. DiskBrush = snap.DiskFreeGb < 10 ? Red : (snap.DiskFreeGb < 30 ? Amber : Green);
  130. }
  131. // 舱室
  132. Houses.Clear();
  133. foreach (var h in snap.Houses)
  134. {
  135. Houses.Add(new HouseMonitorRowVm
  136. {
  137. HouseSn = h.HouseSn,
  138. PortName = string.IsNullOrEmpty(h.PortName) ? "—" : h.PortName,
  139. RunState = string.IsNullOrEmpty(h.RunState) ? "—" : h.RunState,
  140. Temperature = $"{h.Temperature:0.0}",
  141. Pressure = $"{h.Pressure:0.0}",
  142. StateText = string.IsNullOrEmpty(h.HouseState) ? "—" : h.HouseState,
  143. StateBrush = (h.HouseState != null && h.HouseState.Contains("关")) ? Green : Red,
  144. ComText = string.IsNullOrEmpty(h.ComState) ? "—" : h.ComState,
  145. ComBrush = (h.ComState == "已连接") ? Green : Red,
  146. CcdText = (h.CcdState == "正常" && !h.CcdError) ? "正常" : "异常",
  147. CcdBrush = (h.CcdState == "正常" && !h.CcdError) ? Green : Red,
  148. // 阶段2 §6 三块:实时活动 / 阀态 / 串口借用让路
  149. WorkingType = string.IsNullOrEmpty(h.WorkingType) ? "—" : h.WorkingType,
  150. ValveState = string.IsNullOrEmpty(h.ValveState) ? "—" : h.ValveState,
  151. CapturePausedText = h.CapturePausedByGate ? "借用中(让路)" : "采集中",
  152. CapturePausedBrush = h.CapturePausedByGate ? Amber : Green,
  153. });
  154. }
  155. // 舱故障(H-08):启动排除 + 运行期突发,红色高亮;无故障显示绿色"舱室均正常"。
  156. Faults.Clear();
  157. foreach (var vm in BuildFaultRows(snap.Faults)) Faults.Add(vm);
  158. HasFaults = Faults.Count > 0;
  159. FaultSummaryText = HasFaults ? $"舱故障({Faults.Count})" : "舱室均正常,无故障";
  160. FaultSummaryBrush = HasFaults ? Red : Green;
  161. FaultListVisibility = HasFaults ? Visibility.Visible : Visibility.Collapsed;
  162. NoFaultVisibility = HasFaults ? Visibility.Collapsed : Visibility.Visible;
  163. }
  164. catch
  165. {
  166. // 监控页只读,取数异常不抛
  167. }
  168. }
  169. /// <summary>
  170. /// 阶段2 §5.4 受护栏整体停止 control:二次确认 + 工程师口令 → POST /shutdown。
  171. /// control 端校验口令(tl13579,App.config engineerPwd)通过才安全停机。
  172. /// </summary>
  173. [RelayCommand]
  174. private void ShutdownControl()
  175. {
  176. var c1 = MessageBox.Show(
  177. "确定要【整体停止 control 后台采集】吗?\n停止后机器将不再被驱动,需重启 operate 才会重新拉起 control。",
  178. "受护栏停止 · 二次确认", MessageBoxButton.YesNo, MessageBoxImage.Warning);
  179. if (c1 != MessageBoxResult.Yes) return;
  180. string pwd = Microsoft.VisualBasic.Interaction.InputBox(
  181. "请输入工程师口令以确认整体停止 control:", "受护栏停止 · 工程师口令", "");
  182. if (string.IsNullOrEmpty(pwd)) return;
  183. bool ok = ivf_tl_Operate.Helpers.ControlClient.Shutdown(pwd);
  184. MessageBox.Show(
  185. ok ? "已发送停机指令,control 正在安全停机(关相机/串口句柄并退出)。"
  186. : "停机失败:工程师口令错误,或 control 未连接。",
  187. "受护栏停止", MessageBoxButton.OK, ok ? MessageBoxImage.Information : MessageBoxImage.Error);
  188. }
  189. private static bool IsStale(DateTime? t) => t == null || (DateTime.Now - t.Value).TotalSeconds > StaleSeconds;
  190. private static Brush LinkBrush(DateTime? t)
  191. {
  192. if (t == null) return Gray; // 未知(尚未成功通讯)
  193. return IsStale(t) ? Red : Green; // 过期=红 / 新鲜=绿
  194. }
  195. private static string FmtOk(DateTime? t) => t == null ? "—" : t.Value.ToString("HH:mm:ss");
  196. /// <summary>
  197. /// 舱故障行映射(H-08):snap.Faults(control 透出的 HouseFaultRow) → 展示行。
  198. /// 文案走纯静态 <see cref="ServiceMonitorFaultMapper"/>(可独立 harness/单测),颜色 Brush 在此加。
  199. /// public static 便于不起 WPF 外壳即可验证有故障场景的中文/时间/隔离映射。
  200. /// </summary>
  201. public static System.Collections.Generic.List<HouseFaultRowVm> BuildFaultRows(
  202. System.Collections.Generic.IEnumerable<ivf_tl_Control.HouseFaultRow> faults)
  203. {
  204. var list = new System.Collections.Generic.List<HouseFaultRowVm>();
  205. if (faults == null) return list;
  206. foreach (var f in faults)
  207. {
  208. list.Add(new HouseFaultRowVm
  209. {
  210. HouseText = ServiceMonitorFaultMapper.HouseText(f.HouseSn),
  211. FaultTypeText = ServiceMonitorFaultMapper.FaultTypeZh(f.FaultType),
  212. Reason = string.IsNullOrEmpty(f.Reason) ? "—" : f.Reason,
  213. Stage = string.IsNullOrEmpty(f.Stage) ? "—" : f.Stage,
  214. AtText = ServiceMonitorFaultMapper.AtText(f.At),
  215. IsolatedText = ServiceMonitorFaultMapper.IsolatedText(f.Isolated),
  216. IsolatedBrush = f.Isolated ? Red : Amber,
  217. });
  218. }
  219. return list;
  220. }
  221. public void Dispose()
  222. {
  223. try { _timer?.Stop(); } catch { }
  224. }
  225. }
  226. /// <summary>单舱室只读展示行。</summary>
  227. public class HouseMonitorRowVm
  228. {
  229. public int HouseSn { get; set; }
  230. public string PortName { get; set; }
  231. public string RunState { get; set; }
  232. public string Temperature { get; set; }
  233. public string Pressure { get; set; }
  234. public string StateText { get; set; }
  235. public Brush StateBrush { get; set; }
  236. public string ComText { get; set; }
  237. public Brush ComBrush { get; set; }
  238. public string CcdText { get; set; }
  239. public Brush CcdBrush { get; set; }
  240. // 阶段2 §6 三块补充
  241. public string WorkingType { get; set; }
  242. public string ValveState { get; set; }
  243. public string CapturePausedText { get; set; }
  244. public Brush CapturePausedBrush { get; set; }
  245. }
  246. /// <summary>单条舱故障只读展示行(H-08)。</summary>
  247. public class HouseFaultRowVm
  248. {
  249. public string HouseText { get; set; }
  250. public string FaultTypeText { get; set; }
  251. public string Reason { get; set; }
  252. public string Stage { get; set; }
  253. public string AtText { get; set; }
  254. public string IsolatedText { get; set; }
  255. public Brush IsolatedBrush { get; set; }
  256. }
  257. }