ModbusTcpMasterCommunicate.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. using Modbus.Device;
  2. using Prism.Events;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Net.Sockets;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using TeamAAS_VP.Events;
  10. using TeamAAS_VP.Models;
  11. namespace TeamAAS_VP.Core
  12. {
  13. /// <summary>
  14. /// Modbus TCP 主站:轮询 MDC V2 电批,锁付过程中累积实时曲线,完成后推送结果
  15. /// 地址默认值参考 MDC V2 Appendix A(Input Register 功能码 0x04)
  16. /// </summary>
  17. public class ModbusTcpMasterCommunicate : IDisposable
  18. {
  19. // MDC V2 事件数据区起始地址(3201~3213 一次读完,保证结果一致性)
  20. private const ushort MdcEventDataStartAddress = 3201;
  21. private const int MdcEventDataRegisterCount = 13;
  22. private readonly ModbusTcpMasterConfig _config;
  23. private readonly object _masterLock = new object();
  24. private readonly IEventAggregator _eventAggregator;
  25. private TcpClient _tcpClient;
  26. private IModbusMaster _master;
  27. private CancellationTokenSource _pollCts;
  28. // 轮询状态
  29. private bool _lastFinishComplete;
  30. private bool _lastLockInProgress;
  31. private int _lastWaveCount;
  32. /// <summary>本次锁付周期内是否检测到 Motor RUN,防止启动时误触发完成事件</summary>
  33. private bool _finishArmed;
  34. // 实时累积波形(MDC 无波形缓冲区,靠轮询 3300/3301 累积)
  35. private readonly List<WaveData> _accumulatedWave = new List<WaveData>();
  36. private DateTime _lockStartTime = DateTime.Now;
  37. private double _accumulatedTurns;
  38. public ModbusTcpMasterConfig Config { get { return _config; } }
  39. public bool IsEnabled { get { return _config.IsEnabled; } }
  40. public bool IsConnected { get; private set; }
  41. public ModbusTcpMasterCommunicate(ModbusTcpMasterConfig config, IEventAggregator eventAggregator)
  42. {
  43. _config = config;
  44. _eventAggregator = eventAggregator;
  45. }
  46. public void Start()
  47. {
  48. if (!_config.IsEnabled)
  49. {
  50. return;
  51. }
  52. Connect();
  53. SyncInitialPollState();
  54. _pollCts = new CancellationTokenSource();
  55. Task.Run(() => PollLoop(_pollCts.Token));
  56. }
  57. public void Stop()
  58. {
  59. if (_pollCts != null)
  60. {
  61. _pollCts.Cancel();
  62. _pollCts.Dispose();
  63. _pollCts = null;
  64. }
  65. Disconnect();
  66. }
  67. public void Dispose()
  68. {
  69. Stop();
  70. }
  71. #region 连接
  72. private void Connect()
  73. {
  74. Disconnect();
  75. _tcpClient = new TcpClient();
  76. _tcpClient.Connect(_config.IP, _config.Port);
  77. _master = ModbusIpMaster.CreateIp(_tcpClient);
  78. _master.Transport.ReadTimeout = 3000;
  79. _master.Transport.WriteTimeout = 3000;
  80. IsConnected = true;
  81. }
  82. private void Disconnect()
  83. {
  84. IsConnected = false;
  85. if (_master != null)
  86. {
  87. _master.Dispose();
  88. _master = null;
  89. }
  90. if (_tcpClient != null)
  91. {
  92. _tcpClient.Close();
  93. _tcpClient = null;
  94. }
  95. }
  96. private bool TryReconnect()
  97. {
  98. try
  99. {
  100. Connect();
  101. SyncInitialPollState();
  102. return true;
  103. }
  104. catch (Exception ex)
  105. {
  106. IsConnected = false;
  107. LogHelper.WriteLogError("Modbus TCP 主站重连失败", ex);
  108. return false;
  109. }
  110. }
  111. /// <summary>
  112. /// 连接/启动时同步电批当前状态,避免 3212 仍为「完成」时误触发锁付完成
  113. /// </summary>
  114. private void SyncInitialPollState()
  115. {
  116. _lastFinishComplete = ReadFinishComplete();
  117. _lastLockInProgress = ReadLockInProgress();
  118. _finishArmed = false;
  119. _lastWaveCount = 0;
  120. ClearAccumulatedWave();
  121. }
  122. #endregion
  123. #region 轮询
  124. private void PollLoop(CancellationToken token)
  125. {
  126. while (!token.IsCancellationRequested)
  127. {
  128. try
  129. {
  130. PollOnce();
  131. }
  132. catch (Exception ex)
  133. {
  134. LogHelper.WriteLogError("Modbus TCP 主站轮询出错", ex);
  135. Disconnect();
  136. Thread.Sleep(2000);
  137. TryReconnect();
  138. }
  139. Thread.Sleep(Math.Max(10, _config.PollIntervalMs));
  140. }
  141. }
  142. private void PollOnce()
  143. {
  144. if (!IsConnected)
  145. {
  146. TryReconnect();
  147. return;
  148. }
  149. bool isLocking = ReadLockInProgress();
  150. bool finishComplete = ReadFinishComplete();
  151. HandleLockStartEdge(isLocking);
  152. if (isLocking)
  153. {
  154. AppendRealtimeWavePoint();
  155. }
  156. bool needCurve = isLocking || finishComplete;
  157. if (!needCurve)
  158. {
  159. ResetPollStateIfIdle();
  160. return;
  161. }
  162. List<WaveData> waveDatas = GetCurrentWaveDatas();
  163. PublishCurveUpdateIfChanged(waveDatas, finishComplete);
  164. // 必须:先检测到 Motor RUN(_finishArmed),再检测 3212 上升沿,才推送完成
  165. if (finishComplete && !_lastFinishComplete && _finishArmed)
  166. {
  167. PublishLockFinish(waveDatas);
  168. ClearAccumulatedWave();
  169. _finishArmed = false;
  170. }
  171. _lastFinishComplete = finishComplete;
  172. _lastLockInProgress = isLocking;
  173. }
  174. /// <summary>检测到锁付开始时清空累积曲线并允许后续完成判定</summary>
  175. private void HandleLockStartEdge(bool isLocking)
  176. {
  177. if (!isLocking || _lastLockInProgress)
  178. {
  179. return;
  180. }
  181. ClearAccumulatedWave();
  182. _lockStartTime = DateTime.Now;
  183. _finishArmed = true;
  184. _lastWaveCount = 0;
  185. }
  186. private void ResetPollStateIfIdle()
  187. {
  188. _lastFinishComplete = false;
  189. _lastLockInProgress = false;
  190. _finishArmed = false;
  191. _lastWaveCount = 0;
  192. ClearAccumulatedWave();
  193. }
  194. private void ClearAccumulatedWave()
  195. {
  196. _accumulatedWave.Clear();
  197. _accumulatedTurns = 0;
  198. }
  199. private List<WaveData> GetCurrentWaveDatas()
  200. {
  201. if (_config.WaveDataMode == WaveDataSourceMode.RegisterBlock)
  202. {
  203. return ReadWaveDatasFromRegisters();
  204. }
  205. return new List<WaveData>(_accumulatedWave);
  206. }
  207. private void PublishCurveUpdateIfChanged(List<WaveData> waveDatas, bool finishComplete)
  208. {
  209. // 完成态已处理过则不再刷曲线,避免 3212 保持为 1 时反复推送空曲线
  210. if (finishComplete && _lastFinishComplete)
  211. {
  212. return;
  213. }
  214. int waveCount = waveDatas.Count;
  215. if (waveCount == _lastWaveCount && !finishComplete)
  216. {
  217. return;
  218. }
  219. _lastWaveCount = waveCount;
  220. _eventAggregator.GetEvent<LockCurveUpdateNotification>().Publish(waveDatas);
  221. }
  222. private void PublishLockFinish(List<WaveData> waveDatas)
  223. {
  224. LockResult result = BuildLockResult(waveDatas);
  225. result.LockCurveImagePath = BuildLockCurveImagePath(result.Timestamp);
  226. result.CurveCsvPath = BuildLockCurveCsvPath(result.Timestamp);
  227. _eventAggregator.GetEvent<LockFinishNotification>().Publish(result);
  228. }
  229. #endregion
  230. #region MDC 实时波形累积
  231. /// <summary>
  232. /// 锁付进行中:读取 3300 扭矩、3301 转速,累积为曲线点
  233. /// </summary>
  234. private void AppendRealtimeWavePoint()
  235. {
  236. float torque = ReadScaledTorque(_config.RealtimeTorqueAddress);
  237. float speed = ReadRegister(_config.RealtimeSpeedAddress);
  238. double intervalSec = _config.PollIntervalMs / 1000.0;
  239. _accumulatedTurns += (speed / 60.0) * intervalSec;
  240. _accumulatedWave.Add(new WaveData
  241. {
  242. Torque = torque,
  243. Speed = speed,
  244. Turns = _accumulatedTurns,
  245. Time = (DateTime.Now - _lockStartTime).TotalSeconds
  246. });
  247. }
  248. #endregion
  249. #region 寄存器读取
  250. private bool ReadLockInProgress()
  251. {
  252. if (_config.LockInProgressAddress == 0)
  253. {
  254. return false;
  255. }
  256. // MDC:3307 Motor RUN = 1 表示正在锁付
  257. return ReadRegister(_config.LockInProgressAddress) == _config.LockInProgressActiveValue;
  258. }
  259. /// <summary>MDC 3212:1=完成,2=NG,均视为锁付结束</summary>
  260. private bool ReadFinishComplete()
  261. {
  262. ushort status = ReadRegister(_config.FinishFlagAddress);
  263. return status == _config.FinishFlagCompleteValue
  264. || status == _config.FinishFlagNgValue;
  265. }
  266. /// <summary>
  267. /// 组装锁付结果。优先批量读取 MDC 3201~3213 事件区,保证各字段同一时刻快照。
  268. /// </summary>
  269. private LockResult BuildLockResult(List<WaveData> waveDatas)
  270. {
  271. if (TryReadMdcEventBlock(out ushort durationMs, out ushort presetNo, out ushort torqueRaw,
  272. out ushort status, out ushort snugAngle))
  273. {
  274. return CreateLockResult(waveDatas, durationMs, presetNo, torqueRaw, status, snugAngle);
  275. }
  276. // 非标准地址布局:逐寄存器读取
  277. ushort fallbackStatus = ReadRegister(_config.FinishFlagAddress);
  278. return CreateLockResult(
  279. waveDatas,
  280. ReadRegister(_config.LockDurationAddress),
  281. ReadRegister(_config.ProgramNumberAddress),
  282. ReadRegister(_config.LockTorqueAddress),
  283. fallbackStatus,
  284. ReadRegister(_config.LockTurnsAddress));
  285. }
  286. private LockResult CreateLockResult(
  287. List<WaveData> waveDatas,
  288. ushort durationRaw,
  289. ushort presetNo,
  290. ushort torqueRaw,
  291. ushort status,
  292. ushort turnsRaw)
  293. {
  294. // MDC 3212:1=OK,2=NG(以事件 Status 为准,比 3305 实时位更可靠)
  295. bool lockPassed = status == _config.FinishFlagCompleteValue;
  296. float durationSec = _config.LockDurationIsMilliseconds
  297. ? durationRaw / 1000f
  298. : durationRaw;
  299. float lockTurns = _config.TurnsScaleFactor > 0
  300. ? turnsRaw / _config.TurnsScaleFactor
  301. : turnsRaw;
  302. float lockTorque = _config.TorqueScaleFactor > 0
  303. ? torqueRaw / _config.TorqueScaleFactor
  304. : torqueRaw;
  305. return new LockResult
  306. {
  307. Timestamp = DateTime.Now,
  308. LockTurns = lockTurns,
  309. LockTorque = lockTorque,
  310. LockDuration = durationSec,
  311. LockPassed = lockPassed,
  312. ScrewdriverProgramNumber = presetNo,
  313. WaveDatas = waveDatas
  314. };
  315. }
  316. /// <summary>
  317. /// 一次读取 MDC 事件数据块 3201~3213(说明书 Appendix A,功能码 0x04)
  318. /// </summary>
  319. private bool TryReadMdcEventBlock(
  320. out ushort durationMs,
  321. out ushort presetNo,
  322. out ushort torqueRaw,
  323. out ushort status,
  324. out ushort snugAngle)
  325. {
  326. durationMs = presetNo = torqueRaw = status = snugAngle = 0;
  327. if (!_config.UseInputRegisters || !IsStandardMdcEventLayout())
  328. {
  329. return false;
  330. }
  331. ushort[] block = ReadRegisters(MdcEventDataStartAddress, MdcEventDataRegisterCount);
  332. if (block == null || block.Length < MdcEventDataRegisterCount)
  333. {
  334. return false;
  335. }
  336. // 相对 3201 的偏移:3201=0, 3202=1, 3204=3, 3212=11, 3213=12
  337. durationMs = block[0];
  338. presetNo = block[1];
  339. torqueRaw = block[3];
  340. status = block[11];
  341. snugAngle = block[12];
  342. return true;
  343. }
  344. /// <summary>判断配置是否为 MDC V2 标准事件区地址布局</summary>
  345. private bool IsStandardMdcEventLayout()
  346. {
  347. return _config.LockDurationAddress == 3201
  348. && _config.ProgramNumberAddress == 3202
  349. && _config.LockTorqueAddress == 3204
  350. && _config.FinishFlagAddress == 3212
  351. && _config.LockTurnsAddress == 3213;
  352. }
  353. /// <summary>RegisterBlock 模式:从连续寄存器块批量读波形</summary>
  354. private List<WaveData> ReadWaveDatasFromRegisters()
  355. {
  356. int pointCount = ReadRegister(_config.WaveDataCountAddress);
  357. if (pointCount <= 0)
  358. {
  359. return new List<WaveData>();
  360. }
  361. pointCount = Math.Min(pointCount, _config.MaxWavePoints);
  362. int totalRegisters = pointCount * _config.WavePointRegisterCount;
  363. ushort[] registers = ReadRegisters(_config.WaveDataStartAddress, totalRegisters);
  364. List<WaveData> waveDatas = new List<WaveData>(pointCount);
  365. for (int i = 0; i < pointCount; i++)
  366. {
  367. waveDatas.Add(ParseWavePointFromBlock(registers, i * _config.WavePointRegisterCount));
  368. }
  369. return waveDatas;
  370. }
  371. private WaveData ParseWavePointFromBlock(ushort[] registers, int baseOffset)
  372. {
  373. return new WaveData
  374. {
  375. Turns = ReadFloatFromRegisters(registers, baseOffset + 0),
  376. Torque = ReadFloatFromRegisters(registers, baseOffset + 2),
  377. LockAngle = ReadFloatFromRegisters(registers, baseOffset + 4),
  378. Speed = ReadFloatFromRegisters(registers, baseOffset + 6),
  379. Slopes = ReadFloatFromRegisters(registers, baseOffset + 8),
  380. Time = ReadFloatFromRegisters(registers, baseOffset + 10)
  381. };
  382. }
  383. private float ReadScaledTorque(ushort address)
  384. {
  385. return ScaleTorque(ReadRegister(address));
  386. }
  387. private float ScaleTorque(ushort raw)
  388. {
  389. if (_config.TorqueScaleFactor <= 0)
  390. {
  391. return raw;
  392. }
  393. return raw / _config.TorqueScaleFactor;
  394. }
  395. private ushort ReadRegister(ushort address)
  396. {
  397. lock (_masterLock)
  398. {
  399. if (_config.UseInputRegisters)
  400. {
  401. return _master.ReadInputRegisters(_config.SlaveID, address, 1)[0];
  402. }
  403. return _master.ReadHoldingRegisters(_config.SlaveID, address, 1)[0];
  404. }
  405. }
  406. private ushort[] ReadRegisters(ushort startAddress, int count)
  407. {
  408. lock (_masterLock)
  409. {
  410. if (_config.UseInputRegisters)
  411. {
  412. return _master.ReadInputRegisters(_config.SlaveID, startAddress, (ushort)count);
  413. }
  414. return _master.ReadHoldingRegisters(_config.SlaveID, startAddress, (ushort)count);
  415. }
  416. }
  417. private static float ReadFloatFromRegisters(ushort[] registers, int index)
  418. {
  419. if (registers == null || index + 1 >= registers.Length)
  420. {
  421. return 0f;
  422. }
  423. byte[] bytes = new byte[4];
  424. BitConverter.GetBytes(registers[index + 1]).CopyTo(bytes, 0);
  425. BitConverter.GetBytes(registers[index + 0]).CopyTo(bytes, 2);
  426. return BitConverter.ToSingle(bytes, 0);
  427. }
  428. private string BuildLockCurveImagePath(DateTime timestamp)
  429. {
  430. string folder = _config.LockCurveImageFolder;
  431. if (!Directory.Exists(folder))
  432. {
  433. Directory.CreateDirectory(folder);
  434. }
  435. return Path.Combine(folder, $"LockCurve_{timestamp:yyyyMMdd_HHmmss_fff}.png");
  436. }
  437. private string BuildLockCurveCsvPath(DateTime timestamp)
  438. {
  439. string folder = Path.Combine(_config.LockCurveImageFolder, "Csv");
  440. if (!Directory.Exists(folder))
  441. {
  442. Directory.CreateDirectory(folder);
  443. }
  444. return Path.Combine(folder, $"LockCurve_{timestamp:yyyyMMdd_HHmmss_fff}.csv");
  445. }
  446. #endregion
  447. }
  448. }