КДПВ


<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();


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

Буду рад почитать конструктивную критику или интересные предложения.

Всем спасибо за внимание!

И всех с наступающими праздниками!