using EasyModbus;
|
using System;
|
using System.Threading;
|
using System.Threading.Tasks;
|
using System.Text;
|
|
namespace GSModbus
|
{
|
/// <summary>
|
/// Modbus TCP通信管理器 - 作为MES客户端与PLC服务器通信
|
/// </summary>
|
public class ModbusManager : IDisposable
|
{
|
#region 常量定义 - Modbus地址映射
|
|
// MES发送给PLC的地址 (MES → PLC)
|
/// <summary>
|
/// MES发送给PLC的心跳包地址 - 每秒交替发送0/1来维持连接
|
/// </summary>
|
private const int MES_HEARTBEAT_ADDRESS = 6000;
|
|
/// <summary>
|
/// MES发送的数据读取确认信号 - 当MES成功读取PLC数据后置1确认
|
/// </summary>
|
private const int MES_DATA_OK_ADDRESS = 6001;
|
|
// PLC发送给MES的地址 (PLC → MES) - 这些是我们需要读取的地址
|
/// <summary>
|
/// PLC发送的心跳包地址 - PLC每秒交替发送0/1
|
/// </summary>
|
private const int PLC_HEARTBEAT_ADDRESS = 6002;
|
|
/// <summary>
|
/// PLC数据准备就绪信号 - PLC置1表示数据已准备好供MES读取
|
/// </summary>
|
private const int PLC_DATA_OK_ADDRESS = 6003;
|
|
/// <summary>
|
/// 产品型号起始地址 - 10个字节存储产品型号(D6004-D6013)
|
/// </summary>
|
private const int PRODUCT_MODEL_START_ADDRESS = 6004;
|
|
/// <summary>
|
/// 测试工位地址 - 存储当前测试的工位号
|
/// </summary>
|
private const int TEST_STATION_ADDRESS = 6014;
|
|
/// <summary>
|
/// 产品码起始地址 - 10个字节存储产品码(D6015-D6024)
|
/// </summary>
|
private const int PRODUCT_CODE_START_ADDRESS = 6015;
|
|
/// <summary>
|
/// 测试结果地址 - 存储测试通过/失败状态
|
/// </summary>
|
private const int TEST_RESULT_ADDRESS = 6025;
|
|
/// <summary>
|
/// 记录时间起始地址 - 7个字节存储时间戳(D6026-D6032)
|
/// </summary>
|
private const int TIMESTAMP_START_ADDRESS = 6026;
|
|
/// <summary>
|
/// 测量数据起始地址 - 各种传感器测量值(D6033-D6048)
|
/// </summary>
|
private const int MEASUREMENT_DATA_START_ADDRESS = 6033;
|
|
// PLC服务器连接信息
|
private const string PLC_IP_ADDRESS = "192.168.3.250";
|
private const int PLC_PORT = 502;
|
|
#endregion
|
|
#region 私有字段
|
|
/// <summary>
|
/// EasyModbus TCP客户端实例
|
/// </summary>
|
private ModbusClient _modbusClient;
|
|
/// <summary>
|
/// 心跳定时器 - 用于定期发送心跳包
|
/// </summary>
|
private System.Timers.Timer _heartbeatTimer;
|
|
/// <summary>
|
/// 数据轮询定时器 - 用于定期读取PLC数据
|
/// </summary>
|
private System.Timers.Timer _dataPollingTimer;
|
|
/// <summary>
|
/// 心跳包状态 - 在0和1之间交替
|
/// </summary>
|
private byte _heartbeatState = 0;
|
|
/// <summary>
|
/// 连接状态标志
|
/// </summary>
|
private bool _isConnected = false;
|
|
/// <summary>
|
/// 取消令牌源 - 用于优雅停止异步操作
|
/// </summary>
|
private CancellationTokenSource _cancellationTokenSource;
|
|
#endregion
|
|
#region 事件定义
|
|
/// <summary>
|
/// 连接状态改变事件
|
/// </summary>
|
public event EventHandler<bool> ConnectionStatusChanged;
|
|
/// <summary>
|
/// 接收到PLC数据事件
|
/// </summary>
|
public event EventHandler<PlcData> DataReceived;
|
|
/// <summary>
|
/// 错误发生事件
|
/// </summary>
|
public event EventHandler<string> ErrorOccurred;
|
|
#endregion
|
|
#region 公共属性
|
|
/// <summary>
|
/// 获取当前连接状态
|
/// </summary>
|
public bool IsConnected => _isConnected;
|
|
#endregion
|
|
#region 构造函数
|
|
/// <summary>
|
/// 初始化Modbus管理器
|
/// </summary>
|
public ModbusManager()
|
{
|
_cancellationTokenSource = new CancellationTokenSource();
|
InitializeModbusClient();
|
InitializeTimers();
|
}
|
|
#endregion
|
|
#region 初始化方法
|
|
/// <summary>
|
/// 初始化Modbus TCP客户端
|
/// </summary>
|
private void InitializeModbusClient()
|
{
|
try
|
{
|
// 创建Modbus TCP客户端实例
|
_modbusClient = new ModbusClient(PLC_IP_ADDRESS, PLC_PORT);
|
|
// 设置连接超时时间(毫秒)
|
_modbusClient.ConnectionTimeout = 5000;
|
|
LogInfo($"Modbus客户端初始化完成,目标PLC地址: {PLC_IP_ADDRESS}:{PLC_PORT}");
|
}
|
catch (Exception ex)
|
{
|
LogError($"初始化Modbus客户端失败: {ex.Message}");
|
throw;
|
}
|
}
|
|
/// <summary>
|
/// 初始化定时器
|
/// </summary>
|
private void InitializeTimers()
|
{
|
// 心跳定时器 - 每1秒发送一次心跳
|
_heartbeatTimer = new System.Timers.Timer(1000);
|
_heartbeatTimer.Elapsed += OnHeartbeatTimerElapsed;
|
_heartbeatTimer.AutoReset = true;
|
|
// 数据轮询定时器 - 每500毫秒读取一次PLC数据
|
_dataPollingTimer = new System.Timers.Timer(500);
|
_dataPollingTimer.Elapsed += OnDataPollingTimerElapsed;
|
_dataPollingTimer.AutoReset = true;
|
|
LogInfo("定时器初始化完成 - 心跳周期:1秒, 数据轮询周期:500毫秒");
|
}
|
|
#endregion
|
|
#region 连接管理方法
|
|
/// <summary>
|
/// 连接到PLC服务器
|
/// </summary>
|
/// <returns>连接是否成功</returns>
|
public async Task<bool> ConnectAsync()
|
{
|
try
|
{
|
LogInfo($"正在连接到PLC服务器 {PLC_IP_ADDRESS}:{PLC_PORT}...");
|
|
// 尝试连接到PLC
|
await Task.Run(() => _modbusClient.Connect());
|
|
_isConnected = true;
|
LogInfo("成功连接到PLC服务器");
|
|
// 启动定时器
|
_heartbeatTimer.Start();
|
_dataPollingTimer.Start();
|
|
// 触发连接状态改变事件
|
ConnectionStatusChanged?.Invoke(this, true);
|
|
return true;
|
}
|
catch (Exception ex)
|
{
|
_isConnected = false;
|
LogError($"连接PLC失败: {ex.Message}");
|
ConnectionStatusChanged?.Invoke(this, false);
|
return false;
|
}
|
}
|
|
/// <summary>
|
/// 断开与PLC的连接
|
/// </summary>
|
public void Disconnect()
|
{
|
try
|
{
|
// 停止定时器
|
_heartbeatTimer?.Stop();
|
_dataPollingTimer?.Stop();
|
|
// 断开Modbus连接
|
if (_modbusClient?.Connected == true)
|
{
|
_modbusClient.Disconnect();
|
}
|
|
_isConnected = false;
|
LogInfo("已断开与PLC的连接");
|
|
// 触发连接状态改变事件
|
ConnectionStatusChanged?.Invoke(this, false);
|
}
|
catch (Exception ex)
|
{
|
LogError($"断开连接时发生错误: {ex.Message}");
|
}
|
}
|
|
#endregion
|
|
#region 定时器事件处理
|
|
/// <summary>
|
/// 心跳定时器事件 - 每秒向PLC发送心跳包
|
/// </summary>
|
private async void OnHeartbeatTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
|
{
|
if (!_isConnected || _cancellationTokenSource.Token.IsCancellationRequested)
|
return;
|
|
try
|
{
|
// 在0和1之间交替心跳状态
|
_heartbeatState = (byte)(_heartbeatState == 0 ? 1 : 0);
|
|
// 向PLC发送心跳包
|
await Task.Run(() =>
|
{
|
_modbusClient.WriteSingleRegister(MES_HEARTBEAT_ADDRESS, _heartbeatState);
|
});
|
|
LogDebug($"发送心跳包到PLC: {_heartbeatState}");
|
}
|
catch (Exception ex)
|
{
|
LogError($"发送心跳包失败: {ex.Message}");
|
// 心跳失败可能表示连接已断开
|
await HandleConnectionLoss();
|
}
|
}
|
|
/// <summary>
|
/// 数据轮询定时器事件 - 定期从PLC读取数据
|
/// </summary>
|
private async void OnDataPollingTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
|
{
|
if (!_isConnected || _cancellationTokenSource.Token.IsCancellationRequested)
|
return;
|
|
try
|
{
|
// 读取PLC数据
|
var plcData = await ReadPlcDataAsync();
|
if (plcData != null)
|
{
|
// 如果PLC表示数据已准备好,则发送确认信号
|
if (plcData.DataOkSignal == 1)
|
{
|
await SendDataReadConfirmationAsync();
|
|
// 触发数据接收事件
|
DataReceived?.Invoke(this, plcData);
|
|
LogInfo("成功读取PLC数据并发送确认信号");
|
}
|
}
|
}
|
catch (Exception ex)
|
{
|
LogError($"读取PLC数据失败: {ex.Message}");
|
}
|
}
|
|
#endregion
|
|
#region 数据读取方法
|
|
/// <summary>
|
/// 从PLC读取所有数据
|
/// </summary>
|
/// <returns>PLC数据结构</returns>
|
private async Task<PlcData> ReadPlcDataAsync()
|
{
|
try
|
{
|
// 读取控制信号
|
var controlSignals = await Task.Run(() =>
|
_modbusClient.ReadHoldingRegisters(PLC_HEARTBEAT_ADDRESS, 2));
|
|
// 检查PLC数据是否准备就绪
|
if (controlSignals[1] == 0) // DataOK信号为0表示数据未准备好
|
{
|
return null;
|
}
|
|
// 读取产品型号 (10个寄存器)
|
var productModelData = await Task.Run(() =>
|
_modbusClient.ReadHoldingRegisters(PRODUCT_MODEL_START_ADDRESS, 10));
|
|
// 读取测试工位
|
var testStationData = await Task.Run(() =>
|
_modbusClient.ReadHoldingRegisters(TEST_STATION_ADDRESS, 1));
|
|
// 读取产品码 (10个寄存器)
|
var productCodeData = await Task.Run(() =>
|
_modbusClient.ReadHoldingRegisters(PRODUCT_CODE_START_ADDRESS, 10));
|
|
// 读取测试结果
|
var testResultData = await Task.Run(() =>
|
_modbusClient.ReadHoldingRegisters(TEST_RESULT_ADDRESS, 1));
|
|
// 读取时间戳 (7个寄存器)
|
var timestampData = await Task.Run(() =>
|
_modbusClient.ReadHoldingRegisters(TIMESTAMP_START_ADDRESS, 7));
|
|
// 读取测量数据 (16个寄存器,从D6033到D6048)
|
var measurementData = await Task.Run(() =>
|
_modbusClient.ReadHoldingRegisters(MEASUREMENT_DATA_START_ADDRESS, 16));
|
|
// 解析并返回数据
|
return new PlcData
|
{
|
// 控制信号
|
PlcHeartbeat = (byte)controlSignals[0],
|
DataOkSignal = (byte)controlSignals[1],
|
|
// 产品信息
|
ProductModel = ParseStringFromRegisters(productModelData),
|
TestStation = (byte)testStationData[0],
|
ProductCode = ParseStringFromRegisters(productCodeData),
|
TestResult = (byte)testResultData[0],
|
|
// 时间戳
|
Timestamp = ParseTimestampFromRegisters(timestampData),
|
|
// 测量数据 (需要除以100来获得实际值,因为PLC以整数*100方式存储小数)
|
MinInstallSize = measurementData[0] / 100.0,
|
MaxInstallSize = measurementData[2] / 100.0,
|
Stroke = measurementData[4] / 100.0,
|
Speed = measurementData[6] / 100.0,
|
WorkingVoltage = measurementData[8] / 100.0,
|
WorkingCurrent = measurementData[10] / 100.0,
|
WorkingPressure = measurementData[12] / 100.0,
|
UpHallSensor = measurementData[14] / 100.0,
|
|
ReadTime = DateTime.Now
|
};
|
}
|
catch (Exception ex)
|
{
|
LogError($"读取PLC数据时发生错误: {ex.Message}");
|
throw;
|
}
|
}
|
|
#endregion
|
|
#region 数据解析方法
|
|
/// <summary>
|
/// 从寄存器数组解析字符串 - 大端模式,每个寄存器包含2个字符
|
/// </summary>
|
/// <param name="registers">寄存器数组</param>
|
/// <returns>解析出的字符串</returns>
|
private string ParseStringFromRegisters(int[] registers)
|
{
|
var bytes = new List<byte>();
|
|
foreach (var register in registers)
|
{
|
// 大端模式:高字节在前,低字节在后
|
bytes.Add((byte)((register >> 8) & 0xFF)); // 高字节
|
bytes.Add((byte)(register & 0xFF)); // 低字节
|
}
|
|
// 移除空字符并转换为字符串
|
var validBytes = bytes.TakeWhile(b => b != 0).ToArray();
|
return Encoding.ASCII.GetString(validBytes);
|
}
|
|
/// <summary>
|
/// 从寄存器数组解析时间戳 - 格式: YYYYMMDDHHMMSS
|
/// </summary>
|
/// <param name="registers">时间戳寄存器数组(7个寄存器)</param>
|
/// <returns>解析出的DateTime</returns>
|
private DateTime ParseTimestampFromRegisters(int[] registers)
|
{
|
try
|
{
|
// 将寄存器数据转换为字符串
|
var timestampString = ParseStringFromRegisters(registers);
|
|
// 解析时间戳字符串 (格式: YYYYMMDDHHMMSS)
|
if (timestampString.Length >= 14)
|
{
|
var year = int.Parse(timestampString.Substring(0, 4));
|
var month = int.Parse(timestampString.Substring(4, 2));
|
var day = int.Parse(timestampString.Substring(6, 2));
|
var hour = int.Parse(timestampString.Substring(8, 2));
|
var minute = int.Parse(timestampString.Substring(10, 2));
|
var second = int.Parse(timestampString.Substring(12, 2));
|
|
return new DateTime(year, month, day, hour, minute, second);
|
}
|
|
LogWarning($"时间戳格式无效: {timestampString}");
|
return DateTime.MinValue;
|
}
|
catch (Exception ex)
|
{
|
LogError($"解析时间戳失败: {ex.Message}");
|
return DateTime.MinValue;
|
}
|
}
|
|
#endregion
|
|
#region 数据发送方法
|
|
/// <summary>
|
/// 向PLC发送数据读取确认信号
|
/// </summary>
|
private async Task SendDataReadConfirmationAsync()
|
{
|
try
|
{
|
// 向PLC发送确认信号 (置1表示MES已成功读取数据)
|
await Task.Run(() =>
|
_modbusClient.WriteSingleRegister(MES_DATA_OK_ADDRESS, 1));
|
|
LogDebug("已向PLC发送数据读取确认信号");
|
}
|
catch (Exception ex)
|
{
|
LogError($"发送数据确认信号失败: {ex.Message}");
|
throw;
|
}
|
}
|
|
#endregion
|
|
#region 错误处理方法
|
|
/// <summary>
|
/// 处理连接丢失
|
/// </summary>
|
private async Task HandleConnectionLoss()
|
{
|
if (_isConnected)
|
{
|
LogWarning("检测到连接丢失,正在尝试重新连接...");
|
|
Disconnect();
|
|
// 等待3秒后重试连接
|
await Task.Delay(3000);
|
|
if (!_cancellationTokenSource.Token.IsCancellationRequested)
|
{
|
await ConnectAsync();
|
}
|
}
|
}
|
|
#endregion
|
|
#region 日志方法
|
|
private void LogInfo(string message)
|
{
|
Console.WriteLine($"[INFO] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
|
}
|
|
private void LogWarning(string message)
|
{
|
Console.WriteLine($"[WARN] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
|
}
|
|
private void LogError(string message)
|
{
|
Console.WriteLine($"[ERROR] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
|
ErrorOccurred?.Invoke(this, message);
|
}
|
|
private void LogDebug(string message)
|
{
|
Console.WriteLine($"[DEBUG] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
|
}
|
|
#endregion
|
|
#region IDisposable实现
|
|
public void Dispose()
|
{
|
_cancellationTokenSource?.Cancel();
|
|
_heartbeatTimer?.Stop();
|
_heartbeatTimer?.Dispose();
|
|
_dataPollingTimer?.Stop();
|
_dataPollingTimer?.Dispose();
|
|
Disconnect();
|
|
// EasyModbus的ModbusClient没有实现IDisposable接口
|
_modbusClient = null;
|
_cancellationTokenSource?.Dispose();
|
}
|
|
#endregion
|
}
|
|
/// <summary>
|
/// PLC数据结构 - 包含从PLC读取的所有数据
|
/// </summary>
|
public class PlcData
|
{
|
/// <summary>
|
/// PLC心跳包状态 (0或1)
|
/// </summary>
|
public byte PlcHeartbeat { get; set; }
|
|
/// <summary>
|
/// PLC数据准备就绪信号 (1表示数据已准备好)
|
/// </summary>
|
public byte DataOkSignal { get; set; }
|
|
/// <summary>
|
/// 产品型号 (10字节字符串)
|
/// </summary>
|
public string ProductModel { get; set; } = string.Empty;
|
|
/// <summary>
|
/// 测试工位号
|
/// </summary>
|
public byte TestStation { get; set; }
|
|
/// <summary>
|
/// 产品码 (10字节字符串)
|
/// </summary>
|
public string ProductCode { get; set; } = string.Empty;
|
|
/// <summary>
|
/// 测试结果 (1表示通过,0表示失败)
|
/// </summary>
|
public byte TestResult { get; set; }
|
|
/// <summary>
|
/// 记录时间戳
|
/// </summary>
|
public DateTime Timestamp { get; set; }
|
|
/// <summary>
|
/// 最小安装尺寸 (毫米,保留2位小数)
|
/// </summary>
|
public double MinInstallSize { get; set; }
|
|
/// <summary>
|
/// 最大安装尺寸 (毫米,保留2位小数)
|
/// </summary>
|
public double MaxInstallSize { get; set; }
|
|
/// <summary>
|
/// 行程 (毫米,保留2位小数)
|
/// </summary>
|
public double Stroke { get; set; }
|
|
/// <summary>
|
/// 速度 (单位根据具体需求确定,保留2位小数)
|
/// </summary>
|
public double Speed { get; set; }
|
|
/// <summary>
|
/// 工作电压 (伏特,保留2位小数)
|
/// </summary>
|
public double WorkingVoltage { get; set; }
|
|
/// <summary>
|
/// 工作电流 (安培,保留2位小数)
|
/// </summary>
|
public double WorkingCurrent { get; set; }
|
|
/// <summary>
|
/// 工作压力 (压力单位,保留2位小数)
|
/// </summary>
|
public double WorkingPressure { get; set; }
|
|
/// <summary>
|
/// 上升霍尔传感器读数 (保留2位小数)
|
/// </summary>
|
public double UpHallSensor { get; set; }
|
|
/// <summary>
|
/// 数据读取时间 (本地时间)
|
/// </summary>
|
public DateTime ReadTime { get; set; }
|
}
|
}
|