Просмотр исходного кода

设备状态在原有心跳判断的基础上增加数据收发的判断

LC 6 дней назад
Родитель
Сommit
57d5cb1c69

+ 1 - 0
.vscode/settings.json

@@ -0,0 +1 @@
+{}

+ 84 - 6
Core/TcpServerFrame.cs

@@ -37,6 +37,10 @@ namespace SWRIS.Core
         public event EventHandler<ClockResultReceivedEventArgs> ClockResultReceived;
         public event EventHandler<EncoderDirectionResultReceivedEventArgs> EncoderDirectionResultReceived;
         public event EventHandler<HeartbeatReceviedEventArgs> HeartbeatReceived;
+        /// <summary>
+        /// 数据激活状态变化事件(数据在正常收发/数据静默切换时触发)
+        /// </summary>
+        public event EventHandler<DataActiveStateChangedEventArgs> DataActiveStateChanged;
         public event EventHandler<DebugMessageReceivedEventArgs> DebugMessageReceived;
         private Socket _listener;
         private bool _isRunning;
@@ -44,6 +48,18 @@ namespace SWRIS.Core
         private readonly int _port;
         private readonly ByteTransform _byteTransform;
         private const string PASSWORD = "WNDTM4";
+        /// <summary>
+        /// 心跳检测周期(秒)
+        /// </summary>
+        private const int HeartbeatCheckIntervalSeconds = 5;
+        /// <summary>
+        /// 心跳超时时间(秒):超过该时间未收到任何数据则判定连接失效
+        /// </summary>
+        private const int HeartbeatTimeoutSeconds = 15;
+        /// <summary>
+        /// 数据静默超时时间(秒):超过该时间未收到有效数据则数据激活状态置为失活
+        /// </summary>
+        private const int DataTimeoutSeconds = 15;
         private static ConcurrentDictionary<string, ClientState> _connectedClients;
         private CancellationTokenSource ctsToken = new CancellationTokenSource();
         public TcpServerFrame(string ipAddress, int port)
@@ -155,15 +171,31 @@ namespace SWRIS.Core
                 {
                     try
                     {
-                        await Task.Delay(TimeSpan.FromSeconds(5), token.Token);
+                        await Task.Delay(TimeSpan.FromSeconds(HeartbeatCheckIntervalSeconds), token.Token);
 
+                        var now = DateTime.UtcNow;
                         foreach (var client in _connectedClients.ToArray())
                         {
-                            if ((DateTime.UtcNow - client.Value.LastActiveTime).TotalSeconds > 15)
+                            var clientState = client.Value;
+
+                            // 数据在正常收发时,链路是活跃的:即使心跳包缺失也应保持连接
+                            // 避免设备持续上报实时数据却被判为离线
+                            if ((now - clientState.LastDataReceivedTime).TotalSeconds <= DataTimeoutSeconds)
                             {
-                                SafeCloseClient(client.Value);
+                                // 数据激活即刷新连接活跃时间,心跳缺失不再触发误判
+                                clientState.LastActiveTime = now;
+                                SetDataActiveState(clientState, true);
+                                continue;
+                            }
+
+                            // 数据已静默,置为失活后再按心跳超时判定
+                            SetDataActiveState(clientState, false);
+
+                            if ((now - clientState.LastActiveTime).TotalSeconds > HeartbeatTimeoutSeconds)
+                            {
+                                SafeCloseClient(clientState);
                                 DebugMessageReceived?.Invoke(this,
-                                    new DebugMessageReceivedEventArgs($"心跳超时,强制断开", ipAddress: client.Value.IpAddress));
+                                    new DebugMessageReceivedEventArgs($"心跳超时,强制断开", ipAddress: clientState.IpAddress));
                             }
                         }
                     }
@@ -299,7 +331,8 @@ namespace SWRIS.Core
             byte[] heartBeatData = new byte[2];
             client.ReceiveBuffer.Read(heartBeatData, 0, 2);
 
-            client.LastActiveTime = DateTime.UtcNow;
+            // 心跳也是有效数据,统一走数据激活标记
+            MarkDataActive(client);
             RespondHeartbeat(heartBeatData, client.IpAddress, client.ClientSocket);
 
             HeartbeatReceived?.Invoke(this, new HeartbeatReceviedEventArgs(client.IpAddress));
@@ -349,6 +382,10 @@ namespace SWRIS.Core
             byte[] payload = new byte[remainingLength];
             Buffer.BlockCopy(packetData, payloadStart, payload, 0, remainingLength);
 
+            // 收到完整有效的业务数据包即标记数据激活,
+            // 使设备在持续上报实时数据时不会因心跳缺失被误判离线
+            MarkDataActive(client);
+
             // 处理数据包
             await ProcessEventByType(eventType, payload, client);
 
@@ -1471,14 +1508,55 @@ namespace SWRIS.Core
                 LogHelper.Error("发送开始或停止实时数据命令时错误", ex);
             }
         }
-        private void SafeCloseClient(ClientState client)
+        /// <summary>
+        /// 标记客户端数据激活:收到任意有效数据(心跳包或业务数据包)时调用。
+        /// 数据在正常收发时保持连接活跃,避免心跳包缺失导致误判离线。
+        /// </summary>
+        private void MarkDataActive(ClientState client)
         {
             if (client == null)
                 return;
+
+            client.LastDataReceivedTime = DateTime.UtcNow;
+            client.LastActiveTime = DateTime.UtcNow;
+            SetDataActiveState(client, true);
+        }
+
+        /// <summary>
+        /// 设置数据激活状态,仅在状态发生变化时对外触发事件
+        /// </summary>
+        private void SetDataActiveState(ClientState client, bool isDataActive)
+        {
+            if (client == null || client.IsDataActive == isDataActive)
+                return;
+
+            client.IsDataActive = isDataActive;
+            DataActiveStateChanged?.Invoke(this,
+                new DataActiveStateChangedEventArgs(client.IpAddress, isDataActive, client.LastDataReceivedTime));
+        }
+
+        /// <summary>
+        /// 获取指定设备的数据激活状态(数据超时窗口内收到过有效数据)
+        /// </summary>
+        public bool IsDataActive(string ipAddress)
+        {
+            if (string.IsNullOrEmpty(ipAddress))
+                return false;
+            return _connectedClients.TryGetValue(ipAddress, out var client) && client.IsDataActive;
+        }
+
+        private void SafeCloseClient(ClientState client)
+        {
+            // 已关闭则直接返回,避免心跳线程与接收回调重复触发断开事件
+            if (client == null || client.IsClosed)
+                return;
+            client.IsClosed = true;
             try
             {
                 ClientDisconnected?.Invoke(this, new ClientDisconnectedEventArgs(client.ClientSocket, client.IpAddress));
                 _connectedClients.TryRemove(client.IpAddress, out _);
+                // 连接断开后数据激活状态置为失活
+                SetDataActiveState(client, false);
                 var socket = client.ClientSocket;
                 client.ClientSocket = null;
                 if (socket != null)

+ 32 - 0
Events/DataActiveStateChangedEventArgs.cs

@@ -0,0 +1,32 @@
+using System;
+
+namespace SWRIS.Events
+{
+    /// <summary>
+    /// 数据激活状态变化事件参数
+    /// </summary>
+    public class DataActiveStateChangedEventArgs : EventArgs
+    {
+        /// <summary>
+        /// 设备IP地址
+        /// </summary>
+        public string IpAddress { get; }
+
+        /// <summary>
+        /// 数据激活状态,true=数据在正常收发,false=数据静默
+        /// </summary>
+        public bool IsDataActive { get; }
+
+        /// <summary>
+        /// 最近一次收到有效数据的时间(UTC)
+        /// </summary>
+        public DateTime LastDataReceivedTime { get; }
+
+        public DataActiveStateChangedEventArgs(string ipAddress, bool isDataActive, DateTime lastDataReceivedTime)
+        {
+            IpAddress = ipAddress;
+            IsDataActive = isDataActive;
+            LastDataReceivedTime = lastDataReceivedTime;
+        }
+    }
+}

+ 13 - 0
MainWindow.xaml.cs

@@ -64,6 +64,7 @@ namespace SWRIS
             App.TcpServer.SetAbsolutePositionDataReceived += TcpServer_SetAbsolutePositionDataReceived;
             App.TcpServer.DebugMessageReceived += TcpServer_DebugMessageReceived;
             App.TcpServer.HeartbeatReceived += TcpServer_HeartbeatReceived;
+            App.TcpServer.DataActiveStateChanged += TcpServer_DataActiveStateChanged;
             App.TcpServer.UpgradedRequestResultReceived += TcpServer_UpgradedRequestResultReceived;
             App.Calibration.ConnectionStateChanged += Calibration_ConnectionStateChanged;
             App.Calibration.LimitStateChanged += Calibration_LimitStateChanged;
@@ -346,6 +347,17 @@ namespace SWRIS
             }
         }
         /// <summary>
+        /// 数据激活状态变化
+        /// </summary>
+        private void TcpServer_DataActiveStateChanged(object sender, DataActiveStateChangedEventArgs e)
+        {
+            var equipmentData = App.DataCenter.Equipments.FirstOrDefault(c => c.IpAddress == e.IpAddress);
+            if (equipmentData != null)
+            {
+                equipmentData.IsDataActive = e.IsDataActive;
+            }
+        }
+        /// <summary>
         /// 客户端连接断开
         /// </summary>
         private void TcpServer_ClientDisconnected(object sender, ClientDisconnectedEventArgs e)
@@ -354,6 +366,7 @@ namespace SWRIS
             if (equipmentData != null)
             {
                 equipmentData.IsConnect = false;
+                equipmentData.IsDataActive = false;
                 equipmentData.ClientSocket = null;
                 equipmentData.Speed = 0;
                 equipmentData.AbsolutePosition = 0;

+ 14 - 0
Models/ClientState.cs

@@ -11,6 +11,20 @@ namespace SWRIS.Models
         public byte[] Buffer { get; set; }
         public ByteCircularBuffer ReceiveBuffer { get; set; } = new ByteCircularBuffer(32768); // 添加环形缓冲区
         public DateTime LastActiveTime { get; set; } = DateTime.UtcNow;
+        /// <summary>
+        /// 最近一次收到有效数据(心跳包或业务数据包)的时间,从未收到过为 DateTime.MinValue
+        /// </summary>
+        public DateTime LastDataReceivedTime { get; set; } = DateTime.MinValue;
+        /// <summary>
+        /// 数据激活状态:数据超时窗口内收到过有效数据。
+        /// 用于区分"链路在收发数据但心跳缺失"与"链路真正静默",
+        /// 避免设备在持续上报实时数据时被判为离线。
+        /// </summary>
+        public bool IsDataActive { get; set; }
+        /// <summary>
+        /// 客户端是否已关闭,避免重复触发断开事件
+        /// </summary>
+        public bool IsClosed { get; set; }
         public ClientState(Socket clientSocket, string ipAddress, string serialNo)
         {
             ClientSocket = clientSocket;

+ 10 - 0
Models/EquipmentDataModel.cs

@@ -20,6 +20,7 @@ namespace SWRIS.Models
         private string ipAddress;
         private string serialNo;
         private bool isConnect = false;
+        private bool isDataActive = false;
         private int sensorCount = 4;
         private double liftHightRatio = 1;
         private double ropeLength = 100f; // 默认长度为100米
@@ -102,6 +103,15 @@ namespace SWRIS.Models
             set { isConnect = value; OnPropertyChanged(nameof(IsConnect)); }
         }
         /// <summary>
+        /// 数据激活状态:最近有正常数据收发。
+        /// 用于在心跳缺失但数据仍在上报时,区分链路真实静默与被误判离线。
+        /// </summary>
+        public bool IsDataActive
+        {
+            get => isDataActive;
+            set { isDataActive = value; OnPropertyChanged(nameof(IsDataActive)); }
+        }
+        /// <summary>
         /// 传感器数量
         /// </summary>
         public int SensorCount

+ 1 - 0
SWRIS.csproj

@@ -300,6 +300,7 @@
     <Compile Include="Enums\WireMaterialType.cs" />
     <Compile Include="Enums\WireSurfaceType.cs" />
     <Compile Include="Events\AlarmDataReceivedEventArgs.cs" />
+    <Compile Include="Events\DataActiveStateChangedEventArgs.cs" />
     <Compile Include="Events\UpgradedResultReceivedEventArgs.cs" />
     <Compile Include="Events\ClientConnectedEventArgs.cs" />
     <Compile Include="Events\ClientDisconnectedEventArgs.cs" />

+ 1 - 1
SWRIS.csproj.user

@@ -9,6 +9,6 @@
     <ErrorReportUrlHistory />
     <FallbackCulture>zh-CN</FallbackCulture>
     <VerifyUploadedFiles>false</VerifyUploadedFiles>
-    <ProjectView>ProjectFiles</ProjectView>
+    <ProjectView>ShowAllFiles</ProjectView>
   </PropertyGroup>
 </Project>