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 context, see "Device Framework Functions". At the heart of a device driver is the function DeviceClassRegister.

DeviceFramework`DeviceClassRegister["class",opts]
register a device driver for the specified class whose operation is defined by the options opts
DeviceFramework`DeviceClassRegister["class","parent",opts]
register a class identical to parent, except for operations defined by the options opts

Creating a device driver.

The one-argument form DeviceClassRegister creates a typical driver and the two-argument form DeviceClassRegister 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.
In[1]:=
Click for copyable input
Out[1]=
In[3]:=
Click for copyable input
Out[3]=
Extend the parent class with a write operation.
In[4]:=
Click for copyable input
Out[4]=
Devices of the child class can read and write.
In[6]:=
Click for copyable input
Out[6]=

Options of DeviceClassRegister 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.

option
default value
"FindFunction"{}&how to discover devices
"OpenFunction"Nonethe function to execute when opening a device
"ConfigureFunction"Nonehow to configure a device
"ReadFunction"Nonehow to read from a device
"ReadBufferFunction"Nonehow to read from a device buffer
"WriteFunction"Nonehow to write to a device
"WriteBufferFunction"Nonehow to write to a device buffer
"ExecuteFunction"Nonehow to execute a command on a device
"ExecuteAsynchronousFunction"Nonehow to execute a command asyncronously
"CloseFunction"Nonethe function to execute when closing a device
"Properties"{}standardized properties
"DeviceIconFunction"Nonehow to create an icon for the device object
"DriverVersion"Missing["NotAvailable"]the version number of the driver

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.

option
default value
"OpenManagerFunction"Nonethe function to execute to open a device manager
"MakeManagerHandleFunction"Identityhow to create a handle to the device manager
"PreconfigureFunction"{}&how to preconfigure a new device
"ReleaseFunction"Null&how to dispose of the device manager object
"ReadTimeSeriesFunction"Automaticthe handler for DeviceReadTimeSeries
"StatusLabelFunction"Automatichow to create the status labels for the device object
"NativeProperties"{}native properties
"NativeMethods"{}native methods
"GetPropertyFunction"DeviceFramework`DeviceGetPropertyhow to get standardized properties
"SetPropertyFunction"DeviceFramework`DeviceSetPropertyhow to set standardized properties
"GetNativePropertyFunction"Automatichow to get native properties
"SetNativePropertyFunction"Automatichow to set native properties
"NativeIDFunction"Nonehow to get the native ID of a device
"OpenReadFunction"Nonehow to open input streams assoociated with the device
"OpenWriteFunction"Nonehow to open output streams assoociated with the device
"DeregisterOnClose"Falsewhether to deregister a device after it is closed
"Singleton"Automaticcreation policy for multiiple devices

Advanced device driver options.

A detailed look at the DeviceClassRegister options follows in the next section. As with any other Wolfram Language options, none of the options of DeviceClassRegister 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 DeviceClassRegister["class",] statement for class must reside in a Wolfram Language package class.m. Note the naming convention; the first string argument of DeviceClassRegister matches the driver file name. Further, the driver must be placed at one of the following locations:

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 Get.

To make a driver available on a specific platform, you can wrap a DeviceClassRegister[] statement in an If.

This driver will only be available on Mac OS X.
In[7]:=
Click for copyable input

Device Driver Options

The options of DeviceClassRegister, 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, "ConfigureFunction" is called during the execution of DeviceConfigure, "FindFunction" is called in 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 DeviceOpen, that includes "OpenManagerFunction", "MakeManagerHandleFunction", "OpenFunction", and "PreconfigureFunction".

The first argument supplied to many driver functions is a list in the form , where ihandle is the handle to the initialization object (or manager handle; see "MakeManagerHandleFunction") and dhandle is the device handle (see "OpenFunction"). This convention, employed in "ConfigureFunction", "ReadFunction", 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 ; to use only dhandle, you provide a rule for ; to skip both handles, use ; 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 DeviceRead[dev,param] and DeviceRead[dev,{param}], your implementation of "ReadFunction"f will receive a call as ; for DeviceRead[dev,{param1,param2,}], the function will be called as ; for DeviceRead[dev], the call will be in the form , 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 $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.
In[1]:=
Click for copyable input
Out[1]=
You can open a dummy device, but attempting to execute any specific operation on it will result in error.
In[2]:=
Click for copyable input
Out[2]=
In[3]:=
Click for copyable input
Out[3]=

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 "ExecuteAsynchronousFunction", look at the demo driver for DeviceExecuteAsynchronous.

Execute the following command to examine a demo implementation of "ExecuteAsynchronousFunction".
In[4]:=
Click for copyable input

"FindFunction"

In order for your device to be discoverable, your driver must implement the option. "FindFunction"f specifies that f[] should be called by FindDevices[class] to find devices of the given class. The function f receives no arguments and should return an output in the form , where is a list of arguments that could be supplied to DeviceOpen[class,{argi1,argi2,}] 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 , the user will also be able to use devices of your class without explicitly opening them first.

Devices without a proper are not automatically discoverable.
In[1]:=
Click for copyable input
Out[1]=
In[2]:=
Click for copyable input
Out[2]=
You must open a device of such a class before it can be used.
In[3]:=
Click for copyable input
Out[3]=
In[4]:=
Click for copyable input
Out[4]=
In[5]:=
Click for copyable input
Out[5]=
This driver extends the previous one by supplying the option.
In[6]:=
Click for copyable input
Out[6]=
Devices of this class can be opened automatically by the framework.
In[7]:=
Click for copyable input
Out[7]=
Devices remain open after performing the required operation until they are explicitly closed.
In[8]:=
Click for copyable input
Out[8]=
In[9]:=
Click for copyable input
Out[9]=

"OpenManagerFunction"

"OpenManagerFunction"f specifies that 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 DeviceOpen["class",{args}]. The return value will be passed to "ReleaseFunction" in the deinitialization stage at the end of the device life cycle. Use DeviceInitObject to retrieve the initialization object at any other time before the device is closed.

You may wish to specify , for instance, to open a WSTP connection to an external program. In that case, the function would return a LinkObject, which you can close with LinkClose in your "ReleaseFunction".

This driver keeps a count of open devices in the class.
In[1]:=
Click for copyable input
The counter is incremented (decremented) when a device is opened (closed).
In[2]:=
Click for copyable input
In[3]:=
Click for copyable input
Out[3]=
In[4]:=
Click for copyable input
Out[4]=
In[5]:=
Click for copyable input
Out[5]=

"MakeManagerHandleFunction"

You can create a separate handle to the initialization object (a manager handle) by specifying "MakeManagerHandleFunction"f. The function will be called after "OpenManagerFunction" and supplied the initialization object obj created by "OpenManagerFunction". The return value is assumed to be the manager handle. In the absence of , 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 DeviceManagerHandle.

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

"OpenFunction"

The function specified with "OpenFunction"f is the main function called by the framework in response to a user call to DeviceOpen. receives the manager handle ihandle created by "MakeManagerHandleFunction" and the arguments args the user supplies to 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 DeviceHandle.

The framework attempts to maintain a one-to-one correspondence between the device handle returned by and the top-level DeviceObject returned by 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 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.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
In[3]:=
Click for copyable input
Out[3]=
Retrieve the device handle and use it to get back the device object.
In[4]:=
Click for copyable input
Out[4]=
In[5]:=
Click for copyable input
Out[5]=

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

This driver does not implement .
In[6]:=
Click for copyable input
In[7]:=
Click for copyable input
Out[7]=
You can still use the default device handle provided by the framework.
In[8]:=
Click for copyable input
Out[8]=

"OpenReadFunction"

"OpenReadFunction"f specifies that should be called during the device opening sequence, after "OpenFunction", to open any input streams associated with a device. The first argument , 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 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.
In[1]:=
Click for copyable input

"OpenWriteFunction"

"OpenWriteFunction"f specifies that should be called during the device opening sequence, after "OpenFunction", to open any output streams associated with a device. The first argument , 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 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.
In[1]:=
Click for copyable input

"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, is guaranteed to be called at the end of a successful call to 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). must return a list of properties that has been configured by the driver, by itself, "OpenFunction", or any other function in the device opening sequence. These properties will not be changed further by the framework until DeviceOpen finishes. All or None can also be returned.

This driver sets a device property based on the device ID.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Out[2]=
In[3]:=
Click for copyable input
Out[3]=

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.
In[4]:=
Click for copyable input
In[5]:=
Click for copyable input
Out[5]=

"ConfigureFunction"

"ConfigureFunction"f specifies that should be called in response to the top-level command DeviceConfigure[dev,{args}] to configure a device. The first argument , 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.

in this driver sets a top-level property.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Open a device and configure it by calling DeviceConfigure.
In[3]:=
Click for copyable input
Out[3]=
In[4]:=
Click for copyable input
Out[4]=
This is the new value of the property.
In[5]:=
Click for copyable input
Out[5]=

"ReadFunction"

"ReadFunction"f specifies that should be called in response to the top-level command DeviceRead[dev,{args}] to read data from a device. The first argument , 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 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 "OpenFunction" and removed in "CloseFunction".
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
In[3]:=
Click for copyable input
In[4]:=
Click for copyable input
In[5]:=
Click for copyable input
In[6]:=
Click for copyable input
In[7]:=
Click for copyable input
Open a device and read a character from it.
In[8]:=
Click for copyable input
Out[8]=
In[9]:=
Click for copyable input
Out[9]=
Open another device and read several characters.
In[10]:=
Click for copyable input
Out[10]=
In[11]:=
Click for copyable input
Out[11]=
The current read counters are remembered for each open device.
In[12]:=
Click for copyable input
Out[12]=
Reading from the first device again gives the next characters, until the device reads the string completely.
In[13]:=
Click for copyable input
Out[13]=
Closing a device clears its counter.
In[14]:=
Click for copyable input
In[15]:=
Click for copyable input
Out[15]=

Your implementation of 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 DeviceFramework`Devices` context.

This driver lets devices read either real or integer random values and implements a rudimentary argument checking.
In[16]:=
Click for copyable input
In[17]:=
Click for copyable input
In[18]:=
Click for copyable input
In[19]:=
Click for copyable input
In[20]:=
Click for copyable input
In[21]:=
Click for copyable input
In[22]:=
Click for copyable input
In[23]:=
Click for copyable input
Open a device and call DeviceRead with legal arguments.
In[24]:=
Click for copyable input
Out[24]=
In[25]:=
Click for copyable input
Out[25]=
In[26]:=
Click for copyable input
Out[26]=
Calling DeviceRead with illegal arguments triggers errors.
In[27]:=
Click for copyable input
Out[27]=
In[28]:=
Click for copyable input
Out[28]=
In[29]:=
Click for copyable input
Out[29]=

"ReadBufferFunction"

"ReadBufferFunction"f specifies that should be called in response to the top-level command DeviceReadBuffer[dev,args] to read data from a device buffer. The first argument , 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:

DeviceReadBuffer[dev]f[{ihandle,dhandle},Automatic]
DeviceReadBuffer[dev,crit]f[{ihandle,dhandle},crit,Automatic]
DeviceReadBuffer[dev,crit,params]

The possible signatures of .

The return value of f will be passed on to the user as an output of the DeviceReadBuffer[] command.

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

"WriteFunction"

"WriteFunction"f specifies that should be called in response to the top-level command DeviceWrite[dev,{args}] to write data to a device. The first argument , 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 DeviceWrite.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Open a device and execute a write operation.
In[3]:=
Click for copyable input
Out[3]=
In[4]:=
Click for copyable input
Out[4]=

If your device writes values of several parameters, it is common to supply their values to DeviceWrite as an association or a set of rules. The job of parsing such rules then falls on your implementation of .

Parse rules in .
In[5]:=
Click for copyable input
In[6]:=
Click for copyable input
Open a device and execute a write operation to create a new notebook with the specified text and title and a blank section cell.
In[7]:=
Click for copyable input
Out[7]=
In[8]:=
Click for copyable input
Out[8]=

"WriteBufferFunction"

"WriteBufferFunction"f specifies that should be called in response to the top-level command DeviceWriteBuffer[dev,{args}] to write data to a device buffer. The first argument , 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 is writing a sequence of bytes to a serial device.

"ExecuteFunction"

"ExecuteFunction"f specifies that should be called in response to the top-level command DeviceExecute[dev,"command",{args}] to execute a command on a device. The first argument , 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 DeviceExecute[] command.

"ExecuteAsynchronousFunction"

"ExecuteAsynchronousFunction"f specifies that should be called in response to the top-level command DeviceExecuteAsynchronous[dev,"command",{args},fun] to start an asynchronous execution of the specified command on a device. The first argument , 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 AsynchronousTaskObject[] or a similar object, will be passed on to the user as an output of the DeviceExecuteAsynchronous[] command.

If command is not supported, the function f must yield a 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 $Failed. In particular, this should happen if the specified command cannot be executed with the given arguments.

command can be , , , , or any other commands that are supported by the driver.

This driver asynchronously saves the content of a URL to a file.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
In[3]:=
Click for copyable input
Out[3]=
Prepare a progress function and a progress indicator.
In[4]:=
Click for copyable input
In[5]:=
Click for copyable input
In[6]:=
Click for copyable input
Out[6]=
Start an asynchronous task and observe the progress.
In[7]:=
Click for copyable input
In[9]:=
Click for copyable input
Out[9]=
The framework issues an error message for unsupported asynchronous operations.
In[10]:=
Click for copyable input
Out[10]=

"CloseFunction"

"CloseFunction"f specifies that should be called in response to the top-level command DeviceClose[dev] to close a device and free related resources. The first argument , 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 DeviceClose[] command. On successful completion, it is expected to be Null.

The implementation of would typically involve closing all ports and sockets and releasing other resources, with the exception of those that are closed in "ReleaseFunction". Streams opened in "OpenReadFunction" and "OpenWriteFunction" are automatically closed by the framework, provided that they can be closed using Close. If that is not the case, you should close the streams in .

A closed device remains available in the current Wolfram Language session. Use "DeregisterOnClose" to completely remove the device after it is closed.

"ReleaseFunction"

"ReleaseFunction"f specifies that should be called after "CloseFunction" during the operation of DeviceClose to destroy the initialization object obj created by "OpenManagerFunction" and perform any other remaining cleanup tasks if necessary. The return value of this function is ignored, unless it is $Failed.

If you used "OpenManagerFunction" to open a WSTP connection to an external program, you could close the connection in .

"DeregisterOnClose"

"DeregisterOnClose"True specifies that DeviceClose should not only release all external resources, but also completely remove the device from the current Wolfram Language session. By default, with "DeregisterOnClose"False, the device remains available and can be reopened with exactly the same parameters.

With the default value of the option, a closed device remains available.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Out[2]=
In[3]:=
Click for copyable input
Out[3]=
The framework keeps track of the parameters used to open the device.
In[4]:=
Click for copyable input
Out[4]=
You can reopen the device without specifying open arguments.
In[5]:=
Click for copyable input
Out[5]=
Devices of this class are automatically deregistered after they are closed.
In[6]:=
Click for copyable input
In[7]:=
Click for copyable input
Out[7]=
In[8]:=
Click for copyable input
Out[8]=

"Singleton"

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

Automaticreturn a new device for a new set of arguments
Truealways return the same device
Falsealways return a new device
critreturn the first of the registered devices for which evaluates to True

Creation policy for multiple devices in a class.

The default value Automatic is equivalent to the criterion crit equal to SameQ[DeviceOpenArguments[#1],#2]&.

With "Singleton"Automatic, DeviceOpen returns a new device only if called with a new set of arguments.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Out[2]=
In[3]:=
Click for copyable input
Out[3]=
Calling DeviceOpen with the same arguments gives the same device.
In[4]:=
Click for copyable input
Out[4]=
With "Singleton"True, the framework ignores all calls to DeviceOpen after the first one and returns the same device even if called with different arguments.
In[5]:=
Click for copyable input
In[6]:=
Click for copyable input
Out[6]=
In[7]:=
Click for copyable input
Out[7]=
With "Singleton"False, DeviceOpen returns a new device even if called with the same arguments.
In[8]:=
Click for copyable input
In[9]:=
Click for copyable input
Out[9]=
In[10]:=
Click for copyable input
Out[10]=

Importantly, the framework will not typically execute the opening sequence of driver functions, including "OpenFunction", if it does not need to open a new device in response to DeviceOpen. However, it will execute the opening sequence if the specified criterion crit returns 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, DeviceOpen returns a new device for every distinct first argument.
In[11]:=
Click for copyable input
In[12]:=
Click for copyable input
Out[12]=
In[13]:=
Click for copyable input
Out[13]=
Reconfigure the first device with a different input argument.
In[14]:=
Click for copyable input
Out[14]=
This demo device has a slightly more robust implementation of the option.
In[15]:=
Click for copyable input

"Properties"

The option 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 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 is DeviceGetProperty, which you can call inside your function f.

This driver keeps track of how many times a standardized property is read.
In[1]:=
Click for copyable input
Set up the counter and open a device.
In[2]:=
Click for copyable input
Out[2]=
In[3]:=
Click for copyable input
Out[3]=
Reading the property value increments the counter.
In[4]:=
Click for copyable input
Out[4]=
In[5]:=
Click for copyable input
Out[5]=

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

"SetPropertyFunction"

"SetPropertyFunction"f specifies that 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 is DeviceSetProperty, which you can call inside your function f.

Although you will typically use 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 and a read-only property .
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
In[3]:=
Click for copyable input
In[4]:=
Click for copyable input
Open a device and change the property .
In[5]:=
Click for copyable input
Out[5]=
In[6]:=
Click for copyable input
Out[6]=
The value of cannot be changed.
In[7]:=
Click for copyable input
Out[7]=

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 . In a real-life driver, you would instead call the external property setter (getter) to change (get the value of) the property.
In[1]:=
Click for copyable input
The 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.
In[2]:=
Click for copyable input
For an open device, the 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.
In[3]:=
Click for copyable input
This sets up the driver. "PreconfigureFunction" in this case tells the driver to skip the configuration of the property .
In[5]:=
Click for copyable input
Open a device and check its property. The value comes from the variable (that is, the value is ignored).
In[6]:=
Click for copyable input
Out[6]=
In[7]:=
Click for copyable input
Out[7]=
Change the property value. The "external" variable also changes.
In[8]:=
Click for copyable input
In[9]:=
Click for copyable input
Out[9]=
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.
In[10]:=
Click for copyable input
In[11]:=
Click for copyable input
Out[11]=
Close the device and change the external property on the device again.
In[12]:=
Click for copyable input
In[13]:=
Click for copyable input
Because there is no communication with the device, the reported value of the property is a stale one.
In[14]:=
Click for copyable input
Out[14]=
The value is automatically updated when the device is reopened.
In[15]:=
Click for copyable input
Out[15]=
In[16]:=
Click for copyable input
Out[16]=

"NativeProperties"

The option 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 DeviceOpen, DeviceRead, 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.
In[1]:=
Click for copyable input

"GetNativePropertyFunction"

"GetNativePropertyFunction"f specifies that 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 Automatic is essentially equivalent to Function[{dhandle,p},dhandle@p].

This demo device provides a sample implementation of .
In[1]:=
Click for copyable input

"SetNativePropertyFunction"

"SetNativePropertyFunction"f specifies that 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 Automatic is essentially equivalent to Function[{dhandle,p,val},dhandle@p=val].

This demo device provides a sample implementation of .
In[2]:=
Click for copyable input

"StatusLabelFunction"

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

This driver implements custom status labels.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Out[2]=
In[3]:=
Click for copyable input
Out[3]=

To construct more sophisticated labels, call DeviceStatusLabels in "PreconfigureFunction".

"DeviceIconFunction"

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

In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Out[2]=

"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 DeviceDriverVersion.

Specify a driver version and retrieve it.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Out[2]=

Device Framework Functions

In addition to DeviceClassRegister, 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.

DeviceFramework`DeviceClassRegister[class,]
register the specified class
DeviceFramework`DeviceClassClear[class]
clear definitions for the class

Adding and removing a driver class.

Internally, DeviceClassRegister calls DeviceClassClear and effectively removes all existing definitions for the specified class before creating any new ones, so you can safely call DeviceClassRegister multiple times when developing your driver.

Working with Driver Files

DeviceFramework`FindDeviceDrivers[form]
give a list of drivers for classes whose names match the string pattern form
DeviceFramework`DeviceDriverLoad[class]
discover and load a driver for the specified class
DeviceFramework`DeviceDriverFile[dev | class]
give a path to the currently registered driver for the device dev or class class
DeviceFramework`DeviceDriverPaclet[dev | class]
give a paclet object for the currently loaded paclet driver for the device dev or class class
DeviceFramework`DeviceDriverVersion[dev | class]
return the version number of the currently loaded driver for the device dev or class class

Working with driver files.

FindDeviceDrivers returns a list of triplets in the form . 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 DeviceOpen.

This opens all available driver files for a class in your system editor.
In[1]:=
Click for copyable input
Out[1]=
In[2]:=
Click for copyable input

For already loaded drivers, you can find out exactly which file is loaded using DeviceDriverFile. 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.
In[3]:=
Click for copyable input
Out[3]=
In[4]:=
Click for copyable input
Out[4]=
Execute the following command to open the driver file as a notebook in your Wolfram Language session.
In[5]:=
Click for copyable input
Out[5]=

Accessing User-Visible Information about a Device

DeviceFramework`DeviceClass[dev]return the class of the device dev
DeviceFramework`DeviceID[dev]return the ID of the device dev
DeviceFramework`DeviceStatusLabels[dev],DeviceFramework`DeviceStatusLabels[dev]={olbl,clbl}
get and set status labels for the open and closed state of the device dev

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 "PreconfigureFunction" before the device is presented to the user for the first time.

Change the built-in status labels for a demo device.
In[1]:=
Click for copyable input
Out[1]=
In[2]:=
Click for copyable input
In[3]:=
Click for copyable input
Out[3]=
In[4]:=
Click for copyable input

Accessing Internal Information about a Device

DeviceFramework`DeviceInitObject[dev]
return the initialization object for the device dev
DeviceFramework`DeviceManagerHandle[dev]
return the handle to the initialization object for the device dev
DeviceFramework`DeviceHandle[dev]return the handle to the device dev
DeviceFramework`DeviceObjectFromHandle[h]
return the device object for the handle h
DeviceFramework`DeviceOpenArguments[dev]
return the arguments with which the device dev was opened
DeviceFramework`DeviceDriverOption[class,"opt"],DeviceFramework`DeviceDriverOption[class,"opt"]=val
get and set the value of a device driver option for the specified class

Accessing internal objects kept by the framework.

Although DeviceDriverOption 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 and calls the parent's "ReadFunction" inside its own.
In[1]:=
Click for copyable input
Reading from the device gives a list of accumulated random values, whereas reading from the parent gives independent values.
In[2]:=
Click for copyable input
In[3]:=
Click for copyable input
Out[3]=
In[4]:=
Click for copyable input
Out[4]=

DeviceDriverOption 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 DeviceRead.
In[5]:=
Click for copyable input
In[6]:=
Click for copyable input
Out[6]=
This changes the driver to read off a sawtooth wave.
In[7]:=
Click for copyable input
In[8]:=
Click for copyable input
Out[8]=

Class Properties

When the framework needs to assign standardized properties in a particular instance of DeviceObject, it takes the values from class properties. Class properties are specified by the "Properties" options of DeviceClassRegister and can be accessed at any time after the driver is loaded.

DeviceFramework`DeviceClassProperties[class]
return a list of available properties for the specified class
DeviceFramework`DeviceClassProperties[class,prop]
return the value of the a property prop for the specified class
DeviceFramework`DeviceClassProperties[class,{p1,p2,}]
return the values of multiple properties
DeviceFramework`DeviceClassProperties[class,prop]=val,DeviceFramework`DeviceClassProperties[class,{p1,p2,}]={v1,v2,}
set the values of the the specified properties

Accessing class properties.

Class properties are available even before any device objects are created for the class.
In[1]:=
Click for copyable input
In[2]:=
Click for copyable input
Out[2]=
In[3]:=
Click for copyable input
Out[3]=
Changing device properties does not alter class properties.
In[4]:=
Click for copyable input
Out[4]=
In[5]:=
Click for copyable input
In[6]:=
Click for copyable input
Out[6]=
Conversely, changing class properties does not reset properties of the existing devices, but it does alter properties of new devices in the class.
In[7]:=
Click for copyable input
In[8]:=
Click for copyable input
Out[8]=
In[9]:=
Click for copyable input
Out[9]=
In[10]:=
Click for copyable input
Out[10]=

Class Preferences

Class preferences, assessed through , 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 before, after, or without ever loading the driver; and that call does not load the driver.

DeviceFramework`DeviceClassPreferences[class]
return an association of available preferences for the specified class
DeviceFramework`DeviceClassPreferences[class,pref]
return the value of the preference pref for the specified class
DeviceFramework`DeviceClassPreferences[class,{p1,p2,}]
return the values of multiple preferences
DeviceFramework`DeviceClassPreferences[class,pref]=val,DeviceFramework`DeviceClassPreferences[class,{p1,p2,}]={v1,v2,}
set the values of the specified preferences
DeviceFramework`DeviceClassPreferences[class]=assoc
assign the class preferences to assoc
DeviceFramework`DeviceClassPreferences[class]=.,DeviceFramework`DeviceClassPreferences[class,pref]=.,DeviceFramework`DeviceClassPreferences[class,{p1,p2,}]=.
clear class preferences or remove the specified keys

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 DeviceClassRegister. If the preferences happen to contain keys corresponding to options of DeviceClassRegister, their values take precedence over options specified in the driver.

Save the "Properties" preferences for a class.
In[1]:=
Click for copyable input
Out[1]=
When a driver is being created, the value from its preferences is used instead of the value prescribed by the DeviceClassRegister statement, which, in this case, calls for simply copying properties from the parent class.
In[2]:=
Click for copyable input
In[3]:=
Click for copyable input
Out[3]=
In[4]:=
Click for copyable input
Out[4]=
In[5]:=
Click for copyable input
Clear preferences and reevaluate the class definition.
In[6]:=
Click for copyable input
In[7]:=
Click for copyable input
In the absence of preferences, this class's properties are derived from the parent class, as requested.
In[8]:=
Click for copyable input
Out[8]=
In[9]:=
Click for copyable input
Out[9]=

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.
In[10]:=
Click for copyable input
In[11]:=
Click for copyable input
Out[11]=
After setting the flag, a device can be found.
In[12]:=
Click for copyable input
In[13]:=
Click for copyable input
Out[13]=
Remove the preferences for the class.
In[14]:=
Click for copyable input

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.

DeviceFramework`DeviceGetProperty[dev,prop]
execute the default handler for "GetPropertyFunction"
DeviceFramework`DeviceSetProperty[dev,prop,val]
execute the default handler for "SetPropertyFunction"
DeviceFramework`DeviceDefaultStatusLabels[]
give a list of the default open and closed status labels
DeviceFramework`DeviceDefaultStatusLabels[p]
give the default status labels based on the parameter p

Using the default property handlers.

Determining Device State

The top-level functions DeviceOpen, DeviceClose, , and Devices respectively open, close, test, and give a list of all devices registered in a current sessionwhether a device is open or closed. The framework also provides several complementing functions for analogous, but less common, operations.

DeviceFramework`OpenDevices[]list of currently open devices
DeviceFramework`DeviceRegisteredQ[dev]
test whether the device dev is registered in the current Wolfram Langauage session
DeviceFramework`DeviceDeregister[dev]
deregister and completely remove the device dev from the current session

Testing and changing the device state.

DeviceDeregister 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.
In[1]:=
Click for copyable input
Out[1]=

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 PacletInfo.m 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.
In[1]:=
Click for copyable input
Out[1]=
The documentation includes a custom reference page with troubleshooting instructions. Click the chevron below to see the instructions.
In[2]:=
Click for copyable input
Out[2]=
To inspect the contents of the paclet, execute the following command.
In[3]:=
Click for copyable input

Examples

Tinker Forge Weather Station

Tinker Forge Weather Station 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.

123.gif

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 bindings 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 Install that executable, which can be conveniently done in "OpenManagerFunction". 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 Install statement in "OpenManagerFunction" normally returns a LinkObject on which LinkClose is invoked in "ReleaseFunction" to close the WSTP connection at the deinitialization stage.

release[link_] := LinkClose[link]

Next, an implementation of the "MakeManagerHandleFunction" 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, , serves as the device manager handle ihandle and is passed to "OpenFunction" 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 "ReadFunction", whereas the manager handle ihandle is not needed there.

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

An implementation of "WriteFunction", on the other hand, may not need sensor handles, but could create a handle to the LCD bricklet from on the fly.

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

Finally, the implementation of "CloseFunction" 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.
In[1]:=
Click for copyable input

Further Info

Please contact Wolfram Research if you are interested in developing and distributing a device driver for the Wolfram Language.