Управление по ИК любым устройством с неизвестным протоколом от платы Arduino на примере взаимодействия с кондиционером.

Привет друзья. Файлы проекта вы можете скачать по этой ссылке.
Жаркое лето 2024 года мы с моим другом кошкой Рокси провели в Индии, в городке Вриндаван, в штате Уттар Прадеш. В том году нам «повезло» испытать аномальную индийскую жару. В Индии и без того очень жаркое лето. В Самые жаркие месяцы, Май и Июнь, температура может достигать +45 градусов в тени. При этом минимальная температура ночью не опускается ниже 30 градусов. Однако в том памятном году температура во Вриндаване достигала +49 градусов (в тени) и такая погода держалась неделями. В Индии это называется «Волна тепла» (Heat Wave).
В индийских СМИ писали что это было самое жаркое лето за последние 150 лет. Вряд ли кто — то знает наверняка что было в Индии 150 лет назад. Предполагаю, что это было самое жаркое время за все времена. Согласно официальным данным, на государственной метеостанции в столице Индии — Дели была зарегистрирована самая высокая в истории температура +52.4 градуса по Цельсию.
Конечно в моей маленькой индийской комнате во Вриндаване у меня есть кондиционер. Но в самое жаркое время кондиционер просто не справлялся. Кондиционеры вообще начинают плохо работать когда температура воздуха снаружи приближается к +50. Даже если днем я устанавливал на кондиционере температуру 29…30 градусов, он работал непрерывно. В таком режиме работы кондиционер может просто выйти из строя.
Поскольку в эти аномальные жаркие два месяца речь уже шла не о комфорте, а о выживании, нужно было периодически выключать кондиционер чтобы он не сгорел. Финансовый вопрос тоже не маловажная вещь. Электричество в Индии довольно дорогое, и постоянно работающий кондиционер вполне мог накрутить долларов 400 в месяц, а то и больше. Постоянно включать и выключать кондишку пультом ДУ было хлопотно, поэтому я решил разработать небольшое устройство на Arduino, которое бы делало это автоматически через заданные промежутки времени.

Мой индийский оконный кондиционер Voltas
на самом деле нет ничего сложного в том, чтобы управлять каким либо устройством по инфракрасному каналу. Это конечно если вы знаете протокол связи этого устройства с п пультом ДУ. Я не смог найти описание IR протокола для моего кондиционера индийского производства. Если возникает такая ситуация, когда вы не знаете протокол, но управлять устройством всё таки необходимо, можно использовать другой подход. При использовании этого способа можно управлять любым устройством, даже не зная его протокола связи с пультом.
Алгоритм действий в общем виде:
-Подключаем к плате Arduino модуль IR приёмника
-Загружаем в плату Arduino специальный временный скетч
-Включаем Arduino
-Направляем на IK приемник пульт ДУ от нашего кондиционера и нажимаем на нужные нам команды.
-Скетч Arduino выдает в терминал данные, которые он получает от пульта ДУ.
-Сохраняем эти данные где-то в текстовом файле. Позже мы будем вставлять их в наш рабочий скетч

Модуль IR — приемника для Arduino
Преимущества такого подхода том, что мы можем управлять практически любым устройством, без знания его протокола связи с пультом.
Недостатков тоже немало:
1. Нам необходим пульт от этого (или такого же) устройства. Не получится научить Arduino управлять кондиционером, если мы потеряли от него пульт. Придется найти такой же, или по крайней мере — того же производителя. Ну или одолжить у кого то на время.
2. Избыточность данных. Блок данных, который передает пульт в кондиционер имеет не такой уж и маленький размер, с учетом размера памяти микроконтроллера.
Дето в том ч то в каждой IR посылке пульт передает абсолютно ВСЕ параметры кондиционера. Передаваемый блок данных имеет фиксированный размер, который может отличаться от модели к модели. Например, если мы нажмем кнопку увеличения установленной температуры, то вместе с информацией о новой температуре передаются данные о режимах работы кондиционера, как то отопление, охлаждение, вентилятор, скорость вентилятора, настройки таймеров… Вообще все настройки, которые поддерживает данная модель. То же самое для команд включения и выключения кондиционера. Здесь кроется самая неприятная особенность данного метода.

По сути нам нужны только команды включения и выключения кондиционера. Но в блоке RAW данных, которые мы прочитаем с пульта в момент нажатия кнопки включения, пуль передаст также и данные о текущей температуре. Таким образом, при включении кондиционера от нашего устройства, он всегда будет включаться именно с той температурой, которая была установлена в момент записи сообщения с пульта.
Это происходит потому, что пульт ДУ не имеет обратной связи с кондиционером. Пульт ничего не знает о том например, какая температура установлена на кондиционере. Мы ведь можем установить эту температуру пальцами прямо на панели самого кондиционера. Просто каждый раз при нажатии на какую либо кнопку на пульте, передаются сразу все параметры кондиционера, даже если они и не менялись. Те параметры, которые хранятся именно в памяти пульта.
Получается что включение кондишки от нашего устройства, оно будет всегда сбрасывать температуру (и все остальные параметры) к тем, какие были в момент записи IR посылки. Для меня это не было большой проблемой, так как я всегда использовал одни и те же параметры кондиционера. Да и изменение настроек температуры не имеет значения при принудительном управлении циклами работы от внешнего устройства. Arduino просто включала кондиционер минут на 15 — 20 а портом выключала его на полчаса. Циклы работы я подобрал опытным путем для достижения приемлемой температуры в комнате. Нужно сказать что когда снаружи у вас +49, то приемлемая температура находится в районе 30..32 градуса. Это уже воспринимается как прохлада.
Ниже приведен скетч для считывания блока данных с пульта:
/* rawR&cv.ino Example sketch for IRLib2
* Illustrate how to capture raw timing values for an unknow protocol.
* You will capture a signal using this sketch. It will output data the
* serial monitor that you can cut and paste into the "rawSend.ino"
* sketch.
*/
// Recommend only use IRLibRecvPCI or IRLibRecvLoop for best results
#include <IRLibRecvPCI.h>
IRrecvPCI myReceiver(2);//pin number for the receiver
void setup() {
Serial.begin(9600);
delay(2000); while (!Serial); //delay for Leonardo
myReceiver.enableIRIn(); // Start the receiver
Serial.println(F("Ready to receive IR signals"));
myReceiver.setFrameTimeout(100000);
}
void loop() {
//Continue looping until you get a complete signal received
if (myReceiver.getResults()) {
Serial.println(F("Do a cut-and-paste of the following lines into the "));
Serial.println(F("designated location in rawSend.ino"));
Serial.print(F("\n#define RAW_DATA_LEN "));
Serial.println(recvGlobal.recvLength,DEC);
Serial.print(F("uint16_t rawData[RAW_DATA_LEN]={\n\t"));
for(bufIndex_t i=1;i<recvGlobal.recvLength;i++) {
Serial.print(recvGlobal.recvBuffer[i],DEC);
Serial.print(F(", "));
if( (i % 8)==0) Serial.print(F("\n\t"));
}
Serial.println(F("1000};"));//Add arbitrary trailing space
myReceiver.enableIRIn(); //Restart receiver
}
}
После загрузки в плату скетч будет ожидать блок данных с пульта ДУ и после успешного считывания выведет эти данные в COM-терминал.
Блок данных для пульта от моего кондиционера выглядит так:
uint16_t rawDataOn[RAW_DATA_LEN]={
894, 658, 918, 682, 890, 2666, 890, 2662,
890, 662, 918, 682, 890, 2662, 894, 2662,
890, 662, 914, 2662, 890, 686, 890, 686,
890, 2638, 942, 662, 890, 658, 942, 662,
890, 2638, 914, 686, 890, 686, 890, 686,
894, 686, 890, 2662, 890, 2662, 890, 2666,
890, 662, 914, 686, 890, 662, 914, 2662,
890, 2666, 890, 2662, 890, 686, 890, 686,
890, 690, 890, 686, 886, 2662, 894, 2662,
890, 2662, 890, 686, 890, 2662, 890, 2666,
890, 686, 890, 686, 890, 2666, 890, 2662,
890, 2662, 890, 686, 890, 2662, 890, 2662,
890, 690, 890, 686, 890, 2662, 890, 2666,
890, 2662, 890, 686, 894, 2662, 914, 2638,
890, 690, 886, 690, 886, 690, 890, 2662,
890, 686, 890, 686, 890, 686, 890, 2666,
890, 686, 890, 686, 890, 686, 890, 686,
890, 686, 890, 686, 890, 686, 890, 686,
890, 686, 890, 686, 890, 686, 890, 2662,
890, 2662, 890, 2662, 890, 2666, 886, 2662,
890, 1000};
Видим что это массив некоторых целых чисел. На самом деле скетч просто считывает факты получения импульсов от пульта. Он не знает что это за данные. Числа в массиве uint16_t rawDataOn[RAW_DATA_LEN] — это просто временные промежутки между импульсами измеренные в микросекундах. Такой блок данных называется RAW данные, то есть «сырые данные».

Схема подключения модуля ИК приёмника к плате Arduino Nano
После того как мы прочитаем и сохраним в текстовом файле все боки RAW данных нужных нам команд, можно содрать схему передатчика для управления кондиционером. Можно использовать эту же плату Arduino.
Схема устройства показана ниже:

Всего в прошивке у меня 6 вариантов сочетаний времени паузы и работы. Инфракрасный светодиод подключен к порту D3 платы через токоограничивающий резистор сопротивлением 100 Ом. Чем меньше сопротивление этого резистора, тем больше будет расстояние, с которого можно будет управлять кондиционером. При уменьшении сопротивления этого резистора нужно следить за тем, чтобы не превысить максимальный выходной ток порта Arduino. Если вы хотите увеличить дальность, лучше подключить ИК светодиод через дополнительный буферный ключ на транзисторе:

Далее привожу полный текст прошивки для Arduino Nano:
#include <IRLibSendBase.h>
#include <IRLib_HashRaw.h>
#include <EEPROM.h>
//LEDS PINS DEFINITIONS
#define LED_MIN 4 //R
#define LED_MID 5 //G
#define LED_MAX 6 //B
#define ON_LED 13 //Built In
//BUTTON PIN
#define BTN 7
//ONN / OFF TIMINGS
#define MIN_OFF_TIME 60 //60 min
#define MIN_ON_TIME 10 //10 min
#define MID_OFF_TIME 45 //45 min
#define MID_ON_TIME 15 //15 min
#define MAX_OFF_TIME 30 //30 min
#define MAX_ON_TIME 30 //30 min
#define E1_OFF_TIME 30 //30 min
#define E1_ON_TIME 10 //10 min
#define E2_OFF_TIME 20 //20 min
#define E2_ON_TIME 10 //10 min
#define E3_OFF_TIME 10 //10 min
#define E3_ON_TIME 10 //10 min
#define MAX_MODES 5 // maximum mode index
//JUST FOR FUN))
#define MS_PER_MINUTE 60000 //milliseconds in one minute
//SHORT DELAY FOR DEBUG and SIMULATION ONLY:
//#define MS_PER_MINUTE 100
//Адреса в EEPROM
#define EE_MODE 0
//AC ON command raw data
#define RAW_DATA_LEN 162
uint16_t rawDataOn[RAW_DATA_LEN]={
894, 658, 918, 682, 890, 2666, 890, 2662,
890, 662, 918, 682, 890, 2662, 894, 2662,
890, 662, 914, 2662, 890, 686, 890, 686,
890, 2638, 942, 662, 890, 658, 942, 662,
890, 2638, 914, 686, 890, 686, 890, 686,
894, 686, 890, 2662, 890, 2662, 890, 2666,
890, 662, 914, 686, 890, 662, 914, 2662,
890, 2666, 890, 2662, 890, 686, 890, 686,
890, 690, 890, 686, 886, 2662, 894, 2662,
890, 2662, 890, 686, 890, 2662, 890, 2666,
890, 686, 890, 686, 890, 2666, 890, 2662,
890, 2662, 890, 686, 890, 2662, 890, 2662,
890, 690, 890, 686, 890, 2662, 890, 2666,
890, 2662, 890, 686, 894, 2662, 914, 2638,
890, 690, 886, 690, 886, 690, 890, 2662,
890, 686, 890, 686, 890, 686, 890, 2666,
890, 686, 890, 686, 890, 686, 890, 686,
890, 686, 890, 686, 890, 686, 890, 686,
890, 686, 890, 686, 890, 686, 890, 2662,
890, 2662, 890, 2662, 890, 2666, 886, 2662,
890, 1000};
//AC OFF command raw data
uint16_t rawDataOff[RAW_DATA_LEN]={
910, 690, 890, 686, 890, 2662, 890, 2666,
890, 686, 890, 686, 890, 2666, 890, 2662,
890, 686, 890, 2666, 890, 686, 890, 666,
910, 2666, 890, 686, 890, 686, 890, 686,
890, 686, 890, 686, 890, 690, 890, 686,
890, 686, 890, 2662, 890, 2662, 894, 2662,
890, 686, 890, 686, 890, 686, 894, 2662,
890, 2662, 890, 2666, 886, 690, 890, 686,
914, 662, 890, 686, 890, 2666, 890, 2662,
890, 2666, 886, 690, 886, 2666, 914, 2638,
890, 686, 890, 690, 886, 2666, 890, 2662,
890, 2662, 890, 690, 890, 2662, 890, 2662,
890, 690, 886, 690, 890, 2662, 890, 2666,
890, 2662, 890, 686, 890, 2666, 886, 2666,
890, 686, 890, 686, 890, 690, 886, 2666,
890, 686, 890, 686, 890, 690, 886, 2666,
890, 686, 890, 686, 890, 686, 890, 690,
886, 690, 886, 690, 890, 686, 890, 686,
914, 2638, 894, 686, 886, 690, 886, 2666,
886, 2666, 890, 2662, 890, 2662, 890, 2666,
890, 1000};
IRsendRaw IRSender;
byte mode; //AC Timing Mode Code. 0 = MIN, 1 = MID, 2 = MAX, 3 = Extra1, 4 = Extra2, 5 = Extra3
unsigned long on_time; //AC Work Time
unsigned long off_time; //AC Idle Time
bool flag = false; //флаг для отслеживания однократного нажатия кнопки
uint32_t btnTimer = 0; //счетчик для подавления дребезга контактов
//----------------------------------------------
void ac_on(){
//Standard frequency is 38 kHz and others are 36, 38, 40, 42, 48 and 56 kHz.
IRSender.send(rawDataOn,RAW_DATA_LEN,36);//buffer,buffer length, frequency
digitalWrite(ON_LED, HIGH);
Serial.println("AC Switched ON");
Serial.println(on_time);
}
//----------------------------------------------
void ac_off(){
//Standard frequency is 38 kHz and others are 36, 38, 40, 42, 48 and 56 kHz.
IRSender.send(rawDataOff,RAW_DATA_LEN,36);//buffer,buffer length, frequency
digitalWrite(ON_LED, LOW);
Serial.println("AC Switched OFF");
Serial.println(off_time);
}
//----------------------------------------------
void set_leds(){
switch (mode) {
case 0:
digitalWrite(LED_MIN, HIGH);
digitalWrite(LED_MID, LOW);
digitalWrite(LED_MAX, LOW);
on_time = MIN_ON_TIME * MS_PER_MINUTE ;
off_time = MIN_OFF_TIME * MS_PER_MINUTE;
Serial.println("Set Mode MINIMUM AC");
Serial.println("10 min ON - 60 min OFF ");
break;
case 1:
digitalWrite(LED_MIN, LOW);
digitalWrite(LED_MID, HIGH);
digitalWrite(LED_MAX, LOW);
on_time = MID_ON_TIME * MS_PER_MINUTE;
off_time = MID_OFF_TIME * MS_PER_MINUTE;
Serial.println("Set Mode MID AC");
Serial.println("15 min ON - 45 min OFF");
break;
case 2:
digitalWrite(LED_MIN, LOW);
digitalWrite(LED_MID, LOW);
digitalWrite(LED_MAX, HIGH);
on_time = MAX_ON_TIME * MS_PER_MINUTE;
off_time = MAX_OFF_TIME * MS_PER_MINUTE;
Serial.println("Set Mode MAX AC");
Serial.println("30 min ON - 30 min OFF");
break;
case 3:
digitalWrite(LED_MIN, HIGH);
digitalWrite(LED_MID, HIGH);
digitalWrite(LED_MAX, LOW);
on_time = E1_ON_TIME * MS_PER_MINUTE;
off_time = E1_OFF_TIME * MS_PER_MINUTE;
Serial.println("Set Mode E1");
Serial.println("10 min ON - 30 min OFF");
break;
case 4:
digitalWrite(LED_MIN, HIGH);
digitalWrite(LED_MID, LOW);
digitalWrite(LED_MAX, HIGH);
on_time = E2_ON_TIME * MS_PER_MINUTE;
off_time = E2_OFF_TIME * MS_PER_MINUTE;
Serial.println("Set Mode E2");
Serial.println("10 min ON - 20 min OFF");
break;
case 5:
digitalWrite(LED_MIN, LOW);
digitalWrite(LED_MID, HIGH);
digitalWrite(LED_MAX, HIGH);
on_time = E3_ON_TIME * MS_PER_MINUTE;
off_time = E3_OFF_TIME * MS_PER_MINUTE;
Serial.println("Set Mode E3");
Serial.println("10 min ON - 10 min OFF");
break;
}
}
//----------------------------------------------
//Цикл выбора таймнга работы кондиционера
void mode_setup(){
//Мигаем всеми светодиодами
for (int i = 0; i<5; i++){
digitalWrite(LED_MIN, HIGH);
digitalWrite(LED_MID, HIGH);
digitalWrite(LED_MAX, HIGH);
delay(100);
digitalWrite(LED_MIN, LOW);
digitalWrite(LED_MID, LOW);
digitalWrite(LED_MAX, LOW);
delay(100);
}
//Проверяем ячейку EEPROM. Если в ней что то больше чем MAX_MODES то записываем туда 0 (режим MIN)
if (EEPROM.read(EE_MODE) > MAX_MODES) {
EEPROM.update(EE_MODE, 0);
Serial.println("EEPROM WAS AUTO UPDATED to 0 (MIN MODE)");
}
//Считываем из EEPROM текущий режим
mode = EEPROM.read(EE_MODE);
set_leds();//включаем нужный светодиод
while(true){
bool btnState = !digitalRead(BTN);
if (btnState && !flag && millis() - btnTimer > 100) {
flag = true;
btnTimer = millis();
mode++;
if (mode > MAX_MODES) {
mode = 0;
}
set_leds();//включаем нужный светодиод
EEPROM.update(EE_MODE, mode); //сохраняем режим в EEPROM
}
if (!btnState && flag && millis() - btnTimer > 100) {
flag = false;
btnTimer = millis();
}
}
}
//----------------------------------------------
void setup() {
Serial.begin(9600);
pinMode(BTN, INPUT_PULLUP);
pinMode(LED_MIN, OUTPUT);
pinMode(LED_MID, OUTPUT);
pinMode(LED_MAX, OUTPUT);
pinMode(ON_LED, OUTPUT);
Serial.println("AC Control Arduino");
Serial.println("performing 2s delay");
delay(2000);
//Если при включении или сбросе нажата кнопка, то переходим в режим установки режима
int buttonState = digitalRead(BTN);
if (!buttonState) {
mode_setup();
}
//Проверяем ячейку EEPROM c режимом работы. Если в ней что то больше чем 2 то записываем туда 0 (режим MIN)
if (EEPROM.read(EE_MODE) > MAX_MODES) {
EEPROM.update(EE_MODE, 0);
Serial.println("EEPROM Mode has been initialized (MIN MODE)");
}
mode = EEPROM.read(EE_MODE); //Считываем рабочий режим
Serial.println("ENTERING WORK MODE");
set_leds();//включаем нужный светодиод и устанавливаем задержки
Serial.println("performing 10s delay");
delay(10000);
}
//----------------------------------------------
void loop() {
ac_on();
delay(on_time);
ac_off();
delay(off_time);
}
//----------------------------------------------