ServiceMonitorViewModel.cs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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 MonitorSnapshot = ivf_tl_Control.MonitorSnapshot;
  8. using ControlAppData = ivf_tl_Control.AppData;
  9. namespace ivf_tl_Operate.ViewModel
  10. {
  11. /// <summary>
  12. /// M5-03-3/4:只读「服务监控」页 ViewModel(需求 7 只读服务监控、需求 10 链路健康 + 不假装实时)。
  13. ///
  14. /// 纯只读:仅定时调用同进程 <see cref="ivf_tl_Control.AppData.Instance"/> 的
  15. /// <see cref="AppData.GetMonitorSnapshot"/> 拉取只读快照并映射到展示属性,
  16. /// **不持有任何可控制 control 的引用、页面无任何写控件/下发按钮**(约束 4 只读边界)。
  17. ///
  18. /// 刷新:DispatcherTimer 默认 2s(阈值/延迟具体值 [D10] 占位,留 M7 校准)。
  19. /// 链路健康按「最后成功通讯时间」是否过期上色:绿=正常、红=失联/过期。
  20. /// [M7] 各字段运行时取值、断线红/恢复绿需运行环境验证(本地不可构建/运行)。
  21. /// </summary>
  22. public partial class ServiceMonitorViewModel : ObservableObject, IDisposable
  23. {
  24. private readonly DispatcherTimer _timer;
  25. // [D10] 失联判定阈值占位:超过该秒数未成功通讯即视为过期/失联(留 M7 按实时指标校准)。
  26. private const int StaleSeconds = 30;
  27. public ServiceMonitorViewModel()
  28. {
  29. Houses = new ObservableCollection<HouseMonitorRowVm>();
  30. Refresh();
  31. _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
  32. _timer.Tick += (s, e) => Refresh();
  33. _timer.Start();
  34. }
  35. // —— 顶部链路健康 ——
  36. [ObservableProperty] private string snapshotTime;
  37. [ObservableProperty] private bool controlHosted;
  38. [ObservableProperty] private string controlHostedText;
  39. [ObservableProperty] private Brush controlHostedBrush;
  40. [ObservableProperty] private string mqttText;
  41. [ObservableProperty] private Brush mqttBrush;
  42. [ObservableProperty] private string mqttLastOk;
  43. [ObservableProperty] private string serverUrl;
  44. [ObservableProperty] private string httpText;
  45. [ObservableProperty] private Brush httpBrush;
  46. [ObservableProperty] private string httpLastOk;
  47. [ObservableProperty] private string kafkaText;
  48. [ObservableProperty] private Brush kafkaBrush;
  49. [ObservableProperty] private string kafkaLastOk;
  50. [ObservableProperty] private string uploadQueueText;
  51. [ObservableProperty] private Brush uploadQueueBrush;
  52. [ObservableProperty] private string diskText;
  53. [ObservableProperty] private Brush diskBrush;
  54. public ObservableCollection<HouseMonitorRowVm> Houses { get; }
  55. private static readonly Brush Green = new SolidColorBrush(Color.FromRgb(0x2E, 0xA0, 0x43));
  56. private static readonly Brush Red = new SolidColorBrush(Color.FromRgb(0xD0, 0x32, 0x2D));
  57. private static readonly Brush Gray = new SolidColorBrush(Color.FromRgb(0x95, 0x95, 0x96));
  58. private static readonly Brush Amber = new SolidColorBrush(Color.FromRgb(0xE0, 0x9A, 0x12));
  59. /// <summary>拉取只读快照并刷新展示属性。任何异常被吞掉,避免监控页崩。</summary>
  60. public void Refresh()
  61. {
  62. try
  63. {
  64. MonitorSnapshot snap = null;
  65. try { snap = ControlAppData.Instance != null ? ControlAppData.Instance.GetMonitorSnapshot() : null; } catch { snap = null; }
  66. if (snap == null)
  67. {
  68. ControlHosted = false;
  69. ControlHostedText = "后台未托管 / 取数失败";
  70. ControlHostedBrush = Gray;
  71. SnapshotTime = "—";
  72. return;
  73. }
  74. SnapshotTime = snap.SnapshotAt.ToString("yyyy-MM-dd HH:mm:ss");
  75. ControlHosted = snap.ControlHosted;
  76. ControlHostedText = snap.ControlHosted ? "后台服务已托管" : "后台未托管";
  77. ControlHostedBrush = snap.ControlHosted ? Green : Red;
  78. // MQTT 链路
  79. MqttText = snap.MqttConnected ? "已连接" : "未连接";
  80. MqttBrush = snap.MqttConnected ? Green : Red;
  81. MqttLastOk = FmtOk(snap.LastMqttOkAt);
  82. // 服务器 / HTTP 链路(按最后成功时间过期判定)
  83. ServerUrl = string.IsNullOrEmpty(snap.ServerUrl) ? "—" : snap.ServerUrl;
  84. HttpBrush = LinkBrush(snap.LastHttpOkAt);
  85. HttpText = snap.LastHttpOkAt == null ? "未知" : (IsStale(snap.LastHttpOkAt) ? "可能失联" : "正常");
  86. HttpLastOk = FmtOk(snap.LastHttpOkAt);
  87. // Kafka / 图片上传链路
  88. KafkaBrush = LinkBrush(snap.LastKafkaOkAt);
  89. KafkaText = snap.LastKafkaOkAt == null ? "未知" : (IsStale(snap.LastKafkaOkAt) ? "可能失联" : "正常");
  90. KafkaLastOk = FmtOk(snap.LastKafkaOkAt);
  91. // 上传队列(内存缓存 + 落盘待传)
  92. int pending = snap.ImageCacheCount + snap.PendingDiskImageCount;
  93. UploadQueueText = snap.ImageBacklogAlarm
  94. ? $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}(堆积告警>{snap.PendingImageAlarmThreshold})"
  95. : $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}";
  96. // M5-04-3:断网累积兜底——以 control 侧权威 ImageBacklogAlarm 判定红色显著提示(不静默堆积);
  97. // 未达阈值但有待传时琥珀提示。[D10] 阈值由 control PendingImageAlarmThreshold 提供,留 M7 校准。
  98. UploadQueueBrush = snap.ImageBacklogAlarm ? Red : (pending == 0 ? Green : Amber);
  99. // 磁盘
  100. if (string.IsNullOrEmpty(snap.DiskPath))
  101. {
  102. DiskText = "—";
  103. DiskBrush = Gray;
  104. }
  105. else if (!snap.DiskExist)
  106. {
  107. DiskText = $"{snap.DiskPath}: 盘不存在";
  108. DiskBrush = Red;
  109. }
  110. else
  111. {
  112. DiskText = $"{snap.DiskPath}: 剩余 {snap.DiskFreeGb:0.0} GB";
  113. DiskBrush = snap.DiskFreeGb < 10 ? Red : (snap.DiskFreeGb < 30 ? Amber : Green);
  114. }
  115. // 舱室
  116. Houses.Clear();
  117. foreach (var h in snap.Houses)
  118. {
  119. Houses.Add(new HouseMonitorRowVm
  120. {
  121. HouseSn = h.HouseSn,
  122. PortName = string.IsNullOrEmpty(h.PortName) ? "—" : h.PortName,
  123. RunState = string.IsNullOrEmpty(h.RunState) ? "—" : h.RunState,
  124. Temperature = $"{h.Temperature:0.0}",
  125. Pressure = $"{h.Pressure:0.0}",
  126. StateText = string.IsNullOrEmpty(h.HouseState) ? "—" : h.HouseState,
  127. StateBrush = (h.HouseState != null && h.HouseState.Contains("关")) ? Green : Red,
  128. ComText = string.IsNullOrEmpty(h.ComState) ? "—" : h.ComState,
  129. ComBrush = (h.ComState == "已连接") ? Green : Red,
  130. CcdText = (h.CcdState == "正常" && !h.CcdError) ? "正常" : "异常",
  131. CcdBrush = (h.CcdState == "正常" && !h.CcdError) ? Green : Red,
  132. });
  133. }
  134. }
  135. catch
  136. {
  137. // 监控页只读,取数异常不抛
  138. }
  139. }
  140. private static bool IsStale(DateTime? t) => t == null || (DateTime.Now - t.Value).TotalSeconds > StaleSeconds;
  141. private static Brush LinkBrush(DateTime? t)
  142. {
  143. if (t == null) return Gray; // 未知(尚未成功通讯)
  144. return IsStale(t) ? Red : Green; // 过期=红 / 新鲜=绿
  145. }
  146. private static string FmtOk(DateTime? t) => t == null ? "—" : t.Value.ToString("HH:mm:ss");
  147. public void Dispose()
  148. {
  149. try { _timer?.Stop(); } catch { }
  150. }
  151. }
  152. /// <summary>单舱室只读展示行。</summary>
  153. public class HouseMonitorRowVm
  154. {
  155. public int HouseSn { get; set; }
  156. public string PortName { get; set; }
  157. public string RunState { get; set; }
  158. public string Temperature { get; set; }
  159. public string Pressure { get; set; }
  160. public string StateText { get; set; }
  161. public Brush StateBrush { get; set; }
  162. public string ComText { get; set; }
  163. public Brush ComBrush { get; set; }
  164. public string CcdText { get; set; }
  165. public Brush CcdBrush { get; set; }
  166. }
  167. }