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

Тема в разделе "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
    swc нравится это.
  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
    swc нравится это.
  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
    swc нравится это.
  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 КБ
      Просмотров:
      530
    Последнее редактирование: 14 июл 2018
    swc и alp69 нравится это.
  5. alp69

    alp69 Форумчанин

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

    ИгорьК Гуру

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

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

    ИгорьК Гуру

    Часы на MAX7219.

    Вот так:



    Мы видим текущее время, текущую температуру за окном, ожидаемые температуру и состояние неба.

    Первый вопрос - зачем? В сети полно готовых решений. Вот замечательный открытый проект от Ивана.

    Наши часы отличаются тем, что решают две узкоспециализированные задачи.

    Первая задача - не опоздать на работу и быстро принять решение о правильной одежде и зонте. О второй задаче - позже.

    То что часы от Ивана крутые и красивые имеет обратную сторону - длительность оповещения о текущей и предстоящей погоде сильно затягивается.
    Когда утром, в спешке, носишься по квартире, надо оттормозиться, чтобы прочитать что сейчас тебе расскажут о погоде в том месте, где ты всегда находишься. Смысл? Как то меня сие раздражает.

    Бегущая строка хороша в общественном транспорте - ты там ничего не делаешь, и можно заняться чтением движущихся буковок. Это даже успокаивает. А вот дома, ИMХО, это не всегда подходит.

    В общем, начнем...

    Погоду будем забирать на сайте https://www.apixu.com/ - регистрируемся и получаем ключ.
    А потом вот файлик askweatherAll.lua - ловите свою погодку:
    Код (Lua):

    local key = "Ваш Ключ"
    local city = "Moscow"
    -- Таблица с рабочими данными
    if not dat then dat = {} end
    -- Разные переменные
    local tnow, sky, max, min, codenow, codef
    -- Стринг полученного ответа от сервера
    local answer = ''
    local conn

    -- Функция обработки полученного запроса
    local function getdata()
        -- Закрываем и уничтожаем соединение
        conn:close()
        conn = nil
        -- Нет ответа - не повезло
        if answer == "" then
            print('Lost answer!')
        else
            -- Захватываем из ответа (1) текущую температуру, (2) код текущей погоды,
            -- (3) максимальную темрературу сегодня, (4) минимальную температуру сегодня,
            -- (5) код ожидаемой погоды
            tnow, codenow, max, min, codef =  string.match(answer,'temp_c":(%p*%d+.%d*),.+"code":(%d+).+maxtemp_c":(%p*%d+).+mintemp_c":(%p*%d+).+"code":(%d+)')
            answer = nil
            -- Если что-то не получилось захватить - делаем значения по умолчанию.
            tnow = tnow or '- 50'
            max = max or '-50'
            min = min or '-50'
            codenow = tonumber(codenow) or 1000
            codef = tonumber(codef) or 1000
            -- Заносим в рабочую таблицу данные
            dat.asktemp = tnow..'0'
            print('Got Temp: '..dat.asktemp)
            dat.maxtemp = max
            dat.mintemp = min
            print('Got Max: '.. dat.maxtemp..', Min: '..dat.mintemp)
            dat.codenow = codenow
            dat.codef = codef
            print('Got Sky codes: '..dat.codenow..' '..dat.codef)
        end
        -- Зачищаем хвосты
        answer = nil
        tnow, sky, max, min, key, city, codenow, codef, getdata = nil, nil, nil, nil, nil, nil, nil, nil, nil
        dat.askw = 0
    end

    -- Готовим текст запроса к серверу
    local request = "GET http://api.apixu.com/v1/forecast.json?key="..key.."&q="..city.." HTTP/1.1\r\n"..
        "Host: api.apixu.com\r\n\r\n"
    -- Определяем соединение
    conn=net.createConnection(net.TCP, 0)
    -- Реакция на соединение - отправляем запрос
    conn:on("connection", function(conn, payload) conn:send(request); request = nil end)

    -- Реакция на получение данных
    conn:on("receive", function(conn, payload)
        -- Данные приходят от сервера кадрами, если первый кадр -
        -- пишем его в переменную answer
        if answer == '' then
            answer = payload
        -- на втором кадре -  добавляем к переменной answer,закрываем соединение
        -- и отправляем ответ в обработку.
        else
            answer = answer .. payload
            payload = nil
            getdata()
        end
    end)
    -- Приступаем к соединению и запросу.
    conn:connect(80, 'api.apixu.com')
    На выходе:

    upload_2018-7-23_14-32-26.png
     
    Последнее редактирование: 29 авг 2018
    swc нравится это.
  8. ИгорьК

    ИгорьК Гуру

    Часы на MAX7219 - 2
    Теперь модуль для работы с MAX7219. В принципе, вы можете его подхватить у Marcel .
    А можно воспользоваться моим. Он чуть-чуть быстрее. Ну просто самую малость.
    Да что там... Я его полностью переписал.

    Не забыть, что в прошивке должны быть модули SPI и bit.

    Работать с модулем так:
    Код (Lua):
    dig = {}
    dig['1'] = {16,48,16,16,16,16,56,0}
    dig['2'] = {56,68,4,56,64,64,124,0}
    dig['3'] = {124,4,8,24,4,68,56,0}
    dig['4'] = {8,24,40,72,124,8,8,0}
    dig['5'] = {124,64,120,4,4,68,56,0}
    dig['6'] = {28,32,64,120,68,68,56,0}
    dig['7'] = {124,4,4,8,16,32,64,0}
    dig['8'] = {56,68,68,56,68,68,56,0}
    dig['9'] = {56,68,68,60,4,8,112,0}
    dig['0'] = {56,68,76,84,100,68,56,0}
    dig['+'] = {0,4,4,31,4,4,0,0}
    dig['-'] = {0,0,0,124,0,0,0,0}
    dig['z'] = {0,0,0,0,0,0,0,0}
    dig['r'] = {16,16,40,68,68,68,56,0} -- rain
    dig['c'] = {0,48,74,69,65,62,0,0} -- cloud
    dig['R'] = {0,4,2,127,2,4,0,0} -- arrow

    max7219 = require("max7219")
    max7219.setup({numberOfModules = 4, SSPin = 8, intensity = 6 })
    max7219.clear()
    max7219.setIntensity(1)

    max7219.write({dig['1'],dig['2'],dig['R'],dig['+']})
    Модуль зацеплен за этот пост.

    Ноги соединять:
    D5 - clock
    D7 - data
    D8 - CS


    Питание на MAX7219 подавать - 5 вольт.
     

    Вложения:

    • max7219.zip
      Размер файла:
      2,6 КБ
      Просмотров:
      479
    Последнее редактирование: 24 июл 2018
    swc нравится это.
  9. ИгорьК

    ИгорьК Гуру

    Часы на MAX7219 - Заключение и все файлы проекта
    Наметился один интересный проект - будем закругляться с часами. С первым вариантом. Второй будет позже, пока не знаю когда.

    Этот - очень прикидочный, невыверенный, тестовый!

    Выкладываю проект полностью, все файлы. Они без комментариев, кроме мест, где надо ввести ваши данные.

    Что делают часы:
    - показывают время, текущую температуру, температуру на сегодня днем, ожидаемое состояние неба;
    - забирают информацию с трех источников одновременно:
    1. сервер apixu.com
    2. брокер MQTT
    3. народный мониторинг.
    С народного можно забирать до пяти датчиков температуры с целью усреднения их значения. Можно и один забирать - усреднится сам с собой. Усредненное значение сравнивается с информацией с apixu.com и если разница более трех градусов (на народном датчики то частенько дохнут) - предпочтение отдается серверу apixu.com.
    - с 6 утра до 23 - яркость увеличенная
    - с 6 утра до 16 - выдается прогноз погоды на день, после 16 - ожидаемая минимальная температура
    - естественно, можно выбирать что в приоритете для текущей погоды. Например, в Москве я беру и усредняю минимальные данные с трех разных датчиков народного мониторинга, на даче - со своего термометра через брокер;
    -- можно включать и выключать в настройках анимацию.

    Анимация - это не бегущая строка! Бегущие строки - для эстетов и крутых парней, которые имеют время пялиться на светодиоды .

    Соединение и модули прошивки указаны в предыдущем посту. К обычным модулям добавка
    SPI и bit - обязательна.
    Файлы в приложении распакуйте, откройте каждый - в некоторых надо внести собственные изменения.

    Где надо уточнять я натыкал восклицательных знаков, увидите сразу.

    Обязательно: _asknarod.lua, askweatherAll.lua - не заработают без ваших персональных данных.

    asknarod.lua - укажите до пяти датчиков для чтения.

    setmqtt.lua
    соединит вас брокером iot.eclipse.org и ни на что не подпишет - тоже ковыряйте.

    Даже если ничего не исправлять Часы подхватят время и будут работать. Вместо прогноза погоды и текущей погоды получите нули. Остановите / закомментируйте таймер tmrwth в конце файла main.lua и получите просто часы с синхронизацией точного времени каждые 1000 секунд.

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

    Вопрос "я сделал все как здесь написано а оно ВООБЩЕ не работает" - задавать здесь.
     

    Вложения:

    • Max7219.zip
      Размер файла:
      9,6 КБ
      Просмотров:
      489
    Последнее редактирование: 23 авг 2018
    mex79 и swc нравится это.
  10. ИгорьК

    ИгорьК Гуру

    Часы MAX7219. Все апдейты неспеша здесь.
    Апдейт. Вечером берет погоду на завтра.
    Повторюсь, пока, до отдельного упоминания, это все прикидки, устройство не находится в разделе готовых проектов.

    UPD-2 - уточнение. Други, я там перед отправкой запроса на сервера забыл проверку наличия wifi. Полагаю, сами сделаете. Надо main.lua поправить:
    Код (Lua):
    if dat.askw > 50 and wifi.sta.status() == 5 then

    Ну, кроме того, еще забыл файлик про снег. Файл dsnow.lua есть, но скрипт им не пользуется. Его надо воткнуть в начало файла prtForecast.lua на основе анализа вот этой странички: http://www.apixu.com/doc/Apixu_weather_conditions.json

    UPD-3. 13/08/2018. По результатам наблюдения за работой часов сделал некоторые обновления.
    Из файла установки связи с MQTT брокером убрал функцию проверки текущего времени - она теперь в файле main.lua.

    main.lua несколько изменил, внеся в него проверку не только наличия wifi, но и выхода в Internet.
    Файл - max7219_3Amperka.zip - полный комплект файлов.

    UPD4. 28/08/2018. Амперка_12_09M - почищены способы обработки температур и их ошибок и т.п. Поправлен выбор состояния неба. Добавлена пара новых состояний.

    UPD5. 31/08/2018. Amperka_12_15 - Поразмыслил над модулями Apixu и Народного мониторинга.
    В народном мониторинге был недосмотр - если мониторинг вернул меньше датчиков чем запрашивалось, все падало. Исправил. Apixu немного переосмыслил с точки зрения борьбы за память.

    UPD6. 03/09/2018. Амперка12_16.zip

    Дальше все будет здесь.
     

    Вложения:

    Последнее редактирование: 11 сен 2018
    Юра 80 и swc нравится это.
  11. ИгорьК

    ИгорьК Гуру

    Модуль для MAX31855.
    Применять так:
    Код (Lua):
    do
        -- Таблица для наполнения температурами от термопары и max31855
        local tmp = {}
        -- Грузим модуль
        max = require('_max31855')
        -- Нога CS - SSPin = 8, а также ноги по умолчанию CLK - 5, SO - 6
        max.setup({SSPin = 8})
        -- Забираем температуру
        max.gettmp(tmp)
        -- Печатаем что получили
        table.foreach(tmp, print)
        -- Выгружаем модуль
        package.loaded['_max31855'] = nil
        -- Ликвидируемся
        max,tmp = nil,nil
    end
    Модуль в приложении. Это понятный вариант.
     

    Вложения:

    • max31855.zip
      Размер файла:
      888 байт
      Просмотров:
      389
    Последнее редактирование: 11 авг 2018
  12. ИгорьК

    ИгорьК Гуру

    И еще один модуль max31855 - поплотнее (меньше памяти) и куда как более индийский (но суть та же) :)
    Применяем:
    Код (Lua):
    do
        local tmp = {}
        max = require('_max31855n')
        -- 8 - нога CS, другие ноги SCL - 5, SO - 6
        max.gettmp(tmp, 8)
        table.foreach(tmp, print)
        package.loaded['_max31855n'] = nil
        max, tmp = nil,nil
    end
    Удалите из модуля инструкции print(...) и будет еще меньше. Удалите проверки на валидность передаваемых данных (пишите скрипты без ошибок) - станет совсем маленьким. "Сама, сама..."

    Правильное применение:

    upload_2018-8-8_11-49-56.png

    Минимальное применение:

    upload_2018-8-8_11-59-35.png

    Модуль не проверяет ответ max31855 на биты ошибок.
     

    Вложения:

    • _max31855n.zip
      Размер файла:
      584 байт
      Просмотров:
      428
    Последнее редактирование: 11 авг 2018
  13. Ilya._.Light

    Ilya._.Light Нуб

    Здравствуйте
    Я собираю умную вентиляцию в ванную, взял за основу ваш код, тестировал работу с mqtt сервером и получил странное поведение. Когда после загрузки модуль не может подключится и ловит ошибку, то заново запускается setmqtt.lua и пытается подключится, но если модуль подключился к серверу и после теряет с ним связь, то после выполнения setmqtt.lua он просто перезагружается без сообщений об ошибках. Причем файл полость отрабатывает но вместо отработки таймера начинается перезагрузка.

    Код (Lua):
    -- Этот файл задает параметры и устанавливает соединение с mqtt брокером.
    -- Скрипт обеспечивает надежное удержание соединения и его самовосстановление
    -- при потере как wifi так и самого брокера по любым причинам.

    if not mqttClient then
        mqttClient = mqtt.Client( MQTT_CLIENT_ID, 60, MQTT_LOGIN, MQTT_PASSWORD)
        mqttClient:lwt(MQTT_CLIENT_ID..'/state', "OFF", 0, 0) --последняя воля
        mqttClient:on("message",
            function(client, topic, data) --получение сообщения от брокера:
                -- очищаем пришедший от брокера топик до чистого топика-команды
                -- приходить они будут в формате 'myClient/command/setTemperature'
                -- "очищенная" и передаваемая для анализа - 'setTemperature'
                local top = string.gsub(topic, MQTT_TOPIC.."/command/","")
                print('Got now:',top, ":", data)
                if data then
                    -- заполняем таблицу killtop данными формата
                    -- '{'<topic>', '<data>'}'
                    table.insert(killtop, {top, data})
                    if not dat.analiz then
                        -- dofile("analize.lua")
                    end
            end
        end)
        mqttClient:on("offline", function(con) --потеря связи с брокером:
            dat.broker = false
            print('mqttClient: offline')
            dofile('setmqtt.lua')
        end)
    end

    -- Если файл вызывается рекурсивно
    -- соединение надо принудительно остановить
    mqttClient:close()
    print("mqttClient:close")

    local count = 0
    print("set count")
    local connecting = function(getmq)
        if wifi.sta.status() == wifi.STA_GOTIP then
            print('Got wifi')
            tmr.stop(getmq)
            print('tmr.stop(getmq)')
            tmr.unregister(getmq)
            print('tmr.unregister(getmq)')
            getmq = nil
            print('getmq = nil')
            mqttClient:connect(MQTT_BROKER_IP, MQTT_BROKER_PORT, 0, 0,
            function(client)
                print("Connected to Broker")
                -- mqttClient:subscribe(MQTT_TOPIC.."/command/#",0, function(conn)
                mqttClient:subscribe("/#",0, function(conn)        
                    print("Subscribed: "..MQTT_TOPIC.."/command/#")
                end)
                mqttClient:publish(MQTT_TOPIC..'/state',"ON",0,0)
                dat.broker = true
                count = nil
            end,
            function(client, reason)
                print("mqtt error: "..reason)
                dofile('setmqtt.lua')
            end)
        else
            print("Wating for WiFi "..count.." times")
            count = count + 1
            -- if count > 20 then node.restart() end
        end
    end
    print("set connecting")
    -- Таймер периодически запускает функцию соединения
    tmr.create():alarm(5000, 1, function(timer)
        print("tmr.create")
        -- и передает в нее свои опознавательные знаки
        -- чтобы его остановить и "убить"
        connecting(timer)
    end)
    tmr.delay(6000)
    print("end")
     
    в консоли я получаю
    Код (Text):
    Connected to Broker
    Subscribed: /bath/ventilation/command/#
    Got now:    /bath/ventilation/state    :    ON
    mqttClient: offline
    mqttClient:close
    set count
    set connecting
    end

    ets Jan  8 2013,rst cause:1, boot mode:(3,6)

    load 0x40100000, len 26040, room 16
    tail 8
    chksum 0xa3
    load 0x3ffe8000, len 2180, room 0
    tail 4
    chksum 0xf9
    load 0x3ffe8884, len 136, room 4
    tail 4
    chksum 0x07
    csum 0x07
    „гм‚'м“{‚уo<дЊdЏp{lc›Я|;“lњoаѓgг l`„г;›dЊdЊ$`ЊгsЫlд$„l`„г{“dЗџ

    NodeMCU custom build by frightanic.com
        branch: master
        commit: 8181c3be7aed9f0a0ceb73ac8137c1a519e8a8e9
        SSL: false
        modules: dht,file,gpio,mqtt,net,node,tmr,uart,wifi
    build created on 2018-07-21 21:25
    powered by Lua 5.1.4 on SDK 2.2.1(cfd48f3)
    lua: cannot open init.lua
    >
     
    предполагая что тут какая то проблем с таймером
     
  14. ИгорьК

    ИгорьК Гуру

    А мой код работает нормально? Проверяли?
     
  15. Ilya._.Light

    Ilya._.Light Нуб

    это он и есть, поменял имена перемененных и константа и добавил коменты ну и закомитал вызовы остальных файлов когда выявил баг, сейчас работают только два файла setglobals.lua и setmqtt.lua
     
  16. ИгорьК

    ИгорьК Гуру

    Все таки попробуйте мой "чистый"
     
  17. Ilya._.Light

    Ilya._.Light Нуб

    попробую, и если он отработает как искать причину бага?
     
  18. ИгорьК

    ИгорьК Гуру

    Странный вопрос, не так ли? :)

    Пытайтесь последовательно отключить все что Вы добавили. Я пока с сотового - завтра посмотрю что к чему.
     
  19. ИгорьК

    ИгорьК Гуру

    Смотрите, в консоли при перезагрузке есть такая строка:
    SmartSelect_20180813-000959_Opera Mini.jpg

    Это значит что причина перезагрузки - сброс питания.
    Проверьте работу на отдельном источнике - не исключаю, что ваш компьютер не додает корма модулю в момент переподключения.
     
  20. ИгорьК

    ИгорьК Гуру