1i7 (1i7) wrote,
1i7
1i7

Categories:

Сервер Роботов: управление смартфоном Android из облака

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


Сервер Роботов: управление смартфоном Андроид Ётафон из облака from 1i7 on Vimeo.

Сервер Роботов запущен в облаке (виртуальная машина Амазон) по адресу robotc.lasto4ka.su:1116 и слушает подключения. Робот Клиент (смартфон Андроид Ётафон) подключается к Серверу Роботов через интернет и переходит в режим приема команд: включить или выключить условную лампочку. Пользователь отправляет команды подключенному смартфону через интерфейс командной строки, запущенный на удаленной системе в оболочке ssh. Смартфон принимает и распознает команду, "включает" или "выключает" "лампочку" и отправляет ответ.

Робот Клиент на плате WF32 и Робот Клиент на Андроиде существуют независимо друг от друга и могут подключаться к одному и тому же Серверу Роботов по очереди.



Предварительные приготовления

1) Настроить систему для разработки приложений Android

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

1.1) Установить среду разработки приложений для операционной системы Google Android - Android SDK на developer.android.com на основе среды Eclipse (скачать для Linux, Mac, Windows).

1.2) Настроить систему для разработки на настоящем смартфоне или планшете Android (в SDK есть эмулятор, но он довольно медленный, и с реальной железкой работать сильно приятнее).

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

1. Откройте меню Настройки
2. Перейдите к разделу «Система», который расположен в самом низу
3. Тапните по пункту «О планшете» (или «О телефоне»)
4. Найдите в самом низу пункт «Номер сборки»
5. Нажмите по нему примерно 10 раз, после чего откроется меню параметров разработчика


- В появившемся меню "Настройки/Для разрабочиков" включить галочку "Отладка по USB"

- Настроить операционную систему для работы с устройством Android в режиме ADB (Android debug bridge - мост отладки Андроид).

В Windows для этого потребуется установить специальный ADB-драйвер, который нужно искать у производителя устройства. Для разных устройств этот драйвер будет разный, поэтому настройка работы устройств Android от разных производителей для разработки в Windows - это каждый раз немного разная история.

Для примера - инструкции по установке драйвера ADB для Yotaphone: официально на 4pda, официально на сайте developer.yotaphone.com.

В Linux ADB драйвер устанавливать не требуется - он уже включен в ядро с незапамятных времен, любые устройства Android появляются в системе, как устройство /dev/ttyUSBX, и готовы к отладке с adb. Однако может потребоваться настроить права доступа к устройству для текущего пользователя: инструкции могут отличаться от системы к системе. Может потребоваться включить пользователя в специальную группу, имеющую права записи во вновь создаваемые устройства USB; в крайнем случае можно запустить сервис adb (входит в состав Android SDK) от имени системного администратора (рута root): "sudo adb start-server" (если сервер уже запущен например Эклипсом, то предварительно выполнить "sudo adb kill-server").

Для Ётафона по какой-то причине еще требуется немного пошаманить с ~/.android/adb_usb.ini, это уже какие-то новшевства судя по всему от всё тех разработчиков андроида.

В Mac не пробовал, скорее всего то же, что в Linux, но без заморочки с правами.

2) Запустить Сервер Роботов

Запустить Сервер Роботов в виртуальной машине Амазон или любом другом облаке.

Исходники

1) Сервер Роботов и Робот Клиент на Java: chipkit-cloud-wifi/JavaTcpServerMaster
2) Робот Клиент на ChipKIT WF32: chipkit-cloud-wifi/chipkit_tcp_client_slave
3) Робот Клиент на Android: chipkit-cloud-wifi/AndroidTcpClientSlave

Протокол
Протокол общения между Сервером Роботов и Роботом Клиентом (плата, смартфон или что-то еще): сервер отправляет команду, клиент выполняет команду и присылает ответ; команды и ответы строковые.

Весь полезный функционал определим двумя основными командами:
ledon - включить лампочку;
ledoff - выключить лампочку.

Ответы от клиента:
ok - в случае успеха выполнения команды;
dontunderstand - команда не распознана.


Подчинённый клиент на Android

Т.к. наш Робот Клиент после подключения к Серверу Роботов переходит в пассивный режим ожидания команд от сервера, будем называть такого Робота Клиента подчинённым клиентом (в английских терминах Slave - раб), а Сервер Роботов - управляющим сервером (англ. Master - хозяин).

Создаём новый проект приложения для Android: chipkit-cloud-wifi/AndroidTcpClientSlave.

В манифесте сразу разрешить приложению выходить в интернет:

AndroidTcpClientSlave/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="edu.nntu.robotserver"
    android:versionCode="1"
    android:versionName="1.0" >

    ...

    <uses-permission android:name="android.permission.INTERNET"/>

    ...

</manifest>



Дальше весь рабочий код в классе экрана edu/nntu/robotserver/RobotClientActivity.java

Подключаем необходимые библиотеки:


package edu.nntu.robotserver;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

public class RobotClientActivity extends Activity {


Константы для протокола:


    public static final String CMD_LEDON = "ledon";
    public static final String CMD_LEDOFF = "ledoff";
    public static final String REPLY_OK = "ok";
    public static final String REPLY_DONTUNDERSTAND = "dontunderstand";


Информация для подключения к Серверу Роботов:


    public static final String DEFAULT_SERVER_HOST = "robotc.lasto4ka.su";
    public static final int DEFAULT_SERVER_PORT = 1116;


Определим состояния приложения в смысле подключения к Серверу Роботов: отключено, подключается, подключено, ошибка подключения:


    private enum ConnectionStatus {
        DISCONNECTED, CONNECTING, CONNECTED, ERROR
    }


Всякие переменные: сокет и потоки ввода-вывода, статус подключения, информация о подключении и сообщение последней ошибки.


    private Socket socket;
    private OutputStream serverOut;
    private InputStream serverIn;

    private ConnectionStatus connectionStatus = ConnectionStatus.DISCONNECTED;
    private String connectionInfo;
    private String connectionErrorMessage;



Подключение к Серверу Роботов в методе connectToServer:

- Передаем имя хоста и порт;

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

- Создаём сокет, открываем потоки ввода-вывода;

- Запускаем процедуру чтения данных с управляющего сервера startServerInputReader().


    /**
     * Подлключиться к серверу и запустить процесс чтения данных.
     */
    private void connectToServer(final String serverHost, final int serverPort) {
        // Все сетевые операции нужно делать в фоновом потоке, чтобы не
        // блокировать интерфейс
        new Thread() {
            @Override
            public void run() {
                try {
                    debug("Connecting to server: " + serverHost + ":"
                            + serverPort + "...");
                    setConnectionStatus(ConnectionStatus.CONNECTING);

                    socket = new Socket(serverHost, serverPort);
                    serverOut = socket.getOutputStream();
                    serverIn = socket.getInputStream();

                    debug("Connected");
                    connectionInfo = socket.getInetAddress().getHostName()
                            + ":" + socket.getPort();
                    setConnectionStatus(ConnectionStatus.CONNECTED);

                    // Подключились к серверу, запустим второй поток
                    // чтения данных
                    startServerInputReader();
                } catch (final Exception e) {
                    socket = null;
                    serverOut = null;
                    serverIn = null;

                    debug("Error connecting to server: " + e.getMessage());
                    setConnectionStatus(ConnectionStatus.ERROR);
                    connectionErrorMessage = e.getMessage();

                    e.printStackTrace();
                }
            }
        }.start();
    }



Процедура чтения данных с Сервера Роботов: ждем данные из потока ввода InputStream serverIn до тех пор, пока сервер не пришлет команду. Получили команду - распознаем и выполняем в методе handleInput, отправляем ответ на сервер через поток вывода OutputStream serverOut. Ждем следующую команду и так бесконечно по кругу, пока соединение не будет разорвано.


    /**
     * Фоновый поток чтения данных с сервера - постоянно ждем команду, когда
     * приходит - выполняем, шлем ответ и начинаем ждать следующую команду.
     */
    private void startServerInputReader() {
        new Thread() {
            @Override
            public void run() {
                final byte[] readBuffer = new byte[256];
                int readSize;

                String inputLine;
                String reply;

                try {
                    // Читаем данные
                    while ((readSize = serverIn.read(readBuffer)) != -1) {
                        inputLine = new String(readBuffer, 0, readSize);
                        debug("Read: " + inputLine);
                        reply = handleInput(inputLine);
                        // Пишем ответ
                        if (reply != null && reply.length() > 0) {
                            debug("Write: " + reply);
                            serverOut.write((reply).getBytes());
                            // сбросим данные через сокет прямо сейчас, иначе
                            // они могут быть отправленны позже, пока
                            // реализация
                            // OutputStream не решит это сделать по своему
                            // усмотрению (заполнится буфер, пройдет
                            // определенный таймаут и т.п.)
                            serverOut.flush();
                        }
                    }
                } catch (final Exception e) {
                    debug("Server read error: " + e.getMessage());
                    e.printStackTrace();
                }
                debug("Server input reader thread finish");
                disconnectFromServer();
            }
        }.start();
    }



Обработка команды, пришедшей с сервера: если команда корректная ledon (включить лампочку) или ledoff (выключить лампочку), "включаем" или "выключаем" лампочку, отправляем ответ ok. На неизвестную команду отправляем dontunderstand.

Так как чтение команды происходит в фоновом потоке, а "включение" и "выключение" лампочки просто меняет картинки на экране, используем механизм Handler.post, который специально предназначен для того, чтобы влиять на интерфейс из фонового потока.


    /**
     * Отработать входные данные и при необходимости выполнить команду.
     *
     * @param cmd
     * @return
     */
    private String handleInput(String cmd) {
        final String reply;
        if (CMD_LEDON.equals(cmd)) {
            debug("Command 'ledon': turn light on");

            // Здесь мы могли бы включить лампочку, если бы она была
            handler.post(new Runnable() {
                @Override
                public void run() {
                    setLedStatus(true);
                }
            });

            reply = REPLY_OK;
        } else if (CMD_LEDOFF.equals(cmd)) {
            debug("Command 'ledoff': turn light off");

            // Здесь мы могли бы выключить лампочку, если бы она была
            handler.post(new Runnable() {
                @Override
                public void run() {
                    setLedStatus(false);
                }
            });

            reply = REPLY_OK;
        } else {
            debug("Unknown command: " + cmd);

            reply = REPLY_DONTUNDERSTAND;
        }
        return reply;
    }


Симулируем включение и выключение лампочки сменой картинок в ImageView:


    /**
     * Включить или выключить светодиод (лампочку). Клипарты светодиодов:
     * http://openclipart.org/search/?query=led+rounded+h
     * http://openclipart.org/detail/28085/led-rounded-h-yellow-by-anonymous
     * http://openclipart.org/detail/28084/led-rounded-h-red-by-anonymous
     * http://openclipart.org/detail/28083/led-rounded-h-purple-by-anonymous
     * http://openclipart.org/detail/28082/led-rounded-h-orange-by-anonymous
     * http://openclipart.org/detail/28081/led-rounded-h-grey-by-anonymous
     * http://openclipart.org/detail/28080/led-rounded-h-green-by-anonymous
     * http://openclipart.org/detail/28079/led-rounded-h-blue-by-anonymous
     * http://openclipart.org/detail/28078/led-rounded-h-black-by-anonymous
     *
     * @param on
     */
    private void setLedStatus(boolean on) {
        if (on) {
            // зажечь лампочку
            txtLed1.setText("led: ON");
            imgLed1.setImageResource(R.drawable.led1_on);
        } else {
            // потушить лампочку
            txtLed1.setText("led: OFF");
            imgLed1.setImageResource(R.drawable.led1_off);
        }
    }



Обновим состояние интерфейса в зависимости от текущего статуса подключения (например, если мы отключены, появляется кнопка "Подключиться", если подключены, то она пропадает):


    /**
     * Обновить элементы управления в зависимости от текущего состояния.
     */
    private void updateViews() {

        switch (connectionStatus) {
        case DISCONNECTED:
            txtStatus.setText(R.string.status_disconnected);

            btnConnect.setVisibility(View.VISIBLE);
            btnConnect.setEnabled(true);

            break;
        case CONNECTED:
            txtStatus.setText(getString(R.string.status_connected) + ": "
                    + connectionInfo);

            btnConnect.setVisibility(View.GONE);
            btnConnect.setEnabled(false);

            break;
        case CONNECTING:
            txtStatus.setText(R.string.status_connecting);

            btnConnect.setVisibility(View.VISIBLE);
            btnConnect.setEnabled(false);

            break;
        case ERROR:
            txtStatus.setText(getString(R.string.status_error) + ": "
                    + connectionErrorMessage);

            btnConnect.setVisibility(View.VISIBLE);
            btnConnect.setEnabled(true);

            break;
        default:
            break;
        }
    }


Отключиться от Сервера Роботов - закрыть все потоки и сокет, почистить переменные:


    /**
     * Отключиться от сервера - закрыть все потоки и сокет, обнулить переменные.
     */
    private void disconnectFromServer() {
        try {
            if (serverIn != null) {
                serverIn.close();
            }
            if (serverOut != null) {
                serverOut.close();
            }
            if (socket != null) {
                socket.close();
            }
        } catch (final IOException e) {
            e.printStackTrace();
        } finally {
            serverIn = null;
            serverOut = null;
            socket = null;

            debug("Disconnected");
            setConnectionStatus(ConnectionStatus.DISCONNECTED);
        }
    }



Подключаемся к Серверу Роботов, когда экран приложения выходит на первый план:


    @Override
    protected void onResume() {
        super.onResume();

        connectToServer(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
    }


Отключаемся, когда экран уходит с переднего плана (сворачивается или скрыт другим приложением):


    @Override
    protected void onPause() {
        super.onPause();

        disconnectFromServer();
    }



Подключаем Робота Клиента Андроид Ётафон к запущенному Серверу Роботов

Итак, Сервер Роботов запущен по адресу robotc.lasto4ka.su:1116 и готов принимать внешние входящие подключения через интернет.

konsole_ssh2_robotserver_start.png

Теперь подключим к нему Робота Клиента на смартфоне Андроид Ётафон - просто запускаем приложение Робот Tcp-Клиент и ждём, пока в приглашении Сервера Роботов не появится сообщение о подключившемся клиенте (на смартфоне должен быть интернет, что очевидно):

konsole_ssh3_robotserver_connected1.png


Робот Клиент подключился, вводим команды.

Включим лампочку: ledon, Enter:

konsole_ssh4_robotserver_ledon.png

Android_client1_ledon.png

server_android2_ledon.jpg


Выключим лампочку:  ledoff, Enter:

konsole_ssh5_robotserver_ledoff.png

Android_client2_ledoff.png

server_android3_ledoff.jpg


Избавимся от подключенного клиента: kick

konsole_ssh6_robotserver_kick.png

Android_client3_kick.png



исходники занятия, подсветка синтаксиса, картинки с лампочками.
Tags: android, облако, сервер роботов, типовые задачи
Subscribe

  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 0 comments