13-统一硬件访问层接口定义.md 29 KB

13 · 统一硬件访问层接口定义(M0-03)

父文档:../00-需求总览.md §5 · 决策 D2 · 关联 01-架构与合并方案.md §303-自动对焦集成方案.md10-术语与契约.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..
握手 ShakeHandsWaitAnalysiser.ParseShakeHandsCommand 同 control HouseMotor.ShakeHands()Protocol.ParseShakeHands(返回 houseSn) operate ComBin.cs:250;af HouseMotor.cs:37Protocol.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,127Protocol.cs:165,177
电机相对/复位 *MotorForwardWait/*Backward/*ResetWait 同 control HorizontalForward/Backward/ResetVerticalForward/Backward/Reset operate ComBin.cs:379,428,453,507,559,579;af HouseMotor.cs:97,109..
读电机位置 ReadHorizontalMotorWait/ReadVerticalMotorWaitAnalysiser.ParseCurrentMotor 同 control ReadHorizontalPosition/ReadVerticalPositionProtocol.ParseMotorPosition([4..7]大端) operate ComBin.cs:475,491;af HouseMotor.cs:117,140Protocol.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 声明 MVCAPImvcapi.dllMV_Usb2* 同 control(同 MVCAPI 内联 P/Invoke(同 dll) control MVCAPI.cs:6;af Camera/Camera.cs
初始化 InitCameraMV_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_Usb2GetRgbDataGetSourceBuffer() 取 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
卸载 UnInitMV_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 入口 + 单例)

namespace IvfTl.Hardware
{
    /// <summary>
    /// 统一硬件访问层(D2)。全进程唯一持有每个 COM 口与每台相机句柄。
    /// operate 调试 / control 采集 / 自动对焦三方都通过本层借用,不再各自 new ComBin/SerialPort/Camera。
    /// 实现需为单例(进程级),内部维护 portName→ISerialChannel、cameraIndex→ICamera 字典。
    /// </summary>
    public interface IHardwareAccessLayer
    {
        /// <summary>进程级单例。合并后由宿主(operate 主进程)在启动时初始化一次。</summary>
        // static IHardwareAccessLayer Instance { get; }   // 实现可用静态属性/DI 提供,签名仅示意

        /// <summary>开机设备发现:枚举相机 0..9 读 SN、扫 COM 握手得 houseSn+CCDSN、按 CCDSN 配对。
        /// 复刻 DeviceScanner.ScanAll。index/COM 每次开机可变,必须现场重扫,禁止写死。</summary>
        IReadOnlyList<HouseDeviceInfo> ScanDevices();

        /// <summary>取(或惰性创建)某 COM 口的唯一串口通道持有者。重复调用返回同一实例。</summary>
        ISerialChannel GetSerial(string portName);

        /// <summary>取(或惰性创建)某相机 index 的唯一相机持有者。重复调用返回同一实例。</summary>
        ICamera GetCamera(int cameraIndex);

        /// <summary>按舱取设备句柄组(串口+配对相机),调试/对焦/采集统一从这里拿。</summary>
        IHouseHandle GetHouse(int houseSn);

        /// <summary>统一关闭路径:关闭并释放所有串口/相机句柄,杜绝句柄泄漏(替代各调试页自己的 ClosePort/UnInit)。</summary>
        void ShutdownAll();
    }

    /// <summary>设备发现结果(对应 af DeviceScanner 的 HouseDevice)。</summary>
    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=未配对)
    }

    /// <summary>一个舱的句柄聚合:串口 + 相机,外加该舱的借用闸门(见 3.4)。</summary>
    public interface IHouseHandle
    {
        int HouseSn { get; }
        ISerialChannel Serial { get; }
        ICamera Camera { get; }
    }
}

3.2 串口能力(ISerialChannel

namespace IvfTl.Hardware
{
    /// <summary>
    /// 单 COM 口的唯一持有者。封装 SerialPort 打开/独占 + 同步请求-响应收发。
    /// 蓝本:autofocustool SerialMotor + HouseMotor + Protocol(最权威),
    /// 并补回 control/operate 的温压门 / 写EEPROM / 气阀 能力。
    /// 所有方法同步阻塞到下位机回复;内部对该口的收发用实例锁串行化(一个 COM 口同一时刻一条在途指令)。
    /// </summary>
    public interface ISerialChannel : IDisposable
    {
        string PortName { get; }
        bool IsOpen { get; }
        Action<string> Log { get; set; }

        // ── 生命周期 ──
        /// <summary>打开串口(含 DiscardIn/OutBuffer)。已开返回 true。独占失败返回 false。</summary>
        bool Open();
        void Close();

        // ── 读超时策略(方案甲:按命令类型分超时,见 SerialMotor.ReadTimeoutForCmd)──
        /// <summary>移动类命令(0x05)读超时(ms),需覆盖最大行程开环回复,默认 12000。</summary>
        int MoveReadTimeoutMs { get; set; }
        /// <summary>查询类命令读超时(ms),下位机立即回复,默认 3000。</summary>
        int QueryReadTimeoutMs { get; set; }
        /// <summary>电机到位后默认稳定延时(ms),移动后等机械停稳再抓图,默认 1500(真机标定)。</summary>
        int MotorSettleMs { get; set; }

        // ── 通用收发(底层,供扩展协议直接用)──
        /// <summary>发送完整命令帧(含校验),按命令码定长阻塞收帧并校验。
        /// extraWaitMs:收到回复后额外等待(电机到位延时)。失败返回 null。</summary>
        byte[] SendWait(byte[] frame, int extraWaitMs = 0);

        // ── 握手 / 自检 ──
        /// <summary>握手,返回下位机自报 houseSn。失败 -1。</summary>
        int ShakeHandsWait();

        // ── EEPROM ──
        /// <summary>读本舱相机序列号 CCDSN(EEPROM int)。失败 -1。</summary>
        int ReadCcdSnWait();
        /// <summary>读 EEPROM 灯光亮度(只读)。失败 -1。</summary>
        int ReadLightBrightnessWait();
        /// <summary>读第 well(1-16) 的水平电机位置脉冲(EEPROM)。失败 -1。</summary>
        int ReadWellHorizontalPosWait(int well);
        /// <summary>读第 well(1-16) 的 Z 对焦零点脉冲(EEPROM)。失败 -1。</summary>
        int ReadWellFocusZeroWait(int well);
        /// <summary>读垂直电机扫描间隔脉冲(每层 Z 步距,EEPROM)。失败 -1。</summary>
        int ReadScanStepWait();
        /// <summary>写第 well(1-16) 的水平电机位置脉冲(命令码 0x12)。af 缺此能力,从 control 补回。
        /// ⚠ 需真机验证:af 端从未回写 EEPROM,写命令字节序/地址表须按 control Commander.cs 复核。</summary>
        bool WriteWellHorizontalPosWait(int well, int pulseValue);

        // ── 电机:垂直(Z=对焦轴)/ 水平(皿孔定位)──
        /// <summary>Z 绝对运动到脉冲,delayMs 缺省用 MotorSettleMs;扫描小步可传短延时提速。失败 false。</summary>
        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);
        /// <summary>读 Z 当前位置脉冲。失败 -1。</summary>
        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();

        /// <summary>转皿到第 well(1-16) 对准相机:读 EEPROM 该 well 位置 → 水平绝对运动过去。返回脉冲位置,失败 -1。</summary>
        int RotateToWellWait(int well);

        // ── 环境量(温压门,control/operate 有,af 无)──
        /// <summary>下盖板温度(℃),失败 -1m。</summary>
        decimal TemperatureWait();
        decimal ShangTemperatureWait();   // 上盖板
        decimal BoLiTemperatureWait();    // 玻璃片下方
        decimal PressureWait();           // 舱内气压
        (decimal pressure, decimal t1, decimal t2) BufferBottleStateWait(); // 缓冲瓶
        /// <summary>仓门状态。失败返回 未知。</summary>
        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();   // 缓冲瓶补气
        /// <summary>自动气体交换开/关(对应 operate AutoWait)。</summary>
        bool AutoAirSwapWait(bool on);
    }

    /// <summary>仓门状态(对应 control State 枚举)。</summary>
    public enum DoorState { 未知, 打开, 关闭 }
}

3.3 相机能力(ICamera

namespace IvfTl.Hardware
{
    /// <summary>
    /// 单台相机(MVC2000)的唯一持有者。封装 MVCAPI P/Invoke。
    /// 蓝本:autofocustool Camera(含 _pDest 空保护 + 锁内释放)。
    /// ⚠ 所有原生调用必须走【全进程统一相机锁】(见 3.4 ICameraGate),替代现状三套各自的 static locker。
    /// ⚠ 所有进 native 的方法实现处必须加 [HandleProcessCorruptedStateExceptions][SecurityCritical] 防 native 崩溃拖垮进程。
    /// </summary>
    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; }

        // ── 生命周期 ──
        /// <summary>初始化(MV_Usb2Init MVC2000)。返回 0 成功。需立即 SetOpMode 才能抓图。</summary>
        int Init(byte redGain = 25, byte greenGain = 14, byte blueGain = 25);
        /// <summary>采集模式:0=单帧拍照(对焦/调试抓图用),1=实时预览。返回 0 成功。</summary>
        int SetOpMode(byte mode = 0, bool strobe = false);
        /// <summary>读相机序列号到 SerialNumber。返回 0 成功。</summary>
        int ReadSerial();
        /// <summary>卸载相机(MV_Usb2Uninit)。返回 0 成功。</summary>
        int UnInit();

        // ── 参数 ──
        /// <summary>设曝光(单位 100us)。返回 0 成功。</summary>
        int SetExposure(int exposure100us);
        /// <summary>设 RGB 三通道增益(0-255)。返回 0 成功。</summary>
        int SetGain(byte red, byte green, byte blue);

        // ── 抓帧(单帧)──
        /// <summary>抓一帧 RGB 到内部缓冲。返回 0 成功;之后用 GetFrameBuffer 取像素。</summary>
        int GrabRgb();
        /// <summary>取当前帧像素(24bpp BGR, W*H*3)。_pDest 已释放返回 null(调用方按抓帧失败重试)。</summary>
        byte[] GetFrameBuffer();

        /// <summary>抓一帧并返回像素的便捷重载,内置"丢残留帧 + 到位延时 + 重试"语义(见 3.5)。
        /// discardStale=true 时先 GrabRgb 丢弃一帧再抓有效帧(运动后旧帧滞留修复,对应 af 双 Grab)。
        /// preDelayMs:抓前等待(电机到位稳定),retry:抓帧失败重试次数。失败返回 null。</summary>
        byte[] GrabStable(int preDelayMs = 0, bool discardStale = true, int retry = 2);

        // ── 实时预览(贴 WPF 窗口,operate 调试/control 预览用;af 不需要)──
        /// <summary>把实时画面贴到宿主控件句柄。对应 control/operate Usb2Start。返回 0 成功。</summary>
        int StartPreview(IntPtr hostControl, int left, int top, int width, int height);
        int StopPreview();
    }
}

3.4 并发控制(借用 / 归还 / 互斥 / 优先级)

namespace IvfTl.Hardware
{
    /// <summary>使用者身份,用于优先级与日志归因。</summary>
    public enum HardwareUser { ControlCapture, OperateDebug, AutoFocus }

    /// <summary>
    /// 按舱借用闸门。同一舱同一时刻只有一个使用者持有 IHardwareLease。
    /// 优先级/抢占策略(建议,需 01 §3 与业务确认):
    ///   · OperateDebug / AutoFocus 是【前台显式操作】,优先级高于 ControlCapture【后台节拍采集】;
    ///   · 申请前台借用时,HAL 通知 control 暂停该舱采集(PauseHouse),归还后恢复(ResumeHouse);
    ///   · 同为前台的 Debug 与 AutoFocus 互斥排队(不可同时操作同一舱电机/相机)。
    /// </summary>
    public interface IHouseGate
    {
        int HouseSn { get; }

        /// <summary>申请独占借用该舱(串口+相机)。阻塞直到拿到或超时。拿不到返回 null。
        /// 拿到 lease 期间,HAL 已暂停其它使用者对本舱的访问。Dispose(lease) 即归还。</summary>
        IHardwareLease Acquire(HardwareUser user, int timeoutMs = 30000);

        /// <summary>尝试借用,不阻塞。拿不到立即返回 false。</summary>
        bool TryAcquire(HardwareUser user, out IHardwareLease lease);

        /// <summary>暂停 control 对本舱的后台采集(前台借用时由 HAL 调用;也可供宿主显式调)。</summary>
        void PauseCapture();
        void ResumeCapture();
    }

    /// <summary>借用凭证。Dispose 即归还闸门并触发 ResumeCapture。务必 using 包裹。</summary>
    public interface IHardwareLease : IDisposable
    {
        HardwareUser Owner { get; }
        ISerialChannel Serial { get; }   // 借用期内安全独占
        ICamera Camera { get; }
    }

    /// <summary>
    /// 全进程统一相机原生调用锁。替代现状 control/operate/af 各自的 static locker(三把不互斥的锁)。
    /// 所有 ICamera 实现的 native 调用都必须经此锁串行化(mvcapi.dll 非线程安全,跨相机也走同一把锁)。
    /// </summary>
    public interface ICameraGate
    {
        /// <summary>在全局相机锁内执行 native 操作。</summary>
        T Invoke<T>(Func<T> nativeCall);
        void Invoke(Action nativeCall);
    }
}

3.5 踩坑相关的接口约定(来自 03 文档实测修复)

接口签名通过下列约定体现 03 文档的真机踩坑修复,实现时必须遵守

  1. 移动后丢残留帧CalibrationEngine.CoarseFocus:355,363-364Grab()): ICamera.GrabStable(preDelayMs, discardStale:true, retry) 内置"先丢一帧再抓有效帧";GrabRgb+GetFrameBuffer 仍保留底层裸抓供调试。
  2. 运动后到位延时HouseMotor.MotorDelayMs=1500CoarseFocus 大行程额外 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/CameraSetExposure→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 为蓝本,迁移成本最低):RotateToWellWaitReadWellFocusZeroWaitVerticalMoveToWait(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:17CoarseFocus: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 会读错。