concepts
设备抽象
虚拟设备和真实设备实现同一套接口——这是「先在虚拟机台上跑通」能成立的前提。
一句话
上层代码不知道自己在跟虚拟设备还是真实设备说话。
这不是为了好看。它决定了三件事能不能成立:
- 没有硬件的人能不能完整跑通流程;
- 流程在虚拟设备上验证过之后,到现场还需不需要重写;
- 能不能在 CI 里跑端到端测试。
接口长什么样
每类设备一个接口,只暴露业务真正需要的能力,不暴露厂商 SDK 的细节:
public interface ICamera : IDevice
{
CameraInfo Info { get; } // 分辨率、像元尺寸
Task<Frame> GrabAsync(CancellationToken ct); // 抓一帧
Task SetExposureAsync(double microseconds);
Task SetGainAsync(double db);
}
public interface IAxis : IDevice
{
AxisInfo Info { get; } // 行程、单位、正方向
double Position { get; } // 当前位置(mm 或 deg)
Task MoveAbsAsync(double target, MoveProfile p, CancellationToken ct);
Task MoveRelAsync(double delta, MoveProfile p, CancellationToken ct);
Task HomeAsync(CancellationToken ct);
Task StopAsync();
}
public interface IDigitalIo : IDevice
{
bool Read(int channel);
Task WriteAsync(int channel, bool value);
IObservable<IoChange> Changes { get; } // 边沿事件
}注意接口里没有的东西:没有 GetBaslerHandle(),没有 SendGCode(string),
没有任何「某一家厂商才有」的口子。一旦开了这种口子,上层就会开始依赖它,抽象就破了。
一台机器由配置拼出来
{
"machine": "demo-2axis-vision",
"devices": [
{ "id": "cam0", "type": "camera", "driver": "virtual",
"params": { "width": 2448, "height": 2048, "pixelUm": 3.45 } },
{ "id": "axisX", "type": "axis", "driver": "virtual",
"params": { "travelMm": 200, "backlashUm": 2.4, "repeatUm": 3 } },
{ "id": "axisY", "type": "axis", "driver": "virtual",
"params": { "travelMm": 200, "backlashUm": 2.4, "repeatUm": 3 } }
]
}把 "driver": "virtual" 换成 "driver": "basler" / "driver": "googoltech",
上层一行都不用改。这就是全部的魔法。
虚拟设备多「虚」
虚拟设备不是返回假数据的空壳,它按物理模型算:
| 设备 | 模拟了什么 |
|---|---|
| 相机 | 投影成像、镜头畸变、景深与离焦、曝光/增益、读出噪声与散粒噪声、坏点 |
| 轴 | 梯形/S 曲线加减速、反向间隙、螺距误差、重复定位误差、限位与急停 |
| IO | 气缸到位延时、光栅遮挡、按钮抖动 |
| 振镜 | 场畸变、跳转延时、标记延时 |
每一项都有开关和参数。教学时最有用的用法是把某一项调大,让新人一眼看清它的后果—— 比如把反向间隙从 2.4 µm 调到 50 µm,然后让他去做一次双向定位。
具体模型写在物理与模型说明里。
什么不该进设备层
- 业务逻辑:「先吹气再拍照」属于工作流,不属于相机。
- 标定结果:标定属于「这台机器的当前状态」,存在配置与标定库里,不在设备对象上。
- 重试策略:设备只如实报错,重试/跳过/中止由工作流统一决定。
这三条守住了,换设备才真的只是换配置。
最后更新: 2026/9/21
这页有帮助吗?