ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
duco.cpp
Go to the documentation of this file.
1#include "duco.h"
2
4#include "esphome/core/log.h"
5
6namespace esphome::duco {
7
8ESPHOME_LOG_TAG(TAG, "duco");
9
10static constexpr uint8_t HEADER_START = 0xAA;
11static constexpr uint8_t HEADER_END = 0x55;
12static constexpr uint8_t STUFFING = 0x01; // follows an 0xAA inside a message, not part of it
13static constexpr uint32_t FRAME_GAP_MS = 50;
14
15static constexpr uint32_t DISCOVERY_START_ID = 0;
16static constexpr uint32_t DISCOVERY_STEP_ID = 1;
17static constexpr uint32_t DISCOVERY_START_MS = 16000;
18static constexpr uint32_t DISCOVERY_STEP_MS = 1600;
19static constexpr uint8_t DISCOVERY_MAX_ATTEMPTS = 3;
20
21void Duco::loop() {
23 // Read first: after a slow loop pass the rest of a message is already waiting, which is not silence
24 this->read_available([this, now](uint8_t byte) {
25 if (this->parse_byte_(byte)) {
26 this->last_byte_ = now;
27 } else {
28 this->reset_frame_();
29 }
30 });
31
32 // The last message of a burst has no header after it, so it ends on silence
33 if (this->rx_size_ != 0 && now - this->last_byte_ > FRAME_GAP_MS) {
34 this->finalize_message_();
35 this->reset_frame_();
36 }
37}
38
40 this->rx_size_ = 0;
41 this->escaped_ = false;
42}
43
45#ifdef DUCO_DEVICE_COUNT
46 for (auto &pending : this->pending_) {
47 if (pending.device == device) {
48 pending.function = 0;
49 pending.missed = false;
50 this->update_warning_();
51 return;
52 }
53 }
54#endif
55}
56
57bool Duco::parse_byte_(uint8_t byte) {
58 const size_t at = this->rx_size_;
59 if (at >= MAX_FRAME_SIZE) {
60 ESP_LOGV(TAG, "Message longer than %u bytes dropped", static_cast<unsigned>(MAX_FRAME_SIZE));
61 return false;
62 }
63 const uint8_t prev = at > 0 ? this->rx_buffer_[at - 1] : 0;
64
65 if (at == 1 && prev == HEADER_START && byte == HEADER_END)
66 return false; // header of the next message
67
68 if (at >= 2) {
69 const bool escaped = this->escaped_;
70 this->escaped_ = false;
71 if (prev == HEADER_START && !escaped) {
72 if (byte == STUFFING) {
73 this->escaped_ = true;
74 return true;
75 }
76 if (at >= 3 && byte == HEADER_END) {
77 this->rx_size_--; // drop the 0xAA of the header
78 this->finalize_message_();
79 return false;
80 }
81 }
82 }
83
84 this->rx_buffer_[this->rx_size_++] = byte;
85 return true;
86}
87
89 const uint8_t *raw = this->rx_buffer_.data();
90 // length (function, id and data), function, id, data, CRC low, CRC high
91 if (this->rx_size_ < 5 || raw[0] < 2 || this->rx_size_ < raw[0] + 3u) {
92 ESP_LOGV(TAG, "Malformed message dropped: %u bytes, length byte %u", this->rx_size_, raw[0]);
93 return;
94 }
95 const uint8_t length = raw[0];
96
97 const uint16_t computed_crc = crc16(raw, length + 1);
98 const uint16_t remote_crc = encode_uint16(raw[length + 2], raw[length + 1]);
99 if (computed_crc != remote_crc) {
100 if (!this->disable_crc_) {
101 ESP_LOGV(TAG, "CRC check failed: %04X != %04X", computed_crc, remote_crc);
102 return;
103 }
104 ESP_LOGV(TAG, "CRC check failed, ignored: %04X != %04X", computed_crc, remote_crc);
105 }
106
107#ifdef DUCO_DEVICE_COUNT
108 // The box first acknowledges a request with function + 1, then answers with function + 2
109 const DucoMessage message{raw[2], raw + 3, static_cast<uint8_t>(length - 2)};
110 for (auto &pending : this->pending_) {
111 if (pending.function != 0 && pending.id == message.id && pending.function + 2 == raw[1]) {
112 pending.function = 0;
113 pending.missed = false;
114 this->update_warning_();
115 pending.device->receive_response(message);
116 return;
117 }
118 }
119 // Acknowledgements (odd function codes) are expected and never match
120 if ((raw[1] & 1) == 0) {
121 ESP_LOGV(TAG, "Answer %02X with id %u matches no open request", raw[1], message.id);
122 }
123#endif
124}
125
126#ifdef DUCO_DEVICE_COUNT
128 for (const auto &pending : this->pending_) {
129 if (pending.missed) {
130 this->status_set_warning(LOG_STR("No answer from the box"));
131 return;
132 }
133 }
134 this->status_clear_warning();
135}
136#endif
137
139 ESP_LOGCONFIG(TAG,
140 "Duco:\n"
141 " CRC Disabled: %s",
142 YESNO(this->disable_crc_));
143}
144
145uint8_t Duco::next_id_() {
146 do {
147 this->last_id_++;
148 } while (this->last_id_ == HEADER_START || this->last_id_ == HEADER_END);
149 return this->last_id_;
150}
151
152void Duco::send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data) {
153 if (data.size() > MAX_REQUEST_DATA) {
154 ESP_LOGE(TAG, "Request of %u bytes is too long", static_cast<unsigned>(data.size()));
155 return;
156 }
157 // Not byte stuffed, as in the author's original and no capture shows a request with 0xAA. An 0xAA followed by any
158 // byte but 0x01 or 0x55 reads the same with or without unstuffing on the box, so only those rare pairs are
159 // unverified.
160 const size_t size = data.size();
161 const uint8_t id = this->next_id_();
162 uint8_t frame[5 + MAX_REQUEST_DATA + 2] = {HEADER_START, HEADER_END, static_cast<uint8_t>(size + 2), function, id};
163 std::copy_n(data.begin(), size, frame + 5);
164 const uint16_t crc = crc16(frame + 2, size + 3);
165 frame[size + 5] = crc & 0xFF;
166 frame[size + 6] = crc >> 8;
167 this->write_array(frame, size + 7);
168 this->flush();
169
170#ifdef DUCO_DEVICE_COUNT
171 for (auto &pending : this->pending_) {
172 if (pending.device == device) {
173 pending.missed = pending.function != 0;
174 if (pending.missed) {
175 ESP_LOGV(TAG, "No answer to request %02X with id %u", pending.function, pending.id);
176 this->update_warning_();
177 }
178 pending.id = id;
179 pending.function = function;
180 break;
181 }
182 }
183#endif
184
185 ESP_LOGV_HEX(TAG, frame + 2, size + 5, sizeof(frame), "Sent %s");
186}
187
188static const LogString *node_type_name(uint8_t type) {
189 switch (type) {
190 case 8:
191 return LOG_STR("UCBAT");
192 case 9:
193 return LOG_STR("UC");
194 case 10:
195 return LOG_STR("UCRH");
196 case 12:
197 return LOG_STR("UCCO2");
198 case 13:
199 return LOG_STR("VLV");
200 case 17:
201 return LOG_STR("BOX");
202 case 18:
203 return LOG_STR("SWITCH");
204 default:
205 return LOG_STR("UNKNOWN");
206 }
207}
208
210 // Give the box time to start before walking its node addresses
211 this->set_timeout(DISCOVERY_START_ID, DISCOVERY_START_MS, [this]() {
212 this->set_interval(DISCOVERY_STEP_ID, DISCOVERY_STEP_MS, [this]() { this->update(); });
213 });
214}
215
217 if (this->attempts_ == DISCOVERY_MAX_ATTEMPTS) {
218 ESP_LOGV(TAG, "No answer for node %u", this->next_node_);
219 this->next_node_++;
220 this->attempts_ = 0;
221 }
222 if (this->next_node_ >= NODE_COUNT) {
223 this->cancel_interval(DISCOVERY_STEP_ID);
224 this->parent_->close_request(this);
225 return;
226 }
227 // Without an answer by the next step the same node is asked again; a retry is not a missed answer
228 if (this->attempts_ > 0)
229 this->parent_->close_request(this);
230 ESP_LOGV(TAG, "Discovering node %u", this->next_node_);
231 this->attempts_++;
232 this->parent_->send(this, FUNCTION_NODE, {0x01, this->next_node_});
233}
234
236 // Only the latest attempt is open, so a late answer to an earlier one never arrives here
237 if (message.size >= 1 && this->next_node_ < NODE_COUNT) {
238 if (message.data[0] != 0) {
239 ESP_LOGV(TAG, "Found node %u: type %u", this->next_node_, message.data[0]);
240 }
241 this->node_types_[this->next_node_++] = message.data[0];
242 this->attempts_ = 0;
243 }
244}
245
247 ESP_LOGCONFIG(TAG, "Discovered nodes:");
248 for (uint8_t node = 0; node < NODE_COUNT; node++) {
249 const uint8_t type = this->node_types_[node];
250 if (type != 0) {
251 ESP_LOGCONFIG(TAG, " Node %u: type %u (%s)", node, type, LOG_STR_ARG(node_type_name(type)));
252 }
253 }
254 if (this->next_node_ < NODE_COUNT) {
255 ESP_LOGCONFIG(TAG, " Still discovering, at node %u", this->next_node_);
256 }
257}
258
259#ifdef USE_TIME
261 const ESPTime now = this->clock_->now();
262 if (!now.is_valid()) {
263 ESP_LOGV(TAG, "Time is not valid yet");
264 return;
265 }
266 const uint32_t ts = now.timestamp;
267 this->parent_->send(nullptr, FUNCTION_PARAMETER,
268 {0x05, static_cast<uint8_t>(ts), static_cast<uint8_t>(ts >> 8), static_cast<uint8_t>(ts >> 16),
269 static_cast<uint8_t>(ts >> 24), 0x00, 0x00, 0x00, 0x00});
270}
271#endif
272
273} // namespace esphome::duco
uint8_t raw[35]
Definition bl0939.h:0
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
bool cancel_interval(const char *name)
Cancel an interval function.
Definition component.cpp:92
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void set_interval(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a const char* name.
Definition component.cpp:88
void status_clear_warning()
Definition component.h:296
Every entity polls the box; deriving from PollingComponent shares its vtable.
Definition duco.h:81
void receive_response(const DucoMessage &message) override
Definition duco.cpp:235
std::array< uint8_t, NODE_COUNT > node_types_
Definition duco.h:97
static constexpr uint8_t NODE_COUNT
Definition duco.h:95
void setup() override
Definition duco.cpp:209
void dump_config() override
Definition duco.cpp:246
void update() override
Definition duco.cpp:216
uint8_t next_id_()
Definition duco.cpp:145
std::array< uint8_t, MAX_FRAME_SIZE > rx_buffer_
Definition duco.h:72
static constexpr size_t MAX_REQUEST_DATA
Definition duco.h:59
uint8_t last_id_
Definition duco.h:75
static constexpr size_t MAX_FRAME_SIZE
Definition duco.h:58
uint8_t rx_size_
Definition duco.h:74
void dump_config() override
Definition duco.cpp:138
uint32_t last_byte_
Definition duco.h:73
bool disable_crc_
Definition duco.h:77
void close_request(DucoDevice *device)
Forget device's open request, for a device that will not send again.
Definition duco.cpp:44
void send(DucoDevice *device, uint8_t function, std::initializer_list< uint8_t > data)
Send a request; its answer goes to device, or nowhere if nullptr.
Definition duco.cpp:152
void loop() override
Definition duco.cpp:21
void update_warning_()
Definition duco.cpp:127
StaticVector< DucoPendingRequest, DUCO_DEVICE_COUNT > pending_
Definition duco.h:70
void reset_frame_()
Definition duco.cpp:39
bool parse_byte_(uint8_t byte)
Definition duco.cpp:57
void finalize_message_()
Definition duco.cpp:88
void update() override
Definition duco.cpp:260
time::RealTimeClock * clock_
Definition duco.h:109
ESPTime now()
Get the time in the currently defined timezone.
UARTFlushResult flush()
Definition uart.h:96
bool read_available(F &&on_byte)
Same as read_available_chunks(), calling on_byte(uint8_t) once per byte.
Definition uart.h:77
void write_array(const uint8_t *data, size_t len)
Definition uart.h:30
const LogString * message
Definition component.cpp:35
uint16_t id
uint8_t type
ESPHOME_LOG_TAG(TAG, "duco")
uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout)
Calculate a CRC-16 checksum of data with size len.
Definition helpers.cpp:95
uint16_t size
Definition helpers.cpp:34
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:998
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
A more user-friendly version of struct tm from time.h.
Definition time.h:23
time_t timestamp
unix epoch time (seconds since UTC Midnight January 1, 1970)
Definition time.h:48
bool is_valid(bool check_day_of_week=true, bool check_day_of_year=true) const
Check if this ESPTime is valid (year >= 2019 and the requested fields are in range).
Definition time.h:82
A received message; data points into the receive buffer and is valid only during the callback.
Definition duco.h:22
const uint8_t * data
Definition duco.h:24