Mike's blog about random software stuff

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

  1. Add pybind11 as a build-system requirement
  2. Import the Pybind11Extension
  3. 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.