Device abstraction
Virtual and real devices implement the same interfaces — that is what makes "prove it on the virtual rig first" a real strategy.
In one line
The layers above do not know whether they are talking to a virtual device or a real one.
That is not an aesthetic choice. It decides three things:
- whether someone with no hardware can run the whole procedure;
- whether a procedure validated on virtual devices has to be rewritten on site;
- whether end-to-end tests can run in CI.
What the interfaces look like
One interface per device class, exposing what the business actually needs — never the vendor SDK's shape:
public interface ICamera : IDevice
{
CameraInfo Info { get; } // resolution, pixel size
Task<Frame> GrabAsync(CancellationToken ct);
Task SetExposureAsync(double microseconds);
Task SetGainAsync(double db);
}
public interface IAxis : IDevice
{
AxisInfo Info { get; } // travel, units, positive direction
double Position { get; } // mm or 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; }
}Note what is missing: no GetBaslerHandle(), no SendGCode(string), no escape hatch
that exists for exactly one vendor. Open one of those and the layers above start depending on
it, and the abstraction is gone.
A machine is assembled from configuration
{
"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 } }
]
}Swap "driver": "virtual" for "driver": "basler" and nothing above changes. That is
the whole trick.
How virtual is "virtual"
These are not stubs returning canned data; they compute from a physical model:
| Device | What is modelled |
|---|---|
| Camera | Projection, lens distortion, depth of field and defocus, exposure/gain, read and shot noise, bad pixels |
| Axis | Trapezoidal/S-curve motion, backlash, pitch error, repeatability, limits, e-stop |
| IO | Cylinder settle delays, light curtain, switch bounce |
| Galvo | Field distortion, jump delay, mark delay |
Every item has a switch and parameters. The most useful teaching move is to exaggerate one — push backlash from 2.4 µm to 50 µm and ask a trainee to do bidirectional positioning. The models are documented in Physics and models.
What does not belong in the device layer
- Business logic. "Blow air, then grab" is workflow, not camera.
- Calibration results. Those describe the machine's current state and live in the calibration store, not on a device object.
- Retry policy. Devices report faults honestly; retry / skip / abort is decided once, in the workflow.
Hold those three lines and swapping hardware really is just a configuration change.