ESP-8266/ESP32 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
    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 КБ
      Просмотров:
      58
    Последнее редактирование: 14 июл 2018
    swc и alp69 нравится это.
  5. alp69

    alp69 Гик

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

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

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

    Сейчас взялся за часы на матрице MAX7219. Утилитарный проект, в отличие от шикарного, что сейчас Иван двигает. Зато проще. Часы уже работают стабильно но код ещё слабоват.
     
    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 КБ
      Просмотров:
      38
    Последнее редактирование: 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 КБ
      Просмотров:
      32
    Последнее редактирование: 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 байт
      Просмотров:
      27
    Последнее редактирование: 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 байт
      Просмотров:
      21
    Последнее редактирование: 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. ИгорьК

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