本文已收錄至:開源 DotNetty 實現的 Modbus TCP/IP 協議html
Client
public class ModbusClient { public string Ip { get; } public int Port { get; } public short UnitIdentifier { get; } public IChannel Channel { get; private set; } private MultithreadEventLoopGroup group; private ConnectionState connectionState; private ushort transactionIdentifier; private readonly string handlerName = "response"; public ModbusClient(short unitIdentifier, string ip, int port = 502) { Ip = ip; Port = port; UnitIdentifier = unitIdentifier; connectionState = ConnectionState.NotConnected; } public async Task Connect() { group = new MultithreadEventLoopGroup(); try { var bootstrap = new Bootstrap(); bootstrap .Group(group) .Channel<TcpSocketChannel>() .Option(ChannelOption.TcpNodelay, true) .Handler(new ActionChannelInitializer<ISocketChannel>(channel => { IChannelPipeline pipeline = channel.Pipeline; pipeline.AddLast("encoder", new ModbusEncoder()); pipeline.AddLast("decoder", new ModbusDecoder(false)); pipeline.AddLast(handlerName, new ModbusResponseHandler()); })); connectionState = ConnectionState.Pending; Channel = await bootstrap.ConnectAsync(new IPEndPoint(IPAddress.Parse(Ip), Port)); connectionState = ConnectionState.Connected; } catch (Exception exception) { throw exception; } } public async Task Close() { if (ConnectionState.Connected == connectionState) { try { await Channel.CloseAsync(); } finally { await group.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)); connectionState = ConnectionState.NotConnected; } } } public ushort CallModbusFunction(ModbusFunction function) { if (ConnectionState.Connected != connectionState || Channel == null) { throw new Exception("Not connected!"); } SetTransactionIdentifier(); ModbusHeader header = new ModbusHeader(transactionIdentifier, UnitIdentifier); ModbusFrame frame = new ModbusFrame(header, function); Channel.WriteAndFlushAsync(frame); return transactionIdentifier; } public T CallModbusFunctionSync<T>(ModbusFunction function) where T : ModbusFunction { var transactionIdentifier = CallModbusFunction(function); var handler = (ModbusResponseHandler)Channel.Pipeline.Get(handlerName); if (handler == null) { throw new Exception("Not connected!"); } return (T)handler.GetResponse(transactionIdentifier).Function; } private void SetTransactionIdentifier() { if (transactionIdentifier < ushort.MaxValue) { transactionIdentifier++; } else { transactionIdentifier = 1; } } public ushort ReadHoldingRegistersAsync(ushort startingAddress, ushort quantity) { var function = new ReadHoldingRegistersRequest(startingAddress, quantity); return CallModbusFunction(function); } public ReadHoldingRegistersResponse ReadHoldingRegisters(ushort startingAddress, ushort quantity) { var function = new ReadHoldingRegistersRequest(startingAddress, quantity); return CallModbusFunctionSync<ReadHoldingRegistersResponse>(function); } } public enum ConnectionState { NotConnected = 0, Connected = 1, Pending = 2, }
(文中代碼僅添加了 0x03 的方法)git
在 Client 中封裝了 Modbus 請求方法,對同一個功能同時有同步方法(ReadHoldingRegistersAsync)和異步方法(ReadHoldingRegisters)。同步方法僅返回 TransactionIdentifier(傳輸標識),異步方法返回響應結果。github
ModbusResponseHandler 修改成:bootstrap
public class ModbusResponseHandler : SimpleChannelInboundHandler<ModbusFrame> { private readonly int timeoutMilliseconds = 2000; private Dictionary<ushort, ModbusFrame> responses = new Dictionary<ushort, ModbusFrame>(); protected override void ChannelRead0(IChannelHandlerContext ctx, ModbusFrame msg) { responses.Add(msg.Header.TransactionIdentifier, msg); } public ModbusFrame GetResponse(ushort transactionIdentifier) { ModbusFrame frame = null; var timeoutDateTime = DateTime.Now.AddMilliseconds(timeoutMilliseconds); do { Thread.Sleep(1); if (responses.ContainsKey(transactionIdentifier)) { frame = responses[transactionIdentifier]; responses.Remove(transactionIdentifier); } } while (frame == null && DateTime.Now < timeoutDateTime); if(frame == null) { throw new Exception("No Response"); } else if(frame.Function is ExceptionFunction) { throw new Exception(frame.Function.ToString()); } return frame; } public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) { context.CloseAsync(); } }
Server
public class ModbusServer { private ModbusResponseService responseService; private ServerState serverState; public int Port { get; } public IChannel Channel { get; private set; } private IEventLoopGroup bossGroup; private IEventLoopGroup workerGroup; public ModbusServer(ModbusResponseService responseService, int port = 502) { this.responseService = responseService; Port = port; serverState = ServerState.NotStarted; } public async Task Start() { bossGroup = new MultithreadEventLoopGroup(1); workerGroup = new MultithreadEventLoopGroup(); try { var bootstrap = new ServerBootstrap(); bootstrap.Group(bossGroup, workerGroup); bootstrap .Channel<TcpServerSocketChannel>() .Option(ChannelOption.SoBacklog, 100) .ChildHandler(new ActionChannelInitializer<IChannel>(channel => { IChannelPipeline pipeline = channel.Pipeline; pipeline.AddLast("encoder", new ModbusEncoder()); pipeline.AddLast("decoder", new ModbusDecoder(true)); pipeline.AddLast("request", new ModbusRequestHandler(responseService)); })); serverState = ServerState.Starting; Channel = await bootstrap.BindAsync(Port); serverState = ServerState.Started; } finally { } } public async Task Stop() { if (ServerState.Starting == serverState) { try { await Channel.CloseAsync(); } finally { await Task.WhenAll( bossGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1)), workerGroup.ShutdownGracefullyAsync(TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(1))); serverState = ServerState.NotStarted; } } } } public enum ServerState { NotStarted = 0, Started = 1, Starting = 2, }
實例化 Server 時須要傳入 ModbusResponseService 的實現,實現示例:dom
public class ModbusResponse : ModbusResponseService { public override ModbusFunction ReadHoldingRegisters(ReadHoldingRegistersRequest request) { var registers = ReadRegisters(request.Quantity); var response = new ReadHoldingRegistersResponse(registers); return response; } private ushort[] ReadRegisters(ushort quantity) { var registers = new ushort[quantity]; Random ran = new Random(); for (int i = 0; i < registers.Length; i++) { registers[i] = (ushort)ran.Next(ushort.MinValue, ushort.MaxValue); } return registers; } }
(文中代碼僅添加了 0x03 的方法)異步
開源地址:modbus-tcpasync