Developing Device Drivers

The Wolfram Device Framework, built into the Wolfram Language, creates symbolic objects that represent external devices, streamlines interaction with devices, and facilitates the authoring of device drivers. A "device" in the framework can represent both an actual device, such as a temperature sensor, or encapsulate a port, such as a serial port. This tutorial explains the internals of the framework for advanced users and developers of device drivers. For details of the interaction with devices at the user level, see "Using Connected Devices".

For most devices, the functions that comprise the framework are not directly concerned with actual device programming or low-level communication with hardware, which would vary on a case-by-case basis. For instance, one implementation might involve writing (or being supplied with by a third party) low-level programs in C, and then using the WSTP API to expose the C interfaces to package-level Wolfram Language functions. These functions would then be strung together in an appropriate fashion by the framework, creating a Wolfram Language device driver. In another implementation, one might avoid low-level C programming altogether and use the Wolfram Language .NET/Link functionality to interact with a device from within driver-level Wolfram Language functions. The responsibility of the framework would again be to integrate these functions, providing a unified way of interfacing with devices through a set of user-level functions.

Overview

Developer-level functions for creating device drivers and encapsulating lower-level interactions with devices are collected in the DeviceFramework` context, see "Device Framework Functions". At the heart of a device driver is the function (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]).

Creating a device driver.

The one-argument form (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673])["class",opts] creates a typical driver and the two-argument form (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673])["class","parent",opts] implements a basic inheritance and allows you extend the parent class without repeating its definitions.

This driver implements only a read operation. Devices of this class cannot write:

DeviceFramework`DeviceClassRegister["parent","ReadFunction"->(Print["reading"]&),"FindFunction"->({{}}&)]

DeviceRead["parent"]

DeviceWrite["parent",foo]

Extend the parent class with a write operation:

DeviceFramework`DeviceClassRegister["child","parent","WriteFunction"->(Print["writing"]&)]

Devices of the child class can read and write:

DeviceRead["child"]

DeviceWrite["child",foo]

Options of (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]) determine various device and driver properties and define functions that the framework will call to discover devices of the class and execute operations that a generic device is expected to perform, such as opening a connection to a device, configuring it, reading from and writing to a device, executing a command, and closing the connection to a device and releasing external resources.

Important device driver options.

A more general workflow could involve creating a device manager (an initialization object and its handle) before opening a device and preconfiguring device properties before exposing a new device object to the user. The initialization object would then be disposed of at the release stage after the device is closed.

It is also possible to define handlers for setting and getting standardized device properties, exposing native device properties and methods, and giving the user access to input and output streams associated with a device. Finally, a driver can define a policy for creating multiple devices of the class as well as specify other custom handlers.

Advanced device driver options.

A detailed look at the (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]) options follows in the next section. As with any other Wolfram Language options, none of the options of (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]) are mandatory and many of them would typically be omitted in a particular driver implementation.

As an example, here is a driver for a simple device that can perform the device read operation, packaged in a file:

BeginPackage["MyDevice`"]

Begin["`Private`"]

read[__] := XXXXX

DeviceFramework`DeviceClassRegister["MyDevice", "ReadFunction" -> read]

End[]

EndPackage[]

A basic driver file MyDevice.m.

To be automatically discoverable by the framework, a (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673])["class",…] statement for class must reside in a Wolfram Language package class.m. Note the naming convention; the first string argument of (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]) matches the driver file name. Further, the driver must be placed at one of the following locations:

in the current evaluation notebook directory ((RefLink[NotebookDirectory, paclet:ref/NotebookDirectory]))

in a special directory (RefLink[ToFileName, paclet:ref/ToFileName])[{(RefLink[$InstallationDirectory, paclet:ref/$InstallationDirectory]),"SystemFiles","Devices","DeviceDrivers"}] on your computer system

anywhere on (RefLink[$Path, paclet:ref/$Path])

on a paclet server or in a driver paclet directory (see "Developing Device Driver Paclets")

A driver can be used even if it does not follow these conventions, but then it must be explicitly loaded in a Wolfram Language session, for instance, with (RefLink[Get, paclet:ref/Get]).

To make a driver available on a specific platform, you can wrap a (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673])[] statement in an (RefLink[If, paclet:ref/If]).

This driver will only be available on Mac OS X:

If[$OperatingSystem==="MacOSX",DeviceFramework`DeviceClassRegister["mac"];]

Device Driver Options

The options of (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]), also called device driver options, allow you to create a driver suitable for your device. Driver options whose names include the word "Function" (driver functions) roughly correspond to user-level functions. For instance, (RefLinkPlain["ConfigureFunction", paclet:tutorial/DevelopingDeviceDrivers#372300196]) is called during the execution of (RefLink[DeviceConfigure, paclet:ref/DeviceConfigure]), (RefLinkPlain["FindFunction", paclet:tutorial/DevelopingDeviceDrivers#2076911133]) is called in (RefLink[FindDevices, paclet:ref/FindDevices]), and so on. However, there are also user-level functions that span more than one driver function. One example is the device opening sequence, executed during (RefLink[DeviceOpen, paclet:ref/DeviceOpen]), that includes (RefLinkPlain["OpenManagerFunction", paclet:tutorial/DevelopingDeviceDrivers#180358514]), (RefLinkPlain["MakeManagerHandleFunction", paclet:tutorial/DevelopingDeviceDrivers#175207631]), (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029]), and (RefLinkPlain["PreconfigureFunction", paclet:tutorial/DevelopingDeviceDrivers#525002300]).

The first argument supplied to many driver functions is a list in the form {ihandle, dhandle}, where ihandle is the handle to the initialization object (or manager handle; see (RefLinkPlain["MakeManagerHandleFunction", paclet:tutorial/DevelopingDeviceDrivers#175207631])) and dhandle is the device handle (see (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029])). This convention, employed in (RefLinkPlain["ConfigureFunction", paclet:tutorial/DevelopingDeviceDrivers#372300196]), (RefLinkPlain["ReadFunction", paclet:tutorial/DevelopingDeviceDrivers#75304213]), and other driver functions, lets you use whatever handle is necessary and skip the other one. For instance, if you only need ihandle, your driver function could be implemented as f[{ih_, _}, args___]:=(* use ih *); to use only dhandle, you provide a rule for f[{_, dh_}, args___]:=…; to skip both handles, use f[_, args___]:=…; and so on. You will find many examples of such usage below.

The remaining arguments supplied to a driver function are typically passed from the parent top-level function, possibly stripped of a list wrapper. For instance, for both (RefLink[DeviceRead, paclet:ref/DeviceRead])[dev,param] and (RefLink[DeviceRead, paclet:ref/DeviceRead])[dev,{param}], your implementation of (RefLinkPlain["ReadFunction", paclet:tutorial/DevelopingDeviceDrivers#75304213])->f will receive a call as f[handles,param]; for (RefLink[DeviceRead, paclet:ref/DeviceRead])[dev,{(Subscript[param, 1]),(Subscript[param, 2]),…}], the function will be called as f[handles,(Subscript[param, 1]),(Subscript[param, 2]),…]; for (RefLink[DeviceRead, paclet:ref/DeviceRead])[dev], the call will be in the form f[handles], and so on. You should anticipate all possible combinations of arguments that are documented for the parent function and issue appropriate error messages if some combinations do not make sense in your case. If it fails, a driver function is expected to return (RefLink[$Failed, paclet:ref/$Failed]), unless specifically stated otherwise in this tutorial.

When reporting errors, it is customary to associate messages with your own user-visible symbols if your driver exports such symbols. Otherwise, you can assign messages for your class to a special symbol DeviceFramework`Devices`class. Some examples of this convention are given in this tutorial. To learn more, see "Messages".

Of course, if your driver does not implement some driver functions, you need not bother about error messages for those functions either. Such messages are issued by the framework.

This dummy driver does not implement any driver functions:

DeviceFramework`DeviceClassRegister["dummy"]

You can open a dummy device, but attempting to execute any specific operation on it will result in error:

dev=DeviceOpen["dummy"]

DeviceConfigure[dev,a->x]

In reading through this tutorial, you will have noticed that most examples employ only top-level Wolfram Language commands. This is done on purpose so that you could reevaluate examples and learn to use the framework without any specific device. Real-life drivers will, of course, also call external programs. See "Calling External Programs" for an overview and the "Examples" section below for a typical implementation.

Besides examples in this tutorial, you might also want to look over the implementation of demo drivers referenced in the Wolfram Language documentation. For instance, to see how you could implement (RefLinkPlain["ExecuteAsynchronousFunction", paclet:tutorial/DevelopingDeviceDrivers#168337089]), look at the demo driver for (RefLink[DeviceExecuteAsynchronous, paclet:ref/DeviceExecuteAsynchronous]).

Execute the following command to examine a demo implementation of (RefLinkPlain["ExecuteAsynchronousFunction", paclet:tutorial/DevelopingDeviceDrivers#168337089]):

DeviceFramework`DeviceDriverLoad["AsynchronousDemo"]//SystemOpen

"FindFunction"

In order for your device to be discoverable, your driver must implement the "FindFunction" option. "FindFunction"->f specifies that f[] should be called by (RefLink[FindDevices, paclet:ref/FindDevices])[class] to find devices of the given class. The function f receives no arguments and should return an output in the form {{(Subscript[arg, 11]),(Subscript[arg, 12]),…},{(Subscript[arg, 21]),(Subscript[arg, 22]),…},…}, where {(Subscript[arg, i1]),(Subscript[arg, i2]),…} is a list of arguments that could be supplied to (RefLink[DeviceOpen, paclet:ref/DeviceOpen])[class,{(Subscript[arg, i1]),(Subscript[arg, i2]),…}] to open the device number i.

Typical arguments returned by the function f would be a port name, a GPIB address, or a camera name. If a device can be opened without arguments, f can return {{}}&.

If your driver implements "FindFunction", the user will also be able to use devices of your class without explicitly opening them first.

Devices without a proper "FindFunction" are not automatically discoverable:

DeviceFramework`DeviceClassRegister["read","ReadFunction"->(Random[]&)]

FindDevices["read"]

You must open a device of such a class before it can be used:

DeviceRead["read"]

DeviceOpen["read"]

DeviceRead["read"]

This driver extends the previous one by supplying the "FindFunction" option:

DeviceFramework`DeviceClassRegister["read2","read","FindFunction"->({{}}&)]

Devices of this class can be opened automatically by the framework:

DeviceRead["read2"]

Devices remain open after performing the required operation until they are explicitly closed:

Devices["read2"]

DeviceClose[%]

"OpenManagerFunction"

"OpenManagerFunction"->f specifies that f[args…] should be called at the outset of the device opening sequence to create an initialization object, also called the "device manager". args are the same as the user supplies to (RefLink[DeviceOpen, paclet:ref/DeviceOpen])["class",{args…}]. The return value will be passed to (RefLinkPlain["ReleaseFunction", paclet:tutorial/DevelopingDeviceDrivers#1264392131]) in the deinitialization stage at the end of the device life cycle. Use (RefLinkPlain[DeviceInitObject, paclet:tutorial/DevelopingDeviceDrivers#1494825346]) to retrieve the initialization object at any other time before the device is closed.

You may wish to specify "OpenManagerFunction", for instance, to open a WSTP connection to an external program. In that case, the function would return a (RefLink[LinkObject, paclet:ref/LinkObject]), which you can close with (RefLink[LinkClose, paclet:ref/LinkClose]) in your (RefLinkPlain["ReleaseFunction", paclet:tutorial/DevelopingDeviceDrivers#1264392131]).

This driver keeps a count of open devices in the class:

DeviceFramework`DeviceClassRegister["count","OpenManagerFunction"->(($counter++)&),"ReleaseFunction"->(($counter--)&)];

The counter is incremented (decremented) when a device is opened (closed):

$counter = 0;

dev=DeviceOpen["count"]

$counter

DeviceClose[dev];$counter

"MakeManagerHandleFunction"

You can create a separate handle to the initialization object (a manager handle) by specifying "MakeManagerHandleFunction"->f. The function f[obj] will be called after (RefLinkPlain["OpenManagerFunction", paclet:tutorial/DevelopingDeviceDrivers#180358514]) and supplied the initialization object obj created by (RefLinkPlain["OpenManagerFunction", paclet:tutorial/DevelopingDeviceDrivers#180358514]). The return value is assumed to be the manager handle. In the absence of "MakeManagerHandleFunction", the manager handle will be the initialization object itself.

The manager handle is given to many driver functions as a part of the first argument. At other times, you can retrieve the handle with (RefLinkPlain[DeviceManagerHandle, paclet:tutorial/DevelopingDeviceDrivers#1494825346]).

An example of a manager handle would be a socket handle or a .NET object.

"OpenFunction"

The function specified with (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029])->f is the main function called by the framework in response to a user call to (RefLink[DeviceOpen, paclet:ref/DeviceOpen]).  f[ihandle,args…] receives the manager handle ihandle created by (RefLinkPlain["MakeManagerHandleFunction", paclet:tutorial/DevelopingDeviceDrivers#175207631]) and the arguments args the user supplies to (RefLink[DeviceOpen, paclet:ref/DeviceOpen])["class",{args…}]. The return value is called the "device handle". That handle will be given to many driver functions along with the manager handle. Otherwise, you can retrieve it with (RefLinkPlain[DeviceHandle, paclet:tutorial/DevelopingDeviceDrivers#1494825346]).

The framework attempts to maintain a one-to-one correspondence between the device handle returned by "OpenFunction" and the top-level (RefLink[DeviceObject, paclet:ref/DeviceObject]) returned by (RefLink[DeviceOpen, paclet:ref/DeviceOpen]). It is therefore strongly recommended that your device handles be unique or at least unlikely to coincide with handles created by other drivers outside your control. One easy way to achieve that is to generate your handle using (RefLink[CreateUUID, paclet:ref/CreateUUID]). Alternatively, you can return a handle in the form "com.company.class"[… ] or use similar means.

Devices of this class print open arguments. The ihandle parameter is not used:

open[_,args___] := (Print[{args}];CreateUUID[])

DeviceFramework`DeviceClassRegister["print","OpenFunction"->open];

dev=DeviceOpen["print",1]

Retrieve the device handle and use it to get back the device object:

DeviceFramework`DeviceHandle[dev]

DeviceFramework`DeviceObjectFromHandle[%]

If "OpenFunction" is omitted, the framework assigns a random device handle to any new device in the class.

This driver does not implement "OpenFunction":

DeviceFramework`DeviceClassRegister["nopen"];

dev=DeviceOpen["nopen"]

You can still use the default device handle provided by the framework:

DeviceFramework`DeviceHandle[dev]//DeviceFramework`DeviceObjectFromHandle

"OpenReadFunction"

"OpenReadFunction"->f specifies that f[{ihandle,dhandle},args…] should be called during the device opening sequence, after (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029]), to open any input streams associated with a device. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The arguments args are those supplied to (RefLink[DeviceOpen, paclet:ref/DeviceOpen])["class",{args…}]. The return value must be an input stream object or a list of input stream objects.

This demo driver provides a sample implementation of input and output streams for a device:

DeviceFramework`DeviceDriverLoad["StreamsDemo"]//SystemOpen

"OpenWriteFunction"

"OpenWriteFunction"->f specifies that f[{ihandle,dhandle},args…] should be called during the device opening sequence, after (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029]), to open any output streams associated with a device. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The arguments args are those supplied to (RefLink[DeviceOpen, paclet:ref/DeviceOpen])["class",{args…}]. The return value must be an output stream object or a list of output stream objects.

This demo driver provides a sample implementation of input and output streams for a device:

DeviceFramework`DeviceDriverLoad["StreamsDemo"]//SystemOpen

"PreconfigureFunction"

The function specified in "PreconfigureFunction"->f concludes the device opening sequence. For the device object dev that is about to be returned to the user, f[dev] is guaranteed to be called at the end of a successful call to (RefLink[DeviceOpen, paclet:ref/DeviceOpen]), but before the initial configuration that reapplies top-level device properties of the device (if they have been set on the device previously) or sets class properties (if they are defined by the driver). "PreconfigureFunction" must return a list of properties that has been configured by the driver, by "PreconfigureFunction" itself, (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029]), or any other function in the device opening sequence. These properties will not be changed further by the framework until (RefLink[DeviceOpen, paclet:ref/DeviceOpen]) finishes. (RefLink[All, paclet:ref/All]) or (RefLink[None, paclet:ref/None]) can also be returned.

This driver sets a device property "parity" based on the device ID:

DeviceFramework`DeviceClassRegister["p","Properties"->{"parity"->Null},"PreconfigureFunction"->((#["parity"]=If[EvenQ[DeviceFramework`DeviceID[#]],"even","odd"];All)&)];

dev=DeviceOpen["p"]

dev["parity"]

"PreconfigureFunction" is also a convenient place to set status labels for the open and closed device states.

This driver automatically closes an open device after 20 seconds and displays a running indicator of time until close:

DeviceFramework`DeviceClassRegister["timeout","PreconfigureFunction"->With[{max=20},($timeout=0;DeviceFramework`DeviceStatusLabels[#]={ProgressIndicator[Dynamic[$timeout],{0,max},ImageSize->{20,3}],"Closed"};SessionSubmit[ScheduledTask[If[++$timeout≥max,DeviceClose[#]],{0.1,max},"AutoRemove"->True]];None)&]];

dev=DeviceOpen["timeout"]

"ConfigureFunction"

"ConfigureFunction"->f specifies that f[{ihandle,dhandle},args…] should be called in response to the top-level command (RefLink[DeviceConfigure, paclet:ref/DeviceConfigure])[dev,{args…}] to configure a device. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The return value of this function is ignored.

"ConfigureFunction" in this driver sets a top-level property:

config[{_,h_},assoc_]:=With[{dev=DeviceFramework`DeviceObjectFromHandle[h]},dev["p"]=Lookup[assoc,"p"]]

DeviceFramework`DeviceClassRegister["c","Properties"->{"p"->1},"ConfigureFunction"->config];

Open a device and configure it by calling (RefLink[DeviceConfigure, paclet:ref/DeviceConfigure]):

dev=DeviceOpen["c"]

DeviceConfigure[dev,"p"->"new"]

This is the new value of the property:

dev["p"]

"ReadFunction"

"ReadFunction"->f specifies that f[{ihandle,dhandle},args…] should be called in response to the top-level command (RefLink[DeviceRead, paclet:ref/DeviceRead])[dev,{args…}] to read data from a device. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The return value of this function will be passed on to the user as an output of the (RefLink[DeviceRead, paclet:ref/DeviceRead])[] command.

Devices of this class read consecutive characters from a string. The current string position for each device is stored in an association, with device handles as keys. Keys are added to the association in (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029]) and removed in (RefLinkPlain["CloseFunction", paclet:tutorial/DevelopingDeviceDrivers#911463099]):

$string="abcde";

$positions=<||>;

read[{_,h_}]:=StringTake[$string,{++$positions[h]}]/;$positions[h]<5

read[___]:=$Failed

open[___]:=Module[{h=CreateUUID[]},$positions[h]=0;h]

close[{_,h_},___]:=$positions[h]=.

DeviceFramework`DeviceClassRegister["r","ReadFunction"->read,"OpenFunction"->open,"CloseFunction"->close];

Open a device and read a character from it:

dev1=DeviceOpen["r",1]

DeviceRead[dev1]

Open another device and read several characters:

dev2=DeviceOpen["r",2]

DeviceReadList[dev2,3]

The current read counters are remembered for each open device:

$positions

Reading from the first device again gives the next characters, until the device reads the string completely:

DeviceReadList[dev1,5]

Closing a device clears its counter:

DeviceClose[dev1]

$positions

Your implementation of "ReadFunction" will be among those that benefit most from a robust argument checking. When necessary, the required error messages can be assigned to the symbol corresponding to your class name in the (RefLink[DeviceFramework`Devices`, paclet:tutorial/DevelopingDeviceDrivers#163433197]) context.

This driver lets devices read either real or integer random values and implements a rudimentary argument checking:

DeviceFramework`Devices`random::arg="Argument `` is not \"Real\" or \"Integer\".";

DeviceFramework`Devices`random::argx="DeviceRead called with `` arguments. For devices in the class \"``\", 2 arguments are expected.";

rand[_,"Real"]:=RandomReal[]

rand[_,"Integer"]:=RandomInteger[]

rand[_,x_]:=(Message[DeviceFramework`Devices`random::arg,x];$Failed)

rand[_,x__]:=(Message[DeviceFramework`Devices`random::arg,{x}];$Failed)

rand[_]:=(Message[DeviceFramework`Devices`random::argx,1,"random"];$Failed)

DeviceFramework`DeviceClassRegister["random","ReadFunction"->rand];

Open a device and call (RefLink[DeviceRead, paclet:ref/DeviceRead]) with legal arguments:

dev=DeviceOpen["random"]

DeviceRead[dev,"Real"]

DeviceRead[dev,"Integer"]

Calling (RefLink[DeviceRead, paclet:ref/DeviceRead]) with illegal arguments triggers errors:

DeviceRead[dev,"foo"]

DeviceRead[dev,{"foo","bar"}]

DeviceRead[dev]

"ReadBufferFunction"

"ReadBufferFunction"->f specifies that f[{ihandle,dhandle},…] should be called in response to the top-level command (RefLink[DeviceReadBuffer, paclet:ref/DeviceReadBuffer])[dev,args…] to read data from a device buffer. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. Arguments passed to the function f are as follows:

The possible signatures of "ReadBufferFunction".

The return value of f will be passed on to the user as an output of the (RefLink[DeviceReadBuffer, paclet:ref/DeviceReadBuffer])[] command.

A typical example of using "ReadBufferFunction" is reading a sequence of bytes from a serial device.

"WriteFunction"

"WriteFunction"->f specifies that f[{ihandle,dhandle},args…] should be called in response to the top-level command (RefLink[DeviceWrite, paclet:ref/DeviceWrite])[dev,{args…}] to write data to a device. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The return value of this function is ignored.

This driver creates a new notebook for every call to (RefLink[DeviceWrite, paclet:ref/DeviceWrite]):

write[_,args___]:=CreateDocument[{args}]

DeviceFramework`DeviceClassRegister["w","WriteFunction"->write];

Open a device and execute a write operation:

dev=DeviceOpen["w"]

DeviceWrite[dev,"Hello world"]

If your device writes values of several parameters, it is common to supply their values to (RefLink[DeviceWrite, paclet:ref/DeviceWrite]) as an association or a set of rules. The job of parsing such rules then falls on your implementation of "WriteFunction".

Parse rules in "WriteFunction":

writer[_,assoc_]:=CreateDocument[MapThread[TextCell,{Lookup[assoc,#,""],#}&@{"Title","Section","Text"} ]]

DeviceFramework`DeviceClassRegister["wr","WriteFunction"->writer];

Open a device and execute a write operation to create a new notebook with the specified text and title and a blank section cell:

dev=DeviceOpen["wr"]

DeviceWrite[dev,<|"Title"->"Hello","Text"->"world"|>]

"WriteBufferFunction"

"WriteBufferFunction"->f specifies that f[{ihandle,dhandle},args…] should be called in response to the top-level command (RefLink[DeviceWriteBuffer, paclet:ref/DeviceWriteBuffer])[dev,{args…}] to write data to a device buffer. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The return value of this function is ignored.

A typical example of using "WriteBufferFunction" is writing a sequence of bytes to a serial device.

"ExecuteFunction"

"ExecuteFunction"->f specifies that f[{ihandle,dhandle},"command",args…] should be called in response to the top-level command (RefLink[DeviceExecute, paclet:ref/DeviceExecute])[dev,"command",{args…}] to execute a command on a device. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The return value of this function will be passed on to the user as an output of the (RefLink[DeviceExecute, paclet:ref/DeviceExecute])[] command.

"ExecuteAsynchronousFunction"

"ExecuteAsynchronousFunction"->f specifies that f[{ihandle,dhandle},"command",args…,fun] should be called in response to the top-level command (RefLink[DeviceExecuteAsynchronous, paclet:ref/DeviceExecuteAsynchronous])[dev,"command",{args…},fun] to start an asynchronous execution of the specified command on a device. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The function f will typically pass the user-specified handler function fun downstream to be executed when an event occurs. The return value of f, which should be (RefLink[AsynchronousTaskObject, paclet:ref/AsynchronousTaskObject])[] or a similar object, will be passed on to the user as an output of the (RefLink[DeviceExecuteAsynchronous, paclet:ref/DeviceExecuteAsynchronous])[] command.

If command is not supported, the function f must yield a (RefLink[Missing, paclet:ref/Missing])[] object or simply return unevaluated. In either case, the framework will issue an appropriate message. In case of other errors, f must issue a proper message and return (RefLink[$Failed, paclet:ref/$Failed]). In particular, this should happen if the specified command cannot be executed with the given arguments.

command can be "Read", "Write", "ReadBuffer", "WriteBuffer", or any other commands that are supported by the driver.

This driver asynchronously saves the content of a URL to a file:

async[_,"Save",url_,file_,opts___,fun_]:=URLSaveAsynchronous[url,file,fun,opts]

DeviceFramework`DeviceClassRegister["async","ExecuteAsynchronousFunction"->async];

dev=DeviceOpen["async"]

Prepare a progress function and a progress indicator:

$progress= 0.;

pfun[_,"progress",{now_,total_,__}]:=Quiet[$progress = now/total]

Dynamic[ProgressIndicator[$progress]]

Start an asynchronous task and observe the progress:

url="http://exampledata.wolfram.com/10mb.dat";
temp=CreateTemporary[];

DeviceExecuteAsynchronous[dev,"Save",{url, temp,"Progress"->True}, pfun]

The framework issues an error message for unsupported asynchronous operations:

DeviceExecuteAsynchronous[dev,"foo",{url, temp,"Progress"->True}, pfun]

"CloseFunction"

"CloseFunction"->f specifies that f[{ihandle,dhandle}] should be called in response to the top-level command (RefLink[DeviceClose, paclet:ref/DeviceClose])[dev] to close a device and free related resources. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The return value of this function will be passed on to the user as an output of the (RefLink[DeviceClose, paclet:ref/DeviceClose])[] command. On successful completion, it is expected to be (RefLink[Null, paclet:ref/Null]).

The implementation of "CloseFunction" would typically involve closing all ports and sockets and releasing other resources, with the exception of those that are closed in (RefLinkPlain["ReleaseFunction", paclet:tutorial/DevelopingDeviceDrivers#1264392131]). Streams opened in (RefLinkPlain["OpenReadFunction", paclet:tutorial/DevelopingDeviceDrivers#1332248205]) and (RefLinkPlain["OpenWriteFunction", paclet:tutorial/DevelopingDeviceDrivers#1557293485]) are automatically closed by the framework, provided that they can be closed using (RefLink[Close, paclet:ref/Close]). If that is not the case, you should close the streams in "CloseFunction".

A closed device remains available in the current Wolfram Language session. Use (RefLinkPlain["DeregisterOnClose", paclet:tutorial/DevelopingDeviceDrivers#1535191473]) to completely remove the device after it is closed.

"ReleaseFunction"

"ReleaseFunction"->f specifies that f[obj] should be called after (RefLinkPlain["CloseFunction", paclet:tutorial/DevelopingDeviceDrivers#911463099]) during the operation of (RefLink[DeviceClose, paclet:ref/DeviceClose]) to destroy the initialization object obj created by (RefLinkPlain["OpenManagerFunction", paclet:tutorial/DevelopingDeviceDrivers#180358514]) and perform any other remaining cleanup tasks if necessary. The return value of this function is ignored, unless it is (RefLink[$Failed, paclet:ref/$Failed]).

If you used (RefLinkPlain["OpenManagerFunction", paclet:tutorial/DevelopingDeviceDrivers#180358514]) to open a WSTP connection to an external program, you could close the connection in "ReleaseFunction".

"DeregisterOnClose"

"DeregisterOnClose"->(RefLink[True, paclet:ref/True]) specifies that (RefLink[DeviceClose, paclet:ref/DeviceClose]) should not only release all external resources, but also completely remove the device from the current Wolfram Language session. By default, with "DeregisterOnClose"->(RefLink[False, paclet:ref/False]), the device remains available and can be reopened with exactly the same parameters.

With the default value of the "DeregisterOnClose" option, a closed device remains available:

DeviceFramework`DeviceClassRegister["dummy"];

dev=DeviceOpen["dummy",1]

DeviceClose[dev]; Devices["dummy"]

The framework keeps track of the parameters used to open the device:

DeviceFramework`DeviceOpenArguments[dev]

You can reopen the device without specifying open arguments:

DeviceOpen[dev]

Devices of this class are automatically deregistered after they are closed:

DeviceFramework`DeviceClassRegister["dereg","DeregisterOnClose"->True];

DeviceOpen["dereg"]

DeviceClose[%];Devices["dereg"]

"Singleton"

The "Singleton" option determines how the framework processes the (RefLink[DeviceOpen, paclet:ref/DeviceOpen])["class",{args…}] command, thereby setting the creation policy for multiple devices. Possible values of the "Singleton" option are as follows:

Creation policy for multiple devices in a class.

The default value (RefLink[Automatic, paclet:ref/Automatic]) is equivalent to the criterion crit equal to (RefLink[SameQ, paclet:ref/SameQ])[(RefLinkPlain[DeviceOpenArguments, paclet:tutorial/DevelopingDeviceDrivers#1494825346])[#1],#2]&.

With "Singleton"->(RefLink[Automatic, paclet:ref/Automatic]), (RefLink[DeviceOpen, paclet:ref/DeviceOpen]) returns a new device only if called with a new set of arguments:

DeviceFramework`DeviceClassRegister["auto"];

DeviceOpen["auto"]

DeviceOpen["auto",1]

Calling (RefLink[DeviceOpen, paclet:ref/DeviceOpen]) with the same arguments gives the same device:

DeviceOpen["auto",1]

With "Singleton"->(RefLink[True, paclet:ref/True]), the framework ignores all calls to (RefLink[DeviceOpen, paclet:ref/DeviceOpen]) after the first one and returns the same device even if called with different arguments:

DeviceFramework`DeviceClassRegister["single","Singleton"->True];

DeviceOpen["single"]

DeviceOpen["single",1]

With "Singleton"->(RefLink[False, paclet:ref/False]), (RefLink[DeviceOpen, paclet:ref/DeviceOpen]) returns a new device even if called with the same arguments:

DeviceFramework`DeviceClassRegister["multi","Singleton"->False];

DeviceOpen["multi"]

DeviceOpen["multi"]

Importantly, the framework will not typically execute the opening sequence of driver functions, including (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029]), if it does not need to open a new device in response to (RefLink[DeviceOpen, paclet:ref/DeviceOpen]). However, it will execute the opening sequence if the specified criterion crit returns (RefLink[True, paclet:ref/True]), but the arguments args do not match exactly the arguments with which the device was previously open. This lets you reconfigure the same device for a different set of arguments.

With this driver, (RefLink[DeviceOpen, paclet:ref/DeviceOpen]) returns a new device for every distinct first argument:

DeviceFramework`DeviceClassRegister["single1","OpenFunction":>((Print[StringForm["Configuring `` with ``",##2]];CreateUUID[])&),"Singleton"->(First[DeviceFramework`DeviceOpenArguments[#1]]===First[#2]&)];

DeviceOpen["single1",{"foo",1}]

DeviceOpen["single1",{"bar",1}]

Reconfigure the first device with a different input argument:

DeviceOpen["single1",{"foo",2}]

This demo device has a slightly more robust implementation of the "Singleton" option:

DeviceFramework`DeviceDriverLoad["SingletonDemo"]//SystemOpen

"Properties"

The option "Properties"->{"(Subscript[property, 1])"->(Subscript[value, 1]),"(Subscript[property, 2])"->(Subscript[value, 2]),…} specifies the class properties that typically determine standardized (top-level) properties of a newly created device. Standardized property names are usually given as strings. A driver can also specify native properties.

"GetPropertyFunction"

"GetPropertyFunction"->f specifies that f[dev,p] should be called to query the value of the standardized property p of the device dev. The return value will be passed on to the user. The default for "GetPropertyFunction" is (RefLinkPlain[DeviceGetProperty, paclet:tutorial/DevelopingDeviceDrivers#118403969]), which you can call inside your function f.

This driver keeps track of how many times a standardized property is read:

DeviceFramework`DeviceClassRegister["get","Properties"->{"p"->"x"},"GetPropertyFunction"->(($counter++;DeviceFramework`DeviceGetProperty[##])&)];

Set up the counter and open a device:

$counter =0; Dynamic[$counter]

dev=DeviceOpen["get"]

Reading the property value increments the counter:

dev["p"]

dev["Rules"]

You will typically define "GetPropertyFunction" to query a property value that is stored on a device.

"SetPropertyFunction"

"SetPropertyFunction"->f specifies that f[dev,p,val] should be called to set the standardized property p on the device dev to the value val. The return value of f is ignored. The default for "SetPropertyFunction" is (RefLinkPlain[DeviceSetProperty, paclet:tutorial/DevelopingDeviceDrivers#118403969]), which you can call inside your function f.

Although you will typically use "SetPropertyFunction" to change property values on a device, you can also use the same function to filter out invalid property values, designate some properties as read only, link standardized properties with native properties, etc.

This driver creates a regular property "p" and a read-only property "r":

set[dev_,p:"r",_] := Message[DeviceObject::ronly,p,DeviceFramework`DeviceClass[dev]]

set[args__] := DeviceFramework`DeviceSetProperty[args]

pre[dev_]:=(
DeviceFramework`DeviceSetProperty[dev,"r",DeviceFramework`DeviceClassProperties[DeviceFramework`DeviceClass[dev],"r"]];"r"
)

DeviceFramework`DeviceClassRegister["ronly","Properties"->{"p"->"x","r"->"fixed"},"SetPropertyFunction"->set,"PreconfigureFunction"->pre];

Open a device and change the property "p":

dev=DeviceOpen["ronly"]

dev["p"]="new";dev["p"]

The value of "r" cannot be changed:

dev["r"]="new";dev["r"]

Advanced Topic: Keeping Top-Level Properties in Sync with the Device

As a somewhat advanced exercise, you can develop a driver that keeps a standardized property in sync with a property that can be independently changed on the device.

In this case, the "external" value is mocked up using the variable $external. In a real-life driver, you would instead call the external property setter (getter) to change (get the value of) the property:

$external=0;

The set function uses the default handler for top-level accounting. It also communicates with the device to set the property value on the device if it is open:

set[dev_,p_,v_]:=(
DeviceFramework`DeviceSetProperty[dev,p,v];
If[DeviceOpenQ[dev],$external=v]
)

For an open device, the get function queries the property value on the device and keeps the value it obtains in the top-level interface. If the device is closed, the function simply returns the top-level value:

get[dev_,p_]:=DeviceFramework`DeviceSetProperty[dev,p,$external]/;DeviceOpenQ[dev]
get[args__]:=DeviceFramework`DeviceGetProperty[args]

This sets up the driver. (RefLinkPlain["PreconfigureFunction", paclet:tutorial/DevelopingDeviceDrivers#525002300]) in this case tells the driver to skip the configuration of the property "p":

DeviceFramework`DeviceClassRegister["set","Properties"->{"p"->"x"},"GetPropertyFunction"->get,"SetPropertyFunction"->set,"PreconfigureFunction"->("p"&)];

Open a device and check its property. The value comes from the variable $external (that is, the value "x" is ignored):

dev=DeviceOpen["set"]

dev["p"]

Change the property value. The "external" variable also changes:

dev["p"]=1;

{dev["p"], $external}

If the "external" variable changed outside this session (e.g., the user pressed a button on the device), the next query of the property picks up the new value:

$external=2;

dev["p"]

Close the device and change the external property on the device again:

DeviceClose[dev];

$external=3;

Because there is no communication with the device, the reported value of the property is a stale one:

dev["p"]

The value is automatically updated when the device is reopened:

DeviceOpen[dev]

dev["p"]

"NativeProperties"

The option "NativeProperties"->{(Subscript[property, 1]),(Subscript[property, 2]),…} specifies a list of native properties that are typically available on a device of a given class. Native properties are useful when you wish to access your device in a standard way provided by the framework and still have an option to work with the device directly. Names of native properties are usually defined by a third-party library outside your control. They can be any "reasonable" Wolfram Language expressions.

As an example, you might want to set up a driver to use (RefLink[DeviceOpen, paclet:ref/DeviceOpen]), (RefLink[DeviceRead, paclet:ref/DeviceRead]), (RefLink[DeviceExecute, paclet:ref/DeviceExecute]), and other standard Wolfram Language functions for a device connected through .NET/Link and at the same time let your users communicate with the device via the .NET/Link interface directly.

This demo device provides a sample implementation of both standardized and native properties:

DeviceFramework`DeviceDriverLoad["PropertiesDemo"]//SystemOpen

"GetNativePropertyFunction"

"GetNativePropertyFunction"->f specifies that f[dhandle,p] should be called to query the value of the native property p of the device identified by the device handle dhandle. The function f should return the property value. The default (RefLink[Automatic, paclet:ref/Automatic]) is essentially equivalent to (RefLink[Function, paclet:ref/Function])[{dhandle,p},dhandle@p].

This demo device provides a sample implementation of "GetNativePropertyFunction":

DeviceFramework`DeviceDriverLoad["PropertiesDemo"]//SystemOpen

"SetNativePropertyFunction"

"SetNativePropertyFunction"->f specifies that f[dhandle,p,val] should be called to set the native property p on the device identified by the device handle dhandle to the value val. The return value of the function is ignored. The default (RefLink[Automatic, paclet:ref/Automatic]) is essentially equivalent to (RefLink[Function, paclet:ref/Function])[{dhandle,p,val},dhandle@p=val].

This demo device provides a sample implementation of "SetNativePropertyFunction":

DeviceFramework`DeviceDriverLoad["PropertiesDemo"]//SystemOpen

"StatusLabelFunction"

By default, the framework uses (RefLinkPlain[DeviceDefaultStatusLabels, paclet:tutorial/DevelopingDeviceDrivers#118403969])[] to create status labels for devices opened with (RefLink[DeviceOpen, paclet:ref/DeviceOpen])[class] and (RefLinkPlain[DeviceDefaultStatusLabels, paclet:tutorial/DevelopingDeviceDrivers#118403969])[p] for devices opened with a parameter p in (RefLink[DeviceOpen, paclet:ref/DeviceOpen])[class,{p,…}]. You can use the option "StatusLabelFunction"->f to create your own labels. The function f[{args}] will be called at the device preparation stage with arguments args supplied in (RefLink[DeviceOpen, paclet:ref/DeviceOpen])["class",{args…}]. It must return a string to replace the label for an open device or a list of two strings {olbl,clbl} to replace both the label olbl for an open device and the label clbl for a closed device.

This driver implements custom status labels:

DeviceFramework`DeviceClassRegister["status","StatusLabelFunction"->({"Last opened "<>DateString[],"Idle"}&)];

dev=DeviceOpen["status"]

DeviceClose[dev]; dev

To construct more sophisticated labels, call (RefLinkPlain[DeviceStatusLabels, paclet:tutorial/DevelopingDeviceDrivers#258574061]) in (RefLinkPlain["PreconfigureFunction", paclet:tutorial/DevelopingDeviceDrivers#525002300]).

"DeviceIconFunction"

"DeviceIconFunction"->f specifies that  f[{ihandle,dhandle},args…] should be called to create a custom icon for the device object created in response to (RefLink[DeviceOpen, paclet:ref/DeviceOpen])["class",{args…}]. The first argument {ihandle,dhandle}, where ihandle is the manager handle and dhandle is the device handle, is common to many driver functions. The function must return a (RefLink[Graphics, paclet:ref/Graphics]) object.

DeviceFramework`DeviceClassRegister["icon","DeviceIconFunction"->(Graphics[{Red,Disk[]}]&)];

dev=DeviceOpen["icon"]

"DriverVersion"

Use "DriverVersion"->num to specify the version for your driver as a numeric value num. The framework will automatically load the highest, most recent version even if there happen to be previous versions available. It is also possible to load other versions; see "Working with Driver Files".

You can retrieve the version number of a loaded driver with (RefLinkPlain[DeviceDriverVersion, paclet:tutorial/DevelopingDeviceDrivers#1958266969]).

Specify a driver version and retrieve it:

DeviceFramework`DeviceClassRegister["v","DriverVersion"->1.001];

DeviceFramework`DeviceDriverVersion["v"]

Device Framework Functions

In addition to (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]), the framework provides functions for working with driver files, extracting both user-visible and internal information about device objects, accessing class preferences, and other utilities.

Adding and removing a driver class.

Internally, (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#939564002]) calls (RefLinkPlain[DeviceClassClear, paclet:tutorial/DevelopingDeviceDrivers#1443390889]) and effectively removes all existing definitions for the specified class before creating any new ones, so you can safely call (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#939564002]) multiple times when developing your driver.

Working with Driver Files

Working with driver files.

(RefLinkPlain[FindDeviceDrivers, paclet:tutorial/DevelopingDeviceDrivers#1958266969]) returns a list of triplets in the form {"path/to/driver",class,version}. You can use this information to inspect a driver file without loading it in your Wolfram Language session, compare different driver versions, or load a prior version of the driver instead of the most recent version, which is automatically loaded by (RefLink[DeviceOpen, paclet:ref/DeviceOpen]).

Find all available drivers for a class and open the corresponding files in your system editor:

DeviceFramework`FindDeviceDrivers["RandomSignalDemo"]

SystemOpen/@First/@%;

For already loaded drivers, you can find out exactly which file is loaded using (RefLinkPlain[DeviceDriverFile, paclet:tutorial/DevelopingDeviceDrivers#1958266969]). This can be useful if you have multiple implementations of your driver and want to make sure that the correct version is loaded by the framework; or to reload the driver after you have edited it; or to simply learn finer points of the implementation of a driver.

Open a demo driver and locate the driver file:

dev=DeviceOpen["PropertiesDemo"]

DeviceFramework`DeviceDriverFile[%]

Execute the following command to open the driver file as a notebook in your Wolfram Language session:

NotebookOpen[%]

Accessing User-Visible Information about a Device

Accessing user-visible information about a device object.

You can change status labels at any time during device operation. The new labels will be displayed the next time the front end creates a UI for your device object. Therefore, most frequently, you will create custom status labels in (RefLinkPlain["PreconfigureFunction", paclet:tutorial/DevelopingDeviceDrivers#525002300]) before the device is presented to the user for the first time.

Change the built-in status labels for a demo device:

dev=DeviceOpen["RandomSignalDemo"]

DeviceFramework`DeviceStatusLabels[dev]={"open","closed"};

dev

DeviceClose[dev]

Accessing Internal Information about a Device

Accessing internal objects kept by the framework.

Although (RefLinkPlain[DeviceDriverOption, paclet:tutorial/DevelopingDeviceDrivers#1494825346]) can be called at any time, it is particularly useful for calling the parent's functions in a derived class.

This driver is based on the "RandomSignalDemo" and calls the parent's (RefLinkPlain["ReadFunction", paclet:tutorial/DevelopingDeviceDrivers#75304213]) inside its own:

DeviceFramework`DeviceClassRegister["rw","RandomSignalDemo","ReadFunction"->(($randomTotal+=DeviceFramework`DeviceDriverOption["RandomSignalDemo","ReadFunction"][Null]-.5)&)];

Reading from the device gives a list of accumulated random values, whereas reading from the parent gives independent values:

$randomTotal=0;

ListLinePlot[DeviceReadList["rw",200]]

ListLinePlot[DeviceReadList["RandomSignalDemo",200]]

(RefLinkPlain[DeviceDriverOption, paclet:tutorial/DevelopingDeviceDrivers#1494825346]) also lets you dynamically change the value of the driver function during operation of the device.

Originally, a device of this class reads off the sine of the parameter supplied to (RefLink[DeviceRead, paclet:ref/DeviceRead]):

DeviceFramework`DeviceClassRegister["f","ReadFunction"->(Sin[#2]&),"FindFunction"->({{}}&)];

ListLinePlot[Table[DeviceRead["f",i],{i,0,20,.1}]]

This changes the driver to read off a sawtooth wave:

DeviceFramework`DeviceDriverOption["f","ReadFunction"]=Mod[#2,5]&;

ListLinePlot[Table[DeviceRead["f",i],{i,0,20,.1}]]

Class Properties

When the framework needs to assign standardized properties in a particular instance of (RefLink[DeviceObject, paclet:ref/DeviceObject]), it takes the values from class properties. Class properties are specified by the (RefLinkPlain["Properties", paclet:tutorial/DevelopingDeviceDrivers#720023240]) options of (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]) and can be accessed at any time after the driver is loaded.

Accessing class properties.

Class properties are available even before any device objects are created for the class:

DeviceFramework`DeviceDriverLoad["PropertiesDemo"];

DeviceFramework`DeviceClassProperties["PropertiesDemo"]

DeviceFramework`DeviceClassProperties["PropertiesDemo","P1"]

Changing device properties does not alter class properties:

dev1=DeviceOpen["PropertiesDemo",1]

dev1["P1"]="newP1";

DeviceFramework`DeviceClassProperties["PropertiesDemo","P1"]

Conversely, changing class properties does not reset properties of the existing devices, but it does alter properties of new devices in the class:

DeviceFramework`DeviceClassProperties["PropertiesDemo","P1"]="classP1";

dev1["P1"]

dev2=DeviceOpen["PropertiesDemo",2]

dev2["P1"]

Class Preferences

Class preferences, assessed through DeviceClassPreferences, is a more flexible storage for a class, which is persistent between Wolfram Language sessions. Preferences are independent of the device driver, so you can call DeviceClassPreferences before, after, or without ever loading the driver; and that call does not load the driver.

Accessing a set of class preferences.

The association of class preferences can contain arbitrary key-value pairs. They are always checked internally at the outset of (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]). If the preferences happen to contain keys corresponding to options of (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]), their values take precedence over options specified in the driver.

Save the (RefLinkPlain["Properties", paclet:tutorial/DevelopingDeviceDrivers#720023240]) preferences for a class:

DeviceFramework`DeviceClassPreferences["prefs","Properties"]={"a"->1}

When a driver is being created, the value from its preferences is used instead of the value prescribed by the (RefLinkPlain[DeviceClassRegister, paclet:tutorial/DevelopingDeviceDrivers#12673]) statement, which, in this case, calls for simply copying properties from the parent class:

DeviceFramework`DeviceClassRegister["prefs","PropertiesDemo"];

dev=DeviceOpen["prefs"]

dev["Properties"]

DeviceClose[dev]

Clear preferences and reevaluate the class definition:

DeviceFramework`DeviceClassPreferences["prefs"]=.

DeviceFramework`DeviceClassRegister["prefs","PropertiesDemo"];

In the absence of preferences, this class's properties are derived from the parent class, as requested:

dev1=DeviceOpen["prefs",1]

dev1["Properties"]

You can set and query device class preferences at any time during a Wolfram Language session. In particular, preferences are useful for storing data needed for device discovery or for simply setting a flag indicating that third-party software required by your driver has been installed on the user's machine.

Devices of this class are not discoverable unless a flag is set in the class preferences:

DeviceFramework`DeviceClassRegister["nf","FindFunction"->(If[Lookup[DeviceFramework`DeviceClassPreferences["nf"],"installed",False],{{}},{}]&)];

FindDevices["nf"]

After setting the flag, a device can be found:

DeviceFramework`DeviceClassPreferences["nf","installed"]=True;

FindDevices["nf"]

Remove the preferences for the class:

DeviceFramework`DeviceClassPreferences["nf"]=.

The Default Property Handlers

When defining a custom property handler for your class, it is often convenient to call the default handler at some point so that your handler would extend the built-in one rather than recreate it from scratch.

Using the default property handlers.

Determining Device State

The top-level functions (RefLink[DeviceOpen, paclet:ref/DeviceOpen]), (RefLink[DeviceClose, paclet:ref/DeviceClose]), DeviceOpenQ, and (RefLink[Devices, paclet:ref/Devices]) respectively open, close, test, and give a list of all devices registered in a current session—whether a device is open or closed. The framework also provides several complementing functions for analogous, but less common, operations.

Testing and changing the device state.

(RefLinkPlain[DeviceDeregister, paclet:tutorial/DevelopingDeviceDrivers#218225314]) also closes a device if it is open. This function can be useful for cleaning slate when developing a driver.

Create a convenience palette to deregister all devices in the current session:

Button["DeregisterAll",DeviceFramework`DeviceDeregister[Devices[]]]//CreatePalette

Developing Device Driver Paclets

The framework automatically loads device drivers from the Wolfram paclet servers as if the driver resided locally on your computer. Therefore, whenever you want your drivers to be widely available, you will want to distribute them as paclets.

To create a device driver paclet, include a special file (WebLink[PacletInfo.m, http://reference.wolfram.com/workbench/index.jsp?topic=/com.wolfram.eclipse.help/html/reference/projects/pacletinfo.html]) in the root device driver directory, parallel to the device driver file. By convention, the name of a driver paclet is the driver class name preceded by "DeviceDriver_"; for instance, "DeviceDriver_MyDevice". Include other supporting files and directories in the root driver directory as needed.

In addition, a paclet can include Wolfram Language documentation for your device. Importantly, the documentation can contain reference pages for your error messages and provide custom troubleshooting instructions for the installation and operation of your device.

The device driver for this device is distributed as a paclet:

dev=DeviceOpen["DocumentationDemo"]

The documentation includes a custom reference page with troubleshooting instructions. Click the chevron » below to see the instructions:

DeviceOpen["DocumentationDemo","error"]

To inspect the contents of the paclet, execute the following command:

DeviceFramework`DeviceDriverFile[dev]//DirectoryName//SystemOpen

Examples

Tinker Forge Weather Station

(WebLink[Tinker Forge Weather Station, https://www.tinkerforge.com/en/shop/kits/starter-kit-weather-station.html]) connects to a computer via a mini USB cable and comprises a set of four ambient sensors (temperature, pressure, humidity, and illuminance) controlled by a 32-bit ARM micro-controller. The device comes with an attached 20×4 character LCD screen on which strings can be displayed. The sensors and the LCD screen are considered to be subdevices or "bricklets" sharing a common platform and together making up the "Tinker Forge Weather Station" device. A read command on the device would be treated as a request to read a measurement from a sensor and a write command would be interpreted as a request to display characters on the LCD screen.

Assume that all the functions for a low-level communication with the device have been implemented in the C programming language using the Tinker Forge C/C++ API (WebLink[bindings, http://www.tinkerforge.com/en/doc/Downloads.html]) and the resulting .c source files compiled into an executable file. Thus, to communicate with the device from the Wolfram Language, the first step would be to (RefLink[Install, paclet:ref/Install]) that executable, which can be conveniently done in (RefLinkPlain["OpenManagerFunction", paclet:tutorial/DevelopingDeviceDrivers#180358514]). The driver would also implement several other functions, as follows:

DeviceFramework`DeviceClassRegister["myTinkerForge",
 	"OpenManagerFunction" -> (Install["\path\to\tinkerforge\executable"]&),
 	"ReleaseFunction" -> release,
 	"MakeManagerHandleFunction" -> make,
 	"OpenFunction" -> open,
 	"ReadFunction" -> read,
 	"WriteFunction" -> write,
 	"CloseFunction" -> close
 ]

The (RefLink[Install, paclet:ref/Install]) statement in (RefLinkPlain["OpenManagerFunction", paclet:tutorial/DevelopingDeviceDrivers#180358514]) normally returns a (RefLink[LinkObject, paclet:ref/LinkObject]) on which (RefLink[LinkClose, paclet:ref/LinkClose]) is invoked in (RefLinkPlain["ReleaseFunction", paclet:tutorial/DevelopingDeviceDrivers#1264392131]) to close the WSTP connection at the deinitialization stage.

release[link_] := LinkClose[link]

Next, an implementation of the (RefLinkPlain["MakeManagerHandleFunction", paclet:tutorial/DevelopingDeviceDrivers#175207631]) returns a handle to an IP connection object representing a TCP/IP connection between the controlling C program and the device.

make[___] := Tinkerforge`IPConnectionCreate[]

This connection handle, ipconn, serves as the device manager handle ihandle and is passed to (RefLinkPlain["OpenFunction", paclet:tutorial/DevelopingDeviceDrivers#1742434029]) where it is used to connect to individual bricklets, create unique handles for each of them, and return a list of these handles. The list therefore serves as a composite device handle dhandle. In this weather station, the barometer bricklet provides readings of both pressure and temperature so there are effectively three bricklets for four sensors.

open[ipconn_, ___] := {
  	LightSensorCreate[ipconn], 
  	HumiditySensorCreate[ipconn], 
  	BarometerCreate[ipconn]
}

The triplet of bricklet handles is used to read weather data in (RefLinkPlain["ReadFunction", paclet:tutorial/DevelopingDeviceDrivers#75304213]), whereas the manager handle ihandle is not needed there.

read[{_, {lightHandle_, humHandle_, baroHandle_}}, rest___] := XXX

An implementation of (RefLinkPlain["WriteFunction", paclet:tutorial/DevelopingDeviceDrivers#372312379]), on the other hand, may not need sensor handles, but could create a handle to the LCD bricklet from ipconn on the fly.

write[{ipconn_, _}, rest___] := XXX

Finally, the implementation of (RefLinkPlain["CloseFunction", paclet:tutorial/DevelopingDeviceDrivers#911463099]) would disconnect the IP connection and destroy sensor bricklets given their handles.

close[{ipconn_, {lightHandle_, humHandle_, baroHandle_}}] := (
  	Tinkerforge`IPConnectionDisconnect[ipconn];
  	LightSensorDestroy[lightHandle];
  	HumiditySensorDestroy[humHandle];
  	BarometerDestroy[baroHandle];
)

There also exists a somewhat more elaborate implementation of the Tinker Forge Weather Station driver.

Execute the following command to open the driver file in your system editor:

DeviceFramework`DeviceDriverLoad["TinkerForgeWeatherStation"]//SystemOpen

Further Info

Please contact (WebLink[Wolfram Research, http://devices.wolfram.com/contact-us.html]) if you are interested in developing and distributing a device driver for the Wolfram Language.