From e2bcbed66fd7545050d252ca6a6df2fdf68c1beb Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 4 Nov 2026 11:02:25 -0600 Subject: [PATCH 1/4] feat(logger): make the tag's mutability a compile-time option (off on ESP-IDF) Changing a Logger's tag after construction needs a mutex, and on ESP-IDF every std::mutex allocates its FreeRTOS semaphore: about 116 B of internal RAM for every Logger, which every espp component has. Almost nothing changes the tag after construction, so: - New Kconfig option CONFIG_ESPP_LOGGER_MUTABLE_TAG (default n). With it off the Logger has no mutex, and Logger::set_tag() / BaseComponent::set_log_tag() fail to compile with a message naming the option, so existing users can turn it on and keep building. - Host / python builds keep the mutable tag (the python bindings expose set_log_tag()); define ESPP_LOGGER_MUTABLE_TAG to 0 / 1 to choose. - RtspSession and PersistentData set their tags in the constructor instead of with set_tag() / set_log_tag(). M5Stack Tab5 example: about 2.9 KB more internal RAM free after initialize_audio(). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- .../base_component/include/base_component.hpp | 16 +++++ components/logger/Kconfig | 12 ++++ components/logger/include/logger.hpp | 65 ++++++++++++++++--- .../include/persistent_data.hpp | 3 +- components/rtsp/include/rtsp_session.hpp | 7 +- components/rtsp/src/rtsp_session.cpp | 11 ++-- 6 files changed, 97 insertions(+), 17 deletions(-) diff --git a/components/base_component/include/base_component.hpp b/components/base_component/include/base_component.hpp index 2d06579652..f410af122c 100644 --- a/components/base_component/include/base_component.hpp +++ b/components/base_component/include/base_component.hpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include "logger.hpp" @@ -15,9 +17,23 @@ class BaseComponent { /// \note This is the tag of the logger const std::string &get_name() const { return logger_.get_tag(); } +#if ESPP_LOGGER_MUTABLE_TAG /// Set the tag for the logger /// \param tag The tag to use for the logger + /// \note Only with ESPP_LOGGER_MUTABLE_TAG (on ESP-IDF the + /// CONFIG_ESPP_LOGGER_MUTABLE_TAG Kconfig option); otherwise pass + /// the tag to the constructor. void set_log_tag(const std::string_view &tag) { logger_.set_tag(tag); } +#else + /// Not available: the tag is fixed at construction (pass it to the + /// constructor). Enable CONFIG_ESPP_LOGGER_MUTABLE_TAG (menuconfig: ESPP + /// Logger -> Mutable tag) to change it afterwards. + template void set_log_tag(const std::string_view &) { + static_assert(!std::is_same_v, + "BaseComponent::set_log_tag() needs CONFIG_ESPP_LOGGER_MUTABLE_TAG " + "(menuconfig: ESPP Logger -> Mutable tag); or pass the tag to the constructor"); + } +#endif /// Get the log level for the logger /// \return The verbosity level of the logger diff --git a/components/logger/Kconfig b/components/logger/Kconfig index f0260ebd62..42f8fde3a6 100644 --- a/components/logger/Kconfig +++ b/components/logger/Kconfig @@ -22,4 +22,16 @@ menu "ESPP Logger" Enable cursor commands for the ESPP Logger. This will allow the user to move the cursor around the terminal and clear the screen. + config ESPP_LOGGER_MUTABLE_TAG + bool "Mutable tag (Logger::set_tag(), BaseComponent::set_log_tag())" + default n + help + Allow a Logger's tag to change after construction, with + Logger::set_tag() / BaseComponent::set_log_tag(). The tag then needs + a mutex, and on ESP-IDF each mutex allocates a FreeRTOS semaphore: + about 116 bytes of internal RAM for every Logger (every espp + component has one). Off, the tag is set once, in the Logger's + Config (or BaseComponent's constructor), and set_tag() / + set_log_tag() do not compile. + endmenu diff --git a/components/logger/include/logger.hpp b/components/logger/include/logger.hpp index 813e249dc1..733e23aada 100644 --- a/components/logger/include/logger.hpp +++ b/components/logger/include/logger.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(ESP_PLATFORM) // include sdkconfig before the rest @@ -71,6 +72,20 @@ class Logger { // If the cursor commands haven't been configured as a define, check the config #if !defined(ESPP_LOGGER_CURSOR_COMMANDS_ENABLED) +// Whether a Logger's tag can change after construction (set_tag()). Changing +// it needs a mutex, which on ESP-IDF allocates a FreeRTOS semaphore (about +// 116 B of internal RAM) for every Logger, so there it is the +// CONFIG_ESPP_LOGGER_MUTABLE_TAG Kconfig option, off by default. Elsewhere +// (host / python builds) it defaults on; define ESPP_LOGGER_MUTABLE_TAG to 0 or +// 1 to choose. +#ifndef ESPP_LOGGER_MUTABLE_TAG +#if defined(CONFIG_ESPP_LOGGER_MUTABLE_TAG) || !defined(ESP_PLATFORM) +#define ESPP_LOGGER_MUTABLE_TAG 1 +#else +#define ESPP_LOGGER_MUTABLE_TAG 0 +#endif +#endif + #if !defined(CONFIG_ESPP_LOGGER_ENABLE_CURSOR_COMMANDS) #define ESPP_LOGGER_CURSOR_COMMANDS_ENABLED 0 #else // CONFIG_ESPP_LOGGER_ENABLE_CURSOR_COMMANDS @@ -134,7 +149,7 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit */ Logger(Logger &&other) noexcept // cppcheck-suppress missingMemberCopy : tag_([&other] { - std::scoped_lock lock(other.tag_mutex_); + [[maybe_unused]] auto lock = other.lock_tag(); return std::move(other.tag_); }()) , rate_limit_(other.rate_limit_) @@ -151,7 +166,9 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit */ Logger &operator=(const Logger &other) { // cppcheck-suppress operatorEqVarError if (this != &other) { +#if ESPP_LOGGER_MUTABLE_TAG std::scoped_lock lock(tag_mutex_, other.tag_mutex_); +#endif tag_ = other.tag_; rate_limit_ = other.rate_limit_; include_time_ = other.include_time_.load(); @@ -167,7 +184,9 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit */ Logger &operator=(Logger &&other) noexcept { if (this != &other) { +#if ESPP_LOGGER_MUTABLE_TAG std::scoped_lock lock(tag_mutex_, other.tag_mutex_); +#endif tag_ = std::move(other.tag_); rate_limit_ = other.rate_limit_; last_print_.store(other.last_print_.load()); @@ -191,14 +210,30 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit */ void set_verbosity(const espp::Logger::Verbosity level) { level_ = level; } +#if ESPP_LOGGER_MUTABLE_TAG /** * @brief Change the tag for the logger. * @param tag The new tag. + * @note Only with ESPP_LOGGER_MUTABLE_TAG (on ESP-IDF the + * CONFIG_ESPP_LOGGER_MUTABLE_TAG Kconfig option); otherwise set the + * tag in the Config. */ void set_tag(const std::string_view tag) { std::scoped_lock lock(tag_mutex_); tag_ = tag; } +#else + /** + * @brief Not available: the tag is fixed at construction (set it in the + * Config). Enable CONFIG_ESPP_LOGGER_MUTABLE_TAG (menuconfig: ESPP + * Logger -> Mutable tag) to change it afterwards. + */ + template void set_tag(const std::string_view) { + static_assert(!std::is_same_v, + "Logger::set_tag() needs CONFIG_ESPP_LOGGER_MUTABLE_TAG (menuconfig: ESPP " + "Logger -> Mutable tag); or set the tag in the Logger's Config"); + } +#endif /** * @brief Get the current tag for the logger. @@ -255,10 +290,10 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit return; auto msg = format(rt_fmt_str, std::forward(args)...); if (include_time_) { - std::scoped_lock lock(tag_mutex_); + [[maybe_unused]] auto lock = lock_tag(); fmt::print(fg(fmt::color::gray), "[{}/D][{}]: {}\n", tag_, get_time(), msg); } else { - std::scoped_lock lock(tag_mutex_); + [[maybe_unused]] auto lock = lock_tag(); fmt::print(fg(fmt::color::gray), "[{}/D]:{}\n", tag_, msg); } #endif @@ -275,10 +310,10 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit return; auto msg = format(rt_fmt_str, std::forward(args)...); if (include_time_) { - std::scoped_lock lock(tag_mutex_); + [[maybe_unused]] auto lock = lock_tag(); fmt::print(fg(fmt::terminal_color::green), "[{}/I][{}]: {}\n", tag_, get_time(), msg); } else { - std::scoped_lock lock(tag_mutex_); + [[maybe_unused]] auto lock = lock_tag(); fmt::print(fg(fmt::terminal_color::green), "[{}/I]:{}\n", tag_, msg); } #endif @@ -295,10 +330,10 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit return; auto msg = format(rt_fmt_str, std::forward(args)...); if (include_time_) { - std::scoped_lock lock(tag_mutex_); + [[maybe_unused]] auto lock = lock_tag(); fmt::print(fg(fmt::terminal_color::yellow), "[{}/W][{}]: {}\n", tag_, get_time(), msg); } else { - std::scoped_lock lock(tag_mutex_); + [[maybe_unused]] auto lock = lock_tag(); fmt::print(fg(fmt::terminal_color::yellow), "[{}/W]:{}\n", tag_, msg); } #endif @@ -315,10 +350,10 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit return; auto msg = format(rt_fmt_str, std::forward(args)...); if (include_time_) { - std::scoped_lock lock(tag_mutex_); + [[maybe_unused]] auto lock = lock_tag(); fmt::print(fg(fmt::terminal_color::red), "[{}/E][{}]: {}\n", tag_, get_time(), msg); } else { - std::scoped_lock lock(tag_mutex_); + [[maybe_unused]] auto lock = lock_tag(); fmt::print(fg(fmt::terminal_color::red), "[{}/E]:{}\n", tag_, msg); } #endif @@ -492,8 +527,18 @@ rate limit. @note Only calls that have _rate_limited suffixed will be rate limit */ static std::chrono::steady_clock::time_point start_time_; +#if ESPP_LOGGER_MUTABLE_TAG mutable std::mutex tag_mutex_; ///< Mutex for the tag. - std::string tag_; ///< Name of the logger to be prepended to all logs. + /// Lock the tag against set_tag() while it is read. + [[nodiscard]] std::unique_lock lock_tag() const { + return std::unique_lock(tag_mutex_); + } +#else + /// The tag is fixed at construction: nothing to lock. + struct NoTagLock {}; + [[nodiscard]] NoTagLock lock_tag() const { return {}; } +#endif + std::string tag_; ///< Name of the logger to be prepended to all logs. clock_t::duration rate_limit_{ clock_t::duration::zero()}; ///< Rate limit for the logger. If zero, no rate limiting is done. std::atomic last_print_{ diff --git a/components/persistent_data/include/persistent_data.hpp b/components/persistent_data/include/persistent_data.hpp index 84aefa9c30..cc0e7f3907 100644 --- a/components/persistent_data/include/persistent_data.hpp +++ b/components/persistent_data/include/persistent_data.hpp @@ -90,11 +90,10 @@ template class PersistentData : pub * @param config The configuration. */ explicit PersistentData(const Config &config) - : BaseComponent("PersistentData", config.log_level) + : BaseComponent("PersistentData:" + config.file_path.filename().string(), config.log_level) , file_path_(config.file_path) , save_after_migration_(config.save_after_migration) , max_file_size_(config.max_file_size) { - set_log_tag("PersistentData:" + file_path_.filename().string()); if (config.auto_load) { std::error_code ec; load(ec); diff --git a/components/rtsp/include/rtsp_session.hpp b/components/rtsp/include/rtsp_session.hpp index 2e0e3a9308..b6b679916c 100644 --- a/components/rtsp/include/rtsp_session.hpp +++ b/components/rtsp/include/rtsp_session.hpp @@ -145,6 +145,11 @@ class RtspSession : public BaseComponent { bool send_rtcp_packet(const espp::RtcpPacket &packet); protected: + /// Construct with a given session id (it is part of the logger tag, which + /// is set before the members are) + RtspSession(std::shared_ptr control_socket, + const espp::RtspSession::Config &config, uint32_t session_id); + /// Send a response to a RTSP request /// @param code The response code /// @param message The response message @@ -216,7 +221,7 @@ class RtspSession : public BaseComponent { /// Session IDs are generated randomly when a client sends a SETUP request and are /// used to identify the client in subsequent requests when managing the RTP session. /// @return The new session id - uint32_t generate_session_id(); + static uint32_t generate_session_id(); /// Parse the RTSP command sequence number from a request /// @param request The request to parse diff --git a/components/rtsp/src/rtsp_session.cpp b/components/rtsp/src/rtsp_session.cpp index 705cf2a2a2..57641811f6 100644 --- a/components/rtsp/src/rtsp_session.cpp +++ b/components/rtsp/src/rtsp_session.cpp @@ -38,15 +38,18 @@ bool parse_decimal_int(std::string_view text, int &value) { } // namespace RtspSession::RtspSession(std::shared_ptr control_socket, const Config &config) - : BaseComponent("RtspSession", config.log_level) + : RtspSession(std::move(control_socket), config, generate_session_id()) {} + +RtspSession::RtspSession(std::shared_ptr control_socket, const Config &config, + uint32_t session_id) + // the logger tag includes the session id + : BaseComponent("RtspSession " + std::to_string(session_id), config.log_level) , control_socket_(std::move(control_socket)) - , session_id_(generate_session_id()) + , session_id_(session_id) , server_address_(config.server_address) , rtsp_path_(config.rtsp_path) , client_address_(control_socket_->get_remote_info().address) , sdp_generator_(config.sdp_generator) { - // set the logger tag to include the session id - logger_.set_tag("RtspSession " + std::to_string(session_id_)); // ensure there is a timeout on the control socket receive control_socket_->set_receive_timeout(config.receive_timeout); // start the session task to handle RTSP commands From 24a1047d9b629c4147dd3e17c15520a994c6441b Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 4 Nov 2026 14:45:04 -0600 Subject: [PATCH 2/4] fix: example builds with the immutable Logger tag - iperf_menu example: add base_component to EXTRA_COMPONENT_DIRS. Without it the component manager pulls the published espp/base_component, whose set_log_tag() calls Logger::set_tag(), which the local logger now rejects when CONFIG_ESPP_LOGGER_MUTABLE_TAG is off. - motorgo-mini: default_motor{1,2}_config copied motor{1,2}_driver_ and encoder{1,2}_, members declared (so constructed) after it - copying shared_ptrs that do not exist yet. init_motor_channel_{1,2}() always set both, so the defaults now hold nullptr. GCC 15 reports it (-Werror=uninitialized) since the Logger change alters inlining of the MotorGoMini constructor, failing the bldc_motor, bldc_haptics and motorgo-mini examples. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- components/iperf_menu/example/CMakeLists.txt | 1 + components/motorgo-mini/include/motorgo-mini.hpp | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/components/iperf_menu/example/CMakeLists.txt b/components/iperf_menu/example/CMakeLists.txt index e7f36bbc39..51553d4846 100644 --- a/components/iperf_menu/example/CMakeLists.txt +++ b/components/iperf_menu/example/CMakeLists.txt @@ -6,6 +6,7 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) # add the component directories that we want to use set(EXTRA_COMPONENT_DIRS + "../../../components/base_component" "../../../components/cli" "../../../components/format" "../../../components/iperf_menu" diff --git a/components/motorgo-mini/include/motorgo-mini.hpp b/components/motorgo-mini/include/motorgo-mini.hpp index 88373a7870..8394f759a0 100644 --- a/components/motorgo-mini/include/motorgo-mini.hpp +++ b/components/motorgo-mini/include/motorgo-mini.hpp @@ -194,8 +194,11 @@ class MotorGoMini : public BaseComponent { .kv_rating = 320, .current_limit = 1.0f, .foc_type = espp::detail::FocType::SPACE_VECTOR_PWM, - .driver = motor1_driver_, // NOTE: user cannot override this - .sensor = encoder1_, // NOTE: user cannot override this + // set by init_motor_channel_1() (the user cannot override them); not + // copied from motor1_driver_ / encoder1_ here, which are declared (so + // constructed) after this member + .driver = nullptr, + .sensor = nullptr, .velocity_pid_config = { .kp = 0.010f, @@ -227,8 +230,11 @@ class MotorGoMini : public BaseComponent { .kv_rating = 320, .current_limit = 1.0f, .foc_type = espp::detail::FocType::SPACE_VECTOR_PWM, - .driver = motor2_driver_, // NOTE: user cannot override this - .sensor = encoder2_, // NOTE: user cannot override this + // set by init_motor_channel_2() (the user cannot override them); not + // copied from motor2_driver_ / encoder2_ here, which are declared (so + // constructed) after this member + .driver = nullptr, + .sensor = nullptr, .velocity_pid_config = { .kp = 0.010f, From 7b2003e0d72ee91ad41aba5860fcaf9485d50dc4 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 4 Nov 2026 15:34:44 -0600 Subject: [PATCH 3/4] fix(logger): define ESPP_LOGGER_MUTABLE_TAG outside the cursor-commands guard It sat inside #if !defined(ESPP_LOGGER_CURSOR_COMMANDS_ENABLED), so a build that defines ESPP_LOGGER_CURSOR_COMMANDS_ENABLED itself left it undefined - which the #if checks read as 0, silently making the tag immutable. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- components/logger/include/logger.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/logger/include/logger.hpp b/components/logger/include/logger.hpp index 733e23aada..0be92be8b6 100644 --- a/components/logger/include/logger.hpp +++ b/components/logger/include/logger.hpp @@ -70,8 +70,6 @@ class Logger { #define ESPP_LOGGER_WARN_ENABLED (CONFIG_ESPP_LOGGER_LOG_LEVEL >= ESPP_LOGGER_LOG_LEVEL_WARN) #define ESPP_LOGGER_ERROR_ENABLED (CONFIG_ESPP_LOGGER_LOG_LEVEL >= ESPP_LOGGER_LOG_LEVEL_ERROR) -// If the cursor commands haven't been configured as a define, check the config -#if !defined(ESPP_LOGGER_CURSOR_COMMANDS_ENABLED) // Whether a Logger's tag can change after construction (set_tag()). Changing // it needs a mutex, which on ESP-IDF allocates a FreeRTOS semaphore (about // 116 B of internal RAM) for every Logger, so there it is the @@ -86,6 +84,8 @@ class Logger { #endif #endif +// If the cursor commands haven't been configured as a define, check the config +#if !defined(ESPP_LOGGER_CURSOR_COMMANDS_ENABLED) #if !defined(CONFIG_ESPP_LOGGER_ENABLE_CURSOR_COMMANDS) #define ESPP_LOGGER_CURSOR_COMMANDS_ENABLED 0 #else // CONFIG_ESPP_LOGGER_ENABLE_CURSOR_COMMANDS From 2a70e537291f207876d74e9d2668b452235671a6 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 4 Nov 2026 17:00:18 -0600 Subject: [PATCH 4/4] fix(logger): check CONFIG_ESPP_LOGGER_MUTABLE_TAG's value, not just that it is defined Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- components/logger/include/logger.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/logger/include/logger.hpp b/components/logger/include/logger.hpp index 0be92be8b6..4b732d16e8 100644 --- a/components/logger/include/logger.hpp +++ b/components/logger/include/logger.hpp @@ -77,7 +77,8 @@ class Logger { // (host / python builds) it defaults on; define ESPP_LOGGER_MUTABLE_TAG to 0 or // 1 to choose. #ifndef ESPP_LOGGER_MUTABLE_TAG -#if defined(CONFIG_ESPP_LOGGER_MUTABLE_TAG) || !defined(ESP_PLATFORM) +#if (defined(CONFIG_ESPP_LOGGER_MUTABLE_TAG) && CONFIG_ESPP_LOGGER_MUTABLE_TAG) || \ + !defined(ESP_PLATFORM) #define ESPP_LOGGER_MUTABLE_TAG 1 #else #define ESPP_LOGGER_MUTABLE_TAG 0