Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
log_helpers.h
Go to the documentation of this file.
1#pragma once
2
3/// @file log_helpers.h
4/// @brief Hub-layer log tag and log/format helpers shared by the hub and its collaborators.
5/// @ingroup hioc_hub
6///
7/// Carries no dependency on the hub itself, so a collaborator that only needs to log can include
8/// this header instead of the hub's private helpers (make include-graph enforces that split).
9
10#include "log_frame.h"
11#include "proto_crypto.h"
12#include "proto_frame.h"
13#include "proto_sizes.h"
14#include "radio_interface.h"
15
16#include "esphome/core/log.h"
17
18#include <array>
19#include <cinttypes>
20#include <cstdint>
21#include <cstdio>
22#include <string>
23
24namespace esphome {
25namespace home_io_control {
26namespace detail {
27
28// ============================================================================
29// Shared constants
30// ============================================================================
31
32inline constexpr const char *TAG = "home_io_control"; ///< Shared log tag for hub-level messages.
33
34// ============================================================================
35// Formatting helpers
36// ============================================================================
37
38/// Buffer size for format_name_and_hex(): longest command name plus "(0xXX)" and a margin.
39inline constexpr size_t NAME_AND_HEX_BUFFER_SIZE = 40;
40
41/// @brief Format a name/value pair as "name(0xXX)", e.g. "execute(0x00)".
42inline std::string format_name_and_hex(const char *name, uint8_t value) {
43 std::array<char, NAME_AND_HEX_BUFFER_SIZE> buffer{};
44 std::snprintf(buffer.data(), buffer.size(), "%s(0x%02X)", name, value);
45 return std::string(buffer.data());
46}
47
48// ============================================================================
49// Key-material display formatting
50// ============================================================================
51
52/// @brief Format a 16-byte key as an uppercase, unseparated hex string for display.
53///
54/// The one deliberate place system-key bytes are formatted for display, shared by both
55/// key-recovery features so neither forks its own copy: 2W "Accept Foreign Pairing"
56/// (build_key_extraction_report(), key_extraction_responder.cpp) and 1W controller-key adoption
57/// (build_oneway_adoption_report(), oneway_key_adoption.cpp). See redaction.h for the masking rules this
58/// intentionally does not apply to — both callers are the deliberate exception, not a loosening
59/// of it.
60/// @param key Pointer to AES_KEY_SIZE key bytes.
61/// @return Uppercase hex string, e.g. "0102030405060708090A0B0C0D0E0F10".
62inline std::string format_key_hex(const uint8_t key[AES_KEY_SIZE]) {
63 std::string out;
64 out.reserve(AES_KEY_SIZE * 2);
65 char byte_buf[3];
66 for (uint8_t i = 0; i < AES_KEY_SIZE; i++) {
67 snprintf(byte_buf, sizeof(byte_buf), "%02X", key[i]);
68 out += byte_buf;
69 }
70 return out;
71}
72
73/// A key check value: the first three bytes of the key's AES encryption of a zero block. It names a
74/// key without revealing it, so it is safe in any log line and in a report that is not bannered.
75/// @param key Pointer to AES_KEY_SIZE key bytes.
76/// @return Six uppercase hex digits, or "------" if the check value could not be computed.
77inline std::string format_key_check_value(const uint8_t key[AES_KEY_SIZE]) {
78 uint8_t kcv[crypto::KEY_CHECK_VALUE_SIZE];
79 if (!crypto::key_check_value(key, kcv))
80 return "------";
81 char buf[(2 * crypto::KEY_CHECK_VALUE_SIZE) + 1];
82 snprintf(buf, sizeof(buf), "%02X%02X%02X", kcv[0], kcv[1], kcv[2]);
83 return std::string(buf);
84}
85
86// ============================================================================
87// Logging helpers
88// ============================================================================
89
90/// @brief Log a frame at the "io_capture" tag with structured fields.
91/// Used for protocol‑level debugging (phases: component, tx, rx, parse_ok/parse_fail).
92/// @param radio Radio driver instance (provides chip name and capture).
93/// @param stage String label for the current phase.
94/// @param buf Raw bytes being logged.
95/// @param len Length of buf.
96/// @param frame Optional parsed IoFrame for decoded fields (cmd, src, dst).
97inline void log_component_capture(const RadioDriver *radio, const char *stage, const uint8_t *buf, uint8_t len,
98 const IoFrame *frame = nullptr) {
99 const RadioCaptureInfo &capture = radio->get_last_capture();
100 char payload_hex[FRAME_LOG_HEX_BUFFER_SIZE];
101 // Masks the 0x32 key-transfer payload exactly like log_frame() (log_frame.h) — this path is
102 // separate from log_frame() and runs on every received frame, including a passively overheard
103 // pairing exchange between two other devices, so it must carry the same redaction guarantee.
104 render_frame_hex_redacted(buf, len, payload_hex, sizeof(payload_hex));
105 if (frame != nullptr) {
106 ESP_LOGD("io_capture",
107 "chip=%s phase=component stage=%s freq=%" PRIu32 " ts=%" PRIu32
108 " len=%u cmd=0x%02X src=%02X%02X%02X dst=%02X%02X%02X payload=%s",
109 radio->chip_name(), stage, capture.freq_hz, capture.timestamp_ms, len, frame->cmd, frame->src[0],
110 frame->src[1], frame->src[2], frame->dst[0], frame->dst[1], frame->dst[2], payload_hex);
111 return;
112 }
113 ESP_LOGD("io_capture", "chip=%s phase=component stage=%s freq=%" PRIu32 " ts=%" PRIu32 " len=%u payload=%s",
114 radio->chip_name(), stage, capture.freq_hz, capture.timestamp_ms, len, payload_hex);
115}
116
117/// @brief Log `prefix` followed by `message`, one line per log call rather than one call for the
118/// whole (possibly multi-line) string.
119///
120/// ESPHome formats each log call into a fixed 512-byte buffer (`ESPHOME_LOGGER_TX_BUFFER_SIZE`,
121/// esphome/core/defines.h) and silently truncates anything longer; a multi-line report (a YAML
122/// snippet plus explanatory prose) routinely exceeds that and truncates mid-line if logged as a
123/// single call — confirmed on real hardware for both call sites this function serves:
124/// `scan_paired_devices()`'s report (a multi-device report cut off mid-snippet) and 1W
125/// controller-key adoption's report (the recovered `system_key` line itself never made it into
126/// the log at all). Splitting by line keeps every individual call's payload small regardless of
127/// how long the full message is. Shared rather than duplicated a third time — a second private
128/// copy is exactly how the 1W path ended up with the bug this fixes.
129/// @param tag Log tag.
130/// @param is_warning True to log at WARN, false for INFO.
131/// @param prefix Prepended to the message's first line only (e.g. "Management action X: ").
132/// @param message Message to log; may contain embedded `\n` line breaks.
133inline void log_multiline_result(const char *tag, bool is_warning, const std::string &prefix,
134 const std::string &message) {
135 size_t start = 0;
136 bool first = true;
137 while (true) {
138 const size_t end = message.find('\n', start);
139 const std::string line = (end == std::string::npos) ? message.substr(start) : message.substr(start, end - start);
140 const std::string out = first ? prefix + line : line;
141 if (is_warning) {
142 ESP_LOGW(tag, "%s", out.c_str());
143 } else {
144 ESP_LOGI(tag, "%s", out.c_str());
145 }
146 first = false;
147 if (end == std::string::npos || end + 1 >= message.size())
148 break;
149 start = end + 1;
150 }
151}
152
153} // namespace detail
154} // namespace home_io_control
155} // namespace esphome
Abstract radio driver for IO-Homecontrol.
const RadioCaptureInfo & get_last_capture() const
Get the most recent radio capture info.
virtual const char * chip_name() const =0
Get a human‑readable chip name.
Shared frame logging helpers for IO-Homecontrol.
bool key_check_value(const uint8_t key[AES_KEY_SIZE], uint8_t out[KEY_CHECK_VALUE_SIZE])
Compute a key check value. See proto_crypto.h for the full contract.
std::string format_name_and_hex(const char *name, uint8_t value)
Format a name/value pair as "name(0xXX)", e.g. "execute(0x00)".
Definition log_helpers.h:42
constexpr const char * TAG
Shared log tag for hub-level messages.
Definition log_helpers.h:32
constexpr size_t NAME_AND_HEX_BUFFER_SIZE
Buffer size for format_name_and_hex(): longest command name plus "(0xXX)" and a margin.
Definition log_helpers.h:39
void log_multiline_result(const char *tag, bool is_warning, const std::string &prefix, const std::string &message)
Log prefix followed by message, one line per log call rather than one call for the whole (possibly mu...
void log_component_capture(const RadioDriver *radio, const char *stage, const uint8_t *buf, uint8_t len, const IoFrame *frame=nullptr)
Log a frame at the "io_capture" tag with structured fields.
Definition log_helpers.h:97
std::string format_key_hex(const uint8_t key[AES_KEY_SIZE])
Format a 16-byte key as an uppercase, unseparated hex string for display.
Definition log_helpers.h:62
std::string format_key_check_value(const uint8_t key[AES_KEY_SIZE])
A key check value: the first three bytes of the key's AES encryption of a zero block.
Definition log_helpers.h:77
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:71
constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE
Fits a full 32-byte frame rendered as spaced hex text.
Definition log_frame.h:19
Cryptographic helpers for the IO‑Homecontrol protocol.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Fundamental IO-Homecontrol frame and crypto size constants.
Radio abstraction layer for IO-Homecontrol.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:93
Diagnostic capture from a radio operation.
uint32_t timestamp_ms
Timestamp of capture (millis).
uint32_t freq_hz
RF frequency of capture (Hz).