- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

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

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

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

. вот что показал 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(&currentPointer[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(&currentPointer[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(&currentPointer[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(&currentPointerStart[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(&currentPointer[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 [7]. На русском языке инструкция опубликована на Arduino.ru [8].

Получилось точно по рецепту (ни шага в сторону, ни прыжков на месте). Файл 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);
  
}

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

Чего я не смог

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

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

Вот и все. Доклад по лабораторной работе закончил.

Автор: spc

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/54489

Ссылки в тексте:

[1] Удобном доме: http://habrahabr.ru/post/210664/

[2] Livolo: http://www.aliexpress.com/store/500715

[3] схему делителя: http://wiki.nethome.nu/doku.php/analyzer/hardware

[4] Protocol Analyzer: http://wiki.nethome.nu/doku.php/analyzer/start

[5] Audacity: http://audacity.sourceforge.net/?lang=ru

[6] TMRpcm: https://github.com/TMRh20/TMRpcm

[7] инструкция на сайте Arduino.cc: http://arduino.cc/en/Hacking/LibraryTutorial

[8] инструкция опубликована на Arduino.ru: http://arduino.ru/Hacking/LibraryTutorial

[9] все одним архивом: https://drive.google.com/file/d/0B0DQ7La4EBHlU2ltTmlmeHhya3M/edit?usp=sharing

[10] Код написал: http://forum.arduino.cc/index.php?PHPSESSID=0tq6dfkft7iun064gkum4mhcn1&topic=153525.msg1560970#msg1560970

[11] Источник: http://habrahabr.ru/post/211594/