В предыдущих сериях я:
- Накупил устройств от Xiaomi для умного дома и посредством паяльника заставил их работать в увлекательной манере без родных серверов через home assistant (ссылка на пост)
- Завернул web interface от home assistant в electron (ссылка на пост) с поддержкой нотификаций, менюшек, точбара итд (код тут)
- Разобрал протокол miio со стороны рассылки сообщений (ссылка на пост) и реализовал поддержку всяческих кнопок в xiaomi_miio.
Со временем накопилось понимание как устроены разные инкарнации умных домов, с точки зрения реализации сценариев и протоколов взаимодействия. С этим знанием я наделал устройств и реализовал для них "правильную" распределенную среду программирования для IoT с lisp-ом, криптографией и сборкой мусора. Под катом поведаю о ходе и результате процесса.
Особенности сред
Xiaomi:
- Простой udp прокол взаимодействия с легким налётом криптографии
- Устройства активно общаются между собой, не требуя центра
- Сценарии выполняются на самих устройствах, а не на центральной машине
Home Assistant:
- Считает все устройства тупыми исполнителями, неспособными ни на что без него
- Собирает все данные и раздаёт все команды
- Сценарии не подразумевает прямого общения между устройствами, минуя Home Assistant
- Single Point of Failure
Главная проблема в централизованном подходе home assistant имущественный ценз. При покупке умной розетки ~1000руб, для её работы с Home Assistant придётся докупить Raspberry PI ~4000руб, ведь Home Assistant надо на чем-то запускать. Такие натяжки бюджета резко охлаждают пыл желающих отведать умных домов и тормозит всю индустрию.
Через несколько абзацев выяснится, что устройства умного дома почти всегда оснащены совсем не детским CPU, силы которых хочется использовать, а не загонять в рамки "Я начальник, ты дурак" им. Home Assistant.
В то же время xiaomi реализовали правильный децентрализованный протокол, но зачем-то сузили возможности устройств ограничив их стрёмным, но развесистым json-based dsl (личное впечатление составлять тут).
В голове созревало видение "правильной" среды для управления умным домом, но без реального приложения реализовывать протокол задумка пустая. Я изучал соседние протоколы zigbee/ble и контроллеры esp32/nrf51 и собирался с мыслями, разбирался и собирался снова
Внезапно во мне самозародилась потребность измерять качество
воздуха в окружающей среде.
У Xiaomi не нашлось измерителя CO2, ломать еще одного китайца,
чтобы он заработал с home assistant не хотелось, а с прошлой серии
во мне дребезжал гештальт, что неплохо бы научиться паять.
Следуя зову паяльника, я решил вручную, то есть самостоятельно, породить устройств для умного дома, а заодно ознакомиться с микроконтроллерами, arduino-ide (мимо меня прошло как-то), понять достаточно ли моих знаний физики, чтобы использовать мультиметр во благо, но без инструкции, а также потешить самолюбие реализацией "правильного" умного дома.
Под этим предлогом была зачата секретная лаборатория
Что хотелось создать в качестве Proof of concept:
- Устройство измеряющее CO2 сенсор 1шт
- Устройство получающее и демонстрирующее данные с сенсора дисплей 1шт
- Создать прошивки для этих устройств 2шт
- Создать интеграцию устройств в home assistant 1шт
- Создать cli инструменты для работы с устройствами 1шт
- Устройства должны иметь возможность работать как с центральным сервером, так и позволять прямое взаимодействие друг с другом
Контроллер
Логи home-assistant при подключении к устройствам Xiaomi сдали их компонентную базу строчкой лога "chuangmi.plug.v3 1.3.0_92 ESP8266 detected". Разведка сообщила, что ESP8266 это дешевый (~100руб/шт) китайский микроконтроллер, нежно любимый и горячо поддерживаемый всеми и каждым. Этот чип умеет общаться без проводов по Wi-Fi, а с проводами по I2C/SPI/UART/GPIO. Также ESP8266 оснащен довольно мощным вычислительным ядром Xtensa 80/160Mhz 32-битный 112Кбайт памяти разработки калифорнийской компании Tensilica (там какая-то мутная история, tensilica была распущена в 2013, а esp8266 был замечен общественностью в августе 2014), а еще там есть 1Мб флеш памяти, которого по легенде должно хватать на всё.
Вот и прекрасно фокусируюсь на них. ESP8266 продается в модулях с разными формфакторами. Для простоты
ручной сборки взял модули esp201 они удобнее других тем, что уже распаяны
на гребенку 2.54mm. В ожидании доставки вникаю в примеры кода
arduino и даташиты
В процессе прокачиваются навыки работы с паяльником и
мультиметром
Всплывает минус ESP8266. Xtensa и Wi-Fi очень прожорливы настолько,
что устройства делающие что-либо отличное от сна будут выжирать
батарейку за недели в лучшем случае. Для батареечных устройств надо
брать например nrf51 (~150руб/шт). Обидно досадно, но ладно. Будем
втыкать устройства в розетку.
Датчик CO2
На просторах aliexpress нашлось несколько типов сенсоров CO2 (подробнее тут). Мой выбор псевдослучайным образом пал на CCS811 (дрова тут). Они оказались довольно дорогими (~500руб/шт) и прожорливыми, так как для замеров CO2 и TVOC сенсор содержит нагревательный элемент, подогревающий воздух перед анализом, греется он не мгновенно и ощутимо жрет энергию до 60мА, что практически исключает применение CCS811 в батареечных устройствах.
Экран
Для экрана подойдут LED модули 8x8 пикселей на max7219 (~100руб/шт). Тоже нуждаются в обильном питании...
Питание
Чтобы эти модули были показательно независимы от всего кроме розетки купил блоков питания hlk-pm03 (~200руб/шт). Это 4х-ногое устройство; две передние ноги которого суются в розетку 220v AC, а две задние подают 3.3v DC в цепь питания esp8266.
Сверху присыпать двумя щепотками резисторов, конденсаторов и кнопок. Канифоль и припой добавляем по вкусу. Припугивать паяльником до готовности (если интересна схемо-распаячная часть с мультиметром и бредбордами реквестуйте в каментах опишу).
Устройства
В результате вышеупомянутых изысканий народилось два устройства:
- SensorPack CCS811 примотанный к ESP201 по I2C (туда же примотаны по инерции еще несколько датчиков, но в пользу краткости описания их опустим)
- Pixel Красноглазая LED матрица 8x8, обвязанная max7219 и доставленная на ESP201 по протоколу SPI
Так выглядит SensorPack в интерьере
А так Pixel на фоне бинокля:
В процессе сборки устройств были накостылены прошивки которые:
- Заливаются через USB/UART в устройство
- Печатают в UART "Hello world!"
- Подключаются к Wi-Fi
- Запускают CCS811 и печатают в UART показания с него
- Включают/выключают пиксели на led матрице
Это простые прошивки в 20-50 строк каждая. Их успешное выполнение доказывает работоспособность сборки и вменяемость драйверов.
Осталось создать прошивку, которая предоставит пользователю:
- Простой протокол обмена сообщениями с/между устройствами
- Хоть немножко шифрования при обмене данных с/между устройствами
- Удобный DSL для взаимодействия с/между устройствами
Протокол обмена
Основанный на udp протокол, используемый Xiaomi в их wi-fi
устройствах, мне понравился своей простотой. Там сообщение это один
udp пакет, в ответ на такой пакет принимающая сторона должна кинуть
на ip/порт источника пакета ответ тоже udp пакет. Если ответа нет
перепосылка. Очень просто, понятно берем как есть Исходников от
xiaomi нет, но есть клиенты опенсорс реализаций (например такая), воспроизводим по наитию,
выкидывая непонятное и/или ненужное под лозунгом "Faster!
Harder! Scooter!" "Меньше лучше!".
В процессе для отладки зародились клиент и сервер для
реализуемого протокола на python.
Сначала клиенты на python начинают общаться по реализуемому
протоколу между собой, затем с устройством.
Шифрование
У Xiaomi-miio из открытых алгоритмов строится очень причудливая симметричная схема шифрования сообщений (детали тут).
Выкидываем её за борт, берем aes128-cbc и реализуем следующий формат сообщений:
- Первые 128бит пакета Initialization Vector (IV) для расшифровки сообщения с aes128
- Остальные данные пакета выровненное по длине блока сообщение, зашифрованное общим ключем с помощью aes128
Аналогичная логика работы с ключом, IV и пакетами реализуется в
python и на устройстве через arduino api.
Вот тут для наглядности можно глянуть python
реализацию
Текущая реализация использует IV, генерируемый случайным образом, поэтому подвержена replay attack. Чуть утешает, что xiaomi тоже подвержена replay attack, чем я активно пользовался :) Зачинится, если часть IV сделать монотонно возрастающим и хранить на устройствах последний IV для каждого устройства, с которым велось взаимодействие. Надеюсь такие финты ушами не нарушат криптографических заповедей...
Как разговаривать будем?
К этому моменту у меня есть устройства, умеющие общаться со мной, посредством python-скриптов, и между собой, используя реализацию протокола в прошивке. Но сообщения эти никак не влияют на поведение устройств, бесцельно летают по воздуху туда-сюда. Что я хочу гонять в этих сообщениях? В устройствах довольно много FLOPSов и есть памяти чуть, а значит их можно использоваться как программируемые устройства, чтобы такие вещи как перевод из Цельсия в Фаренгейты выполнялись прямо на сенсоре, дальше больше. Так может они и скрип выполнить могут, чтобы на каждый чих прошивку не обновлять?! Форматировать строки, анимировать картинку на led матрице, вычислять медиану для n последних замеров это всё можно и должно делаться на устройстве.
Первой мысль было взять JS его все знают. Но ESP8266 отказался выполнять JS, ибо вменяемых реализаций в дикой природе обнаружено не было. Зато нашелся Lua, да еще с готовой прошивкой NodeMCU для ESP8266. Подходит, заодно и lua попрактикую.
Реализовал протокол и шифрование уже на lua и всё было хорошо пока
Пока не оказалось, что:
- В 1мб доступный на esp201 много кода lua не загнать
- Пропагандируемый в NodeMCU подход реализовывать драйвера
устройств прямо на lua приводил к жонглированию прошивкой, дабы она
не вылетала из за нехватки памяти. Штирлица учили, что системы
показавшие свою хрупкость при создании прототипа, должны в
прототипе и остаться
lua был отправлен на свалку, и начались поиски более правильного инструмента.
uLisp
"Что проще всего реализовать? подумал я, и в голову пришел lisp. Уже засучив рукава и нависнув над клавиатурой, решил поискать, а нет ли уже ма-а-а-а-аленького lisp для esp8266, и он дан был мне в виде uLisp. Взял его и c помощью его автора многоуважаемого @technoblogy запилил прошивку которая:
- Принимает udp пакет
- Расшифровывает его
- Декодирует сообщение в строку
- Интерпретирует строку через ulisp
- Кодирует в строку ответа s-expression результата работы интерпретатора
- Шифрует ответ
- Отправляет ответ в ip/порт источника входного пакета
- Добавляет в api ulisp функции для работы с CCS811 и Max7219
В отличии от Lua прошивки, с ulisp я брал уже драйвера от arduino и заворачивал их в ulisp функции. Сам ulisp используется исключительно как клей, объединяющий высокоуровневые функции, реализованные на C++. Такой подход позволяет экономить память, сделать lisp программы более читабельными и использовать огромную базу драйверов от arduino.
В моей сети устройства адресуются так:
192.168.2.99 IP адрес SensorBoard
192.168.2.174 IP адрес Pixel
Как это работает в лабораторных условиях:
python tools/client.py --ip 192.168.2.99 --message '(+ 1 2)' --key YOUR_AES128_HEX_KEY>> (+ 1 2)<< 3
Для получения доступных возможностей устройства можно вызвать метод discovery. Он возвращает список специфичных для устройства функций, доступных на устройстве в дополнение к стандартным функциям ulisp.
python tools/client.py --ip 192.168.2.99 --message '(discovery)' --key YOUR_AES128_HEX_KEY>> (discovery)<< ("light-read" "co2-read" "tvoc-read" "humidity-read" "temperature-read")
Через эти функции читаем показания CCS811
python tools/client.py --ip 192.168.2.99 --message '(list (co2-read) (tvoc-read))' --key YOUR_AES128_HEX_KEY>> (list (co2-read) (tvoc-read))<< (840 67)
Вот такой сетевой REPL, но это слишком просто Еще у нас есть методы для прямого взаимодействия
python tools/client.py --ip 192.168.2.174 --message '(discovery)' --key YOUR_AES128_HEX_KEY>> (discovery)<< ("show" "request")
show выводит на экран led матрицы бегущую строку
request это метод, отправляющий сообщение другому устройству,
используя его можно сделать инициатором сообщений, например вот
так
# форматируем сообщение и отправляем его бегущей строкой на led матрицу на 30 секунд (defun show_sensor (x) (show (format nil "CO2=~a TVOC=~a" (first x) (second x)) 30000))# раз в 33 секунды, запрашивает данные с сенсора и передает их в show_sensor(periodic 33000 (quote (request "192.168.2.99" 54321 (quote (list (co2-read) (tvoc-read))) 'show_sensor)))
Загружаем эту программу в Pixel
$ python tools/client.py --ip 192.168.2.174 --mfile ulisp_scripts/red.ulisp --key YOUR_AES128_HEX_KEY
Наблюдаем как Pixel отрисовывает полученные с SensorPack показатели датчиков
Такая вот lisp среда позволяет работать с устройствами умного дома и реализовывать сценарии без центральной машины. И по заветам Raspberry PI даёт пользователю возможность обучиться создавать доселе невиданное. По идее можно написать транслятор, берущий на вход автоматизации из-под Home Assistant и выплёвывающий lisp код. Это позволит избавиться от SPoF, но потребует довольно существенного вмешательства в устройство Home Assistant.
Интегрировать в Home Assistant такие устройства тоже несложно,
пример тут
PoC кода прошивки, скриптов, утилит и прочих артефактов
производства тут
Еще фото SensorPack с хвостом USB и антенной Wi-Fi
По ходу жизнедеятельности мне приходилось создавать базы данных, трансляторы, среды исполнения, dsl, но до сих пор не было случая применить lisp для пользы дела. Кажется здесь он вошел под правильным углом, предоставляя динамическую среду исполнения со сборщиком мусора и s-expression для кодирования структур (аналог json, но еще глубже вшиты в язык, любое выражение lisp это s-expression).
Пойду лепить более другие устройства на этом же протоколе, на
очереди ESP32 и NRF51
И параллельно допиливать прошивку до более продуктового состояния,
если вдруг интересно присоединяйтесь!
P.S. Все нападки на Home Assistant выполнены с большой любовью, я там даже чуть комитер.