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