From d708f8081ae069793cb23dc73c7aa44a86826b0c Mon Sep 17 00:00:00 2001 From: David Rebbe Date: Wed, 8 Jul 2026 01:25:23 +0000 Subject: [PATCH] C2: Add termination group support --- api/icsneoc2/icsneoc2_internal.h | 5 ++ api/icsneoc2/icsneoc2settings.cpp | 76 +++++++++++++++++++++ docs/icsneoc2/examples.rst | 8 +++ examples/CMakeLists.txt | 5 ++ examples/c2/termination/CMakeLists.txt | 6 ++ examples/c2/termination/src/main.c | 91 ++++++++++++++++++++++++++ include/icsneo/icsneoc2settings.h | 56 +++++++++++++++- include/icsneo/icsneoc2types.h | 2 + test/unit/icsneoc2.cpp | 65 ++++++++++++++++++ 9 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 examples/c2/termination/CMakeLists.txt create mode 100644 examples/c2/termination/src/main.c diff --git a/api/icsneoc2/icsneoc2_internal.h b/api/icsneoc2/icsneoc2_internal.h index 9ee8308..4dd55d6 100644 --- a/api/icsneoc2/icsneoc2_internal.h +++ b/api/icsneoc2/icsneoc2_internal.h @@ -48,6 +48,11 @@ typedef struct icsneoc2_chip_versions_t { icsneoc2_chip_versions_t* next; } icsneoc2_chip_versions_t; +typedef struct icsneoc2_termination_group_t { + std::vector netids; + icsneoc2_termination_group_t* next; +} icsneoc2_termination_group_t; + typedef struct icsneoc2_mac_addr_entry_t { uint16_t network_id; uint8_t address[ICSNEO_MAC_ADDRESS_LEN]; diff --git a/api/icsneoc2/icsneoc2settings.cpp b/api/icsneoc2/icsneoc2settings.cpp index ce5bf18..52a0c3c 100644 --- a/api/icsneoc2/icsneoc2settings.cpp +++ b/api/icsneoc2/icsneoc2settings.cpp @@ -179,6 +179,82 @@ icsneoc2_error_t icsneoc2_settings_termination_set(icsneoc2_device_t* device, ic return icsneoc2_error_success; } +icsneoc2_error_t icsneoc2_settings_termination_groups_enumerate(icsneoc2_device_t* device, icsneoc2_termination_group_t** groups, size_t* count) { + // Make sure the device is valid + auto res = icsneoc2_device_is_valid(device); + if(res != icsneoc2_error_success) { + return res; + } + if(!groups) { + return icsneoc2_error_invalid_parameters; + } + + auto termination_groups = device->device->settings->getTerminationGroups(); + + icsneoc2_termination_group_t* head = nullptr; + icsneoc2_termination_group_t* tail = nullptr; + for(const auto& group : termination_groups) { + auto* node = new (std::nothrow) icsneoc2_termination_group_t; + if(!node) { + icsneoc2_settings_termination_groups_free(head); + return icsneoc2_error_out_of_memory; + } + node->netids.reserve(group.size()); + for(const auto& network : group) { + node->netids.push_back(static_cast(network.getNetID())); + } + node->next = nullptr; + if(!head) { + head = node; + } else { + tail->next = node; + } + tail = node; + } + + *groups = head; + if(count) { + *count = termination_groups.size(); + } + return icsneoc2_error_success; +} + +icsneoc2_termination_group_t* icsneoc2_termination_group_next(const icsneoc2_termination_group_t* group) { + if(!group) { + return nullptr; + } + return group->next; +} + +icsneoc2_error_t icsneoc2_termination_group_networks_get(const icsneoc2_termination_group_t* group, icsneoc2_netid_t* networks, size_t* count) { + if(!group || !count) { + return icsneoc2_error_invalid_parameters; + } + if(!networks) { + *count = group->netids.size(); + return icsneoc2_error_success; + } + size_t to_copy = std::min(*count, group->netids.size()); + for(size_t i = 0; i < to_copy; i++) { + networks[i] = group->netids[i]; + } + *count = to_copy; + return icsneoc2_error_success; +} + +icsneoc2_error_t icsneoc2_settings_termination_groups_free(icsneoc2_termination_group_t* groups) { + if(!groups) { + return icsneoc2_error_invalid_parameters; + } + + while(groups) { + auto* next = groups->next; + delete groups; + groups = next; + } + return icsneoc2_error_success; +} + icsneoc2_error_t icsneoc2_settings_commander_resistor_enabled(icsneoc2_device_t* device, icsneoc2_netid_t netid, bool* enabled) { // Make sure the device is valid auto res = icsneoc2_device_is_valid(device); diff --git a/docs/icsneoc2/examples.rst b/docs/icsneoc2/examples.rst index 3f57223..ff1ad82 100644 --- a/docs/icsneoc2/examples.rst +++ b/docs/icsneoc2/examples.rst @@ -114,3 +114,11 @@ PerfTest .. literalinclude:: ../../examples/c2/perf_test/src/main.c :language: c +Termination Groups +================== + +:download:`Download example <../../examples/c2/termination/src/main.c>` + +.. literalinclude:: ../../examples/c2/termination/src/main.c + :language: c + diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b8ed7cc..f57c476 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -8,6 +8,7 @@ option(LIBICSNEO_BUILD_C2_DISKFORMAT_EXAMPLE "Build the C2 disk format example." option(LIBICSNEO_BUILD_C2_RECONNECT_EXAMPLE "Build the C2 reconnect example." ON) option(LIBICSNEO_BUILD_C2_DEVICE_INFO_EXAMPLE "Build the C2 device info example." ON) option(LIBICSNEO_BUILD_C2_CHIP_VERSIONS_EXAMPLE "Build the C2 chip versions example." ON) +option(LIBICSNEO_BUILD_C2_TERMINATION_EXAMPLE "Build the C2 termination groups example." ON) option(LIBICSNEO_BUILD_C2_LIN_EXAMPLE "Build the C2 LIN example." ON) option(LIBICSNEO_BUILD_C2_LIN_TRANSMIT_EXAMPLE "Build the C2 LIN transmit example." ON) option(LIBICSNEO_BUILD_C2_ETHERNET_TRANSMIT_EXAMPLE "Build the C2 ethernet transmit example." ON) @@ -78,6 +79,10 @@ if(LIBICSNEO_BUILD_C2_CHIP_VERSIONS_EXAMPLE) add_subdirectory(c2/chip_versions) endif() +if(LIBICSNEO_BUILD_C2_TERMINATION_EXAMPLE) + add_subdirectory(c2/termination) +endif() + if(LIBICSNEO_BUILD_C2_LIN_EXAMPLE) add_subdirectory(c2/lin) endif() diff --git a/examples/c2/termination/CMakeLists.txt b/examples/c2/termination/CMakeLists.txt new file mode 100644 index 0000000..d330587 --- /dev/null +++ b/examples/c2/termination/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(libicsneoc2-termination-example src/main.c) +target_link_libraries(libicsneoc2-termination-example icsneoc2-static) + +if(WIN32) + target_compile_definitions(libicsneoc2-termination-example PRIVATE _CRT_SECURE_NO_WARNINGS) +endif() diff --git a/examples/c2/termination/src/main.c b/examples/c2/termination/src/main.c new file mode 100644 index 0000000..7e0dcad --- /dev/null +++ b/examples/c2/termination/src/main.c @@ -0,0 +1,91 @@ +#include +#include +#include + +#include +#include + +int print_error_code(const char* message, icsneoc2_error_t error) { + char error_str[64]; + size_t error_str_len = sizeof(error_str); + icsneoc2_error_t res = icsneoc2_error_code_get(error, error_str, &error_str_len); + if(res != icsneoc2_error_success) { + printf("%s: Failed to get string for error code %u with error code %u\n", message, error, res); + return (int)res; + } + printf("%s: \"%s\" (%u)\n", message, error_str, error); + return (int)error; +} + +int main() { + icsneoc2_error_t res; + + icsneoc2_device_t* device = NULL; + res = icsneoc2_device_open_first(0, icsneoc2_open_options_default, &device); + if(res != icsneoc2_error_success) { + return print_error_code("Failed to open first device", res); + } + + char description[128] = {0}; + size_t description_length = sizeof(description); + icsneoc2_device_description_get(device, description, &description_length); + printf("Opened device: %s\n\n", description); + + size_t count = 0; + icsneoc2_termination_group_t* groups = NULL; + res = icsneoc2_settings_termination_groups_enumerate(device, &groups, &count); + if(res != icsneoc2_error_success) { + print_error_code("Failed to enumerate termination groups", res); + icsneoc2_device_close(device); + icsneoc2_device_free(device); + return 1; + } + + if(count == 0) { + printf("This device does not support software switchable termination.\n"); + } else { + printf("Found %zu termination group(s) (only one network per group may be terminated at a time):\n", count); + } + + size_t group_index = 0; + for(icsneoc2_termination_group_t* group = groups; group; group = icsneoc2_termination_group_next(group), group_index++) { + // First call with a NULL buffer to learn how many networks are in this group. + size_t network_count = 0; + icsneoc2_termination_group_networks_get(group, NULL, &network_count); + + icsneoc2_netid_t* netids = (icsneoc2_netid_t*)malloc(network_count * sizeof(icsneoc2_netid_t)); + if(!netids) { + print_error_code("Out of memory", icsneoc2_error_out_of_memory); + break; + } + + // Second call to fill the buffer. + res = icsneoc2_termination_group_networks_get(group, netids, &network_count); + if(res != icsneoc2_error_success) { + print_error_code("Failed to get termination group networks", res); + free(netids); + continue; + } + + printf(" Group %zu:", group_index); + for(size_t i = 0; i < network_count; i++) { + char name[64] = {0}; + size_t name_length = sizeof(name); + if(icsneoc2_netid_name_get(netids[i], name, &name_length) == icsneoc2_error_success) { + printf(" %s", name); + } else { + printf(" %u", netids[i]); + } + if(i + 1 < network_count) { + printf(","); + } + } + printf("\n"); + free(netids); + } + + icsneoc2_settings_termination_groups_free(groups); + icsneoc2_device_close(device); + icsneoc2_device_free(device); + return 0; +} diff --git a/include/icsneo/icsneoc2settings.h b/include/icsneo/icsneoc2settings.h index f10abd1..3869adb 100644 --- a/include/icsneo/icsneoc2settings.h +++ b/include/icsneo/icsneoc2settings.h @@ -140,7 +140,61 @@ icsneoc2_error_t icsneoc2_settings_termination_is_enabled(icsneoc2_device_t* dev */ icsneoc2_error_t icsneoc2_settings_termination_set(icsneoc2_device_t* device, icsneoc2_netid_t netid, bool enable); -// TODO: getTerminationGroups +/** + * Enumerate the termination groups for a device. + * + * Some devices have groupings of networks where software switchable termination + * can only be applied to one network in the group at a time. This enumerates + * those groups. Use icsneoc2_termination_group_next() to walk the list and + * icsneoc2_termination_group_networks_get() to read each group's networks. + * + * These groups apply to the CAN termination controlled by the + * icsneoc2_settings_termination_* functions. 10BASE-T1S termination + * (icsneoc2_settings_t1s_*) is per-network and is never grouped. + * + * If the device does not support software switchable termination, the list is + * empty (*groups is set to NULL and *count, if provided, is set to 0). + * + * @param[in] device The device to query. + * @param[out] groups Receives a newly allocated termination group handle, or NULL if there are no groups. The caller owns this handle and must free it with icsneoc2_settings_termination_groups_free() when done. + * @param[out] count Receives the number of termination groups. May be NULL if not needed. + * + * @return icsneoc2_error_t icsneoc2_error_success if successful, icsneoc2_error_invalid_parameters or icsneoc2_error_out_of_memory otherwise. + */ +icsneoc2_error_t icsneoc2_settings_termination_groups_enumerate(icsneoc2_device_t* device, icsneoc2_termination_group_t** groups, size_t* count); + +/** + * Advance to the next termination group in an enumeration list. + * + * @param[in] group The current termination group handle. + * + * @return The next termination group handle, or NULL at the end of the list. + */ +icsneoc2_termination_group_t* icsneoc2_termination_group_next(const icsneoc2_termination_group_t* group); + +/** + * Get the networks belonging to a termination group. + * + * Call with networks set to NULL to query the number of networks in the group; + * *count will be set to the required size. Then call again with a buffer of at + * least that size. On success *count is updated with the number of netids written. + * + * @param[in] group The termination group handle to query. + * @param[out] networks Pointer to a buffer to copy the group's netids into. May be NULL to query the required size. + * @param[in,out] count On input, the capacity of the networks buffer. On output, the number of netids written (or required, if networks is NULL). + * + * @return icsneoc2_error_t icsneoc2_error_success if successful, icsneoc2_error_invalid_parameters otherwise. + */ +icsneoc2_error_t icsneoc2_termination_group_networks_get(const icsneoc2_termination_group_t* group, icsneoc2_netid_t* networks, size_t* count); + +/** + * Free a termination group list returned by icsneoc2_settings_termination_groups_enumerate(). + * + * @param[in] groups The termination group handle to free. Passing NULL returns icsneoc2_error_invalid_parameters. + * + * @return icsneoc2_error_t icsneoc2_error_success if successful, icsneoc2_error_invalid_parameters otherwise. + */ +icsneoc2_error_t icsneoc2_settings_termination_groups_free(icsneoc2_termination_group_t* groups); /** * Check if the commander resistor is currently enabled for a network. diff --git a/include/icsneo/icsneoc2types.h b/include/icsneo/icsneoc2types.h index 2bc11a2..932b6aa 100644 --- a/include/icsneo/icsneoc2types.h +++ b/include/icsneo/icsneoc2types.h @@ -546,6 +546,8 @@ typedef uint8_t icsneoc2_tc10_sleep_status_t; typedef struct icsneoc2_chip_versions_t icsneoc2_chip_versions_t; +typedef struct icsneoc2_termination_group_t icsneoc2_termination_group_t; + typedef enum _icsneoc2_chip_id_t { icsneoc2_chip_id_neovifire_mchip = 0, icsneoc2_chip_id_neovifire_lchip = 1, diff --git a/test/unit/icsneoc2.cpp b/test/unit/icsneoc2.cpp index 3efbf04..04d39ab 100644 --- a/test/unit/icsneoc2.cpp +++ b/test/unit/icsneoc2.cpp @@ -261,6 +261,13 @@ TEST(icsneoc2, test_icsneoc2_error_invalid_parameters_and_invalid_device) ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_termination_can_enable(NULL, 0, &placeholderBool)); ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_termination_is_enabled(NULL, 0, &placeholderBool)); ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_termination_set(NULL, 0, false)); + { + icsneoc2_termination_group_t* placeholderGroups = nullptr; + ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_termination_groups_enumerate(NULL, &placeholderGroups, &placeholderSizeT)); + ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_termination_group_networks_get(NULL, NULL, &placeholderSizeT)); + ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_termination_groups_free(NULL)); + ASSERT_EQ(nullptr, icsneoc2_termination_group_next(NULL)); + } ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_commander_resistor_enabled(NULL, 0, &placeholderBool)); ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_commander_resistor_set(NULL, 0, false)); ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_lin_mode_get(NULL, 0, &placeholderLinMode)); @@ -1143,6 +1150,64 @@ TEST(icsneoc2, test_icsneoc2_chip_versions_free_list) ASSERT_EQ(icsneoc2_error_success, icsneoc2_chip_versions_free(head)); } +TEST(icsneoc2, test_icsneoc2_termination_group_walk_and_networks) +{ + // Build an in-memory list of two termination groups: + // group A: { DWCAN_01, DWCAN_02 } + // group B: { DWCAN_03 } + icsneoc2_termination_group_t second{}; + second.netids = { icsneoc2_netid_dwcan_03 }; + second.next = nullptr; + + icsneoc2_termination_group_t first{}; + first.netids = { icsneoc2_netid_dwcan_01, icsneoc2_netid_dwcan_02 }; + first.next = &second; + + // next() walks the list + ASSERT_EQ(&second, icsneoc2_termination_group_next(&first)); + ASSERT_EQ(nullptr, icsneoc2_termination_group_next(&second)); + + // networks_get with NULL buffer reports the required count + size_t count = 0; + ASSERT_EQ(icsneoc2_error_success, icsneoc2_termination_group_networks_get(&first, NULL, &count)); + ASSERT_EQ(count, 2u); + + // networks_get fills the buffer and updates count + icsneoc2_netid_t netids[2] = {0, 0}; + count = 2; + ASSERT_EQ(icsneoc2_error_success, icsneoc2_termination_group_networks_get(&first, netids, &count)); + ASSERT_EQ(count, 2u); + ASSERT_EQ(netids[0], icsneoc2_netid_dwcan_01); + ASSERT_EQ(netids[1], icsneoc2_netid_dwcan_02); + + // second group has a single network + count = 0; + ASSERT_EQ(icsneoc2_error_success, icsneoc2_termination_group_networks_get(&second, NULL, &count)); + ASSERT_EQ(count, 1u); + + // a buffer smaller than the group truncates to capacity + icsneoc2_netid_t one[1] = {0}; + count = 1; + ASSERT_EQ(icsneoc2_error_success, icsneoc2_termination_group_networks_get(&first, one, &count)); + ASSERT_EQ(count, 1u); + ASSERT_EQ(one[0], icsneoc2_netid_dwcan_01); +} + +TEST(icsneoc2, test_icsneoc2_termination_groups_free_list) +{ + // Allocate a two-node list the same way the API does, so free() walks and deletes it. + auto* head = new icsneoc2_termination_group_t{}; + head->netids = { icsneoc2_netid_dwcan_01 }; + head->next = new icsneoc2_termination_group_t{}; + head->next->netids = { icsneoc2_netid_dwcan_02 }; + head->next->next = nullptr; + + ASSERT_EQ(icsneoc2_error_success, icsneoc2_settings_termination_groups_free(head)); + + // Freeing an empty (NULL) list returns invalid_parameters, matching chip_versions_free. + ASSERT_EQ(icsneoc2_error_invalid_parameters, icsneoc2_settings_termination_groups_free(nullptr)); +} + TEST(icsneoc2, test_icsneoc2_open_options_default) { icsneoc2_open_options_t expected = ICSNEOC2_OPEN_OPTIONS_GO_ONLINE | ICSNEOC2_OPEN_OPTIONS_SYNC_RTC | ICSNEOC2_OPEN_OPTIONS_ENABLE_AUTO_UPDATE;