Универсальный дистанционный пульт
с ESP8266.

Ретранслятор IR команд

Для универсального пульта, над проектом которым я работал, не хватало ретранслятора IR кодов. Сигналы WiFI пробивают стены, а вот с инфракрасными лучами сложнее - только прямая видимость или, в крайнем случае, отраженный сигнал. Поскольку такая задача может иметь и самостоятельное значение, то я буду описывать ее как отдельный, самостоятельный проект.

Что требуется:

Hardware/железо Software/программы Описание
ESP8266 12E
ведущий
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <pgmspace.h> //PROGMEM
IR приемник #include <IRremoteESP8266.h> GPIO2 (pin D4)
второй комплект
ESP8266 12E
ведомый
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <pgmspace.h> //PROGMEM
IR излучатель #include <IRremoteESP8266.h> GPIO15 (pin D8)

В программе ведущего модуля (ESP_reciver_trans.ino) измените подключение излучателя, например, также на GPIO15 (pin D8), как и у ведомого модуля. Эта инициализация у меня осталась от универсального пульта, где был дефицит со свободными ножками. Когда излучатель подключен к GPIO0, то это мешает запуску модуля после его прошивки - приходится все время переключаться.

Работа над проектом

Сам проект, по сути, простой и понятный, но я столкнулся с некоторыми проблемами, о решении которых и пойдет речь.

Первая и непонятная проблема связана с raw массивами. При повторном декодировании IR сигнала программа сбрасывалась и перезапускалась. Пришлось ввести промежуточную временную переменную temp_int_array[76]

Формирование IR массива
String temp_str = "";
unsigned int temp_int_array[76];

//подпрограмма формирования последовательности импульсов и передачи на излучатель
//cod_len - длина массива, вычисляется при декодировании IR сигнала
void send_ircod() { //max_len - запиши значение, чтобы не было ошибки компиляции
for (int i = 0; i <= cod_len; i++) {
   //формирование строки для своего излучателя, если есть.
   //irSignal[i] = irSignal[i] * USECPERTICK; // программа перезапускалась
   temp_int_array[i] = irSignal[i] * USECPERTICK;//в оригинале: USECPERTICK - MARK_EXCESS;
   //формирование строки для wifi
   temp_str += String(irSignal[i]);
   temp_str += ';';
}
//Serial.println( temp_str); //test
irsend.sendRaw(temp_int_array, cod_len, 38);
}

В конечном итоге получилось так

Формирование IR массива
String temp_str = "";
//подпрограмма формирования последовательности импульсов и передачи на излучатель void send_ircod() { //max_len - запиши значение, чтобы не было ошибки компиляции
unsigned int int_temp;
temp_str = "";
for (int i = 0; i <= cod_len; i++) {
  int_temp = irSignal[i];
   //без следующей проверки программа сбрасывается,
   //так как "- 45" исключает нулевые значения irSignal[i]
   if (int_temp != 0) {
     //irSignal[i] = int_temp * USECPERTICK - MARK_EXCESS; //оригинал, MARK_EXCESS=100;
    irSignal[i] = int_temp * USECPERTICK - 45;   //45 подогнал по осциллографу
    temp_str += String(irSignal[i]) + ';'; //строка для передачи по WiFi
   }
}
irsend.sendRaw(irSignal,cod_len, 38); //cod_khz = 38 - частота
//******************
send_irwifi();//**** передать строку temp_str
//******************
irrecv.enableIRIn(); // разрешить receiver
irrecv.resume(); // принять следующий код
}

В программе (смотри скетч) под комментариями есть предложения разработчиков библиотеки по коррекции значений массива, четных и нечетных импульсов. Для информации я их оставил, но сам не использовал.

Следующая проблема вскрылась при опробовании излучателя на основном модуле. Проблема связана, скорее всего, с IR библиотекой. Считывание raw массива необходимо производить с индекса = 1, а на излучатель библиотека считывает массив с нулевого индекса (в предыдущем примере for (int i = 0; i <= cod_len; i++) .

Далее, я обратил внимание, что посылая на декодер одну и ту же команду, импульсы на излучателе становились с каждым разом длиннее. Пришлось включить осциллограф и убедиться в этом. Оказалось, что, несмотря на фиксированный, заданный в программе, размер массива, запись в цикле увеличивает этот массив, как бы смещая указатель массива. Пришлось обойти эту проблему, предварительно обнуляя массив irSignal[i]

Получение кодов IR массива

if (irrecv.decode(&results)) { //принята IR команда
cod_len = results.rawlen;
//проверка параметров...
...
...
cod_hex = String(results.value, HEX); //запись кода кнопки = имя строки
cod_bits = results.bits; //запись кода кнопки = число бит
//cod_ir[0] = "!!!" + String(results.value,HEX)+":::"; //запись кода кнопки = имя строки
irrecv.resume(); // можно принимать следующий код - операция завершена
//####### retranslane #######
//обнуляем ВЕСЬ буфер - только так я добился однозначных результатов!!!
for (int i = 0; i < max_len; i++) {
   irSignal[i] = (char)0;
}
//формируем строку кода
for (int i = 1; i <= cod_len; i++) {
   irSignal[i-1] += (char)results.rawbuf[i];
}
//====================
send_ircod(); //===
//====================

WiFi - передача IR команд

Может я решил эту задачу не самым оптимальным способом, но на данном этапе такой способ мне понятнее - это передача строковой переменной. Для этого значения последовательности импульсов записаны в строку, разделенные точкой с запятой (;). Строка для передачи выглядит примерно так:

4450;4650;500;... ... ...1850;500;1850;500;0;

Добавив служебные метки, информацию о последовательности (длина кодовой последовательности, частота) пересылаем на IP адрес другого (ведомого) модуля:

&ip=192.168.0.166;result=&rate=38&code_len=68&ir=4450;4650;500;... ... ...1850;500;1850;500;0;

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

Получение кодов IR массива
//обнуление массива
for (int i = 0; i < 76; i++) temp_int_array[i] = (char)0;
//=================
int count_arr = 0;
ind1 = priem.indexOf(';'); //обязательно перед циклом
while (ind1 >= 0) {
  temp_str = priem.substring(0, ind1); //записали цифровое значение импульса)
  priem.remove(0, ind1 + 1); //удалили считанную часть
  temp_int = temp_str.toInt();
  temp_int_array[count_arr] = (unsigned int)temp_int;
  // (unsigned int)temp_str.toInt;
  count_arr += 1;
  ind1 = priem.indexOf(';');
}
//посылаем массив на излучатель паетом из трех посылок
for (int i = 0; i < 2; i++) {
irsend.sendRaw(temp_int_array, cod_len, cod_khz );

delay(47);//47-пауза между кодовыми посылками (samsung)
}

puls

При отладке IR канала я обнаружил, что наименьшее расхождение с несущей частотой (38 КГц) получается при cod_khz=43. На осциллограмме показана разница частот. Кроме того, у Sony импульсы не меандр. Это тоже можно регулировать, но уже в библиотеке: IRremoteESP8266.cpp

  
void IRsend::mark(int time) {
   long beginning = micros();
   while(micros() - beginning < time){
     digitalWrite(IRpin, HIGH);
     //delayMicroseconds(halfPeriodicTime); //original
     delayMicroseconds(halfPeriodicTime-4);
     digitalWrite(IRpin, LOW); //ruben???
     //delayMicroseconds(halfPeriodicTime); //original;
     delayMicroseconds(halfPeriodicTime+4);
   }
}
  

В основном модуле я жестко задал значение коэффициента несущей частоты cod_khz=43. Других частот, на других пультах, у меня не было.

puls2

Теперь о формировании самих кодовых импульсов. Здесь тоже пришлось подгонять. На рисунке, сверху вниз: реальный код с пульта SONY, подогнанный код с указанными коэффициентами и красным цветом полученный через библиотеку код без коррекции. Видно, что код без коррекции несколько растянут.

Формирование кодовых импульсов происходит в основном модуле. Есть три варианта, можете выбрать любой:


for (int i = 0; i < cod_len; i++) {
  
//без следующей проверки программа сбрасывается,
   //так как "- 45" исключает нулевые значения irSignal[i]
   if (irSignal[i] != 0) {
     int_temp = irSignal[i];
     if (i % 2) { //по модулю 2 - четные // Mark
      // irSignal[i] = int_temp * USECPERTICK + MARK_EXCESS; //вариант разработчиков библиотеки
      // irSignal[i] = int_temp * USECPERTICK + MARK_EXCESS/2; //максимально близкий к оригиналу
       irSignal[i] = int_temp * USECPERTICK; //оптимальный
    } else {  //нечетные, Space
      // irSignal[i] = int_temp * USECPERTICK - MARK_EXCESS; //вариант разработчиков библиотеки
      // irSignal[i] = int_temp * USECPERTICK - 150; //максимально близкий к оригиналу
      irSignal[i] = int_temp * USECPERTICK - 45; //оптимальный - подогнал по осциллографу
    }
  }
  temp_str += String(irSignal[i]) + ';';
}

Когда я начал практически использовать пульт, то оказалось, что одной посылки кода недостаточно, необходимо передавать пакет из двух-трех посылок. После того, как я это реализовал, все заработало отлично!

ir_pult

 

Пульт IR transponder

В программе пульту ретранслятора присвоен статический IP адрес 192.168.0.166. На пульте есть общий выключатель ретранслятора и четыре строки с IP адресами WiFi модулей с излучателями IR сигналов.

На каждой панели с IP адресом имеются две кнопки: слева - выбор для изменения в окне редакции, справа - разрешение на ретрансляцию.

Запись каждой панели можно изменить и передать в память модуля: переменные ex_TD[1] - ex_TD[4]. Также в памяти модуля хранятся состояния флажков панелей: переменные cb_TD[1] - cb_TD[4]. Состояние общего выключателя фиксируется в переменной cb_TD[0]. Далее, по желанию можно их запомнить, используя EEPROM или SD card. Главное, что все эти данные доступны и могут быть использованы, в зависимости от конфигурации оборудования.

Программа, получив от приемника IR команду, последовательно пытается соединиться с разрешенными модулями. Иногда на это требуется больше времени, чем хотелось бы, особенно если в числе разрешенных модулей есть отключенные от питания устройства. При выборе одной панели (одного модуля) скорость реакции максимальная и возможна передача двойных команд.

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

Не увлекайтесь установкой автоматического опроса, чтобы не увеличивать время реакции модуля на ваши действия.

В программе ведомого модуля (192.168.0.130), за основу взята прошивка WiFi розетки из проекта универсального модуля. Стоит набрать в браузере этот адрес, чтобы увидеть картинку. Управляющий сигнал подается на GPIO12. Следует знать, что сигнал инвертирован для твердотельного реле. Если по этой части будут вопросы, то ответы на них вы найдете на страничках универсально модуля.

И последнее, чтобы ретранслятор работал для модуля 192.168.0.130 без загрузки страницы в браузер, следует в программе установить: boolean cb_TD[5] = {1, 1, 0, 0, 0}; //[0] - это выключатель

Ретранслятор в универсальном пульте

В составе универсального пульта данные для ретранслятора могут иметь два различных источника:
- "живой сигнал", поступающий непосредственно от IR приемника и декодируемый библиотекой;
- сигнал, записанный на SD карту для какого-либо IR пульта.

В каждом из этих случаев сигнал в виде преобразованной raw последовательности может ретранслироваться на другие ESP модули только при разрешенных условиях, которые предварительно выставляются на пульте "transponder".

В режиме сканирования IR команд и записи их на SD карту, ретрансляция IR команд запрещена.

Кодировка

Желая сохранить установки пульта в памяти или на SD карте, я столкнулся с проблемой кодирования строковых переменных при передачи Get запросов от браузера после нажатия на кнопкку "ЗАПИСАТЬ".

Если передать " проба", то в ESP8266 получим вот такую последовательность "%20%D0%BF%D1%80%D0%BE%D0%B1%D0%B0" . Если попытаться ее записать обратно, то она так и будет выглядеть в искаженном виде. Немного подергавшись, в том числе и в Инете, я написал декодер под Win1251. Но это меня не устроило, так как требовалась кодировка utf-8. Чтобы текстовые файлы с SD карты правильно отображались в браузере, файлы должны быть в utf-8 кодировке. Пришлось еще немного повозиться написать функции String decode_(String mm) и int decode_int(char ch1, char ch2).

  

String decode_(String mm) {
  String result_win = "";
  String result = "";
  char inChar;
  mm.replace("%20", " "); //декодировать все пробелы
  for (int i = 0; i < mm.length(); i++) {
     inChar = mm[i];
     if (inChar == '%') {
       ind2 = decode_int(char(mm[i + 1]), char(mm[i + 2]));
       //Serial.print(":ind2:" );
       //Serial.println(ind2);
       if (ind2 == 208) { //вылавливаем русский код
         result += char(ind2); //utf-8 - первый байт
         ind2 = decode_int(char(mm[i + 4]), char(mm[i + 5])); //чтение второго байта после %
         result += char(ind2); //utf-8 -второй байт
         //ind2 = decode_int(char(mm[i + 4]), char(mm[i + 5])) + 48;
         ind2 += 48; //win-1251
         i += 3; //прошли еще три символа
       } else {
         if (ind2 == 209) { //вылавливаем русский код, начиная с р
           result += char(ind2); //utf-8 - первый байт
           ind2 = decode_int(char(mm[i + 4]), char(mm[i + 5])); //чтение второго байта после %
           result += char(ind2); //utf-8 -второй байт + 112
           ind2 += 112; //win-1251
           i += 3; //прошли еще три символа
         }
       }
       result_win += char(ind2); //преобразовали в Win1251
       i += 2; //прочли группу с первыми двумя символами
     } else { //все остальные, кроме русских, символы
       result += inChar;
       result_win += inChar; //преобразовали в Win1251
     }
  }
  return result; //строка в кодировке utf-8
  //result_win - строка в кодировке Win1251
}


int decode_int(char ch1, char ch2) {
//123%D0%B6%D0%96zZ == 123жЖzZ
//сюда попадает только пара символов после %    int result;
   if (ch1> 57) { //D from D0
     result = (ch1 - 55) * 16;
   } else {
     result = (ch1 - 48) * 16;
   }
   if (ch2> 57) {
     result = result + (ch2 - 55);
   } else { //0 from D0
     result = result + (ch2 - 48);
   }
   return result;
}

  

Все! проект закончен! Теперь только обкатка...

ESP8266 - universal remote control with a daily schedule /v3.3 - 27 июля 2016/   zip (zip).

Hosted by uCoz