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

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

  1. irkutyanin

    irkutyanin Нуб

    Добрый день. Я только недавно начал осваивать ESP8266 и язык LUA.
    Скажите пожалуйста, а вы можете сделать программу для управления диммером с детектором "ноля" с её подробным разбором? Например для такого устройства AC Light Dimmer Module
    Для него есть программы под Arduino IDE, но программ на LUA, работающих без мерцания я не нашёл. Мне кажется, что это было бы интересно не только мне и многим читателям ваших замечательных уроков.
     
  2. ИгорьК

    ИгорьК Гуру

    Улыбнуло.

    Программ под Lua, с большой долей вероятности, быть не может, и вот почему.

    Lua - надстойка на Си, что работает поверх закрытого SDK.
    В результате, до Lua есть два уровня прерываний, причем разного приоритета.

    Верхний - обеспечение нужд работы SDK, в частности подсистемы WiFi, управления таймерами и работы с памятью.

    Невзирая ни на что, система постоянно прерывается на 10 мкс, чтобы порешать свои проблемы.

    Другой уровень - прерывания Си надстройки, как там ее замышляют авторы.

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

    Чтобы не было мерцания, прерывание от детектора ноля должно иметь наивысший приоритет, что в условиях NodeMCU просто недостижимо.

    Если сильно надо немерцать (а зачем? проще работать со светодиодными лентами), то без какой-нибудь attiny45 в качестве исполнителя, полагаю, здесь не обойтись.
     
    SergeiL нравится это.
  3. SergeiL

    SergeiL Оракул Модератор

    AtTiny25 стоит меньше 100 руб. и прекрасно обслуживает два канала диммирования без намека на мерцание.
    Связь с ней по I2C - очень простая задача.
     
    ИгорьК нравится это.
  4. ИгорьК

    ИгорьК Гуру

    ... и где-то вы выкладывали решение, кажется.
     
  5. SergeiL

    SergeiL Оракул Модератор

    Да, выкладывал тут.
    В коридорах после захода солнца включается дежурка на 15%, когда кто то дома.
    Ночью, когда в комнатах и на кухне верхний свет не включен, при входе в санузлы свет включается на 20% яркости.
    Днем, или если свет в комнатах включен, - на 100% яркости.
    Если включили выключателем - 100% всегда.
    В санузлах галогенки, при включении и выключении яркость меняется плавно, по моему 1% на полупериод.
    Поставил диммер в ноябре 2018-го, после этого не поменял ни одной лампочки.
    А до этого две сгорели, за месяц, пока простым выключателем включались.
     
    ИгорьК нравится это.
  6. irkutyanin

    irkutyanin Нуб

    Благодарю. Я пришёл к тому же мнению. Делаю автомат для террариума. Подсветку хотелось бы включать и выключать плавно - это существенно увеличивает срок службы ламп и для животных лучше. А в подсветке используются галогенные лампы. Есть у меня в запасе ардуинки, буду на них делать. А вообще парадокс, конечно, на более мощном железе сложнее делать какие то простые вещи, связанные с прерываниями.

    Наверное возможно сделать, но надо будет программировать более глубоко. Какие нибудь модули на С. Ведь если из под ардуино ide делать, то всё работает плавненько
     
    Последнее редактирование: 24 июл 2019
  7. ИгорьК

    ИгорьК Гуру

    Я это уже делал. Но почему-то люблю Lua.
     
  8. irkutyanin

    irkutyanin Нуб

    Благодарю. Мне тоже нравится LUA. Буду копать дальше.
     
  9. swc

    swc Гик

    Для простых задач системы прерываний LUA для ESP8266 достаточно с большим запасом.
     
  10. ИгорьК

    ИгорьК Гуру

    41. MH-Z19B module.

    Для затравки, совещание длилось час:
    upload_2019-7-29_12-36-53.png

    Код с пояснениями, сам модуль в приложении:
    Код (Lua):
    M = {}
    -- Проверяем результат чтения порта:
    M.check = function(kl)
        local sum = 0 -- Будет контрольная сумма
        uart.on("data") -- Выключаем callback на UART
        -- uart.alt(0) -- Уходим с 7 и 8 ног UART на стандартные
        -- восстанавливаем стандартные настройки порта
        uart.setup(0, 115200, 8, uart.PARITY_NONE, uart.STOPBITS_1, 0)
        -- Чтение могло быть успешным (получено 9 байт) и безуспешным (Пришло меньше 9 байт)
        -- в течение тех секунд ожидание будет прекращено таймером, но если получено 9 байт,
        -- таймер надо остановить и уничтожить:
        if M.t then M.t:stop(); M.t:unregister(); M.t = nil end
        -- Если чтение порта убито таймером, то оповещаем об этом, вызываем сallback
        -- и завершаем работу модуля.
        if kl then print('UART Killed!'); if M.call then M.call() end return end
        --print('length = ', #M.RAW) -- Отладочная информация
        --table.foreach(M.RAW, print) -- Отладочная информация

        -- Вычисляем контрольную сумму 2-8 байты
        for i = 2, 8 do
            M.RAW[i] = M.RAW[i] and M.RAW[i] or 0
            sum = sum + M.RAW[i]
        end
        sum = 255 - bit.band(255, sum) + 1
        -- print('Sum: ', sum, M.RAW[9]) -- Отладочная информация

        if sum == M.RAW[9] then
            M.tbl.co2 = M.RAW[3]*256 + M.RAW[4]
            print('Now CO2:', M.tbl.co2) -- Итого - концентрация СО2
        end
        if M.call then M.call() end -- Вызываем callback сли есть.
    end

    -- Создаем таймер уничтожения чтения из порта с задержкой 3 секунды
    -- задержку меняйте по своему усмотрению
    M.t = tmr.create()
    M.t:register(3000, tmr.ALARM_SINGLE, function() M.check(true) end)

    -- Опрос датчика и получение данных
    M.askmh = function(tbl, call)
        local ustart = false -- Читать еще не начали
        -- uart.alt(1) -- На прошивке 1.5 можно уйти на альтернативные ноги 7 и 8
        -- Модуль обязательно потребляет таблицу для внесения в нее данных о СО2,
        -- нет - сразу завершает работу с оповещением об убийстве чтения
        if not tbl then return M.check(true)  end
        M.tbl = tbl -- Копируем ссылку на внешнюю таблицу в модуль
        M.RAW = {} -- Создаем внутреннюю таблицу для получения данных от датчика
        if call then M.call = call end -- Копируем ссылку на callback если он есть

        -- Выставляем необходимую скорость UART
        uart.setup(0, 9600, 8, uart.PARITY_NONE, uart.STOPBITS_1, 0)
        -- Определяем как будем читать UART: побайтно!!!
        uart.on("data",1,
            function(data)
                -- Если это не число 255 - мимо ушей
                if ustart == false and string.byte(data, 1) ~= 255    then
                    return
                -- Если 255 - отменяем предыдущую и эту проверки
                elseif ustart == false then
                    ustart = true
                end
                -- Создаем массив байт, опираясь на его собственную длинну
                M.RAW[#M.RAW+1] = string.byte(data, 1)
                -- Если набрали 9 байт - отправляем на проверку
                if #M.RAW == 9 then M.check() end
            end, 0)
        -- Задаем вопрос датчику:
        uart.write(0,255,1,134,0,0,0,0,0,121)
        -- Запускаем таймер самоликвидации:
        M.t:start()
    end
    return M
    Применение модуля. Обязательно его выгружать, ибо там есть таймер, который самоликвидируется, повторный вызов без выгрузки будет безуспешным, "не сказать еще хужей". Если кто-то решит держать модуль в памяти (зачем?), поменяйте в нем пару инструкций, сами должны знать каких.

    Код (Lua):
    do
        if not wth then wth = {} end
        local call = function()
            package.loaded['_mh-z19b'], mh = nil, nil
            print('Got co2:', wth.co2)
        end
        local mh = require('_mh-z19b')
        mh.askmh(wth, call)
    end
    Почему в модуле применено побайтное чтение, вместо чтения сразу 9 байтов, что позволяет NodeMCU?
    Ответ таков: Иногда датчик шлет их чуть меньше (или ошибка приема байта), чтение зависает, и, что самое плохое, пока UART ждет своего последнего байта, никакими средствами отказаться от этого ожидания невозможно.
    То есть, логика опроса, что изложена здесь, (кстати, отличный каталог ссылок) была повторена в первую очередь, но натурные испытания показали, что устройство обязательно перестает слать данные в интервале от 3 до 6 часов, оставаясь при этом вполне работоспособным.

    Небольшой проект с датчиком здесь.
     

    Вложения:

    • _mh-z19b.zip
      Размер файла:
      730 байт
      Просмотров:
      346
    Последнее редактирование: 30 июл 2019
    SergeiL и swc нравится это.
  11. ИгорьК

    ИгорьК Гуру

    У меня был опыт управлять освещением на Искра ЖС http://forum.amperka.ru/threads/Плавное-управление-сетевым-освещением-pid-Вопрос-разработчикам.9765/

    Вместе с Василием (@acos ), Царство ему Небесное, чудесный был человек, мы просидели час с осциллографом и генератором, пытаясь удерживать работу на JavaScript. Если ничего больше не делать - получалось, если хоть что-то еще - шел срыв управления.
    Полагаю, что и NodeMCU то же самое. Хотя сам не проверял - не было нужды.
     
  12. SergeiL

    SergeiL Оракул Модератор

    Тоже все хочу подключить и попробовать.
    Датчик лежит давно, все руки не доходят...
     
  13. ИгорьК

    ИгорьК Гуру

    Тогда чуть позже полный проект(ик) выложу.
     
  14. ИгорьК

    ИгорьК Гуру

    41.1. MH-Z19B кратко проект.

    Модуль работает с прошивкой (SDK 1.5), что в приложении, и висит на 7 и 8 ноге.
    Общая картинка:

    upload_2019-7-30_15-30-13.png


    Устройство цепляется к брокеру iot.eclipse.org (меняйте) и гонит на него информацию.

    Через топики 'MHZ19/com/clb' принимает три команды на калибровку: ABCOFF, ABCON,ZERO.
    В ответ в топике 'MHZ19/answer' присылает волшебные цифры, если команда была выполнена, и молчит, если мимо.

    Через топик 'MHZ19/com/ide' командой ON входит в режим программирования по сети, с предварительной выдачей своего адреса.

    B топик 'MHZ19/boot' шлет информацию из файла _aaversion.lua (держу там информацию о версии) а также о том, когда и почему перегрузилось.

    Топик 'MHZ19/state' всегда отражает текущее состояние устройства.


    upload_2019-7-30_15-50-23.png

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

    Файл _printREG.lua для просмотра информации в ходе работы устройства.

    Файл _renameinit.lua делает волшебные пасы с именем файла _init.lua.

    P.S. После обкатки модуля буду делать устройство с выводом на 7-сегментный индикатор, воткну сюда:
    upload_2019-7-31_9-32-54.png
     

    Вложения:

    Последнее редактирование: 31 июл 2019
  15. ИгорьК

    ИгорьК Гуру

    42. ESP32: пробуем.

    Несколько раз брался за ESP32, но жизненно необходимый модуль mqtt не желал вести себя достойно. После пары попыток восстановления коннекта с брокером все вылетало в трубу:

    upload_2019-7-31_18-4-25.png

    Тем не менее, с учетом "многодури", - ESP32 вещь зело заманчивая. И все там хорошо, кроме этого окаянного модуля.

    И вот, мне кажется, удалось с ним справиться тупым лайфхаком - после каждой потери коннекта уничтожаем объект mqtt со всеми потрохами, и через некоторое время воссоздаем вновь.

    Итак, код для ESP32 (кстати, и для ESP-8266 подойдет, коль связь с брокером часто падает):
    файл mqttset.lua.
    Код (Lua):
    do
        -- В скриптах я пользуюсь рабочей таблицей dat, но чтобы
        -- можно было тестить этот модуль сам по себе - создаем
        if not dat then dat = {}; dat.clnt = 'test' end
        dat.brk = 'iot.eclipse.org'
        dat.port = 1883
        -- Функции заталкиваем в локальные переменные
        local subscribe, merror, newm, mconnect
        -- Все как обычно
        function subscribe(con)
            print("Connected")
            dat.broker = true
            con:subscribe(dat.clnt.."/com/#", 0)
            con:publish(dat.clnt..'/state', "ON", 0, 1)
            print("Subscribed")
        end

        -- Вот оно, гнездо птици Феникс, уничтожает все что касается
        -- объекта mqtt
        function merror(con)
            con = nil
            m = nil
            -- И заказывает новый mqtt через 30 секунд, за это время сборщик мусора
            -- должен почистить память
            tmr.create():alarm(30000, tmr.ALARM_SINGLE, function() mconnect(newm()) end)
        end

        -- Типичное создание mqtt
        function newm()
            m = mqtt.Client(dat.clnt, 25, dat.clnt, 'pass22')
            m:lwt(dat.clnt..'/state', "OFF", 0, 1)
            m:on("offline", function(con)
                con:close()
                dat.broker = false
                print("offline")
                -- Потерялись? Вызываем ликвидатора, нефиг теряться!
                merror(con)
            end)
            m:on("message", function(con, top, dt)
                -- Это для случаев, когда сообщений много - кидаем их в
                -- глобальную таблицу, которую надо сделать
                if not killtop then killtop = {} end
                -- Если пришло сообщение - забираем все что после последнего "/"
                top = string.match(top, "/(%w+)$")
                print('Got', top, dt)
                if dt then
                    table.insert(killtop, {top, dt})
                    if not dat.analiz then
                       -- dofile("mqttanalize.lua")
                    end
                end
            end)
            -- Возвращаем ссылку на объект mqtt, несмотря на то, что он глобальный
            return m
        end

        -- Оно конектится со всем, что ему скормят
        function mconnect(con)
            con:connect(dat.brk, dat.port, 0, 0, subscribe, merror)
        end
        -- Кормим коннекту ссылку на новый объект mqtt
        mconnect(newm())
    end
    Пробуем, кому интересно.
    В приложении - прошивка ESP32 и инструкция как шить. И сюда поглядываем.

    upload_2019-8-5_8-58-47.png

    В Интернеты заводим так (wifi32.lua):
    Код (Lua):
    do
    wifi.start()
    wifi.mode(wifi.STATION, true)
    local scfg = {}
    scfg.auto = true
    scfg.save = true
    scfg.ssid = 'WiFiAP'
    scfg.pwd = 'Superpassword'
    wifi.sta.config(scfg, true)
    wifi.sta.on("got_ip", function(ev, info)
      print("NodeMCU IP config:", info.ip)
    end)
    wifi.sta.connect()
    end
    Запустив вручную один раз этот файл, в init.lua достаточно пару строк, поставив их самыми первыми:
    Код (Lua):
    wifi.start()
    wifi.sta.on("got_ip", function(ev, info)
      print("NodeMCU IP config:", info.ip)
    end)
    Файлы wifi32.lua и mqttset.lua можно запускать в любой последовательности без привязке к программе в целом, они, думаю, порадуют вас сами по себе.

    Это оно!

    upload_2019-8-1_10-40-54.png
     

    Вложения:

    • esp32.zip
      Размер файла:
      598,4 КБ
      Просмотров:
      509
    • mqttset.zip
      Размер файла:
      668 байт
      Просмотров:
      445
    Последнее редактирование: 5 авг 2019
    swc нравится это.
  16. ИгорьК

    ИгорьК Гуру

    Таки переложил проект с датчиком mh-Z19b на ноты ESP32. Прошивки bin из предыдущего поста.

    Что прикольного.

    1. Не удается выполнить команду node.bootreason(), пишет "нет такой буквы". Возможно я при компиляции прошивки сильно зажал вывод отладочной информации. Собственно, это не так важно, можно обойтись.

    2. Вследствие "многодури" файл ide.lua может быть запущен на работающем устройстве без перезагрузки. У нас появляется рабочая web IDE.

    В остальном - все работает.
     

    Вложения:

    Последнее редактирование: 2 авг 2019
  17. DimaCh

    DimaCh Нуб

    Пытаюсь запустить mqtt на esp8266. На старой прошивке Lua 5.1.4 on SDK 2.1.0 все запустилось, отправка и прием сообщений от брокера, все работает.
    При обновлении прошивки на Lua 5.1.4 on SDK 2.2.1 точно такой же код выдает ошибку.
    PANIC: unprotected error in call to Lua API (setmqtt.lua:28: attempt to call field 'stop' (a nil value))

    Непонятно зачем нужен аргумент в функции tmr.stop(getmq) и в tmr.unregister(getmq)
     

    Вложения:

    • tmr.jpg
      tmr.jpg
      Размер файла:
      284,8 КБ
      Просмотров:
      451
  18. ИгорьК

    ИгорьК Гуру

    Это то понятно - функции остановки таймера нужна ссылка на таймер, который требуется остановить.

    Если раньше работало, а теперь нет - это
    1. или прошивка была скомпилирована с ошибкой,
    2. или код был случайно изменен, в частности синтаксическая ошибка в названии таймера.
    3. или к моменту вызова функции таймер уже уничтожен чем-то, или вообще не создавался.

    tmr.stop(timer) - классическая функция, куда она денется.
     
  19. DimaCh

    DimaCh Нуб

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

    Код (Lua):
    local fun = function(getmq)
       print("eto t1")
    end
    tmr.create():alarm(5000, 1, function(t)
        print("eto t1")
        fun(t)
    end)
    Ошибка
    PANIC: unprotected error in call to Lua API (stdin:3: attempt to call global 'fun' (a nil value))
     
    Последнее редактирование модератором: 14 авг 2019
  20. ИгорьК

    ИгорьК Гуру

    Не знаю, но в показаниях путаетесь. Это новая версия.

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

    Обрамляйте этот кусок кода операторами do ... end