Привет!
В нашей предыдущей статье мы показывали, как наш контроллер для домашней автоматизации работает с устройствами системы NooLite без использования родных USB-донглов для передачи и приёма сигнала. Чтобы это стало возможно, мы провели реверс-инжиниринг протокола NooLite.
Собственно эта статья содержит описание протокола и рассказывает, как именно мы его разбирали.
Введение
Про систему NooLite на хабре много писали раньше. Система состоит из беспроводных пультов и исполнительных устройств, которые работают на частоте 433.92 MHz.
Связь в системе односторонняя — пульт отправляет данные по радио, а исполнительные устройства его принимают и обрабатывают. Обратного канала нет, а следовательно, нет ни подтверждения получения сообщения, ни контроля статуса исполнительных устройств.
Отдельного внимания заслуживает схема связи пультов и исполнительных устройств в Noolite. В системе у каждого пульта имеется один либо несколько адресов. Эти адреса зашиты в пульты при производстве и отличаются для каждого пульта. Каждый исполнительный блок можно привязать к одному или нескольким пультам (точнее адресам), при этом блок сохраняет в памяти список разрешённых адресов, от которых можно принимать сигналы управления.
Привязка осуществляется следующим образом: сначала на исполнительном блоке надо нажать специальную (единственную) кнопку, после чего лампочка на блоке начинает часто мигать, сигнализируя о входе в режим программирования. Затем в режим программирования, опять же с помощью специальной кнопки, надо перевести пульт, который требуется привязать. После этого надо нажать на любую кнопку на пульте, который отправит специальный сигнал привязки. Исполнительный блок получает этот сигнал, после чего надо подтвердить занесение его в память ещё одним нажатием кнопки программирования.
Отвязка пульта от блока делается похожим образом.
Интересным в этой схеме является то, что уникальные идентификаторы в системе имеют не исполнительные блоки, а пульты. Соответственно команды от пульта имеют вид «я пульт с адресом NNN, отправляю команду XXX». Все блоки, настроенные «слушать» данный адрес NNN выполняют после получения такого сообщения команду XXX.
Приступаем
Мы конечно же начали с того, что разобрали пульты Noolite.
(до и после)
(разные другие фотографии разобранных и собранных устройств можно найти в упомянутой статье)
Что видно внутри?
Внутри на плате видно три большие сенсорные площадки, реализующие сенсорные кнопки.
В правой верхней четверти на плате нарисована антенна на 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.
(Макетка с собственно логическим анализатором, ардуинкой и радиомодулем RFM69H, используемом в Wiren Board Smart Home)
Дампы
Итак, сигнал с отмеченной красным ножки мы вывели на первый канал логического анализатора, соединили земли и настроили триггер в клиенте анализатора. Вот что вышло:
На картинке — дамп пакета, который пересылает по радио пульт NooLite при нажатии кнопки.
Что можно сказать по такой картинке? На самом деле довольно много:
Минимальная длина единичек и ноликов одинакова и составляет 500us. Данные передаются на физическом уровне на частоте 2000 bit/s.
В начале пакета передачи виден большой кусок прямоугольного меандра. Это так называемая преамбула, которая помогает приёмнику подстроить частоту передачи данных (битрейт) под конкретный приёмник. И приёмник и передатчик используют встроенные RC-генераторы для тактирования микроконтроллера, поэтому «2000 бод» у приёмника и передатчика могут отличаться процентов на 10.
После преабмулы начинаются данные. Данные следуют весьма узнаваемому паттерну манчестерского кодирования: в коде встречаются только пары длинный ноль-длинная единица и короткий ноль-короткая единица. «Длинные» участки с постоянным уровнем ровно в два раза длиннее коротких, т.е. представляют собой два «физических» бита. В посылке отсутствуют последовательности нулей или единиц длиннее 2 бит.
Такой хараткерный паттерн получается, если закодировать исходные данные следующим образом: передавать два бита, «01», для каждой единицы и «10» для каждого нуля. Что такое манчестерский код подробно написано в википедии, но если совсем коротко, то используют его для двух вещей: во-первых он позволяет избавиться от постоянной составляющей сигнала, а во-вторых, не требует совпадение частоты приёмника и передатчика, позволяя восстановить частоту из принятого сигнала.
Ещё одно наблюдение, касающееся данных: в пакете один и тот же блок данных передаётся дважды. Делается это видимо чтобы уменьшить шансы порчи пакета из-за интерференции и помех (напомним, в Noolite нет подтверждения доставки сообщения)
(два идентичных пакета с данными, более крупно)
Оцифровываем данные
К сожалению, в ПО для логического анализатора довольно куцые возможности для анализа произвольных протоколов. Нам так и не удалось заставить заработать в нём декодер манчестерского кодирования, поэтому напишем свой.
Для начала экспортируем данные в виде 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
(картинка из википедии)
Логика декодирования манчестерского кода очень простая: можно заметить, что длинный промежуток между фронтами кодирует бит данных, который отличается от предыдущего. Если очередной бит данных от предыдущего не отличается, то он будет кодироваться двумя короткими промежутками между фронтами.
#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 в начале меняются между программированием и норм. режимом и в кнопках
# на отпускание посылается одна и та же команда
Анализ
Нажимаем разные кнопки на разных пультах, записываем, что получается. В процессе мы заметили, что отправка команд на диммирование производится оригинальным способом: при начале нажатия кнопки пульт отправляет одну команду («начать увеличивать яркость»), при отпускании отправляет ещё одну («закончить увеличивать яркость»).
Собираем статистику в табличку:
Некоторую подсказку даёт документация на оригинальные модули 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 есть хорошее свойство, которое можно проверить: эта контрольная сумма линейна по аргументам.
Таким образом, если взять два известных пакета, сложить их побитово (т.е. сделать xor), то получившийся пакет будет иметь верную контрольную сумму!
Выбрав два подходящих пакета, проверяем это предположение и подтверждаем, что чексумма — линейна по аргументам.
Контрольная сумма 3
Следующим нашим действием был брутфорс контрольных сумм. С помощью бинарного поиска для заданной последовательности бит контрольная сумма подбирается за несколько минут. Делается это очевидным образом: запускаем скетч, перебирающий контрольные суммы от 1 до 128 и смотрим на приёмник. Если за время работы скетча он моргнул (получил валидный пакет), то мы знаем, что искомая контрольная сумма где-то от 1 до 128. И т.д.
Теперь, когда мы умеем определять контрольные суммы произвольного пакета, можно попробовать восстановить функцию. Т.к. CRC линейна по аргументам, то, зная как чексумма меняется при изменении каждого бита, можно восстановить функцию.
Чтобы восстановить функцию, надо подобрать чексуммы к количеству пакетов равному количеству бит в пакете, т.е. к 29 пакетам.
Делается это всё медленно и успело порядком надоесть. Так что, пройдя больше половины бит, мы решили быстро попробовать перебрать чексуммы в оффлайне.
Контрольная сумма 4
Как писалось выше, у CRC есть несколько входных параметров: полином (8-бит), начальное значение (8-бит), значение, которое добавляется в конце (8-бит). Кроме этого, 1 бит — задаёт является ли алгоритм инвертированным или нет.
Кроме этих параметров, по-разному можно подготавливать байты, которыми оперирует алгоритм CRC. В посылке noolite 29 бит, т.е. нецелое число байт. Возникает вопрос, каким способом формировать первый байт. Кроме этого, каждый байт можно перевернуть при вычислении CRC. Более того, теоретически переворачиваться могут не только биты в байте, но и байты в парах (словах).
Переберём всё это грубой силой. Для грубой силы мы использовали Python и библиотечку crcmod.
import crcmod
samples = [
#~ ['x00x00x01' + chr(0b11110000), chr(0b11011010)],
#~ ['x00x00x03' + chr(0b11110000), chr(0b10010101)],
[chr(0b11110000) + 'x01x00x00', chr(0b11011010)],
[chr(0b11110000) + 'x03x00x00', 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, т.е. переворачиваются.
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 с помощью нашего кода разбора протокола.
Что получилось:
Видно, что препоследний байт, который был нулевым в наших экспериментах, — это на самом деле выбор формата. Мы наблюдали fmt=0, кроме этого возможны значения 1, 3 и 4.
Формат 1 используется для команды установки яркости, при этом в начало пакета добавляется один байт со значением яркости.
Формат 3 используется для команды установки цвета, в начало пакета добавляется 4 байта. Первые три задают компоненты цвета, четвёртый всегда нулевой, его значение непонятно (видимо зарезервирован).
Формат 4 используется для команд переключения режима. В этом формате в начало пакета после команды добавляется почему-то 4 бита аргумента. Весь пакет при этом сдвигается для вычисления контрольной суммы, т.е. байты отсчитываются с левой границы, оставшиеся биты дополняются нулями до байта.
Итого
Итого, мы полностью разобрались, как работает протокол Noolite. Что можно сказать про протокол:
- Используется преамбула и манчестерское кодирование, это есть хорошая практика
- Используется всего 16 бит адреса. Это конечно лучше, чем 4-8 бита в совсем дешёвых китайских радиокомплектах, но всё равно довольно мало. Например, новые китайские микросхемы для радиопультов имеют длину кода в 21 бит. 16 бит — это всего 65536 возможных комбинаций, что позволяет перебрать их довольно быстро. При наличии мощного передатчика и хорошей антенны, можно например запустить отправку команды на выключение света в цикле со всеми возможными адресами. Такая штука переберёт все адреса за пару часов и гарантировано выключит свет у всех ваших соседей. А так как связь односторонняя, то количество соседей, которым вы можете выключить свет, ограничено только мощностью вашего передатчика.
- Избыточность в протоколе реализуется за счёт повторной отправки пакета и контроля его целостности. Наличие контроля целостности — это безусловное преимущество протокола. С другой стороны, избыточное кодирование можно было бы реализовать и более компактным способом, хотя в случае передачи коротких пакетов по радио это может и не являться преимуществом
- В протоколе полностью отсутствует какая-либо защита. В данном случае, вполне можно было бы использовать код типа KeeLoq — это код, который, грубо говоря, основан на синхронизации счётчиков в передатчике и приёмнике, используется в автомобильных брелоках. Реализовать что-то подобное можно даже на железе, которое сейчас используется в NooLite
- Используется довольно странная структура пакета. Выбор формата (т.е. в том числе и длины посылки) кодируется последним байтом пакета. Таким образом, разделять пакеты приходится на физическом уровне. Более логичным (и часто используемым) является кодирование длины пакета в самом начале этого пакета. Также не очень понятно, зачем пакет обрезан не по границе байт. Довольно странная экономия на трёх битах, учитывая то, что под код формата отводится целых 8 бит.
P.S.
Производители NooLite собираются выпустить модуль MT1132 с интерфейсом UART, который предназначен для использования с Arduino и т.п. или в собственных устройствах.
До нас доходили слухи, что стоить он будет несколько адекватнее, чем родные USB-брелоки.
Неприкрытая реклама
Собственно протокол NooLite, как вы наверное уже поняли, мы разбирали, чтобы добавить его поддержку в Wiren Board Smart Home — нашем контроллере домашней автоматизации с линуксом на борту и большими возможностями по подключению периферии, который, в частности, имеет универсальный приёмопередатчик на частоте 433MHz.
Заказ на первую партию мы отправляем на завод в эту среду, поэтому мы решили продлить предзаказ на три дня до вторника, 18 марта, включительно. Купить контроллер (отгрузка в конце апреля — начале мая) можно у нас в магазине.
Автор: evgeny_boger