Использование Android устройства в качестве тонкого UI для С++ программ

в 12:00, , рубрики: android, c++, UI, Программирование, С++, тонкие клиенты

Хочу поделиться с сообществом проектом, которым я потихоньку занимаюсь последние несколько месяцев.

Предисловие

Часто возникают ситуации, когда хочется управлять своей программой с телефона или планшета, но при этом нецелесообразно (или нет возможности) написать для этого отдельное приложение на телефон (слишком много трудозатрат для данного проекта, нет опыта разработки под android, и.т.д.). Такая ситуация периодически возникла у меня, и в конце концов я решил разобраться с этой проблемой раз и навсегда. В результате получилась система, архитектура и использование которой описаны в данной статье.

Цель

Создание системы, позволяющей реализовывать в С++ программах подключаемый UI на базе android устройств. При этом хотелось минимизировать зависимости пользовательского С++ кода от сторонних библиотек, а так же абстрагировать его от протокола передачи данных. Система должна состоять из двух частей: С++ библиотеки и android приложения.

Архитектура системы

Система имеет клиент-серверную архитектуру, где в качестве клиентов выступают android устройства, сервером является пользовательская программа. Коммуникация между ними осуществляется при помощи TCP/IP сокетов. Для реализации протокола общения была создана библиотека TAU.

Основные задачи, за которые отвечает библиотека:

  • генерация и обработка данных, передаваемых между сервером и клиентом
  • передача управления пользовательскому коду для реакции на различные события, произошедшие на клиенте (обработчики UI событий)
  • создание и обслуживание соединения между сервером и клиентом
  • генерация необходимых структур данных для описания конфигурации элементов UI, отображаемых на клиентах

Библиотека состоит следующих пространств имён:

  • tau::communications_handling — отвечает за формирование пакетов, парсинг данных пришедших от клиента, вызов обработчиков в пользовательском коде. Всё, что происходит между моментами подключения и отключения клиента контролируется кодом классов из этого пространства имён.
  • tau::layout_generation — содержит функциональность, позволяющую создавать json-структуры, описывающие расположение и поведение элементов пользовательского интерфейса. Эти данные затем отправляются на клиент и он отображает соответствующий UI.
  • tau::util — содержит различную вспомогательную функциональность, которая не обязательна для использования библиотеки в пользовательском С++ проекте, однако зачастую оказывается полезна. Классы в этом пространстве имён — единственные, которые могут использовать сторонние библиотеки или нестандартные расширения компилятора. Поэтому, здесь находятся классы, отвечающие за работу с TCP/IP сокетами — это платформозависимый код. Сейчас есть две реализации сетевого общения: на базе boost::asio и C++/CLI. Вынос реализации всех сетевых вызовов за пределы tau::communications_handling позволяет пользователю библиотеки при желании написать всю сетевую часть самостоятельно.
  • tau::common — содержит классы, используемые из других частей библиотеки (они выведены сюда, чтобы не было зависимостей между tau::layout_generation и tau::communications_handling)

Использование. Пример №1 — hello, world

Демонстрацию использования библиотеки начнём с простейшего примера, в котором на экран будет просто выведено приветствующее сообщение. Вот как будет выглядеть пользовательский код в нашем случае:

Скрытый текст
#include <tau/layout_generation/layout_info.h>
#include <tau/util/basic_events_dispatcher.h>
#include <tau/util/boost_asio_server.h>
class MyEventsDispatcher : public tau::util::BasicEventsDispatcher
{
public:
    MyEventsDispatcher(
        tau::communications_handling::OutgiongPacketsGenerator & outgoingGeneratorToUse): 
            tau::util::BasicEventsDispatcher(outgoingGeneratorToUse)
        {};

    virtual void packetReceived_requestProcessingError(
		std::string const & layoutID, std::string const & additionalData)
    {
        std::cout << "Error received from client:nLayouID: "
			<< layoutID << "nError: " << additionalData << "n";
    }

    virtual void onClientConnected(
        tau::communications_handling::ClientConnectionInfo const & connectionInfo)
    {
        std::cout << "Client connected: remoteAddr: "
            << connectionInfo.getRemoteAddrDump()
            << ", localAddr : "
            << connectionInfo.getLocalAddrDump() << "n";
    }
    void packetReceived_clientDeviceInfo(
        tau::communications_handling::ClientDeviceInfo const & info)
    {
        using namespace tau::layout_generation;
        std::cout << "Received client information packetn";
        std::string greetingMessage = "Hello, habrahabr!";
        sendPacket_resetLayout(LayoutInfo().pushLayoutPage(
            LayoutPage(tau::common::LayoutPageID("FIRST_LAYOUT_PAGE"),
            LabelElement(greetingMessage))).getJson());
    }
};

int main(int argc, char ** argv)
{
    boost::asio::io_service io_service;
    short port = 12345;
    tau::util::SimpleBoostAsioServer<MyEventsDispatcher>::type s(io_service, port);
    std::cout << "Starting server on port " << port << "...n";
    s.start();
    std::cout << "Calling io_service.run()n";
    io_service.run();
    return 0;
}

Основной класс, который содержит всю пользовательскую логику, взаимодействующую с клиентским устройством (MyEventsDispatcher) должен быть отнаследован от tau::util::BasicEventsDispatcher. В нём переопределены 2 метода из базового класса: onClientConnected() и packetReceived_clientDeviceInfo(). Первый вызывается в момент подключения клиента. Второй метод будет выполнен, когда на сервер придёт информация о клиентском устройстве после подключения (первый пакет после подключения отправляется клиентом).

В нашем случае первый метод тривиален — он только выводит информационное сообщение на консоль. Во втором методе сервер отправляет клиенту лэйаут (layout) — данные о том, какой интерфейс должен быть отображён на клиенте.

Весь код, отвечающий за передачу данных по сети находится в main(). В данном случае, для реализации коммуникации используется библиотека boost::asio. В пространстве имён tau::util есть соответствующие абстракции, что делает данный пример максимально компактным. Использование boost необязательно — любая реализация TCP/IP сокетов может быть довольно легко использована вместе с библиотекой.

Компиляция

В качестве примера, для компиляции будем использовать g++. В нашем случае команда будет следующей:

g++ -lboost_system -pthread -lboost_thread -D TAU_HEADERONLY -D TAU_CPP_03_COMPATIBILITY -I $LIBRARY_LOCATION main.cpp -o demo

Как видно, компилятору передаётся несколько дополнительных параметров:

  • include path до исходников библиотеки (опция -I $LIBRARY_LOCATION)
  • дополнительные библиотеки, необходимые для boost::asio (опции -lboost_system -pthread -lboost_thread)
  • объявления дополнительных макросов, указывающих, каким образом мы компилируем библиотеку в нашем проекте (-D TAU_HEADERONLY -D TAU_CPP_03_COMPATIBILITY)

Данный набор опций — самый общий вариант сборки, который позволят включить библиотеку в любой проект с минимальными усилиями.

От них всех можно при желании избавиться. Если использовать библиотеку внутри проекта, не нужно указывать -I $LIBRARY_LOCATION и -D TAU_HEADERONLY. Для компиляторов, совместимых с C++11, опция -D TAU_CPP_03_COMPATIBILITY не нужна. Зависимость от boost::asio имеет только сетевая часть, которую довольно легко можно переписать без зависимостей.

После компиляции и запуска, сервер начинает слушать на порту 12345.

Запускаем клиент на телефоне, создаём соединение и подключаемся к нему для отображения сообщения. Вот, как это будет выглядеть (я запускал сервер на удалённом компьютере через PuTTY, а клиент запускался в эмуляторе):

Создание соединения с сервером

Использование Android устройства в качестве тонкого UI для С++ программ - 1

Данный пример не предусматривает передачу и получение дополнительных уведомлений между клиентом и сервером, поэтому давайте перейдём к следующему примеру.

Пример №2 — более развёрнутая демонстрация возможностей системы

В данном примере мы добавим в нашем сервере несколько различных элементов, научимся получать от них уведомления, изменять их состояние и переключать страницы.

Код сервера будет выглядеть так:

Скрытый текст

#include <tau/layout_generation/layout_info.h>
#include <tau/util/basic_events_dispatcher.h>
#include <tau/util/boost_asio_server.h>

namespace {
    std::string const INITIAL_TEXT_VALUE("initial text");    
    tau::common::LayoutID const LAYOUT_ID("SAMPLE_LAYOUT_ID");
    tau::common::LayoutPageID const LAYOUT_PAGE1_ID("LAYOUT_PAGE_1");
    tau::common::LayoutPageID const LAYOUT_PAGE2_ID("LAYOUT_PAGE_2");
    tau::common::ElementID const BUTTON_WITH_NOTE_TO_REPLACE_ID("BUTTON_WITH_NOTE_TO_REPLACE");
    tau::common::ElementID const BUTTON_TO_RESET_VALUES_ID("BUTTON_TO_RESET_NOTES");
    tau::common::ElementID const BUTTON_TO_PAGE_1_ID("BUTTON_TO_PG1");
    tau::common::ElementID const BUTTON_TO_PAGE_2_ID("BUTTON_TO_PG2");
    tau::common::ElementID const BUTTON_1_ID("BUTTON_1");
    tau::common::ElementID const BUTTON_2_ID("BUTTON_2");
    tau::common::ElementID const BUTTON_3_ID("BUTTON_3");
    tau::common::ElementID const BUTTON_4_ID("BUTTON_4");
    tau::common::ElementID const TEXT_INPUT_ID("TEXT_INPUT");
    tau::common::ElementID const BOOL_INPUT_ID("BOOL_INPUT");
    tau::common::ElementID const LABEL_ON_PAGE2_ID("LABEL_ON_PAGE2");
};

class MyEventsDispatcher : public tau::util::BasicEventsDispatcher
{
public:
    MyEventsDispatcher(
        tau::communications_handling::OutgiongPacketsGenerator & outgoingGeneratorToUse): 
            tau::util::BasicEventsDispatcher(outgoingGeneratorToUse)
        {};

    virtual void packetReceived_requestProcessingError(
		std::string const & layoutID, std::string const & additionalData)
    {
        std::cout << "Error received from client:nLayouID: "
			<< layoutID << "nError: " << additionalData << "n";
    }

    virtual void onClientConnected(
        tau::communications_handling::ClientConnectionInfo const & connectionInfo)
    {
        std::cout << "Client connected: remoteAddr: "
            << connectionInfo.getRemoteAddrDump()
            << ", localAddr : "
            << connectionInfo.getLocalAddrDump() << "n";
    }
    virtual void packetReceived_clientDeviceInfo(
        tau::communications_handling::ClientDeviceInfo const & info)
    {
        using namespace tau::layout_generation;
        std::cout << "Received client information packetn";
        LayoutInfo resultLayout;
        resultLayout.pushLayoutPage(LayoutPage(LAYOUT_PAGE1_ID, 
            EvenlySplitLayoutElementsContainer(true)
                .push(EvenlySplitLayoutElementsContainer(false)
                    .push(BooleanInputLayoutElement(true).note(INITIAL_TEXT_VALUE).ID(BOOL_INPUT_ID))
                    .push(ButtonLayoutElement().note(INITIAL_TEXT_VALUE)
                        .ID(BUTTON_WITH_NOTE_TO_REPLACE_ID)))
                .push(TextInputLayoutElement().ID(TEXT_INPUT_ID).initialValue(INITIAL_TEXT_VALUE))
                .push(EmptySpace())
                .push(EmptySpace())
                .push(EmptySpace())
                .push(EvenlySplitLayoutElementsContainer(false)
                    .push(ButtonLayoutElement().note("reset notes").ID(BUTTON_TO_RESET_VALUES_ID))
                    .push(EmptySpace())
                    .push(ButtonLayoutElement().note("go to page 2").ID(BUTTON_TO_PAGE_2_ID)
                        .switchToAnotherLayoutPageOnClick(LAYOUT_PAGE2_ID))
                    )
            )
        );
        resultLayout.pushLayoutPage(LayoutPage(LAYOUT_PAGE2_ID, 
            EvenlySplitLayoutElementsContainer(true)
                .push(EvenlySplitLayoutElementsContainer(false)
                    .push(ButtonLayoutElement().note("1").ID(BUTTON_1_ID))
                    .push(ButtonLayoutElement().note("2").ID(BUTTON_2_ID)))
                .push(EvenlySplitLayoutElementsContainer(false)
                    .push(ButtonLayoutElement().note("3").ID(BUTTON_3_ID))
                    .push(ButtonLayoutElement().note("4").ID(BUTTON_4_ID)))
                .push(EvenlySplitLayoutElementsContainer(true)
                    .push(LabelElement("").ID(LABEL_ON_PAGE2_ID))
                    .push(ButtonLayoutElement().note("back to page 1").ID(BUTTON_TO_PAGE_1_ID)))
        ));
        resultLayout.setStartLayoutPage(LAYOUT_PAGE1_ID);
        sendPacket_resetLayout(resultLayout.getJson());
    }
    virtual void packetReceived_buttonClick(
        tau::common::ElementID const & buttonID)
    {
        std::cout << "event: buttonClick, id=" << buttonID << "n";
        if (buttonID == BUTTON_TO_RESET_VALUES_ID) {
            sendPacket_updateTextValue(TEXT_INPUT_ID, INITIAL_TEXT_VALUE);
        } else if (buttonID == BUTTON_TO_PAGE_1_ID) {
            sendPacket_changeShownLayoutPage(LAYOUT_PAGE1_ID);
        } else if (buttonID == BUTTON_1_ID) {
            sendPacket_changeElementNote(LABEL_ON_PAGE2_ID, "Button 1 pressed");
        } else if (buttonID == BUTTON_2_ID) {
            sendPacket_changeElementNote(LABEL_ON_PAGE2_ID, "Button 2 pressed");
        } else if (buttonID == BUTTON_3_ID) {
            sendPacket_changeElementNote(LABEL_ON_PAGE2_ID, "Button 3 pressed");
        } else if (buttonID == BUTTON_4_ID) {
            sendPacket_changeElementNote(LABEL_ON_PAGE2_ID, "Button 4 pressed");
        }
    }
    virtual void packetReceived_layoutPageSwitched(
        tau::common::LayoutPageID const & newActiveLayoutPageID)
    {
        std::cout << "event: layoutPageSwitch, id=" << newActiveLayoutPageID << "n";
    }
    virtual void packetReceived_boolValueUpdate(
        tau::common::ElementID const & inputBoxID,
        bool new_value, bool is_automatic_update)
    {
        std::cout << "event: boolValueUpdate, id="
            << inputBoxID << ", value=" << new_value << "n";
    }
    virtual void packetReceived_textValueUpdate(
        tau::common::ElementID const & inputBoxID,
        std::string const & new_value, bool is_automatic_update)
    {
        std::cout << "event: textValueUpdate, id="
            << inputBoxID << ",ntvalue=" << new_value << "n";
        sendPacket_changeElementNote(BOOL_INPUT_ID, new_value);
        sendPacket_changeElementNote(BUTTON_WITH_NOTE_TO_REPLACE_ID, new_value);
    }
};

int main(int argc, char ** argv)
{
    boost::asio::io_service io_service;
    short port = 12345;
    tau::util::SimpleBoostAsioServer<MyEventsDispatcher>::type s(io_service, port);
    std::cout << "Starting server on port " << port << "...n";
    s.start();
    std::cout << "Calling io_service.run()n";
    io_service.run();
    return 0;
}

Все изменения по сравнению с предыдущим примером были сделаны в классе MyEventsDispatcher. Были добавлены следующие методы-обработчики событий от клиента:

  • обработчик события нажатия кнопки packetReceived_buttonClick. ID кнопки передаётся методу в качестве параметра.
  • обработчики пакетов, передающих значения переменных от клиента к серверу: packetReceived_boolValueUpdate, packetReceived_intValueUpdate, packetReceived_floatPointValueUpdate, packetReceived_textValueUpdate
  • обработчик события смены отображаемой страницы с элементами packetReceived_layoutPageSwitched

Кроме того, соответственно изменился лэйаут, отправляемый клиенту при подключении.

Поскольку у нас демонстрационный пример, код в обработчиках максимально простой — дамп информации о событиях в консоль, а также отправка различных команд клиенту.

Все команды будут отправляются клиенту из обработчика нажатий кнопок packetReceived_buttonClick() (естественно, это не обязательно делать там, но так проще и нагляднее).

Каждой из команд соответствует пакет, передаваемый от сервера клиенту. Формирование и отправка этих пакетов происходит при вызове специальных методов, определённых в BasicEventsDispatcher:

  • sendPacket_resetLayout() — замена всего лэйаута
  • sendPacket_requestValue() — запрос значения переменной в одном из инпутов
  • sendPacket_updateBooleanValue(), sendPacket_updateIntValue(), sendPacket_updateFloatPointValue, sendPacket_updateTextValue() — изменение значения переменных в инпутах
  • sendPacket_changeElementNote() — изменение какого-либо read-only текста (текст на кнопках, чекбоксах, лейблах)
  • sendPacket_changeShownLayoutPage() — переключение на другую страницу с элементами
  • sendPacket_changeElementEnabledState() — переключение активного статуса элементов (неактивные элементы отображаются, но с ними нельзя взаимодействовать)

Вот как работает данный пример:

Демонстрация работы с элементами UI

Использование Android устройства в качестве тонкого UI для С++ программ - 2

Как видно, для каждого действия на клиентском устройстве, на сервере выполняется соответствующий код. Когда меняются значения элементах пользовательского ввода, сервер получает уведомление о новом значении переменной в этом элементе. В обработчике нажатий кнопок различные пакеты отправляются от сервера клиенту (тип пакета зависит от того, какая из кнопок была нажата). В этом примере также показано, как работает переключение страниц. Разбиение лэйаута на страницы позволяет группировать элементы в соответствии их функциям. На экране клиента всегда отображена только одна из страниц, что уменьшает загруженность интерфейса.

Пример №3 — что-нибудь полезное

Последний на сегодня пример — частичная реализация одной из задач, для которой я начал этот проект. Это простейший эмулятор клавиатурного ввода для windows (использует winapi-функцию SendInput()).

Код этого примера лежит у меня на гитхабе. Тут приводить его не буду — он ничего нового в использовании библиотеки не демонстрирует по сравнению со вторым примером. Тут приведу только демо его работы:

Эмуляция клавиатурного ввода

Использование Android устройства в качестве тонкого UI для С++ программ - 3

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

Эпилог

Вместо заключения, хочу обратиться к сообществу. Нужна ли подобная система? Не изобретаю ли я очередной велосипед? Что нужно описать более детально? В каком направлении нужно развиваться дальше?

Мне сейчас приходят в голову следующие пути развития (они почти полностью ортогональны, поэтому хотелось бы расставить приоритеты):

  • добавление клиентских приложений для других платформ (IOS, PC)
  • расширение функциональности протокола передачи данных (heartbeat packets for connection monitoring, communication control commands)
  • добавление новых UI элементов (drop-down boxes, images, e.t.c)
  • более глубокая кастомизация внешнего вида элементов на клиенте (цвета, шрифты, стили)
  • поддержка более специфичных функций клиентского устройства (notifications, sensors, volume buttons, e.t.c)
  • добавление серверных библиотек для других языков программирования

Кроме того, буду рад услышать критику по поводу архитектуры и реализации библиотеки в том состоянии, в котором она сейчас находится.

Ссылки:

Автор: taco_attaco

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js