📜 ⬆️ ⬇️

Using Android device as a thin UI for C ++ programs

I want to share with the community a project that I have been quietly doing over the past few months.

Foreword


There are often situations when you want to manage your program from a phone or tablet, but it is impractical (or not possible) to write a separate application for this on the phone (too much effort for this project, no development experience for android, etc.) ). This situation periodically arose in me, and in the end I decided to deal with this problem once and for all. The result is a system, the architecture and use of which are described in this article.

purpose


Creating a system that allows you to implement a plug-in UI based on android devices in C ++ programs. At the same time, we wanted to minimize the dependencies of custom C ++ code on third-party libraries, as well as to abstract it from the data transfer protocol. The system should consist of two parts: C ++ libraries and android applications.

System architecture


The system has a client-server architecture, where clients are android devices, the server is a user program. Communication between them is carried out using TCP / IP sockets. To implement the communication protocol, the TAU library was created.
')
The main tasks for which the library is responsible:


The library consists of the following namespaces:


Using. Example # 1 - hello, world


We'll start the demonstration of using the library with the simplest example, in which a welcome message will be displayed on the screen. Here is what the custom code will look like in our case:

Hidden text
#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; } 

The main class that contains all user logic that interacts with the client device ( MyEventsDispatcher ) must be inherited from tau :: util :: BasicEventsDispatcher . It overrides 2 methods from the base class: onClientConnected () and packetReceived_clientDeviceInfo () . The first is called when the client connects. The second method will be executed when the client device information comes after the connection (the first packet after connection is sent by the client).

In our case, the first method is trivial - it only displays an informational message on the console. In the second method, the server sends a layout to the client — data on which interface should be displayed on the client.

All the code responsible for transferring data over the network is in main (). In this case, boost :: asio is used to implement the communication. In the tau :: util namespace there are corresponding abstractions, which makes this example as compact as possible. Using boost is optional - any implementation of TCP / IP sockets can be fairly easily used with a library.

Compilation


As an example, we will use g ++ for compilation. In our case, the command will be as follows:

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

As you can see, the compiler is passed several additional parameters:


This set of options is the most common version of the assembly, which will allow the library to be included in any project with minimal effort.

You can get rid of them all if you want. If you use the library within the project, you do not need to specify -I $ LIBRARY_LOCATION and -D TAU_HEADERONLY . For compilers that are compatible with C ++ 11, the -D TAU_CPP_03_COMPATIBILITY option is not needed. Dependence on boost :: asio has only the network part, which can be easily rewritten without dependencies.

After compiling and running, the server starts listening on port 12345.

We start the client on the phone, create a connection and connect to it to display a message. This is how it will look like (I started the server on a remote computer via PuTTY, and the client was started in the emulator):

Creating a server connection

This example does not provide for the transmission and receipt of additional notifications between the client and the server, so let's move on to the following example.

Example 2 - a more detailed demonstration of the system's capabilities


In this example, we will add several different elements in our server, learn how to receive notifications from them, change their state and switch pages.

The server code will look like this:

Hidden text
 #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; } 

All changes from the previous example were made in the class MyEventsDispatcher . The following event handler methods from the client have been added:


In addition, the layout sent to the client upon connection has changed accordingly.

Since we have a demo, the code in the handlers is as simple as possible - dumping event information into the console, as well as sending various commands to the client.

All commands will be sent to the client from the handler for pressing the packetReceived_buttonClick () buttons (of course, this is not necessarily done there, but it’s easier and clearer).

Each command corresponds to a packet transmitted from the server to the client. Formation and sending of these packets occurs when calling special methods defined in BasicEventsDispatcher :


Here is how this example works:

Demonstration of working with UI elements

As you can see, for each action on the client device, the corresponding code is executed on the server. When the values ​​of the user input elements change, the server receives a notification about the new value of the variable in this element. In the button press handler, various packages are sent from the server to the client (the type of package depends on which button was pressed). This example also shows how page switching works. Breaking the layout into pages allows you to group elements according to their functions. On the client screen, only one of the pages is always displayed, which reduces the load on the interface.

Example 3 - something useful


The last example today is the partial implementation of one of the tasks for which I started this project. This is the simplest keyboard input emulator for windows (uses the SendInput () winapi function).

The code for this example is on my githaba . I will not give it here - it does not demonstrate anything new in the use of the library compared to the second example. Here I will give only a demo of his work:

Keyboard Emulation

The code of this example is easy to expand to more complex tasks of emulating keyboard input.

Epilogue


Instead of a conclusion, I want to appeal to the community. Do you need a similar system? Do I invent another bicycle? What needs to be described in more detail? In which direction do you need to develop further?

The following development paths occur to me now (they are almost completely orthogonal, so I would like to prioritize):


In addition, I will be glad to hear criticism about the architecture and implementation of the library in the state in which it is now.

References:

Source: https://habr.com/ru/post/309882/


All Articles