From 59218819c13c9ee5973fdd3e856596b0a043df45 Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 29 Jan 2026 16:03:55 -0600 Subject: [PATCH] Added end-of-blink callback for warn-off and warn-relinquish lighting commands (#1214) * Added Blink.Callback and Blink.Priority to support end-of-blink actions for WARN_OFF/WARN_RELINQUISH. The Blink.Callback is invoked when a blink sequence ends or when a new command interrupts a WARN_OFF/WARN_RELINQUISH in progress. * Added Lighting Output (LO) WARN_OFF/WARN_RELINQUISH post-egress priority array handling using the Blink.Callback --- CHANGELOG.md | 3 + src/bacnet/basic/object/lo.c | 65 +++++++-- src/bacnet/basic/sys/lighting_command.c | 64 ++++++++- src/bacnet/basic/sys/lighting_command.h | 12 ++ .../basic/sys/lighting_command/src/main.c | 136 +++++++++++------- 5 files changed, 216 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b0475c7..3f0d48ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,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 an update at the specified priority slot + shall occur after an egress time delay. (#1214) * 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) diff --git a/src/bacnet/basic/object/lo.c b/src/bacnet/basic/object/lo.c index a3ccb383..85d5e739 100644 --- a/src/bacnet/basic/object/lo.c +++ b/src/bacnet/basic/object/lo.c @@ -610,6 +610,42 @@ Lighting_Command_Warn(struct object_data *pObject, unsigned priority) } } +/** + * @brief Callback that manipulates the value at the specified priority slot + after a delay of Egress_Time seconds. + * @param object_instance object-instance number of the object + * @param operation BACnet lighting operation + * @param priority BACnet priority array value 1..16 + */ +static void Lighting_Command_Blink_Stop( + uint32_t object_instance, + BACNET_LIGHTING_OPERATION operation, + uint8_t priority) +{ + struct object_data *pObject; + + switch (operation) { + case BACNET_LIGHTS_WARN_OFF: + /* writes the value 0.0% to the specified priority slot + after a delay of Egress_Time seconds. */ + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)Present_Value_Set(pObject, 0.0, priority); + } + break; + case BACNET_LIGHTS_WARN_RELINQUISH: + /* relinquishes the value at the specified priority slot + after a delay of Egress_Time seconds. */ + pObject = Keylist_Data(Object_List, object_instance); + if (pObject) { + (void)Present_Value_Relinquish(pObject, priority); + } + break; + default: + break; + } +} + /** * @brief Set the lighting command if the priority is active * @param object [in] BACnet object instance @@ -619,6 +655,7 @@ static void Lighting_Command_Warn_Off(struct object_data *pObject, unsigned priority) { unsigned current_priority; + BACNET_LIGHTING_COMMAND_WARN_DATA blink; if (!pObject) { return; @@ -637,14 +674,14 @@ Lighting_Command_Warn_Off(struct object_data *pObject, unsigned priority) 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; + memmove( + &blink, &pObject->Lighting_Command.Blink, + sizeof(BACNET_LIGHTING_COMMAND_WARN_DATA)); + blink.Duration = pObject->Egress_Time_Seconds * 1000UL; + blink.Priority = priority; + blink.Callback = Lighting_Command_Blink_Stop; 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); + &pObject->Lighting_Command, BACNET_LIGHTS_WARN_OFF, &blink); } else { /* the value 0.0% written at the specified priority immediately */ Present_Value_Set(pObject, 0.0, priority); @@ -688,6 +725,7 @@ static void Lighting_Command_Warn_Relinquish(struct object_data *pObject, unsigned priority) { uint8_t current_priority; + BACNET_LIGHTING_COMMAND_WARN_DATA blink; if (!pObject) { return; @@ -708,14 +746,15 @@ Lighting_Command_Warn_Relinquish(struct object_data *pObject, unsigned priority) (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; + memmove( + &blink, &pObject->Lighting_Command.Blink, + sizeof(BACNET_LIGHTING_COMMAND_WARN_DATA)); + blink.Duration = pObject->Egress_Time_Seconds * 1000UL; + blink.Priority = priority; + blink.Callback = Lighting_Command_Blink_Stop; 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); + &blink); } else { /* the value at the specified priority shall be relinquished immediately */ diff --git a/src/bacnet/basic/sys/lighting_command.c b/src/bacnet/basic/sys/lighting_command.c index 85110ba3..a160897b 100644 --- a/src/bacnet/basic/sys/lighting_command.c +++ b/src/bacnet/basic/sys/lighting_command.c @@ -63,8 +63,7 @@ void lighting_command_notification_add( /** * @brief call the lighting command tracking value callbacks * @param data - dimmer data structure - * @param old_value - value prior to write - * @param value - value of the write + * @param milliseconds - number of elapsed milliseconds since the last call */ static void lighting_command_timer_notify( struct bacnet_lighting_command_data *data, uint16_t milliseconds) @@ -105,6 +104,31 @@ void lighting_command_timer_notfication_add( } while (head); } +/** + * @brief call the lighting command blink stop callback + * for WARN_OFF/WARN_RELINQUISH operations + * @param data - dimmer data structure + */ +static void +lighting_command_blink_stop_notify(struct bacnet_lighting_command_data *data) +{ + if (data->Blink.Callback) { + /* do some checking to avoid extra callbacks */ + switch (data->Lighting_Operation) { + case BACNET_LIGHTS_WARN_OFF: + case BACNET_LIGHTS_WARN_RELINQUISH: + if (data->Blink.Priority != 0) { + data->Blink.Callback( + data->Key, data->Lighting_Operation, + data->Blink.Priority); + } + break; + default: + break; + } + } +} + /** * @brief Clamps the ramp rate value * @param ramp_rate [in] ramp rate value @@ -570,6 +594,7 @@ static void lighting_command_blink_handler( } if (data->Blink.Duration == 0) { /* 'end' operation */ + lighting_command_blink_stop_notify(data); data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; target_value = data->Blink.End_Value; @@ -601,6 +626,7 @@ static void lighting_command_blink_handler( } if (data->Blink.Count == 0) { /* 'end' operation */ + lighting_command_blink_stop_notify(data); data->In_Progress = BACNET_LIGHTING_IDLE; data->Lighting_Operation = BACNET_LIGHTS_STOP; target_value = data->Blink.End_Value; @@ -718,6 +744,9 @@ void lighting_command_fade_to( if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_FADE_TO; data->Target_Level = value; @@ -739,6 +768,9 @@ void lighting_command_ramp_to( if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ data->Ramp_Rate = lighting_command_ramp_rate_clamp(ramp_rate); data->Lighting_Operation = BACNET_LIGHTS_RAMP_TO; data->Target_Level = value; @@ -764,6 +796,9 @@ void lighting_command_step( if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ if (((operation == BACNET_LIGHTS_STEP_UP) || (operation == BACNET_LIGHTS_STEP_DOWN)) && (!islessgreater(data->Tracking_Value, 0.0))) { @@ -818,9 +853,14 @@ void lighting_command_blink_warn( if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the new warning */ data->Lighting_Operation = operation; data->Blink.Target_Interval = blink->Interval; data->Blink.Duration = blink->Duration; + data->Blink.Priority = blink->Priority; + data->Blink.Callback = blink->Callback; data->Blink.Count = blink->Count; data->Blink.On_Value = blink->On_Value; data->Blink.Off_Value = blink->Off_Value; @@ -842,6 +882,9 @@ void lighting_command_stop(struct bacnet_lighting_command_data *data) if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ data->Lighting_Operation = BACNET_LIGHTS_STOP; if (isgreaterequal(data->Tracking_Value, 1.0)) { /* the last value that was greater than or equal to 1.0%.*/ @@ -859,6 +902,9 @@ void lighting_command_none(struct bacnet_lighting_command_data *data) if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ data->Lighting_Operation = BACNET_LIGHTS_NONE; } @@ -873,6 +919,9 @@ void lighting_command_restore_on( if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_RESTORE_ON; data->Target_Level = data->Last_On_Value; @@ -889,6 +938,9 @@ void lighting_command_default_on( if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_DEFAULT_ON; data->Target_Level = data->Default_On_Value; @@ -905,6 +957,9 @@ void lighting_command_toggle_restore( if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_TOGGLE_RESTORE; if (isless(data->Tracking_Value, 1.0f)) { @@ -927,6 +982,9 @@ void lighting_command_toggle_default( if (!data) { return; } + /* possibly interrupting a blink warn, so notify */ + lighting_command_blink_stop_notify(data); + /* configure the lighting operation */ data->Fade_Time = fade_time; data->Lighting_Operation = BACNET_LIGHTS_TOGGLE_DEFAULT; if (isless(data->Tracking_Value, 1.0f)) { @@ -962,6 +1020,8 @@ void lighting_command_init(struct bacnet_lighting_command_data *data) data->Blink.Interval = 0; data->Blink.Duration = 0; data->Blink.State = false; + data->Blink.Callback = NULL; + data->Blink.State = false; data->Notification_Head.next = NULL; data->Notification_Head.callback = NULL; } diff --git a/src/bacnet/basic/sys/lighting_command.h b/src/bacnet/basic/sys/lighting_command.h index 12719bb5..d61b508c 100644 --- a/src/bacnet/basic/sys/lighting_command.h +++ b/src/bacnet/basic/sys/lighting_command.h @@ -41,11 +41,23 @@ struct lighting_command_timer_notification { lighting_command_timer_callback callback; }; +/** + * @brief Callback that manipulates the value at the specified priority slot + after a delay of Egress_Time seconds. + * @param object_instance object-instance number of the object + * @param operation BACnet lighting operation + * @param priority BACnet priority array value 1..16 + */ +typedef void (*lighting_command_blink_callback)( + uint32_t key, BACNET_LIGHTING_OPERATION operation, uint8_t priority); + typedef struct bacnet_lighting_command_warn_data { /* warn */ float On_Value; float Off_Value; float End_Value; + uint8_t Priority; + lighting_command_blink_callback Callback; uint16_t Target_Interval; /* internal tracking */ uint16_t Interval; diff --git a/test/bacnet/basic/sys/lighting_command/src/main.c b/test/bacnet/basic/sys/lighting_command/src/main.c index 778ecaf4..e5e584f9 100644 --- a/test/bacnet/basic/sys/lighting_command/src/main.c +++ b/test/bacnet/basic/sys/lighting_command/src/main.c @@ -61,6 +61,25 @@ static bool is_float_equal(float x1, float x2) return fabs(x1 - x2) < 0.001; } +static uint32_t Test_Blink_End_Key; +static BACNET_LIGHTING_OPERATION Test_Blink_End_Operation; +static uint8_t Test_Blink_End_Priority; + +/** + * @brief Callback that manipulates the value at the specified priority slot + after a delay of Egress_Time seconds. + * @param object_instance object-instance number of the object + * @param operation BACnet lighting operation + * @param priority BACnet priority array value 1..16 + */ +static void test_blink_end( + uint32_t key, BACNET_LIGHTING_OPERATION operation, uint8_t priority) +{ + Test_Blink_End_Key = key; + Test_Blink_End_Operation = operation; + Test_Blink_End_Priority = priority; +} + /** * @brief test dimmer blink handler * @param data - dimmer data @@ -72,64 +91,83 @@ static void test_lighting_command_blink_unit(BACNET_LIGHTING_COMMAND_DATA *data) uint16_t milliseconds = 10; uint32_t duration = 0; BACNET_LIGHTING_COMMAND_WARN_DATA *blink = &data->Blink; + BACNET_LIGHTING_OPERATION operation[] = { BACNET_LIGHTS_WARN, + BACNET_LIGHTS_WARN_OFF, + BACNET_LIGHTS_WARN_RELINQUISH }; + unsigned i; lighting_command_fade_to(data, data->Max_Actual_Value, 0); lighting_command_timer(data, milliseconds); zassert_true(data->In_Progress == BACNET_LIGHTING_IDLE, NULL); zassert_true(is_float_equal(Tracking_Value, data->Max_Actual_Value), NULL); - if (blink->Duration == 0) { - /* immediate */ - lighting_command_blink_warn(data, BACNET_LIGHTS_WARN, blink); - lighting_command_timer(data, milliseconds); - zassert_true(data->In_Progress == BACNET_LIGHTING_IDLE, NULL); - zassert_true(is_float_equal(Tracking_Value, blink->End_Value), NULL); - } else if (blink->Interval == 0) { - /* no blink, just egress timing */ - lighting_command_blink_warn(data, BACNET_LIGHTS_WARN, blink); - lighting_command_timer(data, milliseconds); - zassert_true(data->In_Progress == BACNET_LIGHTING_OTHER, NULL); - zassert_true(is_float_equal(Tracking_Value, blink->On_Value), NULL); - milliseconds = blink->Duration; - lighting_command_blink_warn(data, BACNET_LIGHTS_WARN, blink); - lighting_command_timer(data, milliseconds); - zassert_true(data->In_Progress == BACNET_LIGHTING_IDLE, NULL); - zassert_true(is_float_equal(Tracking_Value, blink->End_Value), NULL); - } else { - /* blinking and egress timing */ - if ((blink->Count > 0) && (blink->Count < UINT16_MAX)) { - duration = blink->Count * blink->Interval * 2UL; - if (duration > blink->Duration) { - duration = blink->Duration; - } - } else { - duration = blink->Duration; - } - milliseconds = blink->Interval; - do { - lighting_command_blink_warn(data, BACNET_LIGHTS_WARN, blink); + for (i = 0; i < ARRAY_SIZE(operation); i++) { + /* blink warn - common */ + data->Blink.Callback = test_blink_end; + data->Blink.Priority = 8; + Test_Blink_End_Priority = 0; + /* special cases */ + if (blink->Duration == 0) { + /* immediate */ + lighting_command_blink_warn(data, operation[i], blink); lighting_command_timer(data, milliseconds); - if (blink->Duration) { - zassert_true( - data->In_Progress == BACNET_LIGHTING_OTHER, - "In_Progress=%d", data->In_Progress); - if (data->Blink.State) { - zassert_true( - is_float_equal(Tracking_Value, blink->Off_Value), - "Tracking_Value=%f", Tracking_Value); - } else { - zassert_true( - is_float_equal(Tracking_Value, blink->On_Value), - "Tracking_Value=%f", Tracking_Value); + zassert_true(data->In_Progress == BACNET_LIGHTING_IDLE, NULL); + zassert_true( + is_float_equal(Tracking_Value, blink->End_Value), NULL); + } else if (blink->Interval == 0) { + /* no blink, just egress timing */ + lighting_command_blink_warn(data, operation[i], blink); + lighting_command_timer(data, milliseconds); + zassert_true(data->In_Progress == BACNET_LIGHTING_OTHER, NULL); + zassert_true(is_float_equal(Tracking_Value, blink->On_Value), NULL); + milliseconds = blink->Duration; + lighting_command_blink_warn(data, operation[i], blink); + lighting_command_timer(data, milliseconds); + zassert_true(data->In_Progress == BACNET_LIGHTING_IDLE, NULL); + zassert_true( + is_float_equal(Tracking_Value, blink->End_Value), NULL); + } else { + /* blinking and egress timing */ + if ((blink->Count > 0) && (blink->Count < UINT16_MAX)) { + duration = blink->Count * blink->Interval * 2UL; + if (duration > blink->Duration) { + duration = blink->Duration; } } else { - zassert_true( - data->In_Progress == BACNET_LIGHTING_IDLE, "In_Progress=%d", - data->In_Progress); - zassert_true( - is_float_equal(Tracking_Value, blink->End_Value), - "Tracking_Value=%f", Tracking_Value); + duration = blink->Duration; } - } while (blink->Duration); + milliseconds = blink->Interval; + do { + lighting_command_blink_warn(data, operation[i], blink); + lighting_command_timer(data, milliseconds); + if (blink->Duration) { + zassert_true( + data->In_Progress == BACNET_LIGHTING_OTHER, + "In_Progress=%d", data->In_Progress); + if (data->Blink.State) { + zassert_true( + is_float_equal(Tracking_Value, blink->Off_Value), + "Tracking_Value=%f", Tracking_Value); + } else { + zassert_true( + is_float_equal(Tracking_Value, blink->On_Value), + "Tracking_Value=%f", Tracking_Value); + } + } else { + zassert_true( + data->In_Progress == BACNET_LIGHTING_IDLE, + "In_Progress=%d", data->In_Progress); + zassert_true( + is_float_equal(Tracking_Value, blink->End_Value), + "Tracking_Value=%f", Tracking_Value); + } + } while (blink->Duration); + } + if (operation[i] != BACNET_LIGHTS_WARN) { + /* callback was called */ + zassert_equal(Test_Blink_End_Priority, blink->Priority, NULL); + zassert_equal(Test_Blink_End_Operation, operation[i], NULL); + zassert_equal(Test_Blink_End_Key, data->Key, NULL); + } } }