← На главную

Декодируем сигнал с OOK-модуляцией и паяем кликер

Из этой заметки вы узнаете, как своими руками сделать пульт для презентаций (a.k.a кликер) из Arduino Leonardo и дешевого радиомодуля на 433 МГц. Помимо прочего, этот проект интересен тем, что в нем реализовано декодирование сигнала с OOK-модуляцией, чему при желании можно найти массу практических применений. Также в проекте утилизируется возможность микроконтроллера ATmega32U4 мастерски притворяться мышью или клавиатурой.

Примечание: Если вы пропустили заметку О работе пультов и радиомодулей на 433 МГц, сначала вам может иметь смысл прочитать ее. Помимо прочего, там с наглядными картинками объясняется, что такое OOK-модуляция. Если же это вы и так знаете, тогда смело читайте дальше.

В собранном виде устройство выглядит как-то так:

DIY пульт для презентаций

На фото слева находится Arduino Leonardo с воткнутым нее Proto Shiled. На шилде крепится уже знакомый нам радиомодуль, а также SMA-разъем для антенны. Это, собственно, основная часть кликера, то есть, та, что подключается к компьютеру. Пульт хоть и не сложно сделать самому, лично мне это не показалось слишком интересным проектом. Поэтому я воспользовался уже готовым пультом с eBay. На фото он изображен справа.

Что же касается кода прошивки, он у меня получился таким:

#include <Arduino.h> #include <Keyboard.h> #define DATA_PIN A0 #define PACKAGE_SIZE 25 // # of short/long signals #define COMMAND_DOWN 1 #define COMMAND_STOP 2 #define COMMAND_UP 3 #define COMMAND_LOCK 4 char package[PACKAGE_SIZE]; const char dipCode[] = { 1, 1, 0, 1, 0, 0, 1, 0 }; // Wait until next package // If we see a low voltage for a long time we consider it a sync void sync() { unsigned long startTime, endTime; do { startTime = millis(); while(analogRead(DATA_PIN) < 600) { delayMicroseconds(3); } endTime = millis(); } while(endTime - startTime < 7); } // Try to receive a new package into the package[] array // Returns true on success and false on error bool readPackage() { unsigned long startTime, highStart, highEnd; bool firstLoop = true; int delayCtr; startTime = millis(); for(int sigNum = 0; sigNum < PACKAGE_SIZE; sigNum++) { if(!firstLoop) { // wait until high signal delayCtr = 0; while((analogRead(DATA_PIN) < 300) && (delayCtr < 500/3)) { delayMicroseconds(3); delayCtr++; } if(delayCtr == 500/3) return false; // too long low signal } firstLoop = false; highStart = micros(); // wait until low signal delayCtr = 0; while((analogRead(DATA_PIN) >= 300) && (delayCtr < 1100/3)) { delayMicroseconds(3); delayCtr++; } if(delayCtr == 1100/3) return false; // too long high signal highEnd = micros(); // long: 1000 us, short: 330 us package[sigNum] = highEnd - highStart > 500 ? 1 : 0; } if(millis() - startTime > 33) return false; // package too long return true; // package received } // Check if package has a valid format bool checkPackage() { bool hasZeroes = false; bool hasOnes = false; if(package[PACKAGE_SIZE - 1] != 0) return false; for(int i = 0; i < PACKAGE_SIZE/2; i++) { if(package[i*2] != package[i*2 + 1]) return false; hasZeroes = hasZeroes || (package[i*2] == 0); hasOnes = hasOnes || (package[i*2] == 1); } return hasZeroes && hasOnes; } // Check if DIP code is correct bool checkDipCode() { for(uint16_t i = 0; i < sizeof(dipCode); i++) if(package[i*2] != dipCode[i]) return false; return true; } // Get command number or 0 in case of error int readCommand() { uint16_t i = sizeof(dipCode); int cmdNum = 1; for(; i < PACKAGE_SIZE/2; i++, cmdNum++) { if(package[i*2] == 1) return cmdNum; } return 0; } void setup() { Serial.begin(9600); Keyboard.begin(); } void loop() { sync(); if(readPackage() && checkPackage() && checkDipCode()) { Serial.print("Good: "); for(int i = 0; i < PACKAGE_SIZE; i++) if(package[i] == 1) Serial.print("1"); else Serial.print("0"); int command = readCommand(); Serial.println(", command: " + String(command)); if(command == COMMAND_DOWN) { Keyboard.press(KEY_RIGHT_ARROW); delay(150); } else if(command == COMMAND_UP) { Keyboard.press(KEY_LEFT_ARROW); delay(150); } Keyboard.releaseAll(); } }

Как видите, мудрить что-то с прерываниями я не стал, так как в фоне устройство все равно ничего не делает. Подробно разжевывать код я, пожалуй, не буду, так как его не много, он достаточно прост и, к тому же, снабжен комментариями.

Интересно, что код работает не только с изображенным на фото модулем, но также и с альтернативными модулями, вроде таких. Интересно также, что в плане себестоимости этот конкретный проект совершенно себя не оправдывает. По моим подсчетам, суммарная стоимость использованных в нем компонентов составила около 16$. В то же время, готовый кликер можно купить на AliExpress где-то за 7$, особенно если найти магазин с распродажей. Впрочем, как уже отмечалось, куда больший интерес представляет не столько сам проект, сколько широкие возможности, открываемые перед нами умением декодировать сигнал с OOK-модуляцией.

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