From b246af3cae9a935a7fd5e9f2dcbbca2e8f7748d0 Mon Sep 17 00:00:00 2001 From: Jorge Morte Date: Fri, 17 Apr 2026 11:04:49 +0200 Subject: [PATCH 1/2] dsap: add SSR TX request support --- lib/wpc/dsap.c | 366 +++++++++++++++++++----- lib/wpc/include/dsap.h | 78 ++++++ lib/wpc/include/wpc_constants.h | 4 + lib/wpc/include/wpc_types.h | 2 + test/CMakeLists.txt | 24 +- test/ssr_routing_unit_tests.cpp | 476 ++++++++++++++++++++++++++++++++ 6 files changed, 875 insertions(+), 75 deletions(-) create mode 100644 test/ssr_routing_unit_tests.cpp diff --git a/lib/wpc/dsap.c b/lib/wpc/dsap.c index 928a991..41edc2a 100644 --- a/lib/wpc/dsap.c +++ b/lib/wpc/dsap.c @@ -142,28 +142,126 @@ static void fill_tx_frag_request(wpc_frame_t * request, memcpy(&payload->apdu, buffer, len); } +static void fill_tx_ssr_request(wpc_frame_t * request, + const uint8_t * buffer, + size_t len, + uint16_t pdu_id, + uint32_t dest_add, + uint8_t qos, + uint8_t src_ep, + uint8_t dest_ep, + uint8_t tx_options, + uint32_t buffering_delay, + uint8_t hop_count, + const uint32_t * hops) +{ + dsap_data_tx_ssr_req_pl_t * payload = &request->payload.dsap_data_tx_ssr_request_payload; -int dsap_data_tx_request(const uint8_t * buffer, - size_t len, - uint16_t pdu_id, - uint32_t dest_add, - uint8_t qos, - uint8_t src_ep, - uint8_t dest_ep, - onDataSent_cb_f on_data_sent_cb, - uint32_t buffering_delay, - bool is_unack_csma_ca, - uint8_t hop_limit) + payload->pdu_id = pdu_id; + payload->src_endpoint = src_ep; + payload->dest_add = dest_add; + payload->dest_endpoint = dest_ep; + payload->qos = qos; + payload->tx_options = tx_options; + payload->buffering_delay = buffering_delay; + payload->hop_count = hop_count; + memcpy(payload->hops, hops, hop_count * sizeof(payload->hops[0])); + payload->apdu_length = (uint8_t) len; + memcpy(payload->apdu, buffer, len); +} + +static void fill_tx_ssr_frag_request(wpc_frame_t * request, + const uint8_t * buffer, + size_t len, + uint16_t pdu_id, + uint32_t dest_add, + uint8_t qos, + uint8_t src_ep, + uint8_t dest_ep, + uint8_t tx_options, + uint32_t buffering_delay, + uint16_t full_packet_id, + uint16_t frag_offset, + bool last_frag, + uint8_t hop_count, + const uint32_t * hops) +{ + dsap_data_tx_ssr_frag_req_pl_t * payload = &request->payload.dsap_data_tx_ssr_frag_request_payload; + uint16_t frag_flag = 0; + + if (last_frag) + { + frag_flag = DSAP_FRAG_LAST_FLAG_MASK; + } + else + { + // Never track intermediate fragment so clear first bit + tx_options &= 0xFE; + } + + *payload = (dsap_data_tx_ssr_frag_req_pl_t) { + .pdu_id = pdu_id, + .src_endpoint = src_ep, + .dest_add = dest_add, + .dest_endpoint = dest_ep, + .qos = qos, + .tx_options = tx_options, + .buffering_delay = buffering_delay, + .full_packet_id = full_packet_id, + .fragment_offset_flag = (frag_offset & DSAP_FRAG_LENGTH_MASK) | frag_flag, + .hop_count = hop_count, + .apdu_length = len, + }; + + memcpy(payload->hops, hops, hop_count * sizeof(payload->hops[0])); + memcpy(payload->apdu, buffer, len); +} + +static uint8_t build_tx_options(onDataSent_cb_f on_data_sent_cb, + bool is_unack_csma_ca, + uint8_t hop_limit) +{ + uint8_t tx_options = 0; + + if (on_data_sent_cb != NULL) + { + tx_options |= 0x1; + } + + if (is_unack_csma_ca) + { + tx_options |= 0x2; + } + + tx_options |= (hop_limit & 0xf) << 2; + + return tx_options; +} + +static int dsap_data_tx_request_internal(const uint8_t * buffer, + size_t len, + uint16_t pdu_id, + uint32_t dest_add, + uint8_t qos, + uint8_t src_ep, + uint8_t dest_ep, + onDataSent_cb_f on_data_sent_cb, + uint32_t buffering_delay, + bool is_unack_csma_ca, + uint8_t hop_limit, + uint8_t hop_count, + const uint32_t * hops) { wpc_frame_t request, confirm; int res; uint8_t confirm_res; - uint8_t tx_options = 0; + uint8_t tx_options = build_tx_options(on_data_sent_cb, is_unack_csma_ca, hop_limit); size_t fragments = 0; size_t last_fragment_size = 0; - // Packet id used for fragmented packet + // Packet ID used for fragmented packets. static uint16_t packet_id = 0; uint8_t max_data_pdu_size = WPC_Int_get_mtu(); + bool use_ssr = hop_count != 0; if (len > MAX_FULL_PACKET_SIZE) { @@ -172,99 +270,161 @@ int dsap_data_tx_request(const uint8_t * buffer, return 6; } + if (use_ssr && (hop_count > SSR_MAX_HOPS || hops == NULL)) + { + return 6; + } + if (len > max_data_pdu_size) { - // Packet must be fragmented - fragments = (len + max_data_pdu_size - 1) / max_data_pdu_size; + // Packet must be fragmented. + fragments = (len + max_data_pdu_size - 1) / max_data_pdu_size; last_fragment_size = len % max_data_pdu_size; if (last_fragment_size == 0) { last_fragment_size = max_data_pdu_size; } - LOGI("Packet of size %d must be splitted in %d fragments (last is %d bytes)\n", len, fragments, last_fragment_size); - } - - // Fill the tx options - if (on_data_sent_cb != NULL) - { - tx_options |= 0x1; - } - - // Is it a unack_csma_ca transmission - if (is_unack_csma_ca) - { - tx_options |= 0x2; + LOGI("%spacket of size %d must be split in %d fragments (last is %d bytes)\n", + use_ssr ? "SSR " : "", + len, + fragments, + last_fragment_size); } - // Add hop limit (on 4 bits) - tx_options |= (hop_limit & 0xf) << 2; - - // Create the frame if (fragments == 0) { - // Full packet in a single frame - // Even with buffering_delay of 0, TX_TT_REQUEST can be used - request.primitive_id = DSAP_DATA_TX_TT_REQUEST; - fill_tx_tt_request(&request, buffer, len, pdu_id, dest_add, qos, src_ep, dest_ep, tx_options, buffering_delay); - request.payload_length = - sizeof(dsap_data_tx_tt_req_pl_t) - (MAX_APDU_DSAP_SIZE - len); - - // Do the sending + if (use_ssr) + { + fill_tx_ssr_request(&request, + buffer, + len, + pdu_id, + dest_add, + qos, + src_ep, + dest_ep, + tx_options, + buffering_delay, + hop_count, + hops); + request.primitive_id = DSAP_DATA_TX_SSR_REQUEST; + request.payload_length = (uint8_t)(sizeof(dsap_data_tx_ssr_req_pl_t) + - (SSR_MAX_HOPS - hop_count) * sizeof(uint32_t) + - (MAX_APDU_DSAP_SIZE - len)); + } + else + { + // Full packet in a single frame. Even with buffering_delay of 0, + // TX_TT_REQUEST can be used. + fill_tx_tt_request(&request, + buffer, + len, + pdu_id, + dest_add, + qos, + src_ep, + dest_ep, + tx_options, + buffering_delay); + request.primitive_id = DSAP_DATA_TX_TT_REQUEST; + request.payload_length = sizeof(dsap_data_tx_tt_req_pl_t) - (MAX_APDU_DSAP_SIZE - len); + } + + // Do the sending. res = WPC_Int_send_request(&request, &confirm); } else { - // id is on 12 bits + // Packet ID is encoded on 12 bits. uint16_t p_id = packet_id++ & 0xfff; - // Packet must be fragmented - request.primitive_id = DSAP_DATA_TX_FRAG_REQUEST; - // Send all fragment except last + + request.primitive_id = use_ssr ? DSAP_DATA_TX_SSR_FRAG_REQUEST : DSAP_DATA_TX_FRAG_REQUEST; for (size_t i = 0; i < fragments; i++) { uint16_t offset = i * max_data_pdu_size; bool last = false; size_t frag_len = max_data_pdu_size; - if (i == (fragments -1)) + if (i == (fragments - 1)) { last = true; - frag_len= last_fragment_size; + frag_len = last_fragment_size; + } + + // Send all fragments, including the last one. + LOGI("Sending %sfrag %d/%d for id = %d\n", + use_ssr ? "SSR " : "", + i, + fragments, + p_id); + + if (use_ssr) + { + fill_tx_ssr_frag_request(&request, + buffer + offset, + frag_len, + pdu_id, + dest_add, + qos, + src_ep, + dest_ep, + tx_options, + buffering_delay, + p_id, + offset, + last, + hop_count, + hops); + request.payload_length = (uint8_t)(sizeof(dsap_data_tx_ssr_frag_req_pl_t) + - (SSR_MAX_HOPS - hop_count) * sizeof(uint32_t) + - (MAX_APDU_DSAP_SIZE - frag_len)); + } + else + { + fill_tx_frag_request(&request, + buffer + offset, + frag_len, + pdu_id, + dest_add, + qos, + src_ep, + dest_ep, + tx_options, + buffering_delay, + p_id, + offset, + last); + request.payload_length = sizeof(dsap_data_tx_frag_req_pl_t) - (MAX_APDU_DSAP_SIZE - frag_len); } - LOGI("Sending frag %d/%d for id = %d\n", i, fragments, p_id); - - fill_tx_frag_request(&request, - buffer + offset, - frag_len, - pdu_id, - dest_add, - qos, - src_ep, - dest_ep, - tx_options, - buffering_delay, - p_id, - offset, - last); - - request.payload_length = - sizeof(dsap_data_tx_frag_req_pl_t) - (MAX_APDU_DSAP_SIZE - frag_len); - - // Do the sending + + // Do the sending. res = WPC_Int_send_request(&request, &confirm); if (res < 0) { - // No way to recall previous fragment, they will be sent - LOGE("Cannot send frag %d/%d for dst=%d id=%d size=%d\n", i, fragments, dest_add, p_id, frag_len); + // No way to recall previous fragments once sent. + LOGE("Cannot send %sfrag %d/%d for dst=%d id=%d size=%d\n", + use_ssr ? "SSR " : "", + i, + fragments, + dest_add, + p_id, + frag_len); return res; } - // Check stack return code + // Check stack return code. confirm_res = confirm.payload.dsap_data_tx_confirm_payload.result; - if (confirm_res != 0) { - // No way to recall previous fragment, they will be sent - LOGE("Stack refused (res=%d) intermediate frag %d/%d for dst=%d id=%d size=%d\n", confirm_res, i, fragments, dest_add, p_id, frag_len); + // No way to recall previous fragments once sent. + LOGE("Stack refused (res=%d) %sfrag %d/%d for dst=%d id=%d size=%d\n", + confirm_res, + use_ssr ? "SSR " : "", + i, + fragments, + dest_add, + p_id, + frag_len); return confirm_res; } } @@ -275,19 +435,77 @@ int dsap_data_tx_request(const uint8_t * buffer, confirm_res = confirm.payload.dsap_data_tx_confirm_payload.result; - // If success, register the callback + // If success, register the callback. if (confirm_res == 0 && on_data_sent_cb != NULL) { set_indication_cb(on_data_sent_cb, pdu_id); } - LOGI("Send data result = 0x%02x capacity = %d \n", + LOGI("%ssend data result = 0x%02x capacity = %d \n", + use_ssr ? "SSR " : "", confirm_res, confirm.payload.dsap_data_tx_confirm_payload.capacity); return confirm_res; } + +int dsap_data_tx_request(const uint8_t * buffer, + size_t len, + uint16_t pdu_id, + uint32_t dest_add, + uint8_t qos, + uint8_t src_ep, + uint8_t dest_ep, + onDataSent_cb_f on_data_sent_cb, + uint32_t buffering_delay, + bool is_unack_csma_ca, + uint8_t hop_limit) +{ + return dsap_data_tx_request_internal(buffer, + len, + pdu_id, + dest_add, + qos, + src_ep, + dest_ep, + on_data_sent_cb, + buffering_delay, + is_unack_csma_ca, + hop_limit, + 0, + NULL); +} + +int dsap_data_tx_ssr_request(const uint8_t * buffer, + size_t len, + uint16_t pdu_id, + uint32_t dest_add, + uint8_t qos, + uint8_t src_ep, + uint8_t dest_ep, + onDataSent_cb_f on_data_sent_cb, + uint32_t buffering_delay, + bool is_unack_csma_ca, + uint8_t hop_limit, + uint8_t hop_count, + const uint32_t * hops) +{ + return dsap_data_tx_request_internal(buffer, + len, + pdu_id, + dest_add, + qos, + src_ep, + dest_ep, + on_data_sent_cb, + buffering_delay, + is_unack_csma_ca, + hop_limit, + hop_count, + hops); +} + void dsap_data_tx_indication_handler(dsap_data_tx_ind_pl_t * payload) { onDataSent_cb_f cb = get_indication_cb(payload->pdu_id); diff --git a/lib/wpc/include/dsap.h b/lib/wpc/include/dsap.h index eabd494..78a78c5 100644 --- a/lib/wpc/include/dsap.h +++ b/lib/wpc/include/dsap.h @@ -103,6 +103,84 @@ typedef struct __attribute__((__packed__)) uint8_t capacity; } dsap_data_tx_conf_pl_t; +/* Maximum number of source-routing hops carried in a single SSR TX frame. */ +#define SSR_MAX_HOPS 3 + +/** + * SSR TX request for a single-frame payload. hop_count entries in hops[] are + * valid; the remaining slots are ignored on the wire (payload_length is set + * accordingly). + * + * The gateway fills hops[] from the First-Hop Table before sending this frame + * to the sink. Each entry is a 32-bit source-routing anchor (Long RD ID), not + * a scalar hop counter. The sink uses the routing path for selective source + * routing toward dest_add. + */ +typedef struct __attribute__((__packed__)) +{ + uint16_t pdu_id; + uint8_t src_endpoint; + uint32_t dest_add; + uint8_t dest_endpoint; + uint8_t qos; + uint8_t tx_options; + uint32_t buffering_delay; + uint8_t hop_count; /**< Number of valid entries in hops[]. */ + uint32_t hops[SSR_MAX_HOPS]; /**< Source-routing anchors (Long RD IDs). */ + uint8_t apdu_length; + uint8_t apdu[MAX_APDU_DSAP_SIZE]; +} dsap_data_tx_ssr_req_pl_t; + +/** + * SSR TX fragment request: like dsap_data_tx_frag_req_pl_t but prepends a + * source-routing hop list. hop_count entries in hops[] are valid; the + * remaining slots are ignored on the wire. + */ +typedef struct __attribute__((__packed__)) +{ + uint16_t pdu_id; + uint8_t src_endpoint; + uint32_t dest_add; + uint8_t dest_endpoint; + uint8_t qos; + uint8_t tx_options; + uint32_t buffering_delay; + uint16_t full_packet_id : 12; + uint16_t fragment_offset_flag; + uint8_t hop_count; + uint32_t hops[SSR_MAX_HOPS]; + uint8_t apdu_length; + uint8_t apdu[MAX_APDU_DSAP_SIZE]; +} dsap_data_tx_ssr_frag_req_pl_t; + +/** + * \brief Function for sending data to the network with SSR source routing. + * + * Identical to dsap_data_tx_request but includes a pre-computed source-routing + * hop list obtained from the First-Hop Table. Both single-frame and + * fragmented payloads are supported. + * + * \param hop_count + * Number of valid entries in hops[]. Must be <= SSR_MAX_HOPS. + * \param hops + * Array of source-routing anchors (Long RD IDs, first hop first). + * \return negative value if the request fails, + * a Mesh positive result otherwise + */ +int dsap_data_tx_ssr_request(const uint8_t * buffer, + size_t len, + uint16_t pdu_id, + uint32_t dest_add, + uint8_t qos, + uint8_t src_ep, + uint8_t dest_ep, + onDataSent_cb_f on_data_sent_cb, + uint32_t buffering_delay, + bool is_unack_csma_ca, + uint8_t hop_limit, + uint8_t hop_count, + const uint32_t * hops); + /** * \brief Function for sending data to the network * diff --git a/lib/wpc/include/wpc_constants.h b/lib/wpc/include/wpc_constants.h index d27d67d..a43d80d 100644 --- a/lib/wpc/include/wpc_constants.h +++ b/lib/wpc/include/wpc_constants.h @@ -28,6 +28,10 @@ #define DSAP_DATA_RX_RESPONSE (SAP_RESPONSE_OFFSET + DSAP_DATA_RX_INDICATION) #define DSAP_DATA_RX_FRAG_INDICATION 0x10 #define DSAP_DATA_RX_FRAG_RESPONSE (SAP_RESPONSE_OFFSET + DSAP_DATA_RX_FRAG_INDICATION) +#define DSAP_DATA_TX_SSR_REQUEST 0x11 +#define DSAP_DATA_TX_SSR_CONFIRM (SAP_CONFIRM_OFFSET + DSAP_DATA_TX_SSR_REQUEST) +#define DSAP_DATA_TX_SSR_FRAG_REQUEST 0x12 +#define DSAP_DATA_TX_SSR_FRAG_CONFIRM (SAP_CONFIRM_OFFSET + DSAP_DATA_TX_SSR_FRAG_REQUEST) /* * Management Service Access Points diff --git a/lib/wpc/include/wpc_types.h b/lib/wpc/include/wpc_types.h index 166714a..c53ff88 100644 --- a/lib/wpc/include/wpc_types.h +++ b/lib/wpc/include/wpc_types.h @@ -53,6 +53,8 @@ typedef struct __attribute__((__packed__)) dsap_data_tx_req_pl_t dsap_data_tx_request_payload; dsap_data_tx_tt_req_pl_t dsap_data_tx_tt_request_payload; dsap_data_tx_frag_req_pl_t dsap_data_tx_frag_request_payload; + dsap_data_tx_ssr_req_pl_t dsap_data_tx_ssr_request_payload; + dsap_data_tx_ssr_frag_req_pl_t dsap_data_tx_ssr_frag_request_payload; msap_stack_start_req_pl_t msap_stack_start_request_payload; msap_app_config_data_write_req_pl_t msap_app_config_data_write_request_payload; msap_attribute_write_req_pl_t sap_attribute_write_request_payload; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 817db28..0812211 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.18) -project(meshAPItest LANGUAGES CXX) +project(meshAPItest LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_BUILD_TYPE RelWithDebInfo) @@ -9,6 +9,11 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_compile_options(-Wall -Werror -Wextra) set(WPC_LIB_DIR "${CMAKE_CURRENT_LIST_DIR}/../lib/") +set(WPC_INTERNAL_INCLUDE_DIRS + ${WPC_LIB_DIR}/api + ${WPC_LIB_DIR}/platform + ${WPC_LIB_DIR}/wpc/include +) include(FetchContent) FetchContent_Declare( @@ -42,3 +47,20 @@ target_link_libraries(${CMAKE_PROJECT_NAME} include(GoogleTest) gtest_discover_tests(${CMAKE_PROJECT_NAME}) +add_executable(meshAPI_ssr_unit + ${WPC_LIB_DIR}/wpc/dsap.c + ${CMAKE_CURRENT_LIST_DIR}/ssr_routing_unit_tests.cpp +) + +target_link_libraries(meshAPI_ssr_unit + GTest::gtest_main +) + +target_include_directories(meshAPI_ssr_unit PRIVATE + ${WPC_INTERNAL_INCLUDE_DIRS} +) + +target_compile_options(meshAPI_ssr_unit PRIVATE -ffunction-sections -fdata-sections) +target_link_options(meshAPI_ssr_unit PRIVATE -Wl,--gc-sections) + +gtest_discover_tests(meshAPI_ssr_unit) diff --git a/test/ssr_routing_unit_tests.cpp b/test/ssr_routing_unit_tests.cpp new file mode 100644 index 0000000..e4e02bd --- /dev/null +++ b/test/ssr_routing_unit_tests.cpp @@ -0,0 +1,476 @@ +#include + +#include +#include +#include +#include + +#define _Static_assert static_assert +extern "C" +{ +#include "dsap.h" +#include "reassembly.h" +#include "util.h" +#include "wpc_constants.h" +#include "wpc_types.h" +} + +namespace +{ +struct TxCallbackState +{ + uint16_t pdu_id = 0; + uint32_t buffering_delay_ms = 0; + uint8_t result = 0; + int calls = 0; + + void reset() + { + pdu_id = 0; + buffering_delay_ms = 0; + result = 0; + calls = 0; + } +}; + +struct SendRequestStub +{ + std::vector frames; + std::vector transport_results; + std::vector confirm_results; + uint8_t mtu = 102; + uint8_t confirm_capacity = 7; + + void reset() + { + frames.clear(); + transport_results.clear(); + confirm_results.clear(); + mtu = 102; + confirm_capacity = 7; + } +}; + +SendRequestStub g_stub; +TxCallbackState g_tx_cb; + +void on_data_sent(uint16_t pdu_id, uint32_t buffering_delay_ms, uint8_t result) +{ + g_tx_cb.pdu_id = pdu_id; + g_tx_cb.buffering_delay_ms = buffering_delay_ms; + g_tx_cb.result = result; + g_tx_cb.calls++; +} +} // namespace + +extern "C" bool Platform_lock_request(void) +{ + return true; +} + +extern "C" void Platform_unlock_request(void) +{ +} + +extern "C" unsigned long long Platform_get_timestamp_ms_monotonic(void) +{ + return 0; +} + +extern "C" void Platform_LOG(char level, char * module, char * format, va_list args) +{ + (void) level; + (void) module; + (void) format; + (void) args; +} + +extern "C" void Platform_print_buffer(uint8_t * buffer, int size) +{ + (void) buffer; + (void) size; +} + +extern "C" int Platform_set_log_level(const int level) +{ + (void) level; + return 0; +} + +extern "C" int Platform_set_module_log_level(const char * const module_name, const int level) +{ + (void) module_name; + (void) level; + return 0; +} + +extern "C" int * Platform_get_logging_module_level(const char * const module_name, + const int default_level) +{ + (void) module_name; + static int level = default_level; + level = default_level; + return &level; +} + +extern "C" uint8_t WPC_Int_get_mtu(void) +{ + return g_stub.mtu; +} + +extern "C" int WPC_Int_send_request(wpc_frame_t * frame, wpc_frame_t * confirm) +{ + g_stub.frames.push_back(*frame); + const size_t call_index = g_stub.frames.size() - 1; + + const int transport_result = + call_index < g_stub.transport_results.size() ? g_stub.transport_results[call_index] : 0; + if (transport_result < 0) + { + return transport_result; + } + + std::memset(confirm, 0, sizeof(*confirm)); + confirm->payload.dsap_data_tx_confirm_payload.result = + call_index < g_stub.confirm_results.size() ? g_stub.confirm_results[call_index] : 0; + confirm->payload.dsap_data_tx_confirm_payload.capacity = g_stub.confirm_capacity; + return transport_result; +} + +extern "C" bool reassembly_add_fragment(reassembly_fragment_t * frag, size_t * full_size_p) +{ + (void) frag; + *full_size_p = 0; + return false; +} + +extern "C" bool reassembly_get_full_message(uint32_t src_add, + uint16_t packet_id, + uint8_t * buffer_p, + size_t * size) +{ + (void) src_add; + (void) packet_id; + (void) buffer_p; + (void) size; + return false; +} + +extern "C" void reassembly_garbage_collect(void) +{ +} + +extern "C" void reassembly_set_max_fragment_duration(unsigned int fragment_max_duration_s) +{ + (void) fragment_max_duration_s; +} + +class SsrRoutingUnitTest : public ::testing::Test +{ +protected: + void SetUp() override + { + g_stub.reset(); + g_tx_cb.reset(); + dsap_init(); + } +}; + +TEST_F(SsrRoutingUnitTest, LegacySingleFrameRequestKeepsTraditionalPrimitive) +{ + const uint8_t payload[] = { 0x10, 0x20, 0x30 }; + + ASSERT_EQ(dsap_data_tx_request(payload, + sizeof(payload), + 0x1234, + 0x01020304, + 1, + 5, + 7, + nullptr, + 128, + false, + 9), + 0); + + ASSERT_EQ(g_stub.frames.size(), 1u); + const wpc_frame_t & frame = g_stub.frames.front(); + EXPECT_EQ(frame.primitive_id, DSAP_DATA_TX_TT_REQUEST); + EXPECT_EQ(frame.payload_length, + sizeof(dsap_data_tx_tt_req_pl_t) - (MAX_APDU_DSAP_SIZE - sizeof(payload))); + + const dsap_data_tx_tt_req_pl_t & request = frame.payload.dsap_data_tx_tt_request_payload; + EXPECT_EQ(request.pdu_id, 0x1234u); + EXPECT_EQ(request.dest_add, 0x01020304u); + EXPECT_EQ(request.qos, 1u); + EXPECT_EQ(request.src_endpoint, 5u); + EXPECT_EQ(request.dest_endpoint, 7u); + EXPECT_EQ(request.tx_options, static_cast(9u << 2)); + EXPECT_EQ(request.buffering_delay, 128u); + ASSERT_EQ(request.apdu_length, sizeof(payload)); + EXPECT_EQ(std::memcmp(request.apdu, payload, sizeof(payload)), 0); +} + +TEST_F(SsrRoutingUnitTest, SsrSingleFrameRequestBuildsExpectedPayloadAndRegistersCallback) +{ + const uint8_t payload[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + const uint32_t hops[] = { 0x11111111, 0x22222222 }; + + ASSERT_EQ(dsap_data_tx_ssr_request(payload, + sizeof(payload), + 0xCAFE, + 0x01020304, + 1, + 8, + 9, + on_data_sent, + 128, + true, + 0x1F, + 2, + hops), + 0); + + ASSERT_EQ(g_stub.frames.size(), 1u); + const wpc_frame_t & frame = g_stub.frames.front(); + EXPECT_EQ(frame.primitive_id, DSAP_DATA_TX_SSR_REQUEST); + EXPECT_EQ(frame.payload_length, + sizeof(dsap_data_tx_ssr_req_pl_t) + - (SSR_MAX_HOPS - 2) * sizeof(uint32_t) + - (MAX_APDU_DSAP_SIZE - sizeof(payload))); + + const dsap_data_tx_ssr_req_pl_t & request = frame.payload.dsap_data_tx_ssr_request_payload; + EXPECT_EQ(request.pdu_id, 0xCAFEu); + EXPECT_EQ(request.dest_add, 0x01020304u); + EXPECT_EQ(request.qos, 1u); + EXPECT_EQ(request.src_endpoint, 8u); + EXPECT_EQ(request.dest_endpoint, 9u); + EXPECT_EQ(request.tx_options, 0x3Fu); + EXPECT_EQ(request.buffering_delay, 128u); + EXPECT_EQ(request.hop_count, 2u); + EXPECT_EQ(request.hops[0], hops[0]); + EXPECT_EQ(request.hops[1], hops[1]); + ASSERT_EQ(request.apdu_length, sizeof(payload)); + EXPECT_EQ(std::memcmp(request.apdu, payload, sizeof(payload)), 0); + + dsap_data_tx_ind_pl_t indication = {}; + indication.pdu_id = 0xCAFE; + indication.buffering_delay = 128; + indication.result = 1; + dsap_data_tx_indication_handler(&indication); + + EXPECT_EQ(g_tx_cb.calls, 1); + EXPECT_EQ(g_tx_cb.pdu_id, 0xCAFEu); + EXPECT_EQ(g_tx_cb.buffering_delay_ms, 1000u); + EXPECT_EQ(g_tx_cb.result, 1u); +} + +TEST_F(SsrRoutingUnitTest, SsrRequestRejectsPacketsLargerThanMaximumFrameSize) +{ + std::vector payload(MAX_FULL_PACKET_SIZE + 1, 0x55); + const uint32_t hop = 0xABCDEF01; + + EXPECT_EQ(dsap_data_tx_ssr_request(payload.data(), + payload.size(), + 1, + 2, + 0, + 1, + 2, + nullptr, + 0, + false, + 0, + 1, + &hop), + 6); + EXPECT_TRUE(g_stub.frames.empty()); +} + +TEST_F(SsrRoutingUnitTest, SsrRequestRejectsInvalidHopArguments) +{ + const uint8_t payload[] = { 0x01 }; + const uint32_t hops[SSR_MAX_HOPS + 1] = {}; + + EXPECT_EQ(dsap_data_tx_ssr_request(payload, + sizeof(payload), + 1, + 2, + 0, + 1, + 2, + nullptr, + 0, + false, + 0, + SSR_MAX_HOPS + 1, + hops), + 6); + + EXPECT_EQ(dsap_data_tx_ssr_request(payload, + sizeof(payload), + 1, + 2, + 0, + 1, + 2, + nullptr, + 0, + false, + 0, + 1, + nullptr), + 6); + EXPECT_TRUE(g_stub.frames.empty()); +} + +TEST_F(SsrRoutingUnitTest, SingleFrameTransportFailureIsPropagated) +{ + const uint8_t payload[] = { 0x01, 0x02 }; + const uint32_t hop = 0x12345678u; + g_stub.transport_results = { -7 }; + + EXPECT_EQ(dsap_data_tx_ssr_request(payload, + sizeof(payload), + 1, + 2, + 0, + 1, + 2, + nullptr, + 0, + false, + 0, + 1, + &hop), + -7); +} + +TEST_F(SsrRoutingUnitTest, LegacyFragmentedRequestUsesTraditionalFragmentPrimitive) +{ + const uint8_t payload[] = { 1, 2, 3, 4, 5 }; + g_stub.mtu = 3; + + ASSERT_EQ(dsap_data_tx_request(payload, + sizeof(payload), + 0x0102, + 0x55667788, + 0, + 3, + 4, + on_data_sent, + 0, + false, + 6), + 0); + + ASSERT_EQ(g_stub.frames.size(), 2u); + EXPECT_EQ(g_stub.frames[0].primitive_id, DSAP_DATA_TX_FRAG_REQUEST); + EXPECT_EQ(g_stub.frames[1].primitive_id, DSAP_DATA_TX_FRAG_REQUEST); + + const dsap_data_tx_frag_req_pl_t & first = + g_stub.frames[0].payload.dsap_data_tx_frag_request_payload; + const dsap_data_tx_frag_req_pl_t & last = + g_stub.frames[1].payload.dsap_data_tx_frag_request_payload; + + EXPECT_EQ(first.tx_options, static_cast(6u << 2)); + EXPECT_EQ(first.apdu_length, 3u); + EXPECT_EQ(first.fragment_offset_flag & DSAP_FRAG_LAST_FLAG_MASK, 0u); + + EXPECT_EQ(last.tx_options, static_cast(1u | (6u << 2))); + EXPECT_EQ(last.apdu_length, 2u); + EXPECT_EQ(last.fragment_offset_flag & DSAP_FRAG_LAST_FLAG_MASK, DSAP_FRAG_LAST_FLAG_MASK); + EXPECT_EQ(last.fragment_offset_flag & DSAP_FRAG_LENGTH_MASK, 3u); +} + +TEST_F(SsrRoutingUnitTest, FragmentedSsrRequestBuildsFragmentsAndKeepsCallbackOnlyOnLastFragment) +{ + const uint8_t payload[] = { 0, 1, 2, 3, 4, 5, 6 }; + const uint32_t hop = 0x01020304; + g_stub.mtu = 4; + + ASSERT_EQ(dsap_data_tx_ssr_request(payload, + sizeof(payload), + 0x0A0B, + 0xDEADBEEF, + 1, + 6, + 7, + on_data_sent, + 0, + true, + 3, + 1, + &hop), + 0); + + ASSERT_EQ(g_stub.frames.size(), 2u); + EXPECT_EQ(g_stub.frames[0].primitive_id, DSAP_DATA_TX_SSR_FRAG_REQUEST); + EXPECT_EQ(g_stub.frames[1].primitive_id, DSAP_DATA_TX_SSR_FRAG_REQUEST); + + const dsap_data_tx_ssr_frag_req_pl_t & first = + g_stub.frames[0].payload.dsap_data_tx_ssr_frag_request_payload; + const dsap_data_tx_ssr_frag_req_pl_t & last = + g_stub.frames[1].payload.dsap_data_tx_ssr_frag_request_payload; + + EXPECT_EQ(first.tx_options, static_cast(0x02u | (3u << 2))); + EXPECT_EQ(first.hop_count, 1u); + EXPECT_EQ(first.hops[0], hop); + EXPECT_EQ(first.apdu_length, 4u); + EXPECT_EQ(first.fragment_offset_flag & DSAP_FRAG_LAST_FLAG_MASK, 0u); + + EXPECT_EQ(last.tx_options, static_cast(0x03u | (3u << 2))); + EXPECT_EQ(last.hop_count, 1u); + EXPECT_EQ(last.hops[0], hop); + EXPECT_EQ(last.apdu_length, 3u); + EXPECT_EQ(last.fragment_offset_flag & DSAP_FRAG_LAST_FLAG_MASK, DSAP_FRAG_LAST_FLAG_MASK); + EXPECT_EQ(last.fragment_offset_flag & DSAP_FRAG_LENGTH_MASK, 4u); +} + +TEST_F(SsrRoutingUnitTest, FragmentedSsrRequestStopsOnTransportFailure) +{ + const uint8_t payload[] = { 1, 2, 3, 4, 5, 6 }; + const uint32_t hop = 0x01020304; + g_stub.mtu = 3; + g_stub.transport_results = { 0, -9 }; + + EXPECT_EQ(dsap_data_tx_ssr_request(payload, + sizeof(payload), + 0xAAAA, + 0x01020304, + 0, + 1, + 2, + nullptr, + 0, + false, + 1, + 1, + &hop), + -9); + EXPECT_EQ(g_stub.frames.size(), 2u); +} + +TEST_F(SsrRoutingUnitTest, FragmentedSsrRequestStopsOnStackRefusal) +{ + const uint8_t payload[] = { 1, 2, 3, 4, 5, 6 }; + const uint32_t hop = 0x01020304; + g_stub.mtu = 3; + g_stub.confirm_results = { 0, 5 }; + + EXPECT_EQ(dsap_data_tx_ssr_request(payload, + sizeof(payload), + 0xBBBB, + 0x01020304, + 0, + 1, + 2, + nullptr, + 0, + false, + 1, + 1, + &hop), + 5); + EXPECT_EQ(g_stub.frames.size(), 2u); +} From 2fd4cfe03700c622276a51018f0ef19ea3df4ca2 Mon Sep 17 00:00:00 2001 From: Jorge Morte Date: Fri, 17 Apr 2026 11:07:16 +0200 Subject: [PATCH 2/2] ssr: add route learning core and registration handling --- lib/platform/linux/platform.c | 42 ++- lib/platform/platform.h | 12 + lib/wpc/CMakeLists.txt | 3 +- lib/wpc/include/fht.h | 72 ++++ lib/wpc/include/msap.h | 24 ++ lib/wpc/include/ssr.h | 127 +++++++ lib/wpc/include/ssr_backend.h | 19 ++ lib/wpc/include/wpc_constants.h | 2 + lib/wpc/include/wpc_types.h | 1 + lib/wpc/makefile | 4 +- lib/wpc/msap.c | 16 + lib/wpc/ssr/fht.c | 281 +++++++++++++++ lib/wpc/ssr/ssr.c | 251 ++++++++++++++ lib/wpc/wpc_internal.c | 13 + test/CMakeLists.txt | 32 ++ test/ssr_routing_unit_tests.cpp | 303 +++++++++++++++-- test/ssr_tests.cpp | 490 +++++++++++++++++++++++++++ test/wpc_internal_ssr_unit_tests.cpp | 276 +++++++++++++++ 18 files changed, 1927 insertions(+), 41 deletions(-) create mode 100644 lib/wpc/include/fht.h create mode 100644 lib/wpc/include/ssr.h create mode 100644 lib/wpc/include/ssr_backend.h create mode 100644 lib/wpc/ssr/fht.c create mode 100644 lib/wpc/ssr/ssr.c create mode 100644 test/ssr_tests.cpp create mode 100644 test/wpc_internal_ssr_unit_tests.cpp diff --git a/lib/platform/linux/platform.c b/lib/platform/linux/platform.c index c12cd23..018b11f 100644 --- a/lib/platform/linux/platform.c +++ b/lib/platform/linux/platform.c @@ -16,6 +16,7 @@ #include "logger.h" #include "platform.h" #include "reassembly.h" +#include "ssr.h" #include "wpc_proto.h" // Maximum number of indication to be retrieved from a single poll @@ -29,6 +30,8 @@ // Mutex for sending, ie serial access static pthread_mutex_t sending_mutex; +// Mutex for the in-process SSR routing state. +static pthread_mutex_t ssr_mutex; // This thread is used to poll for indication static pthread_t thread_polling; @@ -309,6 +312,30 @@ void Platform_unlock_request() pthread_mutex_unlock(&sending_mutex); } +bool Platform_lock_ssr() +{ + int res = pthread_mutex_lock(&ssr_mutex); + if (res != 0) + { + if (res == EINVAL) + { + LOGW("SSR mutex no longer exists (destroyed)\n"); + } + else + { + LOGE("SSR mutex lock failed %d\n", res); + } + return false; + } + + return true; +} + +void Platform_unlock_ssr() +{ + pthread_mutex_unlock(&ssr_mutex); +} + unsigned long long Platform_get_timestamp_ms_epoch() { struct timespec spec; @@ -377,11 +404,17 @@ bool Platform_init(Platform_get_indication_f get_indication_f, goto error2; } + if (pthread_mutex_init(&ssr_mutex, &attr) != 0) + { + LOGE("SSR Mutex init failed\n"); + goto error3; + } + // Start a thread to poll for indication if (pthread_create(&thread_polling, NULL, poll_for_indication, NULL) != 0) { LOGE("Cannot create polling thread\n"); - goto error3; + goto error4; } m_dispatch_thread_running = true; @@ -389,13 +422,15 @@ bool Platform_init(Platform_get_indication_f get_indication_f, if (pthread_create(&thread_dispatch, NULL, dispatch_indication, NULL) != 0) { LOGE("Cannot create dispatch thread\n"); - goto error4; + goto error5; } return true; -error4: +error5: pthread_kill(thread_polling, SIGKILL); +error4: + pthread_mutex_destroy(&ssr_mutex); error3: pthread_mutex_destroy(&m_queue_mutex); error2: @@ -431,6 +466,7 @@ void Platform_close() } // Destroy our mutexes + pthread_mutex_destroy(&ssr_mutex); pthread_mutex_destroy(&m_queue_mutex); pthread_mutex_destroy(&sending_mutex); } diff --git a/lib/platform/platform.h b/lib/platform/platform.h index 0a57d98..ebd74de 100644 --- a/lib/platform/platform.h +++ b/lib/platform/platform.h @@ -91,6 +91,18 @@ bool Platform_lock_request(); */ void Platform_unlock_request(); +/** + * \brief Call at the beginning of an SSR critical section. + * \note This lock protects the in-process SSR routing state, which is + * accessed both from the dispatch thread and from caller threads. + */ +bool Platform_lock_ssr(); + +/** + * \brief Called at the end of an SSR critical section. + */ +void Platform_unlock_ssr(); + /** * \brief Dynamic memory allocation * \param size diff --git a/lib/wpc/CMakeLists.txt b/lib/wpc/CMakeLists.txt index f9cd00e..4cb4046 100644 --- a/lib/wpc/CMakeLists.txt +++ b/lib/wpc/CMakeLists.txt @@ -7,6 +7,8 @@ add_library(wpc STATIC ${CMAKE_CURRENT_LIST_DIR}/wpc.c ${CMAKE_CURRENT_LIST_DIR}/wpc_internal.c ${CMAKE_CURRENT_LIST_DIR}/reassembly/reassembly.c + ${CMAKE_CURRENT_LIST_DIR}/ssr/fht.c + ${CMAKE_CURRENT_LIST_DIR}/ssr/ssr.c ) target_include_directories(wpc PUBLIC @@ -17,4 +19,3 @@ target_include_directories(wpc PRIVATE ${PROJECT_SOURCE_DIR}/platform ${CMAKE_CURRENT_LIST_DIR}/include ) - diff --git a/lib/wpc/include/fht.h b/lib/wpc/include/fht.h new file mode 100644 index 0000000..96e1022 --- /dev/null +++ b/lib/wpc/include/fht.h @@ -0,0 +1,72 @@ +/* Wirepas Oy licensed under Apache License, Version 2.0 + * + * See file LICENSE for full license details. + * + * First-Hop Table (FHT) for Selective Source Routing bookkeeping. + * + * The table stores the first hop learned for each registered node and is used + * to build SSR downlink transmissions. + */ +#ifndef FHT_H__ +#define FHT_H__ + +#include + +/** Opaque first-hop table handle. */ +typedef struct fht fht_t; + +/** Create a new first-hop table. Returns NULL on allocation failure. */ +fht_t *fht_create(void); + +/** Destroy the table and free all memory. Safe to call with NULL. */ +void fht_destroy(fht_t *table); + +/** Remove all entries from the table while keeping its allocation. */ +void fht_clear(fht_t *table); + +/** Set the validity timer applied to all entries (seconds). */ +void fht_set_validity(fht_t *table, uint32_t seconds); + +/** Get the current validity timer (seconds). */ +uint32_t fht_get_validity(const fht_t *table); + +/** + * \brief Insert or update a route entry. + * + * \param table First-hop table. + * \param node_id Target node. + * \param first_hop_id Next hop toward that node (source routing anchor). + * \param timestamp Caller-provided time of this update (seconds). + * \return 0 on success, -1 on allocation failure. + * + * \note If an entry for node_id already exists and timestamp is not newer + * than the stored last_refresh, the update is silently ignored. + */ +int fht_update_route(fht_t *table, + uint32_t node_id, + uint32_t first_hop_id, + uint32_t timestamp); + +/** + * \brief Look up the first hop for a node. + * + * If the entry is missing or expired, the output parameter is set to 0. + * + * \param table First-hop table. + * \param node_id Target node to look up. + * \param now Caller-provided current time (seconds). + * \param first_hop_id [out] First hop address, or 0 if not found/expired. + */ +void fht_get_first_hop(const fht_t *table, + uint32_t node_id, + uint32_t now, + uint32_t *first_hop_id); + +/** + * \brief Remove all expired entries and rehash the table. + * + * \param now Caller-provided current time (seconds). + */ +void fht_purge_expired(fht_t *table, uint32_t now); + +#endif /* FHT_H__ */ diff --git a/lib/wpc/include/msap.h b/lib/wpc/include/msap.h index dbc6552..62cd438 100644 --- a/lib/wpc/include/msap.h +++ b/lib/wpc/include/msap.h @@ -231,6 +231,22 @@ typedef struct __attribute__((__packed__)) uint8_t payload[MAXIMUM_CDC_ITEM_PAYLOAD_SIZE]; } msap_config_data_item_rx_ind_pl_t; +/** + * SSR registration indication (sink → gateway). + * + * Sent by the sink's Wirepas stack each time a mesh node registers its + * source-routing anchor. The gateway uses this information to update its + * First-Hop Table so it can build SSR-routed downlink transmissions. + */ +typedef struct __attribute__((__packed__)) +{ + uint8_t indication_status; + uint32_t source_address; /**< Node that sent the registration packet. */ + uint32_t source_routing_id; /**< First-hop anchor (Long RD ID). */ + uint32_t sink_address; /**< Sink that received the registration. */ + uint32_t delay_hp; /**< End-to-end delay in 1/1024 second units. */ +} msap_ssr_registration_ind_pl_t; + static inline void convert_internal_to_app_scratchpad_status(app_scratchpad_status_t * status_p, msap_scratchpad_status_conf_pl_t * internal_status_p) @@ -561,6 +577,14 @@ void msap_scan_nbors_indication_handler(msap_scan_nbors_ind_pl_t * payload); */ void msap_config_data_item_rx_indication_handler(msap_config_data_item_rx_ind_pl_t * payload); +/** + * \brief Handler for SSR registration indication + * \param payload + * Pointer to the received SSR registration payload. + * Called by dispatch_indication() for MSAP_SSR_REGISTRATION_INDICATION. + */ +void msap_ssr_registration_indication_handler(msap_ssr_registration_ind_pl_t * payload); + /** * \brief Register for app config data * \param cb diff --git a/lib/wpc/include/ssr.h b/lib/wpc/include/ssr.h new file mode 100644 index 0000000..e10579d --- /dev/null +++ b/lib/wpc/include/ssr.h @@ -0,0 +1,127 @@ +/* Wirepas Oy licensed under Apache License, Version 2.0 + * + * See file LICENSE for full license details. + * + * SSR (Selective Source Routing) integration layer for c-mesh-api. + * + * This module maintains a First-Hop Table (FHT) fed by MSAP SSR registration + * indications received from the connected sink. + * + * Typical lifecycle: + * 1. ssr_init() - called once during WPC_initialize(). + * 2. ssr_on_registration() - called by the MSAP handler each time a sink + * forwards an SSR registration packet from a mesh node. + * 3. ssr_get_first_hop() - queried by the send path or tests to retrieve + * the routing anchor for a node. + * 4. ssr_purge_expired() - called periodically from the platform dispatch + * thread (every DISPATCH_WAKEUP_TIMEOUT_S seconds). + * 5. ssr_deinit() - optional cleanup (primarily for tests). + */ +#ifndef SSR_H__ +#define SSR_H__ + +#include +#include + +/** + * \brief Callback invoked each time a new SSR registration is learned. + * + * \param source_address Node address that registered. + * \param first_hop_id First-hop anchor (source routing ID). + * \param delay_hp End-to-end delay in 1/1024 second units. + */ +typedef void (*onSsrRegistration_cb_f)(uint32_t source_address, + uint32_t first_hop_id, + uint32_t delay_hp); + +/** + * \brief Initialise the SSR module and allocate the First-Hop Table. + * + * Safe to call multiple times; subsequent calls are no-ops if already + * initialised. + * + * \return 0 on success, -1 on failure. + */ +int ssr_init(void); + +/** + * \brief Release all SSR resources. + * + * After this call the module is in the same state as before ssr_init(). + * Primarily intended for unit-test teardown. + */ +void ssr_deinit(void); + +/** + * \brief Configure the sink address accepted by SSR registrations. + * + * Registrations learned from any other sink address are discarded. + * + * \param sink_address Node address of the connected sink. + */ +void ssr_set_sink_address(uint32_t sink_address); + +/** + * \brief Disable SSR sink-address filtering until a sink address is set again. + * + * After this call all incoming registrations are discarded. + */ +void ssr_clear_sink_address(void); + +/** + * \brief Clear all learned SSR routes while keeping the module initialised. + */ +void ssr_reset_routes(void); + +/** + * \brief Process an incoming SSR registration from a sink. + * + * Called by the MSAP indication handler whenever + * MSAP_SSR_REGISTRATION_INDICATION is received. + * + * \param source_address Node that sent the registration packet. + * \param source_routing_id First-hop anchor chosen by the mesh for that node. + * \param sink_address Sink that received the registration. + * \param delay_hp End-to-end delay in 1/1024 second units; used to + * back-calculate the generation time of the packet. + */ +void ssr_on_registration(uint32_t source_address, + uint32_t source_routing_id, + uint32_t sink_address, + uint32_t delay_hp); + +/** + * \brief Query the first hop to reach a destination node. + * + * \param dest Destination node address. + * \param first_hop_id [out] First-hop anchor address; 0 if no valid route. + * \return true if a valid, non-expired route was found. + */ +bool ssr_get_first_hop(uint32_t dest, + uint32_t *first_hop_id); + +/** + * \brief Purge expired entries from the First-Hop Table. + * + * Should be called periodically (e.g. from the platform dispatch thread). + */ +void ssr_purge_expired(void); + +/** + * \brief Register a callback to be notified on each new SSR registration. + * + * Only one callback can be registered at a time. + * + * \param cb Callback function. + * \return true on success, false if a callback is already registered. + */ +bool ssr_register_for_registration(onSsrRegistration_cb_f cb); + +/** + * \brief Unregister the SSR registration callback. + * + * \return true on success, false if no callback was registered. + */ +bool ssr_unregister_from_registration(void); + +#endif /* SSR_H__ */ diff --git a/lib/wpc/include/ssr_backend.h b/lib/wpc/include/ssr_backend.h new file mode 100644 index 0000000..d5bebdf --- /dev/null +++ b/lib/wpc/include/ssr_backend.h @@ -0,0 +1,19 @@ +#ifndef SSR_BACKEND_H__ +#define SSR_BACKEND_H__ + +#include +#include + +/* + * Private SSR backend interface. + * + * These entry points assume the caller already holds Platform_lock_ssr(). + * They are used by the WPC integration layer to update/query backend routing + * state atomically with its own cached sink eligibility state. + */ +void ssr_set_sink_address_locked(uint32_t sink_address); +void ssr_clear_sink_address_locked(void); +void ssr_reset_routes_locked(void); +bool ssr_get_first_hop_locked(uint32_t dest, uint32_t * first_hop_id); + +#endif /* SSR_BACKEND_H__ */ diff --git a/lib/wpc/include/wpc_constants.h b/lib/wpc/include/wpc_constants.h index a43d80d..19640b6 100644 --- a/lib/wpc/include/wpc_constants.h +++ b/lib/wpc/include/wpc_constants.h @@ -98,6 +98,8 @@ #define MSAP_CONFIG_DATA_ITEM_RX_RESPONSE (SAP_RESPONSE_OFFSET + MSAP_CONFIG_DATA_ITEM_RX_INDICATION) #define MSAP_CONFIG_DATA_ITEM_LIST_ITEMS_REQUEST 0x32 #define MSAP_CONFIG_DATA_ITEM_LIST_ITEMS_CONFIRM (SAP_RESPONSE_OFFSET + MSAP_CONFIG_DATA_ITEM_LIST_ITEMS_REQUEST) +#define MSAP_SSR_REGISTRATION_INDICATION 0x33 +#define MSAP_SSR_REGISTRATION_RESPONSE (SAP_RESPONSE_OFFSET + MSAP_SSR_REGISTRATION_INDICATION) /* * Configuration Service Access Points diff --git a/lib/wpc/include/wpc_types.h b/lib/wpc/include/wpc_types.h index c53ff88..6ec69c8 100644 --- a/lib/wpc/include/wpc_types.h +++ b/lib/wpc/include/wpc_types.h @@ -79,6 +79,7 @@ typedef struct __attribute__((__packed__)) msap_scan_nbors_ind_pl_t msap_scan_nbors_indication_payload; generic_ind_pl_t generic_indication_payload; msap_config_data_item_rx_ind_pl_t msap_config_data_item_rx_indication_payload; + msap_ssr_registration_ind_pl_t msap_ssr_registration_indication_payload; // Confirm sap_generic_conf_pl_t sap_generic_confirm_payload; dsap_data_tx_conf_pl_t dsap_data_tx_confirm_payload; diff --git a/lib/wpc/makefile b/lib/wpc/makefile index 41e4a54..a22d858 100644 --- a/lib/wpc/makefile +++ b/lib/wpc/makefile @@ -9,5 +9,7 @@ SOURCES += $(WPC_MODULE)msap.c SOURCES += $(WPC_MODULE)csap.c SOURCES += $(WPC_MODULE)attribute.c SOURCES += $(WPC_MODULE)reassembly/reassembly.c +SOURCES += $(WPC_MODULE)ssr/fht.c +SOURCES += $(WPC_MODULE)ssr/ssr.c -CFLAGS += -I$(WPC_MODULE)include/ \ No newline at end of file +CFLAGS += -I$(WPC_MODULE)include/ diff --git a/lib/wpc/msap.c b/lib/wpc/msap.c index 1519d5f..f197a7a 100644 --- a/lib/wpc/msap.c +++ b/lib/wpc/msap.c @@ -13,6 +13,7 @@ #include "platform.h" #include "string.h" +#include "ssr.h" /** * \brief Registered callback for app config @@ -682,3 +683,18 @@ bool msap_unregister_from_config_data_item() { return UNREGISTER_CB(m_config_data_item_cb); } + +void msap_ssr_registration_indication_handler(msap_ssr_registration_ind_pl_t * payload) +{ + LOGI("SSR registration ind: src=%u hop=%u sink=%u delay=%u\n", + payload->source_address, + payload->source_routing_id, + payload->sink_address, + payload->delay_hp); + + /* Update the First-Hop Table maintained by the SSR module. */ + ssr_on_registration(payload->source_address, + payload->source_routing_id, + payload->sink_address, + payload->delay_hp); +} diff --git a/lib/wpc/ssr/fht.c b/lib/wpc/ssr/fht.c new file mode 100644 index 0000000..9cc8ddc --- /dev/null +++ b/lib/wpc/ssr/fht.c @@ -0,0 +1,281 @@ +/* Wirepas Oy licensed under Apache License, Version 2.0 + * + * See file LICENSE for full license details. + * + * First-Hop Table (FHT) implementation. + * + * Open-addressing hash table with linear probing and tombstone deletion. + * Automatically grows (doubles) when the (occupied + tombstone) load exceeds + * 0.7 of capacity. Rehashes on every fht_purge_expired call to eliminate + * tombstones accumulated since the last purge. + */ +#define LOG_MODULE_NAME "fht" +#define MAX_LOG_LEVEL INFO_LOG_LEVEL +#include "logger.h" + +#include "fht.h" /* lib/wpc/include/fht.h via PRIVATE include path */ +#include "platform.h" + +#include + +#define FHT_INITIAL_CAPACITY 64 +#define FHT_DEFAULT_VALIDITY 2400 /* seconds (40 min) */ +#define FHT_LOAD_NUM 7 +#define FHT_LOAD_DEN 10 /* max load factor = 0.7 */ +#define FHT_INVALID_SLOT ((size_t) -1) + +typedef enum +{ + SLOT_EMPTY = 0, + SLOT_OCCUPIED = 1, + SLOT_TOMBSTONE = 2 +} slot_state_t; + +typedef struct +{ + uint32_t node_id; + uint32_t first_hop_id; + uint32_t last_refresh; + slot_state_t state; +} fht_slot_t; + +struct fht +{ + fht_slot_t * slots; + size_t capacity; /* always a power of 2 */ + size_t count; /* occupied entries */ + size_t tombstones; + uint32_t validity_s; +}; + +/* ---- hash ---------------------------------------------------------------- */ + +static uint32_t hash_u32(uint32_t k) +{ + k ^= k >> 16; + k *= UINT32_C(0x45d9f3b); + k ^= k >> 16; + k *= UINT32_C(0x45d9f3b); + k ^= k >> 16; + return k; +} + +/* ---- probe --------------------------------------------------------------- */ + +/* + * Linear-probe for node_id. + * Sets *found = 1 and returns the slot index if the key exists. + * Otherwise sets *found = 0 and returns the best insertion slot + * (first tombstone along the probe path, or the first empty slot). + * Returns FHT_INVALID_SLOT only if no usable slot exists. + */ +static size_t probe(const fht_t * t, uint32_t node_id, int * found) +{ + size_t mask = t->capacity - 1; + size_t idx = hash_u32(node_id) & mask; + size_t tomb = FHT_INVALID_SLOT; + + *found = 0; + + for (size_t i = 0; i < t->capacity; i++) + { + size_t pos = (idx + i) & mask; + const fht_slot_t * s = &t->slots[pos]; + + if (s->state == SLOT_EMPTY) + { + return (tomb != FHT_INVALID_SLOT) ? tomb : pos; + } + if (s->state == SLOT_TOMBSTONE) + { + if (tomb == FHT_INVALID_SLOT) + tomb = pos; + continue; + } + if (s->node_id == node_id) + { + *found = 1; + return pos; + } + } + /* Should not happen with correct load-factor management. */ + return tomb; +} + +/* ---- resize / rehash ----------------------------------------------------- */ + +static int resize(fht_t * t, size_t new_cap) +{ + fht_slot_t * old = t->slots; + size_t old_n = t->capacity; + size_t old_count = t->count; + size_t old_tombstones = t->tombstones; + + fht_slot_t * fresh = (fht_slot_t *) Platform_malloc(new_cap * sizeof(*fresh)); + if (!fresh) + return -1; + memset(fresh, 0, new_cap * sizeof(*fresh)); + + t->slots = fresh; + t->capacity = new_cap; + t->count = 0; + t->tombstones = 0; + + for (size_t i = 0; i < old_n; i++) + { + if (old[i].state != SLOT_OCCUPIED) + continue; + int found = 0; + size_t pos = probe(t, old[i].node_id, &found); + if (pos == FHT_INVALID_SLOT || found) + { + Platform_free(fresh, new_cap * sizeof(*fresh)); + t->slots = old; + t->capacity = old_n; + t->count = old_count; + t->tombstones = old_tombstones; + return -1; + } + t->slots[pos] = old[i]; + t->slots[pos].state = SLOT_OCCUPIED; + t->count++; + } + + Platform_free(old, old_n * sizeof(*old)); + return 0; +} + +static int maybe_grow(fht_t * t) +{ + size_t used = t->count + t->tombstones; + if (used * FHT_LOAD_DEN < t->capacity * FHT_LOAD_NUM) + return 0; + return resize(t, t->capacity * 2); +} + +/* ---- public API ---------------------------------------------------------- */ + +fht_t * fht_create(void) +{ + fht_t * t = (fht_t *) Platform_malloc(sizeof(*t)); + if (!t) + return NULL; + memset(t, 0, sizeof(*t)); + + t->slots = (fht_slot_t *) Platform_malloc(FHT_INITIAL_CAPACITY * sizeof(*t->slots)); + if (!t->slots) + { + Platform_free(t, sizeof(*t)); + return NULL; + } + memset(t->slots, 0, FHT_INITIAL_CAPACITY * sizeof(*t->slots)); + + t->capacity = FHT_INITIAL_CAPACITY; + t->validity_s = FHT_DEFAULT_VALIDITY; + return t; +} + +void fht_destroy(fht_t * t) +{ + if (!t) + return; + Platform_free(t->slots, t->capacity * sizeof(*t->slots)); + Platform_free(t, sizeof(*t)); +} + +void fht_clear(fht_t * t) +{ + if (!t) + return; + + memset(t->slots, 0, t->capacity * sizeof(*t->slots)); + t->count = 0; + t->tombstones = 0; +} + +void fht_set_validity(fht_t * t, uint32_t seconds) +{ + t->validity_s = seconds; +} + +uint32_t fht_get_validity(const fht_t * t) +{ + return t->validity_s; +} + +int fht_update_route(fht_t * t, + uint32_t node_id, + uint32_t first_hop_id, + uint32_t timestamp) +{ + if (maybe_grow(t) < 0) + return -1; + + int found = 0; + size_t pos = probe(t, node_id, &found); + if (pos == FHT_INVALID_SLOT) + return -1; + + fht_slot_t * s = &t->slots[pos]; + if (found) + { + /* Reject stale updates. */ + if (timestamp <= s->last_refresh) + return 0; + } + else + { + if (s->state == SLOT_TOMBSTONE) + t->tombstones--; + t->count++; + } + + s->node_id = node_id; + s->first_hop_id = first_hop_id; + s->last_refresh = timestamp; + s->state = SLOT_OCCUPIED; + + LOGD("FHT update: node=%u hop=%u ts=%u\n", + node_id, first_hop_id, timestamp); + + return 0; +} + +void fht_get_first_hop(const fht_t * t, + uint32_t node_id, + uint32_t now, + uint32_t * first_hop_id) +{ + int found = 0; + size_t pos = probe(t, node_id, &found); + + if (pos != FHT_INVALID_SLOT && found) + { + const fht_slot_t * s = &t->slots[pos]; + if (now - s->last_refresh < t->validity_s) + { + *first_hop_id = s->first_hop_id; + return; + } + } + + *first_hop_id = 0; +} + +void fht_purge_expired(fht_t * t, uint32_t now) +{ + for (size_t i = 0; i < t->capacity; i++) + { + fht_slot_t * s = &t->slots[i]; + if (s->state == SLOT_OCCUPIED && now - s->last_refresh >= t->validity_s) + { + s->state = SLOT_TOMBSTONE; + t->count--; + t->tombstones++; + } + } + + /* Rehash to eliminate accumulated tombstones. */ + if (t->tombstones > 0) + resize(t, t->capacity); +} diff --git a/lib/wpc/ssr/ssr.c b/lib/wpc/ssr/ssr.c new file mode 100644 index 0000000..37d8431 --- /dev/null +++ b/lib/wpc/ssr/ssr.c @@ -0,0 +1,251 @@ +/* Wirepas Oy licensed under Apache License, Version 2.0 + * + * See file LICENSE for full license details. + * + * SSR integration layer for c-mesh-api. + * + * The module stores the first hop learned from SSR registrations and exposes + * sink-filtered lookups for the send path. + */ +#define LOG_MODULE_NAME "ssr" +#define MAX_LOG_LEVEL INFO_LOG_LEVEL +#include "logger.h" + +#include "ssr.h" /* lib/wpc/include/ssr.h via PRIVATE include path */ +#include "ssr_backend.h" +#include "fht.h" /* lib/wpc/include/fht.h via PRIVATE include path */ +#include "platform.h" + +static fht_t * m_fht = NULL; +static onSsrRegistration_cb_f m_reg_cb = NULL; +static uint32_t m_sink_address = 0; +static bool m_sink_address_configured = false; + +static void clear_route_table_locked(void) +{ + if (m_fht != NULL) + { + fht_clear(m_fht); + } +} + +void ssr_set_sink_address_locked(uint32_t sink_address) +{ + if (!m_sink_address_configured || m_sink_address != sink_address) + { + clear_route_table_locked(); + } + + m_sink_address = sink_address; + m_sink_address_configured = true; +} + +void ssr_clear_sink_address_locked(void) +{ + clear_route_table_locked(); + m_sink_address = 0; + m_sink_address_configured = false; +} + +void ssr_reset_routes_locked(void) +{ + clear_route_table_locked(); +} + +bool ssr_get_first_hop_locked(uint32_t dest, + uint32_t *first_hop_id) +{ + uint32_t now = (uint32_t) (Platform_get_timestamp_ms_monotonic() / 1000); + uint32_t hop = 0; + + *first_hop_id = 0; + + if (m_fht == NULL) + { + return false; + } + + fht_get_first_hop(m_fht, dest, now, &hop); + if (hop == 0) + { + return false; + } + + *first_hop_id = hop; + return true; +} + +int ssr_init(void) +{ + if (!Platform_lock_ssr()) + return -1; + + if (m_fht != NULL) + { + /* Already initialised; idempotent. */ + Platform_unlock_ssr(); + return 0; + } + + m_fht = fht_create(); + if (m_fht == NULL) + { + LOGE("SSR: failed to allocate First-Hop Table\n"); + Platform_unlock_ssr(); + return -1; + } + + LOGI("SSR: initialised\n"); + Platform_unlock_ssr(); + return 0; +} + +void ssr_deinit(void) +{ + if (!Platform_lock_ssr()) + return; + + fht_destroy(m_fht); + m_fht = NULL; + m_reg_cb = NULL; + m_sink_address = 0; + m_sink_address_configured = false; + + Platform_unlock_ssr(); +} + +void ssr_set_sink_address(uint32_t sink_address) +{ + if (!Platform_lock_ssr()) + return; + + ssr_set_sink_address_locked(sink_address); + Platform_unlock_ssr(); +} + +void ssr_clear_sink_address(void) +{ + if (!Platform_lock_ssr()) + return; + + ssr_clear_sink_address_locked(); + Platform_unlock_ssr(); +} + +void ssr_reset_routes(void) +{ + if (!Platform_lock_ssr()) + return; + + ssr_reset_routes_locked(); + Platform_unlock_ssr(); +} + +void ssr_on_registration(uint32_t source_address, + uint32_t source_routing_id, + uint32_t sink_address, + uint32_t delay_hp) +{ + onSsrRegistration_cb_f reg_cb = NULL; + + if (!Platform_lock_ssr()) + return; + + if (m_fht == NULL) + { + Platform_unlock_ssr(); + return; + } + + if (!m_sink_address_configured) + { + LOGW("SSR registration discarded: local sink address not configured\n"); + Platform_unlock_ssr(); + return; + } + + if (sink_address != m_sink_address) + { + LOGW("SSR registration discarded: src=%u hop=%u sink=%u expected=%u\n", + source_address, source_routing_id, sink_address, m_sink_address); + Platform_unlock_ssr(); + return; + } + + uint32_t now = (uint32_t) (Platform_get_timestamp_ms_monotonic() / 1000); + uint32_t delay_s = delay_hp / 1024; + /* Back-calculate generation time: delay_hp is in 1/1024 s units. */ + uint32_t generated_at = delay_s >= now ? 0 : now - delay_s; + + LOGI("SSR registration: src=%u hop=%u sink=%u delay_hp=%u ts=%u\n", + source_address, source_routing_id, sink_address, delay_hp, generated_at); + + fht_update_route(m_fht, + source_address, + source_routing_id, + generated_at); + + reg_cb = m_reg_cb; + Platform_unlock_ssr(); + + if (reg_cb != NULL) + { + reg_cb(source_address, source_routing_id, delay_hp); + } +} + +bool ssr_get_first_hop(uint32_t dest, + uint32_t *first_hop_id) +{ + if (!Platform_lock_ssr()) + return false; + bool found = ssr_get_first_hop_locked(dest, first_hop_id); + Platform_unlock_ssr(); + return found; +} + +void ssr_purge_expired(void) +{ + if (!Platform_lock_ssr()) + return; + + if (m_fht == NULL) + { + Platform_unlock_ssr(); + return; + } + + uint32_t now = (uint32_t) (Platform_get_timestamp_ms_monotonic() / 1000); + fht_purge_expired(m_fht, now); + Platform_unlock_ssr(); +} + +bool ssr_register_for_registration(onSsrRegistration_cb_f cb) +{ + if (!Platform_lock_ssr()) + return false; + + if (m_reg_cb != NULL) + { + Platform_unlock_ssr(); + return false; + } + m_reg_cb = cb; + Platform_unlock_ssr(); + return true; +} + +bool ssr_unregister_from_registration(void) +{ + if (!Platform_lock_ssr()) + return false; + + if (m_reg_cb == NULL) + { + Platform_unlock_ssr(); + return false; + } + m_reg_cb = NULL; + Platform_unlock_ssr(); + return true; +} diff --git a/lib/wpc/wpc_internal.c b/lib/wpc/wpc_internal.c index 3ffd678..367ed41 100644 --- a/lib/wpc/wpc_internal.c +++ b/lib/wpc/wpc_internal.c @@ -14,6 +14,7 @@ #include "serial.h" #include "slip.h" +#include "ssr.h" #include "wpc_types.h" #include "wpc_internal.h" #include "util.h" @@ -262,6 +263,9 @@ static void dispatch_indication(wpc_frame_t * frame, unsigned long long timestam case MSAP_CONFIG_DATA_ITEM_RX_INDICATION: msap_config_data_item_rx_indication_handler(&frame->payload.msap_config_data_item_rx_indication_payload); break; + case MSAP_SSR_REGISTRATION_INDICATION: + msap_ssr_registration_indication_handler(&frame->payload.msap_ssr_registration_indication_payload); + break; default: LOGE("Unknown indication 0x%02x\n", frame->primitive_id); LOG_PRINT_BUFFER((uint8_t *) frame, FRAME_SIZE(frame)); @@ -431,6 +435,13 @@ int WPC_Int_initialize(const char * port_name, unsigned long bitrate) return WPC_INT_GEN_ERROR; } + if (ssr_init() < 0) + { + Platform_close(); + Serial_close(); + return WPC_INT_GEN_ERROR; + } + dsap_init(); m_last_successful_answer_ts = Platform_get_timestamp_ms_monotonic(); @@ -441,6 +452,8 @@ int WPC_Int_initialize(const char * port_name, unsigned long bitrate) void WPC_Int_close(void) { + ssr_deinit(); + Platform_close(); Serial_close(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0812211..455f5e2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -37,6 +37,7 @@ add_executable(${CMAKE_PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}/scratchpad_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/callback_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/cdd_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/ssr_tests.cpp ) target_link_libraries(${CMAKE_PROJECT_NAME} @@ -44,11 +45,24 @@ target_link_libraries(${CMAKE_PROJECT_NAME} wpc ) +# Expose internal WPC headers needed by SSR unit tests. +# (fht.h and ssr.h live in lib/wpc/include/ which is PRIVATE to the wpc lib.) +target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE + ${WPC_LIB_DIR}/wpc/include +) + include(GoogleTest) gtest_discover_tests(${CMAKE_PROJECT_NAME}) +# SSR tests are split into smaller executables because some of them link WPC +# sources directly with local platform/request stubs. add_executable(meshAPI_ssr_unit ${WPC_LIB_DIR}/wpc/dsap.c + ${WPC_LIB_DIR}/wpc/msap.c + ${WPC_LIB_DIR}/wpc/reassembly/reassembly.c + ${WPC_LIB_DIR}/wpc/ssr/fht.c + ${WPC_LIB_DIR}/wpc/ssr/ssr.c + ${CMAKE_CURRENT_LIST_DIR}/ssr_tests.cpp ${CMAKE_CURRENT_LIST_DIR}/ssr_routing_unit_tests.cpp ) @@ -64,3 +78,21 @@ target_compile_options(meshAPI_ssr_unit PRIVATE -ffunction-sections -fdata-secti target_link_options(meshAPI_ssr_unit PRIVATE -Wl,--gc-sections) gtest_discover_tests(meshAPI_ssr_unit) + +add_executable(meshAPI_wpc_internal_unit + ${WPC_LIB_DIR}/wpc/wpc_internal.c + ${CMAKE_CURRENT_LIST_DIR}/wpc_internal_ssr_unit_tests.cpp +) + +target_link_libraries(meshAPI_wpc_internal_unit + GTest::gtest_main +) + +target_include_directories(meshAPI_wpc_internal_unit PRIVATE + ${WPC_INTERNAL_INCLUDE_DIRS} +) + +target_compile_options(meshAPI_wpc_internal_unit PRIVATE -ffunction-sections -fdata-sections) +target_link_options(meshAPI_wpc_internal_unit PRIVATE -Wl,--gc-sections) + +gtest_discover_tests(meshAPI_wpc_internal_unit) diff --git a/test/ssr_routing_unit_tests.cpp b/test/ssr_routing_unit_tests.cpp index e4e02bd..0428475 100644 --- a/test/ssr_routing_unit_tests.cpp +++ b/test/ssr_routing_unit_tests.cpp @@ -1,15 +1,18 @@ #include -#include #include +#include #include +#include #include #define _Static_assert static_assert extern "C" { #include "dsap.h" -#include "reassembly.h" +#include "fht.h" +#include "msap.h" +#include "ssr.h" #include "util.h" #include "wpc_constants.h" #include "wpc_types.h" @@ -40,6 +43,9 @@ struct SendRequestStub std::vector confirm_results; uint8_t mtu = 102; uint8_t confirm_capacity = 7; + unsigned long long now_ms = 0; + int malloc_call_count = 0; + int fail_on_malloc_call = -1; void reset() { @@ -48,6 +54,9 @@ struct SendRequestStub confirm_results.clear(); mtu = 102; confirm_capacity = 7; + now_ms = 0; + malloc_call_count = 0; + fail_on_malloc_call = -1; } }; @@ -61,20 +70,51 @@ void on_data_sent(uint16_t pdu_id, uint32_t buffering_delay_ms, uint8_t result) g_tx_cb.result = result; g_tx_cb.calls++; } + +enum MirrorSlotState +{ + MirrorSlotEmpty = 0, + MirrorSlotOccupied = 1, + MirrorSlotTombstone = 2, +}; + +struct MirrorFhtSlot +{ + uint32_t node_id; + uint32_t first_hop_id; + uint32_t last_refresh; + MirrorSlotState state; +}; + +struct MirrorFht +{ + MirrorFhtSlot * slots; + size_t capacity; + size_t count; + size_t tombstones; + uint32_t validity_s; +}; } // namespace -extern "C" bool Platform_lock_request(void) +extern "C" void * Platform_malloc(size_t size) { - return true; + g_stub.malloc_call_count++; + if (g_stub.fail_on_malloc_call == g_stub.malloc_call_count) + { + return nullptr; + } + return std::malloc(size); } -extern "C" void Platform_unlock_request(void) +extern "C" void Platform_free(void * ptr, size_t size) { + (void) size; + std::free(ptr); } extern "C" unsigned long long Platform_get_timestamp_ms_monotonic(void) { - return 0; + return g_stub.now_ms; } extern "C" void Platform_LOG(char level, char * module, char * format, va_list args) @@ -113,6 +153,15 @@ extern "C" int * Platform_get_logging_module_level(const char * const module_nam return &level; } +extern "C" bool Platform_lock_ssr(void) +{ + return true; +} + +extern "C" void Platform_unlock_ssr(void) +{ +} + extern "C" uint8_t WPC_Int_get_mtu(void) { return g_stub.mtu; @@ -137,32 +186,12 @@ extern "C" int WPC_Int_send_request(wpc_frame_t * frame, wpc_frame_t * confirm) return transport_result; } -extern "C" bool reassembly_add_fragment(reassembly_fragment_t * frag, size_t * full_size_p) -{ - (void) frag; - *full_size_p = 0; - return false; -} - -extern "C" bool reassembly_get_full_message(uint32_t src_add, - uint16_t packet_id, - uint8_t * buffer_p, - size_t * size) +extern "C" int WPC_Int_send_request_timeout(wpc_frame_t * frame, + wpc_frame_t * confirm, + uint16_t timeout_ms) { - (void) src_add; - (void) packet_id; - (void) buffer_p; - (void) size; - return false; -} - -extern "C" void reassembly_garbage_collect(void) -{ -} - -extern "C" void reassembly_set_max_fragment_duration(unsigned int fragment_max_duration_s) -{ - (void) fragment_max_duration_s; + (void) timeout_ms; + return WPC_Int_send_request(frame, confirm); } class SsrRoutingUnitTest : public ::testing::Test @@ -172,7 +201,12 @@ class SsrRoutingUnitTest : public ::testing::Test { g_stub.reset(); g_tx_cb.reset(); - dsap_init(); + ssr_deinit(); + } + + void TearDown() override + { + ssr_deinit(); } }; @@ -368,10 +402,8 @@ TEST_F(SsrRoutingUnitTest, LegacyFragmentedRequestUsesTraditionalFragmentPrimiti EXPECT_EQ(g_stub.frames[0].primitive_id, DSAP_DATA_TX_FRAG_REQUEST); EXPECT_EQ(g_stub.frames[1].primitive_id, DSAP_DATA_TX_FRAG_REQUEST); - const dsap_data_tx_frag_req_pl_t & first = - g_stub.frames[0].payload.dsap_data_tx_frag_request_payload; - const dsap_data_tx_frag_req_pl_t & last = - g_stub.frames[1].payload.dsap_data_tx_frag_request_payload; + const dsap_data_tx_frag_req_pl_t & first = g_stub.frames[0].payload.dsap_data_tx_frag_request_payload; + const dsap_data_tx_frag_req_pl_t & last = g_stub.frames[1].payload.dsap_data_tx_frag_request_payload; EXPECT_EQ(first.tx_options, static_cast(6u << 2)); EXPECT_EQ(first.apdu_length, 3u); @@ -474,3 +506,202 @@ TEST_F(SsrRoutingUnitTest, FragmentedSsrRequestStopsOnStackRefusal) 5); EXPECT_EQ(g_stub.frames.size(), 2u); } + +TEST_F(SsrRoutingUnitTest, MappedSsrRegistrationIgnoresOlderGeneratedTimestamp) +{ + ssr_init(); + ssr_set_sink_address(0xBBBB0001u); + g_stub.now_ms = 100u * 1000u; + + msap_ssr_registration_ind_pl_t payload = {}; + payload.source_address = 0x1234u; + payload.source_routing_id = 0xAAAAu; + payload.sink_address = 0xBBBB0001u; + payload.delay_hp = 0; + msap_ssr_registration_indication_handler(&payload); + + payload.source_routing_id = 0xCCCCu; + payload.delay_hp = 2048; + msap_ssr_registration_indication_handler(&payload); + + uint32_t hop = 0; + ASSERT_TRUE(ssr_get_first_hop(0x1234u, &hop)); + EXPECT_EQ(hop, 0xAAAAu); +} + +TEST_F(SsrRoutingUnitTest, SsrDiscardedUntilSinkAddressIsConfigured) +{ + ssr_init(); + g_stub.now_ms = 10u * 1000u; + + ssr_on_registration(0x1111u, 0x2222u, 0x3333u, 0); + + uint32_t hop = 0xFFFFFFFFu; + EXPECT_FALSE(ssr_get_first_hop(0x1111u, &hop)); + EXPECT_EQ(hop, 0u); +} + +TEST_F(SsrRoutingUnitTest, SsrApiGracefullyHandlesUseBeforeInit) +{ + uint32_t hop = 0xFFFFFFFFu; + + EXPECT_FALSE(ssr_get_first_hop(0x1234u, &hop)); + EXPECT_EQ(hop, 0u); + EXPECT_NO_FATAL_FAILURE(ssr_on_registration(0x1234u, 0x5678u, 0x9ABCu, 0)); + EXPECT_NO_FATAL_FAILURE(ssr_purge_expired()); +} + +TEST_F(SsrRoutingUnitTest, SsrInitHandlesFirstHopTableAllocationFailure) +{ + g_stub.fail_on_malloc_call = 1; + + EXPECT_EQ(ssr_init(), -1); + + uint32_t hop = 0xFFFFFFFFu; + EXPECT_FALSE(ssr_get_first_hop(0x1111u, &hop)); + EXPECT_EQ(hop, 0u); +} + +TEST_F(SsrRoutingUnitTest, SsrPurgeExpiredRemovesRoute) +{ + ssr_init(); + ssr_set_sink_address(0xAAAAu); + g_stub.now_ms = 50u * 1000u; + ssr_on_registration(0x2000u, 0x3000u, 0xAAAAu, 0); + + g_stub.now_ms = (50u + 2400u) * 1000u; + ssr_purge_expired(); + + uint32_t hop = 123u; + EXPECT_FALSE(ssr_get_first_hop(0x2000u, &hop)); + EXPECT_EQ(hop, 0u); +} + +TEST_F(SsrRoutingUnitTest, ChangingSinkAddressFlushesExistingRoutes) +{ + ssr_init(); + ssr_set_sink_address(0xAAAAu); + g_stub.now_ms = 10u * 1000u; + ssr_on_registration(0x2000u, 0x3000u, 0xAAAAu, 0); + + ssr_set_sink_address(0xBBBBu); + + uint32_t hop = 123u; + EXPECT_FALSE(ssr_get_first_hop(0x2000u, &hop)); + EXPECT_EQ(hop, 0u); +} + +TEST_F(SsrRoutingUnitTest, RegistrationDelayLargerThanUptimeDoesNotPoisonRoute) +{ + ssr_init(); + ssr_set_sink_address(0xAAAAu); + + g_stub.now_ms = 5u * 1000u; + ssr_on_registration(0x2000u, 0x3000u, 0xAAAAu, 10u * 1024u); + + g_stub.now_ms = 6u * 1000u; + ssr_on_registration(0x2000u, 0x4000u, 0xAAAAu, 0); + + uint32_t hop = 0; + ASSERT_TRUE(ssr_get_first_hop(0x2000u, &hop)); + EXPECT_EQ(hop, 0x4000u); +} + +TEST_F(SsrRoutingUnitTest, FhtCreateReturnsNullWhenInitialAllocationFails) +{ + g_stub.fail_on_malloc_call = 1; + EXPECT_EQ(fht_create(), nullptr); +} + +TEST_F(SsrRoutingUnitTest, FhtCreateReturnsNullWhenSlotAllocationFails) +{ + g_stub.fail_on_malloc_call = 2; + EXPECT_EQ(fht_create(), nullptr); +} + +TEST_F(SsrRoutingUnitTest, FhtUpdateRouteReturnsErrorWhenResizeAllocationFails) +{ + fht_t * table = fht_create(); + ASSERT_NE(table, nullptr); + fht_set_validity(table, UINT32_MAX / 2); + + for (uint32_t node = 1; node <= 45; node++) + { + ASSERT_EQ(fht_update_route(table, node, node + 1000u, 1u), 0); + } + + g_stub.fail_on_malloc_call = g_stub.malloc_call_count + 1; + EXPECT_EQ(fht_update_route(table, 46u, 1046u, 1u), -1); + fht_destroy(table); +} + +TEST_F(SsrRoutingUnitTest, FhtLookupTraversesTombstoneWhenPurgeRehashFails) +{ + fht_t * table = fht_create(); + ASSERT_NE(table, nullptr); + fht_set_validity(table, 1u); + ASSERT_EQ(fht_update_route(table, 0x55u, 0x66u, 0u), 0); + + g_stub.fail_on_malloc_call = g_stub.malloc_call_count + 1; + fht_purge_expired(table, 1u); + + uint32_t hop = 0xFFFFFFFFu; + fht_get_first_hop(table, 0x55u, 1u, &hop); + EXPECT_EQ(hop, 0u); + fht_destroy(table); +} + +TEST_F(SsrRoutingUnitTest, FhtUpdateRouteReusesTombstoneWhenPurgeRehashFails) +{ + fht_t * table = fht_create(); + ASSERT_NE(table, nullptr); + fht_set_validity(table, 1u); + ASSERT_EQ(fht_update_route(table, 0x55u, 0x66u, 0u), 0); + + g_stub.fail_on_malloc_call = g_stub.malloc_call_count + 1; + fht_purge_expired(table, 1u); + + ASSERT_EQ(fht_update_route(table, 0x55u, 0x77u, 2u), 0); + + uint32_t hop = 0u; + fht_get_first_hop(table, 0x55u, 2u, &hop); + EXPECT_EQ(hop, 0x77u); + fht_destroy(table); +} + +TEST_F(SsrRoutingUnitTest, FhtLookupReturnsZeroWhenTableHasNoEmptySlot) +{ + fht_t * table = fht_create(); + ASSERT_NE(table, nullptr); + + MirrorFht * mirror = reinterpret_cast(table); + for (size_t i = 0; i < mirror->capacity; i++) + { + mirror->slots[i].node_id = static_cast(i + 1); + mirror->slots[i].first_hop_id = static_cast(1000 + i); + mirror->slots[i].last_refresh = 0u; + mirror->slots[i].state = MirrorSlotOccupied; + } + mirror->count = mirror->capacity; + mirror->tombstones = 0u; + + uint32_t hop = 0xFFFFFFFFu; + fht_get_first_hop(table, UINT32_MAX, 1u, &hop); + EXPECT_EQ(hop, 0u); + fht_destroy(table); +} + +TEST_F(SsrRoutingUnitTest, FhtPurgeWithoutExpiredEntriesKeepsRoute) +{ + fht_t * table = fht_create(); + ASSERT_NE(table, nullptr); + fht_set_validity(table, 100u); + ASSERT_EQ(fht_update_route(table, 0x55u, 0x66u, 1000u), 0); + + fht_purge_expired(table, 1050u); + + uint32_t hop = 0; + fht_get_first_hop(table, 0x55u, 1050u, &hop); + EXPECT_EQ(hop, 0x66u); + fht_destroy(table); +} diff --git a/test/ssr_tests.cpp b/test/ssr_tests.cpp new file mode 100644 index 0000000..e984b00 --- /dev/null +++ b/test/ssr_tests.cpp @@ -0,0 +1,490 @@ +/* Wirepas Oy licensed under Apache License, Version 2.0 + * + * See file LICENSE for full license details. + * + * Unit tests for the SSR bookkeeping module (FHT + SSR integration layer). + * + * These tests do NOT require a connected Wirepas sink; they exercise only the + * in-process data structures. Platform_malloc/free map to malloc/free on + * Linux, so no WPC_initialize() call is needed. + */ + +#include + +extern "C" +{ +#include "fht.h" +#include "ssr.h" +#include "dsap.h" +#include "msap.h" +} + +#include +#include +#include + +/* -------------------------------------------------------------------------- */ +/* FHT lifecycle */ +/* -------------------------------------------------------------------------- */ + +TEST(FhtLifecycle, CreateDestroy) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_destroy(t); +} + +TEST(FhtLifecycle, DestroyNull) +{ + /* Must not crash. */ + EXPECT_NO_FATAL_FAILURE(fht_destroy(nullptr)); +} + +TEST(FhtLifecycle, DefaultValidity) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + EXPECT_EQ(fht_get_validity(t), 2400u); + fht_destroy(t); +} + +TEST(FhtLifecycle, SetGetValidity) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, 600u); + EXPECT_EQ(fht_get_validity(t), 600u); + fht_destroy(t); +} + +/* -------------------------------------------------------------------------- */ +/* FHT insert and lookup */ +/* -------------------------------------------------------------------------- */ + +class FhtInsertLookup : public ::testing::Test +{ +protected: + void SetUp() override + { + t = fht_create(); + ASSERT_NE(t, nullptr); + /* Use a large validity so entries never expire during tests. */ + fht_set_validity(t, UINT32_MAX / 2); + } + + void TearDown() override { fht_destroy(t); } + + fht_t * t = nullptr; +}; + +TEST_F(FhtInsertLookup, InsertAndLookup) +{ + ASSERT_EQ(fht_update_route(t, 0x1000, 0xAAAA, 100), 0); + + uint32_t hop = 0; + fht_get_first_hop(t, 0x1000, 101, &hop); + EXPECT_EQ(hop, 0xAAAAu); +} + +TEST_F(FhtInsertLookup, LookupMissingReturnsZero) +{ + uint32_t hop = 0xDEAD; + fht_get_first_hop(t, 0x9999, 100, &hop); + EXPECT_EQ(hop, 0u); +} + +TEST_F(FhtInsertLookup, UpdateOverwritesOlderTimestamp) +{ + ASSERT_EQ(fht_update_route(t, 0x1000, 0xAAAA, 100), 0); + ASSERT_EQ(fht_update_route(t, 0x1000, 0xCCCC, 200), 0); + + uint32_t hop = 0; + fht_get_first_hop(t, 0x1000, 201, &hop); + EXPECT_EQ(hop, 0xCCCCu); +} + +TEST_F(FhtInsertLookup, MultipleDifferentNodes) +{ + ASSERT_EQ(fht_update_route(t, 0x0001, 0xAA01, 10), 0); + ASSERT_EQ(fht_update_route(t, 0x0002, 0xAA02, 10), 0); + ASSERT_EQ(fht_update_route(t, 0x0003, 0xAA03, 10), 0); + + uint32_t hop; + fht_get_first_hop(t, 0x0001, 11, &hop); + EXPECT_EQ(hop, 0xAA01u); + + fht_get_first_hop(t, 0x0002, 11, &hop); + EXPECT_EQ(hop, 0xAA02u); + + fht_get_first_hop(t, 0x0003, 11, &hop); + EXPECT_EQ(hop, 0xAA03u); +} + +/* -------------------------------------------------------------------------- */ +/* FHT validity / expiry */ +/* -------------------------------------------------------------------------- */ + +class FhtValidity : public ::testing::Test +{ +protected: + void SetUp() override + { + t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, 100); /* 100 second window */ + } + + void TearDown() override { fht_destroy(t); } + + fht_t * t = nullptr; +}; + +TEST_F(FhtValidity, EntryValidBeforeExpiry) +{ + ASSERT_EQ(fht_update_route(t, 0x1000, 0xAAAA, 1000), 0); + + uint32_t hop = 0; + /* now = 1099; delta = 99 < validity 100 → still valid */ + fht_get_first_hop(t, 0x1000, 1099, &hop); + EXPECT_EQ(hop, 0xAAAAu); +} + +TEST_F(FhtValidity, EntryExpiredAfterWindow) +{ + ASSERT_EQ(fht_update_route(t, 0x1000, 0xAAAA, 1000), 0); + + uint32_t hop = 0xDEAD; + /* now = 1100; delta = 100 >= validity 100 → expired */ + fht_get_first_hop(t, 0x1000, 1100, &hop); + EXPECT_EQ(hop, 0u); +} + +/* -------------------------------------------------------------------------- */ +/* FHT "no overwrite with stale update" */ +/* -------------------------------------------------------------------------- */ + +TEST(FhtNoOverwrite, OlderTimestampIgnored) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, UINT32_MAX / 2); + + /* Insert with ts=200 first. */ + ASSERT_EQ(fht_update_route(t, 0x1000, 0xAA01, 200), 0); + /* This update is older (ts=100 < stored 200) and must be silently ignored. */ + ASSERT_EQ(fht_update_route(t, 0x1000, 0xAA02, 100), 0); + + uint32_t hop = 0; + fht_get_first_hop(t, 0x1000, 201, &hop); + /* First (newer) update must win. */ + EXPECT_EQ(hop, 0xAA01u); + + fht_destroy(t); +} + +/* -------------------------------------------------------------------------- */ +/* FHT purge */ +/* -------------------------------------------------------------------------- */ + +TEST(FhtPurge, ExpiredEntriesRemovedOnPurge) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, 50); + + /* Insert one fresh and one soon-to-expire entry. */ + ASSERT_EQ(fht_update_route(t, 0x0001, 0xA001, 1000), 0); + ASSERT_EQ(fht_update_route(t, 0x0002, 0xA002, 900), 0); + + /* At t=1060: node 0x0002 has age 160 > 50 → expired; 0x0001 has age 60 > 50 → expired too. */ + fht_purge_expired(t, 1060); + + uint32_t hop; + fht_get_first_hop(t, 0x0001, 1060, &hop); + EXPECT_EQ(hop, 0u); /* expired at purge */ + fht_get_first_hop(t, 0x0002, 1060, &hop); + EXPECT_EQ(hop, 0u); /* expired at purge */ + + fht_destroy(t); +} + +TEST(FhtPurge, FreshEntryKeptAfterPurge) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, 50); + + ASSERT_EQ(fht_update_route(t, 0x0001, 0xA001, 1000), 0); + ASSERT_EQ(fht_update_route(t, 0x0002, 0xA002, 900), 0); + + /* At t=1040: 0x0001 has age 40 < 50 (fresh); 0x0002 has age 140 > 50 (stale). */ + fht_purge_expired(t, 1040); + + uint32_t hop; + fht_get_first_hop(t, 0x0001, 1040, &hop); + EXPECT_EQ(hop, 0xA001u); /* still valid */ + + fht_get_first_hop(t, 0x0002, 1040, &hop); + EXPECT_EQ(hop, 0u); /* purged */ + + fht_destroy(t); +} + +/* -------------------------------------------------------------------------- */ +/* FHT resize (load-factor triggered growth) */ +/* -------------------------------------------------------------------------- */ + +TEST(FhtResize, GrowsBeyondInitialCapacity) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, UINT32_MAX / 2); + + /* Insert enough entries to exceed the 0.7 load factor of the initial + * 64-slot table (>44 entries). The table should resize transparently. */ + const int N = 100; + for (int i = 1; i <= N; i++) + { + ASSERT_EQ(fht_update_route(t, (uint32_t) i, (uint32_t) (i + 1000), 500), 0) + << "insert failed at i=" << i; + } + + /* Verify all entries are still reachable. */ + for (int i = 1; i <= N; i++) + { + uint32_t hop = 0; + fht_get_first_hop(t, (uint32_t) i, 501, &hop); + EXPECT_EQ(hop, (uint32_t) (i + 1000)) << "wrong hop for node " << i; + } + + fht_destroy(t); +} + +/* -------------------------------------------------------------------------- */ +/* FHT edge cases */ +/* -------------------------------------------------------------------------- */ + +TEST(FhtEdgeCases, NodeIdZero) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, UINT32_MAX / 2); + + ASSERT_EQ(fht_update_route(t, 0u, 0xAAAA, 10), 0); + + uint32_t hop = 0; + fht_get_first_hop(t, 0u, 11, &hop); + EXPECT_EQ(hop, 0xAAAAu); + + fht_destroy(t); +} + +TEST(FhtEdgeCases, NodeIdMax) +{ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, UINT32_MAX / 2); + + ASSERT_EQ(fht_update_route(t, UINT32_MAX, 0x1234, 10), 0); + + uint32_t hop = 0; + fht_get_first_hop(t, UINT32_MAX, 11, &hop); + EXPECT_EQ(hop, 0x1234u); + + fht_destroy(t); +} + +TEST(FhtEdgeCases, ManyCollisionsProbedCorrectly) +{ + /* Insert a batch of consecutive IDs that are likely to share hash buckets, + * then verify every entry is retrievable. */ + fht_t * t = fht_create(); + ASSERT_NE(t, nullptr); + fht_set_validity(t, UINT32_MAX / 2); + + const int N = 60; /* slightly less than initial capacity to stay in one table */ + for (int i = 0; i < N; i++) + { + ASSERT_EQ(fht_update_route(t, (uint32_t) i, (uint32_t) (i + 500), 1), 0); + } + for (int i = 0; i < N; i++) + { + uint32_t hop = 0; + fht_get_first_hop(t, (uint32_t) i, 2, &hop); + EXPECT_EQ(hop, (uint32_t) (i + 500)) << "collision probe failed for node " << i; + } + + fht_destroy(t); +} + +/* -------------------------------------------------------------------------- */ +/* SSR module */ +/* -------------------------------------------------------------------------- */ + +class SsrModuleTest : public ::testing::Test +{ +protected: + void SetUp() override + { + ssr_init(); + } + + void TearDown() override + { + ssr_deinit(); + } +}; + +TEST_F(SsrModuleTest, InitIdempotent) +{ + /* Second init must not crash or leak. */ + EXPECT_NO_FATAL_FAILURE(ssr_init()); +} + +TEST_F(SsrModuleTest, LookupAfterRegistration) +{ + /* Simulate a registration arriving with zero delay. */ + ssr_set_sink_address(0xBBBB); + ssr_on_registration(0x1111, 0xAAAA, 0xBBBB, 0); + + uint32_t hop = 0; + bool found = ssr_get_first_hop(0x1111, &hop); + EXPECT_TRUE(found); + EXPECT_EQ(hop, 0xAAAAu); +} + +TEST_F(SsrModuleTest, LookupMissingNodeReturnsFalse) +{ + uint32_t hop = 0xDEAD; + bool found = ssr_get_first_hop(0xDEAD, &hop); + EXPECT_FALSE(found); + EXPECT_EQ(hop, 0u); +} + +/* -------------------------------------------------------------------------- */ +/* SSR sink filtering */ +/* -------------------------------------------------------------------------- */ + +TEST_F(SsrModuleTest, RegistrationFromUnexpectedSinkIsDiscarded) +{ + ssr_set_sink_address(0xBEEF0001u); + ssr_on_registration(0x5000, 0xAA01, 0xBEEF0002u, 0); + + uint32_t hop = 0; + bool found = ssr_get_first_hop(0x5000, &hop); + EXPECT_FALSE(found); + EXPECT_EQ(hop, 0u); +} + +/* -------------------------------------------------------------------------- */ +/* SSR callback */ +/* -------------------------------------------------------------------------- */ + +static uint32_t g_cb_src = 0; +static uint32_t g_cb_hop = 0; +static uint32_t g_cb_delay = 0; +static int g_cb_count = 0; + +static void ssr_test_cb(uint32_t src, uint32_t hop, uint32_t delay) +{ + g_cb_src = src; + g_cb_hop = hop; + g_cb_delay = delay; + g_cb_count++; +} + +TEST_F(SsrModuleTest, CallbackInvokedOnRegistration) +{ + g_cb_count = 0; + + ASSERT_TRUE(ssr_register_for_registration(ssr_test_cb)); + + ssr_set_sink_address(0xDDDD); + ssr_on_registration(0x2222, 0xCCCC, 0xDDDD, 512); + + EXPECT_EQ(g_cb_count, 1); + EXPECT_EQ(g_cb_src, 0x2222u); + EXPECT_EQ(g_cb_hop, 0xCCCCu); + EXPECT_EQ(g_cb_delay, 512u); + + ASSERT_TRUE(ssr_unregister_from_registration()); +} + +TEST_F(SsrModuleTest, RegisterTwiceFails) +{ + ASSERT_TRUE(ssr_register_for_registration(ssr_test_cb)); + EXPECT_FALSE(ssr_register_for_registration(ssr_test_cb)); + ASSERT_TRUE(ssr_unregister_from_registration()); +} + +TEST_F(SsrModuleTest, UnregisterWhenNotRegisteredReturnsFalse) +{ + EXPECT_FALSE(ssr_unregister_from_registration()); +} + +/* -------------------------------------------------------------------------- */ +/* Frame layout assertions */ +/* -------------------------------------------------------------------------- */ + +TEST(FrameLayout, SsrTxFrameFitsInPayloadByte) +{ + /* The wpc_frame_t union payload must fit in UINT8_MAX bytes. */ + EXPECT_LE(sizeof(dsap_data_tx_ssr_req_pl_t), 255u); +} + +TEST(FrameLayout, SsrTxFragFrameFitsInPayloadByte) +{ + EXPECT_LE(sizeof(dsap_data_tx_ssr_frag_req_pl_t), 255u); +} + +TEST(FrameLayout, MsapSsrRegFitsInPayloadByte) +{ + EXPECT_LE(sizeof(msap_ssr_registration_ind_pl_t), 255u); +} + +TEST(FrameLayout, SsrTxHopCountFieldOffset) +{ + /* Validate that hop_count is located after the fixed header fields + * (pdu_id, src_ep, dest_add, dest_ep, qos, tx_options, buffering_delay). */ + constexpr size_t expected_offset = + sizeof(uint16_t) /* pdu_id */ + + sizeof(uint8_t) /* src_endpoint */ + + sizeof(uint32_t) /* dest_add */ + + sizeof(uint8_t) /* dest_endpoint */ + + sizeof(uint8_t) /* qos */ + + sizeof(uint8_t) /* tx_options */ + + sizeof(uint32_t) /* buffering_delay */; + + EXPECT_EQ(offsetof(dsap_data_tx_ssr_req_pl_t, hop_count), expected_offset); +} + +TEST(FrameLayout, SsrTxFragHopCountFieldOffset) +{ + constexpr size_t expected_offset = + sizeof(uint16_t) /* pdu_id */ + + sizeof(uint8_t) /* src_endpoint */ + + sizeof(uint32_t) /* dest_add */ + + sizeof(uint8_t) /* dest_endpoint */ + + sizeof(uint8_t) /* qos */ + + sizeof(uint8_t) /* tx_options */ + + sizeof(uint32_t) /* buffering_delay */ + + sizeof(uint16_t) /* full_packet_id bits */ + + sizeof(uint16_t) /* fragment_offset_flag */; + + EXPECT_EQ(offsetof(dsap_data_tx_ssr_frag_req_pl_t, hop_count), expected_offset); +} + +TEST(FrameLayout, MsapSsrRegFieldOrder) +{ + /* indication_status must be first. */ + EXPECT_EQ(offsetof(msap_ssr_registration_ind_pl_t, indication_status), 0u); + + /* source_address immediately after. */ + EXPECT_EQ(offsetof(msap_ssr_registration_ind_pl_t, source_address), + sizeof(uint8_t)); +} + +TEST(FrameLayout, SsrMaxHopsConstant) +{ + EXPECT_EQ(SSR_MAX_HOPS, 3); +} diff --git a/test/wpc_internal_ssr_unit_tests.cpp b/test/wpc_internal_ssr_unit_tests.cpp new file mode 100644 index 0000000..c76c515 --- /dev/null +++ b/test/wpc_internal_ssr_unit_tests.cpp @@ -0,0 +1,276 @@ +#include + +#include +#include + +#define _Static_assert static_assert +extern "C" +{ +#include "dsap.h" +#include "msap.h" +#include "platform.h" +#include "slip.h" +#include "wpc_constants.h" +#include "wpc_internal.h" +} + +namespace +{ +Platform_dispatch_indication_f g_dispatch_indication = nullptr; +Platform_get_indication_f g_get_indication = nullptr; +bool g_platform_close_called = false; +int g_serial_close_calls = 0; +int g_dsap_init_calls = 0; +int g_ssr_init_calls = 0; +int g_ssr_deinit_calls = 0; +int g_ssr_init_result = 0; + +struct RegistrationSpy +{ + int calls = 0; + msap_ssr_registration_ind_pl_t payload = {}; + + void reset() + { + calls = 0; + payload = {}; + } +} g_registration_spy; +} // namespace + +extern "C" int Serial_open(const char * port_name, unsigned long bitrate) +{ + (void) port_name; + (void) bitrate; + return 0; +} + +extern "C" int Serial_close(void) +{ + g_serial_close_calls++; + return 0; +} + +extern "C" int Serial_read(unsigned char * c, unsigned int timeout_ms) +{ + (void) c; + (void) timeout_ms; + return 0; +} + +extern "C" int Serial_write(const unsigned char * buffer, unsigned int buffer_size) +{ + (void) buffer; + (void) buffer_size; + return 0; +} + +extern "C" int Slip_init(write_f write_fn, read_f read_fn) +{ + (void) write_fn; + (void) read_fn; + return 0; +} + +extern "C" int Slip_send_buffer(uint8_t * buffer, uint32_t len) +{ + (void) buffer; + (void) len; + return 0; +} + +extern "C" int Slip_get_buffer(uint8_t * buffer, uint32_t len, uint16_t timeout_ms) +{ + (void) buffer; + (void) len; + (void) timeout_ms; + return -1; +} + +extern "C" bool Platform_init(Platform_get_indication_f get_indication_f, + Platform_dispatch_indication_f dispatch_indication_f) +{ + g_get_indication = get_indication_f; + g_dispatch_indication = dispatch_indication_f; + return true; +} + +extern "C" void Platform_close(void) +{ + g_platform_close_called = true; +} + +extern "C" unsigned long long Platform_get_timestamp_ms_monotonic(void) +{ + return 1234; +} + +extern "C" void Platform_LOG(char level, char * module, char * format, va_list args) +{ + (void) level; + (void) module; + (void) format; + (void) args; +} + +extern "C" void Platform_print_buffer(uint8_t * buffer, int size) +{ + (void) buffer; + (void) size; +} + +extern "C" int Platform_set_log_level(const int level) +{ + (void) level; + return 0; +} + +extern "C" int Platform_set_module_log_level(const char * const module_name, const int level) +{ + (void) module_name; + (void) level; + return 0; +} + +extern "C" int * Platform_get_logging_module_level(const char * const module_name, + const int default_level) +{ + (void) module_name; + static int level = default_level; + level = default_level; + return &level; +} + +extern "C" unsigned long long Platform_get_timestamp_ms_epoch(void) +{ + return 0; +} + +extern "C" bool Platform_lock_request(void) +{ + return true; +} + +extern "C" void Platform_unlock_request(void) +{ +} + +extern "C" void dsap_init(void) +{ + g_dsap_init_calls++; +} + +extern "C" int ssr_init(void) +{ + g_ssr_init_calls++; + return g_ssr_init_result; +} + +extern "C" void ssr_deinit(void) +{ + g_ssr_deinit_calls++; +} + +extern "C" void dsap_data_tx_indication_handler(dsap_data_tx_ind_pl_t * payload) +{ + (void) payload; +} + +extern "C" void dsap_data_rx_indication_handler(dsap_data_rx_ind_pl_t * payload, + unsigned long long timestamp_ms) +{ + (void) payload; + (void) timestamp_ms; +} + +extern "C" void dsap_data_rx_frag_indication_handler(dsap_data_rx_frag_ind_pl_t * payload, + unsigned long long timestamp_ms) +{ + (void) payload; + (void) timestamp_ms; +} + +extern "C" void msap_stack_state_indication_handler(msap_stack_state_ind_pl_t * payload) +{ + (void) payload; +} + +extern "C" void msap_app_config_data_rx_indication_handler(msap_app_config_data_rx_ind_pl_t * payload) +{ + (void) payload; +} + +extern "C" void msap_scan_nbors_indication_handler(msap_scan_nbors_ind_pl_t * payload) +{ + (void) payload; +} + +extern "C" void msap_config_data_item_rx_indication_handler(msap_config_data_item_rx_ind_pl_t * payload) +{ + (void) payload; +} + +extern "C" void msap_ssr_registration_indication_handler(msap_ssr_registration_ind_pl_t * payload) +{ + g_registration_spy.calls++; + g_registration_spy.payload = *payload; +} + +TEST(WpcInternalSsrUnitTest, DispatchCallbackRoutesSsrRegistrationIndications) +{ + g_dispatch_indication = nullptr; + g_get_indication = nullptr; + g_platform_close_called = false; + g_serial_close_calls = 0; + g_dsap_init_calls = 0; + g_ssr_init_calls = 0; + g_ssr_deinit_calls = 0; + g_ssr_init_result = 0; + g_registration_spy.reset(); + + ASSERT_EQ(WPC_Int_initialize("loopback", 125000), 0); + ASSERT_NE(g_get_indication, nullptr); + ASSERT_NE(g_dispatch_indication, nullptr); + EXPECT_EQ(g_ssr_init_calls, 1); + EXPECT_EQ(g_dsap_init_calls, 1); + + wpc_frame_t frame = {}; + frame.primitive_id = MSAP_SSR_REGISTRATION_INDICATION; + frame.payload.msap_ssr_registration_indication_payload.source_address = 0x1001u; + frame.payload.msap_ssr_registration_indication_payload.source_routing_id = 0x2002u; + frame.payload.msap_ssr_registration_indication_payload.sink_address = 0x3003u; + frame.payload.msap_ssr_registration_indication_payload.delay_hp = 0x4004u; + + g_dispatch_indication(&frame, 999u); + + ASSERT_EQ(g_registration_spy.calls, 1); + EXPECT_EQ(g_registration_spy.payload.source_address, 0x1001u); + EXPECT_EQ(g_registration_spy.payload.source_routing_id, 0x2002u); + EXPECT_EQ(g_registration_spy.payload.sink_address, 0x3003u); + EXPECT_EQ(g_registration_spy.payload.delay_hp, 0x4004u); + + WPC_Int_close(); + EXPECT_EQ(g_ssr_deinit_calls, 1); + EXPECT_TRUE(g_platform_close_called); + EXPECT_EQ(g_serial_close_calls, 1); +} + +TEST(WpcInternalSsrUnitTest, InitializeFailsWhenSsrInitFails) +{ + g_dispatch_indication = nullptr; + g_get_indication = nullptr; + g_platform_close_called = false; + g_serial_close_calls = 0; + g_dsap_init_calls = 0; + g_ssr_init_calls = 0; + g_ssr_deinit_calls = 0; + g_ssr_init_result = -1; + g_registration_spy.reset(); + + EXPECT_EQ(WPC_Int_initialize("loopback", 125000), WPC_INT_GEN_ERROR); + EXPECT_EQ(g_ssr_init_calls, 1); + EXPECT_EQ(g_dsap_init_calls, 0); + EXPECT_EQ(g_ssr_deinit_calls, 0); + EXPECT_TRUE(g_platform_close_called); + EXPECT_EQ(g_serial_close_calls, 1); +}