Tcl package supporting multiple ADU100s from Ontrak Control Systems via libusb and SWIG.
Table of Contents
- tcladu
- Demonstration
- What did we just do?
- Unpacked the TGZ
- Appended the package to Tcl's
auto_path
- Required the package
- Populated the connected device database
- Queried the device database for device 0
- Sent the command to set/close the ADU100's relay
- Sent the command to read the relay status
- Read from the ADU100
- Sent the command to reset/open the ADU100's relay
- Sent the command to check the relay status again
- Read from the ADU100
- What did we just do?
- Getting started
- Command reference
- References
- Demonstration
Let's say you've downloaded a release binary from Sourceforge, and you have a few (two) ADU100s connected. You also need permissions to access the device, but let's say you have those.
johnpeck@darkstar:~/Downloads $ tar xzvf tcladu-1.0.0-linux-x64.tar.gz
tcladu/
tcladu/pkgIndex.tcl
tcladu/tcladu.so
johnpeck@darkstar:~/Downloads $ cd ~
johnpeck@darkstar:~ $ tclsh
% lappend auto_path ~/Downloads
/usr/share/tcltk/tcl8.6 /usr/share/tcltk ... ~/Downloads
% puts [package require tcladu]
1.0.0
% puts [tcladu::discovered_devices]
2
% puts [tcladu::serial_number 0]
B02597
% puts [tcladu::write_device 0 "SK0" 200]
0
% puts [tcladu::write_device 0 "RPK0" 200]
0
% puts [tcladu::read_device 0 8 200]
0 1
% puts [tcladu::write_device 0 "RK0" 200]
0
% puts [tcladu::write_device 0 "RPK0" 200]
0
% puts [tcladu::read_device 0 8 200]
0 0
The package is just two files: pkgIndex.tcl
, used by Tcl's
package procedure, and
tcladu.so
, a binary produced from some c
code.
The auto_path list tells Tcl where to look for packages.
This both loads procedures into the tcladu
namespace and initializes libusb.
The discovered_devices
command will populate a device database with
things like device handles and serial numbers. This must be called
before writing to or reading from devices.
The serial_number
command doesn't do anything with connected
hardware -- it just returns a serial number populated by
discovered_devices
.
The write_device
command takes a device index instead of some kind
of handle to identify the targeted device. It then takes an ASCII
command that you can find in the ADU100
manual to manipulate the
hardware relay. The last argument is a timeout for libusb (in
milliseconds), which will become more interesting when we get into
reading from the hardware.
Reading the relay status starts with telling the ADU100 to read the
status. It will prepare the result to be read by the next libusb
read. The return value for the RPK0
command is just a success code
-- not the relay status.
The read_device
command takes a device index, followed by the number
of bytes we want to read. This payload size is a placeholder for now,
although it has to be 8 bytes or larger. I want to keep it to handle
larger payloads on other Ontrak devices this might support in the
future.
The final argument is the familiar ms timeout. Libusb will throw a timeout error if the read takes longer than this value. But this error isn't fatal, and your code can catch this and simply try again. This gives your application a chance to stay active while you wait for a long hardware read.
The result is a Tcl list
containing the success code and return value. In this case, a 1
shows us that the relay is set/closed.
This is the opposite of the set command.
We'll now expect the hardware to report 0 for the relay status.
The returned list is now 0 0
, telling us that the command succeeded
and that the relay is reset/open.
We need libusb-1.0-0
to call the libusb functions in tcladu. This
comes from Ubuntu's libusb-1.0-0
package, and I'm using version
2:1.0.25-1ubuntu2
on 2024-Mar-07.
We need libusb-1.0/libusb.h
to build the package, which comes from
Ubuntu's libusb-1.0-0-dev
package. I have the same version of the
binary and dev packages.
You can't communicate with the ADU100 without permission, and
udev allows configuring that
permission when devices are plugged in. I copy the rule here to /usr/lib/udev/rules.d
and then call
sudo udevadm control --reload-rules
...to activate the new rule. Remember that these rules are only
applied to new devices, so you'll need to unplug and plug your device
after reloading the rules. The rule I've linked is for any Ontrak
device, and it sets the device mode to 0666
. You can use
a nice permissions calculator to
set whatever permissions you need.
You can make sure permissions are working with lsusb
from usbutils
.
johnpeck@darkstar:~ $ lsusb | grep Ontrak
Bus 001 Device 017: ID 0a07:0064 Ontrak Control Systems Inc. ADU100 Data Acquisition Interface
...which means the device is located at /dev/bus/usb/001/017
. We can check the permissions with
johnpeck@darkstar:~ $ ls -al /dev/bus/usb/001/017
crw-rw-rw- 1 root root 189, 16 Mar 2 05:44 /dev/bus/usb/001/017
...showing that our rule is working.
Place the package somewhere the Tcl auto loader can find it. I like
usr/share/tcltk
. This path must be in the auto_path
list. For
example,
% puts $auto_path
/usr/share/tcltk/tcllib1.20 ... /usr/share/tcltk ...
...my auto_path
includes /usr/share/tcltk
. When I put tcladu in that directory, I can require it with
% package require tcladu
1.1.1
...where the command returns the loaded version. I can then make sure the package got loaded from the right place with
% package ifneeded tcladu 1.1.1
load /usr/share/tcltk/tcladu1.1.1/tcladu.so
source /usr/share/tcltk/tcladu1.1.1/tcladu.tcl
...showing the path I expected. This step is more important if you build the package yourself, as you might have intermediate builds around with the same version number. See the references below for more information about package ifneeded.
These are commands implemented in tcladu.c
and broken out via
SWIG.
This command returns the number of ADU100 devices discovered on USB. The key line is
if ( desc.idVendor == 0x0a07 && desc.idProduct == 0x0064 ) {
...showing how USB descriptors are used to discover ADU100s. This command also populates the device database -- required for using numbers like the device index in other commands.
None
- 0 to the number of discovered ADU100 devices if successful
- -1 if there was an error during discovery
We can get the number of connected ADU100s and populate the internal database with
% package require tcladu
1.1.1
% tcladu::discovered_devices
1
...where tcladu correctly found 1 connected ADU100.
This command is more about initializing the USB interface than it is about initializing the ADU100 hardware. But it acts on one device instead of some broader initialization. It does two things:
- Enable libusb's automatic kernel driver detachment for the chosen ADU100.
- Claim interface number 0 for the chosen ADU100. See the libusb documentation.
The device database must be populated with
tcladu::serial_number_list
or tcladu::discovered_devices
before
calling this function.
- Device index (0, 1, ..., connected ADU100s -1)
- 0 on success
% package require tcladu
1.1.1
% tcladu::serial_number_list
B02797
% tcladu::initialize_device 0
0
Returns a list of connected ADU100 devices. This calls
tcladu::discovered_devices
internally to populate the connected
device database.
None
A list of discovered ADU100 devices. This list will be empty if no devices are discovered.
% package require tcladu
1.1.0
% tcladu::serial_number_list
B02597 B02797
Returns a list of success code
execution time
after repeatedly
calling read_device() to clear the ADU100's transmit buffer. This
prevents confusion from queries returning old data.
- Device index (0, 1, ..., connected ADU100s -1)
Clear the queue (of the ADU100 at index 0) with
% package require tcladu
1.1.1
% tcladu::serial_number_list
B02597
% tcladu::initialize_device 0
0
% tcladu::clear_queue 0
0 12
...and the return tells us this took 12ms to succeed. We need to call
initialize_device
here because clear_queue
sends commands to the
device.
Returns a list of success code
execution time
after sending an
ASCII command. You must call serial_number_list
to populate the
device database before calling send_command
.
- Device index (0, 1, ..., connected ADU100s -1)
- Command
This sequence shows populating the device database, then setting (closing) the ADU100's relay.
% package require tcladu
1.1.0
% tcladu::serial_number_list
B02597 B02797
% tcladu::initialize_device 0
0
% tcladu::send_command 0 "SK0"
0 8
The return is 0
(success), followed by 8
-- it took 8ms to get a
response from the ADU100 (this is not how long it takes to close the
relay).
Returns a list of success code
response
execution time
after
sending an ASCII query command. You must call serial_number_list
to populate the
device database before calling query
.
- Device index (0, 1, ..., connected ADU100s -1)
- Command
This sequence shows querying the (open/reset) relay status, closing the relay, then querying again.
% package require tcladu
1.1.1
% tcladu::serial_number_list
B02597
% tcladu::clear_queue 0
0 13
% tcladu::query 0 RPK0
0 0 10
% tcladu::send_command 0 SK0
0 5
% tcladu::query 0 RPK0
0 1 14
The final response of 1
shows that the relay is closed.
- See the Tcler's Wiki for a description of package ifneeded.
- See the libusb 1.0 API documentation for better descriptions of the low-level commands.