Уголок kvg
Пишу, что придёт в голову, и, если не придет, то всё равно пишу - я как кузнец, я не могу никуя! :crazy
Аватара пользователя
kvg
Администратор
Сообщения: 1266
Зарегистрирован: 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
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Б) 4 скачивания

Сначала схему собрал на макетной плате:
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Б) 8 скачиваний

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

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

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

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

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

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

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

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

Как итог, получилась вот такая кормушка, я скрестил банку с табуреткой:)
F12.jpg

Она уже на этих выходных начнет работу, также в комментариях я выложу и программу и все остальное, как дойдут руки.
Последний раз редактировалось kvg 10 янв 2025, 17:40, всего редактировалось 3 раза.
3 комментария 96 просмотров
Комментарии
Аватара пользователя
kvg
Администратор
Сообщения: 1266
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:

kvg » 10 янв 2025, 17:14

Схема подключения:
Feed-zadvijka.jpg

Программа.

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

#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() >= 86400)) {
    feed = true; // Разрешаем новое срабатывание, если прошло 24 часа
    }
}
  // Функция установки времени срабатывания сервопривода
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 градус), но точность нам и не нужна.
И предел температуры и второе время кормления жестко заданы прямо в программе, при желании их можно поменять, я прокомментировал эти строчки кода, чтобы было проще найти

Программа (срабатывание второго раза в 15:00 при температуре <= -10°С):

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

#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; // Флаг для для разрешения срабатывания серво по времени
bool feedByTemp = true; // Флаг для разрешения срабатывания серво по температуре
char key;
int r[4];
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.
             //Переход на функцию установки времени и продолжительности работы серво
        }
    }
  grad = rtc.getTemperature(); // Получение текущей температуры
  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());
  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");
  lcd.setCursor(14, 0);
  lcd.print(grad);

  // Условие срабатывания серво - должны совпасть часы, минуты и feed быть true
  if (feed && now.hour() == feedingHour && now.minute() == feedingMinute) {
    triggerServo();
    feed = false; // Отключаем возможность повторного срабатывания
    lastFeedTime = now; // Запоминаем время срабатывания
  }

  // Условие срабатывания серво раз в сутки в 15:00 при температуре в минус 10 градусов цельсия
  if (feedByTemp && grad <= -10 && now.hour() == 15 && now.minute() == 0) {
    triggerServo();
    feedByTemp = false; // Отключаем повторное срабатывание по температуре
  }

    if (!feedByTemp && now.hour() == 0 && now.minute() == 0) {
       feedByTemp = true; // Разрешаем срабатывание по температуре после 24 часов
}

   // Проверяем, прошли ли сутки с момента последнего кормления
   //(без этой проверки кормушка иногда не срабатывала)
  if (!feed && (now.unixtime() - lastFeedTime.unixtime() >= 86400)) {
    feed = true; // Разрешаем новое срабатывание, если прошло 24 часа
    }
}
  // Функция установки времени срабатывания сервопривода
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); // Возвращение сервомотора в исходное положение
  }
На экран, для контроля окружающей температуры, после значения часов вывел значение температуры:
grad.jpg

В принципе, если сократить название Time до T и подвинуть влево часы, то можно после значения градусов поставить значок градуса ° и букву С, ну это так, для красоты.
В данным момент кормушка с этой программой стоит тестируется, жду, когда похолодает, чтобы проверить срабатывания второго кормления.
Последний раз редактировалось kvg 15 янв 2025, 17:07, всего редактировалось 17 раз.
Аватара пользователя
kvg
Администратор
Сообщения: 1266
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:

kvg » 10 янв 2025, 17:15

Резерв
Аватара пользователя
kvg
Администратор
Сообщения: 1266
Зарегистрирован: 09 дек 2021, 22:09
Откуда: СПб
Блог: 28 постов
Контактная информация:

kvg » 10 янв 2025, 17:15

Резерв1