Signed-off-by: Mikhail Sazanov <m@sazanof.ru>
This commit is contained in:
Mikhail Sazanov 2024-06-24 20:58:38 +03:00
commit 70c3ffd31f
10 changed files with 1308 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.devcontainer
.vscode
build
sdkconfig

10
CMakeLists.txt Executable file
View file

@ -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)

59
README.md Executable file
View file

@ -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 <chip_name>`.
### 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.

View file

@ -0,0 +1,5 @@
idf_component_register(
SRCS opentherm.c
INCLUDE_DIRS .
PRIV_REQUIRES driver esp_timer log
)

View file

@ -0,0 +1,840 @@
/**
* @package Opentherm library for ESP-IDF framework
* @author: Mikhail Sazanof
* @copyright Copyright (C) 2024 - Sazanof.ru
* @licence MIT
*/
#include <stdio.h>
#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;
}

View file

@ -0,0 +1,259 @@
/**
* @package Opentherm library for ESP-IDF framework
* @author: Mikhail Sazanof
* @copyright Copyright (C) 2024 - Sazanof.ru
* @licence MIT
*/
#include <stdio.h>
#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 TSPs ventilation / heat-recovery
MSG_ID_TSPindexTSP_VALUE_VENTILATION_HEAT_RECOVERY = 89, // u8/u8 Index number / Value of referred-to transparent TSPs 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 TSPs Solar Storage
MSG_ID_TSP_INDEX_TSP_VALUE_SOLAR_STORAGE = 106, // u8/u8 Index number / Value of referred - to transparent TSPs 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();

3
main/CMakeLists.txt Normal file
View file

@ -0,0 +1,3 @@
idf_component_register(SRCS "ot_example.c"
PRIV_REQUIRES opentherm driver esp_timer
INCLUDE_DIRS ".")

13
main/Kconfig.projbuild Normal file
View file

@ -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

6
main/component.mk Normal file
View file

@ -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/

109
main/ot_example.c Normal file
View file

@ -0,0 +1,109 @@
/**
* @package Opentherm library for ESP-IDF framework - EXAMPLE
* @author: Mikhail Sazanof
* @copyright Copyright (C) 2024 - Sazanof.ru
* @licence MIT
*/
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "opentherm.h"
#include <esp_log.h>
#include <esp_err.h>
#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);
}