Fit this lamp: 4 alarms, full data model, knock snooze, server use

Decode DP127 as 48 bytes (4 alarms), plus DP128, DP107 and DP126, from Tuya's
data model. Add day switches, alarm and sunset controls, a knock sensor on PA18,
display schedule, bedtime fade, radio timer and grouped web UI. Pin LibreTiny
v1.14.0 for the OTA restart fix. Ship the patched UART as an external component
so a server needs only a short YAML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-11-05 12:27:54 +01:00
1 parent 3e3358af08
commit a8f6bb3ca5
36 files changed
+5132 -214

No files matched your search

+17 -1
View File
@@ -132,9 +132,25 @@ The product payload decodes to:
The exact-PID `tuya-local` profile maps DP132 color strings to white/red/orange/yellow/lime/cyan/blue/magenta and also uses the same DP for an effect field. Treat that as reference information until this hardware revision is tested directly.
## Using this from an ESPHome server
Copy `server-example.yaml` to the server and add `wifi_ssid`, `wifi_password`, `ap_password` and `api_encryption_key` to its `secrets.yaml`. The patched UART in `components/uart` loads automatically, so the server's ESPHome install needs no patch. It's a copy from ESPHome 2026.10.2, so recheck it when updating ESPHome. To release a change, update `ref` in `wake-up-light-ii.yaml`, tag that commit, then update `ref` in the server's YAML.
## Corrections from Tuya's data model
Tuya's data model for this family was posted in [tuya-local issue #3821](https://github.com/make-all/tuya-local/issues/3821) for the sibling product `rslo8vqc1xjtaoy9`. It matches what this unit reports, and it replaces the older notes below where they differ.
- **DP127** is 48 bytes on the unit tested here: 12 bytes per alarm, 4 alarms. Logs cut it off at 16 bytes, which likely explains the older 16-byte notes. Bytes: 0 enabled; 1-2 minutes since midnight; 3 repeat days (bit 0 = Monday, 0 = one time); 4 ring duration; 5 mode (0 sound, 1 sound + light, 2 radio, 3 radio + light); 6 sound; 7 volume; 8 radio station; 9 sunrise minutes; 10 brightness after sunrise; 11 snooze (0 allowed, 1 not).
- **DP128** is 10 bytes: 0 unused; 1 mode, as above; 2 sound 1-4; 3 volume; 4 sound duration; 5 radio station; 6 radio duration; 7 start brightness; 8 sunset duration; 9 snooze. Bytes 4, 6 and 8 should match.
- **DP107** is stations found by a scan, then the current station. **DP126** starts (1) or ends (0) a channel search.
- **DP117** and **DP132** are enums on this unit, so values count from 0. DP132 runs cycle, red, orange, yellow, green, cyan, blue, purple.
- **DP125** is online time synchronization. **DP130** is snooze.
Observed behaviour on this unit: a mode without light sets brightness to 0, and a light mode with brightness 0 falls back to no light. Alarm stations were reset to 1 while no stations had been scanned. DP127 writes are ignored while an alarm is snoozed. DP130 stays on after snoozing a ringing alarm and returns to off at once otherwise. Default light transitions flood the MCU with writes, so keep them at 0.
## DP127 alarm structure
DP127 is 16 bytes on this unit. The safest implementation is to **cache the MCU-provided packet and modify only decoded bytes**.
Older notes, see the corrections above. DP127 is 16 bytes on this unit. The safest implementation is to **cache the MCU-provided packet and modify only decoded bytes**.
| Byte(s) | Meaning | Confidence |
|---|---|---|
+260
View File
@@ -0,0 +1,260 @@
###############################################################################
# One alarm's controls. Included once per alarm from wake-up-light-ii.yaml.
# n = alarm number
# o = offset of this alarm's 12-byte block inside DP127
# dp = this alarm's on/off datapoint
# Byte layout is documented next to the DP127 handler in the main file.
###############################################################################
# Bit 0 = Monday, per Tuya's definition and a ringing test. No days set = one time.
packages:
mon: !include { file: alarm_day.yaml, vars: { day: "Mon", bit: "0", w: "11" } }
tue: !include { file: alarm_day.yaml, vars: { day: "Tue", bit: "1", w: "12" } }
wed: !include { file: alarm_day.yaml, vars: { day: "Wed", bit: "2", w: "13" } }
thu: !include { file: alarm_day.yaml, vars: { day: "Thu", bit: "3", w: "14" } }
fri: !include { file: alarm_day.yaml, vars: { day: "Fri", bit: "4", w: "15" } }
sat: !include { file: alarm_day.yaml, vars: { day: "Sat", bit: "5", w: "16" } }
sun: !include { file: alarm_day.yaml, vars: { day: "Sun", bit: "6", w: "17" } }
web_server:
sorting_groups:
- id: group_alarm_${n}
name: "Alarm ${n}"
sorting_weight: 3${n}
switch:
- platform: tuya
id: alarm_${n}_enable
name: "Alarm ${n}"
switch_datapoint: ${dp}
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 1
# Stored inverted: 0 = snooze allowed, 1 = not allowed.
- platform: template
id: alarm_${n}_snooze_allowed
name: "Alarm ${n} Snooze Allowed"
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 10
lambda: |-
if (!id(alarm_raw_valid)) return {};
return id(alarm_raw)[${o} + 11] == 0;
turn_on_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 11] = 0;
- script.execute: send_alarm_settings
turn_off_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 11] = 1;
- script.execute: send_alarm_settings
datetime:
- platform: template
id: alarm_${n}_time
name: "Alarm ${n} Time"
type: time
update_interval: 2s
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 2
lambda: |-
if (!id(alarm_raw_valid)) return {};
uint16_t mins = (static_cast<uint16_t>(id(alarm_raw)[${o} + 1]) << 8) | id(alarm_raw)[${o} + 2];
if (mins >= 1440) return {};
ESPTime t{};
t.hour = mins / 60;
t.minute = mins % 60;
t.second = 0;
return t;
set_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
uint16_t mins = static_cast<uint16_t>(x.hour) * 60 + x.minute;
id(alarm_raw)[${o} + 1] = (mins >> 8) & 0xFF;
id(alarm_raw)[${o} + 2] = mins & 0xFF;
- script.execute: send_alarm_settings
select:
- platform: template
id: alarm_${n}_mode
name: "Alarm ${n} Mode"
options:
- "Sound"
- "Sound + Light"
- "Radio"
- "Radio + Light"
update_interval: 2s
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 2.5
lambda: |-
if (!id(alarm_raw_valid)) return {};
switch (id(alarm_raw)[${o} + 5]) {
case 0: return std::string("Sound");
case 1: return std::string("Sound + Light");
case 2: return std::string("Radio");
case 3: return std::string("Radio + Light");
default: return {};
}
set_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
uint8_t value = 0;
if (x == "Sound + Light") value = 1;
else if (x == "Radio") value = 2;
else if (x == "Radio + Light") value = 3;
id(alarm_raw)[${o} + 5] = value;
// The MCU drops a light mode if brightness is 0, so give it a level.
if ((value == 1 || value == 3) && id(alarm_raw)[${o} + 10] == 0) id(alarm_raw)[${o} + 10] = 10;
- script.execute: send_alarm_settings
- platform: template
id: alarm_${n}_sound
name: "Alarm ${n} Sound"
options:
- "Off"
- "Sound 1"
- "Sound 2"
- "Sound 3"
- "Sound 4"
- "Sound 5"
- "Sound 6"
- "Sound 7"
- "FM Radio"
update_interval: 2s
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 3
lambda: |-
if (!id(alarm_raw_valid)) return {};
uint8_t s = id(alarm_raw)[${o} + 6];
if (s == 0) return std::string("Off");
if (s == 8) return std::string("FM Radio");
if (s >= 1 && s <= 7) return std::string("Sound ") + std::to_string(s);
return {};
set_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
uint8_t value = 0;
if (x == "Sound 1") value = 1;
else if (x == "Sound 2") value = 2;
else if (x == "Sound 3") value = 3;
else if (x == "Sound 4") value = 4;
else if (x == "Sound 5") value = 5;
else if (x == "Sound 6") value = 6;
else if (x == "Sound 7") value = 7;
else if (x == "FM Radio") value = 8;
id(alarm_raw)[${o} + 6] = value;
- script.execute: send_alarm_settings
number:
- platform: template
id: alarm_${n}_volume
name: "Alarm ${n} Volume"
min_value: 1
max_value: 16
step: 1
update_interval: 2s
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 4
lambda: |-
if (!id(alarm_raw_valid)) return {};
return static_cast<float>(id(alarm_raw)[${o} + 7]);
set_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 7] = static_cast<uint8_t>(x);
- script.execute: send_alarm_settings
- platform: template
id: alarm_${n}_wake_brightness
name: "Alarm ${n} Wake Light"
min_value: 1
max_value: 20
step: 1
update_interval: 2s
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 5
lambda: |-
if (!id(alarm_raw_valid)) return {};
return static_cast<float>(id(alarm_raw)[${o} + 10]);
set_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 10] = static_cast<uint8_t>(x);
- script.execute: send_alarm_settings
# Tuya's definition says 10-20, default 15, but this lamp reports 30.
- platform: template
id: alarm_${n}_ring_duration
name: "Alarm ${n} Ring Duration"
unit_of_measurement: "min"
min_value: 10
max_value: 60
step: 5
update_interval: 2s
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 8
lambda: |-
if (!id(alarm_raw_valid)) return {};
return static_cast<float>(id(alarm_raw)[${o} + 4]);
set_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 4] = static_cast<uint8_t>(x);
- script.execute: send_alarm_settings
- platform: template
id: alarm_${n}_radio_station
name: "Alarm ${n} Radio Station"
min_value: 1
max_value: 60
step: 1
update_interval: 2s
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 9
lambda: |-
if (!id(alarm_raw_valid)) return {};
return static_cast<float>(id(alarm_raw)[${o} + 8]);
set_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 8] = static_cast<uint8_t>(x);
- script.execute: send_alarm_settings
- platform: template
id: alarm_${n}_sunrise_duration
name: "Alarm ${n} Sunrise Duration"
unit_of_measurement: "min"
min_value: 10
max_value: 60
step: 10
update_interval: 2s
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 6
lambda: |-
if (!id(alarm_raw_valid)) return {};
return static_cast<float>(id(alarm_raw)[${o} + 9]);
set_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 9] = static_cast<uint8_t>(x);
- script.execute: send_alarm_settings
text_sensor:
- platform: template
id: alarm_${n}_summary
name: "Alarm ${n} Summary"
update_interval: never
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: 7
+27
View File
@@ -0,0 +1,27 @@
###############################################################################
# One weekday switch for one alarm. Included per day from alarm_block.yaml.
# n, o come from alarm_block.yaml; day = label, bit = bit in the weekday mask,
# w = sort position in the web UI.
###############################################################################
switch:
- platform: template
id: alarm_${n}_day_${bit}
name: "Alarm ${n} ${day}"
icon: "mdi:calendar"
web_server:
sorting_group_id: group_alarm_${n}
sorting_weight: ${w}
lambda: |-
if (!id(alarm_raw_valid)) return {};
return (id(alarm_raw)[${o} + 3] >> ${bit}) & 1;
turn_on_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 3] |= (1 << ${bit});
- script.execute: send_alarm_settings
turn_off_action:
- lambda: |-
if (!id(alarm_raw_valid)) return;
id(alarm_raw)[${o} + 3] &= ~(1 << ${bit});
- script.execute: send_alarm_settings
+611
View File
@@ -0,0 +1,611 @@
from logging import getLogger
import math
import re
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components.const import CONF_DATA_BITS, CONF_PARITY, CONF_STOP_BITS
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32H4,
VARIANT_ESP32H21,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
variant_filtered_enum,
)
from esphome.config_helpers import (
filter_source_files_from_defines,
filter_source_files_from_platform,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_AFTER,
CONF_BAUD_RATE,
CONF_BYTES,
CONF_DATA,
CONF_DEBUG,
CONF_DELIMITER,
CONF_DIRECTION,
CONF_DUMMY_RECEIVER,
CONF_DUMMY_RECEIVER_ID,
CONF_FLOW_CONTROL_PIN,
CONF_ID,
CONF_LAMBDA,
CONF_NUMBER,
CONF_PORT,
CONF_RX_BUFFER_SIZE,
CONF_RX_PIN,
CONF_SEQUENCE,
CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_TX_BUFFER_SIZE,
CONF_TX_PIN,
CONF_UART_ID,
PLATFORM_HOST,
PlatformFramework,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.yaml_util import make_data_base
_LOGGER = getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
DOMAIN = "uart"
uart_ns = cg.esphome_ns.namespace("uart")
UARTComponent = uart_ns.class_("UARTComponent")
IDFUARTComponent = uart_ns.class_("IDFUARTComponent", UARTComponent, cg.Component)
ESP8266UartComponent = uart_ns.class_(
"ESP8266UartComponent", UARTComponent, cg.Component
)
RP2UartComponent = uart_ns.class_("RP2UartComponent", UARTComponent, cg.Component)
LibreTinyUARTComponent = uart_ns.class_(
"LibreTinyUARTComponent", UARTComponent, cg.Component
)
HostUartComponent = uart_ns.class_("HostUartComponent", UARTComponent, cg.Component)
NATIVE_UART_CLASSES = (
str(IDFUARTComponent),
str(ESP8266UartComponent),
str(RP2UartComponent),
str(LibreTinyUARTComponent),
)
HOST_BAUD_RATES = [
50,
75,
110,
134,
150,
200,
300,
600,
1200,
1800,
2400,
4800,
9600,
19200,
38400,
57600,
115200,
230400,
460800,
500000,
576000,
921600,
1000000,
1152000,
1500000,
2000000,
2500000,
3000000,
3500000,
4000000,
]
UARTDevice = uart_ns.class_("UARTDevice")
UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action)
UARTDebugger = uart_ns.class_("UARTDebugger", cg.Component, automation.Action)
UARTDummyReceiver = uart_ns.class_("UARTDummyReceiver", cg.Component)
MULTI_CONF = True
MULTI_CONF_NO_DEFAULT = True
def validate_raw_data(value):
if isinstance(value, str):
return value.encode("utf-8")
if isinstance(value, str):
return value
if isinstance(value, list):
return cv.Schema([cv.hex_uint8_t])(value)
raise cv.Invalid(
"data must either be a string wrapped in quotes or a list of bytes"
)
def validate_rx_pin(value):
value = pins.internal_gpio_input_pin_schema(value)
if CORE.is_esp8266 and value[CONF_NUMBER] >= 16:
raise cv.Invalid("Pins GPIO16 and GPIO17 cannot be used as RX pins on ESP8266.")
return value
def validate_host_config(config):
if CORE.is_host:
if CONF_TX_PIN in config or CONF_RX_PIN in config:
raise cv.Invalid(
"TX and RX pins are not supported for UART on host platform."
)
if config[CONF_BAUD_RATE] not in HOST_BAUD_RATES:
raise cv.Invalid(
f"Host platform doesn't support baud rate {config[CONF_BAUD_RATE]}",
path=[CONF_BAUD_RATE],
)
return config
def validate_rx_buffer_size(config):
if CORE.is_esp32:
# ESP32 UART hardware FIFO is 128 bytes (LP UART is 16 bytes, but we use 128 as safe minimum)
# rx_buffer_size must be greater than the hardware FIFO length
min_buffer_size = 128
if config[CONF_RX_BUFFER_SIZE] <= min_buffer_size:
_LOGGER.warning(
"UART rx_buffer_size (%d bytes) is too small and must be greater than the hardware "
"FIFO size (%d bytes). The buffer size will be automatically adjusted at runtime.",
config[CONF_RX_BUFFER_SIZE],
min_buffer_size,
)
return config
def _uart_declare_type(value):
if CORE.is_esp8266:
return cv.declare_id(ESP8266UartComponent)(value)
if CORE.is_esp32:
return cv.declare_id(IDFUARTComponent)(value)
if CORE.is_rp2:
return cv.declare_id(RP2UartComponent)(value)
if CORE.is_libretiny:
return cv.declare_id(LibreTinyUARTComponent)(value)
if CORE.is_host:
return cv.declare_id(HostUartComponent)(value)
raise NotImplementedError
UARTParityOptions = uart_ns.enum("UARTParityOptions")
UART_PARITY_OPTIONS = {
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
"EVEN": UARTParityOptions.UART_CONFIG_PARITY_EVEN,
"ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD,
}
CONF_FLUSH_TIMEOUT = "flush_timeout"
CONF_RX_FULL_THRESHOLD = "rx_full_threshold"
CONF_RX_TIMEOUT = "rx_timeout"
CONF_CLOCK_SOURCE = "clock_source"
UARTClockSource = cg.global_ns.enum("uart_sclk_t")
UART_CLOCK_SOURCES = {
"DEFAULT": UARTClockSource.UART_SCLK_DEFAULT,
"APB": UARTClockSource.UART_SCLK_APB,
"XTAL": UARTClockSource.UART_SCLK_XTAL,
"RTC": UARTClockSource.UART_SCLK_RTC,
"REF_TICK": UARTClockSource.UART_SCLK_REF_TICK,
}
# Keep in sync with SOC_UART_SUPPORT_* in ESP-IDF's per-variant soc_caps.h.
UART_CLOCK_SOURCES_BY_VARIANT = {
VARIANT_ESP32: ["DEFAULT", "APB", "REF_TICK"],
VARIANT_ESP32C2: ["DEFAULT", "XTAL", "RTC"],
VARIANT_ESP32C3: ["DEFAULT", "APB", "XTAL", "RTC"],
VARIANT_ESP32C5: ["DEFAULT", "XTAL", "RTC"],
VARIANT_ESP32C6: ["DEFAULT", "XTAL", "RTC"],
VARIANT_ESP32C61: ["DEFAULT", "XTAL", "RTC"],
VARIANT_ESP32H2: ["DEFAULT", "XTAL", "RTC"],
VARIANT_ESP32H4: ["DEFAULT", "XTAL", "RTC"],
VARIANT_ESP32H21: ["DEFAULT", "XTAL", "RTC"],
VARIANT_ESP32P4: ["DEFAULT", "XTAL", "RTC"],
VARIANT_ESP32S2: ["DEFAULT", "APB", "REF_TICK"],
VARIANT_ESP32S3: ["DEFAULT", "APB", "XTAL", "RTC"],
VARIANT_ESP32S31: ["DEFAULT", "XTAL", "RTC"],
}
UARTDirection = uart_ns.enum("UARTDirection")
UART_DIRECTIONS = {
"RX": UARTDirection.UART_DIRECTION_RX,
"TX": UARTDirection.UART_DIRECTION_TX,
"BOTH": UARTDirection.UART_DIRECTION_BOTH,
}
# The reason for having CONF_BYTES at 150 by default:
#
# The log message buffer size is 512 bytes by default. About 35 bytes are
# used for the log prefix. That leaves us with 477 bytes for logging data.
# The default log output is hex, which uses 3 characters per represented
# byte (2 hex chars + 1 separator). That means that 477 / 3 = 159 bytes
# can be represented in a single log line. Using 150, because people love
# round numbers.
AFTER_DEFAULTS = {CONF_BYTES: 150, CONF_TIMEOUT: "100ms"}
CONF_DEBUG_PREFIX = "debug_prefix"
# By default, log in hex format when no specific sequence is provided.
DEFAULT_DEBUG_OUTPUT = "UARTDebug::log_hex(direction, bytes, ':', debug_prefix);"
DEFAULT_SEQUENCE = [{CONF_LAMBDA: make_data_base(DEFAULT_DEBUG_OUTPUT)}]
def maybe_empty_debug(value):
if value is None:
value = {}
return DEBUG_SCHEMA(value)
def validate_port(value):
if not re.match(r"^/(?:[^/]+/)[^/]+$", value):
raise cv.Invalid("Port must be a valid device path")
return value
DEBUG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UARTDebugger),
cv.Optional(CONF_DIRECTION, default="BOTH"): cv.enum(
UART_DIRECTIONS, upper=True
),
cv.Optional(CONF_AFTER, default=AFTER_DEFAULTS): cv.Schema(
{
cv.Optional(
CONF_BYTES, default=AFTER_DEFAULTS[CONF_BYTES]
): cv.validate_bytes,
cv.Optional(
CONF_TIMEOUT, default=AFTER_DEFAULTS[CONF_TIMEOUT]
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_DELIMITER): cv.templatable(validate_raw_data),
}
),
cv.Optional(
CONF_SEQUENCE, default=DEFAULT_SEQUENCE
): automation.validate_automation(),
cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean,
cv.GenerateID(CONF_DUMMY_RECEIVER_ID): cv.declare_id(UARTDummyReceiver),
cv.Optional(CONF_DEBUG_PREFIX, default=""): cv.string,
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): _uart_declare_type,
cv.Required(CONF_BAUD_RATE): cv.int_range(min=1),
cv.Optional(CONF_TX_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_RX_PIN): validate_rx_pin,
cv.Optional(CONF_FLOW_CONTROL_PIN): cv.All(
cv.only_on_esp32, pins.internal_gpio_output_pin_schema
),
cv.Optional(CONF_PORT): cv.All(validate_port, cv.only_on(PLATFORM_HOST)),
cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes,
cv.Optional(CONF_TX_BUFFER_SIZE): cv.All(
cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=129)
),
cv.Optional(CONF_RX_FULL_THRESHOLD): cv.All(
cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=1, max=120)
),
cv.SplitDefault(CONF_RX_TIMEOUT, esp32=2): cv.All(
cv.only_on_esp32, cv.validate_bytes, cv.int_range(min=0, max=92)
),
cv.Optional(CONF_FLUSH_TIMEOUT): cv.All(
cv.only_on_esp32, cv.positive_time_period_milliseconds
),
cv.Optional(CONF_CLOCK_SOURCE): cv.All(
cv.only_on_esp32,
variant_filtered_enum(UART_CLOCK_SOURCES_BY_VARIANT, upper=True),
),
cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True),
cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8),
cv.Optional(CONF_PARITY, default="NONE"): cv.enum(
UART_PARITY_OPTIONS, upper=True
),
cv.Optional(CONF_DEBUG): maybe_empty_debug,
}
).extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT),
validate_host_config,
validate_rx_buffer_size,
)
async def debug_to_code(config, parent):
trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], parent)
await cg.register_component(trigger, config)
for action in config[CONF_SEQUENCE]:
await automation.build_automation(
trigger,
[
(UARTDirection, "direction"),
(cg.std_vector.template(cg.uint8), "bytes"),
(cg.StringRef, "debug_prefix"),
],
action,
)
cg.add(trigger.set_direction(config[CONF_DIRECTION]))
after = config[CONF_AFTER]
cg.add(trigger.set_after_bytes(after[CONF_BYTES]))
cg.add(trigger.set_after_timeout(after[CONF_TIMEOUT]))
if CONF_DELIMITER in after:
data = after[CONF_DELIMITER]
if isinstance(data, bytes):
data = list(data)
for byte in after[CONF_DELIMITER]:
cg.add(trigger.add_delimiter_byte(byte))
if config[CONF_DUMMY_RECEIVER]:
dummy = cg.new_Pvariable(config[CONF_DUMMY_RECEIVER_ID], parent)
await cg.register_component(dummy, {})
if debug_prefix := config[CONF_DEBUG_PREFIX]:
cg.add(trigger.set_debug_prefix(debug_prefix))
cg.add_define("USE_UART_DEBUGGER")
async def to_code(config):
cg.add_global(uart_ns.using)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add(var.set_baud_rate(config[CONF_BAUD_RATE]))
if CONF_TX_PIN in config:
tx_pin = await cg.gpio_pin_expression(config[CONF_TX_PIN])
cg.add(var.set_tx_pin(tx_pin))
if CONF_RX_PIN in config:
rx_pin = await cg.gpio_pin_expression(config[CONF_RX_PIN])
cg.add(var.set_rx_pin(rx_pin))
if CONF_FLOW_CONTROL_PIN in config:
flow_control_pin = await cg.gpio_pin_expression(config[CONF_FLOW_CONTROL_PIN])
cg.add(var.set_flow_control_pin(flow_control_pin))
if CONF_PORT in config:
cg.add(var.set_name(config[CONF_PORT]))
cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE]))
if CORE.is_esp32:
if CONF_RX_FULL_THRESHOLD not in config:
# Calculate rx_full_threshold to be 10ms
bytelength = config[CONF_DATA_BITS] + config[CONF_STOP_BITS] + 1
if config[CONF_PARITY] != "NONE":
bytelength += 1
config[CONF_RX_FULL_THRESHOLD] = max(
1,
min(
120,
math.floor((config[CONF_BAUD_RATE] / (bytelength * 1000 / 10)) - 1),
),
)
cg.add(var.set_rx_full_threshold(config[CONF_RX_FULL_THRESHOLD]))
cg.add(var.set_rx_timeout(config[CONF_RX_TIMEOUT]))
if CONF_FLUSH_TIMEOUT in config:
cg.add(var.set_flush_timeout(config[CONF_FLUSH_TIMEOUT]))
if (tx_buffer_size := config.get(CONF_TX_BUFFER_SIZE)) is not None:
cg.add(var.set_tx_buffer_size(tx_buffer_size))
# The member already defaults to UART_SCLK_DEFAULT, so only emit a real choice
if (clock_source := config.get(CONF_CLOCK_SOURCE, "DEFAULT")) != "DEFAULT":
cg.add(var.set_clock_source(UART_CLOCK_SOURCES[clock_source]))
cg.add(var.set_stop_bits(config[CONF_STOP_BITS]))
cg.add(var.set_data_bits(config[CONF_DATA_BITS]))
# Skip the setter when the config matches the C++ initializer (UART_CONFIG_PARITY_NONE).
if (parity := config[CONF_PARITY]) != "NONE":
cg.add(var.set_parity(parity))
if CONF_DEBUG in config:
await debug_to_code(config[CONF_DEBUG], var)
# ESP8266: Enable the Arduino Serial objects that might be used based on pin config
# The C++ code selects hardware serial at runtime based on these pin combinations:
# - Serial (UART0): TX=1 or null, RX=3 or null
# - Serial (UART0 swap): TX=15 or null, RX=13 or null
# - Serial1: TX=2 or null, RX=8 or null
if CORE.is_esp8266:
from esphome.components.esp8266.const import enable_serial, enable_serial1
tx_num = config[CONF_TX_PIN][CONF_NUMBER] if CONF_TX_PIN in config else None
rx_num = config[CONF_RX_PIN][CONF_NUMBER] if CONF_RX_PIN in config else None
# Check if this config could use Serial (UART0 regular or swap)
if (tx_num is None or tx_num in (1, 15)) and (
rx_num is None or rx_num in (3, 13)
):
enable_serial()
cg.add_define("USE_ESP8266_UART_SERIAL")
# Check if this config could use Serial1
if (tx_num is None or tx_num == 2) and (rx_num is None or rx_num == 8):
enable_serial1()
cg.add_define("USE_ESP8266_UART_SERIAL1")
CORE.add_job(final_step)
# A schema to use for all UART devices, all UART integrations must extend this!
UART_DEVICE_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_UART_ID): cv.use_id(UARTComponent),
}
)
KEY_UART_DEVICES = "uart_devices"
def final_validate_device_schema(
name: str,
*,
uart_bus: str = CONF_UART_ID,
baud_rate: int | None = None,
require_tx: bool = False,
require_rx: bool = False,
data_bits: int | None = None,
parity: str | None = None,
stop_bits: int | None = None,
):
def validate_baud_rate(value):
if value != baud_rate:
raise cv.Invalid(
f"Component {name} requires baud rate {baud_rate} for the uart referenced by {uart_bus}"
)
return value
def validate_pin(opt, device):
def validator(value):
if opt in device and not CORE.testing_mode:
raise cv.Invalid(
f"The uart {opt} is used both by {name} and {device[opt]}, "
f"but can only be used by one. Please create a new uart bus for {name}."
)
device[opt] = name
return value
return validator
def validate_data_bits(value):
if value != data_bits:
raise cv.Invalid(
f"Component {name} requires {data_bits} data bits for the uart referenced by {uart_bus}"
)
return value
def validate_parity(value):
if value != parity:
raise cv.Invalid(
f"Component {name} requires parity {parity} for the uart referenced by {uart_bus}"
)
return value
def validate_stop_bits(value):
# usb_uart channels store stop bits as strings ("1", "1.5", "2").
if float(value) != stop_bits:
raise cv.Invalid(
f"Component {name} requires {stop_bits} stop bits for the uart referenced by {uart_bus}"
)
return value
def validate_hub(hub_config):
hub_schema = {}
uart_id = hub_config[CONF_ID]
uart_id_type_str = str(uart_id.type)
devices = fv.full_config.get().data.setdefault(KEY_UART_DEVICES, {})
device = devices.setdefault(uart_id, {})
if require_tx and uart_id_type_str in NATIVE_UART_CLASSES:
hub_schema[
cv.Required(
CONF_TX_PIN,
msg=f"Component {name} requires uart referenced by {uart_bus} to declare a tx_pin",
)
] = validate_pin(CONF_TX_PIN, device)
if require_rx and uart_id_type_str in NATIVE_UART_CLASSES:
hub_schema[
cv.Required(
CONF_RX_PIN,
msg=f"Component {name} requires uart referenced by {uart_bus} to declare a rx_pin",
)
] = validate_pin(CONF_RX_PIN, device)
if baud_rate is not None:
hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate
if data_bits is not None:
hub_schema[cv.Required(CONF_DATA_BITS)] = validate_data_bits
if parity is not None:
hub_schema[cv.Required(CONF_PARITY)] = validate_parity
if stop_bits is not None:
hub_schema[cv.Required(CONF_STOP_BITS)] = validate_stop_bits
return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config)
return cv.Schema(
{cv.Required(uart_bus): fv.id_declaration_match_schema(validate_hub)},
extra=cv.ALLOW_EXTRA,
)
async def register_uart_device(var, config):
"""Register a UART device, setting up all the internal values.
This is a coroutine, you need to await it with an 'await' expression!
"""
parent = await cg.get_variable(config[CONF_UART_ID])
cg.add(var.set_uart_parent(parent))
@automation.register_action(
"uart.write",
UARTWriteAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(UARTComponent),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
},
key=CONF_DATA,
),
synchronous=True,
)
async def uart_write_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_DATA]
if isinstance(data, bytes):
data = list(data)
if cg.is_template(data):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
return var
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure optional UART features."""
if (CORE.is_esp32 or CORE.is_esp8266) and CORE.has_networking:
# Wake-on-RX is essentially free (just an ISR function pointer
# registration on ESP32, an inline flag set on ESP8266 software
# serial) — enable by default to reduce RX buffer overflow risk by
# waking the main loop immediately when data arrives.
cg.add_define("USE_UART_WAKE_LOOP_ON_RX")
_platform_filter = filter_source_files_from_platform(
{
"uart_component_esp_idf.cpp": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
"uart_component_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"uart_component_host.cpp": {PlatformFramework.HOST_NATIVE},
"uart_component_rp2.cpp": {PlatformFramework.RP2_ARDUINO},
"uart_component_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)
# uart_debugger.cpp is fully #ifdef'd on USE_UART_DEBUGGER, set only when a
# debug block is configured.
_define_filter = filter_source_files_from_defines(
{"uart_debugger.cpp": "USE_UART_DEBUGGER"}
)
def FILTER_SOURCE_FILES() -> list[str]:
return _platform_filter() + _define_filter()
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "uart.h"
#include "esphome/core/automation.h"
#include <vector>
namespace esphome::uart {
template<typename... Ts> class UARTWriteAction final : public Action<Ts...>, public Parented<UARTComponent> {
public:
void set_data_template(std::vector<uint8_t> (*func)(Ts...)) {
// Stateless lambdas (generated by ESPHome) implicitly convert to function pointers
this->code_.func = func;
this->len_ = -1; // Sentinel value indicates template mode
}
// Store pointer to static data in flash (no RAM copy)
void set_data_static(const uint8_t *data, size_t len) {
this->code_.data = data;
this->len_ = len; // Length >= 0 indicates static mode
}
void play(const Ts &...x) override {
if (this->len_ >= 0) {
// Static mode: use pointer and length
this->parent_->write_array(this->code_.data, static_cast<size_t>(this->len_));
} else {
// Template mode: call function
auto val = this->code_.func(x...);
this->parent_->write_array(val);
}
}
protected:
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
union Code {
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
const uint8_t *data; // Pointer to static data in flash
} code_;
};
} // namespace esphome::uart
+36
View File
@@ -0,0 +1,36 @@
import esphome.codegen as cg
from esphome.components import button, uart
import esphome.config_validation as cv
from esphome.const import CONF_DATA
from esphome.core import HexInt
from .. import uart_ns, validate_raw_data
CODEOWNERS = ["@ssieb"]
DEPENDENCIES = ["uart"]
UARTButton = uart_ns.class_("UARTButton", button.Button, uart.UARTDevice, cg.Component)
CONFIG_SCHEMA = (
button.button_schema(UARTButton)
.extend(
{
cv.Required(CONF_DATA): validate_raw_data,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = await button.new_button(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
data = config[CONF_DATA]
if isinstance(data, bytes):
data = [HexInt(x) for x in data]
cg.add(var.set_data(cg.ArrayInitializer(*data)))
+15
View File
@@ -0,0 +1,15 @@
#include "uart_button.h"
#include "esphome/core/log.h"
namespace esphome::uart {
static const char *const TAG = "uart.button";
void UARTButton::press_action() {
ESP_LOGD(TAG, "'%s': Sending data", this->get_name().c_str());
this->write_array(this->data_.data(), this->data_.size());
}
void UARTButton::dump_config() { LOG_BUTTON("", "UART Button", this); }
} // namespace esphome::uart
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/button/button.h"
#include <vector>
namespace esphome::uart {
class UARTButton final : public button::Button, public UARTDevice, public Component {
public:
void set_data(std::vector<uint8_t> &&data) { this->data_ = std::move(data); }
void set_data(std::initializer_list<uint8_t> data) { this->data_ = std::vector<uint8_t>(data); }
void dump_config() override;
protected:
void press_action() override;
std::vector<uint8_t> data_;
};
} // namespace esphome::uart
+90
View File
@@ -0,0 +1,90 @@
import esphome.codegen as cg
from esphome.components import event, uart
import esphome.config_validation as cv
from esphome.const import CONF_EVENT_TYPES, CONF_ID
from esphome.core import ID
from esphome.types import ConfigType
from .. import uart_ns
CODEOWNERS = ["@eoasmxd"]
DEPENDENCIES = ["uart"]
UARTEvent = uart_ns.class_("UARTEvent", event.Event, uart.UARTDevice, cg.Component)
def validate_event_types(value) -> list[tuple[str, str | list[int]]]:
if not isinstance(value, list):
raise cv.Invalid("Event type must be a list of key-value mappings.")
processed: list[tuple[str, str | list[int]]] = []
for item in value:
if not isinstance(item, dict):
raise cv.Invalid(f"Event type item must be a mapping (dictionary): {item}")
if len(item) != 1:
raise cv.Invalid(
f"Event type item must be a single key-value mapping: {item}"
)
# Get the single key-value pair
event_name, match_data = next(iter(item.items()))
if not isinstance(event_name, str):
raise cv.Invalid(f"Event name (key) must be a string: {event_name}")
try:
# Try to validate as list of hex bytes
match_data_bin = cv.ensure_list(cv.hex_uint8_t)(match_data)
processed.append((event_name, match_data_bin))
continue
except cv.Invalid:
pass # Not binary, try string
try:
# Try to validate as string
match_data_str = cv.string_strict(match_data)
processed.append((event_name, match_data_str))
continue
except cv.Invalid:
pass # Not string either
# If neither validation passed
raise cv.Invalid(
f"Event match data for '{event_name}' must be a string or a list of hex bytes. Invalid data: {match_data}"
)
if not processed:
raise cv.Invalid("event_types must contain at least one event mapping.")
return processed
CONFIG_SCHEMA = (
event.event_schema(UARTEvent)
.extend(
{
cv.Required(CONF_EVENT_TYPES): validate_event_types,
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config: ConfigType) -> None:
event_names = [item[0] for item in config[CONF_EVENT_TYPES]]
var = await event.new_event(config, event_types=event_names)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for i, (event_name, match_data) in enumerate(config[CONF_EVENT_TYPES]):
if isinstance(match_data, str):
match_data = [ord(c) for c in match_data]
match_data_var_id = ID(
f"match_data_{config[CONF_ID]}_{i}", is_declaration=True, type=cg.uint8
)
match_data_var = cg.static_const_array(
match_data_var_id, cg.ArrayInitializer(*match_data)
)
cg.add(var.add_event_matcher(event_name, match_data_var, len(match_data)))
+48
View File
@@ -0,0 +1,48 @@
#include "uart_event.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome::uart {
static const char *const TAG = "uart.event";
void UARTEvent::setup() {}
void UARTEvent::dump_config() { LOG_EVENT("", "UART Event", this); }
void UARTEvent::loop() { this->read_data_(); }
void UARTEvent::add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len) {
this->matchers_.push_back({event_name, match_data, match_data_len});
if (match_data_len > this->max_matcher_len_) {
this->max_matcher_len_ = match_data_len;
}
}
void UARTEvent::read_data_() {
while (this->available()) {
uint8_t data;
this->read_byte(&data);
this->buffer_.push_back(data);
bool match_found = false;
for (const auto &matcher : this->matchers_) {
if (this->buffer_.size() < matcher.data_len) {
continue;
}
if (std::equal(matcher.data, matcher.data + matcher.data_len, this->buffer_.end() - matcher.data_len)) {
this->trigger(matcher.event_name);
this->buffer_.clear();
match_found = true;
break;
}
}
if (!match_found && this->max_matcher_len_ > 0 && this->buffer_.size() > this->max_matcher_len_) {
this->buffer_.erase(this->buffer_.begin());
}
}
}
} // namespace esphome::uart
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/event/event.h"
#include "esphome/components/uart/uart.h"
#include <vector>
namespace esphome::uart {
class UARTEvent final : public event::Event, public UARTDevice, public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
void add_event_matcher(const char *event_name, const uint8_t *match_data, size_t match_data_len);
protected:
struct EventMatcher {
const char *event_name;
const uint8_t *data;
size_t data_len;
};
void read_data_();
std::vector<EventMatcher> matchers_;
std::vector<uint8_t> buffer_;
size_t max_matcher_len_ = 0;
};
} // namespace esphome::uart
@@ -0,0 +1,20 @@
from esphome.components.packet_transport import (
PacketTransport,
new_packet_transport,
transport_schema,
)
from esphome.cpp_types import PollingComponent
from .. import UART_DEVICE_SCHEMA, register_uart_device, uart_ns
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["uart"]
UARTTransport = uart_ns.class_("UARTTransport", PacketTransport, PollingComponent)
CONFIG_SCHEMA = transport_schema(UARTTransport).extend(UART_DEVICE_SCHEMA)
async def to_code(config):
var, _ = await new_packet_transport(config)
await register_uart_device(var, config)
@@ -0,0 +1,81 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include "uart_transport.h"
namespace esphome::uart {
static const char *const TAG = "uart_transport";
void UARTTransport::loop() {
PacketTransport::loop();
while (this->parent_->available()) {
uint8_t byte;
if (!this->parent_->read_byte(&byte)) {
ESP_LOGW(TAG, "Failed to read byte from UART");
return;
}
if (byte == FLAG_BYTE) {
if (this->rx_started_ && this->receive_buffer_.size() > 6) {
auto len = this->receive_buffer_.size();
auto crc = crc16(this->receive_buffer_.data(), len - 2);
if (crc != (this->receive_buffer_[len - 2] | (this->receive_buffer_[len - 1] << 8))) {
ESP_LOGD(TAG, "CRC mismatch, discarding packet");
this->rx_started_ = false;
this->receive_buffer_.clear();
continue;
}
this->receive_buffer_.resize(len - 2);
this->process_(this->receive_buffer_);
this->rx_started_ = false;
} else {
this->rx_started_ = true;
}
this->receive_buffer_.clear();
this->rx_control_ = false;
continue;
}
if (!this->rx_started_)
continue;
if (byte == CONTROL_BYTE) {
this->rx_control_ = true;
continue;
}
if (this->rx_control_) {
byte ^= 0x20;
this->rx_control_ = false;
}
if (this->receive_buffer_.size() == MAX_PACKET_SIZE) {
ESP_LOGD(TAG, "Packet too large, discarding");
this->rx_started_ = false;
this->receive_buffer_.clear();
continue;
}
this->receive_buffer_.push_back(byte);
}
}
/**
* Write a byte to the UART bus. If the byte is a flag or control byte, it will be escaped.
* @param byte The byte to write.
*/
void UARTTransport::write_byte_(uint8_t byte) const {
if (byte == FLAG_BYTE || byte == CONTROL_BYTE) {
this->parent_->write_byte(CONTROL_BYTE);
byte ^= 0x20;
}
this->parent_->write_byte(byte);
}
void UARTTransport::send_packet(const std::vector<uint8_t> &buf) const {
this->parent_->write_byte(FLAG_BYTE);
for (uint8_t byte : buf) {
this->write_byte_(byte);
}
auto crc = crc16(buf.data(), buf.size());
this->write_byte_(crc & 0xFF);
this->write_byte_(crc >> 8);
this->parent_->write_byte(FLAG_BYTE);
}
} // namespace esphome::uart
@@ -0,0 +1,38 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/packet_transport/packet_transport.h"
#include <vector>
#include "../uart.h"
namespace esphome::uart {
/**
* A transport protocol for sending and receiving packets over a UART connection.
* The protocol is based on Asynchronous HDLC framing. (https://en.wikipedia.org/wiki/High-Level_Data_Link_Control)
* There are two special bytes: FLAG_BYTE and CONTROL_BYTE.
* A 16-bit CRC is appended to the packet, then
* the protocol wraps the resulting data between FLAG_BYTEs.
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
* XORed with 0x20.
*/
static const uint16_t MAX_PACKET_SIZE = 508;
static const uint8_t FLAG_BYTE = 0x7E;
static const uint8_t CONTROL_BYTE = 0x7D;
class UARTTransport final : public packet_transport::PacketTransport, public UARTDevice {
public:
void loop() override;
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
protected:
void write_byte_(uint8_t byte) const;
void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override { return true; };
size_t get_max_packet_size() override { return MAX_PACKET_SIZE; }
std::vector<uint8_t> receive_buffer_{};
bool rx_started_{};
bool rx_control_{};
};
} // namespace esphome::uart
@@ -0,0 +1,179 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esphome/core/helpers.h"
namespace esphome::uart {
/// Software serial RX decoder: on_edge() counts bits from the time between edges;
/// finalize() completes a byte whose idle high tail has no closing edge.
/// Times are in any steady tick unit.
class SoftwareSerialRxDecoder {
public:
static constexpr uint8_t RX_IDLE = 0xFF;
/// Configure framing and buffer; drops all state. Follow with reset().
void setup(uint32_t bit_ticks, uint8_t data_bits, bool parity, uint8_t stop_bits, uint8_t *buffer,
size_t buffer_size) {
this->bit_ticks_ = bit_ticks;
this->data_bits_ = data_bits;
this->stop_bit_ = data_bits + (parity ? 1 : 0);
this->max_run_ticks_ = bit_ticks * (this->stop_bit_ + stop_bits + 2);
this->buffer_ = buffer;
this->buffer_size_ = buffer_size;
this->in_pos_ = 0;
this->out_pos_ = 0;
this->reset(0, true);
}
/// Forget any partial frame; `level` is the current line level.
void reset(uint32_t now, bool level) {
this->bit_ = RX_IDLE;
this->cur_byte_ = 0;
this->last_level_ = level;
this->last_edge_ = now;
}
/// ISR. True to wake the loop: a byte was pushed, or the line went high in an
/// open frame (a data 1 and the idle tail look alike) so finalize() may be needed.
bool ESPHOME_ALWAYS_INLINE on_edge(uint32_t now, bool level) {
const bool last_level = this->last_level_;
// Collapsed edge
if (level == last_level)
return false;
// LX106 has no divider, so count
uint32_t delta = now - this->last_edge_;
if (delta > this->max_run_ticks_)
delta = this->max_run_ticks_;
delta += this->bit_ticks_ / 2;
uint32_t bits = 0;
while (delta >= this->bit_ticks_) {
delta -= this->bit_ticks_;
bits++;
}
const bool pushed = this->consume_run_(bits, last_level);
this->last_edge_ = now;
this->last_level_ = level;
return pushed || (level && this->bit_ != RX_IDLE);
}
bool pending() const { return this->bit_ != RX_IDLE && this->last_level_; }
bool finalize_due(uint32_t now) const {
const uint8_t bit = this->bit_;
if (bit == RX_IDLE)
return false;
return now - this->last_edge_ >= this->tail_ticks_(bit);
}
/// Call with the ISR masked.
void finalize(uint32_t now) {
if (!this->last_level_ || !this->finalize_due(now))
return;
// Remaining bits are all ones
const uint8_t bit = this->bit_;
uint8_t cur = this->cur_byte_;
if (bit < this->data_bits_)
cur |= ((1U << this->data_bits_) - 1) & ~((1U << bit) - 1);
this->push_byte(cur);
this->bit_ = RX_IDLE;
}
/// Drops the byte when full; holds buffer_size - 1 bytes.
bool ESPHOME_ALWAYS_INLINE push_byte(uint8_t data) {
size_t in = this->in_pos_;
size_t next = in + 1;
if (next == this->buffer_size_)
next = 0;
if (next == this->out_pos_)
return false;
this->buffer_[in] = data;
this->in_pos_ = next;
return true;
}
uint8_t *buffer() const { return this->buffer_; }
size_t buffer_size() const { return this->buffer_size_; }
size_t available() const {
// Read volatile in_pos_ once to avoid TOCTOU race with ISR.
size_t in = this->in_pos_;
if (in >= this->out_pos_)
return in - this->out_pos_;
return this->buffer_size_ - this->out_pos_ + in;
}
uint8_t peek_byte() const {
if (this->in_pos_ == this->out_pos_)
return 0;
return this->buffer_[this->out_pos_];
}
uint8_t read_byte() {
if (this->in_pos_ == this->out_pos_)
return 0;
uint8_t data = this->buffer_[this->out_pos_];
size_t next = this->out_pos_ + 1;
this->out_pos_ = next == this->buffer_size_ ? 0 : next;
return data;
}
protected:
uint32_t tail_ticks_(uint8_t bit) const {
return (this->stop_bit_ + 1 - bit) * this->bit_ticks_ + this->bit_ticks_ / 2;
}
bool ESPHOME_ALWAYS_INLINE consume_run_(uint32_t bits, bool level) {
uint8_t bit = this->bit_;
uint8_t cur = this->cur_byte_;
bool pushed = false;
while (bits > 0) {
if (bit == RX_IDLE) {
if (level)
break;
// Start bit
bit = 0;
cur = 0;
bits--;
} else if (bit < this->data_bits_) {
uint8_t n = this->data_bits_ - bit;
if (n > bits)
n = bits;
if (level)
cur |= ((1U << n) - 1) << bit;
bit += n;
bits -= n;
} else if (bit < this->stop_bit_) {
// Parity, not checked
bit++;
bits--;
} else {
// Stop bit; low is a framing error
if (level)
pushed = this->push_byte(cur);
bit = RX_IDLE;
break;
}
}
this->bit_ = bit;
this->cur_byte_ = cur;
return pushed;
}
// Members ordered largest to smallest to minimize padding
uint32_t bit_ticks_{0};
uint32_t max_run_ticks_{0};
volatile uint32_t last_edge_{0};
uint8_t *buffer_{nullptr};
size_t buffer_size_{0};
volatile size_t in_pos_{0};
size_t out_pos_{0}; // reader only
/// Next frame bit (data, parity, then stop at stop_bit_) or RX_IDLE.
volatile uint8_t bit_{RX_IDLE};
volatile uint8_t cur_byte_{0};
volatile bool last_level_{true};
uint8_t data_bits_{8};
uint8_t stop_bit_{8};
};
} // namespace esphome::uart
+59
View File
@@ -0,0 +1,59 @@
import esphome.codegen as cg
from esphome.components import switch, uart
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_SEND_EVERY
from esphome.core import HexInt
from .. import uart_ns, validate_raw_data
DEPENDENCIES = ["uart"]
UARTSwitch = uart_ns.class_("UARTSwitch", switch.Switch, uart.UARTDevice, cg.Component)
CONF_TURN_OFF = "turn_off"
CONF_TURN_ON = "turn_on"
CONFIG_SCHEMA = (
switch.switch_schema(UARTSwitch, block_inverted=True)
.extend(
{
cv.Required(CONF_DATA): cv.Any(
validate_raw_data,
cv.Schema(
{
cv.Optional(CONF_TURN_OFF): validate_raw_data,
cv.Optional(CONF_TURN_ON): validate_raw_data,
}
),
),
cv.Optional(CONF_SEND_EVERY): cv.positive_time_period_milliseconds,
},
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = await switch.new_switch(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
data = config[CONF_DATA]
if isinstance(data, dict):
if data_on := data.get(CONF_TURN_ON):
if isinstance(data_on, bytes):
data_on = [HexInt(x) for x in data_on]
cg.add(var.set_data_on(cg.ArrayInitializer(*data_on)))
if data_off := data.get(CONF_TURN_OFF):
if isinstance(data_off, bytes):
data_off = [HexInt(x) for x in data_off]
cg.add(var.set_data_off(cg.ArrayInitializer(*data_off)))
else:
data = config[CONF_DATA]
if isinstance(data, bytes):
data = [HexInt(x) for x in data]
cg.add(var.set_data_on(cg.ArrayInitializer(*data)))
cg.add(var.set_single_state(True))
if CONF_SEND_EVERY in config:
cg.add(var.set_send_every(config[CONF_SEND_EVERY]))
+60
View File
@@ -0,0 +1,60 @@
#include "uart_switch.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::uart {
static const char *const TAG = "uart.switch";
void UARTSwitch::loop() {
if (this->send_every_) {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ > this->send_every_) {
this->write_command_(this->state);
this->last_transmission_ = now;
}
}
}
void UARTSwitch::write_command_(bool state) {
if (state && !this->data_on_.empty()) {
ESP_LOGD(TAG, "'%s': Sending on data", this->get_name().c_str());
this->write_array(this->data_on_.data(), this->data_on_.size());
}
if (!state && !this->data_off_.empty()) {
ESP_LOGD(TAG, "'%s': Sending off data", this->get_name().c_str());
this->write_array(this->data_off_.data(), this->data_off_.size());
}
}
void UARTSwitch::write_state(bool state) {
if (!this->single_state_) {
this->publish_state(state);
this->write_command_(state);
this->last_transmission_ = millis();
return;
}
if (!state) {
this->publish_state(false);
return;
}
this->publish_state(true);
this->write_command_(true);
if (this->send_every_ == 0) {
this->publish_state(false);
} else {
this->last_transmission_ = millis();
}
}
void UARTSwitch::dump_config() {
LOG_SWITCH("", "UART Switch", this);
if (this->send_every_) {
ESP_LOGCONFIG(TAG, " Send Every: %" PRIu32, this->send_every_);
}
}
} // namespace esphome::uart
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/switch/switch.h"
#include <cinttypes>
#include <vector>
namespace esphome::uart {
class UARTSwitch final : public switch_::Switch, public UARTDevice, public Component {
public:
void loop() override;
void set_data_on(std::vector<uint8_t> &&data) { this->data_on_ = std::move(data); }
void set_data_on(std::initializer_list<uint8_t> data) { this->data_on_ = std::vector<uint8_t>(data); }
void set_data_off(std::vector<uint8_t> &&data) { this->data_off_ = std::move(data); }
void set_data_off(std::initializer_list<uint8_t> data) { this->data_off_ = std::vector<uint8_t>(data); }
void set_send_every(uint32_t send_every) { this->send_every_ = send_every; }
void set_single_state(bool single) { this->single_state_ = single; }
void dump_config() override;
protected:
void write_command_(bool state);
void write_state(bool state) override;
std::vector<uint8_t> data_on_;
std::vector<uint8_t> data_off_;
bool single_state_{false};
uint32_t send_every_;
uint32_t last_transmission_;
};
} // namespace esphome::uart
+40
View File
@@ -0,0 +1,40 @@
#include "uart.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <cinttypes>
namespace esphome::uart {
static const char *const TAG = "uart";
// UART parity strings indexed by UARTParityOptions enum (0-4): NONE, EVEN, ODD, MARK, SPACE
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "MARK", "SPACE", "UNKNOWN");
void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity,
uint8_t data_bits) {
if (this->parent_->get_baud_rate() != baud_rate) {
ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %" PRIu32 " but you have %" PRIu32 "!",
baud_rate, this->parent_->get_baud_rate());
}
if (this->parent_->get_stop_bits() != stop_bits) {
ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits,
this->parent_->get_stop_bits());
}
if (this->parent_->get_data_bits() != data_bits) {
ESP_LOGE(TAG, " Invalid number of data bits: Integration requested %u data bits but you have %u!", data_bits,
this->parent_->get_data_bits());
}
if (this->parent_->get_parity() != parity) {
ESP_LOGE(TAG, " Invalid parity: Integration requested parity %s but you have %s!",
LOG_STR_ARG(parity_to_str(parity)), LOG_STR_ARG(parity_to_str(this->parent_->get_parity())));
}
}
const LogString *parity_to_str(UARTParityOptions parity) {
return UARTParityStrings::get_log_str(static_cast<uint8_t>(parity), UARTParityStrings::LAST_INDEX);
}
} // namespace esphome::uart
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "uart_component.h"
namespace esphome::uart {
class UARTDevice {
public:
UARTDevice() = default;
UARTDevice(UARTComponent *parent) : parent_(parent) {}
void set_uart_parent(UARTComponent *parent) { this->parent_ = parent; }
void write_byte(uint8_t data) { this->parent_->write_byte(data); }
void set_rx_full_threshold(size_t rx_full_threshold) { this->parent_->set_rx_full_threshold(rx_full_threshold); }
void set_rx_full_threshold_ms(size_t time) { this->parent_->set_rx_full_threshold_ms(time); }
size_t get_rx_full_threshold() { return this->parent_->get_rx_full_threshold(); }
void set_rx_timeout(size_t rx_timeout) { this->parent_->set_rx_timeout(rx_timeout); }
size_t get_rx_timeout() { return this->parent_->get_rx_timeout(); }
void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); }
void write_array(const std::vector<uint8_t> &data) { this->parent_->write_array(data); }
template<size_t N> void write_array(const std::array<uint8_t, N> &data) {
this->parent_->write_array(data.data(), data.size());
}
void write_str(const char *str) { this->parent_->write_str(str); }
bool read_byte(uint8_t *data) { return this->parent_->read_byte(data); }
bool peek_byte(uint8_t *data) { return this->parent_->peek_byte(data); }
bool read_array(uint8_t *data, size_t len) { return this->parent_->read_array(data, len); }
template<size_t N> optional<std::array<uint8_t, N>> read_array() { // NOLINT
// One object returned on both paths; a separate empty optional trips GCC's -Wmaybe-uninitialized.
optional<std::array<uint8_t, N>> res(std::in_place);
if (!this->read_array(res->data(), N)) {
res.reset();
}
return res;
}
size_t available() { return this->parent_->available(); }
UARTFlushResult flush() { return this->parent_->flush(); }
// Compat APIs
int read() {
uint8_t data;
if (!this->read_byte(&data))
return -1;
return data;
}
size_t write(uint8_t data) {
this->write_byte(data);
return 1;
}
int peek() {
uint8_t data;
if (!this->peek_byte(&data))
return -1;
return data;
}
/// Check that the configuration of the UART bus matches the provided values and otherwise print a warning
ESPDEPRECATED("Use uart.final_validate_device_schema() in Python instead. Removed in 2027.3.0", "2026.9.0")
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1,
UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8);
protected:
UARTComponent *parent_{nullptr};
};
} // namespace esphome::uart
+30
View File
@@ -0,0 +1,30 @@
#include "uart_component.h"
namespace esphome::uart {
static const char *const TAG = "uart";
bool UARTComponent::check_read_timeout_(size_t len) {
if (this->available() >= len)
return true;
uint32_t start_time = millis();
while (this->available() < len) {
if (millis() - start_time > 100) {
ESP_LOGE(TAG, "Reading from UART timed out at byte %zu!", this->available());
return false;
}
yield();
}
return true;
}
void UARTComponent::set_rx_full_threshold_ms(uint8_t time) {
uint8_t bytelength = this->data_bits_ + this->stop_bits_ + 1;
if (this->parity_ != UARTParityOptions::UART_CONFIG_PARITY_NONE)
bytelength += 1;
int32_t val = clamp<int32_t>((this->baud_rate_ / (bytelength * 1000 / time)) - 1, 1, 120);
this->set_rx_full_threshold(val);
}
} // namespace esphome::uart
+229
View File
@@ -0,0 +1,229 @@
#pragma once
#include <vector>
#include <cstdint>
#include <cstring>
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_UART_DEBUGGER
#include "esphome/core/automation.h"
#endif
namespace esphome::uart {
enum UARTParityOptions {
UART_CONFIG_PARITY_NONE,
UART_CONFIG_PARITY_EVEN,
UART_CONFIG_PARITY_ODD,
// Only USB serial adapters (usb_uart) support these
UART_CONFIG_PARITY_MARK,
UART_CONFIG_PARITY_SPACE,
};
#ifdef USE_UART_DEBUGGER
enum UARTDirection {
UART_DIRECTION_RX,
UART_DIRECTION_TX,
UART_DIRECTION_BOTH,
};
#endif
const LogString *parity_to_str(UARTParityOptions parity);
/// Result of a flush() call.
enum class UARTFlushResult {
UART_FLUSH_RESULT_SUCCESS, ///< Confirmed: all bytes left the TX FIFO.
UART_FLUSH_RESULT_TIMEOUT, ///< Confirmed: timed out before TX completed.
UART_FLUSH_RESULT_FAILED, ///< Confirmed: driver or hardware error.
UART_FLUSH_RESULT_ASSUMED_SUCCESS, ///< Platform cannot report result; success is assumed.
};
class UARTComponent {
public:
static constexpr size_t RX_FULL_THRESHOLD_UNSET = 0;
// Writes an array of bytes to the UART bus.
// @param data A vector of bytes to be written.
void write_array(const std::vector<uint8_t> &data) { this->write_array(&data[0], data.size()); }
// Writes a single byte to the UART bus.
// @param data The byte to be written.
void write_byte(uint8_t data) { this->write_array(&data, 1); };
// Writes a null-terminated string to the UART bus.
// @param str Pointer to the null-terminated string.
void write_str(const char *str) {
const auto *data = reinterpret_cast<const uint8_t *>(str);
this->write_array(data, strlen(str));
};
// Pure virtual method to write an array of bytes to the UART bus.
// @param data Pointer to the array of bytes.
// @param len Length of the array.
virtual void write_array(const uint8_t *data, size_t len) = 0;
// Reads a single byte from the UART bus.
// @param data Pointer to the byte where the read data will be stored.
// @return True if a byte was successfully read, false otherwise.
bool read_byte(uint8_t *data) { return this->read_array(data, 1); };
// Pure virtual method to peek the next byte in the UART buffer without removing it.
// @param data Pointer to the byte where the peeked data will be stored.
// @return True if a byte is available to peek, false otherwise.
virtual bool peek_byte(uint8_t *data) = 0;
// Pure virtual method to read an array of bytes from the UART bus.
// @param data Pointer to the array where the read data will be stored.
// @param len Number of bytes to read.
// @return True if the specified number of bytes were successfully read, false otherwise.
virtual bool read_array(uint8_t *data, size_t len) = 0;
// Pure virtual method to return the number of bytes available for reading.
// @return Number of available bytes.
virtual size_t available() = 0;
// Returns how many bytes write_array() accepts right now without blocking.
// Platforms that cannot tell return SIZE_MAX: write_array() takes everything and may block.
virtual size_t available_for_write() { return SIZE_MAX; }
// Pure virtual method to block until all bytes have been written to the UART bus.
// @return UARTFlushResult indicating whether the flush was confirmed, timed out, failed, or assumed successful.
virtual UARTFlushResult flush() = 0;
// Returns true if the underlying transport is connected and operational.
// Hardware UARTs always return true. USB-backed UARTs override to reflect actual connection state.
virtual bool is_connected() { return true; }
// Sets the maximum time to wait for TX to drain during flush().
// Only meaningful on ESP32 (IDF). Other platforms ignore this value.
// @param flush_timeout_ms Timeout in milliseconds; 0 means wait indefinitely.
virtual void set_flush_timeout(uint32_t flush_timeout_ms) {}
// Sets the TX (transmit) pin for the UART bus.
// @param tx_pin Pointer to the internal GPIO pin used for transmission.
void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; }
// Sets the RX (receive) pin for the UART bus.
// @param rx_pin Pointer to the internal GPIO pin used for reception.
void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; }
// Sets the flow control pin for the UART bus.
// @param flow_control_pin Pointer to the internal GPIO pin used for flow control.
void set_flow_control_pin(InternalGPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
// Sets the size of the RX buffer.
// @param rx_buffer_size Size of the RX buffer in bytes.
void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; }
// Gets the size of the RX buffer.
// @return Size of the RX buffer in bytes.
size_t get_rx_buffer_size() { return this->rx_buffer_size_; }
// Sets the RX FIFO full interrupt threshold.
// @param rx_full_threshold RX full interrupt threshold in bytes.
virtual void set_rx_full_threshold(size_t rx_full_threshold) {}
// Sets the RX FIFO full interrupt threshold.
// @param time RX full interrupt threshold in ms.
void set_rx_full_threshold_ms(uint8_t time);
// Gets the RX FIFO full interrupt threshold.
// @return RX full interrupt threshold in bytes.
size_t get_rx_full_threshold() { return this->rx_full_threshold_; }
// Sets the RX timeout interrupt threshold.
// @param rx_timeout RX timeout interrupt threshold (unit: time of sending one byte).
virtual void set_rx_timeout(size_t rx_timeout) {}
// Gets the RX timeout interrupt threshold.
// @return RX timeout interrupt threshold (unit: time of sending one byte).
size_t get_rx_timeout() { return this->rx_timeout_; }
// Sets the number of stop bits used in UART communication.
// @param stop_bits Number of stop bits.
void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; }
// Gets the number of stop bits used in UART communication.
// @return Number of stop bits.
uint8_t get_stop_bits() const { return this->stop_bits_; }
// Set the number of data bits used in UART communication.
// @param data_bits Number of data bits.
void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; }
// Get the number of data bits used in UART communication.
// @return Number of data bits.
uint8_t get_data_bits() const { return this->data_bits_; }
// Set the parity used in UART communication.
// @param parity Parity option.
void set_parity(UARTParityOptions parity) { this->parity_ = parity; }
// Get the parity used in UART communication.
// @return Parity option.
UARTParityOptions get_parity() const { return this->parity_; }
// Set the baud rate for UART communication.
// @param baud_rate Baud rate in bits per second.
void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; }
// Get the baud rate for UART communication.
// @return Baud rate in bits per second.
uint32_t get_baud_rate() const { return baud_rate_; }
#if defined(USE_ESP8266) || defined(USE_ESP32)
/**
* Load the UART settings.
* @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly.
*
* Example:
* ```cpp
* id(uart1).load_settings(false);
* ```
*
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
virtual void load_settings(bool dump_config) = 0;
/**
* Load the UART settings.
*
* Example:
* ```cpp
* id(uart1).load_settings();
* ```
*
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
void load_settings() { this->load_settings(true); }
#endif // USE_ESP8266 || USE_ESP32
#ifdef USE_UART_DEBUGGER
template<typename F> void add_debug_callback(F &&callback) { this->debug_callback_.add(std::forward<F>(callback)); }
#endif
protected:
virtual void check_logger_conflict() = 0;
bool check_read_timeout_(size_t len = 1);
InternalGPIOPin *tx_pin_{};
InternalGPIOPin *rx_pin_{};
InternalGPIOPin *flow_control_pin_{};
size_t rx_buffer_size_{};
// ESP32 (both Arduino and ESP-IDF) always sets this at codegen time via set_rx_full_threshold().
// Other platforms (USB UART, Arduino, etc.) leave it unset.
size_t rx_full_threshold_{RX_FULL_THRESHOLD_UNSET};
size_t rx_timeout_{0};
uint32_t baud_rate_{0};
uint8_t stop_bits_{0};
uint8_t data_bits_{0};
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE}; // Must match the parity check in __init__.py
#ifdef USE_UART_DEBUGGER
CallbackManager<void(UARTDirection, uint8_t)> debug_callback_{};
#endif
};
} // namespace esphome::uart
+416
View File
@@ -0,0 +1,416 @@
#ifdef USE_ESP8266
#include "uart_component_esp8266.h"
#include <esp8266_peri.h>
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_UART_WAKE_LOOP_ON_RX
#include "esphome/core/wake.h"
#endif
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
namespace esphome::uart {
ESPHOME_LOG_TAG(TAG, "uart");
// Edge decoder up to this baud rate; at 38400 WiFi interrupt latency corrupts bytes.
static constexpr uint32_t SW_SERIAL_EDGE_MODE_MAX_BAUD = 19200;
// Edge decoder time base: 1/16 us from the 1 MHz system timer, which keeps its rate during a
// CpuFrequencyBoost; the cycle counter does not.
static constexpr uint32_t RX_TICKS_PER_SECOND = 16000000;
__attribute__((always_inline)) static inline uint32_t rx_now() { return micros() << 4; }
bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
uint32_t ESP8266UartComponent::get_config() {
uint32_t config = 0;
if (this->parity_ == UART_CONFIG_PARITY_NONE) {
config |= UART_PARITY_NONE;
} else if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
config |= UART_PARITY_EVEN;
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
config |= UART_PARITY_ODD;
}
switch (this->data_bits_) {
case 5:
config |= UART_NB_BIT_5;
break;
case 6:
config |= UART_NB_BIT_6;
break;
case 7:
config |= UART_NB_BIT_7;
break;
case 8:
config |= UART_NB_BIT_8;
break;
}
if (this->stop_bits_ == 1) {
config |= UART_NB_STOP_BIT_1;
} else {
config |= UART_NB_STOP_BIT_2;
}
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted())
config |= BIT(22);
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted())
config |= BIT(19);
return config;
}
void ESP8266UartComponent::setup() {
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
}
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
pin->setup();
}
};
setup_pin_if_needed(this->rx_pin_);
if (this->rx_pin_ != this->tx_pin_) {
setup_pin_if_needed(this->tx_pin_);
}
// Use Arduino HardwareSerial UARTs if all used pins match the ones
// preconfigured by the platform. For example if RX disabled but TX pin
// is 1 we still want to use Serial.
SerialConfig config = static_cast<SerialConfig>(get_config());
#ifdef USE_ESP8266_UART_SERIAL
if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 1) &&
(rx_pin_ == nullptr || rx_pin_->get_pin() == 3)
#ifdef USE_LOGGER
// we will use UART0 if logger isn't using it in swapped mode
&& (logger::global_logger->get_hw_serial() == nullptr ||
logger::global_logger->get_uart() != logger::UART_SELECTION_UART0_SWAP)
#endif
) {
this->hw_serial_ = &Serial;
this->hw_serial_->begin(this->baud_rate_, config);
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
ESP8266UartComponent::serial0_in_use = true;
} else if (!ESP8266UartComponent::serial0_in_use && (tx_pin_ == nullptr || tx_pin_->get_pin() == 15) &&
(rx_pin_ == nullptr || rx_pin_->get_pin() == 13)
#ifdef USE_LOGGER
// we will use UART0 swapped if logger isn't using it in regular mode
&& (logger::global_logger->get_hw_serial() == nullptr ||
logger::global_logger->get_uart() != logger::UART_SELECTION_UART0)
#endif
) {
this->hw_serial_ = &Serial;
this->hw_serial_->begin(this->baud_rate_, config);
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
this->hw_serial_->swap();
ESP8266UartComponent::serial0_in_use = true;
} else
#endif // USE_ESP8266_UART_SERIAL
#ifdef USE_ESP8266_UART_SERIAL1
if ((tx_pin_ == nullptr || tx_pin_->get_pin() == 2) && (rx_pin_ == nullptr || rx_pin_->get_pin() == 8)) {
this->hw_serial_ = &Serial1;
this->hw_serial_->begin(this->baud_rate_, config);
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
} else
#endif // USE_ESP8266_UART_SERIAL1
{
this->sw_serial_ = new ESP8266SoftwareSerial(); // NOLINT
this->sw_serial_->setup(tx_pin_, rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_,
this->rx_buffer_size_);
}
}
void ESP8266UartComponent::load_settings(bool dump_config) {
ESP_LOGCONFIG(TAG, "Loading UART bus settings");
if (this->hw_serial_ != nullptr) {
SerialConfig config = static_cast<SerialConfig>(get_config());
this->hw_serial_->begin(this->baud_rate_, config);
this->hw_serial_->setRxBufferSize(this->rx_buffer_size_);
} else {
this->sw_serial_->setup(this->tx_pin_, this->rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_,
this->parity_, this->rx_buffer_size_);
}
if (dump_config) {
ESP_LOGCONFIG(TAG, "UART bus was reloaded.");
this->dump_config();
}
}
void ESP8266UartComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus:");
LOG_PIN(" TX Pin: ", this->tx_pin_);
LOG_PIN(" RX Pin: ", this->rx_pin_);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT
}
ESP_LOGCONFIG(TAG,
" Baud Rate: %u baud\n"
" Data Bits: %u\n"
" Parity: %s\n"
" Stop bits: %u",
this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_);
if (this->hw_serial_ != nullptr) {
ESP_LOGCONFIG(TAG, " Using hardware serial interface.");
} else {
ESP_LOGCONFIG(TAG, " Using software serial"
#ifdef USE_UART_WAKE_LOOP_ON_RX
"\n Wake on data RX: ENABLED"
#endif
);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX decoder: %s",
this->baud_rate_ <= SW_SERIAL_EDGE_MODE_MAX_BAUD ? LOG_STR_LITERAL("edge")
: LOG_STR_LITERAL("start bit sampler"));
}
}
this->check_logger_conflict();
}
void ESP8266UartComponent::check_logger_conflict() {
#ifdef USE_LOGGER
if (this->hw_serial_ == nullptr || logger::global_logger->get_baud_rate() == 0) {
return;
}
if (this->hw_serial_ == logger::global_logger->get_hw_serial()) {
ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please "
"disable logging over the serial port by setting logger->baud_rate to 0.");
}
#endif
}
void ESP8266UartComponent::write_array(const uint8_t *data, size_t len) {
if (this->hw_serial_ != nullptr) {
this->hw_serial_->write(data, len);
} else {
for (size_t i = 0; i < len; i++)
this->sw_serial_->write_byte(data[i]);
}
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
}
bool ESP8266UartComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
if (this->hw_serial_ != nullptr) {
*data = this->hw_serial_->peek();
} else {
*data = this->sw_serial_->peek_byte();
}
return true;
}
bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) {
if (!this->check_read_timeout_(len))
return false;
if (this->hw_serial_ != nullptr) {
this->hw_serial_->readBytes(data, len);
} else {
for (size_t i = 0; i < len; i++)
data[i] = this->sw_serial_->read_byte();
}
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return true;
}
size_t ESP8266UartComponent::available() {
if (this->hw_serial_ != nullptr) {
return this->hw_serial_->available();
} else {
return this->sw_serial_->available();
}
}
size_t ESP8266UartComponent::available_for_write() {
if (this->hw_serial_ != nullptr) {
return this->hw_serial_->availableForWrite();
}
return SIZE_MAX; // software serial bit-bangs each byte synchronously; there is no buffer to fill
}
UARTFlushResult ESP8266UartComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
if (this->hw_serial_ != nullptr) {
this->hw_serial_->flush();
} else {
this->sw_serial_->flush();
}
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate,
uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity,
size_t rx_buffer_size) {
// load_settings() re-enters here
if (this->gpio_rx_pin_ != nullptr)
this->gpio_rx_pin_->detach_interrupt();
this->bit_time_ = F_CPU / baud_rate;
this->stop_bits_ = stop_bits;
this->data_bits_ = data_bits;
this->parity_ = parity;
// to_isr() allocates, so call it only for a new or changed pin
if (tx_pin != nullptr) {
if (tx_pin != this->gpio_tx_pin_)
this->tx_pin_ = tx_pin->to_isr();
gpio_tx_pin_ = tx_pin;
gpio_tx_pin_->setup();
tx_pin_.digital_write(true);
}
if (rx_pin != nullptr) {
if (rx_pin != this->gpio_rx_pin_)
this->rx_pin_ = rx_pin->to_isr();
gpio_rx_pin_ = rx_pin;
gpio_rx_pin_->setup();
uint8_t *buffer = this->rx_.buffer();
if (this->rx_.buffer_size() != rx_buffer_size) {
delete[] buffer; // NOLINT
buffer = new uint8_t[rx_buffer_size]; // NOLINT
}
this->rx_.setup((RX_TICKS_PER_SECOND + baud_rate / 2) / baud_rate, data_bits, parity != UART_CONFIG_PARITY_NONE,
stop_bits, buffer, rx_buffer_size);
this->rx_.reset(rx_now(), this->rx_pin_.digital_read());
if (baud_rate <= SW_SERIAL_EDGE_MODE_MAX_BAUD) {
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr_edge, this, gpio::INTERRUPT_ANY_EDGE);
} else {
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
}
}
}
// A byte can arrive while a CpuFrequencyBoost has an 80 MHz build at 160 MHz; the clock select bit doubles
// the bit time then. The whole byte is read inside the ISR, so the clock cannot change partway through.
__attribute__((always_inline)) static inline uint32_t rx_bit_time(uint32_t bit_time) {
#if F_CPU != 160000000L
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- CPU2X is MMIO at a fixed address
return bit_time << (CPU2X & 1);
#else
return bit_time;
#endif
}
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
const uint32_t bit_time = rx_bit_time(arg->bit_time_);
uint32_t wait = bit_time + bit_time / 3 - 500;
const uint32_t start = arch_get_cpu_cycle_count();
uint8_t rec = 0;
// Manually unroll the loop
for (int i = 0; i < arg->data_bits_; i++)
rec |= arg->read_bit_(&wait, start, bit_time) << i;
/* If parity is enabled, just read it and ignore it. */
/* TODO: Should we check parity? Or is it too slow for nothing added..*/
if (arg->parity_ == UART_CONFIG_PARITY_EVEN || arg->parity_ == UART_CONFIG_PARITY_ODD)
arg->read_bit_(&wait, start, bit_time);
// Stop bit
arg->wait_(&wait, start, bit_time);
if (arg->stop_bits_ == 2)
arg->wait_(&wait, start, bit_time);
arg->rx_.push_byte(rec);
// Clear RX pin so that the interrupt doesn't re-trigger right away again.
arg->rx_pin_.clear_interrupt();
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Wake the main loop so the consuming component drains the byte promptly
// instead of waiting for the next loop_interval_ tick. Important for timing
// sensitive setups that poll read() in a tight loop (e.g. fingerprint_grow).
wake_loop_isrsafe();
#endif
}
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr_edge(ESP8266SoftwareSerial *arg) {
const uint32_t now = rx_now();
const bool level = arg->rx_pin_.digital_read();
[[maybe_unused]] const bool wake = arg->rx_.on_edge(now, level);
#ifdef USE_UART_WAKE_LOOP_ON_RX
if (wake)
wake_loop_isrsafe();
#endif
}
void ESP8266SoftwareSerial::rx_finalize_pending_() {
if (!this->rx_.finalize_due(rx_now())) {
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Tail not elapsed; wake again only once drained
if (this->rx_.available() == 0)
wake_loop_threadsafe();
#endif
return;
}
InterruptLock lock;
this->rx_.finalize(rx_now());
}
void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
if (this->gpio_tx_pin_ == nullptr) {
ESP_LOGE(TAG, "UART doesn't have TX pins set!");
return;
}
bool parity_bit = false;
bool need_parity_bit = true;
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
parity_bit = false;
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
parity_bit = true;
} else {
need_parity_bit = false;
}
{
// Transmit runs from the main loop and never overlaps a CpuFrequencyBoost
InterruptLock lock;
const uint32_t bit_time = this->bit_time_;
uint32_t wait = bit_time;
const uint32_t start = arch_get_cpu_cycle_count();
// Start bit
this->write_bit_(false, &wait, start, bit_time);
for (int i = 0; i < this->data_bits_; i++) {
bool bit = data & (1 << i);
this->write_bit_(bit, &wait, start, bit_time);
if (need_parity_bit)
parity_bit ^= bit;
}
if (need_parity_bit)
this->write_bit_(parity_bit, &wait, start, bit_time);
// Stop bit
this->write_bit_(true, &wait, start, bit_time);
if (this->stop_bits_ == 2)
this->wait_(&wait, start, bit_time);
}
}
void IRAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time) {
while (arch_get_cpu_cycle_count() - start < *wait)
;
*wait += bit_time;
}
bool IRAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start, uint32_t bit_time) {
this->wait_(wait, start, bit_time);
return this->rx_pin_.digital_read();
}
void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time) {
this->tx_pin_.digital_write(bit);
this->wait_(wait, start, bit_time);
}
uint8_t ESP8266SoftwareSerial::read_byte() {
this->rx_sync_();
return this->rx_.read_byte();
}
uint8_t ESP8266SoftwareSerial::peek_byte() {
this->rx_sync_();
return this->rx_.peek_byte();
}
void ESP8266SoftwareSerial::flush() {
// Flush is a NO-OP with software serial, all bytes are written immediately.
}
size_t ESP8266SoftwareSerial::available() {
this->rx_sync_();
return this->rx_.available();
}
} // namespace esphome::uart
#endif // USE_ESP8266
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#ifdef USE_ESP8266
#include <HardwareSerial.h>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "software_serial_rx_decoder.h"
#include "uart_component.h"
namespace esphome::uart {
class ESP8266SoftwareSerial {
public:
void setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate, uint8_t stop_bits,
uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size);
uint8_t read_byte();
uint8_t peek_byte();
void flush();
void write_byte(uint8_t data);
size_t available();
protected:
/// Above SW_SERIAL_EDGE_MODE_MAX_BAUD; reads the whole byte in the ISR.
static void gpio_intr(ESP8266SoftwareSerial *arg);
static void gpio_intr_edge(ESP8266SoftwareSerial *arg);
void wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time);
bool read_bit_(uint32_t *wait, const uint32_t &start, uint32_t bit_time);
void write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time);
void rx_finalize_pending_();
void ESPHOME_ALWAYS_INLINE rx_sync_() {
if (this->rx_.pending())
this->rx_finalize_pending_();
}
// Members ordered largest to smallest to minimize padding
uint32_t bit_time_{0};
InternalGPIOPin *gpio_tx_pin_{nullptr};
ISRInternalGPIOPin tx_pin_;
InternalGPIOPin *gpio_rx_pin_{nullptr};
ISRInternalGPIOPin rx_pin_;
SoftwareSerialRxDecoder rx_;
UARTParityOptions parity_;
uint8_t stop_bits_;
uint8_t data_bits_;
};
class ESP8266UartComponent final : public UARTComponent, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
size_t available_for_write() override;
UARTFlushResult flush() override;
uint32_t get_config();
/**
* Load the UART with the current settings.
* @param dump_config (Optional, default `true`): True for displaying new settings or
* false to change it quitely
*
* Example:
* ```cpp
* id(uart1).load_settings();
* ```
*
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
void load_settings(bool dump_config) override;
using UARTComponent::load_settings; // also bring in the no-arg overload for convenience
protected:
void check_logger_conflict() override;
HardwareSerial *hw_serial_{nullptr};
ESP8266SoftwareSerial *sw_serial_{nullptr};
private:
static bool serial0_in_use; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
};
} // namespace esphome::uart
#endif // USE_ESP8266
+520
View File
@@ -0,0 +1,520 @@
#ifdef USE_ESP32
#include "uart_component_esp_idf.h"
#include <cinttypes>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/gpio.h"
#include "driver/gpio.h"
#include "hal/uart_ll.h"
#include "esp_private/gpio.h"
#include "soc/gpio_num.h"
#include "soc/soc_caps.h"
#include "soc/uart_pins.h"
#ifdef USE_UART_WAKE_LOOP_ON_RX
#include "esphome/core/application.h"
#endif
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
namespace esphome::uart {
static const char *const TAG = "uart";
/// Check if a pin number matches one of the default UART0 GPIO pins.
/// These pins may have residual IOMUX state from the ROM bootloader that
/// must be cleared before UART reconfiguration.
///
/// ESP-IDF's uart_set_pin() has an asymmetry: when routing TX via GPIO matrix,
/// it calls gpio_func_sel(PIN_FUNC_GPIO) to clear IOMUX, but for RX it only
/// calls gpio_input_enable() which does NOT clear the IOMUX function select.
/// If a default UART0 TX pin (configured as TX via IOMUX during boot) is later
/// reassigned as RX via GPIO matrix, the old IOMUX TX function remains active,
/// causing TX data to loop back into RX on the same pin.
static constexpr bool is_default_uart0_pin(int8_t pin_num) {
return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM;
}
// clock_source_ is stored in a byte; every uart_sclk_t value is a soc_module_clk_t below SOC_MOD_CLK_INVALID
static_assert(SOC_MOD_CLK_INVALID <= UINT8_MAX, "uart_sclk_t no longer fits in uint8_t clock_source_");
static const LogString *clock_source_to_str(uart_sclk_t clock_source) {
switch (clock_source) {
#if SOC_UART_SUPPORT_APB_CLK
case UART_SCLK_APB:
return LOG_STR("APB");
#endif
#if SOC_UART_SUPPORT_XTAL_CLK
case UART_SCLK_XTAL:
return LOG_STR("XTAL");
#endif
#if SOC_UART_SUPPORT_RTC_CLK
case UART_SCLK_RTC:
return LOG_STR("RTC");
#endif
#if SOC_UART_SUPPORT_REF_TICK
case UART_SCLK_REF_TICK:
return LOG_STR("REF_TICK");
#endif
default:
return clock_source == UART_SCLK_DEFAULT ? LOG_STR("DEFAULT") : LOG_STR("UNKNOWN");
}
}
uart_config_t IDFUARTComponent::get_config_() {
uart_parity_t parity = UART_PARITY_DISABLE;
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
parity = UART_PARITY_EVEN;
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
parity = UART_PARITY_ODD;
}
uart_word_length_t data_bits;
switch (this->data_bits_) {
case 5:
data_bits = UART_DATA_5_BITS;
break;
case 6:
data_bits = UART_DATA_6_BITS;
break;
case 7:
data_bits = UART_DATA_7_BITS;
break;
case 8:
data_bits = UART_DATA_8_BITS;
break;
default:
data_bits = UART_DATA_BITS_MAX;
break;
}
uart_config_t uart_config{};
uart_config.baud_rate = this->baud_rate_;
uart_config.data_bits = data_bits;
uart_config.parity = parity;
uart_config.stop_bits = this->stop_bits_ == 1 ? UART_STOP_BITS_1 : UART_STOP_BITS_2;
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
uart_config.source_clk = static_cast<uart_sclk_t>(this->clock_source_);
uart_config.rx_flow_ctrl_thresh = 122;
return uart_config;
}
void IDFUARTComponent::setup() {
static uint8_t next_uart_num = 0;
#ifdef USE_LOGGER
bool logger_uses_hardware_uart = true;
#ifdef USE_LOGGER_USB_CDC
if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_CDC) {
// this is not a hardware UART, ignore it
logger_uses_hardware_uart = false;
}
#endif // USE_LOGGER_USB_CDC
#ifdef USE_LOGGER_USB_SERIAL_JTAG
if (logger::global_logger->get_uart() == logger::UART_SELECTION_USB_SERIAL_JTAG) {
// this is not a hardware UART, ignore it
logger_uses_hardware_uart = false;
}
#endif // USE_LOGGER_USB_SERIAL_JTAG
if (logger_uses_hardware_uart && logger::global_logger->get_baud_rate() > 0 &&
logger::global_logger->get_uart_num() == next_uart_num) {
next_uart_num++;
}
#endif // USE_LOGGER
if (next_uart_num >= SOC_UART_NUM) {
ESP_LOGW(TAG, "Maximum number of UART components created already");
this->mark_failed();
return;
}
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
#if (SOC_UART_LP_NUM >= 1)
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
#else
size_t fifo_len = SOC_UART_FIFO_LEN;
#endif
if (this->rx_buffer_size_ <= fifo_len) {
ESP_LOGW(TAG, "rx_buffer_size is too small, must be greater than %zu", fifo_len);
this->rx_buffer_size_ = fifo_len * 2;
}
this->load_settings(false);
}
void IDFUARTComponent::load_settings(bool dump_config) {
esp_err_t err;
if (uart_is_driver_installed(this->uart_num_)) {
err = uart_driver_delete(this->uart_num_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
}
err = uart_driver_install(this->uart_num_, // UART number
this->rx_buffer_size_, // RX ring buffer size
this->tx_buffer_size_, // TX ring buffer size; 0 makes uart_write_bytes() block until
// the FIFO has taken everything
0, // event queue size/depth
nullptr, // event queue
0 // Flags used to allocate the interrupt
);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
// uart_param_config must be called after uart_driver_install and before any
// other uart_set_*() calls. The driver installation resets the UART peripheral
// registers to their default state, overwriting any previously configured baud
// rate or framing settings. Calling uart_param_config here ensures the requested
// settings are applied after the reset and before pin routing, inversion, and
// threshold configuration.
uart_config_t uart_config = this->get_config_();
err = uart_param_config(this->uart_num_, &uart_config);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_param_config failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
this->last_good_framing_ = this->framing_();
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
// Clear residual IOMUX function on UART0 default pins left by the ROM bootloader.
// See is_default_uart0_pin() comment for details on the ESP-IDF uart_set_pin() bug.
if (is_default_uart0_pin(tx)) {
gpio_func_sel(static_cast<gpio_num_t>(tx), PIN_FUNC_GPIO);
}
if (is_default_uart0_pin(rx)) {
gpio_func_sel(static_cast<gpio_num_t>(rx), PIN_FUNC_GPIO);
}
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
}
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
pin->setup();
}
};
setup_pin_if_needed(this->rx_pin_);
if (this->rx_pin_ != this->tx_pin_) {
setup_pin_if_needed(this->tx_pin_);
}
// Must precede uart_set_pin() so an inverted TX line never shows the wrong idle
// level; apply_line_settings_() repeats it later for the reset registers.
err = uart_set_line_inverse(this->uart_num_, this->line_inversion_mask_());
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
err = uart_set_pin(this->uart_num_, tx, rx, flow_control, UART_PIN_NO_CHANGE);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_pin failed: %s", esp_err_to_name(err));
this->mark_failed();
return;
}
if (this->apply_line_settings_() != ESP_OK) {
this->mark_failed();
return;
}
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Register ISR callback to wake the main loop when UART data arrives.
// The callback runs in ISR context and uses vTaskNotifyGiveFromISR() to
// wake the main loop task directly — no queue or FreeRTOS task needed.
uart_set_select_notif_callback(this->uart_num_, IDFUARTComponent::uart_rx_isr_callback);
#endif // USE_UART_WAKE_LOOP_ON_RX
if (dump_config) {
ESP_LOGCONFIG(TAG, "Reloaded UART %u", this->uart_num_);
this->dump_config();
}
}
uint32_t IDFUARTComponent::line_inversion_mask_() {
uint32_t invert = 0;
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
invert |= UART_SIGNAL_TXD_INV;
}
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted()) {
invert |= UART_SIGNAL_RXD_INV;
}
if (this->flow_control_pin_ != nullptr && this->flow_control_pin_->is_inverted()) {
invert |= UART_SIGNAL_RTS_INV;
}
return invert;
}
esp_err_t IDFUARTComponent::apply_line_settings_() {
// uart_param_config() resets these; call after every use of it.
esp_err_t err = uart_set_line_inverse(this->uart_num_, this->line_inversion_mask_());
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err));
return err;
}
err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
return err;
}
err = uart_set_rx_timeout(this->uart_num_, this->rx_timeout_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err));
return err;
}
// Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install().
auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART;
err = uart_set_mode(this->uart_num_, mode);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
void IDFUARTComponent::set_framing_(const Framing &framing) {
this->baud_rate_ = framing.baud_rate;
this->data_bits_ = framing.data_bits;
this->stop_bits_ = framing.stop_bits;
this->parity_ = framing.parity;
this->rx_full_threshold_ = framing.rx_full_threshold;
}
esp_err_t IDFUARTComponent::apply_settings_live() {
if (this->is_failed()) {
return ESP_ERR_INVALID_STATE;
}
// No driver yet: nothing to reconfigure in place.
if (!uart_is_driver_installed(this->uart_num_)) {
this->load_settings(false);
return this->is_failed() ? ESP_FAIL : ESP_OK;
}
// Keeps the driver ring buffers; flushes both hardware FIFOs (in-flight bytes lost).
uart_config_t uart_config = this->get_config_();
esp_err_t err = uart_param_config(this->uart_num_, &uart_config);
if (err != ESP_OK) {
// Failure leaves the registers reset; put back the last accepted framing so the
// getters still describe the hardware.
if (this->last_good_framing_.baud_rate == 0) {
ESP_LOGE(TAG, "uart_param_config (live) failed: %s; no previous framing to restore", esp_err_to_name(err));
this->mark_failed();
return err;
}
ESP_LOGW(TAG, "uart_param_config (live) failed: %s; restoring %" PRIu32 " baud", esp_err_to_name(err),
this->last_good_framing_.baud_rate);
this->set_framing_(this->last_good_framing_);
uart_config = this->get_config_();
esp_err_t restore_err = uart_param_config(this->uart_num_, &uart_config);
if (restore_err != ESP_OK) {
ESP_LOGE(TAG, "UART left unconfigured after failed live reconfigure: %s", esp_err_to_name(restore_err));
this->mark_failed();
return err;
}
// Previous framing is live again; report the refusal (line-setting errors log).
this->apply_line_settings_();
return err;
}
this->last_good_framing_ = this->framing_();
// The new framing is live; a line-setting failure here only logs.
this->apply_line_settings_();
return ESP_OK;
}
void IDFUARTComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_);
LOG_PIN(" TX Pin: ", this->tx_pin_);
LOG_PIN(" RX Pin: ", this->rx_pin_);
LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG,
" RX Buffer Size: %u\n"
" RX Full Threshold: %u\n"
" RX Timeout: %u",
this->rx_buffer_size_, this->rx_full_threshold_, this->rx_timeout_);
}
if (this->tx_buffer_size_ > 0) {
ESP_LOGCONFIG(TAG, " TX Buffer Size: %zu", this->tx_buffer_size_);
}
if (this->flush_timeout_ms_ > 0) {
ESP_LOGCONFIG(TAG, " Flush Timeout: %" PRIu32 " ms", this->flush_timeout_ms_);
}
ESP_LOGCONFIG(TAG,
" Baud Rate: %" PRIu32 " baud\n"
" Data Bits: %u\n"
" Parity: %s\n"
" Stop bits: %u\n"
" Clock Source: %s"
#ifdef USE_UART_WAKE_LOOP_ON_RX
"\n Wake on data RX: ENABLED"
#endif
,
this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_,
LOG_STR_ARG(clock_source_to_str(static_cast<uart_sclk_t>(this->clock_source_))));
this->check_logger_conflict();
}
void IDFUARTComponent::set_rx_full_threshold(size_t rx_full_threshold) {
if (this->is_ready()) {
esp_err_t err = uart_set_rx_full_threshold(this->uart_num_, rx_full_threshold);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err));
return;
}
}
this->rx_full_threshold_ = rx_full_threshold;
}
void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
if (this->is_ready()) {
esp_err_t err = uart_set_rx_timeout(this->uart_num_, rx_timeout);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err));
return;
}
}
this->rx_timeout_ = rx_timeout;
}
size_t IDFUARTComponent::available_for_write() {
if (this->uart_num_ == UART_NUM_MAX || !uart_is_driver_installed(this->uart_num_))
return 0;
if (this->tx_buffer_size_ == 0) {
return uart_ll_get_txfifo_len(UART_LL_GET_HW(this->uart_num_));
}
// The driver's figure already deducts its ring item headers, so a write of this size fits
size_t free = 0;
uart_get_tx_buffer_free_size(this->uart_num_, &free);
return free;
}
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
if (write_len != (int32_t) len) {
ESP_LOGW(TAG, "uart_write_bytes failed: %" PRId32 " != %zu", write_len, len);
this->mark_failed();
}
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
}
bool IDFUARTComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
if (this->has_peek_) {
*data = this->peek_byte_;
} else {
int len = uart_read_bytes(this->uart_num_, data, 1, 20 / portTICK_PERIOD_MS);
if (len == 0) {
*data = 0;
} else {
this->has_peek_ = true;
this->peek_byte_ = *data;
}
}
return true;
}
bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
if (len == 0) {
return false;
}
size_t length_to_read = len;
int32_t read_len = 0;
if (!this->check_read_timeout_(len))
return false;
if (this->has_peek_) {
length_to_read--;
*data = this->peek_byte_;
this->has_peek_ = false;
}
if (length_to_read > 0)
read_len = uart_read_bytes(this->uart_num_, data + (len - length_to_read), length_to_read, 20 / portTICK_PERIOD_MS);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return read_len == (int32_t) length_to_read;
}
size_t IDFUARTComponent::available() {
size_t available = 0;
esp_err_t err;
err = uart_get_buffered_data_len(this->uart_num_, &available);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
this->mark_failed();
}
if (this->has_peek_) {
available++;
}
return available;
}
UARTFlushResult IDFUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
TickType_t ticks = this->flush_timeout_ms_ == 0 ? portMAX_DELAY : pdMS_TO_TICKS(this->flush_timeout_ms_);
esp_err_t err = uart_wait_tx_done(this->uart_num_, ticks);
if (err == ESP_OK)
return UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
if (err == ESP_ERR_TIMEOUT)
return UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT;
return UARTFlushResult::UART_FLUSH_RESULT_FAILED;
}
void IDFUARTComponent::check_logger_conflict() {}
#ifdef USE_UART_WAKE_LOOP_ON_RX
// ISR callback invoked by the ESP-IDF UART driver when data arrives.
// Wakes the main loop directly via vTaskNotifyGiveFromISR() — no queue or task needed.
void IRAM_ATTR IDFUARTComponent::uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif,
BaseType_t *task_woken) {
if (uart_select_notif == UART_SELECT_READ_NOTIF) {
Application::wake_loop_isrsafe(task_woken);
}
}
#endif // USE_UART_WAKE_LOOP_ON_RX
void IDFUARTComponent::on_shutdown() {
if (this->uart_num_ == UART_NUM_MAX || !uart_is_driver_installed(this->uart_num_))
return;
uart_wait_tx_done(this->uart_num_, pdMS_TO_TICKS(100));
// Keep the peripheral quiet across a soft reset so ROM output does not reach the attached device (#15472)
esp_err_t err = uart_driver_delete(this->uart_num_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_driver_delete failed: %s", esp_err_to_name(err));
}
}
} // namespace esphome::uart
#endif // USE_ESP32
+126
View File
@@ -0,0 +1,126 @@
#pragma once
#ifdef USE_ESP32
#include <driver/uart.h>
#include "esphome/core/component.h"
#include "uart_component.h"
#ifdef USE_UART_WAKE_LOOP_ON_RX
#include <driver/uart_select.h>
#endif
namespace esphome::uart {
/// ESP-IDF UART driver wrapper.
///
/// Thread safety: All public methods must only be called from the main loop.
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
/// peek byte state (has_peek_/peek_byte_) is not synchronized.
class IDFUARTComponent final : public UARTComponent, public Component {
public:
// User provided, not "= default": `new(p) IDFUARTComponent()` would zero-fill .bss that is already zero.
IDFUARTComponent() {}
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void set_rx_full_threshold(size_t rx_full_threshold) override;
void set_rx_timeout(size_t rx_timeout) override;
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
size_t available_for_write() override;
UARTFlushResult flush() override;
void set_flush_timeout(uint32_t flush_timeout_ms) override { this->flush_timeout_ms_ = flush_timeout_ms; }
/// TX ring buffer size for the driver; 0 leaves TX unbuffered so write_array() blocks until
/// the FIFO has taken everything.
void set_tx_buffer_size(size_t tx_buffer_size) { this->tx_buffer_size_ = tx_buffer_size; }
void set_clock_source(uart_sclk_t clock_source) { this->clock_source_ = static_cast<uint8_t>(clock_source); }
uint8_t get_hw_serial_number() { return this->uart_num_; }
/// Discard everything received so far: the peek cache and the driver's RX buffer.
void flush_input() {
this->has_peek_ = false;
uart_flush_input(this->uart_num_);
}
/**
* Load the UART with the current settings.
* @param dump_config (Optional, default `true`): True for displaying new settings or
* false to change it quitely
*
* Example:
* ```cpp
* id(uart1).load_settings();
* ```
*
* This will load the current UART interface with the latest settings (baud_rate, parity, etc).
*/
void load_settings(bool dump_config) override;
using UARTComponent::load_settings; // also bring in the no-arg overload for convenience
/**
* Apply the current framing (baud rate, parity, data/stop bits) to the installed
* driver in place, without the delete/reinstall of load_settings(). Tasks blocked in
* the driver survive and the ring buffers are kept, but both hardware FIFOs are
* flushed: a frame in flight reaches the peer truncated and bytes not yet out of the
* RX FIFO are dropped. No lock is taken: quiesce writers first if that matters.
* rx_full_threshold is not rescaled (call set_rx_full_threshold_ms() first if it
* should follow the baud rate); a rollback restores the value from the last accepted
* configuration, undoing a standalone set_rx_full_threshold() made since. Without an
* installed driver this is a full load_settings(false) instead.
*
* @return ESP_OK once the new framing is live (a line-setting error after that only
* logs). On rejection (unreachable baud rate) the previous framing is restored and
* the driver's error returned; if the restore fails too the component is marked
* failed. ESP_ERR_INVALID_STATE if already failed; ESP_FAIL if the fallback
* load_settings() fails.
*/
esp_err_t apply_settings_live();
void on_shutdown() override;
protected:
void check_logger_conflict() override;
uint32_t line_inversion_mask_();
// Re-applies what uart_param_config() resets: inversion, RX threshold/timeout, mode.
esp_err_t apply_line_settings_();
uart_port_t uart_num_{UART_NUM_MAX};
uart_config_t get_config_();
struct Framing {
uint32_t baud_rate;
uint8_t data_bits;
uint8_t stop_bits;
UARTParityOptions parity;
size_t rx_full_threshold; // sized for the baud rate, so rolled back with it
};
Framing framing_() const {
return {this->baud_rate_, this->data_bits_, this->stop_bits_, this->parity_, this->rx_full_threshold_};
}
void set_framing_(const Framing &framing);
// Last framing the driver accepted; baud_rate 0 means none yet.
Framing last_good_framing_{};
bool has_peek_{false};
uint8_t peek_byte_{0};
uint8_t clock_source_{UART_SCLK_DEFAULT}; ///< uart_sclk_t stored in a byte; the IDF values are all small.
uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY).
size_t tx_buffer_size_{0};
#ifdef USE_UART_WAKE_LOOP_ON_RX
// ISR callback for UART RX data notification — wakes the main loop directly.
static void uart_rx_isr_callback(uart_port_t uart_num, uart_select_notif_t uart_select_notif, BaseType_t *task_woken);
#endif // USE_UART_WAKE_LOOP_ON_RX
};
} // namespace esphome::uart
#endif // USE_ESP32
+297
View File
@@ -0,0 +1,297 @@
#ifdef USE_HOST
#include "uart_component_host.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#if !(defined(__linux__) || defined(__APPLE__))
#error This HostUartComponent implementation is not supported on this host OS
#endif
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
namespace {
speed_t get_baud(int baud) {
#ifdef __APPLE__
return baud;
#else
switch (baud) {
case 50:
return B50;
case 75:
return B75;
case 110:
return B110;
case 134:
return B134;
case 150:
return B150;
case 200:
return B200;
case 300:
return B300;
case 600:
return B600;
case 1200:
return B1200;
case 1800:
return B1800;
case 2400:
return B2400;
case 4800:
return B4800;
case 9600:
return B9600;
case 19200:
return B19200;
case 38400:
return B38400;
case 57600:
return B57600;
case 115200:
return B115200;
case 230400:
return B230400;
case 460800:
return B460800;
case 500000:
return B500000;
case 576000:
return B576000;
case 921600:
return B921600;
case 1000000:
return B1000000;
case 1152000:
return B1152000;
case 1500000:
return B1500000;
case 2000000:
return B2000000;
case 2500000:
return B2500000;
case 3000000:
return B3000000;
case 3500000:
return B3500000;
case 4000000:
return B4000000;
default:
return B0;
}
#endif
}
} // namespace
namespace esphome::uart {
static const char *const TAG = "uart";
HostUartComponent::~HostUartComponent() {
if (this->file_descriptor_ != -1) {
close(this->file_descriptor_);
this->file_descriptor_ = -1;
}
}
void HostUartComponent::setup() {
ESP_LOGCONFIG(TAG, "Opening UART port");
speed_t baud = get_baud(this->baud_rate_);
if (baud == B0) {
ESP_LOGE(TAG, "Unsupported baud rate: %d", this->baud_rate_);
this->mark_failed();
return;
}
this->file_descriptor_ = ::open(this->port_name_.c_str(), O_RDWR | O_NOCTTY | O_NDELAY);
if (this->file_descriptor_ == -1) {
this->update_error_(strerror(errno));
this->mark_failed();
return;
}
fcntl(this->file_descriptor_, F_SETFL, 0);
struct termios options;
tcgetattr(this->file_descriptor_, &options);
cfmakeraw(&options);
options.c_cflag &= ~CRTSCTS;
options.c_cflag |= CREAD | CLOCAL;
options.c_iflag &= ~(IXOFF | IXANY);
// Set data bits
options.c_cflag &= ~CSIZE; // Mask the character size bits
switch (this->data_bits_) {
case 5:
options.c_cflag |= CS5;
break;
case 6:
options.c_cflag |= CS6;
break;
case 7:
options.c_cflag |= CS7;
break;
case 8:
default:
options.c_cflag |= CS8;
break;
}
// Set parity
switch (this->parity_) {
case UART_CONFIG_PARITY_NONE:
options.c_cflag &= ~PARENB;
break;
case UART_CONFIG_PARITY_EVEN:
options.c_cflag |= PARENB;
options.c_cflag &= ~PARODD;
break;
case UART_CONFIG_PARITY_ODD:
options.c_cflag |= PARENB;
options.c_cflag |= PARODD;
break;
default:
break;
};
// Set stop bits
if (this->stop_bits_ == 2) {
options.c_cflag |= CSTOPB;
} else {
options.c_cflag &= ~CSTOPB;
}
cfsetispeed(&options, baud);
cfsetospeed(&options, baud);
tcsetattr(this->file_descriptor_, TCSANOW, &options);
}
void HostUartComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART:");
ESP_LOGCONFIG(TAG, " Port: %s", this->port_name_.c_str());
if (this->file_descriptor_ == -1) {
ESP_LOGCONFIG(TAG, " Port status: Not opened");
if (!this->first_error_.empty()) {
ESP_LOGCONFIG(TAG, " Error: %s", this->first_error_.c_str());
}
return;
}
ESP_LOGCONFIG(TAG,
" Port status: opened\n"
" Baud Rate: %d\n"
" Data Bits: %d\n"
" Parity: %s\n"
" Stop Bits: %d",
this->baud_rate_, this->data_bits_,
this->parity_ == UART_CONFIG_PARITY_NONE ? "None"
: this->parity_ == UART_CONFIG_PARITY_EVEN ? "Even"
: "Odd",
this->stop_bits_);
this->check_logger_conflict();
}
void HostUartComponent::write_array(const uint8_t *data, size_t len) {
if (this->file_descriptor_ == -1) {
return;
}
size_t written = ::write(this->file_descriptor_, data, len);
if (written != len) {
this->update_error_(strerror(errno));
return;
}
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
return;
}
bool HostUartComponent::peek_byte(uint8_t *data) {
if (this->file_descriptor_ == -1) {
return false;
}
if (!this->has_peek_) {
if (!this->check_read_timeout_()) {
return false;
}
if (::read(this->file_descriptor_, &this->peek_byte_, 1) != 1) {
this->update_error_(strerror(errno));
return false;
}
this->has_peek_ = true;
}
*data = this->peek_byte_;
return true;
}
bool HostUartComponent::read_array(uint8_t *data, size_t len) {
if ((this->file_descriptor_ == -1) || (len == 0)) {
return false;
}
if (!this->check_read_timeout_(len))
return false;
size_t length_to_read = len;
if (this->has_peek_) {
length_to_read--;
*data = this->peek_byte_;
this->has_peek_ = false;
}
if (length_to_read > 0) {
int sz = ::read(this->file_descriptor_, data + (len - length_to_read), length_to_read);
if (sz == -1) {
this->update_error_(strerror(errno));
return false;
}
}
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return true;
}
size_t HostUartComponent::available() {
if (this->file_descriptor_ == -1) {
return 0;
}
int available;
int res = ioctl(this->file_descriptor_, FIONREAD, &available);
if (res == -1) {
this->update_error_(strerror(errno));
return 0;
}
size_t result = available;
if (this->has_peek_)
result++;
return result;
};
UARTFlushResult HostUartComponent::flush() {
if (this->file_descriptor_ == -1) {
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
ESP_LOGV(TAG, " Flushing");
if (tcdrain(this->file_descriptor_) == -1) {
this->update_error_(strerror(errno));
return UARTFlushResult::UART_FLUSH_RESULT_FAILED;
}
return UARTFlushResult::UART_FLUSH_RESULT_SUCCESS;
}
void HostUartComponent::update_error_(const std::string &error) {
if (this->first_error_.empty()) {
this->first_error_ = error;
}
ESP_LOGE(TAG, "Port error: %s", error.c_str());
}
} // namespace esphome::uart
#endif // USE_HOST
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#ifdef USE_HOST
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "uart_component.h"
namespace esphome::uart {
class HostUartComponent final : public UARTComponent, public Component {
public:
virtual ~HostUartComponent();
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
UARTFlushResult flush() override;
void set_name(std::string port_name) { port_name_ = port_name; };
protected:
void update_error_(const std::string &error);
void check_logger_conflict() override {}
std::string port_name_;
std::string first_error_;
int file_descriptor_ = -1;
bool has_peek_{false};
uint8_t peek_byte_;
};
} // namespace esphome::uart
#endif // USE_HOST
@@ -0,0 +1,208 @@
#ifdef USE_LIBRETINY
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "uart_component_libretiny.h"
#include "esphome/components/libretiny/core.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
#if LT_ARD_HAS_SOFTSERIAL
#include <SoftwareSerial.h>
#endif
namespace esphome::uart {
static const char *const TAG = "uart";
static const char *const UART_TYPE[] = {
"hardware",
"software",
};
uint16_t LibreTinyUARTComponent::get_config() {
uint16_t config = 0;
switch (this->parity_) {
case UART_CONFIG_PARITY_NONE:
config |= SERIAL_PARITY_NONE;
break;
case UART_CONFIG_PARITY_EVEN:
config |= SERIAL_PARITY_EVEN;
break;
case UART_CONFIG_PARITY_ODD:
config |= SERIAL_PARITY_ODD;
break;
default:
break;
}
config |= (this->data_bits_ - 4) << 8;
config |= 0x10 + (this->stop_bits_ - 1) * 0x20;
return config;
}
void LibreTinyUARTComponent::setup() {
int16_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin();
int16_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin();
auto should_fallback_to_software_serial = [&]() -> bool {
auto has_flags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool {
return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE;
};
if (has_flags(this->tx_pin_,
gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) ||
has_flags(this->rx_pin_,
gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) {
#if LT_ARD_HAS_SOFTSERIAL
ESP_LOGI(TAG, "Pins have flags set. Using Software Serial");
return true;
#else
ESP_LOGW(TAG, "Pin flags are set but not supported for hardware serial. Ignoring");
#endif
}
return false;
};
if (false) { // NOLINT(readability-simplify-boolean-expr)
return;
}
#if LT_HW_UART0
else if ((((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) &&
(rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) ||
(tx_pin == 14 && rx_pin == 13)) &&
!should_fallback_to_software_serial()) {
this->serial_ = &Serial0;
this->hardware_idx_ = 0;
}
#endif
#if LT_HW_UART1
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX) &&
!should_fallback_to_software_serial()) {
this->serial_ = &Serial1;
this->hardware_idx_ = 1;
}
#endif
#if LT_HW_UART2
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX) &&
!should_fallback_to_software_serial()) {
this->serial_ = &Serial2;
this->hardware_idx_ = 2;
}
#endif
else {
#if LT_ARD_HAS_SOFTSERIAL
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted();
bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted();
this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted);
#else
this->serial_ = &Serial;
ESP_LOGE(TAG, " SoftwareSerial is not implemented for this chip. Only hardware pins are supported:");
#if LT_HW_UART0
ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL0_TX, PIN_SERIAL0_RX);
#endif
#if LT_HW_UART1
ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL1_TX, PIN_SERIAL1_RX);
#endif
#if LT_HW_UART2
ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL2_TX, PIN_SERIAL2_RX);
#endif
this->mark_failed(LOG_STR("SoftwareSerial is not implemented for this chip."));
return;
#endif
}
if (this->hardware_idx_ >= 0)
libretiny::ensure_serial_handle(this->hardware_idx_);
if (this->hardware_idx_ == 0 && tx_pin == 14 && rx_pin == 13) {
ESP_LOGI(TAG, "Using alternate UART0 pins RX=PA13 TX=PA14");
Serial0.setPins(rx_pin, tx_pin);
}
this->serial_->begin(this->baud_rate_, get_config());
}
void LibreTinyUARTComponent::dump_config() {
bool is_software = this->hardware_idx_ == -1;
ESP_LOGCONFIG(TAG,
"UART Bus:\n"
" Type: %s",
UART_TYPE[is_software]);
if (!is_software) {
ESP_LOGCONFIG(TAG, " Port number: %d", this->hardware_idx_);
}
LOG_PIN(" TX Pin: ", tx_pin_);
LOG_PIN(" RX Pin: ", rx_pin_);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_);
}
ESP_LOGCONFIG(TAG,
" Baud Rate: %" PRIu32 " baud\n"
" Data Bits: %u\n"
" Parity: %s\n"
" Stop bits: %u",
this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_);
this->check_logger_conflict();
}
void LibreTinyUARTComponent::write_array(const uint8_t *data, size_t len) {
this->serial_->write(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
}
bool LibreTinyUARTComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
*data = this->serial_->peek();
return true;
}
bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) {
if (!this->check_read_timeout_(len))
return false;
this->serial_->readBytes(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return true;
}
size_t LibreTinyUARTComponent::available() { return this->serial_->available(); }
UARTFlushResult LibreTinyUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
this->serial_->flush();
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
void LibreTinyUARTComponent::check_logger_conflict() {
#ifdef USE_LOGGER
if (this->hardware_idx_ == -1 || logger::global_logger->get_baud_rate() == 0) {
return;
}
if (this->serial_ == logger::global_logger->get_hw_serial()) {
ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please "
"disable logging over the serial port by setting logger->baud_rate to 0.");
}
#endif
}
} // namespace esphome::uart
#endif // USE_LIBRETINY
@@ -0,0 +1,40 @@
#pragma once
#ifdef USE_LIBRETINY
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "uart_component.h"
namespace esphome::uart {
class LibreTinyUARTComponent final : public UARTComponent, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
UARTFlushResult flush() override;
uint16_t get_config();
HardwareSerial *get_hw_serial() { return this->serial_; }
int8_t get_hw_serial_number() { return this->hardware_idx_; }
protected:
void check_logger_conflict() override;
HardwareSerial *serial_{nullptr};
int8_t hardware_idx_{-1};
};
} // namespace esphome::uart
#endif // USE_LIBRETINY
+218
View File
@@ -0,0 +1,218 @@
#ifdef USE_RP2
#include "uart_component_rp2.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <hardware/uart.h>
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
namespace esphome::uart {
static const char *const TAG = "uart";
uint16_t RP2UartComponent::get_config() {
uint16_t config = 0;
if (this->parity_ == UART_CONFIG_PARITY_NONE) {
config |= UART_PARITY_NONE;
} else if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
config |= UART_PARITY_EVEN;
} else if (this->parity_ == UART_CONFIG_PARITY_ODD) {
config |= UART_PARITY_ODD;
}
switch (this->data_bits_) {
case 5:
config |= SERIAL_DATA_5;
break;
case 6:
config |= SERIAL_DATA_6;
break;
case 7:
config |= SERIAL_DATA_7;
break;
case 8:
config |= SERIAL_DATA_8;
break;
}
if (this->stop_bits_ == 1) {
config |= SERIAL_STOP_BIT_1;
} else {
config |= SERIAL_STOP_BIT_2;
}
return config;
}
void RP2UartComponent::setup() {
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
}
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
pin->setup();
}
};
setup_pin_if_needed(this->rx_pin_);
if (this->rx_pin_ != this->tx_pin_) {
setup_pin_if_needed(this->tx_pin_);
}
uint16_t config = get_config();
constexpr uint32_t valid_tx_uart_0 = __bitset({0, 12, 16, 28});
constexpr uint32_t valid_tx_uart_1 = __bitset({4, 8, 20, 24});
constexpr uint32_t valid_rx_uart_0 = __bitset({1, 13, 17, 29});
constexpr uint32_t valid_rx_uart_1 = __bitset({5, 9, 21, 25});
int8_t tx_hw = -1;
int8_t rx_hw = -1;
if (this->tx_pin_ != nullptr) {
if (this->tx_pin_->is_inverted()) {
ESP_LOGD(TAG, "An inverted TX pin %u can only be used with SerialPIO", this->tx_pin_->get_pin());
} else {
if (((1 << this->tx_pin_->get_pin()) & valid_tx_uart_0) != 0) {
tx_hw = 0;
} else if (((1 << this->tx_pin_->get_pin()) & valid_tx_uart_1) != 0) {
tx_hw = 1;
} else {
ESP_LOGD(TAG, "TX pin %u can only be used with SerialPIO", this->tx_pin_->get_pin());
}
}
}
if (this->rx_pin_ != nullptr) {
if (this->rx_pin_->is_inverted()) {
ESP_LOGD(TAG, "An inverted RX pin %u can only be used with SerialPIO", this->rx_pin_->get_pin());
} else {
if (((1 << this->rx_pin_->get_pin()) & valid_rx_uart_0) != 0) {
rx_hw = 0;
} else if (((1 << this->rx_pin_->get_pin()) & valid_rx_uart_1) != 0) {
rx_hw = 1;
} else {
ESP_LOGD(TAG, "RX pin %u can only be used with SerialPIO", this->rx_pin_->get_pin());
}
}
}
// Determine which hardware UART to use. A pin that is not specified
// should not prevent hardware UART selection — one-way UART is valid.
// When both pins are configured, both must be HW-capable and agree on UART number.
// When only one pin is configured (nullptr other), use that pin's HW UART.
// If a pin is configured but not HW-capable (inverted/invalid), fall back to SerialPIO.
int8_t hw_uart = -1;
const bool tx_configured = (this->tx_pin_ != nullptr);
const bool rx_configured = (this->rx_pin_ != nullptr);
if (tx_configured && rx_configured) {
// Both pins configured — both must map to the same hardware UART
if (tx_hw != -1 && rx_hw != -1 && tx_hw == rx_hw) {
hw_uart = tx_hw;
}
} else if (tx_configured) {
hw_uart = tx_hw;
} else if (rx_configured) {
hw_uart = rx_hw;
}
#ifdef USE_LOGGER
if (hw_uart != -1 && logger::global_logger->get_uart() == hw_uart) {
ESP_LOGD(TAG, "Using SerialPIO as UART%d is taken by the logger", hw_uart);
hw_uart = -1;
}
#endif
if (hw_uart == -1) {
ESP_LOGV(TAG, "Using SerialPIO");
pin_size_t tx = this->tx_pin_ == nullptr ? NOPIN : this->tx_pin_->get_pin();
pin_size_t rx = this->rx_pin_ == nullptr ? NOPIN : this->rx_pin_->get_pin();
auto *serial = new SerialPIO(tx, rx, this->rx_buffer_size_); // NOLINT(cppcoreguidelines-owning-memory)
serial->begin(this->baud_rate_, config);
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted())
gpio_set_outover(tx, GPIO_OVERRIDE_INVERT);
if (this->rx_pin_ != nullptr && this->rx_pin_->is_inverted())
gpio_set_inover(rx, GPIO_OVERRIDE_INVERT);
this->serial_ = serial;
} else {
ESP_LOGV(TAG, "Using Hardware Serial");
SerialUART *serial;
if (hw_uart == 0) {
serial = &Serial1;
} else {
serial = &Serial2;
}
if (this->tx_pin_ != nullptr)
serial->setTX(this->tx_pin_->get_pin());
if (this->rx_pin_ != nullptr)
serial->setRX(this->rx_pin_->get_pin());
serial->setFIFOSize(this->rx_buffer_size_);
serial->begin(this->baud_rate_, config);
this->serial_ = serial;
this->hw_serial_ = true;
}
}
void RP2UartComponent::dump_config() {
ESP_LOGCONFIG(TAG, "UART Bus:");
LOG_PIN(" TX Pin: ", tx_pin_);
LOG_PIN(" RX Pin: ", rx_pin_);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_);
}
ESP_LOGCONFIG(TAG,
" Baud Rate: %u baud\n"
" Data Bits: %u\n"
" Parity: %s\n"
" Stop bits: %u",
this->baud_rate_, this->data_bits_, LOG_STR_ARG(parity_to_str(this->parity_)), this->stop_bits_);
if (this->hw_serial_) {
ESP_LOGCONFIG(TAG, " Using hardware serial");
} else {
ESP_LOGCONFIG(TAG, " Using SerialPIO");
}
}
void RP2UartComponent::write_array(const uint8_t *data, size_t len) {
this->serial_->write(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
}
bool RP2UartComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
*data = this->serial_->peek();
return true;
}
bool RP2UartComponent::read_array(uint8_t *data, size_t len) {
if (!this->check_read_timeout_(len))
return false;
this->serial_->readBytes(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return true;
}
size_t RP2UartComponent::available() { return this->serial_->available(); }
UARTFlushResult RP2UartComponent::flush() {
ESP_LOGVV(TAG, " Flushing");
this->serial_->flush();
return UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS;
}
} // namespace esphome::uart
#endif // USE_RP2
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#ifdef USE_RP2
#include <SerialPIO.h>
#include <SerialUART.h>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "uart_component.h"
namespace esphome::uart {
class RP2UartComponent final : public UARTComponent, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
UARTFlushResult flush() override;
uint16_t get_config();
bool is_hw_serial() { return this->hw_serial_; }
HardwareSerial *get_hw_serial() { return this->serial_; }
protected:
void check_logger_conflict() override {}
bool hw_serial_{false};
HardwareSerial *serial_{nullptr};
};
} // namespace esphome::uart
#endif // USE_RP2
+202
View File
@@ -0,0 +1,202 @@
#include "esphome/core/defines.h"
#ifdef USE_UART_DEBUGGER
#include <vector>
#include "uart_debugger.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::uart {
static const char *const TAG = "uart_debug";
UARTDebugger::UARTDebugger(UARTComponent *parent) {
parent->add_debug_callback([this](UARTDirection direction, uint8_t byte) {
if (!this->is_my_direction_(direction) || this->is_recursive_()) {
return;
}
this->trigger_after_direction_change_(direction);
this->store_byte_(direction, byte);
this->trigger_after_delimiter_(byte);
this->trigger_after_bytes_();
});
}
void UARTDebugger::loop() { this->trigger_after_timeout_(); }
bool UARTDebugger::is_my_direction_(UARTDirection direction) {
return this->for_direction_ == UART_DIRECTION_BOTH || this->for_direction_ == direction;
}
bool UARTDebugger::is_recursive_() { return this->is_triggering_; }
void UARTDebugger::trigger_after_direction_change_(UARTDirection direction) {
if (this->has_buffered_bytes_() && this->for_direction_ == UART_DIRECTION_BOTH &&
this->last_direction_ != direction) {
this->fire_trigger_();
}
}
void UARTDebugger::store_byte_(UARTDirection direction, uint8_t byte) {
this->bytes_.push_back(byte);
this->last_direction_ = direction;
this->last_time_ = millis();
}
void UARTDebugger::trigger_after_delimiter_(uint8_t byte) {
if (this->after_delimiter_.empty() || !this->has_buffered_bytes_()) {
return;
}
if (this->after_delimiter_[this->after_delimiter_pos_] != byte) {
this->after_delimiter_pos_ = 0;
return;
}
this->after_delimiter_pos_++;
if (this->after_delimiter_pos_ == this->after_delimiter_.size()) {
this->fire_trigger_();
this->after_delimiter_pos_ = 0;
}
}
void UARTDebugger::trigger_after_bytes_() {
if (this->has_buffered_bytes_() && this->after_bytes_ > 0 && this->bytes_.size() >= this->after_bytes_) {
this->fire_trigger_();
}
}
void UARTDebugger::trigger_after_timeout_() {
if (this->has_buffered_bytes_() && this->after_timeout_ > 0 && millis() - this->last_time_ >= this->after_timeout_) {
this->fire_trigger_();
}
}
bool UARTDebugger::has_buffered_bytes_() { return !this->bytes_.empty(); }
void UARTDebugger::fire_trigger_() {
this->is_triggering_ = true;
trigger(this->last_direction_, this->bytes_, this->debug_prefix_);
this->bytes_.clear();
this->is_triggering_ = false;
}
void UARTDummyReceiver::loop() {
// Reading up to a limited number of bytes, to make sure that this loop()
// won't lock up the system on a continuous incoming stream of bytes.
uint8_t data;
int count = 50;
while (this->available() && count--) {
this->read_byte(&data);
}
}
// In the upcoming log functions, a delay was added after all log calls.
// This is done to allow the system to ship the log lines via the API
// TCP connection(s). Without these delays, debug log lines could go
// missing when UART devices block the main loop for too long.
void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) {
std::string res;
if (direction == UART_DIRECTION_RX) {
res += "<<< ";
} else {
res += ">>> ";
}
size_t len = bytes.size();
char buf[5];
for (size_t i = 0; i < len; i++) {
if (i > 0) {
res += separator;
}
buf_append_printf(buf, sizeof(buf), 0, "%02X", bytes[i]);
res += buf;
}
ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str());
delay(10);
}
void UARTDebug::log_string(UARTDirection direction, std::vector<uint8_t> bytes, StringRef prefix) {
std::string res;
if (direction == UART_DIRECTION_RX) {
res += "<<< \"";
} else {
res += ">>> \"";
}
size_t len = bytes.size();
char buf[5];
for (size_t i = 0; i < len; i++) {
if (bytes[i] == 7) {
res += "\\a";
} else if (bytes[i] == 8) {
res += "\\b";
} else if (bytes[i] == 9) {
res += "\\t";
} else if (bytes[i] == 10) {
res += "\\n";
} else if (bytes[i] == 11) {
res += "\\v";
} else if (bytes[i] == 12) {
res += "\\f";
} else if (bytes[i] == 13) {
res += "\\r";
} else if (bytes[i] == 27) {
res += "\\e";
} else if (bytes[i] == 34) {
res += "\\\"";
} else if (bytes[i] == 39) {
res += "\\'";
} else if (bytes[i] == 92) {
res += "\\\\";
} else if (bytes[i] < 32 || bytes[i] > 127) {
buf_append_printf(buf, sizeof(buf), 0, "\\x%02X", bytes[i]);
res += buf;
} else {
res += bytes[i];
}
}
res += '"';
ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str());
delay(10);
}
void UARTDebug::log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) {
std::string res;
size_t len = bytes.size();
if (direction == UART_DIRECTION_RX) {
res += "<<< ";
} else {
res += ">>> ";
}
char buf[4]; // max 3 digits for uint8_t (255) + null
for (size_t i = 0; i < len; i++) {
if (i > 0) {
res += separator;
}
buf_append_printf(buf, sizeof(buf), 0, "%u", bytes[i]);
res += buf;
}
ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str());
delay(10);
}
void UARTDebug::log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator, StringRef prefix) {
std::string res;
size_t len = bytes.size();
if (direction == UART_DIRECTION_RX) {
res += "<<< ";
} else {
res += ">>> ";
}
char buf[20];
for (size_t i = 0; i < len; i++) {
if (i > 0) {
res += separator;
}
buf_append_printf(buf, sizeof(buf), 0, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]);
res += buf;
}
ESP_LOGD(TAG, "%s%s", prefix.c_str(), res.c_str());
delay(10);
}
} // namespace esphome::uart
#endif
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_UART_DEBUGGER
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/string_ref.h"
#include "uart.h"
#include "uart_component.h"
namespace esphome::uart {
/// The UARTDebugger class adds debugging support to a UART bus.
///
/// It accumulates bytes that travel over the UART bus and triggers one or
/// more actions that can log the data at an appropriate time. What
/// 'appropriate time' means exactly, is determined by a number of
/// configurable constraints. E.g. when a given number of bytes is gathered
/// and/or when no more data has been seen for a given time interval.
class UARTDebugger final : public Component, public Trigger<UARTDirection, std::vector<uint8_t>, StringRef> {
public:
explicit UARTDebugger(UARTComponent *parent);
void loop() override;
/// Set the direction in which to inspect the bytes: incoming, outgoing
/// or both. When debugging in both directions, logging will be triggered
/// when the direction of the data stream changes.
void set_direction(UARTDirection direction) { this->for_direction_ = direction; }
/// Set the maximum number of bytes to accumulate. When the number of bytes
/// is reached, logging will be triggered.
void set_after_bytes(size_t size) { this->after_bytes_ = size; }
/// Set a timeout for the data stream. When no new bytes are seen during
/// this timeout, logging will be triggered.
void set_after_timeout(uint32_t timeout) { this->after_timeout_ = timeout; }
/// Add a delimiter byte. This can be called multiple times to setup a
/// multi-byte delimiter (a typical example would be '\r\n').
/// When the constructed byte sequence is found in the data stream,
/// logging will be triggered.
void add_delimiter_byte(uint8_t byte) { this->after_delimiter_.push_back(byte); }
void set_debug_prefix(const char *prefix) { this->debug_prefix_ = StringRef(prefix); }
protected:
UARTDirection for_direction_;
UARTDirection last_direction_{};
std::vector<uint8_t> bytes_{};
size_t after_bytes_;
uint32_t after_timeout_;
uint32_t last_time_{};
std::vector<uint8_t> after_delimiter_{};
size_t after_delimiter_pos_{};
bool is_triggering_{false};
StringRef debug_prefix_{};
bool is_my_direction_(UARTDirection direction);
bool is_recursive_();
void store_byte_(UARTDirection direction, uint8_t byte);
void trigger_after_direction_change_(UARTDirection direction);
void trigger_after_delimiter_(uint8_t byte);
void trigger_after_bytes_();
void trigger_after_timeout_();
bool has_buffered_bytes_();
void fire_trigger_();
};
/// This UARTDevice is used by the serial debugger to read data from a
/// serial interface when the 'dummy_receiver' option is enabled.
/// The data are not stored, nor processed. This is most useful when the
/// debugger is used to reverse engineer a serial protocol, for which no
/// specific UARTDevice implementation exists (yet), but for which the
/// incoming bytes must be read to drive the debugger.
class UARTDummyReceiver final : public Component, public UARTDevice {
public:
UARTDummyReceiver(UARTComponent *parent) : UARTDevice(parent) {}
void loop() override;
};
/// This class contains some static methods, that can be used to easily
/// create a logging action for the debugger.
class UARTDebug {
public:
/// Log the bytes as hex values, separated by the provided separator
/// character.
static void log_hex(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator,
StringRef prefix = StringRef());
/// Log the bytes as string values, escaping unprintable characters.
static void log_string(UARTDirection direction, std::vector<uint8_t> bytes, StringRef prefix = StringRef());
/// Log the bytes as integer values, separated by the provided separator
/// character.
static void log_int(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator,
StringRef prefix = StringRef());
/// Log the bytes as '<binary> (<hex>)' values, separated by the provided
/// separator.
static void log_binary(UARTDirection direction, std::vector<uint8_t> bytes, uint8_t separator,
StringRef prefix = StringRef());
};
} // namespace esphome::uart
#endif
+12
View File
@@ -0,0 +1,12 @@
# Copy this to your ESPHome server's config folder. Everything else comes from
# the repo, and the values for the secrets below come from the server's own
# secrets.yaml: wifi_ssid, wifi_password, ap_password, api_encryption_key.
substitutions:
timezone: "Europe/Amsterdam"
packages:
lamp:
url: https://mangavalk.com/mangavalk/esphome-wake-up-light-ii.git
ref: v1.0.0
files: [wake-up-light-ii.yaml]
refresh: 1d
+759 -213
View File
File diff suppressed because it is too large. Load diff