ESPHome 2026.9.0
Loading...
Searching...
No Matches
ufm01.cpp
Go to the documentation of this file.
1#include "ufm01.h"
2#include "esphome/core/hal.h"
4#include "esphome/core/log.h"
5
6#include <array>
7#include <cinttypes>
8#include <cstring>
9
10namespace esphome::ufm01 {
11
12static const char *const TAG = "ufm01";
13
14static constexpr uint8_t COMMAND_ACK = 0xE5;
15static constexpr uint32_t COMMAND_ACK_TIMEOUT_MS = 500;
16static constexpr uint32_t STARTUP_DELAY_MS = 2000;
17static constexpr uint32_t POST_RESET_DELAY_MS = 2000;
18static constexpr uint32_t RESET_RETRY_DELAY_MS = 800;
19static constexpr uint32_t STARTUP_RETRY_MS = 3000;
20static constexpr uint32_t PASSIVE_POLL_INTERVAL_MS = 1000;
21static constexpr uint32_t ACTIVE_STALE_MS = 5000;
22static constexpr uint32_t PASSIVE_READ_TIMEOUT_MS = 1000;
23static constexpr uint32_t ACTIVE_FRAME_TIMEOUT_MS = 3000;
24
25static constexpr float L_PER_M3 = 1000.0f;
26static constexpr float M3_PER_L = 1.0f / L_PER_M3;
27
28static constexpr std::array<uint8_t, 7> ACTIVE_MODE = {0xFE, 0xFE, 0x11, 0x5C, 0x00, 0x5C, 0x16};
29static constexpr std::array<uint8_t, 7> CLEAR_ACCUMULATED_FLOW = {0xFE, 0xFE, 0x11, 0x5A, 0xFD, 0x57, 0x16};
30static constexpr std::array<uint8_t, 7> RESET_DEVICE = {0xFE, 0xFE, 0x11, 0x5D, 0xCB, 0x28, 0x16};
31static constexpr std::array<uint8_t, 7> READ_SENSOR_DATA_NO_ID = {0xFE, 0xFE, 0x11, 0x5B, 0x0F, 0x6A, 0x16};
32
33// Active-mode frame layout (datasheet Table 7)
34static constexpr size_t FRAME_CHECKSUM_INDEX = 30;
35static constexpr size_t FRAME_STOP_INDEX = 31;
36static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
37static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
38static constexpr uint8_t PASSIVE_START_BYTE_2 = 0x64;
39static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
40static constexpr uint8_t FRAME_INDEX_INSTANT_FLOW_FLAG = 15;
41static constexpr uint8_t FRAME_INDEX_RESERVED_SECTION = 21;
42static constexpr uint8_t FRAME_INDEX_TEMP_FLAG = 24;
43static constexpr uint8_t FRAME_FLAG_INSTANT_FLOW = 0x0B;
44static constexpr uint8_t FRAME_FLAG_RESERVED_SECTION = 0x0C;
45static constexpr uint8_t FRAME_FLAG_TEMP = 0x0D;
46
47// Measurement decoding
48static constexpr uint8_t FRAME_ACC_FLOW_FLAG_INDEX = 8;
49static constexpr uint8_t ACC_FLOW_M3_FLAG = 0x1A;
50static constexpr uint8_t FRAME_FLOW_SIGN_INDEX = 20;
51static constexpr uint8_t FLOW_NEGATIVE_SIGN = 0x80;
52
53// Status bytes (datasheet ST1 / ST2)
54static constexpr uint8_t FRAME_ST1_INDEX = 28;
55static constexpr uint8_t FRAME_ST2_INDEX = 29;
56static constexpr uint8_t ST1_EMPTY_TUBE_MASK = 0x20;
57static constexpr uint8_t ST2_UFC_ERROR_MASK = 0x20;
58static constexpr uint8_t ST2_FLOW_DIRECTION_WRONG_MASK = 0x08;
59static constexpr uint8_t ST2_FLOW_RATE_OUT_OF_RANGE_MASK = 0x04;
60
61static float to_float(uint8_t data) { return (data >> 4) * 10 + (data & 0x0F); }
62
63static bool check_byte(const uint8_t data[FRAME_SIZE], size_t index, uint8_t expected, const char *name) {
64 if (data[index] == expected)
65 return true;
66 ESP_LOGW(TAG, "%s (byte %zu) - expected 0x%02X, but was 0x%02X", name, index, expected, data[index]);
67 return false;
68}
69
70static bool validate_active_frame(const uint8_t data[FRAME_SIZE]) {
71 uint8_t sum = 0;
72 for (size_t i = 0; i < FRAME_CHECKSUM_INDEX; ++i)
73 sum += data[i];
74 return check_byte(data, 0, FRAME_START_BYTE_1, "start byte 1") &&
75 check_byte(data, 1, FRAME_START_BYTE_2, "start byte 2") &&
76 check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
77 check_byte(data, FRAME_INDEX_RESERVED_SECTION, FRAME_FLAG_RESERVED_SECTION, "reserved section flag") &&
78 check_byte(data, FRAME_INDEX_TEMP_FLAG, FRAME_FLAG_TEMP, "temperature flag") &&
79 check_byte(data, FRAME_CHECKSUM_INDEX, sum, "checksum") &&
80 check_byte(data, FRAME_STOP_INDEX, FRAME_STOP_BYTE, "stop byte");
81}
82
83static bool validate_passive_frame(const uint8_t data[PASSIVE_FRAME_SIZE]) {
84 if (data[0] != FRAME_START_BYTE_1 || data[1] != PASSIVE_START_BYTE_2 || data[22] != FRAME_STOP_BYTE)
85 return false;
86 uint8_t sum = 0;
87 for (size_t i = 0; i < 21; ++i)
88 sum += data[i];
89 return data[21] == (sum & 0xFF);
90}
91
92static void passive_no_id_to_active_frame(const uint8_t passive[PASSIVE_FRAME_SIZE], uint8_t active[FRAME_SIZE]) {
93 std::memset(active, 0, FRAME_SIZE);
94 active[0] = FRAME_START_BYTE_1;
95 active[1] = FRAME_START_BYTE_2;
96 active[7] = 0x01;
97 active[8] = passive[2];
98 for (size_t i = 0; i < 6; ++i)
99 active[9 + i] = passive[3 + i];
100 active[15] = passive[9];
101 for (size_t i = 0; i < 5; ++i)
102 active[16 + i] = passive[10 + i];
103 active[21] = FRAME_FLAG_RESERVED_SECTION;
104 active[24] = passive[15];
105 for (size_t i = 0; i < 3; ++i)
106 active[25 + i] = passive[16 + i];
107 active[28] = passive[19];
108 active[29] = passive[20];
109 active[30] = passive[21];
110 active[31] = FRAME_STOP_BYTE;
111}
112
113static float read_accumulated_flow(const uint8_t data[FRAME_SIZE]) {
114 return (data[FRAME_ACC_FLOW_FLAG_INDEX] == ACC_FLOW_M3_FLAG ? L_PER_M3 : 1.0f) *
115 (to_float(data[14]) * 10000000.0f + to_float(data[13]) * 100000.0f + to_float(data[12]) * 1000.0f +
116 to_float(data[11]) * 10.0f + to_float(data[10]) * 0.1f + to_float(data[9]) * 0.001f);
117}
118
119static float read_flow(const uint8_t data[FRAME_SIZE]) {
120 return (data[FRAME_FLOW_SIGN_INDEX] == FLOW_NEGATIVE_SIGN ? -1.0f : 1.0f) *
121 (to_float(data[19]) * 10000.0f + to_float(data[18]) * 100.0f + to_float(data[17]) +
122 to_float(data[16]) * 0.01f) *
123 M3_PER_L;
124}
125
126static void log_hex(const uint8_t *data, size_t len) {
127 char hex_buf[format_hex_pretty_size(FRAME_SIZE)];
128 ESP_LOGD(TAG, "%s", format_hex_pretty_to(hex_buf, data, len, ' '));
129}
130
131static float read_temperature(const uint8_t data[FRAME_SIZE]) {
132 // happens sometimes before getting a real reading
133 if (data[27] == 0x00 && (data[26] == 0x00 || data[26] == 0x70) && data[25] == 0x00) {
134 return NAN;
135 }
136 return to_float(data[27]) * 100.0f + to_float(data[26]) + to_float(data[25]) * 0.01f;
137}
138
139static bool read_ufc_chip_error(const uint8_t data[FRAME_SIZE]) { return data[FRAME_ST2_INDEX] & ST2_UFC_ERROR_MASK; }
140
141static bool read_flow_direction_wrong(const uint8_t data[FRAME_SIZE]) {
142 return data[FRAME_ST2_INDEX] & ST2_FLOW_DIRECTION_WRONG_MASK;
143}
144
145static bool read_empty_tube(const uint8_t data[FRAME_SIZE]) { return data[FRAME_ST1_INDEX] & ST1_EMPTY_TUBE_MASK; }
146
147static bool read_flow_rate_out_of_range(const uint8_t data[FRAME_SIZE]) {
148 return data[FRAME_ST2_INDEX] & ST2_FLOW_RATE_OUT_OF_RANGE_MASK;
149}
150
151void UFM01Component::flush_rx_() {
152 while (this->available()) {
153 uint8_t byte;
154 this->read_byte(&byte);
155 }
156 this->read_index_ = 0;
157}
158
159void UFM01Component::send_command_no_wait_(const std::array<uint8_t, 7> &command) {
160 this->flush_rx_();
161 this->write_array(command);
162 this->flush();
163}
164
165// Drains whatever is currently in the RX buffer, looking for a command ACK.
166bool UFM01Component::consume_ack_() {
167 while (this->available()) {
168 uint8_t byte;
169 if (!this->read_byte(&byte))
170 return false;
171 if (byte == COMMAND_ACK)
172 return true;
173 ESP_LOGV(TAG, "Unexpected byte while waiting for command ACK: 0x%02X", byte);
174 }
175 return false;
176}
177
178bool UFM01Component::send_command_(const std::array<uint8_t, 7> &command) {
179 this->send_command_no_wait_(command);
180 const uint32_t start = millis();
181 while (millis() - start < COMMAND_ACK_TIMEOUT_MS) {
182 if (this->consume_ack_())
183 return true;
184 delay(1);
185 }
186 return false;
187}
188
189bool UFM01Component::reset_device_() { return this->send_command_(RESET_DEVICE); }
190
191bool UFM01Component::clear_accumulated_flow_() { return this->send_command_(CLEAR_ACCUMULATED_FLOW); }
192
193bool UFM01Component::set_active_mode_() { return this->send_command_(ACTIVE_MODE); }
194
195float UFM01Component::get_setup_priority() const { return setup_priority::LATE; }
196
198 ESP_LOGI(TAG, "Setting up UFM-01...");
199 this->startup_wait_ms_ = STARTUP_DELAY_MS;
200 this->set_startup_phase_(StartupPhase::WAIT);
201}
202
203void UFM01Component::dump_config() {
204 ESP_LOGCONFIG(TAG, "UFM-01:");
205#ifdef USE_SENSOR
206 LOG_SENSOR(" ", "Accumulated Flow", this->accumulated_flow_sensor_);
207 LOG_SENSOR(" ", "Flow", this->flow_sensor_);
208 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
209#endif
210#ifdef USE_BINARY_SENSOR
211 LOG_BINARY_SENSOR(" ", "UFC Chip Error", this->ufc_chip_error_binary_sensor_);
212 LOG_BINARY_SENSOR(" ", "Flow Direction Wrong", this->flow_direction_wrong_binary_sensor_);
213 LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_);
214 LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_);
215#endif
216}
217
218void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE]) {
219 bool empty_tube = read_empty_tube(data);
220#ifdef USE_BINARY_SENSOR
221 if (this->ufc_chip_error_binary_sensor_ != nullptr)
222 this->ufc_chip_error_binary_sensor_->publish_state(read_ufc_chip_error(data));
223 if (this->flow_direction_wrong_binary_sensor_ != nullptr)
224 this->flow_direction_wrong_binary_sensor_->publish_state(read_flow_direction_wrong(data));
225 if (this->empty_tube_binary_sensor_ != nullptr)
226 this->empty_tube_binary_sensor_->publish_state(empty_tube);
227 if (this->flow_rate_out_of_range_binary_sensor_ != nullptr)
228 this->flow_rate_out_of_range_binary_sensor_->publish_state(read_flow_rate_out_of_range(data));
229#endif
230
231#ifdef USE_SENSOR
232 // Total volume remains valid when the tube is dry; flow and temperature are not.
233 if (this->accumulated_flow_sensor_ != nullptr)
234 this->accumulated_flow_sensor_->publish_state(read_accumulated_flow(data));
235
236 if (empty_tube) {
237 if (this->flow_sensor_ != nullptr)
238 this->flow_sensor_->publish_state(NAN);
239 if (this->temperature_sensor_ != nullptr)
240 this->temperature_sensor_->publish_state(NAN);
241 } else {
242 if (this->flow_sensor_ != nullptr)
243 this->flow_sensor_->publish_state(read_flow(data));
244 if (this->temperature_sensor_ != nullptr)
245 this->temperature_sensor_->publish_state(read_temperature(data));
246 }
247#endif
248 this->last_valid_frame_ms_ = millis();
249 this->status_clear_warning();
250 this->status_clear_error();
251}
252
253bool UFM01Component::process_active_stream_() {
254 bool got_valid_frame = false;
255
256 while (this->available()) {
257 if (!this->read_byte(&this->data_[this->read_index_])) {
258 ESP_LOGW(TAG, "unable to read byte");
259 this->read_index_ = 0;
260 continue;
261 }
262 if ((this->read_index_ == 0 && this->data_[0] != FRAME_START_BYTE_1) ||
263 (this->read_index_ == 1 && this->data_[1] != FRAME_START_BYTE_2)) {
264 ESP_LOGD(TAG, "not start of data at %d (is 0x%02X)", this->read_index_, this->data_[this->read_index_]);
265 this->read_index_ = 0;
266 continue;
267 }
268 if (++this->read_index_ < static_cast<int32_t>(FRAME_SIZE))
269 continue;
270
271 if (validate_active_frame(this->data_)) {
272 this->on_active_frame_(this->data_);
273 this->read_index_ = 0;
274 got_valid_frame = true;
275 continue;
276 }
277
278 log_hex(this->data_, sizeof(this->data_));
279 ESP_LOGW(TAG, "unable to read data");
280 for (int32_t i = 2;
281 i < static_cast<int32_t>(FRAME_STOP_INDEX) && this->read_index_ == static_cast<int32_t>(FRAME_SIZE); ++i) {
282 if ((this->data_[i] == FRAME_START_BYTE_1) && (this->data_[i + 1] == FRAME_START_BYTE_2)) {
283 for (int32_t j = i; j < static_cast<int32_t>(FRAME_SIZE); ++j)
284 this->data_[j - i] = this->data_[j];
285 this->read_index_ = static_cast<int32_t>(FRAME_SIZE) - i;
286 }
287 }
288 if (this->read_index_ == static_cast<int32_t>(FRAME_SIZE))
289 this->read_index_ = 0;
290 }
291
292 return got_valid_frame;
293}
294
295void UFM01Component::set_startup_phase_(StartupPhase phase) {
296 this->startup_phase_ = phase;
297 this->phase_start_ms_ = millis();
298}
299
300void UFM01Component::enter_active_stream_(const char *reason) {
301 ESP_LOGI(TAG, "UFM-01 active stream %s", reason);
302 this->operating_mode_ = OperatingMode::ACTIVE_STREAM;
303 this->passive_read_pending_ = false;
304}
305
306void UFM01Component::start_passive_read_() {
307 this->send_command_no_wait_(READ_SENSOR_DATA_NO_ID);
308 this->passive_index_ = 0;
309 this->passive_start_ms_ = millis();
310}
311
312// Accumulates the reply to a passive read request across loop iterations.
313PassiveReadResult UFM01Component::continue_passive_read_() {
314 while (this->available() && this->passive_index_ < PASSIVE_FRAME_SIZE) {
315 uint8_t byte;
316 if (!this->read_byte(&byte))
317 break;
318
319 if (this->passive_index_ == 0 && byte != FRAME_START_BYTE_1)
320 continue;
321 if (this->passive_index_ == 1 && byte != PASSIVE_START_BYTE_2) {
322 // The mismatched byte may itself be the start of the real frame
323 this->passive_index_ = (byte == FRAME_START_BYTE_1) ? 1 : 0;
324 continue;
325 }
326 this->passive_frame_[this->passive_index_++] = byte;
327 }
328
329 if (this->passive_index_ < PASSIVE_FRAME_SIZE) {
330 if (millis() - this->passive_start_ms_ < PASSIVE_READ_TIMEOUT_MS)
332 ESP_LOGD(TAG, "passive read timeout (%zu/%zu bytes)", this->passive_index_, PASSIVE_FRAME_SIZE);
334 }
335
336 if (!validate_passive_frame(this->passive_frame_)) {
337 log_hex(this->passive_frame_, PASSIVE_FRAME_SIZE);
338 ESP_LOGW(TAG, "invalid passive frame");
340 }
341
342 uint8_t active_frame[FRAME_SIZE];
343 passive_no_id_to_active_frame(this->passive_frame_, active_frame);
344 this->on_active_frame_(active_frame);
346}
347
348void UFM01Component::loop_startup_() {
349 const uint32_t elapsed = millis() - this->phase_start_ms_;
350
351 switch (this->startup_phase_) {
353 // Pick up an already-streaming device without resetting it
354 if (this->process_active_stream_()) {
355 this->enter_active_stream_("started");
356 return;
357 }
358 if (elapsed < this->startup_wait_ms_)
359 return;
360 ESP_LOGD(TAG, "Running startup sequence");
361 this->status_set_warning("initializing UFM-01");
362 this->reset_retried_ = false;
363 this->send_command_no_wait_(RESET_DEVICE);
364 this->set_startup_phase_(StartupPhase::RESET_WAIT_ACK);
365 return;
366
368 if (this->consume_ack_()) {
369 this->set_startup_phase_(StartupPhase::POST_RESET_WAIT);
370 return;
371 }
372 if (elapsed < COMMAND_ACK_TIMEOUT_MS)
373 return;
374 if (!this->reset_retried_) {
375 ESP_LOGW(TAG, "Reset not acknowledged, retrying in %" PRIu32 " ms", RESET_RETRY_DELAY_MS);
376 this->set_startup_phase_(StartupPhase::RESET_RETRY_WAIT);
377 } else {
378 ESP_LOGW(TAG, "Reset failed during startup");
379 this->set_startup_phase_(StartupPhase::POST_RESET_WAIT);
380 }
381 return;
382
384 if (elapsed < RESET_RETRY_DELAY_MS)
385 return;
386 this->reset_retried_ = true;
387 this->send_command_no_wait_(RESET_DEVICE);
388 this->set_startup_phase_(StartupPhase::RESET_WAIT_ACK);
389 return;
390
392 if (elapsed < POST_RESET_DELAY_MS)
393 return;
394 this->send_command_no_wait_(ACTIVE_MODE);
395 this->set_startup_phase_(StartupPhase::ACTIVE_WAIT_FRAME);
396 return;
397
399 // The command ACK (0xE5) is consumed by the frame parser as noise
400 if (this->process_active_stream_()) {
401 this->enter_active_stream_("started");
402 return;
403 }
404 if (elapsed < ACTIVE_FRAME_TIMEOUT_MS)
405 return;
406 this->start_passive_read_();
407 this->set_startup_phase_(StartupPhase::PASSIVE_WAIT_REPLY);
408 return;
409
411 switch (this->continue_passive_read_()) {
413 return;
415 ESP_LOGI(TAG, "UFM-01 using passive polling");
416 this->operating_mode_ = OperatingMode::PASSIVE_POLL;
417 this->passive_read_pending_ = false;
418 this->last_poll_ms_ = millis();
419 return;
421 ESP_LOGW(TAG, "Startup failed, retrying in %" PRIu32 " ms", STARTUP_RETRY_MS);
422 this->startup_wait_ms_ = STARTUP_RETRY_MS;
423 this->set_startup_phase_(StartupPhase::WAIT);
424 return;
425 }
426 }
427}
428
429void UFM01Component::loop_active_stream_() {
430 this->process_active_stream_();
431 if (this->last_valid_frame_ms_ != 0 && millis() - this->last_valid_frame_ms_ > ACTIVE_STALE_MS) {
432 ESP_LOGW(TAG, "Active stream stale, switching to passive polling");
433 this->operating_mode_ = OperatingMode::PASSIVE_POLL;
434 this->passive_read_pending_ = false;
435 this->last_poll_ms_ = 0;
436 this->status_set_warning("UFM-01 passive poll");
437 }
438}
439
440void UFM01Component::loop_passive_poll_() {
441 if (this->passive_read_pending_) {
442 const PassiveReadResult result = this->continue_passive_read_();
444 return;
445 this->passive_read_pending_ = false;
447 this->status_set_warning("UFM-01 passive poll failed");
448 return;
449 }
450
451 if (this->process_active_stream_()) {
452 this->enter_active_stream_("resumed");
453 return;
454 }
455
456 if (millis() - this->last_poll_ms_ >= PASSIVE_POLL_INTERVAL_MS) {
457 this->last_poll_ms_ = millis();
458 this->start_passive_read_();
459 this->passive_read_pending_ = true;
460 }
461}
462
463void UFM01Component::loop() {
464 switch (this->operating_mode_) {
466 this->loop_startup_();
467 return;
469 this->loop_active_stream_();
470 return;
472 this->loop_passive_poll_();
473 return;
474 }
475}
476
477} // namespace esphome::ufm01
virtual void setup()
Where the component's initialization should happen.
Definition component.cpp:84
void status_clear_error()
Definition component.h:295
void status_clear_warning()
Definition component.h:289
UARTFlushResult flush()
Definition uart.h:49
bool read_byte(uint8_t *data)
Definition uart.h:35
void write_array(const uint8_t *data, size_t len)
Definition uart.h:27
constexpr float LATE
For components that should be initialized at the very end of the setup process.
Definition component.h:59
const void size_t len
Definition hal.h:64
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:425
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:1469
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t