====== Жалюзи с электроприводом ======
{{ :projects:irjalousie:overview.jpg?direct&700 |}}
  * Платформы: Arduino Uno
  * Языки программирования: Arduino (C++)
  * Тэги: дистанционное управление, IR, ИК-пульт, электропривод, сервомотор
===== Что это? =====
Автоматизация рутины — одна из самых популярных тем в мире DIY-электроники. На этот раз мы автоматизируем управление жалюзями при помощи Arduino и сервопривода постоянного вращения. В качестве канала связи используем инфракрасный свет, а в качестве пульта — первый попавшийся пульт от телевизора или музыкального центра.
Мы сделаем так, чтобы кнопки открывания и закрывания можно было назначать самостоятельно. Так мы сможем управлять устройством теми кнопками на пульте, которые обычно никак не используются.
===== Что нам понадобится? =====
{{ :projects:irjalousie:parts.jpg?direct&700 |}}
  - [[amp>product/arduino-uno?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | Arduino Uno]]
  - [[amp>product/arduino-troyka-shield?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | Troyka Shield]]
  - [[amp>product/breadboard-mini?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | Breadboard Mini]]
  - [[amp>product/troyka-buzzer?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | Зуммер (Troyka-модуль)]]
  - [[amp>product/servo-fs5103r?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | Привод постоянного вращения FS5103R]]
  - [[amp>product/ir-receiver?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | ИК-приёмник]]
  - [[amp>product/jumper-wires?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | Набор перемычек]]
  - [[amp>product/servo-bracket-pair?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | Соединительные скобы для сервопривода]]
  - [[amp>product/wall-plug-1a?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | Импульсный блок питания]]
  - Пластина крепления сервопривода
  - Какая-нибудь пластина для крепления Arduino. Мы использовали акриловую площадку из набора [[amp>product/matryoshka-z | «Матрёшка»]]
  - Пульт дистанционного управления
  - Жалюзи со стандартным скобяным креплением
  - Запасные скобы крепления жалюзи ×2 шт 
===== Как собрать? =====
  - Установите Arduino Uno на платформу. Мы использовали акриловую площадку из набора [[amp>product/matryoshka-z | «Матрёшка»]]. Так как нам нужна только часть по размерам платы, мы отпилили ненужную половину площадки. А для закрепления скобы мы просверлили отверстие в акриле.
  - Приклейте макетную плату с помощью её клейкого основания на Troyka Shield. {{ :projects:irjalousie:build.1.jpg?direct&700 |}}
  - Соберите крепление сервомотора и муфту. В нашем случае муфтой является втулка, с ввинченными в неё тонкими шурупами. {{ :projects:irjalousie:build.2.jpg?direct&700 |}}
  - Подключите привод постоянного вращения к цифровому пину ''3'', фотоприёмник к пину ''2'' и пьезопищалку — к пину ''4''. {{ :projects:irjalousie:build.3.jpg?direct&700 |}}
  - Установите Troyka Shield на Arduino Uno.
  - Установите сервопривод и Arduino Uno на жалюзи.
  - Прошейте в Arduino Uno скетч, приведённый ниже.
===== Алгоритм =====
Сразу после подачи питания вызывается функция ''setup''. В её теле
инициализируется ИК-библиотека. Затем подаётся сигнал старта программы (три
коротких сигнала) и запускается процедура «обучения» устройства. Обучение
заключается в запоминании устройством кнопок, которые пользователь использует
для открытия и закрытия жалюзей. Пользователь нажимает кнопку на пульте,
устройство получает код нажатой кнопки, запоминает его и подтверждает это
событие коротким сигналом. Затем всё повторяется для второй кнопки. После того,
как обе кнопки заданы, устройство издаёт длинный звуковой сигнал и переходит в
рабочий цикл (функция ''loop'').
В рабочем цикле устройство ожидает нажатия кнопки пульта. Как только получен
сигнал о нажатии, проверяется было ли нажатие этой же кнопки в недалёком
прошлом. Если да, то повторное нажатие воспринимается, как команда «продолжать
крутить мотор». Если недавно нажатий не было, запоминаем момент нажатия и
включаем привод в нужном направлении.
===== Исходный код =====
// Мы будем использовать библиотеки для работы с ИК и с сервоприводами
#include 
#include 
// Определим номера используемых пинов
#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);
    }
  }
}
==== Пример программы с сенсором освещенности ====
В данном примере жалюзи открываются и закрываются в зависимости от освещенности. [[amp>product/ir-receiver?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | ИК-приёмник]] нам не понадобиться, но необходимо добавить  [[amp>product/troyka-light-sensor?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | датчик освещённости (Troyka-модуль)]] и [[amp>product/troyka-potentiometer?utm_source=proj&utm_campaign=irjalousie&utm_medium=wiki | потенциометр (Troyka-модуль)]]. 
// подключаем библиотеку для работы с сервоприводами
#include 
// определим номера используемых пинов
// сенсор освещенности
#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;
  }
}
===== Демонстрация работы устройства =====
{{youtube>LVlx5uxnv5c?large}}
===== Что дальше? =====
  * Неудобно каждый раз при включении устройства прошивать в него коды кнопок пульта. Но у ATmega328p есть энергонезависимая память — [[wpru>EEPROM]], в которой можно сохранить один раз эти коды и загружать их от туда при каждом старте. Для работы с этой памятью есть [[http://arduino.cc/en/Reference/EEPROM | библиотека EEPROM]].
  * В нашей конструкции есть изъян: нельзя сделать кнопку, которая автоматически полностью закроет жалюзи. Если установить датчик полного закрытия/открытия, то такую функцию реализовать станет возможным. Для этого можно, например, снять одну полоску и установить вместо неё потенциометр.