Зубочистка-детектив раскрывает секрет радиопротокола

  • Tutorial
Это небольшая зарисовка к сюжету об "Удобном доме". Просто иллюстрация того, что даже с не слишком большими знаниями и опытом можно кое-чего добиться. Иными словами, достаточно настойчивый дятел задолбит любое дерево.

Началось все с простого желания управлять светом в доме с помощью Arduino. В том числе — выключателями Livolo, купленными еще до этой безумной затеи с домашней автоматикой. Но, в отличие от радиорозеток, «щелкать» ими с помощью моей любимой библиотеки RC-Switch не получилось, а поиск других готовых решений показал их полное отсутствие.

Да и китайцы производители-продавцы на вопрос о протоколе отвечали, что эта штука работает на частоте 433 МГц. Не слишком полезная информация. Впрочем, не буду изображать святую невинность. Я ведь вместе с Arduino купил и пару блоков по четыре реле, чтобы, если что, банально замыкать избранные кнопки пультов. И это, кстати, довольно популярное решение, потому что быстро, относительно дешево и очень сердито.

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


Былинные провалы

Сначала, впрочем, о зубочистках я не думал. Зато в процессе чтения всякого более-менее простого про расшифровку радиопротоколов наткнулся на замечательный ресурс NetHome. А там автор публикует, во-первых, схему делителя, который позволяет записать демодулированный сигнал с приемника на компьютер через обычный микрофонный вход и заодно — простую утилиту Protocol Analyzer для записи и анализа сигнала.

. виновники торжества
image
image

Так что я собрал делитель, подключил его к ноутбуку, нажал на кнопку пульта и стал разглядывать результаты — по счастью, (амплитудная) модуляция пульта совпала с модуляцией приемника. Protocol Analyzer — вообще довольно классная вещь. Программа идентифицирует наиболее популярные протоколы, а если сталкивается с неизвестным — можно посмотреть «осциллограмму» с раскладкой по импульсам. К сожалению, протокол Livolo она не знала. И даже немного запутала меня, так как не совсем очевидно показала истинную форму сигнала пульта Livolo.

Выяснилось это случайно, когда мне пришло в голову посмотреть сигнал еще и в Audacity. Здесь стали четко видны импульсы и, как мне кажется, очевидна причина неприятностей Protocol Analyzer: крайне небольшая длительность этих самых импульсов — от 100 до 500 микросекунд. В этом же редакторе я решил пойти простым путем — записать полученный сигнал в WAV, а потом воспроизвести его при помощи Arduino на пин, к которому подключен передатчик. Ведь у меня же был Ethernet-шилд со слотом microSD, который вполне подходил для «плеера». Немного поисков — нашлась и «музыкальная» библиотека TMRpcm.

. вот что показал Protocol Analyzer


. сравните с Audacity


Идея базировалась на том факте, что аналогичные по своей сути ИК-протоколы удачно и довольно просто имитируются путем подключения ИК-светодиода к аудиовыходу компьютера или другого гаджета, способного на воспроизведение звука с подходящими параметрами. Сказано — сделано. И отложено в сторону: выключатели шутку не поняли, а просмотр излученного таким образом сигнала в Audacity показал, что форма импульсов слишком сильно искажена.

Повторение формы

Тогда я решился на крайние меры. А именно — тупо повторить форму сигнала, не вдаваясь в логический уровень. Для этого необходимо и достаточно жестко закодировать кодовые посылки в скетче Arduino. И здесь мне крупно повезло.

Если рассмотреть кодовую посылку пульта Livolo, то можно заметить, что она состоит из множества (около 100) многократно повторяемых пакетов импульсов. Так вот, все пакеты в кодовой посылке совершенно одинаковы — это своеобразная защита от помех: избыточное количество пакетов гарантирует, во-первых, надежный захват сигнала АРУ приемника, и, во-вторых, — прием самой команды.

. вот такая картинка, если нажимать кнопки подряд


. понять, где сигнал, а где шум довольно просто. Здесь же можете оценить масштаб бедствия: сигнал — это всего одна кнопка
image

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

. повторенье — не только мать ученья, но и залог уверенного приема сигнала


Это я и называю везением: фиксированный код безо всяких выкрутасов.

Таким образом, требовалось последовательно нажать и записать сигнал всех нужных кнопок пульта в Audacity, а потом — посчитать количество импульсов в пакете каждой кнопки, узнать их длительность и перенести все это в код Arduino. Для этого потребовался инструмент, достаточно тонкий, чтобы не закрывать обзор сигнала в Audacity и достаточно нейтральный, чтобы не поцарапать в процессе дисплей ноутбука.

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

При достаточном увеличении видно, что пакет состоит из пяти разновидностей импульсов (условно: длинный вниз, короткий вверх, короткий вниз, средний вверх, средний вниз).

.
image

Если увеличить еще больше, то можно на глаз прикинуть и длину импульсов по линейке Audacity, что я и сделал для всех пяти. Кроме того, каждому импульсу присвоил порядковый номер — это в расчете на использования переменных типа byte, чтобы сэкономить память Arduino. Это я только сейчас подумал, что можно было бы поделить на 10 и не мучиться с «аббревиатурами».

. синим и красным выделены теоретические границы импульсов, поскольку в идеале фронты должны быть вертикальными, но это если без радиоканала
image

Работа оказалась не столько интеллектуальная, сколько муторная. Количество отдельных импульсов «плавало» от кнопки к кнопке. И хотя я предполагал, что с разумной точки зрения так быть не должно, до анализа логического уровня не дошел. Просто закодировал полученный результат и попробовал его в работе.

С первого раза ничего не получилось. Впрочем, это было ожидаемо. Чего я не ожидал, так это того, что все заработает со второго раза. А дело оказалось в том, что при прямом кодировании (т.е. если импульс вверх — кодируем OUTPUT/HIGH) сигнал получился перевернутым — очевидно, такая особенность передатчика. Решить это было проще простого: инвертируем уровни в коде (т.е. импульс вверх кодируем OUTPUT/LOW). Сравнение имитации и оригинального сигнала (в Audactiy, на глаз) также показало небольшое расхождение в длине импульсов — это я тоже поправил.

Первая версия, великая и ужасная
int txPin = 9; // pin connected to RF transmitter
int i; // counter to send command pulses
int pulse; // count pulse repetitions
int incomingByte = 0;   // for incoming serial data

// hard coded commands (see txButton): 1 - pulse start, 2 - zero, 3 - one, 4 - pause, 5 - low
int button1[45]={44, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2};
int button2[43]={43, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2};
int button3[41]={40, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 5, 3, 4, 2, 4, 2, 4, 2};
int button4[43]={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 4, 2, 4, 2};
int button5[43]={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2};
int button6[43]={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 4, 2, 4, 2, 4, 2};
int button7[41]={40, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 5, 3, 4, 2, 4, 2};
int button8[43]={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 4, 2};
int button9[43]={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2};
int button10[43]={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 2, 4, 3, 4, 2, 4, 2, 4, 2};
int button11[41]={40, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 5, 2, 4, 3, 4, 2};

void setup () {

pinMode(txPin, OUTPUT);
     Serial.begin(9600);
     Serial.println("Number = button;  a to press 0;  b to shut off all");

}

    void loop(){
      if (Serial.available() > 0) {
        // read the incoming byte:
        incomingByte = Serial.read();
        switch(incomingByte) {
        case 49:
        txButton(button1);
        Serial.println("Switching on 1");
        break;
        case 50:
        txButton(button2);
        Serial.println("Switching on 2");
        break;
        case 51:
        txButton(button3);
        Serial.println("Switching on 3");
        break;
        case 52:
        txButton(button4);
        Serial.println("Switching on 4");
        break;
        case 53:
        txButton(button5);
        Serial.println("Switching on 5");
        break;
        case 54:
        txButton(button6);
        Serial.println("Switching on 6");
        break;
        case 55:
        txButton(button7);
        Serial.println("Switching on 7");
        break;
        case 56:
        txButton(button8);
        Serial.println("Switching on 8");
        break;
        case 57:
        txButton(button9);
        Serial.println("Switching on 9");
        break;
        case 97:
        txButton(button10);
        Serial.println("Switching on 0");
        break;
        case 98:
        txButton(button11);
        Serial.println("Switching All off");
        break;
        }
      } // end if serial available
    }// end void loop
    
// transmit command. Due to transmitter (or something, I don't know) transmission code should be INVERTED. Ex: one is coded as LOW-delay->HIGH instead of HIGH-delay-LOW
void txButton(int cmd[]) {
Serial.print("Processing. Array size is ");
Serial.println(cmd[0]);
digitalWrite(txPin, HIGH); // not sure if its required, just an attempt to start transmission to enable AGC of the receiver
delay(1000);

for (pulse= 0; pulse <= 100; pulse=pulse+1) { // repeat command 100 times
for (i = 1; i < cmd[0]+1; i = i + 1) { // transmit command

  switch(cmd[i]) {
   case 1: // start
   digitalWrite(txPin, HIGH);
   delayMicroseconds(550);
   digitalWrite(txPin, LOW);
//   Serial.print("s");
   break;
   case 2: // "zero", that is short high spike
   digitalWrite(txPin, LOW);
   delayMicroseconds(110);
   digitalWrite(txPin, HIGH);
//   Serial.print("0");
   break;   
   case 3: // "one", that is long high spike
   digitalWrite(txPin, LOW);
   delayMicroseconds(303);
   digitalWrite(txPin, HIGH);
//   Serial.print("1");
   break;      
   case 4: // pause, that is short low spike
   digitalWrite(txPin, HIGH);
   delayMicroseconds(110);
   digitalWrite(txPin, LOW);
//   Serial.print("p");
   break;      
   case 5: // low, that is long low spike
   digitalWrite(txPin, HIGH);
   delayMicroseconds(290);
   digitalWrite(txPin, LOW);
//   Serial.print("l");   
   break;      
  }
    
  }

} 


}




Завершающим штрихом этого этапа стало занесение кодовых последовательностей во флеш-память Arduino (PROGMEM), чтобы не занимать драгоценную оперативную. В таком виде код проездил, по-моему, более полугода, а потом мне стало скучно, да и вообще снова захотелось чего-то прекрасного.

Вторая версия с PROGMEM
#include <avr/pgmspace.h> // needed to use PROGMEM

#define  txPin  8 // pin connected to RF transmitter (pin 8)
byte i; // command pulses counter for Livolo (0 - 100)
byte pulse; // counter for command repeat

// commands stored in PROGMEM arrays (see on PROGMEM use here: http://arduino.cc/forum/index.php?topic=53240.0)
// first array element is length of command
const prog_uchar button1[45] PROGMEM ={44, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2};
const prog_uchar button2[43] PROGMEM ={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2};
const prog_uchar button3[41] PROGMEM ={40, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 5, 3, 4, 2, 4, 2, 4, 2};
const prog_uchar button4[43] PROGMEM ={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 4, 2, 4, 2};
const prog_uchar button5[43] PROGMEM ={42, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2};
const prog_uchar button7[41] PROGMEM ={40, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 5, 3, 4, 2, 4, 2};
const prog_uchar button11[41] PROGMEM ={40, 1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 5, 3, 4, 2, 5, 2, 4, 3, 4, 2};

// pointers to command arrays
PROGMEM const prog_uchar *buttonPointer[] = {button1, button2, button3, button4, button5, button7, button11};

void setup() {

// sipmle example: send button "button2" once. Note that array elements numbered starting from "0" (so button1 is 0, button2 is 1 and so on)

txButton(1);

}

void loop() {
}

// transmitting part
// zeroes and ones here are not actual 0 and 1. I just called these pulses for my own convenience. 
// also note that I had to invert pulses to get everything working
// that said in actual command "start pulse" is long low; "zero" = short high; "one" = long high; "pause" is short low; "low" is long low.

void txButton(byte cmd) { 
prog_uchar *currentPointer = (prog_uchar *)pgm_read_word(&buttonPointer[cmd]); // current pointer to command array passed as txButton(cmd) argument
byte cmdCounter = pgm_read_byte(¤tPointer[0]); // read array length

for (pulse= 0; pulse <= 180; pulse = pulse+1) { // how many times to transmit a command
for (i = 1; i < cmdCounter+1; i = i + 1) { // counter for reading command array
  byte currentCmd = pgm_read_byte(¤tPointer[i]); // readpulse type from array
  switch(currentCmd) { // transmit pulse
   case 1: // start pulse
   digitalWrite(txPin, HIGH);
   delayMicroseconds(550);
   digitalWrite(txPin, LOW);
   break;
   case 2: // "zero"
   digitalWrite(txPin, LOW);
   delayMicroseconds(110);
   digitalWrite(txPin, HIGH);
   break;   
   case 3: // "one"
   digitalWrite(txPin, LOW);
   delayMicroseconds(303);
   digitalWrite(txPin, HIGH);
   break;      
   case 4: // "pause"
   digitalWrite(txPin, HIGH);
   delayMicroseconds(110);
   digitalWrite(txPin, LOW);
   break;      
   case 5: // "low"
   digitalWrite(txPin, HIGH);
   delayMicroseconds(290);
   digitalWrite(txPin, LOW);
   break;      
  } 
  }
 } 
 digitalWrite(txPin, LOW);
}



Выделение общего

В очередной раз просматривая WAV я ни на что особенно не надеялся. Однако более внимательное, чем раньше, сравнение кодовых посылок преподнесло приятный сюрприз. Часть последовательности в начале каждого пакета импульсов оказалась общей и для всех пакетов одной кодовой посылки, и для всех кнопок пульта вообще.

. нарезка разных кнопок — и сразу видно, что часть пакета не изменяется


До понимания протокола было еще далеко, но это небольшое открытие позволило дополнительно сэкономить память контроллера. Единую часть посылки я просто вынес в отдельный массив, который автоматически «воспроизводился» перед каждой уникальной частью.

Общая часть теперь поселилась в отдельном массиве
#include <avr/pgmspace.h> // needed to use PROGMEM

#define  txPin  8 // pin connected to RF transmitter (pin 8)
byte i; // command pulses counter for Livolo (0 - 100)
byte pulse; // counter for command repeat

// commands stored in PROGMEM arrays (see on PROGMEM use here: http://arduino.cc/forum/index.php?topic=53240.0)
// first array element is length of command
const prog_uchar start[30] PROGMEM = {1, 2, 4, 2, 4, 2, 4, 3, 5, 2, 4, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2}; // remote ID - no need to store it with each command
const prog_uchar button1[15] PROGMEM ={14, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2}; // only command bits
const prog_uchar button2[13] PROGMEM ={12, 5, 3, 4, 2, 4, 2, 4, 2, 4, 2, 4, 2};
const prog_uchar button3[11] PROGMEM ={10, 5, 3, 5, 3, 4, 2, 4, 2, 4, 2};
const prog_uchar button4[13] PROGMEM ={12, 4, 2, 4, 2, 5, 3, 4, 2, 4, 2, 4, 2};
const prog_uchar button5[13] PROGMEM ={12, 5, 2, 4, 3, 4, 2, 4, 2, 4, 2, 4, 2};
const prog_uchar button7[11] PROGMEM ={10, 5, 3, 4, 2, 5, 3, 4, 2, 4, 2};
const prog_uchar button11[11] PROGMEM ={10, 5, 3, 4, 2, 5, 2, 4, 3, 4, 2};

// pointers to command arrays
PROGMEM const prog_uchar *buttonPointer[] = {start, button1, button2, button3, button4, button5, button7, button11};

void setup() {

// sipmle example: send button "button2" once. Note that array elements numbered starting from "0" (so button1 is 0, button2 is 1 and so on)
// Serial.begin(9600);


}

void loop() {

txButton(3);
delay(1000);
}

// transmitting part
// zeroes and ones here are not actual 0 and 1. I just called these pulses for my own convenience. 
// also note that I had to invert pulses to get everything working
// that said in actual command "start pulse" is long low; "zero" = short high; "one" = long high; "pause" is short low; "low" is long low.

void txButton(byte cmd) { 
prog_uchar *currentPointer = (prog_uchar *)pgm_read_word(&buttonPointer[cmd]); // current pointer to command array passed as txButton(cmd) argument
byte cmdCounter = pgm_read_byte(¤tPointer[0]); // read array length

prog_uchar *currentPointerStart = (prog_uchar *)pgm_read_word(&buttonPointer[0]); // current pointer to start command array


for (pulse= 0; pulse <= 180; pulse = pulse+1) { // how many times to transmit a command
for (i = 0; i<30; i=i+1) {

byte currentCmd = pgm_read_byte(¤tPointerStart[i]);
sendPulse(currentCmd);
// Serial.print(currentCmd);
// Serial.print(", ");
}


for (i = 1; i < cmdCounter+1; i = i + 1) { // counter for reading command array
  byte currentCmd = pgm_read_byte(¤tPointer[i]); // readpulse type from array

  sendPulse(currentCmd);
//  Serial.print(currentCmd);
// Serial.print(", ");
    }
  }
}

void sendPulse(byte txPulse) {

  switch(txPulse) { // transmit pulse
   case 1: // start pulse
   digitalWrite(txPin, HIGH);
   delayMicroseconds(550);
   digitalWrite(txPin, LOW);
   break;
   case 2: // "zero"
   digitalWrite(txPin, LOW);
   delayMicroseconds(110);
   digitalWrite(txPin, HIGH);
   break;   
   case 3: // "one"
   digitalWrite(txPin, LOW);
   delayMicroseconds(303);
   digitalWrite(txPin, HIGH);
   break;      
   case 4: // "pause"
   digitalWrite(txPin, HIGH);
   delayMicroseconds(110);
   digitalWrite(txPin, LOW);
   break;      
   case 5: // "low"
   digitalWrite(txPin, HIGH);
   delayMicroseconds(290);
   digitalWrite(txPin, LOW);
   break;      
  } 
 digitalWrite(txPin, LOW);
}




Несмотря на некоторый успех и то, что код вполне себе работал и хлеба не просил, меня продолжал смутно беспокоить от факт, что количество импульсов в посылках было разным. С одной стороны, протоколу ничего не мешало иметь такую особенность, с другой же — под руками было множество примеров похожих протоколов (от розеток, например, и метеостанций), где прослеживалась тенденция к 24-битной посылке.

Поиск закономерностей

Третий подход к станку состоял в том, что я попытался разгадать логику разработчика протокола. С самого начала мне было комфортнее считать, что короткие импульсы обозначают «0», а средней длительности — «1». При этом самый длинный импульс в пакете я принял за стартовый, и другим смыслом его не нагружал.

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

1. Существует четкое правило следования импульсов: за импульсом «вверх» всегда идет импульс «вниз», вне зависимости от длительности импульса.
2. Два коротких импульса подряд в моей системе координат означают «0».
3. Аналогично, каждый импульс средней длительности означает «1».
4. Самый длинный импульс посылки — старт или стоп, что не играет роли и зависит только от точки взгляда.

Если применить эти правила к пакету импульсов, то становится видно, что его общая длина всегда составляет 24 бита, включая «старт-стоп». Из них 16 бит — обнаруженная ранее «фиксированная» часть и 7 бит — часть уникальная для каждой цифровой кнопки пульта. Собственно, постоянная длина пакета привела меня к заключению, что опознание логического уровня прошло успешно.

. по всем правилам


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

Все вместе означает, что есть отличная возможность имитировать практически неограниченное количество пультов Livolo в зависимости от собственных фантазий и потребностей. Главное — соблюдать правило: 16 бит — идентификатор пульта, и пользоваться либо известными кнопками, либо генерировать их по принципу 7 бит — «кнопка».

Практика, однако, показала, что подходят не все 16-битные идентификаторы пультов. Но это не слишком страшно: по той же практике, найти подходящий идентификатор не представляет особого труда.

Осталось только переписать код, и, наконец, избавиться от этих ужасных неуклюжих массивов.

И вот результат
#define  txPin  8 // pin connected to RF transmitter (pin 8)
byte i; // just a counter
byte pulse; // counter for command repeat
boolean high = true; // pulse "sign"

// keycodes #1: 0, #2: 96, #3: 120, #4: 24, #5: 80, #6: 48, #7: 108, #8: 12, #9: 72; #10: 40, #OFF: 106
// real remote IDs: 6400; 19303
// tested "virtual" remote ID: 8500, other IDs could work too, as long as they do not exceed 16 bit
// known issue: not all 16 bit remote ID are valid
// have not tested other buttons, but as there is dimmer control, some keycodes could be strictly system
// use: sendButton(remoteID, keycode); 
// see void loop for an example of use

void setup() {


}

void loop() {

sendButton(6400, 120); // blink button #3 every 3 seconds using remote with remoteID #6400
delay(3000);

}

void sendButton(unsigned int remoteID, byte keycode) {

  for (pulse= 0; pulse <= 180; pulse = pulse+1) { // how many times to transmit a command
  sendPulse(1); // Start  
  high = true; // first pulse is always high

  for (i = 16; i>0; i--) { // transmit remoteID
    byte txPulse=bitRead(remoteID, i-1); // read bits from remote ID
    selectPulse(txPulse);    
    }

  for (i = 7; i>0; i--) { // transmit keycode
    byte txPulse=bitRead(keycode, i-1); // read bits from keycode
    selectPulse(txPulse);    
    }    
  }
   digitalWrite(txPin, LOW);
}

// build transmit sequence so that every high pulse is followed by low and vice versa

void selectPulse(byte inBit) {
  
      switch (inBit) {
      case 0: 
       for (byte ii=1; ii<3; ii++) {
        if (high == true) {   // if current pulse should be high, send High Zero
          sendPulse(2); 
        } else {              // else send Low Zero
                sendPulse(4);
        }
        high=!high; // invert next pulse
       }
        break;
      case 1:                // if current pulse should be high, send High One
        if (high == true) {
          sendPulse(3);
        } else {             // else send Low One
                sendPulse(5);
        }
        high=!high; // invert next pulse
        break;        
      }
}

// transmit pulses
// slightly corrected pulse length, use old (commented out) values if these not working for you

void sendPulse(byte txPulse) {

  switch(txPulse) { // transmit pulse
   case 1: // Start
   digitalWrite(txPin, HIGH);
   delayMicroseconds(500); // 550
   digitalWrite(txPin, LOW);
   break;
   case 2: // "High Zero"
   digitalWrite(txPin, LOW);
   delayMicroseconds(100); // 110
   digitalWrite(txPin, HIGH);
   break;   
   case 3: // "High One"
   digitalWrite(txPin, LOW);
   delayMicroseconds(300); // 303
   digitalWrite(txPin, HIGH);
   break;      
   case 4: // "Low Zero"
   digitalWrite(txPin, HIGH);
   delayMicroseconds(100); // 110
   digitalWrite(txPin, LOW);
   break;      
   case 5: // "Low One"
   digitalWrite(txPin, HIGH);
   delayMicroseconds(300); // 290
   digitalWrite(txPin, LOW);
   break;      
  } 
}



Сдаем в библиотеку

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

В этом процессе неоценимую помощь оказала инструкция на сайте Arduino.cc. На русском языке инструкция опубликована на Arduino.ru.

Получилось точно по рецепту (ни шага в сторону, ни прыжков на месте). Файл h, cpp, readme и небольшой пример, показывающий как всем этим счастьем пользоваться.

Lilvolo.h
/*
  Livolo.h - Library for Livolo wireless switches.
  Created by Sergey Chernov, October 25, 2013.
  Released into the public domain.
*/

#ifndef Livolo_h
#define Livolo_h

#include "Arduino.h"

class Livolo
{
  public:
    Livolo(byte pin);
    void sendButton(unsigned int remoteID, byte keycode);
  private:
    byte txPin;
	byte i; // just a counter
	byte pulse; // counter for command repeat
	boolean high; // pulse "sign"
	void selectPulse(byte inBit);
	void sendPulse(byte txPulse);
};

#endif



Lilvolo.cpp
/*
  Livolo.cpp - Library for Livolo wireless switches.
  Created by Sergey Chernov, October 25, 2013.
  Released into the public domain.
  
  01/12/2013 - code optimization, thanks Maarten! http://forum.arduino.cc/index.php?topic=153525.msg1489857#msg1489857
  
*/

#include "Arduino.h"
#include "Livolo.h"

Livolo::Livolo(byte pin)
{
  pinMode(pin, OUTPUT);
  txPin = pin;
}

// keycodes #1: 0, #2: 96, #3: 120, #4: 24, #5: 80, #6: 48, #7: 108, #8: 12, #9: 72; #10: 40, #OFF: 106
// real remote IDs: 6400; 19303
// tested "virtual" remote IDs: 10550; 8500; 7400
// other IDs could work too, as long as they do not exceed 16 bit
// known issue: not all 16 bit remote ID are valid
// have not tested other buttons, but as there is dimmer control, some keycodes could be strictly system
// use: sendButton(remoteID, keycode), see example blink.ino; 


void Livolo::sendButton(unsigned int remoteID, byte keycode) {

  for (pulse= 0; pulse <= 180; pulse = pulse+1) { // how many times to transmit a command
  sendPulse(1); // Start  
  high = true; // first pulse is always high

  for (i = 16; i>0; i--) { // transmit remoteID
    byte txPulse=bitRead(remoteID, i-1); // read bits from remote ID
    selectPulse(txPulse);    
    }

  for (i = 7; i>0; i--) { // transmit keycode
    byte txPulse=bitRead(keycode, i-1); // read bits from keycode
    selectPulse(txPulse);    
    }    
  }
   digitalWrite(txPin, LOW);
}

// build transmit sequence so that every high pulse is followed by low and vice versa

void Livolo::selectPulse(byte inBit) {
  
      switch (inBit) {
      case 0: 
       for (byte ii=1; ii<3; ii++) {
        if (high == true) {   // if current pulse should be high, send High Zero
          sendPulse(2); 
        } else {              // else send Low Zero
                sendPulse(4);
        }
        high=!high; // invert next pulse
       }
        break;
      case 1:                // if current pulse should be high, send High One
        if (high == true) {
          sendPulse(3);
        } else {             // else send Low One
                sendPulse(5);
        }
        high=!high; // invert next pulse
        break;        
      }
}

// transmit pulses
// slightly corrected pulse length, use old (commented out) values if these not working for you

void Livolo::sendPulse(byte txPulse) {

  switch(txPulse) { // transmit pulse
   case 1: // Start
   digitalWrite(txPin, HIGH);
   delayMicroseconds(500); // 550
   // digitalWrite(txPin, LOW); 
   break;
   case 2: // "High Zero"
   digitalWrite(txPin, LOW);
   delayMicroseconds(100); // 110
   digitalWrite(txPin, HIGH);
   break;   
   case 3: // "High One"
   digitalWrite(txPin, LOW);
   delayMicroseconds(300); // 303
   digitalWrite(txPin, HIGH);
   break;      
   case 4: // "Low Zero"
   digitalWrite(txPin, HIGH);
   delayMicroseconds(100); // 110
   digitalWrite(txPin, LOW);
   break;      
   case 5: // "Low One"
   digitalWrite(txPin, HIGH);
   delayMicroseconds(300); // 290
   digitalWrite(txPin, LOW);
   break;      
  } 
}



readme.txt
This is a library to control Livolo branded wireless switches. 

Features:

- emulates buttons 1 to 0 and ALL OFF of Livolo remote controller

Usage:

Basically you need two things to get it to work:

1) Create Livolo instance
2) Use sendButton (unsigned int remoteID, byte keycode) function to "push" the buttons

sendButton function uses to arguments: remote ID and keycode. Typically, remote IDs are 16 bit unsigned values, but
not all of them are valid (maybe there are some IDs reserved only for system use or there is something I don't know).

Tested remote IDs: 

- read from real remote IDs: 6400; 19303
- "virtual" remote IDs: 10550; 8500; 7400

You can try and find new IDs as well: put your switch into learning mode and start sendButton with remote ID you wish to use. If
it is a valid ID, switch will accept it.

Keycodes read from real remote:

#1: 0, #2: 96, #3: 120, #4: 24, #5: 80, #6: 48, #7: 108, #8: 12, #9: 72; #10: 40, #OFF: 106

Keycodes are 7 bit values (actually I use 8 bit values, just skip most significant (leftmost) bit), but other keycodes
could be reserved for system use (dimmer, for example).

For an example sketch see blink.ino under examples folder.



blink.ino
// Simple blink example of Livolo.h library for Livolo wireless light switches

#include <livolo.h>

Livolo livolo(8); // transmitter connected to pin #8


void setup() {
}

void loop() {
 
  livolo.sendButton(6400, 120); // blink button #3 every 3 seconds using remote with remoteID #6400
  delay(3000);
  
}




Или все одним архивом.

Чего я не смог

Собственно, мне удалось решить основную задачу — имитацию произвольного пульта Livolo для управления выключателями, но не получилось «прочитать» идентификатор уже имеющегося. Для этого любой желающий имитировать свой пульт (чтобы использовать его параллельно с Arduino) должен был бы записать его сигнал в Audacity (или чем-то похожем) и вычислить идентификатор по кодовой посылке.

Не слишком приятно, но с этим я поделать сначала ничего не смог (то времени не хватало, то ума), а немного позже пропала необходимость. Код написал один из товарищей, ознакомившихся с моими страданиями по поводу Livolo.

Вот и все. Доклад по лабораторной работе закончил.
Метки:
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 29
  • +9
    Странно, что в статье не прозвучали слова «манчестерский код» и Biphase mark coding (кажется, у вас именно он): en.wikipedia.org/wiki/Biphase_mark_code

    Кстати, вы не пробовали отправлять такую последовательность используя UART, и скармливать ему сразу байты (скорость в 2 раза увеличить, байты предварительно закодировать)?
    • +2
      На самом деле отсутствие упоминания алгоритмов кодирования закономерно — для меня это совершенно темный лес. Поэтому первая попытка и заключалась в обычном повторении формы сигнала.

      Собственно, я даже идею отправки через UART не понимаю.
      • +3
        Я сейчас буду гордо кидаться теорией :)

        USART позволяет записать в буфер 1 байт данных, которые на выходе выплюнутся в ножку микросхемы. www.atmega8.ru/wiki/view/doc.17.html. Скорость, в которой биты выплёнываются в провод настраивается.
        Кодирование, которое вы описали, увеличивает в 2 раза количество данных, то есть 1 бит информации (допустим, 0) превращается в 2 бита в проводе (01 или 10). То есть 1 байт данных после кодирования превратится в 2 байта. Никакой магии, алгоритм просто и понятен, примерно это у вас изображено в функции selectPulse().

        Если совместить эти 2 факта — то можно максимально разгрузить вычислительное ядро атмеги и заставить работать блок USART. Тут подставу могут организовать старт-стоп биты и контроль чётности, придётся поэкспериментировать в эмуляторе или с осциллографом, ну или с той же audacity. Возможно, придётся использовать USART в режиме SPI-мастера и использовать ножку передачи данных.

        Я такое на практике не делал, но поэкспериментировать в этом направлении определённо имеет смысл.
        • +1
          Теория интересная, но я, пожалуй ее не освою (

          К тому же кодирование в примере не в два раза увеличивает количество данных: это «0» кодируется двумя «битами», а «1» как была одним «битом» в проводе, так и осталась. Скорость конкретно передачи важна, но не максимальная, а правильная. Иначе приемник не поймет — он понимает только импульсы определенной длины.
          • 0
            Единица тоже в два бита превращается, 11 или 00.
            • 0
              В протоколе, который в моих выключателях — не превращается никак. Если мы про этот протокол говорим, конечно.
              • +1
                Выражаю вам огромное уважение за проделанную работу и за неисчерпаемое усердие))
                Вы меня просто вдохновили встать и пойти делать))

                А по поводу протоколов я бы хотел пояснить.

                TosSHiC все правильно сказал, но достаточно сумбурно и я совершенно не удивлен что вы не уловили смысл с первого раза. Постараюсь прояснить и поведать свои мысли так чтобы и вы и другие уважаемые читатели все поняли =)

                Итак.
                1) Замер производится всегда в центра импульса. Не в начале, как можно было бы подумать))
                Что это означает в нашем случае — нужно интерпретировать сигнал немного иначе. Я буду опираться на ваши картинки.
                2) Интерпритация логики такова что у нас есть четыре состояния импульса, а именно:
                а) Переход сверху вниз (Предположим, 00)
                б) Низ без перехода (Предположим, 01)
                в) Переход снизу вверх (Предположим, 10)
                г) Верх без перехода (Предположим, 11)

                Таким образом битовая скорость у нас в два раза больше скорости бодовой (канальной).

                Вот именно это, как мне кажется, ToSHiC и хотел сказать))

                Дальнейший анализ протокола имеет исключительно академический интерес.
                Потому как практически все уже стабильно работает.

                Но мне очень интересно поковырять протокол)))
                • +1
                  Спасибо за пояснения ) Я начинаю понимать, что действительно мало, что понимаю. Но хотя бы подогнал решение задачи под ответ — и то хорошо.
                • 0
                  Судя по картинкам, превращается. «Долгий» импульс — это всего лишь два одинаковых коротких подряд.
                  • +1
                    По измерениям «на глаз» соотношение между ними 1:3, но в целом все может быть. Я понемногу пересматриваю свои заявления, поскольку мне объясняют, где именно и как именно я ошибаюсь.
                  • 0
                    Смотрите, давайте на примере покажу.
                    image
                    Исходная последовательность бит 00011001000000001111000
                    после кодирования 10 10 10 11 00 10 10 11 01 01 01 01 01 01 01 01 00 11 00 11 01 01 01
                    • +1
                      Да, я уперся рогом в свое видение, а оно ошибочно. Но, кажется, уже начинаю немного понимать о чем идет речь. И, похоже, у меня есть что почитать на этих выходных )
                    • +1
                      У меня встречный вопрос: я использовал приемник с амплитудной модуляцией. Разве возможно таким приемником принять и демодулировать сигнал с фазовой модуляцией?
                      • +1
                        Ваш приемник уже демодулировал АМ, то что вы видите это демодулированный после АМ сигнал(ИК приемник, если прочитать на него документацию имеет инверсный выход — разомкнут когда нет сигнала и притягивает выход к нулю когда ИК-сигнал есть, за счет подтяжки к +5В он имеет высокий уровень при отсутствии сигнала). Он в свою очередь промодулирован цифровой фазовой модуляцией, модуляция получается как матрешка — одна в другой, а та в третьей и т.д.
                  • +1
                    Кодирование, которое вы описали, увеличивает в 2 раза количество данных, то есть 1 бит информации (допустим, 0) превращается в 2 бита в проводе (01 или 10). То есть 1 байт данных после кодирования превратится в 2 байта. Никакой магии, алгоритм просто и понятен, примерно это у вас изображено в функции selectPulse().

                    Правильней привязываться к фронту сигнала.
                    0 кодируется переходом от низкого уровня к высокому.
                    1 кодируется переходом от высокого уровня к низкому.

                    Код самосинхронизириуем, т.е. небольшое изменение скорости передачи будет скомпенсировано автоматически.
                    Поэтому надо представленный в статье график разделить на более ровные отрезки и сдвинуть на полфазы. В результате должно получиться, что переход сверху-вниз — это 1 и снизу-вверх — 0. Можно и наоборот.
                    В этом случае код получается немного другой, но график полностью соответсвует манчестерскому кодированию.

                    • 0
                      Одинаковая часть в начале каждого пакета — это импульсы для настройки приемника. По ним он подстраивается на правильный прием данных.
                      • 0
                        А это точно так? Дело в том, что эта часть разная у разных пультов, но, разумеется, одинаковая для всех кнопок-кодов одного и того же пульта.

                        Или это просто так сложилось, что идентификатор пульта одновременно выполняет функции настройки приемника?
                        • 0
                          Я про несколько одинаковых нулей в самом начале посылки.
                          А так все правильно — протоколы у пультов разные. Иначе бы можно было любым пультом управлять любой техникой.
                          • 0
                            Мне кажется, с нулями не все так однозначно. Я не приводил сигналы второго пульта, поскольку подумал, что особого смысла в них нет. Но вот так выглядит его пакет импульсов:



                            Моя изначальная теория гласит, что многократное повторение позволяет физическому приемнику (АРУ) успеть надежно захватить радиосигнал, и то же самое многократное повторение позволяет логическому приемнику «зацепиться» за стартовый импульс.

                            Этим я пытался воспользоваться, когда еще не оставил надежды написать свой приемник. Я просто считал импульсы 500 микросекунд за определенное время, и если это совпадало с характеристиками сигнала Livolo, то сообщал себе любимому, что кто-то нажал на кнопку пульта. Получилось довольно надежно — особенно, если учесть, что вокруг, как выяснилось, огромное количество помех с похожими по длине импульсами.

                            Возможно, что и родной приемник работает по похожему принципу. Ну или я, как обычно, ошибаюсь.
                  • 0
                    Я почему-то сразу узрел в этом коде простую цифровую фазовую модуляцию. «1» передается импульсами фиксированной длительности 0-1 а «0» импульсами 1-0 как-то так. когда соединяются вместе 0 и 1 или 1 и 0 получается длинный импульс, когда идут подряд 111 или 000 — короткие импульсы.
                • +1
                  ф
                  • +2
                    Такой же алгоритм кодирования bi-phase (но возможно с другими длительностями) — в протоколе RC5. А вот здесь описан простой способ дешифровки. Я около полугода назад тоже намучился, пока разобрался с этим протоколом и научился безошибочно расшифровывать посылки (на микроконтроллере msp430).
                    • +3
                      Зашел из-за заголовка, но подробное описание зубочистки не нашел.
                      • 0
                        Она у меня скромная.
                      • 0
                        Хочу сказать большое спасибо автору, все собрал и оно работает! Осталось прикрутить к ардуинке вайфай.
                        • 0
                          Рад, что все получилось!
                          • +1
                            Кстати, если кому-то (например для интеграции с Blynk) понадобится версия библиотеки для nodejs, то вот пожалуйста: https://github.com/crankru/nodejs-livolo

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.