diff --git a/CMakeLists.txt b/CMakeLists.txt index bf8de3e0..51a92c43 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,13 @@ if (UNIX) src/main/CanVendorSocketCan.cpp src/main/CanVendorSocketCanSystec.cpp ) +elseif ("${CANMODULE_BUILD_SYSTEC_WINDOWS}" STREQUAL ON) + include(cmake/systec.cmake) + include_directories(${SYSTEC_PATH_HEADERS}) + list(APPEND VENDOR_SOURCES + src/main/CanVendorSystec.cpp + ) + add_compile_definitions(CANMODULE_BUILD_SYSTEC_WINDOWS) endif() if (NOT DEFINED CAN_MODULE_MAIN_ONLY) @@ -63,11 +70,18 @@ if (UNIX) libsocketcan ) else() + target_include_directories(CanModuleMain PUBLIC ${systec_BINARY_DIR}/Examples/Include) target_link_libraries(CanModuleMain PUBLIC - ${anagate_SOURCE_DIR}/Win64/AnaGateCanDll64.lib - ) + ${anagate_SOURCE_DIR}/Win64/AnaGateCanDll64.lib) file(COPY "${anagate_SOURCE_DIR}/Win64/AnaGateCan64.dll" DESTINATION "${CMAKE_BINARY_DIR}/Release") + + if ("${CANMODULE_BUILD_SYSTEC_WINDOWS}" STREQUAL ON) + target_link_libraries(CanModuleMain PUBLIC + ${systec_BINARY_DIR}/Examples/lib/USBCAN64.lib) + file(COPY "${systec_BINARY_DIR}/Examples/lib/USBCAN64.dll" + DESTINATION "${CMAKE_BINARY_DIR}/Release") + endif() endif() if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") diff --git a/cmake/systec.cmake b/cmake/systec.cmake new file mode 100644 index 00000000..451d83a5 --- /dev/null +++ b/cmake/systec.cmake @@ -0,0 +1,40 @@ +include(FetchContent) + +if(NOT DEFINED SYSTEC_LIBRARY) + set(SYSTEC_LIBRARY "https://www.systec-electronic.com/media/default/Redakteur/produkte/Interfaces_Gateways/sysWORXX_USB_CANmodul_Series/Downloads/SO-387.zip") +endif() + +set(SYSTEC_FETCHCONTENT_ARGS + URL "${SYSTEC_LIBRARY}" + DOWNLOAD_EXTRACT_TIMESTAMP True +) + +if(EXISTS "${SYSTEC_LIBRARY}") + message(STATUS "Using local Systec archive: ${SYSTEC_LIBRARY}") +elseif(SYSTEC_LIBRARY MATCHES "^https?://") + message(STATUS "Downloading Systec archive: ${SYSTEC_LIBRARY}") +else() + message(FATAL_ERROR "SYSTEC_LIBRARY must be an existing local archive or an http(s) URL. Got: ${SYSTEC_LIBRARY}") +endif() + +FetchContent_Declare( + Systec + ${SYSTEC_FETCHCONTENT_ARGS} +) + +FetchContent_MakeAvailable(Systec) + +execute_process( + COMMAND ${systec_SOURCE_DIR}/SO-387.exe /SP- /VERYSILENT /DIR=${systec_BINARY_DIR} /LOG=${systec_SOURCE_DIR}/build.log + WORKING_DIRECTORY ${systec_SOURCE_DIR} + RESULT_VARIABLE systec_build_result + OUTPUT_VARIABLE systec_build_output +) + +if(NOT systec_build_result EQUAL 0) + message(FATAL_ERROR "Error installing USB-CANmodul Utility Disk: ${systec_build_output}") +endif() + +if (WIN32) + add_compile_definitions(WIN32) +endif() diff --git a/docs/CANMODULE-UTILS.md b/docs/CANMODULE-UTILS.md index ca36c066..8ce7db1f 100644 --- a/docs/CANMODULE-UTILS.md +++ b/docs/CANMODULE-UTILS.md @@ -6,7 +6,9 @@ The tool is loosely based on `can-utils` for SocketCAN on Linux. ## Installation -To use this tool, ensure you have a recent version of Python installed (tested with version 3.9.18). Additionally, you need to have `libsocketcan` installed on your Linux system. +To use this tool, ensure you have a recent version of Python installed (tested with version 3.9.18). +Additionally, you need to have `libsocketcan` installed on your Linux system, or `USB-CANmodul Utility Disk` +on Windows to communicate with Systec devices. You will also require the `canmodule.cpython*` file and all Anagate-related libraries (such as `.so` files on Linux or `.dll` files on Windows). These files can be found in the build artifacts, located in the `/build` directory on Linux and the `/build/Release` directory on Windows. @@ -19,6 +21,7 @@ It opens a connection to the device and print all received frames. The syntax is ```bash python canmodule-utils.py anagate [host] [port number] dump python canmodule-utils.py socketcan [device] dump +python canmodule-utils.py systec [device] dump ``` ### send @@ -28,6 +31,7 @@ It sends a single CAN frame. The syntax is: ```bash python canmodule-utils.py anagate [host] [port number] send [can_frame] python canmodule-utils.py socketcan [device] send [can_frame] +python canmodule-utils.py systec [device] send [can_frame] ``` Examples of CAN frames are: @@ -47,6 +51,7 @@ It opens a connection and send random frames. The syntax is: ```bash python canmodule-utils.py anagate [host] [port number] gen python canmodule-utils.py socketcan [device] gen +python canmodule-utils.py systec [device] gen ``` ### diag @@ -54,6 +59,7 @@ python canmodule-utils.py socketcan [device] gen It opens a connection and print the diagnostics. The syntax is: ```bash -python python canmodule-utils.py.py anagate [host] [port number] diag -python python canmodule-utils.py.py socketcan [device] diag +python canmodule-utils.py anagate [host] [port number] diag +python canmodule-utils.py socketcan [device] diag +python canmodule-utils.py systec [device] diag ``` diff --git a/python/canmodule-utils.py b/python/canmodule-utils.py index 7a8d9f37..0debf638 100644 --- a/python/canmodule-utils.py +++ b/python/canmodule-utils.py @@ -10,7 +10,7 @@ def parse_arguments(): Parses command-line arguments for the CAN Module Utilities. The function uses argparse to define and parse command-line arguments for different CAN module operations. - It supports two CAN modules: Anagate and SocketCAN. Each module has its own subcommands for different actions, + It supports Anagate, SocketCAN on Linux and Systec on Windows. Each module has its own subcommands for different actions, such as dumping frames, generating random frames, sending specified frames, and printing diagnostics. Parameters: @@ -35,15 +35,6 @@ def parse_arguments(): dest="action", required=True, help="Anagate actions" ) - anagate_subparsers.add_parser("dump", help="Dump frames") - anagate_subparsers.add_parser("diag", help="Print diagnostics") - anagate_subparsers.add_parser("gen", help="Generate and send random frames") - - anagate_send_parser = anagate_subparsers.add_parser( - "send", help="Send a specified frame" - ) - anagate_send_parser.add_argument("can_frame", type=str, help="CAN frame to send") - # socketcan subparser socketcan_parser = subparsers.add_parser( "socketcan", help="Use the SocketCAN module" @@ -54,14 +45,27 @@ def parse_arguments(): dest="action", required=True, help="SocketCAN actions" ) - socketcan_subparsers.add_parser("dump", help="Dump frames") - socketcan_subparsers.add_parser("diag", help="Print diagnostics") - socketcan_subparsers.add_parser("gen", help="Generate and send random frames") + # systec subparser + systec_parser = subparsers.add_parser("systec", help="Use the Systec module") + systec_parser.add_argument("device", type=int, help="Bus number for Systec") + systec_parser.add_argument( + "baudrate", + type=int, + default=125000, + help="Baud rate for Systec CAN channel", + nargs="?", + ) - socketcan_send_parser = socketcan_subparsers.add_parser( - "send", help="Send a specified frame" + systec_subparsers = systec_parser.add_subparsers( + dest="action", required=True, help="systec actions" ) - socketcan_send_parser.add_argument("can_frame", type=str, help="CAN frame to send") + + for subparsers in [anagate_subparsers, socketcan_subparsers, systec_subparsers]: + subparsers.add_parser("dump", help="Dump frames") + subparsers.add_parser("diag", help="Print diagnostics") + subparsers.add_parser("gen", help="Generate and send random frames") + send_parser = subparsers.add_parser("send", help="Send a specified frame") + send_parser.add_argument("can_frame", type=str, help="CAN frame to send") return parser.parse_args() @@ -96,6 +100,13 @@ def main(): "device": args.device, } ) + elif args.command == "systec": + device.update( + { + "device": args.device, + "bitrate": args.baudrate, + } + ) if args.action == "dump": dump(device) @@ -173,17 +184,22 @@ def process_device(device): Processes the device dictionary to create a CanDeviceConfiguration object. This function takes a dictionary representing a CAN device and creates a CanDeviceConfiguration object - based on the provided device information. The function supports two types of CAN devices: Anagate and SocketCAN. + based on the provided device information. The function supports several CAN devices: Anagate, SocketCAN + on Linux and Systec on Windows. For Anagate devices, the function sets the host and bus number in the configuration object. For SocketCAN devices, - the function sets the bus name in the configuration object. If an unsupported vendor is provided, the function + the function sets the bus name in the configuration object. For Systec devices, the function sets the + bus number in the configuration object. If an unsupported vendor is provided, the function prints an error message and exits with a status code of 1. Parameters: device (dict): A dictionary representing the CAN device. The dictionary should contain the following keys: - - "vendor" (str): The vendor of the CAN device. It can be either "anagate" or "socketcan". + - "vendor" (str): The vendor of the CAN device. It can be either "anagate", + "socketcan" on Linux or "systec" on Windows. - "host" (str): The host address for Anagate devices. - "port" (int): The port number for Anagate devices. - - "device" (str): The device name for SocketCAN devices. + - "device" (str): The device name for SocketCAN devices + or device number of Systec devices. + - "baudrate" (int): The baud rate for Systec devices. Returns: CanDeviceConfiguration: A CanDeviceConfiguration object configured based on the provided device information. @@ -194,6 +210,9 @@ def process_device(device): configuration.bus_number = device["port"] elif device["vendor"] == "socketcan": configuration.bus_name = device["device"] + elif device["vendor"] == "systec": + configuration.bus_number = device["device"] + configuration.bitrate = device["bitrate"] else: print(f"Unsupported vendor: {device['vendor']}") exit(1) @@ -206,10 +225,13 @@ def dump(device): Parameters: device (dict): A dictionary representing the CAN device. The dictionary should contain the following keys: - - "vendor" (str): The vendor of the CAN device. It can be either "anagate" or "socketcan". + - "vendor" (str): The vendor of the CAN device. It can be either "anagate", + "socketcan" on Linux or "systec" on Windows. - "host" (str): The host address for Anagate devices. - "port" (int): The port number for Anagate devices. - - "device" (str): The device name for SocketCAN devices. + - "device" (str): The device name for SocketCAN devices + or device number of Systec devices. + - "baudrate" (int): The baud rate for Systec devices. Returns: None @@ -277,10 +299,13 @@ def send(device, frame): Parameters: device (dict): A dictionary representing the CAN device. The dictionary should contain the following keys: - - "vendor" (str): The vendor of the CAN device. It can be either "anagate" or "socketcan". + - "vendor" (str): The vendor of the CAN device. It can be either "anagate", + "socketcan" on Linux or "systec" on Windows. - "host" (str): The host address for Anagate devices. - "port" (int): The port number for Anagate devices. - - "device" (str): The device name for SocketCAN devices. + - "device" (str): The device name for SocketCAN devices + or device number of Systec devices. + - "baudrate" (int): The baud rate for Systec devices. frame (dict): A dictionary representing the CAN frame to be sent. The dictionary should contain the following keys: - "can_id" (str): The hexadecimal representation of the CAN identifier. - "len" (int): The number of data bytes for a remote request frame. @@ -347,10 +372,13 @@ def gen(device): Parameters: device (dict): A dictionary representing the CAN device. The dictionary should contain the following keys: - - "vendor" (str): The vendor of the CAN device. It can be either "anagate" or "socketcan". + - "vendor" (str): The vendor of the CAN device. It can be either "anagate", + "socketcan" on Linux or "systec" on Windows. - "host" (str): The host address for Anagate devices. - "port" (int): The port number for Anagate devices. - - "device" (str): The device name for SocketCAN devices. + - "device" (str): The device name for SocketCAN devices + or device number of Systec devices. + - "baudrate" (int): The baud rate for Systec devices. Returns: None @@ -385,10 +413,13 @@ def diag(device): Parameters: device (dict): A dictionary representing the CAN device. The dictionary should contain the following keys: - - "vendor" (str): The vendor of the CAN device. It can be either "anagate" or "socketcan". + - "vendor" (str): The vendor of the CAN device. It can be either "anagate", + "socketcan" on Linux or "systec" on Windows. - "host" (str): The host address for Anagate devices. - "port" (int): The port number for Anagate devices. - - "device" (str): The device name for SocketCAN devices. + - "device" (str): The device name for SocketCAN devices + or device number of Systec devices. + - "baudrate" (int): The baud rate for Systec devices. Returns: None diff --git a/src/include/CanDeviceConfiguration.h b/src/include/CanDeviceConfiguration.h index 93731a99..3fb60acf 100644 --- a/src/include/CanDeviceConfiguration.h +++ b/src/include/CanDeviceConfiguration.h @@ -95,6 +95,17 @@ struct CanDeviceConfiguration { */ std::optional sent_acknowledgement; + /** + * @brief Enable or disable closing both channels on systec module when one is + * closed. + * + * This parameter is optional for Systec on Windows and defaults to false. + * If turned on, calling close on a Systec CanDevice will deintialise both + * channels of a module when one is closed. + * + */ + std::optional close_both_channels; + std::string to_string() const noexcept; }; diff --git a/src/include/CanDiagnostics.h b/src/include/CanDiagnostics.h index a0218620..4b44fc18 100644 --- a/src/include/CanDiagnostics.h +++ b/src/include/CanDiagnostics.h @@ -47,7 +47,8 @@ struct CanDiagnostics { std::optional temperature; ///< Optional temperature reading for Anagate devices. - std::optional uptime; ///< Optional uptime for Anagate devices. + std::optional uptime; ///< Optional uptime in seconds for Anagate + ///< and Systec for Windows. std::optional tcp_rx; ///< Optional TCP Received counter for ///< both SocketCAN and Anagate devices. diff --git a/src/include/CanVendorSystec.h b/src/include/CanVendorSystec.h new file mode 100644 index 00000000..b8e9b22b --- /dev/null +++ b/src/include/CanVendorSystec.h @@ -0,0 +1,74 @@ +#ifndef SRC_INCLUDE_CANVENDORSYSTEC_H_ +#define SRC_INCLUDE_CANVENDORSYSTEC_H_ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include //NOLINT +#include +#include +#include +#include + +#include "CanDevice.h" +#include "CanDiagnostics.h" +#include "CanVendorLoopback.h" + +/** + * @struct CanVendorSystec + * @brief Represents a specific implementation of a CanDevice for Systec devices + * on Windows utilising libraries from USB-CANmodul Utility Disk. + * + * This struct provides methods for opening, closing, sending, and receiving CAN + * frames using the Systec CAN-over-USB interface. It also provides diagnostics + * information. + */ +struct CanVendorSystec : CanDevice { + explicit CanVendorSystec(const CanDeviceArguments& args); + ~CanVendorSystec() { vendor_close(); } + int SystecRxThread(); + static std::string_view UsbCanGetErrorText(uint16_t err_code); + static std::string UsbCanGetStatusText(uint16_t err_code); + + private: + std::atomic m_module_in_use{false}; + std::atomic m_queued_reads; + int m_module_number; + int m_channel_number; + int m_port_number; + DWORD m_baud_rate; + std::thread m_SystecRxThread; + + std::optional get_module_handle() { + if (auto mapping = m_module_to_handle_map.find(m_module_number); + mapping != m_module_to_handle_map.end()) { + return mapping->second; + } + return std::nullopt; + } + + CanReturnCode vendor_open() noexcept override; + CanReturnCode vendor_close() noexcept override; + CanReturnCode vendor_send(const CanFrame& frame) noexcept override; + CanDiagnostics vendor_diagnostics() noexcept override; + + CanReturnCode init_can_port(); + static std::mutex m_handles_lock; + static std::unordered_map m_module_to_handle_map; + static std::unordered_map m_port_to_vendor_map; + + friend void systec_receive(tUcanHandle UcanHandle_p, DWORD bEvent_p, + BYTE bChannel_p, void* pArg_p); + + CanReturnCode deinit_channel(tUcanHandle handle) noexcept; + CanReturnCode deinit_other_channel(tUcanHandle handle, + CanVendorSystec* other) noexcept; +}; + +#endif // SRC_INCLUDE_CANVENDORSYSTEC_H_ diff --git a/src/main/CanDevice.cpp b/src/main/CanDevice.cpp index 204b70ea..34d6d816 100644 --- a/src/main/CanDevice.cpp +++ b/src/main/CanDevice.cpp @@ -13,6 +13,8 @@ #ifndef _WIN32 #include "CanVendorSocketCan.h" #include "CanVendorSocketCanSystec.h" +#elif defined(CANMODULE_BUILD_SYSTEC_WINDOWS) +#include "CanVendorSystec.h" #endif /** @@ -163,6 +165,11 @@ std::unique_ptr CanDevice::create( LOG(Log::DBG, CanLogIt::h()) << "Creating SocketCAN Systec CAN device"; return std::make_unique(configuration); } +#elif defined(CANMODULE_BUILD_SYSTEC_WINDOWS) + if (vendor == "systec") { + LOG(Log::DBG, CanLogIt::h()) << "Creating Systec CAN device for Windows"; + return std::make_unique(configuration); + } #endif if (vendor == "anagate") { diff --git a/src/main/CanVendorSystec.cpp b/src/main/CanVendorSystec.cpp new file mode 100644 index 00000000..2129f6ff --- /dev/null +++ b/src/main/CanVendorSystec.cpp @@ -0,0 +1,653 @@ +#include "CanVendorSystec.h" + +#include +#include + +#include +#include +#include +#include +#include + +std::mutex CanVendorSystec::m_handles_lock; +std::unordered_map CanVendorSystec::m_module_to_handle_map; +std::unordered_map CanVendorSystec::m_port_to_vendor_map; +static bool module_control_callback_registered = false; + +template +uint16_t CallAndLog(T f, const char* name, Args... args) { + uint16_t code = f(args...); + if (code != USBCAN_SUCCESSFUL) + LOG(Log::ERR, CanLogIt::h()) << "Got error code calling " << name << ": " + << CanVendorSystec::UsbCanGetErrorText(code); + return code; +} + +void connect_control_callback(BYTE bEvent_p, DWORD dwParam_p) { + switch (bEvent_p) { + case USBCAN_EVENT_CONNECT: + LOG(Log::DBG) << "USB CAN module connected"; + break; + case USBCAN_EVENT_DISCONNECT: + LOG(Log::WRN) << "USB CAN module disconnected"; + break; + case USBCAN_EVENT_FATALDISCON: + LOG(Log::ERR) << "USB CAN module with handle " + << static_cast(dwParam_p) << "fatally disconnected"; + break; + } +} + +// Callback registered per-module to handle receive events +void systec_receive(tUcanHandle UcanHandle_p, DWORD bEvent_p, BYTE bChannel_p, + void* pArg_p) { + if (bEvent_p == USBCAN_EVENT_RECEIVE) { + int module_number = *(reinterpret_cast(pArg_p)); + int port_number = 2 * module_number + bChannel_p; + CanVendorSystec* vendorPtr = + CanVendorSystec::m_port_to_vendor_map[port_number]; + if (vendorPtr) + ++(vendorPtr->m_queued_reads); // [] returns nullptr if not found in map; + } +} + +CanVendorSystec::CanVendorSystec(const CanDeviceArguments& args) + : CanDevice("systec", args), m_queued_reads{0} { + if (!args.config.bus_number.has_value() || !args.config.bitrate.has_value()) { + throw std::invalid_argument("Missing required configuration parameters"); + } + + switch (args.config.bitrate.value()) { + case 50000: + m_baud_rate = USBCAN_BAUD_50kBit; + break; + case 100000: + m_baud_rate = USBCAN_BAUD_100kBit; + break; + case 125000: + m_baud_rate = USBCAN_BAUD_125kBit; + break; + case 250000: + m_baud_rate = USBCAN_BAUD_250kBit; + break; + case 500000: + m_baud_rate = USBCAN_BAUD_500kBit; + break; + case 1000000: + m_baud_rate = USBCAN_BAUD_1MBit; + break; + default: { + throw std::invalid_argument("Invalid bitrate provided"); + } + } + + m_port_number = args.config.bus_number.value(); + m_module_number = m_port_number / 2; + m_channel_number = m_port_number % 2; + m_port_to_vendor_map[m_port_number] = this; +} + +CanReturnCode CanVendorSystec::init_can_port() { + BYTE systec_call_return = USBCAN_SUCCESSFUL; + tUcanHandle can_module_handle; + + tUcanInitCanParam init_params; + init_params.m_dwSize = sizeof(init_params); // size of this struct + init_params.m_bMode = kUcanModeNormal; // normal operation mode + init_params.m_bBTR0 = HIBYTE(m_baud_rate); // baudrate + init_params.m_bBTR1 = LOBYTE(m_baud_rate); + init_params.m_bOCR = 0x1A; // standard output + init_params.m_dwAMR = USBCAN_AMR_ALL; // receive all CAN messages + init_params.m_dwACR = USBCAN_ACR_ALL; + init_params.m_dwBaudrate = USBCAN_BAUDEX_USE_BTR01; + init_params.m_wNrOfRxBufferEntries = USBCAN_DEFAULT_BUFFER_ENTRIES; + init_params.m_wNrOfTxBufferEntries = USBCAN_DEFAULT_BUFFER_ENTRIES; + + // check if USB-CANmodul is already initialized + std::lock_guard guard(CanVendorSystec::m_handles_lock); + auto handle = get_module_handle(); + if (!handle.has_value()) { // module not in use + if (!module_control_callback_registered) { + if (!CallAndLog(UcanInitHwConnectControl, "hw connect control callback", + connect_control_callback)) + module_control_callback_registered = true; + } + if (auto systec_code = + CallAndLog(UcanInitHardwareEx, "init hardware", &can_module_handle, + m_module_number, systec_receive, + reinterpret_cast(&m_module_number)); + systec_code != 0) { + CallAndLog(UcanDeinitHardware, "deinit hardware", can_module_handle); + return CanReturnCode::unknown_open_error; + } + LOG(Log::INF, CanLogIt::h()) + << "Initialised hardware for Systec module " << m_module_number; + m_module_to_handle_map[m_module_number] = can_module_handle; + } else { // find existing handle of module + can_module_handle = handle.value(); + LOG(Log::INF, CanLogIt::h()) + << "Reusing handle (" << static_cast(can_module_handle) + << ") for module " << m_module_number + << " already in use, skipping UCanInitHardwareEx"; + } + + if (CallAndLog(UcanInitCanEx2, "init channel", can_module_handle, + m_channel_number, &init_params)) { + deinit_channel(can_module_handle); + return CanReturnCode::unknown_open_error; + } + + // investigate the minimum amount of things to reset to restore good state + CallAndLog(UcanResetCanEx, "reset channel", can_module_handle, + (BYTE)m_channel_number, (DWORD)0); + + LOG(Log::INF, CanLogIt::h()) + << "Successfully opened CAN port on module " << m_module_number + << ", channel " << m_channel_number; + return CanReturnCode::success; +} + +CanReturnCode CanVendorSystec::vendor_open() noexcept { + CanReturnCode return_code = CanReturnCode::unknown_open_error; + + try { + return_code = init_can_port(); + if (return_code != CanReturnCode::success) return return_code; + m_module_in_use = true; + m_SystecRxThread = std::thread(&CanVendorSystec::SystecRxThread, this); + } catch (...) { + return_code = CanReturnCode::internal_api_error; + } + + return return_code; +} + +CanReturnCode CanVendorSystec::vendor_close() noexcept { + auto return_code = CanReturnCode::success; + std::lock_guard guard(CanVendorSystec::m_handles_lock); + + bool other_in_use = false; + CanVendorSystec* other = nullptr; + // toggle last bit to get the other port on the same module + // e.g. if channel 0, we need to check if channel 1 is in use and vice versa + if (auto mapping = m_port_to_vendor_map.find(m_port_number ^ 1); + mapping != m_port_to_vendor_map.end()) { + other = mapping->second; + other_in_use = other->m_module_in_use; + } + + bool close_both_channels = args().config.close_both_channels.value_or(false); + + try { + m_module_in_use = false; + if (m_SystecRxThread.joinable()) m_SystecRxThread.join(); + + auto handle = get_module_handle(); + if (!handle.has_value()) { + LOG(Log::WRN, CanLogIt::h()) + << "No handle found for module, close() may have already been called"; + return CanReturnCode::success; // is success correct? + } + + if (close_both_channels && other_in_use) { + LOG(Log::WRN, CanLogIt::h()) << "Deinitialising other channel " + << other->m_channel_number << " on module."; + return_code = deinit_other_channel(handle.value(), other); + } + return_code = deinit_channel(handle.value()); + + // if there are no channels still using the handle, deinit hardware + // and erase handle from map + if (!other_in_use || close_both_channels) { + if (auto systec_code = + CallAndLog(UcanDeinitHardware, "deinit hw", handle.value()); + systec_code != 0) + return_code = CanReturnCode::unknown_close_error; + m_module_to_handle_map.erase(m_module_number); + } + } catch (...) { + return_code = CanReturnCode::internal_api_error; + } + return return_code; +}; + +CanReturnCode CanVendorSystec::deinit_channel(tUcanHandle handle) noexcept { + auto internal_return_code = CanReturnCode::success; + if (CallAndLog(UcanDeinitCanEx, "deinit channel", handle, m_channel_number)) + internal_return_code = CanReturnCode::unknown_close_error; + return internal_return_code; +} + +CanReturnCode CanVendorSystec::deinit_other_channel( + tUcanHandle handle, CanVendorSystec* other) noexcept { + auto internal_return_code = CanReturnCode::success; + if (CallAndLog(UcanDeinitCanEx, "deinit channel", handle, + other->m_channel_number)) + internal_return_code = CanReturnCode::unknown_close_error; + other->m_module_in_use = false; + return internal_return_code; +} + +CanReturnCode CanVendorSystec::vendor_send(const CanFrame& frame) noexcept { + std::vector message = frame.message(); + + tCanMsgStruct can_msg_to_send; + + can_msg_to_send.m_dwID = frame.id(); + can_msg_to_send.m_bDLC = frame.length(); + can_msg_to_send.m_bFF = 0; + if (frame.is_remote_request()) { + can_msg_to_send.m_bFF = USBCAN_MSG_FF_RTR; + } + + std::copy(message.begin(), message.begin() + can_msg_to_send.m_bDLC, + can_msg_to_send.m_bData); + + auto handle = get_module_handle(); + if (!handle.has_value()) { + LOG(Log::ERR, CanLogIt::h()) + << "Could not send message, no handle found for module " + << m_module_number; + return CanReturnCode::disconnected; + } + switch (CallAndLog(UcanWriteCanMsgEx, "write", handle.value(), + m_channel_number, &can_msg_to_send, nullptr)) { + case USBCAN_SUCCESSFUL: + break; + case USBCAN_ERR_CANNOTINIT: + case USBCAN_ERR_ILLHANDLE: + return CanReturnCode::disconnected; + case USBCAN_ERR_DLL_TXFULL: + return CanReturnCode::tx_buffer_overflow; + case USBCAN_ERR_MAXINSTANCES: + return CanReturnCode::too_many_connections; + case USBCAN_ERR_ILLPARAM: + case USBCAN_ERR_ILLHW: + case USBCAN_ERR_ILLCHANNEL: + case USBCAN_WARN_TXLIMIT: + case USBCAN_WARN_FW_TXOVERRUN: + default: + return CanReturnCode::unknown_send_error; + } + return CanReturnCode::success; +}; + +CanDiagnostics CanVendorSystec::vendor_diagnostics() noexcept { + CanDiagnostics diagnostics{}; + tStatusStruct status; + auto handle = get_module_handle(); + diagnostics.log_entries = std::vector(); + if (!handle.has_value()) { + LOG(Log::ERR, CanLogIt::h()) + << "Could not get diagnostics as no handle found for module " + << m_module_number; + diagnostics.mode = "OFFLINE"; + } else { + auto handle_value = handle.value(); + CallAndLog(UcanGetStatusEx, "get status", handle_value, m_channel_number, + &status); + WORD can_status = status.m_wCanStatus; + diagnostics.state = UsbCanGetStatusText(can_status); + if (can_status) { + diagnostics.log_entries.value().push_back(diagnostics.state.value()); + } + + tUcanMsgCountInfo msg_count_info; + uint16_t err_code; + if (err_code = CallAndLog(UcanGetMsgCountInfoEx, "get msg counts", + handle_value, m_channel_number, &msg_count_info); + err_code == 0) { + diagnostics.tx = msg_count_info.m_wSentMsgCount; + diagnostics.rx = msg_count_info.m_wRecvdMsgCount; + } else { + diagnostics.log_entries.value().push_back( + std::string(UsbCanGetErrorText(err_code))); + } + + DWORD tx_error, rx_error; + if (err_code = + CallAndLog(UcanGetCanErrorCounter, "get errors", handle_value, + m_channel_number, &tx_error, &rx_error); + err_code == 0) { + diagnostics.tx_error = tx_error; + diagnostics.rx_error = rx_error; + } else { + diagnostics.log_entries.value().push_back( + std::string(UsbCanGetErrorText(err_code))); + } + + tUcanHardwareInfo hw_info; + if (err_code = CallAndLog(UcanGetHardwareInfo, "get hw info", handle_value, + &hw_info); + err_code != 0) { + diagnostics.mode = "OFFLINE"; + diagnostics.log_entries.value().push_back( + std::string(UsbCanGetErrorText(err_code))); + } else { + switch (hw_info.m_bMode) { + case kUcanModeNormal: + diagnostics.mode = "NORMAL"; + break; + case kUcanModeListenOnly: + diagnostics.mode = "LISTEN_ONLY"; + break; + case kUcanModeTxEcho: + diagnostics.mode = "LOOPBACK"; + break; + } + } + + DWORD module_time; // in ms + if (err_code = CallAndLog(UcanGetModuleTime, "get module time", + handle_value, &module_time); + err_code == 0) + diagnostics.uptime = static_cast(module_time) / 1000; + else + diagnostics.log_entries.value().push_back( + std::string(UsbCanGetErrorText(err_code))); + } + return diagnostics; +}; + +/** + * thread to handle reception of Can messages from the systec device + */ +int CanVendorSystec::SystecRxThread() { + BYTE status; + tCanMsgStruct read_can_message; + LOG(Log::DBG, CanLogIt::h()) << "SystecRxThread Started. m_module_in_use = [" + << m_module_in_use << "]"; + size_t to_read; + + auto handle = get_module_handle(); + if (!handle.has_value()) { + LOG(Log::ERR, CanLogIt::h()) + << "Could not start rx thread without valid handle for module"; + return -1; // TODO(jsouter): more useful error code + } + while (m_module_in_use) { + to_read = m_queued_reads; + if (to_read < 1) continue; + status = UcanReadCanMsgEx(handle.value(), + reinterpret_cast(&m_channel_number), + &read_can_message, NULL); + switch (status) { + case USBCAN_WARN_SYS_RXOVERRUN: + case USBCAN_WARN_DLL_RXOVERRUN: + case USBCAN_WARN_FW_RXOVERRUN: + LOG(Log::WRN, CanLogIt::h()) << UsbCanGetErrorText(status); + [[fallthrough]]; + case USBCAN_SUCCESSFUL: { + --m_queued_reads; + if (read_can_message.m_bFF & USBCAN_MSG_FF_RTR) break; + std::vector data( + read_can_message.m_bData, + read_can_message.m_bData + read_can_message.m_bDLC); + CanFrame can_frame(read_can_message.m_dwID, data, + read_can_message.m_bFF); + received(can_frame); + break; + } + case USBCAN_WARN_NODATA: + m_queued_reads -= to_read; + LOG(Log::WRN, CanLogIt::h()) << UsbCanGetErrorText(status); + break; + default: // errors + // USBCAN_ERR_MAXINSTANCES, USBCAN_ERR_ILLHANDLE, USBCAN_ERR_CANNOTINIT, + // USBCAN_ERR_ILLPARAM, USBCAN_ERR_ILLHW, USBCAN_ERR_ILLCHANNEL + LOG(Log::ERR, CanLogIt::h()) << UsbCanGetErrorText(status); + break; + } + } + + return 0; +} + +std::string_view CanVendorSystec::UsbCanGetErrorText(uint16_t err_code) { + switch (err_code) { + case USBCAN_SUCCESSFUL: + return "success"; + + case USBCAN_ERR_RESOURCE: + return "This error code returns if one resource could not be generated. " + "In this case the term resource means memory and handles provided " + "by the Windows OS"; + + case USBCAN_ERR_MAXMODULES: + return "An application has tried to open more than 64 USB-CANmodul " + "devices. The standard version of the DLL only supports up to 64 " + "USB-CANmodul devices at the same time. This error also appears " + "if several applications try to access more than 64 USB-CANmodul " + "devices. For example, application 1 has opened 60 modules, " + "application 2 has opened 4 modules and application 3 wants to " + "open a module. Application 3 receives this error code."; + + case USBCAN_ERR_HWINUSE: + return "An application tries to initialize an USB-CANmodul with the " + "given device number. If this module has already been initialized " + "by its own or by another application, this error code is " + "returned."; + + case USBCAN_ERR_ILLVERSION: + return "This error code returns if the firmware version of the " + "USB-CANmodul is not compatible to the software version of the " + "DLL. " + "In this case, install the latest driver for the USB-CANmodul. " + "Furthermore make sure that the latest firmware version is " + "programmed to the USB-CANmodul."; + + case USBCAN_ERR_ILLHW: + return "This error code returns if an USB-CANmodul with the given device " + "number is not found. If the function UcanInitHardware() or " + "UcanInitHardwareEx() has been called with the device number " + "USBCAN_ANY_MODULE, and the error code appears, it indicates that " + "no module is connected to the PC or all connected modules are " + "already in use."; + + case USBCAN_ERR_ILLHANDLE: + return "This error code returns if a function received an incorrect " + "USBCAN handle. The function first checks which USB-CANmodul is " + "related to this handle. This error occurs if no device belongs " + "this handle."; + + case USBCAN_ERR_ILLPARAM: + return "This error code returns if a wrong parameter is passed to the " + "function. For example, the value NULL has been passed to a " + "pointer variable instead of a valid address."; + + case USBCAN_ERR_BUSY: + return "This error code occurs if several threads are accessing an " + "USB-CANmodul within a single application. After the other " + "threads have finished their tasks, the function may be called " + "again."; + + case USBCAN_ERR_TIMEOUT: + return "This error code occurs if the function transmits a command to " + "the USB-CANmodul but no reply is returned. To solve this " + "problem, close the application, disconnect the USB-CANmodul, " + "and connect it again."; + + case USBCAN_ERR_IOFAILED: + return "This error code occurs if the communication to the kernel driver " + "was interrupted. This happens, for example, if the USB-CANmodul " + "is disconnected during transferring data or commands to the " + "USB-CANmodul."; + + case USBCAN_ERR_DLL_TXFULL: + return "The function UcanWriteCanMsg() or UcanWriteCanMsgEx() first " + "checks if the transmit buffer within the DLL has enough capacity " + "to store new CAN messages. If the buffer is full, this error " + "code returns. " + "The CAN message passed to these functions will not be written " + "into the transmit buffer in order to protect other CAN messages " + "against overwriting. The size of the transmit buffer is " + "configurable (refer to function UcanInitCanEx() and structure " + "tUcanInitCanParam)."; + + case USBCAN_ERR_MAXINSTANCES: + return "A maximum amount of 64 applications are able to have access to " + "the DLL. If more applications attempting to access to the DLL, " + "this error code is returned. In this case, it is not possible to " + "use an USB-CANmodul by this application."; + + case USBCAN_ERR_CANNOTINIT: + return "This error code returns if an application tries to call an API " + "function which only can be called in software state CAN_INIT but " + "the current software is still in state HW_INIT. Refer to section " + "4.3.1 and Table 11 for detailed information."; + + case USBCAN_ERR_DISCONNECT: + return "This error code occurs if an API function was called for an " + "USB-CANmodul that was plugged-off from the computer recently."; + + case USBCAN_ERR_ILLCHANNEL: + return "This error code is returned if an extended function of the DLL " + "is called with parameter bChannel_p = USBCAN_CHANNEL_CH1, but a " + "single-channel USB-CANmodul was used."; + + case USBCAN_ERR_ILLHWTYPE: + return "This error code occurs if an extended function of the DLL was " + "called for a hardware which does not support the feature."; + + case USBCAN_ERRCMD_NOTEQU: + return "This error code occurs during communication between the PC and " + "an USB-CANmodul. The PC sends a command to the USB-CANmodul, " + "then the module executes the command and returns a response to " + "the PC. This error code returns if the reply does not correspond " + "to the command."; + + case USBCAN_ERRCMD_REGTST: + return "The software tests the CAN controller on the USB-CANmodul when " + "the CAN interface is initialized. Several registers of the CAN " + "controller are checked. This error code returns if an error " + "appears during this register test."; + + case USBCAN_ERRCMD_ILLCMD: + return "This error code returns if the USB-CANmodul receives a " + "non-defined command. This error represents a version conflict " + "between the firmware in the USB-CANmodul and the DLL."; + + case USBCAN_ERRCMD_EEPROM: + return "The USB-CANmodul has a built-in EEPROM. This EEPROM contains " + "several configurations, e.g. the device number and the serial " + "number. If an error occurs while reading these values, this " + "error code is returned."; + + case USBCAN_ERRCMD_ILLBDR: + return "The USB-CANmodul has been initialized with an invalid baud rate " + "(refer to section 4.3.4)."; + + case USBCAN_ERRCMD_NOTINIT: + return "It was tried to access a CAN-channel of a multi-channel " + "USB-CANmodul that was not initialized."; + + case USBCAN_ERRCMD_ALREADYINIT: + return "The accessed CAN-channel of a multi-channel USB-CANmodul was " + "already initialized"; + + case USBCAN_ERRCMD_ILLSUBCMD: + return "An internal error occurred within the DLL. In this case an " + "unknown sub-command was called instead of a main command (e.g. " + "for the cyclic CAN message-feature)."; + + case USBCAN_ERRCMD_ILLIDX: + return "An internal error occurred within the DLL. In this case an " + "invalid index for a list was delivered to the firmware (e.g. for " + "the cyclic CAN message-feature)."; + + case USBCAN_ERRCMD_RUNNING: + return "The caller tries to define a new list of cyclic CAN messages but " + "this feature was already started. For defining a new list, it is " + "necessary to stop the feature beforehand."; + + case USBCAN_WARN_NODATA: + return "If the function UcanReadCanMsg() or UcanReadCanMsgEx() returns " + "with this warning, it is an indication that the receive buffer " + "contains no CAN messages."; + + case USBCAN_WARN_SYS_RXOVERRUN: + return "This is returned by UcanReadCanMsg() or UcanReadCanMsgEx() if " + "the receive buffer within the kernel driver runs over. The " + "function nevertheless returns a valid CAN message. It also " + "indicates that at least one CAN message are lost. However, it " + "does not indicate the position of the lost CAN messages."; + + case USBCAN_WARN_DLL_RXOVERRUN: + return "The DLL automatically requests CAN messages from the " + "USB-CANmodul and stores the messages into a buffer of the DLL. " + "If more CAN messages are received than the DLL buffer size " + "allows, this error code returns and CAN messages are lost. " + "However, it does not indicate the position of the lost CAN " + "messages. The size of the receive buffer is configurable (refer " + "to function UcanInitCanEx() and structure tUcanInitCanParam)."; + + case USBCAN_WARN_FW_TXOVERRUN: + return "This warning is returned by function UcanWriteCanMsg() or " + "UcanWriteCanMsgEx() if flag USBCAN_CANERR_QXMTFULL is set in " + "the CAN driver status. However, the transmit CAN message could " + "be stored to the DLL transmit buffer. This warning indicates " + "that at least one transmit CAN message got lost in the device " + "firmware layer. This warning does not indicate the position of " + "the lost CAN message."; + + case USBCAN_WARN_FW_RXOVERRUN: + return "This warning is returned by function UcanWriteCanMsg() or " + "UcanWriteCanMsgEx() if flag USBCAN_CANERR_QOVERRUN or flag " + "USBCAN_CANERR_OVERRUN are set in the CAN driver status. The " + "function has returned with a valid CAN message. This warning " + "indicates that at least one received CAN message got lost in the " + "firmware layer. This warning does not indicate the position of " + "the lost CAN message."; + + case USBCAN_WARN_NULL_PTR: + return "This warning is returned by functions UcanInitHwConnectControl() " + "or UcanInitHwConnectControlEx() if a NULL pointer was passed as " + "callback function address."; + + case USBCAN_WARN_TXLIMIT: + return "This warning is returned by the function UcanWriteCanMsgEx() if " + "it was called to transmit more than one CAN message, but a part " + "of them could not be stored to the transmit buffer within the " + "DLL because the buffer is full). The returned variable addressed " + "by the parameter pdwCount_p indicates the number of CAN messages " + "which are stored successfully to the transmit buffer."; + + default: + std::stringstream ss; + ss << "Unknown error code: 0x" << std::hex << err_code; + return ss.str(); + } +} + +std::string CanVendorSystec::UsbCanGetStatusText(uint16_t err_code) { + switch (err_code) { + case USBCAN_CANERR_OK: + return "No error."; + case USBCAN_CANERR_XMTFULL: + return "Transmit buffer in CAN controller is overrun."; + case USBCAN_CANERR_OVERRUN: + return "Receive buffer in CAN controller is overrun."; + case USBCAN_CANERR_BUSLIGHT: + return " Error limit 1 in CAN controller exceeded, CAN controller " + "is in state “Warning limit” now."; + case USBCAN_CANERR_BUSHEAVY: + return "Error limit 2 in CAN controller exceeded, CAN controller " + "is in state “Error Passive” now"; + case USBCAN_CANERR_BUSOFF: + return "CAN controller is in BUSOFF state."; + case USBCAN_CANERR_QOVERRUN: + return "Receive buffer in module is overrun."; + case USBCAN_CANERR_QXMTFULL: + return "Transmit buffer in module is overrun."; + case USBCAN_CANERR_REGTEST: + return "CAN controller not found (hardware error)."; + case USBCAN_CANERR_TXMSGLOST: + return "A transmit CAN message was deleted automatically by the " + "firmware because transmission timeout run over (refer to " + "function UcanSetTxTimeout() )."; + default: + std::stringstream ss; + ss << "Unknown error code: 0x" << std::hex << err_code; + return ss.str(); + } +} diff --git a/test/python/test_systec.py b/test/python/test_systec.py new file mode 100644 index 00000000..bdc2b7ed --- /dev/null +++ b/test/python/test_systec.py @@ -0,0 +1,79 @@ +from time import sleep, time +import pytest +from common import * +import os +import subprocess +import struct +import socket + +# pytestmark = pytest.mark.skipif( +# not sys.platform.startswith("win"), +# reason="This test module is only for Windows environments.", +# ) + +pytestmark = pytest.mark.skipif( + socket.gethostname() != "pcaticswin11", + reason="Tests currently only work when run on the pcaticswin11 development server", +) + +ELMB_ID = 15 + + +@pytest.fixture +def device_and_frames(): + config = CanDeviceConfiguration() + config.bitrate = 125_000 + config.enable_termination = True + config.high_speed = True + config.bus_number = 0 + received = [] + device = CanDevice.create("systec", CanDeviceArguments(config, received.append)) + o1 = device.open() + assert o1 == CanReturnCode.success + received.clear() + yield device, received + c1 = device.close() + assert c1 == CanReturnCode.success + + +def test_sync_messages_elmb(device_and_frames): + device, received = device_and_frames + # on new connect, all statistics reset (for now) + diag = device.diagnostics() + assert diag.mode == "NORMAL" + assert diag.state == "No error." + assert isinstance(diag.uptime, int) + rx = diag.rx + tx = diag.tx + assert diag.rx_error == 0 + assert diag.tx_error == 0 + + sleep(1) + received.clear() # hopefully clear any previously buffered frames + r = device.send(CanFrame(0x80)) # send sync message + assert r == CanReturnCode.success + + start = time() + while time() - start < 15: + if len(received) == 65: + break + else: + raise RuntimeError( + f"Did not receive expected frames before timeout: {len(received)}/65" + ) + diff = time() - start + + diag = device.diagnostics() + assert diag.state == "No error." + assert diag.rx - rx == 65 + assert diag.tx - tx == 1 + assert diag.rx_per_second == pytest.approx(65 / diff, rel=0.3) + assert diag.tx_per_second == pytest.approx(1 / diff, rel=0.3) + + digital_input_frame = received[0] + # see page 16 https://www.nikhef.nl/pub/departments/ct/po/html/ELMB128/ELMB24.pdf + assert digital_input_frame.id() == 0x180 + ELMB_ID + assert ord(digital_input_frame.message()[0]) == 255 + for idx, frame in enumerate(received[1:]): + assert frame.id() == 911 + assert ord(frame.message()[0]) == idx