Как скрипты могут помочь при взаимодействии компонентов, типичных для IoT? С помощью языка Lua. Наш ведущий разработчик Леонид Садофьев рассказывает и показывает на примере своего демоприложения про использование Lua на проектах уровня бизнес-логики (BL). Его приложение разработано на С++ и применяется для умного дома, но может подойти и для приложений других типов. Всё важное под катом — передаём слово автору.

Уровень BL как оптимальная точка использования скриптов

Для начала рассмотрим, почему уровень BL подходит для применения скриптов. 

Он имеет принципиальные особенности:

  1. При правильном проектировании приложения на этом уровне не проводятся сложные вычисления или обработка больших массивов данных. Задача компонентов уровня BL в первую очередь — управление состоянием других компонентов системы. Как следствие, требования скорости работы и эффективности кода для этого уровня являются минимальными.

  2. Бизнес-логику приложения, как правило, хуже всего определяют в техническом задании и чаще всего переделывают по требованиям заказчика. Таким образом возможность внесения изменений на этом уровне с минимальными усилиями позволит сократить время и стоимость разработки.

В итоге имеем максимальную гибкость с минимальными потерями общей производительности системы.

Выбор скриптового языка

При выборе скриптового языка стоит учитывать следующие требования.

  1. Возможность непосредственного встраивания интерпретатора в приложение. Очевидно, что в случае реализации интерпретатора в качестве отдельной программы расходы на организацию межпроцессного взаимодействия перекрывают все возможные выгоды от применения скриптов.

  2. Максимально тесная интеграция с кодом на C++. Односторонний процесс, то есть только вызов функций скрипта из кода на C++, для задач бизнес-логики не подойдёт. Скрипт должен иметь возможность в полном объёме обращаться к объектам C++.

  3. Лёгкость включения интерпретатора в код и минимальные накладные расходы на его работу.

Эти требования очевидны. Однако существует ещё один менее очевидный момент. Для реализации задач BL нам не нужен скриптовый язык с очень развитой функциональностью. Хранение и обработка данных, коммуникации и тому подобное должны реализовываться компонентами, написанными на C++. Поэтому целесообразно пожертвовать функциональностью скриптового языка в пользу лёгкости интеграции интерпретатора.

Простота выбираемого скриптового языка имеет ещё одно менее явное преимущество — это возможность перевалить конфигурирование и настройку системы на пользователя. Конечно, заставлять бедного юзера писать скрипты даже на самом простом языке — слишком жестоко. А вот разработать генератор скриптов на основании задаваемых пользователем настроек вполне можно. В этом случае, чем проще структура выбранного скриптового языка, тем проще окажется такой генератор и тем меньше сил придётся потратить на его разработку.

Давайте рассмотрим два скриптовых языка, позволяющих решить поставленную задачу. Это Python и Lua. Кратко упомяну некоторые основные моменты:

  • Lua может быть подключена к проекту на C++ за счёт включения простой и лёгкой библиотеки интерпретатора (объём кода — менее 100 килобайт). Подключение дополнительных средств C++ не требуется. Python может быть подключён только с использованием классов библиотеки BOOST. Тащить эту весьма объёмную библиотеку далеко не всегда имеет смысл. Особенно если мы говорим о встраиваемом ПО, которое работает в условиях ограниченных ресурсов;

  • Python разрабатывался как самостоятельный язык программирования. Как следствие, он намного более развит и функционален. Однако за это приходится платить объёмом и сложностью интерпретатора. Lua в свою очередь разрабатывалась именно как скриптовое расширение для программ на С/С++. Безусловно, по функциональности Lua заметно уступает Python. Однако выше уже говорилось о неважности избыточной функциональности скриптового языка; 

  • чем сложнее используемый скриптовый язык, тем больше времени потребуется на его освоение. В этом отношении Lua имеет неоспоримое преимущество, так как для написания простых скриптов хватит пары часов знакомства. Даже шапочное знакомство с Python потребует заметно больше времени.

Lua

Python

Библиотека

Простая и лёгкая библиотека интерпретатора

Только с использованием классов библиотеки BOOST

Разработка и функциональность

Скриптовое расширение

Самостоятельный язык

Время освоения

Два часа на ознакомление

Определённо больше двух часов

Поэтому в качестве скриптового языка для задач уровня бизнес-логики намного проще и целесообразнее выбрать Lua. Вопросы использования Lua для реализации бизнес-логики приложения подробно рассмотрены в этой статье.

Итак, пора переходить от слов к делу.

Постановка задачи

Для иллюстрации принципов применения Lua я разработал простую модельную систему класса «умный дом». В нашей модельной системе есть три источника света, два датчика освещённости, выключатель. Всё максимально просто. Общая схема представлена на рисунке 1.

Рис 1. Общая схема
Рис 1. Общая схема

Проблема состоит в том, что у нас нет полной определённости в том, как именно включение и выключение света должно зависеть от других компонентов, то есть в логике работы системы. Должны ли лампы включаться только при низкой освещённости в комнате? Или дополнительно должен быть включён выключатель? Для разработчика это означает перспективу многократной переделки приложения. Если код будет полностью написан на C++, каждое изменение потребует пересборки приложения и нового цикла тестирования, то есть больших трудозатрат. Да и гибкость приложения пострадает достаточно сильно. Применение конфигурационных файлов, конечно, сможет помочь, но приведёт к достаточно громоздкому коду машины состояний. Вот тут-то нас и могут выручить скрипты.

Задача таких скриптов проста — проверить состояние нужного датчика (или датчиков) и в зависимости от этого дать команду лампе на включение или выключение. Размер скрипта составит несколько строк. Не больше. Меняется взаимосвязь между датчиками и лампами — меняется скрипт. Всё предельно просто.

Очевидно, нет никакой гарантии, что все используемые источники освещения будутиметь один тип и одинаково управляться. Тут нас может выручить ООП. Самый простой вариант — представить драйвер каждого из типов ламп в виде класса С++, унаследованного от общего базового класса, который определяет требуемый интерфейс. В простейшем случае это две функции:

void TurnOn(void);
void TurnOff(void);

В базовом классе эти функции будут абстрактными, а каждый унаследованный из него класс драйвера будет иметь соответствующую реализацию.

Аналогичная ситуация и с датчиками освещённости, хотя базовая функция там будет несколько другой. Нам понадобится функция, возвращающая значение освещённости в процентах от максимальной:

unsigned short GetLightPercent(void);

Опять каждый класс драйвера датчика будет иметь его собственную реализацию.

Ну и для полноты картины — третий тип устройства — выключатель. Понятно, что для него понадобится только функция bool IsOn(void), возвращающая true, если выключатель включён, и false в противном случае.

Очевидно, что скрипт должен уметь добираться до конкретных экземпляров классов и действовать, используя описанные выше интерфейсы. Всё просто, очевидно и понятно. 

В нашем случае схема взаимодействия объектов в приложении близка к известному паттерну проектирования «Visitor» («Посетитель»). Те, кому этот паттерн не знаком, могут изучить статью Википедии. Согласно ему, мы имеем дело с набором управляемых компонентов (Elements) и управляющим компонентом (Visitor). Однако классическая реализация данного паттерна требует определения в классе Visitor функции для каждого типа управляемых компонентов. Это является существенным недостатком. Если мы используем скрипты, то сможем избавиться от указанного недостатка, определив только одну функцию «Visit», которая будет сводиться к вызову скрипта. 

Подключаем Lua

Для подключения нам понадобятся две библиотеки. 

  1. Собственно библиотека интерпретатора Lua, загружаемая в виде исходных текстов c сайта lua.org. Библиотека распространяется по лицензии MIT, что допускает свободное использование как в частных, так и в коммерческих проектах. Для сборки интерпретатора надо распаковать скачанный архив, создать проект статической библиотеки и включить в него все исходные файлы из архива за исключением lua.c и luac.c (эти два файла нужны в случае сборки интерпретатора как отдельной программы). Возможна сборка и 32-битной, и 64-битной версий библиотеки.

  2. Вспомогательная библиотека LuaBridge. Эта библиотека значительно упрощает доступ к объектам C++ из Lua скриптов.

Важно! Интерпретатор Lua позволяет не только загружать скрипт из файла, но и передавать его в виде текстовой строки.

Демонстрационное приложение

Для демонстрации использования Lua я разработал простое Windows-приложение. Разработка велась в Microsoft Visual Studio Community edition с использованием библиотеки MFC. Исходный код приложения доступен на GitHub.

Рисунок 2. Окно демонстрационного приложения
Рисунок 2. Окно демонстрационного приложения

Задавая ползунками уровень освещённости двух детекторов, можно управлять включением ламп. Их поведение в зависимости от сигналов «детекторов» определяет загруженный скрипт. Он может быть загружен из файла или отредактирован непосредственно в соответствующем поле («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.

Комментарии (2)


  1. nzlgd
    21.02.2022 12:42

    Мда, "освоение lua меньше двух часов"

    Просто удачи как говорится


  1. Playa
    22.02.2022 01:25

    Серьезно, скриншоты с кодом?