父文档:
../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 的硬件实例会同时存在并争用同一物理资源:
每舱 COM 口(串口独占) —— 最硬冲突。
new ComBin(houseId, portName) 在后台采集线程常驻持有;operate 调试页(HouseDebugPageViewModel)另起一套 ComBin 打开同一 COM 口;autofocustool new HouseMotor(portName) 又开一次。SerialPort.Open() 对同一口是独占的,第二个打开者直接抛"端口被占用"。即使错开打开,三套各有自己的发送队列/同步等待,时序互相穿插会撞帧(一方收到另一方指令的回复)。每台相机句柄(MVCAPI 句柄争用) —— 软冲突 + 崩溃风险。
Camera 与 operate Camera 各自 MV_Usb2Init("MVC2000", out index,...) 拿独立 hImager;autofocustool Camera 再 Init 一次。同一相机 index 被多次 Init/UnInit。locker/Locker)。但三套是三个不同 static 锁(分属不同程序集/类型),合并后并不互斥 —— control 采集线程 GetRgbData 与 operate 调试 SetPartOfCapInfo、af 对焦 GrabRgb 会并发进同一块非线程安全的 mvcapi.dll,可能段错误/句柄错乱。DeviceScanner 注释明确"不能写死"),三方各自枚举会重复 Init 拖慢且互相干扰。采集 vs 调试 vs 对焦的时序争用(同一舱) —— 业务级互斥。
HAL 必须解决三层:① 物理句柄唯一持有(杜绝重复 Open/Init);② 原生调用全进程统一串行化(一把跨程序集的锁,替代三套各自的 static 锁);③ 按舱借用/归还的业务互斥与优先级。
命名空间建议 IvfTl.Hardware。按"串口 / 相机 / 并发"三组给出。所有方法均为同步阻塞风格(沿用 af SerialMotor.Send 与 operate XxxWait 的请求-响应模型)。返回值约定:位置/计数类失败返回 -1,温压类失败返回 -1m,布尔操作类失败返回 false。
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; }
}
}
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 { 未知, 打开, 关闭 }
}
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();
}
}
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);
}
}
接口签名通过下列约定体现 03 文档的真机踩坑修复,实现时必须遵守:
CalibrationEngine.CoarseFocus:355,363-364 双 Grab()):
ICamera.GrabStable(preDelayMs, discardStale:true, retry) 内置"先丢一帧再抓有效帧";GrabRgb+GetFrameBuffer 仍保留底层裸抓供调试。HouseMotor.MotorDelayMs=1500、CoarseFocus 大行程额外 CoarseSettleMs):
电机方法均带可选 delayMs(缺省取 MotorSettleMs);扫描小步移动传短延时提速,大行程移动后由调用方追加等待 + GrabStable(preDelayMs)。SerialMotor.ReadTimeoutForCmd,开环移动回复随行程增长):
MoveReadTimeoutMs(12000) / QueryReadTimeoutMs(3000) 暴露为属性,禁止写死单一超时。ICamera 进 native 的实现方法加 [HandleProcessCorruptedStateExceptions][SecurityCritical](control/operate/af 三套均如此);GetFrameBuffer 须做 _pDest==IntPtr.Zero 空保护返回 null(af Camera.cs:137)。locker/Locker 合并为单一 ICameraGate(3.4),所有相机 native 调用经它串行化。SendWait 实现须沿用 af 语义——回复 [n-2] 非 0 视为下位机操作失败返回 null(SerialMotor.cs:109)。合并后,三方都从 IHardwareAccessLayer.Instance 借用,不再各自 new。
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。using var lease = houseGate.Acquire(HardwareUser.ControlCapture, timeoutMs);拿不到(前台正在调试/对焦)则跳过本轮该舱,下一节拍重试(低优先级让路)。lease.Serial(温压门/电机/补排气)+ lease.Camera(按曝光抓帧)跑原采集流程。PauseCapture/ResumeCapture:前台借用时本舱采集挂起。using var lease = houseGate.Acquire(HardwareUser.AutoFocus)。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) 与 afProtocol.ParseEepromInt(Protocol.cs:207) 对 EEPROM 回复 [4..7] 的拼接顺序看似相反,HAL 统一解析前必须真机核对,否则 well 位置/CCDSN 会读错。