<irony>
Не прошло и полугода… Но зато конструкция прошла проверку временем!
</irony>
В продолжение первой части о проектировании максимально универсального семисегментного дисплея сделаем на получившихся модулях первое, что приходит в голову — конечно же часы! Так что это очередная статья про очередные часы. Без кнопок, на ESP8266, на NodeMCU и Lua. Кому до сих пор интересно — прошу под кат.
Кусочек hardware
Для создания часов требуется четырехразрядный индикатор (или шести, если отображать еще и секунды). Так как часы планируются полностью автономными настенными я решил делать их из двух модулей по два трехдюймовых индикатора. В наличии такие были красные с общим анодом, так что устанавливаем элементы master-платы согласно первой части статьи, для slave-платы устанавливает только боковой разъём и индикаторы. Соединяем вместе и вперед программировать!
Стартуем с NodeMCU
Писать на arduino-вых скетчах мне не позволяет религия, извините, а bare-metal прошивка под ESP8266 для данной задачи это явно перебор. Так что выбор вполне логично пал на NodeMCU и скриптовый язык lua. Вкратце, что такое NodeMCU — это открытый бесплатный проект на основе lua, имеющий отличную гибкость и достаточную мощность, что позволяет быстро и эффективно создавать разнообразные проекты. NodeMCU — модульная прошивка, а это значит, что можно собрать вариант конкретно под свой проект без лишних модулей. Благодаря обширной комьюнити NodeMCU уже умеет работать с разными протоколами обмена данных поверх WiFi (HTTP, MQTT, JSON, CoAP), периферией, с несколькими десятками популярных датчиков, с дисплеями, и даже умеет в файловую систему FatFS.
Для того, чтобы собрать прошивку под свой проект переходим на сайт www.nodemcu-build.com, вводим свою электронную почту, отмечаем галочками нужные модули и жмем Start your build.
Для часов нам потребуется минималистичный набор модулей:
wifi — окно во внешний мир
enduser_setup — удобный интерфейс для подключения к сети WiFi
file — проект будет состоять из разных файлов, нужно уметь с ними работать
gpio — дергать ножками
net — модуль сетевого клиента
rtctime — часы реального времени
sntp — синхронизация часов по сети, кнопок то нет
spi — интерфейс для MAX7219
tmr — таймеры
Отметили, нажали на большую синюю кнопку и ждем пару минут, пока на почту упадет ссылка на готовый бинарный образ для заливки в контроллер. Система просто отличная.
Для заливки образа, как и для сохранения lua-скриптов используется UART. Для подключения внешнего адаптера USB-to-UART (3.3V!) используется разъём J3 — UART. Как упоминалось в первой части, на плате присутствует посадочное место под преобразователь CH340. В случае его использования все общение с контроллером (и питание платы) будет производится через порт USB на плате. Удобно если проект требует частых изменений или длительного процесса разработки программы. Для переключения в режим записи во флеш нужно предварительно установить на плате перемычку J4. Скорость UART — 115200 бод, номер правильного СОМ порта оставляю на вас.
Для прошивки образа рекомендую утилиту NodeMCU-PyFlasher. Возможно, она покажется не такой простой как популярная NodeMCU-Flasher, но является более универсальной и помогает в ситуациях, когда NodeMCU-Flasher просто молча глохнет при попытках прошивки.
Процесс успешной заливки образа должен выглядеть следующим образом:
Теперь перемычку J4 можно снять, перезапустить плату и начать писать скрипты в программе ESPlorer. Я не преследую цели написать курс по программированию на lua, эта тема хорошо освещена на многих ресурсах. Лично от себя могу дать рекомендацию на блог avislab — там понятным языком написана целая серия статей, в которых освещаются вопросы от азов до общения с облачными хранилищами.
Ниже приведу минимальный набор скриптов для реализации вполне себе функциональных (показывающих время!) часов, требующих только стартовой настройки — подключению к сети WiFi. Часики прошли уже проверку временем, все работает отлично, не сбоит, за более чем полугода работы зависли один раз, как я понял, через проблемы с интернетом, полечились простым перезапуском.
local spi_index = 1;
local cs_pin = 3;
-- MAX7219 SPI Master Initialization
function max7219_spi_init()
print('SPI init');
spi.setup(spi_index, spi.MASTER, spi.CPOL_LOW, spi.CPHA_LOW, 16, 80, spi.HALFDUPLEX);
gpio.mode(cs_pin, gpio.OUTPUT, gpio.PULLUP);
gpio.write(cs_pin, gpio.HIGH);
end
-- MAX7219 Output
function max7219_output(digit, value)
local data = digit*256 + value;
gpio.write(cs_pin, gpio.LOW);
spi.send(spi_index, data);
gpio.write(cs_pin, gpio.HIGH);
end
-- MAX7219 set intensity
function max7219_intensity(value)
local data = 0x0A00 + value;
gpio.write(cs_pin, gpio.LOW);
spi.send(spi_index, data);
gpio.write(cs_pin, gpio.HIGH);
end
-- MAX7219 Initialization
function max7219_init(digits, intensity)
print(string.format("MAX7219 init for %d digits", digits));
gpio.write(cs_pin, gpio.LOW);
-- Display test mode off
spi.send(spi_index, 0x0F00);
gpio.write(cs_pin, gpio.HIGH);
gpio.write(cs_pin, gpio.LOW);
-- Normal Operation mode
spi.send(spi_index, 0x0C01);
gpio.write(cs_pin, gpio.HIGH);
gpio.write(cs_pin, gpio.LOW);
-- Intensity duty cycle
-- [min 0x0A00 .. 0x0A0F max]
spi.send(spi_index, 0x0A00 + intensity);
gpio.write(cs_pin, gpio.HIGH);
gpio.write(cs_pin, gpio.LOW);
-- Decode-Mode
-- [0 - no decode, 1 - B-Code mode]
spi.send(spi_index, 0x09FF);
gpio.write(cs_pin, gpio.HIGH);
gpio.write(cs_pin, gpio.LOW);
-- Scan-Limit Register Format
spi.send(spi_index, 0x0B04);
gpio.write(cs_pin, gpio.HIGH);
-- Set blank as default
for d=0, digits do
max7219_output(d, 0x0F);
end
end
collectgarbage();
local point = 0;
local time_zone = 3;
local sntp_cnt = 1;
local cur_intensity = 0x0F;
function timer_do()
tm = rtctime.epoch2cal(rtctime.get());
if point == 0 then point = 1; else point = 0; end;
max7219_intensity(cur_intensity);
max7219_output(5, tm["min"]%10);
max7219_output(4, tm["min"]/10);
max7219_output(2, tm["hour"]%10 + (128*point));
max7219_output(1, tm["hour"]/10);
if tm["hour"] <= 7 then
-- from 0 to 8
cur_intensity = 0x01;
else
if tm["hour"] <= 18 then
-- from 8 to 19
cur_intensity = 0x0F;
else
if tm["hour"] <= 22 then
-- from 19 to 22
cur_intensity = 0x05;
else
-- from 23 to 24
cur_intensity = 0x01;
end
end
end
end
function sntp_sync()
print ("SNTP sync");
sntp.sync("194.54.161.214",
function(sec, usec, server, info)
rtctime.set(sec + 3600*time_zone)
tm = rtctime.epoch2cal(rtctime.get());
print(string.format("%04d/%02d/%02d %02d:%02d:%02d", tm["year"], tm["mon"], tm["day"], tm["hour"], tm["min"], tm["sec"]));
sntp_cnt = 4320;
end,
function(err, str)
print("Nope...")
end
)
end
function timer_sntp()
if sntp_cnt > 0 then
sntp_cnt = sntp_cnt - 1;
else
if wifi.sta.status() == wifi.STA_GOTIP then
print("Connected to WiFi as:" .. wifi.sta.getip());
sntp_cnt = 6;
sntp_sync();
else
print("No WiFi");
end;
end
end
require("max7219");
max7219_spi_init();
max7219_init(5, cur_intensity);
rtctime.set(1577872800 + 3600*time_zone);
tm = rtctime.epoch2cal(rtctime.get());
print(string.format("%02d:%02d:%02d", tm["hour"], tm["min"], tm["sec"]));
enduser_setup.start(
function()
print("Connected to WiFi as:" .. wifi.sta.getip())
sntp_sync();
end,
function(err, str)
print("enduser_setup: Err #" .. err .. ": " .. str)
end
);
local mytimer = tmr.create();
mytimer:register(500, tmr.ALARM_AUTO, timer_do);
mytimer:start()
local sntp_timer = tmr.create();
sntp_timer:register(10000, tmr.ALARM_AUTO, timer_sntp);
sntp_timer:start()
collectgarbage();
local p = {}
p.wifi_ssid="ssid"
p.wifi_password="password"
-- your own parameters:
p.utc_zone="xxx"
return p
Во флеш контроллера также нужно залить страницу enduser_setup.html с интерфейсом подключения к сети WiFi.
Несмотря на такой компактный скрипт часы действительно получаются функционально законченными. Реализован следующий сценарий: при включении, на основе enduser_setup модуля создаётся открытая WiFi-точка с названием SetupGaget_xxx.
При подключении к которой и попытке перейти по какому-либо адресу (или просто по 192.168.4.1) открывается интерфейс подключения к доступным сетям.
Такая себе landing-page, куда нужно ввести название сети и пароль. Можно ввести вручную или выбрать из списка доступных. При нажатии на кнопку контроллер пытается подключится к выбранной сети и в случае успеха выводит радостное сообщение и отключает WiFi-точку. Дополнительно я добавил на страницу настройку часового пояса.
После подключения к Интернету часы синхронизируются с сервером точного времени по протоколу SNTP и начинают тихо выполнять свою основную функцию — отображать время на дисплее, помигивая точкой второго разряда.
Буквально в несколько строчек можно добавить периодическую синхронизацию времени и изменение яркости в зависимости от времени суток. Если вы счастливый обладатель модулей с 512кБ памяти придется писать проверками, как в коде выше, если же есть возможность использовать master branch версию — рекомендую использовать модуль простого планировщика событий cron.
Аналогично и с функцией изменения яркости дисплея, которая выше также реализована на банальных проверках.
cron.schedule("0 */12 * * *", function(e)
print("Every 12 hours");
sntp_sync();
end)
Сразу прошу прощения за фото, съемка ярких светодиодных индикаторов оказалась той еще задачей, даже при хорошем фронтальном освещении картинка выглядит не очень. В жизни часы выглядят яркими, равномерными и вокруг солнечный день.
Что еще?..
Теперь пара слов о других идеях. С помощью универсального семисегментного дисплея и простого lua-скрипта под NodeMCU можно буквально за час сделать настольные/настенные счетчики событий (клиенты, коммиты, факапы) или отсчитыватели времени до чего-то, будь до дедлайн или отпуск. Или считать дни без падений сервера.
Возможно несколько вариантов решения. Самый простой — использовать все тот же модуль enduser_setup добавив на стартовую страницу необходимые параметры, например, инкрементировать или декрементировать число и с каким периодом.
Второй, более гибкий вариант — подвязать дисплей к какой-либо странице в Интернете, откуда он будет брать актуальные данные. Этот вариант подходит для отображения курсов валют, температуры воздуха на улице или количества выздоровевших от коронавируса и любых других часто обновляемых данных.
Возможен так же вариант прямого управления дисплеем с телефона используя любую из множества программ для прямой коммуникации с esp8266 по WiFi. Такое решение будет подходящим для отображения счета в настольных играх или на спортивных событиях, например, школьного масштаба.
И конечно же, никто не запрещает подключить всевозможные датчики к esp8266 и отображать температуру, влажность или давление. Хоть уровень углекислого раза в помещение.
Как простенький пример, и как раз по случаю грядущего праздника, я запилил счетчик дней до Нового Года.
local time_zone = 3;
local sntp_cnt = 1;
local cur_intensity = 0x0F;
local days = 189;
function print_days()
max7219_intensity(cur_intensity);
max7219_output(3, days%10);
max7219_output(2, (days%100)/10);
max7219_output(1, days/100);
if tm["hour"] <= 7 then
-- from 0 to 8
cur_intensity = 0x01;
else
if tm["hour"] <= 18 then
-- from 8 to 19
cur_intensity = 0x0F;
else
if tm["hour"] <= 22 then
-- from 19 to 22
cur_intensity = 0x05;
else
-- from 23 to 24
cur_intensity = 0x01;
end
end
end
end
local dpm = {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
function days_till_ny()
tm = rtctime.epoch2cal(rtctime.get());
days = dpm[tm["mon"]-1] - tm["day"];
if (tm["year"]%4) and (tm["mon"]<=2) then days = days - 1; end;
local month = tm["mon"];
while(month < 12)
do
days = days + dpm[month];
month = month + 1;
end;
print_days();
end
function sntp_sync()
if wifi.sta.status() == wifi.STA_GOTIP then
print("Connected to WiFi as:" .. wifi.sta.getip());
print ("SNTP sync");
sntp.sync("194.54.161.214",
function(sec, usec, server, info)
rtctime.set(sec + 3600*time_zone)
tm = rtctime.epoch2cal(rtctime.get());
print(string.format("%04d/%02d/%02d %02d:%02d:%02d", tm["year"], tm["mon"], tm["day"], tm["hour"], tm["min"], tm["sec"]));
days_till_ny();
end,
function(err, str)
print("Nope...")
end
);
else
print("No WiFi");
end;
end;
require("max7219");
max7219_spi_init();
max7219_init(3, cur_intensity);
rtctime.set(1577872800 + 3600*time_zone);
tm = rtctime.epoch2cal(rtctime.get());
print(string.format("%02d:%02d:%02d", tm["hour"], tm["min"], tm["sec"]));
enduser_setup.start(
function()
sntp_sync()
end,
function(err, str)
print("enduser_setup: Err #" .. err .. ": " .. str)
end
)
cron.schedule("0 */12 * * *", function(e)
print("Every 12 hours");
sntp_sync();
end)
collectgarbage();
Идей, как и вариантов их воплощения, великое множество. Наличие на борту контроллера с подключением к сети Интернет, модульная конструкция и возможность установки разного количества индикаторов разного размера и цветов открывает целое поле для полета фантазии. Вот такой вот универсальный индикатор получился.
Буду рад почитать конструктивную критику или интересные предложения.
Всем спасибо за внимание!
И всех с наступающими праздниками!
DX168B
Свое знакомство с ESP8266 я тоже начал с NodeMCU, так как с Lua знаком давно. Ибо их SDK уж точно не подходит для «быстрого старта». Но на Lua более-менее сложный проект не напишешь. ОЗУ маловато. Один из проектов упорно не хотел влезать в память даже несмотря на ухищрения, вроде запуска уже скомпилированных скриптов и разбития кода на множество файлов. Тогда я начал искать альтернативы прямого программирования посредством SDK и решил использовать PlatformIO в связке с Arduino framework. Решение получилось хорошее, так как становятся доступными различные библиотеки, среди которых есть хороший асинхронный веб-сервер, позволяющий создавать веб страницы по-взрослому. То есть, размещать весь веб-контент в виде файлов в SPIFFS и пользоваться аналогом Апачевского mod-rewrite. Плюс кодинг на C++ последней редакции стандарта. Этот проект был переписан на C++, наворочено функционала даже больше, чем было, а края ОЗУ даже и близко не видно.
ARMag Автор
Да, PlatformIO достаточно популярный вектор развития при работе с ESP8266, хотя тут дело вкуса. Например, ютьюбер-популяризатор DIY электроники AlexGyver, который достаточно плотно с работает с модулем, не очень лестно отзывается о данной платформе, вроде даже статья у него была такая.
DX168B
Там бывают косяки, но это всё же лучше, чем иные варианты. Arduino IDE слишком простая для программиста, работающего в нормальных средах. Лично мне неудобно работать в Arduino IDE. Там нет даже банального «дерева файлов проекта», позволяющего переключаться между файлами исходников гораздо оперативнее, чем шариться по вкладкам. А вот Visual Studio Code уже намного более удобная. Я уже не говорю о таких уже привычных вещах, как IntelliSense. Впрочем, грех жаловаться на то, что было придумано для совсем начинающих.
Сам-то я о платформе AVR и PIC давно уже забыл и в основном у меня используются STM32, которые программирую в IAR. STM32 я достаточно хорошо знаю, на них есть практически вся документация. Но ESP8266 с их SDK… Помоему, даже SDK для ESP32 гораздо проще в использовании.