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

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

  1. Не могли бы мне дать? :) Хотелось бы чтобы не Яндекс Диск, а то у нас заблокирован :) но буду рад любому
     
  2. alp69

    alp69 Гик

    Почту в личку кидайте.
     
  3. alp69

    alp69 Гик

    Прошу прощения, книжка не *.fb2, а *.epub
     
  4. Securbond

    Securbond Гик

    Кинул в телегу
     
  5. Тогда не надо, благодарю :)
     
  6. alp69

    alp69 Гик

    Еще раз сорри. Все ОК. Fb2. Сейчас на почту скину. Если будут проблемы с открытием, проверьте, чтобы расширение было fb2.
     
  7. ок
     
  8. ИгорьК

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

    17. Азы: изучаем память.
    Этот топик, конечно, должен писать кто-то более опытный, поэтому критика и матюки - приветствуются.
    Итак, ко мне обратился один из читателей:
    upload_2018-1-25_18-5-44.png

    Попробуем разобраться, или хотя бы понять методику.

    Что представляет собой код, который вы заливаетете в модуль?
    Это:
    1. некоторое API от производителя модуля
    2. на который сделана Lua "обвязка" на языке Си от разработчика Lua
    3. на котором вы написали некоторый код, возможно
    4. скопипастив часть его у какого-то неуча, типа меня.

    Во всех этих четырех местах может происходить затык. Кто помнит, первые версии прошивок любых видов от espressif были чрезвычайно глючные.
    Да и сейчас иногда проскакивают проблемы даже в АТ прошивке.

    Разработчик прошивки Lua также может косячить: попробуйте запустить вот этот пример:
    upload_2018-1-25_18-21-16.png

    Надеюсь, вы сами найдете ошибку.

    Итак, если код загружается в модуль без ошибок и запускается - это уже полвина дела.
    Дальше надо изучить состояние памяти.

    1. Первая задача - изловить все переменные и понять, нужны ли они в глобальной таблице.
    Делаем рестарт модуля и выполняем код из п. 7. Получаем вот это:
    upload_2018-1-25_18-50-25.png


    Перед нами две таблицы: _G - глобальная таблица переменных, ниже - таблица загруженных модулей.
    Раз мы выполнили код после перезагрузки и никакой скрипт не выполняется - перед нами данные "по умолчанию". Они всегда есть и будут. Их нужно запомнить и когда начнем работать с кодом - исключать из рассмотрения. Обычно мы имеем 8 записей в таблице _G и две - ниже.

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

    upload_2018-1-25_18-58-30.png

    В таблице _G не 8 а 11 записей. Найдем и разберемся с тремя новыми:
    dat - таблица данных, она должна быть глобальной (типа она наполняется по разным событиям и должна быть видна всем).
    srv - наш сервер, всегда висит в памяти - нормально что глобальный.
    receiver - а это зачем??? Не за чем - просто забыл поставить "local" перед функцией и теперь она в памяти постоянно, не выгружается.
    Плохо это или нет? Все зависит от обстоятельств. Лучше избавляться от всех переменных, от которых можно избавиться. Если, в определенных случаях, это не ведет к утечке памяти.

    2. Ищем утечки памяти.
    Это уже пилотаж повыше.

    Маленькое отступление. Утечка памяти - это очень очень плохо. С утечкой памяти модуль будет работать неустойчиво и в конце концов... Ну, что будет? Зависнет как Ардуино?
    А вот и нет! Перезагрузится и начнет работать заново - запустится init.lua! И если вы не управляете чем-то ответственным в реальном времени (а делать это на скриптовых языках чрезвычайно не рекомендуется) - НИЧЕГО СТРАШНОГО НЕ ПРОИЗОЙДЕТ!
    Вы этого даже можете не заметить, с учетом того, что в последних прошивках модуля при перезагрузке сеть даже не отваливается. Если вы знаете, что память течет и ничего с этим поделать не можете - есть грязный хак: сохранить данные и принудительно перегрузить модуль, когда память уменьшится до некоторого эмпирически установленного вами значения.

    Первое что надо понять - течет память или нет.
    Узнается это исполнением
    Код (Lua):
    print(node.heap())
    Вот вариант маленького сервера, где я добавил отчет о памяти (и загнал в локальные receiver):
    Код (Lua):
    do
    dat = {
        temp = 27.4,
        -- humi = "Не знаю, но там сухо!"
        humi = "No Info!"
    }
    local function expand (s)
        return (string.gsub(s, "$(%w+)", dat))
    end
    srv = net.createServer(net.TCP, 15)
    local function receiver(sck, data)
        data = nil
        local function closec()
            sck:close()
        end
        local function send()
            if file.open("page.lua", "r") then
                repeat
                    local line = file.readline()
                    if line then
                        line = expand(line)
                        sck:send(line)
                    end
                until line == nil
                file.close()
                -- file = nil
            else
              sck:close()
            end
            print('on sent:', node.heap())
        end
        sck:on("sent", closec)
        send()
    end
    srv:listen(80, function(conn)
      conn:on("receive", receiver)
    end)
    tmr.create():alarm(30000, 1, function() print(node.heap()) end)
    print(node.heap())
    end
    Память течет! Никакой collectgatbage() нас не спасает.

    От чего же она, подлюка, может течь?
    От ошибок API и разработчика Lua. Да-да. Это надо знать заранее и проверять.

    Оптимально - брать с сайта http://nodemcu.readthedocs.io эталонный код и тестировать его в хвост и в гриву.

    Если говорить о сервере, то здесь все криво: эталонный код с сайта, что я привел выше, разрывает соединение с браузером и в принципе не работоспособен. Проверяйте сами.

    От этого не легче - течет! Можно пойти двумя путями - правильным и моим, эмпирическим (щас меня начнут бить!)

    Правильно - читать http://nodemcu.readthedocs.io/en/master/en/lua-developer-faq/ и в особенности вот это:

    upload_2018-1-25_20-21-20.png

    (Помните я говорил, что локалить функции надо не всегда. Здесь говорится об этом же)

    НО! Я делаю проще - в локальных функциях принудительно убиваю все отработавшие локальные переменные, присваивая им значения nil - что спецификацией языка не требуется!

    Однако, работает! (Замечу, что делать это следует не всегда, а именно в тех случаях, когда вы обнаруживаете утечку памяти).

    Итак, код маленького сервера, когда память перестает течь:
    Код (Lua):
    do
    dat = {
        temp = 27.4,
        -- humi = "Не знаю, но там сухо!"
        humi = "No Info!"
    }
    local function expand (s)
        return (string.gsub(s, "$(%w+)", dat))
    end
    srv = net.createServer(net.TCP, 15)
    local function receiver(sck, data)
        data = nil
        local function closec()
            sck:close()
            sck = nil
            closec = nil
        end
        local function send()
            if file.open("page.lua", "r") then
                repeat
                    local line = file.readline()
                    if line then
                        line = expand(line)
                        sck:send(line)
                    end
                until line == nil
                file.close()
                file = nil
                line = nil
            else
              sck:close()
              sck = nil
            end
            print('on sent:', node.heap())
        end
        sck:on("sent", closec)
        send()
    end
    srv:listen(80, function(conn)
      conn:on("receive", receiver)
    end)
    tmr.create():alarm(30000, 1, function() print(node.heap()) end)
    print(node.heap())
    end
    И результат работы:
    upload_2018-1-25_20-3-34.png

    Что же конкретно там не срабатывало - можете выяснить сами, путем постепенного прощения и оставления в живых локальных переменных.

    А вот если несколько раз быстро обновить страницу? Попробуем.

    upload_2018-1-25_20-9-33.png


    Только одно но!!! Пока я писал эту заметку и мой сервер тихо висел на модуле - память утекла без обращения к нему:
    upload_2018-1-25_20-12-7.png

    Я не вызывал ответ сервера! Что там могло произойти? Может быть wifi переподключился, может быть лиса мимо пробежала или змей-горыныч пролетел...
    Но я (или вы) - точно не виноваты. Это не баг, это фича.

    Выводы.
    1. Память в модуле может течь, а может не течь. За памятью надо наблюдать.
    2. Как это делается - я постарался объяснить, но может кто-то сделает лучше.
    3. Утечка памяти не так страшна - ее недостаток обернется перезагрузкой модуля и новой работой с чистого листа.
    4. Три причины потери/утечки памяти: (1)ошибки API (2) ошибки Lua (3) забытые вами переменные.
    5. Ошибки API обойти не возможно. Поставьте в какой-нибудь постоянно тикающий таймер следующий код:
      Код (C++):
      if эмпирическое_значение > node.heap() then
          node.restart()
      end  
      Определить значения для нашего случая можно так: быстро обновляйте страницу и наблюдайте за реакцией модуля, при определенных значениях node.heap() вы поймете что пора остановиться - вот вам и предельно минимальный объем памяти
    6. Особенности Lua в реализации NodeMCU таковы, что изредка система теряет upvalues - неглобальные переменные и в документации об этом прямо говорится (их зависание в памяти увидеть нельзя и вызов функции collectgarbage() не помогает). Решением предлагается использовать глобальные переменные а при окончании работы - удалять их. Это не всегда удобно. Мой вариант - принудительно уничтожать все до одной локальные переменные после использования и это работает. (Напоминаю, что это особенность NodeMCU Lua, а не требования языка)
    7. Вы можете (1.) забывать в глобальном пространстве имен переменные. Вы можете (2.) забывать выгружать модули. И, часто, самое забавное - (3.) выгруженные модули могут оставлять в глобальном пространстве имен переменные. Это легко поставит в тупик, особенно если вы пользуетесь чужими модулями. Но эти три вещи при должном старании отлавливается с применением простого кода, который я показал выше.
    ... но в общем случае все работает и так.

    Вот наблюдение за памятью одного из устройств:
    upload_2018-1-31_17-27-59.png

    И еще один вариант наблюдения за памятью:
    upload_2018-2-14_11-38-22.png

    Идем дальше.
     
    Последнее редактирование: 28 фев 2018
    Securbond, MESS, virtual и 2 другим нравится это.
  9. ИгорьК

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

    Обновил заметку. Систематизировал заключение. Важно.
     
  10. Securbond

    Securbond Гик

    Пора Вам уже цикл тематических статей на Хабре делать )))
     
  11. ИгорьК

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

    Сюда придут кому надо.
     
  12. ИгорьК

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

    На сайте esp8266.ru выложен код сервера для OTA. Хвала автору. Применяем, кому полезен.
     
  13. ИгорьК

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

    18. Грузим Lua скрипты по wifi.

    Этот топик был бы не возможен без замечательной работы Zin4enkoSV который и сделал сервер, чем мы и воспользуемся. Сервер я покоцал в силу своего (не)понимания происходящего, сократив всю работу автора до минимально необходимых нам возможностей. (Авторскую работу я приложу к этому топику отдельным файлом.)

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

    Есть два варианта работы - сервер сидит в памяти постоянно или, по команде, загружается в ESP-8266 и выгружается из него. Мы пойдем по последнему пути, поскольку практически все устройства умного дома завязаны на MQTT брокер и ничто не мешает получить от него команду на запуск http сервера.

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

    Чтобы все работало правильно, в прошивке должны обязательно находиться модули rtcmem и rtctime.
    rtcmem является зависимым модулем от rtctime. rtcmem позволяет писать информацию в память внутренних часов и считывать ее от туда. Объем памяти составляет 127 слотов 32 битных чисел. Запись сохраняется и после перезагрузки модуля, но не после снятия питания.

    Логика будет такова, что файл init.lua считывает установленную ячейку памяти, и если там находится подходящая цифра (здесь - 501) запускает файл сервера http.lua.

    Чтобы сервер запустился, то есть в ячейке ноль появилась цифра 501, ваш скрипт должен выполнить:
    Код (Lua):
    rtcmem.write32(0, 501) -- пишем в память число 501
    node.restart()
    Сервер, отработав правильно и загрузив необходимые файлы, меняет в этой ячейке "секретное число" на другое и вызывает рестарт модуля. init.lua, проверив ячейку и не найдя там "секретного числа" запускает файл main.lua. Если файл не найден - опять запускает http.lua.

    Три файла защищены от удаления (ошибки пьяного юзера): init.lua, http.lua, index.htm - эта троица позволяет не "убить" модуль действиями оператора и, в конце концов, загнать в него необходимые файлы.
    Повторюсь, отсутствие файла main.lua всегда будет вызывать загрузку сервера в память. В связи с этим, при работе с сервером полагаю правильно удалять его в первую очередь, а загружать - в последнюю.

    init.lua:
    Код (Lua):
    local w = rtcmem.read32(0)
    if w == 501 then
       print("Start Server!")
       dofile("http.lua")
    else
    print("Try Run main.lua!")
    tmr.create():alarm(15000, 0, function()
       if file.exists("main.lua") then
          dofile("main.lua")
       else
           print("No main.lua!")
           rtcmem.write32(0, 501)
           node.restart()
       end
    end)
    end
    Как это выглядит:
    upload_2018-1-31_15-0-55.png

    Нажатие на кнопку "!!!RESTART Module!!!" вызывает запись в ячейку памяти 0 числа 0 и перезагрузку модуля, после чего init.lua пытается запустить файл main.lua.


    В приложении - два файла.
    myServer.zip - файл этого проекта
    NodeMCU_LUA_WebServer2.zip - авторский файл.

    UPD. Вот еще интересный проект. Грузить файлы не позволяет, но редактировать и создавать прямо на модуле - можно.

    upload_2018-3-20_14-30-11.png





    Дальше.
     

    Вложения:

    Последнее редактирование: 20 мар 2018
    alp69 и Securbond нравится это.
  14. ИгорьК

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

    Бонус: простой скрипт для переименования init.lua, чтобы не вызывать постоянного старта модуля.
    Код (Lua):
    if file.exists("init.lua") then
         file.rename("init.lua","_init.lua")
         node.restart()
    elseif file.exists("_init.lua") then
         print("Really rename to init.lua? \n 10 sec. delay!")
         tmr.create():alarm(10000, 0, function()
              file.rename("_init.lua","init.lua")
              node.restart()
         end)
    end
    Этот файл можно выполнить прямо на сервере - init переименуется в _init. И обратно, если запустить сервер вручную через вызов http.lua. Полезно пока идет отладка программ.
     
    Последнее редактирование: 31 янв 2018
  15. Crazy_Volt

    Crazy_Volt Нуб

    Здравствуйте, уважаемые форумчане. Работаю с модулем ESP-12-E. Прошивка NodeMCU. Пишу скрипты на LUA и заливаю через ESPlorer. Совсем не профи в данной области, только учусь. На espшке запущен сервер и в браузере открывается html-страница. Пользуюсь примерами с данного сайта: Как сделать Web-интерфейс для ESP8266 под NodeMCU | Avislab. Возникла необходимость по нажатию кнопки на html-форме запускать скрипт на espшке, который по UART будет отправлять данные(символ,слово,цифра не важно). Возможно ли реализовать данную задачу?
     
  16. ИгорьК

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

    Если вы рискнете почитать эту тему... то найдете прямой ответ на вопрос.
    Иначе, лучше спросите у автора того сайта, на который даете ссылку.

    Да, и давайте вместе наблюдать за этой темой, там автор ставит аналогичную задачу перед форумчанами.

    upload_2018-2-13_15-32-3.png
     
    Последнее редактирование: 13 фев 2018
    SergeiL нравится это.
  17. Crazy_Volt

    Crazy_Volt Нуб

    К сожалению, среди множества сообщении, мой взгляд упустил ответ на вопрос данный. Направьте, если не трудно. Я не ленивый,просто реально среди кучи сообщений об "интересном коде" и "роли LUA в интимной жизни программиста" мог пропустить :)
     
  18. Airbus

    Airbus Оракул

    Там же на этом сайте всё есть.Вы просто не понимаете на украинскоi мовi там внизу ссылка "Попереднi записи" давите на неё и попадёте на страницу где написано "ESP8266 NodeMCU Прошивка. Робимо WiFi розетку" что в переводе значит "програмируя ESP8266 делаем WiFi розетку". Вам ведь это надо?Копипастите код оттуда грузите в ESP8266 и будет Вам щастье-но не сразу а почему не сразу не скажу-посмотрите и найдите маленький косячок в коде и исправьте и тогда Щастье точно будет!Ну или почитайте наш Форум мы это обсуждали а искать где я не помню да и не буду за Вас.Удачи!Да ps там русский язык тоже есть если что.....
     
  19. Crazy_Volt

    Crazy_Volt Нуб

    Немного не то. В браузер грузится страница (допустим index.html). Как по нажатию на кнопке <button> в html-форме запустить выполнение другого lua скрипта?
     
  20. ИгорьК

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

    А это с другого форума:

    Зачитываюсь :)