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

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

  1. alp69

    alp69 Форумчанин

    Вот в этом же все дело.
    Кто это будет делать после нас. Потомки смогут?:D
    В нашем возрасте пора думать о потомках...:rolleyes:
    Это я философствую по случаю ДР. :cool:
     
  2. ИгорьК

    ИгорьК Гуру

    Это какой там возраст... Но все равно, ПОЗДРАВЛЕНИЯ :)
    А дело продолжат внуки! Главное воспитать. :)
     
    alp69 нравится это.
  3. alp69

    alp69 Форумчанин

    Спасибо :)
     
  4. ValentinIva

    ValentinIva Нуб

    ИгорьК здравствуйте!
    Я сделал счетчик воды по вашей инструкции. все работает. Но подключаю эту конструкцию к счетчикам через некоторое время все перестает работать. Почему не могу понять.
    p/s/ счетчики у меня БЕРЕГУН с импульсным выходом
     
  5. ИгорьК

    ИгорьК Гуру

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

    Повторяйте: http://forum.amperka.ru/threads/Умный-счетчик-расхода-воды-бюджетный-подарок-к-Новому-Году.10164/
     
    Последнее редактирование: 9 дек 2017
  6. ИгорьК

    ИгорьК Гуру

    Спрашиваем Народный Мониторинг значения датчиков.

    Вот код.
    Код (Lua):

    do
    local uuid = "Ваш Униальный uuid"
    local api_key= "Ваш уникальный api_key"
    --  Переменные для приема значений
    local press, pressval, wind, windval, temp, tempval
    -- local mysensors = "11113,9429,9197"
    -- Список номеров датчиков для запроса через запятую без пробелов
    local mysensors = "1312,9429,9197"

    local srv = net.createConnection(net.TCP, 0)

    local function parce(sck, c)
      -- print(c)
      local z = string.find(c, "{")
      c = string.sub(c, z)
      -- print(c)
      press, pressval, wind, windval, temp, tempval = string.match(c, '.-id":(%d+).-value":([+-]*%d+%.*%d*).-id":(%d+).-value":([+-]*%d+%.*%d*).-id":(%d+).-value":([+-]*%d+%.*%d*)')
      print(press, '=', pressval,'\n', wind, '=' , windval, '\n', temp, '=', tempval )
      sck = nil
      srv = nil
    end

    local function asknarod()
      srv:on("receive", parce)
      srv:on("connection", function(sck, c)
      -- sck:send("GET /api/sensorsOnDevice?id=1424&uuid="..uuid.."&api_key="..api_key.."&lang=en HTTP/1.1\r\nUser-Agent: Mozilla/5.0\r\nHost: narodmon.ru\r\nConnection: close\r\n\r\n")
      sck:send("GET /api/sensorsValues?sensors="..mysensors.."&uuid="..uuid.."&api_key="..api_key.."&lang=en HTTP/1.1\r\nUser-Agent: Mozilla/5.0\r\nHost: narodmon.ru\r\nConnection: close\r\n\r\n")

      end)
      srv:connect(80,"narodmon.ru")
    end
    asknarod()
    end
     
    Как работает:
    upload_2018-1-12_16-53-27.png


    Поскольку памяти в модуле не много, а ответы приходят типа:
    Код (Bash):
    HTTP/1.1 200 OK
    Server: nginx
    Date: Fri, 12 Jan 2018 13:13:55 GMT
    Content-Type: application/json; charset=utf-8
    Content-Length: 263
    Connection: close
    X-Powered-By: PHP/5.6.33
    Access-Control-Allow-Origin: *

    {"sensors":[{"id":1312,"type":1,"value":-6.3,"time":1515762720,"changed":1515762460,"trend":0},{"id":9197,"type":3,"value":764.1,"time":1515762540,"changed":1515761956,"trend":0},{"id":9429,"type":4,"value":2.02,"time":1515762556,"changed":1515762556,"trend":0}]}
    не рекомендуется за один прием запрашивать много датчиков, лучше вразбивку.

    Кстати, опросить свыше 5 датчиков и не удастся - одна из функций ограничивает количество своих аргументов и вызывает ошибку, если их число превышено.

    Также, не забывать об ограничении - запрос не чаще чем в одну минуту.
     
    Последнее редактирование: 19 янв 2018
  7. ИгорьК

    ИгорьК Гуру

    Модуль Народного Мониторинга (1).

    // Рекомендую открыть конечный код здесь, и после этого читать его описание.

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

    Ясно, что запрос информации с любого сайта есть явление асинхронное. То есть, нельзя написать некоторую функцию запроса, вызвать ее и за ней вызвать какой-то обработчик данных: не прокатит!
    Вот так делать бесполезно:
    Код (Lua):
    -- таблица значений датчиков
    t = {}
    function askNarod(tbl)
        -- получает таблицу для заполнения
        -- и что-то там спрашивает у Народного Мониторинга
        ...
    end
    function showdata()
        --значения таблицы датчиков
        print(t )
    end
    -- Поехали:
    askNarod(t)
    showdata()
    Это будет работать, но вывод на печать таблицы t всегда будет опережать получение в нее информации.

    Правильный будет алгоритм
    1. готовим функцию для обработки будущих значений;
    2. передаем ее асинхронной функции для исполнения ПОСЛЕ добычи информации.
    То есть, правильно:
    Код (Lua):
    -- таблица значений датчиков
    t = {}
    function showdata()
       --значения таблицы датчиков
        print(t)
    end
    function askNarod(tbl, callback)
        -- получает таблицу для заполнения и ссылку на фнкцию
        -- что-то спрашивает у Народного Мониторинга
        -- получив, заполняет таблицу
       -- и вызывает callback
        ....
        callback()
    end
    -- Поехали:
    askNarod(t, showdata)
    Друзья, этот алгоритм нужно хорошо понимать.


    Итак, с нашим модулем будем работать следующим образом:
    Код (Lua):
    do
    -- Таблица (делаем глобальной) для сохранения
    -- значений датчиков
    nSensors = {}

    -- callback функция, которая будет вызвана после
    -- получения значений от сервера
    local call = function()
        print('\n\n')
        -- Так можно печатать значения таблиц
        table.foreach(nSensors, print)
        -- Уничтожим переменную, которая будет содержать
        -- ссылку на модуль
        narod = nil
        -- Уничтожим сам модуль
        package.loaded["_asknarod"]=nil
    end

    -- Начало работы
    -- Загружаем модуль и получаем на него ссылку
    narod = require("_asknarod")
    -- Делаем таблицу с номерами запрашиваемых датчиков
    local sens = {11113,9429,9197}
    local uuid = 'Ваш UUID'
    local api_key = 'Ваш api_key'
    -- Устанавливаем необходимое для работы
    narod.setnarod(sens, uuid, api_key)
    -- Делаем запрос, с передачей ему как таблицы для значений
    -- так и функции, которая будет вызвана
    -- после допроса Народного Мониторинга
    narod.asknarod(nSensors, call)
    end
     
    Последнее редактирование: 16 янв 2018
  8. ИгорьК

    ИгорьК Гуру

    Модуль Народного Мониторинга (2).

    Любой модуль представляет собой таблицу.
    Несложно представить себе ее структуру. Накидаем:
    Код (Lua):
    local M = {}

    M.uuid = "Ваш UUID"
    M.api_key= "Ваш api_key"
    M.sensors = {1312,9429,9197}

    M.setnarod = function(sensors, uuid, api_key)
        ...
    end

    M.asknarod = function(sensnar, call)
        ...
    end

    return M
     
    Думаю, все предельно понятно. Таблица содержит три обязательных значения - uud, ключ и номера датчиков.

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

    Безусловно, в таблице должна быть главная функция - опрос Народного Мониторинга.

    Займемся функцией установки значений. Она такова:
    Код (Lua):
    M.setnarod = function(sensors, uuid, api_key)
        M.sensors = sensors or M.sensors
        M.uuid = uuid or M.uuid
        M.api_key = api_key or M.api_key
    end
    Предлагаю запомнить прием, с использованием оператора "or".
    Каждая строка означает, что следует присвоить установочное значение таблице, или, если его нет, то оставить прежнее.

    Как можно вызывать такую функцию? Не обязательно указывать ВСЕ значения. Можно, например, так:
    Код (Lua):
    M.setnarod = function({123, 457, 224}, , api_key)
    То есть, пропускать значения, в том числе и первое. Но наличие запятых - обязательно:
    Код (Lua):
    -- Меняет api_key, оставляет датчики и uuid
    M.setnarod = function( , , api_key)

    ============================

    Займемся функцией запроса значений с Народного Мониторинга. Часть этой функции есть в предыдущей теме.
    Подтянем свои знания чтением документации здесь.
    Логика там все та же: объявляется событие, готовится реакция на него и событие разрешается/запускается.

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

    Итак, спрашивать Народ мы будем следующим образом:
    Код (Lua):
    -- Объявляем соединение (событие)
    local srv = net.createConnection(net.TCP, 0)
    -- Реакция на получение ответа от сервера - вызов вункции парсинга ответа
    srv:on("receive", parce)
    -- Реакция на соедниение - запрос серверу
    srv:on("connection", function(sck, c)
        sck:send("GET /api/sensorsValues?sensors="..sensors.."&uuid="..M.uuid.."&api_key="..M.api_key.."&lang=en HTTP/1.1\r\nUser-Agent: Mozilla/5.0\r\nHost: narodmon.ru\r\nConnection: close\r\n\r\n")
    end)
    --    Все определено - соединяемся!
    srv:connect(80,"narodmon.ru")
    Заметим, что в теле запроса присутствует все три аргумента - датчики, UUID и api_key. Однако, в отличие от двух последних, датчики (sensors) не имеют перед собой буквы M - то есть не принадлежат таблице.
    Правильно - в запросе они перечисляются в текстовом виде через запятую без пробелов, а в нашей таблице М они находятся в цифровом виде, да еще в таблице. Значит, мы должны переработать таблицу M.sensors в строку типа "1345,5423,3242".
    Делаем это так:
    Код (Lua):
    -- Локальная переменная, которую потом воткнем в запрос
    local sensors = ""

    -- Хватаем таблицу M.sensors
    -- Извлекаем из нее данные и превращаем в стринг,
    -- добавляем запятую и формируем строку
    for _, v in pairs(M.sensors) do
        sensors = sensors..tostring(v)..","
    end
    -- Удаляем последнюю запятую
    sensors = string.sub(sensors, 1, -2)
     
    Последнее редактирование: 15 янв 2018
    alp69 нравится это.
  9. ИгорьК

    ИгорьК Гуру

    Модуль Народного Мониторинга (3).
    Выше мы сформировали запрос и сумели задать его серверу Народного Мониторинга.
    При определении событий, мы дали предписание модулю, по получению ответа сервера вызвать функцию парсинга:
    Код (Lua):
    srv:on("receive", parce)
    Пора ей заняться.
    Сначала, лучше всего, освежить голову чтением страниц 242-245 книги Иерусалимски - там говорится о захватах.

    Захват осуществляется функцией string.match(c, patt), которой передается два аргумента: с - текст для обработки, patt - шаблон по которому будет произведен захват.
    Функция возвращает набор переменных. Сколько содержится захватов в шаблоне - столько и вернется переменных.

    Исходя из анализа структуры ответов Народного мониторинга, он возвращает JSON ответ, который можно было бы ловить стандартным модулем NodeMCU Lua sjson и не париться. Однако есть проблема: ответ сервера настолько обширный по информативности, что таблица скорее всего забьет память модуля. Поэтому работаем с захватами, а модуль sjson, надеюсь, вы изучите самостоятельно.

    Итак, пара id датчика - value (значение) захватывается следующим шаблоном:
    Код (Lua):
    '.-id":(%d+).-value":([+-]*%d+%.*%d*)'
    Выглядит это ужасно, но при некоторой усидчивости и чтении книги вы поймете что к чему.
    Проблема в другом - при установке количества датчиков для запроса мы можем задать и один и три датчика. То есть распарсивать ответ придется с любым их количеством.
    Это создает некоторые трудности при формировании шаблона разбора ответа и еще большие при формировании итоговой таблицы значений.
     
    Последнее редактирование: 15 янв 2018
    SergeiL, alp69 и Securbond нравится это.
  10. ИгорьК

    ИгорьК Гуру

    Как же в целом выглядит функция парсинга:
    Код (Lua):
    -- Функция принимает данные от объекта srv, который
    -- передает их в количестве две штуки - ссылку на себя (здесь "sck") и,
    -- собственно, пришедший текст (эдесь "с")
    local parce = function(sck, c)
        -- Раскомментируйте и посмотрите их:
        -- print(c)
        -- Готовим шаблон выборки, умножая единичный на количество датчиков (#M.sensors) функцией string.rep
        local patt = string.rep('.-id":(%d+).-value":([+-]*%d+%.*%d*)', #M.sensors)
        -- Чтобы уменьшить объем памяти, вырезаем большой заголовок ответа сервера до "{"
        local z = string.find(c, "{")
        c = string.sub(c, z)
        -- Можно посмотреть что получилось:
        -- print(c)
        -- Это обработчик тела данных, выглядит просто, но не все так однозначно.
        -- "string.match" применяет шаблон "patt" к данным "c", выдает значения в количестве
        -- (заранее неизвестное) число датчиков умноженое на два и втыкает его в функцию "maketable"
        maketable(string.match(c, patt))
        -- Отработав, мажем масло маслом и уничтожаем и объект и ссылку на него на всякий случай
        sck = nil
        srv = nil
    end
    Внимание! Функция string.match(...) ограничивает число захватов десятью (реализация Lua NodeMСU). Поскольку на каждый датчик приходится два захвата, больше пяти датчиков опросить не удастся.

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

    Освежим то место, в которое едим, чтением Иерусалимски стр. 68-70 и за работу.

    Делаем:

    Код (Lua):
    -- Таким образом функия принимает любое число аргументов:
    local function maketable(...)
        -- Здесь будем изготавливать ключи таблицы
        -- то есть учитывать номера датчиков
        local nk = ""
        -- Эта штука выделяет пару цифровой ключ (k) и его значение (v) из таблицы
        -- которая автоматом формируется из пришедшего перечня аргументов выражением "{...}"
        for k, v in ipairs{...} do
            -- Если крюч четный, то это номер датчика
            if k%2 == 1 then
                -- мы его переводим в текст и приклеиваем к началу букву "d"
                nk = "d"..tostring(v)
                -- или можно так, без буквы:
                -- nk = tostring(v)
            else
                -- А если аргумент нечетный, то это эначение датчика
                -- вставляем его в табицу, которую получили для заполения как пара
                -- ключ (номер датчика) - его значение
                sensnar[nk] = v
            end
        end
        -- А отработав задачу - вызываем callback функцию:
        call()
    end
     
    Последнее редактирование: 19 янв 2018
    alp69, SergeiL и Securbond нравится это.
  11. ИгорьК

    ИгорьК Гуру

    И в заключение, модуль работы с Народным Мониторингом _asknarod.lua выглядит так:
    Код (C++):
    local M = {}

    M.uuid = "ВАШ UUID"
    M.api_key= "ВАШ api_key"
    -- Заранее датчики, но можно и без них, только "{}"
    M.sensors = {1312,9429,9197}

    M.asknarod = function(sensnar, call)
        local sensors = ""
        for _, v in pairs(M.sensors) do
            sensors = sensors..tostring(v)..","
        end  
        sensors = string.sub(sensors, 1, -2)
     
        local srv = net.createConnection(net.TCP, 0)
         
        local function maketable(...)
            local nk = ""
            for k, v in ipairs{...} do
                if k%2 == 1 then
                    nk = "d"..tostring(v)
                else
                    sensnar[nk] = v
                end
            end
            call()
        end
     
        local parce = function(sck, c)
            -- print(c)
            local patt = string.rep('.-id":(%d+).-value":([+-]*%d+%.*%d*)', #M.sensors)
            local z = string.find(c, "{")
            c = string.sub(c, z)
            -- print(c)
            maketable(string.match(c, patt))
            sck = nil
            srv = nil
        end

        srv:on("receive", parce)
        srv:on("connection", function(sck, c)
            sck:send("GET /api/sensorsValues?sensors="..sensors.."&uuid="..M.uuid.."&api_key="..M.api_key.."&lang=en HTTP/1.1\r\nUser-Agent: Mozilla/5.0\r\nHost: narodmon.ru\r\nConnection: close\r\n\r\n")
        end)  
        srv:connect(80,"narodmon.ru")

    end

    M.setnarod = function(sensors, uuid, api_key)
        M.sensors = sensors or M.sensors
        M.uuid = uuid or M.uuid
        M.api_key = api_key or M.api_key
    end

    return M
    Модуль вызывается так:
    Код (Lua):
    do
    local nSensors = {}

    local call = function()
        table.foreach(nSensors, print)
        narod = nil
        package.loaded["_asknarod"]=nil
    end

    narod = require("_asknarod")
    local sens = {11113,9429,9197}
    narod.setnarod(sens)
    narod.asknarod(nSensors, call)
    end
    =================
    Рекомендую всем, кому небезразлично, разобраться с эти примером, даже если он вам практически не нужен.
    Здесь, несколькими строками кода, мы решили следующие задачи:
    • построили модуль
    • организовали взаимодействие с асинхронной функцией через callback
    • научились парсить ответ сервера
    • научились принимать переменное число аргументов
    • научились лабать таблицы из разрозненных аргументов

    Удачи, друзья, терзайте Народный Мониторинг, чтобы не зазнавался :)
     
    SergeiL нравится это.
  12. ИгорьК

    ИгорьК Гуру

    И в заключение работы с модулем его обязательно надо проверить на потерю памяти.
    Это касается вообще любого кода и любого проекта - ищем утечку памяти и лишние глобальные переменные.
    Поэтому, запускаем такой код:
    Код (Lua):
    do
    local nSensors = {}

    local call = function()
        print("\n")
        table.foreach(nSensors, print)
        print("\n", node.heap())
        narod = nil
        package.loaded["_asknarod"]=nil
    end

    local function boooze()
        narod = require("_asknarod")
        local sens = {11113,9429,9197}
        narod.setnarod(sens)
        narod.asknarod(nSensors, call)
    end
    boooze()

    tmr.create():alarm(60000, 1, boooze)
    end
    Смотрим что происходит с памятью:
    upload_2018-1-16_13-9-5.png

    А также проверяем, нет ли неплановых хвостов в виде висящих в памяти глобальных переменных:

    Код (Lua):
    do
    print("\n\n\n\n\n\n\n\n\n\n\n\n\n=========== _G table: ===========")
    table.foreach(_G, print)
    print("===== package.loaded table: =====")
    table.foreach(_G.package.loaded, print)
    print("=================================")
    end

    upload_2018-1-16_13-14-11.png

    Следующий шаг.
     
    Последнее редактирование: 14 фев 2018
    alp69, MESS, SergeiL и ещё 1-му нравится это.
  13. MESS

    MESS Гик

    ИгорьК - Спасибо Вам большое за статьи по ESP, респект! Очень доходчиво и грамотным русским языком.
    Удачи в делах!
     
  14. ИгорьК

    ИгорьК Гуру

    Спасибо!
     
  15. Добрый день , подскажите пожалуйста ссылочку вот к примеру маска ":(%d+).-value":([+-]*%d+%.*%d*)' где почитать как ее составлять? ибо похоже на и java но нет не оно с++ тоже не то...
     

  16. Добрый день нужна помощь копирую пример в esplorer получаю ошибку в строке tmr.create():alarm(5000, tmr.ALARM_AUTO, function()

    call field 'create' (a nil value)
    что я не так делаю? почему нет метода create ??
     
  17. ИгорьК

    ИгорьК Гуру

    Два варианта.

    1. У вас старая прошивка модуля. Надо сходить https://nodemcu-build.com/ и руководствуясь первым пунктом этих заметок перешить модуль.

    Прошивка при перезагрузке должна показывать что-то типа:
    upload_2018-1-19_16-34-2.png

    2. При копипасте кода с этого сайта иногда проскакивают скрытые символы. Что делать - набирать текст руками. Это проблема совместимости некоторых версий браузеров с этим форумом.
     
    Последнее редактирование: 19 янв 2018
  18. ИгорьК

    ИгорьК Гуру

    Внимательнее, там указана ссылка на книгу Иерусалимски "Программирование на языке Lua".
    Это называется "Захваты".
    upload_2018-1-19_16-39-28.png
     
  19. alp69

    alp69 Форумчанин

    У меня есть не в pdf, а в fb2. С кликабельным оглавлением.
     
  20. alp69

    alp69 Форумчанин

    Удалите цитату. Не подставляйте коллегу!