Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HW]: add VSCAN hardware interface #528

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions hardware_integration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ if("NTCAN" IN_LIST CAN_DRIVER)
list(APPEND HARDWARE_INTEGRATION_SRC "ntcan_plugin.cpp")
list(APPEND HARDWARE_INTEGRATION_INCLUDE "ntcan_plugin.hpp")
endif()
if("VSCAN" IN_LIST CAN_DRIVER)
list(APPEND HARDWARE_INTEGRATION_SRC "vscan_plugin.cpp")
list(APPEND HARDWARE_INTEGRATION_INCLUDE "vscan_plugin.hpp")
endif()

# Prepend the source directory path to all the source files
prepend(HARDWARE_INTEGRATION_SRC ${HARDWARE_INTEGRATION_SRC_DIR}
Expand Down Expand Up @@ -348,6 +352,65 @@ if("NTCAN" IN_LIST CAN_DRIVER)
)
endif()
endif()
if("VSCAN" IN_LIST CAN_DRIVER)
if(MSVC)
# See https://gitlab.kitware.com/cmake/cmake/-/issues/15170
set(CMAKE_SYSTEM_PROCESSOR ${MSVC_CXX_ARCHITECTURE_ID})
endif()
if(DEFINED ENV{VSCAN_API_DIR})
target_include_directories(HardwareIntegration PUBLIC $ENV{VSCAN_API_DIR})
if(WIN32)
message(
AUTHOR_WARNING
"The VSCAN driver requires vs_can_api.dll to be in the same directory as the executable. You can find this file in the VSCAN API directory."
)
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_SYSTEM_PROCESSOR
STREQUAL "x64")
message(STATUS "Detected Windows x64, linking to VSCAN API x64 library")
target_link_libraries(HardwareIntegration
PRIVATE $ENV{VSCAN_API_DIR}/Win64/vs_can_api.lib)
elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" OR CMAKE_SYSTEM_PROCESSOR
STREQUAL "X86")
message(STATUS "Detected Windows x86, linking to VSCAN API x86 library")
target_link_libraries(HardwareIntegration
PRIVATE $ENV{VSCAN_API_DIR}/Win32/vs_can_api.lib)
else()
message(
FATAL_ERROR
"VSCAN Selected but no supported processor arch was detected. Only x64 and x86 are supported."
)
endif()
elseif(UNIX)
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_SYSTEM_PROCESSOR
STREQUAL "x64")
message(STATUS "Detected Linux x64, linking to VSCAN API x64 library")
target_link_libraries(HardwareIntegration
PRIVATE $ENV{VSCAN_API_DIR}/Linux/libvs_can_api.a)
elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" OR CMAKE_SYSTEM_PROCESSOR
STREQUAL "X86")
message(STATUS "Detected Linux x86, linking to VSCAN API x86 library")
target_link_libraries(
HardwareIntegration
PRIVATE "$ENV{VSCAN_API_DIR}/Linux x86-64/libvs_can_api.a")
else()
message(
FATAL_ERROR
"VSCAN Selected but no supported processor arch was detected. Only x64 and x86 are supported."
)
endif()
else()
message(
FATAL_ERROR
"VSCAN Selected but no supported OS was detected. Only Windows and Linux are supported currently."
)
endif()
else()
message(
FATAL_ERROR
"VSCAN Selected but no VSCAN API was found. Set the 'VSCAN_API_DIR' environment variable to the path of the VSCAN SDK."
)
endif()
endif()

# Mark the compiled CAN drivers available to other modules. In the form:
# `ISOBUS_<uppercase CAN_DRIVER>_AVAILABLE` as a preprocessor definition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@
#include "isobus/hardware_integration/ntcan_plugin.hpp"
#endif

#ifdef ISOBUS_VSCAN_AVAILABLE
#include "isobus/hardware_integration/vscan_plugin.hpp"
#endif

#endif // AVAILABLE_CAN_DRIVERS_HPP
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//================================================================================================
/// @file vscan_plugin.hpp
///
/// @brief An interface for using a VSCOM VSCAN driver.
/// @attention Use of the VSCAN driver is governed in part by their license, and requires you
/// to install their driver first, which in-turn requires you to agree to their terms and conditions.
/// @author Daan Steenbergen
///
/// @copyright 2025 The Open-Agriculture Developers
//================================================================================================
#ifndef VSCAN_PLUGIN_HPP
#define VSCAN_PLUGIN_HPP

#include <string>
#include "vs_can_api.h"
ad3154 marked this conversation as resolved.
Show resolved Hide resolved

#include "isobus/hardware_integration/can_hardware_plugin.hpp"
#include "isobus/isobus/can_hardware_abstraction.hpp"
#include "isobus/isobus/can_message_frame.hpp"

namespace isobus
{
/// @brief A CAN Driver for VSCOM VSCAN Devices
class VSCANPlugin : public CANHardwarePlugin
{
public:
/// @brief Constructor for the VSCOM VSCAN CAN driver
/// @param[in] channel The COM port or IP address of the VSCAN device to use.
/// @param[in] baudrate The baudrate to use for the CAN connection.
VSCANPlugin(const std::string &channel, void *baudrate = VSCAN_SPEED_250K);

/// @brief The destructor for VSCANPlugin
virtual ~VSCANPlugin() = default;

/// @brief Returns if the connection with the hardware is valid
/// @returns `true` if connected, `false` if not connected
bool get_is_valid() const override;

/// @brief Closes the connection to the hardware
void close() override;

/// @brief Connects to the hardware you specified in the constructor's channel argument
void open() override;

/// @brief Returns a frame from the hardware (synchronous), or `false` if no frame can be read.
/// @param[in, out] canFrame The CAN frame that was read
/// @returns `true` if a CAN frame was read, otherwise `false`
bool read_frame(isobus::CANMessageFrame &canFrame) override;

/// @brief Writes a frame to the bus (synchronous)
/// @param[in] canFrame The frame to write to the bus
/// @returns `true` if the frame was written, otherwise `false`
bool write_frame(const isobus::CANMessageFrame &canFrame) override;

/// @brief Changes previously set configuration parameters. Only works if the device is not open.
/// @param[in] channel The COM port or IP address of the VSCAN device to use.
/// @param[in] baudrate The baudrate to use for the CAN connection.
/// @returns True if the configuration was changed, otherwise false (if the device is open false will be returned)
bool reconfigure(const std::string &channel, void *baudrate = VSCAN_SPEED_250K);

private:
/// @brief Parses the error from the status code
/// @param[in] status The status code to parse
/// @returns The error message
static std::string parse_error_from_status(VSCAN_STATUS status);

std::string channel; ///< The COM port or IP address of the VSCAN device to use.
void *baudrate; ///< The baudrate to use for the CAN connection.
ad3154 marked this conversation as resolved.
Show resolved Hide resolved
VSCAN_HANDLE handle; ///< The handle as defined in the NTCAN driver API
VSCAN_STATUS status = VSCAN_ERR_OK; ///< Stores the result of the call to begin CAN communication. Used for is_valid check later.
};
}

#endif // NTCAN_PLUGIN_HPP
1 change: 1 addition & 0 deletions hardware_integration/src/ntcan_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ namespace isobus
if (NTCAN_NO_HANDLE != handle)
{
isobus::CANStackLogger::error("[NTCAN]: Attempting to open a connection that is already open");
return;
}
std::uint32_t mode = 0;
std::int32_t txQueueSize = 8;
Expand Down
169 changes: 169 additions & 0 deletions hardware_integration/src/vscan_plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//================================================================================================
/// @file vscan_plugin.cpp
///
/// @brief An interface for using a VSCOM VSCAN driver.
/// @attention Use of the VSCAN driver is governed in part by their license, and requires you
/// to install their driver first, which in-turn requires you to agree to their terms and conditions.
/// @author Daan Steenbergen
///
/// @copyright 2025 The Open-Agriculture Developers
//================================================================================================

#include "isobus/hardware_integration/vscan_plugin.hpp"
#include "isobus/isobus/can_stack_logger.hpp"

#include <chrono>
#include <cstring>
#include <thread>

namespace isobus
{
VSCANPlugin::VSCANPlugin(const std::string &channel, void *baudrate) :
channel(channel),
baudrate(baudrate)
{
}

bool VSCANPlugin::get_is_valid() const
{
return (VSCAN_ERR_OK == status) && (handle > 0);
}

void VSCANPlugin::close()
{
VSCAN_Close(handle);
handle = 0;
}

void VSCANPlugin::open()
{
if (get_is_valid())
{
LOG_ERROR("[VSCAN]: Attempting to open a connection that is already open");
return;
}

VSCAN_API_VERSION version;
status = VSCAN_Ioctl(0, VSCAN_IOCTL_GET_API_VERSION, &version);
if (status != VSCAN_ERR_OK)
{
LOG_ERROR("[VSCAN] Failed to get API version: %s, trying to continue anyway", parse_error_from_status(status).c_str());
}

LOG_DEBUG("[VSCAN] API Version %d.%d.%d", version.Major, version.Minor, version.SubMinor);

// We create a buffer to guarantee the content to be non-const
std::vector<char> channelBuffer(channel.begin(), channel.end());
channelBuffer.push_back('\0');
handle = VSCAN_Open(channelBuffer.data(), VSCAN_MODE_NORMAL);
if (handle <= 0)
{
LOG_ERROR("[VSCAN]: Error trying to open the connection: %s", parse_error_from_status(handle).c_str());
return;
}

status = VSCAN_Ioctl(handle, VSCAN_IOCTL_SET_SPEED, baudrate);
if (VSCAN_ERR_OK != status)
{
LOG_ERROR("[VSCAN]: Error trying to set the baudrate: %s", parse_error_from_status(status).c_str());
close();
return;
}

status = VSCAN_Ioctl(handle, VSCAN_IOCTL_SET_BLOCKING_READ, VSCAN_IOCTL_ON);
if (VSCAN_ERR_OK != status)
{
LOG_ERROR("[VSCAN]: Error trying to set blocking read mode: %s", parse_error_from_status(status).c_str());
close();
return;
}
}

bool VSCANPlugin::read_frame(CANMessageFrame &canFrame)
{
VSCAN_MSG message;
DWORD rv;
bool retVal = false;

status = VSCAN_Read(handle, &message, 1, &rv);

if (VSCAN_ERR_OK == status && rv)
{
canFrame.dataLength = message.Size;
memcpy(canFrame.data, message.Data, message.Size);
canFrame.identifier = message.Id;
canFrame.isExtendedFrame = (message.Flags & VSCAN_FLAGS_EXTENDED);
retVal = true;
}
else
{
LOG_ERROR("[VSCAN]: Error trying to read a frame: %s, closing connection", parse_error_from_status(status).c_str());
close();
}

return retVal;
}

bool VSCANPlugin::write_frame(const CANMessageFrame &canFrame)
{
VSCAN_MSG message;
DWORD rv;
bool retVal = false;

message.Id = canFrame.identifier;
message.Size = canFrame.dataLength;
memcpy(message.Data, canFrame.data, message.Size);
message.Flags = canFrame.isExtendedFrame ? VSCAN_FLAGS_EXTENDED : VSCAN_FLAGS_STANDARD;

status = VSCAN_Write(handle, &message, 1, &rv);

if (VSCAN_ERR_OK == status && rv)
{
status = VSCAN_Flush(handle);
if (VSCAN_ERR_OK == status)
{
retVal = true;
}
else
{
LOG_ERROR("[VSCAN]: Error trying to flush the write buffer: %s, closing connection", parse_error_from_status(status).c_str());
close();
}
}
else
{
LOG_ERROR("[VSCAN]: Error trying to write a frame: %s, closing connection", parse_error_from_status(status).c_str());
close();
}

return retVal;
}

bool VSCANPlugin::reconfigure(const std::string &channel, void *baudrate)
{
bool retVal = false;

if (!get_is_valid())
{
this->channel = channel;
this->baudrate = baudrate;
retVal = true;
}
return retVal;
}

std::string VSCANPlugin::parse_error_from_status(VSCAN_STATUS status)
{
// Arbitrary buffer size, should be enough for most error messages
size_t bufferSize = 256;
std::vector<char> errorBuffer(bufferSize, 0);

VSCAN_GetErrorString(status, errorBuffer.data(), bufferSize);

// Ensure the string is null-terminated, just in case
errorBuffer[bufferSize - 1] = '\0';

return std::string(errorBuffer.data());
}
}
// namespace isobus
1 change: 1 addition & 0 deletions sphinx/source/api/hardware/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ When compiling with CMake, a default CAN driver plug-in will be selected for you
- :code:`-DCAN_DRIVER=TouCAN` for the Rusoku TouCAN (Windows)
- :code:`-DCAN_DRIVER=SYS_TEC` for a SYS TEC sysWORXX USB CAN adapter (Windows)
- :code:`-DCAN_DRIVER=NTCAN` for the NTCAN driver (Windows)
- :code:`-DCAN_DRIVER=VSCAN` for the VSCAN made by VSCOM (Windows/Linux)

Or specify multiple using a semicolon separated list: :code:`-DCAN_DRIVER="<driver1>;<driver2>"`

Expand Down
Loading