Так сложилось, что проект NodeMCU Lua появился раньше, чем модули ESP8266 были "захвачены" ардуино сообществом. Думаю, эта ситуация длилась даже более года.

Мне сразу понравился этот модуль и я начал пытаться изучать его в начале 2015 года, имея опыт создания аж одного проекта на Ардуино. То есть, ни к электронике ни к программированию я отношения не имею. Просто люблю делать полезные самоделки для дома. Модуль ESP8266 отлично для этого подходил, но приличной документации в то время по нему не было, поэтому опыт приобретался методом проб и ошибок, изучения доступного чужого кода.

Через некоторое время документация появилась и стала пополняться по адресу https://nodemcu.readthedocs.io/en/dev/

Надо заметить, что язык Lua хорош тем, что он может быть изучен всего по одной книге. Отсутствие выбора иногда совсем неплохо. Да и книга невелика - вполне доступна для самоделкина, тем более и читать всю ее необходимости нет: вполне достаточно одолеть первые 15 глав, что в объеме не превышает 200 страниц.

... однако. После прочтения у ардуинщика, меня, в частности, не возникает понимания как писать программы для ESP8266. Привычка мыслить категорией loop(а) не дает возможности "увидеть" будущую структуру программы, которая, на самом деле, куда проще.

Все дело в том, что "The NodeMCU runtime implements a non-blocking threaded model that is similar to that of node.js, and hence most Lua execution is initiated from event-triggered callback (CB) routines". То есть, программирование на Lua для ESP8266 асинхронное и событийное. В общем, голова должна работать не так, как привык адруинщик.

Попробую в этой заметке рассказать о первых шагах в написании кода на Lua для ESP.

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

Итак, коль скоро в программе нет loop, а некоторые события должны происходить в вашем модуле периодически, у NodeMCU Lua есть таймеры - явление, которое в "нормальном" Lua по умолчанию отсутствует (зато есть в JavaScript, что в написании кода сильно роднит с ним NodeMCU).

Про таймер все написано здесь, но новичку его логика не всегда очевидна.

  1. Совсем азы.

Давайте мигать светодиодом. Даже без него самого обойдемся:

print('Write Pin '..0)
print('Write Pin '..1) 

вполне достаточно для начала.

Напишем функцию, вызов которой будет "управлять" светодиодом:

do
	data = 0
	function myled()
   		-- типа тренарного оператора: 
        data = data == 0 and 1 or 0
        print('Write Pin '..data)
	end
end

Проверяем, как это работает с модулем:

Итак, наш "вместо_loop" таймер создается:

mytimer = tmr.create() -- создаем объект Timer

Что должен делать таймер? Периодически вызывать функцию myled()

Организуем его деятельность целиком:

-- То что пишем в пин:
data = 0
-- Как часто мигаем, мс:
blinktime = 1000
-- Создали таймер:
mytimer = tmr.create()
-- Регистрируем три аргумента таймера, (1) как часто вызывается таймер,
-- (2) режим таймера, (3) функция, которая будет вызываться. 
mytimer:register(blinktime, tmr.ALARM_AUTO, function(t)
    -- знакомая функция
    data = data == 0 and 1 or 0
    print('Write Pin '..data)
end)
-- Начали:
mytimer:start()

Повторю код для копипасты:

data = 0
blinktime = 1000
mytimer = tmr.create()
mytimer:register(blinktime, tmr.ALARM_AUTO, function(t)
    data = data == 0 and 1 or 0
    print('Write Pin '..data)
end)
mytimer:start()

Сохраним его в файл "_smallled.lua" и запустим на выполнение. Код будет работать пока не получит команду на перезагрузку:

Итог азов таймеростроения. Создается объект Timer, который, вполне логично, определяет три аргумента: как часто он срабатывает, режим (мы выбрали самый простой и популярный), функция, которая будет вызываться каждое срабатывание.

2. Что такое асинхронный режим?

Срабатывание таймера (вызов им функции) можно представить как событие. Мы можем определить другое событие, и оно будет "жить своей жизнью", независимо от первого.

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

-- file "_smallled2.lua"
---------- Первый таймер -----------
data = 0
blinktime = 1000
mytimer = tmr.create()
mytimer:register(blinktime, tmr.ALARM_AUTO, function(t)
    data = data == 0 and 1 or 0
    print('Write Pin '..data)
end)
mytimer:start()

-- Независимый от первого второй таймер ---- 
data2 = 0
blinktime2 = 2000
mytimer2 = tmr.create()
mytimer2:register(blinktime2, tmr.ALARM_AUTO, function(t)
    data2 = data2 == 0 and 1 or 0
    print('Write Pin2 '..data2)
end)
mytimer2:start()

Что же мы получаем:

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

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

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

3. Таймер внимательнее.

Таймер очень гибкий объект. Его надо изучить в первую очередь и знать достаточно глубоко. Этим займемся.

Таймер создается вызовом:

mytimer = tmr.create()

Определяем чем и как он будет заниматься:

mytimer:register(5000, tmr.ALARM_SINGLE, function(t) print("Я таймер!") end)

Первый аргумент - через какое время вызывается функция, миллисекунды.

Второй аргумент описывается так:

tmr.ALARM_SINGLE -- одиночный вызов, этот таймер не требует его уничтожения, 
-- он самоуничтожается после срабатывания, НО ЗДЕСЬ ЕСТЬ ОДНА ХИТРОСТЬ!!!  
tmr.ALARM_SEMI -- каждый раз требует вызова, чтобы запуститься вновь 
tmr.ALARM_AUTO -- раз запустили - и буде работать.

Третий аргумент - функция для вызова. Кстати, вы заметили, что она никак не называется (что вполне логично)? Таки это и есть страшная (для понимания ардуинщика) анонимная функция, которая в других языках может называться лямбда-функция.

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

Вызываемую функцию можно написать отдельно, а в таймер передать лишь ссылку на нее. Вот так:

do
  function askedbytimer()
    print("Я таймер!")
  end

  mytimer:register(5000, tmr.ALARM_SINGLE, askedbytimer)
end

Все очень просто. Ну, не скажите! Теперь анонимная функция превратилась в не менее страшную callback-функцию "askedbytimer()". То есть, это функция, которая передается другой функции для вызова. Мы "вытащили" из таймера функцию, и передали ему лишь ее имя - оно же ссылка.

Дальше таймер можно запустить:

tmr.start(mytimer)
-- или
mytimer:start()

Можно остановить:

tmr.stop(mytimer)
-- или
mytimer:stop()

Можно поменять интервал:

mytimer:interval(3000)

После остановки таймер можно уничтожить (смотри следующий раздел!!! Хитрость там):

-- не лучший вариант, но иногда (а не постоянно) можно:
tmr.stop(mytimer) -- останавливаем
tmr.unregister(mytimer) -- удаляем регистрацию того, чем он занимался
mytimer = nil -- удаляем ссылку на таймер

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

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

Анонимный таймер запускается в момент создания, ибо внешней ссылки на него не создается и "подтолкнуть" снаружи его не возможно:

do
	function askedbytimer()
		print("Я таймер!")
	end

	tmr.create():alarm(5000, tmr.ALARM_AUTO, askedbytimer)
end

Эта конструкция бесконечно вызывает callback функцию.

4. Внутренняя ссылка на таймер.

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

О чем речь. Вернемся к регистрации таймера:

mytimer = tmr.create()
mytimer:register(5000, tmr.ALARM_AUTO, function(t)
    print('Я таймер!')
end)

Функция здесь имеет обязательный аргумент "t". Это тоже ссылка на тот таймер, на который ссылается и "mytimer".

Напомню, что в Lua все объекты самостоятельны и не привязаны к ссылкам на них. Если на объект отсутствуют (удалены) все ссылки, то он уничтожается сборщиком мусора. Если остается хоть одна ссылка - объект продолжает находиться в памяти.

Последнее важно иметь в виду, если вы собираетесь уничтожать таймер. И это происходит чаще, чем кажется на первый взгляд. Тот же опрос датчика, произведенный через некоторое время после получения им команды на чтение данных, требует таймер. Но оный не нужен, когда опрос осуществлен. Модуль, содержащий таймер, будет выгружен из памяти, а вот таймер перед этим, следует удалить правильно. Вернемся к практике.

Сначала пример использования внутренней ссылки:

flag = true
mytimer = tmr.create()
mytimer:alarm(5000, tmr.ALARM_AUTO, function(t)
    if not flag then t:stop(); return end
    print('Я таймер!')
end)

Если флаг "flag" изменил свое состояние на "false" - таймер останавливается через внутреннюю ссылку. По внешней ссылке мы можем запустить его вновь:

mytimer:start()

Вот вам управление миганием информационного светодиода, например.

А теперь как таймер самоуничтожается:

flag = true
mytimer = tmr.create()
mytimer:alarm(5000, tmr.ALARM_AUTO, function(t)
    if not flag then 
    	-- останавливаем
    	t:stop() 
    	-- прекращаем регистрацию
    	t:unregister()
    	-- удаляем внутреннюю и внешние ссылки
    	t, mytimer = nil, nil 
      return
    end
    print('Я таймер!')
end)

Удалять таймер нужно именно так. Остановка и удаление таймера через внешнюю ссылку не уничтожит таймер как объект. Прекращение регистрации уменьшит занимаемый им объем памяти, но объект останется все равно.

Или если применяется callback:

flag = true
mytimer = tmr.create()
function askedbytimer(t)
    if not flag then 
      t:stop() 
      t:unregister()
      t, mytimer = nil, nil
      return
    end
	print("Я таймер!")
end
mytimer:alarm(5000, tmr.ALARM_AUTO, askedbytimer)

Если режим таймера "tmr.ALARM_SINGLE " то уничтожение будет таким:

mytimer = tmr.create()
function askedbytimer(t)
    t, mytimer = nil, nil
    print("Killed!")
end
mytimer:alarm(1000, tmr.ALARM_SINGLE , askedbytimer)

-- или уничтожаем анононимный таймер
tmr.create():alarm(2000, tmr.ALARM_SINGLE , function(t)
    t = nil -- Обязательно!
    print('Killed!')
end)

Заметим, что в некоторых случаях я показывал аргумент "t" (его можно называть как угодно, товарищ новичок) в функциях таймера, в некоторых - нет. Lua не требует выделять память под аргументы, которые могут передаваться функции, но при этом не нужны ей. Как теперь ясно, если вы планируете внутреннее управление таймером или его уничтожение - аргумент объявлять обязательно. Если таймер будет тикать и тикать всю программу - такой необходимости нет.

Итого.

Эта заметка написана чайником для чайников. В ней рассмотрены азы написания программы на Lua, показаны кое-какие особенности таймера, затронуты вопросы неведомых ардуинщику, но совсем нестрашных анонимных и callback функций. (Меня эти слова, поначалу, вводили в замешательство.)

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

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

Если эта тема кого-то заинтересует, можно ее и продолжить, например, разбором публикации данных какого-нибудь датчика. Хотя, как мне видится, сейчас мир DIY захватывают не желающие программировать, а потребители  ESPEasy, Tasmota, ESPHome, еtс.