Compare commits

...
13 changed files with 1010 additions and 95 deletions

No files matched your search

@@ -17,16 +17,20 @@ void IPAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const n
char buf[network::IP_ADDRESS_BUFFER_SIZE];
ips[0].str_to(buf);
this->publish_state(buf);
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
uint8_t sensor = 0;
for (const auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
if (!ip.is_set())
continue;
if (sensor == ETHERNET_INFO_IP_SENSOR_COUNT)
break;
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
}
#endif
}
void DNSAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
@@ -18,14 +18,19 @@ class IPAddressEthernetInfo final : public Component,
void setup() override;
void dump_config() override;
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
#endif
// EthernetIPStateListener interface
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
protected:
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
// Sized to the highest configured address_N
std::array<text_sensor::TextSensor *, ETHERNET_INFO_IP_SENSOR_COUNT> ip_sensors_{};
#endif
};
class DNSAddressEthernetInfo final : public Component,
@@ -12,6 +12,7 @@ from esphome.types import ConfigType
DEPENDENCIES = ["ethernet"]
ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info")
_request_ip_sensor_slot = cg.slot_counter("ETHERNET_INFO_IP_SENSOR_COUNT")
IPAddressEthernetInfo = ethernet_info_ns.class_(
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.Component
@@ -57,10 +58,13 @@ async def to_code(config: ConfigType) -> None:
if conf := config.get(CONF_IP_ADDRESS):
ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
await cg.register_component(ip_info, config[CONF_IP_ADDRESS])
for x in range(5):
if sensor_conf := conf.get(f"address_{x}"):
sens = await text_sensor.new_text_sensor(sensor_conf)
cg.add(ip_info.add_ip_sensors(x, sens))
indexes = [x for x in range(5) if f"address_{x}" in conf]
# One shared array size: the largest highest index of any entry, so add_ip_sensors(x) stays in range
for _ in range(max(indexes, default=-1) + 1):
_request_ip_sensor_slot(str(ip_info))
for x in indexes:
sens = await text_sensor.new_text_sensor(conf[f"address_{x}"])
cg.add(ip_info.add_ip_sensors(x, sens))
if conf := config.get(CONF_DNS_ADDRESS):
dns_info = await text_sensor.new_text_sensor(conf)
await cg.register_component(dns_info, conf)
+194 -45
View File
@@ -9,16 +9,33 @@ ESPHOME_LOG_TAG(TAG, "rx8111");
// Indexed by the TSEL value stored in timer_clock_
PROGMEM_STRING_TABLE(TimerClockNames, "4096 Hz", "64 Hz", "1 Hz", "1/60 Hz");
PROGMEM_STRING_TABLE(FoutNames, "32768 Hz", "1024 Hz", "1 Hz", "off");
PROGMEM_STRING_TABLE(EvinPullNames, "none", "pullup_500k", "pullup_1m", "pullup_10m", "pulldown_500k");
PROGMEM_STRING_TABLE(BootFailureNames, "RX8111 battery configuration failed", "RX8111 FOUT configuration failed",
"RX8111 EVIN configuration failed", ESP_LOG_MSG_COMM_FAIL, ESP_LOG_MSG_COMM_FAIL);
static constexpr uint8_t RX8111_REG_SEC = 0x10;
static constexpr uint8_t RX8111_REG_MIN_ALARM = 0x17;
static constexpr uint8_t RX8111_REG_HOUR_ALARM = 0x18;
static constexpr uint8_t RX8111_REG_DAY_ALARM = 0x19;
static constexpr uint8_t RX8111_REG_EXTENSION = 0x1D;
static constexpr uint8_t RX8111_REG_FLAG = 0x1E;
static constexpr uint8_t RX8111_REG_TIMER0 = 0x1A;
static constexpr uint8_t RX8111_REG_CONTROL = 0x1F;
static constexpr uint8_t RX8111_REG_SEC_ALARM = 0x2C;
static constexpr uint8_t RX8111_REG_TIMER_CONTROL = 0x2D;
static constexpr uint8_t RX8111_REG_POWER_SWITCH = 0x32;
static constexpr uint8_t RX8111_REG_TIMESTAMP_CONTROL = 0x34;
static constexpr uint8_t RX8111_TIMESTAMP_EISEL = 0x04;
static constexpr uint8_t RX8111_REG_EVIN = 0x2B;
static constexpr uint8_t RX8111_EXTENSION_ETS = 0x04;
static constexpr uint8_t RX8111_CONTROL_EIE = 0x04;
static constexpr uint8_t RX8111_FLAG_EVF = 0x04;
static constexpr uint8_t RX8111_POWER_CHGEN = 0x80;
static constexpr uint8_t RX8111_POWER_INIEN = 0x40;
static constexpr uint8_t RX8111_POWER_SWSEL = 0x0C;
static constexpr uint8_t RX8111_POWER_VDD_ONLY = 0x08;
static constexpr uint8_t RX8111_FOUT_FSEL = 0xC0;
static constexpr uint8_t RX8111_TIMER_TMPIN = 0x02;
static constexpr uint8_t RX8111_TIMER_MODE = 0x0F; // TBKON, TBKE, TMPIN, TSTP
static constexpr uint8_t RX8111_ALARM_DISABLED = 0x80;
static constexpr uint8_t RX8111_EXTENSION_WADA = 0x08;
static constexpr uint8_t RX8111_CONTROL_AIE = 0x08;
@@ -36,17 +53,121 @@ static constexpr uint8_t RX8111_KEEP_EVENT_FLAGS = 0x3C;
static constexpr uint32_t RX8111_EVENT_INTERVAL_ID = 0;
static constexpr uint32_t RX8111_BOOT_ALARM_RETRY_ID = 1;
static constexpr uint32_t RX8111_BOOT_TIMER_RETRY_ID = 2;
static constexpr uint32_t RX8111_BOOT_BATTERY_RETRY_ID = 3;
static constexpr uint32_t RX8111_BOOT_FOUT_RETRY_ID = 4;
static constexpr uint32_t RX8111_BOOT_EVENT_RETRY_ID = 5;
static constexpr uint8_t RX8111_BOOT_MAX_ATTEMPTS = 3;
void RX8111Component::setup() {
if (this->battery_configured_)
this->configure_boot_battery_();
if (this->fout_configured_)
this->configure_boot_fout_();
if (this->event_configured_)
this->configure_boot_event_();
this->configure_boot_alarm_();
this->configure_boot_timer_();
this->read_time();
if (this->alarm_enabled_ || this->watching_()) {
if (this->watching_()) {
this->start_event_polling_();
}
}
void RX8111Component::configure_boot_event_() {
const bool success = this->configure_event_();
if (success) {
this->event_boot_pending_ = false;
if (this->watching_())
this->start_event_polling_();
} else {
this->event_boot_pending_ = true;
}
this->boot_config_result_(BOOT_EVENT, success);
}
bool RX8111Component::configure_event_() {
// An explicit disable affects only /INT; leave retained capture settings untouched.
if (!this->event_enabled_)
return this->update_register(RX8111_REG_CONTROL, RX8111_CONTROL_EIE, 0);
// ETS gates EVIN detection, not just timestamp publication. Preserve all other
// extension bits, including FOUT, alarm and timer settings, and retained records.
return this->update_register(RX8111_REG_EVIN, 0xFC, this->evin_settings_ & 0xFC) &&
this->update_register(RX8111_REG_TIMESTAMP_CONTROL, RX8111_TIMESTAMP_EISEL, 0) &&
this->update_register(RX8111_REG_EXTENSION, RX8111_EXTENSION_ETS, RX8111_EXTENSION_ETS) &&
this->update_register(RX8111_REG_CONTROL, RX8111_CONTROL_EIE, RX8111_CONTROL_EIE);
}
bool RX8111Component::clear_event_flag() {
// FLAG is write-zero-to-clear; ones preserve other events, even if they latch during the write.
if (!this->write_byte(RX8111_REG_FLAG, static_cast<uint8_t>(~RX8111_FLAG_EVF))) {
this->set_warning_(WARN_EVENT_FLAG_CLEAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->event_flag_seen_ = false;
this->clear_warning_(WARN_EVENT_FLAG_CLEAR);
return true;
}
void RX8111Component::configure_boot_battery_() {
// Own SWSEL as well as INIEN/CHGEN: 10 selects VDD when automatic switching is disabled
// and is legal when enabled (unlike 11). Preserve SMPT and reserved bits.
const uint8_t value = RX8111_POWER_VDD_ONLY | (this->battery_backup_ ? RX8111_POWER_INIEN : 0) |
(this->battery_backup_ && this->battery_charging_ ? RX8111_POWER_CHGEN : 0);
this->boot_config_result_(BOOT_BATTERY,
this->update_register(RX8111_REG_POWER_SWITCH,
RX8111_POWER_INIEN | RX8111_POWER_CHGEN | RX8111_POWER_SWSEL, value));
}
void RX8111Component::configure_boot_fout_() {
// TMPIN routes timer interrupts to FOUT instead of /INT; do not silently change that routing.
uint8_t timer_control;
bool success = this->read_byte(RX8111_REG_TIMER_CONTROL, &timer_control);
if (success && this->fout_frequency_ != 3 && (timer_control & RX8111_TIMER_TMPIN)) {
ESP_LOGW(TAG, "FOUT is routed to the wake-up timer (TMPIN); cannot configure clock output");
success = false;
} else if (success) {
success =
this->update_register(RX8111_REG_EXTENSION, RX8111_FOUT_FSEL, static_cast<uint8_t>(this->fout_frequency_ << 6));
}
this->boot_config_result_(BOOT_FOUT, success);
}
// A timer start clears TMPIN, which may be what failed FOUT; give it a fresh set of attempts.
void RX8111Component::retry_fout_after_timer_start_() {
if (!this->fout_configured_ || (this->warnings_ & WARN_FOUT) == 0)
return;
this->cancel_timeout(RX8111_BOOT_FOUT_RETRY_ID);
this->boot_attempts_[BOOT_FOUT] = 0;
this->configure_boot_fout_();
}
void RX8111Component::boot_config_result_(BootFeature feature, bool success) {
struct BootConfig {
uint32_t retry_id;
void (RX8111Component::*configure)();
uint16_t warning;
};
static constexpr BootConfig CONFIGS[BOOT_FEATURE_COUNT] PROGMEM = {
{RX8111_BOOT_BATTERY_RETRY_ID, &RX8111Component::configure_boot_battery_, WARN_BATTERY},
{RX8111_BOOT_FOUT_RETRY_ID, &RX8111Component::configure_boot_fout_, WARN_FOUT},
{RX8111_BOOT_EVENT_RETRY_ID, &RX8111Component::configure_boot_event_, WARN_EVENT},
{RX8111_BOOT_ALARM_RETRY_ID, &RX8111Component::configure_boot_alarm_, WARN_ALARM},
{RX8111_BOOT_TIMER_RETRY_ID, &RX8111Component::configure_boot_timer_, WARN_TIMER},
};
BootConfig config;
progmem_memcpy(&config, &CONFIGS[feature], sizeof(config));
const auto attempts = ++this->boot_attempts_[feature];
if (success) {
this->clear_warning_(config.warning);
return;
}
this->set_warning_(config.warning, BootFailureNames::get_log_str(feature, BootFailureNames::LAST_INDEX));
if (attempts < RX8111_BOOT_MAX_ATTEMPTS) {
this->set_timeout(config.retry_id, this->boot_retry_delay_ms_,
[this, configure = config.configure]() { (this->*configure)(); });
}
}
void RX8111Component::configure_boot_alarm_() {
bool success = true;
if (this->alarm_enabled_) {
@@ -74,7 +195,7 @@ void RX8111Component::configure_boot_alarm_() {
} else if (this->alarm_explicitly_configured_) {
success = this->set_alarm_interrupt_(false);
}
this->boot_config_result_(true, success);
this->boot_config_result_(BOOT_ALARM, success);
}
void RX8111Component::configure_boot_timer_() {
@@ -92,29 +213,10 @@ void RX8111Component::configure_boot_timer_() {
this->timer_retained_event_pending_ = true;
this->start_event_polling_();
}
if (success && start_timer)
this->retry_fout_after_timer_start_();
}
this->boot_config_result_(false, success);
}
void RX8111Component::boot_config_result_(bool alarm, bool success) {
auto &attempts = alarm ? this->boot_alarm_attempts_ : this->boot_timer_attempts_;
const uint8_t warning = alarm ? WARN_ALARM : WARN_TIMER;
++attempts;
if (success) {
this->clear_warning_(warning);
return;
}
this->set_warning_(warning, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (attempts >= RX8111_BOOT_MAX_ATTEMPTS)
return;
this->set_timeout(alarm ? RX8111_BOOT_ALARM_RETRY_ID : RX8111_BOOT_TIMER_RETRY_ID, this->boot_retry_delay_ms_,
[this, alarm]() {
if (alarm) {
this->configure_boot_alarm_();
} else {
this->configure_boot_timer_();
}
});
this->boot_config_result_(BOOT_TIMER, success);
}
// A runtime action supersedes pending boot programming of the same feature
@@ -144,12 +246,12 @@ void RX8111Component::update() {
this->check_event_flags_();
}
void RX8111Component::set_warning_(uint8_t bit, const LogString *message) {
void RX8111Component::set_warning_(uint16_t bit, const LogString *message) {
this->warnings_ |= bit;
this->status_set_warning(message);
}
void RX8111Component::clear_warning_(uint8_t bit) {
void RX8111Component::clear_warning_(uint16_t bit) {
if ((this->warnings_ & bit) == 0)
return;
this->warnings_ &= ~bit;
@@ -160,6 +262,23 @@ void RX8111Component::clear_warning_(uint8_t bit) {
void RX8111Component::dump_config() {
ESP_LOGCONFIG(TAG, "RX8111:");
LOG_I2C_DEVICE(this);
if (this->battery_configured_) {
ESP_LOGCONFIG(TAG, " Battery backup: %s, charging: %s", ONOFF(this->battery_backup_),
ONOFF(this->battery_charging_));
}
if (this->fout_configured_) {
ESP_LOGCONFIG(TAG, " FOUT: %s", LOG_STR_ARG(FoutNames::get_log_str(this->fout_frequency_, FoutNames::LAST_INDEX)));
}
if (this->event_configured_) {
ESP_LOGCONFIG(TAG, " EVIN interrupt: %s", ONOFF(this->event_enabled_));
if (this->event_enabled_) {
ESP_LOGCONFIG(
TAG, " EVIN: %s, filter: %u, pull: %s",
(this->evin_settings_ & 0x80) ? LOG_STR_LITERAL("high") : LOG_STR_LITERAL("low"),
(this->evin_settings_ >> 5) & 3,
LOG_STR_ARG(EvinPullNames::get_log_str((this->evin_settings_ >> 2) & 7, EvinPullNames::LAST_INDEX)));
}
}
ESP_LOGCONFIG(TAG,
" Alarm interrupt: %s\n"
" Wake-up timer: %s",
@@ -181,12 +300,20 @@ void RX8111Component::read_time() {
}
bool RX8111Component::read_calendar_(ESPTime &rtc_time) {
uint8_t flags;
if (!this->read_byte(RX8111_REG_FLAG, &flags)) {
if (this->time_write_invalid_) {
this->set_warning_(WARN_CALENDAR, LOG_STR("RTC write incomplete; write trusted time again"));
return false;
}
uint8_t regs[2]; // Adjacent FLAG and CONTROL registers.
if (!this->read_bytes(RX8111_REG_FLAG, regs, sizeof(regs))) {
this->set_warning_(WARN_CALENDAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
if (flags & (RX8111_FLAG_VLF | RX8111_FLAG_XST | RX8111_FLAG_POR)) {
if (regs[1] & RX8111_CTRL_STOP) {
this->set_warning_(WARN_CALENDAR, LOG_STR("RTC counter stopped; not synchronizing"));
return false;
}
if (regs[0] & (RX8111_FLAG_VLF | RX8111_FLAG_XST | RX8111_FLAG_POR)) {
ESP_LOGW(TAG, "RTC time invalidated by power loss or oscillator stop; write trusted time");
this->set_warning_(WARN_CALENDAR, LOG_STR("RTC time invalidated; write trusted time"));
return false;
@@ -199,12 +326,12 @@ bool RX8111Component::read_calendar_(ESPTime &rtc_time) {
return false;
}
// decode_bcd_calendar() masks the same bits; the check here catches a corrupted register
static constexpr uint8_t CALENDAR_MASKS[7] = {0x7F, 0x7F, 0x3F, 0x00, 0x3F, 0x1F, 0xFF};
static constexpr uint8_t CALENDAR_MASKS[7] PROGMEM = {0x7F, 0x7F, 0x3F, 0x00, 0x3F, 0x1F, 0xFF};
bool valid = true;
for (size_t i = 0; i < sizeof(buf); i++) {
if (i == 3)
continue; // Weekday uses one-hot encoding, not BCD.
buf[i] &= CALENDAR_MASKS[i];
buf[i] &= progmem_read_byte(&CALENDAR_MASKS[i]);
valid = valid && (buf[i] & 0x0F) <= 9 && (buf[i] >> 4) <= 9;
}
const uint8_t onehot = buf[3];
@@ -236,23 +363,31 @@ void RX8111Component::write_time() {
uint8_t buf[7];
time::encode_bcd_calendar<time::EPSON_BCD_CALENDAR>(now, buf);
const bool written = this->write_bytes(RX8111_REG_SEC, buf, sizeof(buf));
// Always try to restart the counter, even after a failed calendar write.
const bool restarted = this->set_stop_bit_(false);
if (!written || !restarted) {
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
this->time_write_invalid_ = true;
const bool calendar_written = this->write_bytes(RX8111_REG_SEC, buf, sizeof(buf));
if (!this->finish_time_write_(calendar_written))
return;
}
if (!this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS)) {
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->clear_warning_(WARN_WRITE_TIME);
this->clear_warning_(WARN_CALENDAR);
ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
now.minute, now.second);
}
bool RX8111Component::finish_time_write_(bool calendar_written) {
// STOP is the battery-backed invalidity marker, including across host resets.
// Never release a partial or delayed snapshot; only a fresh trusted write can restart it.
if (!calendar_written) {
this->set_warning_(WARN_WRITE_TIME, LOG_STR("RTC write incomplete; write trusted time again"));
return false;
}
if (!this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS) || !this->set_stop_bit_(false)) {
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return false;
}
this->time_write_invalid_ = false;
this->clear_warning_(WARN_WRITE_TIME);
this->clear_warning_(WARN_CALENDAR);
return true;
}
bool RX8111Component::set_stop_bit_(bool stop) {
return this->update_register(RX8111_REG_CONTROL, RX8111_CTRL_STOP, stop ? RX8111_CTRL_STOP : 0);
}
@@ -383,6 +518,11 @@ bool RX8111Component::configure_timer_(bool clear_stale_flag) {
if (!this->disable_timer_(extension, control))
return false;
// Explicit starts own the timer mode: count in both power modes, do not pause,
// and route interrupt pulses to /INT rather than FOUT. Leave reserved bits alone.
if (!this->update_register(RX8111_REG_TIMER_CONTROL, RX8111_TIMER_MODE, 0))
return false;
const uint8_t count[] = {static_cast<uint8_t>(this->timer_count_), static_cast<uint8_t>(this->timer_count_ >> 8),
static_cast<uint8_t>(this->timer_count_ >> 16)};
if (!this->write_bytes(RX8111_REG_TIMER0, count, sizeof(count)) ||
@@ -414,6 +554,7 @@ bool RX8111Component::start_timer() {
this->timer_enabled_ = true;
this->timer_retained_event_pending_ = false;
this->start_event_polling_();
this->retry_fout_after_timer_start_();
return true;
}
@@ -445,6 +586,12 @@ bool RX8111Component::poll_event_flags_() {
if (!this->read_byte(RX8111_REG_FLAG, &flags))
return false;
this->clear_warning_(WARN_EVENT_POLL);
// EVF is not cleared here: users may use it to hold /INT asserted across sleep.
const bool event_active = (flags & RX8111_FLAG_EVF) != 0;
const bool event_rising =
event_active && !this->event_flag_seen_ && this->event_enabled_ && !this->event_boot_pending_;
if (this->event_enabled_ && !this->event_boot_pending_)
this->event_flag_seen_ = event_active;
const bool alarm_active = (flags & RX8111_FLAG_AF) != 0;
const bool timer_active = (flags & RX8111_FLAG_TF) != 0;
// alarm_armed_ is only ever set while the alarm is enabled
@@ -466,6 +613,8 @@ bool RX8111Component::poll_event_flags_() {
// An alarm callback may stop the timer before its callback is delivered.
if (timer_rising && this->timer_flag_seen_ && (this->timer_enabled_ || deliver_stopped_event))
this->timer_callbacks_.call();
if (event_rising && this->event_flag_seen_)
this->event_callbacks_.call();
return true;
}
+65 -18
View File
@@ -29,6 +29,28 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
void set_timer_enabled(bool enabled) { this->timer_enabled_ = enabled; }
void set_timer_configured(bool configured) { this->timer_configured_ = configured; }
void set_event_poll_interval(uint32_t interval) { this->event_poll_interval_ = interval; }
void set_battery_backup(bool enabled, bool charging) {
this->battery_configured_ = true;
this->battery_backup_ = enabled;
this->battery_charging_ = charging;
}
void set_fout_frequency(uint8_t frequency) {
this->fout_configured_ = true;
this->fout_frequency_ = frequency;
}
void set_event_config(uint8_t settings) {
this->event_configured_ = true;
this->event_enabled_ = true;
this->evin_settings_ = settings;
}
void set_event_disabled() {
this->event_configured_ = true;
this->event_enabled_ = false;
}
template<typename F> void add_on_event_callback(F &&callback) {
this->event_callbacks_.add(std::forward<F>(callback));
}
bool clear_event_flag();
template<typename F> void add_on_alarm_callback(F &&callback) {
this->alarm_callbacks_.add(std::forward<F>(callback));
}
@@ -51,7 +73,7 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
static constexpr uint8_t ALARM_FIELD_UNSET = 0xFF;
// One bit per fault source, so a recovery in one path cannot hide another path's fault
enum Warning : uint8_t {
enum Warning : uint16_t {
WARN_CALENDAR = 1 << 0,
WARN_EVENT_POLL = 1 << 1,
WARN_ALARM = 1 << 2,
@@ -59,13 +81,28 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
WARN_WRITE_TIME = 1 << 4,
WARN_ALARM_FLAG_CLEAR = 1 << 5,
WARN_TIMER_FLAG_CLEAR = 1 << 6,
WARN_BATTERY = 1 << 7,
WARN_FOUT = 1 << 8,
WARN_EVENT = 1 << 9,
WARN_EVENT_FLAG_CLEAR = 1 << 10,
};
bool set_stop_bit_(bool stop);
bool finish_time_write_(bool calendar_written);
bool read_calendar_(ESPTime &rtc_time);
void set_warning_(uint8_t bit, const LogString *message);
void clear_warning_(uint8_t bit);
bool watching_() const { return this->alarm_armed_ || this->timer_enabled_ || this->timer_retained_event_pending_; }
void set_warning_(uint16_t bit, const LogString *message);
void clear_warning_(uint16_t bit);
void configure_boot_battery_();
void configure_boot_fout_();
void retry_fout_after_timer_start_();
enum BootFeature : uint8_t { BOOT_BATTERY, BOOT_FOUT, BOOT_EVENT, BOOT_ALARM, BOOT_TIMER, BOOT_FEATURE_COUNT };
void boot_config_result_(BootFeature feature, bool success);
bool watching_() const {
return this->alarm_armed_ || this->timer_enabled_ || this->timer_retained_event_pending_ ||
(this->event_enabled_ && !this->event_boot_pending_ && !this->event_callbacks_.empty());
}
void configure_boot_event_();
bool configure_event_();
void check_event_flags_();
bool program_alarm_(uint8_t second, uint8_t minute, uint8_t hour, uint8_t weekday, uint8_t day, bool boot = false);
bool set_alarm_interrupt_(bool enabled);
@@ -75,33 +112,43 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
bool poll_event_flags_();
void configure_boot_alarm_();
void configure_boot_timer_();
void boot_config_result_(bool alarm, bool success);
void cancel_boot_retry_(bool alarm);
LazyCallbackManager<void()> alarm_callbacks_;
LazyCallbackManager<void()> timer_callbacks_;
LazyCallbackManager<void()> event_callbacks_;
uint32_t timer_count_{0};
uint32_t event_poll_interval_{1000};
uint32_t boot_retry_delay_ms_{BOOT_RETRY_DELAY_MS};
uint8_t warnings_{0};
uint16_t warnings_{0};
uint8_t boot_attempts_[BOOT_FEATURE_COUNT]{};
uint8_t evin_settings_{0};
uint8_t fout_frequency_{3};
uint8_t timer_clock_{2}; // 1 Hz
uint8_t boot_alarm_attempts_{0};
uint8_t boot_timer_attempts_{0};
uint8_t alarm_second_{ALARM_FIELD_UNSET};
uint8_t alarm_minute_{ALARM_FIELD_UNSET};
uint8_t alarm_hour_{ALARM_FIELD_UNSET};
uint8_t alarm_weekday_{ALARM_FIELD_UNSET};
uint8_t alarm_day_{ALARM_FIELD_UNSET};
bool timer_enabled_{false};
bool timer_configured_{false};
bool timer_flag_seen_{false};
bool timer_retained_event_pending_{false};
bool event_polling_started_{false};
bool boot_alarm_event_seen_{false};
bool alarm_enabled_{false};
bool alarm_explicitly_configured_{false};
bool alarm_flag_seen_{false};
bool alarm_armed_{false};
bool time_write_invalid_ : 1 {false};
bool battery_configured_ : 1 {false};
bool battery_backup_ : 1 {false};
bool battery_charging_ : 1 {false};
bool fout_configured_ : 1 {false};
bool event_configured_ : 1 {false};
bool event_enabled_ : 1 {false};
bool event_flag_seen_ : 1 {false};
bool event_boot_pending_ : 1 {false};
bool timer_enabled_ : 1 {false};
bool timer_configured_ : 1 {false};
bool timer_flag_seen_ : 1 {false};
bool timer_retained_event_pending_ : 1 {false};
bool event_polling_started_ : 1 {false};
bool boot_alarm_event_seen_ : 1 {false};
bool alarm_enabled_ : 1 {false};
bool alarm_explicitly_configured_ : 1 {false};
bool alarm_flag_seen_ : 1 {false};
bool alarm_armed_ : 1 {false};
};
} // namespace esphome::rx8111
+48
View File
@@ -10,6 +10,7 @@ from esphome.const import (
CONF_HOUR,
CONF_ID,
CONF_MINUTE,
CONF_ON_EVENT,
CONF_SECOND,
)
from esphome.types import ConfigType
@@ -20,6 +21,14 @@ DEPENDENCIES = ["i2c"]
rx8111_ns = cg.esphome_ns.namespace("rx8111")
RX8111Component = rx8111_ns.class_("RX8111Component", time.RealTimeClock, i2c.I2CDevice)
TIMER_CLOCKS = {"4096hz": 0, "64hz": 1, "1hz": 2, "1_60hz": 3}
FOUT_FREQUENCIES = {"32768hz": 0, "1024hz": 1, "1hz": 2, "off": 3}
EVIN_PULLS = {
"none": 0,
"pullup_500k": 0x04,
"pullup_1m": 0x08,
"pullup_10m": 0x0C,
"pulldown_500k": 0x10,
}
# set_alarm() ignores a field set to this value
ALARM_FIELD_UNSET = 0xFF
@@ -36,6 +45,13 @@ CONF_TIMER_ENABLED = "timer_enabled"
CONF_ON_TIMER = "on_timer"
CONF_WEEKDAY = "weekday"
CONF_EVENT_POLL_INTERVAL = "event_poll_interval"
CONF_BATTERY_BACKUP = "battery_backup"
CONF_BATTERY_CHARGING = "battery_charging"
CONF_FOUT_FREQUENCY = "fout_frequency"
CONF_EVENT_ENABLED = "event_enabled"
CONF_EVENT_LEVEL = "event_level"
CONF_EVIN_FILTER = "evin_filter"
CONF_EVIN_PULL = "evin_pull"
_ALARM_FIELDS = (
CONF_ALARM_SECOND,
@@ -47,6 +63,7 @@ _ALARM_FIELDS = (
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_ALARM, "add_on_alarm_callback"),
automation.CallbackAutomation(CONF_ON_TIMER, "add_on_timer_callback"),
automation.CallbackAutomation(CONF_ON_EVENT, "add_on_event_callback"),
)
@@ -62,6 +79,16 @@ def _validate_alarm_config(config: ConfigType) -> ConfigType:
config.get(CONF_TIMER_ENABLED) or CONF_ON_TIMER in config
):
raise cv.Invalid("timer_enabled or on_timer requires timer_count")
if CONF_BATTERY_CHARGING in config and CONF_BATTERY_BACKUP not in config:
raise cv.Invalid("battery_charging requires an explicit battery_backup setting")
if config.get(CONF_BATTERY_CHARGING) and not config.get(CONF_BATTERY_BACKUP):
raise cv.Invalid("battery_charging: true requires battery_backup: true")
if CONF_ON_EVENT in config and not config.get(CONF_EVENT_ENABLED):
raise cv.Invalid("on_event requires event_enabled: true")
if any(
k in config for k in (CONF_EVENT_LEVEL, CONF_EVIN_FILTER, CONF_EVIN_PULL)
) and not config.get(CONF_EVENT_ENABLED):
raise cv.Invalid("EVIN settings require event_enabled: true")
return config
@@ -86,6 +113,16 @@ CONFIG_SCHEMA = cv.All(
TIMER_CLOCKS, lower=True
),
cv.Optional(CONF_TIMER_ENABLED): cv.boolean,
cv.Optional(CONF_BATTERY_BACKUP): cv.boolean,
cv.Optional(CONF_BATTERY_CHARGING): cv.boolean,
cv.Optional(CONF_FOUT_FREQUENCY): cv.enum(FOUT_FREQUENCIES, lower=True),
cv.Optional(CONF_EVENT_ENABLED): cv.boolean,
cv.Optional(CONF_EVENT_LEVEL): cv.enum(
{"low": 0, "high": 0x80}, lower=True
),
cv.Optional(CONF_EVIN_FILTER): cv.int_range(min=0, max=3),
cv.Optional(CONF_EVIN_PULL): cv.enum(EVIN_PULLS, lower=True),
cv.Optional(CONF_ON_EVENT): automation.validate_automation({}),
cv.Optional(CONF_ON_TIMER): automation.validate_automation({}),
# Below 500 ms a scheduler interval costs more than it saves
cv.Optional(CONF_EVENT_POLL_INTERVAL, default="1s"): cv.All(
@@ -107,6 +144,7 @@ for _method in (
"read_time",
"clear_alarm_flag",
"clear_timer_flag",
"clear_event_flag",
"unset_alarm",
"stop_timer",
"start_timer",
@@ -184,6 +222,16 @@ async def to_code(config: ConfigType) -> None:
var = await i2c.new_i2c_component(config)
await time.register_time(var, config)
cg.add(var.set_event_poll_interval(config[CONF_EVENT_POLL_INTERVAL]))
if (backup := config.get(CONF_BATTERY_BACKUP)) is not None:
cg.add(var.set_battery_backup(backup, config.get(CONF_BATTERY_CHARGING, False)))
cg.add_optional(var.set_fout_frequency, config.get(CONF_FOUT_FREQUENCY))
if config.get(CONF_EVENT_ENABLED):
level = config[CONF_EVENT_LEVEL].enum_value if CONF_EVENT_LEVEL in config else 0
pull = config[CONF_EVIN_PULL].enum_value if CONF_EVIN_PULL in config else 0
filter_bits = config.get(CONF_EVIN_FILTER, 0) << 5
cg.add(var.set_event_config(level | filter_bits | pull))
elif CONF_EVENT_ENABLED in config:
cg.add(var.set_event_disabled())
cg.add_optional(var.set_alarm_second, config.get(CONF_ALARM_SECOND))
cg.add_optional(var.set_alarm_minute, config.get(CONF_ALARM_MINUTE))
cg.add_optional(var.set_alarm_hour, config.get(CONF_ALARM_HOUR))
+8 -4
View File
@@ -16,6 +16,7 @@ from esphome.types import ConfigType
DEPENDENCIES = ["wifi"]
wifi_info_ns = cg.esphome_ns.namespace("wifi_info")
_request_ip_sensor_slot = cg.slot_counter("WIFI_INFO_IP_SENSOR_COUNT")
IPAddressWiFiInfo = wifi_info_ns.class_(
"IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component
)
@@ -110,7 +111,10 @@ async def to_code(config: ConfigType) -> None:
if conf := config.get(CONF_IP_ADDRESS):
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
await cg.register_component(wifi_info, config[CONF_IP_ADDRESS])
for x in range(5):
if sensor_conf := conf.get(f"address_{x}"):
sens = await text_sensor.new_text_sensor(sensor_conf)
cg.add(wifi_info.add_ip_sensors(x, sens))
indexes = [x for x in range(5) if f"address_{x}" in conf]
# One shared array size: the largest highest index of any entry, so add_ip_sensors(x) stays in range
for _ in range(max(indexes, default=-1) + 1):
_request_ip_sensor_slot(str(wifi_info))
for x in indexes:
sens = await text_sensor.new_text_sensor(conf[f"address_{x}"])
cg.add(wifi_info.add_ip_sensors(x, sens))
@@ -26,16 +26,20 @@ void IPAddressWiFiInfo::on_ip_state(const network::IPAddresses &ips, const netwo
char buf[network::IP_ADDRESS_BUFFER_SIZE];
ips[0].str_to(buf);
this->publish_state(buf);
#ifdef WIFI_INFO_IP_SENSOR_COUNT
uint8_t sensor = 0;
for (const auto &ip : ips) {
if (ip.is_set()) {
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
if (!ip.is_set())
continue;
if (sensor == WIFI_INFO_IP_SENSOR_COUNT)
break;
if (this->ip_sensors_[sensor] != nullptr) {
ip.str_to(buf);
this->ip_sensors_[sensor]->publish_state(buf);
}
sensor++;
}
#endif
}
/*********************
@@ -16,14 +16,19 @@ class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor
public:
void setup() override;
void dump_config() override;
#ifdef WIFI_INFO_IP_SENSOR_COUNT
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
#endif
// WiFiIPStateListener interface
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
protected:
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
#ifdef WIFI_INFO_IP_SENSOR_COUNT
// Sized to the highest configured address_N
std::array<text_sensor::TextSensor *, WIFI_INFO_IP_SENSOR_COUNT> ip_sensors_{};
#endif
};
class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener {
+2
View File
@@ -167,6 +167,7 @@
#define LV_USE_LOG 1
#define USE_MDNS
#define USE_MDNS_STORE_SERVICES
#define ETHERNET_INFO_IP_SENSOR_COUNT 5
#define GPS_LISTENER_COUNT 1
#define HDMI_CEC_MESSAGE_CALLBACK_COUNT 2
#define I2C_PORT_FREQUENCY_COUNT 1
@@ -194,6 +195,7 @@
#define PYLONTECH_SENSOR_CELL_COUNT 2
#define REMOTE_BASE_DUMPER_COUNT 1
#define REMOTE_BASE_LISTENER_COUNT 1
#define WIFI_INFO_IP_SENSOR_COUNT 5
#define USE_REMOTE_PROTOCOL_ABBWELCOME
#define USE_REMOTE_PROTOCOL_AEHA
#define USE_REMOTE_PROTOCOL_AOK
@@ -1,5 +1,7 @@
"""Validate RX8111 alarm and timer options before generating firmware."""
from unittest import mock
import pytest
from esphome.components.rx8111.time import (
@@ -8,6 +10,8 @@ from esphome.components.rx8111.time import (
CONF_ALARM_DAY,
CONF_ALARM_ENABLED,
CONF_ALARM_WEEKDAY,
CONF_BATTERY_BACKUP,
CONF_BATTERY_CHARGING,
CONF_DAY,
CONF_ON_ALARM,
CONF_ON_TIMER,
@@ -15,6 +19,7 @@ from esphome.components.rx8111.time import (
CONF_TIMER_COUNT,
CONF_TIMER_ENABLED,
CONF_WEEKDAY,
FOUT_FREQUENCIES,
_validate_alarm_config,
_validate_alarm_options,
_validate_set_alarm,
@@ -74,3 +79,104 @@ def test_on_alarm_requires_enabled_alarm() -> None:
with pytest.raises(cv.Invalid, match="on_alarm"):
_validate_alarm_config({CONF_ON_ALARM: [{}]})
assert _validate_alarm_config({CONF_ON_ALARM: [{}], CONF_ALARM_ENABLED: True})
def test_charging_requires_explicit_backup() -> None:
for config in (
{CONF_BATTERY_CHARGING: True},
{CONF_BATTERY_CHARGING: False},
{CONF_BATTERY_BACKUP: False, CONF_BATTERY_CHARGING: True},
):
with pytest.raises(cv.Invalid, match="battery_backup"):
_validate_alarm_config(config)
assert _validate_alarm_config(
{CONF_BATTERY_BACKUP: True, CONF_BATTERY_CHARGING: True}
)
@pytest.mark.parametrize("backup", [False, True])
def test_charging_disable_requires_and_accepts_explicit_backup(backup: bool) -> None:
config = {CONF_BATTERY_BACKUP: backup, CONF_BATTERY_CHARGING: False}
assert _validate_alarm_config(config) == config
@pytest.mark.parametrize("value", ["32768hz", "1024hz", "1hz", "off"])
def test_fout_frequencies(value: str) -> None:
assert (
cv.enum(FOUT_FREQUENCIES, lower=True)(value).enum_value
== FOUT_FREQUENCIES[value]
)
def test_invalid_fout_frequency() -> None:
with pytest.raises(cv.Invalid):
cv.enum(FOUT_FREQUENCIES, lower=True)("100hz")
def test_event_configuration_requires_enablement() -> None:
from esphome.components.rx8111.time import (
CONF_EVENT_ENABLED,
CONF_EVENT_LEVEL,
CONF_EVIN_FILTER,
CONF_ON_EVENT,
)
for config in (
{CONF_ON_EVENT: [{}]},
{CONF_EVENT_LEVEL: 0},
{CONF_EVIN_FILTER: 2},
):
with pytest.raises(cv.Invalid):
_validate_alarm_config(config)
assert _validate_alarm_config({CONF_EVENT_ENABLED: True, CONF_ON_EVENT: [{}]})
assert _validate_alarm_config({CONF_EVENT_ENABLED: False})
@pytest.mark.parametrize("option", ["event_timestamp", "timestamp_record_mode"])
def test_timestamp_options_are_not_in_time_platform(option: str) -> None:
from esphome.components.rx8111.time import CONFIG_SCHEMA
with pytest.raises(cv.Invalid):
CONFIG_SCHEMA({option: "latest"})
@pytest.mark.asyncio
@pytest.mark.parametrize(
("options", "settings"),
[
({"event_enabled": True}, 0),
(
{
"event_enabled": True,
"event_level": "high",
"evin_filter": 2,
"evin_pull": "pullup_500k",
},
0xC4,
),
({"event_enabled": False}, None),
],
)
async def test_event_codegen(options: dict, settings: int | None) -> None:
from esphome.components.rx8111 import time as rx8111
config = rx8111.CONFIG_SCHEMA(options)
var = mock.MagicMock()
with (
mock.patch.object(
rx8111.i2c, "new_i2c_component", mock.AsyncMock(return_value=var)
),
mock.patch.object(rx8111.time, "register_time", mock.AsyncMock()),
mock.patch.object(rx8111.cg, "add"),
mock.patch.object(rx8111.cg, "add_optional"),
mock.patch.object(
rx8111.automation, "build_callback_automations", mock.AsyncMock()
),
):
await rx8111.to_code(config)
if settings is None:
var.set_event_config.assert_not_called()
var.set_event_disabled.assert_called_once_with()
else:
var.set_event_config.assert_called_once_with(settings)
var.set_event_disabled.assert_not_called()
+11
View File
@@ -23,6 +23,7 @@ esphome:
duration: !lambda return uint32_t{60};
- rx8111.clear_alarm_flag: rtc
- rx8111.clear_timer_flag: rtc
- rx8111.clear_event_flag: rtc
- rx8111.stop_timer: rtc
- rx8111.start_timer: rtc
- rx8111.unset_alarm: rtc
@@ -38,6 +39,16 @@ time:
timer_count: 2
timer_clock: 1hz
event_poll_interval: 500ms
battery_backup: true
battery_charging: true
fout_frequency: 1024hz
event_enabled: true
event_level: low
evin_filter: 2
evin_pull: pullup_500k
on_event:
then:
- rx8111.clear_event_flag: rtc
on_alarm:
then:
- rx8111.clear_alarm_flag: rtc
+536 -10
View File
@@ -2,6 +2,7 @@
#include <array>
#include <ctime>
#include <utility>
#include <vector>
#include "esphome/components/rx8111/rx8111.h"
@@ -19,14 +20,21 @@ static constexpr uint8_t CONTROL = 0x1F;
static constexpr uint8_t EXTENSION = 0x1D;
static constexpr uint8_t SEC_ALARM = 0x2C;
static constexpr uint8_t MIN_ALARM = 0x17;
static constexpr uint8_t POWER_SWITCH = 0x32;
static constexpr uint8_t TIMER_CONTROL = 0x2D;
static constexpr uint8_t EVIN = 0x2B;
static constexpr uint8_t TIMESTAMP_CONTROL = 0x34;
class RegisterBus final : public i2c::I2CBus {
public:
std::array<uint8_t, 256> regs{};
std::vector<uint8_t> writes;
std::vector<std::pair<uint8_t, size_t>> reads;
int fail_read{-1};
int fail_read_once{-1};
int fail_write{-1};
int fail_control_write_number{-1};
unsigned control_writes{0};
unsigned flag_reads{0};
bool latch_event_before_flag_write{false};
@@ -38,17 +46,20 @@ class RegisterBus final : public i2c::I2CBus {
return i2c::ERROR_TIMEOUT;
const uint8_t reg = out[0];
if (in_size > 0) {
this->reads.emplace_back(reg, in_size);
if (reg == FLAG)
this->flag_reads++;
if (reg == this->fail_read)
if (this->fail_read >= reg && this->fail_read < reg + in_size)
return i2c::ERROR_TIMEOUT;
if (reg == this->fail_read_once) {
if (this->fail_read_once >= reg && this->fail_read_once < reg + in_size) {
this->fail_read_once = -1;
return i2c::ERROR_TIMEOUT;
}
for (size_t i = 0; i < in_size; i++)
in[i] = this->regs[reg + i];
} else {
if (reg == CONTROL && ++this->control_writes == this->fail_control_write_number)
return i2c::ERROR_TIMEOUT;
if (reg == this->fail_write)
return i2c::ERROR_TIMEOUT;
this->writes.push_back(reg);
@@ -80,6 +91,9 @@ class RX8111Test : public ::testing::Test {
App.scheduler.cancel_interval(&this->rtc_, uint32_t{0});
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{1});
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{2});
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{3});
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{4});
App.scheduler.cancel_timeout(&this->rtc_, uint32_t{5});
App.scheduler.call(millis()); // Reap cancelled timers while the fixture is still alive.
}
@@ -91,12 +105,501 @@ class RX8111Test : public ::testing::Test {
// TEST_F bodies are subclasses, so the friendship stops here
bool polling_started_() const { return this->rtc_.event_polling_started_; }
bool timer_enabled_() const { return this->rtc_.timer_enabled_; }
void configure_battery_() { this->rtc_.configure_boot_battery_(); }
bool power_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_BATTERY) != 0; }
bool fout_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_FOUT) != 0; }
bool timer_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_TIMER) != 0; }
bool event_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_EVENT) != 0; }
bool event_clear_warning_() const { return (this->rtc_.warnings_ & RX8111Component::WARN_EVENT_FLAG_CLEAR) != 0; }
void poll_() { this->rtc_.check_event_flags_(); }
RegisterBus bus_;
RX8111Component rtc_;
unsigned syncs_{0};
};
TEST_F(RX8111Test, OmittedPowerAndFoutOptionsDoNotTouchBatteryBackedRegisters) {
this->bus_.regs[POWER_SWITCH] = 0xC5;
this->bus_.regs[EXTENSION] = 0xA7;
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0xC5);
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xA7);
EXPECT_TRUE(this->bus_.writes.empty());
}
TEST_F(RX8111Test, EventSettingsPreserveUnrelatedBitsAndLatchedFlag) {
this->bus_.regs[TIMESTAMP_CONTROL] = 0x85; // EISEL plus TSRAM and a reserved bit.
this->bus_.regs[EVIN] = 1;
this->bus_.regs[EXTENSION] = 0x90;
this->bus_.regs[CONTROL] = 0x90;
this->bus_.regs[FLAG] = 0x04;
this->rtc_.set_event_config(0xC4);
int callbacks = 0;
this->rtc_.add_on_event_callback([&]() { callbacks++; });
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[EVIN], 0xC5);
EXPECT_EQ(this->bus_.regs[EXTENSION], 0x94); // ETS enables EVIN detection; preserve other bits.
EXPECT_EQ(this->bus_.regs[CONTROL], 0x94);
EXPECT_EQ(this->bus_.regs[TIMESTAMP_CONTROL], 0x81);
this->poll_();
this->poll_();
EXPECT_EQ(callbacks, 1);
EXPECT_EQ(this->bus_.regs[FLAG], 0x04);
EXPECT_TRUE(this->rtc_.clear_event_flag());
this->bus_.regs[FLAG] = 0x04;
this->poll_();
EXPECT_EQ(callbacks, 2);
}
TEST_F(RX8111Test, EventWithoutCallbackConfiguresHardwareWithoutPolling) {
this->rtc_.set_event_config(0xC4);
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0x04);
EXPECT_FALSE(this->polling_started_());
const auto reads = this->bus_.flag_reads;
this->rtc_.update();
// One calendar FLAG read, no event FLAG read.
EXPECT_EQ(this->bus_.flag_reads, reads + 1);
EXPECT_TRUE(this->rtc_.clear_event_flag());
}
TEST_F(RX8111Test, EventWithCallbackStartsPolling) {
this->rtc_.set_event_config(0);
this->rtc_.add_on_event_callback([]() {});
this->rtc_.setup();
EXPECT_TRUE(this->polling_started_());
}
TEST_F(RX8111Test, EventConfigurationRetriesWithoutDroppingRetainedFlag) {
this->bus_.regs[EVIN] = 0x03; // Preserve EVIN's reserved and retained capture bits.
this->bus_.regs[FLAG] = 0x04;
this->bus_.fail_write = EVIN;
this->rtc_.set_event_config(0xC4);
int callbacks = 0;
this->rtc_.add_on_event_callback([&]() { callbacks++; });
this->rtc_.setup();
EXPECT_TRUE(this->event_warning_());
EXPECT_EQ(this->bus_.regs[EVIN], 0x03);
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x04, 0);
this->poll_();
EXPECT_EQ(callbacks, 0);
this->bus_.fail_write = -1;
this->advance_retry_();
EXPECT_FALSE(this->event_warning_());
EXPECT_EQ(this->bus_.regs[EVIN], 0xC7);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x04, 0x04);
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0x04);
this->poll_();
EXPECT_EQ(callbacks, 1);
EXPECT_EQ(this->bus_.regs[FLAG], 0x04);
}
TEST_F(RX8111Test, EventExtensionFailureRetriesBeforeEnablingInterrupt) {
this->bus_.regs[EXTENSION] = 0xA0;
this->bus_.fail_write = EXTENSION;
this->rtc_.set_event_config(0xD0);
this->rtc_.setup();
EXPECT_TRUE(this->event_warning_());
EXPECT_EQ(this->bus_.regs[EVIN], 0xD0);
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xA0);
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0);
this->bus_.fail_write = -1;
this->advance_retry_();
EXPECT_FALSE(this->event_warning_());
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xA4);
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0x04);
}
TEST_F(RX8111Test, ExhaustedEventStartupRetriesDoNotKeepPollingFlags) {
this->rtc_.set_event_config(0);
unsigned callbacks = 0;
this->rtc_.add_on_event_callback([&callbacks]() { callbacks++; });
this->bus_.fail_write = EVIN;
this->rtc_.setup();
EXPECT_FALSE(this->polling_started_());
this->advance_retry_();
this->advance_retry_();
EXPECT_TRUE(this->event_warning_());
this->bus_.fail_write = -1;
this->bus_.regs[FLAG] = 0x04;
const auto reads = this->bus_.flag_reads;
for (unsigned i = 0; i < 5; i++) {
this->advance_retry_();
this->poll_();
}
EXPECT_EQ(this->bus_.flag_reads, reads);
EXPECT_EQ(callbacks, 0);
EXPECT_FALSE(this->polling_started_());
EXPECT_TRUE(this->event_warning_());
}
TEST_F(RX8111Test, FailedEventStartupDoesNotStopTimerPolling) {
this->rtc_.set_event_config(0);
this->rtc_.add_on_event_callback([]() { FAIL() << "EVIN configuration never succeeded"; });
this->rtc_.set_timer_count(2);
this->rtc_.set_timer_configured(true);
this->rtc_.set_timer_enabled(true);
unsigned callbacks = 0;
this->rtc_.add_on_timer_callback([&callbacks]() { callbacks++; });
this->bus_.fail_write = EVIN;
this->rtc_.setup();
this->advance_retry_();
this->advance_retry_();
EXPECT_TRUE(this->polling_started_());
this->bus_.regs[FLAG] = 0x14;
this->poll_();
EXPECT_EQ(callbacks, 1);
EXPECT_TRUE(this->event_warning_());
}
TEST_F(RX8111Test, ClearEventFlagPreservesOtherFlagsAndRetriesAfterFailure) {
this->bus_.regs[FLAG] = 0x1C;
this->bus_.fail_write = FLAG;
EXPECT_FALSE(this->rtc_.clear_event_flag());
EXPECT_TRUE(this->event_clear_warning_());
EXPECT_EQ(this->bus_.regs[FLAG], 0x1C);
this->bus_.fail_write = -1;
EXPECT_TRUE(this->rtc_.clear_event_flag());
EXPECT_FALSE(this->event_clear_warning_());
EXPECT_EQ(this->bus_.regs[FLAG], 0x18);
}
TEST_F(RX8111Test, EventFlagClearDoesNotHideBootConfigurationFailure) {
this->rtc_.set_event_config(0xC4);
this->bus_.fail_write = EVIN;
this->rtc_.setup();
ASSERT_TRUE(this->event_warning_());
this->bus_.fail_write = FLAG;
EXPECT_FALSE(this->rtc_.clear_event_flag());
EXPECT_TRUE(this->event_clear_warning_());
this->bus_.fail_write = EVIN;
EXPECT_TRUE(this->rtc_.clear_event_flag());
EXPECT_FALSE(this->event_clear_warning_());
EXPECT_TRUE(this->event_warning_());
EXPECT_TRUE(this->rtc_.status_has_warning());
this->bus_.fail_write = -1;
this->advance_retry_();
EXPECT_FALSE(this->event_warning_());
EXPECT_FALSE(this->event_clear_warning_());
}
TEST_F(RX8111Test, ExplicitEventDisableDoesNotChangeCaptureOrFlag) {
this->bus_.regs[EXTENSION] = 0x04;
this->bus_.regs[CONTROL] = 0x0C;
this->bus_.regs[FLAG] = 0x04;
this->rtc_.set_event_disabled();
this->rtc_.setup();
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL}));
EXPECT_EQ(this->bus_.regs[CONTROL], 0x08);
EXPECT_EQ(this->bus_.regs[EXTENSION], 0x04);
EXPECT_EQ(this->bus_.regs[FLAG], 0x04);
}
TEST_F(RX8111Test, BatteryAndFoutPreserveUnrelatedBitsAndTimerRouting) {
this->bus_.regs[POWER_SWITCH] = 0xB5;
this->bus_.regs[EXTENSION] = 0xDA;
this->rtc_.set_battery_backup(true, false);
this->rtc_.set_fout_frequency(1);
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x79);
EXPECT_EQ(this->bus_.regs[EXTENSION], 0x5A);
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{POWER_SWITCH, EXTENSION}));
EXPECT_FALSE(this->power_warning_());
EXPECT_FALSE(this->fout_warning_());
}
TEST_F(RX8111Test, ChargingRequiresExplicitEnableAndCanBeDisabled) {
this->bus_.regs[POWER_SWITCH] = 0x15;
this->rtc_.set_battery_backup(true, true);
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0xD9);
this->rtc_.set_battery_backup(false, false);
this->configure_battery_();
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x19);
}
TEST_F(RX8111Test, FoutRefusesTimerPinRoutingWithoutModifyingIt) {
this->bus_.regs[TIMER_CONTROL] = 0x02;
this->bus_.regs[EXTENSION] = 0xC5;
this->rtc_.set_fout_frequency(0);
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xC5);
EXPECT_TRUE(this->fout_warning_());
EXPECT_TRUE(this->bus_.writes.empty());
this->bus_.regs[TIMER_CONTROL] = 0;
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[EXTENSION], 0x05);
EXPECT_FALSE(this->fout_warning_());
}
TEST_F(RX8111Test, BootTimerClearingTimerPinRoutingLetsFoutFinishAfterItsRetriesRanOut) {
this->bus_.regs[TIMER_CONTROL] = 0x02;
this->bus_.regs[EXTENSION] = 0xC0;
this->bus_.fail_write = TIMER_CONTROL;
this->rtc_.set_fout_frequency(0);
this->rtc_.set_timer_count(2);
this->rtc_.set_timer_configured(true);
this->rtc_.set_timer_enabled(true);
this->rtc_.setup();
this->advance_retry_();
EXPECT_TRUE(this->fout_warning_());
this->bus_.fail_write = -1;
// The last FOUT attempt runs before the last timer attempt and still sees TMPIN
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0xC0, 0);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
EXPECT_FALSE(this->fout_warning_());
EXPECT_FALSE(this->timer_warning_());
}
TEST_F(RX8111Test, RuntimeTimerStartClearingTimerPinRoutingLetsFoutFinish) {
this->bus_.regs[TIMER_CONTROL] = 0x02;
this->bus_.regs[EXTENSION] = 0xC0;
this->rtc_.set_fout_frequency(0);
this->rtc_.set_timer_count(2);
this->rtc_.setup();
this->advance_retry_();
this->advance_retry_();
EXPECT_TRUE(this->fout_warning_());
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0xC0, 0xC0);
ASSERT_TRUE(this->rtc_.start_timer());
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0xC0, 0);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
EXPECT_FALSE(this->fout_warning_());
}
TEST_F(RX8111Test, PowerWriteFailureRetriesAndLeavesWarningOnExhaustion) {
this->bus_.regs[POWER_SWITCH] = 0x85;
this->bus_.fail_write = POWER_SWITCH;
this->rtc_.set_battery_backup(true, false);
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x85);
EXPECT_TRUE(this->power_warning_());
this->advance_retry_();
this->advance_retry_();
this->bus_.fail_write = -1;
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x85);
EXPECT_TRUE(this->power_warning_());
this->configure_battery_();
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], 0x49);
EXPECT_FALSE(this->power_warning_());
}
TEST_F(RX8111Test, ExplicitBatteryConfigurationNormalizesEverySwitchSelection) {
for (uint8_t selection = 0; selection < 4; selection++) {
for (bool enabled : {false, true}) {
this->bus_.regs[POWER_SWITCH] = 0xB3 | (selection << 2);
this->rtc_.set_battery_backup(enabled, false);
this->configure_battery_();
EXPECT_EQ(this->bus_.regs[POWER_SWITCH], enabled ? 0x7B : 0x3B);
}
}
}
TEST_F(RX8111Test, TimestampSelectionFailureRetriesBeforeEnablingDetection) {
this->bus_.regs[TIMESTAMP_CONTROL] = 0x05;
this->bus_.fail_write = TIMESTAMP_CONTROL;
this->rtc_.set_event_config(0);
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x04, 0);
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0);
this->bus_.fail_write = -1;
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[TIMESTAMP_CONTROL], 0x01);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x04, 0x04);
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x04, 0x04);
}
TEST_F(RX8111Test, ExplicitTimerStartNormalizesRetainedModeAtBootAndRuntime) {
this->rtc_.set_timer_count(2);
this->rtc_.set_timer_configured(true);
this->rtc_.set_timer_enabled(true);
this->bus_.regs[TIMER_CONTROL] = 0xFF;
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0xF0);
for (uint8_t mode : {0x01, 0x02, 0x04, 0x08, 0x0C, 0x0F}) {
this->bus_.regs[TIMER_CONTROL] = 0xF0 | mode;
ASSERT_TRUE(this->rtc_.start_timer());
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0xF0);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
}
}
TEST_F(RX8111Test, TimerModeFailureDoesNotStartCounterAndBootRetries) {
this->rtc_.set_timer_count(2);
this->rtc_.set_timer_configured(true);
this->rtc_.set_timer_enabled(true);
this->bus_.regs[TIMER_CONTROL] = 1;
this->bus_.fail_write = TIMER_CONTROL;
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0);
this->bus_.fail_write = -1;
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
this->bus_.fail_write = TIMER_CONTROL;
EXPECT_FALSE(this->rtc_.start_timer());
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0);
this->bus_.fail_write = -1;
ASSERT_TRUE(this->rtc_.start_timer());
EXPECT_EQ(this->bus_.regs[TIMER_CONTROL], 0);
}
TEST_F(RX8111Test, FailedRestartKeepsStopAcrossHostResetUntilFreshTrustedWrite) {
this->bus_.fail_control_write_number = 2;
this->rtc_.write_time();
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
EXPECT_TRUE(this->rtc_.status_has_warning());
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 0);
this->bus_.writes.clear();
this->bus_.fail_write = CONTROL;
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
EXPECT_TRUE(this->rtc_.status_has_warning());
this->bus_.fail_write = -1;
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
EXPECT_TRUE(this->bus_.writes.empty());
EXPECT_TRUE(this->rtc_.status_has_warning());
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 0); // Even with initially clear hardware validity flags.
RX8111Component rebooted;
rebooted.set_i2c_bus(&this->bus_);
rebooted.set_i2c_address(ADDRESS);
rebooted.add_on_time_sync_callback([this]() { this->syncs_++; });
rebooted.setup();
EXPECT_EQ(this->syncs_, 0); // The RAM guard is gone, but STOP survives.
EXPECT_EQ(this->bus_.regs[FLAG], 0); // Even with validity flags already cleared.
rebooted.write_time();
rebooted.read_time();
EXPECT_EQ(this->syncs_, 1);
this->rtc_.write_time();
EXPECT_FALSE(this->rtc_.status_has_warning());
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 2);
}
TEST_F(RX8111Test, FailedCalendarWriteKeepsStopAndDoesNotRetry) {
this->bus_.regs[FLAG] = 0x83;
this->bus_.fail_write = SEC;
this->bus_.fail_control_write_number = 2;
this->rtc_.write_time();
this->bus_.fail_write = -1;
this->bus_.writes.clear();
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
EXPECT_EQ(this->bus_.regs[FLAG], 0x83);
EXPECT_TRUE(this->bus_.writes.empty());
EXPECT_TRUE(this->rtc_.status_has_warning());
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 0);
}
TEST_F(RX8111Test, FailedCalendarWriteRejectsPreviouslyValidCalendarUntilFreshWrite) {
this->rtc_.write_time();
ASSERT_EQ(this->bus_.regs[FLAG], 0);
this->bus_.fail_write = SEC;
this->rtc_.write_time();
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 0);
this->bus_.fail_write = -1;
this->rtc_.write_time();
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 1);
}
TEST_F(RX8111Test, IncompleteWritesRemainInvalidAfterComponentRecreation) {
for (const uint8_t failed_register : {SEC, FLAG, CONTROL}) {
this->bus_.fail_write = -1;
this->rtc_.write_time();
ASSERT_EQ(this->bus_.regs[FLAG], 0);
this->bus_.fail_write = failed_register == CONTROL ? -1 : failed_register;
this->bus_.fail_control_write_number = failed_register == CONTROL ? this->bus_.control_writes + 2 : -1;
this->rtc_.write_time();
ASSERT_EQ(this->bus_.regs[CONTROL] & 1, 1);
this->bus_.fail_write = -1;
RX8111Component rebooted;
rebooted.set_i2c_bus(&this->bus_);
rebooted.set_i2c_address(ADDRESS);
unsigned syncs = 0;
rebooted.add_on_time_sync_callback([&syncs]() { syncs++; });
rebooted.setup();
rebooted.read_time();
EXPECT_EQ(syncs, 0) << "failed register " << unsigned(failed_register);
rebooted.write_time();
rebooted.read_time();
EXPECT_EQ(syncs, 1);
}
}
TEST_F(RX8111Test, PersistentControlFailureDoesNotRearmRecovery) {
this->bus_.fail_control_write_number = 2;
this->rtc_.write_time();
this->bus_.fail_write = CONTROL;
const auto control_writes = this->bus_.control_writes;
for (unsigned i = 0; i < 10; i++)
this->advance_retry_();
EXPECT_EQ(this->bus_.control_writes, control_writes);
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
EXPECT_TRUE(this->rtc_.status_has_warning());
}
TEST_F(RX8111Test, NewTrustedWriteRestartsStoppedCounterWithoutDeferredRecovery) {
this->bus_.fail_control_write_number = 2;
this->rtc_.write_time();
ASSERT_EQ(this->bus_.regs[CONTROL] & 1, 1);
this->rtc_.write_time();
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 0);
EXPECT_FALSE(this->rtc_.status_has_warning());
this->bus_.writes.clear();
this->advance_retry_();
EXPECT_TRUE(this->bus_.writes.empty());
}
TEST_F(RX8111Test, FailedValidityCleanupRequiresFreshWriteAndPreservesOtherFaults) {
this->bus_.regs[FLAG] = 0x83;
this->bus_.fail_write = FLAG;
this->rtc_.write_time();
EXPECT_TRUE(this->rtc_.status_has_warning());
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
this->bus_.fail_write = POWER_SWITCH;
this->rtc_.set_battery_backup(false, false);
this->configure_battery_();
this->bus_.writes.clear();
this->advance_retry_();
EXPECT_TRUE(this->bus_.writes.empty());
EXPECT_EQ(this->bus_.regs[FLAG], 0x83);
EXPECT_TRUE(this->rtc_.status_has_warning());
this->bus_.fail_write = -1;
this->configure_battery_();
EXPECT_TRUE(this->rtc_.status_has_warning());
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 0);
this->rtc_.write_time();
EXPECT_FALSE(this->rtc_.status_has_warning());
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 1);
}
TEST_F(RX8111Test, FoutReadFailureDoesNotAlterExtensionAndRecovers) {
this->bus_.regs[EXTENSION] = 0xC5;
this->bus_.fail_read = EXTENSION;
this->rtc_.set_fout_frequency(3);
this->rtc_.setup();
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xC5);
EXPECT_TRUE(this->fout_warning_());
this->bus_.fail_read = -1;
this->advance_retry_();
EXPECT_EQ(this->bus_.regs[EXTENSION], 0xC5);
EXPECT_FALSE(this->fout_warning_());
}
TEST_F(RX8111Test, WritesCalendarAndPreservesEventFlagsWithoutReadModifyWrite) {
this->bus_.regs[FLAG] = 0xB8; // POR + existing UF/TF/AF.
this->bus_.regs[CONTROL] = 0x5C;
@@ -111,7 +614,7 @@ TEST_F(RX8111Test, WritesCalendarAndPreservesEventFlagsWithoutReadModifyWrite) {
(this->bus_.regs[SEC] == bcd(after.second) && this->bus_.regs[DAY] == bcd(after.day_of_month)));
EXPECT_TRUE(this->bus_.regs[WEEK] == (1 << (before.day_of_week - 1)) ||
this->bus_.regs[WEEK] == (1 << (after.day_of_week - 1)));
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL, SEC, CONTROL, FLAG}));
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL, SEC, FLAG, CONTROL}));
}
TEST_F(RX8111Test, RejectsInvalidFlagsBCDWeekdayAndCalendar) {
@@ -154,7 +657,7 @@ TEST_F(RX8111Test, RejectsInvalidFlagsBCDWeekdayAndCalendar) {
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 1);
EXPECT_TRUE(this->rtc_.status_has_warning());
this->bus_.regs[SEC] = 0x60; // ESPTime permits leap seconds, but RX8111 only counts 00..59.
this->bus_.regs[SEC] = 0x60; // Hardware supports leap adjustment; synchronization requires normal seconds.
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 1);
EXPECT_TRUE(this->rtc_.status_has_warning());
@@ -164,7 +667,27 @@ TEST_F(RX8111Test, RejectsInvalidFlagsBCDWeekdayAndCalendar) {
EXPECT_FALSE(this->rtc_.status_has_warning());
}
TEST_F(RX8111Test, I2CFailuresDoNotSyncOrLeaveTheCounterStopped) {
TEST_F(RX8111Test, CalendarReadCombinesFlagAndControlAndRejectsReadFailures) {
this->rtc_.write_time();
this->bus_.reads.clear();
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 1);
EXPECT_EQ(this->bus_.reads, (std::vector<std::pair<uint8_t, size_t>>{{FLAG, 2}, {SEC, 7}}));
for (const uint8_t failed_register : {FLAG, CONTROL}) {
this->bus_.reads.clear();
this->bus_.fail_read = failed_register;
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 1);
EXPECT_TRUE(this->rtc_.status_has_warning());
EXPECT_EQ(this->bus_.reads, (std::vector<std::pair<uint8_t, size_t>>{{FLAG, 2}}));
}
this->bus_.fail_read = -1;
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 2);
EXPECT_FALSE(this->rtc_.status_has_warning());
}
TEST_F(RX8111Test, I2CFailuresDoNotSyncAndIncompleteWritesLeaveCounterStopped) {
this->bus_.fail_read = FLAG;
this->rtc_.read_time();
EXPECT_EQ(this->syncs_, 0);
@@ -178,13 +701,13 @@ TEST_F(RX8111Test, I2CFailuresDoNotSyncOrLeaveTheCounterStopped) {
EXPECT_TRUE(this->bus_.writes.empty());
this->bus_.fail_write = SEC;
this->rtc_.write_time();
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL, CONTROL}));
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 0);
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{CONTROL}));
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
this->bus_.fail_write = FLAG;
this->bus_.regs[FLAG] = 0x83;
this->rtc_.write_time();
EXPECT_EQ(this->bus_.regs[FLAG], 0x83);
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 0);
EXPECT_EQ(this->bus_.regs[CONTROL] & 1, 1);
}
TEST_F(RX8111Test, WriteFailureClearsOnSuccessfulWriteButPreservesEventPollWarning) {
@@ -690,7 +1213,8 @@ TEST_F(RX8111Test, StartTimerRestartsFullCountAndClearsOnlyStaleTimerFlag) {
EXPECT_EQ(this->bus_.regs[0x1C], 0);
EXPECT_EQ(this->bus_.regs[EXTENSION] & 0x10, 0x10);
EXPECT_EQ(this->bus_.regs[CONTROL] & 0x10, 0x10);
EXPECT_EQ(this->bus_.writes, (std::vector<uint8_t>{EXTENSION, CONTROL, 0x1A, EXTENSION, FLAG, CONTROL, EXTENSION}));
EXPECT_EQ(this->bus_.writes,
(std::vector<uint8_t>{EXTENSION, CONTROL, TIMER_CONTROL, 0x1A, EXTENSION, FLAG, CONTROL, EXTENSION}));
this->rtc_.update();
EXPECT_EQ(calls, 0);
this->bus_.regs[FLAG] |= 0x10;
@@ -1004,8 +1528,10 @@ TEST_F(RX8111Test, RetainedAlarmInspectionRetriesAndDeliversWakeFlag) {
this->rtc_.add_on_alarm_callback([&calls]() { calls++; });
this->bus_.regs[FLAG] = 0x08;
this->advance_retry_();
EXPECT_FALSE(this->rtc_.status_has_warning());
// Alarm recovery cannot clear the calendar's independent failed CONTROL-read warning.
EXPECT_TRUE(this->rtc_.status_has_warning());
this->rtc_.update();
EXPECT_FALSE(this->rtc_.status_has_warning());
this->rtc_.update();
EXPECT_EQ(calls, 1);
}