- PVSM.RU - https://www.pvsm.ru -
Привет!
В нашей предыдущей статье [1] мы показывали, как наш контроллер для домашней автоматизации [2] работает с устройствами системы NooLite без использования родных USB-донглов для передачи и приёма сигнала. Чтобы это стало возможно, мы провели реверс-инжиниринг протокола NooLite.
Собственно эта статья содержит описание протокола и рассказывает, как именно мы его разбирали.
Про систему NooLite на хабре много писали [3] раньше. Система состоит из беспроводных пультов и исполнительных устройств, которые работают на частоте 433.92 MHz.
Связь в системе односторонняя — пульт отправляет данные по радио, а исполнительные устройства его принимают и обрабатывают. Обратного канала нет, а следовательно, нет ни подтверждения получения сообщения, ни контроля статуса исполнительных устройств.
Отдельного внимания заслуживает схема связи пультов и исполнительных устройств в Noolite. В системе у каждого пульта имеется один либо несколько адресов. Эти адреса зашиты в пульты при производстве и отличаются для каждого пульта. Каждый исполнительный блок можно привязать к одному или нескольким пультам (точнее адресам), при этом блок сохраняет в памяти список разрешённых адресов, от которых можно принимать сигналы управления.
Привязка осуществляется следующим образом: сначала на исполнительном блоке надо нажать специальную (единственную) кнопку, после чего лампочка на блоке начинает часто мигать, сигнализируя о входе в режим программирования. Затем в режим программирования, опять же с помощью специальной кнопки, надо перевести пульт, который требуется привязать. После этого надо нажать на любую кнопку на пульте, который отправит специальный сигнал привязки. Исполнительный блок получает этот сигнал, после чего надо подтвердить занесение его в память ещё одним нажатием кнопки программирования.
Отвязка пульта от блока делается похожим образом.
Интересным в этой схеме является то, что уникальные идентификаторы в системе имеют не исполнительные блоки, а пульты. Соответственно команды от пульта имеют вид «я пульт с адресом NNN, отправляю команду XXX». Все блоки, настроенные «слушать» данный адрес NNN выполняют после получения такого сообщения команду XXX.
Мы конечно же начали с того, что разобрали пульты Noolite.
(до и после)
(разные другие фотографии разобранных и собранных устройств можно найти в упомянутой статье [3])
Что видно внутри?
Внутри на плате видно три большие сенсорные площадки, реализующие сенсорные кнопки.
В правой верхней четверти на плате нарисована антенна на 433MHz.
Большая круглая штука слева — литиевая батарейка. К её минусу мы припаяли для удобства провод.
Схема справа реализует всё логику и передающую часть. Микросхема в корпусе с 8 ножками — микроконтроллер PIC12F.
Справа от микроконтроллера видна кнопка, а ещё правее вся аналоговая часть.
Аналоговая часть — это передатчик, передающий данные в т.к. называемой OOK-модуляции. Передатчик полностью аналоговый и собран на дискретных компонентах. Несущая частота в 433.92 MHz задаётся SAW-резонатором [4], который на схеме обозначен ZQ1.
В подобных девайсах в подавляющем большинстве случаев используется так называемая OOK-модуляция [5]. OOK расшифровывается как «on-off keyring», т.е. такой подвид амплитудной модуляции, при котором модуляция фактически осуществляется включением-выключением передатчика. Таким образом, наличие в определённый момент времени сигнала на частоте 433.92MHz означает логическую единицу, а отсутствие — логический ноль.
Как писалось выше, схема радио-передатчика очень простая и полностью аналоговая. Цифровой сигнал (последовательность нулей и единиц, которая отправляется по радио) выходит с ножки, отмеченной красным на фото. Соответственно, наличие напряжения на ножке включает передатчик, отсутствие — выключает.
Данные мы решили снимать не с помощью радио-приёмника, а просто записав сигнал с ножки цифрового выхода микросхемы. Такой подход заметно проще, так как с цифровым сигналом можно работать напрямую и он свободен от всякого рода помех.
Снимать сигнал будем логическим анализатором. Грубо говоря, логический анализатор — это осциллограф, который имеет однобитный АЦП, т.е. может различить только наличие и отсутсвие напряжения в канале. Применяются логические анализаторы, как ни странно, для анализа цифровых сигналов.
В работе мы пользуемся логическим анализатором Open Bench Logic Sniffer [6] — это открытый проект с открытой прошивкой и открытым софтом. Стоит всего $50 и имеет приличную скорость захвата в 50 миллионов семплов в секунду. Испольуземое клиентское приложение OLS [7] — написано на java и кроссплатформенное.
В приципе, для целей этой статьи подошли бы и гораздо более дешёвые логические анализаторы на чипе Cypress CY7C68013A (искать по ключевым словам «saleae clone» или «usbee clone»), которые стоят у китайцев что-то вроде $7.
(Макетка с собственно логическим анализатором, ардуинкой и радиомодулем RFM69H, используемом в Wiren Board Smart Home [2])
Итак, сигнал с отмеченной красным ножки мы вывели на первый канал логического анализатора, соединили земли и настроили триггер в клиенте анализатора. Вот что вышло:
На картинке — дамп пакета, который пересылает по радио пульт NooLite при нажатии кнопки.
Что можно сказать по такой картинке? На самом деле довольно много:
Минимальная длина единичек и ноликов одинакова и составляет 500us. Данные передаются на физическом уровне на частоте 2000 bit/s.
В начале пакета передачи виден большой кусок прямоугольного меандра. Это так называемая преамбула, которая помогает приёмнику подстроить частоту передачи данных (битрейт) под конкретный приёмник. И приёмник и передатчик используют встроенные RC-генераторы для тактирования микроконтроллера, поэтому «2000 бод» у приёмника и передатчика могут отличаться процентов на 10.
После преабмулы начинаются данные. Данные следуют весьма узнаваемому паттерну манчестерского кодирования [8]: в коде встречаются только пары длинный ноль-длинная единица и короткий ноль-короткая единица. «Длинные» участки с постоянным уровнем ровно в два раза длиннее коротких, т.е. представляют собой два «физических» бита. В посылке отсутствуют последовательности нулей или единиц длиннее 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 в начале меняются между программированием и норм. режимом и в кнопках
# на отпускание посылается одна и та же команда
Нажимаем разные кнопки на разных пультах, записываем, что получается. В процессе мы заметили, что отправка команд на диммирование производится оригинальным способом: при начале нажатия кнопки пульт отправляет одну команду («начать увеличивать яркость»), при отпускании отправляет ещё одну («закончить увеличивать яркость»).
Собираем статистику в табличку:
Некоторую подсказку даёт документация [9] на оригинальные модули Noolite для компьютера.
Сначала возьмём из документации коды команд и попробуем найти их в потоке бит. Код команды обнаруживается в 4-х битах, начиная со второго. Код записывается по схеме LSB [10], т.е. первой значащей цифрой назад.
Кроме команды легко выделяются два байта адреса (остаются неизменными для любых команд одного пульта) и заполненный нулями байт «аргумента». При повторных посылках одной и той же команды между двумя возможными значениями меняется значение первого бита и 8-ми последних бит.
Все посылки имеют постоянную чётность, т.е. xor всех бит даёт одну и ту же величину. Такое поведение намекает на наличие контрольной суммы.
Первая идея заключалась в том, что в качестве контрольной суммы используется контроль чётности, который записывается в первый бит посылки. В пользу этой версии говорила сохраняющаяся чётность посылки и то, что первый бит стоит отдельно от других бит в пакете.
Однако, попытки выяснить значение последнего байта не привели ни к чему хорошему.
Следующая идея, оказавшаяся верной, заключалась в том, что контрольная сумма — это 8 последних бит. Первый же бит используется для того, чтобы приёмник мог отличить два последовательных нажатия кнопки от одного (как мы говорили, при одном нажатии посылка повторяется несколько раз для надёжности).
Первый кандидат в контрольную сумму — это конечно CRC [11]. Однако, алгоритм 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(). Длительность импульсов регулируется задержкой.
После того, как мы проверили, что можем воспроизвести один из записанных пакетов, и блок ноолайта его принимает за свой, мы начали дальнейшие эксперименты.
Как было написано выше, нужно было понять алгоритм генерации контрольной суммы.
У алгоритма CRC есть хорошее свойство, которое можно проверить: эта контрольная сумма линейна по аргументам.
Таким образом, если взять два известных пакета, сложить их побитово (т.е. сделать xor), то получившийся пакет будет иметь верную контрольную сумму!
Выбрав два подходящих пакета, проверяем это предположение и подтверждаем, что чексумма — линейна по аргументам.
Следующим нашим действием был брутфорс контрольных сумм. С помощью бинарного поиска для заданной последовательности бит контрольная сумма подбирается за несколько минут. Делается это очевидным образом: запускаем скетч, перебирающий контрольные суммы от 1 до 128 и смотрим на приёмник. Если за время работы скетча он моргнул (получил валидный пакет), то мы знаем, что искомая контрольная сумма где-то от 1 до 128. И т.д.
Теперь, когда мы умеем определять контрольные суммы произвольного пакета, можно попробовать восстановить функцию. Т.к. CRC линейна по аргументам, то, зная как чексумма меняется при изменении каждого бита, можно восстановить функцию.
Чтобы восстановить функцию, надо подобрать чексуммы к количеству пакетов равному количеству бит в пакете, т.е. к 29 пакетам.
Делается это всё медленно и успело порядком надоесть. Так что, пройдя больше половины бит, мы решили быстро попробовать перебрать чексуммы в оффлайне.
Как писалось выше, у CRC есть несколько входных параметров: полином (8-бит), начальное значение (8-бит), значение, которое добавляется в конце (8-бит). Кроме этого, 1 бит — задаёт является ли алгоритм инвертированным или нет.
Кроме этих параметров, по-разному можно подготавливать байты, которыми оперирует алгоритм CRC. В посылке noolite 29 бит, т.е. нецелое число байт. Возникает вопрос, каким способом формировать первый байт. Кроме этого, каждый байт можно перевернуть при вычислении CRC. Более того, теоретически переворачиваться могут не только биты в байте, но и байты в парах (словах).
Переберём всё это грубой силой. Для грубой силы мы использовали Python и библиотечку crcmod [12].
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 [13] иина dima117 [14] за оперативно предоставленные для этого устройства)
При наличии родного модуля, отправлять команды можно, например, с помощью вот этой программы [15].
Кроме команды установки заданной яркости, бывают ещё команды, управляющие режимом переключения яркости и цвета у RGB-блоков NooLite, а также устанавливающие значение цвета (RGB).
В этот раз, перехватывать команды мы будем с помощью пакетного радио RFM69H, которое установлено в Wiren Board Smart Home [2] с помощью нашего кода разбора протокола [16].
Что получилось:
Видно, что препоследний байт, который был нулевым в наших экспериментах, — это на самом деле выбор формата. Мы наблюдали fmt=0, кроме этого возможны значения 1, 3 и 4.
Формат 1 используется для команды установки яркости, при этом в начало пакета добавляется один байт со значением яркости.
Формат 3 используется для команды установки цвета, в начало пакета добавляется 4 байта. Первые три задают компоненты цвета, четвёртый всегда нулевой, его значение непонятно (видимо зарезервирован).
Формат 4 используется для команд переключения режима. В этом формате в начало пакета после команды добавляется почему-то 4 бита аргумента. Весь пакет при этом сдвигается для вычисления контрольной суммы, т.е. байты отсчитываются с левой границы, оставшиеся биты дополняются нулями до байта.
Итого, мы полностью разобрались, как работает протокол Noolite. Что можно сказать про протокол:
Производители NooLite собираются выпустить модуль MT1132 [18] с интерфейсом UART, который предназначен для использования с Arduino и т.п. или в собственных устройствах.
До нас доходили слухи, что стоить он будет несколько адекватнее, чем родные USB-брелоки [19].
Собственно протокол NooLite, как вы наверное уже поняли, мы разбирали, чтобы добавить его поддержку в Wiren Board Smart Home [2] — нашем контроллере домашней автоматизации с линуксом на борту и большими возможностями по подключению периферии, который, в частности, имеет универсальный приёмопередатчик на частоте 433MHz.
Заказ на первую партию мы отправляем на завод в эту среду, поэтому мы решили продлить предзаказ на три дня до вторника, 18 марта, включительно. Купить контроллер (отгрузка в конце апреля — начале мая) можно у нас в магазине [20].
Автор: evgeny_boger
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/news/57179
Ссылки в тексте:
[1] предыдущей статье: http://habrahabr.ru/company/contactless/blog/214381/
[2] контроллер для домашней автоматизации: http://habrahabr.ru/company/contactless/blog/213243/
[3] писали: http://habrahabr.ru/company/boxowerview/blog/168039/
[4] SAW-резонатором: http://www.rfm.com/products/data/ro3101e-11.pdf
[5] OOK-модуляция: http://en.wikipedia.org/wiki/On-off_keying
[6] Open Bench Logic Sniffer: http://dangerousprototypes.com/docs/Open_Bench_Logic_Sniffer/ru
[7] OLS: http://www.lxtreme.nl/ols/
[8] манчестерского кодирования: http://en.wikipedia.org/wiki/Manchester_code
[9] документация: http://www.noo.com.by/assets/files/PDF/MT1132.pdf
[10] LSB: http://en.wikipedia.org/wiki/Least_significant_bit
[11] CRC: http://en.wikipedia.org/wiki/Cyclic_redundancy_check
[12] crcmod: http://crcmod.sourceforge.net/crcmod.html
[13] thinking-home.ru: http://thinking-home.ru
[14] dima117: http://habrahabr.ru/users/dima117/
[15] вот этой программы: https://github.com/ermolenkom/noolite
[16] кода разбора протокола: https://github.com/contactless/rfm69-linux/blob/master/noolite.py
[17] KeeLoq: https://en.wikipedia.org/wiki/KeeLoq
[18] модуль MT1132: http://thinking-home.ru/news/28.aspx
[19] USB-брелоки: http://thinking-home.ru/product/29.aspx
[20] у нас в магазине: http://contactless.ru/store/#!/~/product/category=8751035&id=34097189
[21] Источник: http://habrahabr.ru/post/216023/
Нажмите здесь для печати.