ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
drayton_protocol.cpp
Go to the documentation of this file.
1#include "drayton_protocol.h"
2#include "esphome/core/log.h"
3
4#include <cinttypes>
5
6namespace esphome::remote_base {
7
8ESPHOME_LOG_TAG(TAG, "remote.drayton");
9
10static constexpr uint32_t BIT_TIME_US = 500;
11static constexpr uint8_t CARRIER_KHZ = 2;
12static constexpr uint8_t NBITS_PREAMBLE = 12;
13static constexpr uint8_t NBITS_SYNC = 4;
14static constexpr uint8_t NBITS_ADDRESS = 16;
15static constexpr uint8_t NBITS_DATA = 4;
16static constexpr uint8_t NBITS_CHECKSUM = 8;
17static constexpr uint8_t NBITS_COMMAND = 7;
18static constexpr uint8_t NBITS_CHANNEL = 5;
19static constexpr uint8_t NBITS_PKT = NBITS_ADDRESS + NBITS_DATA + NBITS_CHECKSUM;
20static constexpr uint8_t MIN_RX_SRC = (NBITS_PKT + NBITS_SYNC / 2);
21
22/*
23Drayton Protocol
24Using an oscilloscope to capture the data transmitted by the Digistat two
25distinct packets for 'On' and 'Off' are transmitted. Each transmitted bit
26has a period of 500us, a bit rate of 2000 baud.
27
28Each packet consists of an initial 1010 pattern to set up the receiver bias.
29The number of these bits seen at the receiver varies depending on the state
30of the bias when the packet transmission starts. The receiver algoritmn takes
31account of this.
32
33The packet is Manchester coded, with a '10' tranmitted pair representing
34a '1' bit and a '01' pair representing a '0' bit. Each packet is begun
35with a '1100' syncronisation symbol which breaks this rule. Following
36the sync are 28 '01' or '10' pairs.
37
38--------------------
39
40Sample Boiler On Command as received:
41101010101010110001101001010101101001010101010101100101010101101001011001
42ppppppppppppSSSS-0-1-1-0-0-0-0-1-1-0-0-0-0-0-0-0-1-0-0-0-0-0-1-1-0-0-1-0
43
44(Where pppp represents the preamble bits and SSSS represents the sync symbol)
45
4628 bits received 01100001100000001000001 10010 (bin) or 6180832 (hex)
47
48Sample Boiler Off Command as received:
49101010101010110001101001010101101001010101010101010101010110011001011001
50ppppppppppppSSSS-0-1-1-0-0-0-0-1-1-0-0-0-0-0-0-0-0-0-0-0-0-1-0-1-0-0-1-0
51
5228 bits received 0110000110000000000001010010 (bin) or 6180052 (hex)
53
54--------------------
55
56The 28 bits consist of 20 bits of payload and 8 bits of checksum.
57
58In order of transmission the payload consists of the following fields:
59
60Bits Description
6112 ID of the thermostat. A Digistat receiver will only respond to a packet if its ID
62 matches the ID that the receiver has previously learnt.
634 Pairing field, normally 0000. The first packets sent by the thermostat after its
64 batteries are inserted have this field set to 0001, which lets a Digistat receiver
65 in pairing mode learn the ID of the thermostat.
661 Thermostat on (1) or off (0).
671 Always 0.
681 Low battery (1) or battery OK (0).
691 Always 0.
70
71The YAML/lambda 'address' is the first 16 bits as sent (the 12 bit ID followed by the
72pairing field, so the pairing flag is the address LSB), and 'data' is the next 4 bits
73(0x8 = on, 0x2 = low battery).
74
75Checksum: see calc_cs(); the +1 when the sum is above 0x7F is empirical. The captured
76packets above give 0x32 (On) and 0x52 (Off). Packets that fail it are logged at verbose level.
77
78Legacy split (from RFLink): address (16), command (7: data + top 3 checksum bits), channel (5: low checksum bits).
79When channel and command are set they are sent as is, with no checksum calculated.
80
81*/
82
83static uint8_t calc_cs(uint32_t out_data) {
84 uint16_t cs = 0;
85 uint32_t rev = reverse_bits(out_data);
86
87 for (uint8_t i = 4; i <= 20; i += 4) {
88 cs <<= 1;
89 cs += (rev >> i) & 0x0F;
90 }
91
92 if (cs > 0x7F) {
93 cs += 1;
94 }
95 // Sent LSB first
96 return reverse_bits(uint8_t(cs << 1));
97}
98
100 uint16_t khz = CARRIER_KHZ;
101 dst->set_carrier_frequency(khz * 1000);
102
103 // Preamble = 101010101010
104 uint32_t out_data = 0x0AAA;
105 for (uint32_t mask = 1UL << (NBITS_PREAMBLE - 1); mask != 0; mask >>= 1) {
106 if (out_data & mask) {
107 dst->mark(BIT_TIME_US);
108 } else {
109 dst->space(BIT_TIME_US);
110 }
111 }
112
113 // Sync = 1100
114 out_data = 0x000C;
115 for (uint32_t mask = 1UL << (NBITS_SYNC - 1); mask != 0; mask >>= 1) {
116 if (out_data & mask) {
117 dst->mark(BIT_TIME_US);
118 } else {
119 dst->space(BIT_TIME_US);
120 }
121 }
122
123 if (data.channel || data.command) {
124 out_data = data.address;
125 out_data <<= NBITS_COMMAND;
126 out_data |= data.command;
127 out_data <<= NBITS_CHANNEL;
128 out_data |= data.channel;
129 } else {
130 out_data = data.address << (NBITS_DATA + NBITS_CHECKSUM);
131 out_data |= (data.data & ((1u << NBITS_DATA) - 1)) << NBITS_CHECKSUM; // a lambda may exceed 4 bits
132 out_data |= calc_cs(out_data);
133 }
134 ESP_LOGD(TAG, "addr=%04x data=%x channel=%02x command=%02x", data.address, data.data, data.channel, data.command);
135 ESP_LOGV(TAG, "out_data %07" PRIx32, out_data);
136
137 for (uint32_t mask = 1UL << (NBITS_PKT - 1); mask != 0; mask >>= 1) {
138 if (out_data & mask) {
139 dst->mark(BIT_TIME_US);
140 dst->space(BIT_TIME_US);
141 } else {
142 dst->space(BIT_TIME_US);
143 dst->mark(BIT_TIME_US);
144 }
145 }
146}
147
150
151 while (src.size() - src.get_index() >= MIN_RX_SRC) {
152 // If first preamble item is a space, skip it
153 if (src.peek_space_at_least(1)) {
154 src.advance(1);
155 }
156
157 // Look for sync pulse, after. If sucessful index points to space of sync symbol
158 while (src.size() - src.get_index() >= MIN_RX_SRC) {
159 ESP_LOGVV(TAG, "sync search %" PRIu32 ", %" PRId32 " %" PRId32, src.size() - src.get_index(), src.peek(),
160 src.peek(1));
161 if (src.peek_mark(2 * BIT_TIME_US) &&
162 (src.peek_space(2 * BIT_TIME_US, 1) || src.peek_space(3 * BIT_TIME_US, 1))) {
163 src.advance(1);
164 ESP_LOGVV(TAG, "Found SYNC %" PRIu32, src.get_index());
165 break;
166 } else {
167 src.advance(2);
168 }
169 }
170
171 // No point continuing if not enough samples remaining to complete a packet
172 if (src.size() - src.get_index() < NBITS_PKT) {
173 ESP_LOGV(TAG, "Fail1 %" PRIu32, src.get_index());
174 break;
175 }
176
177 // Read data. Index points to space of sync symbol
178 // Extract first bit
179 // Checks next bit to leave index pointing correctly
180 uint32_t out_data = 0;
181 uint8_t bit = NBITS_PKT - 1;
182 if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) {
183 out_data |= 0 << bit;
184 } else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) &&
185 (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
186 out_data |= 1 << bit;
187 } else {
188 ESP_LOGVV(TAG, "Fail2 %" PRId32 " %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), src.peek(1));
189 continue;
190 }
191
192 // Before/after each bit is read the index points to the transition at the start of the bit period or,
193 // if there is no transition at the start of the bit period, then the transition in the middle of
194 // the previous bit period.
195 while (--bit >= 1) {
196 ESP_LOGVV(TAG, "Data %2d %08" PRIx32, bit, out_data);
197 if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) &&
198 (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) {
199 out_data |= 0 << bit;
200 } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) &&
201 (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) {
202 out_data |= 1 << bit;
203 } else {
204 break;
205 }
206 }
207
208 if (bit > 0) {
209 ESP_LOGVV(TAG, "Fail3 %" PRId32 " %" PRId32 " %" PRId32, src.peek(-1), src.peek(0), src.peek(1));
210 continue;
211 }
212
213 if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) {
214 out_data |= 0;
215 } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) {
216 out_data |= 1;
217 } else {
218 continue;
219 }
220
221 uint8_t cs = out_data & 0xFF;
222 uint8_t expected_cs = calc_cs(out_data);
223
224 if (cs != expected_cs) {
225 ESP_LOGV(TAG, "Dropped %07" PRIx32 ": checksum 0x%02x, expected 0x%02x", out_data, cs, expected_cs);
226 continue;
227 }
228
229 ESP_LOGV(TAG, "Data %07" PRIx32, out_data);
230
231 out.data = (out_data >> NBITS_CHECKSUM) & ((1u << NBITS_DATA) - 1);
232 out.address = out_data >> (NBITS_CHECKSUM + NBITS_DATA);
233 out.channel = out_data & ((1u << NBITS_CHANNEL) - 1);
234 out.command = (out_data >> NBITS_CHANNEL) & ((1u << NBITS_COMMAND) - 1);
235
236 return out;
237 }
238 return {};
239}
240
242 ESP_LOGI(TAG, "Received Drayton: address=0x%04X, data=0x%X", data.address, data.data);
243}
244
245} // namespace esphome::remote_base
optional< DraytonData > decode(RemoteReceiveData src)
void encode(RemoteTransmitData *dst, const DraytonData &data)
void dump(const DraytonData &data)
void set_carrier_frequency(uint32_t carrier_frequency)
Definition remote_base.h:64
std::vector< uint8_t > & out
Definition gif.cpp:49
ESPHOME_LOG_TAG(TAG, "remote.abbwelcome")
const void * src
Definition hal.h:68
uint8_t reverse_bits(uint8_t x)
Reverse the order of 8 bits.
Definition helpers.h:1081
static void uint32_t