diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 06b022c513..7a7681082e 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -5,6 +5,7 @@ from esphome.components import uart from esphome.const import ( CONF_ID, CONF_UART_ID, + CONF_RECEIVE_TIMEOUT, ) CODEOWNERS = ["@glmnet", "@zuidwijk"] @@ -52,7 +53,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_REQUEST_INTERVAL): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_REQUEST_INTERVAL, default="0ms" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_RECEIVE_TIMEOUT, default="200ms" + ): cv.positive_time_period_milliseconds, } ).extend(uart.UART_DEVICE_SCHEMA), cv.only_with_arduino, @@ -70,10 +76,8 @@ async def to_code(config): if CONF_REQUEST_PIN in config: request_pin = await cg.gpio_pin_expression(config[CONF_REQUEST_PIN]) cg.add(var.set_request_pin(request_pin)) - if CONF_REQUEST_INTERVAL in config: - cg.add( - var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds) - ) + cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds)) + cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID]) diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 03e418662a..7b339e5fe0 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -24,7 +24,7 @@ void Dsmr::loop() { if (this->decryption_key_.empty()) { this->receive_telegram_(); } else { - this->receive_encrypted_(); + this->receive_encrypted_telegram_(); } } } @@ -57,14 +57,42 @@ bool Dsmr::request_interval_reached_() { return millis() - this->last_request_time_ > this->request_interval_; } +bool Dsmr::receive_timeout_reached_() { return millis() - this->last_read_time_ > this->receive_timeout_; } + bool Dsmr::available_within_timeout_() { - uint8_t tries = READ_TIMEOUT_MS / 5; - while (tries--) { - delay(5); - if (this->available()) { - return true; + // Data are available for reading on the UART bus? + // Then we can start reading right away. + if (this->available()) { + this->last_read_time_ = millis(); + return true; + } + // When we're not in the process of reading a telegram, then there is + // no need to actively wait for new data to come in. + if (!header_found_) { + return false; + } + // A telegram is being read. The smart meter might not deliver a telegram + // in one go, but instead send it in chunks with small pauses in between. + // When the UART RX buffer cannot hold a full telegram, then make sure + // that the UART read buffer does not overflow while other components + // perform their work in their loop. Do this by not returning control to + // the main loop, until the read timeout is reached. + if (this->parent_->get_rx_buffer_size() < this->max_telegram_len_) { + while (!this->receive_timeout_reached_()) { + delay(5); + if (this->available()) { + this->last_read_time_ = millis(); + return true; + } } } + // No new data has come in during the read timeout? Then stop reading the + // telegram and start waiting for the next one to arrive. + if (this->receive_timeout_reached_()) { + ESP_LOGW(TAG, "Timeout while reading data for telegram"); + this->reset_telegram_(); + } + return false; } @@ -96,30 +124,31 @@ void Dsmr::stop_requesting_data_() { } } -void Dsmr::receive_telegram_() { - while (true) { - if (!this->available()) { - if (!this->header_found_ || !this->available_within_timeout_()) { - return; - } - } +void Dsmr::reset_telegram_() { + this->header_found_ = false; + this->footer_found_ = false; + this->bytes_read_ = 0; + this->crypt_bytes_read_ = 0; + this->crypt_telegram_len_ = 0; + this->last_read_time_ = 0; +} +void Dsmr::receive_telegram_() { + while (this->available_within_timeout_()) { const char c = this->read(); // Find a new telegram header, i.e. forward slash. if (c == '/') { ESP_LOGV(TAG, "Header of telegram found"); + this->reset_telegram_(); this->header_found_ = true; - this->footer_found_ = false; - this->telegram_len_ = 0; } if (!this->header_found_) continue; // Check for buffer overflow. - if (this->telegram_len_ >= this->max_telegram_len_) { - this->header_found_ = false; - this->footer_found_ = false; + if (this->bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); return; } @@ -129,9 +158,9 @@ void Dsmr::receive_telegram_() { // proper parsing, remove these new line characters. if (c == '(') { while (true) { - auto previous_char = this->telegram_[this->telegram_len_ - 1]; + auto previous_char = this->telegram_[this->bytes_read_ - 1]; if (previous_char == '\n' || previous_char == '\r') { - this->telegram_len_--; + this->bytes_read_--; } else { break; } @@ -139,8 +168,8 @@ void Dsmr::receive_telegram_() { } // Store the byte in the buffer. - this->telegram_[this->telegram_len_] = c; - this->telegram_len_++; + this->telegram_[this->bytes_read_] = c; + this->bytes_read_++; // Check for a footer, i.e. exlamation mark, followed by a hex checksum. if (c == '!') { @@ -152,28 +181,14 @@ void Dsmr::receive_telegram_() { if (this->footer_found_ && c == '\n') { // Parse the telegram and publish sensor values. this->parse_telegram(); - - this->header_found_ = false; + this->reset_telegram_(); return; } } } -void Dsmr::receive_encrypted_() { - this->encrypted_telegram_len_ = 0; - size_t packet_size = 0; - - while (true) { - if (!this->available()) { - if (!this->header_found_) { - return; - } - if (!this->available_within_timeout_()) { - ESP_LOGW(TAG, "Timeout while reading data for encrypted telegram"); - return; - } - } - +void Dsmr::receive_encrypted_telegram_() { + while (this->available_within_timeout_()) { const char c = this->read(); // Find a new telegram start byte. @@ -182,50 +197,58 @@ void Dsmr::receive_encrypted_() { continue; } ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); + this->reset_telegram_(); this->header_found_ = true; } // Check for buffer overflow. - if (this->encrypted_telegram_len_ >= this->max_telegram_len_) { - this->header_found_ = false; + if (this->crypt_bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); return; } - this->encrypted_telegram_[this->encrypted_telegram_len_++] = c; + // Store the byte in the buffer. + this->crypt_telegram_[this->crypt_bytes_read_] = c; + this->crypt_bytes_read_++; - if (packet_size == 0 && this->encrypted_telegram_len_ > 20) { + // Read the length of the incoming encrypted telegram. + if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { // Complete header + data bytes - packet_size = 13 + (this->encrypted_telegram_[11] << 8 | this->encrypted_telegram_[12]); - ESP_LOGV(TAG, "Encrypted telegram size: %d bytes", packet_size); + this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); + ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); } - if (this->encrypted_telegram_len_ == packet_size && packet_size > 0) { - ESP_LOGV(TAG, "End of encrypted telegram found"); - GCM *gcmaes128{new GCM()}; - gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); - // the iv is 8 bytes of the system title + 4 bytes frame counter - // system title is at byte 2 and frame counter at byte 15 - for (int i = 10; i < 14; i++) - this->encrypted_telegram_[i] = this->encrypted_telegram_[i + 4]; - constexpr uint16_t iv_size{12}; - gcmaes128->setIV(&this->encrypted_telegram_[2], iv_size); - gcmaes128->decrypt(reinterpret_cast(this->telegram_), - // the ciphertext start at byte 18 - &this->encrypted_telegram_[18], - // cipher size - this->encrypted_telegram_len_ - 17); - delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) - this->telegram_len_ = strnlen(this->telegram_, this->max_telegram_len_); - ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->telegram_len_); - ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); - - this->parse_telegram(); - - this->header_found_ = false; - this->telegram_len_ = 0; - return; + // Check for the end of the encrypted telegram. + if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { + continue; } + ESP_LOGV(TAG, "End of encrypted telegram found"); + + // Decrypt the encrypted telegram. + GCM *gcmaes128{new GCM()}; + gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); + // the iv is 8 bytes of the system title + 4 bytes frame counter + // system title is at byte 2 and frame counter at byte 15 + for (int i = 10; i < 14; i++) + this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; + constexpr uint16_t iv_size{12}; + gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); + gcmaes128->decrypt(reinterpret_cast(this->telegram_), + // the ciphertext start at byte 18 + &this->crypt_telegram_[18], + // cipher size + this->crypt_bytes_read_ - 17); + delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) + + this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); + ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); + ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); + + // Parse the decrypted telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; } } @@ -234,11 +257,11 @@ bool Dsmr::parse_telegram() { ESP_LOGV(TAG, "Trying to parse telegram"); this->stop_requesting_data_(); ::dsmr::ParseResult res = - ::dsmr::P1Parser::parse(&data, this->telegram_, this->telegram_len_, false, + ::dsmr::P1Parser::parse(&data, this->telegram_, this->bytes_read_, false, this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. if (res.err) { // Parsing error, show it - auto err_str = res.fullError(this->telegram_, this->telegram_ + this->telegram_len_); + auto err_str = res.fullError(this->telegram_, this->telegram_ + this->bytes_read_); ESP_LOGE(TAG, "%s", err_str.c_str()); return false; } else { @@ -251,7 +274,7 @@ bool Dsmr::parse_telegram() { void Dsmr::dump_config() { ESP_LOGCONFIG(TAG, "DSMR:"); ESP_LOGCONFIG(TAG, " Max telegram length: %d", this->max_telegram_len_); - + ESP_LOGCONFIG(TAG, " Receive timeout: %.1fs", this->receive_timeout_ / 1e3f); if (this->request_pin_ != nullptr) { LOG_PIN(" Request Pin: ", this->request_pin_); } @@ -270,9 +293,9 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) { if (decryption_key.length() == 0) { ESP_LOGI(TAG, "Disabling decryption"); this->decryption_key_.clear(); - if (this->encrypted_telegram_ != nullptr) { - delete[] this->encrypted_telegram_; - this->encrypted_telegram_ = nullptr; + if (this->crypt_telegram_ != nullptr) { + delete[] this->crypt_telegram_; + this->crypt_telegram_ = nullptr; } return; } @@ -293,13 +316,11 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) { this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); } - if (this->encrypted_telegram_ == nullptr) { - this->encrypted_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT + if (this->crypt_telegram_ == nullptr) { + this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT } } -void Dsmr::set_max_telegram_length(size_t length) { max_telegram_len_ = length; } - } // namespace dsmr } // namespace esphome diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 0430eb93ed..db0bf95ca1 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -16,8 +16,6 @@ namespace esphome { namespace dsmr { -static constexpr uint32_t READ_TIMEOUT_MS = 200; - using namespace ::dsmr::fields; // DSMR_**_LIST generated by ESPHome and written in esphome/core/defines @@ -71,11 +69,10 @@ class Dsmr : public Component, public uart::UARTDevice { void dump_config() override; void set_decryption_key(const std::string &decryption_key); - - void set_max_telegram_length(size_t length); - + void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } + void set_receive_timeout(uint32_t timeout) { this->receive_timeout_ = timeout; } // Sensor setters #define DSMR_SET_SENSOR(s) \ @@ -88,7 +85,8 @@ class Dsmr : public Component, public uart::UARTDevice { protected: void receive_telegram_(); - void receive_encrypted_(); + void receive_encrypted_telegram_(); + void reset_telegram_(); /// Wait for UART data to become available within the read timeout. /// @@ -101,24 +99,26 @@ class Dsmr : public Component, public uart::UARTDevice { /// lost in the process. bool available_within_timeout_(); - // Data request + // Request telegram + uint32_t request_interval_; + bool request_interval_reached_(); GPIOPin *request_pin_{nullptr}; - uint32_t request_interval_{0}; uint32_t last_request_time_{0}; bool requesting_data_{false}; bool ready_to_request_data_(); - bool request_interval_reached_(); void start_requesting_data_(); void stop_requesting_data_(); - // Telegram buffer + // Read telegram + uint32_t receive_timeout_; + bool receive_timeout_reached_(); size_t max_telegram_len_; char *telegram_{nullptr}; - int telegram_len_{0}; - uint8_t *encrypted_telegram_{nullptr}; - int encrypted_telegram_len_{0}; - - // Serial parser + int bytes_read_{0}; + uint8_t *crypt_telegram_{nullptr}; + size_t crypt_telegram_len_{0}; + int crypt_bytes_read_{0}; + uint32_t last_read_time_{0}; bool header_found_{false}; bool footer_found_{false}; diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 73694d3db7..42702cf5b8 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -52,6 +52,7 @@ class UARTComponent { void set_tx_pin(InternalGPIOPin *tx_pin) { this->tx_pin_ = tx_pin; } void set_rx_pin(InternalGPIOPin *rx_pin) { this->rx_pin_ = rx_pin; } void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } + size_t get_rx_buffer_size() { return this->rx_buffer_size_; } void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } uint8_t get_stop_bits() const { return this->stop_bits_; } diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp index 9a535656a2..e2d92eac60 100644 --- a/esphome/components/uart/uart_debugger.cpp +++ b/esphome/components/uart/uart_debugger.cpp @@ -90,6 +90,11 @@ void UARTDummyReceiver::loop() { } } +// In the upcoming log functions, a delay was added after all log calls. +// This is done to allow the system to ship the log lines via the API +// TCP connection(s). Without these delays, debug log lines could go +// missing when UART devices block the main loop for too long. + void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uint8_t separator) { std::string res; if (direction == UART_DIRECTION_RX) { @@ -107,6 +112,7 @@ void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uin res += buf; } ESP_LOGD(TAG, "%s", res.c_str()); + delay(10); } void UARTDebug::log_string(UARTDirection direction, std::vector bytes) { @@ -150,6 +156,7 @@ void UARTDebug::log_string(UARTDirection direction, std::vector bytes) } res += '"'; ESP_LOGD(TAG, "%s", res.c_str()); + delay(10); } void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uint8_t separator) { @@ -167,6 +174,7 @@ void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uin res += to_string(bytes[i]); } ESP_LOGD(TAG, "%s", res.c_str()); + delay(10); } void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, uint8_t separator) { @@ -186,6 +194,7 @@ void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, res += buf; } ESP_LOGD(TAG, "%s", res.c_str()); + delay(10); } } // namespace uart diff --git a/tests/test3.yaml b/tests/test3.yaml index 61c7a3dcad..8ae4a383e0 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1313,6 +1313,7 @@ dsmr: max_telegram_length: 1000 request_pin: D5 request_interval: 20s + receive_timeout: 100ms daly_bms: update_interval: 20s