Skip to content

Commit 9313e9f

Browse files
mairasclaude
andcommitted
fix(log): strip ANSI escape codes from buffered web log lines
When CONFIG_LOG_COLORS is enabled (the ESP-IDF default), ESP_LOG wraps each line in ANSI color sequences. LogBuffer captured these raw, and the ESC byte (0x1b) is a control character ArduinoJson does not escape -- so /api/log returned invalid JSON and the web log viewer failed to parse every poll, showing a permanent "Reconnecting...". The precompiled Arduino framework ships with colors off, which is why this only surfaced on espidf builds. Strip ANSI sequences in push_line(), which stores the buffered copy. The console output is forwarded to the previous vprintf handler before push_line() runs, so serial/USB log colors are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c571b66 commit 9313e9f

1 file changed

Lines changed: 43 additions & 3 deletions

File tree

src/sensesp/system/log_buffer.cpp

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,39 @@ int LogBuffer::vprintf_trampoline(const char* format, va_list args) {
6969
return ret;
7070
}
7171

72+
namespace {
73+
// Copy text with ANSI/VT100 escape sequences removed. ESP_LOG wraps each line
74+
// in color codes when CONFIG_LOG_COLORS is enabled; the raw ESC byte (0x1b) is
75+
// a control character that ArduinoJson does not escape, so it would otherwise
76+
// reach the /api/log response unescaped and make the JSON unparseable in the
77+
// browser. A CSI sequence is ESC '[' then parameter/intermediate bytes up to a
78+
// final byte in 0x40-0x7e; a lone ESC is simply dropped.
79+
std::string strip_ansi(const char* text, size_t length) {
80+
std::string out;
81+
out.reserve(length);
82+
size_t i = 0;
83+
while (i < length) {
84+
if (text[i] == '\x1b') {
85+
i++; // drop ESC
86+
if (i < length && text[i] == '[') {
87+
i++; // drop '['
88+
while (i < length) {
89+
unsigned char c = static_cast<unsigned char>(text[i]);
90+
i++;
91+
if (c >= 0x40 && c <= 0x7e) {
92+
break; // final byte, end of sequence
93+
}
94+
}
95+
}
96+
continue;
97+
}
98+
out.push_back(text[i]);
99+
i++;
100+
}
101+
return out;
102+
}
103+
} // namespace
104+
72105
void LogBuffer::push_line(const char* text, size_t length, uint32_t now_ms) {
73106
// Strip trailing CR/LF so each record is one display line.
74107
while (length > 0 && (text[length - 1] == '\n' || text[length - 1] == '\r')) {
@@ -77,13 +110,20 @@ void LogBuffer::push_line(const char* text, size_t length, uint32_t now_ms) {
77110
if (length == 0) {
78111
return;
79112
}
80-
if (length > max_line_length_) {
81-
length = max_line_length_;
113+
114+
// Remove ANSI color escapes so the buffered copy served over /api/log is
115+
// plain, JSON-safe text. The console output above keeps its colors.
116+
std::string line = strip_ansi(text, length);
117+
if (line.empty()) {
118+
return;
119+
}
120+
if (line.size() > max_line_length_) {
121+
line.resize(max_line_length_);
82122
}
83123

84124
xSemaphoreTake(mutex_, portMAX_DELAY);
85125
prune_locked(now_ms);
86-
records_.push_back({next_seq_, now_ms, std::string(text, length)});
126+
records_.push_back({next_seq_, now_ms, std::move(line)});
87127
next_seq_++;
88128
// Enforce the count cap after inserting the new record.
89129
while (records_.size() > max_lines_) {

0 commit comments

Comments
 (0)