| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- 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
- {
- /// <summary>
- /// M5-03-3/4:只读「服务监控」页 ViewModel(需求 7 只读服务监控、需求 10 链路健康 + 不假装实时)。
- ///
- /// 纯只读:仅定时调用同进程 <see cref="ivf_tl_Control.AppData.Instance"/> 的
- /// <see cref="AppData.GetMonitorSnapshot"/> 拉取只读快照并映射到展示属性,
- /// **不持有任何可控制 control 的引用、页面无任何写控件/下发按钮**(约束 4 只读边界)。
- ///
- /// 刷新:DispatcherTimer 默认 2s(阈值/延迟具体值 [D10] 占位,留 M7 校准)。
- /// 链路健康按「最后成功通讯时间」是否过期上色:绿=正常、红=失联/过期。
- /// [M7] 各字段运行时取值、断线红/恢复绿需运行环境验证(本地不可构建/运行)。
- /// </summary>
- public partial class ServiceMonitorViewModel : ObservableObject, IDisposable
- {
- private readonly DispatcherTimer _timer;
- // [D10] 失联判定阈值占位:超过该秒数未成功通讯即视为过期/失联(留 M7 按实时指标校准)。
- private const int StaleSeconds = 30;
- public ServiceMonitorViewModel()
- {
- Houses = new ObservableCollection<HouseMonitorRowVm>();
- Faults = new ObservableCollection<HouseFaultRowVm>();
- 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;
- // —— 舱故障(H-08:读 /status Faults,红色高亮展示)——
- /// <summary>是否存在舱故障(内部逻辑用)。</summary>
- [ObservableProperty] private bool hasFaults;
- /// <summary>故障区标题文案(有故障="舱故障(N)" / 无故障="舱室均正常")。</summary>
- [ObservableProperty] private string faultSummaryText;
- /// <summary>故障区标题色(有故障=红 / 无故障=绿)。</summary>
- [ObservableProperty] private Brush faultSummaryBrush;
- /// <summary>故障列表显隐(有故障可见)。直接给 Visibility,避免依赖未注册的布尔转换器。</summary>
- [ObservableProperty] private Visibility faultListVisibility = Visibility.Collapsed;
- /// <summary>"无故障"绿条显隐(无故障可见)。</summary>
- [ObservableProperty] private Visibility noFaultVisibility = Visibility.Visible;
- public ObservableCollection<HouseMonitorRowVm> Houses { get; }
- /// <summary>舱故障只读行(启动排除 + 运行期突发,来源 snap.Faults)。</summary>
- public ObservableCollection<HouseFaultRowVm> Faults { 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));
- /// <summary>拉取只读快照并刷新展示属性。任何异常被吞掉,避免监控页崩。</summary>
- 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,
- });
- }
- // 舱故障(H-08):启动排除 + 运行期突发,红色高亮;无故障显示绿色"舱室均正常"。
- Faults.Clear();
- foreach (var vm in BuildFaultRows(snap.Faults)) Faults.Add(vm);
- HasFaults = Faults.Count > 0;
- FaultSummaryText = HasFaults ? $"舱故障({Faults.Count})" : "舱室均正常,无故障";
- FaultSummaryBrush = HasFaults ? Red : Green;
- FaultListVisibility = HasFaults ? Visibility.Visible : Visibility.Collapsed;
- NoFaultVisibility = HasFaults ? Visibility.Collapsed : Visibility.Visible;
- }
- catch
- {
- // 监控页只读,取数异常不抛
- }
- }
- /// <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)
- {
- if (t == null) return Gray; // 未知(尚未成功通讯)
- return IsStale(t) ? Red : Green; // 过期=红 / 新鲜=绿
- }
- private static string FmtOk(DateTime? t) => t == null ? "—" : t.Value.ToString("HH:mm:ss");
- /// <summary>
- /// 舱故障行映射(H-08):snap.Faults(control 透出的 HouseFaultRow) → 展示行。
- /// 文案走纯静态 <see cref="ServiceMonitorFaultMapper"/>(可独立 harness/单测),颜色 Brush 在此加。
- /// public static 便于不起 WPF 外壳即可验证有故障场景的中文/时间/隔离映射。
- /// </summary>
- public static System.Collections.Generic.List<HouseFaultRowVm> BuildFaultRows(
- System.Collections.Generic.IEnumerable<ivf_tl_Control.HouseFaultRow> faults)
- {
- var list = new System.Collections.Generic.List<HouseFaultRowVm>();
- if (faults == null) return list;
- foreach (var f in faults)
- {
- list.Add(new HouseFaultRowVm
- {
- HouseText = ServiceMonitorFaultMapper.HouseText(f.HouseSn),
- FaultTypeText = ServiceMonitorFaultMapper.FaultTypeZh(f.FaultType),
- Reason = string.IsNullOrEmpty(f.Reason) ? "—" : f.Reason,
- Stage = string.IsNullOrEmpty(f.Stage) ? "—" : f.Stage,
- AtText = ServiceMonitorFaultMapper.AtText(f.At),
- IsolatedText = ServiceMonitorFaultMapper.IsolatedText(f.Isolated),
- IsolatedBrush = f.Isolated ? Red : Amber,
- });
- }
- return list;
- }
- public void Dispose()
- {
- try { _timer?.Stop(); } catch { }
- }
- }
- /// <summary>单舱室只读展示行。</summary>
- 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; }
- }
- /// <summary>单条舱故障只读展示行(H-08)。</summary>
- public class HouseFaultRowVm
- {
- public string HouseText { get; set; }
- public string FaultTypeText { get; set; }
- public string Reason { get; set; }
- public string Stage { get; set; }
- public string AtText { get; set; }
- public string IsolatedText { get; set; }
- public Brush IsolatedBrush { get; set; }
- }
- }
|