20constexpr uint8_t UTF8_SINGLE_BYTE_MAX = 0x80;
21constexpr uint8_t UTF8_TWO_BYTE_LEAD_BASE = 0xC0;
22constexpr uint8_t UTF8_CONTINUATION_BASE = 0x80;
23constexpr uint8_t UTF8_CONTINUATION_MASK = 0x3F;
24constexpr uint8_t UTF8_TWO_BYTE_SHIFT = 6;
25constexpr uint8_t NAME_PADDING_NUL = 0x00;
26constexpr uint8_t NAME_PADDING_SPACE = 0x20;
27constexpr uint8_t UTF8_TWO_BYTE_MASK = 0xE0;
28constexpr uint8_t UTF8_TWO_BYTE_PREFIX = 0xC0;
29constexpr uint8_t UTF8_THREE_BYTE_MASK = 0xF0;
30constexpr uint8_t UTF8_THREE_BYTE_PREFIX = 0xE0;
31constexpr uint8_t UTF8_FOUR_BYTE_MASK = 0xF8;
32constexpr uint8_t UTF8_FOUR_BYTE_PREFIX = 0xF0;
33constexpr uint8_t UTF8_CONTINUATION_PREFIX_MASK = 0xC0;
34constexpr uint8_t UTF8_CONTINUATION_PREFIX = 0x80;
35constexpr uint8_t UTF8_TWO_BYTE_VALUE_MASK = 0x1F;
36constexpr uint8_t ASCII_MAX = 0x7F;
37constexpr uint8_t DISCOVERY_TIMESTAMP_MSB_SHIFT = 8;
42constexpr uint8_t ONEWAY_ADD_CONTROLLER_ENC_KEY_OFFSET = 0;
43constexpr uint8_t ONEWAY_ADD_CONTROLLER_MANUFACTURER_OFFSET = AES_KEY_SIZE;
44constexpr uint8_t ONEWAY_ADD_CONTROLLER_SEQUENCE_OFFSET = AES_KEY_SIZE + 2;
45constexpr uint8_t ONEWAY_ADD_CONTROLLER_PAYLOAD_SIZE = AES_KEY_SIZE + 4;
46constexpr uint8_t ONEWAY_ADD_CONTROLLER_SEQUENCE_MSB_SHIFT = 8;
49constexpr uint8_t ONEWAY_ADD_CONTROLLER_MAC_SPAN_SIZE = 1 + AES_KEY_SIZE;
51std::string latin1_to_utf8(
const uint8_t *data,
size_t len) {
53 result.reserve(len * 2);
55 for (
size_t index = 0; index < len; index++) {
56 uint8_t
const byte = data[index];
57 if (
byte < UTF8_SINGLE_BYTE_MAX) {
58 if (result.length() + 1 >= DEVICE_NAME_BUFFER_SIZE)
60 result.push_back(
static_cast<char>(
byte));
64 if (result.length() + 2 >= DEVICE_NAME_BUFFER_SIZE)
67 result.push_back(
static_cast<char>(UTF8_TWO_BYTE_LEAD_BASE | (
byte >> UTF8_TWO_BYTE_SHIFT)));
68 result.push_back(
static_cast<char>(UTF8_CONTINUATION_BASE | (
byte & UTF8_CONTINUATION_MASK)));
78 while (begin < value.length() && std::isspace(
static_cast<unsigned char>(value[begin])) != 0)
81 size_t end = value.length();
82 while (end > begin && std::isspace(
static_cast<unsigned char>(value[end - 1])) != 0)
85 return value.substr(begin, end - begin);
89 if (data ==
nullptr || len == 0)
92 const uint8_t begin = data[0] > NAME_PADDING_SPACE ? 0 : 1;
96 size_t raw_len = len - begin;
98 (data[begin + raw_len - 1] == NAME_PADDING_NUL || data[begin + raw_len - 1] == NAME_PADDING_SPACE))
104 return latin1_to_utf8(data + begin, raw_len);
108 uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE],
109 std::string &normalized_name) {
110 if (payload ==
nullptr)
113 std::memset(payload, 0, DEVICE_NAME_WRITE_PAYLOAD_SIZE);
114 normalized_name.clear();
117 if (trimmed_name.empty())
120 uint8_t latin1_len = 0;
121 for (
size_t index = 0; index < trimmed_name.length();) {
122 const auto byte =
static_cast<uint8_t
>(trimmed_name[index]);
123 uint16_t codepoint = 0;
126 if (
byte <= ASCII_MAX) {
128 }
else if ((
byte & UTF8_TWO_BYTE_MASK) == UTF8_TWO_BYTE_PREFIX) {
129 if (index + 1 >= trimmed_name.length())
132 const auto continuation =
static_cast<uint8_t
>(trimmed_name[index + 1]);
133 if ((continuation & UTF8_CONTINUATION_PREFIX_MASK) != UTF8_CONTINUATION_PREFIX)
136 codepoint =
static_cast<uint16_t
>(((
byte & UTF8_TWO_BYTE_VALUE_MASK) << UTF8_TWO_BYTE_SHIFT) |
137 (continuation & UTF8_CONTINUATION_MASK));
138 if (codepoint < UTF8_SINGLE_BYTE_MAX)
141 }
else if ((
byte & UTF8_THREE_BYTE_MASK) == UTF8_THREE_BYTE_PREFIX ||
142 (
byte & UTF8_FOUR_BYTE_MASK) == UTF8_FOUR_BYTE_PREFIX) {
148 if (codepoint > LATIN1_CODEPOINT_MAX)
151 if (latin1_len >= DEVICE_NAME_WRITE_CHAR_LIMIT)
154 payload[latin1_len++] =
static_cast<uint8_t
>(codepoint);
158 normalized_name = latin1_to_utf8(payload, latin1_len);
171 return "INVALID_UTF8";
173 return "UNSUPPORTED_CHAR";
175 return "UNKNOWN_DEVICE_NAME_VALIDATION_ERROR";
182 return "name accepted";
184 return "device name must not be empty";
186 return "device name exceeds the 15-character write limit";
188 return "device name must be valid UTF-8";
190 return "device name contains characters outside Latin-1";
192 return "unknown device-name validation error";
200 uint8_t
const suffix = addr[2] & ADDRESS_SUFFIX_MASK;
201 bool const has_type_bits = (addr[1] != 0) || ((addr[2] & 0xC0) != 0);
204 if (suffix == ADDRESS_SUFFIX_DISCOVERY)
212 if (suffix == ADDRESS_SUFFIX_BROADCAST)
214 if (addr[1] == 0 && addr[2] == 0)
221 switch (address_class) {
225 return "broadcast_all";
227 return "broadcast_type";
232 return "unknown_broadcast";
242 uint16_t
const type_raw =
243 (
static_cast<uint16_t
>(addr[1]) << DEVICE_TYPE_LOW_BITS_SHIFT) | (addr[2] >> DEVICE_TYPE_HIGH_BITS_SHIFT);
252 const auto type_raw =
static_cast<uint16_t
>(type);
254 out[1] =
static_cast<uint8_t
>(type_raw >> DEVICE_TYPE_LOW_BITS_SHIFT);
255 out[2] =
static_cast<uint8_t
>((type_raw << DEVICE_TYPE_HIGH_BITS_SHIFT) | DEVICE_SUBTYPE_MASK);
262 if (main0 == POS_STOP) {
263 snprintf(out, out_size,
"STOP");
266 if (main0 == POS_FAVORITE) {
267 if (main1 == POS_VENT_MODIFIER) {
268 snprintf(out, out_size,
"VENT");
270 snprintf(out, out_size,
"FAVORITE");
274 if (main0 == POS_UNKNOWN) {
275 snprintf(out, out_size,
"UNCHANGED");
278 if (main0 == POS_FORCE_OPEN) {
283 snprintf(out, out_size,
"FORCE_OPEN");
286 if (main0 == POS_SECURED_TARGET) {
287 snprintf(out, out_size,
"SECURED_TARGET");
290 if (main0 == POS_DEFAULT) {
291 snprintf(out, out_size,
"DEFAULT");
295 if (main0 <= POSITION_WIRE_MAX) {
296 uint8_t
const percent = main0 / POSITION_WIRE_SCALE;
298 snprintf(out, out_size,
"OPEN");
300 }
else if (percent == 100) {
301 snprintf(out, out_size,
"CLOSE");
303 snprintf(out, out_size,
"position %u%%", percent);
308 snprintf(out, out_size,
"0x%02X", main0);
315 if (main0 == POS_STOP || main0 == POS_FAVORITE || main0 == POS_UNKNOWN || main0 == POS_FORCE_OPEN ||
316 main0 == POS_SECURED_TARGET || main0 == POS_DEFAULT) {
321 if (main0 <= POSITION_WIRE_MAX) {
322 uint8_t
const percent = main0 / POSITION_WIRE_SCALE;
323 return static_cast<float>(percent);
333 memcpy(info.
src, frame.
src, NODE_ID_SIZE);
345 info.
acei_level = (frame.
data[1] & ACEI_LEVEL_MASK) >> ACEI_LEVEL_SHIFT;
361 memcpy(device.
node_id, frame.
src, NODE_ID_SIZE);
373 device.
target = UNKNOWN_POSITION;
378 if (frame.
data_len > DISCOVERY_RESP_MANUFACTURER_OFFSET) {
381 if (frame.
data_len > DISCOVERY_RESP_BACKBONE_OFFSET + NODE_ID_SIZE - 1) {
382 memcpy(info.
backbone, &frame.
data[DISCOVERY_RESP_BACKBONE_OFFSET], NODE_ID_SIZE);
384 if (frame.
data_len > DISCOVERY_RESP_FLAGS_OFFSET) {
385 info.
flags = frame.
data[DISCOVERY_RESP_FLAGS_OFFSET];
387 if (frame.
data_len > DISCOVERY_RESP_TIMESTAMP_OFFSET + 1) {
389 static_cast<uint16_t
>((frame.
data[DISCOVERY_RESP_TIMESTAMP_OFFSET] << DISCOVERY_TIMESTAMP_MSB_SHIFT) |
390 frame.
data[DISCOVERY_RESP_TIMESTAMP_OFFSET + 1]);
405 if ((frame.
ctrl0 & CTRL0_PROTOCOL_1W) == 0)
407 if (frame.
cmd != CMD_ONEWAY_ADD_CONTROLLER)
409 if (frame.
data_len != ONEWAY_ADD_CONTROLLER_PAYLOAD_SIZE)
412 const uint8_t *enc_key = &frame.
data[ONEWAY_ADD_CONTROLLER_ENC_KEY_OFFSET];
420 out.
sequence =
static_cast<uint16_t
>(
421 (frame.
data[ONEWAY_ADD_CONTROLLER_SEQUENCE_OFFSET] << ONEWAY_ADD_CONTROLLER_SEQUENCE_MSB_SHIFT) |
422 frame.
data[ONEWAY_ADD_CONTROLLER_SEQUENCE_OFFSET + 1]);
434 uint8_t mac_span[ONEWAY_ADD_CONTROLLER_MAC_SPAN_SIZE];
435 mac_span[0] = frame.
cmd;
436 memcpy(&mac_span[1], enc_key, AES_KEY_SIZE);
438 uint8_t expected_mac[HMAC_SIZE];
446 for (uint8_t i = 0; i < HMAC_SIZE; i++)
447 diff |=
static_cast<uint8_t
>(frame.
mac[i] ^ expected_mac[i]);
455 if (frame.
data_len < base + NODE_ID_SIZE + 1)
bool crypt_1w_key(const uint8_t node[NODE_ID_SIZE], const uint8_t in[AES_KEY_SIZE], uint8_t out[AES_KEY_SIZE])
Encrypt or decrypt a 1W controller key during add-controller key adoption (CMD 0x30).
bool create_1w_hmac(const uint8_t *data, uint8_t len, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], uint8_t hmac[HMAC_SIZE])
Create the 6-byte authenticator for a 1W frame.
@ VERIFIED
frame.has_mac was true and the MAC verified under the recovered key.
@ NOT_PRESENT
frame.has_mac was false; nothing to verify.
@ FAILED
frame.has_mac was true and the MAC did NOT verify under the recovered key.
static constexpr uint8_t ONEWAY_EXECUTE_MIN_DATA_LEN
Minimum data bytes for decode of execute/activate‑mode intent fields.
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ UNKNOWN
Unknown/unspecified device.
@ SWINGING_SHUTTER
Swinging shutter.
void encode_broadcast_address(DeviceType type, uint8_t out[NODE_ID_SIZE])
Encode a device type into its typed-broadcast destination address — the exact inverse of broadcast_ta...
OneWayAddControllerDecodeError decode_1w_add_controller(const IoFrame &frame, OneWayAdoptedKey &out)
Decode a CMD_ONEWAY_ADD_CONTROLLER (0x30) frame into a recovered controller identity.
bool default_inverted_for_type(DeviceType type)
Determine whether a device type has inverted position mapping by default.
std::optional< float > oneway_intent_to_target(uint8_t main0, uint8_t main1)
Resolve a 1W main-byte pair to an optimistic IO target position, if unambiguous.
const char * oneway_target_label(const OneWayFrameInfo &info)
The destination label a decoded 1W frame renders as in a log line.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
LastCommandRecord decode_last_command_record(const IoFrame &frame, uint8_t base)
Decode the last-command record at base from a status-bearing payload.
OneWayAddControllerDecodeError
Decoding outcome for decode_1w_add_controller().
@ WRONG_COMMAND
frame.cmd is not CMD_ONEWAY_ADD_CONTROLLER.
@ NOT_ONEWAY
CTRL0_PROTOCOL_1W is not set — not a 1W frame at all.
@ NONE
Decode succeeded; out is populated.
@ BAD_LENGTH
Declared payload is not exactly the expected 20 bytes.
@ KEY_UNWRAP_FAILED
crypto::crypt_1w_key() itself reported failure.
OneWayFrameInfo decode_1w_frame(const IoFrame &frame)
Decode a parsed 1W frame into a structured OneWayFrameInfo.
DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype)
Decode a protocol-packed device type from two metadata bytes.
const char * device_name_validation_error_name(DeviceNameValidationError error)
Return a stable symbolic name for a device-name validation result.
DeviceNameValidationError
Validation result for outbound device-name writes.
@ UNSUPPORTED_CHAR
Name contains characters outside Latin-1.
@ TOO_LONG
Name exceeds the 15-character write limit.
@ NONE
Name is valid and encodable.
@ EMPTY
Name is empty after normalization.
@ INVALID_UTF8
Name contains malformed UTF-8 bytes.
std::string trim_ascii_whitespace(const std::string &value)
Trim leading and trailing ASCII whitespace from a string.
DeviceNameValidationError encode_device_name_payload(const std::string &name, uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE], std::string &normalized_name)
Validate and encode a user-supplied UTF-8 device name into the fixed Latin-1 write payload.
AddressClass classify_address(const uint8_t addr[NODE_ID_SIZE])
Classify an IO-Homecontrol 3-byte address.
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.
std::string decode_device_name_payload(const uint8_t *data, uint8_t len)
Decode a device-name payload from IO-homecontrol's Latin-1 wire format into UTF-8.
DeviceType broadcast_target_type(const uint8_t addr[NODE_ID_SIZE])
Extract the target device type from a typed broadcast address.
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
AddressClass
Address classification categories for diagnostic purposes.
@ UNKNOWN_BROADCAST
Broadcast pattern that does not match known suffixes.
@ UNICAST
Normal device-to-device unicast address (first byte != 0x00).
@ BROADCAST_TYPE
Broadcast to specific device type with non-standard suffix.
@ BROADCAST_ALL
Broadcast to all devices of a type (address suffix 0x3F).
@ DISCOVERY
Discovery-related broadcast (address suffix 0x3B).
const char * address_class_name(AddressClass address_class)
Get a human-readable name for an address classification.
DiscoveryResponseInfo decode_discovery_response(const IoFrame &frame, IoDevice &device, std::string &device_id)
Decode a discovery-response payload (CMD_DISCOVER_RESP 0x29 or CMD_DISCOVER_SPE_RESP 0x2B — both carr...
const char * device_name_validation_error_description(DeviceNameValidationError error)
Return a human-readable explanation for a device-name validation result.
void decode_1w_main_intent(uint8_t main0, uint8_t main1, char *out, size_t out_size)
Decode the "main" position/command bytes from a 1W execute payload.
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol command IDs, result codes and protocol enumerations.
Cryptographic helpers for the IO‑Homecontrol protocol.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Extended discovery-response fields (manufacturer, Multi Information Byte, backbone address,...
bool has_extended
data_len >= DISCOVERY_RESP_FULL_SIZE (mfr/flags/timestamp present).
uint8_t backbone[NODE_ID_SIZE]
Backbone address as reported by the device.
bool metadata_complete
data_len >= DEVICE_METADATA_SIZE (type/subtype present).
uint8_t manufacturer
Raw manufacturer ID; name via manufacturer_name().
uint8_t flags
Multi Information Byte; decode with DISCOVERY_FLAGS_* masks.
uint16_t timestamp
Device timestamp field (advances between replies).
Runtime state of a paired IO‑Homecontrol device.
float target
Target position the device is moving toward.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint8_t subtype
Device subtype (manufacturer‑specific).
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
DeviceType type
Device type (shutter, awning, etc.).
bool is_stopped
True if device is not moving.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes). Never includes mac.
uint8_t mac[HMAC_SIZE]
Out-of-length authenticator trailer; meaningful only when has_mac.
bool has_mac
True if this frame carries the mac trailer (see struct doc).
uint8_t ctrl0
Control byte 0: flags + length.
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
uint8_t data_len
Actual length of data.
One decoded last-command record.
uint8_t commander[NODE_ID_SIZE]
Controller that last commanded the device.
bool valid
False when the payload was too short, or the record was unpopulated.
uint8_t originator
That command's Command Originator (ORIGINATOR_*).
Recovered controller identity from a decoded CMD_ONEWAY_ADD_CONTROLLER (0x30) frame.
uint8_t manufacturer
Manufacturer ID byte from the payload (man_id).
OneWayMacStatus mac_status
MAC-verification outcome; see OneWayMacStatus.
uint8_t system_key[AES_KEY_SIZE]
Recovered network system key (crypto::crypt_1w_key() output).
uint8_t sender_node[NODE_ID_SIZE]
Sender's node address (frame.src) — the new identity's node.
uint16_t sequence
2-byte rolling sequence from the payload (big-endian on wire).
Decoded representation of a 1W remote frame.
bool has_intent
True if originator/ACEI/intent fields were decoded.
uint8_t originator
Command originator byte (e.g., ORIGINATOR_USER_REMOTE).
DeviceType target_type
Target device class from broadcast address.
uint8_t main0
Raw first main byte (has_intent only); feeds oneway_intent_to_target().
uint8_t acei_level
ACEI priority level (0–7).
uint8_t main1
Raw second main byte (has_intent only); feeds oneway_intent_to_target().
uint8_t cmd
Command ID (e.g., CMD_EXECUTE, CMD_ACTIVATE_MODE).
AddressClass address_class
Classification of the broadcast address.
char intent[ONEWAY_INTENT_BUFFER_SIZE]
Human-readable command intent (e.g., "CLOSE").
uint8_t src[NODE_ID_SIZE]
Remote source node ID (3 bytes).
uint8_t data_len
Raw data length (for commands without decoded intent).