using EasyModbus;
|
using GSModbus.Config;
|
using GSModbus.Database;
|
using System;
|
using System.Threading;
|
using System.Threading.Tasks;
|
|
namespace GSModbus
|
{
|
/// <summary>
|
/// 通用Modbus TCP通信管理器 - 支持配置驱动的动态通信
|
/// </summary>
|
public class UniversalModbusManager : IDisposable
|
{
|
#region 私有字段
|
|
/// <summary>
|
/// Modbus配置
|
/// </summary>
|
private ModbusConfiguration? _configuration;
|
|
/// <summary>
|
/// EasyModbus TCP客户端实例
|
/// </summary>
|
private ModbusClient? _modbusClient;
|
|
/// <summary>
|
/// 心跳定时器
|
/// </summary>
|
private System.Timers.Timer? _heartbeatTimer;
|
|
/// <summary>
|
/// 数据轮询定时器
|
/// </summary>
|
private System.Timers.Timer? _dataPollingTimer;
|
|
/// <summary>
|
/// 心跳包状态
|
/// </summary>
|
private byte _heartbeatState = 0;
|
|
/// <summary>
|
/// 连接状态标志
|
/// </summary>
|
private bool _isConnected = false;
|
|
/// <summary>
|
/// 取消令牌源
|
/// </summary>
|
private CancellationTokenSource? _cancellationTokenSource;
|
|
/// <summary>
|
/// 重试计数器
|
/// </summary>
|
private int _retryCount = 0;
|
|
/// <summary>
|
/// 数据库管理器
|
/// </summary>
|
private DatabaseManager? _databaseManager;
|
|
#endregion
|
|
#region 事件定义
|
|
/// <summary>
|
/// 连接状态改变事件
|
/// </summary>
|
public event EventHandler<bool>? ConnectionStatusChanged;
|
|
/// <summary>
|
/// 接收到动态数据事件
|
/// </summary>
|
public event EventHandler<DynamicModbusData>? DataReceived;
|
|
/// <summary>
|
/// 错误发生事件
|
/// </summary>
|
public event EventHandler<string>? ErrorOccurred;
|
|
/// <summary>
|
/// 通信统计更新事件
|
/// </summary>
|
public event EventHandler<CommunicationStats>? StatsUpdated;
|
|
/// <summary>
|
/// 数据库日志事件
|
/// </summary>
|
public event EventHandler<string>? DatabaseLogOccurred;
|
|
/// <summary>
|
/// 数据库错误事件
|
/// </summary>
|
public event EventHandler<string>? DatabaseErrorOccurred;
|
|
#endregion
|
|
#region 公共属性
|
|
/// <summary>
|
/// 获取当前连接状态
|
/// </summary>
|
public bool IsConnected => _isConnected;
|
|
/// <summary>
|
/// 获取当前配置
|
/// </summary>
|
public ModbusConfiguration? Configuration => _configuration;
|
|
/// <summary>
|
/// 通信统计信息
|
/// </summary>
|
public CommunicationStats Stats { get; private set; } = new();
|
|
#endregion
|
|
#region 构造函数
|
|
/// <summary>
|
/// 初始化通用Modbus管理器
|
/// </summary>
|
/// <param name="configuration">Modbus配置</param>
|
public UniversalModbusManager(ModbusConfiguration? configuration = null)
|
{
|
_cancellationTokenSource = new CancellationTokenSource();
|
|
if (configuration != null)
|
{
|
SetConfiguration(configuration);
|
}
|
}
|
|
#endregion
|
|
#region 配置管理方法
|
|
/// <summary>
|
/// 设置新的配置
|
/// </summary>
|
/// <param name="configuration">新配置</param>
|
public void SetConfiguration(ModbusConfiguration configuration)
|
{
|
// 如果当前已连接,需要先断开
|
if (_isConnected)
|
{
|
LogWarning("配置更改时自动断开连接");
|
Disconnect();
|
}
|
|
// 释放现有数据库管理器
|
_databaseManager?.Dispose();
|
_databaseManager = null;
|
|
_configuration = configuration;
|
|
// 释放现有资源
|
DisposeTimers();
|
|
// 初始化新配置
|
InitializeWithConfiguration();
|
|
LogInfo($"配置已更新: {configuration.ProjectName}");
|
}
|
|
/// <summary>
|
/// 根据配置初始化组件
|
/// </summary>
|
private void InitializeWithConfiguration()
|
{
|
if (_configuration == null)
|
{
|
LogError("无法初始化:配置为空");
|
return;
|
}
|
|
try
|
{
|
// 创建Modbus客户端
|
_modbusClient = new ModbusClient(_configuration.Connection.IpAddress, _configuration.Connection.Port);
|
_modbusClient.ConnectionTimeout = _configuration.Connection.ConnectionTimeoutMs;
|
|
// 初始化定时器
|
InitializeTimers();
|
|
// 初始化数据库管理器
|
InitializeDatabaseManager();
|
|
LogInfo($"已初始化Modbus客户端: {_configuration.Connection.IpAddress}:{_configuration.Connection.Port}");
|
}
|
catch (Exception ex)
|
{
|
LogError($"初始化配置时发生错误: {ex.Message}");
|
throw;
|
}
|
}
|
|
/// <summary>
|
/// 初始化定时器
|
/// </summary>
|
private void InitializeTimers()
|
{
|
if (_configuration == null) return;
|
|
// 心跳定时器
|
_heartbeatTimer = new System.Timers.Timer(_configuration.Communication.HeartbeatIntervalMs);
|
_heartbeatTimer.Elapsed += OnHeartbeatTimerElapsed;
|
_heartbeatTimer.AutoReset = true;
|
|
// 数据轮询定时器
|
_dataPollingTimer = new System.Timers.Timer(_configuration.Communication.DataPollingIntervalMs);
|
_dataPollingTimer.Elapsed += OnDataPollingTimerElapsed;
|
_dataPollingTimer.AutoReset = true;
|
|
LogInfo($"定时器已初始化 - 心跳:{_configuration.Communication.HeartbeatIntervalMs}ms, 轮询:{_configuration.Communication.DataPollingIntervalMs}ms");
|
}
|
|
/// <summary>
|
/// 初始化数据库管理器
|
/// </summary>
|
private void InitializeDatabaseManager()
|
{
|
if (_configuration?.Database?.Enabled == true)
|
{
|
try
|
{
|
_databaseManager = new DatabaseManager(
|
_configuration.Database,
|
_configuration.ProjectName,
|
_configuration.Connection.IpAddress
|
);
|
|
// 订阅数据库事件
|
_databaseManager.DatabaseLogOccurred += (sender, message) => DatabaseLogOccurred?.Invoke(this, message);
|
_databaseManager.DatabaseErrorOccurred += (sender, error) => DatabaseErrorOccurred?.Invoke(this, error);
|
|
LogInfo("数据库管理器初始化成功");
|
}
|
catch (Exception ex)
|
{
|
LogError($"初始化数据库管理器失败: {ex.Message}");
|
}
|
}
|
else
|
{
|
LogInfo("数据库功能未启用");
|
}
|
}
|
|
#endregion
|
|
#region 连接管理方法
|
|
/// <summary>
|
/// 连接到PLC服务器
|
/// </summary>
|
/// <returns>连接是否成功</returns>
|
public async Task<bool> ConnectAsync()
|
{
|
if (_configuration == null)
|
{
|
LogError("无法连接:配置未设置");
|
return false;
|
}
|
|
if (_modbusClient == null)
|
{
|
LogError("无法连接:Modbus客户端未初始化");
|
return false;
|
}
|
|
try
|
{
|
LogInfo($"正在连接到PLC服务器 {_configuration.Connection.IpAddress}:{_configuration.Connection.Port}...");
|
|
// 尝试连接到PLC
|
await Task.Run(() => _modbusClient.Connect());
|
|
_isConnected = true;
|
_retryCount = 0;
|
Stats.ConnectionAttempts++;
|
Stats.LastConnectedTime = DateTime.Now;
|
|
LogInfo("成功连接到PLC服务器");
|
|
// 记录数据库日志
|
await _databaseManager?.LogCommunicationAsync(EventTypes.Connected, "成功连接到PLC服务器", true)!;
|
|
// 启动定时器
|
_heartbeatTimer?.Start();
|
_dataPollingTimer?.Start();
|
|
// 触发连接状态改变事件
|
ConnectionStatusChanged?.Invoke(this, true);
|
|
return true;
|
}
|
catch (Exception ex)
|
{
|
_isConnected = false;
|
Stats.ConnectionFailures++;
|
LogError($"连接PLC失败: {ex.Message}");
|
|
// 记录数据库错误日志
|
await _databaseManager?.LogErrorAsync("ConnectionError", $"连接PLC失败: {ex.Message}", ex, ErrorSeverity.High, _retryCount)!;
|
await _databaseManager?.LogCommunicationAsync(EventTypes.ConnectionError, ex.Message, false)!;
|
|
ConnectionStatusChanged?.Invoke(this, false);
|
|
// 如果启用自动重连,安排重连
|
if (_configuration.Communication.EnableAutoReconnect &&
|
_retryCount < _configuration.Communication.MaxRetryCount)
|
{
|
ScheduleReconnect();
|
}
|
|
return false;
|
}
|
}
|
|
/// <summary>
|
/// 断开与PLC的连接
|
/// </summary>
|
public async void Disconnect()
|
{
|
try
|
{
|
// 停止定时器
|
_heartbeatTimer?.Stop();
|
_dataPollingTimer?.Stop();
|
|
// 断开Modbus连接
|
if (_modbusClient?.Connected == true)
|
{
|
_modbusClient.Disconnect();
|
}
|
|
_isConnected = false;
|
Stats.LastDisconnectedTime = DateTime.Now;
|
LogInfo("已断开与PLC的连接");
|
|
// 记录数据库日志
|
await _databaseManager?.LogCommunicationAsync(EventTypes.Disconnected, "手动断开PLC连接", true)!;
|
|
// 触发连接状态改变事件
|
ConnectionStatusChanged?.Invoke(this, false);
|
}
|
catch (Exception ex)
|
{
|
LogError($"断开连接时发生错误: {ex.Message}");
|
}
|
}
|
|
/// <summary>
|
/// 安排自动重连
|
/// </summary>
|
private void ScheduleReconnect()
|
{
|
if (_configuration == null || _cancellationTokenSource?.Token.IsCancellationRequested == true)
|
return;
|
|
_retryCount++;
|
var delay = _configuration.Communication.ReconnectDelayMs;
|
|
LogWarning($"将在 {delay}ms 后尝试第 {_retryCount} 次重连");
|
|
Task.Delay(delay, _cancellationTokenSource.Token).ContinueWith(async _ =>
|
{
|
if (!_cancellationTokenSource.Token.IsCancellationRequested)
|
{
|
await ConnectAsync();
|
}
|
}, _cancellationTokenSource.Token);
|
}
|
|
#endregion
|
|
#region 定时器事件处理
|
|
/// <summary>
|
/// 心跳定时器事件
|
/// </summary>
|
private async void OnHeartbeatTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
{
|
if (!_isConnected || _configuration == null || _modbusClient == null ||
|
_cancellationTokenSource?.Token.IsCancellationRequested == true)
|
return;
|
|
try
|
{
|
// 在0和1之间交替心跳状态
|
_heartbeatState = (byte)(_heartbeatState == 0 ? 1 : 0);
|
|
// 向PLC发送心跳包
|
var heartbeatAddress = _configuration.OutputAddresses.HeartbeatAddress.Address;
|
await Task.Run(() => _modbusClient.WriteSingleRegister(heartbeatAddress, _heartbeatState));
|
|
Stats.HeartbeatsSent++;
|
LogDebug($"发送心跳包到地址 {heartbeatAddress}: {_heartbeatState}");
|
}
|
catch (Exception ex)
|
{
|
Stats.CommunicationErrors++;
|
LogError($"发送心跳包失败: {ex.Message}");
|
await HandleConnectionLoss();
|
}
|
}
|
|
/// <summary>
|
/// 数据轮询定时器事件
|
/// </summary>
|
private async void OnDataPollingTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
{
|
if (!_isConnected || _configuration == null || _modbusClient == null ||
|
_cancellationTokenSource?.Token.IsCancellationRequested == true)
|
return;
|
|
try
|
{
|
// 读取PLC数据
|
var dynamicData = await ReadDynamicDataAsync();
|
if (dynamicData != null)
|
{
|
// 检查是否需要发送确认信号
|
var dataReadySignal = GetDataReadySignal(dynamicData);
|
if (dataReadySignal == 1)
|
{
|
await SendDataReadConfirmationAsync();
|
|
// 保存到数据库
|
await _databaseManager?.SaveModbusDataAsync(dynamicData)!;
|
await _databaseManager?.LogCommunicationAsync(EventTypes.DataReceived, "成功读取PLC数据", true)!;
|
|
// 触发数据接收事件
|
DataReceived?.Invoke(this, dynamicData);
|
Stats.SuccessfulReads++;
|
|
LogDebug("成功读取PLC数据并发送确认信号");
|
}
|
}
|
}
|
catch (Exception ex)
|
{
|
Stats.CommunicationErrors++;
|
LogError($"读取PLC数据失败: {ex.Message}");
|
|
// 记录数据库错误日志
|
await _databaseManager?.LogErrorAsync("DataReadError", ex.Message, ex, ErrorSeverity.Medium)!;
|
await _databaseManager?.LogCommunicationAsync(EventTypes.DataReadError, ex.Message, false)!;
|
}
|
finally
|
{
|
// 更新统计信息
|
StatsUpdated?.Invoke(this, Stats);
|
}
|
}
|
|
#endregion
|
|
#region 数据读取方法
|
|
/// <summary>
|
/// 读取动态配置的数据
|
/// </summary>
|
/// <returns>动态数据对象</returns>
|
private async Task<DynamicModbusData?> ReadDynamicDataAsync()
|
{
|
if (_configuration?.InputAddresses == null || _modbusClient == null)
|
return null;
|
|
var dynamicData = new DynamicModbusData
|
{
|
SourceConfiguration = _configuration,
|
ReadTime = DateTime.Now
|
};
|
|
try
|
{
|
// 读取控制信号
|
await ReadFieldGroup(dynamicData, _configuration.InputAddresses.ControlSignals, "控制信号");
|
|
// 读取产品数据
|
await ReadFieldGroup(dynamicData, _configuration.InputAddresses.ProductData, "产品数据");
|
|
// 读取测量数据
|
await ReadFieldGroup(dynamicData, _configuration.InputAddresses.MeasurementData, "测量数据");
|
|
return dynamicData;
|
}
|
catch (Exception ex)
|
{
|
LogError($"读取动态数据时发生错误: {ex.Message}");
|
return null;
|
}
|
}
|
|
/// <summary>
|
/// 读取字段组数据
|
/// </summary>
|
/// <param name="dynamicData">动态数据容器</param>
|
/// <param name="fieldGroup">字段组配置</param>
|
/// <param name="groupName">组名称(用于日志)</param>
|
private async Task ReadFieldGroup(DynamicModbusData dynamicData, Dictionary<string, DataField> fieldGroup, string groupName)
|
{
|
if (_modbusClient == null)
|
{
|
LogWarning($"Modbus客户端为null,无法读取 {groupName}");
|
return;
|
}
|
|
if (!_isConnected)
|
{
|
LogWarning($"Modbus未连接,无法读取 {groupName}");
|
return;
|
}
|
|
if (fieldGroup == null || fieldGroup.Count == 0)
|
{
|
LogDebug($"{groupName} 没有配置字段,跳过读取");
|
return;
|
}
|
|
foreach (var kvp in fieldGroup)
|
{
|
var fieldName = kvp.Key;
|
var fieldConfig = kvp.Value;
|
|
try
|
{
|
// 检查Modbus客户端状态
|
if (_modbusClient == null || !_modbusClient.Connected)
|
{
|
LogWarning($"Modbus客户端未连接,跳过字段 '{fieldName}'");
|
continue;
|
}
|
|
// 读取原始寄存器数据
|
var registers = await Task.Run(() =>
|
_modbusClient.ReadHoldingRegisters(fieldConfig.Address, fieldConfig.Length));
|
|
// 检查读取结果是否为null
|
if (registers == null)
|
{
|
LogWarning($"字段 '{fieldName}' 读取结果为null,地址: {fieldConfig.Address}, 长度: {fieldConfig.Length}");
|
continue;
|
}
|
|
// 验证数据长度
|
if (!ModbusDataParser.ValidateRegisterLength(registers, fieldConfig))
|
{
|
LogWarning($"字段 '{fieldName}' 的数据长度不足:期望{fieldConfig.Length},实际{registers?.Length ?? 0}");
|
continue;
|
}
|
|
// 解析数据
|
var parsedValue = ModbusDataParser.ParseFieldData(registers, fieldConfig);
|
|
// 存储到动态数据容器
|
dynamicData.SetFieldData(fieldName, parsedValue, registers);
|
|
LogDebug($"成功读取字段 '{fieldName}': {parsedValue}");
|
}
|
catch (Exception ex)
|
{
|
LogError($"读取字段 '{fieldName}' 失败: {ex.Message}");
|
|
// 记录详细错误信息
|
await _databaseManager?.LogErrorAsync(
|
"FieldReadError",
|
$"读取字段 '{fieldName}' 失败: {ex.Message}",
|
ex,
|
ErrorSeverity.Medium)!;
|
|
// 继续读取其他字段
|
}
|
}
|
}
|
|
/// <summary>
|
/// 获取数据就绪信号
|
/// </summary>
|
private byte GetDataReadySignal(DynamicModbusData data)
|
{
|
// 查找数据就绪信号字段
|
var controlSignals = _configuration?.InputAddresses.ControlSignals;
|
if (controlSignals != null)
|
{
|
foreach (var kvp in controlSignals)
|
{
|
var fieldConfig = kvp.Value;
|
if (fieldConfig.Description?.Contains("数据") == true &&
|
fieldConfig.Description?.Contains("就绪") == true)
|
{
|
return data.GetByte(kvp.Key);
|
}
|
}
|
}
|
|
// 如果没有找到明确的数据就绪信号,返回1(假设数据总是就绪)
|
return 1;
|
}
|
|
#endregion
|
|
#region 数据发送方法
|
|
/// <summary>
|
/// 向PLC发送数据读取确认信号
|
/// </summary>
|
private async Task SendDataReadConfirmationAsync()
|
{
|
if (_configuration == null || _modbusClient == null) return;
|
|
try
|
{
|
var confirmAddress = _configuration.OutputAddresses.DataConfirmationAddress.Address;
|
await Task.Run(() => _modbusClient.WriteSingleRegister(confirmAddress, 1));
|
|
Stats.ConfirmationsSent++;
|
LogDebug($"已向地址 {confirmAddress} 发送数据读取确认信号");
|
}
|
catch (Exception ex)
|
{
|
LogError($"发送数据确认信号失败: {ex.Message}");
|
throw;
|
}
|
}
|
|
#endregion
|
|
#region 错误处理方法
|
|
/// <summary>
|
/// 处理连接丢失
|
/// </summary>
|
private async Task HandleConnectionLoss()
|
{
|
if (_isConnected)
|
{
|
LogWarning("检测到连接丢失");
|
|
Disconnect();
|
|
// 如果启用自动重连
|
if (_configuration?.Communication.EnableAutoReconnect == true &&
|
_retryCount < _configuration.Communication.MaxRetryCount)
|
{
|
ScheduleReconnect();
|
}
|
}
|
}
|
|
#endregion
|
|
#region 资源清理方法
|
|
/// <summary>
|
/// 释放定时器资源
|
/// </summary>
|
private void DisposeTimers()
|
{
|
_heartbeatTimer?.Stop();
|
_heartbeatTimer?.Dispose();
|
_heartbeatTimer = null;
|
|
_dataPollingTimer?.Stop();
|
_dataPollingTimer?.Dispose();
|
_dataPollingTimer = null;
|
}
|
|
public void Dispose()
|
{
|
_cancellationTokenSource?.Cancel();
|
|
DisposeTimers();
|
Disconnect();
|
|
_modbusClient = null;
|
_databaseManager?.Dispose();
|
_databaseManager = null;
|
_cancellationTokenSource?.Dispose();
|
}
|
|
#endregion
|
|
#region 日志方法
|
|
private void LogInfo(string message)
|
{
|
Console.WriteLine($"[UNIVERSAL-INFO] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
|
}
|
|
private void LogWarning(string message)
|
{
|
Console.WriteLine($"[UNIVERSAL-WARN] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
|
}
|
|
private void LogError(string message)
|
{
|
Console.WriteLine($"[UNIVERSAL-ERROR] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
|
ErrorOccurred?.Invoke(this, message);
|
}
|
|
private void LogDebug(string message)
|
{
|
// 只在Debug模式下输出详细日志
|
#if DEBUG
|
Console.WriteLine($"[UNIVERSAL-DEBUG] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
|
#endif
|
}
|
|
#endregion
|
}
|
|
#region 通信统计类
|
|
/// <summary>
|
/// 通信统计信息
|
/// </summary>
|
public class CommunicationStats
|
{
|
/// <summary>
|
/// 连接尝试次数
|
/// </summary>
|
public long ConnectionAttempts { get; set; }
|
|
/// <summary>
|
/// 连接失败次数
|
/// </summary>
|
public long ConnectionFailures { get; set; }
|
|
/// <summary>
|
/// 发送的心跳包数量
|
/// </summary>
|
public long HeartbeatsSent { get; set; }
|
|
/// <summary>
|
/// 发送的确认信号数量
|
/// </summary>
|
public long ConfirmationsSent { get; set; }
|
|
/// <summary>
|
/// 成功读取次数
|
/// </summary>
|
public long SuccessfulReads { get; set; }
|
|
/// <summary>
|
/// 通信错误次数
|
/// </summary>
|
public long CommunicationErrors { get; set; }
|
|
/// <summary>
|
/// 最后连接时间
|
/// </summary>
|
public DateTime? LastConnectedTime { get; set; }
|
|
/// <summary>
|
/// 最后断开时间
|
/// </summary>
|
public DateTime? LastDisconnectedTime { get; set; }
|
|
/// <summary>
|
/// 获取连接成功率
|
/// </summary>
|
public double ConnectionSuccessRate =>
|
ConnectionAttempts > 0 ? (double)(ConnectionAttempts - ConnectionFailures) / ConnectionAttempts * 100 : 0;
|
|
/// <summary>
|
/// 获取连接成功率
|
/// </summary>
|
public double TotalConnectionAttempts => ConnectionAttempts;
|
|
/// <summary>
|
/// 获取成功连接数
|
/// </summary>
|
public double SuccessfulConnections => ConnectionAttempts - ConnectionFailures;
|
|
/// <summary>
|
/// 获取总读取数
|
/// </summary>
|
public double TotalDataReads => SuccessfulReads + CommunicationErrors;
|
|
/// <summary>
|
/// 获取数据读取成功率
|
/// </summary>
|
public double DataReadSuccessRate =>
|
TotalDataReads > 0 ? SuccessfulReads / TotalDataReads * 100 : 0;
|
|
/// <summary>
|
/// 获取总错误数
|
/// </summary>
|
public double TotalErrors => CommunicationErrors + ConnectionFailures;
|
|
/// <summary>
|
/// 平均响应时间(毫秒)
|
/// </summary>
|
public double AverageResponseTimeMs { get; set; } = 0;
|
|
/// <summary>
|
/// 最大响应时间(毫秒)
|
/// </summary>
|
public int MaxResponseTimeMs { get; set; } = 0;
|
|
/// <summary>
|
/// 最小响应时间(毫秒)
|
/// </summary>
|
public int MinResponseTimeMs { get; set; } = int.MaxValue;
|
|
/// <summary>
|
/// 获取统计摘要字符串
|
/// </summary>
|
public string GetSummary()
|
{
|
return $"连接: {ConnectionAttempts - ConnectionFailures}/{ConnectionAttempts} " +
|
$"({ConnectionSuccessRate:F1}%), " +
|
$"读取: {SuccessfulReads}, " +
|
$"错误: {CommunicationErrors}";
|
}
|
}
|
|
#endregion
|
}
|