diff --git a/ports/win32/Microsoft Visual Studio 2019/BACnet_Object_Definitions/BACnet_Object_Definitions.vcxproj b/ports/win32/Microsoft Visual Studio 2019/BACnet_Object_Definitions/BACnet_Object_Definitions.vcxproj
index 569386a5..8e78629d 100644
--- a/ports/win32/Microsoft Visual Studio 2019/BACnet_Object_Definitions/BACnet_Object_Definitions.vcxproj
+++ b/ports/win32/Microsoft Visual Studio 2019/BACnet_Object_Definitions/BACnet_Object_Definitions.vcxproj
@@ -178,6 +178,7 @@
+
@@ -207,6 +208,7 @@
+
diff --git a/src/bacnet/bacaudit.c b/src/bacnet/bacaudit.c
new file mode 100644
index 00000000..53fd1c3a
--- /dev/null
+++ b/src/bacnet/bacaudit.c
@@ -0,0 +1,1095 @@
+/**
+ * @file
+ * @brief BACnetActionCommand codec used by Command objects
+ * @author Steve Karg
+ * @date November 2024
+ * @copyright SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0
+ */
+#include
+#include
+#include
+/* BACnet Stack defines - first */
+#include "bacnet/bacdef.h"
+/* BACnet Stack API */
+#include "bacnet/bacdcode.h"
+#include "bacnet/bactimevalue.h"
+#include "bacnet/timestamp.h"
+/* me! */
+#include "bacnet/bacaudit.h"
+
+/**
+ * @brief Encode the BACnetAuditValue
+ * @param apdu - buffer of data to be encoded, or NULL for length
+ * @param value - value to be encoded
+ * @return the number of apdu bytes encoded
+ */
+int bacnet_audit_value_encode(uint8_t *apdu, const BACNET_AUDIT_VALUE *value)
+{
+ /* total length of the apdu, return value */
+ int apdu_len = 0;
+
+ if (!value) {
+ return 0;
+ }
+ switch (value->tag) {
+ case BACNET_APPLICATION_TAG_BOOLEAN:
+ apdu_len =
+ encode_application_boolean(apdu, value->type.boolean_value);
+ break;
+ case BACNET_APPLICATION_TAG_UNSIGNED_INT:
+ apdu_len =
+ encode_application_unsigned(apdu, value->type.unsigned_value);
+ break;
+ case BACNET_APPLICATION_TAG_SIGNED_INT:
+ apdu_len =
+ encode_application_signed(apdu, value->type.integer_value);
+ break;
+ case BACNET_APPLICATION_TAG_REAL:
+ apdu_len = encode_application_real(apdu, value->type.real_value);
+ break;
+ case BACNET_APPLICATION_TAG_ENUMERATED:
+ apdu_len = encode_application_enumerated(
+ apdu, value->type.enumerated_value);
+ break;
+ case BACNET_APPLICATION_TAG_NULL:
+ default:
+ if (apdu) {
+ apdu[0] = value->tag;
+ }
+ apdu_len++;
+ break;
+ }
+
+ return apdu_len;
+}
+
+/**
+ * @brief Encode the BACnetAuditValue
+ * @param apdu - buffer of data to be encoded, or NULL for length
+ * @param tag_number - context tag number to be encoded
+ * @param value - value to be encoded
+ * @return the number of apdu bytes encoded
+ */
+int bacnet_audit_value_context_encode(
+ uint8_t *apdu, uint8_t tag_number, const BACNET_AUDIT_VALUE *value)
+{
+ int len;
+ int apdu_len = 0;
+
+ len = encode_opening_tag(apdu, tag_number);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ len = bacnet_audit_value_encode(apdu, value);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ len = encode_closing_tag(apdu, tag_number);
+ apdu_len += len;
+
+ return apdu_len;
+}
+
+/**
+ * @brief Decode the BACnetAuditValue
+ * @param apdu - buffer of data to be encoded, or NULL for length
+ * @param tag_number - context tag number to be encoded
+ * @param value - value to be encoded
+ * @return the number of apdu bytes encoded, or BACNET_STATUS_ERROR if an error
+ * occurs
+ */
+int bacnet_audit_value_decode(
+ const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_VALUE *value)
+{
+ int len = 0;
+ int apdu_len = 0;
+ BACNET_TAG tag = { 0 };
+ bool boolean_value = false;
+ float real_value = 0.0f;
+ uint32_t enumerated_value = 0;
+ BACNET_UNSIGNED_INTEGER unsigned_value = 0;
+ int32_t integer_value = 0;
+
+ if (!value) {
+ return BACNET_STATUS_ERROR;
+ }
+ if (!apdu) {
+ return BACNET_STATUS_ERROR;
+ }
+ len = bacnet_tag_decode(apdu, apdu_size, &tag);
+ if ((len > 0) && tag.application) {
+ if (value) {
+ value->tag = tag.number;
+ }
+ switch (tag.number) {
+ case BACNET_APPLICATION_TAG_BOOLEAN:
+ apdu_len = bacnet_boolean_application_decode(
+ apdu, apdu_size, &boolean_value);
+ if (apdu_len > 0) {
+ if (value) {
+ value->type.boolean_value = boolean_value;
+ }
+ }
+ break;
+ case BACNET_APPLICATION_TAG_UNSIGNED_INT:
+ apdu_len = bacnet_unsigned_application_decode(
+ apdu, apdu_size, &unsigned_value);
+ if (apdu_len > 0) {
+ if (value) {
+ value->type.unsigned_value = unsigned_value;
+ }
+ }
+ break;
+ case BACNET_APPLICATION_TAG_SIGNED_INT:
+ apdu_len = bacnet_signed_application_decode(
+ apdu, apdu_size, &integer_value);
+ if (apdu_len > 0) {
+ if (value) {
+ value->type.integer_value = integer_value;
+ }
+ }
+ break;
+ case BACNET_APPLICATION_TAG_REAL:
+ apdu_len = bacnet_real_application_decode(
+ apdu, apdu_size, &real_value);
+ if (apdu_len > 0) {
+ if (value) {
+ value->type.real_value = real_value;
+ }
+ }
+ break;
+ case BACNET_APPLICATION_TAG_ENUMERATED:
+ apdu_len = bacnet_enumerated_application_decode(
+ apdu, apdu_size, &enumerated_value);
+ if (apdu_len > 0) {
+ if (value) {
+ value->type.enumerated_value = enumerated_value;
+ }
+ }
+ break;
+ case BACNET_APPLICATION_TAG_NULL:
+ default:
+ apdu_len = len;
+ break;
+ }
+ } else {
+ apdu_len = BACNET_STATUS_ERROR;
+ }
+
+ return apdu_len;
+}
+
+/**
+ * @brief Decode a context encoded BACnetAuditValue from a buffer
+ * @param apdu - the APDU buffer
+ * @param apdu_size - the size of the APDU buffer
+ * @param tag_number - the tag number
+ * @param value - BACnetAuditValue to decode into
+ * @return number of bytes decoded, zero if tag mismatch, or
+ * BACNET_STATUS_ERROR on failure.
+ */
+int bacnet_audit_value_context_decode(
+ const uint8_t *apdu,
+ uint16_t apdu_size,
+ uint8_t tag_number,
+ BACNET_AUDIT_VALUE *value)
+{
+ int len = 0;
+ int apdu_len = 0;
+
+ if (!apdu) {
+ return BACNET_STATUS_ERROR;
+ }
+ if (!bacnet_is_opening_tag_number(
+ &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) {
+ return 0;
+ }
+ apdu_len += len;
+ len =
+ bacnet_audit_value_decode(&apdu[apdu_len], apdu_size - apdu_len, value);
+ if (len > 0) {
+ apdu_len += len;
+ if (bacnet_is_closing_tag_number(
+ &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) {
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+
+ return apdu_len;
+}
+
+/**
+ * @brief Compare two BACnetActionPropertyValue complex datatypes
+ * @param value1 [in] The first structure to compare
+ * @param value2 [in] The second structure to compare
+ * @return true if the two structures are the same
+ */
+bool bacnet_audit_value_same(
+ const BACNET_AUDIT_VALUE *value1, const BACNET_AUDIT_VALUE *value2)
+{
+ bool status = false; /*return value */
+
+ if ((value1 == NULL) || (value2 == NULL)) {
+ return false;
+ }
+ if (value1->tag == value2->tag) {
+ switch (value1->tag) {
+ case BACNET_APPLICATION_TAG_NULL:
+ status = true;
+ break;
+ case BACNET_APPLICATION_TAG_BOOLEAN:
+ status =
+ value1->type.boolean_value == value2->type.boolean_value;
+ break;
+ case BACNET_APPLICATION_TAG_UNSIGNED_INT:
+ status =
+ value1->type.unsigned_value == value2->type.unsigned_value;
+ break;
+ case BACNET_APPLICATION_TAG_SIGNED_INT:
+ status =
+ value1->type.integer_value == value2->type.integer_value;
+ break;
+ case BACNET_APPLICATION_TAG_REAL:
+ status = !islessgreater(
+ value1->type.real_value, value2->type.real_value);
+ break;
+ case BACNET_APPLICATION_TAG_ENUMERATED:
+ status = value1->type.enumerated_value ==
+ value2->type.enumerated_value;
+ break;
+ default:
+ break;
+ }
+ }
+
+ return status;
+}
+
+/**
+ * @brief Encode the BACnetAuditNotification
+ * @param apdu - buffer of data to be encoded, or NULL for length
+ * @param value - value to be encoded
+ * @return the number of apdu bytes encoded
+ */
+int bacnet_audit_log_notification_encode(
+ uint8_t *apdu, const BACNET_AUDIT_NOTIFICATION *value)
+{
+ int len, apdu_len = 0; /* total length of the apdu, return value */
+
+ if (!value) {
+ return 0;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE
+ /* source-timestamp [0] BACnetTimeStamp OPTIONAL */
+ len = bacapp_encode_context_timestamp(apdu, 0, &value->source_timestamp);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE
+ /* target-timestamp [1] BACnetTimeStamp OPTIONAL */
+ len = bacapp_encode_context_timestamp(apdu, 1, &value->target_timestamp);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+ /* source-device [2] BACnetRecipient */
+ len = bacnet_recipient_context_encode(apdu, 2, &value->source_device);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE
+ /* source-object [3] BACnetObjectIdentifier OPTIONAL */
+ len = encode_context_object_id(
+ apdu, 3, value->source_object.type, value->source_object.instance);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+ /* operation [4] BACnetAuditOperation */
+ len = encode_context_unsigned(apdu, 4, value->operation);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE
+ /* source-comment [5] CharacterString OPTIONAL */
+ len = encode_context_character_string(apdu, 5, &value->source_comment);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE
+ /* target-comment [6] CharacterString OPTIONAL */
+ len = encode_context_character_string(apdu, 6, &value->target_comment);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE
+ /* invoke-id [7] Unsigned8 OPTIONAL */
+ len = encode_context_unsigned(apdu, 7, value->invoke_id);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE
+ /* source-user-id [8] Unsigned16 OPTIONAL */
+ len = encode_context_unsigned(apdu, 8, value->source_user_id);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE
+ /* source-user-role [9] Unsigned8 OPTIONAL */
+ len = encode_context_unsigned(apdu, 9, value->source_user_role);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+ /* target-device [10] BACnetRecipient */
+ len = bacnet_recipient_context_encode(apdu, 10, &value->target_device);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE
+ /* target-object [11] BACnetObjectIdentifier OPTIONAL */
+ len = encode_context_object_id(
+ apdu, 11, value->target_object.type, value->target_object.instance);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE
+ /* target-property [12] BACnetPropertyReference OPTIONAL */
+ len = bacnet_property_reference_context_encode(
+ apdu, 12, &value->target_property);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE
+ /* target-priority [13] Unsigned (1..16) OPTIONAL */
+ len = encode_context_unsigned(apdu, 13, value->target_priority);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE
+ /* target-value [14] ABSTRACT-SYNTAX.&Type OPTIONAL */
+ len = bacnet_audit_value_context_encode(apdu, 14, &value->target_value);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE
+ /* current-value [15] ABSTRACT-SYNTAX.&Type OPTIONAL */
+ len = bacnet_audit_value_context_encode(apdu, 15, &value->current_value);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE
+ /* result [16] Error OPTIONAL */
+ len = encode_context_enumerated(apdu, 16, value->result);
+ apdu_len += len;
+#endif
+
+ return apdu_len;
+}
+
+/**
+ * @brief Decode the BACnetAuditNotification
+ * @param apdu - buffer of data to be decoded
+ * @param value - value to hold the decoded data
+ * @return the number of apdu bytes encoded, or BACNET_STATUS_ERROR if an error
+ * occurs
+ */
+int bacnet_audit_log_notification_decode(
+ const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_NOTIFICATION *value)
+{
+ int len = 0;
+ int apdu_len = 0;
+ BACNET_UNSIGNED_INTEGER unsigned_value;
+ BACNET_TIMESTAMP timestamp;
+ BACNET_OBJECT_TYPE object_type;
+ uint32_t object_instance;
+ BACNET_CHARACTER_STRING char_string;
+ BACNET_RECIPIENT recipient;
+ struct BACnetPropertyReference property_reference;
+ BACNET_AUDIT_VALUE audit_value;
+ uint32_t enumerated_value;
+
+ if (!apdu) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* source-timestamp [0] BACnetTimeStamp OPTIONAL */
+ len = bacnet_timestamp_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 0, ×tamp);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE
+ if (value) {
+ bacapp_timestamp_copy(&value->source_timestamp, ×tamp);
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* target-timestamp [1] BACnetTimeStamp OPTIONAL */
+ len = bacnet_timestamp_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 1, ×tamp);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE
+ if (value) {
+ bacapp_timestamp_copy(&value->target_timestamp, ×tamp);
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* source-device [2] BACnetRecipient */
+ len = bacnet_recipient_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 2, &recipient);
+ if (len > 0) {
+ apdu_len += len;
+ if (value) {
+ bacnet_recipient_copy(&value->source_device, &recipient);
+ }
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ /* source-object [3] BACnetObjectIdentifier OPTIONAL */
+ len = bacnet_object_id_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 3, &object_type,
+ &object_instance);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE
+ if (value) {
+ value->source_object.type = object_type;
+ value->source_object.instance = object_instance;
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* operation [4] BACnetAuditOperation */
+ len = bacnet_unsigned_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 4, &unsigned_value);
+ if (len > 0) {
+ if (unsigned_value < AUDIT_OPERATION_MAX) {
+ if (value) {
+ value->operation = (BACNET_AUDIT_OPERATION)unsigned_value;
+ }
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ /* source-comment [5] CharacterString OPTIONAL */
+ len = bacnet_character_string_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 5, &char_string);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE
+ if (value) {
+ characterstring_copy(&value->source_comment, &char_string);
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* target-comment [6] CharacterString OPTIONAL */
+ len = bacnet_character_string_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 6, &char_string);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE
+ if (value) {
+ characterstring_copy(&value->target_comment, &char_string);
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* invoke-id [7] Unsigned8 OPTIONAL */
+ len = bacnet_unsigned_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 7, &unsigned_value);
+ if (len > 0) {
+ apdu_len += len;
+ if (unsigned_value > UINT8_MAX) {
+ return BACNET_STATUS_ERROR;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE
+ if (value) {
+ value->invoke_id = (uint8_t)unsigned_value;
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* source-user-id [8] Unsigned16 OPTIONAL */
+ if (bacnet_is_context_tag_number(
+ &apdu[apdu_len], apdu_size - apdu_len, 8, NULL, NULL)) {
+ len = bacnet_unsigned_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 8, &unsigned_value);
+ if (len > 0) {
+ apdu_len += len;
+ if (unsigned_value > UINT16_MAX) {
+ return BACNET_STATUS_ERROR;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE
+ if (value) {
+ value->source_user_id = (uint16_t)unsigned_value;
+ }
+#endif
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ }
+ /* source-user-role [9] Unsigned8 OPTIONAL */
+ len = bacnet_unsigned_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 9, &unsigned_value);
+ if (len > 0) {
+ apdu_len += len;
+ if (unsigned_value > UINT8_MAX) {
+ return BACNET_STATUS_ERROR;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE
+ if (value) {
+ value->source_user_role = (uint8_t)unsigned_value;
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* target-device [10] BACnetRecipient */
+ len = bacnet_recipient_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 10, &recipient);
+ if (len > 0) {
+ apdu_len += len;
+ if (value) {
+ bacnet_recipient_copy(&value->target_device, &recipient);
+ }
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ /* target-object [11] BACnetObjectIdentifier OPTIONAL */
+ len = bacnet_object_id_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 11, &object_type,
+ &object_instance);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE
+ if (value) {
+ value->target_object.type = object_type;
+ value->target_object.instance = object_instance;
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* target-property [12] BACnetPropertyReference OPTIONAL */
+ len = bacnet_property_reference_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 12, &property_reference);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE
+ if (value) {
+ memcpy(
+ &value->target_property, &property_reference,
+ sizeof(property_reference));
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* target-priority [13] Unsigned (1..16) OPTIONAL */
+ len = bacnet_unsigned_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 13, &unsigned_value);
+ if (len > 0) {
+ apdu_len += len;
+ if (unsigned_value > UINT8_MAX) {
+ return BACNET_STATUS_ERROR;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE
+ if (value) {
+ value->target_priority = (uint8_t)unsigned_value;
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* target-value [14] ABSTRACT-SYNTAX.&Type OPTIONAL */
+ len = bacnet_audit_value_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 14, &audit_value);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE
+ if (value) {
+ memcpy(&value->target_value, &audit_value, sizeof(audit_value));
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* current-value [15] ABSTRACT-SYNTAX.&Type OPTIONAL */
+ len = bacnet_audit_value_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 15, &audit_value);
+ if (len > 0) {
+ apdu_len += len;
+#ifdef BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE
+ if (value) {
+ memcpy(&value->current_value, &audit_value, sizeof(audit_value));
+ }
+#endif
+ }
+ /* result [16] Error OPTIONAL */
+ len = bacnet_enumerated_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 16, &enumerated_value);
+ if (len > 0) {
+ apdu_len += len;
+ if (enumerated_value > UINT16_MAX) {
+ return BACNET_STATUS_ERROR;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE
+ if (value) {
+ value->result = (BACNET_ERROR_CODE)enumerated_value;
+ }
+#endif
+ } else if (len < 0) {
+ return BACNET_STATUS_ERROR;
+ }
+
+ return apdu_len;
+}
+
+/**
+ * @brief Decode a context BACnetAuditLogNotification context data.
+ * Check for an opening tag and a closing tag as well.
+ *
+ * @param apdu Pointer to the buffer containing the encoded value
+ * @param apdu_size Size of the buffer containing the encoded value
+ * @param tag_number Tag number
+ * @param value Pointer to the structure that shall be decoded into.
+ *
+ * @return number of bytes decoded, zero if tag mismatch,
+ * or #BACNET_STATUS_ERROR (-1) if malformed
+ */
+int bacnet_audit_log_notification_context_decode(
+ const uint8_t *apdu,
+ uint16_t apdu_size,
+ uint8_t tag_number,
+ BACNET_AUDIT_NOTIFICATION *value)
+{
+ int len = 0;
+ int apdu_len = 0;
+
+ if (!apdu) {
+ return BACNET_STATUS_ERROR;
+ }
+ if (!bacnet_is_opening_tag_number(
+ &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) {
+ return 0;
+ }
+ apdu_len += len;
+ len = bacnet_audit_log_notification_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, value);
+ if (len > 0) {
+ apdu_len += len;
+ if (bacnet_is_closing_tag_number(
+ &apdu[apdu_len], apdu_size - apdu_len, tag_number, &len)) {
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+
+ return apdu_len;
+}
+
+/**
+ * @brief Compare two BACnetAuditNotification for same-ness
+ * @param value1 - value 1 structure
+ * @param value2 - value 2 structure
+ * @return true if the values are the same
+ */
+bool bacnet_audit_log_notification_same(
+ const BACNET_AUDIT_NOTIFICATION *value1,
+ const BACNET_AUDIT_NOTIFICATION *value2)
+{
+ if (!value1 || !value2) {
+ return false;
+ }
+ if (value1->operation != value2->operation) {
+ return false;
+ }
+ if (!bacnet_recipient_same(
+ &value1->source_device, &value2->source_device)) {
+ return false;
+ }
+ if (!bacnet_recipient_same(
+ &value1->target_device, &value2->target_device)) {
+ return false;
+ }
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE
+ if (!bacapp_timestamp_same(
+ &value1->source_timestamp, &value2->source_timestamp)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE
+ if (!bacapp_timestamp_same(
+ &value1->target_timestamp, &value2->target_timestamp)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE
+ if (!bacnet_object_id_same(
+ value1->source_object.type, value1->source_object.instance,
+ value2->source_object.type, value2->source_object.instance)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE
+ if (!characterstring_same(
+ &value1->source_comment, &value2->source_comment)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE
+ if (!characterstring_same(
+ &value1->target_comment, &value2->target_comment)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE
+ if (value1->invoke_id != value2->invoke_id) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE
+ if (value1->source_user_id != value2->source_user_id) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE
+ if (value1->source_user_role != value2->source_user_role) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE
+ if (!bacnet_object_id_same(
+ value1->target_object.type, value1->target_object.instance,
+ value2->target_object.type, value2->target_object.instance)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE
+ if (!bacnet_property_reference_same(
+ &value1->target_property, &value2->target_property)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE
+ if (value1->target_priority != value2->target_priority) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE
+ if (!bacnet_audit_value_same(
+ &value1->target_value, &value2->target_value)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE
+ if (!bacnet_audit_value_same(
+ &value1->current_value, &value2->current_value)) {
+ return false;
+ }
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE
+ if (value1->result != value2->result) {
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+/**
+ * @brief Encode property value according to the application tag
+ *
+ * BACnetAuditLogRecord ::= SEQUENCE {
+ * timestamp [0] BACnetDateTime,
+ * log-datum [1] CHOICE {
+ * log-status [0] BACnetLogStatus,
+ * audit-notification [1] BACnetAuditNotification,
+ * time-change [2] REAL
+ * }
+ * }
+ *
+ * @param apdu - Pointer to the buffer to encode to, or NULL for length
+ * @param value - Pointer to the property value to encode from
+ * @return number of bytes encoded
+ */
+int bacnet_audit_log_record_encode(
+ uint8_t *apdu, const BACNET_AUDIT_LOG_RECORD *value)
+{
+ int len, apdu_len = 0; /* total length of the apdu, return value */
+ BACNET_BIT_STRING log_status = { 0 };
+
+ if (!value) {
+ return 0;
+ }
+ /* timestamp [0] BACnetDateTime */
+ len = bacapp_encode_context_datetime(apdu, 0, &value->timestamp);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ /* log-datum [1] CHOICE */
+ len = encode_opening_tag(apdu, 1);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ switch (value->tag) {
+ case AUDIT_LOG_DATUM_TAG_STATUS:
+ /* log-status [0] BACnetLogStatus */
+ bitstring_bits_used_set(&log_status, LOG_STATUS_MAX);
+ bitstring_set_octet(&log_status, 0, value->log_datum.log_status);
+ len = encode_context_bitstring(apdu, value->tag, &log_status);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ break;
+ case AUDIT_LOG_DATUM_TAG_NOTIFICATION:
+ /* audit-notification [1] BACnetAuditNotification */
+ len = encode_opening_tag(apdu, value->tag);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ len = bacnet_audit_log_notification_encode(
+ apdu, &value->log_datum.notification);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ len = encode_closing_tag(apdu, value->tag);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ break;
+ case AUDIT_LOG_DATUM_TAG_TIME_CHANGE:
+ /* time-change [2] REAL */
+ len = encode_context_real(
+ apdu, value->tag, value->log_datum.time_change);
+ apdu_len += len;
+ if (apdu) {
+ apdu += len;
+ }
+ break;
+ default:
+ break;
+ }
+ /* log-datum [1] CHOICE */
+ len = encode_closing_tag(apdu, 1);
+ apdu_len += len;
+
+ return apdu_len;
+}
+
+/**
+ * @brief Decode property value from the application buffer
+ * @param apdu - Pointer to the buffer to decode from
+ * @param apdu_size Size of the buffer to decode from
+ * @param value - Pointer to the property value to decode to
+ * @return number of bytes encoded, or BACNET_STATUS_ERROR if an error
+ * occurs
+ */
+int bacnet_audit_log_record_decode(
+ const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_LOG_RECORD *value)
+{
+ int len = 0;
+ int apdu_len = 0;
+ BACNET_TAG tag = { 0 };
+ BACNET_DATE_TIME bdatetime = { 0 };
+ BACNET_BIT_STRING log_status = { 0 };
+ BACNET_AUDIT_NOTIFICATION notification = { 0 };
+ float real_value = 0.0f;
+
+ if (!apdu) {
+ return BACNET_STATUS_ERROR;
+ }
+ /* timestamp [0] BACnetDateTime */
+ len = bacnet_datetime_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, 0, &bdatetime);
+ if (len > 0) {
+ if (value) {
+ datetime_copy(&value->timestamp, &bdatetime);
+ }
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ /* log-datum [1] CHOICE */
+ if (bacnet_is_opening_tag_number(
+ &apdu[apdu_len], apdu_size - apdu_len, 1, &len)) {
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ len = bacnet_tag_decode(&apdu[apdu_len], apdu_size - apdu_len, &tag);
+ if (len <= 0) {
+ return BACNET_STATUS_ERROR;
+ }
+ if (value) {
+ value->tag = tag.number;
+ }
+ /* ignore the len. len is included in context decoder below. */
+ switch (tag.number) {
+ case AUDIT_LOG_DATUM_TAG_STATUS:
+ /* log-status [0] BACnetLogStatus */
+ len = bacnet_bitstring_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, tag.number, &log_status);
+ if (len > 0) {
+ if (value) {
+ value->log_datum.log_status =
+ bitstring_octet(&log_status, 0);
+ }
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ break;
+ case AUDIT_LOG_DATUM_TAG_NOTIFICATION:
+ /* audit-notification [1] BACnetAuditNotification */
+ len = bacnet_audit_log_notification_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, tag.number,
+ ¬ification);
+ if (len > 0) {
+ if (value) {
+ memmove(
+ &value->log_datum.notification, ¬ification,
+ sizeof(BACNET_AUDIT_NOTIFICATION));
+ }
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ break;
+ case AUDIT_LOG_DATUM_TAG_TIME_CHANGE:
+ /* time-change [2] REAL */
+ len = bacnet_real_context_decode(
+ &apdu[apdu_len], apdu_size - apdu_len, tag.number, &real_value);
+ if (len > 0) {
+ if (value) {
+ value->log_datum.time_change = real_value;
+ }
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+ break;
+ default:
+ return BACNET_STATUS_ERROR;
+ }
+ /* log-datum [1] CHOICE */
+ if (bacnet_is_closing_tag_number(
+ &apdu[apdu_len], apdu_size - apdu_len, 1, &len)) {
+ apdu_len += len;
+ } else {
+ return BACNET_STATUS_ERROR;
+ }
+
+ return apdu_len;
+}
+
+/**
+ * @brief Compare two BACnetActionPropertyValue complex datatypes
+ * @param value1 [in] The first structure to compare
+ * @param value2 [in] The second structure to compare
+ * @return true if the two structures are the same
+ */
+bool bacnet_audit_log_record_same(
+ const BACNET_AUDIT_LOG_RECORD *value1,
+ const BACNET_AUDIT_LOG_RECORD *value2)
+{
+ bool status = false; /*return value */
+
+ if ((value1 == NULL) || (value2 == NULL)) {
+ return false;
+ }
+ /* does the tag match? */
+ if (value1->tag == value2->tag) {
+ status = true;
+ }
+ if (status) {
+ status = false;
+ /* does the timestamp match? */
+ if (datetime_compare(&value1->timestamp, &value2->timestamp) == 0) {
+ status = true;
+ }
+ }
+ if (status) {
+ status = false;
+ switch (value1->tag) {
+ case AUDIT_LOG_DATUM_TAG_STATUS:
+ if (value1->log_datum.log_status ==
+ value2->log_datum.log_status) {
+ status = true;
+ }
+ break;
+ case AUDIT_LOG_DATUM_TAG_NOTIFICATION:
+ if (bacnet_audit_log_notification_same(
+ &value1->log_datum.notification,
+ &value2->log_datum.notification)) {
+ status = true;
+ }
+ break;
+ case AUDIT_LOG_DATUM_TAG_TIME_CHANGE:
+ if (!islessgreater(
+ value1->log_datum.time_change,
+ value2->log_datum.time_change)) {
+ status = true;
+ }
+ break;
+ default:
+ status = false;
+ break;
+ }
+ }
+
+ return status;
+}
diff --git a/src/bacnet/bacaudit.h b/src/bacnet/bacaudit.h
new file mode 100644
index 00000000..7b19da62
--- /dev/null
+++ b/src/bacnet/bacaudit.h
@@ -0,0 +1,258 @@
+/**
+ * @file
+ * @brief API for BACnetAuditNotification and BACnetAuditLogRecord codec used
+ * by Audit Log objects
+ * @author Mikhail Antropov
+ * @author Steve Karg
+ * @date November 2024
+ * @copyright SPDX-License-Identifier: MIT
+ */
+#ifndef BACNET_AUDIT_H
+#define BACNET_AUDIT_H
+#include
+#include
+/* BACnet Stack defines - first */
+#include "bacnet/bacdef.h"
+/* BACnet Stack API */
+#include "bacnet/bacdest.h"
+#include "bacnet/cov.h"
+#include "bacnet/datetime.h"
+#include "bacnet/bacdevobjpropref.h"
+
+/**
+ * @brief Container to hold the target value or current value
+ * @note This must be a separate struct from BACapp to avoid
+ * recursive a recursive include.
+ * @note This is a union to save space. The application tag
+ * is used to determine which type is used.
+ *
+ *
+ */
+typedef struct BACnet_Audit_Value {
+ uint8_t tag; /* application tag data type */
+ union {
+ /* NULL - not needed as it is encoded in the tag alone */
+ bool boolean_value;
+ float real_value;
+ uint32_t enumerated_value;
+ uint32_t unsigned_value;
+ int32_t integer_value;
+ /* note: use ifdef for values larger than 32-bits are supported */
+ } type;
+} BACNET_AUDIT_VALUE;
+
+/**
+ * @brief Storage structures for Audit Log record
+ *
+ * @note I've tried to minimize the storage requirements here
+ * as the memory requirements for logging in embedded
+ * implementations are frequently a big issue. For PC or
+ * embedded Linux type setupz this may seem like overkill
+ * but if you have limited memory and need to squeeze as much
+ * logging capacity as possible every little byte counts!
+ */
+#if !( \
+ defined(BACNET_AUDIT_NOTIFICATION_MINIMAL_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_OPTIONAL_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE) || \
+ defined(BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE))
+#define BACNET_AUDIT_NOTIFICATION_OPTIONAL_ENABLE
+#endif
+
+#ifdef BACNET_AUDIT_NOTIFICATION_OPTIONAL_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE
+#define BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE
+#endif
+
+/*
+ * BACnetAuditNotification ::= SEQUENCE {
+ * source-timestamp [0] BACnetTimeStamp OPTIONAL,
+ * target-timestamp [1] BACnetTimeStamp OPTIONAL,
+ * source-device [2] BACnetRecipient,
+ * source-object [3] BACnetObjectIdentifier OPTIONAL,
+ * operation [4] BACnetAuditOperation,
+ * source-comment [5] CharacterString OPTIONAL,
+ * target-comment [6] CharacterString OPTIONAL,
+ * invoke-id [7] Unsigned8 OPTIONAL,
+ * source-user-id [8] Unsigned16 OPTIONAL,
+ * source-user-role [9] Unsigned8 OPTIONAL,
+ * target-device [10] BACnetRecipient,
+ * target-object [11] BACnetObjectIdentifier OPTIONAL,
+ * target-property [12] BACnetPropertyReference OPTIONAL,
+ * target-priority [13] Unsigned (1..16) OPTIONAL,
+ * target-value [14] ABSTRACT-SYNTAX.&Type OPTIONAL,
+ * current-value [15] ABSTRACT-SYNTAX.&Type OPTIONAL,
+ * result [16] Error OPTIONAL
+ * }
+ */
+typedef struct BACnetAuditNotification {
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_TIMESTAMP_ENABLE
+ /* source-timestamp [0] BACnetTimeStamp OPTIONAL */
+ BACNET_TIMESTAMP source_timestamp;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_TIMESTAMP_ENABLE
+ /* target-timestamp [1] BACnetTimeStamp OPTIONAL */
+ BACNET_TIMESTAMP target_timestamp;
+#endif
+ /* source-device [2] BACnetRecipient */
+ BACNET_RECIPIENT source_device;
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_OBJECT_ENABLE
+ /* source-object [3] BACnetObjectIdentifier OPTIONAL */
+ BACNET_OBJECT_ID source_object;
+#endif
+ /* operation [4] BACnetAuditOperation */
+ uint8_t operation;
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_COMMENT_ENABLE
+ /* source-comment [5] CharacterString OPTIONAL */
+ BACNET_CHARACTER_STRING source_comment;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_COMMENT_ENABLE
+ /* target-comment [6] CharacterString OPTIONAL */
+ BACNET_CHARACTER_STRING target_comment;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_INVOKE_ID_ENABLE
+ /* invoke-id [7] Unsigned8 OPTIONAL */
+ uint8_t invoke_id;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ID_ENABLE
+ /* source-user-id [8] Unsigned16 OPTIONAL */
+ uint16_t source_user_id;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_SOURCE_USER_ROLE_ENABLE
+ /* source-user-role [9] Unsigned8 OPTIONAL */
+ uint8_t source_user_role;
+#endif
+ /* target-device [10] BACnetRecipient */
+ BACNET_RECIPIENT target_device;
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_OBJECT_ENABLE
+ /* target-object [11] BACnetObjectIdentifier OPTIONAL */
+ BACNET_OBJECT_ID target_object;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PROPERTY_ENABLE
+ /* target-property [12] BACnetPropertyReference OPTIONAL */
+ struct BACnetPropertyReference target_property;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_PRIORITY_ENABLE
+ /* target-priority [13] Unsigned (1..16) OPTIONAL */
+ uint8_t target_priority;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_TARGET_VALUE_ENABLE
+ /* target-value [14] ABSTRACT-SYNTAX.&Type OPTIONAL */
+ BACNET_AUDIT_VALUE target_value;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_CURRENT_VALUE_ENABLE
+ /* current-value [15] ABSTRACT-SYNTAX.&Type OPTIONAL */
+ BACNET_AUDIT_VALUE current_value;
+#endif
+#ifdef BACNET_AUDIT_NOTIFICATION_RESULT_ENABLE
+ /* result [16] Error OPTIONAL */
+ BACNET_ERROR_CODE result;
+#endif
+} BACNET_AUDIT_NOTIFICATION;
+
+/**
+ * @brief Datum types associated with a BACnetAuditLogRecord.
+ * The tag numbers are used when encoding or decoding the log_datum field.
+ */
+#define AUDIT_LOG_DATUM_TAG_STATUS 0
+#define AUDIT_LOG_DATUM_TAG_NOTIFICATION 1
+#define AUDIT_LOG_DATUM_TAG_TIME_CHANGE 2
+
+/*
+ * BACnetAuditLogRecord ::= SEQUENCE {
+ * timestamp [0] BACnetDateTime,
+ * log-datum [1] CHOICE {
+ * log-status [0] BACnetLogStatus,
+ * audit-notification [1] BACnetAuditNotification,
+ * time-change [2] REAL
+ * }
+ * }
+ */
+typedef struct BACnetAuditLogRecord {
+ BACNET_DATE_TIME timestamp;
+ uint8_t tag;
+ union {
+ uint8_t log_status;
+ BACNET_AUDIT_NOTIFICATION notification;
+ float time_change;
+ } log_datum;
+} BACNET_AUDIT_LOG_RECORD;
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+BACNET_STACK_EXPORT
+int bacnet_audit_log_record_encode(
+ uint8_t *apdu, const BACNET_AUDIT_LOG_RECORD *value);
+BACNET_STACK_EXPORT
+int bacnet_audit_log_record_decode(
+ const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_LOG_RECORD *value);
+BACNET_STACK_EXPORT
+bool bacnet_audit_log_record_same(
+ const BACNET_AUDIT_LOG_RECORD *value1,
+ const BACNET_AUDIT_LOG_RECORD *value2);
+
+BACNET_STACK_EXPORT
+int bacnet_audit_log_notification_encode(
+ uint8_t *apdu, const BACNET_AUDIT_NOTIFICATION *value);
+BACNET_STACK_EXPORT
+int bacnet_audit_log_notification_decode(
+ const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_NOTIFICATION *value);
+BACNET_STACK_EXPORT
+int bacnet_audit_log_notification_context_decode(
+ const uint8_t *apdu,
+ uint16_t apdu_size,
+ uint8_t tag_number,
+ BACNET_AUDIT_NOTIFICATION *value);
+BACNET_STACK_EXPORT
+bool bacnet_audit_log_notification_same(
+ const BACNET_AUDIT_NOTIFICATION *value1,
+ const BACNET_AUDIT_NOTIFICATION *value2);
+
+BACNET_STACK_EXPORT
+int bacnet_audit_value_encode(uint8_t *apdu, const BACNET_AUDIT_VALUE *value);
+BACNET_STACK_EXPORT
+int bacnet_audit_value_context_encode(
+ uint8_t *apdu, uint8_t tag_number, const BACNET_AUDIT_VALUE *value);
+BACNET_STACK_EXPORT
+int bacnet_audit_value_decode(
+ const uint8_t *apdu, uint32_t apdu_size, BACNET_AUDIT_VALUE *value);
+BACNET_STACK_EXPORT
+int bacnet_audit_value_context_decode(
+ const uint8_t *apdu,
+ uint16_t apdu_size,
+ uint8_t tag_number,
+ BACNET_AUDIT_VALUE *value);
+BACNET_STACK_EXPORT
+bool bacnet_audit_value_same(
+ const BACNET_AUDIT_VALUE *value1, const BACNET_AUDIT_VALUE *value2);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
diff --git a/src/bacnet/bacenum.h b/src/bacnet/bacenum.h
index 97e96380..5eedbd54 100644
--- a/src/bacnet/bacenum.h
+++ b/src/bacnet/bacenum.h
@@ -1861,7 +1861,8 @@ typedef enum BACnetStatusFlags {
typedef enum BACnetLogStatus {
LOG_STATUS_LOG_DISABLED = 0,
LOG_STATUS_BUFFER_PURGED = 1,
- LOG_STATUS_LOG_INTERRUPTED = 2
+ LOG_STATUS_LOG_INTERRUPTED = 2,
+ LOG_STATUS_MAX = 3
} BACNET_LOG_STATUS;
typedef enum BACnetLoggingType {
@@ -3123,7 +3124,8 @@ typedef enum BACnetAuditOperation {
The enumerated values match the bit positions in
BACnetAuditOperationFlags. */
AUDIT_OPERATION_PROPRIETARY_MIN = 32,
- AUDIT_OPERATION_PROPRIETARY_MAX = 63
+ AUDIT_OPERATION_PROPRIETARY_MAX = 63,
+ AUDIT_OPERATION_MAX = 64
} BACNET_AUDIT_OPERATION;
typedef enum BACnetSCHubConnectorState {
diff --git a/src/bacnet/basic/object/auditlog.c b/src/bacnet/basic/object/auditlog.c
new file mode 100644
index 00000000..dd49d6ac
--- /dev/null
+++ b/src/bacnet/basic/object/auditlog.c
@@ -0,0 +1,1486 @@
+/**
+ * @file
+ * @brief Audit Log object, customize for your use
+ * @details An Audit Log object combines audit notifications
+ * from operation sources and operation targets and stores the
+ * combined record in an internal buffer for subsequent retrieval.
+ * Each timestamped buffer entry is called an audit log "record."
+ *
+ * Each Audit Log object maintains an internal, persistent, optionally
+ * fixed-size log buffer. This log buffer fills or grows as audit
+ * log records are added. If the log buffer becomes full, the least recent
+ * log records are overwritten when new log records are added.
+ * Log buffers are transferred as a list of BACnetAuditLogRecord values
+ * using the ReadRange and AuditLogQuery services. Each log record in the
+ * log buffer has an implied sequence number that is equal to the value
+ * of the Total_Record_Count property immediately after the record is added.
+ * See Clause 19.6 for a full description of how audit notifications are
+ * added to audit logs.
+ *
+ * As records are added into the log, the Audit Log object will scan
+ * existing entries for a matching record. A record is a match if:
+ * (a) the record contains the timestamp for the opposite actor (the
+ * record contains the operation source timestamp when merging
+ * in an operation target notification and vice-versa);
+ * (b) the operation-source, operation, invoke-id, target-device,
+ * target-property, are all equal;
+ * (c) if the user-id, user-role, target-value fields are provided
+ * in both notifications then they are equal; and
+ * (d) if the source-timestamp and target-timestamp values are
+ * approximately equal (+/- APDU_Timeout * 2).
+ *
+ * If a match is found, the existing log record is updated.
+ * Otherwise, a new record is created. If a match is found,
+ * and it already contains both an operation source and an
+ * operation target portion, then the notification is dropped.
+ * When creating a new record, those fields which are not supplied
+ * in the notification (such as the 'Source Timestamp' when a
+ * server notification is received) shall be absent from the record.
+ * When updating an existing record, those fields not supplied in the
+ * original notification are updated from the new notification, if present.
+ * For the 'Current Value' field, a value provided by the operation target
+ * device shall always take precedence over a value provided by an operation
+ * source device. As such, if the values provided in the peer notifications
+ * differ, the operation target value shall be the one used in the record.
+ *
+ * Logging may be enabled and disabled through the Enable property.
+ * Audit Log enabling and disabling is recorded in the audit log buffer.
+ *
+ * Unlike other log objects, Audit Log objects do not use the BUFFER_READY
+ * event algorithm.
+ *
+ * The acquisition of log records by remote devices has
+ * no effect upon the state of the Audit Log object itself. This allows
+ * completely independent, but properly sequential, access to its log records
+ * by all remote devices. Any remote device can independently update its
+ * log records at any time.
+ *
+ * Audit Log objects may optionally support forwarding of audit notifications
+ * to “parent” audit logs. This functionality improves the reliability of the
+ * audit system by allowing intermediaries to buffer audit notifications
+ * in the case where the ultimate audit logger is offline for a short period
+ * of time. It is expected that intermediaries be capable of storing a larger
+ * number of records than devices which report auditable actions. It is also
+ * useful for buffering of audit notifications so they can be sent in bulk to
+ * the parent audit log. When operating in this mode, with the
+ * Delete_On_Forward property set to TRUE, the object is not required
+ * to perform audit notification matching and combining.
+ *
+ * @author Mikhail Antropov
+ * @author Steve Karg
+ * @date July 2023
+ * @copyright SPDX-License-Identifier: MIT
+ */
+#include
+#include
+#include
+#include
+/* BACnet Stack defines - first */
+#include "bacnet/bacdef.h"
+/* BACnet Stack API */
+#include "bacnet/bacdcode.h"
+#include "bacnet/bacapp.h"
+#include "bacnet/bacaudit.h"
+#include "bacnet/apdu.h"
+#include "bacnet/bacdevobjpropref.h"
+#include "bacnet/datetime.h"
+#include "bacnet/rp.h"
+#include "bacnet/wp.h"
+#include "bacnet/basic/services.h"
+#include "bacnet/basic/sys/keylist.h"
+/* me! */
+#include "auditlog.h"
+
+#ifndef BACNET_AUDIT_LOG_RECORDS_MAX
+#define BACNET_AUDIT_LOG_RECORDS_MAX 128
+#endif
+
+struct object_data {
+ bool Enable;
+ bool Out_Of_Service;
+ int Buffer_Size;
+ OS_Keylist Records;
+ int Record_Count_Total;
+ const char *Object_Name;
+ const char *Description;
+};
+/* Key List for storing the object data sorted by instance number */
+static OS_Keylist Object_List;
+
+static const int Properties_Required[] = {
+ /* required properties that are supported for this object */
+ PROP_OBJECT_IDENTIFIER,
+ PROP_OBJECT_NAME,
+ PROP_OBJECT_TYPE,
+ PROP_STATUS_FLAGS,
+ PROP_EVENT_STATE,
+ PROP_ENABLE,
+ PROP_BUFFER_SIZE,
+ PROP_LOG_BUFFER,
+ PROP_RECORD_COUNT,
+ PROP_TOTAL_RECORD_COUNT,
+ -1
+};
+
+static const int Properties_Optional[] = { PROP_DESCRIPTION, -1 };
+
+static const int Properties_Proprietary[] = { -1 };
+
+static const int BACnetARRAY_Properties[] = {
+ /* standard properties that are arrays for this object */
+ PROP_LOG_BUFFER,
+ PROP_EVENT_TIME_STAMPS,
+ PROP_EVENT_MESSAGE_TEXTS,
+ PROP_EVENT_MESSAGE_TEXTS_CONFIG,
+ PROP_TAGS,
+ -1
+};
+
+/**
+ * @brief Determine if the object property is a BACnetARRAY property
+ * @param object_property - object-property to be checked
+ * @return true if the property is a BACnetARRAY property
+ */
+static bool BACnetARRAY_Property(int object_property)
+{
+ return property_list_member(BACnetARRAY_Properties, object_property);
+}
+
+/**
+ * Returns the list of required, optional, and proprietary properties.
+ * Used by ReadPropertyMultiple service.
+ *
+ * @param pRequired - pointer to list of int terminated by -1, of
+ * BACnet required properties for this object.
+ * @param pOptional - pointer to list of int terminated by -1, of
+ * BACnet optkional properties for this object.
+ * @param pProprietary - pointer to list of int terminated by -1, of
+ * BACnet proprietary properties for this object.
+ */
+void Audit_Log_Property_Lists(
+ const int **pRequired, const int **pOptional, const int **pProprietary)
+{
+ if (pRequired) {
+ *pRequired = Properties_Required;
+ }
+ if (pOptional) {
+ *pOptional = Properties_Optional;
+ }
+ if (pProprietary) {
+ *pProprietary = Properties_Proprietary;
+ }
+
+ return;
+}
+
+/**
+ * @brief Gets an object from the list using an instance number as the key
+ * @param object_instance - object-instance number of the object
+ * @return object found in the list, or NULL if not found
+ */
+static struct object_data *Object_Data(uint32_t object_instance)
+{
+ return Keylist_Data(Object_List, object_instance);
+}
+
+/**
+ * @brief Determines if a given object instance is valid
+ * @param object_instance - object-instance number of the object
+ * @return true if the instance is valid, and false if not
+ */
+bool Audit_Log_Valid_Instance(uint32_t object_instance)
+{
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * @brief Determines the number of objects
+ * @return Number of objects
+ */
+unsigned Audit_Log_Count(void)
+{
+ return Keylist_Count(Object_List);
+}
+
+/**
+ * @brief Determines the object instance-number for a given 0..N index
+ * of objects where N is the count.
+ * @param index - 0..N value
+ * @return object instance-number for a valid given index, or UINT32_MAX
+ */
+uint32_t Audit_Log_Index_To_Instance(unsigned index)
+{
+ uint32_t instance = UINT32_MAX;
+
+ (void)Keylist_Index_Key(Object_List, index, &instance);
+
+ return instance;
+}
+
+/**
+ * @brief For a given object instance-number, determines a 0..N index
+ * of objects where N is the count.
+ * @param object_instance - object-instance number of the object
+ * @return index for the given instance-number, or count if not valid.
+ */
+unsigned Audit_Log_Instance_To_Index(uint32_t object_instance)
+{
+ return Keylist_Index(Object_List, object_instance);
+}
+
+/**
+ * For a given object instance-number, returns the Audit Log entity by index.
+ *
+ * @param object_instance - object-instance number of the object
+ * @param index - index of entity
+ *
+ * @return Audit Log entity, or NULL if not found.
+ */
+BACNET_AUDIT_LOG_RECORD *
+Audit_Log_Record_Entry(uint32_t object_instance, uint32_t index)
+{
+ BACNET_AUDIT_LOG_RECORD *entry = NULL;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ entry = Keylist_Data_Index(pObject->Records, index);
+ }
+
+ return entry;
+}
+
+/**
+ * @brief Delete a record entry from the log buffer
+ * @param object_instance - object-instance number of the object
+ * @param index - record index of entity
+ */
+void Audit_Log_Record_Entry_Delete(uint32_t object_instance, uint32_t index)
+{
+ BACNET_AUDIT_LOG_RECORD *entry = NULL;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ entry = Keylist_Data_Delete(pObject->Records, index);
+ free(entry);
+ }
+}
+
+/**
+ * For a given object instance-number, adds a Audit Log entity to entities list.
+ *
+ * @note If the log buffer becomes full, the least recent log records are
+ * overwritten when new log records are added.
+ *
+ * @param object_instance - object-instance number of the object
+ * @param entity - Audit Log entity
+ *
+ * @return true if the entity is add successfully.
+ */
+bool Audit_Log_Record_Entry_Add(
+ uint32_t object_instance, const BACNET_AUDIT_LOG_RECORD *value)
+{
+ BACNET_AUDIT_LOG_RECORD *entry = NULL;
+ struct object_data *pObject;
+ int index;
+
+ pObject = Object_Data(object_instance);
+ if (!pObject) {
+ return false;
+ }
+ if (Keylist_Count(pObject->Records) >= pObject->Buffer_Size) {
+ /* log is full, so delete oldest record before adding a new record */
+ entry = Keylist_Data_Delete_By_Index(pObject->Records, 0);
+ }
+ if (!entry) {
+ entry = calloc(1, sizeof(BACNET_AUDIT_LOG_RECORD));
+ if (!entry) {
+ return false;
+ }
+ }
+ memcpy(entry, value, sizeof(BACNET_AUDIT_LOG_RECORD));
+ index =
+ Keylist_Data_Add(pObject->Records, pObject->Record_Count_Total, entry);
+ if (index < 0) {
+ free(entry);
+ return false;
+ }
+ pObject->Record_Count_Total++;
+
+ return true;
+}
+
+/**
+ * @brief Get the log record maximum length for this object instance
+ * @param object_instance - object-instance number of the object
+ * @return maximum number of log records
+ * @note For products that support very large log objects,
+ * the value 2^32-1 is reserved to indicate that the buffer size is
+ * unknown and is constrained solely by currently available resources.
+ */
+uint32_t Audit_Log_Buffer_Size(uint32_t object_instance)
+{
+ struct object_data *pObject;
+ pObject = Object_Data(object_instance);
+ if (!pObject) {
+ return 0;
+ }
+
+ return pObject->Buffer_Size;
+}
+
+/**
+ * @brief Set the log record maximum length for this object instance
+ * @param object_instance - object-instance number of the object
+ * @param buffer_size - maximum number of log records
+ * @return true if the maximum number of log records is set
+ */
+bool Audit_Log_Buffer_Size_Set(uint32_t object_instance, uint32_t buffer_size)
+{
+ struct object_data *pObject;
+ BACNET_AUDIT_LOG_RECORD *entry = NULL;
+ int i;
+
+ pObject = Object_Data(object_instance);
+ if (!pObject) {
+ return false;
+ }
+ if (buffer_size > INT_MAX) {
+ return false;
+ }
+ if (buffer_size < pObject->Buffer_Size) {
+ /* The disposition of existing log records when Buffer_Size is written
+ is a local matter. We can shrink the log buffer. */
+ for (i = buffer_size; i < pObject->Buffer_Size; i++) {
+ entry = Keylist_Data_Delete_By_Index(pObject->Records, i);
+ free(entry);
+ }
+ }
+ pObject->Buffer_Size = buffer_size;
+
+ return true;
+}
+
+/**
+ * For a given object instance-number, loads the object-name into
+ * a characterstring. Note that the object name must be unique
+ * within this device.
+ *
+ * @param object_instance - object-instance number of the object
+ * @param object_name - holds the object-name retrieved
+ *
+ * @return true if object-name was retrieved
+ */
+bool Audit_Log_Object_Name(
+ uint32_t object_instance, BACNET_CHARACTER_STRING *object_name)
+{
+ bool status = false;
+ struct object_data *pObject;
+ char name_text[32] = "AUDIT-LOG-4194303";
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ if (pObject->Object_Name) {
+ status =
+ characterstring_init_ansi(object_name, pObject->Object_Name);
+ } else {
+ snprintf(
+ name_text, sizeof(name_text), "AUDIT-LOG-%lu",
+ (unsigned long)object_instance);
+ status = characterstring_init_ansi(object_name, name_text);
+ }
+ }
+
+ return status;
+}
+
+/**
+ * For a given object instance-number, sets the object-name
+ * Note that the object name must be unique within this device.
+ *
+ * @param object_instance - object-instance number of the object
+ * @param new_name - holds the object-name to be set
+ *
+ * @return true if object-name was set
+ */
+bool Audit_Log_Name_Set(uint32_t object_instance, const char *new_name)
+{
+ bool status = false; /* return value */
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ status = true;
+ pObject->Object_Name = new_name;
+ }
+
+ return status;
+}
+
+/**
+ * @brief Return the object name C string
+ * @param object_instance [in] BACnet object instance number
+ * @return object name or NULL if not found
+ */
+const char *Audit_Log_Name_ASCII(uint32_t object_instance)
+{
+ const char *name = NULL;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ name = pObject->Object_Name;
+ }
+
+ return name;
+}
+
+/**
+ * For a given object instance-number, returns the description
+ *
+ * @param object_instance - object-instance number of the object
+ *
+ * @return description text or NULL if not found
+ */
+const char *Audit_Log_Description(uint32_t object_instance)
+{
+ const char *name = NULL;
+ const struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ if (pObject->Description) {
+ name = pObject->Description;
+ }
+ }
+
+ return name;
+}
+
+/**
+ * For a given object instance-number, sets the description
+ *
+ * @param object_instance - object-instance number of the object
+ * @param new_name - holds the description to be set
+ *
+ * @return true if object-name was set
+ */
+bool Audit_Log_Description_Set(uint32_t object_instance, const char *new_name)
+{
+ bool status = false; /* return value */
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ status = true;
+ pObject->Description = new_name;
+ }
+
+ return status;
+}
+
+/**
+ * @brief Determines a object enabled flag state
+ *
+ * @note Logging occurs if and only if Enable is TRUE.
+ * Log_Buffer records of type log-status are recorded
+ * without regard to the value of the Enable property.
+ *
+ * @param object_instance - object-instance number of the object
+ * @return enabled status flag
+ */
+bool Audit_Log_Enable(uint32_t object_instance)
+{
+ bool value = false;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ value = pObject->Enable;
+ }
+
+ return value;
+}
+
+/**
+ * @brief Apply the log enabled algormithm
+ * @param pObject - object data
+ * @param enable log enable flag
+ * @return true if the log enable flag is applied
+ */
+bool Audit_Log_Enable_Set(uint32_t object_instance, bool enable)
+{
+ bool status = false;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ if (pObject->Enable != enable) {
+ /* Only trigger this validation on a potential change of state */
+ pObject->Enable = enable;
+ if (enable == false) {
+ Audit_Log_Record_Status_Insert(
+ object_instance, LOG_STATUS_LOG_DISABLED, true);
+ } else {
+ Audit_Log_Record_Status_Insert(
+ object_instance, LOG_STATUS_LOG_DISABLED, false);
+ }
+ }
+ status = true;
+ }
+
+ return status;
+}
+
+/**
+ * @brief For a given object instance-number, sets the object enabled flag
+ * @param object_instance - object-instance number of the object
+ * @param enable - holds the value to be set
+ * @param error_class - BACnet error class
+ * @param error_code - BACnet error code
+ * @return true if set
+ */
+static bool Audit_Log_Enable_Write(
+ uint32_t object_instance,
+ bool enable,
+ BACNET_ERROR_CLASS *error_class,
+ BACNET_ERROR_CODE *error_code)
+{
+ bool status = false;
+
+ status = Audit_Log_Enable_Set(object_instance, enable);
+ if (!status) {
+ *error_class = ERROR_CLASS_OBJECT;
+ *error_code = ERROR_CODE_LOG_BUFFER_FULL;
+ }
+
+ return status;
+}
+
+/**
+ * @brief For a given object instance-number, determines the property value
+ * @param object_instance - object-instance number of the object
+ * @return the property value
+ */
+uint32_t Audit_Log_Record_Count(uint32_t object_instance)
+{
+ uint32_t record_count = 0;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ record_count = Keylist_Count(pObject->Records);
+ }
+
+ return record_count;
+}
+
+/**
+ * @brief For a given object instance-number, determines the property value
+ * @param object_instance - object-instance number of the object
+ * @return the property value
+ */
+uint32_t Audit_Log_Total_Record_Count(uint32_t object_instance)
+{
+ uint32_t total_count = 0;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ total_count = pObject->Record_Count_Total;
+ }
+
+ return total_count;
+}
+
+/**
+ * @brief For a given object instance-number, writes the property value
+ * @details If writable, it may not be written when Enable is TRUE.
+ * @param object_instance - object-instance number of the object
+ * @param buffer_size - holds the value to be set
+ * @param error_class - BACnet error class
+ * @param error_code - BACnet error code
+ * @return true if set
+ */
+bool Audit_Log_Buffer_Size_Write(
+ uint32_t object_instance,
+ uint32_t buffer_size,
+ BACNET_ERROR_CLASS *error_class,
+ BACNET_ERROR_CODE *error_code)
+{
+ bool status = false;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (pObject) {
+ if (pObject->Enable) {
+ *error_class = ERROR_CLASS_PROPERTY;
+ *error_code = ERROR_CODE_WRITE_ACCESS_DENIED;
+ } else if (buffer_size > INT_MAX) {
+ /* keylist library uses 'int' so that is our limit */
+ *error_class = ERROR_CLASS_PROPERTY;
+ *error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
+ } else {
+ status = Audit_Log_Buffer_Size_Set(object_instance, buffer_size);
+ }
+ }
+
+ return status;
+}
+
+/**
+ * ReadProperty handler for this object. For the given ReadProperty
+ * data, the application_data is loaded or the error flags are set.
+ *
+ * @param rpdata - BACNET_READ_PROPERTY_DATA data, including
+ * requested data and space for the reply, or error response.
+ *
+ * @return number of APDU bytes in the response, or
+ * BACNET_STATUS_ERROR on error.
+ */
+int Audit_Log_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata)
+{
+ int apdu_len = 0; /* return value */
+ BACNET_CHARACTER_STRING char_string = { 0 };
+ BACNET_BIT_STRING bit_string = { 0 };
+ uint8_t *apdu = NULL;
+ int apdu_max = 0;
+
+ if ((rpdata == NULL) || (rpdata->application_data == NULL) ||
+ (rpdata->application_data_len == 0)) {
+ return 0;
+ }
+ apdu = rpdata->application_data;
+ apdu_max = rpdata->application_data_len;
+ switch (rpdata->object_property) {
+ case PROP_OBJECT_IDENTIFIER:
+ apdu_len = encode_application_object_id(
+ &apdu[0], rpdata->object_type, rpdata->object_instance);
+ break;
+ case PROP_OBJECT_NAME:
+ Audit_Log_Object_Name(rpdata->object_instance, &char_string);
+ apdu_len =
+ encode_application_character_string(&apdu[0], &char_string);
+ break;
+ case PROP_OBJECT_TYPE:
+ apdu_len =
+ encode_application_enumerated(&apdu[0], rpdata->object_type);
+ break;
+ case PROP_ENABLE:
+ apdu_len = encode_application_boolean(
+ &apdu[0], Audit_Log_Enable(rpdata->object_instance));
+ break;
+ case PROP_BUFFER_SIZE:
+ apdu_len = encode_application_unsigned(
+ &apdu[0], Audit_Log_Buffer_Size(rpdata->object_instance));
+ break;
+ case PROP_LOG_BUFFER:
+ /* You can only read the buffer via the ReadRange service */
+ rpdata->error_class = ERROR_CLASS_PROPERTY;
+ rpdata->error_code = ERROR_CODE_READ_ACCESS_DENIED;
+ apdu_len = BACNET_STATUS_ERROR;
+ break;
+ case PROP_RECORD_COUNT:
+ apdu_len = encode_application_unsigned(
+ &apdu[0], Audit_Log_Record_Count(rpdata->object_instance));
+ break;
+ case PROP_TOTAL_RECORD_COUNT:
+ apdu_len = encode_application_unsigned(
+ &apdu[0],
+ Audit_Log_Total_Record_Count(rpdata->object_instance));
+ break;
+ case PROP_EVENT_STATE:
+ /* note: see the details in the standard on how to use this */
+ apdu_len =
+ encode_application_enumerated(&apdu[0], EVENT_STATE_NORMAL);
+ break;
+ case PROP_STATUS_FLAGS:
+ /* note: see the details in the standard on how to use these */
+ bitstring_init(&bit_string);
+ bitstring_set_bit(&bit_string, STATUS_FLAG_IN_ALARM, false);
+ bitstring_set_bit(&bit_string, STATUS_FLAG_FAULT, false);
+ /* OVERRIDDEN The value of this flag shall be Logical FALSE */
+ bitstring_set_bit(&bit_string, STATUS_FLAG_OVERRIDDEN, false);
+ /* OUT_OF_SERVICE The value of this flag shall be Logical FALSE */
+ bitstring_set_bit(&bit_string, STATUS_FLAG_OUT_OF_SERVICE, false);
+ apdu_len = encode_application_bitstring(&apdu[0], &bit_string);
+ break;
+ case PROP_DESCRIPTION:
+ characterstring_init_ansi(
+ &char_string, Audit_Log_Description(rpdata->object_instance));
+ apdu_len =
+ encode_application_character_string(&apdu[0], &char_string);
+ break;
+ default:
+ rpdata->error_class = ERROR_CLASS_PROPERTY;
+ rpdata->error_code = ERROR_CODE_UNKNOWN_PROPERTY;
+ apdu_len = BACNET_STATUS_ERROR;
+ break;
+ }
+ /* only array properties can have array options */
+ if ((apdu_len >= 0) && (!BACnetARRAY_Property(rpdata->object_property)) &&
+ (rpdata->array_index != BACNET_ARRAY_ALL)) {
+ rpdata->error_class = ERROR_CLASS_PROPERTY;
+ rpdata->error_code = ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY;
+ apdu_len = BACNET_STATUS_ERROR;
+ }
+
+ return apdu_len;
+}
+
+/**
+ * WriteProperty handler for this object. For the given WriteProperty
+ * data, the application_data is loaded or the error flags are set.
+ *
+ * @param wp_data - BACNET_WRITE_PROPERTY_DATA data, including
+ * requested data and space for the reply, or error response.
+ *
+ * @return false if an error is loaded, true if no errors
+ */
+bool Audit_Log_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data)
+{
+ bool status = false; /* return value */
+ int len = 0;
+ BACNET_APPLICATION_DATA_VALUE value;
+
+ /* decode the some of the request */
+ len = bacapp_decode_application_data(
+ wp_data->application_data, wp_data->application_data_len, &value);
+ /* FIXME: len < application_data_len: more data? */
+ if (len < 0) {
+ /* error while decoding - a value larger than we can handle */
+ wp_data->error_class = ERROR_CLASS_PROPERTY;
+ wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE;
+ return false;
+ }
+ if ((!BACnetARRAY_Property(wp_data->object_property)) &&
+ (wp_data->array_index != BACNET_ARRAY_ALL)) {
+ /* only array properties can have array options */
+ wp_data->error_class = ERROR_CLASS_PROPERTY;
+ wp_data->error_code = ERROR_CODE_PROPERTY_IS_NOT_AN_ARRAY;
+ return false;
+ }
+ switch (wp_data->object_property) {
+ case PROP_ENABLE:
+ status = write_property_type_valid(
+ wp_data, &value, BACNET_APPLICATION_TAG_BOOLEAN);
+ if (status) {
+ status = Audit_Log_Enable_Write(
+ wp_data->object_instance, value.type.Boolean,
+ &wp_data->error_class, &wp_data->error_code);
+ }
+ break;
+ case PROP_BUFFER_SIZE:
+ status = write_property_type_valid(
+ wp_data, &value, BACNET_APPLICATION_TAG_UNSIGNED_INT);
+ if (status) {
+ status = Audit_Log_Buffer_Size_Write(
+ wp_data->object_instance, value.type.Unsigned_Int,
+ &wp_data->error_class, &wp_data->error_code);
+ }
+ break;
+ default:
+ if (property_lists_member(
+ Properties_Required, Properties_Optional,
+ Properties_Proprietary, wp_data->object_property)) {
+ wp_data->error_class = ERROR_CLASS_PROPERTY;
+ wp_data->error_code = ERROR_CODE_WRITE_ACCESS_DENIED;
+ } else {
+ wp_data->error_class = ERROR_CLASS_PROPERTY;
+ wp_data->error_code = ERROR_CODE_UNKNOWN_PROPERTY;
+ }
+ break;
+ }
+
+ return status;
+}
+
+/**
+ * Inserts a status notification into a audit log
+ *
+ * @param instance - object-instance number of the object
+ * @param log_status - log status
+ * @param state - notificate state
+ */
+void Audit_Log_Record_Status_Insert(
+ uint32_t object_instance, BACNET_LOG_STATUS log_status, bool state)
+{
+ BACNET_AUDIT_LOG_RECORD record;
+ struct object_data *pObject;
+
+ pObject = Object_Data(object_instance);
+ if (!pObject) {
+ return;
+ }
+ datetime_local(&record.timestamp.date, &record.timestamp.time, NULL, NULL);
+ record.tag = AUDIT_LOG_DATUM_TAG_STATUS;
+ record.log_datum.log_status = 0;
+ /* Note we set the bits in correct order so that we can place them directly
+ * into the bitstring structure later on when we have to encode them */
+ switch (log_status) {
+ case LOG_STATUS_LOG_DISABLED:
+ if (state) {
+ record.log_datum.log_status = 1 << LOG_STATUS_LOG_DISABLED;
+ }
+ break;
+ case LOG_STATUS_BUFFER_PURGED:
+ if (state) {
+ record.log_datum.log_status = 1 << LOG_STATUS_BUFFER_PURGED;
+ }
+ break;
+ case LOG_STATUS_LOG_INTERRUPTED:
+ if (state) {
+ record.log_datum.log_status = 1 << LOG_STATUS_LOG_INTERRUPTED;
+ }
+ break;
+ default:
+ break;
+ }
+ Audit_Log_Record_Entry_Add(object_instance, &record);
+}
+
+/**
+ * @brief Determines if a given log notification is the same as another
+ * @param object_instance - object-instance number of the object
+ * @param record - log record
+ * @return the index of the found log record, or -1 if not found
+ */
+static int Audit_Log_Record_Search(
+ uint32_t object_instance, BACNET_AUDIT_LOG_RECORD *record)
+{
+ int i;
+ BACNET_AUDIT_LOG_RECORD *entry;
+ struct object_data *pObject;
+
+ if (!record) {
+ return -1;
+ }
+ pObject = Object_Data(object_instance);
+ if (!pObject) {
+ return -1;
+ }
+ for (i = 0; i < pObject->Buffer_Size; i++) {
+ entry = Audit_Log_Record_Entry(object_instance, i);
+ if (!entry) {
+ break;
+ }
+ if (entry->tag == record->tag) {
+ if (entry->tag == AUDIT_LOG_DATUM_TAG_STATUS) {
+ if (entry->log_datum.log_status ==
+ record->log_datum.log_status) {
+ return i;
+ }
+ } else if (entry->tag == AUDIT_LOG_DATUM_TAG_NOTIFICATION) {
+ if (bacnet_audit_log_notification_same(
+ &entry->log_datum.notification,
+ &record->log_datum.notification)) {
+ return i;
+ }
+ }
+ }
+ }
+
+ return -1;
+}
+
+/**
+ * Insert a notification record into a audit log.
+ *
+ * @param object_instance - object-instance number of the object
+ * @param notif - notificatione
+ */
+void Audit_Log_Record_Notification_Insert(
+ uint32_t object_instance, BACNET_AUDIT_NOTIFICATION *notification)
+{
+ BACNET_AUDIT_LOG_RECORD seek_entry = { 0 };
+ struct object_data *pObject;
+ int index;
+
+ pObject = Object_Data(object_instance);
+ if (!pObject) {
+ return;
+ }
+ if (!Audit_Log_Enable(object_instance)) {
+ return;
+ }
+ /* As records are added into the log, the Audit Log object will scan
+ existing entries for a matching record. */
+ datetime_local(
+ &seek_entry.timestamp.date, &seek_entry.timestamp.time, NULL, NULL);
+ seek_entry.tag = AUDIT_LOG_DATUM_TAG_NOTIFICATION;
+ memcpy(
+ &seek_entry.log_datum.notification, notification,
+ sizeof(BACNET_AUDIT_NOTIFICATION));
+ index = Audit_Log_Record_Search(object_instance, &seek_entry);
+ if (index >= 0) {
+ /* If a match is found, the existing record is updated with the new
+ time stamp and the record is moved to the end of the list.
+ i.e. delete the old entry and add the new entry */
+ Audit_Log_Record_Entry_Delete(object_instance, index);
+ }
+ Audit_Log_Record_Entry_Add(object_instance, &seek_entry);
+}
+
+/**
+ * For a given read range request, encodes log records
+ *
+ * @param apdu - buffer to hold the bytes
+ * @param pRequest - the read range request
+ *
+ * @return number of bytes encoded, or 0 if unable to encode.
+ */
+int Audit_Log_Read_Range(uint8_t *apdu, BACNET_READ_RANGE_DATA *pRequest)
+{
+ int apdu_len = 0;
+
+ /* buffer and buffer size is passed in BACNET_READ_RANGE_DATA */
+ (void)apdu;
+ /* Initialise result flags to all false */
+ bitstring_init(&pRequest->ResultFlags);
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_FIRST_ITEM, false);
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_LAST_ITEM, false);
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, false);
+ pRequest->ItemCount = 0; /* Start out with nothing */
+ if ((pRequest->RequestType == RR_BY_POSITION) ||
+ (pRequest->RequestType == RR_READ_ALL)) {
+ apdu_len = Audit_Log_Read_Range_By_Position(pRequest);
+ } else if (pRequest->RequestType == RR_BY_SEQUENCE) {
+ apdu_len = Audit_Log_Read_Range_By_Sequence(pRequest);
+ } else {
+ apdu_len = Audit_Log_Read_Range_By_Time(pRequest);
+ }
+
+ return apdu_len;
+}
+
+/**
+ * @brief Handle encoding for the By Position and All options.
+ * Does All option by converting to a By Position request starting at index
+ * 1 and of maximum log size length.
+ * @param pRequest - the read range request
+ * @return number of bytes encoded, or 0 if unable to encode.
+ */
+int Audit_Log_Read_Range_By_Position(BACNET_READ_RANGE_DATA *pRequest)
+{
+ int log_index = 0;
+ int apdu_len = 0;
+ size_t apdu_size;
+ int len;
+ uint8_t *apdu;
+ uint32_t record_count = 0;
+ BACNET_AUDIT_LOG_RECORD *entry = NULL;
+ int32_t iTemp = 0;
+ uint32_t uiIndex = 0; /* Current entry number */
+ uint32_t uiFirst = 0; /* Entry number we started encoding from */
+ uint32_t uiLast = 0; /* Entry number we finished encoding on */
+ uint32_t uiTarget = 0; /* Last entry we are required to encode */
+
+ record_count = Audit_Log_Record_Count(pRequest->object_instance);
+ /* See how much space we have */
+ apdu_size = pRequest->application_data_len - pRequest->Overhead;
+ if (pRequest->RequestType == RR_READ_ALL) {
+ /*
+ * Read all the list or as much as will fit in the buffer by selecting
+ * a range that covers the whole list and falling through to the next
+ * section of code
+ */
+ pRequest->Count = record_count;
+ /* Starting at the beginning */
+ pRequest->Range.RefIndex = 1;
+ }
+ if (pRequest->Count < 0) {
+ /*
+ * negative count means work from index backwards
+ *
+ * Convert from end index/negative count to
+ * start index/positive count and then process as
+ * normal. This assumes that the order to return items
+ * is always first to last, if this is not true we will
+ * have to handle this differently.
+ *
+ * Note: We need to be careful about how we convert these
+ * values due to the mix of signed and unsigned types - don't
+ * try to optimise the code unless you understand all the
+ * implications of the data type conversions!
+ */
+ /* pull out and convert to signed */
+ iTemp = pRequest->Range.RefIndex;
+ /* Adjust backwards, remember count is -ve */
+ iTemp += pRequest->Count + 1;
+ if (iTemp < 1) {
+ /* if count is too much, return from 1 to start index */
+ pRequest->Count = pRequest->Range.RefIndex;
+ pRequest->Range.RefIndex = 1;
+ } else {
+ /* Otherwise adjust the start index and make count +ve */
+ pRequest->Range.RefIndex = iTemp;
+ pRequest->Count = -pRequest->Count;
+ }
+ }
+ /* From here on in we only have a starting point and a positive count */
+ if (pRequest->Range.RefIndex > record_count) {
+ /* Nothing to return as we are past the end of the list */
+ return 0;
+ }
+ /* Index of last required entry */
+ uiTarget = pRequest->Range.RefIndex + pRequest->Count - 1;
+ if (uiTarget > record_count) {
+ /* Capped at end of list if necessary */
+ uiTarget = record_count;
+ }
+ uiIndex = pRequest->Range.RefIndex;
+ /* Record where we started from */
+ uiFirst = uiIndex;
+ apdu = pRequest->application_data;
+ while (uiIndex <= uiTarget) {
+ log_index = uiIndex - 1;
+ entry = Audit_Log_Record_Entry(pRequest->object_instance, log_index);
+ len = bacnet_audit_log_record_encode(NULL, entry);
+ if (len > (apdu_size - apdu_len)) {
+ /*
+ * Can't fit any more in! We just set the result flag to say there
+ * was more and drop out of the loop early
+ */
+ bitstring_set_bit(
+ &pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, true);
+ break;
+ }
+ len = bacnet_audit_log_record_encode(apdu, entry);
+ apdu += len;
+ apdu_len += len;
+ /* Record the last entry encoded */
+ uiLast = uiIndex;
+ /* and get ready for next one */
+ uiIndex++;
+ /* Chalk up another one for the response count */
+ pRequest->ItemCount++;
+ }
+ /* Set remaining result flags if necessary */
+ if (uiFirst == 1) {
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_FIRST_ITEM, true);
+ }
+ if (uiLast == record_count) {
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_LAST_ITEM, true);
+ }
+
+ return apdu_len;
+}
+
+/**
+ * Handle encoding for the By Sequence option.
+ * The fact that the buffer always has at least a single entry is used
+ * implicetly in the following as we don't have to handle the case of an
+ * empty buffer.
+ *
+ * @param apdu - buffer to hold the bytes
+ * @param pRequest - the read range request
+ *
+ * @return number of bytes encoded, or 0 if unable to encode.
+ */
+int Audit_Log_Read_Range_By_Sequence(BACNET_READ_RANGE_DATA *pRequest)
+{
+ int apdu_len = 0;
+ size_t apdu_size;
+ int len;
+ uint8_t *apdu;
+ int record_index = 0;
+ uint32_t record_count;
+ uint32_t total_record_count;
+ BACNET_AUDIT_LOG_RECORD *entry = NULL;
+ uint32_t uiIndex = 0; /* Current entry number */
+ uint32_t uiFirst = 0; /* Entry number we started encoding from */
+ uint32_t uiLast = 0; /* Entry number we finished encoding on */
+ uint32_t uiSequence = 0; /* Tracking sequence number when encoding */
+ uint32_t uiFirstSeq = 0; /* Sequence number for 1st record in log */
+ uint32_t uiBegin = 0; /* Starting Sequence number for request */
+ uint32_t uiEnd = 0; /* Ending Sequence number for request */
+ bool bWrapReq =
+ false; /* Has request sequence range spanned the max for uint32_t? */
+ bool bWrapLog =
+ false; /* Has log sequence range spanned the max for uint32_t? */
+
+ /* See how much space we have */
+ apdu = pRequest->application_data;
+ apdu_size = pRequest->application_data_len - pRequest->Overhead;
+ record_count = Audit_Log_Record_Count(pRequest->object_instance);
+ total_record_count =
+ Audit_Log_Total_Record_Count(pRequest->object_instance);
+ /* Figure out the sequence number for the first record, last is
+ * ulTotalRecordCount */
+ uiFirstSeq = total_record_count - record_count - 1;
+ /* Calculate start and end sequence numbers from request */
+ if (pRequest->Count < 0) {
+ uiBegin = pRequest->Range.RefSeqNum + pRequest->Count + 1;
+ uiEnd = pRequest->Range.RefSeqNum;
+ } else {
+ uiBegin = pRequest->Range.RefSeqNum;
+ uiEnd = pRequest->Range.RefSeqNum + pRequest->Count - 1;
+ }
+ /* See if we have any wrap around situations */
+ if (uiBegin > uiEnd) {
+ bWrapReq = true;
+ }
+ if (uiFirstSeq > total_record_count) {
+ bWrapLog = true;
+ }
+
+ if ((bWrapReq == false) && (bWrapLog == false)) {
+ /* Simple case no wraps */
+ /* If no overlap between request range and buffer contents bail out */
+ if ((uiEnd < uiFirstSeq) || (uiBegin > total_record_count)) {
+ return (0);
+ }
+ /* Truncate range if necessary so it is guaranteed to lie
+ * between the first and last sequence numbers in the buffer
+ * inclusive.
+ */
+ if (uiBegin < uiFirstSeq) {
+ uiBegin = uiFirstSeq;
+ }
+
+ if (uiEnd > total_record_count) {
+ uiEnd = total_record_count;
+ }
+ } else {
+ /* There are wrap arounds to contend with */
+ /* First check for non overlap condition as it is common to all */
+ if ((uiBegin > total_record_count) && (uiEnd < uiFirstSeq)) {
+ return (0);
+ }
+
+ if (bWrapLog == false) {
+ /* Only request range wraps */
+ if (uiEnd < uiFirstSeq) {
+ uiEnd = total_record_count;
+ if (uiBegin < uiFirstSeq) {
+ uiBegin = uiFirstSeq;
+ }
+ } else {
+ uiBegin = uiFirstSeq;
+ if (uiEnd > total_record_count) {
+ uiEnd = total_record_count;
+ }
+ }
+ } else if (bWrapReq == false) {
+ /* Only log wraps */
+ if (uiBegin > total_record_count) {
+ if (uiBegin > uiFirstSeq) {
+ uiBegin = uiFirstSeq;
+ }
+ } else {
+ if (uiEnd > total_record_count) {
+ uiEnd = total_record_count;
+ }
+ }
+ } else { /* Both wrap */
+ if (uiBegin < uiFirstSeq) {
+ uiBegin = uiFirstSeq;
+ }
+
+ if (uiEnd > total_record_count) {
+ uiEnd = total_record_count;
+ }
+ }
+ }
+ /* We now have a range that lies completely within the log buffer
+ * and we need to figure out where that starts in the buffer.
+ */
+ uiIndex = uiBegin - uiFirstSeq + 1;
+ uiSequence = uiBegin;
+ uiFirst = uiIndex; /* Record where we started from */
+ while (uiSequence != uiEnd + 1) {
+ record_index = uiIndex - 1;
+ entry = Audit_Log_Record_Entry(pRequest->object_instance, record_index);
+ len = bacnet_audit_log_record_encode(NULL, entry);
+ if (len > (apdu_size - apdu_len)) {
+ /*
+ * Can't fit any more in! We just set the result flag to say there
+ * was more and drop out of the loop early
+ */
+ bitstring_set_bit(
+ &pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, true);
+ break;
+ }
+ len = bacnet_audit_log_record_encode(apdu, entry);
+ apdu += len;
+ apdu_len += len;
+ uiLast = uiIndex; /* Record the last entry encoded */
+ uiIndex++; /* and get ready for next one */
+ uiSequence++;
+ pRequest->ItemCount++; /* Chalk up another one for the response count */
+ }
+
+ /* Set remaining result flags if necessary */
+ if (uiFirst == 1) {
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_FIRST_ITEM, true);
+ }
+
+ if (uiLast == record_count) {
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_LAST_ITEM, true);
+ }
+ pRequest->FirstSequence = uiBegin;
+
+ return apdu_len;
+}
+
+/**
+ * Handle encoding for the By Time option.
+ * The fact that the buffer always has at least a single entry is used
+ * implicetly in the following as we don't have to handle the case of an
+ * empty buffer.
+ *
+ * @param apdu - buffer to hold the bytes
+ * @param pRequest - the read range request
+ *
+ * @return number of bytes encoded, or 0 if unable to encode.
+ */
+int Audit_Log_Read_Range_By_Time(BACNET_READ_RANGE_DATA *pRequest)
+{
+ int apdu_len = 0;
+ size_t apdu_size;
+ int len;
+ uint8_t *apdu;
+ int record_index = 0;
+ uint32_t record_count;
+ uint32_t total_record_count;
+ BACNET_AUDIT_LOG_RECORD *entry = NULL;
+ int diff;
+
+ int32_t iTemp = 0;
+ int iCount = 0;
+ uint32_t uiIndex = 0; /* Current entry number */
+ uint32_t uiFirst = 0; /* Entry number we started encoding from */
+ uint32_t uiLast = 0; /* Entry number we finished encoding on */
+ uint32_t uiFirstSeq = 0; /* Sequence number for 1st record in log */
+
+ /* See how much space we have */
+ apdu = pRequest->application_data;
+ apdu_size = pRequest->application_data_len - pRequest->Overhead;
+ record_count = Audit_Log_Record_Count(pRequest->object_instance);
+ total_record_count =
+ Audit_Log_Total_Record_Count(pRequest->object_instance);
+ if (pRequest->Count < 0) {
+ /* Start at end of log and look for record which has
+ * timestamp greater than or equal to the reference.
+ */
+ iCount = record_count - 1;
+ /* Start out with the sequence number for the last record */
+ uiFirstSeq = total_record_count;
+ for (;;) {
+ record_index = iCount;
+ entry =
+ Audit_Log_Record_Entry(pRequest->object_instance, record_index);
+ diff =
+ datetime_compare(&entry->timestamp, &pRequest->Range.RefTime);
+ if (diff < 0) {
+ /* If datetime1 is before datetime2, returns negative.*/
+ break;
+ }
+ uiFirstSeq--;
+ if (iCount) {
+ iCount--;
+ } else {
+ /* end of records, not found */
+ return 0;
+ }
+ }
+ /* We have an and point for our request,
+ * now work backwards to find where we should start from
+ */
+ pRequest->Count = -pRequest->Count; /* Convert to +ve count */
+ /* If count would bring us back beyond the limits
+ * Of the buffer then pin it to the start of the buffer
+ * otherwise adjust starting point and sequence number
+ * appropriately.
+ */
+ iTemp = pRequest->Count - 1;
+ if (iTemp > iCount) {
+ uiFirstSeq -= iCount;
+ pRequest->Count = iCount + 1;
+ iCount = 0;
+ } else {
+ uiFirstSeq -= iTemp;
+ iCount -= iTemp;
+ }
+ } else {
+ /* Start at beginning of log and look for 1st record which has
+ * timestamp greater than the reference time.
+ */
+ iCount = 0;
+ /* Figure out the sequence number for the first record, last is
+ * ulTotalRecordCount */
+ uiFirstSeq = total_record_count - record_count - 1;
+ for (;;) {
+ record_index = iCount;
+ entry =
+ Audit_Log_Record_Entry(pRequest->object_instance, record_index);
+ diff =
+ datetime_compare(&entry->timestamp, &pRequest->Range.RefTime);
+ if (diff > 0) {
+ /* If datetime1 is after datetime2, returns positive.*/
+ break;
+ }
+ uiFirstSeq++;
+ iCount++;
+ if ((uint32_t)iCount == record_count) {
+ return (0);
+ }
+ }
+ }
+
+ /* We now have a starting point for the operation and a +ve count */
+ uiIndex = iCount + 1; /* Convert to BACnet 1 based reference */
+ uiFirst = uiIndex; /* Record where we started from */
+ iCount = pRequest->Count;
+ while (iCount != 0) {
+ record_index = uiIndex - 1;
+ entry = Audit_Log_Record_Entry(pRequest->object_instance, record_index);
+ len = bacnet_audit_log_record_encode(NULL, entry);
+ if (len > (apdu_size - apdu_len)) {
+ /*
+ * Can't fit any more in! We just set the result flag to say there
+ * was more and drop out of the loop early
+ */
+ bitstring_set_bit(
+ &pRequest->ResultFlags, RESULT_FLAG_MORE_ITEMS, true);
+ break;
+ }
+ len = bacnet_audit_log_record_encode(apdu, entry);
+ apdu += len;
+ apdu_len += len;
+ uiLast = uiIndex; /* Record the last entry encoded */
+ uiIndex++; /* and get ready for next one */
+ pRequest->ItemCount++; /* Chalk up another one for the response count */
+ iCount--; /* And finally cross another one off the requested count */
+
+ if (uiIndex > record_count) {
+ /* Finish up if we hit the end of the log */
+ break;
+ }
+ }
+ /* Set remaining result flags if necessary */
+ if (uiFirst == 1) {
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_FIRST_ITEM, true);
+ }
+ if (uiLast == record_count) {
+ bitstring_set_bit(&pRequest->ResultFlags, RESULT_FLAG_LAST_ITEM, true);
+ }
+ pRequest->FirstSequence = uiFirstSeq;
+
+ return apdu_len;
+}
+
+/**
+ * @brief Creates a Audit Log object
+ * @param object_instance - object-instance number of the object
+ * @return object_instance if the object is created, else BACNET_MAX_INSTANCE
+ */
+uint32_t Audit_Log_Create(uint32_t object_instance)
+{
+ struct object_data *pObject = NULL;
+ int index = 0;
+
+ if (object_instance > BACNET_MAX_INSTANCE) {
+ return BACNET_MAX_INSTANCE;
+ } else if (object_instance == BACNET_MAX_INSTANCE) {
+ /* wildcard instance */
+ /* the Object_Identifier property of the newly created object
+ shall be initialized to a value that is unique within the
+ responding BACnet-user device. The method used to generate
+ the object identifier is a local matter.*/
+ object_instance = Keylist_Next_Empty_Key(Object_List, 1);
+ }
+ pObject = Keylist_Data(Object_List, object_instance);
+ if (!pObject) {
+ pObject = calloc(1, sizeof(struct object_data));
+ if (!pObject) {
+ return BACNET_MAX_INSTANCE;
+ }
+ pObject->Object_Name = NULL;
+ pObject->Description = NULL;
+ pObject->Records = Keylist_Create();
+ pObject->Buffer_Size = BACNET_AUDIT_LOG_RECORDS_MAX;
+ pObject->Enable = false;
+ pObject->Out_Of_Service = false;
+ /* add to list */
+ index = Keylist_Data_Add(Object_List, object_instance, pObject);
+ if (index < 0) {
+ free(pObject);
+ return BACNET_MAX_INSTANCE;
+ }
+ }
+
+ return object_instance;
+}
+
+/**
+ * @brief Deletes all the Audit Logs and their data
+ */
+static void Audit_Log_Records_Cleanup(OS_Keylist list)
+{
+ BACNET_AUDIT_LOG_RECORD *entry;
+
+ while (Keylist_Count(list) > 0) {
+ entry = Keylist_Data_Pop(list);
+ free(entry);
+ }
+ Keylist_Delete(list);
+}
+
+/**
+ * @brief Deletes an Audit Log object
+ * @param object_instance - object-instance number of the object
+ * @return true if the object is deleted
+ */
+bool Audit_Log_Delete(uint32_t object_instance)
+{
+ bool status = false;
+ struct object_data *pObject = NULL;
+
+ pObject = Keylist_Data_Delete(Object_List, object_instance);
+ if (pObject) {
+ Audit_Log_Records_Cleanup(pObject->Records);
+ free(pObject);
+ status = true;
+ }
+
+ return status;
+}
+
+/**
+ * @brief Deletes all the Audit Logs and their data
+ */
+void Audit_Log_Cleanup(void)
+{
+ struct object_data *pObject;
+
+ if (Object_List) {
+ do {
+ pObject = Keylist_Data_Pop(Object_List);
+ if (pObject) {
+ Audit_Log_Records_Cleanup(pObject->Records);
+ free(pObject);
+ }
+ } while (pObject);
+ Keylist_Delete(Object_List);
+ Object_List = NULL;
+ }
+}
+
+/**
+ * @brief Initializes the Audit Log object data
+ */
+void Audit_Log_Init(void)
+{
+ if (!Object_List) {
+ Object_List = Keylist_Create();
+ }
+}
diff --git a/src/bacnet/basic/object/auditlog.h b/src/bacnet/basic/object/auditlog.h
new file mode 100644
index 00000000..a07ac14b
--- /dev/null
+++ b/src/bacnet/basic/object/auditlog.h
@@ -0,0 +1,124 @@
+/**
+ * @file
+ * @author Mikhail Antropov
+ * @date Jul 2023
+ * @brief Auditlog object, customize for your use
+ *
+ * @section DESCRIPTION
+ *
+ * An Audit Log object combines audit notifications from operation sources and
+ * operation targets and stores the combined record in an internal buffer for
+ * subsequent retrieval. Each timestamped buffer entry is called an audit log
+ * "record."
+ *
+ * @section LICENSE
+ *
+ * Copyright (C) 2023 Steve Karg
+ *
+ * SPDX-License-Identifier: MIT
+ */
+#ifndef AUDITLOG_H
+#define AUDITLOG_H
+
+#include
+#include
+/* BACnet Stack defines - first */
+#include "bacnet/bacdef.h"
+/* BACnet Stack API */
+#include "bacnet/bacaudit.h"
+#include "bacnet/bacdest.h"
+#include "bacnet/cov.h"
+#include "bacnet/datetime.h"
+#include "bacnet/readrange.h"
+#include "bacnet/rp.h"
+#include "bacnet/wp.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+BACNET_STACK_EXPORT
+void Audit_Log_Property_Lists(
+ const int **pRequired, const int **pOptional, const int **pProprietary);
+
+BACNET_STACK_EXPORT
+bool Audit_Log_Valid_Instance(uint32_t object_instance);
+BACNET_STACK_EXPORT
+unsigned Audit_Log_Count(void);
+BACNET_STACK_EXPORT
+uint32_t Audit_Log_Index_To_Instance(unsigned index);
+BACNET_STACK_EXPORT
+unsigned Audit_Log_Instance_To_Index(uint32_t instance);
+BACNET_STACK_EXPORT
+bool Audit_Log_Object_Instance_Add(uint32_t instance);
+
+BACNET_STACK_EXPORT
+bool Audit_Log_Object_Name(
+ uint32_t object_instance, BACNET_CHARACTER_STRING *object_name);
+BACNET_STACK_EXPORT
+bool Audit_Log_Name_Set(uint32_t object_instance, const char *new_name);
+BACNET_STACK_EXPORT
+const char *Audit_Log_Name_ASCII(uint32_t object_instance);
+
+BACNET_STACK_EXPORT
+const char *Audit_Log_Description(uint32_t instance);
+BACNET_STACK_EXPORT
+bool Audit_Log_Description_Set(uint32_t instance, const char *new_name);
+
+BACNET_STACK_EXPORT
+int Audit_Log_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata);
+BACNET_STACK_EXPORT
+bool Audit_Log_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data);
+BACNET_STACK_EXPORT
+uint32_t Audit_Log_Create(uint32_t object_instance);
+BACNET_STACK_EXPORT
+bool Audit_Log_Delete(uint32_t object_instance);
+
+BACNET_STACK_EXPORT
+void Audit_Log_Cleanup(void);
+BACNET_STACK_EXPORT
+void Audit_Log_Init(void);
+
+BACNET_STACK_EXPORT
+uint32_t Audit_Log_Buffer_Size(uint32_t object_instance);
+BACNET_STACK_EXPORT
+bool Audit_Log_Buffer_Size_Set(uint32_t object_instance, uint32_t buffer_size);
+
+BACNET_STACK_EXPORT
+uint32_t Audit_Log_Record_Count(uint32_t object_instance);
+BACNET_STACK_EXPORT
+uint32_t Audit_Log_Total_Record_Count(uint32_t object_instance);
+
+BACNET_STACK_EXPORT
+void Audit_Log_Record_Status_Insert(
+ uint32_t instance, BACNET_LOG_STATUS eStatus, bool bState);
+BACNET_STACK_EXPORT
+void Audit_Log_Record_Notification_Insert(
+ uint32_t instance, BACNET_AUDIT_NOTIFICATION *notification);
+BACNET_STACK_EXPORT
+BACNET_AUDIT_LOG_RECORD *
+Audit_Log_Record_Entry(uint32_t object_instance, uint32_t index);
+BACNET_STACK_EXPORT
+void Audit_Log_Record_Entry_Delete(uint32_t object_instance, uint32_t index);
+BACNET_STACK_EXPORT
+bool Audit_Log_Record_Entry_Add(
+ uint32_t object_instance, const BACNET_AUDIT_LOG_RECORD *value);
+
+BACNET_STACK_EXPORT
+bool Audit_Log_Enable(uint32_t instance);
+BACNET_STACK_EXPORT
+bool Audit_Log_Enable_Set(uint32_t object_instance, bool enable);
+
+BACNET_STACK_EXPORT
+int Audit_Log_Read_Range_By_Position(BACNET_READ_RANGE_DATA *pRequest);
+BACNET_STACK_EXPORT
+int Audit_Log_Read_Range_By_Sequence(BACNET_READ_RANGE_DATA *pRequest);
+BACNET_STACK_EXPORT
+int Audit_Log_Read_Range_By_Time(BACNET_READ_RANGE_DATA *pRequest);
+BACNET_STACK_EXPORT
+int Audit_Log_Read_Range(uint8_t *apdu, BACNET_READ_RANGE_DATA *pRequest);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
diff --git a/src/bacnet/property.c b/src/bacnet/property.c
index 426a0a5f..d5571d06 100644
--- a/src/bacnet/property.c
+++ b/src/bacnet/property.c
@@ -2688,6 +2688,45 @@ static const int Trend_Log_Multiple_Properties_Optional[] = {
-1
};
+static const int Audit_Log_Properties_Required[] = {
+ /* unordered list of properties */
+ PROP_OBJECT_IDENTIFIER,
+ PROP_OBJECT_NAME,
+ PROP_OBJECT_TYPE,
+ PROP_STATUS_FLAGS,
+ PROP_EVENT_STATE,
+ PROP_ENABLE,
+ PROP_BUFFER_SIZE,
+ PROP_LOG_BUFFER,
+ PROP_RECORD_COUNT,
+ PROP_TOTAL_RECORD_COUNT,
+ -1
+};
+
+static const int Audit_Log_Properties_Optional[] = {
+ /* unordered list of properties */
+ PROP_DESCRIPTION,
+ PROP_RELIABILITY,
+ PROP_MEMBER_OF,
+ PROP_DELETE_ON_FORWARD,
+ PROP_ISSUE_CONFIRMED_NOTIFICATIONS,
+ PROP_EVENT_DETECTION_ENABLE,
+ PROP_NOTIFICATION_CLASS,
+ PROP_EVENT_ENABLE,
+ PROP_ACKED_TRANSITIONS,
+ PROP_NOTIFY_TYPE,
+ PROP_EVENT_TIME_STAMPS,
+ PROP_EVENT_MESSAGE_TEXTS,
+ PROP_EVENT_MESSAGE_TEXTS_CONFIG,
+ PROP_RELIABILITY_EVALUATION_INHIBIT,
+ PROP_AUDIT_LEVEL,
+ PROP_AUDITABLE_OPERATIONS,
+ PROP_TAGS,
+ PROP_PROFILE_LOCATION,
+ PROP_PROFILE_NAME,
+ -1
+};
+
/**
* Function that returns the list of all Optional properties
* of known standard objects.
@@ -2891,6 +2930,9 @@ const int *property_list_optional(BACNET_OBJECT_TYPE object_type)
case OBJECT_TREND_LOG_MULTIPLE:
pList = Trend_Log_Multiple_Properties_Optional;
break;
+ case OBJECT_AUDIT_LOG:
+ pList = Audit_Log_Properties_Optional;
+ break;
default:
pList = Default_Properties_Optional;
break;
@@ -3102,6 +3144,9 @@ const int *property_list_required(BACNET_OBJECT_TYPE object_type)
case OBJECT_TREND_LOG_MULTIPLE:
pList = Trend_Log_Multiple_Properties_Required;
break;
+ case OBJECT_AUDIT_LOG:
+ pList = Audit_Log_Properties_Required;
+ break;
default:
pList = Default_Properties_Required;
break;
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 292975f1..57986297 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -78,6 +78,7 @@ list(APPEND testdirs
bacnet/awf
bacnet/bacaddr
bacnet/bacapp
+ bacnet/bacaudit
bacnet/bacdcode
bacnet/bacdevobjpropref
bacnet/bacdest
@@ -143,6 +144,7 @@ list(APPEND testdirs
bacnet/basic/object/ai
bacnet/basic/object/ao
bacnet/basic/object/av
+ bacnet/basic/object/auditlog
bacnet/basic/object/bacfile
bacnet/basic/object/bi
bacnet/basic/object/bitstring_value
diff --git a/test/bacnet/bacaudit/CMakeLists.txt b/test/bacnet/bacaudit/CMakeLists.txt
new file mode 100644
index 00000000..99204cb6
--- /dev/null
+++ b/test/bacnet/bacaudit/CMakeLists.txt
@@ -0,0 +1,70 @@
+# 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-Z_/-]*$"
+ "/src"
+ SRC_DIR
+ ${CMAKE_CURRENT_SOURCE_DIR})
+string(REGEX REPLACE
+ "/test/bacnet/[a-zA-Z_/-]*$"
+ "/test"
+ TST_DIR
+ ${CMAKE_CURRENT_SOURCE_DIR})
+set(ZTST_DIR "${TST_DIR}/ztest/src")
+
+add_compile_definitions(
+ BIG_ENDIAN=0
+ CONFIG_ZTEST=1
+ BACAPP_MINIMAL=1
+ BACAPP_DESTINATION=1
+ )
+
+include_directories(
+ ${SRC_DIR}
+ ${TST_DIR}/ztest/include
+ )
+
+add_executable(${PROJECT_NAME}
+ # File(s) under test
+ ${SRC_DIR}/bacnet/bacaudit.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/bacerror.c
+ ${SRC_DIR}/bacnet/bacint.c
+ ${SRC_DIR}/bacnet/bacreal.c
+ ${SRC_DIR}/bacnet/bacstr.c
+ ${SRC_DIR}/bacnet/bactext.c
+ ${SRC_DIR}/bacnet/basic/sys/bigend.c
+ ${SRC_DIR}/bacnet/datetime.c
+ ${SRC_DIR}/bacnet/basic/sys/days.c
+ ${SRC_DIR}/bacnet/indtext.c
+ ${SRC_DIR}/bacnet/hostnport.c
+ ${SRC_DIR}/bacnet/lighting.c
+ ${SRC_DIR}/bacnet/timestamp.c
+ ${SRC_DIR}/bacnet/timesync.c
+ ${SRC_DIR}/bacnet/weeklyschedule.c
+ ${SRC_DIR}/bacnet/bactimevalue.c
+ ${SRC_DIR}/bacnet/dailyschedule.c
+ ${SRC_DIR}/bacnet/calendar_entry.c
+ ${SRC_DIR}/bacnet/special_event.c
+ ${SRC_DIR}/bacnet/channel_value.c
+ ${SRC_DIR}/bacnet/secure_connect.c
+ # Test and test library files
+ ./src/main.c
+ ${ZTST_DIR}/ztest_mock.c
+ ${ZTST_DIR}/ztest.c
+ )
diff --git a/test/bacnet/bacaudit/src/main.c b/test/bacnet/bacaudit/src/main.c
new file mode 100644
index 00000000..2c0bb173
--- /dev/null
+++ b/test/bacnet/bacaudit/src/main.c
@@ -0,0 +1,259 @@
+/**
+ * @file
+ * @brief Unit test for for BACnetAuditNotification and BACnetAuditLogRecord
+ * @author Steve Karg
+ * @date November 2024
+ * @copyright SPDX-License-Identifier: MIT
+ */
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * @addtogroup bacnet_tests
+ * @{
+ */
+
+#if defined(CONFIG_ZTEST_NEW_API)
+ZTEST(bacnet_audit_tests, test_bacnet_audit_value)
+#else
+static void test_bacnet_audit_value(void)
+#endif
+{
+ uint8_t apdu[MAX_APDU] = { 0 };
+ BACNET_AUDIT_VALUE value = { 0 }, test_value = { 0 };
+ int apdu_len = 0, null_len = 0, test_len = 0, tag_len = 0, value_len = 0;
+ uint8_t tag_number = 1;
+ bool status = false;
+
+ null_len = bacnet_audit_value_encode(NULL, &value);
+ apdu_len = bacnet_audit_value_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_value_decode(apdu, apdu_len, &value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_value_same(&value, &test_value), NULL);
+
+ /* decoding, some negative tests */
+ test_len = bacnet_audit_value_decode(NULL, apdu_len, &test_value);
+ zassert_equal(test_len, BACNET_STATUS_ERROR, NULL);
+ test_len = bacnet_audit_value_decode(apdu, 0, &test_value);
+ zassert_equal(test_len, BACNET_STATUS_ERROR, NULL);
+ test_len = bacnet_audit_value_decode(apdu, apdu_len, NULL);
+ zassert_equal(test_len, BACNET_STATUS_ERROR, NULL);
+ /* out of range tag */
+ value.tag = 255;
+ apdu_len = bacnet_audit_value_encode(apdu, &value);
+ test_len = bacnet_audit_value_decode(apdu, apdu_len, &value);
+ zassert_equal(test_len, BACNET_STATUS_ERROR, NULL);
+
+ /* value type = boolean */
+ value.tag = BACNET_APPLICATION_TAG_BOOLEAN;
+ value.type.boolean_value = true;
+ null_len = bacnet_audit_value_encode(NULL, &value);
+ apdu_len = bacnet_audit_value_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_value_same(&value, &test_value), NULL);
+
+ /* value type = unsigned */
+ value.tag = BACNET_APPLICATION_TAG_UNSIGNED_INT;
+ value.type.unsigned_value = 1234;
+ null_len = bacnet_audit_value_encode(NULL, &value);
+ apdu_len = bacnet_audit_value_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_value_same(&value, &test_value), NULL);
+
+ /* value type = signed */
+ value.tag = BACNET_APPLICATION_TAG_SIGNED_INT;
+ value.type.integer_value = -1234;
+ null_len = bacnet_audit_value_encode(NULL, &value);
+ apdu_len = bacnet_audit_value_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_value_same(&value, &test_value), NULL);
+
+ /* value type = REAL */
+ value.tag = BACNET_APPLICATION_TAG_REAL;
+ value.type.real_value = 3.14159f;
+ null_len = bacnet_audit_value_encode(NULL, &value);
+ apdu_len = bacnet_audit_value_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_value_same(&value, &test_value), NULL);
+
+ /* value type = ENUMERATED */
+ value.tag = BACNET_APPLICATION_TAG_ENUMERATED;
+ value.type.enumerated_value = 1234;
+ null_len = bacnet_audit_value_encode(NULL, &value);
+ apdu_len = bacnet_audit_value_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_value_decode(apdu, apdu_len, &test_value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_value_same(&value, &test_value), NULL);
+
+ /* negative tests */
+ null_len = bacnet_audit_value_encode(NULL, NULL);
+ zassert_equal(null_len, 0, NULL);
+ value.tag = 255;
+ null_len = bacnet_audit_value_encode(NULL, &value);
+ zassert_equal(null_len, 1, NULL);
+
+ /* context encoded */
+ value.tag = BACNET_APPLICATION_TAG_BOOLEAN;
+ value.type.boolean_value = true;
+ null_len = bacnet_audit_value_context_encode(NULL, tag_number, &value);
+ apdu_len = bacnet_audit_value_context_encode(apdu, tag_number, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = 0;
+ status = bacnet_is_opening_tag_number(apdu, apdu_len, tag_number, &tag_len);
+ zassert_true(status, NULL);
+ test_len += tag_len;
+ value_len = bacnet_audit_value_decode(
+ &apdu[test_len], apdu_len - test_len, &test_value);
+ test_len += value_len;
+ status = bacnet_is_closing_tag_number(
+ &apdu[test_len], apdu_len - test_len, tag_number, &tag_len);
+ zassert_true(status, NULL);
+ test_len += tag_len;
+ zassert_equal(apdu_len, test_len, NULL);
+}
+
+#if defined(CONFIG_ZTEST_NEW_API)
+ZTEST(bacnet_audit_tests, test_bacnet_audit_log_notification)
+#else
+static void test_bacnet_audit_log_notification(void)
+#endif
+{
+ uint8_t apdu[MAX_APDU] = { 0 };
+ BACNET_AUDIT_NOTIFICATION value = { 0 }, test_value = { 0 };
+ int apdu_len = 0, null_len = 0, test_len = 0;
+
+ null_len = bacnet_audit_log_notification_encode(NULL, &value);
+ apdu_len = bacnet_audit_log_notification_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_log_notification_decode(apdu, apdu_len, &value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_log_notification_same(&value, &test_value), NULL);
+
+ /* decoding, some negative tests */
+ test_len =
+ bacnet_audit_log_notification_decode(NULL, apdu_len, &test_value);
+ zassert_equal(test_len, BACNET_STATUS_ERROR, NULL);
+ test_len = bacnet_audit_log_notification_decode(apdu, 0, &test_value);
+ zassert_equal(test_len, BACNET_STATUS_ERROR, NULL);
+ test_len = bacnet_audit_log_notification_decode(apdu, apdu_len, NULL);
+ zassert_equal(test_len, apdu_len, NULL);
+}
+
+uint8_t Test_APDU[MAX_APDU];
+#if defined(CONFIG_ZTEST_NEW_API)
+ZTEST(bacnet_audit_tests, test_bacnet_audit_log_record)
+#else
+static void test_bacnet_audit_log_record(void)
+#endif
+{
+ uint8_t *apdu = Test_APDU;
+ BACNET_AUDIT_LOG_RECORD value = { 0 }, test_value = { 0 };
+ int apdu_len = 0, null_len = 0, test_len = 0;
+ bool status = false;
+ BACNET_AUDIT_NOTIFICATION *notification = NULL;
+
+ value.tag = AUDIT_LOG_DATUM_TAG_STATUS;
+ datetime_date_init_ascii(&value.timestamp.date, "2024/11/30");
+ datetime_time_init_ascii(&value.timestamp.time, "23:59:59.99");
+ null_len = bacnet_audit_log_record_encode(NULL, &value);
+ apdu_len = bacnet_audit_log_record_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_log_record_decode(apdu, apdu_len, &test_value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_log_record_same(&value, &test_value), NULL);
+
+ /* decoding, some negative tests */
+ test_len = bacnet_audit_log_record_decode(NULL, apdu_len, &test_value);
+ zassert_equal(test_len, BACNET_STATUS_ERROR, NULL);
+ test_len = bacnet_audit_log_record_decode(apdu, 0, &test_value);
+ zassert_equal(test_len, BACNET_STATUS_ERROR, NULL);
+ test_len = bacnet_audit_log_record_decode(apdu, apdu_len, NULL);
+ zassert_equal(test_len, apdu_len, NULL);
+ status = bacnet_audit_log_record_same(&value, NULL);
+ zassert_false(status, NULL);
+ status = bacnet_audit_log_record_same(NULL, &value);
+ zassert_false(status, NULL);
+ value.tag = 255;
+ test_value.tag = 255;
+ status = bacnet_audit_log_record_same(&value, &test_value);
+ zassert_false(status, NULL);
+
+ /* record type = notification */
+ value.tag = AUDIT_LOG_DATUM_TAG_NOTIFICATION;
+ notification = &value.log_datum.notification;
+
+ bacapp_timestamp_sequence_set(¬ification->source_timestamp, 1234);
+ bacapp_timestamp_sequence_set(¬ification->target_timestamp, 5678);
+ notification->source_device.tag = BACNET_RECIPIENT_TAG_DEVICE;
+ notification->source_device.type.device.instance = 1234;
+ notification->source_device.type.device.type = OBJECT_DEVICE;
+ notification->source_object.type = OBJECT_ANALOG_INPUT;
+ notification->source_object.instance = 5678;
+ notification->operation = AUDIT_OPERATION_DEVICE_RESET;
+ characterstring_init_ansi(¬ification->source_comment, "Hello, World!");
+ characterstring_init_ansi(¬ification->target_comment, "Goodbye, World!");
+ notification->invoke_id = 123;
+ notification->source_user_id = 456;
+ notification->source_user_role = 7;
+ notification->target_device.tag = BACNET_RECIPIENT_TAG_DEVICE;
+ notification->target_device.type.device.instance = 5678;
+ notification->target_device.type.device.type = OBJECT_DEVICE;
+ notification->target_object.type = OBJECT_ANALOG_INPUT;
+ notification->target_object.instance = 1234;
+ notification->target_property.property_identifier = PROP_PRESENT_VALUE;
+ notification->target_property.property_array_index = BACNET_ARRAY_ALL;
+ notification->target_priority = 8;
+ notification->target_value.tag = BACNET_APPLICATION_TAG_REAL;
+ notification->target_value.type.real_value = 3.14159f;
+ notification->current_value.tag = BACNET_APPLICATION_TAG_REAL;
+ notification->current_value.type.real_value = 2.71828f;
+ notification->result = ERROR_CODE_OTHER;
+ null_len = bacnet_audit_log_record_encode(NULL, &value);
+ apdu_len = bacnet_audit_log_record_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_log_record_decode(apdu, apdu_len, &test_value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_log_record_same(&value, &test_value), NULL);
+
+ /* record type = time-change */
+ value.tag = AUDIT_LOG_DATUM_TAG_TIME_CHANGE;
+ value.log_datum.time_change = 3.14159;
+ null_len = bacnet_audit_log_record_encode(NULL, &value);
+ apdu_len = bacnet_audit_log_record_encode(apdu, &value);
+ zassert_equal(apdu_len, null_len, NULL);
+ test_len = bacnet_audit_log_record_decode(apdu, apdu_len, &test_value);
+ zassert_equal(apdu_len, test_len, NULL);
+ zassert_true(bacnet_audit_log_record_same(&value, &test_value), NULL);
+}
+/**
+ * @}
+ */
+
+#if defined(CONFIG_ZTEST_NEW_API)
+ZTEST_SUITE(bacnet_audit_tests, NULL, NULL, NULL, NULL, NULL);
+#else
+void test_main(void)
+{
+ ztest_test_suite(
+ bacnet_audit_tests, ztest_unit_test(test_bacnet_audit_log_record),
+ ztest_unit_test(test_bacnet_audit_log_notification),
+ ztest_unit_test(test_bacnet_audit_value));
+
+ ztest_run_test_suite(bacnet_audit_tests);
+}
+#endif
diff --git a/test/bacnet/basic/object/auditlog/CMakeLists.txt b/test/bacnet/basic/object/auditlog/CMakeLists.txt
new file mode 100644
index 00000000..aad98616
--- /dev/null
+++ b/test/bacnet/basic/object/auditlog/CMakeLists.txt
@@ -0,0 +1,73 @@
+# 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-Z_/-]*$"
+ "/src"
+ SRC_DIR
+ ${CMAKE_CURRENT_SOURCE_DIR})
+string(REGEX REPLACE
+ "/test/bacnet/[a-zA-Z_/-]*$"
+ "/test"
+ TST_DIR
+ ${CMAKE_CURRENT_SOURCE_DIR})
+set(ZTST_DIR "${TST_DIR}/ztest/src")
+
+add_compile_definitions(
+ BIG_ENDIAN=0
+ CONFIG_ZTEST=1
+ BACAPP_MINIMAL=1
+ BACAPP_DATETIME=1
+ )
+
+include_directories(
+ ${SRC_DIR}
+ ${TST_DIR}/ztest/include
+ ${TST_DIR}/bacnet/basic/object/test
+ )
+
+add_executable(${PROJECT_NAME}
+ # File(s) under test
+ ${SRC_DIR}/bacnet/basic/object/auditlog.c
+ # Support files and stubs (pathname alphabetical)
+ ${SRC_DIR}/bacnet/bacaddr.c
+ ${SRC_DIR}/bacnet/bacaudit.c
+ ${SRC_DIR}/bacnet/bacapp.c
+ ${SRC_DIR}/bacnet/bacdcode.c
+ ${SRC_DIR}/bacnet/bacdest.c
+ ${SRC_DIR}/bacnet/bacdevobjpropref.c
+ ${SRC_DIR}/bacnet/bacerror.c
+ ${SRC_DIR}/bacnet/bacint.c
+ ${SRC_DIR}/bacnet/bacreal.c
+ ${SRC_DIR}/bacnet/bacstr.c
+ ${SRC_DIR}/bacnet/bactext.c
+ ${SRC_DIR}/bacnet/basic/sys/bigend.c
+ ${SRC_DIR}/bacnet/basic/sys/debug.c
+ ${SRC_DIR}/bacnet/basic/sys/keylist.c
+ ${SRC_DIR}/bacnet/basic/sys/days.c
+ ${SRC_DIR}/bacnet/datetime.c
+ ${SRC_DIR}/bacnet/dcc.c
+ ${SRC_DIR}/bacnet/indtext.c
+ ${SRC_DIR}/bacnet/hostnport.c
+ ${SRC_DIR}/bacnet/lighting.c
+ ${SRC_DIR}/bacnet/timestamp.c
+ ${SRC_DIR}/bacnet/wp.c
+ ${SRC_DIR}/bacnet/weeklyschedule.c
+ ${SRC_DIR}/bacnet/bactimevalue.c
+ ${SRC_DIR}/bacnet/dailyschedule.c
+ ${SRC_DIR}/bacnet/proplist.c
+ # Test and test library files
+ ./src/main.c
+ ${TST_DIR}/bacnet/basic/object/test/property_test.c
+ ${TST_DIR}/bacnet/basic/object/test/device_mock.c
+ ${TST_DIR}/bacnet/basic/object/test/datetime_local.c
+ ${ZTST_DIR}/ztest_mock.c
+ ${ZTST_DIR}/ztest.c
+ )
diff --git a/test/bacnet/basic/object/auditlog/src/main.c b/test/bacnet/basic/object/auditlog/src/main.c
new file mode 100644
index 00000000..f225de3d
--- /dev/null
+++ b/test/bacnet/basic/object/auditlog/src/main.c
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2023 Legrand North America, LLC.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/* @file
+ * @brief test BACnet auditlog APIs
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * @addtogroup bacnet_tests
+ * @{
+ */
+
+/**
+ * @brief Test Auditlog handling
+
+ */
+static void testAuditlog(void)
+{
+ int len = 0, index = 0;
+ const uint32_t instance = 1;
+ uint32_t test_instance = 0;
+ const int skip_fail_property_list[] = { -1 };
+ bool status = false;
+ BACNET_WRITE_PROPERTY_DATA wp_data = { 0 };
+ uint32_t buffer_size = 0;
+
+ Audit_Log_Init();
+ len = Audit_Log_Count();
+ zassert_true(len == 0, NULL);
+ status = Audit_Log_Valid_Instance(instance);
+ zassert_false(status, NULL);
+ test_instance = Audit_Log_Create(instance);
+ zassert_true(test_instance == instance, NULL);
+ len = Audit_Log_Count();
+ zassert_true(len == 1, NULL);
+ test_instance = Audit_Log_Index_To_Instance(0);
+ zassert_true(test_instance == instance, NULL);
+ index = Audit_Log_Instance_To_Index(instance);
+ zassert_true(index == 0, NULL);
+ status = Audit_Log_Valid_Instance(instance);
+ zassert_true(status, NULL);
+
+ /* configure ReadProperty test */
+ /* perform a general test for RP/WP */
+ bacnet_object_properties_read_write_test(
+ OBJECT_AUDIT_LOG, instance, Audit_Log_Property_Lists,
+ Audit_Log_Read_Property, Audit_Log_Write_Property,
+ skip_fail_property_list);
+
+ wp_data.object_type = OBJECT_AUDIT_LOG;
+ wp_data.object_instance = instance;
+ wp_data.priority = BACNET_NO_PRIORITY;
+ wp_data.array_index = BACNET_ARRAY_ALL;
+
+ /* test buffer size */
+ buffer_size = 512;
+ wp_data.object_property = PROP_BUFFER_SIZE;
+ wp_data.application_data_len =
+ encode_application_unsigned(wp_data.application_data, buffer_size);
+ status = Audit_Log_Write_Property(&wp_data);
+ zassert_true(status, NULL);
+ buffer_size = INT_MAX;
+ buffer_size++;
+ wp_data.application_data_len =
+ encode_application_unsigned(wp_data.application_data, buffer_size);
+ status = Audit_Log_Write_Property(&wp_data);
+ /* error out of range */
+ zassert_false(status, NULL);
+ zassert_equal(wp_data.error_class, ERROR_CLASS_PROPERTY, NULL);
+ zassert_equal(wp_data.error_code, ERROR_CODE_VALUE_OUT_OF_RANGE, NULL);
+
+ wp_data.object_property = PROP_ENABLE;
+ wp_data.application_data_len =
+ encode_application_boolean(wp_data.application_data, true);
+ status = Audit_Log_Write_Property(&wp_data);
+ zassert_true(status, NULL);
+
+ wp_data.object_property = PROP_BUFFER_SIZE;
+ wp_data.application_data_len =
+ encode_application_unsigned(wp_data.application_data, buffer_size);
+ status = Audit_Log_Write_Property(&wp_data);
+ /* error when enabled=true */
+ zassert_false(status, NULL);
+ zassert_equal(wp_data.error_class, ERROR_CLASS_PROPERTY, NULL);
+ zassert_equal(wp_data.error_code, ERROR_CODE_WRITE_ACCESS_DENIED, NULL);
+
+ /* test object name */
+ bacnet_object_name_ascii_test(
+ instance, Audit_Log_Name_Set, Audit_Log_Name_ASCII);
+ bacnet_object_name_ascii_test(
+ instance, Audit_Log_Description_Set, Audit_Log_Description);
+
+ status = Audit_Log_Delete(instance);
+ zassert_true(status, NULL);
+ Audit_Log_Cleanup();
+ len = Audit_Log_Count();
+ zassert_true(len == 0, NULL);
+ status = Audit_Log_Valid_Instance(instance);
+ zassert_false(status, NULL);
+}
+
+static void testLogs(void)
+{
+ const uint32_t instance = 1;
+ uint32_t test_instance = 0;
+ int len = 0;
+ uint32_t buffer_size = 0, test_buffer_size = 0, original_buffer_size = 0;
+ uint32_t record_count = 0;
+ uint32_t total_record_count = 0;
+ bool status = false;
+ BACNET_AUDIT_LOG_RECORD *record;
+
+ Audit_Log_Init();
+ test_instance = Audit_Log_Create(instance);
+ zassert_true(test_instance == instance, NULL);
+ len = Audit_Log_Count();
+ zassert_true(len > 0, NULL);
+ /* Log Buffer settings */
+ buffer_size = Audit_Log_Buffer_Size(instance);
+ zassert_true(buffer_size > 0, NULL);
+ buffer_size = INT_MAX;
+ buffer_size++;
+ status = Audit_Log_Buffer_Size_Set(instance, buffer_size);
+ zassert_false(status, NULL);
+ original_buffer_size = Audit_Log_Buffer_Size(instance);
+ buffer_size = original_buffer_size / 2;
+ status = Audit_Log_Buffer_Size_Set(instance, buffer_size);
+ zassert_true(status, NULL);
+ test_buffer_size = Audit_Log_Buffer_Size(instance);
+ zassert_true(test_buffer_size == buffer_size, NULL);
+ status = Audit_Log_Buffer_Size_Set(instance, original_buffer_size);
+ zassert_true(status, NULL);
+ /* Log Buffer records manipulation */
+ record_count = Audit_Log_Record_Count(instance);
+ zassert_true(record_count == 0, NULL);
+ total_record_count = Audit_Log_Total_Record_Count(instance);
+ zassert_true(total_record_count == 0, NULL);
+ status = Audit_Log_Enable(instance);
+ zassert_false(status, NULL);
+ status = Audit_Log_Enable_Set(instance, false);
+ zassert_true(status, NULL);
+ /* start logging */
+ status = Audit_Log_Enable_Set(instance, true);
+ zassert_true(status, NULL);
+ record_count = Audit_Log_Record_Count(instance);
+ zassert_true(record_count == 1, NULL);
+ total_record_count = Audit_Log_Total_Record_Count(instance);
+ zassert_true(total_record_count == 1, NULL);
+ record = Audit_Log_Record_Entry(instance, 0);
+ zassert_not_null(record, NULL);
+ zassert_true(record->tag == AUDIT_LOG_DATUM_TAG_STATUS, NULL);
+ zassert_true(
+ BIT_CHECK(record->log_datum.log_status, LOG_STATUS_LOG_DISABLED) == 0,
+ NULL);
+ Audit_Log_Record_Status_Insert(instance, LOG_STATUS_LOG_INTERRUPTED, true);
+ record_count = Audit_Log_Record_Count(instance);
+ zassert_true(record_count == 2, NULL);
+ total_record_count = Audit_Log_Total_Record_Count(instance);
+ zassert_true(total_record_count == 2, NULL);
+ record = Audit_Log_Record_Entry(instance, 1);
+ zassert_not_null(record, NULL);
+ zassert_true(record->tag == AUDIT_LOG_DATUM_TAG_STATUS, NULL);
+ zassert_true(
+ BIT_CHECK(record->log_datum.log_status, LOG_STATUS_LOG_INTERRUPTED),
+ NULL);
+ Audit_Log_Record_Entry_Delete(instance, 1);
+ record_count = Audit_Log_Record_Count(instance);
+ zassert_true(record_count == 1, "record_count: %d", record_count);
+ total_record_count = Audit_Log_Total_Record_Count(instance);
+ zassert_true(total_record_count == 2, NULL);
+ record = Audit_Log_Record_Entry(instance, 0);
+ zassert_not_null(record, NULL);
+ zassert_true(record->tag == AUDIT_LOG_DATUM_TAG_STATUS, NULL);
+ zassert_true(
+ BIT_CHECK(record->log_datum.log_status, LOG_STATUS_LOG_DISABLED) == 0,
+ NULL);
+
+ Audit_Log_Cleanup();
+}
+/**
+ * @}
+ */
+
+void test_main(void)
+{
+ ztest_test_suite(
+ auditlog_tests, ztest_unit_test(testAuditlog),
+ ztest_unit_test(testLogs));
+
+ ztest_run_test_suite(auditlog_tests);
+}