ESPHome 2026.4.0-dev
Loading...
Searching...
No Matches
log_buffer.h
Go to the documentation of this file.
1#pragma once
2
3#include "esphome/core/hal.h"
5#include "esphome/core/log.h"
6
7namespace esphome::logger {
8
9// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
10static constexpr uint16_t MAX_HEADER_SIZE = 128;
11
12// ANSI color code last digit (30-38 range, store only last digit to save RAM on ESP8266)
13static const char LOG_LEVEL_COLOR_DIGIT[] PROGMEM = {
14 '\0', // NONE
15 '1', // ERROR (31 = red)
16 '3', // WARNING (33 = yellow)
17 '2', // INFO (32 = green)
18 '5', // CONFIG (35 = magenta)
19 '6', // DEBUG (36 = cyan)
20 '7', // VERBOSE (37 = gray)
21 '8', // VERY_VERBOSE (38 = white)
22};
23
24static const char LOG_LEVEL_LETTER_CHARS[] PROGMEM = {
25 '\0', // NONE
26 'E', // ERROR
27 'W', // WARNING
28 'I', // INFO
29 'C', // CONFIG
30 'D', // DEBUG
31 'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
32};
33
34// Buffer wrapper for log formatting functions
35struct LogBuffer {
36 char *data;
37 uint16_t size;
38 uint16_t pos{0};
39 // Replaces the null terminator with a newline for console output.
40 // Must be called after notify_listeners_() since listeners need null-terminated strings.
41 // Console output uses length-based writes (buf.pos), so null terminator is not needed.
43 if (this->pos < this->size) {
44 this->data[this->pos++] = '\n';
45 } else if (this->size > 0) {
46 // Buffer was full - replace last char with newline to ensure it's visible
47 this->data[this->size - 1] = '\n';
48 this->pos = this->size;
49 }
50 }
51 void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
52 // Early return if insufficient space - intentionally don't update pos to prevent partial writes
53 if (this->pos + MAX_HEADER_SIZE > this->size)
54 return;
55
56 char *p = this->current_();
57
58 // Write ANSI color
59 this->write_ansi_color_(p, level);
60
61 // Construct: [LEVEL][tag:line]
62 *p++ = '[';
63 if (level != 0) {
64 if (level >= 7) {
65 *p++ = 'V'; // VERY_VERBOSE = "VV"
66 *p++ = 'V';
67 } else {
68 *p++ = static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(&LOG_LEVEL_LETTER_CHARS[level])));
69 }
70 }
71 *p++ = ']';
72 *p++ = '[';
73
74 // Copy tag
75 this->copy_string_(p, tag);
76
77 *p++ = ':';
78
79 // Format line number using subtraction loops (no division - important for ESP8266 which lacks hardware divider)
80 if (line > 999) [[unlikely]] {
81 write_digit(p, line, 1000);
82 }
83 write_digit(p, line, 100);
84 write_digit(p, line, 10);
85 *p++ = '0' + line;
86 *p++ = ']';
87
88#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
89 // Write thread name with bold red color
90 if (thread_name != nullptr) {
91 this->write_ansi_color_(p, 1); // Bold red for thread name
92 *p++ = '[';
93 this->copy_string_(p, thread_name);
94 *p++ = ']';
95 this->write_ansi_color_(p, level); // Restore original color
96 }
97#endif
98
99 *p++ = ':';
100 *p++ = ' ';
101
102 this->pos = p - this->data;
103 }
104 void HOT format_body(const char *format, va_list args) {
105 this->format_vsnprintf_(format, args);
106 this->finalize_();
107 }
108#ifdef USE_STORE_LOG_STR_IN_FLASH
109 void HOT format_body_P(PGM_P format, va_list args) {
110 this->format_vsnprintf_P_(format, args);
111 this->finalize_();
112 }
113#endif
114 void write_body(const char *text, uint16_t text_length) {
115 const uint16_t available = this->remaining_();
116 const uint16_t copy_len = (text_length < available) ? text_length : available;
117 if (copy_len > 0) {
118 memcpy(this->current_(), text, copy_len);
119 this->pos += copy_len;
120 }
121 this->finalize_();
122 }
123
124 private:
125 bool full_() const { return this->pos >= this->size; }
126 uint16_t remaining_() const { return this->size - this->pos; }
127 char *current_() { return this->data + this->pos; }
128 void finalize_() {
129 this->write_ansi_reset_();
130 // Null terminate
131 this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
132 }
133 // Write ANSI reset sequence inline ("\033[0m") - avoids write_() call overhead
134 static constexpr uint16_t ANSI_RESET_LEN = 4; // "\033[0m"
135 void write_ansi_reset_() {
136 if (this->remaining_() >= ANSI_RESET_LEN) {
137 char *p = this->current_();
138 *p++ = '\033';
139 *p++ = '[';
140 *p++ = '0';
141 *p++ = 'm';
142 this->pos += ANSI_RESET_LEN;
143 }
144 }
145 void strip_trailing_newlines_() {
146 while (this->pos > 0 && this->data[this->pos - 1] == '\n')
147 this->pos--;
148 }
149 void process_vsnprintf_result_(int ret) {
150 if (ret < 0)
151 return;
152 const uint16_t rem = this->remaining_();
153 this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
154 this->strip_trailing_newlines_();
155 }
156 void format_vsnprintf_(const char *format, va_list args) {
157 if (this->full_())
158 return;
159 this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
160 }
161#ifdef USE_STORE_LOG_STR_IN_FLASH
162 void format_vsnprintf_P_(PGM_P format, va_list args) {
163 if (this->full_())
164 return;
165 this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
166 }
167#endif
168 // Extract one decimal digit via subtraction (no division - important for ESP8266)
169 static inline void ESPHOME_ALWAYS_INLINE write_digit(char *&p, int &value, int divisor) {
170 char d = '0';
171 while (value >= divisor) {
172 d++;
173 value -= divisor;
174 }
175 *p++ = d;
176 }
177 // Write ANSI color escape sequence to buffer, updates pointer in place
178 // Caller is responsible for ensuring buffer has sufficient space
179 void write_ansi_color_(char *&p, uint8_t level) {
180 if (level == 0)
181 return;
182 // Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
183 *p++ = '\033';
184 *p++ = '[';
185 *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
186 *p++ = ';';
187 *p++ = '3';
188 *p++ = static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(&LOG_LEVEL_COLOR_DIGIT[level])));
189 *p++ = 'm';
190 }
191 // Copy string without null terminator, updates pointer in place
192 // Caller is responsible for ensuring buffer has sufficient space
193 void copy_string_(char *&p, const char *str) {
194 const size_t len = strlen(str);
195 // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
196 // piece
197 memcpy(p, str, len);
198 p += len;
199 }
200};
201
202} // namespace esphome::logger
const char int line
Definition log.h:74
const char * tag
Definition log.h:74
const char int const __FlashStringHelper * format
Definition log.h:74
const char int const __FlashStringHelper va_list args
Definition log.h:74
std::string size_t len
Definition helpers.h:1045
uint8_t progmem_read_byte(const uint8_t *addr)
Definition core.cpp:34
void HOT format_body_P(PGM_P format, va_list args)
Definition log_buffer.h:109
void write_body(const char *text, uint16_t text_length)
Definition log_buffer.h:114
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name)
Definition log_buffer.h:51
void HOT format_body(const char *format, va_list args)
Definition log_buffer.h:104