1i7

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

Nov 16, 2014 04:05

В прошлый раз мы смогли помигать лампочкой из облака Амазон, но это происходило в ручном режиме с клавиатуры через приглашение 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 покажу в следующий раз.

исходники занятия, подсветка синтаксиса.

облако, типовые задачи, сервер роботов, chipkit

Previous post Next post
Up