commit 70c3ffd31f12b16de970f279a66f5bf5261f7cd9 Author: Mikhail Sazanov Date: Mon Jun 24 20:58:38 2024 +0300 Hello OT Signed-off-by: Mikhail Sazanov diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9d66c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.devcontainer +.vscode +build +sdkconfig \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100755 index 0000000..2fc8d68 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +cmake_minimum_required(VERSION 3.5) + +set(EXTRA_COMPONENT_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../components) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(opentherm) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..d3723b4 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | +| | ✓ | ? | ? | ? | ? | ? | ? | + +# ESP-IDF Opentherm + +This component provide an implementation of Opentherm protocol with ESP-IDF. Tested on ESP-IDF > 5 version. + +## How to Use Example + +Before project configuration and build, be sure to set the correct chip target using `idf.py set-target `. + +### Hardware Required + +* A development board with normal LED or addressable LED on-board (e.g., ESP32-S3-DevKitC, ESP32-C6-DevKitC etc.) +* A USB cable for Power supply and programming + +See [Development Boards](https://www.espressif.com/en/products/devkits) for more information about it. + +### Configure the Project + +Open the project configuration menu (`idf.py menuconfig`). + +In the `OpenTherm Configuration` menu: + +* Select GPIO in pin +* Select GPIO out pin + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +```text +I (9369) OT: ====== OPENTHERM ===== +I (9369) OT: Free heap size before: 293684 +I (9369) OT: Central Heating: OFF +I (9369) OT: Hot Water: OFF +I (9369) OT: Flame: OFF +I (9379) OT: Fault: NO +I (9659) OT: Set CH Temp to: 60 +I (9929) OT: Set DHW Temp to: 59 +I (10199) OT: DHW Temp: 0.0 +I (10469) OT: CH Temp: 44.3 +I (10739) OT: Slave OT Version: 0.0 +I (11009) OT: Slave Version: C07FA308 +I (11279) OT: Slave OT Version: 3.0 +I (11279) OT: Free heap size after: 293684 +I (11279) OT: ====== OPENTHERM ===== +``` + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/sazanof/esp-idf-opentherm/issues) on GitHub. We will get back to you soon. diff --git a/components/opentherm/CMakeLists.txt b/components/opentherm/CMakeLists.txt new file mode 100644 index 0000000..62d5522 --- /dev/null +++ b/components/opentherm/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS opentherm.c + INCLUDE_DIRS . + PRIV_REQUIRES driver esp_timer log +) \ No newline at end of file diff --git a/components/opentherm/opentherm.c b/components/opentherm/opentherm.c new file mode 100644 index 0000000..3a283d3 --- /dev/null +++ b/components/opentherm/opentherm.c @@ -0,0 +1,840 @@ +/** +* @package Opentherm library for ESP-IDF framework +* @author: Mikhail Sazanof +* @copyright Copyright (C) 2024 - Sazanof.ru +* @licence MIT +*/ + +#include +#include "esp_log.h" +#include "opentherm.h" +#include "rom/ets_sys.h" + +static const char *TAG = "ot-example"; + +gpio_num_t pin_in = CONFIG_OT_IN_PIN; +gpio_num_t pin_out = CONFIG_OT_OUT_PIN; + +typedef uint8_t byte; + +bool esp_ot_is_slave; + +void (*esp_ot_process_response_callback)(unsigned long, open_therm_response_status_t); + +volatile unsigned long response; + +volatile esp_ot_opentherm_status_t esp_ot_status; + +volatile open_therm_response_status_t esp_ot_response_status; + +volatile unsigned long esp_ot_response_timestamp; + +volatile byte esp_ot_response_bit_index; + +/** + * Initialize opentherm: gpio, install isr, basic data + * + * @return void + */ +esp_err_t esp_ot_init( + gpio_num_t _pin_in, + gpio_num_t _pin_out, + bool _esp_ot_is_slave, + void (*esp_ot_process_responseCallback)(unsigned long, open_therm_response_status_t)) +{ + + esp_err_t err = gpio_install_isr_service(0); + if (err != ESP_OK) + { + ESP_LOGE("ISR", "Error with state %s", esp_err_to_name(err)); + } + + pin_in = _pin_in; + pin_out = _pin_out; + + // Initialize the GPIO + gpio_config_t io_conf; + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pin_bit_mask = (1ULL << pin_in); + io_conf.intr_type = GPIO_INTR_ANYEDGE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = (1ULL << pin_out); + io_conf.intr_type = GPIO_INTR_DISABLE; + io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_conf.pull_up_en = GPIO_PULLUP_DISABLE; + gpio_config(&io_conf); + + gpio_isr_handler_add(pin_in, esp_ot_handle_interrupt, NULL); + + esp_ot_is_slave = _esp_ot_is_slave; + + esp_ot_process_response_callback = esp_ot_process_responseCallback; + + response = 0; + + esp_ot_response_status = OT_STATUS_NONE; + + esp_ot_response_timestamp = 0; + + gpio_intr_enable(pin_in); + + esp_ot_status = OT_READY; + + ESP_LOGI(TAG, "Initialize opentherm with in: %d out: %d", pin_in, pin_out); + + return ESP_OK; +} + + +/** + * Send bit helper + * + * @return void + */ +void esp_ot_send_bit(bool high) +{ + if (high) + esp_ot_set_active_state(); + else + esp_ot_set_idle_state(); + ets_delay_us(500); + if (high) + esp_ot_set_idle_state(); + else + esp_ot_set_active_state(); + ets_delay_us(500); +} + +/** + * Request builder for boiler status + * + * @return long + */ +unsigned long esp_ot_build_set_boiler_status_request(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2) +{ + unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4); + data <<= 8; + return esp_ot_build_request(OT_READ_DATA, MSG_ID_STATUS, data); +} + +/** + * Request builder for setting up boiler temperature + * + * @param float temperature + * + * @return long + */ +unsigned long esp_ot_build_set_boiler_temperature_request(float temperature) +{ + unsigned int data = esp_ot_temperature_to_data(temperature); + return esp_ot_build_request(OT_WRITE_DATA, MSG_ID_T_SET, data); +} + +/** + * Request builder to get boiler temperature + * + * @return long + */ +unsigned long esp_ot_build_get_boiler_temperature_request() +{ + return esp_ot_build_request(OT_READ_DATA, MSG_ID_TBOILER, 0); +} + +/** + * [IRAM_ATTR] Check if status is ready + * + * @return bool + */ +bool IRAM_ATTR esp_ot_is_ready() +{ + return esp_ot_status == OT_READY; +} + +/** + * [IRAM_ATTR] Read pin in state + * + * @return int [return description] + */ +int IRAM_ATTR esp_ot_read_state() +{ + return gpio_get_level(pin_in); +} + +/** + * Set active state helper + * + * @return void + */ +void esp_ot_set_active_state() +{ + gpio_set_level(pin_out, 0); +} + +/** + * Set idle state helper + * + * @return void + */ +void esp_ot_set_idle_state() +{ + gpio_set_level(pin_out, 1); +} + +/** + * Activate boiler helper + * + * @return void + */ +void esp_ot_activate_boiler() +{ + esp_ot_set_idle_state(); + vTaskDelay(1000 / portTICK_PERIOD_MS); +} +/** + * Process response execution + * + * @return void + */ +void esp_ot_process_response() +{ + if (esp_ot_process_response_callback != NULL) + { + // ESP_LOGI("PROCESS RESPONSE", "esp_ot_process_response, %ld, %d", response, esp_ot_response_status); + esp_ot_process_response_callback(response, esp_ot_response_status); + } +} + +/** + * Get message type + * + * @param long message + * + * @return open_therm_message_type_t + */ +open_therm_message_type_t esp_ot_get_message_type(unsigned long message) +{ + open_therm_message_type_t msg_type = (open_therm_message_type_t)((message >> 28) & 7); + return msg_type; +} + +/** + * Get data id + * + * @param long frame + * + * @return open_therm_message_id_t + */ +open_therm_message_id_t esp_ot_get_data_id(unsigned long frame) +{ + return (open_therm_message_id_t)((frame >> 16) & 0xFF); +} + +/** + * Build a request + * + * @param int data + * + * @return long + */ +unsigned long esp_ot_build_request(open_therm_message_type_t type, open_therm_message_id_t id, unsigned int data) +{ + unsigned long request = data; + if (type == OT_WRITE_DATA) + { + request |= 1ul << 28; + } + request |= ((unsigned long)id) << 16; + if (parity(request)) + { + request |= (1ul << 31); + } + return request; +} + +/** + * Build response + * + * @param int data + * + * @return long + */ +unsigned long esp_ot_build_response(open_therm_message_type_t type, open_therm_message_id_t id, unsigned int data) +{ + unsigned long response = data; + response |= ((unsigned long)type) << 28; + response |= ((unsigned long)id) << 16; + if (parity(response)) + response |= (1ul << 31); + return response; +} + +/** + * Check if request is valid + * + * @param long request + * + * @return bool + */ +bool esp_ot_is_valid_request(unsigned long request) +{ + if (parity(request)) + return false; + byte msgType = (request << 1) >> 29; + return msgType == (byte)OT_READ_DATA || msgType == (byte)OT_WRITE_DATA; +} + +/** + * Check if response is valid + * + * @param long response + * + * @return bool + */ +bool esp_ot_is_valid_response(unsigned long response) +{ + if (parity(response)) + return false; + byte msgType = (response << 1) >> 29; + return msgType == (byte)OT_READ_ACK || msgType == (byte)OT_WRITE_ACK; +} + +/** + * Parity helper + * + * @param long frame + * + * @return bool + */ +bool parity(unsigned long frame) // odd parity +{ + byte p = 0; + while (frame > 0) + { + if (frame & 1) + p++; + frame = frame >> 1; + } + return (p & 1); +} + +/** + * Reset helper + * + * @return long + */ +unsigned long ot_reset() +{ + unsigned int data = 1 << 8; + return esp_ot_send_request(esp_ot_build_request(OT_WRITE_DATA, MSG_ID_REMOTE_REQUEST, data)); +} + +/** + * Get slave product version + * + * @return long + */ +unsigned long esp_ot_get_slave_product_version() +{ + unsigned long response = esp_ot_send_request(esp_ot_build_request(OT_READ_DATA, MSG_ID_SLAVE_VERSION, 0)); + return esp_ot_is_valid_response(response) ? response : 0; +} + +/** + * Get slave configuration + * + * @return long + */ +unsigned long ot_get_slave_configuration() +{ + unsigned long response = esp_ot_send_request(esp_ot_build_request(OT_READ_DATA, MSG_ID_S_CONFIG_S_MEMEBER_ID_CODE, 0)); + return esp_ot_is_valid_response(response) ? response : 0; +} + +/** + * Get slave opentherm version + * + * @return float + */ +float esp_ot_get_slave_ot_version() +{ + unsigned long response = esp_ot_send_request(esp_ot_build_request(OT_READ_DATA, MSG_ID_OPENTERM_VERSION_SLAVE, 0)); + return esp_ot_is_valid_response(response) ? esp_ot_get_float(response) : 0; +} + +/** + * Handle interrupt helper + * + * @return void + */ +void IRAM_ATTR esp_ot_handle_interrupt() +{ + // ESP_DRAM_LOGI("esp_ot_handle_interrupt", "%ld", status); + if (esp_ot_is_ready()) + { + if (esp_ot_is_slave && esp_ot_read_state() == 1) + { + esp_ot_status = OT_RESPONSE_WAITING; + } + else + { + return; + } + } + + unsigned long newTs = esp_timer_get_time(); + if (esp_ot_status == OT_RESPONSE_WAITING) + { + if (esp_ot_read_state() == 1) + { + // ESP_DRAM_LOGI("BIT", "Set start bit"); + esp_ot_status = OT_RESPONSE_START_BIT; + esp_ot_response_timestamp = newTs; + } + else + { + // ESP_DRAM_LOGI("BIT", "Set OT_RESPONSE_INVALID"); + esp_ot_status = OT_RESPONSE_INVALID; + esp_ot_response_timestamp = newTs; + } + } + else if (esp_ot_status == OT_RESPONSE_START_BIT) + { + if ((newTs - esp_ot_response_timestamp < 750) && esp_ot_read_state() == 0) + { + esp_ot_status = OT_RESPONSE_RECEIVING; + esp_ot_response_timestamp = newTs; + esp_ot_response_bit_index = 0; + } + else + { + esp_ot_status = OT_RESPONSE_INVALID; + esp_ot_response_timestamp = newTs; + } + } + else if (esp_ot_status == OT_RESPONSE_RECEIVING) + { + if ((newTs - esp_ot_response_timestamp) > 750) + { + if (esp_ot_response_bit_index < 32) + { + response = (response << 1) | !esp_ot_read_state(); + esp_ot_response_timestamp = newTs; + esp_ot_response_bit_index++; + } + else + { // stop bit + esp_ot_status = OT_RESPONSE_READY; + esp_ot_response_timestamp = newTs; + } + } + } +} + +/** + * Process function + * + * @return void + */ +void process() +{ + PORT_ENTER_CRITICAL; + esp_ot_opentherm_status_t st = esp_ot_status; + unsigned long ts = esp_ot_response_timestamp; + PORT_EXIT_CRITICAL; + + if (st == OT_READY) + { + return; + } + unsigned long newTs = esp_timer_get_time(); + if (st != OT_NOT_INITIALIZED && st != OT_DELAY && (newTs - ts) > 1000000) + { + esp_ot_status = OT_READY; + ESP_LOGI("SET STATUS", "set status to READY"); // here READY + esp_ot_response_status = OT_STATUS_TIMEOUT; + esp_ot_process_response(); + } + else if (st == OT_RESPONSE_INVALID) + { + ESP_LOGE("SET STATUS", "set status to OT_RESPONSE_INVALID"); // here OT_RESPONSE_INVALID + esp_ot_status = OT_DELAY; + esp_ot_response_status = OT_STATUS_INVALID; + esp_ot_process_response(); + } + else if (st == OT_RESPONSE_READY) + { + esp_ot_status = OT_DELAY; + esp_ot_response_status = (esp_ot_is_slave ? esp_ot_is_valid_request(response) : esp_ot_is_valid_response(response)) ? OT_STATUS_SUCCESS : OT_STATUS_INVALID; + esp_ot_process_response(); + } + else if (st == OT_DELAY) + { + if ((newTs - ts) > (esp_ot_is_slave ? 20000 : 100000)) + { + esp_ot_status = OT_READY; + } + } +} + +/** + * Send request async + * + * @param long request + * + * @return bool + */ +bool esp_ot_send_request_async(unsigned long request) +{ + PORT_ENTER_CRITICAL; + const bool ready = esp_ot_is_ready(); + PORT_EXIT_CRITICAL; + if (!ready) + { + return false; + } + PORT_ENTER_CRITICAL; + esp_ot_status = OT_REQUEST_SENDING; + response = 0; + esp_ot_response_status = OT_STATUS_NONE; + PORT_EXIT_CRITICAL; + + // vTaskSuspendAll(); + + esp_ot_send_bit(1); // start bit + for (int i = 31; i >= 0; i--) + { + esp_ot_send_bit(bitRead(request, i)); + } + esp_ot_send_bit(1); // stop bit + esp_ot_set_idle_state(); + + esp_ot_response_timestamp = esp_timer_get_time(); + esp_ot_status = OT_RESPONSE_WAITING; + + // xTaskResumeAll(); + + return true; +} + +/** + * Send request + * + * @param long request + * + * @return long + */ +unsigned long esp_ot_send_request(unsigned long request) +{ + if (!esp_ot_send_request_async(request)) + { + return 0; + } + // ESP_LOGI("STATUS", "esp_ot_send_request with status %d", status); // here WAITING + while (!esp_ot_is_ready()) + { + process(); + vPortYield(); + } + return response; +} + +/** + * Check if response fault + * + * @param long response + * + * @return bool + */ +bool esp_ot_is_fault(unsigned long response) +{ + return response & 0x1; +} + +/** + * Check if central heating is active + * + * @param long response + * + * @return bool + */ +bool esp_ot_is_central_heating_active(unsigned long response) +{ + return response & 0x2; +} + +/** + * Check if hot water is active + * + * @param long response + * + * @return bool + */ +bool esp_ot_is_hot_water_active(unsigned long response) +{ + return response & 0x4; +} + +/** + * Check if flame is on + * + * @param long response + * + * @return bool + */ +bool esp_ot_is_flame_on(unsigned long response) +{ + return response & 0x8; +} + +/** + * Check if cooling is active + * + * @param long response [response description] + * + * @return bool [return description] + */ +bool esp_ot_is_cooling_active(unsigned long response) +{ + return response & 0x10; +} + +/** + * Check if response has diagnostic + * + * @param long response [response description] + * + * @return bool [return description] + */ +bool esp_ot_is_diagnostic(unsigned long response) +{ + return response & 0x40; +} + +/** + * Get uint value + * + * @param long response + * + * @return uint16_t + */ +uint16_t esp_ot_get_uint(const unsigned long response) +{ + const uint16_t u88 = response & 0xffff; + return u88; +} + +/** + * Get float value + * + * @param long response + * + * @return float + */ +float esp_ot_get_float(const unsigned long response) +{ + const uint16_t u88 = esp_ot_get_uint(response); + const float f = (u88 & 0x8000) ? -(0x10000L - u88) / 256.0f : u88 / 256.0f; + return f; +} + +/** + * Get data from temperature + * + * @param float temperature + * + * @return int + */ +unsigned int esp_ot_temperature_to_data(float temperature) +{ + if (temperature < 0) + { + temperature = 0; + } + + if (temperature > 100) + { + temperature = 100; + } + + unsigned int data = (unsigned int)(temperature * 256); + return data; +} + +/** + * Sets bioler status + * + * @param bool enableCentralHeating enable central heating or not + * @param bool enableHotWater + * @param bool enableCooling + * @param bool enableOutsideTemperatureCompensation + * @param bool enableCentralHeating2 + * + * @return long boiler status + */ +unsigned long esp_ot_set_boiler_status( + bool enableCentralHeating, + bool enableHotWater, + bool enableCooling, + bool enableOutsideTemperatureCompensation, + bool enableCentralHeating2) +{ + return esp_ot_send_request(esp_ot_build_set_boiler_status_request(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2)); +} + +/** + * Set boler temperature + * + * @param float temperature target temperature setpoint + * + * @return bool + */ +bool esp_ot_set_boiler_temperature(float temperature) +{ + unsigned long response = esp_ot_send_request(esp_ot_build_set_boiler_temperature_request(temperature)); + return esp_ot_is_valid_response(response); +} + +/** + * Get current boiler temperature + * + * @return float target temperature + */ +float esp_ot_get_boiler_temperature() +{ + unsigned long response = esp_ot_send_request(esp_ot_build_get_boiler_temperature_request()); + return esp_ot_is_valid_response(response) ? esp_ot_get_float(response) : 0; +} + +/** + * Get return temperature data + * + * @return float + */ +float esp_ot_get_return_temperature() +{ + unsigned long response = esp_ot_send_request(esp_ot_build_request(OT_READ_DATA, MSG_ID_TRET, 0)); + return esp_ot_is_valid_response(response) ? esp_ot_get_float(response) : 0; +} + + +/** + * Set hot water setpoint + * + * @param float temperature + * + * @return bool + */ +bool esp_ot_set_dhw_setpoint(float temperature) +{ + unsigned int data = esp_ot_temperature_to_data(temperature); + unsigned long response = esp_ot_send_request(esp_ot_build_request(OT_WRITE_DATA, MSG_ID_TDHW_SET, data)); + return esp_ot_is_valid_response(response); +} + +/** + * Get hot water temperature + * + * @return float + */ +float esp_ot_get_dhw_temperature() +{ + unsigned long response = esp_ot_send_request(esp_ot_build_request(OT_READ_DATA, MSG_ID_TDHW, 0)); + return esp_ot_is_valid_response(response) ? esp_ot_get_float(response) : 0; +} + +/** + * Get modulation + * + * @return float + */ +float esp_ot_get_modulation() +{ + unsigned long response = esp_ot_send_request(esp_ot_build_request(OT_READ_DATA, MSG_ID_REL_MOD_LEVEL, 0)); + return esp_ot_is_valid_response(response) ? esp_ot_get_float(response) : 0; +} + +/** + * Get pressure + * + * @return float + */ +float esp_ot_get_pressure() +{ + unsigned long response = esp_ot_send_request(esp_ot_build_request(OT_READ_DATA, MSG_ID_CH_PRESSURE, 0)); + return esp_ot_is_valid_response(response) ? esp_ot_get_float(response) : 0; +} + +/** + * Is boiler status fault, get ASF flags + * + * @return char [return description] + */ +unsigned char esp_ot_get_fault() +{ + return ((esp_ot_send_request(esp_ot_build_request(OT_READ_DATA, MSG_ID_ASF_FLAGS, 0)) >> 8) & 0xff); +} + +/** + * Get last response status + * + * @return open_therm_response_status_t + */ +open_therm_response_status_t esp_ot_get_last_response_status() +{ + return esp_ot_response_status; +} + + +/** + * Send response + * + * @param long request + * + * @return bool + */ +bool esp_ot_send_response(unsigned long request) +{ + PORT_ENTER_CRITICAL; + const bool ready = esp_ot_is_ready(); + + if (!ready) + { + PORT_EXIT_CRITICAL; + return false; + } + + esp_ot_status = OT_REQUEST_SENDING; + response = 0; + esp_ot_response_status = OT_STATUS_NONE; + + // vTaskSuspendAll(); + + PORT_EXIT_CRITICAL; + + esp_ot_send_bit(1); // start bit + for (int i = 31; i >= 0; i--) + { + esp_ot_send_bit(bitRead(request, i)); + } + esp_ot_send_bit(1); // stop bit + esp_ot_set_idle_state(); + esp_ot_status = OT_READY; + // xTaskResumeAll(); + + return true; +} + + +/** + * Get last response + * + * @return long + */ +unsigned long esp_ot_get_last_response() +{ + return response; +} \ No newline at end of file diff --git a/components/opentherm/opentherm.h b/components/opentherm/opentherm.h new file mode 100644 index 0000000..ca2c645 --- /dev/null +++ b/components/opentherm/opentherm.h @@ -0,0 +1,259 @@ +/** +* @package Opentherm library for ESP-IDF framework +* @author: Mikhail Sazanof +* @copyright Copyright (C) 2024 - Sazanof.ru +* @licence MIT +*/ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" +#include "esp_timer.h" + +static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; +#define PORT_ENTER_CRITICAL portENTER_CRITICAL(&mux) +#define PORT_EXIT_CRITICAL portEXIT_CRITICAL(&mux) + +#ifndef bit +#define bit(b) (1UL << (b)) +#define bitRead(value, bit) ((value >> bit) & 0x01) +#endif + +// ENUMS +typedef enum OpenThermResponseStatus +{ + OT_STATUS_NONE, + OT_STATUS_SUCCESS, + OT_STATUS_INVALID, + OT_STATUS_TIMEOUT +} open_therm_response_status_t; + +typedef enum OpenThermMessageType // old name OpenThermRequestType; // for backwared compatibility +{ + /* Master to Slave */ + OT_READ_DATA = 0b000, + OT_WRITE_DATA = 0b001, + OT_INVALID_DATA = 0b010, + OT_RESERVED = 0b011, + /* Slave to Master */ + OT_READ_ACK = 0b100, + OT_WRITE_ACK = 0b101, + OT_DATA_INVALID = 0b110, + OT_UNKNOWN_DATA_ID = 0b111 +} open_therm_message_type_t; + +typedef enum OpenThermMessageID +{ + MSG_ID_STATUS = 0, // flag8/flag8 Master and Slave Status flags. + MSG_ID_T_SET = 1, // f8.8 Control Setpoint i.e.CH water temperature Setpoint(°C) + MSG_ID_M_CONFIG_M_MEMEBER_ID_CODE = 2, // flag8/u8 Master Configuration Flags / Master MemberID Code + MSG_ID_S_CONFIG_S_MEMEBER_ID_CODE = 3, // flag8/u8 Slave Configuration Flags / Slave MemberID Code + MSG_ID_REMOTE_REQUEST = 4, // u8/u8 Remote Request + MSG_ID_ASF_FLAGS = 5, // flag8/u8 Application - specific fault flags and OEM fault code + MSG_ID_RBP_FLAGS = 6, // flag8/flag8 Remote boiler parameter transfer - enable & read / write flags + MSG_ID_COOLING_CONTROL = 7, // f8.8 Cooling control signal(%) + MSG_ID_T_SET_CH2 = 8, // f8.8 Control Setpoint for 2e CH circuit(°C) + MSG_ID_TR_OVERRIDE = 9, // f8.8 Remote override room Setpoint + MSG_ID_TSP = 10, // u8/u8 Number of Transparent - Slave - Parameters supported by slave + MSG_ID_TSP_INDEX_TSP_VALUE = 11, // u8/u8 Index number / Value of referred - to transparent slave parameter. + MSG_ID_FHB_SIZE = 12, // u8/u8 Size of Fault - History - Buffer supported by slave + MSG_ID_FHB_INDEX_FHB_VALUE = 13, // u8/u8 Index number / Value of referred - to fault - history buffer entry. + MSG_ID_MAX_REL_MOD_LEVEL_SETTINGG = 14, // f8.8 Maximum relative modulation level setting(%) + MSG_ID_MAX_CAPACITY_MIN_MOD_LEVEL = 15, // u8/u8 Maximum boiler capacity(kW) / Minimum boiler modulation level(%) + MSG_ID_TR_SET = 16, // f8.8 Room Setpoint(°C) + MSG_ID_REL_MOD_LEVEL = 17, // f8.8 Relative Modulation Level(%) + MSG_ID_CH_PRESSURE = 18, // f8.8 Water pressure in CH circuit(bar) + MSG_ID_DHW_FLOW_RATE = 19, // f8.8 Water flow rate in DHW circuit. (litres / minute) + MSG_ID_DAY_TIME = 20, // special/u8 Day of Week and Time of Day + MSG_ID_DATE = 21, // u8/u8 Calendar date + MSG_ID_YEAR = 22, // u16 Calendar year + MSG_ID_TR_SET_CH2 = 23, // f8.8 Room Setpoint for 2nd CH circuit(°C) + MSG_ID_TR = 24, // f8.8 Room temperature(°C) + MSG_ID_TBOILER = 25, // f8.8 Boiler flow water temperature(°C) + MSG_ID_TDHW = 26, // f8.8 DHW temperature(°C) + MSG_ID_TOUTSIDE = 27, // f8.8 Outside temperature(°C) + MSG_ID_TRET = 28, // f8.8 Return water temperature(°C) + MSG_ID_TSTORAGE = 29, // f8.8 Solar storage temperature(°C) + MSG_ID_TCOLLECTOR = 30, // f8.8 Solar collector temperature(°C) + MSG_ID_T_FLOW_CH2 = 31, // f8.8 Flow water temperature CH2 circuit(°C) + MSG_ID_TDHW2 = 32, // f8.8 Domestic hot water temperature 2 (°C) + MSG_ID_TEXHAUST = 33, // s16 Boiler exhaust temperature(°C) + MSG_ID_TBOILER_HEAT_EEXCHANGER = 34, // f8.8 Boiler heat exchanger temperature(°C) + MSG_ID_BOILER_FAN_SSPEED_SETPOINT_AND_ACTIAL = 35, // u8/u8 Boiler fan speed Setpoint and actual value + MSG_ID_FLAME_CURRENT = 36, // f8.8 Electrical current through burner flame[μA] + MSG_ID_TR_CH2 = 37, // f8.8 Room temperature for 2nd CH circuit(°C) + MSG_ID_RELATIVE_HUMIDITY = 38, // f8.8 Actual relative humidity as a percentage + MSG_ID_TR_OOVERRIDE2 = 39, // f8.8 Remote Override Room Setpoint 2 + MSG_ID_TDHW_SET_UBT_DHW_SET_LB = 48, // s8/s8 DHW Setpoint upper & lower bounds for adjustment(°C) + MSG_ID_MAX_TSET_UB_MAX_TSET_LB = 49, // s8/s8 Max CH water Setpoint upper & lower bounds for adjustment(°C) + MSG_ID_TDHW_SET = 56, // f8.8 DHW Setpoint(°C) (Remote parameter 1) + MSG_ID_MAX_TSET = 57, // f8.8 Max CH water Setpoint(°C) (Remote parameters 2) + MSG_ID_STATUS_VENTILATION_HEAT_RECOVERY = 70, // flag8/flag8 Master and Slave Status flags ventilation / heat - recovery + MSG_ID_VSET = 71, // -/u8 Relative ventilation position (0-100%). 0% is the minimum set ventilation and 100% is the maximum set ventilation. + MSG_ID_ASF_FLAGS_OEM_FAULT_CODE_VENTILATION_HEAT_RECOVERY = 72, // flag8/u8 Application-specific fault flags and OEM fault code ventilation / heat-recovery + MSG_ID_OEM_DDIAGNOSTIC_CODE_VENTILATION_HEAT_RECOVERY = 73, // u16 An OEM-specific diagnostic/service code for ventilation / heat-recovery system + MSG_ID_S_CONFIG_S_MEMEBER_ID_CODE_VENTILATION_HEAT_RECOVERY = 74, // flag8/u8 Slave Configuration Flags / Slave MemberID Code ventilation / heat-recovery + MSG_ID_OPENTHERM_VVERSION_VENTILATION_HEAT_RECOVERY = 75, // f8.8 The implemented version of the OpenTherm Protocol Specification in the ventilation / heat-recovery system. + MSG_ID_VENTILATION_HEAT_RECOVERY_VERSION = 76, // u8/u8 Ventilation / heat-recovery product version number and type + MSG_ID_REL_VENT_LEVEL = 77, // -/u8 Relative ventilation (0-100%) + MSG_ID_RH_EXHAUST = 78, // -/u8 Relative humidity exhaust air (0-100%) + MSG_ID_CO2_EXHAUST = 79, // u16 CO2 level exhaust air (0-2000 ppm) + MSG_ID_TSI = 80, // f8.8 Supply inlet temperature (°C) + MSG_ID_TSO = 81, // f8.8 Supply outlet temperature (°C) + MSG_ID_TEI = 82, // f8.8 Exhaust inlet temperature (°C) + MSG_ID_TEO = 83, // f8.8 Exhaust outlet temperature (°C) + MSG_ID_RPM_EXHAUST = 84, // u16 Exhaust fan speed in rpm + MSG_ID_RPM_SUPPLY = 85, // u16 Supply fan speed in rpm + MSG_ID_RBP_FLAGS_VENTILATION_HEAT_RECOVERY = 86, // flag8/flag8 Remote ventilation / heat-recovery parameter transfer-enable & read/write flags + MSG_ID_NOMINAL_VENTILATION_VALUE = 87, // u8/- Nominal relative value for ventilation (0-100 %) + MSG_ID_TSP_VENTILATION_HEAT_RECOVERY = 88, // u8/u8 Number of Transparent-Slave-Parameters supported by TSP’s ventilation / heat-recovery + MSG_ID_TSPindexTSP_VALUE_VENTILATION_HEAT_RECOVERY = 89, // u8/u8 Index number / Value of referred-to transparent TSP’s ventilation / heat-recovery parameter. + MSG_ID_FHB_SIZE_VENTILATION_HEAT_RECOVERY = 90, // u8/u8 Size of Fault-History-Buffer supported by ventilation / heat-recovery + MSG_ID_FHB_INDEX_FHB_VALUE_VENTILATION_HEAT_RECOVERY = 91, // u8/u8 Index number / Value of referred-to fault-history buffer entry ventilation / heat-recovery + MSG_ID_BRAND = 93, // u8/u8 Index number of the character in the text string ASCII character referenced by the above index number + MSG_ID_BRAND_VERSION = 94, // u8/u8 Index number of the character in the text string ASCII character referenced by the above index number + MSG_ID_BRAND_SERIAL_NUMBER = 95, // u8/u8 Index number of the character in the text string ASCII character referenced by the above index number + MSG_ID_COOLING_OPERATION_HOURS = 96, // u16 Number of hours that the slave is in Cooling Mode. Reset by zero is optional for slave + MSG_ID_POWER_CYCLES = 97, // u16 Number of Power Cycles of a slave (wake-up after Reset), Reset by zero is optional for slave + MSG_ID_RF_SENSOR_STATUS_INFORMATION = 98, // special/special For a specific RF sensor the RF strength and battery level is written + MSG_ID_REMOTE_OVERRIDE_OOPERATING_MODE_HEATING_DHW = 99, // special/special Operating Mode HC1, HC2/ Operating Mode DHW + MSG_ID_REMOTE_OVERRIDE_FUNCTION = 100, // flag8/- Function of manual and program changes in master and remote room Setpoint + MSG_ID_STATUS_SOLAR_STORAGE = 101, // flag8/flag8 Master and Slave Status flags Solar Storage + MSG_ID_ASF_FLAGS_OEMFAULT_CODE_SOLAR_STORAGE = 102, // flag8/u8 Application-specific fault flags and OEM fault code Solar Storage + MSG_ID_S_CONFIG_S_MEMBER_ID_CODE_SOLAR_STORAGE = 103, // flag8/u8 Slave Configuration Flags / Slave MemberID Code Solar Storage + MSG_ID_SOLAR_STORAGE_VERSION = 104, // u8/u8 Solar Storage product version number and type + MSG_ID_TSP_SOLAR_SSTORAGE = 105, // u8/u8 Number of Transparent - Slave - Parameters supported by TSP’s Solar Storage + MSG_ID_TSP_INDEX_TSP_VALUE_SOLAR_STORAGE = 106, // u8/u8 Index number / Value of referred - to transparent TSP’s Solar Storage parameter. + MSG_ID_FHB_SIZE_SOLAR_STORAGE = 107, // u8/u8 Size of Fault - History - Buffer supported by Solar Storage + MSG_ID_FHB_INDEX_FHB_VALUE_SOLAR_STORAGE = 108, // u8/u8 Index number / Value of referred - to fault - history buffer entry Solar Storage + MSG_ID_ELECTRICITY_PRODUCER_STARTS = 109, // U16 Number of start of the electricity producer. + MSG_ID_ELECTRICITY_PRODUCER_HOURS = 110, // U16 Number of hours the electricity produces is in operation + MSG_ID_ELECTRICITY_PRODUCTION = 111, // U16 Current electricity production in Watt. + MSG_ID_CUMULATIV_ELECTRICITY_PRODUCTION = 112, // U16 Cumulative electricity production in KWh. + MSG_ID_UNSUCCESSFULL_BURNER_STARTS = 113, // u16 Number of un - successful burner starts + MSG_ID_FLAME_SIGNAL_TOO_LOW_NUMBER = 114, // u16 Number of times flame signal was too low + MSG_ID_OEM_DDIAGNOSTIC_CODE = 115, // u16 OEM - specific diagnostic / service code + MSG_ID_SUCESSFULL_BURNER_SSTARTS = 116, // u16 Number of succesful starts burner + MSG_ID_CH_PUMP_STARTS = 117, // u16 Number of starts CH pump + MSG_ID_DHW_PUPM_VALVE_STARTS = 118, // u16 Number of starts DHW pump / valve + MSG_ID_DHW_BURNER_STARTS = 119, // u16 Number of starts burner during DHW mode + MSG_ID_BURNER_OPERATION_HOURS = 120, // u16 Number of hours that burner is in operation(i.e.flame on) + MSG_ID_CH_PUMP_OPERATION_HOURS = 121, // u16 Number of hours that CH pump has been running + MSG_ID_DHW_PUMP_VALVE_OPERATION_HOURS = 122, // u16 Number of hours that DHW pump has been running or DHW valve has been opened + MSG_ID_DHW_BURNER_OOPERATION_HOURS = 123, // u16 Number of hours that burner is in operation during DHW mode + MSG_ID_OPENTERM_VERSION_MASTER = 124, // f8.8 The implemented version of the OpenTherm Protocol Specification in the master. + MSG_ID_OPENTERM_VERSION_SLAVE = 125, // f8.8 The implemented version of the OpenTherm Protocol Specification in the slave. + MSG_ID_MASTER_VERSION = 126, // u8/u8 Master product version number and type + MSG_ID_SLAVE_VERSION = 127, // u8/u8 Slave product version number and type +} open_therm_message_id_t; + +typedef enum OpenThermStatus +{ + OT_NOT_INITIALIZED, + OT_READY, + OT_DELAY, + OT_REQUEST_SENDING, + OT_RESPONSE_WAITING, + OT_RESPONSE_START_BIT, + OT_RESPONSE_RECEIVING, + OT_RESPONSE_READY, + OT_RESPONSE_INVALID +} esp_ot_opentherm_status_t; +// ENUMS + +esp_err_t esp_ot_init( + gpio_num_t _pin_in, + gpio_num_t _pin_out, + bool _esp_ot_is_slave, + void (*esp_ot_process_responseCallback)(unsigned long, open_therm_response_status_t)); + +bool esp_ot_is_ready(); + +unsigned long esp_ot_send_request(unsigned long request); + +bool esp_ot_send_response(unsigned long request); + +bool esp_ot_send_request_async(unsigned long request); + +unsigned long esp_ot_build_request(open_therm_message_type_t type, open_therm_message_id_t id, unsigned int data); + +unsigned long esp_ot_build_response(open_therm_message_type_t type, open_therm_message_id_t id, unsigned int data); + +unsigned long esp_ot_get_last_response(); + +open_therm_response_status_t esp_ot_get_last_response_status(); + +void esp_ot_handle_interrupt(); + +void process(); + +bool parity(unsigned long frame); + +open_therm_message_type_t esp_ot_get_message_type(unsigned long message); + +open_therm_message_id_t esp_ot_get_data_id(unsigned long frame); + +bool esp_ot_is_valid_request(unsigned long request); + +bool esp_ot_is_valid_response(unsigned long response); + +int esp_ot_read_state(); + +void esp_ot_set_active_state(); + +void esp_ot_set_idle_state(); + +void esp_ot_activate_boiler(); + +void esp_ot_send_bit(bool high); + +void esp_ot_process_response(); + +unsigned long esp_ot_build_set_boiler_status_request(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2); + +unsigned long esp_ot_build_set_boiler_temperature_request(float temperature); + +unsigned long esp_ot_build_get_boiler_temperature_request(); + +bool esp_ot_is_fault(unsigned long response); + +bool esp_ot_is_central_heating_active(unsigned long response); + +bool esp_ot_is_hot_water_active(unsigned long response); + +bool esp_ot_is_flame_on(unsigned long response); + +bool esp_ot_is_cooling_active(unsigned long response); + +bool esp_ot_is_diagnostic(unsigned long response); + +uint16_t esp_ot_get_uint(const unsigned long response); + +float esp_ot_get_float(const unsigned long response); + +unsigned int esp_ot_temperature_to_data(float temperature); + +unsigned long esp_ot_set_boiler_status(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2); + +bool esp_ot_set_boiler_temperature(float temperature); + +float esp_ot_get_boiler_temperature(); + +float esp_ot_get_return_temperature(); + +bool esp_ot_set_dhw_setpoint(float temperature); + +float esp_ot_get_dhw_temperature(); + +float esp_ot_get_modulation(); + +float esp_ot_get_pressure(); + +unsigned char esp_ot_get_fault(); + +unsigned long ot_reset(); + +unsigned long esp_ot_get_slave_product_version(); + +float esp_ot_get_slave_ot_version(); \ No newline at end of file diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 0000000..aa2c19b --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "ot_example.c" +PRIV_REQUIRES opentherm driver esp_timer + INCLUDE_DIRS ".") \ No newline at end of file diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild new file mode 100644 index 0000000..7ace8e3 --- /dev/null +++ b/main/Kconfig.projbuild @@ -0,0 +1,13 @@ +menu "OpenTherm Configuration" + config OT_IN_PIN + int "Opentherm in pin" + default 21 + help + Opentherm in pin + + config OT_OUT_PIN + int "Opentherm out pin" + default 22 + help + Opentherm outpin +endmenu \ No newline at end of file diff --git a/main/component.mk b/main/component.mk new file mode 100644 index 0000000..dd03732 --- /dev/null +++ b/main/component.mk @@ -0,0 +1,6 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + +COMPONENT_ADD_INCLUDEDIRS = . include/ \ No newline at end of file diff --git a/main/ot_example.c b/main/ot_example.c new file mode 100644 index 0000000..aaf9e3d --- /dev/null +++ b/main/ot_example.c @@ -0,0 +1,109 @@ +/** +* @package Opentherm library for ESP-IDF framework - EXAMPLE +* @author: Mikhail Sazanof +* @copyright Copyright (C) 2024 - Sazanof.ru +* @licence MIT +*/ + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "opentherm.h" +#include +#include + +#define GPIO_OT_IN GPIO_NUM_21 +#define GPIO_OT_OUT GPIO_NUM_22 +#define ESP_INTR_FLAG_DEFAULT 0 + +volatile float dhwTemp = 0; +volatile float chTemp = 0; +volatile bool fault = false; +static int targetDHWTemp = 59; +static int targetCHTemp = 60; + +static const char *T = "OT"; + +static void IRAM_ATTR esp_ot_process_response_callback(unsigned long response, open_therm_response_status_t esp_ot_response_status) +{ + // ESP_DRAM_LOGW(T, "Response from esp_ot_process_responseCallback!"); + // ESP_DRAM_LOGW(T, "var response From CB: %lu", response); + // ESP_DRAM_LOGW(T, "var esp_ot_response_status from CB: %d", (int)esp_ot_response_status); +} + +void esp_ot_control_task_handler(void *pvParameter) +{ + while (1) + { + unsigned long status = esp_ot_set_boiler_status(false, true, false, false, false); + + ESP_LOGI(T, "====== OPENTHERM ====="); + ESP_LOGI(T, "Free heap size before: %ld", esp_get_free_heap_size()); + open_therm_response_status_t esp_ot_response_status = esp_ot_get_last_response_status(); + if (esp_ot_response_status == OT_STATUS_SUCCESS) + { + ESP_LOGI(T, "Central Heating: %s", esp_ot_is_central_heating_active(status) ? "ON" : "OFF"); + ESP_LOGI(T, "Hot Water: %s", esp_ot_is_hot_water_active(status) ? "ON" : "OFF"); + ESP_LOGI(T, "Flame: %s", esp_ot_is_flame_on(status) ? "ON" : "OFF"); + fault = esp_ot_is_fault(status); + ESP_LOGI(T, "Fault: %s", fault ? "YES" : "NO"); + if (fault) + { + ot_reset(); + } + esp_ot_set_boiler_temperature(targetCHTemp); + ESP_LOGI(T, "Set CH Temp to: %i", targetCHTemp); + + esp_ot_set_dhw_setpoint(targetDHWTemp); + ESP_LOGI(T, "Set DHW Temp to: %i", targetDHWTemp); + + dhwTemp = esp_ot_get_dhw_temperature(); + ESP_LOGI(T, "DHW Temp: %.1f", dhwTemp); + + chTemp = esp_ot_get_boiler_temperature(); + ESP_LOGI(T, "CH Temp: %.1f", chTemp); + + float pressure = esp_ot_get_pressure(); + ESP_LOGI(T, "Slave OT Version: %.1f", pressure); + + unsigned long slaveProductVersion = esp_ot_get_slave_product_version(); + ESP_LOGI(T, "Slave Version: %08lX", slaveProductVersion); + + float slaveOTVersion = esp_ot_get_slave_ot_version(); + ESP_LOGI(T, "Slave OT Version: %.1f", slaveOTVersion); + } + else if (esp_ot_response_status == OT_STATUS_TIMEOUT) + { + ESP_LOGE(T, "OT Communication Timeout"); + } + else if (esp_ot_response_status == OT_STATUS_INVALID) + { + ESP_LOGE(T, "OT Communication Invalid"); + } + else if (esp_ot_response_status == OT_STATUS_NONE) + { + ESP_LOGE(T, "OpenTherm not initialized"); + } + + if (fault) + { + ESP_LOGE(T, "Fault Code: %i", esp_ot_get_fault()); + } + ESP_LOGI(T, "Free heap size after: %ld", esp_get_free_heap_size()); + ESP_LOGI(T, "====== OPENTHERM =====\r\n\r\n"); + + vTaskDelay(1000 / portTICK_PERIOD_MS); + } +} + +void app_main() +{ + esp_ot_init( + GPIO_OT_IN, + GPIO_OT_OUT, + false, + esp_ot_process_response_callback); + + xTaskCreate(esp_ot_control_task_handler, T, configMINIMAL_STACK_SIZE * 4, NULL, 3, NULL); + vTaskSuspend(NULL); +}