mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 13:56:10 +00:00
Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52c4c0310d | ||
|
|
547dbf18d4 | ||
|
|
ba3af07a23 | ||
|
|
9c6ae9c615 | ||
|
|
de02ef8555 | ||
|
|
38b23152f4 | ||
|
|
0849e4b412 | ||
|
|
375e8bc39b | ||
|
|
16a91ab760 | ||
|
|
2456e8d686 | ||
|
|
0c8e1c3f1c | ||
|
|
a3c575d643 | ||
|
|
0573c8b568 | ||
|
|
d01205fa62 | ||
|
|
de7ea89d0b | ||
|
|
0077f68158 | ||
|
|
7458f8c7d8 | ||
|
|
0e5e0a8ff6 | ||
|
|
752c230405 | ||
|
|
cfc92657a2 | ||
|
|
32f32f144d | ||
|
|
9b60e7cff2 | ||
|
|
02b8db1f3f | ||
|
|
f860c8a8d9 | ||
|
|
bb7a060138 | ||
|
|
84839507bd | ||
|
|
18348b8794 | ||
|
|
f7dc2ecb51 | ||
|
|
ddcc5874ed | ||
|
|
ef54eb14d8 | ||
|
|
60aa3e24f1 | ||
|
|
63566999bd | ||
|
|
1728d3326b | ||
|
|
2b99c56754 | ||
|
|
d79cd9a7eb | ||
|
|
28013a60e1 | ||
|
|
fa18994fab | ||
|
|
1090706216 | ||
|
|
966193cb09 | ||
|
|
e1077748be | ||
|
|
cc89a011e2 | ||
|
|
ced497995d | ||
|
|
228c553f60 | ||
|
|
563e197c6d | ||
|
|
9d07f1f400 | ||
|
|
846fc70bdf | ||
|
|
c76e9e1d62 | ||
|
|
7a3aac407f | ||
|
|
b0a1054e8d | ||
|
|
74212d8d04 | ||
|
|
101c9cd840 | ||
|
|
8152dcf224 | ||
|
|
f2d50b4aa7 | ||
|
|
a8a8acb918 | ||
|
|
3ae5e1dfad | ||
|
|
ef79d48b6f | ||
|
|
f75b3d6ded | ||
|
|
3cf29bf4fb | ||
|
|
f752e3b150 | ||
|
|
94804cc741 | ||
|
|
879c5e5e14 | ||
|
|
a5bc7070d1 | ||
|
|
b2eebfb010 | ||
|
|
a596d1d3c8 | ||
|
|
6701aedae2 | ||
|
|
6fddbd1d48 | ||
|
|
03aaa00d75 | ||
|
|
b612eb2929 | ||
|
|
63c041a95b | ||
|
|
f8d5096093 | ||
|
|
f835600fa6 |
@@ -12,6 +12,7 @@ Checks: >-
|
||||
-bugprone-multi-level-implicit-pointer-conversion,
|
||||
-bugprone-narrowing-conversions,
|
||||
-bugprone-tagged-union-member-count,
|
||||
-bugprone-signed-bitwise,
|
||||
-bugprone-signed-char-misuse,
|
||||
-bugprone-switch-missing-default-case,
|
||||
-cert-dcl50-cpp,
|
||||
@@ -39,6 +40,7 @@ Checks: >-
|
||||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-explicit-constructor,
|
||||
-cppcoreguidelines-init-variables,
|
||||
-cppcoreguidelines-macro-to-enum,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
@@ -84,6 +86,7 @@ Checks: >-
|
||||
-llvm-use-ranges,
|
||||
-llvmlibc-*,
|
||||
-misc-const-correctness,
|
||||
-misc-explicit-constructor,
|
||||
-misc-include-cleaner,
|
||||
-misc-multiple-inheritance,
|
||||
-misc-no-recursion,
|
||||
@@ -113,6 +116,7 @@ Checks: >-
|
||||
-objc-*,
|
||||
-performance-enum-size,
|
||||
-portability-avoid-pragma-once,
|
||||
-portability-no-assembler,
|
||||
-portability-template-virtual-member-function,
|
||||
-readability-ambiguous-smartptr-reset-call,
|
||||
-readability-avoid-nested-conditional-operator,
|
||||
@@ -129,9 +133,13 @@ Checks: >-
|
||||
-readability-named-parameter,
|
||||
-readability-redundant-casting,
|
||||
-readability-redundant-inline-specifier,
|
||||
-readability-redundant-lambda-parameter-list,
|
||||
-readability-redundant-member-init,
|
||||
-readability-redundant-nested-if,
|
||||
-readability-redundant-parentheses,
|
||||
-readability-redundant-typename,
|
||||
-readability-trailing-comma,
|
||||
-readability-trivial-switch,
|
||||
-readability-uppercase-literal-suffix,
|
||||
-readability-use-anyofallof,
|
||||
-readability-use-std-min-max,
|
||||
|
||||
@@ -38,7 +38,7 @@ repos:
|
||||
- id: yamllint
|
||||
exclude: ^(\.clang-format|\.clang-tidy)$
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: v22.1.8
|
||||
rev: v23.1.3
|
||||
hooks:
|
||||
- id: clang-format
|
||||
types_or: [c, c++]
|
||||
|
||||
@@ -700,6 +700,7 @@ esphome/components/veml7700/* @latonita
|
||||
esphome/components/version/* @esphome/core
|
||||
esphome/components/vevor_dtx/* @Bofr5555
|
||||
esphome/components/vl53l1x/* @will-tm
|
||||
esphome/components/vnc/* @MindFreeze
|
||||
esphome/components/voice_assistant/* @jesserockz @kahrendt
|
||||
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
|
||||
@@ -111,6 +111,8 @@ static const LogString *input_to_str(uint8_t input) {
|
||||
return LOG_STR("VDD");
|
||||
case NRF_SAADC_INPUT_VDDHDIV5:
|
||||
return LOG_STR("VDDHDIV5");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return LOG_STR("undefined input");
|
||||
}
|
||||
|
||||
@@ -261,6 +261,8 @@ void AirtonClimate::parse_state_frame_(const uint8_t *frame) {
|
||||
case AIRTON_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this->target_temperature = (frame[3] & 0b00001111) + AIRTON_TEMP_MIN;
|
||||
|
||||
@@ -608,6 +608,7 @@ message ListEntitiesCoverResponse {
|
||||
bool supports_stop = 12;
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
bool supports_stop_tilt = 14;
|
||||
bool supports_toggle = 15;
|
||||
}
|
||||
|
||||
// Deprecated in API version 1.1
|
||||
@@ -670,6 +671,7 @@ message CoverCommandRequest {
|
||||
bool stop = 8;
|
||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||
bool stop_tilt = 10;
|
||||
bool toggle = 11;
|
||||
}
|
||||
|
||||
// ==================== FAN ====================
|
||||
@@ -1783,6 +1785,8 @@ message MediaPlayerSupportedFormat {
|
||||
uint32 num_channels = 3;
|
||||
MediaPlayerFormatPurpose purpose = 4;
|
||||
uint32 sample_bytes = 5;
|
||||
// Constant bitrate in kbps for lossy formats; 0 means no preference
|
||||
uint32 bitrate = 6;
|
||||
}
|
||||
message ListEntitiesMediaPlayerResponse {
|
||||
option (id) = 63;
|
||||
|
||||
@@ -555,6 +555,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
|
||||
msg.supports_tilt = traits.get_supports_tilt();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
msg.supports_stop_tilt = traits.get_supports_stop_tilt();
|
||||
msg.supports_toggle = traits.get_supports_toggle();
|
||||
return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size);
|
||||
}
|
||||
void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
@@ -567,6 +568,8 @@ void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
|
||||
call.set_command_stop();
|
||||
if (msg.stop_tilt)
|
||||
call.set_command_stop_tilt();
|
||||
if (msg.toggle)
|
||||
call.set_command_toggle();
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -1128,6 +1131,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
media_format.num_channels = supported_format.num_channels;
|
||||
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
||||
media_format.sample_bytes = supported_format.sample_bytes;
|
||||
media_format.bitrate = supported_format.bitrate;
|
||||
}
|
||||
return fill_and_encode_entity_info(media_player, msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ void HelloRequest::decode_field(void *self, uint32_t tag, const uint8_t *data, p
|
||||
msg.outgoing_connection_target = value.as_bool();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *HelloResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -51,6 +53,8 @@ void DisconnectRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.reason = static_cast<enums::DisconnectReason>(value.as_varint());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *DisconnectRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -396,6 +400,8 @@ void WizardInputSetRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
case proto_tag(2, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.entity_id = StringRef(value.data(), value.size());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -478,6 +484,7 @@ uint8_t *ListEntitiesCoverResponse::encode_msg(const void *self, uint8_t *__rest
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, msg.device_id);
|
||||
#endif
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 14, msg.supports_stop_tilt);
|
||||
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 15, msg.supports_toggle);
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesCoverResponse::calc_size_msg(const void *self) {
|
||||
@@ -500,6 +507,7 @@ uint32_t ListEntitiesCoverResponse::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.device_id);
|
||||
#endif
|
||||
size += ProtoSize::calc_bool(1, msg.supports_stop_tilt);
|
||||
size += ProtoSize::calc_bool(1, msg.supports_toggle);
|
||||
return size;
|
||||
}
|
||||
uint8_t *CoverStateResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -559,6 +567,11 @@ void CoverCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
case proto_tag(10, WIRE_TYPE_VARINT):
|
||||
msg.stop_tilt = value.as_bool();
|
||||
break;
|
||||
case proto_tag(11, WIRE_TYPE_VARINT):
|
||||
msg.toggle = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -679,6 +692,8 @@ void FanCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -885,6 +900,8 @@ void LightCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1028,6 +1045,8 @@ void SwitchCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1098,6 +1117,8 @@ void SubscribeLogsRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.dump_config = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -1127,6 +1148,8 @@ void NoiseEncryptionSetKeyRequest::decode_field(void *self, uint32_t tag, const
|
||||
msg.key = value.data();
|
||||
msg.key_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *NoiseEncryptionSetKeyResponse::encode_msg(const void *self,
|
||||
@@ -1233,6 +1256,8 @@ void HomeassistantActionResponse::decode_field(void *self, uint32_t tag, const u
|
||||
msg.response_data_len = value.size();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1267,6 +1292,8 @@ void HomeAssistantStateResponse::decode_field(void *self, uint32_t tag, const ui
|
||||
case proto_tag(3, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.attribute = StringRef(value.data(), value.size());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1292,6 +1319,8 @@ void DSTRule::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_
|
||||
case proto_tag(6, WIRE_TYPE_VARINT):
|
||||
msg.day_of_week = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void ParsedTimezone::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -1310,6 +1339,8 @@ void ParsedTimezone::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
case proto_tag(4, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
value.decode_to_message(msg.dst_end);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -1323,6 +1354,8 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
|
||||
value.decode_to_message(msg.parsed_timezone);
|
||||
msg.has_parsed_timezone = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
@@ -1413,6 +1446,8 @@ void ExecuteServiceArgument::decode_field(void *self, uint32_t tag, const uint8_
|
||||
case proto_tag(9, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.string_array.push_back(value.as_string());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
||||
@@ -1447,6 +1482,8 @@ void ExecuteServiceRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.return_response = value.as_bool();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
|
||||
@@ -1541,6 +1578,8 @@ void CameraImageRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.stream = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1777,6 +1816,8 @@ void ClimateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1905,6 +1946,8 @@ void WaterHeaterCommandRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
case proto_tag(8, WIRE_TYPE_FIXED32):
|
||||
msg.target_temperature_high = value.as_float();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1996,6 +2039,8 @@ void NumberCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2075,6 +2120,8 @@ void SelectCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2177,6 +2224,8 @@ void SirenCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2260,6 +2309,8 @@ void LockCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2309,6 +2360,8 @@ void ButtonCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2320,6 +2373,7 @@ uint8_t *MediaPlayerSupportedFormat::encode_msg(const void *self, uint8_t *__res
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.num_channels);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.purpose));
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.sample_bytes);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.bitrate);
|
||||
return pos;
|
||||
}
|
||||
uint32_t MediaPlayerSupportedFormat::calc_size_msg(const void *self) {
|
||||
@@ -2330,6 +2384,7 @@ uint32_t MediaPlayerSupportedFormat::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.num_channels);
|
||||
size += msg.purpose ? 2 : 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.sample_bytes);
|
||||
size += ProtoSize::calc_uint32(1, msg.bitrate);
|
||||
return size;
|
||||
}
|
||||
uint8_t *ListEntitiesMediaPlayerResponse::encode_msg(const void *self,
|
||||
@@ -2436,6 +2491,8 @@ void MediaPlayerCommandRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2448,6 +2505,8 @@ void SubscribeBluetoothLEAdvertisementsRequest::decode_field(void *self, uint32_
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.flags = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -2506,6 +2565,8 @@ void BluetoothDeviceRequest::decode_field(void *self, uint32_t tag, const uint8_
|
||||
case proto_tag(4, WIRE_TYPE_VARINT):
|
||||
msg.address_type = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothDeviceConnectionResponse::encode_msg(const void *self,
|
||||
@@ -2534,6 +2595,8 @@ void BluetoothGATTGetServicesRequest::decode_field(void *self, uint32_t tag, con
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.address = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothGATTDescriptor::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -2660,6 +2723,8 @@ void BluetoothGATTReadRequest::decode_field(void *self, uint32_t tag, const uint
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.handle = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothGATTReadResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -2695,6 +2760,8 @@ void BluetoothGATTWriteRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void BluetoothGATTReadDescriptorRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -2708,6 +2775,8 @@ void BluetoothGATTReadDescriptorRequest::decode_field(void *self, uint32_t tag,
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.handle = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void BluetoothGATTWriteDescriptorRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -2725,6 +2794,8 @@ void BluetoothGATTWriteDescriptorRequest::decode_field(void *self, uint32_t tag,
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void BluetoothGATTNotifyRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -2741,6 +2812,8 @@ void BluetoothGATTNotifyRequest::decode_field(void *self, uint32_t tag, const ui
|
||||
case proto_tag(3, WIRE_TYPE_VARINT):
|
||||
msg.enable = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothGATTNotifyDataResponse::encode_msg(const void *self,
|
||||
@@ -2898,6 +2971,8 @@ void BluetoothScannerSetModeRequest::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.mode = static_cast<enums::BluetoothScannerMode>(value.as_varint());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -2913,6 +2988,8 @@ void SubscribeVoiceAssistantRequest::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.flags = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *VoiceAssistantAudioSettings::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -2961,6 +3038,8 @@ void VoiceAssistantResponse::decode_field(void *self, uint32_t tag, const uint8_
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.error = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantEventData::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -2973,6 +3052,8 @@ void VoiceAssistantEventData::decode_field(void *self, uint32_t tag, const uint8
|
||||
case proto_tag(2, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.value = StringRef(value.data(), value.size());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantEventResponse::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -2987,6 +3068,8 @@ void VoiceAssistantEventResponse::decode_field(void *self, uint32_t tag, const u
|
||||
msg.data.emplace_back();
|
||||
value.decode_to_message(msg.data.back());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantAudio::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
|
||||
@@ -3004,6 +3087,8 @@ void VoiceAssistantAudio::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
msg.data2 = value.data();
|
||||
msg.data2_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *VoiceAssistantAudio::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -3044,6 +3129,8 @@ void VoiceAssistantTimerEventResponse::decode_field(void *self, uint32_t tag, co
|
||||
case proto_tag(6, WIRE_TYPE_VARINT):
|
||||
msg.is_active = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantAnnounceRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -3063,6 +3150,8 @@ void VoiceAssistantAnnounceRequest::decode_field(void *self, uint32_t tag, const
|
||||
case proto_tag(4, WIRE_TYPE_VARINT):
|
||||
msg.start_conversation = value.as_bool();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *VoiceAssistantAnnounceFinished::encode_msg(const void *self,
|
||||
@@ -3124,6 +3213,8 @@ void VoiceAssistantExternalWakeWord::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(7, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.url = StringRef(value.data(), value.size());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void VoiceAssistantConfigurationRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -3135,6 +3226,8 @@ void VoiceAssistantConfigurationRequest::decode_field(void *self, uint32_t tag,
|
||||
msg.external_wake_words.emplace_back();
|
||||
value.decode_to_message(msg.external_wake_words.back());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *VoiceAssistantConfigurationResponse::encode_msg(const void *self,
|
||||
@@ -3173,6 +3266,8 @@ void VoiceAssistantSetConfiguration::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(1, WIRE_TYPE_LENGTH_DELIMITED):
|
||||
msg.active_wake_words.push_back(value.as_string());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3254,6 +3349,8 @@ void AlarmControlPanelCommandRequest::decode_field(void *self, uint32_t tag, con
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3333,6 +3430,8 @@ void TextCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3414,6 +3513,8 @@ void DateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3495,6 +3596,8 @@ void TimeCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3644,6 +3747,8 @@ void ValveCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t *
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3718,6 +3823,8 @@ void DateTimeCommandRequest::decode_field(void *self, uint32_t tag, const uint8_
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3807,6 +3914,8 @@ void UpdateCommandRequest::decode_field(void *self, uint32_t tag, const uint8_t
|
||||
msg.device_id = value.as_varint();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -3819,6 +3928,8 @@ void ZWaveProxyFrame::decode_field(void *self, uint32_t tag, const uint8_t *data
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -3845,6 +3956,8 @@ void ZWaveProxyRequest::decode_field(void *self, uint32_t tag, const uint8_t *da
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *ZWaveProxyRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -3940,6 +4053,8 @@ void InfraredRFTransmitRawTimingsRequest::decode_field(void *self, uint32_t tag,
|
||||
case proto_tag(6, WIRE_TYPE_VARINT):
|
||||
msg.modulation = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -4056,6 +4171,8 @@ void SerialProxyConfigureRequest::decode_field(void *self, uint32_t tag, const u
|
||||
case proto_tag(6, WIRE_TYPE_VARINT):
|
||||
msg.data_size = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
@@ -4084,6 +4201,8 @@ void SerialProxyWriteRequest::decode_field(void *self, uint32_t tag, const uint8
|
||||
msg.data = value.data();
|
||||
msg.data_len = value.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void SerialProxySetModemPinsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -4097,6 +4216,8 @@ void SerialProxySetModemPinsRequest::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.line_states = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
void SerialProxyGetModemPinsRequest::decode_field(void *self, uint32_t tag, const uint8_t *data,
|
||||
@@ -4107,6 +4228,8 @@ void SerialProxyGetModemPinsRequest::decode_field(void *self, uint32_t tag, cons
|
||||
case proto_tag(1, WIRE_TYPE_VARINT):
|
||||
msg.instance = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *SerialProxyGetModemPinsResponse::encode_msg(const void *self,
|
||||
@@ -4135,6 +4258,8 @@ void SerialProxyRequest::decode_field(void *self, uint32_t tag, const uint8_t *d
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.type = static_cast<enums::SerialProxyRequestType>(value.as_varint());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *SerialProxyRequestResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -4165,6 +4290,8 @@ void SerialProxySetModeRequest::decode_field(void *self, uint32_t tag, const uin
|
||||
case proto_tag(2, WIRE_TYPE_VARINT):
|
||||
msg.mode = static_cast<enums::SerialProxyMode>(value.as_varint());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
@@ -4233,6 +4360,8 @@ void BluetoothSetConnectionParamsRequest::decode_field(void *self, uint32_t tag,
|
||||
case proto_tag(5, WIRE_TYPE_VARINT):
|
||||
msg.timeout = value.as_varint();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t *BluetoothSetConnectionParamsResponse::encode_msg(const void *self,
|
||||
|
||||
@@ -911,7 +911,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage {
|
||||
class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 13;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 59;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 61;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_cover_response"); }
|
||||
#endif
|
||||
@@ -921,6 +921,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
|
||||
ProgmemStringRef device_class{};
|
||||
bool supports_stop{false};
|
||||
bool supports_stop_tilt{false};
|
||||
bool supports_toggle{false};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
@@ -958,7 +959,7 @@ class CoverStateResponse final : public StateResponseProtoMessage {
|
||||
class CoverCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 30;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 29;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("cover_command_request"); }
|
||||
#endif
|
||||
@@ -968,6 +969,7 @@ class CoverCommandRequest final : public CommandProtoMessage {
|
||||
float tilt{0.0f};
|
||||
bool stop{false};
|
||||
bool stop_tilt{false};
|
||||
bool toggle{false};
|
||||
void decode(const uint8_t *buffer, size_t length) {
|
||||
ProtoDecodableMessage::decode_fields(this, buffer, length, &decode_field);
|
||||
}
|
||||
@@ -2261,6 +2263,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage {
|
||||
uint32_t num_channels{0};
|
||||
enums::MediaPlayerFormatPurpose purpose{};
|
||||
uint32_t sample_bytes{0};
|
||||
uint32_t bitrate{0};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -1246,6 +1246,7 @@ const char *ListEntitiesCoverResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("supports_stop_tilt"), this->supports_stop_tilt);
|
||||
dump_field(out, ESPHOME_PSTR("supports_toggle"), this->supports_toggle);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *CoverStateResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -1271,6 +1272,7 @@ const char *CoverCommandRequest::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("stop_tilt"), this->stop_tilt);
|
||||
dump_field(out, ESPHOME_PSTR("toggle"), this->toggle);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
@@ -2097,6 +2099,7 @@ const char *MediaPlayerSupportedFormat::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("num_channels"), this->num_channels);
|
||||
dump_field(out, ESPHOME_PSTR("purpose"), static_cast<enums::MediaPlayerFormatPurpose>(this->purpose));
|
||||
dump_field(out, ESPHOME_PSTR("sample_bytes"), this->sample_bytes);
|
||||
dump_field(out, ESPHOME_PSTR("bitrate"), this->bitrate);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -301,7 +301,7 @@ constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
|
||||
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
|
||||
|
||||
/// Generated encode body: writes the fields at pos, returns the cursor past them.
|
||||
using ProtoEncodeFn = uint8_t *(*) (const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
|
||||
using ProtoEncodeFn = uint8_t *(*)(const void *, uint8_t *PROTO_ENCODE_DEBUG_PARAM);
|
||||
|
||||
/// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
|
||||
/// returns it advanced, so outlined calls at -Os chain through the return register instead of a
|
||||
|
||||
@@ -168,6 +168,8 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case BALLU_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -111,10 +111,9 @@ static const uint8_t BL0906_USR_WRPROT = 0x9E;
|
||||
// Reset Register
|
||||
static const uint8_t BL0906_SOFT_RESET = 0x9F;
|
||||
|
||||
const uint8_t BL0906_INIT[2][6] = {
|
||||
// Reset to default
|
||||
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
|
||||
// Enable User Operation Write
|
||||
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
|
||||
const uint8_t BL0906_INIT[2][6] = {// Reset to default
|
||||
{BL0906_WRITE_COMMAND, BL0906_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x52},
|
||||
// Enable User Operation Write
|
||||
{BL0906_WRITE_COMMAND, BL0906_USR_WRPROT, 0x55, 0x55, 0x00, 0xB7}};
|
||||
|
||||
} // namespace esphome::bl0906
|
||||
|
||||
@@ -268,6 +268,7 @@ int BluedroidGattClient::discover_services() {
|
||||
case SearchState::REPORT_PENDING:
|
||||
return 0; // One completion is already owed to this claimant.
|
||||
case SearchState::NONE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
int err = this->check_and_log_error_("esp_ble_gattc_search_service",
|
||||
|
||||
@@ -123,7 +123,7 @@ void BMI160Component::internal_setup_(int stage) {
|
||||
|
||||
bool BMI160Component::setup_stage_(int stage) {
|
||||
switch (stage) {
|
||||
case 0:
|
||||
case 0: {
|
||||
uint8_t chipid;
|
||||
if (!this->read_byte(BMI160_REGISTER_CHIPID, &chipid) || (chipid != 0b11010001)) {
|
||||
return false;
|
||||
@@ -138,7 +138,7 @@ bool BMI160Component::setup_stage_(int stage) {
|
||||
// min 5ms, 10ms
|
||||
this->set_timeout(10, [this]() { this->internal_setup_(1); });
|
||||
break;
|
||||
|
||||
}
|
||||
case 1:
|
||||
ESP_LOGV(TAG, " Bringing gyroscope out of sleep");
|
||||
if (!this->write_byte(BMI160_REGISTER_CMD, (uint8_t) Cmd::GYRO_SET_PMU_MODE | (uint8_t) GyroPmuMode::NORMAL)) {
|
||||
@@ -149,7 +149,7 @@ bool BMI160Component::setup_stage_(int stage) {
|
||||
this->set_timeout(GYRO_WAKEUP_TIMEOUT_MS, [this]() { this->internal_setup_(2); });
|
||||
break;
|
||||
|
||||
case 2:
|
||||
case 2: {
|
||||
ESP_LOGV(TAG, " Setting up Gyro Config");
|
||||
uint8_t gyro_config = (uint8_t) GyroBandwidth::OSR4 | (uint8_t) GyroOuputDataRate::HZ_25;
|
||||
ESP_LOGV(TAG, " Output gyro_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(gyro_config));
|
||||
@@ -178,6 +178,10 @@ bool BMI160Component::setup_stage_(int stage) {
|
||||
}
|
||||
|
||||
this->setup_complete_ = true;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ class CC1101Component final : public Component,
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
static void IRAM_ATTR gpio_intr(CC1101Component *arg);
|
||||
static void gpio_intr(CC1101Component *arg);
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -175,6 +175,8 @@ uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) {
|
||||
case POWER_PAC:
|
||||
coeff = this->data_.coefficient[POWER_PAC] * scale;
|
||||
return coeff ? 0x80000000 / coeff : 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -156,6 +156,8 @@ static const char *regout0_to_str(uint32_t value) {
|
||||
return "3.0V";
|
||||
case (UICR_REGOUT0_VOUT_3V3):
|
||||
return "3.3V";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "???V";
|
||||
}
|
||||
@@ -337,6 +339,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
return "QFxx - 6x6 48-pin QFN";
|
||||
case 0x2005:
|
||||
return "CKxx - 3.544 x 3.607 WLCSP";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return "Unspecified";
|
||||
};
|
||||
|
||||
@@ -143,6 +143,8 @@ bool DelonghiClimate::parse_state_frame_(const uint8_t frame[]) {
|
||||
case DELONGHI_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->publish_state();
|
||||
return true;
|
||||
|
||||
@@ -250,6 +250,8 @@ void DFPlayer::loop() {
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "epaper_goodisplay_3c.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
void EPaperGoodDisplay3C::power_on() {}
|
||||
|
||||
void EPaperGoodDisplay3C::refresh_screen(bool partial) {
|
||||
EPaperWeAct3C::refresh_screen(partial);
|
||||
this->next_delay_ = REFRESH_START_MS;
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_weact_3c.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Good Display 3-color e-paper displays (SSD1680 controller), e.g. GDEY029Z95.
|
||||
*
|
||||
* Driven as EPaperWeAct3C, except that there is no separate power-on: the full refresh (0xF7) powers the panel
|
||||
* up itself. Busy then only goes high a little after the refresh starts, so the next state waits before
|
||||
* checking it.
|
||||
*/
|
||||
class EPaperGoodDisplay3C : public EPaperWeAct3C {
|
||||
public:
|
||||
using EPaperWeAct3C::EPaperWeAct3C;
|
||||
|
||||
protected:
|
||||
// Time from master activation (0x20) until busy can be relied on
|
||||
static constexpr uint16_t REFRESH_START_MS = 50;
|
||||
|
||||
// With EPaperWeAct3C's power-on (0x22 0xF8) first, this panel's refresh ends almost at once and the image
|
||||
// does not change.
|
||||
void power_on() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -4,11 +4,17 @@ Supported models:
|
||||
- weact-2.13in-3c: 122x250 pixels (2.13" display)
|
||||
- weact-2.9in-3c: 128x296 pixels (2.9" display)
|
||||
- weact-4.2in-3c: 400x300 pixels (4.2" display)
|
||||
- goodisplay-gdey029z95-2.9: 128x296 pixels (Good Display GDEY029Z95 2.9" display)
|
||||
|
||||
These displays use SSD1680 or SSD1683 controller and require a specific initialization
|
||||
sequence. The DRV_OUT_CTL command is calculated from the display height.
|
||||
The Good Display panel is driven as the WeAct ones, except that it must not get the
|
||||
separate power-on before the refresh, so it has its own driver class, EPaperGoodDisplay3C.
|
||||
"""
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
|
||||
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
@@ -17,8 +23,10 @@ class WeActBWR(EpaperModel):
|
||||
|
||||
source_files = ("epaper_weact_3c.cpp",)
|
||||
|
||||
def __init__(self, name, **defaults):
|
||||
super().__init__(name, "EPaperWeAct3C", **defaults)
|
||||
def __init__(
|
||||
self, name: str, class_name: str = "EPaperWeAct3C", **defaults: object
|
||||
) -> None:
|
||||
super().__init__(name, class_name, **defaults)
|
||||
|
||||
def get_init_sequence(self, config):
|
||||
"""Generate initialization sequence for WeAct BWR displays.
|
||||
@@ -75,3 +83,27 @@ weact_4p2in3c = WeActBWR(
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="10s",
|
||||
)
|
||||
|
||||
|
||||
class GoodDisplayBWR(WeActBWR):
|
||||
"""Good Display Black/White/Red displays using SSD1680 controller."""
|
||||
|
||||
source_files = ("epaper_weact_3c.cpp", "epaper_goodisplay_3c.cpp")
|
||||
|
||||
def __init__(self, name: str, **defaults: object) -> None:
|
||||
super().__init__(name, "EPaperGoodDisplay3C", **defaults)
|
||||
|
||||
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
|
||||
# Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy
|
||||
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
|
||||
return cv.Required(name)
|
||||
return super().option(name, fallback)
|
||||
|
||||
|
||||
# Model: Good Display GDEY029Z95 2.9" BWR - 128x296 pixels, SSD1680 controller
|
||||
GoodDisplayBWR(
|
||||
"goodisplay-gdey029z95-2.9",
|
||||
width=128,
|
||||
height=296,
|
||||
minimum_update_interval="10s",
|
||||
)
|
||||
|
||||
@@ -46,6 +46,7 @@ from esphome.const import (
|
||||
CONF_TYPE,
|
||||
CONF_USE_ADDRESS,
|
||||
CONF_VALUE,
|
||||
CONF_WIFI,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
Platform,
|
||||
@@ -562,6 +563,25 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate_wifi(config: ConfigType) -> None:
|
||||
"""A wifi station competes with ethernet for the primary interface, so both must be in network: priority:."""
|
||||
full_config = fv.full_config.get()
|
||||
if (wifi_config := full_config.get(CONF_WIFI)) is None:
|
||||
return
|
||||
|
||||
from esphome.components.wifi import wifi_stays_ap_only
|
||||
|
||||
if wifi_stays_ap_only(wifi_config, full_config):
|
||||
return # an access point does not claim the primary interface
|
||||
|
||||
priority_ifaces = get_priority_interfaces_from_full_config(full_config)
|
||||
if missing := [i for i in (DOMAIN, CONF_WIFI) if i not in priority_ifaces]:
|
||||
raise cv.Invalid(
|
||||
"When ethernet and a wifi station are used together, 'network: priority:' "
|
||||
f"must list both interfaces; missing: {', '.join(missing)}"
|
||||
)
|
||||
|
||||
|
||||
def _final_validate_spi(config: ConfigType) -> None:
|
||||
if not CORE.is_esp32:
|
||||
return # SPI interface validation is ESP32-only
|
||||
@@ -846,22 +866,7 @@ def _final_validate_rmii_pins(config: ConfigType) -> None:
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
"""Final validation for Ethernet component."""
|
||||
# Allow ethernet + wifi coexistence only when both are declared in network: priority:.
|
||||
if "wifi" in fv.full_config.get():
|
||||
priority_ifaces = get_priority_interfaces_from_full_config(fv.full_config.get())
|
||||
missing = [i for i in ("ethernet", "wifi") if i not in priority_ifaces]
|
||||
if missing and priority_ifaces:
|
||||
# A priority list exists but is incomplete: point at what to add.
|
||||
raise cv.Invalid(
|
||||
"When ethernet and wifi are used together, 'network: priority:' must "
|
||||
f"list both interfaces; missing: {', '.join(missing)}"
|
||||
)
|
||||
if missing:
|
||||
raise cv.Invalid(
|
||||
"Component ethernet cannot be used together with component wifi "
|
||||
"unless both are listed under 'network: priority:'"
|
||||
)
|
||||
|
||||
_final_validate_wifi(config)
|
||||
_final_validate_spi(config)
|
||||
_final_validate_rmii_pins(config)
|
||||
|
||||
|
||||
@@ -175,6 +175,8 @@ void EzoPMP::read_command_result_() {
|
||||
third_parameter_buffer[position_in_parameter_buffer] = current_char;
|
||||
third_parameter_buffer[position_in_parameter_buffer + 1] = '\0';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
position_in_parameter_buffer++;
|
||||
}
|
||||
|
||||
@@ -194,6 +194,8 @@ uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
|
||||
ESP_LOGE(TAG, "Could not find fingerprint features");
|
||||
this->finger_scan_invalid_callback_.call();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return send_result;
|
||||
}
|
||||
|
||||
@@ -318,20 +318,21 @@ void HaierClimateBase::process_protocol_reset() {
|
||||
|
||||
bool HaierClimateBase::prepare_pending_action() {
|
||||
if (this->action_request_.has_value()) {
|
||||
switch (this->action_request_.value().action) {
|
||||
PendingAction &request = this->action_request_.value();
|
||||
switch (request.action) {
|
||||
case ActionRequest::SEND_CUSTOM_COMMAND:
|
||||
return true;
|
||||
case ActionRequest::TURN_POWER_ON:
|
||||
this->action_request_.value().message = this->get_power_message(true);
|
||||
request.message = this->get_power_message(true);
|
||||
return true;
|
||||
case ActionRequest::TURN_POWER_OFF:
|
||||
this->action_request_.value().message = this->get_power_message(false);
|
||||
request.message = this->get_power_message(false);
|
||||
return true;
|
||||
case ActionRequest::TOGGLE_POWER:
|
||||
this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
|
||||
request.message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
|
||||
return true;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action);
|
||||
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) request.action);
|
||||
this->action_request_.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1071,6 +1071,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
|
||||
this->fan_mode = CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
should_publish = should_publish || (!old_fan_mode.has_value()) ||
|
||||
(old_fan_mode.value_or(CLIMATE_FAN_ON) != this->fan_mode.value_or(CLIMATE_FAN_ON));
|
||||
@@ -1144,6 +1146,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
case (uint8_t) hon_protocol::ConditioningMode::AUTO:
|
||||
this->mode = CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
should_publish = should_publish || (old_mode != this->mode);
|
||||
|
||||
@@ -40,6 +40,8 @@ void decode_mitsubishi_heavy_zmp(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENG
|
||||
case MITSUBISHI_HEAVY_ZMP_ECONO:
|
||||
climate.preset = climate::CLIMATE_PRESET_ECO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t swing_h = frame[BYTE_SWING_H] & HORIZONTAL_SWING_MASK;
|
||||
|
||||
@@ -260,6 +260,8 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
|
||||
case HITACHI_AC344_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
@@ -294,6 +296,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
|
||||
case HITACHI_AC344_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -255,6 +255,8 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) {
|
||||
case HITACHI_AC424_MODE_FAN:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
@@ -289,6 +291,8 @@ bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) {
|
||||
case HITACHI_AC424_FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ size_t ImprovBase::get_formatted_next_url_(char *buffer, size_t buffer_size) {
|
||||
void ImprovBase::add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len) {
|
||||
// The builder rejects strings above 254 bytes, so anything longer than this
|
||||
// buffer could never be sent anyway
|
||||
char url_buffer[256];
|
||||
char url_buffer[256] = {};
|
||||
size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
|
||||
if (len == 0) {
|
||||
return;
|
||||
|
||||
@@ -61,6 +61,8 @@ void Kuntze::on_read_holding_registers(uint16_t start_address, std::span<const u
|
||||
if (this->oci_sensor_ != nullptr)
|
||||
this->oci_sensor_->publish_state(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -407,12 +407,10 @@ void LD2410Component::handle_periodic_data_() {
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
std::function<void()> set_number_value(number::Number *n, float value) {
|
||||
void set_number_value(number::Number *n, float value) {
|
||||
if (n != nullptr && (!n->has_state() || n->state != value)) {
|
||||
n->state = value;
|
||||
return [n, value]() { n->publish_state(value); };
|
||||
n->publish_state(value);
|
||||
}
|
||||
return []() {};
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -560,29 +558,24 @@ bool LD2410Component::handle_ack_data_() {
|
||||
Moving distance range: 13th byte
|
||||
Still distance range: 14th byte
|
||||
*/
|
||||
std::vector<std::function<void()>> updates;
|
||||
updates.push_back(set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]));
|
||||
updates.push_back(set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]));
|
||||
set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12]);
|
||||
set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13]);
|
||||
/*
|
||||
Moving Sensitivities: 15~23th bytes
|
||||
*/
|
||||
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) {
|
||||
updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]));
|
||||
for (size_t i = 0; i < TOTAL_GATES; i++) {
|
||||
set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i]);
|
||||
}
|
||||
/*
|
||||
Still Sensitivities: 24~32th bytes
|
||||
*/
|
||||
for (std::vector<number::Number *>::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) {
|
||||
updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]));
|
||||
for (size_t i = 0; i < TOTAL_GATES; i++) {
|
||||
set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i]);
|
||||
}
|
||||
/*
|
||||
None Duration: 33~34th bytes
|
||||
*/
|
||||
updates.push_back(
|
||||
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32])));
|
||||
for (auto &update : updates) {
|
||||
update();
|
||||
}
|
||||
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[33], this->buffer_data_[32]));
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -218,6 +218,8 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) {
|
||||
case 10:
|
||||
buf[i] = j;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class MCP23016 final : public Component, public i2c::I2CDevice, public gpio_expa
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(MCP23016 *arg);
|
||||
static void gpio_intr(MCP23016 *arg);
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -10,7 +10,7 @@ enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANG
|
||||
|
||||
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
|
||||
// ISR in flash.
|
||||
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg);
|
||||
void mcp23xxx_gpio_intr(Component *arg);
|
||||
|
||||
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
|
||||
public:
|
||||
|
||||
@@ -81,6 +81,8 @@ void Mcp4461Component::initialize_terminal_disabled(Mcp4461WiperIdx wiper, char
|
||||
case 'w':
|
||||
this->reg_[wiper_idx].terminal_w = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from collections.abc import Callable
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio
|
||||
from esphome.components.const import CONF_BITRATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
@@ -55,6 +56,30 @@ FORMAT_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
# MP3 constant bitrates in kbps; the encoder rounds anything else to the nearest one
|
||||
MP3_BITRATES = (
|
||||
8,
|
||||
16,
|
||||
24,
|
||||
32,
|
||||
40,
|
||||
48,
|
||||
56,
|
||||
64,
|
||||
80,
|
||||
96,
|
||||
112,
|
||||
128,
|
||||
144, # MPEG-2 only, at 16, 22.05 and 24 kHz
|
||||
160,
|
||||
192,
|
||||
224,
|
||||
256,
|
||||
320,
|
||||
)
|
||||
validate_mp3_bitrate = cv.one_of(*MP3_BITRATES, int=True)
|
||||
|
||||
|
||||
def build_supported_format_struct(
|
||||
format_config: ConfigType, purpose: MockObj
|
||||
) -> cg.StructInitializer:
|
||||
@@ -75,6 +100,10 @@ def build_supported_format_struct(
|
||||
if format_config[CONF_FORMAT] != "MP3":
|
||||
args.append(("sample_bytes", 2))
|
||||
|
||||
# Omitted means no preference, and the client keeps encoding as it always has.
|
||||
if bitrate := format_config.get(CONF_BITRATE):
|
||||
args.append(("bitrate", bitrate))
|
||||
|
||||
return cg.StructInitializer(*args)
|
||||
|
||||
|
||||
@@ -91,6 +120,14 @@ def validate_preferred_format(
|
||||
inherit_property_from(CONF_NUM_CHANNELS, audio_device_key)(config)
|
||||
inherit_property_from(CONF_SAMPLE_RATE, audio_device_key)(config)
|
||||
|
||||
# A constant bitrate is only meaningful for the one lossy format whose
|
||||
# encoder Home Assistant can be told to fix
|
||||
if CONF_BITRATE in config and config.get(CONF_FORMAT) != "MP3":
|
||||
raise cv.Invalid(
|
||||
f"{CONF_BITRATE} is only supported for the MP3 format",
|
||||
path=[CONF_BITRATE],
|
||||
)
|
||||
|
||||
# Opus only supports 48 kHz
|
||||
if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000:
|
||||
raise cv.Invalid("Opus only supports a sample rate of 48000 Hz")
|
||||
|
||||
@@ -77,6 +77,7 @@ struct MediaPlayerSupportedFormat {
|
||||
uint32_t num_channels;
|
||||
MediaPlayerFormatPurpose purpose;
|
||||
uint32_t sample_bytes;
|
||||
uint32_t bitrate;
|
||||
};
|
||||
|
||||
// Base features always reported for all media players
|
||||
|
||||
@@ -244,9 +244,9 @@ class MipiSpi : public display::Display,
|
||||
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
|
||||
// Don't spam the log after setup
|
||||
if (this->init_sequence_len_ == 0) {
|
||||
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
esph_log_v(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
} else {
|
||||
esph_log_d(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
esph_log_d(TAG, "Command %02X, length %zu, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
}
|
||||
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||
this->enable();
|
||||
|
||||
@@ -254,6 +254,8 @@ bool NoblexClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case IRNoblexMode::IR_NOBLEX_MODE_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 decoding a JPEG as it downloads
|
||||
static constexpr uint32_t STREAM_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 &&
|
||||
@@ -188,6 +205,7 @@ void OnlineImage::loop() {
|
||||
if (len > 0) {
|
||||
this->last_data_time_ = App.get_loop_component_start_time();
|
||||
this->download_buffer_.write(len);
|
||||
this->report_progress_(false);
|
||||
|
||||
// Feed data to decoder
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
@@ -255,6 +273,104 @@ 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) {
|
||||
const size_t available = std::min(this->download_buffer_.free_capacity(), this->download_buffer_initial_size_);
|
||||
if (available == 0) {
|
||||
// Waiting for the decoder, not the network
|
||||
this->last_data_time_ = now;
|
||||
} else {
|
||||
const int len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||
if (len > 0) {
|
||||
this->last_data_time_ = now;
|
||||
this->download_buffer_.write(len);
|
||||
this->report_progress_(false);
|
||||
} else 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 < STREAM_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 +404,8 @@ void OnlineImage::end_connection_() {
|
||||
this->downloader_ = nullptr;
|
||||
}
|
||||
this->download_buffer_.reset();
|
||||
this->streaming_ = false;
|
||||
this->stream_started_ = false;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
|
||||
@@ -70,9 +70,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 +89,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 +125,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
|
||||
|
||||
@@ -27,7 +27,7 @@ class PCA6416AComponent final : public Component,
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA6416AComponent *arg);
|
||||
static void gpio_intr(PCA6416AComponent *arg);
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -29,7 +29,7 @@ class PCA9505Component final : public Component,
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA9505Component *arg);
|
||||
static void gpio_intr(PCA9505Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -38,7 +38,7 @@ class PCA9554Component final : public Component,
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA9554Component *arg);
|
||||
static void gpio_intr(PCA9554Component *arg);
|
||||
|
||||
bool read_inputs_();
|
||||
bool write_register_(uint8_t reg, uint16_t value) { return this->write_address_(reg * this->reg_width_, value); }
|
||||
|
||||
@@ -33,7 +33,7 @@ class PCF8574Component final : public Component,
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCF8574Component *arg);
|
||||
static void gpio_intr(PCF8574Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -25,7 +25,7 @@ class PI4IOE5V6408Component final : public Component,
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PI4IOE5V6408Component *arg);
|
||||
static void gpio_intr(PI4IOE5V6408Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
|
||||
@@ -656,6 +656,8 @@ void Pipsolar::handle_qpiws_(const char *message) {
|
||||
this->publish_binary_sensor_(enabled, this->warning_battery_equalization_);
|
||||
value_warnings_present |= enabled.value_or(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class QMC5883LComponent final : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(QMC5883LComponent *arg);
|
||||
static void gpio_intr(QMC5883LComponent *arg);
|
||||
void read_sensor_();
|
||||
|
||||
QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ};
|
||||
|
||||
@@ -92,7 +92,17 @@ class JPEGFormat(Format):
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
|
||||
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
|
||||
# 1.8.4 plus decodeBegin()/decodeRows(), so an image can be decoded a few rows at a time and
|
||||
# can wait for input that has not arrived yet, and fixes for: input that arrives in short
|
||||
# reads, garbled RGB8888 output of grayscale images, and two ARM NEON faults seen on arm64 hosts
|
||||
# (swapped red and blue in the 4:2:0 RGB8888 path, writes past the row on clipped MCUs). It also
|
||||
# uses the ESP32-S3 SIMD colour conversion in ESP-IDF builds, not only Arduino ones, and is built
|
||||
# with -O2.
|
||||
cg.add_library(
|
||||
"JPEGDEC",
|
||||
"1.8.4",
|
||||
"https://github.com/esphome-libs/JPEGDEC#1.8.4-esphome.3",
|
||||
)
|
||||
if CORE.is_host:
|
||||
# JPEGDEC's host detection checks __MACH__/__LINUX__, but gcc only
|
||||
# predefines the lowercase __linux__; without this a Linux host
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/color.h"
|
||||
#include "image_format.h"
|
||||
|
||||
@@ -9,6 +11,14 @@ enum DecodeError : int {
|
||||
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
|
||||
DECODE_ERROR_OUT_OF_MEMORY = -3,
|
||||
DECODE_ERROR_INTERNAL_DECODER_ERROR = -4,
|
||||
DECODE_ERROR_READ = -5,
|
||||
};
|
||||
|
||||
/// Results of ImageDecoder::decode_step() other than a DecodeError
|
||||
enum DecodeStep : int {
|
||||
DECODE_STEP_DONE = 0,
|
||||
DECODE_STEP_MORE = 1,
|
||||
DECODE_STEP_NEED_DATA = 2, // The source has nothing more yet; call again once more has arrived
|
||||
};
|
||||
|
||||
constexpr const char *decode_error_to_string(int error) {
|
||||
@@ -21,6 +31,8 @@ constexpr const char *decode_error_to_string(int error) {
|
||||
return "Out of memory";
|
||||
case DECODE_ERROR_INTERNAL_DECODER_ERROR:
|
||||
return "Internal decoder error";
|
||||
case DECODE_ERROR_READ:
|
||||
return "Read error";
|
||||
default:
|
||||
return "Unknown error";
|
||||
}
|
||||
@@ -28,6 +40,19 @@ constexpr const char *decode_error_to_string(int error) {
|
||||
|
||||
class RuntimeImage;
|
||||
|
||||
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
|
||||
class DecodeSource {
|
||||
public:
|
||||
virtual ~DecodeSource() = default;
|
||||
/**
|
||||
* Read up to len bytes at the current position. Returns the number read, which is less than len, even 0, when
|
||||
* no more has arrived yet or at the end of the input, or negative on error.
|
||||
*/
|
||||
virtual int32_t read(uint8_t *buf, uint32_t len) = 0;
|
||||
/// Move to a position counted from the start. Returns false on error.
|
||||
virtual bool seek(uint32_t position) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Class to abstract decoding different image formats.
|
||||
*/
|
||||
@@ -58,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +113,28 @@ class ImageDecoder {
|
||||
*/
|
||||
virtual int decode(uint8_t *buffer, size_t size) = 0;
|
||||
|
||||
/// Whether this decoder can read a DecodeSource itself, a step at a time, instead of being fed data.
|
||||
virtual bool supports_source() const { return false; }
|
||||
|
||||
/**
|
||||
* @brief Start decoding from a source the decoder reads itself. Call after prepare(), in place of decode().
|
||||
*
|
||||
* The image headers (everything before the image data) must be readable from the source when this is called;
|
||||
* only decode_step() can wait for more to arrive. The source must hold expected_size bytes in the end, and stay
|
||||
* valid until the session ends.
|
||||
* @return int 0 on success, or a {@see DecodeError} value.
|
||||
*/
|
||||
virtual int begin_source(DecodeSource *source) { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
|
||||
|
||||
/**
|
||||
* @brief Decode a little more of an image started with begin_source(), keeping each call short.
|
||||
*
|
||||
* @return int DECODE_STEP_MORE while there is more to do, DECODE_STEP_NEED_DATA when the source has not
|
||||
* delivered enough to go on, DECODE_STEP_DONE when the image is complete, or a {@see DecodeError}
|
||||
* value.
|
||||
*/
|
||||
virtual int decode_step() { return DECODE_ERROR_UNSUPPORTED_FORMAT; }
|
||||
|
||||
/**
|
||||
* @brief Request the image to be resized once the actual dimensions are known.
|
||||
* Called by the callback functions, to be able to access the parent Image class.
|
||||
@@ -112,6 +159,9 @@ class ImageDecoder {
|
||||
*/
|
||||
void draw(int x, int y, int w, int h, const Color &color);
|
||||
|
||||
/// Bytes of the image the decoder has used so far
|
||||
size_t get_decoded_bytes() const { return this->decoded_bytes_; }
|
||||
|
||||
/**
|
||||
* @brief Check if the decoder has finished processing.
|
||||
*
|
||||
@@ -134,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));
|
||||
}
|
||||
@@ -135,13 +135,24 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
const int err = this->start_();
|
||||
if (err != 0)
|
||||
return err;
|
||||
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = size;
|
||||
this->jpeg_.close();
|
||||
return size;
|
||||
}
|
||||
|
||||
int JpegDecoder::start_() {
|
||||
auto jpeg_type = this->jpeg_.getJPEGType();
|
||||
if (jpeg_type == JPEG_MODE_INVALID) {
|
||||
ESP_LOGE(TAG, "Unsupported JPEG image");
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
} else if (jpeg_type == JPEG_MODE_PROGRESSIVE) {
|
||||
ESP_LOGE(TAG, "Progressive JPEG images not supported");
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
|
||||
|
||||
@@ -154,32 +165,133 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
} 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;
|
||||
}
|
||||
if (!this->jpeg_.decode(0, 0, 0)) {
|
||||
auto error = this->jpeg_.getLastError();
|
||||
ESP_LOGE(TAG, "Error while decoding: %d", error);
|
||||
this->jpeg_.close();
|
||||
switch (error) {
|
||||
case JPEG_ERROR_MEMORY:
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
case JPEG_UNSUPPORTED_FEATURE:
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
case JPEG_INVALID_FILE:
|
||||
case JPEG_INVALID_PARAMETER:
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
case JPEG_DECODE_ERROR:
|
||||
default:
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
}
|
||||
}
|
||||
this->decoded_bytes_ = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int JpegDecoder::fail_() {
|
||||
const bool read_failed = this->source_ != nullptr && this->source_failed_;
|
||||
const int error = this->jpeg_.getLastError();
|
||||
this->jpeg_.close();
|
||||
return size;
|
||||
this->source_ = nullptr;
|
||||
if (read_failed) {
|
||||
ESP_LOGE(TAG, "Reading the image failed");
|
||||
return DECODE_ERROR_READ;
|
||||
}
|
||||
ESP_LOGE(TAG, "Error while decoding: %d", error);
|
||||
switch (error) {
|
||||
case JPEG_ERROR_MEMORY:
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
case JPEG_UNSUPPORTED_FEATURE:
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
case JPEG_INVALID_FILE:
|
||||
case JPEG_INVALID_PARAMETER:
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
case JPEG_DECODE_ERROR:
|
||||
default:
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
void JpegDecoder::reset() {
|
||||
// A decode from a source that was given up part way still has the image open
|
||||
if (this->source_ != nullptr) {
|
||||
this->jpeg_.close();
|
||||
this->source_ = nullptr;
|
||||
}
|
||||
ImageDecoder::reset();
|
||||
}
|
||||
|
||||
int32_t JpegDecoder::source_read(JPEGFILE *file, uint8_t *buf, int32_t len) {
|
||||
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
|
||||
const int32_t n = decoder->source_->read(buf, len);
|
||||
if (n < 0) {
|
||||
// JPEGDEC has no way to report a failed read, so remember it and give it no data
|
||||
decoder->source_failed_ = true;
|
||||
return 0;
|
||||
}
|
||||
file->iPos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
int32_t JpegDecoder::source_seek(JPEGFILE *file, int32_t position) {
|
||||
auto *decoder = static_cast<JpegDecoder *>(file->fHandle);
|
||||
if (position < 0 || !decoder->source_->seek(position)) {
|
||||
decoder->source_failed_ = true;
|
||||
return file->iPos;
|
||||
}
|
||||
file->iPos = position;
|
||||
return position;
|
||||
}
|
||||
|
||||
int JpegDecoder::begin_source(DecodeSource *source) {
|
||||
if (this->expected_size_ == 0 || this->expected_size_ > INT32_MAX) {
|
||||
ESP_LOGE(TAG, "Image size %zu cannot be read from a source", this->expected_size_);
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
if (!source->seek(0)) {
|
||||
ESP_LOGE(TAG, "Could not read the image");
|
||||
return DECODE_ERROR_READ;
|
||||
}
|
||||
this->source_ = source;
|
||||
this->source_failed_ = false;
|
||||
if (!this->jpeg_.open(this, this->expected_size_, source_close, source_read, source_seek, draw_callback)) {
|
||||
// The caller may recover, for example by reading the whole image first, so it reports the failure
|
||||
ESP_LOGD(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
|
||||
this->source_ = nullptr;
|
||||
return this->source_failed_ ? DECODE_ERROR_READ : DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
const int err = this->start_();
|
||||
if (err != 0) {
|
||||
this->jpeg_.close();
|
||||
this->source_ = nullptr;
|
||||
return err;
|
||||
}
|
||||
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
|
||||
return this->fail_();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int HOT JpegDecoder::decode_step() {
|
||||
if (this->source_ == nullptr)
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
const int result = this->jpeg_.decodeRows(1);
|
||||
// JPEGDEC may decode on with the missing data, so check the read before its result
|
||||
if (this->source_failed_)
|
||||
return this->fail_();
|
||||
if (result == JPEG_DECODE_MORE)
|
||||
return DECODE_STEP_MORE;
|
||||
if (result == JPEG_DECODE_NEED_DATA)
|
||||
return DECODE_STEP_NEED_DATA;
|
||||
if (result == 0)
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = this->expected_size_;
|
||||
this->jpeg_.close();
|
||||
this->source_ = nullptr;
|
||||
return DECODE_STEP_DONE;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -22,19 +22,41 @@ class JpegDecoder : public ImageDecoder {
|
||||
~JpegDecoder() override {}
|
||||
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
void reset() override;
|
||||
|
||||
/// The source is read as the decode needs it, through JPEGDEC's own 2 KB buffer
|
||||
bool supports_source() const override { return true; }
|
||||
int begin_source(DecodeSource *source) override;
|
||||
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
|
||||
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_();
|
||||
|
||||
static int32_t source_read(JPEGFILE *file, uint8_t *buf, int32_t len);
|
||||
static int32_t source_seek(JPEGFILE *file, int32_t position);
|
||||
static void source_close(void *handle) {}
|
||||
|
||||
JPEGDEC jpeg_{};
|
||||
FixedVector<uint16_t> x_boundaries_{};
|
||||
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};
|
||||
};
|
||||
|
||||
} // 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;
|
||||
@@ -239,6 +241,25 @@ int RuntimeImage::feed_data(uint8_t *data, size_t len) {
|
||||
return consumed;
|
||||
}
|
||||
|
||||
int RuntimeImage::begin_source(DecodeSource *source) {
|
||||
if (!this->decoder_supports_source()) {
|
||||
ESP_LOGE(TAG, "Decoder cannot read a source");
|
||||
return DECODE_ERROR_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
return this->decoder_->begin_source(source);
|
||||
}
|
||||
|
||||
int RuntimeImage::decode_step() {
|
||||
if (!this->is_decoding()) {
|
||||
ESP_LOGE(TAG, "No decoder initialized");
|
||||
return DECODE_ERROR_INVALID_TYPE;
|
||||
}
|
||||
const int result = this->decoder_->decode_step();
|
||||
if (result == DECODE_STEP_DONE)
|
||||
this->decoded_bytes_ = this->decoder_->get_decoded_bytes();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool RuntimeImage::end_decode() {
|
||||
if (!this->is_decoding()) {
|
||||
return false;
|
||||
|
||||
@@ -76,6 +76,22 @@ class RuntimeImage : public image::Image {
|
||||
*/
|
||||
int feed_data(uint8_t *data, size_t len);
|
||||
|
||||
/// Whether the active decoder can read a DecodeSource itself (see begin_source()).
|
||||
bool decoder_supports_source() const { return this->is_decoding() && this->decoder_->supports_source(); }
|
||||
|
||||
/**
|
||||
* @brief Decode from a source the decoder reads itself, in place of feed_data().
|
||||
*
|
||||
* Call after begin_decode() when decoder_supports_source() is true, once the image headers can be read from the
|
||||
* source, then call decode_step() until it
|
||||
* returns DECODE_STEP_DONE, and finish with end_decode().
|
||||
* @return 0 on success, or a DecodeError value.
|
||||
*/
|
||||
int begin_source(DecodeSource *source);
|
||||
|
||||
/// Decode a little more from the source; see ImageDecoder::decode_step().
|
||||
int decode_step();
|
||||
|
||||
/**
|
||||
* @brief Complete the decoding process.
|
||||
*
|
||||
|
||||
@@ -39,13 +39,16 @@ from esphome.const import (
|
||||
CONF_REFERENCE_TEMPERATURE,
|
||||
CONF_SEND_EVERY,
|
||||
CONF_SEND_FIRST_AT,
|
||||
CONF_SENSOR_ID,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_TEMPERATURE_OFFSET,
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USE_FAHRENHEIT,
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WINDOW_SIZE,
|
||||
@@ -310,6 +313,7 @@ OrFilter = sensor_ns.class_("OrFilter", Filter)
|
||||
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
|
||||
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
|
||||
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
|
||||
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", Filter)
|
||||
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
|
||||
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
|
||||
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
|
||||
@@ -1281,6 +1285,23 @@ async def calibrate_ntc_temperature_filter_to_code(config, filter_id):
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"rh_correction",
|
||||
RHCorrectionFilter,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TEMPERATURE_OFFSET): cv.templatable(cv.float_),
|
||||
cv.Required(CONF_SENSOR_ID): cv.use_id(Sensor),
|
||||
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
async def rh_correction_filter_to_code(config: ConfigType, filter_id: ID) -> MockObj:
|
||||
sensor = await cg.get_variable(config[CONF_SENSOR_ID])
|
||||
offset = await cg.templatable(config[CONF_TEMPERATURE_OFFSET], [], cg.float_)
|
||||
return cg.new_Pvariable(filter_id, offset, sensor, config[CONF_USE_FAHRENHEIT])
|
||||
|
||||
|
||||
def _mean(xs):
|
||||
return sum(xs) / len(xs)
|
||||
|
||||
|
||||
@@ -493,6 +493,48 @@ optional<float> ToNTCTemperatureFilter::new_value(float value) {
|
||||
return temp;
|
||||
}
|
||||
|
||||
// RHCorrectionFilter
|
||||
static constexpr float MAGNUS_B = 17.67f, MAGNUS_C = 243.5f;
|
||||
|
||||
static bool rh_correction_temp_in_band(float t) { return t >= -40.0f && t <= 50.0f; }
|
||||
|
||||
optional<float> RHCorrectionFilter::new_value(float value) {
|
||||
if (!this->temperature_sensor_->has_state()) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): no temperature yet, passing humidity through uncorrected", this);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
float offset = this->temperature_offset_.value();
|
||||
float tc = this->temperature_sensor_->get_state();
|
||||
if (this->use_fahrenheit_) {
|
||||
tc = fahrenheit_to_celsius(tc);
|
||||
offset /= 1.8f;
|
||||
}
|
||||
const float tnc = tc - offset;
|
||||
// A NaN temperature or offset is a faulted input: keep the last corrected value
|
||||
if (std::isnan(tnc)) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): temperature or offset is NaN, dropping humidity updates", this);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (!rh_correction_temp_in_band(tc) || !rh_correction_temp_in_band(tnc)) {
|
||||
if (!this->warned_) {
|
||||
ESP_LOGW(TAG, "RHCorrectionFilter(%p): Temperature out of range -40..50 °C. Tc=%f Tnc=%f", this, tc, tnc);
|
||||
this->warned_ = true;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
this->warned_ = false;
|
||||
const float rh =
|
||||
clamp(value * std::exp(MAGNUS_B * MAGNUS_C * (tnc - tc) / ((tnc + MAGNUS_C) * (tc + MAGNUS_C))), 0.0f, 100.0f);
|
||||
ESP_LOGVV(TAG, "RHCorrectionFilter(%p)::new_value(%f) -> %f", this, value, rh);
|
||||
return rh;
|
||||
}
|
||||
|
||||
// StreamingFilter (base class)
|
||||
StreamingFilter::StreamingFilter(uint16_t window_size, uint16_t send_first_at)
|
||||
: window_size_(window_size), send_first_at_(send_first_at) {}
|
||||
|
||||
@@ -647,6 +647,26 @@ class ToNTCTemperatureFilter : public Filter {
|
||||
double c_;
|
||||
};
|
||||
|
||||
/** RH Temperature correction filter
|
||||
*
|
||||
* Allows for Relative Humidity Correction when a temperature offset is applied to the temperature sensor.
|
||||
* Uses the Magnus formula, rh = rh_nc * exp(b*(t_nc/(c+t_nc) - t/(c+t))) with b = 17.67, c = 243.5 °C;
|
||||
* see https://en.wikipedia.org/wiki/Dew_point
|
||||
*/
|
||||
class RHCorrectionFilter : public Filter {
|
||||
public:
|
||||
RHCorrectionFilter(TemplatableFn<float> offset, const Sensor *temperature_sensor, bool use_fahrenheit)
|
||||
: temperature_offset_(offset), temperature_sensor_(temperature_sensor), use_fahrenheit_(use_fahrenheit) {}
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
TemplatableFn<float> temperature_offset_;
|
||||
const Sensor *temperature_sensor_;
|
||||
bool use_fahrenheit_;
|
||||
bool warned_{false};
|
||||
};
|
||||
|
||||
/** Base class for streaming filters (batch windows where window_size == send_every).
|
||||
*
|
||||
* When window_size equals send_every, we don't need a sliding window.
|
||||
|
||||
@@ -48,6 +48,8 @@ void SmlTextSensor::publish_val(const ObisInfo &obis_info) {
|
||||
publish_state(reinterpret_cast<const char *>(obis_info.value.begin()), obis_info.value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.components import (
|
||||
)
|
||||
from esphome.components.const import (
|
||||
CONF_ANNOUNCEMENT_VOLUME,
|
||||
CONF_BITRATE,
|
||||
CONF_VOLUME_INCREMENT,
|
||||
CONF_VOLUME_INITIAL,
|
||||
CONF_VOLUME_MAX,
|
||||
@@ -107,6 +108,7 @@ PIPELINE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
|
||||
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
|
||||
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
|
||||
cv.Optional(CONF_BITRATE): media_player.validate_mp3_bitrate,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import audio, media_player, media_source, speaker
|
||||
from esphome.components.const import (
|
||||
CONF_ANNOUNCEMENT_VOLUME,
|
||||
CONF_BITRATE,
|
||||
CONF_SOURCES,
|
||||
CONF_VOLUME_INCREMENT,
|
||||
CONF_VOLUME_INITIAL,
|
||||
@@ -78,6 +79,7 @@ PIPELINE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
|
||||
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
|
||||
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
|
||||
cv.Optional(CONF_BITRATE): media_player.validate_mp3_bitrate,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ class ST25R : public PollingComponent, public nfc::Nfcc {
|
||||
void finish_scan_();
|
||||
void finalize_scan_();
|
||||
void remove_tag_();
|
||||
static void IRAM_ATTR isr(ST25R *arg);
|
||||
static void isr(ST25R *arg);
|
||||
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
InternalGPIOPin *irq_pin_{nullptr};
|
||||
|
||||
@@ -107,7 +107,7 @@ class SX126x final : public Component,
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(SX126x *arg);
|
||||
static void gpio_intr(SX126x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_packet_params_(uint8_t payload_length);
|
||||
|
||||
@@ -91,7 +91,7 @@ class SX127x final : public Component,
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(SX127x *arg);
|
||||
static void gpio_intr(SX127x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_mode_(uint8_t modulation, uint8_t mode);
|
||||
|
||||
@@ -58,7 +58,7 @@ class TCA8418Component : public key_provider::KeyProvider, public Component, pub
|
||||
#endif
|
||||
|
||||
/// Wakes the loop so queued events are read. Runs in interrupt context.
|
||||
static void IRAM_ATTR interrupt_handler(TCA8418Component *arg);
|
||||
static void interrupt_handler(TCA8418Component *arg);
|
||||
|
||||
protected:
|
||||
/// Write the matrix / individual-input pin configuration. Returns false on I2C failure.
|
||||
|
||||
@@ -27,7 +27,7 @@ class TCA9555Component final : public Component,
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(TCA9555Component *arg);
|
||||
static void gpio_intr(TCA9555Component *arg);
|
||||
// Virtual methods from GpioExpander base class — names come from base
|
||||
bool digital_read_hw(uint8_t pin) override; // NOLINT(readability-identifier-naming)
|
||||
bool digital_read_cache(uint8_t pin) override; // NOLINT(readability-identifier-naming)
|
||||
|
||||
@@ -191,6 +191,8 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case TCL112_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto temp = TCL112_TEMP_MAX - remote_state[7];
|
||||
|
||||
@@ -39,6 +39,8 @@ void TEM3200Component::setup() {
|
||||
ESP_LOGE(TAG, "STALE data. Data has not been updated since last fetch");
|
||||
this->status_set_warning();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +129,8 @@ void TEM3200Component::update() {
|
||||
ESP_LOGE(TAG, "Warning: STALE data. Data has not been updated since last fetch");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
float temperature = convert_temperature(raw_temperature);
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"""VNC (RFB) client that shows a remote screen on a display."""
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, network, socket
|
||||
from esphome.components.const import (
|
||||
BYTE_ORDER_LITTLE,
|
||||
CONF_HOST,
|
||||
CONF_RECONNECT_INTERVAL,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_DISPLAY_ID,
|
||||
CONF_ID,
|
||||
CONF_ON_CONNECT,
|
||||
CONF_ON_DISCONNECT,
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@MindFreeze"]
|
||||
DEPENDENCIES = ["display", "network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
DOMAIN = "vnc"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_MAX_UPDATE_DELAY = "max_update_delay"
|
||||
CONF_UPDATE_DELAY = "update_delay"
|
||||
|
||||
vnc_ns = cg.esphome_ns.namespace("vnc")
|
||||
VncClient = vnc_ns.class_("VncClient", cg.Component)
|
||||
|
||||
# Drivers that cannot take another component's pixels yet.
|
||||
UNSUPPORTED_DISPLAYS = dict.fromkeys(
|
||||
("mipi_dsi", "mipi_rgb", "mipi_spi"), "it is not supported by vnc yet"
|
||||
) | {
|
||||
"waveshare_epaper": "it draws every colour except pure black as ink; use epaper_spi"
|
||||
}
|
||||
|
||||
|
||||
def _register_writer(config: ConfigType) -> ConfigType:
|
||||
display.register_external_writer(config[CONF_DISPLAY_ID], DOMAIN)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(VncClient),
|
||||
cv.GenerateID(CONF_DISPLAY_ID): cv.use_id(display.Display),
|
||||
cv.Required(CONF_HOST): socket.ipv4_host,
|
||||
cv.Optional(CONF_PORT, default=5900): cv.port,
|
||||
cv.Optional(CONF_RECONNECT_INTERVAL, default="5s"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(min=cv.TimePeriod(seconds=1)),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_UPDATE_DELAY, default="200ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(
|
||||
CONF_MAX_UPDATE_DELAY, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_register_writer,
|
||||
socket.consume_sockets(1, DOMAIN),
|
||||
network.require_ipv4(DOMAIN),
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()
|
||||
disp_id = config[CONF_DISPLAY_ID]
|
||||
disp_conf = fv.get_declaring_config(disp_id)
|
||||
|
||||
if (platform := disp_conf.get(CONF_PLATFORM)) in UNSUPPORTED_DISPLAYS:
|
||||
raise cv.Invalid(
|
||||
f"Display '{disp_id}' uses the {platform} platform: "
|
||||
f"{UNSUPPORTED_DISPLAYS[platform]}",
|
||||
path=[CONF_DISPLAY_ID],
|
||||
)
|
||||
if (
|
||||
display.get_display_metadata(disp_id).has_writer
|
||||
or disp_conf.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"vnc draws the whole of display '{disp_id}'; remove lambda, pages, "
|
||||
"auto_clear_enabled and show_test_card from it",
|
||||
path=[CONF_DISPLAY_ID],
|
||||
)
|
||||
if len(writers := display.get_external_writers(disp_id)) > 1:
|
||||
raise cv.Invalid(
|
||||
f"Display '{disp_id}' is drawn by more than one component: {', '.join(writers)}",
|
||||
path=[CONF_DISPLAY_ID],
|
||||
)
|
||||
lvgl_configs = full_config.get("lvgl") or []
|
||||
if isinstance(lvgl_configs, dict):
|
||||
lvgl_configs = [lvgl_configs]
|
||||
for lvgl_config in lvgl_configs:
|
||||
if any(d.id == disp_id.id for d in lvgl_config.get("displays", [])):
|
||||
raise cv.Invalid(
|
||||
f"Display '{disp_id}' is also used by LVGL", path=[CONF_DISPLAY_ID]
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_CONNECT, "add_on_connect_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_DISCONNECT, "add_on_disconnect_callback"),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
disp = await cg.get_variable(config[CONF_DISPLAY_ID])
|
||||
# Ask the server for the display's own byte order, so block-copying drivers skip the per-pixel path
|
||||
meta = display.get_display_metadata(config[CONF_DISPLAY_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID], disp, meta.byte_order != BYTE_ORDER_LITTLE)
|
||||
await cg.register_component(var, config)
|
||||
socket.require_tcp_client_link()
|
||||
cg.add(var.set_host(config[CONF_HOST]))
|
||||
cg.add(var.set_port(config[CONF_PORT]))
|
||||
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]))
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
VNC_ID_SCHEMA = automation.maybe_simple_id(
|
||||
{cv.GenerateID(): cv.use_id(VncClient)},
|
||||
)
|
||||
|
||||
automation.register_apply_condition("vnc.is_connected", VNC_ID_SCHEMA, "is_connected()")
|
||||
@@ -0,0 +1,702 @@
|
||||
#include "vnc_client.h"
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "vnc");
|
||||
|
||||
// Below the 50 ms blocking warning, and the same slice it8951 uses for its transfers.
|
||||
static constexpr uint32_t LOOP_BUDGET_MS = 20;
|
||||
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
|
||||
static constexpr uint32_t STABLE_SESSION_MS = 60000;
|
||||
static constexpr uint32_t MAX_BACKOFF_MS = 60000;
|
||||
static constexpr uint32_t CONFIG_RETRY_MS = 300000;
|
||||
static constexpr uint32_t HEARTBEAT_MS = 60000;
|
||||
static constexpr uint32_t MIN_REQUEST_SPACING_MS = 50;
|
||||
// The longest the first refresh waits for a display with a loop to park once after boot.
|
||||
static constexpr uint32_t DISPLAY_START_MS = 5000;
|
||||
// Longer than any driver's own busy timeout: a panel that stays busy this long is stuck
|
||||
static constexpr uint32_t DISPLAY_BUSY_TIMEOUT_MS = 120000;
|
||||
static constexpr uint16_t MAX_CURSOR_SIDE = 1024;
|
||||
static constexpr uint32_t MAX_TEXT_LEN = 63;
|
||||
|
||||
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_RICH_CURSOR = -239;
|
||||
|
||||
static constexpr uint8_t MSG_FRAMEBUFFER_UPDATE = 0;
|
||||
static constexpr uint8_t MSG_SET_COLOUR_MAP = 1;
|
||||
static constexpr uint8_t MSG_BELL = 2;
|
||||
static constexpr uint8_t MSG_SERVER_CUT_TEXT = 3;
|
||||
|
||||
static inline uint16_t read_be16(const uint8_t *p) { return encode_uint16(p[0], p[1]); }
|
||||
static inline uint32_t read_be32(const uint8_t *p) { return encode_uint32(p[0], p[1], p[2], p[3]); }
|
||||
static inline void write_be16(uint8_t *p, uint16_t v) {
|
||||
p[0] = v >> 8;
|
||||
p[1] = v;
|
||||
}
|
||||
static inline void write_be32(uint8_t *p, uint32_t v) {
|
||||
p[0] = v >> 24;
|
||||
p[1] = v >> 16;
|
||||
p[2] = v >> 8;
|
||||
p[3] = v;
|
||||
}
|
||||
|
||||
static int parse_three_digits(const uint8_t *p) {
|
||||
int value = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (p[i] < '0' || p[i] > '9') {
|
||||
return -1;
|
||||
}
|
||||
value = value * 10 + (p[i] - '0');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void VncClient::setup() {
|
||||
this->setup_ms_ = App.get_loop_component_start_time();
|
||||
this->link_.begin(TAG);
|
||||
}
|
||||
|
||||
void VncClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"VNC:\n"
|
||||
" Server: %s:%u\n"
|
||||
" Reconnect Interval: %" PRIu32 "ms\n"
|
||||
" Update Delay: %" PRIu32 "ms\n"
|
||||
" Max Update Delay: %" PRIu32 "ms",
|
||||
this->link_.host(), this->link_.port(), this->link_.reconnect_interval(), this->update_delay_ms_,
|
||||
this->max_update_delay_ms_);
|
||||
}
|
||||
|
||||
void VncClient::on_shutdown() { this->link_.close(); }
|
||||
|
||||
void VncClient::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->pass_start_ms_ = millis();
|
||||
if (this->display_->is_failed()) {
|
||||
this->link_.close(); // a connect still in flight would otherwise keep its socket for good
|
||||
if (this->state_ != VncState::VNC_STATE_IDLE) {
|
||||
this->session_end_(now, VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Display failed"));
|
||||
}
|
||||
this->status_set_error(LOG_STR("Display failed"));
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
// 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.
|
||||
if (this->display_->is_idle()) {
|
||||
this->display_parks_ = true;
|
||||
this->display_busy_ = false;
|
||||
} else {
|
||||
this->display_busy_ = this->display_parks_;
|
||||
}
|
||||
// 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;
|
||||
if (this->busy_warned_) {
|
||||
this->busy_warned_ = false;
|
||||
// The connection path owns the warning while no session runs
|
||||
if (this->state_ == VncState::VNC_STATE_RUNNING) {
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
} else if (!this->busy_warned_ && now - this->busy_since_ms_ >= DISPLAY_BUSY_TIMEOUT_MS) {
|
||||
this->busy_warned_ = true;
|
||||
this->status_set_warning(LOG_STR("Display busy too long"));
|
||||
}
|
||||
|
||||
if (!this->link_.connected()) {
|
||||
if (this->state_ != VncState::VNC_STATE_IDLE) {
|
||||
this->session_lost_(now);
|
||||
}
|
||||
this->connect_step_(now);
|
||||
if (!this->link_.connected()) {
|
||||
return;
|
||||
}
|
||||
this->start_handshake_(now);
|
||||
}
|
||||
if (this->state_ == VncState::VNC_STATE_HANDSHAKE && now - this->session_start_ms_ >= HANDSHAKE_TIMEOUT_MS) {
|
||||
this->fail_(this->lost_failure_(), LOG_STR("Handshake timed out"));
|
||||
return;
|
||||
}
|
||||
|
||||
const bool blocked = this->in_update_ && this->display_busy_;
|
||||
// A display still starting up (it8951) can go from its start-up straight into a refresh without parking.
|
||||
const bool display_starting =
|
||||
!this->display_parks_ && this->display_->has_overridden_loop() && now - this->setup_ms_ < DISPLAY_START_MS;
|
||||
// Wait until the screen stops changing, so an animation becomes one refresh showing where it ended.
|
||||
if (this->refresh_pending_ && !this->in_update_ && !this->display_busy_ && !display_starting &&
|
||||
(now - this->last_change_ms_ >= this->update_delay_ms_ ||
|
||||
now - this->first_change_ms_ >= this->max_update_delay_ms_)) {
|
||||
// Synchronous displays flush inside update(), so this pass does nothing else.
|
||||
this->refresh_pending_ = false;
|
||||
this->display_->update();
|
||||
this->display_busy_ = this->display_parks_ && !this->display_->is_idle();
|
||||
} else if (!blocked && (this->rx_pending_ || this->link_.ready())) {
|
||||
this->service_rx_(now);
|
||||
}
|
||||
if (this->state_ == VncState::VNC_STATE_RUNNING) {
|
||||
// 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"));
|
||||
return;
|
||||
}
|
||||
this->pace_(now);
|
||||
}
|
||||
this->link_.flush_tx();
|
||||
if (this->rx_pending_ && !(this->in_update_ && this->display_busy_)) {
|
||||
this->high_freq_.start();
|
||||
} else {
|
||||
this->high_freq_.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::connect_step_(uint32_t now) {
|
||||
const bool up = network::is_connected();
|
||||
if (up && !this->network_was_up_) {
|
||||
this->backoff_ms_ = 0;
|
||||
this->hold_ms_ = 0;
|
||||
}
|
||||
this->network_was_up_ = up;
|
||||
if (!up) {
|
||||
return;
|
||||
}
|
||||
if (!this->link_.connecting() && (now - this->hold_start_ms_ < this->hold_ms_ || this->link_.in_backoff())) {
|
||||
return;
|
||||
}
|
||||
this->link_.poll();
|
||||
// DNS failures, refused connects and connect timeouts all leave no socket and start the link's backoff.
|
||||
if (!this->link_.connected() && !this->link_.connecting() && this->link_.in_backoff()) {
|
||||
this->session_end_(now, VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Cannot reach the server"));
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::reset_session_() {
|
||||
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->frame_drawn_ = false;
|
||||
this->step_ = VncStep::VNC_STEP_VERSION;
|
||||
this->reason_failure_ = VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
this->high_freq_.stop();
|
||||
}
|
||||
|
||||
void VncClient::start_handshake_(uint32_t now) {
|
||||
this->reset_session_();
|
||||
this->state_ = VncState::VNC_STATE_HANDSHAKE;
|
||||
this->session_start_ms_ = now;
|
||||
this->last_rx_ms_ = now;
|
||||
}
|
||||
|
||||
VncFailure VncClient::lost_failure_() const {
|
||||
if (this->state_ == VncState::VNC_STATE_HANDSHAKE) {
|
||||
if (this->step_ == VncStep::VNC_STEP_REASON_LENGTH || this->step_ == VncStep::VNC_STEP_REASON) {
|
||||
return this->reason_failure_;
|
||||
}
|
||||
}
|
||||
return VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
}
|
||||
|
||||
void VncClient::session_lost_(uint32_t now) {
|
||||
this->session_end_(now, this->lost_failure_(), LOG_STR("Connection lost"));
|
||||
}
|
||||
|
||||
VncParse VncClient::fail_(VncFailure failure, const LogString *reason) {
|
||||
this->link_.close();
|
||||
this->link_.note_attempt();
|
||||
this->session_end_(App.get_loop_component_start_time(), failure, reason);
|
||||
return VncParse::VNC_PARSE_STOP;
|
||||
}
|
||||
|
||||
void VncClient::session_end_(uint32_t now, VncFailure failure, const LogString *reason) {
|
||||
const bool was_running = this->state_ == VncState::VNC_STATE_RUNNING;
|
||||
this->reset_session_();
|
||||
this->state_ = VncState::VNC_STATE_IDLE;
|
||||
this->hold_start_ms_ = now;
|
||||
if (was_running) {
|
||||
const uint32_t lasted = now - this->session_start_ms_;
|
||||
ESP_LOGW(TAG, "%s after %" PRIu32 "s", LOG_STR_ARG(reason), lasted / 1000);
|
||||
this->disconnect_callback_.call();
|
||||
this->status_set_warning();
|
||||
if (lasted >= STABLE_SESSION_MS) {
|
||||
this->backoff_ms_ = 0;
|
||||
this->hold_ms_ = 0;
|
||||
return;
|
||||
}
|
||||
failure = VncFailure::VNC_FAILURE_TRANSIENT;
|
||||
}
|
||||
switch (failure) {
|
||||
case VncFailure::VNC_FAILURE_CONFIG:
|
||||
this->hold_ms_ = CONFIG_RETRY_MS;
|
||||
ESP_LOGE(TAG, "%s; retrying in %" PRIu32 "s", LOG_STR_ARG(reason), this->hold_ms_ / 1000);
|
||||
this->status_set_error(reason);
|
||||
return;
|
||||
default: {
|
||||
const uint32_t base = this->link_.reconnect_interval();
|
||||
this->backoff_ms_ =
|
||||
this->backoff_ms_ == 0 ? base : std::min(this->backoff_ms_ * 2, std::max(MAX_BACKOFF_MS, base));
|
||||
this->hold_ms_ = this->backoff_ms_;
|
||||
if (!was_running) {
|
||||
ESP_LOGW(TAG, "%s; retrying in %" PRIu32 "s", LOG_STR_ARG(reason), this->hold_ms_ / 1000);
|
||||
}
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool VncClient::budget_exhausted_() const { return millis() - this->pass_start_ms_ >= LOOP_BUDGET_MS; }
|
||||
|
||||
void VncClient::service_rx_(uint32_t now) {
|
||||
for (;;) {
|
||||
VncParse result = this->parse_();
|
||||
if (result == VncParse::VNC_PARSE_STOP) {
|
||||
return;
|
||||
}
|
||||
if (result == VncParse::VNC_PARSE_PAUSE) {
|
||||
this->rx_pending_ = true;
|
||||
return;
|
||||
}
|
||||
// Less than one item is left, so the buffer always has room after compacting.
|
||||
if (this->rx_pos_ != 0) {
|
||||
this->rx_len_ -= this->rx_pos_;
|
||||
std::memmove(this->rx_, this->rx_ + this->rx_pos_, this->rx_len_);
|
||||
this->rx_pos_ = 0;
|
||||
}
|
||||
if (this->budget_exhausted_()) {
|
||||
this->rx_pending_ = true;
|
||||
return;
|
||||
}
|
||||
ssize_t count = this->link_.read(this->rx_ + this->rx_len_, RX_BUFFER_SIZE - this->rx_len_);
|
||||
if (count < 0) {
|
||||
this->session_lost_(now);
|
||||
return;
|
||||
}
|
||||
if (count == 0) {
|
||||
this->rx_pending_ = false;
|
||||
return;
|
||||
}
|
||||
this->rx_len_ += static_cast<uint16_t>(count);
|
||||
this->rx_total_ += count;
|
||||
this->last_rx_ms_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::skip_(uint32_t count, VncStep next) {
|
||||
this->skip_left_ = count;
|
||||
this->after_skip_ = next;
|
||||
this->step_ = VncStep::VNC_STEP_SKIP;
|
||||
}
|
||||
|
||||
bool VncClient::send_client_init_() {
|
||||
const uint8_t shared = 1;
|
||||
return this->send_(&shared, 1);
|
||||
}
|
||||
|
||||
VncParse VncClient::parse_() {
|
||||
for (;;) {
|
||||
const uint8_t *p = this->rx_ + this->rx_pos_;
|
||||
const size_t avail = this->rx_len_ - this->rx_pos_;
|
||||
switch (this->step_) {
|
||||
case VncStep::VNC_STEP_VERSION: {
|
||||
if (avail < 12) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const int major = parse_three_digits(p + 4);
|
||||
const int minor = parse_three_digits(p + 8);
|
||||
if (std::memcmp(p, "RFB ", 4) != 0 || p[7] != '.' || p[11] != '\n' || major < 0 || minor < 0) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, LOG_STR("Not a VNC server"));
|
||||
}
|
||||
if (major < 3) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, LOG_STR("Unsupported RFB version"));
|
||||
}
|
||||
this->minor_ = (major > 3 || minor >= 8) ? 8 : (minor == 7 ? 7 : 3);
|
||||
this->rx_pos_ += 12;
|
||||
uint8_t reply[12] = {'R', 'F', 'B', ' ', '0', '0', '3', '.', '0', '0', '8', '\n'};
|
||||
reply[10] = '0' + this->minor_;
|
||||
if (!this->send_(reply, sizeof(reply))) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = this->minor_ == 3 ? VncStep::VNC_STEP_SECURITY_TYPE_33 : VncStep::VNC_STEP_SECURITY_COUNT;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SECURITY_COUNT: {
|
||||
if (avail < 1) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->sec_count_ = p[0];
|
||||
this->rx_pos_ += 1;
|
||||
if (this->sec_count_ == 0) {
|
||||
// A refusal before authentication, such as a lockout caused by another client.
|
||||
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
|
||||
} else {
|
||||
this->step_ = VncStep::VNC_STEP_SECURITY_LIST;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SECURITY_LIST: {
|
||||
if (avail < this->sec_count_) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
uint8_t chosen = 0;
|
||||
bool wants_password = false;
|
||||
for (uint8_t i = 0; i < this->sec_count_; i++) {
|
||||
if (p[i] == SECURITY_NONE) {
|
||||
chosen = p[i];
|
||||
break;
|
||||
}
|
||||
wants_password |= p[i] == SECURITY_VNC_AUTH;
|
||||
}
|
||||
this->rx_pos_ += this->sec_count_;
|
||||
if (chosen == 0) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, wants_password ? LOG_STR("The server requires a password")
|
||||
: LOG_STR("No supported security type"));
|
||||
}
|
||||
if (!this->send_(&chosen, 1)) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
if (this->minor_ == 8) {
|
||||
this->step_ = VncStep::VNC_STEP_SECURITY_RESULT;
|
||||
} else {
|
||||
if (!this->send_client_init_()) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SECURITY_TYPE_33: {
|
||||
if (avail < 4) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const uint32_t type = read_be32(p);
|
||||
this->rx_pos_ += 4;
|
||||
if (type == 0) {
|
||||
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
|
||||
} else if (type == SECURITY_NONE) {
|
||||
if (!this->send_client_init_()) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
|
||||
} else {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_CONFIG, type == SECURITY_VNC_AUTH
|
||||
? LOG_STR("The server requires a password")
|
||||
: LOG_STR("No supported security type"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SECURITY_RESULT: {
|
||||
if (avail < 4) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
const uint32_t result = read_be32(p);
|
||||
this->rx_pos_ += 4;
|
||||
if (result == 0) {
|
||||
if (!this->send_client_init_()) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = VncStep::VNC_STEP_SERVER_INIT;
|
||||
break;
|
||||
}
|
||||
this->reason_failure_ = VncFailure::VNC_FAILURE_CONFIG;
|
||||
this->step_ = VncStep::VNC_STEP_REASON_LENGTH;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_REASON_LENGTH: {
|
||||
if (avail < 4) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->text_len_ = static_cast<uint8_t>(std::min(read_be32(p), MAX_TEXT_LEN));
|
||||
this->rx_pos_ += 4;
|
||||
this->step_ = VncStep::VNC_STEP_REASON;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_REASON: {
|
||||
if (avail < this->text_len_) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
ESP_LOGE(TAG, "Server: %.*s", static_cast<int>(this->text_len_), reinterpret_cast<const char *>(p));
|
||||
return this->fail_(this->reason_failure_, LOG_STR("Connection refused"));
|
||||
}
|
||||
case VncStep::VNC_STEP_SERVER_INIT: {
|
||||
if (avail < 24) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->fb_w_ = read_be16(p);
|
||||
this->fb_h_ = read_be16(p + 2);
|
||||
// Reused for the length of the name that follows.
|
||||
this->skip_left_ = read_be32(p + 20);
|
||||
this->rx_pos_ += 24;
|
||||
if (this->fb_w_ == 0 || this->fb_h_ == 0) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("The server screen is empty"));
|
||||
}
|
||||
this->text_len_ = static_cast<uint8_t>(std::min(this->skip_left_, MAX_TEXT_LEN));
|
||||
this->step_ = VncStep::VNC_STEP_SERVER_NAME;
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SERVER_NAME: {
|
||||
if (avail < this->text_len_) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
ESP_LOGI(TAG, "Server '%.*s', %ux%u, RFB 3.%u", static_cast<int>(this->text_len_),
|
||||
reinterpret_cast<const char *>(p), this->fb_w_, this->fb_h_, this->minor_);
|
||||
this->rx_pos_ += this->text_len_;
|
||||
this->skip_(this->skip_left_ - this->text_len_, VncStep::VNC_STEP_SESSION_START);
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_SESSION_START:
|
||||
if (!this->start_session_(App.get_loop_component_start_time())) {
|
||||
return this->fail_send_();
|
||||
}
|
||||
this->step_ = VncStep::VNC_STEP_MESSAGE;
|
||||
break;
|
||||
case VncStep::VNC_STEP_MESSAGE: {
|
||||
if (avail < 1) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
switch (p[0]) {
|
||||
case MSG_FRAMEBUFFER_UPDATE:
|
||||
if (avail < 4) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->rects_left_ = read_be16(p + 2);
|
||||
this->rx_pos_ += 4;
|
||||
this->in_update_ = true;
|
||||
this->update_start_ms_ = millis();
|
||||
this->update_start_bytes_ = this->rx_total_ - (this->rx_len_ - this->rx_pos_);
|
||||
this->rect_done_();
|
||||
break;
|
||||
case MSG_SET_COLOUR_MAP:
|
||||
if (avail < 6) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->rx_pos_ += 6;
|
||||
this->skip_(6u * read_be16(p + 4), VncStep::VNC_STEP_MESSAGE);
|
||||
break;
|
||||
case MSG_BELL:
|
||||
this->rx_pos_ += 1;
|
||||
break;
|
||||
case MSG_SERVER_CUT_TEXT:
|
||||
if (avail < 8) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->rx_pos_ += 8;
|
||||
this->skip_(read_be32(p + 4), VncStep::VNC_STEP_MESSAGE);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown message type %u", p[0]);
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_RECT: {
|
||||
if (this->display_busy_) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
if (avail < 12) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->rect_x_ = read_be16(p);
|
||||
this->rect_y_ = read_be16(p + 2);
|
||||
this->rect_w_ = read_be16(p + 4);
|
||||
this->rect_h_ = read_be16(p + 6);
|
||||
const auto encoding = static_cast<int32_t>(read_be32(p + 8));
|
||||
this->rx_pos_ += 12;
|
||||
this->rects_left_--;
|
||||
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_,
|
||||
this->rect_x_, this->rect_y_, this->fb_w_, this->fb_h_);
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
|
||||
}
|
||||
if (this->rect_w_ == 0 || this->rect_h_ == 0) {
|
||||
this->rect_done_();
|
||||
} else {
|
||||
this->pos_x_ = 0;
|
||||
this->pos_y_ = 0;
|
||||
this->step_ = VncStep::VNC_STEP_RAW;
|
||||
}
|
||||
} else if (encoding == ENCODING_RICH_CURSOR) {
|
||||
if (this->rect_w_ > MAX_CURSOR_SIDE || this->rect_h_ > MAX_CURSOR_SIDE) {
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
|
||||
}
|
||||
const uint32_t size = static_cast<uint32_t>(this->rect_w_) * this->rect_h_ * this->bpp_ +
|
||||
((this->rect_w_ + 7u) / 8u) * this->rect_h_;
|
||||
this->skip_(size, VncStep::VNC_STEP_RECT_DONE);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unsupported encoding %" PRId32, encoding);
|
||||
return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Protocol error"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case VncStep::VNC_STEP_RECT_DONE:
|
||||
this->rect_done_();
|
||||
break;
|
||||
case VncStep::VNC_STEP_RAW: {
|
||||
if (this->display_busy_) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
const size_t pixels = avail / this->bpp_;
|
||||
if (pixels == 0) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
if (this->budget_exhausted_()) {
|
||||
return VncParse::VNC_PARSE_PAUSE;
|
||||
}
|
||||
const auto count = static_cast<uint16_t>(std::min<size_t>(pixels, this->rect_w_ - this->pos_x_));
|
||||
this->draw_row_(this->rect_x_ + this->pos_x_, this->rect_y_ + this->pos_y_, count, p);
|
||||
this->rx_pos_ += count * this->bpp_;
|
||||
this->pos_x_ += count;
|
||||
if (this->pos_x_ == this->rect_w_) {
|
||||
this->pos_x_ = 0;
|
||||
if (++this->pos_y_ == this->rect_h_) {
|
||||
this->rect_done_();
|
||||
}
|
||||
}
|
||||
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;
|
||||
this->skip_left_ -= count;
|
||||
if (this->skip_left_ != 0) {
|
||||
return VncParse::VNC_PARSE_MORE;
|
||||
}
|
||||
this->step_ = this->after_skip_;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::rect_done_() {
|
||||
if (this->rects_left_ == 0) {
|
||||
this->frame_done_();
|
||||
} else {
|
||||
this->step_ = VncStep::VNC_STEP_RECT;
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::frame_done_() {
|
||||
this->in_update_ = false;
|
||||
this->request_outstanding_ = false;
|
||||
this->step_ = VncStep::VNC_STEP_MESSAGE;
|
||||
if (this->frame_drawn_) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->frame_drawn_ = false;
|
||||
if (!this->refresh_pending_) {
|
||||
this->first_change_ms_ = now;
|
||||
}
|
||||
this->last_change_ms_ = now;
|
||||
this->refresh_pending_ = true;
|
||||
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_);
|
||||
}
|
||||
}
|
||||
|
||||
void VncClient::draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels) {
|
||||
if (y >= this->vis_h_ || x >= this->vis_w_) {
|
||||
return;
|
||||
}
|
||||
const int visible = std::min<int>(count, this->vis_w_ - x);
|
||||
this->display_->draw_pixels_at(x, y, visible, 1, pixels, this->bitness_, this->big_endian_);
|
||||
this->frame_drawn_ = true;
|
||||
}
|
||||
|
||||
bool VncClient::start_session_(uint32_t now) {
|
||||
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;
|
||||
|
||||
const int disp_w = this->display_->get_width();
|
||||
const int disp_h = this->display_->get_height();
|
||||
this->vis_w_ = std::min<int>(this->fb_w_, disp_w);
|
||||
this->vis_h_ = std::min<int>(this->fb_h_, disp_h);
|
||||
if (this->fb_w_ != disp_w || this->fb_h_ != disp_h) {
|
||||
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_);
|
||||
}
|
||||
|
||||
// SetPixelFormat: true colour, in the byte order the display takes
|
||||
uint8_t pixel_format[20] = {};
|
||||
pixel_format[4] = rgb565 ? 16 : 8;
|
||||
pixel_format[5] = rgb565 ? 16 : 8;
|
||||
pixel_format[6] = this->big_endian_;
|
||||
pixel_format[7] = 1;
|
||||
write_be16(pixel_format + 8, rgb565 ? 31 : 7);
|
||||
write_be16(pixel_format + 10, rgb565 ? 63 : 7);
|
||||
write_be16(pixel_format + 12, rgb565 ? 31 : 3);
|
||||
pixel_format[14] = rgb565 ? 11 : 5;
|
||||
pixel_format[15] = rgb565 ? 5 : 2;
|
||||
if (!this->send_(pixel_format, sizeof(pixel_format))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
this->state_ = VncState::VNC_STATE_RUNNING;
|
||||
this->session_start_ms_ = now;
|
||||
if (!this->busy_warned_) {
|
||||
this->status_clear_warning(); // a stuck display keeps its warning through a reconnect
|
||||
}
|
||||
this->status_clear_error();
|
||||
this->connect_callback_.call();
|
||||
return true;
|
||||
}
|
||||
|
||||
void VncClient::pace_(uint32_t now) {
|
||||
if (this->display_busy_ || this->in_update_ || now - this->last_request_ms_ < MIN_REQUEST_SPACING_MS) {
|
||||
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)) {
|
||||
this->send_update_request_(true, now);
|
||||
}
|
||||
}
|
||||
|
||||
bool VncClient::send_(const uint8_t *data, size_t len) {
|
||||
if (this->link_.tx_free() < len) {
|
||||
this->link_.flush_tx();
|
||||
if (this->link_.tx_free() < len) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->link_.queue(data, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VncClient::send_update_request_(bool incremental, uint32_t now) {
|
||||
uint8_t msg[10] = {3, static_cast<uint8_t>(incremental ? 1 : 0)};
|
||||
write_be16(msg + 6, this->vis_w_);
|
||||
write_be16(msg + 8, this->vis_h_);
|
||||
if (!this->send_(msg, sizeof(msg))) {
|
||||
return false;
|
||||
}
|
||||
this->request_outstanding_ = true;
|
||||
this->last_request_ms_ = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::vnc
|
||||
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/socket/tcp_client_link.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::vnc {
|
||||
|
||||
enum class VncState : uint8_t {
|
||||
VNC_STATE_IDLE,
|
||||
VNC_STATE_HANDSHAKE,
|
||||
VNC_STATE_RUNNING,
|
||||
};
|
||||
|
||||
enum class VncStep : uint8_t {
|
||||
VNC_STEP_VERSION,
|
||||
VNC_STEP_SECURITY_COUNT,
|
||||
VNC_STEP_SECURITY_LIST,
|
||||
VNC_STEP_SECURITY_TYPE_33,
|
||||
VNC_STEP_SECURITY_RESULT,
|
||||
VNC_STEP_REASON_LENGTH,
|
||||
VNC_STEP_REASON,
|
||||
VNC_STEP_SERVER_INIT,
|
||||
VNC_STEP_SERVER_NAME,
|
||||
VNC_STEP_SESSION_START,
|
||||
VNC_STEP_MESSAGE,
|
||||
VNC_STEP_RECT,
|
||||
VNC_STEP_RECT_DONE,
|
||||
VNC_STEP_RAW,
|
||||
VNC_STEP_SKIP,
|
||||
};
|
||||
|
||||
enum class VncParse : uint8_t {
|
||||
VNC_PARSE_MORE, // the next step needs bytes that have not arrived
|
||||
VNC_PARSE_PAUSE, // out of time, or the display is busy; resume without new data
|
||||
VNC_PARSE_STOP, // the session ended
|
||||
};
|
||||
|
||||
enum class VncFailure : uint8_t {
|
||||
VNC_FAILURE_TRANSIENT,
|
||||
VNC_FAILURE_CONFIG,
|
||||
};
|
||||
|
||||
/// RFB (VNC) client that draws the remote framebuffer into a display.
|
||||
class VncClient : public Component {
|
||||
public:
|
||||
explicit VncClient(display::Display *display, bool big_endian = false) : display_(display), big_endian_(big_endian) {}
|
||||
|
||||
void set_host(const char *host) { this->link_.set_host(host); }
|
||||
void set_port(uint16_t port) { this->link_.set_port(port); }
|
||||
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 setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
bool is_connected() const { return this->state_ == VncState::VNC_STATE_RUNNING; }
|
||||
|
||||
template<typename F> void add_on_connect_callback(F &&callback) {
|
||||
this->connect_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_disconnect_callback(F &&callback) {
|
||||
this->disconnect_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr size_t RX_BUFFER_SIZE = 512;
|
||||
|
||||
void connect_step_(uint32_t now);
|
||||
void start_handshake_(uint32_t now);
|
||||
void service_rx_(uint32_t now);
|
||||
VncParse parse_();
|
||||
void pace_(uint32_t now);
|
||||
bool start_session_(uint32_t now);
|
||||
bool send_client_init_();
|
||||
void frame_done_();
|
||||
void rect_done_();
|
||||
void draw_row_(uint16_t x, uint16_t y, uint16_t count, const uint8_t *pixels);
|
||||
void skip_(uint32_t count, VncStep next);
|
||||
bool budget_exhausted_() const;
|
||||
bool send_(const uint8_t *data, size_t len);
|
||||
bool send_update_request_(bool incremental, uint32_t now);
|
||||
VncParse fail_(VncFailure failure, const LogString *reason);
|
||||
VncParse fail_send_() { return this->fail_(VncFailure::VNC_FAILURE_TRANSIENT, LOG_STR("Send failed")); }
|
||||
VncFailure lost_failure_() const;
|
||||
void session_lost_(uint32_t now);
|
||||
void session_end_(uint32_t now, VncFailure failure, const LogString *reason);
|
||||
void reset_session_();
|
||||
|
||||
display::Display *display_;
|
||||
socket::TcpClientLink link_;
|
||||
LazyCallbackManager<void()> connect_callback_;
|
||||
LazyCallbackManager<void()> disconnect_callback_;
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
|
||||
uint32_t update_delay_ms_{200};
|
||||
uint32_t max_update_delay_ms_{1000};
|
||||
uint32_t backoff_ms_{0};
|
||||
uint32_t hold_ms_{0};
|
||||
uint32_t hold_start_ms_{0};
|
||||
uint32_t setup_ms_{0};
|
||||
uint32_t busy_since_ms_{0};
|
||||
uint32_t session_start_ms_{0};
|
||||
uint32_t update_start_ms_{0};
|
||||
// The first and the latest frame drawn since the display last refreshed.
|
||||
uint32_t first_change_ms_{0};
|
||||
uint32_t last_change_ms_{0};
|
||||
uint32_t rx_total_{0};
|
||||
uint32_t update_start_bytes_{0};
|
||||
uint32_t last_request_ms_{0};
|
||||
uint32_t last_rx_ms_{0};
|
||||
uint32_t pass_start_ms_{0};
|
||||
uint32_t skip_left_{0};
|
||||
|
||||
uint16_t rx_pos_{0};
|
||||
uint16_t rx_len_{0};
|
||||
uint16_t fb_w_{0};
|
||||
uint16_t fb_h_{0};
|
||||
uint16_t vis_w_{0};
|
||||
uint16_t vis_h_{0};
|
||||
uint16_t rects_left_{0};
|
||||
uint16_t rect_x_{0};
|
||||
uint16_t rect_y_{0};
|
||||
uint16_t rect_w_{0};
|
||||
uint16_t rect_h_{0};
|
||||
// Raw: column and row inside the rect.
|
||||
uint16_t pos_x_{0};
|
||||
uint16_t pos_y_{0};
|
||||
|
||||
uint8_t bpp_{2};
|
||||
uint8_t minor_{8};
|
||||
uint8_t sec_count_{0};
|
||||
uint8_t text_len_{0}; // clamped to MAX_TEXT_LEN
|
||||
display::ColorBitness bitness_{display::COLOR_BITNESS_565};
|
||||
VncState state_{VncState::VNC_STATE_IDLE};
|
||||
VncStep step_{VncStep::VNC_STEP_VERSION};
|
||||
VncStep after_skip_{VncStep::VNC_STEP_MESSAGE};
|
||||
VncFailure reason_failure_{VncFailure::VNC_FAILURE_TRANSIENT};
|
||||
|
||||
bool rx_pending_{false};
|
||||
bool in_update_{false};
|
||||
bool request_outstanding_{false};
|
||||
bool refresh_pending_{false};
|
||||
bool frame_drawn_{false};
|
||||
bool display_parks_{false};
|
||||
bool display_busy_{false};
|
||||
bool busy_warned_{false};
|
||||
bool network_was_up_{false};
|
||||
bool big_endian_; // the pixel order the display takes
|
||||
|
||||
uint8_t rx_[RX_BUFFER_SIZE];
|
||||
};
|
||||
|
||||
} // namespace esphome::vnc
|
||||
@@ -1043,7 +1043,7 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
|
||||
if (is_on) {
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("effect"), call,
|
||||
static_cast<light::LightCall &(light::LightCall::*) (const char *, size_t)>(&decltype(call)::set_effect));
|
||||
static_cast<light::LightCall &(light::LightCall::*)(const char *, size_t)>(&decltype(call)::set_effect));
|
||||
}
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
@@ -1404,7 +1404,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
|
||||
auto call = obj->make_call();
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("value"), call,
|
||||
static_cast<text::TextCall &(text::TextCall::*) (const char *, size_t)>(&decltype(call)::set_value));
|
||||
static_cast<text::TextCall &(text::TextCall::*)(const char *, size_t)>(&decltype(call)::set_value));
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1464,7 +1464,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
|
||||
auto call = obj->make_call();
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("option"), call,
|
||||
static_cast<select::SelectCall &(select::SelectCall::*) (const char *, size_t)>(&decltype(call)::set_option));
|
||||
static_cast<select::SelectCall &(select::SelectCall::*)(const char *, size_t)>(&decltype(call)::set_option));
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1522,26 +1522,26 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
|
||||
// Parse string mode parameters
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("mode"), call,
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(&decltype(call)::set_mode));
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(&decltype(call)::set_mode));
|
||||
parse_cstr_param_(request, ESPHOME_F("fan_mode"), call,
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
|
||||
&decltype(call)::set_fan_mode));
|
||||
parse_cstr_param_(request, ESPHOME_F("swing_mode"), call,
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
|
||||
&decltype(call)::set_swing_mode));
|
||||
parse_cstr_param_(request, ESPHOME_F("preset"), call,
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
|
||||
static_cast<climate::ClimateCall &(climate::ClimateCall::*)(const char *, size_t)>(
|
||||
&decltype(call)::set_preset));
|
||||
|
||||
// Parse temperature parameters
|
||||
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
|
||||
using ClimateCall = decltype(call);
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
|
||||
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_high));
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
|
||||
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature_low));
|
||||
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
|
||||
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
|
||||
static_cast<ClimateCall &(ClimateCall::*)(float)>(&ClimateCall::set_target_temperature));
|
||||
|
||||
DEFER_ACTION(call, call.perform());
|
||||
request->send(200);
|
||||
@@ -1818,8 +1818,8 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
|
||||
auto call = obj->make_call();
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("code"), call,
|
||||
static_cast<alarm_control_panel::AlarmControlPanelCall &(
|
||||
alarm_control_panel::AlarmControlPanelCall::*) (const char *, size_t)>(&decltype(call)::set_code));
|
||||
static_cast<alarm_control_panel::AlarmControlPanelCall &(alarm_control_panel::AlarmControlPanelCall::*)(
|
||||
const char *, size_t)>(&decltype(call)::set_code));
|
||||
|
||||
static constexpr ActionEntry<alarm_control_panel::AlarmControlPanelCall> ACTIONS[] PROGMEM = {
|
||||
{"disarm", &alarm_control_panel::AlarmControlPanelCall::disarm},
|
||||
@@ -1893,7 +1893,7 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
|
||||
// Parse mode parameter
|
||||
parse_cstr_param_(
|
||||
request, ESPHOME_F("mode"), base_call,
|
||||
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*) (const char *, size_t)>(
|
||||
static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*)(const char *, size_t)>(
|
||||
&water_heater::WaterHeaterCall::set_mode));
|
||||
|
||||
// Parse temperature parameters
|
||||
|
||||
@@ -450,9 +450,12 @@ uart::UARTFlushResult WeikaiChannel::flush() {
|
||||
|
||||
size_t WeikaiChannel::xfer_fifo_to_buffer_() {
|
||||
size_t total = 0;
|
||||
size_t to_transfer;
|
||||
size_t free;
|
||||
while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) {
|
||||
while (true) {
|
||||
size_t to_transfer = this->rx_in_fifo_();
|
||||
size_t free = this->receive_buffer_.free();
|
||||
if (to_transfer == 0 || free == 0) {
|
||||
break;
|
||||
}
|
||||
// while bytes in fifo and some room in the buffer we transfer
|
||||
if (to_transfer > XFER_MAX_SIZE)
|
||||
to_transfer = XFER_MAX_SIZE; // we can only do so much
|
||||
|
||||
@@ -309,6 +309,8 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
case WHIRLPOOL_AUTO:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ from esphome.const import (
|
||||
CONF_ENABLE_BTM,
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_ENABLE_RRM,
|
||||
CONF_ETHERNET,
|
||||
CONF_FAST_CONNECT,
|
||||
CONF_GATEWAY,
|
||||
CONF_HIDDEN,
|
||||
@@ -431,8 +432,26 @@ def _apply_min_auth_mode_default(config):
|
||||
return config
|
||||
|
||||
|
||||
def wifi_has_sta(config: ConfigType) -> bool:
|
||||
"""Whether the validated config has at least one station network."""
|
||||
return bool(config.get(CONF_NETWORKS))
|
||||
|
||||
|
||||
# Components that save station credentials at run time
|
||||
_PROVISIONERS = ("captive_portal", "improv_ble", "improv_serial")
|
||||
|
||||
|
||||
def wifi_stays_ap_only(config: ConfigType, full_config: ConfigType) -> bool:
|
||||
"""Whether no station can appear at run time: no networks, no DPP, no provisioner."""
|
||||
return not (
|
||||
wifi_has_sta(config)
|
||||
or CONF_DPP in config
|
||||
or any(c in full_config for c in _PROVISIONERS)
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
has_sta = bool(config.get(CONF_NETWORKS, True))
|
||||
has_sta = wifi_has_sta(config)
|
||||
has_ap = CONF_AP in config
|
||||
has_dpp = CONF_DPP in config
|
||||
full_config = fv.full_config.get()
|
||||
@@ -447,9 +466,11 @@ def final_validate(config):
|
||||
|
||||
ap_is_wisp = has_ap and config[CONF_AP][CONF_AP_MODE] == WIFI_AP_MODE_WISP
|
||||
if ap_is_wisp:
|
||||
if not has_sta:
|
||||
# The clients are routed through the station, or through ethernet
|
||||
if not has_sta and CONF_ETHERNET not in full_config:
|
||||
raise cv.Invalid(
|
||||
"WISP mode requires at least one STA network (wifi.ssid or wifi.networks)"
|
||||
"WISP mode needs a station network (wifi.ssid or wifi.networks), "
|
||||
"or ethernet as the upstream interface"
|
||||
)
|
||||
if has_captive_portal:
|
||||
raise cv.Invalid(
|
||||
@@ -458,14 +479,27 @@ def final_validate(config):
|
||||
# Every client packet passes through the WiFi driver, none through the device's own sockets
|
||||
require_high_performance_wifi()
|
||||
if CONF_DNS_SERVERS not in config[CONF_AP]:
|
||||
# The access point passes the station's resolvers on, which a static address has to name
|
||||
for network in [config, *config.get(CONF_NETWORKS, [])]:
|
||||
# The access point passes the upstream's resolvers on, which a static address has to name
|
||||
upstreams = [
|
||||
([], config),
|
||||
*(
|
||||
([CONF_NETWORKS, i], network)
|
||||
for i, network in enumerate(config.get(CONF_NETWORKS, []))
|
||||
),
|
||||
(None, full_config.get(CONF_ETHERNET, {})),
|
||||
]
|
||||
for path, network in upstreams:
|
||||
static = network.get(CONF_MANUAL_IP)
|
||||
if static and static[CONF_DNS1].is_unspecified:
|
||||
if not static or not static[CONF_DNS1].is_unspecified:
|
||||
continue
|
||||
if path is None:
|
||||
raise cv.Invalid(
|
||||
"manual_ip needs dns1 in WISP mode unless ap.dns_servers is set",
|
||||
path=[CONF_MANUAL_IP, CONF_DNS1],
|
||||
"ethernet manual_ip needs dns1 in WISP mode unless ap.dns_servers is set"
|
||||
)
|
||||
raise cv.Invalid(
|
||||
"manual_ip needs dns1 in WISP mode unless ap.dns_servers is set",
|
||||
path=[*path, CONF_MANUAL_IP, CONF_DNS1],
|
||||
)
|
||||
|
||||
if has_ap and not has_captive_portal and not has_web_server and not ap_is_wisp:
|
||||
_LOGGER.warning(
|
||||
@@ -565,7 +599,7 @@ def _report_provisioning_credentials(config):
|
||||
"""
|
||||
from esphome.components import provisioning
|
||||
|
||||
if config.get(CONF_NETWORKS):
|
||||
if wifi_has_sta(config):
|
||||
provisioning.report_hardcoded_credentials("wifi")
|
||||
elif CONF_AP in config:
|
||||
# An access point with no station credentials: the AP shuts down when the
|
||||
|
||||
@@ -783,12 +783,9 @@ void WiFiComponent::start() {
|
||||
// Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
|
||||
this->start_initial_connection_();
|
||||
#endif
|
||||
#ifdef USE_WIFI_AP
|
||||
} else if (this->has_ap()) {
|
||||
#if defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP)
|
||||
} else if (this->has_ap()) { // a bridge waits in loop() for the upstream's resolver
|
||||
this->setup_ap_config_();
|
||||
if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
|
||||
ESP_LOGV(TAG, "Setting Output Power Option failed");
|
||||
}
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
// Where the radio scans alongside the AP, the portal can list networks.
|
||||
if (captive_portal::global_captive_portal != nullptr && !WIFI_AP_EXCLUSIVE) {
|
||||
@@ -797,7 +794,7 @@ void WiFiComponent::start() {
|
||||
}
|
||||
#endif
|
||||
this->start_ap_portal_();
|
||||
#endif // USE_WIFI_AP
|
||||
#endif // USE_WIFI_AP && !USE_WIFI_AP_WISP
|
||||
}
|
||||
#ifdef USE_WIFI_DPP
|
||||
// Without networks there is nothing to try first, so listen for a configurator right away
|
||||
@@ -937,15 +934,7 @@ void WiFiComponent::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP_WISP
|
||||
// The bridge comes up once it has a resolver to offer; a start that failed is tried again after ap_timeout
|
||||
if (!this->ap_setup_ && this->ap_dns_known_() &&
|
||||
(this->ap_changed_ == 0 || now - this->ap_changed_ > this->ap_timeout_)) {
|
||||
ESP_LOGI(TAG, "Starting AP");
|
||||
this->ap_changed_ = now;
|
||||
this->setup_ap_config_();
|
||||
}
|
||||
#elif defined(USE_WIFI_AP)
|
||||
#if defined(USE_WIFI_AP) && !defined(USE_WIFI_AP_WISP)
|
||||
// Once the provisioning window has closed, don't bring up the fallback AP (or
|
||||
// the captive portal on it) - the device must stay unprovisionable until it is
|
||||
// power-cycled.
|
||||
@@ -975,7 +964,7 @@ void WiFiComponent::loop() {
|
||||
this->start_ap_portal_();
|
||||
}
|
||||
}
|
||||
#endif // USE_WIFI_AP
|
||||
#endif // USE_WIFI_AP && !USE_WIFI_AP_WISP
|
||||
|
||||
#ifdef USE_IMPROV_BLE
|
||||
if (improv_ble::global_improv_component != nullptr && !improv_ble::global_improv_component->is_active() &&
|
||||
@@ -1013,6 +1002,16 @@ void WiFiComponent::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_AP_WISP
|
||||
// The bridge comes up once it has a resolver to offer; a start that failed is tried again after ap_timeout
|
||||
if (!this->ap_setup_ && this->state_ != WIFI_COMPONENT_STATE_DISABLED && this->ap_dns_known_() &&
|
||||
(this->ap_changed_ == 0 || now - this->ap_changed_ > this->ap_timeout_)) {
|
||||
ESP_LOGI(TAG, "Starting AP");
|
||||
this->ap_changed_ = now;
|
||||
this->setup_ap_config_();
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI_DPP
|
||||
// Without networks no cooldown starts the listen, so a failed or abandoned enrollment is listened for again
|
||||
// here, at most once per timeout
|
||||
@@ -1098,6 +1097,11 @@ void WiFiComponent::setup_ap_config_() {
|
||||
// Reset before the AP exists: afterwards only the platform's event path writes the counter
|
||||
this->ap_clients_ = 0;
|
||||
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
|
||||
// The station path applied it in start(); without a station the radio only exists from here
|
||||
if (this->ap_setup_ && !this->has_sta() && !std::isnan(this->output_power_) &&
|
||||
!this->wifi_apply_output_power_(this->output_power_)) {
|
||||
ESP_LOGV(TAG, "Setting Output Power Option failed");
|
||||
}
|
||||
#if defined(USE_WIFI_AP_EXCLUSIVE) || defined(USE_WIFI_AP_WISP)
|
||||
// A failed start must not leave the radio in AP mode: on an exclusive radio it would refuse
|
||||
// the STA attempts until the next try, and a bridge would take clients it cannot serve.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#endif
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
@@ -813,13 +814,8 @@ class WiFiComponent final : public Component {
|
||||
}
|
||||
|
||||
bool all_networks_hidden_() const {
|
||||
if (this->sta_.empty())
|
||||
return false;
|
||||
for (const auto &ap : this->sta_) {
|
||||
if (!ap.get_hidden())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return !this->sta_.empty() &&
|
||||
std::all_of(this->sta_.begin(), this->sta_.end(), [](const WiFiAP &ap) { return ap.get_hidden(); });
|
||||
}
|
||||
|
||||
bool wifi_loop_();
|
||||
|
||||
@@ -62,7 +62,6 @@ static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid
|
||||
#ifdef USE_WIFI_AP
|
||||
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
#endif // USE_WIFI_AP
|
||||
static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -128,6 +127,10 @@ void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, voi
|
||||
#if USE_NETWORK_IPV6
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) {
|
||||
memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t));
|
||||
#endif
|
||||
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP) { // NOLINT(bugprone-branch-clone)
|
||||
// no data: the bridge rereads the resolvers an ethernet upstream just received
|
||||
#endif
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone)
|
||||
// no data
|
||||
@@ -1037,7 +1040,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
s_sta_started = true;
|
||||
// re-apply power save mode
|
||||
wifi_apply_power_save_();
|
||||
#ifdef SOC_WIFI_SUPPORT_5G
|
||||
@@ -1046,7 +1048,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
|
||||
ESP_LOGV(TAG, "STA stop");
|
||||
s_sta_started = false;
|
||||
s_sta_connecting = false;
|
||||
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
|
||||
@@ -1126,6 +1127,10 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
this->notify_disconnect_state_listeners_();
|
||||
#endif
|
||||
|
||||
#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ETH_GOT_IP) {
|
||||
this->apply_ap_dns_();
|
||||
#endif
|
||||
} else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) {
|
||||
const auto &it = data->data.ip_got_ip;
|
||||
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
|
||||
@@ -1400,10 +1405,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
esp_err_t err;
|
||||
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
|
||||
// Check if the AP interface is initialized before using it
|
||||
if (s_ap_netif == nullptr) {
|
||||
ESP_LOGW(TAG, "AP interface not initialized");
|
||||
@@ -1481,8 +1482,8 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
#endif
|
||||
|
||||
err = esp_netif_dhcps_start(s_ap_netif);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
// Before the access point is up, its start event brings the server up with these settings
|
||||
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_IF_NOT_READY) {
|
||||
ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
@@ -1491,6 +1492,17 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
// The address and DHCP settings go in before the access point starts: its start event starts the DHCP
|
||||
// server on the event task, and settings written after that race it.
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
#else
|
||||
if (!this->wifi_ap_ip_config_({})) {
|
||||
#endif
|
||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||
return false;
|
||||
}
|
||||
|
||||
// enable AP
|
||||
if (!this->wifi_mode_({}, true))
|
||||
return false;
|
||||
@@ -1527,19 +1539,6 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
|
||||
ESP_LOGE(TAG, "esp_wifi_set_config failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
|
||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (!this->wifi_ap_ip_config_({})) {
|
||||
ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1684,7 +1683,7 @@ bool WiFiComponent::write_ap_dns_() {
|
||||
#ifndef USE_WIFI_AP_WISP_STATIC_DNS
|
||||
void WiFiComponent::apply_ap_dns_() {
|
||||
if (!this->ap_setup_)
|
||||
return; // loop() brings the bridge up once the station has a resolver, and its setup writes them
|
||||
return; // loop() brings the bridge up once the upstream has a resolver, and its setup writes them
|
||||
// The DHCP server holds what it offers, so there is nothing to cache; a failed update is noticed again with the
|
||||
// next lease. A resolver the station lost stays offered: set_dns_info rejects an empty address.
|
||||
bool changed = false;
|
||||
|
||||
@@ -166,8 +166,9 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() const {
|
||||
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
|
||||
case CYW43_LINK_NONET:
|
||||
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
|
||||
default:
|
||||
return WiFiSTAConnectStatus::IDLE;
|
||||
}
|
||||
return WiFiSTAConnectStatus::IDLE;
|
||||
}
|
||||
|
||||
int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace esphome::xiaomi_hhccpot002 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "xiaomi_hhccpot002");
|
||||
|
||||
void XiaomiHHCCPOT002 ::dump_config() {
|
||||
void XiaomiHHCCPOT002::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002");
|
||||
LOG_SENSOR(" ", "Moisture", this->moisture_);
|
||||
LOG_SENSOR(" ", "Conductivity", this->conductivity_);
|
||||
|
||||
@@ -40,8 +40,9 @@ static void advertise(k_work *work) {
|
||||
truncate_name_to(name, sizeof(name), app_name.c_str(), app_name.length(),
|
||||
App.is_name_add_mac_suffix_enabled() ? MAC_ADDRESS_SUFFIX_WITH_SEPARATOR_LEN : 0);
|
||||
const uint8_t name_type = name_len < app_name.length() ? BT_DATA_NAME_SHORTENED : BT_DATA_NAME_COMPLETE;
|
||||
static constexpr uint8_t FLAGS[] = {BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR};
|
||||
const bt_data ad[] = {
|
||||
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
|
||||
BT_DATA(BT_DATA_FLAGS, FLAGS, sizeof(FLAGS)),
|
||||
BT_DATA(name_type, name, static_cast<uint8_t>(name_len)),
|
||||
};
|
||||
|
||||
@@ -255,6 +256,8 @@ static const char *role_str(uint8_t role) {
|
||||
return "Central";
|
||||
case BT_CONN_ROLE_PERIPHERAL:
|
||||
return "Peripheral";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
|
||||
@@ -80,6 +80,8 @@ static const char *swap_type_str(uint8_t type) {
|
||||
return "revert";
|
||||
case BOOT_SWAP_TYPE_FAIL:
|
||||
return "fail";
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
|
||||
+18
-11
@@ -658,6 +658,8 @@ class EsphomeCore:
|
||||
self.main_statements: list[Statement] = []
|
||||
# A list of statements to insert in the global block (includes and global variables)
|
||||
self.global_statements: list[Statement] = []
|
||||
# Generated code emitted after the user includes, so it can use what they declare
|
||||
self.global_statements_after_includes: list[Statement] = []
|
||||
# A map of platformio libraries to add to the project (shortname: (name, version, repository))
|
||||
self.platformio_libraries: dict[str, Library] = {}
|
||||
# A set of build flags to set in the platformio project
|
||||
@@ -728,6 +730,7 @@ class EsphomeCore:
|
||||
self.variables = {}
|
||||
self.main_statements = []
|
||||
self.global_statements = []
|
||||
self.global_statements_after_includes = []
|
||||
self.platformio_libraries = {}
|
||||
self.build_flags = set()
|
||||
self.cmake_args = {}
|
||||
@@ -1045,8 +1048,8 @@ class EsphomeCore:
|
||||
except RuntimeError as e:
|
||||
raise EsphomeError(str(e)) from e
|
||||
|
||||
def add(self, expression, prepend=False) -> "Statement":
|
||||
"""Add an expression or statement to the main setup() block."""
|
||||
@staticmethod
|
||||
def _as_statement(expression) -> "Statement":
|
||||
from esphome.cpp_generator import Expression, Statement, statement
|
||||
|
||||
if isinstance(expression, Expression):
|
||||
@@ -1055,7 +1058,11 @@ class EsphomeCore:
|
||||
raise ValueError(
|
||||
f"Add '{expression}' must be expression or statement, not {type(expression)}"
|
||||
)
|
||||
return expression
|
||||
|
||||
def add(self, expression, prepend=False) -> "Statement":
|
||||
"""Add an expression or statement to the main setup() block."""
|
||||
expression = self._as_statement(expression)
|
||||
if prepend:
|
||||
self.main_statements.insert(0, expression)
|
||||
else:
|
||||
@@ -1064,14 +1071,7 @@ class EsphomeCore:
|
||||
return expression
|
||||
|
||||
def add_global(self, expression, prepend=False) -> "Statement":
|
||||
from esphome.cpp_generator import Expression, Statement, statement
|
||||
|
||||
if isinstance(expression, Expression):
|
||||
expression = statement(expression)
|
||||
if not isinstance(expression, Statement):
|
||||
raise ValueError(
|
||||
f"Add '{expression}' must be expression or statement, not {type(expression)}"
|
||||
)
|
||||
expression = self._as_statement(expression)
|
||||
if prepend:
|
||||
self.global_statements.insert(0, expression)
|
||||
else:
|
||||
@@ -1079,6 +1079,13 @@ class EsphomeCore:
|
||||
_LOGGER.debug("Adding global: %s", expression)
|
||||
return expression
|
||||
|
||||
def add_global_after_includes(self, expression) -> "Statement":
|
||||
"""Add a global after the user includes, for generated code that may use them."""
|
||||
expression = self._as_statement(expression)
|
||||
self.global_statements_after_includes.append(expression)
|
||||
_LOGGER.debug("Adding global after includes: %s", expression)
|
||||
return expression
|
||||
|
||||
def add_library(self, library: Library):
|
||||
if not isinstance(library, Library):
|
||||
raise TypeError(
|
||||
@@ -1258,7 +1265,7 @@ class EsphomeCore:
|
||||
from esphome.cpp_generator import statement
|
||||
|
||||
global_code = []
|
||||
for exp in self.global_statements:
|
||||
for exp in (*self.global_statements, *self.global_statements_after_includes):
|
||||
text = str(statement(exp))
|
||||
text = text.rstrip()
|
||||
global_code.append(text)
|
||||
|
||||
@@ -590,7 +590,8 @@ async def add_arduino_global_workaround():
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def add_includes(includes: list[str], is_c_header: bool = False) -> None:
|
||||
# Add includes at the very end, so that the included files can access global variables
|
||||
# After the id declarations, so the headers can use id(x); the generated function bodies
|
||||
# and flash tables come later still, so their lambdas can use the headers.
|
||||
for include in includes:
|
||||
path = CORE.relative_config_path(include)
|
||||
if path.is_dir():
|
||||
|
||||
@@ -59,13 +59,15 @@ template<typename T> class PlainAtomic {
|
||||
constexpr PlainAtomic(T value) : value_(value) {}
|
||||
T load(std::memory_order order = std::memory_order_seq_cst) const {
|
||||
T value = value_;
|
||||
if (order != std::memory_order_relaxed)
|
||||
if (order != std::memory_order_relaxed) {
|
||||
ESPHOME_LFQ_COMPILER_BARRIER(); // acquire: later reads may not hoist above this load
|
||||
}
|
||||
return value;
|
||||
}
|
||||
void store(T value, std::memory_order order = std::memory_order_seq_cst) {
|
||||
if (order != std::memory_order_relaxed)
|
||||
if (order != std::memory_order_relaxed) {
|
||||
ESPHOME_LFQ_COMPILER_BARRIER(); // release: earlier writes may not sink below this store
|
||||
}
|
||||
value_ = value;
|
||||
}
|
||||
T fetch_add(T amount, std::memory_order /*order*/ = std::memory_order_seq_cst) {
|
||||
|
||||
+20
-10
@@ -536,8 +536,10 @@ def shared_progmem_array(
|
||||
used |= {str(i) for i in CORE.variables}
|
||||
used |= set(RESERVED_IDS) | CORE.loaded_integrations
|
||||
id_ = ID(ensure_unique_string(name, used), is_declaration=True, type=type_)
|
||||
# Global, so any scope can use it; anything a lambda references is already declared.
|
||||
CORE.add_global(ProgmemAssignmentExpression(type_, id_, rhs, constexpr))
|
||||
# After the user includes, so a lambda in the table can use what their headers declare.
|
||||
CORE.add_global_after_includes(
|
||||
ProgmemAssignmentExpression(type_, id_, rhs, constexpr)
|
||||
)
|
||||
array = MockObj(id_, ".")
|
||||
CORE.register_variable(id_, array)
|
||||
if share:
|
||||
@@ -789,15 +791,16 @@ def static_function(
|
||||
"""Emit ``static <return_type> <name>(parameters) { body }`` at global scope and return an
|
||||
expression naming it, for use as a template argument or a function pointer.
|
||||
|
||||
Every id the body names must already be declared, which holds when the statements were
|
||||
rendered through ``get_variable`` or ``process_lambda``.
|
||||
The declaration goes with the other globals, so a template argument can name the function
|
||||
at once; the body goes after the user includes, where every id and everything those
|
||||
headers declare is in scope.
|
||||
"""
|
||||
params = ParameterListExpression(*parameters)
|
||||
add_global(
|
||||
RawStatement(
|
||||
f"static {safe_exp(return_type)} {name}({params}) {{\n"
|
||||
f"{indent(chr(10).join(body))}\n}}"
|
||||
)
|
||||
signature = (
|
||||
f"static {safe_exp(return_type)} {name}({ParameterListExpression(*parameters)})"
|
||||
)
|
||||
add_global(RawStatement(f"{signature};"))
|
||||
add_global_after_includes(
|
||||
RawStatement(f"{signature} {{\n{indent(chr(10).join(body))}\n}}")
|
||||
)
|
||||
return RawExpression(name)
|
||||
|
||||
@@ -816,6 +819,13 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False):
|
||||
CORE.add_global(expression, prepend)
|
||||
|
||||
|
||||
def add_global_after_includes(expression: SafeExpType | Statement):
|
||||
"""Add a global after the user's ``includes:`` headers, for generated code whose
|
||||
lambdas may use what those headers declare.
|
||||
"""
|
||||
CORE.add_global_after_includes(expression)
|
||||
|
||||
|
||||
def add_library(name: str, version: str | None, repository: str | None = None):
|
||||
"""Add a library to the codegen library storage.
|
||||
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ lib_deps =
|
||||
improv/Improv@1.2.7 ; improv_serial / improv_ble
|
||||
kikuchan98/pngle@1.1.0 ; runtime_image
|
||||
; Using the repository directly, otherwise ESP-IDF can't use the library
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
|
||||
https://github.com/esphome-libs/JPEGDEC.git#1.8.4-esphome.3 ; runtime_image
|
||||
; This dependency is used only in unit tests.
|
||||
; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py
|
||||
; See scripts/cpp_unit_test.py and tests/components/README.md
|
||||
@@ -253,7 +253,7 @@ lib_deps =
|
||||
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
|
||||
improv/Improv@1.2.7 ; improv_serial
|
||||
kikuchan98/pngle@1.1.0 ; runtime_image
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
|
||||
https://github.com/esphome-libs/JPEGDEC.git#1.8.4-esphome.3 ; runtime_image
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
-DUSE_LIBRETINY
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ pyserial==3.5
|
||||
platformio==6.2.0
|
||||
esptool==5.6.0
|
||||
click==8.5.0
|
||||
aioesphomeapi==46.18.0
|
||||
aioesphomeapi==46.20.0
|
||||
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
|
||||
zeroconf==0.151.5
|
||||
puremagic==2.2.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Useful stuff when working in a development environment
|
||||
clang-format==22.1.8 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
clang-tidy==22.1.8
|
||||
clang-format==23.1.3 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
clang-tidy==23.1.3
|
||||
yamllint==1.38.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py
|
||||
|
||||
@@ -2711,6 +2711,8 @@ def build_message_type(
|
||||
o += " const ProtoFieldValue value(data, scalar);\n"
|
||||
o += " switch (tag) {\n"
|
||||
o += indent("\n".join(decode), " ").replace("this->", "msg.") + "\n"
|
||||
o += " default:\n"
|
||||
o += " break;\n"
|
||||
o += " }\n"
|
||||
o += "}\n"
|
||||
cpp += o
|
||||
|
||||
+8
-14
@@ -319,21 +319,15 @@ def run_tidy(
|
||||
disabled_checks = []
|
||||
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx")):
|
||||
# MMIO peripheral access on these bare-metal platforms is all
|
||||
# fixed-address.
|
||||
disabled_checks.append("-clang-analyzer-core.FixedAddressDereference")
|
||||
# fixed-address. clang-tidy 23 moved the checker under optin.
|
||||
disabled_checks += [
|
||||
"-clang-analyzer-core.FixedAddressDereference",
|
||||
"-clang-analyzer-optin.core.FixedAddressDereference",
|
||||
]
|
||||
if args.environment.startswith("host"):
|
||||
# glibc's assert() expands to a call the check mistakes for a
|
||||
# constant condition.
|
||||
disabled_checks += ["-misc-static-assert", "-cert-dcl03-c"]
|
||||
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx", "host")):
|
||||
# bugprone-pointer-arithmetic-on-polymorphic-object (and its
|
||||
# cert-ctr56-cpp alias) crashes clang-tidy 22 with infinite matcher
|
||||
# recursion on lvgl_esphome.h under the RP2 defines and on the
|
||||
# all-include file under the host defines.
|
||||
disabled_checks += [
|
||||
"-bugprone-pointer-arithmetic-on-polymorphic-object",
|
||||
"-cert-ctr56-cpp",
|
||||
]
|
||||
if os.path.normpath(path) in ANALYZER_SKIPPED_FILES:
|
||||
disabled_checks.append("-clang-analyzer-*")
|
||||
if disabled_checks:
|
||||
@@ -523,7 +517,7 @@ def main():
|
||||
failed_files = []
|
||||
timings = []
|
||||
try:
|
||||
executable = get_binary("clang-tidy", 22)
|
||||
executable = get_binary("clang-tidy", 23)
|
||||
task_queue = queue.Queue(args.jobs)
|
||||
lock = threading.Lock()
|
||||
for _ in range(args.jobs):
|
||||
@@ -571,13 +565,13 @@ def main():
|
||||
try:
|
||||
try:
|
||||
subprocess.call(
|
||||
["clang-apply-replacements-22", tmpdir], close_fds=False
|
||||
["clang-apply-replacements-23", tmpdir], close_fds=False
|
||||
)
|
||||
except FileNotFoundError:
|
||||
subprocess.call(["clang-apply-replacements", tmpdir], close_fds=False)
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"Error please install clang-apply-replacements-22 or clang-apply-replacements.\n",
|
||||
"Error please install clang-apply-replacements-23 or clang-apply-replacements.\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
except:
|
||||
|
||||
@@ -621,6 +621,69 @@ def test_uc8279_width_must_be_a_multiple_of_8(
|
||||
)
|
||||
|
||||
|
||||
GDEY029Z95 = "goodisplay-gdey029z95-2.9"
|
||||
|
||||
|
||||
def _gdey029z95_config(**overrides: Any) -> ConfigType:
|
||||
config: ConfigType = {
|
||||
"id": "test_display",
|
||||
"model": GDEY029Z95,
|
||||
CONF_DC_PIN: 21,
|
||||
CONF_RESET_PIN: 22,
|
||||
CONF_BUSY_PIN: 23,
|
||||
}
|
||||
config.update(overrides)
|
||||
return config
|
||||
|
||||
|
||||
def test_gdey029z95_uses_the_good_display_driver() -> None:
|
||||
"""The GDEY029Z95 uses EPaperGoodDisplay3C with the WeAct init sequence."""
|
||||
model = MODELS[GDEY029Z95.upper()]
|
||||
|
||||
assert model.class_name == "EPaperGoodDisplay3C"
|
||||
assert model.get_init_sequence({}) == (
|
||||
(0x01, 0x27, 0x01, 0x00),
|
||||
(0x11, 0x03),
|
||||
(0x3C, 0x05),
|
||||
(0x18, 0x80),
|
||||
(0x21, 0x00, 0x80),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pin", [CONF_RESET_PIN, CONF_BUSY_PIN])
|
||||
def test_gdey029z95_requires_reset_and_busy_pins(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
pin: str,
|
||||
) -> None:
|
||||
"""Only a hardware reset wakes the panel from deep sleep, and refreshes are timed by busy."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
|
||||
|
||||
config = _gdey029z95_config()
|
||||
del config[pin]
|
||||
with pytest.raises(cv.Invalid, match=pin):
|
||||
run_schema_validation(config)
|
||||
|
||||
|
||||
def test_gdey029z95_has_no_partial_update(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19})
|
||||
|
||||
run_schema_validation(_gdey029z95_config())
|
||||
with pytest.raises(cv.Invalid, match="does not support partial update"):
|
||||
run_schema_validation(_gdey029z95_config(full_update_every=2))
|
||||
|
||||
|
||||
def test_uc8279_x4pro_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user