🔧 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! 🚀
- 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 - How to use the library
2.1 BasicBus
2.2 Bus
2.3 Device
2.4 DeviceFactory - Utility applications
- Minimal examples
4.1 Using the BasicBus
4.2 Using the Bus
4.3 Using the logic analyzer
- 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.
- 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-rulesYou 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.
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=ReleaseThe 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 theCMAKE_PREFIX_PATHenvironment 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 runningsudo ldconfigafter the installation.
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 optionsBUILD_SHARED_LIBS,BUILD_EXAMPLES,BUILD_UTILS,BUILD_TESTS,ENABLE_INTERACTIVE_TESTS,ENABLE_TESTS_WITH_OTHER_DEVICES, andUSE_WINAPIare deprecated and will show a warning. Use theUSBTINGO_*prefixed options instead.
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.
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.
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.
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
Deviceat the same time.
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.
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.
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.
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 ...
}
};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 ...
}
};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: 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
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...
}
};