diff --git a/.gitignore b/.gitignore index 06b7fbb2..fc42dba6 100644 --- a/.gitignore +++ b/.gitignore @@ -77,6 +77,8 @@ CMakeCache.txt CMakeFiles/ cmake_install.cmake CTestTestfile.cmake +CMakeSettings.json + # zephyr artifacts /twister-out/* /test/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1005bb33..8ecf9816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ The git repositories are hosted at the following sites: ### Added +* Added property_list_read_only_member function to check for READ-ONLY + properties. (#1258) * Added WriteProperty support in the basic Structured View object. Converted Structured View internal storage to dynamically allocated character strings and a keylist-based subordinate list that is resizable diff --git a/src/bacnet/proplist.c b/src/bacnet/proplist.c index 49f822df..6cd272e2 100644 --- a/src/bacnet/proplist.c +++ b/src/bacnet/proplist.c @@ -601,3 +601,188 @@ bool property_list_commandable_member( return status; } + +/** + * @brief Determine if the object property is a READ-ONLY property + * @param object_type - object-type to be checked + * @param object_property - object-property to be checked + * @return true if the property is a READ-ONLY property + * @note generally read-only per EPICS property gathering, + * sometimes stated explicitly in the standard and sometimes not. + * Used in the EPICS tools to determine if a property should + * avoid being written to determine writability of the property. + */ +bool property_list_read_only_member( + BACNET_OBJECT_TYPE object_type, BACNET_PROPERTY_ID object_property) +{ + /* exceptions where the property is READ-ONLY only in specific objects */ + switch (object_type) { + case OBJECT_AVERAGING: + switch (object_property) { + case PROP_MINIMUM_VALUE: + case PROP_MINIMUM_VALUE_TIMESTAMP: + case PROP_AVERAGE_VALUE: + case PROP_VARIANCE_VALUE: + case PROP_MAXIMUM_VALUE: + case PROP_MAXIMUM_VALUE_TIMESTAMP: + return true; + default: + break; + } + break; + case OBJECT_CALENDAR: + switch (object_property) { + case PROP_PRESENT_VALUE: + return true; + default: + break; + } + break; + case OBJECT_COMMAND: + switch (object_property) { + case PROP_IN_PROCESS: + case PROP_ALL_WRITES_SUCCESSFUL: + return true; + default: + break; + } + break; + case OBJECT_DEVICE: + switch (object_property) { + case PROP_SYSTEM_STATUS: + case PROP_VENDOR_NAME: + case PROP_VENDOR_IDENTIFIER: + case PROP_MODEL_NAME: + case PROP_FIRMWARE_REVISION: + case PROP_APPLICATION_SOFTWARE_VERSION: + case PROP_PROTOCOL_VERSION: + case PROP_PROTOCOL_REVISION: + case PROP_PROTOCOL_SERVICES_SUPPORTED: + case PROP_PROTOCOL_OBJECT_TYPES_SUPPORTED: + case PROP_SEGMENTATION_SUPPORTED: + case PROP_ACTIVE_VT_SESSIONS: + case PROP_DATABASE_REVISION: + case PROP_LAST_RESTORE_TIME: + case PROP_TIME_OF_DEVICE_RESTART: + case PROP_BACKUP_AND_RESTORE_STATE: + case PROP_ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS: + case PROP_ACTIVE_COV_SUBSCRIPTIONS: + return true; + default: + break; + } + break; + case OBJECT_PROGRAM: + switch (object_property) { + case PROP_REASON_FOR_HALT: + return true; + default: + break; + } + break; + case OBJECT_FILE: + switch (object_property) { + case PROP_MODIFICATION_DATE: + return true; + default: + break; + } + break; + case OBJECT_PULSE_CONVERTER: + switch (object_property) { + case PROP_COUNT_BEFORE_CHANGE: + return true; + default: + break; + } + break; + case OBJECT_TRENDLOG: + case OBJECT_EVENT_LOG: + case OBJECT_AUDIT_LOG: + case OBJECT_TREND_LOG_MULTIPLE: + switch (object_property) { + case PROP_TOTAL_RECORD_COUNT: + case PROP_RECORDS_SINCE_NOTIFICATION: + case PROP_LAST_NOTIFY_RECORD: + case PROP_LOG_BUFFER: + return true; + default: + break; + } + break; + case OBJECT_GLOBAL_GROUP: + switch (object_property) { + case PROP_MEMBER_STATUS_FLAGS: + return true; + default: + break; + } + break; + case OBJECT_CHANNEL: + switch (object_property) { + case PROP_WRITE_STATUS: + return true; + default: + break; + } + break; + case OBJECT_LIGHTING_OUTPUT: + switch (object_property) { + case PROP_IN_PROGRESS: + case PROP_EGRESS_ACTIVE: + return true; + default: + break; + } + break; + case OBJECT_BINARY_LIGHTING_OUTPUT: + switch (object_property) { + case PROP_EGRESS_ACTIVE: + return true; + default: + break; + } + break; + case OBJECT_NETWORK_PORT: + switch (object_property) { + case PROP_CHANGES_PENDING: + case PROP_SLAVE_ADDRESS_BINDING: + case PROP_WRITE_STATUS: + return true; + default: + break; + } + break; + case OBJECT_LOAD_CONTROL: + switch (object_property) { + case PROP_PRESENT_VALUE: + case PROP_EXPECTED_SHED_LEVEL: + case PROP_ACTUAL_SHED_LEVEL: + return true; + default: + break; + } + break; + default: + break; + } + if ((object_property >= PROP_PROPRIETARY_RANGE_MIN) && + (object_property <= PROP_PROPRIETARY_RANGE_MAX)) { + /* all proprietary properties could be read-only */ + return true; + } + /* Some properties, like Present_Value and Reliability, + may be temporarily writable under specific test + conditions (per Addendum 2020ci), but are defined + as read-only by default.*/ + if (object_property == PROP_PRESENT_VALUE) { + if (!property_list_commandable_member(object_type, object_property)) { + return true; + } + } + if (object_property == PROP_RELIABILITY) { + return true; + } + + return false; +} diff --git a/src/bacnet/proplist.h b/src/bacnet/proplist.h index 403c7b9e..3f1b8d9c 100644 --- a/src/bacnet/proplist.h +++ b/src/bacnet/proplist.h @@ -83,6 +83,9 @@ bool property_list_bacnet_list_member( BACNET_STACK_EXPORT bool property_list_commandable_member( BACNET_OBJECT_TYPE object_type, BACNET_PROPERTY_ID object_property); +BACNET_STACK_EXPORT +bool property_list_read_only_member( + BACNET_OBJECT_TYPE object_type, BACNET_PROPERTY_ID object_property); #ifdef __cplusplus } diff --git a/test/bacnet/property/src/main.c b/test/bacnet/property/src/main.c index ed9a2f47..1aa69431 100644 --- a/test/bacnet/property/src/main.c +++ b/test/bacnet/property/src/main.c @@ -7,6 +7,7 @@ * @copyright SPDX-License-Identifier: MIT */ #include +#include #include #include @@ -106,6 +107,352 @@ static void testPropList(void) } count = property_list_count(property_list_bacnet_array()); zassert_true(count > 0, NULL); + + /* property is read-only */ + zassert_true( + property_list_read_only_member(OBJECT_AVERAGING, PROP_MINIMUM_VALUE), + "Averaging minimum-value should be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_CALENDAR, PROP_PRESENT_VALUE), + "Calendar present-value should be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_COMMAND, PROP_IN_PROCESS), + "Command in-process should be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_ANALOG_INPUT, PROP_PRESENT_VALUE), + "Analog Input present-value should be read-only"); + zassert_false( + property_list_read_only_member( + OBJECT_ANALOG_OUTPUT, PROP_PRESENT_VALUE), + "Analog Output present-value should not be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_DEVICE, PROP_VENDOR_NAME), + "Device vendor-name should be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_PROGRAM, PROP_REASON_FOR_HALT), + "Program reason-for-halt should be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_FILE, PROP_MODIFICATION_DATE), + "File modification-date should be read-only"); + zassert_true( + property_list_read_only_member( + OBJECT_PULSE_CONVERTER, PROP_COUNT_BEFORE_CHANGE), + "Pulse Converter count-before-change should be read-only"); + zassert_true( + property_list_read_only_member( + OBJECT_TRENDLOG, PROP_TOTAL_RECORD_COUNT), + "Trend Log total-record-count should be read-only"); + zassert_true( + property_list_read_only_member( + OBJECT_GLOBAL_GROUP, PROP_MEMBER_STATUS_FLAGS), + "Global Group member-status-flags should be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_CHANNEL, PROP_WRITE_STATUS), + "Channel write-status should be read-only"); + zassert_true( + property_list_read_only_member( + OBJECT_LIGHTING_OUTPUT, PROP_IN_PROGRESS), + "Lighting Output in-progress should be read-only"); + zassert_true( + property_list_read_only_member( + OBJECT_BINARY_LIGHTING_OUTPUT, PROP_EGRESS_ACTIVE), + "Binary Lighting Output egress-active should be read-only"); + zassert_true( + property_list_read_only_member( + OBJECT_NETWORK_PORT, PROP_CHANGES_PENDING), + "Network Port changes-pending should be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_LOAD_CONTROL, PROP_PRESENT_VALUE), + "Load Control present-value should be read-only"); + zassert_true( + property_list_read_only_member(OBJECT_DEVICE, PROP_RELIABILITY), + "Reliability should be read-only"); + zassert_true( + property_list_read_only_member( + OBJECT_DEVICE, PROP_PROPRIETARY_RANGE_MIN), + "Proprietary properties should be read-only"); + for (i = 0; i < OBJECT_PROPRIETARY_MIN; i++) { + object_type = i; + status = property_list_read_only_member(object_type, PROP_OBJECT_NAME); + zassert_false( + status, "Object name should not be read-only for object type %d", + object_type); + } + /* test property_lists_member() - checks all three lists at once */ + property_list_special(OBJECT_ANALOG_INPUT, &property_list); + /* required property must be found */ + zassert_true( + property_lists_member( + property_list.Required.pList, property_list.Optional.pList, + property_list.Proprietary.pList, PROP_PRESENT_VALUE), + "Analog Input present-value should be in property lists"); + /* optional property must be found */ + zassert_true( + property_lists_member( + property_list.Required.pList, property_list.Optional.pList, + property_list.Proprietary.pList, PROP_COV_INCREMENT), + "Analog Input cov-increment should be in property lists"); + /* property not in any list must not be found */ + zassert_false( + property_lists_member( + property_list.Required.pList, property_list.Optional.pList, + property_list.Proprietary.pList, PROP_PRIORITY_ARRAY), + "Analog Input priority-array should not be in property lists"); + /* NULL lists must not find any property */ + zassert_false( + property_lists_member(NULL, NULL, NULL, PROP_PRESENT_VALUE), + "NULL lists should not find any property"); +} + +/** + * @brief Test property_list_common() - common properties for all objects + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(property_tests, testPropListCommon) +#else +static void testPropListCommon(void) +#endif +{ + /* object-identifier and object-type are common (handled by this module) */ + zassert_true( + property_list_common(PROP_OBJECT_IDENTIFIER), + "object-identifier should be a common property"); + zassert_true( + property_list_common(PROP_OBJECT_TYPE), + "object-type should be a common property"); + /* other properties are not common */ + zassert_false( + property_list_common(PROP_OBJECT_NAME), + "object-name should not be a common property"); + zassert_false( + property_list_common(PROP_PRESENT_VALUE), + "present-value should not be a common property"); + zassert_false( + property_list_common(PROP_DESCRIPTION), + "description should not be a common property"); +} + +/** + * @brief Test property_list_encode() and property_list_common_encode() + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(property_tests, testPropListEncode) +#else +static void testPropListEncode(void) +#endif +{ + /* known property lists with 8 required, 2 optional, 0 proprietary */ + static const int32_t pRequired[] = { + PROP_OBJECT_IDENTIFIER, PROP_OBJECT_NAME, PROP_OBJECT_TYPE, + PROP_PRESENT_VALUE, PROP_STATUS_FLAGS, PROP_EVENT_STATE, + PROP_OUT_OF_SERVICE, PROP_UNITS, -1 + }; + static const int32_t pOptional[] = { PROP_DESCRIPTION, PROP_COV_INCREMENT, + -1 }; + static const int32_t pProprietary[] = { -1 }; + /* count: 8 required - 3 standard + 2 optional = 7 entries in + * PROP_PROPERTY_LIST */ + const uint32_t expected_count = 7; + uint8_t apdu[MAX_APDU] = { 0 }; + BACNET_READ_PROPERTY_DATA rpdata = { 0 }; + int len = 0; + BACNET_UNSIGNED_INTEGER decoded_count = 0; + BACNET_OBJECT_TYPE decoded_type = 0; + uint32_t decoded_instance = 0; + + /* --- property_list_encode --- */ + /* NULL rpdata returns 0 */ + len = property_list_encode(NULL, pRequired, pOptional, pProprietary); + zassert_equal(len, 0, "NULL rpdata should return 0"); + + /* no application data buffer returns 0 */ + rpdata.object_property = PROP_PROPERTY_LIST; + rpdata.array_index = BACNET_ARRAY_ALL; + rpdata.application_data = NULL; + rpdata.application_data_len = 0; + len = property_list_encode(&rpdata, pRequired, pOptional, pProprietary); + zassert_equal(len, 0, "no buffer should return 0"); + + /* array_index == 0: encode count of entries */ + rpdata.application_data = apdu; + rpdata.application_data_len = sizeof(apdu); + rpdata.array_index = 0; + len = property_list_encode(&rpdata, pRequired, pOptional, pProprietary); + zassert_true(len > 0, "array_index=0 should encode the count"); + zassert_equal( + bacnet_unsigned_application_decode(apdu, len, &decoded_count), len, + "decoded length should match encoded length"); + zassert_equal( + decoded_count, expected_count, "decoded count should match expected"); + + /* array_index == BACNET_ARRAY_ALL: encode all non-standard entries */ + rpdata.array_index = BACNET_ARRAY_ALL; + len = property_list_encode(&rpdata, pRequired, pOptional, pProprietary); + zassert_true(len > 0, "array_index=ALL should encode all entries"); + + /* array_index == 1: encode first non-standard required property */ + rpdata.array_index = 1; + len = property_list_encode(&rpdata, pRequired, pOptional, pProprietary); + zassert_true(len > 0, "array_index=1 should encode first entry"); + + /* array_index == last valid index */ + rpdata.array_index = expected_count; + len = property_list_encode(&rpdata, pRequired, pOptional, pProprietary); + zassert_true(len > 0, "array_index=last should encode the last entry"); + + /* array_index out of range: error */ + rpdata.array_index = expected_count + 1; + len = property_list_encode(&rpdata, pRequired, pOptional, pProprietary); + zassert_equal( + len, BACNET_STATUS_ERROR, + "out-of-range array_index should return BACNET_STATUS_ERROR"); + + /* non-PROP_PROPERTY_LIST property: error */ + rpdata.array_index = BACNET_ARRAY_ALL; + rpdata.object_property = PROP_OBJECT_NAME; + len = property_list_encode(&rpdata, pRequired, pOptional, pProprietary); + zassert_equal( + len, BACNET_STATUS_ERROR, + "unknown property should return BACNET_STATUS_ERROR"); + + /* --- property_list_common_encode --- */ + /* NULL rpdata returns 0 */ + len = property_list_common_encode(NULL, 0); + zassert_equal(len, 0, "NULL rpdata should return 0"); + + /* no buffer returns 0 */ + rpdata.application_data = NULL; + rpdata.application_data_len = 0; + rpdata.object_property = PROP_OBJECT_IDENTIFIER; + len = property_list_common_encode(&rpdata, 1); + zassert_equal(len, 0, "no buffer should return 0"); + + /* PROP_OBJECT_IDENTIFIER encodes object type + instance */ + rpdata.object_type = OBJECT_ANALOG_INPUT; + rpdata.object_instance = 1; + rpdata.object_property = PROP_OBJECT_IDENTIFIER; + rpdata.application_data = apdu; + rpdata.application_data_len = sizeof(apdu); + len = property_list_common_encode(&rpdata, 0); + zassert_true(len > 0, "PROP_OBJECT_IDENTIFIER should encode successfully"); + zassert_equal( + bacnet_object_id_application_decode( + apdu, len, &decoded_type, &decoded_instance), + len, "decoded length should match encoded length"); + zassert_equal( + decoded_type, OBJECT_ANALOG_INPUT, "decoded object type should match"); + zassert_equal( + decoded_instance, (uint32_t)1, "decoded object instance should match"); + + /* PROP_OBJECT_IDENTIFIER on Device uses device_instance_number */ + rpdata.object_type = OBJECT_DEVICE; + rpdata.object_instance = BACNET_MAX_INSTANCE; + rpdata.object_property = PROP_OBJECT_IDENTIFIER; + len = property_list_common_encode(&rpdata, 1234); + zassert_true(len > 0, "Device PROP_OBJECT_IDENTIFIER should encode"); + zassert_equal( + bacnet_object_id_application_decode( + apdu, len, &decoded_type, &decoded_instance), + len, "decoded length should match encoded length"); + zassert_equal( + decoded_type, OBJECT_DEVICE, "decoded type should be OBJECT_DEVICE"); + zassert_equal( + decoded_instance, (uint32_t)1234, + "Device instance should use device_instance_number parameter"); + + /* PROP_OBJECT_TYPE encodes the object type enumeration */ + rpdata.object_type = OBJECT_ANALOG_OUTPUT; + rpdata.object_property = PROP_OBJECT_TYPE; + len = property_list_common_encode(&rpdata, 0); + zassert_true(len > 0, "PROP_OBJECT_TYPE should encode successfully"); + + /* unknown property returns BACNET_STATUS_ERROR */ + rpdata.object_property = PROP_OBJECT_NAME; + len = property_list_common_encode(&rpdata, 0); + zassert_equal( + len, BACNET_STATUS_ERROR, + "unknown property should return BACNET_STATUS_ERROR"); +} + +/** + * @brief Test property_list_bacnet_list() and + * property_list_bacnet_list_member() + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(property_tests, testPropListBACnetList) +#else +static void testPropListBACnetList(void) +#endif +{ + const int32_t *pList = NULL; + uint32_t count = 0; + + /* list pointer is non-NULL and has at least one entry */ + pList = property_list_bacnet_list(); + zassert_not_null(pList, "BACnetLIST should not be NULL"); + count = property_list_count(pList); + zassert_true(count > 0, "BACnetLIST should have at least one entry"); + + /* known BACnetLIST members - present in the global list */ + zassert_true( + property_list_bacnet_list_member(OBJECT_DEVICE, PROP_DATE_LIST), + "date-list should be a BACnetLIST member"); + zassert_true( + property_list_bacnet_list_member( + OBJECT_DEVICE, PROP_ACTIVE_COV_SUBSCRIPTIONS), + "active-cov-subscriptions should be a BACnetLIST member"); + zassert_true( + property_list_bacnet_list_member( + OBJECT_DEVICE, PROP_DEVICE_ADDRESS_BINDING), + "device-address-binding should be a BACnetLIST member"); + zassert_true( + property_list_bacnet_list_member( + OBJECT_NOTIFICATION_CLASS, PROP_RECIPIENT_LIST), + "recipient-list should be a BACnetLIST member"); + zassert_true( + property_list_bacnet_list_member(OBJECT_TRENDLOG, PROP_LOG_BUFFER), + "log-buffer should be a BACnetLIST member"); + + /* BACnetARRAY properties are not BACnetLIST */ + zassert_false( + property_list_bacnet_list_member( + OBJECT_ANALOG_OUTPUT, PROP_PRIORITY_ARRAY), + "priority-array should not be a BACnetLIST member"); + zassert_false( + property_list_bacnet_list_member(OBJECT_DEVICE, PROP_OBJECT_LIST), + "object-list should not be a BACnetLIST member"); + + /* standard scalar properties are not BACnetLIST */ + zassert_false( + property_list_bacnet_list_member( + OBJECT_ANALOG_INPUT, PROP_PRESENT_VALUE), + "Analog Input present-value should not be a BACnetLIST member"); + zassert_false( + property_list_bacnet_list_member(OBJECT_DEVICE, PROP_VENDOR_NAME), + "vendor-name should not be a BACnetLIST member"); + + /* object-type exceptions: Group present-value is a BACnetLIST */ + zassert_true( + property_list_bacnet_list_member(OBJECT_GROUP, PROP_PRESENT_VALUE), + "Group present-value should be a BACnetLIST member"); + + /* object-type exceptions: Channel list-of-object-property-references + is NOT a BACnetLIST (it is a BACnetARRAY) */ + zassert_false( + property_list_bacnet_list_member( + OBJECT_CHANNEL, PROP_LIST_OF_OBJECT_PROPERTY_REFERENCES), + "Channel list-of-object-property-references should not be a " + "BACnetLIST member"); + + /* proprietary range properties are always considered BACnetLIST */ + zassert_true( + property_list_bacnet_list_member( + OBJECT_ANALOG_INPUT, PROP_PROPRIETARY_RANGE_MIN), + "proprietary properties should be considered BACnetLIST members"); + zassert_true( + property_list_bacnet_list_member( + OBJECT_DEVICE, PROP_PROPRIETARY_RANGE_MAX), + "proprietary properties should be considered BACnetLIST members"); } /** @@ -117,7 +464,11 @@ ZTEST_SUITE(property_tests, NULL, NULL, NULL, NULL, NULL); #else void test_main(void) { - ztest_test_suite(property_tests, ztest_unit_test(testPropList)); + ztest_test_suite( + property_tests, ztest_unit_test(testPropList), + ztest_unit_test(testPropListCommon), + ztest_unit_test(testPropListEncode), + ztest_unit_test(testPropListBACnetList)); ztest_run_test_suite(property_tests); }