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

Предисловие


Часто возникают ситуации, когда хочется управлять своей программой с телефона или планшета, но при этом нецелесообразно (или нет возможности) написать для этого отдельное приложение на телефон (слишком много трудозатрат для данного проекта, нет опыта разработки под 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 packet\n";
        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, а клиент запускался в эмуляторе):

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

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

Пример №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 packet\n";
        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 << ",\n\tvalue=" << 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

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

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


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

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

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

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

Эпилог


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

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

  • добавление клиентских приложений для других платформ (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)
  • добавление серверных библиотек для других языков программирования

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

Ссылки:

Поделиться с друзьями
-->

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


  1. OldFisher
    13.09.2016 15:39

    Очень хорошая и нужная штука. Уже вижу потенциальные возможности применения по работе. Я со своей стороны проголосую за улучшенный UI (особенно интересует возможность передачи изображений) и за поддержку специфических функций клиентского устройства.
    Заодно интересует и возможность надёжной идентификации/авторизации пользователей.


    1. taco_attaco
      13.09.2016 17:14

      Изображения и авторизация — две вещи, которые крутятся у меня в голове, но пока я не решил, как лучше их реализовывать в библиотеке с архитектурной точки зрения.
      Основное требование, которого я придерживаюсь при написании библиотеки — её легковесность и кросплатформенность. Именно поэтому весь сетевой код вынесен в отдельное пространство имён, которое необязательно для использования. Для авторизации и шифрования скорее всего придётся использовать внешние библиотеки, что может поставить под вопрос кросплатформенность.
      С изображениями немного попроще — передать картинки на клиентское устройство можно средствами операционной системы (отправить URL картинки клиенту, который сам её скачает). Это, конечно, урезанный вариант, но, как мне кажется, большую долю use-кейсов он покроет.

      Идентификация клиентских устройств в базовом виде есть уже сейчас. Во время инициации соединения первый пакет данных отправляется от клиента к серверу. В этом пакете есть строковый параметр — deviceID, который можно использовать в качестве идентификатора устройства.
      В С++ коде эти данные доступны в параметре коллбэка:

      void packetReceived_clientDeviceInfo(
              tau::communications_handling::ClientDeviceInfo const & info)
      


      1. OldFisher
        13.09.2016 19:16
        +1

        По поводу изображений — я имел в виду картинки, генерируемые самим приложением-сервером. Например, регулярно обновляемый background с красивыми индикаторами состояния или даже какая-то 3D-визуализация.

        А вот для серьёзной авторизации «как у взрослых дядь», видимо, понадобится в светлом, но отдалённом будущем реализовать какой-то протокол с обеих сторон, рассмотреть способы генерации и защиты идентификационных данных и всё такое. Сложная тема.


        1. taco_attaco
          14.09.2016 13:18

          С рисованием на серверной стороне есть несколько вариантов реализации.

          1. Рисовать картинку на сервере и отправлять полученный в результате bitmap на клиент. Этот подход проще в реализации, но усложняет пользовательский код. Необходимо на пользовательской стороне иметь возможность генерировать битмапки, что неизбежно потребует дополнительных усилий и/или добавит зависимостей.
          2. Рисовать напрямую на клиенте. Это можно сделать пересылая все команды рисования по сети и воспроизводя эти команды на клиенте. На клиентской стороне в UI будет элемент типа Canvas, на котором будут воспроизводиться все операции. Проблема тут в том, что такие объекты имеют очень богатый интерфейс рисования, который необходимо будет переносить в С++ библиотеку. Кроме того, Canvas-объекты различаются на разных платформах, поэтому, если будут добавляться клиенты для других платформ, возникнет проблема унификации отображаемого изображения.
          3. Компромисом между предыдущими двумя вариантами может быть использование svg изображений для таких задач. Поскольку svg открытый текстовый формат, его должно быть довольно легко как генерировать на пользовательской стороне, так и отображать на клиентской. При этом, если пользователю нужно отображать довольно ограниченное множество изображений, то он сможет обойтись без использования внешних библиотек (достаточно иметь базовый шаблон изображения, из которого можно делать конечные svg объекты). Стандартизация svg должна позволить отображать изображения на клиенте одинаково на всех платформах.

          Мне пока что из них больше всего нравится третий вариант.


          1. OldFisher
            14.09.2016 19:18

            Мне это представлялось как один или несколько элементов управления на стороне клиента, способных отображать переданные сервером битмапы (в общем смысле, необязательно именно BMP): background, image box, button… В конце концов, если серверу так приспичило отображать свои картинки, он и битмап сгенерирует и ничего страшного с ним не случится. А если там 3D, так и совсем просто.

            Возможность загружать svg тоже ценная и полезная возможность, но в моём-то случае нужны именно битмапы.


            1. taco_attaco
              15.09.2016 11:54
              +1

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

              Про 3D не совсем понял. Вы имеете ввиду рендеринг 3D сцены в буфер в памяти и отправку этого буфера как картинки на клиент?


              1. OldFisher
                15.09.2016 12:50

                Да, на GPU (а следовательно, в OpenGL, DirectX и Vulkan) это достаточно легко делается.


  1. tzlom
    13.09.2016 16:22

    На лету менять лейауты можно? При этом интерфейс как себя поведёт? Исходники андроид приложения не нашёл.


    1. taco_attaco
      13.09.2016 17:34

      Да, лэйауты можно менять в любой момент. При этом старый лэйаут просто заменяется на новый. Поскольку у клиента практически нет своего состояния, это можно делать без особых проблем.
      Исходники приложения для андроид я пока публиковать не собирался.


  1. zelyony
    13.09.2016 16:52
    +2

    • сейчас зависит от TAU. насколько быстро будет оно развиваться?
    • заюзать для клиента Qt/QML:
      • готовые контролы (шрифты, цвета, стили)
      • встроенная сетевая либка: QTcp-/Udp-/Ssl-/WebSocket...
      • формы выглядят как QML-скрипты (или в каком-то ином виде скриптов), которые можно распарсить или сгенерировать
      • встроенные скрипты и обработчики — валидация/маски инпута на клиенте, а не отправка каждого изменения текстового поля на сервер
    • с кодогенерацией будет совсем хорошо: есть "скриптовое" описание UI с логикой работы, на основе него можно сгенерить/cбилдять/присобачить и код для сервера с JITом — C++/CLang/Qt(нет проблем с сериализацией), C#/Roslyn (по мне, он менее монструозный, чем CLang, но будут вопросы с сериализацией из/в Qt-клиент) и код для клиента — QML.


    1. taco_attaco
      13.09.2016 18:21

      Перенос логики на клиентское устройство — противоположный подход к данной проблеме. Он позволяет гораздо серьёзнее кастомизовать клиентскую часть.
      Проблемой с моей точки зрения является то, что при этом на клиенте возникает сложное состояние, которое надо синхронизировать с сервером. Это серьёзно усложнит серверную часть библиотеки (а также добавит зависимостей в клиентском коде), чего мне хотелось избежать. Кроме того, возникает вопрос отладки скриптов клиентской части, что может тоже оказаться проблематичным.


  1. zelyony
    13.09.2016 17:10

    есть еще "конкурент": UbiqMobile


  1. Durimar123
    13.09.2016 18:27

    Возможно в 80% случаев будет проще настроить HTML server и сделать билд HTML страниц, а на андроиде/ios/другой комп и т.д.
    грузить эти страницы?


    1. taco_attaco
      13.09.2016 18:45

      Поднятие web-сервера внутри пользовательского приложения это альтернативный подход, который очень часто используется. Вот хороший пример этого подхода. Однако, зачастую, это серьёзное усложнение с точки зрения пользовательского кода.
      Данную систему лучше всего рассматривать как средство пользовательского ввода/вывода: основная её функция — передавать команды и данные от клиента серверу, отображать информацию для клиента.


      1. MacIn
        14.09.2016 17:36
        +1

        Вы можете написать конвертер, который будет принимать примерно такие же параметры, иметь примерно такой же API, как вы привыкли, но генерировать HTML.
        И это будет много лучше, потому что будет работать и на desktop системах, и на Android'е и на iOS и на других ХренаксОС, которые появятся/прибавят в популярности.


  1. x893
    13.09.2016 18:46

    Добавить поддержку последовательного интерфейса (через USB), поддержку стандартного протокола и получится HMI клиент.


  1. alex3d
    14.09.2016 03:02
    +1

    Использовать стандартный протокол WebSocket (или даже WAMP) и стандартную сериализацию json/msgpack/protobuf по вкусу.
    Это упростит «добавление серверных библиотек для других языков программирования» и с авторизацией и шифрованием сразу понятно будет что делать.

    Про HTML уже написали (можно генерировать HTML код если так хочется писать только на c++)


  1. mmans
    14.09.2016 06:45

    кажется автор picoLisp сейчас доделывает нечто подобное — универсальный UI для Андроида, на серверной стороне лисп (работает на множестве posix-систем)


  1. tiamaton
    14.09.2016 10:00

    Господа, я покорно прошу прощения.

    Но этот подход уже 13 лет как используется мной в проекте Glan (Известен также как Kalpa.Cloud). ( http://www.slideshare.net/olegshalnev/kalpa-doklad )
    Система была (в рабочем виде) показана в 2005 году. В 2010 получила специальный приз за лучший свободный проект.
    glan много лет используется для разработки различных приложений, в том числе и зарубежными группами. Мой продукт был изначально кроссплатформенен и поддерживает все коммерчески значимые системы.

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

    Сейчас мой проект выведен из Open Source (по требованию партнеров) и используется как технологическая платформа для создания семейства коммерческих продуктов.

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


    1. savostin
      14.09.2016 10:36

      пардон не проснулся


    1. taco_attaco
      14.09.2016 13:20

      Спасибо за слова поддержки.
      Одной из мотиваций к написанию мной данной статьи была возможность получить информацию об уже существующих системах с аналогичным подходом.
      Философия вашего проекта, насколько я могу судить, схожа с моей — позволить пользователю максимально абстрагироваться от сетевой инфраструктуры и писать свою программу так, как будто это обычное десктопное приложение с UI.


      1. tiamaton
        14.09.2016 13:40

        Да. Именно потому я попытался максимально приблизить API к Qt, чтобы дать возможность разработчику максимально использовать свой опыт разработки standalone приложений в деле создания сетевых без переучивания и освоения новых технологий и новых подходов.

        Сторонюсь подхода QML(XUL) основанного на декларативном описании интерфейса. На стороне сервера такой подход возможен, но для генерации интерфейса необходимо использовать атомарные сигналы. Это позволяет крайне динамично и легко управлять сценой на стороне клиента. Интерфейс описанный в «жесткой форме» такой гибкости лишен.

        И конечно подход позволил работать не только с пользовательским интерфейсом, но взаимодействовать (например) с аппаратной частью стереотипно, в том числе и асинхронно.


        1. taco_attaco
          15.09.2016 11:27

          По части генерации интерфейса множеством атомарных сигналов, я опасался, что этот подход приведёт к тому, что будет тяжело на серверной стороне поддерживать актуальное представление состояния на клиенте. Что, в свою очередь, усложняет дальнейшие изменения сцены на клиенте.
          Не возникает ли у вас такая проблема?


          1. tiamaton
            15.09.2016 14:03

            Нет, не возникает. Никаких проблем с поддержание актуального представления.


            1. MacIn
              15.09.2016 16:15

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


    1. MacIn
      14.09.2016 17:27

      По сути это всё свои реализации xterm'а.

      У нас аналогичный подход используется с 2002-2004 года.
      Представляя полный объем работы и все сложности, которые встретит автор, могу только пожелать удачи и стойкости.


  1. bormental
    15.09.2016 10:38

    Спасибо, в общем подход понравился. Особенно сопряжение с C++ c помощью asio, который я очень часто использую.
    Из предложенных направлений дальнейшего развития я бы проголовал в первую очередь за:


    1. поддержка более специфичных функций клиентского устройства (notifications, sensors, volume buttons, e.t.c)
    2. более глубокая кастомизация внешнего вида элементов на клиенте (цвета, шрифты, стили)
    3. добавление новых UI элементов (drop-down boxes, images, e.t.c)

    Кроме того, думаю, что необходима аутентификация не сервере. Да и использование TLS соединения не помешало бы (на asio это легко реализуемо).


    Что не понравилось, это "ручное" создание layout к коде на сервере. Лично для меня было бы идеальным использование визуального конструктора наподобие Glade. Руками на С++ я бы разве что binding-у с контролами согласился бы помочь только.


    1. taco_attaco
      15.09.2016 11:39

      По-моему, основная проблема с «ручным» созданием layout — отсутствие документации. Я надеюсь, что после её добавления, будет проще в этом разобраться. Конечно, графический конструктор более интуитивен, но пока что для меня он не в приоритете.

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


      1. bormental
        15.09.2016 16:50

        Не могу говорить за всех, но для меня лично "ручное" описание layout — самая скучная и неприятная операция независимо от наличия документации.


        И я не предлагаю Вам создавать граф-й редактор с нуля, но, возможно, стоит использовать результаты Glade, разумеется с какими-то ограничениями. Там довольно простой XML на выходе… Возможно, если Ваш проект будет востребован, кто-то другой напишет такой загрузчик layout из внешнего файла.