Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
1abe999
Create outline of build for CanVendorSystec
jsouter Feb 19, 2026
caf8377
Outline of Systec implementation for Windows
jsouter Feb 19, 2026
6440de5
use switch case to handle systec read responses
jsouter Feb 24, 2026
df040c4
reconnect systec on a failed send
jsouter Feb 24, 2026
e89d908
fix header guard for CanVendorSystec
jsouter Feb 24, 2026
9eba564
use std::copy to copy Systec can message data to CanFrame
jsouter Feb 24, 2026
87ddd7d
small cleanups
jsouter Feb 24, 2026
3a8298c
renames, cleanups, reorderings
jsouter Feb 24, 2026
dbc8a1e
fix whitespace issue with warning messages for Systec vendor
jsouter Feb 24, 2026
8517d00
avoid std::copy by initializing vector with data directly
jsouter Feb 25, 2026
b9179c7
use lock guards on Systec handle map access
jsouter Feb 26, 2026
e9c11b5
use snake_case in CanVendorSystec
jsouter Feb 26, 2026
a9cf432
replace Windows specific thread calls with std library calls
jsouter Feb 27, 2026
93fd7c5
don't copy beyond m_bDLC into Systec data vector
jsouter Mar 2, 2026
ebab3d2
remove comments copies from STCanScan.cpp
jsouter Mar 2, 2026
540c026
use fallthrough attributes
jsouter Mar 2, 2026
4850aff
Log send errors in systec and use fallthrough attributes
jsouter Mar 2, 2026
70f52a8
only join systec thread if joinable
jsouter Mar 3, 2026
88c9561
switch case formatting
jsouter Mar 3, 2026
a94c576
convert indentation to spaces
jsouter Mar 3, 2026
74ee911
remove inline from handle map, make error text function a member method
jsouter Mar 3, 2026
bce764b
add uptime to diagnostics for Systec
jsouter Mar 3, 2026
c2b80ad
replace broken memcpy with std::copy, some reformatting
jsouter Mar 3, 2026
17889fc
update CanVendorSystec docstring
jsouter Mar 3, 2026
dbc934e
check return codes of open and close on systec reconnect
jsouter Mar 3, 2026
be434c5
reduce [[ fallthrough ]] use to only where it prevents a warning
jsouter Mar 4, 2026
b348b1b
improve vendor return codes for systec
jsouter Mar 4, 2026
0784fe2
make m_receive_thread_flag atomic
jsouter Mar 4, 2026
44e2882
cleanup comments
jsouter Mar 4, 2026
be35575
add get_module_handle() method
jsouter Mar 4, 2026
bb9cc92
Use systec callback to signal when frame ready to read
jsouter Mar 5, 2026
9a1f951
handle parsing of can and vcan port names for systec
jsouter Mar 5, 2026
32f03f7
fix reconnect logic for multiple channels on same module
jsouter Mar 5, 2026
d461b91
fixes for systec build in cmake
jsouter Mar 6, 2026
5b1cc9d
remove :: prefix for systec api calls
jsouter Mar 17, 2026
ede7de6
split up systec close logic, deinit hardware on failed open
jsouter Mar 17, 2026
66f70cc
don't build systec for windows by default
jsouter Mar 18, 2026
ae0b19d
move reconnect_channel to new method
jsouter Mar 18, 2026
18d14d7
remove erroneous FATAL_ERROR from systec.cmake message
jsouter Mar 18, 2026
4af4598
remove check on message length in systec vendor_send
jsouter Mar 20, 2026
17f4cc7
Add WIP systec test for pcaticswin11
jsouter Mar 20, 2026
78178bd
update test_systec for ELMB id 15
jsouter Mar 20, 2026
7755662
implement suggestions from review
jsouter Mar 23, 2026
5fc4c1e
Use descriptive error messages for systec status in diagnostics.state
jsouter Mar 23, 2026
eba8463
fixes for test_systec
jsouter Mar 23, 2026
24cf1c9
throw in systec if invalid bitrate specified
jsouter Mar 23, 2026
8b0e775
wrap init_can_port in try block
jsouter Mar 23, 2026
f2b84aa
delay adding CanVendorSystec to port to vendor map
jsouter Mar 24, 2026
9fc556d
Only call deinit on nonzero handles, clean up some logging messages
jsouter Mar 24, 2026
38d9398
Improve logging for error messages
jsouter Mar 24, 2026
af2f3f0
Use std::optional for systec handles, avoid use of [] accessor
jsouter Mar 24, 2026
389eb41
use convenience template to log error messages for all ucan calls
jsouter Mar 25, 2026
7a7b349
remove reconnect_channel
jsouter Mar 25, 2026
826cde5
Log status error messages and add to log_entries
jsouter Mar 25, 2026
f055593
fix broken if
jsouter Mar 25, 2026
3b2079e
move lock_guard out of deinit_channel to prevent double lock acquisition
jsouter Mar 25, 2026
d903c0d
return early from vendor_diagnostics on error
jsouter Mar 25, 2026
23d0853
add config option to deinit both channels when vendor_close called on…
jsouter Mar 26, 2026
61aa60a
add systec to canmodule-utils.py on windows
jsouter Apr 7, 2026
205159f
build systec dependency from zip file installer\
jsouter Apr 7, 2026
f55351a
black format python files
jsouter Apr 7, 2026
f07c9cd
cpplint fixes
jsouter Apr 8, 2026
b53784c
clang-format on CanVendorSystec
jsouter Apr 8, 2026
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
18 changes: 16 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is systec planned to be supported also in the power supplies and/or several OPC Servers? or is it only a single-case? The answer will tell us if having a variable to activate that part of the build is good or bad idea.

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")
Expand Down
40 changes: 40 additions & 0 deletions cmake/systec.cmake
Original file line number Diff line number Diff line change
@@ -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()
12 changes: 9 additions & 3 deletions docs/CANMODULE-UTILS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -47,13 +51,15 @@ 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

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
```
87 changes: 59 additions & 28 deletions python/canmodule-utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
Expand All @@ -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()

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/include/CanDeviceConfiguration.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ struct CanDeviceConfiguration {
*/
std::optional<uint32_t> 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<bool> close_both_channels;

std::string to_string() const noexcept;
};

Expand Down
3 changes: 2 additions & 1 deletion src/include/CanDiagnostics.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ struct CanDiagnostics {

std::optional<float>
temperature; ///< Optional temperature reading for Anagate devices.
std::optional<uint32_t> uptime; ///< Optional uptime for Anagate devices.
std::optional<uint32_t> uptime; ///< Optional uptime in seconds for Anagate
///< and Systec for Windows.

std::optional<uint32_t> tcp_rx; ///< Optional TCP Received counter for
///< both SocketCAN and Anagate devices.
Expand Down
74 changes: 74 additions & 0 deletions src/include/CanVendorSystec.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#ifndef SRC_INCLUDE_CANVENDORSYSTEC_H_
#define SRC_INCLUDE_CANVENDORSYSTEC_H_

#include <Winsock2.h>
#include <tchar.h>
#include <usbcan32.h>
#include <windows.h>

#include <atomic>
#include <cstdint>
#include <map>
#include <memory>
#include <mutex> //NOLINT
#include <optional>
#include <string>
#include <thread>
#include <unordered_map>

#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<bool> m_module_in_use{false};
std::atomic<size_t> 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<tUcanHandle> 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<int, tUcanHandle> m_module_to_handle_map;
static std::unordered_map<int, CanVendorSystec*> 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_
Loading