Developing Device Drivers
Overview | Developing Device Driver Paclets |
Device Driver Options | Examples |
Device Framework Functions | Further Info |
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.
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 |
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.
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 |
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 |
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[]
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:
- in the current evaluation notebook directory (NotebookDirectory)
- in a special directory FileNameJoin[{$InstallationDirectory,"SystemFiles","Devices","DeviceDrivers"}] on your computer system
- anywhere on $Path
- on a paclet server or in a driver paclet directory (see "Developing Device Driver Paclets")
A driver can be used even if it does not follow these conventions, but then it must be explicitly loaded in a Wolfram Language session, for instance, with Get.
To make a driver available on a specific platform, you can wrap a DeviceClassRegister[] statement in an If.
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.
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.
"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".
"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.
If "OpenFunction" is omitted, the framework assigns a random device handle to any new device in the class.
"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.
"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.
"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.
"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.
Open a device and configure it by calling DeviceConfigure:
"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":
Reading from the first device again gives the next characters, until the device reads the string completely:
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]
|
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:
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".
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.
"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.
"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.
"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 |
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:
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:
"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.
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.
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):
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:
Because there is no communication with the device, the reported value of the property is a stale one:
"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.
"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].
"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].
"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.
"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.
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 |
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 |
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.
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.
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 |
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.
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 |
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:
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 |
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 |
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:
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.
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 |
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 session—whether 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 |
DeviceDeregister also closes a device if it is open. This function can be useful for cleaning slate when developing a driver.
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 documentation includes a custom reference page with troubleshooting instructions. Click the chevron following to see the instructions:
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.
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.
Please contact Wolfram Research if you are interested in developing and distributing a device driver for the Wolfram Language.