
<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.
Shit happens
Мне несказанно «повезло» и все мои модули ESP-07 оказались с флешем 512кБ на борту. Хотя по документации, описанию на сайте продавца и фото в интернете должно быть 1Мб. В связи с чем я целый вечер искал причину, почему модуль или не шьется вовсе или шлёт мусор в СОМ-порт при включении неистово мигая синим светодиодом. Оказалось master branch NodeMCU требует от 1 Мб флеша. Для таких же счастливчиков, как я нужно поставить галочку на сайте рядом с branch-ем версии 1.5.4.1 — это финальная версия, которая работает с 512кБ.
Для часов нам потребуется минималистичный набор модулей:
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 просто молча глохнет при попытках прошивки.
Конкретнее
В некоторых непонятных ситуациях при смене обычной прошивки на NodeMCU модули на ESP8266 перестают правильно инициализироваться. Это лечится или предварительной зашивкой файла esp_init_data_default.bin по адресу 0x7C000 или установкой гало��ки Erase Chip в NodeMCU-PyFlasher.
Процесс успешной заливки образа должен выглядеть следующим образом:

Теперь перемычку J4 можно снять, перезапустить плату и начать писать скрипты в программе ESPlorer. Я не преследую цели написать курс по программированию на lua, эта тема хорошо освещена на многих ресурсах. Лично от себя могу дать рекомендацию на блог avislab — там понятным языком написана целая серия статей, в которых освещаются вопросы от азов до общения с облачными хранилищами.
Ниже приведу минимальный набор скриптов для реализации вполне себе функциональных (показывающих время!) часов, требующих только стартовой настройки — подключению к сети WiFi. Часики прошли уже проверку временем, все работает отлично, не сбоит, за более чем полугода работы зависли один раз, как я понял, через проблемы с интернетом, полечились простым перезапуском.
Библиотека по работе с MAX7219 - max7219.lua
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();
main cкрипт - init.lua
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();
Файл для шаринга параметров enduser_setup - enduser_setup.lua
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
cron.schedule("0 */12 * * *", function(e) print("Every 12 hours"); sntp_sync(); end)

Сразу прошу прощения за фото, съемка ярких светодиодных индикаторов оказалась той еще задачей, даже при хорошем фронтальном освещении картинка выглядит не очень. В жизни часы выглядят яркими, равномерными и вокруг солнечный день.
Что еще?..
Теперь пара слов о других идеях. С помощью универсального семисегментного дисплея и простого lua-скрипта под NodeMCU можно буквально за час сделать настольные/настенные счетчики событий (клиенты, коммиты, факапы) или отсчитыватели времени до чего-то, будь до дедлайн или отпуск. Или считать дни без падений сервера.
Возможно несколько вариантов решения. Самый простой — использовать все тот же модуль enduser_setup добавив на стартовую страницу необходимые параметры, например, инкрементировать или декрементировать число и с каким периодом.
Второй, более гибкий вариант — подвязать дисплей к какой-либо странице в Интернете, откуда он будет брать актуальные данные. Этот вариант подходит для отображения курсов валют, температуры воздуха на улице или количества выздоровевших от коронавируса и любых других часто обновляемых данных.
Возможен так же вариант прямого управления дисплеем с телефона используя любую из множества программ для прямой коммуникации с esp8266 по WiFi. Такое решение будет подходящим для отображения счета в настольных играх или на спортивных событиях, например, школьного масштаба.
И конечно же, никто не запрещает подключить всевозможные датчики к esp8266 и отображать температуру, влажность или давление. Хоть уровень углекислого раза в помещение.
Как простенький пример, и как раз по случаю грядущего праздника, я запилил счетчик дней до Нового Года.


NY ждун
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();
Идей, как и вариантов их воплощения, великое множество. Наличие на борту контроллера с подключением к сети Интернет, модульная конструкция и возможность установки разного количества индикаторов разного размера и цветов открывает целое поле для полета фантазии. Вот такой вот универсальный индикатор получился.
Буду рад почитать конструктивную критику или интересные предложения.
Всем спасибо за внимание!
И всех с наступающими праздниками!
