Ver código fonte

fix(serial): 按命令类型动态读超时, 修复大行程移动被误判超时(方案甲)

根因:下位机移动命令等机械到位才回复,回复耗时随行程增长(大行程实测>6秒),
但读超时写死3000ms→大行程首移还没收到回复就被判超时→上层误判移动失败并重试,
后续步骤在电机未到位时抓图→粗对焦全程检不出圆→对焦退回边界。
(well1日志佐证:168发166收仅2次超时,均为本well首次大行程移动,后续小步0超时)

- 移动类命令(CMD_MOTOR=0x05)用MoveReadTimeoutMs(默认12000),覆盖最大行程
- 查询类命令(握手/读EEPROM/读位置/设IO)用QueryReadTimeoutMs(默认3000),快速失败
- SerialPort.ReadTimeout提到12000避免底层提前抛异常
- 全程加完整注释说明根因与策略

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 1 semana atrás
pai
commit
adea570db3
1 arquivos alterados com 34 adições e 3 exclusões
  1. 34 3
      Serial/SerialMotor.cs

+ 34 - 3
Serial/SerialMotor.cs

@@ -22,6 +22,22 @@ namespace AutoFocusTool.Serial
         public string PortName { get; }
         public bool IsOpen => _port != null && _port.IsOpen;
 
+        // ───────────────────────── 读超时策略(方案甲:按命令类型动态超时)─────────────────────────
+        // 背景/根因:下位机的“移动命令”是开环阻塞式——它在机械真正转到位之后才回复
+        // (回复即“到位确认”)。因此回复耗时 ∝ 移动距离。大行程移动(如水平从 206150
+        // 转到 71500,跨 ~13.5 万脉冲)实测需 6 秒以上。
+        // 旧实现把读超时写死 3000ms:大行程移动还没等到回复就被判“接收超时”→ 返回 null →
+        // 上层误判“移动失败”并重试,且后续步骤在电机尚未稳定到位时就抓图,导致粗对焦
+        // 整段检不出 well 圆、对焦退回边界。(详见 well1 标定日志:仅 2 次超时,均为本 well
+        // 首次大行程移动,后续 166 次小步移动 0 超时。)
+        // 方案甲:把“移动类命令”与“查询类命令”分开给超时——
+        //   · 移动类(电机绝对/相对/复位,命令码 CMD_MOTOR=0x05):长超时,覆盖最大行程移动耗时。
+        //   · 查询类(握手/读EEPROM/读电机位置/设IO等):下位机立即回复,保持短超时即可。
+        /// <summary>移动类命令(电机运动,命令码0x05)的读超时(ms)。需覆盖最大行程移动耗时,默认12000。</summary>
+        public int MoveReadTimeoutMs { get; set; } = 12000;
+        /// <summary>查询类命令(握手/读EEPROM/读位置/设IO)的读超时(ms)。下位机立即回复,默认3000。</summary>
+        public int QueryReadTimeoutMs { get; set; } = 3000;
+
         public SerialMotor(string portName)
         {
             PortName = portName;
@@ -32,7 +48,9 @@ namespace AutoFocusTool.Serial
                 DataBits = 8,
                 StopBits = StopBits.One,
                 Parity = Parity.None,
-                ReadTimeout = 3000,
+                // SerialPort.ReadTimeout 取两者较大值:实际每次读用的超时由 Send() 按命令类型
+                // 再细分(见 ReadTimeoutForCmd)。这里设为大值,避免底层 Read 提前抛 TimeoutException。
+                ReadTimeout = 12000,
                 WriteTimeout = 3000,
             };
         }
@@ -76,10 +94,13 @@ namespace AutoFocusTool.Serial
                     _port.Write(frame, 0, frame.Length);
                     Log?.Invoke($"[{PortName}] 发送: {ToHex(frame)}");
 
-                    byte[] reply = ReadFixed(replyLen, 3000);
+                    // 方案甲:按命令类型选读超时。移动类命令要等机械到位才回复,耗时随行程增长,
+                    // 必须给足超时;查询类命令下位机立即回复,短超时即可(快速失败、不空等)。
+                    int readTimeout = ReadTimeoutForCmd(frame[1]);
+                    byte[] reply = ReadFixed(replyLen, readTimeout);
                     if (reply == null)
                     {
-                        Log?.Invoke($"[{PortName}] 接收超时(期望{replyLen}字节)");
+                        Log?.Invoke($"[{PortName}] 接收超时(期望{replyLen}字节,超时{readTimeout}ms)");
                         return null;
                     }
                     Log?.Invoke($"[{PortName}] 接收: {ToHex(reply)}{(Protocol.CheckChecksum(reply) ? "" : " [校验失败]")}");
@@ -102,6 +123,16 @@ namespace AutoFocusTool.Serial
             }
         }
 
+        /// <summary>
+        /// 按命令码返回该命令的读超时(ms)。方案甲核心:
+        /// 移动类命令(CMD_MOTOR=0x05,含绝对/相对/复位运动)要等机械到位才回复,
+        /// 回复耗时随移动行程增长(大行程实测 >6 秒),给 MoveReadTimeoutMs(默认12秒);
+        /// 其余查询/设置类命令(握手0x01/读EEPROM0x11/读位置0x18/设IO0x09等)下位机立即回复,
+        /// 给 QueryReadTimeoutMs(默认3秒),快速失败不空等。
+        /// </summary>
+        private int ReadTimeoutForCmd(byte cmd)
+            => cmd == Protocol.CMD_MOTOR ? MoveReadTimeoutMs : QueryReadTimeoutMs;
+
         /// <summary>阻塞读取指定字节数,超时返回 null。</summary>
         private byte[] ReadFixed(int count, int timeoutMs)
         {