ServiceMonitorViewModel.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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. Refresh();
  32. _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
  33. _timer.Tick += (s, e) => Refresh();
  34. _timer.Start();
  35. }
  36. // —— 顶部链路健康 ——
  37. [ObservableProperty] private string snapshotTime;
  38. [ObservableProperty] private bool controlHosted;
  39. [ObservableProperty] private string controlHostedText;
  40. [ObservableProperty] private Brush controlHostedBrush;
  41. [ObservableProperty] private string mqttText;
  42. [ObservableProperty] private Brush mqttBrush;
  43. [ObservableProperty] private string mqttLastOk;
  44. [ObservableProperty] private string serverUrl;
  45. [ObservableProperty] private string httpText;
  46. [ObservableProperty] private Brush httpBrush;
  47. [ObservableProperty] private string httpLastOk;
  48. [ObservableProperty] private string kafkaText;
  49. [ObservableProperty] private Brush kafkaBrush;
  50. [ObservableProperty] private string kafkaLastOk;
  51. [ObservableProperty] private string uploadQueueText;
  52. [ObservableProperty] private Brush uploadQueueBrush;
  53. [ObservableProperty] private string diskText;
  54. [ObservableProperty] private Brush diskBrush;
  55. public ObservableCollection<HouseMonitorRowVm> Houses { get; }
  56. private static readonly Brush Green = new SolidColorBrush(Color.FromRgb(0x2E, 0xA0, 0x43));
  57. private static readonly Brush Red = new SolidColorBrush(Color.FromRgb(0xD0, 0x32, 0x2D));
  58. private static readonly Brush Gray = new SolidColorBrush(Color.FromRgb(0x95, 0x95, 0x96));
  59. private static readonly Brush Amber = new SolidColorBrush(Color.FromRgb(0xE0, 0x9A, 0x12));
  60. /// <summary>拉取只读快照并刷新展示属性。任何异常被吞掉,避免监控页崩。</summary>
  61. public void Refresh()
  62. {
  63. try
  64. {
  65. MonitorSnapshot snap = null;
  66. // 阶段2:拆分后 control 是独立进程,改经本地 HTTP /status 跨进程读快照(不再读同进程 AppData)。
  67. try { snap = ivf_tl_Operate.Helpers.ControlClient.GetStatusSnapshot(); } catch { snap = null; }
  68. if (snap == null)
  69. {
  70. ControlHosted = false;
  71. ControlHostedText = "后台未托管 / 取数失败";
  72. ControlHostedBrush = Gray;
  73. SnapshotTime = "—";
  74. return;
  75. }
  76. SnapshotTime = snap.SnapshotAt.ToString("yyyy-MM-dd HH:mm:ss");
  77. ControlHosted = snap.ControlHosted;
  78. ControlHostedText = snap.ControlHosted ? "后台服务已托管" : "后台未托管";
  79. ControlHostedBrush = snap.ControlHosted ? Green : Red;
  80. // MQTT 链路
  81. MqttText = snap.MqttConnected ? "已连接" : "未连接";
  82. MqttBrush = snap.MqttConnected ? Green : Red;
  83. MqttLastOk = FmtOk(snap.LastMqttOkAt);
  84. // 服务器 / HTTP 链路(按最后成功时间过期判定)
  85. ServerUrl = string.IsNullOrEmpty(snap.ServerUrl) ? "—" : snap.ServerUrl;
  86. HttpBrush = LinkBrush(snap.LastHttpOkAt);
  87. HttpText = snap.LastHttpOkAt == null ? "未知" : (IsStale(snap.LastHttpOkAt) ? "可能失联" : "正常");
  88. HttpLastOk = FmtOk(snap.LastHttpOkAt);
  89. // Kafka / 图片上传链路
  90. KafkaBrush = LinkBrush(snap.LastKafkaOkAt);
  91. KafkaText = snap.LastKafkaOkAt == null ? "未知" : (IsStale(snap.LastKafkaOkAt) ? "可能失联" : "正常");
  92. KafkaLastOk = FmtOk(snap.LastKafkaOkAt);
  93. // 上传队列(内存缓存 + 落盘待传)
  94. int pending = snap.ImageCacheCount + snap.PendingDiskImageCount;
  95. UploadQueueText = snap.ImageBacklogAlarm
  96. ? $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}(堆积告警>{snap.PendingImageAlarmThreshold})"
  97. : $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}";
  98. // M5-04-3:断网累积兜底——以 control 侧权威 ImageBacklogAlarm 判定红色显著提示(不静默堆积);
  99. // 未达阈值但有待传时琥珀提示。[D10] 阈值由 control PendingImageAlarmThreshold 提供,留 M7 校准。
  100. UploadQueueBrush = snap.ImageBacklogAlarm ? Red : (pending == 0 ? Green : Amber);
  101. // 磁盘
  102. if (string.IsNullOrEmpty(snap.DiskPath))
  103. {
  104. DiskText = "—";
  105. DiskBrush = Gray;
  106. }
  107. else if (!snap.DiskExist)
  108. {
  109. DiskText = $"{snap.DiskPath}: 盘不存在";
  110. DiskBrush = Red;
  111. }
  112. else
  113. {
  114. DiskText = $"{snap.DiskPath}: 剩余 {snap.DiskFreeGb:0.0} GB";
  115. DiskBrush = snap.DiskFreeGb < 10 ? Red : (snap.DiskFreeGb < 30 ? Amber : Green);
  116. }
  117. // 舱室
  118. Houses.Clear();
  119. foreach (var h in snap.Houses)
  120. {
  121. Houses.Add(new HouseMonitorRowVm
  122. {
  123. HouseSn = h.HouseSn,
  124. PortName = string.IsNullOrEmpty(h.PortName) ? "—" : h.PortName,
  125. RunState = string.IsNullOrEmpty(h.RunState) ? "—" : h.RunState,
  126. Temperature = $"{h.Temperature:0.0}",
  127. Pressure = $"{h.Pressure:0.0}",
  128. StateText = string.IsNullOrEmpty(h.HouseState) ? "—" : h.HouseState,
  129. StateBrush = (h.HouseState != null && h.HouseState.Contains("关")) ? Green : Red,
  130. ComText = string.IsNullOrEmpty(h.ComState) ? "—" : h.ComState,
  131. ComBrush = (h.ComState == "已连接") ? Green : Red,
  132. CcdText = (h.CcdState == "正常" && !h.CcdError) ? "正常" : "异常",
  133. CcdBrush = (h.CcdState == "正常" && !h.CcdError) ? Green : Red,
  134. // 阶段2 §6 三块:实时活动 / 阀态 / 串口借用让路
  135. WorkingType = string.IsNullOrEmpty(h.WorkingType) ? "—" : h.WorkingType,
  136. ValveState = string.IsNullOrEmpty(h.ValveState) ? "—" : h.ValveState,
  137. CapturePausedText = h.CapturePausedByGate ? "借用中(让路)" : "采集中",
  138. CapturePausedBrush = h.CapturePausedByGate ? Amber : Green,
  139. });
  140. }
  141. }
  142. catch
  143. {
  144. // 监控页只读,取数异常不抛
  145. }
  146. }
  147. /// <summary>
  148. /// 阶段2 §5.4 受护栏整体停止 control:二次确认 + 工程师口令 → POST /shutdown。
  149. /// control 端校验口令(tl13579,App.config engineerPwd)通过才安全停机。
  150. /// </summary>
  151. [RelayCommand]
  152. private void ShutdownControl()
  153. {
  154. var c1 = MessageBox.Show(
  155. "确定要【整体停止 control 后台采集】吗?\n停止后机器将不再被驱动,需重启 operate 才会重新拉起 control。",
  156. "受护栏停止 · 二次确认", MessageBoxButton.YesNo, MessageBoxImage.Warning);
  157. if (c1 != MessageBoxResult.Yes) return;
  158. string pwd = Microsoft.VisualBasic.Interaction.InputBox(
  159. "请输入工程师口令以确认整体停止 control:", "受护栏停止 · 工程师口令", "");
  160. if (string.IsNullOrEmpty(pwd)) return;
  161. bool ok = ivf_tl_Operate.Helpers.ControlClient.Shutdown(pwd);
  162. MessageBox.Show(
  163. ok ? "已发送停机指令,control 正在安全停机(关相机/串口句柄并退出)。"
  164. : "停机失败:工程师口令错误,或 control 未连接。",
  165. "受护栏停止", MessageBoxButton.OK, ok ? MessageBoxImage.Information : MessageBoxImage.Error);
  166. }
  167. private static bool IsStale(DateTime? t) => t == null || (DateTime.Now - t.Value).TotalSeconds > StaleSeconds;
  168. private static Brush LinkBrush(DateTime? t)
  169. {
  170. if (t == null) return Gray; // 未知(尚未成功通讯)
  171. return IsStale(t) ? Red : Green; // 过期=红 / 新鲜=绿
  172. }
  173. private static string FmtOk(DateTime? t) => t == null ? "—" : t.Value.ToString("HH:mm:ss");
  174. public void Dispose()
  175. {
  176. try { _timer?.Stop(); } catch { }
  177. }
  178. }
  179. /// <summary>单舱室只读展示行。</summary>
  180. public class HouseMonitorRowVm
  181. {
  182. public int HouseSn { get; set; }
  183. public string PortName { get; set; }
  184. public string RunState { get; set; }
  185. public string Temperature { get; set; }
  186. public string Pressure { get; set; }
  187. public string StateText { get; set; }
  188. public Brush StateBrush { get; set; }
  189. public string ComText { get; set; }
  190. public Brush ComBrush { get; set; }
  191. public string CcdText { get; set; }
  192. public Brush CcdBrush { get; set; }
  193. // 阶段2 §6 三块补充
  194. public string WorkingType { get; set; }
  195. public string ValveState { get; set; }
  196. public string CapturePausedText { get; set; }
  197. public Brush CapturePausedBrush { get; set; }
  198. }
  199. }