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

Веб-интерфейс для Сервера Роботов: веб-приложение на Scala+Unfiltered

Продолжаем делать веб-интерфейс для Сервера Роботов.

В прошлый раз мы доработали управляющий сервер (Сервер Роботов2) так, чтобы он научился принимать команды для подключенных к нему роботов программно через отдельный сокет, а не через консольное приглашение для ввода с клавиатуры, как было сделано в первой версии. Мы также посмотрели, как можно писать веб-приложения на языке Scala с использованием фреймворка Unfiltered и запускать его в облаке. Сейчас напишем веб-приложение на Scala+Unfiltered, которое позволит пользователю через интернет включать и выключать лампочку на плате ChipKIT WF32, подключенной к Серверу Роботов, нажимая кнопки в браузере.


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

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

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

Исходники
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



Веб-приложение на Scala+Unfiltered с HTML+JavaScript

Весь код находится в двух файлах:
- Серверная часть на Scala+Unfiltered: RobotCloudWeb1.scala
- Клиентская часть HTML+JavaScript: index.html (бонусом к HTML файл со стилями CSS site.css, отвечающий за внешний вид)

Серверная часть на Scala+Unfiltered

Структура веб-приложения:
- / (корень сайта): страница управления лампочкой
- /cmd/command: отправить команду подключенному роботу, command - произвольная строка, имя команды; возвращает ответ от робота.

дополнительные публичные ресурсы:
/css/site.css
/img/* - разные картинки в каталоге

Традиционное начало - пэкэдж, импорты и имя главного класса.

ScalaUnfilteredWebFrontend/src/main/scala/edu/nntu/robotcloud/RobotCloudWeb1.scala

package edu.nntu.robotcloud

import unfiltered.request.Path
import unfiltered.request.Seg
import unfiltered.response.Ok
import unfiltered.response.HtmlContent
import unfiltered.response.PlainTextContent
import unfiltered.response.ResponseString

/**
 * Веб-интерфейс для Сервера Роботов на Scala+Unfiltered
 * http://unfiltered.databinder.net/Try+Unfiltered.html
 *
 * с нормальной поддержкой UTF-8 в строке запроса и в теле ответа.
 *
 */
object RobotCloudWeb1 {


Фильтр для http-запросов, определяет структуру веб-приложения.


  val handlePath = unfiltered.filter.Planify {


Главная и единственная страница сайта - загружаем из ресурсов index.html.


    // HTML-страницы
    // начальная страница
    case Path(Seg(Nil)) =>
      Ok ~> HtmlContent ~> ResponseString(
        scala.io.Source.fromInputStream(getClass.getResourceAsStream("/html/index.html"), "UTF-8").mkString)


Из сегмента /cmd/command выбираем значение command, отправляем на Сервер Роботов через сокет на порт 1117 (её там ждет обновленный Сервер Роботов2), ждём ответ и возвращаем его как результат в виде обычного текста.

Если Сервер Роботов запущен и к нему подключен робот, он перешлёт команду роботу, дождётся ответ и перешлёт его веб-приложению.
Если Сервер Роботов запущен, но к нему не подключен робот, он вернет ответ "rs:disconnected" (rs - сокращение от robot server, значит, что ответ пришел от Сервера Роботов).
Если Сервер Роботов не запущен (не получилось отправить команду на порт 1117), веб-приложение вернёт результат "rc:notstarted" (rc - сокращение от robot cloud, значит, что ответ пришел от веб-приложения).


    // Запросы к сервису: команды для Сервера Роботов
    case Path(Seg("cmd" :: command :: Nil)) =>
      var reply = ""
      try {
        // подключимся к серверу роботов как управляющий интерфейс
        val socket = new java.net.Socket("localhost", 1117)
        val out = socket.getOutputStream()
        val in = socket.getInputStream()

        // отправим команду
        out.write(command.getBytes)
        out.flush
//        println("Write: " + command)

        // прочитаем ответ
        // так красиво не получится - mkString зависнет до тех пор,
        // пока не будет закрыт сокет
        //      var src = scala.io.Source.fromInputStream(in)
        //      reply = src.mkString

        val readBuffer = new Array[Byte](256);
        val readSize = in.read(readBuffer);
        if (readSize != -1) {
          reply = new String(readBuffer, 0, readSize);
        }
//        println("Read: " + reply)

        // закроем подключение
        socket.close
      } catch {
        case e: Exception =>
          reply = "rc:notstarted"
//          e.printStackTrace()
      }

      // покажем ответ
      Ok ~> PlainTextContent ~> ResponseString(reply)



Запускаем веб-приложение. Обратите внимание на сегмент .resources(getClass.getResource("/public")) в цепочке запуска http-сервера Jetty - он говорит веб-серверу о том, что все локальные ресурсы, доступные по этому адресу (в данном случае все файлы, которые находятся внутри jar-архива в каталоге "/public"), станут доступны публично наравне с определенными выше вручную сегментами в адресной строке веб-приложения.

Например файл стилей CSS site.css втури архива веб-приложения хранится как robotcloud-web-1.0-SNAPSHOT.jar/public/css/site.css, будет доступен как http://robotc.lasto4ka.su/css/site.css (при условии, что веб-приложение размещено по адресу http://robotc.lasto4ka.su). Аналогичная история со всеми картинками в каталоге robotcloud-web-1.0-SNAPSHOT.jar/public/img/ - они будут доступны по адресу http://robotc.lasto4ka.su/img/.

Замечание: в прошлый раз мы встраивали файлы стилей css и картинки в структуру веб-приложения в коде Scala вручную, в том числе самостоятельно выставляли значение ContentType (CssContent "text/css", JpegContent "image/jpeg" или PngContent "image/png"). Очевидно, что такой ручной подход был интересен чисто академически, на практике для доступа к статическим ресурсам удобнее использовать приведенный способ, особенно, если их много. Jetty сам найдет все файлы в указанном каталоге, проанализирует их тип и сообщит браузеру правильное значение ContentType.


  def main(args: Array[String]) {
    println("Starting Robot Cloud web frontend (on jetty http server)...")
    println("Resources dir: " + getClass.getResource("/public"))

    // Запустить веб-сервер
    unfiltered.jetty.Http.apply(8080).resources(getClass.getResource("/public")).filter(handlePath).run()
  }
}


Клиентская часть HTML+JavaScript

robotc-03.png

Страница управления роботом с лампочкой содержит элементы:
- статус робота: подключен/отключен (плюс кнопка "отключить", чтобы отправить роботу команду "kick" и отключить его от Сервера Роботов)
- статус лампочки: включена/выключена
- управление лампочкой: кнопки "включить лампочку"/"выключить лампочку"
- область отладочных сообщений

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

Весь динамический функционал реализован с JavaScript/AJAX, на сервер отправляются фоновые http-запросы, ответы обновляются на странице безе перезагрузки.

По внешнему виду. Дизайн и вёрстка страниц с HTML И CSS - это большая отдельная история, здесь только пара нюансов.

Обычные ссылки из тега <a> легко превращаем в красивые кнопки при помощи стилей CSS (искать в интернете по "fancy css buttons")

Картинки с лампочками берём с openclipart.org:
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

Вся остальная верстка и дизайн с CSS по вкусу.

ScalaUnfilteredWebFrontend/src/main/resources/html/index.html


<!DOCTYPE html>
<html>
    <head>
        <title>Сервер Роботов: управление лампочкой</title>
        <meta charset="UTF-8"/>
        <link href="/css/site.css" rel="STYLESHEET" type="text/css"/>

        <script type="text/javascript">
            function sendServerRequest(url, requestHandler) {
                var req = new XMLHttpRequest();
                req.onreadystatechange = function() {
                    if (req.readyState === 4) { // only if req is "loaded"
                        if (req.status === 200) { // only if "OK"
                            requestHandler(req.responseText);
                        } else {
                            // error
                        }
                    }
                };
                // can't use GET method here as it would quickly 
                // exceede max length limitation
                req.open("POST", url, true);

                //Send the proper header information along with the request
                req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                req.send();
            }

            function printResponseMessage(responseText) {
                var message;

                if (responseText === "ok") {
                    message = "Робот: хорошо (ok)";
                } else if (responseText === "dontunderstand") {
                    message = "Робот: простите, не понял (dontunderstand)"
                } else if (responseText === "rs:disconnected") {
                    message = "Сервер Роботов: робот не подключен (rs:disconnected)"
                } else if (responseText === "rc:notstarted") {
                    message = "Облако Роботов: не запущен Сервер Роботов (rc:notstarted)"
                } else {
                    message = responseText;
                }
                document.getElementById("debug_console").innerHTML +=
                        "\n" + message;
            }

            /**
             * Отправить роботу команду ledstatus - узнать текущий статус лампочки
             * и заодно узнать, подключена ли плата вообще
             * @returns {undefined}
             */
            function cmd_ledstatus() {
                sendServerRequest("/cmd/ledstatus", function(responseText) {
                    if (responseText === "rs:disconnected" || responseText === "rc:notstarted") {
                        // статус подключения
                        document.getElementById("robot_status").innerHTML = "ОТКЛЮЧЕН";
                        document.getElementById("robot_status").className = "robot_status_disconnected";
                        document.getElementById("kick").style.visibility = "hidden";

                        // обновим статус лампочки
                        document.getElementById("led_status").
                                setAttribute("src", "/img/led_none.png");
                    } else {
                        // статус подключения
                        document.getElementById("robot_status").innerHTML = "ПОДКЛЮЧЕН";
                        document.getElementById("robot_status").className = "robot_status_connected";
                        document.getElementById("kick").style.visibility = "visible";

                        // обновим статус лампочки
                        if (responseText === "on") {
                            document.getElementById("led_status").
                                    setAttribute("src", "/img/led1_on.png");
                        } else if (responseText === "off") {
                            document.getElementById("led_status").
                                    setAttribute("src", "/img/led_off.png");
                        }
                    }
                });
            }

            /**
             * Отправить роботу команду ledon - включить лампочку
             * @returns {undefined}
             */
            function cmd_ledon() {
                document.getElementById("debug_console").innerHTML +=
                        "\nЯ: " + "Робот, включи, пожалуйста, лампочку (ledon)";
                sendServerRequest("/cmd/ledon", function(responseText) {
                    // обновим статус лампочки
                    if (responseText === "ok") {
                        document.getElementById("led_status").
                                setAttribute("src", "/img/led1_on.png");
                    }

                    // отладочное сообщение
                    printResponseMessage(responseText);
                });
            }

            /**
             * Отправить роботу команду ledoff - выключить лампочку
             * @returns {undefined}
             */
            function cmd_ledoff() {
                document.getElementById("debug_console").innerHTML +=
                        "\nЯ: " + "Робот, выключи, пожалуйста, лампочку (ledoff)";
                sendServerRequest("/cmd/ledoff", function(responseText) {
                    // обновим статус лампочки
                    if (responseText === "ok") {
                        document.getElementById("led_status").
                                setAttribute("src", "/img/led_off.png");
                    }

                    // отладочное сообщение
                    printResponseMessage(responseText);
                });
            }
            
            /**
             * Отправить Серверу Роботов команду kick - оборвать соединение с 
             * подключенным роботом
             * @returns {undefined}
             */
            function cmd_kick() {
                document.getElementById("debug_console").innerHTML +=
                        "\nЯ: " + "Сервер Роботов, отпусти Робота погулять (kick)";
                sendServerRequest("/cmd/kick", function(responseText) {
                    // отладочное сообщение
                    printResponseMessage(responseText);
                });
            }
            
            /**
             * Разные настройки при загрузке страницы
             * @returns {undefined}
             */
            function onLoad() {
                // Для отображения актуального статуса подключения будем 
                // отправлять роботу команду ledstatus каждую секунду
                setInterval(cmd_ledstatus, 1000);
                // первую команду отправим сразу не дожидаясь таймера
                cmd_ledstatus();
            }
        </script>
    </head>
    <body onload="onLoad()">
        <div class="header">
            <div class="robot_server">
                <!--Роботов - фамилия, поэтому с большой буквы-->
                Сервер Роботов
            </div>
            <div class="caption_content">
                управляем роботом-с-лампочкой из облака: включаем и выключаем лампочку
            </div>
            <a class="logo" href="http://1i7.livejournal.com" target="_blank"></a>
        </div>
        <div id="page">
            <div class ="page_content">
                <div id="robot_status_block">
                    статус робота: <span id="robot_status" class="robot_status_disconnected">ОТКЛЮЧЕН</span><br>
                    <a id="kick" href="javascript:cmd_kick()">(отключить)</a>
                </div>

                <div style="text-align: center">
                    <!--Клипарты светодиодов:
* 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
                    -->
                    <div style="margin: 30px">
                        <img width="150" src="/img/led_none.png" alt="led"/>
                    </div>
                    <div style="margin: 30px">
                        <a class="button orange" href="javascript:cmd_ledon()">Включить лампочку (команда 'ledon')</a>
                    </div>
                    <div style="margin: 30px">
                        <a class="button blue" href="javascript:cmd_ledoff()">Выключить лампочку (команда 'ledoff')</a>
                    </div>
                </div>
                <h3>
                    Беседа с роботом:
                </h3>
                <pre id="debug_console"></pre>
            </div>
        </div>


    </body>
</html>



Запускаем, проверяем

1) Запустили в облаке Сервер Роботов2,
2) развернули и запустили веб-приложение в облаке,
3) подключили робота с лампочкой (плату ChipKIT WF32) к Серверу Роботов.

(живая демонстрация с работающим Сервером Роботов и веб-приложением запущена по адресу http://robotc.lasto4ka.su, там также постоянно висит подключенная ChipKIT WF32, ее можно легко заменить на своего робота и помигать лампочкой через интернет, не разворачивая собственный сервер).

robotc-01.png

Включили лампочку (она также загорится на плате):

robotc-02.png

Выключили лампочку (она также потухнет на плате):

robotc-03.png

Отключили робота:

robotc-04.png



исходники занятия, подсветка синтаксиса
Tags: облако, сервер роботов, типовые задачи
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