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

Category:

Веб-интерфейс для Сервера Роботов: Сервер Роботов2

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


Сервер Роботов: управление роботом-с-лампочкой через веб from 1i7 on Vimeo.

Человек нажимает кнопку (включить лампочку) в браузере, кнопка (через Http-запрос с JavaScript) отправляет команду (включить лампочку) веб-приложению, веб-приложение переправляет команду (включить лампочку) Серверу Роботов, Сервер Роботов отправляет команду (включить лампочку) подключенному роботу, робот выполняет команду (включает лампочку) и отправляет ответ (ok) Серверу Роботов, Сервер Роботов переправляет ответ (ok) веб-приложению, веб-приложение возвращает ответ (ok) в браузер, JavaScript в браузере отображает ответ (рисует картинку с включенной лампочкой), человек видит результат нажатия кнопки (картинку с включенной лампочкой).

Общий смысл примерно такой, плюс некоторые нюансы и дополнения.

Сервер Роботов и подключающиеся к нему роботы (плата ChipKIT со светодиодом и приложение для Андроида) у нас уже есть. Осталось добавить веб-приложение, а также немного доработать Сервер Роботов и подключающихся к нему роботов так, чтобы они смогли работать внутри новой цепочки.

Веб-приложение напишем на языке Scala на базе набора инструментов (фреймфорка) для разработки веб-приложений Unfiltered. При желании его можно легко заменить на любой другой любимый движок для веб-приложений на любом другом языке программирования.

Финальный результат онлайн: http://robotc.lasto4ka.su/
Еще финальный результат (+веб-камера с творчески доработанной инсталляцией, в базовую инструкцию не включены): В глубинах океана

Предварительные приготовления
Виртуальный хостинг на Амазоне
Сервер Роботов: запуск управляющего сервера на Java в облаке Amazon
Сервер Роботов: управление платой ChipKIT WF32 из облака
Сервер Роботов: управление смартфоном Android из облака

плюс
Робот Машинка на Сервере Роботов (без веб-интерфейса)

Исходники
1) Управляющее веб-приложение: chipkit-cloud-wifi/ScalaUnfilteredWebFrontend
2) Сервер Роботов2: chipkit-cloud-wifi/JavaTcpServerMaster/src/main/java/edu/nntu/robotserver/RobotServer2.java
3.1) Робот с лампочкой на ChipKIT: chipkit-cloud-wifi/chipkit_tcp_client_slave2/chipkit_tcp_client_slave2.pde
3.2) Робот с лампочкой на Android: chipkit-cloud-wifi/AndroidTcpClientSlave
3.3) Условный робот на Java: chipkit-cloud-wifi/JavaTcpServerMaster/src/main/java/edu/nntu/robotserver/RobotClient2.java



План работы

Обновленный Сервер Роботов2 принимает подключения от робота на порту 1116 и подключения от управляющего веб-приложения на порту 1117 (в предыдущей версии он также ждал робота на 1116, но примал команды от пользователя напрямую через приглашение консоли).

Веб-приложение показывает пользователю страницу HTML с 2мя кнопками: "Включить лампочку" и "Выключить лампочку". Нажатия на кнопки обрабатываются при помощи JavaScript (чтобы не перезагружать страницу) и отправляют Http-запрос с нужной командой на сервер веб-приложению. Веб-приложение через Сервер Роботов выполняет команду на роботе, получает результат, возвращает результат странице в ответе на запрос Http. JavaScript получает результат запроса Http и рисует нужный статус на странице, динамически правя ее код HTML.

Также JavaScript на странице в фоновом режиме по таймеру (каждую секунду) постоянно опрашивает робота о его текущем статусе: подключен или не подключен; если подключен, включена или выключена лампочка. Статус динамически отображается на странице опять при помощи JavaScript (рисуется нужная картинка и текст). Постоянный опрос в нашем случае позволяет отображать актуальный статус робота одновременно на нескольких устройствах, где загружена страница с пультом управления. Но он также может быть полезен в том случае, если робот по какой-то причине решит включить или выключить лампочку самостоятельно.

В нашем случае система организована таким образом, что робот не может самостоятельно отправлять сообщения управляющему пульту, он только присылает ответы на запросы.

Замечание: более компактным решением также может быть замена Http-запросов JavaScript, которые попадают к Серверу Роботов через веб-приложение, на прямое взамодействие страницы с Сервером Роботов при помощи веб-сокетов HTML5.

Прошивки для управляемых роботов

К прошивкам для управляемых роботов (платы ChipKIT и приложения Андроид) добавим две команды:
ping - проверить статус робота (работает/не работает), возвращает ok
ledstatus - узнать статус лампочки: возвращает on (лампочка включена) или off (лампочка выключена).

Код почти идентичен предыдущему, добавления для новых команд можно посмотреть самостоятельно:
ChipKIT (отдельная обновленная прошивка): chipkit-cloud-wifi/chipkit_tcp_client_slave2/chipkit_tcp_client_slave2.pde
Android (новые команды добавлены сразу в оригинальный проект): chipkit-cloud-wifi/AndroidTcpClientSlave/src/edu/nntu/robotserver/RobotClientActivity.java

Старые версии прошивок также будут работать, просто в ответ на новые команды ping и ledstatus робот будет возвращать dontuderstand, отображение актуального статуса лампочки работать НЕ будет, но управление лампочкой работать БУДЕТ.

Сервер Роботов2

Весь код в одном файле, детали разобрать самостоятельно.

JavaTcpServerMaster/src/main/java/edu/nntu/robotserver/RobotServer2.java

package edu.nntu.robotserver;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author benderamp
 */
public class RobotServer2 {

    // Локальные команды для сервера
    public static final String SCMD_KICK = "kick";

    // Ответы от сервера управляющему интерфейсу
    public static final String SREPLY_OK = "rs:ok";
    public static final String SREPLY_DISCONNECTED = "rs:disconnected";

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

    // Сокет для подключение роботов
    private int serverPort;
    private ServerSocket serverSocket;

    private InputStream clientIn;
    private OutputStream clientOut;

    private boolean robotIsConnected = false;

    // Сокет для подключения управляющего интерфейса
    private int frontendPort;
    private ServerSocket frontendSocket;

    public RobotServer2() {
        this(DEFAULT_SERVER_PORT, DEFAULT_FRONTEND_PORT);
    }

    public RobotServer2(int serverPort, int frontendPort) {
        this.serverPort = serverPort;
        this.frontendPort = frontendPort;
    }

    /**
     * Запускает сервер слушать входящие подключения от роботов на указанном
     * порте serverPort.
     *
     * Простой однопоточный сервер - ждет ввод от пользователя, отправляет
     * введенную команду клиенту, ждет ответ и дальше по кругу.
     *
     * Сбросить подключенного клиента - ввести локальную команду 'kick'.
     *
     * @throws java.io.IOException
     */
    public void startServer() throws IOException {
        System.out.println("Starting Robot Server on port " + serverPort + "...");
        // Открыли сокет
        serverSocket = new ServerSocket(serverPort);

        Socket clientSocket = null;
        while (true) {
            try {
                System.out.println("Waiting for robot...");
                // Ждём подключения клиента (робота)
                clientSocket = serverSocket.accept();
                System.out.println("Robot accepted: " + clientSocket.getInetAddress().getHostAddress());

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

                // Получаем доступ к потокам ввода/вывода сокета для общения 
                // с подключившимся клиентом (роботом)
                clientIn = clientSocket.getInputStream();
                clientOut = clientSocket.getOutputStream();

                // робот подключен
                robotIsConnected = true;

                // Висим здесь все время, пока подключен робот
                while (robotIsConnected) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ex) {
                        Logger.getLogger(RobotServer2.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            } catch (IOException ex2) {
                // Попадём сюда только после того, как клиент отключится и сервер
                // попробует отправить ему любую команду 
                // (в более правильной реализации можно добавить в протокол 
                // команду проверки статуса клиента 'isalive' и отправлять её 
                // клиенту с некоторой периодичностью).
                System.out.println("Robot disconnected: " + ex2.getMessage());
            } finally {
                // закрыть неактуальный сокет
                if (clientIn != null) {
                    clientIn.close();
                }
                if (clientOut != null) {
                    clientOut.close();
                }
                if (clientSocket != null) {
                    clientSocket.close();
                }
            }
        }
    }

    public void startFrontendInterface() throws IOException {
        System.out.println("Starting frontend interface on port " + frontendPort + "...");
        // Открыли сокет
        frontendSocket = new ServerSocket(frontendPort);

        Socket frontendClientSocket = null;
        InputStream frontendIn = null;
        OutputStream frontendOut = null;
        while (true) {
            try {
                System.out.println("Waiting for frontend...");
                // Ждём подключения управляющего интерфейса
                frontendClientSocket = frontendSocket.accept();
                System.out.println("Frontend accepted: " 
                    + frontendClientSocket.getInetAddress().getHostAddress());

                frontendClientSocket.setSoTimeout(CLIENT_SO_TIMEOUT);

                // Получаем доступ к потокам ввода/вывода сокета для общения 
                // с подключившимся управляющим интерфейсом
                frontendIn = frontendClientSocket.getInputStream();
                frontendOut = frontendClientSocket.getOutputStream();

                // Команда от управляющего интерфейса
                final byte[] readBuffer = new byte[256];
                int readSize;
                String inputLine;

                // Ждем команду от управляющего интерфейса              
                while ((readSize = frontendIn.read(readBuffer)) != -1) {
                    // Ответ управляющему интерфейсу
                    String reply = "";

                    // Превратим байты в строку
                    inputLine = new String(readBuffer, 0, readSize);
                    System.out.println("Frontend read: " + inputLine);

                    if (robotIsConnected) {
                        // Робот подключен - выполняем команду

                        if (SCMD_KICK.equals(inputLine)) {
                            // Локальная команда - отключить робота
                            robotIsConnected = false;
                            reply = SREPLY_DISCONNECTED;

                            System.out.println("Robot disconnected: KICK");
                        } else if (inputLine.length() > 0) {
                            // отправим команду роботу
                            try {
                                System.out.println("Robot write: " + inputLine);
                                clientOut.write((inputLine).getBytes());
                                clientOut.flush();

                                // и сразу прочитаем ответ
                                final byte[] robotReadBuffer = new byte[256];
                                final int robotReadSize = clientIn.read(robotReadBuffer);
                                if (robotReadSize != -1) {
                                    // ответ от робота
                                    reply = new String(robotReadBuffer, 0, robotReadSize);
                                    System.out.println("Robot read: " + reply);
                                } else {
                                    // Соединение разорвано.
                                    // Такая ситуация проявляется например при связи
                                    // Server:OpenJDK - Client:Android - клиент отключается,
                                    // но сервер это не распознаёт: запись write завершается
                                    // без исключений, чтение read возвращается не дожидаясь
                                    // таймаута без исключения, но при этом возвращает -1.
                                    throw new IOException("End of stream");
                                }
                            } catch (IOException e) {
                                // в процессе обмена данными с роботом что-то пошло не так,
                                // будем ждать следующего робота
                                robotIsConnected = false;
                                reply = SREPLY_DISCONNECTED;
                            }
                        }
                    } else {
                        // робот не подключен
                        reply = SREPLY_DISCONNECTED;
                    }

                    // ответ управляющему интерфейсу
                    frontendOut.write(reply.getBytes());
                    frontendOut.flush();
                    System.out.println("Frontend write: " + reply);
                }
            } catch (SocketTimeoutException ex1) {
                System.out.println("Frontend disconnected: " + ex1.getMessage());
            } catch (IOException ex2) {
                System.out.println("Frontend disconnected: " + ex2.getMessage());
            } finally {
                // закрыть неактуальный сокет
                if (frontendIn != null) {
                    frontendIn.close();
                }
                if (frontendOut != null) {
                    frontendOut.close();
                }
                if (frontendClientSocket != null) {
                    frontendClientSocket.close();
                }
            }
        }
    }

    public static void main(String args[]) {
        final RobotServer2 server = new RobotServer2();
        new Thread() {
            @Override
            public void run() {
                try {
                    server.startServer();
                } catch (IOException ex) {
                    Logger.getLogger(RobotServer2.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                try {
                    server.startFrontendInterface();
                } catch (IOException ex) {
                    Logger.getLogger(RobotServer2.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }.start();
    }
}


Процедура запуска полностью аналогична предыдущей версии сервера с ручным управлением RobotServer1, только в самый последний момент указываем новое имя класса запускаемого сервера RobotServer2:

] java -cp JavaTcpServer-1.0-SNAPSHOT.jar edu.nntu.robotserver.RobotServer2

Как и предыдущая версия сервера, RobotServer2 ожидает подключения робота на порту 1116. Но, в отличии от RobotServer1, RobotServer2 не выводит приглашение для пользователя вводить команды вручную, а вместо этого ожидает параллельного подключения управляющего приложения на порту 1117. Управляющим приложением в нашем случае будет специальное веб-приложение, которое позволит пользователю отправлять команды роботу с любого компьютера, смартфона или планшета, подключенного к интернет, через графический интерфейс браузера.

Как написать и запустить управляющее веб-приложение на Scala+Unfiltered покажу в следующий раз.



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