Feature/date time mstimer clock (#861)

* Added daylight savings time calculation module with unit testing.

* Added datetime daylight savings time and clock API

* Added basic datetime_local() clock using mstimer as basis and time-sync option.  Integrated clock with ports/stm32f4xx example.
This commit is contained in:
Steve Karg
2024-11-24 11:20:25 -06:00
committed by GitHub
parent cdda524afc
commit fd3be47d86
19 changed files with 992 additions and 93 deletions
+262
View File
@@ -0,0 +1,262 @@
/**
* @file
* @brief API for Milliseconds Timer based Time-of-Day Clock
* @author Steve Karg <skarg@users.sourceforge.net>
* @date 2024
* @copyright SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0
*/
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
/* BACnet Stack defines - first */
#include "bacnet/bacdef.h"
/* BACnet Stack API */
#include "bacnet/basic/sys/dst.h"
#include "bacnet/basic/sys/mstimer.h"
#include "bacnet/datetime.h"
/* local time */
static BACNET_DATE_TIME BACnet_Date_Time;
static int16_t UTC_Offset_Minutes;
/* starting and stopping dates/times to determine DST */
static struct daylight_savings_data DST_Range;
static bool DST_Enabled;
/* local time based on mstimer */
static struct mstimer Date_Timer;
/**
* @brief Synchronize the local time from the millisecond timer
*/
static void datetime_sync(void)
{
bacnet_time_t seconds, elapsed_seconds;
unsigned long milliseconds;
milliseconds = mstimer_elapsed(&Date_Timer);
elapsed_seconds = milliseconds / 1000UL;
if (elapsed_seconds) {
mstimer_restart(&Date_Timer);
seconds = datetime_seconds_since_epoch(&BACnet_Date_Time);
seconds += elapsed_seconds;
datetime_since_epoch_seconds(&BACnet_Date_Time, seconds);
/* generate a hundredths value */
milliseconds -= (elapsed_seconds * 1000UL);
BACnet_Date_Time.time.hundredths = milliseconds / 10;
}
}
/**
* @brief Get the local determination of daylight savings time being active
* @return true if DST is active, false otherwise
*/
static bool datetime_dst_active(
struct daylight_savings_data *dst,
BACNET_DATE_TIME *bdatetime,
bool enabled)
{
bool active = false;
if (enabled) {
active = dst_active(
dst, bdatetime->date.year, bdatetime->date.month,
bdatetime->date.day, bdatetime->time.hour, bdatetime->time.min,
bdatetime->time.sec);
}
return active;
}
/**
* @brief Get the local date and time
* @param bdate [out] The date to get
* @param btime [out] The time to get
* @param utc_offset_minutes [out] The UTC offset in minutes
* @param dst_active [out] The DST flag
* @return true if successful, false on error
*/
bool datetime_local(
BACNET_DATE *bdate,
BACNET_TIME *btime,
int16_t *utc_offset_minutes,
bool *dst_active)
{
datetime_sync();
if (bdate) {
datetime_copy_date(bdate, &BACnet_Date_Time.date);
}
if (btime) {
datetime_copy_time(btime, &BACnet_Date_Time.time);
}
if (utc_offset_minutes) {
*utc_offset_minutes = UTC_Offset_Minutes;
}
if (dst_active) {
*dst_active =
datetime_dst_active(&DST_Range, &BACnet_Date_Time, DST_Enabled);
}
return true;
}
/**
* @brief Get the UTC offset in minutes
* @return The UTC offset in minutes
*/
int16_t datetime_utc_offset_minutes(void)
{
return UTC_Offset_Minutes;
}
/**
* @brief Set the UTC offset in minutes
* @param minutes [in] The UTC offset in minutes
* @return true if successful, false on error
*/
bool datetime_utc_offset_minutes_set(int16_t minutes)
{
UTC_Offset_Minutes = minutes;
return true;
}
/**
* @brief Get the Daylight Savings Enabled flag
* @return Daylight Savings Enabled flag
*/
bool datetime_dst_enabled(void)
{
return DST_Enabled;
}
/**
* @brief Set the Daylight Savings Enabled flag
* @param flag [in] The Daylight Savings Enabled flag
*/
void datetime_dst_enabled_set(bool flag)
{
DST_Enabled = flag;
}
/**
* @brief Get the local DST start and end date range
* @param dst_range [out] The DST range to get
* @return true if successful, false on error
*/
bool datetime_dst_ordinal_range(
uint8_t *start_month,
uint8_t *start_week,
uint8_t *start_day,
uint8_t *end_month,
uint8_t *end_week,
uint8_t *end_day)
{
if (!DST_Range.Ordinal) {
return false;
}
*start_month = DST_Range.Begin_Month;
*start_week = DST_Range.Begin_Week;
*start_day = DST_Range.Begin_Day;
*end_month = DST_Range.End_Month;
*end_week = DST_Range.End_Week;
*end_day = DST_Range.End_Day;
return true;
}
bool datetime_dst_ordinal_range_set(
uint8_t start_month,
uint8_t start_week,
BACNET_WEEKDAY start_day,
uint8_t end_month,
uint8_t end_week,
BACNET_WEEKDAY end_day)
{
DST_Range.Ordinal = true;
DST_Range.Begin_Month = start_month;
DST_Range.Begin_Week = start_week;
DST_Range.Begin_Day = start_day;
DST_Range.End_Month = end_month;
DST_Range.End_Week = end_week;
DST_Range.End_Day = end_day;
return true;
}
/**
* @brief Get the local DST start and end date range for specific month day
* @param dst_range [in] The DST range
* @return true if the start and end month day values are returned
*/
bool datetime_dst_date_range(
uint8_t *start_month,
uint8_t *start_day,
uint8_t *end_month,
uint8_t *end_day)
{
if (DST_Range.Ordinal) {
return false;
}
*start_month = DST_Range.Begin_Month;
*start_day = DST_Range.Begin_Day;
*end_month = DST_Range.End_Month;
*end_day = DST_Range.End_Day;
return true;
}
/**
* @brief Set the local DST start and end date range for specific month day
* @param dst_range [in] The DST range to set
* @return true if successful, false on error
*/
bool datetime_dst_date_range_set(
uint8_t start_month, uint8_t start_day, uint8_t end_month, uint8_t end_day)
{
DST_Range.Ordinal = false;
DST_Range.Begin_Month = start_month;
DST_Range.Begin_Day = start_day;
DST_Range.End_Month = end_month;
DST_Range.End_Day = end_day;
return true;
}
/**
* @brief Set the local date and time from a BACnet TimeSynchronization request
* @param bdate [in] The date to set
* @param btime [in] The time to set
* @param utc [in] true if originating from an UTCTimeSynchronization request
*/
void datetime_timesync(BACNET_DATE *bdate, BACNET_TIME *btime, bool utc)
{
BACNET_DATE_TIME local_time = { 0 };
const int32_t dst_adjust_minutes = 60L;
if (utc) {
if (bdate && btime) {
datetime_copy_date(&local_time.date, bdate);
datetime_copy_time(&local_time.time, btime);
datetime_add_minutes(&local_time, UTC_Offset_Minutes);
if (datetime_dst_active(&DST_Range, &local_time, DST_Enabled)) {
datetime_add_minutes(&local_time, dst_adjust_minutes);
}
datetime_copy(&BACnet_Date_Time, &local_time);
mstimer_restart(&Date_Timer);
}
} else {
datetime_copy_date(&BACnet_Date_Time.date, bdate);
datetime_copy_time(&BACnet_Date_Time.time, btime);
mstimer_restart(&Date_Timer);
}
}
/**
* @brief Initialize the local date and time timer
*/
void datetime_init(void)
{
dst_init_defaults(&DST_Range);
mstimer_set(&Date_Timer, 0);
}
+21 -3
View File
@@ -221,6 +221,8 @@ days_since_epoch(uint16_t epoch_year, uint16_t year, uint8_t month, uint8_t day)
days += days_per_month(year, mm);
}
days += day;
/* 'days since' is one less */
days -= 1;
}
return (days);
@@ -243,15 +245,15 @@ void days_since_epoch_to_date(
uint8_t *pDay)
{
uint8_t month = 1;
uint8_t day = 0;
uint8_t day = 1;
uint16_t year;
year = epoch_year;
while (days > days_per_year(year)) {
while (days >= days_per_year(year)) {
days -= days_per_year(year);
year++;
}
while (days > days_per_month(year, month)) {
while (days >= days_per_month(year, month)) {
days -= days_per_month(year, month);
month++;
}
@@ -291,3 +293,19 @@ bool days_date_is_valid(uint16_t year, uint8_t month, uint8_t day)
return (valid);
}
/**
* Returns the day of the week value
*
* @param epoch_day - day of week for epoch day
* @param days - number of days since epoch
* @return day of week 1..7 offset by epoch day
*/
uint8_t days_of_week(uint8_t epoch_day, uint32_t days)
{
uint8_t dow = epoch_day;
dow += (days % 7);
return dow;
}
+3
View File
@@ -48,6 +48,9 @@ void days_since_epoch_to_date(
uint8_t *pMonth,
uint8_t *pDay);
BACNET_STACK_EXPORT
uint8_t days_of_week(uint8_t epoch_day, uint32_t days);
BACNET_STACK_EXPORT
bool days_date_is_valid(uint16_t year, uint8_t month, uint8_t day);
+229
View File
@@ -0,0 +1,229 @@
/**
* @file
* @brief computes whether day is during daylight savings time
* @note Public domain algorithms from ACM
* @author Steve Karg <skarg@users.sourceforge.net>
* @date 1997
* @copyright SPDX-License-Identifier: CC-PDDC
*/
#include <stdint.h>
#include <stdbool.h>
#include "days.h"
#include "dst.h"
/**
* This function returns the number of seconds after midnight
*
* @param hours - hours after midnight (0..23)
* @param minutes - minutes after hour (0..59)
* @param seconds - holds seconds after minute (0..59)
*
* @return true if date-time falls in DST. false if not.
*/
static uint32_t
time_to_seconds(uint32_t hours, uint32_t minutes, uint32_t seconds)
{
return (((hours) * 60 * 60) + ((minutes) * 60) + (seconds));
}
/**
* This function returns the day of the month for starting the Nth week
*
* @param year - Year of our Lord A.D. (1900..9999)
* @param month - months of the year (1=Jan,...,12=Dec)
* @param ordinal - Ordinal Day of the Month
* 1=1st, 2=2nd, 3=3rd, 4=4th, or 5=LAST
* @return day of the month (1..31)
*/
static uint8_t
ordinal_week_month_day(uint16_t year, uint8_t month, uint8_t ordinal)
{
uint8_t day = 0;
if (ordinal == 5) {
/* last week of the month */
day = days_per_month(year, month) - 6;
} else {
if (ordinal) {
ordinal--;
day = 1 + (ordinal * 7);
}
}
return day;
}
/**
* This function returns true if the date-time is during DST
*
* @param year - Year of our Lord A.D. (2000,2001,..2099)
* @param month - months of the year (1=Jan,...,12=Dec)
* @param day - day of the month (1..31)
* @param hour - hours after midnight (0..23)
* @param minute - minutes after hour (0..59)
* @param second - holds seconds after minute (0..59)
*
* @return true if date-time falls in DST. false if not.
*/
bool dst_active(
struct daylight_savings_data *data,
uint16_t year,
uint8_t month,
uint8_t day,
uint8_t hour,
uint8_t minute,
uint8_t second)
{
bool active = false;
uint8_t i = 0;
uint32_t time_now = 0;
uint32_t time_dst = 0;
uint8_t day_of_week = 0;
uint8_t days = 0;
uint32_t days_begin = 0;
uint32_t days_now = 0;
uint32_t days_end = 0;
if (data->Ordinal) {
if ((month >= data->Begin_Month) && (month <= data->End_Month)) {
if (month == data->Begin_Month) {
days = days_per_month(year, month);
i = ordinal_week_month_day(year, month, data->Begin_Week);
for (; i <= days; i++) {
day_of_week = days_of_week(
data->Epoch_Day,
days_since_epoch(data->Epoch_Year, year, month, i));
if (day_of_week == data->Begin_Day) {
if (day == i) {
time_now = time_to_seconds(hour, minute, second);
/* begins at 2 AM Standard Time */
time_dst = time_to_seconds(2, 0, 0);
if (time_now >= time_dst) {
active = true;
}
} else if (day > i) {
active = true;
}
break;
}
}
} else if (month == data->End_Month) {
days = days_per_month(year, month);
i = ordinal_week_month_day(year, month, data->End_Week);
for (; i <= days; i++) {
day_of_week = days_of_week(
data->Epoch_Day,
days_since_epoch(data->Epoch_Year, year, month, i));
if (day_of_week == data->End_Day) {
if (day == i) {
time_now = time_to_seconds(hour, minute, second);
/* ends at 2 AM Daylight time,
which is 1 AM Standard Time */
time_dst = time_to_seconds(1, 0, 0);
if (time_now < time_dst) {
active = true;
}
} else if (day < i) {
active = true;
}
break;
}
}
} else {
/* months between the beginning and end months */
active = true;
}
}
} else {
days_now = days_since_epoch(data->Epoch_Year, year, month, day);
days_begin = days_since_epoch(
data->Epoch_Year, year, data->Begin_Month, data->Begin_Day);
days_end = days_since_epoch(
data->Epoch_Year, year, data->End_Month, data->End_Day);
if ((days_now >= days_begin) && (days_now <= days_end)) {
if (days_now == days_begin) {
time_now = time_to_seconds(hour, minute, second);
/* begins at 2 AM Standard Time */
time_dst = time_to_seconds(2, 0, 0);
if (time_now >= time_dst) {
active = true;
}
} else if (days_now == days_end) {
time_now = time_to_seconds(hour, minute, second);
/* ends at 2 AM Daylight time,
which is 1 AM Standard Time */
time_dst = time_to_seconds(1, 0, 0);
if (time_now < time_dst) {
active = true;
}
} else {
active = true;
}
}
}
return active;
}
/**
* @brief This function sets the daylight savings time parameters
* @param data - daylight savings time data
* @param ordinal - true when ordinal day of month used, false if specific dates
* @param begin_month - month DST begins 1=Jan,...,12=Dec
* @param begin_day - day of the month DST begins
* 1..31 for specific day, or day of week 1..7 for ordinal day
* @param begin_which_day - which ordinal day of the month is used
* 1=1st, 2=2nd, 3=3rd, 4=4th, or 5=LAST
* @param end_month - month DST ends 1=Jan,...,12=Dec
* @param end_day - day of the month DST ends
* 1..31 for specific day, or day of week 1..7 for ordinal day
* @param end_which_day - which ordinal day of the month is used
* 1=1st, 2=2nd, 3=3rd, 4=4th, or 5=LAST
* @param epoch_day - day of the week for the BACnet Epoch (1=Monday..7=Sunday)
* @param epoch_year - year of the BACnet Epoch (1900..9999)
*/
void dst_init(
struct daylight_savings_data *data,
bool ordinal,
uint8_t begin_month,
uint8_t begin_day,
uint8_t begin_which_day,
uint8_t end_month,
uint8_t end_day,
uint8_t end_which_day,
uint8_t epoch_day,
uint16_t epoch_year)
{
if (data) {
data->Ordinal = ordinal;
data->Begin_Month = begin_month;
data->Begin_Day = begin_day;
data->Begin_Week = begin_which_day;
data->End_Month = end_month;
data->End_Day = end_day;
data->End_Week = end_which_day;
data->Epoch_Day = epoch_day;
data->Epoch_Year = epoch_year;
}
}
/**
* Initializes the daylight savings time parameters to their defaults.
*/
void dst_init_defaults(struct daylight_savings_data *data)
{
if (data) {
/* Starts: Second=2 Sunday=7 in March=3 */
/* Ends: First=1 Sunday=7 in November=11 */
data->Ordinal = true;
data->Begin_Month = 3;
data->Begin_Day = 7;
data->Begin_Week = 2;
data->End_Month = 11;
data->End_Day = 7;
data->End_Week = 1;
/* BACnet Epoch */
data->Epoch_Day = 1 /* Monday=1 */;
data->Epoch_Year = 1900;
}
}
+58
View File
@@ -0,0 +1,58 @@
/**
* @file
* @brief This file contains the function prototypes for for the module.
* @author Steve Karg <skarg@users.sourceforge.net>
* @date 1997
* @note Public domain algorithms from ACM
* @copyright SPDX-License-Identifier: CC-PDDC
*/
#ifndef BACNET_BASIC_SYS_DST_H
#define BACNET_BASIC_SYS_DST_H
#include <stdint.h>
#include <stdbool.h>
/* BACnet Stack defines - first */
#include "bacnet/bacdef.h"
struct daylight_savings_data {
bool Ordinal : 1;
uint8_t Begin_Month;
uint8_t Begin_Day;
uint8_t Begin_Week;
uint8_t End_Month;
uint8_t End_Day;
uint8_t End_Week;
uint16_t Epoch_Year;
uint8_t Epoch_Day;
};
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
bool dst_active(
struct daylight_savings_data *dst,
uint16_t year,
uint8_t month,
uint8_t day,
uint8_t hour,
uint8_t minute,
uint8_t second);
void dst_init(
struct daylight_savings_data *data,
bool automatic,
uint8_t begin_month,
uint8_t begin_day,
uint8_t begin_which_day,
uint8_t end_month,
uint8_t end_day,
uint8_t end_which_day,
uint8_t epoch_day,
uint16_t epoch_year);
/* initialization */
void dst_init_defaults(struct daylight_savings_data *data);
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif