4 changed files with 779 additions and 101 deletions
@ -0,0 +1,45 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using TouchSocket.Core; |
|||
using TouchSocket.SerialPorts; |
|||
using TouchSocket.Sockets; |
|||
|
|||
namespace SunlightCentralizedControlManagement_SCCM_.UserClass |
|||
{ |
|||
public class AsyncSerialPortClient |
|||
{ |
|||
public static async Task PortClient(SerialPortClient portclient, string com, int BAUD) |
|||
{ |
|||
//var client = new SerialPortClient();
|
|||
portclient.Connecting = (client, e) => { return EasyTask.CompletedTask; };//即将连接到端口
|
|||
portclient.Connected = (client, e) => { return EasyTask.CompletedTask; };//成功连接到端口
|
|||
portclient.Closing = (client, e) => { return EasyTask.CompletedTask; };//即将从端口断开连接。此处仅主动断开才有效。
|
|||
portclient.Closed = (client, e) => { return EasyTask.CompletedTask; };//从端口断开连接,当连接不成功时不会触发。
|
|||
portclient.Received = async (c, e) => |
|||
{ |
|||
|
|||
await Console.Out.WriteLineAsync(e.ByteBlock.Span.ToString(Encoding.UTF8)); |
|||
}; |
|||
|
|||
await portclient.SetupAsync(new TouchSocketConfig() |
|||
.SetSerialPortOption(new SerialPortOption() |
|||
{ |
|||
BaudRate = BAUD,//波特率
|
|||
DataBits = 8,//数据位
|
|||
Parity = System.IO.Ports.Parity.None,//校验位
|
|||
PortName = com,//COM
|
|||
StopBits = System.IO.Ports.StopBits.One,//停止位
|
|||
}) |
|||
.SetSerialDataHandlingAdapter(() => new PeriodPackageAdapter() { CacheTimeout = TimeSpan.FromMilliseconds(100) }) |
|||
.ConfigurePlugins(a => |
|||
{ |
|||
//a.Add<MyPlugin>();
|
|||
})); |
|||
|
|||
await portclient.ConnectAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,617 @@ |
|||
using System; |
|||
using System.Collections.Concurrent; |
|||
using System.Collections.Generic; |
|||
using System.IO.Ports; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using TouchSocket.Core; |
|||
using TouchSocket.SerialPorts; |
|||
using TouchSocket.Sockets; |
|||
|
|||
namespace SunlightCentralizedControlManagement_SCCM_.UserClass |
|||
{ |
|||
#region 基础数据结构
|
|||
/// <summary>
|
|||
/// 485 设备地址
|
|||
/// </summary>
|
|||
public struct DeviceAddress |
|||
{ |
|||
public byte Value { get; } |
|||
|
|||
public DeviceAddress(byte address) |
|||
{ |
|||
if (address == 0) throw new ArgumentException("设备地址不能为0(广播地址)"); |
|||
Value = address; |
|||
} |
|||
|
|||
public static implicit operator byte(DeviceAddress address) => address.Value; |
|||
public static implicit operator DeviceAddress(byte address) => new DeviceAddress(address); |
|||
|
|||
public override string ToString() => $"0x{Value:X2}"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 485 命令帧
|
|||
/// </summary>
|
|||
public class CommandFrame |
|||
{ |
|||
public DeviceAddress DeviceAddress { get; set; } |
|||
public byte FunctionCode { get; set; } |
|||
public byte[] Data { get; set; } |
|||
public ushort CRC { get; set; } |
|||
|
|||
public byte[] ToBytes() |
|||
{ |
|||
// 实现帧序列化逻辑,包括CRC计算
|
|||
var dataLength = Data?.Length ?? 0; |
|||
var frame = new byte[3 + dataLength + 2]; // 地址+功能码+数据+CRC
|
|||
|
|||
frame[0] = DeviceAddress; |
|||
frame[1] = FunctionCode; |
|||
|
|||
if (dataLength > 0) |
|||
Array.Copy(Data, 0, frame, 2, dataLength); |
|||
|
|||
// 计算CRC (这里使用Modbus CRC算法示例)
|
|||
CRC = CalculateCRC(frame, 0, frame.Length - 2); |
|||
frame[frame.Length - 2] = (byte)(CRC & 0xFF); |
|||
frame[frame.Length - 1] = (byte)(CRC >> 8); |
|||
|
|||
return frame; |
|||
} |
|||
|
|||
private ushort CalculateCRC(byte[] data, int offset, int length) |
|||
{ |
|||
// 简化的CRC计算示例,实际应根据协议实现
|
|||
ushort crc = 0xFFFF; |
|||
for (int i = offset; i < offset + length; i++) |
|||
{ |
|||
crc ^= data[i]; |
|||
for (int j = 0; j < 8; j++) |
|||
{ |
|||
if ((crc & 0x0001) != 0) |
|||
{ |
|||
crc >>= 1; |
|||
crc ^= 0xA001; |
|||
} |
|||
else |
|||
{ |
|||
crc >>= 1; |
|||
} |
|||
} |
|||
} |
|||
return crc; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 响应帧
|
|||
/// </summary>
|
|||
public class ResponseFrame |
|||
{ |
|||
public DeviceAddress DeviceAddress { get; set; } |
|||
public byte FunctionCode { get; set; } |
|||
public byte[] Data { get; set; } |
|||
public ushort CRC { get; set; } |
|||
public bool IsValid { get; set; } |
|||
public string Error { get; set; } |
|||
|
|||
public static ResponseFrame FromBytes(byte[] data) |
|||
{ |
|||
// 实现帧解析逻辑,包括CRC验证
|
|||
var frame = new ResponseFrame(); |
|||
|
|||
if (data.Length < 4) // 最小帧长度
|
|||
{ |
|||
frame.IsValid = false; |
|||
frame.Error = "帧长度不足"; |
|||
return frame; |
|||
} |
|||
|
|||
frame.DeviceAddress = data[0]; |
|||
frame.FunctionCode = data[1]; |
|||
|
|||
// 检查CRC
|
|||
ushort receivedCRC = (ushort)(data[data.Length - 1] << 8 | data[data.Length - 2]); |
|||
ushort calculatedCRC = CalculateCRC(data, 0, data.Length - 2); |
|||
|
|||
if (receivedCRC != calculatedCRC) |
|||
{ |
|||
frame.IsValid = false; |
|||
frame.Error = "CRC校验失败"; |
|||
return frame; |
|||
} |
|||
|
|||
// 提取数据部分
|
|||
if (data.Length > 4) |
|||
{ |
|||
frame.Data = new byte[data.Length - 4]; |
|||
Array.Copy(data, 2, frame.Data, 0, frame.Data.Length); |
|||
} |
|||
|
|||
frame.IsValid = true; |
|||
return frame; |
|||
} |
|||
|
|||
private static ushort CalculateCRC(byte[] data, int offset, int length) |
|||
{ |
|||
// 与CommandFrame相同的CRC算法
|
|||
ushort crc = 0xFFFF; |
|||
for (int i = offset; i < offset + length; i++) |
|||
{ |
|||
crc ^= data[i]; |
|||
for (int j = 0; j < 8; j++) |
|||
{ |
|||
if ((crc & 0x0001) != 0) |
|||
{ |
|||
crc >>= 1; |
|||
crc ^= 0xA001; |
|||
} |
|||
else |
|||
{ |
|||
crc >>= 1; |
|||
} |
|||
} |
|||
} |
|||
return crc; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 通信任务
|
|||
/// </summary>
|
|||
public class CommunicationTask |
|||
{ |
|||
public DeviceAddress DeviceAddress { get; set; } |
|||
public CommandFrame Command { get; set; } |
|||
public TaskCompletionSource<ResponseFrame> CompletionSource { get; set; } |
|||
public int TimeoutMs { get; set; } = 1000; |
|||
public int RetryCount { get; set; } = 3; |
|||
} |
|||
#endregion
|
|||
|
|||
#region 异常类
|
|||
public class RS485CommunicationException : Exception |
|||
{ |
|||
public DeviceAddress DeviceAddress { get; } |
|||
public byte FunctionCode { get; } |
|||
|
|||
public RS485CommunicationException(DeviceAddress address, byte functionCode, string message) |
|||
: base($"设备 {address} 功能码 0x{functionCode:X2} 通信失败: {message}") |
|||
{ |
|||
DeviceAddress = address; |
|||
FunctionCode = functionCode; |
|||
} |
|||
} |
|||
|
|||
public class RS485TimeoutException : RS485CommunicationException |
|||
{ |
|||
public RS485TimeoutException(DeviceAddress address, byte functionCode) |
|||
: base(address, functionCode, "响应超时") |
|||
{ |
|||
} |
|||
} |
|||
|
|||
public class RS485CRCException : RS485CommunicationException |
|||
{ |
|||
public RS485CRCException(DeviceAddress address, byte functionCode) |
|||
: base(address, functionCode, "CRC校验失败") |
|||
{ |
|||
} |
|||
} |
|||
#endregion
|
|||
|
|||
#region 主站实现
|
|||
/// <summary>
|
|||
/// 485 主站实现
|
|||
/// </summary>
|
|||
public class RS485MasterStation : IDisposable |
|||
{ |
|||
private readonly SerialPortClient _serialPortClient; |
|||
private readonly ConcurrentDictionary<DeviceAddress, DeviceState> _deviceStates; |
|||
private readonly ConcurrentQueue<CommunicationTask> _taskQueue; |
|||
private readonly CancellationTokenSource _cancellationTokenSource; |
|||
private Task _processingTask; |
|||
private bool _isDisposed = false; |
|||
|
|||
// 响应超时时间(毫秒)
|
|||
public int ResponseTimeout { get; set; } = 1000; |
|||
|
|||
// 帧间延迟(毫秒)
|
|||
public int InterFrameDelay { get; set; } = 10; |
|||
|
|||
// 最大重试次数
|
|||
public int MaxRetries { get; set; } = 3; |
|||
|
|||
/// <summary>
|
|||
/// 设备状态
|
|||
/// </summary>
|
|||
private class DeviceState |
|||
{ |
|||
public DeviceAddress Address { get; set; } |
|||
public DateTime LastCommunication { get; set; } |
|||
public bool IsOnline { get; set; } |
|||
public int TimeoutCount { get; set; } |
|||
} |
|||
|
|||
public RS485MasterStation(string portName, int baudRate = 9600) |
|||
{ |
|||
_serialPortClient = new SerialPortClient(); |
|||
_deviceStates = new ConcurrentDictionary<DeviceAddress, DeviceState>(); |
|||
_taskQueue = new ConcurrentQueue<CommunicationTask>(); |
|||
_cancellationTokenSource = new CancellationTokenSource(); |
|||
|
|||
// 配置串口
|
|||
_serialPortClient.Setup(new TouchSocketConfig() |
|||
.SetSerialPortOption(new SerialPortOption() |
|||
{ |
|||
PortName = portName, |
|||
BaudRate = baudRate, |
|||
DataBits = 8, |
|||
Parity = Parity.None, |
|||
StopBits = StopBits.One |
|||
})); |
|||
|
|||
// 注册数据接收事件
|
|||
_serialPortClient.Received = OnDataReceived; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 启动主站
|
|||
/// </summary>
|
|||
public void Start() |
|||
{ |
|||
_serialPortClient.Connect(); |
|||
_processingTask = Task.Run(ProcessQueue, _cancellationTokenSource.Token); |
|||
Console.WriteLine("485主站已启动"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 停止主站
|
|||
/// </summary>
|
|||
public void Stop() |
|||
{ |
|||
_cancellationTokenSource.Cancel(); |
|||
_serialPortClient.Close(); |
|||
Console.WriteLine("485主站已停止"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 添加设备到监控列表
|
|||
/// </summary>
|
|||
public void AddDevice(DeviceAddress address) |
|||
{ |
|||
_deviceStates[address] = new DeviceState |
|||
{ |
|||
Address = address, |
|||
LastCommunication = DateTime.MinValue, |
|||
IsOnline = false, |
|||
TimeoutCount = 0 |
|||
}; |
|||
Console.WriteLine($"已添加设备: {address}"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 从监控列表移除设备
|
|||
/// </summary>
|
|||
public void RemoveDevice(DeviceAddress address) |
|||
{ |
|||
_deviceStates.TryRemove(address, out _); |
|||
Console.WriteLine($"已移除设备: {address}"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 发送命令到设备
|
|||
/// </summary>
|
|||
public Task<ResponseFrame> SendCommandAsync(DeviceAddress address, byte functionCode, byte[] data = null, int timeoutMs = 1000, int retryCount = 3) |
|||
{ |
|||
var command = new CommandFrame |
|||
{ |
|||
DeviceAddress = address, |
|||
FunctionCode = functionCode, |
|||
Data = data ?? new byte[0] |
|||
}; |
|||
|
|||
var task = new CommunicationTask |
|||
{ |
|||
DeviceAddress = address, |
|||
Command = command, |
|||
CompletionSource = new TaskCompletionSource<ResponseFrame>(), |
|||
TimeoutMs = timeoutMs, |
|||
RetryCount = retryCount |
|||
}; |
|||
|
|||
_taskQueue.Enqueue(task); |
|||
return task.CompletionSource.Task; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 读取保持寄存器
|
|||
/// </summary>
|
|||
public async Task<byte[]> ReadHoldingRegistersAsync(DeviceAddress address, ushort startAddress, ushort numberOfRegisters) |
|||
{ |
|||
var data = new byte[4]; |
|||
data[0] = (byte)(startAddress >> 8); // 起始地址高字节
|
|||
data[1] = (byte)(startAddress & 0xFF); // 起始地址低字节
|
|||
data[2] = (byte)(numberOfRegisters >> 8); // 寄存器数量高字节
|
|||
data[3] = (byte)(numberOfRegisters & 0xFF); // 寄存器数量低字节
|
|||
|
|||
var response = await SendCommandAsync(address, 0x03, data); |
|||
|
|||
if (!response.IsValid) |
|||
throw new RS485CommunicationException(address, 0x03, response.Error); |
|||
|
|||
return response.Data; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 写入单个寄存器
|
|||
/// </summary>
|
|||
public async Task WriteSingleRegisterAsync(DeviceAddress address, ushort registerAddress, ushort value) |
|||
{ |
|||
var data = new byte[4]; |
|||
data[0] = (byte)(registerAddress >> 8); // 寄存器地址高字节
|
|||
data[1] = (byte)(registerAddress & 0xFF); // 寄存器地址低字节
|
|||
data[2] = (byte)(value >> 8); // 值高字节
|
|||
data[3] = (byte)(value & 0xFF); // 值低字节
|
|||
|
|||
var response = await SendCommandAsync(address, 0x06, data); |
|||
|
|||
if (!response.IsValid) |
|||
throw new RS485CommunicationException(address, 0x06, response.Error); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理任务队列
|
|||
/// </summary>
|
|||
private async Task ProcessQueue() |
|||
{ |
|||
while (!_cancellationTokenSource.Token.IsCancellationRequested) |
|||
{ |
|||
if (_taskQueue.TryDequeue(out var task)) |
|||
{ |
|||
await ProcessTask(task); |
|||
} |
|||
else |
|||
{ |
|||
// 队列为空,执行设备轮询
|
|||
await PollDevices(); |
|||
await Task.Delay(100, _cancellationTokenSource.Token); // 短暂休眠
|
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 处理单个通信任务
|
|||
/// </summary>
|
|||
private async Task ProcessTask(CommunicationTask task) |
|||
{ |
|||
int retryCount = 0; |
|||
ResponseFrame response = null; |
|||
|
|||
while (retryCount < task.RetryCount && !_cancellationTokenSource.Token.IsCancellationRequested) |
|||
{ |
|||
try |
|||
{ |
|||
// 发送命令
|
|||
var commandData = task.Command.ToBytes(); |
|||
_serialPortClient.Send(commandData); |
|||
|
|||
// 等待响应
|
|||
var timeoutTask = Task.Delay(task.TimeoutMs, _cancellationTokenSource.Token); |
|||
var responseTask = task.CompletionSource.Task; |
|||
|
|||
var completedTask = await Task.WhenAny(responseTask, timeoutTask); |
|||
|
|||
if (completedTask == responseTask) |
|||
{ |
|||
response = await responseTask; |
|||
if (response.IsValid) |
|||
{ |
|||
UpdateDeviceState(task.DeviceAddress, true); |
|||
task.CompletionSource.SetResult(response); |
|||
return; |
|||
} |
|||
else |
|||
{ |
|||
throw new RS485CRCException(task.DeviceAddress, task.Command.FunctionCode); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
throw new RS485TimeoutException(task.DeviceAddress, task.Command.FunctionCode); |
|||
} |
|||
} |
|||
catch (RS485CommunicationException ex) |
|||
{ |
|||
retryCount++; |
|||
UpdateDeviceState(task.DeviceAddress, false); |
|||
|
|||
if (retryCount >= task.RetryCount) |
|||
{ |
|||
task.CompletionSource.SetException(ex); |
|||
return; |
|||
} |
|||
|
|||
// 等待一段时间后重试
|
|||
await Task.Delay(InterFrameDelay, _cancellationTokenSource.Token); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 轮询所有设备状态
|
|||
/// </summary>
|
|||
private async Task PollDevices() |
|||
{ |
|||
foreach (var deviceState in _deviceStates.Values.ToList()) |
|||
{ |
|||
// 检查设备是否需要轮询(例如,超过一定时间未通信)
|
|||
if ((DateTime.Now - deviceState.LastCommunication).TotalSeconds > 30) |
|||
{ |
|||
try |
|||
{ |
|||
// 发送诊断命令(如读取设备状态)
|
|||
await ReadHoldingRegistersAsync(deviceState.Address, 0, 1); |
|||
Console.WriteLine($"设备 {deviceState.Address} 轮询成功"); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Console.WriteLine($"设备 {deviceState.Address} 轮询失败: {ex.Message}"); |
|||
} |
|||
|
|||
// 添加帧间延迟
|
|||
await Task.Delay(InterFrameDelay, _cancellationTokenSource.Token); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 数据接收处理
|
|||
/// </summary>
|
|||
private Task OnDataReceived(ISerialPortClient client, ReceivedDataEventArgs e) |
|||
{ |
|||
try |
|||
{ |
|||
// 解析响应帧
|
|||
var response = ResponseFrame.FromBytes(e.ByteBlock.ReadBytesPackage()); |
|||
|
|||
if (response.IsValid) |
|||
{ |
|||
// 查找匹配的任务并设置结果
|
|||
foreach (var task in _taskQueue.Where(t => t.DeviceAddress.Value == response.DeviceAddress.Value)) |
|||
{ |
|||
task.CompletionSource.TrySetResult(response); |
|||
} |
|||
|
|||
UpdateDeviceState(response.DeviceAddress, true); |
|||
} |
|||
else |
|||
{ |
|||
Console.WriteLine($"收到无效响应: {response.Error}"); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
Console.WriteLine($"处理接收数据时出错: {ex.Message}"); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 更新设备状态
|
|||
/// </summary>
|
|||
private void UpdateDeviceState(DeviceAddress address, bool communicationSuccess) |
|||
{ |
|||
if (_deviceStates.TryGetValue(address, out var state)) |
|||
{ |
|||
state.LastCommunication = DateTime.Now; |
|||
|
|||
if (communicationSuccess) |
|||
{ |
|||
state.IsOnline = true; |
|||
state.TimeoutCount = 0; |
|||
} |
|||
else |
|||
{ |
|||
state.TimeoutCount++; |
|||
if (state.TimeoutCount > MaxRetries) |
|||
{ |
|||
state.IsOnline = false; |
|||
Console.WriteLine($"设备 {address} 已离线"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取所有设备状态
|
|||
/// </summary>
|
|||
public Dictionary<DeviceAddress, bool> GetDeviceStatuses() |
|||
{ |
|||
return _deviceStates.ToDictionary( |
|||
kvp => kvp.Key, |
|||
kvp => kvp.Value.IsOnline); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 资源释放
|
|||
/// </summary>
|
|||
public void Dispose() |
|||
{ |
|||
if (!_isDisposed) |
|||
{ |
|||
_isDisposed = true; |
|||
_cancellationTokenSource.Cancel(); |
|||
_serialPortClient.SafeDispose(); |
|||
_cancellationTokenSource.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
#endregion
|
|||
|
|||
#region 使用示例
|
|||
class Program |
|||
{ |
|||
static async Task Main(string[] args) |
|||
{ |
|||
// 创建485主站实例
|
|||
using (var master = new RS485MasterStation("COM1", 9600)) |
|||
|
|||
try |
|||
{ |
|||
// 添加要监控的设备
|
|||
master.AddDevice(0x01); |
|||
master.AddDevice(0x02); |
|||
master.AddDevice(0x03); |
|||
|
|||
// 启动主站
|
|||
master.Start(); |
|||
|
|||
// 等待一段时间让主站初始化
|
|||
await Task.Delay(1000); |
|||
|
|||
// 示例1: 读取设备1的保持寄存器
|
|||
try |
|||
{ |
|||
var data = await master.ReadHoldingRegistersAsync(0x01, 0, 10); |
|||
Console.WriteLine($"成功读取设备1的寄存器数据: {BitConverter.ToString(data)}"); |
|||
} |
|||
catch (RS485CommunicationException ex) |
|||
{ |
|||
Console.WriteLine(ex.Message); |
|||
} |
|||
|
|||
// 示例2: 向设备2写入寄存器值
|
|||
try |
|||
{ |
|||
await master.WriteSingleRegisterAsync(0x02, 0x1000, 0x1234); |
|||
Console.WriteLine("成功向设备2写入寄存器值"); |
|||
} |
|||
catch (RS485CommunicationException ex) |
|||
{ |
|||
Console.WriteLine(ex.Message); |
|||
} |
|||
|
|||
// 显示所有设备状态
|
|||
var statuses = master.GetDeviceStatuses(); |
|||
foreach (var status in statuses) |
|||
{ |
|||
Console.WriteLine($"设备 {status.Key}: {(status.Value ? "在线" : "离线")}"); |
|||
} |
|||
|
|||
// 保持运行
|
|||
Console.WriteLine("按任意键退出..."); |
|||
Console.ReadKey(); |
|||
} |
|||
finally |
|||
{ |
|||
master.Stop(); |
|||
} |
|||
} |
|||
} |
|||
#endregion
|
|||
} |
|||
Loading…
Reference in new issue