Как скрипты могут помочь при взаимодействии компонентов, типичных для IoT? С помощью языка Lua. Наш ведущий разработчик Леонид Садофьев рассказывает и показывает на примере своего демоприложения про использование Lua на проектах уровня бизнес-логики (BL). Его приложение разработано на С++ и применяется для умного дома, но может подойти и для приложений других типов. Всё важное под катом — передаём слово автору.
Уровень BL как оптимальная точка использования скриптов
Для начала рассмотрим, почему уровень BL подходит для применения скриптов.
Он имеет принципиальные особенности:
При правильном проектировании приложения на этом уровне не проводятся сложные вычисления или обработка больших массивов данных. Задача компонентов уровня BL в первую очередь — управление состоянием других компонентов системы. Как следствие, требования скорости работы и эффективности кода для этого уровня являются минимальными.
Бизнес-логику приложения, как правило, хуже всего определяют в техническом задании и чаще всего переделывают по требованиям заказчика. Таким образом возможность внесения изменений на этом уровне с минимальными усилиями позволит сократить время и стоимость разработки.
В итоге имеем максимальную гибкость с минимальными потерями общей производительности системы.
Выбор скриптового языка
При выборе скриптового языка стоит учитывать следующие требования.
Возможность непосредственного встраивания интерпретатора в приложение. Очевидно, что в случае реализации интерпретатора в качестве отдельной программы расходы на организацию межпроцессного взаимодействия перекрывают все возможные выгоды от применения скриптов.
Максимально тесная интеграция с кодом на C++. Односторонний процесс, то есть только вызов функций скрипта из кода на C++, для задач бизнес-логики не подойдёт. Скрипт должен иметь возможность в полном объёме обращаться к объектам C++.
Лёгкость включения интерпретатора в код и минимальные накладные расходы на его работу.
Эти требования очевидны. Однако существует ещё один менее очевидный момент. Для реализации задач BL нам не нужен скриптовый язык с очень развитой функциональностью. Хранение и обработка данных, коммуникации и тому подобное должны реализовываться компонентами, написанными на C++. Поэтому целесообразно пожертвовать функциональностью скриптового языка в пользу лёгкости интеграции интерпретатора.
Простота выбираемого скриптового языка имеет ещё одно менее явное преимущество — это возможность перевалить конфигурирование и настройку системы на пользователя. Конечно, заставлять бедного юзера писать скрипты даже на самом простом языке — слишком жестоко. А вот разработать генератор скриптов на основании задаваемых пользователем настроек вполне можно. В этом случае, чем проще структура выбранного скриптового языка, тем проще окажется такой генератор и тем меньше сил придётся потратить на его разработку.
Давайте рассмотрим два скриптовых языка, позволяющих решить поставленную задачу. Это Python и Lua. Кратко упомяну некоторые основные моменты:
Lua может быть подключена к проекту на C++ за счёт включения простой и лёгкой библиотеки интерпретатора (объём кода — менее 100 килобайт). Подключение дополнительных средств C++ не требуется. Python может быть подключён только с использованием классов библиотеки BOOST. Тащить эту весьма объёмную библиотеку далеко не всегда имеет смысл. Особенно если мы говорим о встраиваемом ПО, которое работает в условиях ограниченных ресурсов;
Python разрабатывался как самостоятельный язык программирования. Как следствие, он намного более развит и функционален. Однако за это приходится платить объёмом и сложностью интерпретатора. Lua в свою очередь разрабатывалась именно как скриптовое расширение для программ на С/С++. Безусловно, по функциональности Lua заметно уступает Python. Однако выше уже говорилось о неважности избыточной функциональности скриптового языка;
чем сложнее используемый скриптовый язык, тем больше времени потребуется на его освоение. В этом отношении Lua имеет неоспоримое преимущество, так как для написания простых скриптов хватит пары часов знакомства. Даже шапочное знакомство с Python потребует заметно больше времени.
Lua |
Python |
|
Библиотека |
Простая и лёгкая библиотека интерпретатора |
Только с использованием классов библиотеки BOOST |
Разработка и функциональность |
Скриптовое расширение |
Самостоятельный язык |
Время освоения |
Два часа на ознакомление |
Определённо больше двух часов |
Поэтому в качестве скриптового языка для задач уровня бизнес-логики намного проще и целесообразнее выбрать Lua. Вопросы использования Lua для реализации бизнес-логики приложения подробно рассмотрены в этой статье.
Итак, пора переходить от слов к делу.
Постановка задачи
Для иллюстрации принципов применения Lua я разработал простую модельную систему класса «умный дом». В нашей модельной системе есть три источника света, два датчика освещённости, выключатель. Всё максимально просто. Общая схема представлена на рисунке 1.
Проблема состоит в том, что у нас нет полной определённости в том, как именно включение и выключение света должно зависеть от других компонентов, то есть в логике работы системы. Должны ли лампы включаться только при низкой освещённости в комнате? Или дополнительно должен быть включён выключатель? Для разработчика это означает перспективу многократной переделки приложения. Если код будет полностью написан на C++, каждое изменение потребует пересборки приложения и нового цикла тестирования, то есть больших трудозатрат. Да и гибкость приложения пострадает достаточно сильно. Применение конфигурационных файлов, конечно, сможет помочь, но приведёт к достаточно громоздкому коду машины состояний. Вот тут-то нас и могут выручить скрипты.
Задача таких скриптов проста — проверить состояние нужного датчика (или датчиков) и в зависимости от этого дать команду лампе на включение или выключение. Размер скрипта составит несколько строк. Не больше. Меняется взаимосвязь между датчиками и лампами — меняется скрипт. Всё предельно просто.
Очевидно, нет никакой гарантии, что все используемые источники освещения будутиметь один тип и одинаково управляться. Тут нас может выручить ООП. Самый простой вариант — представить драйвер каждого из типов ламп в виде класса С++, унаследованного от общего базового класса, который определяет требуемый интерфейс. В простейшем случае это две функции:
void TurnOn(void);
void TurnOff(void);
В базовом классе эти функции будут абстрактными, а каждый унаследованный из него класс драйвера будет иметь соответствующую реализацию.
Аналогичная ситуация и с датчиками освещённости, хотя базовая функция там будет несколько другой. Нам понадобится функция, возвращающая значение освещённости в процентах от максимальной:
unsigned short GetLightPercent(void);
Опять каждый класс драйвера датчика будет иметь его собственную реализацию.
Ну и для полноты картины — третий тип устройства — выключатель. Понятно, что для него понадобится только функция bool IsOn(void)
, возвращающая true, если выключатель включён, и false в противном случае.
Очевидно, что скрипт должен уметь добираться до конкретных экземпляров классов и действовать, используя описанные выше интерфейсы. Всё просто, очевидно и понятно.
В нашем случае схема взаимодействия объектов в приложении близка к известному паттерну проектирования «Visitor» («Посетитель»). Те, кому этот паттерн не знаком, могут изучить статью Википедии. Согласно ему, мы имеем дело с набором управляемых компонентов (Elements) и управляющим компонентом (Visitor). Однако классическая реализация данного паттерна требует определения в классе Visitor функции для каждого типа управляемых компонентов. Это является существенным недостатком. Если мы используем скрипты, то сможем избавиться от указанного недостатка, определив только одну функцию «Visit», которая будет сводиться к вызову скрипта.
Подключаем Lua
Для подключения нам понадобятся две библиотеки.
Собственно библиотека интерпретатора Lua, загружаемая в виде исходных текстов c сайта lua.org. Библиотека распространяется по лицензии MIT, что допускает свободное использование как в частных, так и в коммерческих проектах. Для сборки интерпретатора надо распаковать скачанный архив, создать проект статической библиотеки и включить в него все исходные файлы из архива за исключением lua.c и luac.c (эти два файла нужны в случае сборки интерпретатора как отдельной программы). Возможна сборка и 32-битной, и 64-битной версий библиотеки.
Вспомогательная библиотека LuaBridge. Эта библиотека значительно упрощает доступ к объектам C++ из Lua скриптов.
Важно! Интерпретатор Lua позволяет не только загружать скрипт из файла, но и передавать его в виде текстовой строки.
Демонстрационное приложение
Для демонстрации использования Lua я разработал простое Windows-приложение. Разработка велась в Microsoft Visual Studio Community edition с использованием библиотеки MFC. Исходный код приложения доступен на GitHub.
Задавая ползунками уровень освещённости двух детекторов, можно управлять включением ламп. Их поведение в зависимости от сигналов «детекторов» определяет загруженный скрипт. Он может быть загружен из файла или отредактирован непосредственно в соответствующем поле («Lua») диалогового окна. Несмотря на свою простоту, приложение хорошо иллюстрирует применение Lua-скриптов для реализации бизнес-логики IoT-приложений.
Исходный код приложения
Переходим к самому интересному — исходному коду. В нашем приложении существуют два типа элементов — лампа и детектор освещённости. Для работы определим интерфейсы для каждого из них как абстрактные базовые классы.
Минимальная функциональность, которая нам нужна для лампы, — возможность включить, выключить её и определить, включена ли она в данный момент. Определим соответствующие методы:
Для детектора освещённости в первую очередь нам нужна возможность прочитать текущее значение.
В проекте приложения эти классы определены в заголовочном файле IoTElement.h.
Поскольку базовые классы являются абстрактными, нам понадобятся выведенные из них производные классы, выполняющие реальную работу. В демоприложении для лампы реальная работа — это замена изображения в диалоговом окне. Для детектора — установка нового значения на основании положения ползунка. Поэтому не стоит тратить место на рассмотрение их кода. Никакой специфики, связанной с Lua в коде этих классов нет.
Реальная работа с Lua скриптами сосредоточена в классе CLuaVisitor. Посмотрим на его описание:
Во-первых, для этого класса нам понадобится включить два библиотечных заголовка — lua.hpp (заголовочный файл библиотеки интерпретатора Lua) и LuaBridge.h (заголовочный файл библиотеки LuaBridge). Во-вторых, появляется (и используется) некий класс CLuaVisitorHelper
. Именно этот класс и предоставляет необходимый для Lua-скрипта интерфейс к объектам C++ (лампам и детекторам). Вот его описание:
Как видно, этот класс предоставляет три интерфейсных функции:
В нашем демонстрационном примере ничего больше и не нужно. Посмотрим, как устроены эти функции:
Все они обращаются к внешнему классу IoTObjectFactory
, который выполняет роль фабрики классов и регулирует доступ к конкретным компонентам. Как видно из представленного кода функций, он является синглтоном и создаётся классом приложения.
Теперь самое интересное — как сделать класс C++ доступным для Lua-скрипта.
Рассмотрим код конструктора класса CLuaVisitor
:
Первое, что необходимо сделать — инициализировать контекст выполнения для интерпретатора. Интерпретатор Lua не использует никаких глобальных переменных, и вся информация о его текущем состоянии хранится в динамической структуре, имеющей тип lua_State
. Естественно, эту динамическую структуру перед использованием надо создать и инициализировать. Эту работу и выполняет функция luaL_newstate()
из библиотеки Lua. Возвращаемое функцией luaL_newstate()
значение необходимо сохранять, так как оно будет использоваться в качестве параметра при всех обращениях к библиотечным функциям Lua.
После того, как мы инициализировали контекст, необходимо инициализировать собственно интерпретатор. Это делает функция luaL_openlibs(m_pLuaState);
После вызова двух этих функций интерпретатор Lua полностью готов к работе.
Далее мы создаём экземпляр класса CLuaVisitorHelper
, который будем передавать скрипту. В данном случае, поскольку скрипты весьма простые, класс мы будем создавать в глобальном пространстве имён Lua. Вызов функции getGlobalNamespace(m_pLuaState)
даёт доступ к этому пространству.
Далее средствами библиотеки LuaBridge мы определяем класс в языке Lua, который и будет использоваться нашими скриптами. Важный момент: при определении класса Lua нет необходимости передавать все методы класса C++ — достаточно определить только те, которые используют на практике.
На этом инициализация Lua для работы наших скриптов завершена. Теперь остаётся только их использовать. И это совсем просто.
При нажатии кнопки Execute текст Lua-скрипта считывается из поля редактирования и передаётся на выполнение.
Скрипт передаётся на выполнение вызовом функции luaL_dostring
. Важно, что эта функция не ограничивает реальную длину скрипта и понятие «строка» здесь в контексте строки C/C++, а не строки исполняемого кода. В демоприложении использована именно эта функция библиотеки Lua вместо функции скрипта из файла, чтобы его можно было редактировать непосредственно внутри. В случае ошибки выполнения скрипта в Lua-интерпретаторе её описание можно получить с помощью функции luaL_tostring
.
Ещё раз обращаю внимание на то, что функции библиотеки Lua в качестве первого параметра принимают указатель на контекст выполнения (переменная m_pLuaState
), который был инициализирован в конструкторе класса CLuaVisitor
.
Вот в общем-то и всё. В заключение рассмотрим Lua-скрипт, использованный в данном приложении:
В первой строке скрипта мы получаем доступ к экземпляру класса CLuaVisitorHelper
, который был передан контексту интерпретатора Lua при инициализации в конструкторе класса CLuaVisitor
. Далее мы используем методы данного класса для чтения текущего состояния ламп и датчиков и для управления лампами в зависимости от прочитанных значений. Используя поле редактирования демоприложения, можно менять условия включения и выключения ламп и смотреть, как это сказывается на поведении системы.
Полная версия проекта демонстрационного приложения с исходным кодом доступна для скачивания на GitHub.
nzlgd
Мда, "освоение lua меньше двух часов"
Просто удачи как говорится