SatCat5
datetime.cc
1 // Copyright 2021-2025 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <satcat5/datetime.h>
7 #include <satcat5/log.h>
8 
9 namespace date = satcat5::datetime;
10 namespace ptp = satcat5::ptp;
14 
15 // Global instance of the datetime::Clock class.
17 
18 // The offset from GPS to PTP is fixed by the IEEE1588 standard.
19 constexpr s64 PTP_EPOCH = 1000LL * 315964819;
20 
21 // Convert a millisecond timestamp to or from PTP format.
22 s64 date::from_ptp(const ptp::Time& time) {
23  return time.delta_msec() - PTP_EPOCH;
24 }
25 
27  s64 secs = s64((time + PTP_EPOCH) / 1000);
28  u32 msec = u32((time + PTP_EPOCH) % 1000);
29  return ptp::Time(secs, msec * ptp::NSEC_PER_MSEC);
30 }
31 
32 // The core "Clock" functions don't require any fancy conversions.
33 Clock::Clock()
34  : satcat5::poll::Timer()
35  , m_tref(SATCAT5_CLOCK->now())
36  , m_tcount(0)
37  , m_gps(0)
38 {
39  // Update about once per millisecond if possible.
40  // (Slower is fine; we just don't want to hog the CPU.)
41  timer_every(1);
42 }
43 
44 u32 Clock::uptime_usec() const {
45  // Add elapsed time since the coarse m_tcount/m_tref pair.
46  return 1000 * m_tcount + m_tref.elapsed_usec();
47 }
48 
49 void Clock::reset(bool full) {
50  m_tref = SATCAT5_CLOCK->now();
51  if (full) {m_tcount = 0; m_gps = 0;}
52 }
53 
54 void Clock::set(s64 gps) {
55  // Reset the current time reference.
56  m_tref = SATCAT5_CLOCK->now();
57  m_gps = gps;
58 }
59 
61  // Sanity check if primary clock has changed.
62  // (Otherwise, may get stuck pointing to the null placeholder.)
63  if (m_tref.clk != SATCAT5_CLOCK) reset();
64 
65  // Elapsed time since the last update?
66  unsigned incr = m_tref.increment_msec();
67 
68  // Increment both time counters.
69  m_tcount += incr; // Always increment uptime
70  if (m_gps) m_gps += incr; // Keep counting once set
71 }
72 
73 // Offset from GPS epoch (1980 Jan 6) to the RtcTime epoch (2000 Jan 1).
74 // Note: 2000 Jan 1 is a Saturday (DOW = 6)
75 static constexpr s64 RTC_EPOCH =
76  1042 * (s64)date::ONE_WEEK + 6 * (s64)date::ONE_DAY;
77 
78 // Convert BCD value to ordinary value and vice-versa.
79 static inline constexpr u8 bcd2int(u8 bcd)
80  { return 10 * (bcd >> 4) + (bcd & 0x0F); }
81 static inline constexpr u8 int2bcd(u8 x)
82  { return 16 * (x / 10) + (x % 10); }
83 
84 // Lookup table converts BCD 12-hour time to 24-hour format.
85 // (Assume AM/PM flags follow the ISL12082 convention.)
86 u8 bcd_convert_24hr(u8 val) {
87  // If MIL flag is already set, simply convert from BCD.
88  if (val & date::RTC_MIL_BIT)
89  return bcd2int(val & 0x7F);
90 
91  // Otherwise, use the following lookup table:
92  switch (val) {
93  case 0x12: return 0; // 12 AM = 00:00 (midnight)
94  case 0x01: return 1; // 1 AM = 01:00
95  case 0x02: return 2; // 2 AM = 02:00
96  case 0x03: return 3; // 3 AM = 03:00
97  case 0x04: return 4; // 4 AM = 04:00
98  case 0x05: return 5; // 5 AM = 05:00
99  case 0x06: return 6; // 6 AM = 06:00
100  case 0x07: return 7; // 7 AM = 07:00
101  case 0x08: return 8; // 8 AM = 08:00
102  case 0x09: return 9; // 9 AM = 09:00
103  case 0x10: return 10; // 10 AM = 10:00
104  case 0x11: return 11; // 11 AM = 11:00
105  case 0x32: return 12; // 12 PM = 12:00 (noon)
106  case 0x21: return 13; // 1 PM = 13:00
107  case 0x22: return 14; // 2 PM = 14:00
108  case 0x23: return 15; // 3 PM = 15:00
109  case 0x24: return 16; // 4 PM = 16:00
110  case 0x25: return 17; // 5 PM = 17:00
111  case 0x26: return 18; // 6 PM = 18:00
112  case 0x27: return 19; // 7 PM = 19:00
113  case 0x28: return 20; // 8 PM = 20:00
114  case 0x29: return 21; // 9 PM = 21:00
115  case 0x30: return 22; // 10 PM = 22:00
116  case 0x31: return 23; // 11 PM = 23:00
117  }
118 
119  // Anything else is invalid.
120  return 0xFF;
121 }
122 
123 // Test if a given century+year is a leap-year.
124 static inline bool is_leap_year(u8 ct, u8 yy) {
125  return (yy % 4 == 0) && ((yy > 0) || (ct % 4 == 0));
126 }
127 
128 // Given century number, return days in that century.
129 static inline unsigned days_per_century(u8 ct) {
130  // Years 2000, 2400 are leap years; but 2100, 2200, 2300 are not.
131  return is_leap_year(ct, 0) ? 36525 : 36524;
132 }
133 
134 // Given century+year, return days in that year.
135 static inline unsigned days_per_year(u8 ct, u8 yy) {
136  return is_leap_year(ct, yy) ? 366 : 365;
137 }
138 
139 // Given year and month, return days in that month.
140 static unsigned days_per_month(u8 ct, u8 yy, u8 mm) {
141  // Special-case for leap years:
142  if ((mm == 2) && is_leap_year(ct, yy))
143  return 29;
144 
145  // All other months by lookup table:
146  switch (mm) {
147  case 2:
148  return 28;
149  case 4: case 6: case 9: case 11:
150  return 30;
151  default:
152  return 31;
153  }
154 }
155 
157  if (!validate()) return UINT32_MAX;
158 
159  // Count days for each full century.
160  unsigned total = 0;
161  for (unsigned c = 20 ; c < ct ; ++c)
162  total += days_per_century(c);
163 
164  // Count days for each full year.
165  for (unsigned y = 0 ; y < yr ; ++y)
166  total += days_per_year(ct, y);
167 
168  // Count days for each full month.
169  for (u32 m = 1 ; m < mo ; ++m)
170  total += days_per_month(ct, yr, m);
171 
172  // Count days in the current month.
173  return total + dt - 1;
174 }
175 
177  // Calculate total offset in milliseconds.
178  if (validate())
179  return 10*ss + 1000*sc + 60000*mn + 3600000*hr;
180  else
181  return UINT32_MAX; // Error
182 }
183 
185  return GpsTime {
186  (s32)(time / date::ONE_WEEK), // Week number
187  (u32)(time % date::ONE_WEEK) // Time of week
188  };
189 }
190 
191 s64 date::from_gps(const GpsTime& time) {
192  return (s64)date::ONE_WEEK * time.wkn + time.tow;
193 }
194 
196  // Convert to the RTC epoch (2000 Jan 1 @ 00:00:00).
197  time -= RTC_EPOCH;
198 
199  // Split time into days-since-epoch and msec-since-midnight.
200  u32 days = (u32)(time / date::ONE_DAY);
201  u32 msec = (u32)(time % date::ONE_DAY);
202 
203  // This format can represent anything from year 2000 - 9999.
204  if ((time < 0) || (days >= 2921940))
205  return date::RTC_ERROR;
206 
207  // Calculate day of week (epoch is a Saturday = 6).
208  u8 dw = (u8)((days + 6) % 7);
209 
210  // Deduct days for each full century.
211  u8 ct = 20;
212  while (days >= days_per_century(ct))
213  days -= days_per_century(ct++);
214 
215  // Deduct days for each full year.
216  u8 yr = 0;
217  while (days >= days_per_year(ct, yr))
218  days -= days_per_year(ct, yr++);
219 
220  // Deduct days for each full month.
221  u8 mo = 1;
222  while (days >= days_per_month(ct, yr, mo))
223  days -= days_per_month(ct, yr, mo++);
224 
225  // Whatever's leftover = Day-of-month.
226  u8 dt = (u8)(days + 1);
227 
228  // Calculate hours, minutes, seconds...
229  u32 rem = msec / 10; // Each tick = 10 msec
230  u8 ss = (u8)(rem % 100); rem /= 100; // Ticks
231  u8 sc = (u8)(rem % 60); rem /= 60; // Seconds
232  u8 mn = (u8)(rem % 60); rem /= 60; // Minutes
233  u8 hr = (u8)(rem);
234 
235  // Construct the new RTC object.
236  return RtcTime {dw, ct, yr, mo, dt, hr, mn, sc, ss};
237 }
238 
239 s64 date::from_rtc(const RtcTime& time) {
240  u32 days = time.days_since_epoch();
241  u32 msec = time.msec_since_midnight();
242  if ((days < UINT32_MAX) && (msec < UINT32_MAX))
243  return RTC_EPOCH + (s64)date::ONE_DAY * days + msec;
244  else
245  return date::TIME_ERROR;
246 }
247 
248 // Comparison and I/O helper functions
249 bool GpsTime::read_from(io::Readable* rd) {
250  if (rd->get_read_ready() < 8)
251  return false;
252 
253  wkn = (s32)rd->read_u32();
254  tow = rd->read_u32();
255  return true;
256 }
257 
258 bool GpsTime::operator<(const GpsTime& other) const {
259  if (wkn < other.wkn) return true;
260  if (wkn > other.wkn) return false;
261  return tow < other.tow;
262 }
263 
264 bool GpsTime::operator==(const GpsTime& other) const {
265  return (wkn == other.wkn) && (tow == other.tow);
266 }
267 
268 bool RtcTime::validate() const {
269  return (ss < 100) && (sc < 60) && (mn < 60) && (hr < 24)
270  && (dt > 0) && (dt <= days_per_month(ct, yr, mo))
271  && (mo > 0) && (mo <= 12) && (yr < 100)
272  && (ct >= 20) && (ct < 100) && (dw < 7);
273 }
274 
276  // Convert each field to BCD format, then write.
277  // TODO: Update format to include century or deprecate this method.
278  u8 temp[8];
279  temp[0] = int2bcd(ss); // Sub-seconds (0-99)
280  temp[1] = int2bcd(sc); // Seconds (0-59)
281  temp[2] = int2bcd(mn); // Minutes (0-59)
282  temp[3] = int2bcd(hr) | date::RTC_MIL_BIT; // Hours (0-23) + MIL bit
283  temp[4] = int2bcd(dt); // Day of month (1-31)
284  temp[5] = int2bcd(mo); // Month (1-12)
285  temp[6] = int2bcd(yr); // Year (00-99)
286  temp[7] = int2bcd(dw); // Day of week (0-6)
287  wr->write_bytes(8, temp);
288 }
289 
291  // Read raw bytes
292  u8 temp[8];
293  bool rdok = rd->read_bytes(8, temp);
294 
295  // Convert each field, ignoring most status flags.
296  // TODO: Update format to include century or deprecate this method.
297  if (rdok) {
298  ss = bcd2int(temp[0] & 0xFF); // Sub-seconds (0-99)
299  sc = bcd2int(temp[1] & 0x7F); // Seconds (0-59)
300  mn = bcd2int(temp[2] & 0x7F); // Minutes (0-59)
301  hr = bcd_convert_24hr(temp[3]); // Hours (0-23)
302  dt = bcd2int(temp[4] & 0x3F); // Day of month (1-31)
303  mo = bcd2int(temp[5] & 0x1F); // Month (1-12)
304  yr = bcd2int(temp[6] & 0xFF); // Year (00-99)
305  ct = 20; // Assume 2000 - 2099
306  dw = bcd2int(temp[7] & 0x07); // Day of week (0-6)
307  }
308 
309  // Validate before returning.
310  bool ok = rdok && validate();
311  if (!ok) {ss = sc = mn = hr = dt = mo = yr = ct = dw = 0;}
312  return ok;
313 }
314 
316  // Format as an ISO8601 / RFC3339 timestamp.
317  u32 year = 100*ct + yr;
318  wr.wr_d32(year, 9999); // Year (4 digits)
319  wr.wr_str("-");
320  wr.wr_d32(mo, 99); // Month (1-12)
321  wr.wr_str("-");
322  wr.wr_d32(dt, 99); // Day-of-month (1-31)
323  wr.wr_str("T");
324  wr.wr_d32(hr & 0x7F, 99); // Hour (0-23) + MIL bit
325  wr.wr_str(":");
326  wr.wr_d32(mn, 99); // Minutes (0-59)
327  wr.wr_str(":");
328  wr.wr_d32(sc, 99); // Seconds (0-59)
329  wr.wr_str(".");
330  wr.wr_d32(ss, 99); // Sub-seconds (0-99)
331  wr.wr_str("Z"); // Time-zone = UTC (sort of)
332 }
333 
334 bool RtcTime::operator<(const RtcTime& other) const {
335  // Note: Ignore day-of-week field.
336  if (ct < other.ct) return true;
337  if (ct > other.ct) return false;
338  if (yr < other.yr) return true;
339  if (yr > other.yr) return false;
340  if (mo < other.mo) return true;
341  if (mo > other.mo) return false;
342  if (dt < other.dt) return true;
343  if (dt > other.dt) return false;
344  if (hr < other.hr) return true;
345  if (hr > other.hr) return false;
346  if (mn < other.mn) return true;
347  if (mn > other.mn) return false;
348  if (sc < other.sc) return true;
349  if (sc > other.sc) return false;
350  return ss < other.ss;
351 }
352 
353 bool RtcTime::operator==(const RtcTime& other) const {
354  // Note: Ignore day-of-week field.
355  return (yr == other.yr)
356  && (mo == other.mo)
357  && (dt == other.dt)
358  && (hr == other.hr)
359  && (mn == other.mn)
360  && (sc == other.sc)
361  && (ss == other.ss);
362 }
Real-time clock for tracking date/time.
Definition: datetime.h:169
void set(s64 gps)
Set current GPS time.
Definition: datetime.cc:54
satcat5::datetime::GpsTime gps() const
Current time as GPS week number and time-of-week.
Definition: datetime.h:195
void reset(bool full=false)
Reset internals after changes to SATCAT5_CLOCK.
Definition: datetime.cc:49
u32 uptime_usec() const
Get elapsed time since startup, in microseconds.
Definition: datetime.cc:44
void timer_event() override
Child class MUST override this method.
Definition: datetime.cc:60
Abstract API for reading byte-streams and packets.
Definition: io_readable.h:68
virtual bool read_bytes(unsigned nbytes, void *dst)
Read 0 or more bytes into a buffer.
Definition: io_readable.cc:190
virtual unsigned get_read_ready() const =0
How many bytes can be read without blocking?
Abstract API for writing byte-streams and packets.
Definition: io_writeable.h:24
virtual void write_bytes(unsigned nbytes, const void *src)
Write 0 or more bytes from a buffer.
Internal buffer used by the Log class.
Definition: log.h:134
void wr_str(const char *str)
Write a null-terminated UTF-8 string.
Definition: log.cc:297
void wr_d32(u32 val, unsigned zpad=0)
Write an unsigned integer (u32) in decimal format.
Definition: log.cc:317
void timer_every(unsigned msec)
Configure a repeating notification every X milliseconds.
Definition: polling.cc:321
High-precision timestamp for use with PTP / IEEE1588.
Definition: ptp_time.h:41
s64 delta_msec() const
Convert time-differences to the designated unit.
Definition: ptp_time.cc:61
Real-time clock conversion functions.
static const u8 RTC_MIL_BIT
Bit-flag in HR field indicating 24-HOUR clock format.
Definition: datetime.h:158
static constexpr u32 ONE_WEEK
Common time-related constants, measured in milliseconds.
Definition: datetime.h:47
static constexpr s64 TIME_ERROR
A date/time of zero indicates an error.
Definition: datetime.h:51
satcat5::ptp::Time to_ptp(s64 time)
Convert an internal timestamp into the designated format.
Definition: datetime.cc:26
static constexpr u32 ONE_DAY
Common time-related constants, measured in milliseconds.
Definition: datetime.h:46
s64 from_rtc(const satcat5::datetime::RtcTime &time)
Convert the designated format into an internal timestamp.
Definition: datetime.cc:239
satcat5::datetime::RtcTime to_rtc(s64 time)
Convert an internal timestamp into the designated format.
Definition: datetime.cc:195
satcat5::datetime::Clock clock
Global instance of the datetime::Clock class.
Definition: datetime.cc:16
static const datetime::RtcTime RTC_ERROR
Special datetime::RtcTime value indicating an error.
Definition: datetime.h:154
satcat5::datetime::GpsTime to_gps(s64 time)
Convert an internal timestamp into the designated format.
Definition: datetime.cc:184
s64 from_ptp(const satcat5::ptp::Time &time)
Convert the designated format into an internal timestamp.
Definition: datetime.cc:22
s64 from_gps(const satcat5::datetime::GpsTime &time)
Convert the designated format into an internal timestamp.
Definition: datetime.cc:191
Diagnostic logging to UART and/or Ethernet ports.
GPS week-number and time-of-week.
Definition: datetime.h:77
s32 wkn
Week number.
Definition: datetime.h:78
u32 tow
Time of week.
Definition: datetime.h:79
Hardware RTC (e.g., Renesas ISL12082).
Definition: datetime.h:109
u8 ss
Sub-seconds (0-99)
Definition: datetime.h:118
u8 dt
Day-of-month (1-31)
Definition: datetime.h:114
u8 hr
Hour (0-23) + MIL bit.
Definition: datetime.h:115
u8 ct
Century (20 = year 20xx)
Definition: datetime.h:111
u8 sc
Seconds (0-59)
Definition: datetime.h:117
u8 mn
Minutes (0-59)
Definition: datetime.h:116
u8 dw
Day of week (0-6, 0 = Sunday)
Definition: datetime.h:110
void write_to(satcat5::io::Writeable *wr) const
Write legacy binary format (Deprecated).
Definition: datetime.cc:275
bool read_from(satcat5::io::Readable *rd)
Read legacy binary format (Deprecated).
Definition: datetime.cc:290
u32 days_since_epoch() const
Days since 2000 Jan 1 (a Saturday).
Definition: datetime.cc:156
void log_to(satcat5::log::LogBuffer &wr) const
Format as an ISO8601 / RFC3339 timestamp.
Definition: datetime.cc:315
bool validate() const
Are current contents valid?
Definition: datetime.cc:268
u32 msec_since_midnight() const
Milliseconds since midnight (0 - 86.4M).
Definition: datetime.cc:176
unsigned elapsed_usec() const
Elapsed time in microseconds.
Definition: timeref.cc:25
TimeRef * clk
Pointer to the parent time reference.
Definition: timeref.h:49
unsigned increment_msec()
Measure elapsed time in milliseconds, then increment by the returned quantized value.
Definition: timeref.cc:39