ESPHome 2026.9.1
Loading...
Searching...
No Matches
serial_proxy.cpp
Go to the documentation of this file.
1#include "serial_proxy.h"
2
3#ifdef USE_SERIAL_PROXY
4
5#include "esphome/core/log.h"
6
7#include <cinttypes>
8#include "esphome/core/util.h"
9
10#ifdef USE_API
13#endif
14
16
17static const char *const TAG = "serial_proxy";
18
20 // Set up modem control pins if configured
21 if (this->rts_pin_ != nullptr) {
22 this->rts_pin_->setup();
23 this->rts_pin_->digital_write(this->rts_state_);
24 }
25 if (this->dtr_pin_ != nullptr) {
26 this->dtr_pin_->setup();
27 this->dtr_pin_->digital_write(this->dtr_state_);
28 }
29#ifdef USE_API
30 // instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data
32#endif
33 // No subscriber at startup; disable loop until a client subscribes
34 this->disable_loop();
35}
36
38#ifdef USE_API
39 // Safety check — loop should only run when subscribed, but guard against races
40 if (this->api_connection_ == nullptr) [[unlikely]] {
41 this->disable_loop();
42 return;
43 }
44
45 // Detect subscriber disconnect
46 if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() ||
48 ESP_LOGW(TAG, "Subscriber disconnected");
49 this->api_connection_ = nullptr;
50 this->disable_loop();
51 return;
52 }
53
54 // Read available data from UART and forward to subscribed client
55 size_t available = this->available();
56 if (available == 0)
57 return;
58
59 this->read_and_send_(available);
60#endif
61}
62
63#ifdef USE_API
64void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) {
65 // Read in chunks up to SERIAL_PROXY_MAX_READ_SIZE
66 uint8_t buffer[SERIAL_PROXY_MAX_READ_SIZE];
67 size_t to_read = std::min(available, sizeof(buffer));
68
69 if (!this->read_array(buffer, to_read))
70 return;
71
72 this->outgoing_msg_.set_data(buffer, to_read);
74}
75#endif
76
78 ESP_LOGCONFIG(TAG,
79 "Serial Proxy [%" PRIu32 "]:\n"
80 " Name: %s\n"
81 " Port Type: %s\n"
82 " RTS Pin: %s\n"
83 " DTR Pin: %s",
84 this->instance_index_, this->name_ != nullptr ? this->name_ : "",
85 this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
86 : this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
87 : LOG_STR_LITERAL("TTL"),
88 this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"),
89 this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"));
90}
91
92SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
93 uint8_t parity, uint8_t stop_bits, uint8_t data_size) {
94#ifdef USE_API
95 if (this->port_claimed_by_other_(api_connection)) {
96 ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_);
98 }
99#endif
100 ESP_LOGD(TAG,
101 "Configuring serial proxy [%" PRIu32 "]: baud=%" PRIu32 ", flow_ctrl=%s, parity=%" PRIu8 ", stop=%" PRIu8
102 ", data=%" PRIu8,
103 this->instance_index_, baudrate, YESNO(flow_control), parity, stop_bits, data_size);
104
105 auto *uart_comp = this->parent_;
106 if (uart_comp == nullptr) {
107 ESP_LOGE(TAG, "UART component not available");
109 }
110
111 // Validate all parameters before applying any (values come from a remote client)
112 if (baudrate == 0) {
113 ESP_LOGW(TAG, "Invalid baud rate: 0");
115 }
117 ESP_LOGW(TAG, "Invalid stop bits: %u (must be 1 or 2)", stop_bits);
119 }
120 if (data_size < 5 || data_size > 8) {
121 ESP_LOGW(TAG, "Invalid data bits: %u (must be 5-8)", data_size);
123 }
124 if (parity > 2) {
125 ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity);
127 }
128 if (flow_control) {
129 ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported");
131 }
132
133 // Skip a no-op reconfigure. Clients routinely re-send identical settings on every
134 // port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
135 // Some bridges watch line-coding changes as a signalling channel (a magic baud
136 // sequence to enter a bootloader, say), so redundant applies are not harmless.
137 static const uart::UARTParityOptions PARITY_MAP[] = {
141 };
142 if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
143 uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
144 ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
146 }
147
148 // Apply validated parameters
149 uart_comp->set_baud_rate(baudrate);
150 uart_comp->set_stop_bits(stop_bits);
151 uart_comp->set_data_bits(data_size);
152
153 uart_comp->set_parity(PARITY_MAP[parity]);
154
155 // load_settings() is available on ESP8266 and ESP32 platforms
156#if defined(USE_ESP8266) || defined(USE_ESP32)
157 uart_comp->load_settings(true);
158#endif
160}
161
162void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {
163#ifdef USE_API
164 // Bytes from a client other than the live subscriber would interleave with the
165 // subscriber's traffic on the wire
166 if (this->port_claimed_by_other_(api_connection)) {
167 ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_);
168 return;
169 }
170#endif
171 if (data == nullptr || len == 0)
172 return;
173 this->write_array(data, len);
174}
175
177#ifdef USE_API
178 if (this->port_claimed_by_other_(api_connection)) {
179 ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_);
181 }
182#endif
183 // Asserting a pin that is not configured must fail so the client learns the signal never
184 // reached the wire; deasserting an absent pin is harmless and stays allowed. Clients can
185 // avoid this by masking against SerialProxyInfo.configured_line_states.
186 if ((line_states & ~this->get_configured_modem_pins()) != 0) {
187 ESP_LOGW(TAG, "Requested modem pin not configured on serial proxy [%" PRIu32 "]", this->instance_index_);
189 }
190 const bool rts = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_RTS) != 0;
191 const bool dtr = (line_states & SERIAL_PROXY_LINE_STATE_FLAG_DTR) != 0;
192 ESP_LOGV(TAG, "Setting modem pins [%" PRIu32 "]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr));
193
194 if (this->rts_pin_ != nullptr) {
195 this->rts_state_ = rts;
196 this->rts_pin_->digital_write(rts);
197 }
198 if (this->dtr_pin_ != nullptr) {
199 this->dtr_state_ = dtr;
200 this->dtr_pin_->digital_write(dtr);
201 }
203}
204
206 return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) |
207 (this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u);
208}
209
211#ifdef USE_API
212 // Flushing stalls the port, so it gets the same ownership check as writes
213 if (this->port_claimed_by_other_(api_connection)) {
214 ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_);
216 }
217#endif
218 ESP_LOGV(TAG, "Flushing serial proxy [%" PRIu32 "]", this->instance_index_);
219 switch (this->flush()) {
228 }
229 return SerialProxyResult::SERIAL_PROXY_RESULT_ERROR; // Unreachable; all enum values handled above
230}
231
232#ifdef USE_API
234 return this->api_connection_ != nullptr && this->api_connection_ != api_connection &&
236}
237
240 switch (type) {
242 if (this->api_connection_ == api_connection) {
243 ESP_LOGV(TAG, "API connection is already subscribed to serial proxy [%" PRIu32 "]", this->instance_index_);
245 }
246 if (this->api_connection_ != nullptr) {
247 // A living subscriber keeps exclusive access. Its connection may be dead without
248 // loop() having noticed yet (e.g. the client crashed and reconnected quickly);
249 // in that case let the new client take over instead of locking it out.
251 ESP_LOGE(TAG, "Only one API subscription is allowed at a time");
253 }
254 ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
255 }
256 this->api_connection_ = api_connection;
257 this->enable_loop();
258 ESP_LOGV(TAG, "API connection subscribed to serial proxy [%" PRIu32 "]", this->instance_index_);
261 // Unsubscribe is idempotent: not being subscribed is not an error
262 if (this->api_connection_ != api_connection) {
263 ESP_LOGV(TAG, "API connection is not subscribed to serial proxy [%" PRIu32 "]", this->instance_index_);
265 }
266 this->api_connection_ = nullptr;
267 this->disable_loop();
268 ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
270 default:
271 ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(type));
273 }
274}
275#endif
276
277} // namespace esphome::serial_proxy
278
279#endif // USE_SERIAL_PROXY
void enable_loop()
Enable this component's loop.
Definition component.h:246
void disable_loop()
Disable this component's loop.
virtual void setup()=0
virtual void digital_write(bool value)=0
void send_serial_proxy_data(const SerialProxyDataReceived &msg)
void set_data(const uint8_t *data, size_t len)
Definition api_pb2.h:3292
SerialProxyResult configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size)
Configure UART parameters and apply them.
void read_and_send_(size_t available)
Read from UART and send to API client (slow path with 256-byte stack buffer)
SerialProxyResult serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type)
Handle a subscribe/unsubscribe request from an API client.
uint32_t get_modem_pins() const
Get current modem pin states as a bitmask of SerialProxyLineStateFlag values.
uint32_t instance_index_
Instance index for identifying this proxy in API messages.
bool port_claimed_by_other_(api::APIConnection *api_connection) const
True when a live subscriber other than the given connection holds the port.
bool rts_state_
Current modem pin states.
SerialProxyResult flush_port(api::APIConnection *api_connection)
Flush the serial port (block until all TX data is sent)
uint32_t get_configured_modem_pins() const
Get the modem pins this instance can drive as a bitmask of SerialProxyLineStateFlag values.
api::SerialProxyDataReceived outgoing_msg_
Pre-allocated outgoing message; instance field is set once in setup()
const char * name_
Human-readable port name (points to a string literal in flash)
api::enums::SerialProxyPortType port_type_
Port type.
GPIOPin * rts_pin_
Optional GPIO pins for modem control.
SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states)
Set modem pin states from a bitmask of SerialProxyLineStateFlag values.
void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len)
Write data received from an API client to the serial device.
api::APIConnection * api_connection_
Subscribed API client (only one allowed at a time)
void set_baud_rate(uint32_t baud_rate)
UARTFlushResult flush()
Definition uart.h:49
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:39
UARTComponent * parent_
Definition uart.h:75
void write_array(const uint8_t *data, size_t len)
Definition uart.h:27
uint8_t UARTParityOptions parity
Definition uart.h:72
struct @66::@67 __attribute__
Wake the main loop task from an ISR. ISR-safe.
Definition main_task.h:32
uint16_t type
@ SERIAL_PROXY_PORT_TYPE_RS232
Definition api_pb2.h:24
@ SERIAL_PROXY_PORT_TYPE_RS485
Definition api_pb2.h:25
@ SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE
Definition api_pb2.h:355
@ SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE
Definition api_pb2.h:354
@ SERIAL_PROXY_LINE_STATE_FLAG_RTS
RTS (Request To Send)
@ SERIAL_PROXY_LINE_STATE_FLAG_DTR
DTR (Data Terminal Ready)
SerialProxyResult
Result of a client-initiated operation; mapped to api::enums::SerialProxyStatus by the API layer.
@ SERIAL_PROXY_RESULT_TIMEOUT
Timed out before TX completed.
@ SERIAL_PROXY_RESULT_ERROR
Driver or hardware error.
@ SERIAL_PROXY_RESULT_NOT_SUPPORTED
Requested feature is not available on this instance.
@ SERIAL_PROXY_RESULT_PORT_IN_USE
Denied: another live client holds the port.
@ SERIAL_PROXY_RESULT_OK
Operation completed or request accepted.
@ SERIAL_PROXY_RESULT_INVALID_ARGUMENT
A parameter value is out of range.
@ SERIAL_PROXY_RESULT_ASSUMED_SUCCESS
Platform cannot confirm TX drain; success assumed.
constexpr size_t SERIAL_PROXY_MAX_READ_SIZE
Maximum bytes to read from UART in a single loop iteration.
@ UART_FLUSH_RESULT_ASSUMED_SUCCESS
Platform cannot report result; success is assumed.
@ UART_FLUSH_RESULT_SUCCESS
Confirmed: all bytes left the TX FIFO.
@ UART_FLUSH_RESULT_FAILED
Confirmed: driver or hardware error.
@ UART_FLUSH_RESULT_TIMEOUT
Confirmed: timed out before TX completed.
const void size_t len
Definition hal.h:64
ESPHOME_ALWAYS_INLINE bool api_is_connected()
Return whether the node has at least one client connected to the native API.
Definition util.h:20
static void uint32_t