diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2d606b..1b0475c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ The git repositories are hosted at the following sites: * https://bacnet.sourceforge.net/ * https://github.com/bacnet-stack/bacnet-stack/ -## [Unreleased] - 2026-01-22 +## [Unreleased] - 2026-01-28 ### Security @@ -32,6 +32,9 @@ The git repositories are hosted at the following sites: ### Added +* Added channel and timer object write-property observers in blinkt app + to monitor internal writes. Added vacancy timer command line argument + for testing initial timer object vacancy time for lights channel. (#1212) * Added debug prints for lighting output properties to assist in identifying out-of-range values. (#1211) * Added API to get the RGB pixel and brightness values from the blinkt @@ -117,6 +120,9 @@ The git repositories are hosted at the following sites: ### Fixed +* Fixed lighting output object lighting-commands for warn-off and + warn-relinquish when blink-warn notification shall not occur. (#1212) +* Fixed timer object task to initiate a write-request at expiration. (#1212) * Fixed the server name in the blinkt app and removed the unnecessary device.c module. (#1211) * Fixed Channel object for Color object present-value which does not diff --git a/apps/blinkt/main.c b/apps/blinkt/main.c index 2787ade7..abf71795 100644 --- a/apps/blinkt/main.c +++ b/apps/blinkt/main.c @@ -52,11 +52,17 @@ static struct mstimer Blinkt_Task; static bool Blinkt_Test = false; /* observer for WriteGroup notifications */ static BACNET_WRITE_GROUP_NOTIFICATION Write_Group_Notification; +/* observers for internal object to object writes */ +static struct channel_write_property_notification + Channel_Write_Property_Observer; +static struct timer_write_property_notification Timer_Write_Property_Observer; + /* object instances */ static uint32_t Light_Channel_Instance = 1; static uint32_t Color_Channel_Instance = 2; static uint32_t CCT_Channel_Instance = 3; static uint32_t Vacancy_Timer_Instance = 1; +static uint32_t Vacancy_Timeout_Milliseconds = 30UL * 60UL * 1000UL; static unsigned Default_Priority = 16; /** * Clean up the Blinkt! interface @@ -66,6 +72,71 @@ static void blinkt_cleanup(void) blinkt_stop(); } +/** + * @brief Log internal object to object WriteProperty calls + * @param object_type The object type being written to + * @param instance The object instance being written to + * @param status The status of the WriteProperty + * @param wp_data The WriteProperty data + */ +static void write_property_observer( + BACNET_OBJECT_TYPE object_type, + uint32_t instance, + bool status, + BACNET_WRITE_PROPERTY_DATA *wp_data) +{ + unsigned i; + char value_string[64 + 1] = { 0 }; + + if (status) { + printf( + "WriteProperty: %s-%d to %s-%u %s@%u\n", + bactext_object_type_name(object_type), instance, + bactext_object_type_name(wp_data->object_type), + wp_data->object_instance, + bactext_property_name(wp_data->object_property), wp_data->priority); + } else { + for (i = 0; i < wp_data->application_data_len && + (i * 2) < sizeof(value_string) - 1; + i++) { + snprintf( + &value_string[i * 2], 3, "%02X", wp_data->application_data[i]); + } + printf( + "WriteProperty: %s-%d to %s-%u %s@%u %s %s-%s\n", + bactext_object_type_name(object_type), instance, + bactext_object_type_name(wp_data->object_type), + wp_data->object_instance, + bactext_property_name(wp_data->object_property), wp_data->priority, + value_string, bactext_error_class_name(wp_data->error_class), + bactext_error_code_name(wp_data->error_code)); + } +} + +/** + * @brief Log internal object to object WriteProperty calls + * @param instance The object instance being written to + * @param status The status of the WriteProperty + * @param wp_data The WriteProperty data + */ +static void channel_write_property_observer( + uint32_t instance, bool status, BACNET_WRITE_PROPERTY_DATA *wp_data) +{ + write_property_observer(OBJECT_CHANNEL, instance, status, wp_data); +} + +/** + * @brief Log internal object to object WriteProperty calls + * @param instance The object instance being written to + * @param status The status of the WriteProperty + * @param wp_data The WriteProperty data + */ +static void timer_write_property_observer( + uint32_t instance, bool status, BACNET_WRITE_PROPERTY_DATA *wp_data) +{ + write_property_observer(OBJECT_TIMER, instance, status, wp_data); +} + /** * @brief Callback for tracking value * @param object_instance - object-instance number of the object @@ -184,7 +255,11 @@ static void BACnet_Object_Table_Init(void *context) /* timer to automatically turn off the lights */ Timer_Create(Vacancy_Timer_Instance); Timer_Name_Set(Vacancy_Timer_Instance, "Vacancy-Timer"); - Timer_Default_Timeout_Set(Vacancy_Timer_Instance, 30UL * 60UL * 1000UL); + Timer_Default_Timeout_Set( + Vacancy_Timer_Instance, Vacancy_Timeout_Milliseconds); + printf( + "Vacancy timeout: %lu milliseconds\n", + (unsigned long)Vacancy_Timeout_Milliseconds); /* to running */ timer_transition.next = NULL; timer_transition.tag = BACNET_APPLICATION_TAG_REAL; @@ -205,6 +280,7 @@ static void BACnet_Object_Table_Init(void *context) Timer_State_Change_Value_Set( Vacancy_Timer_Instance, TIMER_TRANSITION_RUNNING_TO_EXPIRED, &timer_transition); + /* timer members */ member.objectIdentifier.type = OBJECT_CHANNEL; member.objectIdentifier.instance = Light_Channel_Instance; member.propertyIdentifier = PROP_PRESENT_VALUE; @@ -281,6 +357,11 @@ static void BACnet_Object_Table_Init(void *context) Color_Temperature_Write_Value_Handler); Lighting_Output_Write_Present_Value_Callback_Set( Lighting_Output_Write_Value_Handler); + /* set the observer callbacks. log the internal object-to-object writes */ + Channel_Write_Property_Observer.callback = channel_write_property_observer; + Channel_Write_Property_Notification_Add(&Channel_Write_Property_Observer); + Timer_Write_Property_Observer.callback = timer_write_property_observer; + Timer_Write_Property_Notification_Add(&Timer_Write_Property_Observer); Write_Group_Notification.callback = Channel_Write_Group; handler_write_group_notification_add(&Write_Group_Notification); /* LEDs run at 0.1s intervals */ @@ -345,7 +426,7 @@ static void BACnet_Object_Task(void *context) static void print_usage(const char *filename) { printf("Usage: %s [device-instance]\n", filename); - printf(" [--device N][--test]\n"); + printf(" [--device N][--test][--color COLOR][--vacancy MS]\n"); printf(" [--version][--help]\n"); } @@ -367,6 +448,9 @@ static void print_help(const char *filename) "--color:\n" "Default CSS color name from W3C, such as black, red, green, etc.\n"); printf("\n"); + printf("--vacancy:\n" + "Vacancy timeout in milliseconds.\n"); + printf("\n"); printf("--test:\n" "Test the Blinkt! RGB LEDs with a cycling pattern.\n"); printf("\n"); @@ -384,7 +468,7 @@ static void print_help(const char *filename) int main(int argc, char *argv[]) { unsigned int target_args = 0; - uint32_t device_id = BACNET_MAX_INSTANCE; + uint32_t device_id = BACNET_MAX_INSTANCE, vacancy_timeout = 0; int argi = 0; const char *filename = NULL; const char *color_name = "darkred"; @@ -431,6 +515,15 @@ int main(int argc, char *argv[]) print_usage(filename); return 1; } + } else if (strcmp(argv[argi], "--vacancy") == 0) { + /* allow the device ID to be set */ + if (++argi < argc) { + if (!bacnet_string_to_uint32(argv[argi], &vacancy_timeout)) { + fprintf(stderr, "vacancy=%s invalid\n", argv[argi]); + return 1; + } + Vacancy_Timeout_Milliseconds = vacancy_timeout; + } } else { if (target_args == 0) { /* allow the device ID to be set */ diff --git a/src/bacnet/basic/object/lo.c b/src/bacnet/basic/object/lo.c index 1dd31c01..a3ccb383 100644 --- a/src/bacnet/basic/object/lo.c +++ b/src/bacnet/basic/object/lo.c @@ -498,110 +498,6 @@ unsigned Lighting_Output_Present_Value_Priority(uint32_t object_instance) return priority; } -/** - * @brief Set the lighting command if the priority is active - * @param object [in] BACnet object instance - * @param priority [in] BACnet priority array value 1..16 - */ -static void -Lighting_Command_Warn(struct object_data *pObject, unsigned priority) -{ - unsigned current_priority; - - if (!pObject) { - return; - } - current_priority = Present_Value_Priority(pObject); - if ((priority <= current_priority) && - (Priority_Array_Active(pObject, priority - 1)) && - (!is_float_equal(Priority_Array_Value(pObject, priority - 1), 0.0)) && - pObject->Blink_Warn_Enable) { - /* The blink-warn notification shall not occur - if any of the following conditions occur: - (a) The specified priority is not the highest - active priority, or - (b) The value at the specified priority is 0.0%, or - (c) Blink_Warn_Enable is FALSE. */ - lighting_command_blink_warn( - &pObject->Lighting_Command, BACNET_LIGHTS_WARN, - &pObject->Lighting_Command.Blink); - } -} - -/** - * @brief Set the lighting command if the priority is active - * @param object [in] BACnet object instance - * @param priority [in] BACnet priority array value 1..16 - */ -static void -Lighting_Command_Warn_Off(struct object_data *pObject, unsigned priority) -{ - unsigned current_priority; - - if (!pObject) { - return; - } - current_priority = Present_Value_Priority(pObject); - if ((priority <= current_priority) && - (Priority_Array_Active(pObject, priority - 1)) && - (!is_float_equal(Priority_Array_Value(pObject, priority - 1), 0.0)) && - (is_float_equal( - Priority_Array_Next_Value(pObject, priority - 1), 0.0)) && - pObject->Blink_Warn_Enable) { - /* The blink-warn notification shall not occur and - the value 0.0% written at the specified - priority immediately if any of the following - conditions occur: - (a) The specified priority is not the highest - active priority, or - (b) The Present_Value is 0.0%, or - (c) Blink_Warn_Enable is FALSE. */ - pObject->Lighting_Command.Blink.Duration = - pObject->Egress_Time_Seconds * 1000UL; - lighting_command_blink_warn( - &pObject->Lighting_Command, BACNET_LIGHTS_WARN_OFF, - &pObject->Lighting_Command.Blink); - } - Present_Value_Set(pObject, 0.0, priority); -} - -/** - * @brief Set the lighting command if the priority is active - * @param object [in] BACnet object instance - * @param priority [in] BACnet priority array value 1..16 - */ -static void -Lighting_Command_Warn_Relinquish(struct object_data *pObject, unsigned priority) -{ - unsigned current_priority; - - if (!pObject) { - return; - } - current_priority = Present_Value_Priority(pObject); - if ((priority <= current_priority) && - (Priority_Array_Active(pObject, priority - 1)) && - (!is_float_equal(Priority_Array_Value(pObject, priority - 1), 0.0)) && - (is_float_equal( - Priority_Array_Next_Value(pObject, priority - 1), 0.0)) && - pObject->Blink_Warn_Enable) { - /* The blink-warn notification shall not occur, - and the value at the specified priority shall be - relinquished immediately if any of the following - conditions occur: - (a) Blink_Warn_Enable is FALSE, or - (b) The Present_Value is 0.0%, or - (c) The Present_Value would not evaluate to 0.0% after - the priority slot is relinquished. */ - pObject->Lighting_Command.Blink.Duration = - pObject->Egress_Time_Seconds * 1000UL; - lighting_command_blink_warn( - &pObject->Lighting_Command, BACNET_LIGHTS_WARN_RELINQUISH, - &pObject->Lighting_Command.Blink); - } - Present_Value_Relinquish(pObject, priority); -} - /** * @brief Set the lighting command if the priority is active * @param object [in] BACnet object instance @@ -654,6 +550,182 @@ static void Lighting_Command_Ramp_To( } } +/** + * @brief Set the lighting command using default values when the priority + * is active + * @param object [in] BACnet object instance + * @param priority [in] BACnet priority array value 1..16 + * @param value - floating point analog value 0.0%, 1.0%-100.0% + */ +static void Lighting_Command_Transition_Default( + struct object_data *pObject, unsigned priority, float value) +{ + unsigned current_priority; + + if (!pObject) { + return; + } + current_priority = Present_Value_Priority(pObject); + if (priority <= current_priority) { + /* we have priority - configure the Lighting Command */ + if (pObject->Transition == BACNET_LIGHTING_TRANSITION_FADE) { + Lighting_Command_Fade_To( + pObject, priority, value, pObject->Default_Fade_Time); + } else if (pObject->Transition == BACNET_LIGHTING_TRANSITION_RAMP) { + Lighting_Command_Ramp_To( + pObject, priority, value, pObject->Default_Ramp_Rate); + } else { + Lighting_Command_Fade_To(pObject, priority, value, 0); + } + } +} + +/** + * @brief Set the lighting command if the priority is active + * @param object [in] BACnet object instance + * @param priority [in] BACnet priority array value 1..16 + */ +static void +Lighting_Command_Warn(struct object_data *pObject, unsigned priority) +{ + unsigned current_priority; + + if (!pObject) { + return; + } + current_priority = Present_Value_Priority(pObject); + if ((priority <= current_priority) && + (Priority_Array_Active(pObject, priority - 1)) && + (!is_float_equal(Priority_Array_Value(pObject, priority - 1), 0.0)) && + pObject->Blink_Warn_Enable) { + /* The blink-warn notification shall not occur + if any of the following conditions occur: + (a) The specified priority is not the highest + active priority, or + (b) The value at the specified priority is 0.0%, or + (c) Blink_Warn_Enable is FALSE. */ + lighting_command_blink_warn( + &pObject->Lighting_Command, BACNET_LIGHTS_WARN, + &pObject->Lighting_Command.Blink); + } +} + +/** + * @brief Set the lighting command if the priority is active + * @param object [in] BACnet object instance + * @param priority [in] BACnet priority array value 1..16 + */ +static void +Lighting_Command_Warn_Off(struct object_data *pObject, unsigned priority) +{ + unsigned current_priority; + + if (!pObject) { + return; + } + current_priority = Present_Value_Priority(pObject); + if (priority <= current_priority) { + if ((Priority_Array_Active(pObject, priority - 1)) && + (!is_float_equal( + Priority_Array_Value(pObject, priority - 1), 0.0)) && + pObject->Blink_Warn_Enable) { + /* The blink-warn notification shall not occur and + the value 0.0% written at the specified + priority immediately if any of the following + conditions occur: + (a) The specified priority is not the highest + active priority, or + (b) The Present_Value is 0.0%, or + (c) Blink_Warn_Enable is FALSE. */ + pObject->Lighting_Command.Blink.Duration = + pObject->Egress_Time_Seconds * 1000UL; + lighting_command_blink_warn( + &pObject->Lighting_Command, BACNET_LIGHTS_WARN_OFF, + &pObject->Lighting_Command.Blink); + /* FIXME: writes the value 0.0% to the specified slot + in the priority array after a delay of Egress_Time seconds. */ + Present_Value_Set(pObject, 0.0, priority); + } else { + /* the value 0.0% written at the specified priority immediately */ + Present_Value_Set(pObject, 0.0, priority); + Lighting_Command_Transition_Default(pObject, priority, 0.0); + } + } else { + Present_Value_Set(pObject, 0.0, priority); + } +} + +/** + * @brief Set the lighting command if the priority is active + * @param object [in] BACnet object instance + * @param priority [in] BACnet priority array value 1..16 + */ +static void +Lighting_Command_Relinquish(struct object_data *pObject, unsigned priority) +{ + bool status = false; + uint8_t old_priority, new_priority; + float value; + + if (pObject) { + old_priority = Present_Value_Priority(pObject); + status = Present_Value_Relinquish(pObject, priority); + new_priority = Present_Value_Priority(pObject); + if (status && (old_priority != new_priority)) { + value = Priority_Array_Next_Value(pObject, 0); + /* we have priority - configure the Lighting Command */ + Lighting_Command_Transition_Default(pObject, new_priority, value); + } + } +} + +/** + * @brief Set the lighting command if the priority is active + * @param object [in] BACnet object instance + * @param priority [in] BACnet priority array value 1..16 + */ +static void +Lighting_Command_Warn_Relinquish(struct object_data *pObject, unsigned priority) +{ + uint8_t current_priority; + + if (!pObject) { + return; + } + current_priority = Present_Value_Priority(pObject); + if (priority <= current_priority) { + if ((Priority_Array_Active(pObject, priority - 1)) && + (!is_float_equal( + Priority_Array_Value(pObject, priority - 1), 0.0)) && + (is_float_equal( + Priority_Array_Next_Value(pObject, priority - 1), 0.0)) && + pObject->Blink_Warn_Enable) { + /* The blink-warn notification shall not occur, + and the value at the specified priority shall be + relinquished immediately if any of the following + conditions occur: + (a) Blink_Warn_Enable is FALSE, or + (b) The Present_Value is 0.0%, or + (c) The Present_Value would not evaluate to 0.0% after + the priority slot is relinquished. */ + pObject->Lighting_Command.Blink.Duration = + pObject->Egress_Time_Seconds * 1000UL; + lighting_command_blink_warn( + &pObject->Lighting_Command, BACNET_LIGHTS_WARN_RELINQUISH, + &pObject->Lighting_Command.Blink); + /* FIXME: relinquishes the value at the specified priority slot + after a delay of Egress_Time seconds.*/ + Present_Value_Relinquish(pObject, priority); + } else { + /* the value at the specified priority shall be + relinquished immediately */ + Lighting_Command_Relinquish(pObject, priority); + } + } else { + Present_Value_Relinquish(pObject, priority); + } +} + /** * @brief Set the lighting command if the priority is active * @details Commands Present_Value to a value equal to the @@ -764,36 +836,6 @@ static void Lighting_Command_Step_Down_Off( } } -/** - * @brief Set the lighting command using default values when the priority - * is active - * @param object [in] BACnet object instance - * @param priority [in] BACnet priority array value 1..16 - * @param value - floating point analog value 0.0%, 1.0%-100.0% - */ -static void Lighting_Command_Transition_Default( - struct object_data *pObject, unsigned priority, float value) -{ - unsigned current_priority; - - if (!pObject) { - return; - } - current_priority = Present_Value_Priority(pObject); - if (priority <= current_priority) { - /* we have priority - configure the Lighting Command */ - if (pObject->Transition == BACNET_LIGHTING_TRANSITION_FADE) { - Lighting_Command_Fade_To( - pObject, priority, value, pObject->Default_Fade_Time); - } else if (pObject->Transition == BACNET_LIGHTING_TRANSITION_RAMP) { - Lighting_Command_Ramp_To( - pObject, priority, value, pObject->Default_Ramp_Rate); - } else { - Lighting_Command_Fade_To(pObject, priority, value, 0); - } - } -} - #if (BACNET_PROTOCOL_REVISION >= 28) /** * @brief Set the lighting command if the priority is active @@ -1094,27 +1136,18 @@ float Lighting_Output_Priority_Array_Value( * @param object_instance - object-instance number of the object * @param priority - priority 1..16 * - * @return true if priority is within range and priority-array slot is - * relinquished. + * @return true if the object exists */ bool Lighting_Output_Present_Value_Relinquish( uint32_t object_instance, unsigned priority) { bool status = false; struct object_data *pObject; - uint8_t old_priority, new_priority; - float value; pObject = Keylist_Data(Object_List, object_instance); if (pObject) { - old_priority = Present_Value_Priority(pObject); - status = Present_Value_Relinquish(pObject, priority); - new_priority = Present_Value_Priority(pObject); - if (status && (old_priority != new_priority)) { - value = Priority_Array_Next_Value(pObject, 0); - /* we have priority - configure the Lighting Command */ - Lighting_Command_Transition_Default(pObject, new_priority, value); - } + status = true; + Lighting_Command_Relinquish(pObject, priority); } return status; diff --git a/src/bacnet/basic/object/timer.c b/src/bacnet/basic/object/timer.c index 8c179e9d..b2ef5df9 100644 --- a/src/bacnet/basic/object/timer.c +++ b/src/bacnet/basic/object/timer.c @@ -2190,6 +2190,10 @@ void Timer_Task(uint32_t object_instance, uint16_t milliseconds) pObject->Timer_State = TIMER_STATE_EXPIRED; pObject->Last_State_Change = TIMER_TRANSITION_RUNNING_TO_EXPIRED; + datetime_local( + &pObject->Update_Time.date, &pObject->Update_Time.time, + NULL, NULL); + Timer_Write_Request_Initiate(object_instance, pObject); } break; case TIMER_STATE_EXPIRED: