mirror of
https://github.com/esphome/esphome.git
synced 2026-10-19 04:32:50 +00:00
[core][time] Add BCD helpers and a shared RTC BCD calendar codec (#20860)
This commit is contained in:
@@ -107,4 +107,37 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
this->time_sync_callback_.call();
|
||||
}
|
||||
|
||||
// Bits of each calendar byte that hold the value, byte 0 lowest; byte 3 is the one-hot weekday.
|
||||
// Packed into one constant so it stays in code rather than RAM on ESP8266.
|
||||
static constexpr uint64_t BCD_CALENDAR_MASKS = 0xFF1F3F7F3F7F7FULL;
|
||||
|
||||
ESPTime decode_bcd_calendar(const uint8_t *raw) {
|
||||
uint8_t value[7];
|
||||
for (size_t i = 0; i < 7; i++)
|
||||
value[i] = bcd_to_uint8(raw[i] & static_cast<uint8_t>(BCD_CALENDAR_MASKS >> (8 * i)));
|
||||
const uint8_t weekday = raw[3] & static_cast<uint8_t>(BCD_CALENDAR_MASKS >> 24);
|
||||
ESPTime time{
|
||||
.second = value[0],
|
||||
.minute = value[1],
|
||||
.hour = value[2],
|
||||
.day_of_week = static_cast<uint8_t>(weekday ? __builtin_ctz(weekday) + 1 : 1),
|
||||
.day_of_month = value[4],
|
||||
.day_of_year = 1,
|
||||
.month = value[5],
|
||||
.year = static_cast<uint16_t>(value[6] + 2000),
|
||||
.is_dst = false,
|
||||
.timestamp = 0,
|
||||
};
|
||||
time.recalc_timestamp_utc(false);
|
||||
return time;
|
||||
}
|
||||
|
||||
void encode_bcd_calendar(const ESPTime &time, uint8_t *raw) {
|
||||
const uint8_t value[7] = {
|
||||
time.second, time.minute, time.hour, 0, time.day_of_month, time.month, static_cast<uint8_t>(time.year % 100)};
|
||||
for (size_t i = 0; i < 7; i++)
|
||||
raw[i] = uint8_to_bcd(value[i]);
|
||||
raw[3] = 1 << (time.day_of_week - 1);
|
||||
}
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
@@ -44,4 +44,10 @@ class RealTimeClock : public PollingComponent {
|
||||
LazyCallbackManager<void()> time_sync_callback_;
|
||||
};
|
||||
|
||||
/// Decode the 7-byte BCD calendar (second, minute, hour, one-hot weekday, day, month, year since 2000)
|
||||
/// shared by Epson RTCs such as the RX8025T, RX8111 and RX8130, and fill in the UTC timestamp.
|
||||
ESPTime decode_bcd_calendar(const uint8_t *raw);
|
||||
/// Encode @p time into that 7-byte layout; @p time must be valid (is_valid()), with the year 2000 to 2099.
|
||||
void encode_bcd_calendar(const ESPTime &time, uint8_t *raw);
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
@@ -1062,6 +1062,11 @@ constexpr std::array<uint8_t, sizeof(T)> decode_value(T val) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Decode a two-digit packed BCD byte (tens in the high nibble) to its value, e.g. 0x59 to 59.
|
||||
constexpr uint8_t bcd_to_uint8(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); }
|
||||
/// Encode a value from 0 to 99 as a two-digit packed BCD byte, e.g. 59 to 0x59.
|
||||
constexpr uint8_t uint8_to_bcd(uint8_t value) { return ((value / 10) << 4) | (value % 10); }
|
||||
|
||||
/// Reverse the order of 8 bits.
|
||||
inline uint8_t reverse_bits(uint8_t x) {
|
||||
x = ((x & 0xAA) >> 1) | ((x & 0x55) << 1);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
static_assert(bcd_to_uint8(0x59) == 59);
|
||||
static_assert(uint8_to_bcd(59) == 0x59);
|
||||
|
||||
TEST(Bcd, RoundTripsEveryTwoDigitValue) {
|
||||
for (uint8_t value = 0; value < 100; value++) {
|
||||
const uint8_t bcd = uint8_to_bcd(value);
|
||||
EXPECT_EQ(bcd >> 4, value / 10);
|
||||
EXPECT_EQ(bcd & 0x0F, value % 10);
|
||||
EXPECT_EQ(bcd_to_uint8(bcd), value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Bcd, DecodesBoundaryBytes) {
|
||||
EXPECT_EQ(bcd_to_uint8(0x00), 0);
|
||||
EXPECT_EQ(bcd_to_uint8(0x09), 9);
|
||||
EXPECT_EQ(bcd_to_uint8(0x10), 10);
|
||||
EXPECT_EQ(bcd_to_uint8(0x99), 99);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,51 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome::time::testing {
|
||||
|
||||
// 2026-04-16 23:59:58, a Thursday (day_of_week 5, one-hot bit 4).
|
||||
static constexpr uint8_t RAW[7] = {0x58, 0x59, 0x23, 0x10, 0x16, 0x04, 0x26};
|
||||
|
||||
TEST(BcdCalendar, DecodesEveryField) {
|
||||
const ESPTime time = decode_bcd_calendar(RAW);
|
||||
EXPECT_EQ(time.second, 58);
|
||||
EXPECT_EQ(time.minute, 59);
|
||||
EXPECT_EQ(time.hour, 23);
|
||||
EXPECT_EQ(time.day_of_week, 5);
|
||||
EXPECT_EQ(time.day_of_month, 16);
|
||||
EXPECT_EQ(time.month, 4);
|
||||
EXPECT_EQ(time.year, 2026);
|
||||
EXPECT_EQ(time.timestamp, 1776383998);
|
||||
EXPECT_TRUE(time.is_valid(true, false));
|
||||
}
|
||||
|
||||
TEST(BcdCalendar, MasksStatusBits) {
|
||||
// Bit 7 of seconds/minutes, the top two bits of hour and day and the top three of month are not time.
|
||||
const uint8_t raw[7] = {0xD8, 0xD9, 0xE3, 0x90, 0xD6, 0xE4, 0x26};
|
||||
const ESPTime time = decode_bcd_calendar(raw);
|
||||
EXPECT_EQ(time.second, 58);
|
||||
EXPECT_EQ(time.minute, 59);
|
||||
EXPECT_EQ(time.hour, 23);
|
||||
EXPECT_EQ(time.day_of_week, 5);
|
||||
EXPECT_EQ(time.day_of_month, 16);
|
||||
EXPECT_EQ(time.month, 4);
|
||||
}
|
||||
|
||||
TEST(BcdCalendar, EmptyWeekdayDecodesAsSunday) {
|
||||
uint8_t raw[7];
|
||||
std::copy(RAW, RAW + 7, raw);
|
||||
raw[3] = 0;
|
||||
EXPECT_EQ(decode_bcd_calendar(raw).day_of_week, 1);
|
||||
}
|
||||
|
||||
TEST(BcdCalendar, EncodeRoundTrips) {
|
||||
uint8_t raw[7];
|
||||
encode_bcd_calendar(decode_bcd_calendar(RAW), raw);
|
||||
for (size_t i = 0; i < 7; i++)
|
||||
EXPECT_EQ(raw[i], RAW[i]) << "byte " << i;
|
||||
}
|
||||
|
||||
} // namespace esphome::time::testing
|
||||
Reference in New Issue
Block a user