Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
log_frame.h
Go to the documentation of this file.
1#pragma once
2
3/// @file log_frame.h
4/// @brief Shared frame logging helpers for IO-Homecontrol.
5/// @ingroup hioc_protocol
6
7#include "proto_frame.h"
8#include "redaction.h"
9
10#include "esphome/core/log.h"
11#include <cinttypes>
12#include <cstdarg>
13#include <cstddef>
14#include <cstdint>
15#include <cstdio>
16
17namespace esphome {
18namespace home_io_control {
19
20inline constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE = 220; ///< Fits a full 32-byte frame rendered as spaced hex text.
21inline constexpr size_t FRAME_LOG_FLAGS_BUFFER_SIZE = 32; ///< Buffer for decoded CTRL1 flag annotations.
22
23inline void bytes_to_hex(const uint8_t *data, uint8_t len, char *out, size_t out_size) {
24 size_t pos = 0;
25 if (out_size == 0)
26 return;
27 out[0] = '\0';
28 for (uint8_t i = 0; i < len && pos + 4 < out_size; i++)
29 pos += snprintf(out + pos, out_size - pos, "%02X ", data[i]);
30}
31
32/// @brief Render decoded CTRL1 flags into a short annotation string.
33/// @param ctrl1 The CTRL1 byte from a frame.
34/// @param out Buffer to write into (at least FRAME_LOG_FLAGS_BUFFER_SIZE bytes).
35/// @param out_size Size of the output buffer.
36inline void decode_ctrl1_flags(uint8_t ctrl1, char *out, size_t out_size) {
37 size_t pos = 0;
38 if (out_size == 0)
39 return;
40 out[0] = '\0';
41 if ((ctrl1 & CTRL1_ACK) != 0)
42 pos += snprintf(out + pos, out_size - pos, "[ACK]");
43 if ((ctrl1 & CTRL1_LOW_POWER) != 0)
44 pos += snprintf(out + pos, out_size - pos, "[LPM]");
45 if ((ctrl1 & CTRL1_PRIORITY) != 0)
46 pos += snprintf(out + pos, out_size - pos, "[PRIO]");
47 if ((ctrl1 & CTRL1_ROUTED) != 0)
48 pos += snprintf(out + pos, out_size - pos, "[R]");
49 if ((ctrl1 & CTRL1_BEACON) != 0)
50 snprintf(out + pos, out_size - pos, "[B]");
51}
52
53/// @brief Render a frame's bytes as spaced hex text, masking the payload when the command
54/// carries key material (see command_carries_key_material()).
55///
56/// Kept outside the IOHOME_FRAME_LOG guard so the redaction behavior is unit-testable
57/// independent of the firmware-only logging build flag.
58///
59/// @warning `IOHOME_UNSAFE_LOG_KEY_MATERIAL` disables the masking below entirely. It exists
60/// solely so a maintainer can capture a genuine pairing exchange for `tests/corpus/` (see
61/// `tests/corpus/README.md`'s key-hygiene section) when the normal `ingest.py --rekey` flow has
62/// no other way to obtain the raw bytes. Never set it outside a deliberate, local,
63/// own-hardware capture session: build with it, capture the one exchange you need, `--rekey`
64/// immediately, then rebuild without it. Never commit a config with it enabled, never use it on
65/// a device whose logs anyone else can see, and never paste output captured under it into an
66/// issue/chat before re-keying. hub_core.cpp's setup() logs a loud warning every boot while this
67/// is defined specifically so an accidentally-left-on build can't stay quiet.
68/// @param data Raw serialized frame bytes (header + payload, no CRC).
69/// @param len Total length of data.
70/// @param out Buffer to write the rendered text into.
71/// @param out_size Size of out.
72inline void render_frame_hex_redacted(const uint8_t *data, uint8_t len, char *out, size_t out_size) {
73 if (out_size == 0)
74 return;
75#ifndef IOHOME_UNSAFE_LOG_KEY_MATERIAL
76 if (len >= FRAME_MIN_SIZE && command_carries_key_material(data[FRAME_CMD_OFFSET])) {
77 // Sized for FRAME_MIN_SIZE bytes of "XX " hex (not the general FRAME_LOG_HEX_BUFFER_SIZE) so
78 // GCC's -Wformat-truncation can prove the snprintf() below never truncates into `out`.
79 // +4 rather than +1: bytes_to_hex()'s loop guard (`pos + 4 < out_size`) needs headroom beyond
80 // the last byte's own 3 chars, not just space for the content + null terminator.
81 char header_hex[(FRAME_MIN_SIZE * 3) + 4];
82 bytes_to_hex(data, FRAME_MIN_SIZE, header_hex, sizeof(header_hex));
83 snprintf(out, out_size, "%s[%u bytes masked]", header_hex, static_cast<unsigned>(len - FRAME_MIN_SIZE));
84 return;
85 }
86#endif
87 bytes_to_hex(data, len, out, out_size);
88}
89
90// === Held frame-log lines ===
91
92/// @brief Log level of one frame-log line, kept with the line while it is held.
93enum class FrameLogLevel : uint8_t { WARN, INFO, DEBUG };
94
95/// Lines one hold can keep. A held window covers one received frame and the answer to it: the
96/// driver's reception lines (at most four, on a failed decode) plus the answer's TX line and the
97/// driver's re-arm line.
98inline constexpr size_t FRAME_LOG_HELD_LINES = 6;
99/// The overflow note flush() prints when lines did not fit.
100inline constexpr size_t FRAME_LOG_NOTE_SIZE = 64;
101/// One rendered frame-log line: the hex dump plus its prefix, frequency, command and flags.
102inline constexpr size_t FRAME_LOG_LINE_SIZE = FRAME_LOG_HEX_BUFFER_SIZE + 96;
103
104/// @brief Keeps frame-log lines back while the hub answers a device, and prints them afterwards.
105///
106/// Printing a log line takes milliseconds, and some receivers drop the hub's answer to their
107/// challenge when it comes that much later (issue #163). While held, write() stores each line
108/// instead of printing it; flush() prints the stored lines in order. Not holding, write() prints
109/// at once. A line that does not fit while held is counted, not printed, and flush() reports the
110/// count, so nothing is printed inside the window.
111///
112/// Kept outside the IOHOME_FRAME_LOG guard so its behaviour is unit-testable; firmware builds use
113/// the one instance behind frame_log_hold() / frame_log_release().
115 public:
116 /// Prints one line. `tag` is the logging tag the line was written with.
117 using Sink = void (*)(FrameLogLevel level, const char *tag, const char *text);
118
119 explicit FrameLogBuffer(Sink sink) : sink_(sink) {}
120
121 /// Start keeping lines back. Holding again while held changes nothing.
122 void hold() { this->holding_ = true; }
123 /// @return true while lines are kept back.
124 [[nodiscard]] bool holding() const { return this->holding_; }
125 /// @return number of lines currently kept back.
126 [[nodiscard]] uint8_t held() const { return this->count_; }
127
128 /// Print `text` now, or keep it until the next flush() while holding.
129 /// @param level Level the line is printed at.
130 /// @param tag Must outlive the hold: a string literal, as every logging tag here is.
131 /// @param text The rendered line; copied (and truncated to FRAME_LOG_LINE_SIZE - 1) when held.
132 void write(FrameLogLevel level, const char *tag, const char *text) {
133 if (!this->holding_) {
134 this->sink_(level, tag, text);
135 return;
136 }
137 if (this->count_ >= FRAME_LOG_HELD_LINES) {
138 if (this->dropped_ < UINT8_MAX)
139 this->dropped_++;
140 return;
141 }
142 Line &line = this->lines_[this->count_++];
143 line.level = level;
144 line.tag = tag;
145 snprintf(line.text, sizeof(line.text), "%s", text);
146 }
147
148 /// Print the kept lines in the order they were written, then a note if any did not fit. Keeps
149 /// holding.
150 void flush() {
151 for (uint8_t i = 0; i < this->count_; i++)
152 this->sink_(this->lines_[i].level, this->lines_[i].tag, this->lines_[i].text);
153 this->count_ = 0;
154 if (this->dropped_ > 0) {
155 char note[FRAME_LOG_NOTE_SIZE];
156 snprintf(note, sizeof(note), "%u frame-log lines not printed (hold buffer full)", this->dropped_);
157 this->sink_(FrameLogLevel::WARN, "io_frame", note);
158 this->dropped_ = 0;
159 }
160 }
161
162 /// Print the kept lines and stop holding.
163 void release() {
164 this->flush();
165 this->holding_ = false;
166 }
167
168 private:
169 /// One kept line.
170 struct Line {
172 const char *tag{""};
173 char text[FRAME_LOG_LINE_SIZE]{};
174 };
175
176 Sink sink_;
177 bool holding_{false};
178 uint8_t count_{0};
179 uint8_t dropped_{0};
180 Line lines_[FRAME_LOG_HELD_LINES]{};
181};
182
183#ifdef IOHOME_FRAME_LOG
184/// @brief Print one frame-log line through ESPHome's logger at its level.
185inline void frame_log_print(FrameLogLevel level, const char *tag, const char *text) {
186 switch (level) {
188 ESP_LOGW(tag, "%s", text);
189 break;
191 ESP_LOGI(tag, "%s", text);
192 break;
194 ESP_LOGD(tag, "%s", text);
195 break;
196 }
197}
198
199/// @brief The one hold buffer every frame-log line goes through (about 2 KB, frame-log builds only).
200inline FrameLogBuffer &frame_log_buffer() {
201 static FrameLogBuffer buffer(frame_log_print);
202 return buffer;
203}
204
205/// @brief Write a printf-style frame-log line; held while frame_log_hold() is in effect.
206/// Driver-level reception/transmission diagnostics use this instead of ESP_LOGx, so they are held
207/// with the frame lines they belong to.
208inline void frame_log_line(FrameLogLevel level, const char *tag, const char *format, ...)
209 __attribute__((format(printf, 3, 4)));
210inline void frame_log_line(FrameLogLevel level, const char *tag, const char *format, ...) {
211 char text[FRAME_LOG_LINE_SIZE];
212 va_list args;
213 va_start(args, format);
214 vsnprintf(text, sizeof(text), format, args);
215 va_end(args);
216 frame_log_buffer().write(level, tag, text);
217}
218
219/// @brief Hold frame-log lines until frame_log_release() (see FrameLogBuffer).
220inline void frame_log_hold() { frame_log_buffer().hold(); }
221/// @brief Print the held lines; keep holding.
222inline void frame_log_flush() { frame_log_buffer().flush(); }
223/// @brief Print the held lines and stop holding.
224inline void frame_log_release() { frame_log_buffer().release(); }
225
226inline void log_frame(const char *prefix, const uint8_t *data, uint8_t len, uint32_t freq, uint16_t preamble = 0) {
228 render_frame_hex_redacted(data, len, hex, sizeof(hex));
229
230 // Decode command name and CTRL1 flags for enhanced readability
231 char flags[FRAME_LOG_FLAGS_BUFFER_SIZE] = {0};
232 const char *cmd_str = "";
233 if (len >= FRAME_MIN_SIZE) {
234 decode_ctrl1_flags(data[1], flags, sizeof(flags));
235 cmd_str = command_name(data[FRAME_CMD_OFFSET]);
236 }
237
238 if (preamble > 0)
239 frame_log_line(FrameLogLevel::INFO, "io_frame", "%s [%u bytes] freq=%" PRIu32 " preamble=%u cmd=%s %s: %s", prefix,
240 len, freq, preamble, cmd_str, flags, hex);
241 else
242 frame_log_line(FrameLogLevel::INFO, "io_frame", "%s [%u bytes] freq=%" PRIu32 " cmd=%s %s: %s", prefix, len, freq,
243 cmd_str, flags, hex);
244}
245#else
246// Without the frame log nothing is ever held, so the exchange engine's calls compile to nothing.
247inline void frame_log_hold() {}
248inline void frame_log_flush() {}
249inline void frame_log_release() {}
250#endif
251
252} // namespace home_io_control
253} // namespace esphome
Keeps frame-log lines back while the hub answers a device, and prints them afterwards.
Definition log_frame.h:114
void(*)(FrameLogLevel level, const char *tag, const char *text) Sink
Prints one line. tag is the logging tag the line was written with.
Definition log_frame.h:117
void write(FrameLogLevel level, const char *tag, const char *text)
Print text now, or keep it until the next flush() while holding.
Definition log_frame.h:132
void flush()
Print the kept lines in the order they were written, then a note if any did not fit.
Definition log_frame.h:150
void release()
Print the kept lines and stop holding.
Definition log_frame.h:163
void hold()
Start keeping lines back. Holding again while held changes nothing.
Definition log_frame.h:122
void bytes_to_hex(const uint8_t *data, uint8_t len, char *out, size_t out_size)
Definition log_frame.h:23
constexpr size_t FRAME_LOG_LINE_SIZE
One rendered frame-log line: the hex dump plus its prefix, frequency, command and flags.
Definition log_frame.h:102
void decode_ctrl1_flags(uint8_t ctrl1, char *out, size_t out_size)
Render decoded CTRL1 flags into a short annotation string.
Definition log_frame.h:36
const char * command_name(uint8_t cmd)
Get a human-readable name for any IO-Homecontrol command ID.
constexpr size_t FRAME_LOG_NOTE_SIZE
The overflow note flush() prints when lines did not fit.
Definition log_frame.h:100
void render_frame_hex_redacted(const uint8_t *data, uint8_t len, char *out, size_t out_size)
Render a frame's bytes as spaced hex text, masking the payload when the command carries key material ...
Definition log_frame.h:72
constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE
Fits a full 32-byte frame rendered as spaced hex text.
Definition log_frame.h:20
constexpr size_t FRAME_LOG_FLAGS_BUFFER_SIZE
Buffer for decoded CTRL1 flag annotations.
Definition log_frame.h:21
constexpr size_t FRAME_LOG_HELD_LINES
Lines one hold can keep.
Definition log_frame.h:98
bool command_carries_key_material(uint8_t cmd)
Whether a frame command's payload carries key material that must never be logged or reported verbatim...
Definition redaction.h:46
FrameLogLevel
Log level of one frame-log line, kept with the line while it is held.
Definition log_frame.h:93
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Key-material redaction helpers shared by every surface that can emit frame bytes or debug text (frame...