ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ira211_protocol.cpp
Go to the documentation of this file.
1#include "ira211_protocol.h"
2#include "esphome/core/hal.h"
4#include "esphome/core/log.h"
5
6namespace esphome::remote_base {
7
8ESPHOME_LOG_TAG(TAG, "remote.ira211");
9
10static constexpr uint32_t CARRIER_FREQUENCY = 38000;
11static constexpr uint32_t T_US = 800;
12static constexpr uint32_t HEADER_US = 19 * T_US / 2; // 7600 us, also the gap that separates frames
13// Midway between the longest data run (9 bits, 7200 us) and the gap, so a gap captured a little short still
14// ends the frame
15static constexpr uint32_t GAP_MIN_US = HEADER_US - T_US / 4;
16static constexpr uint8_t MIN_HALF_DEGREES = static_cast<uint8_t>(IRA211_MIN_TEMPERATURE * 2);
17static constexpr uint8_t MAX_HALF_DEGREES = static_cast<uint8_t>(IRA211_MAX_TEMPERATURE * 2);
18
19static constexpr uint8_t DEVICE_ID = 0x66;
20static constexpr uint8_t BITS_PER_PACKET = 10;
21static constexpr uint8_t MIN_PACKETS = 4;
22static constexpr uint8_t MAX_PACKETS = 7;
23static constexpr uint8_t MAX_BITS = MAX_PACKETS * BITS_PER_PACKET;
24
25// Fields each command carries, in wire order after the command byte: temperature (whole degrees
26// then tenths), mode, fan.
27static constexpr uint8_t FIELD_TEMPERATURE = 1 << 0;
28static constexpr uint8_t FIELD_MODE = 1 << 1;
29static constexpr uint8_t FIELD_FAN = 1 << 2;
30
31struct CommandLayout {
32 IRA211Command command;
33 uint8_t fields;
34};
35
36static const CommandLayout LAYOUTS[] PROGMEM = {
37 {IRA211Command::IRA211_COMMAND_TEMP_UP, FIELD_TEMPERATURE},
38 {IRA211Command::IRA211_COMMAND_TEMP_DOWN, FIELD_TEMPERATURE},
41 {IRA211Command::IRA211_COMMAND_POWER, FIELD_TEMPERATURE | FIELD_MODE | FIELD_FAN},
42 {IRA211Command::IRA211_COMMAND_SYNC, FIELD_TEMPERATURE | FIELD_MODE | FIELD_FAN},
43};
44
45static optional<CommandLayout> find_layout(IRA211Command command) {
46 for (const auto &slot : LAYOUTS) {
47 CommandLayout layout;
48 progmem_memcpy(&layout, &slot, sizeof(layout));
49 if (layout.command == command)
50 return layout;
51 }
52 return {};
53}
54
55// Device ID, command, fields, checksum
56static uint8_t packet_count(const CommandLayout &layout) {
57 return 3 + ((layout.fields & FIELD_TEMPERATURE) ? 2 : 0) + ((layout.fields & FIELD_MODE) ? 1 : 0) +
58 ((layout.fields & FIELD_FAN) ? 1 : 0);
59}
60
61// Value to wire byte and back; the transform is its own inverse
62static uint8_t wire_xform(uint8_t value) { return reverse_bits(static_cast<uint8_t>(~value)); }
63
64static bool is_known_mode(uint8_t value) {
65 switch (static_cast<IRA211Mode>(value)) {
69 return true;
70 default:
71 return false;
72 }
73}
74
75static bool is_known_fan(uint8_t value) {
76 switch (static_cast<IRA211Fan>(value)) {
81 return true;
82 default:
83 return false;
84 }
85}
86
87static uint8_t checksum(const uint8_t *bytes, uint8_t count) {
88 uint8_t sum = count;
89 for (uint8_t i = 0; i < count; i++)
90 sum += reverse_bits(bytes[i]);
91 return reverse_bits(sum);
92}
93
94bool IRA211Data::operator==(const IRA211Data &rhs) const {
95 if (this->command != rhs.command)
96 return false;
97 const optional<CommandLayout> layout = find_layout(this->command);
98 if (!layout.has_value())
99 return true;
100 if ((layout->fields & FIELD_TEMPERATURE) && this->half_degrees != rhs.half_degrees)
101 return false;
102 if ((layout->fields & FIELD_MODE) && this->mode != rhs.mode)
103 return false;
104 return !(layout->fields & FIELD_FAN) || this->fan == rhs.fan;
105}
106
108 const optional<CommandLayout> layout = find_layout(data.command);
109 if (!layout.has_value())
110 return;
111
112 uint8_t bytes[MAX_PACKETS];
113 uint8_t count = 0;
114 bytes[count++] = DEVICE_ID;
115 bytes[count++] = wire_xform(static_cast<uint8_t>(data.command));
116 if (layout->fields & FIELD_TEMPERATURE) {
117 bytes[count++] = wire_xform(data.half_degrees / 2);
118 bytes[count++] = wire_xform((data.half_degrees & 1) ? 5 : 0);
119 }
120 if (layout->fields & FIELD_MODE)
121 bytes[count++] = wire_xform(static_cast<uint8_t>(data.mode));
122 if (layout->fields & FIELD_FAN)
123 bytes[count++] = wire_xform(static_cast<uint8_t>(data.fan));
124 bytes[count] = checksum(bytes, count);
125 count++;
126
127 dst->set_carrier_frequency(CARRIER_FREQUENCY);
128 // Preamble, at most one item per bit, trailing mark and gap
129 dst->reserve(4 + count * BITS_PER_PACKET + 2);
130 dst->item(HEADER_US, T_US);
131 dst->item(T_US, HEADER_US);
132
133 // Each packet is a 1 bit, the byte MSB first, then a 0 bit; runs of equal bits become one mark or space
134 bool level = true;
135 uint32_t run = 0;
136 for (uint8_t p = 0; p < count; p++) {
137 const uint16_t packet = (1 << 9) | (bytes[p] << 1);
138 for (int8_t i = BITS_PER_PACKET - 1; i >= 0; i--) {
139 const bool bit = (packet >> i) & 1;
140 if (bit != level && run != 0) {
141 if (level) {
142 dst->mark(run * T_US);
143 } else {
144 dst->space(run * T_US);
145 }
146 run = 0;
147 }
148 level = bit;
149 run++;
150 }
151 }
152 // Every packet ends with a 0 bit, so the frame ends with a space, closed by a trailing mark
153 dst->space(run * T_US);
154 dst->mark(T_US);
155 dst->space(HEADER_US);
156}
157
159 if (!src.expect_item(HEADER_US, T_US) || !src.expect_item(T_US, HEADER_US))
160 return {};
161
162 // Read bits until the end-of-frame gap, checking packet framing and collecting bytes as they arrive
163 uint8_t bytes[MAX_PACKETS]{};
164 uint8_t bits = 0;
165 while (src.is_valid()) {
166 const int32_t raw = src.peek();
167 const bool is_mark = raw > 0;
168 const uint32_t duration = is_mark ? raw : -raw;
169 // A mark followed by the gap or the end of the data is the trailing mark, not a data bit
170 if (is_mark && (!src.is_valid(1) || src.peek(1) <= -static_cast<int32_t>(GAP_MIN_US)))
171 break;
172 if (!is_mark && duration >= GAP_MIN_US)
173 break;
174 const uint32_t run = (duration + T_US / 2) / T_US;
175 if (run == 0 || bits + run > MAX_BITS)
176 return {};
177 for (uint32_t i = 0; i < run; i++, bits++) {
178 const uint8_t pos = bits % BITS_PER_PACKET;
179 if ((pos == 0 && !is_mark) || (pos == BITS_PER_PACKET - 1 && is_mark))
180 return {};
181 if (pos != 0 && pos != BITS_PER_PACKET - 1) {
182 uint8_t &byte = bytes[bits / BITS_PER_PACKET];
183 byte = (byte << 1) | (is_mark ? 1 : 0);
184 }
185 }
186 src.advance();
187 }
188
189 const uint8_t count = bits / BITS_PER_PACKET;
190 if (bits % BITS_PER_PACKET != 0 || count < MIN_PACKETS || bytes[0] != DEVICE_ID)
191 return {};
192 IRA211Data out{};
193 out.command = static_cast<IRA211Command>(wire_xform(bytes[1]));
194 const optional<CommandLayout> layout = find_layout(out.command);
195 if (!layout.has_value() || packet_count(*layout) != count || checksum(bytes, count - 1) != bytes[count - 1]) {
196 ESP_LOGV(TAG, "Invalid frame (%u bits)", bits);
197 return {};
198 }
199
200 // A checksum-valid frame can still carry values outside what the thermostat accepts
201 uint8_t index = 2;
202 if (layout->fields & FIELD_TEMPERATURE) {
203 const uint8_t whole = wire_xform(bytes[index++]);
204 const uint8_t tenths = wire_xform(bytes[index++]);
205 const uint16_t half_degrees = whole * 2 + (tenths >= 5 ? 1 : 0);
206 if (half_degrees < MIN_HALF_DEGREES || half_degrees > MAX_HALF_DEGREES)
207 return {};
208 out.half_degrees = static_cast<uint8_t>(half_degrees);
209 }
210 if (layout->fields & FIELD_MODE) {
211 const uint8_t mode = wire_xform(bytes[index++]);
212 if (!is_known_mode(mode))
213 return {};
214 out.mode = static_cast<IRA211Mode>(mode);
215 }
216 if (layout->fields & FIELD_FAN) {
217 const uint8_t fan = wire_xform(bytes[index++]);
218 if (!is_known_fan(fan))
219 return {};
220 out.fan = static_cast<IRA211Fan>(fan);
221 }
222 return out;
223}
224
225static const LogString *command_to_string(IRA211Command command) {
226 switch (command) {
228 return LOG_STR("TEMP_UP");
230 return LOG_STR("TEMP_DOWN");
232 return LOG_STR("MODE");
234 return LOG_STR("FAN");
236 return LOG_STR("POWER");
238 return LOG_STR("SYNC");
239 default:
240 return LOG_STR("UNKNOWN");
241 }
242}
243
244static const LogString *mode_to_string(IRA211Mode mode) {
245 switch (mode) {
247 return LOG_STR("Protection");
249 return LOG_STR("Timer");
251 return LOG_STR("Comfort");
252 default:
253 return LOG_STR("Unknown");
254 }
255}
256
257static const LogString *fan_to_string(IRA211Fan fan) {
258 switch (fan) {
260 return LOG_STR("Auto");
262 return LOG_STR("1/3");
264 return LOG_STR("2/3");
266 return LOG_STR("3/3");
267 default:
268 return LOG_STR("Unknown");
269 }
270}
271
273 const optional<CommandLayout> layout = find_layout(data.command);
274 const uint8_t fields = layout.has_value() ? layout->fields : 0;
275 const unsigned whole = data.half_degrees / 2;
276 const unsigned tenths = (data.half_degrees & 1) ? 5 : 0;
277 const auto *command = LOG_STR_ARG(command_to_string(data.command));
278 if (fields == (FIELD_TEMPERATURE | FIELD_MODE | FIELD_FAN)) {
279 ESP_LOGI(TAG, "Received IRA211: command=%s, temperature=%u.%uC, mode=%s, fan=%s", command, whole, tenths,
280 LOG_STR_ARG(mode_to_string(data.mode)), LOG_STR_ARG(fan_to_string(data.fan)));
281 } else if (fields == FIELD_TEMPERATURE) {
282 ESP_LOGI(TAG, "Received IRA211: command=%s, temperature=%u.%uC", command, whole, tenths);
283 } else if (fields == FIELD_MODE) {
284 ESP_LOGI(TAG, "Received IRA211: command=%s, mode=%s", command, LOG_STR_ARG(mode_to_string(data.mode)));
285 } else if (fields == FIELD_FAN) {
286 ESP_LOGI(TAG, "Received IRA211: command=%s, fan=%s", command, LOG_STR_ARG(fan_to_string(data.fan)));
287 } else {
288 ESP_LOGI(TAG, "Received IRA211: command=%s", command);
289 }
290}
291
292} // namespace esphome::remote_base
BedjetMode mode
BedJet operating mode.
uint8_t checksum
Definition bl0906.h:3
uint8_t raw[35]
Definition bl0939.h:0
optional< IRA211Data > decode(RemoteReceiveData src)
void encode(RemoteTransmitData *dst, const IRA211Data &data)
void dump(const IRA211Data &data)
void set_carrier_frequency(uint32_t carrier_frequency)
Definition remote_base.h:31
void item(uint32_t mark, uint32_t space)
Definition remote_base.h:26
std::vector< uint8_t > & out
Definition gif.cpp:49
unsigned count
Definition gif.cpp:51
uint8_t duration
Definition msa3xx.h:0
ESPHOME_LOG_TAG(TAG, "remote.abbwelcome")
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
size_t size_t pos
Definition helpers.h:1179
const void * src
Definition hal.h:68
uint8_t reverse_bits(uint8_t x)
Reverse the order of 8 bits.
Definition helpers.h:1019
static void uint32_t
bool operator==(const IRA211Data &rhs) const
Compares the command and only the fields that command carries.