Writing a Device Driver
A dabs driver is an ordinary MATLAB class. Deriving from the right abstract device class
and following a handful of conventions is enough for ScanImage® to discover it, offer it in
the resource configuration editor, persist its settings, and use it wherever that kind of
device is expected.
This page walks through the anatomy of a driver using
dabs.generic.DigitalShutter - a shutter driven by one digital output - as the worked
example. It is about 250 lines, and every convention below appears in it.
Attention
A driver you write yourself lives in the ScanImage installation folder and is not covered by ScanImage support. Copy an existing driver from the same device family as a starting point; the abstract classes are documented in Device Classes.
The class declaration
classdef DigitalShutter < dabs.resources.devices.Shutter ...
& most.HasMachineDataFile ...
& dabs.resources.configuration.HasConfigPage
Three mixins, each earning its place:
the abstract device class decides what the rest of ScanImage may assume about your object,
most.HasMachineDataFilegives it a heading in the machine data file,dabs.resources.configuration.HasConfigPagegives it a page in the configuration editor.
Add dabs.resources.widget.HasWidget if the device should also appear in the widget bar.
Machine data file plumbing
most.HasMachineDataFile requires a constant block naming the heading and declaring the
default entries:
properties (Constant, Hidden)
mdfClassName = mfilename('class');
mdfHeading = 'Shutter';
mdfDependsOnClasses;
mdfDirectProp;
mdfPropPrefix;
mdfDefault = defaultMdfSection();
end
with a local function after the classdef block building the defaults:
function s = defaultMdfSection()
s = [...
most.HasMachineDataFile.makeEntry('DOControl' ,'' ,'control terminal e.g. ''/vDAQ0/DIO0''')...
most.HasMachineDataFile.makeEntry('invertOutput',false,'invert output drive signal to shutter')...
most.HasMachineDataFile.makeEntry('openTime_s' ,0.5 ,'settling time for shutter in seconds')...
];
end
loadMdf and saveMdf move values between the file and the object.
safeSetPropFromMdf reports failure rather than throwing, so a stale configuration puts the
device into an error state instead of breaking startup:
function loadMdf(obj)
success = true;
success = success & obj.safeSetPropFromMdf('hDOControl' ,'DOControl');
success = success & obj.safeSetPropFromMdf('invertOutput','invertOutput');
success = success & obj.safeSetPropFromMdf('openTime_s' ,'openTime_s');
if ~success
obj.errorMsg = 'Error loading config';
end
end
function saveMdf(obj)
obj.safeWriteVarToHeading('DOControl' ,obj.hDOControl);
obj.safeWriteVarToHeading('invertOutput',obj.invertOutput);
obj.safeWriteVarToHeading('openTime_s' ,obj.openTime_s);
end
Notice that hDOControl is written as the resource, not as a string: the MDF stores the
name, and the property setter resolves it back into a handle.
Discovery
HasConfigPage requires a config page class and a static list of descriptive names. The
names use backslashes to build the category tree the configuration editor shows:
properties (SetAccess = protected, Hidden)
ConfigPageClass = 'dabs.resources.configuration.resourcePages.DigitalShutterPage';
end
methods (Static)
function names = getDescriptiveNames()
names = {'Shutter\Digital Shutter'};
end
end
dabs.resources.Device.findAllDevices() walks +dabs and returns every non-abstract
class with a public constructor, so a driver placed in the package is found without
registration.
Constructor, reinit and deinit
The constructor names the resource and then runs the standard sequence:
function obj = DigitalShutter(name)
obj@dabs.resources.devices.Shutter(name);
obj = obj@most.HasMachineDataFile(true);
obj.deinit();
obj.loadMdf();
obj.reinit();
end
function delete(obj)
obj.deinit();
end
reinit reserves what it needs, initializes the hardware, and clears the error state. It
catches everything and reports through errorMsg:
function reinit(obj)
try
obj.deinit();
if ~most.idioms.isValidObj(obj.hDOControl)
obj.errorMsg = 'No digital output for shutter control specified';
return
end
obj.hDOControl.reserve(obj);
obj.hDOListener(end+1) = most.ErrorHandler.addCatchingListener( ...
obj.hDOControl,'lastKnownValueChanged',@(varargin)obj.updateStatus);
obj.errorMsg = '';
obj.warnMsg = '';
obj.transition(false);
catch ME
obj.deinit();
obj.errorMsg = sprintf('%s: initialization error: %s',obj.name,ME.message);
most.ErrorHandler.logError(ME,obj.errorMsg);
end
end
deinit is the mirror image. Order matters: put the hardware in a safe state before
invalidating the object, because the safety action usually goes through a method that refuses
to run once errorMsg is set.
function deinit(obj)
if isempty(obj.errorMsg)
try
obj.close(); % safe state first
catch
end
end
obj.errorMsg = 'Uninitialized';
obj.warnMsg = '';
delete(obj.hDOListener);
obj.hDOListener = event.listener.empty(0,1);
if most.idioms.isValidObj(obj.hDOControl)
try
obj.hDOControl.tristate();
catch ME
most.ErrorHandler.logAndReportError(ME);
end
obj.hDOControl.unreserve(obj);
end
end
Important
Neither method may throw. Everything a user needs to know goes into
errorMsg and warnMsg.
Resource-valued properties
A property holding another resource follows one pattern throughout dabs:
function set.hDOControl(obj,val)
val = obj.hResourceStore.filterByName(val); % accept a name or a handle
if ~isequal(val,obj.hDOControl)
obj.deinit(); % changing wiring invalidates init
if most.idioms.isValidObj(obj.hDOControl)
obj.hDOControl.unregisterUser(obj);
end
if most.idioms.isValidObj(val)
validateattributes(val,{'dabs.resources.ios.DO','dabs.resources.ios.PFI'},{'scalar'});
val.registerUser(obj,'Control');
end
obj.hDOControl = val;
end
end
Resolving through filterByName is what lets the machine data file, the configuration
editor and a user script all assign the same property by name. registerUser with a short
description is what lets the editor tell the user which device is already claiming a
terminal.
Implementing the device interface
Finally, implement the abstract members. For a shutter that is the non-blocking transition
pair; Shutter builds open, close and transition on top of them.
function startTransition(obj,tf)
most.ErrorHandler.assert(isempty(obj.errorMsg), ...
'Shutter is in an error state: %s',obj.errorMsg);
validateattributes(tf,{'numeric','logical'},{'scalar','binary'});
applyWaitTime = tf && ~obj.isOpen;
obj.hDOControl.setValue(xor(tf,obj.invertOutput));
obj.isOpen = tf;
if applyWaitTime
obj.lastTransitionEvent = tic();
end
end
function waitTransitionComplete(obj)
while toc(obj.lastTransitionEvent) < obj.openTime_s
pause(0.001);
end
end
Conventions worth copying
Convention |
Why |
|---|---|
|
Errors raised in a device callback are logged rather than propagating into whatever code happened to trigger the notification. |
|
Guards against both empty and deleted handles in one call. |
|
A bad value is rejected at the point of assignment, with a message naming the property. |
Set |
|
Poll on |
Devices that need background polling attach to the shared timer’s beacons instead of
creating their own timer. |
Publish state through observable properties |
Widgets and ScanImage components track a device by listening, not by polling it. |
Testing a driver
A dabs device is usable without ScanImage running, which is the fastest way to develop
one:
hShutter = dabs.generic.DigitalShutter('My Shutter');
hShutter.hDOControl = '/vDAQ0/D0.0';
hShutter.reinit();
disp(hShutter.errorMsg);
hShutter.open();
pause(1);
hShutter.close();
hShutter.delete();
Once it behaves, add it to the machine data file through
dabs.resources.ResourceStore.showConfig() and restart ScanImage.