SerialMotor.cs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO.Ports;
  4. using System.Threading;
  5. namespace AutoFocusTool.Serial
  6. {
  7. /// <summary>
  8. /// 单舱室串口控制器(马达 + LED)。自包含,无外部实体依赖。
  9. /// 采用同步请求-响应:写命令帧 → 按该命令固定回复长度阻塞读 → 校验。
  10. /// 比原工程的事件+环形缓冲简单,适合调试台单线程操作。
  11. /// 串口参数固定:9600 8N1,读写超时 3000ms。
  12. /// </summary>
  13. public class SerialMotor : IDisposable
  14. {
  15. private readonly SerialPort _port;
  16. private readonly object _ioLock = new object();
  17. /// <summary>日志回调(发送/接收/错误)</summary>
  18. public Action<string> Log;
  19. public string PortName { get; }
  20. public bool IsOpen => _port != null && _port.IsOpen;
  21. public SerialMotor(string portName)
  22. {
  23. PortName = portName;
  24. _port = new SerialPort
  25. {
  26. PortName = portName,
  27. BaudRate = 9600,
  28. DataBits = 8,
  29. StopBits = StopBits.One,
  30. Parity = Parity.None,
  31. ReadTimeout = 3000,
  32. WriteTimeout = 3000,
  33. };
  34. }
  35. public bool Open()
  36. {
  37. try
  38. {
  39. if (!_port.IsOpen) _port.Open();
  40. _port.DiscardInBuffer();
  41. _port.DiscardOutBuffer();
  42. return true;
  43. }
  44. catch (Exception ex)
  45. {
  46. Log?.Invoke($"[{PortName}] 打开串口失败: {ex.Message}");
  47. return false;
  48. }
  49. }
  50. public void Close()
  51. {
  52. try { if (_port.IsOpen) _port.Close(); } catch { }
  53. }
  54. /// <summary>
  55. /// 发送命令帧并读取固定长度回复。返回回复字节(失败返回 null)。
  56. /// </summary>
  57. /// <param name="frame">完整命令帧(含校验)</param>
  58. /// <param name="extraWaitMs">读到回复后额外等待(电机到位延时)</param>
  59. public byte[] Send(byte[] frame, int extraWaitMs = 0)
  60. {
  61. if (!IsOpen && !Open()) return null;
  62. int replyLen = Protocol.ReplyLength(frame[1]);
  63. lock (_ioLock)
  64. {
  65. try
  66. {
  67. _port.DiscardInBuffer();
  68. _port.Write(frame, 0, frame.Length);
  69. Log?.Invoke($"[{PortName}] 发送: {ToHex(frame)}");
  70. byte[] reply = ReadFixed(replyLen, 3000);
  71. if (reply == null)
  72. {
  73. Log?.Invoke($"[{PortName}] 接收超时(期望{replyLen}字节)");
  74. return null;
  75. }
  76. Log?.Invoke($"[{PortName}] 接收: {ToHex(reply)}{(Protocol.CheckChecksum(reply) ? "" : " [校验失败]")}");
  77. // 回复 [n-2] 为下位机结果位,非0视为失败(返回null表示操作失败)
  78. if (replyLen >= 2 && reply[replyLen - 2] != 0)
  79. {
  80. Log?.Invoke($"[{PortName}] 下位机结果位非0(操作失败)");
  81. return null;
  82. }
  83. if (extraWaitMs > 0) Thread.Sleep(extraWaitMs);
  84. return reply;
  85. }
  86. catch (Exception ex)
  87. {
  88. Log?.Invoke($"[{PortName}] 收发异常: {ex.Message}");
  89. return null;
  90. }
  91. }
  92. }
  93. /// <summary>阻塞读取指定字节数,超时返回 null。</summary>
  94. private byte[] ReadFixed(int count, int timeoutMs)
  95. {
  96. byte[] buf = new byte[count];
  97. int got = 0;
  98. var sw = Stopwatch.StartNew();
  99. while (got < count)
  100. {
  101. if (sw.ElapsedMilliseconds > timeoutMs) return null;
  102. try
  103. {
  104. int n = _port.Read(buf, got, count - got);
  105. got += n;
  106. }
  107. catch (TimeoutException)
  108. {
  109. if (sw.ElapsedMilliseconds > timeoutMs) return null;
  110. }
  111. }
  112. return buf;
  113. }
  114. private static string ToHex(byte[] b)
  115. {
  116. if (b == null) return "(null)";
  117. var sb = new System.Text.StringBuilder(b.Length * 3);
  118. foreach (var x in b) sb.Append(x.ToString("X2")).Append(' ');
  119. return sb.ToString().TrimEnd();
  120. }
  121. public void Dispose()
  122. {
  123. Close();
  124. _port?.Dispose();
  125. }
  126. }
  127. }