Умный дом на ESP8266+MajorDoMo. Часть 2. Написание скриптов NodeMCU для работы с HC-SR501.

В прошлой части мы закончили на заливке прошивки NodeMCU в платку ESP-12E. В этой поговорим о непосредственном программировании чипа.

О чипе ESP8266 есть два мнения — хорошее и плохое. Пессимисты уверены в том, что данный чип ни на что не годится, оптимисты делают на нем WiFi-розетки и довольны как слоны. Перечитав множество статей и форумов, я пришел к выводу, что этот DIY-чип исключительно для домашних целей. Причем в разовых, т.е. если обвешать дом множеством таких чипов — они начнут глючить, а небольшое количество работает нормально. Как говорится, лучше 1 раз увидеть, чем 100 раз услышать. Сразу повешать 15-20 в работу чипов у меня нет возможности, но постепенно за разработками умного дома буду увеличивать их количество, а там увидим 🙂

На данный момент у меня есть уверенность в том, что если не нагружать чип на 100%, т.е. не использовать на нем ПИД-регуляторы для управления марсоходами и тому подобные вещи, а просто снимать показания с датчиков, дискретно управлять исполнительными механизмами в зависимости от показаний, то не будет никаких PANIC ERROR 🙂 Да и сторожевой таймер еще никто не отменял.

Итак, NodeMCU является интерпретатором языка LUA, т.е. можно заливать и выполнять скрипты не перезагружая модуль. В этом нам поможет замечательная программка ESPlorer. Через нее можно даже можно давать команды чипу непосредственно через терминал, не заливая скрипты в модуль, что очень удобно.

Про NodeMCU + LUA могу сказать, что очень непривычно. Я программировал на СИ разные контроллеры, и этот язык мне очень дорог. LUA — это невесть что ))) Синтаксис, асинхронность выполнения, и что самое главное — начало счета не с «0», а с «1» ломает мозг. Но, приноровившись, можно поймать дзен, и работать ))) На что только не пойдешь ради отвязки от внешнего контроллера, уменьшения габаритов устройства и его стоимости.

Одна из главных ньюансов — асинхронность. Суть заключается в том, что некоторые функции могут выполняться долго, но программа на них не заканчивается, а продолжает работать дальше, как бы в отдельном потоке. Поэтому если критично, чтобы одна функция выполнялась после полного завершения второй — необходимо использовать callback’и — функции обратной связи. А учитывая, что непонятно, какая функция выполняется быстро, а какая медленно — использовать функции ОС нужно всегда и везде, а показания датчиков снимать не в суперцикле, а по-циклическому таймеру.

Приступим. Делать мырг-мырг и «hello world» я не буду, этого и так хватает в интернете, займусь сразу полезными делами, а именно подключением преобразователя движения HC-SR501 непосредственно к ESP-12E, который будет передавать данные на сервер с установленной системой MajorDoMo.

Расскажу немного про структуру программы, которую я выбрал для себя. Программа будет состоять не из 1 файла (скетча), а из 4. Почему — можно почитать тут и тут. Вкратце — ESP8266  имеет ограниченное количество оперативной памяти для исполнения скриптов. Причем в процессе работы при неграмотном написании скрипта память может постепенно уменьшаться. Поэтому лучше разбить программу на несколько файлов, и при необходимости подгружать их, а после их выполнения — убирать за ними мусор, что позволит удалять из памяти неиспользуемые переменные, и тем самым освобождать память.

Структура моей программы такова:

  1. init.lua — автозагрузка модуля. Первый файл, который загружается в память — без него ESPlorer будет выдавать ошибку «lua: cannot open init.lua» — на ней мы закончили в конце первой части. В принципе все действие может происходить в нем, но я для основного цикла программы создал 2 файл — main.lua
  2. main.lua — основной файл выполнения программы.
  3. config.lua — файл общих настроек. Тут будут храниться логины/пароли от WiFi, при необходимости IP модуля, и прочие глобальные данные
  4. HCSR501.lua — «библиотека» модуля HC-SR501. Основная функция — чтение статуса датчика.

Если подключить чип с залитыми скриптами к компьютеру и нажать кнопку RELOAD — мы увидим все файлы, загруженные в чип.

Почему я решил писать код не в init.lua, а в отдельном файле main.lua — для автоматизации обновления прошивки в будущем. На данный момент файл init.lua подгружает в себя файл config.lua и main.lua. Причем именно в таком порядке, т.к. файл сразу выполняется. Если мы сперва загрузим main.lua, то программа не увидит данных из config.lua, что вызовет ошибку выполнения. Выполнение кода вне файла init.lua позволит нам использовать его в качестве загрузчика скриптов. В дальнейшем можно будет поставить задержку выполнения основной программы на 5 секунд, и если за это время пришел специальный POST-запрос, можно будет открыть любой из файлов, или же создать новый — и залить в него данные из POST-запроса. А заливать прошивку в файл init.lua (читай сам в себя во время выполнения себя же) — думаю ничего хорошего из этого не выйдет.

Код файла init.lua:

dofile("config.lua") -- подгружаем файл config.lua
dofile("main.lua")   -- подгружаем основной файл программы main.lua

Код файла config.lua:

-- Для дальнейшего удобства лучше хранить данные в таблице, а не в переменной.
-- При изменении какого-либо значения по-воздуху POST-запросом
-- удобнее будет перезаписывать данные.
-- Для редактирования в ESPlorer тип значений может быть любой

config = {}
config.LanSSID = "ssid"              -- название wifi-сети
config.LanPWD = "password"           -- пароль wifi-сети
config.LanServer = "192.168.1.15"    -- IP-адрес сервера с установленной системой MajorDoMo

Код файла main.lua:

-[[ Конфигурируем чип как клиента, присоединяемся к существующей сети WiFi
     Присоединяемся к сети WiFi
     При неудачном соединении создаем свою точку доступа 192.168.1.200
     При удачном - поднимаем веб-сервер, выводим список доступных дочек доступа
     Опрашиваем датчик HC-SR501 каждые 500 мс

]]


dofile("HCSR501.lua")

connect=0   --статус присоединеия к существующей сети WiFi
action=0    --для однократной обработки события

-- поднимаем точку доступа
function setSOFTAP()
    local cfg={}
    cfg.ssid="ApESP8266"
    cfg.pwd="password"

    wifi.setmode(wifi.SOFTAP)
    wifi.ap.config(cfg)
    print(wifi.ap.getip())

end

-- Проверка на соединение с точкой доступа (проверка получения IP)
function getConnect()
    if wifi.sta.getip() ~= nil then      -- если IP-адрес получен (не равен NIL)
        connect = 1     -- ставим статус получения адреса
        print("IP-адрес получен: "..wifi.sta.getip())
        startWork()       -- запускаем функцию начала цикла работы
    else
        connect = 0
        print("IP-адрес не получен, поднимаем точку доступа")
        setSOFTAP()         -- если присоединиться не удалось, поднимаем свою точку доступа
        
    end
end

-- Конфигурируем чип как клиента, присоединяемся к существуюзей сети WiFi
-- Заводим таймер на 5 сек. Если по прошествии этого времени не получили IP?
-- то поднимаем свою точку доступа, свой веб-сервер
wifi.setmode(wifi.STATION)          -- конфигурируем чип как клиент
wifi.sta.autoconnect(1)             -- включаем автоприсоединение к сети
wifi.sta.config(config.LanSSID, config.LanPWD, true)    -- присоединяемся к сети, сохраняем настройки конфигурации во FLASH
tmr.alarm(0, 5000, tmr.ALARM_SINGLE, getConnect)   --заводим таймер на 5 секунд



function startWork()
    -- поднимаем сервер на 80 порту с таймаутом 30 сек
    srv = net.createServer(net.TCP, 30)
    -- формируем ответ из нескольких строк клиенту и отправляем
    function receiver(sck, data)
        
        print(data)
        
        local response = {}
    
        response[#response + 1] = "<html><body style=\"font-family: Verdana, Arial, Helvetica, sans-serif;\"><center>"
        response[#response + 1] = "<h1>Ap SmartHome</h1> <br/>На данный момент action="..read_HCSR501()
        response[#response + 1] = "</center></body></html>"
    
        -- отправляем и удаляем первый элемент из таблицы response, пока не отправим все
        local function send(localSocket)
            if #response > 0 then       --пока длина таблицы response не равна нулю
                localSocket:send(table.remove(response, 1))
            else
                localSocket:close()     -- если все отправили - закрываем сокет
                response = nil          -- удаляем переменную "response"
            end
        end
    
        -- triggers the send() function again once the first chunk of data was sent
        sck:on("sent", send)    --если отправили посылку - отправляем еще раз
        send(sck)
    end
    
    -- Мониторим 80 порт. Если есть запрос от клиента - выполняем функцию "receiver"
    if srv then
        srv:listen(80, function(conn)
            conn:on("receive", receiver)
        end)
    end
    
    -- Отправка данных о тревоге на сервер MajorDoMo
    local function alarmMotion()
        print("АЛЯРМА!!! Обнаружено проникновение!!!")  -- печатаем сообщение о тревоге
        srv = net.createConnection(net.TCP, 0)  -- создаем новое TCP-соединение
        srv:connect(80,config.LanServer)          -- коннектимся к серверу с установленным MajorDoMo
        srv:on("connection", function(sck, c)   -- если соединение установлено, отправляем данные на сервер
        sck:send("GET /objects/?object=motionKitchen&op=set&p=alarm&v=1 HTTP/1.1\r\nHost: "..config.LanServer.."\r\nConnection: keep-alive\r\nAccept: */*\r\n\r\n")
        end)
    end



    
    -- Функция опроса датчика HCSR501
    -- т.к. минимальное время импульса датчика равна 3 секундам, а мы опрашиваем его каждые 0,5 секунды
    -- то при сработке датчика мы успеем отправить на сервер 4...6 сообщений о тревоге.
    -- чтобы иэтого извежать, заводим переменную action, которую будем устанавливать в лог. 1 при тревоге, а в лог. 0
    -- только после того, как датчик установит на своем выходе лог. 1
    local function HCSR501()
        if read_HCSR501()==1 then
            if action==0 then
                print(node.heap())
                alarmMotion()
                action=1
            end
        else
            action=0
            collectgarbage()
        end
        tmr.wdclr()
    end
    
    -- Таймер на опрос HCSR501 каждые 0,5 секунды
    tmr.alarm(0, 500, tmr.ALARM_AUTO, HCSR501)
end

Код файла HCSR501.lua:

local pin=1  -- создаем локальную переменную, назначаем ей номер выхода чипа
             -- к данной ножке и подключаем выход преобразователя движения HC-SR501

gpio.mode(pin,gpio.INPUT)  -- конфигурируем пин как выход
gpio.mode(pin,gpio.LOW)    -- с подтяжкой к земле

--- Функция опроса датчика. Просто возвращает состояние сконфигурированной ножки
function read_HCSR501()
return gpio.read(pin)
end

Расскажу немного о функции отправки данных клиенту (которые отобразятся у него в браузере). Нам необходимо вывести на экран небольшую информацию о статусе преобразователя движения. В дальнейшем можно будет добавить его настройку (включить/отключить слежение) и прочее, но пока остановимся на этом. Нет ничего проще вывести всю информацию одной строкой:

<html><body style=\"font-family: Verdana, Arial, Helvetica, sans-serif;\"><center><h1>Ap SmartHome</h1> <br/>На данный момент action="..read_HCSR501()</center></body></html>

Но ресурс памяти у ESP8266 ограничен. В строке выше этим можно пренебречь, т.к. выводимых данных немного. Но если повешать на чип еще несколько датчиков, да добавить таблицу стилей для красивого отображения, память улетучится. Поэтому выводить данные лучше порциями, постепенно.

За вывод данных пользователю отвечает функция net.socket:send(). На старых версиях SDK можно было использовать эту функцию несколько раз, друг за другом, разбивая выводимые данные на части. В новых версиях SDK функция работает только первый раз, потом закрывает соединение что ли, не знаю физику процесса точно. Но если написать несколько функций подряд — выполнится только первая.

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

Опрос датчика движения происходит каждые 500 мс. При обнаружении движения создаем соединение к серверу с установленной системой МДМ, и отправляем GET-запросом данные о срабатывании датчика. Т.к. запрос содержит данные объектов и свойств системы MajorDoMo, то об этом будет рассказано в следующей части эпопеи 🙂

Теперь можно ввести в браузере адрес нашего чипа (мне DHCP выдал адрес 192.168.10.23), и увидеть состояние датчика. При обнаружении движения состояние переменной action будет равняться 1, при тишине и спокойствии — 0.

Ну и при сработке датчика в терминале можно будет увидеть сообщение о тревоге:

В данном коде есть функции, которые не используются. Например при неудачной попытке соединения с роутером (например, неправильный пароль) чип ESP8266 сам поднимает точку доступа. Для чего? В дальнейшем можно написать функцию вода правильного пароля через веб-морду, для удобства. На данный момент мне это не надо, но когда я установлю это устройство в подрозетник/потолок, не важно куда, и решу сменить SSID или пароль своей сети, не надо будет лезть с ноутом под потолок, по UART перезаливать файл config.lua. Мне достаточно будет просто зайти на адрес модуля (в настройках точки доступа мы сами можем указать адрес), ввести в форму данные от сети, и сохранить их. Но это уже не тема данной статьи, поэтому решил не нагромождать код.

Комментарии 7

  • И как всегда вовремя! Прекрасные уроки. Именно такой помощи сейчас и не хватало. Хотелось бы очень увидеть связку Веб-сервер UART, чтобы нажимая на кнопочку или ставя единичку на сайте по UART убегало соответствующее сообщение «:LCDINI» и тому подобное). Буду рад видеть снова статью.

  • Под веб-сервером вы имеете в виду сервер, поднятый на самой ESP8266? Чтобы при нажатии на кнопку данные в форме отправлялись по UART?

  • Ага, а много ли надо? На ней памяти хватит чтобы разместить классические кнопочки и текстбоксы)

  • Будем ждать), иначе никак)

  • Емаё а все-же надо быть внимательнее print(«:LCDINI»); А ищу то что лежит совсем под носом). Ну что с тебя статья) жду не-дождусь! А я пока буду калхозить уж руки чешутся)

  • Здравствуйте!
    подскажите чайнику в чём проблема
    в config.lua указал ssid своей сети, но он ругается.
    dofile(«main.lua»)
    main.lua:46: bad argument #1 to ‘config’ (config table not found!)
    stack traceback:
    [C]: in function ‘config’
    main.lua:46: in main chunk
    [C]: in function ‘dofile’
    stdin:1: in main chunk

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.