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

Category:

Управление платой ChipKIT со смартфона Android через WiFi: пульт Android

Продолжаем управлять платой ChipKIT WF32 со смартфона Android напрямую через WiFi. В прошлый раз написали прошивку для управляемого робота - платы ChipKIT WF32. Сейчас сделаем пульт - напишем управляющее приложение для Android.

Управление платой ChipKIT WF32 со смартфона Android через WiFi from 1i7 on Vimeo.




Управляющий клиент на Android

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

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

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

RobotCarPult/AndroidManifest.xml


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

    ...

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

    ...

</manifest>



Весь рабочий код в одном классе экрана AndroidTcpClientMaster/src/edu/nntu/robotpult/RobotPultActivity.java


package edu.nntu.robotpult;

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.TextView;
import android.widget.Toast;

public class RobotPultActivity extends Activity {


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


    public static final String CMD_LEDON = "ledon";
    public static final String CMD_LEDOFF = "ledoff";
    public static final String CMD_PING = "ping";


Ip-адрес робота-платы и порт:


    public static final String DEFAULT_SERVER_HOST = "192.168.43.117";
    public static final int DEFAULT_SERVER_PORT = 44114;


Максимальное время ожидания ответа от сервера - если ответ на команду не получен за 5 секунд, считаем, что связь разорвана:


    /**
     * Таймаут для чтения ответа на команды - сервер должет прислать ответ на
     * запрос за 5 секунд, иначе он будет считаться отключенным.
     */
    private static final int SERVER_SO_TIMEOUT = 5000;



Максимальное время бездействия пользователя (5 секунд), по истечении которого пульт отправляет роботу команду ping, чтобы тот со своей стороны не разорвал соединение.


    /**
     * Максимальное время неактивности пользователя, если пользователь не
     * отправлял команды на сервер роботу 5 секунд, приложение само отправит
     * команду ping, чтобы держать подключение открытым.
     */
    private final long MAX_IDLE_TIMEOUT = 5000;



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


    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;



Обратные вызовы (кол-бэки) для получения оповещений о статусе выполнении команд:


    /**
     * Обратный вызов для получения результата выполения команды, добавленной в
     * очередь на выполнение в фоновом потоке.
     */
    private interface CommandListener {
        /**
         * Команды была выполнена на устройстве, получен ответ.
         *
         * @param cmd
         * выполненная команда
         * @param reply
         * ответ от устройства
         */
        void onCommandExecuted(final String cmd, final String reply);
    }



Команды на сервер будем отправлять одну за одной из фонового потока. В очереди может быть только одна команда; до тех пор, пока она не выполнена, другие команды не добавляются. Для помещения команды в очередь нужно устновить значение переменной nextCommand, для получения сообщений о ходе выполнения команды установить значение nextCommandListener.


    /**
     * "Очередь" команд для выполнения на сервере, состоящая из одного элемента.
     */
    private String nextCommand;
    private CommandListener nextCommandListener;



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

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

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

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

- Запускаем процедуру отправки команд на робота-плату startServerOutpuWriter().


    /**
     * Подлключиться к серверу и запустить процесс чтения данных.
     */
    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);

                    // Подключились к серверу:
                    // Установим таймаут для чтения ответа на команды -
                    // сервер должет прислать ответ за 5 секунд, иначе он будет
                    // считаться отключенным (в нашем случае это позволит
                    // предотвратить вероятные зависания на блокирующем read,
                    // когда например робот отключился до того, как прислал
                    // ответ и сокет не распрознал это как разрыв связи с
                    // выбросом IOException)
                    socket.setSoTimeout(SERVER_SO_TIMEOUT);

                    // Получаем доступ к потокам ввода/вывода сокета для общения
                    // с сервером (роботом)
                    serverOut = socket.getOutputStream();
                    serverIn = socket.getInputStream();

                    debug("Connected");
                    // TODO: разобраться, почему на этой строке может подвисать
                    connectionInfo = socket.getInetAddress().getHostName()
                            + ":" + socket.getPort();
                    debug("Connection info: " + connectionInfo);
                    setConnectionStatus(ConnectionStatus.CONNECTED);

                    // Подключились к серверу, теперь можно отправлять команды
                    startServerOutputWriter();
                } 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();
    }



Фоновый поток отправки команд роботу. Берем команду из переменной nextCommand, отправляем на сервер, получаем ответ, отчитываемся о выполнении с nextCommandListener.onCommandExcecuted, разрешаем установить значение новой команды в nextCommand.

Здесь же автоматически добавляем к отправке команду ping, если пользователь сам ничего не нажимал продолжительное время (MAX_IDLE_TIMEOUT) 5 секунд.


    /**
     * Фоновый поток отправки данных на сервер: получаем команду от пользователя
     * (в переменной nextCommand), отправляем на сервер, ждем ответ, получаем
     * ответ, сообщаем о результате, ждем следующую команду от пользователя.
     */
    private void startServerOutputWriter() {
        new Thread() {
            @Override
            public void run() {
                try {
                    long lastCmdTime = System.currentTimeMillis();
                    while (true) {

                        String execCommand;
                        if (nextCommand != null) {
                            execCommand = nextCommand;
                        } else if (System.currentTimeMillis() - lastCmdTime > MAX_IDLE_TIMEOUT) {
                            execCommand = CMD_PING;
                        } else {
                            execCommand = null;
                        }

                        if (execCommand != null) {

                            // отправить команду на сервер
                            debug("Write: " + execCommand);
                            serverOut.write((execCommand).getBytes());
                            serverOut.flush();

                            // и сразу прочитать ответ
                            final byte[] readBuffer = new byte[256];
                            final int readSize = serverIn.read(readBuffer);
                            if (readSize != -1) {
                                final String reply = new String(readBuffer, 0,
                                        readSize);
                                debug("Read: " + "num bytes=" + readSize
                                        + ", value=" + reply);
                                if (nextCommandListener != null) {
                                    nextCommandListener.onCommandExecuted(
                                            execCommand, reply);
                                }
                            } else {
                                throw new IOException("End of stream");
                            }

                            // очистим "очередь" - можно добавлять следующую
                            // команду.
                            nextCommand = null;
                            nextCommandListener = null;

                            lastCmdTime = System.currentTimeMillis();
                        } else {
                            // на всякий случай - не будем напрягать систему
                            // холостыми циклами
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                            }
                        }
                    }
                } catch (final Exception e) {
                    debug("Connection error: " + e.getMessage());
                    e.printStackTrace();
                }
                debug("Server output writer thread finish");
                disconnectFromServer();
            }
        }.start();
    }


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


    /**
     * Поставить комнаду в очередь для выполнения на сервере. При переполнении
     * очереди новые команды игнорируются. (в простой реализации в очереди может
     * быть всего один элемент).
     *
     * @param cmd
     * @param cmdListener
     * @return true, если команда успешно добавлена в очередь false, если
     * очередь переполнена и команда не может быть добавлена.
     */
    private boolean sendCommand(final String cmd,
            final CommandListener cmdListener) {
        if (nextCommand == null) {
            nextCommand = cmd;
            this.nextCommandListener = cmdListener;
            return true;
        } else {
            return false;
        }
    }



Установить статус подключения - задать статус и обновить интерфейс:


    private void setConnectionStatus(final ConnectionStatus status) {
        System.out.println("setConnectionStatus: " + status);
        this.connectionStatus = status;
        handler.post(new Runnable() {
            @Override
            public void run() {
                updateViews();
            }
        });
    }


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


    /**
     * Обновить элементы управления в зависимости от текущего состояния.
     */
    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;
        }

        if (ConnectionStatus.CONNECTED.equals(connectionStatus)) {
            btnCmdLedOn.setEnabled(true);
            btnCmdLedOff.setEnabled(true);
        } else {
            btnCmdLedOn.setEnabled(false);
            btnCmdLedOff.setEnabled(false);
        }
    }



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


    /**
     * Отключиться от сервера - закрыть все потоки и сокет, обнулить переменные.
     */
    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;

            // очистить "очередь" команд
            nextCommand = null;
            nextCommandListener = 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();
    }




Подключаем пульт на Андроид Ётафон к роботу на плате ChipKIT WF32

Загружаем в плату ChipKIT WF32 прошивку и ждем некоторое время (может быть секунд 15-20), пока она не подключится к точке доступа WiFi (точку доступа WiFi для платы нужно тоже не забыть включить).

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

ChipKIT WF32-TcpServer-Android-connected.jpg

Пульт Android-connected.png

Пульт подключен к роботу, отправляем команды.


Включим лампочку: нажмем кнопку "Включить лампочку", к роботу уйдет команда ledon, светодиод загорится:

ChipKIT WF32-TcpServer-Android-ledon.jpg

Пульт Android-ledon.png


Выключим лампочку: нажмем кнопку "Выключить лампочку", к роботу уйдет команда ledoff, светодиод погаснет:

ChipKIT WF32-TcpServer-Android-ledoff.jpg

Пульт Android-ledoff.png


Подождем ничего не нажимая - убедимся, что в области отладочных сообщений каждые 5 секунд начнут появятся сообщения об отправке команды ping:

Пульт Android-ping.png



исходники занятия, подсветка синтаксиса.
Tags: android, chipkit, типовые задачи
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.
  • 3 comments