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();
Faults = 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;
// —— 舱故障(H-08:读 /status Faults,红色高亮展示)——
/// 是否存在舱故障(内部逻辑用)。
[ObservableProperty] private bool hasFaults;
/// 故障区标题文案(有故障="舱故障(N)" / 无故障="舱室均正常")。
[ObservableProperty] private string faultSummaryText;
/// 故障区标题色(有故障=红 / 无故障=绿)。
[ObservableProperty] private Brush faultSummaryBrush;
/// 故障列表显隐(有故障可见)。直接给 Visibility,避免依赖未注册的布尔转换器。
[ObservableProperty] private Visibility faultListVisibility = Visibility.Collapsed;
/// "无故障"绿条显隐(无故障可见)。
[ObservableProperty] private Visibility noFaultVisibility = Visibility.Visible;
public ObservableCollection Houses { get; }
/// 舱故障只读行(启动排除 + 运行期突发,来源 snap.Faults)。
public ObservableCollection 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));
/// 拉取只读快照并刷新展示属性。任何异常被吞掉,避免监控页崩。
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
{
// 监控页只读,取数异常不抛
}
}
///
/// 阶段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");
///
/// 舱故障行映射(H-08):snap.Faults(control 透出的 HouseFaultRow) → 展示行。
/// 文案走纯静态 (可独立 harness/单测),颜色 Brush 在此加。
/// public static 便于不起 WPF 外壳即可验证有故障场景的中文/时间/隔离映射。
///
public static System.Collections.Generic.List BuildFaultRows(
System.Collections.Generic.IEnumerable faults)
{
var list = new System.Collections.Generic.List();
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 { }
}
}
/// 单舱室只读展示行。
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; }
}
/// 单条舱故障只读展示行(H-08)。
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; }
}
}