ИК-контроллер бытового кондиционера или реверс-инжиниринг пульта ДУ

в 9:51, , рубрики: Без рубрики

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

Итак, у меня имеется сплит-система Panasonic CS-XE9DKE. Идея управлять кондиционером сводилась к созданию «своего» ИК-пульта. Назовем его «ИК-контроллер». А уж управлять самим контроллером — это задача десятая. И это зависит от конкретного места применения. Мне, например, было бы удобнее управлять с домашнего сервера через WizFi200(только вот достать его за приемлемые деньги проблематично). Моим родителям — с помощью отправки SMS(на SIM900) на номер контроллера. Возможны и другие варианты. Но это не главное, о чем я хотел рассказать.

Реверс-инжиниринг

Так как же родной пульт передает команду? Имея в наличии ИК-приемник TSOP17XX я начал анализировать поток данных от пульта. Выяснилось, что пульт шлет 2 посылки с небольшой паузой. Первая — заголовок, вторая — команда. Заголовок был всегда одинаковый.

Ознакомившись с существующими системами кодирования ИК-сигналов(RC5, RC6, NEC, JVC и другие) стало ясно, что здесь применяется что-то другое, хотя принцип схож.

Сингал выглядит следующим образом:
(активный уровень — низкий)

image

(значения бит для примера, время в мкс приблизительно)

В начале идет пилотный сигнал, за ним примерно вдвое короче второй пилотный(пауза), за ним стартовый бит длительностью примерно в 8 раз короче чем первый пилотный. Далее начинается битовый поток. Один бит кодируется длительностью паузы(высокий уровень). Если пауза одинарная — это НОЛЬ, если пауза тройная — это ЕДИНИЦА.
Всего при одном нажатии кнопки пульт шлет 2 таких посылки. Первая — заголовок, вторая — команда. Пауза между посылками около 10 мс.

Таким образом заголовок содержит 64 бита, или 8 байт. Сначала идут младшие биты.

Поток данных заголовка выглядит следующим образом:

0100000000000100000001110010000000000000000000000000000001100000
Или в байтах(HEX): 02 20 E0 40 00 00 00 06

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

Теперь немного отвлечемся на сам пульт. Пульт имеет следующие кнопки:

  • ON/OFF(включение/выключение)
  • ± (настройка температуры)
  • O2 (генератор кислорода)
  • ion (ионизатор)
  • quiet (тихий режим)
  • mode ( auto, heat, cool, dry, fan)
  • fan speed
  • swing <>(горизонтальное направление)
  • swing ^v (вертикальное направление)

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

3 кнопки, которые отличаются от других, это:

  • ion
  • quiet
  • O2

Смысл в том, что эти кнопки не посылают все настройки, а включают/выключают конкретный режим. Т.е. Эти 3 режима можно включать/выключать в любом состоянии кондиционера.

Теперь вернемся к протоколу передачи. Любой команде предшествует заголовок.
Байты заголовка: 02 20 E0 40 00 00 00 06
Далее идет вторая посылка с командой.
Команды трех особых кнопок:
ion: 02 20 E0 04 80 48 33 01
oxyg: 02 20 E0 04 80 50 33 09
quiet: 02 20 E0 04 80 81 33 3A
Отмечу, что, как и в заголовке, последний байт — контрольная сумма.

А вот все остальные кнопки посылают пакет со всеми настройками сразу. И формат этого пакета следующий:

Поток бит:
0100000000000100000001110010000000000000PNF1mmm00ccccc0000000001vvvvFFFF
hhhh0000nnnnnnnnnnn0fffffffffff0000010000000000010000000ttttttttttt00000ssssssss

Расшифровка полей по порядку следования:
P — 1, если нажата кнопка ON/OFF. При нажатии других кнопок здесь 0.
N — 1, если установлен таймер включения
F — 1, если установлен таймер выключения
mmm — режим(Mode). Auto — 0, heat — 4, cool — 3, dry — 2, fan — 6
ccccc — температура. От 16 до 30
vvvv — вертикальное направление. Auto — 15, 1 — под потолок, … 5 — в пол.
FFFF — скорость вентилятора. Auto — 10, F1 — 3, F2 — 4, F3 — 5, F4 — 6, F5 — 7
hhhh — горизонтальное направление. Auto — 13, | | — 6, / / — 9, / | — 10, | — 11, — 12
nnnnnnnnnnn — время включения. Номер минуты в сутках. (например 16:00 = 960)
fffffffffff — время выключения. Номер минуты в сутках.
ttttttttttt — текущее время.
ssssssss — контрольная сумма посылки.

Если таймер включения не установлен, то время включения д.б. 1536 = 0x600 = 0b11000000000. С временем таймера выключения аналогично. По крайней мере так шлет родной пульт.

Разделим поток бит на байты и развернем биты:

00000010 02
00100000 20
11100000 E0
00000100 04
00000000 00
0mmm1FNP Mode<<4 + 8 + FlagOFF<<2 + FlagON<<1 + Power
00ccccc0 Temperature<<1
10000000 80
FFFFvvvv Fan<<4 + Vert
0000hhhh Horiz
nnnnnnnn OnTime%0xFF
ffff0nnn OnTime>>8 + OffTime%0x0F
0fffffff OffTime>>4
00010000 10
00000000 00
00000001 01
tttttttt Time%0xFF
00000ttt Time>>8
ssssssss Sum

Итого команда 19 байт.

Контроллер

Для тестирования я собрал схему(позаимствовал здесь) на макетке для беспаечного монтажа. Компоненты:

  • atmega8
  • ИК-диод
  • транзистор кт361
  • резистор 470 на базу транзистора
  • резистор 220 ограничивает ток диода
  • резистор 10к на RESET

Отличие моей схемы от схемы по ссылке — светодиод подключен к PB1, а база транзистора к PB3.

При отпускании кнопки на порту PD7 запускается таймер 1 в режиме CTC с выводом сигнала на PB1/OC1A, а так же таймер 2 с выводом на PB3/OC2 для несущей частоты, на которой и модулируется основной сигнал. Таймеры работают с предделителем 8.

Прошивка

Исходный код для AVR Studio 4.

#include <avr/io.h>
#include <avr/interrupt.h>

#define F_CPU 8000000

#define LED_CARIER DDB3
#define LED_SIGNAL DDB1

#define BASE 480
#define PAUSE 10000
#define START_1 BASE*8
#define START_2 BASE*4
#define DELIM 520
#define PULSE1 BASE*3
#define PULSE0 BASE


unsigned char state = 0;
unsigned char byteIndex = 0;
unsigned char frameIndex = 0;
unsigned char bit = 0;
unsigned char phase = 0;
unsigned char packetLen = 0;
unsigned char process = 0;

char header[] = {0x02, 0x20, 0xE0, 0x04, 0x00, 0x00, 0x00, 0x06};
char cmd_ion[] = {0x02, 0x20, 0xE0, 0x04, 0x80, 0x48, 0x33, 0x01};
char cmd_oxygen[] = {0x02, 0x20, 0xE0, 0x04, 0x80, 0x50, 0x33, 0x09};
char cmd_quiet[] = {0x02, 0x20, 0xE0, 0x04, 0x80, 0x81, 0x33, 0x3A};

char* data;
char* cmd;
unsigned char cmdLen;

void stop();

ISR(SIG_OUTPUT_COMPARE1A) {
	switch (state) {
		case 0:
			OCR1AH = START_2 / 256;
			OCR1AL = START_2 % 256;
			state = 1;
			break;
		case 1:
			OCR1AH = DELIM / 256;
			OCR1AL = DELIM % 256;
			state = 2;
			byteIndex = 0;
			frameIndex++;
			if (frameIndex == 1) {
				data = header;
				packetLen = 8;
			} else {
				data = cmd;
				packetLen = cmdLen;
			}
			bit = 0;
			phase = 0;
			break;

		case 2:
			if (byteIndex < packetLen) {
				if (phase == 0) {
					if (data[byteIndex]&(1 << bit)) {
						OCR1AH = PULSE1 / 256;
						OCR1AL = PULSE1 % 256;
					} else {
						OCR1AH = PULSE0 / 256;
						OCR1AL = PULSE0 % 256;
					}
				} else {
					OCR1AH = DELIM / 256;
					OCR1AL = DELIM % 256;
					bit++;
					if (bit == 8) {
						bit = 0;
						byteIndex++;
					}
				}
				phase = 1 - phase;
			} else {
				OCR1AH = PAUSE / 256;
				OCR1AL = PAUSE % 256;
				if (frameIndex == 2) {
					stop();
				} else {
					state = 3;
				}
			}
			break;

		case 3:
			OCR1AH = START_1 / 256;
			OCR1AL = START_1 % 256;
			state = 0;
			break;
	}
}

void start() {
	state = 0;
	byteIndex = 0;
	frameIndex = 0;
	packetLen = sizeof (header);
	TCNT1H = 0;
	TCNT1L = 0;
	OCR1AH = START_1 / 256;
	OCR1AL = START_1 % 256;
	TCCR1A = (1 << COM1A0);
	TCCR1B = (1 << WGM12) | (1 << CS11);

	TCNT2 = 0;
	OCR2 = 13;
	TCCR2 = (1 << WGM21) | (1 << COM20) | (1 << CS21);

	TIMSK = (1 << OCIE1A);
	sei();
}

void stop() {
	cli();
	TCCR2 = (1 << WGM21) | (1 << COM20);
	TCCR1A = (1 << FOC1A);
	TCCR1B = (1 << WGM12) | (0 << CS12) | (0 << CS11) | (0 << CS10);
	TIMSK = 0;
	PORTB = (1 << LED_SIGNAL) | (1 << LED_CARIER);
	process = 0;
}

int main(void) {

	process = 0;
	PORTB = (1 << LED_SIGNAL) | (1 << LED_CARIER);
	DDRB = (1 << LED_SIGNAL) | (1 << LED_CARIER);

	PORTD = (1 << 7); // PD7 input, pull up
	DDRD = 0;

	while (1) {
		int but = (PIND & (1 << PD7));
		if (process == 0 && but == 0) {
			process = 1;
		} else if (process == 1 && but != 0) {
			process = 2;
			cmd = cmd_oxygen;
			cmdLen = 8;
			start();
		}
	}
}

Вывод

Текущая тестовая реализация пока умеет слать заголовок и фиксированную команду. Но вот тут у меня возник вопрос — а какая должна быть у этого контроллера глубина реализации? Ведь можно контроллеру сказать «включи охлаждение на 23 градуса», а можно просто сказать «передай команду» и передать весь поток бит с уже сформированной командой. А ведь можно еще и тайминги указать. Тогда можно управлять через один контроллер-излучатель различными устройствами. Надеюсь в комментариях вы мне поможете принять решение.
Планируется также подключение пары датчиков температуры для контроля температуры помещения и выходного потока воздуха из кондиционера. Это удобно для обратной связи(например оповещение по смс с текущими параметрами климата) и контроля(а включился ли кондиционер?).

Автор: karakum22

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js