Pull to refresh

Взламываем беспроводное управление светом nooLite

Reading time 17 min
Views 42K
image

Привет!

В нашей предыдущей статье мы показывали, как наш контроллер для домашней автоматизации работает с устройствами системы NooLite без использования родных USB-донглов для передачи и приёма сигнала. Чтобы это стало возможно, мы провели реверс-инжиниринг протокола NooLite.

Собственно эта статья содержит описание протокола и рассказывает, как именно мы его разбирали.


Введение


Про систему NooLite на хабре много писали раньше. Система состоит из беспроводных пультов и исполнительных устройств, которые работают на частоте 433.92 MHz.

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

Отдельного внимания заслуживает схема связи пультов и исполнительных устройств в Noolite. В системе у каждого пульта имеется один либо несколько адресов. Эти адреса зашиты в пульты при производстве и отличаются для каждого пульта. Каждый исполнительный блок можно привязать к одному или нескольким пультам (точнее адресам), при этом блок сохраняет в памяти список разрешённых адресов, от которых можно принимать сигналы управления.
Привязка осуществляется следующим образом: сначала на исполнительном блоке надо нажать специальную (единственную) кнопку, после чего лампочка на блоке начинает часто мигать, сигнализируя о входе в режим программирования. Затем в режим программирования, опять же с помощью специальной кнопки, надо перевести пульт, который требуется привязать. После этого надо нажать на любую кнопку на пульте, который отправит специальный сигнал привязки. Исполнительный блок получает этот сигнал, после чего надо подтвердить занесение его в память ещё одним нажатием кнопки программирования.
Отвязка пульта от блока делается похожим образом.

Интересным в этой схеме является то, что уникальные идентификаторы в системе имеют не исполнительные блоки, а пульты. Соответственно команды от пульта имеют вид «я пульт с адресом NNN, отправляю команду XXX». Все блоки, настроенные «слушать» данный адрес NNN выполняют после получения такого сообщения команду XXX.

Приступаем



Мы конечно же начали с того, что разобрали пульты Noolite.
imageimage
(до и после)

(разные другие фотографии разобранных и собранных устройств можно найти в упомянутой статье)

Что видно внутри?

Внутри на плате видно три большие сенсорные площадки, реализующие сенсорные кнопки.
В правой верхней четверти на плате нарисована антенна на 433MHz.
Большая круглая штука слева — литиевая батарейка. К её минусу мы припаяли для удобства провод.

Схема справа реализует всё логику и передающую часть. Микросхема в корпусе с 8 ножками — микроконтроллер PIC12F.
Справа от микроконтроллера видна кнопка, а ещё правее вся аналоговая часть.

Аналоговая часть — это передатчик, передающий данные в т.к. называемой OOK-модуляции. Передатчик полностью аналоговый и собран на дискретных компонентах. Несущая частота в 433.92 MHz задаётся SAW-резонатором, который на схеме обозначен ZQ1.

Схема передачи

В подобных девайсах в подавляющем большинстве случаев используется так называемая OOK-модуляция. OOK расшифровывается как «on-off keyring», т.е. такой подвид амплитудной модуляции, при котором модуляция фактически осуществляется включением-выключением передатчика. Таким образом, наличие в определённый момент времени сигнала на частоте 433.92MHz означает логическую единицу, а отсутствие — логический ноль.

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

Снимаем данные



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

Снимать сигнал будем логическим анализатором. Грубо говоря, логический анализатор — это осциллограф, который имеет однобитный АЦП, т.е. может различить только наличие и отсутсвие напряжения в канале. Применяются логические анализаторы, как ни странно, для анализа цифровых сигналов.

В работе мы пользуемся логическим анализатором Open Bench Logic Sniffer — это открытый проект с открытой прошивкой и открытым софтом. Стоит всего $50 и имеет приличную скорость захвата в 50 миллионов семплов в секунду. Испольуземое клиентское приложение OLS — написано на java и кроссплатформенное.

В приципе, для целей этой статьи подошли бы и гораздо более дешёвые логические анализаторы на чипе Cypress CY7C68013A (искать по ключевым словам «saleae clone» или «usbee clone»), которые стоят у китайцев что-то вроде $7.

image
(Макетка с собственно логическим анализатором, ардуинкой и радиомодулем RFM69H, используемом в Wiren Board Smart Home)

Дампы



Итак, сигнал с отмеченной красным ножки мы вывели на первый канал логического анализатора, соединили земли и настроили триггер в клиенте анализатора. Вот что вышло:
image

На картинке — дамп пакета, который пересылает по радио пульт NooLite при нажатии кнопки.
Что можно сказать по такой картинке? На самом деле довольно много:

Минимальная длина единичек и ноликов одинакова и составляет 500us. Данные передаются на физическом уровне на частоте 2000 bit/s.

В начале пакета передачи виден большой кусок прямоугольного меандра. Это так называемая преамбула, которая помогает приёмнику подстроить частоту передачи данных (битрейт) под конкретный приёмник. И приёмник и передатчик используют встроенные RC-генераторы для тактирования микроконтроллера, поэтому «2000 бод» у приёмника и передатчика могут отличаться процентов на 10.

После преабмулы начинаются данные. Данные следуют весьма узнаваемому паттерну манчестерского кодирования: в коде встречаются только пары длинный ноль-длинная единица и короткий ноль-короткая единица. «Длинные» участки с постоянным уровнем ровно в два раза длиннее коротких, т.е. представляют собой два «физических» бита. В посылке отсутствуют последовательности нулей или единиц длиннее 2 бит.
Такой хараткерный паттерн получается, если закодировать исходные данные следующим образом: передавать два бита, «01», для каждой единицы и «10» для каждого нуля. Что такое манчестерский код подробно написано в википедии, но если совсем коротко, то используют его для двух вещей: во-первых он позволяет избавиться от постоянной составляющей сигнала, а во-вторых, не требует совпадение частоты приёмника и передатчика, позволяя восстановить частоту из принятого сигнала.

Ещё одно наблюдение, касающееся данных: в пакете один и тот же блок данных передаётся дважды. Делается это видимо чтобы уменьшить шансы порчи пакета из-за интерференции и помех (напомним, в Noolite нет подтверждения доставки сообщения)

image
(два идентичных пакета с данными, более крупно)

Оцифровываем данные



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

Для начала экспортируем данные в виде csv. В файле будет записано время каждого изменения нашего сигнала.
Выглядят они примерно так
Первая колонка — время, последняя колонка — состояние нашего канала.
"timestamp (abs)","timestamp (rel)","sample rate (Hz)","Channel-7","Channel-6","
Channel-5","Channel-4","Channel-3","Channel-2","Channel-1","Channel-0"
0,-2454,100000,0,0,0,0,0,0,0,0
2456,2,100000,0,0,0,0,0,0,0,1
2505,51,100000,0,0,0,0,0,0,0,0
2555,101,100000,0,0,0,0,0,0,0,1
2605,151,100000,0,0,0,0,0,0,0,0
2655,201,100000,0,0,0,0,0,0,0,1
2704,250,100000,0,0,0,0,0,0,0,0
2755,301,100000,0,0,0,0,0,0,0,1
2804,350,100000,0,0,0,0,0,0,0,0
2854,400,100000,0,0,0,0,0,0,0,1
2904,450,100000,0,0,0,0,0,0,0,0
2954,500,100000,0,0,0,0,0,0,0,1
3003,549,100000,0,0,0,0,0,0,0,0
3054,600,100000,0,0,0,0,0,0,0,1
3103,649,100000,0,0,0,0,0,0,0,0
3153,699,100000,0,0,0,0,0,0,0,1
3203,749,100000,0,0,0,0,0,0,0,0



image
(картинка из википедии)

Логика декодирования манчестерского кода очень простая: можно заметить, что длинный промежуток между фронтами кодирует бит данных, который отличается от предыдущего. Если очередной бит данных от предыдущего не отличается, то он будет кодироваться двумя короткими промежутками между фронтами.

Соответствующий код на Python
#coding: utf-8
import csv
import sys, time

reader = csv.DictReader(open(sys.argv[1]))
#~ reader.next()

channel = 'Channel-0'

delays = []

freq = None

prev_ts = None

state = False
out_short = True

def print_packet(packet):
	checksum = False
	line = ''
	for bit in packet:
		line+= '1' if bit else '0'
		line+= ','
		checksum ^= bit
	#~ print line, "=", checksum
	print line

packet = []

packets = []

prev_val = None
for row in reader:
	freq = int(row['sample rate (Hz)'])
	ts = int(row['timestamp (abs)'])
	val = int(row[channel])
	#~ print row
	if val == prev_val:
		continue
	prev_val = val




	if prev_ts:
		delay = 1.0/freq * (ts-prev_ts)
		if delay > 1000E-6 * 1.3:
			#~ print "pause"
			state = False
			packets.append(packet)
			packet = []
			out_short = True

		elif delay > 500E-6 * 1.3:
			#~ print "long"

			state = not state
			packet.append(state)
			out_short = True
		else:
			#~ print "short"
			# short
			out_short = not out_short
			if out_short:
				packet.append(state)

	prev_ts = ts




#~ for packet in packets:
	#~ print_packet(packet)
print sys.argv[1],",",

#~ assert packets[0] == [0,]*37
#~ assert (packets[1] == packets[2])
#~ print packets
print_packet(packets[1])
print_packet(packets[2])

# 5 в начале меняются между программированием и норм. режимом и в кнопках
# на отпускание посылается одна и та же команда



Анализ


Нажимаем разные кнопки на разных пультах, записываем, что получается. В процессе мы заметили, что отправка команд на диммирование производится оригинальным способом: при начале нажатия кнопки пульт отправляет одну команду («начать увеличивать яркость»), при отпускании отправляет ещё одну («закончить увеличивать яркость»).

Собираем статистику в табличку:
image

Некоторую подсказку даёт документация на оригинальные модули Noolite для компьютера.

Сначала возьмём из документации коды команд и попробуем найти их в потоке бит. Код команды обнаруживается в 4-х битах, начиная со второго. Код записывается по схеме LSB, т.е. первой значащей цифрой назад.

Кроме команды легко выделяются два байта адреса (остаются неизменными для любых команд одного пульта) и заполненный нулями байт «аргумента». При повторных посылках одной и той же команды между двумя возможными значениями меняется значение первого бита и 8-ми последних бит.

Все посылки имеют постоянную чётность, т.е. xor всех бит даёт одну и ту же величину. Такое поведение намекает на наличие контрольной суммы.

Контрольная сумма


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

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

Первый кандидат в контрольную сумму — это конечно CRC. Однако, алгоритм CRC-8 имеет в общем случае три 8-битных параметра, а перебор пары часто используемых комбинаций к успеху не привёл.

Отправляем команды


Было решено попробовать генерировать пакеты Noolite и проверять их «валидность» с помощью исполнительного блока, который мигает при приёме валидного пакета.

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

Скетч выглядит примерно так
#define UINT8 unsigned char

// Pin 13 has an LED connected on most Arduino boards.
// give it a name:
int gpio = 12;

// the setup routine runs once when you press reset:
void setup() {                
  // initialize the digital pin as an output.
  pinMode(gpio, OUTPUT);     
  Serial.begin(115200);
}

const unsigned int PERIOD = 500; //usec

void send_sequence(int count, unsigned char * sequence) {
  char clock = 1;
  
  for (int i = 0; i < count; ++i) {
    char data = sequence[i];
    clock = !clock;
    digitalWrite(gpio, clock ^ (!data));
    delayMicroseconds(PERIOD);
    clock = !clock;
    digitalWrite(gpio, clock ^ (!data));
    delayMicroseconds(PERIOD);
  }
  
  
}


// Automatically generated CRC function
// polynomial: 0x131, bit reverse algorithm
UINT8
crc8_maxim(UINT8 *data, int len, UINT8 crc)
{
    static const UINT8 table[256] = {
    0x00U,0x5EU,0xBCU,0xE2U,0x61U,0x3FU,0xDDU,0x83U,
    0xC2U,0x9CU,0x7EU,0x20U,0xA3U,0xFDU,0x1FU,0x41U,
    0x9DU,0xC3U,0x21U,0x7FU,0xFCU,0xA2U,0x40U,0x1EU,
    0x5FU,0x01U,0xE3U,0xBDU,0x3EU,0x60U,0x82U,0xDCU,
    0x23U,0x7DU,0x9FU,0xC1U,0x42U,0x1CU,0xFEU,0xA0U,
    0xE1U,0xBFU,0x5DU,0x03U,0x80U,0xDEU,0x3CU,0x62U,
    0xBEU,0xE0U,0x02U,0x5CU,0xDFU,0x81U,0x63U,0x3DU,
    0x7CU,0x22U,0xC0U,0x9EU,0x1DU,0x43U,0xA1U,0xFFU,
    0x46U,0x18U,0xFAU,0xA4U,0x27U,0x79U,0x9BU,0xC5U,
    0x84U,0xDAU,0x38U,0x66U,0xE5U,0xBBU,0x59U,0x07U,
    0xDBU,0x85U,0x67U,0x39U,0xBAU,0xE4U,0x06U,0x58U,
    0x19U,0x47U,0xA5U,0xFBU,0x78U,0x26U,0xC4U,0x9AU,
    0x65U,0x3BU,0xD9U,0x87U,0x04U,0x5AU,0xB8U,0xE6U,
    0xA7U,0xF9U,0x1BU,0x45U,0xC6U,0x98U,0x7AU,0x24U,
    0xF8U,0xA6U,0x44U,0x1AU,0x99U,0xC7U,0x25U,0x7BU,
    0x3AU,0x64U,0x86U,0xD8U,0x5BU,0x05U,0xE7U,0xB9U,
    0x8CU,0xD2U,0x30U,0x6EU,0xEDU,0xB3U,0x51U,0x0FU,
    0x4EU,0x10U,0xF2U,0xACU,0x2FU,0x71U,0x93U,0xCDU,
    0x11U,0x4FU,0xADU,0xF3U,0x70U,0x2EU,0xCCU,0x92U,
    0xD3U,0x8DU,0x6FU,0x31U,0xB2U,0xECU,0x0EU,0x50U,
    0xAFU,0xF1U,0x13U,0x4DU,0xCEU,0x90U,0x72U,0x2CU,
    0x6DU,0x33U,0xD1U,0x8FU,0x0CU,0x52U,0xB0U,0xEEU,
    0x32U,0x6CU,0x8EU,0xD0U,0x53U,0x0DU,0xEFU,0xB1U,
    0xF0U,0xAEU,0x4CU,0x12U,0x91U,0xCFU,0x2DU,0x73U,
    0xCAU,0x94U,0x76U,0x28U,0xABU,0xF5U,0x17U,0x49U,
    0x08U,0x56U,0xB4U,0xEAU,0x69U,0x37U,0xD5U,0x8BU,
    0x57U,0x09U,0xEBU,0xB5U,0x36U,0x68U,0x8AU,0xD4U,
    0x95U,0xCBU,0x29U,0x77U,0xF4U,0xAAU,0x48U,0x16U,
    0xE9U,0xB7U,0x55U,0x0BU,0x88U,0xD6U,0x34U,0x6AU,
    0x2BU,0x75U,0x97U,0xC9U,0x4AU,0x14U,0xF6U,0xA8U,
    0x74U,0x2AU,0xC8U,0x96U,0x15U,0x4BU,0xA9U,0xF7U,
    0xB6U,0xE8U,0x0AU,0x54U,0xD7U,0x89U,0x6BU,0x35U,
    };
    
    while (len > 0)
    {
        crc = table[*data ^ (UINT8)crc];
        data++;
        len--;
    }
    return crc;
}




void convert_to_buf(unsigned char val, unsigned char* buf) {
  unsigned char  mask = 1;
  for (int i = 0; i  < 8; ++ i) {
    if (val & mask) {
      buf[i] = 1;
    } else {
      buf[i] = 0;
    }
    mask = mask << 1;
  }
}


unsigned char calc_checksum(int count, unsigned char * sequence) {
  unsigned char data[] = {0,0,0,0};
  unsigned char mask  ;
  // first byte from 1 to 5 bit (0-based)
  
  for (int i=1; i < 6; ++i) {
    if (sequence[i]) {
      //bit 1 to  2**3 mask
      mask = 1 << (i + 2);
      data[0] |= mask;
    }
  }
  
  for (int byte_n=0; byte_n < 3; ++byte_n) {
    // [] = 6 + byte_n * 8 + i
    
    for (int i=0; i < 8; ++i) {
      if (sequence[6 + byte_n * 8 + i]) {
        mask = 1 << i;  
        data[byte_n + 1] |= mask;
     }
    }
  }
  
  return crc8_maxim(data, 4, 0);
}



  unsigned char preamble[] = {1,1,1,1,1,1,1,1,
                     1,1,1,1,1,1,1,1,
                     1,1,1,1,1,1,1,1,
                     1,1,1,1,1,1,1,1,
                     1,1,1,1,1,1};

  
  unsigned char command[] = {1, 
                   0,
                   1,0,0,1,
                   
                   1,0,0,1,0,1,1,0,
                   0,0,1,0,0,0,0,1,
                   
                   
                   /*1,1,1,1,1,0,0,0,
                   0,0,0,0,0,0,0,0,*/
                   0,0,0,0,0,0,0,0,
                   
  };


// the loop routine runs over and over again forever:
void loop() {

    
 
  unsigned char checksum[] = {0,0,0,0,0,0,0,0};
                             
  for (unsigned int addr_lo = 0; addr_lo < 256; ++addr_lo) {
    for (unsigned int addr_hi = 0; addr_hi < 256; ++addr_hi) {    
      Serial.println(addr_hi);
      Serial.println(addr_lo);

      
      convert_to_buf(addr_hi, command + 6);
      convert_to_buf(addr_lo, command + 6 + 8);
      
  
                             
                             
      unsigned char checksum_val = calc_checksum(sizeof(command), command);
    
    
    
          convert_to_buf(checksum_val, checksum);
          // Serial.println(checksum_val);
          
        //  if (checksum_val==198) continue;
         
          
          delay(10);
          
          
      
      
      
          
          send_sequence(sizeof(preamble), preamble);
          
          digitalWrite(gpio, LOW);
          delayMicroseconds(500 * 3);
        
          send_sequence(sizeof(command), command);
          send_sequence(sizeof(checksum), checksum);
          digitalWrite(gpio, LOW);
          delayMicroseconds(500 * 3);
          send_sequence(sizeof(command), command);
          send_sequence(sizeof(checksum), checksum);
          digitalWrite(gpio, LOW);
          
          delayMicroseconds(500 * 3);
          digitalWrite(gpio, LOW);

  
      }
    }
  
  //    command[1] = !command[1];
    //  Serial.println(command[1]);
    //  delay(4000);



  while (1) {};
  
  
}



Здесь мы с просто выдаём в линию единички и нолики с помощью digitalWrite(). Длительность импульсов регулируется задержкой.

После того, как мы проверили, что можем воспроизвести один из записанных пакетов, и блок ноолайта его принимает за свой, мы начали дальнейшие эксперименты.

Контрольная сумма 2

Как было написано выше, нужно было понять алгоритм генерации контрольной суммы.
У алгоритма CRC есть хорошее свойство, которое можно проверить: эта контрольная сумма линейна по аргументам.

image

Таким образом, если взять два известных пакета, сложить их побитово (т.е. сделать xor), то получившийся пакет будет иметь верную контрольную сумму!

Выбрав два подходящих пакета, проверяем это предположение и подтверждаем, что чексумма — линейна по аргументам.

Контрольная сумма 3

Следующим нашим действием был брутфорс контрольных сумм. С помощью бинарного поиска для заданной последовательности бит контрольная сумма подбирается за несколько минут. Делается это очевидным образом: запускаем скетч, перебирающий контрольные суммы от 1 до 128 и смотрим на приёмник. Если за время работы скетча он моргнул (получил валидный пакет), то мы знаем, что искомая контрольная сумма где-то от 1 до 128. И т.д.

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

Чтобы восстановить функцию, надо подобрать чексуммы к количеству пакетов равному количеству бит в пакете, т.е. к 29 пакетам.
image

Делается это всё медленно и успело порядком надоесть. Так что, пройдя больше половины бит, мы решили быстро попробовать перебрать чексуммы в оффлайне.

Контрольная сумма 4

Как писалось выше, у CRC есть несколько входных параметров: полином (8-бит), начальное значение (8-бит), значение, которое добавляется в конце (8-бит). Кроме этого, 1 бит — задаёт является ли алгоритм инвертированным или нет.

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

Переберём всё это грубой силой. Для грубой силы мы использовали Python и библиотечку crcmod.

примерный код

import crcmod


samples = [
 #~ ['\x00\x00\x01' + chr(0b11110000),  chr(0b11011010)],
 #~ ['\x00\x00\x03' + chr(0b11110000),  chr(0b10010101)],

 [chr(0b11110000) + '\x01\x00\x00',  chr(0b11011010)],
 [chr(0b11110000) + '\x03\x00\x00',  chr(0b10010101)],
 [chr(0b11110000) + chr(0b1) + chr(0b1) + '\x00',  chr(0b00011110)],

 [chr(0b11111000) + chr(0b1) + chr(0b1) + '\x00',  chr(0b00000010)],


 #~ [chr(0b11111000) + chr(0b1) + chr(0b1) + '\x00',  chr(0b00000010)],


]

#~ predef = crcmod.predefined.mkPredefinedCrcFun('crc-8-maxim')
predef = crcmod.Crc(256 + 0x31,initCrc=0x00,rev=True)

for data, checksum in samples:
	print "="*10

	for poly in xrange(255):
		for init_crc in (0, 0xff):
			for rev in (True, False):
				digest = crcmod.Crc(256 + poly,initCrc=init_crc,rev=rev).new(data).digest()
				if digest == checksum:
					print poly, init_crc, rev

for data, checksum in samples:
	print "expected: ", hex(ord(checksum))
	print predef.new(data).hexdigest()

import sys
print predef.generateCode("crc8_maxim", sys.stdout)






Функция нашлась! Это схема «crc8_maxim», первые 5 бит сначала добивается нулями слева. Затем все байты записываются в LSB, т.е. переворачиваются.

Код алгоритма на Python
crc8_table = [
    0x00,0x5E,0xBC,0xE2,0x61,0x3F,0xDD,0x83,
    0xC2,0x9C,0x7E,0x20,0xA3,0xFD,0x1F,0x41,
    0x9D,0xC3,0x21,0x7F,0xFC,0xA2,0x40,0x1E,
    0x5F,0x01,0xE3,0xBD,0x3E,0x60,0x82,0xDC,
    0x23,0x7D,0x9F,0xC1,0x42,0x1C,0xFE,0xA0,
    0xE1,0xBF,0x5D,0x03,0x80,0xDE,0x3C,0x62,
    0xBE,0xE0,0x02,0x5C,0xDF,0x81,0x63,0x3D,
    0x7C,0x22,0xC0,0x9E,0x1D,0x43,0xA1,0xFF,
    0x46,0x18,0xFA,0xA4,0x27,0x79,0x9B,0xC5,
    0x84,0xDA,0x38,0x66,0xE5,0xBB,0x59,0x07,
    0xDB,0x85,0x67,0x39,0xBA,0xE4,0x06,0x58,
    0x19,0x47,0xA5,0xFB,0x78,0x26,0xC4,0x9A,
    0x65,0x3B,0xD9,0x87,0x04,0x5A,0xB8,0xE6,
    0xA7,0xF9,0x1B,0x45,0xC6,0x98,0x7A,0x24,
    0xF8,0xA6,0x44,0x1A,0x99,0xC7,0x25,0x7B,
    0x3A,0x64,0x86,0xD8,0x5B,0x05,0xE7,0xB9,
    0x8C,0xD2,0x30,0x6E,0xED,0xB3,0x51,0x0F,
    0x4E,0x10,0xF2,0xAC,0x2F,0x71,0x93,0xCD,
    0x11,0x4F,0xAD,0xF3,0x70,0x2E,0xCC,0x92,
    0xD3,0x8D,0x6F,0x31,0xB2,0xEC,0x0E,0x50,
    0xAF,0xF1,0x13,0x4D,0xCE,0x90,0x72,0x2C,
    0x6D,0x33,0xD1,0x8F,0x0C,0x52,0xB0,0xEE,
    0x32,0x6C,0x8E,0xD0,0x53,0x0D,0xEF,0xB1,
    0xF0,0xAE,0x4C,0x12,0x91,0xCF,0x2D,0x73,
    0xCA,0x94,0x76,0x28,0xAB,0xF5,0x17,0x49,
    0x08,0x56,0xB4,0xEA,0x69,0x37,0xD5,0x8B,
    0x57,0x09,0xEB,0xB5,0x36,0x68,0x8A,0xD4,
    0x95,0xCB,0x29,0x77,0xF4,0xAA,0x48,0x16,
    0xE9,0xB7,0x55,0x0B,0x88,0xD6,0x34,0x6A,
    0x2B,0x75,0x97,0xC9,0x4A,0x14,0xF6,0xA8,
    0x74,0x2A,0xC8,0x96,0x15,0x4B,0xA9,0xF7,
    0xB6,0xE8,0x0A,0x54,0xD7,0x89,0x6B,0x35,
    ]

def crc8_maxim(data):
    crc = 0
    for i, ch in enumerate(data):
        crc = crc8_table[ord(ch) ^ crc]
    return crc



Промежуточный итог


Теперь мы знаем о протоколе почти всё и можем генерировать произвольные команды включения, выключения, начала регулировки яркости и конца регулировки яркости с произвольными значениями адреса, эмулируя произвольные пульты Noolite.

Этого, однако, не совсем хватает. Дело в том, что среди этих команд отсутствует команда типа «установить яркость на уровень X», что очень неудобно при использовании с системой умного дома. Управлять яркостью с помощью задержки между двумя командами, как это сделано в обычных пультах Noolite — довольно странно.

В то же время, документация к модулям NooLite для компьютера показывает, что такие команды существуют. Например документация к команде с кодом 6 говорит "значение=6 – установить заданную в «Байт данных 0» яркость, установить заданную в Байт данных 0, 1, 2 яркость ".
Естественным претендентом на «Байт данных 0» является предпоследний байт в пакете, который в наших экспериментах был всегда нулевым. Однако, попытки отправить команду, в которой этот байт отличен от нуля не увенчались успехом. По всей видимости, формат посылки при отправке команд с аргументами отличается.

Расширенные команды


Чтобы разобраться с командами с аргументами и поставить точку в разборе протокола Noolite, нужны родные модули NooLite.

(Здесь сразу хотелось бы поблагодарить магазин thinking-home.ru и хабровчанина dima117 за оперативно предоставленные для этого устройства)

При наличии родного модуля, отправлять команды можно, например, с помощью вот этой программы.

Кроме команды установки заданной яркости, бывают ещё команды, управляющие режимом переключения яркости и цвета у RGB-блоков NooLite, а также устанавливающие значение цвета (RGB).

В этот раз, перехватывать команды мы будем с помощью пакетного радио RFM69H, которое установлено в Wiren Board Smart Home с помощью нашего кода разбора протокола.

Что получилось:

image

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

Формат 1 используется для команды установки яркости, при этом в начало пакета добавляется один байт со значением яркости.
Формат 3 используется для команды установки цвета, в начало пакета добавляется 4 байта. Первые три задают компоненты цвета, четвёртый всегда нулевой, его значение непонятно (видимо зарезервирован).
Формат 4 используется для команд переключения режима. В этом формате в начало пакета после команды добавляется почему-то 4 бита аргумента. Весь пакет при этом сдвигается для вычисления контрольной суммы, т.е. байты отсчитываются с левой границы, оставшиеся биты дополняются нулями до байта.

Итого



Итого, мы полностью разобрались, как работает протокол Noolite. Что можно сказать про протокол:

  1. Используется преамбула и манчестерское кодирование, это есть хорошая практика
  2. Используется всего 16 бит адреса. Это конечно лучше, чем 4-8 бита в совсем дешёвых китайских радиокомплектах, но всё равно довольно мало. Например, новые китайские микросхемы для радиопультов имеют длину кода в 21 бит. 16 бит — это всего 65536 возможных комбинаций, что позволяет перебрать их довольно быстро. При наличии мощного передатчика и хорошей антенны, можно например запустить отправку команды на выключение света в цикле со всеми возможными адресами. Такая штука переберёт все адреса за пару часов и гарантировано выключит свет у всех ваших соседей. А так как связь односторонняя, то количество соседей, которым вы можете выключить свет, ограничено только мощностью вашего передатчика.
  3. Избыточность в протоколе реализуется за счёт повторной отправки пакета и контроля его целостности. Наличие контроля целостности — это безусловное преимущество протокола. С другой стороны, избыточное кодирование можно было бы реализовать и более компактным способом, хотя в случае передачи коротких пакетов по радио это может и не являться преимуществом
  4. В протоколе полностью отсутствует какая-либо защита. В данном случае, вполне можно было бы использовать код типа KeeLoq — это код, который, грубо говоря, основан на синхронизации счётчиков в передатчике и приёмнике, используется в автомобильных брелоках. Реализовать что-то подобное можно даже на железе, которое сейчас используется в NooLite
  5. Используется довольно странная структура пакета. Выбор формата (т.е. в том числе и длины посылки) кодируется последним байтом пакета. Таким образом, разделять пакеты приходится на физическом уровне. Более логичным (и часто используемым) является кодирование длины пакета в самом начале этого пакета. Также не очень понятно, зачем пакет обрезан не по границе байт. Довольно странная экономия на трёх битах, учитывая то, что под код формата отводится целых 8 бит.


Хоть это и не имеет отношения к собственно протоколу, нам показались интересными результаты вскрытия карманных радио-пультов:
вскрытие показало
Собственно радиопульт (картинки из статьи ):
image
Он же, разобранный:
image
А вот плата с другой стороны:
image

Плата мало того, что в традиционном китайском исполнении (т.е. загажена флюсом), так на ней ещё и написано «livolo.com».
Стоит ли говорить, что пульты от livolo выглядят идентично? При этом стоят они меньше $5.

Судя по всему, товарищи разработчики NooLite покупают готовые пульты livolo и перепрошивают их своей прошивкой, благо пульт сделан на мк PIC16, а на плате удобно выведен разъём для его программирования. Надо сказать, весьма умное решение!


P.S.


Производители NooLite собираются выпустить модуль MT1132 с интерфейсом UART, который предназначен для использования с Arduino и т.п. или в собственных устройствах.

До нас доходили слухи, что стоить он будет несколько адекватнее, чем родные USB-брелоки.

Неприкрытая реклама



Собственно протокол NooLite, как вы наверное уже поняли, мы разбирали, чтобы добавить его поддержку в Wiren Board Smart Home — нашем контроллере домашней автоматизации с линуксом на борту и большими возможностями по подключению периферии, который, в частности, имеет универсальный приёмопередатчик на частоте 433MHz.

Заказ на первую партию мы отправляем на завод в эту среду, поэтому мы решили продлить предзаказ на три дня до вторника, 18 марта, включительно. Купить контроллер (отгрузка в конце апреля — начале мая) можно у нас в магазине.
Only registered users can participate in poll. Log in, please.
Что бы нам ещё разобрать и поддержать?
69.54% Выключатели Livolo 105
54.3% Wireless X10 82
5.3% (предложите в комментариях) 8
151 users voted. 88 users abstained.
Tags:
Hubs:
+32
Comments 15
Comments Comments 15

Articles

Information

Website
contactless.ru
Registered
Founded
Employees
2–10 employees
Location
Россия