General driver development
The local and remote nodes support devices implemented via general drivers.
These drivers have a .drv extension and may be placed in a directory under
the domotcl drivers directory.
Common structure
The code in a driver is executed in different stages. When writing a driver,
care should be taken that only actions necessary for the applicable stage
are performed.
- Definition
- Domotcl will initially source the driver file to obtain the information
necessary to present to a user for configuring the driver parameters.
The code should only do the minimal amount of work necessary to define the
driver parameters and the supported device classes. The driver should not
yet load support packages it may need for its operation, schedule events, or
start any kind of program loop. To enforce this, domotcl puts restrictions
on the number of commands that may be executed in this stage. It also
prohibits certain actions, like entering the event loop.
- Driver initialization
- The initialization stage is intended for performing any initial setup
for actually running the driver. This is the time to load support packages,
set global variables to their initial values and initiate the connection to
the hardware the driver interfaces with.
- Device initialization
- During device initialization, the defined devices are created one by one
by running their constructor code. Subdevices are always created after their
parents.
- Normal execution
- After the initialization stages are complete, the driver starts its
normal operation. Any code executed during the normal operation of a driver
should not be blocking the application for significant amounts of time.
Simple drivers may use the init command for the definition and
driver initialization stages, as well as cleanup when the driver is
terminated.
The init command takes two or three arguments: a list of driver
parameters, the code body, and optional cleanup code.
Example:
init {busname number:q} {
global bus num
package require dbus
set bus [dbus connect $busname]
set num $number
} {
# Stop any running timers
foreach id [after info] {after cancel $id}
}
More complex drivers will want to use the connect module for driver
definition and initialization.
Drivers should normally use the device module for device definition and
initialization.
See the relevant sections below for a detailed description of these modules.
The main command is available if a driver needs to run some
code to start its normal operation. This command only takes a code body
argument.
Example:
main {
coroutine startup try {
global bus num
while 1 {
getvalues $bus $num
after 30000 [list [info coroutine]]
yield
}
} on error {msg opts} {
alert [dict get $opts -errorinfo]
}
}
config
config {*}[dict map {key val} [config] {}]
make type name description args
Connect module
Many drivers will need to connect to some piece of equipment to perform their
function.
The connect module simplifies the use of such connections by taking care of
setting up the connection, monitoring, recovering dropped connections, and
communicating over the connection. The connect module is included in a driver
through the command:
use connect
A connection is defined using connection arguments. The first argument is
the connection type. The remaining arguments depend on the value of the
connection type. There are currently two connection types supported:
- socket host port
- Create a connection using a TCP socket.
- comport device baudrate
- Create a connection using an RS232 serial device.
Defining a connection
The connect module provides a command to be used during the driver
definition stage:
- definition initcmd [name default ...]
- Build a driver definition. The initcmd argument specifies a
command prefix to be invoked for driver initialization. Any following
name default pairs specify default values for the connection
arguments.
During driver initialization, the configured connection arguments are
appended to the command prefix, which is then executed.
Creating a connection
The connect module provides a connection class. Instances of the class can
be created to set up, maintain, and re-establish connections.
A new connection can be created with a specified name, or an auto-generated
name:
- connection new type arg ... [option
value ...]
- Create a connection with a new unique auto-generated name
- connection create name type arg ...
[option value ...]
- Create a connection with the specified name.
Both commands expect connection parameters consisting of a connection type
followed by the applicable arguments for that connection type. These may be
follwed by additional option value pairs to configure the
connection.
The following options are available:
- -blocking boolean
- -buffering {none|line|full}
- -buffersize size
- -encoding name
- -eofchar spec
- -translation spec
Both commands return the name of the connection object, which can be used to
transfer data over the connection.
Connection methods
- callback prefix
- Configure a command to be invoked whenever data is received.
- chat send expect ...
- Run the specified chat script.
- check send expect ...
- Define a chat script to be used to check the connected device. This
script will be executed whenever a chat script fails. If the check script
also fails, recovery actions will be performed.
- destroy
- Close the connection
- fd
- Returns the channel descriptor, or an empty string if the connection is
currently down.
- filter prefix
- Configure a command to manipulate the received data.
- login send expect ...
- Define a chat script to be executed every time after reconnecting.
- output data
- Transmit data over the connection
- read [count] [timeout]
- Receive data.
- verify prefix
- Configure a command to be invoked just before data is transmitted in a
chat script. The command is invoked with the "send"-string and
"expect"-string as additional arguments. If the command returns 0, the
send/expect pair is skipped.
- stateevent prefix
- Configure a command to be invoked whenever the state of the connection
changes. At such a time, the command prefix is invoked with two additional
arguments appended; the connection name and the event, which is either
connect or disconnect.
Chat scripts
A chat script defines a conversational exchange between the driver and the
connected device. The script consists of one or more "send-expect" pairs of
strings. The script may also contain directives. The chat engine will send
each "send"-string to the device and then waits for the "expect"-string.
The following directives are available:
- LITERAL
- The following string is a literal "send"-string. This should be used to
send strings that look like a directive.
- TIMEOUT ms
- Set the time to wait for a response. Default is 1000.
- RETRY count
- Set the number of times to repeat a "send"-string until the associated
"expect"-string is received. Default is 3.
- MATCH {exact|glob|regexp [-nocase]}
- Set the method used to match the "expect"-string against the received
data. Default is "exact".
- DELAY ms
- Pause some time before transmitting the next "send"-string.
- NEWLINE
- Send a newline sequence. The exact characters being sent depend on the
channel configuration.
Additional commands
- connect type arg ...
- Convenience procedure that creates a connection instance called fd.
- usbreset device
- USB devices may sometimes lock up. When the driver detects this
situation, this command can be used to reset the USB interface. This command
needs the binary command /usr/local/sbin/usbreset to be available.
Device module
The device module can be used to facilitate writing a driver for a class of
devices. The module is automatically loaded if one of the commands
device, or foundation is used.
The module can also be loaded explicitly through the command:
use device
The module provides the commands: "device", and "foundation".
Device type implementation
The device create command is used to define a device type. The
command takes two arguments: a device type name and the definition block.
The device type name must be unique within the driver and should normally be
short and describe the function of the device type. This name will be
presented to the user when creating a new device.
The definition block specifies various features of the device type. It is
normally enclosed in braces. The keywords used to specify the features are
described below. All keywords are optional.
- constructor
-
The constructor keyword specifies the name and type of parameters of a
device, the code to initialize a new device, and the code to configure the
device.
More extensive specification of the parameters of a device can be done with
the typedef keyword described later.
Both code blocks are executed for every new device of the current device
type. Only the second code block gets executed when a device is reconfigured.
Example:
constructor {address:i} {
variable value 0
} {
if {$address < 0} {
error "address must not be negative"
}
my UID $address
}
The device type in the example has one integer parameter called address. The
code checks that the specified address is not negative, and then registers
the address as the unique identifier for the device using the my UID
command. As the name implies, not more than one device can have a specific
unique identifier. If an attempt is made to create a device with an already
existing unique identifier, an error will be raised and the device is not
created.
destructor
The destructor keyword allows the developer to specify some code to
be executed when a device is deleted. This code will typically clean up some
resources used by the device.
Example:
destructor {
my variable afterid
after cancel $afterid
}
extend
When a device type is an extended version of another device type, the
definition can build on the existing device type by using the extend
keyword followed by the name of the other device type.
If the new device type redefines items that were defined in the original
device type, the definition in the new device type takes precedence. Procs,
funcs and methods defined in the new device type may invoke their counter
parts in the original device type using the next and
nextto commands. The next command invokes the method in
the next device type in the stacking order. Using the nextto
command, the desired source device class can be selected.
Example:
nextto [device class switch] $data
event
The event keyword is used to specify which events a device can generate.
Events can optionally have a value, in which case they are also known as
properties.
Example:
event fire
event amount value:d
The device implementation code generates an event using the my signal
command. For properties the command must specify the new value.
Examples:
my signal fire
my signal amount 42.0
proc
A proc defines a command, its arguments, and implementation that
can be invoked on a device. The proc body may optionally be followed by a
set of options in the form of key/value pairs. Currently supported options
are:
- confirm A conditional expression indicating whether the user must
be prompted for confirmation when manually running the command. Defaults to
true when a prompt option exists, and false otherwise.
- prompt The text to show to the user when prompting for
confirmation. Defaults to "Are you sure?"
Example:
proc add {num:i} {
my variable value
my signal amount [expr {$value + $num}]
} {
confirm {$num < 0}
prompt {Decrease the amount?}
}
func
A func defines a function of the device, its return value, arguments,
and implementation. A func is very similar to a proc, except it has a return
value.
Example:
func when sec:i {time:i} {
return [expr {$time - [clock seconds]}]
}
method
A piece of helper code that can be called by other code that implements the
device. Methods are not visible for the user.
typedef
The parameters of the constructor, values of events, arguments of procs and
funcs, and return values of funcs can be refined using the typedef
keyword. The typedef keyword takes a parameter/argument/value name
and one or more definitions. See type definitions below for the details.
graph
The graph keyword indicates that the user interface may present a
graph of the changes to the value over time. For each value, the following
attributes can be specified:
- type
- currently the only supported value is line
- color
- default is blue.
The minimum and maximum values specified via the typedef keyword are
used to determine the boundaries of the graph.
variable
Specifies variables that are automatically available to all code blocks of
the device implementation without the need to declare them inside the code
block using the my variable or variable command.
extend
Base the device type on previously defined device type.
track
Snapshots of the value can be stored in the track database. The code will
need to call the store command at appropriate times to actually
store the value in the database.
device
On driver installation, create a device of the current type. The device
command must be followed by the relative name for the device, the arguments
for the constructor, and an optional description for the device.
Foundation
A foundation is very similar to a device type, except that it won't be
listed as a device type the user can choose. Its purpose is to be used as a
common building block for several device types that share only some features.
Feature
A feature is another way to add features to a device definition. The most
important difference is that this can be done dynamically. A device may
auto-detect that it supports a certain feature and then load that feature
definition.
One or more features are loaded into a device using the features
method. This method takes a list of feature names. The list replaces any
previously defined list of features. This allows features to be both added
and removed.
The "feature" command is auto-loaded when used, but can also be explicitly
loaded through the use feature command.
Features end up at the top of the stacking order. This means that procs,
funcs and methods defined in features override the definitions in the device
type. The feature definitions may invoke the device type methods using the
next and nextto commands.
Type definitions
The general type of parameters, arguments and values is specified by
following the name with a colon and a single letter. The following types are
supported:
| b | boolean, false (0) or true (1) |
| y | byte (8-bit unsigned integer) |
| n | 16-bit signed integer |
| q | 16-bit unsigned integer |
| i | 32-bit signed integer |
| u | 32-bit unsigned integer |
| x | 64-bit signed integer |
| t | 64-bit unsigned integer |
| d | 8-byte double in IEEE 754 format |
| s | string |
| o | object |
The typedef keyword defines various aspects of arguments or values.
The definition part must be a sequence of option/value pairs. The following
options are supported:
- buttons
- Show a series of buttons instead of a pull-down menu and a Run button in
the device control section of the editor.
This is only applicable for commands that have a single argument that is
defined as a non-editable enum (see the edit and enum options).
A button will be shown for each enum value.
The buttons option specifies a mapping of enum values to zero or more button
settings. The icon setting will cause the specified icon to be shown on the
button, if found. Otherwise the text setting is used to display text on the
button. If neither is specified, the button will show the enum value.
A help setting may also be specified to provide additional information to
the user when they hover the mouse over the button.
- default
- The default value for an argument.
- display
- How to present the name of the argument to the user.
- edit
- Indicates whether a value may be entered that differs from the
predefined enumeration values.
- enum
- Specifies a list of names for the possible values of an argument.
- flags
- A list of options that can individually be set to on or off. The value
of the argument is a list of all options that are on.
- format
- A specification of how to present the value to the user.
- help
- An explanation of the argument to present to the user when they hover the
mouse over the entry field.
- mask
- Mask the entered text by showing a circle for each character. Commonly used
for entering passwords.
- maximum
- The highest value an argument may have.
- minimum
- The lowest value an argument may have.
- pattern
- A list of format strings defining the allowed values for the argument.
- presentation
- Tcl code to transform the property value into a string for presenting to
the user. The property value is available via the value variable.
- size
- A suggestion for the width of the entry field.
- step
- The size of the increments when changing the value.
- types
- A list of allowed object types (only applies to object arguments).
- visibility
- Whether the property should be visible in the explorer. Supported values
are:
- normal
- The property is always visible in the Status area of the device.
- optional
- The property is visible when the Status area is expanded and hidden when
it is collapsed.
- hidden
- The property is never shown in the Status area of the device.
Formatting
The format keyword understands a specification similar to the ANSI C
sprintf function. In addition it can also be used to format an integer
value representing seconds since epoch as a time & date string. The table
below shows how 17:35:15 on Dec 4, 2016 would be formatted using different
format specifiers:
| Standard format |
Alternate format |
| %T | 17:35:15 | %#T | 05:35:15 PM |
| %hT | 17:35 | %#hT | 05:35 PM |
| %lT | Sun 17:35:15 | %#lT | Sun 05:35:15 PM |
| %D | 2016-12-04 | %#D | 12/04/2016 |
| %hD | 04-12 | %#hD | 12/04 |
| %lD | 04 Dec 2016 | %#lD | Dec 04, 2016 |
| %llD | Sun 04 Dec 2016 | %#llD | Sun Dec 04, 2016 |
These format specifiers can be combined with the regular field width- and
positional specifiers. For example, to show both the time and date, the
following format specification can be used: %1$T %1$D
Proc options
The options dictionary that can be specified for a proc is
currently only used by the explorer. The following options are available:
- confirm boolean
- Ask for confirmation when the user clicks the "Run" button, via an "Are
you sure?" pop up dialog.
- display string
- How to present the command to the user. If not specified, the command
name is used.
- help string
- An explanation of the command to present to the user when they hover
the mouse over the "Run" button.
- prompt string
- The prompt to use in the confirmation dialog instead of "Are you sure?".
This option implies confirm true.
Predefined methods
The implementation code for the device can use some predefined methods. As
with all methods, these can be called using the my command:
- name
- Returns the relative name of the device.
- signal name [value] [repeat]
- Generate an event or notify domotcl of a changed property. If this
method is executed with an unchanged property value, it will not actually
generate an event unless reapeat is true.
- property name [default]
- Retrieves the current value of a property. If the property does not
currently have a value, the method call returns the optional default value
or an empty string.
- init var value ...
- Initialize variables for the device. Sometimes the device parameter names
may be equal to some variable names. This method allows the variables to be
set without causing a conflict.
- callback method [arg ...]
- A convenience method that simplifies attaching a method to an event handler.
- UID [address] [class]
- Claim a unique identifier for the device. Optionally a class may be
specified if the identifier must be unique across multiple device types. The
method may be used without any arguments to ensure that there will only be a
single device of the current type.
- store
- Store a snapshot of the tracked properties in the track database.
Interface code
The device connection command may be used to define the driver
parameters and code for a simple interface. For a more complex interface,
the connect module can be used.
The interface code will regularly need to locate a device and pass
information to it. The device address command can be used for this
purpose. The command returns a reference to the device of the specified type
that registered the address as its unique identifier. This reference can be
used to invoke public methods of the device. If no device registered the
address, a dummy reference is returned. The dummy reference accepts any
method call with an arbitrary number of arguments without producing an
error. This allows the reference to be used without the need to check if
there actually is a matching device.
If the interface code needs a list of all devices of a specific type, the
device list command can be used.
MQTT device types
A communication protocol that is frequently used in home automation is MQTT.
To simplify the definition of device types that use this protocol, the
mqttdevice command can be used.
These device types should only be used in drivers loaded into a node of the
mqtt type (i.e. in drivers with the .mqd extension). The command gets
auto-loaded when the mqttdevice command is used.
The mqttdevice command works just like the device command,
but it recognizes some additional or changed keywords:
- topic
- Define the MQTT base topics that the device uses to communicate. The
topics may be provided as alternating name and value arguments, or as a
single argument containing a name/value dict.
There are two relevant topic names:
- action
- The topic to use to send instructions to the device.
Default: actions/<deviceid>
- event
- The topic the device uses to report information.
Default: events/<devicetype>/<deviceid>
Any "%" character in the topic definitions is replaced by the device ID. Use
"%%" for a literal "%".
- action
- Define an MQTT action name and arguments. When the named command is
executed in a domotcl action, the associated MQTT message is sent.
Example: Suppose a domotcl device called /lights/lamp1 contains action
level {pct:y} in its device type definition. Then /lights/lamp1
level 50 will result in an MQTT message with topic "actions/lamp1" and
body "50".
- event
- The event definition is the same as for a regular device type. By
default the event will fire when a matching MQTT message is received.
If the default behavior is not correct for the device type, the definition
can include replacements for the action and event methods. These methods are
invoked when a command is executed or an MQTT message is received.
This may be necessary when the device works with JSON encoded messages, for
example.
Note: It is not mandatory to use mqttdevice definitions in an mqtt type
driver. If the device uses a very different strategy concerning its MQTT
messages, it may make more sense to use a regular device definition and
handle the MQTT messages using subscribe and publish
commands.
Driver evolution
With increasing insights it may become desirable to modify the driver, which
may include adding driver- or device parameters. However, the current
schedule may already have instances of the driver and devices without those
parameters. To handle this situation, it is possible to provide evolution
instructions.
The evolution instructions have the following format:
evolve {
version 1 {
interface {
<rule>
<rule>
...
}
type <devtype1> {
<rule>
<rule>
...
}
type <devtype2> {
<rule>
<rule>
...
}
...
}
version 2 {
interface {
<rule>
<rule>
...
}
type <devtype1> {
<rule>
<rule>
...
}
type <devtype2> {
<rule>
<rule>
...
}
...
}
...
}
Comments may be placed in the evolve specification by starting a line with #.
A driver that does not contain any evolve instructions is considered to be
at version 0. The rules in the "version 1" evolve section describe the
changes needed to go from version 0 to version 1. The rules in the "version
2" section describe the changes from version 1 to version 2, etc. The version
sections should be listed in increasing order. The last version section
determines the current version of the driver.
When a user replaces a driver with a newer version. All version sections
between the old driver version and the new version will be applied.
Rules
The following rules are currently implemented:
- add arg name init [selection]
- A device argument has been added. Specify the name and the value to use
for existing devices.
- remove arg name [selection]
- The indicated device argument has been removed. This doesn't actually
have to be specified because the code can figure that out.
- rename arg old new [selection]
- A device argument was renamed from old to new.
- rename event old new
- A device event was renamed from old to new. "trigger" may
be used as an alias for "event".
- replace name [selection]
- The specified device type replaces the old device type name.
Selection
Some rules accept an additional selection specification to limit which
devices the rule applies to. The following selection criteria are supported:
- arg name operation value
- Selection based on the value of a named argument. Valid operations are
= (or ==), <, >, <=, >=, != (or <>).
Example
The system/time driver used to have a device type called event, that
had 3 arguments: base, time, and random. The
base argument could have 1 of 4 values: clock, sunrise,
sunset, or dark. For values other than clock, the time
argument actually represented the offset from the selected base. In those
cases the time value was allowed to be negative, but was limited to +/- 4
hours.
So, the combination of these 4 possibilites in one device type was a bit
awkward. It was therefor decided to split the old event device type
into two new ones: clock, for events based on wall-clock time, and
solar, for events based on the sun rise or sun set.
The follwoing evolve section was added to the driver:
evolve {
version 1 {
type clock {
replace event arg base = clock
remove arg base
}
type solar {
replace event
rename arg time offset
}
}
}
This means:
- The new clock device type replaces the old event device
type for only those devices where the base argument was "clock".
- The base argument no longer exists on the new clock device
type and must be removed.
- The new solar device type replaces the old event device
type for all remaining devices.
- The time argument of the old event device type has been
renamed to offset on the new solar device type.
|