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

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

  1. ИгорьК

    ИгорьК Гуру

    Что такое MQTT.

    Активный участник этой темы снял потрясное видео о протоколе MQTT.
    Этот протокол, большей частью, и используется в Интернете Вещей.

    В общем, для реального пипла, получающего высшее образование по сплетням, фильмам и форумам, рекомендую:


    Не забывайте там лайкать, и писать в комментах, что я прислал :)

    А для тупняков, которые вообще не схватывают и русский не знают, даю ссылку, где на картинках все нарисовано.
    mqtt5.jpg
     
    Последнее редактирование: 25 мар 2018
    Mitrandir и Ujine нравится это.
  2. ИгорьК

    ИгорьК Гуру

    Счетчик расхода воды. Пошагово.

    Сырье здесь.

    Внимание! Здесь рассматривается логика кода для понимания основ программирования.
    Готовый проект (для ардуинщиков, тех кто повторяет а не учится программировать) - здесь.

    Вдохновившись видео из предыдущего поста и, благодаря ему, поняв все о мире IoT приступим к неспешному изготовлению счетчика расхода воды.
    Железо повторим из этого проекта.
    [​IMG]

    Или еще проще:

    water08.jpg


    1. Как зайти в сеть.
    Первоисточник здесь.
    Модуль ESP-8266 может работать с сетью в четырех режимах:
    1. как точка доступа
    2. как клиент
    3. как то и другое вместе
    4. никак не работать
    Для нас, древних и замшелых, интересен клиент.
    Чтобы зайти в сеть, следует, ясный пень,
    1. установить соответствующий режим
    2. загнать в память модуля реквизиты сети
    3. дать команду "фас"
    Все это можно выполнять программно каждый раз при включении модуля, но есть тонкость: если залить во флэш-память необходимые реквизиты - повторно это можно не делать.

    Удобнее всего в ESPlorer создать сниппет (шаблон, который вызывается кнопкой в ESPlorer) и заводить в сетку каждый вновь прибывший модуль однократно, после чего он сам будет восстанавливать соединение при включении и нам остается только ожидать приятных сюрпризов самостоятельного появления wifi соединения.

    Делаем сниппет:
    Код (Lua):
    wifi.setmode(wifi.STATION) -- установка режима
    wifi.sta.clearconfig() -- очистка от барахла
    local scfg={} -- таблица установок ржима
    scfg.auto = true -- входить и поддерживать сеть автоматически
    scfg.save = true -- запомнить эти установки во флэше
    scfg.ssid = 'AP_Home' -- название сетки
    scfg.pwd = 'superpassword'-- пароль сетки
    wifi.sta.config(scfg) -- конфигурируем сеть
    wifi.sta.connect() -- старт соединения
    -- через 15 секунд посмотрим что получилось:
    tmr.create():alarm(15000, tmr.ALARM_SINGLE, function() print('\n', wifi.sta.getip()) end)
    Вот как выглядит сниппет установки сети;
    water01.jpg

    Итак, на модуль подали питание и ему требуется некоторое время, чтобы войти в сеть.
    Вход в сеть - событие. Раньше чем оно не наступит делать мы ничего не будем.

    Первый файл, который мы загрузим в ESP-8266 будет модуль, который и будет ждать соединения с сеткой getwifi.lua.
    Код (Lua):

    -- При загрузке любого модуля ему передается
    -- единственный параметр - его имя.
    -- Имя модуля ловится так:
    local modn = ...
    print("Got name", modn)
    -- Локальная таблица
    local M = {}
    -- Эта таблица состоит из одной функции
    -- которой передается callback - то
    -- что надо будет сделать после входа в сетку
    M.getwifi = function(call)
        -- есть ли сетка?
        local ip = (wifi.sta.getip())
        -- функция выполнит callback и
        -- выгрузит модуль  из памяти
        local killall = function()
           -- выполнение callback
           if call then call() end
           -- самоуничтожение модуля
           package.loaded[modn] = nil
        end

        -- если мы не в сетке
        if not ip then
            -- создаем СОБЫТИЕ - постоянный таймер на 5 секунд
            tmr.create():alarm(5000, 1, function(t)
                -- узнаем есть ли сетка
                ip = (wifi.sta.getip())
                print("ip now", ip)
                -- если сеть
                if ip then
                    -- останавливаем таймер
                    tmr.stop(t)
                    -- убиваем таймер
                    tmr.unregister(t)
                    -- убиваем переменную, что содержит таймер
                    t = nil
                    -- выполняем что надо
                    killall()
                end
            end)
            -- событие обработки соединения создано -
            -- начинаем само соединение
            wifi.sta.connect()
        -- ну а если в мы в сетке
        else
            -- выполняем что надо
            killall()
        end
    end
    return M

    Вызовем модуль вот такой процедурой:
    Код (Lua):
    do
    -- загрузка модуля
    wf = require("getwifi")
    -- готовим реакцию на событие: что будет, когда появится сеть? А вот что:
    -- callback функция, которая выполняется после входа в сеть
    local call = function()
        print("Yes! It Works!")
        -- убиваем переменную, что содержала модуль
        wf = nil
        print("Kill Module, wf = ", wf)
    end
    -- вызываем функцию из модуля и передаем ей callback
    wf.getwifi(call)
    end
    И увидим:
    1. загрузка модуля
    2. вставляем код вызова
    3. выделяем код, отправляем на исполнение клавишей Block
    4. видим результат

    water02.jpg
     
    Последнее редактирование: 22 окт 2020
    alp69 и SergeiL нравится это.
  3. ИгорьК

    ИгорьК Гуру

    2. Как работать с герконом (он же кнопка)
    Еще один модуль - debm.lua

    Модуль работает с дребезгом и через 2,5 секунды после размыкания геркона на счетчике выполняет callback функцию, которую принимает при старте.

    Код (Lua):
    -- Генерим новые таблицы
    local function setnew()
        return {}
    end
    -- Таблица для экспорта
    local M = {}
    -- Одна функция в таблице
    -- принимает номер ноги и callback для исполнения
    M.set = function(pin, short)
    -- Устанавливаем ногу на прием
    gpio.mode(pin, gpio.INPUT, gpio.PULLUP)
    -- Добываем пустую таблицу для этой ноги
    local o = setnew ()
    -- Нога еще будет нужна для опроса
    o.buttonPin = pin
    -- Счетчик результато опроса ноги
    o.cicle = 0
    -- Словили нажатие или нет
    o.gotpress = false
    -- Склад callback(ов)
    o.doshort = short
    -- Состояние ноги для отлова
    o.catch = 0
    -- Функция работы с ногой
    o.startpin = function(self)
        -- Установка события - прерывание на обвал ноги в ноль
        -- Очищаем прерывание
        gpio.trig(self.buttonPin)
        -- Устанавливаем заново
        gpio.trig(self.buttonPin, "down",function (level)
            -- Первый ноль - отключаем дальнейшую реакцию на прерывания
            if self.gotpress == false then
                self.gotpress = true
                -- Функция, которая выполнится в результате срабатывания геркона
                local function exitnow(buf)
                    -- Таймер опроса остановлен, снят с регистрации и уничтожен
                    tmr.stop(buf)
                    tmr.unregister( buf)
                    buf = nil
                    -- Выполняем callback
                    self.doshort()
                    -- Устанавливаем в исходное таблицу
                    self.cicle, self.gotpress = 0, false
                end
                -- На первое прерывание устанавливаем таймер на 50 мс
                tmr.create():alarm(50, 1, function(buf)
                    -- Который читает ногу и считает результаты считывания
                    -- в зависимости от состояния o.catch, сначала ловит ноль,
                    -- а потом единицу - то есть геркон размкнулся
                    if gpio.read(self.buttonPin) == o.catch then
                        self.cicle = self.cicle + 1
                    end
                    -- Если 5 х 50 = 250 мс
                    if self.cicle >=5 then
                        -- если геркон еще разомкнут
                        if o.catch == 0 then
                            -- устанавливаем счетчик в минус 15 чтобы
                            -- сработка произошла через 20х50 = 2500 (2,5 секунды)
                            -- после азмыкания геркона
                            self.cicle = -15
                            -- меняем результат считывания на единицу
                            o.catch = 1
                        -- если геркон замкнулся
                        else
                            -- счетчик в ноль
                            self.cicle = 0
                            -- данные для отлова в ноль
                            o.catch = 0
                            -- выполняем callback
                            exitnow(buf)
                        end
                    end
                end)
            end
        end)
    end
    -- Поехали:
    return o:startpin()
    end
    return M

    Вызываем так:
    Код (Lua):
    do
    -- Склад данных о воде
    water = {
        cool = 0,
        hot = 0
    }
    -- вызов модуля
    deb = require("debm")
    -- callback для третей ноги
    function doshort3()
        water.cool = water.cool + 0.01
        print("Cool got "..water.cool.." m3.")
    end
    -- callback для четвертой ноги
    function doshort4()
        water.hot = water.hot + 0.01
        print("Hot got "..water.hot.." m3.")
    end
    -- определение callback(ов) и ног
    deb.set(3, doshort3)
    deb.set(4, doshort4)
    end
    Видим это:
    water03.jpg

    1. загружаем модуль
    2. вставляем код
    3. выделяем код и жмeм Block
    4. нажимаем на кнопку/геркон и наблюдаем

    Бонус. Вот здесь я для вас библу накатал, чтобы работать с кнопками по-полной.
     
    Последнее редактирование: 6 сен 2017
    alp69 и SergeiL нравится это.
  4. ИгорьК

    ИгорьК Гуру

    3. Соединение с брокером MQTT.
    Соединение будем осуществлять файлом workmqtt.lua

    Код (Lua):
    -- Вспомогательные элементы, потом удалим
    if not water then water = {hot = 0, cool = 0} end
    if not mod then mod = {} end

    -- Глобальные переменные
    mtop = "" -- Топик от брокера
    mload = "" -- Данные в топике
    myClient = "water02" -- Имя клиента
    local pass = "superpass" -- Пароль

    -- Создаем MQTT объект
    m = mqtt.Client(myClient, 60, myClient, pass)
    -- При потере связи брокер выдаст сообщение
    m:lwt(myClient, "OFF", 0, 0)

    -- Событие - потеря связи
    m:on("offline", function(con)
        -- Закрыть соединение
        m:close()
        -- Сбросить флаг наличия связи с брокером
        mod.broker = false
    end)

    -- Событие - приход сообщения
    m:on("message", function(conn, topic, data)
        -- Выделяем чистый топик, без информации  об устройстве
        topic = string.gsub(topic, myClient.."/","")
        -- Делаем соообщение глобальным
        mtop = topic
        mload = data
        print(mtop, mload)
        -- Вызываем обработчик глобального сообщения
        -- dofile("analize_broker.lua")
    end)

    -- Выполняем соединение с брокером
    m:connect("iot.eclipse.org", 1883, 0, 0,
        function(con)
            print("Connected to Broker")
            -- Подписываемся на два топика - установки горячей и холодной воды
            m:subscribe({[myClient.."/cool"]=0,[myClient.."/hot"]=0}, function(conn)
                print("Subscribed.")
            end)
            -- Публикуем сообщение на брокере что мы в сети
            m:publish(myClient, "ON", 0,0)
            -- Устанавливаем флаг связи с брокером
            mod.broker = true
    end)
    Однако вызвать его "в лоб" нельзя: у нас есть wifi соединение или нет? Значит вызов должен быть осуществлен только в callback модуля getwifi.lua, что рассматривали ранее.

    Итак, как это вызывается:
    Код (Lua):
    do
    wf = require("getwifi")
    local call = function()
        print("Yes! It Works!")
        wf = nil
        print("Kill Module, wf = ", wf)
        -- Новая строчка:
        dofile("workmqtt.lua")
    end
    wf.getwifi(call)
    end
    И как выглядит:
    water008.jpg
    1. загружаем два файла
    2. набираем код выполнения
    3. выделяем код и нажимаем кнопку Block
    4. наблюдаем результат
    Кроме того, пора разобраться с таким явлением как MqttSpy. Это задание для самостоятельной работы.

    Запустите приложение, установите связь со своим брокером и подпишитесь на топик "water02/#"

    В результате надо получить вот такую картинку:

    water009.jpg
     
    Последнее редактирование: 23 авг 2017
    alp69, Securbond и SergeiL нравится это.
  5. ИгорьК

    ИгорьК Гуру

    4. Логика программ и блок MQTT чуть глубже.

    Пора сказать несколько слов об организации программ.
    ESP-8266 интересное явление. Оперативки чуть, а флэша - сколько напаяешь.
    ВердуиноЯзык этому не учит, а вот Lua позволяет в ходе основного тела программы выполнять код прямо из флэша.

    Для этого действа используется (в основном) два приема.
    • dofile("ИМЯФАЙЛА.lua")
    • require("ИМЯФАЙЛА.lua")
    dofile - прямо выполняет код из файла, после чего все что там было объявлено локальным - удаляется из памяти автоматом.

    require - загружает код в память и делает доступными функции, которые в нем содержатся. require имеет возможность выгружать из памяти модули в "ручном режиме" и вычищать память.

    При внешнем различии, глубинной разницы между этими способами немного.
    Различие в том, что если вызываемому коду надо передать данные, в dofile это делается через глобальные переменные, а в результате действий require становятся доступными функции, которым можно передавать аргументы обычным порядком. (И в dofile можно передавать данные, но для этого код должен возвращать анонимную функцию для исполнения. Это требует определенных навыков)

    В этом проекте мы видим оба способа вызовов программ из памяти модуля.

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

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

    Итак, для общения вещей из интернета нужны:
    1. два общающихся и
    2. брокер между ними.
    Любое конечное устройство может выступать и передатчиком информации и приемником и "оба рядом".
    Протокол легкий, и зарегламентирован по самые помидоры. А именно,
    • информация передается в виде пары: ТОПИК and ДАННЫЕ.
    • и то и другое представляет собой строки.
    За раз модуль передает одну пару.

    Есть три режима доставки пары в работе брокера (качество обслуживания): "0" - пнул и забыл, "2" - два раза перепроверил, и "1" - что-то между нулем и двойкой.

    Есть режим сохранения последнего сообщения на брокере. Смысл. Когда на брокер приходит сообщение, он передает его всем подписчикам, КОТОРЫЕ В ЭТОТ МОМЕНТ висят на связи с ним, после чего сообщение херится полностью.

    Если включен режим сохранения (включение передается в теле источника сообщения) - брокер помнит последнее сообщение источника и втыкает его каждому вновь появившемуся подписчику. Причем если подписчик "ушел", а потом - "вернулся", сообщение втыкается ему вновь.

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

    Чтобы "срубить" режим сохранения сообщения, источник должен отправить на брокер пару ТОПИК(как обычно) и ДАННЫЕ - пустое поле.

    В заключение, надо знать - есть "последняя воля погибшего" - lwt. При установлении связи с брокером клиент передает ему специальное сообщение, которое будет опубликовано всем подписчикам, если клиент "почит в бозе".

    Оно может и не передаваться, но делать его - хорошая практика. Вот смотрите, при установке связи с брокером мы можем передать в некоторый топик единожды, например, сообщение "ON". Подписчики, прочитав его, узнают что "пиво есть!".

    Если же одновременно будет создано сообщение в этот же топик как "последня воля" с данными "OFF" - те же клиенты узнают о потере связи. Это очень и очень удобно. В нашем мини проекте все так и реализовано.

    Теперь кусочки кода workmqtt.lua в картинках.

    water04.jpg

    water05.jpg
     
    Последнее редактирование: 18 ноя 2017
    Securbond, SergeiL и alp69 нравится это.
  6. ИгорьК

    ИгорьК Гуру

    5. Обработка управляющей информации.
    В файле workmqtt.lua есть закомментированная строка - dofile("analize_broker.lua")
    analize_broker.lua
    - реакция на данные, пришедшие от брокера нашему устройству.

    !!! не забудьте расскоментировать эту строку и перезагрузить файл workmqtt.lua в модуль.

    А что устройство может получить от брокера? Установочную информацию о текущем положении дел на расходомерах воды. Наше же устройство просто считает размыкания геркона и не знает, сколько воды израсходовано до наших злодейств.

    В общем, workmqtt.lua обеспечивает реакцию на событие - прием пары Топик/Данные от брокера, путем формирования двух глобальных переменных mtop и mload а затем вызывает обработчик.

    Вот его код.
    Код (Lua):
    -- служебное:
    if not water then water = {} end
    myClient = "water02"
    -- копируем обе глобальные переменные в локальные
    -- чем освобождаем глобальные для других возможных событий
    -- в нашем случае не обязательно, но это хорошая практика
    local top = mtop

    -- поскольку MQTT гоняет текстовую информацию, преобразуем
    -- ее в цифровую. Причем, если она "непреобразуемая" - заменяем нулем
    local data = tonumber(mload) or 0

    -- Если пришли нормальные цифры
    if data ~= 0 then
        -- вставляем их в таблицу текущих значений холодной и горячей воды
        if top == "cool" then
            water.cool =  data
            print("Set water.cool to "..water.cool)
        end
        if top == "hot" then
            water.hot = data
            print("Set water.hot to "..water.hot)
        end

        --[[
        Публикуем на брокер пустые данные в топики
        установки значений воды. Зачем? Вспоминаем, что
        так снимается флаг "retain" - сохранения данных для
        вновь появившихся подписчиков.
        --]]

        m:publish(myClient.."/"..top, "", 0, 1)
    end

    Теперь скрипт запуска будет выглядеть так:
    Код (Lua):
    do
    mod = {}
    water = {
        cool = 0,
        hot = 0,
    }

    deb = require("debm")
    function doshort3()
        water.cool = water.cool + 0.01
        print("Cool got "..water.cool.." m3.")
    end

    function doshort4()
        water.hot = water.hot + 0.01
        print("Hot got "..water.hot.." m3.")
    end
    deb.set(3, doshort3)
    deb.set(4, doshort4)

    function gotmqttnow()
        wf = require("getwifi")
        local call = function()
            dofile("workmqtt.lua")
            wf = nil
            print("wf = ", wf)
        end
        wf.getwifi(call)
    end
    gotmqttnow()
    end
    Ничего нового в нем нет, а вот работа с ESPLorer и MqttSpy доставит нам удовольствие.

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

    Понаблюдаем за реакцией устройства, понажимаем кнопочки...
    water06.jpg

    water07.jpg

    Мы видим, что устройство начинает оживать - можно установить начальные значения воды, а срабатывание геркона/кнопки их увеличивает, что и видно в консоли ESPLorer(a).

    Осталось наладить периодическую отправку текущих значений воды обратно брокеру и дело сделано...
     
    Последнее редактирование: 24 авг 2017
    alp69 нравится это.
  7. ИгорьК

    ИгорьК Гуру

    6. Публикуем данные.
    Нынче изготовим два файла - публикации и сохранения данных.
    Для публикации - pubmqtt.lua
    Код (Lua):

    -- Если все соединено
    if mod.broker == true then
      -- Вспомогательное
      if not water then water = {} end
      if not sent then sent = {} end
      -- Сохраним данные, что будем отправлять
      dofile("savedata.lua")
      -- Локальная таблица - сюда скопируем данные для отправки
      local topub = {}
      -- Копируем данные
      for k,v in pairs(water) do
          -- для отправки
          table.insert(topub, {k,v})
          -- в таблицу отправленных данных
          sent[k] = v
      end
      -- Публикация
      local function punow()
          -- Если в таблице что-то есть
          if #topub ~= 0 then
              -- Изымаем из нее оди н элемент -  таблицу из двух элементов
              local tp = table.remove(topub)
              -- Формируем топик из первого элемента
              local top = "water02".."/"..tp[1].."/state"
              -- Данные - ворой элемент
              local dat = tp[2]
              -- Публикуем
              m:publish(top, dat, 0,0,
                  -- и в callack рекурсивно вызываем себя
                  function(cl)
                      punow()
              end)
          end
      end
      -- Первый запуск публикации
      punow()
    end
     
    Еще один файл - сохраняет последние публикуемые данные в память модуля.

    Зачем это нужно? Если произойдет сбой питания или перезагрузка модуля по какой-то причине, при старте мы восстановим последние данные.
    Файл savedata.lua:

    Код (Lua):
    do
    -- Открываем файл на запись,
    -- этот временный файл потом будет переименован
    -- в файл с названием "data.lua"

    lst = file.open("list.lua", "w")
    -- Если открыли
    if lst then
        -- Локальная переменная
        local line
        -- Читаем пару значений из таблицы сосотяния воды
        for k,v in pairs(water) do
            --Формируем строку типа "water.cool = 22.11"
            line = "water."..k.."="..v.."\n"
            -- Пишем строку в файл
            lst:write(line)
        end
    end
    -- Закрываем файл, удаляем ссылку на него
    lst:close(); lst = nil
    -- Удаляем прежний файл с данными
    file.remove("data.lua")
    -- Переименовываем файл
    file.rename("list.lua", "data.lua")
    end
     
    Последнее редактирование: 25 авг 2017
  8. ИгорьК

    ИгорьК Гуру

    7. Объединяем все в одну программу.
    В одну программу объединит все файл main.lua.
    Что же он делает в целом?
    • Создает глобальные таблицы water и sent
    • Вызывает файл загрузки water сохраненными данными
    • Назначает функции на герконы
    • Запускает функцию соединения с wifi и брокером
    • Запускает таймер отправки данных о воде на брокер
    Практически все элементы этого скрипта мы рассмотрели ранее - там где говорилось о способах вызова того или иного скрипта, поэтому ничего сложного в этом вы не увидите.

    Вот он:
    Код (Lua):
    -- main.lua
    -- Глобальная таблица для поддержания информации
    -- о состоянии соединения.
    mod = {}
    -- Таблица данных счетчиков воды
    water = {
        cool = 0,
        hot = 0,
    }
    -- Таблица состояния последних переданнных данных
    sent = {
        cool = 0,
        hot = 0,
    }
    -- Загрузка таблицы счетчика сохраненными данными
    -- о последней отправке при включении питания
    dofile("data.lua")

    -- Вызов модуля обратки дребезга
    deb = require("debm")

    -- Две функции увеличения данных в таблице воды
    function doshort3()
        water.cool = water.cool + 0.01
        print("Cool got "..water.cool.." m3.")
    end

    function doshort4()
        water.hot = water.hot + 0.01
        print("Hot got "..water.hot.." m3.")
    end

    -- Установка обработчиков герконов
    deb.set(3, doshort3)
    deb.set(4, doshort4)

    -- Зпуск всех соединений
    function gotmqttnow()
        -- Загрузка модуля ожидания wifi
        wf = require("getwifi")
        -- Сallback на появление сети
        local call = function()
            -- Создание объекта MQTT, подписки, обработки установочных
            -- данных о воде
            dofile("workmqtt.lua")
            -- уничтожение переменной, содержавшей ссылку на модуль wifi,
            -- выгрузка этого модуля происходит в нем самом после появления
            -- сети
            wf = nil
            print("wf = ", wf)
        end
        wf.getwifi(call)
    end
    -- первый запуск после включения
    gotmqttnow()

    -- Таймер периодической отправки данных на брокер
    tmr.create():alarm(30000, 1,  function()
        -- Только если информация изменилась
        if (water.cool ~= sent.cool) or (water.hot ~= sent.hot) then
            dofile("pubwater.lua")
        else
            print("No changes!")
        end
    end)
     
    Последнее редактирование: 25 авг 2017
  9. ИгорьК

    ИгорьК Гуру

    8. Заключение.
    В этом небольшом проекте я изложил основные подходы к написанию законченных решений на языке Lua для ESP-8266.

    Обобщим и подведем итог.
    Вот как выглядит работающее устройство:
    water09.jpg

    В проекте показаны основные моменты, которыми отличается программирование Lua от Си.

    Мы определяем необходимые нам события и готовим реакцию на них.

    Код в проектах исполняется асинхронно и нам нет причин думать о том, как "развести" между собой то или иное событие.

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

    В нашем проекте обработичик геркона считает сам по себе размыкания и записывает их в таблицу. Заметьте: публикация результатов никак не взаимодействует со счетчиком. Данные берутся и публикуются. Мы не пытались "сбалансировать" эти события между собой и не думали: "А что будет, если в момент публикации (а это время!) произойдет срабатывание геркона?" Да ничего не будет - система сама это увяжет, счетчик увеличится, и если опубликован старый результат, в следующий раз опубликуется новый.

    Программы на Lua можно делать из кусочков-скриптов, что позволяет задействовать огромный ресурс памяти, при ограниченной оперативке. Замечу, что такого нет даже в Espruino/Iskra JS.

    Вызов кусочка-скрипта осуществляется через dofile("script.lua"). При необходимости для этого скрипта могут быть подготовлены данные в виде глобальных переменных, таблиц.
    Этот же скрипт может и оставлять за собой глобальные данные.

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

    Вызывать постоянно функцию collectgarbage(), как пишут на форумах, не обязательно - система делает это самостоятельно.

    Если требуется постоянное "зависание" в памяти некоторых функций, можно делать модули.
    Применение модулей позволяет гибко передавать данные в их функции, в том числе и задействовать callback(и).

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

    Таким образом, программирование на Lua отличается от Си не другими управляющими конструкциями (они везде приблизительно одинаковые), а логикой, к которой сразу непросто привыкнуть.

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

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

    Удачи всем любителям Ардуино :)

    water10.jpg


    Дальше: Маленький Сервер.
     
    Последнее редактирование: 19 янв 2018
    alp69, SergeiL, Securbond и ещё 1-му нравится это.
  10. Securbond

    Securbond Гуру

    Здравствуйте Игорь. Я так понимаю, что в конечном варианте вы переименовали модуль - pubmqtt.lua в pubwater.lua . ?
     
  11. ИгорьК

    ИгорьК Гуру

    Видимо. Все файлы на bitbucket - посмотрите.
     
  12. ИгорьК

    ИгорьК Гуру

    12. Маленький сервер.

    Введение: совсем маленький сервер с пояснениями.

    Cоздадим сервер, да не простой, а с подстановкой данных из таблицы в генерируемую html страницу.

    1. Начнем со страницы. Идем, например, сюда и добываем код html:
    Код (HTML5):
    <p align="center"><img alt="" height="72" src="http://forum.amperka.ru/styles/AmperkaXF/logo.png" width="148"></p>
    <p align="center">Температура в доме: $temp</p>
    <p align="center">Влажность в подвале : $humi</p>
     
    2. Сохраняем страничку с названием page.lua и загружаем этот код в ESP-8266 следующим образом:

    upload_2017-11-14_13-31-36.png

    1 - вставляем код в левое окно
    2 - сохраняем файл с именем page.lua
    3 - отправляем файл на модуль
    4 - получаем ошибку исполнения: а мы там ничего для исполнения и не заказывали, так что все нормально
    5 - жмем кнопку Reload и видим файл и в окне (6) и на панели (7)
    У меня там еще всякие другие - не обращайте внимания.

    3. Создаем и запускаем файл server.lua вот таким образом:
    Код (Lua):
    do
    dat = {
        temp = 27.4,
        humi = "Не знаю, но там сухо!"
    }
    local function expand (s)
        return (string.gsub(s, "$(%w+)", dat))
    end
    srv = net.createServer(net.TCP)
    function receiver(sck, data)
        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()
            else
              sck:close()
            end
        end
        sck:on("sent", closec)
        send()
    end
    srv:listen(80, function(conn)
      conn:on("receive", receiver)
    end)
    end
    4. Профит в окошке браузера:

    upload_2017-11-14_13-45-7.png
    Это пример.
    Чтобы все работало, в табличку dat нужно закидывать правильные данные. Температуру я написал в виде числа, а влажность - текстом, причем кириллицей, чтобы была видна ее поддержка.

    В html страничке для замены должны находиться текстовые переменные формата "$temp"
    upload_2017-11-14_15-18-21.png
    В таблице dat им соответствует поле temp = значение.
    upload_2017-11-14_15-19-22.png
    Сервер при коннекте к нему построчно читает содержание файла page.lua, находит переменные, заменяет их табличными данными и выдает в браузер.

    Помня о том, что код в Lua асинхронный, приляпать этот сервер к любому проекту - плевое дело. Таблица dat (или аналогичная с другим названием), обычно, рабочая таблица данных в каком-то другом файле, и она глобальна.
    А если не таблица, а россыпь глобальных переменных? Вспоминаем что значит _G.

    Создаем сервер и необходимую страничку, сервер запускается в каком-нибудь месте программы
    Код (Lua):
    dofile("server.lua")
    и можно забыть о его существовании.

    Если только не нужно обработать GET запрос...
     
    Последнее редактирование: 28 апр 2018
    tpolimer, alp69, SergeiL и ещё 1-му нравится это.
  13. alp69

    alp69 Форумчанин

    Если пойти дальше этим путем, то родится альтернатива Openhab'у? На одной ESP будет крутиться сервер, а остальные устройства будут ему периодически слать свои файлы с таблицами. Или у каждого модуля свой сервер, но с возможностью по ссылке перейти на сервер любого модуля в сети...
    На главной странице - ссылки на странички администрирования каждого модуля в сети. В т.ч. заливка кода "на лету".
    Эх... фантазия разыгралась... :p
     
  14. ИгорьК

    ИгорьК Гуру

    Вообще, меня от серверов на ESP-8266 не плющит. Этот пост - дань моде.
    Это же куруто - завести модуль в сеть и поднять на нем сервер: умный дом(ик) :)
     
    Последнее редактирование: 7 янв 2021
  15. alp69

    alp69 Форумчанин

    Дык это же здорово!
    Малину нафик! Даешь территориально распределенный моск умного дома! ;):) Можно ведь реализовать таким образом более гибкую систему. Ведь если Малина (флэшка в Малине) сдохнет, то придется вручную все опять восстанавливать. И данные не восстановить. А так - система дублируется на модулях. Данные не потеряются. Только к модулям sd-карточки добавить и все... Даже новый модуль, встав на место сдохшего, сможет сам себя восстановить и настроить, взяв данные о "покойнике" от соседа.
     
  16. ИгорьК

    ИгорьК Гуру

    ... и все :) и те же проблемы что и у малины.
    Если говорить о надежности, то надо применять что то с более продвинутой памятью и/или резервировать.
    В частности, Domoticz осуществляет полное свое резервирование ежедневно и ежечасно.
    Ничто не мешает создать bash скрипт и писать его backup куда-то в надежное место, например домашнее сетевое хранилище.
    Вся конфигурация OpenHab у меня тоже бэкапится через WinSCP. То есть правка конфигурационных файлов на локальном компьютере с дублированием на Малине и бэкапом на NAS.
     
    Последнее редактирование: 15 ноя 2017
  17. SergeiL

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

    Продолжу в начатом Вами стиле :)

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

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

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

    Он, собака, залез даже в закрытые паролем шары на NAS.

    Вот, после этого, и бэкапся на надежный NAS… И комп будет зашифорван и бэкап на NAS…
     
    Последнее редактирование: 15 ноя 2017
  18. ИгорьК

    ИгорьК Гуру

    Маленький сервер 2.
    Мы можем не только отображать данные, но и принимать их из формы
    Слегка изменим файлы страницы и сервера:
    HTML:
    <!DOCTYPE HTML>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Set Data</title>
    </head>
    <body>
      <p align="center">Температура в доме: $temp</p>
      <p align="center">Установить: $temp</p>
      <form action="/handler.php">
       <p align="center"><input name="target"></p>
       <p align="center"><input type="submit"></p>
      </form>
    </body>
    </html>
    Сервер:
    Код (C++):
    do
    dat = {
        temp = 27.4,
        humi = "Не знаю, но там сухо!",
        target = 10
    }
    local function expand (s)
        return (string.gsub(s, "$(%w+)", dat))
    end
    srv = net.createServer(net.TCP)
    function receiver(sck, data)
        local tg, val
        tg, val = string.match(data,"?(%a+)=(%d+%.*%d*)")
        print(tg, val)
        val = tonumber(val)
        if type(val) == 'number' and tg == 'target' and val > 5 and val < 27 then
            dat.target = val
            print('Set Target Temperature to '..val)
        end
        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('<p align="center">')
                        sck:send(line)
                        sck:send('</p>')
                    end
                until line == nil
                file.close()
            else
              sck:close()
            end
        end
        sck:on("sent", closec)
        send()
    end
    srv:listen(80, function(conn)
      conn:on("receive", receiver)
    end)
    end
    Профит:
    upload_2017-11-16_12-19-19.png

    Путем нехитрых изменений странички и парсинга GET запроса от браузера мы в таблице dat получили поле target в которое записываются данные, если они в интервале от 5 до 27.
    upload_2017-11-16_12-24-11.png

    Если вы разобрались с прошлым топиком, вы сможете легко изменить код (page.lua) и для того, чтобы страничка отражала текущее целевое значение target.

    upload_2017-11-16_12-37-9.png

    Работает так:

     
    Последнее редактирование: 16 ноя 2017
    tpolimer, alp69 и SergeiL нравится это.
  19. ИгорьК

    ИгорьК Гуру

    О расписаниях.
    Предположим, у нас есть задача делать что-то по недельному расписанию. Например, ту же температуру устанавливать.

    Так и сделаем расписание. Вот такое, например.
    Код (Text):
    6, 15:20, 11.35
    2, 18:30, 8
    3, 12:50, 19
    4, 15:30, 20.5
    7, 9:30, 22.5
    7, 22:30, 7.5
    Совершенно ясно, что каждая строка расписания представляет собой день недели, время и температуру, которую надо установить.
    В первой строке - суббота, когда в 15:20 мы установим температуру 11.35 градусов.
    Почему не понедельник в первой строке, как казалось бы удобнее написать? А в учебных целях, чтобы за одно научиться сортировке.

    А так, вполне понятный и читаемый установочный файл.
    Загрузим его под названием sced.lua в модуль. Также как раньше грузили page.lua.

    Теперь наша задача превратить его в другой файл - удобный для работы, а не для составления расписания.
    В итоге он должен выглядеть так
    Код (Javascript):
    3990:8
    5090:19
    6690:20.5
    9560:11.35
    10650:22.5
    11430:7.5
    Здесь ка;дая строка - время в минутах от начала недели и через двоеточие желаемая температура.
    Наша итоговая программа будет загружать каждую строку, проверять текущее время (от начала недели) с временем расписания и задавать нужную температуру.

    А вот и код transformscedtbl.lua
    Код (Lua):
    do
    -- временная таблица
    local temptable={}
    -- удаляем старый файл машинного расписания, если он есть
    file.remove("timeschd.lua")
    -- локальные переменные для обработки "человеческого расписания"
    local d,h,m,t
    -- открываем для чтения файл расписания
    fr = file.open("sced.lua", "r")
    -- открылось?
    if fr then
        -- Пока есть строки
        repeat
            -- Читаем строку
            local line = (fr:readline())
            if line then
                -- Захватываем день, час, минуту и температуру из строки
                d,h,m,t = string.match(line, "%s*(%d+)%s*,%s*(%d+):(%d+)%s*,%s*(%d+%.*%d*)")
                -- Превращаем текстовые значенияв цифровые
                d = tonumber(d)
                h = tonumber(h)
                m = tonumber(m)
                t = tonumber(t)
                -- высчитываем количество минут с начала недели
                local timechck = d*24*60 + h*60 + m
                -- во временную таблицу вставляем строки вида "{минуты:температура}"
                table.insert(temptable, {timechck, t})
            end
        until line == nil
    end
    -- Строки кончились - закрываем файл "человеческого" расписания
    fr.close()

    -- Сортируем временную таблицу так, чтобы минуты в строках шли по возрастанию
    table.sort(temptable, function(a,b)
        return b[1]>a[1]
    end)
    -- Открываем файл "машинного" расписания
    fw = file.open("timeschd.lua", "a+")
    if fw then
        -- Пишем строки
        for _,v in pairs(temptable) do
            -- формат строки: "минуты:температура"
            fw:writeline(""..v[1]..":"..v[2])
            print(""..v[1]..':'..v[2])
        end
    end
    -- все, закрываем лавочку!
    fw:close()
    end
    ... и вы только скажите, что на lua писать сложнее, чем на ардуиноязыке.

    Вот файл отработал, получили "машинное" расписание, упорядоченное по времени. Заметили где стоит строка с температурой 11.35?

    upload_2017-11-16_16-53-27.png
     
    Последнее редактирование: 16 ноя 2017
  20. ИгорьК

    ИгорьК Гуру

    Установка температуры по расписанию.

    Код ниже хватает файл timeschd.lua из предыдущего поста и устанавливает температуру по расписанию (вы уже умеете ее на страничку выводить, а также устанавливать через web-морду?):

    Код (Lua):
    do
    -- День, час и минута "сейчас", позже мы их научимся добывать.
    -- Сейчас просто для тренировки.

    nd = 1
    nh = 0
    nm = 0
    -- Если мы не имеем таблицу "dat" - самое время ее создать
    if not dat then
        dat = {}
        -- И воткнуть туда поле "target" с целевой температурой 5 градусов
        dat.target = 5
    end

    local function chck()
    -- Если создан файл машинного распиания
    if file.open("timeschd.lua", "r") then
        -- время, температура и цель для установки
        local d,t,toset
        -- флаг нахождения записи
        local gotrecord = false
        -- функция устанавливает новую температуру, если отличается от старой
        local function  settg(t)
            if dat.target ~= t then
                dat.target = t
                print('Set temp '..dat.target)
            else
                print('Temp is eq! Not changed '..dat.target)
            end
        end

        -- Превращаем текущее время в минуты
        local timenow = nd*24*60+nh*60+nm
        -- Перебираем строки "машинного расписания"
        function analize()
            repeat
                -- читаем строку
                local line = (file.readline())
                -- есть строка!
                if line then
                -- Парсим на время и температуру
                d,t = string.match(line, "(%d+):(%d+%.*%d*)")
                -- В цифры, сукины дети!
                d = tonumber(d)
                t = tonumber(t)
                -- сравниваем с текущим временем
                if timenow >= d  then
                    -- нашли подходящую запись
                    gotrecord = true
                    toset = t
                end
                end
            -- Наконец, строки кончились!
            until line == nil
          -- Все! Закрыть файл
          file.close()
            -- Не нашли подходящую запись
            -- например, сейчас понедельник, но именно в понедельник
            -- ничего не меняется
            if not gotrecord then
                -- Установим последнюю запись из таблицы
                -- не зря же мы ее сортировали!!!
                toset = t
            end
        end
        analize()

      -- Обработать температуру
      settg(toset)
    end
    end
    -- Первый запуск анализа таблицы
    chck()
    -- И потом каждые 30 секунд проверяем время
    tmr.create():alarm(30000, 1, chck)
    end
    А вот как в ручном режиме можно менять день недели и наблюдать смену температуры(30-секундные промежутки запуска скрипта здесь укорочены):


    ... ну или вместо всего описанного выше воспользоваться модулем cron.

    Дальше: Народный Мониторинг.
     
    Последнее редактирование: 19 янв 2018
    petr0vsk и alp69 нравится это.