Как сделать Web-интерфейс для ESP8266 под NodeMCU


19.01.2018

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
Коментарі:
Додати коментар
Code
* - обов'язкові поля

Архіви