ESP-8266 Lua: азы программирования NodeMCU.

Тема в разделе "ESP8266, ESP32", создана пользователем ИгорьК, 25 июл 2017.

  1. ИгорьК

    ИгорьК Давно здесь

    Скелет программы IoT ч.3.4. Плюс полив огорода.
    В предыдущем топике обещал упаковать все в программу и закончить устройство. Но, подумалось - а чем еще можно догрузить бедный ESP-8266?
    O! Будем поливать огород! Пусть наша маленькая железка пищит от напряжения!
    Полив огорода будет таким:
    • два раза в день - с восходом и на закате;
    • основное управление - от внутренних часов модуля, резервное - от датчика освещенности;
    • можно задавать интервалы дней полива как утром так и в ночь.
    Интервалы - вещь достаточно важная. Например, помидоры во время вызревания плодов поливают один раз в три дня. А вот во время роста - хоть каждый день да по два раза.

    Полив можно делать разный, но, ИМХО, последнее время популярным становится капельный (иногда - сочащийся). Чем он хорош? Вода медленно струится по трубкам - не требуется емкость для ее нагрева. Какова бы ни была температура на входе системы полива - под корень растения она попадет с уличной температурой. Главное здесь не забыть сбросить давление в системе полива - не более 1 Атм.

    Кран у нас будет шаровый, типа такого. Он управляется двумя проводниками: подаем на один 12 вольт на 30 секунд - открываем, на другой - закрываем.
    [​IMG]

    В общем, с добавлением этой функции у нас появляется некислая смесь бульдога с носорогом, а модуль начинает трещать по швам:
    upload_2018-7-9_16-25-55.png

    Вот такое семейство файлов там нынче трудится. В этой конфигурации модуль уже не принимает команды через терминал: при превышении некоторого количества/объема терминал "ломается".
    В целом не мешает жить для готового устройства, но настраивать его в терминале уже становится невозможно.

    Но, проект наш учебный, не так ли? Доведем модуль "до изнеможения": в момент обработки команд от брокера его куча уменьшается до 7000 попугаев! И ничего, работает!

    Кстати, так выглядит вывод MqttSpy:
    upload_2018-7-9_16-50-26.png

    В общем, описание законченного устройства опять откладывается, раскрываю суть работы полива.

    На полив работают три файла:
    • analize.lua - ясный перец, принимает от брокера команды на установку интервалов полива и на немедленные запуск/прекращение полива:
    • garden.lua - непосредственно решает про полив;
    • turnvalve.lua - открывает и закрывает кран.
     
    Последнее редактирование: 10 июл 2018
  2. ИгорьК

    ИгорьК Давно здесь

    Как работают скрипты?
    analyze.lua - там все типовое, парсит команду и дает указания. Итоговый файл будет выдан позже, по причине того, что обслуживает все части программы.

    Кусок analyze.lua:
    Код (Lua):
     -- ...
          elseif top == "water" then
                if dt == "ON" then
                    dat.startwater = 1
                    print("Watering Now")
                end
                if dt == "OFF" then
                    dat.startwater = 0
                    print("Stop Water Now")
                end
            elseif top == "day" then
                dat.intervalDay = tonumber(dt) or 0
                dat.lastDay = 0
                print('Set dat.intervalDay at '..dat.intervalDay)
                dofile('savedata.lua')
                table.insert(topub, {'intervalDay', dat.intervalDay})
            elseif top == "night" then
                dat.intervalNgt =  tonumber(dt) or 0
                dat.lastNgt = 0
                print('Set dat.intervalNgt at '..dat.intervalNgt)
                dofile('savedata.lua')
                table.insert(topub, {'intervalNgt', dat.intervalNgt})
            -- ...

    Далее:
    Код (Lua):
        -- Измеряем освещенность
        dat.lux = 1024 - adc.read(0)
        table.insert(topub, {'lux',dat.lux})
        print("lux: "..dat.lux)
        -- Узнаем который час
        local tm = rtctime.epoch2cal(rtctime.get()+3*60*60)
        --print(string.format("%02d:%02d:%02d", tm.hour, tm.min, tm.sec))
        -- Если с часами все нормально и знаем время заката и рассвета
        if tm.year ~= 1970 and dat.setH ~= 100 and dat.riseH ~= 100 then
            -- Время минут сейчас
            local tmnow = tm.hour * 60 + tm.min
            -- Время минут заката
            local nght = dat.setH * 60 + dat.setM
            -- Время  минут рассвета
            local day = dat.riseH * 60 + dat.riseM

            -- Сейчас день или ночь по времени?
            if tmnow > nght and dat.now == 'Day' then
                dat.now = 'Night'
            elseif tmnow > day and dat.now == 'Night' then
                dat.now = 'Day'
            end
        else
            -- Плохо со временем -  узнаем по освещенности
            if dat.lux > 100 and dat.now == 'Night' then
                dat.now = 'Day'
            elseif  dat.lux < 5  and dat.now == 'Day' then
                dat.now = 'Night'
            end

        end
        print('Now is '..dat.now)

        -- Если это включение устройства (устанавливается при старте)
        if dat.powerON  then
            dat.powerON = nil
            -- выключаем полив а всякий случай
            dat.startwater = 0
            -- Типа решение о поливе уже принято
            if dat.now == 'Day' then
                dat.makedesDay = 1
            else
                dat.makedesNgt = 1
            end
        end
        -- Сейчас день и мы не принимали решени о поливе
    if dat.now == 'Day' and dat.makedesDay == 0 then
        -- День наступил - сбрасываем флаг принятия ночных решений
        dat.makedesNgt = 0
        -- Соотносим с количеством дней интервала между поливами
        -- Если выждали нормально
        if dat.lastDay >= dat.intervalDay then
            -- Поливаем
            dat.lastDay = 0
            print('Water at Day!')
            dat.startwater = 1
        else
            -- Иначе не поливаем но добавляем день
            dat.lastDay = dat.lastDay + 1
            print('Day Interval:', dat.lastDay)
            dofile('savedata.lua')
        end
        -- Оповещаем о решении брокер
        table.insert(topub, {'lastDay', dat.lastDay})
        -- Выставляем флаг что все решено
        dat.makedesDay = 1
    end

    -- Ночью действуем аналогично дневному алгоритму

    if dat.now == 'Night' and dat.makedesNgt == 0 then
        dat.makedesDay = 0
        if dat.lastNgt >= dat.intervalNgt then
            dat.lastNgt = 0
            print('Water at Night!')
            dat.startwater = 1
        else
            dat.lastNgt = dat.lastNgt + 1
            print('Night interval:', dat.lastNgt)
            dofile('savedata.lua')
        end
        table.insert(topub, {'lastNgt', dat.lastNgt})
        dat.makedesNgt = 1
    end
    -- Если есть хоть какое-то значение dat.startwater:
    if dat.startwater then
        dofile('turnvalve.lua')
    end

    tm, tmnow, nght, day = nil, nil, nil, nil

    И кран:
    Код (Lua):
    --
    do
        print('turnvalve works.')
        -- Этот файл просто так не вызывается!
        -- Раз вызвали - прекращаем подачу питания на любые ноги крана, если она есть.
        if dat.vlpowered then
            gpio.write(pins.open, gpio.HIGH)
            gpio.write(pins.close, gpio.HIGH)
        end
        -- Если есть таймер подачи питания на ноги крана - убиваем
        if dat.powerpin then
            dat.powerpin:stop()
            dat.powerpin:unregister()
            dat.powerpin = nil
        end

        -- Функция подачи питания на ногу крана и удержания ее там 30 секунд
        local function runpin(pin)
            --Есть питание на ноге
            dat.vlpowered = true
            -- Подаем электричество
            gpio.write(pin, gpio.LOW)
            print('ON pin '..pin)
            -- Создаем таймер отключения питания
            dat.powerpin = tmr.create()
            dat.powerpin:register(30000, tmr.ALARM_SINGLE, function(tm)
                -- Отключаем питание
                gpio.write(pin, gpio.HIGH)
                dat.vlpowered = nil
                tm = nil
                dat.powerpin = nil
                print("OFF by 30 sec. pin "..pin)
            end)
            -- Запуск таймера
            dat.powerpin:start()
        end

        -- При вызове файла проверяем зпись в таблице о том открывать или закрывать кран

        -- Если открывать
        if dat.startwater == 1 then
            -- Открываем
            runpin(pins.open)
            -- Оповещаем брокер
            table.insert(topub, {'iswatering',"ON", 1})

            -- Если не запущен таймер закрытия крана - создаем
            if not dat.tmrpoliv then
                dat.tmrpoliv = tmr.create()
                -- Определяем когда закрыть
                dat.tmrpoliv:register(dat.poliv * 1000, tmr.ALARM_SINGLE, function(t)
                    -- Оповещаем брокер
                    table.insert(topub, {'iswatering',"OFF", 1})
                    -- Шлем питание на ногу
                    runpin(pins.close)
                    t = nil
                    dat.tmrpoliv = nl
                end)
                -- Запускаем таймер
                dat.tmrpoliv:start()
            end

        end

        -- Если закрыть
        if dat.startwater == 0 then
            print("Got stop watering!")
            -- Если работает таймер полива - убиваем
            if dat.tmrpoliv then
                dat.tmrpoliv:stop()
                dat.tmrpoliv:unregister()
                dat.tmrpoliv = nil
            end
            -- Оповещаем брокер
            table.insert(topub, {'iswatering',"OFF", 1})
            -- Шлем питание на ногу закрытия крана
            runpin(pins.close)
        end
        dat.startwater = nil
    end

    Все. В следующий раз точно поясню как это безмерное количество файлов собирается в одну программу. И мы организуем прикол: запустим "умный дом на ардуино ESP-8266" :)

    upload_2018-7-9_17-29-19.png
     
    Последнее редактирование: 10 июл 2018
  3. ИгорьК

    ИгорьК Давно здесь

    Скелет программы IoT ч.4. Вся программа.
    Наконец, настало время собрать воедино наши кубики.

    Вспомним, что Lua от NodeMCU - асинхронный код и заботиться о согласовании событий особо нет необходимости. То есть, например, мы можем создать таймер и раз в минуту выполнять такую функцию:
    Код (Lua):
    function dispatch()
        dofile('getsun.lua') -- Получили время закат/рассвет
        dofile('lightnow.lua') -- Управляем освещением
        dofile('scedset.lua') -- Проверяем расписание для отопителя
        dofile('temper.lua') -- Управляем отопителем
        dofile('garden.lua') -- Управляем поливом
        dofile('makepubl.lua') -- Готовим и публикуем данные на брокер
    end
    tmr.create():alarm(60000, 1, dispatch)
    Это должно работать, но следует учесть две особенности.

    1. Если у нас присутствует асинхронный код, то обычный, "линейный" код будет выполняться без ожидания результатов асинхронного.

    В данном случае у нас:
    getsun.lua - добывает данные о солнце в асинхроне. lightnow.lua начнет работать без его результатов.
    temper.lua - имеет задержку на чтение датчиков температуры до 750 мс, следовательно garden.lua проскочит без температуры (но она для него не важна), а вот makepubl.lua точно не увидит результатов работы термометра и управления отопителем в целом, а это уже плохо.

    Мы с вами уже знаем как решить проблему асинхрона - он всегда имеет callback функцию и в нее можно втолкнуть, например, тот же dofile('....lua').

    Для обычного случая так и рекомендуется делать. Вот пример. В getsun.lua затолкаем lightnow.lua:
    Код (Lua):
    local offset = 3
    -- 'api.sunrise-sunset.org/json?lat=55.75583&lng=37.61778'
    local function rep(dt)
        print(dt..': Sunset at '..dat.setH..':'.. dat.setM, ' Sunrise at '..dat.riseH..':'..dat.riseM)
        table.insert(topub, {'sunset',dat.setH..':'.. dat.setM})
        table.insert(topub, {'sunrise',dat.riseH..':'..dat.riseM})
        setH, setM, riseH, riseM, request, makeDig, ttmm, offset, rep = nil, nil, nil, nil, nil, nil, nil, nil, nil

        ------------  Втыкаем: -------------------
        dofile('lightnow.lua')
        -------------------------------
    end

    if dat.setHMcount < 10 then
        dat.setHMcount = dat.setHMcount + 1
        rep('Just')
    else
        local setH, setM, riseH, riseM, makeDig
        local request = "GET /json?lat=55.75583&lng=37.61778&formatted=0 HTTP/1.1\r\n"..
            "Host: api.sunrise-sunset.org\r\n\r\n"
        conn=net.createConnection(net.TCP, 0)
        conn:on("connection", function(conn, payload) conn:send(request) end)
        conn:on("receive", function(conn, payload)
            local ttmm = string.sub(payload,string.find(payload,'sunrise')+20,string.find(payload,'sunset')+24)
            payload = nil
            riseH, riseM, setH, setM = string.match(ttmm,'(%d+):(%d+).+sunset.+T(%d+):(%d+)')
            makeDig = function(dig, trans)
                dig = tonumber(dig)
                if trans then
                    dig = (dig + offset < 24) and (dig + offset) or (dig - 21)
                end
                return dig
            end
            if setH and setM then
                dat.setH = makeDig(setH, true)
                dat.setM = makeDig(setM)
            end
            if riseH and riseM then
                dat.riseH = makeDig(riseH, true)
                dat.riseM = makeDig(riseM)
            end
            dat.setHMcount =  0
            rep('Got')
            conn:close()
            end)
        conn:connect(80, 'api.sunrise-sunset.org')
    end

    Отлично! Но, решив одну проблему мы продили другую: теперь getsun.lua и lightnow.lua стали асинхронными, и тот же makepubl.lua не увидит итогов их работы.
    Да, ладно - справимся! Воткнем и его в callback:
    Код (C++):
    --...
    local function rep(dt)
        print(dt..': Sunset at '..dat.setH..':'.. dat.setM, ' Sunrise at '..dat.riseH..':'..dat.riseM)
        table.insert(topub, {'sunset',dat.setH..':'.. dat.setM})
        table.insert(topub, {'sunrise',dat.riseH..':'..dat.riseM})
        setH, setM, riseH, riseM, request, makeDig, ttmm, offset, rep = nil, nil, nil, nil, nil, nil, nil, nil, nil

        ------------  Втыкаем: -------------------
        dofile('lightnow.lua')
        dofile('makepubl.lua')
        -------------------------------
    end
    -- ...
     
    Все! Проблема решена!
    Стоп! Забыли про температуру. Но ее тоже можно отвесить в каллбэки таким же образом.

    То есть через правильное прописывание очередного dofile('... .lua') в предыдущем проблема решается.
    И так можно делать. Да более того - так и делается, в основном.

    Продвинутый вариант - продумать и разделить программу на асинхронные и последовательные куски кода. Аснхрон(ы) вызывать заранее и кидть результаты в таблицу, а потом, через некоторое время, по окончанию работы самого "тугого" асинхрона callback вызывает все остальное. Тоже работает.

    2. Но тут вступает в работу вторая особенность: в тяжелых случаях может банально не хватить памяти.
    Продвинутые пользователи тут же вспомнят о функции collectgarbage ( ) - и правильно.
    Но,
    • для ее работы тоже нужно "место";
    • для ее работы тоже нужно "время".
    Втыкание ее в последовательный код мало что дает. Вот если "оттормозиться" - тогда функция отработает хорошо. Однако, при любой паузе в линейном коде система сама запускает освобождение памяти и без нашего напоминания collectgarbage ( ) .

    Как быть? Есть неспешный вариант! (А куда нам спешить - мы все равно исполняем наши файлы раз в минуту, а можно и реже - чего "забивать эфир" брокера повторяющейся информацией!)

    Вспомним, таблицы в Lua - гора самоцветов. Записью таблицы может быть все что угодно, и даже - функция. Мы навтыкаем в таблицу последовательность вызовов файлов на исполнение и будем неспеша, каждые три секунды вызывать.

    Почему три? По наблюдению, вызов getsun (самый длинный) занимает до 1.5 секунд. Еще столько-же хватит системе подчистить память.

    Вот как это выглядит:
    Код (Lua):
    dispatch = function()
        -- Таблица функций для вызова
        threads = {
            function() dofile('getsun.lua') end,
            function() dofile('lightnow.lua')end,
            function() dofile('scedset.lua')end,
            function() dofile('temper.lua')end,
            function() dofile('garden.lua')end,
            function() dofile('makepubl.lua')end
        }
        -- Счетчик шагов вызова из таблицы threads
        local nextc = 1
        -- Вызываем следующую запись из таблицы
        local function ne()
            if nextc <= #threads then
                print('no '..nextc)
                -- Это непосредственно вызов очередной записи
                threads[nextc]()
                nextc = nextc + 1
            end
        end
        -- Первый вызов
        ne()
        -- Остальное вызываем пошгово через 3 секунды:
        tmr.create():alarm(3000, 1, function(t)
            if nextc <= #threads then
                ne()
            else
                t:stop()
                t:unregister()
                t = nil
                nextc, ne, threads = nil, nil, nil
            end
        end)

    end

    tmr.create():alarm(60000, 1, dispatch)



    .... Еще не конец!
     
    Последнее редактирование: 10 июл 2018
  4. ИгорьК

    ИгорьК Давно здесь

    Все, коллеги. Перед вами универсальный диспетчер запуска любых чужих скопипащенных кусков кода!

    На это можно было бы и закончить - уже все работает, но есть еще любопытная деталь. Она касается именно нашей, крутейшей железки.

    Вы, конечно, еще не проверили код выше - а он выдаст вот такую ошибку:

    upload_2018-7-10_16-14-11.png

    Какой-то объект уже очищен, мы что-то пытаемся соединить с nil и никаких указаний на проблемное место!
    Приехали... Вроде раньше было все логично - и на тебе!
    Хорошо что счетчик указывает на проблему в третьем, scedset.lua, файле, но где она - не понятно! Если закомментировать строку с его вызовом - все работает.
    Проблема касается того, что мы в первых трех файлах создаем и уничтожаем локальные переменные с одинаковыми (вполне разумными) названиями. По-отдельности все это работало бы, но вместе - вот какая незадача.

    Вы можете поковыряться в файлах и найти что там не так, но я точно знаю, что оно вам не надо.
    И это в коде, который буквально построчно закомментирован. А если это код, где двадцать переменных каждая с обозначением из пары буков?..

    В арсенале Lua есть функция защищенного вызова того, что может повести себя неадекватно - pcall(...).
    Можно переписать код (мой) код с ее применением. Но оно вам надо? Есть прикольнее решение - называется сопрограммы (coroutine)! Вот здесь я начинал о ней, но так и не закончил :(

    Ок, тем не менее, применим эту штуку здесь:
    Код (Lua):
    -- Функция вызова обработки файлов по таймеру

    dispatch = function()
        print('\n')
        -- Флаг работы глдавной функции
        dat.dispatch = true
        -- Таблица запускаемых на исполнение файлов - корутин
        local threads = {}
        -- Перечень файлов, что надо последовательно выполнять
        local filestorun = {
            'getsun.lua',    -- (Для 1 и 3)Проверка времени восход/закат
            'lightnow.lua',    -- (1)Управление освещением
            'scedset.lua',    -- (2)Проверка расписания для отопителя
            'temper.lua',    -- (2)Управление отопителем
            'garden.lua',    -- (3)Полив
            'makepubl.lua'    -- Подготовка даных к публикации
        }
        -- Переменная для создания сопрограммы (корутины)
        local co
        -- Перебираем таблицу с именами файлов для выполнения
        for _, v in ipairs(filestorun) do
            -- Создаем корутину для каждого имени файла
            co = coroutine.create(function ()
                -- Корутина не исполняет свой код до прямой команды на исполнение:
                return dofile(v)
            end)
            -- Вставляем задачу сопрограммы в таблицу для исполнения
            table.insert(threads, co)
        end
        -- Счетчик вызванных сопрограмм из таблицы
        local nextc = 1
        -- Функция вызова очередной сопрограммы
        local function ne()
            -- Если не достигли конца таблицы
            if nextc <= #threads then
                -- Вызываем корутину и одновременно печатаем результат ее вызова
                print('no '..nextc, filestorun[nextc] , (coroutine.resume(threads[nextc])))
                -- Увличиваем счетчик очередного вызова
                nextc = nextc + 1
            end
        end
        -- Старт вызова первой сопрограммы
        ne()
        -- Отальные сопрограммы вызываются через 3 секунды каждая
        tmr.create():alarm(3000, 1, function(t)
            if nextc <= #threads then
                ne()
            else
                t:stop()
                t:unregister()
                t = nil
                nextc, ne, threads = nil, nil, nil
            end
        end)

    end
    -- Каждую минуту выполняем наши файлы
    tmr.create():alarm(60000, 1, dispatch)

    Итак, выше перед вами файл main.lua - недостающее звено нашего проекта. Он несложен, совсем несложен, но делает великое дело - изолирует друг от друга код и запускает его в защищенном режиме.

    upload_2018-7-10_16-37-0.png

    Также с комментариями файл:
    Код (Lua):
    myClient = "test001"  -- (!!!) Заменить на свое!
    dat = {
        target = 22, -- (!!!) Целевая температура отопителя по умолчанию
        askds = '', -- (!!!) DS18b20 адрес для управления отопителем
        -- askds = 't0054', -- Пример установки адреса
        poliv = 3600, -- (!!!) Время полива в секундах
    -----------------------------------------
        dispatch = false,
        light = 'OFF',
        lightF = false,
        arm = 'OFF',
        heat = 'OFF',
        broker = false,
        siren = 'OFF',
        auto = 'ON',
        clpub = 0, -- count lost publications
        ----- Восход/закат
        -- 100 - до получения правильных данных
        setH = 100,
        setM = 100,
        riseH = 100,
        riseM = 100,
        setHMcount = 10,
        ------ Полив
        now = 'Night',
        lux = 0,
        makedesDay = 0,
        lastDay = 0,
        intervalDay = 0,
        makedesNgt = 0,
        lastNgt = 0,
        intervalNgt = 0,
        -- Включение после подачи питания
        -- чтобы сразу не начался полив
        powerON = 'true'
    }
    -- Если вам лень переименовывать клиент - я переименую
    if myClient == "test001" then
        myClient = "home_"..node.chipid()
        print('\n\n\nClient is: '..myClient..'!!!!!!!!!\n\n\n')
    end
    pins = { --Ноги
        light = 0, -- освещение
        heat = 7, -- нагреватель
        siren = 6, -- сирена
        open = 8, -- открыть кран
        close = 9, -- закрыть кран
        pir = 5, -- ПИР датчик
    }
    for _,v in pairs(pins) do
        gpio.mode(v, gpio.OUTPUT)
    end
    gpio.mode(pins.pir,gpio.INPUT)

    topub = {}
    killtop = {}

    -- Перерабатываем расписание в машинное
    if file.exists('transform.lua') and file.exists('sced.lua') and  not file.exists('timeschd.lua') then
          dofile('transform.lua')
    end

    -- Восстанавливаем сохраненные данные с прошлой сессии
    if file.exists('setsaveddat.lua') then
          dofile('setsaveddat.lua')
    end

    -- Если сохранен режим охраны - ставим
    if dat.arm == "ON" then
        dofile('setarm.lua')
    end
    -- Коннект к брокеру
    dofile('setmqtt.lua')
    -- Главная программа
    dofile('main.lua')

    Вот оно в работе:

    upload_2018-7-10_16-50-46.png

    Настало время приложить архив всего о чем писалось ранее - прилагаю.
    В нем две папки - версия без полива и с поливом. С поливом - максимально тяжелая ситуевина.
    Полагаю, вы сами сможете "распилить" этот проект на части, если вам, например, нужен лишь полив или охрана + полив.

    Осталось написать только заключительную часть. В ней постараюсь объяснить, почему весь этот мой труд, хотя он отлично работает, вам ни разу не нужен и вы можете счастливо прожить без него.
    Наверное.
     

    Вложения:

    • iot04.zip
      Размер файла:
      40,6 КБ
      Просмотров:
      7
    Последнее редактирование: 14 июл 2018 в 00:34
    alp69 нравится это.
  5. alp69

    alp69 Гик

    Была бы возможность нажать "Мне нравится" дважды - нажал бы трижды! :)
    Может начнете писать книгу, альтернативную изданию Иерусалимски? Слог у Вас шикарный! Супер!:);)
     
    ИгорьК нравится это.
  6. ИгорьК

    ИгорьК Давно здесь

    Спасибо! Однако, эти заметки мало кому нужны :)

    Сейчас взялся за часы на матрице MAX7219. Утилитарный проект, в отличие от шикарного, что сейчас Иван двигает. Зато проще. Часы уже работают стабильно но код ещё слабоват.