mirror of
https://github.com/esphome/esphome.git
synced 2026-11-15 17:45:57 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b310c985e0 | ||
|
|
7bc87b9b00 | ||
|
|
a5c1b4f532 | ||
|
|
8fc6fcf724 | ||
|
|
22a2efd8fd | ||
|
|
e0354ae794 | ||
|
|
f9299c1c58 | ||
|
|
09c40438e2 | ||
|
|
63df68acc4 | ||
|
|
8936e4f54b | ||
|
|
1c1cf21b16 | ||
|
|
71a0b9cfca | ||
|
|
ebc4dd2cc9 | ||
|
|
8f705e7cdb | ||
|
|
c2067f4452 | ||
|
|
e1b38f5171 | ||
|
|
ef10ea99b9 |
No files matched your search
+1
-3
@@ -149,7 +149,6 @@ esphome/components/cst226/* @clydebarrow
|
||||
esphome/components/cst328/* @latonita
|
||||
esphome/components/cst816/* @clydebarrow
|
||||
esphome/components/cst9220/* @clydebarrow
|
||||
esphome/components/cstxxx/* @clydebarrow @latonita
|
||||
esphome/components/ct_clamp/* @jesserockz
|
||||
esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/cw2017/* @exciton
|
||||
@@ -467,7 +466,6 @@ esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/noise/* @esphome/core
|
||||
esphome/components/npi19/* @bakerkj
|
||||
esphome/components/npm1300/* @tomaszduda23
|
||||
esphome/components/nrf52/* @tomaszduda23
|
||||
esphome/components/ns2009/* @tchilov
|
||||
esphome/components/ns2009/touchscreen/* @tchilov
|
||||
@@ -743,7 +741,7 @@ esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/weikai/* @DrCoolZic
|
||||
esphome/components/weikai_i2c/* @DrCoolZic
|
||||
esphome/components/weikai_spi/* @DrCoolZic
|
||||
esphome/components/whirlpool/* @arpena @glmnet @Rodrigoah
|
||||
esphome/components/whirlpool/* @arpena @glmnet
|
||||
esphome/components/whynter/* @aeonsablaze
|
||||
esphome/components/wiegand/* @ssieb
|
||||
esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard
|
||||
|
||||
@@ -114,10 +114,10 @@ void BME280Component::setup() {
|
||||
this->calibration_.h1 = read_u8_(BME280_REGISTER_DIG_H1);
|
||||
this->calibration_.h2 = read_s16_le_(BME280_REGISTER_DIG_H2);
|
||||
this->calibration_.h3 = read_u8_(BME280_REGISTER_DIG_H3);
|
||||
this->calibration_.h4 = sign_extend<12>(
|
||||
encode_uint12(read_u8_(BME280_REGISTER_DIG_H4), low_nibble(read_u8_(BME280_REGISTER_DIG_H4 + 1))));
|
||||
this->calibration_.h5 = sign_extend<12>(
|
||||
encode_uint12(read_u8_(BME280_REGISTER_DIG_H5 + 1), high_nibble(read_u8_(BME280_REGISTER_DIG_H5))));
|
||||
this->calibration_.h4 =
|
||||
sign_extend<12>(read_u8_(BME280_REGISTER_DIG_H4) << 4 | (read_u8_(BME280_REGISTER_DIG_H4 + 1) & 0x0F));
|
||||
this->calibration_.h5 =
|
||||
sign_extend<12>(read_u8_(BME280_REGISTER_DIG_H5 + 1) << 4 | (read_u8_(BME280_REGISTER_DIG_H5) >> 4));
|
||||
this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6);
|
||||
|
||||
uint8_t humid_control_val = 0;
|
||||
|
||||
@@ -70,9 +70,8 @@ bool BME680Component::setup_registers_() {
|
||||
this->calibration_.t2 = encode_value_le<int16_t>(&coeff1[1]);
|
||||
this->calibration_.t3 = coeff1[3];
|
||||
|
||||
// H1 and H2 are 12-bit values sharing byte 1: its low nibble ends H1, its high nibble ends H2
|
||||
this->calibration_.h1 = encode_uint12(coeff2[2], low_nibble(coeff2[1]));
|
||||
this->calibration_.h2 = encode_uint12(coeff2[0], high_nibble(coeff2[1]));
|
||||
this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F);
|
||||
this->calibration_.h2 = encode_uint16(coeff2[0], coeff2[1]) >> 4;
|
||||
this->calibration_.h3 = coeff2[3];
|
||||
this->calibration_.h4 = coeff2[4];
|
||||
this->calibration_.h5 = coeff2[5];
|
||||
@@ -244,7 +243,7 @@ void BME680Component::read_data_() {
|
||||
uint32_t raw_pressure = encode_uint24(data[2], data[3], data[4]) >> 4;
|
||||
uint32_t raw_humidity = encode_uint16(data[8], data[9]);
|
||||
uint16_t raw_gas = encode_uint16(data[13], data[14]) >> 6;
|
||||
uint8_t gas_range = low_nibble(data[14]);
|
||||
uint8_t gas_range = data[14] & 0x0F;
|
||||
|
||||
float temperature = this->calc_temperature_(raw_temperature);
|
||||
float pressure = this->calc_pressure_(raw_pressure);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
@@ -15,7 +13,6 @@ from esphome.const import (
|
||||
CONF_VALUE,
|
||||
CONF_WAIT_TIME,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@lygris", "@gabest11"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
@@ -25,8 +22,6 @@ MULTI_CONF = True
|
||||
ns = cg.esphome_ns.namespace("cc1101")
|
||||
CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice)
|
||||
CC1101Listener = ns.class_("CC1101Listener")
|
||||
PACKET_TYPE = cg.std_vector.template(cg.uint8)
|
||||
RX_FIFO_SIZE = 64
|
||||
|
||||
# Config keys
|
||||
CONF_RX_ATTENUATION = "rx_attenuation"
|
||||
@@ -246,34 +241,20 @@ CONFIG_MAP = {
|
||||
cv.Optional(CONF_BS_POST_KI): cv.enum(BS_POST_KI, upper=False),
|
||||
cv.Optional(CONF_BS_POST_KP): cv.enum(BS_POST_KP, upper=False),
|
||||
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
def _is_long_packet(packet_length: Any) -> bool:
|
||||
"""A packet_length that does not fit the RX FIFO, or is a lambda."""
|
||||
return cg.is_template(packet_length) or (packet_length or 0) > RX_FIFO_SIZE
|
||||
|
||||
|
||||
def _validate_packet_mode(config: ConfigType) -> ConfigType:
|
||||
packet_mode = config.get(CONF_PACKET_MODE, False)
|
||||
packet_length = config.get(CONF_PACKET_LENGTH)
|
||||
if packet_mode:
|
||||
def _validate_packet_mode(config):
|
||||
if config.get(CONF_PACKET_MODE, False):
|
||||
if CONF_GDO0_PIN not in config:
|
||||
raise cv.Invalid("gdo0_pin is required when packet_mode is enabled")
|
||||
if packet_length is None:
|
||||
if CONF_PACKET_LENGTH not in config:
|
||||
raise cv.Invalid("packet_length is required when packet_mode is enabled")
|
||||
elif cg.is_template(packet_length):
|
||||
raise cv.Invalid("packet_mode is required when packet_length is a lambda")
|
||||
if (
|
||||
packet_mode
|
||||
and _is_long_packet(packet_length)
|
||||
and config.get(CONF_CRC_ENABLE, False)
|
||||
):
|
||||
raise cv.Invalid(
|
||||
"crc_enable is not supported with packet_length > 64 or a lambda"
|
||||
)
|
||||
if config[CONF_PACKET_LENGTH] > 64:
|
||||
raise cv.Invalid("packet_length must be <= 64 (FIFO size)")
|
||||
return config
|
||||
|
||||
|
||||
@@ -282,9 +263,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CC1101Component),
|
||||
cv.Optional(CONF_GDO0_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_PACKET_LENGTH): cv.templatable(
|
||||
cv.int_range(min=0, max=1024)
|
||||
),
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
@@ -304,24 +282,11 @@ async def to_code(config):
|
||||
cg.add(getattr(var, f"set_{key}")(config[key]))
|
||||
|
||||
await cg.add_pin(var.set_gdo0_pin, config.get(CONF_GDO0_PIN))
|
||||
if (packet_length := config.get(CONF_PACKET_LENGTH)) is not None:
|
||||
# packet_mode can also be switched on at run time, so this does not check it
|
||||
if _is_long_packet(packet_length):
|
||||
cg.add_define("USE_CC1101_LONG_PACKET")
|
||||
if cg.is_template(packet_length):
|
||||
lambda_ = await cg.process_lambda(
|
||||
packet_length,
|
||||
[(PACKET_TYPE.operator("ref").operator("const"), "x")],
|
||||
return_type=cg.int32,
|
||||
)
|
||||
cg.add(var.set_packet_length_lambda(lambda_))
|
||||
else:
|
||||
cg.add(var.set_packet_length(packet_length))
|
||||
if CONF_ON_PACKET in config:
|
||||
await automation.build_automation(
|
||||
var.get_packet_trigger(),
|
||||
[
|
||||
(PACKET_TYPE, "x"),
|
||||
(cg.std_vector.template(cg.uint8), "x"),
|
||||
(cg.float_, "freq_offset"),
|
||||
(cg.float_, "rssi"),
|
||||
(cg.uint8, "lqi"),
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#include "cc1101.h"
|
||||
#include "cc1101pa.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
@@ -16,15 +14,9 @@ namespace esphome::cc1101 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cc1101");
|
||||
static constexpr size_t TX_FIFO_SIZE = 64;
|
||||
static constexpr size_t RX_FIFO_SIZE = 64;
|
||||
// RX FIFO threshold for FIFO_THR = 7 in long packet mode
|
||||
static constexpr uint8_t RX_FIFO_THRESHOLD = 32;
|
||||
// The crystal takes about 150 us to start after CSn goes low
|
||||
static constexpr uint32_t WAKEUP_DELAY_US = 200;
|
||||
|
||||
static float freq_offset_hz(uint8_t freqest) { return static_cast<int8_t>(freqest) * (XTAL_FREQUENCY / (1 << 14)); }
|
||||
static float rssi_dbm(int8_t rssi) { return (rssi * RSSI_STEP) - RSSI_OFFSET; }
|
||||
|
||||
// MDMCFG2 MOD_FORMAT values
|
||||
PROGMEM_STRING_TABLE(ModulationStrings, "2-FSK", "GFSK", "UNUSED", "ASK/OOK", "4-FSK", "UNUSED", "UNUSED", "MSK");
|
||||
|
||||
@@ -122,6 +114,8 @@ CC1101Component::CC1101Component() {
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
}
|
||||
|
||||
void IRAM_ATTR CC1101Component::gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
|
||||
@@ -199,220 +193,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
|
||||
this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi);
|
||||
}
|
||||
|
||||
float CC1101Component::symbol_rate_() const {
|
||||
return (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
|
||||
}
|
||||
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
bool CC1101Component::is_long_packet_mode_() const {
|
||||
return this->packet_mode_ && (this->packet_length_ > RX_FIFO_SIZE || this->packet_length_func_ != nullptr);
|
||||
}
|
||||
|
||||
void CC1101Component::arm_length_(uint16_t length) {
|
||||
// Over 255 bytes or unknown starts in infinite mode (datasheet section 15.2.2). PKTLEN must not be 0, so a
|
||||
// multiple of 256 stays in infinite mode and ends on the MCU byte count alone.
|
||||
this->packet_expected_length_ = length;
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(length > 0 && length < 256 ? LengthConfig::LENGTH_CONFIG_FIXED
|
||||
: LengthConfig::LENGTH_CONFIG_INFINITE);
|
||||
this->state_.PKTLEN = length % 256 == 0 ? 0xFF : static_cast<uint8_t>(length % 256);
|
||||
}
|
||||
|
||||
void CC1101Component::reset_long_packet_rx_() {
|
||||
this->packet_.clear();
|
||||
this->packet_last_byte_ms_ = 0;
|
||||
this->high_freq_.stop();
|
||||
this->arm_length_(this->packet_length_func_ != nullptr ? 0 : this->packet_length_);
|
||||
}
|
||||
|
||||
void CC1101Component::restart_long_packet_rx_() {
|
||||
this->enter_idle_();
|
||||
this->enter_rx_();
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void CC1101Component::maybe_switch_long_packet_to_fixed_() {
|
||||
const uint16_t expected = this->packet_expected_length_;
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED) || expected == 0 ||
|
||||
expected % 256 == 0) {
|
||||
return;
|
||||
}
|
||||
const uint16_t received = this->packet_.size();
|
||||
if (expected - received >= 256) {
|
||||
return;
|
||||
}
|
||||
// PKTLEN keeps the (length % 256) set at the start, as in TI Design Note DN500 section 2.3
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->write_(Register::PKTCTRL0);
|
||||
ESP_LOGVV(TAG, "Switched long packet RX to fixed mode: received=%u expected=%u pktlen=%u", received, expected,
|
||||
this->state_.PKTLEN);
|
||||
}
|
||||
|
||||
void CC1101Component::finish_long_packet_() {
|
||||
this->packet_.resize(this->packet_expected_length_);
|
||||
this->read_(Register::FREQEST);
|
||||
this->read_(Register::LQI);
|
||||
const float freq_offset = freq_offset_hz(this->state_.FREQEST);
|
||||
const uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
||||
// No CRC check: crc_enable is rejected for long packets, CRC_OK is only set after the last payload byte.
|
||||
// Restart RX before dispatching so a callback may change settings; swapping back keeps the reserved capacity
|
||||
// unless a callback reserved a larger buffer.
|
||||
std::vector<uint8_t> packet;
|
||||
packet.swap(this->packet_);
|
||||
this->restart_long_packet_rx_();
|
||||
this->call_listeners_(packet, freq_offset, rssi_dbm(this->long_packet_rssi_), lqi);
|
||||
if (packet.capacity() > this->packet_.capacity()) {
|
||||
packet.clear();
|
||||
this->packet_.swap(packet);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::handle_long_packet_(uint8_t rx_bytes) {
|
||||
// Leave one byte in the FIFO unless it holds the rest of the packet (datasheet section 20)
|
||||
uint16_t to_read = rx_bytes > 0 ? rx_bytes - 1 : 0;
|
||||
if (this->packet_expected_length_ > 0) {
|
||||
const uint16_t remaining = this->packet_expected_length_ - this->packet_.size();
|
||||
if (remaining <= rx_bytes) {
|
||||
to_read = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
if (to_read == 0) {
|
||||
if (this->packet_.empty()) {
|
||||
return;
|
||||
}
|
||||
// Discard a packet when no byte arrived for the air time of PACKET_IDLE_TIMEOUT_BYTES
|
||||
const uint32_t timeout_ms =
|
||||
std::max(static_cast<uint32_t>(std::ceil((8.0f * PACKET_IDLE_TIMEOUT_BYTES * 1000.0f) / this->symbol_rate_())),
|
||||
PACKET_IDLE_TIMEOUT_MIN_MS);
|
||||
if (App.get_loop_component_start_time() - this->packet_last_byte_ms_ > timeout_ms) {
|
||||
ESP_LOGW(TAG, "Packet receive timeout, discarding packet: received=%u expected=%u fifo=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
|
||||
this->restart_long_packet_rx_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t old_size = this->packet_.size();
|
||||
if (old_size == 0) {
|
||||
// The RSSI register follows the live signal; sample it while the packet is still on air
|
||||
this->read_(Register::RSSI);
|
||||
this->long_packet_rssi_ = this->state_.RSSI;
|
||||
// The FIFO threshold signal does not fire for the bytes after the last threshold crossing; poll until done
|
||||
this->high_freq_.start();
|
||||
}
|
||||
// Read through an aligned buffer: for an unaligned target the ESP32 SPI driver allocates a bounce buffer
|
||||
alignas(4) uint8_t staged[RX_FIFO_SIZE];
|
||||
this->read_(Register::FIFO, staged, to_read);
|
||||
this->packet_.resize(old_size + to_read);
|
||||
std::memcpy(this->packet_.data() + old_size, staged, to_read);
|
||||
this->packet_last_byte_ms_ = App.get_loop_component_start_time();
|
||||
ESP_LOGVV(TAG, "Drained RX FIFO: read=%u received=%u expected=%u fifo=%u", to_read,
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
|
||||
|
||||
if (this->packet_expected_length_ == 0 && this->packet_length_func_ != nullptr) {
|
||||
const int32_t length = this->packet_length_func_(this->packet_);
|
||||
if (length < 0) {
|
||||
ESP_LOGVV(TAG, "Packet length lambda discarded packet: received=%u", static_cast<uint16_t>(this->packet_.size()));
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
if (length > MAX_PACKET_LENGTH) {
|
||||
ESP_LOGW(TAG, "Packet length exceeds maximum, discarding packet: length=%" PRId32 " max=%u", length,
|
||||
MAX_PACKET_LENGTH);
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
if (length > 0) {
|
||||
this->arm_length_(static_cast<uint16_t>(length));
|
||||
// A packet shorter than the first FIFO drain is already complete; finish_long_packet_() truncates it
|
||||
if (this->packet_expected_length_ <= this->packet_.size()) {
|
||||
this->finish_long_packet_();
|
||||
return;
|
||||
}
|
||||
this->write_(Register::PKTLEN);
|
||||
this->write_(Register::PKTCTRL0);
|
||||
ESP_LOGVV(TAG, "Resolved packet length: received=%u expected=%u pktlen=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, this->state_.PKTLEN);
|
||||
}
|
||||
}
|
||||
|
||||
// Only reachable while the length is still unknown; a known length caps every read
|
||||
if (this->packet_.size() > MAX_PACKET_LENGTH) {
|
||||
ESP_LOGW(TAG, "Packet length exceeds maximum, discarding packet: length=%u max=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), MAX_PACKET_LENGTH);
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
|
||||
this->maybe_switch_long_packet_to_fixed_();
|
||||
|
||||
if (this->packet_expected_length_ > 0 && this->packet_.size() >= this->packet_expected_length_) {
|
||||
this->finish_long_packet_();
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::loop_long_packet_() {
|
||||
if (this->sleeping_ || this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) ||
|
||||
this->gdo0_pin_ == nullptr) {
|
||||
this->high_freq_.stop();
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
// While GDO0 (RX FIFO threshold) is high, the FIFO holds at least RX_FIFO_THRESHOLD bytes, so drain without
|
||||
// reading RXBYTES. At high data rates RXBYTES changes between two reads, so waiting for two reads that agree
|
||||
// and retrying on the next loop pass lets the FIFO overflow. MARCSTATE does not change during RX.
|
||||
for (uint8_t i = 0; i < RX_FIFO_SIZE / RX_FIFO_THRESHOLD + 2 && this->gdo0_pin_->digital_read(); i++) {
|
||||
this->read_(Register::MARCSTATE);
|
||||
if (static_cast<State>(this->state_.MARC_STATE) == State::RXFIFO_OVERFLOW) {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, discarding packet: received=%u expected=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_);
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
this->handle_long_packet_(RX_FIFO_THRESHOLD);
|
||||
if (this->packet_.empty()) {
|
||||
return; // finished or discarded
|
||||
}
|
||||
}
|
||||
// Below the threshold the next crossing raises GDO0 again, and after the packet the count no longer changes,
|
||||
// so retrying on the next loop pass is safe here. RXBYTES can be misread while bytes arrive (TI SWRZ020), so
|
||||
// only trust two consecutive reads that agree.
|
||||
this->read_(Register::RXBYTES);
|
||||
uint8_t rx_bytes = this->state_.RXBYTES;
|
||||
bool stable = false;
|
||||
for (uint8_t retries = RXBYTES_STABLE_RETRIES; retries > 0 && !stable; retries--) {
|
||||
const uint8_t previous = rx_bytes;
|
||||
this->read_(Register::RXBYTES);
|
||||
rx_bytes = this->state_.RXBYTES;
|
||||
stable = rx_bytes == previous;
|
||||
}
|
||||
if (!stable) {
|
||||
return;
|
||||
}
|
||||
rx_bytes = this->state_.NUM_RXBYTES;
|
||||
// More than the FIFO holds can only be a misread; the staged drain buffer is FIFO sized
|
||||
if (this->state_.RXFIFO_OVERFLOW || rx_bytes > RX_FIFO_SIZE) {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, discarding packet: received=%u expected=%u fifo=%u",
|
||||
static_cast<uint16_t>(this->packet_.size()), this->packet_expected_length_, rx_bytes);
|
||||
this->restart_long_packet_rx_();
|
||||
return;
|
||||
}
|
||||
if (rx_bytes == 0 && this->packet_.empty() && !this->gdo0_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
return;
|
||||
}
|
||||
this->handle_long_packet_(rx_bytes);
|
||||
}
|
||||
#endif
|
||||
|
||||
void CC1101Component::loop() {
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->is_long_packet_mode_()) {
|
||||
this->loop_long_packet_();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->disable_loop();
|
||||
if (this->sleeping_ || this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) ||
|
||||
this->gdo0_pin_ == nullptr || !this->gdo0_pin_->digital_read()) {
|
||||
@@ -440,7 +221,7 @@ void CC1101Component::loop() {
|
||||
payload_length = this->state_.PKTLEN;
|
||||
expected_rx = payload_length;
|
||||
}
|
||||
if (payload_length == 0 || payload_length > RX_FIFO_SIZE || rx_bytes != expected_rx) {
|
||||
if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) {
|
||||
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
@@ -454,8 +235,8 @@ void CC1101Component::loop() {
|
||||
this->read_(Register::FREQEST);
|
||||
this->read_(Register::RSSI);
|
||||
this->read_(Register::LQI);
|
||||
float freq_offset = freq_offset_hz(this->state_.FREQEST);
|
||||
float rssi = rssi_dbm(this->state_.RSSI);
|
||||
float freq_offset = static_cast<int8_t>(this->state_.FREQEST) * (XTAL_FREQUENCY / (1 << 14));
|
||||
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
|
||||
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||
@@ -475,7 +256,7 @@ void CC1101Component::loop() {
|
||||
void CC1101Component::dump_config() {
|
||||
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
|
||||
XTAL_FREQUENCY / (1 << 16);
|
||||
float symbol_rate = this->symbol_rate_();
|
||||
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
|
||||
float bw = XTAL_FREQUENCY / (8.0f * (4 + this->state_.CHANBW_M) * (1 << this->state_.CHANBW_E));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CC1101:\n"
|
||||
@@ -490,20 +271,6 @@ void CC1101Component::dump_config() {
|
||||
LOG_STR_ARG(ModulationStrings::get_log_str(this->state_.MOD_FORMAT & 0x07, 0)), symbol_rate, bw,
|
||||
this->output_power_effective_);
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" GDO0 Pin: ", this->gdo0_pin_);
|
||||
|
||||
if (this->packet_mode_) {
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->packet_length_func_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Packet Length: lambda");
|
||||
} else
|
||||
#endif
|
||||
if (this->packet_length_ == 0) {
|
||||
ESP_LOGCONFIG(TAG, " Packet Length: variable");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Packet Length: %u", this->packet_length_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::begin_tx() {
|
||||
@@ -549,14 +316,6 @@ void CC1101Component::reset() {
|
||||
void CC1101Component::set_idle() {
|
||||
ESP_LOGV(TAG, "Setting IDLE state");
|
||||
this->enter_idle_();
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->is_long_packet_mode_()) {
|
||||
// Drop a partial packet so a pending loop pass cannot finish it and re-enter RX
|
||||
this->strobe_(Command::FRX);
|
||||
this->reset_long_packet_rx_();
|
||||
this->disable_loop();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void CC1101Component::power_down() {
|
||||
@@ -634,18 +393,7 @@ void CC1101Component::enter_idle_() {
|
||||
}
|
||||
}
|
||||
|
||||
bool CC1101Component::enter_rx_() {
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->is_long_packet_mode_()) {
|
||||
// Every RX entry starts a fresh long packet; callers leave the chip in IDLE, where SFRX is allowed
|
||||
this->strobe_(Command::FRX);
|
||||
this->reset_long_packet_rx_();
|
||||
this->write_(Register::PKTLEN);
|
||||
this->write_(Register::PKTCTRL0);
|
||||
}
|
||||
#endif
|
||||
return this->enter_calibrated_(State::RX, Command::RX);
|
||||
}
|
||||
bool CC1101Component::enter_rx_() { return this->enter_calibrated_(State::RX, Command::RX); }
|
||||
|
||||
bool CC1101Component::enter_tx_() { return this->enter_calibrated_(State::TX, Command::TX); }
|
||||
|
||||
@@ -708,22 +456,12 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
|
||||
|
||||
CC1101Error CC1101Component::transmit_packet(const uint8_t *data, size_t len) {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
ESP_LOGW(TAG, "Cannot transmit packet: packet mode is not FIFO");
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
// Long packet TX needs chunked FIFO refills and is not implemented
|
||||
if (this->is_long_packet_mode_()) {
|
||||
ESP_LOGW(TAG, "Cannot transmit packet: long packet mode is RX-only");
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
#endif
|
||||
const bool variable = this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
// In variable length mode the FIFO also holds the length byte
|
||||
if (len + (variable ? 1 : 0) > TX_FIFO_SIZE) {
|
||||
ESP_LOGW(TAG,
|
||||
"Cannot transmit packet: long packet TX is not implemented, payload length %u exceeds TX FIFO limit %u",
|
||||
static_cast<unsigned>(len), static_cast<unsigned>(TX_FIFO_SIZE));
|
||||
ESP_LOGE(TAG, "Packet of %u bytes does not fit the TX FIFO", static_cast<unsigned>(len));
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
|
||||
@@ -1096,60 +834,19 @@ void CC1101Component::set_bs_post_kp(BsPostKp value) {
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::configure_packet_mode_() {
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
// Ends a long reception in progress, also when switching to a short packet mode
|
||||
this->high_freq_.stop();
|
||||
this->packet_.clear();
|
||||
#endif
|
||||
if (!this->packet_mode_) {
|
||||
this->state_.PKT_FORMAT = static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
} else {
|
||||
this->state_.PKT_FORMAT = static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO);
|
||||
this->state_.APPEND_STATUS = 0;
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
if (this->is_long_packet_mode_()) {
|
||||
// Threshold-only signal, reasserts after each partial FIFO drain
|
||||
this->state_.GDO0_CFG = 0x00;
|
||||
this->state_.FIFO_THR = 7;
|
||||
// Reserve once so receiving never allocates; a lambda may read one FIFO drain past MAX_PACKET_LENGTH
|
||||
this->packet_.reserve(this->packet_length_func_ != nullptr ? MAX_PACKET_LENGTH + RX_FIFO_SIZE
|
||||
: this->packet_length_);
|
||||
this->reset_long_packet_rx_();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->state_.GDO0_CFG = 0x01;
|
||||
this->state_.FIFO_THR = 15;
|
||||
if (this->packet_length_ == 0) {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
} else {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->state_.PKTLEN = static_cast<uint8_t>(this->packet_length_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
this->write_(Register::PKTLEN);
|
||||
this->write_(Register::IOCFG0);
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_mode(bool value) {
|
||||
this->packet_mode_ = value;
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
// Long packet RX must drop the FIFO along with a partial packet, so go through IDLE and restart RX
|
||||
if (this->initialized_ && this->is_long_packet_mode_()) {
|
||||
this->reconfigure_packet_length_();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->configure_packet_mode_();
|
||||
this->state_.PKT_FORMAT =
|
||||
static_cast<uint8_t>(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
|
||||
if (value) {
|
||||
// Configure GDO0 for FIFO status (asserts on RX FIFO threshold or end of packet)
|
||||
this->state_.GDO0_CFG = 0x01;
|
||||
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
|
||||
this->state_.FIFO_THR = 15;
|
||||
// Don't append status bytes to FIFO - we read from registers instead
|
||||
this->state_.APPEND_STATUS = 0;
|
||||
} else {
|
||||
// Configure GDO0 for serial data (async serial mode)
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
@@ -1159,41 +856,24 @@ void CC1101Component::set_packet_mode(bool value) {
|
||||
this->gdo0_pin_->detach_interrupt();
|
||||
}
|
||||
}
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
this->write_(Register::IOCFG0);
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_length(uint16_t value) {
|
||||
#ifndef USE_CC1101_LONG_PACKET
|
||||
if (value > RX_FIFO_SIZE) {
|
||||
ESP_LOGW(TAG, "Packet length %u needs a configured packet_length above %u", value,
|
||||
static_cast<unsigned>(RX_FIFO_SIZE));
|
||||
return;
|
||||
void CC1101Component::set_packet_length(uint8_t value) {
|
||||
if (value == 0) {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
} else {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->state_.PKTLEN = value;
|
||||
}
|
||||
#else
|
||||
this->packet_length_func_ = nullptr;
|
||||
#endif
|
||||
this->packet_length_ = value;
|
||||
this->reconfigure_packet_length_();
|
||||
}
|
||||
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
void CC1101Component::set_packet_length_lambda(packet_length_func_t func) {
|
||||
this->packet_length_func_ = func;
|
||||
this->reconfigure_packet_length_();
|
||||
}
|
||||
#endif
|
||||
|
||||
void CC1101Component::reconfigure_packet_length_() {
|
||||
// While asleep the writes wait for wake_up() and the radio stays asleep, like set_frequency()
|
||||
if (!this->initialized_ || this->sleeping_) {
|
||||
this->configure_packet_mode_();
|
||||
return;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTLEN);
|
||||
}
|
||||
// Like the other runtime setters: go through IDLE, and flush bytes of a reception in progress
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->configure_packet_mode_();
|
||||
this->enter_rx_();
|
||||
}
|
||||
|
||||
void CC1101Component::set_crc_enable(bool value) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "cc1101defs.h"
|
||||
@@ -11,7 +10,6 @@
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
|
||||
using packet_length_func_t = int32_t (*)(const std::vector<uint8_t> &);
|
||||
|
||||
class CC1101Listener {
|
||||
public:
|
||||
@@ -88,10 +86,7 @@ class CC1101Component final : public Component,
|
||||
|
||||
// Packet mode settings
|
||||
void set_packet_mode(bool value);
|
||||
void set_packet_length(uint16_t value);
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
void set_packet_length_lambda(packet_length_func_t func);
|
||||
#endif
|
||||
void set_packet_length(uint8_t value);
|
||||
void set_crc_enable(bool value);
|
||||
void set_whitening(bool value);
|
||||
|
||||
@@ -106,7 +101,6 @@ class CC1101Component final : public Component,
|
||||
protected:
|
||||
uint16_t chip_id_{0};
|
||||
bool initialized_{false};
|
||||
bool packet_mode_{false};
|
||||
bool sleeping_{false};
|
||||
bool registers_dirty_{false};
|
||||
|
||||
@@ -118,38 +112,13 @@ class CC1101Component final : public Component,
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
// Inline like App.get_loop_component_start_time(): on RP2040 an out-of-line IRAM_ATTR function in the same
|
||||
// file as that inline one fails with a section type conflict
|
||||
static void IRAM_ATTR gpio_intr(CC1101Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
static void gpio_intr(CC1101Component *arg);
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
void configure_packet_mode_();
|
||||
void reconfigure_packet_length_();
|
||||
float symbol_rate_() const;
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
bool is_long_packet_mode_() const;
|
||||
void arm_length_(uint16_t length);
|
||||
void reset_long_packet_rx_();
|
||||
void restart_long_packet_rx_();
|
||||
void loop_long_packet_();
|
||||
void handle_long_packet_(uint8_t rx_bytes);
|
||||
void finish_long_packet_();
|
||||
void maybe_switch_long_packet_to_fixed_();
|
||||
#endif
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> packet_trigger_;
|
||||
std::vector<uint8_t> packet_;
|
||||
std::vector<CC1101Listener *> listeners_;
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
packet_length_func_t packet_length_func_{nullptr};
|
||||
uint32_t packet_last_byte_ms_{0};
|
||||
#endif
|
||||
uint16_t packet_length_{0};
|
||||
#ifdef USE_CC1101_LONG_PACKET
|
||||
uint16_t packet_expected_length_{0};
|
||||
int8_t long_packet_rssi_{0}; // raw RSSI register
|
||||
HighFrequencyLoopRequester high_freq_;
|
||||
#endif
|
||||
|
||||
// Low-level Helpers
|
||||
uint8_t strobe_(Command cmd);
|
||||
|
||||
@@ -15,11 +15,6 @@ static constexpr uint8_t PLL_LOCK_RETRIES = 3;
|
||||
static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80;
|
||||
static constexpr uint8_t STATUS_LQI_MASK = 0x7F;
|
||||
|
||||
static constexpr uint16_t MAX_PACKET_LENGTH = 1024;
|
||||
static constexpr uint16_t PACKET_IDLE_TIMEOUT_BYTES = 64;
|
||||
static constexpr uint32_t PACKET_IDLE_TIMEOUT_MIN_MS = 5;
|
||||
static constexpr uint8_t RXBYTES_STABLE_RETRIES = 4;
|
||||
|
||||
static constexpr uint8_t BUS_BURST = 0x40;
|
||||
static constexpr uint8_t BUS_READ = 0x80;
|
||||
static constexpr uint8_t BUS_WRITE = 0x00;
|
||||
|
||||
@@ -1,15 +1,36 @@
|
||||
from esphome.components import cstxxx
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import cst226_ns
|
||||
|
||||
AUTO_LOAD = ["cstxxx"]
|
||||
|
||||
CST226Touchscreen = cst226_ns.class_("CST226Touchscreen", cstxxx.CSTTouchscreen)
|
||||
CST226Touchscreen = cst226_ns.class_(
|
||||
"CST226Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
CST226ButtonListener = cst226_ns.class_("CST226ButtonListener")
|
||||
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST226Touchscreen, 0x5A)
|
||||
CONFIG_SCHEMA = (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CST226Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x5A))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await cstxxx.new_cst_touchscreen(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
@@ -14,10 +14,6 @@ void CST226Touchscreen::setup() {
|
||||
}
|
||||
|
||||
void CST226Touchscreen::update_touches() {
|
||||
if (!this->setup_complete_) {
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
}
|
||||
uint8_t data[28];
|
||||
if (!this->status_comm_result(this->read_bytes(CST226_REG_STATUS, data, sizeof data))) {
|
||||
this->skip_update_ = true;
|
||||
@@ -28,39 +24,56 @@ void CST226Touchscreen::update_touches() {
|
||||
return;
|
||||
}
|
||||
this->update_button_state_(false);
|
||||
if (data[cstxxx::SYNC_IDX] != cstxxx::SYNC_BYTE || data[0] == cstxxx::SYNC_BYTE ||
|
||||
data[cstxxx::FINGER_COUNT_IDX] == 0x80) {
|
||||
if (data[6] != 0xAB || data[0] == 0xAB || data[5] == 0x80) {
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
}
|
||||
uint8_t num_of_touches = data[cstxxx::FINGER_COUNT_IDX] & 0x7F;
|
||||
uint8_t num_of_touches = data[5] & 0x7F;
|
||||
if (num_of_touches == 0 || num_of_touches > 5) {
|
||||
this->write_byte(CST226_REG_STATUS, cstxxx::SYNC_BYTE);
|
||||
this->write_byte(0, 0xAB);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t index = 0;
|
||||
for (uint8_t i = 0; i != num_of_touches; i++) {
|
||||
auto touch = cstxxx::decode_touch(data, i);
|
||||
this->add_raw_touch_position_(touch.id, touch.x, touch.y, touch.z);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", touch.id, touch.x, touch.y);
|
||||
uint8_t id = data[index] >> 4;
|
||||
int16_t x = (data[index + 1] << 4) | ((data[index + 3] >> 4) & 0x0F);
|
||||
int16_t y = (data[index + 2] << 4) | (data[index + 3] & 0x0F);
|
||||
int16_t z = data[index + 4];
|
||||
this->add_raw_touch_position_(id, x, y, z);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
|
||||
index += 5;
|
||||
if (i == 0)
|
||||
index += 2;
|
||||
}
|
||||
}
|
||||
|
||||
bool CST226Touchscreen::read16_(uint16_t addr, uint8_t *data, size_t len) {
|
||||
if (this->read_register16(addr, data, len) != i2c::ERROR_OK) {
|
||||
ESP_LOGE(TAG, "Read data from 0x%04X failed", addr);
|
||||
this->mark_failed();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void CST226Touchscreen::continue_setup_() {
|
||||
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
|
||||
if (!this->enter_command_mode_())
|
||||
uint8_t buffer[8];
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
if (!this->setup_check(this->write_byte_reg16(0xD1, 0xD1), LOG_STR("Write byte to 0xD1 failed")))
|
||||
return;
|
||||
delay(10);
|
||||
if (!this->check_firmware_())
|
||||
return;
|
||||
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
|
||||
LOG_STR("Failed to read chip ID")))
|
||||
return;
|
||||
if (this->read16_(0xD204, buffer, 4)) {
|
||||
uint16_t chip_id = buffer[2] + (buffer[3] << 8);
|
||||
uint16_t project_id = buffer[0] + (buffer[1] << 8);
|
||||
ESP_LOGCONFIG(TAG, "Chip ID %X, project ID %x", chip_id, project_id);
|
||||
}
|
||||
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
|
||||
uint16_t x_max, y_max;
|
||||
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
|
||||
this->x_raw_max_ = x_max;
|
||||
this->y_raw_max_ = y_max;
|
||||
if (this->read16_(0xD1F8, buffer, 4)) {
|
||||
this->x_raw_max_ = buffer[0] + (buffer[1] << 8);
|
||||
this->y_raw_max_ = buffer[2] + (buffer[3] << 8);
|
||||
if (this->swap_x_y_)
|
||||
std::swap(this->x_raw_max_, this->y_raw_max_);
|
||||
} else {
|
||||
@@ -68,10 +81,8 @@ void CST226Touchscreen::continue_setup_() {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
}
|
||||
this->status_comm_check(this->write_register16(cstxxx::REG_NORMAL_MODE, nullptr, 0) == i2c::ERROR_OK);
|
||||
this->setup_complete_ = true;
|
||||
}
|
||||
|
||||
void CST226Touchscreen::update_button_state_(bool state) {
|
||||
if (this->button_touched_ == state)
|
||||
return;
|
||||
@@ -85,10 +96,6 @@ void CST226Touchscreen::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Chip ID: 0x%04X, Project ID: 0x%04X\n"
|
||||
" X/Y raw max: %d/%d",
|
||||
this->chip_id_, this->project_id_, this->x_raw_max_, this->y_raw_max_);
|
||||
}
|
||||
|
||||
} // namespace esphome::cst226
|
||||
@@ -1,12 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/cstxxx/cstxxx.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::cst226 {
|
||||
|
||||
static const uint8_t CST226_REG_STATUS = 0x00;
|
||||
@@ -16,23 +15,28 @@ class CST226ButtonListener {
|
||||
virtual void update_button(bool state) = 0;
|
||||
};
|
||||
|
||||
class CST226Touchscreen final : public cstxxx::CSTTouchscreen {
|
||||
class CST226Touchscreen final : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update_touches() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
bool can_proceed() override { return this->setup_complete_ || this->is_failed(); }
|
||||
void register_button_listener(CST226ButtonListener *listener) { this->button_listeners_.push_back(listener); }
|
||||
|
||||
protected:
|
||||
bool read16_(uint16_t addr, uint8_t *data, size_t len);
|
||||
void continue_setup_();
|
||||
void update_button_state_(bool state);
|
||||
|
||||
bool button_touched_{};
|
||||
uint16_t chip_id_{};
|
||||
uint16_t project_id_{};
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
uint8_t chip_id_{};
|
||||
bool setup_complete_{};
|
||||
std::vector<CST226ButtonListener *> button_listeners_;
|
||||
bool button_touched_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cst226
|
||||
@@ -1,15 +1,37 @@
|
||||
from esphome.components import cstxxx
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import cst328_ns
|
||||
|
||||
AUTO_LOAD = ["cstxxx"]
|
||||
|
||||
CST328Touchscreen = cst328_ns.class_("CST328Touchscreen", cstxxx.CSTTouchscreen)
|
||||
CST328Touchscreen = cst328_ns.class_(
|
||||
"CST328Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
CST328ButtonListener = cst328_ns.class_("CST328ButtonListener")
|
||||
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST328Touchscreen, 0x1A)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CST328Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x1A))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await cstxxx.new_cst_touchscreen(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
@@ -1,13 +1,35 @@
|
||||
#include "cst328_touchscreen.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cst328 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cst328.touchscreen");
|
||||
|
||||
static constexpr uint32_t CST328_BEFORE_RESET_TIMEOUT = 50; // 50 ms from datasheet
|
||||
static constexpr uint32_t CST328_TRANSITION_TIMEOUT = 300; // 200 ms from datasheet, but typically much less
|
||||
static const uint32_t CST328_BEFORE_RESET_TIMEOUT = 50; // 50 ms from datasheet
|
||||
static const uint32_t CST328_TRANSITION_TIMEOUT = 300; // 200 ms from datasheet, but typically much less
|
||||
static const uint16_t CST328_FW_CRC = 0xCACA; // Expected firmware CRC value
|
||||
static const uint8_t CST328_SYNC_BYTE = 0xAB; // Sync byte used in communication
|
||||
|
||||
static const uint8_t ZERO_BYTE = 0;
|
||||
|
||||
#define I2C_WARN_ON_ERROR(x, log_tag, format, ...) \
|
||||
do { \
|
||||
i2c::ErrorCode err_rc_ = (x); \
|
||||
if (err_rc_ != i2c::ERROR_OK) { \
|
||||
ESP_LOGW(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \
|
||||
this->status_set_warning(format); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define I2C_FAIL_ON_ERROR(x, log_tag, format, ...) \
|
||||
do { \
|
||||
i2c::ErrorCode err_rc_ = (x); \
|
||||
if (err_rc_ != i2c::ERROR_OK) { \
|
||||
ESP_LOGE(log_tag, "%s(%d): [error %d] " format, __FUNCTION__, __LINE__, err_rc_, ##__VA_ARGS__); \
|
||||
this->mark_failed(); \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
void CST328Touchscreen::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up CST328 Touchscreen...");
|
||||
@@ -28,36 +50,50 @@ void CST328Touchscreen::reset_device_() {
|
||||
void CST328Touchscreen::continue_setup_() {
|
||||
ESP_LOGV(TAG, "Continuing CST328 setup...");
|
||||
|
||||
if (!this->enter_command_mode_() || !this->check_firmware_())
|
||||
return;
|
||||
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
|
||||
LOG_STR("Failed to read chip and project ID")))
|
||||
uint8_t data_byte{0};
|
||||
uint8_t buf[24]{};
|
||||
|
||||
I2C_FAIL_ON_ERROR(this->write_register16(CST_WM_DEBUG_INFO, buf, 0), TAG, "Failed to enter debug/info mode");
|
||||
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_CRC_AND_BOOT_TIME, buf, 4), TAG,
|
||||
"Failed to read FW CRC and boot time");
|
||||
|
||||
uint16_t fw_crc = buf[2] + (buf[3] << 8);
|
||||
if (fw_crc != CST328_FW_CRC) {
|
||||
ESP_LOGE(TAG, "Error: Firmware CRC mismatch, expected 0x%04X but got 0x%04X", CST328_FW_CRC, fw_crc);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_CHIP_TYPE_AND_PROJECT_ID, buf, 4), TAG,
|
||||
"Failed to read chip and project ID");
|
||||
|
||||
this->chip_id_ = buf[2] + (buf[3] << 8);
|
||||
this->project_id_ = buf[0] + (buf[1] << 8);
|
||||
ESP_LOGD(TAG, "Chip ID %X, project ID %X", this->chip_id_, this->project_id_);
|
||||
uint16_t fw_version;
|
||||
if (!this->setup_check(this->read_words_(cstxxx::REG_FW_VERSION, this->fw_build_, fw_version),
|
||||
LOG_STR("Failed to read FW version")))
|
||||
return;
|
||||
this->fw_ver_major_ = fw_version >> 8;
|
||||
this->fw_ver_minor_ = fw_version & 0xFF;
|
||||
I2C_FAIL_ON_ERROR(this->read_register16(CST_REG_FW_REVISION, buf, 4), TAG, "Failed to read FW version");
|
||||
|
||||
this->fw_ver_major_ = buf[3];
|
||||
this->fw_ver_minor_ = buf[2];
|
||||
this->fw_build_ = buf[0] + (buf[1] << 8);
|
||||
ESP_LOGV(TAG, "FW version %d.%d.%d", this->fw_ver_major_, this->fw_ver_minor_, this->fw_build_);
|
||||
|
||||
uint16_t x_max, y_max;
|
||||
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
|
||||
this->x_raw_max_ = x_max;
|
||||
this->y_raw_max_ = y_max;
|
||||
if (i2c::ERROR_OK == this->read_register16(CST_REG_X_Y_RESOLUTION, buf, 4)) {
|
||||
this->x_raw_max_ = buf[0] + (buf[1] << 8);
|
||||
this->y_raw_max_ = buf[2] + (buf[3] << 8);
|
||||
} else {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
|
||||
uint8_t sync;
|
||||
bool ok = this->write_register16(cstxxx::REG_NORMAL_MODE, nullptr, 0) == i2c::ERROR_OK;
|
||||
ok &= this->read_byte_reg16(cstxxx::REG_TOUCH_DATA, &sync);
|
||||
ok &= this->write_byte_reg16(cstxxx::REG_TOUCH_DATA, cstxxx::SYNC_BYTE);
|
||||
this->status_comm_check(ok);
|
||||
I2C_WARN_ON_ERROR(this->write_register16(CST_WM_NORMAL, buf, 0), TAG, "Failed to enter normal mode");
|
||||
I2C_WARN_ON_ERROR(this->read_register16(CST_REG_TOUCH_INFORMATION, &data_byte, 1), TAG, "Failed to read sync");
|
||||
I2C_WARN_ON_ERROR(this->write_register16(CST_REG_TOUCH_INFORMATION, &CST328_SYNC_BYTE, 1), TAG,
|
||||
"Failed to write sync");
|
||||
|
||||
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
this->setup_complete_ = true;
|
||||
ESP_LOGV(TAG, "CST328 setup complete");
|
||||
@@ -100,21 +136,27 @@ void CST328Touchscreen::update_touches() {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t touch_cnt = low_nibble(touch_data[CST_REG_FINGER_COUNT_IDX]);
|
||||
uint8_t touch_cnt = touch_data[CST_REG_FINGER_COUNT_IDX] & 0x0F;
|
||||
if (touch_cnt == 0 || touch_cnt > CST328_TOUCH_MAX_POINTS) {
|
||||
this->update_button_state_(false);
|
||||
} else {
|
||||
this->update_button_state_(true);
|
||||
|
||||
uint8_t data_idx = 0;
|
||||
for (uint8_t i = 0; i < touch_cnt; i++) {
|
||||
auto touch = cstxxx::decode_touch(touch_data, i);
|
||||
this->add_raw_touch_position_(touch.id, touch.x, touch.y, touch.z);
|
||||
uint8_t id = touch_data[data_idx] >> 4;
|
||||
int16_t x = (touch_data[data_idx + 1] << 4) | ((touch_data[data_idx + 3] >> 4) & 0x0F);
|
||||
int16_t y = (touch_data[data_idx + 2] << 4) | (touch_data[data_idx + 3] & 0x0F);
|
||||
int16_t z = touch_data[data_idx + 4];
|
||||
|
||||
this->add_raw_touch_position_(id, x, y, z);
|
||||
data_idx += (i == 0) ? 7 : 5;
|
||||
}
|
||||
}
|
||||
|
||||
bool cleanup_error = false;
|
||||
cleanup_error |= !this->write_byte_reg16(cstxxx::REG_FINGER_COUNT, 0);
|
||||
cleanup_error |= !this->write_byte_reg16(cstxxx::REG_TOUCH_DATA, cstxxx::SYNC_BYTE);
|
||||
cleanup_error |= !this->write_byte_reg16(CST_REG_TOUCH_FINGER_NUMBER, ZERO_BYTE);
|
||||
cleanup_error |= !this->write_byte_reg16(CST_REG_TOUCH_INFORMATION, CST328_SYNC_BYTE);
|
||||
|
||||
if (cleanup_error) {
|
||||
ESP_LOGW(TAG, "Failed to clean up touch registers");
|
||||
|
||||
@@ -1,45 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/cstxxx/cstxxx.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::cst328 {
|
||||
|
||||
static constexpr uint8_t CST328_TOUCH_MAX_POINTS = 5;
|
||||
static constexpr uint8_t CST328_TOUCH_DATA_SIZE = cstxxx::report_size(CST328_TOUCH_MAX_POINTS);
|
||||
static const uint8_t CST328_TOUCH_MAX_POINTS = 5;
|
||||
static const uint8_t CST328_TOUCH_DATA_SIZE = CST328_TOUCH_MAX_POINTS * 5 + 2;
|
||||
|
||||
static constexpr uint16_t CST_REG_TOUCH_INFORMATION = cstxxx::REG_TOUCH_DATA;
|
||||
static constexpr uint16_t CST_REG_FINGER_COUNT_IDX = cstxxx::FINGER_COUNT_IDX;
|
||||
static constexpr uint16_t CST_REG_X_Y_RESOLUTION = cstxxx::REG_RESOLUTION;
|
||||
static constexpr uint16_t CST_REG_FW_CRC_AND_BOOT_TIME = cstxxx::REG_FW_CHECK_CODE;
|
||||
static const uint16_t CST_REG_TOUCH_INFORMATION = 0xD000;
|
||||
static const uint16_t CST_REG_TOUCH_FINGER_NUMBER = 0xD005;
|
||||
|
||||
static const uint16_t CST_REG_FINGER_COUNT_IDX = CST_REG_TOUCH_FINGER_NUMBER - CST_REG_TOUCH_INFORMATION;
|
||||
|
||||
static const uint16_t CST_REG_X_Y_RESOLUTION = 0xD1F8;
|
||||
static const uint16_t CST_REG_FW_CRC_AND_BOOT_TIME = 0xD1FC;
|
||||
static const uint16_t CST_REG_CHIP_TYPE_AND_PROJECT_ID = 0xD204;
|
||||
static const uint16_t CST_REG_FW_REVISION = 0xD208;
|
||||
|
||||
static const uint16_t CST_WM_DEBUG_INFO = 0xD101;
|
||||
static const uint16_t CST_WM_NORMAL = 0xD109;
|
||||
|
||||
class CST328ButtonListener {
|
||||
public:
|
||||
virtual void update_button(bool state) = 0;
|
||||
};
|
||||
|
||||
class CST328Touchscreen : public cstxxx::CSTTouchscreen {
|
||||
class CST328Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void register_button_listener(CST328ButtonListener *listener) { this->button_listeners_.push_back(listener); }
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
void update_touches() override;
|
||||
void reset_device_();
|
||||
void continue_setup_();
|
||||
void update_button_state_(bool state);
|
||||
|
||||
bool button_touched_{};
|
||||
uint8_t fw_ver_major_{};
|
||||
uint8_t fw_ver_minor_{};
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
|
||||
std::vector<CST328ButtonListener *> button_listeners_;
|
||||
bool button_touched_{};
|
||||
|
||||
uint16_t chip_id_{};
|
||||
uint16_t project_id_{};
|
||||
uint8_t fw_ver_major_{};
|
||||
uint8_t fw_ver_minor_{};
|
||||
uint16_t fw_build_{};
|
||||
|
||||
bool setup_complete_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cst328
|
||||
@@ -1,14 +1,35 @@
|
||||
from esphome.components import cstxxx
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import cst9220_ns
|
||||
|
||||
AUTO_LOAD = ["cstxxx"]
|
||||
CST9220Touchscreen = cst9220_ns.class_(
|
||||
"CST9220Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
CST9220Touchscreen = cst9220_ns.class_("CST9220Touchscreen", cstxxx.CSTTouchscreen)
|
||||
|
||||
CONFIG_SCHEMA = cstxxx.cst_touchscreen_schema(CST9220Touchscreen, 0x5A)
|
||||
CONFIG_SCHEMA = (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CST9220Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x5A))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await cstxxx.new_cst_touchscreen(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "cst9220_touchscreen.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::cst9220 {
|
||||
|
||||
@@ -13,29 +16,55 @@ void CST9220Touchscreen::setup() {
|
||||
}
|
||||
|
||||
void CST9220Touchscreen::continue_setup_() {
|
||||
this->setup_interrupt_pin_(gpio::INTERRUPT_FALLING_EDGE);
|
||||
uint8_t buffer[4];
|
||||
|
||||
if (!this->enter_command_mode_())
|
||||
return;
|
||||
delay(10);
|
||||
if (!this->check_firmware_())
|
||||
return;
|
||||
|
||||
// Read the panel resolution unless the user supplied calibration values.
|
||||
uint16_t x_max, y_max;
|
||||
if (this->read_words_(cstxxx::REG_RESOLUTION, x_max, y_max)) {
|
||||
if (this->x_raw_max_ == this->x_raw_min_)
|
||||
this->x_raw_max_ = x_max;
|
||||
if (this->y_raw_max_ == this->y_raw_min_)
|
||||
this->y_raw_max_ = y_max;
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
if (!this->setup_check(this->read_words_(cstxxx::REG_CHIP_INFO, this->project_id_, this->chip_id_),
|
||||
LOG_STR("Failed to read chip ID")))
|
||||
// Enter command mode so the configuration registers can be read.
|
||||
if (this->write_register16(REG_CMD_MODE, buffer, 0) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to enter command mode"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
delay(10);
|
||||
|
||||
// The firmware check code confirms that valid firmware is loaded.
|
||||
if (this->read_register16(REG_CHECKCODE, buffer, 4) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to read check code"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
uint32_t checkcode = encode_uint32(buffer[3], buffer[2], buffer[1], buffer[0]);
|
||||
if ((checkcode & 0xFFFF0000) != 0xCACA0000) {
|
||||
ESP_LOGE(TAG, "Invalid firmware check code: 0x%08" PRIX32, checkcode);
|
||||
this->status_set_error(LOG_STR("Invalid firmware check code"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the panel resolution unless the user supplied calibration values.
|
||||
if (this->read_register16(REG_RESOLUTION, buffer, 4) == i2c::ERROR_OK) {
|
||||
if (this->x_raw_max_ == this->x_raw_min_)
|
||||
this->x_raw_max_ = encode_value_le<uint16_t>(buffer);
|
||||
if (this->y_raw_max_ == this->y_raw_min_)
|
||||
this->y_raw_max_ = encode_value_le<uint16_t>(&buffer[2]);
|
||||
}
|
||||
|
||||
// Read the chip type and project id and validate the controller.
|
||||
if (this->read_register16(REG_CHIP_INFO, buffer, 4) != i2c::ERROR_OK) {
|
||||
this->status_set_error(LOG_STR("Failed to read chip ID"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->chip_id_ = encode_value_le<uint16_t>(&buffer[2]);
|
||||
this->project_id_ = encode_value_le<uint16_t>(buffer);
|
||||
if (this->chip_id_ != CST9220_CHIP_ID && this->chip_id_ != CST9217_CHIP_ID) {
|
||||
ESP_LOGE(TAG, "Unknown chip ID: 0x%04X", this->chip_id_);
|
||||
this->mark_failed(LOG_STR("Unknown chip ID"));
|
||||
this->status_set_error(LOG_STR("Unknown chip ID"));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,7 +84,7 @@ void CST9220Touchscreen::update_touches() {
|
||||
// Only an actual I2C failure should skip the update; a successful read with no
|
||||
// touches is a real "all fingers lifted" state that must flow through so the
|
||||
// base class can generate the release event.
|
||||
if (!this->status_comm_result(this->read_register16(cstxxx::REG_TOUCH_DATA, data, sizeof(data)) == i2c::ERROR_OK)) {
|
||||
if (!this->status_comm_result(this->read_register16(REG_TOUCH_DATA, data, sizeof(data)) == i2c::ERROR_OK)) {
|
||||
this->skip_update_ = true;
|
||||
return;
|
||||
}
|
||||
@@ -63,23 +92,30 @@ void CST9220Touchscreen::update_touches() {
|
||||
// A valid report carries the marker at offset 6. Writing an acknowledge back makes the controller return
|
||||
// invalid frames during a long press, so none is written. An invalid frame keeps the previous points; while
|
||||
// a finger is down, read again shortly so a lost release frame does not leave the touch held.
|
||||
if (data[cstxxx::SYNC_IDX] != cstxxx::SYNC_BYTE) {
|
||||
if (data[6] != TOUCH_ACK) {
|
||||
this->skip_update_ = true;
|
||||
if (!this->touches_.empty())
|
||||
this->set_timeout(REREAD_TIMEOUT_ID, REREAD_DELAY_MS, [this] { this->store_.touched = true; });
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t num_touches = data[cstxxx::FINGER_COUNT_IDX] & 0x7F;
|
||||
uint8_t num_touches = data[5] & 0x7F;
|
||||
if (num_touches > CST9220_MAX_TOUCHES)
|
||||
num_touches = CST9220_MAX_TOUCHES;
|
||||
|
||||
for (uint8_t i = 0; i < num_touches; i++) {
|
||||
auto touch = cstxxx::decode_touch(data, i);
|
||||
if (touch.event != TOUCH_EVENT_DOWN)
|
||||
// The first point starts at offset 0; subsequent points are offset by the
|
||||
// two status bytes that follow it.
|
||||
const uint8_t *p = data + i * 5 + (i == 0 ? 0 : 2);
|
||||
uint8_t id = p[0] >> 4;
|
||||
uint8_t event = p[0] & 0x0F;
|
||||
if (event != TOUCH_EVENT_DOWN)
|
||||
continue;
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", touch.id, touch.x, touch.y);
|
||||
this->add_raw_touch_position_(touch.id, touch.x, touch.y);
|
||||
// p[3] is shared: high nibble holds the X LSBs, low nibble the Y LSBs.
|
||||
uint16_t x = (p[1] << 4) | (p[3] >> 4);
|
||||
uint16_t y = (p[2] << 4) | (p[3] & 0x0F);
|
||||
ESP_LOGV(TAG, "Read touch %d: %d/%d", id, x, y);
|
||||
this->add_raw_touch_position_(id, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/cstxxx/cstxxx.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -9,26 +10,41 @@ namespace esphome::cst9220 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
|
||||
|
||||
static constexpr uint8_t TOUCH_EVENT_DOWN = 0x06;
|
||||
// The CST92xx family uses 16-bit (big-endian) register addresses.
|
||||
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
|
||||
static const uint16_t REG_CMD_MODE = 0xD101; // enter command mode
|
||||
static const uint16_t REG_CHECKCODE = 0xD1FC; // firmware check code
|
||||
static const uint16_t REG_RESOLUTION = 0xD1F8; // panel resolution
|
||||
static const uint16_t REG_CHIP_INFO = 0xD204; // chip type + project id
|
||||
|
||||
static constexpr uint16_t CST9220_CHIP_ID = 0x9220;
|
||||
static constexpr uint16_t CST9217_CHIP_ID = 0x9217;
|
||||
static const uint8_t TOUCH_ACK = 0xAB;
|
||||
static const uint8_t TOUCH_EVENT_DOWN = 0x06;
|
||||
|
||||
static constexpr uint8_t CST9220_MAX_TOUCHES = 5;
|
||||
static const uint16_t CST9220_CHIP_ID = 0x9220;
|
||||
static const uint16_t CST9217_CHIP_ID = 0x9217;
|
||||
|
||||
// Maximum simultaneous touch points reported by the family.
|
||||
static const uint8_t CST9220_MAX_TOUCHES = 5;
|
||||
// Report layout: 5 bytes per touch point plus 5 bytes of status/ack overhead.
|
||||
static constexpr size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
|
||||
static const size_t CST9220_DATA_LENGTH = CST9220_MAX_TOUCHES * 5 + 5;
|
||||
|
||||
class CST9220Touchscreen : public cstxxx::CSTTouchscreen {
|
||||
class CST9220Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
void update_touches() override;
|
||||
void continue_setup_();
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
uint16_t chip_id_{};
|
||||
uint16_t project_id_{};
|
||||
bool setup_complete_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cst9220
|
||||
@@ -1,42 +0,0 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow", "@latonita"]
|
||||
DEPENDENCIES = ["i2c", "touchscreen"]
|
||||
DOMAIN = "cstxxx"
|
||||
|
||||
cstxxx_ns = cg.esphome_ns.namespace("cstxxx")
|
||||
CSTTouchscreen = cstxxx_ns.class_(
|
||||
"CSTTouchscreen", touchscreen.Touchscreen, i2c.I2CDevice
|
||||
)
|
||||
|
||||
|
||||
def cst_touchscreen_schema(touchscreen_class: MockObjClass, address: int) -> cv.Schema:
|
||||
"""Return the touchscreen schema of a Hynitron driver."""
|
||||
return (
|
||||
touchscreen.touchscreen_schema("100ms")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(touchscreen_class),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(address))
|
||||
)
|
||||
|
||||
|
||||
async def new_cst_touchscreen(config: ConfigType) -> MockObj:
|
||||
"""Create a Hynitron touchscreen and set its pins."""
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
await cg.add_pin(var.set_interrupt_pin, config.get(CONF_INTERRUPT_PIN))
|
||||
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
|
||||
return var
|
||||
@@ -1,13 +0,0 @@
|
||||
#include "cstxxx.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cstxxx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "cstxxx");
|
||||
|
||||
void CSTTouchscreen::fail_check_code_(uint16_t check_code) {
|
||||
ESP_LOGE(TAG, "Firmware check code 0x%04X", check_code);
|
||||
this->mark_failed(LOG_STR("Invalid firmware check code"));
|
||||
}
|
||||
|
||||
} // namespace esphome::cstxxx
|
||||
@@ -1,99 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::cstxxx {
|
||||
|
||||
static constexpr uint16_t REG_TOUCH_DATA = 0xD000;
|
||||
static constexpr uint16_t REG_FINGER_COUNT = 0xD005;
|
||||
static constexpr uint16_t REG_COMMAND_MODE = 0xD101;
|
||||
static constexpr uint16_t REG_NORMAL_MODE = 0xD109;
|
||||
static constexpr uint16_t REG_RESOLUTION = 0xD1F8;
|
||||
static constexpr uint16_t REG_FW_CHECK_CODE = 0xD1FC;
|
||||
static constexpr uint16_t REG_CHIP_INFO = 0xD204;
|
||||
static constexpr uint16_t REG_FW_VERSION = 0xD208;
|
||||
|
||||
static constexpr uint8_t FINGER_COUNT_IDX = REG_FINGER_COUNT - REG_TOUCH_DATA;
|
||||
static constexpr uint8_t SYNC_IDX = FINGER_COUNT_IDX + 1;
|
||||
static constexpr uint8_t SYNC_BYTE = 0xAB;
|
||||
static constexpr uint16_t FW_CHECK_CODE = 0xCACA;
|
||||
|
||||
/// Bytes of a touch report holding `points` records; two status bytes follow the first record.
|
||||
constexpr size_t report_size(uint8_t points) { return points * 5 + 2; }
|
||||
|
||||
struct TouchRecord {
|
||||
uint8_t id;
|
||||
uint8_t event;
|
||||
uint16_t x;
|
||||
uint16_t y;
|
||||
uint8_t z;
|
||||
};
|
||||
|
||||
/// Decodes record `index` of a touch report. Each record is 5 bytes:
|
||||
/// [id:4 | event:4] [x 11..4] [y 11..4] [x 3..0 | y 3..0] [z].
|
||||
/// The first record is followed by 2 extra bytes, so later records start 2 bytes further on.
|
||||
inline TouchRecord decode_touch(const uint8_t *report, uint8_t index) {
|
||||
const uint8_t *r = report + index * 5 + (index == 0 ? 0 : 2);
|
||||
return {
|
||||
.id = high_nibble(r[0]),
|
||||
.event = low_nibble(r[0]),
|
||||
.x = encode_uint12(r[1], high_nibble(r[3])),
|
||||
.y = encode_uint12(r[2], low_nibble(r[3])),
|
||||
.z = r[4],
|
||||
};
|
||||
}
|
||||
|
||||
class CSTTouchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
/// Sets up the interrupt pin, if configured, to fire on `type`.
|
||||
void setup_interrupt_pin_(gpio::InterruptType type) {
|
||||
if (this->interrupt_pin_ == nullptr)
|
||||
return;
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, type);
|
||||
}
|
||||
/// Reads two little-endian words starting at `reg`.
|
||||
bool ESPHOME_ALWAYS_INLINE read_words_(uint16_t reg, uint16_t &low, uint16_t &high) {
|
||||
uint16_t words[2];
|
||||
if (this->read_register16(reg, reinterpret_cast<uint8_t *>(words), sizeof(words)) != i2c::ERROR_OK)
|
||||
return false;
|
||||
low = convert_little_endian(words[0]);
|
||||
high = convert_little_endian(words[1]);
|
||||
return true;
|
||||
}
|
||||
/// Enters command mode, marking the component failed if the write fails.
|
||||
bool enter_command_mode_() {
|
||||
return this->setup_check(this->write_register16(REG_COMMAND_MODE, nullptr, 0) == i2c::ERROR_OK,
|
||||
LOG_STR("Failed to enter command mode"));
|
||||
}
|
||||
/// Checks the firmware check code, marking the component failed if it is unreadable or wrong.
|
||||
bool check_firmware_() {
|
||||
uint16_t boot_time, check_code;
|
||||
if (!this->setup_check(this->read_words_(REG_FW_CHECK_CODE, boot_time, check_code)))
|
||||
return false;
|
||||
if (check_code == FW_CHECK_CODE)
|
||||
return true;
|
||||
this->fail_check_code_(check_code);
|
||||
return false;
|
||||
}
|
||||
/// Logs a wrong firmware check code and marks the component failed.
|
||||
void fail_check_code_(uint16_t check_code);
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
bool setup_complete_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::cstxxx
|
||||
@@ -35,12 +35,12 @@ void EKTF2232Touchscreen::setup() {
|
||||
if (err == i2c::ERROR_OK) {
|
||||
err = this->read(received, 4);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
this->x_raw_max_ = encode_uint16(high_nibble(received[3]), received[2]);
|
||||
this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
|
||||
err = this->write(GET_Y_RES, 4);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
err = this->read(received, 4);
|
||||
if (err == i2c::ERROR_OK) {
|
||||
this->y_raw_max_ = encode_uint16(high_nibble(received[3]), received[2]);
|
||||
this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,9 +74,8 @@ void EKTF2232Touchscreen::update_touches() {
|
||||
|
||||
for (int i = 0; i < touch_count; i++) {
|
||||
uint8_t *d = raw + 1 + (i * 3);
|
||||
// d[0] holds the top 4 bits of X (high nibble) and of Y (low nibble)
|
||||
x_raw = encode_uint16(high_nibble(d[0]), d[1]);
|
||||
y_raw = encode_uint16(low_nibble(d[0]), d[2]);
|
||||
x_raw = (d[0] & 0xF0) << 4 | d[1];
|
||||
y_raw = (d[0] & 0x0F) << 8 | d[2];
|
||||
this->add_raw_touch_position_(i, x_raw, y_raw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/network/ip_address.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#ifdef USE_ETHERNET
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -368,9 +366,6 @@ class EthernetComponent final : public Component {
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
StaticVector<EthernetIPStateListener *, ESPHOME_ETHERNET_IP_STATE_LISTENERS> ip_state_listeners_;
|
||||
#ifdef USE_ESP32
|
||||
std::atomic<uint8_t> ip_state_pending_{0};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_CONNECT_TRIGGER
|
||||
|
||||
@@ -129,14 +129,6 @@ void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *me
|
||||
void EthernetComponent::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
// The IP events arrive on the event task; the listeners publish states, so notify them here
|
||||
if (this->ip_state_pending_.load(std::memory_order_acquire) != 0) {
|
||||
this->ip_state_pending_.store(0, std::memory_order_relaxed);
|
||||
this->notify_ip_state_listeners_();
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (this->state_) {
|
||||
case EthernetComponentState::STOPPED:
|
||||
if (this->started_) {
|
||||
@@ -778,8 +770,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
|
||||
event_name = "ETH connected";
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
|
||||
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
|
||||
global_eth_component->enable_loop_soon_any_context();
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
#if USE_NETWORK_IPV6
|
||||
// Start SLAAC on link-up, not after the DHCPv4 lease. This also restores the
|
||||
@@ -807,9 +798,6 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
|
||||
const esp_netif_ip_info_t *ip_info = &event->ip_info;
|
||||
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
|
||||
global_eth_component->got_ipv4_address_ = true;
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
|
||||
#endif
|
||||
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
@@ -817,6 +805,9 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
|
||||
global_eth_component->connected_ = true;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
@@ -828,9 +819,6 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
|
||||
// after a link flap fires another event for the same address.
|
||||
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
|
||||
global_eth_component->ipv6_count_ = esp_netif_get_all_ip6(global_eth_component->eth_netif_, if_ip6s);
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->ip_state_pending_.store(1, std::memory_order_release);
|
||||
#endif
|
||||
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
|
||||
global_eth_component->connected_ =
|
||||
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
|
||||
@@ -839,6 +827,9 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
|
||||
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
|
||||
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
|
||||
#endif
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
global_eth_component->notify_ip_state_listeners_();
|
||||
#endif
|
||||
}
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
|
||||
@@ -69,8 +69,8 @@ void FT5x06Touchscreen::update_touches() {
|
||||
for (uint8_t i = 0; i != touch_cnt; i++) {
|
||||
uint8_t status = data[i][0] >> 6;
|
||||
uint8_t id = data[i][2] >> 3;
|
||||
uint16_t x = encode_uint16(low_nibble(data[i][0]), data[i][1]);
|
||||
uint16_t y = encode_uint16(low_nibble(data[i][2]), data[i][3]);
|
||||
uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]);
|
||||
uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]);
|
||||
|
||||
ESP_LOGV(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
|
||||
if (status == 0 || status == 2) {
|
||||
|
||||
@@ -90,31 +90,31 @@ void FT63X6Touchscreen::update_touches() {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t FT63X6Touchscreen::read_touch_number_() { return low_nibble(this->read_byte_(FT63X6_ADDR_TD_STATUS)); }
|
||||
uint8_t FT63X6Touchscreen::read_touch_number_() { return this->read_byte_(FT63X6_ADDR_TD_STATUS) & 0x0F; }
|
||||
// Touch 1 functions
|
||||
uint16_t FT63X6Touchscreen::read_touch_x_(uint8_t touch) {
|
||||
uint8_t read_buf[2];
|
||||
read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6));
|
||||
read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_X + 1 + (touch * 6));
|
||||
return encode_uint16(low_nibble(read_buf[0]), read_buf[1]);
|
||||
return encode_uint16(read_buf[0] & 0x0f, read_buf[1]);
|
||||
}
|
||||
uint16_t FT63X6Touchscreen::read_touch_y_(uint8_t touch) {
|
||||
uint8_t read_buf[2];
|
||||
read_buf[0] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + (touch * 6));
|
||||
read_buf[1] = this->read_byte_(FT63X6_ADDR_TOUCH1_Y + 1 + (touch * 6));
|
||||
return encode_uint16(low_nibble(read_buf[0]), read_buf[1]);
|
||||
return encode_uint16(read_buf[0] & 0x0f, read_buf[1]);
|
||||
}
|
||||
uint8_t FT63X6Touchscreen::read_touch_event_(uint8_t touch) {
|
||||
return this->read_byte_(FT63X6_ADDR_TOUCH1_X + (touch * 6)) >> 6;
|
||||
}
|
||||
uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t touch) {
|
||||
return high_nibble(this->read_byte_(FT63X6_ADDR_TOUCH1_ID + (touch * 6)));
|
||||
return this->read_byte_(FT63X6_ADDR_TOUCH1_ID + (touch * 6)) >> 4;
|
||||
}
|
||||
uint8_t FT63X6Touchscreen::read_touch_weight_(uint8_t touch) {
|
||||
return this->read_byte_(FT63X6_ADDR_TOUCH1_WEIGHT + (touch * 6));
|
||||
}
|
||||
uint8_t FT63X6Touchscreen::read_touch_misc_(uint8_t touch) {
|
||||
return high_nibble(this->read_byte_(FT63X6_ADDR_TOUCH1_MISC + (touch * 6)));
|
||||
return this->read_byte_(FT63X6_ADDR_TOUCH1_MISC + (touch * 6)) >> 4;
|
||||
}
|
||||
|
||||
uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "gsl3670_touchscreen.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::gsl3670 {
|
||||
|
||||
@@ -67,11 +66,10 @@ void GSL3670Touchscreen::update_touches() {
|
||||
|
||||
// Build gsl_touch_info exactly as the Seeed driver does
|
||||
for (uint8_t j = 0; j != finger_num; j++) {
|
||||
// Record: y_lo, y_hi, x_lo, then the id (high nibble) and the top 4 bits of X (low nibble)
|
||||
const uint8_t *record = &buf[(j + 1) * 4];
|
||||
auto x = encode_uint16(low_nibble(record[3]), record[2]);
|
||||
auto y = encode_uint16(record[1], record[0]);
|
||||
auto id = high_nibble(record[3]);
|
||||
// buf[(j+1)*4 + 0..3]: byte0=y_lo, byte1=y_hi, byte2=x_lo, byte3=id|x_hi
|
||||
auto x = (uint16_t) (((buf[(j + 1) * 4 + 3] & 0x0f) << 8) | buf[(j + 1) * 4 + 2]);
|
||||
auto y = (uint16_t) ((buf[(j + 1) * 4 + 1] << 8) | buf[(j + 1) * 4 + 0]);
|
||||
auto id = (buf[(j + 1) * 4 + 3] >> 4) & 0x0f;
|
||||
ESP_LOGV(TAG, "Touch id=%u, x=%u y=%u", id, x, y);
|
||||
if (x <= 8192 && y <= 8192)
|
||||
this->add_raw_touch_position_(id, x, y);
|
||||
|
||||
@@ -62,7 +62,7 @@ void LilygoT547Touchscreen::update_touches() {
|
||||
return;
|
||||
}
|
||||
|
||||
point = low_nibble(buffer[5]);
|
||||
point = buffer[5] & 0xF;
|
||||
if (point > 2) {
|
||||
ESP_LOGW(TAG, "Invalid touch point count: %d", point);
|
||||
point = 2;
|
||||
@@ -88,11 +88,9 @@ void LilygoT547Touchscreen::update_touches() {
|
||||
|
||||
uint16_t id, x_raw, y_raw;
|
||||
for (uint8_t i = 0; i < point; i++) {
|
||||
const uint8_t *record = buffer + i * 5;
|
||||
id = high_nibble(record[0]);
|
||||
// The low 4 bits of Y and X share byte 3
|
||||
y_raw = encode_uint12(record[1], high_nibble(record[3]));
|
||||
x_raw = encode_uint12(record[2], low_nibble(record[3]));
|
||||
id = (buffer[i * 5] >> 4) & 0x0F;
|
||||
y_raw = (uint16_t) ((buffer[i * 5 + 1] << 4) | ((buffer[i * 5 + 3] >> 4) & 0x0F));
|
||||
x_raw = (uint16_t) ((buffer[i * 5 + 2] << 4) | (buffer[i * 5 + 3] & 0x0F));
|
||||
this->add_raw_touch_position_(id, x_raw, y_raw);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ from esphome.types import ConfigType
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DOMAIN = "logger"
|
||||
logger_ns = cg.esphome_ns.namespace("logger")
|
||||
LOG_LEVELS = {
|
||||
"NONE": cg.global_ns.ESPHOME_LOG_LEVEL_NONE,
|
||||
@@ -93,6 +94,7 @@ UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
|
||||
|
||||
UART_SELECTION_NRF52 = [USB_CDC, UART0]
|
||||
|
||||
# Read by device-builder
|
||||
HARDWARE_UART_TO_UART_SELECTION = {
|
||||
UART0: logger_ns.UART_SELECTION_UART0,
|
||||
UART0_SWAP: logger_ns.UART_SELECTION_UART0_SWAP,
|
||||
@@ -108,8 +110,6 @@ HARDWARE_UART_TO_SERIAL = {
|
||||
UART0: cg.global_ns.Serial,
|
||||
UART0_SWAP: cg.global_ns.Serial,
|
||||
UART1: cg.global_ns.Serial1,
|
||||
UART2: cg.global_ns.Serial2,
|
||||
DEFAULT: cg.global_ns.Serial,
|
||||
},
|
||||
PLATFORM_RP2: {
|
||||
UART0: cg.global_ns.Serial1,
|
||||
@@ -503,13 +503,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
CORE.data.setdefault(DOMAIN, {})[CONF_LEVEL] = level
|
||||
tx_buffer_size = config[CONF_TX_BUFFER_SIZE]
|
||||
cg.add_define("ESPHOME_LOGGER_TX_BUFFER_SIZE", tx_buffer_size)
|
||||
# Determine task log buffer size. The buffer is a direct member of Logger
|
||||
# (no separate heap allocation).
|
||||
task_log_buffer_size = 0
|
||||
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
|
||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||
elif CORE.is_host:
|
||||
task_log_buffer_size = 64 # Fixed 64 slots for host
|
||||
# The host has a fixed 64 slots
|
||||
task_log_buffer_size = (
|
||||
64 if CORE.is_host else config.get(CONF_TASK_LOG_BUFFER_SIZE, 0)
|
||||
)
|
||||
if task_log_buffer_size > 0:
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
cg.add_define("ESPHOME_TASK_LOG_BUFFER_SIZE", task_log_buffer_size)
|
||||
@@ -542,17 +539,17 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(log.set_boot_log_level(tag, LOG_LEVELS[log_level]))
|
||||
if CORE.is_esp32 or CORE.is_host:
|
||||
cg.add(log.create_pthread_key())
|
||||
# set_uart_selection() must be called before pre_setup() because
|
||||
# pre_setup() switches on uart_ to decide which hardware to initialize
|
||||
# (e.g. UART0 vs USB_SERIAL_JTAG). Without this, uart_ is still the
|
||||
# default UART_SELECTION_UART0 and the wrong hardware gets initialized.
|
||||
# uart_ is UART0 in C++ except on LibreTiny where it is DEFAULT; skip the
|
||||
# setter when the config matches it.
|
||||
cpp_default_uart = DEFAULT if CORE.is_libretiny else UART0
|
||||
if (
|
||||
hardware_uart := config.get(CONF_HARDWARE_UART)
|
||||
) is not None and hardware_uart != cpp_default_uart:
|
||||
cg.add(log.set_uart_selection(HARDWARE_UART_TO_UART_SELECTION[hardware_uart]))
|
||||
if (hardware_uart := config.get(CONF_HARDWARE_UART)) is not None:
|
||||
cg.add_define(f"USE_LOGGER_UART_SELECTION_{hardware_uart}")
|
||||
cg.add_define(
|
||||
"ESPHOME_LOGGER_UART_SELECTION",
|
||||
cg.RawExpression(f"logger::UART_SELECTION_{hardware_uart}"),
|
||||
)
|
||||
# ESP8266 without serial logging never opens a port
|
||||
cg.add_define(
|
||||
"ESPHOME_LOGGER_UART_NAME",
|
||||
"NONE" if CORE.is_esp8266 and baud_rate == 0 else hardware_uart,
|
||||
)
|
||||
# pre_setup() sets global_logger and must run before any other code
|
||||
# that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV).
|
||||
cg.add(log.pre_setup())
|
||||
@@ -647,10 +644,8 @@ async def _late_logger_init(config: ConfigType) -> None:
|
||||
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
|
||||
# Log V2 formats centrally in esp_log() (~4KB flash, ~180B RAM). 6.1
|
||||
# adds CONSTRAINED_ENV_SAFE=n, keeping early/DRAM logs on
|
||||
# esp_rom_printf instead of pulling ~1.2KB esp_rom_vprintf into IRAM.
|
||||
@@ -667,8 +662,13 @@ async def _late_logger_init(config: ConfigType) -> None:
|
||||
cg.add_define("USE_ESP32_LOG_V2")
|
||||
# --wrap: a strong definition collides; see logger_esp32.cpp
|
||||
cg.add_build_flag("-Wl,--wrap=esp_log_format")
|
||||
if CORE.is_rp2 and config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
# Zephyr polls the USB port only when it logs there
|
||||
if (
|
||||
CORE.using_zephyr
|
||||
and has_serial_logging
|
||||
and config[CONF_HARDWARE_UART] == USB_CDC
|
||||
):
|
||||
cg.add_define("USE_LOGGER_ZEPHYR_CDC_LOOP")
|
||||
# Define platform support flags for components that need auto-detection
|
||||
try:
|
||||
uart_selection(USB_SERIAL_JTAG)
|
||||
@@ -718,7 +718,6 @@ async def _late_logger_init(config: ConfigType) -> None:
|
||||
if config[CONF_HARDWARE_UART] == UART1:
|
||||
zephyr_add_overlay("""&uart1 { status = "okay";};""")
|
||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
|
||||
zephyr_add_prj_conf("UART_LINE_CTRL", True)
|
||||
|
||||
# Register at end for safe mode
|
||||
@@ -739,8 +738,6 @@ async def _late_logger_init(config: ConfigType) -> None:
|
||||
conf,
|
||||
)
|
||||
|
||||
CORE.add_job(final_step)
|
||||
|
||||
|
||||
def validate_printf(value: ConfigType) -> ConfigType:
|
||||
# https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python
|
||||
@@ -875,11 +872,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
}
|
||||
)
|
||||
|
||||
# Keys for CORE.data storage
|
||||
DOMAIN = "logger"
|
||||
KEY_LEVEL_LISTENERS = "level_listeners"
|
||||
KEY_LOG_LISTENERS = "log_listeners"
|
||||
|
||||
|
||||
def request_logger_level_listeners() -> None:
|
||||
"""Request that logger level listeners be compiled in.
|
||||
@@ -888,29 +880,18 @@ def request_logger_level_listeners() -> None:
|
||||
function during their code generation. This enables the add_level_listener()
|
||||
method and compiles in the listener vector.
|
||||
"""
|
||||
CORE.data.setdefault(DOMAIN, {})[KEY_LEVEL_LISTENERS] = True
|
||||
cg.add_define("USE_LOGGER_LEVEL_LISTENERS")
|
||||
|
||||
|
||||
_request_log_listener_slot = cg.slot_counter("ESPHOME_LOG_MAX_LISTENERS")
|
||||
|
||||
|
||||
def request_console() -> None:
|
||||
"""Compile in Logger::console_read() and console_write(); call from to_code."""
|
||||
cg.add_define("USE_LOGGER_CONSOLE")
|
||||
|
||||
|
||||
def request_log_listener() -> None:
|
||||
"""Request a log listener slot.
|
||||
|
||||
Components that need to receive log messages should call this function
|
||||
during their code generation. This increments the listener count used
|
||||
to size the StaticVector.
|
||||
"""
|
||||
data = CORE.data.setdefault(DOMAIN, {})
|
||||
data[KEY_LOG_LISTENERS] = data.get(KEY_LOG_LISTENERS, 0) + 1
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def final_step() -> None:
|
||||
"""Final code generation step to configure optional logger features."""
|
||||
domain_data = CORE.data.get(DOMAIN, {})
|
||||
if domain_data.get(KEY_LEVEL_LISTENERS, False):
|
||||
cg.add_define("USE_LOGGER_LEVEL_LISTENERS")
|
||||
|
||||
# Only generate log listener code if any component needs it
|
||||
log_listener_count = domain_data.get(KEY_LOG_LISTENERS, 0)
|
||||
if log_listener_count > 0:
|
||||
cg.add_define("USE_LOG_LISTENERS")
|
||||
cg.add_define("ESPHOME_LOG_MAX_LISTENERS", log_listener_count)
|
||||
"""Reserve a log listener slot; call once per listener from to_code."""
|
||||
cg.add_define("USE_LOG_LISTENERS")
|
||||
_request_log_listener_slot()
|
||||
@@ -32,7 +32,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// Get task handle once - used for both main task check and passing to non-main thread handler
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
const bool is_main_task = (current_task == this->main_task_);
|
||||
#elif (USE_ZEPHYR)
|
||||
#elif defined(USE_ZEPHYR)
|
||||
k_tid_t current_task = k_current_get();
|
||||
const bool is_main_task = (current_task == this->main_task_);
|
||||
#else // USE_HOST
|
||||
@@ -57,10 +57,10 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// (we already have the handle from the main task check above).
|
||||
// Host: pass a stack buffer for pthread_getname_np to write into.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
const char *thread_name = get_thread_name_(current_task);
|
||||
const char *thread_name = this->get_thread_name_(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char thread_name_buf[MAX_POINTER_REPRESENTATION];
|
||||
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
|
||||
const char *thread_name = this->get_thread_name_(thread_name_buf, current_task);
|
||||
#else // USE_HOST
|
||||
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||
@@ -103,10 +103,10 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
{
|
||||
#ifdef USE_HOST
|
||||
// Host always has console output - no baud_rate check needed
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
||||
static constexpr size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
||||
#else
|
||||
// Maximum size for console log messages (includes null terminator)
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
||||
static constexpr size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
||||
#endif
|
||||
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
||||
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
|
||||
@@ -118,13 +118,11 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
}
|
||||
#else
|
||||
// Implementation for single-task platforms (ESP8266, RP2040)
|
||||
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
||||
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
||||
// Logging calls are NOT thread-safe: the recursion guard is a plain bool and tx_buffer_ has no locking.
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
if (level > this->level_for(tag) || this->main_task_recursion_guard_)
|
||||
return;
|
||||
// Other single-task platforms don't have thread names, so pass nullptr
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
}
|
||||
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
|
||||
|
||||
@@ -136,10 +134,10 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
//
|
||||
void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
|
||||
va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
if (level > this->level_for(tag) || this->main_task_recursion_guard_)
|
||||
return;
|
||||
|
||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
}
|
||||
#endif // USE_ESP8266
|
||||
|
||||
@@ -184,10 +182,10 @@ Logger::Logger(uint32_t baud_rate) : baud_rate_(baud_rate) {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
|
||||
void Logger::loop() {
|
||||
this->process_messages_();
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
this->cdc_loop_();
|
||||
#endif
|
||||
}
|
||||
@@ -213,11 +211,11 @@ void Logger::process_messages_() {
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
}
|
||||
}
|
||||
#if !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
// Zephyr with USB CDC keeps the loop running for cdc_loop_()
|
||||
#ifndef USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
// This reduces overhead when there's no async logging activity
|
||||
this->disable_loop_when_buffer_empty_();
|
||||
// A concurrent enable_loop_soon_any_context() still wakes the next iteration
|
||||
this->disable_loop();
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
@@ -265,7 +263,7 @@ void Logger::boot_log_capture_(uint8_t level, const char *tag, const LogBuffer &
|
||||
|
||||
void Logger::replay_boot_logs() {
|
||||
// The replay shares tx_buffer_ with normal logging, so it is dropped like any other re-entrant log
|
||||
bool &recursion_guard = this->main_loop_recursion_guard_();
|
||||
bool &recursion_guard = this->main_task_recursion_guard_;
|
||||
if (recursion_guard)
|
||||
return;
|
||||
this->boot_log_done_ = true; // Stops capture first, so the replay output is not buffered again
|
||||
@@ -307,6 +305,25 @@ static const LogString *get_log_level_str(uint8_t level) {
|
||||
return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
#ifndef USE_HOST
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
#ifdef USE_LOGGER_UART_SELECTION_DEFAULT
|
||||
// LibreTiny resolves DEFAULT to the board's port only when serial logging is on
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->get_uart()) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
default:
|
||||
return LOG_STR("UART2");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return LOG_STR(ESPHOME_LOGGER_UART_NAME);
|
||||
}
|
||||
#endif
|
||||
|
||||
void Logger::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Logger:\n"
|
||||
@@ -342,7 +359,7 @@ void Logger::dump_config() {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZEPHYR
|
||||
dump_crash_();
|
||||
this->dump_crash_();
|
||||
#ifdef USE_BOOTLOADER_ADAFRUIT
|
||||
this->dump_bootloader_();
|
||||
#endif
|
||||
|
||||
@@ -42,9 +42,6 @@ struct device;
|
||||
namespace esphome::logger {
|
||||
|
||||
/** Lightweight callback for receiving log messages without virtual dispatch overhead.
|
||||
*
|
||||
* Replaces the former LogListener virtual interface to eliminate per-implementer
|
||||
* vtable sub-tables and thunk code (~39 bytes saved per class that used LogListener).
|
||||
*
|
||||
* Usage:
|
||||
* // In your component's setup():
|
||||
@@ -73,7 +70,7 @@ struct LogCallback {
|
||||
* public:
|
||||
* void setup() override {
|
||||
* if (logger::global_logger != nullptr)
|
||||
* logger::global_logger->add_logger_level_listener(this);
|
||||
* logger::global_logger->add_level_listener(this);
|
||||
* }
|
||||
* void on_log_level_change(uint8_t level) override {
|
||||
* // Handle log level change
|
||||
@@ -127,6 +124,9 @@ enum UARTSelection : uint8_t {
|
||||
UART_SELECTION_UART0_SWAP,
|
||||
#endif // USE_ESP8266
|
||||
};
|
||||
|
||||
// The port chosen by hardware_uart, fixed at build time
|
||||
static constexpr UARTSelection CONFIGURED_UART_SELECTION = ESPHOME_LOGGER_UART_SELECTION;
|
||||
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2 || USE_LIBRETINY || USE_ZEPHYR
|
||||
|
||||
/**
|
||||
@@ -138,7 +138,7 @@ enum UARTSelection : uint8_t {
|
||||
* - On ESP32: Uses task-specific recursion guards
|
||||
* * Main task: Uses a dedicated boolean member variable for efficiency
|
||||
* * Other tasks: Uses pthread TLS with a dynamically allocated key for task-specific state
|
||||
* - On other platforms: Uses a simple global recursion guard
|
||||
* - On single-task platforms: Uses the main task guard alone
|
||||
*
|
||||
* We use pthread TLS via pthread_key_create to create a unique key for storing
|
||||
* task-specific recursion state, which:
|
||||
@@ -149,7 +149,7 @@ enum UARTSelection : uint8_t {
|
||||
class Logger final : public Component {
|
||||
public:
|
||||
explicit Logger(uint32_t baud_rate);
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
|
||||
void loop() override;
|
||||
#endif
|
||||
#if defined(USE_RP2_CRASH_HANDLER) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
@@ -157,21 +157,52 @@ class Logger final : public Component {
|
||||
#endif
|
||||
/// Manually set the baud rate for serial, set to 0 to disable.
|
||||
void set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
uint32_t get_baud_rate() const { return this->baud_rate_; }
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
Stream *get_hw_serial() const { return hw_serial_; }
|
||||
Stream *get_hw_serial() const { return this->hw_serial_; }
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
uart_port_t get_uart_num() const { return uart_num_; }
|
||||
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
|
||||
/// UART_NUM_0 when logging to USB
|
||||
constexpr uart_port_t get_uart_num() const {
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
|
||||
return UART_NUM_0;
|
||||
#else
|
||||
return static_cast<uart_port_t>(CONFIGURED_UART_SELECTION);
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
|
||||
}
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
void create_pthread_key() { pthread_key_create(&this->log_recursion_key_, nullptr); }
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
|
||||
/// Get the UART used by the logger.
|
||||
UARTSelection get_uart() const { return this->uart_; }
|
||||
#ifdef USE_LIBRETINY
|
||||
// DEFAULT resolves to the board's default port
|
||||
constexpr UARTSelection get_uart() const {
|
||||
return CONFIGURED_UART_SELECTION == UART_SELECTION_DEFAULT ? static_cast<UARTSelection>(LT_UART_DEFAULT_SERIAL + 1)
|
||||
: CONFIGURED_UART_SELECTION;
|
||||
}
|
||||
#else
|
||||
constexpr UARTSelection get_uart() const { return CONFIGURED_UART_SELECTION; }
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_LOGGER_CONSOLE
|
||||
// Raw bytes on the console port, without log framing; request with logger.request_console()
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
size_t console_write(const uint8_t *data, size_t len) { return this->hw_serial_->write(data, len); }
|
||||
bool console_read(uint8_t &byte) {
|
||||
int c = this->hw_serial_->read();
|
||||
if (c < 0)
|
||||
return false;
|
||||
byte = c;
|
||||
return true;
|
||||
}
|
||||
#elif defined(USE_ESP32)
|
||||
/// Blocking write; returns the bytes written.
|
||||
size_t console_write(const uint8_t *data, size_t len);
|
||||
/// Non-blocking read of one byte; false if none is waiting.
|
||||
bool console_read(uint8_t &byte);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// Set the default log level for this logger.
|
||||
@@ -242,14 +273,17 @@ class Logger final : public Component {
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
const char *thread_name);
|
||||
#endif
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
void cdc_loop_();
|
||||
#endif
|
||||
void process_messages_();
|
||||
#if defined(USE_HOST) || defined(USE_ZEPHYR)
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
// msg already ends in a newline
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
inline void HOT write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
|
||||
#elif defined(USE_ESP32)
|
||||
inline void write_msg_(const char *msg, uint16_t len); // Defined in logger_esp32.h
|
||||
#else
|
||||
inline void write_msg_(const char *msg, uint16_t len); // Defined in platform-specific logger_*.h
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
#endif
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
@@ -317,7 +351,6 @@ class Logger final : public Component {
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// Helper to format a pre-formatted message from the task log buffer and notify listeners
|
||||
// Used by process_messages_ to avoid code duplication between ESP32 and host platforms
|
||||
inline void HOT format_buffered_message_and_notify_(uint8_t level, const char *tag, uint16_t line,
|
||||
const char *thread_name, const char *text, uint16_t text_length,
|
||||
LogBuffer &buf) {
|
||||
@@ -359,15 +392,8 @@ class Logger final : public Component {
|
||||
#ifdef USE_HOST
|
||||
pthread_t main_thread_{}; // Main thread for pthread_equal() comparison
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
// Task-specific recursion guards:
|
||||
// - Main task uses a dedicated member variable for efficiency
|
||||
// - Other tasks use pthread TLS with a dynamically created key via pthread_key_create
|
||||
pthread_key_t log_recursion_key_; // 4 bytes
|
||||
uart_port_t uart_num_; // 4 bytes (enum defaults to int size)
|
||||
#endif
|
||||
#ifdef USE_HOST
|
||||
// Thread-specific recursion guards using pthread TLS
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
// Non-main task recursion guard (pthread TLS)
|
||||
pthread_key_t log_recursion_key_;
|
||||
#endif
|
||||
|
||||
@@ -392,23 +418,10 @@ class Logger final : public Component {
|
||||
bool boot_log_done_{false}; // Set on first replay; stops further capture
|
||||
#endif
|
||||
uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_ZEPHYR)
|
||||
UARTSelection uart_{UART_SELECTION_UART0}; // Must match cpp_default_uart in __init__.py
|
||||
#endif
|
||||
#ifdef USE_LIBRETINY
|
||||
UARTSelection uart_{UART_SELECTION_DEFAULT}; // Must match cpp_default_uart in __init__.py
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
bool main_task_recursion_guard_{false};
|
||||
bool main_task_recursion_guard_{false}; // Guards tx_buffer_ on the main loop
|
||||
#ifdef USE_LIBRETINY
|
||||
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
|
||||
#endif
|
||||
// The guard that protects tx_buffer_ on the main loop, for callers that run on any platform
|
||||
bool &main_loop_recursion_guard_() { return this->main_task_recursion_guard_; }
|
||||
#else
|
||||
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
||||
bool &main_loop_recursion_guard_() { return this->global_recursion_guard_; }
|
||||
#endif
|
||||
|
||||
// Large buffers placed last to keep frequently-accessed member offsets small
|
||||
char tx_buffer_[ESPHOME_LOGGER_TX_BUFFER_SIZE + 1]; // +1 for null terminator
|
||||
@@ -477,9 +490,9 @@ class Logger final : public Component {
|
||||
class NonMainTaskRecursionGuard {
|
||||
public:
|
||||
explicit NonMainTaskRecursionGuard(pthread_key_t key) : key_(key) {
|
||||
pthread_setspecific(key_, reinterpret_cast<void *>(1));
|
||||
pthread_setspecific(this->key_, reinterpret_cast<void *>(1));
|
||||
}
|
||||
~NonMainTaskRecursionGuard() { pthread_setspecific(key_, nullptr); }
|
||||
~NonMainTaskRecursionGuard() { pthread_setspecific(this->key_, nullptr); }
|
||||
NonMainTaskRecursionGuard(const NonMainTaskRecursionGuard &) = delete;
|
||||
NonMainTaskRecursionGuard &operator=(const NonMainTaskRecursionGuard &) = delete;
|
||||
NonMainTaskRecursionGuard(NonMainTaskRecursionGuard &&) = delete;
|
||||
@@ -490,40 +503,24 @@ class Logger final : public Component {
|
||||
};
|
||||
|
||||
// Check if non-main task is already in recursion (via TLS)
|
||||
inline bool HOT is_non_main_task_recursive_() const { return pthread_getspecific(log_recursion_key_) != nullptr; }
|
||||
inline bool HOT is_non_main_task_recursive_() const {
|
||||
return pthread_getspecific(this->log_recursion_key_) != nullptr;
|
||||
}
|
||||
|
||||
// Create RAII guard for non-main task recursion
|
||||
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
|
||||
|
||||
#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
|
||||
// - Main task uses dedicated boolean (same as ESP32)
|
||||
// - Non-main tasks share a single recursion guard
|
||||
// This is safe because:
|
||||
// - Recursion from logging within logging is the main concern
|
||||
// - Cross-task "recursion" is prevented by the buffer mutex anyway
|
||||
// - Missing a recursive call from another task is acceptable (falls back to direct output)
|
||||
//
|
||||
// Zephyr use __thread as TLS
|
||||
|
||||
// Check if non-main task is already in recursion
|
||||
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
|
||||
|
||||
// Create RAII guard for non-main task recursion (uses shared boolean for all non-main tasks)
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) && defined(USE_LOGGER_UART_SELECTION_USB_CDC))
|
||||
// Disable loop when task buffer is empty
|
||||
// Zephyr with USB CDC needs loop active to poll port readiness via cdc_loop_()
|
||||
inline void disable_loop_when_buffer_empty_() {
|
||||
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
|
||||
// concurrently. If that happens between our check and disable_loop(), the enable request
|
||||
// will be processed on the next main loop iteration since:
|
||||
// - disable_loop() takes effect immediately
|
||||
// - enable_loop_soon_any_context() sets a pending flag that's checked at loop start
|
||||
this->disable_loop();
|
||||
inline NonMainTaskRecursionGuard make_non_main_task_guard_() {
|
||||
return NonMainTaskRecursionGuard(this->log_recursion_key_);
|
||||
}
|
||||
|
||||
#elif defined(USE_LIBRETINY)
|
||||
// No FreeRTOS TLS on LibreTiny: all non-main tasks share one guard
|
||||
inline bool HOT is_non_main_task_recursive_() const { return this->non_main_task_recursion_guard_; }
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(this->non_main_task_recursion_guard_); }
|
||||
|
||||
#elif defined(USE_ZEPHYR)
|
||||
// Thread-local guard (__thread)
|
||||
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard; }
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard); }
|
||||
#endif
|
||||
};
|
||||
extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -563,14 +560,7 @@ void log_crash_to_client();
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
// Platform-specific inline implementations of write_msg_()
|
||||
// Must be included after the Logger class definition is complete
|
||||
#if defined(USE_ESP32)
|
||||
// ESP32 inline write_msg_(), included after the Logger class definition is complete
|
||||
#ifdef USE_ESP32
|
||||
#include "logger_esp32.h"
|
||||
#elif defined(USE_ESP8266)
|
||||
#include "logger_esp8266.h"
|
||||
#elif defined(USE_RP2)
|
||||
#include "logger_rp2.h"
|
||||
#elif defined(USE_LIBRETINY)
|
||||
#include "logger_libretiny.h"
|
||||
#endif
|
||||
@@ -17,6 +17,9 @@
|
||||
#include <driver/usb_serial_jtag_vfs.h>
|
||||
#endif
|
||||
#endif
|
||||
#if defined(USE_LOGGER_CONSOLE) && defined(USE_LOGGER_UART_SELECTION_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
|
||||
#include <esp_private/usb_console.h>
|
||||
#endif
|
||||
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
|
||||
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
|
||||
#include "esp_sleep.h"
|
||||
@@ -73,7 +76,8 @@ static void init_usb_serial_jtag() {
|
||||
}
|
||||
#endif
|
||||
|
||||
void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
|
||||
#if !defined(USE_LOGGER_UART_SELECTION_USB_CDC) && !defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
|
||||
static void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
|
||||
uart_config_t uart_config{};
|
||||
uart_config.baud_rate = (int) baud_rate;
|
||||
uart_config.data_bits = UART_DATA_8_BITS;
|
||||
@@ -97,37 +101,22 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
|
||||
esp_sleep_set_console_uart_handling_mode(ESP_SLEEP_ALWAYS_FLUSH_UART);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// get_uart_num() and pre_setup() cast the selection straight to a port
|
||||
static_assert(static_cast<int>(UART_SELECTION_UART0) == static_cast<int>(UART_NUM_0) &&
|
||||
static_cast<int>(UART_SELECTION_UART1) == static_cast<int>(UART_NUM_1));
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
static_assert(static_cast<int>(UART_SELECTION_UART2) == static_cast<int>(UART_NUM_2));
|
||||
#endif
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->uart_num_ = UART_NUM_0;
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
this->uart_num_ = UART_NUM_0;
|
||||
init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->uart_num_ = UART_NUM_1;
|
||||
init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE);
|
||||
break;
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
case UART_SELECTION_UART2:
|
||||
this->uart_num_ = UART_NUM_2;
|
||||
init_uart(this->uart_num_, baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE);
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_LOGGER_USB_SERIAL_JTAG
|
||||
case UART_SELECTION_USB_SERIAL_JTAG:
|
||||
#ifdef USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG
|
||||
init_usb_serial_jtag();
|
||||
init_usb_serial_jtag();
|
||||
#elif !defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
init_uart(this->get_uart_num(), this->baud_rate_, ESPHOME_LOGGER_TX_BUFFER_SIZE);
|
||||
#endif
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
@@ -139,29 +128,36 @@ void Logger::pre_setup() {
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
case UART_SELECTION_UART2:
|
||||
return LOG_STR("UART2");
|
||||
#ifdef USE_LOGGER_CONSOLE
|
||||
size_t Logger::console_write(const uint8_t *data, size_t len) {
|
||||
int written = 0;
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
#ifdef CONFIG_ESP_CONSOLE_USB_CDC
|
||||
written = esp_usb_console_write_buf(reinterpret_cast<const char *>(data), len);
|
||||
#endif
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#elif defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
|
||||
written = usb_serial_jtag_write_bytes(data, len, 20 / portTICK_PERIOD_MS);
|
||||
#else
|
||||
written = uart_write_bytes(this->get_uart_num(), data, len);
|
||||
#endif
|
||||
#ifdef USE_LOGGER_USB_SERIAL_JTAG
|
||||
case UART_SELECTION_USB_SERIAL_JTAG:
|
||||
return LOG_STR("USB_SERIAL_JTAG");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return written > 0 ? written : 0;
|
||||
}
|
||||
|
||||
bool Logger::console_read(uint8_t &byte) {
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
#ifdef CONFIG_ESP_CONSOLE_USB_CDC
|
||||
return esp_usb_console_available_for_read() && esp_usb_console_read_buf(reinterpret_cast<char *>(&byte), 1) == 1;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
#elif defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
|
||||
return usb_serial_jtag_read_bytes(&byte, 1, 0) > 0;
|
||||
#else
|
||||
return uart_read_bytes(this->get_uart_num(), &byte, 1, 0) > 0;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CRASH_HANDLER
|
||||
bool crash_log_pending() { return esp32::crash_handler_has_data(); }
|
||||
void log_crash_to_client() {
|
||||
|
||||
@@ -19,7 +19,7 @@ inline void HOT Logger::write_msg_(const char *msg, uint16_t len) {
|
||||
fwrite(msg, 1, len, stdout);
|
||||
#else
|
||||
// Regular UART - single write including newline (already in buffer)
|
||||
uart_write_bytes(this->uart_num_, msg, len);
|
||||
uart_write_bytes(this->get_uart_num(), msg, len);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ void Logger::pre_setup() {
|
||||
#if defined(USE_ESP8266_LOGGER_SERIAL)
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
Serial.swap();
|
||||
}
|
||||
#ifdef USE_LOGGER_UART_SELECTION_UART0_SWAP
|
||||
Serial.swap();
|
||||
#endif
|
||||
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
#elif defined(USE_ESP8266_LOGGER_SERIAL1)
|
||||
this->hw_serial_ = &Serial1;
|
||||
@@ -35,19 +35,6 @@ void Logger::pre_setup() {
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
#if defined(USE_ESP8266_LOGGER_SERIAL)
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
return LOG_STR("UART0_SWAP");
|
||||
}
|
||||
return LOG_STR("UART0");
|
||||
#elif defined(USE_ESP8266_LOGGER_SERIAL1)
|
||||
return LOG_STR("UART1");
|
||||
#else
|
||||
return LOG_STR("NONE");
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_CRASH_HANDLER
|
||||
bool crash_log_pending() { return esp8266::crash_handler_has_data(); }
|
||||
void log_crash_to_client() { esp8266::crash_handler_log(); }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// Single write with newline already in buffer (added by caller)
|
||||
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -12,45 +12,28 @@ ESPHOME_LOG_TAG(TAG, "logger");
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
#if LT_HW_UART0
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial0;
|
||||
libretiny::ensure_serial_handle(0);
|
||||
Serial0.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART1
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
libretiny::ensure_serial_handle(1);
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART2
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
libretiny::ensure_serial_handle(2);
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ != UART_SELECTION_DEFAULT) {
|
||||
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
|
||||
"The default port was used instead.");
|
||||
}
|
||||
break;
|
||||
#if defined(USE_LOGGER_UART_SELECTION_UART0) && LT_HW_UART0
|
||||
this->hw_serial_ = &Serial0;
|
||||
libretiny::ensure_serial_handle(0);
|
||||
Serial0.begin(this->baud_rate_);
|
||||
#elif defined(USE_LOGGER_UART_SELECTION_UART1) && LT_HW_UART1
|
||||
this->hw_serial_ = &Serial1;
|
||||
libretiny::ensure_serial_handle(1);
|
||||
Serial1.begin(this->baud_rate_);
|
||||
#elif defined(USE_LOGGER_UART_SELECTION_UART2) && LT_HW_UART2
|
||||
this->hw_serial_ = &Serial2;
|
||||
libretiny::ensure_serial_handle(2);
|
||||
Serial2.begin(this->baud_rate_);
|
||||
#else
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (CONFIGURED_UART_SELECTION != UART_SELECTION_DEFAULT) {
|
||||
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
|
||||
"The default port was used instead.");
|
||||
}
|
||||
|
||||
#endif
|
||||
// change lt_log() port to match default Serial
|
||||
if (this->uart_ == UART_SELECTION_DEFAULT) {
|
||||
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
|
||||
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
|
||||
} else {
|
||||
lt_log_set_port(this->uart_ - 1);
|
||||
}
|
||||
lt_log_set_port(this->get_uart() - 1);
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
@@ -60,20 +43,6 @@ void Logger::pre_setup() {
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_DEFAULT:
|
||||
return LOG_STR("DEFAULT");
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART2:
|
||||
default:
|
||||
return LOG_STR("UART2");
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_LIBRETINY_CRASH_HANDLER
|
||||
bool crash_log_pending() { return libretiny::crash_handler_has_data(); }
|
||||
void log_crash_to_client() { libretiny::crash_handler_log(); }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// Single write with newline already in buffer (added by caller)
|
||||
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -26,20 +26,16 @@ void Logger::setup() {
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_USB_CDC:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
break;
|
||||
}
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
#elif defined(USE_LOGGER_UART_SELECTION_UART1)
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
#else
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
#endif
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
@@ -49,21 +45,6 @@ void Logger::pre_setup() {
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_CRASH_HANDLER
|
||||
bool crash_log_pending() { return rp2::crash_handler_has_data(); }
|
||||
void log_crash_to_client() { rp2::crash_handler_log(); }
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_RP2
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// Single write with newline already in buffer (added by caller)
|
||||
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -42,9 +42,9 @@ __attribute__((section(".noinit"))) struct {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "logger");
|
||||
|
||||
#ifdef USE_LOGGER_UART_SELECTION_USB_CDC
|
||||
#ifdef USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
void Logger::cdc_loop_() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
||||
if (this->uart_dev_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
static bool opened = false;
|
||||
@@ -74,26 +74,20 @@ void Logger::cdc_loop_() {
|
||||
void Logger::pre_setup() {
|
||||
#ifdef CONFIG_SERIAL
|
||||
if (this->baud_rate_ > 0) {
|
||||
static const struct device *uart_dev = nullptr;
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0: // NOLINT(bugprone-branch-clone)
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ESPHOME_LOGGER_UART0_NODE));
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
|
||||
break;
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
const struct device *uart_dev = nullptr;
|
||||
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
#if defined(CONFIG_USB_DEVICE_STACK) || defined(CONFIG_USB_DEVICE_STACK_NEXT)
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||
if (device_is_ready(uart_dev)) {
|
||||
// Nothing can log this early, and the logger has no output without USB anyway
|
||||
zephyr::usb_device_enable();
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
#endif
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||
if (device_is_ready(uart_dev)) {
|
||||
// Nothing can log this early, and the logger has no output without USB anyway
|
||||
zephyr::usb_device_enable();
|
||||
}
|
||||
#endif
|
||||
#elif defined(USE_LOGGER_UART_SELECTION_UART1)
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
|
||||
#else
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(ESPHOME_LOGGER_UART0_NODE));
|
||||
#endif
|
||||
if (device_is_ready(uart_dev)) {
|
||||
this->uart_dev_ = uart_dev;
|
||||
#if defined(USE_LOGGER_WAIT_FOR_CDC) && defined(USE_LOGGER_UART_SELECTION_USB_CDC)
|
||||
@@ -114,14 +108,10 @@ void Logger::pre_setup() {
|
||||
char reason_buffer[zephyr::RESET_REASON_BUFFER_SIZE];
|
||||
const char *reset_reason = zephyr::get_reset_reason(std::span<char, zephyr::RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
ESP_LOGI(TAG, "Reset reason: %s", reset_reason);
|
||||
dump_crash_();
|
||||
this->dump_crash_();
|
||||
#endif
|
||||
#ifdef USE_CRASH_HANDLER
|
||||
#ifdef USE_LOGGER_UART_SELECTION_USB_CDC
|
||||
// Nobody reads a USB port this early; cdc_loop_() reports the dump once the port is opened
|
||||
if (this->uart_ == UART_SELECTION_USB_CDC)
|
||||
return;
|
||||
#endif
|
||||
#if defined(USE_CRASH_HANDLER) && !defined(USE_LOGGER_ZEPHYR_CDC_LOOP)
|
||||
// Without a UART (baud_rate: 0, or one that did not come up) nothing reaches the console
|
||||
if (this->uart_dev_ != nullptr)
|
||||
log_crash_to_client();
|
||||
@@ -143,22 +133,7 @@ void HOT Logger::write_msg_(const char *msg, uint16_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
static const uint8_t REASON_BUF_SIZE = 32;
|
||||
static constexpr uint8_t REASON_BUF_SIZE = 32;
|
||||
|
||||
static const char *reason_to_str(unsigned int reason, char *buf) {
|
||||
switch (reason) {
|
||||
|
||||
@@ -44,19 +44,15 @@ void TaskLogBuffer::release_message_main_loop() {
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
// Measure on a copy; the caller's va_list formats the text later
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
uint16_t text_length = ret;
|
||||
|
||||
// Calculate total size needed (header + text length + null terminator)
|
||||
size_t total_size = sizeof(LogMessage) + text_length + 1;
|
||||
@@ -69,24 +65,10 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
return false; // Failed to acquire memory
|
||||
}
|
||||
|
||||
// Set up the message header in the acquired memory
|
||||
LogMessage *msg = static_cast<LogMessage *>(acquired_memory);
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
|
||||
// Store the thread name now instead of waiting until main loop processing
|
||||
// This avoids crashes if the task completes or is deleted between when this message
|
||||
// is enqueued and when it's processed by the main loop
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
} else {
|
||||
msg->thread_name[0] = '\0'; // Empty string if no thread name
|
||||
}
|
||||
fill_task_log_header(msg, level, tag, line, thread_name);
|
||||
|
||||
// Format the message text directly into the acquired memory
|
||||
// We add 1 to text_length to ensure space for null terminator during formatting
|
||||
char *text_area = msg->text_data();
|
||||
ret = vsnprintf(text_area, text_length + 1, format, args);
|
||||
|
||||
@@ -96,12 +78,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove trailing newlines
|
||||
while (text_length > 0 && text_area[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
|
||||
msg->text_length = text_length;
|
||||
msg->text_length = trim_trailing_newlines(text_area, text_length);
|
||||
// Complete the send operation with the acquired memory
|
||||
result = xRingbufferSendComplete(this->handle_(), acquired_memory);
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/ringbuf.h>
|
||||
|
||||
#include "task_log_message.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
/**
|
||||
@@ -32,19 +34,7 @@ namespace esphome::logger {
|
||||
*/
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Structure for a log message header (text data follows immediately after)
|
||||
struct LogMessage {
|
||||
const char *tag; // We store the pointer, assuming tags are static
|
||||
char thread_name[16]; // Store thread name directly (only used for non-main threads)
|
||||
uint16_t text_length; // Length of the message text (up to ~64KB)
|
||||
uint16_t line; // Source code line number
|
||||
uint8_t level; // Log level (0-7)
|
||||
|
||||
// Methods for accessing message contents
|
||||
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
|
||||
|
||||
inline const char *text_data() const { return reinterpret_cast<const char *>(this) + sizeof(LogMessage); }
|
||||
};
|
||||
using LogMessage = TaskLogMessage;
|
||||
|
||||
TaskLogBuffer();
|
||||
// No destructor: Logger is never destroyed
|
||||
|
||||
@@ -71,18 +71,8 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
|
||||
LogMessage &msg = this->slots_[slot_index];
|
||||
|
||||
// Fill in the message header
|
||||
msg.level = level;
|
||||
msg.tag = tag;
|
||||
msg.line = line;
|
||||
|
||||
// Store the thread name now to avoid crashes if thread exits before processing
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
|
||||
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
|
||||
} else {
|
||||
msg.thread_name[0] = '\0';
|
||||
}
|
||||
// Copied now; the thread may exit before processing
|
||||
fill_task_log_header(&msg, level, tag, line, thread_name);
|
||||
|
||||
// Format the message text
|
||||
int ret = vsnprintf(msg.text, sizeof(msg.text), format, args);
|
||||
@@ -94,10 +84,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
msg.text_length = static_cast<uint16_t>(std::min(static_cast<size_t>(ret), sizeof(msg.text) - 1));
|
||||
}
|
||||
|
||||
// Remove trailing newlines
|
||||
while (msg.text_length > 0 && msg.text[msg.text_length - 1] == '\n') {
|
||||
msg.text_length--;
|
||||
}
|
||||
msg.text_length = trim_trailing_newlines(msg.text, msg.text_length);
|
||||
msg.text[msg.text_length] = '\0';
|
||||
|
||||
// Commit the slot
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <cstring>
|
||||
#include <pthread.h>
|
||||
|
||||
#include "task_log_message.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,13 +13,6 @@ TaskLogBuffer::TaskLogBuffer() {
|
||||
this->mutex_ = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
TaskLogBuffer::~TaskLogBuffer() {
|
||||
if (this->mutex_ != nullptr) {
|
||||
vSemaphoreDelete(this->mutex_);
|
||||
this->mutex_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
size_t TaskLogBuffer::available_contiguous_space_() const {
|
||||
if (this->head_ >= this->tail_) {
|
||||
// head is ahead of or equal to tail
|
||||
@@ -85,19 +78,15 @@ void TaskLogBuffer::release_message_main_loop() {
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
// Measure on a copy; the caller's va_list formats the text later
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
uint16_t text_length = ret;
|
||||
|
||||
// Calculate total size needed (header + text length + null terminator)
|
||||
size_t total_size = message_total_size(text_length);
|
||||
@@ -128,17 +117,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
|
||||
// Write message header
|
||||
LogMessage *msg = reinterpret_cast<LogMessage *>(this->storage_ + write_pos);
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
|
||||
// Store the thread name now to avoid crashes if task is deleted before processing
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
|
||||
} else {
|
||||
msg->thread_name[0] = '\0';
|
||||
}
|
||||
fill_task_log_header(msg, level, tag, line, thread_name);
|
||||
|
||||
// Format the message text directly into the buffer
|
||||
char *text_area = msg->text_data();
|
||||
@@ -150,9 +129,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
}
|
||||
|
||||
// Trim before storing so the next message reuses the freed bytes
|
||||
while (text_length > 0 && text_area[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
text_length = trim_trailing_newlines(text_area, text_length);
|
||||
text_area[text_length] = '\0';
|
||||
msg->text_length = text_length;
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <semphr.h>
|
||||
#include <task.h>
|
||||
|
||||
#include "task_log_message.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
/**
|
||||
@@ -42,25 +44,14 @@ namespace esphome::logger {
|
||||
*/
|
||||
class TaskLogBuffer {
|
||||
public:
|
||||
// Structure for a log message header (text data follows immediately after)
|
||||
struct LogMessage {
|
||||
const char *tag; // We store the pointer, assuming tags are static
|
||||
char thread_name[16]; // Store thread name directly (only used for non-main threads)
|
||||
uint16_t text_length; // Length of the message text (up to ~64KB)
|
||||
uint16_t line; // Source code line number
|
||||
uint8_t level; // Log level (0-7)
|
||||
|
||||
// Methods for accessing message contents
|
||||
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
|
||||
inline const char *text_data() const { return reinterpret_cast<const char *>(this) + sizeof(LogMessage); }
|
||||
};
|
||||
using LogMessage = TaskLogMessage;
|
||||
|
||||
// Padding marker level to indicate wrap-around point (stored in LogMessage.level field)
|
||||
// Valid log levels are 0-7, so 0xFF cannot be a real message
|
||||
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
|
||||
|
||||
TaskLogBuffer();
|
||||
~TaskLogBuffer();
|
||||
// No destructor: Logger is never destroyed
|
||||
|
||||
// NOT thread-safe - borrow a message from the buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
__thread bool non_main_task_recursion_guard; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
@@ -29,29 +29,21 @@ TaskLogBuffer::TaskLogBuffer() {
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
// Measure on a copy; the caller's va_list formats the text later
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
int ret = task_log_cap_length(vsnprintf(nullptr, 0, format, args_copy));
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
uint16_t text_length = ret;
|
||||
size_t total_size = total_size_in_32bit_words(text_length);
|
||||
auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT));
|
||||
if (msg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
fill_task_log_header(msg, level, tag, line, thread_name);
|
||||
|
||||
// Format the message text directly into the acquired memory
|
||||
// We add 1 to text_length to ensure space for null terminator during formatting
|
||||
@@ -90,11 +82,7 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &tex
|
||||
// we claimed buffer already, const_cast is safe here
|
||||
message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_));
|
||||
|
||||
text_length = message->text_length;
|
||||
// Remove trailing newlines
|
||||
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
text_length = trim_trailing_newlines(message->text_data(), message->text_length);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <zephyr/sys/mpsc_pbuf.h>
|
||||
|
||||
#include "task_log_message.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// "0x" + 2 hex digits per byte + '\0'
|
||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||
|
||||
extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern __thread bool non_main_task_recursion_guard; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
@@ -34,7 +36,6 @@ class TaskLogBuffer {
|
||||
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
|
||||
};
|
||||
TaskLogBuffer();
|
||||
~TaskLogBuffer() = default;
|
||||
|
||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||
inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); }
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Header of a variable-size task log message; the text follows it
|
||||
struct TaskLogMessage {
|
||||
const char *tag; // We store the pointer, assuming tags are static
|
||||
char thread_name[16]; // Copied, the task may be gone before the main loop reads it
|
||||
uint16_t text_length;
|
||||
uint16_t line;
|
||||
uint8_t level;
|
||||
|
||||
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(TaskLogMessage); }
|
||||
};
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
static constexpr int TASK_LOG_MAX_TEXT_SIZE = 255;
|
||||
|
||||
inline int task_log_cap_length(int ret) { return ret > TASK_LOG_MAX_TEXT_SIZE ? TASK_LOG_MAX_TEXT_SIZE : ret; }
|
||||
#endif
|
||||
|
||||
template<typename Message>
|
||||
inline void fill_task_log_header(Message *msg, uint8_t level, const char *tag, uint16_t line, const char *thread_name) {
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
|
||||
} else {
|
||||
msg->thread_name[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
inline uint16_t trim_trailing_newlines(const char *text, uint16_t length) {
|
||||
while (length > 0 && text[length - 1] == '\n')
|
||||
length--;
|
||||
return length;
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
@@ -1,303 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import CONF_CHARGE_CURRENT, CONF_ENABLED
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DURATION, CONF_ID, CONF_MODE, CONF_VOLTAGE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@tomaszduda23"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "npm1300"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_NPM1300_ID = "npm1300_id"
|
||||
CONF_VBUS_CURRENT_LIMIT = "vbus_current_limit"
|
||||
CONF_THERMISTOR_RESISTANCE = "thermistor_resistance"
|
||||
CONF_THERMISTOR_BETA = "thermistor_beta"
|
||||
CONF_CHARGER = "charger"
|
||||
CONF_TERMINATION_VOLTAGE = "termination_voltage"
|
||||
CONF_WARM_TERMINATION_VOLTAGE = "warm_termination_voltage"
|
||||
CONF_TERMINATION_CURRENT = "termination_current"
|
||||
CONF_TRICKLE_VOLTAGE = "trickle_voltage"
|
||||
CONF_DISCHARGE_CURRENT_LIMIT = "discharge_current_limit"
|
||||
CONF_RECHARGE = "recharge"
|
||||
CONF_BUCK1 = "buck1"
|
||||
CONF_BUCK2 = "buck2"
|
||||
CONF_LDO1 = "ldo1"
|
||||
CONF_LDO2 = "ldo2"
|
||||
|
||||
npm1300_ns = cg.esphome_ns.namespace("npm1300")
|
||||
NPM1300Component = npm1300_ns.class_(
|
||||
"NPM1300Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
NPM1300Output = npm1300_ns.enum("NPM1300Output", is_class=True)
|
||||
NPM1300OutputConfig = npm1300_ns.struct("NPM1300OutputConfig")
|
||||
NPM1300ChargerConfig = npm1300_ns.struct("NPM1300ChargerConfig")
|
||||
NPM1300Config = npm1300_ns.struct("NPM1300Config")
|
||||
|
||||
NPM1300_CHILD_SCHEMA = cv.Schema(
|
||||
{cv.GenerateID(CONF_NPM1300_ID): cv.use_id(NPM1300Component)}
|
||||
)
|
||||
|
||||
# Status registers read on each update: VBUSINSTATUS, BCHGCHARGESTATUS and BCHGERRREASON
|
||||
STATUS_PUBLISHER = cg.HubFunction(
|
||||
"status",
|
||||
cg.void,
|
||||
[
|
||||
(cg.uint8, "vbus_status"),
|
||||
(cg.uint8, "charge_status"),
|
||||
(cg.uint8, "error_reason"),
|
||||
],
|
||||
define="USE_NPM1300_STATUS_PUBLISHER",
|
||||
)
|
||||
VBUS_STATUS = STATUS_PUBLISHER.param("vbus_status")
|
||||
CHARGE_STATUS = STATUS_PUBLISHER.param("charge_status")
|
||||
ERROR_REASON = STATUS_PUBLISHER.param("error_reason")
|
||||
# The ADC results and the amperes per code of the battery current
|
||||
DATA_PUBLISHER = cg.HubFunction(
|
||||
"data",
|
||||
cg.void,
|
||||
[(cg.uint8.operator("const").operator("ptr"), "adc"), (cg.float_, "ibat_scale")],
|
||||
define="USE_NPM1300_DATA_PUBLISHER",
|
||||
)
|
||||
ADC = DATA_PUBLISHER.param("adc")
|
||||
IBAT_SCALE = DATA_PUBLISHER.param("ibat_scale")
|
||||
# The dump lines of the children decoded by the two functions above
|
||||
DUMP = cg.HubFunction("dump", cg.void, [(cg.const_char_ptr, "tag")])
|
||||
DUMP_TAG = DUMP.param("tag")
|
||||
|
||||
# Config key of each regulator and its NPM1300Output enumerator
|
||||
OUTPUTS = NPM1300Output.options(
|
||||
CONF_BUCK1, CONF_BUCK2, CONF_LDO1, CONF_LDO2, prefix="NPM1300_OUTPUT_"
|
||||
)
|
||||
|
||||
# Written for a field the YAML leaves out
|
||||
NPM1300_UNSET = 0xFF
|
||||
# ADCNTCRSEL codes, by kiloohms
|
||||
THERMISTOR_RESISTANCES = {10: 1, 47: 2, 100: 3}
|
||||
LDO_MODES = {"load_switch": 0, "ldo": 1}
|
||||
TERMINATION_CURRENTS = {"10%": 0, "20%": 1}
|
||||
# BCHGVTRICKLESEL, by millivolts
|
||||
TRICKLE_VOLTAGES = {2900: 0, 2500: 1}
|
||||
# BCHGISETDISCHARGE codes, by milliamperes
|
||||
DISCHARGE_CURRENT_LIMITS = {200: 84, 1000: 415}
|
||||
# BCHGVTERM: 3.50 V to 3.65 V, then 4.00 V to 4.45 V, both in 50 mV steps
|
||||
TERMINATION_VOLTAGES = [3500 + 50 * i for i in range(4)] + [
|
||||
4000 + 50 * i for i in range(10)
|
||||
]
|
||||
# The wake-up timer counts 16 ms ticks in a 24-bit register
|
||||
HIBERNATE_TICK_MS = 16
|
||||
HIBERNATE_MAX_TICKS = 0xFFFFFF
|
||||
|
||||
|
||||
def _millis(value: float) -> int:
|
||||
return round(value * 1000)
|
||||
|
||||
|
||||
def _one_of_units(
|
||||
validator: Callable[[object], float],
|
||||
allowed: list[int],
|
||||
unit: str,
|
||||
scale: float = 1000,
|
||||
) -> Callable[[object], float]:
|
||||
"""Validate a quantity with `validator` and accept only the listed values, given in units * `scale`."""
|
||||
|
||||
def validate(value: object) -> float:
|
||||
value = validator(value)
|
||||
if round(value * scale) not in allowed:
|
||||
options = ", ".join(f"{v / scale:g}{unit}" for v in allowed)
|
||||
raise cv.Invalid(f"Must be one of {options}")
|
||||
return value
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
def _stepped(
|
||||
validator: Callable[[object], float],
|
||||
minimum: int,
|
||||
maximum: int,
|
||||
step: int,
|
||||
unit: str,
|
||||
) -> Callable[[object], float]:
|
||||
"""Validate a quantity in a range of milli-units, in steps of `step`."""
|
||||
allowed = list(range(minimum, maximum + 1, step))
|
||||
in_range = cv.All(validator, cv.Range(min=minimum / 1000, max=maximum / 1000))
|
||||
|
||||
def validate(value: object) -> float:
|
||||
value = in_range(value)
|
||||
if _millis(value) not in allowed:
|
||||
raise cv.Invalid(f"Must be a multiple of {step}m{unit}")
|
||||
return value
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
OUTPUT_VOLTAGE = _stepped(cv.voltage, 1000, 3300, 100, "V")
|
||||
VBUS_CURRENT_LIMIT = _stepped(cv.current, 100, 1500, 100, "A")
|
||||
|
||||
BUCK_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_VOLTAGE): OUTPUT_VOLTAGE,
|
||||
cv.Optional(CONF_ENABLED): cv.boolean,
|
||||
}
|
||||
)
|
||||
LDO_SCHEMA = BUCK_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_MODE): cv.enum(LDO_MODES, lower=True),
|
||||
}
|
||||
)
|
||||
|
||||
CHARGER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CHARGE_CURRENT): _stepped(cv.current, 32, 800, 2, "A"),
|
||||
cv.Optional(CONF_TERMINATION_VOLTAGE, default="4.2V"): _one_of_units(
|
||||
cv.voltage, TERMINATION_VOLTAGES, "V"
|
||||
),
|
||||
cv.Optional(CONF_WARM_TERMINATION_VOLTAGE): _one_of_units(
|
||||
cv.voltage, TERMINATION_VOLTAGES, "V"
|
||||
),
|
||||
cv.Optional(CONF_TERMINATION_CURRENT, default="10%"): cv.enum(
|
||||
TERMINATION_CURRENTS
|
||||
),
|
||||
cv.Optional(CONF_TRICKLE_VOLTAGE, default="2.9V"): _one_of_units(
|
||||
cv.voltage, list(TRICKLE_VOLTAGES), "V"
|
||||
),
|
||||
cv.Optional(CONF_DISCHARGE_CURRENT_LIMIT, default="1000mA"): _one_of_units(
|
||||
cv.current, list(DISCHARGE_CURRENT_LIMITS), "A"
|
||||
),
|
||||
cv.Optional(CONF_RECHARGE, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(NPM1300Component),
|
||||
cv.Optional(CONF_VBUS_CURRENT_LIMIT): VBUS_CURRENT_LIMIT,
|
||||
cv.Optional(CONF_THERMISTOR_RESISTANCE): _one_of_units(
|
||||
cv.resistance, list(THERMISTOR_RESISTANCES), "kOhm", scale=0.001
|
||||
),
|
||||
cv.Optional(CONF_THERMISTOR_BETA, default=3380): cv.int_range(
|
||||
min=1, max=65535
|
||||
),
|
||||
cv.Optional(CONF_CHARGER): CHARGER_SCHEMA,
|
||||
cv.Optional(CONF_BUCK1): BUCK_SCHEMA,
|
||||
cv.Optional(CONF_BUCK2): BUCK_SCHEMA,
|
||||
cv.Optional(CONF_LDO1): LDO_SCHEMA,
|
||||
cv.Optional(CONF_LDO2): LDO_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x6B))
|
||||
)
|
||||
|
||||
|
||||
def _termination_voltage_code(volts: float) -> int:
|
||||
return TERMINATION_VOLTAGES.index(_millis(volts))
|
||||
|
||||
|
||||
def _vbus_current_limit_code(amps: float) -> int:
|
||||
"""VBUSINILIM0: 100 mA per step of the code."""
|
||||
return _millis(amps) // 100
|
||||
|
||||
|
||||
def _charger_config(config: ConfigType) -> cg.StructInitializer:
|
||||
termination = _termination_voltage_code(config[CONF_TERMINATION_VOLTAGE])
|
||||
warm = config.get(CONF_WARM_TERMINATION_VOLTAGE)
|
||||
return cg.StructInitializer(
|
||||
NPM1300ChargerConfig,
|
||||
("current", _millis(config[CONF_CHARGE_CURRENT]) // 2),
|
||||
(
|
||||
"discharge_limit",
|
||||
DISCHARGE_CURRENT_LIMITS[_millis(config[CONF_DISCHARGE_CURRENT_LIMIT])],
|
||||
),
|
||||
("termination_voltage", termination),
|
||||
(
|
||||
"warm_termination_voltage",
|
||||
termination if warm is None else _termination_voltage_code(warm),
|
||||
),
|
||||
("trickle_voltage", TRICKLE_VOLTAGES[_millis(config[CONF_TRICKLE_VOLTAGE])]),
|
||||
("termination_current", TERMINATION_CURRENTS[config[CONF_TERMINATION_CURRENT]]),
|
||||
("recharge", config[CONF_RECHARGE]),
|
||||
("enabled", config[CONF_ENABLED]),
|
||||
)
|
||||
|
||||
|
||||
def _output_config(config: ConfigType | None) -> cg.StructInitializer:
|
||||
config = config or {}
|
||||
voltage = config.get(CONF_VOLTAGE)
|
||||
mode = config.get(CONF_MODE)
|
||||
enabled = config.get(CONF_ENABLED)
|
||||
return cg.StructInitializer(
|
||||
NPM1300OutputConfig,
|
||||
(
|
||||
"voltage",
|
||||
NPM1300_UNSET if voltage is None else (_millis(voltage) - 1000) // 100,
|
||||
),
|
||||
("ldo_mode", NPM1300_UNSET if mode is None else LDO_MODES[mode]),
|
||||
("enabled", NPM1300_UNSET if enabled is None else int(enabled)),
|
||||
)
|
||||
|
||||
|
||||
def _config(config: ConfigType) -> cg.StructInitializer:
|
||||
charger = config.get(CONF_CHARGER)
|
||||
resistance = config.get(CONF_THERMISTOR_RESISTANCE)
|
||||
return cg.StructInitializer(
|
||||
NPM1300Config,
|
||||
(
|
||||
"outputs",
|
||||
cg.ArrayInitializer(*(_output_config(config.get(key)) for key in OUTPUTS)),
|
||||
),
|
||||
("charger", None if charger is None else _charger_config(charger)),
|
||||
("thermistor_beta", config[CONF_THERMISTOR_BETA]),
|
||||
(
|
||||
"thermistor",
|
||||
NPM1300_UNSET
|
||||
if resistance is None
|
||||
else THERMISTOR_RESISTANCES[round(resistance / 1000)],
|
||||
),
|
||||
("charger_configured", charger is not None),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# In flash: only setup() and dump_config() read it
|
||||
table = cg.shared_progmem_array(
|
||||
"npm1300_config", NPM1300Config, cg.ArrayInitializer(_config(config))
|
||||
)
|
||||
var = await i2c.new_i2c_component(config, table)
|
||||
if (limit := config.get(CONF_VBUS_CURRENT_LIMIT)) is not None:
|
||||
cg.add(var.set_vbus_current_limit(_vbus_current_limit_code(limit)))
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"npm1300.enter_ship_mode",
|
||||
automation.maybe_simple_id({cv.Required(CONF_ID): cv.use_id(NPM1300Component)}),
|
||||
automation.ApplyCall("enter_ship_mode()"),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"npm1300.hibernate",
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(NPM1300Component),
|
||||
cv.Required(CONF_DURATION): cv.templatable(
|
||||
cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(
|
||||
min=cv.TimePeriod(milliseconds=HIBERNATE_TICK_MS),
|
||||
max=cv.TimePeriod(
|
||||
milliseconds=HIBERNATE_TICK_MS * HIBERNATE_MAX_TICKS
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
automation.ApplyField(CONF_DURATION, "hibernate", cg.uint32),
|
||||
)
|
||||
@@ -1,106 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from esphome.automation import parent_ref
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_BATTERY_CHARGING,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_PROBLEM,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
from esphome.cpp_generator import HexIntLiteral
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
CHARGE_STATUS,
|
||||
CONF_NPM1300_ID,
|
||||
DUMP,
|
||||
DUMP_TAG,
|
||||
ERROR_REASON,
|
||||
NPM1300_CHILD_SCHEMA,
|
||||
STATUS_PUBLISHER,
|
||||
VBUS_STATUS,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["npm1300"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class _StatusBits:
|
||||
"""On when any bit of mask is set in the status register."""
|
||||
|
||||
key: str
|
||||
register: cg.MockObj
|
||||
mask: int
|
||||
name: str
|
||||
device_class: str = cv.UNDEFINED
|
||||
entity_category: str = cv.UNDEFINED
|
||||
|
||||
|
||||
_FIELDS = (
|
||||
_StatusBits(
|
||||
key="vbus_present",
|
||||
register=VBUS_STATUS,
|
||||
mask=0x01,
|
||||
name="VBUS Present",
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
),
|
||||
_StatusBits(
|
||||
key="vbus_current_limit_active",
|
||||
register=VBUS_STATUS,
|
||||
mask=0x02,
|
||||
name="VBUS Current Limit Active",
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
# Trickle, constant current or constant voltage charging
|
||||
_StatusBits(
|
||||
key="charging",
|
||||
register=CHARGE_STATUS,
|
||||
mask=0x1C,
|
||||
name="Charging",
|
||||
device_class=DEVICE_CLASS_BATTERY_CHARGING,
|
||||
),
|
||||
_StatusBits(
|
||||
key="charge_complete",
|
||||
register=CHARGE_STATUS,
|
||||
mask=0x02,
|
||||
name="Charge Complete",
|
||||
),
|
||||
_StatusBits(
|
||||
key="charger_error",
|
||||
register=ERROR_REASON,
|
||||
mask=0xFF,
|
||||
name="Charger Error",
|
||||
device_class=DEVICE_CLASS_PROBLEM,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = NPM1300_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(field.key): binary_sensor.binary_sensor_schema(
|
||||
device_class=field.device_class, entity_category=field.entity_category
|
||||
)
|
||||
for field in _FIELDS
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_NPM1300_ID])
|
||||
sensors = binary_sensor.sub_binary_sensors(config)
|
||||
for field in _FIELDS:
|
||||
if (var := await sensors(field.key, None)) is not None:
|
||||
state = (field.register & HexIntLiteral(field.mask)) != 0
|
||||
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(state))
|
||||
DUMP.add(
|
||||
hub,
|
||||
binary_sensor.binary_sensor_ns.log_binary_sensor(
|
||||
DUMP_TAG,
|
||||
" ",
|
||||
cg.RawExpression(f'LOG_STR_LITERAL("{field.name}")'),
|
||||
parent_ref(var),
|
||||
),
|
||||
)
|
||||
@@ -1,483 +0,0 @@
|
||||
#include "npm1300.h"
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/core/progmem.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::npm1300 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "npm1300");
|
||||
|
||||
// Registers: the base address of the peripheral in the high byte, the offset in the low byte
|
||||
static constexpr uint16_t REG_VBUSIN_TASK_UPDATE_ILIM = 0x0200;
|
||||
static constexpr uint16_t REG_VBUSIN_ILIM0 = 0x0201;
|
||||
static constexpr uint16_t REG_VBUSIN_ILIM_STARTUP = 0x0202;
|
||||
static constexpr uint16_t REG_VBUSIN_STATUS = 0x0207;
|
||||
static constexpr uint16_t REG_BCHG_TASK_RELEASE_ERR = 0x0300;
|
||||
static constexpr uint16_t REG_BCHG_TASK_CLEAR_CHG_ERR = 0x0301;
|
||||
static constexpr uint16_t REG_BCHG_ENABLE_SET = 0x0304;
|
||||
static constexpr uint16_t REG_BCHG_ENABLE_CLR = 0x0305;
|
||||
static constexpr uint16_t REG_BCHG_DISABLE_SET = 0x0306;
|
||||
static constexpr uint16_t REG_BCHG_DISABLE_CLR = 0x0307;
|
||||
static constexpr uint16_t REG_BCHG_ISET_MSB = 0x0308;
|
||||
static constexpr uint16_t REG_BCHG_ISET_DISCHARGE_MSB = 0x030A;
|
||||
static constexpr uint16_t REG_BCHG_VTERM = 0x030C;
|
||||
static constexpr uint16_t REG_BCHG_VTERM_R = 0x030D;
|
||||
static constexpr uint16_t REG_BCHG_VTRICKLE_SEL = 0x030E;
|
||||
static constexpr uint16_t REG_BCHG_ITERM_SEL = 0x030F;
|
||||
static constexpr uint16_t REG_BCHG_CHARGE_STATUS = 0x0334;
|
||||
static constexpr uint16_t REG_BCHG_ERR_REASON = 0x0336;
|
||||
static constexpr uint16_t REG_BUCK1_ENA_SET = 0x0400;
|
||||
static constexpr uint16_t REG_BUCK1_NORM_VOUT = 0x0408;
|
||||
static constexpr uint16_t REG_BUCK_SW_CTRL_SEL = 0x040F;
|
||||
static constexpr uint16_t REG_BUCK_STATUS = 0x0434;
|
||||
static constexpr uint16_t REG_ADC_TASK_VBAT = 0x0500;
|
||||
static constexpr uint16_t REG_ADC_TASK_VBUS = 0x0507;
|
||||
static constexpr uint16_t REG_ADC_NTC_R_SEL = 0x050A;
|
||||
static constexpr uint16_t REG_ADC_TASK_AUTO_TIM_UPDATE = 0x050C;
|
||||
static constexpr uint16_t REG_ADC_RESULTS = 0x0510;
|
||||
static constexpr uint16_t REG_ADC_IBAT_MEAS_EN = 0x0524;
|
||||
static constexpr uint16_t REG_TIMER_TARGET_STROBE = 0x0703;
|
||||
static constexpr uint16_t REG_TIMER_CONFIG = 0x0705;
|
||||
static constexpr uint16_t REG_TIMER_HI_BYTE = 0x0708;
|
||||
static constexpr uint16_t REG_LDSW1_TASK_SET = 0x0800;
|
||||
static constexpr uint16_t REG_LDSW_STATUS = 0x0804;
|
||||
static constexpr uint16_t REG_LDSW1_LDO_SEL = 0x0808;
|
||||
static constexpr uint16_t REG_LDSW1_VOUT_SEL = 0x080C;
|
||||
static constexpr uint16_t REG_SHIP_TASK_HIBERNATE = 0x0B00;
|
||||
static constexpr uint16_t REG_SHIP_TASK_SHIP_MODE = 0x0B02;
|
||||
|
||||
// VBUSINSTATUS
|
||||
static constexpr uint8_t VBUS_PRESENT = 0x01;
|
||||
// BCHGCHARGESTATUS; bit 0 (battery detected) is reserved since C00 silicon
|
||||
static constexpr uint8_t CHARGE_STATUS_COMPLETED = 0x02;
|
||||
static constexpr uint8_t CHARGE_STATUS_TRICKLE = 0x04;
|
||||
static constexpr uint8_t CHARGE_STATUS_CONSTANT_CURRENT = 0x08;
|
||||
static constexpr uint8_t CHARGE_STATUS_CONSTANT_VOLTAGE = 0x10;
|
||||
static constexpr uint8_t CHARGE_STATUS_DIE_TEMP_HIGH = 0x40;
|
||||
// BCHGDISABLESET and BCHGDISABLECLR
|
||||
static constexpr uint8_t CHARGER_DISABLE_RECHARGE = 0x01;
|
||||
static constexpr uint8_t CHARGER_DISABLE_NTC = 0x02;
|
||||
// BUCKSTATUS power good, LDSWSTATUS powered up as load switch or as LDO
|
||||
static constexpr uint8_t BUCK_POWER_GOOD[2] = {0x04, 0x40};
|
||||
static constexpr uint8_t LDSW_POWERED[2] = {0x03, 0x0C};
|
||||
// TIMERCONFIG: wake-up timer with the 16 ms prescaler
|
||||
static constexpr uint8_t TIMER_CONFIG_WAKEUP = 0x04;
|
||||
static constexpr uint32_t TIMER_TICK_MS = 16;
|
||||
static constexpr uint32_t TIMER_MAX_TICKS = 0xFFFFFF;
|
||||
|
||||
// BCHGISETDISCHARGE codes up to this one select the 200 mA discharge limit, the others 1000 mA
|
||||
static constexpr uint16_t DISCHARGE_LIMIT_200MA_CODE = 84;
|
||||
|
||||
static uint16_t discharge_limit_ma(uint16_t code) { return code <= DISCHARGE_LIMIT_200MA_CODE ? 200 : 1000; }
|
||||
|
||||
// BCHGISET and BCHGISETDISCHARGE hold a 9-bit code: the high 8 bits, then bit 0 in the next register
|
||||
static void split_9bit(uint16_t code, uint8_t *out) {
|
||||
out[0] = code >> 1;
|
||||
out[1] = code & 1;
|
||||
}
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
static uint16_t join_9bit(const uint8_t *in) { return (in[0] << 1) | (in[1] & 1); }
|
||||
|
||||
// ADCIBATMEASSTATUS
|
||||
static constexpr uint8_t IBAT_MEAS_INVALID = 0x10;
|
||||
static constexpr uint8_t IBAT_MODE_SHIFT = 2;
|
||||
static constexpr uint8_t IBAT_MODE_DISCHARGE = 0x01;
|
||||
static constexpr uint8_t IBAT_MODE_CHARGE = 0x03;
|
||||
// Full scale of the battery current measurement per milliampere of the charge and discharge limits
|
||||
static constexpr float CHARGE_FULL_SCALE_FACTOR = 1.25f;
|
||||
static constexpr float DISCHARGE_FULL_SCALE_FACTOR = 1.12f;
|
||||
static constexpr float ADC_MAX_CODE = 1023.0f;
|
||||
// Five conversions of 250 us each; the loop runs the timeout later than this anyway
|
||||
static constexpr uint32_t CONVERSION_TIME_MS = 2;
|
||||
|
||||
static constexpr uint32_t READ_TIMEOUT_ID = 0;
|
||||
|
||||
float ntc_temperature(uint16_t code, float inv_beta) {
|
||||
if (code == 0)
|
||||
return NAN;
|
||||
// The NTC divider gives code = 1024 * R / (R + R_nominal)
|
||||
return 1.0f / (1.0f / 298.15f - logf(1024.0f / code - 1.0f) * inv_beta) - 273.15f;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
PROGMEM_STRING_TABLE(ChargerStatusStrings, "Error", "Paused, Die Too Hot", "Charge Complete", "Trickle Charge",
|
||||
"Constant Current", "Constant Voltage", "Discharging", "Not Charging");
|
||||
// BCHGCHARGESTATUS bits in order of priority; bit i is reported as string i + 1
|
||||
static const uint8_t CHARGER_STATUS_BITS[] PROGMEM = {CHARGE_STATUS_DIE_TEMP_HIGH, CHARGE_STATUS_COMPLETED,
|
||||
CHARGE_STATUS_TRICKLE, CHARGE_STATUS_CONSTANT_CURRENT,
|
||||
CHARGE_STATUS_CONSTANT_VOLTAGE};
|
||||
|
||||
static uint8_t charger_status_index(uint8_t vbus_status, uint8_t charge_status, uint8_t error_reason) {
|
||||
if (error_reason != 0)
|
||||
return 0;
|
||||
for (size_t i = 0; i < sizeof(CHARGER_STATUS_BITS); i++) {
|
||||
if ((charge_status & progmem_read_byte(&CHARGER_STATUS_BITS[i])) != 0)
|
||||
return i + 1;
|
||||
}
|
||||
return (vbus_status & VBUS_PRESENT) == 0 ? 6 : 7;
|
||||
}
|
||||
|
||||
void publish_charger_status(text_sensor::TextSensor *sensor, uint8_t &last, uint8_t vbus_status, uint8_t charge_status,
|
||||
uint8_t error_reason) {
|
||||
const uint8_t index = charger_status_index(vbus_status, charge_status, error_reason);
|
||||
if (index == last)
|
||||
return;
|
||||
last = index;
|
||||
sensor->publish_state(ChargerStatusStrings::get_progmem_str(index, ChargerStatusStrings::LAST_INDEX));
|
||||
}
|
||||
#endif
|
||||
|
||||
NPM1300Config NPM1300Component::load_config_() const {
|
||||
NPM1300Config config;
|
||||
progmem_memcpy(&config, this->config_, sizeof(config));
|
||||
return config;
|
||||
}
|
||||
|
||||
void NPM1300Component::setup() {
|
||||
uint8_t status;
|
||||
if (!this->setup_check(this->read_byte_reg16(REG_BCHG_CHARGE_STATUS, &status)))
|
||||
return;
|
||||
|
||||
// Raise the VBUS current limit before anything turns on a regulator: at the power-on limit of 100 mA the
|
||||
// inrush current of a regulator can trip the over-current protection and brown out the board
|
||||
if (this->vbus_current_limit_ != NPM1300_UNSET &&
|
||||
!this->setup_check(this->write_byte_reg16(REG_VBUSIN_ILIM_STARTUP, this->vbus_current_limit_) &&
|
||||
this->apply_vbus_current_limit_()))
|
||||
return;
|
||||
|
||||
const NPM1300Config config = this->load_config_();
|
||||
if (config.thermistor != NPM1300_UNSET &&
|
||||
!this->setup_check(this->write_byte_reg16(REG_ADC_NTC_R_SEL, config.thermistor)))
|
||||
return;
|
||||
|
||||
if (config.charger_configured && !this->setup_check(this->configure_charger_(config)))
|
||||
return;
|
||||
|
||||
for (uint8_t i = 0; i < NPM1300_OUTPUT_COUNT; i++) {
|
||||
if (!this->setup_check(this->configure_output_(i, config.outputs[i])))
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
if (this->data_publisher_ != nullptr) {
|
||||
// The battery current is measured after every battery voltage measurement
|
||||
if (!this->setup_check(this->write_byte_reg16(REG_ADC_IBAT_MEAS_EN, 1)))
|
||||
return;
|
||||
if (config.charger_configured) {
|
||||
this->charge_code_ = config.charger.current;
|
||||
this->discharge_limit_high_ = discharge_limit_ma(config.charger.discharge_limit) == 1000;
|
||||
} else if (!this->setup_check(this->read_charge_currents_())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
this->update();
|
||||
}
|
||||
|
||||
bool NPM1300Component::apply_vbus_current_limit_() {
|
||||
return this->write_byte_reg16(REG_VBUSIN_ILIM0, this->vbus_current_limit_) &&
|
||||
this->write_byte_reg16(REG_VBUSIN_TASK_UPDATE_ILIM, 1);
|
||||
}
|
||||
|
||||
bool NPM1300Component::configure_charger_(const NPM1300Config &config) {
|
||||
const NPM1300ChargerConfig &charger = config.charger;
|
||||
// The charge settings are only taken while the charger is off
|
||||
if (!this->write_byte_reg16(REG_BCHG_ENABLE_CLR, 1))
|
||||
return false;
|
||||
uint8_t current[2];
|
||||
uint8_t discharge[2];
|
||||
split_9bit(charger.current, current);
|
||||
split_9bit(charger.discharge_limit, discharge);
|
||||
uint8_t disable = charger.recharge ? 0 : CHARGER_DISABLE_RECHARGE;
|
||||
// Without a thermistor the charger reports an NTC error and does not charge
|
||||
if (config.thermistor == NPM1300_UNSET)
|
||||
disable |= CHARGER_DISABLE_NTC;
|
||||
const uint8_t enable = (CHARGER_DISABLE_RECHARGE | CHARGER_DISABLE_NTC) & ~disable;
|
||||
if (this->write_register16(REG_BCHG_ISET_MSB, current, sizeof(current)) != i2c::ERROR_OK ||
|
||||
this->write_register16(REG_BCHG_ISET_DISCHARGE_MSB, discharge, sizeof(discharge)) != i2c::ERROR_OK ||
|
||||
!this->write_byte_reg16(REG_BCHG_VTERM, charger.termination_voltage) ||
|
||||
!this->write_byte_reg16(REG_BCHG_VTERM_R, charger.warm_termination_voltage) ||
|
||||
!this->write_byte_reg16(REG_BCHG_VTRICKLE_SEL, charger.trickle_voltage) ||
|
||||
!this->write_byte_reg16(REG_BCHG_ITERM_SEL, charger.termination_current) ||
|
||||
!this->write_byte_reg16(REG_BCHG_DISABLE_SET, disable) || !this->write_byte_reg16(REG_BCHG_DISABLE_CLR, enable) ||
|
||||
// Measure the battery temperature automatically while charging
|
||||
!this->write_byte_reg16(REG_ADC_TASK_AUTO_TIM_UPDATE, 1))
|
||||
return false;
|
||||
return !charger.enabled || this->set_charger_enabled(true);
|
||||
}
|
||||
|
||||
bool NPM1300Component::configure_output_(uint8_t index, const NPM1300OutputConfig &config) {
|
||||
const uint8_t channel = index & 1;
|
||||
if (index < 2) {
|
||||
// The software voltage replaces the one set by the VSET pin only once its SWCTRLSEL bit is set
|
||||
if (config.voltage != NPM1300_UNSET &&
|
||||
(!this->write_byte_reg16(REG_BUCK1_NORM_VOUT + channel * 2, config.voltage) ||
|
||||
!this->update_register16(REG_BUCK_SW_CTRL_SEL, 1 << channel, 1 << channel)))
|
||||
return false;
|
||||
} else if ((config.voltage != NPM1300_UNSET &&
|
||||
!this->write_byte_reg16(REG_LDSW1_VOUT_SEL + channel, config.voltage)) ||
|
||||
(config.ldo_mode != NPM1300_UNSET &&
|
||||
!this->write_byte_reg16(REG_LDSW1_LDO_SEL + channel, config.ldo_mode))) {
|
||||
return false;
|
||||
}
|
||||
return config.enabled == NPM1300_UNSET ||
|
||||
this->set_output_enabled(static_cast<NPM1300Output>(index), config.enabled != 0);
|
||||
}
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
bool NPM1300Component::read_charge_currents_() {
|
||||
uint8_t charge[2];
|
||||
uint8_t discharge[2];
|
||||
if (this->read_register16(REG_BCHG_ISET_MSB, charge, sizeof(charge)) != i2c::ERROR_OK ||
|
||||
this->read_register16(REG_BCHG_ISET_DISCHARGE_MSB, discharge, sizeof(discharge)) != i2c::ERROR_OK)
|
||||
return false;
|
||||
this->charge_code_ = join_9bit(charge);
|
||||
this->discharge_limit_high_ = discharge_limit_ma(join_9bit(discharge)) == 1000;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool NPM1300Component::set_charger_enabled(bool enabled) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
if (!enabled)
|
||||
return this->write_byte_reg16(REG_BCHG_ENABLE_CLR, 1);
|
||||
// A charger error stops the charger until the error is cleared and the charger released
|
||||
return this->write_byte_reg16(REG_BCHG_TASK_CLEAR_CHG_ERR, 1) &&
|
||||
this->write_byte_reg16(REG_BCHG_TASK_RELEASE_ERR, 1) && this->write_byte_reg16(REG_BCHG_ENABLE_SET, 1);
|
||||
}
|
||||
|
||||
bool NPM1300Component::set_output_enabled(NPM1300Output output, bool enabled) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
const uint8_t index = static_cast<uint8_t>(output);
|
||||
// A pair of SET and CLR task registers per output
|
||||
const uint16_t base = index < 2 ? REG_BUCK1_ENA_SET : REG_LDSW1_TASK_SET;
|
||||
const uint16_t reg = base + (index & 1) * 2 + (enabled ? 0 : 1);
|
||||
if (!this->write_byte_reg16(reg, 1)) {
|
||||
ESP_LOGW(TAG, "Turning output %u %s failed", index, enabled ? LOG_STR_LITERAL("on") : LOG_STR_LITERAL("off"));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void NPM1300Component::enter_ship_mode() {
|
||||
ESP_LOGI(TAG, "Entering ship mode");
|
||||
this->write_byte_reg16(REG_SHIP_TASK_SHIP_MODE, 1);
|
||||
}
|
||||
|
||||
void NPM1300Component::hibernate(uint32_t duration_ms) {
|
||||
// A zero period would never wake the chip
|
||||
const uint32_t ticks = std::clamp<uint32_t>(duration_ms / TIMER_TICK_MS, 1, TIMER_MAX_TICKS);
|
||||
uint8_t timer[3];
|
||||
put_be24(timer, ticks);
|
||||
ESP_LOGI(TAG, "Hibernating for %" PRIu32 " ms", ticks * TIMER_TICK_MS);
|
||||
if (!this->write_byte_reg16(REG_TIMER_CONFIG, TIMER_CONFIG_WAKEUP) ||
|
||||
this->write_register16(REG_TIMER_HI_BYTE, timer, sizeof(timer)) != i2c::ERROR_OK ||
|
||||
!this->write_byte_reg16(REG_TIMER_TARGET_STROBE, 1)) {
|
||||
ESP_LOGW(TAG, "Setting the wake-up timer failed");
|
||||
return;
|
||||
}
|
||||
// The timer needs a moment to load the new period
|
||||
delay(1);
|
||||
this->write_byte_reg16(REG_SHIP_TASK_HIBERNATE, 1);
|
||||
}
|
||||
|
||||
void NPM1300Component::update() {
|
||||
uint8_t vbus_status;
|
||||
uint8_t charge_status;
|
||||
uint8_t error_reason;
|
||||
if (!this->status_comm_check(this->read_byte_reg16(REG_VBUSIN_STATUS, &vbus_status) &&
|
||||
this->read_byte_reg16(REG_BCHG_CHARGE_STATUS, &charge_status) &&
|
||||
this->read_byte_reg16(REG_BCHG_ERR_REASON, &error_reason)))
|
||||
return;
|
||||
|
||||
// The chip goes back to its default VBUS current limit whenever VBUS is removed
|
||||
const bool vbus_present = (vbus_status & VBUS_PRESENT) != 0;
|
||||
if (vbus_present && !this->vbus_present_ && this->vbus_current_limit_ != NPM1300_UNSET &&
|
||||
!this->status_comm_check(this->apply_vbus_current_limit_()))
|
||||
return;
|
||||
this->vbus_present_ = vbus_present;
|
||||
|
||||
if (error_reason != this->last_error_reason_) {
|
||||
this->last_error_reason_ = error_reason;
|
||||
if (error_reason != 0) {
|
||||
ESP_LOGW(TAG, "Charger error, BCHGERRREASON 0x%02X", error_reason);
|
||||
}
|
||||
}
|
||||
#ifdef USE_NPM1300_STATUS_PUBLISHER
|
||||
if (this->status_publisher_ != nullptr)
|
||||
this->status_publisher_(vbus_status, charge_status, error_reason);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
if (!this->status_comm_check(this->publish_switches_()))
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
if (this->vbus_current_limit_sensor_ != nullptr) {
|
||||
uint8_t code;
|
||||
if (!this->status_comm_check(this->read_byte_reg16(REG_VBUSIN_ILIM0, &code)))
|
||||
return;
|
||||
// 100 mA per step of the code, but code 0 is 500 mA
|
||||
const uint16_t milliamps = code == 0 ? 500 : code * 100;
|
||||
this->vbus_current_limit_sensor_->publish_state(milliamps / 1000.0f);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
if (this->data_publisher_ != nullptr) {
|
||||
// Start the battery voltage (followed by the battery current), NTC, die temperature and system voltage
|
||||
// conversions, then the VBUS one
|
||||
static constexpr uint8_t START[4] = {1, 1, 1, 1};
|
||||
if (!this->status_comm_check(this->write_register16(REG_ADC_TASK_VBAT, START, sizeof(START)) == i2c::ERROR_OK &&
|
||||
this->write_byte_reg16(REG_ADC_TASK_VBUS, 1)))
|
||||
return;
|
||||
this->set_timeout(READ_TIMEOUT_ID, CONVERSION_TIME_MS, [this]() { this->read_measurements_(); });
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->status_comm_result(true);
|
||||
}
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
bool NPM1300Component::publish_switches_() {
|
||||
uint8_t value;
|
||||
bool ok = true;
|
||||
if (this->charger_switch_ != nullptr) {
|
||||
if (this->read_byte_reg16(REG_BCHG_ENABLE_SET, &value)) {
|
||||
this->charger_switch_->publish_state((value & 1) != 0);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
switch_::Switch *const bucks[2] = {this->buck1_switch_, this->buck2_switch_};
|
||||
if (bucks[0] != nullptr || bucks[1] != nullptr) {
|
||||
if (this->read_byte_reg16(REG_BUCK_STATUS, &value)) {
|
||||
for (uint8_t i = 0; i < 2; i++)
|
||||
PUBLISH_STATE(bucks[i], (value & BUCK_POWER_GOOD[i]) != 0);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
switch_::Switch *const ldos[2] = {this->ldo1_switch_, this->ldo2_switch_};
|
||||
if (ldos[0] != nullptr || ldos[1] != nullptr) {
|
||||
if (this->read_byte_reg16(REG_LDSW_STATUS, &value)) {
|
||||
for (uint8_t i = 0; i < 2; i++)
|
||||
PUBLISH_STATE(ldos[i], (value & LDSW_POWERED[i]) != 0);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
void NPM1300Component::read_measurements_() {
|
||||
// ADCIBATMEASSTATUS, then the VBAT, NTC, TEMP and VSYS results, their low bits, VBAT0 to VBAT3 (VBAT2 holds the
|
||||
// battery current and VBAT3 the VBUS voltage) and their low bits
|
||||
uint8_t adc[11];
|
||||
if (!this->status_comm_result(this->read_register16(REG_ADC_RESULTS, adc, sizeof(adc)) == i2c::ERROR_OK))
|
||||
return;
|
||||
// Amperes per code of the battery current, positive while charging and negative while discharging
|
||||
float ibat_scale = NAN;
|
||||
// Any other mode means the system runs from VBUS and the battery current is not measured
|
||||
const uint8_t mode = (adc[0] >> IBAT_MODE_SHIFT) & 0x03;
|
||||
if ((adc[0] & IBAT_MEAS_INVALID) == 0) {
|
||||
if (mode == IBAT_MODE_CHARGE) {
|
||||
ibat_scale = this->charge_code_ * (2 * CHARGE_FULL_SCALE_FACTOR) / (ADC_MAX_CODE * 1000.0f);
|
||||
} else if (mode == IBAT_MODE_DISCHARGE) {
|
||||
ibat_scale = -(this->discharge_limit_high_ ? 1000 : 200) * DISCHARGE_FULL_SCALE_FACTOR / (ADC_MAX_CODE * 1000.0f);
|
||||
}
|
||||
}
|
||||
if (this->data_publisher_ != nullptr)
|
||||
this->data_publisher_(adc, ibat_scale);
|
||||
}
|
||||
#endif
|
||||
|
||||
// BCHGVTERM: 3.50 V to 3.65 V, then 4.00 V to 4.45 V, both in 50 mV steps
|
||||
static uint16_t termination_voltage_mv(uint8_t code) { return code < 4 ? 3500 + 50 * code : 4000 + 50 * (code - 4); }
|
||||
|
||||
static const LogString *output_name(uint8_t index) {
|
||||
switch (static_cast<NPM1300Output>(index)) {
|
||||
case NPM1300Output::NPM1300_OUTPUT_BUCK1:
|
||||
return LOG_STR("BUCK1");
|
||||
case NPM1300Output::NPM1300_OUTPUT_BUCK2:
|
||||
return LOG_STR("BUCK2");
|
||||
case NPM1300Output::NPM1300_OUTPUT_LDO1:
|
||||
return LOG_STR("LDO1");
|
||||
default:
|
||||
return LOG_STR("LDO2");
|
||||
}
|
||||
}
|
||||
|
||||
void NPM1300Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "nPM1300:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->vbus_current_limit_ != NPM1300_UNSET) {
|
||||
// 100 mA per step of the code
|
||||
ESP_LOGCONFIG(TAG, " VBUS current limit: %u mA", this->vbus_current_limit_ * 100u);
|
||||
}
|
||||
const NPM1300Config config = this->load_config_();
|
||||
if (config.charger_configured) {
|
||||
const NPM1300ChargerConfig &charger = config.charger;
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Charger:\n"
|
||||
" Current: %u mA\n"
|
||||
" Discharge limit: %u mA\n"
|
||||
" Termination voltage: %u mV\n"
|
||||
" Warm termination voltage: %u mV\n"
|
||||
" Termination current: %u%%\n"
|
||||
" Trickle voltage: %u mV\n"
|
||||
" Enabled: %s\n"
|
||||
" Recharge: %s",
|
||||
charger.current * 2u, discharge_limit_ma(charger.discharge_limit),
|
||||
termination_voltage_mv(charger.termination_voltage),
|
||||
termination_voltage_mv(charger.warm_termination_voltage), charger.termination_current ? 20u : 10u,
|
||||
charger.trickle_voltage ? 2500u : 2900u, YESNO(charger.enabled), YESNO(charger.recharge));
|
||||
}
|
||||
for (uint8_t i = 0; i < NPM1300_OUTPUT_COUNT; i++) {
|
||||
const NPM1300OutputConfig &output = config.outputs[i];
|
||||
if (output.voltage == NPM1300_UNSET && output.ldo_mode == NPM1300_UNSET && output.enabled == NPM1300_UNSET)
|
||||
continue;
|
||||
ESP_LOGCONFIG(TAG, " %s:", LOG_STR_ARG(output_name(i)));
|
||||
if (output.voltage != NPM1300_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Voltage: %u mV", 1000u + output.voltage * 100u);
|
||||
}
|
||||
if (output.ldo_mode != NPM1300_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Mode: %s",
|
||||
output.ldo_mode != 0 ? LOG_STR_LITERAL("LDO") : LOG_STR_LITERAL("load switch"));
|
||||
}
|
||||
if (output.enabled != NPM1300_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Enabled at boot: %s", YESNO(output.enabled != 0));
|
||||
}
|
||||
}
|
||||
if (config.thermistor != NPM1300_UNSET) {
|
||||
ESP_LOGCONFIG(TAG, " Thermistor B constant: %u", config.thermistor_beta);
|
||||
}
|
||||
#ifdef USE_SENSOR
|
||||
LOG_SENSOR(" ", "VBUS Current Limit", this->vbus_current_limit_sensor_);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
LOG_SWITCH(" ", "Charger", this->charger_switch_);
|
||||
LOG_SWITCH(" ", "BUCK1", this->buck1_switch_);
|
||||
LOG_SWITCH(" ", "BUCK2", this->buck2_switch_);
|
||||
LOG_SWITCH(" ", "LDO1", this->ldo1_switch_);
|
||||
LOG_SWITCH(" ", "LDO2", this->ldo2_switch_);
|
||||
#endif
|
||||
#if defined(USE_NPM1300_STATUS_PUBLISHER) || defined(USE_NPM1300_DATA_PUBLISHER)
|
||||
if (this->dump_publisher_ != nullptr)
|
||||
this->dump_publisher_(TAG);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esphome::npm1300
|
||||
@@ -1,145 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Product specification: https://docs.nordicsemi.com/bundle/ps_npm1300/page/keyfeatures_html5.html
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::npm1300 {
|
||||
|
||||
/// The regulators, in the order of their enable task registers
|
||||
enum class NPM1300Output : uint8_t {
|
||||
NPM1300_OUTPUT_BUCK1 = 0,
|
||||
NPM1300_OUTPUT_BUCK2 = 1,
|
||||
NPM1300_OUTPUT_LDO1 = 2,
|
||||
NPM1300_OUTPUT_LDO2 = 3,
|
||||
};
|
||||
static constexpr uint8_t NPM1300_OUTPUT_COUNT = 4;
|
||||
|
||||
/// A configuration field the YAML leaves out, so the chip keeps its own value
|
||||
static constexpr uint8_t NPM1300_UNSET = 0xFF;
|
||||
|
||||
/// Register codes for one regulator, NPM1300_UNSET where the YAML leaves the field out
|
||||
struct NPM1300OutputConfig {
|
||||
uint8_t voltage; // VOUT: (millivolts - 1000) / 100
|
||||
uint8_t ldo_mode; // LDSWxLDOSEL, LDO1 and LDO2 only: 1 LDO, 0 load switch
|
||||
uint8_t enabled; // turned on (1) or off (0) at boot
|
||||
};
|
||||
|
||||
/// Register codes for the battery charger
|
||||
struct NPM1300ChargerConfig {
|
||||
uint16_t current; // BCHGISET: milliamperes / 2
|
||||
uint16_t discharge_limit; // BCHGISETDISCHARGE: 84 for 200 mA, 415 for 1000 mA
|
||||
uint8_t termination_voltage; // BCHGVTERM
|
||||
uint8_t warm_termination_voltage; // BCHGVTERMR
|
||||
uint8_t trickle_voltage; // BCHGVTRICKLESEL: 0 for 2.9 V, 1 for 2.5 V
|
||||
uint8_t termination_current; // BCHGITERMSEL: 0 for 10 %, 1 for 20 %
|
||||
bool recharge;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
/// The settings applied in setup(), built by code generation into a PROGMEM table
|
||||
struct NPM1300Config {
|
||||
NPM1300OutputConfig outputs[NPM1300_OUTPUT_COUNT];
|
||||
NPM1300ChargerConfig charger; // only applied when charger_configured
|
||||
uint16_t thermistor_beta;
|
||||
uint8_t thermistor; // ADCNTCRSEL code: 1 for 10 kOhm, 2 for 47 kOhm, 3 for 100 kOhm, or NPM1300_UNSET
|
||||
bool charger_configured;
|
||||
};
|
||||
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
/// The 10-bit ADC result whose high 8 bits are adc[msb] and low 2 bits are at `shift` in adc[lsbs]
|
||||
inline uint16_t adc_code(const uint8_t *adc, uint8_t msb, uint8_t lsbs, uint8_t shift) {
|
||||
return (adc[msb] << 2) | ((adc[lsbs] >> shift) & 0x03);
|
||||
}
|
||||
/// Battery temperature in degrees Celsius from the NTC code and 1 / B constant of the thermistor
|
||||
float ntc_temperature(uint16_t code, float inv_beta);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
/// Publish the charger status text, only when it differs from `last`
|
||||
void publish_charger_status(text_sensor::TextSensor *sensor, uint8_t &last, uint8_t vbus_status, uint8_t charge_status,
|
||||
uint8_t error_reason);
|
||||
#endif
|
||||
|
||||
class NPM1300Component : public PollingComponent, public i2c::I2CDevice {
|
||||
#ifdef USE_SENSOR
|
||||
SUB_SENSOR(vbus_current_limit)
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
SUB_SWITCH(charger)
|
||||
SUB_SWITCH(buck1)
|
||||
SUB_SWITCH(buck2)
|
||||
SUB_SWITCH(ldo1)
|
||||
SUB_SWITCH(ldo2)
|
||||
#endif
|
||||
|
||||
public:
|
||||
/// `config` points into PROGMEM
|
||||
explicit NPM1300Component(const NPM1300Config *config) : config_(config) {}
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
// Right after the I2C bus: the regulators may power other peripherals
|
||||
float get_setup_priority() const override { return setup_priority::BUS - 1.0f; }
|
||||
|
||||
#ifdef USE_NPM1300_STATUS_PUBLISHER
|
||||
HUB_PUBLISHER(status, uint8_t, uint8_t, uint8_t)
|
||||
#endif
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
HUB_PUBLISHER(data, const uint8_t *, float)
|
||||
#endif
|
||||
#if defined(USE_NPM1300_STATUS_PUBLISHER) || defined(USE_NPM1300_DATA_PUBLISHER)
|
||||
HUB_PUBLISHER(dump, const char *)
|
||||
#endif
|
||||
|
||||
/// VBUSINILIM0 code: 100 mA per step (code 0, the reset value, is 500 mA)
|
||||
void set_vbus_current_limit(uint8_t code) { this->vbus_current_limit_ = code; }
|
||||
|
||||
/// Turn the battery charger on or off; turning it on also clears a charger error
|
||||
bool set_charger_enabled(bool enabled);
|
||||
/// Turn a regulator on or off
|
||||
bool set_output_enabled(NPM1300Output output, bool enabled);
|
||||
/// Enter ship mode: every output is turned off until the SHPHLD button is pressed or VBUS is connected
|
||||
void enter_ship_mode();
|
||||
/// Enter hibernate mode: every output is turned off for `duration_ms`, or until the SHPHLD button is
|
||||
/// pressed or VBUS is connected
|
||||
void hibernate(uint32_t duration_ms);
|
||||
|
||||
protected:
|
||||
NPM1300Config load_config_() const;
|
||||
bool apply_vbus_current_limit_();
|
||||
bool configure_charger_(const NPM1300Config &config);
|
||||
bool configure_output_(uint8_t index, const NPM1300OutputConfig &config);
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
bool read_charge_currents_();
|
||||
void read_measurements_();
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
/// Returns false when a status read failed
|
||||
bool publish_switches_();
|
||||
#endif
|
||||
|
||||
const NPM1300Config *config_;
|
||||
#ifdef USE_NPM1300_DATA_PUBLISHER
|
||||
// BCHGISET code and discharge limit, for the full scale of the battery current measurement
|
||||
uint16_t charge_code_{0};
|
||||
bool discharge_limit_high_{false};
|
||||
#endif
|
||||
uint8_t vbus_current_limit_{NPM1300_UNSET};
|
||||
uint8_t last_error_reason_{0};
|
||||
bool vbus_present_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::npm1300
|
||||
@@ -1,151 +0,0 @@
|
||||
from esphome.automation import parent_ref
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ADC,
|
||||
CONF_NPM1300_ID,
|
||||
CONF_THERMISTOR_BETA,
|
||||
CONF_THERMISTOR_RESISTANCE,
|
||||
CONF_VBUS_CURRENT_LIMIT,
|
||||
DATA_PUBLISHER,
|
||||
DOMAIN,
|
||||
DUMP,
|
||||
DUMP_TAG,
|
||||
IBAT_SCALE,
|
||||
NPM1300_CHILD_SCHEMA,
|
||||
npm1300_ns,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["npm1300"]
|
||||
|
||||
CONF_BATTERY_CURRENT = "battery_current"
|
||||
CONF_BATTERY_TEMPERATURE = "battery_temperature"
|
||||
CONF_DIE_TEMPERATURE = "die_temperature"
|
||||
CONF_SYSTEM_VOLTAGE = "system_voltage"
|
||||
CONF_VBUS_VOLTAGE = "vbus_voltage"
|
||||
|
||||
ADC_MAX_CODE = 1023
|
||||
|
||||
|
||||
def _adc(msb: int, lsbs: int, shift: int) -> cg.MockObj:
|
||||
"""The 10-bit result in adc[msb] and bits shift..shift+1 of adc[lsbs]."""
|
||||
return npm1300_ns.adc_code(ADC, msb, lsbs, shift)
|
||||
|
||||
|
||||
# The decoded value and the dump name of each ADC sensor but the battery temperature, which needs the hub config.
|
||||
# Voltages scale by their full scale (product specification, section ADC).
|
||||
STATES = {
|
||||
CONF_BATTERY_VOLTAGE: (_adc(1, 5, 0) * (5.0 / ADC_MAX_CODE), "Battery Voltage"),
|
||||
CONF_BATTERY_CURRENT: (_adc(8, 10, 4) * IBAT_SCALE, "Battery Current"),
|
||||
# Product specification: T = 394.67 - 0.7926 * code
|
||||
CONF_DIE_TEMPERATURE: (394.67 - 0.7926 * _adc(3, 5, 4), "Die Temperature"),
|
||||
CONF_SYSTEM_VOLTAGE: (_adc(4, 5, 6) * (6.375 / ADC_MAX_CODE), "System Voltage"),
|
||||
CONF_VBUS_VOLTAGE: (_adc(9, 10, 6) * (7.5 / ADC_MAX_CODE), "VBUS Voltage"),
|
||||
}
|
||||
|
||||
|
||||
def _voltage_schema() -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
|
||||
def _temperature_schema(**extra: str) -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
**extra,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = NPM1300_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): _voltage_schema(),
|
||||
cv.Optional(CONF_BATTERY_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_TEMPERATURE): _temperature_schema(),
|
||||
cv.Optional(CONF_DIE_TEMPERATURE): _temperature_schema(
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
|
||||
),
|
||||
cv.Optional(CONF_SYSTEM_VOLTAGE): _voltage_schema(),
|
||||
cv.Optional(CONF_VBUS_VOLTAGE): _voltage_schema(),
|
||||
cv.Optional(CONF_VBUS_CURRENT_LIMIT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if CONF_BATTERY_TEMPERATURE not in config:
|
||||
return
|
||||
hub = fv.get_declaring_config(config[CONF_NPM1300_ID])
|
||||
if CONF_THERMISTOR_RESISTANCE not in hub:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_BATTERY_TEMPERATURE}' needs '{CONF_THERMISTOR_RESISTANCE}' "
|
||||
"set on the npm1300 component",
|
||||
path=[CONF_BATTERY_TEMPERATURE],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def _add_adc_sensor(
|
||||
hub: cg.MockObj, var: cg.MockObj, state: cg.MockObj, name: str
|
||||
) -> None:
|
||||
DATA_PUBLISHER.add(hub, parent_ref(var).publish_state(state))
|
||||
DUMP.add(
|
||||
hub,
|
||||
sensor.sensor_ns.log_sensor(
|
||||
DUMP_TAG,
|
||||
" ",
|
||||
cg.RawExpression(f'LOG_STR_LITERAL("{name}")'),
|
||||
parent_ref(var),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub_id = config[CONF_NPM1300_ID]
|
||||
hub = await cg.get_variable(hub_id)
|
||||
sensors = sensor.sub_sensors(config)
|
||||
for key, (state, name) in STATES.items():
|
||||
if (var := await sensors(key, None)) is not None:
|
||||
_add_adc_sensor(hub, var, state, name)
|
||||
if (var := await sensors(CONF_BATTERY_TEMPERATURE, None)) is not None:
|
||||
hub_config = next(c for c in CORE.config[DOMAIN] if c[CONF_ID] == hub_id)
|
||||
state = npm1300_ns.ntc_temperature(
|
||||
_adc(2, 5, 2), 1.0 / hub_config[CONF_THERMISTOR_BETA]
|
||||
)
|
||||
_add_adc_sensor(hub, var, state, "Battery Temperature")
|
||||
await sensors(CONF_VBUS_CURRENT_LIMIT, hub.set_vbus_current_limit_sensor)
|
||||
@@ -1,95 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
CONF_BUCK1,
|
||||
CONF_BUCK2,
|
||||
CONF_CHARGER,
|
||||
CONF_LDO1,
|
||||
CONF_LDO2,
|
||||
CONF_NPM1300_ID,
|
||||
CONF_THERMISTOR_RESISTANCE,
|
||||
NPM1300_CHILD_SCHEMA,
|
||||
OUTPUTS,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["npm1300"]
|
||||
|
||||
|
||||
def _switch_schema() -> cv.Schema:
|
||||
# The switches show the state of the chip, so nothing is restored at boot
|
||||
return switch.switch_schema(
|
||||
switch.ApplySwitch,
|
||||
default_restore_mode="DISABLED",
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = NPM1300_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_CHARGER): _switch_schema(),
|
||||
cv.Optional(CONF_BUCK1): _switch_schema(),
|
||||
cv.Optional(CONF_BUCK2): _switch_schema(),
|
||||
cv.Optional(CONF_LDO1): _switch_schema(),
|
||||
cv.Optional(CONF_LDO2): _switch_schema(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
if CONF_CHARGER not in config:
|
||||
return
|
||||
hub = fv.get_declaring_config(config[CONF_NPM1300_ID])
|
||||
# Only the charger block turns off the NTC check, without which the charger never starts
|
||||
if CONF_CHARGER not in hub and CONF_THERMISTOR_RESISTANCE not in hub:
|
||||
raise cv.Invalid(
|
||||
f"The '{CONF_CHARGER}' switch needs a '{CONF_CHARGER}' block or "
|
||||
f"'{CONF_THERMISTOR_RESISTANCE}' on the npm1300 component",
|
||||
path=[CONF_CHARGER],
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
def _output_apply(key: str) -> str:
|
||||
return f"set_output_enabled({OUTPUTS[key]}, state)"
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_NPM1300_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(
|
||||
CONF_CHARGER,
|
||||
hub.set_charger_switch,
|
||||
apply="set_charger_enabled(state)",
|
||||
publish="on_success",
|
||||
)
|
||||
await switches(
|
||||
CONF_BUCK1,
|
||||
hub.set_buck1_switch,
|
||||
apply=_output_apply(CONF_BUCK1),
|
||||
publish="on_success",
|
||||
)
|
||||
await switches(
|
||||
CONF_BUCK2,
|
||||
hub.set_buck2_switch,
|
||||
apply=_output_apply(CONF_BUCK2),
|
||||
publish="on_success",
|
||||
)
|
||||
await switches(
|
||||
CONF_LDO1,
|
||||
hub.set_ldo1_switch,
|
||||
apply=_output_apply(CONF_LDO1),
|
||||
publish="on_success",
|
||||
)
|
||||
await switches(
|
||||
CONF_LDO2,
|
||||
hub.set_ldo2_switch,
|
||||
apply=_output_apply(CONF_LDO2),
|
||||
publish="on_success",
|
||||
)
|
||||
@@ -1,63 +0,0 @@
|
||||
from esphome.automation import parent_ref
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ICON_BATTERY
|
||||
from esphome.cpp_generator import (
|
||||
AssignmentExpression,
|
||||
HexIntLiteral,
|
||||
VariableDeclarationExpression,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
CHARGE_STATUS,
|
||||
CONF_NPM1300_ID,
|
||||
DUMP,
|
||||
DUMP_TAG,
|
||||
ERROR_REASON,
|
||||
NPM1300_CHILD_SCHEMA,
|
||||
STATUS_PUBLISHER,
|
||||
VBUS_STATUS,
|
||||
npm1300_ns,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["npm1300"]
|
||||
|
||||
CONF_CHARGER_STATUS = "charger_status"
|
||||
|
||||
CONFIG_SCHEMA = NPM1300_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_CHARGER_STATUS): text_sensor.text_sensor_schema(
|
||||
icon=ICON_BATTERY,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_NPM1300_ID])
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
if (var := await text_sensors(CONF_CHARGER_STATUS, None)) is None:
|
||||
return
|
||||
last = cg.MockObj(f"esphome__{var}__last")
|
||||
# 0xFF so the first update publishes
|
||||
declaration = VariableDeclarationExpression(cg.uint8, "", last, static=True)
|
||||
STATUS_PUBLISHER.add(
|
||||
hub, AssignmentExpression(None, None, declaration, HexIntLiteral(0xFF))
|
||||
)
|
||||
STATUS_PUBLISHER.add(
|
||||
hub,
|
||||
npm1300_ns.publish_charger_status(
|
||||
parent_ref(var), last, VBUS_STATUS, CHARGE_STATUS, ERROR_REASON
|
||||
),
|
||||
)
|
||||
DUMP.add(
|
||||
hub,
|
||||
text_sensor.text_sensor_ns.log_text_sensor(
|
||||
DUMP_TAG,
|
||||
" ",
|
||||
cg.RawExpression('LOG_STR_LITERAL("Charger Status")'),
|
||||
parent_ref(var),
|
||||
),
|
||||
)
|
||||
@@ -48,7 +48,7 @@ CONF_WHITENING_ENABLE = "whitening_enable"
|
||||
CONF_WHITENING_INITIAL = "whitening_initial"
|
||||
|
||||
sx126x_ns = cg.esphome_ns.namespace("sx126x")
|
||||
SX126x = sx126x_ns.class_("SX126x", cg.Component)
|
||||
SX126x = sx126x_ns.class_("SX126x", cg.Component, spi.SPIDevice)
|
||||
SX126xListener = sx126x_ns.class_("SX126xListener")
|
||||
SX126xBw = sx126x_ns.enum("SX126xBw")
|
||||
SX126xPacketType = sx126x_ns.enum("SX126xPacketType")
|
||||
@@ -217,9 +217,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Required(CONF_RF_SWITCH): cv.boolean,
|
||||
cv.Optional(CONF_SHAPING, default="NONE"): cv.enum(SHAPING),
|
||||
cv.Optional(CONF_SPREADING_FACTOR, default=7): cv.int_range(min=6, max=12),
|
||||
cv.Optional(CONF_SYNC_VALUE, default=[]): cv.All(
|
||||
cv.ensure_list(cv.hex_uint8_t), cv.Length(max=8)
|
||||
),
|
||||
cv.Optional(CONF_SYNC_VALUE, default=[]): cv.ensure_list(cv.hex_uint8_t),
|
||||
cv.Optional(CONF_TCXO_VOLTAGE, default="NONE"): cv.enum(TCXO_VOLTAGE),
|
||||
cv.Optional(CONF_TCXO_DELAY, default="5ms"): cv.All(
|
||||
cv.positive_time_period_microseconds,
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
#pragma once
|
||||
#include "sx126x_reg.h"
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
enum SX126xBw : uint8_t {
|
||||
// FSK
|
||||
SX126X_BW_4800,
|
||||
SX126X_BW_5800,
|
||||
SX126X_BW_7300,
|
||||
SX126X_BW_9700,
|
||||
SX126X_BW_11700,
|
||||
SX126X_BW_14600,
|
||||
SX126X_BW_19500,
|
||||
SX126X_BW_23400,
|
||||
SX126X_BW_29300,
|
||||
SX126X_BW_39000,
|
||||
SX126X_BW_46900,
|
||||
SX126X_BW_58600,
|
||||
SX126X_BW_78200,
|
||||
SX126X_BW_93800,
|
||||
SX126X_BW_117300,
|
||||
SX126X_BW_156200,
|
||||
SX126X_BW_187200,
|
||||
SX126X_BW_234300,
|
||||
SX126X_BW_312000,
|
||||
SX126X_BW_373600,
|
||||
SX126X_BW_467000,
|
||||
// LORA
|
||||
SX126X_BW_7810,
|
||||
SX126X_BW_10420,
|
||||
SX126X_BW_15630,
|
||||
SX126X_BW_20830,
|
||||
SX126X_BW_31250,
|
||||
SX126X_BW_41670,
|
||||
SX126X_BW_62500,
|
||||
SX126X_BW_125000,
|
||||
SX126X_BW_250000,
|
||||
SX126X_BW_500000,
|
||||
};
|
||||
|
||||
// Requested settings are separate from the last successfully applied profile.
|
||||
// Bounded sync storage makes profile snapshots allocation-free.
|
||||
struct RadioProfile {
|
||||
uint32_t bitrate{0};
|
||||
uint32_t deviation{0};
|
||||
uint32_t frequency{0};
|
||||
uint32_t tcxo_delay{0};
|
||||
uint16_t crc_polynomial{0};
|
||||
uint16_t crc_initial{0};
|
||||
uint16_t whitening_initial{0};
|
||||
uint16_t preamble_detect{0};
|
||||
uint16_t preamble_size{0};
|
||||
SX126xBw bandwidth{SX126X_BW_125000};
|
||||
uint8_t crc_size{0};
|
||||
uint8_t tcxo_voltage{0};
|
||||
uint8_t coding_rate{0};
|
||||
uint8_t modulation{PACKET_TYPE_LORA};
|
||||
uint8_t pa_ramp{0};
|
||||
uint8_t shaping{0};
|
||||
uint8_t spreading_factor{0};
|
||||
int8_t pa_power{0};
|
||||
uint8_t payload_length{0};
|
||||
bool sx1261{false};
|
||||
bool crc_enable{false};
|
||||
bool crc_inverted{false};
|
||||
bool whitening_enable{false};
|
||||
bool rx_start{false};
|
||||
bool rf_switch{false};
|
||||
std::array<uint8_t, 8> sync_value{};
|
||||
uint8_t sync_size{0};
|
||||
bool sync_valid{true};
|
||||
void set_sync_value(std::span<const uint8_t> bytes) {
|
||||
this->sync_valid = bytes.size() <= this->sync_value.size();
|
||||
if (!this->sync_valid) {
|
||||
return;
|
||||
}
|
||||
std::copy(bytes.begin(), bytes.end(), this->sync_value.begin());
|
||||
this->sync_size = bytes.size();
|
||||
}
|
||||
size_t max_packet_size() const { return this->payload_length > 0 ? this->payload_length : 255; }
|
||||
};
|
||||
|
||||
// The parts of the last successfully applied profile that runtime operations read.
|
||||
// SetPacketParams is encoded once; sends only change its payload length byte.
|
||||
struct AppliedProfile {
|
||||
uint32_t frequency{0};
|
||||
std::array<uint8_t, 9> packet_params{};
|
||||
uint8_t packet_params_size{0};
|
||||
uint8_t payload_length{0};
|
||||
uint8_t modulation{PACKET_TYPE_LORA};
|
||||
bool rx_start{false};
|
||||
size_t max_packet_size() const { return this->payload_length > 0 ? this->payload_length : 255; }
|
||||
};
|
||||
} // namespace esphome::sx126x
|
||||
@@ -2,217 +2,534 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sx126x");
|
||||
static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000;
|
||||
static constexpr uint32_t MAX_TRANSMIT_TIMEOUT_MS = 262143; // 24-bit SetTx timeout in 1/64 ms steps
|
||||
static constexpr uint32_t RECONFIGURE_INTERVAL_ID = 0;
|
||||
static constexpr uint32_t RECONFIGURE_INTERVAL_MS = 1000;
|
||||
static constexpr uint16_t RAMP[8] PROGMEM = {10, 20, 40, 80, 200, 800, 1700, 3400};
|
||||
// Aligned 32 bit loads from flash are safe on ESP8266, so this table is read directly
|
||||
static constexpr uint32_t BW_HZ[31] PROGMEM = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400,
|
||||
29300, 39000, 46900, 58600, 78200, 93800, 117300, 156200,
|
||||
187200, 234300, 312000, 373600, 467000, 7810, 10420, 15630,
|
||||
20830, 31250, 41670, 62500, 125000, 250000, 500000};
|
||||
static constexpr uint8_t BW_LORA[10] PROGMEM = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830,
|
||||
LORA_BW_31250, LORA_BW_41670, LORA_BW_62500, LORA_BW_125000,
|
||||
LORA_BW_250000, LORA_BW_500000};
|
||||
static constexpr uint8_t BW_FSK[21] PROGMEM = {
|
||||
FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500,
|
||||
FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800,
|
||||
FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
|
||||
|
||||
// Indexed by SX126xError.
|
||||
PROGMEM_STRING_TABLE(SX126xErrorStrings, "none", "timeout", "invalid payload", "radio not ready", "bus timeout",
|
||||
"radio busy", "cancelled");
|
||||
static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
|
||||
static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
|
||||
static constexpr uint32_t SWITCHING_DELAY_US = 1;
|
||||
static constexpr uint32_t TRANSMIT_TIMEOUT_MS = 4000;
|
||||
static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
|
||||
|
||||
// OCP (Over Current Protection) values
|
||||
static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current
|
||||
static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current
|
||||
|
||||
// LoRa low data rate optimization threshold
|
||||
static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms
|
||||
|
||||
uint8_t SX126x::read_fifo_(uint8_t offset, std::vector<uint8_t> &packet) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->transfer_byte(RADIO_READ_BUFFER);
|
||||
this->transfer_byte(offset);
|
||||
uint8_t status = this->transfer_byte(0x00);
|
||||
for (uint8_t &byte : packet) {
|
||||
byte = this->transfer_byte(0x00);
|
||||
}
|
||||
this->disable();
|
||||
return status;
|
||||
}
|
||||
|
||||
void SX126x::write_fifo_(uint8_t offset, const uint8_t *data, size_t len) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->transfer_byte(RADIO_WRITE_BUFFER);
|
||||
this->transfer_byte(offset);
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->transfer_byte(data[i]);
|
||||
}
|
||||
this->disable();
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
|
||||
uint8_t SX126x::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->transfer_byte(opcode);
|
||||
uint8_t status = this->transfer_byte(0x00);
|
||||
for (int32_t i = 0; i < size; i++) {
|
||||
data[i] = this->transfer_byte(0x00);
|
||||
}
|
||||
this->disable();
|
||||
return status;
|
||||
}
|
||||
|
||||
void SX126x::write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->transfer_byte(opcode);
|
||||
for (int32_t i = 0; i < size; i++) {
|
||||
this->transfer_byte(data[i]);
|
||||
}
|
||||
this->disable();
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
|
||||
void SX126x::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->write_byte(RADIO_READ_REGISTER);
|
||||
this->write_byte((reg >> 8) & 0xFF);
|
||||
this->write_byte((reg >> 0) & 0xFF);
|
||||
this->write_byte(0x00);
|
||||
for (int32_t i = 0; i < size; i++) {
|
||||
data[i] = this->transfer_byte(0x00);
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void SX126x::write_register_(uint16_t reg, uint8_t *data, uint8_t size) {
|
||||
this->enable();
|
||||
this->wait_busy_();
|
||||
this->write_byte(RADIO_WRITE_REGISTER);
|
||||
this->write_byte((reg >> 8) & 0xFF);
|
||||
this->write_byte((reg >> 0) & 0xFF);
|
||||
for (int32_t i = 0; i < size; i++) {
|
||||
this->transfer_byte(data[i]);
|
||||
}
|
||||
this->disable();
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
|
||||
void IRAM_ATTR SX126x::gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void SX126x::setup() {
|
||||
// Sized once so later packet reads never reallocate; read_packet() sets the length.
|
||||
this->packet_.resize(SX126X_MAX_PACKET_SIZE);
|
||||
// setup pins
|
||||
GPIOPin *dio1 = this->radio_.get_dio1_pin();
|
||||
this->radio_.bus().get_busy_pin()->setup();
|
||||
this->radio_.get_rst_pin()->setup();
|
||||
dio1->setup();
|
||||
if (dio1->is_internal()) {
|
||||
static_cast<InternalGPIOPin *>(dio1)->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
this->busy_pin_->setup();
|
||||
this->rst_pin_->setup();
|
||||
this->dio1_pin_->setup();
|
||||
if (this->dio1_pin_->is_internal()) {
|
||||
static_cast<InternalGPIOPin *>(this->dio1_pin_)
|
||||
->attach_interrupt(&SX126x::gpio_intr, this, gpio::INTERRUPT_RISING_EDGE);
|
||||
}
|
||||
|
||||
// start spi
|
||||
this->radio_.bus().setup();
|
||||
this->spi_setup();
|
||||
|
||||
// configure rf; a failure is reported through mark_failed() only, with its own reason
|
||||
const RadioEffects effects = this->radio_.configure(this->requested_profile_, this->version_);
|
||||
if (effects.warning != nullptr) {
|
||||
this->mark_failed(effects.warning);
|
||||
return;
|
||||
}
|
||||
this->apply_(effects);
|
||||
}
|
||||
|
||||
void SX126x::apply_(const RadioEffects &effects) {
|
||||
// User code may only run once the radio has committed. A rejection during another
|
||||
// operation's command sequence carries no status or callbacks.
|
||||
ESPHOME_DEBUG_ASSERT(!this->radio_.busy() || (!effects.report && effects.listener == nullptr && !effects.received));
|
||||
if (effects.sleep_loop) {
|
||||
this->disable_loop();
|
||||
} else if (this->radio_.state() != RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
|
||||
this->enable_loop();
|
||||
}
|
||||
this->watch_configuration_();
|
||||
// The packet buffer is borrowed from the status report until listeners return;
|
||||
// loop() will not refill it meanwhile.
|
||||
if (effects.received) {
|
||||
this->rx_dispatching_ = true;
|
||||
}
|
||||
if (effects.report) {
|
||||
this->status_comm_result(effects.warning == nullptr, effects.warning);
|
||||
}
|
||||
if (effects.listener != nullptr) {
|
||||
effects.listener->on_transmit_complete(effects.result);
|
||||
}
|
||||
if (effects.received) {
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->on_packet(this->packet_, effects.rssi, effects.snr);
|
||||
}
|
||||
this->packet_trigger_.trigger(this->packet_, effects.rssi, effects.snr);
|
||||
this->rx_dispatching_ = false;
|
||||
}
|
||||
// configure rf
|
||||
this->configure();
|
||||
}
|
||||
|
||||
void SX126x::configure() {
|
||||
// Before setup() the bus is not set up, and after a failed setup the radio stays unused
|
||||
if (!this->is_ready()) {
|
||||
uint8_t buf[8];
|
||||
|
||||
// toggle chip reset
|
||||
this->rst_pin_->digital_write(true);
|
||||
delayMicroseconds(RESET_DELAY_HIGH_US);
|
||||
this->rst_pin_->digital_write(false);
|
||||
delayMicroseconds(RESET_DELAY_LOW_US);
|
||||
this->rst_pin_->digital_write(true);
|
||||
delayMicroseconds(RESET_DELAY_HIGH_US);
|
||||
|
||||
// wakeup
|
||||
this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
|
||||
|
||||
// config tcxo
|
||||
if (this->tcxo_voltage_ != TCXO_CTRL_NONE) {
|
||||
uint32_t delay = this->tcxo_delay_ >> 6;
|
||||
buf[0] = this->tcxo_voltage_;
|
||||
buf[1] = (delay >> 16) & 0xFF;
|
||||
buf[2] = (delay >> 8) & 0xFF;
|
||||
buf[3] = (delay >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
|
||||
buf[0] = 0x7F;
|
||||
this->write_opcode_(RADIO_CALIBRATE, buf, 1);
|
||||
}
|
||||
|
||||
// clear errors
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
|
||||
|
||||
// rf switch
|
||||
if (this->rf_switch_) {
|
||||
buf[0] = 0x01;
|
||||
this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
|
||||
}
|
||||
|
||||
// check silicon version to make sure hw is ok
|
||||
this->read_register_(REG_VERSION_STRING, (uint8_t *) this->version_, sizeof(this->version_));
|
||||
this->version_[sizeof(this->version_) - 1] = '\0';
|
||||
if (strncmp(this->version_, "SX126", 5) != 0 && strncmp(this->version_, "LLCC68", 6) != 0) {
|
||||
this->mark_failed(LOG_STR("Unsupported chip version"));
|
||||
return;
|
||||
}
|
||||
if (!this->radio_.can_configure()) {
|
||||
ESP_LOGW(TAG, "Radio operation in progress, configuration ignored");
|
||||
return;
|
||||
}
|
||||
this->cold_sleep_ = false;
|
||||
this->apply_(this->radio_.configure(this->requested_profile_, this->version_));
|
||||
}
|
||||
|
||||
// A failed command leaves the radio needing configuration, which nothing else would do, so a node
|
||||
// that only uses YAML would stay deaf. Cold sleep is asked for, so it waits for a mode command.
|
||||
void SX126x::watch_configuration_() {
|
||||
const bool needed = this->radio_.state() == RadioState::RADIO_STATE_NEEDS_CONFIGURATION && !this->cold_sleep_;
|
||||
if (needed == this->reconfigure_pending_) {
|
||||
return;
|
||||
}
|
||||
this->reconfigure_pending_ = needed;
|
||||
if (needed) {
|
||||
this->set_interval(RECONFIGURE_INTERVAL_ID, RECONFIGURE_INTERVAL_MS, [this]() { this->configure(); });
|
||||
} else {
|
||||
this->cancel_interval(RECONFIGURE_INTERVAL_ID);
|
||||
}
|
||||
}
|
||||
// setup packet type
|
||||
buf[0] = this->modulation_;
|
||||
this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
|
||||
|
||||
void SX126x::set_mode_rx() { this->change_mode_(ModeCommand::MODE_COMMAND_RX); }
|
||||
void SX126x::set_mode_tx() { this->change_mode_(ModeCommand::MODE_COMMAND_TX); }
|
||||
void SX126x::set_mode_standby(SX126xStandbyMode mode) { this->change_mode_(ModeCommand::MODE_COMMAND_STANDBY, mode); }
|
||||
void SX126x::set_mode_sleep(bool cold) {
|
||||
this->change_mode_(cold ? ModeCommand::MODE_COMMAND_COLD_SLEEP : ModeCommand::MODE_COMMAND_SLEEP);
|
||||
}
|
||||
void SX126x::run_image_cal() { this->change_mode_(ModeCommand::MODE_COMMAND_IMAGE_CAL); }
|
||||
// calibrate image
|
||||
this->run_image_cal();
|
||||
|
||||
void SX126x::change_mode_(ModeCommand command, uint8_t arg) {
|
||||
const bool sleep = command == ModeCommand::MODE_COMMAND_SLEEP || command == ModeCommand::MODE_COMMAND_COLD_SLEEP;
|
||||
if (this->cold_sleep_) {
|
||||
if (sleep) {
|
||||
return;
|
||||
// set frequency
|
||||
uint64_t freq = ((uint64_t) this->frequency_ << 25) / XTAL_FREQ;
|
||||
buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
|
||||
buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
|
||||
buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
|
||||
buf[3] = (uint8_t) (freq & 0xFF);
|
||||
this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
|
||||
|
||||
// configure pa
|
||||
int8_t pa_power = this->pa_power_;
|
||||
if (this->sx1261_) {
|
||||
// the following values were taken from section 13.1.14.1 table 13-21
|
||||
// in rev 2.1 of the datasheet
|
||||
if (pa_power == 15) {
|
||||
uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
} else {
|
||||
uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
}
|
||||
// Cold sleep lost the configuration, so waking needs it applied again first
|
||||
this->configure();
|
||||
pa_power = std::max(pa_power, (int8_t) -3);
|
||||
pa_power = std::min(pa_power, (int8_t) 14);
|
||||
buf[0] = OCP_80MA;
|
||||
this->write_register_(REG_OCP, buf, 1);
|
||||
} else {
|
||||
// the following values were taken from section 13.1.14.1 table 13-21
|
||||
// in rev 2.1 of the datasheet
|
||||
uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
pa_power = std::max(pa_power, (int8_t) -3);
|
||||
pa_power = std::min(pa_power, (int8_t) 22);
|
||||
buf[0] = OCP_140MA;
|
||||
this->write_register_(REG_OCP, buf, 1);
|
||||
}
|
||||
if (!this->radio_.can_change_mode()) {
|
||||
ESP_LOGW(TAG, "Radio unavailable, mode change ignored");
|
||||
return;
|
||||
buf[0] = pa_power;
|
||||
buf[1] = this->pa_ramp_;
|
||||
this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
|
||||
|
||||
// configure modem
|
||||
if (this->modulation_ == PACKET_TYPE_LORA) {
|
||||
// set modulation params
|
||||
float duration = 1000.0f * (1UL << this->spreading_factor_) / BW_HZ[this->bandwidth_];
|
||||
buf[0] = this->spreading_factor_;
|
||||
buf[1] = progmem_read_byte(&BW_LORA[this->bandwidth_ - SX126X_BW_7810]);
|
||||
buf[2] = this->coding_rate_;
|
||||
buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
|
||||
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
|
||||
|
||||
// set packet params and sync word
|
||||
this->set_packet_params_(this->get_max_packet_size());
|
||||
if (this->sync_value_.size() == 2) {
|
||||
this->write_register_(REG_LORA_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
|
||||
}
|
||||
} else {
|
||||
// set modulation params
|
||||
uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / this->bitrate_;
|
||||
uint32_t fdev = ((uint64_t) this->deviation_ << 25) / XTAL_FREQ;
|
||||
buf[0] = (bitrate >> 16) & 0xFF;
|
||||
buf[1] = (bitrate >> 8) & 0xFF;
|
||||
buf[2] = (bitrate >> 0) & 0xFF;
|
||||
buf[3] = this->shaping_;
|
||||
buf[4] = progmem_read_byte(&BW_FSK[this->bandwidth_ - SX126X_BW_4800]);
|
||||
buf[5] = (fdev >> 16) & 0xFF;
|
||||
buf[6] = (fdev >> 8) & 0xFF;
|
||||
buf[7] = (fdev >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
|
||||
|
||||
// set crc params
|
||||
if (this->crc_enable_) {
|
||||
buf[0] = this->crc_initial_ >> 8;
|
||||
buf[1] = this->crc_initial_ & 0xFF;
|
||||
this->write_register_(REG_CRC_INITIAL, buf, 2);
|
||||
buf[0] = this->crc_polynomial_ >> 8;
|
||||
buf[1] = this->crc_polynomial_ & 0xFF;
|
||||
this->write_register_(REG_CRC_POLYNOMIAL, buf, 2);
|
||||
}
|
||||
|
||||
// set whitening params
|
||||
if (this->whitening_enable_) {
|
||||
// according to the datasheet, section 12 table 12-1 "The user should not
|
||||
// change the value of the 7 MSB of this register"
|
||||
this->read_register_(REG_WHITENING_INITIAL, buf, 1);
|
||||
buf[0] = (buf[0] & 0xFE) | ((this->whitening_initial_ >> 8) & 0x01);
|
||||
buf[1] = this->whitening_initial_ & 0xFF;
|
||||
this->write_register_(REG_WHITENING_INITIAL, buf, 2);
|
||||
}
|
||||
|
||||
// set packet params and sync word
|
||||
this->set_packet_params_(this->get_max_packet_size());
|
||||
if (!this->sync_value_.empty()) {
|
||||
this->write_register_(REG_GFSK_SYNCWORD, this->sync_value_.data(), this->sync_value_.size());
|
||||
}
|
||||
}
|
||||
|
||||
// switch to rx or sleep
|
||||
if (this->rx_start_) {
|
||||
this->set_mode_rx();
|
||||
} else {
|
||||
this->set_mode_sleep();
|
||||
}
|
||||
this->cold_sleep_ = command == ModeCommand::MODE_COMMAND_COLD_SLEEP;
|
||||
this->apply_(this->radio_.change_mode(command, arg));
|
||||
}
|
||||
|
||||
size_t SX126x::get_max_packet_size() { return this->radio_.max_packet_size(); }
|
||||
size_t SX126x::get_max_packet_size() {
|
||||
if (this->payload_length_ > 0) {
|
||||
return this->payload_length_;
|
||||
}
|
||||
return SX126X_MAX_PACKET_SIZE;
|
||||
}
|
||||
|
||||
void SX126x::set_packet_params_(uint8_t payload_length) {
|
||||
uint8_t buf[9];
|
||||
if (this->modulation_ == PACKET_TYPE_LORA) {
|
||||
buf[0] = (this->preamble_size_ >> 8) & 0xFF;
|
||||
buf[1] = (this->preamble_size_ >> 0) & 0xFF;
|
||||
buf[2] = (this->payload_length_ > 0) ? 0x01 : 0x00;
|
||||
buf[3] = payload_length;
|
||||
buf[4] = (this->crc_enable_) ? 0x01 : 0x00;
|
||||
buf[5] = 0x00;
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 6);
|
||||
} else {
|
||||
uint16_t preamble_size = this->preamble_size_ * 8;
|
||||
buf[0] = (preamble_size >> 8) & 0xFF;
|
||||
buf[1] = (preamble_size >> 0) & 0xFF;
|
||||
buf[2] = (this->preamble_detect_ > 0) ? ((this->preamble_detect_ - 1) | 0x04) : 0x00;
|
||||
buf[3] = this->sync_value_.size() * 8;
|
||||
buf[4] = 0x00;
|
||||
buf[5] = (this->payload_length_ > 0) ? 0x00 : 0x01;
|
||||
buf[6] = payload_length;
|
||||
if (this->crc_enable_) {
|
||||
buf[7] = (this->crc_inverted_ ? 0x04 : 0x00) + (this->crc_size_ & 0x02);
|
||||
} else {
|
||||
buf[7] = 0x01;
|
||||
}
|
||||
buf[8] = (this->whitening_enable_) ? 0x01 : 0x00;
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, buf, 9);
|
||||
}
|
||||
}
|
||||
|
||||
SX126xError SX126x::transmit_packet(const uint8_t *data, size_t len) {
|
||||
SX126xError result = this->start_transmit_(nullptr, data, len, TRANSMIT_TIMEOUT_MS);
|
||||
if (result == SX126xError::SX126X_ERROR_NONE) {
|
||||
// Wait for this send's own completion. A send without a listener cannot be cancelled,
|
||||
// and no user code runs between these polls, so the send cannot be replaced.
|
||||
result = SX126xError::SX126X_ERROR_NOT_READY;
|
||||
while (this->radio_.is_transmitting()) {
|
||||
const RadioEffects effects = this->radio_.poll_transmit(millis());
|
||||
if (effects.finished) {
|
||||
result = effects.result;
|
||||
this->apply_(effects);
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
}
|
||||
}
|
||||
if (result != SX126xError::SX126X_ERROR_NONE) {
|
||||
ESP_LOGW(
|
||||
TAG, "Packet send failed: %s",
|
||||
LOG_STR_ARG(SX126xErrorStrings::get_log_str(static_cast<uint8_t>(result), SX126xErrorStrings::LAST_INDEX)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
SX126xError SX126x::start_transmit_async(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
|
||||
uint32_t timeout_ms) {
|
||||
if (listener == nullptr || timeout_ms == 0 || timeout_ms > MAX_TRANSMIT_TIMEOUT_MS) {
|
||||
return SX126xError::SX126X_ERROR_INVALID_PARAMS;
|
||||
}
|
||||
return this->start_transmit_(listener, data, len, timeout_ms);
|
||||
}
|
||||
|
||||
SX126xError SX126x::start_transmit_(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
|
||||
uint32_t timeout_ms) {
|
||||
// Callers such as a template number's on_value can run before setup() or after a failed setup
|
||||
if (!this->is_ready()) {
|
||||
ESP_LOGW(TAG, "Radio not ready, packet dropped");
|
||||
return SX126xError::SX126X_ERROR_NOT_READY;
|
||||
}
|
||||
const RadioEffects effects = this->radio_.start_transmit(listener, data, len, timeout_ms);
|
||||
this->apply_(effects);
|
||||
return effects.result;
|
||||
if (this->payload_length_ > 0 && this->payload_length_ != len) {
|
||||
ESP_LOGE(TAG, "Packet size does not match config");
|
||||
return SX126xError::INVALID_PARAMS;
|
||||
}
|
||||
if (len == 0 || len > this->get_max_packet_size()) {
|
||||
ESP_LOGE(TAG, "Packet size out of range");
|
||||
return SX126xError::INVALID_PARAMS;
|
||||
}
|
||||
|
||||
SX126xError ret = SX126xError::NONE;
|
||||
this->set_mode_standby(STDBY_XOSC);
|
||||
if (this->payload_length_ == 0) {
|
||||
this->set_packet_params_(len);
|
||||
}
|
||||
this->write_fifo_(0x00, data, len);
|
||||
this->set_mode_tx();
|
||||
|
||||
// wait until transmit completes, typically the delay will be less than 100 ms
|
||||
uint32_t start = millis();
|
||||
while (!this->dio1_pin_->digital_read()) {
|
||||
App.feed_wdt();
|
||||
if (millis() - start > TRANSMIT_TIMEOUT_MS) {
|
||||
ESP_LOGE(TAG, "Transmit packet failure");
|
||||
ret = SX126xError::TIMEOUT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t buf[2];
|
||||
buf[0] = 0xFF;
|
||||
buf[1] = 0xFF;
|
||||
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
|
||||
if (this->payload_length_ == 0) {
|
||||
this->set_packet_params_(this->get_max_packet_size());
|
||||
}
|
||||
if (this->rx_start_) {
|
||||
this->set_mode_rx();
|
||||
} else {
|
||||
this->set_mode_sleep();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool SX126x::cancel_transmit_async(SX126xTransmitListener *listener) {
|
||||
const RadioEffects effects = this->radio_.cancel_transmit(listener);
|
||||
if (!effects.finished) {
|
||||
return false;
|
||||
void SX126x::call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr) {
|
||||
for (auto &listener : this->listeners_) {
|
||||
listener->on_packet(packet, rssi, snr);
|
||||
}
|
||||
this->apply_(effects);
|
||||
return true;
|
||||
this->packet_trigger_.trigger(packet, rssi, snr);
|
||||
}
|
||||
|
||||
void SX126x::loop() {
|
||||
if (this->rx_dispatching_ || this->is_failed()) {
|
||||
if (this->dio1_pin_->is_internal()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
if (!this->dio1_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
this->apply_(this->radio_.poll(App.get_loop_component_start_time(), this->packet_));
|
||||
|
||||
uint16_t status;
|
||||
uint8_t buf[3];
|
||||
uint8_t rssi;
|
||||
int8_t snr;
|
||||
this->read_opcode_(RADIO_GET_IRQSTATUS, buf, 2);
|
||||
this->write_opcode_(RADIO_CLR_IRQSTATUS, buf, 2);
|
||||
status = (buf[0] << 8) | buf[1];
|
||||
if ((status & IRQ_RX_DONE) == IRQ_RX_DONE) {
|
||||
if ((status & IRQ_CRC_ERROR) != IRQ_CRC_ERROR) {
|
||||
this->read_opcode_(RADIO_GET_PACKETSTATUS, buf, 3);
|
||||
if (this->modulation_ == PACKET_TYPE_LORA) {
|
||||
rssi = buf[0];
|
||||
snr = buf[1];
|
||||
} else {
|
||||
rssi = buf[2];
|
||||
snr = 0;
|
||||
}
|
||||
this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, buf, 2);
|
||||
this->packet_.resize(buf[0]);
|
||||
this->read_fifo_(buf[1], this->packet_);
|
||||
this->call_listeners_(this->packet_, (float) rssi / -2.0f, (float) snr / 4.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SX126x::run_image_cal() {
|
||||
// the following values were taken from section 9.2.1 table 9-2
|
||||
// in rev 2.1 of the datasheet
|
||||
uint8_t buf[2] = {0, 0};
|
||||
if (this->frequency_ > 900000000) {
|
||||
buf[0] = 0xE1;
|
||||
buf[1] = 0xE9;
|
||||
} else if (this->frequency_ > 850000000) {
|
||||
buf[0] = 0xD7;
|
||||
buf[1] = 0xDB;
|
||||
} else if (this->frequency_ > 770000000) {
|
||||
buf[0] = 0xC1;
|
||||
buf[1] = 0xC5;
|
||||
} else if (this->frequency_ > 460000000) {
|
||||
buf[0] = 0x75;
|
||||
buf[1] = 0x81;
|
||||
} else if (this->frequency_ > 425000000) {
|
||||
buf[0] = 0x6B;
|
||||
buf[1] = 0x6F;
|
||||
}
|
||||
if (buf[0] > 0 && buf[1] > 0) {
|
||||
this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void SX126x::set_mode_rx() {
|
||||
uint8_t buf[8];
|
||||
|
||||
// configure irq params
|
||||
uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
|
||||
buf[0] = (irq >> 8) & 0xFF;
|
||||
buf[1] = (irq >> 0) & 0xFF;
|
||||
buf[2] = (irq >> 8) & 0xFF;
|
||||
buf[3] = (irq >> 0) & 0xFF;
|
||||
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
|
||||
|
||||
// set timeout to 0
|
||||
buf[0] = 0x00;
|
||||
this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
|
||||
|
||||
// switch to continuous mode rx
|
||||
buf[0] = 0xFF;
|
||||
buf[1] = 0xFF;
|
||||
buf[2] = 0xFF;
|
||||
this->write_opcode_(RADIO_SET_RX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126x::set_mode_tx() {
|
||||
uint8_t buf[8];
|
||||
|
||||
// configure irq params
|
||||
uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
|
||||
buf[0] = (irq >> 8) & 0xFF;
|
||||
buf[1] = (irq >> 0) & 0xFF;
|
||||
buf[2] = (irq >> 8) & 0xFF;
|
||||
buf[3] = (irq >> 0) & 0xFF;
|
||||
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
|
||||
|
||||
// switch to single mode tx
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
buf[2] = 0x00;
|
||||
this->write_opcode_(RADIO_SET_TX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126x::set_mode_sleep(bool cold) {
|
||||
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
|
||||
uint8_t buf[1];
|
||||
buf[0] = cold ? 0x00 : 0x04;
|
||||
this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
|
||||
}
|
||||
|
||||
void SX126x::set_mode_standby(SX126xStandbyMode mode) {
|
||||
uint8_t buf[1];
|
||||
buf[0] = mode;
|
||||
this->write_opcode_(RADIO_SET_STANDBY, buf, 1);
|
||||
}
|
||||
|
||||
void SX126x::wait_busy_() {
|
||||
// wait if the device is busy, the maximum delay is only be a few ms
|
||||
// with most commands taking only a few us
|
||||
uint32_t start = millis();
|
||||
while (this->busy_pin_->digital_read()) {
|
||||
if (millis() - start > BUSY_TIMEOUT_MS) {
|
||||
this->mark_failed(LOG_STR("Wait busy timeout"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SX126x::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SX126x:\n Ready for operation: %s",
|
||||
YESNO(!this->is_failed() && this->radio_.state() == RadioState::RADIO_STATE_IDLE));
|
||||
LOG_PIN(" CS Pin: ", this->radio_.bus().get_cs_pin());
|
||||
LOG_PIN(" BUSY Pin: ", this->radio_.bus().get_busy_pin());
|
||||
LOG_PIN(" RST Pin: ", this->radio_.get_rst_pin());
|
||||
LOG_PIN(" DIO1 Pin: ", this->radio_.get_dio1_pin());
|
||||
ESP_LOGCONFIG(TAG, "SX126x:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_PIN(" BUSY Pin: ", this->busy_pin_);
|
||||
LOG_PIN(" RST Pin: ", this->rst_pin_);
|
||||
LOG_PIN(" DIO1 Pin: ", this->dio1_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" HW Version: %15s\n"
|
||||
" Requested configuration:\n"
|
||||
" Frequency: %" PRIu32 " Hz\n"
|
||||
" Bandwidth: %" PRIu32 " Hz\n"
|
||||
" PA Power: %" PRId8 " dBm\n"
|
||||
" PA Ramp: %" PRIu16 " us\n"
|
||||
" Payload Length: %" PRIu8 "\n"
|
||||
" Payload Length: %" PRIu32 "\n"
|
||||
" CRC Enable: %s\n"
|
||||
" Rx Start: %s",
|
||||
this->version_, this->requested_profile_.frequency,
|
||||
SX126xCommands::bandwidth_hz(this->requested_profile_.bandwidth), this->requested_profile_.pa_power,
|
||||
SX126xCommands::ramp_us(this->requested_profile_.pa_ramp), this->requested_profile_.payload_length,
|
||||
TRUEFALSE(this->requested_profile_.crc_enable), TRUEFALSE(this->requested_profile_.rx_start));
|
||||
if (this->requested_profile_.modulation == PACKET_TYPE_GFSK) {
|
||||
this->version_, this->frequency_, BW_HZ[this->bandwidth_], this->pa_power_,
|
||||
progmem_read_uint16(&RAMP[this->pa_ramp_]), this->payload_length_, TRUEFALSE(this->crc_enable_),
|
||||
TRUEFALSE(this->rx_start_));
|
||||
if (this->modulation_ == PACKET_TYPE_GFSK) {
|
||||
const char *shaping = "NONE";
|
||||
if (this->requested_profile_.shaping == GAUSSIAN_BT_0_3) {
|
||||
if (this->shaping_ == GAUSSIAN_BT_0_3) {
|
||||
shaping = "GAUSSIAN_BT_0_3";
|
||||
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_0_5) {
|
||||
} else if (this->shaping_ == GAUSSIAN_BT_0_5) {
|
||||
shaping = "GAUSSIAN_BT_0_5";
|
||||
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_0_7) {
|
||||
} else if (this->shaping_ == GAUSSIAN_BT_0_7) {
|
||||
shaping = "GAUSSIAN_BT_0_7";
|
||||
} else if (this->requested_profile_.shaping == GAUSSIAN_BT_1_0) {
|
||||
} else if (this->shaping_ == GAUSSIAN_BT_1_0) {
|
||||
shaping = "GAUSSIAN_BT_1_0";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -222,15 +539,14 @@ void SX126x::dump_config() {
|
||||
" Preamble Size: %" PRIu16 "\n"
|
||||
" Preamble Detect: %" PRIu16 "\n"
|
||||
" Bitrate: %" PRIu32 "b/s",
|
||||
this->requested_profile_.deviation, shaping, this->requested_profile_.preamble_size,
|
||||
this->requested_profile_.preamble_detect, this->requested_profile_.bitrate);
|
||||
} else if (this->requested_profile_.modulation == PACKET_TYPE_LORA) {
|
||||
this->deviation_, shaping, this->preamble_size_, this->preamble_detect_, this->bitrate_);
|
||||
} else if (this->modulation_ == PACKET_TYPE_LORA) {
|
||||
const char *cr = "4/8";
|
||||
if (this->requested_profile_.coding_rate == LORA_CR_4_5) {
|
||||
if (this->coding_rate_ == LORA_CR_4_5) {
|
||||
cr = "4/5";
|
||||
} else if (this->requested_profile_.coding_rate == LORA_CR_4_6) {
|
||||
} else if (this->coding_rate_ == LORA_CR_4_6) {
|
||||
cr = "4/6";
|
||||
} else if (this->requested_profile_.coding_rate == LORA_CR_4_7) {
|
||||
} else if (this->coding_rate_ == LORA_CR_4_7) {
|
||||
cr = "4/7";
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -238,17 +554,16 @@ void SX126x::dump_config() {
|
||||
" Spreading Factor: %" PRIu8 "\n"
|
||||
" Coding Rate: %s\n"
|
||||
" Preamble Size: %" PRIu16,
|
||||
this->requested_profile_.spreading_factor, cr, this->requested_profile_.preamble_size);
|
||||
this->spreading_factor_, cr, this->preamble_size_);
|
||||
}
|
||||
if (this->requested_profile_.sync_size != 0) {
|
||||
if (!this->sync_value_.empty()) {
|
||||
char hex_buf[17]; // 8 bytes max = 16 hex chars + null
|
||||
ESP_LOGCONFIG(
|
||||
TAG, " Sync Value: 0x%s",
|
||||
format_hex_to(hex_buf, this->requested_profile_.sync_value.data(), this->requested_profile_.sync_size));
|
||||
ESP_LOGCONFIG(TAG, " Sync Value: 0x%s",
|
||||
format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size()));
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Whitening Enable: %s", TRUEFALSE(this->requested_profile_.whitening_enable));
|
||||
if (this->requested_profile_.whitening_enable) {
|
||||
ESP_LOGCONFIG(TAG, " Whitening Initial: 0x%03x", this->requested_profile_.whitening_initial);
|
||||
ESP_LOGCONFIG(TAG, " Whitening Enable: %s", TRUEFALSE(this->whitening_enable_));
|
||||
if (this->whitening_enable_) {
|
||||
ESP_LOGCONFIG(TAG, " Whitening Initial: 0x%03x", this->whitening_initial_);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,123 +1,157 @@
|
||||
#pragma once
|
||||
|
||||
#include "sx126x_radio.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "sx126x_reg.h"
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
static constexpr size_t SX126X_MAX_PACKET_SIZE = 255;
|
||||
|
||||
enum SX126xBw : uint8_t {
|
||||
// FSK
|
||||
SX126X_BW_4800,
|
||||
SX126X_BW_5800,
|
||||
SX126X_BW_7300,
|
||||
SX126X_BW_9700,
|
||||
SX126X_BW_11700,
|
||||
SX126X_BW_14600,
|
||||
SX126X_BW_19500,
|
||||
SX126X_BW_23400,
|
||||
SX126X_BW_29300,
|
||||
SX126X_BW_39000,
|
||||
SX126X_BW_46900,
|
||||
SX126X_BW_58600,
|
||||
SX126X_BW_78200,
|
||||
SX126X_BW_93800,
|
||||
SX126X_BW_117300,
|
||||
SX126X_BW_156200,
|
||||
SX126X_BW_187200,
|
||||
SX126X_BW_234300,
|
||||
SX126X_BW_312000,
|
||||
SX126X_BW_373600,
|
||||
SX126X_BW_467000,
|
||||
// LORA
|
||||
SX126X_BW_7810,
|
||||
SX126X_BW_10420,
|
||||
SX126X_BW_15630,
|
||||
SX126X_BW_20830,
|
||||
SX126X_BW_31250,
|
||||
SX126X_BW_41670,
|
||||
SX126X_BW_62500,
|
||||
SX126X_BW_125000,
|
||||
SX126X_BW_250000,
|
||||
SX126X_BW_500000,
|
||||
};
|
||||
|
||||
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS, SX126X_ERROR_NOT_READY };
|
||||
|
||||
class SX126xListener {
|
||||
public:
|
||||
// Called from loop(). The packet is borrowed until this returns; copy what you keep.
|
||||
// Listeners may send, change mode or configure. A send returns BUSY if another packet
|
||||
// is already waiting, which is then delivered by the next loop().
|
||||
virtual void on_packet(const std::vector<uint8_t> &packet, float rssi, float snr) = 0;
|
||||
};
|
||||
|
||||
class SX126x final : public Component {
|
||||
class SX126x final : public Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_8MHZ> {
|
||||
public:
|
||||
void set_spi_parent(spi::SPIComponent *parent) { this->radio_.bus().set_spi_parent(parent); }
|
||||
void set_cs_pin(GPIOPin *pin) { this->radio_.bus().set_cs_pin(pin); }
|
||||
void set_data_rate(uint32_t rate) { this->radio_.bus().set_data_rate(rate); }
|
||||
void set_mode(spi::SPIMode mode) { this->radio_.bus().set_mode(mode); }
|
||||
void set_bit_order(spi::SPIBitOrder order) { this->radio_.bus().set_bit_order(order); }
|
||||
void set_release_device(bool release) { this->radio_.bus().set_release_device(release); }
|
||||
void set_write_only(bool write_only) { this->radio_.bus().set_write_only(write_only); }
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
void set_psram_dma(bool enable) { this->radio_.bus().set_psram_dma(enable); }
|
||||
#endif
|
||||
// Releases the SPI bus, as SPIDevice::spi_teardown() did before SX126x stopped being one
|
||||
void spi_teardown() { this->radio_.bus().teardown(); }
|
||||
size_t get_max_packet_size();
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void set_bandwidth(SX126xBw bandwidth) { this->requested_profile_.bandwidth = bandwidth; }
|
||||
void set_bitrate(uint32_t bitrate) { this->requested_profile_.bitrate = bitrate; }
|
||||
void set_busy_pin(GPIOPin *busy_pin) { this->radio_.bus().set_busy_pin(busy_pin); }
|
||||
void set_coding_rate(uint8_t coding_rate) { this->requested_profile_.coding_rate = coding_rate; }
|
||||
void set_crc_enable(bool crc_enable) { this->requested_profile_.crc_enable = crc_enable; }
|
||||
void set_crc_inverted(bool crc_inverted) { this->requested_profile_.crc_inverted = crc_inverted; }
|
||||
void set_crc_size(uint8_t crc_size) { this->requested_profile_.crc_size = crc_size; }
|
||||
void set_crc_polynomial(uint16_t crc_polynomial) { this->requested_profile_.crc_polynomial = crc_polynomial; }
|
||||
void set_crc_initial(uint16_t crc_initial) { this->requested_profile_.crc_initial = crc_initial; }
|
||||
void set_whitening_enable(bool whitening_enable) { this->requested_profile_.whitening_enable = whitening_enable; }
|
||||
void set_whitening_initial(uint16_t whitening_initial) {
|
||||
this->requested_profile_.whitening_initial = whitening_initial;
|
||||
}
|
||||
void set_deviation(uint32_t deviation) { this->requested_profile_.deviation = deviation; }
|
||||
void set_dio1_pin(GPIOPin *dio1_pin) { this->radio_.set_dio1_pin(dio1_pin); }
|
||||
void set_frequency(uint32_t frequency) { this->requested_profile_.frequency = frequency; }
|
||||
void set_sx1261(bool sx1261) { this->requested_profile_.sx1261 = sx1261; }
|
||||
// Mode changes are ignored with a warning unless the radio is idle and configured.
|
||||
void set_bandwidth(SX126xBw bandwidth) { this->bandwidth_ = bandwidth; }
|
||||
void set_bitrate(uint32_t bitrate) { this->bitrate_ = bitrate; }
|
||||
void set_busy_pin(GPIOPin *busy_pin) { this->busy_pin_ = busy_pin; }
|
||||
void set_coding_rate(uint8_t coding_rate) { this->coding_rate_ = coding_rate; }
|
||||
void set_crc_enable(bool crc_enable) { this->crc_enable_ = crc_enable; }
|
||||
void set_crc_inverted(bool crc_inverted) { this->crc_inverted_ = crc_inverted; }
|
||||
void set_crc_size(uint8_t crc_size) { this->crc_size_ = crc_size; }
|
||||
void set_crc_polynomial(uint16_t crc_polynomial) { this->crc_polynomial_ = crc_polynomial; }
|
||||
void set_crc_initial(uint16_t crc_initial) { this->crc_initial_ = crc_initial; }
|
||||
void set_whitening_enable(bool whitening_enable) { this->whitening_enable_ = whitening_enable; }
|
||||
void set_whitening_initial(uint16_t whitening_initial) { this->whitening_initial_ = whitening_initial; }
|
||||
void set_deviation(uint32_t deviation) { this->deviation_ = deviation; }
|
||||
void set_dio1_pin(GPIOPin *dio1_pin) { this->dio1_pin_ = dio1_pin; }
|
||||
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
|
||||
void set_sx1261(bool sx1261) { this->sx1261_ = sx1261; }
|
||||
void set_mode_rx();
|
||||
void set_mode_tx();
|
||||
void set_mode_standby(SX126xStandbyMode mode);
|
||||
void set_mode_sleep(bool cold = false);
|
||||
void set_modulation(uint8_t modulation) { this->requested_profile_.modulation = modulation; }
|
||||
void set_pa_power(int8_t power) { this->requested_profile_.pa_power = power; }
|
||||
void set_pa_ramp(uint8_t ramp) { this->requested_profile_.pa_ramp = ramp; }
|
||||
void set_payload_length(uint8_t payload_length) { this->requested_profile_.payload_length = payload_length; }
|
||||
void set_preamble_detect(uint16_t preamble_detect) { this->requested_profile_.preamble_detect = preamble_detect; }
|
||||
void set_preamble_size(uint16_t preamble_size) { this->requested_profile_.preamble_size = preamble_size; }
|
||||
void set_rst_pin(GPIOPin *rst_pin) { this->radio_.set_rst_pin(rst_pin); }
|
||||
void set_rx_start(bool rx_start) { this->requested_profile_.rx_start = rx_start; }
|
||||
void set_rf_switch(bool rf_switch) { this->requested_profile_.rf_switch = rf_switch; }
|
||||
void set_shaping(uint8_t shaping) { this->requested_profile_.shaping = shaping; }
|
||||
void set_spreading_factor(uint8_t spreading_factor) { this->requested_profile_.spreading_factor = spreading_factor; }
|
||||
void set_sync_value(const std::vector<uint8_t> &sync_value) { this->requested_profile_.set_sync_value(sync_value); }
|
||||
void set_tcxo_voltage(uint8_t tcxo_voltage) { this->requested_profile_.tcxo_voltage = tcxo_voltage; }
|
||||
void set_tcxo_delay(uint32_t tcxo_delay) { this->requested_profile_.tcxo_delay = tcxo_delay; }
|
||||
void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
|
||||
void set_pa_power(int8_t power) { this->pa_power_ = power; }
|
||||
void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
|
||||
void set_payload_length(uint8_t payload_length) { this->payload_length_ = payload_length; }
|
||||
void set_preamble_detect(uint16_t preamble_detect) { this->preamble_detect_ = preamble_detect; }
|
||||
void set_preamble_size(uint16_t preamble_size) { this->preamble_size_ = preamble_size; }
|
||||
void set_rst_pin(GPIOPin *rst_pin) { this->rst_pin_ = rst_pin; }
|
||||
void set_rx_start(bool rx_start) { this->rx_start_ = rx_start; }
|
||||
void set_rf_switch(bool rf_switch) { this->rf_switch_ = rf_switch; }
|
||||
void set_shaping(uint8_t shaping) { this->shaping_ = shaping; }
|
||||
void set_spreading_factor(uint8_t spreading_factor) { this->spreading_factor_ = spreading_factor; }
|
||||
void set_sync_value(const std::vector<uint8_t> &sync_value) { this->sync_value_ = sync_value; }
|
||||
void set_tcxo_voltage(uint8_t tcxo_voltage) { this->tcxo_voltage_ = tcxo_voltage; }
|
||||
void set_tcxo_delay(uint32_t tcxo_delay) { this->tcxo_delay_ = tcxo_delay; }
|
||||
void run_image_cal();
|
||||
// Mode changes and configuration are allowed during RX callbacks when the radio
|
||||
// is available; they never modify the borrowed packet buffer.
|
||||
void configure();
|
||||
SX126xError transmit_packet(const uint8_t *data, size_t len);
|
||||
SX126xError transmit_packet(const std::vector<uint8_t> &packet) {
|
||||
return this->transmit_packet(packet.data(), packet.size());
|
||||
}
|
||||
// Main-loop context only. SX126X_ERROR_NONE accepts one operation and promises one completion.
|
||||
// Data is copied into the chip FIFO before return; listener must outlive completion.
|
||||
// Pending RX rejects admission with BUSY and is delivered by a later loop().
|
||||
// Timeout: 1..262143 ms. SPI/BUSY waits are synchronous; airtime is asynchronous.
|
||||
[[nodiscard]] SX126xError start_transmit_async(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
|
||||
uint32_t timeout_ms = 4000);
|
||||
[[nodiscard]] SX126xError start_transmit_async(SX126xTransmitListener *listener, const std::vector<uint8_t> &packet,
|
||||
uint32_t timeout_ms = 4000) {
|
||||
return this->start_transmit_async(listener, packet.data(), packet.size(), timeout_ms);
|
||||
}
|
||||
// Normal completion runs in loop(); cancellation completes synchronously in its caller.
|
||||
// The radio is released before callbacks, which may start another operation if recovery
|
||||
// succeeded. True accepts cancellation; the completion result reports cleanup failure.
|
||||
bool cancel_transmit_async(SX126xTransmitListener *listener);
|
||||
bool is_transmitting() const { return this->radio_.is_transmitting(); }
|
||||
void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); }
|
||||
Trigger<std::vector<uint8_t>, float, float> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
private:
|
||||
static void IRAM_ATTR gpio_intr(SX126x *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void change_mode_(ModeCommand command, uint8_t arg = 0);
|
||||
// The only place that runs user code for a radio operation: loop scheduling, then status,
|
||||
// then the completion or packet listeners. SX126xRadio has already committed its state.
|
||||
void apply_(const RadioEffects &effects);
|
||||
void watch_configuration_();
|
||||
SX126xError start_transmit_(SX126xTransmitListener *listener, const uint8_t *data, size_t len, uint32_t timeout_ms);
|
||||
SX126xRadio radio_;
|
||||
RadioProfile requested_profile_;
|
||||
char version_[16]{};
|
||||
protected:
|
||||
static void gpio_intr(SX126x *arg);
|
||||
void configure_fsk_ook_();
|
||||
void configure_lora_();
|
||||
void set_packet_params_(uint8_t payload_length);
|
||||
uint8_t read_fifo_(uint8_t offset, std::vector<uint8_t> &packet);
|
||||
void write_fifo_(uint8_t offset, const uint8_t *data, size_t len);
|
||||
void write_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
|
||||
uint8_t read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
|
||||
void write_register_(uint16_t reg, uint8_t *data, uint8_t size);
|
||||
void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float rssi, float snr);
|
||||
void wait_busy_();
|
||||
Trigger<std::vector<uint8_t>, float, float> packet_trigger_;
|
||||
std::vector<SX126xListener *> listeners_;
|
||||
std::vector<uint8_t> packet_;
|
||||
bool rx_dispatching_{false};
|
||||
bool cold_sleep_{false};
|
||||
bool reconfigure_pending_{false};
|
||||
std::vector<uint8_t> sync_value_;
|
||||
GPIOPin *busy_pin_{nullptr};
|
||||
GPIOPin *dio1_pin_{nullptr};
|
||||
GPIOPin *rst_pin_{nullptr};
|
||||
uint32_t bitrate_{0};
|
||||
uint32_t deviation_{0};
|
||||
uint32_t frequency_{0};
|
||||
uint32_t payload_length_{0};
|
||||
uint32_t tcxo_delay_{0};
|
||||
uint16_t crc_polynomial_{0};
|
||||
uint16_t crc_initial_{0};
|
||||
uint16_t whitening_initial_{0};
|
||||
uint16_t preamble_detect_{0};
|
||||
uint16_t preamble_size_{0};
|
||||
char version_[16];
|
||||
SX126xBw bandwidth_{SX126X_BW_125000};
|
||||
uint8_t crc_size_{0};
|
||||
uint8_t tcxo_voltage_{0};
|
||||
uint8_t coding_rate_{0};
|
||||
uint8_t modulation_{PACKET_TYPE_LORA};
|
||||
uint8_t pa_ramp_{0};
|
||||
uint8_t shaping_{0};
|
||||
uint8_t spreading_factor_{0};
|
||||
int8_t pa_power_{0};
|
||||
bool sx1261_{false};
|
||||
bool crc_enable_{false};
|
||||
bool crc_inverted_{false};
|
||||
bool whitening_enable_{false};
|
||||
bool rx_start_{false};
|
||||
bool rf_switch_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -1,355 +0,0 @@
|
||||
#include "sx126x_commands.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
static constexpr uint16_t RAMP[8] PROGMEM = {10, 20, 40, 80, 200, 800, 1700, 3400};
|
||||
// Aligned 32 bit loads from flash are safe on ESP8266, so this table is read directly
|
||||
static constexpr uint32_t BW_HZ[31] PROGMEM = {4800, 5800, 7300, 9700, 11700, 14600, 19500, 23400,
|
||||
29300, 39000, 46900, 58600, 78200, 93800, 117300, 156200,
|
||||
187200, 234300, 312000, 373600, 467000, 7810, 10420, 15630,
|
||||
20830, 31250, 41670, 62500, 125000, 250000, 500000};
|
||||
static constexpr uint8_t BW_LORA[10] PROGMEM = {LORA_BW_7810, LORA_BW_10420, LORA_BW_15630, LORA_BW_20830,
|
||||
LORA_BW_31250, LORA_BW_41670, LORA_BW_62500, LORA_BW_125000,
|
||||
LORA_BW_250000, LORA_BW_500000};
|
||||
static constexpr uint8_t BW_FSK[21] PROGMEM = {
|
||||
FSK_BW_4800, FSK_BW_5800, FSK_BW_7300, FSK_BW_9700, FSK_BW_11700, FSK_BW_14600, FSK_BW_19500,
|
||||
FSK_BW_23400, FSK_BW_29300, FSK_BW_39000, FSK_BW_46900, FSK_BW_58600, FSK_BW_78200, FSK_BW_93800,
|
||||
FSK_BW_117300, FSK_BW_156200, FSK_BW_187200, FSK_BW_234300, FSK_BW_312000, FSK_BW_373600, FSK_BW_467000};
|
||||
|
||||
// OCP (Over Current Protection) values
|
||||
static constexpr uint8_t OCP_80MA = 0x18; // 80 mA max current
|
||||
static constexpr uint8_t OCP_140MA = 0x38; // 140 mA max current
|
||||
|
||||
// LoRa low data rate optimization threshold
|
||||
static constexpr float LOW_DATA_RATE_OPTIMIZE_THRESHOLD = 16.38f; // 16.38 ms
|
||||
|
||||
void SX126xCommands::read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size) {
|
||||
const uint8_t header[] = {opcode, 0};
|
||||
this->spi_device_.read(header, {data, size});
|
||||
}
|
||||
|
||||
void SX126xCommands::write_opcode_(uint8_t opcode, const uint8_t *data, uint8_t size) {
|
||||
this->spi_device_.write({&opcode, 1}, {data, size});
|
||||
}
|
||||
|
||||
void SX126xCommands::read_register_(uint16_t reg, uint8_t *data, uint8_t size) {
|
||||
const uint8_t header[] = {RADIO_READ_REGISTER, static_cast<uint8_t>(reg >> 8), static_cast<uint8_t>(reg), 0};
|
||||
this->spi_device_.read(header, {data, size});
|
||||
}
|
||||
|
||||
void SX126xCommands::write_register_(uint16_t reg, const uint8_t *data, uint8_t size) {
|
||||
const uint8_t header[] = {RADIO_WRITE_REGISTER, static_cast<uint8_t>(reg >> 8), static_cast<uint8_t>(reg)};
|
||||
this->spi_device_.write(header, {data, size});
|
||||
}
|
||||
|
||||
ConfigurationResult SX126xCommands::configure(const RadioProfile &profile, std::span<char, 16> version,
|
||||
AppliedProfile &applied) {
|
||||
applied.frequency = profile.frequency;
|
||||
applied.payload_length = profile.payload_length;
|
||||
applied.modulation = profile.modulation;
|
||||
applied.rx_start = profile.rx_start;
|
||||
encode_packet_params(profile, applied);
|
||||
uint8_t buf[8];
|
||||
// wakeup
|
||||
this->read_opcode_(RADIO_GET_STATUS, nullptr, 0);
|
||||
|
||||
// config tcxo
|
||||
if (profile.tcxo_voltage != TCXO_CTRL_NONE) {
|
||||
uint32_t delay = profile.tcxo_delay >> 6;
|
||||
buf[0] = profile.tcxo_voltage;
|
||||
buf[1] = (delay >> 16) & 0xFF;
|
||||
buf[2] = (delay >> 8) & 0xFF;
|
||||
buf[3] = (delay >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_TCXOMODE, buf, 4);
|
||||
buf[0] = 0x7F;
|
||||
this->write_opcode_(RADIO_CALIBRATE, buf, 1);
|
||||
}
|
||||
|
||||
// clear errors
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x00;
|
||||
this->write_opcode_(RADIO_CLR_ERROR, buf, 2);
|
||||
|
||||
// rf switch
|
||||
if (profile.rf_switch) {
|
||||
buf[0] = 0x01;
|
||||
this->write_opcode_(RADIO_SET_RFSWITCHMODE, buf, 1);
|
||||
}
|
||||
|
||||
// check silicon version to make sure hw is ok
|
||||
this->read_register_(REG_VERSION_STRING, reinterpret_cast<uint8_t *>(version.data()), version.size());
|
||||
if (!this->spi_device_.ok()) {
|
||||
return ConfigurationResult::CONFIGURATION_RESULT_BUSY_TIMEOUT;
|
||||
}
|
||||
version[version.size() - 1] = '\0';
|
||||
if (strncmp(version.data(), "SX126", 5) != 0 && strncmp(version.data(), "LLCC68", 6) != 0) {
|
||||
return ConfigurationResult::CONFIGURATION_RESULT_UNSUPPORTED;
|
||||
}
|
||||
|
||||
// setup packet type
|
||||
buf[0] = profile.modulation;
|
||||
this->write_opcode_(RADIO_SET_PACKETTYPE, buf, 1);
|
||||
|
||||
// calibrate image
|
||||
this->calibrate_image(profile.frequency);
|
||||
|
||||
// set frequency
|
||||
uint64_t freq = ((uint64_t) profile.frequency << 25) / XTAL_FREQ;
|
||||
buf[0] = (uint8_t) ((freq >> 24) & 0xFF);
|
||||
buf[1] = (uint8_t) ((freq >> 16) & 0xFF);
|
||||
buf[2] = (uint8_t) ((freq >> 8) & 0xFF);
|
||||
buf[3] = (uint8_t) (freq & 0xFF);
|
||||
this->write_opcode_(RADIO_SET_RFFREQUENCY, buf, 4);
|
||||
|
||||
// configure pa
|
||||
int8_t pa_power = profile.pa_power;
|
||||
if (profile.sx1261) {
|
||||
// the following values were taken from section 13.1.14.1 table 13-21
|
||||
// in rev 2.1 of the datasheet
|
||||
if (pa_power == 15) {
|
||||
uint8_t cfg[4] = {0x06, 0x00, 0x01, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
} else {
|
||||
uint8_t cfg[4] = {0x04, 0x00, 0x01, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
}
|
||||
pa_power = clamp(pa_power, (int8_t) -3, (int8_t) 14);
|
||||
buf[0] = OCP_80MA;
|
||||
this->write_register_(REG_OCP, buf, 1);
|
||||
} else {
|
||||
// the following values were taken from section 13.1.14.1 table 13-21
|
||||
// in rev 2.1 of the datasheet
|
||||
uint8_t cfg[4] = {0x04, 0x07, 0x00, 0x01};
|
||||
this->write_opcode_(RADIO_SET_PACONFIG, cfg, 4);
|
||||
pa_power = clamp(pa_power, (int8_t) -3, (int8_t) 22);
|
||||
buf[0] = OCP_140MA;
|
||||
this->write_register_(REG_OCP, buf, 1);
|
||||
}
|
||||
buf[0] = pa_power;
|
||||
buf[1] = profile.pa_ramp;
|
||||
this->write_opcode_(RADIO_SET_TXPARAMS, buf, 2);
|
||||
|
||||
// configure modem
|
||||
if (profile.modulation == PACKET_TYPE_LORA) {
|
||||
// set modulation params
|
||||
float duration = 1000.0f * (1UL << profile.spreading_factor) / BW_HZ[profile.bandwidth];
|
||||
buf[0] = profile.spreading_factor;
|
||||
buf[1] = progmem_read_byte(&BW_LORA[profile.bandwidth - SX126X_BW_7810]);
|
||||
buf[2] = profile.coding_rate;
|
||||
buf[3] = (duration > LOW_DATA_RATE_OPTIMIZE_THRESHOLD) ? 0x01 : 0x00;
|
||||
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 4);
|
||||
|
||||
// set packet params and sync word
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
|
||||
if (profile.sync_size == 2) {
|
||||
this->write_register_(REG_LORA_SYNCWORD, profile.sync_value.data(), profile.sync_size);
|
||||
}
|
||||
} else {
|
||||
// set modulation params
|
||||
uint32_t bitrate = ((uint64_t) XTAL_FREQ * 32) / profile.bitrate;
|
||||
uint32_t fdev = ((uint64_t) profile.deviation << 25) / XTAL_FREQ;
|
||||
buf[0] = (bitrate >> 16) & 0xFF;
|
||||
buf[1] = (bitrate >> 8) & 0xFF;
|
||||
buf[2] = (bitrate >> 0) & 0xFF;
|
||||
buf[3] = profile.shaping;
|
||||
buf[4] = progmem_read_byte(&BW_FSK[profile.bandwidth - SX126X_BW_4800]);
|
||||
buf[5] = (fdev >> 16) & 0xFF;
|
||||
buf[6] = (fdev >> 8) & 0xFF;
|
||||
buf[7] = (fdev >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_MODULATIONPARAMS, buf, 8);
|
||||
|
||||
// set crc params
|
||||
if (profile.crc_enable) {
|
||||
buf[0] = profile.crc_initial >> 8;
|
||||
buf[1] = profile.crc_initial & 0xFF;
|
||||
this->write_register_(REG_CRC_INITIAL, buf, 2);
|
||||
buf[0] = profile.crc_polynomial >> 8;
|
||||
buf[1] = profile.crc_polynomial & 0xFF;
|
||||
this->write_register_(REG_CRC_POLYNOMIAL, buf, 2);
|
||||
}
|
||||
|
||||
// set whitening params
|
||||
if (profile.whitening_enable) {
|
||||
// according to the datasheet, section 12 table 12-1 "The user should not
|
||||
// change the value of the 7 MSB of this register"
|
||||
this->read_register_(REG_WHITENING_INITIAL, buf, 1);
|
||||
buf[0] = (buf[0] & 0xFE) | ((profile.whitening_initial >> 8) & 0x01);
|
||||
buf[1] = profile.whitening_initial & 0xFF;
|
||||
this->write_register_(REG_WHITENING_INITIAL, buf, 2);
|
||||
}
|
||||
|
||||
// set packet params and sync word
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
|
||||
if (profile.sync_size != 0) {
|
||||
this->write_register_(REG_GFSK_SYNCWORD, profile.sync_value.data(), profile.sync_size);
|
||||
}
|
||||
}
|
||||
|
||||
// switch to rx or sleep
|
||||
if (profile.rx_start) {
|
||||
this->receive();
|
||||
} else {
|
||||
this->sleep();
|
||||
}
|
||||
return this->spi_device_.ok() ? ConfigurationResult::CONFIGURATION_RESULT_OK
|
||||
: ConfigurationResult::CONFIGURATION_RESULT_BUSY_TIMEOUT;
|
||||
}
|
||||
|
||||
void SX126xCommands::encode_packet_params(const RadioProfile &profile, AppliedProfile &applied) {
|
||||
auto &buf = applied.packet_params;
|
||||
const uint8_t payload_length = profile.max_packet_size();
|
||||
if (profile.modulation == PACKET_TYPE_LORA) {
|
||||
buf[0] = (profile.preamble_size >> 8) & 0xFF;
|
||||
buf[1] = (profile.preamble_size >> 0) & 0xFF;
|
||||
buf[2] = (profile.payload_length > 0) ? 0x01 : 0x00;
|
||||
buf[3] = payload_length;
|
||||
buf[4] = (profile.crc_enable) ? 0x01 : 0x00;
|
||||
buf[5] = 0x00;
|
||||
applied.packet_params_size = 6;
|
||||
} else {
|
||||
uint16_t preamble_size = profile.preamble_size * 8;
|
||||
buf[0] = (preamble_size >> 8) & 0xFF;
|
||||
buf[1] = (preamble_size >> 0) & 0xFF;
|
||||
buf[2] = (profile.preamble_detect > 0) ? ((profile.preamble_detect - 1) | 0x04) : 0x00;
|
||||
buf[3] = profile.sync_size * 8;
|
||||
buf[4] = 0x00;
|
||||
buf[5] = (profile.payload_length > 0) ? 0x00 : 0x01;
|
||||
buf[6] = payload_length;
|
||||
if (profile.crc_enable) {
|
||||
buf[7] = (profile.crc_inverted ? 0x04 : 0x00) + (profile.crc_size & 0x02);
|
||||
} else {
|
||||
buf[7] = 0x01;
|
||||
}
|
||||
buf[8] = (profile.whitening_enable) ? 0x01 : 0x00;
|
||||
applied.packet_params_size = 9;
|
||||
}
|
||||
}
|
||||
|
||||
void SX126xCommands::set_packet_length(AppliedProfile &applied, uint8_t length) {
|
||||
applied.packet_params[applied.modulation == PACKET_TYPE_LORA ? 3 : 6] = length;
|
||||
this->write_opcode_(RADIO_SET_PACKETPARAMS, applied.packet_params.data(), applied.packet_params_size);
|
||||
}
|
||||
|
||||
void SX126xCommands::calibrate_image(uint32_t frequency) {
|
||||
// the following values were taken from section 9.2.1 table 9-2
|
||||
// in rev 2.1 of the datasheet
|
||||
uint8_t buf[2] = {0, 0};
|
||||
if (frequency > 900000000) {
|
||||
buf[0] = 0xE1;
|
||||
buf[1] = 0xE9;
|
||||
} else if (frequency > 850000000) {
|
||||
buf[0] = 0xD7;
|
||||
buf[1] = 0xDB;
|
||||
} else if (frequency > 770000000) {
|
||||
buf[0] = 0xC1;
|
||||
buf[1] = 0xC5;
|
||||
} else if (frequency > 460000000) {
|
||||
buf[0] = 0x75;
|
||||
buf[1] = 0x81;
|
||||
} else if (frequency > 425000000) {
|
||||
buf[0] = 0x6B;
|
||||
buf[1] = 0x6F;
|
||||
}
|
||||
if (buf[0] > 0 && buf[1] > 0) {
|
||||
this->write_opcode_(RADIO_CALIBRATEIMAGE, buf, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void SX126xCommands::receive() {
|
||||
uint8_t buf[8];
|
||||
|
||||
// configure irq params
|
||||
uint16_t irq = IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR;
|
||||
buf[0] = (irq >> 8) & 0xFF;
|
||||
buf[1] = (irq >> 0) & 0xFF;
|
||||
buf[2] = (irq >> 8) & 0xFF;
|
||||
buf[3] = (irq >> 0) & 0xFF;
|
||||
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
|
||||
|
||||
// set timeout to 0
|
||||
buf[0] = 0x00;
|
||||
this->write_opcode_(RADIO_SET_LORASYMBTIMEOUT, buf, 1);
|
||||
|
||||
// switch to continuous mode rx
|
||||
buf[0] = 0xFF;
|
||||
buf[1] = 0xFF;
|
||||
buf[2] = 0xFF;
|
||||
this->write_opcode_(RADIO_SET_RX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126xCommands::transmit(uint32_t timeout_ms) {
|
||||
uint8_t buf[8];
|
||||
|
||||
// configure irq params
|
||||
uint16_t irq = IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT;
|
||||
buf[0] = (irq >> 8) & 0xFF;
|
||||
buf[1] = (irq >> 0) & 0xFF;
|
||||
buf[2] = (irq >> 8) & 0xFF;
|
||||
buf[3] = (irq >> 0) & 0xFF;
|
||||
buf[4] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[5] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
buf[6] = (IRQ_RADIO_NONE >> 8) & 0xFF;
|
||||
buf[7] = (IRQ_RADIO_NONE >> 0) & 0xFF;
|
||||
this->write_opcode_(RADIO_SET_DIOIRQPARAMS, buf, 8);
|
||||
|
||||
const uint32_t ticks = timeout_ms * 64;
|
||||
buf[0] = ticks >> 16;
|
||||
buf[1] = ticks >> 8;
|
||||
buf[2] = ticks;
|
||||
this->write_opcode_(RADIO_SET_TX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126xCommands::sleep(bool cold) {
|
||||
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
|
||||
const uint8_t mode = cold ? 0x00 : 0x04;
|
||||
this->write_opcode_(RADIO_SET_SLEEP, &mode, 1);
|
||||
}
|
||||
|
||||
void SX126xCommands::standby(SX126xStandbyMode mode) {
|
||||
const uint8_t value = mode;
|
||||
this->write_opcode_(RADIO_SET_STANDBY, &value, 1);
|
||||
}
|
||||
|
||||
uint32_t SX126xCommands::bandwidth_hz(SX126xBw bw) { return BW_HZ[bw]; }
|
||||
uint16_t SX126xCommands::ramp_us(uint8_t ramp) { return progmem_read_uint16(&RAMP[ramp]); }
|
||||
|
||||
void SX126xCommands::clear_irqs(uint16_t mask) {
|
||||
const uint8_t data[] = {static_cast<uint8_t>(mask >> 8), static_cast<uint8_t>(mask)};
|
||||
this->write_opcode_(RADIO_CLR_IRQSTATUS, data, 2);
|
||||
}
|
||||
|
||||
uint16_t SX126xCommands::read_irq() {
|
||||
uint8_t data[2]{};
|
||||
this->read_opcode_(RADIO_GET_IRQSTATUS, data, 2);
|
||||
return (uint16_t(data[0]) << 8) | data[1];
|
||||
}
|
||||
|
||||
void SX126xCommands::write_fifo(const uint8_t *data, size_t size) {
|
||||
const uint8_t header[] = {RADIO_WRITE_BUFFER, 0};
|
||||
this->spi_device_.write(header, {data, size});
|
||||
}
|
||||
|
||||
void SX126xCommands::read_packet(uint8_t modulation, std::vector<uint8_t> &packet, float &rssi, float &snr) {
|
||||
uint8_t data[3]{};
|
||||
this->read_opcode_(RADIO_GET_PACKETSTATUS, data, 3);
|
||||
rssi = (modulation == PACKET_TYPE_LORA ? data[0] : data[2]) / -2.0f;
|
||||
// FSK packets report no SNR
|
||||
snr = modulation == PACKET_TYPE_LORA ? static_cast<int8_t>(data[1]) / 4.0f : NAN;
|
||||
this->read_opcode_(RADIO_GET_RXBUFFERSTATUS, data, 2);
|
||||
if (!this->spi_device_.ok()) {
|
||||
return; // The length and offset were not read.
|
||||
}
|
||||
packet.resize(data[0]);
|
||||
const uint8_t header[] = {RADIO_READ_BUFFER, data[1], 0};
|
||||
this->spi_device_.read(header, packet);
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -1,49 +0,0 @@
|
||||
#pragma once
|
||||
#include "radio_profile.h"
|
||||
#include "sx126x_spi_device.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
enum class ConfigurationResult {
|
||||
CONFIGURATION_RESULT_OK,
|
||||
CONFIGURATION_RESULT_BUSY_TIMEOUT,
|
||||
CONFIGURATION_RESULT_UNSUPPORTED
|
||||
};
|
||||
|
||||
// Encodes chip commands. No ESPHome lifecycle, callbacks, or raw SPI access.
|
||||
// Commands record failure in the SPI device: callers start a sequence with begin()
|
||||
// and check ok() at its end, or before acting on a value that was read.
|
||||
class SX126xCommands {
|
||||
public:
|
||||
SX126xCommands() = default;
|
||||
SX126xCommands(const SX126xCommands &) = delete;
|
||||
SX126xCommands &operator=(const SX126xCommands &) = delete;
|
||||
SX126xSpiDevice &bus() { return this->spi_device_; }
|
||||
// Fills applied as it goes; only use it if the result is OK.
|
||||
[[nodiscard]] ConfigurationResult configure(const RadioProfile &profile, std::span<char, 16> version,
|
||||
AppliedProfile &applied);
|
||||
// Sets the payload length in the applied packet parameters and writes them.
|
||||
void set_packet_length(AppliedProfile &applied, uint8_t length);
|
||||
void calibrate_image(uint32_t frequency);
|
||||
void standby(SX126xStandbyMode mode);
|
||||
void receive();
|
||||
void transmit(uint32_t timeout_ms = 0);
|
||||
void sleep(bool cold = false);
|
||||
void clear_irqs(uint16_t mask = 0xFFFF);
|
||||
// Returns 0 if the read was skipped or failed.
|
||||
uint16_t read_irq();
|
||||
void write_fifo(const uint8_t *data, size_t size);
|
||||
// Leaves the packet unchanged if a read fails.
|
||||
void read_packet(uint8_t modulation, std::vector<uint8_t> &packet, float &rssi, float &snr);
|
||||
static uint32_t bandwidth_hz(SX126xBw bw);
|
||||
static uint16_t ramp_us(uint8_t ramp);
|
||||
|
||||
private:
|
||||
void read_opcode_(uint8_t opcode, uint8_t *data, uint8_t size);
|
||||
void write_opcode_(uint8_t opcode, const uint8_t *data, uint8_t size);
|
||||
void read_register_(uint16_t reg, uint8_t *data, uint8_t size);
|
||||
void write_register_(uint16_t reg, const uint8_t *data, uint8_t size);
|
||||
static void encode_packet_params(const RadioProfile &profile, AppliedProfile &applied);
|
||||
SX126xSpiDevice spi_device_;
|
||||
};
|
||||
} // namespace esphome::sx126x
|
||||
@@ -1,253 +0,0 @@
|
||||
#include "sx126x_radio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
// No ESP_LOG calls in this file: log listeners are user code, see sx126x_radio.h.
|
||||
|
||||
static constexpr uint32_t RESET_DELAY_HIGH_US = 5000;
|
||||
static constexpr uint32_t RESET_DELAY_LOW_US = 2000;
|
||||
static constexpr uint32_t RECEIVE_RETRY_INTERVAL_MS = 200;
|
||||
|
||||
RadioEffects SX126xRadio::comm_report(bool ok) {
|
||||
RadioEffects effects;
|
||||
effects.report = true;
|
||||
effects.warning = ok ? nullptr : LOG_STR("Wait busy timeout");
|
||||
return effects;
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::configure(const RadioProfile &requested, std::span<char, 16> version) {
|
||||
ESPHOME_DEBUG_ASSERT(this->can_configure());
|
||||
this->busy_ = true;
|
||||
this->rx_retry_pending_ = false;
|
||||
this->pending_irq_ = 0;
|
||||
RadioEffects effects;
|
||||
effects.report = true;
|
||||
effects.warning = this->apply_configuration_(requested, version);
|
||||
this->commit_(effects.warning == nullptr ? RadioState::RADIO_STATE_IDLE
|
||||
: RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
return effects;
|
||||
}
|
||||
|
||||
const LogString *SX126xRadio::apply_configuration_(const RadioProfile &requested, std::span<char, 16> version) {
|
||||
if (!requested.sync_valid) {
|
||||
return LOG_STR("Invalid sync value");
|
||||
}
|
||||
this->rst_pin_->digital_write(true);
|
||||
delayMicroseconds(RESET_DELAY_HIGH_US);
|
||||
this->rst_pin_->digital_write(false);
|
||||
delayMicroseconds(RESET_DELAY_LOW_US);
|
||||
this->rst_pin_->digital_write(true);
|
||||
delayMicroseconds(RESET_DELAY_HIGH_US);
|
||||
this->bus().begin();
|
||||
AppliedProfile applied;
|
||||
switch (this->commands_.configure(requested, version, applied)) {
|
||||
case ConfigurationResult::CONFIGURATION_RESULT_OK:
|
||||
this->applied_ = applied;
|
||||
return nullptr;
|
||||
case ConfigurationResult::CONFIGURATION_RESULT_UNSUPPORTED:
|
||||
return LOG_STR("Unsupported chip version");
|
||||
default:
|
||||
return LOG_STR("Wait busy timeout");
|
||||
}
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::change_mode(ModeCommand command, uint8_t arg) {
|
||||
ESPHOME_DEBUG_ASSERT(this->can_change_mode());
|
||||
this->busy_ = true;
|
||||
this->bus().begin();
|
||||
switch (command) {
|
||||
case ModeCommand::MODE_COMMAND_RX:
|
||||
this->commands_.receive();
|
||||
break;
|
||||
case ModeCommand::MODE_COMMAND_TX:
|
||||
this->commands_.transmit();
|
||||
break;
|
||||
case ModeCommand::MODE_COMMAND_STANDBY:
|
||||
this->commands_.standby(static_cast<SX126xStandbyMode>(arg));
|
||||
break;
|
||||
case ModeCommand::MODE_COMMAND_SLEEP:
|
||||
case ModeCommand::MODE_COMMAND_COLD_SLEEP:
|
||||
this->commands_.sleep(command == ModeCommand::MODE_COMMAND_COLD_SLEEP);
|
||||
break;
|
||||
case ModeCommand::MODE_COMMAND_IMAGE_CAL:
|
||||
this->commands_.calibrate_image(this->applied_.frequency);
|
||||
break;
|
||||
}
|
||||
const bool ok = this->bus().ok();
|
||||
// Cold sleep loses the configuration, and a failed command leaves the chip state unknown.
|
||||
this->commit_(ok && command != ModeCommand::MODE_COMMAND_COLD_SLEEP ? RadioState::RADIO_STATE_IDLE
|
||||
: RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
return comm_report(ok);
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::start_transmit(SX126xTransmitListener *listener, const uint8_t *data, size_t len,
|
||||
uint32_t timeout_ms) {
|
||||
RadioEffects effects;
|
||||
if (this->state_ == RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
|
||||
effects.result = SX126xError::SX126X_ERROR_NOT_READY;
|
||||
return effects;
|
||||
}
|
||||
if (this->busy_ || this->state_ != RadioState::RADIO_STATE_IDLE) {
|
||||
effects.result = SX126xError::SX126X_ERROR_BUSY;
|
||||
return effects;
|
||||
}
|
||||
const uint8_t fixed_length = this->applied_.payload_length;
|
||||
if (data == nullptr || len == 0 || len > this->max_packet_size() || (fixed_length != 0 && len != fixed_length)) {
|
||||
effects.result = SX126xError::SX126X_ERROR_INVALID_PARAMS;
|
||||
return effects;
|
||||
}
|
||||
this->busy_ = true;
|
||||
this->bus().begin();
|
||||
this->commands_.standby(STDBY_XOSC);
|
||||
// RX cannot advance in standby. Keep a packet that completed before standby intact for
|
||||
// poll(), before clearing IRQs or overwriting the shared FIFO. A failed read returns 0.
|
||||
// A packet that failed its CRC is not delivered, so it does not hold up the send.
|
||||
const uint16_t irq = this->commands_.read_irq() | this->pending_irq_;
|
||||
if ((irq & IRQ_RX_DONE) && !(irq & IRQ_CRC_ERROR)) {
|
||||
this->commit_(RadioState::RADIO_STATE_HOLDING_RX);
|
||||
effects.result = SX126xError::SX126X_ERROR_BUSY;
|
||||
return effects;
|
||||
}
|
||||
this->commands_.clear_irqs();
|
||||
this->pending_irq_ = 0;
|
||||
if (fixed_length == 0) {
|
||||
this->commands_.set_packet_length(this->applied_, len);
|
||||
}
|
||||
this->commands_.write_fifo(data, len);
|
||||
this->commands_.transmit(timeout_ms);
|
||||
if (!this->bus().ok()) {
|
||||
const bool restored = this->restore_();
|
||||
this->commit_(restored ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
effects = comm_report(restored);
|
||||
effects.result = SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
|
||||
return effects;
|
||||
}
|
||||
this->tx_ = {listener, millis(), timeout_ms};
|
||||
this->commit_(RadioState::RADIO_STATE_TRANSMITTING);
|
||||
// No status report on success, so no user code runs before the caller has the result.
|
||||
// The completion reports status instead.
|
||||
return effects;
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::poll_transmit(uint32_t now) {
|
||||
if (this->busy_ || !this->is_transmitting()) {
|
||||
return {};
|
||||
}
|
||||
bool done = false;
|
||||
SX126xError outcome = SX126xError::SX126X_ERROR_NONE;
|
||||
if (this->dio1_pin_->digital_read()) {
|
||||
this->busy_ = true;
|
||||
this->bus().begin();
|
||||
const uint16_t status = this->commands_.read_irq();
|
||||
this->commands_.clear_irqs(status);
|
||||
this->busy_ = false;
|
||||
if (!this->bus().ok()) {
|
||||
outcome = SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
|
||||
done = true;
|
||||
} else if (status & IRQ_RX_TX_TIMEOUT) {
|
||||
outcome = SX126xError::SX126X_ERROR_TIMEOUT;
|
||||
done = true;
|
||||
} else if (status & IRQ_TX_DONE) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
// A cached loop tick can precede admission; signed subtraction also handles wraparound.
|
||||
if (!done && static_cast<int32_t>(now - this->tx_.started_ms) >= static_cast<int32_t>(this->tx_.timeout_ms)) {
|
||||
outcome = SX126xError::SX126X_ERROR_TIMEOUT;
|
||||
done = true;
|
||||
}
|
||||
return done ? this->finish_transmit_(outcome) : RadioEffects{};
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::cancel_transmit(SX126xTransmitListener *listener) {
|
||||
if (this->busy_ || !this->is_transmitting() || listener == nullptr || listener != this->tx_.listener) {
|
||||
return {};
|
||||
}
|
||||
return this->finish_transmit_(SX126xError::SX126X_ERROR_CANCELLED);
|
||||
}
|
||||
|
||||
// The only place a send ends.
|
||||
RadioEffects SX126xRadio::finish_transmit_(SX126xError result) {
|
||||
this->busy_ = true;
|
||||
const bool restored = this->restore_();
|
||||
this->commit_(restored ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
RadioEffects effects = comm_report(restored);
|
||||
effects.result = restored ? result : SX126xError::SX126X_ERROR_BUSY_TIMEOUT;
|
||||
effects.listener = std::exchange(this->tx_.listener, nullptr);
|
||||
effects.finished = true;
|
||||
return effects;
|
||||
}
|
||||
|
||||
bool SX126xRadio::restore_() {
|
||||
this->bus().begin();
|
||||
this->commands_.standby(STDBY_XOSC);
|
||||
this->commands_.clear_irqs();
|
||||
if (this->applied_.payload_length == 0) {
|
||||
this->commands_.set_packet_length(this->applied_, this->max_packet_size());
|
||||
}
|
||||
if (this->applied_.rx_start) {
|
||||
this->commands_.receive();
|
||||
} else {
|
||||
this->commands_.sleep();
|
||||
}
|
||||
return this->bus().ok();
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::poll(uint32_t now, std::vector<uint8_t> &packet) {
|
||||
if (this->busy_) {
|
||||
return {};
|
||||
}
|
||||
if (this->is_transmitting()) {
|
||||
return this->poll_transmit(now);
|
||||
}
|
||||
RadioEffects effects;
|
||||
if (this->state_ == RadioState::RADIO_STATE_NEEDS_CONFIGURATION) {
|
||||
effects.sleep_loop = true; // Polling cannot recover the radio; configure() can.
|
||||
return effects;
|
||||
}
|
||||
if (this->rx_retry_pending_ && now - this->rx_retry_started_ms_ < RECEIVE_RETRY_INTERVAL_MS) {
|
||||
return effects;
|
||||
}
|
||||
const bool holding = this->state_ == RadioState::RADIO_STATE_HOLDING_RX;
|
||||
if (!holding && this->pending_irq_ == 0 && !this->dio1_pin_->digital_read()) {
|
||||
effects.sleep_loop = this->dio1_pin_->is_internal(); // Its interrupt wakes the loop.
|
||||
return effects;
|
||||
}
|
||||
return this->receive_(holding, now, packet);
|
||||
}
|
||||
|
||||
RadioEffects SX126xRadio::receive_(bool holding, uint32_t now, std::vector<uint8_t> &packet) {
|
||||
this->busy_ = true;
|
||||
this->bus().begin();
|
||||
// A packet whose read failed has had its flags cleared, so they are kept until it is read.
|
||||
const uint16_t status = this->commands_.read_irq() | this->pending_irq_;
|
||||
const bool received = (status & IRQ_RX_DONE) && !(status & IRQ_CRC_ERROR);
|
||||
if (!holding) {
|
||||
// Cleared before the read, so a packet that completes during the read raises DIO1 again.
|
||||
// A held packet keeps its IRQ until restoration.
|
||||
this->commands_.clear_irqs(status);
|
||||
}
|
||||
float rssi = 0, snr = 0;
|
||||
if (received) {
|
||||
this->commands_.read_packet(this->applied_.modulation, packet, rssi, snr);
|
||||
}
|
||||
const bool read = this->bus().ok();
|
||||
this->pending_irq_ = read ? 0 : status;
|
||||
// Do not spend a BUSY timeout on every loop while a read keeps failing.
|
||||
this->rx_retry_pending_ = !read;
|
||||
this->rx_retry_started_ms_ = now;
|
||||
RadioState next = this->state_;
|
||||
if (read && holding) {
|
||||
next = this->restore_() ? RadioState::RADIO_STATE_IDLE : RadioState::RADIO_STATE_NEEDS_CONFIGURATION;
|
||||
}
|
||||
this->commit_(next);
|
||||
RadioEffects effects = comm_report(read && next != RadioState::RADIO_STATE_NEEDS_CONFIGURATION);
|
||||
effects.received = read && received;
|
||||
effects.rssi = rssi;
|
||||
effects.snr = snr;
|
||||
return effects;
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -1,149 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "radio_profile.h"
|
||||
#include "sx126x_commands.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
enum class SX126xError {
|
||||
SX126X_ERROR_NONE = 0,
|
||||
SX126X_ERROR_TIMEOUT,
|
||||
SX126X_ERROR_INVALID_PARAMS,
|
||||
SX126X_ERROR_NOT_READY,
|
||||
SX126X_ERROR_BUSY_TIMEOUT,
|
||||
SX126X_ERROR_BUSY,
|
||||
SX126X_ERROR_CANCELLED
|
||||
};
|
||||
|
||||
class SX126xTransmitListener {
|
||||
public:
|
||||
virtual void on_transmit_complete(SX126xError result) = 0;
|
||||
};
|
||||
|
||||
// Who may use the radio between operations. This is driver state, not the chip's mode.
|
||||
enum class RadioState : uint8_t {
|
||||
// Startup, cold sleep, or a failed configuration, mode command or restoration. Only
|
||||
// configure() is accepted; sends return NOT_READY and the loop sleeps. SX126x configures
|
||||
// again every second after a failure, and on the next mode command after cold sleep.
|
||||
RADIO_STATE_NEEDS_CONFIGURATION,
|
||||
// Available. The chip is receiving or sleeping, as configured, or after the TX mode command
|
||||
// sending whatever the FIFO holds. That send is not tracked: a send or mode command may cut
|
||||
// it short, and poll() clears its TX_DONE as a flag without a packet.
|
||||
RADIO_STATE_IDLE,
|
||||
// A send is on air. SPI is free between polls; only polling and cancellation are accepted.
|
||||
RADIO_STATE_TRANSMITTING,
|
||||
// Standby with a received packet left in the FIFO, found when a send entered standby.
|
||||
// The send was rejected with BUSY; the next poll() delivers the packet and restores the
|
||||
// radio. configure() may discard it to recover from repeated read failures.
|
||||
RADIO_STATE_HOLDING_RX,
|
||||
};
|
||||
|
||||
enum class ModeCommand : uint8_t {
|
||||
MODE_COMMAND_RX,
|
||||
MODE_COMMAND_TX,
|
||||
MODE_COMMAND_STANDBY,
|
||||
MODE_COMMAND_SLEEP,
|
||||
MODE_COMMAND_COLD_SLEEP,
|
||||
MODE_COMMAND_IMAGE_CAL,
|
||||
};
|
||||
|
||||
// What a radio operation leaves for its caller to do. The radio has already committed its
|
||||
// state; the caller reports status first, then calls the listener or delivers the packet.
|
||||
struct RadioEffects {
|
||||
SX126xError result{SX126xError::SX126X_ERROR_NONE};
|
||||
// The completion to notify when finished is set; null for a blocking send.
|
||||
SX126xTransmitListener *listener{nullptr};
|
||||
// With report set: the warning to set, or null to clear it.
|
||||
const LogString *warning{nullptr};
|
||||
float rssi{0};
|
||||
float snr{0};
|
||||
bool report{false};
|
||||
// A send ended, and result is its outcome.
|
||||
bool finished{false};
|
||||
// The packet buffer holds a packet to deliver.
|
||||
bool received{false};
|
||||
// Nothing to poll until the next DIO1 edge or configuration.
|
||||
bool sleep_loop{false};
|
||||
};
|
||||
|
||||
// Owns the radio state and every SPI command sequence.
|
||||
//
|
||||
// Status reports, completions and packet listeners can run user code, which may call back
|
||||
// into the driver. Every operation therefore does its SPI work with busy() set, commits the
|
||||
// next state, and returns RadioEffects; SX126x::apply_() then reports status and calls the
|
||||
// listeners. This class has no access to component status or listeners, so callbacks only
|
||||
// ever see a committed state. Do not log here either: log listeners are user code. busy()
|
||||
// covers re-entry during a sequence, for example from a GPIO pin that logs.
|
||||
//
|
||||
// Each sequence starts with bus().begin(); after a BUSY timeout the rest is skipped and
|
||||
// bus().ok() is false. A failed send preparation restores the radio in a new sequence. A
|
||||
// failed restoration needs configuration, and its result overrides the send result. A
|
||||
// successful send start reports nothing, so no user code runs before the caller has the
|
||||
// result. Every send ends in finish_transmit_(), exactly once.
|
||||
//
|
||||
// A failed receive read keeps the packet and retries after RECEIVE_RETRY_INTERVAL_MS, so a
|
||||
// stuck BUSY line does not cost a 20 ms wait on every loop.
|
||||
class SX126xRadio {
|
||||
public:
|
||||
SX126xRadio() = default;
|
||||
SX126xRadio(const SX126xRadio &) = delete;
|
||||
SX126xRadio &operator=(const SX126xRadio &) = delete;
|
||||
SX126xSpiDevice &bus() { return this->commands_.bus(); }
|
||||
void set_dio1_pin(GPIOPin *pin) { this->dio1_pin_ = pin; }
|
||||
void set_rst_pin(GPIOPin *pin) { this->rst_pin_ = pin; }
|
||||
GPIOPin *get_dio1_pin() const { return this->dio1_pin_; }
|
||||
GPIOPin *get_rst_pin() const { return this->rst_pin_; }
|
||||
RadioState state() const { return this->state_; }
|
||||
bool busy() const { return this->busy_; }
|
||||
bool is_transmitting() const { return this->state_ == RadioState::RADIO_STATE_TRANSMITTING; }
|
||||
// A held packet may be discarded: explicit reconfiguration is the recovery path.
|
||||
bool can_configure() const { return !this->busy_ && !this->is_transmitting(); }
|
||||
bool can_change_mode() const { return !this->busy_ && this->state_ == RadioState::RADIO_STATE_IDLE; }
|
||||
size_t max_packet_size() const { return this->applied_.max_packet_size(); }
|
||||
|
||||
// Requires can_configure(). Resets the chip and applies the requested profile.
|
||||
RadioEffects configure(const RadioProfile &requested, std::span<char, 16> version);
|
||||
// Requires can_change_mode(). Cold sleep and failed commands need configuration afterwards.
|
||||
RadioEffects change_mode(ModeCommand command, uint8_t arg);
|
||||
// Result SX126X_ERROR_NONE starts a send that finishes exactly once, through poll_transmit() or
|
||||
// cancel_transmit(). Any other result owes no completion.
|
||||
RadioEffects start_transmit(SX126xTransmitListener *listener, const uint8_t *data, size_t len, uint32_t timeout_ms);
|
||||
// Sets finished only when it ended the current send.
|
||||
RadioEffects poll_transmit(uint32_t now);
|
||||
// Sets finished only if listener owns the current send.
|
||||
RadioEffects cancel_transmit(SX126xTransmitListener *listener);
|
||||
// One loop step: polls a send, or reads a received packet into packet.
|
||||
RadioEffects poll(uint32_t now, std::vector<uint8_t> &packet);
|
||||
|
||||
private:
|
||||
struct TxOperation {
|
||||
SX126xTransmitListener *listener{nullptr};
|
||||
uint32_t started_ms{0};
|
||||
uint32_t timeout_ms{0};
|
||||
};
|
||||
void commit_(RadioState state) {
|
||||
this->state_ = state;
|
||||
this->busy_ = false;
|
||||
}
|
||||
static RadioEffects comm_report(bool ok);
|
||||
const LogString *apply_configuration_(const RadioProfile &requested, std::span<char, 16> version);
|
||||
bool restore_();
|
||||
RadioEffects finish_transmit_(SX126xError result);
|
||||
RadioEffects receive_(bool holding, uint32_t now, std::vector<uint8_t> &packet);
|
||||
SX126xCommands commands_;
|
||||
AppliedProfile applied_;
|
||||
TxOperation tx_;
|
||||
uint32_t rx_retry_started_ms_{0};
|
||||
uint16_t pending_irq_{0}; // flags of a received packet whose read failed after they were cleared
|
||||
GPIOPin *dio1_pin_{nullptr};
|
||||
GPIOPin *rst_pin_{nullptr};
|
||||
RadioState state_{RadioState::RADIO_STATE_NEEDS_CONFIGURATION};
|
||||
bool busy_{false};
|
||||
bool rx_retry_pending_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -1,45 +0,0 @@
|
||||
#include "sx126x_spi_device.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
static constexpr uint32_t BUSY_TIMEOUT_MS = 20;
|
||||
static constexpr uint32_t SWITCHING_DELAY_US = 1;
|
||||
|
||||
void SX126xSpiDevice::exchange_(std::span<const uint8_t> header, std::span<const uint8_t> tx, std::span<uint8_t> rx) {
|
||||
if (!this->ok_) {
|
||||
return;
|
||||
}
|
||||
// Selecting first wakes a sleeping radio. The guard releases CS even on timeout.
|
||||
Selection selected(this->device_);
|
||||
const uint32_t start = millis();
|
||||
while (this->busy_pin_->digital_read()) {
|
||||
if (millis() - start > BUSY_TIMEOUT_MS) {
|
||||
this->ok_ = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (auto byte : header) {
|
||||
this->device_.transfer_byte(byte);
|
||||
}
|
||||
for (auto byte : tx) {
|
||||
this->device_.transfer_byte(byte);
|
||||
}
|
||||
for (auto &byte : rx) {
|
||||
byte = this->device_.transfer_byte(0);
|
||||
}
|
||||
}
|
||||
|
||||
void SX126xSpiDevice::write(std::span<const uint8_t> header, std::span<const uint8_t> data) {
|
||||
this->exchange_(header, data, {});
|
||||
if (this->ok_) {
|
||||
// This delay belongs after CS is released, not in the selected interval.
|
||||
delayMicroseconds(SWITCHING_DELAY_US);
|
||||
}
|
||||
}
|
||||
|
||||
void SX126xSpiDevice::read(std::span<const uint8_t> header, std::span<uint8_t> data) {
|
||||
this->exchange_(header, {}, data);
|
||||
}
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -1,63 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include <span>
|
||||
|
||||
namespace esphome::sx126x {
|
||||
|
||||
// Persistent radio device. read()/write() use exchange_() for one complete selected interval.
|
||||
// No raw SPI access escapes. Call only from the main loop; spans are borrowed until return.
|
||||
// Failures stick: after a BUSY timeout every exchange is skipped until the next begin(),
|
||||
// so a command sequence checks ok() once instead of after every command.
|
||||
class SX126xSpiDevice {
|
||||
public:
|
||||
SX126xSpiDevice() = default;
|
||||
SX126xSpiDevice(const SX126xSpiDevice &) = delete;
|
||||
SX126xSpiDevice &operator=(const SX126xSpiDevice &) = delete;
|
||||
void set_spi_parent(spi::SPIComponent *parent) { this->device_.set_spi_parent(parent); }
|
||||
void set_cs_pin(GPIOPin *pin) { this->device_.set_cs_pin(pin); }
|
||||
void set_data_rate(uint32_t rate) { this->device_.set_data_rate(rate); }
|
||||
void set_mode(spi::SPIMode mode) { this->device_.set_mode(mode); }
|
||||
void set_bit_order(spi::SPIBitOrder order) { this->device_.set_bit_order(order); }
|
||||
void set_release_device(bool release) { this->device_.set_release_device(release); }
|
||||
void set_write_only(bool write_only) { this->device_.set_write_only(write_only); }
|
||||
#ifdef USE_SPI_PSRAM_DMA
|
||||
void set_psram_dma(bool enable) { this->device_.set_psram_dma(enable); }
|
||||
#endif
|
||||
|
||||
void set_busy_pin(GPIOPin *pin) { this->busy_pin_ = pin; }
|
||||
GPIOPin *get_busy_pin() const { return this->busy_pin_; }
|
||||
GPIOPin *get_cs_pin() const { return this->device_.get_cs_pin(); }
|
||||
void setup() { this->device_.spi_setup(); }
|
||||
void teardown() { this->device_.spi_teardown(); }
|
||||
// Starts a command sequence with no recorded failure.
|
||||
void begin() { this->ok_ = true; }
|
||||
// False once any exchange since begin() timed out waiting for BUSY.
|
||||
bool ok() const { return this->ok_; }
|
||||
void write(std::span<const uint8_t> header, std::span<const uint8_t> data);
|
||||
// Leaves data unchanged when the exchange is skipped or times out.
|
||||
void read(std::span<const uint8_t> header, std::span<uint8_t> data);
|
||||
|
||||
private:
|
||||
class Device final : public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_8MHZ> {
|
||||
public:
|
||||
GPIOPin *get_cs_pin() const { return this->cs_; }
|
||||
};
|
||||
class Selection {
|
||||
public:
|
||||
explicit Selection(Device &device) : device_(device) { this->device_.enable(); }
|
||||
~Selection() { this->device_.disable(); }
|
||||
Selection(const Selection &) = delete;
|
||||
Selection &operator=(const Selection &) = delete;
|
||||
|
||||
private:
|
||||
Device &device_;
|
||||
};
|
||||
void exchange_(std::span<const uint8_t> header, std::span<const uint8_t> tx, std::span<uint8_t> rx);
|
||||
Device device_;
|
||||
GPIOPin *busy_pin_{nullptr};
|
||||
bool ok_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::sx126x
|
||||
@@ -5,15 +5,13 @@ from esphome.const import CONF_MODEL
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["climate_ir"]
|
||||
CODEOWNERS = ["@glmnet", "@arpena", "@Rodrigoah"]
|
||||
CODEOWNERS = ["@glmnet", "@arpena"]
|
||||
|
||||
whirlpool_ns = cg.esphome_ns.namespace("whirlpool")
|
||||
WhirlpoolClimate = whirlpool_ns.class_("WhirlpoolClimate", climate_ir.ClimateIR)
|
||||
|
||||
Model = whirlpool_ns.enum("Model")
|
||||
MODELS = Model.options(
|
||||
"DG11J1-3A", "DG11J1-91", "HISENSE_ARG", "HITACHI_HCRA31NEWH", prefix="MODEL_"
|
||||
)
|
||||
MODELS = Model.options("DG11J1-3A", "DG11J1-91", "HISENSE_ARG", prefix="MODEL_")
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend(
|
||||
{
|
||||
@@ -28,5 +26,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
# cv.enum keeps the YAML key as a str subclass; the MockObj value cannot be compared
|
||||
if config[CONF_MODEL] == "HISENSE_ARG":
|
||||
cg.add_define("USE_WHIRLPOOL_HISENSE_ARG")
|
||||
elif config[CONF_MODEL] == "HITACHI_HCRA31NEWH":
|
||||
cg.add_define("USE_WHIRLPOOL_HITACHI_HCRA31NEWH")
|
||||
@@ -2,8 +2,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::whirlpool {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "whirlpool.climate");
|
||||
@@ -46,17 +44,6 @@ static constexpr uint8_t HISENSE_ARG_POWER_TOGGLE = 0x01; // byte 15
|
||||
static constexpr uint8_t HISENSE_ARG_SETTINGS_ONLY = 0x02; // byte 15
|
||||
static constexpr uint8_t HISENSE_ARG_FIXED_TEMP_STEP = 7; // auto and fan-only send 23 C
|
||||
|
||||
// Hitachi HCRA31NEWH model: power is a state in every frame
|
||||
static constexpr uint8_t HITACHI_BYTE10 = 0x20; // byte 10
|
||||
static constexpr uint8_t HITACHI_COOL = 0x02; // byte 15
|
||||
static constexpr uint8_t HITACHI_OFF = 0x01; // byte 15
|
||||
static constexpr uint8_t HITACHI_BYTE18_ON = 0x38; // byte 18
|
||||
static constexpr uint8_t HITACHI_BYTE18_OFF = 0x28; // byte 18
|
||||
static constexpr uint8_t HITACHI_BYTE19 = 0x08; // byte 19
|
||||
|
||||
// Header, 21 bytes of 8 bits, two dividers and the footer mark
|
||||
static constexpr uint32_t WHIRLPOOL_FRAME_TIMINGS = 2 + WHIRLPOOL_STATE_LENGTH * 16 + 2 * 2 + 1;
|
||||
|
||||
WhirlpoolClimate::WhirlpoolClimate()
|
||||
: climate_ir::ClimateIR(
|
||||
WHIRLPOOL_DG11J1_3A_TEMP_MIN, WHIRLPOOL_DG11J1_3A_TEMP_MAX, 1.0f, true, true,
|
||||
@@ -68,19 +55,19 @@ void WhirlpoolClimate::transmit_state() {
|
||||
this->last_transmit_time_ = millis(); // setting the time of the last transmission.
|
||||
uint8_t remote_state[WHIRLPOOL_STATE_LENGTH];
|
||||
memset(remote_state, 0, sizeof(remote_state));
|
||||
put_be16(remote_state, this->remote_id_());
|
||||
remote_state[0] = 0x83;
|
||||
remote_state[1] = 0x06;
|
||||
remote_state[6] = 0x80;
|
||||
remote_state[18] = 0x08;
|
||||
remote_state[18] = 0x08; // the same on every supported model
|
||||
|
||||
const bool powered_on = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
// Hitachi sends power as a state instead of a toggle
|
||||
const bool power_toggle = !this->is_hitachi_hcra31newh_() && powered_on != this->powered_on_assumed;
|
||||
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
const bool power_toggle = powered_on != this->powered_on_assumed;
|
||||
if (power_toggle) {
|
||||
// Set power toggle command
|
||||
remote_state[2] = 4;
|
||||
remote_state[15] = 1;
|
||||
this->powered_on_assumed = powered_on;
|
||||
}
|
||||
this->powered_on_assumed = powered_on;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
// set fan auto
|
||||
@@ -106,10 +93,6 @@ void WhirlpoolClimate::transmit_state() {
|
||||
remote_state[15] = 6;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
// These remotes keep the cool code in the power-off frame
|
||||
if (this->is_hisense_arg_() || this->is_hitachi_hcra31newh_())
|
||||
remote_state[3] = WHIRLPOOL_COOL;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -119,7 +102,6 @@ void WhirlpoolClimate::transmit_state() {
|
||||
// Temperature
|
||||
auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_()));
|
||||
const auto temp_step = (uint8_t) (temp - this->temperature_min_());
|
||||
this->target_temperature = temp; // publish what the unit was sent
|
||||
if (hisense_arg && this->fixed_temp_mode_()) {
|
||||
remote_state[3] |= HISENSE_ARG_FIXED_TEMP_STEP << 4;
|
||||
} else {
|
||||
@@ -149,6 +131,7 @@ void WhirlpoolClimate::transmit_state() {
|
||||
remote_state[2] |= WHIRLPOOL_SWING_MASK;
|
||||
remote_state[8] = WHIRLPOOL_SWING_BYTE8;
|
||||
}
|
||||
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
|
||||
} else if (this->send_swing_cmd_) {
|
||||
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) {
|
||||
remote_state[2] |= WHIRLPOOL_SWING_MASK;
|
||||
@@ -156,11 +139,6 @@ void WhirlpoolClimate::transmit_state() {
|
||||
}
|
||||
}
|
||||
|
||||
if (hisense_arg)
|
||||
this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
|
||||
if (this->is_hitachi_hcra31newh_())
|
||||
this->encode_hitachi_hcra31newh_(remote_state);
|
||||
|
||||
// Checksum
|
||||
remote_state[13] = xor_bytes(&remote_state[2], 11, remote_state[13]);
|
||||
remote_state[20] = xor_bytes(&remote_state[14], 6, remote_state[20]);
|
||||
@@ -170,7 +148,6 @@ void WhirlpoolClimate::transmit_state() {
|
||||
// Send code
|
||||
auto transmit = this->transmitter_->transmit();
|
||||
auto *data = transmit.get_data();
|
||||
data->reserve(WHIRLPOOL_FRAME_TIMINGS);
|
||||
|
||||
data->set_carrier_frequency(38000);
|
||||
|
||||
@@ -203,6 +180,10 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
|
||||
remote_state[15] = power_toggle ? HISENSE_ARG_POWER_TOGGLE : HISENSE_ARG_SETTINGS_ONLY;
|
||||
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
// The remote keeps the cool code in the power-off frame
|
||||
remote_state[3] = (remote_state[3] & 0xF0) | WHIRLPOOL_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
// The unit runs these fan speeds itself; the user's choice is kept for the next mode change
|
||||
remote_state[2] &= ~WHIRLPOOL_FAN_MASK;
|
||||
@@ -231,27 +212,6 @@ void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_tog
|
||||
#endif
|
||||
}
|
||||
|
||||
void WhirlpoolClimate::encode_hitachi_hcra31newh_(uint8_t *remote_state) const {
|
||||
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
|
||||
remote_state[10] = HITACHI_BYTE10;
|
||||
remote_state[18] = HITACHI_BYTE18_ON;
|
||||
remote_state[19] = HITACHI_BYTE19;
|
||||
// The unit takes the whole state from every frame: only the off frame carries the power bit
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
remote_state[15] = HITACHI_COOL;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
remote_state[2] |= WHIRLPOOL_POWER;
|
||||
remote_state[15] = HITACHI_OFF;
|
||||
remote_state[18] = HITACHI_BYTE18_OFF;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
// Check if the esp isn't currently transmitting.
|
||||
if (millis() - this->last_transmit_time_ < 500) {
|
||||
@@ -274,8 +234,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP))
|
||||
return false;
|
||||
}
|
||||
// The Hitachi remote always sends all 21 bytes; byte 18 holds its power state
|
||||
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid() && !this->is_hitachi_hcra31newh_()) {
|
||||
if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid()) {
|
||||
// Remote control only sent 14 bytes, nothing more to read, not even the footer
|
||||
ESP_LOGV(TAG, "Remote control only sent %d bytes", i);
|
||||
skip_footer = true;
|
||||
@@ -302,18 +261,13 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
ESP_LOGV_HEX(TAG, remote_state, WHIRLPOOL_STATE_LENGTH, WHIRLPOOL_STATE_LENGTH, "Received: %s");
|
||||
|
||||
// verify header remote code
|
||||
if (encode_uint16(remote_state[0], remote_state[1]) != this->remote_id_())
|
||||
if (remote_state[0] != 0x83 || remote_state[1] != 0x06)
|
||||
return false;
|
||||
|
||||
// powr on/off button
|
||||
ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER));
|
||||
|
||||
if (this->is_hitachi_hcra31newh_()) {
|
||||
// Power is a state: byte 18 says whether the unit is off
|
||||
this->powered_on_assumed = remote_state[18] != HITACHI_BYTE18_OFF;
|
||||
if (!this->powered_on_assumed)
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
} else if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
|
||||
if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
|
||||
auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
|
||||
|
||||
if (powered_on) {
|
||||
@@ -355,7 +309,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
int temp = remote_state[3] & 0xF0;
|
||||
ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
|
||||
temp = (uint8_t) temp >> 4;
|
||||
temp = std::min(temp + static_cast<int>(this->temperature_min_()), static_cast<int>(this->temperature_max_()));
|
||||
temp += static_cast<int>(this->temperature_min_());
|
||||
ESP_LOGVV(TAG, "Temperature Climate: %u", temp);
|
||||
this->target_temperature = temp;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,9 @@ namespace esphome::whirlpool {
|
||||
|
||||
/// Simple enum to represent models.
|
||||
enum Model {
|
||||
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
|
||||
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
|
||||
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
|
||||
MODEL_HITACHI_HCRA31NEWH = 3, /// Hitachi with the HCRA31NEWH remote, 16 to 30
|
||||
MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32
|
||||
MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30
|
||||
MODEL_HISENSE_ARG = 2, /// Hisense/BGH (Argentina), 16 to 30
|
||||
};
|
||||
|
||||
// Temperature
|
||||
@@ -30,9 +29,7 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
|
||||
|
||||
/// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override {
|
||||
// The toggle remotes flip swing on every press, so only send one when the mode really changes
|
||||
auto swing = call.get_swing_mode();
|
||||
this->send_swing_cmd_ = swing.has_value() && *swing != this->swing_mode;
|
||||
send_swing_cmd_ = call.get_swing_mode().has_value();
|
||||
climate_ir::ClimateIR::control(call);
|
||||
}
|
||||
|
||||
@@ -58,22 +55,10 @@ class WhirlpoolClimate final : public climate_ir::ClimateIR {
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
/// False when the model is compiled out, so the compiler drops the Hitachi branches.
|
||||
bool is_hitachi_hcra31newh_() const {
|
||||
#ifdef USE_WHIRLPOOL_HITACHI_HCRA31NEWH
|
||||
return this->model_ == MODEL_HITACHI_HCRA31NEWH;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
/// The remote ID sent in bytes 0 and 1.
|
||||
uint16_t remote_id_() const { return this->is_hitachi_hcra31newh_() ? 0xA959 : 0x8306; }
|
||||
/// Modes in which the Hisense ARG model sends a fixed temperature.
|
||||
bool fixed_temp_mode_() const;
|
||||
/// Add the Hisense ARG bytes on top of the common whirlpool frame.
|
||||
void encode_hisense_arg_(uint8_t *remote_state, bool power_toggle, uint8_t temp_step) const;
|
||||
/// Add the Hitachi HCRA31NEWH bytes on top of the common whirlpool frame.
|
||||
void encode_hitachi_hcra31newh_(uint8_t *remote_state) const;
|
||||
/// Set the time of the last transmission.
|
||||
uint32_t last_transmit_time_{};
|
||||
|
||||
|
||||
@@ -563,7 +563,7 @@ class WiFiComponent final : public Component {
|
||||
// (In most use cases you won't need these)
|
||||
/// Setup WiFi interface.
|
||||
void setup() override;
|
||||
#if defined(USE_ESP32) || defined(USE_LN882X) || defined(USE_BK72XX)
|
||||
#if defined(USE_LN882X) || defined(USE_BK72XX)
|
||||
void on_powerdown() override;
|
||||
#endif
|
||||
void start();
|
||||
|
||||
@@ -1635,21 +1635,6 @@ bool WiFiComponent::wifi_dpp_start_listen_() {
|
||||
void WiFiComponent::wifi_dpp_stop_listen_() { esp_supp_dpp_stop_listen(); }
|
||||
#endif
|
||||
|
||||
void WiFiComponent::on_powerdown() {
|
||||
// Leave the AP before esp_restart() or deep sleep stops Wi-Fi. esp_wifi_stop() while associated
|
||||
// makes the ESP32 retransmit its Disassociation up to the retry limit although the AP ACKs it
|
||||
// (32 copies with protected management frames), which some APs handle badly.
|
||||
// Upstream bug: https://github.com/espressif/esp-idf/issues/19443 - remove this once it is fixed
|
||||
// in the ESP-IDF versions we support.
|
||||
// Ask the driver: s_sta_connected lags until the main loop has handled the connect event.
|
||||
wifi_ap_record_t ap_info;
|
||||
if (esp_wifi_sta_get_ap_info(&ap_info) != ESP_OK)
|
||||
return;
|
||||
ESP_LOGD(TAG, "Disconnecting before powerdown");
|
||||
esp_wifi_disconnect();
|
||||
delay(100); // NOLINT: once per reboot or deep sleep, lets the disassociation go out
|
||||
}
|
||||
|
||||
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
|
||||
|
||||
bssid_t WiFiComponent::wifi_bssid() {
|
||||
|
||||
@@ -294,8 +294,6 @@ void Application::reboot() {
|
||||
for (auto &component : std::ranges::reverse_view(this->components_)) {
|
||||
component->on_shutdown();
|
||||
}
|
||||
// No teardown on a forced reboot, but still power hardware down (Wi-Fi leaves the AP)
|
||||
this->run_powerdown_hooks();
|
||||
arch_restart();
|
||||
}
|
||||
void Application::safe_reboot() {
|
||||
|
||||
@@ -185,12 +185,10 @@ class Component {
|
||||
*/
|
||||
virtual bool teardown() { return true; }
|
||||
|
||||
/** Called last before a reboot or deep sleep to power down hardware.
|
||||
/** Called after teardown is complete to power down hardware.
|
||||
*
|
||||
* On a safe shutdown this is called after all components have finished their teardown process,
|
||||
* This is called after all components have finished their teardown process,
|
||||
* making it safe to power down hardware like ethernet PHY.
|
||||
* On a forced reboot (Application::reboot()) it is called right after on_shutdown() with no
|
||||
* teardown, so implementations must not assume teardown() has run.
|
||||
*/
|
||||
virtual void on_powerdown() {}
|
||||
|
||||
|
||||
+25
-7
@@ -70,7 +70,6 @@
|
||||
#define USE_BUTTON
|
||||
#define USE_CAMERA
|
||||
#define USE_CANBUS_FD
|
||||
#define USE_CC1101_LONG_PACKET
|
||||
#define USE_CLIMATE
|
||||
#define USE_CLIMATE_VISUAL_OVERRIDES
|
||||
#define USE_CONTROLLER_REGISTRY
|
||||
@@ -152,6 +151,7 @@
|
||||
#define USE_LINE_READER
|
||||
#define USE_LOCK
|
||||
#define USE_LOGGER
|
||||
#define USE_LOGGER_CONSOLE
|
||||
#define USE_LOGGER_LEVEL_LISTENERS
|
||||
#define USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
#define USE_LVGL
|
||||
@@ -320,8 +320,6 @@
|
||||
#define USE_NEXTION_TRIGGER_CUSTOM_SWITCH
|
||||
#define USE_NEXTION_TRIGGER_CUSTOM_TEXT_SENSOR
|
||||
#define USE_NEXTION_WAVEFORM
|
||||
#define USE_NPM1300_DATA_PUBLISHER
|
||||
#define USE_NPM1300_STATUS_PUBLISHER
|
||||
#define USE_NUMBER
|
||||
#define USE_OTA_STATE_LISTENER
|
||||
#define USE_OUTPUT
|
||||
@@ -390,7 +388,6 @@
|
||||
#define USE_WATER_HEATER
|
||||
#define USE_WATER_HEATER_VISUAL_OVERRIDES
|
||||
#define USE_WHIRLPOOL_HISENSE_ARG
|
||||
#define USE_WHIRLPOOL_HITACHI_HCRA31NEWH
|
||||
#define USE_ZWAVE_PROXY
|
||||
|
||||
// Feature flags which do not work for zephyr
|
||||
@@ -762,16 +759,26 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
#define USE_LOGGER_USB_CDC
|
||||
#define USE_LOGGER_UART_SELECTION_USB_CDC
|
||||
#define ESPHOME_LOGGER_UART_NAME "USB_CDC"
|
||||
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_CDC
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32H21)
|
||||
#define USE_LOGGER_USB_SERIAL_JTAG
|
||||
#define USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG
|
||||
#define ESPHOME_LOGGER_UART_NAME "USB_SERIAL_JTAG"
|
||||
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_SERIAL_JTAG
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32H21) || defined(USE_ESP32_VARIANT_ESP32H4) || defined(USE_ESP32_VARIANT_ESP32P4) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
defined(USE_ESP32_VARIANT_ESP32H4) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S3) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
#define USE_LOGGER_USB_CDC
|
||||
#define USE_LOGGER_UART_SELECTION_USB_CDC
|
||||
#define USE_LOGGER_USB_SERIAL_JTAG
|
||||
#define USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG
|
||||
#define ESPHOME_LOGGER_UART_NAME "USB_SERIAL_JTAG"
|
||||
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_SERIAL_JTAG
|
||||
#else
|
||||
#define USE_LOGGER_UART_SELECTION_UART0
|
||||
#define ESPHOME_LOGGER_UART_NAME "UART0"
|
||||
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_UART0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -785,6 +792,9 @@
|
||||
#define USE_WIFI_SCAN_RESULTS_LOCK
|
||||
#define USE_ESP8266_LOGGER_SERIAL
|
||||
#define USE_ESP8266_LOGGER_SERIAL1
|
||||
#define USE_LOGGER_UART_SELECTION_UART0
|
||||
#define ESPHOME_LOGGER_UART_NAME "UART0"
|
||||
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_UART0
|
||||
#define USE_ESP8266_PREFERENCES_FLASH
|
||||
#define ESP8266_PREFERENCES_FLASH_WORDS 128
|
||||
#define ESP8266_PREFERENCES_RTC_EBOOT_WORDS 32
|
||||
@@ -830,6 +840,8 @@
|
||||
#define USE_I2C
|
||||
#define USE_LOGGER_USB_CDC
|
||||
#define USE_LOGGER_UART_SELECTION_USB_CDC
|
||||
#define ESPHOME_LOGGER_UART_NAME "USB_CDC"
|
||||
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_CDC
|
||||
#define USE_SOCKET_IMPL_LWIP_TCP
|
||||
#define USE_RP2040_BLE
|
||||
#define USE_RP2_BLE_TRACKER
|
||||
@@ -860,6 +872,9 @@
|
||||
#if defined(USE_BK72XX) || defined(USE_RTL87XX) || defined(USE_LN882X)
|
||||
#define USE_LIBRETINY_CRASH_HANDLER
|
||||
#endif
|
||||
#define USE_LOGGER_UART_SELECTION_DEFAULT
|
||||
#define ESPHOME_LOGGER_UART_NAME "DEFAULT"
|
||||
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_DEFAULT
|
||||
#ifdef USE_BK72XX
|
||||
#define USE_REMOTE_RECEIVER_TIMER_SAMPLING
|
||||
#endif
|
||||
@@ -914,8 +929,11 @@
|
||||
#define ESPHOME_TASK_LOG_BUFFER_SIZE 768
|
||||
#define USE_LOGGER_EARLY_MESSAGE
|
||||
#define USE_LOGGER_UART_SELECTION_USB_CDC
|
||||
#define ESPHOME_LOGGER_UART_NAME "USB_CDC"
|
||||
#define ESPHOME_LOGGER_UART_SELECTION logger::UART_SELECTION_USB_CDC
|
||||
#define USE_LOGGER_USB_CDC
|
||||
#define USE_LOGGER_WAIT_FOR_CDC
|
||||
#define USE_LOGGER_ZEPHYR_CDC_LOOP
|
||||
#define USE_NRF52_DFU
|
||||
#define USE_NRF52_REG0_VOUT 5
|
||||
#define USE_NRF52_UICR_ERASE
|
||||
|
||||
+1
-11
@@ -1155,16 +1155,6 @@ bool random_bytes(uint8_t *data, size_t len);
|
||||
/// @name Bit manipulation
|
||||
///@{
|
||||
|
||||
/// The high 4 bits of a byte, e.g. high_nibble(0xA5) == 0xA.
|
||||
constexpr uint8_t high_nibble(uint8_t byte) { return byte >> 4; }
|
||||
/// The low 4 bits of a byte, e.g. low_nibble(0xA5) == 0x5.
|
||||
constexpr uint8_t low_nibble(uint8_t byte) { return byte & 0x0F; }
|
||||
|
||||
/// Encode a 12-bit value from its high 8 and low 4 bits, e.g. encode_uint12(0x12, 0x3) == 0x123.
|
||||
/// Bits above the low 4 of \p low_bits are ignored.
|
||||
constexpr uint16_t encode_uint12(uint8_t high_bits, uint8_t low_bits) {
|
||||
return (static_cast<uint16_t>(high_bits) << 4) | low_nibble(low_bits);
|
||||
}
|
||||
/// Encode a 16-bit value given the most and least significant byte.
|
||||
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb) {
|
||||
return (static_cast<uint16_t>(msb) << 8) | (static_cast<uint16_t>(lsb));
|
||||
@@ -1267,7 +1257,7 @@ constexpr uint8_t *put_le32(uint8_t *dst, uint32_t value) {
|
||||
}
|
||||
|
||||
/// Decode a two-digit packed BCD byte (tens in the high nibble) to its value, e.g. 0x59 to 59.
|
||||
constexpr uint8_t bcd_to_uint8(uint8_t bcd) { return high_nibble(bcd) * 10 + low_nibble(bcd); }
|
||||
constexpr uint8_t bcd_to_uint8(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); }
|
||||
/// Encode a value from 0 to 99 as a two-digit packed BCD byte, e.g. 59 to 0x59.
|
||||
constexpr uint8_t uint8_to_bcd(uint8_t value) { return ((value / 10) << 4) | (value % 10); }
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
"""Tests for cc1101 packet_length validation."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.cc1101 import _validate_packet_mode
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import Lambda
|
||||
|
||||
LENGTH_LAMBDA = Lambda("return x.size() < 3 ? 0 : x[2] + 12;")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
{"packet_mode": True, "gdo0_pin": 2, "packet_length": LENGTH_LAMBDA},
|
||||
{"packet_mode": True, "gdo0_pin": 2, "packet_length": 1024},
|
||||
{"packet_mode": True, "gdo0_pin": 2, "packet_length": 64, "crc_enable": True},
|
||||
{"packet_mode": False, "packet_length": 200, "crc_enable": True},
|
||||
],
|
||||
)
|
||||
def test_packet_length_valid(config: dict[str, Any]) -> None:
|
||||
"""Fixed lengths, a lambda, and CRC on packets that fit the FIFO are accepted."""
|
||||
assert _validate_packet_mode(config) is config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "message"),
|
||||
[
|
||||
(
|
||||
{"packet_mode": False, "packet_length": LENGTH_LAMBDA},
|
||||
"packet_mode is required when packet_length is a lambda",
|
||||
),
|
||||
(
|
||||
{"packet_mode": True, "gdo0_pin": 2},
|
||||
"packet_length is required when packet_mode is enabled",
|
||||
),
|
||||
(
|
||||
{
|
||||
"packet_mode": True,
|
||||
"gdo0_pin": 2,
|
||||
"packet_length": 65,
|
||||
"crc_enable": True,
|
||||
},
|
||||
"crc_enable is not supported",
|
||||
),
|
||||
(
|
||||
{
|
||||
"packet_mode": True,
|
||||
"gdo0_pin": 2,
|
||||
"packet_length": LENGTH_LAMBDA,
|
||||
"crc_enable": True,
|
||||
},
|
||||
"crc_enable is not supported",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_packet_length_invalid(config: dict[str, Any], message: str) -> None:
|
||||
"""Invalid packet_length combinations are rejected with a clear message."""
|
||||
with pytest.raises(cv.Invalid, match=message):
|
||||
_validate_packet_mode(config)
|
||||
@@ -68,38 +68,6 @@ def test_logger_pre_setup_before_other_components(generate_main):
|
||||
)
|
||||
|
||||
|
||||
def test_default_uart_selection_is_not_emitted(generate_main):
|
||||
"""UART0 is the C++ initializer on ESP8266, so the setter is skipped."""
|
||||
main_cpp = generate_main("tests/component_tests/logger/test_logger.yaml")
|
||||
|
||||
assert "set_uart_selection(" not in main_cpp
|
||||
|
||||
|
||||
def test_custom_uart_selection_is_emitted(generate_main):
|
||||
"""A non default UART still reaches the setter before pre_setup()."""
|
||||
main_cpp = generate_main("tests/component_tests/logger/test_logger_uart1.yaml")
|
||||
|
||||
assert "set_uart_selection(logger::UART_SELECTION_UART1);" in main_cpp
|
||||
|
||||
|
||||
def test_libretiny_default_uart_selection_is_not_emitted(generate_main):
|
||||
"""DEFAULT is the C++ initializer on LibreTiny, so the setter is skipped."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/logger/test_logger_libretiny_default.yaml"
|
||||
)
|
||||
|
||||
assert "set_uart_selection(" not in main_cpp
|
||||
|
||||
|
||||
def test_libretiny_uart0_is_emitted(generate_main):
|
||||
"""UART0 is not the LibreTiny initializer, so it must still be set."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/logger/test_logger_libretiny_uart0.yaml"
|
||||
)
|
||||
|
||||
assert "set_uart_selection(logger::UART_SELECTION_UART0);" in main_cpp
|
||||
|
||||
|
||||
def test_ram_log_strings_is_deprecated(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
@@ -168,23 +136,108 @@ def test_log_v2_only_on_native_toolchain(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "usb_cdc"),
|
||||
("config", "port", "name", "zephyr_cdc_loop"),
|
||||
[
|
||||
pytest.param("test_logger_rp2_usb_cdc.yaml", True, id="usb_cdc"),
|
||||
pytest.param("test_logger_rp2_uart0.yaml", False, id="uart0"),
|
||||
pytest.param("test_logger.yaml", "UART0", "UART0", False, id="esp8266_default"),
|
||||
pytest.param(
|
||||
"test_logger_uart1.yaml", "UART1", "UART1", False, id="esp8266_uart1"
|
||||
),
|
||||
pytest.param(
|
||||
"test_logger_uart0_swap.yaml",
|
||||
"UART0_SWAP",
|
||||
"UART0_SWAP",
|
||||
False,
|
||||
id="esp8266_uart0_swap",
|
||||
),
|
||||
# ESP8266 without serial logging never opens a port
|
||||
pytest.param(
|
||||
"test_logger_no_serial.yaml", "UART0", "NONE", False, id="esp8266_no_serial"
|
||||
),
|
||||
pytest.param(
|
||||
"test_logger_libretiny_default.yaml",
|
||||
"DEFAULT",
|
||||
"DEFAULT",
|
||||
False,
|
||||
id="libretiny_default",
|
||||
),
|
||||
pytest.param(
|
||||
"test_logger_libretiny_uart0.yaml",
|
||||
"UART0",
|
||||
"UART0",
|
||||
False,
|
||||
id="libretiny_uart0",
|
||||
),
|
||||
pytest.param(
|
||||
"test_logger_rp2_usb_cdc.yaml",
|
||||
"USB_CDC",
|
||||
"USB_CDC",
|
||||
False,
|
||||
id="rp2_usb_cdc",
|
||||
),
|
||||
pytest.param(
|
||||
"test_logger_rp2_uart0.yaml", "UART0", "UART0", False, id="rp2_uart0"
|
||||
),
|
||||
pytest.param(
|
||||
"test_logger_esp32_usb_cdc.yaml",
|
||||
"USB_CDC",
|
||||
"USB_CDC",
|
||||
False,
|
||||
id="esp32_usb_cdc",
|
||||
),
|
||||
pytest.param(
|
||||
"test_logger_esp32_usb_serial_jtag.yaml",
|
||||
"USB_SERIAL_JTAG",
|
||||
"USB_SERIAL_JTAG",
|
||||
False,
|
||||
id="esp32_usb_serial_jtag",
|
||||
),
|
||||
pytest.param(
|
||||
"test_logger_nrf52_usb_cdc.yaml",
|
||||
"USB_CDC",
|
||||
"USB_CDC",
|
||||
True,
|
||||
id="nrf52_usb_cdc",
|
||||
),
|
||||
# Zephyr polls the USB port only when it logs there
|
||||
pytest.param(
|
||||
"test_logger_nrf52_usb_cdc_no_serial.yaml",
|
||||
"USB_CDC",
|
||||
"USB_CDC",
|
||||
False,
|
||||
id="nrf52_no_serial",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rp2_usb_cdc_selection_define(
|
||||
def test_uart_selection_defines(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
config: str,
|
||||
usb_cdc: bool,
|
||||
port: str,
|
||||
name: str,
|
||||
zephyr_cdc_loop: bool,
|
||||
) -> None:
|
||||
"""The USB CDC crash replay only compiles in when the logger uses USB CDC."""
|
||||
"""The chosen port is fixed at build time: one flag, its enum value and its dump_config name."""
|
||||
generate_main(f"tests/component_tests/logger/{config}")
|
||||
|
||||
defines = {d.name: d.value for d in CORE.defines}
|
||||
prefix = "USE_LOGGER_UART_SELECTION_"
|
||||
assert {d.removeprefix(prefix) for d in defines if d.startswith(prefix)} == {port}
|
||||
assert (
|
||||
"USE_LOGGER_UART_SELECTION_USB_CDC" in {d.name for d in CORE.defines}
|
||||
) is usb_cdc
|
||||
str(defines["ESPHOME_LOGGER_UART_SELECTION"])
|
||||
== f"logger::UART_SELECTION_{port}"
|
||||
)
|
||||
assert str(defines["ESPHOME_LOGGER_UART_NAME"]) == f'"{name}"'
|
||||
assert ("USE_LOGGER_ZEPHYR_CDC_LOOP" in defines) is zephyr_cdc_loop
|
||||
|
||||
|
||||
def test_log_listener_slots_count_every_listener(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""Two on_message triggers and the API each take a slot."""
|
||||
generate_main("tests/component_tests/logger/test_logger_listeners.yaml")
|
||||
|
||||
assert (
|
||||
str({d.name: d.value for d in CORE.defines}["ESPHOME_LOG_MAX_LISTENERS"]) == "3"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_CDC
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c3
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_SERIAL_JTAG
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtesttest
|
||||
|
||||
api:
|
||||
|
||||
logger:
|
||||
on_message:
|
||||
- level: ERROR
|
||||
then:
|
||||
- lambda: "(void) message;"
|
||||
- level: WARN
|
||||
then:
|
||||
- lambda: "(void) message;"
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
logger:
|
||||
baud_rate: 0
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_CDC
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
bootloader: adafruit_nrf52_sd140_v6
|
||||
|
||||
logger:
|
||||
hardware_uart: USB_CDC
|
||||
baud_rate: 0
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
logger:
|
||||
hardware_uart: UART0_SWAP
|
||||
Whitespace-only changes.
@@ -1,114 +0,0 @@
|
||||
"""Tests for the npm1300 register encoders and validators."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.const import CONF_ENABLED
|
||||
from esphome.components.npm1300 import (
|
||||
CHARGER_SCHEMA,
|
||||
NPM1300_UNSET,
|
||||
OUTPUT_VOLTAGE,
|
||||
VBUS_CURRENT_LIMIT,
|
||||
_charger_config,
|
||||
_output_config,
|
||||
_termination_voltage_code,
|
||||
_vbus_current_limit_code,
|
||||
)
|
||||
from esphome.const import CONF_MODE, CONF_VOLTAGE
|
||||
from esphome.cpp_generator import StructInitializer
|
||||
from esphome.types import ConfigType
|
||||
|
||||
|
||||
def _fields(init: StructInitializer) -> dict[str, str]:
|
||||
return {key: str(value) for key, value in init.args.items()}
|
||||
|
||||
|
||||
def _charger(config: ConfigType) -> dict[str, str]:
|
||||
return _fields(_charger_config(CHARGER_SCHEMA(config)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current", "code"), [("32mA", 16), ("150mA", 75), ("800mA", 400)]
|
||||
)
|
||||
def test_charge_current_code(current: str, code: int) -> None:
|
||||
assert _charger({"charge_current": current})["current"] == str(code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("current", ["30mA", "802mA", "151mA"])
|
||||
def test_charge_current_rejected(current: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CHARGER_SCHEMA({"charge_current": current})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("voltage", "code"),
|
||||
[(3.5, 0), (3.65, 3), (4.0, 4), (4.2, 8), (4.45, 13)],
|
||||
)
|
||||
def test_termination_voltage_code(voltage: float, code: int) -> None:
|
||||
assert _termination_voltage_code(voltage) == code
|
||||
|
||||
|
||||
@pytest.mark.parametrize("voltage", ["3.7V", "3.95V", "4.5V"])
|
||||
def test_termination_voltage_gap_rejected(voltage: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CHARGER_SCHEMA({"charge_current": "100mA", "termination_voltage": voltage})
|
||||
|
||||
|
||||
def test_warm_termination_defaults_to_termination() -> None:
|
||||
fields = _charger({"charge_current": "100mA", "termination_voltage": "4.1V"})
|
||||
assert fields["termination_voltage"] == "6"
|
||||
assert fields["warm_termination_voltage"] == "6"
|
||||
|
||||
|
||||
def test_warm_termination_code() -> None:
|
||||
fields = _charger(
|
||||
{
|
||||
"charge_current": "100mA",
|
||||
"termination_voltage": "4.2V",
|
||||
"warm_termination_voltage": "3.6V",
|
||||
}
|
||||
)
|
||||
assert fields["termination_voltage"] == "8"
|
||||
assert fields["warm_termination_voltage"] == "2"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("voltage", "code"), [("1.0V", 0), ("1.8V", 8), ("3.3V", 23)])
|
||||
def test_output_voltage_code(voltage: str, code: int) -> None:
|
||||
config = {CONF_VOLTAGE: OUTPUT_VOLTAGE(voltage)}
|
||||
assert _fields(_output_config(config))["voltage"] == str(code)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("voltage", ["0.9V", "3.4V", "1.85V"])
|
||||
def test_output_voltage_rejected(voltage: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
OUTPUT_VOLTAGE(voltage)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", [None, {}])
|
||||
def test_unset_output_fields(config: ConfigType | None) -> None:
|
||||
unset = str(NPM1300_UNSET)
|
||||
assert _fields(_output_config(config)) == {
|
||||
"voltage": unset,
|
||||
"ldo_mode": unset,
|
||||
"enabled": unset,
|
||||
}
|
||||
|
||||
|
||||
def test_output_mode_and_enabled() -> None:
|
||||
fields = _fields(_output_config({CONF_MODE: "load_switch", CONF_ENABLED: True}))
|
||||
assert fields["ldo_mode"] == "0"
|
||||
assert fields["enabled"] == "1"
|
||||
assert fields["voltage"] == str(NPM1300_UNSET)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("limit", "code"), [("100mA", 1), ("300mA", 3), ("500mA", 5), ("1.5A", 15)]
|
||||
)
|
||||
def test_vbus_current_limit_code(limit: str, code: int) -> None:
|
||||
assert _vbus_current_limit_code(VBUS_CURRENT_LIMIT(limit)) == code
|
||||
|
||||
|
||||
@pytest.mark.parametrize("limit", ["50mA", "1600mA", "550mA"])
|
||||
def test_vbus_current_limit_rejected(limit: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
VBUS_CURRENT_LIMIT(limit)
|
||||
@@ -1,18 +0,0 @@
|
||||
"""Validate the sync-word limit before firmware generation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.sx126x import CONF_SYNC_VALUE, CONFIG_SCHEMA
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [0, 1, 2, 8])
|
||||
def test_supported_sync_lengths(size: int) -> None:
|
||||
value = [0x14] * size
|
||||
assert CONFIG_SCHEMA.schema[CONF_SYNC_VALUE](value) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [9, 256])
|
||||
def test_oversized_sync_word(size: int) -> None:
|
||||
with pytest.raises(cv.Invalid, match="at most 8"):
|
||||
CONFIG_SCHEMA.schema[CONF_SYNC_VALUE]([0x14] * size)
|
||||
@@ -1,26 +0,0 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
cc1101:
|
||||
# Above 255 bytes: infinite length mode, then fixed for the remainder
|
||||
- id: transceiver_long_fixed
|
||||
cs_pin: GPIO5
|
||||
gdo0_pin: GPIO4
|
||||
packet_mode: true
|
||||
packet_length: 1024
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("cc1101", "long packet %u bytes", x.size());'
|
||||
|
||||
# Length taken from the packet header
|
||||
- id: transceiver_long_lambda
|
||||
cs_pin: GPIO15
|
||||
gdo0_pin: GPIO2
|
||||
packet_mode: true
|
||||
packet_length: !lambda |-
|
||||
if (x.size() < 3) return 0;
|
||||
if (x[0] != 0x54) return -1;
|
||||
return x[2] + 12;
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("cc1101", "lambda packet %u bytes rssi %.1f", x.size(), rssi);'
|
||||
@@ -1,24 +0,0 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
|
||||
|
||||
cc1101:
|
||||
- id: transceiver_long_fixed
|
||||
cs_pin: GPIO5
|
||||
gdo0_pin: GPIO4
|
||||
packet_mode: true
|
||||
packet_length: 1024
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("cc1101", "long packet %u bytes", x.size());'
|
||||
|
||||
- id: transceiver_long_lambda
|
||||
cs_pin: GPIO15
|
||||
gdo0_pin: GPIO2
|
||||
packet_mode: true
|
||||
packet_length: !lambda |-
|
||||
if (x.size() < 3) return 0;
|
||||
if (x[0] != 0x54) return -1;
|
||||
return x[2] + 12;
|
||||
on_packet:
|
||||
then:
|
||||
- lambda: 'ESP_LOGD("cc1101", "lambda packet %u bytes rssi %.1f", x.size(), rssi);'
|
||||
@@ -1,7 +0,0 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
gdo0_pin: GPIO4
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
|
||||
cc1101: !include common.yaml
|
||||
@@ -55,26 +55,6 @@ TEST(HelpersTest, Ilog10RoundTripMatchesLog10) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HelpersTest, Nibbles) {
|
||||
EXPECT_EQ(high_nibble(0xA5), 0xA);
|
||||
EXPECT_EQ(low_nibble(0xA5), 0x5);
|
||||
EXPECT_EQ(high_nibble(0x0F), 0x0);
|
||||
EXPECT_EQ(low_nibble(0xF0), 0x0);
|
||||
static_assert(high_nibble(0xFF) == 0xF && low_nibble(0xFF) == 0xF, "usable in constant expressions");
|
||||
}
|
||||
|
||||
TEST(HelpersTest, EncodeUint12) {
|
||||
EXPECT_EQ(encode_uint12(0x12, 0x3), 0x123);
|
||||
EXPECT_EQ(encode_uint12(0xFF, 0xF), 0xFFF);
|
||||
EXPECT_EQ(encode_uint12(0x00, 0x0), 0x000);
|
||||
EXPECT_EQ(encode_uint12(0x12, 0xA3), 0x123);
|
||||
// A touch record byte packing the low nibbles of X (high) and Y (low)
|
||||
const uint8_t shared = 0x5C;
|
||||
EXPECT_EQ(encode_uint12(0x34, high_nibble(shared)), 0x345);
|
||||
EXPECT_EQ(encode_uint12(0x67, low_nibble(shared)), 0x67C);
|
||||
static_assert(encode_uint12(0xAB, 0xC) == 0xABC, "usable in constant expressions");
|
||||
}
|
||||
|
||||
TEST(HelpersTest, SignExtend) {
|
||||
EXPECT_EQ(sign_extend<24>(0x000000), 0);
|
||||
EXPECT_EQ(sign_extend<24>(0x7FFFFF), 8388607);
|
||||
|
||||
@@ -20,14 +20,7 @@ class FakePin final : public GPIOPin {
|
||||
bool level{false};
|
||||
};
|
||||
|
||||
// Fills a 16-bit register read: the firmware check code read gets a valid code, other registers zeros.
|
||||
inline void answer_setup_read(const uint8_t *data, size_t size, uint8_t *out, size_t out_size) {
|
||||
std::memset(out, 0, out_size);
|
||||
if (size == 2 && encode_uint16(data[0], data[1]) == cstxxx::REG_FW_CHECK_CODE && out_size == 4)
|
||||
out[2] = out[3] = 0xCA;
|
||||
}
|
||||
|
||||
// Answers the 8-bit status read with a set report and 16-bit setup reads through answer_setup_read().
|
||||
// Answers the 8-bit status read with a set report and 16-bit setup reads with zeros.
|
||||
class StatusBus final : public i2c::I2CBus {
|
||||
public:
|
||||
std::array<uint8_t, 28> status{};
|
||||
@@ -42,7 +35,7 @@ class StatusBus final : public i2c::I2CBus {
|
||||
if (size == 1 && data[0] == CST226_REG_STATUS) {
|
||||
std::memcpy(out, this->status.data(), std::min(out_size, this->status.size()));
|
||||
} else {
|
||||
answer_setup_read(data, size, out, out_size);
|
||||
std::memset(out, 0, out_size);
|
||||
}
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/cst226/touchscreen/cst226_touchscreen.h"
|
||||
#include "../common.h"
|
||||
|
||||
@@ -17,45 +14,6 @@ TEST(CST226TouchscreenTest, SetupCompletes) {
|
||||
EXPECT_FALSE(hub.is_failed());
|
||||
}
|
||||
|
||||
// Records the writes that carry no read, and answers reads through answer_setup_read().
|
||||
class WriteLogBus final : public i2c::I2CBus {
|
||||
public:
|
||||
std::vector<std::vector<uint8_t>> writes;
|
||||
bool bad_check_code{false};
|
||||
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
if (out_size == 0) {
|
||||
this->writes.emplace_back(data, data + size);
|
||||
} else {
|
||||
answer_setup_read(data, size, out, out_size);
|
||||
if (this->bad_check_code)
|
||||
std::memset(out, 0, out_size);
|
||||
}
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(CST226TouchscreenTest, SetupEntersCommandModeAndLeavesIt) {
|
||||
WriteLogBus bus;
|
||||
CST226Touchscreen hub;
|
||||
hub.set_i2c_bus(&bus);
|
||||
hub.setup();
|
||||
ASSERT_GE(bus.writes.size(), 2u);
|
||||
EXPECT_EQ(bus.writes.front(), (std::vector<uint8_t>{0xD1, 0x01}));
|
||||
EXPECT_EQ(bus.writes.back(), (std::vector<uint8_t>{0xD1, 0x09}));
|
||||
EXPECT_FALSE(hub.is_failed());
|
||||
}
|
||||
|
||||
TEST(CST226TouchscreenTest, SetupFailsOnAWrongCheckCode) {
|
||||
WriteLogBus bus;
|
||||
bus.bad_check_code = true;
|
||||
CST226Touchscreen hub;
|
||||
hub.set_i2c_bus(&bus);
|
||||
hub.setup();
|
||||
EXPECT_TRUE(hub.is_failed());
|
||||
}
|
||||
|
||||
TEST(CST226TouchscreenTest, SetupFailsWithoutTheChip) {
|
||||
StatusBus bus;
|
||||
bus.fail = true;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/components/cstxxx/cstxxx.h"
|
||||
|
||||
namespace esphome::cstxxx::testing {
|
||||
|
||||
TEST(CSTxxxDecodeTouch, FirstRecordStartsTheReport) {
|
||||
const uint8_t report[report_size(1)] = {0x16, 0x12, 0x34, 0x56, 0x78, 0x01, SYNC_BYTE};
|
||||
auto touch = decode_touch(report, 0);
|
||||
EXPECT_EQ(touch.id, 1);
|
||||
EXPECT_EQ(touch.event, 0x06);
|
||||
EXPECT_EQ(touch.x, 0x125);
|
||||
EXPECT_EQ(touch.y, 0x346);
|
||||
EXPECT_EQ(touch.z, 0x78);
|
||||
}
|
||||
|
||||
TEST(CSTxxxDecodeTouch, LaterRecordsSkipTheStatusBytes) {
|
||||
uint8_t report[report_size(3)]{};
|
||||
report[FINGER_COUNT_IDX] = 3;
|
||||
report[SYNC_IDX] = SYNC_BYTE;
|
||||
const uint8_t third[5] = {0x26, 0xFF, 0x00, 0xF0, 0x05};
|
||||
std::copy(third, third + 5, report + 12);
|
||||
auto touch = decode_touch(report, 2);
|
||||
EXPECT_EQ(touch.id, 2);
|
||||
EXPECT_EQ(touch.x, 0xFFF);
|
||||
EXPECT_EQ(touch.y, 0x000);
|
||||
EXPECT_EQ(touch.z, 5);
|
||||
}
|
||||
|
||||
} // namespace esphome::cstxxx::testing
|
||||
@@ -2,7 +2,3 @@ packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
# IPv6 with an IP sensor: compiles the IPv6 event handler's listener notification
|
||||
network:
|
||||
enable_ipv6: true
|
||||
@@ -0,0 +1,54 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
// The cap and message header are compiled only for platforms with a task log buffer
|
||||
#define USE_LIBRETINY
|
||||
#ifndef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#endif
|
||||
#include "esphome/components/logger/task_log_message.h"
|
||||
#undef USE_LIBRETINY
|
||||
|
||||
namespace esphome::logger::testing {
|
||||
|
||||
TEST(TaskLogMessageTest, CapLengthStopsAt255) {
|
||||
EXPECT_EQ(task_log_cap_length(0), 0);
|
||||
EXPECT_EQ(task_log_cap_length(255), 255);
|
||||
EXPECT_EQ(task_log_cap_length(256), 255);
|
||||
EXPECT_EQ(task_log_cap_length(1000), 255);
|
||||
}
|
||||
|
||||
TEST(TaskLogMessageTest, HeaderCopiesFieldsAndThreadName) {
|
||||
TaskLogMessage msg{};
|
||||
fill_task_log_header(&msg, 3, "tag", 42, "worker");
|
||||
EXPECT_EQ(msg.level, 3);
|
||||
EXPECT_STREQ(msg.tag, "tag");
|
||||
EXPECT_EQ(msg.line, 42);
|
||||
EXPECT_STREQ(msg.thread_name, "worker");
|
||||
}
|
||||
|
||||
TEST(TaskLogMessageTest, HeaderNullThreadNameIsEmpty) {
|
||||
TaskLogMessage msg{};
|
||||
std::memset(msg.thread_name, 'x', sizeof(msg.thread_name));
|
||||
fill_task_log_header(&msg, 1, "tag", 1, nullptr);
|
||||
EXPECT_STREQ(msg.thread_name, "");
|
||||
}
|
||||
|
||||
TEST(TaskLogMessageTest, HeaderTruncatesLongThreadName) {
|
||||
TaskLogMessage msg{};
|
||||
fill_task_log_header(&msg, 1, "tag", 1, "a_thread_name_longer_than_sixteen");
|
||||
EXPECT_EQ(std::strlen(msg.thread_name), sizeof(msg.thread_name) - 1);
|
||||
EXPECT_EQ(std::strncmp(msg.thread_name, "a_thread_name_l", sizeof(msg.thread_name) - 1), 0);
|
||||
}
|
||||
|
||||
TEST(TaskLogMessageTest, TrimTrailingNewlines) {
|
||||
EXPECT_EQ(trim_trailing_newlines("", 0), 0);
|
||||
EXPECT_EQ(trim_trailing_newlines("abc", 3), 3);
|
||||
EXPECT_EQ(trim_trailing_newlines("abc\n\n", 5), 3);
|
||||
EXPECT_EQ(trim_trailing_newlines("\n\n\n", 3), 0);
|
||||
EXPECT_EQ(trim_trailing_newlines("a\nb", 3), 3);
|
||||
}
|
||||
|
||||
} // namespace esphome::logger::testing
|
||||
@@ -1,84 +0,0 @@
|
||||
npm1300:
|
||||
id: pmic
|
||||
i2c_id: i2c_bus
|
||||
vbus_current_limit: 500mA
|
||||
thermistor_resistance: 10kOhm
|
||||
thermistor_beta: 3380
|
||||
charger:
|
||||
charge_current: 150mA
|
||||
termination_voltage: 4.2V
|
||||
warm_termination_voltage: 4.0V
|
||||
termination_current: 20%
|
||||
trickle_voltage: 2.5V
|
||||
discharge_current_limit: 1000mA
|
||||
recharge: true
|
||||
buck2:
|
||||
voltage: 3.3V
|
||||
ldo1:
|
||||
voltage: 3.3V
|
||||
mode: ldo
|
||||
enabled: true
|
||||
ldo2:
|
||||
mode: load_switch
|
||||
enabled: false
|
||||
|
||||
sensor:
|
||||
- platform: npm1300
|
||||
battery_voltage:
|
||||
name: Battery Voltage
|
||||
battery_current:
|
||||
name: Battery Current
|
||||
battery_temperature:
|
||||
name: Battery Temperature
|
||||
die_temperature:
|
||||
name: PMIC Temperature
|
||||
system_voltage:
|
||||
name: System Voltage
|
||||
vbus_voltage:
|
||||
name: VBUS Voltage
|
||||
vbus_current_limit:
|
||||
name: VBUS Current Limit
|
||||
|
||||
binary_sensor:
|
||||
- platform: npm1300
|
||||
vbus_present:
|
||||
name: VBUS Present
|
||||
vbus_current_limit_active:
|
||||
name: VBUS Current Limit Active
|
||||
charging:
|
||||
name: Charging
|
||||
charge_complete:
|
||||
name: Charge Complete
|
||||
charger_error:
|
||||
name: Charger Error
|
||||
|
||||
text_sensor:
|
||||
- platform: npm1300
|
||||
charger_status:
|
||||
name: Charger Status
|
||||
|
||||
switch:
|
||||
- platform: npm1300
|
||||
charger:
|
||||
name: Charger
|
||||
buck1:
|
||||
name: BUCK1
|
||||
ldo1:
|
||||
name: LDO1
|
||||
ldo2:
|
||||
name: LDO2
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Ship Mode
|
||||
on_press:
|
||||
- npm1300.enter_ship_mode: pmic
|
||||
- platform: template
|
||||
name: Hibernate
|
||||
on_press:
|
||||
- npm1300.hibernate:
|
||||
id: pmic
|
||||
duration: 1h
|
||||
- npm1300.hibernate:
|
||||
id: pmic
|
||||
duration: !lambda return 60000;
|
||||
@@ -1,3 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
npm1300: !include common.yaml
|
||||
@@ -1,3 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/host.yaml
|
||||
npm1300: !include common.yaml
|
||||
@@ -1,3 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
npm1300: !include common.yaml
|
||||
@@ -1,16 +0,0 @@
|
||||
"""Enable real logger callbacks for radio reentrancy regression tests."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.logger import request_log_listener
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
request_log_listener()
|
||||
# The tests build a software SPI bus in C++, which no configured bus asks for
|
||||
cg.add_define("USE_SPI_SOFTWARE")
|
||||
|
||||
manifest.multi_conf = False
|
||||
manifest.to_code = to_code
|
||||
@@ -7,22 +7,3 @@ button:
|
||||
cold: true
|
||||
- sx126x.set_mode_sleep:
|
||||
cold: !lambda return id(sx126x_my_number).state > 1;
|
||||
|
||||
- platform: template
|
||||
name: "SX126x Async Send or Cancel"
|
||||
on_press:
|
||||
- lambda: |-
|
||||
class Listener : public sx126x::SX126xTransmitListener {
|
||||
public:
|
||||
void on_transmit_complete(sx126x::SX126xError result) override {
|
||||
ESP_LOGD("sx126x_test", "Async completion: %d", static_cast<int>(result));
|
||||
}
|
||||
};
|
||||
static Listener listener;
|
||||
if (id(sx126x_radio).is_transmitting()) {
|
||||
id(sx126x_radio).cancel_transmit_async(&listener);
|
||||
} else {
|
||||
const uint8_t data[] = {0x11, 0x22, 0x33};
|
||||
auto result = id(sx126x_radio).start_transmit_async(&listener, data, sizeof(data), 4000);
|
||||
ESP_LOGD("sx126x_test", "Async admission: %d", static_cast<int>(result));
|
||||
}
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user