Espruino:Примеры/Интерактивный пользовательский веб-интерфейс

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Перевод: Максим Кузьмин (Cubewriter) Контакты:</br>* Skype: cubewriter</br>* E-mail: cubewriter@gmail.com</br>* Максим Кузьмин на freelance.ru
Проверка/Оформление/Редактирование: Мякишев Е.А.


Интерактивный пользовательский веб-интерфейс[1]

Interactive Web UI mobile.png

Если в вашем проекте используется Ethernet/WiFi и веб-страница, то вы можете украсить её современным пользовательским интерфейсом.

Делать интерактивный интерфейс из обычных HTML-элементов довольно проблематично. Но современные браузеры поддерживают SVG-элементы, что позволяет вам встроить в веб-страницу векторную графику и модифицировать её – как будто это обычные HTML-элементы.

В этом руководстве мы покажем, как сделать современно выглядящий пользовательский интерфейс в редакторе векторной графики вроде Inkscape, а также то, как при помощи Espruino сделать этот интерфейс быстрым и лёгким для использования.

Нам понадобятся

Подсоединение

О том, как подключить эти устройства, читайте в статьях о модулях ESP8266 WiFi и WIZnet W550io Ethernet.

Код и графика

Сначала запускаем Inkscape и рисуем графику. Я создал температурную шкалу, состоящую из нескольких простых элементов – кольца, текста, стрелочек вверх/вниз и приборной стрелки (серого цвета).

Interactive Web UI inkscape.png

Когда создадите что-нибудь подобное, кликните правой кнопкой мыши по каждому элементу, выберите пункт Object Properties и задайте в id какое-нибудь простое обозначение. Я задал ring, label, up, dn и needle. Затем сохраните этот файл как Plain SVG при помощи Save As. Это удалит все ненужные теги Inkscape. Наконец, создайте простой HTML-файл и вставьте его внутрь него созданное выше SVG-изображение (я уже удалил несколько тегов):

<html>
    <body>
<svg width="500" height="500">
    <path
       style="fill:#80e5ff;fill-opacity:1;fill-rule:nonzero;stroke:none"
       d="M 250 0 C 111.92882 3.7895613e-14 0 111.92882 0 250 C -1.249508e-14 341.05067 48.689713 420.72528 121.4375 464.4375 L 154.625 409.40625 C 100.50052 376.95218 64.28125 317.69934 64.28125 250 C 64.28125 147.43284 147.43284 64.28125 250 64.28125 C 352.56716 64.28125 435.71875 147.43284 435.71875 250 C 435.71875 317.53896 399.66155 376.65256 345.75 409.15625 L 378.71875 464.34375 C 451.37991 420.61135 500 340.98541 500 250 C 500 111.92882 388.07118 -1.8947806e-14 250 0 z " id="ring"/>
    <rect
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
       id="needle"
       width="16"
       height="80"
       x="242"/>
    <text
       xml:space="preserve"
       style="font-size:122.59261322px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Helvetica;-inkscape-font-specification:Helvetica"
       x="250.01915"
       y="845.31812"
       id="text"><tspan
         id="label"
         x="250.01915"
         y="292.95594">0</tspan></text>
    <path
       style="fill:#d5f6ff;fill-opacity:1;fill-rule:nonzero;stroke:none"
       id="up"
       d="m 294.75099,133.39225 -90.93056,0 45.46528,-78.748173 z"
       transform="matrix(0.61903879,0,0,0.61903879,95.682477,91.16682)"
       />
    <path
       transform="matrix(0.61903879,0,0,-0.61903879,95.682477,408.80767)"
       d="m 294.75099,133.39225 -90.93056,0 45.46528,-78.748173 z"
       id="dn"
       style="fill:#d5f6ff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</svg>

</body></html>

Если загрузить этот HTML-файл, то внутри него уже должно быть наше SVG-изображение. Теперь давайте внесём в него несколько изменений...

  • Добавьте тег <head>, а если точнее – <head><meta name="viewport" content="width=device-width,initial-scale=1"></head>. В итоге это SVG-изображение будет показываться на мобильных устройствах в режиме полного экрана.
  • Обязательно добавьте тег <body>, а если точнее – <body style="width:100%;height:100%;overflow:hidden;">. Это нужно, опять же, для режима полного экрана.
  • Теперь сделайте так, чтобы само SVG-изображение можно было растянуть до нового размера. Замените <svg width="500" height="500" id="svg"> на <svg style="width:100%;height:100%;" viewbox="0 0 500 500" id="svg">.
  • Теперь нам надо добавить немного JavaScript-кода, чтобы сделать SVG-изображение интерактивным. То, каким должен быть этот код, сильно зависит от того, что именно вы нарисовали, но код для моей графики выглядит вот так (он находится в самом низу):
<html>
<head><meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="width:100%;height:100%;overflow:hidden;">
<svg style="width:100%;height:100%;"
   viewbox="0 0 500 500" id="svg">
    <path
       style="fill:#80e5ff;fill-opacity:1;fill-rule:nonzero;stroke:none"
       d="M 250 0 C 111.92882 3.7895613e-14 0 111.92882 0 250 C -1.249508e-14 341.05067 48.689713 420.72528 121.4375 464.4375 L 154.625 409.40625 C 100.50052 376.95218 64.28125 317.69934 64.28125 250 C 64.28125 147.43284 147.43284 64.28125 250 64.28125 C 352.56716 64.28125 435.71875 147.43284 435.71875 250 C 435.71875 317.53896 399.66155 376.65256 345.75 409.15625 L 378.71875 464.34375 C 451.37991 420.61135 500 340.98541 500 250 C 500 111.92882 388.07118 -1.8947806e-14 250 0 z " id="ring"/>
    <rect
       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
       id="needle"
       width="16"
       height="80"
       x="242"/>
    <text
       xml:space="preserve"
       style="font-size:122.59261322px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Helvetica;-inkscape-font-specification:Helvetica"
       x="250.01915"
       y="845.31812"
       id="text"><tspan
         id="label"
         x="250.01915"
         y="292.95594">0</tspan></text>
    <path
       style="fill:#d5f6ff;fill-opacity:1;fill-rule:nonzero;stroke:none"
       id="up"
       d="m 294.75099,133.39225 -90.93056,0 45.46528,-78.748173 z"
       transform="matrix(0.61903879,0,0,0.61903879,95.682477,91.16682)"
       />
    <path
       transform="matrix(0.61903879,0,0,-0.61903879,95.682477,408.80767)"
       d="m 294.75099,133.39225 -90.93056,0 45.46528,-78.748173 z"
       id="dn"
       style="fill:#d5f6ff;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</svg>

<script>
// Если используется мобильное устройство,
// здесь мы конвертируем касание в событие, сгенерированное мышкой: 
function touchHandler(event) {
  var touches = event.changedTouches,
       first = touches[0], type = "";
  switch(event.type) {
    case "touchstart": type="mousedown"; break;
    case "touchmove":  type="mousemove"; break;        
    case "touchend":   type="mouseup"; break;
    default: return;
  }
  var simulatedEvent = document.createEvent("MouseEvent");
  simulatedEvent.initMouseEvent(type, true, true, window, 1,
                            first.screenX, first.screenY,
                            first.clientX, first.clientY, false,
                            false, false, false, 0/*left*/, null);
  first.target.dispatchEvent(simulatedEvent);
  event.preventDefault();
}
document.addEventListener("touchstart", touchHandler, true);
document.addEventListener("touchmove", touchHandler, true);
document.addEventListener("touchend", touchHandler, true);
document.addEventListener("touchcancel", touchHandler, true);

// Перемещаем приборную стрелку на нужную позицию:
var pos = 50;
function setPos(p) {
  if (p<0) p=0;
  if (p>100) p=100;
  pos = p;
  // Меняем текст надписи:
  document.getElementById("label").textContent = pos;    
  // Перемещаем приборную стрелку 
  // по шкале SVG-изображения в нужное место: 
  var a = (pos-50)*2.8;
  document.getElementById("needle").setAttribute("transform","rotate("+a+" 250 250)");    
}
setPos(pos);

// Обрабатываем события:
var dragging = false;
function dragStart() {
  dragging = true;
  document.getElementById("ring").style.fill = "#ff0000";
}
document.addEventListener("mousemove", function(e) {
  if (dragging) {
    e.preventDefault();
    var svg = document.getElementById("svg");
    // Рассчитываем угол, исходя из координат.
    // Обратите внимание, что координаты задаются
    // в пикселях окна, а не в пикселях SVG-изображения.
    var ang = Math.atan2(e.clientX-(svg.clientWidth/2),(svg.clientHeight/2)-e.clientY)*180/Math.PI;
    // Перемещаем на новое место:
    setPos(Math.round((ang/2.8)+50));
  }
});
document.addEventListener("mouseup", function(e) {
  dragging = false;
  document.getElementById("ring").style.fill = "#80e5ff";
  document.getElementById("up").style.fill = "#d5f6ff";
  document.getElementById("dn").style.fill = "#d5f6ff";
});
document.getElementById("ring").onmousedown = dragStart;
document.getElementById("needle").onmousedown = dragStart;
document.getElementById("up").onmousedown = function(e) { e.preventDefault(); this.style.fill = "#ff0000"; };
document.getElementById("dn").onmousedown = function(e) { e.preventDefault(); this.style.fill = "#00ff00"; };
document.getElementById("up").onmouseup = function(e) { setPos(pos+10); };
document.getElementById("dn").onmouseup = function(e) { setPos(pos-10); };
</script>
</body>
</html>

Это довольно простой код. Во-первых, мы используем общий шаблонный код для преобразования событий, генерируемых мобильными устройствами при касаниях по тачпаду, в события использования мышки. Затем мы используем обычные DOM-команды JavaScript, чтобы получить доступ к SVG-элементам – чтобы перемещать их или менять их фоновые цвета.

Если вы воспользуетесь кодом выше на обычной веб-странице, на ней будет полнофункциональная шкала со стрелкой, способной перемещаться между «0» и «100».

И теперь нам надо встроить всё это в Espruino. Давайте отредактируем обработчик mouseup, чтобы он при помощи функции XMLHttpRequest() отправлял на Espruino новые данные сразу после того, как пользователь отпустит палец от экрана (т.е. уберёт палец с мышки).

document.addEventListener("mouseup", function(e) {
  dragging = false;
  document.getElementById("ring").style.fill = "#80e5ff";
  document.getElementById("up").style.fill = "#d5f6ff";
  document.getElementById("dn").style.fill = "#d5f6ff";
  // Отправляем данные на Espruino при помощи запроса POST:
  var req=new XMLHttpRequest();
  req.open("POST","?pos="+pos, true);
  req.send();
});

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

Наконец, пишем немного кода для Espruino, который будет отвечать за обслуживание веб-страницы и обрабатывать запрос POST с данными, отправленными через HTTP на Espruino.

var page = "<html>\n<head><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head>\n<body style=\"width:100%;height:100%;overflow:hidden;\">\n<svg style=\"width:100%;height:100%;\"\n   viewbox=\"0 0 500 500\" id=\"svg\">\n    <path\n       style=\"fill:#80e5ff;fill-opacity:1;fill-rule:nonzero;stroke:none\"\n       d=\"M 250 0 C 111.92882 3.7895613e-14 0 111.92882 0 250 C -1.249508e-14 341.05067 48.689713 420.72528 121.4375 464.4375 L 154.625 409.40625 C 100.50052 376.95218 64.28125 317.69934 64.28125 250 C 64.28125 147.43284 147.43284 64.28125 250 64.28125 C 352.56716 64.28125 435.71875 147.43284 435.71875 250 C 435.71875 317.53896 399.66155 376.65256 345.75 409.15625 L 378.71875 464.34375 C 451.37991 420.61135 500 340.98541 500 250 C 500 111.92882 388.07118 -1.8947806e-14 250 0 z \" id=\"ring\"/>\n    <rect\n       style=\"fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none\"\n       id=\"needle\"\n       width=\"16\"\n       height=\"80\"\n       x=\"242\"/>\n    <text\n       xml:space=\"preserve\"\n       style=\"font-size:122.59261322px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-align:center;line-height:125%;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;font-family:Helvetica;-inkscape-font-specification:Helvetica\"\n       x=\"250.01915\"\n       y=\"845.31812\"\n       id=\"text\"><tspan\n         id=\"label\"\n         x=\"250.01915\"\n         y=\"292.95594\">0</tspan></text>\n    <path\n       style=\"fill:#d5f6ff;fill-opacity:1;fill-rule:nonzero;stroke:none\"\n       id=\"up\"\n       d=\"m 294.75099,133.39225 -90.93056,0 45.46528,-78.748173 z\"\n       transform=\"matrix(0.61903879,0,0,0.61903879,95.682477,91.16682)\"\n       />\n    <path\n       transform=\"matrix(0.61903879,0,0,-0.61903879,95.682477,408.80767)\"\n       d=\"m 294.75099,133.39225 -90.93056,0 45.46528,-78.748173 z\"\n       id=\"dn\"\n       style=\"fill:#d5f6ff;fill-opacity:1;fill-rule:nonzero;stroke:none\" />\n</svg>\n\n<script>\n// Convert touch to mouse event for mobile devices\nfunction touchHandler(event) {\n  var touches = event.changedTouches,\n       first = touches[0], type = \"\";\n  switch(event.type) {\n    case \"touchstart\": type=\"mousedown\"; break;\n    case \"touchmove\":  type=\"mousemove\"; break;        \n    case \"touchend\":   type=\"mouseup\"; break;\n    default: return;\n  }\n  var simulatedEvent = document.createEvent(\"MouseEvent\");\n  simulatedEvent.initMouseEvent(type, true, true, window, 1, \n                            first.screenX, first.screenY, \n                            first.clientX, first.clientY, false, \n                            false, false, false, 0/*left*/, null);\n  first.target.dispatchEvent(simulatedEvent);\n  event.preventDefault();\n}\ndocument.addEventListener(\"touchstart\", touchHandler, true);\ndocument.addEventListener(\"touchmove\", touchHandler, true);\ndocument.addEventListener(\"touchend\", touchHandler, true);\ndocument.addEventListener(\"touchcancel\", touchHandler, true); \n\n// rotate needle to correct position\nvar pos = 50;\nfunction setPos(p) {\n  if (p<0) p=0;\n  if (p>100) p=100;\n  pos = p;\n  document.getElementById(\"label\").textContent = pos;    \n  var a = (pos-50)*2.8;\n  document.getElementById(\"needle\").setAttribute(\"transform\",\"rotate(\"+a+\" 250 250)\");    \n}\nsetPos(pos);\n\n// handle events\nvar dragging = false;\nfunction dragStart() {\n  dragging = true;\n  document.getElementById(\"ring\").style.fill = \"#ff0000\";\n}\ndocument.addEventListener(\"mousemove\", function(e) {\n  if (dragging) {\n    e.preventDefault();\n    var svg = document.getElementById(\"svg\");\n    var ang = Math.atan2(e.clientX-(svg.clientWidth/2),(svg.clientHeight/2)-e.clientY)*180/Math.PI;\n    setPos(Math.round((ang/2.8)+50));\n  }\n});\ndocument.addEventListener(\"mouseup\", function(e) {\n  dragging = false;\n  document.getElementById(\"ring\").style.fill = \"#80e5ff\";\n  document.getElementById(\"up\").style.fill = \"#d5f6ff\";\n  document.getElementById(\"dn\").style.fill = \"#d5f6ff\";\n  // POST data to Espruino\n  var req=new XMLHttpRequest();\n  req.open(\"POST\",\"?pos=\"+pos, true);\n  req.send();\n});\ndocument.getElementById(\"ring\").onmousedown = dragStart;\ndocument.getElementById(\"needle\").onmousedown = dragStart;\ndocument.getElementById(\"up\").onmousedown = function(e) { e.preventDefault(); this.style.fill = \"#ff0000\"; };\ndocument.getElementById(\"dn\").onmousedown = function(e) { e.preventDefault(); this.style.fill = \"#00ff00\"; };\ndocument.getElementById(\"up\").onmouseup = function(e) { setPos(pos+10); };\ndocument.getElementById(\"dn\").onmouseup = function(e) { setPos(pos-10); };\n</script>\n</body>\n</html>";


// Задаём значение для переменной «pos»:
var pos = 50;
// Генерируем простую ШИМ на светодиоде LED1,
// чтобы показать значение «pos»:
setInterval(function() {
  digitalPulse(LED1, 1, pos/7);
}, 20);


function setPos(p) {
  pos = p;
  console.log("Значение «pos» изменилось на "+p);
}

// При запросе веб-страницы:
function pageHandler(req, res) {
  if (req.method=="POST") {
    // Если это запрос POST, сохраняем данные:
    var info = url.parse(req.url,true);
    console.log("POST ",info);
    if (info.query && "pos" in info.query)
      setPos(parseFloat(info.query.pos));
    res.writeHead(200);
    res.end("Ok.");
  } else {
    // В противном случае показываем веб-страницу:
    console.log("GET "+req.url);
    if (req.url=="/") {
      res.writeHead(200);
      res.end(page);
    } else {
      res.writeHead(404);
      res.end("404: Not found");
    }
  }
}

// Для ESP8266 WiFi:
/*var WIFI_NAME = "wifi_name";
var WIFI_PASS = "wifi_key";

Serial2.setup(9600, { rx: A3, tx : A2 });
var wifi = require("ESP8266WiFi").connect(Serial2, function(err) {
  if (err) throw err;
  console.log("Подключение к WiFi");
  wifi.connect(WIFI_NAME, WIFI_PASS, function(err) {
    if (err) throw err;
    console.log("Подключились");
    // Печатаем IP-адрес:
    wifi.getIP(console.log);
    // Создаём сервер:
    require("http").createServer(pageHandler).listen(80);
  });
});*/

// Для WIZnet W550io:
SPI2.setup({ mosi:B15, miso:B14, sck:B13 });
var eth = require("WIZnet").connect(SPI2, B10);
console.log("IP = "+eth.getIP().ip);
require("http").createServer(pageHandler).listen(80);

Вставьте этот код в правую часть Web IDE, но не забудьте раскомментировать код для своего сетевого устройства и, наоборот, закомментировать код для устройства, которым не пользуетесь. Также не забудьте указать свои WiFi-настройки, если используете WiFi. После этого кликните на кнопку загрузки кода в центре Web IDE.

Подключение к WiFi займёт какое-то время, после чего Espruino напечатает свой IP-адрес (например, «http://192.168.1.84»). Вставьте его в адресную строку браузера и нажмите  ↵ Enter .

После этого в браузере должна появиться веб-страница с созданным нами интерфейсом – это страница обслуживается исключительно мощностями Espruino. Если подвигать стрелку или понажимать на кнопки, на Espruino будет сделан запрос HTTP POST. В результате Espruino обработает этот запрос и вызовет функцию setPos() с новым значением, которое изменит яркость красного светодиода.

См.также

Внешние ссылки