using System; using System.ComponentModel; using System.Globalization; using System.Threading.Tasks; using HslCommunication; using HslCommunication.ModBus; using Newtonsoft.Json; using TeamAAS.Communication.Attributes; using TeamAAS.Communication.Base; using TeamAAS.Communication.Interfaces; namespace TeamAAS.Communication.PLCs { [Communication("Modbus TCP", "PLC", "HslCommunication ModbusTcpNet (HR/IR/C/DI)")] public class ModbusTcpPlc : PlcCommunicationBase { [JsonIgnore] private ModbusTcpNet _master; [JsonIgnore] private bool _connected; public override string PlcVendor => "Modbus TCP"; private string _ipAddress = "127.0.0.1"; [Category("II.连接配置"), DisplayName("IP 地址"), Description("从站 IP 地址")] public string IpAddress { get { return _ipAddress; } set { if (SetProperty(ref _ipAddress, value)) Notify(nameof(EndpointUrl)); } } private int _port = 502; [Category("II.连接配置"), DisplayName("端口"), Description("从站端口(默认 502)")] public int Port { get { return _port; } set { if (SetProperty(ref _port, value)) Notify(nameof(EndpointUrl)); } } public override string EndpointUrl { get { return $"tcp://{IpAddress}:{Port}"; } set { if (!string.IsNullOrWhiteSpace(value) && value.StartsWith("tcp://")) { var uri = new Uri(value); IpAddress = uri.Host; Port = uri.Port; } } } [JsonIgnore, Browsable(false)] public override bool IsConnected => _connected; public override event Action ConnectChangedEvent; public override event Action DataReceivedEvent; public ModbusTcpPlc() { } public override void Connect() { if (string.IsNullOrWhiteSpace(IpAddress)) throw new InvalidOperationException("Modbus TCP IP 不能为空。"); if (Port <= 0) throw new InvalidOperationException("Modbus TCP 端口必须大于 0。"); Disconnect(); _master = new ModbusTcpNet { IpAddress = IpAddress, Port = Port, Station = Station, ConnectTimeOut = 2000 }; var result = _master.ConnectServer(); if (!result.IsSuccess) { _master = null; throw new InvalidOperationException("Modbus TCP 连接失败: " + result.Message); } Driver = _master; _connected = true; ConnectChangedEvent?.Invoke(this, true); Notify(nameof(IsConnected)); } public override Task ConnectAsync() => Task.Run(() => Connect()); public override void Disconnect() { _connected = false; Driver = null; if (_master != null) { try { _master.ConnectClose(); } catch { } _master = null; } ConnectChangedEvent?.Invoke(this, false); Notify(nameof(IsConnected)); } public override void Dispose() => Disconnect(); private enum ModbusArea { Coil, DiscreteInput, HoldingRegister, InputRegister } private (ModbusArea Area, string Address) ParseUnifiedAddress(string address) { if (string.IsNullOrWhiteSpace(address)) throw new ArgumentException("Modbus 地址不能为空。", nameof(address)); var input = address.Trim().ToUpperInvariant(); var atIdx = input.IndexOf('@'); if (atIdx >= 0) input = input.Substring(0, atIdx); var colonIdx = input.IndexOf(':'); if (colonIdx < 0) return (ModbusArea.HoldingRegister, input); var areaText = input.Substring(0, colonIdx).Trim(); var offset = input.Substring(colonIdx + 1).Trim(); switch (areaText) { case "C": case "COIL": return (ModbusArea.Coil, offset); case "DI": case "INPUT": return (ModbusArea.DiscreteInput, offset); case "HR": case "HOLDING": return (ModbusArea.HoldingRegister, offset); case "IR": case "INPUTREGISTER": return (ModbusArea.InputRegister, offset); default: return (ModbusArea.HoldingRegister, input); } } // Modbus 的 Coil/Discrete 区域和 HoldingRegister/InputRegister 用不同的 Driver 方法, // 且只读区域不支持写入。覆写 Read/Write 做路由。 public override object ReadValue(string address, DebugDataType type, ushort length = 1) { if (Driver == null) throw new InvalidOperationException("Modbus TCP 尚未连接。"); var (area, addr) = ParseUnifiedAddress(address); if (area == ModbusArea.Coil) { // Coil 区域:只能 Bool 类型 if (type != DebugDataType.Bool) throw new InvalidOperationException("Coil 区域(C:) 只支持 Bool 类型读取"); ushort len = Math.Max(length, (ushort)1); if (len == 1) { var r = _master.ReadCoil(addr); if (!r.IsSuccess) throw new InvalidOperationException(r.Message); return r.Content; } var r2 = _master.ReadCoil(addr, len); if (!r2.IsSuccess) throw new InvalidOperationException(r2.Message); return r2.Content; } if (area == ModbusArea.DiscreteInput) { if (type != DebugDataType.Bool) throw new InvalidOperationException("DiscreteInput 区域(DI:) 只支持 Bool 类型读取"); ushort len = Math.Max(length, (ushort)1); if (len == 1) { var r = _master.ReadDiscrete(addr); if (!r.IsSuccess) throw new InvalidOperationException(r.Message); return r.Content; } var r2 = _master.ReadDiscrete(addr, len); if (!r2.IsSuccess) throw new InvalidOperationException(r2.Message); return r2.Content; } // HoldingRegister / InputRegister:走基类默认(Driver.ReadXxx) return base.ReadValue(address, type, length); } public override void WriteValue(string address, object value, DebugDataType type, ushort length = 1) { if (Driver == null) throw new InvalidOperationException("Modbus TCP 尚未连接。"); var (area, addr) = ParseUnifiedAddress(address); if (area == ModbusArea.DiscreteInput || area == ModbusArea.InputRegister) throw new InvalidOperationException($"{area} 为只读区域,不支持写入。"); if (area == ModbusArea.Coil) { if (type != DebugDataType.Bool) throw new InvalidOperationException("Coil 区域(C:) 只支持 Bool 类型写入"); var r = _master.Write(addr, Convert.ToBoolean(value)); if (!r.IsSuccess) throw new InvalidOperationException(r.Message); return; } // HoldingRegister:走基类默认 base.WriteValue(address, value, type, length); } } }