Merge pull request #27 from ElVit/heatpump/feature/uart-task-handling

Use threads to read UART message
This commit is contained in:
ElVit 2026-03-26 22:05:20 +01:00 committed by GitHub
commit 41b7588eea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 350 additions and 408 deletions

View File

@ -26,7 +26,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
board: [esp8266, esp32s2, esp32c3, full] board: [esp32s2, esp32c3, full]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@ -6,7 +6,7 @@
### What you need ### What you need
* ESPHome compatible microcontroller (e.g. ESP8266, ESP32, ESP32-S2, ESP32-C3, ...) * ESPHome compatible microcontroller (e.g. ESP32, ESP32-S2, ESP32-C3, ...)
* RJ12 cable (phone cable, RJ11 may also work) * RJ12 cable (phone cable, RJ11 may also work)
### Wiring ### Wiring

View File

@ -4,13 +4,28 @@
### What you need ### What you need
* ESPHome compatible microcontroller (e.g. ESP8266, ESP32, ESP32-S2, ESP32-C3, ...) * ESPHome compatible microcontroller (e.g. ESP32, ESP32-S2, ESP32-C3, ...)
* ADUM1201 Dual Channel Digital Magnetic Isolator * ADUM1201 Dual Channel Digital Magnetic Isolator
(to convert 5V UART signal from the heatpump to 3.3V UART signal of the ESP controller) (to convert 5V UART signal from the heatpump to 3.3V UART signal of the ESP controller)
* CN-CNT cable/connectors to Heatpump/CZ-TAW1 (see [Heishamon](https://github.com/Egyras/HeishaMon) github site for more information) * CN-CNT cable/connectors to Heatpump/CZ-TAW1 (see [Heishamon](https://github.com/heishamon/HeishaMon) github site for more information)
* For example: [S05B-XASK-1 JST Connector](https://a.aliexpress.com/_EvkmGVo) * For example: [S05B-XASK-1 JST Connector](https://a.aliexpress.com/_EvkmGVo)
* For example: [XAP-05V-1 5Pin Cable with Female to Female Connector](https://a.aliexpress.com/_ExPT82E) * For example: [XAP-05V-1 5Pin Cable with Female to Female Connector](https://a.aliexpress.com/_ExPT82E)
> [!IMPORTANT]
> The support for ESP8266 and the arduino framework is deprecated for this component.
> Since v0.0.8 this component uses threads to process the UART communication.
> But if you still want to use an ESP8266 controller or the arduino framework,
> please use the branch "heatpump/arduino".
>
> ```yaml
> external_components:
> - source:
> type: git
> url: https://github.com/ElVit/esphome_components
> ref: 'heatpump/arduino'
> components: [ panasonic_heatpump ]
> ```
### Wiring ### Wiring
![wiring_adum1201.png](../../prototypes/panasonic_heatpump/wiring_adum1201.png) ![wiring_adum1201.png](../../prototypes/panasonic_heatpump/wiring_adum1201.png)
@ -644,7 +659,7 @@ water_heater:
## Custom Entities (For Advanced Users) ## Custom Entities (For Advanced Users)
If you review the [ProtocolByteDecrypt.md](https://github.com/Egyras/HeishaMon/blob/master/ProtocolByteDecrypt.md) file you will find also some TOPs and SETs which are not implemented yet in heishamon. If you review the [ProtocolByteDecrypt.md](https://github.com/heishamon/HeishaMon/blob/main/ProtocolByteDecrypt.md) file you will find also some TOPs and SETs which are not implemented yet in heishamon.
They are usually marked as TOP (without a number). They are usually marked as TOP (without a number).
The nice part of ESPHome is that it is so highly customizeable. The nice part of ESPHome is that it is so highly customizeable.
So if you want some additional TOP or SET entities you can easily create your own. So if you want some additional TOP or SET entities you can easily create your own.
@ -664,7 +679,7 @@ sensor:
unit_of_measurement: °C unit_of_measurement: °C
lambda: |- lambda: |-
// get the requried byte // get the requried byte
int byte = my_heatpump->getResponseByte(46); int byte = my_heatpump->get_response_byte(46);
// a valid byte range is 0x00-0xFF // a valid byte range is 0x00-0xFF
// do not update if the byte is invalid // do not update if the byte is invalid
if (byte < 0) return {}; if (byte < 0) return {};
@ -678,7 +693,7 @@ text_sensor:
update_interval: 3s update_interval: 3s
lambda: |- lambda: |-
// get the requried byte // get the requried byte
int byte = my_heatpump->getResponseByte(9); int byte = my_heatpump->get_response_byte(9);
// a valid byte range is 0x00-0xFF // a valid byte range is 0x00-0xFF
// do not update if the byte is invalid // do not update if the byte is invalid
if (byte < 0) return {}; if (byte < 0) return {};
@ -703,5 +718,5 @@ After a power on the heatpump should respond to the requests.
## Sources ## Sources
:heart: A big THANKS to [Egyras](https://github.com/Egyras) and the work done on the repository [HeishaMon](https://github.com/Egyras/HeishaMon) for decoding the panasonic uart protocol and providing information to build hardware based on an ESP Chip. :heart: A big THANKS to [Egyras](https://github.com/Egyras), [IgorYbema](https://github.com/IgorYbema) and the work done on the repository [HeishaMon](https://github.com/heishamon/HeishaMon) for decoding the panasonic uart protocol and providing information to build hardware based on an ESP Chip.
:heart: Thanks to the whole home assistant community for sharing their knowlege and helping me to create this ESPHome component! :heart: Thanks to the whole home assistant community for sharing their knowlege and helping me to create this ESPHome component!

View File

@ -31,7 +31,7 @@ climate::ClimateTraits PanasonicHeatpumpClimate::traits() {
void PanasonicHeatpumpClimate::control(const climate::ClimateCall& call) { void PanasonicHeatpumpClimate::control(const climate::ClimateCall& call) {
if (call.get_mode().has_value()) { if (call.get_mode().has_value()) {
int byte6 = this->parent_->getResponseByte(6); int byte6 = this->parent_->get_response_byte(6);
if (byte6 >= 0) { if (byte6 >= 0) {
climate::ClimateMode new_mode = *call.get_mode(); climate::ClimateMode new_mode = *call.get_mode();
uint8_t newByte6 = this->setClimateMode(new_mode, (uint8_t)byte6); uint8_t newByte6 = this->setClimateMode(new_mode, (uint8_t)byte6);

View File

@ -4,13 +4,13 @@ namespace esphome {
namespace panasonic_heatpump { namespace panasonic_heatpump {
static const char* const TAG = "panasonic_heatpump"; static const char* const TAG = "panasonic_heatpump";
void PanasonicHelpers::log_uart_hex(UartLogDirection direction, const std::vector<uint8_t>& data, void PanasonicHelpers::write_uart_log(UartLogDirection direction, const std::vector<uint8_t>& data,
const char separator) { const char separator, bool logBytes) {
PanasonicHelpers::log_uart_hex(direction, &data[0], data.size(), separator); PanasonicHelpers::write_uart_log(direction, &data[0], data.size(), separator, logBytes);
} }
void PanasonicHelpers::log_uart_hex(UartLogDirection direction, const uint8_t* data, const size_t length, void PanasonicHelpers::write_uart_log(UartLogDirection direction, const uint8_t* data, const size_t length,
const char separator) { const char separator, bool logBytes) {
std::string logStr = ""; std::string logStr = "";
std::string msgDir = direction == UART_LOG_TX ? ">>>" : "<<<"; std::string msgDir = direction == UART_LOG_TX ? ">>>" : "<<<";
std::string msgType = direction == UART_LOG_TX ? "request" : "response"; std::string msgType = direction == UART_LOG_TX ? "request" : "response";
@ -31,6 +31,9 @@ void PanasonicHelpers::log_uart_hex(UartLogDirection direction, const uint8_t* d
ESP_LOGI(TAG, "%s %s[%i]", msgDir.c_str(), msgType.c_str(), length); ESP_LOGI(TAG, "%s %s[%i]", msgDir.c_str(), msgType.c_str(), length);
delay(10); delay(10);
if (!logBytes)
return;
logStr += byte_array_to_hex_string(data, length, separator); logStr += byte_array_to_hex_string(data, length, separator);
// Log in chunks to avoid ESP_LOG buffer overflow (https://developers.esphome.io/architecture/logging/). // Log in chunks to avoid ESP_LOG buffer overflow (https://developers.esphome.io/architecture/logging/).

View File

@ -18,8 +18,10 @@ enum UartLogDirection : uint8_t {
class PanasonicHelpers { class PanasonicHelpers {
public: public:
static void log_uart_hex(UartLogDirection direction, const std::vector<uint8_t>& data, const char separator); static void write_uart_log(UartLogDirection direction, const std::vector<uint8_t>& data, const char separator,
static void log_uart_hex(UartLogDirection direction, const uint8_t* data, const size_t length, const char separator); bool logBytes);
static void write_uart_log(UartLogDirection direction, const uint8_t* data, const size_t length, const char separator,
bool logBytes);
static std::string byte_array_to_hex_string(const std::vector<uint8_t>& data, const char separator); static std::string byte_array_to_hex_string(const std::vector<uint8_t>& data, const char separator);
static std::string byte_array_to_hex_string(const uint8_t* data, const size_t length, const char separator); static std::string byte_array_to_hex_string(const uint8_t* data, const size_t length, const char separator);
}; };

View File

@ -25,51 +25,75 @@ void PanasonicHeatpumpComponent::dump_config() {
void PanasonicHeatpumpComponent::setup() { void PanasonicHeatpumpComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up Panasonic Heatpump ..."); ESP_LOGCONFIG(TAG, "Setting up Panasonic Heatpump ...");
delay(10); // NOLINT
this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_EVEN, 8); this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
this->update(); this->response_queue_handle_ = xQueueCreate(8, sizeof(std::vector<uint8_t>*));
if (this->response_queue_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create response queue!");
this->mark_failed();
return;
}
this->request_queue_handle_ = xQueueCreate(8, sizeof(std::vector<uint8_t>*));
if (this->request_queue_handle_ == nullptr) {
ESP_LOGE(TAG, "Failed to create request queue!");
this->mark_failed();
return;
}
// Start task
xTaskCreatePinnedToCore(PanasonicHeatpumpComponent::uart_task, "uart_handler", 4096, this,
tskIDLE_PRIORITY + 1, // Low priority, important for single-core C3
&this->uart_task_handle_,
tskNO_AFFINITY // important for single-core C3
);
if (this->uart_client_ != nullptr) {
xTaskCreatePinnedToCore(PanasonicHeatpumpComponent::uart_client_task, "uart_client_handler", 4096, this,
tskIDLE_PRIORITY + 1, // Low priority, important for single-core C3
&this->uart_client_task_handle_,
tskNO_AFFINITY // important for single-core C3
);
}
if (this->uart_client_ != nullptr && this->uart_client_timeout_ < 100) {
ESP_LOGI(TAG, "Self polling disabled (uart_client_timeout_ < 100ms). Not sending initial request.");
return;
}
} }
void PanasonicHeatpumpComponent::update() { void PanasonicHeatpumpComponent::update() {
if (this->uart_client_ != nullptr) // Do not send polling requests if a uart client (CZ-TAW1) is configured and timeout is set too low.
if (this->uart_client_ != nullptr && this->uart_client_timeout_ < 100)
return; return;
this->next_request_ = this->send_extra_request_ ? RequestType::POLLING_EXTRA : RequestType::POLLING;
// If a uart client (CZ-TAW1) is configured, check if the last request from the client is too long ago.
// If so, send polling request to heatpump again.
if (this->uart_client_ != nullptr && !this->uart_client_timeout_exceeded_) {
if (millis() - this->last_client_request_time_ > uart_client_timeout_)
this->uart_client_timeout_exceeded_ = true;
else
return;
}
ESP_LOGD(TAG, "Queue polling request");
this->queue_request(build_message(PanasonicCommand::PollingMessage));
} }
void PanasonicHeatpumpComponent::loop() { void PanasonicHeatpumpComponent::loop() {
// Check if no request was sent for uart_client_timeout when uart_client is configured
if (this->uart_client_ != nullptr && this->uart_client_timeout_ > 100) {
uint32_t current_time = millis();
if (current_time - this->last_request_time_ >= this->uart_client_timeout_) {
this->next_request_ = RequestType::POLLING;
this->uart_client_timeout_exceeded_ = true;
}
}
switch (this->loop_state_) { switch (this->loop_state_) {
case LoopState::READ_RESPONSE: case LoopState::READ_RESPONSE: {
this->read_response(); switch (this->read_response()) {
this->loop_state_ = LoopState::CHECK_RESPONSE;
break;
case LoopState::CHECK_RESPONSE:
this->current_response_ = this->check_response(this->response_message_);
switch (this->current_response_) {
case ResponseType::UNKNOWN:
this->response_message_.clear();
this->loop_state_ = LoopState::SEND_REQUEST;
break;
case ResponseType::RECEIVING:
this->loop_state_ = LoopState::SEND_REQUEST;
break;
case ResponseType::STANDARD: case ResponseType::STANDARD:
this->loop_state_ = LoopState::PUBLISH_SENSOR; this->loop_state_ = LoopState::PUBLISH_SENSOR;
break; break;
case ResponseType::EXTRA: case ResponseType::EXTRA:
this->loop_state_ = LoopState::PUBLISH_EXTRA_SENSOR; this->loop_state_ = LoopState::PUBLISH_EXTRA_SENSOR;
break; break;
default:
this->loop_state_ = LoopState::SEND_REQUEST;
break;
}; };
break; break;
}
case LoopState::PUBLISH_SENSOR: case LoopState::PUBLISH_SENSOR:
for (auto* entity : this->sensors_) { for (auto* entity : this->sensors_) {
entity->publish_new_state(this->heatpump_default_message_); entity->publish_new_state(this->heatpump_default_message_);
@ -125,233 +149,226 @@ void PanasonicHeatpumpComponent::loop() {
this->loop_state_ = LoopState::SEND_REQUEST; this->loop_state_ = LoopState::SEND_REQUEST;
break; break;
case LoopState::SEND_REQUEST: case LoopState::SEND_REQUEST:
this->send_request(this->next_request_); this->send_request();
this->loop_state_ = LoopState::READ_REQUEST; // fallthrough
break;
case LoopState::READ_REQUEST:
this->read_request();
this->loop_state_ = LoopState::RESTART_LOOP;
break;
default: default:
this->loop_state_ = LoopState::READ_RESPONSE; this->loop_state_ = LoopState::READ_RESPONSE;
break; break;
}; };
} }
void PanasonicHeatpumpComponent::read_response() { void PanasonicHeatpumpComponent::uart_task(void* pvParameters) {
while (this->available()) { auto* self = static_cast<PanasonicHeatpumpComponent*>(pvParameters);
// Read each byte from heatpump and forward it directly to the client (CZ-TAW1) std::vector<uint8_t> rx_buffer;
this->read_byte(&byte_); rx_buffer.reserve(256);
if (this->uart_client_ != nullptr) {
this->uart_client_->write_byte(byte_);
}
// Message shall start with 0x31, 0x71 or 0xF1, if not skip this byte while (true) {
if (!this->response_receiving_) { // Process the data from the UART interface connected to the heatpump
if (byte_ != 0x31 && byte_ != 0x71 && byte_ != 0xF1) if (self->receive_from_uart(self->parent_, rx_buffer)) {
continue; auto* message = new std::vector<uint8_t>(rx_buffer);
this->response_message_.clear(); if (xQueueSend(self->response_queue_handle_, &message, 0) != pdPASS) {
this->response_receiving_ = true; ESP_LOGW(TAG, "Response queue full or unavailable, dropping message");
} delete message;
// Add current byte to message buffer
this->response_message_.push_back(byte_);
// 2. byte contains the payload size
if (this->response_message_.size() == 2) {
this->payload_length_ = byte_;
}
// 3. byte shall be 0x01 or 0x10
if (this->response_message_.size() == 3 && byte_ != 0x01 && byte_ != 0x10) {
this->response_receiving_ = false;
ESP_LOGW(TAG, "Invalid response message: 0x%s. Expected last byte to be 0x01 or 0x10",
PanasonicHelpers::byte_array_to_hex_string(this->response_message_, ',').c_str());
delay(10); // NOLINT
continue;
}
// 4. byte shall be 0x01, 0x10 or 0x21
if (this->response_message_.size() == 4 && byte_ != 0x01 && byte_ != 0x10 && byte_ != 0x21) {
this->response_receiving_ = false;
ESP_LOGW(TAG, "Invalid response message: 0x%s. Expected last byte to be 0x01, 0x10 or 0x21",
PanasonicHelpers::byte_array_to_hex_string(this->response_message_, ',').c_str());
delay(10); // NOLINT
continue;
}
// Check if message is complete
if (this->response_message_.size() > 2 && this->response_message_.size() == this->payload_length_ + 3) {
this->response_receiving_ = false;
this->current_response_count_++;
if (this->log_uart_msg_)
PanasonicHelpers::log_uart_hex(UART_LOG_RX, this->response_message_, ',');
}
}
}
void PanasonicHeatpumpComponent::send_request(RequestType requestType) {
switch (requestType) {
case RequestType::COMMAND:
if (this->log_uart_msg_)
PanasonicHelpers::log_uart_hex(UART_LOG_TX, this->command_message_, ',');
this->write_array(this->command_message_);
this->flush();
break;
case RequestType::INITIAL:
// Probably not necessary but CZ-TAW1 sends this query on boot
if (this->log_uart_msg_)
PanasonicHelpers::log_uart_hex(UART_LOG_TX, PanasonicCommand::InitialRequest, INIT_REQUEST_SIZE, ',');
this->write_array(PanasonicCommand::InitialRequest, INIT_REQUEST_SIZE);
this->flush();
break;
case RequestType::POLLING:
if (this->log_uart_msg_)
PanasonicHelpers::log_uart_hex(UART_LOG_TX, PanasonicCommand::PollingMessage, DATA_MESSAGE_SIZE, ',');
this->write_array(PanasonicCommand::PollingMessage, DATA_MESSAGE_SIZE);
this->flush();
break;
case RequestType::POLLING_EXTRA:
if (this->log_uart_msg_)
PanasonicHelpers::log_uart_hex(UART_LOG_TX, PanasonicCommand::PollingExtraMessage, DATA_MESSAGE_SIZE, ',');
this->write_array(PanasonicCommand::PollingExtraMessage, DATA_MESSAGE_SIZE);
this->flush();
break;
};
if (requestType != RequestType::NONE && requestType != RequestType::INITIAL) {
// Update last request time when request was sent
this->last_request_time_ = millis();
}
this->next_request_ = RequestType::NONE;
}
void PanasonicHeatpumpComponent::read_request() {
if (this->uart_client_ == nullptr)
return;
while (this->uart_client_->available()) {
// Read each byte from client and forward it directly to the heatpump
this->uart_client_->read_byte(&byte_);
this->write_byte(byte_);
// Message shall start with 0x31, 0x71 or 0xF1, if not skip this byte
if (!this->request_receiving_) {
if (byte_ != 0x31 && byte_ != 0x71 && byte_ != 0xF1)
continue;
this->request_message_.clear();
this->request_receiving_ = true;
}
// Add current byte to message buffer
this->request_message_.push_back(byte_);
// 2. byte contains the payload size
if (this->request_message_.size() == 2) {
this->payload_length_ = byte_;
}
// 3. byte shall be 0x01 or 0x10
if (this->request_message_.size() == 3 && byte_ != 0x01 && byte_ != 0x10) {
this->request_receiving_ = false;
ESP_LOGW(TAG, "Invalid request message: 0x%s. Expected last byte to be 0x01 or 0x10",
PanasonicHelpers::byte_array_to_hex_string(this->request_message_, ',').c_str());
delay(10); // NOLINT
continue;
}
// 4. byte shall be 0x01, 0x10 or 0x21
if (this->request_message_.size() == 4 && byte_ != 0x01 && byte_ != 0x10 && byte_ != 0x21) {
this->request_receiving_ = false;
ESP_LOGW(TAG, "Invalid request message: 0x%s. Expected last byte to be 0x01, 0x10 or 0x21",
PanasonicHelpers::byte_array_to_hex_string(this->request_message_, ',').c_str());
delay(10); // NOLINT
continue;
}
// Check if message is complete
if (this->request_message_.size() > 2 && this->request_message_.size() == this->payload_length_ + 3) {
this->request_receiving_ = false;
if (this->log_uart_msg_)
PanasonicHelpers::log_uart_hex(UART_LOG_TX, this->request_message_, ',');
if (this->request_message_[0] != 0x31) {
// Update last request time when request is complete
this->last_request_time_ = millis();
this->uart_client_timeout_exceeded_ = false;
} }
// ... and pass on a copy to CZ-TAW1
if (self->uart_client_ != nullptr) {
self->uart_client_->write_array(rx_buffer);
}
} else {
vTaskDelay(pdMS_TO_TICKS(10));
} }
} }
} }
int PanasonicHeatpumpComponent::getResponseByte(const int index) { void PanasonicHeatpumpComponent::uart_client_task(void* pvParameters) {
if (this->heatpump_default_message_.size() > index) auto* self = static_cast<PanasonicHeatpumpComponent*>(pvParameters);
return this->heatpump_default_message_[index]; std::vector<uint8_t> rx_buffer;
return -1; rx_buffer.reserve(256);
while (true) {
// Process the data from the UART interface connected to the client (CZ-TAW1)
if (self->receive_from_uart(self->uart_client_, rx_buffer)) {
auto* message = new std::vector<uint8_t>(rx_buffer);
if (xQueueSend(self->request_queue_handle_, &message, 0) != pdPASS) {
ESP_LOGW(TAG, "Request queue full or unavailable, dropping message");
delete message;
}
self->last_client_request_time_ = millis();
self->uart_client_timeout_exceeded_ = false;
} else {
vTaskDelay(pdMS_TO_TICKS(10));
}
}
} }
int PanasonicHeatpumpComponent::getExtraResponseByte(const int index) { // Used for both uart interfaces
if (this->heatpump_extra_message_.size() > index) bool PanasonicHeatpumpComponent::receive_from_uart(uart::UARTComponent* uartComp, std::vector<uint8_t>& buffer) {
return this->heatpump_extra_message_[index]; uint8_t start_byte;
return -1;
// Wait for the start byte to be available
while (!uartComp->available())
vTaskDelay(pdMS_TO_TICKS(5));
// Read the first byte
if (!uartComp->read_byte(&start_byte))
return false;
// Message shall start with 0x31, 0x71 or 0xF1, if not skip this byte
if (start_byte != 0x31 && start_byte != 0x71 && start_byte != 0xF1) {
ESP_LOGW(TAG, "Invalid start byte: 0x%x", start_byte);
return false;
}
// Prepare buffer for header reading.
// Header is 4 bytes long and first byte is already read.
// Message may be up to 256 bytes long,
// so reserve enough space to avoid dynamic resizing during reading.
buffer.clear();
buffer.reserve(256);
buffer.resize(HEADER_SIZE);
buffer[0] = start_byte;
// Wait for header
if (uartComp->available() < HEADER_SIZE - 1)
vTaskDelay(pdMS_TO_TICKS(5));
// Write header to buffer
auto succeed = uartComp->read_array(&buffer[1], HEADER_SIZE - 1);
// Verify header (start byte, message type and length)
if (!verify_message_header(buffer, succeed))
return false;
// Calculate total message length
size_t total_expected = buffer[1] + 3;
size_t remaining = total_expected - buffer.size();
// Write rest of the message to buffer
while (remaining > 0) {
size_t current_size = buffer.size();
size_t to_read = std::min((size_t)8, remaining);
buffer.resize(current_size + to_read);
if (uartComp->available() < to_read)
vTaskDelay(pdMS_TO_TICKS(10));
if (!uartComp->read_array(&buffer[current_size], to_read)) {
ESP_LOGW(TAG, "Timeout while reading message body");
return false;
}
remaining -= to_read;
}
// Verify checksum
if (!verify_message_checksum(buffer)) {
return false;
}
// Message is complete
return true;
} }
ResponseType PanasonicHeatpumpComponent::check_response(const std::vector<uint8_t>& data) { bool PanasonicHeatpumpComponent::verify_message_header(const std::vector<uint8_t>& message, bool reading_succeeded) {
if (!reading_succeeded) {
ESP_LOGW(TAG, "Timeout while reading message header");
return false;
}
if (message.size() < HEADER_SIZE) {
ESP_LOGW(TAG, "Message too short to contain valid header");
return false;
}
if ((message[2] != 0x01 && message[2] != 0x10) || // 3. byte shall be 0x01 or 0x10
(message[3] != 0x01 && message[3] != 0x10 && message[3] != 0x21)) { // 4. byte shall be 0x01, 0x10 or 0x21
ESP_LOGW(TAG, "Invalid message header: 0x%s. Drop message.",
PanasonicHelpers::byte_array_to_hex_string(message, ',').c_str());
return false;
}
return true;
}
bool PanasonicHeatpumpComponent::verify_message_checksum(const std::vector<uint8_t>& message) {
uint8_t checksum = 0;
for (const auto b : message)
checksum += b;
// Last byte contains chechsum.
// Only if the sum of all bytes & 0xFF is 0, the message is valid.
if (checksum != 0) {
ESP_LOGW(TAG, "Invalid message checksum: 0x%02X. Last byte: 0x%02X", checksum, message.back());
return false;
}
return true;
}
ResponseType PanasonicHeatpumpComponent::read_response() {
std::vector<uint8_t>* message{nullptr};
if (xQueueReceive(this->response_queue_handle_, &message, 0) != pdPASS || message == nullptr) {
return ResponseType::UNKNOWN;
}
PanasonicHelpers::write_uart_log(UART_LOG_RX, *message, ',', this->log_uart_msg_);
if (!this->check_response_length(*message)) {
delete message;
return ResponseType::UNKNOWN;
}
// Get response type and save the response
auto responseType = ResponseType::UNKNOWN;
const uint8_t type = (*message)[3];
if (type == 0x10) {
responseType = ResponseType::STANDARD;
this->heatpump_default_message_ = std::move(*message);
// Is an extra request required?
if (this->heatpump_default_message_.size() > 199 && this->heatpump_default_message_[199] > 0x02) {
ESP_LOGD(TAG, "Queue extra polling request");
this->queue_request(build_message(PanasonicCommand::PollingExtraMessage));
}
} else if (type == 0x21) {
responseType = ResponseType::EXTRA;
this->heatpump_extra_message_ = std::move(*message);
} else {
ESP_LOGW(TAG, "Unknown response type in byte 3: 0x%02X", type);
responseType = ResponseType::UNKNOWN;
}
delete message;
return responseType;
}
bool PanasonicHeatpumpComponent::check_response_length(const std::vector<uint8_t>& message) {
// Read response message: // Read response message:
// format: 0x71 [payload_length] 0x01 [0x10 || 0x21] [[TOP0 - TOP114] ...] 0x00 [checksum] // format: 0x71 [payload_length] 0x01 [0x10 || 0x21] [[TOP0 - TOP114] ...] 0x00 [checksum]
// payload_length: payload_length + 3 = packet_length // payload_length: payload_length + 3 = packet_length
// checksum: if (sum(all bytes) & 0xFF == 0) ==> valid packet // checksum: if (sum(all bytes) & 0xFF == 0) ==> valid packet
if (message.size() == RESPONSE_MSG_SIZE)
return true;
if (data.empty()) ESP_LOGW(TAG, "Response message too short: received %u - expected %u", message.size(), RESPONSE_MSG_SIZE);
return ResponseType::UNKNOWN; return false;
if (data[0] != 0x71) }
return ResponseType::UNKNOWN;
if (this->response_receiving_) void PanasonicHeatpumpComponent::send_request() {
return ResponseType::RECEIVING; if (millis() - request_send_time_ < REQUEST_SEND_INTERVAL) {
if (data.size() != RESPONSE_MSG_SIZE) { // wait until the interval is over
ESP_LOGW(TAG, "Invalid response message length: recieved %d - expected %d", data.size(), RESPONSE_MSG_SIZE); return;
delay(10); // NOLINT
return ResponseType::UNKNOWN;
} }
// Verify checksum // Get message from queue
uint8_t checksum = 0; std::vector<uint8_t>* message{nullptr};
for (int i = 0; i < data.size(); i++) { if (xQueueReceive(this->request_queue_handle_, &message, 0) != pdPASS || message == nullptr) {
checksum += data[i]; return; // nothing queued
}
// all bytes (including checksum byte) shall be 0x00
if (checksum != 0) {
ESP_LOGW(TAG, "Invalid response message: checksum = 0x%02X, last_byte = 0x%02X", checksum, data[202]);
delay(10); // NOLINT
return ResponseType::UNKNOWN;
} }
PanasonicHelpers::write_uart_log(UART_LOG_TX, *message, ',', this->log_uart_msg_);
this->send_extra_request_ = data[3] == 0x10 && data[199] > 0x02 && this->send_extra_request_ == false ? true : false; // Send vector content over UART (robust API usage)
this->write_array(message->data(), message->size());
// Get response type and save the response delete message;
auto responseType = ResponseType::UNKNOWN; request_send_time_ = millis();
if (data[3] == 0x10) {
responseType = ResponseType::STANDARD;
this->heatpump_default_message_ = data;
} else if (data[3] == 0x21) {
responseType = ResponseType::EXTRA;
this->heatpump_extra_message_ = data;
}
if (responseType == ResponseType::UNKNOWN) {
ESP_LOGW(TAG, "Unknown response type (4. byte): 0x%02X. Expected 0x10 or 0x21.", data[3]);
delay(10); // NOLINT
return responseType;
}
// Check if the current response is a new response
if (this->last_response_count_ == this->current_response_count_)
return ResponseType::UNKNOWN;
this->last_response_count_ = this->current_response_count_;
return responseType;
} }
void PanasonicHeatpumpComponent::set_command_high_nibble(const uint8_t value, const uint8_t index) { void PanasonicHeatpumpComponent::set_command_high_nibble(const uint8_t value, const uint8_t index) {
if (this->next_request_ != RequestType::COMMAND) { this->command_message_ = build_message(PanasonicCommand::CommandMessage);
// initialize the command
this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage),
std::end(PanasonicCommand::CommandMessage));
}
uint8_t lowNibble = this->heatpump_default_message_[index] & 0b1111; uint8_t lowNibble = this->heatpump_default_message_[index] & 0b1111;
uint8_t highNibble = value << 4; uint8_t highNibble = value << 4;
// set command byte // set command byte
@ -360,16 +377,13 @@ void PanasonicHeatpumpComponent::set_command_high_nibble(const uint8_t value, co
this->command_message_.back() = this->command_message_.back() =
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
// command will be send on next loop ESP_LOGD(TAG, "Queue command request");
this->next_request_ = RequestType::COMMAND; this->queue_request(this->command_message_);
} }
void PanasonicHeatpumpComponent::set_command_low_nibble(const uint8_t value, const uint8_t index) { void PanasonicHeatpumpComponent::set_command_low_nibble(const uint8_t value, const uint8_t index) {
if (this->next_request_ != RequestType::COMMAND) { this->command_message_ = build_message(PanasonicCommand::CommandMessage);
// initialize the command
this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage),
std::end(PanasonicCommand::CommandMessage));
}
uint8_t highNibble = this->heatpump_default_message_[index] & 0b11110000; uint8_t highNibble = this->heatpump_default_message_[index] & 0b11110000;
uint8_t lowNibble = value & 0b1111; uint8_t lowNibble = value & 0b1111;
// set command byte // set command byte
@ -378,32 +392,25 @@ void PanasonicHeatpumpComponent::set_command_low_nibble(const uint8_t value, con
this->command_message_.back() = this->command_message_.back() =
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
// command will be send on next loop ESP_LOGD(TAG, "Queue command request");
this->next_request_ = RequestType::COMMAND; this->queue_request(this->command_message_);
} }
void PanasonicHeatpumpComponent::set_command_byte(const uint8_t value, const uint8_t index) { void PanasonicHeatpumpComponent::set_command_byte(const uint8_t value, const uint8_t index) {
if (this->next_request_ != RequestType::COMMAND) { this->command_message_ = build_message(PanasonicCommand::CommandMessage);
// initialize the command
this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage),
std::end(PanasonicCommand::CommandMessage));
}
// set command byte // set command byte
this->command_message_[index] = value; this->command_message_[index] = value;
// calculate and set set checksum (last element) // calculate and set set checksum (last element)
this->command_message_.back() = this->command_message_.back() =
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
// command will be send on next loop ESP_LOGD(TAG, "Queue command request");
this->next_request_ = RequestType::COMMAND; this->queue_request(this->command_message_);
} }
void PanasonicHeatpumpComponent::set_command_curve(const uint8_t value, const uint8_t index) { void PanasonicHeatpumpComponent::set_command_curve(const uint8_t value, const uint8_t index) {
if (this->next_request_ != RequestType::COMMAND) { this->command_message_ = build_message(PanasonicCommand::CommandMessage);
// initialize the command
this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage),
std::end(PanasonicCommand::CommandMessage));
}
// Set zone 1 curve bytes // Set zone 1 curve bytes
if (index == 75 || index == 76 || index == 77 || index == 78 || index == 86 || index == 87 || index == 88 || if (index == 75 || index == 76 || index == 77 || index == 78 || index == 86 || index == 87 || index == 88 ||
@ -436,8 +443,32 @@ void PanasonicHeatpumpComponent::set_command_curve(const uint8_t value, const ui
this->command_message_.back() = this->command_message_.back() =
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
// command will be send on next loop ESP_LOGD(TAG, "Queue command request");
this->next_request_ = RequestType::COMMAND; this->queue_request(this->command_message_);
}
void PanasonicHeatpumpComponent::queue_request(const std::vector<uint8_t>& message) {
auto* cmd = new std::vector<uint8_t>(message);
// Check request_queue_handle_, function is called before setup() initializes it!
if (this->request_queue_handle_ == nullptr || xQueueSend(this->request_queue_handle_, &cmd, 0) != pdPASS) {
ESP_LOGW(TAG, "Request queue full or unavailable, dropping message");
delete cmd;
}
}
// This function can be used in esphome lambda to get a specific byte
int PanasonicHeatpumpComponent::get_response_byte(const int index) {
if (this->heatpump_default_message_.size() > index)
return this->heatpump_default_message_[index];
return -1;
}
// This function can be used in esphome lambda to get a specific byte
int PanasonicHeatpumpComponent::get_extra_response_byte(const int index) {
if (this->heatpump_extra_message_.size() > index)
return this->heatpump_extra_message_[index];
return -1;
} }
} // namespace panasonic_heatpump } // namespace panasonic_heatpump
} // namespace esphome } // namespace esphome

View File

@ -23,7 +23,6 @@ namespace esphome {
namespace panasonic_heatpump { namespace panasonic_heatpump {
enum LoopState : uint8_t { enum LoopState : uint8_t {
READ_RESPONSE, READ_RESPONSE,
CHECK_RESPONSE,
PUBLISH_SENSOR, PUBLISH_SENSOR,
PUBLISH_BINARY_SENSOR, PUBLISH_BINARY_SENSOR,
PUBLISH_TEXT_SENSOR, PUBLISH_TEXT_SENSOR,
@ -34,7 +33,6 @@ enum LoopState : uint8_t {
PUBLISH_WATER_HEATER, PUBLISH_WATER_HEATER,
PUBLISH_EXTRA_SENSOR, PUBLISH_EXTRA_SENSOR,
SEND_REQUEST, SEND_REQUEST,
READ_REQUEST,
RESTART_LOOP, RESTART_LOOP,
}; };
@ -48,7 +46,6 @@ enum RequestType : uint8_t {
enum ResponseType : uint8_t { enum ResponseType : uint8_t {
UNKNOWN, UNKNOWN,
RECEIVING,
STANDARD, STANDARD,
EXTRA, EXTRA,
}; };
@ -86,9 +83,9 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev
void set_log_uart_msg(bool active) { void set_log_uart_msg(bool active) {
this->log_uart_msg_ = active; this->log_uart_msg_ = active;
} }
// uart message variables to use in lambda functions // functions to use in esphome lambda
int getResponseByte(const int index); int get_response_byte(const int index);
int getExtraResponseByte(const int index); int get_extra_response_byte(const int index);
// command functions // command functions
void set_command_high_nibble(const uint8_t value, const uint8_t index); void set_command_high_nibble(const uint8_t value, const uint8_t index);
void set_command_low_nibble(const uint8_t value, const uint8_t index); void set_command_low_nibble(const uint8_t value, const uint8_t index);
@ -128,27 +125,25 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev
protected: protected:
// options variables // options variables
uart::UARTComponent* uart_client_{nullptr};
bool log_uart_msg_{false}; bool log_uart_msg_{false};
uint32_t last_request_time_{0}; uint32_t last_client_request_time_{0};
uint32_t uart_client_timeout_{10000}; uint32_t uart_client_timeout_{10000}; // 10 sec
// uart message variables uint32_t request_send_time_{5000}; // 5 sec --> default is 5 sec so first request is not sent too fast after startup
static const uint32_t REQUEST_SEND_INTERVAL{250}; // 250 ms
static const size_t HEADER_SIZE = 4;
// uart message variables, process in main loop
TaskHandle_t uart_task_handle_{nullptr};
TaskHandle_t uart_client_task_handle_{nullptr};
QueueHandle_t response_queue_handle_{nullptr};
QueueHandle_t request_queue_handle_{nullptr};
uart::UARTComponent* uart_client_{nullptr};
std::vector<uint8_t> heatpump_default_message_; std::vector<uint8_t> heatpump_default_message_;
std::vector<uint8_t> heatpump_extra_message_; std::vector<uint8_t> heatpump_extra_message_;
std::vector<uint8_t> response_message_;
std::vector<uint8_t> request_message_;
std::vector<uint8_t> command_message_; std::vector<uint8_t> command_message_;
uint8_t payload_length_;
uint8_t byte_;
uint8_t current_response_count_{0};
uint8_t last_response_count_{0};
bool response_receiving_{false};
bool request_receiving_{false};
bool send_extra_request_{false};
bool uart_client_timeout_exceeded_{false}; bool uart_client_timeout_exceeded_{false};
LoopState loop_state_{LoopState::RESTART_LOOP}; LoopState loop_state_{LoopState::RESTART_LOOP};
RequestType next_request_{RequestType::INITIAL};
ResponseType current_response_{ResponseType::UNKNOWN};
// entity vectors // entity vectors
std::vector<PanasonicHeatpumpEntity*> binary_sensors_; std::vector<PanasonicHeatpumpEntity*> binary_sensors_;
std::vector<PanasonicHeatpumpEntity*> climates_; std::vector<PanasonicHeatpumpEntity*> climates_;
@ -161,10 +156,21 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev
std::vector<PanasonicHeatpumpEntity*> extra_sensors_; std::vector<PanasonicHeatpumpEntity*> extra_sensors_;
// uart message functions // uart message functions
void read_response(); static void uart_task(void* pvParameters);
void send_request(RequestType requestType); static void uart_client_task(void* pvParameters);
void read_request(); bool receive_from_uart(uart::UARTComponent* src, std::vector<uint8_t>& buffer);
ResponseType check_response(const std::vector<uint8_t>& data);
void send_request();
void queue_request(const std::vector<uint8_t>& message);
ResponseType read_response();
static bool check_response_length(const std::vector<uint8_t>& message);
static bool verify_message_header(const std::vector<uint8_t>& message, bool reading_succeeded);
static bool verify_message_checksum(const std::vector<uint8_t>& message);
template <size_t N>
static std::vector<uint8_t> build_message(const uint8_t (&msg)[N]) {
return std::vector<uint8_t>(msg, msg + N);
}
}; };
} // namespace panasonic_heatpump } // namespace panasonic_heatpump
} // namespace esphome } // namespace esphome

View File

@ -23,7 +23,7 @@ water_heater::WaterHeaterTraits PanasonicHeatpumpWaterHeater::traits() {
void PanasonicHeatpumpWaterHeater::control(const water_heater::WaterHeaterCall& call) { void PanasonicHeatpumpWaterHeater::control(const water_heater::WaterHeaterCall& call) {
if (call.get_mode().has_value()) { if (call.get_mode().has_value()) {
int byte6 = this->parent_->getResponseByte(6); int byte6 = this->parent_->get_response_byte(6);
if (byte6 >= 0) { if (byte6 >= 0) {
water_heater::WaterHeaterMode new_mode = *call.get_mode(); water_heater::WaterHeaterMode new_mode = *call.get_mode();
uint8_t newByte6 = this->setWaterHeaterMode(new_mode, (uint8_t)byte6); uint8_t newByte6 = this->setWaterHeaterMode(new_mode, (uint8_t)byte6);

View File

@ -11,7 +11,6 @@ tests/
│ ├── README.md │ ├── README.md
│ ├── test_panasonic_heatpump_unit.py # Unit tests │ ├── test_panasonic_heatpump_unit.py # Unit tests
│ ├── test_panasonic_heatpump_integration.py # Integration tests │ ├── test_panasonic_heatpump_integration.py # Integration tests
│ ├── test_panasonic_heatpump_esp8266.yaml # ESP8266 test config
│ ├── test_panasonic_heatpump_esp32s2.yaml # ESP32-S2 test config │ ├── test_panasonic_heatpump_esp32s2.yaml # ESP32-S2 test config
│ ├── test_panasonic_heatpump_esp32c3.yaml # ESP32-C3 test config │ ├── test_panasonic_heatpump_esp32c3.yaml # ESP32-C3 test config
│ ├── test_panasonic_heatpump_cztaw1.yaml # CZ-TAW1 test config │ ├── test_panasonic_heatpump_cztaw1.yaml # CZ-TAW1 test config

View File

@ -14,13 +14,11 @@ This directory contains comprehensive tests for the `panasonic_heatpump` ESPHome
### Integration Tests (`test_panasonic_heatpump_integration.py`) ### Integration Tests (`test_panasonic_heatpump_integration.py`)
- Tests minimal feature configuration on ESP32 board - Tests minimal feature configuration on ESP32 board
- Tests full feature configuration on ESP32 board - Tests full feature configuration on ESP32 board
- Tests configuration on ESP8266 board
- Tests configuration on ESP32-S2 board - Tests configuration on ESP32-S2 board
- Tests configuration on ESP32-C3 board - Tests configuration on ESP32-C3 board
- Tests configuration with CZ-TAW1 (UART-proxy) - Tests configuration with CZ-TAW1 (UART-proxy)
### Test Configuration Files ### Test Configuration Files
- `test_panasonic_heatpump_esp8266.yaml` - ESP8266 Wemos D1 Mini
- `test_panasonic_heatpump_esp32s2.yaml` - ESP32-S2 Wemos S2 Mini - `test_panasonic_heatpump_esp32s2.yaml` - ESP32-S2 Wemos S2 Mini
- `test_panasonic_heatpump_esp32c3.yaml` - ESP32-C3 mini (RISC-V) - `test_panasonic_heatpump_esp32c3.yaml` - ESP32-C3 mini (RISC-V)
- `test_panasonic_heatpump_cztaw1.yaml` - UART proxy setup for CZ-TAW1 client support - `test_panasonic_heatpump_cztaw1.yaml` - UART proxy setup for CZ-TAW1 client support
@ -68,7 +66,7 @@ The GitHub Actions workflow (`.github/workflows/test_panasonic_heatpump.yml`) in
### Jobs ### Jobs
1. **test-build**: Validates and compiles the component 1. **test-build**: Validates and compiles the component
- Tests on multiple board types: ESP8266, ESP32 (full), ESP32-S2, ESP32-C3 and with CZ-TAW1 (UART-proxy) - Tests on multiple board types: ESP32 (full), ESP32-S2, ESP32-C3 and with CZ-TAW1 (UART-proxy)
- Uses esphome/build-action for firmware compilation - Uses esphome/build-action for firmware compilation
2. **lint-code**: Runs clang-format on C++ code 2. **lint-code**: Runs clang-format on C++ code
@ -196,7 +194,7 @@ All tests must pass before merging to main branch. The GitHub Actions workflow e
- Code follows formatting standards - Code follows formatting standards
- All unit and integration tests pass - All unit and integration tests pass
- Latest ESPHome versions is supported - Latest ESPHome versions is supported
- ESP8266 and ESP32 boards are supported - Only ESP32 boards are supported
## Contributing ## Contributing

View File

@ -1,81 +0,0 @@
---
# Test configuration for panasonic_heatpump component on ESP8266
# ESP8266 has limited GPIO pins and only one UART
esp8266:
board: d1_mini
esphome:
name: test-panasonic-heatpump-esp8266
friendly_name: "Test Panasonic Heatpump ESP8266"
logger:
level: DEBUG
external_components:
- source:
type: local
path: ../../components
components: [panasonic_heatpump]
uart:
- id: uart_heatpump
tx_pin: GPIO13
rx_pin: GPIO15
baud_rate: 9600
data_bits: 8
parity: EVEN
stop_bits: 1
# Test basic configuration
panasonic_heatpump:
id: my_heatpump
uart_id: uart_heatpump
# Test sensor types - first item only
sensor:
- platform: panasonic_heatpump
top1:
name: "Pump Flow"
# Test binary sensors - first item only
binary_sensor:
- platform: panasonic_heatpump
top0:
name: "Heatpump State"
# Test text sensors - first item only
text_sensor:
- platform: panasonic_heatpump
top4:
name: "Operating Mode State"
# Test number controls - first item only
number:
- platform: panasonic_heatpump
set5:
name: "Set Z1 Heat Request Temperature"
# Test select controls - first item only
select:
- platform: panasonic_heatpump
set2:
name: "Set Holiday Mode"
# Test switch controls - first item only
switch:
- platform: panasonic_heatpump
set1:
name: "Set Heatpump"
# Test climate controls - first item only
climate:
- platform: panasonic_heatpump
zone1:
name: "Zone 1"
# Test water_heater controls - first item only
water_heater:
- platform: panasonic_heatpump
tank:
name: "DHW"

View File

@ -12,17 +12,6 @@ import os
class TestPanasonicHeatpumpIntegration: class TestPanasonicHeatpumpIntegration:
"""Integration tests using ESPHome CLI.""" """Integration tests using ESPHome CLI."""
@pytest.fixture
def test_yaml_esp8266(self):
"""Get the path to the ESP8266 test YAML file."""
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
return os.path.join(
base_dir,
"tests",
"panasonic_heatpump",
"test_panasonic_heatpump_esp8266.yaml",
)
@pytest.fixture @pytest.fixture
def test_yaml_esp32(self): def test_yaml_esp32(self):
"""Get the path to the ESP32 test YAML file.""" """Get the path to the ESP32 test YAML file."""
@ -64,26 +53,6 @@ class TestPanasonicHeatpumpIntegration:
"test_panasonic_heatpump_cztaw1.yaml", "test_panasonic_heatpump_cztaw1.yaml",
) )
def test_validate_esp8266_config(self, test_yaml_esp8266: str):
"""Test that the ESP8266 configuration is valid."""
if not os.path.exists(test_yaml_esp8266):
pytest.skip(f"Test file not found: {test_yaml_esp8266}")
try:
result = subprocess.run(
["esphome", "config", test_yaml_esp8266],
capture_output=True,
text=True,
timeout=120,
)
assert (
result.returncode == 0
), f"ESP8266 config validation failed: {result.stderr}"
except FileNotFoundError:
pytest.skip("ESPHome not installed")
except subprocess.TimeoutExpired:
pytest.fail("ESPHome config validation timed out")
def test_validate_esp32_config(self, test_yaml_esp32: str): def test_validate_esp32_config(self, test_yaml_esp32: str):
"""Test that the ESP32 configuration is valid.""" """Test that the ESP32 configuration is valid."""
if not os.path.exists(test_yaml_esp32): if not os.path.exists(test_yaml_esp32):