Жалюзи с электроприводом

  • Платформы: Arduino Uno
  • Языки программирования: Arduino (C++)
  • Тэги: дистанционное управление, IR, ИК-пульт, электропривод, сервомотор

Что это?

Автоматизация рутины — одна из самых популярных тем в мире DIY-электроники. На этот раз мы автоматизируем управление жалюзями при помощи Arduino и сервопривода постоянного вращения. В качестве канала связи используем инфракрасный свет, а в качестве пульта — первый попавшийся пульт от телевизора или музыкального центра.

Мы сделаем так, чтобы кнопки открывания и закрывания можно было назначать самостоятельно. Так мы сможем управлять устройством теми кнопками на пульте, которые обычно никак не используются.

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

  1. Пластина крепления сервопривода
  2. Какая-нибудь пластина для крепления Arduino. Мы использовали акриловую площадку из набора «Матрёшка»
  3. Пульт дистанционного управления
  4. Жалюзи со стандартным скобяным креплением
  5. Запасные скобы крепления жалюзи ×2 шт

Как собрать?

  1. Установите Arduino Uno на платформу. Мы использовали акриловую площадку из набора «Матрёшка». Так как нам нужна только часть по размерам платы, мы отпилили ненужную половину площадки. А для закрепления скобы мы просверлили отверстие в акриле.
  2. Приклейте макетную плату с помощью её клейкого основания на Troyka Shield.
  3. Соберите крепление сервомотора и муфту. В нашем случае муфтой является втулка, с ввинченными в неё тонкими шурупами.
  4. Подключите привод постоянного вращения к цифровому пину 3, фотоприёмник к пину 2 и пьезопищалку — к пину 4.
  5. Установите Troyka Shield на Arduino Uno.
  6. Установите сервопривод и Arduino Uno на жалюзи.
  7. Прошейте в Arduino Uno скетч, приведённый ниже.

Алгоритм

Сразу после подачи питания вызывается функция setup. В её теле инициализируется ИК-библиотека. Затем подаётся сигнал старта программы (три коротких сигнала) и запускается процедура «обучения» устройства. Обучение заключается в запоминании устройством кнопок, которые пользователь использует для открытия и закрытия жалюзей. Пользователь нажимает кнопку на пульте, устройство получает код нажатой кнопки, запоминает его и подтверждает это событие коротким сигналом. Затем всё повторяется для второй кнопки. После того, как обе кнопки заданы, устройство издаёт длинный звуковой сигнал и переходит в рабочий цикл (функция loop).

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

Исходный код

irjalousie.ino
// Мы будем использовать библиотеки для работы с ИК и с сервоприводами
#include <IRremote.h>
#include <Servo.h>
 
 
// Определим номера используемых пинов
#define RECV_PIN   2
#define SERV_PIN   3
#define BUZZER_PIN 4
 
 
// Определим тип «действие»
enum Command
{
  CMD_NONE = 0,
  CMD_OPEN = 1,
  CMD_CLOSE = 2
};
 
// Создадим объект для пользования ИК-библиотекой
IRrecv irrecv(RECV_PIN);
decode_results results;
 
// Создадим переменные, которые будут хранить коды кнопок пульта
unsigned long codeOpen = 0;
unsigned long codeClose = 0;
 
// Определим переменные для хранения полученной команды и времени её получения
unsigned long cmdStartTime;
unsigned char cmd;
 
// Создадим объект для управления сервоприводом постоянного вращения
Servo srv;
 
 
// Из-за особенностей реализации нельзя использовать библиотеку IRremote
// вместе с функцией beep (они используют одно и тоже прерывание). Чтобы
// справиться с этой бедой мы, не используя прерываний, реализовали свою версию
// функции beep.
 
void beeep(int pin, int freq, unsigned long duration)
{
  unsigned long start;
 
  // Переводит пин в выход
  pinMode(pin, OUTPUT);
 
  // Запоминаем момент начала выполнения
  start = millis();
 
  // В течение duration миллисекунд пищим
  while (millis() - start < duration) {
    // Частота писка определяется паузами между изменениями состояния пина
    digitalWrite(pin, HIGH);
    delay(1000/freq/2);
    digitalWrite(pin, LOW);
    delay(1000/freq/2);
  }
 
  // На всякий случай переводит пин обратно в режим чтения
  pinMode(pin, INPUT_PULLUP);
}
 
 
// Функция "обучения" устройства (запоминания кодов кнопок открытия/закрытия)
 
void learn(void)
{
  // Ждём получения корректной команды
  // (условие окончания цикла находится в его теле)
  while (true) {
    if (irrecv.decode(&results)) {
      // Получили какой-то код. Сообщим библиотеке, что мы обработали событие.
      irrecv.resume();
 
      // Пропускаем коды короче 16 бит (наши пульты давали коды 16 или 32 бита,
      // остальные считаем ошибочными)
      if (results.bits >= 16) {
        // Сохраняем пришедший код как код на открытие
        codeOpen = results.value;
 
        // В знак подтверждения пикаем
        beeep(BUZZER_PIN, 500, 100);
 
        break;
      }
 
      // Ждём 100 мс, чтобы пропустить случайный приём пачки одинаковых кодов
      delay(100);
    }
  }
 
  // На всякий случай ждём 200 мс (пользователь явно быстрее не будет на кнопки
  // нажимать)
  delay(200);
 
  // Таким же методом принимаем второй код (на закрытие)
  while (true) {
    if (irrecv.decode(&results)) {
      irrecv.resume();
 
      if (results.bits >= 16 && results.value != codeOpen) {
        codeClose = results.value;
 
        beeep(BUZZER_PIN, 500, 100);
 
        break;
      }
 
      delay(100);
    }
  }
}
 
 
// Функция инициализации устройства
 
void setup(void)
{
  // Запускаем библиотеку IRRemote
  irrecv.enableIRIn();
 
  // Инициализируем команду (не было команды)
  cmd = CMD_NONE;
 
  // Сообщаем пользователю, что мы запустились тройным пиком
  beeep(BUZZER_PIN, 500, 100); delay(100);
  beeep(BUZZER_PIN, 500, 100); delay(100);
  beeep(BUZZER_PIN, 500, 100); delay(100);
 
  // Запускаем процедуру "обучения"
  learn();
 
  // Пищим в знак окончания процедуры обучения
  beeep(BUZZER_PIN, 2000, 1000);
}
 
 
// Пуск сервопривода постоянного вращения
void start(void)
{
  srv.attach(SERV_PIN);
 
  srv.write(cmd == CMD_OPEN ? 0 : 120);
}
 
// Остановка мотора
void stop(void)
{
  // Самый простой способ остановить серву постоянного вращения — отсоединиться
  // от неё
  srv.detach();
}
 
 
// Рабочий цикл программы
 
void loop(void)
{
  unsigned long codeValue;
  int codeLen;
 
  // Запускаем сканирование команды по ИК
  if (irrecv.decode(&results)) {
    // Сообщаем библиотеке, что приняли её информацию
    irrecv.resume();
 
    // Получаем код и длину кода
    codeValue = results.value;
    codeLen = results.bits;
 
    // Отсеиваем неправильные коды
    if (codeLen >= 16) {
      // Если пришёл код на открытие и на данный момент привод не крутится в
      // противоположную сторону, включаем открытие.
      if (codeValue == codeOpen && cmd != CMD_CLOSE) {
        cmd = CMD_OPEN;
        cmdStartTime = millis();
 
        start();
      } else if (codeValue == codeClose && cmd != CMD_OPEN) {
        // Если пришёл код на закрытие и на данный момент привод не крутится в
        // противоположную сторону, включаем открытие.
        cmd = CMD_CLOSE;
        cmdStartTime = millis();
 
        start();
      }
      // Если приходит команда на резкую смену направления вращения — спасаем
      // серву от смерти, игнорируя такую плохую команду.
    } else if (codeLen == 0 && (cmd == CMD_OPEN || cmd == CMD_CLOSE)) {
      // Некоторые пульты при удержании кнопки в нажатом состоянии посылают код
      // нулевой длины. Будем считать это повтором команды.
      cmdStartTime = millis();
    }
 
    delay(100);
  }
 
  // Здесь мы выключаем привод через 500 мс после последней полученной команды
  if (cmd == CMD_OPEN || cmd == CMD_CLOSE) {
    if (millis() - cmdStartTime > 500) {
      cmd = CMD_NONE;
      stop();
      // Дождёмся полной остановки привода
      delay(1000);
    }
  }
}

Пример программы с сенсором освещенности

В данном примере жалюзи открываются и закрываются в зависимости от освещенности. ИК-приёмник нам не понадобиться, но необходимо добавить датчик освещённости (Troyka-модуль) и потенциометр (Troyka-модуль).

light_jalousie.ino
// подключаем библиотеку для работы с сервоприводами
#include <Servo.h>
 
// определим номера используемых пинов
// сенсор освещенности
#define LIGHT_PIN   A0
// чувствительность к свету
#define POT_PIN     A5
// пин сервопривода
#define SERV_PIN    3
// пин пищалки
#define BUZZER_PIN  4
 
// определим тип «действие»
enum Command
{
  CMD_NONE = 0,
  CMD_OPEN = 1,
  CMD_CLOSE = 2
};
 
// в начале программы мы не знаем открыты жалюзи или нет
int cmd = CMD_NONE;
 
// создадим объект для управления сервоприводом постоянного вращения
Servo srv;
 
void setup()
{
  // сообщаем пользователю, что мы запустились тройным пиком
  tone(BUZZER_PIN, 500, 100);
  delay(200);
  tone(BUZZER_PIN, 500, 100);
  delay(200);
  tone(BUZZER_PIN, 500, 100);
  delay(200);
}
 
void loop()
{
  int light = analogRead(LIGHT_PIN);
  int lightSens = analogRead(POT_PIN);
  Serial.print(light);
  Serial.print("\t\t");
  Serial.print(lightSens);
  Serial.print("\t\t");
  Serial.println(cmd);
  if (light > lightSens && (cmd == CMD_OPEN || cmd == CMD_NONE)) {
    srv.attach(SERV_PIN);
    srv.write(120);
    // ждём указанное время открытия/закрытие жалюзей
    delay(2000);
    // самый простой способ остановить серву постоянного вращения
    // отсоединиться от неё
    srv.detach();
    cmd = CMD_CLOSE;
  } else if (light < lightSens && (cmd == CMD_CLOSE || cmd == CMD_NONE)) {
    srv.attach(SERV_PIN);
    // ждём указанное время открытия/закрытие жалюзей
    srv.write(30);
    delay(2000);
    // самый простой способ остановить серву постоянного вращения
    // отсоединиться от неё
    srv.detach();
    cmd = CMD_OPEN;
  }
}

Демонстрация работы устройства

Что дальше?

  • Неудобно каждый раз при включении устройства прошивать в него коды кнопок пульта. Но у ATmega328p есть энергонезависимая память — EEPROM, в которой можно сохранить один раз эти коды и загружать их от туда при каждом старте. Для работы с этой памятью есть библиотека EEPROM.
  • В нашей конструкции есть изъян: нельзя сделать кнопку, которая автоматически полностью закроет жалюзи. Если установить датчик полного закрытия/открытия, то такую функцию реализовать станет возможным. Для этого можно, например, снять одну полоску и установить вместо неё потенциометр.