# 13 · 统一硬件访问层接口定义(M0-03) > 父文档:`../00-需求总览.md` §5 · 决策 D2 · 关联 `01-架构与合并方案.md §3`、`03-自动对焦集成方案.md`、`10-术语与契约.md` > 状态:**接口骨架(只签名 + 注释,不含实现体)**。本文不修改任何源码。 > 目标:定义全进程唯一持有每个 COM 口 / 每台相机的【统一硬件访问层 HAL】接口,供 operate 调试、control 采集、自动对焦三方借用,互斥/排队共享。 --- ## ① 现状三套硬件代码对比表 下位机串口协议三套完全同源(帧头 `0x5E`、累加和校验、按命令码定长收帧),但封装风格、收发模型、阻塞策略各不相同。相机三套都是同一块 `MVCAPI.dll`(MVC2000)的 P/Invoke,operate / control 几乎逐行相同,autofocustool 是精简重写版。 ### 串口能力 | 能力 | control (`ivf_tl_SerialHelper`) | operate (`ivf_tl_Entity/ComEntitys`) | autofocustool (`Serial/`) | 文件:行号 | |---|---|---|---|---| | 串口实例/打开 | `Channel.GetSerialPortValue(...)` 建 `SerialPort` + `DataReceived` 事件 | 同 control(同源拷贝) | `SerialMotor(portName)` 固定 9600 8N1;`Open()` 含 `DiscardIn/OutBuffer` | control `Channel.cs:124`;operate `Channel.cs:56,64`;af `SerialMotor.cs:41,58` | | 收发模型 | **事件 + 环形缓冲** `RingBufferManager` + 发送队列线程 | 同 control | **同步请求-响应**:写帧→按命令固定回复长度阻塞读→校验 | control `ComBin.cs:34`;operate `ComBin.cs:37,114`;af `SerialMotor.Send():84` | | 同步发指令收帧 | `ComBin.XxxWait(CustomProtocol)`(`taskAutoResetEvent.WaitOne()` 阻塞) | 同 control,方法更全(运动类带 `waitTime` 参数) | `SerialMotor.Send(byte[] frame, int extraWaitMs=0)`→`ReadFixed(len,timeout)` | operate `ComBin.cs:234-579`;af `SerialMotor.cs:84,137` | | 收帧定长表 | `Commander.CustomProtocolLength(custom)` switch 命令码 | 同 control | `Protocol.ReplyLength(byte cmd)` switch(同表) | control `Commander.cs:43`;af `Protocol.cs:43` | | 帧构造 + 校验 | `Commander.Create*Command` 扩展方法 + `CreateORC`/`CheckORC` | 同 control | `Protocol.*()` 静态 + `WithChecksum`/`CheckChecksum` | control `Commander.cs:15,32,96..`;af `Protocol.cs:22,31,64..` | | 握手 | `ShakeHandsWait`→`Analysiser.ParseShakeHandsCommand` | 同 control | `HouseMotor.ShakeHands()`→`Protocol.ParseShakeHands`(返回 houseSn) | operate `ComBin.cs:250`;af `HouseMotor.cs:37`;`Protocol.cs:200` | | 读 EEPROM | `Commander.CreateReadEEPROM*` + `Analysiser.ParseEEPROMPulse` | 同 control(well 地址表 1-16 在 `Commander.cs`) | `Protocol.ReadWellHorizontalPos(well)`/`ReadWellFocusZero(well)`/`ReadScanStep`/`GetCCDSN`/`ReadLightBrightness` + `ParseEepromInt` | control `Commander.cs:294,398`;af `Protocol.cs:70,75,83,114,141,207` | | 写 EEPROM | `Commander.CreateWriteEEPROMhoriMtWellHoriPos(well,val)`(命令码 0x12) | 同 control | **无写命令**(只读;标定结果只落 `calibration.json`,见 03) | control `Commander.cs:398`;af 缺 | | 电机绝对运动 | `HorizontalMotorAbsoluteWait(custom,waitTime,newValue)` / `VerticalMotorAbsoluteWait(...)` | 同 control(垂直带 `currentVer/Hor/pictureId/well/focal`) | `HouseMotor.HorizontalMoveTo(pulse[,delayMs])` / `VerticalMoveTo(pulse[,delayMs])`→`Protocol.*Absolute`(32位大端) | operate `ComBin.cs:403,534`;af `HouseMotor.cs:101,127`;`Protocol.cs:165,177` | | 电机相对/复位 | `*MotorForwardWait`/`*Backward`/`*ResetWait` | 同 control | `HorizontalForward/Backward/Reset`、`VerticalForward/Backward/Reset` | operate `ComBin.cs:379,428,453,507,559,579`;af `HouseMotor.cs:97,109..` | | 读电机位置 | `ReadHorizontalMotorWait`/`ReadVerticalMotorWait`→`Analysiser.ParseCurrentMotor` | 同 control | `ReadHorizontalPosition`/`ReadVerticalPosition`→`Protocol.ParseMotorPosition`([4..7]大端) | operate `ComBin.cs:475,491`;af `HouseMotor.cs:117,140`;`Protocol.cs:191` | | 读温压/门 | `TemperatureWait`/`ShangTemperatureWait`/`BoLiTemperatureWait`/`PressureWait`/`DoorStatusWait`/`BufferBottleState` | 同 control | **不实现**(对焦工具不读环境量) | operate `ComBin.cs:287,305,323,341,358,269`;解析 `Analysiser.cs:38,49,65` | | LED / 气阀 / IO | `Commander.CreateOpen/CloseLEDCommand`、进/排气阀、舱补/排气、`AutoWait` | 同 control | `HouseMotor.OpenLED()/CloseLED()`(仅 LED) | control `Commander.cs:107,127,167,177`;operate `ComBin.cs:234`;af `HouseMotor.cs:92` | | 读超时策略 | `SendCommand` 固定 `milliseconds` 超时;`WaitTime` 指令后延时 | 同 control(队列线程 `Thread.Sleep(custom.WaitTime)`) | **按命令分超时**:移动类(0x05) `MoveReadTimeoutMs=12000`,查询类 `QueryReadTimeoutMs=3000`(开环阻塞回复随行程增长) | operate `ComBin.cs:155,189`;af `SerialMotor.cs:37,39,133` | ### 相机能力 | 能力 | control (`ivf_tl_CameraHelper`) | operate (`CameraEntitys`) | autofocustool (`Camera/`) | 文件:行号 | |---|---|---|---|---| | P/Invoke 声明 | `MVCAPI`(`mvcapi.dll`,`MV_Usb2*`) | 同 control(同 `MVCAPI`) | 内联 P/Invoke(同 dll) | control `MVCAPI.cs:6`;af `Camera/Camera.cs` | | 初始化 | `InitCamera`→`MV_Usb2Init("MVC2000",...)`,**static `locker` + `[HandleProcessCorruptedStateExceptions]`** | 同 control(逐行相同) | `Init(redGain,greenGain,blueGain)`→同,**static `Locker`** | control `Camera.cs:261`;operate `Camera.cs:252`;af `Camera.cs:50,63` | | 采集模式 | `SetOpMode()` | `SetOpMode()` | `SetOpMode(byte mode=0,bool strobe=false)`(0=单帧拍照,1=实时) | af `Camera.cs:72` | | 设曝光 | `SetPartOfCapInfo(int Exposure)`(含 native 异常防护) | `SetPartOfCapInfo(int Exposure)` / `SetExposure(...)`(VM 入口 `HouseDebugPageViewModel.SetExposure`) | `SetExposure(int exposure100us)` | control/operate `Camera.cs:450,457`;operate VM `HouseDebugPageViewModel.cs:800`;af `Camera.cs:111` | | 设增益 | (随 capInfo 设置) | 同 | `SetGain(byte r,g,b)` | af `Camera.cs:122` | | 抓帧 | `GetRgbData()`/`GetRawData()`/`RawToRgb()`→`MV_Usb2GetRgbData` | 同 control | `GrabRgb()`→`MV_Usb2GetRgbData`;`GetSourceBuffer()` 取 24bpp BGR(含 `_pDest==Zero` 空保护) | control `Camera.cs:355,382`;operate `Camera.cs:355,371,393`;af `Camera.cs:101,137` | | 实时预览(贴窗口) | `Usb2Start(IntPtr ctrl,l,t,w,h)`/`Usb2Stop` | `Usb2Start(...)`/`Usb2Stop` | 不实现(对焦只单帧) | control `Camera.cs:219`;operate `Camera.cs:208` | | 读序列号 | `MV_Usb2GetSerial` | 同 | `ReadSerial()`→`SerialNumber` | af `Camera.cs:85` | | 卸载 | `UnInit`→`MV_Usb2Uninit`(static 锁内) | 同 | `UnInit()`/`Dispose()`(锁内释放 `_pDest`/`_capInfo.Buffer`) | af `Camera.cs:149,162` | | 设备发现 | (开机枚举,散在各处) | 同 | `DeviceScanner.ScanCameras/ScanHouses/ScanAll`:枚举相机 0..9 读 SN、扫 COM 握手得 houseSn+CCDSN、按 CCDSN 配对 | af `DeviceScanner.cs:23,67,121` | > **结论**:autofocustool 的 `Protocol.cs` / `SerialMotor.cs` / `HouseMotor.cs` / `Camera.cs` 是三套里最干净、注释最权威、踩坑修复最完整的一套,HAL 接口应以它为蓝本(D2/03 已确认协议以 `Protocol.cs` 为准)。control/operate 端能力更全(温压门、写 EEPROM、实时预览),需把这些能力补进 HAL。 --- ## ② 冲突点分析 合并成单进程后,三方原本各自 `new`/`Open` 的硬件实例会同时存在并争用同一物理资源: 1. **每舱 COM 口(串口独占)** —— 最硬冲突。 - 现状:control `new ComBin(houseId, portName)` 在后台采集线程常驻持有;operate 调试页(`HouseDebugPageViewModel`)另起一套 `ComBin` 打开**同一 COM 口**;autofocustool `new HouseMotor(portName)` 又开一次。 - 后果:`SerialPort.Open()` 对同一口是**独占**的,第二个打开者直接抛"端口被占用"。即使错开打开,三套各有自己的发送队列/同步等待,时序互相穿插会撞帧(一方收到另一方指令的回复)。 - 这正是 01 §3 / 需求 §5 要求 HAL 的核心动机:**每 COM 口唯一 ComBin/SerialMotor 持有者**。 2. **每台相机句柄(MVCAPI 句柄争用)** —— 软冲突 + 崩溃风险。 - 现状:control `Camera` 与 operate `Camera` 各自 `MV_Usb2Init("MVC2000", out index,...)` 拿独立 `hImager`;autofocustool `Camera` 再 Init 一次。同一相机 index 被多次 Init/UnInit。 - 现有缓解:control/operate/af 各自用**进程内 static 锁**串行化原生调用(`locker`/`Locker`)。但三套是**三个不同 static 锁**(分属不同程序集/类型),合并后并不互斥 —— control 采集线程 `GetRgbData` 与 operate 调试 `SetPartOfCapInfo`、af 对焦 `GrabRgb` 会并发进同一块非线程安全的 `mvcapi.dll`,可能段错误/句柄错乱。 - 另:相机 index 每次开机可变(`DeviceScanner` 注释明确"不能写死"),三方各自枚举会重复 Init 拖慢且互相干扰。 3. **采集 vs 调试 vs 对焦的时序争用(同一舱)** —— 业务级互斥。 - 即便端口/句柄各自打开成功,"control 正在按节拍采集该舱"与"operate 调试该舱"/"对该舱自动对焦"在**同一时刻**操作同一舱的电机+相机,会互相打断(对焦正在 Z 扫描时 control 把皿转走)。 - 需求 §5 / 01 §3 要求:**进入调试或对焦时暂停 control 对该舱的采集**,同一舱同一时刻只有一个使用者。 > **HAL 必须解决三层**:① 物理句柄唯一持有(杜绝重复 Open/Init);② 原生调用全进程统一串行化(一把跨程序集的锁,替代三套各自的 static 锁);③ 按舱借用/归还的业务互斥与优先级。 --- ## ③ 统一接口签名(仅签名 + 注释,不含实现体) 命名空间建议 `IvfTl.Hardware`。按"串口 / 相机 / 并发"三组给出。所有方法均为**同步阻塞**风格(沿用 af `SerialMotor.Send` 与 operate `XxxWait` 的请求-响应模型)。返回值约定:位置/计数类失败返回 `-1`,温压类失败返回 `-1m`,布尔操作类失败返回 `false`。 ### 3.1 资源持有与生命周期(HAL 入口 + 单例) ```csharp namespace IvfTl.Hardware { /// /// 统一硬件访问层(D2)。全进程唯一持有每个 COM 口与每台相机句柄。 /// operate 调试 / control 采集 / 自动对焦三方都通过本层借用,不再各自 new ComBin/SerialPort/Camera。 /// 实现需为单例(进程级),内部维护 portName→ISerialChannel、cameraIndex→ICamera 字典。 /// public interface IHardwareAccessLayer { /// 进程级单例。合并后由宿主(operate 主进程)在启动时初始化一次。 // static IHardwareAccessLayer Instance { get; } // 实现可用静态属性/DI 提供,签名仅示意 /// 开机设备发现:枚举相机 0..9 读 SN、扫 COM 握手得 houseSn+CCDSN、按 CCDSN 配对。 /// 复刻 DeviceScanner.ScanAll。index/COM 每次开机可变,必须现场重扫,禁止写死。 IReadOnlyList ScanDevices(); /// 取(或惰性创建)某 COM 口的唯一串口通道持有者。重复调用返回同一实例。 ISerialChannel GetSerial(string portName); /// 取(或惰性创建)某相机 index 的唯一相机持有者。重复调用返回同一实例。 ICamera GetCamera(int cameraIndex); /// 按舱取设备句柄组(串口+配对相机),调试/对焦/采集统一从这里拿。 IHouseHandle GetHouse(int houseSn); /// 统一关闭路径:关闭并释放所有串口/相机句柄,杜绝句柄泄漏(替代各调试页自己的 ClosePort/UnInit)。 void ShutdownAll(); } /// 设备发现结果(对应 af DeviceScanner 的 HouseDevice)。 public sealed class HouseDeviceInfo { public int HouseSn { get; init; } // 握手返回的舱号 public string PortName { get; init; } // COM 口 public string CcdSn { get; init; } // 该舱 EEPROM 记录的相机序列号 public int CcdIndex { get; init; } // 配对到的相机枚举 index(-1=未配对) } /// 一个舱的句柄聚合:串口 + 相机,外加该舱的借用闸门(见 3.4)。 public interface IHouseHandle { int HouseSn { get; } ISerialChannel Serial { get; } ICamera Camera { get; } } } ``` ### 3.2 串口能力(`ISerialChannel`) ```csharp namespace IvfTl.Hardware { /// /// 单 COM 口的唯一持有者。封装 SerialPort 打开/独占 + 同步请求-响应收发。 /// 蓝本:autofocustool SerialMotor + HouseMotor + Protocol(最权威), /// 并补回 control/operate 的温压门 / 写EEPROM / 气阀 能力。 /// 所有方法同步阻塞到下位机回复;内部对该口的收发用实例锁串行化(一个 COM 口同一时刻一条在途指令)。 /// public interface ISerialChannel : IDisposable { string PortName { get; } bool IsOpen { get; } Action Log { get; set; } // ── 生命周期 ── /// 打开串口(含 DiscardIn/OutBuffer)。已开返回 true。独占失败返回 false。 bool Open(); void Close(); // ── 读超时策略(方案甲:按命令类型分超时,见 SerialMotor.ReadTimeoutForCmd)── /// 移动类命令(0x05)读超时(ms),需覆盖最大行程开环回复,默认 12000。 int MoveReadTimeoutMs { get; set; } /// 查询类命令读超时(ms),下位机立即回复,默认 3000。 int QueryReadTimeoutMs { get; set; } /// 电机到位后默认稳定延时(ms),移动后等机械停稳再抓图,默认 1500(真机标定)。 int MotorSettleMs { get; set; } // ── 通用收发(底层,供扩展协议直接用)── /// 发送完整命令帧(含校验),按命令码定长阻塞收帧并校验。 /// extraWaitMs:收到回复后额外等待(电机到位延时)。失败返回 null。 byte[] SendWait(byte[] frame, int extraWaitMs = 0); // ── 握手 / 自检 ── /// 握手,返回下位机自报 houseSn。失败 -1。 int ShakeHandsWait(); // ── EEPROM ── /// 读本舱相机序列号 CCDSN(EEPROM int)。失败 -1。 int ReadCcdSnWait(); /// 读 EEPROM 灯光亮度(只读)。失败 -1。 int ReadLightBrightnessWait(); /// 读第 well(1-16) 的水平电机位置脉冲(EEPROM)。失败 -1。 int ReadWellHorizontalPosWait(int well); /// 读第 well(1-16) 的 Z 对焦零点脉冲(EEPROM)。失败 -1。 int ReadWellFocusZeroWait(int well); /// 读垂直电机扫描间隔脉冲(每层 Z 步距,EEPROM)。失败 -1。 int ReadScanStepWait(); /// 写第 well(1-16) 的水平电机位置脉冲(命令码 0x12)。af 缺此能力,从 control 补回。 /// ⚠ 需真机验证:af 端从未回写 EEPROM,写命令字节序/地址表须按 control Commander.cs 复核。 bool WriteWellHorizontalPosWait(int well, int pulseValue); // ── 电机:垂直(Z=对焦轴)/ 水平(皿孔定位)── /// Z 绝对运动到脉冲,delayMs 缺省用 MotorSettleMs;扫描小步可传短延时提速。失败 false。 bool VerticalMoveToWait(int pulse, int delayMs = -1); bool VerticalForwardWait(int pulse, int delayMs = -1); bool VerticalBackwardWait(int pulse, int delayMs = -1); bool VerticalResetWait(int delayMs = -1); /// 读 Z 当前位置脉冲。失败 -1。 int ReadVerticalPositionWait(); bool HorizontalMoveToWait(int pulse, int delayMs = -1); bool HorizontalForwardWait(int pulse, int delayMs = -1); bool HorizontalBackwardWait(int pulse, int delayMs = -1); bool HorizontalResetWait(int delayMs = -1); int ReadHorizontalPositionWait(); /// 转皿到第 well(1-16) 对准相机:读 EEPROM 该 well 位置 → 水平绝对运动过去。返回脉冲位置,失败 -1。 int RotateToWellWait(int well); // ── 环境量(温压门,control/operate 有,af 无)── /// 下盖板温度(℃),失败 -1m。 decimal TemperatureWait(); decimal ShangTemperatureWait(); // 上盖板 decimal BoLiTemperatureWait(); // 玻璃片下方 decimal PressureWait(); // 舱内气压 (decimal pressure, decimal t1, decimal t2) BufferBottleStateWait(); // 缓冲瓶 /// 仓门状态。失败返回 未知。 DoorState DoorStatusWait(); // ── IO / LED / 气阀(control/operate 全,af 仅 LED)── bool OpenLedWait(); bool CloseLedWait(); bool OpenIntakeValveWait(); bool CloseIntakeValveWait(); bool OpenExhaustValveWait(); bool CloseExhaustValveWait(); bool HouseAerationWait(); // 舱室补气 bool HouseVentWait(); // 舱室排气 bool BufferBottleAerationWait(); // 缓冲瓶补气 /// 自动气体交换开/关(对应 operate AutoWait)。 bool AutoAirSwapWait(bool on); } /// 仓门状态(对应 control State 枚举)。 public enum DoorState { 未知, 打开, 关闭 } } ``` ### 3.3 相机能力(`ICamera`) ```csharp namespace IvfTl.Hardware { /// /// 单台相机(MVC2000)的唯一持有者。封装 MVCAPI P/Invoke。 /// 蓝本:autofocustool Camera(含 _pDest 空保护 + 锁内释放)。 /// ⚠ 所有原生调用必须走【全进程统一相机锁】(见 3.4 ICameraGate),替代现状三套各自的 static locker。 /// ⚠ 所有进 native 的方法实现处必须加 [HandleProcessCorruptedStateExceptions][SecurityCritical] 防 native 崩溃拖垮进程。 /// public interface ICamera : IDisposable { int Index { get; } int Width { get; } int Height { get; } int Exposure { get; } // 单位 100us string SerialNumber { get; } bool IsInit { get; } bool IsStart { get; } // ── 生命周期 ── /// 初始化(MV_Usb2Init MVC2000)。返回 0 成功。需立即 SetOpMode 才能抓图。 int Init(byte redGain = 25, byte greenGain = 14, byte blueGain = 25); /// 采集模式:0=单帧拍照(对焦/调试抓图用),1=实时预览。返回 0 成功。 int SetOpMode(byte mode = 0, bool strobe = false); /// 读相机序列号到 SerialNumber。返回 0 成功。 int ReadSerial(); /// 卸载相机(MV_Usb2Uninit)。返回 0 成功。 int UnInit(); // ── 参数 ── /// 设曝光(单位 100us)。返回 0 成功。 int SetExposure(int exposure100us); /// 设 RGB 三通道增益(0-255)。返回 0 成功。 int SetGain(byte red, byte green, byte blue); // ── 抓帧(单帧)── /// 抓一帧 RGB 到内部缓冲。返回 0 成功;之后用 GetFrameBuffer 取像素。 int GrabRgb(); /// 取当前帧像素(24bpp BGR, W*H*3)。_pDest 已释放返回 null(调用方按抓帧失败重试)。 byte[] GetFrameBuffer(); /// 抓一帧并返回像素的便捷重载,内置"丢残留帧 + 到位延时 + 重试"语义(见 3.5)。 /// discardStale=true 时先 GrabRgb 丢弃一帧再抓有效帧(运动后旧帧滞留修复,对应 af 双 Grab)。 /// preDelayMs:抓前等待(电机到位稳定),retry:抓帧失败重试次数。失败返回 null。 byte[] GrabStable(int preDelayMs = 0, bool discardStale = true, int retry = 2); // ── 实时预览(贴 WPF 窗口,operate 调试/control 预览用;af 不需要)── /// 把实时画面贴到宿主控件句柄。对应 control/operate Usb2Start。返回 0 成功。 int StartPreview(IntPtr hostControl, int left, int top, int width, int height); int StopPreview(); } } ``` ### 3.4 并发控制(借用 / 归还 / 互斥 / 优先级) ```csharp namespace IvfTl.Hardware { /// 使用者身份,用于优先级与日志归因。 public enum HardwareUser { ControlCapture, OperateDebug, AutoFocus } /// /// 按舱借用闸门。同一舱同一时刻只有一个使用者持有 IHardwareLease。 /// 优先级/抢占策略(建议,需 01 §3 与业务确认): /// · OperateDebug / AutoFocus 是【前台显式操作】,优先级高于 ControlCapture【后台节拍采集】; /// · 申请前台借用时,HAL 通知 control 暂停该舱采集(PauseHouse),归还后恢复(ResumeHouse); /// · 同为前台的 Debug 与 AutoFocus 互斥排队(不可同时操作同一舱电机/相机)。 /// public interface IHouseGate { int HouseSn { get; } /// 申请独占借用该舱(串口+相机)。阻塞直到拿到或超时。拿不到返回 null。 /// 拿到 lease 期间,HAL 已暂停其它使用者对本舱的访问。Dispose(lease) 即归还。 IHardwareLease Acquire(HardwareUser user, int timeoutMs = 30000); /// 尝试借用,不阻塞。拿不到立即返回 false。 bool TryAcquire(HardwareUser user, out IHardwareLease lease); /// 暂停 control 对本舱的后台采集(前台借用时由 HAL 调用;也可供宿主显式调)。 void PauseCapture(); void ResumeCapture(); } /// 借用凭证。Dispose 即归还闸门并触发 ResumeCapture。务必 using 包裹。 public interface IHardwareLease : IDisposable { HardwareUser Owner { get; } ISerialChannel Serial { get; } // 借用期内安全独占 ICamera Camera { get; } } /// /// 全进程统一相机原生调用锁。替代现状 control/operate/af 各自的 static locker(三把不互斥的锁)。 /// 所有 ICamera 实现的 native 调用都必须经此锁串行化(mvcapi.dll 非线程安全,跨相机也走同一把锁)。 /// public interface ICameraGate { /// 在全局相机锁内执行 native 操作。 T Invoke(Func nativeCall); void Invoke(Action nativeCall); } } ``` ### 3.5 踩坑相关的接口约定(来自 03 文档实测修复) 接口签名通过下列约定体现 03 文档的真机踩坑修复,**实现时必须遵守**: 1. **移动后丢残留帧**(`CalibrationEngine.CoarseFocus:355,363-364` 双 `Grab()`): `ICamera.GrabStable(preDelayMs, discardStale:true, retry)` 内置"先丢一帧再抓有效帧";`GrabRgb`+`GetFrameBuffer` 仍保留底层裸抓供调试。 2. **运动后到位延时**(`HouseMotor.MotorDelayMs=1500`、`CoarseFocus` 大行程额外 `CoarseSettleMs`): 电机方法均带可选 `delayMs`(缺省取 `MotorSettleMs`);扫描小步移动传短延时提速,大行程移动后由调用方追加等待 + `GrabStable(preDelayMs)`。 3. **按命令分读超时**(`SerialMotor.ReadTimeoutForCmd`,开环移动回复随行程增长): `MoveReadTimeoutMs(12000)` / `QueryReadTimeoutMs(3000)` 暴露为属性,禁止写死单一超时。 4. **native 崩溃防护**:所有 `ICamera` 进 native 的实现方法加 `[HandleProcessCorruptedStateExceptions][SecurityCritical]`(control/operate/af 三套均如此);`GetFrameBuffer` 须做 `_pDest==IntPtr.Zero` 空保护返回 null(af `Camera.cs:137`)。 5. **统一 static 锁**:三套各自的 `locker`/`Locker` 合并为单一 `ICameraGate`(3.4),所有相机 native 调用经它串行化。 6. **下位机结果位校验**:`SendWait` 实现须沿用 af 语义——回复 `[n-2]` 非 0 视为下位机操作失败返回 null(`SerialMotor.cs:109`)。 --- ## ④ 与三方调用者的接入方式 合并后,三方都从 `IHardwareAccessLayer.Instance` 借用,**不再各自 new**。 ### operate 调试(`HouseDebugPageViewModel`) - 打开调试页 → `gate = HAL.GetHouse(houseSn).…`;`using var lease = houseGate.Acquire(HardwareUser.OperateDebug)`(HAL 自动暂停 control 对该舱采集)。 - 调试动作改调 `lease.Serial.HorizontalMoveToWait/...`、`lease.Camera.SetExposure/GrabStable/StartPreview`(替代现状 VM 里自建 `ComBin`/`Camera` 与 `SetExposure→camera.SetPartOfCapInfo`)。 - 关闭调试页 → `lease.Dispose()` 归还,HAL 恢复 control 采集。不再各自 `ClosePort/UnInit`。 ### control 采集(后台节拍) - 采集线程对每舱 `using var lease = houseGate.Acquire(HardwareUser.ControlCapture, timeoutMs)`;拿不到(前台正在调试/对焦)则**跳过本轮该舱**,下一节拍重试(低优先级让路)。 - 拿到后用 `lease.Serial`(温压门/电机/补排气)+ `lease.Camera`(按曝光抓帧)跑原采集流程。 - 响应 `PauseCapture/ResumeCapture`:前台借用时本舱采集挂起。 ### 自动对焦(autofocustool 集成) - 对某舱对焦:`using var lease = houseGate.Acquire(HardwareUser.AutoFocus)`。 - 把 af 的 `HouseMotor`/`Camera` 调用**改为对 `lease.Serial`/`lease.Camera` 的同名方法**(接口本就以 af 为蓝本,迁移成本最低):`RotateToWellWait`、`ReadWellFocusZeroWait`、`VerticalMoveToWait(z, ScanDelayMs)`、`GrabStable(preDelayMs, discardStale:true)` 跑粗/精对焦。 - 标定结果回写:调 `WriteWellHorizontalPosWait`(af 原缺,HAL 从 control 补回;⚠ 真机验证后启用)。 --- ## ⑤ 需真机验证的点(建议登记到待验证清单) | # | 待验证项 | 原因/依据 | 验证方式 | |---|---|---|---| | V1 | 写 EEPROM 命令(`WriteWellHorizontalPosWait`,0x12)字节序与 well 地址表 | af 端**从未回写 EEPROM**,写命令只在 control `Commander.cs:398` 存在且未经对焦链路验证 | 真机写一个 well 后读回比对,确认不损坏其它地址 | | V2 | 移动类读超时 12000ms 是否够覆盖最大行程 | `SerialMotor.cs:37` 注释"大行程实测 >6 秒",仅 well1 日志样本 | 跨最大行程(如水平 206150→71500)多次实测回复耗时上限 | | V3 | 电机到位延时 `MotorSettleMs=1500` / 粗对焦 `CoarseSettleMs` | `HouseMotor.cs:17`、`CoarseFocus:348` 标注"真机标定",且 03 记录偶发运动拖影伪峰 | 不同行程移动后抓图测运动模糊,标定最小稳定延时 | | V4 | "丢残留帧"丢 1 帧是否足够 | `CoarseFocus` 用单次额外 `Grab()` 丢残留,03 标注偶发伪峰复现率低未根除 | 大行程移动后连抓多帧,确认第几帧才稳定(可能需丢 ≥1 帧) | | V5 | 跨相机共用一把 `ICameraGate` 锁是否拖慢多舱并行采集 | 现状三套各自 static 锁;统一成一把全局锁后多舱抓帧被串行化 | 多舱同时采集压测,测吞吐是否可接受;必要时按相机分锁但保跨调用者互斥 | | V6 | 前台借用→暂停 control 采集的恢复时序 | 01 §3 要求"进入调试/对焦暂停该舱采集",但暂停点/恢复后是否丢节拍未定 | 调试↔采集切换最小验证(01 §3 M1 必过):不报端口占用、不死锁、采集节拍可恢复 | | V7 | 相机 index 与 COM 口开机漂移 + CCDSN 配对 | `DeviceScanner.cs:15` 明确"每次开机可能变,禁止写死" | 多次重启确认 `ScanDevices` 配对稳定,CCDSN→index 唯一 | | V8 | 握手 houseSn / CCDSN 解析字节位 | control(`ParseShakeHandsCommand` 取 [2]) 与 af(`ParseShakeHands` 取 [2]) 一致,但 EEPROM int 取位 control([4..7] 大端 via ParseEEPROMPulse) vs af([4..7] 小端 ParseEepromInt) **字节序疑似不一致** | 真机读同一 EEPROM 值,比对两套解析结果,统一字节序 | > **特别提示(V8)**:control `Analysiser.ParseEEPROMPulse`(`Analysiser.cs:12`) 与 af `Protocol.ParseEepromInt`(`Protocol.cs:207`) 对 EEPROM 回复 [4..7] 的拼接顺序看似相反,HAL 统一解析前必须真机核对,否则 well 位置/CCDSN 会读错。