Ùهرست منبع

阶段1完æˆ: 接入PLC轮询/æ•°æ®æ—¥å¿—/报警æœåŠ¡ - ViewAViewModel使用真实数æ®

磊 曹 6 روز پیش
والد
کامیت
dc0fe13e33
3ÙØ§ÛŒÙ„های تغییر ÛŒØ§ÙØªÙ‡ به همراه331 Ø§ÙØ²ÙˆØ¯Ù‡ شده Ùˆ 134 حذ٠شده
  1. 30 3
      src/YZWater.Avalonia/App.axaml.cs
  2. 3 0
      src/YZWater.Core/Services/AlarmService.cs
  3. 298 131
      src/YZWater.Core/ViewModels/ViewAViewModel.cs

+ 30 - 3
src/YZWater.Avalonia/App.axaml.cs

@@ -23,17 +23,47 @@ public partial class App : Application
             { Source = new Uri("avares://YZWater.Avalonia/Themes/IndustrialThemeLight.axaml") };
 
         ThemeService.Instance.ThemeChanged += ApplyTheme;
+
+        // 全局异常处ç†
+        AppDomain.CurrentDomain.UnhandledException += (s, e) =>
+        {
+            Serilog.Log.Fatal(e.ExceptionObject as Exception, "未处ç†çš„异常");
+        };
+        TaskScheduler.UnobservedTaskException += (s, e) =>
+        {
+            Serilog.Log.Error(e.Exception, "未观察的任务异常");
+            e.SetObserved();
+        };
     }
 
     public override void OnFrameworkInitializationCompleted()
     {
+        // åˆå§‹åŒ–æœåŠ¡
         DatabaseService.Initialize();
         PlcService.Initialize();
         ThemeHelper.SetTheme(false);
 
+        // åˆå§‹åŒ–轮询和报警æœåŠ¡
+        PlcPollingService.Instance.Start();
+        AlarmService.Instance.Start();
+
+        // å¯åŠ¨æ•°æ®æ—¥å¿—
+        var dataLogger = new DataLoggingService(PlcPollingService.Instance);
+        dataLogger.Start();
+
         if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
         {
             desktop.MainWindow = new MainWindow();
+
+            // 窗å£å…³é—­æ—¶æ¸…ç†
+            desktop.ShutdownRequested += (_, _) =>
+            {
+                PlcPollingService.Instance.Dispose();
+                AlarmService.Instance.Dispose();
+                dataLogger.Dispose();
+                PlcService.Dispose();
+                DatabaseService.Dispose();
+            };
         }
 
         base.OnFrameworkInitializationCompleted();
@@ -44,14 +74,11 @@ public partial class App : Application
         var resources = Resources.MergedDictionaries;
         if (resources.Count == 0) return;
 
-        // æ¸…ç©ºå¹¶é‡æ–°åŠ è½½ä¸»é¢˜èµ„æº
         var targetTheme = ThemeService.Instance.IsDarkTheme ? _darkTheme : _lightTheme;
         if (targetTheme == null) return;
 
         resources.Clear();
         resources.Add(targetTheme);
-
-        // åŒæ­¥æ›´æ–°ç”»åˆ·ç¼“å­˜
         ThemeHelper.SetTheme(ThemeService.Instance.IsDarkTheme);
     }
 }

+ 3 - 0
src/YZWater.Core/Services/AlarmService.cs

@@ -9,6 +9,9 @@ namespace YZWater.Core.Services;
 /// </summary>
 public partial class AlarmService : ObservableObject, IDisposable
 {
+    private static AlarmService? _instance;
+    public static AlarmService Instance => _instance ??= new AlarmService(PlcPollingService.Instance);
+
     private readonly PlcPollingService _pollingService;
     private CancellationTokenSource? _cts;
     private bool _disposed;

+ 298 - 131
src/YZWater.Core/ViewModels/ViewAViewModel.cs

@@ -7,87 +7,107 @@ using YZWater.Core.Services;
 namespace YZWater.Core.ViewModels;
 
 /// <summary>
-/// 主工艺视图 ViewModel(工业风格版)
+/// 主工艺视图 ViewModel(接入真实 PLC æ•°æ®ï¼‰
 /// </summary>
 public partial class ViewAViewModel : ObservableObject
 {
+    private readonly LanguageService _lang = LanguageService.Instance;
     private DateTime _startTime = DateTime.Now;
 
-    // ─── æ¶²ä½æ•°æ® ───
-    [ObservableProperty] private float _tank1Level = 65.5f;
-    [ObservableProperty] private float _tank2Level = 72.3f;
-    [ObservableProperty] private float _tank3Level = 58.8f;
-    [ObservableProperty] private float _tank4Level = 45.2f;
-
-    // ─── æµé‡æ•°æ® ───
-    [ObservableProperty] private float _inflowRate = 35.6f;
-    [ObservableProperty] private float _outflowRate = 32.1f;
-    [ObservableProperty] private float _flowDelta = 3.5f;
-
-    // ─── æ³µçŠ¶æ€ â”€â”€â”€
-    [ObservableProperty] private bool _pump1Running = true;
-    [ObservableProperty] private bool _pump2Running = false;
-    [ObservableProperty] private bool _pump3Running = true;
-    [ObservableProperty] private bool _pump4Running = false;
-    [ObservableProperty] private bool _pump5Running = true;
-
-    // ─── 泵状æ€é¢œè‰² ───
-    [ObservableProperty] private string _pump1StatusColor = "#00897B";
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  水箱液ä½
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    [ObservableProperty] private float _tank1Level;
+    [ObservableProperty] private float _tank2Level;
+    [ObservableProperty] private float _tank3Level;
+    [ObservableProperty] private float _tank4Level;
+
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  æµé‡
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    [ObservableProperty] private float _inflowRate;
+    [ObservableProperty] private float _outflowRate;
+    [ObservableProperty] private float _flowDelta;
+
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  泵状æ€
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    [ObservableProperty] private bool _pump1Running;
+    [ObservableProperty] private bool _pump2Running;
+    [ObservableProperty] private bool _pump3Running;
+    [ObservableProperty] private bool _pump4Running;
+    [ObservableProperty] private bool _pump5Running;
+
+    // 泵状æ€é¢œè‰²
+    [ObservableProperty] private string _pump1StatusColor = "#4B5563";
     [ObservableProperty] private string _pump2StatusColor = "#4B5563";
-    [ObservableProperty] private string _pump3StatusColor = "#00897B";
+    [ObservableProperty] private string _pump3StatusColor = "#4B5563";
     [ObservableProperty] private string _pump4StatusColor = "#4B5563";
-    [ObservableProperty] private string _pump5StatusColor = "#00897B";
+    [ObservableProperty] private string _pump5StatusColor = "#4B5563";
 
-    // ─── æ³µçŠ¶æ€æ–‡æœ¬ ───
-    [ObservableProperty] private string _pump1Status = "RUNNING 50Hz";
+    // æ³µçŠ¶æ€æ–‡æœ¬
+    [ObservableProperty] private string _pump1Status = "STOPPED";
     [ObservableProperty] private string _pump2Status = "STOPPED";
-    [ObservableProperty] private string _pump3Status = "RUNNING 40Hz";
+    [ObservableProperty] private string _pump3Status = "STOPPED";
     [ObservableProperty] private string _pump4Status = "STOPPED";
-    [ObservableProperty] private string _pump5Status = "RUNNING 55Hz";
-
-    // ─── é£Žæ‰‡çŠ¶æ€ â”€â”€â”€
-    [ObservableProperty] private bool _fan1Running = true;
-    [ObservableProperty] private bool _fan2Running = false;
-    [ObservableProperty] private string _fan1StatusColor = "#00897B";
+    [ObservableProperty] private string _pump5Status = "STOPPED";
+
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  风扇状æ€
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    [ObservableProperty] private bool _fan1Running;
+    [ObservableProperty] private bool _fan2Running;
+    [ObservableProperty] private string _fan1StatusColor = "#4B5563";
     [ObservableProperty] private string _fan2StatusColor = "#4B5563";
-    [ObservableProperty] private string _fan1Status = "RUNNING 1500RPM";
+    [ObservableProperty] private string _fan1Status = "STOPPED";
     [ObservableProperty] private string _fan2Status = "STOPPED";
 
-    // ─── é˜€é—¨çŠ¶æ€ â”€â”€â”€
-    [ObservableProperty] private ValveStatus _valve1Status = ValveStatus.Open;
-    [ObservableProperty] private ValveStatus _valve2Status = ValveStatus.Open;
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  阀门状æ€
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    [ObservableProperty] private ValveStatus _valve1Status = ValveStatus.Middle;
+    [ObservableProperty] private ValveStatus _valve2Status = ValveStatus.Middle;
     [ObservableProperty] private ValveStatus _valve3Status = ValveStatus.Middle;
-    [ObservableProperty] private ValveStatus _valve4Status = ValveStatus.Closed;
-    [ObservableProperty] private string _valve1StatusColor = "#00897B";
-    [ObservableProperty] private string _valve2StatusColor = "#00897B";
-    [ObservableProperty] private string _valve3StatusColor = "#F59E0B";
+    [ObservableProperty] private ValveStatus _valve4Status = ValveStatus.Middle;
+    [ObservableProperty] private string _valve1StatusColor = "#4B5563";
+    [ObservableProperty] private string _valve2StatusColor = "#4B5563";
+    [ObservableProperty] private string _valve3StatusColor = "#4B5563";
     [ObservableProperty] private string _valve4StatusColor = "#4B5563";
 
-    // ─── æ¨¡å¼ â”€â”€â”€
+    // 模å¼
     [ObservableProperty] private ValveStatus _modeStatus = ValveStatus.Open;
 
-    // ─── æµåŠ¨çŠ¶æ€ â”€â”€â”€
+    // æµåŠ¨çŠ¶æ€
     [ObservableProperty] private bool _isInflowRunning = true;
     [ObservableProperty] private bool _isOutflowRunning = true;
 
-    // ─── 报警 ───
-    [ObservableProperty] private bool _hasAlarm = true;
-    [ObservableProperty] private string _alarmMessage = "TANK1 LEVEL HIGH (65.5%)";
-    [ObservableProperty] private int _alarmCount = 1;
-
-    // ─── 连接 ───
-    [ObservableProperty] private bool _isPlcConnected = true;
-
-    // ─── 统计 ───
-    [ObservableProperty] private int _runningDeviceCount = 5;
-    [ObservableProperty] private string _runningTime = "0d 2h 35m";
-    [ObservableProperty] private double _efficiency = 92.5;
-
-    // ─── 时间 ───
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  统计
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    [ObservableProperty] private int _runningDeviceCount;
+    [ObservableProperty] private string _runningTime = "0d 0h 0m";
+    [ObservableProperty] private double _efficiency = 90.0;
+
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  报警
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    [ObservableProperty] private bool _hasAlarm;
+    [ObservableProperty] private string _alarmMessage = string.Empty;
+    [ObservableProperty] private int _alarmCount;
+
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  连接状æ€
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    [ObservableProperty] private bool _isPlcConnected;
+
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  æ—¶é—´
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
     [ObservableProperty] private DateTime _currentTime = DateTime.Now;
 
-    // ─── 翻译文字 ───
-    private readonly LanguageService _lang = LanguageService.Instance;
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  翻译文字
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
     [ObservableProperty] private string _titleText;
     [ObservableProperty] private string _subtitleText;
     [ObservableProperty] private string _plcStatusText;
@@ -132,7 +152,16 @@ public partial class ViewAViewModel : ObservableObject
     {
         UpdateTexts();
         _lang.LanguageChanged += UpdateTexts;
-        StartTimers();
+
+        // 订阅 PLC 轮询数æ®
+        PlcPollingService.Instance.DataUpdated += OnDataUpdated;
+        PlcPollingService.Instance.ConnectionStateChanged += OnConnectionStateChanged;
+
+        // 订阅报警
+        AlarmService.Instance.AlarmStateChanged += OnAlarmStateChanged;
+
+        // æ—¶é’Ÿæ›´æ–°
+        StartClockTimer();
     }
 
     private void UpdateTexts()
@@ -178,38 +207,128 @@ public partial class ViewAViewModel : ObservableObject
         ProcessFlowText = _lang.Get("ProcessFlow");
     }
 
-    private void StartTimers()
+    private void StartClockTimer()
     {
-        var timeTimer = new System.Timers.Timer(1000);
-        timeTimer.Elapsed += (s, e) =>
+        var timer = new System.Timers.Timer(1000);
+        timer.Elapsed += (s, e) =>
         {
             CurrentTime = DateTime.Now;
             var elapsed = DateTime.Now - _startTime;
             RunningTime = $"{elapsed.Days}d {elapsed.Hours}h {elapsed.Minutes}m";
         };
-        timeTimer.Start();
+        timer.Start();
+    }
 
-        var dataTimer = new System.Timers.Timer(3000);
-        dataTimer.Elapsed += (s, e) => SimulateDataChanges();
-        dataTimer.Start();
+    /// <summary>
+    /// PLC æ•°æ®æ›´æ–°å›žè°ƒ
+    /// </summary>
+    private void OnDataUpdated(PlcDataModel data)
+    {
+        // 更新水箱液ä½
+        Tank1Level = data.Tank1Level;
+        Tank2Level = data.Tank2Level;
+        Tank3Level = data.Tank3Level;
+        Tank4Level = data.Tank4Level;
+
+        // æ›´æ–°æµé‡
+        InflowRate = data.InflowRate;
+        OutflowRate = data.OutflowRate;
+        FlowDelta = data.FlowDelta;
+
+        // 更新泵状æ€
+        Pump1Running = data.Pump1Running;
+        Pump2Running = data.Pump2Running;
+        Pump3Running = data.Pump3Running;
+        Pump4Running = data.Pump4Running;
+        Pump5Running = data.Pump5Running;
+
+        // 更新风扇状æ€
+        Fan1Running = data.Fan1Running;
+        Fan2Running = data.Fan2Running;
+
+        // 更新阀门状æ€
+        Valve1Status = data.Valve1Position > 50 ? ValveStatus.Open :
+                       data.Valve1Position < 50 ? ValveStatus.Closed : ValveStatus.Middle;
+        Valve2Status = data.Valve2Position > 50 ? ValveStatus.Open :
+                       data.Valve2Position < 50 ? ValveStatus.Closed : ValveStatus.Middle;
+        Valve3Status = data.Valve3Position > 50 ? ValveStatus.Open :
+                       data.Valve3Position < 50 ? ValveStatus.Closed : ValveStatus.Middle;
+        Valve4Status = data.Valve4Position > 50 ? ValveStatus.Open :
+                       data.Valve4Position < 50 ? ValveStatus.Closed : ValveStatus.Middle;
+
+        // 更新模å¼
+        ModeStatus = data.IsAutoMode ? ValveStatus.Open : ValveStatus.Closed;
+
+        // æ›´æ–°æµåŠ¨çŠ¶æ€
+        IsInflowRunning = data.InflowRate > 0;
+        IsOutflowRunning = data.OutflowRate > 0;
+
+        // 更新设备状æ€
+        UpdateDeviceStatuses(data);
+
+        // 更新统计
+        RunningDeviceCount = data.RunningPumpCount + data.RunningFanCount;
+        Efficiency = 85 + (data.InflowRate > 0 ? (data.OutflowRate / data.InflowRate * 100) : 0);
+
+        // 检查报警
+        CheckAlarms(data);
     }
 
-    private void SimulateDataChanges()
+    /// <summary>
+    /// 连接状æ€å˜æ›´
+    /// </summary>
+    private void OnConnectionStateChanged(bool connected)
     {
-        var r = new Random();
-        Tank1Level = Clamp(Tank1Level + (float)(r.NextDouble() * 2 - 1), 0, 100);
-        Tank2Level = Clamp(Tank2Level + (float)(r.NextDouble() * 2 - 1), 0, 100);
-        Tank3Level = Clamp(Tank3Level + (float)(r.NextDouble() * 2 - 1), 0, 100);
-        Tank4Level = Clamp(Tank4Level + (float)(r.NextDouble() * 2 - 1), 0, 100);
-        InflowRate = Math.Max(0, InflowRate + (float)(r.NextDouble() * 4 - 2));
-        OutflowRate = Math.Max(0, OutflowRate + (float)(r.NextDouble() * 4 - 2));
-        FlowDelta = InflowRate - OutflowRate;
-        Efficiency = 85 + r.NextDouble() * 10;
-        CheckAlarms();
-        UpdateDeviceStatuses();
+        IsPlcConnected = connected;
     }
 
-    private void CheckAlarms()
+    /// <summary>
+    /// 报警状æ€å˜æ›´
+    /// </summary>
+    private void OnAlarmStateChanged()
+    {
+        HasAlarm = AlarmService.Instance.HasActiveAlarm;
+        AlarmMessage = AlarmService.Instance.ActiveAlarmMessage;
+        AlarmCount = AlarmService.Instance.ActiveAlarmCount;
+    }
+
+    /// <summary>
+    /// 更新设备状æ€
+    /// </summary>
+    private void UpdateDeviceStatuses(PlcDataModel data)
+    {
+        Pump1StatusColor = data.Pump1Running ? "#00897B" : (data.Pump1Fault ? "#EF4444" : "#4B5563");
+        Pump2StatusColor = data.Pump2Running ? "#00897B" : (data.Pump2Fault ? "#EF4444" : "#4B5563");
+        Pump3StatusColor = data.Pump3Running ? "#00897B" : (data.Pump3Fault ? "#EF4444" : "#4B5563");
+        Pump4StatusColor = data.Pump4Running ? "#00897B" : (data.Pump4Fault ? "#EF4444" : "#4B5563");
+        Pump5StatusColor = data.Pump5Running ? "#00897B" : (data.Pump5Fault ? "#EF4444" : "#4B5563");
+
+        Fan1StatusColor = data.Fan1Running ? "#00897B" : (data.Fan1Fault ? "#EF4444" : "#4B5563");
+        Fan2StatusColor = data.Fan2Running ? "#00897B" : (data.Fan2Fault ? "#EF4444" : "#4B5563");
+
+        Valve1StatusColor = data.Valve1Position > 50 ? "#00897B" :
+                            data.Valve1Position < 50 ? "#F59E0B" : "#4B5563";
+        Valve2StatusColor = data.Valve2Position > 50 ? "#00897B" :
+                            data.Valve2Position < 50 ? "#F59E0B" : "#4B5563";
+        Valve3StatusColor = data.Valve3Position > 50 ? "#00897B" :
+                            data.Valve3Position < 50 ? "#F59E0B" : "#4B5563";
+        Valve4StatusColor = data.Valve4Position > 50 ? "#00897B" :
+                            data.Valve4Position < 50 ? "#F59E0B" : "#4B5563";
+
+        Pump1Status = data.Pump1Running ? $"RUNNING {data.Pump1Freq:F0}Hz" : (data.Pump1Fault ? "FAULT" : "STOPPED");
+        Pump2Status = data.Pump2Running ? $"RUNNING {data.Pump2Freq:F0}Hz" : (data.Pump2Fault ? "FAULT" : "STOPPED");
+        Pump3Status = data.Pump3Running ? $"RUNNING {data.Pump3Freq:F0}Hz" : (data.Pump3Fault ? "FAULT" : "STOPPED");
+        Pump4Status = data.Pump4Running ? $"RUNNING {data.Pump4Freq:F0}Hz" : (data.Pump4Fault ? "FAULT" : "STOPPED");
+        Pump5Status = data.Pump5Running ? $"RUNNING {data.Pump5Freq:F0}Hz" : (data.Pump5Fault ? "FAULT" : "STOPPED");
+
+        Fan1Status = data.Fan1Running ? "RUNNING" : (data.Fan1Fault ? "FAULT" : "STOPPED");
+        Fan2Status = data.Fan2Running ? "RUNNING" : (data.Fan2Fault ? "FAULT" : "STOPPED");
+    }
+
+    /// <summary>
+    /// 检查报警(从 PlcPollingService æ•°æ®ï¼‰
+    /// </summary>
+    private void CheckAlarms(PlcDataModel data)
     {
         var config = ConfigService.GetConfig();
         if (config == null) return;
@@ -218,84 +337,132 @@ public partial class ViewAViewModel : ObservableObject
         AlarmMessage = string.Empty;
         AlarmCount = 0;
 
-        if (Tank1Level > config.LevelHighAlarm)
+        if (data.Tank1Level > config.LevelHighAlarm || data.Tank1Level < config.LevelLowAlarm)
         {
             HasAlarm = true;
-            AlarmMessage = $"TANK1 LEVEL HIGH ({Tank1Level:F1}%)";
             AlarmCount++;
         }
-        else if (Tank1Level < config.LevelLowAlarm)
+        if (data.Tank2Level > config.LevelHighAlarm || data.Tank2Level < config.LevelLowAlarm)
         {
             HasAlarm = true;
-            AlarmMessage = $"TANK1 LEVEL LOW ({Tank1Level:F1}%)";
             AlarmCount++;
         }
-
-        if (InflowRate > config.FlowHighAlarm)
+        if (data.InflowRate > config.FlowHighAlarm)
+        {
+            HasAlarm = true;
+            AlarmCount++;
+        }
+        if (data.HasAnyFault)
         {
             HasAlarm = true;
-            AlarmMessage += $"\nINFLOW HIGH ({InflowRate:F1} m³/h)";
             AlarmCount++;
         }
 
-        RunningDeviceCount = (Pump1Running ? 1 : 0) + (Pump2Running ? 1 : 0) +
-                            (Pump3Running ? 1 : 0) + (Pump4Running ? 1 : 0) +
-                            (Pump5Running ? 1 : 0) + (Fan1Running ? 1 : 0) +
-                            (Fan2Running ? 1 : 0);
+        RunningDeviceCount = data.RunningPumpCount + data.RunningFanCount;
+    }
+
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+    //  PLC 控制命令
+    // â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
+
+    [RelayCommand]
+    private async Task ConnectPlcAsync() => IsPlcConnected = await PlcService.ConnectAsync();
+
+    [RelayCommand]
+    private void DisconnectPlc() { PlcService.Disconnect(); IsPlcConnected = false; }
+
+    [RelayCommand]
+    private async Task RefreshDataAsync() => await PlcPollingService.Instance.RefreshNowAsync();
+
+    /// <summary>
+    /// 控制泵å¯åœ
+    /// </summary>
+    [RelayCommand]
+    private async Task TogglePumpAsync(string pumpId)
+    {
+        if (!PlcService.IsConnected) { Log.Warning("PLC 未连接"); return; }
+
+        var index = pumpId switch { "P001" => 0, "P002" => 1, "P003" => 2, "P004" => 3, "P005" => 4, _ => -1 };
+        if (index < 0) return;
+
+        var address = PlcConfig.GetPumpRunAddress(index);
+        var currentState = PlcPollingService.Instance.Data.GetPumpRunning(index);
+        await PlcService.WriteBoolAsync(address, !currentState);
     }
 
-    private void UpdateDeviceStatuses()
+    /// <summary>
+    /// 控制风机å¯åœ
+    /// </summary>
+    [RelayCommand]
+    private async Task ToggleFanAsync(string fanId)
     {
-        Pump1StatusColor = Pump1Running ? "#00897B" : "#4B5563";
-        Pump2StatusColor = Pump2Running ? "#00897B" : "#4B5563";
-        Pump3StatusColor = Pump3Running ? "#00897B" : "#4B5563";
-        Pump4StatusColor = Pump4Running ? "#00897B" : "#4B5563";
-        Pump5StatusColor = Pump5Running ? "#00897B" : "#4B5563";
-        Fan1StatusColor = Fan1Running ? "#00897B" : "#4B5563";
-        Fan2StatusColor = Fan2Running ? "#00897B" : "#4B5563";
-
-        Valve1StatusColor = Valve1Status == ValveStatus.Open ? "#00897B" :
-                            Valve1Status == ValveStatus.Middle ? "#F59E0B" : "#4B5563";
-        Valve2StatusColor = Valve2Status == ValveStatus.Open ? "#00897B" :
-                            Valve2Status == ValveStatus.Middle ? "#F59E0B" : "#4B5563";
-        Valve3StatusColor = Valve3Status == ValveStatus.Open ? "#00897B" :
-                            Valve3Status == ValveStatus.Middle ? "#F59E0B" : "#4B5563";
-        Valve4StatusColor = Valve4Status == ValveStatus.Open ? "#00897B" :
-                            Valve4Status == ValveStatus.Middle ? "#F59E0B" : "#4B5563";
-
-        Pump1Status = Pump1Running ? "RUNNING 50Hz" : "STOPPED";
-        Pump2Status = Pump2Running ? "RUNNING 45Hz" : "STOPPED";
-        Pump3Status = Pump3Running ? "RUNNING 40Hz" : "STOPPED";
-        Pump4Status = Pump4Running ? "RUNNING 35Hz" : "STOPPED";
-        Pump5Status = Pump5Running ? "RUNNING 55Hz" : "STOPPED";
-        Fan1Status = Fan1Running ? "RUNNING 1500RPM" : "STOPPED";
-        Fan2Status = Fan2Running ? "RUNNING 1200RPM" : "STOPPED";
+        if (!PlcService.IsConnected) { Log.Warning("PLC 未连接"); return; }
+
+        var address = fanId == "F001" ? PlcConfig.Fan1Start : PlcConfig.Fan2Start;
+        var currentState = fanId == "F001" ? PlcPollingService.Instance.Data.Fan1Running : PlcPollingService.Instance.Data.Fan2Running;
+        await PlcService.WriteBoolAsync(address, !currentState);
     }
 
-    private static float Clamp(float value, float min, float max) => Math.Max(min, Math.Min(max, value));
+    /// <summary>
+    /// 控制阀门
+    /// </summary>
+    [RelayCommand]
+    private async Task ToggleValveAsync(string valveId)
+    {
+        if (!PlcService.IsConnected) { Log.Warning("PLC 未连接"); return; }
+
+        var index = valveId switch { "V001" => 0, "V002" => 1, "V003" => 2, "V004" => 3, _ => -1 };
+        if (index < 0) return;
+
+        var address = PlcConfig.GetValveControlAddress(index);
+        await PlcService.WriteBoolAsync(address, true);
+        await Task.Delay(500);
+        await PlcService.WriteBoolAsync(address, false);
+    }
 
+    /// <summary>
+    /// åˆ‡æ¢æ¨¡å¼
+    /// </summary>
     [RelayCommand]
-    private async Task ConnectPlcAsync() => IsPlcConnected = await PlcService.ConnectAsync();
+    private async Task ToggleModeAsync()
+    {
+        if (!PlcService.IsConnected) return;
+        await PlcService.WriteBoolAsync(PlcConfig.SystemMode, ModeStatus != ValveStatus.Open);
+    }
 
+    /// <summary>
+    /// 全部å¯åЍ
+    /// </summary>
     [RelayCommand]
-    private void DisconnectPlc() { PlcService.Disconnect(); IsPlcConnected = false; }
+    private async Task StartAllAsync()
+    {
+        if (!PlcService.IsConnected) return;
+        for (int i = 0; i < 5; i++)
+            await PlcService.WriteBoolAsync(PlcConfig.GetPumpRunAddress(i), true);
+        for (int i = 0; i < 2; i++)
+            await PlcService.WriteBoolAsync(i == 0 ? PlcConfig.Fan1Start : PlcConfig.Fan2Start, true);
+    }
 
+    /// <summary>
+    /// å…¨éƒ¨åœæ­¢
+    /// </summary>
     [RelayCommand]
-    private async Task RefreshDataAsync()
+    private async Task StopAllAsync()
     {
         if (!PlcService.IsConnected) return;
-        try
-        {
-            Tank1Level = await PlcService.ReadFloatAsync("VD100");
-            Tank2Level = await PlcService.ReadFloatAsync("VD104");
-            Tank3Level = await PlcService.ReadFloatAsync("VD108");
-            Tank4Level = await PlcService.ReadFloatAsync("VD112");
-            InflowRate = await PlcService.ReadFloatAsync("VD200");
-            OutflowRate = await PlcService.ReadFloatAsync("VD204");
-            FlowDelta = InflowRate - OutflowRate;
-            CheckAlarms();
-        }
-        catch (Exception ex) { Log.Error(ex, "Refresh failed"); }
+        for (int i = 0; i < 5; i++)
+            await PlcService.WriteBoolAsync(PlcConfig.GetPumpRunAddress(i), false);
+        for (int i = 0; i < 2; i++)
+            await PlcService.WriteBoolAsync(i == 0 ? PlcConfig.Fan1Start : PlcConfig.Fan2Start, false);
+    }
+
+    /// <summary>
+    /// 确认报警
+    /// </summary>
+    [RelayCommand]
+    private async Task AckAlarmAsync()
+    {
+        await AlarmService.Instance.AcknowledgeAllAsync();
     }
 }