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

16 октября 2017

Из этой заметки вы узнаете, как своими руками сделать пульт для презентаций (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. Ну и, как обычно, я буду рад любым вашим вопросам и дополнениям.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.