mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 22:06:12 +00:00
Compare commits
101
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
305a1bf3fa | ||
|
|
d00820fae7 | ||
|
|
2503dc49e3 | ||
|
|
2cd6ea7496 | ||
|
|
679283c28f | ||
|
|
4dc829e091 | ||
|
|
03a76a788f | ||
|
|
807217ead6 | ||
|
|
7eb279be69 | ||
|
|
549aa0a6ba | ||
|
|
95fad9aaf5 | ||
|
|
e31de889ef | ||
|
|
6897c8a4a4 | ||
|
|
e70adbb060 | ||
|
|
5bdcf2b39d | ||
|
|
87855c5967 | ||
|
|
e84efbb934 | ||
|
|
f7b2bb8c19 | ||
|
|
9dfa251d5b | ||
|
|
eb66aafc30 | ||
|
|
e24418d201 | ||
|
|
066e95077a | ||
|
|
62f1a9b08f | ||
|
|
4921173f6c | ||
|
|
9ce82f98cc | ||
|
|
0bdd700c2e | ||
|
|
9545322be0 | ||
|
|
15afdf4dda | ||
|
|
74bdda341b | ||
|
|
6d90969f5f | ||
|
|
b1f83bc2dd | ||
|
|
4b81c8712b | ||
|
|
660b7b11f6 | ||
|
|
d039160876 | ||
|
|
67219ac41b | ||
|
|
416762802e | ||
|
|
f0b0801c3f | ||
|
|
331ee3a278 | ||
|
|
7392ac662a | ||
|
|
a453dd66c6 | ||
|
|
52c4c0310d | ||
|
|
547dbf18d4 | ||
|
|
ba3af07a23 | ||
|
|
9c6ae9c615 | ||
|
|
de02ef8555 | ||
|
|
38b23152f4 | ||
|
|
0bc55bcdc6 | ||
|
|
0849e4b412 | ||
|
|
375e8bc39b | ||
|
|
16a91ab760 | ||
|
|
2456e8d686 | ||
|
|
0c8e1c3f1c | ||
|
|
a3c575d643 | ||
|
|
0573c8b568 | ||
|
|
d01205fa62 | ||
|
|
de7ea89d0b | ||
|
|
0077f68158 | ||
|
|
7458f8c7d8 | ||
|
|
0e5e0a8ff6 | ||
|
|
752c230405 | ||
|
|
cfc92657a2 | ||
|
|
32f32f144d | ||
|
|
9b60e7cff2 | ||
|
|
02b8db1f3f | ||
|
|
f860c8a8d9 | ||
|
|
bb7a060138 | ||
|
|
84839507bd | ||
|
|
1de1794574 | ||
|
|
84638cf2b6 | ||
|
|
4df1b53d85 | ||
|
|
bac443840c | ||
|
|
ef54eb14d8 | ||
|
|
60aa3e24f1 | ||
|
|
63566999bd | ||
|
|
1728d3326b | ||
|
|
2b99c56754 | ||
|
|
d79cd9a7eb | ||
|
|
fa18994fab | ||
|
|
1090706216 | ||
|
|
ced497995d | ||
|
|
228c553f60 | ||
|
|
563e197c6d | ||
|
|
9d07f1f400 | ||
|
|
846fc70bdf | ||
|
|
74212d8d04 | ||
|
|
101c9cd840 | ||
|
|
8152dcf224 | ||
|
|
f2d50b4aa7 | ||
|
|
f75b3d6ded | ||
|
|
3cf29bf4fb | ||
|
|
f752e3b150 | ||
|
|
a5bc7070d1 | ||
|
|
b2eebfb010 | ||
|
|
a596d1d3c8 | ||
|
|
6701aedae2 | ||
|
|
6fddbd1d48 | ||
|
|
03aaa00d75 | ||
|
|
b612eb2929 | ||
|
|
63c041a95b | ||
|
|
f8d5096093 | ||
|
|
f835600fa6 |
@@ -446,8 +446,6 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(CONF_BLUETOOTH, None, apply="set_bluetooth(state)", publish="after")
|
||||
```
|
||||
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
|
||||
to still drive a state when the restore mode is disabled.
|
||||
|
||||
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
|
||||
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
|
||||
|
||||
@@ -8,7 +8,10 @@ ESPHOME_LOG_TAG(TAG, "copy.lock");
|
||||
void CopyLock::setup() {
|
||||
source_->add_on_state_callback([this](lock::LockState state) { this->publish_state(state); });
|
||||
|
||||
this->traits = this->source_->traits;
|
||||
traits.set_assumed_state(source_->traits.get_assumed_state());
|
||||
traits.set_requires_code(source_->traits.get_requires_code());
|
||||
traits.set_supported_states_mask(source_->traits.get_supported_states_mask());
|
||||
traits.set_supports_open(source_->traits.get_supports_open());
|
||||
|
||||
this->publish_state(source_->state);
|
||||
}
|
||||
|
||||
@@ -1,37 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::cover {
|
||||
|
||||
class CoverTraits {
|
||||
public:
|
||||
CoverTraits() = default;
|
||||
|
||||
bool get_is_assumed_state() const { return this->flags_.is_assumed_state; }
|
||||
void set_is_assumed_state(bool is_assumed_state) { this->flags_.is_assumed_state = is_assumed_state; }
|
||||
bool get_supports_position() const { return this->flags_.supports_position; }
|
||||
void set_supports_position(bool supports_position) { this->flags_.supports_position = supports_position; }
|
||||
bool get_supports_tilt() const { return this->flags_.supports_tilt; }
|
||||
void set_supports_tilt(bool supports_tilt) { this->flags_.supports_tilt = supports_tilt; }
|
||||
bool get_supports_toggle() const { return this->flags_.supports_toggle; }
|
||||
void set_supports_toggle(bool supports_toggle) { this->flags_.supports_toggle = supports_toggle; }
|
||||
bool get_supports_stop() const { return this->flags_.supports_stop; }
|
||||
void set_supports_stop(bool supports_stop) { this->flags_.supports_stop = supports_stop; }
|
||||
bool get_supports_stop_tilt() const { return this->flags_.supports_stop_tilt; }
|
||||
void set_supports_stop_tilt(bool supports_stop_tilt) { this->flags_.supports_stop_tilt = supports_stop_tilt; }
|
||||
bool get_is_assumed_state() const { return this->is_assumed_state_; }
|
||||
void set_is_assumed_state(bool is_assumed_state) { this->is_assumed_state_ = is_assumed_state; }
|
||||
bool get_supports_position() const { return this->supports_position_; }
|
||||
void set_supports_position(bool supports_position) { this->supports_position_ = supports_position; }
|
||||
bool get_supports_tilt() const { return this->supports_tilt_; }
|
||||
void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; }
|
||||
bool get_supports_toggle() const { return this->supports_toggle_; }
|
||||
void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; }
|
||||
bool get_supports_stop() const { return this->supports_stop_; }
|
||||
void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; }
|
||||
bool get_supports_stop_tilt() const { return this->supports_stop_tilt_; }
|
||||
void set_supports_stop_tilt(bool supports_stop_tilt) { this->supports_stop_tilt_ = supports_stop_tilt; }
|
||||
|
||||
protected:
|
||||
// Bit-packed flags to save memory (1 byte instead of 6)
|
||||
struct CoverTraitFlags {
|
||||
uint8_t is_assumed_state : 1;
|
||||
uint8_t supports_position : 1;
|
||||
uint8_t supports_tilt : 1;
|
||||
uint8_t supports_toggle : 1;
|
||||
uint8_t supports_stop : 1;
|
||||
uint8_t supports_stop_tilt : 1;
|
||||
uint8_t reserved : 2;
|
||||
} flags_{};
|
||||
bool is_assumed_state_{false};
|
||||
bool supports_position_{false};
|
||||
bool supports_tilt_{false};
|
||||
bool supports_toggle_{false};
|
||||
bool supports_stop_{false};
|
||||
bool supports_stop_tilt_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::cover
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace esphome::deep_sleep {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "deep_sleep");
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
switch (lt_get_reboot_reason()) {
|
||||
case REBOOT_REASON_SLEEP_GPIO:
|
||||
@@ -19,6 +20,7 @@ WakeupCause get_wakeup_cause() {
|
||||
return WAKEUP_CAUSE_NONE;
|
||||
}
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ struct WakeupCauseToRunDuration {
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
/// Why the device woke from deep sleep. Passed to on_wake automations.
|
||||
enum WakeupCause : uint8_t {
|
||||
/// The device did not wake from deep sleep (for example a cold boot, reset or OTA restart).
|
||||
@@ -77,11 +79,6 @@ enum WakeupCause : uint8_t {
|
||||
/// Return why the device woke from deep sleep. Implemented per platform.
|
||||
WakeupCause get_wakeup_cause();
|
||||
|
||||
/// Return true when this boot is a wake from deep sleep.
|
||||
inline bool woke_from_deep_sleep() { return get_wakeup_cause() != WAKEUP_CAUSE_NONE; }
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
/** Setup priority of on_wake triggers.
|
||||
*
|
||||
* Between restoring global variables (setup_priority::HARDWARE, 800) and on_boot automations at
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace esphome::deep_sleep {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "deep_sleep");
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
switch (esp_sleep_get_wakeup_cause()) {
|
||||
case ESP_SLEEP_WAKEUP_EXT0:
|
||||
@@ -46,6 +47,7 @@ WakeupCause get_wakeup_cause() {
|
||||
return WAKEUP_CAUSE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
|
||||
if (this->wakeup_cause_to_run_duration_.has_value()) {
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
|
||||
#include <Esp.h>
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace esphome::deep_sleep {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "deep_sleep");
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
// The ESP8266 can only wake from deep sleep through the RTC timer (via GPIO16 -> RST).
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
@@ -19,6 +22,7 @@ WakeupCause get_wakeup_cause() {
|
||||
}
|
||||
return WAKEUP_CAUSE_NONE;
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
|
||||
@@ -31,9 +31,6 @@ static bool wakeable_delay_feed_wdt(uint32_t ms) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A wake from System OFF is not told apart from a reset yet.
|
||||
WakeupCause get_wakeup_cause() { return WAKEUP_CAUSE_NONE; }
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
void DeepSleepComponent::dump_config_platform_() {}
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
from esphome import automation, pins
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import CONF_ENABLED
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FREQUENCY,
|
||||
CONF_HOUR,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_MINUTE,
|
||||
CONF_MODE,
|
||||
CONF_OFFSET,
|
||||
CONF_SECOND,
|
||||
CONF_STATE,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@linkedupbits"]
|
||||
@@ -21,245 +10,23 @@ MULTI_CONF = True
|
||||
DOMAIN = "ds3231"
|
||||
|
||||
CONF_DS3231_ID = "ds3231_id"
|
||||
CONF_ALARM = "alarm"
|
||||
CONF_ALARM_CHECK_INTERVAL = "alarm_check_interval"
|
||||
CONF_AGING_OFFSET = "aging_offset"
|
||||
CONF_AUTO_CLEAR_ALARMS = "auto_clear_alarms"
|
||||
CONF_BATTERY_BACKED_SQUARE_WAVE = "battery_backed_square_wave"
|
||||
CONF_DAY_OF_MONTH = "day_of_month"
|
||||
CONF_DAY_OF_WEEK = "day_of_week"
|
||||
CONF_ON_ALARM_1 = "on_alarm_1"
|
||||
CONF_ON_ALARM_2 = "on_alarm_2"
|
||||
CONF_OUTPUT_32KHZ = "output_32khz"
|
||||
CONF_SQUARE_WAVE_FREQUENCY = "square_wave_frequency"
|
||||
|
||||
ds3231_ns = cg.esphome_ns.namespace("ds3231")
|
||||
DS3231Component = ds3231_ns.class_("DS3231Component", cg.Component, i2c.I2CDevice)
|
||||
|
||||
AlarmMode = ds3231_ns.enum("AlarmMode")
|
||||
ALARM_1_MODES = AlarmMode.options(
|
||||
"every_second",
|
||||
"every_minute",
|
||||
"hourly",
|
||||
"daily",
|
||||
"monthly",
|
||||
"weekly",
|
||||
prefix="ALARM_MODE_",
|
||||
)
|
||||
# Alarm 2 has no seconds register.
|
||||
ALARM_2_MODES = {k: v for k, v in ALARM_1_MODES.items() if k != "every_second"}
|
||||
|
||||
SquareWaveFrequency = ds3231_ns.enum("SquareWaveFrequency")
|
||||
# Keyed by the frequency in Hz; cv.frequency turns "1024Hz", "1.024kHz" or 1024 into it.
|
||||
SQUARE_WAVE_FREQUENCIES = {
|
||||
1: SquareWaveFrequency.SQUARE_WAVE_1HZ,
|
||||
1024: SquareWaveFrequency.SQUARE_WAVE_1024HZ,
|
||||
4096: SquareWaveFrequency.SQUARE_WAVE_4096HZ,
|
||||
8192: SquareWaveFrequency.SQUARE_WAVE_8192HZ,
|
||||
}
|
||||
validate_square_wave_frequency = cv.All(
|
||||
cv.frequency, cv.enum(SQUARE_WAVE_FREQUENCIES, int=True)
|
||||
)
|
||||
|
||||
validate_aging_offset = cv.int_range(min=-128, max=127)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DS3231Component),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_SQUARE_WAVE_FREQUENCY): validate_square_wave_frequency,
|
||||
cv.Optional(CONF_BATTERY_BACKED_SQUARE_WAVE): cv.boolean,
|
||||
cv.Optional(CONF_OUTPUT_32KHZ): cv.boolean,
|
||||
cv.Optional(CONF_AGING_OFFSET): validate_aging_offset,
|
||||
cv.Optional(CONF_AUTO_CLEAR_ALARMS, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_ALARM_CHECK_INTERVAL, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_ALARM_1): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_ON_ALARM_2): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema({cv.GenerateID(): cv.declare_id(DS3231Component)})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x68)),
|
||||
cv.has_at_most_one_key(CONF_INTERRUPT_PIN, CONF_SQUARE_WAVE_FREQUENCY),
|
||||
.extend(i2c.i2c_device_schema(0x68))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await i2c.new_i2c_component(config)
|
||||
await i2c.new_i2c_component(config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
cg.add_optional(
|
||||
var.set_square_wave_frequency, config.get(CONF_SQUARE_WAVE_FREQUENCY)
|
||||
)
|
||||
cg.add_optional(
|
||||
var.set_battery_backed_square_wave, config.get(CONF_BATTERY_BACKED_SQUARE_WAVE)
|
||||
)
|
||||
cg.add_optional(var.set_output_32khz_config, config.get(CONF_OUTPUT_32KHZ))
|
||||
cg.add_optional(var.set_aging_offset_config, config.get(CONF_AGING_OFFSET))
|
||||
cg.add(var.set_auto_clear_alarms(config[CONF_AUTO_CLEAR_ALARMS]))
|
||||
cg.add(var.set_alarm_check_interval(config[CONF_ALARM_CHECK_INTERVAL]))
|
||||
# The alarm flags are only read when something uses them; the binary sensor platform
|
||||
# turns this on as well. With an interrupt pin the check also releases INT/SQW.
|
||||
if any(
|
||||
key in config for key in (CONF_INTERRUPT_PIN, CONF_ON_ALARM_1, CONF_ON_ALARM_2)
|
||||
):
|
||||
cg.add(var.set_monitor_alarms(True))
|
||||
|
||||
# One callback for both alarms; each automation runs only for its own alarm number.
|
||||
for conf_key, alarm in ((CONF_ON_ALARM_1, 1), (CONF_ON_ALARM_2, 2)):
|
||||
if (conf := config.get(conf_key)) is not None:
|
||||
await automation.build_callback_automation(
|
||||
var,
|
||||
"add_on_alarm_callback",
|
||||
[],
|
||||
conf,
|
||||
params=[(cg.uint8, "alarm")],
|
||||
forward=[],
|
||||
when=f"alarm == {alarm}",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
# Actions
|
||||
# ---------------------------------------------------------------------------------------------
|
||||
|
||||
DS3231_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(DS3231Component)}
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.force_temperature_conversion",
|
||||
DS3231_ACTION_SCHEMA,
|
||||
automation.maybe_simple_id({cv.GenerateID(): cv.use_id(DS3231Component)}),
|
||||
automation.ApplyCall("force_temperature_conversion()"),
|
||||
)
|
||||
|
||||
|
||||
def _validate_alarm_day(config: ConfigType) -> ConfigType:
|
||||
mode = config[CONF_MODE]
|
||||
if mode == "monthly" and CONF_DAY_OF_MONTH not in config:
|
||||
raise cv.Invalid(f"'{CONF_DAY_OF_MONTH}' is required in monthly mode")
|
||||
if mode == "weekly" and CONF_DAY_OF_WEEK not in config:
|
||||
raise cv.Invalid(f"'{CONF_DAY_OF_WEEK}' is required in weekly mode")
|
||||
# The unused day still has to be a valid register value.
|
||||
config.setdefault(CONF_DAY_OF_MONTH, 1)
|
||||
config.setdefault(CONF_DAY_OF_WEEK, 1)
|
||||
return config
|
||||
|
||||
|
||||
def _alarm_schema(modes: dict, with_second: bool) -> cv.Schema:
|
||||
schema = {
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_MODE): cv.enum(modes, lower=True),
|
||||
cv.Optional(CONF_MINUTE, default=0): cv.templatable(
|
||||
cv.int_range(min=0, max=59)
|
||||
),
|
||||
cv.Optional(CONF_HOUR, default=0): cv.templatable(cv.int_range(min=0, max=23)),
|
||||
cv.Optional(CONF_DAY_OF_MONTH): cv.templatable(cv.int_range(min=1, max=31)),
|
||||
# 1 = Sunday ... 7 = Saturday, as in ESPHome's time component.
|
||||
cv.Optional(CONF_DAY_OF_WEEK): cv.templatable(cv.int_range(min=1, max=7)),
|
||||
cv.Optional(CONF_ENABLED, default=True): cv.templatable(cv.boolean),
|
||||
}
|
||||
if with_second:
|
||||
schema[cv.Optional(CONF_SECOND, default=0)] = cv.templatable(
|
||||
cv.int_range(min=0, max=59)
|
||||
)
|
||||
return cv.All(cv.Schema(schema), _validate_alarm_day)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.set_alarm_1",
|
||||
_alarm_schema(ALARM_1_MODES, with_second=True),
|
||||
automation.ApplyCall(
|
||||
"set_alarm_1({}, {}, {}, {}, {}, {}, {})",
|
||||
(
|
||||
(CONF_MODE, AlarmMode),
|
||||
(CONF_SECOND, cg.uint8),
|
||||
(CONF_MINUTE, cg.uint8),
|
||||
(CONF_HOUR, cg.uint8),
|
||||
(CONF_DAY_OF_MONTH, cg.uint8),
|
||||
(CONF_DAY_OF_WEEK, cg.uint8),
|
||||
(CONF_ENABLED, cg.bool_),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.set_alarm_2",
|
||||
_alarm_schema(ALARM_2_MODES, with_second=False),
|
||||
automation.ApplyCall(
|
||||
"set_alarm_2({}, {}, {}, {}, {}, {})",
|
||||
(
|
||||
(CONF_MODE, AlarmMode),
|
||||
(CONF_MINUTE, cg.uint8),
|
||||
(CONF_HOUR, cg.uint8),
|
||||
(CONF_DAY_OF_MONTH, cg.uint8),
|
||||
(CONF_DAY_OF_WEEK, cg.uint8),
|
||||
(CONF_ENABLED, cg.bool_),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
DS3231_ALARM_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_ALARM): cv.templatable(cv.int_range(min=1, max=2)),
|
||||
}
|
||||
)
|
||||
|
||||
for _name, _method in (
|
||||
("ds3231.enable_alarm", "enable_alarm"),
|
||||
("ds3231.disable_alarm", "disable_alarm"),
|
||||
("ds3231.clear_alarm", "clear_alarm"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name,
|
||||
DS3231_ALARM_ACTION_SCHEMA,
|
||||
automation.ApplyField(CONF_ALARM, _method, cg.uint8),
|
||||
)
|
||||
|
||||
automation.register_apply_condition(
|
||||
"ds3231.is_alarm_triggered",
|
||||
DS3231_ALARM_ACTION_SCHEMA,
|
||||
automation.ApplyCall("is_alarm_triggered({})", ((CONF_ALARM, cg.uint8),)),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.enable_square_wave",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_FREQUENCY): validate_square_wave_frequency,
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_FREQUENCY, "enable_square_wave", SquareWaveFrequency),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.disable_square_wave",
|
||||
DS3231_ACTION_SCHEMA,
|
||||
automation.ApplyCall("disable_square_wave()"),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.set_32khz_output",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_STATE): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_STATE, "set_output_32khz", cg.bool_),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.set_aging_offset",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(DS3231Component),
|
||||
cv.Required(CONF_OFFSET): cv.templatable(validate_aging_offset),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_OFFSET, "set_aging_offset", cg.int8),
|
||||
)
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, DEVICE_CLASS_PROBLEM, ENTITY_CATEGORY_DIAGNOSTIC
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DS3231_ID, DS3231Component
|
||||
|
||||
DEPENDENCIES = ["ds3231"]
|
||||
|
||||
CONF_ALARM_1 = "alarm_1"
|
||||
CONF_ALARM_2 = "alarm_2"
|
||||
CONF_OSCILLATOR_STOPPED = "oscillator_stopped"
|
||||
|
||||
ICON_ALARM = "mdi:alarm"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DS3231_ID): cv.use_id(DS3231Component),
|
||||
cv.Optional(CONF_ALARM_1): binary_sensor.binary_sensor_schema(
|
||||
icon=ICON_ALARM
|
||||
),
|
||||
cv.Optional(CONF_ALARM_2): binary_sensor.binary_sensor_schema(
|
||||
icon=ICON_ALARM
|
||||
),
|
||||
cv.Optional(CONF_OSCILLATOR_STOPPED): binary_sensor.binary_sensor_schema(
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key(CONF_ALARM_1, CONF_ALARM_2, CONF_OSCILLATOR_STOPPED),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DS3231_ID])
|
||||
binary_sensors = binary_sensor.sub_binary_sensors(config)
|
||||
await binary_sensors(CONF_ALARM_1, hub.set_alarm_1_binary_sensor)
|
||||
await binary_sensors(CONF_ALARM_2, hub.set_alarm_2_binary_sensor)
|
||||
await binary_sensors(
|
||||
CONF_OSCILLATOR_STOPPED, hub.set_oscillator_stopped_binary_sensor
|
||||
)
|
||||
if CONF_ALARM_1 in config or CONF_ALARM_2 in config:
|
||||
cg.add(hub.set_monitor_alarms(True))
|
||||
@@ -9,184 +9,29 @@ namespace esphome::ds3231 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ds3231");
|
||||
|
||||
static constexpr uint8_t DS3231_REG_SECONDS = 0x00;
|
||||
static constexpr uint8_t DS3231_REG_ALARM_1 = 0x07;
|
||||
static constexpr uint8_t DS3231_REG_ALARM_2 = 0x0B;
|
||||
static constexpr uint8_t DS3231_REG_CONTROL = 0x0E;
|
||||
static constexpr uint8_t DS3231_REG_STATUS = 0x0F;
|
||||
static constexpr uint8_t DS3231_REG_AGING_OFFSET = 0x10;
|
||||
static constexpr uint8_t DS3231_REG_TEMPERATURE = 0x11;
|
||||
|
||||
static constexpr uint8_t ALARM_MASK_BIT = 0x80; // AxMy: this register is ignored when matching
|
||||
static constexpr uint8_t ALARM_DAY_OF_WEEK = 0x40; // DY/DT: match day of week instead of date
|
||||
|
||||
/// How long an alarm binary sensor stays on when the flag is cleared automatically.
|
||||
static constexpr uint32_t ALARM_PULSE_MS = 1000;
|
||||
|
||||
static const LogString *alarm_state_to_string(uint8_t control, uint8_t alarm) {
|
||||
return (control & (alarm == 1 ? 0x01 : 0x02)) != 0 ? LOG_STR("enabled") : LOG_STR("disabled");
|
||||
}
|
||||
|
||||
static const LogString *square_wave_to_string(SquareWaveFrequency frequency) {
|
||||
switch (frequency) {
|
||||
case SQUARE_WAVE_1HZ:
|
||||
return LOG_STR("1 Hz");
|
||||
case SQUARE_WAVE_1024HZ:
|
||||
return LOG_STR("1.024 kHz");
|
||||
case SQUARE_WAVE_4096HZ:
|
||||
return LOG_STR("4.096 kHz");
|
||||
case SQUARE_WAVE_8192HZ:
|
||||
return LOG_STR("8.192 kHz");
|
||||
default:
|
||||
return LOG_STR("unknown");
|
||||
}
|
||||
}
|
||||
|
||||
void IRAM_ATTR DS3231Component::interrupt_handler(DS3231Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void DS3231Component::setup() {
|
||||
if (!this->read_control_status_()) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep the oscillator running on battery. INT/SQW is the alarm interrupt line unless a square
|
||||
// wave is configured. The alarm interrupt enables (A1IE/A2IE) are kept as they are: the alarms
|
||||
// live on the chip and survive a reboot of the ESP on battery.
|
||||
uint8_t mask = CONTROL_EOSC | CONTROL_INTCN;
|
||||
uint8_t value = CONTROL_INTCN;
|
||||
if (this->square_wave_configured_) {
|
||||
mask |= CONTROL_RS_MASK;
|
||||
value = this->square_wave_frequency_ << CONTROL_RS_SHIFT;
|
||||
}
|
||||
if (this->battery_backed_square_wave_ >= 0) {
|
||||
mask |= CONTROL_BBSQW;
|
||||
assign_bits(value, CONTROL_BBSQW, this->battery_backed_square_wave_ != 0);
|
||||
}
|
||||
if (!this->update_control_(mask, value)) {
|
||||
this->control_reg_ &= ~CONTROL_EOSC; // keep the oscillator running on battery
|
||||
// INT/SQW is the (unused) interrupt line rather than a square-wave output.
|
||||
this->control_reg_ |= CONTROL_INTCN;
|
||||
if (!this->write_control_()) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->output_32khz_ >= 0) {
|
||||
assign_bits(this->status_reg_, STATUS_EN32KHZ, this->output_32khz_ != 0);
|
||||
if (!this->write_status_(0)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->aging_offset_configured_ && !this->write_aging_offset_(this->aging_offset_)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
if ((this->status_reg_ & STATUS_OSF) != 0) {
|
||||
ESP_LOGW(TAG, "Oscillator was stopped (battery empty or missing?), the stored time is invalid.");
|
||||
}
|
||||
this->publish_oscillator_stopped_();
|
||||
|
||||
if (!this->monitor_alarms_) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start from a known state; an alarm that fired while the ESP was off is picked up by the first
|
||||
// check, which runs from the loop right after setup.
|
||||
this->publish_alarm_state_(1, false);
|
||||
this->publish_alarm_state_(2, false);
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
// INT/SQW is open drain and active low: a falling edge means an enabled alarm matched.
|
||||
// The handler only wakes the loop; the flags are read from there.
|
||||
this->interrupt_pin_->attach_interrupt(&DS3231Component::interrupt_handler, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
// Without an interrupt pin this is how alarms are seen. With one it is a fallback: it picks up an
|
||||
// edge that was missed, and latched flags (auto_clear_alarms: false) that were cleared elsewhere.
|
||||
this->set_interval(this->alarm_check_interval_, [this]() { this->check_alarms_(); });
|
||||
}
|
||||
|
||||
void DS3231Component::loop() {
|
||||
// Only runs once after setup and when woken by the interrupt pin.
|
||||
this->disable_loop();
|
||||
this->check_alarms_();
|
||||
}
|
||||
|
||||
void DS3231Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DS3231:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed())
|
||||
return;
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
if ((this->control_reg_ & CONTROL_INTCN) == 0) {
|
||||
ESP_LOGCONFIG(TAG, " INT/SQW: square wave %s",
|
||||
LOG_STR_ARG(square_wave_to_string(
|
||||
static_cast<SquareWaveFrequency>((this->control_reg_ & CONTROL_RS_MASK) >> CONTROL_RS_SHIFT))));
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " INT/SQW: alarm interrupt");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Battery-backed square wave: %s\n"
|
||||
" 32 kHz output: %s\n"
|
||||
" Alarm 1: %s\n"
|
||||
" Alarm 2: %s\n"
|
||||
" Auto clear alarms: %s",
|
||||
YESNO(this->control_reg_ & CONTROL_BBSQW), ONOFF(this->status_reg_ & STATUS_EN32KHZ),
|
||||
LOG_STR_ARG(alarm_state_to_string(this->control_reg_, 1)),
|
||||
LOG_STR_ARG(alarm_state_to_string(this->control_reg_, 2)), YESNO(this->auto_clear_alarms_));
|
||||
if (this->aging_offset_configured_) {
|
||||
ESP_LOGCONFIG(TAG, " Aging offset: %d", this->aging_offset_);
|
||||
}
|
||||
if (this->monitor_alarms_) {
|
||||
ESP_LOGCONFIG(TAG, " Alarm check interval: %" PRIu32 " ms", this->alarm_check_interval_);
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Alarm 1", this->alarm_1_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Alarm 2", this->alarm_2_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Oscillator Stopped", this->oscillator_stopped_binary_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Clock
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
bool DS3231Component::read_calendar(uint8_t *raw) {
|
||||
if (this->is_failed() || !this->status_comm_result(this->read_bytes(DS3231_REG_SECONDS, raw, CALENDAR_LEN)))
|
||||
return false;
|
||||
// Without a valid status the oscillator-stop flag is unknown, so the time can't be trusted.
|
||||
if (!this->read_control_status_())
|
||||
return false;
|
||||
this->publish_oscillator_stopped_();
|
||||
if ((this->status_reg_ & STATUS_OSF) != 0) {
|
||||
ESP_LOGW(TAG, "Oscillator stop flag set, the RTC time is invalid. Write a valid time first.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS3231Component::write_calendar(const uint8_t *raw) {
|
||||
if (this->is_failed() || !this->status_comm_result(this->write_bytes(DS3231_REG_SECONDS, raw, CALENDAR_LEN)))
|
||||
return false;
|
||||
|
||||
// The time is valid again: clear the oscillator stop flag. If that fails, the RTC would keep
|
||||
// reporting its time as invalid, so the write is not reported as successful.
|
||||
if (!this->read_control_status_())
|
||||
return false;
|
||||
if ((this->status_reg_ & STATUS_OSF) != 0) {
|
||||
if (!this->write_status_(STATUS_OSF))
|
||||
return false;
|
||||
this->status_reg_ &= ~STATUS_OSF;
|
||||
}
|
||||
this->publish_oscillator_stopped_();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Temperature
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
bool DS3231Component::read_temperature(float &out) {
|
||||
uint8_t raw[2];
|
||||
if (this->is_failed() || !this->read_bytes(DS3231_REG_TEMPERATURE, raw, sizeof(raw))) {
|
||||
@@ -208,280 +53,26 @@ void DS3231Component::force_temperature_conversion() {
|
||||
ESP_LOGW(TAG, "A temperature conversion is already in progress.");
|
||||
return;
|
||||
}
|
||||
// CONV is a one-shot command bit, so it is not kept in the cached value.
|
||||
this->status_comm_result(this->write_byte(DS3231_REG_CONTROL, this->control_reg_ | CONTROL_CONV));
|
||||
this->control_reg_ |= CONTROL_CONV;
|
||||
this->write_control_();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Alarms
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
void DS3231Component::set_alarm_1(AlarmMode mode, uint8_t second, uint8_t minute, uint8_t hour, uint8_t day_of_month,
|
||||
uint8_t day_of_week, bool enabled) {
|
||||
if (second > 59 || minute > 59 || hour > 23 || day_of_month < 1 || day_of_month > 31 || day_of_week < 1 ||
|
||||
day_of_week > 7 || mode > ALARM_MODE_WEEKLY) {
|
||||
ESP_LOGE(TAG, "Invalid alarm 1 setting.");
|
||||
return;
|
||||
}
|
||||
// Mask bits A1M1..A1M4: a set bit means the register is ignored when matching.
|
||||
uint8_t regs[4] = {
|
||||
uint8_to_bcd(second),
|
||||
uint8_to_bcd(minute),
|
||||
uint8_to_bcd(hour),
|
||||
mode == ALARM_MODE_WEEKLY ? static_cast<uint8_t>(ALARM_DAY_OF_WEEK | day_of_week) : uint8_to_bcd(day_of_month),
|
||||
};
|
||||
switch (mode) {
|
||||
case ALARM_MODE_EVERY_SECOND:
|
||||
regs[0] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_EVERY_MINUTE:
|
||||
regs[1] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_HOURLY:
|
||||
regs[2] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_DAILY:
|
||||
regs[3] |= ALARM_MASK_BIT;
|
||||
break;
|
||||
default: // MONTHLY / WEEKLY: everything matches
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting alarm 1: mode %u, %02u:%02u:%02u, day of month %u, day of week %u, %s", mode, hour, minute,
|
||||
second, day_of_month, day_of_week, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
|
||||
this->write_alarm_(1, regs, sizeof(regs), enabled);
|
||||
}
|
||||
|
||||
void DS3231Component::set_alarm_2(AlarmMode mode, uint8_t minute, uint8_t hour, uint8_t day_of_month,
|
||||
uint8_t day_of_week, bool enabled) {
|
||||
if (minute > 59 || hour > 23 || day_of_month < 1 || day_of_month > 31 || day_of_week < 1 || day_of_week > 7 ||
|
||||
mode == ALARM_MODE_EVERY_SECOND || mode > ALARM_MODE_WEEKLY) {
|
||||
ESP_LOGE(TAG, "Invalid alarm 2 setting.");
|
||||
return;
|
||||
}
|
||||
// Mask bits A2M2..A2M4.
|
||||
uint8_t regs[3] = {
|
||||
uint8_to_bcd(minute),
|
||||
uint8_to_bcd(hour),
|
||||
mode == ALARM_MODE_WEEKLY ? static_cast<uint8_t>(ALARM_DAY_OF_WEEK | day_of_week) : uint8_to_bcd(day_of_month),
|
||||
};
|
||||
switch (mode) {
|
||||
case ALARM_MODE_EVERY_MINUTE:
|
||||
regs[0] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_HOURLY:
|
||||
regs[1] |= ALARM_MASK_BIT;
|
||||
[[fallthrough]];
|
||||
case ALARM_MODE_DAILY:
|
||||
regs[2] |= ALARM_MASK_BIT;
|
||||
break;
|
||||
default: // MONTHLY / WEEKLY: everything matches
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting alarm 2: mode %u, %02u:%02u, day of month %u, day of week %u, %s", mode, hour, minute,
|
||||
day_of_month, day_of_week, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
|
||||
this->write_alarm_(2, regs, sizeof(regs), enabled);
|
||||
}
|
||||
|
||||
void DS3231Component::write_alarm_(uint8_t alarm, const uint8_t *regs, size_t len, bool enabled) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (!this->status_comm_result(this->write_bytes(alarm == 1 ? DS3231_REG_ALARM_1 : DS3231_REG_ALARM_2, regs, len)))
|
||||
return;
|
||||
// Changing the registers may have matched on the way: start from a clean flag. Enabling the
|
||||
// alarm with a stale flag would assert INT/SQW right away.
|
||||
if (!this->clear_alarm_flag_(alarm)) {
|
||||
ESP_LOGW(TAG, "Alarm %u written, but its flag could not be cleared; not changing its enable.", alarm);
|
||||
return;
|
||||
}
|
||||
this->set_alarm_enabled_(alarm, enabled);
|
||||
}
|
||||
|
||||
void DS3231Component::set_alarm_enabled_(uint8_t alarm, bool enabled) {
|
||||
if (alarm != 1 && alarm != 2) {
|
||||
ESP_LOGE(TAG, "Invalid alarm number %u.", alarm);
|
||||
return;
|
||||
}
|
||||
if (this->is_failed())
|
||||
return;
|
||||
const uint8_t bit = alarm == 1 ? CONTROL_A1IE : CONTROL_A2IE;
|
||||
if (!this->update_control_(bit, enabled ? bit : 0))
|
||||
return;
|
||||
if (enabled && (this->control_reg_ & CONTROL_INTCN) == 0) {
|
||||
ESP_LOGW(TAG, "Alarm %u enabled, but INT/SQW is a square-wave output: only the flag will be set.", alarm);
|
||||
}
|
||||
ESP_LOGD(TAG, "Alarm %u %s.", alarm, enabled ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
|
||||
}
|
||||
|
||||
void DS3231Component::enable_alarm(uint8_t alarm) {
|
||||
// The chip sets the flag on every match even while the alarm is disabled; enabling with it set
|
||||
// would fire the trigger (and INT/SQW) for that old match right away.
|
||||
if (this->clear_alarm_flag_(alarm))
|
||||
this->set_alarm_enabled_(alarm, true);
|
||||
}
|
||||
|
||||
void DS3231Component::clear_alarm(uint8_t alarm) { this->clear_alarm_flag_(alarm); }
|
||||
|
||||
bool DS3231Component::clear_alarm_flag_(uint8_t alarm) {
|
||||
if (alarm != 1 && alarm != 2) {
|
||||
ESP_LOGE(TAG, "Invalid alarm number %u.", alarm);
|
||||
return false;
|
||||
}
|
||||
if (this->is_failed() || !this->read_control_status_())
|
||||
return false;
|
||||
const uint8_t flag = alarm == 1 ? STATUS_A1F : STATUS_A2F;
|
||||
if ((this->status_reg_ & flag) != 0) {
|
||||
// On failure the flag is still set on the chip: keep reporting the alarm as it is.
|
||||
if (!this->write_status_(flag))
|
||||
return false;
|
||||
this->status_reg_ &= ~flag;
|
||||
}
|
||||
this->alarm_active_[alarm - 1] = false;
|
||||
this->publish_alarm_state_(alarm, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS3231Component::is_alarm_triggered(uint8_t alarm) {
|
||||
if (this->is_failed() || !this->read_control_status_())
|
||||
return false;
|
||||
return (this->status_reg_ & (alarm == 1 ? STATUS_A1F : STATUS_A2F)) != 0;
|
||||
}
|
||||
|
||||
void DS3231Component::check_alarms_() {
|
||||
if (this->is_failed() || !this->read_control_status_())
|
||||
return;
|
||||
this->publish_oscillator_stopped_();
|
||||
|
||||
uint8_t to_clear = 0;
|
||||
for (uint8_t alarm = 1; alarm <= 2; alarm++) {
|
||||
const uint8_t flag = alarm == 1 ? STATUS_A1F : STATUS_A2F;
|
||||
const uint8_t enable = alarm == 1 ? CONTROL_A1IE : CONTROL_A2IE;
|
||||
const bool triggered = (this->status_reg_ & flag) != 0;
|
||||
const bool enabled = (this->control_reg_ & enable) != 0;
|
||||
bool &active = this->alarm_active_[alarm - 1];
|
||||
|
||||
if (!triggered) {
|
||||
if (active && !this->auto_clear_alarms_)
|
||||
this->publish_alarm_state_(alarm, false);
|
||||
active = false;
|
||||
continue;
|
||||
}
|
||||
// The flag is set on every match even when the alarm is disabled; only enabled alarms count.
|
||||
if (enabled && !active) {
|
||||
ESP_LOGI(TAG, "Alarm %u triggered.", alarm);
|
||||
active = true;
|
||||
this->publish_alarm_state_(alarm, true);
|
||||
this->alarm_callback_.call(alarm);
|
||||
}
|
||||
if (this->auto_clear_alarms_)
|
||||
to_clear |= flag;
|
||||
}
|
||||
|
||||
if (to_clear != 0 && this->write_status_(to_clear)) {
|
||||
this->status_reg_ &= ~to_clear;
|
||||
for (uint8_t alarm = 1; alarm <= 2; alarm++) {
|
||||
if ((to_clear & (alarm == 1 ? STATUS_A1F : STATUS_A2F)) == 0 || !this->alarm_active_[alarm - 1])
|
||||
continue;
|
||||
this->alarm_active_[alarm - 1] = false;
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
// Keep the binary sensor on long enough for the state to be seen, then turn it off again.
|
||||
this->set_timeout(ALARM_PULSE_TIMEOUT_ID + alarm - 1, ALARM_PULSE_MS,
|
||||
[this, alarm]() { this->publish_alarm_state_(alarm, false); });
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DS3231Component::publish_alarm_state_(uint8_t alarm, bool state) {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
auto *sensor = alarm == 1 ? this->alarm_1_binary_sensor_ : this->alarm_2_binary_sensor_;
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state(state);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DS3231Component::publish_oscillator_stopped_() {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->oscillator_stopped_binary_sensor_ != nullptr)
|
||||
this->oscillator_stopped_binary_sensor_->publish_state((this->status_reg_ & STATUS_OSF) != 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Outputs and calibration
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
void DS3231Component::enable_square_wave(SquareWaveFrequency frequency) {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
ESP_LOGE(TAG, "Can't enable the square wave: INT/SQW is used as the alarm interrupt pin.");
|
||||
return;
|
||||
}
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (this->update_control_(CONTROL_INTCN | CONTROL_RS_MASK, frequency << CONTROL_RS_SHIFT)) {
|
||||
ESP_LOGD(TAG, "Square wave enabled at %s.", LOG_STR_ARG(square_wave_to_string(frequency)));
|
||||
}
|
||||
}
|
||||
|
||||
void DS3231Component::disable_square_wave() {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (this->update_control_(CONTROL_INTCN, CONTROL_INTCN)) {
|
||||
ESP_LOGD(TAG, "Square wave disabled, INT/SQW is the alarm interrupt.");
|
||||
}
|
||||
}
|
||||
|
||||
void DS3231Component::set_output_32khz(bool enabled) {
|
||||
if (this->is_failed() || !this->read_control_status_())
|
||||
return;
|
||||
assign_bits(this->status_reg_, STATUS_EN32KHZ, enabled);
|
||||
if (this->write_status_(0)) {
|
||||
ESP_LOGD(TAG, "32 kHz output %s.", ONOFF(enabled));
|
||||
}
|
||||
}
|
||||
|
||||
void DS3231Component::set_aging_offset(int8_t offset) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
if (!this->write_aging_offset_(offset))
|
||||
return;
|
||||
ESP_LOGD(TAG, "Aging offset set to %d.", offset);
|
||||
// The new offset takes effect with the next temperature conversion.
|
||||
this->force_temperature_conversion();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Register access
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
bool DS3231Component::read_control_status_() {
|
||||
uint8_t raw[2];
|
||||
if (!this->status_comm_result(this->read_bytes(DS3231_REG_CONTROL, raw, sizeof(raw))))
|
||||
if (!this->read_bytes(DS3231_REG_CONTROL, raw, sizeof(raw))) {
|
||||
ESP_LOGW(TAG, "Can't read control/status registers.");
|
||||
return false;
|
||||
this->control_reg_ = raw[0] & ~CONTROL_CONV;
|
||||
}
|
||||
this->control_reg_ = raw[0];
|
||||
this->status_reg_ = raw[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS3231Component::update_control_(uint8_t mask, uint8_t value) {
|
||||
// CONV is a one-shot command bit: always write it back as 0 so a control change never starts
|
||||
// a temperature conversion.
|
||||
if (!this->status_comm_result(this->update_register(DS3231_REG_CONTROL, mask | CONTROL_CONV, value)))
|
||||
bool DS3231Component::write_control_() {
|
||||
if (!this->write_byte(DS3231_REG_CONTROL, this->control_reg_)) {
|
||||
ESP_LOGW(TAG, "Can't write control register.");
|
||||
return false;
|
||||
this->control_reg_ = (this->control_reg_ & ~mask) | (value & mask);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS3231Component::write_status_(uint8_t clear) {
|
||||
// The flags can only be written to 0; writing 1 leaves them unchanged, so a flag that was set
|
||||
// between the read and this write is never lost.
|
||||
const uint8_t value = ((this->status_reg_ & STATUS_EN32KHZ) | STATUS_FLAGS) & ~clear;
|
||||
return this->status_comm_result(this->write_byte(DS3231_REG_STATUS, value));
|
||||
}
|
||||
|
||||
bool DS3231Component::write_aging_offset_(int8_t offset) {
|
||||
if (!this->status_comm_result(this->write_byte(DS3231_REG_AGING_OFFSET, static_cast<uint8_t>(offset))))
|
||||
return false;
|
||||
this->aging_offset_ = offset;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,163 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::ds3231 {
|
||||
|
||||
/// When an alarm fires. Alarm 1 supports every mode, alarm 2 has no seconds register and so no EVERY_SECOND.
|
||||
enum AlarmMode : uint8_t {
|
||||
ALARM_MODE_EVERY_SECOND = 0, ///< Alarm 1 only.
|
||||
ALARM_MODE_EVERY_MINUTE, ///< Alarm 1: when the seconds match. Alarm 2: at second 00 of every minute.
|
||||
ALARM_MODE_HOURLY, ///< When minutes (and seconds) match.
|
||||
ALARM_MODE_DAILY, ///< When hours, minutes (and seconds) match.
|
||||
ALARM_MODE_MONTHLY, ///< When date, hours, minutes (and seconds) match.
|
||||
ALARM_MODE_WEEKLY, ///< When day of week, hours, minutes (and seconds) match.
|
||||
};
|
||||
|
||||
/// Frequency of the INT/SQW pin when it is a square-wave output (RS2:RS1 in the control register).
|
||||
enum SquareWaveFrequency : uint8_t {
|
||||
SQUARE_WAVE_1HZ = 0,
|
||||
SQUARE_WAVE_1024HZ = 1,
|
||||
SQUARE_WAVE_4096HZ = 2,
|
||||
SQUARE_WAVE_8192HZ = 3,
|
||||
};
|
||||
|
||||
/// Hub for a single DS3231 real-time clock. Owns all register access; the platforms talk to the
|
||||
/// chip through this class.
|
||||
class DS3231Component final : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
/// Seconds through year, the 7-byte BCD calendar the time platform decodes with time::DS_BCD_CALENDAR.
|
||||
static constexpr size_t CALENDAR_LEN = 7;
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
// ---- Configuration (set from codegen) ----
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_square_wave_frequency(SquareWaveFrequency frequency) {
|
||||
this->square_wave_frequency_ = frequency;
|
||||
this->square_wave_configured_ = true;
|
||||
}
|
||||
void set_battery_backed_square_wave(bool enabled) { this->battery_backed_square_wave_ = enabled ? 1 : 0; }
|
||||
void set_output_32khz_config(bool enabled) { this->output_32khz_ = enabled ? 1 : 0; }
|
||||
void set_aging_offset_config(int8_t offset) {
|
||||
this->aging_offset_ = offset;
|
||||
this->aging_offset_configured_ = true;
|
||||
}
|
||||
void set_auto_clear_alarms(bool auto_clear) { this->auto_clear_alarms_ = auto_clear; }
|
||||
void set_alarm_check_interval(uint32_t interval) { this->alarm_check_interval_ = interval; }
|
||||
void set_monitor_alarms(bool monitor) { this->monitor_alarms_ = monitor; }
|
||||
|
||||
/// Register a void(uint8_t alarm) callback that runs when an enabled alarm (1 or 2) matches.
|
||||
template<typename F> void add_on_alarm_callback(F &&callback) {
|
||||
this->alarm_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(alarm_1)
|
||||
SUB_BINARY_SENSOR(alarm_2)
|
||||
SUB_BINARY_SENSOR(oscillator_stopped)
|
||||
#endif
|
||||
|
||||
// ---- Clock registers (used by the time platform) ----
|
||||
/// Read the calendar registers into @p raw. Fails if the oscillator was stopped since the time was last set.
|
||||
bool read_calendar(uint8_t *raw);
|
||||
/// Write the calendar registers from @p raw and clear the oscillator-stop flag.
|
||||
bool write_calendar(const uint8_t *raw);
|
||||
|
||||
// ---- Temperature ----
|
||||
bool read_temperature(float &out);
|
||||
void force_temperature_conversion();
|
||||
|
||||
// ---- Alarms ----
|
||||
/// Program alarm 1. @p day_of_month is used in MONTHLY mode, @p day_of_week (1 = Sunday) in WEEKLY mode.
|
||||
void set_alarm_1(AlarmMode mode, uint8_t second, uint8_t minute, uint8_t hour, uint8_t day_of_month,
|
||||
uint8_t day_of_week, bool enabled);
|
||||
/// Program alarm 2 (minute resolution, fires at second 00).
|
||||
void set_alarm_2(AlarmMode mode, uint8_t minute, uint8_t hour, uint8_t day_of_month, uint8_t day_of_week,
|
||||
bool enabled);
|
||||
/// Enable @p alarm. A match from while it was disabled is cleared first, so only the next one fires.
|
||||
void enable_alarm(uint8_t alarm);
|
||||
void disable_alarm(uint8_t alarm) { this->set_alarm_enabled_(alarm, false); }
|
||||
/// Clear the alarm flag, which also releases the INT/SQW line.
|
||||
void clear_alarm(uint8_t alarm);
|
||||
/// True while the alarm flag of @p alarm (1 or 2) is set on the chip.
|
||||
bool is_alarm_triggered(uint8_t alarm);
|
||||
|
||||
// ---- Outputs and calibration ----
|
||||
/// Switch INT/SQW to a square-wave output at @p frequency. Alarms then no longer drive the pin.
|
||||
void enable_square_wave(SquareWaveFrequency frequency);
|
||||
/// Switch INT/SQW back to the (active-low) alarm interrupt output.
|
||||
void disable_square_wave();
|
||||
void set_output_32khz(bool enabled);
|
||||
/// Aging offset in LSB (about 0.1 ppm each at 25 degC); positive values slow the clock down.
|
||||
void set_aging_offset(int8_t offset);
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t CONTROL_A1IE = 0x01;
|
||||
static constexpr uint8_t CONTROL_A2IE = 0x02;
|
||||
static constexpr uint8_t CONTROL_INTCN = 0x04;
|
||||
static constexpr uint8_t CONTROL_RS_MASK = 0x18;
|
||||
static constexpr uint8_t CONTROL_RS_SHIFT = 3;
|
||||
static constexpr uint8_t CONTROL_CONV = 0x20;
|
||||
static constexpr uint8_t CONTROL_BBSQW = 0x40;
|
||||
static constexpr uint8_t CONTROL_EOSC = 0x80;
|
||||
|
||||
static constexpr uint8_t STATUS_A1F = 0x01;
|
||||
static constexpr uint8_t STATUS_A2F = 0x02;
|
||||
static constexpr uint8_t STATUS_BSY = 0x04;
|
||||
static constexpr uint8_t STATUS_EN32KHZ = 0x08;
|
||||
static constexpr uint8_t STATUS_OSF = 0x80;
|
||||
static constexpr uint8_t STATUS_FLAGS = STATUS_A1F | STATUS_A2F | STATUS_OSF;
|
||||
|
||||
/// Scheduler ids: one pulse timer per alarm (id + alarm - 1).
|
||||
static constexpr uint32_t ALARM_PULSE_TIMEOUT_ID = 0;
|
||||
|
||||
static void interrupt_handler(DS3231Component *arg);
|
||||
|
||||
/// Read the control and status registers into the cached copies. Sets the warning flag on failure.
|
||||
bool read_control_status_();
|
||||
/// Replace the bits in @p mask of the control register with @p value, on the chip and in the cache.
|
||||
bool update_control_(uint8_t mask, uint8_t value);
|
||||
/// Write the status register, clearing the flags in @p clear (writing 1 to a flag leaves it unchanged).
|
||||
bool write_status_(uint8_t clear);
|
||||
bool write_aging_offset_(int8_t offset);
|
||||
/// Clear the flag of @p alarm and report it off. Returns false (state unchanged) if that fails.
|
||||
bool clear_alarm_flag_(uint8_t alarm);
|
||||
void set_alarm_enabled_(uint8_t alarm, bool enabled);
|
||||
/// Write the alarm registers, clear the stale flag and set the interrupt enable.
|
||||
void write_alarm_(uint8_t alarm, const uint8_t *regs, size_t len, bool enabled);
|
||||
/// Read the flags, fire triggers and update the binary sensors.
|
||||
void check_alarms_();
|
||||
void publish_alarm_state_(uint8_t alarm, bool state);
|
||||
void publish_oscillator_stopped_();
|
||||
bool write_control_();
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
LazyCallbackManager<void(uint8_t)> alarm_callback_;
|
||||
|
||||
uint32_t alarm_check_interval_{1000};
|
||||
uint8_t control_reg_{0};
|
||||
uint8_t status_reg_{0};
|
||||
int8_t aging_offset_{0};
|
||||
SquareWaveFrequency square_wave_frequency_{SQUARE_WAVE_1HZ};
|
||||
/// -1: leave the chip as it is, 0: off, 1: on.
|
||||
int8_t battery_backed_square_wave_{-1};
|
||||
int8_t output_32khz_{-1};
|
||||
bool square_wave_configured_{false};
|
||||
bool aging_offset_configured_{false};
|
||||
bool auto_clear_alarms_{true};
|
||||
bool monitor_alarms_{false};
|
||||
/// Set while an alarm flag has been reported, so a latched flag fires its trigger only once.
|
||||
bool alarm_active_[2]{false, false};
|
||||
};
|
||||
|
||||
} // namespace esphome::ds3231
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DS3231_ID, DS3231Component, ds3231_ns
|
||||
|
||||
DEPENDENCIES = ["ds3231"]
|
||||
|
||||
DS3231Time = ds3231_ns.class_(
|
||||
"DS3231Time", time.RealTimeClock, cg.Parented.template(DS3231Component)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DS3231Time),
|
||||
cv.GenerateID(CONF_DS3231_ID): cv.use_id(DS3231Component),
|
||||
}
|
||||
)
|
||||
|
||||
DS3231_TIME_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(DS3231Time)}
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.write_time", DS3231_TIME_ACTION_SCHEMA, automation.ApplyCall("write_time()")
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"ds3231.read_time", DS3231_TIME_ACTION_SCHEMA, automation.ApplyCall("read_time()")
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_DS3231_ID])
|
||||
await time.register_time(var, config)
|
||||
@@ -1,48 +0,0 @@
|
||||
#include "ds3231_time.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ds3231 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ds3231.time");
|
||||
|
||||
void DS3231Time::update() { this->read_time(); }
|
||||
|
||||
void DS3231Time::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DS3231 Time:");
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
void DS3231Time::read_time() {
|
||||
uint8_t raw[DS3231Component::CALENDAR_LEN];
|
||||
if (!this->status_comm_result(this->parent_->read_calendar(raw), LOG_STR("No valid time from the RTC")))
|
||||
return;
|
||||
const ESPTime rtc_time = time::decode_bcd_calendar<time::DS_BCD_CALENDAR>(raw);
|
||||
ESP_LOGD(TAG, "Read UTC time: %04u-%02u-%02u %02u:%02u:%02u (day of week %u)", rtc_time.year, rtc_time.month,
|
||||
rtc_time.day_of_month, rtc_time.hour, rtc_time.minute, rtc_time.second, rtc_time.day_of_week);
|
||||
// The day of week is a free-running counter on the DS3231 and may have been set with a different
|
||||
// numbering by other software, so it is not used to validate the time.
|
||||
if (!this->status_comm_result(rtc_time.is_valid(/*check_day_of_week=*/false, /*check_day_of_year=*/false),
|
||||
LOG_STR("Invalid RTC time")))
|
||||
return;
|
||||
this->synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
void DS3231Time::write_time() {
|
||||
auto now = this->utcnow();
|
||||
if (!now.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
|
||||
return;
|
||||
}
|
||||
if (now.year > 2199) {
|
||||
ESP_LOGE(TAG, "Year %u is out of the DS3231 range (2000-2199).", now.year);
|
||||
return;
|
||||
}
|
||||
uint8_t raw[DS3231Component::CALENDAR_LEN];
|
||||
time::encode_bcd_calendar<time::DS_BCD_CALENDAR>(now, raw);
|
||||
if (!this->status_comm_result(this->parent_->write_calendar(raw)))
|
||||
return;
|
||||
ESP_LOGD(TAG, "Wrote UTC time: %04u-%02u-%02u %02u:%02u:%02u", now.year, now.month, now.day_of_month, now.hour,
|
||||
now.minute, now.second);
|
||||
}
|
||||
|
||||
} // namespace esphome::ds3231
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
#include "../ds3231.h"
|
||||
|
||||
namespace esphome::ds3231 {
|
||||
|
||||
class DS3231Time final : public time::RealTimeClock, public Parented<DS3231Component> {
|
||||
public:
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Set the system clock from the RTC.
|
||||
void read_time();
|
||||
/// Store the system clock in the RTC.
|
||||
void write_time();
|
||||
};
|
||||
|
||||
} // namespace esphome::ds3231
|
||||
@@ -2213,22 +2213,6 @@ def idf_version() -> cv.Version:
|
||||
return CORE.data[KEY_ESP32][KEY_IDF_VERSION]
|
||||
|
||||
|
||||
def require_idf_version(
|
||||
minimum: cv.Version, feature: str = "This feature"
|
||||
) -> Callable[[Any], Any]:
|
||||
"""Validator for an option that needs the underlying ESP-IDF at ``minimum`` or newer."""
|
||||
|
||||
def validator(value: Any) -> Any:
|
||||
cv.only_on_esp32(value)
|
||||
if (version := idf_version()) < minimum:
|
||||
raise cv.Invalid(
|
||||
f"{feature} requires ESP-IDF {minimum} or newer, got {version}"
|
||||
)
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def require_fatfs() -> None:
|
||||
"""Mark that FATFS support is required by a component.
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
RESTORE_STATE_NONE,
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
@@ -57,6 +56,7 @@ from .effects import (
|
||||
)
|
||||
from .restore_state import (
|
||||
LEGACY_RESTORE_MODES,
|
||||
RESTORE_STATE_NONE,
|
||||
RESTORE_STATE_SCHEMA,
|
||||
_build_state_lambda,
|
||||
_initial_state_overridden_by_legacy_mode,
|
||||
|
||||
@@ -11,14 +11,6 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_COLOR_MODE, CONF_STATE
|
||||
from esphome.core import Lambda
|
||||
from esphome.core.entity_helpers import (
|
||||
RESTORE_STATE_INITIAL,
|
||||
RESTORE_STATE_INVERT,
|
||||
RESTORE_STATE_KEEP,
|
||||
RESTORE_STATE_NONE,
|
||||
restore_state_keyword,
|
||||
restore_state_schema,
|
||||
)
|
||||
from esphome.cpp_generator import call_lambda
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
@@ -26,6 +18,12 @@ from esphome.types import ConfigType
|
||||
from .automation import LIGHT_STATE_FIELDS, validate_light_state
|
||||
from .types import ColorMode, LightStateRTCState
|
||||
|
||||
RESTORE_STATE_KEEP = "KEEP"
|
||||
RESTORE_STATE_INVERT = "INVERT"
|
||||
RESTORE_STATE_INITIAL = "INITIAL"
|
||||
RESTORE_STATE_ALL = "ALL"
|
||||
RESTORE_STATE_NONE = "NONE"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LegacyRestoreMode:
|
||||
@@ -292,12 +290,10 @@ def _keep_or(validator: Callable[[Any], Any]) -> Callable[[Any], Any]:
|
||||
except cv.Invalid:
|
||||
inner = ()
|
||||
return (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL, *inner)
|
||||
if (
|
||||
keyword := restore_state_keyword(
|
||||
value, (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL)
|
||||
)
|
||||
) is not None:
|
||||
return keyword
|
||||
if isinstance(value, str):
|
||||
upper = value.strip().upper()
|
||||
if upper in (RESTORE_STATE_KEEP, RESTORE_STATE_INITIAL):
|
||||
return upper
|
||||
return validator(value)
|
||||
|
||||
return validate
|
||||
@@ -312,12 +308,10 @@ def _validate_restore_state_state(value: Any) -> str | bool:
|
||||
RESTORE_STATE_INITIAL,
|
||||
*validate_light_state(SCHEMA_EXTRACT),
|
||||
)
|
||||
if (
|
||||
keyword := restore_state_keyword(
|
||||
value, (RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL)
|
||||
)
|
||||
) is not None:
|
||||
return keyword
|
||||
if isinstance(value, str):
|
||||
upper = value.strip().upper()
|
||||
if upper in (RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL):
|
||||
return upper
|
||||
return validate_light_state(value)
|
||||
|
||||
|
||||
@@ -333,7 +327,19 @@ _RESTORE_STATE_FIELDS_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
# A mapping of per-field overrides, `all`, or `none`, which stays the keyword for to_code
|
||||
RESTORE_STATE_SCHEMA = restore_state_schema(
|
||||
_RESTORE_STATE_FIELDS_SCHEMA, RESTORE_STATE_NONE
|
||||
)
|
||||
@schema_extractor("schema")
|
||||
def RESTORE_STATE_SCHEMA(value: Any) -> ConfigType | str:
|
||||
"""
|
||||
The restore_state: config key can be a mapping of per-field overrides, `all`, or `none`.
|
||||
"""
|
||||
if value == SCHEMA_EXTRACT:
|
||||
# The `all`/`none` string shorthands have no representation in the extracted
|
||||
# docs schema; only the per-field mapping form is walked here.
|
||||
return _RESTORE_STATE_FIELDS_SCHEMA
|
||||
if isinstance(value, str):
|
||||
upper = value.strip().upper()
|
||||
if upper == RESTORE_STATE_ALL:
|
||||
value = {}
|
||||
elif upper == RESTORE_STATE_NONE:
|
||||
return RESTORE_STATE_NONE
|
||||
return _RESTORE_STATE_FIELDS_SCHEMA(value)
|
||||
|
||||
@@ -35,12 +35,12 @@ class LockTraits {
|
||||
public:
|
||||
LockTraits() = default;
|
||||
|
||||
bool get_supports_open() const { return this->flags_.supports_open; }
|
||||
void set_supports_open(bool supports_open) { this->flags_.supports_open = supports_open; }
|
||||
bool get_requires_code() const { return this->flags_.requires_code; }
|
||||
void set_requires_code(bool requires_code) { this->flags_.requires_code = requires_code; }
|
||||
bool get_assumed_state() const { return this->flags_.assumed_state; }
|
||||
void set_assumed_state(bool assumed_state) { this->flags_.assumed_state = assumed_state; }
|
||||
bool get_supports_open() const { return this->supports_open_; }
|
||||
void set_supports_open(bool supports_open) { this->supports_open_ = supports_open; }
|
||||
bool get_requires_code() const { return this->requires_code_; }
|
||||
void set_requires_code(bool requires_code) { this->requires_code_ = requires_code; }
|
||||
bool get_assumed_state() const { return this->assumed_state_; }
|
||||
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
|
||||
|
||||
bool supports_state(LockState state) const { return supported_states_mask_ & (1 << state); }
|
||||
void set_supported_states(std::initializer_list<LockState> states) {
|
||||
@@ -54,13 +54,9 @@ class LockTraits {
|
||||
void add_supported_state(LockState state) { supported_states_mask_ |= (1 << state); }
|
||||
|
||||
protected:
|
||||
// Bit-packed flags to save memory (1 byte instead of 3)
|
||||
struct LockTraitFlags {
|
||||
uint8_t supports_open : 1;
|
||||
uint8_t requires_code : 1;
|
||||
uint8_t assumed_state : 1;
|
||||
uint8_t reserved : 5;
|
||||
} flags_{};
|
||||
bool supports_open_{false};
|
||||
bool requires_code_{false};
|
||||
bool assumed_state_{false};
|
||||
uint8_t supported_states_mask_{(1 << LOCK_STATE_NONE) | (1 << LOCK_STATE_LOCKED) | (1 << LOCK_STATE_UNLOCKED)};
|
||||
};
|
||||
|
||||
@@ -180,8 +176,8 @@ class Lock : public EntityBase {
|
||||
*/
|
||||
virtual void control(const LockCall &call) = 0;
|
||||
|
||||
Deduplicator<LockState> publish_dedup_; // packs with state and traits ahead of the pointer members
|
||||
LazyCallbackManager<void(LockState)> state_callback_{};
|
||||
Deduplicator<LockState> publish_dedup_;
|
||||
ESPPreferenceObject rtc_;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "download_buffer.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::online_image {
|
||||
@@ -34,6 +35,46 @@ size_t DownloadBuffer::read(size_t len) {
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
void DownloadSource::compact() {
|
||||
if (this->keep_read_ || this->pos_ == this->start_)
|
||||
return;
|
||||
this->buffer_.read(this->pos_ - this->start_);
|
||||
this->start_ = this->pos_;
|
||||
}
|
||||
|
||||
int32_t DownloadSource::read(uint8_t *buf, uint32_t len) {
|
||||
const size_t offset = this->pos_ - this->start_;
|
||||
const size_t n = std::min<size_t>(len, this->buffer_.unread() - offset);
|
||||
memcpy(buf, this->buffer_.data(offset), n);
|
||||
this->pos_ += n;
|
||||
return static_cast<int32_t>(n);
|
||||
}
|
||||
|
||||
bool DownloadSource::seek(uint32_t position) {
|
||||
// Only what is still in the buffer can be read again; a decoder never skips ahead of the download
|
||||
if (position < this->start_ || position > this->start_ + this->buffer_.unread())
|
||||
return false;
|
||||
this->pos_ = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DownloadBuffer::grow(size_t size) {
|
||||
if (this->size_ >= size)
|
||||
return true;
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buffer = allocator.allocate(size);
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
|
||||
allocator.get_max_free_block_size());
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, this->buffer_, this->unread_);
|
||||
allocator.deallocate(this->buffer_, this->size_);
|
||||
this->buffer_ = buffer;
|
||||
this->size_ = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::resize(size_t size) {
|
||||
if (this->size_ >= size) {
|
||||
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -35,6 +36,8 @@ class DownloadBuffer {
|
||||
|
||||
void reset() { this->unread_ = 0; }
|
||||
size_t resize(size_t size);
|
||||
/// Make the buffer at least size bytes, keeping the unread data. Returns false if the allocation failed.
|
||||
bool grow(size_t size);
|
||||
|
||||
protected:
|
||||
uint8_t *buffer_;
|
||||
@@ -43,4 +46,35 @@ class DownloadBuffer {
|
||||
size_t unread_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lets a decoder read a download as it arrives, from the download buffer.
|
||||
*
|
||||
* The decoder reads at its own position in the buffer. Bytes it has read stay there until compact() drops
|
||||
* them, and compact() keeps everything while set_keep_read(true) is in effect, so the decoder can seek back
|
||||
* while it reads the headers.
|
||||
*/
|
||||
class DownloadSource : public runtime_image::DecodeSource {
|
||||
public:
|
||||
explicit DownloadSource(DownloadBuffer &buffer) : buffer_(buffer) {}
|
||||
|
||||
/// Start again at the beginning of a new download, keeping what is read
|
||||
void reset() {
|
||||
this->start_ = 0;
|
||||
this->pos_ = 0;
|
||||
this->keep_read_ = true;
|
||||
}
|
||||
void set_keep_read(bool keep) { this->keep_read_ = keep; }
|
||||
/// Drop the bytes the decoder has read, to make room for more of the download
|
||||
void compact();
|
||||
|
||||
int32_t read(uint8_t *buf, uint32_t len) override;
|
||||
bool seek(uint32_t position) override;
|
||||
|
||||
protected:
|
||||
DownloadBuffer &buffer_;
|
||||
size_t start_{0}; // Position in the download of the first byte in the buffer
|
||||
size_t pos_{0}; // Position in the download the decoder reads next
|
||||
bool keep_read_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::online_image
|
||||
|
||||
@@ -22,6 +22,8 @@ DEPENDENCIES = ["http_request"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
||||
|
||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||
CONF_ON_DOWNLOAD_STARTED = "on_download_started"
|
||||
CONF_ON_DOWNLOAD_PROGRESS = "on_download_progress"
|
||||
CONF_UPDATE = "update"
|
||||
|
||||
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||
@@ -43,7 +45,9 @@ ONLINE_IMAGE_SCHEMA = (
|
||||
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
|
||||
cv.Schema({cv.string: cv.templatable(cv.string)})
|
||||
),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_STARTED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_PROGRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
@@ -96,9 +100,17 @@ automation.register_apply_action(
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_STARTED, "add_on_download_started_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_PROGRESS,
|
||||
"add_on_progress_callback",
|
||||
[(float, "progress"), (cg.size_t, "bytes_read")],
|
||||
),
|
||||
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "online_image");
|
||||
@@ -15,6 +16,11 @@ static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
|
||||
|
||||
namespace esphome::online_image {
|
||||
|
||||
// Time one loop pass may spend reading, and decoding a JPEG as it downloads
|
||||
static constexpr uint32_t PASS_BUDGET_MS = 20;
|
||||
// Shortest time between two on_download_progress calls
|
||||
static constexpr uint32_t PROGRESS_INTERVAL_MS = 100;
|
||||
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
|
||||
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
|
||||
uint32_t buffer_size, bool is_big_endian)
|
||||
@@ -140,8 +146,12 @@ void OnlineImage::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
// JPEG requires the complete image in the download buffer before decoding
|
||||
if (format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
|
||||
// A JPEG of known size is decoded as it arrives, so the download buffer only has to hold its headers.
|
||||
// Otherwise the decoder needs the complete image in the buffer.
|
||||
this->streaming_ = total_size > 0 && total_size <= INT32_MAX && this->decoder_supports_source();
|
||||
this->stream_started_ = false;
|
||||
this->source_.reset();
|
||||
if (!this->streaming_ && format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
|
||||
this->download_buffer_.resize(total_size) == 0) {
|
||||
// resize() logged the failed allocation; the next update() allocates the configured buffer again
|
||||
this->end_connection_();
|
||||
@@ -152,6 +162,8 @@ void OnlineImage::update() {
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||
this->start_time_ = millis();
|
||||
this->last_data_time_ = this->start_time_;
|
||||
this->download_started_callback_.call();
|
||||
this->report_progress_(true);
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
@@ -169,6 +181,11 @@ void OnlineImage::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->streaming_) {
|
||||
this->loop_stream_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if download is complete — use decoder's format-specific completion check
|
||||
// to handle both known content-length and chunked transfer encoding
|
||||
if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
|
||||
@@ -179,15 +196,22 @@ void OnlineImage::loop() {
|
||||
}
|
||||
|
||||
// Download and decode more data
|
||||
size_t available = this->download_buffer_.free_capacity();
|
||||
if (available > 0) {
|
||||
// Download in chunks to avoid blocking
|
||||
available = std::min(available, this->download_buffer_initial_size_);
|
||||
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||
|
||||
if (len > 0) {
|
||||
this->last_data_time_ = App.get_loop_component_start_time();
|
||||
if (this->download_buffer_.free_capacity() > 0) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
size_t received = 0;
|
||||
int len;
|
||||
do {
|
||||
len = this->downloader_->read(this->download_buffer_.append(),
|
||||
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
|
||||
if (len <= 0)
|
||||
break;
|
||||
this->download_buffer_.write(len);
|
||||
received += len;
|
||||
} while (this->download_buffer_.free_capacity() != 0 && millis() - now < PASS_BUDGET_MS);
|
||||
|
||||
if (received > 0) {
|
||||
this->last_data_time_ = now;
|
||||
this->report_progress_(false);
|
||||
|
||||
// Feed data to decoder
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
@@ -207,30 +231,47 @@ void OnlineImage::loop() {
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else if (this->downloader_->is_read_complete()) {
|
||||
// The response has ended, so data left in the buffer can never be completed
|
||||
size_t unread = this->download_buffer_.unread();
|
||||
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
|
||||
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
|
||||
if (this->is_decode_finished() || !this->decoder_knows_end()) {
|
||||
this->finish_download_();
|
||||
} else {
|
||||
// Nothing new arrived. The decoder works for a limited time per call, so it may still have data to use.
|
||||
if (this->download_buffer_.unread() != 0) {
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
if (consumed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", esphome::runtime_image::decode_error_to_string(consumed));
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
if (consumed > 0) {
|
||||
this->download_buffer_.read(consumed);
|
||||
this->last_data_time_ = App.get_loop_component_start_time();
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
} else if (unread == 0) {
|
||||
ESP_LOGE(TAG, "Server sent an empty response");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
|
||||
}
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
// ESP-IDF keeps returning 0 after the server closes the connection early
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
if (this->downloader_->is_read_complete()) {
|
||||
// The response has ended, so data left in the buffer can never be completed
|
||||
size_t unread = this->download_buffer_.unread();
|
||||
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
|
||||
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
|
||||
if (this->is_decode_finished() || !this->decoder_knows_end()) {
|
||||
this->finish_download_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
} else if (unread == 0) {
|
||||
ESP_LOGE(TAG, "Server sent an empty response");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
|
||||
}
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
// ESP-IDF keeps returning 0 after the server closes the connection early
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Buffer is full, need to decode some data first
|
||||
@@ -255,6 +296,114 @@ void OnlineImage::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::loop_stream_() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const size_t total = this->downloader_->content_length;
|
||||
if (this->stream_started_)
|
||||
this->source_.compact();
|
||||
|
||||
if (this->downloader_->get_bytes_read() < total) {
|
||||
if (this->download_buffer_.free_capacity() == 0) {
|
||||
// compact() already ran, so a full buffer means the decoder read nothing in the last pass
|
||||
if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
ESP_LOGE(TAG, "Image does not fit in the %zu byte download buffer; increase buffer_size",
|
||||
this->download_buffer_.size());
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
int len;
|
||||
do {
|
||||
len = this->downloader_->read(this->download_buffer_.append(),
|
||||
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
|
||||
if (len <= 0)
|
||||
break;
|
||||
this->last_data_time_ = now;
|
||||
this->download_buffer_.write(len);
|
||||
this->report_progress_(false);
|
||||
} while (this->downloader_->get_bytes_read() < total && this->download_buffer_.free_capacity() != 0 &&
|
||||
millis() - now < PASS_BUDGET_MS);
|
||||
if (len < 0) {
|
||||
ESP_LOGE(TAG, "Error downloading image: %d", len);
|
||||
this->fail_download_();
|
||||
return;
|
||||
} else if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool arrived = this->downloader_->get_bytes_read() >= total;
|
||||
|
||||
if (!this->stream_started_) {
|
||||
// The decoder reads the headers when it opens the image, so wait until they are in the buffer
|
||||
if (!arrived && this->download_buffer_.free_capacity() != 0)
|
||||
return;
|
||||
const int err = this->begin_source(&this->source_);
|
||||
if (err != 0) {
|
||||
// Headers cut short by a full buffer fail to parse, or make the decoder seek past the buffer. Such an image
|
||||
// still decodes once it is all in memory, as it did before JPEG images were decoded as they download.
|
||||
if (!arrived && (err == runtime_image::DECODE_ERROR_INVALID_TYPE || err == runtime_image::DECODE_ERROR_READ)) {
|
||||
ESP_LOGW(TAG,
|
||||
"The image headers do not fit the %zu byte download buffer, so the whole image is downloaded "
|
||||
"before it is decoded; increase buffer_size to decode it as it downloads",
|
||||
this->download_buffer_.size());
|
||||
// grow() logs a failed allocation
|
||||
if (this->download_buffer_.grow(total)) {
|
||||
this->streaming_ = false;
|
||||
} else {
|
||||
this->fail_download_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(err));
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
this->source_.set_keep_read(false);
|
||||
this->stream_started_ = true;
|
||||
// Opening the image and allocating its buffer is enough for one pass
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
const int result = this->decode_step();
|
||||
if (result == runtime_image::DECODE_STEP_MORE)
|
||||
continue;
|
||||
if (result == runtime_image::DECODE_STEP_DONE) {
|
||||
this->finish_download_();
|
||||
} else if (result == runtime_image::DECODE_STEP_NEED_DATA) {
|
||||
if (!arrived)
|
||||
return;
|
||||
// JPEGDEC only waits for bytes short of the size it was given, so this guards against a hang
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
this->fail_download_();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(result));
|
||||
this->fail_download_();
|
||||
}
|
||||
return;
|
||||
} while (millis() - now < PASS_BUDGET_MS);
|
||||
}
|
||||
|
||||
void OnlineImage::report_progress_(bool force) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const size_t bytes = this->downloader_->get_bytes_read();
|
||||
const size_t total = this->downloader_->content_length;
|
||||
// The last bytes are always reported, so a progress bar reaches 100%
|
||||
if (!force && now - this->last_progress_time_ < PROGRESS_INTERVAL_MS && (total == 0 || bytes < total))
|
||||
return;
|
||||
this->last_progress_time_ = now;
|
||||
const float progress = total > 0 ? std::min(100.0f, bytes * 100.0f / total) : NAN;
|
||||
this->download_progress_callback_.call(progress, bytes);
|
||||
}
|
||||
|
||||
void OnlineImage::fail_download_() {
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
}
|
||||
|
||||
void OnlineImage::finish_download_() {
|
||||
// Must be read before end_decode() ends the decoding session
|
||||
bool decoder_finished = this->is_decode_finished();
|
||||
@@ -288,6 +437,8 @@ void OnlineImage::end_connection_() {
|
||||
this->downloader_ = nullptr;
|
||||
}
|
||||
this->download_buffer_.reset();
|
||||
this->streaming_ = false;
|
||||
this->stream_started_ = false;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ using t_http_codes = enum {
|
||||
HTTP_CODE_NOT_FOUND = 404,
|
||||
};
|
||||
|
||||
/// Largest download read(). A read() blocks until it has every byte it asked for, so this bounds how long one loop
|
||||
/// pass waits on a slow link.
|
||||
static constexpr size_t READ_CHUNK = 1460;
|
||||
|
||||
/**
|
||||
* @brief Download an image from a given URL, and decode it using the specified decoder.
|
||||
* The image will then be stored in a buffer, so that it can be re-displayed without the
|
||||
@@ -70,9 +74,17 @@ class OnlineImage final : public PollingComponent,
|
||||
*/
|
||||
void release();
|
||||
|
||||
/// Called when the server has accepted the request and the image starts to download
|
||||
template<typename F> void add_on_download_started_callback(F &&callback) {
|
||||
this->download_started_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_finished_callback(F &&callback) {
|
||||
this->download_finished_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
/// Called with the percentage downloaded (NAN when the server did not send the size) and the bytes read so far
|
||||
template<typename F> void add_on_progress_callback(F &&callback) {
|
||||
this->download_progress_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_error_callback(F &&callback) {
|
||||
this->download_error_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
@@ -81,12 +93,20 @@ class OnlineImage final : public PollingComponent,
|
||||
bool validate_url_(const std::string &url);
|
||||
void end_connection_();
|
||||
void finish_download_();
|
||||
/// loop() for a JPEG that is decoded as it downloads
|
||||
void loop_stream_();
|
||||
void fail_download_();
|
||||
/// Reports download progress, at most every PROGRESS_INTERVAL_MS unless force is set
|
||||
void report_progress_(bool force);
|
||||
|
||||
CallbackManager<void(bool)> download_finished_callback_{};
|
||||
CallbackManager<void()> download_error_callback_{};
|
||||
LazyCallbackManager<void()> download_started_callback_{};
|
||||
LazyCallbackManager<void(float, size_t)> download_progress_callback_{};
|
||||
|
||||
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
||||
DownloadBuffer download_buffer_;
|
||||
DownloadSource source_{download_buffer_};
|
||||
/**
|
||||
* This is the *initial* size of the download buffer, not the current size.
|
||||
* The download buffer can be resized at runtime; the download_buffer_initial_size_
|
||||
@@ -109,6 +129,9 @@ class OnlineImage final : public PollingComponent,
|
||||
|
||||
uint32_t start_time_{0};
|
||||
uint32_t last_data_time_{0};
|
||||
uint32_t last_progress_time_{0};
|
||||
bool streaming_{false}; // The decoder reads the download itself, as it arrives
|
||||
bool stream_started_{false}; // The decoder has read the headers
|
||||
};
|
||||
|
||||
} // namespace esphome::online_image
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "runtime_image.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
@@ -11,19 +10,48 @@ ESPHOME_LOG_TAG(TAG, "image_decoder");
|
||||
bool ImageDecoder::set_size(int width, int height) {
|
||||
bool success = this->image_->resize(width, height) > 0;
|
||||
this->size_valid_ = success;
|
||||
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
|
||||
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
|
||||
this->draw_width_ = width;
|
||||
this->draw_height_ = height;
|
||||
return success;
|
||||
}
|
||||
|
||||
int ImageDecoder::scaled_x_(int x) const {
|
||||
if (this->draw_width_ <= 0)
|
||||
return 0;
|
||||
const uint32_t buffer_width = this->image_->get_buffer_width();
|
||||
return static_cast<int>((static_cast<uint32_t>(x) * buffer_width + this->draw_width_ - 1) / this->draw_width_);
|
||||
}
|
||||
|
||||
int ImageDecoder::scaled_y_(int y) const {
|
||||
if (this->draw_height_ <= 0)
|
||||
return 0;
|
||||
const uint32_t buffer_height = this->image_->get_buffer_height();
|
||||
return static_cast<int>((static_cast<uint32_t>(y) * buffer_height + this->draw_height_ - 1) / this->draw_height_);
|
||||
}
|
||||
|
||||
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
if (!this->size_valid_) {
|
||||
return;
|
||||
}
|
||||
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||
for (int i = x * this->x_scale_; i < width; i++) {
|
||||
for (int j = y * this->y_scale_; j < height; j++) {
|
||||
// A source pixel paints the target pixels from ceil(start * scale) up to ceil(end * scale). The spans of neighbouring
|
||||
// pixels meet without overlapping, so when an image is shrunk each target pixel is painted once, by the first
|
||||
// source pixel that reaches it, rather than by every pixel that lands on it. Integer maths: the S3 has no double FPU.
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
int x1 = x;
|
||||
int y1 = y;
|
||||
int x2 = x + w;
|
||||
int y2 = y + h;
|
||||
if (buffer_width != this->draw_width_ || buffer_height != this->draw_height_) {
|
||||
x1 = this->scaled_x_(x);
|
||||
y1 = this->scaled_y_(y);
|
||||
x2 = this->scaled_x_(x + w);
|
||||
y2 = this->scaled_y_(y + h);
|
||||
}
|
||||
x2 = std::min(buffer_width, x2);
|
||||
y2 = std::min(buffer_height, y2);
|
||||
for (int i = x1; i < x2; i++) {
|
||||
for (int j = y1; j < y2; j++) {
|
||||
this->image_->draw_pixel(i, j, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,8 @@ class ImageDecoder {
|
||||
this->expected_size_ = 0;
|
||||
this->decoded_bytes_ = 0;
|
||||
this->size_valid_ = true;
|
||||
this->x_scale_ = 1.0;
|
||||
this->y_scale_ = 1.0;
|
||||
this->draw_width_ = 0;
|
||||
this->draw_height_ = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,8 +184,13 @@ class ImageDecoder {
|
||||
uint8_t *image_buffer_() const;
|
||||
bool image_is_big_endian_() const;
|
||||
|
||||
double x_scale_ = 1.0;
|
||||
double y_scale_ = 1.0;
|
||||
/// The first buffer column, or row, that source column x, or row y, paints: ceil(x * buffer size / draw size)
|
||||
int scaled_x_(int x) const;
|
||||
int scaled_y_(int y) const;
|
||||
|
||||
// Size of the image as the decoder draws it, which draw() spreads over the buffer
|
||||
int draw_width_ = 0;
|
||||
int draw_height_ = 0;
|
||||
RuntimeImage *image_;
|
||||
size_t expected_size_ = 0; // Expected data size (0 if unknown)
|
||||
size_t decoded_bytes_ = 0; // Bytes processed so far
|
||||
|
||||
@@ -31,8 +31,8 @@ bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
|
||||
const uint32_t target_width = this->image_->get_buffer_width();
|
||||
const uint32_t target_height = this->image_->get_buffer_height();
|
||||
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
|
||||
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6;
|
||||
// the generic draw() path rounds down and keeps the odd ones instead
|
||||
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6,
|
||||
// as the generic draw() path does
|
||||
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
|
||||
}
|
||||
@@ -138,7 +138,7 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
const int err = this->start_();
|
||||
if (err != 0)
|
||||
return err;
|
||||
if (!this->jpeg_.decode(0, 0, 0))
|
||||
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = size;
|
||||
this->jpeg_.close();
|
||||
@@ -165,10 +165,28 @@ int JpegDecoder::start_() {
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
}
|
||||
if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
const int width = this->jpeg_.getWidth();
|
||||
const int height = this->jpeg_.getHeight();
|
||||
if (!this->set_size(width, height)) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
// JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work. Use the smallest size that still has at least as
|
||||
// many pixels as the buffer, so a large image shown small is not decoded in full only to be thrown away.
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
this->scale_ = 1;
|
||||
while (this->scale_ < JPEG_SCALE_EIGHTH && width / (this->scale_ * 2) >= buffer_width &&
|
||||
height / (this->scale_ * 2) >= buffer_height) {
|
||||
this->scale_ *= 2;
|
||||
}
|
||||
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
|
||||
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
|
||||
this->draw_width_ = decoded_width;
|
||||
this->draw_height_ = decoded_height;
|
||||
if (this->scale_ != 1) {
|
||||
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
|
||||
}
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
return 0;
|
||||
@@ -252,7 +270,7 @@ int JpegDecoder::begin_source(DecodeSource *source) {
|
||||
this->source_ = nullptr;
|
||||
return err;
|
||||
}
|
||||
if (!this->jpeg_.decodeBegin(0, 0, 0))
|
||||
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
|
||||
return this->fail_();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -31,12 +31,16 @@ class JpegDecoder : public ImageDecoder {
|
||||
int HOT decode_step() override;
|
||||
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
/// JPEGDEC decodes the image at 1/get_scale() of its size: 1, 2, 4 or 8
|
||||
int get_scale() const { return this->scale_; }
|
||||
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
|
||||
|
||||
protected:
|
||||
bool prepare_rgb565_scaling_(int width, int height);
|
||||
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
|
||||
int start_();
|
||||
/// The JPEGDEC option for scale_: JPEG_SCALE_HALF, _QUARTER and _EIGHTH are the divisors themselves
|
||||
int scale_option_() const { return this->scale_ == 1 ? 0 : this->scale_; }
|
||||
/// The DecodeError for a failed read of the source or JPEGDEC's last error, closing the image
|
||||
int fail_();
|
||||
|
||||
@@ -49,6 +53,7 @@ class JpegDecoder : public ImageDecoder {
|
||||
FixedVector<uint16_t> y_boundaries_{};
|
||||
int source_width_{0};
|
||||
int source_height_{0};
|
||||
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
|
||||
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
|
||||
bool direct_rgb565_{false};
|
||||
bool source_failed_{false};
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "image_decoder.png");
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
// Compressed bytes handed to pngle at a time, and the longest one decode() call keeps feeding it
|
||||
static constexpr size_t FEED_PIECE_SIZE = 64;
|
||||
static constexpr uint32_t FEED_TIME_LIMIT_MS = 15;
|
||||
|
||||
/**
|
||||
* @brief Callback method that will be called by the PNGLE engine when the basic
|
||||
* data of the image is received (i.e. width and height);
|
||||
@@ -93,16 +99,25 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
ESP_LOGD(TAG, "Waiting for more data");
|
||||
return 0;
|
||||
}
|
||||
auto fed = pngle_feed(this->pngle_, buffer, size);
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
} else if (!this->size_valid_) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
} else {
|
||||
// A few compressed bytes can hold many rows of pixels, so feed small pieces and stop once the call has run for
|
||||
// FEED_TIME_LIMIT_MS; the caller passes the rest again later
|
||||
const uint32_t start = millis();
|
||||
size_t total = 0;
|
||||
while (total < size) {
|
||||
const size_t piece = std::min(FEED_PIECE_SIZE, size - total);
|
||||
const int fed = pngle_feed(this->pngle_, buffer + total, piece);
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
}
|
||||
if (!this->size_valid_)
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
this->decoded_bytes_ += fed;
|
||||
total += fed;
|
||||
if (static_cast<size_t>(fed) < piece || millis() - start >= FEED_TIME_LIMIT_MS)
|
||||
break;
|
||||
}
|
||||
return fed;
|
||||
return total;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -116,7 +116,9 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
}
|
||||
case image::IMAGE_TYPE_GRAYSCALE: {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
// 0.2125, 0.7154 and 0.0721 in 1/256ths, summing to 256 so white stays 255. Integer maths: the S3 has no
|
||||
// double FPU
|
||||
auto gray = static_cast<uint8_t>((54 * color.r + 183 * color.g + 19 * color.b) >> 8);
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
|
||||
@@ -1,226 +1,79 @@
|
||||
#include "rx8111.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::rx8111 {
|
||||
|
||||
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");
|
||||
|
||||
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_ALARM_DISABLED = 0x80;
|
||||
static constexpr uint8_t RX8111_EXTENSION_WADA = 0x08;
|
||||
static constexpr uint8_t RX8111_CONTROL_AIE = 0x08;
|
||||
static constexpr uint8_t RX8111_CONTROL_TIE = 0x10;
|
||||
static constexpr uint8_t RX8111_EXTENSION_TE = 0x10;
|
||||
static constexpr uint8_t RX8111_EXTENSION_TSEL = 0x03;
|
||||
static constexpr uint8_t RX8111_FLAG_TF = 0x10;
|
||||
static constexpr uint8_t RX8111_FLAG_AF = 0x08;
|
||||
static constexpr uint8_t RX8111_FLAG_POR = 0x80;
|
||||
static constexpr uint8_t RX8111_FLAG_VLF = 0x02;
|
||||
static constexpr uint8_t RX8111_FLAG_XST = 0x01;
|
||||
static constexpr uint8_t RX8111_CTRL_STOP = 0x01;
|
||||
// FLAG bits are write-zero-to-clear; keep UF/TF/AF/EVF even if they latch during the write.
|
||||
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 uint8_t RX8111_BOOT_MAX_ATTEMPTS = 3;
|
||||
|
||||
void RX8111Component::setup() {
|
||||
this->configure_boot_alarm_();
|
||||
this->configure_boot_timer_();
|
||||
this->read_time();
|
||||
if (this->alarm_enabled_ || this->watching_()) {
|
||||
this->start_event_polling_();
|
||||
}
|
||||
}
|
||||
void RX8111Component::setup() { this->read_time(); }
|
||||
|
||||
void RX8111Component::configure_boot_alarm_() {
|
||||
bool success = true;
|
||||
if (this->alarm_enabled_) {
|
||||
if (this->alarm_second_ != ALARM_FIELD_UNSET || this->alarm_minute_ != ALARM_FIELD_UNSET ||
|
||||
this->alarm_hour_ != ALARM_FIELD_UNSET || this->alarm_weekday_ != ALARM_FIELD_UNSET ||
|
||||
this->alarm_day_ != ALARM_FIELD_UNSET) {
|
||||
success = this->program_alarm_(this->alarm_second_, this->alarm_minute_, this->alarm_hour_, this->alarm_weekday_,
|
||||
this->alarm_day_, true);
|
||||
if (success) {
|
||||
// Only the first successful boot program can introduce a boot wake-up edge.
|
||||
if (!this->boot_alarm_event_seen_)
|
||||
this->alarm_flag_seen_ = false;
|
||||
this->start_event_polling_();
|
||||
}
|
||||
} else {
|
||||
// Preserve an alarm programmed by an action before a reboot or deep sleep.
|
||||
uint8_t control;
|
||||
success = this->read_byte(RX8111_REG_CONTROL, &control);
|
||||
if (success) {
|
||||
this->alarm_armed_ = (control & RX8111_CONTROL_AIE) != 0;
|
||||
if (this->alarm_armed_)
|
||||
this->start_event_polling_();
|
||||
}
|
||||
}
|
||||
} else if (this->alarm_explicitly_configured_) {
|
||||
success = this->set_alarm_interrupt_(false);
|
||||
}
|
||||
this->boot_config_result_(true, success);
|
||||
}
|
||||
|
||||
void RX8111Component::configure_boot_timer_() {
|
||||
bool success = true;
|
||||
// An omitted timer setting leaves battery-backed state alone; explicit false stops it.
|
||||
if (this->timer_configured_) {
|
||||
const bool start_timer = this->timer_enabled_;
|
||||
if (start_timer) {
|
||||
success = this->configure_timer_();
|
||||
} else {
|
||||
uint8_t extension, control;
|
||||
success = this->disable_timer_(extension, control);
|
||||
}
|
||||
if (success && !start_timer && this->timer_count_ != 0) {
|
||||
this->timer_retained_event_pending_ = true;
|
||||
this->start_event_polling_();
|
||||
}
|
||||
}
|
||||
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_();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// A runtime action supersedes pending boot programming of the same feature
|
||||
void RX8111Component::cancel_boot_retry_(bool alarm) {
|
||||
this->cancel_timeout(alarm ? RX8111_BOOT_ALARM_RETRY_ID : RX8111_BOOT_TIMER_RETRY_ID);
|
||||
}
|
||||
|
||||
void RX8111Component::check_event_flags_() {
|
||||
if (this->watching_() && !this->poll_event_flags_())
|
||||
this->set_warning_(WARN_EVENT_POLL, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
// A callback or a runtime action may have stopped the last watched feature
|
||||
if (!this->watching_() && this->event_polling_started_) {
|
||||
this->cancel_interval(RX8111_EVENT_INTERVAL_ID);
|
||||
this->event_polling_started_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void RX8111Component::start_event_polling_() {
|
||||
if (this->event_polling_started_)
|
||||
return;
|
||||
this->set_interval(RX8111_EVENT_INTERVAL_ID, this->event_poll_interval_, [this]() { this->check_event_flags_(); });
|
||||
this->event_polling_started_ = true;
|
||||
}
|
||||
|
||||
void RX8111Component::update() {
|
||||
this->read_time();
|
||||
this->check_event_flags_();
|
||||
}
|
||||
|
||||
void RX8111Component::set_warning_(uint8_t bit, const LogString *message) {
|
||||
this->warnings_ |= bit;
|
||||
this->status_set_warning(message);
|
||||
}
|
||||
|
||||
void RX8111Component::clear_warning_(uint8_t bit) {
|
||||
if ((this->warnings_ & bit) == 0)
|
||||
return;
|
||||
this->warnings_ &= ~bit;
|
||||
if (this->warnings_ == 0)
|
||||
this->status_clear_warning();
|
||||
}
|
||||
void RX8111Component::update() { this->read_time(); }
|
||||
|
||||
void RX8111Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "RX8111:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Alarm interrupt: %s\n"
|
||||
" Wake-up timer: %s",
|
||||
ONOFF(this->alarm_enabled_), ONOFF(this->timer_enabled_));
|
||||
if (this->timer_enabled_) {
|
||||
ESP_LOGCONFIG(TAG, " Count: %" PRIu32 ", clock: %s", this->timer_count_,
|
||||
LOG_STR_ARG(TimerClockNames::get_log_str(this->timer_clock_, TimerClockNames::LAST_INDEX)));
|
||||
}
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
void RX8111Component::read_time() {
|
||||
ESPTime rtc_time;
|
||||
if (!this->read_calendar_(rtc_time))
|
||||
return;
|
||||
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
|
||||
rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
bool RX8111Component::read_calendar_(ESPTime &rtc_time) {
|
||||
uint8_t flags;
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags)) {
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (flags & (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;
|
||||
this->status_set_warning(LOG_STR("RTC time invalidated; write trusted time"));
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t buf[7];
|
||||
// I2C access freezes the RX8111 counter, yielding a coherent calendar snapshot.
|
||||
if (!this->read_bytes(RX8111_REG_SEC, buf, sizeof(buf))) {
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
// 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};
|
||||
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];
|
||||
valid = valid && (buf[i] & 0x0F) <= 9 && (buf[i] >> 4) <= 9;
|
||||
if ((buf[i] & 0x0F) > 9 || (buf[i] >> 4) > 9) {
|
||||
ESP_LOGW(TAG, "Invalid BCD in RTC time");
|
||||
this->status_set_warning(LOG_STR("Invalid RTC time"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const uint8_t onehot = buf[3];
|
||||
valid = valid && onehot != 0 && onehot <= 0x40 && (onehot & (onehot - 1)) == 0;
|
||||
if (valid) {
|
||||
rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(buf);
|
||||
valid = rtc_time.is_valid(true, false) && rtc_time.second < 60 &&
|
||||
ESPTime::from_epoch_utc(rtc_time.timestamp).day_of_week == rtc_time.day_of_week;
|
||||
if (onehot == 0 || onehot > 0x40 || (onehot & (onehot - 1)) != 0) {
|
||||
ESP_LOGW(TAG, "Invalid RTC weekday");
|
||||
this->status_set_warning(LOG_STR("Invalid RTC time"));
|
||||
return;
|
||||
}
|
||||
if (!valid) {
|
||||
ESPTime rtc_time = time::decode_bcd_calendar<time::EPSON_BCD_CALENDAR>(buf);
|
||||
if (!rtc_time.is_valid(true, false) || rtc_time.second >= 60) {
|
||||
ESP_LOGW(TAG, "Invalid RTC time, not syncing to system clock");
|
||||
this->set_warning_(WARN_CALENDAR, LOG_STR("Invalid RTC time"));
|
||||
return false;
|
||||
this->status_set_warning(LOG_STR("Invalid RTC time"));
|
||||
return;
|
||||
}
|
||||
this->clear_warning_(WARN_CALENDAR);
|
||||
return true;
|
||||
if (!this->status_comm_result(ESPTime::from_epoch_utc(rtc_time.timestamp).day_of_week == rtc_time.day_of_week,
|
||||
LOG_STR("Invalid RTC time"))) {
|
||||
ESP_LOGW(TAG, "RTC weekday does not match calendar date");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
|
||||
rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
void RX8111Component::write_time() {
|
||||
@@ -230,7 +83,7 @@ void RX8111Component::write_time() {
|
||||
return;
|
||||
}
|
||||
if (!this->set_stop_bit_(true)) {
|
||||
this->set_warning_(WARN_WRITE_TIME, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -240,233 +93,25 @@ void RX8111Component::write_time() {
|
||||
// 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->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
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));
|
||||
if (!this->status_comm_result(this->write_byte(RX8111_REG_FLAG, RX8111_KEEP_EVENT_FLAGS)))
|
||||
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::set_stop_bit_(bool stop) {
|
||||
return this->update_register(RX8111_REG_CONTROL, RX8111_CTRL_STOP, stop ? RX8111_CTRL_STOP : 0);
|
||||
}
|
||||
|
||||
bool RX8111Component::set_alarm_interrupt_(bool enabled) {
|
||||
return this->update_register(RX8111_REG_CONTROL, RX8111_CONTROL_AIE, enabled ? RX8111_CONTROL_AIE : 0);
|
||||
}
|
||||
|
||||
bool RX8111Component::program_alarm_(uint8_t second, uint8_t minute, uint8_t hour, uint8_t weekday, uint8_t day,
|
||||
bool boot) {
|
||||
uint8_t extension;
|
||||
if (!this->read_byte(RX8111_REG_EXTENSION, &extension))
|
||||
return false;
|
||||
if (weekday != ALARM_FIELD_UNSET) {
|
||||
extension &= ~RX8111_EXTENSION_WADA;
|
||||
} else if (day != ALARM_FIELD_UNSET) {
|
||||
extension |= RX8111_EXTENSION_WADA;
|
||||
}
|
||||
|
||||
const uint8_t fields[] = {
|
||||
second == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(second),
|
||||
minute == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(minute),
|
||||
hour == ALARM_FIELD_UNSET ? RX8111_ALARM_DISABLED : uint8_to_bcd(hour),
|
||||
weekday != ALARM_FIELD_UNSET ? static_cast<uint8_t>(1U << (weekday - 1))
|
||||
: day != ALARM_FIELD_UNSET ? uint8_to_bcd(day)
|
||||
: RX8111_ALARM_DISABLED,
|
||||
};
|
||||
// Disable the interrupt during partial programming; keep AF (past events) latched.
|
||||
if (!this->set_alarm_interrupt_(false))
|
||||
return false;
|
||||
this->alarm_armed_ = false;
|
||||
uint8_t flags;
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags))
|
||||
return false;
|
||||
if (!boot)
|
||||
this->alarm_flag_seen_ = (flags & RX8111_FLAG_AF) != 0;
|
||||
if (!this->write_byte(RX8111_REG_SEC_ALARM, fields[0]) || !this->write_bytes(RX8111_REG_MIN_ALARM, &fields[1], 3) ||
|
||||
!this->write_byte(RX8111_REG_EXTENSION, extension))
|
||||
return false;
|
||||
if (!this->set_alarm_interrupt_(this->alarm_enabled_))
|
||||
return false;
|
||||
this->alarm_armed_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::set_alarm(uint16_t second, uint16_t minute, uint16_t hour, uint16_t weekday, uint16_t day) {
|
||||
if (!this->alarm_enabled_) {
|
||||
ESP_LOGE(TAG, "Alarm interrupt disabled; set alarm_enabled: true");
|
||||
return false;
|
||||
}
|
||||
constexpr uint16_t unset = ALARM_FIELD_UNSET;
|
||||
if ((second != unset && second > 59) || (minute != unset && minute > 59) || (hour != unset && hour > 23) ||
|
||||
(weekday != unset && (weekday < 1 || weekday > 7)) || (day != unset && (day < 1 || day > 31)) ||
|
||||
(weekday != unset && day != unset) ||
|
||||
(second == unset && minute == unset && hour == unset && weekday == unset && day == unset)) {
|
||||
ESP_LOGE(TAG, "Invalid alarm fields");
|
||||
return false;
|
||||
}
|
||||
this->cancel_boot_retry_(true);
|
||||
if (!this->program_alarm_(second, minute, hour, weekday, day)) {
|
||||
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->clear_warning_(WARN_ALARM);
|
||||
this->boot_alarm_event_seen_ = true;
|
||||
this->start_event_polling_();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::schedule_alarm_in(uint32_t seconds) {
|
||||
if (!this->alarm_enabled_ || seconds == 0 || seconds >= 28U * 86400U) {
|
||||
ESP_LOGE(TAG, "Relative alarm requires alarm_enabled and a duration of 1 second to less than 28 days");
|
||||
return false;
|
||||
}
|
||||
ESPTime now;
|
||||
if (!this->read_calendar_(now)) {
|
||||
ESP_LOGE(TAG, "Cannot read valid RTC time for relative alarm");
|
||||
return false;
|
||||
}
|
||||
auto target = ESPTime::from_epoch_utc(now.timestamp + seconds);
|
||||
if (!target.is_valid() || target.year > 2099) {
|
||||
ESP_LOGE(TAG, "Relative alarm target is outside RTC year range");
|
||||
return false;
|
||||
}
|
||||
return this->set_alarm(target.second, target.minute, target.hour, ALARM_FIELD_UNSET, target.day_of_month);
|
||||
}
|
||||
|
||||
bool RX8111Component::unset_alarm() {
|
||||
this->cancel_boot_retry_(true);
|
||||
if (!this->set_alarm_interrupt_(false)) {
|
||||
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->alarm_armed_ = false;
|
||||
// Minute, hour and day alarm registers are contiguous
|
||||
static constexpr uint8_t DISABLED_FIELDS[3] = {RX8111_ALARM_DISABLED, RX8111_ALARM_DISABLED, RX8111_ALARM_DISABLED};
|
||||
if (!this->write_byte(RX8111_REG_SEC_ALARM, RX8111_ALARM_DISABLED) ||
|
||||
!this->write_bytes(RX8111_REG_MIN_ALARM, DISABLED_FIELDS, sizeof(DISABLED_FIELDS))) {
|
||||
this->set_warning_(WARN_ALARM, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->clear_warning_(WARN_ALARM);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::clear_alarm_flag() {
|
||||
if (!this->write_byte(RX8111_REG_FLAG, static_cast<uint8_t>(~RX8111_FLAG_AF))) {
|
||||
this->set_warning_(WARN_ALARM_FLAG_CLEAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->alarm_flag_seen_ = false;
|
||||
this->clear_warning_(WARN_ALARM_FLAG_CLEAR);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clears TE and TIE; returns the register values read so callers can build on them
|
||||
bool RX8111Component::disable_timer_(uint8_t &extension, uint8_t &control) {
|
||||
return this->read_byte(RX8111_REG_EXTENSION, &extension) &&
|
||||
this->write_byte(RX8111_REG_EXTENSION, extension & ~RX8111_EXTENSION_TE) &&
|
||||
this->read_byte(RX8111_REG_CONTROL, &control) &&
|
||||
this->write_byte(RX8111_REG_CONTROL, control & ~RX8111_CONTROL_TIE);
|
||||
}
|
||||
|
||||
bool RX8111Component::configure_timer_(bool clear_stale_flag) {
|
||||
// Stop counting before updating the 24-bit reload value; preserve the boot flag unless explicitly starting.
|
||||
uint8_t extension;
|
||||
uint8_t control;
|
||||
if (!this->disable_timer_(extension, control))
|
||||
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)) ||
|
||||
!this->write_byte(RX8111_REG_EXTENSION,
|
||||
(extension & ~(RX8111_EXTENSION_TE | RX8111_EXTENSION_TSEL)) | this->timer_clock_) ||
|
||||
(clear_stale_flag && !this->clear_timer_flag()) ||
|
||||
!this->write_byte(RX8111_REG_CONTROL, control | RX8111_CONTROL_TIE))
|
||||
return false;
|
||||
return this->write_byte(RX8111_REG_EXTENSION,
|
||||
(extension & ~RX8111_EXTENSION_TSEL) | this->timer_clock_ | RX8111_EXTENSION_TE);
|
||||
}
|
||||
|
||||
bool RX8111Component::start_timer() {
|
||||
if (this->timer_count_ == 0 || this->timer_count_ > 0xFFFFFF) {
|
||||
ESP_LOGW(TAG, "Cannot start timer without a valid timer_count");
|
||||
if (!this->read_byte(RX8111_REG_CONTROL, &control))
|
||||
return false;
|
||||
if (stop) {
|
||||
control |= RX8111_CTRL_STOP;
|
||||
} else {
|
||||
control &= ~RX8111_CTRL_STOP;
|
||||
}
|
||||
this->cancel_boot_retry_(false);
|
||||
if (!this->configure_timer_(true)) {
|
||||
// A partial configuration can leave either enable bit set. Disable both if the bus permits it;
|
||||
// otherwise keep watching a timer that may still be running.
|
||||
uint8_t extension, control;
|
||||
if (this->disable_timer_(extension, control))
|
||||
this->timer_enabled_ = false;
|
||||
this->set_warning_(WARN_TIMER, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->clear_warning_(WARN_TIMER);
|
||||
this->timer_enabled_ = true;
|
||||
this->timer_retained_event_pending_ = false;
|
||||
this->start_event_polling_();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::clear_timer_flag() {
|
||||
if (!this->write_byte(RX8111_REG_FLAG, static_cast<uint8_t>(~RX8111_FLAG_TF))) {
|
||||
this->set_warning_(WARN_TIMER_FLAG_CLEAR, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->timer_flag_seen_ = false;
|
||||
this->timer_retained_event_pending_ = false;
|
||||
this->clear_warning_(WARN_TIMER_FLAG_CLEAR);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::stop_timer() {
|
||||
this->cancel_boot_retry_(false);
|
||||
uint8_t extension, control;
|
||||
if (!this->disable_timer_(extension, control)) {
|
||||
this->set_warning_(WARN_TIMER, LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return false;
|
||||
}
|
||||
this->timer_enabled_ = false;
|
||||
this->clear_warning_(WARN_TIMER);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RX8111Component::poll_event_flags_() {
|
||||
uint8_t flags;
|
||||
if (!this->read_byte(RX8111_REG_FLAG, &flags))
|
||||
return false;
|
||||
this->clear_warning_(WARN_EVENT_POLL);
|
||||
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
|
||||
const bool timer_watched = this->timer_enabled_ || this->timer_retained_event_pending_;
|
||||
if (alarm_active && this->alarm_armed_)
|
||||
this->boot_alarm_event_seen_ = true;
|
||||
const bool alarm_rising = alarm_active && !this->alarm_flag_seen_ && this->alarm_armed_;
|
||||
const bool timer_rising = timer_active && !this->timer_flag_seen_ && timer_watched;
|
||||
const bool deliver_stopped_event = timer_rising && this->timer_retained_event_pending_;
|
||||
// Record eligible edges before callbacks, which may clear flags synchronously.
|
||||
// Leave flags unseen while boot configuration is still waiting to enable delivery.
|
||||
if (this->alarm_armed_)
|
||||
this->alarm_flag_seen_ = alarm_active;
|
||||
if (timer_watched)
|
||||
this->timer_flag_seen_ = timer_active;
|
||||
this->timer_retained_event_pending_ = false;
|
||||
if (alarm_rising)
|
||||
this->alarm_callbacks_.call();
|
||||
// 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();
|
||||
return true;
|
||||
return this->write_byte(RX8111_REG_CONTROL, control);
|
||||
}
|
||||
|
||||
} // namespace esphome::rx8111
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome::rx8111 {
|
||||
|
||||
namespace testing {
|
||||
class RX8111Test;
|
||||
} // namespace testing
|
||||
|
||||
class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -17,91 +12,9 @@ class RX8111Component final : public time::RealTimeClock, public i2c::I2CDevice
|
||||
void dump_config() override;
|
||||
void read_time();
|
||||
void write_time();
|
||||
void set_alarm_enabled(bool enabled) { this->alarm_enabled_ = enabled; }
|
||||
void set_alarm_explicitly_configured(bool configured) { this->alarm_explicitly_configured_ = configured; }
|
||||
void set_alarm_second(uint8_t value) { this->alarm_second_ = value; }
|
||||
void set_alarm_minute(uint8_t value) { this->alarm_minute_ = value; }
|
||||
void set_alarm_hour(uint8_t value) { this->alarm_hour_ = value; }
|
||||
void set_alarm_weekday(uint8_t value) { this->alarm_weekday_ = value; }
|
||||
void set_alarm_day(uint8_t value) { this->alarm_day_ = value; }
|
||||
void set_timer_count(uint32_t count) { this->timer_count_ = count; }
|
||||
void set_timer_clock(uint8_t clock) { this->timer_clock_ = clock; }
|
||||
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; }
|
||||
template<typename F> void add_on_alarm_callback(F &&callback) {
|
||||
this->alarm_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
bool set_alarm(uint16_t second, uint16_t minute, uint16_t hour, uint16_t weekday, uint16_t day);
|
||||
bool schedule_alarm_in(uint32_t seconds);
|
||||
bool unset_alarm();
|
||||
bool clear_alarm_flag();
|
||||
bool clear_timer_flag();
|
||||
bool start_timer();
|
||||
bool stop_timer();
|
||||
template<typename F> void add_on_timer_callback(F &&callback) {
|
||||
this->timer_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
friend class testing::RX8111Test;
|
||||
|
||||
static constexpr uint32_t BOOT_RETRY_DELAY_MS = 1000;
|
||||
// set_alarm() and the boot alarm ignore a field set to this value
|
||||
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 {
|
||||
WARN_CALENDAR = 1 << 0,
|
||||
WARN_EVENT_POLL = 1 << 1,
|
||||
WARN_ALARM = 1 << 2,
|
||||
WARN_TIMER = 1 << 3,
|
||||
WARN_WRITE_TIME = 1 << 4,
|
||||
WARN_ALARM_FLAG_CLEAR = 1 << 5,
|
||||
WARN_TIMER_FLAG_CLEAR = 1 << 6,
|
||||
};
|
||||
|
||||
bool set_stop_bit_(bool stop);
|
||||
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 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);
|
||||
bool disable_timer_(uint8_t &extension, uint8_t &control);
|
||||
bool configure_timer_(bool clear_stale_flag = false);
|
||||
void start_event_polling_();
|
||||
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_;
|
||||
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};
|
||||
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};
|
||||
};
|
||||
|
||||
} // namespace esphome::rx8111
|
||||
|
||||
@@ -4,14 +4,6 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DAY,
|
||||
CONF_DURATION,
|
||||
CONF_HOUR,
|
||||
CONF_ID,
|
||||
CONF_MINUTE,
|
||||
CONF_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@techdregs"]
|
||||
@@ -19,186 +11,23 @@ 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}
|
||||
# set_alarm() ignores a field set to this value
|
||||
ALARM_FIELD_UNSET = 0xFF
|
||||
|
||||
CONF_ALARM_SECOND = "alarm_second"
|
||||
CONF_ALARM_MINUTE = "alarm_minute"
|
||||
CONF_ALARM_HOUR = "alarm_hour"
|
||||
CONF_ALARM_WEEKDAY = "alarm_weekday"
|
||||
CONF_ALARM_DAY = "alarm_day"
|
||||
CONF_ALARM_ENABLED = "alarm_enabled"
|
||||
CONF_ON_ALARM = "on_alarm"
|
||||
CONF_TIMER_COUNT = "timer_count"
|
||||
CONF_TIMER_CLOCK = "timer_clock"
|
||||
CONF_TIMER_ENABLED = "timer_enabled"
|
||||
CONF_ON_TIMER = "on_timer"
|
||||
CONF_WEEKDAY = "weekday"
|
||||
CONF_EVENT_POLL_INTERVAL = "event_poll_interval"
|
||||
|
||||
_ALARM_FIELDS = (
|
||||
CONF_ALARM_SECOND,
|
||||
CONF_ALARM_MINUTE,
|
||||
CONF_ALARM_HOUR,
|
||||
CONF_ALARM_WEEKDAY,
|
||||
CONF_ALARM_DAY,
|
||||
)
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_ALARM, "add_on_alarm_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_TIMER, "add_on_timer_callback"),
|
||||
)
|
||||
|
||||
|
||||
def _validate_alarm_config(config: ConfigType) -> ConfigType:
|
||||
"""Rules that depend on the derived alarm_enabled and timer_enabled defaults."""
|
||||
if CONF_ON_ALARM in config and not config.get(
|
||||
CONF_ALARM_ENABLED, any(f in config for f in _ALARM_FIELDS)
|
||||
):
|
||||
raise cv.Invalid(
|
||||
"on_alarm requires alarm_enabled: true or alarm matching fields"
|
||||
)
|
||||
if CONF_TIMER_COUNT not in config and (
|
||||
config.get(CONF_TIMER_ENABLED) or CONF_ON_TIMER in config
|
||||
):
|
||||
raise cv.Invalid("timer_enabled or on_timer requires timer_count")
|
||||
return config
|
||||
|
||||
|
||||
_validate_alarm_options = cv.All(
|
||||
cv.has_at_most_one_key(CONF_ALARM_DAY, CONF_ALARM_WEEKDAY), _validate_alarm_config
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(RX8111Component),
|
||||
cv.Optional(CONF_ALARM_SECOND): cv.int_range(min=0, max=59),
|
||||
cv.Optional(CONF_ALARM_MINUTE): cv.int_range(min=0, max=59),
|
||||
cv.Optional(CONF_ALARM_HOUR): cv.int_range(min=0, max=23),
|
||||
cv.Optional(CONF_ALARM_WEEKDAY): cv.int_range(min=1, max=7),
|
||||
cv.Optional(CONF_ALARM_DAY): cv.int_range(min=1, max=31),
|
||||
cv.Optional(CONF_ALARM_ENABLED): cv.boolean,
|
||||
cv.Optional(CONF_ON_ALARM): automation.validate_automation({}),
|
||||
cv.Optional(CONF_TIMER_COUNT): cv.int_range(min=1, max=0xFFFFFF),
|
||||
cv.Optional(CONF_TIMER_CLOCK, default="1hz"): cv.enum(
|
||||
TIMER_CLOCKS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_TIMER_ENABLED): cv.boolean,
|
||||
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(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(milliseconds=500)),
|
||||
),
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x32)),
|
||||
_validate_alarm_options,
|
||||
)
|
||||
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
{cv.GenerateID(): cv.declare_id(RX8111Component)}
|
||||
).extend(i2c.i2c_device_schema(0x32))
|
||||
|
||||
RX8111_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(RX8111Component)}
|
||||
)
|
||||
|
||||
# Actions that call one argument-less method on the component
|
||||
for _method in (
|
||||
"write_time",
|
||||
"read_time",
|
||||
"clear_alarm_flag",
|
||||
"clear_timer_flag",
|
||||
"unset_alarm",
|
||||
"stop_timer",
|
||||
"start_timer",
|
||||
):
|
||||
automation.register_apply_action(
|
||||
f"rx8111.{_method}", RX8111_ACTION_SCHEMA, automation.ApplyCall(f"{_method}()")
|
||||
)
|
||||
|
||||
_SET_ALARM_FIELDS = (CONF_SECOND, CONF_MINUTE, CONF_HOUR, CONF_WEEKDAY, CONF_DAY)
|
||||
|
||||
|
||||
def _fill_unset_alarm_fields(config: ConfigType) -> ConfigType:
|
||||
"""Marks the fields not given as unset, so set_alarm() always gets all five."""
|
||||
for field in _SET_ALARM_FIELDS:
|
||||
config.setdefault(field, ALARM_FIELD_UNSET)
|
||||
return config
|
||||
|
||||
|
||||
_validate_set_alarm = cv.All(
|
||||
cv.has_at_least_one_key(*_SET_ALARM_FIELDS),
|
||||
cv.has_at_most_one_key(CONF_WEEKDAY, CONF_DAY),
|
||||
_fill_unset_alarm_fields,
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"rx8111.set_alarm",
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(RX8111Component),
|
||||
cv.Optional(CONF_SECOND): cv.templatable(cv.int_range(min=0, max=59)),
|
||||
cv.Optional(CONF_MINUTE): cv.templatable(cv.int_range(min=0, max=59)),
|
||||
cv.Optional(CONF_HOUR): cv.templatable(cv.int_range(min=0, max=23)),
|
||||
cv.Optional(CONF_WEEKDAY): cv.templatable(cv.int_range(min=1, max=7)),
|
||||
cv.Optional(CONF_DAY): cv.templatable(cv.int_range(min=1, max=31)),
|
||||
}
|
||||
),
|
||||
_validate_set_alarm,
|
||||
),
|
||||
automation.ApplyCall(
|
||||
"set_alarm({}, {}, {}, {}, {})",
|
||||
(
|
||||
(CONF_SECOND, cg.uint16),
|
||||
(CONF_MINUTE, cg.uint16),
|
||||
(CONF_HOUR, cg.uint16),
|
||||
(CONF_WEEKDAY, cg.uint16),
|
||||
(CONF_DAY, cg.uint16),
|
||||
),
|
||||
),
|
||||
"rx8111.write_time", RX8111_ACTION_SCHEMA, automation.ApplyCall("write_time()")
|
||||
)
|
||||
|
||||
|
||||
# The alarm registers hold a day of month, so the target must stay inside the current month cycle
|
||||
_ALARM_DURATION = cv.All(
|
||||
cv.positive_time_period_seconds,
|
||||
cv.Range(
|
||||
min=cv.TimePeriod(seconds=1), max=cv.TimePeriod(days=28), max_included=False
|
||||
),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"rx8111.schedule_alarm_in",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(RX8111Component),
|
||||
cv.Required(CONF_DURATION): cv.templatable(_ALARM_DURATION),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_DURATION, "schedule_alarm_in", cg.uint32),
|
||||
"rx8111.read_time", RX8111_ACTION_SCHEMA, automation.ApplyCall("read_time()")
|
||||
)
|
||||
|
||||
|
||||
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]))
|
||||
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))
|
||||
cg.add_optional(var.set_alarm_weekday, config.get(CONF_ALARM_WEEKDAY))
|
||||
cg.add_optional(var.set_alarm_day, config.get(CONF_ALARM_DAY))
|
||||
cg.add(
|
||||
var.set_alarm_enabled(
|
||||
config.get(CONF_ALARM_ENABLED, any(f in config for f in _ALARM_FIELDS))
|
||||
)
|
||||
)
|
||||
cg.add(var.set_alarm_explicitly_configured(CONF_ALARM_ENABLED in config))
|
||||
if (count := config.get(CONF_TIMER_COUNT)) is not None:
|
||||
cg.add(var.set_timer_count(count))
|
||||
cg.add(var.set_timer_clock(config[CONF_TIMER_CLOCK]))
|
||||
# Validation guarantees on_timer comes with a timer_count
|
||||
cg.add(var.set_timer_enabled(config.get(CONF_TIMER_ENABLED, count is not None)))
|
||||
cg.add(var.set_timer_configured(CONF_TIMER_ENABLED in config or count is not None))
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
@@ -17,8 +17,8 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
idf_version,
|
||||
only_on_variant,
|
||||
require_idf_version,
|
||||
)
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
@@ -143,12 +143,19 @@ TYPE_CLASS = {
|
||||
}
|
||||
|
||||
|
||||
def _require_psram_dma_idf(value: Any) -> Any:
|
||||
if idf_version() < cv.Version(5, 5, 3):
|
||||
raise cv.Invalid("psram_dma requires ESP-IDF 5.5.3 or newer")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_psram_dma(value: Any) -> bool:
|
||||
value = cv.boolean(value)
|
||||
if not value:
|
||||
return value
|
||||
return cv.All(
|
||||
require_idf_version(cv.Version(5, 5, 3), "psram_dma"),
|
||||
cv.only_on_esp32,
|
||||
_require_psram_dma_idf,
|
||||
only_on_variant(
|
||||
supported=[
|
||||
VARIANT_ESP32C5,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from esphome import automation, preferences
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server, zigbee
|
||||
@@ -10,16 +8,13 @@ from esphome.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_INITIAL_STATE,
|
||||
CONF_INVERTED,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_STATE,
|
||||
CONF_ON_TURN_OFF,
|
||||
CONF_ON_TURN_ON,
|
||||
CONF_RESTORE_MODE,
|
||||
CONF_RESTORE_STATE,
|
||||
CONF_STATE,
|
||||
CONF_STORAGE,
|
||||
CONF_WEB_SERVER,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_OUTLET,
|
||||
@@ -27,14 +22,10 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
RESTORE_STATE_INITIAL,
|
||||
RESTORE_STATE_INVERT,
|
||||
RESTORE_STATE_KEEP,
|
||||
ApplyEntity,
|
||||
SubEntities,
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
restore_state_schema,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -56,8 +47,6 @@ SwitchPtr = Switch.operator("ptr")
|
||||
ApplySwitch = switch_ns.class_("ApplySwitch", Switch)
|
||||
|
||||
SwitchRestoreMode = switch_ns.enum("SwitchRestoreMode")
|
||||
RESTORE_MODE_STORAGE_FLASH_MASK = switch_ns.RESTORE_MODE_STORAGE_FLASH_MASK
|
||||
RESTORE_MODE_STORAGE_RTC_MASK = switch_ns.RESTORE_MODE_STORAGE_RTC_MASK
|
||||
|
||||
RESTORE_MODES = {
|
||||
"RESTORE_DEFAULT_OFF": SwitchRestoreMode.SWITCH_RESTORE_DEFAULT_OFF,
|
||||
@@ -71,115 +60,6 @@ RESTORE_MODES = {
|
||||
|
||||
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True)
|
||||
validate_restore_mode = cv.enum(RESTORE_MODES, upper=True, space="_")
|
||||
|
||||
|
||||
def _validate_restore_state_storage(config: ConfigType) -> ConfigType:
|
||||
if CONF_STORAGE in config and config[CONF_STATE] == RESTORE_STATE_INITIAL:
|
||||
raise cv.Invalid(
|
||||
"'storage' only applies when a saved state is restored (KEEP or INVERT)",
|
||||
path=[CONF_STORAGE],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _validate_restore_state_state(value: object) -> str:
|
||||
# light accepts ON/OFF here (restore, then force a state); a switch has no such mode, and
|
||||
# YAML turns ON into True, so say what to use instead of asking for quotes
|
||||
if isinstance(value, bool) or (
|
||||
isinstance(value, str) and value.upper() in ("ON", "OFF")
|
||||
):
|
||||
raise cv.Invalid(
|
||||
"a switch can't restore and then force ON or OFF; to always start in a fixed state, "
|
||||
"use 'initial_state: ON' (or OFF) with 'restore_state: none'"
|
||||
)
|
||||
return cv.one_of(
|
||||
RESTORE_STATE_KEEP, RESTORE_STATE_INVERT, RESTORE_STATE_INITIAL, upper=True
|
||||
)(value)
|
||||
|
||||
|
||||
_RESTORE_STATE_FIELDS_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_STATE, default=RESTORE_STATE_KEEP
|
||||
): _validate_restore_state_state,
|
||||
# No default: when unset, the existing restore_mode storage applies (on ESP8266,
|
||||
# restore_from_flash decides), as with wifi
|
||||
cv.Optional(CONF_STORAGE): preferences.validate_storage,
|
||||
}
|
||||
)
|
||||
_RESTORE_STATE_FIELDS_SCHEMA.add_extra(_validate_restore_state_storage)
|
||||
|
||||
|
||||
# A mapping of `state` and `storage`, or the shorthands `all` (KEEP) and `none` (INITIAL)
|
||||
RESTORE_STATE_SCHEMA = restore_state_schema(
|
||||
_RESTORE_STATE_FIELDS_SCHEMA, {CONF_STATE: RESTORE_STATE_INITIAL}
|
||||
)
|
||||
|
||||
|
||||
def _validate_initial_state(config: ConfigType) -> ConfigType:
|
||||
if CONF_INITIAL_STATE in config and CONF_RESTORE_MODE in config:
|
||||
raise cv.Invalid(
|
||||
"'initial_state' can't be combined with 'restore_mode', which already sets "
|
||||
"the boot state; use 'restore_state' instead",
|
||||
path=[CONF_INITIAL_STATE],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
# (restore_state state, initial_state) -> restore mode
|
||||
_RESTORE_MODE_FOR_STATE = {
|
||||
(RESTORE_STATE_KEEP, False): "RESTORE_DEFAULT_OFF",
|
||||
(RESTORE_STATE_KEEP, True): "RESTORE_DEFAULT_ON",
|
||||
(RESTORE_STATE_INVERT, False): "RESTORE_INVERTED_DEFAULT_OFF",
|
||||
(RESTORE_STATE_INVERT, True): "RESTORE_INVERTED_DEFAULT_ON",
|
||||
(RESTORE_STATE_INITIAL, False): "ALWAYS_OFF",
|
||||
(RESTORE_STATE_INITIAL, True): "ALWAYS_ON",
|
||||
}
|
||||
_STATE_FOR_RESTORE_MODE = {
|
||||
mode: state for (state, _), mode in _RESTORE_MODE_FOR_STATE.items()
|
||||
}
|
||||
|
||||
|
||||
def _apply_default_restore_mode(
|
||||
default_restore_mode: str,
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
# cv.Exclusive has no default, so apply it here when no restore option is configured.
|
||||
# With only initial_state set, a restoring default keeps restoring and initial_state
|
||||
# supplies the state used when nothing is saved.
|
||||
default = validate_restore_mode(default_restore_mode)
|
||||
state = _STATE_FOR_RESTORE_MODE.get(str(default), RESTORE_STATE_INITIAL)
|
||||
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
if CONF_RESTORE_MODE in config or CONF_RESTORE_STATE in config:
|
||||
return config
|
||||
if CONF_INITIAL_STATE in config:
|
||||
config[CONF_RESTORE_STATE] = {CONF_STATE: state}
|
||||
else:
|
||||
config[CONF_RESTORE_MODE] = default
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def _resolve_restore_mode(config: ConfigType) -> MockObj | str:
|
||||
"""Translate `initial_state` / `restore_state` into one of the RESTORE_MODES, with the storage bit."""
|
||||
if (restore_mode := config.get(CONF_RESTORE_MODE)) is not None:
|
||||
return restore_mode
|
||||
restore_state = config.get(CONF_RESTORE_STATE, {})
|
||||
state = restore_state.get(CONF_STATE, RESTORE_STATE_INITIAL)
|
||||
mode = RESTORE_MODES[
|
||||
_RESTORE_MODE_FOR_STATE[state, config.get(CONF_INITIAL_STATE, False)]
|
||||
]
|
||||
if (storage := restore_state.get(CONF_STORAGE)) is None:
|
||||
return mode
|
||||
cg.add_define("USE_SWITCH_STORAGE")
|
||||
mask = (
|
||||
RESTORE_MODE_STORAGE_FLASH_MASK
|
||||
if preferences.is_in_flash(storage)
|
||||
else RESTORE_MODE_STORAGE_RTC_MASK
|
||||
)
|
||||
return cg.RawExpression(f"static_cast<{SwitchRestoreMode}>({mode} | {mask})")
|
||||
|
||||
|
||||
_SWITCH_SCHEMA = (
|
||||
@@ -190,9 +70,9 @@ _SWITCH_SCHEMA = (
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSwitchComponent),
|
||||
cv.Optional(CONF_INVERTED): cv.boolean,
|
||||
cv.Exclusive(CONF_RESTORE_MODE, "restore"): validate_restore_mode,
|
||||
cv.Exclusive(CONF_RESTORE_STATE, "restore"): RESTORE_STATE_SCHEMA,
|
||||
cv.Optional(CONF_INITIAL_STATE): cv.boolean,
|
||||
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
|
||||
RESTORE_MODES, upper=True, space="_"
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation({}),
|
||||
@@ -206,7 +86,6 @@ _SWITCH_SCHEMA = (
|
||||
|
||||
_SWITCH_SCHEMA.add_extra(entity_duplicate_validator("switch"))
|
||||
_SWITCH_SCHEMA.add_extra(zigbee.validate_switch)
|
||||
_SWITCH_SCHEMA.add_extra(_validate_initial_state)
|
||||
|
||||
|
||||
def switch_schema(
|
||||
@@ -224,6 +103,13 @@ def switch_schema(
|
||||
(CONF_DEVICE_CLASS, device_class, validate_device_class),
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
(
|
||||
CONF_RESTORE_MODE,
|
||||
default_restore_mode,
|
||||
cv.enum(RESTORE_MODES, upper=True, space="_")
|
||||
if default_restore_mode is not cv.UNDEFINED
|
||||
else cv.UNDEFINED,
|
||||
),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
@@ -233,15 +119,7 @@ def switch_schema(
|
||||
"Inverted is not supported for this platform!"
|
||||
)
|
||||
|
||||
result = _SWITCH_SCHEMA.extend(schema)
|
||||
result.add_extra(
|
||||
_apply_default_restore_mode(
|
||||
"ALWAYS_OFF"
|
||||
if default_restore_mode is cv.UNDEFINED
|
||||
else default_restore_mode
|
||||
)
|
||||
)
|
||||
return result
|
||||
return _SWITCH_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
@@ -281,7 +159,7 @@ async def setup_switch_core_(var, config):
|
||||
|
||||
setup_device_class(config)
|
||||
|
||||
cg.add(var.set_restore_mode(_resolve_restore_mode(config)))
|
||||
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
|
||||
await zigbee.setup_switch(var, config)
|
||||
|
||||
|
||||
|
||||
@@ -33,14 +33,7 @@ optional<bool> Switch::get_initial_state() {
|
||||
if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK))
|
||||
return {};
|
||||
|
||||
#ifdef USE_SWITCH_STORAGE
|
||||
if (restore_mode & RESTORE_MODE_STORAGE_MASK) {
|
||||
this->rtc_ = this->make_entity_preference<bool>(0, restore_mode & RESTORE_MODE_STORAGE_FLASH_MASK);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->rtc_ = this->make_entity_preference<bool>();
|
||||
}
|
||||
this->rtc_ = this->make_entity_preference<bool>();
|
||||
bool initial_state;
|
||||
if (!this->rtc_.load(&initial_state))
|
||||
return {};
|
||||
@@ -60,13 +53,6 @@ optional<bool> Switch::get_initial_state_with_restore_mode() {
|
||||
}
|
||||
return initial_state;
|
||||
}
|
||||
void Switch::restore() {
|
||||
if (auto initial = this->get_initial_state_with_restore_mode())
|
||||
this->control(*initial);
|
||||
}
|
||||
void Switch::restore(bool when_disabled) {
|
||||
this->control(this->get_initial_state_with_restore_mode().value_or(when_disabled));
|
||||
}
|
||||
void Switch::publish_state(bool state) {
|
||||
if (!this->publish_dedup_.next(state))
|
||||
return;
|
||||
|
||||
@@ -14,16 +14,11 @@ namespace esphome::switch_ {
|
||||
public: \
|
||||
void set_##name##_switch(switch_::Switch *s) { this->name##_switch_ = s; }
|
||||
|
||||
// bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled.
|
||||
// bit4: restore from flash. bit5: restore from RTC memory (neither: platform default).
|
||||
// Code generation ors the storage bit into the restore mode passed to set_restore_mode().
|
||||
// bit0: on/off. bit1: persistent. bit2: inverted. bit3: disabled
|
||||
constexpr int RESTORE_MODE_ON_MASK = 0x01;
|
||||
constexpr int RESTORE_MODE_PERSISTENT_MASK = 0x02;
|
||||
constexpr int RESTORE_MODE_INVERTED_MASK = 0x04;
|
||||
constexpr int RESTORE_MODE_DISABLED_MASK = 0x08;
|
||||
constexpr int RESTORE_MODE_STORAGE_FLASH_MASK = 0x10;
|
||||
constexpr int RESTORE_MODE_STORAGE_RTC_MASK = 0x20;
|
||||
constexpr int RESTORE_MODE_STORAGE_MASK = RESTORE_MODE_STORAGE_FLASH_MASK | RESTORE_MODE_STORAGE_RTC_MASK;
|
||||
|
||||
enum SwitchRestoreMode : uint8_t {
|
||||
SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK,
|
||||
@@ -115,11 +110,6 @@ class Switch : public EntityBase {
|
||||
*/
|
||||
optional<bool> get_initial_state_with_restore_mode();
|
||||
|
||||
/// Drive the switch to the state its restore mode yields, through control(), so inversion and
|
||||
/// write_state() apply; a disabled restore mode drives \p when_disabled, or nothing without one.
|
||||
void restore();
|
||||
void restore(bool when_disabled);
|
||||
|
||||
/** Return whether this switch uses an assumed state - i.e. if both the ON/OFF actions should be displayed in Home
|
||||
* Assistant because the real state is unknown.
|
||||
*
|
||||
@@ -149,9 +139,9 @@ class Switch : public EntityBase {
|
||||
LazyCallbackManager<void(bool)> state_callback_{};
|
||||
|
||||
// Small types grouped together
|
||||
Deduplicator<bool> publish_dedup_; // 3 bytes (bool has_value_ + bool value_unknown_ + bool last_value_)
|
||||
Deduplicator<bool> publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_)
|
||||
bool inverted_{false}; // 1 byte
|
||||
// Total: 4 bytes, no padding
|
||||
// Total: 3 bytes, 1 byte padding
|
||||
};
|
||||
|
||||
#define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj))
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
@@ -11,7 +9,6 @@ from esphome.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
@@ -20,15 +17,12 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_HOUR,
|
||||
UNIT_KILOVOLT_AMPS,
|
||||
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
|
||||
UNIT_KILOWATT,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MILLIAMP,
|
||||
UNIT_MILLIVOLT,
|
||||
UNIT_MINUTE,
|
||||
UNIT_SECOND,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
@@ -39,8 +33,6 @@ from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, teleinfo_ns
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
|
||||
|
||||
|
||||
@@ -67,9 +59,6 @@ _APPARENT_POWER_KVA = _preset(
|
||||
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
|
||||
_DURATION_MIN = _preset(UNIT_MINUTE, DEVICE_CLASS_DURATION, STATE_CLASS_MEASUREMENT)
|
||||
# Numeric values without a physical unit (indexes, bit fields, versions)
|
||||
_NO_UNIT: dict[str, str] = {}
|
||||
|
||||
# Legacy defaults, kept for tags without a known preset
|
||||
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
|
||||
@@ -86,7 +75,6 @@ _COMPATIBLE_UNITS = {
|
||||
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
|
||||
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
|
||||
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
|
||||
DEVICE_CLASS_DURATION: {UNIT_SECOND, UNIT_MINUTE, UNIT_HOUR},
|
||||
}
|
||||
|
||||
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
|
||||
@@ -102,10 +90,6 @@ TIC_TAG_CONFIGS = {
|
||||
"CC": _POWER,
|
||||
"PREF": _APPARENT_POWER_KVA,
|
||||
"PCOUP": _APPARENT_POWER_KVA,
|
||||
"VTIC": _NO_UNIT,
|
||||
"NTARF": _NO_UNIT,
|
||||
"NJOURF": _NO_UNIT,
|
||||
"RELAIS": _NO_UNIT,
|
||||
# Historical mode
|
||||
"BASE": _ENERGY,
|
||||
"HCH": _ENERGY,
|
||||
@@ -117,42 +101,15 @@ TIC_TAG_CONFIGS = {
|
||||
"ADPS": _CURRENT,
|
||||
"ADIR": _CURRENT,
|
||||
"PAPP": _APPARENT_POWER_VA,
|
||||
"PMAX": _APPARENT_POWER_VA,
|
||||
"PEJP": _DURATION_MIN,
|
||||
"PMAX": _POWER,
|
||||
}
|
||||
|
||||
# Prefixes of text tags, which a sensor cannot publish; a text_sensor should be used
|
||||
# instead. They get no preset, and no prefix here is a prefix of a preset one either.
|
||||
TIC_TEXT_TAGS = (
|
||||
# Standard mode
|
||||
"ADSC",
|
||||
"DATE",
|
||||
"NGTF",
|
||||
"LTARF",
|
||||
"STGE",
|
||||
"DPM",
|
||||
"FPM",
|
||||
"MSG",
|
||||
"PRM",
|
||||
"PJOURF",
|
||||
"PPOINTE",
|
||||
# Historical mode
|
||||
"ADCO",
|
||||
"OPTARIF",
|
||||
"PTEC",
|
||||
"DEMAIN",
|
||||
"HHPHC",
|
||||
"MOTDETAT",
|
||||
"PPOT",
|
||||
)
|
||||
|
||||
|
||||
def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
"""Apply preset configurations based on the tag name.
|
||||
|
||||
Only keys not set by the user are filled in. For known tags, a preset unit or
|
||||
device class is dropped if it is incompatible with the one set by the user.
|
||||
Known text tags get no preset and log a warning, as a sensor cannot publish them.
|
||||
Unknown tags get the legacy defaults, merged key by key as before.
|
||||
"""
|
||||
if CONF_TAG_NAME not in config:
|
||||
@@ -160,12 +117,6 @@ def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
|
||||
# Runs before cv.string, so the raw value may not be a string yet
|
||||
tag_name = str(config[CONF_TAG_NAME])
|
||||
if tag_name.startswith(TIC_TEXT_TAGS):
|
||||
_LOGGER.warning(
|
||||
"TeleInfo tag '%s' holds a text value, use a text_sensor instead", tag_name
|
||||
)
|
||||
return config
|
||||
|
||||
preset = next(
|
||||
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
|
||||
None,
|
||||
@@ -177,8 +128,8 @@ def apply_tag_config(config: ConfigType) -> ConfigType:
|
||||
config.setdefault(key, value)
|
||||
return config
|
||||
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset.get(CONF_UNIT_OF_MEASUREMENT))
|
||||
device_class = config.get(CONF_DEVICE_CLASS, preset.get(CONF_DEVICE_CLASS))
|
||||
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
|
||||
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
|
||||
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
|
||||
|
||||
for key, value in preset.items():
|
||||
|
||||
@@ -2,57 +2,95 @@
|
||||
|
||||
namespace esphome::unicode {
|
||||
|
||||
// Reads count continuation bytes into code_point, stopping at the first byte that is not one so a
|
||||
// sequence cut short by the terminator is never read past
|
||||
static bool read_continuation(const uint8_t *¤t, uint8_t count, uint32_t &code_point) {
|
||||
for (uint8_t i = 0; i < count; i++) {
|
||||
const uint8_t c = *current;
|
||||
if ((c & 0xC0) != 0x80) {
|
||||
return false;
|
||||
}
|
||||
current++;
|
||||
code_point = (code_point << 6) | (c & 0x3F);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
|
||||
const auto *start = reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
const uint8_t *current = start;
|
||||
const uint8_t c1 = *current++;
|
||||
uint8_t count;
|
||||
uint32_t code_point;
|
||||
uint32_t lowest; // the smallest code point this sequence length may encode; shorter forms are overlong
|
||||
// Safely cast to uint8_t* for correct bitwise operations on bytes
|
||||
const uint8_t *current = reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
uint32_t code_point = 0;
|
||||
uint8_t c1 = *current++;
|
||||
|
||||
// check for end of string
|
||||
if (c1 == 0) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// --- 1-Byte Sequence: 0xxxxxxx (ASCII) ---
|
||||
if (c1 < 0x80) {
|
||||
count = 0;
|
||||
// Valid ASCII byte.
|
||||
code_point = c1;
|
||||
lowest = 0;
|
||||
} else if ((c1 & 0xE0) == 0xC0) {
|
||||
count = 1;
|
||||
code_point = c1 & 0x1F;
|
||||
lowest = 0x80;
|
||||
} else if ((c1 & 0xF0) == 0xE0) {
|
||||
count = 2;
|
||||
code_point = c1 & 0x0F;
|
||||
lowest = 0x800;
|
||||
} else if ((c1 & 0xF8) == 0xF0) {
|
||||
count = 3;
|
||||
code_point = c1 & 0x07;
|
||||
lowest = 0x10000;
|
||||
} else {
|
||||
*length = 0; // a continuation byte or an invalid lead byte
|
||||
return 0;
|
||||
// Optimization: No need to check for continuation bytes.
|
||||
}
|
||||
if (!read_continuation(current, count, code_point) || code_point < lowest || code_point > 0x10FFFF ||
|
||||
(code_point >= 0xD800 && code_point <= 0xDFFF)) {
|
||||
// --- 2-Byte Sequence: 110xxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xE0) == 0xC0) {
|
||||
uint8_t c2 = *current++;
|
||||
|
||||
// Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx)
|
||||
if ((c2 & 0xC0) != 0x80) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x1F) << 6;
|
||||
code_point |= (c2 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (2-byte must be > 0x7F)
|
||||
if (code_point <= 0x7F) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF0) == 0xE0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x0F) << 12;
|
||||
code_point |= (c2 & 0x3F) << 6;
|
||||
code_point |= (c3 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (3-byte must be > 0x7FF)
|
||||
// Also check for surrogates (0xD800-0xDFFF)
|
||||
if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ---
|
||||
else if ((c1 & 0xF8) == 0xF0) {
|
||||
uint8_t c2 = *current++;
|
||||
uint8_t c3 = *current++;
|
||||
uint8_t c4 = *current++;
|
||||
|
||||
// Error Check 1: Check continuation bytes
|
||||
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
code_point = (c1 & 0x07) << 18;
|
||||
code_point |= (c2 & 0x3F) << 12;
|
||||
code_point |= (c3 & 0x3F) << 6;
|
||||
code_point |= (c4 & 0x3F);
|
||||
|
||||
// Error Check 2: Overlong check (4-byte must be > 0xFFFF)
|
||||
// Also check for valid Unicode range (must be <= 0x10FFFF)
|
||||
if (code_point <= 0xFFFF || code_point > 0x10FFFF) {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) ---
|
||||
else {
|
||||
*length = 0;
|
||||
return 0;
|
||||
}
|
||||
*length = current - start;
|
||||
*length = current - reinterpret_cast<const uint8_t *>(utf8_str);
|
||||
return code_point;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
idf_version,
|
||||
only_on_variant,
|
||||
require_idf_version,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DEVICES, CONF_FRAMEWORK, CONF_ID
|
||||
@@ -202,9 +201,10 @@ def fifo_split() -> tuple[int, int, int] | None:
|
||||
def _reconcile_fifo() -> None:
|
||||
if (split := fifo_split()) is None:
|
||||
return
|
||||
require_idf_version(
|
||||
FIFO_CUSTOM_MIN_IDF, f"Sizing the USB FIFO on the {get_esp32_variant()}"
|
||||
)(None)
|
||||
if idf_version() < FIFO_CUSTOM_MIN_IDF:
|
||||
raise cv.Invalid(
|
||||
f"Sizing the USB FIFO on the {get_esp32_variant()} needs ESP-IDF {FIFO_CUSTOM_MIN_IDF} or later"
|
||||
)
|
||||
rx, nptx, ptx = split
|
||||
cg.add_define("USE_USB_HOST_FIFO_CONFIG")
|
||||
cg.add_define("USB_HOST_FIFO_RX_LINES", rx)
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::valve {
|
||||
|
||||
class ValveTraits {
|
||||
public:
|
||||
ValveTraits() = default;
|
||||
|
||||
bool get_is_assumed_state() const { return this->flags_.is_assumed_state; }
|
||||
void set_is_assumed_state(bool is_assumed_state) { this->flags_.is_assumed_state = is_assumed_state; }
|
||||
bool get_supports_position() const { return this->flags_.supports_position; }
|
||||
void set_supports_position(bool supports_position) { this->flags_.supports_position = supports_position; }
|
||||
bool get_supports_toggle() const { return this->flags_.supports_toggle; }
|
||||
void set_supports_toggle(bool supports_toggle) { this->flags_.supports_toggle = supports_toggle; }
|
||||
bool get_supports_stop() const { return this->flags_.supports_stop; }
|
||||
void set_supports_stop(bool supports_stop) { this->flags_.supports_stop = supports_stop; }
|
||||
bool get_is_assumed_state() const { return this->is_assumed_state_; }
|
||||
void set_is_assumed_state(bool is_assumed_state) { this->is_assumed_state_ = is_assumed_state; }
|
||||
bool get_supports_position() const { return this->supports_position_; }
|
||||
void set_supports_position(bool supports_position) { this->supports_position_ = supports_position; }
|
||||
bool get_supports_toggle() const { return this->supports_toggle_; }
|
||||
void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; }
|
||||
bool get_supports_stop() const { return this->supports_stop_; }
|
||||
void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; }
|
||||
|
||||
protected:
|
||||
// Bit-packed flags to save memory (1 byte instead of 4)
|
||||
struct ValveTraitFlags {
|
||||
uint8_t is_assumed_state : 1;
|
||||
uint8_t supports_position : 1;
|
||||
uint8_t supports_toggle : 1;
|
||||
uint8_t supports_stop : 1;
|
||||
uint8_t reserved : 4;
|
||||
} flags_{};
|
||||
bool is_assumed_state_{false};
|
||||
bool supports_position_{false};
|
||||
bool supports_toggle_{false};
|
||||
bool supports_stop_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::valve
|
||||
|
||||
@@ -29,15 +29,9 @@ MULTI_CONF = True
|
||||
|
||||
CONF_MAX_UPDATE_DELAY = "max_update_delay"
|
||||
CONF_UPDATE_DELAY = "update_delay"
|
||||
CONF_PIXEL_FORMAT = "pixel_format"
|
||||
|
||||
vnc_ns = cg.esphome_ns.namespace("vnc")
|
||||
VncClient = vnc_ns.class_("VncClient", cg.Component)
|
||||
VncPixelFormat = vnc_ns.enum("VncPixelFormat", is_class=True)
|
||||
|
||||
PIXEL_FORMATS = VncPixelFormat.options(
|
||||
"auto", "rgb565", "rgb332", prefix="VNC_PIXEL_FORMAT_"
|
||||
)
|
||||
|
||||
# Drivers that cannot take another component's pixels yet.
|
||||
UNSUPPORTED_DISPLAYS = dict.fromkeys(
|
||||
@@ -69,9 +63,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_MAX_UPDATE_DELAY, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_PIXEL_FORMAT, default="auto"): cv.enum(
|
||||
PIXEL_FORMATS, lower=True
|
||||
),
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
|
||||
}
|
||||
@@ -137,7 +128,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
|
||||
cg.add(var.set_update_delay(config[CONF_UPDATE_DELAY]))
|
||||
cg.add(var.set_max_update_delay(config[CONF_MAX_UPDATE_DELAY]))
|
||||
cg.add(var.set_pixel_format(config[CONF_PIXEL_FORMAT]))
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@@ -145,8 +135,4 @@ VNC_ID_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(VncClient)},
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"vnc.refresh", VNC_ID_SCHEMA, automation.ApplyCall("refresh()")
|
||||
)
|
||||
|
||||
automation.register_apply_condition("vnc.is_connected", VNC_ID_SCHEMA, "is_connected()")
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
@@ -34,19 +32,7 @@ static constexpr uint8_t SECURITY_NONE = 1;
|
||||
static constexpr uint8_t SECURITY_VNC_AUTH = 2;
|
||||
|
||||
static constexpr int32_t ENCODING_RAW = 0;
|
||||
static constexpr int32_t ENCODING_HEXTILE = 5;
|
||||
static constexpr int32_t ENCODING_RICH_CURSOR = -239;
|
||||
// In preference order: TightVNC uses the first real encoding it supports for every rectangle.
|
||||
static constexpr int32_t ENCODINGS[] = {ENCODING_HEXTILE, ENCODING_RAW, ENCODING_RICH_CURSOR};
|
||||
|
||||
// Indexed by VncPixelFormat
|
||||
PROGMEM_STRING_TABLE(PixelFormatStrings, "auto", "RGB565", "RGB332");
|
||||
|
||||
static constexpr uint8_t HEXTILE_RAW = 0x01;
|
||||
static constexpr uint8_t HEXTILE_BACKGROUND = 0x02;
|
||||
static constexpr uint8_t HEXTILE_FOREGROUND = 0x04;
|
||||
static constexpr uint8_t HEXTILE_ANY_SUBRECTS = 0x08;
|
||||
static constexpr uint8_t HEXTILE_SUBRECTS_COLOURED = 0x10;
|
||||
|
||||
static constexpr uint8_t MSG_FRAMEBUFFER_UPDATE = 0;
|
||||
static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
|
||||
@@ -80,32 +66,6 @@ static int parse_three_digits(const uint8_t *p) {
|
||||
void VncClient::setup() {
|
||||
this->setup_ms_ = App.get_loop_component_start_time();
|
||||
this->link_.begin(TAG);
|
||||
// Sized for the display now; a rotation changed later swaps the sides but keeps the cell count.
|
||||
const uint32_t wanted = static_cast<uint32_t>((this->display_->get_width() + 15) / 16) *
|
||||
static_cast<uint32_t>((this->display_->get_height() + 15) / 16);
|
||||
const uint32_t cells = std::min<uint32_t>(wanted, UINT16_MAX);
|
||||
if (cells < wanted) {
|
||||
ESP_LOGW(TAG, "Display has %" PRIu32 " tiles, more than the %u the cache holds", wanted, UINT16_MAX);
|
||||
}
|
||||
RAMAllocator<uint32_t> allocator;
|
||||
this->tile_hashes_ = allocator.allocate(cells);
|
||||
if (this->tile_hashes_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Not enough memory to skip unchanged tiles");
|
||||
return;
|
||||
}
|
||||
this->hash_capacity_ = cells;
|
||||
this->clear_tiles_();
|
||||
}
|
||||
|
||||
void VncClient::refresh() {
|
||||
this->need_full_ = true;
|
||||
this->clear_tiles_();
|
||||
}
|
||||
|
||||
void VncClient::clear_tiles_() {
|
||||
if (this->tile_hashes_ != nullptr) {
|
||||
std::memset(this->tile_hashes_, 0, sizeof(uint32_t) * this->hash_capacity_);
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::dump_config() {
|
||||
@@ -114,13 +74,9 @@ void VncClient::dump_config() {
|
||||
" Server: %s:%u\n"
|
||||
" Reconnect Interval: %" PRIu32 "ms\n"
|
||||
" Update Delay: %" PRIu32 "ms\n"
|
||||
" Max Update Delay: %" PRIu32 "ms\n"
|
||||
" Pixel Format: %s\n"
|
||||
" Tile Cache: %u tiles",
|
||||
" Max Update Delay: %" PRIu32 "ms",
|
||||
this->link_.host(), this->link_.port(), this->link_.reconnect_interval(), this->update_delay_ms_,
|
||||
this->max_update_delay_ms_,
|
||||
LOG_STR_ARG(PixelFormatStrings::get_log_str(static_cast<uint8_t>(this->pixel_format_), 0)),
|
||||
this->hash_capacity_);
|
||||
this->max_update_delay_ms_);
|
||||
}
|
||||
|
||||
void VncClient::on_shutdown() { this->link_.close(); }
|
||||
@@ -139,28 +95,12 @@ void VncClient::loop() {
|
||||
}
|
||||
// A display that parks its loop when idle is busy while that loop runs. Displays that never
|
||||
// park (no loop, or one that always runs) update synchronously, so they are never busy.
|
||||
const bool was_busy = this->display_busy_;
|
||||
if (this->display_->is_idle()) {
|
||||
this->display_parks_ = true;
|
||||
this->display_busy_ = false;
|
||||
} else {
|
||||
this->display_busy_ = this->display_parks_;
|
||||
}
|
||||
if (was_busy && !this->display_busy_) {
|
||||
// A refresh the driver gave up on (it sets its warning) leaves the panel behind the buffer: retry it
|
||||
// once and forget the cached tiles, which no longer say what the panel shows
|
||||
if (!this->display_->status_has_warning()) {
|
||||
this->refresh_retried_ = false;
|
||||
} else if (!this->refresh_retried_) {
|
||||
this->refresh_retried_ = true;
|
||||
this->clear_tiles_();
|
||||
if (!this->refresh_pending_) {
|
||||
this->first_change_ms_ = now;
|
||||
}
|
||||
this->last_change_ms_ = now;
|
||||
this->refresh_pending_ = true;
|
||||
}
|
||||
}
|
||||
// A stuck panel, or another owner of the display's loop, would otherwise freeze the screen silently
|
||||
if (!this->display_busy_) {
|
||||
this->busy_since_ms_ = now;
|
||||
@@ -207,11 +147,6 @@ void VncClient::loop() {
|
||||
this->service_rx_(now);
|
||||
}
|
||||
if (this->state_ == VncState::VNC_STATE_RUNNING) {
|
||||
// A new session lays the screen out again for the new rotation and redraws all of it.
|
||||
if (static_cast<uint8_t>(this->display_->get_rotation() / 90) != this->hash_rotation_) {
|
||||
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display rotation changed"));
|
||||
return;
|
||||
}
|
||||
// Mid-frame there is nothing to send, so a vanished server would never fail a write.
|
||||
if (this->in_update_ && !blocked && !this->rx_pending_ && now - this->last_rx_ms_ >= HEARTBEAT_MS) {
|
||||
this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Server stopped sending"));
|
||||
@@ -248,20 +183,12 @@ void VncClient::connect_step_(uint32_t now) {
|
||||
}
|
||||
|
||||
void VncClient::reset_session_() {
|
||||
// Pixels drawn but never shown would match the cache when the next session resends them.
|
||||
if (this->refresh_pending_ || this->frame_drawn_) {
|
||||
this->clear_tiles_();
|
||||
}
|
||||
// Hextile colours carry from tile to tile; a new server must not inherit the last one's
|
||||
std::memset(this->hex_bg_, 0, sizeof(this->hex_bg_));
|
||||
std::memset(this->hex_fg_, 0, sizeof(this->hex_fg_));
|
||||
this->rx_pos_ = 0;
|
||||
this->rx_len_ = 0;
|
||||
this->rx_pending_ = false;
|
||||
this->in_update_ = false;
|
||||
this->request_outstanding_ = false;
|
||||
this->refresh_pending_ = false;
|
||||
this->need_full_ = false;
|
||||
this->frame_drawn_ = false;
|
||||
this->step_ = VncStep::VNC_STEP_VERSION;
|
||||
this->reason_failure_ = VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
@@ -551,7 +478,6 @@ VncParse VncClient::parse_() {
|
||||
this->in_update_ = true;
|
||||
this->update_start_ms_ = millis();
|
||||
this->update_start_bytes_ = this->rx_total_ - (this->rx_len_ - this->rx_pos_);
|
||||
this->drawn_tiles_ = 0;
|
||||
this->rect_done_();
|
||||
break;
|
||||
case MSG_SET_COLOUR_MAP:
|
||||
@@ -591,7 +517,7 @@ VncParse VncClient::parse_() {
|
||||
const auto encoding = static_cast<int32_t>(read_be32(p + 8));
|
||||
this->rx_pos_ += 12;
|
||||
this->rects_left_--;
|
||||
if (encoding == ENCODING_RAW || encoding == ENCODING_HEXTILE) {
|
||||
if (encoding == ENCODING_RAW) {
|
||||
if (static_cast<uint32_t>(this->rect_x_) + this->rect_w_ > this->fb_w_ ||
|
||||
static_cast<uint32_t>(this->rect_y_) + this->rect_h_ > this->fb_h_) {
|
||||
ESP_LOGW(TAG, "Rectangle %ux%u at %u,%u is outside the %ux%u screen", this->rect_w_, this->rect_h_,
|
||||
@@ -600,15 +526,10 @@ VncParse VncClient::parse_() {
|
||||
}
|
||||
if (this->rect_w_ == 0 || this->rect_h_ == 0) {
|
||||
this->rect_done_();
|
||||
} else if (encoding == ENCODING_RAW) {
|
||||
this->forget_tiles_(this->rect_x_, this->rect_y_, this->rect_w_, this->rect_h_);
|
||||
} else {
|
||||
this->pos_x_ = 0;
|
||||
this->pos_y_ = 0;
|
||||
this->step_ = VncStep::VNC_STEP_RAW;
|
||||
} else {
|
||||
this->pos_x_ = this->rect_x_;
|
||||
this->pos_y_ = this->rect_y_;
|
||||
this->step_ = VncStep::VNC_STEP_TILE;
|
||||
}
|
||||
} else if (encoding == ENCODING_RICH_CURSOR) {
|
||||
if (this->rect_w_ > MAX_CURSOR_SIDE || this->rect_h_ > MAX_CURSOR_SIDE) {
|
||||
@@ -649,84 +570,6 @@ VncParse VncClient::parse_() {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_TILE: {
|
||||
if (this->display_busy_) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
if (avail < 1) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
if (this->budget_exhausted_()) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
const uint8_t flags = p[0];
|
||||
this->tile_w_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_x_) + this->rect_w_ - this->pos_x_);
|
||||
this->tile_h_ = std::min<uint32_t>(16, static_cast<uint32_t>(this->rect_y_) + this->rect_h_ - this->pos_y_);
|
||||
this->tile_hidden_ = this->pos_x_ >= this->vis_w_ || this->pos_y_ >= this->vis_h_;
|
||||
// With the raw bit set the other bits mean nothing and the pixels follow at once.
|
||||
if (flags & HEXTILE_RAW) {
|
||||
this->rx_pos_ += 1;
|
||||
this->tile_fill_ = 0;
|
||||
this->step_ = VncStep::VNC_STEP_TILE_RAW;
|
||||
break;
|
||||
}
|
||||
const size_t need = 1 + ((flags & HEXTILE_BACKGROUND) ? this->bpp_ : 0) +
|
||||
((flags & HEXTILE_FOREGROUND) ? this->bpp_ : 0) + ((flags & HEXTILE_ANY_SUBRECTS) ? 1 : 0);
|
||||
if (avail < need) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const uint8_t *q = p + 1;
|
||||
if (flags & HEXTILE_BACKGROUND) {
|
||||
std::memcpy(this->hex_bg_, q, this->bpp_);
|
||||
q += this->bpp_;
|
||||
}
|
||||
if (flags & HEXTILE_FOREGROUND) {
|
||||
std::memcpy(this->hex_fg_, q, this->bpp_);
|
||||
q += this->bpp_;
|
||||
}
|
||||
this->subrects_left_ = (flags & HEXTILE_ANY_SUBRECTS) ? *q : 0;
|
||||
this->subrects_coloured_ = (flags & HEXTILE_SUBRECTS_COLOURED) != 0;
|
||||
this->rx_pos_ += need;
|
||||
this->fill_tile_(0, 0, this->tile_w_, this->tile_h_, this->hex_bg_);
|
||||
this->step_ = VncStep::VNC_STEP_SUBRECTS;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_TILE_RAW: {
|
||||
const size_t total = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
|
||||
const size_t count = std::min(avail, total - this->tile_fill_);
|
||||
if (count == 0) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
std::memcpy(this->tile_ + this->tile_fill_, p, count);
|
||||
this->rx_pos_ += count;
|
||||
this->tile_fill_ += count;
|
||||
if (this->tile_fill_ == total) {
|
||||
this->draw_tile_();
|
||||
this->next_tile_();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SUBRECTS: {
|
||||
if (this->subrects_left_ == 0) {
|
||||
this->draw_tile_();
|
||||
this->next_tile_();
|
||||
break;
|
||||
}
|
||||
const size_t need = (this->subrects_coloured_ ? this->bpp_ : 0) + 2;
|
||||
if (avail < need) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const uint8_t *pixel = this->hex_fg_;
|
||||
const uint8_t *q = p;
|
||||
if (this->subrects_coloured_) {
|
||||
pixel = q;
|
||||
q += this->bpp_;
|
||||
}
|
||||
this->fill_tile_(q[0] >> 4, q[0] & 0x0F, (q[1] >> 4) + 1, (q[1] & 0x0F) + 1, pixel);
|
||||
this->rx_pos_ += need;
|
||||
this->subrects_left_--;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SKIP: {
|
||||
const auto count = static_cast<uint32_t>(std::min<size_t>(avail, this->skip_left_));
|
||||
this->rx_pos_ += count;
|
||||
@@ -749,20 +592,6 @@ void VncClient::rect_done_() {
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::next_tile_() {
|
||||
this->step_ = VncStep::VNC_STEP_TILE;
|
||||
if (static_cast<uint32_t>(this->pos_x_) + 16 < static_cast<uint32_t>(this->rect_x_) + this->rect_w_) {
|
||||
this->pos_x_ += 16;
|
||||
return;
|
||||
}
|
||||
this->pos_x_ = this->rect_x_;
|
||||
if (static_cast<uint32_t>(this->pos_y_) + 16 < static_cast<uint32_t>(this->rect_y_) + this->rect_h_) {
|
||||
this->pos_y_ += 16;
|
||||
return;
|
||||
}
|
||||
this->rect_done_();
|
||||
}
|
||||
|
||||
void VncClient::frame_done_() {
|
||||
this->in_update_ = false;
|
||||
this->request_outstanding_ = false;
|
||||
@@ -775,9 +604,9 @@ void VncClient::frame_done_() {
|
||||
}
|
||||
this->last_change_ms_ = now;
|
||||
this->refresh_pending_ = true;
|
||||
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms, %u hextile tiles drawn",
|
||||
ESP_LOGD(TAG, "Update: %" PRIu32 " bytes in %" PRIu32 "ms",
|
||||
this->rx_total_ - (this->rx_len_ - this->rx_pos_) - this->update_start_bytes_,
|
||||
millis() - this->update_start_ms_, this->drawn_tiles_);
|
||||
millis() - this->update_start_ms_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,96 +619,8 @@ void VncClient::draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t
|
||||
this->frame_drawn_ = true;
|
||||
}
|
||||
|
||||
void VncClient::draw_tile_() {
|
||||
if (this->tile_hidden_) {
|
||||
return;
|
||||
}
|
||||
const int w = std::min<int>(this->tile_w_, this->vis_w_ - this->pos_x_);
|
||||
const int h = std::min<int>(this->tile_h_, this->vis_h_ - this->pos_y_);
|
||||
uint32_t *cell = nullptr;
|
||||
uint32_t hash = 0;
|
||||
if (this->hash_cols_ != 0) {
|
||||
if ((this->pos_x_ | this->pos_y_) & 15) {
|
||||
// An off-grid tile spans several cells, and a later write to any of them would make its hash stale.
|
||||
this->forget_tiles_(this->pos_x_, this->pos_y_, w, h);
|
||||
} else {
|
||||
cell = this->tile_hashes_ + (this->pos_y_ / 16) * this->hash_cols_ + this->pos_x_ / 16;
|
||||
hash = this->tile_hash_();
|
||||
if (*cell == hash) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->display_->draw_pixels_at(this->pos_x_, this->pos_y_, w, h, this->tile_, this->bitness_, this->big_endian_, 0, 0,
|
||||
this->tile_w_ - w);
|
||||
this->frame_drawn_ = true;
|
||||
this->drawn_tiles_++;
|
||||
if (cell != nullptr) {
|
||||
*cell = hash;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t VncClient::tile_hash_() const {
|
||||
// The tile's size, pixel size and pixels; the cell gives its place. Zero means "unknown".
|
||||
uint32_t hash = fnv1a_hash_extend(FNV1_OFFSET_BASIS,
|
||||
static_cast<uint32_t>(this->tile_w_) << 16 | this->tile_h_ << 8 | this->bpp_);
|
||||
const size_t len = static_cast<size_t>(this->tile_w_) * this->tile_h_ * this->bpp_;
|
||||
size_t i = 0;
|
||||
for (; i + 4 <= len; i += 4) {
|
||||
uint32_t word;
|
||||
std::memcpy(&word, this->tile_ + i, sizeof(word)); // tile_ is aligned, so this is one load
|
||||
// Byte by byte: XOR-ing whole words lets high-bit changes in two words cancel.
|
||||
hash = fnv1a_hash_extend(hash, word);
|
||||
}
|
||||
for (; i < len; i++) {
|
||||
hash = fnv1a_hash_extend(hash, this->tile_[i]);
|
||||
}
|
||||
return hash == 0 ? 1 : hash;
|
||||
}
|
||||
|
||||
void VncClient::forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
|
||||
if (this->hash_cols_ == 0 || w == 0 || h == 0) {
|
||||
return;
|
||||
}
|
||||
const uint16_t col_end = std::min<uint16_t>((x + w - 1) / 16, this->hash_cols_ - 1);
|
||||
const uint16_t row_end = std::min<uint16_t>((y + h - 1) / 16, this->hash_rows_ - 1);
|
||||
for (uint16_t row = y / 16; row <= row_end; row++) {
|
||||
for (uint16_t col = x / 16; col <= col_end; col++) {
|
||||
this->tile_hashes_[row * this->hash_cols_ + col] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel) {
|
||||
if (this->tile_hidden_ || x >= this->tile_w_ || y >= this->tile_h_) {
|
||||
return;
|
||||
}
|
||||
w = std::min<uint8_t>(w, this->tile_w_ - x);
|
||||
h = std::min<uint8_t>(h, this->tile_h_ - y);
|
||||
const size_t stride = static_cast<size_t>(this->tile_w_) * this->bpp_;
|
||||
const size_t row_len = static_cast<size_t>(w) * this->bpp_;
|
||||
uint8_t *first = this->tile_ + y * stride + x * this->bpp_;
|
||||
if (this->bpp_ == 1) {
|
||||
std::memset(first, pixel[0], w);
|
||||
} else {
|
||||
for (size_t i = 0; i < row_len; i += 2) {
|
||||
first[i] = pixel[0];
|
||||
first[i + 1] = pixel[1];
|
||||
}
|
||||
}
|
||||
for (uint8_t row = 1; row < h; row++) {
|
||||
std::memcpy(first + row * stride, first, row_len);
|
||||
}
|
||||
}
|
||||
|
||||
bool VncClient::start_session_(uint32_t now) {
|
||||
VncPixelFormat format = this->pixel_format_;
|
||||
if (format == VncPixelFormat::VNC_PIXEL_FORMAT_AUTO) {
|
||||
format = this->display_->get_display_type() == display::DISPLAY_TYPE_BINARY
|
||||
? VncPixelFormat::VNC_PIXEL_FORMAT_RGB332
|
||||
: VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
|
||||
}
|
||||
const bool rgb565 = format == VncPixelFormat::VNC_PIXEL_FORMAT_RGB565;
|
||||
const bool rgb565 = this->display_->get_display_type() != display::DISPLAY_TYPE_BINARY;
|
||||
this->bpp_ = rgb565 ? 2 : 1;
|
||||
this->bitness_ = rgb565 ? display::COLOR_BITNESS_565 : display::COLOR_BITNESS_332;
|
||||
|
||||
@@ -891,24 +632,6 @@ bool VncClient::start_session_(uint32_t now) {
|
||||
ESP_LOGW(TAG, "Server screen is %ux%u but the display is %dx%d; showing the top-left %ux%u", this->fb_w_,
|
||||
this->fb_h_, disp_w, disp_h, this->vis_w_, this->vis_h_);
|
||||
}
|
||||
const uint16_t cols = (this->vis_w_ + 15) / 16;
|
||||
const uint16_t rows = (this->vis_h_ + 15) / 16;
|
||||
const auto rotation = static_cast<uint8_t>(this->display_->get_rotation() / 90);
|
||||
const bool rotated = rotation != this->hash_rotation_;
|
||||
this->hash_rotation_ = rotation;
|
||||
if (this->tile_hashes_ == nullptr) {
|
||||
this->hash_cols_ = 0;
|
||||
this->hash_rows_ = 0;
|
||||
} else if (static_cast<uint32_t>(cols) * rows > this->hash_capacity_) {
|
||||
ESP_LOGW(TAG, "Screen has %u tiles, more than the %u cached; every tile is drawn", cols * rows,
|
||||
this->hash_capacity_);
|
||||
this->hash_cols_ = 0;
|
||||
this->hash_rows_ = 0;
|
||||
} else if (cols != this->hash_cols_ || rows != this->hash_rows_ || rotated) {
|
||||
this->hash_cols_ = cols;
|
||||
this->hash_rows_ = rows;
|
||||
this->clear_tiles_();
|
||||
}
|
||||
|
||||
// SetPixelFormat: true colour, in the byte order the display takes
|
||||
uint8_t pixel_format[20] = {};
|
||||
@@ -925,12 +648,10 @@ bool VncClient::start_session_(uint32_t now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SetEncodings
|
||||
uint8_t encodings[4 + 4 * std::size(ENCODINGS)] = {2};
|
||||
write_be16(encodings + 2, std::size(ENCODINGS));
|
||||
for (size_t i = 0; i < std::size(ENCODINGS); i++) {
|
||||
write_be32(encodings + 4 + 4 * i, static_cast<uint32_t>(ENCODINGS[i]));
|
||||
}
|
||||
// SetEncodings: TightVNC uses the first real encoding it supports for every rectangle.
|
||||
uint8_t encodings[12] = {2, 0, 0, 2};
|
||||
write_be32(encodings + 4, ENCODING_RAW);
|
||||
write_be32(encodings + 8, static_cast<uint32_t>(ENCODING_RICH_CURSOR));
|
||||
if (!this->send_(encodings, sizeof(encodings)) || !this->send_update_request_(false, now)) {
|
||||
return false;
|
||||
}
|
||||
@@ -948,12 +669,6 @@ void VncClient::pace_(uint32_t now) {
|
||||
if (this->display_busy_ || this->in_update_ || now - this->last_request_ms_ < MIN_REQUEST_SPACING_MS) {
|
||||
return;
|
||||
}
|
||||
if (this->need_full_) {
|
||||
if (this->send_update_request_(false, now)) {
|
||||
this->need_full_ = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Repeating a parked request costs nothing on the server and detects a dead link.
|
||||
if (!this->request_outstanding_ ||
|
||||
(now - this->last_rx_ms_ >= HEARTBEAT_MS && now - this->last_request_ms_ >= HEARTBEAT_MS)) {
|
||||
|
||||
@@ -10,12 +10,6 @@
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
enum class VncPixelFormat : uint8_t {
|
||||
VNC_PIXEL_FORMAT_AUTO,
|
||||
VNC_PIXEL_FORMAT_RGB565,
|
||||
VNC_PIXEL_FORMAT_RGB332,
|
||||
};
|
||||
|
||||
enum class VncState : uint8_t {
|
||||
VNC_STATE_IDLE,
|
||||
VNC_STATE_HANDSHAKE,
|
||||
@@ -37,9 +31,6 @@ enum class VncStep : uint8_t {
|
||||
VNC_STEP_RECT,
|
||||
VNC_STEP_RECT_DONE,
|
||||
VNC_STEP_RAW,
|
||||
VNC_STEP_TILE,
|
||||
VNC_STEP_TILE_RAW,
|
||||
VNC_STEP_SUBRECTS,
|
||||
VNC_STEP_SKIP,
|
||||
};
|
||||
|
||||
@@ -64,7 +55,6 @@ class VncClient : public Component {
|
||||
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
|
||||
void set_update_delay(uint32_t ms) { this->update_delay_ms_ = ms; }
|
||||
void set_max_update_delay(uint32_t ms) { this->max_update_delay_ms_ = ms; }
|
||||
void set_pixel_format(VncPixelFormat format) { this->pixel_format_ = format; }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
@@ -73,8 +63,6 @@ class VncClient : public Component {
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
bool is_connected() const { return this->state_ == VncState::VNC_STATE_RUNNING; }
|
||||
/// Ask the server for the whole screen again.
|
||||
void refresh();
|
||||
|
||||
template<typename F> void add_on_connect_callback(F &&callback) {
|
||||
this->connect_callback_.add(std::forward<F>(callback));
|
||||
@@ -85,7 +73,6 @@ class VncClient : public Component {
|
||||
|
||||
protected:
|
||||
static constexpr size_t RX_BUFFER_SIZE = 512;
|
||||
static constexpr size_t TILE_BUFFER_SIZE = 16 * 16 * 2;
|
||||
|
||||
void connect_step_(uint32_t now);
|
||||
void start_handshake_(uint32_t now);
|
||||
@@ -96,13 +83,7 @@ class VncClient : public Component {
|
||||
bool send_client_init_();
|
||||
void frame_done_();
|
||||
void rect_done_();
|
||||
void next_tile_();
|
||||
void draw_tile_();
|
||||
uint32_t tile_hash_() const;
|
||||
void clear_tiles_();
|
||||
void forget_tiles_(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
|
||||
void draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels);
|
||||
void fill_tile_(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *pixel);
|
||||
void skip_(uint32_t count, VncStep next);
|
||||
bool budget_exhausted_() const;
|
||||
bool send_(const uint8_t *data, size_t len);
|
||||
@@ -119,9 +100,6 @@ class VncClient : public Component {
|
||||
LazyCallbackManager<void()> connect_callback_;
|
||||
LazyCallbackManager<void()> disconnect_callback_;
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
// Fingerprint of the tile last drawn at each 16x16 cell, so tiles the server resends unchanged are skipped.
|
||||
// A 32-bit FNV-1a collision keeps a stale tile until it changes again or vnc.refresh runs; accepted.
|
||||
uint32_t *tile_hashes_{nullptr};
|
||||
|
||||
uint32_t update_delay_ms_{200};
|
||||
uint32_t max_update_delay_ms_{1000};
|
||||
@@ -153,28 +131,15 @@ class VncClient : public Component {
|
||||
uint16_t rect_y_{0};
|
||||
uint16_t rect_w_{0};
|
||||
uint16_t rect_h_{0};
|
||||
// Raw: column and row inside the rect. Hextile: absolute origin of the current tile.
|
||||
// Raw: column and row inside the rect.
|
||||
uint16_t pos_x_{0};
|
||||
uint16_t pos_y_{0};
|
||||
uint16_t tile_fill_{0};
|
||||
uint16_t drawn_tiles_{0};
|
||||
uint16_t hash_capacity_{0};
|
||||
uint16_t hash_cols_{0};
|
||||
uint16_t hash_rows_{0};
|
||||
|
||||
uint8_t tile_w_{0};
|
||||
uint8_t tile_h_{0};
|
||||
uint8_t subrects_left_{0};
|
||||
uint8_t bpp_{2};
|
||||
uint8_t minor_{8};
|
||||
uint8_t sec_count_{0};
|
||||
uint8_t text_len_{0}; // clamped to MAX_TEXT_LEN
|
||||
// The display rotation the cached tiles were drawn at, in quarter turns.
|
||||
uint8_t hash_rotation_{0};
|
||||
uint8_t hex_bg_[2]{};
|
||||
uint8_t hex_fg_[2]{};
|
||||
display::ColorBitness bitness_{display::COLOR_BITNESS_565};
|
||||
VncPixelFormat pixel_format_{VncPixelFormat::VNC_PIXEL_FORMAT_AUTO};
|
||||
VncState state_{VncState::VNC_STATE_IDLE};
|
||||
VncStep step_{VncStep::VNC_STEP_VERSION};
|
||||
VncStep after_skip_{VncStep::VNC_STEP_MESSAGE};
|
||||
@@ -184,19 +149,14 @@ class VncClient : public Component {
|
||||
bool in_update_{false};
|
||||
bool request_outstanding_{false};
|
||||
bool refresh_pending_{false};
|
||||
bool need_full_{false};
|
||||
bool frame_drawn_{false};
|
||||
bool subrects_coloured_{false};
|
||||
bool tile_hidden_{false};
|
||||
bool display_parks_{false};
|
||||
bool display_busy_{false};
|
||||
bool busy_warned_{false};
|
||||
bool refresh_retried_{false}; // one retry per refresh the driver abandoned
|
||||
bool network_was_up_{false};
|
||||
bool big_endian_; // the pixel order the display takes
|
||||
|
||||
uint8_t rx_[RX_BUFFER_SIZE];
|
||||
alignas(4) uint8_t tile_[TILE_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
} // namespace esphome::vnc
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include "weikai.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::weikai {
|
||||
|
||||
@@ -38,34 +37,35 @@ const char *p2s(uart::UARTParityOptions parity) {
|
||||
}
|
||||
|
||||
/// @brief Display a buffer in hexadecimal format (32 hex values / line) for debug
|
||||
void print_buffer(const char *tag, const uint8_t *data, size_t length, int level) {
|
||||
void print_buffer(const uint8_t *data, size_t length) {
|
||||
char hex_buffer[100];
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
|
||||
if (i % 32 == 31) {
|
||||
esp_log_printf_(level, tag, __LINE__, ESPHOME_LOG_FORMAT(" %s"), hex_buffer);
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
if (length % 32) {
|
||||
// null terminate if incomplete line
|
||||
hex_buffer[3 * (length % 32) + 2] = 0;
|
||||
esp_log_printf_(level, tag, __LINE__, ESPHOME_LOG_FORMAT(" %s"), hex_buffer);
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
PROGMEM_STRING_TABLE(RegNamesPage0, "GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER", "SIFR", "TFCNT",
|
||||
"RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485");
|
||||
PROGMEM_STRING_TABLE(RegNamesPage1, "GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL", "TFTL", "FWTH",
|
||||
"FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY");
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
|
||||
const LogString *reg_to_str(int reg, bool page1) {
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return LOG_STR("GPDAT");
|
||||
return "GPDAT";
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return LOG_STR("GPDIR");
|
||||
return "GPDIR";
|
||||
} else {
|
||||
return page1 ? RegNamesPage1::get_log_str(reg & 0x0F, 0) : RegNamesPage0::get_log_str(reg & 0x0F, 0);
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +495,24 @@ class Increment {
|
||||
uint8_t i_;
|
||||
};
|
||||
|
||||
/// @brief Hex converter to print/display a buffer in hexadecimal format (32 hex values / line).
|
||||
/// @param buffer contains the values to display
|
||||
void print_buffer(std::vector<uint8_t> buffer) {
|
||||
char hex_buffer[100];
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < buffer.size(); i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", buffer[i]);
|
||||
if (i % 32 == 31) {
|
||||
ESP_LOGI(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
if (buffer.size() % 32) {
|
||||
// null terminate if incomplete line
|
||||
hex_buffer[3 * (buffer.size() % 32) + 1] = 0;
|
||||
ESP_LOGI(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief test the write_array method
|
||||
void WeikaiChannel::uart_send_test_(char *message) {
|
||||
auto start_exec = micros();
|
||||
@@ -542,7 +560,7 @@ bool WeikaiChannel::uart_receive_test_(char *message) {
|
||||
for (size_t i = 0; i < RING_BUFFER_SIZE; i++) {
|
||||
if (buffer[i] != i % XFER_MAX_SIZE) {
|
||||
ESP_LOGE(TAG, "Read buffer contains error b=%x i=%x", buffer[i], i % XFER_MAX_SIZE);
|
||||
print_buffer(TAG, buffer.data(), buffer.size(), ESPHOME_LOG_LEVEL_INFO);
|
||||
print_buffer(buffer);
|
||||
status = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -40,12 +40,6 @@ constexpr size_t FIFO_SIZE = 256;
|
||||
/// @brief size of the ring buffer set to size of the FIFO
|
||||
constexpr size_t RING_BUFFER_SIZE = FIFO_SIZE;
|
||||
|
||||
/// @brief Name of a register for the log messages
|
||||
const LogString *reg_to_str(int reg, bool page1);
|
||||
|
||||
/// Logs a buffer in hexadecimal, 32 values per line, under @p tag at @p level.
|
||||
void print_buffer(const char *tag, const uint8_t *data, size_t length, int level = ESPHOME_LOG_LEVEL_VERY_VERBOSE);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// @brief This is an helper class that provides a simple ring buffers that works as a FIFO
|
||||
/// @details This ring buffer is used to buffer the bytes received in the FIFO of the Weika device. The best way to read
|
||||
|
||||
@@ -8,7 +8,38 @@
|
||||
namespace esphome::weikai_i2c {
|
||||
ESPHOME_LOG_TAG(TAG, "weikai_i2c");
|
||||
|
||||
/// @brief Display a buffer in hexadecimal format (32 hex values / line).
|
||||
void print_buffer(const uint8_t *data, size_t length) {
|
||||
char hex_buffer[100];
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
|
||||
if (i % 32 == 31) {
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
if (length % 32) {
|
||||
// null terminate if incomplete line
|
||||
hex_buffer[3 * (length % 32) + 2] = 0;
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
using namespace weikai;
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return "GPDAT";
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return "GPDIR";
|
||||
} else {
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
}
|
||||
}
|
||||
enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO
|
||||
|
||||
/// @brief Computes the I²C bus's address used to access the component
|
||||
@@ -41,11 +72,11 @@ uint8_t WeikaiRegisterI2C::read_reg() const {
|
||||
if (error == i2c::NO_ERROR) {
|
||||
this->comp_->status_clear_warning();
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterI2C::read_reg() @%02X reg=%s ch=%u I2C_code:%d, buf=%02X", address,
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
ESP_LOGE(TAG, "WeikaiRegisterI2C::read_reg() @%02X reg=%s ch=%u I2C_code:%d, buf=%02X", address,
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -60,7 +91,7 @@ void WeikaiRegisterI2C::read_fifo(uint8_t *data, size_t length) const {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterI2C::read_fifo() @%02X ch=%d I2C_code:%d len=%d buffer", address, this->channel_,
|
||||
(int) error, length);
|
||||
print_buffer(TAG, data, length);
|
||||
print_buffer(data, length);
|
||||
#endif
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
@@ -77,11 +108,11 @@ void WeikaiRegisterI2C::write_reg(uint8_t value) {
|
||||
if (error == i2c::NO_ERROR) {
|
||||
this->comp_->status_clear_warning();
|
||||
ESP_LOGVV(TAG, "WK2168Reg::write_reg() @%02X reg=%s ch=%d I2C_code:%d buf=%02X", address,
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
ESP_LOGE(TAG, "WK2168Reg::write_reg() @%02X reg=%s ch=%d I2C_code:%d buf=%d", address,
|
||||
LOG_STR_ARG(reg_to_str(this->register_, comp_i2c->page1())), this->channel_, (int) error, value);
|
||||
reg_to_str(this->register_, comp_i2c->page1()), this->channel_, (int) error, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +126,7 @@ void WeikaiRegisterI2C::write_fifo(uint8_t *data, size_t length) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "WK2168Reg::write_fifo() @%02X ch=%d I2C_code:%d len=%d buffer", address, this->channel_,
|
||||
(int) error, length);
|
||||
print_buffer(TAG, data, length);
|
||||
print_buffer(data, length);
|
||||
#endif
|
||||
} else { // error
|
||||
this->comp_->status_set_warning();
|
||||
|
||||
@@ -35,6 +35,39 @@ const char *p2s(uart::UARTParityOptions parity) {
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Display a buffer in hexadecimal format (32 hex values / line).
|
||||
void print_buffer(const uint8_t *data, size_t length) {
|
||||
char hex_buffer[100];
|
||||
hex_buffer[(3 * 32) + 1] = 0;
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", data[i]);
|
||||
if (i % 32 == 31) {
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
if (length % 32) {
|
||||
// null terminate if incomplete line
|
||||
hex_buffer[3 * (length % 32) + 2] = 0;
|
||||
ESP_LOGVV(TAG, " %s", hex_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
static const char *const REG_TO_STR_P0[16] = {"GENA", "GRST", "GMUT", "SPAGE", "SCR", "LCR", "FCR", "SIER",
|
||||
"SIFR", "TFCNT", "RFCNT", "FSR", "LSR", "FDAT", "FWCR", "RS485"};
|
||||
static const char *const REG_TO_STR_P1[16] = {"GENA", "GRST", "GMUT", "SPAGE", "BAUD1", "BAUD0", "PRES", "RFTL",
|
||||
"TFTL", "FWTH", "FWTL", "XON1", "XOFF1", "SADR", "SAEN", "RTSDLY"};
|
||||
|
||||
// method to print a register value as text: used in the log messages ...
|
||||
const char *reg_to_str(int reg, bool page1) {
|
||||
if (reg == WKREG_GPDAT) {
|
||||
return "GPDAT";
|
||||
} else if (reg == WKREG_GPDIR) {
|
||||
return "GPDIR";
|
||||
} else {
|
||||
return page1 ? REG_TO_STR_P1[reg & 0x0F] : REG_TO_STR_P0[reg & 0x0F];
|
||||
}
|
||||
}
|
||||
|
||||
enum RegType { REG = 0, FIFO = 1 }; ///< Register or FIFO
|
||||
enum CmdType { WRITE_CMD = 0, READ_CMD = 1 }; ///< Read or Write transfer
|
||||
|
||||
@@ -68,7 +101,7 @@ uint8_t WeikaiRegisterSPI::read_reg() const {
|
||||
spi_comp->disable();
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, cmd), cmd,
|
||||
LOG_STR_ARG(reg_to_str(this->register_, this->comp_->page1())), this->channel_, val);
|
||||
reg_to_str(this->register_, this->comp_->page1()), this->channel_, val);
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -83,7 +116,7 @@ void WeikaiRegisterSPI::read_fifo(uint8_t *data, size_t length) const {
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::read_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
|
||||
this->channel_, length);
|
||||
print_buffer(TAG, data, length);
|
||||
print_buffer(data, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -95,7 +128,7 @@ void WeikaiRegisterSPI::write_reg(uint8_t value) {
|
||||
spi_comp->disable();
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_reg() cmd=%s(%02X) reg=%s ch=%d buf=%02X", format_bin_to(bin_buf, buf[0]),
|
||||
buf[0], LOG_STR_ARG(reg_to_str(this->register_, this->comp_->page1())), this->channel_, buf[1]);
|
||||
buf[0], reg_to_str(this->register_, this->comp_->page1()), this->channel_, buf[1]);
|
||||
}
|
||||
|
||||
void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
@@ -110,7 +143,7 @@ void WeikaiRegisterSPI::write_fifo(uint8_t *data, size_t length) {
|
||||
char bin_buf[9];
|
||||
ESP_LOGVV(TAG, "WeikaiRegisterSPI::write_fifo() cmd=%s(%02X) ch=%d len=%d buffer", format_bin_to(bin_buf, cmd), cmd,
|
||||
this->channel_, length);
|
||||
print_buffer(TAG, data, length);
|
||||
print_buffer(data, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
const,
|
||||
get_esp32_variant,
|
||||
idf_version,
|
||||
only_on_variant,
|
||||
request_wifi,
|
||||
require_idf_version,
|
||||
require_mbedtls_tls,
|
||||
require_mbedtls_tls_extras,
|
||||
)
|
||||
@@ -232,8 +232,8 @@ def _validate_ap_mode(value: str) -> str:
|
||||
def _validate_dns_servers(value: list) -> list:
|
||||
if any(ip.is_unspecified for ip in value):
|
||||
raise cv.Invalid("0.0.0.0 is not a DNS server the access point can offer")
|
||||
if len(value) > 1:
|
||||
require_idf_version(cv.Version(6, 0, 0), "A second DNS server")(value)
|
||||
if len(value) > 1 and CORE.is_esp32 and idf_version() < cv.Version(6, 0, 0):
|
||||
raise cv.Invalid("The access point offers a second DNS server from ESP-IDF 6.0")
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -297,7 +297,6 @@
|
||||
#define USE_STATUS_LED
|
||||
#define USE_STATUS_SENSOR
|
||||
#define USE_SWITCH
|
||||
#define USE_SWITCH_STORAGE
|
||||
#define USE_TEXT
|
||||
#define USE_TEXT_SENSOR
|
||||
#define USE_TEXT_SENSOR_FILTER
|
||||
|
||||
@@ -42,7 +42,6 @@ from esphome.helpers import (
|
||||
sanitize,
|
||||
snake_case,
|
||||
)
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType, EntityMetadata, Expression, SafeExpType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -765,50 +764,3 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
# restore_state: keywords, shared by the entity domains that save their state
|
||||
RESTORE_STATE_KEEP = "KEEP"
|
||||
RESTORE_STATE_INVERT = "INVERT"
|
||||
RESTORE_STATE_INITIAL = "INITIAL"
|
||||
RESTORE_STATE_ALL = "ALL"
|
||||
RESTORE_STATE_NONE = "NONE"
|
||||
|
||||
|
||||
def restore_state_keyword(value: Any, keywords: tuple[str, ...]) -> str | None:
|
||||
"""The upper-cased keyword when `value` spells one of `keywords`, else None."""
|
||||
if isinstance(value, str):
|
||||
upper = value.strip().upper()
|
||||
if upper in keywords:
|
||||
return upper
|
||||
return None
|
||||
|
||||
|
||||
def restore_state_schema(fields_schema: cv.Schema, none: Any) -> Callable[[Any], Any]:
|
||||
"""The validator for a domain's `restore_state:` key.
|
||||
|
||||
A mapping is validated with `fields_schema`. The shorthand `all` is the empty mapping,
|
||||
so every field keeps its saved value, and `none` is returned as it is; a mapping goes through
|
||||
`fields_schema` like one the user wrote, so each config gets its own copy.
|
||||
"""
|
||||
|
||||
@schema_extractor("schema")
|
||||
def validate(value: Any) -> Any:
|
||||
if value == SCHEMA_EXTRACT:
|
||||
# The shorthands have no representation in the extracted schema
|
||||
return fields_schema
|
||||
keyword = restore_state_keyword(value, (RESTORE_STATE_ALL, RESTORE_STATE_NONE))
|
||||
if keyword == RESTORE_STATE_ALL:
|
||||
value = {}
|
||||
elif keyword == RESTORE_STATE_NONE:
|
||||
if not isinstance(none, dict):
|
||||
return none
|
||||
value = none
|
||||
elif isinstance(value, str):
|
||||
raise cv.Invalid(
|
||||
f"Unknown restore_state '{value}'; expected 'all', 'none' or a mapping, "
|
||||
"for example '{state: INVERT}'"
|
||||
)
|
||||
return fields_schema(value)
|
||||
|
||||
return validate
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
"""Tests for the ds3231 hub and alarm action validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components import ds3231
|
||||
from esphome.components.ds3231 import (
|
||||
ALARM_1_MODES,
|
||||
ALARM_2_MODES,
|
||||
CONF_DAY_OF_MONTH,
|
||||
CONF_DAY_OF_WEEK,
|
||||
SQUARE_WAVE_FREQUENCIES,
|
||||
SquareWaveFrequency,
|
||||
validate_square_wave_frequency,
|
||||
)
|
||||
from esphome.const import CONF_MODE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "frequency"),
|
||||
[
|
||||
("1Hz", SquareWaveFrequency.SQUARE_WAVE_1HZ),
|
||||
("1024Hz", SquareWaveFrequency.SQUARE_WAVE_1024HZ),
|
||||
("1.024kHz", SquareWaveFrequency.SQUARE_WAVE_1024HZ),
|
||||
("4096 Hz", SquareWaveFrequency.SQUARE_WAVE_4096HZ),
|
||||
(8192, SquareWaveFrequency.SQUARE_WAVE_8192HZ),
|
||||
],
|
||||
)
|
||||
def test_square_wave_frequency_accepts_every_spelling(
|
||||
value: str | int, frequency: object
|
||||
) -> None:
|
||||
assert validate_square_wave_frequency(value) == frequency
|
||||
|
||||
|
||||
def test_square_wave_frequency_rejects_other_rates() -> None:
|
||||
with pytest.raises(cv.Invalid, match="valid options are"):
|
||||
validate_square_wave_frequency("2048Hz")
|
||||
assert list(SQUARE_WAVE_FREQUENCIES) == [1, 1024, 4096, 8192]
|
||||
|
||||
|
||||
def test_alarm_modes_map_to_their_enumerators() -> None:
|
||||
assert str(ALARM_1_MODES["every_second"]) == "ds3231::ALARM_MODE_EVERY_SECOND"
|
||||
assert str(ALARM_1_MODES["weekly"]) == "ds3231::ALARM_MODE_WEEKLY"
|
||||
assert "every_second" not in ALARM_2_MODES
|
||||
|
||||
|
||||
def _alarm_1_schema() -> cv.Schema:
|
||||
return ds3231._alarm_schema(ALARM_1_MODES, with_second=True)
|
||||
|
||||
|
||||
def test_monthly_alarm_needs_a_day_of_month() -> None:
|
||||
with pytest.raises(cv.Invalid, match="'day_of_month' is required in monthly mode"):
|
||||
_alarm_1_schema()({CONF_MODE: "monthly"})
|
||||
|
||||
|
||||
def test_weekly_alarm_needs_a_day_of_week() -> None:
|
||||
with pytest.raises(cv.Invalid, match="'day_of_week' is required in weekly mode"):
|
||||
_alarm_1_schema()({CONF_MODE: "weekly"})
|
||||
|
||||
|
||||
def test_daily_alarm_fills_the_unused_days() -> None:
|
||||
config = _alarm_1_schema()({CONF_MODE: "daily"})
|
||||
assert config[CONF_DAY_OF_MONTH] == 1
|
||||
assert config[CONF_DAY_OF_WEEK] == 1
|
||||
|
||||
|
||||
def test_alarm_2_rejects_every_second() -> None:
|
||||
schema = ds3231._alarm_schema(ALARM_2_MODES, with_second=False)
|
||||
with pytest.raises(cv.Invalid, match="Unknown value 'every_second'"):
|
||||
schema({CONF_MODE: "every_second"})
|
||||
@@ -32,10 +32,8 @@ from esphome.components.esp32 import (
|
||||
_user_sdkconfig_wants_tls,
|
||||
allow_insecure_tls,
|
||||
idf_version,
|
||||
require_idf_version,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_BOARD,
|
||||
KEY_ESP32,
|
||||
KEY_EXCLUDE_COMPONENTS,
|
||||
KEY_IDF_VERSION,
|
||||
@@ -2444,44 +2442,3 @@ def test_allow_insecure_tls_only_switches_the_flags_on(
|
||||
"CONFIG_ESP_TLS_INSECURE": True,
|
||||
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY": True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("platform_framework", "idf", "ok"),
|
||||
[
|
||||
(PlatformFramework.ESP32_IDF, cv.Version(5, 5, 3), True),
|
||||
(PlatformFramework.ESP32_IDF, cv.Version(5, 5, 2), False),
|
||||
(PlatformFramework.ESP32_ARDUINO, cv.Version(5, 5, 3), True),
|
||||
(PlatformFramework.ESP32_ARDUINO, cv.Version(5, 5, 2), False),
|
||||
],
|
||||
)
|
||||
def test_require_idf_version_compares_the_idf_underneath(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
platform_framework: PlatformFramework,
|
||||
idf: cv.Version,
|
||||
ok: bool,
|
||||
) -> None:
|
||||
set_core_config(
|
||||
platform_framework,
|
||||
platform_data={
|
||||
KEY_BOARD: "esp32dev",
|
||||
KEY_VARIANT: VARIANT_ESP32,
|
||||
KEY_IDF_VERSION: idf,
|
||||
},
|
||||
)
|
||||
validator = require_idf_version(cv.Version(5, 5, 3), "The thing")
|
||||
if ok:
|
||||
assert validator("x") == "x"
|
||||
else:
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="The thing requires ESP-IDF 5.5.3 or newer, got 5.5.2"
|
||||
):
|
||||
validator("x")
|
||||
|
||||
|
||||
def test_require_idf_version_rejects_other_platforms(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
||||
with pytest.raises(cv.Invalid, match="only available on"):
|
||||
require_idf_version(cv.Version(5, 5, 3))("x")
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
"""Validate RX8111 alarm and timer options before generating firmware."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.rx8111.time import (
|
||||
_ALARM_DURATION,
|
||||
ALARM_FIELD_UNSET,
|
||||
CONF_ALARM_DAY,
|
||||
CONF_ALARM_ENABLED,
|
||||
CONF_ALARM_WEEKDAY,
|
||||
CONF_DAY,
|
||||
CONF_ON_ALARM,
|
||||
CONF_ON_TIMER,
|
||||
CONF_SECOND,
|
||||
CONF_TIMER_COUNT,
|
||||
CONF_TIMER_ENABLED,
|
||||
CONF_WEEKDAY,
|
||||
_validate_alarm_config,
|
||||
_validate_alarm_options,
|
||||
_validate_set_alarm,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[{}, {CONF_SECOND: 0, CONF_WEEKDAY: 1, CONF_DAY: 1}],
|
||||
)
|
||||
def test_set_alarm_rejects_empty_or_conflicting_fields(config: dict) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
_validate_set_alarm(config)
|
||||
|
||||
|
||||
def test_set_alarm_marks_missing_fields_unset() -> None:
|
||||
config = _validate_set_alarm({CONF_SECOND: 5})
|
||||
assert config[CONF_SECOND] == 5
|
||||
assert config[CONF_DAY] == ALARM_FIELD_UNSET
|
||||
|
||||
|
||||
def test_fixed_alarm_rejects_day_and_weekday() -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
_validate_alarm_options({CONF_ALARM_DAY: 1, CONF_ALARM_WEEKDAY: 2})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("duration", ["0s", "28d", "29d"])
|
||||
def test_relative_alarm_rejects_out_of_range_duration(duration: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
_ALARM_DURATION(duration)
|
||||
|
||||
|
||||
def test_relative_alarm_accepts_mixed_units() -> None:
|
||||
assert _ALARM_DURATION({"days": 1, "minutes": 1}).total_seconds == 86460
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{CONF_TIMER_ENABLED: True},
|
||||
{CONF_ON_TIMER: [{}]},
|
||||
{CONF_TIMER_ENABLED: False, CONF_ON_TIMER: [{}]},
|
||||
],
|
||||
)
|
||||
def test_timer_requires_count(config: dict) -> None:
|
||||
with pytest.raises(cv.Invalid, match="timer_count"):
|
||||
_validate_alarm_config(config)
|
||||
|
||||
|
||||
def test_timer_count_allows_explicit_disabled_configuration() -> None:
|
||||
config = {CONF_TIMER_COUNT: 0xFFFFFF, CONF_TIMER_ENABLED: False}
|
||||
assert _validate_alarm_config(config) == config
|
||||
|
||||
|
||||
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})
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
logger:
|
||||
|
||||
switch:
|
||||
- {platform: template, id: legacy, optimistic: true, restore_mode: RESTORE_DEFAULT_OFF}
|
||||
- {platform: template, id: plain, optimistic: true}
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
logger:
|
||||
|
||||
switch:
|
||||
- {platform: template, id: keep_rtc, optimistic: true, initial_state: ON, restore_state: {state: KEEP, storage: rtc}}
|
||||
- {platform: template, id: keep_flash, optimistic: true, restore_state: {state: KEEP, storage: flash}}
|
||||
- {platform: template, id: keep_short, optimistic: true, restore_state: {state: KEEP}}
|
||||
- {platform: template, id: all_short, optimistic: true, restore_state: ALL}
|
||||
- {platform: template, id: invert_on, optimistic: true, initial_state: ON, restore_state: {state: INVERT}}
|
||||
- {platform: template, id: initial_on, optimistic: true, initial_state: ON, restore_state: {state: INITIAL}}
|
||||
- {platform: template, id: none_on, optimistic: true, initial_state: ON, restore_state: NONE}
|
||||
- {platform: template, id: initial_only, optimistic: true, initial_state: ON}
|
||||
- {platform: template, id: legacy, optimistic: true, restore_mode: RESTORE_DEFAULT_OFF}
|
||||
- {platform: template, id: plain, optimistic: true}
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: esp12e
|
||||
restore_from_flash: true
|
||||
|
||||
logger:
|
||||
|
||||
switch:
|
||||
- {platform: template, id: keep_rtc, optimistic: true, initial_state: ON, restore_state: {state: KEEP, storage: rtc}}
|
||||
@@ -1,216 +0,0 @@
|
||||
"""Tests for switch `initial_state` / `restore_state` (including `storage`)."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import switch
|
||||
from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231T,
|
||||
FAMILY_BK7251,
|
||||
FAMILY_LN882H,
|
||||
FAMILY_RTL8720C,
|
||||
KEY_FAMILY,
|
||||
KEY_LIBRETINY,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
GATE = "USE_SWITCH_STORAGE"
|
||||
ESP32_RTC = "USE_ESP32_RTC_PREFERENCES"
|
||||
|
||||
|
||||
def _defines() -> set[str]:
|
||||
return {define.name for define in CORE.defines}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("switch_id", "mode"),
|
||||
[
|
||||
("keep_short", "SWITCH_RESTORE_DEFAULT_OFF"),
|
||||
("all_short", "SWITCH_RESTORE_DEFAULT_OFF"),
|
||||
("invert_on", "SWITCH_RESTORE_INVERTED_DEFAULT_ON"),
|
||||
("initial_on", "SWITCH_ALWAYS_ON"),
|
||||
("none_on", "SWITCH_ALWAYS_ON"),
|
||||
("initial_only", "SWITCH_ALWAYS_ON"),
|
||||
("legacy", "SWITCH_RESTORE_DEFAULT_OFF"),
|
||||
("plain", "SWITCH_ALWAYS_OFF"),
|
||||
],
|
||||
)
|
||||
def test_restore_state_maps_to_a_restore_mode(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
switch_id: str,
|
||||
mode: str,
|
||||
) -> None:
|
||||
"""Every initial_state / restore_state combination becomes one existing restore mode."""
|
||||
main_cpp = generate_main(component_config_path("restore_state_esp32.yaml"))
|
||||
|
||||
assert f"{switch_id}->set_restore_mode(switch_::{mode});" in main_cpp
|
||||
|
||||
|
||||
def test_storage_is_passed_only_when_set(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("restore_state_esp32.yaml"))
|
||||
|
||||
assert (
|
||||
"keep_rtc->set_restore_mode(static_cast<switch_::SwitchRestoreMode>("
|
||||
"switch_::SWITCH_RESTORE_DEFAULT_ON | switch_::RESTORE_MODE_STORAGE_RTC_MASK));"
|
||||
) in main_cpp
|
||||
assert (
|
||||
"keep_flash->set_restore_mode(static_cast<switch_::SwitchRestoreMode>("
|
||||
"switch_::SWITCH_RESTORE_DEFAULT_OFF | switch_::RESTORE_MODE_STORAGE_FLASH_MASK));"
|
||||
) in main_cpp
|
||||
assert (
|
||||
"keep_short->set_restore_mode(switch_::SWITCH_RESTORE_DEFAULT_OFF);" in main_cpp
|
||||
)
|
||||
assert GATE in _defines()
|
||||
assert ESP32_RTC in _defines()
|
||||
|
||||
|
||||
def test_nothing_new_emitted_without_restore_state(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Configs using only restore_mode generate exactly what they did before."""
|
||||
main_cpp = generate_main(component_config_path("restore_state_absent_esp32.yaml"))
|
||||
|
||||
assert "RESTORE_MODE_STORAGE_" not in main_cpp
|
||||
assert GATE not in _defines()
|
||||
assert ESP32_RTC not in _defines()
|
||||
|
||||
|
||||
def test_rtc_applies_per_switch_on_esp8266_with_restore_from_flash(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("restore_state_esp8266.yaml"))
|
||||
|
||||
assert (
|
||||
"keep_rtc->set_restore_mode(static_cast<switch_::SwitchRestoreMode>("
|
||||
"switch_::SWITCH_RESTORE_DEFAULT_ON | switch_::RESTORE_MODE_STORAGE_RTC_MASK));"
|
||||
) in main_cpp
|
||||
assert ESP32_RTC not in _defines()
|
||||
|
||||
|
||||
def _validate(config: dict, *, default_restore_mode: str = cv.UNDEFINED) -> dict:
|
||||
schema = switch.switch_schema(
|
||||
switch.Switch, default_restore_mode=default_restore_mode
|
||||
)
|
||||
return schema({"id": "test_switch", **config})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def esp8266(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("esp8266")
|
||||
def test_restore_mode_and_restore_state_are_exclusive() -> None:
|
||||
with pytest.raises(cv.Invalid, match="restore"):
|
||||
_validate({"restore_mode": "RESTORE_DEFAULT_OFF", "restore_state": "all"})
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("esp8266")
|
||||
def test_initial_state_rejected_with_restore_mode() -> None:
|
||||
"""restore_mode already decides the boot state, so initial_state would be ignored."""
|
||||
with pytest.raises(cv.Invalid, match="initial_state"):
|
||||
_validate({"restore_mode": "ALWAYS_OFF", "initial_state": True})
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("esp8266")
|
||||
def test_storage_rejected_when_nothing_is_restored() -> None:
|
||||
with pytest.raises(cv.Invalid, match="only applies"):
|
||||
_validate({"restore_state": {"state": "INITIAL", "storage": "rtc"}})
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("esp8266")
|
||||
@pytest.mark.parametrize("value", ["RESTORE", "ON", "OFF", "KEEP", "INVERT", "INITIAL"])
|
||||
def test_unsupported_restore_state_values_rejected(value: object) -> None:
|
||||
# Only light's shorthands (all, none) are accepted as a string
|
||||
with pytest.raises(cv.Invalid, match="expected 'all', 'none' or a mapping"):
|
||||
_validate({"restore_state": value})
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("esp8266")
|
||||
@pytest.mark.parametrize("state", [True, False, "ON", "off"])
|
||||
def test_restore_state_on_off_explains_what_to_use(state: object) -> None:
|
||||
# Valid on light, so people will try it; YAML turns ON into True
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="initial_state: ON' \\(or OFF\\) with 'restore_state: none'"
|
||||
):
|
||||
_validate({"restore_state": {"state": state}})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("platform", "family"),
|
||||
[
|
||||
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7251),
|
||||
(PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8720C),
|
||||
(PlatformFramework.LN882X_ARDUINO, FAMILY_LN882H),
|
||||
(PlatformFramework.RP2040_ARDUINO, None),
|
||||
(PlatformFramework.NRF52_ZEPHYR, None),
|
||||
(PlatformFramework.HOST_NATIVE, None),
|
||||
],
|
||||
)
|
||||
def test_rtc_rejected_without_an_rtc_backend(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
platform: PlatformFramework,
|
||||
family: str | None,
|
||||
) -> None:
|
||||
"""These preferences backends ignore in_flash, so the state would silently stay in flash."""
|
||||
set_core_config(platform)
|
||||
if family is not None:
|
||||
CORE.data[KEY_LIBRETINY] = {KEY_FAMILY: family}
|
||||
with pytest.raises(cv.Invalid, match="'rtc' storage is not supported"):
|
||||
_validate({"restore_state": {"state": "KEEP", "storage": "rtc"}})
|
||||
|
||||
|
||||
def test_rtc_accepted_on_a_beken_chip_with_the_emulation(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.BK72XX_ARDUINO)
|
||||
CORE.data[KEY_LIBRETINY] = {KEY_FAMILY: FAMILY_BK7231T}
|
||||
_validate({"restore_state": {"state": "KEEP", "storage": "rtc"}})
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("esp8266")
|
||||
def test_rtc_accepted_on_esp8266() -> None:
|
||||
_validate({"restore_state": {"state": "KEEP", "storage": "rtc"}})
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("esp8266")
|
||||
def test_platform_default_restore_mode_applies_only_when_nothing_is_set() -> None:
|
||||
"""Platforms such as haier default to DISABLED; restore_state must still be usable there."""
|
||||
assert _validate({}, default_restore_mode="DISABLED")["restore_mode"] == "DISABLED"
|
||||
configured = _validate({"restore_state": "all"}, default_restore_mode="DISABLED")
|
||||
assert "restore_mode" not in configured
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("esp8266")
|
||||
@pytest.mark.parametrize(
|
||||
("default_restore_mode", "initial_state", "expected"),
|
||||
[
|
||||
# A restoring default keeps restoring; initial_state only replaces its fallback
|
||||
("RESTORE_DEFAULT_ON", False, "RESTORE_DEFAULT_OFF"),
|
||||
("RESTORE_DEFAULT_OFF", True, "RESTORE_DEFAULT_ON"),
|
||||
("RESTORE_INVERTED_DEFAULT_OFF", True, "RESTORE_INVERTED_DEFAULT_ON"),
|
||||
# A default that restores nothing starts in initial_state
|
||||
("ALWAYS_OFF", True, "ALWAYS_ON"),
|
||||
("DISABLED", True, "ALWAYS_ON"),
|
||||
],
|
||||
)
|
||||
def test_initial_state_alone_keeps_a_restoring_platform_default(
|
||||
default_restore_mode: str, initial_state: bool, expected: str
|
||||
) -> None:
|
||||
config = _validate(
|
||||
{"initial_state": initial_state}, default_restore_mode=default_restore_mode
|
||||
)
|
||||
assert "restore_mode" not in config
|
||||
assert "storage" not in config.get("restore_state", {})
|
||||
assert switch._resolve_restore_mode(config) is switch.RESTORE_MODES[expected]
|
||||
@@ -1,15 +1,9 @@
|
||||
"""Tests for teleinfo sensor tag presets."""
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.teleinfo import CONF_TAG_NAME
|
||||
from esphome.components.teleinfo.sensor import (
|
||||
TIC_TAG_CONFIGS,
|
||||
TIC_TEXT_TAGS,
|
||||
apply_tag_config,
|
||||
)
|
||||
from esphome.components.teleinfo.sensor import apply_tag_config
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ICON,
|
||||
@@ -17,7 +11,6 @@ from esphome.const import (
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_REACTIVE_ENERGY,
|
||||
@@ -29,7 +22,6 @@ from esphome.const import (
|
||||
UNIT_KILOVOLT_AMPS,
|
||||
UNIT_KILOWATT_HOURS,
|
||||
UNIT_MILLIAMP,
|
||||
UNIT_MINUTE,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_VOLT_AMPS_REACTIVE_HOURS,
|
||||
@@ -68,13 +60,7 @@ from esphome.const import (
|
||||
("IINST1", UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT),
|
||||
("ADIR2", UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT),
|
||||
("PAPP", UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT),
|
||||
(
|
||||
"PMAX",
|
||||
UNIT_VOLT_AMPS,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
("PEJP", UNIT_MINUTE, DEVICE_CLASS_DURATION, STATE_CLASS_MEASUREMENT),
|
||||
("PMAX", UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT),
|
||||
],
|
||||
)
|
||||
def test_known_tag_preset(
|
||||
@@ -87,42 +73,6 @@ def test_known_tag_preset(
|
||||
assert CONF_ICON not in config
|
||||
|
||||
|
||||
def test_no_prefix_is_prefix_of_another() -> None:
|
||||
prefixes = [*TIC_TAG_CONFIGS, *TIC_TEXT_TAGS]
|
||||
assert len(prefixes) == len(set(prefixes))
|
||||
for prefix in prefixes:
|
||||
for other in prefixes:
|
||||
assert prefix == other or not other.startswith(prefix)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", ["NTARF", "NJOURF", "NJOURF+1", "RELAIS", "VTIC"])
|
||||
def test_unitless_tag_gets_no_unit(tag: str, caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
config = apply_tag_config({CONF_TAG_NAME: tag})
|
||||
assert config == {CONF_TAG_NAME: tag}
|
||||
assert not caplog.records
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tag", ["LTARF", "NGTF", "PTEC", "OPTARIF", "MSG1", "PPOT"])
|
||||
def test_text_tag_gets_no_unit_and_warns(
|
||||
tag: str, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
config = apply_tag_config({CONF_TAG_NAME: tag})
|
||||
assert config == {CONF_TAG_NAME: tag}
|
||||
assert "text_sensor" in caplog.text
|
||||
|
||||
|
||||
def test_unitless_tag_keeps_user_settings() -> None:
|
||||
config = apply_tag_config(
|
||||
{CONF_TAG_NAME: "NTARF", CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT}
|
||||
)
|
||||
assert config == {
|
||||
CONF_TAG_NAME: "NTARF",
|
||||
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
|
||||
}
|
||||
|
||||
|
||||
def test_unknown_tag_keeps_legacy_defaults() -> None:
|
||||
config = apply_tag_config({CONF_TAG_NAME: "MYTAG"})
|
||||
assert config[CONF_UNIT_OF_MEASUREMENT] == UNIT_WATT_HOURS
|
||||
|
||||
@@ -168,5 +168,5 @@ def test_final_validate_needs_idf_5_5(
|
||||
) -> None:
|
||||
_set_variant(set_core_config, VARIANT_ESP32S3, idf=idf)
|
||||
usb_host.request_periodic_fifo(out_bytes=196)
|
||||
with pytest.raises(cv.Invalid, match="requires ESP-IDF 5.5.0 or newer"):
|
||||
with pytest.raises(cv.Invalid, match="needs ESP-IDF 5.5.0 or later"):
|
||||
usb_host.FINAL_VALIDATE_SCHEMA({})
|
||||
|
||||
@@ -168,7 +168,6 @@ def test_it8951_owned_by_vnc(
|
||||
assert "set_reconnect_interval(5000);" in main_cpp
|
||||
assert "set_update_delay(200);" in main_cpp
|
||||
assert "set_max_update_delay(1000);" in main_cpp
|
||||
assert "set_pixel_format(vnc::VncPixelFormat::VNC_PIXEL_FORMAT_AUTO);" in main_cpp
|
||||
|
||||
|
||||
def test_epaper_spi_owned_by_vnc(
|
||||
@@ -222,7 +221,6 @@ vnc:
|
||||
reconnect_interval: 2s
|
||||
update_delay: 0ms
|
||||
max_update_delay: 3s
|
||||
pixel_format: RGB332
|
||||
on_connect:
|
||||
- logger.log: up
|
||||
on_disconnect:
|
||||
@@ -236,7 +234,6 @@ vnc:
|
||||
assert "set_reconnect_interval(2000);" in main_cpp
|
||||
assert "set_update_delay(0);" in main_cpp
|
||||
assert "set_max_update_delay(3000);" in main_cpp
|
||||
assert "set_pixel_format(vnc::VncPixelFormat::VNC_PIXEL_FORMAT_RGB332);" in main_cpp
|
||||
assert "add_on_connect_callback(" in main_cpp
|
||||
assert "add_on_disconnect_callback(" in main_cpp
|
||||
|
||||
@@ -263,28 +260,6 @@ vnc:
|
||||
assert TEST_CARD not in main_cpp
|
||||
|
||||
|
||||
def test_action_codegen(
|
||||
generate_main: Callable[[str | Path], str], write_yaml: Callable[..., Path]
|
||||
) -> None:
|
||||
main_cpp = generate_main(
|
||||
write_yaml(
|
||||
IT8951,
|
||||
"""
|
||||
vnc:
|
||||
- id: remote
|
||||
host: 192.168.1.20
|
||||
|
||||
interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- vnc.refresh
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
||||
assert "remote->refresh();" in main_cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("display_options", "error"),
|
||||
[
|
||||
@@ -414,9 +389,6 @@ display:
|
||||
id="reconnect",
|
||||
),
|
||||
pytest.param(" port: 0\n", "value must be at least 1", id="port"),
|
||||
pytest.param(
|
||||
" pixel_format: rgb888\n", "Unknown value 'rgb888'", id="format"
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_option_rejected(
|
||||
|
||||
@@ -14,8 +14,3 @@ esphome:
|
||||
then:
|
||||
- deep_sleep.enter:
|
||||
sleep_duration: 60min
|
||||
- if:
|
||||
condition:
|
||||
lambda: 'return deep_sleep::woke_from_deep_sleep();'
|
||||
then:
|
||||
- logger.log: Woke from deep sleep
|
||||
|
||||
@@ -1,74 +1,15 @@
|
||||
ds3231:
|
||||
- id: ds3231_component
|
||||
i2c_id: i2c_bus
|
||||
address: 0x68
|
||||
|
||||
sensor:
|
||||
- platform: ds3231
|
||||
ds3231_id: ds3231_component
|
||||
name: DS3231 Temperature
|
||||
update_interval: never
|
||||
|
||||
binary_sensor:
|
||||
- platform: ds3231
|
||||
ds3231_id: ds3231_component
|
||||
alarm_1:
|
||||
name: DS3231 Alarm 1
|
||||
alarm_2:
|
||||
name: DS3231 Alarm 2
|
||||
oscillator_stopped:
|
||||
name: DS3231 Oscillator Stopped
|
||||
|
||||
time:
|
||||
- platform: ds3231
|
||||
id: ds3231_time
|
||||
ds3231_id: ds3231_component
|
||||
update_interval: never
|
||||
|
||||
interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- ds3231.force_temperature_conversion: ds3231_component
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: DS3231 Actions
|
||||
on_press:
|
||||
- ds3231.write_time:
|
||||
id: ds3231_time
|
||||
- ds3231.read_time: ds3231_time
|
||||
- ds3231.set_alarm_1:
|
||||
id: ds3231_component
|
||||
mode: daily
|
||||
hour: 7
|
||||
minute: 30
|
||||
second: 15
|
||||
- ds3231.set_alarm_1:
|
||||
mode: every_second
|
||||
enabled: false
|
||||
- ds3231.set_alarm_1:
|
||||
mode: monthly
|
||||
day_of_month: 15
|
||||
hour: !lambda return 12;
|
||||
- ds3231.set_alarm_2:
|
||||
mode: weekly
|
||||
day_of_week: 2
|
||||
hour: 6
|
||||
minute: !lambda return 45;
|
||||
- ds3231.set_alarm_2:
|
||||
mode: every_minute
|
||||
- ds3231.enable_alarm:
|
||||
alarm: 1
|
||||
- ds3231.disable_alarm:
|
||||
alarm: 2
|
||||
- ds3231.clear_alarm:
|
||||
alarm: !lambda return 1;
|
||||
- if:
|
||||
condition:
|
||||
ds3231.is_alarm_triggered:
|
||||
alarm: 2
|
||||
then:
|
||||
- ds3231.clear_alarm:
|
||||
alarm: 2
|
||||
- ds3231.enable_square_wave:
|
||||
frequency: 1Hz
|
||||
- ds3231.disable_square_wave: ds3231_component
|
||||
- ds3231.set_32khz_output:
|
||||
state: true
|
||||
- ds3231.set_aging_offset:
|
||||
offset: !lambda return 3;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
ds3231:
|
||||
- id: ds3231_component
|
||||
i2c_id: i2c_bus
|
||||
address: 0x68
|
||||
interrupt_pin:
|
||||
number: ${interrupt_pin}
|
||||
mode:
|
||||
input: true
|
||||
pullup: true
|
||||
output_32khz: true
|
||||
auto_clear_alarms: false
|
||||
on_alarm_1:
|
||||
- logger.log: "DS3231 alarm 1"
|
||||
- ds3231.clear_alarm:
|
||||
alarm: 1
|
||||
@@ -1,14 +0,0 @@
|
||||
ds3231:
|
||||
- id: ds3231_component
|
||||
i2c_id: i2c_bus
|
||||
address: 0x68
|
||||
square_wave_frequency: 1024Hz
|
||||
battery_backed_square_wave: false
|
||||
output_32khz: false
|
||||
aging_offset: -2
|
||||
auto_clear_alarms: true
|
||||
alarm_check_interval: 2s
|
||||
on_alarm_1:
|
||||
- logger.log: "DS3231 alarm 1"
|
||||
on_alarm_2:
|
||||
- logger.log: "DS3231 alarm 2"
|
||||
@@ -1,7 +0,0 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO14
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
ds3231_hub: !include hub-interrupt.yaml
|
||||
ds3231: !include common.yaml
|
||||
@@ -1,7 +1,3 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO13
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
ds3231_hub: !include hub-interrupt.yaml
|
||||
ds3231: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
ds3231_hub: !include hub-polling.yaml
|
||||
ds3231: !include common.yaml
|
||||
|
||||
@@ -19,6 +19,11 @@ image:
|
||||
} else {
|
||||
ESP_LOGD("online_image", "Cache miss: fresh download");
|
||||
}
|
||||
on_download_started:
|
||||
- logger.log: Download started
|
||||
on_download_progress:
|
||||
lambda: |-
|
||||
ESP_LOGD("online_image", "Downloaded %.0f%% (%zu bytes)", progress, bytes_read);
|
||||
- platform: online_image
|
||||
id: online_binary_transparent_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
@@ -27,6 +28,9 @@ class FakeContainer : public http_request::HttpContainer {
|
||||
}
|
||||
|
||||
int read(uint8_t *buf, size_t max_len) override {
|
||||
this->largest_read = std::max(this->largest_read, max_len);
|
||||
if (this->read_error_)
|
||||
return -1;
|
||||
size_t len = std::min(max_len, this->available_ - this->bytes_read_);
|
||||
std::memcpy(buf, this->body_.data() + this->bytes_read_, len);
|
||||
this->bytes_read_ += len;
|
||||
@@ -42,13 +46,21 @@ class FakeContainer : public http_request::HttpContainer {
|
||||
}
|
||||
|
||||
void set_content_length(size_t length) { this->content_length = length; }
|
||||
/// Make every read from now on fail, like a dropped connection
|
||||
void fail_reads() { this->read_error_ = true; }
|
||||
/// Let len more bytes of the body arrive
|
||||
void deliver(size_t len) { this->available_ = std::min(this->body_.size(), this->available_ + len); }
|
||||
void add_response_header(const std::string &name, const std::string &value) {
|
||||
this->response_headers_.push_back({name, value});
|
||||
}
|
||||
|
||||
/// The most bytes any one read() asked for
|
||||
size_t largest_read{0};
|
||||
|
||||
protected:
|
||||
std::vector<uint8_t> body_;
|
||||
size_t available_;
|
||||
bool read_error_{false};
|
||||
};
|
||||
|
||||
class FakeHttpRequest : public http_request::HttpRequestComponent {
|
||||
@@ -123,13 +135,299 @@ static constexpr std::array<uint8_t, 69> PNG_1X1{
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xC9,
|
||||
0xFE, 0x92, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};
|
||||
|
||||
// 800x600 RGB PNG of a single color, 2312 bytes: the decoder needs several calls to draw it
|
||||
static const uint8_t PNG_SOLID[] = {
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03,
|
||||
0x20, 0x00, 0x00, 0x02, 0x58, 0x08, 0x02, 0x00, 0x00, 0x00, 0x15, 0x14, 0x15, 0x27, 0x00, 0x00, 0x08, 0xCF, 0x49,
|
||||
0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0xD6, 0x31, 0x11, 0x00, 0x30, 0x08, 0x00, 0xB1, 0x52, 0x21, 0xE8, 0x44, 0x22,
|
||||
0xB2, 0x30, 0xC1, 0x31, 0x25, 0x12, 0x7E, 0xFA, 0xC8, 0xEA, 0x07, 0x00, 0xC0, 0x9E, 0x2F, 0x01, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0xDC, 0x19, 0x79, 0xA5, 0x06, 0x0E, 0x33, 0xD4, 0xAF,
|
||||
0xF2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
};
|
||||
|
||||
// 8x8 progressive JPEG, 521 bytes
|
||||
static const uint8_t JPEG_PROGRESSIVE[] = {
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
||||
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x10, 0x0B, 0x0C, 0x0E, 0x0C, 0x0A, 0x10, 0x0E, 0x0D, 0x0E, 0x12, 0x11, 0x10,
|
||||
0x13, 0x18, 0x28, 0x1A, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25, 0x1D, 0x28, 0x3A, 0x33, 0x3D, 0x3C, 0x39, 0x33,
|
||||
0x38, 0x37, 0x40, 0x48, 0x5C, 0x4E, 0x40, 0x44, 0x57, 0x45, 0x37, 0x38, 0x50, 0x6D, 0x51, 0x57, 0x5F, 0x62, 0x67,
|
||||
0x68, 0x67, 0x3E, 0x4D, 0x71, 0x79, 0x70, 0x64, 0x78, 0x5C, 0x65, 0x67, 0x63, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x11,
|
||||
0x12, 0x12, 0x18, 0x15, 0x18, 0x2F, 0x1A, 0x1A, 0x2F, 0x63, 0x42, 0x38, 0x42, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
|
||||
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
|
||||
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63,
|
||||
0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0xFF, 0xC2, 0x00, 0x11, 0x08, 0x00, 0x08, 0x00, 0x08, 0x03, 0x01, 0x22, 0x00,
|
||||
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x15, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xFF, 0xC4, 0x00, 0x15, 0x01, 0x01, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x05, 0xFF, 0xDA, 0x00, 0x0C, 0x03,
|
||||
0x01, 0x00, 0x02, 0x10, 0x03, 0x10, 0x00, 0x00, 0x01, 0x94, 0x05, 0x3F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08,
|
||||
0x01, 0x01, 0x00, 0x01, 0x05, 0x02, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F,
|
||||
0x01, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x01, 0x7F, 0xFF, 0xC4, 0x00,
|
||||
0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x06, 0x3F, 0x02, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01,
|
||||
0x01, 0x00, 0x01, 0x3F, 0x21, 0x7F, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00,
|
||||
0x10, 0x0B, 0xFF, 0xC4, 0x00, 0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x03, 0x01, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xC4, 0x00,
|
||||
0x14, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0xDA, 0x00, 0x08, 0x01, 0x02, 0x01, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01,
|
||||
0x01, 0x00, 0x01, 0x3F, 0x10, 0x7F, 0xFF, 0xD9,
|
||||
};
|
||||
|
||||
// 48x40 4:2:0 JPEG (quality 95), 2344 bytes: three rows of 16 pixel MCUs, with the scan starting at byte 609
|
||||
static const uint8_t JPEG_48X40[] = {
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
||||
0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x02, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02,
|
||||
0x02, 0x02, 0x04, 0x03, 0x02, 0x02, 0x02, 0x02, 0x05, 0x04, 0x04, 0x03, 0x04, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05,
|
||||
0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x06, 0x07, 0x09, 0x07, 0x06, 0x06, 0x08, 0x0B, 0x08, 0x09, 0x0A, 0x0A, 0x0A,
|
||||
0x0A, 0x0A, 0x06, 0x08, 0x0B, 0x0C, 0x0B, 0x0A, 0x0C, 0x09, 0x0A, 0x0A, 0x0A, 0xFF, 0xDB, 0x00, 0x43, 0x01, 0x02,
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x05, 0x03, 0x03, 0x05, 0x0A, 0x07, 0x06, 0x07, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
|
||||
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
|
||||
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A,
|
||||
0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0x0A, 0xFF, 0xC0, 0x00, 0x11, 0x08, 0x00, 0x28, 0x00, 0x30, 0x03, 0x01, 0x22, 0x00,
|
||||
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xFF, 0xC4, 0x00, 0x1F, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
|
||||
0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00,
|
||||
0x00, 0x01, 0x7D, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
|
||||
0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xA1, 0x08, 0x23, 0x42, 0xB1, 0xC1, 0x15, 0x52, 0xD1, 0xF0, 0x24, 0x33, 0x62,
|
||||
0x72, 0x82, 0x09, 0x0A, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x34, 0x35, 0x36, 0x37,
|
||||
0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A,
|
||||
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x83, 0x84, 0x85,
|
||||
0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6,
|
||||
0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7,
|
||||
0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
|
||||
0xE8, 0xE9, 0xEA, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFF, 0xC4, 0x00, 0x1F, 0x01, 0x00,
|
||||
0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03,
|
||||
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0xFF, 0xC4, 0x00, 0xB5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04,
|
||||
0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31,
|
||||
0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xA1, 0xB1, 0xC1, 0x09,
|
||||
0x23, 0x33, 0x52, 0xF0, 0x15, 0x62, 0x72, 0xD1, 0x0A, 0x16, 0x24, 0x34, 0xE1, 0x25, 0xF1, 0x17, 0x18, 0x19, 0x1A,
|
||||
0x26, 0x27, 0x28, 0x29, 0x2A, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A,
|
||||
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x73, 0x74, 0x75,
|
||||
0x76, 0x77, 0x78, 0x79, 0x7A, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x92, 0x93, 0x94, 0x95, 0x96,
|
||||
0x97, 0x98, 0x99, 0x9A, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7,
|
||||
0xB8, 0xB9, 0xBA, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8,
|
||||
0xD9, 0xDA, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9,
|
||||
0xFA, 0xFF, 0xDA, 0x00, 0x0C, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3F, 0x00, 0xFC, 0xAA, 0xB8, 0xB7,
|
||||
0xD3, 0xB5, 0xD8, 0x1E, 0xD6, 0xD1, 0xA5, 0x5B, 0x4B, 0x46, 0x10, 0xC1, 0x06, 0xA1, 0x29, 0x69, 0x25, 0xB7, 0xC2,
|
||||
0xC6, 0x0C, 0x71, 0x26, 0x5E, 0x50, 0x15, 0x5D, 0xB6, 0x9C, 0x9E, 0x00, 0xC1, 0x67, 0x67, 0x4D, 0xCF, 0x84, 0xF7,
|
||||
0x57, 0xA6, 0xDA, 0x08, 0x52, 0xF5, 0xE5, 0xBA, 0xB8, 0x7B, 0x5B, 0x68, 0xEE, 0x2F, 0x66, 0x89, 0xB7, 0xB7, 0x9B,
|
||||
0xE5, 0x9C, 0x23, 0xAA, 0xFE, 0xF7, 0x68, 0x11, 0x9E, 0x5D, 0x18, 0x12, 0x48, 0x56, 0x5C, 0x9F, 0x57, 0xB6, 0xF8,
|
||||
0x33, 0xF0, 0xE2, 0x5B, 0xFB, 0xCF, 0xF4, 0x51, 0x0C, 0xC6, 0x64, 0x30, 0x49, 0x71, 0x7C, 0xE2, 0x16, 0x90, 0x2E,
|
||||
0xF4, 0x72, 0xEA, 0xE0, 0x34, 0x81, 0x77, 0x93, 0x9C, 0x07, 0xC2, 0x93, 0xB9, 0x61, 0xC0, 0xD3, 0xF0, 0x17, 0xC1,
|
||||
0xBF, 0x03, 0xCD, 0xE1, 0xAD, 0x42, 0x3F, 0x0A, 0xD8, 0xC7, 0x2D, 0xAC, 0x36, 0xCB, 0xF6, 0x79, 0x22, 0x69, 0x84,
|
||||
0xC4, 0x93, 0x29, 0x2A, 0x89, 0xB9, 0x00, 0x50, 0x62, 0x7C, 0xFF, 0x00, 0x16, 0xD9, 0x65, 0x00, 0x16, 0x65, 0x07,
|
||||
0xFA, 0x5B, 0x36, 0xE2, 0xAC, 0xBF, 0x2D, 0x55, 0x79, 0x29, 0xB5, 0x26, 0x9B, 0xA9, 0x18, 0xB5, 0x15, 0x36, 0x9B,
|
||||
0x53, 0xD1, 0xCA, 0x10, 0x93, 0x69, 0xB6, 0x94, 0x27, 0x16, 0xAF, 0x19, 0x29, 0xB8, 0xBB, 0x3F, 0x9A, 0xE1, 0x1E,
|
||||
0x2A, 0xCB, 0x9E, 0x79, 0x4A, 0x92, 0x84, 0xDA, 0xD6, 0xED, 0xA4, 0xED, 0x7A, 0x72, 0xB5, 0xAC, 0xF6, 0xEB, 0xAD,
|
||||
0xD5, 0x97, 0x91, 0xC9, 0x4D, 0xA4, 0xE9, 0xD7, 0x56, 0x45, 0x66, 0xD3, 0x9D, 0x85, 0xB1, 0x86, 0x59, 0x7E, 0xD9,
|
||||
0x7E, 0xA0, 0xE0, 0xA8, 0x02, 0x17, 0x02, 0x3C, 0x36, 0x59, 0x18, 0xE2, 0x42, 0x1D, 0x40, 0x56, 0x0A, 0x77, 0x30,
|
||||
0xAE, 0xFA, 0x56, 0xD4, 0xA4, 0x8A, 0xD9, 0xAD, 0xD6, 0x38, 0x1D, 0x7F, 0x77, 0x77, 0x1C, 0xB2, 0x4C, 0x22, 0x8C,
|
||||
0x2B, 0xF3, 0x14, 0x64, 0x45, 0x85, 0x52, 0x47, 0x92, 0x7E, 0xF2, 0x0F, 0x34, 0x1D, 0xBC, 0x56, 0xDE, 0x85, 0xF0,
|
||||
0x4B, 0xE1, 0x0A, 0xE9, 0xB0, 0xDB, 0x2D, 0x8C, 0x4C, 0xCD, 0x74, 0xF1, 0x42, 0x96, 0x93, 0xCF, 0xF6, 0x54, 0x0C,
|
||||
0xB3, 0x44, 0x03, 0xAA, 0x6C, 0x49, 0x10, 0x87, 0x00, 0x88, 0xC2, 0xEE, 0x66, 0x6F, 0x9B, 0xAB, 0x57, 0xA4, 0x68,
|
||||
0xDF, 0x0A, 0x3E, 0x1B, 0xDA, 0xE9, 0xDF, 0xD9, 0x96, 0xFE, 0x03, 0xB2, 0x8E, 0xD9, 0x8A, 0xDD, 0xC1, 0x77, 0x14,
|
||||
0x97, 0x50, 0x2E, 0xE3, 0x20, 0x62, 0xA0, 0x33, 0x0C, 0xC6, 0x58, 0x48, 0x36, 0x6D, 0xC1, 0x20, 0xA2, 0x2B, 0x00,
|
||||
0xB5, 0xF1, 0x99, 0xCF, 0x19, 0x65, 0x79, 0xAD, 0x0C, 0x3C, 0x2B, 0x61, 0xF9, 0x28, 0xD6, 0x52, 0x97, 0x35, 0x95,
|
||||
0xD4, 0x61, 0x28, 0x5B, 0x99, 0xB8, 0xA9, 0xA9, 0x47, 0xD9, 0x3A, 0x69, 0xA9, 0xC6, 0x51, 0x52, 0x6A, 0x0D, 0x4A,
|
||||
0x74, 0xA9, 0xC3, 0xFA, 0x7F, 0xC3, 0xFE, 0x25, 0xC1, 0x4E, 0xAE, 0x26, 0x2E, 0x13, 0x71, 0x5C, 0x8D, 0xFB, 0xB1,
|
||||
0xD6, 0xEA, 0x6F, 0xF9, 0xAF, 0xBE, 0xB7, 0x4B, 0x55, 0x1D, 0x75, 0x4C, 0xF3, 0xDF, 0x05, 0xD9, 0xF8, 0x7B, 0xC3,
|
||||
0xBA, 0xB5, 0xF1, 0xBC, 0x8D, 0x63, 0xDF, 0x1C, 0x32, 0x88, 0x1A, 0xF3, 0x74, 0x73, 0x89, 0x15, 0xC4, 0x8A, 0x25,
|
||||
0x66, 0xDC, 0xAA, 0x98, 0x60, 0x06, 0x7F, 0xBD, 0xF2, 0x81, 0xF3, 0x36, 0xFE, 0x9D, 0xAF, 0x78, 0x8F, 0x54, 0xD3,
|
||||
0xAD, 0xA6, 0x9E, 0xC5, 0xE5, 0x9C, 0x96, 0xDC, 0xB7, 0xE8, 0x84, 0x47, 0xFB, 0xB4, 0x54, 0x42, 0x0F, 0x03, 0xF8,
|
||||
0xD7, 0xE5, 0x39, 0x60, 0xF8, 0x04, 0x79, 0x7B, 0xE3, 0xF4, 0x9F, 0x85, 0x5F, 0x08, 0xBE, 0x0D, 0x2C, 0x57, 0xCE,
|
||||
0x6C, 0xA5, 0x2B, 0xA8, 0x08, 0x20, 0x82, 0x04, 0xB9, 0xB8, 0x82, 0x43, 0x28, 0x72, 0x55, 0xD5, 0x83, 0xB9, 0x05,
|
||||
0xC2, 0x44, 0x55, 0x77, 0x8C, 0xFC, 0xEC, 0xD8, 0xCB, 0xE7, 0xBE, 0x9B, 0xE0, 0x5F, 0xC3, 0x9D, 0x32, 0xD4, 0x45,
|
||||
0x26, 0x8B, 0x2C, 0x93, 0xC0, 0xF3, 0x95, 0x49, 0xEE, 0x2E, 0x0C, 0x26, 0x26, 0x31, 0xC9, 0xF3, 0xED, 0x95, 0x96,
|
||||
0x32, 0xEC, 0x10, 0xE1, 0xB8, 0x60, 0x3A, 0x90, 0x1B, 0x3F, 0x9E, 0x66, 0x58, 0xFC, 0x14, 0x31, 0x15, 0xFD, 0x9C,
|
||||
0x79, 0x57, 0x3A, 0x92, 0x84, 0x1C, 0x3D, 0xC9, 0xCD, 0x27, 0x6D, 0x12, 0xE5, 0xE7, 0xA6, 0xD5, 0x4D, 0x64, 0x93,
|
||||
0xD1, 0x45, 0xC6, 0xAD, 0xE7, 0x2F, 0xE9, 0xEE, 0x17, 0xE3, 0x0C, 0x0A, 0xE1, 0x9C, 0x37, 0x34, 0x67, 0x2B, 0x73,
|
||||
0x2B, 0xB4, 0xBA, 0x49, 0xF4, 0x72, 0x57, 0x51, 0xDD, 0x24, 0x9B, 0x5A, 0x36, 0x9A, 0x49, 0x1E, 0x15, 0xA7, 0x7E,
|
||||
0xCE, 0xF6, 0x89, 0x6A, 0x96, 0x43, 0x55, 0xB5, 0xB3, 0xB6, 0xB8, 0x96, 0x2B, 0x9B, 0x78, 0xA0, 0xB4, 0x9E, 0xE5,
|
||||
0x61, 0x90, 0xE0, 0xC2, 0xEC, 0x61, 0x91, 0x47, 0xCC, 0xDB, 0xF1, 0x80, 0xC0, 0x98, 0xD4, 0xE5, 0xDC, 0x64, 0xEC,
|
||||
0x7C, 0x29, 0xFD, 0x98, 0x6C, 0xAF, 0xEC, 0xF5, 0x76, 0xB7, 0xF1, 0x36, 0x9A, 0x5D, 0x6F, 0x96, 0xE1, 0x74, 0xB5,
|
||||
0xD3, 0x44, 0x91, 0xA8, 0x45, 0x0C, 0xE2, 0x3D, 0xAC, 0x85, 0xF9, 0x3B, 0x5B, 0x19, 0x8A, 0x43, 0x1F, 0xCA, 0x50,
|
||||
0x9D, 0x87, 0xD2, 0x0F, 0xC2, 0x1F, 0x89, 0x4F, 0x6D, 0x04, 0x0F, 0xE1, 0x61, 0xAA, 0xEE, 0xB4, 0x8E, 0x56, 0xFB,
|
||||
0x06, 0xAF, 0x04, 0xF3, 0x40, 0x9B, 0x55, 0x4E, 0xCB, 0x66, 0x91, 0xCB, 0x18, 0xC2, 0x88, 0xB6, 0xB1, 0x65, 0x60,
|
||||
0xA4, 0x6E, 0xDC, 0xA4, 0x9E, 0xAF, 0xE1, 0xBF, 0xC2, 0x2F, 0x89, 0xA9, 0xE1, 0xFB, 0x8D, 0x6E, 0xC2, 0x3B, 0x4B,
|
||||
0x30, 0xD7, 0x60, 0xDA, 0x41, 0x2D, 0xCC, 0x6C, 0x2E, 0x49, 0xF3, 0x1E, 0x42, 0x19, 0x5D, 0x58, 0x3C, 0x8A, 0x81,
|
||||
0x82, 0xB8, 0x0C, 0xC9, 0xF2, 0xE5, 0x8F, 0x03, 0x8F, 0x36, 0xE3, 0x8A, 0x98, 0xCC, 0x14, 0xAB, 0x62, 0x2B, 0xC2,
|
||||
0x34, 0xA2, 0xA5, 0x78, 0x37, 0x42, 0x12, 0x70, 0x8C, 0x62, 0xD2, 0x6D, 0x72, 0x7B, 0xAF, 0x9B, 0xDD, 0xFD, 0xE5,
|
||||
0x4E, 0x79, 0x49, 0x45, 0x73, 0xA9, 0x72, 0x2F, 0xF0, 0x73, 0x83, 0xF8, 0xCE, 0xB4, 0x33, 0xE8, 0x3F, 0xAE, 0x42,
|
||||
0xF7, 0x76, 0xB3, 0x83, 0x57, 0xE5, 0x95, 0xBE, 0xCE, 0x8F, 0x7B, 0xDE, 0xD7, 0x4B, 0x6B, 0x9C, 0x1E, 0x8D, 0xF0,
|
||||
0x56, 0x7B, 0xDD, 0x26, 0x34, 0x5F, 0x1F, 0xDB, 0x9D, 0x37, 0x53, 0xB8, 0x96, 0xED, 0xA0, 0x16, 0xB1, 0xC9, 0x2C,
|
||||
0x8B, 0xF2, 0x26, 0x58, 0x89, 0x37, 0xEE, 0x04, 0xB2, 0x97, 0x54, 0x2D, 0x88, 0xC0, 0x60, 0x0B, 0x03, 0x5E, 0x9B,
|
||||
0x67, 0xFB, 0x36, 0xE9, 0x31, 0x2D, 0x9C, 0x37, 0x9E, 0x32, 0x84, 0x5D, 0x2C, 0x4A, 0xD6, 0xAD, 0x73, 0x63, 0x28,
|
||||
0x36, 0xAA, 0x53, 0xF7, 0x90, 0x29, 0x8E, 0x5C, 0x92, 0xBE, 0x41, 0xDC, 0xC0, 0xB3, 0x2B, 0x46, 0x32, 0x4B, 0x96,
|
||||
0x35, 0xAD, 0x1F, 0xC2, 0xDF, 0x89, 0x17, 0xF6, 0xB6, 0xFA, 0xB5, 0xED, 0xB8, 0xB7, 0x8E, 0x78, 0x66, 0x96, 0x09,
|
||||
0x6C, 0x6E, 0x23, 0x32, 0x28, 0x08, 0xE8, 0xF2, 0xBA, 0xF9, 0x86, 0x48, 0xC0, 0x68, 0xFE, 0x63, 0xC8, 0x23, 0x82,
|
||||
0xCC, 0xA5, 0x4A, 0xFA, 0x66, 0x8F, 0xF0, 0x37, 0xE2, 0x6D, 0xE5, 0xC9, 0x3A, 0x87, 0x86, 0xB4, 0xFB, 0x28, 0xAE,
|
||||
0x61, 0x0F, 0x0B, 0x59, 0xDF, 0xC0, 0xD1, 0xAC, 0xE0, 0x24, 0x41, 0x42, 0xC4, 0x49, 0x1B, 0x10, 0x21, 0x50, 0xC0,
|
||||
0x09, 0x11, 0x8B, 0xEE, 0x6E, 0x71, 0xE2, 0xE2, 0x78, 0xBB, 0x33, 0xFA, 0xC5, 0x1A, 0xB4, 0xEA, 0xC6, 0x74, 0xD2,
|
||||
0x8C, 0x24, 0xA5, 0x2A, 0x3C, 0xB5, 0x2E, 0xB9, 0x9B, 0x71, 0x97, 0x34, 0xE0, 0xAA, 0x54, 0x93, 0x71, 0x56, 0x49,
|
||||
0xB9, 0xC6, 0x4E, 0x9A, 0x9B, 0xD3, 0xFA, 0x7F, 0x81, 0xB8, 0xD6, 0xAC, 0xDD, 0x6F, 0x65, 0x8D, 0x82, 0x77, 0x85,
|
||||
0x95, 0xE1, 0x7D, 0x1C, 0xD2, 0x8B, 0x7A, 0x27, 0xA2, 0xF4, 0xED, 0xEE, 0x9E, 0x67, 0xE0, 0x8F, 0x81, 0x37, 0xBA,
|
||||
0xEF, 0x87, 0x67, 0xBF, 0x8F, 0xC4, 0xD2, 0xDD, 0x4F, 0x05, 0xA8, 0x61, 0x7B, 0x1E, 0x9E, 0xE1, 0x8C, 0xEA, 0xF3,
|
||||
0x18, 0x99, 0xA3, 0xDE, 0x76, 0x66, 0x36, 0x2A, 0x30, 0xFB, 0x9B, 0x0F, 0xC3, 0x1C, 0xA3, 0x76, 0x4B, 0xFB, 0x3B,
|
||||
0xE8, 0xBA, 0x66, 0xBF, 0x0E, 0x93, 0xFD, 0xAB, 0x23, 0xD9, 0xA5, 0xCE, 0x62, 0xF3, 0x34, 0xEB, 0x7F, 0x29, 0x0A,
|
||||
0xA7, 0x04, 0x48, 0xF2, 0x79, 0x82, 0x42, 0x1D, 0x46, 0x4B, 0xA9, 0x1B, 0xD1, 0xBE, 0x52, 0x54, 0xD7, 0xA8, 0xF8,
|
||||
0x33, 0xE1, 0xAF, 0xC4, 0xA8, 0x24, 0xD6, 0x6D, 0xED, 0xBC, 0x0A, 0xD3, 0x5A, 0x5C, 0xC4, 0xDF, 0x64, 0x17, 0x97,
|
||||
0x36, 0xF0, 0x65, 0x52, 0x39, 0xD4, 0x23, 0x22, 0x9D, 0xD8, 0x64, 0x83, 0x04, 0x85, 0x38, 0xEF, 0xB8, 0x49, 0x91,
|
||||
0xBB, 0xFF, 0x00, 0x0A, 0xDF, 0xE3, 0x77, 0x84, 0xAC, 0xD3, 0x4A, 0xBC, 0xF0, 0x3A, 0x5A, 0x5B, 0x4F, 0x72, 0x5E,
|
||||
0xD7, 0x53, 0x8E, 0xE2, 0x12, 0x6D, 0xC6, 0x40, 0x20, 0x47, 0xE6, 0x3E, 0x71, 0x26, 0x70, 0x24, 0xE4, 0xB4, 0x63,
|
||||
0x2A, 0xC0, 0xE0, 0x7E, 0x71, 0x98, 0x71, 0x35, 0x5C, 0x4E, 0x69, 0x5A, 0xBD, 0x7C, 0x7A, 0x9C, 0xE3, 0xC9, 0xCF,
|
||||
0x38, 0x46, 0x8D, 0xA4, 0xDC, 0x63, 0x38, 0x4A, 0xE9, 0xED, 0x5A, 0x4E, 0x4A, 0x6D, 0xF3, 0xCA, 0x2D, 0xB9, 0x4D,
|
||||
0xD3, 0x77, 0x8C, 0x7F, 0xA8, 0xF8, 0x53, 0x8D, 0x31, 0x32, 0xC8, 0xA9, 0xDB, 0x15, 0x04, 0x9F, 0xB4, 0xD2, 0xF4,
|
||||
0xD3, 0xF8, 0xA5, 0x77, 0xCD, 0xB3, 0x49, 0x6C, 0xB4, 0xEA, 0x97, 0x46, 0x78, 0x45, 0xC4, 0x16, 0x5A, 0xDD, 0xA5,
|
||||
0xA6, 0x8D, 0xA7, 0xEA, 0x77, 0x4A, 0x54, 0x47, 0x3C, 0x31, 0x41, 0x20, 0x06, 0x68, 0x00, 0x9A, 0x4F, 0x2A, 0x40,
|
||||
0x5D, 0x5F, 0x69, 0xF3, 0x15, 0xCB, 0x32, 0x96, 0xDC, 0xC0, 0x90, 0xA5, 0xF0, 0xBD, 0xAF, 0xC3, 0x96, 0x83, 0x51,
|
||||
0xB8, 0xD6, 0xA7, 0x48, 0xD2, 0xE8, 0x2B, 0x2C, 0x2D, 0x6B, 0xF6, 0xA4, 0x55, 0x8E, 0x45, 0x5F, 0xBA, 0x54, 0x2A,
|
||||
0x36, 0x77, 0x12, 0x72, 0xE5, 0x9C, 0x14, 0x3B, 0x63, 0x6C, 0x62, 0x8A, 0x2B, 0xDE, 0xCD, 0x70, 0x74, 0xA7, 0x92,
|
||||
0xE2, 0x31, 0x35, 0x2D, 0x29, 0x54, 0xBB, 0x97, 0xBB, 0x18, 0xF3, 0x38, 0x52, 0xA5, 0x38, 0xDF, 0x92, 0x31, 0x7E,
|
||||
0xEB, 0x9B, 0x6A, 0xCD, 0x6B, 0xEF, 0x7C, 0x6E, 0x52, 0x7F, 0xE1, 0xF7, 0x0E, 0x54, 0x96, 0x1B, 0x3D, 0x52, 0x86,
|
||||
0xF0, 0x73, 0x4B, 0xEE, 0x92, 0xD6, 0xD6, 0xBF, 0xC2, 0x8D, 0x9F, 0x0B, 0xDE, 0xEB, 0xF3, 0x69, 0x6B, 0x06, 0x9B,
|
||||
0xA9, 0x5B, 0xE9, 0xF6, 0x6F, 0x76, 0x1E, 0x45, 0x8E, 0xE1, 0x54, 0xC8, 0xCD, 0xF3, 0x28, 0x8B, 0x1C, 0x9E, 0x25,
|
||||
0x55, 0xDA, 0xA5, 0x58, 0x80, 0xCA, 0x46, 0x42, 0xA8, 0xF4, 0x1B, 0xEB, 0xBB, 0xBB, 0x8B, 0x69, 0xAF, 0xA5, 0x8E,
|
||||
0xD0, 0x49, 0x25, 0x9C, 0x37, 0x73, 0xE9, 0xD7, 0xF2, 0x14, 0x8E, 0x55, 0x91, 0x64, 0x90, 0x46, 0xA8, 0xF9, 0x50,
|
||||
0x57, 0x73, 0x2E, 0xE0, 0x55, 0xF6, 0x87, 0x45, 0x20, 0x2E, 0xC7, 0x28, 0xAF, 0xCD, 0x78, 0xA6, 0xA5, 0x5C, 0x26,
|
||||
0x33, 0x01, 0x35, 0x2E, 0x67, 0x52, 0x35, 0xA0, 0xEE, 0x93, 0x56, 0x70, 0x4F, 0x48, 0xDB, 0x95, 0x6B, 0x76, 0xF4,
|
||||
0xB4, 0xA5, 0x27, 0x29, 0x29, 0x35, 0x16, 0xBF, 0xA8, 0x7C, 0x3E, 0x92, 0xF6, 0x98, 0xA8, 0x5B, 0x4B, 0xC5, 0xDB,
|
||||
0x5E, 0x8E, 0x4F, 0x7D, 0xFA, 0xDB, 0x57, 0xB2, 0x56, 0xD6, 0xED, 0xF7, 0x3E, 0x10, 0xB2, 0x82, 0xF6, 0xC2, 0xF2,
|
||||
0xF3, 0x4E, 0xD4, 0xAF, 0xEE, 0x5A, 0xCA, 0xCE, 0xD2, 0xF1, 0xA7, 0xB8, 0x9D, 0xD4, 0x42, 0x8C, 0xF2, 0x3C, 0x8E,
|
||||
0x42, 0x02, 0x24, 0xFD, 0xDA, 0x96, 0x60, 0xAD, 0xB0, 0xC9, 0x0E, 0x00, 0xC8, 0xC8, 0xEA, 0xEE, 0x12, 0x7B, 0x09,
|
||||
0x6C, 0x6E, 0xCE, 0xA2, 0x0B, 0xAA, 0x32, 0xD9, 0xB9, 0x49, 0x66, 0xDF, 0x74, 0xCC, 0x31, 0x1B, 0xE4, 0xE6, 0x63,
|
||||
0xF2, 0x91, 0x95, 0x42, 0x15, 0x8B, 0x7D, 0xE0, 0xAC, 0xB4, 0x51, 0x5F, 0x33, 0x3A, 0xD8, 0x8C, 0x46, 0x7A, 0x9C,
|
||||
0xA6, 0xD7, 0x2C, 0x13, 0x49, 0x25, 0x6E, 0x6A, 0x73, 0xE4, 0x84, 0x9E, 0x9A, 0xC9, 0x29, 0x24, 0x9E, 0xE9, 0x42,
|
||||
0x11, 0x8D, 0xA2, 0x9C, 0x5F, 0xF5, 0x37, 0x0D, 0x36, 0xF8, 0x7E, 0x9A, 0xEC, 0xE5, 0xE5, 0x7E, 0x67, 0x77, 0x7B,
|
||||
0x7A, 0xBF, 0x5E, 0xB7, 0x3F, 0xFF, 0xD9,
|
||||
};
|
||||
|
||||
static void set_loop_time(uint32_t now) { LoopBlockingGuard clock(nullptr, LOG_STR("test"), now); }
|
||||
|
||||
class OnlineImageDownloadEnd : public ::testing::Test {
|
||||
protected:
|
||||
std::unique_ptr<OnlineImage> make_image_(runtime_image::ImageFormat format) {
|
||||
std::unique_ptr<OnlineImage> make_image_(runtime_image::ImageFormat format, uint32_t buffer_size = 256) {
|
||||
auto img = std::make_unique<OnlineImage>("http://example.com/image", 0, 0, format, image::IMAGE_TYPE_RGB,
|
||||
image::TRANSPARENCY_OPAQUE, nullptr, 256);
|
||||
image::TRANSPARENCY_OPAQUE, nullptr, buffer_size);
|
||||
img->set_parent(&this->http_);
|
||||
return img;
|
||||
}
|
||||
@@ -286,4 +584,317 @@ TEST_F(OnlineImageDownloadEnd, JpegBufferAllocationFailureReportsError) {
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// The whole PNG arrives in one read, but the decoder stops after a short time per call: the rest is decoded on
|
||||
// later passes, not reported as data the decoder could not use
|
||||
TEST_F(OnlineImageDownloadEnd, PngDecodedOverSeveralPassesAfterTheDownloadEnds) {
|
||||
for (bool chunked : {false, true}) {
|
||||
this->finished_ = 0;
|
||||
this->errors_ = 0;
|
||||
auto image = this->make_image_(runtime_image::PNG, 4096);
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(PNG_SOLID, PNG_SOLID + sizeof(PNG_SOLID)),
|
||||
sizeof(PNG_SOLID), chunked);
|
||||
this->start_(*image, container);
|
||||
|
||||
int passes = 0;
|
||||
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++)
|
||||
image->loop();
|
||||
|
||||
EXPECT_EQ(this->finished_, 1) << (chunked ? "chunked" : "with Content-Length");
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_GT(passes, 2);
|
||||
EXPECT_EQ(image->get_width(), 800);
|
||||
EXPECT_EQ(image->get_pixel(400, 300).b, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// The JPEG decoded with all of it in memory at once, to compare the downloaded images with
|
||||
static std::vector<uint8_t> reference_jpeg_pixels() {
|
||||
runtime_image::RuntimeImage ref(runtime_image::JPEG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
std::vector<uint8_t> data(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
|
||||
EXPECT_TRUE(ref.begin_decode(data.size()));
|
||||
EXPECT_EQ(ref.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
EXPECT_TRUE(ref.end_decode());
|
||||
return std::vector<uint8_t>(ref.get_data_start(), ref.get_data_start() + ref.get_width_stride() * ref.get_height());
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> image_pixels(OnlineImage &image) {
|
||||
return std::vector<uint8_t>(image.get_data_start(),
|
||||
image.get_data_start() + image.get_width_stride() * image.get_height());
|
||||
}
|
||||
|
||||
// A JPEG of known size is decoded as it arrives, so a buffer smaller than the file only has to hold the headers
|
||||
TEST_F(OnlineImageDownloadEnd, JpegDecodesThroughASmallBuffer) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
// Bytes after the image are never needed. Waiting for the whole file before decoding would read them all.
|
||||
std::vector<uint8_t> body(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
|
||||
body.resize(body.size() + 8192, 0);
|
||||
auto container = std::make_shared<FakeContainer>(body, body.size(), false);
|
||||
container->add_response_header("etag", "\"abc\"");
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_LT(container->get_bytes_read(), body.size()) << "the image was not decoded as it arrived";
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
ASSERT_EQ(image->get_width(), 48);
|
||||
ASSERT_EQ(image->get_height(), 40);
|
||||
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
|
||||
|
||||
// A complete image is cached
|
||||
this->http_.next = std::make_shared<FakeContainer>(std::vector<uint8_t>{}, 0, false);
|
||||
image->update();
|
||||
EXPECT_TRUE(this->http_.sent_header("if-none-match"));
|
||||
}
|
||||
|
||||
// A read() waits for every byte it asks for, so a large buffer is filled a little at a time rather than in one
|
||||
// read that blocks the loop until the whole buffer has arrived
|
||||
TEST_F(OnlineImageDownloadEnd, JpegReadsInSmallPiecesWithALargeBuffer) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 65536);
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 0, false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
|
||||
container->deliver(sizeof(JPEG_48X40));
|
||||
image->loop();
|
||||
}
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_LE(container->largest_read, READ_CHUNK);
|
||||
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
|
||||
}
|
||||
|
||||
// A downloaded PNG is read a little at a time too
|
||||
TEST_F(OnlineImageDownloadEnd, PngReadsInSmallPiecesWithALargeBuffer) {
|
||||
auto image = this->make_image_(runtime_image::PNG, 65536);
|
||||
std::vector<uint8_t> png(PNG_1X1.begin(), PNG_1X1.end());
|
||||
auto container = std::make_shared<FakeContainer>(png, png.size(), false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_LE(container->largest_read, READ_CHUNK);
|
||||
}
|
||||
|
||||
// A slow link: the decoder waits part way through the image for the rest to arrive
|
||||
TEST_F(OnlineImageDownloadEnd, JpegWaitsForASlowDownload) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1024, false);
|
||||
this->start_(*image, container);
|
||||
|
||||
int passes = 0;
|
||||
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++) {
|
||||
image->loop();
|
||||
if (passes == 5) {
|
||||
EXPECT_EQ(this->finished_, 0) << "the image cannot be finished before the rest of it arrives";
|
||||
}
|
||||
container->deliver(40);
|
||||
}
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_GT(passes, 10);
|
||||
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
|
||||
}
|
||||
|
||||
// A download that stops part way through times out rather than waiting for ever
|
||||
TEST_F(OnlineImageDownloadEnd, JpegDownloadThatStopsTimesOut) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1500, false);
|
||||
set_loop_time(1000);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
image->loop();
|
||||
EXPECT_EQ(this->errors_, 0) << "still within the timeout";
|
||||
|
||||
set_loop_time(1000 + this->http_.get_timeout());
|
||||
image->loop();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// Headers that do not fit the download buffer cannot be read as the image arrives, so the whole image is
|
||||
// downloaded first, keeping the bytes already read
|
||||
TEST_F(OnlineImageDownloadEnd, JpegHeadersLargerThanTheBufferDownloadTheWholeImage) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 256);
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
|
||||
sizeof(JPEG_48X40), false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
|
||||
image->loop();
|
||||
container->deliver(256);
|
||||
}
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(container->get_bytes_read(), sizeof(JPEG_48X40));
|
||||
EXPECT_EQ(image_pixels(*image), reference_jpeg_pixels());
|
||||
}
|
||||
|
||||
// The pass that opens the image does not decode any of it
|
||||
TEST_F(OnlineImageDownloadEnd, JpegOpensInAPassOfItsOwn) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
|
||||
sizeof(JPEG_48X40), false);
|
||||
this->start_(*image, container);
|
||||
|
||||
image->loop();
|
||||
EXPECT_EQ(this->finished_ + this->errors_, 0);
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
EXPECT_EQ(this->finished_, 1);
|
||||
}
|
||||
|
||||
// A progressive JPEG is refused while it downloads, without blaming the buffer size
|
||||
TEST_F(OnlineImageDownloadEnd, JpegProgressiveReportsError) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 256);
|
||||
std::vector<uint8_t> body(JPEG_PROGRESSIVE, JPEG_PROGRESSIVE + sizeof(JPEG_PROGRESSIVE));
|
||||
auto container = std::make_shared<FakeContainer>(body, 256, false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 10 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// on_download_started fires once the server has accepted the request, before the first progress report, and not
|
||||
// for a response that has no image to download
|
||||
TEST_F(OnlineImageDownloadEnd, StartedFiresOnlyWhenTheDownloadBegins) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
std::vector<std::string> events;
|
||||
image->add_on_download_started_callback([&events]() { events.emplace_back("started"); });
|
||||
image->add_on_progress_callback([&events](float, size_t) { events.emplace_back("progress"); });
|
||||
this->start_(*image,
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)),
|
||||
sizeof(JPEG_48X40), false));
|
||||
ASSERT_GE(events.size(), 2u);
|
||||
EXPECT_EQ(events[0], "started");
|
||||
EXPECT_EQ(events[1], "progress");
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
EXPECT_EQ(std::count(events.begin(), events.end(), std::string("started")), 1);
|
||||
|
||||
for (int status : {304, 404}) {
|
||||
events.clear();
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>{}, 0, false);
|
||||
container->status_code = status;
|
||||
this->http_.next = container;
|
||||
image->update();
|
||||
EXPECT_TRUE(events.empty()) << "HTTP " << status;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress starts at 0, is reported at most every 100 ms, and always reaches 100%
|
||||
TEST_F(OnlineImageDownloadEnd, ProgressIsReportedAsTheImageDownloads) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
std::vector<float> reports;
|
||||
image->add_on_progress_callback([&reports](float progress, size_t) { reports.push_back(progress); });
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 100, false);
|
||||
uint32_t now = 1000;
|
||||
set_loop_time(now);
|
||||
this->start_(*image, container);
|
||||
ASSERT_EQ(reports.size(), 1u);
|
||||
EXPECT_EQ(reports[0], 0.0f);
|
||||
|
||||
int passes = 0;
|
||||
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++) {
|
||||
now += 50;
|
||||
set_loop_time(now);
|
||||
container->deliver(100);
|
||||
image->loop();
|
||||
}
|
||||
|
||||
ASSERT_EQ(this->finished_, 1);
|
||||
EXPECT_EQ(reports.back(), 100.0f);
|
||||
EXPECT_TRUE(std::is_sorted(reports.begin(), reports.end()));
|
||||
// A report every 100 ms of a 50 ms loop, plus the start and the end
|
||||
EXPECT_LE(reports.size(), static_cast<size_t>(passes / 2 + 2));
|
||||
EXPECT_GT(reports.size(), 3u);
|
||||
}
|
||||
|
||||
// Without a Content-Length the percentage is unknown, but the bytes read are still reported
|
||||
TEST_F(OnlineImageDownloadEnd, ProgressOfAChunkedDownloadHasNoPercentage) {
|
||||
auto image = this->make_image_(runtime_image::PNG);
|
||||
std::vector<std::pair<float, size_t>> reports;
|
||||
image->add_on_progress_callback(
|
||||
[&reports](float progress, size_t bytes_read) { reports.emplace_back(progress, bytes_read); });
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(PNG_1X1.begin(), PNG_1X1.end()), PNG_1X1.size(), true);
|
||||
set_loop_time(1000);
|
||||
this->start_(*image, container);
|
||||
set_loop_time(2000);
|
||||
for (int i = 0; i < 10 && this->finished_ + this->errors_ == 0; i++)
|
||||
image->loop();
|
||||
|
||||
ASSERT_GE(reports.size(), 2u);
|
||||
EXPECT_TRUE(std::isnan(reports.back().first));
|
||||
EXPECT_EQ(reports.back().second, PNG_1X1.size());
|
||||
}
|
||||
|
||||
// Scan data that cannot be decoded fails the download part way through
|
||||
TEST_F(OnlineImageDownloadEnd, JpegDecodeErrorFailsTheDownload) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
std::vector<uint8_t> body(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40));
|
||||
std::fill(body.begin() + 700, body.end() - 2, 0xFE);
|
||||
auto container = std::make_shared<FakeContainer>(body, 1024, false);
|
||||
this->start_(*image, container);
|
||||
|
||||
for (int i = 0; i < 100 && this->finished_ + this->errors_ == 0; i++) {
|
||||
image->loop();
|
||||
container->deliver(256);
|
||||
}
|
||||
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// A read that fails part way through the download ends it with an error
|
||||
TEST_F(OnlineImageDownloadEnd, JpegReadErrorFailsTheDownload) {
|
||||
auto image = this->make_image_(runtime_image::JPEG, 1024);
|
||||
auto container =
|
||||
std::make_shared<FakeContainer>(std::vector<uint8_t>(JPEG_48X40, JPEG_48X40 + sizeof(JPEG_48X40)), 1024, false);
|
||||
this->start_(*image, container);
|
||||
for (int i = 0; i < 5; i++)
|
||||
image->loop();
|
||||
ASSERT_EQ(this->errors_, 0);
|
||||
|
||||
container->fail_reads();
|
||||
image->loop();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
TEST(DownloadBufferGrow, KeepsTheUnreadData) {
|
||||
DownloadBuffer buffer(16);
|
||||
std::memcpy(buffer.append(), "abcdef", 6);
|
||||
buffer.write(6);
|
||||
buffer.read(2);
|
||||
|
||||
ASSERT_TRUE(buffer.grow(64));
|
||||
EXPECT_EQ(buffer.size(), 64u);
|
||||
ASSERT_EQ(buffer.unread(), 4u);
|
||||
EXPECT_EQ(std::memcmp(buffer.data(), "cdef", 4), 0);
|
||||
|
||||
// A failed allocation leaves the buffer as it was
|
||||
EXPECT_FALSE(buffer.grow(SIZE_MAX / 2));
|
||||
EXPECT_EQ(buffer.size(), 64u);
|
||||
EXPECT_EQ(std::memcmp(buffer.data(), "cdef", 4), 0);
|
||||
// Growing to a smaller size changes nothing
|
||||
EXPECT_TRUE(buffer.grow(8));
|
||||
EXPECT_EQ(buffer.size(), 64u);
|
||||
}
|
||||
|
||||
} // namespace esphome::online_image::testing
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/components/runtime_image/jpeg_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
@@ -362,6 +364,23 @@ static std::vector<uint8_t> pixel_bytes(TestableRuntimeImage &img) {
|
||||
return std::vector<uint8_t>(start, start + img.get_width_stride() * img.get_height());
|
||||
}
|
||||
|
||||
// Shrinking a PNG keeps the first source pixel of each span, as the JPEG path does: the even rows and columns
|
||||
TEST(RuntimeImageDecoder, ShrunkPngKeepsTheFirstPixelOfEachSpan) {
|
||||
TestableRuntimeImage img(PNG, image::IMAGE_TYPE_RGB, false, 2, 2);
|
||||
ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB), PNG));
|
||||
ASSERT_EQ(img.get_width(), 2);
|
||||
ASSERT_EQ(img.get_height(), 2);
|
||||
for (int row = 0; row < 2; row++) {
|
||||
for (int col = 0; col < 2; col++) {
|
||||
const Color pixel = img.get_pixel(col, row);
|
||||
const uint8_t *expected = PNG_RGB_EXPECTED[row * 2][col * 2];
|
||||
EXPECT_EQ(pixel.r, expected[0]) << col << "," << row;
|
||||
EXPECT_EQ(pixel.g, expected[1]) << col << "," << row;
|
||||
EXPECT_EQ(pixel.b, expected[2]) << col << "," << row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
|
||||
TestableRuntimeImage img(JPEG);
|
||||
|
||||
@@ -377,6 +396,12 @@ TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
|
||||
EXPECT_EQ(pixel_bytes(img), first_pixels) << "reused decoder must reproduce identical pixels";
|
||||
}
|
||||
|
||||
// The RGB565 word at (x, y) of a decoded image, in its own byte order
|
||||
static uint16_t rgb565_word(const TestableRuntimeImage &img, int x, int y) {
|
||||
const uint8_t *p = img.get_data_start() + (y * img.get_width() + x) * 2;
|
||||
return p[0] | (p[1] << 8);
|
||||
}
|
||||
|
||||
// JPEGDEC's colour conversion differs by one step between its SIMD and scalar builds, so each
|
||||
// channel is checked within one step; the byte order is still exact
|
||||
static void expect_rgb565_near(uint16_t actual, uint16_t expected, const char *order, size_t pixel) {
|
||||
@@ -386,16 +411,12 @@ static void expect_rgb565_near(uint16_t actual, uint16_t expected, const char *o
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
|
||||
// Halving the 8x8 gradient keeps its even rows and columns. The words are JPEGDEC's output for those
|
||||
// pixels, red in the top five bits: a decode of the fixture with PIL gives (51, 7, 30) for source
|
||||
// pixel (2, 0), which is word 0x3803 here.
|
||||
static const uint16_t EXPECTED[4 * 4] = {
|
||||
0x0000, 0x3803, 0x8008, 0xB80B, 0x0203, 0x3A07, 0x81EC, 0xBA0F,
|
||||
0x03E7, 0x3C0B, 0x83F0, 0xBC13, 0x05EC, 0x3E0F, 0x85F4, 0xBE17,
|
||||
};
|
||||
// Halving the 8x8 gradient makes JPEGDEC decode it at half size, which averages each 2x2 block of pixels
|
||||
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB565, false, 8, 8);
|
||||
TestableRuntimeImage rgb565_little_endian(JPEG, image::IMAGE_TYPE_RGB565, false, 4, 4);
|
||||
TestableRuntimeImage rgb565_big_endian(JPEG, image::IMAGE_TYPE_RGB565, true, 4, 4);
|
||||
|
||||
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_TRUE(decode_all(rgb565_little_endian, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_TRUE(decode_all(rgb565_big_endian, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(rgb565_little_endian.get_width(), 4);
|
||||
@@ -405,9 +426,53 @@ TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
|
||||
|
||||
const uint8_t *little = rgb565_little_endian.get_data_start();
|
||||
const uint8_t *big = rgb565_big_endian.get_data_start();
|
||||
for (size_t i = 0; i < 4 * 4; i++) {
|
||||
expect_rgb565_near(little[i * 2] | (little[i * 2 + 1] << 8), EXPECTED[i], "little-endian", i);
|
||||
expect_rgb565_near((big[i * 2] << 8) | big[i * 2 + 1], EXPECTED[i], "big-endian", i);
|
||||
for (int row = 0; row < 4; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
for (int dy = 0; dy < 2; dy++) {
|
||||
for (int dx = 0; dx < 2; dx++) {
|
||||
const uint16_t word = rgb565_word(full, col * 2 + dx, row * 2 + dy);
|
||||
red += word >> 11;
|
||||
green += (word >> 5) & 0x3F;
|
||||
blue += word & 0x1F;
|
||||
}
|
||||
}
|
||||
const uint16_t expected = ((red + 2) / 4) << 11 | ((green + 2) / 4) << 5 | (blue + 2) / 4;
|
||||
const size_t i = row * 4 + col;
|
||||
expect_rgb565_near(little[i * 2] | (little[i * 2 + 1] << 8), expected, "little-endian", i);
|
||||
expect_rgb565_near((big[i * 2] << 8) | big[i * 2 + 1], expected, "big-endian", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The generic draw() path gets JPEGDEC's half size RGB8888 output, so it too averages each 2x2 block of pixels
|
||||
TEST(RuntimeImageDecoder, JpegDecodesToScaledRgb) {
|
||||
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB, false, 8, 8);
|
||||
TestableRuntimeImage half(JPEG, image::IMAGE_TYPE_RGB, false, 4, 4);
|
||||
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_TRUE(decode_all(half, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(static_cast<JpegDecoder *>(half.decoder())->get_scale(), 2);
|
||||
|
||||
for (int row = 0; row < 4; row++) {
|
||||
for (int col = 0; col < 4; col++) {
|
||||
int red = 0;
|
||||
int green = 0;
|
||||
int blue = 0;
|
||||
for (int dy = 0; dy < 2; dy++) {
|
||||
for (int dx = 0; dx < 2; dx++) {
|
||||
const Color pixel = full.get_pixel(col * 2 + dx, row * 2 + dy);
|
||||
red += pixel.r;
|
||||
green += pixel.g;
|
||||
blue += pixel.b;
|
||||
}
|
||||
}
|
||||
const Color pixel = half.get_pixel(col, row);
|
||||
EXPECT_NEAR(pixel.r, (red + 2) / 4, 8) << "pixel " << col << "," << row << " red";
|
||||
EXPECT_NEAR(pixel.g, (green + 2) / 4, 8) << "pixel " << col << "," << row << " green";
|
||||
EXPECT_NEAR(pixel.b, (blue + 2) / 4, 8) << "pixel " << col << "," << row << " blue";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,10 +480,15 @@ TEST(RuntimeImageDecoder, JpegDecodesDirectlyToScaledRgb565WithBothByteOrders) {
|
||||
// prepare_rgb565_scaling_ rounds: source n covers targets ceil(n * target / source) up to the next start
|
||||
static uint16_t scaled_boundary(int n, int source, int target) { return (n * target + source - 1) / source; }
|
||||
|
||||
// The RGB565 word at (x, y) of a decoded image, in its own byte order
|
||||
static uint16_t rgb565_word(const TestableRuntimeImage &img, int x, int y) {
|
||||
const uint8_t *p = img.get_data_start() + (y * img.get_width() + x) * 2;
|
||||
return p[0] | (p[1] << 8);
|
||||
// JPEGDEC decodes at the smallest of full, 1/2, 1/4 and 1/8 size that still covers the buffer
|
||||
TEST(RuntimeImageDecoder, JpegDecodesAtTheSmallestSizeThatCoversTheBuffer) {
|
||||
for (auto [size, scale] :
|
||||
{std::pair{8, 1}, std::pair{5, 1}, std::pair{4, 2}, std::pair{3, 2}, std::pair{2, 4}, std::pair{1, 8}}) {
|
||||
TestableRuntimeImage img(JPEG, image::IMAGE_TYPE_RGB565, false, size, size);
|
||||
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
EXPECT_EQ(img.get_width(), size);
|
||||
EXPECT_EQ(static_cast<JpegDecoder *>(img.decoder())->get_scale(), scale) << size << "x" << size;
|
||||
}
|
||||
}
|
||||
|
||||
// Every scaled direct decode must equal the unscaled one sampled through the boundary table,
|
||||
@@ -427,7 +497,8 @@ TEST(RuntimeImageDecoder, JpegDirectRgb565ScalesBySourcePixelSpans) {
|
||||
TestableRuntimeImage full(JPEG, image::IMAGE_TYPE_RGB565, false, 8, 8);
|
||||
ASSERT_TRUE(decode_all(full, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
|
||||
for (int size : {4, 6, 16}) {
|
||||
// Sizes JPEGDEC cannot reach by scaling, so the full size decode is spread over the buffer
|
||||
for (int size : {5, 6, 16}) {
|
||||
TestableRuntimeImage scaled(JPEG, image::IMAGE_TYPE_RGB565, false, size, size);
|
||||
ASSERT_TRUE(decode_all(scaled, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(scaled.get_width(), size);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
class GrayscaleImage : public RuntimeImage {
|
||||
public:
|
||||
GrayscaleImage() : RuntimeImage(BMP, image::IMAGE_TYPE_GRAYSCALE, image::TRANSPARENCY_OPAQUE) {}
|
||||
uint8_t pixel(int x) const { return this->buffer_[x]; }
|
||||
};
|
||||
|
||||
// The integer weights stay within one step of the floating point formula they replace, and keep white at 255
|
||||
TEST(GrayscalePixel, MatchesTheFloatingPointWeights) {
|
||||
GrayscaleImage img;
|
||||
ASSERT_GT(img.resize(256, 1), 0);
|
||||
for (int r = 0; r < 256; r += 5) {
|
||||
for (int g = 0; g < 256; g += 5) {
|
||||
for (int b = 0; b < 256; b += 5) {
|
||||
img.draw_pixel(0, 0, Color(r, g, b));
|
||||
const int expected = static_cast<int>(0.2125 * r + 0.7154 * g + 0.0721 * b);
|
||||
ASSERT_LE(std::abs(img.pixel(0) - expected), 1) << "r=" << r << " g=" << g << " b=" << b;
|
||||
}
|
||||
}
|
||||
}
|
||||
img.draw_pixel(1, 0, Color(255, 255, 255));
|
||||
EXPECT_EQ(img.pixel(1), 255);
|
||||
img.draw_pixel(2, 0, Color(0, 0, 0));
|
||||
EXPECT_EQ(img.pixel(2), 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/components/runtime_image/jpeg_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
@@ -178,6 +179,7 @@ class SourceImage : public RuntimeImage {
|
||||
explicit SourceImage(image::ImageType type = image::IMAGE_TYPE_RGB, int width = 0, int height = 0)
|
||||
: RuntimeImage(JPEG, type, image::TRANSPARENCY_OPAQUE, nullptr, false, width, height) {}
|
||||
size_t decoded_bytes() const { return this->decoded_bytes_; }
|
||||
int scale() const { return static_cast<const JpegDecoder *>(this->decoder_.get())->get_scale(); }
|
||||
};
|
||||
|
||||
/// The file in memory; fails reads once fail_at bytes have been read
|
||||
@@ -291,6 +293,23 @@ TEST(JpegSourceDecode, SteppedDirectRgb565MatchesFedDecode) {
|
||||
EXPECT_EQ(pixels(stepped), pixels(fed));
|
||||
}
|
||||
|
||||
// decodeBegin() is given the same reduced scale as decode(), and decodeRows() keeps it for every step
|
||||
TEST(JpegSourceDecode, SteppedScaledDecodeMatchesFedDecode) {
|
||||
for (auto type : {image::IMAGE_TYPE_RGB565, image::IMAGE_TYPE_RGB}) {
|
||||
// 24x20 is half of 48x40, so JPEGDEC decodes at 1/2 size
|
||||
SourceImage fed(type, 24, 20);
|
||||
ASSERT_TRUE(decode_fed(fed));
|
||||
ASSERT_EQ(fed.scale(), 2);
|
||||
|
||||
SourceImage stepped(type, 24, 20);
|
||||
MemorySource source(JPEG_ROWS, sizeof(JPEG_ROWS));
|
||||
int steps;
|
||||
ASSERT_EQ(decode_stepped(stepped, source, steps), DECODE_STEP_DONE);
|
||||
ASSERT_EQ(stepped.scale(), 2);
|
||||
EXPECT_EQ(pixels(stepped), pixels(fed)) << "image type " << type;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JpegSourceDecode, WaitsForDataThatHasNotArrived) {
|
||||
SourceImage fed;
|
||||
ASSERT_TRUE(decode_fed(fed));
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
// 800x600 RGB PNG of a single color (30, 120, 200), 2312 bytes: little data, but many pixels to draw
|
||||
static const uint8_t PNG_SOLID[] = {
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03,
|
||||
0x20, 0x00, 0x00, 0x02, 0x58, 0x08, 0x02, 0x00, 0x00, 0x00, 0x15, 0x14, 0x15, 0x27, 0x00, 0x00, 0x08, 0xCF, 0x49,
|
||||
0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0xD6, 0x31, 0x11, 0x00, 0x30, 0x08, 0x00, 0xB1, 0x52, 0x21, 0xE8, 0x44, 0x22,
|
||||
0xB2, 0x30, 0xC1, 0x31, 0x25, 0x12, 0x7E, 0xFA, 0xC8, 0xEA, 0x07, 0x00, 0xC0, 0x9E, 0x2F, 0x01, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0xDC, 0x19, 0x79, 0xA5, 0x06, 0x0E, 0x33, 0xD4, 0xAF,
|
||||
0xF2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
};
|
||||
|
||||
// One feed_data() call stops after a short time, so a large image is decoded over several calls
|
||||
TEST(PngTimeLimit, LargeImageIsDecodedOverSeveralCalls) {
|
||||
RuntimeImage img(PNG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
std::vector<uint8_t> data(PNG_SOLID, PNG_SOLID + sizeof(PNG_SOLID));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
|
||||
size_t offset = 0;
|
||||
int calls = 0;
|
||||
while (offset < data.size() && calls < 1000) {
|
||||
const int consumed = img.feed_data(data.data() + offset, data.size() - offset);
|
||||
ASSERT_GT(consumed, 0);
|
||||
offset += consumed;
|
||||
calls++;
|
||||
}
|
||||
EXPECT_GT(calls, 1) << "the whole image was decoded in one call";
|
||||
ASSERT_EQ(offset, data.size());
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
|
||||
ASSERT_EQ(img.get_width(), 800);
|
||||
ASSERT_EQ(img.get_height(), 600);
|
||||
for (auto [col, row] : {std::pair{0, 0}, std::pair{400, 300}, std::pair{799, 599}}) {
|
||||
const Color pixel = img.get_pixel(col, row);
|
||||
EXPECT_EQ(pixel.r, 30);
|
||||
EXPECT_EQ(pixel.g, 120);
|
||||
EXPECT_EQ(pixel.b, 200);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
@@ -3,44 +3,8 @@ esphome:
|
||||
then:
|
||||
- rx8111.read_time: rtc
|
||||
- rx8111.write_time: rtc
|
||||
- rx8111.set_alarm:
|
||||
id: rtc
|
||||
second: 0
|
||||
minute: 30
|
||||
hour: 7
|
||||
weekday: 2
|
||||
- rx8111.set_alarm:
|
||||
id: rtc
|
||||
day: 15
|
||||
- rx8111.schedule_alarm_in:
|
||||
id: rtc
|
||||
duration: 2min
|
||||
- rx8111.set_alarm:
|
||||
id: rtc
|
||||
minute: !lambda return uint16_t{15};
|
||||
- rx8111.schedule_alarm_in:
|
||||
id: rtc
|
||||
duration: !lambda return uint32_t{60};
|
||||
- rx8111.clear_alarm_flag: rtc
|
||||
- rx8111.clear_timer_flag: rtc
|
||||
- rx8111.stop_timer: rtc
|
||||
- rx8111.start_timer: rtc
|
||||
- rx8111.unset_alarm: rtc
|
||||
|
||||
time:
|
||||
- platform: rx8111
|
||||
id: rtc
|
||||
i2c_id: i2c_bus
|
||||
alarm_enabled: true
|
||||
alarm_second: 0
|
||||
alarm_minute: 30
|
||||
alarm_hour: 7
|
||||
timer_count: 2
|
||||
timer_clock: 1hz
|
||||
event_poll_interval: 500ms
|
||||
on_alarm:
|
||||
then:
|
||||
- rx8111.clear_alarm_flag: rtc
|
||||
on_timer:
|
||||
then:
|
||||
- rx8111.clear_timer_flag: rtc
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The gtest binary has no YAML switch to emit USE_SWITCH_STORAGE, so define it here.
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
cg.add_define("USE_SWITCH_STORAGE")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -21,13 +21,6 @@ switch:
|
||||
- logger.log: "Switch is now ON"
|
||||
on_turn_off:
|
||||
- logger.log: "Switch is now OFF"
|
||||
- platform: template
|
||||
name: "Flash Storage Switch"
|
||||
id: flash_storage_switch
|
||||
optimistic: true
|
||||
restore_state:
|
||||
state: KEEP
|
||||
storage: flash
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#ifdef USE_HOST
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include "esphome/components/host/preferences.h"
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::switch_::testing {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
/// Switch is abstract; this is the minimum needed to instantiate one.
|
||||
class TestSwitch : public Switch {
|
||||
public:
|
||||
void write_state(bool state) override {}
|
||||
/// The preference key is derived from object_id_hash_; two instances with the same value share stored state.
|
||||
void set_test_id(uint32_t hash) { this->object_id_hash_ = hash; }
|
||||
};
|
||||
|
||||
class SwitchRestoreStorageTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->temp_dir_ = fs::temp_directory_path() / "esphome_switch_storage_test";
|
||||
fs::remove_all(this->temp_dir_);
|
||||
fs::create_directories(this->temp_dir_);
|
||||
setenv("ESPHOME_PREFDIR", this->temp_dir_.c_str(), 1);
|
||||
App.pre_setup("test_switch_storage", strlen("test_switch_storage"), "", 0);
|
||||
host::setup_preferences();
|
||||
// Host preferences also keep an in-memory store, which would leak between tests and GTEST_REPEAT iterations.
|
||||
host::host_preferences->reset();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
unsetenv("ESPHOME_PREFDIR");
|
||||
fs::remove_all(this->temp_dir_);
|
||||
}
|
||||
|
||||
fs::path temp_dir_;
|
||||
};
|
||||
|
||||
// Host ignores flash vs RTC, so this checks that a switch with a storage bit in its restore mode still binds,
|
||||
// saves and restores its preference; which memory it lands in is checked by the ESP32/ESP8266 compile tests.
|
||||
TEST_F(SwitchRestoreStorageTest, SwitchWithAStorageChoiceStillRestoresItsState) {
|
||||
static constexpr uint32_t ID = 0x5001;
|
||||
const auto mode = static_cast<SwitchRestoreMode>(SWITCH_RESTORE_DEFAULT_OFF | RESTORE_MODE_STORAGE_RTC_MASK);
|
||||
{
|
||||
TestSwitch sw;
|
||||
sw.set_test_id(ID);
|
||||
sw.set_restore_mode(mode);
|
||||
auto first = sw.get_initial_state_with_restore_mode();
|
||||
ASSERT_TRUE(first.has_value());
|
||||
EXPECT_FALSE(first.value()) << "nothing stored yet, so RESTORE_DEFAULT_OFF gives OFF";
|
||||
sw.publish_state(true);
|
||||
global_preferences->sync();
|
||||
}
|
||||
TestSwitch sw2;
|
||||
sw2.set_test_id(ID);
|
||||
sw2.set_restore_mode(mode);
|
||||
auto restored = sw2.get_initial_state_with_restore_mode();
|
||||
ASSERT_TRUE(restored.has_value());
|
||||
EXPECT_TRUE(restored.value()) << "the saved state was not restored";
|
||||
}
|
||||
|
||||
} // namespace esphome::switch_::testing
|
||||
#endif // USE_HOST
|
||||
@@ -1,49 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::switch_::testing {
|
||||
|
||||
class RecordingSwitch : public Switch {
|
||||
public:
|
||||
std::vector<bool> writes;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override {
|
||||
this->writes.push_back(state);
|
||||
this->publish_state(state);
|
||||
}
|
||||
};
|
||||
|
||||
TEST(SwitchRestore, DrivesTheRestoreModeStateThroughInversion) {
|
||||
RecordingSwitch on, off, inverted;
|
||||
on.set_restore_mode(SWITCH_ALWAYS_ON);
|
||||
off.set_restore_mode(SWITCH_ALWAYS_OFF);
|
||||
inverted.set_restore_mode(SWITCH_ALWAYS_ON);
|
||||
inverted.set_inverted(true);
|
||||
on.restore();
|
||||
off.restore();
|
||||
inverted.restore();
|
||||
EXPECT_EQ(on.writes, std::vector<bool>{true});
|
||||
EXPECT_EQ(off.writes, std::vector<bool>{false});
|
||||
EXPECT_EQ(inverted.writes, std::vector<bool>{false}); // the hardware level
|
||||
EXPECT_TRUE(on.state);
|
||||
EXPECT_FALSE(off.state);
|
||||
EXPECT_TRUE(inverted.state);
|
||||
}
|
||||
|
||||
TEST(SwitchRestore, DefaultAppliesOnlyWhenTheModeIsDisabled) {
|
||||
RecordingSwitch disabled, on;
|
||||
disabled.set_restore_mode(SWITCH_RESTORE_DISABLED);
|
||||
on.set_restore_mode(SWITCH_ALWAYS_ON);
|
||||
disabled.restore();
|
||||
EXPECT_TRUE(disabled.writes.empty());
|
||||
disabled.restore(true);
|
||||
on.restore(false);
|
||||
EXPECT_EQ(disabled.writes, std::vector<bool>{true});
|
||||
EXPECT_EQ(on.writes, std::vector<bool>{true});
|
||||
}
|
||||
|
||||
} // namespace esphome::switch_::testing
|
||||
@@ -1,8 +0,0 @@
|
||||
switch:
|
||||
- platform: template
|
||||
name: "RTC Storage Switch"
|
||||
id: rtc_storage_switch
|
||||
optimistic: true
|
||||
initial_state: ON
|
||||
restore_state:
|
||||
storage: rtc
|
||||
@@ -1,3 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rtc_storage: !include rtc_storage.yaml
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rtc_storage: !include rtc_storage.yaml
|
||||
|
||||
@@ -100,14 +100,6 @@ sensor:
|
||||
name: pcoup
|
||||
tag_name: PCOUP
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: ntarf
|
||||
tag_name: NTARF
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: pejp
|
||||
tag_name: PEJP
|
||||
teleinfo_id: test_teleinfo
|
||||
# Explicit override - user-specified unit should not be overwritten
|
||||
- platform: teleinfo
|
||||
name: hchp_override
|
||||
@@ -125,7 +117,3 @@ text_sensor:
|
||||
name: optarif
|
||||
tag_name: OPTARIF
|
||||
teleinfo_id: test_teleinfo
|
||||
- platform: teleinfo
|
||||
name: ltarf
|
||||
tag_name: LTARF
|
||||
teleinfo_id: test_teleinfo
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#include "esphome/components/unicode/unicode.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::unicode::testing {
|
||||
|
||||
struct Decoded {
|
||||
uint32_t code_point;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
// Decodes the first sequence of text from an allocation of exactly its size, so a read past the
|
||||
// terminator is a heap overflow the sanitizer reports instead of a silent pass
|
||||
static Decoded decode(const char *text) {
|
||||
const size_t size = std::strlen(text) + 1;
|
||||
auto buffer = std::make_unique<char[]>(size);
|
||||
std::memcpy(buffer.get(), text, size);
|
||||
Decoded result{};
|
||||
result.code_point = extract_unicode_codepoint(buffer.get(), &result.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST(Unicode, DecodesEachSequenceLength) {
|
||||
EXPECT_EQ(decode("A").code_point, 0x41u);
|
||||
EXPECT_EQ(decode("A").length, 1u);
|
||||
EXPECT_EQ(decode("\xC3\xA9").code_point, 0xE9u); // é
|
||||
EXPECT_EQ(decode("\xC3\xA9").length, 2u);
|
||||
EXPECT_EQ(decode("\xE2\x82\xAC").code_point, 0x20ACu); // €
|
||||
EXPECT_EQ(decode("\xE2\x82\xAC").length, 3u);
|
||||
EXPECT_EQ(decode("\xF0\x9F\x98\x80").code_point, 0x1F600u); // 😀
|
||||
EXPECT_EQ(decode("\xF0\x9F\x98\x80").length, 4u);
|
||||
}
|
||||
|
||||
TEST(Unicode, AcceptsTheEdgeOfEachRange) {
|
||||
// The smallest value of each length, both neighbours of the surrogate range and the last code point
|
||||
EXPECT_EQ(decode("\xC2\x80").code_point, 0x80u);
|
||||
EXPECT_EQ(decode("\xC2\x80").length, 2u);
|
||||
EXPECT_EQ(decode("\xE0\xA0\x80").code_point, 0x800u);
|
||||
EXPECT_EQ(decode("\xE0\xA0\x80").length, 3u);
|
||||
EXPECT_EQ(decode("\xED\x9F\xBF").code_point, 0xD7FFu);
|
||||
EXPECT_EQ(decode("\xEE\x80\x80").code_point, 0xE000u);
|
||||
EXPECT_EQ(decode("\xF0\x90\x80\x80").code_point, 0x10000u);
|
||||
EXPECT_EQ(decode("\xF0\x90\x80\x80").length, 4u);
|
||||
EXPECT_EQ(decode("\xF4\x8F\xBF\xBF").code_point, 0x10FFFFu);
|
||||
EXPECT_EQ(decode("\xF4\x8F\xBF\xBF").length, 4u);
|
||||
}
|
||||
|
||||
TEST(Unicode, EmptyStringGivesNothing) {
|
||||
EXPECT_EQ(decode("").code_point, 0u);
|
||||
EXPECT_EQ(decode("").length, 0u);
|
||||
}
|
||||
|
||||
TEST(Unicode, TruncatedSequenceStopsAtTheTerminator) {
|
||||
for (const char *text : {"\xC3", "\xE2\x82", "\xE2", "\xF0\x9F\x98", "\xF0\x9F", "\xF0"}) {
|
||||
EXPECT_EQ(decode(text).code_point, 0u) << text;
|
||||
EXPECT_EQ(decode(text).length, 0u) << text;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Unicode, RejectsMalformedSequences) {
|
||||
EXPECT_EQ(decode("\x80").length, 0u); // lone continuation byte
|
||||
EXPECT_EQ(decode("\xF8\x88\x80\x80\x80").length, 0u); // 5-byte lead
|
||||
EXPECT_EQ(decode("\xC0\xAF").length, 0u); // overlong '/'
|
||||
EXPECT_EQ(decode("\xE0\x80\xAF").length, 0u); // overlong '/'
|
||||
EXPECT_EQ(decode("\xF0\x80\x80\xAF").length, 0u); // overlong '/'
|
||||
EXPECT_EQ(decode("\xED\xA0\x80").length, 0u); // surrogate U+D800
|
||||
EXPECT_EQ(decode("\xF4\x90\x80\x80").length, 0u); // U+110000, past the last code point
|
||||
EXPECT_EQ(decode("\xF5\x80\x80\x80").length, 0u); // lead byte past U+10FFFF
|
||||
EXPECT_EQ(decode("\xC3\x41").length, 0u); // second byte is not a continuation byte
|
||||
}
|
||||
|
||||
} // namespace esphome::unicode::testing
|
||||
@@ -6,7 +6,6 @@ vnc:
|
||||
reconnect_interval: 10s
|
||||
update_delay: 300ms
|
||||
max_update_delay: 2s
|
||||
pixel_format: rgb565
|
||||
on_connect:
|
||||
- logger.log: VNC connected
|
||||
on_disconnect:
|
||||
@@ -15,7 +14,6 @@ vnc:
|
||||
interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- vnc.refresh: vnc_client
|
||||
- if:
|
||||
condition:
|
||||
vnc.is_connected: vnc_client
|
||||
@@ -26,5 +24,4 @@ interval:
|
||||
vnc.is_connected:
|
||||
id: vnc_client
|
||||
then:
|
||||
- vnc.refresh:
|
||||
id: vnc_client
|
||||
- logger.log: VNC is still connected
|
||||
|
||||
@@ -132,7 +132,7 @@ TEST(VncBackoff, NextSessionStartsClean) {
|
||||
set_now(now_ms() + 100);
|
||||
// Leave a frame half parsed and a refresh pending, then lose the link.
|
||||
s.deliver(Rfb().fbu(1).raw_rect(0, 0, 2, 1, {1, 2}));
|
||||
s.deliver(Rfb().fbu(1).rect(0, 0, 4, 4, 5).u8(0x02));
|
||||
s.deliver(Rfb().fbu(1).rect(0, 0, 4, 4, 0).u8(0x02));
|
||||
ASSERT_TRUE(s.client.in_update());
|
||||
s.close_peer();
|
||||
s.loop();
|
||||
|
||||
@@ -26,7 +26,21 @@ static void expect_block(const FakeDisplay &d, int x, int y, int w, int h, const
|
||||
}
|
||||
}
|
||||
|
||||
class VncDecode : public VncSessionTest {};
|
||||
static void expect_solid(const FakeDisplay &d, int x, int y, int w, int h, uint16_t px, uint8_t bpp) {
|
||||
expect_block(d, x, y, w, h, std::vector<uint16_t>(static_cast<size_t>(w) * h, px), bpp);
|
||||
}
|
||||
|
||||
class VncDecode : public ::testing::Test {
|
||||
protected:
|
||||
void start(FakeDisplay &display, uint16_t fb_w, uint16_t fb_h) {
|
||||
this->session_ = std::make_unique<Session>(&display);
|
||||
this->session_->start(fb_w, fb_h);
|
||||
// Past the 50 ms request spacing, so the next request goes out as soon as a frame ends.
|
||||
set_now(now_ms() + 100);
|
||||
}
|
||||
Session &s() { return *this->session_; }
|
||||
std::unique_ptr<Session> session_;
|
||||
};
|
||||
|
||||
TEST_F(VncDecode, Raw16bpp) {
|
||||
FakeDisplay d(64, 48);
|
||||
@@ -71,225 +85,6 @@ TEST_F(VncDecode, EmptyUpdateRequestsAgainWithoutRefresh) {
|
||||
EXPECT_EQ(d.updates(), 0);
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileRawTile) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
auto px = pattern(256, 0x0F0F);
|
||||
Rfb f;
|
||||
f.fbu(1).rect(16, 16, 16, 16, HEXTILE).u8(0x01);
|
||||
for (auto p : px) {
|
||||
f.px(p);
|
||||
}
|
||||
s().deliver(f);
|
||||
expect_block(d, 16, 16, 16, 16, px, 2);
|
||||
EXPECT_EQ(d.drawn_count(), 256u);
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileRawTileIgnoresOtherFlags) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
auto px = pattern(4 * 3, 0x7777);
|
||||
Rfb f;
|
||||
// 0x1F: Raw plus every other bit. Only the pixels follow.
|
||||
f.fbu(2).rect(0, 0, 4, 3, HEXTILE).u8(0x1F);
|
||||
for (auto p : px) {
|
||||
f.px(p);
|
||||
}
|
||||
f.raw_rect(10, 10, 1, 1, {0xABCD});
|
||||
s().deliver(f);
|
||||
expect_block(d, 0, 0, 4, 3, px, 2);
|
||||
EXPECT_EQ(d.pixel(10, 10), expect_px(0xABCD, 2));
|
||||
EXPECT_TRUE(s().client.is_connected());
|
||||
EXPECT_EQ(hex(s().take()), hex(fbur(true, 64, 48)));
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileBackgroundOnly) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
s().deliver(Rfb().fbu(1).rect(0, 0, 16, 16, HEXTILE).u8(0x02).px(0x1234));
|
||||
EXPECT_TRUE(solid(d, 0, 0, 16, 16, 0x1234));
|
||||
EXPECT_EQ(d.drawn_count(), 256u);
|
||||
}
|
||||
|
||||
TEST(VncDecodeBigEndian, HextileTilesKeepTheServerByteOrder) {
|
||||
FakeDisplay d(64, 48);
|
||||
Session s(&d, 1000, true);
|
||||
s.start(64, 48);
|
||||
set_now(now_ms() + 100);
|
||||
s.deliver(
|
||||
Rfb().fbu(2).rect(0, 0, 16, 16, HEXTILE).u8(0x02).u16(0x1234).rect(16, 0, 1, 1, HEXTILE).u8(0x01).u16(0xF800));
|
||||
EXPECT_TRUE(solid(d, 0, 0, 16, 16, 0x1234));
|
||||
EXPECT_EQ(d.pixel(16, 0), expect_px(0xF800, 2));
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileForegroundSubrects) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
// bg 0x1111, fg 0x2222, two subrects: (1,2) 3x4 and (10,0) 6x1.
|
||||
s().deliver(Rfb()
|
||||
.fbu(1)
|
||||
.rect(0, 0, 16, 16, HEXTILE)
|
||||
.u8(0x02 | 0x04 | 0x08)
|
||||
.px(0x1111)
|
||||
.px(0x2222)
|
||||
.u8(2)
|
||||
.u8(0x12)
|
||||
.u8(0x23)
|
||||
.u8(0xA0)
|
||||
.u8(0x50));
|
||||
for (int y = 0; y < 16; y++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
const bool fg = (x >= 1 && x < 4 && y >= 2 && y < 6) || (x >= 10 && y == 0);
|
||||
ASSERT_EQ(d.pixel(x, y), expect_px(fg ? 0x2222 : 0x1111, 2)) << x << "," << y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileColouredSubrects) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
s().deliver(Rfb()
|
||||
.fbu(1)
|
||||
.rect(16, 0, 16, 16, HEXTILE)
|
||||
.u8(0x02 | 0x08 | 0x10)
|
||||
.px(0x0001)
|
||||
.u8(2)
|
||||
.px(0xF800)
|
||||
.u8(0x00)
|
||||
.u8(0x11)
|
||||
.px(0x07E0)
|
||||
.u8(0xFF)
|
||||
.u8(0x00));
|
||||
EXPECT_TRUE(solid(d, 16, 0, 2, 2, 0xF800));
|
||||
EXPECT_EQ(d.pixel(31, 15), expect_px(0x07E0, 2));
|
||||
EXPECT_EQ(d.pixel(18, 0), expect_px(0x0001, 2));
|
||||
EXPECT_EQ(d.pixel(30, 15), expect_px(0x0001, 2));
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileCarriesColoursBetweenTiles) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
s().deliver(Rfb()
|
||||
.fbu(1)
|
||||
.rect(0, 0, 48, 16, HEXTILE)
|
||||
// Tile 1: bg A, fg B, one subrect.
|
||||
.u8(0x02 | 0x04 | 0x08)
|
||||
.px(0xAAAA)
|
||||
.px(0xBBBB)
|
||||
.u8(1)
|
||||
.u8(0x00)
|
||||
.u8(0x00)
|
||||
// Tile 2: nothing specified, so solid A.
|
||||
.u8(0x00)
|
||||
// Tile 3: subrects in the carried fg B on the carried bg A.
|
||||
.u8(0x08)
|
||||
.u8(1)
|
||||
.u8(0xFF)
|
||||
.u8(0x00));
|
||||
EXPECT_EQ(d.pixel(0, 0), expect_px(0xBBBB, 2));
|
||||
EXPECT_TRUE(solid(d, 1, 1, 15, 15, 0xAAAA));
|
||||
EXPECT_TRUE(solid(d, 16, 0, 16, 16, 0xAAAA));
|
||||
EXPECT_EQ(d.pixel(47, 15), expect_px(0xBBBB, 2));
|
||||
EXPECT_TRUE(solid(d, 32, 0, 15, 15, 0xAAAA));
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileCarriesColoursAcrossRawTile) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
auto raw = pattern(256, 0x0400);
|
||||
Rfb f;
|
||||
f.fbu(1).rect(0, 16, 48, 16, HEXTILE).u8(0x02 | 0x04).px(0xCCCC).px(0xDDDD).u8(0x01);
|
||||
for (auto p : raw) {
|
||||
f.px(p);
|
||||
}
|
||||
f.u8(0x08).u8(1).u8(0x00).u8(0x00);
|
||||
s().deliver(f);
|
||||
EXPECT_TRUE(solid(d, 0, 16, 16, 16, 0xCCCC));
|
||||
expect_block(d, 16, 16, 16, 16, raw, 2);
|
||||
EXPECT_EQ(d.pixel(32, 16), expect_px(0xDDDD, 2));
|
||||
EXPECT_EQ(d.pixel(33, 16), expect_px(0xCCCC, 2));
|
||||
EXPECT_EQ(d.pixel(47, 31), expect_px(0xCCCC, 2));
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileSingleEdgeTile) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
auto px = pattern(13 * 7, 0x3000);
|
||||
Rfb f;
|
||||
f.fbu(1).rect(5, 3, 13, 7, HEXTILE).u8(0x01);
|
||||
for (auto p : px) {
|
||||
f.px(p);
|
||||
}
|
||||
s().deliver(f);
|
||||
expect_block(d, 5, 3, 13, 7, px, 2);
|
||||
EXPECT_EQ(d.drawn_count(), 13u * 7u);
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileTileGrid) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
// 37x21 is 3x2 tiles: 16, 16, 5 wide and 16, 5 tall.
|
||||
Rfb f;
|
||||
f.fbu(1).rect(2, 1, 37, 21, HEXTILE);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
f.u8(0x02).px(0x1000 + i);
|
||||
}
|
||||
s().deliver(f);
|
||||
const int xs[] = {2, 18, 34};
|
||||
const int ws[] = {16, 16, 5};
|
||||
const int ys[] = {1, 17};
|
||||
const int hs[] = {16, 5};
|
||||
for (int row = 0; row < 2; row++) {
|
||||
for (int col = 0; col < 3; col++) {
|
||||
EXPECT_TRUE(solid(d, xs[col], ys[row], ws[col], hs[row], 0x1000 + row * 3 + col));
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(d.drawn_count(), 37u * 21u);
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, HextileSubrectClippedToTile) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
// A 5x5 edge tile with a subrect at 3,3 claiming 16x16, and one starting outside the tile.
|
||||
s().deliver(Rfb()
|
||||
.fbu(1)
|
||||
.rect(59, 43, 5, 5, HEXTILE)
|
||||
.u8(0x02 | 0x04 | 0x08)
|
||||
.px(0x0101)
|
||||
.px(0xFFFF)
|
||||
.u8(2)
|
||||
.u8(0x33)
|
||||
.u8(0xFF)
|
||||
.u8(0x90)
|
||||
.u8(0x00));
|
||||
for (int y = 0; y < 5; y++) {
|
||||
for (int x = 0; x < 5; x++) {
|
||||
const bool fg = x >= 3 && y >= 3;
|
||||
ASSERT_EQ(d.pixel(59 + x, 43 + y), expect_px(fg ? 0xFFFF : 0x0101, 2)) << x << "," << y;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(d.drawn_count(), 25u);
|
||||
EXPECT_TRUE(s().client.is_connected());
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, Hextile8bpp) {
|
||||
FakeDisplay d(64, 48, display::DISPLAY_TYPE_BINARY);
|
||||
this->start(d, 64, 48);
|
||||
auto raw = pattern(4, 0x00E3);
|
||||
Rfb f;
|
||||
f.fbu(1).rect(0, 0, 18, 2, HEXTILE).u8(0x02 | 0x04 | 0x08).px(0x00, 1).px(0xFF, 1).u8(1).u8(0x10).u8(0x00).u8(0x01);
|
||||
for (auto p : raw) {
|
||||
f.px(p & 0xFF, 1);
|
||||
}
|
||||
s().deliver(f);
|
||||
EXPECT_EQ(d.pixel(0, 0), expect_px(0x00, 1));
|
||||
EXPECT_EQ(d.pixel(1, 0), expect_px(0xFF, 1));
|
||||
EXPECT_EQ(d.pixel(1, 1), expect_px(0x00, 1));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
EXPECT_EQ(d.pixel(16 + i % 2, i / 2), expect_px(raw[i] & 0xFF, 1));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, RichCursorIsSkippedAndNeverRefreshes) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
@@ -366,15 +161,9 @@ TEST(VncDecodeClip, ServerLargerThanDisplay) {
|
||||
auto raw = pattern(90 * 70, 0x0021);
|
||||
Rfb f;
|
||||
f.fbu(3).raw_rect(5, 5, 90, 70, raw);
|
||||
// A hextile rect straddling both edges, and one entirely off screen.
|
||||
f.rect(56, 40, 20, 20, HEXTILE);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
f.u8(0x02).px(0x5000 + i);
|
||||
}
|
||||
f.rect(70, 50, 16, 16, HEXTILE).u8(0x01);
|
||||
for (auto p : pattern(256, 1)) {
|
||||
f.px(p);
|
||||
}
|
||||
// A raw rect straddling both edges, and one entirely off screen.
|
||||
f.raw_rect(56, 40, 20, 20, std::vector<uint16_t>(20 * 20, 0x5000));
|
||||
f.raw_rect(70, 50, 16, 16, pattern(256, 1));
|
||||
s.deliver(f);
|
||||
EXPECT_EQ(d.out_of_bounds(), 0);
|
||||
for (int y = 5; y < 40; y++) {
|
||||
@@ -382,7 +171,7 @@ TEST(VncDecodeClip, ServerLargerThanDisplay) {
|
||||
ASSERT_EQ(d.pixel(x, y), expect_px(raw[(y - 5) * 90 + (x - 5)], 2)) << x << "," << y;
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(solid(d, 56, 40, 8, 8, 0x5000));
|
||||
expect_solid(d, 56, 40, 8, 8, 0x5000, 2);
|
||||
EXPECT_EQ(d.pixel(4, 4), UNSET);
|
||||
EXPECT_TRUE(s.client.is_connected());
|
||||
EXPECT_EQ(hex(s.take()), hex(fbur(true, 64, 48)));
|
||||
@@ -439,8 +228,8 @@ TEST_P(VncRectOutside, Reconnects) {
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(Rects, VncRectOutside,
|
||||
::testing::Values(OutsideCase{60, 0, 5, 1, RAW}, OutsideCase{0, 47, 1, 2, RAW},
|
||||
OutsideCase{65535, 0, 2, 1, RAW}, OutsideCase{0, 65535, 1, 2, HEXTILE},
|
||||
OutsideCase{64, 0, 1, 1, HEXTILE}, OutsideCase{0, 0, 65, 1, HEXTILE}));
|
||||
OutsideCase{65535, 0, 2, 1, RAW}, OutsideCase{0, 65535, 1, 2, RAW},
|
||||
OutsideCase{64, 0, 1, 1, RAW}, OutsideCase{0, 0, 65, 1, RAW}));
|
||||
|
||||
// --- A frame whose last step needs no bytes must still finish when its last byte arrives.
|
||||
|
||||
@@ -462,43 +251,6 @@ class VncZeroByteEnd : public VncDecode {
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(VncZeroByteEnd, SolidHextileTile) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
Rfb f;
|
||||
f.fbu(1).rect(0, 0, 32, 16, HEXTILE).u8(0x01);
|
||||
for (auto p : pattern(256, 3)) {
|
||||
f.px(p);
|
||||
}
|
||||
f.u8(0x02).px(0x4444);
|
||||
this->deliver_bytewise(f.bytes, f.bytes.size());
|
||||
EXPECT_TRUE(solid(d, 16, 0, 16, 16, 0x4444));
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncZeroByteEnd, CarriedBackgroundTile) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
Rfb f;
|
||||
f.fbu(1).rect(0, 0, 32, 16, HEXTILE).u8(0x02).px(0x5555).u8(0x00);
|
||||
this->deliver_bytewise(f.bytes, f.bytes.size());
|
||||
EXPECT_TRUE(solid(d, 0, 0, 32, 16, 0x5555));
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncZeroByteEnd, TileWithZeroSubrects) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
Rfb f;
|
||||
f.fbu(1).rect(0, 0, 16, 16, HEXTILE).u8(0x02 | 0x08).px(0x6666).u8(0);
|
||||
this->deliver_bytewise(f.bytes, f.bytes.size());
|
||||
EXPECT_TRUE(solid(d, 0, 0, 16, 16, 0x6666));
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncZeroByteEnd, ZeroSizeCursor) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
@@ -533,14 +285,17 @@ TEST_F(VncZeroByteEnd, EmptyCutTextAfterFrame) {
|
||||
EXPECT_EQ(d.pixel(1, 0), expect_px(0x7002, 2));
|
||||
}
|
||||
|
||||
// --- The 20 ms budget: with a slow display, one tile per pass.
|
||||
// --- The 20 ms budget: with a slow display, one row per pass.
|
||||
|
||||
TEST_F(VncDecode, BudgetPausesBetweenTilesAndFinishesOnTheLastOne) {
|
||||
TEST_F(VncDecode, BudgetPausesBetweenRectsAndFinishesOnTheLastOne) {
|
||||
FakeDisplay d(64, 48);
|
||||
this->start(d, 64, 48);
|
||||
d.set_draw_delay_us(25000);
|
||||
Rfb f;
|
||||
f.fbu(1).rect(0, 0, 48, 16, HEXTILE).u8(0x02).px(0x0A0A).u8(0x02).px(0x0B0B).u8(0x02).px(0x0C0C);
|
||||
f.fbu(3)
|
||||
.raw_rect(0, 0, 16, 1, std::vector<uint16_t>(16, 0x0A0A))
|
||||
.raw_rect(16, 0, 16, 1, std::vector<uint16_t>(16, 0x0B0B))
|
||||
.raw_rect(32, 0, 16, 1, std::vector<uint16_t>(16, 0x0C0C));
|
||||
s().send(f);
|
||||
s().loop();
|
||||
EXPECT_EQ(d.draw_calls(), 1);
|
||||
@@ -557,7 +312,7 @@ TEST_F(VncDecode, BudgetPausesBetweenTilesAndFinishesOnTheLastOne) {
|
||||
s().loop(2);
|
||||
EXPECT_FALSE(s().client.rx_pending());
|
||||
EXPECT_FALSE(HighFrequencyLoopRequester::is_high_frequency());
|
||||
EXPECT_TRUE(solid(d, 32, 0, 16, 16, 0x0C0C));
|
||||
expect_solid(d, 32, 0, 16, 1, 0x0C0C, 2);
|
||||
}
|
||||
|
||||
TEST_F(VncDecode, BudgetPausesInsideARawRect) {
|
||||
@@ -638,8 +393,6 @@ static Generated generate(uint32_t seed) {
|
||||
g.handshake_tx = cat({g.handshake_tx, bpp == 2 ? set_pixel_format_565() : set_pixel_format_332(), set_encodings(),
|
||||
fbur(false, g.vis_w, g.vis_h)});
|
||||
|
||||
uint16_t bg = 0;
|
||||
uint16_t fg = 0;
|
||||
const int frames = rnd(3, 8);
|
||||
for (int frame = 0; frame < frames; frame++) {
|
||||
Rfb f;
|
||||
@@ -667,99 +420,19 @@ static Generated generate(uint32_t seed) {
|
||||
if (kind == 9) {
|
||||
const int x = rnd(0, fb_w);
|
||||
const int y = rnd(0, fb_h);
|
||||
f.rect(x, y, rnd(0, 1) ? 0 : rnd(0, fb_w - x), 0, rnd(0, 1) ? RAW : HEXTILE);
|
||||
f.rect(x, y, rnd(0, 1) ? 0 : rnd(0, fb_w - x), 0, RAW);
|
||||
continue;
|
||||
}
|
||||
const int x = rnd(0, fb_w - 1);
|
||||
const int y = rnd(0, fb_h - 1);
|
||||
const int w = rnd(1, std::min(fb_w - x, 48));
|
||||
const int h = rnd(1, std::min(fb_h - y, 40));
|
||||
if (kind < 4) {
|
||||
f.rect(x, y, w, h, RAW);
|
||||
for (int yy = 0; yy < h; yy++) {
|
||||
for (int xx = 0; xx < w; xx++) {
|
||||
uint16_t p = pixel();
|
||||
f.px(p, bpp);
|
||||
g.model.set(x + xx, y + yy, p);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
f.rect(x, y, w, h, HEXTILE);
|
||||
bool bg_valid = false;
|
||||
bool fg_valid = false;
|
||||
for (int ty = y; ty < y + h; ty += 16) {
|
||||
for (int tx = x; tx < x + w; tx += 16) {
|
||||
const int tw = std::min(16, x + w - tx);
|
||||
const int th = std::min(16, y + h - ty);
|
||||
const int choice = rnd(0, 5);
|
||||
if (choice == 0) {
|
||||
f.u8(static_cast<uint8_t>(0x01 | (rnd(0, 15) << 1)));
|
||||
for (int yy = 0; yy < th; yy++) {
|
||||
for (int xx = 0; xx < tw; xx++) {
|
||||
uint16_t p = pixel();
|
||||
f.px(p, bpp);
|
||||
g.model.set(tx + xx, ty + yy, p);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
uint8_t flags = 0;
|
||||
const bool set_bg = !bg_valid || choice == 1 || rnd(0, 3) == 0;
|
||||
const bool subrects = choice >= 3;
|
||||
const bool coloured = choice == 5;
|
||||
const bool set_fg = subrects && !coloured && (!fg_valid || choice == 3);
|
||||
const bool extra_fg = coloured && rnd(0, 1);
|
||||
if (set_bg) {
|
||||
flags |= 0x02;
|
||||
bg = pixel();
|
||||
bg_valid = true;
|
||||
}
|
||||
if (set_fg || extra_fg) {
|
||||
flags |= 0x04;
|
||||
fg = pixel();
|
||||
fg_valid = true;
|
||||
}
|
||||
if (subrects) {
|
||||
flags |= 0x08;
|
||||
}
|
||||
if (coloured) {
|
||||
flags |= 0x10;
|
||||
}
|
||||
f.u8(flags);
|
||||
if (flags & 0x02) {
|
||||
f.px(bg, bpp);
|
||||
}
|
||||
if (flags & 0x04) {
|
||||
f.px(fg, bpp);
|
||||
}
|
||||
std::vector<uint16_t> tile(static_cast<size_t>(tw) * th, bg);
|
||||
if (subrects) {
|
||||
const int count = rnd(0, 6);
|
||||
f.u8(count);
|
||||
for (int i = 0; i < count; i++) {
|
||||
uint16_t colour = fg;
|
||||
if (coloured) {
|
||||
colour = pixel();
|
||||
f.px(colour, bpp);
|
||||
}
|
||||
const int sx = rnd(0, 15);
|
||||
const int sy = rnd(0, 15);
|
||||
const int sw = rnd(1, 16);
|
||||
const int sh = rnd(1, 16);
|
||||
f.u8(static_cast<uint8_t>((sx << 4) | sy)).u8(static_cast<uint8_t>(((sw - 1) << 4) | (sh - 1)));
|
||||
for (int yy = sy; yy < std::min(sy + sh, th); yy++) {
|
||||
for (int xx = sx; xx < std::min(sx + sw, tw); xx++) {
|
||||
tile[yy * tw + xx] = colour;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int yy = 0; yy < th; yy++) {
|
||||
for (int xx = 0; xx < tw; xx++) {
|
||||
g.model.set(tx + xx, ty + yy, tile[yy * tw + xx]);
|
||||
}
|
||||
}
|
||||
f.rect(x, y, w, h, RAW);
|
||||
for (int yy = 0; yy < h; yy++) {
|
||||
for (int xx = 0; xx < w; xx++) {
|
||||
uint16_t p = pixel();
|
||||
f.px(p, bpp);
|
||||
g.model.set(x + xx, y + yy, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ TEST(VncHandshake, Rfb38NonePipelined) {
|
||||
|
||||
struct PixelFormatCase {
|
||||
display::DisplayType type;
|
||||
VncPixelFormat format;
|
||||
bool rgb565;
|
||||
};
|
||||
|
||||
@@ -51,7 +50,6 @@ TEST_P(VncPixelFormatSelection, SetPixelFormatMatchesTheDisplay) {
|
||||
const auto ¶m = GetParam();
|
||||
FakeDisplay display(64, 48, param.type);
|
||||
Session s(&display);
|
||||
s.client.set_pixel_format(param.format);
|
||||
s.attach();
|
||||
s.deliver(handshake_38(64, 48));
|
||||
Bytes tx = s.take();
|
||||
@@ -61,13 +59,10 @@ TEST_P(VncPixelFormatSelection, SetPixelFormatMatchesTheDisplay) {
|
||||
EXPECT_EQ(s.client.bpp(), param.rgb565 ? 2 : 1);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
Formats, VncPixelFormatSelection,
|
||||
::testing::Values(PixelFormatCase{display::DISPLAY_TYPE_COLOR, VncPixelFormat::VNC_PIXEL_FORMAT_AUTO, true},
|
||||
PixelFormatCase{display::DISPLAY_TYPE_GRAYSCALE, VncPixelFormat::VNC_PIXEL_FORMAT_AUTO, true},
|
||||
PixelFormatCase{display::DISPLAY_TYPE_BINARY, VncPixelFormat::VNC_PIXEL_FORMAT_AUTO, false},
|
||||
PixelFormatCase{display::DISPLAY_TYPE_COLOR, VncPixelFormat::VNC_PIXEL_FORMAT_RGB332, false},
|
||||
PixelFormatCase{display::DISPLAY_TYPE_BINARY, VncPixelFormat::VNC_PIXEL_FORMAT_RGB565, true}));
|
||||
INSTANTIATE_TEST_SUITE_P(Formats, VncPixelFormatSelection,
|
||||
::testing::Values(PixelFormatCase{display::DISPLAY_TYPE_COLOR, true},
|
||||
PixelFormatCase{display::DISPLAY_TYPE_GRAYSCALE, true},
|
||||
PixelFormatCase{display::DISPLAY_TYPE_BINARY, false}));
|
||||
|
||||
TEST(VncHandshake, Rfb37NoneHasNoSecurityResult) {
|
||||
FakeDisplay display(64, 48);
|
||||
@@ -254,8 +249,6 @@ TEST(VncHandshake, DumpConfig) {
|
||||
FakeDisplay display(64, 48);
|
||||
Session s(&display);
|
||||
s.client.dump_config();
|
||||
s.client.set_pixel_format(VncPixelFormat::VNC_PIXEL_FORMAT_RGB332);
|
||||
s.client.dump_config();
|
||||
}
|
||||
|
||||
TEST(VncHandshake, HandshakeTimeout) {
|
||||
|
||||
@@ -9,7 +9,6 @@ static Bytes frame(uint16_t x = 0, uint16_t pixel = 0x1234) {
|
||||
}
|
||||
|
||||
static const Bytes INCREMENTAL = fbur(true, 64, 48);
|
||||
static const Bytes FULL = fbur(false, 64, 48);
|
||||
|
||||
// The session starts at t=1000, when the first (full) request goes out.
|
||||
class VncPacing : public ::testing::Test {
|
||||
@@ -149,40 +148,6 @@ TEST_F(VncPacing, CursorOnlyFrameNeverRefreshes) {
|
||||
EXPECT_EQ(d_.updates(), 0);
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, RefreshSendsAFullRequestEvenWithOneOutstanding) {
|
||||
set_now(1100);
|
||||
s().client.refresh();
|
||||
s().loop();
|
||||
EXPECT_EQ(hex(s().take()), hex(FULL));
|
||||
s().run_for(1000);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, RefreshKeepsThe50msFloor) {
|
||||
set_now(1100);
|
||||
s().deliver(frame());
|
||||
EXPECT_EQ(hex(s().take()), hex(INCREMENTAL));
|
||||
s().client.refresh();
|
||||
s().at(1149);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
s().at(1150);
|
||||
EXPECT_EQ(hex(s().take()), hex(FULL));
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, RefreshDuringAFrameWaitsForItsEnd) {
|
||||
Bytes f = Rfb().fbu(1).raw_rect(0, 0, 4, 1, {1, 2, 3, 4}).bytes;
|
||||
set_now(1100);
|
||||
s().deliver(Bytes(f.begin(), f.end() - 2));
|
||||
s().client.refresh();
|
||||
s().run_for(500);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
s().deliver(Bytes(f.end() - 2, f.end()));
|
||||
// Only the full request: the incremental one it replaces is not sent as well.
|
||||
EXPECT_EQ(hex(s().take()), hex(FULL));
|
||||
s().run_for(500);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(VncPacing, HeartbeatAfterAMinuteOfSilence) {
|
||||
set_now(1100);
|
||||
s().deliver(frame());
|
||||
@@ -268,22 +233,6 @@ TEST_F(VncPacingParked, BusyDisplayHoldsDrawsAndRequests) {
|
||||
EXPECT_EQ(d_.updates(), 2);
|
||||
}
|
||||
|
||||
// The pass that starts a refresh already sees the panel busy, so it sends no request while the refresh runs.
|
||||
TEST_F(VncPacingParked, NoRequestInThePassThatStartsARefresh) {
|
||||
set_now(1100);
|
||||
s().deliver(frame());
|
||||
EXPECT_EQ(hex(s().take()), hex(INCREMENTAL));
|
||||
// vnc.refresh from a button handled earlier in the same main loop iteration.
|
||||
s().client.refresh();
|
||||
s().at(1300);
|
||||
ASSERT_EQ(d_.updates(), 1);
|
||||
ASSERT_TRUE(d_.busy());
|
||||
EXPECT_EQ(s().take().size(), 0u) << "request sent while the panel refreshes";
|
||||
d_.finish();
|
||||
s().tick(10);
|
||||
EXPECT_EQ(hex(s().take()), hex(FULL));
|
||||
}
|
||||
|
||||
TEST(VncPacingParkedDelay, NoRequestInTheRefreshPassWithZeroDelay) {
|
||||
FakeParkingDisplay d;
|
||||
Session s(&d);
|
||||
@@ -317,16 +266,16 @@ TEST_F(VncPacingParked, FailedDisplayEndsTheSessionAndStops) {
|
||||
}
|
||||
|
||||
TEST_F(VncPacingParked, NoRequestWhileBusyBetweenFrames) {
|
||||
set_now(1100);
|
||||
// Inside the 50 ms floor, so the next request is still due when the panel turns busy.
|
||||
set_now(1010);
|
||||
s().deliver(Rfb().fbu(0));
|
||||
EXPECT_EQ(hex(s().take()), hex(INCREMENTAL));
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
d_.external_update();
|
||||
s().client.refresh();
|
||||
s().run_for(2000);
|
||||
EXPECT_EQ(s().take().size(), 0u);
|
||||
d_.finish();
|
||||
s().tick(10);
|
||||
EXPECT_EQ(hex(s().take()), hex(FULL));
|
||||
EXPECT_EQ(hex(s().take()), hex(INCREMENTAL));
|
||||
}
|
||||
|
||||
TEST_F(VncPacingParked, ExternalUpdateMidFrameStopsReading) {
|
||||
@@ -361,9 +310,6 @@ template<typename D> static void runs_frames_without_deadlock() {
|
||||
EXPECT_EQ(d.updates(), i + 1) << "frame " << i;
|
||||
EXPECT_EQ(d.pixel(i * 4, 0), expect_px(0x1000 + i, 2));
|
||||
}
|
||||
s.client.refresh();
|
||||
s.tick(100);
|
||||
EXPECT_EQ(hex(s.take()), hex(FULL));
|
||||
}
|
||||
|
||||
TEST(VncPacingDisplays, PlainDisplay) { runs_frames_without_deadlock<FakeDisplay>(); }
|
||||
@@ -422,39 +368,6 @@ TEST_F(VncPacingParked, DisplayBusyTooLongSetsAWarningUntilItParks) {
|
||||
EXPECT_FALSE(s().client.status_has_warning());
|
||||
}
|
||||
|
||||
// A refresh the driver gave up on (its warning is set when it parks) is retried once; a retry that fails
|
||||
// too is not repeated until the driver finishes a refresh cleanly.
|
||||
TEST_F(VncPacingParked, AbandonedRefreshIsRetriedOnce) {
|
||||
set_now(1100);
|
||||
s().deliver(frame());
|
||||
s().at(1300);
|
||||
ASSERT_EQ(d_.updates(), 1);
|
||||
d_.status_set_warning(LOG_STR("Busy pin timeout"));
|
||||
d_.finish();
|
||||
s().tick(10);
|
||||
EXPECT_TRUE(s().client.refresh_pending());
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d_.updates(), 2);
|
||||
d_.finish();
|
||||
s().run_for(3000);
|
||||
EXPECT_EQ(d_.updates(), 2) << "a failed retry was retried again";
|
||||
|
||||
// A clean refresh arms the retry again
|
||||
d_.status_clear_warning();
|
||||
s().deliver(frame(16, 0x4321));
|
||||
s().run_for(300);
|
||||
ASSERT_EQ(d_.updates(), 3);
|
||||
d_.finish();
|
||||
s().tick(10);
|
||||
d_.status_set_warning(LOG_STR("Busy pin timeout"));
|
||||
s().deliver(frame(32, 0x5678));
|
||||
s().run_for(300);
|
||||
ASSERT_EQ(d_.updates(), 4);
|
||||
d_.finish();
|
||||
s().tick(10);
|
||||
EXPECT_TRUE(s().client.refresh_pending());
|
||||
}
|
||||
|
||||
TEST_F(VncPacingParked, DisplayParkingKeepsAConnectionWarning) {
|
||||
d_.external_update();
|
||||
s().run_for(120000, 1000);
|
||||
|
||||
@@ -1,360 +0,0 @@
|
||||
#include "vnc_test_helpers.h"
|
||||
|
||||
#ifdef USE_HOST
|
||||
|
||||
namespace esphome::vnc::testing {
|
||||
|
||||
struct Tile {
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
uint16_t colour;
|
||||
};
|
||||
|
||||
/// One update with a 16x16 hextile rect per tile, each a solid background.
|
||||
static Bytes tiles(std::initializer_list<Tile> list) {
|
||||
Rfb f;
|
||||
f.fbu(static_cast<uint16_t>(list.size()));
|
||||
for (const auto &t : list) {
|
||||
f.rect(t.x, t.y, 16, 16, HEXTILE).u8(0x02).px(t.colour);
|
||||
}
|
||||
return f.bytes;
|
||||
}
|
||||
|
||||
/// One update covering the whole w x h screen with solid tiles.
|
||||
static Bytes screen(uint16_t w, uint16_t h, uint16_t seed) {
|
||||
Rfb f;
|
||||
f.fbu(1).rect(0, 0, w, h, HEXTILE);
|
||||
const int count = ((w + 15) / 16) * ((h + 15) / 16);
|
||||
for (int i = 0; i < count; i++) {
|
||||
f.u8(0x02).px(static_cast<uint16_t>(seed + i * 0x0841));
|
||||
}
|
||||
return f.bytes;
|
||||
}
|
||||
|
||||
static Bytes off_grid(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t colour) {
|
||||
return Rfb().fbu(1).rect(x, y, w, h, HEXTILE).u8(0x02).px(colour).bytes;
|
||||
}
|
||||
|
||||
static int count_draws(Session &s, const FakeDisplay &d, const Bytes &bytes) {
|
||||
const int before = d.draw_calls();
|
||||
s.deliver(bytes);
|
||||
return d.draw_calls() - before;
|
||||
}
|
||||
|
||||
class VncTileCache : public VncSessionTest {
|
||||
protected:
|
||||
void SetUp() override { this->start(this->d_, 64, 48); }
|
||||
int draws(const Bytes &bytes) { return count_draws(s(), this->d_, bytes); }
|
||||
void reconnect() {
|
||||
s().close_peer();
|
||||
s().loop();
|
||||
ASSERT_FALSE(s().client.is_connected());
|
||||
s().start(64, 48);
|
||||
set_now(now_ms() + 100);
|
||||
}
|
||||
FakeDisplay d_;
|
||||
};
|
||||
|
||||
TEST_F(VncTileCache, UnchangedTileIsNotDrawnAgain) {
|
||||
const Bytes f = tiles({{16, 16, 0x1234}});
|
||||
EXPECT_EQ(this->draws(f), 1);
|
||||
s().run_for(300);
|
||||
ASSERT_EQ(d_.updates(), 1);
|
||||
s().take();
|
||||
EXPECT_EQ(this->draws(f), 0);
|
||||
EXPECT_FALSE(s().client.refresh_pending());
|
||||
EXPECT_EQ(hex(s().take()), hex(fbur(true, 64, 48)));
|
||||
s().run_for(2000);
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
EXPECT_TRUE(solid(d_, 16, 16, 16, 16, 0x1234));
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, OnlyChangedTilesAreDrawn) {
|
||||
EXPECT_EQ(this->draws(tiles({{0, 0, 0x1111}, {16, 0, 0x2222}, {32, 0, 0x3333}})), 3);
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(this->draws(tiles({{0, 0, 0x1111}, {16, 0, 0x4444}, {32, 0, 0x3333}})), 1);
|
||||
EXPECT_TRUE(solid(d_, 16, 0, 16, 16, 0x4444));
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d_.updates(), 2);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, SkippedFrameDoesNotPostponeThePendingRefresh) {
|
||||
const Bytes f = tiles({{0, 0, 0x1111}});
|
||||
set_now(1200);
|
||||
ASSERT_EQ(this->draws(f), 1);
|
||||
set_now(1350);
|
||||
EXPECT_EQ(this->draws(f), 0);
|
||||
s().at(1399);
|
||||
EXPECT_EQ(d_.updates(), 0);
|
||||
s().at(1400);
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, SameBytesInAnotherShapeAreDrawn) {
|
||||
const Bytes wide = Rfb().fbu(1).rect(0, 0, 16, 8, HEXTILE).u8(0x02).px(0x1234).bytes;
|
||||
const Bytes tall = Rfb().fbu(1).rect(0, 0, 8, 16, HEXTILE).u8(0x02).px(0x1234).bytes;
|
||||
EXPECT_EQ(this->draws(wide), 1);
|
||||
EXPECT_EQ(this->draws(tall), 1);
|
||||
EXPECT_TRUE(solid(d_, 0, 8, 8, 8, 0x1234));
|
||||
EXPECT_EQ(this->draws(tall), 0);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, RawRectForgetsOnlyTheCellsItCovers) {
|
||||
const Bytes f = tiles({{0, 16, 0x1111}, {16, 16, 0x2222}, {32, 32, 0x3333}});
|
||||
ASSERT_EQ(this->draws(f), 3);
|
||||
// Ends on the last column of the cell at 0,16.
|
||||
s().deliver(Rfb().fbu(1).raw_rect(0, 20, 16, 1, std::vector<uint16_t>(16, 0xF800)));
|
||||
ASSERT_EQ(d_.pixel(15, 20), expect_px(0xF800, 2));
|
||||
EXPECT_EQ(this->draws(f), 1);
|
||||
EXPECT_TRUE(solid(d_, 0, 16, 16, 16, 0x1111));
|
||||
|
||||
s().deliver(Rfb().fbu(1).raw_rect(20, 20, 1, 1, {0xF800}));
|
||||
ASSERT_EQ(d_.pixel(20, 20), expect_px(0xF800, 2));
|
||||
EXPECT_EQ(this->draws(f), 1);
|
||||
EXPECT_TRUE(solid(d_, 16, 16, 16, 16, 0x2222));
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, OffGridTileForgetsTheCellsItCovers) {
|
||||
const Bytes f = tiles({{0, 0, 0x1111}, {16, 0, 0x2222}, {0, 16, 0x3333}, {16, 16, 0x4444}, {32, 0, 0x5555}});
|
||||
ASSERT_EQ(this->draws(f), 5);
|
||||
const Bytes across = off_grid(8, 8, 9, 9, 0xF800);
|
||||
// Never skipped itself.
|
||||
EXPECT_EQ(this->draws(across), 1);
|
||||
EXPECT_EQ(this->draws(across), 1);
|
||||
EXPECT_EQ(this->draws(f), 4);
|
||||
EXPECT_TRUE(solid(d_, 0, 0, 16, 16, 0x1111));
|
||||
EXPECT_TRUE(solid(d_, 16, 16, 16, 16, 0x4444));
|
||||
|
||||
// Ends on the last pixel of the first cell.
|
||||
EXPECT_EQ(this->draws(off_grid(8, 8, 8, 8, 0xF800)), 1);
|
||||
EXPECT_EQ(this->draws(f), 1);
|
||||
EXPECT_TRUE(solid(d_, 0, 0, 16, 16, 0x1111));
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, RefreshForgetsEveryTile) {
|
||||
const Bytes f = tiles({{16, 16, 0x1234}});
|
||||
ASSERT_EQ(this->draws(f), 1);
|
||||
s().run_for(300);
|
||||
ASSERT_EQ(d_.updates(), 1);
|
||||
s().take();
|
||||
s().client.refresh();
|
||||
s().loop();
|
||||
EXPECT_EQ(hex(s().take()), hex(fbur(false, 64, 48)));
|
||||
EXPECT_EQ(this->draws(f), 1);
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d_.updates(), 2);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, RefreshedTilesStayCachedInTheNextSession) {
|
||||
const Bytes full = screen(64, 48, 0x1000);
|
||||
ASSERT_EQ(this->draws(full), 12);
|
||||
s().run_for(300);
|
||||
ASSERT_EQ(d_.updates(), 1);
|
||||
this->reconnect();
|
||||
EXPECT_EQ(this->draws(full), 0);
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, DropBeforeTheRefreshRedrawsInTheNextSession) {
|
||||
const Bytes full = screen(64, 48, 0x1000);
|
||||
ASSERT_EQ(this->draws(full), 12);
|
||||
s().tick(100);
|
||||
ASSERT_TRUE(s().client.refresh_pending());
|
||||
this->reconnect();
|
||||
EXPECT_EQ(d_.updates(), 0);
|
||||
EXPECT_EQ(this->draws(full), 12);
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCache, DropMidFrameRedrawsInTheNextSession) {
|
||||
ASSERT_EQ(this->draws(screen(64, 48, 0x1000)), 12);
|
||||
s().run_for(300);
|
||||
ASSERT_EQ(d_.updates(), 1);
|
||||
const Bytes next = screen(64, 48, 0x2000);
|
||||
// The update and rect headers, then six of the twelve tiles.
|
||||
ASSERT_EQ(this->draws(Bytes(next.begin(), next.begin() + 4 + 12 + 6 * 3)), 6);
|
||||
ASSERT_TRUE(s().client.in_update());
|
||||
ASSERT_FALSE(s().client.refresh_pending());
|
||||
this->reconnect();
|
||||
EXPECT_EQ(this->draws(next), 12);
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d_.updates(), 2);
|
||||
}
|
||||
|
||||
enum class SessionEnd { PEER_CLOSED, PROTOCOL_ERROR };
|
||||
|
||||
class VncTileCacheSessionEnd : public VncTileCache, public ::testing::WithParamInterface<SessionEnd> {};
|
||||
|
||||
TEST_P(VncTileCacheSessionEnd, PixelsNotYetShownAreDrawnAgain) {
|
||||
const Bytes full = screen(64, 48, 0x1000);
|
||||
ASSERT_EQ(this->draws(full), 12);
|
||||
switch (GetParam()) {
|
||||
case SessionEnd::PEER_CLOSED:
|
||||
s().close_peer();
|
||||
s().loop();
|
||||
break;
|
||||
case SessionEnd::PROTOCOL_ERROR:
|
||||
s().deliver(Rfb().u8(7));
|
||||
break;
|
||||
}
|
||||
ASSERT_FALSE(s().client.is_connected());
|
||||
s().start(64, 48);
|
||||
set_now(now_ms() + 100);
|
||||
EXPECT_EQ(this->draws(full), 12);
|
||||
s().run_for(300);
|
||||
EXPECT_EQ(d_.updates(), 1);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(Ends, VncTileCacheSessionEnd,
|
||||
::testing::Values(SessionEnd::PEER_CLOSED, SessionEnd::PROTOCOL_ERROR));
|
||||
|
||||
// Bit 7 of every fourth byte only reached bit 31 of the old word-wise hash, so an even number of flips cancelled.
|
||||
TEST(VncTileCacheHash, Rgb332TilesDifferingInEveryFourthByteAreBothDrawn) {
|
||||
FakeDisplay d(64, 48, display::DISPLAY_TYPE_BINARY);
|
||||
Session s(&d);
|
||||
s.start(64, 48);
|
||||
ASSERT_EQ(s.client.bpp(), 1);
|
||||
set_now(now_ms() + 100);
|
||||
const Bytes a(256, 0x00);
|
||||
Bytes b = a;
|
||||
const int flips[][2] = {{3, 0}, {7, 2}, {11, 4}, {15, 6}};
|
||||
for (const auto &f : flips) {
|
||||
b[f[1] * 16 + f[0]] ^= 0x80;
|
||||
}
|
||||
auto raw_tile = [](const Bytes &px) { return Rfb().fbu(1).rect(16, 16, 16, 16, HEXTILE).u8(0x01).raw(px).bytes; };
|
||||
EXPECT_EQ(count_draws(s, d, raw_tile(a)), 1);
|
||||
EXPECT_EQ(count_draws(s, d, raw_tile(b)), 1);
|
||||
for (const auto &f : flips) {
|
||||
EXPECT_EQ(d.pixel(16 + f[0], 16 + f[1]), expect_px(0x80, 1)) << f[0] << "," << f[1];
|
||||
}
|
||||
EXPECT_EQ(count_draws(s, d, raw_tile(a)), 1);
|
||||
for (const auto &f : flips) {
|
||||
EXPECT_EQ(d.pixel(16 + f[0], 16 + f[1]), expect_px(0x00, 1)) << f[0] << "," << f[1];
|
||||
}
|
||||
}
|
||||
|
||||
// --- The display size can change between sessions; the cache was sized for 64x48 (12 cells) at setup.
|
||||
|
||||
class VncTileCacheLayout : public VncSessionTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->session_ = std::make_unique<Session>(&this->d_);
|
||||
ASSERT_EQ(c().hash_capacity(), 12u);
|
||||
}
|
||||
TestVncClient &c() { return s().client; }
|
||||
int draws(const Bytes &bytes) { return count_draws(s(), this->d_, bytes); }
|
||||
/// Ends any session, resizes the display and starts a session with a server of the same size.
|
||||
void session(int w, int h) {
|
||||
if (c().is_connected()) {
|
||||
s().close_peer();
|
||||
s().loop();
|
||||
}
|
||||
this->d_.resize(w, h);
|
||||
s().start(w, h);
|
||||
set_now(now_ms() + 100);
|
||||
}
|
||||
FakeDisplay d_{64, 48};
|
||||
};
|
||||
|
||||
TEST_F(VncTileCacheLayout, EveryLayoutWithinTheCellsUsesTheCache) {
|
||||
const int sizes[][2] = {{64, 48}, {48, 64}, {96, 32}, {32, 96}, {192, 16}, {16, 192}, {50, 40}};
|
||||
for (const auto &size : sizes) {
|
||||
const int w = size[0];
|
||||
const int h = size[1];
|
||||
this->session(w, h);
|
||||
EXPECT_EQ(c().hash_cols(), (w + 15) / 16) << w << "x" << h;
|
||||
EXPECT_EQ(c().hash_rows(), (h + 15) / 16) << w << "x" << h;
|
||||
const Bytes full = screen(w, h, 0x1000);
|
||||
EXPECT_EQ(this->draws(full), 12) << w << "x" << h;
|
||||
EXPECT_EQ(this->draws(full), 0) << w << "x" << h;
|
||||
s().run_for(300);
|
||||
}
|
||||
EXPECT_EQ(d_.out_of_bounds(), 0);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCacheLayout, RotationForgetsTheTiles) {
|
||||
this->session(64, 48);
|
||||
const Bytes tile = tiles({{16, 0, 0x1234}});
|
||||
ASSERT_EQ(this->draws(tile), 1);
|
||||
s().run_for(300);
|
||||
this->session(48, 64);
|
||||
// Same place, same pixels, but the panel turned underneath them.
|
||||
EXPECT_EQ(this->draws(tile), 1);
|
||||
EXPECT_EQ(this->draws(tile), 0);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCacheLayout, HalfTurnForgetsTheTiles) {
|
||||
this->session(64, 48);
|
||||
const Bytes full = screen(64, 48, 0x1000);
|
||||
ASSERT_EQ(this->draws(full), 12);
|
||||
s().run_for(300);
|
||||
// A half turn keeps the cell grid but puts every tile somewhere else on the panel.
|
||||
d_.set_rotation(display::DISPLAY_ROTATION_180_DEGREES);
|
||||
this->session(64, 48);
|
||||
EXPECT_EQ(this->draws(full), 12);
|
||||
EXPECT_EQ(this->draws(full), 0);
|
||||
}
|
||||
|
||||
TEST_F(VncTileCacheLayout, RotationDuringASessionReconnects) {
|
||||
this->session(64, 48);
|
||||
ASSERT_EQ(this->draws(screen(64, 48, 0x1000)), 12);
|
||||
s().run_for(300);
|
||||
d_.set_rotation(display::DISPLAY_ROTATION_180_DEGREES);
|
||||
s().tick(10);
|
||||
EXPECT_EQ(c().state(), VncState::VNC_STATE_IDLE);
|
||||
EXPECT_FALSE(c().link().connected());
|
||||
}
|
||||
|
||||
TEST_F(VncTileCacheLayout, TooManyCellsTurnsTheCacheOff) {
|
||||
this->session(64, 48);
|
||||
const Bytes tile = tiles({{0, 0, 0x1234}});
|
||||
ASSERT_EQ(this->draws(tile), 1);
|
||||
s().run_for(300);
|
||||
|
||||
this->session(80, 64);
|
||||
EXPECT_EQ(c().hash_cols(), 0);
|
||||
const Bytes full = screen(80, 64, 0x2000);
|
||||
EXPECT_EQ(this->draws(full), 20);
|
||||
EXPECT_EQ(this->draws(full), 20);
|
||||
s().deliver(Rfb()
|
||||
.fbu(2)
|
||||
.raw_rect(70, 60, 10, 4, std::vector<uint16_t>(40, 0x1111))
|
||||
.rect(72, 56, 8, 8, HEXTILE)
|
||||
.u8(0x02)
|
||||
.px(0x2222));
|
||||
EXPECT_TRUE(c().is_connected());
|
||||
s().run_for(300);
|
||||
|
||||
// The cache saw nothing of the last session, so it starts empty.
|
||||
this->session(64, 48);
|
||||
EXPECT_EQ(c().hash_cols(), 4);
|
||||
EXPECT_EQ(this->draws(tile), 1);
|
||||
EXPECT_EQ(this->draws(tile), 0);
|
||||
EXPECT_EQ(d_.out_of_bounds(), 0);
|
||||
}
|
||||
|
||||
TEST(VncTileCacheClip, ServerLargerThanTheDisplayUsesTheVisibleCells) {
|
||||
FakeDisplay d(72, 48);
|
||||
Session s(&d);
|
||||
s.start(100, 80);
|
||||
ASSERT_EQ(s.client.hash_capacity(), 15u);
|
||||
EXPECT_EQ(s.client.hash_cols(), 5);
|
||||
EXPECT_EQ(s.client.hash_rows(), 3);
|
||||
set_now(now_ms() + 100);
|
||||
// Six tiles: 48,32 is fully visible, 64,32 is cut at the right edge, the rest are off screen.
|
||||
Rfb f;
|
||||
f.fbu(1).rect(48, 32, 48, 32, HEXTILE);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
f.u8(0x02).px(0x1000 + i);
|
||||
}
|
||||
EXPECT_EQ(count_draws(s, d, f.bytes), 2);
|
||||
EXPECT_EQ(count_draws(s, d, f.bytes), 0);
|
||||
EXPECT_TRUE(solid(d, 64, 32, 8, 16, 0x1001));
|
||||
EXPECT_EQ(d.out_of_bounds(), 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::vnc::testing
|
||||
|
||||
#endif // USE_HOST
|
||||
@@ -7,8 +7,7 @@ vnc:
|
||||
interval:
|
||||
- interval: 60s
|
||||
then:
|
||||
- vnc.refresh
|
||||
- if:
|
||||
condition: vnc.is_connected
|
||||
then:
|
||||
- vnc.refresh
|
||||
- logger.log: VNC is connected
|
||||
|
||||
@@ -119,12 +119,6 @@ class FakeDisplay : public display::Display {
|
||||
int out_of_bounds() const { return this->out_of_bounds_; }
|
||||
int draws_while_busy() const { return this->draws_while_busy_; }
|
||||
void set_draw_delay_us(uint32_t us) { this->draw_delay_us_ = us; }
|
||||
/// A new size for the next session, like a rotation changed at runtime. Clears the pixels.
|
||||
void resize(int width, int height) {
|
||||
this->width_ = width;
|
||||
this->height_ = height;
|
||||
this->pixels_.assign(static_cast<size_t>(width) * height, UNSET);
|
||||
}
|
||||
|
||||
protected:
|
||||
int width_;
|
||||
@@ -265,9 +259,6 @@ class TestVncClient : public VncClient {
|
||||
uint16_t vis_w() const { return this->vis_w_; }
|
||||
uint16_t vis_h() const { return this->vis_h_; }
|
||||
uint8_t bpp() const { return this->bpp_; }
|
||||
uint16_t hash_cols() const { return this->hash_cols_; }
|
||||
uint16_t hash_rows() const { return this->hash_rows_; }
|
||||
uint32_t hash_capacity() const { return this->hash_capacity_; }
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -367,7 +358,7 @@ inline Bytes fbur(bool incremental, uint16_t w, uint16_t h) {
|
||||
}
|
||||
inline Bytes set_pixel_format_565() { return {0, 0, 0, 0, 16, 16, 0, 1, 0, 31, 0, 63, 0, 31, 11, 5, 0, 0, 0, 0}; }
|
||||
inline Bytes set_pixel_format_332() { return {0, 0, 0, 0, 8, 8, 0, 1, 0, 7, 0, 7, 0, 3, 5, 2, 0, 0, 0, 0}; }
|
||||
inline Bytes set_encodings() { return {2, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0x11}; }
|
||||
inline Bytes set_encodings() { return {2, 0, 0, 2, 0, 0, 0, 0, 0xFF, 0xFF, 0xFF, 0x11}; }
|
||||
inline Bytes version_reply(char minor) { return text(std::string("RFB 003.00") + minor + "\n"); }
|
||||
|
||||
/// Splits a stream of client messages that are all FramebufferUpdateRequests.
|
||||
@@ -448,8 +439,7 @@ class Listener {
|
||||
|
||||
class Session {
|
||||
public:
|
||||
explicit Session(display::Display *display, uint32_t start_ms = 1000, bool big_endian = false)
|
||||
: client(display, big_endian) {
|
||||
explicit Session(display::Display *display, uint32_t start_ms = 1000) : client(display) {
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
set_now(start_ms);
|
||||
this->client.set_host("127.0.0.1");
|
||||
@@ -586,33 +576,6 @@ class Session {
|
||||
size_t out_pos_{0};
|
||||
};
|
||||
|
||||
inline constexpr int32_t HEXTILE = 5;
|
||||
|
||||
/// Whether the w x h block at x,y shows one RGB565 wire colour.
|
||||
inline bool solid(const FakeDisplay &d, int x, int y, int w, int h, uint16_t colour) {
|
||||
for (int row = y; row < y + h; row++) {
|
||||
for (int col = x; col < x + w; col++) {
|
||||
if (d.pixel(col, row) != expect_px(colour, 2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class VncSessionTest : public ::testing::Test {
|
||||
protected:
|
||||
void TearDown() override { this->session_.reset(); }
|
||||
/// Starts a session, then moves past the 50 ms request spacing so the next request goes out as soon as a frame ends.
|
||||
void start(FakeDisplay &display, uint16_t fb_w, uint16_t fb_h) {
|
||||
this->session_ = std::make_unique<Session>(&display);
|
||||
this->session_->start(fb_w, fb_h);
|
||||
set_now(now_ms() + 100);
|
||||
}
|
||||
Session &s() { return *this->session_; }
|
||||
std::unique_ptr<Session> session_;
|
||||
};
|
||||
|
||||
} // namespace esphome::vnc::testing
|
||||
|
||||
#endif // USE_HOST
|
||||
|
||||
@@ -239,10 +239,11 @@ await initial_state_helper.wait_for_initial_states()
|
||||
# Now perform your test actions - on_state will only receive new changes
|
||||
# ... trigger state changes ...
|
||||
|
||||
# Wait for expected state; on timeout the test fails and shows the states received
|
||||
result = await wait_or_fail(
|
||||
state_future, 5.0, "Expected state not received", states=states
|
||||
)
|
||||
# Wait for expected state
|
||||
try:
|
||||
result = await asyncio.wait_for(state_future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail(f"Expected state not received. Got: {list(states.values())}")
|
||||
```
|
||||
|
||||
**Legacy: Manual State Tracking**
|
||||
@@ -267,9 +268,10 @@ def on_state(state: EntityState) -> None:
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Wait for state with timeout
|
||||
result = await wait_or_fail(
|
||||
state_future, 5.0, "Expected state not received", states=states
|
||||
)
|
||||
try:
|
||||
result = await asyncio.wait_for(state_future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail(f"Expected state not received. Got: {list(states.values())}")
|
||||
```
|
||||
|
||||
##### Service Execution Pattern
|
||||
|
||||
@@ -12,9 +12,6 @@ api:
|
||||
- snapshot.take:
|
||||
id: disp
|
||||
filename: !lambda return filename;
|
||||
- action: vnc_refresh
|
||||
then:
|
||||
- vnc.refresh: remote
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
@@ -5,7 +5,7 @@ handshake for RFB 3.3, 3.7 and 3.8, the same security lists and failure messages
|
||||
connection that closes on any client message type it does not know.
|
||||
|
||||
The framebuffer is an RGB888 image the test draws into. Updates are translated into the pixel
|
||||
format the client asked for and encoded as Raw, Hextile or a RichCursor shape. Every client
|
||||
format the client asked for and encoded as Raw or a RichCursor shape. Every client
|
||||
message is recorded, and the test decides when to answer: ``wait_for("fbur")`` then
|
||||
``send_update(...)``.
|
||||
"""
|
||||
@@ -13,7 +13,6 @@ message is recorded, and the test decides when to answer: ``wait_for("fbur")`` t
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import Counter
|
||||
from collections.abc import Callable, Iterable, Sequence
|
||||
import contextlib
|
||||
from dataclasses import asdict, dataclass
|
||||
@@ -29,15 +28,8 @@ SECURITY_NONE = 1
|
||||
SECURITY_TIGHT = 16
|
||||
|
||||
ENCODING_RAW = 0
|
||||
ENCODING_HEXTILE = 5
|
||||
ENCODING_RICH_CURSOR = -239
|
||||
|
||||
HEXTILE_RAW = 0x01
|
||||
HEXTILE_BACKGROUND = 0x02
|
||||
HEXTILE_FOREGROUND = 0x04
|
||||
HEXTILE_ANY_SUBRECTS = 0x08
|
||||
HEXTILE_SUBRECTS_COLOURED = 0x10
|
||||
|
||||
#: ``chunk`` value that writes in random 1-7 byte pieces.
|
||||
HOSTILE_CHUNK = -1
|
||||
|
||||
@@ -46,13 +38,12 @@ MAX_CLIENT_CUT_TEXT = 1 << 20
|
||||
|
||||
|
||||
RGB = tuple[int, int, int]
|
||||
Encoding = Literal["raw", "hextile", "hextile-raw-tiles", "rich-cursor"]
|
||||
Encoding = Literal["raw", "rich-cursor"]
|
||||
Rect = tuple[int, int, int, int, Encoding]
|
||||
Message = dict[str, Any]
|
||||
|
||||
_PIXEL_FORMAT = struct.Struct(">BBBBHHHBBB3x")
|
||||
_VERSION_RE = re.compile(rb"RFB (\d{3})\.(\d{3})\n")
|
||||
_TILE = 16
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -321,24 +312,17 @@ class FakeRfbServer:
|
||||
rects: Sequence[Rect],
|
||||
*,
|
||||
pixel_format: PixelFormat | None = None,
|
||||
always_send_bg: bool = False,
|
||||
raw_tile_flags: int = HEXTILE_RAW,
|
||||
) -> bytes:
|
||||
"""Encode one FramebufferUpdate from the image.
|
||||
|
||||
Each rect is ``(x, y, w, h, encoding)``. For ``rich-cursor`` x and y are the hotspot
|
||||
and the shape is a fixed pattern. ``hextile-raw-tiles`` sends every tile raw.
|
||||
and the shape is a fixed pattern.
|
||||
|
||||
Args:
|
||||
rects: The rectangles, in order.
|
||||
pixel_format: Format to encode in; defaults to the current client's.
|
||||
always_send_bg: Send the Hextile background on every tile, even when unchanged.
|
||||
raw_tile_flags: Subencoding byte of raw Hextile tiles. Bits other than Raw are
|
||||
ignored by decoders, so 0x1F is valid.
|
||||
"""
|
||||
fmt = pixel_format or self.pixel_format
|
||||
if not raw_tile_flags & HEXTILE_RAW:
|
||||
raise ValueError("raw_tile_flags must include the Raw bit")
|
||||
out = bytearray(struct.pack(">BxH", 0, len(rects)))
|
||||
for x, y, w, h, encoding in rects:
|
||||
if encoding == "rich-cursor":
|
||||
@@ -350,15 +334,6 @@ class FakeRfbServer:
|
||||
if encoding == "raw":
|
||||
out += struct.pack(">HHHHi", x, y, w, h, ENCODING_RAW)
|
||||
out += _pixels(fmt, (v for row in values for v in row))
|
||||
elif encoding in ("hextile", "hextile-raw-tiles"):
|
||||
out += struct.pack(">HHHHi", x, y, w, h, ENCODING_HEXTILE)
|
||||
out += _hextile(
|
||||
fmt,
|
||||
values,
|
||||
force_raw=encoding == "hextile-raw-tiles",
|
||||
always_send_bg=always_send_bg,
|
||||
raw_tile_flags=raw_tile_flags,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"unknown encoding {encoding!r}")
|
||||
return bytes(out)
|
||||
@@ -368,8 +343,6 @@ class FakeRfbServer:
|
||||
rects: Sequence[Rect],
|
||||
chunk: int | None = None,
|
||||
*,
|
||||
always_send_bg: bool = False,
|
||||
raw_tile_flags: int = HEXTILE_RAW,
|
||||
delay: float = 0.0,
|
||||
) -> None:
|
||||
"""Send one FramebufferUpdate built by ``build_update()``.
|
||||
@@ -378,17 +351,10 @@ class FakeRfbServer:
|
||||
rects: The rectangles, in order.
|
||||
chunk: Split the write into pieces of at most this many bytes; ``HOSTILE_CHUNK``
|
||||
uses seeded random 1-7 byte pieces.
|
||||
always_send_bg: See ``build_update()``.
|
||||
raw_tile_flags: See ``build_update()``.
|
||||
delay: Sleep between pieces.
|
||||
"""
|
||||
session = self._require_session()
|
||||
data = self.build_update(
|
||||
rects,
|
||||
pixel_format=session.pixel_format,
|
||||
always_send_bg=always_send_bg,
|
||||
raw_tile_flags=raw_tile_flags,
|
||||
)
|
||||
data = self.build_update(rects, pixel_format=session.pixel_format)
|
||||
await self._write(session, data, chunk, delay)
|
||||
|
||||
async def send_cut_text(
|
||||
@@ -661,95 +627,3 @@ def _cursor_shape(fmt: PixelFormat, w: int, h: int) -> bytes:
|
||||
light if (x + y) % 2 else dark for y in range(h) for x in range(w)
|
||||
)
|
||||
return pixels + b"\x5a" * (((w + 7) // 8) * h)
|
||||
|
||||
|
||||
def _subrects(tile: list[list[int]], bg: int) -> list[tuple[int, int, int, int, int]]:
|
||||
"""Cover every non-background pixel with same-coloured rects, greedily."""
|
||||
height, width = len(tile), len(tile[0])
|
||||
done = [[False] * width for _ in range(height)]
|
||||
rects = []
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
colour = tile[y][x]
|
||||
if colour == bg or done[y][x]:
|
||||
continue
|
||||
right = x
|
||||
while (
|
||||
right + 1 < width
|
||||
and tile[y][right + 1] == colour
|
||||
and not done[y][right + 1]
|
||||
):
|
||||
right += 1
|
||||
bottom = y
|
||||
while bottom + 1 < height and all(
|
||||
tile[bottom + 1][i] == colour and not done[bottom + 1][i]
|
||||
for i in range(x, right + 1)
|
||||
):
|
||||
bottom += 1
|
||||
for line in range(y, bottom + 1):
|
||||
done[line][x : right + 1] = [True] * (right + 1 - x)
|
||||
rects.append((x, y, right + 1 - x, bottom + 1 - y, colour))
|
||||
return rects
|
||||
|
||||
|
||||
def _hextile(
|
||||
fmt: PixelFormat,
|
||||
values: list[list[int]],
|
||||
*,
|
||||
force_raw: bool,
|
||||
always_send_bg: bool,
|
||||
raw_tile_flags: int,
|
||||
) -> bytes:
|
||||
"""Hextile-encode a region given as rows of pixel values.
|
||||
|
||||
Background and foreground carry over between tiles, but never past a raw tile or a
|
||||
coloured-subrect tile, as libvncserver's encoder does.
|
||||
"""
|
||||
out = bytearray()
|
||||
height = len(values)
|
||||
width = len(values[0]) if values else 0
|
||||
bg: int | None = None
|
||||
fg: int | None = None
|
||||
for ty in range(0, height, _TILE):
|
||||
for tx in range(0, width, _TILE):
|
||||
tile = [row[tx : tx + _TILE] for row in values[ty : ty + _TILE]]
|
||||
flat = [v for row in tile for v in row]
|
||||
raw_size = len(flat) * fmt.bytes_per_pixel
|
||||
if not force_raw:
|
||||
counts = Counter(flat)
|
||||
tile_bg = counts.most_common(1)[0][0]
|
||||
flags = 0
|
||||
body = bytearray()
|
||||
if tile_bg != bg or always_send_bg:
|
||||
flags |= HEXTILE_BACKGROUND
|
||||
body += fmt.to_bytes(tile_bg)
|
||||
if len(counts) == 1:
|
||||
out.append(flags)
|
||||
out += body
|
||||
bg = tile_bg
|
||||
continue
|
||||
subrects = _subrects(tile, tile_bg)
|
||||
coloured = len(counts) > 2
|
||||
flags |= HEXTILE_ANY_SUBRECTS
|
||||
if coloured:
|
||||
flags |= HEXTILE_SUBRECTS_COLOURED
|
||||
tile_fg = None
|
||||
else:
|
||||
tile_fg = next(c for c in counts if c != tile_bg)
|
||||
if tile_fg != fg:
|
||||
flags |= HEXTILE_FOREGROUND
|
||||
body += fmt.to_bytes(tile_fg)
|
||||
body.append(len(subrects))
|
||||
for x, y, w, h, colour in subrects:
|
||||
if coloured:
|
||||
body += fmt.to_bytes(colour)
|
||||
body += bytes([(x << 4) | y, ((w - 1) << 4) | (h - 1)])
|
||||
if len(body) < raw_size:
|
||||
out.append(flags)
|
||||
out += body
|
||||
bg, fg = tile_bg, tile_fg
|
||||
continue
|
||||
out.append(raw_tile_flags)
|
||||
out += _pixels(fmt, flat)
|
||||
bg = fg = None
|
||||
return bytes(out)
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import collections
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
from .state_utils import assert_device_name
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
from .wait_utils import wait_or_fail
|
||||
@@ -23,41 +23,54 @@ async def test_action_concurrent_reentry(
|
||||
This test runs a script in parallel with varying arguments and verifies if
|
||||
each script keeps its original argument throughout its execution
|
||||
"""
|
||||
waiter = LineWaiter()
|
||||
test_complete = asyncio.Event()
|
||||
expected = {0, 1, 2, 3, 4}
|
||||
|
||||
def captures(pattern: str) -> list[tuple[int, ...]]:
|
||||
return [
|
||||
tuple(map(int, m.groups()))
|
||||
for line in waiter.lines
|
||||
if (m := re.search(pattern, line))
|
||||
]
|
||||
# Patterns to match in logs
|
||||
after_wait_until_pattern = re.compile(r"AFTER wait_until ARG (\d+)")
|
||||
after_script_wait_pattern = re.compile(r"AFTER script\.wait ARG (\d+)")
|
||||
after_repeat_pattern = re.compile(r"AFTER repeat ARG (\d+)")
|
||||
in_repeat_pattern = re.compile(r"IN repeat (\d+) ARG (\d+)")
|
||||
after_while_pattern = re.compile(r"AFTER while ARG (\d+)")
|
||||
in_while_pattern = re.compile(r"IN while ARG (\d+)")
|
||||
|
||||
def arg_list(pattern: str) -> list[int]:
|
||||
return [arg for (arg,) in captures(pattern)]
|
||||
after_wait_until_args = []
|
||||
after_script_wait_args = []
|
||||
after_while_args = []
|
||||
in_while_args = []
|
||||
after_repeat_args = []
|
||||
in_repeat_args = collections.defaultdict(list)
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if test_complete.is_set():
|
||||
return
|
||||
|
||||
if mo := after_wait_until_pattern.search(line):
|
||||
after_wait_until_args.append(int(mo.group(1)))
|
||||
elif mo := after_script_wait_pattern.search(line):
|
||||
after_script_wait_args.append(int(mo.group(1)))
|
||||
elif mo := in_while_pattern.search(line):
|
||||
in_while_args.append(int(mo.group(1)))
|
||||
elif mo := after_while_pattern.search(line):
|
||||
after_while_args.append(int(mo.group(1)))
|
||||
elif mo := in_repeat_pattern.search(line):
|
||||
in_repeat_args[int(mo.group(1))].append(int(mo.group(2)))
|
||||
elif mo := after_repeat_pattern.search(line):
|
||||
after_repeat_args.append(int(mo.group(1)))
|
||||
if len(after_repeat_args) == len(expected):
|
||||
test_complete.set()
|
||||
|
||||
# Run with log monitoring
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=waiter.callback),
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify device info
|
||||
await assert_device_name(client, "action-concurrent-reentry")
|
||||
|
||||
# Wait for tests to complete with timeout
|
||||
await wait_or_fail(
|
||||
waiter.wait_for_each(*(f"AFTER repeat ARG {arg}" for arg in expected)),
|
||||
8.0,
|
||||
"test timed out",
|
||||
)
|
||||
after_wait_until_args = arg_list(r"AFTER wait_until ARG (\d+)")
|
||||
after_script_wait_args = arg_list(r"AFTER script\.wait ARG (\d+)")
|
||||
after_repeat_args = arg_list(r"AFTER repeat ARG (\d+)")
|
||||
after_while_args = arg_list(r"AFTER while ARG (\d+)")
|
||||
in_while_args = arg_list(r"IN while ARG (\d+)")
|
||||
in_repeat_args = collections.defaultdict(list)
|
||||
for iteration, arg in captures(r"IN repeat (\d+) ARG (\d+)"):
|
||||
in_repeat_args[iteration].append(arg)
|
||||
await wait_or_fail(test_complete, 8.0, "test timed out")
|
||||
|
||||
# order may change, but all args must be present
|
||||
for args in in_repeat_args.values():
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.helpers import fnv1_hash
|
||||
|
||||
from .log_utils import LineWaiter
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@@ -17,10 +19,20 @@ async def test_api_action_metadata(
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Action and argument metadata reach the client and the actions still run."""
|
||||
waiter = LineWaiter()
|
||||
loop = asyncio.get_running_loop()
|
||||
buzzer_called = loop.create_future()
|
||||
plain_called = loop.create_future()
|
||||
buzzer_pattern = re.compile(r"Buzzer: two_short")
|
||||
plain_pattern = re.compile(r"Plain action called")
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
if not buzzer_called.done() and buzzer_pattern.search(line):
|
||||
buzzer_called.set_result(True)
|
||||
elif not plain_called.done() and plain_pattern.search(line):
|
||||
plain_called.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=waiter.callback),
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
_, services = await client.list_entities_services()
|
||||
@@ -60,5 +72,5 @@ async def test_api_action_metadata(
|
||||
buzzer, {"song_str": "two_short:d=4,o=5,b=100:16e6,16e6", "volume": 3}
|
||||
)
|
||||
await client.execute_service(plain, {"value": 1})
|
||||
await waiter.wait_for("Buzzer: two_short", timeout=5.0)
|
||||
await waiter.wait_for("Plain action called", timeout=5.0)
|
||||
await asyncio.wait_for(buzzer_called, timeout=5.0)
|
||||
await asyncio.wait_for(plain_called, timeout=5.0)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user