Как сделать Web-интерфейс для ESP8266 под NodeMCU
WiFi модули на базе микроконтроллера ESP8266 имеют достаточно интересный функционал, включая возможность использовать WiFi. Это позволяет использовать их в различных домашних устройствах. Создание Web-интерфейса для таких устройств - наиболее привлекательная, но не всегда простая тема. В этой статье рассматриваются примеры создания web-интерфейса для ESP8266 под framework NodeMCU на языке LUA. В примерах от простого к сложному ознакомимся с преимуществами ESP8266 и научимся бороться с его недостатками. Главный недостаток ESP8266, особенно при построение web-интерфейса, это конечный объем оперативной памяти. Этого можно не заметить при создании простых приложений, но при решении более сложных задач Вы неизбежно столкнетесь с недостатком памяти. Надеюсь, эта статья поможет обойти подобные проблемы.
Во всех примерах использовался модуль ESP12E и фреймворк NodeMCU собранный с модулями: adc, bme280, cron, crypto, dht, file, gpio, http, i2c, mqtt, net, node, pwm, rtctime, sjson, sntp, spi, tmr, u8g, uart, websocket, wifi, tls.
Такое количество модулей не обязательно. Эта сборка использовалачь для примеров к другим статьям. Необходимые модули: file, net, sjson, websocket, wifi.
Скачать фреймворк NodeMCUможно здесь
Скачать примеры здесь.
Пример №1 (динамическая страница)
Для того, чтобы реализовать web-интерфейс, нам надо сделать свой крохотный web-сервер. B мы начнем с самого простого примера. NodeMCU позволяет создавать TCP сервер оной командой. Далее мы подключаем слушателя на нужный нам порт. В нашем случае порт 80. Это стандартный порт для HTTP протокола. Указываем какие функции вызывать при возникновении событий. Доступны такие события: "connection" - подключение, "reconnection" — повторное подключение, "disconnection" - отключение, "receive" - получение данных, "sent"- завершение отправки данных. Нас не особо интересует момент подключения. Нам интересен запрос который отправляет браузер уже после подключения. Поэтому, на подключение и отключение ничего не вешаем, обрабатываем только получение данных.
--Create Server
sv=net.createServer(net.TCP)
function receiver(sck, data)
-- Print received data
print(data)
-- Send response
sck:on("sent", function(sck) sck:close() end)
sck:send("HTTP/1.0 200 OK\r\nServer: NodeMCU\r\nContent-Type: text/html\r\n\r\n"..
"<html><title>NodeMCU</title><body>"..
"<h1>NodeMCU</h1>"..
"<hr>"..
"Hello world!"..
"<p>Time: "..tmr.now().."</p>"..
"</body></html>")
end
if sv then
sv:listen(80, function(conn)
conn:on("receive", receiver)
end)
end
Т.е., в простейшем объяснении, диалог между веб сервером и браузером происходит так: Браузер подключается к серверу (наш модуль) на порт 80, отправляет запрос (структуру запроса рассмотрим позже). Сервер обрабатывает запрос и отправляет ответ браузеру, после чего разрывает соединение. Обратите внимание, соединение разрывает сервер после отправки всех данных.
Примечание: для решения некоторых задач соединение между сервером и браузером может не разрываться. Но этот вариант работы в статье не рассматривается.
Первый, простейший пример web-сервера, ожидает запрос от браузера, причем сам запрос не анализируется. После поучения любого запроса генерирует и отправляет HTML страницу. В страницу вставлен счетчик таймера. Вместо таймера можно вставить переменную, скажем значение датчика температуры, и таким образом получаем простейший web-интерфейс с полезным содержимым.
Внимание! Перед запуском примеров нужно отредактировать и затем запустить скрипт wifi.lua. В этом файле следует описать настройки подключения к Вашей локальной Wi-Fi сети. Модуль ESP после подключения к Вашей локальной Wi-Fi сети должен получить IP адрес. В примерах используется 192.168.0.108, у Вас будет другой. Узнать IP адрес можно командой:
=wifi.sta.getip()
Пример 1: Скачать файлы примера. необходимо залить файлы: web1.lua запустить web-сервер, выполнив скрипт: web1.lua В браузере открыть ссылку: http://192.168.0.108/ |
Примечание: Если в браузере набрать ссылку вида http://192.168.0.1048/ или http://192.168.0.108/index.html Вы все равно получите один и тот же ответ, поскольку запрос пока не анализируется.
В примере вставлена команда вывода в консоль текста запроса полученного от браузера. Его анализом мы займемся чуть позже. Сам запрос выглядит примерно следующим образом:
GET /index.html HTTP/1.1 Host: 192.168.0.108 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Upgrade-Insecure-Requests: 1 Cache-Control: max-age=0
Пример №2 (статическая страница)
Иногда требуется получить статическую страницу (картинку, файл стилей, Java-скрипт, и т.п.), а не генерировать динамически скриптом. NodeMCU имеет простую файловую систему, что позволяет записать файл, скажем index.html, и отдавать его браузеру при запросе. Это мы и сделаем во втором примере.
-- Close old Server
if sv then
sv:close()
end
--Create Server
sv=net.createServer(net.TCP)
function receiver(sck, data)
-- Print received data
print(data)
-- Send response
sck:on("sent", function(sck) sck:close() end)
filecontent = ``;
-- read file:
if file.open("index.html", "r") then
filecontent = file.read()
file.close()
end
sck:send(filecontent)
end
if sv then
sv:listen(80, function(conn)
conn:on("receive", receiver)
end)
end
Пример 2: Скачать файлы примера. необходимо залить файлы: web2.lua запустить web-сервер, выполнив скрипт: web2.lua В браузере открыть ссылку: http://192.168.0.108/ |
Пример №3 (слишком большой файл)
Теперь попробуем сделать файл побольше размером и получим проблему о котором я говорил в самом начале - память в микроконтроллере имеет конечный объем и этот объем не очень большой. Система не может прочитать слишком большой файл. Мы увидим лишь часть файла. Аналогичная ситуация будет при динамической генерации страницы большого объема.
Пример 3: Скачать файлы примера. необходимо залить файлы: web3.lua, large.html запустить web-сервер, выполнив скрипт: web3.lua В браузере открыть ссылку: http://192.168.0.108/ |
Мы видим часть файла, на самом деле в нем более 40 строк.
Пример №3 (слишком большой объем данных)
-- Close old Server
if sv then
sv:close()
end
--Create Server
sv=net.createServer(net.TCP)
function receiver(sck, data)
-- Print received data
print(data)
-- Send response
sck:on("sent", function(sck) sck:close() end)
response = "<html>"..
" <title>NodeMCU</title>"..
"<body>"..
"<h1>NodeMCU</h1>"..
"<hr>"..
"Hello world! It is a BIG HTML file `index1.html`"..
"<p>1 Something big Something big Something big Something big Something big Something big</p>"..
"<p>2 Something big Something big Something big Something big Something big Something big</p>"..
"<p>3 Something big Something big Something big Something big Something big Something big</p>"..
"<p>4 Something big Something big Something big Something big Something big Something big</p>"..
"<p>5 Something big Something big Something big Something big Something big Something big</p>"..
"<p>6 Something big Something big Something big Something big Something big Something big</p>"..
"<p>7 Something big Something big Something big Something big Something big Something big</p>"..
"<p>8 Something big Something big Something big Something big Something big Something big</p>"..
"<p>9 Something big Something big Something big Something big Something big Something big</p>"..
"<p>10 Something big Something big Something big Something big Something big Something big</p>"..
"<p>11 Something big Something big Something big Something big Something big Something big</p>"..
"<p>12 Something big Something big Something big Something big Something big Something big</p>"..
"<p>13 Something big Something big Something big Something big Something big Something big</p>"..
"<p>14 Something big Something big Something big Something big Something big Something big</p>"..
"<p>15 Something big Something big Something big Something big Something big Something big</p>"..
"<p>16 Something big Something big Something big Something big Something big Something big</p>"..
"<p>17 Something big Something big Something big Something big Something big Something big</p>"..
"<p>18 Something big Something big Something big Something big Something big Something big</p>"..
"<p>19 Something big Something big Something big Something big Something big Something big</p>"..
"<p>20 Something big Something big Something big Something big Something big Something big</p>"..
"<p>21 Something big Something big Something big Something big Something big Something big</p>"..
"<p>22 Something big Something big Something big Something big Something big Something big</p>"..
"<p>23 Something big Something big Something big Something big Something big Something big</p>"..
"<p>24 Something big Something big Something big Something big Something big Something big</p>"..
"<p>25 Something big Something big Something big Something big Something big Something big</p>"..
"<p>26 Something big Something big Something big Something big Something big Something big</p>"..
"<p>27 Something big Something big Something big Something big Something big Something big</p>"..
"<p>28 Something big Something big Something big Something big Something big Something big</p>"..
"<p>29 Something big Something big Something big Something big Something big Something big</p>"..
"<p>30 Something big Something big Something big Something big Something big Something big</p>"..
"<p>31 Something big Something big Something big Something big Something big Something big</p>"..
"<p>32 Something big Something big Something big Something big Something big Something big</p>"..
"<p>33 Something big Something big Something big Something big Something big Something big</p>"..
"<p>34 Something big Something big Something big Something big Something big Something big</p>"..
"<p>35 Something big Something big Something big Something big Something big Something big</p>"..
"<p>36 Something big Something big Something big Something big Something big Something big</p>"..
"<p>37 Something big Something big Something big Something big Something big Something big</p>"..
"<p>38 Something big Something big Something big Something big Something big Something big</p>"..
"<p>39 Something big Something big Something big Something big Something big Something big</p>"..
"<p>40 Something big Something big Something big Something big Something big Something big</p>"..
"</body>"..
"</html>"
sck:send(response)
end
if sv then
sv:listen(80, function(conn)
conn:on("receive", receiver)
end)
end
Если отправлять большой объем данных прямо из скрипта, то NodeMCU может перезагружаться после сообщения "PANIC: unprotected error in call to Lua API (web4.lua:64: out of memory)"
Пример 4: Скачать файлы примера. необходимо залить файлы: web4.lua запустить web-сервер, выполнив скрипт: web4.lua В браузере открыть ссылку: http://192.168.0.108/ |
Пример №5 (отправка данных частями)
Обойти проблему нехватки памяти можно отправляя данные (страницы или файла) частями. Сделаем массив и будем отправлять построчно.
-- Close old Server
if sv then
sv:close()
end
--Create HTTP Server
sv=net.createServer(net.TCP)
function receiver(sck, data)
-- Print received data
print(data)
local response = {"<html>"}
response[#response+1]="<title>NodeMCU</title>"
response[#response+1]="<body>"
response[#response+1]="<h1>NodeMCU</h1>"
response[#response+1]="<hr>"
response[#response+1]="Hello world! It is a BIG HTML file `index1.html`"
response[#response+1]="</body>"
response[#response+1]="</html>"
response[#response+1]="<p>1 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>2 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>3 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>4 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>5 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>6 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>7 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>8 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>9 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>10 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>11 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>12 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>13 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>14 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>15 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>16 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>17 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>18 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>19 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>20 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>21 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>22 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>23 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>24 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>25 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>26 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>27 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>28 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>29 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>30 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>31 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>32 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>33 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>34 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>35 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>36 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>37 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>38 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>39 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="<p>40 Something big Something big Something big Something big Something big Something big</p>"
response[#response+1]="</body>"
response[#response+1]="</html>"
Пример 5: Скачать файлы примера. необходимо залить файлы: web5.lua запустить web-сервер, выполнив скрипт: web5.lua В браузере открыть ссылку: http://192.168.0.108/ |
Все данные (все 40 строк) успешно отправлены.
Пример №6 (отправка файла частями)
Отправка файла частями выполняется следующим образом. Сервер открывает файл, считывает небольшую часть файла и ставит ее на отправку. После того, как первая часть будет отправлена, сервер считывает и ставит на отправку следующую порцию, и так пока файл ее будет полностью отправлен. После отправки последней порции сервер закрывает файл и закрывает соединение с клиентом (браузером). Поскольку процедура отправки файла занимает относительно большое время, в течении которого микроконтроллеру нужно заниматься другим текущими делами, мы не можем просто ожидать окончания отправки кусочка файла, поэтому используем события.
-- Close old Server
if sv then
sv:close()
end
--Create HTTP Server
sv=net.createServer(net.TCP)
function receiver(sck, data)
-- Print received data
print(data)
fd = file.open("large.html", "r")
if fd then
local function send(localSocket)
local response = fd:read(512)
if response then
localSocket:send(response)
else
if fd then
fd:close()
end
localSocket:close()
end
end
sck:on("sent", send)
send(sck)
else
localSocket:close()
end
end
if sv then
sv:listen(80, function(conn)
conn:on("receive", receiver)
end)
end
Пример 6: Скачать файлы примера. необходимо залить файлы: web6.lua, large.html запустить web-сервер, выполнив скрипт: web6.lua В браузере открыть ссылку: http://192.168.0.108/ |
Весь файл large.html успешно получен браузером.
Пример №7 (многопоточность, CGI, метод GET)
Теперь, когда нам кажется, что мы все победили, встречаем следующую проблему. Иногда большие файлы все же прилетают не полностью. Но чаще все работает как надо. Почему? Потому, что браузер иногда отправляет еще один запрос, он хочет получить файл favicon.ico. И этот запрос приходит до того, как наш сервер завершил отправку большого файла. И как он реагирует на новый запрос? Он начинает обработку нового запроса, не закончив предыдущий. Аналогичная ситуация может случится, если в страницу вставить несколько картинок или ссылку на файл стилей. Браузер попытается загружать несколько файлов одновременно, а наш сервер с этим не справиться. Выход - нужно сделать так, чтобы сервер был много-поточным.
-- Close old Server
if httpd then
httpd:close()
end
httpd=net.createServer(net.TCP)
-- decode URI
function decodeURI(s)
if(s) then
s = string.gsub(s, `%%(%x%x)`,
function (hex) return string.char(tonumber(hex,16)) end )
end
return s
end
function receive_http(sck, data)
-- sendfile class
local sendfile = {}
sendfile.__index = sendfile
function sendfile.new(sck, fname)
local self = setmetatable({}, sendfile)
self.sck = sck
self.fd = file.open(fname, "r")
if self.fd then
local function send(localSocket)
local response = self.fd:read(128)
if response then
localSocket:send(response)
else
if self.fd then
self.fd:close()
end
localSocket:close()
self = nil
end
end
self.sck:on("sent", send)
send(self.sck)
else
localSocket:close()
end
return self
end
-----
local host_name = string.match(data,"Host: ([0-9,\.]*)\n",1)
local url_file = string.match(data,"[^/]*\/([^ ?]*)[ ?]",1)
local uri = decodeURI(string.match(data,"[^?]*\?([^ ]*)[ ]",1))
-- parse GET parameters
GET={}
if uri then
for key, value in string.gmatch(uri, "([^=&]*)=([^&]*)") do
GET[key]=value
print(key, value)
end
end
print("HTTP request:", uri)
request_OK = false
-- if file not specified then send index.html
if url_file == `` then
sendfile.new(sck, `index.html`)
request_OK = true
else
local fext=url_file:match("^.+(%..+)$")
if fext == `.html` or
fext == `.txt` or
fext == `.js` or
fext == `.json` or
fext == `.css` or
fext == `.png` or
--fext == `.gif` or
fext == `.ico` then
if file.exists(url_file) then
sendfile.new(sck, url_file)
request_OK = true
end
end
-- execute LUA file
-- IT IS HAZARDOUS
if fext == `.lua` then
if file.exists(url_file) then
response=dofile(url_file)
sck:on("sent", function() sck:close() end)
sck:send(response)
request_OK = true
end
end
end
if request_OK == false then
sck:on("sent", function() sck:close() end)
sck:send(`Something wrong`)
end
end
if httpd then
httpd:listen(80, function(conn)
conn:on("receive", receive_http)
end)
end
Примечание: Рекомендую посмотреть что прилетает в запросе. Из всего увиденного нас будет интересовать URL и данные, которые приходят GET и POST методом. Пока научимся извлекать URL и параметры GET запроса.
Пример 7: Скачать файлы примера. необходимо залить файлы: web7.lua, cgi.lua, index.html, large.html, test.html, image.gif, image.png запустить web-сервер, выполнив скрипт: web7.lua В браузере открыть ссылки: http://192.168.0.108/ http://192.168.0.108/index.html http://192.168.0.108/large.html http://192.168.0.108/nothing.html http://192.168.0.108/cgi.lua?name=andre |
Этот пример может загружать разные файлы в зависимости от URL, принимать и разбирать параметры GET запроса. Запускать lua скрипты в качестве cgi скриптов. Вот теперь сервер сможет параллельно обрабатывать запросы. Однако и это не решает всех проблем. Если запросов будет слишком много у контроллера все равно рано или поздно закончиться память. Как можно отодвинуть этот рубеж?
- Уменьшит буфер чтения из файла. Так можно увеличить количество потоков, которые сервер сможет обработать. Но тем самым мы увеличиваем количество операций чтения с flash памяти и немного увеличиваем время отправки данных.
- Стараться строить web-интерфейс таким образом, чтобы не выполнялось много одновременных загрузок. Например не вставлять много картинок в HTML. По возможности картинки, скрипты, стили Java скрипты располагать на внешних сайтах. Так же можно доработать скрипт нашего сервера и ввести ограничение на количество одновременно обрабатываемых запросов. И если количество обрабатываемых запросов выше допустимого, просто игнорировать новые запросы. Это опять же не решит всех проблем, что-то не загрузится, но хотя бы предотвратит перезагрузку NodeMCU.
В этом примере реализован разбор параметров приходящих методом GET, т.е. в адресной строке. Кроме того, сделана отправка файла по умолчанию (index.html если в url нет четкого указания файла). Так же есть список разрешенных расширений файлов, которые нашему серверу разрешено отдавать. Откройте ссылку http://192.168.0.108/test.html GIF-файл не отобразиться, а PNG мы увидим. За это отвечает этот фрагмент кода:
local fext=url_file:match("^.+(%..+)$")
if fext == `.html` or
fext == `.txt` or
fext == `.js` or
fext == `.json` or
fext == `.css` or
fext == `.png` or
--fext == `.gif` or
fext == `.ico` then
if file.exists(url_file) then
sendfile.new(sck, url_file)
request_OK = true
end
end
Кроме того, добавлена возможность запуска lua файлов. На примере рассмотрим как это работает.
CGI. Работа с параметрами GET
Разберем пример, когда в адресной строке передаются параметры: http://192.168.0.103/cgi.lua?name=Andre
В данном случае сервер увидит, что запрос содержит обращение к файлу с расширением lua, запустит его, дождется от него ответа и отправит ответ браузеру. В этом случае исполняемый файл lua должен возвращать ответ содержащий HTTP заголовок. Ниже приведен пример такого скрипта:
response="HTTP/1.0 200 OK\r\nServer: NodeMCU\r\nContent-Type: text/html\r\n\r\n"
response=response.."It`s LUA file response"
if GET[`name`] ~= nil then
response=response.."<p> Parameter `name` is <b>"..GET[`name`].."</b></p>"
end
return response
Скрипт возвращает значение параметра name, переданного в адресной строке. Как видите, доступ к переменным сделан через массив GET[]. Наш "web-сервер" подготавливает этот массив перед тем как запустить скрипт. Таким образом, можно принимать параметры переданные в URL, обрабатывать их в Вашем скрипте и формировать ответ в соответствии с запрошенными параметрами.
К сожалению, таким образом не получится формировать ответы значительного объема из за описанной ранее проблемы с конечным объемом памяти. Кроме того, такой подход кроет в себе проблему с безопасностью. Через URL теперь можно запустить любой lua скрипт, который есть на файловой системе NodeMCU. В статье я буду и далее использовать файлы с расширением lua, но Вам рекомендую для файлов, которые будут использованы для запуска из под нашего web сервера, использовать другое расширение. Они все равно будут исполнятся, но таким образом Вы сможете разграничить lua-файлы для внутренней работы и файлы с другим расширением для web-интерфейса. И решить вопросы с безопасносью, используя ранее описанный прием с разрешенными расширениями файлов.
Пример №8 (метод POST)
Передача данных методом POST. Этот метод часто используется при отправки данных html-форм. В файле web8.lua реализован разбор параметров приходящих на web-сервер методом POST.
-- Close old Server
if httpd then
httpd:close()
end
httpd=net.createServer(net.TCP)
-- decode URI
function decodeURI(s)
if(s) then
s = string.gsub(s, `%%(%x%x)`,
function (hex) return string.char(tonumber(hex,16)) end )
end
return s
end
function receive_http(sck, data)
-- sendfile class
local sendfile = {}
sendfile.__index = sendfile
function sendfile.new(sck, fname)
local self = setmetatable({}, sendfile)
self.sck = sck
self.fd = file.open(fname, "r")
if self.fd then
local function send(localSocket)
local response = self.fd:read(128)
if response then
localSocket:send(response)
else
if self.fd then
self.fd:close()
end
localSocket:close()
self = nil
end
end
self.sck:on("sent", send)
send(self.sck)
else
localSocket:close()
end
return self
end
-----
local host_name = string.match(data,"Host: ([0-9,\.]*)\n",1)
local url_file = string.match(data,"[^/]*\/([^ ?]*)[ ?]",1)
local uri = decodeURI(string.match(data,"[^?]*\?([^ ]*)[ ]",1))
-- parse GET parameters
GET={}
if uri then
for key, value in string.gmatch(uri, "([^=&]*)=([^&]*)") do
GET[key]=value
print(key, value)
end
end
-- parse POST parameters
local post = string.match(data,"([^]*)$",1):gsub("+", " ")
POST={}
if post then
for key, value in string.gmatch(post, "([^=&]*)=([^&]*)") do
POST[key]=decodeURI(value)
print(key, POST[key])
end
end
print("HTTP request:", data)
request_OK = false
-- if file not specified then send index.html
if url_file == `` then
sendfile.new(sck, `index.html`)
request_OK = true
else
local fext=url_file:match("^.+(%..+)$")
if fext == `.html` or
fext == `.txt` or
fext == `.js` or
fext == `.json` or
fext == `.css` or
fext == `.png` or
--fext == `.gif` or
fext == `.ico` then
if file.exists(url_file) then
sendfile.new(sck, url_file)
request_OK = true
end
end
-- execute LUA file
-- IT IS HAZARDOUS
if fext == `.lua` then
if file.exists(url_file) then
response=dofile(url_file)
sck:on("sent", function() sck:close() end)
sck:send(response)
request_OK = true
end
end
end
if request_OK == false then
sck:on("sent", function() sck:close() end)
sck:send(`Something wrong`)
end
end
if httpd then
httpd:listen(80, function(conn)
conn:on("receive", receive_http)
end)
end
Пример 8: Скачать файлы примера. необходимо залить файлы: web8.lua, index.html, form.html, post.lua запустить web-сервер, выполнив скрипт: web8.lua В браузере открыть ссылку: http://192.168.0.108/form.html |
Пример №9 (JSON)
Для создания современных web-приложний не обойтись без формата JSON. В NodeMCU есть модуль sjson который включает в себя функции, необходимые для перевода данных в строку JSON и для конвертации JSON строки обратно в объект с данными. JSON очень удобен и поддерживается многими языками программирования. Подробнее о JSON читайте здесь: https://ru.wikipedia.org/wiki/JSON. Прелесть работы с этим форматом заключается в том, что объект с данными можно преобразовать в JSON-строку, отправить или сохранить ее, затем считать или принять строку и преобразовать в объект с данными. Ниже приведен пример как данные конвертировать в JSON, сохранить в файл, прочитать данные с файла и конвертировать содержимое файла (JSON строку) в объект с данными.
Этот пример не использует "web-сервер", а демонстрирует работу с JSON, приемы передачи, обработка и хранение данных. Необходимо залить и выполнить скрипт json_test.lua
-- make object & set values
local obj = {}
obj.name=`Albert Fisher`
obj.phone=`+380673044742`
obj.wifi={}
obj.wifi.ssid=`WIFISSID`
obj.wifi.passwd=`password`
obj.list={`item one`, `item two`, -35, 712}
-- convert object to JSON content sctring
local json_str = sjson.encode(obj);
-- pring JSON
print(json_str);
-- rewrite JSON to file
if file.open("settings.json", "w") then
file.write(json_str)
file.close()
end
-- read JSON from file
if file.open("settings.json", "r") then
-- convert JSON to object
jobj = sjson.decode(file.read())
file.close()
end
-- print object values
print (jobj.name)
print (jobj.phone)
print(jobj.wifi) --incorrect
print(jobj.wifi.ssid)
print(jobj.wifi.passwd)
print(jobj.list[1])
print(jobj.list[3])
Пример 9: Скачать файл примера.
Сначала создается obj и заполняется данными. Как видите, данные могут быть разного формата.
local obj = {}
obj.name=`Albert Fisher`
obj.phone=`+380673044742`
obj.wifi={}
obj.wifi.ssid=`WIFISSID`
obj.wifi.passwd=`password`
obj.list={`item one`, `item two`, -35, 712}
Теперь преобразуем данные в JSON строку и выведем ее в консоль:
-- convert object to JSON content sctring
local json_str = sjson.encode(obj);
-- pring JSON
print(json_str);
Эту строку можно сохранить в файл. Таким простым образом мы можем сохранить все данные:
-- rewrite JSON to file
if file.open("settings.json", "w") then
file.write(json_str)
file.close()
end
Теперь проделаем обратную операцию. Считаем данные из файла, преобразуем в объект:
-- read JSON from file
if file.open("settings.json", "r") then
-- convert JSON to object
jobj = sjson.decode(file.read())
file.close()
end
Выводим данные в консоль:
-- print object values
print (jobj.name)
print (jobj.phone)
print(jobj.wifi) --incorrect
print(jobj.wifi.ssid)
print(jobj.wifi.passwd)
print(jobj.list[1])
print(jobj.list[3])
В результате работы скрипта был создан файл settings.json, в который записывались а затем считывались данные. Содержимое файла следующее:
{"phone":"+380673044742","wifi":{"ssid":"WIFISSID","passwd":"password"},"name":"Albert Fisher","list":["item one","item two",-35,712]}
Пример №10 (JSON & AJAX)
Теперь рассмотрим передачу данных с использованием JSON. Сделаем скрипт для нашего web-сервера, который будет формировать JSON и отдавать при запросе браузера: см. файл: get_json.lua
Пример 10: Скачать файлы примера. необходимо залить файлы: web8.lua, index.html, ajax.html, get_json.lua запустить web-сервер, выполнив скрипт: web8.lua В браузере открыть ссылку: http://192.168.0.108/get_json.lua |
См. скрипт get_json.lua:
-- make object & set values
if weather == nil then
weather = {}
weather.temperature=23.7
weather.humidity=58
end
weather.timer = tmr.now()
response="HTTP/1.0 200 OK\r\nServer: NodeMCU\r\nContent-Type: application/json\r\n\r\n"
response=response..sjson.encode(weather)
return response
В нем мы создаем объект weather, преобразовываем в строку и отправляем. Обратите внимание, что в отличии от предыдущих скриптов в этом мы формируем заголовок HTTP ответа и задаем Content-Type. По совести говоря, это надо делать всегда, но современные браузеры не особо требовательны к Content-Type, если удается распознать тип контента, например, по расширению файла. В нашем случае lua файл формирует JSON, Content-Type нужно указывать обязательно.
Теперь будем использовать скрипт get_json.lua из html страницы с помощью Java скрипта. См. файл http://192.168.0.108/ajax.html
<html>
<title>NodeMCU</title>
<body>
<h1>NodeMCU AJAX</h1>
<hr>
Temperature: <div id="temperature"></div>
Humidity: <div id="humidity"></div>
Timer: <div id="timer"></div>
<script>
id = 0;
refresh_data();
function refresh_data() {
loadJSON(`get_json.lua`, function(response) {
var json_obj = JSON.parse(response);
document.getElementById(`temperature`).innerHTML = json_obj.temperature;
document.getElementById(`humidity`).innerHTML = json_obj.humidity;
document.getElementById(`timer`).innerHTML = `<b>`+json_obj.timer+`</b>`;
clearTimeout(id);
id = setTimeout("refresh_data()",1000);
});
}
function loadJSON(json_url, callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open(`GET`, json_url, true);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
};
xobj.send(null);
}
</script>
</body>
</html>
В этом файле сделано периодическое обновление данных на HTML странице без перезагрузки всей страницы.
Пример №11 (AngularJS)
В последнее время для сокращения времени разработки больших проектов стали использовать различные Framework-и, все реже пишут на чистом JavaScript.
В файле http://192.168.0.103/angular_autorefresh.html пример периодического обновления данных на странице без перезагрузки самой страницы, но уже средствами AngularJS.
<html>
<title>NodeMCU</title>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<script>
var app = angular.module("myApp", []);
app.controller("myCtrl", function($http, $scope, $interval) {
$scope.get_data = function() {
$http.get(`get_json.lua`).then(function(response) {
$scope.data = response.data;
});
}
$scope.get_data();
$interval($scope.get_data, 1000);
});
</script>
<h1>NodeMCU Auto refresh (angular)</h1>
<hr>
<div ng-app="myApp" ng-controller="myCtrl">
Temperature: <div>{{data.temperature}}</div>
Humidity: <div>{{data.humidity}}</div>
Timer: <div><b>{{data.timer}}</b></div>
</div>
</body>
</html>
Пример 11: Скачать файлы примера. необходимо залить файлы: web8.lua, index.html, angular_autorefresh.html, get_json.lua запустить web-сервер, выполнив скрипт: web8.lua В браузере открыть ссылку: http://192.168.0.108/angular_autorefresh.html |
Пример №12 (JSON + Форма)
Рассмотрим пример более приближенный к практике. У нас есть форма в которой, нужно изменять и сохранять данные. Данные, разумеется, будут сохранятся в файле на файловой системе NodeMCU в виде JSON файла. При открытии ссылки http://1092.168.0.104/form2.html JavaScript запросит файл settings.json, разберет данные и заполнит поля формы. После редактирования данных и нажатия кнопки "Submit" данные будут отправлены скрипту post2.lua методом POST:
response="HTTP/1.0 200 OK\r\nServer: NodeMCU\r\nContent-Type: text/html\r\n\r\n"
response=response.."It`s LUA file response"
if POST[`name`] ~= nil then
response=response.."<p> Parameter `name` is <b>"..POST[`name`].."</b></p>"
response=response.."<p> Parameter `phone` is <b>"..POST[`phone`].."</b></p>"
-- save data to JSON file
local json_str = sjson.encode(POST);
if file.open("settings.json", "w") then
file.write(json_str)
file.close()
end
end
return response
Скрипт преобразует данные в строку JSON и сохранит в файл settings.json. Круг замкнулся.
Пример 12: Скачать файлы примера. необходимо залить файлы: web8.lua, index.html, form2.html, post2.lua, settings.json запустить web-сервер, выполнив скрипт: web8.lua В браузере открыть ссылку: http://192.168.0.108/form2.html |
Пример №13 (JSON + Форма + AngularJS)
Теперь сделаем тоже самое, но средствами AngularJS. Данные теперь отправляются скрипту post_json.lua. Обратите внимание, что все данные формы преобразуются в JSON строку, которая отправляется POST методом в единственном параметре data. Скрипт post_json.lua:
response="HTTP/1.0 200 OK\r\nServer: NodeMCU\r\nContent-Type: application/json\r\n"
if POST[`data`] ~= nil then
local data = sjson.decode(POST[`data`])
if data.name == nil or data.name == `` then
response=response..`{"result": "Error. Name is empty"}`
else
if data.phone == nil or data.phone == `` then
response=response..`{"result": "Error. Phone is empty"}`
else
-- save data to JSON file
if file.open("settings.json", "w") then
file.write(POST[`data`])
file.close()
end
response=response..`{"result": "OK"}`
end
end
end
return response
Поскольку перезагрузка страницы с формой не предполагается, скрипт возаращает ответ, чтобы можно было сообщить пользователю результат выполнения операции.
Пример 13: Скачать файлы примера. необходимо залить файлы: web8.lua, index.html, angular.html, post_json.lua, settings.json запустить web-сервер, выполнив скрипт: web8.lua В браузере открыть ссылку: http://192.168.0.103/angular.html |
P.S. При всем моем уважении к модулям ESP, не могу утверждать, что работа с ним мне приносила сплошное удовольствие. Стабильность их работы совместно c NodeMCU оставляет желать лучшего. При всем богатстве функционала, который дает NodeMCU, не стоит забывать, что ESP8266 - это всего лишь микроконтроллер. И ему под силу задачи, свойственные микроконтроллерам но не более. Давайте это не забывать. Надеюсь эта статья поможет Вам разобраться с Web-интерфейсом для ESP, и найти более изящные решения.
Успехов.
Дивись також:
- ESP8266 NodeMCU Перше знайомство. Робимо WiFi розетку
- ESP8266 NodeMCU. PWM
- ESP8266 NodeMCU. ADC
- ESP8266 NodeMCU. timer, rtc, SNTP, cron
- ESP8266 NodeMCU. файлова система, SD Card
- ESP8266 NodeMCU. UART
- GPS-трекер на базі ESP8266
- GPS-трекер + Дисплей SSD1306
- ESP8266 NodeMCU. SSD1306. U8G
- ESP-01 (ESP8266) upgrade flash memory to 4MB
- ESP8266 NodeMCU. I2C. BME280/
- Метеостанція на ESP8266
Недавні записи
- Text to speech. Українська мова
- LCD Display ST7567S (IIC)
- Розпізнавання мови (Speech recognition)
- Selenium
- Комп'ютерний зір (Computer Vision)
- Деякі думки про точність вимірювань в електроприводі
- Датчики Холла 120/60 градусів
- Модуль драйверів напівмосту IGBT транзисторів
- Драйвер IGBT транзисторів на A316J
- AS5600. Варіант встановлення на BLDC мотор
Tags
docker sensors rtc led timer wifi remap sms css websocket mongodb bme280 rfid bluetooth esp8266 nodemcu bkp piezo bmp280 i2c mpu-6050 encoder examples avr brushless ngnix nvic displays bldc java-script pmsm barometer pwm lcd ethernet stm32 raspberry-pi capture usart gpio exti atmega meteo solar smd dma adc eeprom eb-500 flask python ssd1306 uart options flash mpx4115a html gps st-link 3d-printer servo dc-dc foc git books battery hih-4000 watchdog dht11 web tim programmator ssd1331 mpu-9250 usb rs-232 motor max1674 soldering
Архіви