====== Жалюзи с электроприводом ====== {{ :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]]. * В нашей конструкции есть изъян: нельзя сделать кнопку, которая автоматически полностью закроет жалюзи. Если установить датчик полного закрытия/открытия, то такую функцию реализовать станет возможным. Для этого можно, например, снять одну полоску и установить вместо неё потенциометр.