Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
pairing_telemetry.cpp
Go to the documentation of this file.
1/// @file pairing_telemetry.cpp
2/// @brief Structured per-attempt telemetry recorder implementation.
3/// @ingroup hioc_hub
4
5#include "pairing_telemetry.h"
6
7#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
9
10#include <cinttypes>
11#include <cstdint>
12#include <cstdio>
13#include <cstring>
14
15namespace esphome {
16namespace home_io_control {
17
18namespace {
19
20const char *const TAG = "home_io_control.pairing";
21
22/// Buffer size for the frozen `v1;` result sensor string.
23/// Sized for the worst case, not the common case: outcome=invalid_response (17) +
24/// phase=wait_discover_response (25) + type=heating_temperature_interface (29) + all three
25/// advice codes ("1w_traffic,channel_busy,foreign_controller", 44) + saturated counters
26/// (attempts=255, lbt=255, dur_ms=4294967295, heard=65535) measures ~189 chars; 256 leaves
27/// headroom for future advice codes without silently truncating this frozen contract.
28constexpr size_t RESULT_SENSOR_STRING_BUFFER_SIZE = 256;
29
30/// Saturation ceiling for the uint8_t retry/attempt counters — a real pairing attempt never
31/// gets close to 255 of either, this just guards against wraparound.
32constexpr uint8_t COUNTER_SATURATION_MAX = 0xFF;
33
34const char *outcome_name(PairingOutcome outcome) {
35 switch (outcome) {
37 return "paired";
39 return "no_response";
41 return "invalid_response";
43 return "key_exchange_failed";
45 return "config_failed";
47 default:
48 return "none";
49 }
50}
51
52// Only referenced from ESP_LOGI() calls in log_summary(); ESP_LOGI is a no-op in host unit
53// test builds, so those call sites (and this function) vanish entirely under -Wunused-function
54// there even though it's genuinely used in firmware builds.
55[[maybe_unused]] const char *event_kind_name(PairingTelemetryEventKind kind) {
56 switch (kind) {
58 return "tx";
60 return "rx";
62 return "rx_reject";
64 return "lbt_defer";
66 return "hop";
68 default:
69 return "phase";
70 }
71}
72
73} // namespace
74
76 this->event_count_ = 0;
77 this->heard_count_ = 0;
78 this->truncated_ = false;
79 this->start_ms_ = millis();
80 this->end_ms_ = this->start_ms_;
81 this->ended_ = false;
82 this->phase_ = pairing::PairingState::IDLE;
83 this->outcome_ = PairingOutcome::NONE;
84 this->discovery_attempts_ = 0;
85 this->lbt_retries_ = 0;
86 this->has_paired_device_ = false;
87 memset(this->paired_node_id_, 0, sizeof(this->paired_node_id_));
88 this->paired_device_type_ = DeviceType::UNKNOWN;
89 this->advice_codes_.clear();
90}
91
92void PairingTelemetry::record_(PairingTelemetryEventKind kind, uint8_t cmd, const uint8_t *src, const uint8_t *dst,
93 int16_t rssi, uint8_t aux, bool oneway) {
95 if (this->heard_count_ < UINT16_MAX)
96 this->heard_count_++;
97 }
98 if (this->event_count_ >= PAIRING_TELEMETRY_MAX_EVENTS) {
99 this->truncated_ = true;
100 return;
101 }
102 PairingTelemetryEvent &event = this->events_[this->event_count_++];
103 event.millis_offset = millis() - this->start_ms_;
104 event.kind = kind;
105 event.cmd = cmd;
106 if (src != nullptr) {
107 memcpy(event.src_node, src, NODE_ID_SIZE);
108 } else {
109 memset(event.src_node, 0, NODE_ID_SIZE);
110 }
111 if (dst != nullptr) {
112 memcpy(event.dst_node, dst, NODE_ID_SIZE);
113 } else {
114 memset(event.dst_node, 0, NODE_ID_SIZE);
115 }
116 event.rssi = rssi;
117 event.aux = aux;
118 event.oneway = oneway;
119}
120
122 this->record_(PairingTelemetryEventKind::TX, cmd, nullptr, nullptr, 0, 0, false);
123}
124
125void PairingTelemetry::record_rx(const IoFrame &frame, int16_t rssi) {
126 this->record_(PairingTelemetryEventKind::RX, frame.cmd, frame.src, frame.dst, rssi, 0,
127 (frame.ctrl0 & CTRL0_PROTOCOL_1W) != 0);
128}
129
130void PairingTelemetry::record_rx_reject(const IoFrame &frame, int16_t rssi) {
131 this->record_(PairingTelemetryEventKind::RX_REJECT, frame.cmd, frame.src, frame.dst, rssi, 0,
132 (frame.ctrl0 & CTRL0_PROTOCOL_1W) != 0);
133}
134
136 if (this->lbt_retries_ < COUNTER_SATURATION_MAX)
137 this->lbt_retries_++;
138 this->record_(PairingTelemetryEventKind::LBT_DEFER, 0, nullptr, nullptr, rssi, this->lbt_retries_, false);
139}
140
141void PairingTelemetry::record_hop() { this->record_(PairingTelemetryEventKind::HOP, 0, nullptr, nullptr, 0, 0, false); }
142
144 this->phase_ = phase;
145 this->record_(PairingTelemetryEventKind::PHASE, 0, nullptr, nullptr, 0, static_cast<uint8_t>(phase), false);
146}
147
149 this->outcome_ = outcome;
150 this->end_ms_ = millis();
151 this->ended_ = true;
152}
153
155 this->has_paired_device_ = true;
156 memcpy(this->paired_node_id_, node_id, NODE_ID_SIZE);
157 this->paired_device_type_ = type;
158}
159
161 if (this->discovery_attempts_ < COUNTER_SATURATION_MAX)
162 this->discovery_attempts_++;
163}
164
165uint32_t PairingTelemetry::duration_ms() const { return (this->ended_ ? this->end_ms_ : millis()) - this->start_ms_; }
166
168 ESP_LOGI(
169 TAG, "Pairing attempt summary: outcome=%s phase=%s attempts=%u lbt=%u dur_ms=%" PRIu32 " heard=%u events=%u%s",
170 outcome_name(this->outcome_), pairing_stage_name(this->phase_), this->discovery_attempts_, this->lbt_retries_,
171 this->duration_ms(), this->heard_count_, this->event_count_, this->truncated_ ? " (truncated)" : "");
172 for (uint8_t i = 0; i < this->event_count_; i++) {
173 const PairingTelemetryEvent &event = this->events_[i];
174 if (event.kind == PairingTelemetryEventKind::RX || event.kind == PairingTelemetryEventKind::RX_REJECT) {
175 ESP_LOGI(TAG, " [%4" PRIu32 " ms] %s cmd=0x%02X src=%s rssi=%d", event.millis_offset,
176 event_kind_name(event.kind), event.cmd, node_id_to_string(event.src_node).c_str(), event.rssi);
177 } else if (event.kind == PairingTelemetryEventKind::TX) {
178 ESP_LOGI(TAG, " [%4" PRIu32 " ms] %s cmd=0x%02X", event.millis_offset, event_kind_name(event.kind), event.cmd);
179 } else if (event.kind == PairingTelemetryEventKind::PHASE) {
180 ESP_LOGI(TAG, " [%4" PRIu32 " ms] %s -> %s", event.millis_offset, event_kind_name(event.kind),
181 pairing_stage_name(static_cast<pairing::PairingState>(event.aux)));
182 } else {
183 ESP_LOGI(TAG, " [%4" PRIu32 " ms] %s aux=%u rssi=%d", event.millis_offset, event_kind_name(event.kind),
184 event.aux, event.rssi);
185 }
186 }
187 if (this->has_paired_device_) {
188 ESP_LOGI(TAG, " paired node=%s type=%s", node_id_to_string(this->paired_node_id_).c_str(),
189 device_type_name(this->paired_device_type_));
190 }
191}
192
194 char buf[RESULT_SENSOR_STRING_BUFFER_SIZE];
195 const std::string node = this->has_paired_device_ ? node_id_to_string(this->paired_node_id_) : "-";
196 const std::string type = this->has_paired_device_ ? device_type_name(this->paired_device_type_) : "-";
197 const std::string advice = this->advice_codes_.empty() ? "none" : this->advice_codes_;
198 snprintf(buf, sizeof(buf),
199 "v1; outcome=%s; phase=%s; node=%s; type=%s; attempts=%u; lbt=%u; dur_ms=%" PRIu32 "; heard=%u; advice=%s",
200 outcome_name(this->outcome_), pairing_stage_name(this->phase_), node.c_str(), type.c_str(),
201 this->discovery_attempts_, this->lbt_retries_, this->duration_ms(), this->heard_count_, advice.c_str());
202 return std::string(buf);
203}
204
205} // namespace home_io_control
206} // namespace esphome
void record_rx_reject(const IoFrame &frame, int16_t rssi)
Record that we received a frame that parsed but was rejected by a classifier.
void increment_discovery_attempt()
Increment the discovery attempt counter (one call per discovery command retry).
void set_paired_device(const uint8_t node_id[NODE_ID_SIZE], DeviceType type)
Record the successfully paired device, if any.
void set_phase(pairing::PairingState phase)
Record a pairing state-machine phase transition.
std::string result_sensor_string() const
Render the frozen v1; machine-readable result string.
void record_tx(uint8_t cmd)
Record that we transmitted a frame.
void record_hop()
Record a frequency hop while waiting.
void log_summary() const
Emit a multi-line human-readable summary via ESP_LOGI. Call once, at the end of the attempt.
void record_lbt_defer(int16_t rssi)
Record a listen-before-talk defer (channel busy).
void begin()
Reset all state and start a new attempt. Call once at discover_and_pair() entry.
void set_outcome(PairingOutcome outcome)
Record the final outcome of the attempt.
void record_rx(const IoFrame &frame, int16_t rssi)
Record that we received and accepted a frame.
PairingState
State machine for the three‑phase pairing flow.
Definition hub_pairing.h:45
@ IDLE
No pairing in progress; idle state.
Definition hub_pairing.h:46
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ UNKNOWN
Unknown/unspecified device.
PairingTelemetryEventKind
Kind of a recorded telemetry event.
@ PHASE
The pairing state machine advanced to a new phase.
@ RX_REJECT
We received a frame that parsed but was rejected (wrong source, wrong command, etc....
@ HOP
The radio hopped to a different channel while waiting.
@ LBT_DEFER
A listen-before-talk check deferred a transmit because the channel was busy.
@ RX
We received and accepted a frame for the current wait.
static constexpr const char * TAG
static constexpr uint8_t CTRL0_PROTOCOL_1W
Bit 5: 1=OneWay protocol, 0=TwoWay protocol.
Definition proto_frame.h:42
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
static constexpr uint8_t PAIRING_TELEMETRY_MAX_EVENTS
Maximum number of events recorded per pairing attempt.
PairingOutcome
Final disposition of a pairing attempt, used by the result sensor string.
@ INVALID_RESPONSE
Discovery saw traffic but nothing valid.
@ KEY_EXCHANGE_FAILED
Discovery succeeded but the key exchange did not complete.
@ PAIRED
All three phases completed successfully.
@ CONFIG_FAILED
Key exchange succeeded but SetConfig1 failed (still counted as paired).
@ NO_RESPONSE
No device responded to discovery.
@ NONE
No attempt has completed yet (initial state).
const char * pairing_stage_name(pairing::PairingState state)
Get a short, log/telemetry-friendly name for a pairing state.
Definition hub_pairing.h:76
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
Structured per-attempt telemetry recorder for the pairing flow.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:71
uint8_t ctrl0
Control byte 0: flags + length.
Definition proto_frame.h:72
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
Definition proto_frame.h:75
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
Definition proto_frame.h:74
uint32_t millis_offset
millis() at record time, relative to PairingTelemetry::begin().