From b9149dd6397358060b4299f8d571e16312bc2c7d Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Mon, 20 Apr 2026 10:53:55 -0500 Subject: [PATCH] Refactor/cov subscriptions encoding decoding test (#1296) * Add BACnetRecipientProcess type encoding and decoding functions with associated tests * Refactor COV subscription encoding and decoding functions to reduce code size and reuse existing unit tested functions. * Refactor COV subscription handling to simplify error checking --- src/bacnet/bacdest.c | 169 +++++++++ src/bacnet/bacdest.h | 34 ++ src/bacnet/basic/object/device.c | 4 +- src/bacnet/basic/server/bacnet_basic.c | 10 + src/bacnet/basic/server/bacnet_device.c | 4 +- src/bacnet/basic/service/h_cov.c | 231 ++++-------- src/bacnet/cov.c | 169 +++++++++ src/bacnet/cov.h | 31 ++ test/CMakeLists.txt | 2 + test/bacnet/bacdest/src/main.c | 86 ++++- test/bacnet/basic/object/test/bip_mock.c | 6 +- test/bacnet/basic/object/test/device_mock.c | 54 +++ .../bacnet/basic/service/h_cov/CMakeLists.txt | 83 +++++ test/bacnet/basic/service/h_cov/src/main.c | 348 ++++++++++++++++++ test/bacnet/cov/src/main.c | 91 +++++ 15 files changed, 1151 insertions(+), 171 deletions(-) create mode 100644 test/bacnet/basic/service/h_cov/CMakeLists.txt create mode 100644 test/bacnet/basic/service/h_cov/src/main.c diff --git a/src/bacnet/bacdest.c b/src/bacnet/bacdest.c index 182957d9..2af5e713 100644 --- a/src/bacnet/bacdest.c +++ b/src/bacnet/bacdest.c @@ -733,6 +733,175 @@ int bacnet_recipient_context_decode( return apdu_len; } +/** + * @brief Encode the BACnetRecipientProcess complex data + * + * BACnetRecipientProcess ::= SEQUENCE { + * recipient[0] BACnetRecipient, + * process-identifier[1] Unsigned32 + * } + * + * @param apdu Pointer to the buffer for encoding. + * @param recipient_process Pointer to the property data to be encoded. + * + * @return bytes encoded or #BACNET_STATUS_REJECT on error. + */ +int bacnet_recipient_process_encode( + uint8_t *apdu, const BACNET_RECIPIENT_PROCESS *recipient_process) +{ + int apdu_len = 0, len = 0; + + if (recipient_process) { + len = bacnet_recipient_context_encode( + apdu, 0, &recipient_process->recipient); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* Process Identifier - Unsigned32 */ + len = encode_context_unsigned( + apdu, 1, recipient_process->process_identifier); + apdu_len += len; + } + + return apdu_len; +} + +/** + * @brief Encode a BACnetRecipientProcess complex data type + * @param apdu - the APDU buffer + * @param tag_number - context tag number + * @param recipient_process Pointer to the property data to be encoded. + * @return length of the APDU buffer, or 0 if not able to encode + */ +int bacnet_recipient_process_context_encode( + uint8_t *apdu, + uint8_t tag_number, + const BACNET_RECIPIENT_PROCESS *recipient_process) +{ + int len = 0; + int apdu_len = 0; + + if (recipient_process) { + len = encode_opening_tag(apdu, tag_number); + apdu_len += len; + if (apdu) { + apdu += len; + } + len = bacnet_recipient_process_encode(apdu, recipient_process); + apdu_len += len; + if (apdu) { + apdu += len; + } + len = encode_closing_tag(apdu, tag_number); + apdu_len += len; + } + + return apdu_len; +} + +/** + * @brief Decode the BACnetRecipientProcess complex data + * + * BACnetRecipientProcess ::= SEQUENCE { + * recipient[0] BACnetRecipient, + * process-identifier[1] Unsigned32 + * } + * + * @param apdu Pointer to the buffer for decoding. + * @param apdu_size Size of the APDU buffer. + * @param recipient_process Pointer to the property data to be decoded. + * + * @return bytes decoded or #BACNET_STATUS_ERROR on error. + */ +int bacnet_recipient_process_decode( + const uint8_t *apdu, + int apdu_size, + BACNET_RECIPIENT_PROCESS *recipient_process) +{ + int len = 0, apdu_len = 0; + BACNET_RECIPIENT recipient = { 0 }; + BACNET_UNSIGNED_INTEGER unsigned_value = 0; + + if (!apdu) { + return BACNET_STATUS_ERROR; + } + /* Recipient */ + len = bacnet_recipient_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 0, &recipient); + if (len <= 0) { + return BACNET_STATUS_ERROR; + } + if (recipient_process) { + bacnet_recipient_copy(&recipient_process->recipient, &recipient); + } + apdu_len += len; + /* Process Identifier */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 1, &unsigned_value); + if (len <= 0) { + return BACNET_STATUS_ERROR; + } + if (recipient_process) { + recipient_process->process_identifier = unsigned_value; + } + apdu_len += len; + + return apdu_len; +} + +/** + * @brief Decode a BACnetRecipientProcess complex data type + * @param apdu Pointer to the APDU buffer. + * @param apdu_size - the APDU buffer length + * @param tag_number The tag number that shall hold the time stamp. + * @param value Pointer to the variable that shall take the time stamp values. + * @return number of bytes decoded, zero on opening tag mismatch, + * or BACNET_STATUS_ERROR if an error occurs + */ +int bacnet_recipient_process_context_decode( + const uint8_t *apdu, + uint32_t apdu_size, + uint8_t tag_number, + BACNET_RECIPIENT_PROCESS *value) +{ + int len = 0; + int apdu_len = 0; + + if (!bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) { + return 0; + } + apdu_len += len; + len = bacnet_recipient_process_decode( + &apdu[apdu_len], apdu_size - apdu_len, value); + if (len < 0) { + return BACNET_STATUS_ERROR; + } + apdu_len += len; + if (!bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) { + return BACNET_STATUS_ERROR; + } + apdu_len += len; + + return apdu_len; +} + +/** + * @brief Copy the BACnetRecipientProcess complex data from src to dest + * @param dest - BACnetRecipientProcess 1 structure + * @param src - BACnetRecipientProcess 2 structure + */ +void bacnet_recipient_process_copy( + BACNET_RECIPIENT_PROCESS *dest, const BACNET_RECIPIENT_PROCESS *src) +{ + if (dest && src) { + dest->process_identifier = src->process_identifier; + bacnet_recipient_copy(&dest->recipient, &src->recipient); + } +} + /** * Convert BACnet_Destination to ASCII for printing * diff --git a/src/bacnet/bacdest.h b/src/bacnet/bacdest.h index 894a6ea0..c65d36f7 100644 --- a/src/bacnet/bacdest.h +++ b/src/bacnet/bacdest.h @@ -36,6 +36,17 @@ typedef struct BACnet_Recipient { } type; } BACNET_RECIPIENT; +/** + * BACnetRecipientProcess ::= SEQUENCE { + * recipient[0] BACnetRecipient, + * process-identifier[1] Unsigned32 + * } + */ +typedef struct BACnetRecipientProcess { + uint32_t process_identifier; + BACNET_RECIPIENT recipient; +} BACNET_RECIPIENT_PROCESS; + struct BACnet_Recipient_List; typedef struct BACnet_Recipient_List { BACNET_RECIPIENT recipient; @@ -136,6 +147,29 @@ int bacnet_recipient_context_decode( uint8_t tag_number, BACNET_RECIPIENT *value); +BACNET_STACK_EXPORT +int bacnet_recipient_process_encode( + uint8_t *apdu, const BACNET_RECIPIENT_PROCESS *recipient_process); +BACNET_STACK_EXPORT +int bacnet_recipient_process_context_encode( + uint8_t *apdu, + uint8_t tag_number, + const BACNET_RECIPIENT_PROCESS *recipient_process); +BACNET_STACK_EXPORT +int bacnet_recipient_process_decode( + const uint8_t *apdu, + int apdu_size, + BACNET_RECIPIENT_PROCESS *recipient_process); +BACNET_STACK_EXPORT +int bacnet_recipient_process_context_decode( + const uint8_t *apdu, + uint32_t apdu_size, + uint8_t tag_number, + BACNET_RECIPIENT_PROCESS *value); +BACNET_STACK_EXPORT +void bacnet_recipient_process_copy( + BACNET_RECIPIENT_PROCESS *dest, const BACNET_RECIPIENT_PROCESS *src); + BACNET_STACK_EXPORT bool bacnet_recipient_address_from_ascii(BACNET_ADDRESS *src, const char *arg); BACNET_STACK_EXPORT diff --git a/src/bacnet/basic/object/device.c b/src/bacnet/basic/object/device.c index 2854b34e..8cfbc867 100644 --- a/src/bacnet/basic/object/device.c +++ b/src/bacnet/basic/object/device.c @@ -2963,8 +2963,8 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) break; #endif case PROP_ACTIVE_COV_SUBSCRIPTIONS: - if ((apdu_len = handler_cov_encode_subscriptions( - &apdu[0], apdu_max)) < 0) { + apdu_len = handler_cov_encode_subscriptions(&apdu[0], apdu_max); + if (apdu_len < 0) { rpdata->error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; apdu_len = BACNET_STATUS_ABORT; diff --git a/src/bacnet/basic/server/bacnet_basic.c b/src/bacnet/basic/server/bacnet_basic.c index d840c259..dd69eb84 100644 --- a/src/bacnet/basic/server/bacnet_basic.c +++ b/src/bacnet/basic/server/bacnet_basic.c @@ -29,6 +29,12 @@ #include "bacnet/basic/server/bacnet_basic.h" #include "bacnet/basic/server/bacnet_port.h" +#ifdef CONFIG_BACNET_BASIC_COV_SUBSCRIPTIONS_SIZE +#define BACNET_COV_SUBSCRIPTIONS_SIZE CONFIG_BACNET_BASIC_COV_SUBSCRIPTIONS_SIZE +#else +#define BACNET_COV_SUBSCRIPTIONS_SIZE 0 +#endif + /* 1s timer for basic non-critical timed tasks */ static struct mstimer BACnet_Task_Timer; /* task timer for object functionality */ @@ -277,11 +283,15 @@ void bacnet_basic_task(void) BACnet_Uptime_Seconds += elapsed_seconds; dcc_timer_seconds(elapsed_seconds); datalink_maintenance_timer(elapsed_seconds); +#if (BACNET_COV_SUBSCRIPTIONS_SIZE > 0) handler_cov_timer_seconds(elapsed_seconds); +#endif } +#if (BACNET_COV_SUBSCRIPTIONS_SIZE > 0) while (!handler_cov_fsm()) { /* waiting for COV processing to be IDLE */ } +#endif /* object specific cyclic tasks */ if (mstimer_expired(&BACnet_Object_Timer)) { elapsed_milliseconds = mstimer_elapsed(&BACnet_Object_Timer); diff --git a/src/bacnet/basic/server/bacnet_device.c b/src/bacnet/basic/server/bacnet_device.c index ec55c922..6b906e7d 100644 --- a/src/bacnet/basic/server/bacnet_device.c +++ b/src/bacnet/basic/server/bacnet_device.c @@ -2826,8 +2826,8 @@ int Device_Read_Property_Local(BACNET_READ_PROPERTY_DATA *rpdata) #endif #if (BACNET_COV_SUBSCRIPTIONS_SIZE > 0) case PROP_ACTIVE_COV_SUBSCRIPTIONS: - if ((apdu_len = handler_cov_encode_subscriptions( - &apdu[0], apdu_max)) < 0) { + apdu_len = handler_cov_encode_subscriptions(&apdu[0], apdu_max); + if (apdu_len < 0) { rpdata->error_code = ERROR_CODE_ABORT_SEGMENTATION_NOT_SUPPORTED; apdu_len = BACNET_STATUS_ABORT; diff --git a/src/bacnet/basic/service/h_cov.c b/src/bacnet/basic/service/h_cov.c index a1b54a5d..bc902450 100644 --- a/src/bacnet/basic/service/h_cov.c +++ b/src/bacnet/basic/service/h_cov.c @@ -8,7 +8,6 @@ #include #include #include -#include #include /* BACnet Stack defines - first */ #include "bacnet/bacdef.h" @@ -37,26 +36,26 @@ #define MAX_COV_PROPERTIES 2 #endif -typedef struct BACnet_COV_Address { +typedef struct BACnet_COV_Handler_Address { bool valid : 1; BACNET_ADDRESS dest; -} BACNET_COV_ADDRESS; +} BACNET_COV_HANDLER_ADDRESS; /* note: This COV service only monitors the properties of an object that have been specified in the standard. */ -typedef struct BACnet_COV_Subscription_Flags { +typedef struct BACnet_COV_Handler_Subscription_Flags { bool issueConfirmedNotifications : 1; /* optional */ bool send_requested : 1; -} BACNET_COV_SUBSCRIPTION_FLAGS; +} BACNET_COV_HANDLER_SUBSCRIPTION_FLAGS; -typedef struct BACnet_COV_Subscription { - BACNET_COV_SUBSCRIPTION_FLAGS flag; +typedef struct BACnet_COV_Handler_Subscription { + BACNET_COV_HANDLER_SUBSCRIPTION_FLAGS flag; unsigned dest_index; uint8_t invokeID; /* for confirmed COV */ uint32_t subscriberProcessIdentifier; uint32_t lifetime; /* optional */ BACNET_OBJECT_ID monitoredObjectIdentifier; -} BACNET_COV_SUBSCRIPTION; +} BACNET_COV_HANDLER_SUBSCRIPTION; #ifndef MAX_COV_SUBSCRIPTIONS #define MAX_COV_SUBSCRIPTIONS 128 @@ -71,7 +70,7 @@ static OS_Keylist COV_Subscriptions_List[MAX_NUM_DEVICES]; #ifndef MAX_COV_ADDRESSES #define MAX_COV_ADDRESSES 16 #endif -static BACNET_COV_ADDRESS COV_Addresses[MAX_COV_ADDRESSES]; +static BACNET_COV_HANDLER_ADDRESS COV_Addresses[MAX_COV_ADDRESSES]; /** * @brief Deletes a subscription @@ -81,7 +80,7 @@ static BACNET_COV_ADDRESS COV_Addresses[MAX_COV_ADDRESSES]; static bool cov_subscription_delete(uint32_t list_idx) { bool status = false; - BACNET_COV_SUBSCRIPTION *subscription = NULL; + BACNET_COV_HANDLER_SUBSCRIPTION *subscription = NULL; subscription = Keylist_Data_Delete_By_Index(COV_Subscriptions, list_idx); if (subscription) { @@ -97,16 +96,17 @@ static bool cov_subscription_delete(uint32_t list_idx) * @param list_key - keylist key * @return the subscription that was created, or NULL */ -static BACNET_COV_SUBSCRIPTION *cov_subscription_create(uint32_t list_key) +static BACNET_COV_HANDLER_SUBSCRIPTION * +cov_subscription_create(uint32_t list_key) { - BACNET_COV_SUBSCRIPTION *subscription = NULL; + BACNET_COV_HANDLER_SUBSCRIPTION *subscription = NULL; int index = 0; if (list_key >= MAX_COV_SUBSCRIPTIONS) { return NULL; } subscription = Keylist_Data(COV_Subscriptions, list_key); if (!subscription) { - subscription = calloc(1, sizeof(BACNET_COV_SUBSCRIPTION)); + subscription = calloc(1, sizeof(BACNET_COV_HANDLER_SUBSCRIPTION)); if (subscription) { index = Keylist_Data_Add(COV_Subscriptions, list_key, subscription); if (index < 0) { @@ -150,7 +150,7 @@ static void cov_address_remove_unused(void) unsigned index = 0; unsigned cov_index = 0; bool found = false; - BACNET_COV_SUBSCRIPTION *subscription = NULL; + BACNET_COV_HANDLER_SUBSCRIPTION *subscription = NULL; #ifdef BAC_ROUTING uint16_t current_dev_id = Routed_Device_Object_Index(); @@ -243,160 +243,62 @@ static int cov_address_add(const BACNET_ADDRESS *dest) return index; } -/* -BACnetCOVSubscription ::= SEQUENCE { -Recipient [0] BACnetRecipientProcess, - BACnetRecipient ::= CHOICE { - device [0] BACnetObjectIdentifier, - address [1] BACnetAddress - BACnetAddress ::= SEQUENCE { - network-number Unsigned16, -- A value of 0 indicates the local network - mac-address OCTET STRING -- A string of length 0 indicates a broadcast - } - } - BACnetRecipientProcess ::= SEQUENCE { - recipient [0] BACnetRecipient, - processIdentifier [1] Unsigned32 - } -MonitoredPropertyReference [1] BACnetObjectPropertyReference, - BACnetObjectPropertyReference ::= SEQUENCE { - objectIdentifier [0] BACnetObjectIdentifier, - propertyIdentifier [1] BACnetPropertyIdentifier, - propertyArrayIndex [2] Unsigned OPTIONAL -- used only with array datatype - -- if omitted with an array the entire array is referenced - } -IssueConfirmedNotifications [2] BOOLEAN, -TimeRemaining [3] Unsigned, -COVIncrement [4] REAL OPTIONAL -*/ - -static int cov_encode_subscription( - uint8_t *apdu, - int max_apdu, - const BACNET_COV_SUBSCRIPTION *cov_subscription) -{ - int len = 0; - int apdu_len = 0; - BACNET_OCTET_STRING octet_string = { 0 }; - BACNET_ADDRESS *dest = NULL; - - (void)max_apdu; - if (!cov_subscription) { - return 0; - } - dest = cov_address_get(cov_subscription->dest_index); - if (!dest) { - return 0; - } - /* Recipient [0] BACnetRecipientProcess - opening */ - len = encode_opening_tag(&apdu[apdu_len], 0); - apdu_len += len; - /* recipient [0] BACnetRecipient - opening */ - len = encode_opening_tag(&apdu[apdu_len], 0); - apdu_len += len; - /* CHOICE - address [1] BACnetAddress - opening */ - len = encode_opening_tag(&apdu[apdu_len], 1); - apdu_len += len; - /* network-number Unsigned16, */ - /* -- A value of 0 indicates the local network */ - len = encode_application_unsigned(&apdu[apdu_len], dest->net); - apdu_len += len; - /* mac-address OCTET STRING */ - /* -- A string of length 0 indicates a broadcast */ - if (dest->net) { - octetstring_init(&octet_string, &dest->adr[0], dest->len); - } else { - octetstring_init(&octet_string, &dest->mac[0], dest->mac_len); - } - len = encode_application_octet_string(&apdu[apdu_len], &octet_string); - apdu_len += len; - /* CHOICE - address [1] BACnetAddress - closing */ - len = encode_closing_tag(&apdu[apdu_len], 1); - apdu_len += len; - /* recipient [0] BACnetRecipient - closing */ - len = encode_closing_tag(&apdu[apdu_len], 0); - apdu_len += len; - /* processIdentifier [1] Unsigned32 */ - len = encode_context_unsigned( - &apdu[apdu_len], 1, cov_subscription->subscriberProcessIdentifier); - apdu_len += len; - /* Recipient [0] BACnetRecipientProcess - closing */ - len = encode_closing_tag(&apdu[apdu_len], 0); - apdu_len += len; - /* MonitoredPropertyReference [1] BACnetObjectPropertyReference, */ - len = encode_opening_tag(&apdu[apdu_len], 1); - apdu_len += len; - /* objectIdentifier [0] */ - len = encode_context_object_id( - &apdu[apdu_len], 0, cov_subscription->monitoredObjectIdentifier.type, - cov_subscription->monitoredObjectIdentifier.instance); - apdu_len += len; - /* propertyIdentifier [1] */ - /* FIXME: we are monitoring 2 properties! How to encode? */ - len = encode_context_enumerated(&apdu[apdu_len], 1, PROP_PRESENT_VALUE); - apdu_len += len; - /* MonitoredPropertyReference [1] - closing */ - len = encode_closing_tag(&apdu[apdu_len], 1); - apdu_len += len; - /* IssueConfirmedNotifications [2] BOOLEAN, */ - len = encode_context_boolean( - &apdu[apdu_len], 2, cov_subscription->flag.issueConfirmedNotifications); - apdu_len += len; - /* TimeRemaining [3] Unsigned, */ - len = - encode_context_unsigned(&apdu[apdu_len], 3, cov_subscription->lifetime); - apdu_len += len; - - return apdu_len; -} - /** Handle a request to list all the COV subscriptions. * @ingroup DSCOV * Invoked by a request to read the Device object's * PROP_ACTIVE_COV_SUBSCRIPTIONS. Loops through the list of COV Subscriptions, * and, for each valid one, adds its description to the APDU. * @param apdu [out] Buffer in which the APDU contents are built. - * @param max_apdu [in] Max length of the APDU buffer. - * @return How many bytes were encoded in the buffer, or -2 if the response - * would not fit within the buffer. + * @param apdu_size [in] Max length of the APDU buffer. + * @return Number of bytes encoded in the buffer, or + * #BACNET_STATUS_ABORT if the response would not fit within the buffer. */ -/* Maximume length for an encoded COV subscription - 31 bytes for BACNET IP6 - * 35 bytes for IPv4 (longest MAC) with the maximum length - * of PID (5 bytes) and lets round it up to the 64bit machine word - * alignment */ -#define MAX_COV_SUB_SIZE (40) -int handler_cov_encode_subscriptions(uint8_t *apdu, int max_apdu) +int handler_cov_encode_subscriptions(uint8_t *apdu, int apdu_size) { - if (apdu) { - uint8_t cov_sub[MAX_COV_SUB_SIZE] = { - 0, - }; - unsigned index = 0; - int apdu_len = 0; - BACNET_COV_SUBSCRIPTION *subscription = NULL; + BACNET_COV_SUBSCRIPTION data = { 0 }; + BACNET_ADDRESS *dest = NULL; + int len = 0; + int apdu_len = 0; + unsigned index = 0; + BACNET_COV_HANDLER_SUBSCRIPTION *subscription = NULL; - for (index = 0; index < Keylist_Count(COV_Subscriptions); index++) { - subscription = Keylist_Data_Index(COV_Subscriptions, index); - if (subscription) { - /* Lets encode a COV subscription into an intermediate buffer - * that can hold it */ - int len = cov_encode_subscription( - &cov_sub[0], max_apdu - apdu_len, subscription); - - if ((apdu_len + len) > max_apdu) { - return -2; - } - - /* Lets copy if and only if it fits in the buffer */ - memcpy(&apdu[apdu_len], cov_sub, len); - apdu_len += len; - } + for (index = 0; index < Keylist_Count(COV_Subscriptions); index++) { + subscription = Keylist_Data_Index(COV_Subscriptions, index); + if (!subscription) { + continue; + } + dest = cov_address_get(subscription->dest_index); + if (!dest) { + continue; + } + data.time_remaining = subscription->lifetime; + data.cov_increment_present = false; + data.cov_increment = 1.0f; /* dummy value */ + data.issue_confirmed_notifications = + subscription->flag.issueConfirmedNotifications; + data.monitored_property_reference.object_identifier.type = + subscription->monitoredObjectIdentifier.type; + data.monitored_property_reference.object_identifier.instance = + subscription->monitoredObjectIdentifier.instance; + data.monitored_property_reference.property_identifier = + PROP_PRESENT_VALUE; + data.monitored_property_reference.property_array_index = + BACNET_ARRAY_ALL; + data.recipient.process_identifier = + subscription->subscriberProcessIdentifier; + data.recipient.recipient.tag = BACNET_RECIPIENT_TAG_ADDRESS; + bacnet_address_copy(&data.recipient.recipient.type.address, dest); + len = bacnet_cov_subscription_encode(apdu, apdu_size - apdu_len, &data); + if (len <= 0) { + return BACNET_STATUS_ABORT; + } + apdu_len += len; + if (apdu) { + apdu += len; } - - return apdu_len; } - return 0; + return apdu_len; } /** Handler to initialize the COV list, clearing and disabling each entry. @@ -440,7 +342,7 @@ static bool cov_list_subscribe( bool found = true; bool address_match = false; const BACNET_ADDRESS *dest = NULL; - BACNET_COV_SUBSCRIPTION *subscription = NULL; + BACNET_COV_HANDLER_SUBSCRIPTION *subscription = NULL; /* unable to subscribe - resources? */ /* unable to cancel subscription - other? */ @@ -535,7 +437,7 @@ static bool cov_list_subscribe( } static bool cov_send_request( - BACNET_COV_SUBSCRIPTION *cov_subscription, + BACNET_COV_HANDLER_SUBSCRIPTION *cov_subscription, BACNET_PROPERTY_VALUE *value_list) { int len = 0; @@ -620,7 +522,7 @@ static void cov_lifetime_expiration_handler( unsigned index, uint32_t elapsed_seconds, uint32_t lifetime_seconds) { if (index < MAX_COV_SUBSCRIPTIONS) { - BACNET_COV_SUBSCRIPTION *subscription = + BACNET_COV_HANDLER_SUBSCRIPTION *subscription = Keylist_Data_Index(COV_Subscriptions, index); /* handle lifetime expiration */ if (lifetime_seconds >= elapsed_seconds) { @@ -655,8 +557,8 @@ static void cov_lifetime_expiration_handler( } } -/** Handler to check the list of subscribed objects for any that have changed - * and so need to have notifications sent. +/** Handler to check the list of subscribed objects for any that have + * changed and so need to have notifications sent. * @ingroup DSCOV * This handler will be invoked by the main program every second or so. * This example only handles Binary Inputs, but can be easily extended to @@ -674,13 +576,14 @@ static void cov_lifetime_expiration_handler( * @note worst case tasking: MS/TP with the ability to send only * one notification per task cycle. * - * @param elapsed_seconds [in] How many seconds have elapsed since last called. + * @param elapsed_seconds [in] How many seconds have elapsed since last + * called. */ void handler_cov_timer_seconds(uint32_t elapsed_seconds) { int index = 0; uint32_t lifetime_seconds = 0; - BACNET_COV_SUBSCRIPTION *subscription = NULL; + BACNET_COV_HANDLER_SUBSCRIPTION *subscription = NULL; int list_cnt = 0; #ifdef BAC_ROUTING uint16_t current_dev_id = Routed_Device_Object_Index(); @@ -755,7 +658,7 @@ bool handler_cov_fsm(void) int index = indices[dev_id]; cov_fsm_state_t cov_task_state = cov_task_states[dev_id]; - BACNET_COV_SUBSCRIPTION *subscription = + BACNET_COV_HANDLER_SUBSCRIPTION *subscription = Keylist_Data_Index(COV_Subscriptions, index); int list_cnt = Keylist_Count(COV_Subscriptions); diff --git a/src/bacnet/cov.c b/src/bacnet/cov.c index 025cc46e..5662a867 100644 --- a/src/bacnet/cov.c +++ b/src/bacnet/cov.c @@ -19,8 +19,177 @@ COV Subscribe COV Subscribe Property COV Notification Unconfirmed COV Notification +COV Subscription */ +/** + * @brief Encode APDU for COV Subscription. + * + * BACnetCOVSubscription ::= SEQUENCE { + * recipient[0] BACnetRecipientProcess, + * monitored-property-reference[1] BACnetObjectPropertyReference, + * issue-confirmed-notifications[2] Boolean, + * time-remaining[3] Unsigned, + * cov-increment[4] Real OPTIONAL-- used only with monitored + * -- properties with a numeric datatype + * } + * + * @param apdu Pointer to the buffer, or NULL for length + * @param data Pointer to the data to encode. + * @return number of bytes encoded, or zero on error. + */ +size_t +cov_subscription_encode(uint8_t *apdu, const BACNET_COV_SUBSCRIPTION *data) +{ + int len; + size_t apdu_len = 0; /* total length of the apdu, return value */ + + if (!data) { + return 0; + } + /* Recipient [0] BACnetRecipientProcess */ + len = bacnet_recipient_process_context_encode(apdu, 0, &data->recipient); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* MonitoredPropertyReference [1] BACnetObjectPropertyReference, */ + len = bacapp_encode_context_obj_property_ref( + apdu, 1, &data->monitored_property_reference); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* IssueConfirmedNotifications [2] BOOLEAN, */ + len = encode_context_boolean(apdu, 2, data->issue_confirmed_notifications); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* TimeRemaining [3] Unsigned, */ + len = encode_context_unsigned(apdu, 3, data->time_remaining); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* cov-increment [4] Real OPTIONAL */ + if (data->cov_increment_present) { + len = encode_context_real(apdu, 4, data->cov_increment); + apdu_len += len; + } + + return apdu_len; +} + +/** + * @brief Encode APDU for COV Subscription. + * @param apdu Pointer to the buffer, or NULL for length + * @param apdu_size number of bytes available in the buffer + * @param data Pointer to the data to encode. + * @return number of bytes encoded, or zero on error. + */ +size_t bacnet_cov_subscription_encode( + uint8_t *apdu, size_t apdu_size, const BACNET_COV_SUBSCRIPTION *data) +{ + size_t apdu_len = 0; /* total length of the apdu, return value */ + + apdu_len = cov_subscription_encode(NULL, data); + if (apdu_len > apdu_size) { + apdu_len = 0; + } else { + apdu_len = cov_subscription_encode(apdu, data); + } + + return apdu_len; +} + +/** + * @brief Decode APDU for COV Subscription. + * + * BACnetCOVSubscription ::= SEQUENCE { + * recipient[0] BACnetRecipientProcess, + * monitored-property-reference[1] BACnetObjectPropertyReference, + * issue-confirmed-notifications[2] Boolean, + * time-remaining[3] Unsigned, + * cov-increment[4] Real OPTIONAL-- used only with monitored + * -- properties with a numeric datatype + * } + * + * @param apdu Pointer to the buffer for decoding. + * @param apdu_size number of bytes available in the buffer + * @param data Pointer to the data to decode into, or NULL for length only. + * @return Bytes decoded or BACNET_STATUS_ERROR on error. + */ +int bacnet_cov_subscription_decode( + const uint8_t *apdu, size_t apdu_size, BACNET_COV_SUBSCRIPTION *data) +{ + int len = 0, apdu_len = 0; + BACNET_UNSIGNED_INTEGER unsigned_value = 0; + BACNET_RECIPIENT_PROCESS recipient = { 0 }; + BACNET_OBJECT_PROPERTY_REFERENCE monitored_property_reference = { 0 }; + bool issue_confirmed_notifications = false; + float float_value = 1.0f; + bool cov_increment_present = false; + + if (!apdu || (apdu_size == 0)) { + return 0; + } + /* Recipient [0] BACnetRecipientProcess */ + len = bacnet_recipient_process_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 0, &recipient); + if (len <= 0) { + return BACNET_STATUS_ERROR; + } + apdu_len += len; + /* MonitoredPropertyReference [1] BACnetObjectPropertyReference */ + len = bacapp_decode_context_obj_property_ref( + &apdu[apdu_len], apdu_size - apdu_len, 1, + &monitored_property_reference); + if (len <= 0) { + return BACNET_STATUS_ERROR; + } + apdu_len += len; + /* IssueConfirmedNotifications [2] BOOLEAN */ + len = bacnet_boolean_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 2, + &issue_confirmed_notifications); + if (len <= 0) { + return BACNET_STATUS_ERROR; + } + apdu_len += len; + /* TimeRemaining [3] Unsigned */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 3, &unsigned_value); + if (len <= 0) { + return BACNET_STATUS_ERROR; + } + apdu_len += len; + /* CovIncrement [4] Real OPTIONAL */ + len = bacnet_real_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 4, &float_value); + if (len < 0) { + return BACNET_STATUS_ERROR; + } else if (len == 0) { + /* not present - skip */ + cov_increment_present = false; + } else { + cov_increment_present = true; + apdu_len += len; + } + /* copy into the data structure */ + if (data) { + bacnet_recipient_process_copy(&data->recipient, &recipient); + bacnet_object_property_reference_copy( + &data->monitored_property_reference, &monitored_property_reference); + data->issue_confirmed_notifications = issue_confirmed_notifications; + data->time_remaining = unsigned_value; + data->cov_increment = float_value; + data->cov_increment_present = cov_increment_present; + } + + return apdu_len; +} + /** * @brief Encode APDU for COV Notification. * @param apdu Pointer to the buffer, or NULL for length diff --git a/src/bacnet/cov.h b/src/bacnet/cov.h index 7dca3936..d7b07c5a 100644 --- a/src/bacnet/cov.h +++ b/src/bacnet/cov.h @@ -14,6 +14,8 @@ #include "bacnet/bacdef.h" /* BACnet Stack API */ #include "bacnet/bacapp.h" +#include "bacnet/bacdest.h" +#include "bacnet/bacdevobjpropref.h" typedef struct BACnet_COV_Data { uint32_t subscriberProcessIdentifier; @@ -40,6 +42,25 @@ typedef struct BACnet_Subscribe_COV_Data { struct BACnet_Subscribe_COV_Data *next; } BACNET_SUBSCRIBE_COV_DATA; +/** + * BACnetCOVSubscription ::= SEQUENCE { + * recipient[0] BACnetRecipientProcess, + * monitored-property-reference[1] BACnetObjectPropertyReference, + * issue-confirmed-notifications[2] Boolean, + * time-remaining[3] Unsigned, + * cov-increment[4] Real OPTIONAL-- used only with monitored + * -- properties with a numeric datatype + * } + */ +typedef struct BACnetCOVSubscription { + uint32_t time_remaining; /* seconds */ + BACNET_RECIPIENT_PROCESS recipient; + BACNET_OBJECT_PROPERTY_REFERENCE monitored_property_reference; + bool issue_confirmed_notifications; + bool cov_increment_present; /* true if present */ + float cov_increment; /* optional */ +} BACNET_COV_SUBSCRIPTION; + /* generic callback for COV notifications */ typedef void (*BACnet_COV_Notification_Callback)(BACNET_COV_DATA *cov_data); struct BACnet_COV_Notification; @@ -52,6 +73,16 @@ typedef struct BACnet_COV_Notification { extern "C" { #endif /* __cplusplus */ +BACNET_STACK_EXPORT +size_t +cov_subscription_encode(uint8_t *apdu, const BACNET_COV_SUBSCRIPTION *data); +BACNET_STACK_EXPORT +size_t bacnet_cov_subscription_encode( + uint8_t *apdu, size_t apdu_size, const BACNET_COV_SUBSCRIPTION *data); +BACNET_STACK_EXPORT +int bacnet_cov_subscription_decode( + const uint8_t *apdu, size_t apdu_size, BACNET_COV_SUBSCRIPTION *data); + BACNET_STACK_EXPORT size_t cov_notify_service_request_encode( uint8_t *apdu, size_t apdu_size, const BACNET_COV_DATA *data); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4cae33a8..ae944b79 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -193,6 +193,8 @@ list(APPEND testdirs bacnet/basic/object/trendlog # basic/program bacnet/basic/program/ubasic + # basic/service + bacnet/basic/service/h_cov # basic/server bacnet/basic/server/bacnet_device # basic/sys diff --git a/test/bacnet/bacdest/src/main.c b/test/bacnet/bacdest/src/main.c index b55a7e24..5b6ca14d 100644 --- a/test/bacnet/bacdest/src/main.c +++ b/test/bacnet/bacdest/src/main.c @@ -245,6 +245,89 @@ static void test_BACnetRecipient_ASCII(void) zassert_true(status, NULL); } +/** + * @brief Test BACnetRecipientProcess encode, decode, copy functions + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacnet_destination_tests, testBACnetRecipientProcess) +#else +static void testBACnetRecipientProcess(void) +#endif +{ + uint8_t apdu[MAX_APDU] = { 0 }; + BACNET_RECIPIENT_PROCESS value = { 0 }, test_value = { 0 }; + BACNET_MAC_ADDRESS mac = { .len = 1, .adr[0] = 0x01 }; + BACNET_MAC_ADDRESS adr = { .len = 1, .adr[0] = 0x02 }; + uint16_t snet = 4321; + BACNET_ADDRESS address = { 0 }; + int apdu_len = 0, null_len = 0, test_len = 0; + uint8_t tag_number = 3; + bool status = false; + + /* device recipient */ + bacnet_recipient_device_set(&value.recipient, OBJECT_DEVICE, 5678); + value.process_identifier = 99; + null_len = bacnet_recipient_process_encode(NULL, &value); + apdu_len = bacnet_recipient_process_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_recipient_process_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_equal( + value.process_identifier, test_value.process_identifier, NULL); + status = bacnet_recipient_same(&value.recipient, &test_value.recipient); + zassert_true(status, NULL); + /* NULL decode target - length-only */ + test_len = bacnet_recipient_process_decode(apdu, apdu_len, NULL); + zassert_equal(apdu_len, test_len, NULL); + + /* copy */ + memset(&test_value, 0, sizeof(test_value)); + bacnet_recipient_process_copy(&test_value, &value); + zassert_equal( + value.process_identifier, test_value.process_identifier, NULL); + status = bacnet_recipient_same(&value.recipient, &test_value.recipient); + zassert_true(status, NULL); + + /* address recipient */ + status = bacnet_address_init(&address, &mac, snet, &adr); + zassert_true(status, NULL); + bacnet_recipient_address_set(&value.recipient, &address); + value.process_identifier = 42; + null_len = bacnet_recipient_process_encode(NULL, &value); + apdu_len = bacnet_recipient_process_encode(apdu, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_recipient_process_decode(apdu, apdu_len, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_equal( + value.process_identifier, test_value.process_identifier, NULL); + status = bacnet_recipient_same(&value.recipient, &test_value.recipient); + zassert_true(status, NULL); + + /* context encode/decode */ + bacnet_recipient_device_set(&value.recipient, OBJECT_DEVICE, 1234); + value.process_identifier = 7; + null_len = + bacnet_recipient_process_context_encode(NULL, tag_number, &value); + apdu_len = + bacnet_recipient_process_context_encode(apdu, tag_number, &value); + zassert_equal(apdu_len, null_len, NULL); + test_len = bacnet_recipient_process_context_decode( + apdu, apdu_len, tag_number, &test_value); + zassert_equal(apdu_len, test_len, NULL); + zassert_equal( + value.process_identifier, test_value.process_identifier, NULL); + status = bacnet_recipient_same(&value.recipient, &test_value.recipient); + zassert_true(status, NULL); + /* wrong tag - expect zero (tag mismatch) */ + test_len = bacnet_recipient_process_context_decode( + apdu, apdu_len, tag_number + 1, &test_value); + zassert_equal(test_len, 0, NULL); + + /* negative: NULL apdu */ + test_len = bacnet_recipient_process_decode(NULL, apdu_len, &test_value); + zassert_equal(test_len, BACNET_STATUS_ERROR, NULL); +} + #if defined(CONFIG_ZTEST_NEW_API) ZTEST_SUITE(bacnet_destination_tests, NULL, NULL, NULL, NULL, NULL); #else @@ -254,7 +337,8 @@ void test_main(void) bacnet_destination_tests, ztest_unit_test(testBACnetDestination), ztest_unit_test(test_BACnetDestination_ASCII), ztest_unit_test(testBACnetRecipient), - ztest_unit_test(test_BACnetRecipient_ASCII)); + ztest_unit_test(test_BACnetRecipient_ASCII), + ztest_unit_test(testBACnetRecipientProcess)); ztest_run_test_suite(bacnet_destination_tests); } diff --git a/test/bacnet/basic/object/test/bip_mock.c b/test/bacnet/basic/object/test/bip_mock.c index 0e47e1cb..f39408cb 100644 --- a/test/bacnet/basic/object/test/bip_mock.c +++ b/test/bacnet/basic/object/test/bip_mock.c @@ -10,14 +10,16 @@ void bip_get_my_address(BACNET_ADDRESS *my_address) { - (void)my_address; + if (my_address) { + my_address->mac_len = 0; + } } int bip_send_pdu( BACNET_ADDRESS *dest, BACNET_NPDU_DATA *npdu_data, uint8_t *pdu, - unsigned pdu_len) + uint16_t pdu_len) { (void)dest; (void)npdu_data; diff --git a/test/bacnet/basic/object/test/device_mock.c b/test/bacnet/basic/object/test/device_mock.c index 8f7852a4..3bd6f82a 100644 --- a/test/bacnet/basic/object/test/device_mock.c +++ b/test/bacnet/basic/object/test/device_mock.c @@ -46,3 +46,57 @@ int Device_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) (void)rpdata; return 0; } + +bool Device_Valid_Object_Id( + BACNET_OBJECT_TYPE object_type, uint32_t object_instance) +{ + (void)object_type; + (void)object_instance; + return true; +} + +bool Device_Value_List_Supported(BACNET_OBJECT_TYPE object_type) +{ + (void)object_type; + return true; +} + +bool Device_COV(BACNET_OBJECT_TYPE object_type, uint32_t object_instance) +{ + (void)object_type; + (void)object_instance; + return false; +} + +void Device_COV_Clear(BACNET_OBJECT_TYPE object_type, uint32_t object_instance) +{ + (void)object_type; + (void)object_instance; +} + +bool Device_Encode_Value_List( + BACNET_OBJECT_TYPE object_type, + uint32_t object_instance, + BACNET_PROPERTY_VALUE *value_list) +{ + (void)object_type; + (void)object_instance; + (void)value_list; + return true; +} + +uint16_t Routed_Device_Object_Index(void) +{ + return 0; +} + +bool Set_Routed_Device_Object_Index(uint16_t index) +{ + (void)index; + return true; +} + +uint16_t Get_Num_Managed_Devices(void) +{ + return 1; +} diff --git a/test/bacnet/basic/service/h_cov/CMakeLists.txt b/test/bacnet/basic/service/h_cov/CMakeLists.txt new file mode 100644 index 00000000..59938882 --- /dev/null +++ b/test/bacnet/basic/service/h_cov/CMakeLists.txt @@ -0,0 +1,83 @@ +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) + +get_filename_component(basename ${CMAKE_CURRENT_SOURCE_DIR} NAME) +project(test_${basename} + VERSION 1.0.0 + LANGUAGES C) + +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z0-9_/-]*$" + "/src" + SRC_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z0-9_/-]*$" + "/test" + TST_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) + +set(ZTST_DIR "${TST_DIR}/ztest/src") + +add_compile_definitions( + BACNET_BIG_ENDIAN=0 + CONFIG_ZTEST=1 + BACAPP_ALL=1 + ) + +include_directories( + ${SRC_DIR} + ${TST_DIR}/ztest/include + ) + +add_executable(${PROJECT_NAME} + # File(s) under test + ${SRC_DIR}/bacnet/basic/service/h_cov.c + # Support files and stubs (pathname alphabetical) + ${SRC_DIR}/bacnet/access_rule.c + ${SRC_DIR}/bacnet/bacaction.c + ${SRC_DIR}/bacnet/bacaddr.c + ${SRC_DIR}/bacnet/bacapp.c + ${SRC_DIR}/bacnet/bacdcode.c + ${SRC_DIR}/bacnet/bacdest.c + ${SRC_DIR}/bacnet/bacdevobjpropref.c + ${SRC_DIR}/bacnet/bacint.c + ${SRC_DIR}/bacnet/baclog.c + ${SRC_DIR}/bacnet/bacreal.c + ${SRC_DIR}/bacnet/bacstr.c + ${SRC_DIR}/bacnet/bactext.c + ${SRC_DIR}/bacnet/bacerror.c + ${SRC_DIR}/bacnet/basic/sys/bigend.c + ${SRC_DIR}/bacnet/basic/sys/debug.c + ${SRC_DIR}/bacnet/basic/tsm/tsm.c + ${SRC_DIR}/bacnet/bactimevalue.c + ${SRC_DIR}/bacnet/calendar_entry.c + ${SRC_DIR}/bacnet/channel_value.c + ${SRC_DIR}/bacnet/cov.c + ${SRC_DIR}/bacnet/dailyschedule.c + ${SRC_DIR}/bacnet/datetime.c + ${SRC_DIR}/bacnet/basic/sys/days.c + ${SRC_DIR}/bacnet/basic/sys/keylist.c + ${SRC_DIR}/bacnet/hostnport.c + ${SRC_DIR}/bacnet/indtext.c + ${SRC_DIR}/bacnet/lighting.c + ${SRC_DIR}/bacnet/memcopy.c + ${SRC_DIR}/bacnet/secure_connect.c + ${SRC_DIR}/bacnet/shed_level.c + ${SRC_DIR}/bacnet/special_event.c + ${SRC_DIR}/bacnet/timer_value.c + ${SRC_DIR}/bacnet/timestamp.c + ${SRC_DIR}/bacnet/weeklyschedule.c + ${SRC_DIR}/bacnet/abort.c + ${SRC_DIR}/bacnet/dcc.c + ${SRC_DIR}/bacnet/npdu.c + ${SRC_DIR}/bacnet/reject.c + # Test and test library files + ./src/main.c + ${TST_DIR}/bacnet/basic/object/test/apdu_mock.c + ${TST_DIR}/bacnet/basic/object/test/bip_mock.c + ${TST_DIR}/bacnet/basic/object/test/device_mock.c + ${ZTST_DIR}/ztest_mock.c + ${ZTST_DIR}/ztest.c + ) diff --git a/test/bacnet/basic/service/h_cov/src/main.c b/test/bacnet/basic/service/h_cov/src/main.c new file mode 100644 index 00000000..e47a69ce --- /dev/null +++ b/test/bacnet/basic/service/h_cov/src/main.c @@ -0,0 +1,348 @@ +/** + * @file + * @brief test BACnet COV service handler + * @date 2025 + * @copyright SPDX-License-Identifier: MIT + */ +#include +#include +#include +#include + +/** + * @addtogroup bacnet_tests + * @{ + */ + +/* Mock functions for Device_ */ +#include + +/* Mocks have been moved to bacnet/basic/object/test/ */ +/** + * @brief Test test_h_cov_init_encode + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(h_cov_tests, test_h_cov_init_encode) +#else +static void test_h_cov_init_encode(void) +#endif +{ + uint8_t apdu[480] = { 0 }; + int apdu_size = sizeof(apdu); + int len; + uint8_t service_request[128] = { 0 }; + BACNET_ADDRESS src = { 0 }; + BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; + BACNET_SUBSCRIBE_COV_DATA cov_data = { 0 }; + + handler_cov_init(); + + len = handler_cov_encode_subscriptions(apdu, apdu_size); + /* Should be zero because we initialized the handler */ + zassert_equal(len, 0, NULL); + + /* Create a valid subscription */ + cov_data.subscriberProcessIdentifier = 1; + cov_data.monitoredObjectIdentifier.type = OBJECT_ANALOG_INPUT; + cov_data.monitoredObjectIdentifier.instance = 0; + cov_data.cancellationRequest = false; + cov_data.issueConfirmedNotifications = false; + cov_data.lifetime = 0; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + zassert_not_equal(len, 0, NULL); + zassert_not_equal(len, BACNET_STATUS_ABORT, NULL); + handler_cov_subscribe(service_request, len, &src, &service_data); + + /* Normal encode */ + len = handler_cov_encode_subscriptions(apdu, apdu_size); + zassert_not_equal(len, 0, NULL); + zassert_not_equal(len, BACNET_STATUS_ABORT, NULL); + + /* edge case: exact size buffer encode */ + len = handler_cov_encode_subscriptions(apdu, len); + zassert_equal(len, len, NULL); + + /* edge case: buffer too small */ + len = handler_cov_encode_subscriptions(apdu, len - 1); + zassert_equal(len, BACNET_STATUS_ABORT, NULL); +} + +/** + * @brief Test test_h_cov_timer + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(h_cov_tests, test_h_cov_timer) +#else +static void test_h_cov_timer(void) +#endif +{ + handler_cov_timer_seconds(1); + zassert_true(true, NULL); +} + +/** + * @brief Test test_h_cov_fsm + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(h_cov_tests, test_h_cov_fsm) +#else +static void test_h_cov_fsm(void) +#endif +{ + bool idle; + uint8_t service_request[128] = { 0 }; + BACNET_ADDRESS src = { 0 }; + BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; + BACNET_SUBSCRIBE_COV_DATA cov_data = { 0 }; + int len; + + handler_cov_init(); + + idle = handler_cov_fsm(); + zassert_true(idle, NULL); /* No subscriptions, quickly back to idle */ + + /* Subscribe Unconfirmed */ + cov_data.subscriberProcessIdentifier = 1; + cov_data.monitoredObjectIdentifier.type = OBJECT_ANALOG_INPUT; + cov_data.monitoredObjectIdentifier.instance = 0; + cov_data.cancellationRequest = false; + cov_data.issueConfirmedNotifications = false; + cov_data.lifetime = 0; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + + /* IDLE -> MARK */ + idle = handler_cov_fsm(); + zassert_false(idle, NULL); + /* MARK -> CLEAR */ + idle = handler_cov_fsm(); + zassert_false(idle, NULL); + /* CLEAR -> FREE */ + idle = handler_cov_fsm(); + zassert_false(idle, NULL); + /* FREE -> SEND */ + idle = handler_cov_fsm(); + zassert_false(idle, NULL); + /* SEND -> IDLE */ + idle = handler_cov_fsm(); + zassert_true(idle, NULL); + + /* Subscribe Confirmed */ + cov_data.subscriberProcessIdentifier = 2; + cov_data.issueConfirmedNotifications = true; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + + /* Run FSM again */ + handler_cov_fsm(); /* IDLE -> MARK */ + handler_cov_fsm(); /* MARK -> CLEAR */ + handler_cov_fsm(); /* CLEAR -> FREE */ + handler_cov_fsm(); /* FREE -> SEND */ + handler_cov_fsm(); /* SEND -> IDLE */ + + /* Trigger Confirmed free branch by expiring */ + cov_data.lifetime = 10; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + + handler_cov_fsm(); /* IDLE -> MARK */ + handler_cov_fsm(); /* MARK -> CLEAR */ + handler_cov_fsm(); /* CLEAR -> FREE */ + handler_cov_fsm(); /* FREE -> SEND */ + handler_cov_fsm(); /* SEND -> IDLE */ + + /* Expire */ + handler_cov_timer_seconds(15); + + handler_cov_task(); + zassert_true(true, NULL); +} + +/** + * @brief Test test_h_cov_subscribe_error_cases + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(h_cov_tests, test_h_cov_subscribe_error_cases) +#else +static void test_h_cov_subscribe_error_cases(void) +#endif +{ + uint8_t service_request[128] = { 0 }; + BACNET_ADDRESS src = { 0 }; + BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; + BACNET_SUBSCRIBE_COV_DATA cov_data = { 0 }; + int len; + + handler_cov_init(); + + /* test segment abort */ + service_data.segmented_message = true; + handler_cov_subscribe(service_request, 10, &src, &service_data); + zassert_true(true, NULL); + + /* test missing required parameter */ + service_data.segmented_message = false; + handler_cov_subscribe(service_request, 0, &src, &service_data); + zassert_true(true, NULL); + + /* test bad request decode */ + handler_cov_subscribe(service_request, 1, &src, &service_data); + zassert_true(true, NULL); + + /* test valid decode but object valid (by mock) */ + cov_data.subscriberProcessIdentifier = 1; + cov_data.monitoredObjectIdentifier.type = OBJECT_ANALOG_INPUT; + cov_data.monitoredObjectIdentifier.instance = 12345; + cov_data.cancellationRequest = false; + cov_data.issueConfirmedNotifications = false; + cov_data.lifetime = 0; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + zassert_true(true, NULL); + + /* test cancellation of non-existing */ + cov_data.cancellationRequest = true; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + zassert_true(true, NULL); +} + +/** + * @brief Test test_h_cov_subscribe_out_of_space + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(h_cov_tests, test_h_cov_subscribe_out_of_space) +#else +static void test_h_cov_subscribe_out_of_space(void) +#endif +{ + uint8_t service_request[128] = { 0 }; + BACNET_ADDRESS src = { 0 }; + BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; + BACNET_SUBSCRIBE_COV_DATA cov_data = { 0 }; + int len; + int i; + + handler_cov_init(); + + cov_data.monitoredObjectIdentifier.type = OBJECT_ANALOG_INPUT; + cov_data.monitoredObjectIdentifier.instance = 0; + cov_data.cancellationRequest = false; + cov_data.issueConfirmedNotifications = false; + cov_data.lifetime = 0; + + for (i = 0; i < 130; i++) { + cov_data.subscriberProcessIdentifier = i; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + } + zassert_true(true, NULL); +} + +/** + * @brief Test test_h_cov_timer_expiration + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(h_cov_tests, test_h_cov_timer_expiration) +#else +static void test_h_cov_timer_expiration(void) +#endif +{ + uint8_t service_request[128] = { 0 }; + BACNET_ADDRESS src = { 0 }; + BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; + BACNET_SUBSCRIBE_COV_DATA cov_data = { 0 }; + int len; + + handler_cov_init(); + + /* Subscribe with lifetime */ + cov_data.subscriberProcessIdentifier = 1; + cov_data.monitoredObjectIdentifier.type = OBJECT_ANALOG_INPUT; + cov_data.monitoredObjectIdentifier.instance = 0; + cov_data.cancellationRequest = false; + cov_data.issueConfirmedNotifications = true; + cov_data.lifetime = 10; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + + /* Check timer decreasing */ + handler_cov_timer_seconds(5); + handler_cov_timer_seconds(10); /* Should expire here */ + zassert_true(true, NULL); +} + +/** + * @brief Test test_h_cov_address_management + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(h_cov_tests, test_h_cov_address_management) +#else +static void test_h_cov_address_management(void) +#endif +{ + uint8_t service_request[128] = { 0 }; + BACNET_ADDRESS src = { 0 }; + BACNET_CONFIRMED_SERVICE_DATA service_data = { 0 }; + BACNET_SUBSCRIBE_COV_DATA cov_data = { 0 }; + int len; + int i; + + handler_cov_init(); + + cov_data.monitoredObjectIdentifier.type = OBJECT_ANALOG_INPUT; + cov_data.monitoredObjectIdentifier.instance = 0; + cov_data.cancellationRequest = false; + cov_data.issueConfirmedNotifications = false; + cov_data.lifetime = 0; + + /* Add subscriptions from 18 different sources */ + for (i = 0; i < 18; i++) { + src.mac_len = 1; + src.mac[0] = i + 1; + cov_data.subscriberProcessIdentifier = i; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + } + + /* Cancel one, to free an address */ + src.mac_len = 1; + src.mac[0] = 1; + cov_data.subscriberProcessIdentifier = 0; + cov_data.cancellationRequest = true; + len = cov_subscribe_service_request_encode( + service_request, sizeof(service_request), &cov_data); + handler_cov_subscribe(service_request, len, &src, &service_data); + + zassert_true(true, NULL); +} + +/** + * @} + */ + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST_SUITE(h_cov_tests, NULL, NULL, NULL, NULL, NULL); +#else +void test_main(void) +{ + ztest_test_suite( + h_cov_tests, ztest_unit_test(test_h_cov_init_encode), + ztest_unit_test(test_h_cov_timer), ztest_unit_test(test_h_cov_fsm), + ztest_unit_test(test_h_cov_subscribe_error_cases), + ztest_unit_test(test_h_cov_subscribe_out_of_space), + ztest_unit_test(test_h_cov_timer_expiration), + ztest_unit_test(test_h_cov_address_management)); + + ztest_run_test_suite(h_cov_tests); +} +#endif diff --git a/test/bacnet/cov/src/main.c b/test/bacnet/cov/src/main.c index c530d4b8..fc8f331b 100644 --- a/test/bacnet/cov/src/main.c +++ b/test/bacnet/cov/src/main.c @@ -342,6 +342,69 @@ static void testCOVSubscribePropertyData( } } +static void testCOVSubscriptionData( + const BACNET_COV_SUBSCRIPTION *data, + const BACNET_COV_SUBSCRIPTION *test_data) +{ + bool status = false; + + zassert_equal( + test_data->time_remaining, data->time_remaining, + "time-remaining mismatch"); + zassert_equal( + test_data->issue_confirmed_notifications, + data->issue_confirmed_notifications, + "issue-confirmed-notifications mismatch"); + zassert_equal( + test_data->cov_increment_present, data->cov_increment_present, + "cov-increment-present mismatch"); + if (test_data->cov_increment_present) { + zassert_false( + islessgreater(test_data->cov_increment, data->cov_increment), + "cov-increment mismatch"); + } + zassert_equal( + test_data->recipient.process_identifier, + data->recipient.process_identifier, + "recipient process-identifier mismatch"); + status = bacnet_recipient_same( + &test_data->recipient.recipient, &data->recipient.recipient); + zassert_true(status, "recipient mismatch"); + status = bacnet_object_property_reference_same( + &test_data->monitored_property_reference, + &data->monitored_property_reference); + zassert_true(status, "monitored-property-reference mismatch"); +} + +static void testCOVSubscriptionEncoding(const BACNET_COV_SUBSCRIPTION *data) +{ + uint8_t apdu[480] = { 0 }; + size_t len = 0, null_len = 0; + BACNET_COV_SUBSCRIPTION test_data = { 0 }; + int decode_len = 0; + + null_len = bacnet_cov_subscription_encode(NULL, sizeof(apdu), data); + len = bacnet_cov_subscription_encode(&apdu[0], sizeof(apdu), data); + zassert_true(len > 0, NULL); + zassert_equal(len, null_len, NULL); + + if (len > 0) { + zassert_equal( + bacnet_cov_subscription_encode(&apdu[0], len - 1, data), 0, NULL); + } + + decode_len = bacnet_cov_subscription_decode(NULL, len, &test_data); + zassert_equal(decode_len, 0, NULL); + decode_len = bacnet_cov_subscription_decode(&apdu[0], 0, &test_data); + zassert_equal(decode_len, 0, NULL); + + decode_len = bacnet_cov_subscription_decode(&apdu[0], len, NULL); + zassert_equal(decode_len, (int)len, NULL); + decode_len = bacnet_cov_subscription_decode(&apdu[0], len, &test_data); + zassert_equal(decode_len, (int)len, NULL); + testCOVSubscriptionData(data, &test_data); +} + static void testCOVSubscribeEncoding( uint8_t invoke_id, const BACNET_SUBSCRIBE_COV_DATA *data) { @@ -450,6 +513,33 @@ static void testCOVSubscribeProperty(void) testCOVSubscribePropertyEncoding(invoke_id, &data); } +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(cov_tests, testCOVSubscription) +#else +static void testCOVSubscription(void) +#endif +{ + BACNET_COV_SUBSCRIPTION data = { 0 }; + + data.time_remaining = 456; + data.issue_confirmed_notifications = true; + data.recipient.process_identifier = 1; + bacnet_recipient_device_set(&data.recipient.recipient, OBJECT_DEVICE, 123); + data.monitored_property_reference.object_identifier.type = + OBJECT_ANALOG_INPUT; + data.monitored_property_reference.object_identifier.instance = 321; + data.monitored_property_reference.property_identifier = PROP_PRESENT_VALUE; + data.monitored_property_reference.property_array_index = BACNET_ARRAY_ALL; + + data.cov_increment_present = false; + data.cov_increment = 0.0f; + testCOVSubscriptionEncoding(&data); + + data.cov_increment_present = true; + data.cov_increment = 1.5f; + testCOVSubscriptionEncoding(&data); +} + #if defined(CONFIG_ZTEST_NEW_API) ZTEST(cov_tests, test_COV_Value_List_Encode) #else @@ -606,6 +696,7 @@ void test_main(void) cov_tests, ztest_unit_test(testCOVNotify), ztest_unit_test(testCOVSubscribe), ztest_unit_test(testCOVSubscribeProperty), + ztest_unit_test(testCOVSubscription), ztest_unit_test(test_COV_Value_List_Encode)); ztest_run_test_suite(cov_tests);