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.HasMachineDataFile gives it a heading in the machine data file,

  • dabs.resources.configuration.HasConfigPage gives 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

most.ErrorHandler.addCatchingListener

Errors raised in a device callback are logged rather than propagating into whatever code happened to trigger the notification.

most.idioms.isValidObj

Guards against both empty and deleted handles in one call.

validateattributes in every public setter

A bad value is rejected at the point of assignment, with a message naming the property.

Set warnMsg for recoverable oddities

DigitalShutter tristates its control line at init and warns if the line floats to a level that would open the shutter. The device still works; the user is told what to fix.

Poll on hResourceStore.hSystemTimer

Devices that need background polling attach to the shared timer’s beacons instead of creating their own timer. dabs.resources.devices.PMT queries status on beacon_1Hz.

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.