Răsfoiți Sursa

fix(control): M-05 修复 0x12 写E方回包帧长合并回归(6→12) + TDD + 真机实证

control ivf_tl_SerialHelper Commander.CustomProtocolLength 对 0x12 写E方
回包帧长误设 6(合并回归;合并前 operate 基线=12),只读前6字节→把数据
字节[4]误当状态字(非0误判失败)、ORC错位、残留6字节污染下次读
(=M-01/02/03 验证时"写后读垃圾值"真因,此前用丢帧间隔 workaround 掩盖)。

真机 raw 抓包实证 0x12 回包确为 12 字节(舱8/舱9 一致,回包[3]=0x0C 自证);
改 Commander.cs:76 帧长表 0x12→12。

TDD:新增 CustomProtocolLengthTests(18测,整表逐项对齐 operate 基线),
red(0x12实得6→2失败)→green(22单测全绿,含既有M-01/02/03 4个)。
真机回环"紧凑写→立即读、无丢帧workaround"舱8/舱9 各12轮 24/24 全干净;
回归 M-01/02/03 三条仍 PASS;control sln + operate Release 双编译0错。

残留(非回归):Write*Wait 仍未把状态字作 bool 成功传回调用方(基线同)。

文档同步:待验证清单 M-05 ☑ + §八降级登记 + 进度状态.yaml + 进度数据.js + 交接卡。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 2 zile în urmă
părinte
comite
e75d407f2f

+ 79 - 0
ivf_tl_operate_2.0/control/ivf_tl_SerialHelper.Tests/CustomProtocolLengthTests.cs

@@ -0,0 +1,79 @@
+using IvfTl.Control.Entity;
+using ivf_tl_SerialHelper.Util;
+using Xunit;
+
+namespace ivf_tl_SerialHelper.Tests
+{
+    /// <summary>
+    /// M-05 合并回归:control 端 Commander.CustomProtocolLength(决定每条指令【回包帧长】)与
+    /// 合并前 operate 黄金真值(临时文件/ivf_tl_operate_2.0/ivf_tl_Entity/ComEntitys/Commander.cs)分歧——
+    ///   · 0x12 写E方:operate=12,control=6     ← 真机实证回包确为 12(回包[3]=0x0C 自证,舱8/舱9 一致)
+    ///   · 0x10 获取IO/读门:operate=7,control 缺→默认6  ← 真机实证读门回包确为 7([3]=0x07,舱8/舱9 一致)
+    ///   · 0x08 读传感器AD:operate=9,control 缺→默认6   ← operate基线+autofocustool 一致;control 不发该命令(死路径,对齐基线)
+    /// control 帧长过短 → 只读前 N 字节、把数据字节误当状态字(非0→误判失败)、ORC 错位,
+    /// 并残留尾字节污染紧接的下一次读(=M-01/02/03 验证时"写后读垃圾值"现象的根因)。
+    /// 帧长表是纯逻辑,这里 red→green;消除帧错位的真机回环走 EEPROM 读写验证。
+    /// </summary>
+    public class CustomProtocolLengthTests
+    {
+        // 构造一条以 cmd 为命令码[1]的最小帧,跑 CustomProtocolLength,返回判定出的回包帧长。
+        private static int LenOf(byte cmd)
+        {
+            var custom = new CustomProtocol { sendBuffer = new byte[] { 0x5E, cmd, 0x00, 0x06, 0x00, 0x00 } };
+            Commander.CustomProtocolLength(custom);
+            return custom.lenght;
+        }
+
+        /// <summary>0x12 写E方回包帧长必须=12(真机实证:舱8/舱9 写排气阀回包均 12 字节,回包[3]=0x0C 自证)。</summary>
+        [Fact]
+        public void 写E方0x12_回包帧长应为12_真机实证()
+        {
+            Assert.Equal(12, LenOf(0x12));
+        }
+
+        /// <summary>0x10 获取IO/读门回包帧长必须=7(真机实证:舱8/舱9 读门回包均 7 字节,回包[3]=0x07 自证)。</summary>
+        [Fact]
+        public void 获取IO0x10_回包帧长应为7_真机实证()
+        {
+            Assert.Equal(7, LenOf(0x10));
+        }
+
+        /// <summary>0x08 读传感器AD回包帧长必须=9(operate基线+autofocustool 一致)。</summary>
+        [Fact]
+        public void 读传感器AD0x08_回包帧长应为9_对齐基线()
+        {
+            Assert.Equal(9, LenOf(0x08));
+        }
+
+        /// <summary>
+        /// 整张帧长表必须与合并前 operate 黄金真值逐项一致(回归护栏)。
+        /// 期望值 = operate ivf_tl_Entity/ComEntitys/Commander.cs CustomProtocolLength(真机已用)。
+        /// </summary>
+        [Theory]
+        [InlineData(0x01, 6)]   // 握手
+        [InlineData(0x02, 6)]   // 自检
+        [InlineData(0x04, 7)]   // 设目标温度
+        [InlineData(0x05, 6)]   // 电机控制
+        [InlineData(0x06, 9)]   // 读传感器信号
+        [InlineData(0x08, 9)]   // 读传感器AD      ← control 原缺
+        [InlineData(0x09, 6)]   // 设IO
+        [InlineData(0x10, 7)]   // 获取IO/读门     ← control 原缺
+        [InlineData(0x11, 10)]  // 读E方
+        [InlineData(0x12, 12)]  // 写E方           ← control 原为 6
+        [InlineData(0x16, 6)]   // 自动换气        ← control 原缺(值与默认同)
+        [InlineData(0x18, 10)]  // 读电机位置
+        [InlineData(0x19, 6)]   // 缓冲瓶补气
+        [InlineData(0x20, 12)]  // 读缓冲瓶数据
+        public void 帧长表逐项对齐operate基线(byte cmd, int expectedLen)
+        {
+            Assert.Equal(expectedLen, LenOf(cmd));
+        }
+
+        /// <summary>未知命令码回退默认 6(与基线一致)。</summary>
+        [Fact]
+        public void 未知命令码_回退默认6()
+        {
+            Assert.Equal(6, LenOf(0x7F));
+        }
+    }
+}

+ 1 - 1
ivf_tl_operate_2.0/control/ivf_tl_SerialHelper/Util/Commander.cs

@@ -74,7 +74,7 @@ namespace ivf_tl_SerialHelper.Util
                     custom.lenght = 10;
                     break;
                 case 0x12://写E方
-                    custom.lenght = 6;
+                    custom.lenght = 12;// M-05:回包确为12字节(真机实证 舱8/舱9 写排气阀回包 5E 92 .. 0C .. 共12,回包[3]=0x0C 自证;与合并前 operate 基线一致)。原合并值6会只读前6字节→状态字/ORC错位+残留6字节污染下次读。
                     break;
                 case 0x18://读电机位置
                     custom.lenght = 10;

+ 16 - 0
项目文档/进度/交接卡.md

@@ -245,3 +245,19 @@
 - **延后专项边界(维持原判,非本轮可无监督完成)**:D2-02 调试页借串口命令代理=需设计大改面(逐操作命令代理);D3-04 ComBin 两栈去重=有风险重构(项目既定"不做无监督风险重构");整机开机自启复测=需真重启(会中断在运行的 control 驱动)。
 - **核实**:M-07 config 实读(outInter=0/urlIp=127.0.0.1:10010)+ Release 排除 DEBUG 覆写;M-06/M-05 两版基线源码逐行比对(均硬编码/无条件 true);M-04 转发链 file:line 核对。
 - **下一步**:M 区可无监督闭环的部分(M-01/02/03 修复 + M-07 验证)已完成;M-04(受控抓帧)/M-05(0x12 回包定位)/M-06(well 级零点领域确认)及三延后专项均需用户决策或受控时段/重启,建议按用户优先级逐个开专项。
+
+---
+
+## 2026-06-23 · M-05 帧错位根因(合并回归)修复:control 0x12 写E方回包帧长 6→12(TDD + 真机raw抓包实证)
+
+- **背景**:接 /goal 用 TDD 续做剩余工作。逐条核查剩余 M 区时,用 codegraph 一次对比两份 `CustomProtocolLength`,发现 control `ivf_tl_SerialHelper/Util/Commander.cs` 对 **0x12 写E方回包帧长**设成 6,而合并前 operate 基线 `ivf_tl_Entity/ComEntitys/Commander.cs`(真机写过 EEPROM)=12 → **真实合并回归**,正是上轮 M-01/02/03 验证时"写后读垃圾值"现象(此前用"丢弃一帧+700ms间隔"workaround 掩盖、并误记为"非回归")的根因。
+- **三方分歧 → 真机定论**:operate 基线 0x12=12;autofocustool `Protocol.ReplyLength` 也写 6 但**它根本没有 0x12 写命令**(抄来的未验证默认);control=6。逻辑上 operate 真机写过 EEPROM(若 12 错、硬件只回 6 则每次写都卡等 12 字节,不可能出货)→ 强指向 12。但有分歧**必须实测**,不猜。
+- **真机 raw 抓包实证(harness `临时文件/FrameLenProbe`,裸 SerialPort 9600 8N1,DataReceived 事件收包,整窗累加计数;非破坏=写回刚读到的原值)**:
+  - 踩坑:① 先用 `BytesToRead` 轮询/阻塞 Read 均抓 0 字节,改用与可用成帧 `Channel` 一致的 **DataReceived 事件**收包才有数据;② `CreateORC` **覆写**数组末位占位字节而非追加,我起初给握手/读帧多塞 1 个 0x00 致帧长字段[3]与实际不符、下位机判非法不回复——去掉多余字节后正常(握手5字节[3]=05、读9字节[3]=09、写12字节[3]=0C)。
+  - 结果:0x12 写排气阀回包 **舱9=12字节** `5E 92 09 0C C8 00 00 00 00 CA 00 97`、**舱8=12字节** `5E 92 08 0C 5A 00 00 00 00 5B 00 B9`(回包自带长度字段[3]=0x0C=12 自证;状态字[10]=0x00、ORC[11])。校准全对:0x11读=10、握手=6、0x10读门=**7**(舱8/舱9一致,[3]=0x07 自证)。磁盘上 control 帧长表已含 0x08=9/0x10=7(codegraph 旧快照曾显示缺,实为索引滞后),实测确认其正确,**唯 0x12 错**。
+- **TDD red→green**:新增 `ivf_tl_operate_2.0/control/ivf_tl_SerialHelper.Tests/CustomProtocolLengthTests.cs`(18测:0x12=12/0x10=7/0x08=9 三焦点 + 整表14项逐项对齐 operate 基线 Theory + 未知码回退6)。先 RED——0x12 实得6,2处失败([Fact]写E方 + Theory 行 cmd=18即0x12);改 `Commander.cs:76` `custom.lenght = 12` 后 **22 单测全绿**(含既有4个 M-01/02/03)。
+- **真机修复验证(harness `临时文件/FrameFixVerify`,经真实 `SerialChannelImpl`,关键=【不用】丢帧/长间隔 workaround,紧凑"写原值→立即读")**:舱9(V0=200)12轮、舱8(V0=90)12轮,**24/24 全干净**(读回精确=写入值,无 -1/垃圾);旧码此处必污染。回归 `临时文件/EepromVerify` M-01/02/03 三条仍 **PASS**。control sln(IvfTl.Hardware)+ operate Release **双编译 0 错**。
+- **修复机理**:control 原只读前6字节 → 把数据字节[4]=0xC8 误当状态字(非0→误判失败)、ORC 错位、残留6字节污染下次读;改12后完整收帧,且 Channel 既有 `receivedBuffer[lenght-2]` 状态字检查落到正确位置[10]。
+- **残留(非回归,未做)**:`Write*Wait` 仍"收回复即 return",未把状态字作 bool 成功传回调用方(=M-05"成功语义传播",基线 operate 同样无条件 true,非合并回归)。
+- **核实**:raw 抓包2舱回包字节实读;0x12/0x11/0x10/握手帧长 raw 实测;TDD red(2失败)→green(22过);紧凑写读 24/24;M-01/02/03 回归 PASS;双编译0错;`临时文件/` 已 gitignore(3 个 harness 不入库);codegraph sync 已跑。文档已同步(待验证清单 M-05 + §八 + 进度状态.yaml + 进度数据.js + 本卡)。
+- **下一步**:提交(代码+测试+文档)。M 区可无监督闭环部分(M-01/02/03/05 修复 + M-07 验证)已完;剩 M-04(存图受控抓帧·相机在用)/M-06(well级零点·涉垂直电机·待决策)+ 延后专项(D2-02 命令代理设计/D3-04 两栈去重风险重构/整机自启复测需重启),均需用户决策或受控时段/重启。

+ 9 - 1
项目文档/进度/待验证清单.md

@@ -70,7 +70,7 @@
 | M-02 | 舱室排气阀时间**读** 真读下位机 | `SerialChannelImpl.cs:137` 已去桩→调 `_com.ReadEEPROMVentWait`(control 补 builder/Wait,地址 00 03 08) | **真机** | ☑ 已验通过 |
 | M-03 | 缓冲瓶灯光亮度**写** EEPROM 真下发 | `SerialChannelImpl.cs:143` 已去桩→调 `_com.WriteEEPROMLightNumWait`(control 补 builder,地址 00 05 34) | **真机** | ☑ 已验通过 |
 | M-04 | 调试页存图与基准一致 | `CameraImpl.cs:148` `SavePic(name,w,h)` **忽略 w/h** 转发 `_camera.SaveBmpPic(name)`(底层从相机自身 SourceBuffer 取帧、自带尺寸→丢弃入参 w/h **无害**);落盘格式/旋转/位深 vs operate 基准 `MVCAPI.SavePic` 等价性需一次**受控抓帧**核对(当前相机正被 control 用于活体成像,不宜无监督抓帧干扰) | **真机** | ◑ 代码核查无害/落盘格式待受控抓帧 |
-| M-05 | 写 EEPROM 类"成功=true"可靠性 | `WriteWellHorizontalPos`/`WriteScanStep`/`WriteOpenIntakeTime` 阻塞收回复即 true,未校验真实成功(SerialChannelImpl.cs:106-118)。**非合并回归**(基线 operate 同样无条件 true)。本轮 M-01~03 验证时印证:0x12 写回包长度(`CustomProtocolLength`=6)与 0x11 读(10)不同→写后紧接读会帧错位污染下次读(加间隔即消失)。改为校验写回包状态字=有风险(改活动写路径,误判会破坏正常写),需真机逐字节定位 0x12 回包格式 | **真机** | ☐ 非回归·现象已定位·改动有风险待决策 |
+| M-05 | 写 EEPROM 类"成功=true"可靠性 | **帧错位根因=合并回归已修复**:control `CustomProtocolLength` 对 0x12 写E方回包帧长误设 6(基线 operate=12),真机实证回包确为 12(舱8/舱9 一致,回包[3]=0x0C 自证)→ 改 control 帧长表 0x12→12(Commander.cs:76)。残留:`Write*Wait` 仍"收回复即 return"不把状态字传回调用方(非回归,基线同) | **真机** | ☑ 帧错位已修复并真机验证(残留"成功语义传播"非回归) |
 | M-06 | `ReadWellFocusZero` 按 well 区分 | **非合并回归**:合并前 operate `ComBin.cs:871` 与 control `ComBin.cs:895` **同样硬编码 `CreateReadEEPROMvertMtStartPulse(1)`**(只读 well-1 零点),builder 虽支持 case 1-16,但 Wait 方法两版基线都传 1。改按 well=**行为变更**(影响 autofocus 的 Z 焦面零点,涉垂直电机),需领域确认 well 级零点是否存在 + 谨慎对焦真机验证,**不盲改** | **真机** | ☐ 非回归·待决策 |
 | M-07 | Release 连内网网关(非测试外网) | ✅ **已验证**:Release 排除 `#if DEBUG`(其 :108 行覆写到 test-gateway 外网);部署 `App.config` `outInter=0`(不触发 :87 外网覆写)+ `urlIp=http://127.0.0.1`+`urlPort=10010`→ BaseUrl=内网网关。已由阶段1 operate WPF 真外壳 Release E2E 真服务器登录成功 + control 10010 ESTABLISHED 坐实。现场换站点改 `urlIp` 即可 | 部署 | ☑ 已验通过 |
 
@@ -80,3 +80,11 @@
 > - **接线**:control `Enums` 补 3 枚举;`ComBin` 补 `WriteEEPROOpenVentTimeWait`/`ReadEEPROMVentWait`/`WriteEEPROMLightNumWait`(镜像既有 intake/light 模式);`SerialChannelImpl` 三方法去桩调真实方法;operate 消费方(`HouseDebugPageViewModel.WriteOpenVentTime/RedVentTime`、`BufferDebugViewModel.writeL`)注释/提示同步更新。control sln + operate Release 双编译 0 错。
 > - **真机验证(非破坏性回环,经真实 SerialChannelImpl 端到端,EEPROM 读写无电机风险)**:读 V→写 V+1→读确认=V+1→写回 V→读确认=V。**舱9 排气阀** 200→201→200 ✓、**舱8 排气阀** 90→91→90 ✓(M-01 写/M-02 读)、**舱8 灯光** 500→501→500 ✓(M-03 写)。写入值真机读回随之变化且已恢复原值→写命令字节正确、真下发下位机。
 > - **旁注**:无排空时"写后紧接读"偶现垃圾值/null=0x12 写回包长度与 0x11 读不同致帧错位(=既有 **M-05** 可靠性问题,非本次新命令字节错误——字节错则干净回环不可能出现写入值);加读前丢弃一帧+间隔后 100% 干净。M-05 帧错位/写成功判定列后续专项。
+
+> **2026-06-23 M-05 ☑ 帧错位根因(合并回归)已修复并真机实证(TDD)**:
+> - **订正定性**:上轮把 M-05 整体记为"非回归"。逐字节复核发现其中"帧错位"那半是**真实合并回归**:control `ivf_tl_SerialHelper/Util/Commander.cs CustomProtocolLength` 对 **0x12 写E方回包帧长**设成 6,而合并前 operate 基线(`ivf_tl_Entity/ComEntitys/Commander.cs`,真机写过 EEPROM)=12;autofocustool `Protocol.ReplyLength` 也写 6 但**它根本没有 0x12 写命令**(抄来的未验证默认值)。三方分歧 → 真机实测定论。
+> - **真机 raw 抓包实证(裸 SerialPort 9600 8N1,DataReceived 事件收包,整窗累加计数)**:0x12 写排气阀回包 **舱9=12 字节** `5E 92 09 0C C8 00 00 00 00 CA 00 97`、**舱8=12 字节** `5E 92 08 0C 5A 00 00 00 00 5B 00 B9`(回包自带长度字段 `[3]=0x0C=12` 自证;状态字 `[10]=0x00`、ORC `[11]`)。校准:0x11 读回 10、握手回 6、0x10 读门回 **7**(舱8/舱9 一致,`[3]=0x07` 自证;control 默认 6——但磁盘上 control 帧长表已含 0x08=9/0x10=7,实测确认其正确,唯 0x12 错)。
+> - **根因机理**:control 只读前 6 字节 → 把数据字节 `[4]=0xC8` 误当状态字(非0→误判"下位机操作失败")、ORC 校验错位,且残留 6 字节 `00 00 00 CA 00 97` 污染紧接的下一次读(=M-01/02/03 验证时"写后读垃圾值/null"现象的真正根因,此前用"丢弃一帧+700ms间隔"掩盖)。
+> - **TDD red→green**:新增 `ivf_tl_SerialHelper.Tests/CustomProtocolLengthTests.cs`(18 测试:0x12=12/0x10=7/0x08=9 三焦点 + 整表 14 项逐项对齐 operate 基线 Theory + 未知码回退6)。先 RED(0x12 实得 6,2 处失败:[Fact] + Theory 行),改 `Commander.cs:76` 0x12→12 后 **22 单测全绿**(含既有 4 个 M-01/02/03)。
+> - **真机修复验证(紧凑写→立即读,无丢帧 workaround)**:经真实 `SerialChannelImpl`,连续多轮"写原值→立即读"——舱9(V0=200)12 轮、舱8(V0=90)12 轮,**24/24 全干净**(读回精确=写入值,无 -1/垃圾);旧码此处必污染。回归 M-01/02/03 三条仍 PASS。control sln + operate Release 双编译 0 错。
+> - **残留(非回归,未做)**:`Write*Wait` 仍"收回复即 return",未把已落到正确位置 `[10]` 的状态字作为 bool 成功传回调用方——属"成功语义传播",基线 operate 同样无条件 true,非合并回归,列后续按需。

+ 4 - 4
项目文档/进度/进度数据.js

@@ -1,10 +1,10 @@
 // 实时面板数据源(监控面板.html 读 window.PROGRESS_DATA)。每推进一步更新本文件。
 window.PROGRESS_DATA = {
   project: "operate/control 双进程拆分",
-  generatedAt: "2026-06-23 01:10",
-  phase: "三阶段主体完成;续做合并遗留M区——M-01/M-02/M-03(排气阀写/读、灯光写)已修复并真机验证",
-  currentTask: "M-01/02/03:control补3个E方builder+ComBin Wait+SerialChannelImpl去桩(TDD 4单测绿);真机非破坏性回环 舱9/舱8排气阀+舱8灯光 全PASS",
-  note: "operate/control双进程拆分三阶段主体完成。本轮续做合并遗留M区:M-01/M-02/M-03(写排气阀时间/读排气阀时间/写灯光亮度)三处降级已补control端builder去桩,TDD red→green 4单测+真机回环验证(写入值读回随变、已恢复原值)。剩M-04(存图)/M-05(写成功判定·帧错位,已定位现象)/M-06(按well零点)/M-07(Release网关)+延后专项(D2-02/D3-04/整机自启复测需重启)。",
+  generatedAt: "2026-06-23 01:40",
+  phase: "三阶段主体完成;续做合并遗留M区——M-01/02/03(排气阀写/读、灯光写)+ M-05(0x12帧长合并回归)已修复并真机验证",
+  currentTask: "M-05:control CustomProtocolLength 0x12写E方回包帧长 6→12(真机raw抓包实证舱8/舱9回包均12字节);TDD 22单测绿 + 紧凑写→立即读 24/24全干净(无丢帧workaround)",
+  note: "operate/control双进程拆分三阶段主体完成。合并遗留M区:M-01/02/03(排气阀写/读、灯光写)+M-05(0x12写E方回包帧长合并回归 6→12)已修复并真机验证;M-07已验证。M-05真因=帧错位污染(此前用丢帧间隔掩盖),真机实证0x12回包确为12字节后改帧长表,紧凑写读24/24全干净。剩M-04(存图受控抓帧)/M-06(按well零点·涉垂直电机·待决策)+延后专项(D2-02/D3-04/整机自启复测需重启)。",
   milestones: [
     { name: "阶段1 · control 独立进程骨架(完成)", tasks: [
       { id: "Task1-7", name: "全过+D1-08死锁修复+operate真外壳E2E+数据入库DB铁证", status: "☑" }

+ 9 - 9
项目文档/进度/进度状态.yaml

@@ -1,16 +1,16 @@
 # 续接断点状态(机器可解析)。换会话/换电脑后首先读它定位。
 # 状态取值: 未开始 / 进行中 / 完成 / 代码完成待验证
 # 纪律:本字段只存【当前断点】,历史细节进 交接卡.md(见 CLAUDE.md 第三节)。
-更新时间: 2026-06-23 M-01/M-02/M-03 合并遗留降级修复(control 补 3 个 E方 builder + 去桩),TDD red→green 4 单测 + 真机非破坏性回环验证(排气阀写/读、灯光写 全 PASS)。
+更新时间: 2026-06-23 M-05 帧错位根因(合并回归)修复:control CustomProtocolLength 0x12 写E方回包帧长 6→12,真机 raw 抓包实证回包确为12(舱8/舱9一致,[3]=0x0C自证);TDD 22单测绿 + 紧凑写→立即读 24/24全干净(无丢帧workaround)。
 当前任务: >
-  【续做合并遗留 M 区:M-01/M-02/M-03 已修复并真机验证】
-  · M-01/02/03 根因:合并时 control Commander 缺 3 个 E方 builder,SerialChannelImpl 返桩(写排气阀/读排气阀/写灯光未真下发)。
-  · 已做(TDD):新建 ivf_tl_SerialHelper.Tests(4 单测绿,断言字节=合并前 operate 黄金真值)+ control Enums/ComBin/Commander 补齐 + SerialChannelImpl 去桩 + operate 消费方注释更新;双编译 0 错。
-  · 真机验证(非破坏性回环,无电机风险):舱9排气阀200→201→200✓、舱8排气阀90→91→90✓、舱8灯光500→501→500✓。
-  · 下一步:提交 M-01~03;剩 M-04(存图)/M-05(写成功判定·帧错位)/M-06(按well零点)/M-07(Release网关) + 延后专项(D2-02调试页命令代理/D3-04两栈去重/整机自启复测)。
+  【续做合并遗留 M 区:M-05 帧错位根因已修复并真机验证】
+  · M-05 根因:control `ivf_tl_SerialHelper/Util/Commander.cs CustomProtocolLength` 把 0x12 写E方回包帧长设成 6(合并回归,基线 operate=12),只读前6字节→状态字/ORC错位+残留6字节污染下次读(=M-01/02/03"写后读垃圾值"真因)。
+  · 实证:裸 SerialPort raw 抓包 0x12 回包=12字节(舱8/舱9);TDD CustomProtocolLengthTests 18测 red→green;真机紧凑"写→立即读"无workaround 24/24全干净;回归M-01/02/03仍PASS;control sln+operate Release 双编0错。
+  · 下一步:提交 M-05;剩 M-04(存图受控抓帧)/M-06(按well零点·涉垂直电机·待决策) + 延后专项(D2-02调试页命令代理/D3-04两栈去重/整机自启复测需重启)。
 说明: >
-  operate/control 双进程拆分三阶段主体早已完成;本轮续做"合并遗留 M 区"——M-01/M-02/M-03(排气阀时间写/读、灯光亮度写)
-  三处降级已补 control 端 builder 去桩并真机验证。剩 M-04~M-07 与延后专项(调试页命令代理/两栈去重/整机自启复测需重启)。
+  operate/control 双进程拆分三阶段主体早已完成;本轮续做"合并遗留 M 区"——M-01/M-02/M-03(排气阀写/读、灯光写)
+  已修复并真机验证;M-05 帧错位根因(0x12 帧长合并回归)本轮修复并真机验证;M-07 已验证。
+  剩 M-04(存图受控抓帧)/M-06(well级零点·涉垂直电机·待决策)与延后专项(命令代理/两栈去重/整机自启复测需重启)。
 阶段概览:
   - id: 阶段1
     名称: control 独立进程骨架
@@ -32,4 +32,4 @@
     名称: 清理老壳 + 装机收尾
     状态: 未开始
     备注: "退役删ivf_tl_ControlTest脏壳 + operate开机自启 + ComBin两套栈去重(G1-2) + 部署文档。待阶段2完成后拆计划"
-下一步: M区可无监督闭环部分已完成(M-01/02/03修复+M-07验证);剩 M-04(受控抓帧)/M-05(0x12回包定位)/M-06(well级零点领域确认,均非回归/有风险)+ 延后专项(D2-02设计/D3-04风险重构/整机自启需重启),均需用户决策或受控时段
+下一步: M区可无监督闭环部分已完成(M-01/02/03 builder去桩 + M-05 帧长回归修复 + M-07 验证);剩 M-04(存图受控抓帧)/M-06(well级零点·涉垂直电机·待决策)+ 延后专项(D2-02设计/D3-04风险重构/整机自启需重启),均需用户决策或受控时段/重启

+ 2 - 1
项目文档/需求文档/操作端逻辑与配置全景.md

@@ -186,7 +186,8 @@
 
 ### 8.2 待验证 / 桩(非降级,但成功语义不可靠或未实现)
 
-- **写 EEPROM 类"成功=true"普遍不可靠**:`WriteWellHorizontalPosWait`/`WriteScanStepWait`/`WriteOpenIntakeTimeWait` 因旧栈写命令无显式成功出口,**"阻塞收到回复即 return true"**,未校验真实成功(SerialChannelImpl.cs:106-118,接口注释 ISerialChannel.cs:54-65)。**有下发**,但成功与否未知。统一挂 `待真机 V-010`。
+- **写 EEPROM 类"成功=true"普遍不可靠**:`WriteWellHorizontalPosWait`/`WriteScanStepWait`/`WriteOpenIntakeTimeWait` 因旧栈写命令无显式成功出口,**"阻塞收到回复即 return true"**,未把回包状态字传播给调用方(SerialChannelImpl.cs:106-118,接口注释 ISerialChannel.cs:54-65)。**有下发**,但成功与否未传回。属 M-05 残留(非回归,基线 operate 同样无条件 true)。
+- **✅ 2026-06-23 M-05 帧错位根因已修复并真机实证**:`control/ivf_tl_SerialHelper/Util/Commander.cs CustomProtocolLength` 对 **0x12 写E方回包帧长**设成 6(合并回归;合并前 operate 基线=12),只读前 6 字节 → 把数据字节 `[4]` 误当状态字(非0→误判失败)、ORC 错位、且残留 6 字节污染紧接的下一次读(=M-01/02/03 验证时"写后读垃圾值"现象的根因)。**真机 raw 抓包实证 0x12 回包确为 12 字节**(舱8/舱9 一致,回包 `[3]=0x0C` 自证),改 control 帧长表 0x12→12。TDD:`ivf_tl_SerialHelper.Tests/CustomProtocolLengthTests.cs` red→green(0x12=12 + 整表对齐基线,22 单测过);真机回环"**紧凑写→立即读、无丢帧 workaround**"舱8/舱9 各 12 轮 **24/24 全干净**(旧码此处必出 -1/垃圾)。修复后 Channel 既有 `receivedBuffer[lenght-2]` 状态字检查也落到正确位置 `[10]`。详见 `进度/待验证清单.md` M-05。
 - **`SerialChannelImpl.SendWait` 返回 null**(:61-66):裸帧通道未实现(TODO M2),当前调试页未用,仅影响未来扩展。
 - **`ReadWellFocusZeroWait` 语义差异**(:94-99):control 端 Z 零点为整舱单值,`well` 入参被忽略,与 autofocus 按 well 读零点有差异(M2 待验证)。
 - **`ServiceMonitorViewModel` 阈值占位**:`StaleSeconds=30`、上传堆积阈值标 `[D10]` 占位、`[M7]` 运行时未验证。