ModbusTcpPlc.cs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. using System;
  2. using System.ComponentModel;
  3. using System.Globalization;
  4. using System.Threading.Tasks;
  5. using HslCommunication;
  6. using HslCommunication.ModBus;
  7. using Newtonsoft.Json;
  8. using TeamAAS.Communication.Attributes;
  9. using TeamAAS.Communication.Base;
  10. using TeamAAS.Communication.Interfaces;
  11. namespace TeamAAS.Communication.PLCs
  12. {
  13. [Communication("Modbus TCP", "PLC", "HslCommunication ModbusTcpNet (HR/IR/C/DI)")]
  14. public class ModbusTcpPlc : PlcCommunicationBase
  15. {
  16. [JsonIgnore] private ModbusTcpNet _master;
  17. [JsonIgnore] private bool _connected;
  18. public override string PlcVendor => "Modbus TCP";
  19. private string _ipAddress = "127.0.0.1";
  20. [Category("II.连接配置"), DisplayName("IP 地址"), Description("从站 IP 地址")]
  21. public string IpAddress
  22. {
  23. get { return _ipAddress; }
  24. set { if (SetProperty(ref _ipAddress, value)) Notify(nameof(EndpointUrl)); }
  25. }
  26. private int _port = 502;
  27. [Category("II.连接配置"), DisplayName("端口"), Description("从站端口(默认 502)")]
  28. public int Port
  29. {
  30. get { return _port; }
  31. set { if (SetProperty(ref _port, value)) Notify(nameof(EndpointUrl)); }
  32. }
  33. public override string EndpointUrl
  34. {
  35. get { return $"tcp://{IpAddress}:{Port}"; }
  36. set
  37. {
  38. if (!string.IsNullOrWhiteSpace(value) && value.StartsWith("tcp://"))
  39. {
  40. var uri = new Uri(value);
  41. IpAddress = uri.Host;
  42. Port = uri.Port;
  43. }
  44. }
  45. }
  46. [JsonIgnore, Browsable(false)]
  47. public override bool IsConnected => _connected;
  48. public override event Action<object, bool> ConnectChangedEvent;
  49. public override event Action<object, string> DataReceivedEvent;
  50. public ModbusTcpPlc() { }
  51. public override void Connect()
  52. {
  53. if (string.IsNullOrWhiteSpace(IpAddress))
  54. throw new InvalidOperationException("Modbus TCP IP 不能为空。");
  55. if (Port <= 0)
  56. throw new InvalidOperationException("Modbus TCP 端口必须大于 0。");
  57. Disconnect();
  58. _master = new ModbusTcpNet { IpAddress = IpAddress, Port = Port, Station = Station, ConnectTimeOut = 2000 };
  59. var result = _master.ConnectServer();
  60. if (!result.IsSuccess)
  61. {
  62. _master = null;
  63. throw new InvalidOperationException("Modbus TCP 连接失败: " + result.Message);
  64. }
  65. Driver = _master;
  66. _connected = true;
  67. ConnectChangedEvent?.Invoke(this, true);
  68. Notify(nameof(IsConnected));
  69. }
  70. public override Task ConnectAsync() => Task.Run(() => Connect());
  71. public override void Disconnect()
  72. {
  73. _connected = false;
  74. Driver = null;
  75. if (_master != null)
  76. {
  77. try { _master.ConnectClose(); } catch { }
  78. _master = null;
  79. }
  80. ConnectChangedEvent?.Invoke(this, false);
  81. Notify(nameof(IsConnected));
  82. }
  83. public override void Dispose() => Disconnect();
  84. private enum ModbusArea { Coil, DiscreteInput, HoldingRegister, InputRegister }
  85. private (ModbusArea Area, string Address) ParseUnifiedAddress(string address)
  86. {
  87. if (string.IsNullOrWhiteSpace(address))
  88. throw new ArgumentException("Modbus 地址不能为空。", nameof(address));
  89. var input = address.Trim().ToUpperInvariant();
  90. var atIdx = input.IndexOf('@');
  91. if (atIdx >= 0) input = input.Substring(0, atIdx);
  92. var colonIdx = input.IndexOf(':');
  93. if (colonIdx < 0) return (ModbusArea.HoldingRegister, input);
  94. var areaText = input.Substring(0, colonIdx).Trim();
  95. var offset = input.Substring(colonIdx + 1).Trim();
  96. switch (areaText)
  97. {
  98. case "C": case "COIL": return (ModbusArea.Coil, offset);
  99. case "DI": case "INPUT": return (ModbusArea.DiscreteInput, offset);
  100. case "HR": case "HOLDING": return (ModbusArea.HoldingRegister, offset);
  101. case "IR": case "INPUTREGISTER": return (ModbusArea.InputRegister, offset);
  102. default: return (ModbusArea.HoldingRegister, input);
  103. }
  104. }
  105. // Modbus 的 Coil/Discrete 区域和 HoldingRegister/InputRegister 用不同的 Driver 方法,
  106. // 且只读区域不支持写入。覆写 Read/Write 做路由。
  107. public override object ReadValue(string address, DebugDataType type, ushort length = 1)
  108. {
  109. if (Driver == null) throw new InvalidOperationException("Modbus TCP 尚未连接。");
  110. var (area, addr) = ParseUnifiedAddress(address);
  111. if (area == ModbusArea.Coil)
  112. {
  113. // Coil 区域:只能 Bool 类型
  114. if (type != DebugDataType.Bool)
  115. throw new InvalidOperationException("Coil 区域(C:) 只支持 Bool 类型读取");
  116. ushort len = Math.Max(length, (ushort)1);
  117. if (len == 1) { var r = _master.ReadCoil(addr); if (!r.IsSuccess) throw new InvalidOperationException(r.Message); return r.Content; }
  118. var r2 = _master.ReadCoil(addr, len); if (!r2.IsSuccess) throw new InvalidOperationException(r2.Message); return r2.Content;
  119. }
  120. if (area == ModbusArea.DiscreteInput)
  121. {
  122. if (type != DebugDataType.Bool)
  123. throw new InvalidOperationException("DiscreteInput 区域(DI:) 只支持 Bool 类型读取");
  124. ushort len = Math.Max(length, (ushort)1);
  125. if (len == 1) { var r = _master.ReadDiscrete(addr); if (!r.IsSuccess) throw new InvalidOperationException(r.Message); return r.Content; }
  126. var r2 = _master.ReadDiscrete(addr, len); if (!r2.IsSuccess) throw new InvalidOperationException(r2.Message); return r2.Content;
  127. }
  128. // HoldingRegister / InputRegister:走基类默认(Driver.ReadXxx)
  129. return base.ReadValue(address, type, length);
  130. }
  131. public override void WriteValue(string address, object value, DebugDataType type, ushort length = 1)
  132. {
  133. if (Driver == null) throw new InvalidOperationException("Modbus TCP 尚未连接。");
  134. var (area, addr) = ParseUnifiedAddress(address);
  135. if (area == ModbusArea.DiscreteInput || area == ModbusArea.InputRegister)
  136. throw new InvalidOperationException($"{area} 为只读区域,不支持写入。");
  137. if (area == ModbusArea.Coil)
  138. {
  139. if (type != DebugDataType.Bool)
  140. throw new InvalidOperationException("Coil 区域(C:) 只支持 Bool 类型写入");
  141. var r = _master.Write(addr, Convert.ToBoolean(value)); if (!r.IsSuccess) throw new InvalidOperationException(r.Message);
  142. return;
  143. }
  144. // HoldingRegister:走基类默认
  145. base.WriteValue(address, value, type, length);
  146. }
  147. }
  148. }