diff --git a/lib/api/wpc.h b/lib/api/wpc.h index ed603f2..a8176ad 100644 --- a/lib/api/wpc.h +++ b/lib/api/wpc.h @@ -952,9 +952,29 @@ app_res_e WPC_send_data(const uint8_t * bytes, * \param message_p * The message to send * \return Return code of the operation + * \note If the node role is sink and a valid SSR first-hop is available + * for the destination, SSR routing is used automatically. */ app_res_e WPC_send_data_with_options(const app_message_t * message_p); +/** + * \brief Enable or disable Selective Source Routing (SSR) + * \param enable + * True to enable SSR, false otherwise + * \return Return code of the operation + * \note Disabling SSR clears the learned SSR routes and prevents new + * routes from being learned until SSR is enabled again. + */ +app_res_e WPC_set_ssr_enabled(uint8_t enable); + +/** + * \brief Flush all learned Selective Source Routing (SSR) routes + * \return Return code of the operation + * \note SSR remains enabled after this call and new registrations can + * repopulate the route table. + */ +app_res_e WPC_flush_ssr_routes(void); + /** * \brief Set config data item * \param endpoint 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..4246834 100644 --- a/lib/wpc/CMakeLists.txt +++ b/lib/wpc/CMakeLists.txt @@ -5,8 +5,11 @@ add_library(wpc STATIC ${CMAKE_CURRENT_LIST_DIR}/msap.c ${CMAKE_CURRENT_LIST_DIR}/slip.c ${CMAKE_CURRENT_LIST_DIR}/wpc.c + ${CMAKE_CURRENT_LIST_DIR}/wpc_ssr.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 +20,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..f5b1540 --- /dev/null +++ b/lib/wpc/include/ssr.h @@ -0,0 +1,125 @@ +/* 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. + */ +void 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_ssr.h b/lib/wpc/include/wpc_ssr.h new file mode 100644 index 0000000..ee5747e --- /dev/null +++ b/lib/wpc/include/wpc_ssr.h @@ -0,0 +1,19 @@ +#ifndef WPC_SSR_H__ +#define WPC_SSR_H__ + +#include +#include + +#include "wpc.h" + +void wpc_ssr_reset(void); +void wpc_ssr_init(void); +void wpc_ssr_close(void); +bool wpc_ssr_set_enabled(bool enabled); +bool wpc_ssr_reset_routes(void); +void wpc_ssr_on_role_read(app_role_t role); +void wpc_ssr_on_role_set(app_role_t role); +void wpc_ssr_on_node_address_known(app_addr_t node_address); +bool wpc_ssr_get_first_hop_if_sink(app_addr_t dst_addr, uint32_t * first_hop_id); + +#endif /* WPC_SSR_H__ */ 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..79c2cb6 100644 --- a/lib/wpc/makefile +++ b/lib/wpc/makefile @@ -2,6 +2,7 @@ WPC_MODULE = $(SOURCEPREFIX)wpc/ SOURCES += $(WPC_MODULE)wpc.c +SOURCES += $(WPC_MODULE)wpc_ssr.c SOURCES += $(WPC_MODULE)slip.c SOURCES += $(WPC_MODULE)wpc_internal.c SOURCES += $(WPC_MODULE)dsap.c @@ -9,5 +10,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..c28134b --- /dev/null +++ b/lib/wpc/ssr/fht.c @@ -0,0 +1,266 @@ +/* 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 */ + +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). + */ +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 = (size_t) -1; + + 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) + { + *found = 0; + return (tomb != (size_t) -1) ? tomb : pos; + } + if (s->state == SLOT_TOMBSTONE) + { + if (tomb == (size_t) -1) + tomb = pos; + continue; + } + if (s->node_id == node_id) + { + *found = 1; + return pos; + } + } + /* Should not happen with correct load-factor management. */ + *found = 0; + 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; + + 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; + size_t pos = probe(t, old[i].node_id, &found); + 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; + size_t pos = probe(t, node_id, &found); + + 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; + size_t pos = probe(t, node_id, &found); + + if (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..0cb913c --- /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; +} + +void ssr_init(void) +{ + if (!Platform_lock_ssr()) + return; + + if (m_fht != NULL) + { + /* Already initialised; idempotent. */ + Platform_unlock_ssr(); + return; + } + + m_fht = fht_create(); + if (m_fht == NULL) + { + LOGE("SSR: failed to allocate First-Hop Table\n"); + } + else + { + LOGI("SSR: initialised\n"); + } + + Platform_unlock_ssr(); +} + +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.c b/lib/wpc/wpc.c index 43c4d34..a8b64c5 100644 --- a/lib/wpc/wpc.c +++ b/lib/wpc/wpc.c @@ -14,6 +14,7 @@ #include "wpc.h" // For DEFAULT_BITRATE #include "wpc_internal.h" +#include "wpc_ssr.h" #include "platform.h" // For Platform_get_timestamp_ms_monotonic() /** @@ -42,13 +43,34 @@ */ static unsigned int m_timeout_after_stop_task_s = DEFAULT_TIMEOUT_AFTER_STOP_STACK_S; +static void refresh_ssr_node_address_if_sink(app_role_t role) +{ + if (GET_BASE_ROLE(role) != APP_ROLE_SINK) + { + return; + } + + app_addr_t node_address; + (void) WPC_get_node_address(&node_address); +} + +static void initialize_ssr_state_from_node(void) +{ + app_role_t role; + + (void) WPC_get_role(&role); +} + app_res_e WPC_initialize(const char * port_name, unsigned long bitrate) { + wpc_ssr_reset(); int res = WPC_Int_initialize(port_name, bitrate); if (res == 0) { WPC_Int_set_mtu(); + wpc_ssr_init(); + initialize_ssr_state_from_node(); } return res == 0 ? APP_RES_OK : APP_RES_INTERNAL_ERROR; @@ -56,6 +78,7 @@ app_res_e WPC_initialize(const char * port_name, unsigned long bitrate) void WPC_close(void) { + wpc_ssr_close(); WPC_Int_close(); } @@ -85,7 +108,7 @@ app_res_e WPC_set_max_poll_fail_duration(unsigned int duration_s) { if (WPC_Int_set_timeout_s_no_answer(duration_s)) { - // keep track of the timeout to stay allign with the timeout for + // keep track of the timeout to stay aligned with the timeout for // status after stack is stopped m_timeout_after_stop_task_s = duration_s; return APP_RES_OK; @@ -111,14 +134,30 @@ app_res_e WPC_set_max_fragment_duration(unsigned int duration_s) app_res_e WPC_get_role(app_role_t * role_p) { int res = csap_attribute_read_request(C_NODE_ROLE_ID, 1, role_p); - return convert_error_code(ATT_READ_ERROR_CODE_LUT, res); + app_res_e ret = convert_error_code(ATT_READ_ERROR_CODE_LUT, res); + + if (ret == APP_RES_OK) + { + wpc_ssr_on_role_read(*role_p); + refresh_ssr_node_address_if_sink(*role_p); + } + + return ret; } app_res_e WPC_set_role(app_role_t role) { uint8_t att = role; int res = csap_attribute_write_request(C_NODE_ROLE_ID, 1, &att); - return convert_error_code(ATT_WRITE_ERROR_CODE_LUT, res); + app_res_e ret = convert_error_code(ATT_WRITE_ERROR_CODE_LUT, res); + + if (ret == APP_RES_OK) + { + wpc_ssr_on_role_set(role); + refresh_ssr_node_address_if_sink(role); + } + + return ret; } app_res_e WPC_get_node_address(app_addr_t * addr_p) @@ -134,6 +173,7 @@ app_res_e WPC_get_node_address(app_addr_t * addr_p) } *addr_p = uint32_decode_le(att); + wpc_ssr_on_node_address_known(*addr_p); return APP_RES_OK; } @@ -143,7 +183,13 @@ app_res_e WPC_set_node_address(app_addr_t add) uint32_encode_le(add, att); int res = csap_attribute_write_request(C_NODE_ADDRESS_ID, 4, att); - return convert_error_code(ATT_WRITE_ERROR_CODE_LUT, res); + app_res_e ret = convert_error_code(ATT_WRITE_ERROR_CODE_LUT, res); + if (ret == APP_RES_OK) + { + wpc_ssr_on_node_address_known(add); + } + + return ret; } app_res_e WPC_get_network_address(net_addr_t * addr_p) @@ -644,6 +690,10 @@ app_res_e WPC_stop_stack(void) } else { + // Stopping the stack reboots the device, so all learned SSR routes + // become stale and must be rebuilt from fresh registrations. + wpc_ssr_reset_routes(); + // Active wait of 500ms to avoid a systematic timeout // at each reboot. If status is asked immediately, // stack cannot answer @@ -1146,28 +1196,64 @@ static const app_res_e SEND_DATA_ERROR_CODE_LUT[] = { APP_RES_INVALID_VALUE, // 9 APP_RES_ACCESS_DENIED // 10 }; -app_res_e WPC_send_data_with_options(const app_message_t * message_t) + +static int send_data_request(const app_message_t * message_t, const uint32_t * first_hop_id) { - int res; uint32_t dst_addr_le; uint16_t pdu_id_le; uint32_t buffering_delay_le; + uint32_encode_le(message_t->dst_addr, (uint8_t *) &dst_addr_le); uint16_encode_le(message_t->pdu_id, (uint8_t *) &pdu_id_le); uint32_encode_le(ms_to_internal_time(message_t->buffering_delay), (uint8_t *) &buffering_delay_le); - res = dsap_data_tx_request(message_t->bytes, - message_t->num_bytes, - pdu_id_le, - dst_addr_le, - (message_t->qos & 0xff) == APP_QOS_HIGH ? 1 : 0, - message_t->src_ep, - message_t->dst_ep, - message_t->on_data_sent_cb, - buffering_delay_le, - message_t->is_unack_csma_ca, - message_t->hop_limit); + if (first_hop_id != NULL) + { + uint32_t first_hop_id_le; + + uint32_encode_le(*first_hop_id, (uint8_t *) &first_hop_id_le); + return dsap_data_tx_ssr_request(message_t->bytes, + message_t->num_bytes, + pdu_id_le, + dst_addr_le, + (message_t->qos & 0xff) == APP_QOS_HIGH ? 1 : 0, + message_t->src_ep, + message_t->dst_ep, + message_t->on_data_sent_cb, + buffering_delay_le, + message_t->is_unack_csma_ca, + message_t->hop_limit, + 1, + &first_hop_id_le); + } + + return dsap_data_tx_request(message_t->bytes, + message_t->num_bytes, + pdu_id_le, + dst_addr_le, + (message_t->qos & 0xff) == APP_QOS_HIGH ? 1 : 0, + message_t->src_ep, + message_t->dst_ep, + message_t->on_data_sent_cb, + buffering_delay_le, + message_t->is_unack_csma_ca, + message_t->hop_limit); +} + +app_res_e WPC_send_data_with_options(const app_message_t * message_t) +{ + int res; + uint32_t first_hop_id = 0; + + if (wpc_ssr_get_first_hop_if_sink(message_t->dst_addr, &first_hop_id)) + { + res = send_data_request(message_t, &first_hop_id); + } + else + { + res = send_data_request(message_t, NULL); + } if (res != 0) { @@ -1205,6 +1291,16 @@ app_res_e WPC_send_data(const uint8_t * bytes, return WPC_send_data_with_options(&message); } +app_res_e WPC_set_ssr_enabled(uint8_t enable) +{ + return wpc_ssr_set_enabled(enable != 0) ? APP_RES_OK : APP_RES_INTERNAL_ERROR; +} + +app_res_e WPC_flush_ssr_routes(void) +{ + return wpc_ssr_reset_routes() ? APP_RES_OK : APP_RES_INTERNAL_ERROR; +} + static const app_res_e CDC_ITEM_SET_ERROR_CODE_LUT[] = { APP_RES_OK, // 0 APP_RES_NODE_NOT_A_SINK, // 1 diff --git a/lib/wpc/wpc_internal.c b/lib/wpc/wpc_internal.c index 3ffd678..527a8e5 100644 --- a/lib/wpc/wpc_internal.c +++ b/lib/wpc/wpc_internal.c @@ -262,6 +262,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)); diff --git a/lib/wpc/wpc_ssr.c b/lib/wpc/wpc_ssr.c new file mode 100644 index 0000000..d7357e2 --- /dev/null +++ b/lib/wpc/wpc_ssr.c @@ -0,0 +1,171 @@ +/* Wirepas Oy licensed under Apache License, Version 2.0 + * + * See file LICENSE for full license details. + * + */ +#define LOG_MODULE_NAME "wpc" +#define MAX_LOG_LEVEL INFO_LOG_LEVEL +#include "logger.h" + +#include "platform.h" +#include "ssr_backend.h" +#include "ssr.h" +#include "wpc_ssr.h" + +static bool m_ssr_role_known = false; +static app_role_t m_ssr_role = APP_ROLE_UNKNOWN; +static bool m_ssr_node_address_known = false; +static app_addr_t m_ssr_node_address = 0; +static bool m_ssr_enabled = true; + +static bool is_sink_role(app_role_t role) +{ + return GET_BASE_ROLE(role) == APP_ROLE_SINK; +} + +static void reset_cached_ssr_state(void) +{ + m_ssr_role_known = false; + m_ssr_role = APP_ROLE_UNKNOWN; + m_ssr_node_address_known = false; + m_ssr_node_address = 0; +} + +static void apply_cached_ssr_state_locked(void) +{ + if (!m_ssr_enabled || !m_ssr_role_known || !is_sink_role(m_ssr_role) || !m_ssr_node_address_known) + { + ssr_clear_sink_address_locked(); + return; + } + + ssr_set_sink_address_locked(m_ssr_node_address); +} + +void wpc_ssr_reset(void) +{ + reset_cached_ssr_state(); + m_ssr_enabled = true; +} + +void wpc_ssr_init(void) +{ + ssr_init(); + if (!Platform_lock_ssr()) + { + return; + } + + ssr_clear_sink_address_locked(); + Platform_unlock_ssr(); +} + +void wpc_ssr_close(void) +{ + ssr_deinit(); + wpc_ssr_reset(); +} + +bool wpc_ssr_set_enabled(bool enabled) +{ + if (!Platform_lock_ssr()) + { + return false; + } + + m_ssr_enabled = enabled; + apply_cached_ssr_state_locked(); + Platform_unlock_ssr(); + return true; +} + +bool wpc_ssr_reset_routes(void) +{ + if (!Platform_lock_ssr()) + { + return false; + } + + ssr_reset_routes_locked(); + Platform_unlock_ssr(); + return true; +} + +static void cache_role(app_role_t role) +{ + m_ssr_role_known = true; + m_ssr_role = role; +} + +void wpc_ssr_on_role_read(app_role_t role) +{ + if (!Platform_lock_ssr()) + { + return; + } + + cache_role(role); + if (!is_sink_role(m_ssr_role)) + { + m_ssr_node_address_known = false; + m_ssr_node_address = 0; + } + + apply_cached_ssr_state_locked(); + Platform_unlock_ssr(); +} + +void wpc_ssr_on_role_set(app_role_t role) +{ + if (!Platform_lock_ssr()) + { + return; + } + + cache_role(role); + if (!is_sink_role(m_ssr_role)) + { + m_ssr_node_address_known = false; + m_ssr_node_address = 0; + } + + apply_cached_ssr_state_locked(); + Platform_unlock_ssr(); +} + +void wpc_ssr_on_node_address_known(app_addr_t node_address) +{ + if (!Platform_lock_ssr()) + { + return; + } + + m_ssr_node_address_known = true; + m_ssr_node_address = node_address; + if (m_ssr_role_known) + { + apply_cached_ssr_state_locked(); + } + + Platform_unlock_ssr(); +} + +bool wpc_ssr_get_first_hop_if_sink(app_addr_t dst_addr, uint32_t * first_hop_id) +{ + *first_hop_id = 0; + + if (!Platform_lock_ssr()) + { + return false; + } + + if (!m_ssr_enabled || !m_ssr_role_known || !is_sink_role(m_ssr_role) || !m_ssr_node_address_known) + { + Platform_unlock_ssr(); + return false; + } + + bool found = ssr_get_first_hop_locked(dst_addr, first_hop_id); + Platform_unlock_ssr(); + return found; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0812211..9e4fd83 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,22 @@ 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}) 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 +76,40 @@ 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) + +add_executable(meshAPI_wpc_unit + ${WPC_LIB_DIR}/wpc/wpc.c + ${WPC_LIB_DIR}/wpc/wpc_ssr.c + ${CMAKE_CURRENT_LIST_DIR}/wpc_ssr_unit_tests.cpp +) + +target_link_libraries(meshAPI_wpc_unit + GTest::gtest_main +) + +target_include_directories(meshAPI_wpc_unit PRIVATE + ${WPC_INTERNAL_INCLUDE_DIRS} +) + +target_compile_options(meshAPI_wpc_unit PRIVATE -ffunction-sections -fdata-sections) +target_link_options(meshAPI_wpc_unit PRIVATE -Wl,--gc-sections) + +gtest_discover_tests(meshAPI_wpc_unit) diff --git a/test/ssr_routing_unit_tests.cpp b/test/ssr_routing_unit_tests.cpp index e4e02bd..959a7e3 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; + + ssr_init(); + + 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..865169a --- /dev/null +++ b/test/wpc_internal_ssr_unit_tests.cpp @@ -0,0 +1,237 @@ +#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; + +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" 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_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_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_TRUE(g_platform_close_called); + EXPECT_EQ(g_serial_close_calls, 1); +} diff --git a/test/wpc_ssr_unit_tests.cpp b/test/wpc_ssr_unit_tests.cpp new file mode 100644 index 0000000..5be0588 --- /dev/null +++ b/test/wpc_ssr_unit_tests.cpp @@ -0,0 +1,870 @@ +#include + +#include +#include +#include +#include +#include + +extern "C" +{ +#include "attribute.h" +#include "csap.h" +#include "dsap.h" +#include "util.h" +#include "wpc.h" +#include "wpc_constants.h" +} + +namespace +{ +enum class Event +{ + SsrDeinit, + IntClose, +}; + +struct DsTxCall +{ + std::vector bytes; + size_t len = 0; + uint16_t pdu_id = 0; + uint32_t dest_add = 0; + uint8_t qos = 0; + uint8_t src_ep = 0; + uint8_t dest_ep = 0; + onDataSent_cb_f on_data_sent_cb = nullptr; + uint32_t buffering_delay = 0; + bool is_unack_csma_ca = false; + uint8_t hop_limit = 0; + uint8_t hop_count = 0; + std::vector hops; +}; + +struct WpcStubState +{ + int int_initialize_result = 0; + int int_initialize_calls = 0; + int int_set_mtu_calls = 0; + int int_close_calls = 0; + int ssr_init_calls = 0; + int ssr_deinit_calls = 0; + int ssr_clear_sink_address_calls = 0; + int ssr_reset_routes_calls = 0; + int ssr_lock_calls = 0; + int ssr_unlock_calls = 0; + bool ssr_lock_result = true; + std::vector events; + std::vector sink_addresses; + int msap_stack_stop_result = 0; + int msap_stack_status_read_result = 0; + uint8_t stack_status = 0; + std::vector poll_request_states; + unsigned long long monotonic_ms = 0; + int attribute_read_result = 0; + int attribute_write_result = 0; + int node_address_read_result = 0; + int node_address_write_result = 0; + int role_read_result = 0; + int role_write_result = 0; + int node_address_read_calls = 0; + int role_read_calls = 0; + app_addr_t node_address = 0; + app_role_t role = APP_ROLE_SINK; + uint8_t last_read_primitive = 0; + uint16_t last_read_attribute_id = 0; + uint8_t last_read_attribute_length = 0; + uint8_t last_write_primitive = 0; + uint16_t last_write_attribute_id = 0; + uint8_t last_write_attribute_length = 0; + std::array last_write_value = {}; + std::array attribute_read_value = {}; + bool ssr_has_route = false; + uint32_t ssr_first_hop = 0; + int ssr_lookup_calls = 0; + uint32_t last_ssr_lookup_dest = 0; + int dsap_result = 0; + int dsap_ssr_result = 0; + bool dsap_called = false; + bool dsap_ssr_called = false; + DsTxCall last_dsap_call; + DsTxCall last_dsap_ssr_call; + + void reset() + { + int_initialize_result = 0; + int_initialize_calls = 0; + int_set_mtu_calls = 0; + int_close_calls = 0; + ssr_init_calls = 0; + ssr_deinit_calls = 0; + ssr_clear_sink_address_calls = 0; + ssr_reset_routes_calls = 0; + ssr_lock_calls = 0; + ssr_unlock_calls = 0; + ssr_lock_result = true; + events.clear(); + sink_addresses.clear(); + msap_stack_stop_result = 0; + msap_stack_status_read_result = 0; + stack_status = 0; + poll_request_states.clear(); + monotonic_ms = 0; + attribute_read_result = 0; + attribute_write_result = 0; + node_address_read_result = 0; + node_address_write_result = 0; + role_read_result = 0; + role_write_result = 0; + node_address_read_calls = 0; + role_read_calls = 0; + node_address = 0; + role = APP_ROLE_SINK; + last_read_primitive = 0; + last_read_attribute_id = 0; + last_read_attribute_length = 0; + last_write_primitive = 0; + last_write_attribute_id = 0; + last_write_attribute_length = 0; + last_write_value.fill(0); + attribute_read_value.fill(0); + ssr_has_route = false; + ssr_first_hop = 0; + ssr_lookup_calls = 0; + last_ssr_lookup_dest = 0; + dsap_result = 0; + dsap_ssr_result = 0; + dsap_called = false; + dsap_ssr_called = false; + last_dsap_call = {}; + last_dsap_ssr_call = {}; + } +}; + +WpcStubState g_stub; + +DsTxCall make_tx_call(const uint8_t * bytes, + 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) +{ + DsTxCall call; + call.bytes.assign(bytes, bytes + len); + call.len = len; + call.pdu_id = pdu_id; + call.dest_add = dest_add; + call.qos = qos; + call.src_ep = src_ep; + call.dest_ep = dest_ep; + call.on_data_sent_cb = on_data_sent_cb; + call.buffering_delay = buffering_delay; + call.is_unack_csma_ca = is_unack_csma_ca; + call.hop_limit = hop_limit; + call.hop_count = hop_count; + if (hop_count != 0 && hops != nullptr) + { + call.hops.assign(hops, hops + hop_count); + } + return call; +} +} // namespace + +extern "C" int WPC_Int_initialize(const char * port_name, unsigned long bitrate) +{ + (void) port_name; + (void) bitrate; + g_stub.int_initialize_calls++; + return g_stub.int_initialize_result; +} + +extern "C" void WPC_Int_set_mtu(void) +{ + g_stub.int_set_mtu_calls++; +} + +extern "C" void WPC_Int_close(void) +{ + g_stub.int_close_calls++; + g_stub.events.push_back(Event::IntClose); +} + +extern "C" void ssr_init(void) +{ + g_stub.ssr_init_calls++; +} + +extern "C" void ssr_deinit(void) +{ + g_stub.ssr_deinit_calls++; + g_stub.events.push_back(Event::SsrDeinit); +} + +extern "C" void ssr_set_sink_address(uint32_t sink_address) +{ + g_stub.sink_addresses.push_back(sink_address); +} + +extern "C" void ssr_set_sink_address_locked(uint32_t sink_address) +{ + g_stub.sink_addresses.push_back(sink_address); +} + +extern "C" void ssr_clear_sink_address(void) +{ + g_stub.ssr_clear_sink_address_calls++; + g_stub.ssr_has_route = false; + g_stub.ssr_first_hop = 0; +} + +extern "C" void ssr_clear_sink_address_locked(void) +{ + g_stub.ssr_clear_sink_address_calls++; + g_stub.ssr_has_route = false; + g_stub.ssr_first_hop = 0; +} + +extern "C" void ssr_reset_routes(void) +{ + g_stub.ssr_reset_routes_calls++; + g_stub.ssr_has_route = false; + g_stub.ssr_first_hop = 0; +} + +extern "C" void ssr_reset_routes_locked(void) +{ + g_stub.ssr_reset_routes_calls++; + g_stub.ssr_has_route = false; + g_stub.ssr_first_hop = 0; +} + +extern "C" bool ssr_get_first_hop(uint32_t dest, uint32_t * first_hop_id) +{ + g_stub.ssr_lookup_calls++; + g_stub.last_ssr_lookup_dest = dest; + if (!g_stub.ssr_has_route) + { + *first_hop_id = 0; + return false; + } + + *first_hop_id = g_stub.ssr_first_hop; + return true; +} + +extern "C" bool ssr_get_first_hop_locked(uint32_t dest, uint32_t * first_hop_id) +{ + g_stub.ssr_lookup_calls++; + g_stub.last_ssr_lookup_dest = dest; + if (!g_stub.ssr_has_route) + { + *first_hop_id = 0; + return false; + } + + *first_hop_id = g_stub.ssr_first_hop; + return true; +} + +extern "C" bool Platform_lock_ssr(void) +{ + g_stub.ssr_lock_calls++; + return g_stub.ssr_lock_result; +} + +extern "C" void Platform_unlock_ssr(void) +{ + g_stub.ssr_unlock_calls++; +} + +extern "C" 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) +{ + g_stub.dsap_called = true; + g_stub.last_dsap_call = make_tx_call(buffer, + len, + pdu_id, + dest_add, + qos, + src_ep, + dest_ep, + on_data_sent_cb, + buffering_delay, + is_unack_csma_ca, + hop_limit, + 0, + nullptr); + return g_stub.dsap_result; +} + +extern "C" 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) +{ + g_stub.dsap_ssr_called = true; + g_stub.last_dsap_ssr_call = make_tx_call(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); + return g_stub.dsap_ssr_result; +} + +extern "C" int attribute_read_request(uint8_t primitive_id, + uint16_t attribute_id, + uint8_t attribute_length, + uint8_t * attribute_value_p) +{ + g_stub.last_read_primitive = primitive_id; + g_stub.last_read_attribute_id = attribute_id; + g_stub.last_read_attribute_length = attribute_length; + if (primitive_id == CSAP_ATTRIBUTE_READ_REQUEST && attribute_id == C_NODE_ADDRESS_ID) + { + g_stub.node_address_read_calls++; + if (g_stub.node_address_read_result == 0) + { + uint32_encode_le(g_stub.node_address, attribute_value_p); + } + return g_stub.node_address_read_result; + } + + if (primitive_id == CSAP_ATTRIBUTE_READ_REQUEST && attribute_id == C_NODE_ROLE_ID) + { + g_stub.role_read_calls++; + if (g_stub.role_read_result == 0) + { + *attribute_value_p = g_stub.role; + } + return g_stub.role_read_result; + } + + if (primitive_id == MSAP_ATTRIBUTE_READ_REQUEST && attribute_id == 1) + { + if (g_stub.msap_stack_status_read_result == 0) + { + *attribute_value_p = g_stub.stack_status; + } + return g_stub.msap_stack_status_read_result; + } + + if (g_stub.attribute_read_result == 0) + { + std::memcpy(attribute_value_p, g_stub.attribute_read_value.data(), attribute_length); + } + return g_stub.attribute_read_result; +} + +extern "C" int attribute_write_request(uint8_t primitive_id, + uint16_t attribute_id, + uint8_t attribute_length, + const uint8_t * attribute_value_p) +{ + g_stub.last_write_primitive = primitive_id; + g_stub.last_write_attribute_id = attribute_id; + g_stub.last_write_attribute_length = attribute_length; + std::memcpy(g_stub.last_write_value.data(), attribute_value_p, attribute_length); + + if (attribute_id == C_NODE_ADDRESS_ID) + { + if (g_stub.node_address_write_result == 0) + { + g_stub.node_address = uint32_decode_le(attribute_value_p); + } + return g_stub.node_address_write_result; + } + + if (attribute_id == C_NODE_ROLE_ID) + { + if (g_stub.role_write_result == 0) + { + g_stub.role = attribute_value_p[0]; + } + return g_stub.role_write_result; + } + + return g_stub.attribute_write_result; +} + +extern "C" int msap_stack_stop_request(void) +{ + return g_stub.msap_stack_stop_result; +} + +extern "C" void WPC_Int_disable_poll_request(bool disable) +{ + g_stub.poll_request_states.push_back(disable); +} + +extern "C" unsigned long long Platform_get_timestamp_ms_monotonic(void) +{ + return g_stub.monotonic_ms++; +} + +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; +} + +class WpcSsrUnitTest : public ::testing::Test +{ +protected: + void SetUp() override + { + WPC_close(); + g_stub.reset(); + } +}; + +TEST_F(WpcSsrUnitTest, InitializePrimesSsrStateFromPreconfiguredSink) +{ + g_stub.role = APP_ROLE_SINK; + g_stub.node_address = 0x11223344u; + + EXPECT_EQ(WPC_initialize("loopback", 125000), APP_RES_OK); + EXPECT_EQ(g_stub.int_initialize_calls, 1); + EXPECT_EQ(g_stub.int_set_mtu_calls, 1); + EXPECT_EQ(g_stub.ssr_init_calls, 1); + ASSERT_EQ(g_stub.sink_addresses.size(), 1u); + EXPECT_EQ(g_stub.sink_addresses.front(), 0x11223344u); + EXPECT_EQ(g_stub.ssr_clear_sink_address_calls, 2); + EXPECT_EQ(g_stub.role_read_calls, 1); + EXPECT_EQ(g_stub.node_address_read_calls, 1); +} + +TEST_F(WpcSsrUnitTest, InitializeSkipsNodeAddressReadWhenNodeIsNotSink) +{ + g_stub.role = APP_ROLE_HEADNODE; + g_stub.node_address = 0x11223344u; + + EXPECT_EQ(WPC_initialize("loopback", 125000), APP_RES_OK); + EXPECT_TRUE(g_stub.sink_addresses.empty()); + EXPECT_EQ(g_stub.role_read_calls, 1); + EXPECT_EQ(g_stub.node_address_read_calls, 0); + EXPECT_EQ(g_stub.ssr_clear_sink_address_calls, 2); +} + +TEST_F(WpcSsrUnitTest, InitializeLeavesSsrDisabledWhenNodeAddressReadFails) +{ + g_stub.role = APP_ROLE_SINK; + g_stub.node_address_read_result = 1; + + EXPECT_EQ(WPC_initialize("loopback", 125000), APP_RES_OK); + EXPECT_TRUE(g_stub.sink_addresses.empty()); + EXPECT_EQ(g_stub.role_read_calls, 1); + EXPECT_EQ(g_stub.node_address_read_calls, 1); + EXPECT_EQ(g_stub.ssr_clear_sink_address_calls, 2); +} + +TEST_F(WpcSsrUnitTest, InitializeFailureSkipsSsrSetup) +{ + g_stub.int_initialize_result = -1; + + EXPECT_EQ(WPC_initialize("loopback", 125000), APP_RES_INTERNAL_ERROR); + EXPECT_EQ(g_stub.int_set_mtu_calls, 0); + EXPECT_EQ(g_stub.ssr_init_calls, 0); + EXPECT_TRUE(g_stub.sink_addresses.empty()); +} + +TEST_F(WpcSsrUnitTest, CloseDeinitializesSsrBeforeClosingTransport) +{ + WPC_close(); + + ASSERT_EQ(g_stub.events.size(), 2u); + EXPECT_EQ(g_stub.events[0], Event::SsrDeinit); + EXPECT_EQ(g_stub.events[1], Event::IntClose); + EXPECT_EQ(g_stub.ssr_deinit_calls, 1); + EXPECT_EQ(g_stub.int_close_calls, 1); +} + +TEST_F(WpcSsrUnitTest, StopStackResetsSsrRoutesWhenStopTriggersReboot) +{ + g_stub.msap_stack_stop_result = 0; + g_stub.stack_status = 0; + + EXPECT_EQ(WPC_stop_stack(), APP_RES_OK); + EXPECT_EQ(g_stub.ssr_reset_routes_calls, 1); + ASSERT_EQ(g_stub.poll_request_states.size(), 2u); + EXPECT_TRUE(g_stub.poll_request_states[0]); + EXPECT_FALSE(g_stub.poll_request_states[1]); +} + +TEST_F(WpcSsrUnitTest, FlushSsrRoutesClearsLearnedRoutesButKeepsSsrEnabled) +{ + static const uint8_t bytes[] = { 0x10, 0x20 }; + ASSERT_EQ(WPC_set_node_address(0x01020304u), APP_RES_OK); + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0x0A0B0C0Du; + + EXPECT_EQ(WPC_flush_ssr_routes(), APP_RES_OK); + EXPECT_EQ(g_stub.ssr_reset_routes_calls, 1); + EXPECT_FALSE(g_stub.ssr_has_route); + + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 0x1234u; + message.dst_addr = 0x11223344u; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_EQ(g_stub.ssr_lookup_calls, 1); + EXPECT_TRUE(g_stub.dsap_called); + EXPECT_FALSE(g_stub.dsap_ssr_called); +} + +TEST_F(WpcSsrUnitTest, FlushSsrRoutesReturnsInternalErrorWhenSsrLockFails) +{ + g_stub.ssr_lock_result = false; + + EXPECT_EQ(WPC_flush_ssr_routes(), APP_RES_INTERNAL_ERROR); + EXPECT_EQ(g_stub.ssr_reset_routes_calls, 0); +} + +TEST_F(WpcSsrUnitTest, SetNodeAddressPropagatesSuccessfulWriteToSsr) +{ + g_stub.node_address = 0x01020304u; + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + + EXPECT_EQ(WPC_set_node_address(0x11223344u), APP_RES_OK); + + ASSERT_EQ(g_stub.sink_addresses.size(), 2u); + EXPECT_EQ(g_stub.sink_addresses.back(), 0x11223344u); + EXPECT_GE(g_stub.ssr_clear_sink_address_calls, 1); + EXPECT_EQ(g_stub.last_write_primitive, CSAP_ATTRIBUTE_WRITE_REQUEST); + EXPECT_EQ(g_stub.last_write_attribute_id, C_NODE_ADDRESS_ID); + EXPECT_EQ(g_stub.last_write_attribute_length, 4u); + EXPECT_EQ(uint32_decode_le(g_stub.last_write_value.data()), 0x11223344u); +} + +TEST_F(WpcSsrUnitTest, SetNodeAddressDoesNotUpdateSsrOnWriteFailure) +{ + g_stub.node_address_write_result = 1; + + EXPECT_NE(WPC_set_node_address(0x11223344u), APP_RES_OK); + EXPECT_TRUE(g_stub.sink_addresses.empty()); +} + +TEST_F(WpcSsrUnitTest, SetNodeAddressClearsSsrWhenRoleIsNotSink) +{ + ASSERT_EQ(WPC_set_role(APP_ROLE_HEADNODE), APP_RES_OK); + + EXPECT_EQ(WPC_set_node_address(0x11223344u), APP_RES_OK); + EXPECT_TRUE(g_stub.sink_addresses.empty()); + EXPECT_GE(g_stub.ssr_clear_sink_address_calls, 1); +} + +TEST_F(WpcSsrUnitTest, SetRoleConfiguresSsrWhenNodeBecomesSink) +{ + g_stub.node_address = 0x55667788u; + + EXPECT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + ASSERT_EQ(g_stub.sink_addresses.size(), 1u); + EXPECT_EQ(g_stub.sink_addresses.front(), 0x55667788u); + EXPECT_EQ(g_stub.role_read_calls, 0); + EXPECT_EQ(g_stub.node_address_read_calls, 1); +} + +TEST_F(WpcSsrUnitTest, GetRoleConfiguresSsrWhenNodeAddressWasKnownFirst) +{ + ASSERT_EQ(WPC_set_node_address(0x55667788u), APP_RES_OK); + g_stub.node_address = 0x55667788u; + g_stub.role = APP_ROLE_SINK; + + app_role_t role = APP_ROLE_UNKNOWN; + EXPECT_EQ(WPC_get_role(&role), APP_RES_OK); + EXPECT_EQ(role, APP_ROLE_SINK); + ASSERT_EQ(g_stub.sink_addresses.size(), 2u); + EXPECT_EQ(g_stub.sink_addresses.back(), 0x55667788u); + EXPECT_EQ(g_stub.role_read_calls, 1); + EXPECT_EQ(g_stub.node_address_read_calls, 1); +} + +TEST_F(WpcSsrUnitTest, GetRolePrimesSsrNodeAddressWhenNodeIsSink) +{ + g_stub.role = APP_ROLE_SINK; + g_stub.node_address = 0x10203040u; + + app_role_t role = APP_ROLE_UNKNOWN; + EXPECT_EQ(WPC_get_role(&role), APP_RES_OK); + EXPECT_EQ(role, APP_ROLE_SINK); + ASSERT_EQ(g_stub.sink_addresses.size(), 1u); + EXPECT_EQ(g_stub.sink_addresses.front(), 0x10203040u); + EXPECT_EQ(g_stub.role_read_calls, 1); + EXPECT_EQ(g_stub.node_address_read_calls, 1); +} + +TEST_F(WpcSsrUnitTest, SetRoleClearsSsrWhenNodeStopsBeingSink) +{ + EXPECT_EQ(WPC_set_role(APP_ROLE_HEADNODE), APP_RES_OK); + EXPECT_TRUE(g_stub.sink_addresses.empty()); + EXPECT_EQ(g_stub.ssr_clear_sink_address_calls, 1); +} + +TEST_F(WpcSsrUnitTest, SetSsrEnabledDisablesSsrAndClearsLearnedRoutes) +{ + static const uint8_t bytes[] = { 0xCC, 0xDD }; + ASSERT_EQ(WPC_set_node_address(0x01020304u), APP_RES_OK); + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0x11111111u; + + EXPECT_EQ(WPC_set_ssr_enabled(0), APP_RES_OK); + EXPECT_FALSE(g_stub.ssr_has_route); + + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 0x4321u; + message.dst_addr = 0x55667788u; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_TRUE(g_stub.dsap_called); + EXPECT_FALSE(g_stub.dsap_ssr_called); + EXPECT_EQ(g_stub.ssr_lookup_calls, 0); +} + +TEST_F(WpcSsrUnitTest, SetSsrEnabledCanReenableSsrAfterRoutesAreRelearned) +{ + static const uint8_t bytes[] = { 0xAB, 0xCD }; + ASSERT_EQ(WPC_set_node_address(0x01020304u), APP_RES_OK); + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0x11111111u; + + ASSERT_EQ(WPC_set_ssr_enabled(0), APP_RES_OK); + ASSERT_EQ(WPC_set_ssr_enabled(1), APP_RES_OK); + + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0x22222222u; + + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 0x1234u; + message.dst_addr = 0x55667788u; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_TRUE(g_stub.dsap_ssr_called); + EXPECT_FALSE(g_stub.dsap_called); + ASSERT_EQ(g_stub.last_dsap_ssr_call.hops.size(), 1u); + EXPECT_EQ(g_stub.last_dsap_ssr_call.hops[0], 0x22222222u); +} + +TEST_F(WpcSsrUnitTest, SetSsrEnabledReturnsInternalErrorWhenSsrLockFails) +{ + ASSERT_EQ(WPC_set_node_address(0x01020304u), APP_RES_OK); + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0x11111111u; + g_stub.ssr_lock_result = false; + + EXPECT_EQ(WPC_set_ssr_enabled(0), APP_RES_INTERNAL_ERROR); + + static const uint8_t bytes[] = { 0x01 }; + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 1u; + message.dst_addr = 2u; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_EQ(g_stub.ssr_lookup_calls, 0); + + g_stub.ssr_lock_result = true; + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_EQ(g_stub.ssr_lookup_calls, 1); + EXPECT_TRUE(g_stub.dsap_ssr_called); +} + +TEST_F(WpcSsrUnitTest, SendDataUsesSsrRequestWhenRouteExistsOnSink) +{ + static const uint8_t bytes[] = { 0x10, 0x20, 0x30 }; + ASSERT_EQ(WPC_set_node_address(0x01020304u), APP_RES_OK); + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0x0A0B0C0Du; + + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 0x1234u; + message.dst_addr = 0x11223344u; + message.qos = APP_QOS_HIGH; + message.src_ep = 5u; + message.dst_ep = 6u; + message.buffering_delay = 1500u; + message.hop_limit = 7u; + message.is_unack_csma_ca = true; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_EQ(g_stub.ssr_lookup_calls, 1); + EXPECT_EQ(g_stub.last_ssr_lookup_dest, 0x11223344u); + EXPECT_TRUE(g_stub.dsap_ssr_called); + EXPECT_FALSE(g_stub.dsap_called); + + EXPECT_EQ(g_stub.last_dsap_ssr_call.bytes, std::vector(bytes, bytes + sizeof(bytes))); + EXPECT_EQ(g_stub.last_dsap_ssr_call.pdu_id, 0x1234u); + EXPECT_EQ(g_stub.last_dsap_ssr_call.dest_add, 0x11223344u); + EXPECT_EQ(g_stub.last_dsap_ssr_call.qos, 1u); + EXPECT_EQ(g_stub.last_dsap_ssr_call.src_ep, 5u); + EXPECT_EQ(g_stub.last_dsap_ssr_call.dest_ep, 6u); + EXPECT_EQ(g_stub.last_dsap_ssr_call.buffering_delay, ms_to_internal_time(1500u)); + EXPECT_TRUE(g_stub.last_dsap_ssr_call.is_unack_csma_ca); + EXPECT_EQ(g_stub.last_dsap_ssr_call.hop_limit, 7u); + EXPECT_EQ(g_stub.last_dsap_ssr_call.hop_count, 1u); + ASSERT_EQ(g_stub.last_dsap_ssr_call.hops.size(), 1u); + EXPECT_EQ(g_stub.last_dsap_ssr_call.hops[0], 0x0A0B0C0Du); + EXPECT_EQ(g_stub.role_read_calls, 0); +} + +TEST_F(WpcSsrUnitTest, SendDataFallsBackToRegularRequestWhenRouteIsMissing) +{ + static const uint8_t bytes[] = { 0xAA, 0xBB }; + ASSERT_EQ(WPC_set_node_address(0x01020304u), APP_RES_OK); + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 0x4321u; + message.dst_addr = 0x55667788u; + message.qos = APP_QOS_NORMAL; + message.src_ep = 3u; + message.dst_ep = 4u; + message.buffering_delay = 500u; + message.hop_limit = 2u; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_TRUE(g_stub.dsap_called); + EXPECT_FALSE(g_stub.dsap_ssr_called); + EXPECT_EQ(g_stub.last_dsap_call.dest_add, 0x55667788u); + EXPECT_EQ(g_stub.last_dsap_call.pdu_id, 0x4321u); + EXPECT_EQ(g_stub.last_dsap_call.qos, 0u); + EXPECT_EQ(g_stub.last_dsap_call.buffering_delay, ms_to_internal_time(500u)); + EXPECT_EQ(g_stub.last_dsap_call.hop_limit, 2u); + EXPECT_EQ(g_stub.role_read_calls, 0); +} + +TEST_F(WpcSsrUnitTest, SendDataFallsBackToRegularRequestWhenSinkNodeAddressReadFails) +{ + static const uint8_t bytes[] = { 0x11, 0x22 }; + g_stub.node_address_read_result = 1; + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0xABCDEF01u; + + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 0x2222u; + message.dst_addr = 0x55667788u; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_TRUE(g_stub.dsap_called); + EXPECT_FALSE(g_stub.dsap_ssr_called); + EXPECT_EQ(g_stub.ssr_lookup_calls, 0); + EXPECT_EQ(g_stub.node_address_read_calls, 1); +} + +TEST_F(WpcSsrUnitTest, SendDataFallsBackToRegularRequestWhenNodeIsNotSink) +{ + static const uint8_t bytes[] = { 0xCC, 0xDD }; + ASSERT_EQ(WPC_set_role(APP_ROLE_HEADNODE), APP_RES_OK); + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0x11111111u; + + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 0x4321u; + message.dst_addr = 0x55667788u; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_OK); + EXPECT_TRUE(g_stub.dsap_called); + EXPECT_FALSE(g_stub.dsap_ssr_called); + EXPECT_EQ(g_stub.ssr_lookup_calls, 0); +} + +TEST_F(WpcSsrUnitTest, SendDataMapsSsrTransportErrorsThroughExistingLut) +{ + static const uint8_t bytes[] = { 0x01 }; + ASSERT_EQ(WPC_set_node_address(0x01020304u), APP_RES_OK); + ASSERT_EQ(WPC_set_role(APP_ROLE_SINK), APP_RES_OK); + g_stub.ssr_has_route = true; + g_stub.ssr_first_hop = 0x22222222u; + g_stub.dsap_ssr_result = 5; + + app_message_t message = {}; + message.bytes = bytes; + message.num_bytes = sizeof(bytes); + message.pdu_id = 1u; + message.dst_addr = 2u; + + EXPECT_EQ(WPC_send_data_with_options(&message), APP_RES_UNKNOWN_DEST); +}