mirror of
https://github.com/esphome/esphome.git
synced 2026-09-04 03:56:04 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d43616507 | ||
|
|
a5bbac084a | ||
|
|
44cea5c0ed | ||
|
|
28ab280c7b | ||
|
|
f2f9b9cbff | ||
|
|
f32e3e75f4 | ||
|
|
5494190d97 | ||
|
|
36468022ea | ||
|
|
19f5641706 | ||
|
|
900dd63883 | ||
|
|
bca4c9dbba | ||
|
|
e1595cb7ef | ||
|
|
86c10ac0c8 | ||
|
|
7b17651837 | ||
|
|
cb7cfa5340 | ||
|
|
9ca77cd7c7 | ||
|
|
da5f43ce41 | ||
|
|
736218e747 | ||
|
|
6fce48f1d5 | ||
|
|
e3820e62bc | ||
|
|
92e663821c |
@@ -374,9 +374,8 @@ jobs:
|
||||
- name: Install apt packages (cached)
|
||||
# ccache speeds up the host compiles. A cache hit never touches apt
|
||||
# (mirror outages cannot hang the job); the timeout bounds the cold
|
||||
# path. Packages and version must match seed-apt-cache exactly.
|
||||
# libsdl2-dev is needed by the headless display tests, which capture
|
||||
# screenshots.
|
||||
# path. Packages and version must match seed-apt-cache exactly;
|
||||
# libsdl2-dev is unused here and carried only for cache-key parity.
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
@@ -439,16 +438,6 @@ jobs:
|
||||
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
||||
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
|
||||
--junitxml=junit-integration.xml "${test_files[@]}"
|
||||
- name: Upload test artifacts
|
||||
# Tests that compare rendered output write the image they actually got here, so a
|
||||
# failure can be looked at without reproducing the whole build locally.
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: integration-test-artifacts-${{ matrix.bucket.name }}
|
||||
path: test_artifacts/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
- name: Upload junit timings
|
||||
# Consumed by sync-integration-durations.yml through
|
||||
# script/update_integration_test_durations.py; only full matrix dev
|
||||
|
||||
@@ -137,8 +137,6 @@ config/
|
||||
!tests/component_tests/**/config/
|
||||
tests/build/
|
||||
tests/.esphome/
|
||||
# Output kept by failing tests for inspection; uploaded by CI
|
||||
test_artifacts/
|
||||
/.temp-clang-tidy.cpp
|
||||
/.temp/
|
||||
.pio/
|
||||
|
||||
@@ -496,7 +496,6 @@ esphome/components/sm2335/* @Cossid
|
||||
esphome/components/sml/* @alengwenus
|
||||
esphome/components/smt100/* @piechade
|
||||
esphome/components/sn74hc165/* @jesserockz
|
||||
esphome/components/snapshot/* @clydebarrow
|
||||
esphome/components/socket/* @esphome/core
|
||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/sound_level/* @kahrendt
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.10.0-dev
|
||||
PROJECT_NUMBER = 2026.9.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -44,7 +44,8 @@ def get_arduino8266_tools_path() -> Path:
|
||||
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
|
||||
|
||||
|
||||
# 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0
|
||||
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
|
||||
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
|
||||
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
|
||||
|
||||
|
||||
@@ -52,16 +53,20 @@ def framework_package_version(ver: Version) -> str:
|
||||
"""Map an Arduino core version to its registry package version (3.1.2 ->
|
||||
3.30102.0; the leading 3 is the package major).
|
||||
|
||||
Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION.
|
||||
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
|
||||
at MIN_FRAMEWORK_VERSION.
|
||||
"""
|
||||
if ver.major > 3:
|
||||
raise EsphomeError(
|
||||
f"Arduino core {ver} is not supported yet; "
|
||||
"the newest known core series is 3.x"
|
||||
)
|
||||
if ver.major < 3:
|
||||
if ver <= Version(2, 6, 2):
|
||||
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
|
||||
# boundary as _format_framework_arduino_version's era guard)
|
||||
raise EsphomeError(
|
||||
f"Arduino core {ver} is not supported; ESPHome requires core 3.x"
|
||||
f"Arduino core {ver} uses an older package encoding than this "
|
||||
"helper implements (newer than 2.6.2)"
|
||||
)
|
||||
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
|
||||
|
||||
@@ -70,17 +70,13 @@ service APIConnection {
|
||||
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
|
||||
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
|
||||
|
||||
rpc zigbee_proxy_request(ZigbeeProxyRequest) returns (void) {}
|
||||
|
||||
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
|
||||
|
||||
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
|
||||
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
|
||||
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_get_usb_info(SerialProxyGetUsbInfoRequest) returns (void) {}
|
||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -231,11 +227,6 @@ enum SerialProxyPortType {
|
||||
SERIAL_PROXY_PORT_TYPE_TTL = 0;
|
||||
SERIAL_PROXY_PORT_TYPE_RS232 = 1;
|
||||
SERIAL_PROXY_PORT_TYPE_RS485 = 2;
|
||||
// A serial device attached through a USB bridge. Set by the device configuration, never
|
||||
// by the user; identifies ports whose USB identity can be read with
|
||||
// SerialProxyGetUsbInfoRequest. Deliberately not a USB endpoint type: serial_proxy
|
||||
// carries serial devices only, whatever bridge chip connects them.
|
||||
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3;
|
||||
}
|
||||
|
||||
message SerialProxyInfo {
|
||||
@@ -340,10 +331,6 @@ message DeviceInfoResponse {
|
||||
// all-zeros PSK, so the api encryption key can be provisioned without being
|
||||
// sent in plaintext (protects against passive sniffing, not active MITM)
|
||||
bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"];
|
||||
|
||||
// Indicates if Zigbee proxy support is available and features supported
|
||||
uint32 zigbee_proxy_feature_flags = 27 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
|
||||
uint64 zigbee_ieee_address = 28 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
|
||||
}
|
||||
|
||||
// ==================== DEVICE CAPABILITIES ====================
|
||||
@@ -2739,8 +2726,7 @@ enum SerialProxyParity {
|
||||
SERIAL_PROXY_PARITY_ODD = 2;
|
||||
}
|
||||
|
||||
// Configure UART parameters for a serial proxy instance. Only the subscribed client may
|
||||
// configure the port; others are refused with PORT_IN_USE (since API 1.17).
|
||||
// Configure UART parameters for a serial proxy instance
|
||||
message SerialProxyConfigureRequest {
|
||||
option (id) = 138;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2766,8 +2752,7 @@ message SerialProxyDataReceived {
|
||||
bytes data = 2; // Raw data received from the serial device
|
||||
}
|
||||
|
||||
// Write data to a serial device. Only the subscribed client may write; writes from
|
||||
// others are ignored (since API 1.17).
|
||||
// Write data to a serial device
|
||||
message SerialProxyWriteRequest {
|
||||
option (id) = 140;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2778,8 +2763,7 @@ message SerialProxyWriteRequest {
|
||||
bytes data = 2; // Raw data to write to the serial device
|
||||
}
|
||||
|
||||
// Set modem control pin states (RTS and DTR). Only the subscribed client may set them;
|
||||
// others are refused with PORT_IN_USE (since API 1.17).
|
||||
// Set modem control pin states (RTS and DTR)
|
||||
message SerialProxySetModemPinsRequest {
|
||||
option (id) = 141;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2818,7 +2802,6 @@ enum SerialProxyRequestType {
|
||||
// error the device answers with INVALID_ARGUMENT.
|
||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
|
||||
}
|
||||
|
||||
enum SerialProxyStatus {
|
||||
@@ -2831,8 +2814,7 @@ enum SerialProxyStatus {
|
||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
|
||||
}
|
||||
|
||||
// Generic request message for simple serial proxy operations. FLUSH requires an active
|
||||
// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17).
|
||||
// Generic request message for simple serial proxy operations
|
||||
message SerialProxyRequest {
|
||||
option (id) = 144;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2856,59 +2838,6 @@ message SerialProxyRequestResponse {
|
||||
string error_message = 4; // Additional detail on failure (optional)
|
||||
}
|
||||
|
||||
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
|
||||
// activates the port's protocol-aware tap (if one is configured), letting it observe
|
||||
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
|
||||
// speaks is a property of the device configuration, discoverable from the tap
|
||||
// component's own API surface. A client that is about to flash firmware selects RAW
|
||||
// first, which definitively disables that injection.
|
||||
enum SerialProxyMode {
|
||||
SERIAL_PROXY_MODE_RAW = 0;
|
||||
SERIAL_PROXY_MODE_PROTOCOL = 1;
|
||||
}
|
||||
|
||||
// Only the subscribed client may change the mode; any other caller -- including one that
|
||||
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
|
||||
// when the port has no protocol-aware tap configured.
|
||||
message SerialProxySetModeRequest {
|
||||
option (id) = 152;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1;
|
||||
SerialProxyMode mode = 2;
|
||||
}
|
||||
|
||||
// Ask for the USB identity of the device behind a USB_SERIAL port. Read-only, so no
|
||||
// subscription is required -- a client typically uses this to decide which port to
|
||||
// subscribe to. Answered with NOT_SUPPORTED on ports that are not USB_SERIAL.
|
||||
message SerialProxyGetUsbInfoRequest {
|
||||
option (id) = 153;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1;
|
||||
}
|
||||
|
||||
// The USB identity of the device currently behind a port, read live from the cached
|
||||
// USB descriptors. Fields are zero/empty while no device is connected.
|
||||
message SerialProxyGetUsbInfoResponse {
|
||||
option (id) = 154;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1;
|
||||
SerialProxyStatus status = 2; // NOT_SUPPORTED when the port is not USB_SERIAL
|
||||
bool connected = 3; // True when a USB device is currently attached
|
||||
uint32 vendor_id = 4;
|
||||
uint32 product_id = 5;
|
||||
uint32 bcd_device = 6;
|
||||
uint32 interface_number = 7; // Channel index on multi-port bridges
|
||||
string manufacturer = 8;
|
||||
string product = 9;
|
||||
string serial_number = 10;
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
message BluetoothSetConnectionParamsRequest {
|
||||
option (id) = 145;
|
||||
@@ -2930,18 +2859,3 @@ message BluetoothSetConnectionParamsResponse {
|
||||
uint64 address = 1;
|
||||
int32 error = 2;
|
||||
}
|
||||
|
||||
// ==================== ZIGBEE ====================
|
||||
|
||||
enum ZigbeeProxyRequestType {
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 0;
|
||||
}
|
||||
|
||||
message ZigbeeProxyRequest {
|
||||
option (id) = 155;
|
||||
option (source) = SOURCE_BOTH;
|
||||
option (ifdef) = "USE_ZIGBEE_PROXY";
|
||||
|
||||
ZigbeeProxyRequestType type = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
@@ -48,12 +48,6 @@
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
#include "esphome/components/zwave_proxy/zwave_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
#include "esphome/components/zigbee_proxy/zigbee_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
#include "esphome/components/usb_host/usb_host.h"
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
@@ -1395,12 +1389,6 @@ void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void APIConnection::on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) {
|
||||
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_request(this, msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||
return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
|
||||
@@ -1654,27 +1642,6 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
SerialProxyGetUsbInfoResponse resp{};
|
||||
resp.instance = msg.instance;
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
} else {
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
// The response's strings are views into this buffer, which outlives the send below
|
||||
usb_host::UsbDeviceInfo info;
|
||||
proxies[msg.instance]->get_usb_info(info, resp);
|
||||
#else
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
|
||||
#endif
|
||||
}
|
||||
if (!this->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
@@ -1694,7 +1661,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
break;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
|
||||
// Response-only discriminators; never valid in a request
|
||||
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
@@ -1707,19 +1673,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
send_serial_proxy_ack(this, msg.instance, msg.type, status);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
|
||||
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
|
||||
serial_proxy_result_to_status(result));
|
||||
}
|
||||
|
||||
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
|
||||
if (!this->send_message(msg)) {
|
||||
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
|
||||
@@ -1831,11 +1784,6 @@ void APIConnection::complete_authentication_() {
|
||||
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
if (zigbee_proxy::global_zigbee_proxy != nullptr) {
|
||||
zigbee_proxy::global_zigbee_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
@@ -1851,7 +1799,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 17;
|
||||
resp.api_version_minor = 16;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
@@ -1988,10 +1936,6 @@ bool APIConnection::send_device_info_response_() {
|
||||
info.configured_line_states = proxy->get_configured_modem_pins();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
resp.zigbee_proxy_feature_flags = zigbee_proxy::global_zigbee_proxy->get_feature_flags();
|
||||
resp.zigbee_ieee_address = zigbee_proxy::global_zigbee_proxy->get_ieee_address();
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
resp.api_encryption_supported = true;
|
||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
||||
|
||||
@@ -223,10 +223,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg);
|
||||
@@ -247,9 +243,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg);
|
||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
|
||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
|
||||
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg);
|
||||
void on_serial_proxy_request(const SerialProxyRequest &msg);
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
|
||||
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -175,12 +175,6 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
ProtoEncode::encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, 28, this->zigbee_ieee_address);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -246,12 +240,6 @@ uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
size += ProtoSize::calc_bool(2, this->api_encryption_provisionable);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
size += ProtoSize::calc_uint32(2, this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
size += ProtoSize::calc_uint64(2, this->zigbee_ieee_address);
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
@@ -4265,57 +4253,6 @@ uint32_t SerialProxyRequestResponse::calculate_size() const {
|
||||
size += ProtoSize::calc_length(1, this->error_message.size());
|
||||
return size;
|
||||
}
|
||||
bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->instance = value;
|
||||
break;
|
||||
case 2:
|
||||
this->mode = static_cast<enums::SerialProxyMode>(value);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool SerialProxyGetUsbInfoRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->instance = value;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *SerialProxyGetUsbInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->status));
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->connected);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->vendor_id);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->product_id);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->bcd_device);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->interface_number);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->manufacturer);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->product);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->serial_number);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SerialProxyGetUsbInfoResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, this->instance);
|
||||
size += this->status ? 2 : 0;
|
||||
size += ProtoSize::calc_bool(1, this->connected);
|
||||
size += ProtoSize::calc_uint32(1, this->vendor_id);
|
||||
size += ProtoSize::calc_uint32(1, this->product_id);
|
||||
size += ProtoSize::calc_uint32(1, this->bcd_device);
|
||||
size += ProtoSize::calc_uint32(1, this->interface_number);
|
||||
size += ProtoSize::calc_length(1, this->manufacturer.size());
|
||||
size += ProtoSize::calc_length(1, this->product.size());
|
||||
size += ProtoSize::calc_length(1, this->serial_number.size());
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
@@ -4353,41 +4290,5 @@ uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const {
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
bool ZigbeeProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->type = static_cast<enums::ZigbeeProxyRequestType>(value);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ZigbeeProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *ZigbeeProxyRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->type));
|
||||
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 2, this->data, this->data_len);
|
||||
return pos;
|
||||
}
|
||||
uint32_t ZigbeeProxyRequest::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += this->type ? 2 : 0;
|
||||
size += ProtoSize::calc_length(1, this->data_len);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -23,7 +23,6 @@ enum SerialProxyPortType : uint32_t {
|
||||
SERIAL_PROXY_PORT_TYPE_TTL = 0,
|
||||
SERIAL_PROXY_PORT_TYPE_RS232 = 1,
|
||||
SERIAL_PROXY_PORT_TYPE_RS485 = 2,
|
||||
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3,
|
||||
};
|
||||
enum EntityCategory : uint32_t {
|
||||
ENTITY_CATEGORY_NONE = 0,
|
||||
@@ -357,7 +356,6 @@ enum SerialProxyRequestType : uint32_t {
|
||||
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
|
||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5,
|
||||
};
|
||||
enum SerialProxyStatus : uint32_t {
|
||||
SERIAL_PROXY_STATUS_OK = 0,
|
||||
@@ -368,15 +366,6 @@ enum SerialProxyStatus : uint32_t {
|
||||
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
|
||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
|
||||
};
|
||||
enum SerialProxyMode : uint32_t {
|
||||
SERIAL_PROXY_MODE_RAW = 0,
|
||||
SERIAL_PROXY_MODE_PROTOCOL = 1,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
enum ZigbeeProxyRequestType : uint32_t {
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 0,
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace enums
|
||||
@@ -560,7 +549,7 @@ class SerialProxyInfo final : public ProtoMessage {
|
||||
class DeviceInfoResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 10;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 322;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 312;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
|
||||
#endif
|
||||
@@ -618,12 +607,6 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool api_encryption_provisionable{false};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
uint32_t zigbee_proxy_feature_flags{0};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
uint64_t zigbee_ieee_address{0};
|
||||
#endif
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
@@ -3420,62 +3403,6 @@ class SerialProxyRequestResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class SerialProxySetModeRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 152;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 6;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); }
|
||||
#endif
|
||||
uint32_t instance{0};
|
||||
enums::SerialProxyMode mode{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||
};
|
||||
class SerialProxyGetUsbInfoRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 153;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 4;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("serial_proxy_get_usb_info_request"); }
|
||||
#endif
|
||||
uint32_t instance{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||
};
|
||||
class SerialProxyGetUsbInfoResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 154;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 51;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("serial_proxy_get_usb_info_response"); }
|
||||
#endif
|
||||
uint32_t instance{0};
|
||||
enums::SerialProxyStatus status{};
|
||||
bool connected{false};
|
||||
uint32_t vendor_id{0};
|
||||
uint32_t product_id{0};
|
||||
uint32_t bcd_device{0};
|
||||
uint32_t interface_number{0};
|
||||
StringRef manufacturer{};
|
||||
StringRef product{};
|
||||
StringRef serial_number{};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
|
||||
@@ -3515,27 +3442,5 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
class ZigbeeProxyRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 155;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 21;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("zigbee_proxy_request"); }
|
||||
#endif
|
||||
enums::ZigbeeProxyRequestType type{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_BLUETOOTH_PROXY) || defined(USE_BLUETOOTH_PROXY_CONNECTIONS)
|
||||
#ifndef USE_API_VARINT64
|
||||
#define USE_API_VARINT64
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::api {} // namespace esphome::api
|
||||
|
||||
@@ -143,8 +143,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::S
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232");
|
||||
case enums::SERIAL_PROXY_PORT_TYPE_RS485:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485");
|
||||
case enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_USB_SERIAL");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -856,8 +854,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -882,26 +878,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_MODE_RAW:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
|
||||
case enums::SERIAL_PROXY_MODE_PROTOCOL:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
template<> const char *proto_enum_to_string<enums::ZigbeeProxyRequestType>(enums::ZigbeeProxyRequestType value) {
|
||||
switch (value) {
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
|
||||
return ESPHOME_PSTR("ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -1032,12 +1008,6 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
dump_field(out, ESPHOME_PSTR("zigbee_proxy_feature_flags"), this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
dump_field(out, ESPHOME_PSTR("zigbee_ieee_address"), this->zigbee_ieee_address);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
@@ -2835,31 +2805,6 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyGetUsbInfoRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetUsbInfoRequest"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyGetUsbInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetUsbInfoResponse"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status));
|
||||
dump_field(out, ESPHOME_PSTR("connected"), this->connected);
|
||||
dump_field(out, ESPHOME_PSTR("vendor_id"), this->vendor_id);
|
||||
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
|
||||
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
|
||||
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
|
||||
dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer);
|
||||
dump_field(out, ESPHOME_PSTR("product"), this->product);
|
||||
dump_field(out, ESPHOME_PSTR("serial_number"), this->serial_number);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -2878,14 +2823,6 @@ const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
const char *ZigbeeProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ZigbeeProxyRequest"));
|
||||
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ZigbeeProxyRequestType>(this->type));
|
||||
dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
|
||||
@@ -712,39 +712,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
this->on_device_capabilities_request();
|
||||
break;
|
||||
}
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxySetModeRequest::MESSAGE_TYPE: {
|
||||
SerialProxySetModeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_set_mode_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxyGetUsbInfoRequest::MESSAGE_TYPE: {
|
||||
SerialProxyGetUsbInfoRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_get_usb_info_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_get_usb_info_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
case ZigbeeProxyRequest::MESSAGE_TYPE: {
|
||||
ZigbeeProxyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_zigbee_proxy_request"), msg);
|
||||
#endif
|
||||
this->on_zigbee_proxy_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -235,20 +235,9 @@ class APIServerConnectionBase {
|
||||
void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &value){};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -404,14 +404,6 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void APIServer::on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) {
|
||||
// Very infrequent and small - send to all clients rather than tracking a subscription
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_message(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
const std::vector<int32_t> *timings) {
|
||||
|
||||
@@ -189,9 +189,6 @@ class APIServer final : public Component,
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg);
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
@@ -368,8 +368,8 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
||||
}
|
||||
|
||||
void Climate::save_state_(const ClimateTraits &traits) {
|
||||
#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY)
|
||||
#pragma GCC diagnostic push
|
||||
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
|
||||
!defined(CLANG_TIDY)
|
||||
#pragma GCC diagnostic ignored "-Wclass-memaccess"
|
||||
#define TEMP_IGNORE_MEMACCESS
|
||||
#endif
|
||||
|
||||
@@ -100,6 +100,7 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
|
||||
void CM1106Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CM1106:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
this->check_uart_settings(9600);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
|
||||
@@ -46,14 +46,6 @@ CONFIG_SCHEMA = (
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"cm1106",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
"""Code generation entry point."""
|
||||
|
||||
@@ -58,6 +58,7 @@ void CSE7761Component::dump_config() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||
}
|
||||
|
||||
void CSE7761Component::update() {
|
||||
|
||||
@@ -68,13 +68,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"cse7761",
|
||||
baud_rate=38400,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
"cse7761", baud_rate=38400, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -255,6 +255,7 @@ void CSE7766Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
|
||||
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
|
||||
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
|
||||
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
|
||||
}
|
||||
|
||||
} // namespace esphome::cse7766
|
||||
|
||||
@@ -84,12 +84,7 @@ CONFIG_SCHEMA = (
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"cse7766",
|
||||
baud_rate=4800,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
"cse7766", baud_rate=4800, parity="EVEN", require_rx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,14 +26,6 @@ CONFIG_SCHEMA = (
|
||||
.extend(cv.polling_component_schema("30s"))
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"daly_bms",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -22,7 +22,10 @@ static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96;
|
||||
|
||||
void DalyBmsComponent::setup() { this->next_request_ = 1; }
|
||||
|
||||
void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); }
|
||||
void DalyBmsComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Daly BMS:");
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
void DalyBmsComponent::update() {
|
||||
this->trigger_next_ = true;
|
||||
|
||||
@@ -22,9 +22,9 @@ void DebugComponent::dump_config() {
|
||||
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
|
||||
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
|
||||
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
|
||||
#ifdef USE_ESP8266
|
||||
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
|
||||
#endif // USE_ESP8266
|
||||
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
#endif // USE_SENSOR
|
||||
|
||||
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
|
||||
|
||||
@@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent {
|
||||
#ifdef USE_SENSOR
|
||||
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
|
||||
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
|
||||
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
@@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent {
|
||||
|
||||
sensor::Sensor *free_sensor_{nullptr};
|
||||
sensor::Sensor *block_sensor_{nullptr};
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
|
||||
sensor::Sensor *fragmentation_sensor_{nullptr};
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
|
||||
@@ -159,10 +159,12 @@ void DebugComponent::update_platform_() {
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
|
||||
}
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
|
||||
if (this->fragmentation_sensor_ != nullptr) {
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -52,9 +52,12 @@ CONFIG_SCHEMA = {
|
||||
),
|
||||
cv.Optional(CONF_FRAGMENTATION): cv.All(
|
||||
cv.Any(
|
||||
cv.only_on_esp8266,
|
||||
cv.All(
|
||||
cv.only_on_esp8266,
|
||||
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
msg="This feature is only available on ESP8266 and ESP32",
|
||||
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
|
||||
),
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
|
||||
@@ -60,12 +60,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
).extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"dfplayer",
|
||||
baud_rate=9600,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
"dfplayer", baud_rate=9600, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -277,6 +277,9 @@ void DFPlayer::loop() {
|
||||
}
|
||||
}
|
||||
}
|
||||
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
|
||||
void DFPlayer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DFPlayer:");
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
} // namespace esphome::dfplayer
|
||||
|
||||
@@ -35,7 +35,7 @@ from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import BOARDS, board_ld_script
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script
|
||||
from .const import (
|
||||
CONF_EARLY_PIN_INIT,
|
||||
CONF_ENABLE_SERIAL,
|
||||
@@ -43,6 +43,8 @@ from .const import (
|
||||
CONF_RESTORE_FROM_FLASH,
|
||||
KEY_BOARD,
|
||||
KEY_ESP8266,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_LDSCRIPT,
|
||||
KEY_PIN_INITIAL_STATES,
|
||||
KEY_SERIAL1_REQUIRED,
|
||||
KEY_SERIAL_REQUIRED,
|
||||
@@ -131,6 +133,10 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
# format the given arduino (https://github.com/esp8266/Arduino/releases) version to
|
||||
# a PIO platformio/framework-arduinoespressif8266 value
|
||||
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
|
||||
if ver <= cv.Version(2, 4, 1):
|
||||
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
if ver <= cv.Version(2, 6, 2):
|
||||
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
# Same encoding the native toolchain uses for its package download, so a
|
||||
# version bump cannot drift between the two paths.
|
||||
from esphome.arduino8266.framework import framework_package_version
|
||||
@@ -153,9 +159,11 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
# - https://github.com/esp8266/Arduino/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
|
||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
|
||||
# The platformio/espressif8266 version to use for arduino 3 framework versions
|
||||
# The platformio/espressif8266 version to use for arduino 2 framework versions
|
||||
# - https://github.com/platformio/platform-espressif8266/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
|
||||
ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3)
|
||||
# for arduino 3 framework versions
|
||||
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
|
||||
# for arduino 4 framework versions
|
||||
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
|
||||
@@ -180,14 +188,6 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
||||
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
|
||||
source = value.get(CONF_SOURCE, None)
|
||||
|
||||
if version < cv.Version(3, 0, 0):
|
||||
raise cv.Invalid(
|
||||
f"Arduino framework {version} is no longer supported; ESPHome requires "
|
||||
f"C++20, which needs Arduino core 3.x. Use the recommended version "
|
||||
f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).",
|
||||
path=[CONF_VERSION],
|
||||
)
|
||||
|
||||
value[CONF_VERSION] = str(version)
|
||||
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
|
||||
|
||||
@@ -195,8 +195,12 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
||||
if platform_version is None:
|
||||
if version >= cv.Version(3, 1, 0):
|
||||
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
|
||||
else:
|
||||
elif version >= cv.Version(3, 0, 0):
|
||||
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
|
||||
elif version >= cv.Version(2, 5, 0):
|
||||
platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION))
|
||||
else:
|
||||
platform_version = _parse_platform_version(str(cv.Version(1, 8, 0)))
|
||||
value[CONF_PLATFORM_VERSION] = platform_version
|
||||
|
||||
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
|
||||
@@ -285,11 +289,29 @@ def check_rosetta() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _choose_ld_script(board: str) -> str:
|
||||
"""The flash ld to pin for this board."""
|
||||
def _choose_ld_script(board: str, ver: cv.Version) -> str | None:
|
||||
"""The flash ld to pin for this board and core, or None for cores
|
||||
without ld-script support."""
|
||||
board_data = BOARDS[board]
|
||||
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
|
||||
if ver <= cv.Version(2, 3, 0):
|
||||
# No ld script support
|
||||
return None
|
||||
if ver <= cv.Version(2, 4, 2):
|
||||
# Old ld script path; the modern per-board override names do not
|
||||
# exist in this core's SDK, so the override cannot be honored.
|
||||
# Substituting the size default would move _FS_end and the
|
||||
# preferences sector, wiping flash-backed state on flash.
|
||||
if KEY_LDSCRIPT in board_data:
|
||||
raise EsphomeError(
|
||||
f"Board {board} requires its {board_data[KEY_LDSCRIPT]} "
|
||||
f"flash layout, which Arduino core {ver} cannot honor; "
|
||||
"use a core newer than 2.4.2"
|
||||
)
|
||||
return ld_scripts[0]
|
||||
# A per-board override preserves a layout the board shipped with
|
||||
# (see d1_wroom_02 in boards.py)
|
||||
return board_ld_script(BOARDS[board])
|
||||
return board_ld_script(board_data)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.PLATFORM)
|
||||
@@ -413,9 +435,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
if config[CONF_BOARD] in BOARDS:
|
||||
cg.add_platformio_option(
|
||||
"board_build.ldscript", _choose_ld_script(config[CONF_BOARD])
|
||||
)
|
||||
ld_script = _choose_ld_script(config[CONF_BOARD], ver)
|
||||
|
||||
if ld_script is not None:
|
||||
cg.add_platformio_option("board_build.ldscript", ld_script)
|
||||
|
||||
CORE.add_job(add_pin_initial_states_array)
|
||||
CORE.add_job(finalize_waveform_config)
|
||||
|
||||
@@ -96,6 +96,7 @@ void HC8Component::dump_config() {
|
||||
" Warmup time: %" PRIu32 " s",
|
||||
this->warmup_seconds_);
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
} // namespace esphome::hc8
|
||||
|
||||
@@ -47,9 +47,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ CoverTraits HE60rCover::get_traits() {
|
||||
|
||||
void HE60rCover::dump_config() {
|
||||
LOG_COVER("", "HE60R Cover", this);
|
||||
this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Open Duration: %.1fs\n"
|
||||
" Close Duration: %.1fs",
|
||||
|
||||
@@ -68,6 +68,8 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
|
||||
void HrxlMaxsonarWrComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:");
|
||||
LOG_SENSOR(" ", "Distance", this);
|
||||
// As specified in the sensor's data sheet
|
||||
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
}
|
||||
|
||||
} // namespace esphome::hrxl_maxsonar_wr
|
||||
|
||||
@@ -23,14 +23,6 @@ CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(uart.UART_DEVICE_SCHEMA)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"hrxl_maxsonar_wr",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
|
||||
@@ -11,6 +11,7 @@ static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREO
|
||||
static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)};
|
||||
|
||||
void HydreonRGxxComponent::dump_config() {
|
||||
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
|
||||
|
||||
@@ -130,14 +130,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
_validate,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"hydreon_rgxx",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -26,6 +26,8 @@ void KamstrupKMPComponent::dump_config() {
|
||||
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
|
||||
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
|
||||
}
|
||||
|
||||
this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
}
|
||||
|
||||
void KamstrupKMPComponent::update() {
|
||||
|
||||
@@ -102,13 +102,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"kamstrup_kmp",
|
||||
baud_rate=1200,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=2,
|
||||
"kamstrup_kmp", baud_rate=1200, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -143,6 +143,8 @@ void MHZ19Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MH-Z19:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
this->check_uart_settings(9600);
|
||||
|
||||
if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
|
||||
ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot");
|
||||
} else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
|
||||
|
||||
@@ -80,14 +80,6 @@ CONFIG_SCHEMA = (
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"mhz19",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -163,7 +163,10 @@ void Mk2PVRouter::publish_value_(const char *tag, const char *val) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void Mk2PVRouter::dump_config() { ESP_LOGCONFIG(TAG, "Mk2PVRouter:"); }
|
||||
void Mk2PVRouter::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Mk2PVRouter:");
|
||||
this->check_uart_settings(BAUD_RATE, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
|
||||
}
|
||||
|
||||
#ifdef MK2PVROUTER_LISTENER_COUNT
|
||||
void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) {
|
||||
|
||||
@@ -43,6 +43,7 @@ class Mk2PVRouter final : public Component, public uart::UARTDevice {
|
||||
|
||||
protected:
|
||||
static constexpr size_t CRC_SUFFIX_LEN = 1;
|
||||
static constexpr uint32_t BAUD_RATE = 9600;
|
||||
|
||||
enum class State : uint8_t {
|
||||
WAITING_FOR_START,
|
||||
|
||||
@@ -209,8 +209,14 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
|
||||
http_client.setTimeout(this->tft_upload_http_timeout_);
|
||||
|
||||
bool begin_status = false;
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
|
||||
http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
|
||||
#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
|
||||
http_client.setFollowRedirects(true);
|
||||
#endif
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
|
||||
http_client.setRedirectLimit(3);
|
||||
#endif
|
||||
begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
|
||||
if (!begin_status) {
|
||||
this->connection_state_.is_updating_ = false;
|
||||
|
||||
@@ -16,6 +16,7 @@ void PM1006Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PM1006:");
|
||||
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
void PM1006Component::update() {
|
||||
|
||||
@@ -48,9 +48,6 @@ def validate_interval_uart(config: ConfigType) -> None:
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)(config)
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ void PMSX003Component::dump_config() {
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
|
||||
}
|
||||
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
void PMSX003Component::loop() {
|
||||
|
||||
@@ -302,13 +302,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
def final_validate(config: ConfigType) -> None:
|
||||
require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s")
|
||||
schema = uart.final_validate_device_schema(
|
||||
"pmsx003",
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=require_tx,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
"pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx
|
||||
)
|
||||
schema(config)
|
||||
|
||||
|
||||
@@ -41,14 +41,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"pylontech",
|
||||
baud_rate=115200,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -33,6 +33,7 @@ static const uint8_t ASCII_LF = 0x0A;
|
||||
PylontechComponent::PylontechComponent() {}
|
||||
|
||||
void PylontechComponent::dump_config() {
|
||||
this->check_uart_settings(115200, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
ESP_LOGCONFIG(TAG, "pylontech:");
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Connection with pylontech failed!");
|
||||
|
||||
@@ -4,7 +4,11 @@ from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, esp32_rmt, remote_base
|
||||
from esphome.components.libretiny import get_libretiny_family
|
||||
from esphome.components.libretiny.const import FAMILY_BK7238, FAMILY_RTL8720C
|
||||
from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_RTL8720C,
|
||||
)
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -45,9 +49,7 @@ DigitalWriteAction = remote_transmitter_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
# Keep in sync with the USE_LIBRETINY_VARIANT_RTL8720C / REMOTE_TRANSMITTER_BK_PWM gates in
|
||||
# remote_transmitter.h, which decide where set_non_blocking() is declared
|
||||
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7238)
|
||||
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7231N, FAMILY_BK7238)
|
||||
|
||||
|
||||
def _validate_non_blocking_platform(value: bool) -> bool:
|
||||
@@ -57,7 +59,9 @@ def _validate_non_blocking_platform(value: bool) -> bool:
|
||||
return cv.boolean(value)
|
||||
if CORE.is_libretiny and get_libretiny_family() in _NON_BLOCKING_LIBRETINY_FAMILIES:
|
||||
return cv.boolean(value)
|
||||
raise cv.Invalid("non_blocking is only supported on ESP32, RTL8720C and BK7238")
|
||||
raise cv.Invalid(
|
||||
"non_blocking is only supported on ESP32, RTL8720C, BK7231N and BK7238"
|
||||
)
|
||||
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
#endif // SOC_RMT_SUPPORTED
|
||||
#endif // USE_ESP32
|
||||
|
||||
// Enables the ISR-driven transmitter on Beken. Gated on BK7238 alone: the shadow-load PWM
|
||||
// block is shared with BK7231N, but LibreTiny builds that family against an older BDK whose
|
||||
// PWM driver has no pwm_init_param()/pwm_start(). See remote_transmitter_bk72xx.cpp.
|
||||
// Keep in sync with _NON_BLOCKING_LIBRETINY_FAMILIES in __init__.py.
|
||||
#ifdef USE_LIBRETINY_VARIANT_BK7238
|
||||
// The BK7231N-style PWM block (hardware shadow-load duty updates) enables the ISR-driven
|
||||
// transmitter on these families; family-level proxy for the SDK's CFG_SOC_NAME gate.
|
||||
// See remote_transmitter_bk72xx.cpp.
|
||||
#if defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7238)
|
||||
#define REMOTE_TRANSMITTER_BK_PWM
|
||||
#endif
|
||||
|
||||
|
||||
@@ -9,13 +9,10 @@
|
||||
// with the core's fixes for type-name collisions between the two
|
||||
#include <ArduinoPrivate.h>
|
||||
|
||||
// Needs the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
|
||||
// for glitch-free per-edge duty updates, and an SDK exposing pwm_init_param()/pwm_start().
|
||||
// BK7231N has the block but LibreTiny builds it against an older BDK offering only the
|
||||
// sddev_control API (CMD_PWM_INIT_PARAM), so it stays on the generic bit-bang path until
|
||||
// someone can add and validate that path on real hardware. Every other Beken SoC lacks the
|
||||
// block. REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h; when it is
|
||||
// unset this file compiles to nothing and remote_transmitter.cpp is used instead.
|
||||
// Only the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
|
||||
// supports glitch-free per-edge duty updates; older SoCs compile the generic bit-bang
|
||||
// implementation (remote_transmitter.cpp) instead, and this file compiles to nothing.
|
||||
// REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h.
|
||||
|
||||
namespace esphome::remote_transmitter {
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// Envelope chain shared by the LibreTiny families that pace transmission from a hardware timer
|
||||
// interrupt: RTL8720C (gtimer) and BK7238 (BKTIMER1). Everything platform-specific sits behind
|
||||
// five hooks implemented in the per-family files -- carrier setup, duty writes, one-shot arming
|
||||
// and timer stop. Families without a usable timer keep the generic bit-bang implementation and
|
||||
// compile none of this.
|
||||
// Envelope chain shared by the LibreTiny families that pace transmission from a hardware
|
||||
// timer interrupt: RTL8720C (gtimer) and the BK7231N-style PWM block (BKTIMER1). Everything
|
||||
// platform-specific sits behind five hooks implemented in the per-family files -- carrier
|
||||
// setup, duty writes, one-shot arming and timer stop. Families without a usable timer keep
|
||||
// the generic bit-bang implementation and compile none of this.
|
||||
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
|
||||
|
||||
namespace esphome::remote_transmitter {
|
||||
|
||||
@@ -1,254 +1 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
|
||||
|
||||
SDL_KEYS = (
|
||||
"SDLK_UNKNOWN",
|
||||
"SDLK_RETURN",
|
||||
"SDLK_ESCAPE",
|
||||
"SDLK_BACKSPACE",
|
||||
"SDLK_TAB",
|
||||
"SDLK_SPACE",
|
||||
"SDLK_EXCLAIM",
|
||||
"SDLK_QUOTEDBL",
|
||||
"SDLK_HASH",
|
||||
"SDLK_PERCENT",
|
||||
"SDLK_DOLLAR",
|
||||
"SDLK_AMPERSAND",
|
||||
"SDLK_QUOTE",
|
||||
"SDLK_LEFTPAREN",
|
||||
"SDLK_RIGHTPAREN",
|
||||
"SDLK_ASTERISK",
|
||||
"SDLK_PLUS",
|
||||
"SDLK_COMMA",
|
||||
"SDLK_MINUS",
|
||||
"SDLK_PERIOD",
|
||||
"SDLK_SLASH",
|
||||
"SDLK_0",
|
||||
"SDLK_1",
|
||||
"SDLK_2",
|
||||
"SDLK_3",
|
||||
"SDLK_4",
|
||||
"SDLK_5",
|
||||
"SDLK_6",
|
||||
"SDLK_7",
|
||||
"SDLK_8",
|
||||
"SDLK_9",
|
||||
"SDLK_COLON",
|
||||
"SDLK_SEMICOLON",
|
||||
"SDLK_LESS",
|
||||
"SDLK_EQUALS",
|
||||
"SDLK_GREATER",
|
||||
"SDLK_QUESTION",
|
||||
"SDLK_AT",
|
||||
"SDLK_LEFTBRACKET",
|
||||
"SDLK_BACKSLASH",
|
||||
"SDLK_RIGHTBRACKET",
|
||||
"SDLK_CARET",
|
||||
"SDLK_UNDERSCORE",
|
||||
"SDLK_BACKQUOTE",
|
||||
"SDLK_a",
|
||||
"SDLK_b",
|
||||
"SDLK_c",
|
||||
"SDLK_d",
|
||||
"SDLK_e",
|
||||
"SDLK_f",
|
||||
"SDLK_g",
|
||||
"SDLK_h",
|
||||
"SDLK_i",
|
||||
"SDLK_j",
|
||||
"SDLK_k",
|
||||
"SDLK_l",
|
||||
"SDLK_m",
|
||||
"SDLK_n",
|
||||
"SDLK_o",
|
||||
"SDLK_p",
|
||||
"SDLK_q",
|
||||
"SDLK_r",
|
||||
"SDLK_s",
|
||||
"SDLK_t",
|
||||
"SDLK_u",
|
||||
"SDLK_v",
|
||||
"SDLK_w",
|
||||
"SDLK_x",
|
||||
"SDLK_y",
|
||||
"SDLK_z",
|
||||
"SDLK_CAPSLOCK",
|
||||
"SDLK_F1",
|
||||
"SDLK_F2",
|
||||
"SDLK_F3",
|
||||
"SDLK_F4",
|
||||
"SDLK_F5",
|
||||
"SDLK_F6",
|
||||
"SDLK_F7",
|
||||
"SDLK_F8",
|
||||
"SDLK_F9",
|
||||
"SDLK_F10",
|
||||
"SDLK_F11",
|
||||
"SDLK_F12",
|
||||
"SDLK_PRINTSCREEN",
|
||||
"SDLK_SCROLLLOCK",
|
||||
"SDLK_PAUSE",
|
||||
"SDLK_INSERT",
|
||||
"SDLK_HOME",
|
||||
"SDLK_PAGEUP",
|
||||
"SDLK_DELETE",
|
||||
"SDLK_END",
|
||||
"SDLK_PAGEDOWN",
|
||||
"SDLK_RIGHT",
|
||||
"SDLK_LEFT",
|
||||
"SDLK_DOWN",
|
||||
"SDLK_UP",
|
||||
"SDLK_NUMLOCKCLEAR",
|
||||
"SDLK_KP_DIVIDE",
|
||||
"SDLK_KP_MULTIPLY",
|
||||
"SDLK_KP_MINUS",
|
||||
"SDLK_KP_PLUS",
|
||||
"SDLK_KP_ENTER",
|
||||
"SDLK_KP_1",
|
||||
"SDLK_KP_2",
|
||||
"SDLK_KP_3",
|
||||
"SDLK_KP_4",
|
||||
"SDLK_KP_5",
|
||||
"SDLK_KP_6",
|
||||
"SDLK_KP_7",
|
||||
"SDLK_KP_8",
|
||||
"SDLK_KP_9",
|
||||
"SDLK_KP_0",
|
||||
"SDLK_KP_PERIOD",
|
||||
"SDLK_APPLICATION",
|
||||
"SDLK_POWER",
|
||||
"SDLK_KP_EQUALS",
|
||||
"SDLK_F13",
|
||||
"SDLK_F14",
|
||||
"SDLK_F15",
|
||||
"SDLK_F16",
|
||||
"SDLK_F17",
|
||||
"SDLK_F18",
|
||||
"SDLK_F19",
|
||||
"SDLK_F20",
|
||||
"SDLK_F21",
|
||||
"SDLK_F22",
|
||||
"SDLK_F23",
|
||||
"SDLK_F24",
|
||||
"SDLK_EXECUTE",
|
||||
"SDLK_HELP",
|
||||
"SDLK_MENU",
|
||||
"SDLK_SELECT",
|
||||
"SDLK_STOP",
|
||||
"SDLK_AGAIN",
|
||||
"SDLK_UNDO",
|
||||
"SDLK_CUT",
|
||||
"SDLK_COPY",
|
||||
"SDLK_PASTE",
|
||||
"SDLK_FIND",
|
||||
"SDLK_MUTE",
|
||||
"SDLK_VOLUMEUP",
|
||||
"SDLK_VOLUMEDOWN",
|
||||
"SDLK_KP_COMMA",
|
||||
"SDLK_KP_EQUALSAS400",
|
||||
"SDLK_ALTERASE",
|
||||
"SDLK_SYSREQ",
|
||||
"SDLK_CANCEL",
|
||||
"SDLK_CLEAR",
|
||||
"SDLK_PRIOR",
|
||||
"SDLK_RETURN2",
|
||||
"SDLK_SEPARATOR",
|
||||
"SDLK_OUT",
|
||||
"SDLK_OPER",
|
||||
"SDLK_CLEARAGAIN",
|
||||
"SDLK_CRSEL",
|
||||
"SDLK_EXSEL",
|
||||
"SDLK_KP_00",
|
||||
"SDLK_KP_000",
|
||||
"SDLK_THOUSANDSSEPARATOR",
|
||||
"SDLK_DECIMALSEPARATOR",
|
||||
"SDLK_CURRENCYUNIT",
|
||||
"SDLK_CURRENCYSUBUNIT",
|
||||
"SDLK_KP_LEFTPAREN",
|
||||
"SDLK_KP_RIGHTPAREN",
|
||||
"SDLK_KP_LEFTBRACE",
|
||||
"SDLK_KP_RIGHTBRACE",
|
||||
"SDLK_KP_TAB",
|
||||
"SDLK_KP_BACKSPACE",
|
||||
"SDLK_KP_A",
|
||||
"SDLK_KP_B",
|
||||
"SDLK_KP_C",
|
||||
"SDLK_KP_D",
|
||||
"SDLK_KP_E",
|
||||
"SDLK_KP_F",
|
||||
"SDLK_KP_XOR",
|
||||
"SDLK_KP_POWER",
|
||||
"SDLK_KP_PERCENT",
|
||||
"SDLK_KP_LESS",
|
||||
"SDLK_KP_GREATER",
|
||||
"SDLK_KP_AMPERSAND",
|
||||
"SDLK_KP_DBLAMPERSAND",
|
||||
"SDLK_KP_VERTICALBAR",
|
||||
"SDLK_KP_DBLVERTICALBAR",
|
||||
"SDLK_KP_COLON",
|
||||
"SDLK_KP_HASH",
|
||||
"SDLK_KP_SPACE",
|
||||
"SDLK_KP_AT",
|
||||
"SDLK_KP_EXCLAM",
|
||||
"SDLK_KP_MEMSTORE",
|
||||
"SDLK_KP_MEMRECALL",
|
||||
"SDLK_KP_MEMCLEAR",
|
||||
"SDLK_KP_MEMADD",
|
||||
"SDLK_KP_MEMSUBTRACT",
|
||||
"SDLK_KP_MEMMULTIPLY",
|
||||
"SDLK_KP_MEMDIVIDE",
|
||||
"SDLK_KP_PLUSMINUS",
|
||||
"SDLK_KP_CLEAR",
|
||||
"SDLK_KP_CLEARENTRY",
|
||||
"SDLK_KP_BINARY",
|
||||
"SDLK_KP_OCTAL",
|
||||
"SDLK_KP_DECIMAL",
|
||||
"SDLK_KP_HEXADECIMAL",
|
||||
"SDLK_LCTRL",
|
||||
"SDLK_LSHIFT",
|
||||
"SDLK_LALT",
|
||||
"SDLK_LGUI",
|
||||
"SDLK_RCTRL",
|
||||
"SDLK_RSHIFT",
|
||||
"SDLK_RALT",
|
||||
"SDLK_RGUI",
|
||||
"SDLK_MODE",
|
||||
"SDLK_AUDIONEXT",
|
||||
"SDLK_AUDIOPREV",
|
||||
"SDLK_AUDIOSTOP",
|
||||
"SDLK_AUDIOPLAY",
|
||||
"SDLK_AUDIOMUTE",
|
||||
"SDLK_MEDIASELECT",
|
||||
"SDLK_WWW",
|
||||
"SDLK_MAIL",
|
||||
"SDLK_CALCULATOR",
|
||||
"SDLK_COMPUTER",
|
||||
"SDLK_AC_SEARCH",
|
||||
"SDLK_AC_HOME",
|
||||
"SDLK_AC_BACK",
|
||||
"SDLK_AC_FORWARD",
|
||||
"SDLK_AC_STOP",
|
||||
"SDLK_AC_REFRESH",
|
||||
"SDLK_AC_BOOKMARKS",
|
||||
"SDLK_BRIGHTNESSDOWN",
|
||||
"SDLK_BRIGHTNESSUP",
|
||||
"SDLK_DISPLAYSWITCH",
|
||||
"SDLK_KBDILLUMTOGGLE",
|
||||
"SDLK_KBDILLUMDOWN",
|
||||
"SDLK_KBDILLUMUP",
|
||||
"SDLK_EJECT",
|
||||
"SDLK_SLEEP",
|
||||
"SDLK_APP1",
|
||||
"SDLK_APP2",
|
||||
"SDLK_AUDIOREWIND",
|
||||
"SDLK_AUDIOFASTFORWARD",
|
||||
"SDLK_SOFTLEFT",
|
||||
"SDLK_SOFTRIGHT",
|
||||
"SDLK_CALL",
|
||||
"SDLK_ENDCALL",
|
||||
)
|
||||
|
||||
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
|
||||
|
||||
@@ -7,15 +7,262 @@ from esphome.core import Lambda
|
||||
from esphome.cpp_generator import ExpressionStatement, RawExpression
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import SDL_KEYMAP
|
||||
from .display import CONF_SDL_ID, Sdl, headless_final_validate
|
||||
from .display import CONF_SDL_ID, Sdl
|
||||
|
||||
CODEOWNERS = ["@bdm310"]
|
||||
|
||||
STATE_ARG = "state"
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = headless_final_validate("binary_sensor")
|
||||
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
|
||||
|
||||
SDL_KEYS = (
|
||||
"SDLK_UNKNOWN",
|
||||
"SDLK_RETURN",
|
||||
"SDLK_ESCAPE",
|
||||
"SDLK_BACKSPACE",
|
||||
"SDLK_TAB",
|
||||
"SDLK_SPACE",
|
||||
"SDLK_EXCLAIM",
|
||||
"SDLK_QUOTEDBL",
|
||||
"SDLK_HASH",
|
||||
"SDLK_PERCENT",
|
||||
"SDLK_DOLLAR",
|
||||
"SDLK_AMPERSAND",
|
||||
"SDLK_QUOTE",
|
||||
"SDLK_LEFTPAREN",
|
||||
"SDLK_RIGHTPAREN",
|
||||
"SDLK_ASTERISK",
|
||||
"SDLK_PLUS",
|
||||
"SDLK_COMMA",
|
||||
"SDLK_MINUS",
|
||||
"SDLK_PERIOD",
|
||||
"SDLK_SLASH",
|
||||
"SDLK_0",
|
||||
"SDLK_1",
|
||||
"SDLK_2",
|
||||
"SDLK_3",
|
||||
"SDLK_4",
|
||||
"SDLK_5",
|
||||
"SDLK_6",
|
||||
"SDLK_7",
|
||||
"SDLK_8",
|
||||
"SDLK_9",
|
||||
"SDLK_COLON",
|
||||
"SDLK_SEMICOLON",
|
||||
"SDLK_LESS",
|
||||
"SDLK_EQUALS",
|
||||
"SDLK_GREATER",
|
||||
"SDLK_QUESTION",
|
||||
"SDLK_AT",
|
||||
"SDLK_LEFTBRACKET",
|
||||
"SDLK_BACKSLASH",
|
||||
"SDLK_RIGHTBRACKET",
|
||||
"SDLK_CARET",
|
||||
"SDLK_UNDERSCORE",
|
||||
"SDLK_BACKQUOTE",
|
||||
"SDLK_a",
|
||||
"SDLK_b",
|
||||
"SDLK_c",
|
||||
"SDLK_d",
|
||||
"SDLK_e",
|
||||
"SDLK_f",
|
||||
"SDLK_g",
|
||||
"SDLK_h",
|
||||
"SDLK_i",
|
||||
"SDLK_j",
|
||||
"SDLK_k",
|
||||
"SDLK_l",
|
||||
"SDLK_m",
|
||||
"SDLK_n",
|
||||
"SDLK_o",
|
||||
"SDLK_p",
|
||||
"SDLK_q",
|
||||
"SDLK_r",
|
||||
"SDLK_s",
|
||||
"SDLK_t",
|
||||
"SDLK_u",
|
||||
"SDLK_v",
|
||||
"SDLK_w",
|
||||
"SDLK_x",
|
||||
"SDLK_y",
|
||||
"SDLK_z",
|
||||
"SDLK_CAPSLOCK",
|
||||
"SDLK_F1",
|
||||
"SDLK_F2",
|
||||
"SDLK_F3",
|
||||
"SDLK_F4",
|
||||
"SDLK_F5",
|
||||
"SDLK_F6",
|
||||
"SDLK_F7",
|
||||
"SDLK_F8",
|
||||
"SDLK_F9",
|
||||
"SDLK_F10",
|
||||
"SDLK_F11",
|
||||
"SDLK_F12",
|
||||
"SDLK_PRINTSCREEN",
|
||||
"SDLK_SCROLLLOCK",
|
||||
"SDLK_PAUSE",
|
||||
"SDLK_INSERT",
|
||||
"SDLK_HOME",
|
||||
"SDLK_PAGEUP",
|
||||
"SDLK_DELETE",
|
||||
"SDLK_END",
|
||||
"SDLK_PAGEDOWN",
|
||||
"SDLK_RIGHT",
|
||||
"SDLK_LEFT",
|
||||
"SDLK_DOWN",
|
||||
"SDLK_UP",
|
||||
"SDLK_NUMLOCKCLEAR",
|
||||
"SDLK_KP_DIVIDE",
|
||||
"SDLK_KP_MULTIPLY",
|
||||
"SDLK_KP_MINUS",
|
||||
"SDLK_KP_PLUS",
|
||||
"SDLK_KP_ENTER",
|
||||
"SDLK_KP_1",
|
||||
"SDLK_KP_2",
|
||||
"SDLK_KP_3",
|
||||
"SDLK_KP_4",
|
||||
"SDLK_KP_5",
|
||||
"SDLK_KP_6",
|
||||
"SDLK_KP_7",
|
||||
"SDLK_KP_8",
|
||||
"SDLK_KP_9",
|
||||
"SDLK_KP_0",
|
||||
"SDLK_KP_PERIOD",
|
||||
"SDLK_APPLICATION",
|
||||
"SDLK_POWER",
|
||||
"SDLK_KP_EQUALS",
|
||||
"SDLK_F13",
|
||||
"SDLK_F14",
|
||||
"SDLK_F15",
|
||||
"SDLK_F16",
|
||||
"SDLK_F17",
|
||||
"SDLK_F18",
|
||||
"SDLK_F19",
|
||||
"SDLK_F20",
|
||||
"SDLK_F21",
|
||||
"SDLK_F22",
|
||||
"SDLK_F23",
|
||||
"SDLK_F24",
|
||||
"SDLK_EXECUTE",
|
||||
"SDLK_HELP",
|
||||
"SDLK_MENU",
|
||||
"SDLK_SELECT",
|
||||
"SDLK_STOP",
|
||||
"SDLK_AGAIN",
|
||||
"SDLK_UNDO",
|
||||
"SDLK_CUT",
|
||||
"SDLK_COPY",
|
||||
"SDLK_PASTE",
|
||||
"SDLK_FIND",
|
||||
"SDLK_MUTE",
|
||||
"SDLK_VOLUMEUP",
|
||||
"SDLK_VOLUMEDOWN",
|
||||
"SDLK_KP_COMMA",
|
||||
"SDLK_KP_EQUALSAS400",
|
||||
"SDLK_ALTERASE",
|
||||
"SDLK_SYSREQ",
|
||||
"SDLK_CANCEL",
|
||||
"SDLK_CLEAR",
|
||||
"SDLK_PRIOR",
|
||||
"SDLK_RETURN2",
|
||||
"SDLK_SEPARATOR",
|
||||
"SDLK_OUT",
|
||||
"SDLK_OPER",
|
||||
"SDLK_CLEARAGAIN",
|
||||
"SDLK_CRSEL",
|
||||
"SDLK_EXSEL",
|
||||
"SDLK_KP_00",
|
||||
"SDLK_KP_000",
|
||||
"SDLK_THOUSANDSSEPARATOR",
|
||||
"SDLK_DECIMALSEPARATOR",
|
||||
"SDLK_CURRENCYUNIT",
|
||||
"SDLK_CURRENCYSUBUNIT",
|
||||
"SDLK_KP_LEFTPAREN",
|
||||
"SDLK_KP_RIGHTPAREN",
|
||||
"SDLK_KP_LEFTBRACE",
|
||||
"SDLK_KP_RIGHTBRACE",
|
||||
"SDLK_KP_TAB",
|
||||
"SDLK_KP_BACKSPACE",
|
||||
"SDLK_KP_A",
|
||||
"SDLK_KP_B",
|
||||
"SDLK_KP_C",
|
||||
"SDLK_KP_D",
|
||||
"SDLK_KP_E",
|
||||
"SDLK_KP_F",
|
||||
"SDLK_KP_XOR",
|
||||
"SDLK_KP_POWER",
|
||||
"SDLK_KP_PERCENT",
|
||||
"SDLK_KP_LESS",
|
||||
"SDLK_KP_GREATER",
|
||||
"SDLK_KP_AMPERSAND",
|
||||
"SDLK_KP_DBLAMPERSAND",
|
||||
"SDLK_KP_VERTICALBAR",
|
||||
"SDLK_KP_DBLVERTICALBAR",
|
||||
"SDLK_KP_COLON",
|
||||
"SDLK_KP_HASH",
|
||||
"SDLK_KP_SPACE",
|
||||
"SDLK_KP_AT",
|
||||
"SDLK_KP_EXCLAM",
|
||||
"SDLK_KP_MEMSTORE",
|
||||
"SDLK_KP_MEMRECALL",
|
||||
"SDLK_KP_MEMCLEAR",
|
||||
"SDLK_KP_MEMADD",
|
||||
"SDLK_KP_MEMSUBTRACT",
|
||||
"SDLK_KP_MEMMULTIPLY",
|
||||
"SDLK_KP_MEMDIVIDE",
|
||||
"SDLK_KP_PLUSMINUS",
|
||||
"SDLK_KP_CLEAR",
|
||||
"SDLK_KP_CLEARENTRY",
|
||||
"SDLK_KP_BINARY",
|
||||
"SDLK_KP_OCTAL",
|
||||
"SDLK_KP_DECIMAL",
|
||||
"SDLK_KP_HEXADECIMAL",
|
||||
"SDLK_LCTRL",
|
||||
"SDLK_LSHIFT",
|
||||
"SDLK_LALT",
|
||||
"SDLK_LGUI",
|
||||
"SDLK_RCTRL",
|
||||
"SDLK_RSHIFT",
|
||||
"SDLK_RALT",
|
||||
"SDLK_RGUI",
|
||||
"SDLK_MODE",
|
||||
"SDLK_AUDIONEXT",
|
||||
"SDLK_AUDIOPREV",
|
||||
"SDLK_AUDIOSTOP",
|
||||
"SDLK_AUDIOPLAY",
|
||||
"SDLK_AUDIOMUTE",
|
||||
"SDLK_MEDIASELECT",
|
||||
"SDLK_WWW",
|
||||
"SDLK_MAIL",
|
||||
"SDLK_CALCULATOR",
|
||||
"SDLK_COMPUTER",
|
||||
"SDLK_AC_SEARCH",
|
||||
"SDLK_AC_HOME",
|
||||
"SDLK_AC_BACK",
|
||||
"SDLK_AC_FORWARD",
|
||||
"SDLK_AC_STOP",
|
||||
"SDLK_AC_REFRESH",
|
||||
"SDLK_AC_BOOKMARKS",
|
||||
"SDLK_BRIGHTNESSDOWN",
|
||||
"SDLK_BRIGHTNESSUP",
|
||||
"SDLK_DISPLAYSWITCH",
|
||||
"SDLK_KBDILLUMTOGGLE",
|
||||
"SDLK_KBDILLUMDOWN",
|
||||
"SDLK_KBDILLUMUP",
|
||||
"SDLK_EJECT",
|
||||
"SDLK_SLEEP",
|
||||
"SDLK_APP1",
|
||||
"SDLK_APP2",
|
||||
"SDLK_AUDIOREWIND",
|
||||
"SDLK_AUDIOFASTFORWARD",
|
||||
"SDLK_SOFTLEFT",
|
||||
"SDLK_SOFTRIGHT",
|
||||
"SDLK_CALL",
|
||||
"SDLK_ENDCALL",
|
||||
)
|
||||
|
||||
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
binary_sensor.binary_sensor_schema(BinarySensor)
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display
|
||||
from esphome.components.snapshot import Snapshot, register_snapshot
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DIMENSIONS,
|
||||
@@ -17,21 +16,14 @@ from esphome.const import (
|
||||
CONF_Y,
|
||||
PLATFORM_HOST,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import SDL_KEYMAP
|
||||
|
||||
AUTO_LOAD = ["snapshot"]
|
||||
|
||||
sdl_ns = cg.esphome_ns.namespace("sdl")
|
||||
Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component, Snapshot)
|
||||
Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component)
|
||||
sdl_window_flags = cg.global_ns.enum("SDL_WindowFlags")
|
||||
|
||||
|
||||
CONF_CENTERED_ON_DISPLAY = "centered_on_display"
|
||||
CONF_HEADLESS = "headless"
|
||||
CONF_SNAPSHOT_KEY = "snapshot_key"
|
||||
CONF_SDL_OPTIONS = "sdl_options"
|
||||
CONF_SDL_ID = "sdl_id"
|
||||
CONF_WINDOW_OPTIONS = "window_options"
|
||||
@@ -75,29 +67,12 @@ def _validate_position(config: dict) -> dict:
|
||||
raise cv.Invalid("Must specify either 'x' and 'y' or 'centered_on_display'")
|
||||
|
||||
|
||||
def _validate_headless(config: ConfigType) -> ConfigType:
|
||||
if not config[CONF_HEADLESS]:
|
||||
return config
|
||||
if CONF_WINDOW_OPTIONS in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_WINDOW_OPTIONS}' has no effect when '{CONF_HEADLESS}' is set - there is no window"
|
||||
)
|
||||
if CONF_SNAPSHOT_KEY in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_SNAPSHOT_KEY}' cannot be used when '{CONF_HEADLESS}' is set - "
|
||||
f"there is no keyboard. Use the 'snapshot.take' action instead"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Sdl),
|
||||
cv.Optional(CONF_SDL_OPTIONS, default=""): get_sdl_options,
|
||||
cv.Optional(CONF_HEADLESS, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SNAPSHOT_KEY): cv.enum(SDL_KEYMAP),
|
||||
cv.Required(CONF_DIMENSIONS): cv.Any(
|
||||
cv.dimensions,
|
||||
cv.Schema(
|
||||
@@ -124,42 +99,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
)
|
||||
),
|
||||
_validate_headless,
|
||||
cv.only_on(PLATFORM_HOST),
|
||||
)
|
||||
|
||||
|
||||
def headless_final_validate(platform: str) -> cv.Schema:
|
||||
"""Build a FINAL_VALIDATE_SCHEMA rejecting a platform whose sdl display is headless.
|
||||
|
||||
Mouse and keyboard platforms are driven by window events, so under a headless display they
|
||||
would never report anything.
|
||||
"""
|
||||
|
||||
def validate_display(display_config: ConfigType) -> ConfigType:
|
||||
if display_config.get(CONF_HEADLESS):
|
||||
raise cv.Invalid(
|
||||
f"The sdl {platform} platform needs a window, but its display has "
|
||||
f"'{CONF_HEADLESS}' set"
|
||||
)
|
||||
return display_config
|
||||
|
||||
return cv.Schema(
|
||||
{cv.Required(CONF_SDL_ID): fv.id_declaration_match_schema(validate_display)},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
for option in config[CONF_SDL_OPTIONS].split():
|
||||
cg.add_build_flag(option)
|
||||
cg.add_build_flag("-DSDL_BYTEORDER=4321")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await register_snapshot(var, config)
|
||||
cg.add(var.set_headless(config[CONF_HEADLESS]))
|
||||
if (key := config.get(CONF_SNAPSHOT_KEY)) is not None:
|
||||
cg.add(var.set_snapshot_key(key))
|
||||
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
|
||||
@@ -2,17 +2,8 @@
|
||||
#include "sdl_esphome.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace esphome::sdl {
|
||||
|
||||
namespace {
|
||||
|
||||
// Key under which each window keeps a pointer back to its Sdl instance.
|
||||
constexpr const char *const WINDOW_DATA_KEY = "esphome_sdl";
|
||||
|
||||
} // namespace
|
||||
|
||||
int Sdl::get_width() {
|
||||
switch (this->rotation_) {
|
||||
case display::DISPLAY_ROTATION_90_DEGREES:
|
||||
@@ -37,96 +28,17 @@ int Sdl::get_height() {
|
||||
}
|
||||
}
|
||||
|
||||
void Sdl::destroy_renderer_() {
|
||||
// Reverse order of creation: the renderer refers to the window or surface it was made from.
|
||||
if (this->shot_target_ != nullptr) {
|
||||
SDL_DestroyTexture(this->shot_target_);
|
||||
this->shot_target_ = nullptr;
|
||||
}
|
||||
if (this->texture_ != nullptr) {
|
||||
SDL_DestroyTexture(this->texture_);
|
||||
this->texture_ = nullptr;
|
||||
}
|
||||
if (this->renderer_ != nullptr) {
|
||||
SDL_DestroyRenderer(this->renderer_);
|
||||
this->renderer_ = nullptr;
|
||||
}
|
||||
if (this->window_ != nullptr) {
|
||||
SDL_DestroyWindow(this->window_);
|
||||
this->window_ = nullptr;
|
||||
}
|
||||
if (this->surface_ != nullptr) {
|
||||
SDL_FreeSurface(this->surface_);
|
||||
this->surface_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool Sdl::setup_failed_(const char *what) {
|
||||
ESP_LOGE(TAG, "%s: %s", what, SDL_GetError());
|
||||
// Give back whatever was created before the failure. Without this a half set up display leaves an
|
||||
// empty window on screen for the life of the process, still registered as an event target.
|
||||
this->destroy_renderer_();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Sdl::setup_renderer_() {
|
||||
SDL_SetMainReady();
|
||||
if (this->headless_) {
|
||||
// SDL_INIT_VIDEO is deliberately not requested: a software renderer bound to a surface needs no
|
||||
// video device, so this works on a machine with no display server at all.
|
||||
if (SDL_Init(0) != 0)
|
||||
return this->setup_failed_("SDL_Init failed");
|
||||
this->surface_ = SDL_CreateRGBSurfaceWithFormat(0, this->width_, this->height_, 16, SDL_PIXELFORMAT_RGB565);
|
||||
if (this->surface_ == nullptr)
|
||||
return this->setup_failed_("Could not create offscreen surface");
|
||||
this->renderer_ = SDL_CreateSoftwareRenderer(this->surface_);
|
||||
} else {
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0)
|
||||
return this->setup_failed_("SDL_Init failed");
|
||||
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
|
||||
this->window_options_);
|
||||
if (this->window_ == nullptr)
|
||||
return this->setup_failed_("Could not create window");
|
||||
// Lets loop() find the display an event belongs to, so one display does not act on another's
|
||||
// input when several windows are open.
|
||||
SDL_SetWindowData(this->window_, WINDOW_DATA_KEY, this);
|
||||
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
|
||||
}
|
||||
if (this->renderer_ == nullptr)
|
||||
return this->setup_failed_("Could not create renderer");
|
||||
if (SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_) != 0)
|
||||
return this->setup_failed_("Could not set renderer logical size");
|
||||
void Sdl::setup() {
|
||||
SDL_Init(SDL_INIT_VIDEO);
|
||||
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
|
||||
this->window_options_);
|
||||
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
|
||||
SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_);
|
||||
this->texture_ =
|
||||
SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_);
|
||||
if (this->texture_ == nullptr)
|
||||
return this->setup_failed_("Could not create texture");
|
||||
// The texture has no alpha channel, so blending is pointless. Headless it would also force a
|
||||
// different software blit path onto the 16 bit target surface.
|
||||
if (SDL_SetTextureBlendMode(this->texture_, this->headless_ ? SDL_BLENDMODE_NONE : SDL_BLENDMODE_BLEND) != 0)
|
||||
return this->setup_failed_("Could not set texture blend mode");
|
||||
return true;
|
||||
SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND);
|
||||
}
|
||||
|
||||
void Sdl::setup() {
|
||||
if (!this->setup_renderer_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
if (this->headless_) {
|
||||
// Nothing generates events, so there is nothing for loop() to do.
|
||||
this->disable_loop();
|
||||
} else if (this->snapshot_key_ != 0) {
|
||||
this->add_key_listener(this->snapshot_key_, [this](bool down) {
|
||||
if (down && !this->take_snapshot(nullptr)) {
|
||||
ESP_LOGW(TAG, "snapshot key did not write a file");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Sdl::update() {
|
||||
if (this->texture_ == nullptr)
|
||||
return;
|
||||
this->do_update_();
|
||||
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_))
|
||||
return;
|
||||
@@ -139,19 +51,12 @@ void Sdl::update() {
|
||||
}
|
||||
|
||||
void Sdl::redraw_(SDL_Rect &rect) {
|
||||
// Nothing to present when headless - a snapshot blits the whole texture when it needs it, so
|
||||
// doing it here as well would just burn CPU. draw_pixels_at() calls this on every partial
|
||||
// update, so it is worth skipping.
|
||||
if (this->headless_)
|
||||
return;
|
||||
SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
|
||||
SDL_RenderPresent(this->renderer_);
|
||||
}
|
||||
|
||||
void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
if (this->texture_ == nullptr)
|
||||
return;
|
||||
SDL_Rect rect{x_start, y_start, w, h};
|
||||
if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
@@ -164,7 +69,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
|
||||
}
|
||||
|
||||
void Sdl::draw_pixel_at(int x, int y, Color color) {
|
||||
if (this->texture_ == nullptr || !this->get_clipping().inside(x, y))
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
|
||||
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
@@ -199,148 +104,61 @@ void Sdl::process_key(uint32_t keycode, bool down) {
|
||||
callback->second(down);
|
||||
}
|
||||
|
||||
Sdl *Sdl::instance_for_window_(uint32_t window_id) {
|
||||
SDL_Window *window = SDL_GetWindowFromID(window_id);
|
||||
if (window == nullptr)
|
||||
return nullptr;
|
||||
return static_cast<Sdl *>(SDL_GetWindowData(window, WINDOW_DATA_KEY));
|
||||
}
|
||||
|
||||
void Sdl::handle_event_(const SDL_Event &event) {
|
||||
switch (event.type) {
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (event.button.button == 1) {
|
||||
this->mouse_x = event.button.x;
|
||||
this->mouse_y = event.button.y;
|
||||
this->mouse_down = event.button.state != 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_MOUSEMOTION:
|
||||
if (event.motion.state & 1) {
|
||||
this->mouse_x = event.motion.x;
|
||||
this->mouse_y = event.motion.y;
|
||||
this->mouse_down = true;
|
||||
} else {
|
||||
this->mouse_down = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_KEYDOWN:
|
||||
// Ignore auto-repeat, otherwise holding a key floods the listeners.
|
||||
if (event.key.repeat != 0)
|
||||
break;
|
||||
ESP_LOGD(TAG, "keydown %d", event.key.keysym.sym);
|
||||
this->process_key(event.key.keysym.sym, true);
|
||||
break;
|
||||
|
||||
case SDL_KEYUP:
|
||||
ESP_LOGD(TAG, "keyup %d", event.key.keysym.sym);
|
||||
this->process_key(event.key.keysym.sym, false);
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
switch (event.window.event) {
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
case SDL_WINDOWEVENT_RESIZED: {
|
||||
SDL_Rect rect{0, 0, this->width_, this->height_};
|
||||
this->redraw_(rect);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Sdl::loop() {
|
||||
SDL_Event e;
|
||||
// Take everything that is waiting, not one event per loop. A touch drag produces a burst of
|
||||
// motion events, and consuming them one at a time lets the queue grow without bound, so the
|
||||
// pointer ends up acting on input from further and further in the past. Draining collapses a
|
||||
// burst to the position it ended at, which is the one the user is asking for anyway.
|
||||
while (SDL_PollEvent(&e)) {
|
||||
if (e.type == SDL_QUIT)
|
||||
exit(0);
|
||||
|
||||
// Events carry the window they happened in, so send each one to the display that owns it.
|
||||
uint32_t window_id;
|
||||
if (SDL_PollEvent(&e)) {
|
||||
switch (e.type) {
|
||||
case SDL_QUIT:
|
||||
exit(0);
|
||||
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
window_id = e.button.windowID;
|
||||
if (e.button.button == 1) {
|
||||
this->mouse_x = e.button.x;
|
||||
this->mouse_y = e.button.y;
|
||||
this->mouse_down = e.button.state != 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_MOUSEMOTION:
|
||||
window_id = e.motion.windowID;
|
||||
if (e.motion.state & 1) {
|
||||
this->mouse_x = e.button.x;
|
||||
this->mouse_y = e.button.y;
|
||||
this->mouse_down = true;
|
||||
} else {
|
||||
this->mouse_down = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_KEYDOWN:
|
||||
ESP_LOGD(TAG, "keydown %d", e.key.keysym.sym);
|
||||
this->process_key(e.key.keysym.sym, true);
|
||||
break;
|
||||
|
||||
case SDL_KEYUP:
|
||||
window_id = e.key.windowID;
|
||||
ESP_LOGD(TAG, "keyup %d", e.key.keysym.sym);
|
||||
this->process_key(e.key.keysym.sym, false);
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
window_id = e.window.windowID;
|
||||
switch (e.window.event) {
|
||||
case SDL_WINDOWEVENT_SIZE_CHANGED:
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
case SDL_WINDOWEVENT_RESIZED: {
|
||||
SDL_Rect rect{0, 0, this->width_, this->height_};
|
||||
this->redraw_(rect);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Anything else, including the touch events SDL reports alongside the mouse events it
|
||||
// synthesises from them, is not used here.
|
||||
ESP_LOGV(TAG, "Event %d", e.type);
|
||||
continue;
|
||||
}
|
||||
|
||||
Sdl *target = instance_for_window_(window_id);
|
||||
if (target == nullptr) {
|
||||
// Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise
|
||||
// input that stops working leaves no trace at all.
|
||||
ESP_LOGV(TAG, "Event %d for unknown window %u", e.type, window_id);
|
||||
continue;
|
||||
}
|
||||
target->handle_event_(e);
|
||||
}
|
||||
}
|
||||
|
||||
bool Sdl::capture_bgr(uint8_t *dest, size_t row_stride) {
|
||||
if (this->texture_ == nullptr || this->renderer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Snapshot requested but SDL is not set up");
|
||||
return false;
|
||||
}
|
||||
if (this->shot_target_ == nullptr) {
|
||||
this->shot_target_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_TARGET,
|
||||
this->width_, this->height_);
|
||||
if (this->shot_target_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not create capture texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
SDL_SetTextureBlendMode(this->shot_target_, SDL_BLENDMODE_NONE);
|
||||
}
|
||||
|
||||
// Render into an offscreen target first. SDL_RenderReadPixels works in physical output pixels and
|
||||
// ignores the logical size, so reading straight off a resizable window would read more pixels than
|
||||
// there is room for.
|
||||
// Every step is checked: a failed clear or copy would otherwise be read back as a blank or stale
|
||||
// picture, written out, and reported as a snapshot that worked.
|
||||
bool ok = false;
|
||||
if (SDL_SetRenderTarget(this->renderer_, this->shot_target_) == 0) {
|
||||
ok = SDL_SetRenderDrawColor(this->renderer_, 0, 0, 0, SDL_ALPHA_OPAQUE) == 0 &&
|
||||
SDL_RenderClear(this->renderer_) == 0 &&
|
||||
SDL_RenderCopy(this->renderer_, this->texture_, nullptr, nullptr) == 0 &&
|
||||
SDL_RenderReadPixels(this->renderer_, nullptr, SDL_PIXELFORMAT_BGR24, dest, static_cast<int>(row_stride)) == 0;
|
||||
if (SDL_SetRenderTarget(this->renderer_, nullptr) != 0) {
|
||||
// Stuck rendering into shot_target_ from here on, so there's no point continuing.
|
||||
ESP_LOGE(TAG, "Could not restore the render target: %s", SDL_GetError());
|
||||
this->mark_failed();
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
ESP_LOGE(TAG, "Could not capture the screen: %s", SDL_GetError());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace esphome::sdl
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/snapshot/snapshot.h"
|
||||
#define SDL_MAIN_HANDLED
|
||||
#include "SDL.h"
|
||||
#include <map>
|
||||
@@ -15,7 +13,7 @@ namespace esphome::sdl {
|
||||
|
||||
constexpr static const char *const TAG = "sdl";
|
||||
|
||||
class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
class Sdl final : public display::Display {
|
||||
public:
|
||||
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
|
||||
void update() override;
|
||||
@@ -34,9 +32,6 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
this->pos_x_ = pos_x;
|
||||
this->pos_y_ = pos_y;
|
||||
}
|
||||
void set_headless(bool headless) { this->headless_ = headless; }
|
||||
void set_snapshot_key(int32_t keycode) { this->snapshot_key_ = keycode; }
|
||||
|
||||
int get_width() override;
|
||||
int get_height() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
@@ -56,40 +51,20 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
void redraw_(SDL_Rect &rect);
|
||||
bool setup_renderer_();
|
||||
/// Release the window, surface, renderer and textures, and forget them.
|
||||
void destroy_renderer_();
|
||||
/// Log an SDL failure during setup, release anything already created, and return false.
|
||||
bool setup_failed_(const char *what);
|
||||
int snapshot_width() override { return this->width_; }
|
||||
int snapshot_height() override { return this->height_; }
|
||||
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
|
||||
void handle_event_(const SDL_Event &event);
|
||||
/// The display owning the given window, or nullptr if it is not one of ours.
|
||||
static Sdl *instance_for_window_(uint32_t window_id);
|
||||
SDL_Renderer *renderer_{};
|
||||
SDL_Window *window_{};
|
||||
SDL_Texture *texture_{};
|
||||
// Offscreen render target used when headless. SDL_CreateSoftwareRenderer only borrows the
|
||||
// surface, and the renderer goes back to using it as its output whenever the capture target is
|
||||
// released, so it has to stay alive as long as the renderer does.
|
||||
SDL_Surface *surface_{};
|
||||
// Capture target, created on first snapshot.
|
||||
SDL_Texture *shot_target_{};
|
||||
std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{};
|
||||
int width_{};
|
||||
int height_{};
|
||||
uint32_t window_options_{0};
|
||||
int32_t pos_x_{SDL_WINDOWPOS_UNDEFINED};
|
||||
int32_t pos_y_{SDL_WINDOWPOS_UNDEFINED};
|
||||
int32_t snapshot_key_{0};
|
||||
SDL_Renderer *renderer_{};
|
||||
SDL_Window *window_{};
|
||||
SDL_Texture *texture_{};
|
||||
uint16_t x_low_{0};
|
||||
uint16_t y_low_{0};
|
||||
uint16_t x_high_{0};
|
||||
uint16_t y_high_{0};
|
||||
bool headless_{false};
|
||||
std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::sdl
|
||||
|
||||
#endif
|
||||
|
||||
@@ -4,12 +4,10 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..display import CONF_SDL_ID, Sdl, headless_final_validate, sdl_ns
|
||||
from ..display import CONF_SDL_ID, Sdl, sdl_ns
|
||||
|
||||
SdlTouchscreen = sdl_ns.class_("SdlTouchscreen", touchscreen.Touchscreen)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = headless_final_validate("touchscreen")
|
||||
|
||||
|
||||
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
|
||||
{
|
||||
|
||||
@@ -31,7 +31,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
baud_rate=115200,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
@@ -33,6 +33,8 @@ void MR60FDA2Component::dump_config() {
|
||||
|
||||
// Initialisation functions
|
||||
void MR60FDA2Component::setup() {
|
||||
this->check_uart_settings(115200);
|
||||
|
||||
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
|
||||
this->current_frame_id_ = 0;
|
||||
this->current_frame_len_ = 0;
|
||||
|
||||
@@ -18,10 +18,9 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_NAME, CONF_UART_ID
|
||||
from esphome.const import CONF_ID, CONF_NAME
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -31,18 +30,14 @@ MULTI_CONF = True
|
||||
|
||||
serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy")
|
||||
SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice)
|
||||
SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap")
|
||||
|
||||
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
|
||||
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
|
||||
# User-selectable electrical types. USB_SERIAL is deliberately absent: it is derived
|
||||
# from the uart_id pointing at a usb_uart channel, never set by the user.
|
||||
SERIAL_PROXY_PORT_TYPES = {
|
||||
"TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL,
|
||||
"RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232,
|
||||
"RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485,
|
||||
}
|
||||
PORT_TYPE_USB_SERIAL = SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_USB_SERIAL
|
||||
|
||||
CONF_DTR_PIN = "dtr_pin"
|
||||
CONF_PORT_TYPE = "port_type"
|
||||
@@ -67,7 +62,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SerialProxy),
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Optional(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
|
||||
cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
|
||||
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
@@ -77,26 +72,6 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
def _uses_usb_uart(config: ConfigType, full_config: ConfigType) -> bool:
|
||||
from esphome.components.usb_uart import is_usb_uart_channel
|
||||
|
||||
return is_usb_uart_channel(config[CONF_UART_ID], full_config)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
if _uses_usb_uart(config, fv.full_config.get()):
|
||||
if CONF_PORT_TYPE in config:
|
||||
raise cv.Invalid(
|
||||
f"{CONF_PORT_TYPE} is set automatically for USB serial ports"
|
||||
)
|
||||
elif CONF_PORT_TYPE not in config:
|
||||
raise cv.Invalid(f"{CONF_PORT_TYPE} is required")
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_serial_proxy_count_define() -> None:
|
||||
"""Emit the SERIAL_PROXY_COUNT define once with the final instance count."""
|
||||
@@ -111,13 +86,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
await uart.register_uart_device(var, config)
|
||||
cg.add(cg.App.register_serial_proxy(var))
|
||||
cg.add(var.set_name(config[CONF_NAME]))
|
||||
if _uses_usb_uart(config, CORE.config):
|
||||
cg.add(var.set_port_type(PORT_TYPE_USB_SERIAL))
|
||||
channel = await cg.get_variable(config[CONF_UART_ID])
|
||||
cg.add(var.set_usb_channel(channel))
|
||||
cg.add_define("USE_SERIAL_PROXY_USB_INFO")
|
||||
else:
|
||||
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
|
||||
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
|
||||
cg.add_define("USE_SERIAL_PROXY")
|
||||
|
||||
# Track instance count for the FINAL priority define
|
||||
|
||||
@@ -12,10 +12,6 @@
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
#include "esphome/components/usb_uart/usb_uart.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::serial_proxy {
|
||||
|
||||
static const char *const TAG = "serial_proxy";
|
||||
@@ -33,57 +29,26 @@ void SerialProxy::setup() {
|
||||
#ifdef USE_API
|
||||
// instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data
|
||||
this->outgoing_msg_.instance = this->instance_index_;
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
// A tap sets itself up before this runs (its setup priority is higher), so it may
|
||||
// already be waiting on the port -- a boot-time handshake with the device, say. Leaving
|
||||
// the loop enabled is what lets that finish; without it the tap would stall until a
|
||||
// client happened to subscribe.
|
||||
if (this->tap_ != nullptr && this->tap_->tap_needs_port()) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// No subscriber at startup; disable loop until a client subscribes
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
void SerialProxy::reset_mode_() {
|
||||
// The mode belongs to a session, not to the port. Carrying a departed client's choice
|
||||
// over to the next one would inject protocol bytes into a stream that never asked for
|
||||
// them -- a firmware upload, or any client built before this request existed and so
|
||||
// unable to turn it off. Guessing RAW is the safe direction: a client that wanted
|
||||
// protocol handling and did not ask for it merely sends its own acknowledgements.
|
||||
if (this->mode_ == api::enums::SERIAL_PROXY_MODE_RAW) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Session ended, returning serial proxy [%" PRIu32 "] to RAW mode", this->instance_index_);
|
||||
this->mode_ = api::enums::SERIAL_PROXY_MODE_RAW;
|
||||
}
|
||||
#endif
|
||||
|
||||
void SerialProxy::loop() {
|
||||
#ifdef USE_API
|
||||
// Detect subscriber disconnect
|
||||
if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() ||
|
||||
!this->api_connection_->is_connection_setup() || !api_is_connected())) {
|
||||
ESP_LOGW(TAG, "Subscriber disconnected");
|
||||
this->api_connection_ = nullptr;
|
||||
this->reset_mode_();
|
||||
}
|
||||
|
||||
// With no subscriber there is normally nothing to do, but a tap may still need the port
|
||||
// read -- it does its protocol work precisely while nobody else is listening.
|
||||
// Safety check — loop should only run when subscribed, but guard against races
|
||||
if (this->api_connection_ == nullptr) [[unlikely]] {
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
#else
|
||||
this->disable_loop();
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Detect subscriber disconnect
|
||||
if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() ||
|
||||
!api_is_connected()) {
|
||||
ESP_LOGW(TAG, "Subscriber disconnected");
|
||||
this->api_connection_ = nullptr;
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read available data from UART and forward to subscribed client
|
||||
@@ -104,54 +69,11 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) {
|
||||
if (!this->read_array(buffer, to_read))
|
||||
return;
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
// Before forwarding, so a tap that answers the device (an acknowledgement, say) is not
|
||||
// waiting on the network round trip to a subscriber that may not even exist.
|
||||
if (this->tap_observing_()) {
|
||||
this->tap_->on_device_rx(buffer, to_read);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (this->api_connection_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
this->outgoing_msg_.set_data(buffer, to_read);
|
||||
this->api_connection_->send_serial_proxy_data(this->outgoing_msg_);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
|
||||
bool SerialProxy::tap_observing_() const {
|
||||
if (this->tap_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
// With no subscriber, a tap doing its own protocol work (the boot-time handshake with
|
||||
// the device, say) is served regardless of mode -- nobody has chosen one yet. Once a
|
||||
// subscriber holds the port, the mode alone decides, so RAW stays inert.
|
||||
if (this->api_connection_ == nullptr && this->tap_->tap_needs_port()) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise the mode decides. RAW must be inert: a client that flips to RAW before
|
||||
// flashing firmware is entitled to a byte pipe with nothing injecting protocol bytes
|
||||
// into it, and "the tap turned out not to recognise the stream" is not good enough.
|
||||
return this->mode_ == api::enums::SERIAL_PROXY_MODE_PROTOCOL;
|
||||
}
|
||||
|
||||
void SerialProxy::tap_pump() {
|
||||
#ifdef USE_API
|
||||
// Nothing would consume the bytes; leave them in the FIFO
|
||||
if (!this->tap_observing_() && this->api_connection_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
const size_t available = this->available();
|
||||
if (available > 0) {
|
||||
this->read_and_send_(available);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void SerialProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Serial Proxy [%" PRIu32 "]:\n"
|
||||
@@ -160,10 +82,9 @@ void SerialProxy::dump_config() {
|
||||
" RTS Pin: %s\n"
|
||||
" DTR Pin: %s",
|
||||
this->instance_index_, this->name_ != nullptr ? this->name_ : "",
|
||||
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
|
||||
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
|
||||
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL ? LOG_STR_LITERAL("USB_SERIAL")
|
||||
: LOG_STR_LITERAL("TTL"),
|
||||
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
|
||||
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
|
||||
: LOG_STR_LITERAL("TTL"),
|
||||
this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"),
|
||||
this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"));
|
||||
}
|
||||
@@ -171,9 +92,8 @@ void SerialProxy::dump_config() {
|
||||
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
|
||||
uint8_t parity, uint8_t stop_bits, uint8_t data_size) {
|
||||
#ifdef USE_API
|
||||
if (!this->is_subscriber_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring configure request from client without port subscription [%" PRIu32 "]",
|
||||
this->instance_index_);
|
||||
if (this->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
#endif
|
||||
@@ -210,26 +130,17 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// Skip a no-op reconfigure. Clients routinely re-send identical settings on every
|
||||
// port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
|
||||
// Some bridges watch line-coding changes as a signalling channel (a magic baud
|
||||
// sequence to enter a bootloader, say), so redundant applies are not harmless.
|
||||
static const uart::UARTParityOptions PARITY_MAP[] = {
|
||||
uart::UART_CONFIG_PARITY_NONE,
|
||||
uart::UART_CONFIG_PARITY_EVEN,
|
||||
uart::UART_CONFIG_PARITY_ODD,
|
||||
};
|
||||
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
|
||||
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
|
||||
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
// Apply validated parameters
|
||||
uart_comp->set_baud_rate(baudrate);
|
||||
uart_comp->set_stop_bits(stop_bits);
|
||||
uart_comp->set_data_bits(data_size);
|
||||
|
||||
// Map parity value to UARTParityOptions
|
||||
static const uart::UARTParityOptions PARITY_MAP[] = {
|
||||
uart::UART_CONFIG_PARITY_NONE,
|
||||
uart::UART_CONFIG_PARITY_EVEN,
|
||||
uart::UART_CONFIG_PARITY_ODD,
|
||||
};
|
||||
uart_comp->set_parity(PARITY_MAP[parity]);
|
||||
|
||||
// load_settings() is available on ESP8266 and ESP32 platforms
|
||||
@@ -239,80 +150,24 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
SerialProxyResult SerialProxy::set_mode_from_client(api::APIConnection *api_connection,
|
||||
api::enums::SerialProxyMode mode) {
|
||||
#ifdef USE_API
|
||||
// Only the live subscriber may change the mode, so the mode cannot outlive a session
|
||||
if (!this->is_subscriber_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring mode request from client without port subscription [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
#endif
|
||||
// Values come from a remote client
|
||||
if (mode != api::enums::SERIAL_PROXY_MODE_RAW && mode != api::enums::SERIAL_PROXY_MODE_PROTOCOL) {
|
||||
ESP_LOGW(TAG, "Invalid mode: %" PRIu32, static_cast<uint32_t>(mode));
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
|
||||
}
|
||||
// PROTOCOL on a port with no tap would be a silent no-op; refuse so the client knows
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
const bool has_tap = this->tap_ != nullptr;
|
||||
#else
|
||||
const bool has_tap = false;
|
||||
#endif
|
||||
if (mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL && !has_tap) {
|
||||
ESP_LOGW(TAG, "No tap on serial proxy [%" PRIu32 "]; PROTOCOL mode unavailable", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
|
||||
}
|
||||
ESP_LOGD(TAG, "Serial proxy [%" PRIu32 "] mode set to %s", this->instance_index_,
|
||||
mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL ? LOG_STR_LITERAL("PROTOCOL") : LOG_STR_LITERAL("RAW"));
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
const bool leaving_protocol_mode =
|
||||
this->mode_ != api::enums::SERIAL_PROXY_MODE_RAW && mode == api::enums::SERIAL_PROXY_MODE_RAW;
|
||||
this->mode_ = mode;
|
||||
|
||||
// Only for an explicit client request, not for reset_mode_() at the end of a session:
|
||||
// an ordinary disconnect says nothing about the device, whereas a client deliberately
|
||||
// asking for raw bytes usually precedes changing what the device is.
|
||||
if (leaving_protocol_mode && this->tap_ != nullptr) {
|
||||
this->tap_->on_protocol_disabled();
|
||||
}
|
||||
#endif
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {
|
||||
#ifdef USE_API
|
||||
// Bytes from anyone but the live subscriber would interleave with the subscriber's
|
||||
// traffic -- or with an active tap's -- on the wire
|
||||
if (!this->is_subscriber_(api_connection)) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
ESP_LOGW(TAG, "Ignoring write from client that does not hold serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
} else {
|
||||
// A legacy client streaming writes without subscribing would flood WARN, one per
|
||||
// request; writes are the only high-rate, unacknowledged operation, so keep this
|
||||
// visible without drowning the log
|
||||
ESP_LOGV(TAG, "Ignoring write from client without port subscription [%" PRIu32 "]", this->instance_index_);
|
||||
}
|
||||
// Bytes from a client other than the live subscriber would interleave with the
|
||||
// subscriber's traffic on the wire
|
||||
if (this->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (data == nullptr || len == 0)
|
||||
return;
|
||||
this->write_array(data, len);
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
// After the write, so the tap observes the same ordering the device does
|
||||
if (this->tap_observing_()) {
|
||||
this->tap_->on_client_tx(data, len);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
|
||||
#ifdef USE_API
|
||||
if (!this->is_subscriber_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring modem pin request from client without port subscription [%" PRIu32 "]",
|
||||
this->instance_index_);
|
||||
if (this->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
#endif
|
||||
@@ -338,27 +193,6 @@ SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
#if defined(USE_SERIAL_PROXY_USB_INFO) && defined(USE_API)
|
||||
void SerialProxy::get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyGetUsbInfoResponse &resp) const {
|
||||
if (this->usb_channel_ == nullptr) {
|
||||
resp.status = api::enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
|
||||
return;
|
||||
}
|
||||
resp.interface_number = this->usb_channel_->get_index();
|
||||
if (!this->usb_channel_->get_parent()->get_device_info(info)) {
|
||||
// No device attached right now; not an error
|
||||
return;
|
||||
}
|
||||
resp.connected = true;
|
||||
resp.vendor_id = info.vendor_id;
|
||||
resp.product_id = info.product_id;
|
||||
resp.bcd_device = info.bcd_device;
|
||||
resp.manufacturer = StringRef(info.manufacturer);
|
||||
resp.product = StringRef(info.product);
|
||||
resp.serial_number = StringRef(info.serial_number);
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t SerialProxy::get_modem_pins() const {
|
||||
return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) |
|
||||
(this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u);
|
||||
@@ -367,8 +201,8 @@ uint32_t SerialProxy::get_modem_pins() const {
|
||||
SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
|
||||
#ifdef USE_API
|
||||
// Flushing stalls the port, so it gets the same ownership check as writes
|
||||
if (!this->is_subscriber_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring flush from client without port subscription [%" PRIu32 "]", this->instance_index_);
|
||||
if (this->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
#endif
|
||||
@@ -387,6 +221,11 @@ SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
|
||||
}
|
||||
|
||||
#ifdef USE_API
|
||||
bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) const {
|
||||
return this->api_connection_ != nullptr && this->api_connection_ != api_connection &&
|
||||
this->api_connection_->is_connection_setup();
|
||||
}
|
||||
|
||||
SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection,
|
||||
api::enums::SerialProxyRequestType type) {
|
||||
switch (type) {
|
||||
@@ -404,10 +243,6 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
|
||||
// End the dead client's session before starting the new one, so its mode
|
||||
// cannot leak into a session that never asked for it
|
||||
this->api_connection_ = nullptr;
|
||||
this->reset_mode_();
|
||||
}
|
||||
this->api_connection_ = api_connection;
|
||||
this->enable_loop();
|
||||
@@ -420,15 +255,7 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
this->api_connection_ = nullptr;
|
||||
this->reset_mode_();
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
// Keep the loop alive for a tap that still needs the port (mirrors loop())
|
||||
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
#else
|
||||
this->disable_loop();
|
||||
#endif
|
||||
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
default:
|
||||
|
||||
@@ -20,22 +20,12 @@
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
namespace esphome::usb_uart {
|
||||
class USBUartChannel;
|
||||
} // namespace esphome::usb_uart
|
||||
namespace esphome::usb_host {
|
||||
struct UsbDeviceInfo;
|
||||
} // namespace esphome::usb_host
|
||||
#endif
|
||||
|
||||
// Forward-declare types needed outside the USE_API guard.
|
||||
namespace esphome::api {
|
||||
class APIConnection;
|
||||
namespace enums {
|
||||
enum SerialProxyPortType : uint32_t;
|
||||
enum SerialProxyRequestType : uint32_t;
|
||||
enum SerialProxyMode : uint32_t;
|
||||
} // namespace enums
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -62,36 +52,6 @@ enum class SerialProxyResult : uint8_t {
|
||||
/// Maximum bytes to read from UART in a single loop iteration
|
||||
inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256;
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
/// Observes a port's traffic without owning it, and may inject bytes of its own.
|
||||
///
|
||||
/// This exists so protocol-aware behaviour can be layered onto a plain byte pipe without
|
||||
/// the pipe knowing anything about the protocol: the tap is compiled in only when some
|
||||
/// component asks for one, so a proxy carrying an RS485 meter pays nothing for it.
|
||||
///
|
||||
/// A tap is an observer, never a gatekeeper -- it cannot suppress or alter the bytes
|
||||
/// flowing in either direction, so a misbehaving tap cannot corrupt the stream.
|
||||
class SerialProxyTap {
|
||||
public:
|
||||
/// Bytes read from the device, before they are forwarded to any subscriber.
|
||||
virtual void on_device_rx(const uint8_t *data, size_t len) = 0;
|
||||
|
||||
/// Bytes a subscriber sent towards the device, after they have been written.
|
||||
virtual void on_client_tx(const uint8_t *data, size_t len) = 0;
|
||||
|
||||
/// True when the port must keep reading even with no subscriber attached, so a tap can
|
||||
/// do its own protocol work while nobody is listening. Honoured only while no
|
||||
/// subscriber holds the port; with one attached, the port mode alone decides.
|
||||
virtual bool tap_needs_port() const = 0;
|
||||
|
||||
/// A client explicitly turned protocol handling off for this port. Distinct from the
|
||||
/// automatic reset when a session ends: this one means a client intends to do something
|
||||
/// else with the device -- reflash it, most likely -- so anything the tap believes about
|
||||
/// it should be treated as suspect.
|
||||
virtual void on_protocol_disabled() = 0;
|
||||
};
|
||||
#endif
|
||||
|
||||
class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -117,9 +77,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// Get the port type
|
||||
api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; }
|
||||
|
||||
/// Handle a mode change requested by an API client
|
||||
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode);
|
||||
|
||||
/// Configure UART parameters and apply them
|
||||
/// @param api_connection The API connection requesting the change
|
||||
/// @param baudrate Baud rate in bits per second
|
||||
@@ -164,78 +121,13 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// Set the DTR GPIO pin (from YAML configuration)
|
||||
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
/// Attach the USB UART channel behind this port (from code generation)
|
||||
void set_usb_channel(usb_uart::USBUartChannel *channel) { this->usb_channel_ = channel; }
|
||||
|
||||
#ifdef USE_API
|
||||
/// Fill a USB info response for this port. The response's strings are views into
|
||||
/// info, so info must outlive the send.
|
||||
void get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyGetUsbInfoResponse &resp) const;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
/// Attach a traffic observer. At most one, set once at setup time.
|
||||
void set_tap(SerialProxyTap *tap) { this->tap_ = tap; }
|
||||
|
||||
/// Write bytes originating from the tap rather than from a client. Bypasses the
|
||||
/// subscriber ownership check, but only while the tap is being served bytes -- so a
|
||||
/// port in RAW mode with a subscriber attached stays inert. Returns false when the
|
||||
/// bytes were dropped for that reason.
|
||||
bool write_from_tap(const uint8_t *data, size_t len) {
|
||||
if (!this->tap_observing_()) {
|
||||
return false;
|
||||
}
|
||||
this->write_array(data, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Whether the tap is currently being served bytes. Can flip false with no callback
|
||||
/// (a subscriber attaching in RAW mode, say), so a tap should check before starting
|
||||
/// protocol work and when a reply seems overdue.
|
||||
bool tap_is_observed() const { return this->tap_observing_(); }
|
||||
|
||||
/// Resume reading after a tap's needs change. loop() disables itself when there is
|
||||
/// neither a subscriber nor a tap that wants the port, so a tap starting fresh work
|
||||
/// must ask for it back. Must be called from the main loop.
|
||||
void tap_request_port() { this->enable_loop(); }
|
||||
|
||||
/// Whether the underlying device is present. On a USB UART this tracks enumeration, so
|
||||
/// a tap can notice the device being unplugged and plugged back in.
|
||||
bool is_device_connected() const { return this->parent_->is_connected(); }
|
||||
|
||||
/// Run one read-and-dispatch cycle immediately. Lets a tap make progress before the
|
||||
/// main loop is running -- during setup, for instance, while a component is still
|
||||
/// blocking on can_proceed(). Must not be called from on_device_rx() or
|
||||
/// on_client_tx(): each nested cycle costs a 256-byte stack frame.
|
||||
void tap_pump();
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#ifdef USE_API
|
||||
/// Read from UART, hand the bytes to any tap, and forward them to a subscriber
|
||||
/// (slow path with a 256-byte stack buffer)
|
||||
/// Read from UART and send to API client (slow path with 256-byte stack buffer)
|
||||
void read_and_send_(size_t available);
|
||||
|
||||
/// True when the given connection is the live subscriber. Every port operation
|
||||
/// (write, configure, modem pins, flush, mode) requires this, so an unsubscribed
|
||||
/// client can never share the wire with the subscriber or an active tap.
|
||||
bool is_subscriber_(api::APIConnection *api_connection) const { return this->api_connection_ == api_connection; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
/// Return the port to RAW when a subscriber goes away, so the mode never outlives it
|
||||
void reset_mode_();
|
||||
#else
|
||||
/// Without a tap, PROTOCOL is refused, so the mode is fixed at RAW and there is
|
||||
/// nothing to reset
|
||||
void reset_mode_() {}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
/// True when the tap should be shown the traffic passing through this port
|
||||
bool tap_observing_() const;
|
||||
/// True when a live subscriber other than the given connection holds the port
|
||||
bool port_claimed_by_other_(api::APIConnection *api_connection) const;
|
||||
#endif
|
||||
|
||||
/// Instance index for identifying this proxy in API messages
|
||||
@@ -255,11 +147,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// Port type
|
||||
api::enums::SerialProxyPortType port_type_{};
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
/// How the bytes passing through are treated; zero is SERIAL_PROXY_MODE_RAW
|
||||
api::enums::SerialProxyMode mode_{};
|
||||
#endif
|
||||
|
||||
/// Optional GPIO pins for modem control
|
||||
GPIOPin *rts_pin_{nullptr};
|
||||
GPIOPin *dtr_pin_{nullptr};
|
||||
@@ -267,15 +154,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
|
||||
/// Current modem pin states
|
||||
bool rts_state_{false};
|
||||
bool dtr_state_{false};
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_TAP
|
||||
SerialProxyTap *tap_{nullptr};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY_USB_INFO
|
||||
/// The USB UART channel behind this port; nullptr on non-USB ports
|
||||
usb_uart::USBUartChannel *usb_channel_{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::serial_proxy
|
||||
|
||||
@@ -68,13 +68,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"smt100",
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
"smt100", baud_rate=9600, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ void SMT100Component::dump_config() {
|
||||
LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
this->check_uart_settings(9600);
|
||||
}
|
||||
|
||||
int SMT100Component::readline_(int readch, char *buffer, int len) {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
"""Shared support for writing what a display is showing out to an image file.
|
||||
|
||||
The component itself has no configuration. It provides the ``snapshot.take`` action and the C++
|
||||
base class behind it, so any display that can hand over its pixels - the in memory display in this
|
||||
component, or an SDL window - saves files the same way, under the same directory, with the same
|
||||
rules about names.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
DOMAIN = "snapshot"
|
||||
|
||||
CONF_FILENAME = "filename"
|
||||
|
||||
snapshot_ns = cg.esphome_ns.namespace("snapshot")
|
||||
Snapshot = snapshot_ns.class_("Snapshot")
|
||||
SnapshotAction = snapshot_ns.class_("SnapshotAction", automation.Action)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"snapshot.take",
|
||||
SnapshotAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Snapshot),
|
||||
cv.Optional(CONF_FILENAME): cv.templatable(cv.string),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def snapshot_take_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
if (filename := config.get(CONF_FILENAME)) is not None:
|
||||
cg.add(var.set_filename(await cg.templatable(filename, args, cg.std_string)))
|
||||
return var
|
||||
|
||||
|
||||
@dataclass
|
||||
class SnapshotData:
|
||||
directory_defined: bool = False
|
||||
|
||||
|
||||
def _get_data() -> SnapshotData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = SnapshotData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
async def register_snapshot(var: MockObj, config: ConfigType) -> None:
|
||||
"""Set up a component so that the snapshot action can write its picture to a file."""
|
||||
data = _get_data()
|
||||
# Only once, however many displays there are: two defines that say the same thing do not
|
||||
# compare equal, so asking for this per display repeats the line in defines.h.
|
||||
if not data.directory_defined:
|
||||
data.directory_defined = True
|
||||
cg.add_define(
|
||||
"ESPHOME_SNAPSHOT_DIR",
|
||||
(CORE.data_dir / "snapshots" / CORE.name).as_posix(),
|
||||
)
|
||||
cg.add(var.set_snapshot_prefix(str(config[CONF_ID])))
|
||||
@@ -1,61 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DIMENSIONS,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_LAMBDA,
|
||||
CONF_WIDTH,
|
||||
PLATFORM_HOST,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import Snapshot, register_snapshot, snapshot_ns
|
||||
|
||||
# The base class and the file writing live in the parent component, which nothing else in a
|
||||
# configuration using only this platform would pull in.
|
||||
AUTO_LOAD = ["snapshot"]
|
||||
|
||||
SnapshotDisplay = snapshot_ns.class_(
|
||||
"SnapshotDisplay", display.DisplayBuffer, cg.Component, Snapshot
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SnapshotDisplay),
|
||||
cv.Required(CONF_DIMENSIONS): cv.Any(
|
||||
cv.dimensions,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): cv.positive_not_null_int,
|
||||
cv.Required(CONF_HEIGHT): cv.positive_not_null_int,
|
||||
}
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.only_on(PLATFORM_HOST),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await register_snapshot(var, config)
|
||||
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]))
|
||||
else:
|
||||
(width, height) = dimensions
|
||||
cg.add(var.set_dimensions(width, height))
|
||||
|
||||
if lamb := config.get(CONF_LAMBDA):
|
||||
lambda_ = await cg.process_lambda(
|
||||
lamb, [(display.DisplayRef, "it")], return_type=cg.void
|
||||
)
|
||||
cg.add(var.set_writer(lambda_))
|
||||
@@ -1,80 +0,0 @@
|
||||
#ifdef USE_HOST
|
||||
#include "snapshot_display.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
static const char *const TAG = "snapshot.display";
|
||||
|
||||
namespace {
|
||||
|
||||
/// Spread a channel that only goes up to `max` over the whole 0 to 255 range, so that the
|
||||
/// brightest value stays the brightest. This is the same arithmetic SDL uses, which is what makes
|
||||
/// a picture taken here come out identical to the same picture taken from an SDL window.
|
||||
constexpr uint8_t expand_channel(uint16_t value, uint16_t max) { return static_cast<uint8_t>(value * 255 / max); }
|
||||
|
||||
constexpr uint16_t RED_MAX = 0x1F;
|
||||
constexpr uint16_t GREEN_MAX = 0x3F;
|
||||
constexpr uint16_t BLUE_MAX = 0x1F;
|
||||
|
||||
} // namespace
|
||||
|
||||
void SnapshotDisplay::setup() {
|
||||
this->init_internal_(static_cast<uint32_t>(this->width_) * this->height_ * 2);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed(LOG_STR("Could not allocate display buffer"));
|
||||
}
|
||||
}
|
||||
|
||||
void SnapshotDisplay::dump_config() { LOG_DISPLAY("", "Snapshot", this); }
|
||||
|
||||
void SnapshotDisplay::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
if (this->buffer_ == nullptr || x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
|
||||
return;
|
||||
this->pixels_()[y * this->width_ + x] = display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB);
|
||||
}
|
||||
|
||||
void SnapshotDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr,
|
||||
display::ColorOrder order, display::ColorBitness bitness, bool big_endian,
|
||||
int x_offset, int y_offset, int x_pad) {
|
||||
if (this->buffer_ == nullptr)
|
||||
return;
|
||||
// Anything that is not already laid out the way the buffer is, or that would reach outside it,
|
||||
// goes through the base class, which turns it into one call per pixel with the bounds checked.
|
||||
const bool copyable = this->rotation_ == display::DISPLAY_ROTATION_0_DEGREES &&
|
||||
bitness == display::COLOR_BITNESS_565 && !big_endian && x_start >= 0 && y_start >= 0 &&
|
||||
x_start + w <= this->width_ && y_start + h <= this->height_;
|
||||
if (!copyable) {
|
||||
DisplayBuffer::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
const size_t stride = static_cast<size_t>(x_offset) + w + x_pad;
|
||||
const uint8_t *src = ptr + (stride * y_offset + x_offset) * 2;
|
||||
for (int y = 0; y != h; y++) {
|
||||
memcpy(&this->pixels_()[(y_start + y) * this->width_ + x_start], src + y * stride * 2, w * 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool SnapshotDisplay::capture_bgr(uint8_t *dest, size_t row_stride) {
|
||||
if (this->buffer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Snapshot requested but there is no buffer to read");
|
||||
return false;
|
||||
}
|
||||
const uint16_t *src = this->pixels_();
|
||||
for (int y = 0; y != this->height_; y++) {
|
||||
uint8_t *out = dest + y * row_stride;
|
||||
for (int x = 0; x != this->width_; x++) {
|
||||
const uint16_t pixel = *src++;
|
||||
*out++ = expand_channel(pixel & BLUE_MAX, BLUE_MAX);
|
||||
*out++ = expand_channel((pixel >> 5) & GREEN_MAX, GREEN_MAX);
|
||||
*out++ = expand_channel(pixel >> 11, RED_MAX);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
#endif
|
||||
@@ -1,48 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/snapshot/snapshot.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
/// A display with nowhere to show anything: it keeps the picture in memory, where the snapshot
|
||||
/// action can pick it up. That makes it a way to see what a configuration draws on a machine with
|
||||
/// no screen, and to check the result in a test.
|
||||
class SnapshotDisplay final : public display::DisplayBuffer, public Snapshot {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override { this->do_update_(); }
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
void set_dimensions(uint16_t width, uint16_t height) {
|
||||
this->width_ = width;
|
||||
this->height_ = height;
|
||||
}
|
||||
|
||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
|
||||
protected:
|
||||
void draw_absolute_pixel_internal(int x, int y, Color color) override;
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
int snapshot_width() override { return this->width_; }
|
||||
int snapshot_height() override { return this->height_; }
|
||||
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
|
||||
|
||||
/// The picture, one 16 bit RGB565 value per pixel, topmost row first. Owned by DisplayBuffer as
|
||||
/// a byte pointer; this is the same memory seen as what is actually stored in it.
|
||||
uint16_t *pixels_() { return reinterpret_cast<uint16_t *>(this->buffer_); }
|
||||
|
||||
int width_{};
|
||||
int height_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
|
||||
#endif
|
||||
@@ -1,248 +0,0 @@
|
||||
#ifdef USE_HOST
|
||||
#include "snapshot.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <strings.h>
|
||||
#include <unistd.h>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *const TAG = "snapshot";
|
||||
|
||||
// Longest name we will build a path from. NAME_MAX is 255 and we may append a collision suffix.
|
||||
constexpr size_t MAX_NAME_LENGTH = 200;
|
||||
// Give up rather than spin forever if every candidate name is taken.
|
||||
constexpr unsigned MAX_NAME_ATTEMPTS = 1000;
|
||||
// A BMP file header followed by a BITMAPINFOHEADER, which is where the pixels start.
|
||||
constexpr size_t BMP_HEADER_SIZE = 54;
|
||||
constexpr size_t BMP_INFO_HEADER_SIZE = 40;
|
||||
constexpr int BMP_BITS_PER_PIXEL = 24;
|
||||
|
||||
/// True if the name already ends in ".bmp". The comparison ignores case, so "shot.BMP" is left
|
||||
/// alone rather than turned into "shot.BMP.bmp".
|
||||
bool has_bmp_suffix(const std::string &name) {
|
||||
return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, ".bmp") == 0;
|
||||
}
|
||||
|
||||
/// Reduce a user supplied name to a single safe path component. Everything outside the allowed set
|
||||
/// is replaced, so "..", "/" and absolute paths cannot escape the snapshot directory.
|
||||
/// Returns an empty string if nothing usable is left.
|
||||
std::string sanitise_filename(const char *const name, bool *name_changed) {
|
||||
std::string result;
|
||||
bool all_dots = true;
|
||||
bool changed = false;
|
||||
for (const char *p = name; *p != '\0'; p++) {
|
||||
if (result.size() >= MAX_NAME_LENGTH) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
char c = *p;
|
||||
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_' || c == '-')) {
|
||||
c = '_';
|
||||
changed = true;
|
||||
}
|
||||
if (c != '.')
|
||||
all_dots = false;
|
||||
result.push_back(c);
|
||||
}
|
||||
if (all_dots) {
|
||||
*name_changed = true;
|
||||
return "";
|
||||
}
|
||||
if (!has_bmp_suffix(result))
|
||||
result += ".bmp";
|
||||
*name_changed = changed;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Insert "-<attempt>" before the file extension, e.g. "shot.bmp" -> "shot-1.bmp".
|
||||
std::string add_suffix(const std::string &name, unsigned attempt) {
|
||||
char suffix[12];
|
||||
snprintf(suffix, sizeof(suffix), "-%u", attempt);
|
||||
auto dot = name.rfind('.');
|
||||
if (dot == std::string::npos)
|
||||
return name + suffix;
|
||||
return name.substr(0, dot) + suffix + name.substr(dot);
|
||||
}
|
||||
|
||||
/// Directory snapshots are written to. The environment variable lets a test redirect output
|
||||
/// without rebuilding, matching how the host platform handles ESPHOME_PREFDIR.
|
||||
const char *snapshot_dir() {
|
||||
const char *dir = getenv("ESPHOME_SNAPSHOT_DIR"); // NOLINT(concurrency-mt-unsafe)
|
||||
return dir != nullptr && dir[0] != '\0' ? dir : ESPHOME_SNAPSHOT_DIR;
|
||||
}
|
||||
|
||||
/// Store a value in as many bytes, least significant first, and step the pointer past it.
|
||||
/// BMP is a little endian format whatever the machine writing it uses.
|
||||
void put_le(uint8_t *&dest, uint32_t value, size_t bytes) {
|
||||
for (size_t i = 0; i != bytes; i++)
|
||||
*dest++ = static_cast<uint8_t>(value >> (8 * i));
|
||||
}
|
||||
|
||||
/// The number of bytes one row of `width` pixels takes up in the file. Rows are padded out to a
|
||||
/// multiple of four bytes.
|
||||
size_t bmp_row_size(int width) { return (static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
|
||||
|
||||
/// Write pixels out as a 24 bit BMP. The rows given start with the topmost and are `row_stride`
|
||||
/// bytes apart, which must leave room for a whole padded row; a BMP holds its rows the other way
|
||||
/// up, so they go out last first.
|
||||
bool write_bmp(FILE *file, const uint8_t *pixels, int width, int height, size_t row_stride) {
|
||||
const size_t row_size = bmp_row_size(width);
|
||||
const size_t pixel_bytes = row_size * height;
|
||||
|
||||
uint8_t header[BMP_HEADER_SIZE];
|
||||
uint8_t *pos = header;
|
||||
*pos++ = 'B';
|
||||
*pos++ = 'M';
|
||||
put_le(pos, static_cast<uint32_t>(BMP_HEADER_SIZE + pixel_bytes), 4);
|
||||
put_le(pos, 0, 4); // reserved
|
||||
put_le(pos, BMP_HEADER_SIZE, 4);
|
||||
put_le(pos, BMP_INFO_HEADER_SIZE, 4);
|
||||
put_le(pos, static_cast<uint32_t>(width), 4);
|
||||
put_le(pos, static_cast<uint32_t>(height), 4);
|
||||
put_le(pos, 1, 2); // one plane
|
||||
put_le(pos, BMP_BITS_PER_PIXEL, 2);
|
||||
put_le(pos, 0, 4); // not compressed
|
||||
put_le(pos, static_cast<uint32_t>(pixel_bytes), 4);
|
||||
put_le(pos, 0, 4); // pixels per metre across, unspecified
|
||||
put_le(pos, 0, 4); // pixels per metre down, unspecified
|
||||
put_le(pos, 0, 4); // no palette
|
||||
put_le(pos, 0, 4); // so no palette entry matters more than another
|
||||
|
||||
if (fwrite(header, 1, sizeof(header), file) != sizeof(header))
|
||||
return false;
|
||||
for (int y = height - 1; y >= 0; y--) {
|
||||
if (fwrite(pixels + static_cast<size_t>(y) * row_stride, 1, row_size, file) != row_size)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Reserve a name in the snapshot directory and write the picture to it.
|
||||
/// With `exact` set the given name is the only one tried; otherwise a number is added on
|
||||
/// collision. Returns true if a file was written.
|
||||
bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t row_stride, const std::string &name,
|
||||
bool exact) {
|
||||
const std::string dir = snapshot_dir();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dir, ec);
|
||||
if (ec) {
|
||||
ESP_LOGE(TAG, "Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// O_EXCL guarantees we never write over a file that is already there.
|
||||
std::string path;
|
||||
int fd = -1;
|
||||
for (unsigned attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) {
|
||||
path = dir + "/" + (attempt == 0 ? name : add_suffix(name, attempt));
|
||||
fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0644);
|
||||
if (fd >= 0)
|
||||
break;
|
||||
if (errno != EEXIST) {
|
||||
ESP_LOGE(TAG, "Could not create %s: %s", path.c_str(), strerror(errno));
|
||||
return false;
|
||||
}
|
||||
if (exact) {
|
||||
// The caller asked for this exact name, so silently writing somewhere else would be worse
|
||||
// than failing - a test asserting on the path would pick up a stale file.
|
||||
ESP_LOGE(TAG, "Snapshot %s already exists, not overwriting", path.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (fd < 0) {
|
||||
ESP_LOGE(TAG, "Could not find an unused name for %s in %s", name.c_str(), dir.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
FILE *file = fdopen(fd, "wb");
|
||||
if (file == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not open %s: %s", path.c_str(), strerror(errno));
|
||||
::close(fd);
|
||||
::unlink(path.c_str());
|
||||
return false;
|
||||
}
|
||||
bool ok = write_bmp(file, pixels, width, height, row_stride);
|
||||
int saved_errno = ok ? 0 : errno;
|
||||
// Closing can fail in its own right - the last of the data is still on its way out.
|
||||
if (fclose(file) != 0) {
|
||||
if (ok)
|
||||
saved_errno = errno;
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) {
|
||||
ESP_LOGE(TAG, "Could not write %s: %s", path.c_str(), strerror(saved_errno));
|
||||
// Leave no truncated file behind - it would block a retry under the same name.
|
||||
::unlink(path.c_str());
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG, "Snapshot written to %s", path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// helper function since ESP_LOGW is disallowed in a header file
|
||||
void Snapshot::log_action_failed() { ESP_LOGW(TAG, "snapshot.take did not write a file"); }
|
||||
|
||||
bool Snapshot::take_snapshot(const char *filename) {
|
||||
const int width = this->snapshot_width();
|
||||
const int height = this->snapshot_height();
|
||||
if (width <= 0 || height <= 0) {
|
||||
ESP_LOGE(TAG, "Snapshot requested but the display is %dx%d", width, height);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
bool exact = false;
|
||||
if (filename != nullptr) {
|
||||
bool name_changed = false;
|
||||
name = sanitise_filename(filename, &name_changed);
|
||||
exact = !name.empty();
|
||||
if (name_changed) {
|
||||
ESP_LOGW(TAG, "Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
|
||||
name.empty() ? "a name made from the time" : name.c_str());
|
||||
}
|
||||
}
|
||||
if (name.empty()) {
|
||||
struct timespec now {};
|
||||
if (clock_gettime(CLOCK_REALTIME, &now) != 0)
|
||||
now = {};
|
||||
struct tm tm_buf {};
|
||||
if (localtime_r(&now.tv_sec, &tm_buf) == nullptr)
|
||||
tm_buf = {};
|
||||
char stamp[32]{};
|
||||
// ::strftime to be sure of the one from <ctime>; display has an unrelated member of that name
|
||||
if (::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &tm_buf) == 0)
|
||||
snprintf(stamp, sizeof(stamp), "unknown-time");
|
||||
char buffer[MAX_NAME_LENGTH];
|
||||
int written =
|
||||
snprintf(buffer, sizeof(buffer), "%s-%s-%03ld.bmp", this->snapshot_prefix_, stamp, now.tv_nsec / 1000000);
|
||||
if (written < 0 || static_cast<size_t>(written) >= sizeof(buffer)) {
|
||||
ESP_LOGW(TAG, "Could not build a timestamped snapshot name, using a fallback");
|
||||
snprintf(buffer, sizeof(buffer), "snapshot.bmp");
|
||||
}
|
||||
name = buffer;
|
||||
}
|
||||
|
||||
// Rows are padded out to a multiple of four bytes, as the file wants them, so each one can be
|
||||
// written straight from the buffer. Zeroed on allocation, which is what the padding must be.
|
||||
const size_t row_stride = bmp_row_size(width);
|
||||
auto pixels = std::make_unique<uint8_t[]>(row_stride * height);
|
||||
if (!this->capture_bgr(pixels.get(), row_stride))
|
||||
return false;
|
||||
return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact);
|
||||
}
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
#endif
|
||||
@@ -1,72 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_HOST
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Directory snapshots are written to. Normally set by codegen to a folder under .esphome; the
|
||||
// fallback keeps the component compiling for static analysis, where no defines.h is generated.
|
||||
#ifndef ESPHOME_SNAPSHOT_DIR
|
||||
#define ESPHOME_SNAPSHOT_DIR "."
|
||||
#endif
|
||||
|
||||
namespace esphome::snapshot {
|
||||
|
||||
/// Base for anything that can hand over the picture it is showing so it can be written to a file.
|
||||
///
|
||||
/// A subclass says how big the picture is and fills in the pixels. Everything else - picking a
|
||||
/// name, staying inside the snapshot directory, not writing over anything, and encoding the file -
|
||||
/// is done here, so every component that can take a snapshot behaves the same way.
|
||||
class Snapshot {
|
||||
public:
|
||||
virtual ~Snapshot() = default;
|
||||
|
||||
/// Set the word generated names start with. Codegen passes the component id, so with more than
|
||||
/// one display in a device it is clear which one a file came from.
|
||||
void set_snapshot_prefix(const char *prefix) { this->snapshot_prefix_ = prefix; }
|
||||
|
||||
/// Write the current picture to a BMP file in the snapshot directory.
|
||||
///
|
||||
/// Pass nullptr to have a name made up from the prefix and the current time. A file that is
|
||||
/// already there is never written over. Returns true if a file was written.
|
||||
bool take_snapshot(const char *filename);
|
||||
|
||||
/// Log that an action-triggered snapshot did not write a file.
|
||||
static void log_action_failed();
|
||||
|
||||
protected:
|
||||
/// Width of the picture in pixels.
|
||||
virtual int snapshot_width() = 0;
|
||||
/// Height of the picture in pixels.
|
||||
virtual int snapshot_height() = 0;
|
||||
/// Fill in the picture: three bytes per pixel in blue, green, red order, topmost row first, with
|
||||
/// `row_stride` bytes from the start of one row to the start of the next. Returns false, having
|
||||
/// logged why, if the picture could not be read.
|
||||
virtual bool capture_bgr(uint8_t *dest, size_t row_stride) = 0;
|
||||
|
||||
const char *snapshot_prefix_{"snapshot"};
|
||||
};
|
||||
|
||||
template<typename... Ts> class SnapshotAction final : public Action<Ts...>, public Parented<Snapshot> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::string, filename)
|
||||
|
||||
protected:
|
||||
void play(const Ts &...x) override {
|
||||
bool ok;
|
||||
if (this->filename_.has_value()) {
|
||||
ok = this->parent_->take_snapshot(this->filename_.value(x...).c_str());
|
||||
} else {
|
||||
ok = this->parent_->take_snapshot(nullptr);
|
||||
}
|
||||
if (!ok)
|
||||
this->parent_->log_action_failed();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::snapshot
|
||||
|
||||
#endif
|
||||
@@ -33,13 +33,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"t6615",
|
||||
baud_rate=19200,
|
||||
require_rx=True,
|
||||
require_tx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
"t6615", baud_rate=19200, require_rx=True, require_tx=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ void T6615Component::query_ppm_() {
|
||||
void T6615Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "T6615:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
this->check_uart_settings(19200);
|
||||
}
|
||||
|
||||
} // namespace esphome::t6615
|
||||
|
||||
@@ -35,22 +35,6 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
# Historical mode runs at 1200 baud, standard mode at 9600 baud.
|
||||
baud_rate = 1200 if config[CONF_HISTORICAL_MODE] else 9600
|
||||
uart.final_validate_device_schema(
|
||||
"teleinfo",
|
||||
baud_rate=baud_rate,
|
||||
data_bits=7,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
)(config)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -184,7 +184,10 @@ void TeleInfo::publish_value_(const std::string &tag, const std::string &val) {
|
||||
element->publish_val(val);
|
||||
}
|
||||
}
|
||||
void TeleInfo::dump_config() { ESP_LOGCONFIG(TAG, "TeleInfo:"); }
|
||||
void TeleInfo::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "TeleInfo:");
|
||||
this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
|
||||
}
|
||||
TeleInfo::TeleInfo(bool historical_mode) {
|
||||
if (historical_mode) {
|
||||
/*
|
||||
@@ -192,9 +195,11 @@ TeleInfo::TeleInfo(bool historical_mode) {
|
||||
*/
|
||||
checksum_area_end_ = 2;
|
||||
separator_ = 0x20;
|
||||
baud_rate_ = 1200;
|
||||
} else {
|
||||
checksum_area_end_ = 1;
|
||||
separator_ = 0x9;
|
||||
baud_rate_ = 9600;
|
||||
}
|
||||
}
|
||||
void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); }
|
||||
|
||||
@@ -31,6 +31,7 @@ class TeleInfo final : public PollingComponent, public uart::UARTDevice {
|
||||
std::vector<TeleInfoListener *> teleinfo_listeners_{};
|
||||
|
||||
protected:
|
||||
uint32_t baud_rate_;
|
||||
int checksum_area_end_;
|
||||
int separator_;
|
||||
char buf_[MAX_BUF_SIZE];
|
||||
|
||||
@@ -36,6 +36,8 @@ cover::CoverTraits Tormatic::get_traits() {
|
||||
|
||||
void Tormatic::dump_config() {
|
||||
LOG_COVER("", "Tormatic Cover", this);
|
||||
this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Open Duration: %.1fs\n"
|
||||
" Close Duration: %.1fs",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <vector>
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "uart_component.h"
|
||||
|
||||
@@ -67,7 +66,6 @@ class UARTDevice {
|
||||
}
|
||||
|
||||
/// Check that the configuration of the UART bus matches the provided values and otherwise print a warning
|
||||
ESPDEPRECATED("Use uart.final_validate_device_schema() in Python instead. Removed in 2027.3.0", "2026.9.0")
|
||||
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1,
|
||||
UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8);
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
baud_rate=2400,
|
||||
data_bits=8,
|
||||
parity="EVEN",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
@@ -213,6 +213,7 @@ void UFM01Component::dump_config() {
|
||||
LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_);
|
||||
LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_);
|
||||
#endif
|
||||
this->check_uart_settings(2400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||
}
|
||||
|
||||
void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE]) {
|
||||
|
||||
@@ -50,7 +50,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
parity=None,
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ void UponorSmatrixComponent::dump_config() {
|
||||
}
|
||||
#endif
|
||||
|
||||
this->check_uart_settings(19200);
|
||||
|
||||
if (!this->unknown_devices_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
|
||||
for (auto device_address : this->unknown_devices_) {
|
||||
|
||||
@@ -117,20 +117,6 @@ struct UsbEvent {
|
||||
|
||||
// callback function type.
|
||||
|
||||
// USB string descriptors hold at most 126 characters; one more for the terminator
|
||||
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
|
||||
|
||||
/// Identity of a connected USB device, copied out of the descriptors the USB host
|
||||
/// stack caches for the lifetime of the connection
|
||||
struct UsbDeviceInfo {
|
||||
uint16_t vendor_id;
|
||||
uint16_t product_id;
|
||||
uint16_t bcd_device;
|
||||
char manufacturer[DESC_STRING_BUF_SIZE];
|
||||
char product[DESC_STRING_BUF_SIZE];
|
||||
char serial_number[DESC_STRING_BUF_SIZE];
|
||||
};
|
||||
|
||||
enum ClientState {
|
||||
USB_CLIENT_INIT = 0,
|
||||
USB_CLIENT_OPEN,
|
||||
@@ -158,10 +144,6 @@ class USBClient : public Component {
|
||||
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
|
||||
const std::vector<uint8_t> &data = {});
|
||||
|
||||
/// Copy the connected device's identity out of the cached USB descriptors.
|
||||
/// Returns false when no device is connected.
|
||||
bool get_device_info(UsbDeviceInfo &info) const;
|
||||
|
||||
// Lock-free event queue and pool for USB task to main loop communication
|
||||
// Must be public for access from static callbacks
|
||||
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
|
||||
|
||||
@@ -143,8 +143,10 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
|
||||
} while (next_desc != NULL);
|
||||
}
|
||||
#endif
|
||||
// bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType),
|
||||
// so character count = (bLength - 2) / 2.
|
||||
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
|
||||
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
|
||||
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
|
||||
|
||||
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
|
||||
if (desc == nullptr || desc->bLength < 2)
|
||||
return "(unspecified)";
|
||||
@@ -160,41 +162,6 @@ static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<c
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
// A missing descriptor copies as an empty string, unlike the "(unspecified)"
|
||||
// placeholder the logging helper above uses
|
||||
static void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
|
||||
buffer[0] = '\0';
|
||||
if (desc == nullptr || desc->bLength < 2)
|
||||
return;
|
||||
int char_count = (desc->bLength - 2) / 2;
|
||||
char *p = buffer.data();
|
||||
char *end = p + buffer.size() - 1;
|
||||
for (int i = 0; i != char_count && p < end; i++) {
|
||||
auto c = desc->wData[i];
|
||||
if (c < 0x100)
|
||||
*p++ = static_cast<char>(c);
|
||||
}
|
||||
*p = '\0';
|
||||
}
|
||||
|
||||
bool USBClient::get_device_info(UsbDeviceInfo &info) const {
|
||||
if (this->state_ != USB_CLIENT_CONNECTED)
|
||||
return false;
|
||||
const usb_device_desc_t *desc;
|
||||
if (usb_host_get_device_descriptor(this->device_handle_, &desc) != ESP_OK)
|
||||
return false;
|
||||
info.vendor_id = desc->idVendor;
|
||||
info.product_id = desc->idProduct;
|
||||
info.bcd_device = desc->bcdDevice;
|
||||
usb_device_info_t dev_info;
|
||||
if (usb_host_device_info(this->device_handle_, &dev_info) != ESP_OK)
|
||||
return false;
|
||||
copy_descriptor_string(dev_info.str_desc_manufacturer, info.manufacturer);
|
||||
copy_descriptor_string(dev_info.str_desc_product, info.product);
|
||||
copy_descriptor_string(dev_info.str_desc_serial_num, info.serial_number);
|
||||
return true;
|
||||
}
|
||||
|
||||
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
|
||||
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
|
||||
auto *client = static_cast<USBClient *>(ptr);
|
||||
|
||||
@@ -16,7 +16,7 @@ from esphome.const import (
|
||||
CONF_DUMMY_RECEIVER,
|
||||
CONF_ID,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_types import Component
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -27,16 +27,6 @@ usb_uart_ns = cg.esphome_ns.namespace("usb_uart")
|
||||
USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component)
|
||||
USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent)
|
||||
|
||||
|
||||
def is_usb_uart_channel(uart_id: ID, full_config: ConfigType) -> bool:
|
||||
"""Return True if the given ID refers to a channel of a configured usb_uart device."""
|
||||
return any(
|
||||
channel[CONF_ID] == uart_id
|
||||
for device in full_config.get("usb_uart") or []
|
||||
for channel in device[CONF_CHANNELS]
|
||||
)
|
||||
|
||||
|
||||
UARTParityOptions = usb_uart_ns.enum("UARTParityOptions")
|
||||
UART_PARITY_OPTIONS = {
|
||||
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
|
||||
|
||||
@@ -164,9 +164,6 @@ class USBUartChannelBase : public uart::UARTComponent, public Parented<USBUartCo
|
||||
/// they arrive, eliminating one full main-loop-wakeup cycle of latency.
|
||||
void set_rx_callback(std::function<void()> cb) { this->rx_callback_ = std::move(cb); }
|
||||
|
||||
/// Channel index on the bridge (interface number on multi-port bridges)
|
||||
uint8_t get_index() const { return this->index_; }
|
||||
|
||||
protected:
|
||||
// Not directly instantiable; construct a concrete channel type instead.
|
||||
USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {}
|
||||
|
||||
@@ -29,14 +29,6 @@ CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
|
||||
}
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"vbus",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
@@ -11,7 +11,10 @@ static const char *const TAG = "vbus";
|
||||
// Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical)
|
||||
static constexpr size_t VBUS_MAX_LOG_BYTES = 64;
|
||||
|
||||
void VBus::dump_config() { ESP_LOGCONFIG(TAG, "VBus:"); }
|
||||
void VBus::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "VBus:");
|
||||
check_uart_settings(9600);
|
||||
}
|
||||
|
||||
static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) {
|
||||
for (int i = 0; i < count; i++, septet >>= 1) {
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ESP8266WiFiType.h>
|
||||
|
||||
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_RP2
|
||||
|
||||
@@ -21,6 +21,7 @@ extern "C" {
|
||||
#include "lwip/apps/sntp.h"
|
||||
#include "lwip/netif.h" // struct netif
|
||||
#include <AddrList.h>
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
|
||||
#include "LwipDhcpServer.h"
|
||||
#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
|
||||
#include <ESP8266WiFi.h>
|
||||
@@ -29,6 +30,7 @@ extern "C" {
|
||||
#define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time)
|
||||
#define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
@@ -291,6 +293,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
conf.bssid_set = 0;
|
||||
}
|
||||
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
|
||||
if (ap.password_.empty()) {
|
||||
conf.threshold.authmode = AUTH_OPEN;
|
||||
} else {
|
||||
@@ -307,6 +310,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
}
|
||||
}
|
||||
conf.threshold.rssi = -127;
|
||||
#endif
|
||||
|
||||
ETS_UART_INTR_DISABLE();
|
||||
bool ret = wifi_station_set_config_current(&conf);
|
||||
@@ -598,6 +602,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
|
||||
case EVENT_OPMODE_CHANGED: {
|
||||
auto it = event->event_info.opmode_changed;
|
||||
ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)),
|
||||
@@ -615,6 +620,7 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -699,6 +705,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
config.bssid = nullptr;
|
||||
config.channel = 0;
|
||||
config.show_hidden = 1;
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
|
||||
config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
|
||||
// Use shorter dwell times for roaming scans - we only need to detect strong
|
||||
// nearby APs, not do a thorough survey. This also reduces off-channel time
|
||||
@@ -717,6 +724,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS;
|
||||
config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS;
|
||||
}
|
||||
#endif
|
||||
bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
|
||||
if (!ret) {
|
||||
ESP_LOGV(TAG, "wifi_station_scan failed");
|
||||
@@ -822,7 +830,7 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
|
||||
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
|
||||
dhcpSoftAP.begin(&info);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -21,14 +21,6 @@ CONFIG_SCHEMA = (
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"wl_134",
|
||||
baud_rate=9600,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
|
||||
@@ -110,5 +110,7 @@ uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t t
|
||||
void Wl134Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "WL-134 Sensor:");
|
||||
LOG_TEXT_SENSOR("", "Tag", this);
|
||||
// As specified in the sensor's data sheet
|
||||
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
|
||||
}
|
||||
} // namespace esphome::wl_134
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import serial_proxy
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["api", "serial_proxy"]
|
||||
|
||||
CONF_INITIAL_TIMEOUT = "initial_timeout"
|
||||
CONF_MIN_TIMEOUT = "min_timeout"
|
||||
CONF_MAX_TIMEOUT = "max_timeout"
|
||||
CONF_SERIAL_PROXY_ID = "serial_proxy_id"
|
||||
|
||||
# Default ACK timeout values for the boot-time metadata harvest
|
||||
_DEFAULT_INITIAL_TIMEOUT = 1600
|
||||
_DEFAULT_MIN_TIMEOUT = 400
|
||||
_DEFAULT_MAX_TIMEOUT = 3200
|
||||
|
||||
zigbee_proxy_ns = cg.esphome_ns.namespace("zigbee_proxy")
|
||||
ZigbeeProxy = zigbee_proxy_ns.class_(
|
||||
"ZigbeeProxy", cg.Component, serial_proxy.SerialProxyTap
|
||||
)
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
full_config = fv.full_config.get()
|
||||
if (wifi_conf := full_config.get(CONF_WIFI)) and (
|
||||
wifi_conf.get(CONF_POWER_SAVE_MODE, "").lower() != "none"
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Zigbee proxy"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ZigbeeProxy),
|
||||
cv.Required(CONF_SERIAL_PROXY_ID): cv.use_id(serial_proxy.SerialProxy),
|
||||
cv.Optional(CONF_BUFFER_SIZE): cv.SplitDefault(
|
||||
cv.int_range(min=256, max=2048),
|
||||
esp8266=512,
|
||||
default=1024,
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_INITIAL_TIMEOUT, default=_DEFAULT_INITIAL_TIMEOUT
|
||||
): cv.int_range(min=10, max=10000),
|
||||
cv.Optional(CONF_MIN_TIMEOUT, default=_DEFAULT_MIN_TIMEOUT): cv.int_range(
|
||||
min=10, max=5000
|
||||
),
|
||||
cv.Optional(CONF_MAX_TIMEOUT, default=_DEFAULT_MAX_TIMEOUT): cv.int_range(
|
||||
min=50, max=10000
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
sp = await cg.get_variable(config[CONF_SERIAL_PROXY_ID])
|
||||
cg.add(var.set_serial_proxy(sp))
|
||||
|
||||
cg.add_define("USE_ZIGBEE_PROXY")
|
||||
# Compiles the tap interface into serial_proxy; without it the port is a plain byte pipe
|
||||
cg.add_define("USE_SERIAL_PROXY_TAP")
|
||||
|
||||
# Set buffer size via define for compile-time allocation
|
||||
if CONF_BUFFER_SIZE in config:
|
||||
cg.add_define("ZIGBEE_PROXY_BUFFER_SIZE", config[CONF_BUFFER_SIZE])
|
||||
|
||||
cg.add(var.set_initial_timeout(config[CONF_INITIAL_TIMEOUT]))
|
||||
cg.add(var.set_min_timeout(config[CONF_MIN_TIMEOUT]))
|
||||
cg.add(var.set_max_timeout(config[CONF_MAX_TIMEOUT]))
|
||||
@@ -1,256 +0,0 @@
|
||||
#include "ash_detector.h"
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
|
||||
namespace esphome::zigbee_proxy {
|
||||
|
||||
// Control byte of an RSTACK, and the only ASH version byte that can follow it
|
||||
static constexpr uint8_t ASH_RSTACK_CONTROL = 0xC1;
|
||||
static constexpr uint8_t ASH_PROTOCOL_VERSION = 0x02;
|
||||
static constexpr size_t ASH_RSTACK_BODY_SIZE = 3; // control, version, reset code
|
||||
static constexpr size_t ASH_CRC_SIZE = 2;
|
||||
// Smallest legal frame on the wire: a bare control byte plus its CRC
|
||||
static constexpr size_t ASH_MIN_FRAME_SIZE = 1 + ASH_CRC_SIZE;
|
||||
|
||||
// The opening EZSP version command is a constant: control 0x00 (frmNum 0, ackNum 0)
|
||||
// followed by [seq=0][frameControl=0][frameId=0] randomized by 0x42 0x21 0xA8. Only the
|
||||
// requested version varies, as version ^ 0x54, so it can be recovered for free.
|
||||
static constexpr uint8_t EZSP_VERSION_CMD_PREFIX[] = {0x00, 0x42, 0x21, 0xA8};
|
||||
static constexpr size_t EZSP_VERSION_CMD_SIZE = 5;
|
||||
static constexpr uint8_t EZSP_VERSION_RANDOM_MASK = 0x54;
|
||||
|
||||
// Consecutive frames we could not accept, with neither a good frame nor a retransmission
|
||||
// in between, before concluding the peer is no longer speaking ASH. A real ASH peer must
|
||||
// retransmit an unacknowledged frame, so the absence of one is the positive evidence
|
||||
// here -- garbage on the line is not, since noise proves nothing either way.
|
||||
static constexpr uint8_t MAX_UNCONFIRMED_REJECTS = 4;
|
||||
|
||||
bool ash_reset_code_is_known(uint8_t code) {
|
||||
switch (code) {
|
||||
case 0x00: // RESET_UNKNOWN
|
||||
case 0x01: // RESET_EXTERNAL
|
||||
case 0x02: // RESET_POWER_ON
|
||||
case 0x03: // RESET_WATCHDOG
|
||||
case 0x06: // RESET_ASSERT
|
||||
case 0x09: // RESET_BOOTLOADER
|
||||
case 0x0B: // RESET_SOFTWARE
|
||||
case 0x51: // ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT
|
||||
case 0x80: // ERROR_CHIP_SPECIFIC
|
||||
case 0x81: // RESET_CHIP_SPECIFIC
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void AshFrameScanner::begin_frame_() {
|
||||
this->index_ = 0;
|
||||
this->crc_ = ASH_CRC_INIT;
|
||||
this->escaped_ = false;
|
||||
this->poisoned_ = false;
|
||||
}
|
||||
|
||||
void AshFrameScanner::reset() {
|
||||
this->begin_frame_();
|
||||
this->frame_length_ = 0;
|
||||
this->discarding_ = false;
|
||||
}
|
||||
|
||||
ScanResult AshFrameScanner::feed(uint8_t byte) {
|
||||
if (byte == ASH_FLAG_BYTE) {
|
||||
// Snapshot everything the verdict depends on: begin_frame_() clears all of it.
|
||||
const bool discarding = this->discarding_;
|
||||
const bool poisoned = this->poisoned_;
|
||||
const bool escaped = this->escaped_;
|
||||
const size_t index = this->index_;
|
||||
const uint16_t crc = this->crc_;
|
||||
// A FLAG always starts the next frame afresh, whatever preceded it
|
||||
this->begin_frame_();
|
||||
this->discarding_ = false;
|
||||
|
||||
if (discarding || index == 0) {
|
||||
// Consecutive delimiters carry no frame at all, so there is nothing to judge
|
||||
this->frame_length_ = 0;
|
||||
return ScanResult::NONE;
|
||||
}
|
||||
// Running the CRC over the body *and* its trailing CRC bytes leaves zero when
|
||||
// correct, so validity needs no second pass over the frame.
|
||||
if (poisoned || escaped || index < ASH_MIN_FRAME_SIZE || crc != 0) {
|
||||
this->frame_length_ = 0;
|
||||
return ScanResult::INVALID;
|
||||
}
|
||||
this->frame_length_ = index - ASH_CRC_SIZE;
|
||||
return ScanResult::FRAME;
|
||||
}
|
||||
|
||||
if (this->discarding_) {
|
||||
return ScanResult::NONE;
|
||||
}
|
||||
|
||||
switch (byte) {
|
||||
case ASH_CANCEL_BYTE:
|
||||
// Everything received since the last FLAG is to be ignored
|
||||
this->begin_frame_();
|
||||
return ScanResult::NONE;
|
||||
|
||||
case ASH_SUBSTITUTE_BYTE:
|
||||
// A low-level error was flagged; ignore everything up to the next FLAG
|
||||
this->discarding_ = true;
|
||||
return ScanResult::NONE;
|
||||
|
||||
case ASH_XON_BYTE:
|
||||
case ASH_XOFF_BYTE:
|
||||
// Transport flow control, not frame content: skip it without disturbing the frame
|
||||
return ScanResult::NONE;
|
||||
|
||||
case ASH_ESCAPE_BYTE:
|
||||
this->escaped_ = true;
|
||||
return ScanResult::NONE;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t value = byte;
|
||||
if (this->escaped_) {
|
||||
this->escaped_ = false;
|
||||
value = byte ^ ASH_XOR_BYTE;
|
||||
// An escape must decode to a reserved byte; anything else is not ASH framing at all
|
||||
if (!ash_is_reserved(value)) {
|
||||
this->poisoned_ = true;
|
||||
return ScanResult::NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if (this->index_ >= sizeof(this->buffer_)) {
|
||||
this->poisoned_ = true;
|
||||
return ScanResult::NONE;
|
||||
}
|
||||
|
||||
this->buffer_[this->index_++] = value;
|
||||
this->crc_ = ash_crc16(&value, 1, this->crc_);
|
||||
return ScanResult::NONE;
|
||||
}
|
||||
|
||||
void AshDetector::reset() {
|
||||
this->ncp_scanner_.reset();
|
||||
this->host_scanner_.reset();
|
||||
this->state_ = AshDetectState::IDLE;
|
||||
this->rx_sequence_ = 0;
|
||||
this->ack_owed_ = false;
|
||||
this->data_frame_ready_ = false;
|
||||
this->unconfirmed_rejects_ = 0;
|
||||
this->negotiated_version_ = 0;
|
||||
}
|
||||
|
||||
void AshDetector::from_ncp(uint8_t byte) {
|
||||
this->data_frame_ready_ = false;
|
||||
switch (this->ncp_scanner_.feed(byte)) {
|
||||
case ScanResult::FRAME:
|
||||
this->handle_ncp_frame_();
|
||||
break;
|
||||
case ScanResult::INVALID:
|
||||
// A delimited chunk that is not a frame. While armed this may be a corrupted ASH
|
||||
// frame, which the peer will retransmit, or a sign the peer stopped speaking ASH.
|
||||
// reject_() distinguishes the two by whether a retransmission ever arrives.
|
||||
this->reject_();
|
||||
break;
|
||||
case ScanResult::NONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void AshDetector::handle_ncp_frame_() {
|
||||
const uint8_t *body = this->ncp_scanner_.frame();
|
||||
const size_t length = this->ncp_scanner_.length();
|
||||
const uint8_t control = body[0];
|
||||
|
||||
// RSTACK is the only way into the handshake, and the only way back after a firmware
|
||||
// swap: a Spinel or bootloader NCP never emits one, so those stay unarmed forever.
|
||||
if (control == ASH_RSTACK_CONTROL) {
|
||||
if (length == ASH_RSTACK_BODY_SIZE && body[1] == ASH_PROTOCOL_VERSION && ash_reset_code_is_known(body[2])) {
|
||||
this->state_ = AshDetectState::SAW_RSTACK;
|
||||
this->rx_sequence_ = 0;
|
||||
this->ack_owed_ = false;
|
||||
this->unconfirmed_rejects_ = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->state_ != AshDetectState::ARMED) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((control & 0x80) != 0) {
|
||||
return; // ACK/NAK/RST/ERROR: nothing is owed for these
|
||||
}
|
||||
|
||||
const uint8_t frame_num = (control >> 4) & ASH_MAX_SEQUENCE;
|
||||
const bool re_tx = (control & 0x08) != 0;
|
||||
|
||||
if (frame_num != this->rx_sequence_) {
|
||||
// A retransmission still proves the peer is speaking ASH even though we cannot use
|
||||
// this copy, so it clears the suspicion without being acknowledged.
|
||||
if (re_tx) {
|
||||
this->unconfirmed_rejects_ = 0;
|
||||
} else {
|
||||
this->reject_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->rx_sequence_ = (this->rx_sequence_ + 1) & ASH_MAX_SEQUENCE;
|
||||
this->pending_ack_ = this->rx_sequence_;
|
||||
this->ack_owed_ = true;
|
||||
this->data_frame_ready_ = true;
|
||||
this->unconfirmed_rejects_ = 0;
|
||||
}
|
||||
|
||||
void AshDetector::reject_() {
|
||||
if (this->state_ != AshDetectState::ARMED) {
|
||||
return;
|
||||
}
|
||||
if (++this->unconfirmed_rejects_ >= MAX_UNCONFIRMED_REJECTS) {
|
||||
this->state_ = AshDetectState::IDLE;
|
||||
this->unconfirmed_rejects_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void AshDetector::from_host(uint8_t byte) {
|
||||
if (this->host_scanner_.feed(byte) != ScanResult::FRAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->state_ != AshDetectState::SAW_RSTACK) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *body = this->host_scanner_.frame();
|
||||
if (this->host_scanner_.length() != EZSP_VERSION_CMD_SIZE) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof(EZSP_VERSION_CMD_PREFIX); i++) {
|
||||
if (body[i] != EZSP_VERSION_CMD_PREFIX[i]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this->negotiated_version_ = body[4] ^ EZSP_VERSION_RANDOM_MASK;
|
||||
this->state_ = AshDetectState::ARMED;
|
||||
this->rx_sequence_ = 0;
|
||||
this->ack_owed_ = false;
|
||||
this->unconfirmed_rejects_ = 0;
|
||||
}
|
||||
|
||||
bool AshDetector::take_pending_ack(uint8_t &ack_num) {
|
||||
if (!this->ack_owed_) {
|
||||
return false;
|
||||
}
|
||||
this->ack_owed_ = false;
|
||||
ack_num = this->pending_ack_;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::zigbee_proxy
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY
|
||||
@@ -1,118 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
|
||||
#include "ash_protocol.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::zigbee_proxy {
|
||||
|
||||
// Decides when it is safe to acknowledge NCP frames on a client's behalf.
|
||||
//
|
||||
// The client suppresses its own ACKs, so nobody else will send them, and injecting ASH
|
||||
// bytes into a stream that is not ASH would corrupt it. Detection is therefore one-sided:
|
||||
// arm only on the session handshake, which is a fixed byte string, and never on frame
|
||||
// validity, which non-ASH traffic can satisfy by luck.
|
||||
//
|
||||
// RSTACK (NCP -> host) c1 02 <reset_code> <crc> 7e
|
||||
// version (host -> NCP) 00 42 21 a8 <version^0x54> <crc> 7e
|
||||
//
|
||||
// Requiring both, in that order, in opposite directions cannot be satisfied by a
|
||||
// unidirectional byte stream whatever it contains -- which is exactly the situation
|
||||
// during a firmware upload. Verified against real .gbl images and real Spinel traffic:
|
||||
// zero false arms, and neither pattern occurs even as a substring.
|
||||
//
|
||||
// Getting it wrong in the other direction is cheap: a frame we decline to acknowledge is
|
||||
// retransmitted by the NCP, so we see a clean copy and lose only the ack timeout. That
|
||||
// asymmetry is why this errs towards silence everywhere.
|
||||
|
||||
enum class AshDetectState : uint8_t {
|
||||
IDLE, // Not ASH, or not yet proven to be
|
||||
SAW_RSTACK, // Handshake half-complete; watching for the version command
|
||||
ARMED, // Session confirmed; acknowledging on the client's behalf
|
||||
};
|
||||
|
||||
enum class ScanResult : uint8_t {
|
||||
NONE, // Mid-frame, or a delimiter that carried nothing
|
||||
FRAME, // frame()/length() hold a complete body with a verified CRC
|
||||
INVALID, // A delimited chunk arrived but was not a well-formed ASH frame
|
||||
};
|
||||
|
||||
// Reassembles one direction of the byte stream into unstuffed, CRC-checked frames.
|
||||
// Mirrors bellows' AshProtocol.data_received: FLAG ends a frame, CANCEL discards what
|
||||
// precedes it, SUBSTITUTE poisons everything up to the next FLAG, and XON/XOFF are
|
||||
// transport flow control removed without disturbing the frame around them.
|
||||
class AshFrameScanner {
|
||||
public:
|
||||
ScanResult feed(uint8_t byte);
|
||||
void reset();
|
||||
|
||||
// Valid only until the next feed() call, which begins overwriting the buffer.
|
||||
const uint8_t *frame() const { return this->buffer_; }
|
||||
size_t length() const { return this->frame_length_; }
|
||||
|
||||
private:
|
||||
void begin_frame_();
|
||||
|
||||
// Frames are bounded by the ASH maximum, so a stream carrying no delimiters cannot
|
||||
// grow the buffer without limit; it just keeps failing.
|
||||
uint8_t buffer_[MAX_ASH_FRAME_SIZE];
|
||||
size_t index_{0}; // accumulation position for the frame being read
|
||||
size_t frame_length_{0}; // body length of the last completed frame
|
||||
uint16_t crc_{ASH_CRC_INIT};
|
||||
bool escaped_{false};
|
||||
bool discarding_{false};
|
||||
bool poisoned_{false};
|
||||
};
|
||||
|
||||
class AshDetector {
|
||||
public:
|
||||
void reset();
|
||||
|
||||
// Feed observed traffic. Neither call gates forwarding: the detector only watches.
|
||||
void from_ncp(uint8_t byte);
|
||||
void from_host(uint8_t byte);
|
||||
|
||||
bool armed() const { return this->state_ == AshDetectState::ARMED; }
|
||||
|
||||
// True only while the host direction can affect the state machine, i.e. while waiting
|
||||
// for the version command. Lets the caller skip scanning that direction entirely the
|
||||
// rest of the time -- it is the one carrying firmware uploads.
|
||||
bool needs_host_scan() const { return this->state_ == AshDetectState::SAW_RSTACK; }
|
||||
|
||||
// An acknowledgement became owed after the last from_ncp() call. Clears the flag.
|
||||
bool take_pending_ack(uint8_t &ack_num);
|
||||
|
||||
// The EZSP frame carried by the DATA frame just accepted, for metadata sniffing. The
|
||||
// ASH control byte is skipped, so offset 0 is the EZSP sequence number. Still
|
||||
// randomized, and valid only until the next from_ncp() call.
|
||||
const uint8_t *last_ezsp_frame() const { return this->ncp_scanner_.frame() + 1; }
|
||||
size_t last_ezsp_frame_length() const {
|
||||
const size_t length = this->ncp_scanner_.length();
|
||||
return length > 0 ? length - 1 : 0;
|
||||
}
|
||||
|
||||
AshDetectState state() const { return this->state_; }
|
||||
uint8_t negotiated_version() const { return this->negotiated_version_; }
|
||||
|
||||
protected:
|
||||
void handle_ncp_frame_();
|
||||
void reject_();
|
||||
|
||||
AshFrameScanner ncp_scanner_;
|
||||
AshFrameScanner host_scanner_;
|
||||
AshDetectState state_{AshDetectState::IDLE};
|
||||
uint8_t rx_sequence_{0};
|
||||
uint8_t pending_ack_{0};
|
||||
bool ack_owed_{false};
|
||||
bool data_frame_ready_{false};
|
||||
uint8_t unconfirmed_rejects_{0};
|
||||
uint8_t negotiated_version_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::zigbee_proxy
|
||||
|
||||
#endif // USE_ZIGBEE_PROXY
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user