Files
mangavalkandClaude Opus 5.5 a8f6bb3ca5 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>
2026-11-05 12:27:54 +01:00

417 lines
14 KiB
C++

#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