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 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["class",opts] creates a typical driver and the two-argument form DeviceClassRegister["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:
Extend the parent class with a write operation:
Devices of the child class can read and write:
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"None
the function to execute when opening a device
"ConfigureFunction"None
how to configure a device
"ReadFunction"None
how to read from a device
"ReadBufferFunction"None
how to read from a device buffer
"WriteFunction"None
how to write to a device
"WriteBufferFunction"None
how to write to a device buffer
"ExecuteFunction"None
how to execute a command on a device
"ExecuteAsynchronousFunction"None
how to execute a command asyncronously
"CloseFunction"None
the function to execute when closing a device
"Properties"{}
standardized properties
"DeviceIconFunction"None
how 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"None
the function to execute to open a device manager
"MakeManagerHandleFunction"Identity
how 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"Automatic
the handler for DeviceReadTimeSeries
"StatusLabelFunction"Automatic
how to create the status labels for the device object
"NativeProperties"{}
native properties
"NativeMethods"{}
native methods
"GetPropertyFunction"DeviceFramework`DeviceGetProperty
how to get standardized properties
"SetPropertyFunction"DeviceFramework`DeviceSetProperty
how to set standardized properties
"GetNativePropertyFunction"Automatic
how to get native properties
"SetNativePropertyFunction"Automatic
how to set native properties
"NativeIDFunction"None
how to get the native ID of a device
"OpenReadFunction"None
how to open input streams assoociated with the device
"OpenWriteFunction"None
how to open output streams assoociated with the device
"DeregisterOnClose"False
whether to deregister a device after it is closed
"Singleton"Automatic
creation 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:
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 {ihandle, dhandle}, 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 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 following.
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 f[handles,param]; for DeviceRead[dev,{param1,param2,}], the function will be called as f[handles,param1,param2,]; for 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 $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:
You can open a dummy device, but attempting to execute any specific operation on it will result in error:
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 following 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":

"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 FindDevices[class] to find devices of the given class. The function f receives no arguments and should return an output in the form {{arg11,arg12,},{arg21,arg22,},}, where {argi1,argi2,} 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 "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:
You must open a device of such a class before it can be used:
This driver extends the previous one by supplying the "FindFunction" option:
Devices of this class can be opened automatically by the framework:
Devices remain open after performing the required operation until they are explicitly closed:

"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 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 "OpenManagerFunction", 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:
The counter is incremented (decremented) when a device is opened (closed):

"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 "OpenManagerFunction" and supplied the initialization object obj created by "OpenManagerFunction". 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 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. f[ihandle,args] 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 "OpenFunction" 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:
Retrieve the device handle and use it to get back the device object:
If "OpenFunction" is omitted, the framework assigns a random device handle to any new device in the class.
This driver does not implement "OpenFunction":
You can still use the default device handle provided by the framework:

"OpenReadFunction"

"OpenReadFunction"f specifies that f[{ihandle,dhandle},args] should be called during the device opening sequence, after "OpenFunction", 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 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:

"OpenWriteFunction"

"OpenWriteFunction"f specifies that f[{ihandle,dhandle},args] should be called during the device opening sequence, after "OpenFunction", 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 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:

"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 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, "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 "parity" based on the device ID:
"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:

"ConfigureFunction"

"ConfigureFunction"f specifies that f[{ihandle,dhandle},args] should be called in response to the top-level command 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:
Open a device and configure it by calling DeviceConfigure:
This is the new value of the property:

"ReadFunction"

"ReadFunction"f specifies that f[{ihandle,dhandle},args] should be called in response to the top-level command 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 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":
Open a device and read a character from it:
Open another device and read several characters:
The current read counters are remembered for each open device:
Reading from the first device again gives the next characters, until the device reads the string completely:
Closing a device clears its counter:
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 DeviceFramework`Devices` context.
This driver lets devices read either real or integer random values and implements a rudimentary argument checking:
Open a device and call DeviceRead with legal arguments:
Calling DeviceRead with illegal arguments triggers errors:

"ReadBufferFunction"

"ReadBufferFunction"f specifies that f[{ihandle,dhandle},] should be called in response to the top-level command 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:
DeviceReadBuffer[dev]
f[{ihandle,dhandle},Automatic]
DeviceReadBuffer[dev,crit]
f[{ihandle,dhandle},crit,Automatic]
DeviceReadBuffer[dev,crit,params]
f[{ihandle,dhandle},crit,params]
The possible signatures of "ReadBufferFunction".
The return value of f will be passed on to the user as an output of the 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 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 DeviceWrite:
Open a device and execute a write operation:
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 "WriteFunction".
Parse rules in "WriteFunction":
Open a device and execute a write operation to create a new notebook with the specified text and title and a blank section cell:

"WriteBufferFunction"

"WriteBufferFunction"f specifies that f[{ihandle,dhandle},args] should be called in response to the top-level command 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 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 DeviceExecute[] command.

"ExecuteAsynchronousFunction"

"ExecuteAsynchronousFunction"f specifies that f[{ihandle,dhandle},"command",args,fun] 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 {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 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 "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:
Prepare a progress function and a progress indicator:
Start an asynchronous task and observe the progress:
The framework issues an error message for unsupported asynchronous operations:

"CloseFunction"

"CloseFunction"f specifies that f[{ihandle,dhandle}] should be called in response to the top-level command 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 DeviceClose[] command. On successful completion, it is expected to be 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 "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 "CloseFunction".
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 f[obj] 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 "ReleaseFunction".

"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 "DeregisterOnClose" option, a closed device remains available:
The framework keeps track of the parameters used to open the device:
You can reopen the device without specifying open arguments:
Devices of this class are automatically deregistered after they are closed:

"Singleton"

The "Singleton" option determines how the framework processes the DeviceOpen["class",{args}] command, thereby setting the creation policy for multiple devices. Possible values of the "Singleton" option are as follows:
Automatic
return a new device for a new set of arguments
True
always return the same device
False
always return a new device
crit
return the first of the registered devices for which crit[dev,{args}] 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:
Calling DeviceOpen with the same arguments gives the same device:
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:
With "Singleton"False, DeviceOpen returns a new device even if called with the same arguments:
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:
Reconfigure the first device with a different input argument:
This demo device has a slightly more robust implementation of the "Singleton" option:

"Properties"

The option "Properties"{"property1"value1,"property2"value2,} 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 DeviceGetProperty, which you can call inside your function f.
This driver prints a time stamp when a standardized property is read:
Open a device and read its property:
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 DeviceSetProperty, 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":
Open a device and change the property "p":
The value of "r" cannot be changed:

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:
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:
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:
This sets up the driver. "PreconfigureFunction" in this case tells the driver to skip the configuration of the property "p":
Open a device and check its property. The value comes from the variable $external (that is, the value "x" is ignored):
Change the property value. The "external" variable also changes:
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:
Close the device and change the external property on the device again:
Because there is no communication with the device, the reported value of the property is a stale one:
The value is automatically updated when the device is reopened:

"NativeProperties"

The option "NativeProperties"{property1,property2,} 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:

"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 Automatic is essentially equivalent to Function[{dhandle,p},dhandle@p].
This demo device provides a sample implementation of "GetNativePropertyFunction":

"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 Automatic is essentially equivalent to Function[{dhandle,p,val},dhandle@p=val].
This demo device provides a sample implementation of "SetNativePropertyFunction":

"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 f[{args}] 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 {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:
To construct more sophisticated labels, call DeviceStatusLabels in "PreconfigureFunction".

"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 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 Graphics object.

"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:
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 {"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 DeviceOpen.
Find all available drivers for a class and open the corresponding files in your system editor:
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:
Execute the following command to open the driver file as a notebook in your Wolfram Language session:

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:

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 "opt" 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 "RandomSignalDemo" and calls the parent's "ReadFunction" inside its own:
Reading from the device gives a list of accumulated random values, whereas reading from the parent gives independent values:
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:
This changes the driver to read off a sawtooth wave:

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:
Changing device properties does not alter class properties:
Conversely, changing class properties does not reset properties of the existing devices, but it does alter properties of new devices in the class:

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.
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:
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:
Clear preferences and reevaluate the class definition:
In the absence of preferences, this class's properties are derived from the parent class, as requested:
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:
After setting the flag, a device can be found:
Remove the preferences for the class:

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, DeviceOpenQ, 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:
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:
The documentation includes a custom reference page with troubleshooting instructions. Click the chevron following to see the instructions:
To inspect the contents of the paclet, execute the following command:
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.

18.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, ipconn, 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 ipconn 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:
Further Info
Please contact Wolfram Research if you are interested in developing and distributing a device driver for the Wolfram Language.