Додаємо «розуму» smart home реле від Tuya
Всім привіт,
Є таке розумне реле Tuya TS0012, яке майже єдине, що можна знайти, якщо шукати двоканальне Zigbee-реле без нейтралі. Воно продається під різними брендами, але всередині майже однакове. Це реле легко інтегрується у Home Assistant, однак має неприємні проблеми:
- При натисканні двох кнопок одночасно вмикає тільки одну лампочку. Щоб працювало нормально, треба робити паузу між натисканнями в 0.5 секунди.
- Досить повільно реагує на команди з Home Assistant із затримкою близько пів секунди.
- Є EndDevice-ом, отже, не може покращувати Zigbee-мережу, хоча й має постійний доступ до електроенергії.
Якщо з останніми двома проблемами ще можна миритися, то перша відразу почала сильно бісити всіх домашніх, бо воно постійно не спрацьовує, і потрібно стояти, натискати туди-сюди, очікуючи, поки нарешті правильно попадеш у «таймінг». З цього виникла ідея спробувати допрацювати цей девайс.
Всередині девайсу
Всередині знайшовся модуль Tuya ZT3L, що відповідає за «розум» девайсу та приєднаний до основної плати реле, яка насправді працює на сімісторах. Хоча й документація від Tuya це старанно ховає, в інтернеті можна знайти, що ZT3L використовує Telink TLSR8258 — досить популярний контролер для Zigbee та BLE-девайсів. На ньому, наприклад, працюють дешеві термометри від Xiaomi, які навчилися перероблювати під Zigbee. Саме для модулів реле теж вже була відкрита прошивка, однак вона майже повністю скопійована з прикладу з документації Telink і не підтримує декілька каналів. Отже, сказавши «чим я гірший?», було вирішено спробувати написати код самому.
Першою перепоною стало те, що Telink для прошивки використовує не звичайний UART, як це наприклад зроблено в ESP32, а пропрієтарний SWS-протокол, що використовує лише один дріт. Однак, на щастя, цей протокол вже дослідили, і існують UART-конвертори, які за допомогою резистора і хитрого поєднання дротів емулюють цей протокол. Однак таких емуляторів багато, і мені вдалося прочитати flash лише через один, а щось записати — тільки через інший. Забігаючи наперед, для подальшої розробки був придбаний окремий модуль, що значно полегшило весь процес.
Отримавши можливість прошивати девайс, можна було починати розробку. Однак документація Telink пропонує використовувати власну IDE, що насправді є Eclipse, з якою розбиратися зовсім не хотілося. Тому збірка прикладу з документації була перенесена на набір Makefile-ів, що дозволило нарешті скомпілювати першу версію прошивки і запустити її на девайсі. Наступним кроком потрібно було повернутися до hardware-частини і отримати розпіновку для TS0012, тобто зрозуміти, які GPIO за що відповідають.
Оскільки це двоканальне реле, то потрібно було знайти 6 пінів: 2 клавіші, 2 реле, 1 кнопку на самому модулі та світлодіод статусу. Для зручності сам модуль ZT3L був випаяний, і виявилося, що до нього, як «бутерброд», приєднана додаткова плата з резисторами і транзисторами. Це трохи спростило роботу, бо на платі з сімісторами, яка більш складна, потрібно було продзвонювати менше.
Отримавши розпіновку, була створена перша версія прошивки, яка мала б робити щось корисне. Однак постало питання того, як це все тестувати. Бо підключати все це до 230В було не дуже безпечно і, до того ж, незручно. Тому до модуля були підпаяні дроти, а далі зібраний на бредборді простенький макет, де реле замінені діодами. Цей імпровізований «девборд» виявився досить зручним і досі успішно працює.
Написання коду
Нарешті можна було повноцінно почати писати код. Документація Telink досить вдала, тому можна відразу сказати, що софтварна частина проєкту пройшла досить легко, якщо не враховувати що розробник (тобто я) звик до бекендного Python-коду і двічі рефакторив увесь проєкт, бо дуже хотілося організувати код більш зручно і звично. І хоча результат точно займає більше SRAM ніж цього можна досягти, 64KB пам’яті має вистачити всім.
Спершу було реалізовано логування по UART. Для цього в SDK достатньо визначити декілька макросів. Разом вони включають програмну емуляцію UART TX, чого достатньо щоб мати можливість читати логи.
#define UART_PRINTF_MODE 1 #define BAUDRATE 115200 #define DEBUG_INFO_TX_PIN GPIO_PB1
Наступним кроком було реалізовано зчитування натискань на кнопки та перемикання реле. Telink SDK підтримує матрицю кнопок і scan-коди, і ця реалізація використовувалась у першій версії прошивки. Однак кількість кнопок та піни потрібно налаштовувати через макроси в compile-time, що стало заважати бажанню мати можливість змінювати розпіновку без перекомпіляції. Тому для кнопок був написаний власний велосипед, який, однак, цілком справляється із задачею і підтримує debounce, довгі та повторювані натискання.
Отримавши прошивку що реагує на кнопки блиманням світлодіодів, було розпочато розробку Zigbee-частини. Zigbee побудований навколо кластерів, які можна назвати окремими сервісами, що відповідають за конкретний функціонал. Кожен тип кластера має свій унікальний ID, визначений у стандарті, та набір атрибутів, з яких він складається. Наприклад, кластер OnOff відповідає за статус реле та має атрибут, що визначає стан реле, та атрибут, що визначає поведінку після перезапуску пристрою. Усі кластери, які підтримує пристрій, мають бути розбиті на ендпоінти, які нагадують порти у TCP і дозволяють мати декілька кластерів одного типу. Більш детально почитати про те, як влаштований Zigbee, можна в серії статей Олександра Маслюченка «Hello Zigbee World», яка дуже допомогла при розробці.
У Telink SDK список кластерів визначається за допомогою спеціальних таблиць:
const zcl_specClusterInfo_t g_sampleSwitchClusterList[] = { {ZCL_CLUSTER_GEN_BASIC, MANUFACTURER_CODE_NONE, ZCL_BASIC_ATTR_NUM, basic_attrTbl, zcl_basic_register, sampleSwitch_basicCb}, {ZCL_CLUSTER_GEN_IDENTIFY, MANUFACTURER_CODE_NONE, ZCL_IDENTIFY_ATTR_NUM, identify_attrTbl, zcl_identify_register, sampleSwitch_identifyCb}, ... }
Де вказуються поінтери на іншу таблицю, що визначає атрибути всередині самого кластера:
const zclAttrInfo_t basic_attrTbl[] = { { ZCL_ATTRID_BASIC_ZCL_VER, ZCL_DATA_TYPE_UINT8, ACCESS_CONTROL_READ, (u8*)&g_zcl_basicAttrs.zclVersion}, { ZCL_ATTRID_BASIC_APP_VER, ZCL_DATA_TYPE_UINT8, ACCESS_CONTROL_READ, (u8*)&g_zcl_basicAttrs.appVersion}, { ZCL_ATTRID_BASIC_STACK_VER, ZCL_DATA_TYPE_UINT8, ACCESS_CONTROL_READ, (u8*)&g_zcl_basicAttrs.stackVersion}, { ZCL_ATTRID_BASIC_HW_VER, ZCL_DATA_TYPE_UINT8, ACCESS_CONTROL_READ, (u8*)&g_zcl_basicAttrs.hwVersion}, { ZCL_ATTRID_BASIC_MFR_NAME, ZCL_DATA_TYPE_CHAR_STR, ACCESS_CONTROL_READ, (u8*)g_zcl_basicAttrs.manuName}, { ZCL_ATTRID_BASIC_MODEL_ID, ZCL_DATA_TYPE_CHAR_STR, ACCESS_CONTROL_READ, (u8*)g_zcl_basicAttrs.modelId}, { ZCL_ATTRID_BASIC_POWER_SOURCE, ZCL_DATA_TYPE_ENUM8, ACCESS_CONTROL_READ, (u8*)&g_zcl_basicAttrs.powerSource}, { ZCL_ATTRID_BASIC_DEV_ENABLED, ZCL_DATA_TYPE_BOOLEAN, ACCESS_CONTROL_READ | ACCESS_CONTROL_WRITE, (u8*)&g_zcl_basicAttrs.deviceEnable}, { ZCL_ATTRID_GLOBAL_CLUSTER_REVISION, ZCL_DATA_TYPE_UINT16, ACCESS_CONTROL_READ, (u8*)&zcl_attr_global_clusterRevision}, };
Як видно, в таблиці атрибутів надаються поінтери на змінні в яких знаходиться значення атрибутів. Якщо приходить команда на зміну атрибуту, SDK само оновлює значення по цьому поінтеру. Також можна додати колбеки, що будуть викликатись при записі атрибутів.
Хоча такий підхід дозволяє швидко описати кластери, він перетворює код на велетенський опис, у якому важко розібратись. І, що більш критично, він змушує копіювати код, якщо потрібно декілька однотипних кластерів, як от кластери для двох реле TS0012. Тому генерацію цих таблиць структур було перенесено у runtime. Для цього було створено «клас» під кожен тип кластера і додано функцію, що додає його в ендпоінт:
typedef struct { u8 deviceEnable; char manuName[32]; char modelId[32]; zclAttrInfo_t attr_infos[12]; } zigbee_basic_cluster; ... void basic_cluster_add_to_endpoint(zigbee_basic_cluster *cluster, zigbee_endpoint *endpoint) { populate_sw_build(); populate_date_code(); // Fill Attrs SETUP_ATTR(0, ZCL_ATTRID_BASIC_ZCL_VER, ZCL_DATA_TYPE_UINT8, ACCESS_CONTROL_READ, zclVersion); SETUP_ATTR(1, ZCL_ATTRID_BASIC_APP_VER, ZCL_DATA_TYPE_UINT8, ACCESS_CONTROL_READ, appVersion); SETUP_ATTR(2, ZCL_ATTRID_BASIC_STACK_VER, ZCL_DATA_TYPE_UINT8, ACCESS_CONTROL_READ, stackVersion); SETUP_ATTR(3, ZCL_ATTRID_BASIC_HW_VER, ZCL_DATA_TYPE_UINT8, ACCESS_CONTROL_READ, hwVersion); SETUP_ATTR(4, ZCL_ATTRID_BASIC_MFR_NAME, ZCL_DATA_TYPE_CHAR_STR, ACCESS_CONTROL_READ, cluster->manuName); SETUP_ATTR(5, ZCL_ATTRID_BASIC_MODEL_ID, ZCL_DATA_TYPE_CHAR_STR, ACCESS_CONTROL_READ, cluster->modelId); SETUP_ATTR(6, ZCL_ATTRID_BASIC_POWER_SOURCE, ZCL_DATA_TYPE_ENUM8, ACCESS_CONTROL_READ, powerSource); SETUP_ATTR(7, ZCL_ATTRID_BASIC_DEV_ENABLED, ZCL_DATA_TYPE_BOOLEAN, ACCESS_CONTROL_READ | ACCESS_CONTROL_WRITE, cluster->deviceEnable); SETUP_ATTR(8, ZCL_ATTRID_BASIC_SW_BUILD_ID, ZCL_DATA_TYPE_CHAR_STR, ACCESS_CONTROL_READ, swBuildId); SETUP_ATTR(9, ZCL_ATTRID_BASIC_DATE_CODE, ZCL_DATA_TYPE_CHAR_STR, ACCESS_CONTROL_READ, dateCode); SETUP_ATTR(10, ZCL_ATTRID_GLOBAL_CLUSTER_REVISION, ZCL_DATA_TYPE_UINT16, ACCESS_CONTROL_READ, zcl_attr_global_clusterRevision); SETUP_ATTR(11, ZCL_ATTRID_BASIC_DEVICE_CONFIG, ZCL_DATA_TYPE_LONG_CHAR_STR, ACCESS_CONTROL_READ | ACCESS_CONTROL_WRITE, config); zigbee_endpoint_add_cluster(endpoint, 1, ZCL_CLUSTER_GEN_BASIC); zcl_specClusterInfo_t *info = zigbee_endpoint_reserve_info(endpoint); info->clusterId = ZCL_CLUSTER_GEN_BASIC; info->manuCode = MANUFACTURER_CODE_NONE; info->attrNum = 12; info->attrTbl = cluster->attr_infos; info->clusterRegisterFunc = zcl_basic_register; info->clusterAppCb = basic_cluster_callback_trampoline; }
Такий підхід дозволив локалізувати код, що визначає кластер та його атрибути, з кодом, що обробляє події, пов’язані з цим кластером, тим самим трохи зменшивши «спагетизованість» коду. Також це дозволило зробити генерацію набору кластерів залежно від конфігурації без перекомпіляції.
Хоча стандарт Zigbee визначає багато різних кластерів і їх атрібутів, він все ж досить обмежений і наприклад не дозволяє визначати тип клавіші (перемикач чи моментальна) або ж довжину довгого натискання. Для підтрики таких можливостей потрібно додавати власні атрибути. Їх визначення у Telink SDK майже не відрізняється, потрібно лише додати власний айді атрибуту. Однак, більше складностей виникають на іншій стороні, а саме інтерфейсі Home Assistant, який про них нічого не знає.
Так як домашня мережа використовує Zigbee2MQTT, то потрібно написати окремий конвертер, що додає підтримку власних атрибутів. На жаль, нормальна докуметація тут відстутня і доводиться робити підглядаючи на інші конвертори. Розібравшись, це зробити досить легко
const switchMode = (name, endpointName) => enumLookup({ name, endpointName, lookup: { toggle: 0, momentary: 1, multifunction: 2 }, cluster: "genOnOffSwitchCfg", attribute: { ID: 0xff00, type: 0x30 }, // Enum8 description: "Select the type of switch connected to the device", }), ... extend: [ ... switchMode("switch_1_mode", "1"), ],
Після підключення конвертеру девайс на дев борді нарешті повноцінно запрацював. Залишалось лише додати OTA-оновлення з рідної прошивки, однак у прошивці для термометрів Xiaomi це вже зроблено, тому це можна було просто перенести.
Результати
Далі був найприємніший момент — прошивка була протестована і без проблем зашита у девайси в квартирі. Нарешті можна було перемикати клавіші, не думаючи про те, щоб не робити це занадто швидко. Після позитивного результату тестувань, прошивка була залита на GitHub, і проєкт почав розвиватися: були додані додаткові фічі, підтримка інших девайсів, процес оновлення став більш зрічним.
Отже, після додавання розуму, девайс навчився:
- нормально реагувати на одночасні натискання;
- затримка реакції, як на фізичне, так і на програмне перемикання, впала майже до нуля;
- з’явилася підтримка відключення клавіші від реле (detached mode).
- з’явилася підтримка довго натискання
Дякую, що прочитали!
Немає коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів