Advantech USB-4761 relays, C++ and Python
Recently I was trying to control the relays of an Advantech USB-4761 DAQ-module from a Linux box in a Python project.
I decided to write a little post about it because I couldn’t find any good documentation or a StackOverflow answer that would suit my needs.
Maybe someone will face a similar problem someday and end up here.
The DAQNavi library
I downloaded the Linux DAQNavi driver install script from Advantech’s website
and ran it. Because of some unsigned kernel module, I needed to disable SecureBoot from my computer for the installation to work.
(BTW, running the install script with argument silent allows for a command-line installation if you ever need to use it in a script)
After installation, I started to look at the example code that was now found under /opt/advantech. I tried my best to look for some library documentation,
but I couldn’t find anything helpful for me. So, my approach was to start hacking away.
Because my goal was to use Python to control the DAQ, I first tried looking at files in the Python example directory.
I didn’t understand how those were supposed to work, so I checked what the C++ examples looked like.
Creating a .cpp file in VS-code and including the DAQ library headers was much better documentation than anything else that I could find.
At least with that, I could more easily search through the available classes and functions. Also, finding a few code snippets online helped me a lot
with the DAQ library. After a bit of tinkering, I managed to write the following C++ code that could control the DAQ device:
#include "/opt/advantech/examples/C++_Console/inc/compatibility.h"
#include "/opt/advantech/inc/bdaqctrl.h"
using namespace Automation::BDaq;
int readPort()
{
ErrorCode ret = Success;
DeviceInformation devInfo(L"USB-4761,BID#0");
const int port = 0;
unsigned char existing = 0;
InstantDoCtrl * instantDoCtrl = InstantDoCtrl::Create();
ret = instantDoCtrl->setSelectedDevice(devInfo);
if (ret == Success) {
ret = instantDoCtrl->Read(port, existing);
}
instantDoCtrl->Dispose();
if (ret != Success) {
return -1;
}
return existing;
}
bool writePort(int data)
{
ErrorCode ret = Success;
DeviceInformation devInfo(L"USB-4761,BID#0");
const int port = 0;
InstantDoCtrl * instantDoCtrl = InstantDoCtrl::Create();
ret = instantDoCtrl->setSelectedDevice(devInfo);
if (ret == Success) {
ret = instantDoCtrl->Write(port, data);
}
instantDoCtrl->Dispose();
return ret == Success;
}
I needed just two functions here; One to read the current state of the relays and one to write new values to them.
What I found confusing was the InstantDoCtrl Read and Write methods.
I didn’t find any documentation for why the port parameter should be 0 to control the relays, but I managed to figure that out from some old Reddit post.
(unfortunately, I already lost the post and couldn’t find it again…)
The data parameter used to control the relays was just an 8-bit number, giving each relay a single control bit.
So, e.g., writing 0b1010000 would pull our relays 0 and 2.
These two simple functions allowed me to close and open the relays. Yay!
Running C++ code from Python.
Because my goal was to control the DAQ with a program written in Python, I had to find some way to run the C++ code in Python.
I could have built the C++ program with some command-line options and just executed that from Python, but I didn’t want to do that.
So instead, I used the pybind11 library to create a Python binding to the C++ code.
I added the pybind11 as a submodule to my project and added the following code to my C++ file:
#include <pybind11/pybind11.h>
...
/* Previous code here */
...
PYBIND11_MODULE(daq_control, handle) {
handle.doc() = "Controller for USB-4761 relays";
handle.def("writePort", &writePort);
handle.def("readPort", &readPort);
}
My Python project was packaged using PyPi setup tools, so to use the Python binding in my other Python code,
I needed to make some changes to my setup.py file and my pyproject.toml file.
What I had to do was to
- Add pybind11 as a build-system requirement
- Import the Pybind11Extension
- Add an ext_modules entry for the binding
The only thing I needed to add to my toml file was:
requires = ["setuptools", "wheel", "pybind11>=2.6.1"]
And the pieces of code added to my setup.py looked like this:
from pybind11.setup_helpers import Pybind11Extension
...
ext_modules = [
Pybind11Extension(
"daq_control",
["myproject/src/relay.cpp"],
extra_link_args=["-lbiodaq"],
),
]
...
setup(
name=NAME,
...
ext_modules=ext_modules,
...
)
For a complete example, I suggest looking at the pybind11 repo example. One thing that
I didn’t find in the example repo is the usage of the extra_link_args keyword, where I needed to add the library for the USB module.
But that was easy to use after I got it figured out.
After all that, I could import the daq_control module and use it to control the relays.