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:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user