Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
tuning_config.cpp
Go to the documentation of this file.
1/// @file tuning_config.cpp
2/// @brief Runtime tuning configuration helpers and snapshot formatting.
3/// @ingroup hioc_tuning
4
5#include "tuning_config.h"
6
7#include "proto_constants.h"
8
9#include <algorithm>
10#include <cctype>
11#include <cstdio>
12
13namespace esphome {
14namespace home_io_control {
15
16namespace {
17
18/// Default tuning configuration used for diffing and reset behavior.
19const TuningConfig DEFAULTS{};
20
21/// Buffer size for a formatted single hex byte such as "0x2E" (4 chars + slack).
22constexpr size_t HEX_BYTE_STR_SIZE = 8;
23/// Buffer size for a formatted 3-byte hex address such as "0x00003B".
24constexpr size_t HEX_ADDR_STR_SIZE = 16;
25/// Buffer size for a formatted bandwidth string such as "117.3kHz".
26constexpr size_t BANDWIDTH_STR_SIZE = 16;
27
28/// Numeric kHz value for each SX1262 RX bandwidth option.
29constexpr float BW_KHZ_58_6 = 58.6F;
30constexpr float BW_KHZ_78_2 = 78.2F;
31constexpr float BW_KHZ_117_3 = 117.3F;
32constexpr float BW_KHZ_156_2 = 156.2F;
33constexpr float BW_KHZ_187_2 = 187.2F;
34
35/// Numeric kHz value for each SX1276 RX bandwidth option.
36constexpr float BW_KHZ_20_8 = 20.8F;
37constexpr float BW_KHZ_41_7 = 41.7F;
38constexpr float BW_KHZ_62_5 = 62.5F;
39constexpr float BW_KHZ_83_3 = 83.3F;
40constexpr float BW_KHZ_125_0 = 125.0F;
41
42/// Numeric kHz value for each LR1121 RX bandwidth option.
43constexpr float BW_KHZ_39_0 = 39.0F;
44constexpr float BW_KHZ_46_9 = 46.9F;
45
46} // namespace
47
49 switch (bw) {
51 return BW_KHZ_58_6;
53 return BW_KHZ_78_2;
55 return BW_KHZ_117_3;
57 return BW_KHZ_156_2;
59 return BW_KHZ_187_2;
60 }
61 return BW_KHZ_117_3;
62}
63
65 char buf[BANDWIDTH_STR_SIZE];
66 snprintf(buf, sizeof(buf), "%.1f", sx1262_bandwidth_to_khz(bw));
67 return std::string(buf);
68}
69
70std::optional<SX1262RxBandwidth> sx1262_bandwidth_from_string(const std::string &value) {
71 std::string normalized = value;
72 // Normalize: strip whitespace and a trailing "kHz"/"khz" suffix.
73 normalized.erase(std::remove_if(normalized.begin(), normalized.end(), ::isspace), normalized.end());
74 for (char &c : normalized)
75 c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
76 if (normalized.size() > 3 && normalized.ends_with("khz"))
77 normalized.resize(normalized.size() - 3);
78
79 if (normalized == "58.6" || normalized == "58")
81 if (normalized == "78.2" || normalized == "78")
83 if (normalized == "117.3" || normalized == "117")
85 if (normalized == "156.2" || normalized == "156")
87 if (normalized == "187.2" || normalized == "187")
89 return std::nullopt;
90}
91
93 switch (bw) {
95 return BW_KHZ_20_8;
97 return BW_KHZ_41_7;
99 return BW_KHZ_62_5;
101 return BW_KHZ_83_3;
103 return BW_KHZ_125_0;
104 }
105 return BW_KHZ_41_7;
106}
107
109 char buf[BANDWIDTH_STR_SIZE];
110 snprintf(buf, sizeof(buf), "%.1f", sx1276_bandwidth_to_khz(bw));
111 return std::string(buf);
112}
113
114std::optional<SX1276RxBandwidth> sx1276_bandwidth_from_string(const std::string &value) {
115 std::string normalized = value;
116 // Normalize: strip whitespace and a trailing "kHz"/"khz" suffix.
117 normalized.erase(std::remove_if(normalized.begin(), normalized.end(), ::isspace), normalized.end());
118 for (char &c : normalized)
119 c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
120 if (normalized.size() > 3 && normalized.ends_with("khz"))
121 normalized.resize(normalized.size() - 3);
122
123 if (normalized == "20.8" || normalized == "20")
125 if (normalized == "41.7" || normalized == "41")
127 if (normalized == "62.5" || normalized == "62")
129 if (normalized == "83.3" || normalized == "83")
131 if (normalized == "125.0" || normalized == "125")
133 return std::nullopt;
134}
135
137 switch (bw) {
139 return BW_KHZ_39_0;
141 return BW_KHZ_46_9;
143 return BW_KHZ_58_6;
145 return BW_KHZ_78_2;
147 return BW_KHZ_117_3;
149 return BW_KHZ_156_2;
151 return BW_KHZ_187_2;
152 }
153 return BW_KHZ_117_3;
154}
155
157 char buf[BANDWIDTH_STR_SIZE];
158 snprintf(buf, sizeof(buf), "%.1f", lr1121_bandwidth_to_khz(bw));
159 return std::string(buf);
160}
161
162std::optional<LR1121RxBandwidth> lr1121_bandwidth_from_string(const std::string &value) {
163 std::string normalized = value;
164 // Normalize: strip whitespace and a trailing "kHz"/"khz" suffix.
165 normalized.erase(std::remove_if(normalized.begin(), normalized.end(), ::isspace), normalized.end());
166 for (char &c : normalized)
167 c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
168 if (normalized.size() > 3 && normalized.ends_with("khz"))
169 normalized.resize(normalized.size() - 3);
170
171 if (normalized == "39.0" || normalized == "39")
173 if (normalized == "46.9" || normalized == "46")
175 if (normalized == "58.6" || normalized == "58")
177 if (normalized == "78.2" || normalized == "78")
179 if (normalized == "117.3" || normalized == "117")
181 if (normalized == "156.2" || normalized == "156")
183 if (normalized == "187.2" || normalized == "187")
185 return std::nullopt;
186}
187
188std::optional<DiscoveryCommand> discovery_command_from_string(const std::string &value) {
189 std::string normalized = value;
190 for (char &c : normalized)
191 c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
192 if (normalized == "0x28" || normalized == "discover")
194 if (normalized == "0x2a" || normalized == "discover_spe")
196 if (normalized == "0x2e" || normalized == "discover_alt")
198 return std::nullopt;
199}
200
202 char buf[HEX_BYTE_STR_SIZE];
203 snprintf(buf, sizeof(buf), "0x%02X", static_cast<uint8_t>(cmd));
204 return std::string(buf);
205}
206
207std::string discovery_commands_to_csv(const std::vector<DiscoveryCommand> &commands) {
208 std::string result;
209 for (size_t i = 0; i < commands.size(); ++i) {
210 if (i > 0)
211 result += ',';
212 result += discovery_command_to_string(commands[i]);
213 }
214 return result;
215}
216
217std::string discovery_commands_to_string(const std::vector<DiscoveryCommand> &commands) {
218 return "[" + discovery_commands_to_csv(commands) + "]";
219}
220
221const uint8_t *resolve_discovery_destination(uint8_t command, bool destination_auto,
222 const uint8_t destination[NODE_ID_SIZE]) {
223 if (!destination_auto)
224 return destination;
225 // Conventional destinations from the protocol / reference captures: standard and SPE
226 // discovery broadcast to 0x00003B; alternate discovery (0x2E) broadcasts to 0x00003F.
227 switch (command) {
228 case CMD_DISCOVER_REQ:
230 return BROADCAST_DISCOVER;
233 default:
234 return BROADCAST_DISCOVER;
235 }
236}
237
238std::string discovery_destination_to_string(bool destination_auto, const uint8_t destination[NODE_ID_SIZE]) {
239 if (destination_auto)
240 return "auto";
241 char buf[HEX_ADDR_STR_SIZE];
242 snprintf(buf, sizeof(buf), "0x%02X%02X%02X", destination[0], destination[1], destination[2]);
243 return std::string(buf);
244}
245
246std::string discovery_payload_to_string(bool payload_enabled, uint8_t payload) {
247 if (!payload_enabled)
248 return "none";
249 char buf[HEX_BYTE_STR_SIZE];
250 snprintf(buf, sizeof(buf), "0x%02X", payload);
251 return std::string(buf);
252}
253
254std::string tuning_update_log_line(const std::string &name, const std::string &value) {
255 return "Tuning updated via HA: " + name + "=" + value;
256}
257
258std::string tuning_config_snapshot(const TuningConfig &cfg) {
259 std::string result;
260
261 if (cfg.sx1262_rx_bandwidth != DEFAULTS.sx1262_rx_bandwidth)
262 result += " sx1262_rx_bandwidth=" + sx1262_bandwidth_to_string(cfg.sx1262_rx_bandwidth);
263 if (cfg.sx1262_response_preamble != DEFAULTS.sx1262_response_preamble)
264 result += " sx1262_response_preamble=" + std::to_string(cfg.sx1262_response_preamble);
265 if (cfg.sx1262_post_tx_settle_us != DEFAULTS.sx1262_post_tx_settle_us)
266 result += " sx1262_post_tx_settle_us=" + std::to_string(cfg.sx1262_post_tx_settle_us);
267 if (cfg.sx1276_rx_bandwidth != DEFAULTS.sx1276_rx_bandwidth)
268 result += " sx1276_rx_bandwidth=" + sx1276_bandwidth_to_string(cfg.sx1276_rx_bandwidth);
269 if (cfg.sx1276_response_preamble != DEFAULTS.sx1276_response_preamble)
270 result += " sx1276_response_preamble=" + std::to_string(cfg.sx1276_response_preamble);
271 if (cfg.sx1276_discovery_hop_slice_ms != DEFAULTS.sx1276_discovery_hop_slice_ms)
272 result += " sx1276_discovery_hop_slice_ms=" + std::to_string(cfg.sx1276_discovery_hop_slice_ms);
273 if (cfg.sx1262_discovery_hop_slice_ms != DEFAULTS.sx1262_discovery_hop_slice_ms)
274 result += " sx1262_discovery_hop_slice_ms=" + std::to_string(cfg.sx1262_discovery_hop_slice_ms);
275 if (cfg.lr1121_rx_bandwidth != DEFAULTS.lr1121_rx_bandwidth)
276 result += " lr1121_rx_bandwidth=" + lr1121_bandwidth_to_string(cfg.lr1121_rx_bandwidth);
277 if (cfg.lr1121_response_preamble != DEFAULTS.lr1121_response_preamble)
278 result += " lr1121_response_preamble=" + std::to_string(cfg.lr1121_response_preamble);
279 if (cfg.lr1121_post_tx_settle_us != DEFAULTS.lr1121_post_tx_settle_us)
280 result += " lr1121_post_tx_settle_us=" + std::to_string(cfg.lr1121_post_tx_settle_us);
281 if (cfg.lr1121_discovery_hop_slice_ms != DEFAULTS.lr1121_discovery_hop_slice_ms)
282 result += " lr1121_discovery_hop_slice_ms=" + std::to_string(cfg.lr1121_discovery_hop_slice_ms);
283 if (cfg.lbt_max_retries != DEFAULTS.lbt_max_retries)
284 result += " lbt_max_retries=" + std::to_string(cfg.lbt_max_retries);
285 if (cfg.lbt_rssi_threshold_dbm != DEFAULTS.lbt_rssi_threshold_dbm)
286 result += " lbt_rssi_threshold_dbm=" + std::to_string(cfg.lbt_rssi_threshold_dbm);
287
288 if (cfg.pairing_discovery_commands != DEFAULTS.pairing_discovery_commands)
289 result += " pairing_discovery_commands=" + discovery_commands_to_string(cfg.pairing_discovery_commands);
290 // The default is auto; an explicit destination is the only non-default state worth emitting
291 // (the destination bytes are unused while auto is set).
293 result +=
294 " pairing_discovery_destination=" + discovery_destination_to_string(cfg.pairing_discovery_destination_auto,
296 }
297 if (cfg.pairing_discovery_payload_enabled != DEFAULTS.pairing_discovery_payload_enabled ||
298 cfg.pairing_discovery_payload != DEFAULTS.pairing_discovery_payload) {
299 result += " pairing_discovery_payload=" +
301 }
302 if (cfg.pairing_discovery_low_power != DEFAULTS.pairing_discovery_low_power)
303 result += " pairing_discovery_low_power=" + std::string(cfg.pairing_discovery_low_power ? "true" : "false");
304 if (cfg.pairing_discovery_wait_ms != DEFAULTS.pairing_discovery_wait_ms)
305 result += " pairing_discovery_wait_ms=" + std::to_string(cfg.pairing_discovery_wait_ms);
306 if (cfg.pairing_discovery_initial_dwell_ms != DEFAULTS.pairing_discovery_initial_dwell_ms)
307 result += " pairing_discovery_initial_dwell_ms=" + std::to_string(cfg.pairing_discovery_initial_dwell_ms);
308 if (cfg.pairing_key_exchange_retries != DEFAULTS.pairing_key_exchange_retries)
309 result += " pairing_key_exchange_retries=" + std::to_string(cfg.pairing_key_exchange_retries);
310
311 return result;
312}
313
315 const std::string result = tuning_config_snapshot(cfg);
316 if (result.empty())
317 return "Tuning: defaults active";
318 return "Tuning overrides active:" + result;
319}
320
321} // namespace home_io_control
322} // namespace esphome
std::string discovery_commands_to_csv(const std::vector< DiscoveryCommand > &commands)
Format the ordered discovery command list as a UI/preset option string.
static constexpr uint8_t CMD_DISCOVER_REQ
Broadcast discovery request.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
std::optional< LR1121RxBandwidth > lr1121_bandwidth_from_string(const std::string &value)
Convert a YAML LR1121 bandwidth string to the enum value.
SX1262RxBandwidth
Valid SX1262 RX bandwidth options (kHz register values).
@ BW_58_6_KHZ
58.6 kHz — narrowest; SX1276-equivalent width, marginal on the default TX→RX turnaround.
@ BW_156_2_KHZ
156.2 kHz — wider tolerance for LO offset.
@ BW_187_2_KHZ
187.2 kHz — widest selectable option.
std::string discovery_payload_to_string(bool payload_enabled, uint8_t payload)
Format a payload option for YAML/logs.
std::optional< DiscoveryCommand > discovery_command_from_string(const std::string &value)
Parse a discovery command string (e.g., "0x28") into the enum.
static constexpr uint8_t CMD_DISCOVER_ALT_REQ
Alternate discovery.
LR1121RxBandwidth
Valid LR1121 RX bandwidth options (register values).
@ BW_39_0_KHZ
39.0 kHz — narrowest; close to SX1276's validated 41.7 kHz default.
@ BW_156_2_KHZ
156.2 kHz — wider tolerance for LO offset.
@ BW_187_2_KHZ
187.2 kHz — widest selectable option.
std::string lr1121_bandwidth_to_string(LR1121RxBandwidth bw)
Format an LR1121 bandwidth enum as its YAML/UI option string (bare kHz number).
std::string tuning_config_snapshot(const TuningConfig &cfg)
Format the current tuning configuration as a one-line YAML-compatible snapshot.
std::optional< SX1262RxBandwidth > sx1262_bandwidth_from_string(const std::string &value)
Convert a YAML bandwidth string to the enum value.
std::string sx1276_bandwidth_to_string(SX1276RxBandwidth bw)
Format an SX1276 bandwidth enum as its YAML/UI option string (bare kHz number).
float lr1121_bandwidth_to_khz(LR1121RxBandwidth bw)
Convert an LR1121 bandwidth enum to the numeric kHz value used in YAML/logs.
std::string discovery_commands_to_string(const std::vector< DiscoveryCommand > &commands)
Format the ordered discovery command list for logs.
std::string tuning_config_full_snapshot(const TuningConfig &cfg)
Format the current tuning configuration as a full one-line snapshot.
float sx1262_bandwidth_to_khz(SX1262RxBandwidth bw)
Convert a bandwidth enum to the numeric kHz value used in YAML/logs.
static constexpr uint8_t CMD_DISCOVER_SPE_REQ
Broadcast roll-call answered by every device that already holds this controller's system key,...
std::string sx1262_bandwidth_to_string(SX1262RxBandwidth bw)
Format a bandwidth enum as its YAML/UI option string (bare kHz number, e.g.
std::string tuning_update_log_line(const std::string &name, const std::string &value)
Format a single tuning update for the log.
DiscoveryCommand
Discovery request command codes.
@ DISCOVER_SPE
SPE roll-call request (self-authenticating; see CMD_DISCOVER_SPE_REQ).
@ DISCOVER
Standard broadcast discovery request (to 0x00003B).
@ DISCOVER_ALT
Alternate broadcast discovery (to 0x00003F), with optional payload byte.
float sx1276_bandwidth_to_khz(SX1276RxBandwidth bw)
Convert an SX1276 bandwidth enum to the numeric kHz value used in YAML/logs.
static constexpr uint8_t BROADCAST_DISCOVER[NODE_ID_SIZE]
Broadcast address for device discovery (0x00003B).
static constexpr uint8_t BROADCAST_DISCOVER_ALT[NODE_ID_SIZE]
Alternate discovery / 1W broadcast address (0x00003F).
std::string discovery_command_to_string(DiscoveryCommand cmd)
Format a discovery command enum for YAML/logs.
SX1276RxBandwidth
Valid SX1276 RX bandwidth options (RegRxBw register bytes).
@ BW_41_7_KHZ
41.7 kHz — default (validated against real devices).
@ BW_125_0_KHZ
125.0 kHz — widest selectable option.
@ BW_20_8_KHZ
20.8 kHz — narrowest; maximal noise rejection, least LO-offset tolerance.
std::optional< SX1276RxBandwidth > sx1276_bandwidth_from_string(const std::string &value)
Convert a YAML SX1276 bandwidth string to the enum value.
const uint8_t * resolve_discovery_destination(uint8_t command, bool destination_auto, const uint8_t destination[NODE_ID_SIZE])
Resolve the destination address for a discovery command.
std::string discovery_destination_to_string(bool destination_auto, const uint8_t destination[NODE_ID_SIZE])
Format a destination option for YAML/logs.
IO-Homecontrol command IDs, result codes and protocol enumerations.
All runtime tunable parameters for pairing and radio diagnostics.
uint16_t pairing_discovery_initial_dwell_ms
Initial dwell on CH2 before discovery hopping begins.
bool pairing_discovery_destination_auto
When true, map commands to conventional addresses.
SX1276RxBandwidth sx1276_rx_bandwidth
SX1276 RX bandwidth selector.
int16_t lbt_rssi_threshold_dbm
LBT channel-free threshold (dBm).
SX1262RxBandwidth sx1262_rx_bandwidth
SX1262 RX bandwidth selector.
std::vector< uint8_t > pairing_discovery_destination
Destination when auto=false.
std::vector< DiscoveryCommand > pairing_discovery_commands
Ordered discovery commands.
uint16_t lr1121_discovery_hop_slice_ms
Per-channel dwell while LR1121 discovery hops.
uint16_t sx1276_response_preamble
SX1276 response preamble in bytes.
uint16_t sx1262_post_tx_settle_us
Delay after SX1262 TX before RX (µs).
uint8_t pairing_key_exchange_retries
Retries for the authenticated key exchange phase.
bool pairing_discovery_low_power
Set LOW_POWER flag in discovery frames.
uint16_t sx1276_discovery_hop_slice_ms
Per-channel dwell while SX1276 discovery hops.
uint16_t sx1262_response_preamble
SX1262 response preamble in bytes.
uint16_t pairing_discovery_wait_ms
Total wait window after sending discovery commands.
bool pairing_discovery_payload_enabled
Whether the optional payload is enabled.
LR1121RxBandwidth lr1121_rx_bandwidth
LR1121 RX bandwidth selector.
uint16_t lr1121_post_tx_settle_us
Delay after LR1121 TX before RX (µs).
uint8_t lbt_max_retries
LBT retries before forced TX.
uint16_t sx1262_discovery_hop_slice_ms
Per-channel dwell while SX1262 discovery hops.
uint16_t lr1121_response_preamble
LR1121 response preamble in bytes.
uint8_t pairing_discovery_payload
Optional payload byte (used for 0x2E).
Runtime tuning configuration for pairing and radio diagnostics.