Espruino:Примеры/Интерактивный пользовательский веб-интерфейс
Интерактивный пользовательский веб-интерфейс[1]
Если в вашем проекте используется Ethernet/WiFi и веб-страница, то вы можете украсить её современным пользовательским интерфейсом.
Делать интерактивный интерфейс из обычных HTML-элементов довольно проблематично. Но современные браузеры поддерживают SVG-элементы, что позволяет вам встроить в веб-страницу векторную графику и модифицировать её – как будто это обычные HTML-элементы.
В этом руководстве мы покажем, как сделать современно выглядящий пользовательский интерфейс в редакторе векторной графики вроде Inkscape, а также то, как при помощи Espruino сделать этот интерфейс быстрым и лёгким для использования.
Нам понадобятся
- Одна плата Espruino Pico
- Модуль ESP8266 WiFi или WIZnet W550io Ethernet
Подсоединение
О том, как подключить эти устройства, читайте в статьях о модулях ESP8266 WiFi и WIZnet W550io Ethernet.
Код и графика
Сначала запускаем Inkscape и рисуем графику. Я создал температурную шкалу, состоящую из нескольких простых элементов – кольца, текста, стрелочек вверх/вниз и приборной стрелки (серого цвета).
Когда создадите что-нибудь подобное, кликните правой кнопкой мыши по каждому элементу, выберите пункт 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() с новым значением, которое изменит яркость красного светодиода.
См.также
Внешние ссылки