Fix lighting output commands for warn-off and warn-relinquish (#1212)

* Fixed lighting output object lighting-commands for warn-off and warn-relinquish when blink-warn notification shall not occur.

* Fixed timer object task to initiate a write-request at expiration.

* Added channel and timer object write-property observers in blinkt app to monitor internal writes.

* Added vacancy timer command line argument in blinkt app for testing initial timer object vacancy time for lights channel.
This commit is contained in:
Steve Karg
2026-01-28 06:39:02 -06:00
committed by GitHub
parent b6d895ccf0
commit 20f402f5b6
4 changed files with 286 additions and 150 deletions
+7 -1
View File
@@ -12,7 +12,7 @@ The git repositories are hosted at the following sites:
* https://bacnet.sourceforge.net/ * https://bacnet.sourceforge.net/
* https://github.com/bacnet-stack/bacnet-stack/ * https://github.com/bacnet-stack/bacnet-stack/
## [Unreleased] - 2026-01-22 ## [Unreleased] - 2026-01-28
### Security ### Security
@@ -32,6 +32,9 @@ The git repositories are hosted at the following sites:
### Added ### 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 * Added debug prints for lighting output properties to assist in identifying
out-of-range values. (#1211) out-of-range values. (#1211)
* Added API to get the RGB pixel and brightness values from the blinkt * 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
* 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 * Fixed the server name in the blinkt app and removed the unnecessary
device.c module. (#1211) device.c module. (#1211)
* Fixed Channel object for Color object present-value which does not * Fixed Channel object for Color object present-value which does not
+96 -3
View File
@@ -52,11 +52,17 @@ static struct mstimer Blinkt_Task;
static bool Blinkt_Test = false; static bool Blinkt_Test = false;
/* observer for WriteGroup notifications */ /* observer for WriteGroup notifications */
static BACNET_WRITE_GROUP_NOTIFICATION Write_Group_Notification; 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 */ /* object instances */
static uint32_t Light_Channel_Instance = 1; static uint32_t Light_Channel_Instance = 1;
static uint32_t Color_Channel_Instance = 2; static uint32_t Color_Channel_Instance = 2;
static uint32_t CCT_Channel_Instance = 3; static uint32_t CCT_Channel_Instance = 3;
static uint32_t Vacancy_Timer_Instance = 1; static uint32_t Vacancy_Timer_Instance = 1;
static uint32_t Vacancy_Timeout_Milliseconds = 30UL * 60UL * 1000UL;
static unsigned Default_Priority = 16; static unsigned Default_Priority = 16;
/** /**
* Clean up the Blinkt! interface * Clean up the Blinkt! interface
@@ -66,6 +72,71 @@ static void blinkt_cleanup(void)
blinkt_stop(); 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 * @brief Callback for tracking value
* @param object_instance - object-instance number of the object * @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 to automatically turn off the lights */
Timer_Create(Vacancy_Timer_Instance); Timer_Create(Vacancy_Timer_Instance);
Timer_Name_Set(Vacancy_Timer_Instance, "Vacancy-Timer"); 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 */ /* to running */
timer_transition.next = NULL; timer_transition.next = NULL;
timer_transition.tag = BACNET_APPLICATION_TAG_REAL; timer_transition.tag = BACNET_APPLICATION_TAG_REAL;
@@ -205,6 +280,7 @@ static void BACnet_Object_Table_Init(void *context)
Timer_State_Change_Value_Set( Timer_State_Change_Value_Set(
Vacancy_Timer_Instance, TIMER_TRANSITION_RUNNING_TO_EXPIRED, Vacancy_Timer_Instance, TIMER_TRANSITION_RUNNING_TO_EXPIRED,
&timer_transition); &timer_transition);
/* timer members */
member.objectIdentifier.type = OBJECT_CHANNEL; member.objectIdentifier.type = OBJECT_CHANNEL;
member.objectIdentifier.instance = Light_Channel_Instance; member.objectIdentifier.instance = Light_Channel_Instance;
member.propertyIdentifier = PROP_PRESENT_VALUE; member.propertyIdentifier = PROP_PRESENT_VALUE;
@@ -281,6 +357,11 @@ static void BACnet_Object_Table_Init(void *context)
Color_Temperature_Write_Value_Handler); Color_Temperature_Write_Value_Handler);
Lighting_Output_Write_Present_Value_Callback_Set( Lighting_Output_Write_Present_Value_Callback_Set(
Lighting_Output_Write_Value_Handler); 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; Write_Group_Notification.callback = Channel_Write_Group;
handler_write_group_notification_add(&Write_Group_Notification); handler_write_group_notification_add(&Write_Group_Notification);
/* LEDs run at 0.1s intervals */ /* LEDs run at 0.1s intervals */
@@ -345,7 +426,7 @@ static void BACnet_Object_Task(void *context)
static void print_usage(const char *filename) static void print_usage(const char *filename)
{ {
printf("Usage: %s [device-instance]\n", 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"); printf(" [--version][--help]\n");
} }
@@ -367,6 +448,9 @@ static void print_help(const char *filename)
"--color:\n" "--color:\n"
"Default CSS color name from W3C, such as black, red, green, etc.\n"); "Default CSS color name from W3C, such as black, red, green, etc.\n");
printf("\n"); printf("\n");
printf("--vacancy:\n"
"Vacancy timeout in milliseconds.\n");
printf("\n");
printf("--test:\n" printf("--test:\n"
"Test the Blinkt! RGB LEDs with a cycling pattern.\n"); "Test the Blinkt! RGB LEDs with a cycling pattern.\n");
printf("\n"); printf("\n");
@@ -384,7 +468,7 @@ static void print_help(const char *filename)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
unsigned int target_args = 0; 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; int argi = 0;
const char *filename = NULL; const char *filename = NULL;
const char *color_name = "darkred"; const char *color_name = "darkred";
@@ -431,6 +515,15 @@ int main(int argc, char *argv[])
print_usage(filename); print_usage(filename);
return 1; 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 { } else {
if (target_args == 0) { if (target_args == 0) {
/* allow the device ID to be set */ /* allow the device ID to be set */
+179 -146
View File
@@ -498,110 +498,6 @@ unsigned Lighting_Output_Present_Value_Priority(uint32_t object_instance)
return priority; 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 * @brief Set the lighting command if the priority is active
* @param object [in] BACnet object instance * @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 * @brief Set the lighting command if the priority is active
* @details Commands Present_Value to a value equal to the * @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) #if (BACNET_PROTOCOL_REVISION >= 28)
/** /**
* @brief Set the lighting command if the priority is active * @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 object_instance - object-instance number of the object
* @param priority - priority 1..16 * @param priority - priority 1..16
* *
* @return true if priority is within range and priority-array slot is * @return true if the object exists
* relinquished.
*/ */
bool Lighting_Output_Present_Value_Relinquish( bool Lighting_Output_Present_Value_Relinquish(
uint32_t object_instance, unsigned priority) uint32_t object_instance, unsigned priority)
{ {
bool status = false; bool status = false;
struct object_data *pObject; struct object_data *pObject;
uint8_t old_priority, new_priority;
float value;
pObject = Keylist_Data(Object_List, object_instance); pObject = Keylist_Data(Object_List, object_instance);
if (pObject) { if (pObject) {
old_priority = Present_Value_Priority(pObject); status = true;
status = Present_Value_Relinquish(pObject, priority); Lighting_Command_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);
}
} }
return status; return status;
+4
View File
@@ -2190,6 +2190,10 @@ void Timer_Task(uint32_t object_instance, uint16_t milliseconds)
pObject->Timer_State = TIMER_STATE_EXPIRED; pObject->Timer_State = TIMER_STATE_EXPIRED;
pObject->Last_State_Change = pObject->Last_State_Change =
TIMER_TRANSITION_RUNNING_TO_EXPIRED; 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; break;
case TIMER_STATE_EXPIRED: case TIMER_STATE_EXPIRED: