#include /* * Описание мотора Tuya TU82TV * * Поддерживаемые интерфейсы: * - RS485 (протокол A3, DOOYA) * - Сухой контакт * - Радио 433 МГц * - Wi-Fi (SmartLife, Алиса, распознаётся как устройство с регулировкой яркости) * - Фазное управление (3-5 жил) * * Провода: * - Жёлто-зелёный: Земля * - Синий: Нейтраль * - Коричневый: Основная фаза (пульт/Wi-Fi/настройка) * - Чёрный и серый: Управляющие фазы (открыть/закрыть) * * Внимание! Не подключайте обе управляющие фазы одновременно. Не используйте для управления и питания разные фазы! * Внимание! Не используйте 5V контакт RS485, чтобы избежать короткого замыкания. Обязательно соедините общую землю между всеми устройствами. * * Настройка мотора: * - На задней стороне мотора есть чёрная кнопка настройки и светодиодный индикатор. * - Сброс Wi-Fi: Нажмите дважды коротко, затем удерживайте 5 секунд. * - Сброс калибровки и адреса: Нажмите кнопку до загорания индикатора, отпустите, затем снова нажмите и удерживайте 10 секунд, пока индикатор мигает с разными интервалами. Это сбрасывает адрес мотора (на 0xFEFE) и калибровку. * - Инициализация: * 1. Сбросьте калибровку (нажмите до загорания, затем удерживайте 10 секунд). * 2. Сбросьте регистрацию (2 коротких + 1 длинное нажатие). * 3. Откройте и закройте карниз до конца (чёрный и серый контакты). * 4. Переведите в режим умного мотора для звонковых выключателей (3 коротких + 1 длинное). * - Переключение режимов: * - Тупой мотор (выключатель с залипанием): 1 короткое + 1 длинное. * - Умный мотор (двухклавишный звонковый): 3 коротких + 1 длинное. * - Умный мотор (одноклавишный звонковый): 4 или 5 коротких + 1 длинное (влияет на состояние штор после восстановления питания: открыто/закрыто). * - Смена адреса по RS485: Нажмите кнопку, пока индикатор не замигает, затем отправьте команду setaddr в течение 6 секунд. */ /* * Описание подключения портов: * * Arduino Uno R3: * - Пин 2: Управление модулем MAX485 (контакты RE/DE). HIGH — режим передачи, LOW — режим приёма. * - Пин 3: RX для SoftwareSerial (приём данных от MAX485). * - Пин 4: TX для SoftwareSerial (передача данных на MAX485). * * MAX485: * - RO (Receiver Output): Подключается к пину 3 Arduino (RX для SoftwareSerial). * - DI (Driver Input): Подключается к пину 4 Arduino (TX для SoftwareSerial). * - RE/DE (Receiver/Driver Enable): Подключается к пину 2 Arduino. * - VCC: Подключается к 5V Arduino. * - GND: Подключается к GND Arduino (обязательно соединить с 6 контактом мотора). * - A: Подключается 8 контакту мотора (A-, отрицательный сигнал RS485). * - B: Подключается 7 контакту мотора (B+, положительный сигнал RS485). * * Мотор (RS485 для штор): * - Контакт 6 (GND): Земля, обязательно подключается к общей земле между всеми устройствами. * - Контакт 7 (B+): Положительный сигнал RS485, подключается к контакту B на MAX485. * - Контакт 8 (A-): Отрицательный сигнал RS485, подключается к контакту A на MAX485. * * Примечание: Для линий RS485 (A и B) используйте подтягивающие резисторы 120 Ом на концах шины, если длина линии превышает 10 метров. */ /* * Описание CRC16-MODBUS: * - Используется алгоритм CRC16-MODBUS с полиномом x^16 + x^15 + x^2 + 1 (Poly=0x8005, реверс=0xA001). * - Начальное значение: 0xFFFF. * - CRC рассчитывается от начального байта (0x55) до последнего байта данных (включая COMMAND и DATA). * - Результат: два байта — CRC16_L (младший), CRC16_H (старший). * - Функции: calculateCRC16 (генерация CRC), checkCRC16 (проверка CRC). * - Временные интервалы: * - Между байтами в пакете: менее 1.56 мс (при 9600 бод). * - Между пакетами: более 3.65 мс. */ /* * Описание команд и ответов (протокол схожий с A3, DOOYA): * * Общие параметры: * - Формат пакета: 0x55 | ID_L (1 байт) | ID_H (1 байт) | Function (1 байт) | Data (N байт) | CRC16_L | CRC16_H. * - Адрес устройства: ID_L, ID_H (по умолчанию 0xFEFE). Всем устройствам: ID_L=0x00, ID_H=0x00 (ответа нет). * - Временные интервалы: <1.56 мс между байтами, >3.65 мс между пакетами (при 9600 бод). * - Ответы: * - Эхо: Мотор возвращает точную копию команды для Function 0x03 и 0x02 при успехе. * - Для Function 0x01 (чтение): Формат ответа 55 ID_L ID_H 01 Length Data CRC_L CRC_H. * - Всем устройствам (ID=0x0000): Ответа нет. * - Ошибка хода: Возвращается 0xFF в поле данных (например, для процента без хода). * * Поддерживаемые команды: * 1. Открыть (Function 0x03): * - Отправлено: 55 FE FE 03 01 B9 24 * - Получено: 55 FE FE 03 01 B9 24 (эхо, успех) * 2. Закрыть (Function 0x03): * - Отправлено: 55 FE FE 03 02 F9 25 * - Получено: 55 FE FE 03 02 F9 25 (эхо, успех) * 3. Стоп (Function 0x03): * - Отправлено: 55 FE FE 03 03 38 E5 * - Получено: 55 FE FE 03 03 38 E5 (эхо, успех) * 4. Процентное управление (задаёт процент закрытия, 30%) (Function 0x03): * - Отправлено: 55 FE FE 03 04 1E 66 EA * - Получено: 55 FE FE 03 04 1E 66 EA (эхо, успех) или 55 FE FE 03 04 00 E6 E2 (нет хода, или позиция 0) * 5. Удалить крайние положения (Function 0x03): * - Отправлено: 55 FE FE 03 07 39 26 * - Получено: 55 FE FE 03 07 39 26 (эхо, успех) * 6. Сброс к заводским настройкам (Function 0x03): * - Отправлено: 55 FE FE 03 08 79 22 * - Получено: 55 FE FE 03 08 79 22 (эхо, успех) * - Примечание: Адрес сбрасывается на 0xFEFE, ход удаляется, пульт отвязывается, Wi-Fi сбрасывается. * 7. Чтение текущей позиции (%) (Function 0x01): * - Отправлено: 55 FE FE 01 02 01 85 42 * - Получено: 55 FE FE 01 01 47 04 40 (возвращает процент раскрытия, 70% - если закрывали на 30%) или 55 FE FE 01 01 00 44 72 (нет хода) * 8. Переключение и чтение направления (Function 0x01): * - Отправлено: 55 FE FE 01 03 01 84 D2 * - Получено: 55 FE FE 01 01 00 44 72 (прямое направление), 55 FE FE 01 01 01 85 B2 (обратное направление) * 9. Установить адрес (Function 0x02): * - Отправлено: 55 00 00 02 00 02 01 05 9C 9B * - Получено: 55 01 05 02 00 02 11 13 (успех) * - Примечание: Отправляется после нажатия кнопки на моторе (удерживать 5 с, LED мигает 2 раза), в течение 6 секунд. */ #define SerialTxControl 2 // Пин для RE/DE MAX485 #define RS485Transmit HIGH // Режим передачи #define RS485Receive LOW // Режим приёма // SoftwareSerial на пинах 3 (RX) и 4 (TX) SoftwareSerial RS485Serial(3, 4); // RX, TX // Поддерживаемые команды const uint8_t cmdOpen[] = {0x55, 0xFE, 0xFE, 0x03, 0x01}; // Открыть const uint8_t cmdClose[] = {0x55, 0xFE, 0xFE, 0x03, 0x02}; // Закрыть const uint8_t cmdStop[] = {0x55, 0xFE, 0xFE, 0x03, 0x03}; // Стоп const uint8_t cmdSetPosition[] = {0x55, 0xFE, 0xFE, 0x03, 0x04, 0x1E}; // Установить позицию (30%) const uint8_t cmdDeleteLimit[] = {0x55, 0xFE, 0xFE, 0x03, 0x07}; // Удалить ход const uint8_t cmdReset[] = {0x55, 0xFE, 0xFE, 0x03, 0x08}; // Сброс к заводским настройкам const uint8_t cmdReadPosition[] = {0x55, 0xFE, 0xFE, 0x01, 0x02, 0x01}; // Чтение текущей позиции const uint8_t cmdToggleDirection[] = {0x55, 0xFE, 0xFE, 0x01, 0x03, 0x01}; // Переключение и чтение направления const uint8_t cmdSetAddress[] = {0x55, 0x00, 0x00, 0x02, 0x00, 0x02, 0x01, 0x05}; // Назначить адрес ID_H=01, ID_L=05 const uint8_t cmdOpenNewAddress[] = {0x55, 0x01, 0x05, 0x03, 0x01}; // Открыть с новым адресом (ID_H=01, ID_L=05) // Структура для хранения команд и их описаний struct Command { const char* name; const uint8_t* cmd; size_t len; }; const Command commands[] = { {"open", cmdOpen, sizeof(cmdOpen)}, {"close", cmdClose, sizeof(cmdClose)}, {"stop", cmdStop, sizeof(cmdStop)}, {"pos30", cmdSetPosition, sizeof(cmdSetPosition)}, {"dellimit", cmdDeleteLimit, sizeof(cmdDeleteLimit)}, {"reset", cmdReset, sizeof(cmdReset)}, {"readpos", cmdReadPosition, sizeof(cmdReadPosition)}, {"toggledir", cmdToggleDirection, sizeof(cmdToggleDirection)}, {"setaddr", cmdSetAddress, sizeof(cmdSetAddress)}, {"opennew", cmdOpenNewAddress, sizeof(cmdOpenNewAddress)} }; const int commandCount = sizeof(commands) / sizeof(commands[0]); void setup() { Serial.begin(9600); // Serial для USB (консоль) на 9600 бод RS485Serial.begin(9600); // SoftwareSerial для MAX485 на 9600 бод pinMode(SerialTxControl, OUTPUT); digitalWrite(SerialTxControl, RS485Receive); // По умолчанию режим приёма // Вывод справки по командам Serial.println("Tuya TU82TV RS485 Control"); Serial.println("Available commands:"); for (int i = 0; i < commandCount; i++) { Serial.print(" "); Serial.print(commands[i].name); Serial.print(": "); printCommandDescription(commands[i].cmd, commands[i].len); } Serial.println("Enter command name to execute."); } void loop() { // Чтение данных от мотора static uint8_t buffer[20]; // Статический буфер static int i = 0; // Индекс буфера static unsigned long lastByteTime = 0; // Время последнего байта static bool receiving = false; // Флаг приёма пакета if (RS485Serial.available()) { if (!receiving) { receiving = true; // Начали приём пакета lastByteTime = millis(); } buffer[i++] = RS485Serial.read(); lastByteTime = millis(); // Обновляем время последнего байта } // Проверяем таймауты if (receiving && i > 0) { unsigned long currentTime = millis(); // Если прошло больше 3.65 мс без новых байт, считаем пакет завершённым if (currentTime - lastByteTime > 4) { Serial.print("Recv: "); printHex(buffer, i, true); if (i >= 7 && checkCRC16(buffer, i)) { // Проверяем CRC для пакетов 7+ байт processMotorResponse(buffer, i); } else { Serial.println(" CRC check failed or incomplete packet!"); } // Сбрасываем буфер и флаг i = 0; receiving = false; } } // Обработка команд из консоли if (Serial.available()) { String input = Serial.readStringUntil('\n'); input.trim(); bool commandFound = false; for (int i = 0; i < commandCount; i++) { if (input == commands[i].name) { sendCommand(commands[i].cmd, commands[i].len); commandFound = true; break; } } if (!commandFound) { Serial.println("Unknown command. Available commands:"); for (int i = 0; i < commandCount; i++) { Serial.print(" "); Serial.print(commands[i].name); Serial.print(": "); printCommandDescription(commands[i].cmd, commands[i].len); } } } } // Вспомогательная функция для вывода текстового описания команды void printCommandDescription(const uint8_t *cmd, size_t len) { // Определяем ID устройства uint8_t id_l = cmd[1]; uint8_t id_h = cmd[2]; if (id_l == 0x00 && id_h == 0x00) { Serial.print(" All devices"); } else { Serial.print(" ID: "); if (id_l < 0x10) Serial.print("0"); Serial.print(id_l, HEX); if (id_h < 0x10) Serial.print("0"); Serial.print(id_h, HEX); } Serial.print(" - "); // Определяем тип команды uint8_t function = cmd[3]; if (function == 0x03) { // Команды управления if (len >= 5) { uint8_t data = cmd[4]; switch (data) { case 0x01: Serial.print("Open"); break; case 0x02: Serial.print("Close"); break; case 0x03: Serial.print("Stop"); break; case 0x04: Serial.print("Set Position "); if (len >= 6) Serial.print(cmd[5], DEC); Serial.print("%"); break; case 0x07: Serial.print("Delete Limit"); break; case 0x08: Serial.print("Reset to Factory"); break; default: Serial.print("Unknown Command"); break; } } } else if (function == 0x01) { // Команды чтения if (len >= 5) { uint8_t data = cmd[4]; switch (data) { case 0x02: Serial.print("Read Current Position"); break; case 0x03: Serial.print("Toggle Direction"); break; default: Serial.print("Unknown Read Command"); break; } } } else if (function == 0x02) { // Команды записи if (len >= 6) { uint8_t data = cmd[4]; switch (data) { case 0x00: Serial.print("Set Address"); break; default: Serial.print("Unknown Write Command"); break; } } } else { Serial.print("Unknown Function"); } Serial.println(); } // Функция отправки команды void sendCommand(const uint8_t *cmd, size_t len) { digitalWrite(SerialTxControl, RS485Transmit); // Режим передачи // Вычисляем CRC16 uint16_t crc = calculateCRC16(cmd, len); uint8_t crcLow = crc & 0xFF; uint8_t crcHigh = (crc >> 8) & 0xFF; // Отправляем команду for (size_t i = 0; i < len; i++) { RS485Serial.write(cmd[i]); } RS485Serial.write(crcLow); RS485Serial.write(crcHigh); RS485Serial.flush(); // Ждём завершения передачи delay(2); // Задержка для >3.65 мс между пакетами digitalWrite(SerialTxControl, RS485Receive); // Режим приёма // Вывод команды в консоль Serial.print("Send: "); printHex(cmd, len, false); Serial.print(crcLow, HEX); Serial.print(" "); Serial.println(crcHigh, HEX); printCommandDescription(cmd, len); // Вывод текстового описания } uint16_t calculateCRC16(const uint8_t *data, size_t len) { uint16_t crc = 0xFFFF; // Начальное значение для CRC16-MODBUS for (size_t pos = 0; pos < len; pos++) { crc ^= (uint16_t)data[pos]; // XOR с текущим байтом for (uint8_t i = 8; i != 0; i--) { if ((crc & 0x0001) != 0) { crc >>= 1; crc ^= 0xA001; // Полином для MODBUS } else { crc >>= 1; } } } return crc; } // Функция проверки CRC16 bool checkCRC16(const uint8_t *data, size_t len) { if (len < 2) return false; // Минимум 2 байта для CRC uint16_t receivedCRC = ((uint16_t)data[len - 1] << 8) | data[len - 2]; // CRC из пакета (H|L) uint16_t calculatedCRC = calculateCRC16(data, len - 2); // Считаем CRC без последних двух байтов return receivedCRC == calculatedCRC; } // Функция обработки ответа от мотора void processMotorResponse(uint8_t *data, size_t len) { if (len < 7 || data[0] != 0x55) { Serial.println(" Invalid format"); return; } // Определяем ID устройства uint8_t id_l = data[1]; uint8_t id_h = data[2]; Serial.print(" "); if (id_l == 0x00 && id_h == 0x00) { Serial.print("All devices"); } else { Serial.print("ID: "); if (id_l < 0x10) Serial.print("0"); Serial.print(id_l, HEX); if (id_h < 0x10) Serial.print("0"); Serial.print(id_h, HEX); } Serial.print(" - "); // Определяем тип ответа uint8_t function = data[3]; if (function == 0x03) { // Команды управления if (len >= 5) { uint8_t data_field = data[4]; if (len >= 6 && data_field == 0x04 && data[5] == 0xFF) { Serial.println("Position Error: No limit set"); } else { switch (data_field) { case 0x01: Serial.println("Open"); break; case 0x02: Serial.println("Close"); break; case 0x03: Serial.println("Stop"); break; case 0x04: Serial.print("Set Position "); if (len >= 6) Serial.print(data[5], DEC); Serial.println("%"); break; case 0x07: Serial.println("Delete Limit"); break; case 0x08: Serial.println("Reset to Factory"); break; default: Serial.println("Unknown Command"); break; } } } } else if (function == 0x01) { // Команды чтения if (len >= 6 && data[4] == 0x01) { uint8_t data_field = data[5]; Serial.print("Value: "); Serial.println(data_field, DEC); } else { Serial.println("Invalid Read Response"); } } else if (function == 0x02) { // Команды записи if (len >= 6) { uint8_t data_field = data[4]; switch (data_field) { case 0x00: Serial.println("Set Address"); break; default: Serial.println("Unknown Write Response"); break; } } } else { Serial.println("Unknown Response"); } } // Функция вывода байтов в hex-формате void printHex(const uint8_t *data, size_t len, bool eol) { for (size_t i = 0; i < len; i++) { if (data[i] < 0x10) Serial.print("0"); Serial.print(data[i], HEX); Serial.print(" "); } if (eol) { Serial.println(); } }