Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_frame.cpp
Go to the documentation of this file.
1/// @file proto_frame.cpp
2/// @brief IO-Homecontrol 2W frame container implementation.
3/// @ingroup hioc_protocol
4
5#include "proto_frame.h"
6
7#include "proto_constants.h"
8
9#include <cctype>
10#include <cstdio>
11#include <cstring>
12
13namespace esphome {
14namespace home_io_control {
15
16namespace {
17
18constexpr int HEX_ALPHA_OFFSET = 10;
19
20} // namespace
21
22static int hex_nibble(char ch) {
23 if (ch >= '0' && ch <= '9')
24 return ch - '0';
25 ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
26 if (ch >= 'A' && ch <= 'F')
27 return HEX_ALPHA_OFFSET + (ch - 'A');
28 return -1;
29}
30
31bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len) {
32 if (out == nullptr)
33 return false;
34
35 memset(out, 0, len);
36 if (hex.length() != static_cast<size_t>(len) * 2)
37 return false;
38
39 for (uint8_t i = 0; i < len; i++) {
40 const int high = hex_nibble(hex[i * 2]);
41 const int low = hex_nibble(hex[(i * 2) + 1]);
42 if (high < 0 || low < 0)
43 return false;
44 out[i] = static_cast<uint8_t>((high << 4) | low);
45 }
46
47 return true;
48}
49
50std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE]) {
51 char buf[NODE_ID_STRING_SIZE];
52 snprintf(buf, sizeof(buf), "%02X%02X%02X", id[0], id[1], id[2]);
53 return std::string(buf);
54}
55
56/// CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
57/// Polynomial: 0x1021 (reversed 0x8408), initial value: 0x0000.
58/// Radio chips with native IO-Homecontrol framing compute this in hardware;
59/// drivers for other chips call this helper instead.
60uint16_t crc_ccitt(const uint8_t *data, uint8_t len) {
61 uint16_t crc = 0x0000;
62 for (uint8_t i = 0; i < len; i++) {
63 crc ^= data[i];
64 for (uint8_t j = 0; j < BITS_PER_BYTE; j++)
65 crc = ((crc & CRC_LSB_MASK) != 0) ? (crc >> 1) ^ CRC_POLYNOMIAL_REVERSED : crc >> 1;
66 }
67 return crc;
68}
69
70void init_frame(IoFrame &f, bool is_2w, bool start, bool end, bool low_power) {
71 memset(&f, 0, sizeof(IoFrame));
72 if (end)
73 f.ctrl0 |= CTRL0_END;
74 if (start)
75 f.ctrl0 |= CTRL0_START;
76 if (!is_2w)
78 if (low_power)
80}
81
82void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE]) { memcpy(f.dst, id, NODE_ID_SIZE); }
83void set_src(IoFrame &f, const uint8_t id[NODE_ID_SIZE]) { memcpy(f.src, id, NODE_ID_SIZE); }
84
85bool set_cmd(IoFrame &f, uint8_t cmd, const uint8_t *params, uint8_t params_len) {
86 if (params_len > FRAME_MAX_DATA_SIZE)
87 return false;
88 f.cmd = cmd;
89 f.data_len = params_len;
90 if (params != nullptr && params_len > 0)
91 memcpy(f.data, params, params_len);
92 uint8_t const total = FRAME_MIN_SIZE + f.data_len;
93 // Refuse to encode inconsistent frame metadata here so malformed commands never make it onto
94 // the radio path and later confuse the serializer or on-air retries.
95 if (total > FRAME_MAX_SIZE)
96 return false;
97 f.ctrl0 = (f.ctrl0 & ~CTRL0_LENGTH_MASK) | ((total - 1) & CTRL0_LENGTH_MASK);
98 return true;
99}
100
101uint8_t frame_length(const IoFrame &f) { return (f.ctrl0 & CTRL0_LENGTH_MASK) + 1; }
102bool is_start(const IoFrame &f) { return (f.ctrl0 & CTRL0_START) != 0; }
103bool is_end(const IoFrame &f) { return (f.ctrl0 & CTRL0_END) != 0; }
104
105uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size) {
106 if (buf == nullptr)
107 return 0;
108 uint8_t const len = frame_length(f);
109 if (len < FRAME_MIN_SIZE || len > FRAME_MAX_SIZE)
110 return 0;
112 return 0;
113 // Keep the wire length derived from ctrl0 and the explicit payload length in lockstep. This
114 // catches partially initialized frames before they are transmitted.
115 if ((uint8_t) (FRAME_MIN_SIZE + f.data_len) != len)
116 return 0;
117 if (buf_size < len)
118 return 0;
119 uint8_t offset = 0;
120 buf[offset++] = f.ctrl0;
121 buf[offset++] = f.ctrl1;
122 memcpy(&buf[offset], f.dst, NODE_ID_SIZE);
123 offset += NODE_ID_SIZE;
124 memcpy(&buf[offset], f.src, NODE_ID_SIZE);
125 offset += NODE_ID_SIZE;
126 buf[offset++] = f.cmd;
127 memcpy(&buf[offset], f.data, f.data_len);
128 offset += f.data_len;
129 return offset;
130}
131
132bool parse(const uint8_t *buf, uint8_t buf_len, IoFrame &f) {
133 if (buf == nullptr)
134 return false;
135 if (buf_len < FRAME_MIN_SIZE)
136 return false;
137 memset(&f, 0, sizeof(IoFrame));
138 uint8_t offset = 0;
139 f.ctrl0 = buf[offset++];
140 f.ctrl1 = buf[offset++];
141 uint8_t const len = frame_length(f);
142 if (len < FRAME_MIN_SIZE || len > FRAME_MAX_SIZE)
143 return false;
144 if (buf_len != len)
145 return false;
146 if (offset + NODE_ID_SIZE > buf_len)
147 return false;
148 memcpy(f.dst, &buf[offset], NODE_ID_SIZE);
149 offset += NODE_ID_SIZE;
150 if (offset + NODE_ID_SIZE > buf_len)
151 return false;
152 memcpy(f.src, &buf[offset], NODE_ID_SIZE);
153 offset += NODE_ID_SIZE;
154 if (offset >= buf_len)
155 return false;
156 f.cmd = buf[offset++];
157 f.data_len = len - FRAME_MIN_SIZE;
159 return false;
160 if (offset + f.data_len > buf_len)
161 return false;
162 memcpy(f.data, &buf[offset], f.data_len);
163 return true;
164}
165
166} // namespace home_io_control
167} // 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 BITS_PER_BYTE
Number of bits in one protocol byte.
Definition proto_sizes.h:27
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 FRAME_MIN_SIZE
Minimum frame: CTRL0+CTRL1+DST(3)+SRC(3)+CMD(1).
Definition proto_sizes.h:29
static constexpr uint8_t CTRL0_END
Control byte 0 (CTRL0) bit definitions.
Definition proto_frame.h:40
static constexpr uint8_t FRAME_MAX_DATA_SIZE
Maximum data bytes after command ID.
Definition proto_sizes.h:31
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:42
static constexpr uint8_t CTRL0_START
Bit 6: first frame in exchange (uses long preamble).
Definition proto_frame.h:41
uint16_t crc_ccitt(const uint8_t *data, uint8_t len)
CRC-CCITT used by the IO-Homecontrol protocol for frame validation.
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.
static constexpr uint8_t FRAME_MAX_SIZE
Maximum frame size (9 header + 23 data).
Definition proto_sizes.h:30
void set_dst(IoFrame &f, const uint8_t id[NODE_ID_SIZE])
Set destination node ID.
static constexpr uint8_t NODE_ID_STRING_SIZE
Uppercase hex node ID plus null terminator.
Definition proto_sizes.h:21
bool is_end(const IoFrame &f)
Check END flag.
static constexpr uint16_t CRC_LSB_MASK
Least-significant-bit mask for reflected CRC update.
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 CTRL0_LENGTH_MASK
Bits [4:0]: frame length - 1.
Definition proto_frame.h:43
static int hex_nibble(char ch)
static constexpr uint8_t CTRL1_LOW_POWER
Bit 5: low-power device (e.g., solar-powered).
Definition proto_frame.h:57
static constexpr uint16_t CRC_POLYNOMIAL_REVERSED
Reversed CRC-CCITT polynomial used by IO-homecontrol.
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.
IO-Homecontrol command IDs, result codes and protocol enumerations.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:71
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
Definition proto_frame.h:77
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
uint8_t data_len
Actual length of data.
Definition proto_frame.h:78
uint8_t ctrl1
Control byte 1: low power, beacon, etc.
Definition proto_frame.h:73