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