|
| 1 | +/** Datetime Subsystem for ZephyrWatch |
| 2 | + * It is a real-time clock implementation based on real-time counters for ESP32. |
| 3 | + * |
| 4 | + * @license: GNU v3 |
| 5 | + * @maintainer: electricalgorithm @ github |
| 6 | +*/ |
| 7 | + |
| 8 | +#include <zephyr/kernel.h> |
| 9 | +#include <zephyr/logging/log.h> |
| 10 | +#include <zephyr/device.h> |
| 11 | +#include <zephyr/drivers/counter.h> |
| 12 | + |
| 13 | +#include "devicetwin/devicetwin.h" |
| 14 | +#include "datetime/datetime.h" |
| 15 | + |
| 16 | +// Configuration for counter alarm. |
| 17 | +#define ALARM_INTERVAL_US 1000000 |
| 18 | +#define ALARM_CHANNEL_ID 0 |
| 19 | + |
| 20 | +/* Register a logger for this library. */ |
| 21 | +LOG_MODULE_REGISTER(ZephyrWatch_Datetime, LOG_LEVEL_INF); |
| 22 | + |
| 23 | +/* Global unix_time variable to store datetime. */ |
| 24 | +uint32_t unix_time = 1748554674; |
| 25 | + |
| 26 | +/* Disable flag to not set alarm again in ISR. |
| 27 | + * 0: Set alarm again. |
| 28 | + * !: Do not set alarm again. |
| 29 | + */ |
| 30 | +static uint8_t reset_alarm = 0; |
| 31 | + |
| 32 | +/* Global alarm configuration structure - must persist for ISR access */ |
| 33 | +static struct counter_alarm_cfg alarm_cfg; |
| 34 | + |
| 35 | +/* Prototype definition of internal static functions and variables */ |
| 36 | +static const uint16_t days_in_month[] = { |
| 37 | + 31, 28, 31, 30, 31, 30, |
| 38 | + 31, 31, 30, 31, 30, 31 |
| 39 | +}; |
| 40 | +static bool is_leap_year(uint16_t year); |
| 41 | +static uint8_t calc_weekday(uint32_t days_since_epoch); |
| 42 | +// static datetime_t unix_to_localtime(int32_t timestamp, int8_t utc_offset_hours); |
| 43 | +// static datetime_t unix_to_utc(uint32_t timestamp); |
| 44 | + |
| 45 | +/* RTC_ISR |
| 46 | + * Interrupt service routine for alarm with real-time counters. This ISR is executed every |
| 47 | + * seconds. The user_data is an counter_alarm_cfg object. It updates static unix_time variable. |
| 48 | + */ |
| 49 | +void rtc_isr(const struct device *dev, uint8_t channel_id, uint32_t ticks, void *user_data) { |
| 50 | + // Cast alarm config from user data. |
| 51 | + struct counter_alarm_cfg *alarm_cfg = user_data; |
| 52 | + |
| 53 | + // Reset alarm if flag is set. |
| 54 | + if (!reset_alarm) { |
| 55 | + alarm_cfg->ticks = counter_us_to_ticks(dev, ALARM_INTERVAL_US); |
| 56 | + counter_set_channel_alarm(dev, ALARM_CHANNEL_ID, alarm_cfg); |
| 57 | + } |
| 58 | + |
| 59 | + // Update device's current time. |
| 60 | + device_twin_t *device_twin = get_device_twin_instance(); |
| 61 | + device_twin->unix_time = device_twin->unix_time + 1; |
| 62 | +} |
| 63 | + |
| 64 | +/* ENABLE_DATETIME_SUBSYSTEM |
| 65 | + * This function sets the soft real-time clock to track the time. It implements the counters |
| 66 | + * with one second alarm to update a global variable. |
| 67 | + */ |
| 68 | +int enable_datetime_subsystem() { |
| 69 | + int ret; |
| 70 | + |
| 71 | + // Set the real time counter to trigger an callback every second. |
| 72 | + const struct device *real_time_counter = DEVICE_DT_GET(DT_NODELABEL(rtc_timer)); |
| 73 | + if (!device_is_ready(real_time_counter)) { |
| 74 | + LOG_ERR("Real time counter device is not ready."); |
| 75 | + return -ENODEV; |
| 76 | + } |
| 77 | + LOG_DBG("Real time counter device is ready."); |
| 78 | + |
| 79 | + // Configure real time counter to track tine. |
| 80 | + ret = counter_start(real_time_counter); |
| 81 | + if (ret) { |
| 82 | + LOG_ERR("Failed to start real time counter (ret %d).", ret); |
| 83 | + return ret; |
| 84 | + } |
| 85 | + LOG_DBG("Real time counter started successfully."); |
| 86 | + |
| 87 | + // Configure the global alarm structure. |
| 88 | + alarm_cfg.flags = 0; |
| 89 | + alarm_cfg.ticks = counter_us_to_ticks(real_time_counter, ALARM_INTERVAL_US); |
| 90 | + alarm_cfg.callback = rtc_isr; |
| 91 | + alarm_cfg.user_data = &alarm_cfg; |
| 92 | + |
| 93 | + ret = counter_set_channel_alarm(real_time_counter, ALARM_CHANNEL_ID, &alarm_cfg); |
| 94 | + if (ret) { |
| 95 | + LOG_ERR("Failed to set channel alarm (ret %d).", ret); |
| 96 | + return ret; |
| 97 | + } |
| 98 | + LOG_DBG("Channel alarm set successfully."); |
| 99 | + |
| 100 | + return 0; |
| 101 | +} |
| 102 | + |
| 103 | +/* DISABLE_DATETIME_SUBSYSTEM |
| 104 | + * Stops the real-time counter and its alarm to disable the datetime subsystem. |
| 105 | + */ |
| 106 | +int disable_datetime_subsystem() { |
| 107 | + int ret; |
| 108 | + |
| 109 | + // Set the real time counter to trigger an callback every second. |
| 110 | + const struct device *real_time_counter = DEVICE_DT_GET(DT_NODELABEL(rtc_timer)); |
| 111 | + if (!device_is_ready(real_time_counter)) { |
| 112 | + LOG_ERR("Real time counter device is not ready."); |
| 113 | + return -ENODEV; |
| 114 | + } |
| 115 | + LOG_DBG("Real time counter device is ready."); |
| 116 | + |
| 117 | + // Disable the alarm first. |
| 118 | + reset_alarm = 1; |
| 119 | + LOG_DBG("Reset flag is cleared."); |
| 120 | + |
| 121 | + // Stop real time counter to track tine. |
| 122 | + ret = counter_stop(real_time_counter); |
| 123 | + if (ret) { |
| 124 | + LOG_ERR("Failed to stop real time counter (ret %d).", ret); |
| 125 | + return ret; |
| 126 | + } |
| 127 | + LOG_DBG("Real time counter stopped successfully."); |
| 128 | + |
| 129 | + return 0; |
| 130 | +} |
| 131 | + |
| 132 | +/* GET_CURRENT_UNIX_TIME |
| 133 | + * Return the UNIX epochs of the current time. |
| 134 | + */ |
| 135 | +uint32_t get_current_unix_time() { |
| 136 | + return unix_time; |
| 137 | +} |
| 138 | + |
| 139 | +/* SET_CURRENT_UNIX_TIME |
| 140 | + * Set the current time with UNIX epoch. |
| 141 | + */ |
| 142 | +int set_current_unix_time(uint32_t new_time) { |
| 143 | + unix_time = new_time; |
| 144 | + return 0; |
| 145 | +} |
| 146 | + |
| 147 | +/* GET_CURRENT_LOCAL_TIME |
| 148 | + * Return the current time in datetime_t object in local time zone. |
| 149 | + */ |
| 150 | +datetime_t get_current_local_time(int8_t utc_offset_hours) { |
| 151 | + return unix_to_localtime(unix_time, utc_offset_hours); |
| 152 | +} |
| 153 | + |
| 154 | +/* UNIX_TO_LOCALTIME |
| 155 | + * Converts Unix time to local time using UTC offset in hours (e.g., +2 or -5) |
| 156 | + */ |
| 157 | +datetime_t unix_to_localtime(int32_t timestamp, int8_t utc_offset_hours) { |
| 158 | + datetime_t utc; |
| 159 | + |
| 160 | + // Apply time zone offset |
| 161 | + int32_t adjusted = timestamp + (utc_offset_hours * 3600); |
| 162 | + |
| 163 | + // Handle negative timestamps (before 1970) |
| 164 | + if (adjusted < 0) { |
| 165 | + // Optional: clamp or handle negative time |
| 166 | + adjusted = 0; |
| 167 | + } |
| 168 | + |
| 169 | + uint32_t seconds = (uint32_t)adjusted; |
| 170 | + |
| 171 | + // Time part |
| 172 | + utc.second = seconds % 60; |
| 173 | + seconds /= 60; |
| 174 | + utc.minute = seconds % 60; |
| 175 | + seconds /= 60; |
| 176 | + utc.hour = seconds % 24; |
| 177 | + uint32_t days = seconds / 24; |
| 178 | + |
| 179 | + utc.weekday = calc_weekday(days); |
| 180 | + |
| 181 | + // Date part |
| 182 | + uint16_t year = 1970; |
| 183 | + while (1) { |
| 184 | + uint16_t days_in_year = is_leap_year(year) ? 366 : 365; |
| 185 | + if (days < days_in_year) break; |
| 186 | + days -= days_in_year; |
| 187 | + year++; |
| 188 | + } |
| 189 | + utc.year = year; |
| 190 | + |
| 191 | + uint8_t month = 0; |
| 192 | + while (1) { |
| 193 | + uint16_t dim = days_in_month[month]; |
| 194 | + if (month == 1 && is_leap_year(year)) dim++; |
| 195 | + if (days < dim) break; |
| 196 | + days -= dim; |
| 197 | + month++; |
| 198 | + } |
| 199 | + utc.month = month + 1; |
| 200 | + utc.day = days + 1; |
| 201 | + |
| 202 | + return utc; |
| 203 | +} |
| 204 | + |
| 205 | +/* UNIX_TO_UTC |
| 206 | + * Converts Unix time to UTC. |
| 207 | + */ |
| 208 | +datetime_t unix_to_utc(uint32_t timestamp) { |
| 209 | + return unix_to_localtime((int32_t)timestamp, 0); |
| 210 | +} |
| 211 | + |
| 212 | +/** **************** **/ |
| 213 | +/** STATIC FUNCTIONS **/ |
| 214 | +/** **************** **/ |
| 215 | + |
| 216 | +/* IS_LEAP_YEAR |
| 217 | + * Check if the year is a leap year. Internal purposes. |
| 218 | + */ |
| 219 | +static bool is_leap_year(uint16_t year) { |
| 220 | + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); |
| 221 | +} |
| 222 | + |
| 223 | +/* CALC_WEEKDAY |
| 224 | + * Calculate the day of the given days after epoch. |
| 225 | + */ |
| 226 | +static uint8_t calc_weekday(uint32_t days_since_epoch) { |
| 227 | + return (days_since_epoch + 4) % 7; // 1970-01-01 = Thursday |
| 228 | +} |
0 commit comments