# 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 会读错。