Skip to content

C++ API for the USBtingo - USB to CAN-FD Interface

License

Notifications You must be signed in to change notification settings

hannesduske/libusbtingo

Repository files navigation

Libusbtingo

C++17 License: MIT Unit tests (master) Unit tests (develop)


🔧 A lightweight C++ API for the USBtingo — USB to CAN FD converter

Libusbtingo makes it easy to interact with the USBtingo, providing high-level access for sending and receiving CAN and CAN FD messages. It also supports using the USBtingo as a 1-channel logic analyzer with a sample rate of up to 40 MHz.


💡 Open to contributions
Feel free to fork, improve, and submit a pull request — looking forward to your improvements! 🚀

Contents

  1. Building and installing the library
    1.1 Requirements for Windows
    1.2 Requirements for Linux
    1.3 Using the library in other CMake projects
    1.4 Building the library from source
    1.5 Installing the library
    1.6 CMake Options
  2. How to use the library
    2.1 BasicBus
    2.2 Bus
    2.3 Device
    2.4 DeviceFactory
  3. Utility applications
  4. Minimal examples
    4.1 Using the BasicBus
    4.2 Using the Bus
    4.3 Using the logic analyzer

1. Building and installing the library

1.1 Requirements for Windows

  • CMake
  • Some C++17 compiler (e.g. MSVC)
  • Windows SDK or libusb

ℹ️ Libusb: It is possible to use libusb instead of the Windows SDK. Refer to the USBTINGO_USE_WINAPI option for further details. This option has not been tested and might require some additional configuration of the CMake files.

1.2 Requirements for Linux

  • CMake
  • Some C++17 compiler (e.g. GCC, Clang)
  • libusb-1.0-0 and libusb-1.0-0-dev

ℹ️ Run the following command to install the dependencies.

sudo apt update
sudo apt install -y cmake build-essential libusb-1.0-0 libusb-1.0-0-dev

⚠️ When using the USBtingo on Linux, a udev rule should be added to allow all users to access the device. Otherwise root privileges are required to access the device.

sudo bash -c $'echo \'SUBSYSTEM=="usb", ATTRS{product}=="USBtingo", MODE="0666"\' > /etc/udev/rules.d/50-USBtingo.rules'
sudo udevadm control --reload-rules

1.3 Using the library in other CMake projects

You can integrate libusbtingo directly into your own CMake project using FetchContent. This fetches the library at configure time if it is not already installed.

Add the following to your CMakeLists.txt (e.g. before defining targets that use usbtingo):

find_package(usbtingo QUIET)

if(usbtingo_FOUND)
  set(USBTINGO_INSTALLED ON)
else()
  message(STATUS "Did not find libusbtingo. Fetching it from GitHub...")
  set(USBTINGO_INSTALLED OFF)

  include(FetchContent)
  FetchContent_Declare(
    usbtingo
    URL https://github.com/hannesduske/libusbtingo/archive/v1.1.4.zip  # specific version
    # URL https://github.com/hannesduske/libusbtingo/archive/refs/heads/master.zip  # latest from master
  )

  set(USBTINGO_INSTALL_DEV_COMPONENTS OFF)
  set(USBTINGO_BUILD_EXAMPLES OFF)
  set(USBTINGO_BUILD_UTILS OFF)
  set(USBTINGO_BUILD_TESTS OFF)

  FetchContent_MakeAvailable(usbtingo)
  add_library(usbtingo::usbtingo ALIAS usbtingo)
endif()

Then link your executable or library against libusbtingo like normal:

add_executable(my_own_app main.cpp)
target_link_libraries(my_own_app PRIVATE usbtingo::usbtingo)

If you use the installed package (usbtingo_FOUND is true), the same usbtingo::usbtingo target is used, so your executable definition does not need to change.

1.4 Building the library from source

The library is built with a standard CMake workflow which is almost identical for Windows and Linux. Use the following commands to build the library.

git clone https://github.com/hannesduske/libusbtingo.git

mkdir libusbtingo/build
cd libusbtingo/build
cmake ..
cmake --build .

ℹ️ For the MSVC compiler on Windows, you need to specify which configuration you want to build.

cmake --build . --config=Release

1.5 Installing the library

The library can be installed to CMakes default location with the cmake --install command. The default install location is C:/Program Files (x86)/libusbtingo on Windows and /usr/local on Linux.

Install command for Windows. Requires terminal with admin rights:

cmake --install .

Install command for Linux:

sudo cmake --install .

The library can be installed to a custom location by specifying an install path. Replace <path> with your desired install directory.

sudo cmake --install . --prefix <path>

⚠️ Custom install paths should be added to the CMAKE_PREFIX_PATH environment variable if the library is installed to a non default location. This enables other packages to find this library.

⚠️ Update the linker cache when installing a shared library by running sudo ldconfig after the installation.

1.6 CMake Options

The build can be configured with CMake options. Options can be set by calling cmake .. with the flag -D. For example, the following command builds the library as a shared library and disables tests.

cmake .. -DUSBTINGO_BUILD_SHARED_LIBS=ON -DUSBTINGO_BUILD_TESTS=OFF
CMake Option Default value Description
USBTINGO_INSTALL ON Enable the installation of the library.
USBTINGO_INSTALL_DEV_COMPONENTS ON Enable the installation of the components required for development, i.e. the libraries headers.
USBTINGO_BUILD_SHARED_LIBS OFF Build libusbtingo as shared library. If set to OFF a static library is built.
USBTINGO_BUILD_EXAMPLES OFF Build the minimal examples.
USBTINGO_BUILD_UTILS ON Build and install utility programs along with the library.
USBTINGO_BUILD_TESTS OFF Build the test utilities for the library. Requires Catch2.
USBTINGO_ENABLE_INTERACTIVE_TESTS OFF Enable tests that have to be confirmed manually.
USBTINGO_ENABLE_TESTS_WITH_OTHER_DEVICES OFF Enable tests that require other CAN devices to send and acknowledge CAN messages.
USBTINGO_USE_WINAPI ON This option is only available on Windows platforms. Choose which USB backend is used. The default backend is the Windows API. When this option is turned OFF, libusb is used instead. This requires libusb to be installed.

ℹ️ Catch2: The tests are built with Catch2. When tests are enabled with USBTINGO_BUILD_TESTS=ON, CMake looks for a local Catch2 installation. If no Catch2 installation is found, the library will fetch it from GitHub.

⚠️ The legacy options BUILD_SHARED_LIBS, BUILD_EXAMPLES, BUILD_UTILS, BUILD_TESTS, ENABLE_INTERACTIVE_TESTS, ENABLE_TESTS_WITH_OTHER_DEVICES, and USE_WINAPI are deprecated and will show a warning. Use the USBTINGO_* prefixed options instead.

2. How to use the library

This library has two interfaces to access a CAN Bus with a USBtingo: The BasicBus and the Bus. Both interfaces use the same underlying implementation and each manage one USBtingo device. They differ in the level of raw data accessibility and ease of use.

2.1 BasicBus

The BasicBus is a simple, easy to use interface with reduced functionality. It is recommended for all applications that exchange simple CAN or CAN FD data messages and do not rely on advanced features of the USBtingo. The BasicBus automatically chooses the first USBtingo device it discovers and does not require manual configuration. An overload of create() that takes a device index is also available to select a specific USBtingo when multiple devices are connected.

A BasicBus object can be directly instantiated using its static create() method. The returned BasicBus object is operational without any additional configuration, provided that a working USBtingo device is connected to the system.

2.2 Bus

The Bus interface offers full control over the USBtingo device and grants access to the raw data buffers that are exchanged with the USBtingo. This interface is more complex and is recommended in cases where the simplified BasicBus does not meet the application requirements.

Bus objects require a valid Device that has to be configured before passing it to a Bus. The Device configuration includes all CAN bus parameters, i.e. its protocol, baudrate and all advanced options. Refer to DeviceFactory for how to safely instantiate Device objects.

2.3 Device

The Device represents the connected USBtingo and implements all necessary interface methods. After creating a valid Device with the DeviceFactory it has to be configured with the desired CAN parameters. After the configuration is complete, a Device can be used to instantiate a BasicBus or a Bus which handles all further communication with the USBtingo.

ℹ️ One physical USBtingo can only be managed by one Device at the same time.

2.4 DeviceFactory

Enumerating devices

The DeviceFactory offers a method to enumerate all connected USBtingo devices which returns a vector of the corresponding serial numbers. The serial numbers can be used in the factory method DeviceFactory::create() to instantiate a specific USBtingo Device.

Creating devices

The Device is an abstract interface and cannot be instantiated directly. Use the DeviceFactory to create Device objects instead. The device factory chooses the correct Device implementation (libusb or WinApi) for the current system. In addition, the Factory makes sure that the specified USBtingo is physically connected and operational before returning the object. If the device does not operate correctly a nullptr is returned instead.

3. Utility applications

The library comes with three small utility applications that illustrate the basic functionality and serve as examples on how to use the library.

USBtingoDetect
Minimal example of a command line program that lists the serial numbers of all connected USBtingo devices. At the start all currently connected USBtingo serial numbers are printed. Subsequently, all connection and disconnection events of USBtingo devices are printed.

USBtingoCansend
Minimal example of a command line program that sends CAN messages. After the configuration, the program sends all entered messages on the CAN Bus.

USBtingoCandump
Minimal example of a command line program that prints out all received CAN messages. After the configuration, a listener is registered as an observer of the CAN Bus instance that gets notified asynchronously when new messages arrive.

ℹ️ Only one of the utility applications can access a USBtingo device at a time. It is currently not possible to run the USBtingoCansend and USBtingoCandump example side by side.

4. Minimal examples

Refer to the utility applications USBtingoDetect, USBtingoCansend and USBtingoCandump in the apps/utils directory for examples on how to use this library. Below are two additional minimal examples on how to use the BasicBus and the Bus.

4.1 Using the BasicBus

Following is a minimal example on how to use the BasicBus to send and receive CAN messages. This is a shortened version of the MinimalExampleBasicBus.cpp. Find the full code of this example here.

MinimalExampleBasicBus.cpp

#include "usbtingo/basic_bus/BasicBus.hpp"
#include "usbtingo/basic_bus/Message.hpp"

#include "MinimalBasicListener.hpp"

#include <cstdint>
#include <chrono>
#include <thread>

using namespace usbtingo;
using namespace std::literals::chrono_literals;

// Setup of the CAN parameters
constexpr std::size_t                 device_index    = 0;
constexpr device::Protocol            protocol        = device::Protocol::CAN_FD;
constexpr std::uint32_t               baudrate        = 1000000;
constexpr std::uint32_t               data_baudrate   = 1000000;

// Data for a CAN test message
constexpr std::uint32_t               testid          = 42;
constexpr std::array<std::uint8_t, 5> testdata        = { 0, 1, 2, 3, 4 };

/**
 * @brief Minimal example of a program that opens a BasicBus to send and receive CAN messages.
 */
int main(int argc, char *argv[])
{
    // Create one USBtingo according to the index
    auto bus = bus::BasicBus::create(device_index, baudrate, data_baudrate, protocol);

    // Check if the device object is valid
    if(!bus) return 0;

    // Register an observer that gets notified when new messages arrive
    MinimalBasicListener listener;
    bus->add_listener(reinterpret_cast<usbtingo::bus::BasicListener *>(&listener));
    
    // Create a tx message with the Message class.
    bus::Message tx_msg(testid, std::vector<std::uint8_t>(testdata.begin(), testdata.end()));

    // Send a message every second until the program is stopped
    while (true)
    {
        // Send message 
        bus->send(tx_msg);   

        // Do something else ...

        // Maybe add some break condition ...

        // Sleep one second
        std::this_thread::sleep_for(1000ms);
    }
    return 1;
}

MinimalBasicListener.hpp

#pragma once

#include <usbtingo/basic_bus/BasicListener.hpp>

using namespace usbtingo;

class MinimalBasicListener : public usbtingo::bus::BasicListener{
public:
    void on_can_receive(const usbtingo::bus::Message msg) override
    {
        // This callback is executed whenever a new CAN message is received.
        // Do something with the received message here, e.g. print it to the command line ...
    }
};

4.2 Using the Bus

Following is a minimal example on how to use the Bus to send and receive CAN messages. This is a shortened version of the MinimalExampleBus.cpp. Find the full code of this example here.

MinimalExampleBus.cpp

#include "usbtingo/can/Dlc.hpp"
#include "usbtingo/bus/Bus.hpp"
#include "usbtingo/basic_bus/Message.hpp"
#include "usbtingo/device/DeviceFactory.hpp"

#include "MinimalCanListener.hpp"

#include <cstdint>
#include <chrono>

using namespace usbtingo;
using namespace std::literals::chrono_literals;

// Setup of the CAN parameters
constexpr std::size_t                 device_index    = 0;
constexpr device::Protocol            protocol        = device::Protocol::CAN_FD;
constexpr std::uint32_t               baudrate        = 1000000;
constexpr std::uint32_t               data_baudrate   = 1000000;

// Data for a CAN test message
constexpr std::uint32_t               testid          = 42;
constexpr std::array<std::uint8_t, 5> testdata        = { 0, 1, 2, 3, 4 };

/**
 * @brief Minimal example of a program that opens a Bus to send and receive CAN messages.
 */
int main(int argc, char *argv[])
{
    // Get all connected USBtingo devices
    auto serial_vec = device::DeviceFactory::detect_available_devices();
    if(serial_vec.size() <= device_index) return 0;

    // Create one USBtingo according to the index
    auto serial = serial_vec.at(device_index);
    auto device = device::DeviceFactory::create(serial);

    // Check if the device object is valid
    if(!device) return 0;

    // Configure the device
    device->set_mode(device::Mode::OFF);                // Device has to be in Mode::OFF for the configuration
    device->set_baudrate(baudrate, data_baudrate);      // Set baudrate
    device->set_protocol(protocol, 0b00010000);         // Set protocol and disable automatic retransmission of failed messages
    device->set_mode(device::Mode::ACTIVE);             // Activate device before passing it to the Bus

    // Create a Bus object
    auto bus = std::make_unique<bus::Bus>(std::move(device));

    // Register an observer that gets notified when new messages arrive
    MinimalCanListener listener;
    bus->add_listener(reinterpret_cast<usbtingo::bus::CanListener *>(&listener));
    
    // Variant 1: Manually create a tx message.
    device::CanTxFrame tx_msg1;
    tx_msg1.id = testid;
    tx_msg1.dlc = can::Dlc::bytes_to_dlc(testdata.size());
    bool is_fd = (protocol == device::Protocol::CAN_2_0) ? false : true;
    tx_msg1.fdf = is_fd;
    std::copy(testdata.begin(), testdata.end(), tx_msg1.data.data());

    // Variant 2: Create a tx message with the Message class.
    bus::Message tx_msg2(testid, std::vector<std::uint8_t>(testdata.begin(), testdata.end()));

    // Send a message every second until the program is stopped
    while (true)
    {
        // Send message with variant 1
        bus->send(tx_msg1);   
        std::this_thread::sleep_for(1000ms);
        
        // Send message with variant 2 (pass is_fd for CAN FD)
        bus->send(tx_msg2.to_CanTxFrame(is_fd));   
        std::this_thread::sleep_for(1000ms);

        // Do something else ...

        // Maybe add some break condition ...
    }

    return 1;
}

MinimalCanListener.hpp

#pragma once

#include <usbtingo/bus/CanListener.hpp>

using namespace usbtingo;

class MinimalCanListener : public usbtingo::bus::CanListener{
public:
    void on_can_receive(const device::CanRxFrame msg) override
    {
        // This callback is executed whenever a new CAN message is received.
        // Do something with the received message here, e.g. print it to the command line ...
    }
};

4.3 Using the logic analyzer

Following is a minimal example on how to use the USBtingo as a 1-channel logic analyzer. When using the device as logic analyzer, the signal has to be connected to the CAN-Low pin.
This is a shortened version of the MinimalExampleLogicStream.cpp. Find the full code of this example here.

Calculating the sample rate
The sample rate of the USBtingo is calculated as follows: $samplerate = {120,\mathrm{MHz}}/{prescaler}$. The value of the prescaler is limited to the interval of 3 ... 255, resulting in sample rates between 470 kHz ... 40 MHz. If a sample rate outside of this interval is specified, it is automatically clamped to the upper or lower limit of this range. If 0 is passed as the sample rate, the rate is derived automatically from the CAN baudrate (10× the nominal or data baudrate, depending on the protocol).

Data returned by the logic analyzer
The USBtingo transmits the logic data as 512 byte chunks with each bit representing a single logic value. For example, one 512 byte data chunk, recorded with a sample rate of $1 MHz$, represents 4096 logic values sampled over $4.096 ms$.

MinimalExampleLogicStream.cpp

#include "usbtingo/bus/Bus.hpp"
#include "usbtingo/device/DeviceFactory.hpp"

#include "MinimalLogicListener.hpp"

#include <cstdint>
#include <chrono>

using namespace usbtingo;
using namespace std::literals::chrono_literals;

// Set the samplerate for the logic data stream
constexpr std::size_t       device_index    = 0;
constexpr std::uint32_t     samplerate_hz   = 1000000;

/**
 * @brief Minimal example of a program that opens the logic data stream and prints it to the command line.
 */
int main(int argc, char *argv[])
{
    // Get all connected USBtingo devices
    auto serial_vec = device::DeviceFactory::detect_available_devices();
    if(serial_vec.size() <= device_index) return 0;

    // Create one USBtingo according to the index
    auto serial = serial_vec.at(device_index);
    auto device = device::DeviceFactory::create(serial);

    // Check if the device object is valid
    if(!device) return 0;

    // Create a Bus object
    std::cout << "Create CAN Bus" << std::endl;
    auto bus = std::make_unique<bus::Bus>(std::move(device));

    // Register an observer that gets notified when new messages arrive
    MinimalLogicListener listener;
    bus->add_listener(reinterpret_cast<usbtingo::bus::LogicListener *>(&listener));

    // Start the logic data stream with the specified sample rate
    bus->start_logic_stream(samplerate_hz);

    // Do something until the logic stream should be stopped, e.g. wait 10 seconds ...
    std::this_thread::sleep_for(10s);

    // Stop the logic data stream
    bus->stop_logic_stream();

    return 1;
}

MinimalLogicListener.hpp

#pragma once

#include <usbtingo/bus/LogicListener.hpp>

using namespace usbtingo;

class MinimalLogicListener : public usbtingo::bus::LogicListener{
public:
    void on_logic_receive(const device::LogicFrame msg) override
    {
        // This callback is executed whenever a new logic frame is received.
        // Do something with the received data here, e.g. print it to the command line or save it to a file...
    }
};

About

C++ API for the USBtingo - USB to CAN-FD Interface

Resources

License

Stars

Watchers

Forks

Packages

No packages published