Compare commits

..
Author SHA1 Message Date
J. Nick Koston b310c985e0 Merge remote-tracking branch 'origin/logger-simplify' into HEAD 2026-11-14 08:39:56 -06:00
J. Nick Koston 7bc87b9b00 Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:36:37 -06:00
J. Nick Koston a5c1b4f532 Merge remote-tracking branch 'origin/logger-simplify' into logger-console-api 2026-11-12 13:35:53 -06:00
J. Nick Koston 8fc6fcf724 Merge remote-tracking branch 'origin/dev' into logger-simplify
# Conflicts:
#	tests/component_tests/logger/test_logger.py
2026-11-12 13:35:46 -06:00
J. Nick Koston 22a2efd8fd Compile init_uart only for UART selections 2026-11-12 09:00:02 -06:00
J. Nick Koston e0354ae794 Merge remote-tracking branch 'origin/logger-simplify' into logger-console-api 2026-11-12 08:21:49 -06:00
J. Nick Koston f9299c1c58 Merge remote-tracking branch 'origin/dev' into logger-simplify
# Conflicts:
#	esphome/components/logger/task_log_buffer_libretiny.cpp
2026-11-12 08:21:44 -06:00
J. Nick Koston 09c40438e2 Merge remote-tracking branch 'origin/logger-simplify' into logger-console-api 2026-11-12 07:43:13 -06:00
J. Nick Koston 63df68acc4 Read defines.h before the test's platform define 2026-11-12 07:43:09 -06:00
J. Nick Koston 8936e4f54b Assert the port name and selection per config, define them beside each port in defines.h 2026-11-12 07:13:56 -06:00
J. Nick Koston 1c1cf21b16 Merge remote-tracking branch 'origin/logger-simplify' into logger-console-api 2026-11-12 07:13:10 -06:00
J. Nick Koston 71a0b9cfca Test the USB selection defines, the listener slot count and the task log message helpers 2026-11-12 07:12:45 -06:00
J. Nick Koston ebc4dd2cc9 Keep the hardware_uart selection map for device-builder, check the USB CDC read count 2026-11-12 06:09:59 -06:00
J. Nick Koston 8f705e7cdb Name the console UART and its selection with one define each 2026-11-12 01:22:29 -06:00
J. Nick Koston c2067f4452 [logger] Fix the console UART at build time and add a console read/write API 2026-11-12 00:59:19 -06:00
J. Nick Koston e1b38f5171 Copy the va_list in each caller, keep the plain CORE.data dict, assert the UART numbering 2026-11-12 00:56:16 -06:00
J. Nick Koston ef10ea99b9 [logger] Share the per-platform copies and drop dead code 2026-11-12 00:41:27 -06:00
38 changed files with 785 additions and 905 deletions

No files matched your search

+44 -38
View File
@@ -27,13 +27,13 @@ CONF_ACCELERATION_WAIT_TIME = "acceleration_wait_time"
CONF_OVERSHOOT_DURATION = "overshoot_duration"
CONF_OBSTACLE_ROLLBACK = "obstacle_rollback"
feedback_ns = cg.esphome_ns.namespace("feedback")
FeedbackCover = feedback_ns.class_("FeedbackCover", cover.Cover, cg.Component)
endstop_ns = cg.esphome_ns.namespace("feedback")
FeedbackCover = endstop_ns.class_("FeedbackCover", cover.Cover, cg.Component)
def validate_infer_endstop(config: ConfigType) -> ConfigType:
if config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]:
if not config[CONF_HAS_BUILT_IN_ENDSTOP]:
if config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] is True:
if config[CONF_HAS_BUILT_IN_ENDSTOP] is False:
raise cv.Invalid(
f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} can only be set if {CONF_HAS_BUILT_IN_ENDSTOP} is also set"
)
@@ -119,52 +119,58 @@ async def to_code(config: ConfigType) -> None:
var = await cover.new_cover(config)
await cg.register_component(var, config)
# STOP
await automation.build_automation(
var.get_stop_trigger(), [], config[CONF_STOP_ACTION]
)
# OPEN
await automation.build_automation(
var.get_open_trigger(), [], config[CONF_OPEN_ACTION]
)
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
await cg.add_variable(var.set_open_endstop, config.get(CONF_OPEN_ENDSTOP))
await cg.add_variable(var.set_open_sensor, config.get(CONF_OPEN_SENSOR))
await cg.add_variable(
var.set_open_obstacle_sensor, config.get(CONF_OPEN_OBSTACLE_SENSOR)
)
# CLOSE
await automation.build_automation(
var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]
)
cg.add(var.set_open_duration(config[CONF_OPEN_DURATION]))
cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION]))
if CONF_OPEN_ENDSTOP in config or CONF_CLOSE_ENDSTOP in config:
cg.add_define("USE_FEEDBACK_COVER_ENDSTOP")
await cg.add_variable(var.set_open_endstop, config.get(CONF_OPEN_ENDSTOP))
await cg.add_variable(var.set_close_endstop, config.get(CONF_CLOSE_ENDSTOP))
# validation requires both movement sensors or neither
if CONF_OPEN_SENSOR in config:
cg.add_define("USE_FEEDBACK_COVER_MOVEMENT")
await cg.add_variable(var.set_open_sensor, config[CONF_OPEN_SENSOR])
await cg.add_variable(var.set_close_sensor, config[CONF_CLOSE_SENSOR])
cg.add(var.set_infer_endstop(config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]))
if CONF_OPEN_OBSTACLE_SENSOR in config or CONF_CLOSE_OBSTACLE_SENSOR in config:
cg.add_define("USE_FEEDBACK_COVER_OBSTACLE")
await cg.add_variable(
var.set_open_obstacle_sensor, config.get(CONF_OPEN_OBSTACLE_SENSOR)
)
await cg.add_variable(
var.set_close_obstacle_sensor, config.get(CONF_CLOSE_OBSTACLE_SENSOR)
)
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))
cg.add_optional(var.set_max_duration, config.get(CONF_MAX_DURATION))
cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP]))
assumed_state = config.get(CONF_ASSUMED_STATE)
if assumed_state is None:
assumed_state = not (
(CONF_CLOSE_ENDSTOP in config and CONF_OPEN_ENDSTOP in config)
or config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]
)
cg.add(var.set_assumed_state(assumed_state))
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
cg.add_optional(
var.set_direction_change_waittime, config.get(CONF_DIRECTION_CHANGE_WAIT_TIME)
await cg.add_variable(var.set_close_endstop, config.get(CONF_CLOSE_ENDSTOP))
await cg.add_variable(var.set_close_sensor, config.get(CONF_CLOSE_SENSOR))
await cg.add_variable(
var.set_close_obstacle_sensor, config.get(CONF_CLOSE_OBSTACLE_SENSOR)
)
# OTHER
cg.add_optional(var.set_max_duration, config.get(CONF_MAX_DURATION))
cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP]))
if CONF_ASSUMED_STATE in config:
cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE]))
else:
cg.add(
var.set_assumed_state(
not (
(CONF_CLOSE_ENDSTOP in config and CONF_OPEN_ENDSTOP in config)
or config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]
)
)
)
cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL]))
cg.add(var.set_infer_endstop(config[CONF_INFER_ENDSTOP_FROM_MOVEMENT]))
if CONF_DIRECTION_CHANGE_WAIT_TIME in config:
cg.add(
var.set_direction_change_waittime(config[CONF_DIRECTION_CHANGE_WAIT_TIME])
)
cg.add(var.set_acceleration_wait_time(config[CONF_ACCELERATION_WAIT_TIME]))
if (overshoot := config[CONF_OVERSHOOT_DURATION]).total_milliseconds:
cg.add_define("USE_FEEDBACK_COVER_OVERSHOOT")
cg.add(var.set_overshoot_duration(overshoot))
cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK]))
+151 -92
View File
@@ -22,15 +22,15 @@ void FeedbackCover::setup() {
}
this->current_operation = COVER_OPERATION_IDLE;
#ifdef USE_FEEDBACK_COVER_ENDSTOP
#ifdef USE_BINARY_SENSOR
// if available, get position from endstop sensors
if (this->open_endstop_ != nullptr && this->open_endstop_->state) {
this->position = COVER_OPEN;
} else if (this->close_endstop_ != nullptr && this->close_endstop_->state) {
this->position = COVER_CLOSED;
}
#endif
#ifdef USE_FEEDBACK_COVER_MOVEMENT
// if available, get moving state from sensors
if (this->open_feedback_ != nullptr && this->open_feedback_->state) {
this->current_operation = COVER_OPERATION_OPENING;
} else if (this->close_feedback_ != nullptr && this->close_feedback_->state) {
@@ -51,36 +51,30 @@ CoverTraits FeedbackCover::get_traits() {
}
void FeedbackCover::dump_config() {
LOG_COVER("", "Feedback Cover", this);
ESP_LOGCONFIG(TAG,
" Open Duration: %.1fs\n"
" Close Duration: %.1fs",
this->open_duration_ / 1e3f, this->close_duration_ / 1e3f);
#ifdef USE_FEEDBACK_COVER_ENDSTOP
LOG_COVER("", "Endstop Cover", this);
ESP_LOGCONFIG(TAG, " Open Duration: %.1fs", this->open_duration_ / 1e3f);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_);
LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_);
#endif
#ifdef USE_FEEDBACK_COVER_MOVEMENT
LOG_BINARY_SENSOR(" ", "Open Feedback", this->open_feedback_);
LOG_BINARY_SENSOR(" ", "Close Feedback", this->close_feedback_);
#endif
#ifdef USE_FEEDBACK_COVER_OBSTACLE
LOG_BINARY_SENSOR(" ", "Open Obstacle", this->open_obstacle_);
#endif
ESP_LOGCONFIG(TAG, " Close Duration: %.1fs", this->close_duration_ / 1e3f);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_);
LOG_BINARY_SENSOR(" ", "Close Feedback", this->close_feedback_);
LOG_BINARY_SENSOR(" ", "Close Obstacle", this->close_obstacle_);
#endif
if (this->has_built_in_endstop_) {
ESP_LOGCONFIG(TAG, " Has builtin endstop: YES");
}
#ifdef USE_FEEDBACK_COVER_MOVEMENT
if (this->infer_endstop_) {
ESP_LOGCONFIG(TAG, " Infer endstop from movement: YES");
}
#endif
if (this->max_duration_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Max Duration: %.1fs", this->max_duration_ / 1e3f);
}
if (this->direction_change_waittime_ != UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Direction change wait time: %.1fs", this->direction_change_waittime_ / 1e3f);
if (this->direction_change_waittime_.has_value()) {
ESP_LOGCONFIG(TAG, " Direction change wait time: %.1fs", *this->direction_change_waittime_ / 1e3f);
}
if (this->acceleration_wait_time_) {
ESP_LOGCONFIG(TAG, " Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f);
@@ -90,51 +84,60 @@ void FeedbackCover::dump_config() {
ESP_LOGCONFIG(TAG, " Overshoot duration: %.1fs", this->overshoot_duration_ / 1e3f);
}
#endif
#ifdef USE_FEEDBACK_COVER_OBSTACLE
if (this->obstacle_rollback_) {
#ifdef USE_BINARY_SENSOR
if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) {
ESP_LOGCONFIG(TAG, " Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100);
}
#endif
}
#ifdef USE_FEEDBACK_COVER_MOVEMENT
#ifdef USE_BINARY_SENSOR
void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback) {
this->open_feedback_ = open_feedback;
open_feedback->add_on_state_callback([this](bool state) { this->on_feedback_(true, state); });
// setup callbacks to react to sensor changes
open_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", LOG_STR_ARG(this->get_log_name()),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
this->endstop_reached_(true);
}
this->set_current_operation_(state ? COVER_OPERATION_OPENING : COVER_OPERATION_IDLE, false);
});
}
void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback) {
this->close_feedback_ = close_feedback;
close_feedback->add_on_state_callback([this](bool state) { this->on_feedback_(false, state); });
close_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", LOG_STR_ARG(this->get_log_name()),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
this->endstop_reached_(false);
}
this->set_current_operation_(state ? COVER_OPERATION_CLOSING : COVER_OPERATION_IDLE, false);
});
}
void FeedbackCover::on_feedback_(bool open, bool state) {
const CoverOperation operation = open ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING;
ESP_LOGD(TAG, "'%s' - %s feedback '%s'.", LOG_STR_ARG(this->get_log_name()),
open ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == operation) {
this->endstop_reached_(open);
}
this->set_current_operation_(state ? operation : COVER_OPERATION_IDLE, false);
}
#endif
#ifdef USE_FEEDBACK_COVER_ENDSTOP
void FeedbackCover::set_open_endstop(binary_sensor::BinarySensor *open_endstop) {
this->open_endstop_ = open_endstop;
open_endstop->add_on_state_callback([this](bool state) {
if (state)
if (state) {
this->endstop_reached_(true);
}
});
}
void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop) {
this->close_endstop_ = close_endstop;
close_endstop->add_on_state_callback([this](bool state) {
if (state)
if (state) {
this->endstop_reached_(false);
}
});
}
#endif
@@ -144,22 +147,22 @@ void FeedbackCover::endstop_reached_(bool open_endstop) {
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
// Only when moving towards it; the cover may be returning from just past it
// only act if endstop activated while moving in the right direction, in case we are coming back
// from a position slightly past the endpoint
if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) {
float dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", LOG_STR_ARG(this->get_log_name()),
open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
// if there is no external mechanism, stop the cover
if (!this->has_built_in_endstop_) {
this->start_direction_(COVER_OPERATION_IDLE);
} else {
this->set_current_operation_(COVER_OPERATION_IDLE, true);
}
// Without movement sensors the stop above already published
if (!this->has_movement_sensors_())
return;
}
// always sync position and publish
this->publish_state();
this->last_publish_time_ = now;
}
@@ -167,54 +170,69 @@ void FeedbackCover::endstop_reached_(bool open_endstop) {
void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool is_triggered) {
if (is_triggered) {
this->current_trigger_operation_ = operation;
// With movement sensors they set the actual operation
if (this->has_movement_sensors_())
return;
}
const uint32_t now = App.get_loop_component_start_time();
this->current_operation = operation;
if (operation != COVER_OPERATION_IDLE)
this->enable_loop();
this->start_dir_time_ = this->last_recompute_time_ = now;
this->publish_state();
this->last_publish_time_ = now;
}
// if it is setting the actual operation (not triggered one) or
// if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately
// thus, triggered operation always sets current operation.
// otherwise, current operation comes from sensor, and may differ from requested operation
// this might be from delays or complex actions, or because the movement was not trigger by the component
// but initiated externally
#ifdef USE_FEEDBACK_COVER_OBSTACLE
void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle) {
this->open_obstacle_ = open_obstacle;
open_obstacle->add_on_state_callback([this](bool state) { this->on_obstacle_(true, state); });
#ifdef USE_BINARY_SENSOR
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
#endif
{
const uint32_t now = App.get_loop_component_start_time();
this->current_operation = operation;
this->start_dir_time_ = this->last_recompute_time_ = now;
this->publish_state();
this->last_publish_time_ = now;
}
}
#ifdef USE_BINARY_SENSOR
void FeedbackCover::set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle) {
this->close_obstacle_ = close_obstacle;
close_obstacle->add_on_state_callback([this](bool state) { this->on_obstacle_(false, state); });
close_obstacle->add_on_state_callback([this](bool state) {
if (state && (this->current_operation == COVER_OPERATION_CLOSING ||
this->current_trigger_operation_ == COVER_OPERATION_CLOSING)) {
ESP_LOGD(TAG, "'%s' - Close obstacle detected.", LOG_STR_ARG(this->get_log_name()));
this->start_direction_(COVER_OPERATION_IDLE);
if (this->obstacle_rollback_) {
this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
this->start_direction_(COVER_OPERATION_OPENING);
}
}
});
}
void FeedbackCover::on_obstacle_(bool open, bool state) {
const CoverOperation operation = open ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING;
if (!state || (this->current_operation != operation && this->current_trigger_operation_ != operation))
return;
ESP_LOGD(TAG, "'%s' - %s obstacle detected.", LOG_STR_ARG(this->get_log_name()),
open ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
this->start_direction_(COVER_OPERATION_IDLE);
void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle) {
this->open_obstacle_ = open_obstacle;
if (this->obstacle_rollback_) {
const float rollback = open ? -this->obstacle_rollback_ : this->obstacle_rollback_;
this->target_position_ = clamp(this->position + rollback, COVER_CLOSED, COVER_OPEN);
this->start_direction_(open ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING);
}
open_obstacle->add_on_state_callback([this](bool state) {
if (state && (this->current_operation == COVER_OPERATION_OPENING ||
this->current_trigger_operation_ == COVER_OPERATION_OPENING)) {
ESP_LOGD(TAG, "'%s' - Open obstacle detected.", LOG_STR_ARG(this->get_log_name()));
this->start_direction_(COVER_OPERATION_IDLE);
if (this->obstacle_rollback_) {
this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN);
this->start_direction_(COVER_OPERATION_CLOSING);
}
}
});
}
#endif
void FeedbackCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE) {
this->disable_loop();
if (this->current_operation == COVER_OPERATION_IDLE)
return;
}
const uint32_t now = App.get_loop_component_start_time();
// Recompute position every loop cycle
this->recompute_position_();
// if we initiated the move, check if we reached position or max time
@@ -246,7 +264,9 @@ void FeedbackCover::loop() {
}
}
// Publish intermediate positions without saving them
// update current position at requested interval, regardless of who started the movement
// so that we also update UI if there was an external movement
// don't save intermediate positions
if (now - this->last_publish_time_ > this->update_interval_) {
this->publish_state(false);
this->last_publish_time_ = now;
@@ -254,9 +274,11 @@ void FeedbackCover::loop() {
}
void FeedbackCover::control(const CoverCall &call) {
// stop action logic
if (call.get_stop()) {
this->start_direction_(COVER_OPERATION_IDLE);
} else if (call.get_toggle().has_value()) {
// toggle action logic: OPEN - STOP - CLOSE
if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
this->start_direction_(COVER_OPERATION_IDLE);
} else {
@@ -272,15 +294,28 @@ void FeedbackCover::control(const CoverCall &call) {
auto pos_opt = call.get_position();
if (!pos_opt.has_value())
return;
// go to position action
auto pos = *pos_opt;
if (pos == this->position) {
// Resend in case the assumed state is wrong
if (this->has_built_in_endstop_ &&
((pos == COVER_OPEN && !this->has_endstop_(true)) || (pos == COVER_CLOSED && !this->has_endstop_(false)))) {
// already at target,
// for covers with built in end stop, if we don't have sensors we should send the command again
// to make sure the assumed state is not wrong
if (this->has_built_in_endstop_ && ((pos == COVER_OPEN
#ifdef USE_BINARY_SENSOR
&& this->open_endstop_ == nullptr
#endif
&& !this->infer_endstop_) ||
(pos == COVER_CLOSED
#ifdef USE_BINARY_SENSOR
&& this->close_endstop_ == nullptr
#endif
&& !this->infer_endstop_))) {
this->target_position_ = pos;
this->start_direction_(pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING);
} else if (this->current_operation != COVER_OPERATION_IDLE ||
this->current_trigger_operation_ != COVER_OPERATION_IDLE) {
// if we are moving, stop
this->start_direction_(COVER_OPERATION_IDLE);
}
} else {
@@ -291,7 +326,7 @@ void FeedbackCover::control(const CoverCall &call) {
}
void FeedbackCover::stop_prev_trigger_() {
if (this->direction_change_waittime_ != UINT32_MAX) {
if (this->direction_change_waittime_.has_value()) {
this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
}
if (this->prev_command_trigger_ != nullptr) {
@@ -301,6 +336,9 @@ void FeedbackCover::stop_prev_trigger_() {
}
bool FeedbackCover::is_at_target_() const {
// if initiated externally, current operation might be different from
// operation that was triggered, thus evaluate position against what was asked
switch (this->current_trigger_operation_) {
case COVER_OPERATION_OPENING:
return this->position >= this->target_position_;
@@ -312,10 +350,10 @@ bool FeedbackCover::is_at_target_() const {
return true;
}
}
void FeedbackCover::start_direction_(CoverOperation dir) {
Trigger<> *trig;
#ifdef USE_FEEDBACK_COVER_OBSTACLE
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *obstacle{nullptr};
#endif
@@ -331,14 +369,14 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
case COVER_OPERATION_OPENING:
this->last_operation_ = dir;
trig = &this->open_trigger_;
#ifdef USE_FEEDBACK_COVER_OBSTACLE
#ifdef USE_BINARY_SENSOR
obstacle = this->open_obstacle_;
#endif
break;
case COVER_OPERATION_CLOSING:
this->last_operation_ = dir;
trig = &this->close_trigger_;
#ifdef USE_FEEDBACK_COVER_OBSTACLE
#ifdef USE_BINARY_SENSOR
obstacle = this->close_obstacle_;
#endif
break;
@@ -348,8 +386,9 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
this->stop_prev_trigger_();
#ifdef USE_FEEDBACK_COVER_OBSTACLE
// An obstacle already present aborts the start
#ifdef USE_BINARY_SENSOR
// check if there is an obstacle to start the new operation -> abort without any change
// the case when an obstacle appears while moving is handled in the callback
if (obstacle != nullptr && obstacle->state) {
ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", LOG_STR_ARG(this->get_log_name()),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
@@ -357,12 +396,14 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
}
#endif
if (this->direction_change_waittime_ != UINT32_MAX && dir != COVER_OPERATION_IDLE &&
// if we are moving and need to move in the opposite direction
// check if we have a wait time
if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
const uint32_t waittime = *this->direction_change_waittime_;
ESP_LOGD(TAG, "'%s' - Reversing direction.", LOG_STR_ARG(this->get_log_name()));
this->start_direction_(COVER_OPERATION_IDLE);
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, this->direction_change_waittime_,
[this, dir]() { this->start_direction_(dir); });
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, waittime, [this, dir]() { this->start_direction_(dir); });
} else {
this->set_current_operation_(dir, true);
this->prev_command_trigger_ = trig;
@@ -384,24 +425,42 @@ void FeedbackCover::recompute_position_() {
float min_pos;
float max_pos;
// With an endstop the estimate stops short of the end; the endstop callback sets it
// endstop sensors update position from their callbacks, and sets the fully open/close value
// If we have endstop, estimation never reaches the fully open/closed state.
// but if movement continues past corresponding endstop (inertia), keep the fully open/close state
switch (this->current_operation) {
case COVER_OPERATION_OPENING:
dir = 1.0f;
action_dur = this->open_duration_;
min_pos = COVER_CLOSED;
max_pos = this->has_endstop_(true) && this->position < COVER_OPEN ? 0.99f : COVER_OPEN;
max_pos = (
#ifdef USE_BINARY_SENSOR
this->open_endstop_ != nullptr ||
#endif
this->infer_endstop_) &&
this->position < COVER_OPEN
? 0.99f
: COVER_OPEN;
break;
case COVER_OPERATION_CLOSING:
dir = -1.0f;
action_dur = this->close_duration_;
min_pos = this->has_endstop_(false) && this->position > COVER_CLOSED ? 0.01f : COVER_CLOSED;
min_pos = (
#ifdef USE_BINARY_SENSOR
this->close_endstop_ != nullptr ||
#endif
this->infer_endstop_) &&
this->position > COVER_CLOSED
? 0.01f
: COVER_CLOSED;
max_pos = COVER_OPEN;
break;
default:
return;
}
// check if we have an acceleration_wait_time, and remove from position computation
if (now - this->start_dir_time_ > this->acceleration_wait_time_) {
uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_;
uint32_t effective_start;
+9 -45
View File
@@ -20,19 +20,13 @@ class FeedbackCover final : public cover::Cover, public Component {
Trigger<> *get_close_trigger() { return &this->close_trigger_; }
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
#ifdef USE_FEEDBACK_COVER_ENDSTOP
#ifdef USE_BINARY_SENSOR
void set_open_endstop(binary_sensor::BinarySensor *open_endstop);
void set_close_endstop(binary_sensor::BinarySensor *close_endstop);
#endif
#ifdef USE_FEEDBACK_COVER_MOVEMENT
void set_open_sensor(binary_sensor::BinarySensor *open_feedback);
void set_close_sensor(binary_sensor::BinarySensor *close_feedback);
void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; }
#endif
#ifdef USE_FEEDBACK_COVER_OBSTACLE
void set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle);
void set_close_endstop(binary_sensor::BinarySensor *close_endstop);
void set_close_sensor(binary_sensor::BinarySensor *close_feedback);
void set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle);
void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; }
#endif
void set_open_duration(uint32_t duration) { this->open_duration_ = duration; }
void set_close_duration(uint32_t duration) { this->close_duration_ = duration; }
@@ -42,7 +36,9 @@ class FeedbackCover final : public cover::Cover, public Component {
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
void set_overshoot_duration(uint32_t overshoot_duration) { this->overshoot_duration_ = overshoot_duration; }
#endif
void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; }
void set_update_interval(uint32_t interval) { this->update_interval_ = interval; }
void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; }
void set_direction_change_waittime(uint32_t waittime) { this->direction_change_waittime_ = waittime; }
void set_acceleration_wait_time(uint32_t waittime) { this->acceleration_wait_time_ = waittime; }
@@ -57,43 +53,15 @@ class FeedbackCover final : public cover::Cover, public Component {
void endstop_reached_(bool open_endstop);
void recompute_position_();
void set_current_operation_(cover::CoverOperation operation, bool is_triggered);
#ifdef USE_FEEDBACK_COVER_MOVEMENT
void on_feedback_(bool open, bool state);
#endif
#ifdef USE_FEEDBACK_COVER_OBSTACLE
void on_obstacle_(bool open, bool state);
#endif
bool has_endstop_(bool open) const {
#ifdef USE_FEEDBACK_COVER_ENDSTOP
if ((open ? this->open_endstop_ : this->close_endstop_) != nullptr)
return true;
#endif
#ifdef USE_FEEDBACK_COVER_MOVEMENT
return this->infer_endstop_;
#else
return false;
#endif
}
bool has_movement_sensors_() const {
#ifdef USE_FEEDBACK_COVER_MOVEMENT
return this->open_feedback_ != nullptr && this->close_feedback_ != nullptr;
#else
return false;
#endif
}
#ifdef USE_FEEDBACK_COVER_ENDSTOP
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *open_endstop_{nullptr};
binary_sensor::BinarySensor *close_endstop_{nullptr};
#endif
#ifdef USE_FEEDBACK_COVER_MOVEMENT
binary_sensor::BinarySensor *open_feedback_{nullptr};
binary_sensor::BinarySensor *close_feedback_{nullptr};
#endif
#ifdef USE_FEEDBACK_COVER_OBSTACLE
binary_sensor::BinarySensor *open_obstacle_{nullptr};
binary_sensor::BinarySensor *close_obstacle_{nullptr};
#endif
Trigger<> open_trigger_;
Trigger<> close_trigger_;
@@ -105,13 +73,12 @@ class FeedbackCover final : public cover::Cover, public Component {
#ifdef USE_FEEDBACK_COVER_OVERSHOOT
uint32_t overshoot_duration_{0};
#endif
uint32_t direction_change_waittime_{UINT32_MAX};
optional<uint32_t> direction_change_waittime_{};
uint32_t acceleration_wait_time_{0};
bool has_built_in_endstop_{false};
bool assumed_state_{false};
#ifdef USE_FEEDBACK_COVER_MOVEMENT
bool infer_endstop_{false};
#endif
float obstacle_rollback_{0};
cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING};
cover::CoverOperation current_trigger_operation_{cover::COVER_OPERATION_IDLE};
@@ -123,9 +90,6 @@ class FeedbackCover final : public cover::Cover, public Component {
uint32_t start_overshoot_time_{0};
#endif
float target_position_{0};
#ifdef USE_FEEDBACK_COVER_OBSTACLE
float obstacle_rollback_{0};
#endif
uint32_t update_interval_{1000};
};
+36 -55
View File
@@ -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()
+37 -20
View File
@@ -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
+74 -84
View File
@@ -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
+42 -46
View File
@@ -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() {
+1 -1
View File
@@ -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
}
+3 -16
View File
@@ -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
+20 -51
View File
@@ -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
+10 -29
View File
@@ -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(); }
-13
View File
@@ -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
+17 -42
View File
@@ -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
+25 -6
View File
@@ -109,9 +109,6 @@
#define USE_EZSP_PROXY_TAP
#define USE_FAN
#define USE_FONT
#define USE_FEEDBACK_COVER_ENDSTOP
#define USE_FEEDBACK_COVER_MOVEMENT
#define USE_FEEDBACK_COVER_OBSTACLE
#define USE_FEEDBACK_COVER_OVERSHOOT
#define USE_GPIO_BINARY_SENSOR_INTERRUPT
#define USE_GPIO_SWITCH_INTERLOCK
@@ -154,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
@@ -761,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
@@ -784,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
@@ -829,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
@@ -859,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
@@ -913,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
+93 -40
View File
@@ -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
@@ -1,13 +0,0 @@
# No endstop, movement or obstacle sensors, so their code is compiled out
cover:
- platform: feedback
id: feedback_minimal
name: Minimal Feedback Cover
open_action:
- logger.log: open
open_duration: 10s
close_action:
- logger.log: close
close_duration: 10s
stop_action:
- logger.log: stop
@@ -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
View File
@@ -17,7 +17,6 @@ void setup() {
App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1);
auto *log = new logger::Logger(115200); // NOLINT
log->pre_setup();
log->set_uart_selection(logger::UART_SELECTION_UART0);
App.register_component_(log);
auto *wifi = new wifi::WiFiComponent(); // NOLINT
@@ -1,69 +0,0 @@
esphome:
name: feedback-loop-wake
host:
api:
batch_delay: 0ms
logger:
level: DEBUG
# Stand in for a motor started outside ESPHome
switch:
- platform: template
name: "External Open"
id: external_open
optimistic: true
- platform: template
name: "External Close"
id: external_close
optimistic: true
binary_sensor:
- platform: template
id: open_moving
internal: true
condition:
switch.is_on: external_open
- platform: template
id: close_moving
internal: true
condition:
switch.is_on: external_close
cover:
- platform: feedback
name: "Time Cover"
open_action:
- logger.log: "time_cover open"
close_action:
- logger.log: "time_cover close"
stop_action:
- logger.log: "time_cover stop"
open_duration: 2s
close_duration: 2s
- platform: feedback
name: "Max Duration Cover"
open_action:
- logger.log: "max_cover open"
close_action:
- logger.log: "max_cover close"
stop_action:
- logger.log: "max_cover stop"
open_duration: 3s
close_duration: 3s
max_duration: 1s
- platform: feedback
name: "Sensor Cover"
open_action:
- logger.log: "sensor_cover open"
close_action:
- logger.log: "sensor_cover close"
stop_action:
- logger.log: "sensor_cover stop"
open_duration: 4s
close_duration: 4s
open_sensor: open_moving
close_sensor: close_moving
update_interval: 200ms
@@ -1,100 +0,0 @@
"""Integration test that every way a feedback cover starts moving wakes its idle loop."""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable
from aioesphomeapi import CoverInfo, CoverOperation, CoverState, EntityState, SwitchInfo
import pytest
from .log_utils import LineWaiter
from .state_utils import StateWaiter, require_entity, subscribe_after_initial_states
from .types import APIClientConnectedFactory, RunCompiledFunction
def _expect_idle(waiter: StateWaiter, cover: CoverInfo) -> Awaitable[EntityState]:
return waiter.expect(
lambda s: (
isinstance(s, CoverState)
and s.key == cover.key
and s.current_operation == CoverOperation.IDLE
),
label=f"{cover.name} IDLE",
)
@pytest.mark.asyncio
async def test_feedback_cover_loop_wake(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that target position, max_duration and movement sensors still work with the loop idled."""
lines = LineWaiter()
max_stopped = lines.expect("Max duration reached")
async with (
run_compiled(yaml_config, line_callback=lines.callback),
api_client_connected() as client,
):
sensor_states: list[CoverState] = []
entities, _ = await client.list_entities_services()
time_cover = require_entity(entities, "time_cover", CoverInfo)
max_cover = require_entity(entities, "max_duration_cover", CoverInfo)
sensor_cover = require_entity(entities, "sensor_cover", CoverInfo)
def on_state(state: EntityState) -> None:
if isinstance(state, CoverState) and state.key == sensor_cover.key:
sensor_states.append(state)
sub = await subscribe_after_initial_states(client, entities, on_state=on_state)
waiter = sub.waiter
# Positions are restored from the previous run, so pick targets away from them
start = sub.initial_states[time_cover.key].position
target = 0.2 if start >= 0.5 else 0.8
# Only loop() stops a time-only cover at its target
done = _expect_idle(waiter, time_cover)
client.cover_command(key=time_cover.key, position=target)
state = await done
assert state.position == pytest.approx(target, abs=0.05)
# Stop mid move, then a new command must move it again and stop at the target
stopped = _expect_idle(waiter, time_cover)
client.cover_command(key=time_cover.key, position=start)
await asyncio.sleep(0.3)
client.cover_command(key=time_cover.key, stop=True)
mid = (await stopped).position
assert mid != pytest.approx(target, abs=0.05)
done = _expect_idle(waiter, time_cover)
client.cover_command(key=time_cover.key, position=target)
state = await done
assert state.position == pytest.approx(target, abs=0.05)
# 1s max_duration against a 3s travel
max_start = sub.initial_states[max_cover.key].position
max_target = 0.0 if max_start >= 0.5 else 1.0
done = _expect_idle(waiter, max_cover)
client.cover_command(key=max_cover.key, position=max_target)
state = await done
await asyncio.wait_for(max_stopped, 1.0)
assert abs(state.position - max_start) == pytest.approx(1 / 3, abs=0.1)
# Moved by the movement sensor alone, no command
sensor_start = sub.initial_states[sensor_cover.key].position
switch_name = "external_close" if sensor_start >= 0.5 else "external_open"
switch = require_entity(entities, switch_name, SwitchInfo)
sensor_states.clear()
client.switch_command(switch.key, True)
await asyncio.sleep(1.2)
settled = _expect_idle(waiter, sensor_cover)
client.switch_command(switch.key, False)
await settled
moving = {
round(s.position, 3)
for s in sensor_states
if s.current_operation != CoverOperation.IDLE
}
# One state from the sensor callback, the rest are loop() updates every 200ms
assert len(moving) >= 4, sensor_states