Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_frame.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_frame.h
4/// @brief IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
5/// @ingroup hioc_protocol
6///
7/// IO-Homecontrol is a proprietary wireless protocol used by Somfy, Velux, and other
8/// manufacturers for controlling shutters, awnings, blinds, and similar devices.
9/// "2W" means two-way: the controller sends commands and receives status feedback.
10///
11/// The protocol uses FSK modulation at 868 MHz with frequency hopping across 3 channels.
12/// Communication is encrypted with AES-128 and authenticated with a 6-byte HMAC.
13/// Each installation has a unique 16-byte "system key" shared between controller and devices.
14///
15/// This header owns only the frame container itself. The rest of the protocol model lives in
16/// cohesive headers (proto_sizes/proto_timing/proto_constants/proto_device_model/proto_codecs).
17/// New code should include the specific header it needs.
18
19#include "proto_sizes.h"
20
21#include <cstdint>
22#include <cstring>
23#include <string>
24
25namespace esphome {
26namespace home_io_control {
27
28// ============================================================================
29// Control Bytes
30// ============================================================================
31
32/// Control byte 0 (CTRL0) bit definitions.
33/// CTRL0 encodes frame flags and the total frame length.
34/// Bits [4:0] = frame_length - 1 (so 0x08 means 9 bytes total).
35/// - START (bit 6): first frame in an exchange. Its TX preamble depends on CTRL1_LOW_POWER, not on
36/// START alone: a low-power target gets the 1024-byte wake-up burst, others a normal preamble.
37/// - END (bit 7): last frame in an exchange; set on responses and command completions.
38/// - 1W (bit 5): 1=OneWay protocol (no response expected), 0=TwoWay (response expected).
39/// For 2W operation, the controller sets START on initial command and device replies with END; subsequent frames in an
40/// authenticated exchange also carry END.
41static constexpr uint8_t CTRL0_END = 0x80; ///< Bit 7: last frame in exchange
42static constexpr uint8_t CTRL0_START = 0x40; ///< Bit 6: first frame in exchange
43static constexpr uint8_t CTRL0_PROTOCOL_1W = 0x20; ///< Bit 5: 1=OneWay protocol, 0=TwoWay protocol
44static constexpr uint8_t CTRL0_LENGTH_MASK = 0x1F; ///< Bits [4:0]: frame length - 1
45
46/// Ties `FRAME_MAX_DECLARED_SIZE` (proto_sizes.h) to the mask that actually defines it. The two
47/// can't be expressed as one expression across the header boundary (proto_sizes.h can't include
48/// this header back without a cycle), so this assert is the drift guard instead.
50 "FRAME_MAX_DECLARED_SIZE must track CTRL0_LENGTH_MASK's 5-bit field");
51
52/// Control byte 1 (CTRL1) bit definitions.
53/// CTRL1 carries protocol metadata flags that describe the frame's routing,
54/// power mode, and priority characteristics.
55/// - VERSION (bits [1:0]): protocol version number (usually 0 for current devices).
56/// - PRIORITY (bit 2): marks a high-priority frame (e.g., discovery, security commands).
57/// - ACK (bit 4): sender can handle 2W responses (set on all outbound 2W frames).
58/// - LOW_POWER (bit 5): device is battery/solar powered; may sleep and requires long preamble to
59/// wake. Set from the target's per-device YAML `low_power` class (default false); drives both
60/// this bit and the start-frame preamble (see proto_commands.h, exchange_engine.cpp).
61/// - ROUTED (bit 6): frame was relayed through a repeater node rather than direct.
62/// - BEACON (bit 7): beacon announcement frame (device presence advertisement).
63static constexpr uint8_t CTRL1_VERSION_MASK = 0x03; ///< Bits [1:0]: protocol version (usually 0).
64static constexpr uint8_t CTRL1_PRIORITY = 0x04; ///< Bit 2: high-priority frame.
65static constexpr uint8_t CTRL1_ACK = 0x10; ///< Bit 4: sender can handle 2W responses (ACK-capable).
66static constexpr uint8_t CTRL1_LOW_POWER = 0x20; ///< Bit 5: low-power device (e.g., solar-powered).
67static constexpr uint8_t CTRL1_ROUTED = 0x40; ///< Bit 6: frame was relayed through a repeater.
68static constexpr uint8_t CTRL1_BEACON = 0x80; ///< Bit 7: beacon announcement frame.
69
70// ============================================================================
71// Frame Structure
72// ============================================================================
73
74/// @brief Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
75/// @ingroup hioc_protocol
76///
77/// Over the air layout: [CTRL0][CTRL1][DST 3B][SRC 3B][CMD][DATA 0-23B][MAC 0/6B][CRC 2B].
78/// The on-air CRC is the radio driver's responsibility (hardware or software,
79/// depending on the chip); it is not included in this struct.
80///
81/// The MAC trailer exists because at least one frame shape's authenticator does not fit inside
82/// CTRL0's 5-bit length field alongside its payload: a 1W CMD 0x30 "add controller" frame is 29
83/// declared bytes plus a 6-byte MAC, and 35 has no 5-bit encoding. That MAC rides after the
84/// declared length instead — still under the CRC, but outside what CTRL0's length bits describe
85/// — so it is modeled as a distinct trailer rather than folded into `data[]`. `has_mac` is false
86/// for every frame shape that predates this (i.e. everything except a MAC-bearing 1W frame), so
87/// `data_len`/`frame_length()` keep meaning exactly what they meant before this field existed.
88struct IoFrame {
89 uint8_t ctrl0; ///< Control byte 0: flags + length.
90 uint8_t ctrl1; ///< Control byte 1: low power, beacon, etc.
91 uint8_t dst[NODE_ID_SIZE]; ///< Destination node ID (3 bytes).
92 uint8_t src[NODE_ID_SIZE]; ///< Source node ID (3 bytes).
93 uint8_t cmd; ///< Command ID.
94 uint8_t data[FRAME_MAX_DATA_SIZE]; ///< Command parameters (0–23 bytes). Never includes `mac`.
95 uint8_t data_len; ///< Actual length of data. `FRAME_MIN_SIZE + data_len == frame_length()`
96 ///< always holds, trailer or not — see `mac`/`has_mac`.
97 uint8_t mac[HMAC_SIZE]; ///< Out-of-length authenticator trailer; meaningful only when `has_mac`.
98 bool has_mac = false; ///< True if this frame carries the `mac` trailer (see struct doc).
99};
100
101// --- Frame construction and parsing ---
102/// Initialize an IoFrame header (ctrl0/ctrl1) with flags.
103///
104/// Note: CTRL1_ACK is NOT automatically set on outbound frames. Some real-world
105/// devices reject frames with unexpected CTRL1 bits, causing total communication
106/// failure. The ACK constant is retained for inbound frame parsing and logging only.
107/// @param f Frame to initialize.
108/// @param is_2w True for 2‑way (default), false for 1‑way.
109/// @param start Set START flag (first frame in exchange).
110/// @param end Set END flag (final frame in exchange).
111/// @param low_power Set LOW_POWER flag.
112void init_frame(IoFrame &f, bool is_2w = true, bool start = false, bool end = false, bool low_power = false);
113/// Set destination node ID.
114/// @param f Frame to modify.
115/// @param id 3‑byte destination address.
116void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE]);
117/// Set source node ID.
118/// @param f Frame to modify.
119/// @param id 3‑byte source address.
120void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE]);
121/// Set command and payload.
122/// @param f Frame to modify.
123/// @param cmd Command ID.
124/// @param params Pointer to payload bytes (may be nullptr for zero‑length).
125/// @param params_len Payload length (0–23).
126/// @return true if frame fits within size limits; false otherwise.
127bool set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params = nullptr, uint8_t params_len = 0);
128/// Get total frame length from ctrl0.
129/// @param f Parsed frame.
130/// @return Length in bytes.
131uint8_t frame_length(const IoFrame &f);
132/// Check START flag.
133/// @param f Parsed frame.
134/// @return true if START flag is set.
135bool is_start(const IoFrame &f);
136/// Check END flag.
137/// @param f Parsed frame.
138/// @return true if END flag is set.
139bool is_end(const IoFrame &f);
140/// Whether wire frames for a command carry the out-of-length MAC trailer described on
141/// `IoFrame::has_mac`/`IoFrame::mac`. This exists because CTRL0's 5-bit length field cannot
142/// describe every command's declared payload plus a 6-byte authenticator in one span — one
143/// command's authenticator is carried outside the declared length instead (see the command's own
144/// Doxygen in proto_constants.h for why). `parse()` consults this before it will accept the
145/// wider `declared_len + HMAC_SIZE` buffer shape for a given command, so an unrelated frame that
146/// merely happens to arrive with 6 extra trailing bytes is never mistaken for a trailer-bearing
147/// one — only a command that genuinely carries a trailer gets the wider shape considered at all.
148/// @param cmd Command byte (`IoFrame::cmd`, or the raw byte at `FRAME_CMD_OFFSET` in a wire buffer).
149/// @return true if `cmd`'s wire frames carry the `mac` trailer after the declared length.
150bool frame_carries_mac_trailer(uint8_t cmd);
151/// Serialize a parsed frame into a wire buffer (without CRC).
152/// @param f Parsed frame. When `f.has_mac`, the 6-byte `mac` trailer is appended after the
153/// declared payload and counted in the returned length, so a caller's CRC (computed over the
154/// returned length) covers it.
155/// @param buf Output buffer (must be at least frame_length(f) bytes, or +HMAC_SIZE when `f.has_mac`).
156/// @param buf_size Size of buf.
157/// @return Number of bytes written (declared length, plus HMAC_SIZE when `f.has_mac`), or 0 on failure.
158uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size);
159/// Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
160/// @param buf Raw byte buffer.
161/// @param buf_len Number of bytes in buf. Accepted shapes: exactly the CTRL0-declared length
162/// (`has_mac` comes out false), or — only for a command where `frame_carries_mac_trailer()` is
163/// true — that length plus HMAC_SIZE (the trailing bytes are copied into `f.mac` and `has_mac`
164/// comes out true). Any other length, or that same wider length for a command that doesn't
165/// carry a trailer, is rejected. `data_len` is always `buf_len`'s declared portion minus
166/// FRAME_MIN_SIZE — the trailer is never data.
167/// @param f Output parsed frame.
168/// @return true if parse succeeded; false otherwise.
169bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f);
170
171// ============================================================================
172// Node ID Helpers
173// ============================================================================
174
175/// @brief Convert a hex string (e.g., "123ABC") to a byte array.
176/// @param hex Hex string (must be exactly len*2 characters).
177/// @param out Output buffer (at least len bytes).
178/// @param len Number of bytes to produce.
179/// @return true on success; false if hex length mismatch or non‑hex characters.
180bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len);
181/// @brief Format a 3‑byte node ID as a 6‑character uppercase hex string.
182/// @param id 3‑byte node ID.
183/// @return Hex string (e.g., "123ABC").
184std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE]);
185
186// ============================================================================
187// CRC
188// ============================================================================
189
190/// @brief Compute CRC‑CCITT (poly 0x1021, init 0x0000) over a buffer.
191/// Used by radio drivers without hardware IO-Homecontrol CRC support and by
192/// frame validation in tests.
193/// @param data Pointer to data bytes.
194/// @param len Number of bytes.
195/// @return 16‑bit CRC value.
196uint16_t crc_ccitt(const uint8_t *data, uint8_t len);
197
198} // namespace home_io_control
199} // namespace esphome
bool set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params, uint8_t params_len)
Set command and payload.
static constexpr uint8_t FRAME_MAX_DECLARED_SIZE
Largest frame length CTRL0's 5-bit length field (bits [4:0], length - 1) can express.
Definition proto_sizes.h:38
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
static constexpr uint8_t CTRL0_END
Control byte 0 (CTRL0) bit definitions.
Definition proto_frame.h:41
static constexpr uint8_t FRAME_MAX_DATA_SIZE
Maximum data bytes after command ID (declared length - header).
Definition proto_sizes.h:46
bool is_start(const IoFrame &f)
Check START flag.
static constexpr uint8_t CTRL0_PROTOCOL_1W
Bit 5: 1=OneWay protocol, 0=TwoWay protocol.
Definition proto_frame.h:43
static constexpr uint8_t CTRL0_START
Bit 6: first frame in exchange.
Definition proto_frame.h:42
static constexpr uint8_t CTRL1_ROUTED
Bit 6: frame was relayed through a repeater.
Definition proto_frame.h:67
uint16_t crc_ccitt(const uint8_t *data, uint8_t len)
CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
static constexpr uint8_t HMAC_SIZE
Authentication HMAC is 6 bytes (truncated AES output).
Definition proto_sizes.h:22
void init_frame(IoFrame &f, bool is_2w, bool start, bool end, bool low_power)
Initialize an IoFrame header (ctrl0/ctrl1) with flags.
uint8_t frame_length(const IoFrame &f)
Get total frame length from ctrl0.
bool frame_carries_mac_trailer(uint8_t cmd)
Whether wire frames for a command carry the out-of-length MAC trailer described on IoFrame::has_mac/I...
void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE])
Set destination node ID.
bool is_end(const IoFrame &f)
Check END flag.
bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f)
Parse a wire buffer into a parsed IoFrame (validates length and CTRL0).
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.
static constexpr uint8_t CTRL1_PRIORITY
Bit 2: high-priority frame.
Definition proto_frame.h:64
static constexpr uint8_t CTRL1_ACK
Bit 4: sender can handle 2W responses (ACK-capable).
Definition proto_frame.h:65
static constexpr uint8_t CTRL0_LENGTH_MASK
Bits [4:0]: frame length - 1.
Definition proto_frame.h:44
static constexpr uint8_t CTRL1_VERSION_MASK
Ties FRAME_MAX_DECLARED_SIZE (proto_sizes.h) to the mask that actually defines it.
Definition proto_frame.h:63
static constexpr uint8_t CTRL1_LOW_POWER
Bit 5: low-power device (e.g., solar-powered).
Definition proto_frame.h:66
static constexpr uint8_t CTRL1_BEACON
Bit 7: beacon announcement frame.
Definition proto_frame.h:68
bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len)
Convert a hex string (e.g., "123ABC") to a byte array.
uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size)
Serialize a parsed frame into a wire buffer (without CRC).
void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE])
Set source node ID.
Fundamental IO-Homecontrol frame and crypto size constants.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:88
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes). Never includes mac.
Definition proto_frame.h:94
uint8_t mac[HMAC_SIZE]
Out-of-length authenticator trailer; meaningful only when has_mac.
Definition proto_frame.h:97
bool has_mac
True if this frame carries the mac trailer (see struct doc).
Definition proto_frame.h:98
uint8_t ctrl0
Control byte 0: flags + length.
Definition proto_frame.h:89
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
Definition proto_frame.h:92
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
Definition proto_frame.h:91
uint8_t data_len
Actual length of data.
Definition proto_frame.h:95
uint8_t ctrl1
Control byte 1: low power, beacon, etc.
Definition proto_frame.h:90