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