From 298bd5ec7219f339f53808eed2f5dac5a1a5a2f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 16 Oct 2026 15:55:28 -1000 Subject: [PATCH] [core] Add ESP_LOGV_HEX, ESP_LOGVV_HEX and ESP_LOGD_HEX (#20931) --- esphome/core/log.h | 32 ++++++++++++ tests/components/core/test_log_hex.cpp | 67 ++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 tests/components/core/test_log_hex.cpp diff --git a/esphome/core/log.h b/esphome/core/log.h index 840da292aee..d7a0ff6b679 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -160,6 +160,38 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #define ESP_LOGV(tag, ...) esph_log_v(tag, __VA_ARGS__) #define ESP_LOGVV(tag, ...) esph_log_vv(tag, __VA_ARGS__) +// Log \p format with up to \p max_bytes of \p data as colon separated hex appended as its last %s argument. +// The stack buffer exists only when the level is compiled in. Needs esphome/core/helpers.h. +#define ESPHOME_LOG_HEX_(log_macro, tag, data, len, max_bytes, format, ...) \ + do { \ + char esph_log_hex_buf_[::esphome::format_hex_pretty_size(max_bytes)]; \ + log_macro(tag, format, __VA_ARGS__ __VA_OPT__(, )::esphome::format_hex_pretty_to(esph_log_hex_buf_, data, len)); \ + } while (0) +#ifdef ESPHOME_LOG_HAS_DEBUG +#define ESP_LOGD_HEX(tag, data, len, max_bytes, format, ...) \ + ESPHOME_LOG_HEX_(ESP_LOGD, tag, data, len, max_bytes, format, __VA_ARGS__) +#else +#define ESP_LOGD_HEX(tag, data, len, max_bytes, format, ...) \ + do { \ + } while (0) +#endif +#ifdef ESPHOME_LOG_HAS_VERBOSE +#define ESP_LOGV_HEX(tag, data, len, max_bytes, format, ...) \ + ESPHOME_LOG_HEX_(ESP_LOGV, tag, data, len, max_bytes, format, __VA_ARGS__) +#else +#define ESP_LOGV_HEX(tag, data, len, max_bytes, format, ...) \ + do { \ + } while (0) +#endif +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define ESP_LOGVV_HEX(tag, data, len, max_bytes, format, ...) \ + ESPHOME_LOG_HEX_(ESP_LOGVV, tag, data, len, max_bytes, format, __VA_ARGS__) +#else +#define ESP_LOGVV_HEX(tag, data, len, max_bytes, format, ...) \ + do { \ + } while (0) +#endif + #define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" #define BYTE_TO_BINARY(byte) \ ((byte) &0x80 ? '1' : '0'), ((byte) &0x40 ? '1' : '0'), ((byte) &0x20 ? '1' : '0'), ((byte) &0x10 ? '1' : '0'), \ diff --git a/tests/components/core/test_log_hex.cpp b/tests/components/core/test_log_hex.cpp new file mode 100644 index 00000000000..70de9af0828 --- /dev/null +++ b/tests/components/core/test_log_hex.cpp @@ -0,0 +1,67 @@ +#include + +#include + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::testing { + +static const char *const TAG = "log_hex_test"; + +TEST(LogHexTest, EvaluatesArgumentsOnlyWhenTheLevelIsCompiledIn) { + static constexpr uint8_t DATA[] = {0x01, 0xAB, 0xFF}; + int debug_calls = 0; + int verbose_calls = 0; + int very_verbose_calls = 0; + auto count = [](int &calls) { + calls++; + return sizeof(DATA); + }; + ESP_LOGD_HEX(TAG, DATA, count(debug_calls), sizeof(DATA), "Data: %s"); + ESP_LOGV_HEX(TAG, DATA, count(verbose_calls), sizeof(DATA), "Frame %u: %s", 7u); + ESP_LOGVV_HEX(TAG, DATA, count(very_verbose_calls), 2, "Type %02X %s: %s", 0x10, "truncated"); +#ifdef ESPHOME_LOG_HAS_DEBUG + EXPECT_EQ(debug_calls, 1); +#else + EXPECT_EQ(debug_calls, 0); +#endif +#ifdef ESPHOME_LOG_HAS_VERBOSE + EXPECT_EQ(verbose_calls, 1); +#else + EXPECT_EQ(verbose_calls, 0); +#endif +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + EXPECT_EQ(very_verbose_calls, 1); +#else + EXPECT_EQ(very_verbose_calls, 0); +#endif +} + +TEST(LogHexTest, IsASingleStatement) { + static constexpr uint8_t DATA[] = {0x12, 0x34}; + bool other_branch = false; + if (DATA[0] == 0x12) // NOLINT(readability-braces-around-statements) + ESP_LOGV_HEX(TAG, DATA, sizeof(DATA), sizeof(DATA), "Data: %s"); + else // NOLINT(readability-braces-around-statements) + other_branch = true; + EXPECT_FALSE(other_branch); +} + +// Stands in for a log macro so the expansion can be checked without the logger backend +static char captured[64]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#define CAPTURE_LOG(tag, ...) snprintf(captured, sizeof(captured), __VA_ARGS__) + +TEST(LogHexTest, HexTextIsTheLastArgument) { + static constexpr uint8_t DATA[] = {0x01, 0xAB, 0xFF}; + ESPHOME_LOG_HEX_(CAPTURE_LOG, TAG, DATA, sizeof(DATA), sizeof(DATA), "Data: %s"); + EXPECT_STREQ(captured, "Data: 01:AB:FF"); +} + +TEST(LogHexTest, ExtraArgumentsComeFirstAndLongDataIsCut) { + static constexpr uint8_t DATA[] = {0x01, 0xAB, 0xFF}; + ESPHOME_LOG_HEX_(CAPTURE_LOG, TAG, DATA, sizeof(DATA), 2, "Type %02X %s: %s", 0x10, "truncated"); + EXPECT_STREQ(captured, "Type 10 truncated: 01:AB"); +} + +} // namespace esphome::testing