Добрый день, в контексте моего хобби по схемотехнике и программированию микроконтроллеров появилась идея реализовать устройство для дистанционного запуска предпускового котла Webasto, в моем случае это “Webasto Thermo Top Evo 5”. Вероятно предложенный материал подойдет и для реализации устройств управления для схожих отопителей данной марки работающих по протоколу W-bus.
В данной статья я затрону базовый вариант реализации и разделю публикацию на две части, принципиальную схему устройства и непосредственно программную часть.
Аппаратная часть

Я логически разделил схему на две части: питание и управление.
Узел питания построен на линейном стабилизаторе напряжения LM7805, импользуемый мною позволяет пропускать до 500 мА ток в теории, на практике - рекомендую брать корпус ТО-220 и крепить к нему радиатор, так как потребление микроконтроллера в среднем будет 50 мА в активном состоянии и 150 мА будет потреблять WiFi или LoRa в момент передачи, опять же нужно смотреть конкретные даташиты. В моем случае без радиатора летом - стабилизатор грелся и тротлил.
Обвязка микросхемы L9637D в целом не сложная, в качестве микроконтроллера можно взять любой удобный, важно наличие UART интерфейса. К Vcc(3) ноге микросхемы L9637D важно подключить питание, уровень которого соответствует уровню логике, тоесть если логи 3.3v и UART приниамет 3.3v, то и к Vcc нужно подключать 3.3v. В моем случае 5v идет на саму “dev” плату, а 3.3v уже получается находящимся на плате линейным регулятором.
Подключение к Webasto
Тут есть несколько вариантов.
Если у вас уже есть штатный таймер в салоне, то в таком случае необходимые +12v, GND, W-bus подключения у вас есть, можно подключиться к ним (красный +12v; коричневый - GND; желтый - W-bus).
Если в салон ничего не выведено и допустим модуль управления по GSM установлен в подкапотном пространстве, то можно подключиться к нему, но нужно будет позаботиться о водонепроницаемости корпуса вашего устройства.
Программная часть
Тут лучше начать с краткого описания протокола (нужно понимать, что описаное в данной статье было получено опытнум путем)
Переда данных в случае наличия одного провода у нас полудуплексная (в один момент времени мы либо отправляем данные/пакет данных, либо принимаем). Схема данных следующая (запрос или ответ):
адресация длина пакета данные контрольная сумма | 1-байт | 1-байт | N-байт | 1-байт |
Видно, что один пакет не может иметь длину больше чем 255 байт. А данные не могут превышать длину в 252 байта.
Теперь немного подробнее о протоколе:
1й байт - это адресация, 0x4 - Webasto; 0x3 - таймер; 0x2 - Telestart; 0xF - тестовый клиент. Первые 4 бита это отправитель, вторые это получатель. Пример 0xF4 - отправляем от клиентскогго устройства к Webasto; 0x4F - получаем ответ от Webasto клиентскому устройству.
2й байт - длина пакета, считается как оставшаяся длина данных + контрольная самма, иными словами это общая длина всех передаваемых данных за минусом двух байт
3й-N байт - непосредственно данные, тут важно отметить, что первый байт в данных является командой и в ответ со стороны Webasto приходит тоже число + выставленный в 1 старший бит. Пример отправляем 0x10 (команда на остановку котла), ответ будет 0x90, тоесть в ответ прибавляется 0x80 к команде.
последний байт - контрольная сумма, считается как последовательная XOR операция, от 1го байта до последнего байта данных
Приведу несколько примеров для основных команд управления котлом:
Остановка котла:
Запрос [ 34 02 10 26 ]
Ответ [ 43 02 90 D1 ]
Запуск котла:
Запрос [ 34 03 21 1E 08 ]
4й байт в данном случае это время - 30min
Ответ [ 43 03 A1 1E FF ]
3й байт в ответе это команда 0x21 + 0x80 (старший бит выставленный в 1)
Hartbeat котла:
Необходимо отправлять каждые 10-15 сек. после запуска котла, в противном случае при отсутствии hartbeat - котел остановится.
Эту команду можно так-же использовать для получения статус - запущен ли котел.
Запрос [ 34 04 44 21 00 55 ]
4й байт в запросе это команда для которой мы отправляем Hartbeat
Ответ [ 43 03 C4 00 84 ]
4й байт в ответе это результат Hartbeat (0 - если котел запущен и работает в штатном режиме, 1 - если котел не находится в режиме запуска определенной команды, например 21)
Реализация
Данная реализация использует фреймфорк esp-idf, но в целом не составит большого труда перенести ее на Arduino или любой другой. В данной статье я представлю основные критические секции кода, раскрывающие основной принцип работы. Параметры UART следующие: 2400_8E1
Для начала определим наш объект для хранения состояния
typedef struct wbus_io_request { uint8_t tx_buffer[WBUS_IO_TX_BUFFER_SIZE]; size_t tx_buffer_size; } wbus_io_request_t; typedef struct wbus_io_sensors { wbus_io_param_float_t battery; wbus_io_param_int8_t coolant; } wbus_io_sensors_t; typedef struct wbus_io { int resource_id; uint32_t tx_timestamp; volatile uint8_t transport_state; volatile uint8_t heater_state; volatile uint32_t heater_started_time; volatile uint32_t heater_duration_time; volatile bool enabled; wbus_io_sensors_t sensors; wbus_io_stats_err_t stats_err; uint8_t error_buffer[WBUS_IO_ERROR_LIST_SIZE * sizeof(wbus_io_error_t)]; ring_buffer_t errors; // esp-idf (переменные и типы специфичные для фрейморка) SemaphoreHandle_t mutex; esp_event_loop_handle_t event_loop; uint8_t request_queue_buffer[WBUS_IO_REQUEST_QUEUE_SIZE * sizeof(wbus_io_request_t)]; StaticQueue_t request_queue_ctx; } wbus_io_t; #define WBUS_ADDR_RX 0x4F #define WBUS_ADDR_TX 0xF4 #define WBUS_CMD_OFF 0x10 #define WBUS_CMD_ON_PH 0x21 #define WBUS_CMD_CHK 0x44 #define WBUS_CMD_QUERY 0x50 #define WBUS_QUERY_PARAMS 0x30 #define WBUS_RESP_APPROVE_FLAG_MASK 0x80 #define WBUS_PARAM_BATTERY_VOLTAGE 0x0E #define WBUS_PARAM_COOLING_LIQUID_TEMPERATURE 0x0C #define WBUS_PROTO_ADDR 0 #define WBUS_PROTO_SIZE 1 #define WBUS_PROTO_CMD 2 #define WBUS_PROTO_BODY_START 3 #define WBUS_CMD_CHK_REPLY_OK 0
Далее определим доп. функции, в нашем случае это рассчет контрольной суммы и функцию подготовки пакета
uint8_t checksum(uint8_t *buf, uint8_t len, uint8_t chk) { for (; len != 0; len--) { chk ^= *buf++; } return chk; } static wbus_err_t query_prepare(wbus_io_t *ctx, uint8_t command, uint8_t tx_size, wbus_io_request_t *request) { request->tx_buffer_size = tx_size; request->tx_buffer[0] = WBUS_ADDR_TX; // адресация request->tx_buffer[1] = request->tx_buffer_size - 2; // длина пакета request->tx_buffer[2] = command; // команда request->tx_buffer[request->tx_buffer_size - 1] = checksum( (uint8_t*)request->tx_buffer, request->tx_buffer_size - 1, 0); // контрльная сумма return xQueueSend((QueueHandle_t) &ctx->request_queue_ctx, request, 0) == pdTRUE ? WBUS_IO_ERR_NONE : WBUS_IO_ERR_INVALID_STATE; }
Так как связь полудуплексная, а Webastо взаимодействует реактивным способом (пока не будет запроса, никаких инициирующих передач данных со стороны котла не будет), запросы и ответы должны быть сериализованы, использование очереди запросов будет наиболее разумным подходом. Обработчик будет работать в отдельном потоке.
static void query_executor(void *arg) { ESP_LOGI(TAG_UART_WBUS, "query task started"); wbus_io_t *ctx = (wbus_io_t *)arg; wbus_io_request_t request; int tx_size, rx_size; uint8_t try_iteration; uint8_t _checksum; uint8_t rx_buffer[WBUS_IO_RX_BUFFER_SIZE]; wbus_io_error_t error; bool query_successfull; while (true) { // достаем подготовленный запрос из очереди (блокирующая операция) if (xQueueReceive((QueueHandle_t) &ctx->request_queue_ctx, &request, portMAX_DELAY) != pdTRUE) { ESP_LOGI(TAG_UART_WBUS, "no requests, continue"); continue; } xSemaphoreTake(ctx->mutex, WBUS_IO_MUTEX_LOCK_TIMEOUT); ctx->transport_state = WBUS_IO_TRANSPORT_STATE_BUSY; xSemaphoreGive(ctx->mutex); try_iteration = WBUS_IO_RETRY_COUNT; query_successfull = false; // так как котел может не ответить, такое может происходить не��колько раз - в цикле пытаемся отправить запрос и ждать ответа while (true) { if (try_iteration == 0) { ESP_LOGE(TAG_UART_WBUS, "retry attempts = 0"); error.error_id = WBUS_IO_ERR_PACK_INCOMPLETE; ring_buffer_write(&ctx->errors, &error); break; } try_iteration--; ESP_LOGI(TAG_UART_WBUS, "-- retries left %d --------", try_iteration); // перед каждой попыткой - сбрасываем данные из входного буфера (UART) uart_flush(ctx->resource_id); // update last tx timestamp ctx->tx_timestamp = esp_timer_get_time() / 1000; // сразу отправляем весь пакет ESP_LOGD(TAG_UART_WBUS, "Ctrl --> Heater"); ESP_LOG_BUFFER_HEX_LEVEL(TAG_UART_WBUS, request.tx_buffer, request.tx_buffer_size, ESP_LOG_DEBUG); tx_size = uart_write_bytes(ctx->resource_id, request.tx_buffer, request.tx_buffer_size); if (tx_size != request.tx_buffer_size) { ESP_LOGE(TAG_UART_WBUS, "send query stage; tx_size(%d) != tx_buffer_size(%d)", tx_size, request.tx_buffer_size); error.error_id = WBUS_IO_ERR_TX; ring_buffer_write(&ctx->errors, &error); break; } // так как коммуникауия происходит по одному проводу rx/tx закольцованы - нам нужно вычитать эхо из входящего буфера if (!uart_read_bytes_until( ctx->resource_id, rx_buffer, request.tx_buffer_size, (size_t *)&rx_size, WBUS_IO_RX_TIMEOUT)) { ESP_LOGE(TAG_UART_WBUS, "read echo stage; rx_size(%d) != required(%d)", rx_size, request.tx_buffer_size); error.error_id = WBUS_IO_ERR_PACK_INCOMPLETE; ring_buffer_write(&ctx->errors, &error); break; } // вычитываем заголово (адрес, длину пакета) if (!uart_read_bytes_until( ctx->resource_id, rx_buffer, 2, (size_t *)&rx_size, WBUS_IO_RX_TIMEOUT)) { ESP_LOGE(TAG_UART_WBUS, "read header stage; timeout rx_size(%d) != required(%d)", rx_size, 2); continue; } ESP_LOGD(TAG_UART_WBUS, "Heater --> Ctrl (header)"); ESP_LOG_BUFFER_HEX_LEVEL(TAG_UART_WBUS, rx_buffer, rx_size, ESP_LOG_DEBUG); if (rx_buffer[WBUS_PROTO_ADDR] != WBUS_ADDR_RX) { ESP_LOGE(TAG_UART_WBUS, "read header stage; addr(%0X) != %0X", rx_buffer[0], WBUS_ADDR_RX); error.error_id = WBUS_IO_ERR_PACK; ring_buffer_write(&ctx->errors, &error); break; } if (rx_buffer[WBUS_PROTO_SIZE] == 0) { ESP_LOGE(TAG_UART_WBUS, "read header stage; size = 0"); error.error_id = WBUS_IO_ERR_PACK; ring_buffer_write(&ctx->errors, &error); break; } // вычитываем оставшийся ответ (данные, контрльная сумма) if (!uart_read_bytes_until( ctx->resource_id, &rx_buffer[2], rx_buffer[WBUS_PROTO_SIZE], (size_t *)&rx_size, WBUS_IO_RX_TIMEOUT)) { ESP_LOGE(TAG_UART_WBUS, "read body stage; timeout rx_size(%d) != required(%d)", rx_size, rx_buffer[WBUS_PROTO_SIZE]); continue; } ESP_LOGD(TAG_UART_WBUS, "Heater --> Ctrl (body)"); ESP_LOG_BUFFER_HEX_LEVEL(TAG_UART_WBUS, &rx_buffer[2], rx_size, ESP_LOG_DEBUG); // проверяем контрльную сумму и команду которую нам в ответ пслал котел rx_size = rx_size + 2; _checksum = checksum(rx_buffer, rx_size - 1, 0); if (rx_buffer[rx_size - 1] != _checksum) { ESP_LOGE(TAG_UART_WBUS, "read body stage; checksum faild; recevied(%0X) != actual(%0X)", rx_buffer[rx_size - 1], _checksum); error.error_id = WBUS_IO_ERR_CHECKSUM; ring_buffer_write(&ctx->errors, &error); break; } if (rx_buffer[WBUS_PROTO_CMD] - WBUS_RESP_APPROVE_FLAG_MASK != request.tx_buffer[WBUS_PROTO_CMD]) { ESP_LOGE(TAG_UART_WBUS, "read body stage; command; recevied(%0X) != actual(%0X)", rx_buffer[WBUS_PROTO_CMD] - WBUS_RESP_APPROVE_FLAG_MASK, request.tx_buffer[WBUS_PROTO_CMD]); error.error_id = WBUS_IO_ERR_CMD; ring_buffer_write(&ctx->errors, &error); break; } query_successfull = true; ESP_LOGI(TAG_UART_WBUS, "response successfully receviced and checked"); break; } xSemaphoreTake(ctx->mutex, WBUS_IO_MUTEX_LOCK_TIMEOUT); ctx->transport_state = WBUS_IO_TRANSPORT_STATE_READY; xSemaphoreGive(ctx->mutex); if (query_successfull) { // если все проверки прошли успешно - обрабатываем ответ котла query_handle_response(ctx, rx_buffer, rx_size); } else { ESP_LOGE(TAG_UART_WBUS, "query failed"); post_event(ctx, WBUS_IO_EVENT_ERR_QUERY); } } }
Далее определим верхнеуровневые функции которые формируют специфичные для конкретной операции данные для отправки
// команда остановки котла wbus_err_t wbus_io_cmd_stop(wbus_io_t *ctx) { if (!ctx->enabled) { return WBUS_IO_ERR_DISABLED; } wbus_io_request_t request; return query_prepare(ctx, WBUS_CMD_OFF, 4, &request); } // команда запуска котла wbus_err_t wbus_io_cmd_start(wbus_io_t *ctx, uint8_t minutes) { if (!ctx->enabled) { return WBUS_IO_ERR_DISABLED; } wbus_io_request_t request; request.tx_buffer[3] = minutes; return query_prepare(ctx, WBUS_CMD_ON_PH, 5, &request); } // команда отправки Hartbeat wbus_err_t wbus_io_cmd_check(wbus_io_t *ctx) { if (!ctx->enabled) { return WBUS_IO_ERR_DISABLED; } wbus_io_request_t request; request.tx_buffer[3] = WBUS_CMD_ON_PH; request.tx_buffer[4] = 0; return query_prepare(ctx, WBUS_CMD_CHK, 6, &request); }
