Главная chevron_right Наши уроки chevron_right Arduino chevron_right Пиксельные часы с таймером

Пиксельные часы с таймером

Собираем пиксельные часы с таймером на Piranha UNO и NeoPixel-кольце из 60 светодиодов. Адаптивная яркость, RTC-модуль, энкодер и звуковой таймер до 1 часа — всё в одном проекте

Видео-обложка запускается при наведении

Общие сведения

Часы "NeoPixel" - проект, разработанный на основе NeoPixel-кольца, Piranha UNO, а также модулей I2C: энкодер, датчик света, RTC-модуль, и I2C Hub. Вы можете приобрести комплект для сборки, или собрать часы из имеющихся у вас модулей. Также вы можете купить только корпус для часов.

Часы показывают время с помощью разноцветных светодиодов, также есть возможность настроить таймер на время до 1 часа. После заданного времени раздастся звуковой сигнал, а таймер начнёт отсчитывать время, пройденное с момента срабатывания.

Яркость часов адаптивна окружающей освещенности, а минимальная и максимальная яркость настраивается с помощью энкодера, и остаётся в энергонезависимой памяти устройства.

Модуль реального времени позволяет сохранять установленное время даже при отключении основного питания часов.

Видео

Редактируется...

В готовый набор входят:

Библиотеки:

При необходимости, ознакомьтесь с нашей инструкцией по установке библиотек в Arduino IDE.

Установка адресов энкодера и датчика освещенности

Необходимо установить адрес датчику освещенности 0х0А, а энкодеру - 0х0В. Это можно сделать двумя способами:

1. Используя установщик адресов I2C

Об установке адресов при помощи установщика адресов можно почитать на странице установщика адресов

2. Используя Piranha UNO и Trema Shield

Подключим датчик освещенности, загрузим скетч, откроем монитор порта, и установим адрес в десятичной системе счисления (для данного проекта адрес датчика освещенности в десятичной системе счисления - 10).

Далее, то же самое проделаем с энкодером (адрес - 11), предварительно поменяв название объекта в директиве define (7 строка).

Для удобства подключения используем Trema Shield.

Скетч установки адресов

#include <Wire.h>                     // Подключаем библиотеку для работы с аппаратной шиной I2C, до подключения библиотек iarduino_I2C_Encoder и iarduino_I2C_DSL.
#include <iarduino_I2C_Encoder.h>     //  Подключаем библиотеку для работы с энкодером
#include <iarduino_I2C_DSL.h>         //  Подключаем библиотеку для работы с датчиков освещенности

iarduino_I2C_DSL lightSensor;         //  Создаём объекты для работы с методами библиотеки
iarduino_I2C_Encoder enc;             //  Создаём объекты для работы с методами библиотеки

#define module lightSensor     // lightSensor  или enc

// Функция очистки буфера последовательного порта
void DiscardSerial()
{
    while(Serial.available())
        Serial.read();
}

void setup()
{
    // Инициируем последовательный порт.
    Serial.begin(9600);
    while(!Serial){;}
    delay(500);

    // Проверяем наличие модуля
    if (!module.begin()) {
        Serial.println("Модуль не найден, проверьте подключение.");
        return;
    }

    // Устанавливаем адрес
    while(true) {
        Serial.print("Текущий адрес в десятеричной системе: ");
        Serial.println(module.getAddress());
        Serial.println(
                "Введите новый адрес и нажмите "
                "<enter> или \"Отправить\""
                );

        DiscardSerial();

        while(!Serial.available());

        String newAddress = Serial.readStringUntil('\n');
        newAddress.trim();
        uint8_t newAddr = newAddress.toInt();

        if (module.changeAddress(newAddr)) {
            Serial.println("Адрес изменён.");
        }
        else {
            Serial.println(
                    "Адрес не изменён, "
                    "проверьте подключение "
                    "и нажмите \"RES\""
                    );
            break;
        }
    }
}

void loop()
{
    delay(1);
}

Подключение

Мы предполагаем, что NeoPixel-кольцо у вас уже собрано (если вы используете готовый набор, или уже спаяли самостоятельно). Если нет, сперва соберите его по этой инструкции, а затем вернитесь к данному проекту.

Установите Trema Shield на Piranha Uno

Соедините по схеме все компоненты при помощи проводов «мама-мама». В I2C Hub датчики можно подключать к любым номерам колодок.

К 3-й колодке контроллера подключено NeoPixel-кольцо, к 5 выводу - зуммер.

TREMA SHIELDПодключение
3NeoPixel-кольцо
5Зуммер
I2CI2C Hub
VCC+5V
GND-5V (GND)

Подключаем USB-кабель, часы получают питание от него. Дополнительный источник питания не нужен.

Скетч проекта

#include <Wire.h>                     // Подключаем библиотеку для работы с аппаратной шиной I2C, до подключения библиотек iarduino_I2C_Encoder, iarduino_RTC и iarduino_I2C_DSL.
#include <iarduino_NeoPixel.h>        //  Подключаем библиотеку для работы с NeoPixel-кольцом
#include <iarduino_I2C_DSL.h>         //  Подключаем библиотеку для работы с датчиков освещенности
#include <iarduino_I2C_Encoder.h>     //  Подключаем библиотеку для работы с энкодером
#include <iarduino_RTC.h>             //  Подключаем библиотеку для работы с модулем реального времени
#include <EEPROM.h>                   //  Подключаем библиотеку для работы с энергонезависимой памятью

iarduino_I2C_DSL lightSensor;         //  Создаём объекты для работы с методами библиотеки
iarduino_I2C_Encoder enc;             //  Создаём объекты для работы с методами библиотеки
iarduino_RTC watch(RTC_DS3231);       //  Создаём объекты для работы с методами библиотеки
iarduino_NeoPixel led(3, 60);         //  Светодиодное кольцо подключено к 3 пину
#define zummerPin 5                   //  зуммер подключен к 7 пину

#define INIT_ADDR 1023                // номер резервной ячейки (для EEPROM)
#define INIT_KEY 50                   // ключ первого запуска. 0-254, на выбор (для EEPROM)

/////////////////////////НАСТРОЙКИ//////////////////////////////////////////////////
int maxLuminance = 45;           //  максимальная яркость (0...255)
int minLuminance = 4;            //  минимальная яркость (0...255)
#define nightBorderLux 2         //  нижняя граница освещенности, при которой включится "ночной режим", Люкс
#define dayBorderLux 400         //  верхняя граница освещенности, соответствующая полной яркости свечения светодиодов, Люкс
#define K_BACK 0.2               //  коэффициент, определяющий яркость свечения заднего фона относительно цифр (0...1)
#define blinkPeriod 300          //  период мигания светодиода (время горения, мс)
uint8_t backMode = 3;            //  установка цвета фона. 0-3
#define arr_size 60              //  число светодиодов в кольце
#define timeAfterClick 60000     //  время ожидания после последнего нажатия энкодера, мс.
#define blinkTime 300            //  время полупериода мигания параметра в режиме настройки
#define currentLimit 45          //  ограничение тока потребления: предел яркости, больше которой выставить её невозможно (0...255) При максимальной яркости (255) кольцо будет потрбелять около 2А
const float average = 5;         // Определяем константу усреднения показаний датчика освещенности (чем выше значение, тем выше инерционность выводимых показаний).
////////////////////////////////////////////////////////////////////////////////////

uint8_t menuMode = 0;               //  режим: 0-отображение времени. 1-оторажение таймера.
                                    //  2-настройка времени. 3-настройка таймера
uint8_t luminance=1;                  //  яркость, рассчитанная  на основании уровня освещенности
uint8_t realLuminance;              //  яркость, рассчитываемая в функции яркости
int lightValue;                     //  уровень освещенности, полученный с датчика, Люкс
int8_t h,m,s;                       //  переменные хранения часов, минут, секунд
int8_t mt=1, st=0;                  //  переменные хранения минут и секунд таймера
long lastKlickTime;                 //  время последнего щелчка
uint8_t blinkMode = 1;              //  режим для мигания параметрами (чамы, минуты секунды)
uint32_t lastChengeBlinkTime;       //  время последней смены состояния параметра (для мигания в режиме настройки)
bool blinkState = true;             //  состояние параметра для мигания
uint32_t steps;                     //  число "шагов" таймера - по сути количество секунд
int32_t stepTime;                   //  время каждого шага таймера, мс
bool endTimerFlag = false;          //  флаг окончания таймера
uint8_t currentStep = 0;            //  текущий шаг при заполнении кольца во время таймера
int8_t minutesAfterEnd = 0;         //  минуты, после завершения отсчёта таймера
int8_t secondsAfterEnd = 0;         //  секунды, после завершения отсчёта таймера
uint32_t timeEndTimer;              //  время, когда завершился отсчёт таймера
bool startZummerFlag = false;       //  флаг, показывающий что зуммер может включаться
uint8_t zummerCount = 0;            //  число звуков, которые уже издал зуммер
uint32_t timeZummerStart;           //  время запуска зуммера
uint32_t startTimerTime;            //  время запуска таймера
bool noMemuFlag = true;             //  флаг, что мы находимся не в меню настроек
bool zummerLuminFlag = true;        //  флаг работы зуммера в режиме настройки уровней освещения
uint32_t timeInterrupt = 0;         //  время для обращения к функции определения освещенности

void setup() {
  Serial.begin(9600);
  lightSensor.begin();                       // Инициируем модули
  enc.begin();                               //  -//-
  watch.begin();                             //  -//-
  led.begin();                               //  -//-

  lightSensor.changeAddress(0x0A);
  enc.changeAddress(0x0B);

  if (EEPROM.read(INIT_ADDR) != INIT_KEY) {     // первый запуск
    EEPROM.write(INIT_ADDR, INIT_KEY);          // записываем ключ
    EEPROM.put(0, minLuminance);                // записываем стандартное значение яркости
    EEPROM.put(2, maxLuminance);                // -//-
  }
  EEPROM.get(0, minLuminance);                  //  читаем значение яркости
  EEPROM.get(2, maxLuminance);                  // -//-
}

void loop() {
  lumin();                         //  обращается к функции, получаем текущие значения освещённости
  luminance *= average-1;                      // Умножаем предыдущее значение яркости на коэффициент усреднения-1
  luminance += realLuminance;                  // Добавляем к полученному значению новые показания яркости
  luminance /= average;                        // Делим полученное значение на коэффициент усреднения

  timeUpdate();                    //  обращаемся к функции, обновляем значения времени

  switch (menuMode){               //  переход в режим в соответствии с menuMode
    case 1:
      timerUpdate();               //  отображение таймера
    break;
    case 2:
      clockSetArrUpdate();         //  настройка времени
    break;
    case 3:
      timerSetArrUpdate();         //  настройка таймера
    break;
    case 4:
      setLuminance();              //  настройка яркости
    break;
    default:                       //  по умолчанию (или menuMode = 0):
      clockArrUpdate();            //  обновление времени
    break;
  }

uint32_t encTime = enc.getButton(KEY_TIME_PRESSED);      //  получаем время удержания энкодера и записываем в переменную

if (noMemuFlag && takeEncoder()>0){                      //  если были не в меню и кнопка энкодера нажата

    mt=1;
    menuMode = 3;                                        //  переходим в режим настройки таймера
    noMemuFlag = false;                                  //  меняем флаг "в меню"
    blinkMode = 1;                                       //  устанавливаем режим мигания - час

  }
  if (encTime > 1000 && encTime < 3000){                 //  если время от 1 до 3 секунд
    lastKlickTime = millis();                            //  запоминаем время предыдущего клика
    noMemuFlag = false;                                  //  меняем флаг "в меню"
    menuMode = 2;                                        //  переходим в режим настройки времени
    blinkMode = 1;                                       //  устанавливаем режим мигания - час
  }
  if (encTime > 3000){                                   //  если время больше 3с.
    menuMode = 4;                                        //  переходим в режим настройки таймера
    noMemuFlag = false;                                  //  меняем флаг "в меню"
    blinkMode = 1;                                       //  устанавливаем режим мигания - 1
    takeEncoder();                                       //  сбрасываем значения энкодера
  }
}

void setLuminance(){                                                        //  функция установки яркости
  if (blinkMode == 1){                                                      //  если режим 1
    if (zummerLuminFlag){                                                   //  если впервые зашли в режим настройки яркости
      zummerLuminFlag = false;                                              //  меняем флаг, чтобы больше не заходить
      tone(zummerPin, 500, 500);                                            //  выводим звуковой сигнал с частотой 500 Гц и длительностью 0,5 сек
    }
    minLuminance = correctLuminance(minLuminance+(2*(int)takeEncoder()));   //  устанавливаем минимальную яркость
    if (minLuminance > maxLuminance) minLuminance = maxLuminance;           //  если минимальная яркость больше максимальной, не даём ей больше расти
    luminance = minLuminance;                                               //  при этом на циферблат выводим вычисляемую сейчас яркость, чтобы сразу её видеть
  }
  if (blinkMode == 2){                                                      //  если режим 2
    if (zummerLuminFlag){                                                   //  если впервые зашли в режим настройки яркости
      zummerLuminFlag = false;                                              //  меняем флаг, чтобы больше не заходить
      tone(zummerPin, 3000, 500);                                           //  выводим звуковой сигнал с частотой 3000 Гц и длительностью 0,5 сек
    }
    maxLuminance = correctLuminance(maxLuminance+(2*(int)takeEncoder()));   //  устанавливаем максимальную яркость
    if (maxLuminance > currentLimit) maxLuminance = currentLimit;
    if (maxLuminance < minLuminance) maxLuminance = minLuminance;           //  если максимальная яркость меньше минимальной, не даём ей уменьшаться
    luminance = maxLuminance;                                               //  при этом на циферблат выводим вычисляемую сейчас яркость, чтобы сразу её видеть
  }
  if (enc.getButton(KEY_PUSHED)){             //  если было нажатие энкодера
    lastKlickTime = millis();                 //  запоминаем время предыдущего клика
      blinkMode += 1;                         //  увеличиваем режим
      if (blinkMode > 2){                     //  если режим больше 2
        blinkMode = 1;                        //  сбрасываем
        menuMode = 0;                         //  переходим в меню
        noMemuFlag = true;                    //  меняем влаг "в меню"
      }
      zummerLuminFlag = true;                 //  сбрасываем флаг первого вхождения в настройку яркости
      EEPROM.put(0, minLuminance);            //  записываем в энергонезависимую память вычисленные значения яркости
      EEPROM.put(2, maxLuminance);            // -//-
    }

  if (lastKlickTime+timeAfterClick < millis()){         //  если предыдущий клик (или оборот энкодера) был не очень поздно (меньше заданного времени timeAfterClick)
    menuMode = 0;                                       //  переходим в режим хода часов
    noMemuFlag = true;                                  //  меняем влаг "в меню"
    blinkMode = 1;                                      //  сбрасываем
  }
    clockArrUpdate();                                   //  обновляем часы
}

int correctLuminance(int param){                        //  функция корректировки яркости
  if (param <= 0) return 0;                             //  если переданный параметр меньше 0 возвращаем 0
  if (param >= 255) return 255;                         //  если переданный параметр больше 255 возвращаем 255
  return param;                                         //  иначе возвращаем исходный параметр
}

void timeUpdate(){                                       //  функция обновления времени
  uint8_t lastH = h;                                     //  запоминаем старый час (необходим для включению режима радуги)
  watch.gettime();                                       //  чтение времени
  h = watch.hours;                                       //  получаем текущие часы        1-12
  m = watch.minutes;                                     //  получаем текущие минуты      0-59
  s = watch.seconds;                                     //  получаем текущие секунды     0-59
}

void clockArrUpdate(){                                   //  обновление времени
  long clockArr[arr_size] = {};                          //  задание массива для хранения цвета каждого светодиода
  for (uint8_t i = 0; i < arr_size; i++){                //  для всех светодиодов кольца
    clockArr[i] = background(backMode);                  //  запись фона в i-й элемент массива времени
  }
  clockArr[correctValue((5*h)-1)] = clockArr[correctValue(5*h)] =  clockArr[correctValue(5*h)+1] = getColor(luminance, 0, 0);  //  запись в массив времени в светодиод часа и +- один от него красного цвета
  clockArr[correctValue(m)] = clockArr[correctValue(m-1)] = getColor(0, luminance, 0);   //  запись в массив времени в светодиод минут и - один от него зеленого цвета
  clockArr[correctValue(s)] = getColor(0, 0, luminance);   //  запись в массив времени в светодиод секунд синего цвета
  arrPrint(clockArr);
}

void clockSetArrUpdate(){                                //  настройка времени
  long clockSetArr[arr_size] = {};                       //  задание массива для хранения цвета каждого светодиода
  for (uint8_t i = 0; i < arr_size; i++){                //  для всех светодиодов кольца
    clockSetArr[i] = background(backMode);               //  запись фона в i-й элемент массива установки времени
  }
  clockSetArr[correctValue((5*h)-1)] = clockSetArr[correctValue(5*h)] =  clockSetArr[correctValue(5*h)+1] = getColor(luminance, 0, 0);  //  запись в массив установки времени в светодиод часа и +- один от него красного цвета
  clockSetArr[correctValue(m)] = clockSetArr[correctValue(m-1)] = getColor(0, luminance, 0);   //  запись в массив установки времени в светодиод минут и - один от него зеленого цвета
  clockSetArr[correctValue(s)] = getColor(0, 0, luminance);   //  запись в массив установки времени в светодиод секунд синего цвета

  if(enc.getButton(KEY_PUSHED)){                      //  если кнопка энкодера была нажата
    blinkMode += 1;                                   //  режим мигания параметром увеличиваем на 1
    lastKlickTime = millis();                         //  запоминаем время предыдущего клика
    if (blinkMode > 3) {                              //  если режим мигания больше 3
      menuMode = 0;                                   //  переходим в режим хода часов
      blinkMode = 1;                                  //  сбрасываем
      noMemuFlag = true;                              //  меняем влаг "в меню"
      blinkState = true;                              //  меняем состояние параметра для мигания
    }
  }
    if (blinkMode == 1) h += takeEncoder();           //  если режим мигания равен 1 (мигаем часом), прибавляем число шагов энкодера
    if(h > 12) h=1;                                   //  при переполнении значения часа, корректируем
    if (h < 1) h=12;
    if (blinkMode == 2) m += takeEncoder();           //  если режим мигания равен 2 (мигаем минутами), прибавляем число шагов энкодера
    if(m > 59) m=0;                                   //  при переполнении значения минут, корректируем
    if (m < 0) m=59;
    if (blinkMode == 3) s=0;                          //  если режим мигания равен 3 (мигаем секундами), прибавляем число шагов энкодера
    watch.settime(s,m,h);                             //  записываем время в модуль RTC

  if (lastKlickTime+timeAfterClick > millis()){       //  если предыдущий клик (или оборот энкодера) был не очень поздно (меньше заданного времени timeAfterClick)
    if (lastChengeBlinkTime+blinkTime < millis()){    //  если пришло время поменять состояние мигающего параметра
      lastChengeBlinkTime = millis();                 //  обновляем время смены состояния мигающего параметра
      blinkState = !blinkState;                       //  инвертируем состояние
    }
    if (blinkMode == 1){                                                                                                                                    //  если мигаем часом
      if (blinkState) clockSetArr[correctValue((5*h)-1)] = clockSetArr[correctValue(5*h)] =  clockSetArr[correctValue(5*h)+1] = getColor(luminance, 0, 0);  //  если состояние blinkState = true, зажигаем светодиоды часа
      else clockSetArr[correctValue((5*h)-1)] = clockSetArr[correctValue(5*h)] =  clockSetArr[correctValue(5*h)+1] = background(backMode);                  //  иначе, гасим (зажигаем фоном)
    }
    if (blinkMode == 2){                                                                                           //  если мигаем минутами
      if (blinkState) clockSetArr[correctValue(m)] = clockSetArr[correctValue(m-1)] = getColor(0, luminance, 0);   //  если состояние blinkState = true, зажигаем светодиоды минут
      else clockSetArr[correctValue(m)] = clockSetArr[correctValue(m-1)] = background(backMode);                   //  иначе, гасим (зажигаем фоном)
    }
    if (blinkMode == 3){                                                             //  если мигаем секундами
      if (blinkState) clockSetArr[correctValue(s)] = getColor(0, 0, luminance);      //  если состояние blinkState = true, зажигаем светодиод секунд
      else clockSetArr[correctValue(s)] = background(backMode);                      //  иначе, гасим (зажигаем фоном)
    }
  }
  else {
    menuMode = 0;          //  иначе, преходим в режим хода часов
    noMemuFlag = true;     //  флаг "не в меню"
    blinkMode = 1;         //  сбрасываем режим мигания
  }
  arrPrint(clockSetArr);   //  выводм (обновляем) часы
}

void timerSetArrUpdate(){                          //  установка таймера
  long timerSetArr[arr_size] = {};                 //  задание массива для хранения цвета каждого светодиода
  for (uint8_t i = 0; i < arr_size; i++){          //  для всех светодиодов кольца
    timerSetArr[i] = background(backMode+1);       //  запись фона в i-й элемент массива установки таймера
  }
  for (uint8_t i=1; i <= mt; i++){                 //  от 1 светодиода до текущего значения минут
    timerSetArr[i] = getColor(0, luminance, 0);    //  заливаем светодиод зеленым цветом
  }

  if (blinkMode == 1) {                            //  если устанавливаем минуты
    int8_t encSteps = takeEncoder();
    mt += encSteps;                                //  к минутам прибавляем число шагов энкодера
    for (uint8_t i=0; i < round(fabs((float)encSteps)); i++){
      tone(zummerPin, 1500, 10);                   //  выводим звуковой сигнал с частотой 2048 Гц и длительностью 10 мс
      delay (30);
    }
  }
    if (mt > 60) mt = 60;                          //  корректировка значений
    if (mt < 0) mt = 0;                            //  -//-

  if(lastKlickTime + 3000 < millis()){             //  если прошло больше 3-х секунд бездействия
    lastKlickTime = millis();                      //  запоминаем время предыдущего клика
      menuMode = 1;                                //  переходим в режим отображения таймера
      noMemuFlag = false;                          //  сбрасываем флаг "не в меню"
      tone(zummerPin, 1500, 300);                  //  выводим звуковой сигнал с частотой 2048 Гц и длительностью 0,3 сек

  }
  stepTime = round((((int32_t)mt*60)+(int32_t)st)*1000/arr_size);   //  вычисляем время одного шага для заливки кольца белым к концу отсчёта таймера
  endTimerFlag = false;                                 //  сбрасываем флаг окончания таймера
  currentStep = 0;                                      //  сбрасываем текущее положение таймера
  startTimerTime = millis();                            //  записываем время начала отсчёта таймера

  if (lastKlickTime+timeAfterClick > millis()){         //  если предыдущий клик (или оборот энкодера) был не очень поздно (меньше заданного времени timeAfterClick)
    for (uint8_t i=1; i <= mt; i++){                    //  от 1 светодиода до текущего значения минут
      timerSetArr[correctValue(i)] = getColor(0, luminance, 0); //  зажигаем светодиоды зеленым
    }
  }
  else {
    menuMode = 0;         //  иначе переходим в режим хода часов
    noMemuFlag = true;    //  сбрасываем флаг "не меню"
    blinkMode = 1;        //  сбрасываем режим мигания
  }
  if (mt == 0){
    menuMode = 0;         //  иначе переходим в режим хода часов
    noMemuFlag = true;    //  сбрасываем флаг "не меню"
    blinkMode = 1;        //  сбрасываем режим мигания
  }
  arrPrint(timerSetArr);  //  выводим на светодиоды массив цветов установки таймера
  enc.getButton(KEY_PUSHED);                                 //  сбрасываем значения хранеия нажатия кнопки
}

void timerUpdate(){                                          // отображение таймера
  long timerArr[arr_size] = {};                              //  задание массива для хранения цвета каждого светодиода
  uint8_t colorRed = round(map(currentStep, 0, 60, 0, luminance*K_BACK));        //перевод числа шагов steps в диапазон цвета 0...200
  uint8_t colorGreen = round(map(currentStep, 0, 60, (luminance*K_BACK)/2, 0));  //перевод числа шагов steps в диапазон цвета 0...100

  if (endTimerFlag) {                                        //  если таймер дошел до конца
    if (lastChengeBlinkTime+blinkTime < millis()){           //  если пришло время поменять состояние мигающего параметра
      lastChengeBlinkTime = millis();                        //  обновляем время смены состояния мигающего параметра
      blinkState = !blinkState;                              //  инвертируем состояние
    }
    if (blinkState){                                         //  если состояние blinkState = true
      for (uint8_t i = 0; i < arr_size; i++){                //  гасим все светодиоды кольца
        timerArr[i] = getColor(0, 0, 0);
      }
    }
    else {
     for (uint8_t i = 0; i < arr_size; i++){                 //  иначе зажигаем красным все светодиоды кольца
        timerArr[i] = getColor(luminance*K_BACK, 0, 0);
      }
    }
    if (millis() - timeEndTimer >= 60000) {                  //  если прошла минута с момента окончания отсчёта таймера
      minutesAfterEnd+=1;                                    //  увеличиваем значение прошедших минут на 1
      timeEndTimer = millis();                               //  сбрасываем время окончания таймера
      startZummerFlag = true;                                //  активируем флаг зуммера
    }
    if (minutesAfterEnd > 60) {                              //  если минут больше 60
      menuMode = 0;                                          //  переходим в режим хода часов
      noMemuFlag = true;                                     //  флаг "не меню"
    }
    for (uint8_t i=1; i <= minutesAfterEnd; i++){            //  зажигаем зеленым минуты после истечения таймера
      timerArr[correctValue(i)] = getColor(0, luminance, 0);
    }
  }
  else{                                                                         //  иначе
   if (stepTime*(long)currentStep < millis()-startTimerTime){                   //  если пришло время зажечь новый светодиод при заполнении таймера
      currentStep += 1;                                                         //  увеличиваем текущий шаг на 1
      if (currentStep > arr_size){                                              //  если заполнено всё кольцо
        currentStep = 0;                                                        //  обнуляем текущий шаг
        endTimerFlag = true;                                                    //  устанавливаем флаг окончания таймера
        timeEndTimer = millis();                                                //  устанавливаем время окончания отсчёта таймера
        startZummerFlag = true;                                                 //  устанавливаем флаг зуммера
        timeZummerStart = millis();                                             //  устанавливаем время начала работы зуммера
        minutesAfterEnd = 0;                                                    //  обнуляем значения минут, прошедших с начала работы таймера
        secondsAfterEnd = 0;                                                    //  обнуляем значения секунд, прошедших с начала работы таймера
      }
    }
    for (uint8_t i = 0; i < arr_size; i++){                                     //  каждый светодиод кольца
      timerArr[i] = getColor(colorRed, colorGreen, 0);                          //  задаём цвет
    }
    for (uint8_t i = 1; i < currentStep; i++){                                  //  от 0 светодиода до текущего
      timerArr[correctValue(i)] = getColor(luminance, luminance, luminance);    //  задаём белый цвет
    }
  }

   if(enc.getButton(KEY_PUSHED) || (enc.getEncoder() != 0)){   //  Если нажат или повернут вал энкодера
    currentStep = 0;                                           //  обнуляем значение текущего шага
    endTimerFlag = false;                                      //  сбрасываем флаг окончания таймера
    menuMode = 0;                                              //  устанавливаем режим меню (отображение часов)
    noMemuFlag = true;                                         //  флаг "не меню"
    startZummerFlag = false;                                   //  сбрасываем флаг работы зуммера
    lastKlickTime = millis();                                  //  запоминаем время предыдущего клика
   }

   if (startZummerFlag){                                       //  если флаг зуммера true
    if (timeZummerStart + 200 < millis()){                     //  если пришло время включения очередного тона
      tone(zummerPin, 2048, 100);                              //  выводим звуковой сигнал с частотой 2048 Гц и длительностью 0,1 сек
      zummerCount++;                                           //  увеличиваем количество воспроизведенных тонов
      timeZummerStart = millis();                              //  обновляем время старта зуммера
      if (zummerCount > 10){                                   //  если воспроизведено больше 20 тонов
        zummerCount = 0;                                       //  обнуляем их количество
        startZummerFlag = false;                               //  сбрасываем флаг активности зуммера
      }
    }
   }
   arrPrint(timerArr);                                         //  выводим массив цветов на кольцо
}

int8_t takeEncoder(){                                          //  тело функции takeEncoder()
  int8_t value = enc.getEncoder();                             //  получаем значение с энкодера (число шагов)
  if (value != 0) lastKlickTime = millis();                    //  если оно не равно 0, обновляем время чтения этого значения (необходимо, чтобы не выходить из  настойки времени, если мы крутим энкодер)
  return value;                                                //  возвращаем это значение
}

int8_t correctValue(int8_t param){                             //  тело функции correctValue() - корректировка параметров
    if (param > 59) return 0;                                  //  если переданный параметр (часы/минуты) больше 59, возвращаем 0
    else if (param < 0) return 59;                             //  если меньше 0, возвращаем 59
    else return param;                                         //  иначе, возвращаем без изменений
}

void arrPrint(long *arr){                                      //  функция вывода значений массива на кольцо
  for (uint8_t i = 0; i < arr_size; i++){                      //  для каждого элемента массива
    led.setColor(i, arr[i]);                                   //  записываем в i-й светодиод цвет, указанный в i-м месте массива
  }
  led.write();                                                 //  зажигаем
}

long background(uint8_t backMode){                                                           //  функция заливки фона
  if (backMode > 4) backMode = 0;                                                            //  проверяем режим фона на корректность
  switch (backMode) {
    case 0: return getColor(luminance*K_BACK, luminance*K_BACK, 0);                          //  в зависимости от режима, вызываем функцию получения цвета с заданными параметрами
    break;
    case 1: return getColor(0, luminance*K_BACK, luminance*K_BACK-0.5*luminance*K_BACK);     //  -//-
    break;
    case 2: return getColor(luminance*K_BACK, 0, luminance*K_BACK);                          //  -//-
    break;
    case 3: return getColor(luminance*K_BACK, luminance*K_BACK, luminance*K_BACK);           //  -//-
    break;
    case 4: return getColor(0, 0, luminance*K_BACK);                                         //  -//-
    break;
  }
}

long getColor(uint8_t r, uint8_t g, uint8_t b){           //  функция преобразования цвета из десятичной системы в шестнадцатеричную
  String Ost, ResR, ResG, ResB;
  String String16 = "0123456789ABCDEF";
  Ost = String16[r%16];
  r = floor(r/16);
  ResR = String16[r];
  ResR += Ost;

  Ost = String16[g%16];
  g = floor(g/16);
  ResG = String16[g];
  ResG += Ost;

  Ost = String16[b%16];
  b = floor(b/16);
  ResB = String16[b];
  ResB += Ost;
  String StringResult = "0x"+ResR+ResG+ResB;
  long resLong = 0;
  sscanf(StringResult.substring(2,4).c_str(), "%x", (int)&resLong+2);
  sscanf(StringResult.substring(4,8).c_str(), "%x", (int)&resLong);
  return resLong;
}

void lumin(){
  lightValue = lightSensor.getLux();                                                           //  получение освещенности с датчика
  if (lightValue <= nightBorderLux) {                                                          //  если освещенность превышает границу ночного режима освещенности
    realLuminance = minLuminance;                                                              //  яркость светодиодов равна минимальной
  }
  else if (lightValue > nightBorderLux && lightValue < dayBorderLux){                          //  иначе, если освещенность находится в диапазоне между нижней и верхней границей освещенности
    realLuminance = map(lightValue, nightBorderLux, dayBorderLux, minLuminance, maxLuminance); //  вычисляем яркость светодиодов в зависимости от уровня освещения
  }
  else if (lightValue >= dayBorderLux) {                                                       //  иначе, если освещенность больше максимальной границы
    realLuminance = maxLuminance;                                                              //  яркость светодиодов равна максимальной
  }
}

Инструкция по работе

Настройка времени

При удержании кнопки энкодера от 1-й до 3-х секунд, произойдет переход в режим настройки времени. Начнут мигать светодиоды, показывающие час. Вращением ручки энкодера можно настроить время в часах. Клик по кнопке энкодера переведёт в режим установки минут, здесь время настраивается аналогично. Еще один клик переведет в режим установки секунд, при этом светодиод, отвечающий за время в секундах, установится на 0 и будет мигать, ожидая следующего нажатия, с помощью которого новое установленное время запускается и секундная "стрелка" начинает движение с 0. Таким образом можно настроить время с пуском устройства точно в нужный момент.

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

Настройка таймера

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

При срабатывании таймера, раздастся звуковой сигнал, а на кольцо начнёт выводиться время, прошедшее с момента окончания таймера (в минутах). Остановить таймер можно в любой момент нажатием или поворотом ручки энкодера.

Через 60 минут после окончания таймера, произойдет автоматический переход в режим часов.

Настройка яркости

Яркость часов изменяется автоматически в зависимости от освещённости окружающей среды. Однако, минимальная и максимальная яркость, между которыми будет происходить автоматическое регулирование, может быть настроена вручную.

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

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

Прочее

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

Сборка корпуса

  1. Подготовьте все элементы;

  1. Установите указатели цифр в углубления;
  1. Закрепите цифры с помощью винтов;
  1. Установите NeoPixel-кольцо: закрепите кольцо ниткой, проденьте её концы через отверстия, завяжите на специальных держателях. После того, как все держатели будут установлены, вращая их, можно увеличить натяжение нитей;
  1. Установите энкодер;
  1. Закрепите контроллер Piranha UNO на стойках;
  1. Соберите настенное крепление;
  1. Установите датчики на площадки согласно рисунку;

  1. Установите Trema Shield;
  1. Установите площадки с датчиками на основу;
  1. Соедините компоненты согласно схемы (см.часть "Подключение");
  1. Установите опоры для возможности использования часов в качестве настольных;

Ссылки

Комплект для повторения проекта

Что понадобится

Это быстрый ориентир по основным модулям и деталям. Полный список для повторения проекта смотрите в самой статье.

17 позиций
Комплект для проекта «Пиксельные часы с таймером» #12 комплект для сборки Открыть в iArduino Конструктор ПВХ Чёрный «Корпус для пиксельных часов с таймером» Корпус из деталей решетчатого конструктора для пиксельных часов Открыть в iArduino Piranha Uno R3 Прямой аналог Arduino Uno c микроконтроллером ATmega328P на ядре AVR для разработки электронных устройств на языке C++ Открыть в iArduino NeoPixel кольцо, 60 светодиодов NeoPixel кольцо 60 светодиодов Открыть в iArduino Энкодер, потенциометр, FLASH-I2C (Trema-модуль) Открыть в iArduino Датчик освещенности, люксметр, FLASH-I2C (Trema-модуль) Открыть в iArduino Часы реального времени RTC DS3231 (Trema-модуль) Электронный энергонезависимый хронометр на чипе DS3231 Открыть в iArduino Зуммер (Trema-модуль) Позволяет излучать звук определённой тональности (частоты). Открыть в iArduino i2C Hub (Trema-модуль) Будет полезен в проектах с большим количеством устройств с интерфейсом I2C Открыть в iArduino Trema Shield Плата расширения для удобного подключения периферийных устройств Открыть в iArduino Шлейф Dupont «Мама – Мама» (4 pin, 20 см) Шлейф из четырёх проводов длиной 20 см с коннекторами Dupont Female Открыть в iArduino Стойка нейлоновая чёрная «папа-мама» М3 / 6 мм (4 шт.) Комплект из четырёх нейлоновых чёрных стоек длиной 6 мм с наружной и внутренней резьбой М3 Открыть в iArduino Винт нейлоновый чёрный М3×6 (4 шт.) Комплект из четырёх нейлоновых чёрных винтов с резьбой М3 и длиной ножки 6 мм Открыть в iArduino Кабель USB (Type A – Type B) / 30 cм Кабель USB 2.0 (A-B) Открыть в iArduino Алюминиевая ручка потенциометра, черная Колпачок для энкодера Открыть в iArduino Шпаргалка по программированию в Arduino IDE Открыть в iArduino Установщик адресов FLASH-I2C Позволяет быстро менять адреса модулей Flash-i2c, узнавать количество и адреса i2c модулей на шине. Открыть в iArduino
Все уроки Arduino