Уголок kvg
Пишу, что придёт в голову, и, если не придет, то всё равно пишу - я как кузнец, я не могу никуя! :crazy
Аватара пользователя
kvg
Администратор
Сообщения: 1305
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:
Календарь
- Январь 2025
+ Декабрь 2024
+ Ноябрь 2023
+ Октябрь 2023
+ Сентябрь 2023
+ Август 2023
+ Июль 2023
+ Июнь 2023
+ Ноябрь 2022
+ Октябрь 2022
+ Август 2022
+ Июнь 2022
+ Март 2022
+ Февраль 2022
+ Январь 2022
+ Декабрь 2021
Поиск по блогам

Кормушка на Ардуино Нано для птиц и кошек

kvg » 10 янв 2025, 17:13

Дисклеймер:
1 - Это моя первая поделка на Ардуино. До конца 24-го года я вообще не знал, про их существование.
2 - Многое можно сделать по другому, это касается и самой конструкции кормушки и программы.
3 - Пост будет периодически дополняться/редактироваться (все сразу не вспомнить), но в комментариях, так как я выбрал лимит на количество знаков в сообщении.

Дано: кормушка для птиц на даче, в которую зимой надо подсыпать корм.

Проблема: Когда на даче никого нет, кормушка пустует — птицы страдают. Оставлять много запасов в кормушке (применять бункерную кормушку) нельзя, так как птицы бесконтрольно все растащат и, по мнению орнитологов, это неправильно, так как они разучатся искать корм самостоятельно. То есть корм надо подсыпать дозировано раз в сутки, а в остальное время птицы добывают корм сами, не теряя навыки.

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

Посмотрев что это такое и сколько к нему идет плат расширения, датчиков и модулей я охренел от этих возможностей и решил, что управлять кормушкой будет этот микроконтроллер.

Первой же моей ошибкой было то, что я не прогуглил уже готовые решения - не правильно ввел запрос, как я позже понял, надо было гуглить кормушку для котов, а не для птиц. Так что я заново изобрел велосипед, а мог бы взять уже готовый проект, но зато немного погрузился в тему.

Итак, что было приобретено для кормушки:
1. Arduino Nano CH340G Type-C (На чипе 328P)
2. Терминальная плата к ней
3. Модуль часов реального времени DS 3231
4. Сервомотор SG90 0 - 180
5. LCD1602 I2C
6. Матричная клавиатура 4x4
7. Пассивный звуковой модуль зуммер KY-006
8. Щиток для автоматов (в качестве корпуса)
9. Мастерок (в качестве дозатора)
10. Разные сантех переходники на 50 и 110 мм
11.Герметичный разъем для подключения дозатора к блоку управления

Корм, а это будут семечки я решил насыпать в девятнадцатилитровую емкость для воды от кулера.
Выбор серво был обусловлен тем, что в сервоприводе организована обратная связь, то есть - подали команду на закрытие и дальше он сам будет пытаться закрыть дозатор до конца, даже если ему будет что-то мешать.

Сначала я сделал дозатор - из мастерка вырезал лепесток, просверлил в нем два отверстия
F1.jpg

И закрепил на нем серво.
F2.jpg

В пластиковом сантех-переходнике прорезал щель и с помощью хомута из подвеса закрепил дозатор. Колено взял изогнутое, подумал, что так легче будет открываться дозатору, да и вода, если попадет на него, будет стекать вниз.
F3.jpg

Важный момент! Полностью перекрывать дозатором трубу нельзя, надо обязательно оставить зазор, чтобы при закрытии дозатор в зазор смог сместить всякий мусор (палочки, сухую траву).
F4.jpg

Чтобы переходник (50 мм) с дозатором закрепить на бутылке надо у бутылки срезать ободок на конце горлышка.
F9.jpg

Снизу (а так сверху) бутыли я вырезал большое отверстие для трубы 110 мм, верх которой используется для засыпки семечек. Трубу в отверстие вклеил каучуковым клеем, он отлично подошел для склейки таких пластиков. Я пробовал и термоклей и другие клеи, но этот держится неплохо, только воняет и сохнет очень долго. Из трубы я достал уплотнительную резинку, чтобы заглушка-крышка (я купил заглушку с вентиляцией) легко вынималась из трубы.
F11.jpg

Испытания показали хорошую работы дозатора, и, даже несмотря на применения самого слабого сервопривода, дозатор при закрытии иногда разрезал семечки пополам. А всякий мусор сдвигался в зазор, откуда потом благополучно вываливался. А в идеале можно было дозатор и заточить. Видео работы дозатора.
(что-то я забыл подключить просмотр видео к блогам, так что пока для просмотра надо скачать видео, просмотр прикручу позже)
Feed1.mp4
(1.41 MБ) 15 скачиваний

Сначала схему собрал на макетной плате:
F10.jpg

После отладки разместил в корпусе от щитка. По краям экрана залил герметик, клавиатура самоклеющаяся герметичная. На синюю кнопку слева не обращайте внимание, в последней версии программы она не используется.
F5.jpg

Внутри на термоклей приклеил все элементы платы, блок питания 5В. Максимальный ток кормушки это включение серво, ток в пике достигает одного ампера, поэтому блок питания взял с запасом - 2А.
F6.jpg

Важное замечание - из модуля часов реального времени DS 3231 надо выпаять smd диод, который находится около батарейки (он там один), через этот диод протекает ток подзарядки, но вместо аккумулятора китайцы поставляют этот модель с литиевое батареей, поэтому батарейку раздувает от протекающего тока и она может взорваться. А если блок DS 3231 с акб или ионистором, то диод, конечно нужен.

И еще такой момент, так как кормушка у меня будет висеть на улице и крыши над ней не будет, то надо закрыть сервопривод от осадков и птиц, чтобы его не расклевали.
Я взял сантехпереход с 50 на 100, вырезал в нем сегмент и приклеил на суперклей над серво. Единственно, полностью закрыть весь дозатор не получилось и поэтому один раз кормушка не сработала.
F8.jpg

Общий вид кормушки:
F7.jpg

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

К написанию программы привлек ChatGPT, без него, как без рук, кстати, чтобы им пользоваться можно в браузере вбить в настройках DNS такой адрес - https://dns.comss.one/dns-query и можно пользоваться этим чат-ботом без vpn.

Кормушка спокойно работает под дождем, снегом, работала в мороз в минус 10 (больше пока не было), я смотрел через видеонаблюдение каждый день, все четко. Но один раз, я как раз был на месте, был очень сильный ветер, лил дождь и потом резко похолодало. Кормушка покрылась ледяной коркой, щель дозатора залило водой и он не смог сдвинуться с места. Но повторное (ручное) включение прошло нормально.
Тут ясно, что надо полностью закрыть дозатор, так что это проблема недостаточной защиты конструкции.
Или можно программно перед основным срабатываем пару раз дергать серво, а затем открывать. В общем, думаю, это не проблема.
Кормушка не боится отключения электричества, так как время и продолжительность срабатывания записывается в eeprom. Часы также не сбиваются, так как питаются от батарейки, потребляют от нее микроамперы.
Видео, как срабатывает кормушка в сборе:
Feed2.mp4
(2.11 MБ) 20 скачиваний

Теперь про программу. Как я писал выше, она срабатывает раз в сутки. После включения кормушки надо нажать кнопку B, осуществляется переход к настройкам времени срабатывания и дозы порции.
Ввод данных осуществляется кнопкой D, на экране будут подсказки. Ошибочно введенное время можно сбросить кнопкой C. Программа не дает ввести, например 34 часа и 87 минут, стоит проверка на корректное время.

Порцию (включение дозатора) регулируется с шагом 0.1 секунды, максимальное значение 9.9 секунд (это все легко изменить в программе). Я провел кучу тестов, этот диапазон времени оптимален для семечек.
Чтобы кормушку выключить, можно ввести время кормления как 0.0, тогда срабатывания не будет.
Чтобы покормить вручную (особенно полезно при настройке) надо нажать кнопку A.

Точное время (и дата) устанавливается при компиляции программы, поэтому, как таковой настройки времени с помощью клавиатуры нет, так как модуль DS 3231 считает время с точностью +- 2 секунды за год, такой точности для кормушки (и не только) хватит за глаза. Но установку времени вручную, конечно, надо сделать, особенно, если потом кто закажет такую кормушку. Так что этот момент в процессе.

Модуль DS 3231 еще интересен тем, что в нем стоит датчик температуры (хотя и в самой Ардуине он тоже стоит, но неточный), так вот, как опять таки советуют орнитологи, в морозы птиц надо кормить чаще, поэтому этот датчик температуры можно задействовать в алгоритме кормушки, например:
Если температура опуститься ниже -10-15 градусов, то сделать или два срабатывания в сутки или увеличивать количество корма в порции, причем можно сделать зависимость объема порции от температуры поградусно. А если температура будет в плюсе, то можно автоматом отключать кормушку на весну-лето.
Также с помощью датчика температуры можно отслеживать резкий переход температуры от плюса к минусу и периодически в этот момент дергать серво, чтобы его не прихватило ледяным дождем.

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

Сама программа. Я постарался максимально прокомментировать код. (Из-за большого объема сообщения программа в самой записи не размещается, поэтому я размещу ее в комментарии, как и схему.)

А при чем тут коты? Спросите вы. Я как-то раз показал кормушку приятелю (не по даче, так что кормушка поедет в другое место) и он попросил ему сделать такую же, но для его пяти котов, которых он прикормил летом, а зимой их кормить каждый день возможности не имеет. Я согласился сделать ее бесплатно (не могу же брать деньги за неизвестно что), но сказал, что кормушку ему не отдам насовсем, а дам лишь попользоваться. Правда может потом кто и закажет такую же у меня за деньги:) Но сначала надо все хорошо отладить.

Задача была посложнее, чем кормушка для птиц, так как надо было сделать питание кормушки от батареек или АКБ, то есть проработать режим энергосбережения, переделать дозатор, чтобы полностью убрать его в трубу, чтобы его не разгрызли коты, изменить алгоритм срабатывания серво, а сам блок разместить в меньшем корпусе.

Как итог, получилась вот такая кормушка для кошек, я скрестил банку с табуреткой:)
Спойлер
F12.jpg
Про кормушку для кошек я напишу позже в комментарии ниже, как только эта кормушка пройдет испытания. Она уже работает, но нужно накопить статистику по глюкам.

А в комментариях под этим постом я выложу программу кормушки для птиц, ее схему и по ходу дела буду вносить изменения в программу, если таковые появятся.
Последний раз редактировалось kvg 04 фев 2025, 01:11, всего редактировалось 9 раз.
9 комментариев 501 просмотр
Комментарии
Аватара пользователя
kvg
Администратор
Сообщения: 1305
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:

kvg » 10 янв 2025, 17:14

Схема подключения:

Feed-zadvijka.jpg

Программа для кормушки с дозатором в виде задвижки без дополнительного кормления по температуре и с однократным срабатыванием в сутки по устиановленному времени.
Программа обновлена 26.01.2025.

Код: Выделить всё

#include <RTClib.h> // Подключение библиотеки часов
#include <Servo.h> // Подключение библиотеки управления серво
#include <Wire.h> // Подключение библиотеки протокола i2c
#include <LiquidCrystal_I2C.h> // Подключение библиотеки экрана
#include <Keypad.h> // Подключение библиотеки клавиатуры
#include <EEPROM.h> // Подключение библиотеки для работы с EEPROM
// Размеры клавиатуры
const byte ROWS = 4; // 4 строки
const byte COLS = 4; // 4 столбца
// Задаём карту расположения клавиш клавиатуры
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};
// Соединение строк клавиатуры с контактами Arduino
byte rowPins[ROWS] = {11, 10, 9, 8};
// Соединение колонок клавиатуры с контактами Arduino
byte colPins[COLS] = {7, 6, 5, 4};
// Создание объекта клавиатуры
Keypad kpd = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
RTC_DS3231 rtc;
Servo servo_feed; // Даем имя для переменной сервомотора
LiquidCrystal_I2C lcd(0x27, 16, 2); // Адрес I2C дисплея
// Установки переменных по умолчанию (цифры по идее не нужны, но без них почему-то глючит)
int feedingHour = 12;    // Время кормления: часы
int feedingMinute = 0;   // Время кормления: минуты
unsigned long duration = 500; // Длительность работы сервопривода (мс)
int wholeSeconds; // Время кормления - секунды (для отображения на экране)
int tenthOfSecond; // Время кормления - десятые доли секунд (для отображения на экране)
//int grad; // Переменная для температуры
bool feed = true; // Переменная для контроля срабатывания серво
char key;
int r[4];
unsigned long feedtimer; //переменная для вычисления времени срабатывания серво
DateTime lastFeedTime; // Переменная для хранения времени последнего кормления
// Адреса для хранения данных в EEPROM
#define FEEDING_HOUR_ADDRESS 0   // Адрес для хранения часов кормления (1 байт)
#define FEEDING_MINUTE_ADDRESS 1 // Адрес для хранения минут кормления (1 байт)
#define DURATION_ADDRESS 2       // Адрес для хранения длительности срабатывания сервомотора (2 байта)
#define BUZZER_PIN 3 // Определение пина для подключения динамика

void setup() {
  //Serial.begin(9600); // Для отладки
  servo_feed.attach(2); // Контакт для подключения сервомотора D2
    if (!rtc.begin()) {
      //Serial.println("Couldn't find RTC");
    while (1); // Остановка программы, если RTC не найден
    }
      // Если часы теряли питание, устанавливаем текущее время
      // с помощью компа при компиляции программы
        //(для первоначальной установки времени надо 		 
      // передернуть батарейку)
    if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // Установка текущего времени при компиляции 	 
   //программы
    }
  lastFeedTime = rtc.now();   // Устанавливаем начальное значение времени
  lcd.init();                 // Инициализация дисплея
  lcd.backlight();            // Включение подсветки
  // Загрузка сохранённых значений из EEPROM (но в самый первый запуск кормушки в eeprom будет ахинея, 
   //не страшно,
   //все равно в первый раз надо выставить время вручную)
  feedingHour = EEPROM.read(FEEDING_HOUR_ADDRESS);
  feedingMinute = EEPROM.read(FEEDING_MINUTE_ADDRESS);
  duration = EEPROM.read(DURATION_ADDRESS) * 256 + EEPROM.read(DURATION_ADDRESS + 1);
  // Установка начального угла поворота сервопривода - чтобы закрыть кормушку
  servo_feed.write(90); // Это значение конкретно для серво 9g 180, 90 это центр
  delay(1000); // Задержка, чтобы серво успел встать в начальное положение
  tone(BUZZER_PIN, 3000, 50); // Короткий звуковой сигнал что система включилась
}

void loop() {
  // Обработка нажатия кнопок A и B
    char key = kpd.getKey(); // Обработчик кнопок
    if (key != NO_KEY) {
        if (key == 'A') {
            triggerServo(); // Ручное кормление кнопкой A. Переход на функцию кормления
        } else if (key == 'B') {
            setFeedingTime(); // Устанавливаем время срабатывания кормушки кнопкой B.
             //Переход на функцию установки времени и продолжительности работы серво
        }
    }
  DateTime now = rtc.now(); // Получение текущего времени
  lcd.setCursor(0, 0);
  lcd.print("Time: ");
  if (now.hour() < 10) { // Устранение смещение отображения для часов (и убираем незначащий ноль часов)
  lcd.setCursor(6, 0);
  lcd.print(' ');
  }
  lcd.print(now.hour());
  lcd.print(':');
  if (now.minute() < 10) lcd.print('0'); // Добавляем ведущий ноль, если нужно
  lcd.print(now.minute());
  lcd.print(':');
  if (now.second() < 10) lcd.print('0'); // Добавляем ведущий ноль, если нужно
  lcd.print(now.second());
  lcd.setCursor(0, 1);
  lcd.print("Feed: ");
  if (feedingHour < 10) lcd.print(' '); // убираем незначащий ноль часов
  lcd.print(feedingHour);
  lcd.print(':');
  if (feedingMinute < 10) lcd.print('0');
  lcd.print(feedingMinute);
  unsigned long durationMs = duration;  // Получаем длительность в миллисекундах
  float durationSec = durationMs / 1000.0;  // Переводим миллисекунды в секунды с плавающей точкой
  // Разделяем целую и дробную части
  wholeSeconds = floor(durationSec);  // Целые секунды
  tenthOfSecond = round((durationSec - wholeSeconds) * 10);  // Десятые доли секунды
  lcd.setCursor(12, 1);
  lcd.print(wholeSeconds);  // Выводим целые секунды
  lcd.print(".");           // Выводим точку
  lcd.print(tenthOfSecond);  // Выводим десятые доли секунды
  lcd.setCursor(15, 1);
  lcd.print("s");
  // Условие срабатывания серво - должны совпасть часы, минуты и feed быть true
  if (feed && now.hour() == feedingHour && now.minute() == feedingMinute) {
    triggerServo();
    feed = false; // Отключаем возможность повторного срабатывания
    lastFeedTime = now; // Запоминаем время срабатывания
  }
   // Проверяем, прошло ли время с момента последнего кормления
   //(без этой проверки кормушка иногда не срабатывала)
  if (!feed && (now.unixtime() - lastFeedTime.unixtime() >= 120)) {
    feed = true; // Разрешаем новое срабатывание, если прошло 2 минуты
    }
}
  // Функция установки времени срабатывания сервопривода
void setFeedingTime() {
  tone(BUZZER_PIN, 1500, 50);
  feed = true;
  int i = 0;
  char j = 0; // Перемещение j наружу, чтобы сохранить состояние между итерациями цикла
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Set feeding Time");
  lcd.setCursor(0, 1);
  lcd.print("HH:MM");
  while (true) {  // Бесконечный цикл
    key = kpd.getKey();
    if (key != NO_KEY) {
      tone(BUZZER_PIN, 1000, 50); // Короткий звуковой сигнал
      delay(51);                  // Ждём окончания звука
      if (key == 'C') {  // Проверка на кнопку C
        i = 0;           // Сброс индекса массива
        j = 0;           // Сброс позиции курсора
        lcd.clear();     // Очистка дисплея
        lcd.setCursor(0, 0);  // Установка курсора обратно
        lcd.print("Set feeding Time");  // Повторный вывод заголовка
        lcd.setCursor(0, 1);            // Переход к строке ввода
        continue;                       // Начало нового прохода цикла
      }
      lcd.setCursor(j, 1);
      lcd.print(key);
      r[i] = key - 48; // Получаем значение цифры из её кода
      i++;
      j++;
      if (j == 2) {
        lcd.print(":");
        j++;  // Пропуск двоеточия
      }
    }
    // Условие проверки завершения ввода времени (4 это часы и минуты)
    if (i >= 4) {
      int hours = r[0] * 10 + r[1];
      int minutes = r[2] * 10 + r[3];
      // Проверка корректности введённого времени
      if (hours < 24 && minutes < 60) {
        // Ожидаем нажатие кнопки D для подтверждения
        lcd.setCursor(0, 0);
        lcd.print("Press D to save ");
        while (true) {
          key = kpd.getKey();
          if (key == 'D') {  // Подтверждение ввода
            tone(BUZZER_PIN, 1000, 50); // Короткий звуковой сигнал
            feedingHour = hours;  // Сохранение часов кормления
            feedingMinute = minutes;  // Сохранение минут кормления
            EEPROM.update(FEEDING_HOUR_ADDRESS, feedingHour); // Сохранение в EEPROM
            EEPROM.update(FEEDING_MINUTE_ADDRESS, feedingMinute); // Сохранение в EEPROM
            getDurationInput(); // Переход на функцию установки времени срабатывания серво
            return;      // Завершение функции после ввода времени
          }
        }
      } else {
        // Время некорректно, вывод сообщения об ошибке
        tone(BUZZER_PIN, 500, 50); // Короткий звуковой сигнал
        // Serial.println("Incorrect time entered. Please try again."); // Вывод в монитор порта
        //для контроля и отладки
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Incorrect time!");
        lcd.setCursor(0, 1);
        lcd.print("Please try again.");
        delay(2000); // Задержка перед повторным запросом времени
        i = 0;       // Сброс индекса массива
        j = 0;       // Сброс позиции курсора
        lcd.clear(); // Очистка дисплея
        lcd.setCursor(0, 0);  // Установка курсора обратно
        lcd.print("Set feeding Time");  // Повторный вывод заголовка
        lcd.setCursor(0, 1);            // Переход к строке ввода
      }
    }
  }
}
  //Установка продолжительности срабатывания серво
void getDurationInput() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Enter feeding:");
  lcd.setCursor(4, 1);
  lcd.print("in X.X sec");
  String inputString = "";  // Пустая строка для хранения введенных чисел
  int cursorPosition = 0;   // Позиция курсора на экране
  while (true) {
    key = kpd.getKey();
    if (key != NO_KEY) {
      tone(BUZZER_PIN, 1000, 50); // Звук при нажатии кнопки
      delay(51);                  // Ожидание завершения звука
      if (key == 'C') {  // Сброс чисел
        inputString = "";
        cursorPosition = 0;
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Enter feeding:");
        lcd.setCursor(4, 1);
        lcd.print("in X.X sec");
        lcd.setCursor(0, 1);
        continue;
      }
        if (isDigit(key) && cursorPosition < 2) {  // Принимаем только две цифры
         //(ограничил время срабатывания кормушки 9.9 секундами)
            inputString += key;  // Добавляем цифру в строку
            if (cursorPosition == 0) {  // Если первая цифра
            lcd.setCursor(cursorPosition++, 1);
            lcd.print(key);
            lcd.print('.');
            } else {  // Если вторая цифра
            lcd.setCursor(cursorPosition - 1, 1);
            lcd.print(inputString[0]);
            lcd.print('.');
            lcd.print(key);
            cursorPosition++;
          }
        } 
          if (cursorPosition == 2) {lcd.setCursor(0, 0);
          lcd.print("Press D to save ");
          }
          if (key == 'D' && inputString.length() == 2) {  // Выход из функции по нажатию клавиши D
          char firstChar = inputString.charAt(0);  // Первая цифра - целые секунды
          char secondChar = inputString.charAt(1); // Вторая цифра - десятые доли секунд
          int wholeSeconds = firstChar - '0';  // Преобразуем символ в число
          int tenthsOfSecond = secondChar - '0';  // Преобразуем символ в число
         float new_duration = wholeSeconds + (tenthsOfSecond / 10.0);  // Формируем итоговое значение
          if (new_duration <= 9.9f) {  // Проверка, что значение не превышает 9.9 секунды
          duration = new_duration * 1000;  // Переводим секунды в миллисекунды
          //Serial.print("New feeding set: "); // Вывод в монитор порта для контроля и отладки
          // Serial.println(new_duration); // Вывод в монитор порта для контроля и отладки
          EEPROM.update(DURATION_ADDRESS, highByte(duration));
          EEPROM.update(DURATION_ADDRESS + 1, lowByte(duration));
          lcd.clear();
          return;
          } else {
          // Обработка ошибки, если введено слишком большое значение (оставил на будущее)
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Error: Max is 9.9!");
          delay(2000);
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Enter feeding:");
          lcd.setCursor(4, 1);
          lcd.print("in X.X sec");
          lcd.setCursor(0, 1);
          inputString = "";
          cursorPosition = 0;
        }
      }
    }
  }
}
  // Если дозатор в виде задвижки
  // Срабатывание сервопривода по времени и при нажатии на кнопку A
void triggerServo() {
  // Вывод на экран времени срабатывания и дозы
  lcd.clear();
  lcd.setCursor(1, 0);
  lcd.print("Feeding Time!");
  lcd.setCursor(0, 1);
  lcd.print("Feed: ");
  if (feedingHour < 10) lcd.print(' '); // убираем незначащий ноль часов
  lcd.print(feedingHour);
  lcd.print(':');
  if (feedingMinute < 10) lcd.print('0');
  lcd.print(feedingMinute);
  lcd.setCursor(12, 1);
  lcd.print(wholeSeconds);  // Выводим целые секунды
  lcd.print(".");           // Выводим точку
  lcd.print(tenthOfSecond);  // Выводим десятые доли секунды
  lcd.setCursor(15, 1);
  lcd.print("s");
  // Конец вывода
  tone(BUZZER_PIN, 700); // Звук при срабатывании
  servo_feed.write(180); // Поворот сервомотора (открытие)
  delay(duration); // Время срабатывания сервомотора
  noTone(BUZZER_PIN); // Отключение звукового сигнала
  servo_feed.write(90); // Возвращение сервомотора в исходное положение
  }
Чтобы проверить, что программа не искажается в редакторе форума, я скопировал программу из этого сообщения, вставил в Ардуино IDE, прошил Ардуино Нано:

Скетч использует 13952 байт (45%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 652 байт (31%) динамической памяти, оставляя 1396 байт для локальных переменных. Максимум: 2048 байт.

И установил Ардуино в кормушку. Кормушка работает.

Позже я добавил в кормушку второе срабатывание, если температура окружающего воздуха опустится ниже минус 10 градусов, то кормушка выдаст вторую порцию корма в 15:00.
Термодатчик, который стоит в модуле часов DS3231, как пишут, неточный, +-3 градуса (в моем модуле он врал на +1 градус).
И предел температуры и второе время кормления, погрешность температурного датчика жестко заданы прямо в программе, при желании их можно поменять, я прокомментировал эти строчки кода, чтобы было проще найти

Программа обновлена 26.01.2025.:
Прописана коррекция неточности датчика температуры модуля DS3231 в коде программы
С помощью клавиатуры осуществляется:
Ввод изначального времени для модуля DS323 (при замене батарейки или коррекции показаний часов)
Ввод времени для первого срабатывания и продолжительность первого срабатывания (размер порции)
Ввод времени для второго срабатывания (размер порции берется из первого срабатывания)
Отключение/включение второго срабатывания по времени, если это нужно.
Настройка отрицательной температуры, при котором кормушка дополнительно сработает и времени этого срабатывания (размер порции берется из первого срабатывания).
Ручное кормление (размер порции берется из первого срабатывания).
Также на экране выводятся все установленные значения.

Программа кормушки для птиц на Ардуино Нано:

Код: Выделить всё

#include <RTClib.h> // Подключение библиотеки часов
#include <Servo.h> // Подключение библиотеки управления серво
#include <Wire.h> // Подключение библиотеки протокола i2c
#include <LiquidCrystal_I2C.h> // Подключение библиотеки экрана
#include <Keypad.h> // Подключение библиотеки клавиатуры
#include <EEPROM.h> // Подключение библиотеки для работы с EEPROM

// Размеры клавиатуры
const byte ROWS = 4; // 4 строки
const byte COLS = 4; // 4 столбца

// Задаём карту расположения клавиш клавиатуры
char keys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

// Соединение строк клавиатуры с контактами Arduino
byte rowPins[ROWS] = {11, 10, 9, 8};
// Соединение колонок клавиатуры с контактами Arduino
byte colPins[COLS] = {7, 6, 5, 4};

// Создание объекта клавиатуры
Keypad kpd = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
RTC_DS3231 rtc;
Servo servo_feed; // Даем имя для переменной сервомотора
LiquidCrystal_I2C lcd(0x27, 16, 2); // Адрес I2C дисплея

// Установки переменных по умолчанию (цифры по идее не нужны, но без них глючит)
int feedingHour = 12;    // Время кормления: часы
int feedingMinute = 0;   // Время кормления: минуты
int secondFeedingHour = 18; // Время второго кормления: часы
int secondFeedingMinute = 0; // Время второго кормления: минуты
unsigned long duration = 500; // Длительность работы сервопривода (мс)
bool enableSecondFeeding = false; // Флаг для включения/отключения второго кормления

int wholeSeconds; // Время кормления - секунды (для отображения на экране)
int tenthOfSecond; // Время кормления - десятые доли секунд (для отображения на экране)
int grad; // Переменная для температуры для увеличения частоты кормлений от температуры
int tempTriggerHour = 13; // Часы для срабатывания по температуре
int tempTriggerMinute = 0; // Минуты для срабатывания по температуре
int tempThreshold = -10; // Температура для срабатывания кормушки
const int TEMP_CORRECTION = 1; // Коррекция погрешности показаний температурного датчика часов,
/ у меня он врет на +1 градуса (устанавливается опытным путем).
bool feed = true; // Флаг для для разрешения срабатывания серво по времени первое кормление
bool feedSecond = true; // Для второго кормления
bool feedByTemp = true; // Флаг для разрешения срабатывания серво по температуре
char key;
int r[4];
//DateTime lastFeedTime; // Переменная для хранения времени последнего кормления
DateTime lastFeedTimeFirst;  // Время последнего первого кормления
DateTime lastFeedTimeSecond; // Время последнего второго кормления

// Адреса для хранения данных в EEPROM
#define FEEDING_HOUR_ADDRESS 0   // Адрес для хранения часов кормления (1 байт)
#define FEEDING_MINUTE_ADDRESS 1 // Адрес для хранения минут кормления (1 байт)
#define DURATION_ADDRESS 2       // Адрес для хранения длительности срабатывания сервомотора (2 байта)
#define SECOND_FEEDING_HOUR_ADDRESS 4  // Адрес для хранения часов второго кормления (1 байт)
#define SECOND_FEEDING_MINUTE_ADDRESS 5 // Адрес для хранения минут второго кормления (1 байт)
#define SECOND_FEEDING_ENABLE_ADDRESS 6 // Адрес для хранения флага включения второго кормления (1 байт)
#define TEMP_THRESHOLD_ADDRESS 7 // Адрес для хранения температуры для срабатывания (1 байт)
#define TEMP_TRIGGER_HOUR_ADDRESS 8 // Адрес для хранения часов для срабатывания по температуре (1 байт)
#define TEMP_TRIGGER_MINUTE_ADDRESS 9 // Адрес для хранения минут для срабатывания по температуре (1 байт)
#define BUZZER_PIN 3 // Определение пина для подключения динамика
 
void setup() {
  servo_feed.attach(2); // Контакт для подключения сервомотора D2
    if (!rtc.begin()) {
    while (1); // Остановка программы, если RTC не найден
    }
      // Если часы теряли питание, устанавливаем текущее время
      // с помощью компа при компиляции программы
        //(для первоначальной установки времени надо 	 
      // передернуть батарейку)
    if (rtc.lostPower()) {
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // Установка текущего времени при компиляции 	 
   //программы
    }
  lastFeedTimeFirst = rtc.now();   // Устанавливаем начальное значение времени для первого кормления
  lastFeedTimeSecond = rtc.now();  // Устанавливаем начальное значение времени для второго кормления
  lcd.init();                 // Инициализация дисплея
  lcd.backlight();            // Включение подсветки

  // Загрузка сохранённых значений из EEPROM
  feedingHour = EEPROM.read(FEEDING_HOUR_ADDRESS);
  feedingMinute = EEPROM.read(FEEDING_MINUTE_ADDRESS);
  duration = EEPROM.read(DURATION_ADDRESS) * 256 + EEPROM.read(DURATION_ADDRESS + 1);
  secondFeedingHour = EEPROM.read(SECOND_FEEDING_HOUR_ADDRESS);
  secondFeedingMinute = EEPROM.read(SECOND_FEEDING_MINUTE_ADDRESS);
  enableSecondFeeding = EEPROM.read(SECOND_FEEDING_ENABLE_ADDRESS);
  tempThreshold = static_cast<int8_t>(EEPROM.read(TEMP_THRESHOLD_ADDRESS));
  tempTriggerHour = EEPROM.read(TEMP_TRIGGER_HOUR_ADDRESS);
  tempTriggerMinute = EEPROM.read(TEMP_TRIGGER_MINUTE_ADDRESS);

  // Установка начального угла поворота сервопривода - чтобы закрыть кормушку
  servo_feed.write(90); // Это значение конкретно для серво 9g 180, 90 это центр
  delay(1000); // Задержка, чтобы серво успел встать в начальное положение
  tone(BUZZER_PIN, 3000, 50); // Короткий звуковой сигнал что система включилась

  // Подсказки по клавишам
  lcd.setCursor(0, 0);
  lcd.print("Press A:");
  lcd.setCursor(0, 1);
  lcd.print("Manual Feeding");
  delay(2000);
  lcd.setCursor(0, 0);
  lcd.print("Press B:");
  lcd.setCursor(0, 1);
  lcd.print("Set Feeding Time");
  delay(2000);
  lcd.setCursor(0, 0);
  lcd.print("Press *:");
  lcd.setCursor(0, 1);
  lcd.print("                ");
  lcd.setCursor(0, 1);
  lcd.print("Set Time & Date");
  delay(2000);
  lcd.setCursor(0, 0);
  lcd.print("Press D:");
  lcd.setCursor(0, 1);
  lcd.print("                ");
  lcd.setCursor(0, 1);
  lcd.print("To save data");
  delay(2000);
  lcd.setCursor(0, 0);
  lcd.print("Press C:");
  lcd.setCursor(0, 1);
  lcd.print("                ");
  lcd.setCursor(0, 1);
  lcd.print("Cancel entry");
  delay(2000);
  lcd.clear();
  tone(BUZZER_PIN, 3500, 50);
}

void loop() {
  // Обработка нажатия кнопок A и B
    char key = kpd.getKey(); // Обработчик кнопок
if (key != NO_KEY) {
    if (key == 'A') {
        triggerServo(); // Ручное кормление кнопкой A. Переход на функцию кормления
    } else if (key == 'B') {
        setFeedingTime(); // Устанавливаем время срабатывания кормушки кнопкой B
        // Переход на функцию установки времени и продолжительности работы серво
    } else if (key == '*') {
        setDateTime(); // Переход на функцию установки времени и даты
    }
}
  int rawTemp = static_cast<int8_t>(rtc.getTemperature()); // Приводим к int8_t, чтобы поддерживать отрицательные значения
  grad = rawTemp - TEMP_CORRECTION;
  DateTime now = rtc.now(); // Получение текущего времени
  lcd.setCursor(0, 0);
  lcd.print("Time:");
  if (now.hour() < 10) { // Устранение смещение отображения для часов (и убираем незначащий ноль часов)
  lcd.setCursor(5, 0);
  lcd.print(' ');
  }
  lcd.print(now.hour());
// Логика мигания двоеточия
static unsigned long previousMillis = 0; // Переменная для хранения времени последнего обновления
static unsigned long previousMillisF = 0;
unsigned long currentMillis = millis(); // Текущее время
static bool colonState = true; // Состояние двоеточия (по умолчанию включено)
static bool sFeed = true;
// Проверяем, прошло ли достаточно времени для переключения состояния
if (currentMillis - previousMillis >= 500) { // 1000 мс = 1 секунда
  colonState = !colonState; // Меняем состояние двоеточия
  previousMillis = currentMillis; // Сохраняем время последнего переключения
}
if (currentMillis - previousMillisF >= 5000) {
  sFeed = !sFeed;
  previousMillisF = currentMillis;
}
// Устанавливаем курсор для двоеточия
lcd.setCursor(7, 0); // Позиция для двоеточия
if (colonState) {
  lcd.print(':'); // Включаем двоеточие
} else {
  lcd.print(' '); // Убираем двоеточие
}
  if (now.minute() < 10) lcd.print('0'); // Добавляем ведущий ноль, если нужно
  lcd.print(now.minute());
  lcd.setCursor(0, 1);
  lcd.print("Feed:");
  if (enableSecondFeeding == 1){
  if (sFeed) {
  lcd.setCursor(5, 1);
  if (feedingHour < 10) lcd.print(' '); // убираем незначащий ноль часов
  lcd.print(feedingHour);
  lcd.print(':');
  if (feedingMinute < 10) lcd.print('0');
  lcd.print(feedingMinute);
  lcd.setCursor(10, 1);
  lcd.print(" ");
  } else {
  lcd.setCursor(5, 1);
  if (secondFeedingHour < 10) lcd.print(' '); // убираем незначащий ноль часов
  lcd.print(secondFeedingHour);
  lcd.print(':');
  if (secondFeedingMinute < 10) lcd.print('0');
  lcd.print(secondFeedingMinute);
    }
  } else {
  lcd.setCursor(5, 1);
  if (feedingHour < 10) lcd.print(' '); // убираем незначащий ноль часов
  lcd.print(feedingHour);
  lcd.print(':');
  if (feedingMinute < 10) lcd.print('0');
  lcd.print(feedingMinute);
  lcd.setCursor(10, 1);
  lcd.print(" ");
  }
  unsigned long durationMs = duration;  // Получаем длительность в миллисекундах
  float durationSec = durationMs / 1000.0;  // Переводим миллисекунды в секунды с плавающей точкой
  // Разделяем целую и дробную части
  wholeSeconds = floor(durationSec);  // Целые секунды
  tenthOfSecond = round((durationSec - wholeSeconds) * 10);  // Десятые доли секунды
  if (sFeed) {
  lcd.setCursor(11, 0);
  lcd.print(grad);
  lcd.write(223);
  lcd.print("C  ");
  lcd.setCursor(11, 1);
  lcd.print(wholeSeconds);  // Выводим целые секунды
  lcd.print(".");           // Выводим точку
  lcd.print(tenthOfSecond);  // Выводим десятые доли секунды
  lcd.setCursor(14, 1);
  lcd.print("s ");  
  } else {
  lcd.setCursor(11, 0);
  lcd.print(tempThreshold);
  lcd.print("on");
  lcd.setCursor(11, 1);
  lcd.print(tempTriggerHour);
  lcd.print(":");
  if (tempTriggerMinute < 10) lcd.print('0');
  lcd.print(tempTriggerMinute);
  }

  // Проверка срабатывания по основному времени кормления
  if (feed && now.hour() == feedingHour && now.minute() == feedingMinute) {
    triggerServo();
    feed = false;
    lastFeedTimeFirst = now;
  }

  // Проверка срабатывания второго времени кормления
  if (enableSecondFeeding && feedSecond && now.hour() == secondFeedingHour && now.minute() == secondFeedingMinute) {
    triggerServo();
    feedSecond = false;
    lastFeedTimeSecond = now;
  }

  // Проверка срабатывания по температуре
  if (feedByTemp && grad <= tempThreshold && now.hour() == tempTriggerHour && now.minute() == tempTriggerMinute) {
    triggerServo();
    feedByTemp = false;
  }
 // Обновление флага через 24 часа
   if (!feedByTemp && now.hour() == 0 && now.minute() == 0) {
    feedByTemp = true;
  }
   // Обновление флагов через 2 минуты
  if (!feed && (now.unixtime() - lastFeedTimeFirst.unixtime() >= 120)) {
    feed = true;
    }
  if (!feedSecond && (now.unixtime() - lastFeedTimeSecond.unixtime() >= 120)) {
    feedSecond = true;
    }  
}
  // Функция установки времени срабатывания сервопривода
void setFeedingTime() {
  tone(BUZZER_PIN, 1500, 50);
  feed = true;
  int i = 0;
  char j = 0; 
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Select feeding:");
  lcd.setCursor(0, 1);
  lcd.print("1-First 2-Second");

  char selectedFeeding = 0;
  while (true) {
    key = kpd.getKey();
    if (key != NO_KEY) {
      tone(BUZZER_PIN, 1000, 50);
      delay(51);
      if (key == '1' || key == '2') {
        selectedFeeding = key;
        break;
      }
    }
  }

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Set time HH:MM");
  lcd.setCursor(0, 1);
  lcd.print("HH:MM");
  while (true) {
    key = kpd.getKey();
    if (key != NO_KEY) {
      tone(BUZZER_PIN, 1000, 50);
      delay(51);
      if (key == 'C') {
        i = 0;
        j = 0;
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Set time HH:MM");
        lcd.setCursor(0, 1);
        lcd.print("HH:MM");
        continue;
      }
      lcd.setCursor(j, 1);
      lcd.print(key);
      r[i] = key - 48;
      i++;
      j++;
      if (j == 2) {
        lcd.print(":");
        j++;
      }
    }

    if (i >= 4) {
      int hours = r[0] * 10 + r[1];
      int minutes = r[2] * 10 + r[3];

      if (hours < 24 && minutes < 60) {
        lcd.setCursor(0, 0);
        lcd.print("Press D to save ");
        while (true) {
          key = kpd.getKey();
          if (key == 'D') {
            tone(BUZZER_PIN, 1000, 50);
            if (selectedFeeding == '1') {
              feedingHour = hours;
              feedingMinute = minutes;
              EEPROM.update(FEEDING_HOUR_ADDRESS, feedingHour);
              EEPROM.update(FEEDING_MINUTE_ADDRESS, feedingMinute);
              getDurationInput(); // Переход к установке длительности после первого времени
            } else if (selectedFeeding == '2') {
              secondFeedingHour = hours;
              secondFeedingMinute = minutes;
              EEPROM.update(SECOND_FEEDING_HOUR_ADDRESS, secondFeedingHour);
              EEPROM.update(SECOND_FEEDING_MINUTE_ADDRESS, secondFeedingMinute);
              lcd.clear();
              lcd.setCursor(0, 0);
              lcd.print("2nd feed enable:");
              lcd.setCursor(0, 1);
              lcd.print("1-Yes 2-No");
              while (true) {
                key = kpd.getKey();
                if (key == '1' || key == '2') {
                  enableSecondFeeding = (key == '1');
                  EEPROM.update(SECOND_FEEDING_ENABLE_ADDRESS, enableSecondFeeding);
                  setTemperatureSettings(); // Вызов функции настройки температуры
                  tone(BUZZER_PIN, 1500, 50);
                  lcd.clear();
                  return;
                }
              }
            }
            return;
          }
        }
      } else {
        tone(BUZZER_PIN, 500, 50);
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Incorrect time!");
        lcd.setCursor(0, 1);
        lcd.print("Please try again.");
        delay(2000);
        i = 0;
        j = 0;
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Set time HH:MM");
        lcd.setCursor(0, 1);
        lcd.print("HH:MM");
      }
    }
  }
}

  //Установка продолжительности срабатывания серво
void getDurationInput() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Enter feeding:");
  lcd.setCursor(4, 1);
  lcd.print("in X.X sec");
  String inputString = "";  // Пустая строка для хранения введенных чисел
  int cursorPosition = 0;   // Позиция курсора на экране
  while (true) {
    key = kpd.getKey();
    if (key != NO_KEY) {
      tone(BUZZER_PIN, 1000, 50); // Звук при нажатии кнопки
      delay(51);                  // Ожидание завершения звука
      if (key == 'C') {  // Сброс чисел
        inputString = "";
        cursorPosition = 0;
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("Enter feeding:");
        lcd.setCursor(4, 1);
        lcd.print("in X.X sec");
        lcd.setCursor(0, 1);
        continue;
      }
        if (isDigit(key) && cursorPosition < 2) {  // Принимаем только две цифры
         //(ограничил время срабатывания кормушки 9.9 секундами)
            inputString += key;  // Добавляем цифру в строку
            if (cursorPosition == 0) {  // Если первая цифра
            lcd.setCursor(cursorPosition++, 1);
            lcd.print(key);
            lcd.print('.');
            } else {  // Если вторая цифра
            lcd.setCursor(cursorPosition - 1, 1);
            lcd.print(inputString[0]);
            lcd.print('.');
            lcd.print(key);
            cursorPosition++;
          }
        } 
          if (cursorPosition == 2) {lcd.setCursor(0, 0);
          lcd.print("Press D to save ");
          }
          if (key == 'D' && inputString.length() == 2) {  // Выход из функции по нажатию клавиши D
          char firstChar = inputString.charAt(0);  // Первая цифра - целые секунды
          char secondChar = inputString.charAt(1); // Вторая цифра - десятые доли секунд
          int wholeSeconds = firstChar - '0';  // Преобразуем символ в число
          int tenthsOfSecond = secondChar - '0';  // Преобразуем символ в число
         float new_duration = wholeSeconds + (tenthsOfSecond / 10.0);  // Формируем итоговое значение
          if (new_duration <= 9.9f) {  // Проверка, что значение не превышает 9.9 секунды
          duration = new_duration * 1000;  // Переводим секунды в миллисекунды
          EEPROM.update(DURATION_ADDRESS, highByte(duration));
          EEPROM.update(DURATION_ADDRESS + 1, lowByte(duration));
          lcd.clear();
          return;
          } else {
          // Обработка ошибки, если введено слишком большое значение (оставил на будущее)
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Error: Max is 9.9!");
          delay(2000);
          lcd.clear();
          lcd.setCursor(0, 0);
          lcd.print("Enter feeding:");
          lcd.setCursor(4, 1);
          lcd.print("in X.X sec");
          lcd.setCursor(0, 1);
          inputString = "";
          cursorPosition = 0;
        }
      }
    }
  }
}
  // Если дозатор в виде задвижки
  // Срабатывание сервопривода по времени, по температуре и при нажатии на кнопку A
void triggerServo() {
  lcd.clear();
  lcd.setCursor(1, 0);
  lcd.print("Feeding Time!");
  tone(BUZZER_PIN, 700); // Звук при срабатывании
  servo_feed.write(180); // Поворот сервомотора (открытие)
  delay(duration); // Время срабатывания сервомотора
  noTone(BUZZER_PIN); // Отключение звукового сигнала
  servo_feed.write(90); // Возвращение сервомотора в исходное положение
  lcd.clear();
  }

void setTemperatureSettings() {
  tone(BUZZER_PIN, 1500, 50);
  int i = 0;
  char j = 0;
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Set Temp -xx");
  lcd.write(223);
  lcd.print("C:");
  lcd.setCursor(0, 1);
  lcd.print(" -");

  // Ввод температуры
  while (true) {
    key = kpd.getKey();
    if (key != NO_KEY) {
      tone(BUZZER_PIN, 1000, 50);
      delay(51);
      if (isdigit(key)) {
        r[i++] = key - '0';
        lcd.setCursor(j + 2, 1);
        lcd.print(key);
        j++;
        if (i == 2) break; // Ограничение на 2 цифры
      } else if (key == 'C') {
        return; // Отмена ввода
      }
    }
  }

  tempThreshold = -(r[0] * 10 + r[1]); // Устанавливаем отрицательное значение температуры

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Set Temp. Time:");
  lcd.setCursor(0, 1);
  lcd.print("HH:MM");

  i = 0;
  j = 0;

  // Ввод времени
  while (true) {
    key = kpd.getKey();
    if (key != NO_KEY) {
      tone(BUZZER_PIN, 1000, 50);
      delay(51);
      if (isdigit(key)) {
        r[i++] = key - '0';
        lcd.setCursor(j, 1);
        lcd.print(key);
        j++;
        if (i == 2 || i == 4) {
          lcd.setCursor(j, 1);
          lcd.print(':');
          j++;
        }
        if (i == 4) break;
      } else if (key == 'C') {
        return; // Отмена ввода
      }
    }
  }

  tempTriggerHour = r[0] * 10 + r[1];
  tempTriggerMinute = r[2] * 10 + r[3];

  // Сохранение в EEPROM
  EEPROM.update(TEMP_THRESHOLD_ADDRESS, static_cast<int8_t>(tempThreshold));
  EEPROM.update(TEMP_TRIGGER_HOUR_ADDRESS, tempTriggerHour);
  EEPROM.update(TEMP_TRIGGER_MINUTE_ADDRESS, tempTriggerMinute);

  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Temp settings");
  lcd.setCursor(0, 1);
  lcd.print("saved!");
  delay(1500);
}

// Функция ручной установки времени и даты модуля DS 3231
void setDateTime() {
    tone(BUZZER_PIN, 1000, 50); // Короткий сигнал
    delay(50);
    String input = ""; // Хранение всего ввода в виде строки
    char key;

    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Set Time & Date");
    lcd.setCursor(0, 1);
    lcd.print("HH:MM DD/MM YYYY");
    delay(1000);

    while (true) {
        // Считываем нажатие клавиши
        key = kpd.getKey();
        if (key != NO_KEY) {
            tone(BUZZER_PIN, 1000, 50); // Короткий сигнал на каждое нажатие
            delay(50);

            if (key == 'C') {
              tone(BUZZER_PIN, 2000, 50);
                                delay(50); // Если нажата кнопка 'C'
                lcd.clear();
                lcd.setCursor(0, 0);
                lcd.print("Input cancelled");
                delay(2000);
                lcd.clear();
                return; // Выход из функции
            }

            if (key >= '0' && key <= '9') { // Если нажата цифра
                if (input.length() < 12) {  // Ограничиваем ввод до 12 символов (ЧЧММДДММГГГГ)
                    input += key;
                }
            }

            // Обновляем строку на экране после каждого нажатия
            lcd.setCursor(0, 1);

            // Формируем строку с разделителями
            lcd.print(input.length() >= 2 ? input.substring(0, 2) : (input.length() >= 1 ? "0" + input.substring(0, 1) : "HH"));
            lcd.print(":");
            lcd.print(input.length() >= 4 ? input.substring(2, 4) : (input.length() >= 3 ? "0" + input.substring(2, 3) : "MM"));
            lcd.print(" ");
            lcd.print(input.length() >= 6 ? input.substring(4, 6) : (input.length() >= 5 ? "0" + input.substring(4, 5) : "DD"));
            lcd.print("/");
            lcd.print(input.length() >= 8 ? input.substring(6, 8) : (input.length() >= 7 ? "0" + input.substring(6, 7) : "MM"));
            lcd.print(" ");
            lcd.print(input.length() >= 12 ? input.substring(8, 12) : (input.length() >= 9 ? "200" + input.substring(8, 9) : "YYYY"));

            // После ввода 12 цифр показываем сообщение
            if (input.length() == 12) {
                int hours = input.substring(0, 2).toInt();
                int minutes = input.substring(2, 4).toInt();
                int day = input.substring(4, 6).toInt();
                int month = input.substring(6, 8).toInt();
                int year = input.substring(8, 12).toInt();

                // Проверка корректности данных
                bool valid = true;

                if (hours < 0 || hours > 23) valid = false;      // Часы: 0-23
                if (minutes < 0 || minutes > 59) valid = false;  // Минуты: 0-59
                if (month < 1 || month > 12) valid = false;      // Месяц: 1-12

                // Проверка дней в месяце
                int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
                if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
                    daysInMonth[1] = 29; // Учитываем високосный год
                }
                if (day < 1 || day > daysInMonth[month - 1]) valid = false;

                if (valid) {
                    lcd.setCursor(0, 0);
                    lcd.print("Press D to save");
                        tone(BUZZER_PIN, 2000, 50);
                        delay(50);
                    while (true) {
                        key = kpd.getKey();
                        if (key == 'C') { // Если нажата кнопка 'C'
                            lcd.clear();
                                tone(BUZZER_PIN, 2000, 50);
                                delay(50);
                            lcd.setCursor(0, 0);
                            lcd.print("Input cancelled");
                            delay(2000);
                            lcd.clear();                        
                            return; // Выход из функции
                        }
                        if (key == 'D') {
                          tone(BUZZER_PIN, 2000, 50);
                                delay(50); // Если нажата клавиша 'D', завершаем ввод
                            break;
                        }
                    }
                    break;
                } else {
                    lcd.clear();
                    lcd.setCursor(0, 0);
                    lcd.print("Invalid input");
                    delay(2000);
                    lcd.clear();
                    lcd.setCursor(0, 0);
                    lcd.print("Set Time & Date");
                    lcd.setCursor(0, 1);
                    lcd.print("HH:MM DD/MM YYYY");
                    input = ""; // Сброс ввода
                }
            }
        }
    }

    // Установка времени и даты в RTC
    rtc.adjust(DateTime(input.substring(8, 12).toInt(), input.substring(6, 8).toInt(), input.substring(4, 6).toInt(),
                        input.substring(0, 2).toInt(), input.substring(2, 4).toInt(), 0)); // Секунды = 0

    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Time & Date Set");
    delay(2000);
    tone(BUZZER_PIN, 2000, 50);
    delay(50);
    lcd.clear();
}
На экран, для контроля окружающей температуры, после значения часов вывел значение температуры:
grad.jpg

В принципе, если сократить название Time до T и подвинуть влево часы, то можно после значения градусов поставить значок градуса ° и букву С, ну это так, для красоты.
В данным момент кормушка с этой программой стоит тестируется, жду, когда похолодает, чтобы проверить срабатывания дополнительного кормления.

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

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

F16.jpg
F16.jpg (14.23 КБ) 352 просмотра

Программа также обновлена.

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

Размещена новая версия программы. Реализован ввод времени и даты с помощью клавиатуры при нажатии на кнопку * (программа проверяет корректность введенных данных):

F15.jpg
F15.jpg (8.59 КБ) 366 просмотров

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

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

Итак, очередное, надеюсь, последнее обновление программы от 25.01.2025
Добавлен ввод времени для второго срабатывания кормушки (то есть можно отдельно выбрать настройки для первого и второго срабатывания:

F17.jpg
F17.jpg (6.74 КБ) 320 просмотров

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

После ввода времени второго срабатывания открывается настройка включения или отключения второго срабатывания кормушки, то есть в этом меню можно оставить только первое срабатывание или включить еще и дополнительное, второе срабатывание:

F18.jpg
F18.jpg (6.83 КБ) 320 просмотров

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

F19.jpg
F19.jpg (4.3 КБ) 320 просмотров

И время для этого срабатывания:

F20.jpg
F20.jpg (5.99 КБ) 320 просмотров

Максимально кормушка, при совпадении всех условий, сработает три раза в сутки - при первом времени, втором и при температуре.

Срабатывание по температуре не отключается, но можно ввести -99 градусов, тогда, конечно, этого срабатывания не будет.

Все данные не пропадают при отключения питания. На каждом шаге можно стереть ввод кнопкой C.

Поскольку на экране мало места, то вывод всех значения я сделал в мигающем режиме, раз в пять секунд кормушка показывает все установленные времена кормлений, продолжительность и установленную температуру при которой она дополнительно сработает. Примеры отображения:
F21.jpg
F21.jpg (7.11 КБ) 320 просмотров
F22.jpg
F22.jpg (7.1 КБ) 320 просмотров
Если второе кормление по времени отключено, то на экране оно не отображается. Кормление по температуре отображается всегда.

Ввод коррекции погрешности для термодатчика (пока) делать не стал, так как подумал, что если кто и будет повторять кормушку, то сделав сопоставления температуры именно своего модуля DS3231 с градусником, внесет эту коррекцию в программу сам на стадии прошивки Ардуино.

P.S Обнаружил глюк в срабатывании кормушки, если время кормления изменить с более позднего на более раннее, в этом случает кормушка сработает только на следующие сутки, пропустив срабатывание.
Это происходит из-за этого выражения:

Код: Выделить всё

  if (!feedSecond && (now.unixtime() - lastFeedTimeSecond.unixtime() >= 86400)) {
    feedSecond = true;
    } 
То есть программа ждет сутки со времени предыдущего срабатывания, а уже после этого на следующие сутки срабатывает по новому времени и далее раз в сутки.
Чтобы этого не было, надо изменить 86400 (время в сутках в секундах) на, например, 120 (неважно на сколько, главное чтобы это время было больше одной минуты):

Код: Выделить всё

  if (!feedSecond && (now.unixtime() - lastFeedTimeSecond.unixtime() >= 120)) {
    feedSecond = true;
    } 
Программа (и первая и вторая) в этом сообщении также обновлена.
Последний раз редактировалось kvg 26 янв 2025, 19:10, всего редактировалось 50 раз.
Аватара пользователя
kvg
Администратор
Сообщения: 1305
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:

kvg » 10 янв 2025, 17:15

За это время кормушка для кошек уже наработала статистику, а поскольку в кормушке конструктивно другой дозатор и программа тоже изменена в сторону энергосбережения (кормушка работает от батарей), то здесь я буду писать про нее.
Итак. Автономная кормушка на Ардуино Нано для кошек.

(Coming soon)
Последний раз редактировалось kvg 19 фев 2025, 13:48, всего редактировалось 2 раза.
Аватара пользователя
kvg
Администратор
Сообщения: 1305
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:

kvg » 10 янв 2025, 17:15

Резерв1
Аватара пользователя
pc-porta
Сообщения: 1790
Зарегистрирован: 13 дек 2021, 13:57
Контактная информация:

pc-porta » 17 янв 2025, 09:57

Круто. Костя - герой
Аватара пользователя
стронций
Сообщения: 894
Зарегистрирован: 13 дек 2021, 10:18
Контактная информация:

стронций » 17 янв 2025, 12:47

Надо было делать кормушку для котоптиц или птицекотов. Универсальная кормушка такая.
Можно было бы сделать датчик движения, как только прилетает котоптица или птицекот открывается кормушки и из нее выпадает еда.
Аватара пользователя
kvg
Администратор
Сообщения: 1305
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:

kvg » 17 янв 2025, 16:04

Да, идей много, была такая идея кормить по датчику движения, но тогда какой смысл в автоматической кормушке? Можно применить бункерную кормушку, откуда птицы сами вытаскивают корм.
Аватара пользователя
xCooLx
Сообщения: 159
Зарегистрирован: 14 дек 2021, 13:11
Контактная информация:

xCooLx » 21 янв 2025, 19:04

2 набора лежит Ардуино, давно хотел заняться изучением, времени нехватка :crying
Мучитель Струйников :fix
Аватара пользователя
kvg
Администратор
Сообщения: 1305
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:

kvg » 21 янв 2025, 19:17

Да я тоже через силу, но интересно же. Вот на днях купил адресную ленту, тоже тема. Но я делаю поделки, которые мне нужны, а не типа для прикола, всякие цветомузыки, мигалки, свистелки-перделки, хотя, это тоже очень интересно, но их сделал и они валяться будут, а вещь (та же кормушка) работает и приносит пользу.
Аватара пользователя
капелька
Сообщения: 233
Зарегистрирован: 13 дек 2021, 08:32
Откуда: Пермь
Контактная информация:

капелька » 27 янв 2025, 08:46

Завидую, есть время на всё это.