mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 07:17:33 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3966dad8fb | ||
|
|
a9bf807f4a | ||
|
|
3e3822e554 | ||
|
|
7564f5ff1a | ||
|
|
a807a8f945 | ||
|
|
280fac11e6 | ||
|
|
9924148302 |
+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.5
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -77,6 +77,7 @@ service APIConnection {
|
||||
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -2696,6 +2697,21 @@ message InfraredRFReceiveEvent {
|
||||
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
|
||||
}
|
||||
|
||||
// Sent only to the client that issued an InfraredRFTransmitRawTimingsRequest, once the
|
||||
// transmitter reports that the transmission (all repeats) has finished, or immediately with
|
||||
// success=false if it could not be started (unknown key, no transmitter, no or invalid timings).
|
||||
// Lets clients pace requests instead of estimating durations (since API 1.18)
|
||||
message InfraredRFTransmitCompleteResponse {
|
||||
option (id) = 153;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
|
||||
option (no_delay) = true;
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2 [(force) = true]; // Key of the transmitter entity from the request
|
||||
bool success = 3; // false if the transmit never started
|
||||
}
|
||||
|
||||
// ==================== RADIO FREQUENCY ====================
|
||||
|
||||
// Lists available radio frequency entity instances
|
||||
@@ -2726,7 +2742,8 @@ enum SerialProxyParity {
|
||||
SERIAL_PROXY_PARITY_ODD = 2;
|
||||
}
|
||||
|
||||
// Configure UART parameters for a serial proxy instance
|
||||
// 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).
|
||||
message SerialProxyConfigureRequest {
|
||||
option (id) = 138;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2752,7 +2769,8 @@ message SerialProxyDataReceived {
|
||||
bytes data = 2; // Raw data received from the serial device
|
||||
}
|
||||
|
||||
// Write data to a serial device
|
||||
// Write data to a serial device. Only the subscribed client may write; writes from
|
||||
// others are ignored (since API 1.17).
|
||||
message SerialProxyWriteRequest {
|
||||
option (id) = 140;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2763,7 +2781,8 @@ message SerialProxyWriteRequest {
|
||||
bytes data = 2; // Raw data to write to the serial device
|
||||
}
|
||||
|
||||
// Set modem control pin states (RTS and DTR)
|
||||
// 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).
|
||||
message SerialProxySetModemPinsRequest {
|
||||
option (id) = 141;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2802,6 +2821,7 @@ 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 {
|
||||
@@ -2814,7 +2834,8 @@ enum SerialProxyStatus {
|
||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
|
||||
}
|
||||
|
||||
// Generic request message for simple serial proxy operations
|
||||
// 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).
|
||||
message SerialProxyRequest {
|
||||
option (id) = 144;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2838,6 +2859,29 @@ 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;
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
message BluetoothSetConnectionParamsRequest {
|
||||
option (id) = 145;
|
||||
|
||||
@@ -1516,6 +1516,18 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
#ifdef USE_DEVICES
|
||||
const uint32_t device_id = msg.device_id;
|
||||
#else
|
||||
const uint32_t device_id = 0;
|
||||
#endif
|
||||
// Register before perform(): blocking transmitters report completion from inside it, and the
|
||||
// non-blocking RMT path flushes the previous frame's completion there
|
||||
const bool want_reply = this->client_supports_api_version(1, 18);
|
||||
if (want_reply) {
|
||||
this->parent_->register_pending_ir_rf_transmit(device_id, msg.key, this);
|
||||
}
|
||||
bool started = false;
|
||||
// Dispatch by key: infrared entities are checked first, then radio frequency entities.
|
||||
// The key is unique across all entity instances on a device, so at most one lookup will succeed.
|
||||
#ifdef USE_INFRARED
|
||||
@@ -1525,8 +1537,7 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
|
||||
call.set_carrier_frequency(msg.carrier_frequency);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.perform();
|
||||
return;
|
||||
started = call.perform();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
@@ -1537,9 +1548,12 @@ void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRF
|
||||
call.set_modulation(static_cast<radio_frequency::RadioFrequencyModulation>(msg.modulation));
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.perform();
|
||||
started = call.perform();
|
||||
}
|
||||
#endif
|
||||
if (want_reply && !started) {
|
||||
this->parent_->fail_pending_ir_rf_transmit(device_id, msg.key, this);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1551,6 +1565,13 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
|
||||
ESP_LOGV(TAG, "IR/RF event dropped, TCP buffer full");
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::send_infrared_rf_transmit_complete_response(const InfraredRFTransmitCompleteResponse &msg) {
|
||||
if (!this->send_message(msg)) {
|
||||
// a lost reply stalls the client's pacing until the server side expiry
|
||||
API_LOG_MSG_DROPPED(TAG, "IR/RF transmit complete");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
@@ -1661,6 +1682,7 @@ 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;
|
||||
@@ -1673,6 +1695,19 @@ 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");
|
||||
@@ -1799,7 +1834,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 16;
|
||||
resp.api_version_minor = 18;
|
||||
// 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());
|
||||
|
||||
@@ -236,6 +236,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
void send_infrared_rf_transmit_complete_response(const InfraredRFTransmitCompleteResponse &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
@@ -244,6 +245,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
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_request(const SerialProxyRequest &msg);
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
|
||||
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -4082,6 +4082,24 @@ InfraredRFReceiveEvent::calculate_size() const {
|
||||
}
|
||||
return size;
|
||||
}
|
||||
uint8_t *InfraredRFTransmitCompleteResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
#ifdef USE_DEVICES
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id);
|
||||
#endif
|
||||
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key);
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->success);
|
||||
return pos;
|
||||
}
|
||||
uint32_t InfraredRFTransmitCompleteResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
#ifdef USE_DEVICES
|
||||
size += ProtoSize::calc_uint32(1, this->device_id);
|
||||
#endif
|
||||
size += 5;
|
||||
size += ProtoSize::calc_bool(1, this->success);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
uint8_t *ListEntitiesRadioFrequencyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -4253,6 +4271,19 @@ 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;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
|
||||
@@ -356,6 +356,7 @@ 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,
|
||||
@@ -366,6 +367,10 @@ 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
|
||||
|
||||
} // namespace enums
|
||||
@@ -3236,6 +3241,26 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class InfraredRFTransmitCompleteResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 153;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 11;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_complete_response"); }
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
uint32_t device_id{0};
|
||||
#endif
|
||||
uint32_t key{0};
|
||||
bool success{false};
|
||||
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_RADIO_FREQUENCY
|
||||
class ListEntitiesRadioFrequencyResponse final : public InfoResponseProtoMessage {
|
||||
@@ -3403,6 +3428,22 @@ 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;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
|
||||
|
||||
@@ -854,6 +854,8 @@ 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");
|
||||
}
|
||||
@@ -878,6 +880,16 @@ 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
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -2728,6 +2740,15 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
|
||||
}
|
||||
return out.c_str();
|
||||
}
|
||||
const char *InfraredRFTransmitCompleteResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitCompleteResponse"));
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("key"), this->key);
|
||||
dump_field(out, ESPHOME_PSTR("success"), this->success);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
const char *ListEntitiesRadioFrequencyResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -2805,6 +2826,12 @@ 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();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -712,6 +712,17 @@ 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
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -235,6 +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_BLUETOOTH_PROXY_CONNECTIONS
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -193,6 +193,9 @@ void APIServer::remove_client_(uint8_t client_index) {
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
this->unregister_pending_ir_rf_transmits_for_connection_(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
@@ -417,6 +420,93 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_infrared_rf_receive_event(resp);
|
||||
}
|
||||
|
||||
// Safety net for transmitters that never report completion (for example a failed component)
|
||||
static constexpr uint32_t IR_RF_TRANSMIT_TIMEOUT_MS = 30000;
|
||||
static constexpr char IR_RF_TRANSMIT_TIMEOUT[] = "ir_rf_tx";
|
||||
|
||||
void APIServer::register_pending_ir_rf_transmit(uint32_t device_id, uint32_t key, APIConnection *conn) {
|
||||
if (this->pending_ir_rf_count_ == this->pending_ir_rf_transmits_.size()) {
|
||||
// only an unpaced client gets here; its oldest request is answered as not started
|
||||
this->complete_pending_ir_rf_transmit_(0, false);
|
||||
}
|
||||
this->pending_ir_rf_transmits_[this->pending_ir_rf_count_++] = {device_id, key, App.get_loop_component_start_time(),
|
||||
conn};
|
||||
if (this->pending_ir_rf_count_ == 1) {
|
||||
this->set_timeout(IR_RF_TRANSMIT_TIMEOUT, IR_RF_TRANSMIT_TIMEOUT_MS,
|
||||
[this]() { this->expire_pending_ir_rf_transmits_(); });
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::fail_pending_ir_rf_transmit(uint32_t device_id, uint32_t key, APIConnection *conn) {
|
||||
auto &pending = this->pending_ir_rf_transmits_;
|
||||
for (size_t i = this->pending_ir_rf_count_; i-- > 0;) {
|
||||
if (pending[i].connection == conn && pending[i].key == key && pending[i].device_id == device_id) {
|
||||
this->complete_pending_ir_rf_transmit_(i, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::send_infrared_rf_transmit_complete(const EntityBase &entity) {
|
||||
uint32_t device_id = 0;
|
||||
#ifdef USE_DEVICES
|
||||
device_id = entity.get_device_id();
|
||||
#endif
|
||||
const uint32_t key = entity.get_object_id_hash();
|
||||
auto &pending = this->pending_ir_rf_transmits_;
|
||||
for (size_t i = 0; i < this->pending_ir_rf_count_; i++) {
|
||||
if (pending[i].key == key && pending[i].device_id == device_id) {
|
||||
this->complete_pending_ir_rf_transmit_(i, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::complete_pending_ir_rf_transmit_(size_t index, bool success) {
|
||||
const auto &entry = this->pending_ir_rf_transmits_[index];
|
||||
InfraredRFTransmitCompleteResponse resp{};
|
||||
#ifdef USE_DEVICES
|
||||
resp.device_id = entry.device_id;
|
||||
#endif
|
||||
resp.key = entry.key;
|
||||
resp.success = success;
|
||||
entry.connection->send_infrared_rf_transmit_complete_response(resp);
|
||||
this->erase_pending_ir_rf_transmit_(index);
|
||||
}
|
||||
|
||||
void APIServer::erase_pending_ir_rf_transmit_(size_t index) {
|
||||
auto &pending = this->pending_ir_rf_transmits_;
|
||||
for (size_t i = index + 1; i < this->pending_ir_rf_count_; i++) {
|
||||
pending[i - 1] = pending[i];
|
||||
}
|
||||
this->pending_ir_rf_count_--;
|
||||
}
|
||||
|
||||
void APIServer::unregister_pending_ir_rf_transmits_for_connection_(APIConnection *conn) {
|
||||
auto &pending = this->pending_ir_rf_transmits_;
|
||||
uint8_t kept = 0;
|
||||
for (size_t i = 0; i < this->pending_ir_rf_count_; i++) {
|
||||
if (pending[i].connection != conn) {
|
||||
pending[kept++] = pending[i];
|
||||
}
|
||||
}
|
||||
this->pending_ir_rf_count_ = kept;
|
||||
}
|
||||
|
||||
// A timer firing on an empty list is harmless, so nothing cancels it
|
||||
void APIServer::expire_pending_ir_rf_transmits_() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
auto &pending = this->pending_ir_rf_transmits_;
|
||||
while (this->pending_ir_rf_count_ != 0 && now - pending[0].registered_ms >= IR_RF_TRANSMIT_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "IR/RF transmit %" PRIu32 " never reported completion", pending[0].key);
|
||||
this->complete_pending_ir_rf_transmit_(0, false);
|
||||
}
|
||||
if (this->pending_ir_rf_count_ != 0) {
|
||||
this->set_timeout(IR_RF_TRANSMIT_TIMEOUT, IR_RF_TRANSMIT_TIMEOUT_MS - (now - pending[0].registered_ms),
|
||||
[this]() { this->expire_pending_ir_rf_transmits_(); });
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
|
||||
@@ -196,6 +196,10 @@ class APIServer final : public Component,
|
||||
#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);
|
||||
// Completion replies for InfraredRFTransmitRawTimingsRequest (API 1.18+); register before perform()
|
||||
void register_pending_ir_rf_transmit(uint32_t device_id, uint32_t key, APIConnection *conn);
|
||||
void fail_pending_ir_rf_transmit(uint32_t device_id, uint32_t key, APIConnection *conn);
|
||||
void send_infrared_rf_transmit_complete(const EntityBase &entity);
|
||||
#endif
|
||||
|
||||
bool is_connected() const { return this->api_connection_count_ != 0; }
|
||||
@@ -342,6 +346,24 @@ class APIServer final : public Component,
|
||||
uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
struct PendingIrRfTransmit {
|
||||
uint32_t device_id;
|
||||
uint32_t key;
|
||||
uint32_t registered_ms;
|
||||
APIConnection *connection;
|
||||
};
|
||||
// FIFO per entity: entities forward only completions of frames they submitted, and each
|
||||
// completion pops the oldest match, so entries are never reordered. One slot per connection:
|
||||
// a pacing client has at most one transmit outstanding. Unlike action calls, transmits share
|
||||
// one named expiry timer, since a scheduler item per entry would churn the heap.
|
||||
std::array<PendingIrRfTransmit, MAX_API_CONNECTIONS> pending_ir_rf_transmits_{};
|
||||
uint8_t pending_ir_rf_count_{0};
|
||||
void complete_pending_ir_rf_transmit_(size_t index, bool success);
|
||||
void erase_pending_ir_rf_transmit_(size_t index);
|
||||
void unregister_pending_ir_rf_transmits_for_connection_(APIConnection *conn);
|
||||
void expire_pending_ir_rf_transmits_();
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
struct PendingActionResponse {
|
||||
uint32_t call_id;
|
||||
|
||||
@@ -153,13 +153,14 @@ bool ES7210::configure_mic_gain_() {
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
|
||||
|
||||
// Configure mic 3
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
// MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains.
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
|
||||
|
||||
// Configure mic 4
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
|
||||
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
|
||||
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
|
||||
|
||||
@@ -47,11 +47,7 @@ InfraredCall &InfraredCall::set_repeat_count(uint32_t count) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
void InfraredCall::perform() {
|
||||
if (this->parent_ != nullptr) {
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
}
|
||||
bool InfraredCall::perform() { return this->parent_ != nullptr && this->parent_->control(*this); }
|
||||
|
||||
// ========== Infrared ==========
|
||||
|
||||
@@ -64,6 +60,15 @@ void Infrared::setup() {
|
||||
if (this->receiver_ != nullptr) {
|
||||
this->receiver_->register_listener(this);
|
||||
}
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
if (this->transmitter_ != nullptr) {
|
||||
// only frames this entity submitted; YAML automations share the transmitter
|
||||
this->transmitter_->set_on_complete_callback([this](uint32_t seq) {
|
||||
if (seq == this->inflight_seq_)
|
||||
this->notify_transmit_complete_();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Infrared::dump_config() {
|
||||
@@ -75,15 +80,15 @@ void Infrared::dump_config() {
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
}
|
||||
|
||||
void Infrared::control(const InfraredCall &call) {
|
||||
bool Infrared::control(const InfraredCall &call) {
|
||||
if (this->transmitter_ == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!call.has_raw_timings()) {
|
||||
ESP_LOGE(TAG, "No raw timings provided");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create transmit data object
|
||||
@@ -107,7 +112,7 @@ void Infrared::control(const InfraredCall &call) {
|
||||
// Decode base64url (URL-safe) into transmit buffer
|
||||
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base64url data");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
// Sanity check: validate timing values are within reasonable bounds
|
||||
constexpr int32_t max_timing_us = 500000; // 500ms absolute max
|
||||
@@ -115,7 +120,7 @@ void Infrared::control(const InfraredCall &call) {
|
||||
int32_t abs_timing = timing < 0 ? -timing : timing;
|
||||
if (abs_timing > max_timing_us) {
|
||||
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
|
||||
@@ -133,7 +138,16 @@ void Infrared::control(const InfraredCall &call) {
|
||||
}
|
||||
|
||||
// Perform transmission
|
||||
this->inflight_seq_ = transmit_call.get_seq();
|
||||
transmit_call.perform();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Infrared::notify_transmit_complete_() {
|
||||
#if defined(USE_API) && defined(USE_IR_RF)
|
||||
if (api::global_api_server != nullptr)
|
||||
api::global_api_server->send_infrared_rf_transmit_complete(*this);
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t Infrared::get_capability_flags() const {
|
||||
|
||||
@@ -54,8 +54,8 @@ class InfraredCall {
|
||||
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
|
||||
InfraredCall &set_repeat_count(uint32_t count);
|
||||
|
||||
/// Perform the transmission
|
||||
void perform();
|
||||
/// Perform the transmission; returns true if a frame was handed to the transmitter
|
||||
bool perform();
|
||||
|
||||
/// Get the carrier frequency
|
||||
const optional<uint32_t> &get_carrier_frequency() const { return this->carrier_frequency_; }
|
||||
@@ -145,8 +145,11 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
|
||||
protected:
|
||||
friend class InfraredCall;
|
||||
|
||||
/// Perform the actual transmission (called by InfraredCall)
|
||||
virtual void control(const InfraredCall &call);
|
||||
/// Perform the actual transmission (called by InfraredCall); false if nothing was transmitted
|
||||
virtual bool control(const InfraredCall &call);
|
||||
/// Forwards the transmitter's completion to the API server
|
||||
void notify_transmit_complete_();
|
||||
uint32_t inflight_seq_{0}; // seq of the frame this entity submitted last
|
||||
|
||||
// Underlying hardware components
|
||||
remote_base::RemoteReceiverBase *receiver_{nullptr};
|
||||
|
||||
@@ -12,16 +12,16 @@ static const char *const TAG = "ir_rf_proxy";
|
||||
// Static template: all instantiations occur in this translation unit.
|
||||
|
||||
template<typename CallT>
|
||||
static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
|
||||
const CallT &call) {
|
||||
static bool transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
|
||||
const CallT &call, uint32_t &inflight_seq) {
|
||||
if (transmitter == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!call.has_raw_timings()) {
|
||||
ESP_LOGE(TAG, "No raw timings provided");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto transmit_call = transmitter->transmit();
|
||||
@@ -36,14 +36,14 @@ static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter
|
||||
} else if (call.is_base64url()) {
|
||||
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base64url data");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
constexpr int32_t max_timing_us = 500000;
|
||||
for (int32_t timing : transmit_data->get_data()) {
|
||||
int32_t abs_timing = timing < 0 ? -timing : timing;
|
||||
if (abs_timing > max_timing_us) {
|
||||
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
|
||||
@@ -58,7 +58,9 @@ static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter
|
||||
transmit_call.set_send_times(call.get_repeat_count());
|
||||
}
|
||||
|
||||
inflight_seq = transmit_call.get_seq();
|
||||
transmit_call.perform();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========== IrRfProxy (Infrared platform) ==========
|
||||
@@ -80,9 +82,9 @@ void IrRfProxy::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
void IrRfProxy::control(const infrared::InfraredCall &call) {
|
||||
bool IrRfProxy::control(const infrared::InfraredCall &call) {
|
||||
uint32_t carrier = call.get_carrier_frequency().value_or(0);
|
||||
transmit_raw_timings(this->transmitter_, carrier, call);
|
||||
return transmit_raw_timings(this->transmitter_, carrier, call, this->inflight_seq_);
|
||||
}
|
||||
|
||||
#endif // USE_IR_RF
|
||||
@@ -101,6 +103,15 @@ void RfProxy::setup() {
|
||||
if (this->receiver_ != nullptr) {
|
||||
this->receiver_->register_listener(this);
|
||||
}
|
||||
#if defined(USE_API) && defined(USE_RADIO_FREQUENCY)
|
||||
if (this->transmitter_ != nullptr) {
|
||||
// only frames this entity submitted; YAML automations share the transmitter
|
||||
this->transmitter_->set_on_complete_callback([this](uint32_t seq) {
|
||||
if (seq == this->inflight_seq_)
|
||||
this->notify_transmit_complete_();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void RfProxy::dump_config() {
|
||||
@@ -122,11 +133,11 @@ void RfProxy::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
|
||||
bool RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
|
||||
// RF: no IR carrier modulation. Any RF front-end coordination (state turnaround, retuning)
|
||||
// happens via the radio_frequency entity's on_control trigger and remote_transmitter's
|
||||
// on_transmit/on_complete triggers — wired up in user YAML.
|
||||
transmit_raw_timings(this->transmitter_, 0, call);
|
||||
return transmit_raw_timings(this->transmitter_, 0, call, this->inflight_seq_);
|
||||
}
|
||||
|
||||
#endif // USE_RADIO_FREQUENCY
|
||||
|
||||
@@ -35,7 +35,7 @@ class IrRfProxy final : public infrared::Infrared {
|
||||
void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); }
|
||||
|
||||
protected:
|
||||
void control(const infrared::InfraredCall &call) override;
|
||||
bool control(const infrared::InfraredCall &call) override;
|
||||
|
||||
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
|
||||
uint32_t frequency_khz_{0};
|
||||
@@ -63,7 +63,7 @@ class RfProxy final : public radio_frequency::RadioFrequency {
|
||||
void set_frequency_hz(uint32_t freq_hz) { this->traits_.set_fixed_frequency_hz(freq_hz); }
|
||||
|
||||
protected:
|
||||
void control(const radio_frequency::RadioFrequencyCall &call) override;
|
||||
bool control(const radio_frequency::RadioFrequencyCall &call) override;
|
||||
|
||||
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
|
||||
remote_base::RemoteReceiverBase *receiver_{nullptr};
|
||||
|
||||
@@ -52,14 +52,15 @@ RadioFrequencyCall &RadioFrequencyCall::set_repeat_count(uint32_t count) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
void RadioFrequencyCall::perform() {
|
||||
if (this->parent_ != nullptr) {
|
||||
// Fire any on_control hooks (user-wired automations) before handing off to
|
||||
// the platform-specific control() — gives users a chance to react to call
|
||||
// parameters (e.g. retune an external RF front-end based on call.get_frequency()).
|
||||
this->parent_->control_callback_.call(*this);
|
||||
this->parent_->control(*this);
|
||||
bool RadioFrequencyCall::perform() {
|
||||
if (this->parent_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
// Fire any on_control hooks (user-wired automations) before handing off to
|
||||
// the platform-specific control() — gives users a chance to react to call
|
||||
// parameters (e.g. retune an external RF front-end based on call.get_frequency()).
|
||||
this->parent_->control_callback_.call(*this);
|
||||
return this->parent_->control(*this);
|
||||
}
|
||||
|
||||
// ========== RadioFrequency ==========
|
||||
@@ -108,4 +109,11 @@ bool RadioFrequency::on_receive(remote_base::RemoteReceiveData data) {
|
||||
return false; // Don't consume the event, allow other listeners to process it
|
||||
}
|
||||
|
||||
void RadioFrequency::notify_transmit_complete_() {
|
||||
#if defined(USE_API) && defined(USE_RADIO_FREQUENCY)
|
||||
if (api::global_api_server != nullptr)
|
||||
api::global_api_server->send_infrared_rf_transmit_complete(*this);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::radio_frequency
|
||||
|
||||
@@ -64,8 +64,8 @@ class RadioFrequencyCall {
|
||||
/// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.)
|
||||
RadioFrequencyCall &set_repeat_count(uint32_t count);
|
||||
|
||||
/// Perform the transmission
|
||||
void perform();
|
||||
/// Perform the transmission; returns true if a frame was handed to the transmitter
|
||||
bool perform();
|
||||
|
||||
/// Get the frequency in Hz
|
||||
const optional<uint32_t> &get_frequency() const { return this->frequency_hz_; }
|
||||
@@ -184,7 +184,11 @@ class RadioFrequency : public Component, public EntityBase, public remote_base::
|
||||
|
||||
/// Perform the actual transmission (called by RadioFrequencyCall::perform())
|
||||
/// Platforms must override this to implement hardware-specific transmission.
|
||||
virtual void control(const RadioFrequencyCall &call) = 0;
|
||||
/// Returns false if nothing was transmitted.
|
||||
virtual bool control(const RadioFrequencyCall &call) = 0;
|
||||
/// Forwards the transmitter's completion to the API server; platforms hook their transmitter to it
|
||||
void notify_transmit_complete_();
|
||||
uint32_t inflight_seq_{0}; // seq of the frame this entity submitted last
|
||||
|
||||
// Traits describing capabilities
|
||||
RadioFrequencyTraits traits_;
|
||||
|
||||
@@ -163,7 +163,7 @@ bool RemoteTransmitData::set_data_from_base64url(const std::string &base64url) {
|
||||
|
||||
/* RemoteTransmitterBase */
|
||||
|
||||
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
|
||||
void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait, uint32_t seq) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
const auto &vec = this->temp_.get_data();
|
||||
char buffer[256];
|
||||
@@ -195,6 +195,7 @@ void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) {
|
||||
ESP_LOGVV(TAG, "%s", buffer);
|
||||
}
|
||||
#endif
|
||||
this->current_seq_ = seq;
|
||||
this->send_internal(send_times, send_wait);
|
||||
}
|
||||
} // namespace esphome::remote_base
|
||||
|
||||
@@ -146,21 +146,24 @@ class RemoteTransmitterBase : public RemoteComponentBase {
|
||||
RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {}
|
||||
class TransmitCall {
|
||||
public:
|
||||
explicit TransmitCall(RemoteTransmitterBase *parent) : parent_(parent) {}
|
||||
TransmitCall(RemoteTransmitterBase *parent, uint32_t seq) : parent_(parent), seq_(seq) {}
|
||||
RemoteTransmitData *get_data() { return &this->parent_->temp_; }
|
||||
void set_send_times(uint32_t send_times) { send_times_ = send_times; }
|
||||
void set_send_wait(uint32_t send_wait) { send_wait_ = send_wait; }
|
||||
void perform() { this->parent_->send_(this->send_times_, this->send_wait_); }
|
||||
/// Identifies this transmission in the completion callback
|
||||
uint32_t get_seq() const { return this->seq_; }
|
||||
void perform() { this->parent_->send_(this->send_times_, this->send_wait_, this->seq_); }
|
||||
|
||||
protected:
|
||||
RemoteTransmitterBase *parent_;
|
||||
uint32_t send_times_{1};
|
||||
uint32_t send_wait_{0};
|
||||
uint32_t seq_;
|
||||
};
|
||||
|
||||
TransmitCall transmit() {
|
||||
this->temp_.reset();
|
||||
return TransmitCall(this);
|
||||
return TransmitCall(this, ++this->next_seq_);
|
||||
}
|
||||
template<typename Protocol>
|
||||
void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) {
|
||||
@@ -170,11 +173,32 @@ class RemoteTransmitterBase : public RemoteComponentBase {
|
||||
call.set_send_wait(send_wait);
|
||||
call.perform();
|
||||
}
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
/// Called with the TransmitCall's seq once that transmission has finished, after the last
|
||||
/// repeat and before the on_complete trigger. One slot: a transmitter is driven by a single
|
||||
/// infrared or radio_frequency entity.
|
||||
template<typename F> void set_on_complete_callback(F &&callback) {
|
||||
this->complete_callback_ = Callback<void(uint32_t)>::create(std::forward<F>(callback));
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void send_(uint32_t send_times, uint32_t send_wait);
|
||||
void send_(uint32_t send_times, uint32_t send_wait, uint32_t seq);
|
||||
virtual void send_internal(uint32_t send_times, uint32_t send_wait) = 0;
|
||||
void send_single_() { this->send_(1, 0); }
|
||||
void send_single_() { this->send_(1, 0, ++this->next_seq_); }
|
||||
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
|
||||
void notify_complete_(uint32_t seq) {
|
||||
if (this->complete_callback_.fn_ != nullptr)
|
||||
this->complete_callback_.call(seq);
|
||||
}
|
||||
|
||||
Callback<void(uint32_t)> complete_callback_{};
|
||||
#else
|
||||
void notify_complete_(uint32_t seq) {}
|
||||
#endif
|
||||
// seq handed to the platform by send_(); platforms copy it when they accept the frame
|
||||
uint32_t next_seq_{0};
|
||||
uint32_t current_seq_{0};
|
||||
|
||||
/// Use same vector for all transmits, avoids many allocations
|
||||
RemoteTransmitData temp_;
|
||||
|
||||
@@ -80,6 +80,7 @@ void RemoteTransmitterComponent::digital_write(bool value) { this->pin_->digital
|
||||
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
ESP_LOGD(TAG, "Sending remote code");
|
||||
this->inflight_seq_ = this->current_seq_;
|
||||
uint32_t on_time, off_time;
|
||||
this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
|
||||
this->transmit_trigger_.trigger();
|
||||
@@ -114,7 +115,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
}
|
||||
}
|
||||
}
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_();
|
||||
}
|
||||
|
||||
} // namespace esphome::remote_transmitter
|
||||
|
||||
@@ -82,6 +82,12 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa
|
||||
|
||||
protected:
|
||||
void send_internal(uint32_t send_times, uint32_t send_wait) override;
|
||||
// completion callbacks run before the user's on_complete automation
|
||||
void fire_complete_() {
|
||||
this->notify_complete_(this->inflight_seq_);
|
||||
this->complete_trigger_.trigger();
|
||||
}
|
||||
uint32_t inflight_seq_{0}; // seq of the frame whose completion is still to be reported
|
||||
#if defined(USE_ESP8266) || \
|
||||
(defined(USE_LIBRETINY) && !defined(USE_LIBRETINY_VARIANT_RTL8720C) && !defined(REMOTE_TRANSMITTER_BK_PWM)) || \
|
||||
defined(USE_RP2) || (defined(USE_ESP32) && !SOC_RMT_SUPPORTED)
|
||||
|
||||
@@ -110,7 +110,7 @@ void RemoteTransmitterComponent::deliver_completion_() {
|
||||
if (!this->stall_aborted_)
|
||||
this->status_clear_warning();
|
||||
this->complete_pending_ = false;
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_();
|
||||
}
|
||||
|
||||
// Waits until no chain is in flight, delivering any deferred completions; a completion
|
||||
@@ -137,6 +137,7 @@ void RemoteTransmitterComponent::wait_until_idle_() {
|
||||
|
||||
// Stages the repeat schedule and stall deadline, then starts the interrupt chain
|
||||
void RemoteTransmitterComponent::arm_chain_(uint32_t send_times, uint32_t send_wait) {
|
||||
this->inflight_seq_ = this->current_seq_;
|
||||
this->isr_repeats_left_ = send_times;
|
||||
this->isr_send_wait_ = send_wait;
|
||||
this->isr_index_ = 0;
|
||||
|
||||
@@ -213,7 +213,7 @@ void RemoteTransmitterComponent::wait_for_rmt_() {
|
||||
this->status_set_warning();
|
||||
}
|
||||
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_();
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
||||
@@ -228,6 +228,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
if (this->non_blocking_ && this->cancel_timeout("complete")) {
|
||||
this->wait_for_rmt_();
|
||||
}
|
||||
this->inflight_seq_ = this->current_seq_;
|
||||
|
||||
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
|
||||
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
|
||||
@@ -300,6 +301,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
this->inflight_seq_ = this->current_seq_;
|
||||
|
||||
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
|
||||
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
|
||||
@@ -364,7 +366,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
if (i + 1 < send_times)
|
||||
delayMicroseconds(send_wait);
|
||||
}
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Sending remote code");
|
||||
this->inflight_seq_ = this->current_seq_;
|
||||
const uint32_t carrier_frequency = this->temp_.get_carrier_frequency();
|
||||
// unmodulated protocols (no carrier or 100% duty) drive the pin constantly during marks
|
||||
float mark_duty =
|
||||
@@ -194,7 +195,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
|
||||
}
|
||||
}
|
||||
}
|
||||
this->complete_trigger_.trigger();
|
||||
this->fire_complete_();
|
||||
}
|
||||
|
||||
#endif // USE_LIBRETINY_VARIANT_RTL8720C
|
||||
|
||||
@@ -6,12 +6,17 @@ from esphome.components import esp32, network, psram, socket, wifi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_ESPHOME,
|
||||
CONF_FORMAT,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_MODEL,
|
||||
CONF_NAME,
|
||||
CONF_PROJECT,
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_SOURCE,
|
||||
CONF_TASK_STACK_IN_PSRAM,
|
||||
CONF_VERSION,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
@@ -27,6 +32,14 @@ DOMAIN = "sendspin"
|
||||
CONF_DISPLAY_OFFSET = "display_offset"
|
||||
CONF_SENDSPIN_ID = "sendspin_id"
|
||||
|
||||
CONF_FIRMWARE_VERSION = "firmware_version"
|
||||
CONF_MANUFACTURER = "manufacturer"
|
||||
|
||||
# An empty device information string would be sent to the server as an empty value rather than
|
||||
# falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps
|
||||
# the length prefix of a protobuf string field to a single byte, matching `esphome: project:`.
|
||||
DEVICE_INFO_STRING = cv.All(cv.string_strict, cv.Length(min=1), cv.ByteLength(max=127))
|
||||
|
||||
CONF_INITIAL_STATIC_DELAY = "initial_static_delay"
|
||||
CONF_FIXED_DELAY = "fixed_delay"
|
||||
CONF_DECODE_MEMORY = "decode_memory"
|
||||
@@ -198,6 +211,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SendspinHub),
|
||||
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
|
||||
cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_MODEL): DEVICE_INFO_STRING,
|
||||
cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING,
|
||||
}
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
@@ -248,6 +264,22 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_task_stack_in_psram(True))
|
||||
psram.request_external_task_stack()
|
||||
|
||||
# Device information for the server's client/hello message. Falls back to the project
|
||||
# information, which is written as `manufacturer.model`. Anything still unset keeps the
|
||||
# default the hub itself applies: the ESPHome name and version.
|
||||
project = CORE.config[CONF_ESPHOME].get(CONF_PROJECT, {})
|
||||
project_manufacturer, _, project_model = project.get(CONF_NAME, "").partition(".")
|
||||
for value, setter in (
|
||||
(config.get(CONF_MANUFACTURER) or project_manufacturer, var.set_manufacturer),
|
||||
(config.get(CONF_MODEL) or project_model, var.set_model),
|
||||
(
|
||||
config.get(CONF_FIRMWARE_VERSION) or project.get(CONF_VERSION),
|
||||
var.set_firmware_version,
|
||||
),
|
||||
):
|
||||
if value:
|
||||
cg.add(setter(value))
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2")
|
||||
|
||||
|
||||
@@ -76,8 +76,12 @@ void SendspinHub::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Sendspin Hub:\n"
|
||||
" Client ID: %s\n"
|
||||
" Manufacturer: %s\n"
|
||||
" Model: %s\n"
|
||||
" Firmware version: %s\n"
|
||||
" Task stack in PSRAM: %s",
|
||||
get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_));
|
||||
get_client_id_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
|
||||
this->firmware_version_, YESNO(this->task_stack_in_psram_));
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
// Slot indices come from the order the image platform entries were declared, so the log is the
|
||||
@@ -127,15 +131,19 @@ const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_P
|
||||
return get_mac_address_pretty_into_buffer(buf);
|
||||
}
|
||||
|
||||
const char *SendspinHub::get_product_name_() const {
|
||||
return this->model_ != nullptr ? this->model_ : App.get_name().c_str();
|
||||
}
|
||||
|
||||
sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
|
||||
sendspin::SendspinClientConfig config;
|
||||
|
||||
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
|
||||
config.name = App.get_friendly_name();
|
||||
config.product_name = App.get_name();
|
||||
config.manufacturer = "ESPHome";
|
||||
config.software_version = ESPHOME_VERSION;
|
||||
config.product_name = this->get_product_name_();
|
||||
config.manufacturer = this->manufacturer_;
|
||||
config.software_version = this->firmware_version_;
|
||||
config.httpd_psram_stack = this->task_stack_in_psram_;
|
||||
|
||||
return config;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#include <sendspin/client.h>
|
||||
#include <sendspin/config.h>
|
||||
@@ -125,6 +126,15 @@ class SendspinHub final : public Component,
|
||||
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
/// @brief Sets the device information reported to the server in the `client/hello` message.
|
||||
///
|
||||
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
|
||||
/// lifetime of the hub. Only called for values the configuration overrides; anything left alone
|
||||
/// keeps the default described on the member below.
|
||||
void set_manufacturer(const char *manufacturer) { this->manufacturer_ = manufacturer; }
|
||||
void set_model(const char *model) { this->model_ = model; }
|
||||
void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; }
|
||||
|
||||
// --- Sendspin role specific methods ---
|
||||
|
||||
#ifdef USE_SENDSPIN_ARTWORK
|
||||
@@ -187,6 +197,9 @@ class SendspinHub final : public Component,
|
||||
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
|
||||
sendspin::SendspinClientConfig build_client_config_();
|
||||
|
||||
/// @brief Returns the product name reported to the server: the configured model, or the device name.
|
||||
const char *get_product_name_() const;
|
||||
|
||||
/// @brief Writes the active network interface's MAC into @p buf and returns its data pointer.
|
||||
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
|
||||
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
|
||||
@@ -268,6 +281,12 @@ class SendspinHub final : public Component,
|
||||
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
|
||||
|
||||
bool task_stack_in_psram_{false};
|
||||
|
||||
// Device information sent in the `client/hello` message. Defaults apply when neither the
|
||||
// sendspin configuration nor the project information supplies a value.
|
||||
const char *manufacturer_{"ESPHome"};
|
||||
const char *model_{nullptr}; // nullptr reports the device name instead
|
||||
const char *firmware_version_{ESPHOME_VERSION};
|
||||
};
|
||||
|
||||
/// @brief Base class for all sendspin subcomponents.
|
||||
|
||||
@@ -30,6 +30,7 @@ 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")
|
||||
|
||||
@@ -29,26 +29,57 @@ 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();
|
||||
}
|
||||
|
||||
void SerialProxy::loop() {
|
||||
#ifdef USE_API
|
||||
// Safety check — loop should only run when subscribed, but guard against races
|
||||
if (this->api_connection_ == nullptr) [[unlikely]] {
|
||||
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_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() ||
|
||||
!api_is_connected()) {
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// Read available data from UART and forward to subscribed client
|
||||
@@ -69,11 +100,54 @@ 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"
|
||||
@@ -92,8 +166,9 @@ 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->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
if (!this->is_subscriber_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring configure request from client without port subscription [%" PRIu32 "]",
|
||||
this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
#endif
|
||||
@@ -159,24 +234,80 @@ 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 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_);
|
||||
// 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_);
|
||||
}
|
||||
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->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
if (!this->is_subscriber_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring modem pin request from client without port subscription [%" PRIu32 "]",
|
||||
this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
#endif
|
||||
@@ -210,8 +341,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->port_claimed_by_other_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_);
|
||||
if (!this->is_subscriber_(api_connection)) {
|
||||
ESP_LOGW(TAG, "Ignoring flush from client without port subscription [%" PRIu32 "]", this->instance_index_);
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
|
||||
}
|
||||
#endif
|
||||
@@ -230,11 +361,6 @@ 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) {
|
||||
@@ -252,6 +378,10 @@ 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();
|
||||
@@ -264,7 +394,15 @@ 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:
|
||||
|
||||
@@ -26,6 +26,7 @@ class APIConnection;
|
||||
namespace enums {
|
||||
enum SerialProxyPortType : uint32_t;
|
||||
enum SerialProxyRequestType : uint32_t;
|
||||
enum SerialProxyMode : uint32_t;
|
||||
} // namespace enums
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -52,6 +53,36 @@ 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;
|
||||
@@ -77,6 +108,9 @@ 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
|
||||
@@ -121,13 +155,67 @@ 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_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 and send to API client (slow path with 256-byte stack buffer)
|
||||
/// Read from UART, hand the bytes to any tap, and forward them to a subscriber
|
||||
/// (slow path with a 256-byte stack buffer)
|
||||
void read_and_send_(size_t available);
|
||||
|
||||
/// True when a live subscriber other than the given connection holds the port
|
||||
bool port_claimed_by_other_(api::APIConnection *api_connection) const;
|
||||
/// 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;
|
||||
#endif
|
||||
|
||||
/// Instance index for identifying this proxy in API messages
|
||||
@@ -147,6 +235,11 @@ 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};
|
||||
@@ -154,6 +247,10 @@ 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
|
||||
};
|
||||
|
||||
} // namespace esphome::serial_proxy
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
#define USE_SENSOR
|
||||
#define USE_SENSOR_FILTER
|
||||
#define USE_SERIAL_PROXY
|
||||
#define USE_SERIAL_PROXY_TAP
|
||||
#define USE_SETUP_PRIORITY_OVERRIDE
|
||||
#define USE_STATUS_LED
|
||||
#define USE_STATUS_SENSOR
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ pyserial==3.5
|
||||
platformio==6.1.19
|
||||
esptool==5.4.0
|
||||
click==8.3.3
|
||||
aioesphomeapi==46.3.0
|
||||
aioesphomeapi==46.4.0
|
||||
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
|
||||
zeroconf==0.151.3
|
||||
puremagic==2.2.0
|
||||
|
||||
@@ -19,7 +19,7 @@ class InfraredCall {
|
||||
return *this;
|
||||
}
|
||||
InfraredCall &set_repeat_count(uint32_t /*count*/) { return *this; }
|
||||
void perform() {}
|
||||
bool perform() { return false; }
|
||||
|
||||
protected:
|
||||
Infrared *parent_;
|
||||
|
||||
@@ -23,7 +23,7 @@ class RadioFrequencyCall {
|
||||
RadioFrequencyCall &set_raw_timings_packed(const uint8_t * /*data*/, uint16_t /*length*/, uint16_t /*count*/) {
|
||||
return *this;
|
||||
}
|
||||
void perform() {}
|
||||
bool perform() { return false; }
|
||||
|
||||
protected:
|
||||
RadioFrequency *parent_;
|
||||
|
||||
@@ -40,6 +40,9 @@ class SerialProxy {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {}
|
||||
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode) {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
@@ -0,0 +1,18 @@
|
||||
esphome:
|
||||
name: test
|
||||
project:
|
||||
name: project_manufacturer.project_model
|
||||
version: 9.9.9
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
manufacturer: Explicit Manufacturer
|
||||
model: Explicit Model
|
||||
firmware_version: 1.2.3
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
project:
|
||||
name: project_manufacturer.project_model
|
||||
version: 9.9.9
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ap:
|
||||
|
||||
sendspin:
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Tests for the device information the sendspin hub reports to the server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.sendspin import (
|
||||
CONF_FIRMWARE_VERSION,
|
||||
CONF_MANUFACTURER,
|
||||
CONFIG_SCHEMA,
|
||||
)
|
||||
from esphome.const import CONF_MODEL, PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def test_explicit_device_info_wins_over_project(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Configured values take precedence over the project information."""
|
||||
main_cpp = generate_main(component_config_path("device_info_explicit.yaml"))
|
||||
|
||||
assert 'set_manufacturer("Explicit Manufacturer")' in main_cpp
|
||||
assert 'set_model("Explicit Model")' in main_cpp
|
||||
assert 'set_firmware_version("1.2.3")' in main_cpp
|
||||
|
||||
|
||||
def test_project_supplies_device_info(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without configured values, the project name splits into manufacturer and model."""
|
||||
main_cpp = generate_main(component_config_path("device_info_project.yaml"))
|
||||
|
||||
assert 'set_manufacturer("project_manufacturer")' in main_cpp
|
||||
assert 'set_model("project_model")' in main_cpp
|
||||
assert 'set_firmware_version("9.9.9")' in main_cpp
|
||||
|
||||
|
||||
def test_no_device_info_leaves_hub_defaults(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""With neither source, nothing is emitted and the hub keeps its own defaults."""
|
||||
main_cpp = generate_main(component_config_path("device_info_default.yaml"))
|
||||
|
||||
assert "set_manufacturer(" not in main_cpp
|
||||
assert "set_model(" not in main_cpp
|
||||
assert "set_firmware_version(" not in main_cpp
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION]
|
||||
)
|
||||
def test_empty_device_info_rejected(
|
||||
set_core_config: SetCoreConfigCallable, conf_key: str
|
||||
) -> None:
|
||||
"""An empty string would be sent to the server as an empty value, so it is not accepted."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: ""})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION]
|
||||
)
|
||||
def test_device_info_capped_at_127_bytes(
|
||||
set_core_config: SetCoreConfigCallable, conf_key: str
|
||||
) -> None:
|
||||
"""The cap is in bytes so the protobuf length prefix stays a single byte."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
CONFIG_SCHEMA({conf_key: "a" * 127})
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: "a" * 128})
|
||||
# 64 two-byte characters is 128 bytes.
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA({conf_key: "é" * 64})
|
||||
@@ -4,3 +4,6 @@ psram:
|
||||
sendspin:
|
||||
id: sendspin_hub_id
|
||||
task_stack_in_psram: true
|
||||
manufacturer: Test Manufacturer
|
||||
model: Test Model
|
||||
firmware_version: 1.2.3
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
# Compile the tap code paths; no tap is attached, so this exercises the
|
||||
# null-tap branches that a normal build never defines.
|
||||
esphome:
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- "-DUSE_SERIAL_PROXY_TAP"
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
serial_proxy: !include common.yaml
|
||||
Reference in New Issue
Block a user