From 51438e7bf9dd6c1cd38cab7e615ba88981adc9ed Mon Sep 17 00:00:00 2001 From: kislovdm Date: Mon, 21 Jul 2025 21:59:04 +0300 Subject: [PATCH] ++ --- coffee-philips-5400.yaml | 26 +- components/saeco_series_6000/__init__.py | 56 +++- .../__pycache__/__init__.cpython-311.pyc | Bin 3488 -> 7476 bytes .../__pycache__/button.cpython-311.pyc | Bin 0 -> 207 bytes components/saeco_series_6000/crc_test.cpp | 70 ++++ components/saeco_series_6000/platformio.ini | 1 + .../saeco_series_6000/saeco_series_6000.cpp | 307 +++++++++++++----- .../saeco_series_6000/saeco_series_6000.h | 27 ++ 8 files changed, 394 insertions(+), 93 deletions(-) create mode 100644 components/saeco_series_6000/__pycache__/button.cpython-311.pyc create mode 100644 components/saeco_series_6000/crc_test.cpp create mode 100644 components/saeco_series_6000/platformio.ini diff --git a/coffee-philips-5400.yaml b/coffee-philips-5400.yaml index 5bb56a3..cab9587 100644 --- a/coffee-philips-5400.yaml +++ b/coffee-philips-5400.yaml @@ -16,14 +16,14 @@ esp32: # Enable logging #Журналирование logger: - level: DEBUG #NONE + level: INFO #NONE baud_rate: 0 # Enable Home Assistant API api: -# web_server: -# port: 80 +web_server: + port: 80 ota: platform: esphome @@ -67,11 +67,10 @@ external_components: path: ./components saeco_series_6000: + id: saeco_series_6000_component uart_display: uart_display uart_mainboard: uart_mainboard debug: true - - #Сенсор в который компонент публикует статус в цифровом виде state_sensor: name: Work State id: idControlSensor @@ -172,4 +171,19 @@ text_sensor: name: "Error Code" id: idErrorCode icon: mdi:alert-circle-outline - update_interval: never \ No newline at end of file + update_interval: never + +button: + - platform: template + name: "Отправить три пакета" + on_press: + then: + - lambda: |- + std::vector> pkts = { + {0xAA,0xAA,0xAA,0x90,0x0B,0x0A,0x05,0x03,0x00,0x02,0x00,0x00,0x64,0x00,0x00,0x00,0x39,0xBF,0x5B,0x74,0x55}, + }; + id(saeco_series_6000_component)->send_packets_to_mainboard(pkts, 50); + +#[20:45:34][I][saeco_series_6000:079]: Packet from Display: AA AA AA 93 0A 01 01 57 15 3A ED 55 +#[20:45:34][I][saeco_series_6000:079]: Packet from Display: AA AA AA 90 0B 0A 05 03 00 02 00 00 64 00 00 00 39 BF 5B 74 55 +#[20:45:35][I][saeco_series_6000:079]: Packet from Display: AA AA AA 91 0C 01 03 42 C0 B0 AD 55" \ No newline at end of file diff --git a/components/saeco_series_6000/__init__.py b/components/saeco_series_6000/__init__.py index 85acddf..96f4b7e 100644 --- a/components/saeco_series_6000/__init__.py +++ b/components/saeco_series_6000/__init__.py @@ -2,8 +2,9 @@ DEPENDENCIES = ["uart", "sensor"] import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import uart, sensor +from esphome.components import uart, sensor, button from esphome.const import CONF_ID, CONF_DEBUG +import esphome.automation as automation CONF_STATE_SENSOR = "state_sensor" CONF_B0_STATUS_SENSOR = "b0_status_sensor" @@ -18,12 +19,14 @@ CONF_PALLET_SENSOR = "pallet_sensor" CONF_ERROR_CODE_SENSOR = "error_code_sensor" CONF_GRAIN_TRAY_SENSOR = "grain_tray_sensor" CONF_STATUS_TEXT_SENSOR = "status_text_sensor" +CONF_PACKETS = "packets" +CONF_SAECO_ID = "saeco_id" saeco_series_6000_ns = cg.esphome_ns.namespace('saeco_series_6000') SaecoSeries6000 = saeco_series_6000_ns.class_('SaecoSeries6000', cg.Component) CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(SaecoSeries6000), + cv.Required(cv.CONF_ID): cv.declare_id(SaecoSeries6000), cv.Required('uart_display'): cv.use_id(uart.UARTComponent), cv.Required('uart_mainboard'): cv.use_id(uart.UARTComponent), cv.Optional(CONF_DEBUG, default=False): cv.boolean, @@ -45,4 +48,51 @@ def to_code(config): if CONF_STATE_SENSOR in config: sens = yield sensor.new_sensor(config[CONF_STATE_SENSOR]) - cg.add(var.set_status_sensor(sens)) \ No newline at end of file + cg.add(var.set_status_sensor(sens)) + +def validate_packet(value): + if isinstance(value, str): + return [int(x, 16) for x in value.split()] + return [cv.hex_uint8_t(x) for x in value] + +SEND_PACKETS_ACTION_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.use_id(SaecoSeries6000), + cv.Required(CONF_PACKETS): [cv.All(validate_packet)], + cv.Optional("delay_ms", default=10): cv.positive_int, +}) + +SendPacketsAction = saeco_series_6000_ns.class_("SendPacketsAction", automation.Action) + +@automation.register_action( + "saeco_series_6000.send_packets", + SendPacketsAction, + SEND_PACKETS_ACTION_SCHEMA, +) +def send_packets_to_mainboard_action_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id) + parent = yield cg.get_variable(config[CONF_ID]) + var.set_parent(parent) + var.set_packets(config[CONF_PACKETS]) + var.set_delay_ms(config.get("delay_ms", 10)) + return var + +SaecoSendPacketsButton = saeco_series_6000_ns.class_("SaecoSendPacketsButton", button.Button, cg.Component) + +SAECO_SEND_PACKETS_BUTTON_SCHEMA = button.BUTTON_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SaecoSendPacketsButton), + cv.Required(CONF_ID): cv.use_id(SaecoSeries6000), + cv.Required(CONF_PACKETS): [cv.All(validate_packet)], + cv.Optional("delay_ms", default=10): cv.positive_int, +}).extend(cv.COMPONENT_SCHEMA) + +async def saeco_send_packets_button_to_code(config): + parent = await cg.get_variable(config[CONF_SAECO_ID]) + var = cg.new_Pvariable(config["id"]) + await cg.register_component(var, config) + await button.register_button(var, config) + cg.add(var.set_parent(parent)) + cg.add(var.set_packets(config[CONF_PACKETS])) + cg.add(var.set_delay_ms(config.get("delay_ms", 10))) + return var + +button.register_button("saeco_series_6000", saeco_send_packets_button_to_code) \ No newline at end of file diff --git a/components/saeco_series_6000/__pycache__/__init__.cpython-311.pyc b/components/saeco_series_6000/__pycache__/__init__.cpython-311.pyc index f7395f9389a9196346b31f63bc164a6157f19617..bebca0b3507ff25646520fe02e99f5970f91162f 100644 GIT binary patch literal 7476 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 H^Vfp+ZW@oG zkUyYhdhsGTRm2~ch$r#l&5IR0$ez5amwMHM&ZOj^_TXdQW8V9|_q{jF%X6J!>#=DX zNZRK+_sV==+scwM+0LK7fe{eUszxor615~t%3NnA3@{5~ECq3v27}pSGqmX7U*enZ!EXN}iPpK>G3=2EsI77P_p{LRhjE(S-&Jl%Z8Oa_Mk0P!!MiAF4 zNBf*!HZEQy>0<9X(Fge0pJF}qSRV!F5cGH-<*~{MF&Cq!{%WHC(w!$}B>j4+VEKI6dPf#$egz_@{h+U{PpjG7;gJqejKX1Om;$iNL zJF|xlIiJLSE)&^91wp+hM`vEV^`2WOg=hCw H=uP|$ok^L& diff --git a/components/saeco_series_6000/__pycache__/button.cpython-311.pyc b/components/saeco_series_6000/__pycache__/button.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f93aee1175dc427d898ed08c239a0f1cba298f98 GIT binary patch literal 207 zcmZ3^%ge<81WVfLG6aD1V-N=hn4pZ$d_cx@h7^Vr#vFzah7_h?22JLdj6gw6##@Y9 zen668CBtWs>R*0tRxtrZ`B|ySCB-qR#RVDpxv4R(!2upXS~t`&C`8w(C^IEJH6}Sf zw;(?+H4mt)I59OjKfXA%C^NM<-ps(jASS7_q$EF2ub}c5hfQvNN@-52T@eS+c#xgN WLO|jJGb1D84F>)OI4oiV3IhPnqcxHM literal 0 HcmV?d00001 diff --git a/components/saeco_series_6000/crc_test.cpp b/components/saeco_series_6000/crc_test.cpp new file mode 100644 index 0000000..1231524 --- /dev/null +++ b/components/saeco_series_6000/crc_test.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include + +static const uint8_t BitReverse[256] = { + 0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0, + 0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8, + 0x04,0x84,0x44,0xC4,0x24,0xA4,0x64,0xE4,0x14,0x94,0x54,0xD4,0x34,0xB4,0x74,0xF4, + 0x0C,0x8C,0x4C,0xCC,0x2C,0xAC,0x6C,0xEC,0x1C,0x9C,0x5C,0xDC,0x3C,0xBC,0x7C,0xFC, + 0x02,0x82,0x42,0xC2,0x22,0xA2,0x62,0xE2,0x12,0x92,0x52,0xD2,0x32,0xB2,0x72,0xF2, + 0x0A,0x8A,0x4A,0xCA,0x2A,0xAA,0x6A,0xEA,0x1A,0x9A,0x5A,0xDA,0x3A,0xBA,0x7A,0xFA, + 0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6, + 0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE, + 0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1, + 0x09,0x89,0x49,0xC9,0x29,0xA9,0x69,0xE9,0x19,0x99,0x59,0xD9,0x39,0xB9,0x79,0xF9, + 0x05,0x85,0x45,0xC5,0x25,0xA5,0x65,0xE5,0x15,0x95,0x55,0xD5,0x35,0xB5,0x75,0xF5, + 0x0D,0x8D,0x4D,0xCD,0x2D,0xAD,0x6D,0xED,0x1D,0x9D,0x5D,0xDD,0x3D,0xBD,0x7D,0xFD, + 0x03,0x83,0x43,0xC3,0x23,0xA3,0x63,0xE3,0x13,0x93,0x53,0xD3,0x33,0xB3,0x73,0xF3, + 0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB, + 0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7, + 0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF +}; + +uint32_t calc_crc(const std::vector& data, size_t start, size_t end) { + uint32_t crc = 0xFFFFFFFF; + const uint32_t POLY = 0x04C11DB7; + if (start >= end) return 0; + uint8_t val = data[start]; + ((uint8_t*)(&crc))[3] ^= BitReverse[val]; + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x80000000) { + crc = (crc << 1) ^ POLY; + } else { + crc <<= 1; + } + } + for (size_t i = start + 1; i < end; i++) { + uint8_t v = data[i]; + ((uint8_t*)(&crc))[3] ^= BitReverse[v]; + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x80000000) { + crc = (crc << 1) ^ POLY; + } else { + crc <<= 1; + } + } + } + uint32_t t32 = 0; + ((uint8_t*)(&t32))[0] = BitReverse[((uint8_t*)(&crc))[3]]; + ((uint8_t*)(&t32))[1] = BitReverse[((uint8_t*)(&crc))[2]]; + ((uint8_t*)(&t32))[2] = BitReverse[((uint8_t*)(&crc))[1]]; + ((uint8_t*)(&t32))[3] = BitReverse[((uint8_t*)(&crc))[0]]; + crc = t32 ^ 0xFFFFFFFF; + return crc; +} + +int main() { + // Пример команды (без CRC и 0x55): + std::vector pkt = { + 0xAA,0xAA,0xAA,0x90,0x0B,0x0A,0x05,0x03,0x00,0x02,0x00,0x00,0x64,0x00,0x00,0x00,0x39,0xBF,0x5B,0x74,0x55 + }; + size_t preamble = 3; + size_t crc_start = preamble; + size_t crc_end = pkt.size() - 5; + uint32_t crc = calc_crc(pkt, crc_start, crc_end); + printf("CRC for test packet: %08X\n", crc); + // assert(crc == 0x????????); // подставьте ожидаемое значение CRC + return 0; +} \ No newline at end of file diff --git a/components/saeco_series_6000/platformio.ini b/components/saeco_series_6000/platformio.ini new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/components/saeco_series_6000/platformio.ini @@ -0,0 +1 @@ + diff --git a/components/saeco_series_6000/saeco_series_6000.cpp b/components/saeco_series_6000/saeco_series_6000.cpp index a05bcf0..2e10609 100644 --- a/components/saeco_series_6000/saeco_series_6000.cpp +++ b/components/saeco_series_6000/saeco_series_6000.cpp @@ -68,103 +68,128 @@ void SaecoSeries6000::process_mainboard_byte(uint8_t byte) { } } -void SaecoSeries6000::parse_display_packet(const uint8_t* buffer, uint16_t size) { - if (debug_) { - ESP_LOGD(TAG, "parse_display_packet: size=%u", size); +void SaecoSeries6000::update_display_counter_from_packet(const uint8_t* buffer, uint16_t size) { + if (size > 4 && buffer[3] != 0xFF) { + last_display_counter_ = buffer[4]; } - // Здесь больше не парсим B0-пакеты, только debug } -void SaecoSeries6000::parse_mainboard_packet(const uint8_t* buffer, uint16_t size) { - last_activity_time = millis(); // сбрасываем таймер активности при любом пакете +void SaecoSeries6000::parse_display_packet(const uint8_t* buffer, uint16_t size) { if (debug_) { std::vector hex_buffer; hex_buffer.resize(size * 3 + 1); for (uint16_t i = 0; i < size; i++) { sprintf(hex_buffer.data() + i * 3, "%02X ", buffer[i]); } - ESP_LOGD(TAG, "Packet from Mainboard: %s", hex_buffer.data()); - ESP_LOGD(TAG, "parse_mainboard_packet: size=%u", size); + if (size > 4 && buffer[3] != 0xFF){ + ESP_LOGI(TAG, "Packet from Display: %s", hex_buffer.data()); + } + } + update_display_counter_from_packet(buffer, size); + // Здесь больше не парсим B0-пакеты, только debug +} + +// Разбор пакета B0 (системные статусы кофемашины) +// buffer[6], buffer[7], buffer[9] содержат коды состояния +void SaecoSeries6000::parse_b0_packet(const uint8_t* buffer, uint16_t size) { + if (size < 10) return; + if (buffer[6] == 0x0E) { + if (buffer[9] == 0x00) { + pub_status(20); // 20: "Опорож. контейнер для коф. гущи" + } else { + if ((buffer[9] & 0x40) != 0) { + pub_status(22); // 22: "Воды нет" + } else { + pub_status(17); // 17: "Вода есть" + } + if ((buffer[9] & 0x80) != 0) { + pub_status(23); // 23: "Извлечен контейнер для жмыха" + } else { + pub_status(18); // 18: "Вставлен контейнер для жмыха" + } + } + } else if (buffer[6] == 0x06) { + pub_status(21); // 21: "Готово/Выберите напиток" + } else if (buffer[6] == 0x0C) { + if (buffer[7] == 0x01) { + pub_status(8); // 8: "Наслаждайтесь" + } else if (buffer[7] == 0x02) { + pub_status(15); // 15: "Что-то (07 0C 02)" + } + } else if (buffer[6] == 0x07) { + if (buffer[7] == 0x0E) { + pub_status(9); // 9: "Нагрев воды" + } else if (buffer[7] == 0x0D) { + pub_status(10); // 10: "Перемалываем зерна" + } else if (buffer[7] == 0x10) { + pub_status(3); // 3: "Наливаем молоко" + } else if (buffer[7] == 0x11) { + pub_status(4); // 4: "Наливаем кофе" + } else if (buffer[7] == 0x12) { + pub_status(5); // 5: "Предварительное дозирование" + } else if (buffer[7] == 0x13) { + pub_status(6); // 6: "Создание пара для молока" + } else if (buffer[7] == 0x14) { + pub_status(7); // 7: "Заварочный узел в положение заваривания" + } else if (buffer[7] == 0x15) { + pub_status(15); // 15: "Наслаждайтесь" + } + } else if (buffer[6] == 0x08) { + if (buffer[7] == 0x0E) { + pub_status(14); // 14: "Нагревание" + } else if (buffer[7] == 0x02) { + pub_status(11); // 11: "Промывка" + } else if (buffer[7] == 0x14) { + pub_status(13); // 13: "Что-то (07 08 14)" + } else if (buffer[7] == 0x05) { + pub_status(12); // 12: "Зерна закончились" + } else if (buffer[7] == 0x16) { + pub_status(24); // 24: "Удаление накипи стадия 1" + } else if (buffer[7] == 0x18) { + pub_status(25); // 25: "Удаление накипи стадия 2" + } + } else if (buffer[7] == 0x00) { + if (buffer[6] == 0x01) { + pub_status(1); // 1: "Что-то (07 01 00)" + } else if (buffer[6] == 0x05) { + pub_status(2); // 2: "Выключено" + } + } +} + +// Разбор пакета B5 (коды ошибок кофемашины) +// buffer[10], buffer[11] содержат коды ошибок +void SaecoSeries6000::parse_b5_packet(const uint8_t* buffer, uint16_t size) { + if (size <= 12) return; + if (buffer[10] == 0x00) { + if (buffer[11] == 0x00) pub_status(30); // 30: "Ошибка 0x00" + else if (buffer[11] == 0x0B) pub_status(31); // 31: "Ошибка 0x0B" + else if (buffer[11] == 0xE6) pub_status(32); // 32: "Ошибка 0xE6" + else if (buffer[11] == 0x80) pub_status(33); // 33: "Ошибка 0x80" + else if (buffer[11] == 0xCB) pub_status(34); // 34: "Ошибка 0xCB" + else if (buffer[11] == 0xFF) pub_status(35); // 35: "Ошибка 0xFF" + else if (buffer[11] == 0xA0) pub_status(36); // 36: "Ошибка 0xA0" + } else if (buffer[10] == 0x01) { + pub_status(37); // 37: "Статус2 0x01" + } +} + +void SaecoSeries6000::parse_mainboard_packet(const uint8_t* buffer, uint16_t size) { + last_activity_time = millis(); // сбрасываем таймер активности при любом пакете + if (debug_) { + // std::vector hex_buffer; + // hex_buffer.resize(size * 3 + 1); + // for (uint16_t i = 0; i < size; i++) { + // sprintf(hex_buffer.data() + i * 3, "%02X ", buffer[i]); + // } + //ESP_LOGD(TAG, "Packet from Mainboard: %s", hex_buffer.data()); + //ESP_LOGD(TAG, "parse_mainboard_packet: size=%u", size); } // Парсим B0 if (size > 4 && buffer[3] == 0xB0) { - // Логика из philips_series_5400.cpp - if (size < 12) return; - if (buffer[6] == 0x0E) { - if (buffer[9] == 0x00) { - pub_status(20); // "Опорож. контейнер для коф. гущи" - } else { - if ((buffer[9] & 0x40) != 0) { - pub_status(22); // "Воды нет" - } else { - pub_status(17); // "Вода есть" - } - if ((buffer[9] & 0x80) != 0) { - pub_status(23); // "Извлечен" - } else { - pub_status(18); // "Вставлен" - } - } - } else if (buffer[6] == 0x06) { - pub_status(21); // "Вода есть","Вставлен","Зерна есть","Выберите напиток","Пустой" - } else if (buffer[6] == 0x0C) { - if (buffer[7] == 0x01) { - pub_status(8); // "Наслаждайтесь" - } else if (buffer[7] == 0x02) { - pub_status(15); // "Что-то (07 0C 02)" - } - } else if (buffer[6] == 0x07) { - if (buffer[7] == 0x0E) { - pub_status(9); // "Нагрев воды" - } else if (buffer[7] == 0x0D) { - pub_status(10); // "Перемалываем зерна" - } else if (buffer[7] == 0x10) { - pub_status(3); // "Наливаем молоко" - } else if (buffer[7] == 0x11) { - pub_status(4); // "Наливаем кофе" - } else if (buffer[7] == 0x12) { - pub_status(5); // "Предварительное дозирование" - } else if (buffer[7] == 0x13) { - pub_status(6); // "Создание пара для молока" - } else if (buffer[7] == 0x14) { - pub_status(7); // "Заварочный узел в положение заваривания" - } else if (buffer[7] == 0x15) { - pub_status(15); // "Наслаждайтесь" - } - } else if (buffer[6] == 0x08) { - if (buffer[7] == 0x0E) { - pub_status(14); // "Нагревание" - } else if (buffer[7] == 0x02) { - pub_status(11); // "Промывка" - } else if (buffer[7] == 0x14) { - pub_status(13); // "Что-то (07 08 14)" - } else if (buffer[7] == 0x05) { - pub_status(12); // "Зерна закончились" - } else if (buffer[7] == 0x16) { - pub_status(24); // "Удаление накипи стадия 1" - } else if (buffer[7] == 0x18) { - pub_status(25); // "Удаление накипи стадия 2" - } - } else if (buffer[7] == 0x00) { - if (buffer[6] == 0x01) { - pub_status(1); // "Что-то (07 01 00)" - } else if (buffer[6] == 0x05) { - pub_status(2); // "Выключено" - } - } + parse_b0_packet(buffer, size); } else if (size > 12 && buffer[3] == 0xB5) { - // Парсим B5 (Error Code) - if (buffer[10] == 0x00) { - if (buffer[11] == 0x00) pub_status(30); - else if (buffer[11] == 0x0B) pub_status(31); - else if (buffer[11] == 0xE6) pub_status(32); - else if (buffer[11] == 0x80) pub_status(33); - else if (buffer[11] == 0xCB) pub_status(34); - else if (buffer[11] == 0xFF) pub_status(35); - else if (buffer[11] == 0xA0) pub_status(36); - } else if (buffer[10] == 0x01) { - pub_status(37); // "Статус2 0x01" - } + parse_b5_packet(buffer, size); } } @@ -181,5 +206,119 @@ void SaecoSeries6000::dump_config() { ESP_LOGCONFIG(TAG, " Debug mode: %s", ONOFF(this->debug_)); } +void SaecoSeries6000::send_packet_to_mainboard(const std::vector &data) { + ESP_LOGI(TAG, "Send to Mainboard pressed."); + if (uart_mainboard_ != nullptr) { + for (auto b : data) { + uart_mainboard_->write_byte(b); + } + } +} + +const uint8_t SaecoSeries6000::BitReverse[256] = { + 0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0, + 0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8, + 0x04,0x84,0x44,0xC4,0x24,0xA4,0x64,0xE4,0x14,0x94,0x54,0xD4,0x34,0xB4,0x74,0xF4, + 0x0C,0x8C,0x4C,0xCC,0x2C,0xAC,0x6C,0xEC,0x1C,0x9C,0x5C,0xDC,0x3C,0xBC,0x7C,0xFC, + 0x02,0x82,0x42,0xC2,0x22,0xA2,0x62,0xE2,0x12,0x92,0x52,0xD2,0x32,0xB2,0x72,0xF2, + 0x0A,0x8A,0x4A,0xCA,0x2A,0xAA,0x6A,0xEA,0x1A,0x9A,0x5A,0xDA,0x3A,0xBA,0x7A,0xFA, + 0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6, + 0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE, + 0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1, + 0x09,0x89,0x49,0xC9,0x29,0xA9,0x69,0xE9,0x19,0x99,0x59,0xD9,0x39,0xB9,0x79,0xF9, + 0x05,0x85,0x45,0xC5,0x25,0xA5,0x65,0xE5,0x15,0x95,0x55,0xD5,0x35,0xB5,0x75,0xF5, + 0x0D,0x8D,0x4D,0xCD,0x2D,0xAD,0x6D,0xED,0x1D,0x9D,0x5D,0xDD,0x3D,0xBD,0x7D,0xFD, + 0x03,0x83,0x43,0xC3,0x23,0xA3,0x63,0xE3,0x13,0x93,0x53,0xD3,0x33,0xB3,0x73,0xF3, + 0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB, + 0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7, + 0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF +}; + +uint32_t SaecoSeries6000::calc_crc(const std::vector& data, size_t start, size_t end) { + uint32_t crc = 0xFFFFFFFF; + const uint32_t POLY = 0x04C11DB7; + if (start >= end) return 0; + // start + uint8_t val = data[start]; + ((uint8_t*)(&crc))[3] ^= BitReverse[val]; + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x80000000) { + crc = (crc << 1) ^ POLY; + } else { + crc <<= 1; + } + } + // остальные байты + for (size_t i = start + 1; i < end; i++) { + uint8_t v = data[i]; + ((uint8_t*)(&crc))[3] ^= BitReverse[v]; + for (uint8_t j = 0; j < 8; j++) { + if (crc & 0x80000000) { + crc = (crc << 1) ^ POLY; + } else { + crc <<= 1; + } + } + } + uint32_t t32 = 0; + ((uint8_t*)(&t32))[0] = BitReverse[((uint8_t*)(&crc))[3]]; + ((uint8_t*)(&t32))[1] = BitReverse[((uint8_t*)(&crc))[2]]; + ((uint8_t*)(&t32))[2] = BitReverse[((uint8_t*)(&crc))[1]]; + ((uint8_t*)(&t32))[3] = BitReverse[((uint8_t*)(&crc))[0]]; + crc = t32 ^ 0xFFFFFFFF; + return crc; +} + +void SaecoSeries6000::send_packets_to_mainboard(const std::vector> &packets, uint32_t delay_ms) { + uint8_t counter = (last_display_counter_ + 1) & 0xFF; + for (size_t i = 0; i < packets.size(); ++i) { + std::vector pkt = packets[i]; + if (pkt.size() > 4) { + pkt[4] = counter; + } + // Логируем пакет ДО пересчёта CRC + { + std::string log_str; + char buf[8]; + for (auto b : pkt) { + snprintf(buf, sizeof(buf), "%02X ", b); + log_str += buf; + } + ESP_LOGI(TAG, "Packet before CRC: %s", log_str.c_str()); + } + // Пересчёт CRC: ищем преамбулу 0xAA 0xAA 0xAA, считаем CRC по данным между преамбулой и последними 5 байтами + if (pkt.size() > 8) { + size_t preamble = 0; + if (pkt[0] == 0xAA && pkt[1] == 0xAA && pkt[2] == 0xAA) { + preamble = 3; + } + size_t crc_start = preamble; + size_t crc_end = pkt.size() - 5; // не включаем 4 CRC и 0x55 + uint32_t crc = calc_crc(pkt, crc_start, crc_end); + // Заменяем последние 4 байта перед 0x55 на новую CRC + pkt[pkt.size()-5] = (crc >> 24) & 0xFF; + pkt[pkt.size()-4] = (crc >> 16) & 0xFF; + pkt[pkt.size()-3] = (crc >> 8) & 0xFF; + pkt[pkt.size()-2] = (crc) & 0xFF; + } + // Логируем пакет ПОСЛЕ пересчёта CRC + { + std::string log_str; + char buf[8]; + for (auto b : pkt) { + snprintf(buf, sizeof(buf), "%02X ", b); + log_str += buf; + } + ESP_LOGI(TAG, "Packet after CRC: %s", log_str.c_str()); + } + send_packet_to_mainboard(pkt); + counter = (counter + 1) & 0xFF; + delay(delay_ms); // пауза между пакетами + } + last_display_counter_ = (counter - 1) & 0xFF; +} + +// Реализация SendPacketsAction полностью в header (saeco_series_6000.h) + } // namespace saeco_series_6000 } // namespace esphome \ No newline at end of file diff --git a/components/saeco_series_6000/saeco_series_6000.h b/components/saeco_series_6000/saeco_series_6000.h index a1bd489..65f1908 100644 --- a/components/saeco_series_6000/saeco_series_6000.h +++ b/components/saeco_series_6000/saeco_series_6000.h @@ -3,6 +3,8 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" +#include "esphome/core/automation.h" +#include "esphome/components/button/button.h" namespace esphome { namespace saeco_series_6000 { @@ -19,6 +21,8 @@ class SaecoSeries6000 : public Component { void set_status_sensor(sensor::Sensor *sensor) { status_sensor_ = sensor; } void loop() override; void dump_config() override; + void send_packet_to_mainboard(const std::vector &data); + void send_packets_to_mainboard(const std::vector> &packets, uint32_t delay_ms = 10); protected: uart::UARTComponent *uart_display_; @@ -38,13 +42,36 @@ class SaecoSeries6000 : public Component { uint8_t display_sync_count_{0}; uint8_t mainboard_sync_count_{0}; + + uint8_t last_display_counter_{0}; + void update_display_counter_from_packet(const uint8_t* buffer, uint16_t size); void process_display_byte(uint8_t byte); void process_mainboard_byte(uint8_t byte); void parse_display_packet(const uint8_t* buffer, uint16_t size); void parse_mainboard_packet(const uint8_t* buffer, uint16_t size); + void parse_b0_packet(const uint8_t* buffer, uint16_t size); + void parse_b5_packet(const uint8_t* buffer, uint16_t size); void pub_status(uint8_t status); + static const uint8_t BitReverse[256]; + uint32_t calc_crc(const std::vector& data, size_t start, size_t end); +}; + +class SaecoSendPacketsButton : public esphome::button::Button, public esphome::Component { + public: + void set_parent(SaecoSeries6000 *parent) { parent_ = parent; } + void set_packets(const std::vector> &packets) { packets_ = packets; } + void set_delay_ms(uint32_t delay_ms) { delay_ms_ = delay_ms; } + void press_action() override { + if (parent_ != nullptr) { + parent_->send_packets_to_mainboard(packets_, delay_ms_); + } + } + private: + SaecoSeries6000 *parent_{nullptr}; + std::vector> packets_; + uint32_t delay_ms_{10}; }; } // namespace saeco_series_6000