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 and queried the connected devices
- Initialized device 0
- Sent the command to set/close the ADU100's relay
- Queried the relay status
- Sent the command to reset/open the ADU100's relay
- Queried the relay status again
- What did we just do?
- Getting started
- Command reference
- References
- Demonstration
Let's say you've downloaded a release binary, and you have an ADU100 connected. You also need permissions to access the device, but let's say you have that.
johnpeck@darkstar:~/Downloads $ tar xzvf tcladu-1.1.3-linux-x64.tar.gz
tcladu/
tcladu/pkgIndex.tcl
tcladu/tcladu.so
tcladu/tcladu.tcl
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.1.3
% tcladu::serial_number_list
B02597
% tcladu::initialize_device 0
0
% tcladu::send_command 0 "SK0"
0 4
% tcladu::query 0 "RPK0"
0 1 12
% tcladu::send_command 0 "RK0"
0 6
% tcladu::query 0 "RPK0"
0 0 13
The package consists of three files:
pkgIndex.tcl
— used by Tcl's package proceduretcladu.tcl
— a Tcl source file containing procedures that call c-functions in a binary filetcladu.so
— a platform-specific binary produced from somec
code.
The auto_path list tells Tcl where to look for packages.
This both loads procedures into the tcladu
namespace and initializes libusb.
The serial_number_list 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. It returns a list of connected-device serial numbers whose indexes are used to identify devices in commands like send_command.
This configures the USB endpoint on device 0.
The send_command 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 return value list tells us that
- The command succeeded
- It took 4ms to execute the commmand (this doesn't include the time it takes to close the relay).
The query command
- Sends a command telling the ADU100 to read its relay state
- Sends a command telling the ADU100 to report its relay state
...and then returns a list of values:
- 0 for success
- 1 for a closed relay
- 12 for 12ms of execution time
This is the opposite of the set command.
We'll now expect the hardware to report 0 for the relay status.
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.3
...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.3
load /usr/share/tcltk/tcladu1.1.3/tcladu.so
source /usr/share/tcltk/tcladu1.1.3/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 should not be called directly. Use serial_number_list instead.
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.3
% tcladu::_discovered_devices
1
...where tcladu correctly found 1 connected ADU100.
This command should not be called directly. Use initialize_device instead.
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
- libusb error code on error
Read the device's response to a command.
- Device index (0, 1, ..., connected ADU100s -1)
- Pointer to memory used for the returned string (handled by SWIG)
- Characters to read
- How long to tell libusb to wait for data (ms)
- On success, a list of
- 0 to indicate success
- The string read from the ADU100
Note that the returned string is handled by SWIG. The only explicit return is the integer returned by the C code.
- On error, a negative error code to be interpreted by read_device.
This command calls the low-level _initialize_device, allowing libusb errors to throw Tcl errors.
- Device index (0, 1, ..., connected ADU100s -1)
- 0 on success
- Tcl error on error
The high-level initialize_device
and low-level _initialize_device
have the same usage.
% package require tcladu
1.1.3
% 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.3
% 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)
- On success, a list of
- 0 to indicate success
- Elapsed time needed to clear the queue
- On failure, a Tcl error
Clear the queue (of the ADU100 at index 0) with
% package require tcladu
1.1.3
% 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
- On success, a list of
- 0 to indicate success
- Elapsed time needed to send the command (not to process the command)
This sequence shows populating the device database, then setting (closing) the ADU100's relay.
% package require tcladu
1.1.3
% 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.3
% 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.