using System; using System.Collections.ObjectModel; 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; namespace ivf_tl_Operate.ViewModel { /// /// M5-03-3/4:只读「服务监控」页 ViewModel(需求 7 只读服务监控、需求 10 链路健康 + 不假装实时)。 /// /// 纯只读:仅定时调用同进程 的 /// 拉取只读快照并映射到展示属性, /// **不持有任何可控制 control 的引用、页面无任何写控件/下发按钮**(约束 4 只读边界)。 /// /// 刷新:DispatcherTimer 默认 2s(阈值/延迟具体值 [D10] 占位,留 M7 校准)。 /// 链路健康按「最后成功通讯时间」是否过期上色:绿=正常、红=失联/过期。 /// [M7] 各字段运行时取值、断线红/恢复绿需运行环境验证(本地不可构建/运行)。 /// public partial class ServiceMonitorViewModel : ObservableObject, IDisposable { private readonly DispatcherTimer _timer; // [D10] 失联判定阈值占位:超过该秒数未成功通讯即视为过期/失联(留 M7 按实时指标校准)。 private const int StaleSeconds = 30; public ServiceMonitorViewModel() { Houses = new ObservableCollection(); Refresh(); _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; _timer.Tick += (s, e) => Refresh(); _timer.Start(); } // —— 顶部链路健康 —— [ObservableProperty] private string snapshotTime; [ObservableProperty] private bool controlHosted; [ObservableProperty] private string controlHostedText; [ObservableProperty] private Brush controlHostedBrush; [ObservableProperty] private string mqttText; [ObservableProperty] private Brush mqttBrush; [ObservableProperty] private string mqttLastOk; [ObservableProperty] private string serverUrl; [ObservableProperty] private string httpText; [ObservableProperty] private Brush httpBrush; [ObservableProperty] private string httpLastOk; [ObservableProperty] private string kafkaText; [ObservableProperty] private Brush kafkaBrush; [ObservableProperty] private string kafkaLastOk; [ObservableProperty] private string uploadQueueText; [ObservableProperty] private Brush uploadQueueBrush; [ObservableProperty] private string diskText; [ObservableProperty] private Brush diskBrush; public ObservableCollection Houses { get; } private static readonly Brush Green = new SolidColorBrush(Color.FromRgb(0x2E, 0xA0, 0x43)); private static readonly Brush Red = new SolidColorBrush(Color.FromRgb(0xD0, 0x32, 0x2D)); private static readonly Brush Gray = new SolidColorBrush(Color.FromRgb(0x95, 0x95, 0x96)); private static readonly Brush Amber = new SolidColorBrush(Color.FromRgb(0xE0, 0x9A, 0x12)); /// 拉取只读快照并刷新展示属性。任何异常被吞掉,避免监控页崩。 public void Refresh() { try { MonitorSnapshot snap = null; // 阶段2:拆分后 control 是独立进程,改经本地 HTTP /status 跨进程读快照(不再读同进程 AppData)。 try { snap = ivf_tl_Operate.Helpers.ControlClient.GetStatusSnapshot(); } catch { snap = null; } if (snap == null) { ControlHosted = false; ControlHostedText = "后台未托管 / 取数失败"; ControlHostedBrush = Gray; SnapshotTime = "—"; return; } SnapshotTime = snap.SnapshotAt.ToString("yyyy-MM-dd HH:mm:ss"); ControlHosted = snap.ControlHosted; ControlHostedText = snap.ControlHosted ? "后台服务已托管" : "后台未托管"; ControlHostedBrush = snap.ControlHosted ? Green : Red; // MQTT 链路 MqttText = snap.MqttConnected ? "已连接" : "未连接"; MqttBrush = snap.MqttConnected ? Green : Red; MqttLastOk = FmtOk(snap.LastMqttOkAt); // 服务器 / HTTP 链路(按最后成功时间过期判定) ServerUrl = string.IsNullOrEmpty(snap.ServerUrl) ? "—" : snap.ServerUrl; HttpBrush = LinkBrush(snap.LastHttpOkAt); HttpText = snap.LastHttpOkAt == null ? "未知" : (IsStale(snap.LastHttpOkAt) ? "可能失联" : "正常"); HttpLastOk = FmtOk(snap.LastHttpOkAt); // Kafka / 图片上传链路 KafkaBrush = LinkBrush(snap.LastKafkaOkAt); KafkaText = snap.LastKafkaOkAt == null ? "未知" : (IsStale(snap.LastKafkaOkAt) ? "可能失联" : "正常"); KafkaLastOk = FmtOk(snap.LastKafkaOkAt); // 上传队列(内存缓存 + 落盘待传) int pending = snap.ImageCacheCount + snap.PendingDiskImageCount; UploadQueueText = snap.ImageBacklogAlarm ? $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}(堆积告警>{snap.PendingImageAlarmThreshold})" : $"内存 {snap.ImageCacheCount} / 落盘待传 {snap.PendingDiskImageCount}"; // M5-04-3:断网累积兜底——以 control 侧权威 ImageBacklogAlarm 判定红色显著提示(不静默堆积); // 未达阈值但有待传时琥珀提示。[D10] 阈值由 control PendingImageAlarmThreshold 提供,留 M7 校准。 UploadQueueBrush = snap.ImageBacklogAlarm ? Red : (pending == 0 ? Green : Amber); // 磁盘 if (string.IsNullOrEmpty(snap.DiskPath)) { DiskText = "—"; DiskBrush = Gray; } else if (!snap.DiskExist) { DiskText = $"{snap.DiskPath}: 盘不存在"; DiskBrush = Red; } else { DiskText = $"{snap.DiskPath}: 剩余 {snap.DiskFreeGb:0.0} GB"; DiskBrush = snap.DiskFreeGb < 10 ? Red : (snap.DiskFreeGb < 30 ? Amber : Green); } // 舱室 Houses.Clear(); foreach (var h in snap.Houses) { Houses.Add(new HouseMonitorRowVm { HouseSn = h.HouseSn, PortName = string.IsNullOrEmpty(h.PortName) ? "—" : h.PortName, RunState = string.IsNullOrEmpty(h.RunState) ? "—" : h.RunState, Temperature = $"{h.Temperature:0.0}", Pressure = $"{h.Pressure:0.0}", StateText = string.IsNullOrEmpty(h.HouseState) ? "—" : h.HouseState, StateBrush = (h.HouseState != null && h.HouseState.Contains("关")) ? Green : Red, ComText = string.IsNullOrEmpty(h.ComState) ? "—" : h.ComState, 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, }); } } catch { // 监控页只读,取数异常不抛 } } /// /// 阶段2 §5.4 受护栏整体停止 control:二次确认 + 工程师口令 → POST /shutdown。 /// control 端校验口令(tl13579,App.config engineerPwd)通过才安全停机。 /// [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) { if (t == null) return Gray; // 未知(尚未成功通讯) return IsStale(t) ? Red : Green; // 过期=红 / 新鲜=绿 } private static string FmtOk(DateTime? t) => t == null ? "—" : t.Value.ToString("HH:mm:ss"); public void Dispose() { try { _timer?.Stop(); } catch { } } } /// 单舱室只读展示行。 public class HouseMonitorRowVm { public int HouseSn { get; set; } public string PortName { get; set; } public string RunState { get; set; } public string Temperature { get; set; } public string Pressure { get; set; } public string StateText { get; set; } public Brush StateBrush { get; set; } public string ComText { get; set; } 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; } } }