piątek, 5 sierpnia 2016

Pilot IR (NEC) z odbiornikiem HX1838. Obsługa w Arduino.

Pilot IR wraz z odbiornikiem HX1838, działający w oparciu o protokół NEC umożliwia tanią, wygodną i bezprzewodową możliwość sterowania urządzeniami. Dodatkowo jest prosty w implementacji w kodzie, gdyż mamy do dyspozycji dostępne biblioteki napisane pod Arduino, takie jak IRemote, czy IRLib opatrzone wieloma, przystępnymi przykładami użycia.


Odbiornik i piloty - szary (po lewej) i biały (po prawej)

Wymieniony w temacie pilot kupiłem z myślą o sterowaniu radiem TEA5767. Jak dotąd do sterowania (wprowadzania danych) korzystałem z takich elementów  jak przycisk, enkoder, czy całkiem wygodny joystick 5 pozycyjny (tj. z enterem). Do sterowania radiem jednak potrzebowałem czegoś bardziej wyszukanego i myślę, że taki pilot całkiem dobrze sprawdził się w tej roli. Zakupione piloty nie są tak wygodne jak te od telewizora, ale przynajmniej kompaktowe. Szary z nich dostałem z HX1838, drugi biały dokupiłem oddzielnie, gdyż wydawało mi się, że jego przyciski odpowiadają lepiej specyfice sterowania radiem. Mam na myśli to, że będzie się nim intuicyjniej sterowało.


Parametry

Poniżej podaję parametry zestawu, wyczytane ze strony sprzedawcy pilota oraz chińskiej dokumentacji HX1838.

Parametry nadajnika:
- zasilanie baterią CR2025 (dostarczona z pilotem)
- zasięg 5-8m
- standard nadawania NEC
- nośna 38kHz

Parametry odbiornika:
- napięcie zasilania od 2,7V do 5,5V
- kąt odbioru 60 stopni
- pobierany prąd 0,8-1,5mA
- LED sygnalizująca zasilanie

Odbiornik


Schemat elektroniczny układu z odbiornikiem
Odbiornik HX1838 otrzymałem oddzielnie wraz z płytką z kilkoma elementami. Można go zarówno wlutować, jak i po prostu włożyć w dedykowane gniazdo. Ja wybrałem drugie rozwiązanie. W wolne pola lutowniczne wlutowałem natomiast brakujący kondensator elektrolityczny 10uF (22uF jak na schemacie akurat nie miałem). Jego zadaniem, jak przypuszczam, jest odfiltrowanie zakłóceń napięcia zasilania.


Co wysyła pilot?

Jak pisałem wcześniej sprawdzałem "biały" pilot, ale oba działają w oparciu o protokół NEC. Oto wnioski:

1) Wysyłają oczywiście kody odpowiadające wciśniętym przyciskom. Są to sześciocyfrowe liczby rozpoczynające się od 0xFF. Kolejne cyfry odpowiadają za identyfikację przycisku. Na przykład przycisk "Power" posiada kod 0xFFA25D. Precyzując, właściwy kod przycisku stanowi liczba złożona z 3 i 4-tej cyfry, czyli A2. Z kolei liczba złożona z cyfry 5 i 6-tej (5D) stanowi jej negację, czyli:
!0xA2 = 0x5D


Kody przycisków testowanego pilota

2) Jeśli przycisk jest przytrzymany przez 108ms (i więcej), to przesyłany jest zgodnie z protokołem NEC kod powtórzenia 0xFFFFFFFF - także co 108ms aż do puszczenia przycisku. Z moich spostrzeżeń wynika, że nawet przy jednorazowym i szybkim wciśnięciu danego przycisku jest duża szansa, że zaraz po kodzie przycisku zostanie wysłany kod powtórzenia. Tak więc, w zależności od potrzeb można ignorować ten kod lub zapamiętywać ostatnio wybrany kod i wtedy jest wiadomo, jaki przycisk został powtórnie wciśnięty czy też przytrzymany.

3) Niestety wysyłają także błędne kody odpowiadające wciśniętym przyciskom, tym razem 8-mio cyfrowe i nie rozpoczynające się do 0xFF. Obrazowo mówiąc, przychodziły co pewien czas dla każdego z przycisków. Wyjątkiem był 'Previous', ale to być może mi po prostu nie udało się znaleźć takiego kodu, bo za mało go używałem. Szczęśliwie, kody te są powtarzalne, tak więc można je wykryć i obsługiwać tak samo jak standardowe kody przycisków.


Co z błędnymi kodami?

Pojawiały się przy użyciu biblioteki IRemote (ale bynajmniej nie z powodu jej użycia). Po uruchomieniu debugowania okazało się, że przy problematycznych kodach nie zostało rozpoznane dekodowanie NEC i program kończył działanie na dekodowaniu Denon, wynikiem czego był ten drugi, inny kod. Z logów generowanych przez IRemote wynikało, że dla NEC nie przechodziła walidacja przesyłanego sygnału (rozjazd w czasach pojawiania się kolejnych impulsów w sygnale niezgodny z protokołem NEC).
Postanowiłem sprawdzić przy tej okazji inną bibliotekę o nazwie IRLib. W jej przypadku dla błędnych kodów otrzymywałem po prostu zero.

Przy obsłudze kodu, można ignorować te nadmiarowe kody (jako błąd) albo traktować jak standardowy kod przycisku. Ja wybrałem to drugie rozwiązanie. Przy włączaniu radia lub zmianie kanałów ignorowanie takich kodów było by irytujące.

W przypadku drugiego z pilotów ("szarego"), tych błędnych kodów było mniej. Po zamianie baterii z pilotów, biały pilot generował zauważalnie mniej błędnych kodów. Użycie zupełnie nowej baterii także nie rozwiązało problemu. To pozwala sądzić, że przynajmniej częściowo, za błędne kody odpowiada zużyta (częściowo) bateria.


Kod obsługi pilota

Jak wspomniałem wcześniej, skorzystałem z biblioteki IRremote. Według posta Arduino: 10 common pitfalls with Infrared Remote Control (5) IRLib jest nowsza i ma większe możliwości. Nie radziła sobie jednak na dzień dobry z błędnymi kodami (tj., nie zwracała ich).

Poniżej przedstawiam kod stosowany przeze mnie przystosowany do białego pilota. Tak jak posty, kod pisany był wieczorami, więc nie biorę odpowiedzialności za jego poprawne działanie ;) Kilka słów jeszcze o nim:
- wykrywa kody standardowe i błędne dla wybranych/wciśniętych przycisków
- obsługuje powtórzenia przycisków
- wykrywa liczby dwucyfrowe, jeśli w ciągu 1,5 sekundy zostaną wybrane dwie cyfry
- informacje o wciśniętych przyciskach (kodach) wysyłane są przez złącze szeregowe na np. terminal PC (forma debuggera)

#include <SoftwareSerial.h>
#include <Wire.h>
#include "libs/IRremote/IRremote.h"

#define RECV_PIN 12               //pin connected with IR receiver
#define DEFAULT_NUMERIC_VALUE 10  //default value for empty variable
#define NUMBER_TIMEOUT 1500       //1.5 second to select two numbers

IRrecv irrecv(RECV_PIN);
decode_results results;

unsigned long currentButtonCode;
unsigned long lastButtonCode;
unsigned long lastNumericButtonTime = millis();
uint8_t lastNumericButton = DEFAULT_NUMERIC_VALUE;
uint8_t newNumericButton = DEFAULT_NUMERIC_VALUE;

void setup() {
    Serial.begin(9600);
    irrecv.enableIRIn(); //Start the receiver

    while (!Serial) {
        // wait for serial port to connect. Needed for native USB port only
    }

    Serial.println(F("Pilot IR test"));
}

void loop() {

    uint8_t numericButton = DEFAULT_NUMERIC_VALUE;

    if (irrecv.decode(&results)) {
        currentButtonCode = results.value;

        if (currentButtonCode == 0xFFFFFFFF) {
            //repeated button, get last button code
            //comment to ignore repeated NEC code
            currentButtonCode = lastButtonCode;
        } else {
            lastButtonCode = currentButtonCode;
        }

        Serial.print(F("selected key: "));
        switch (currentButtonCode) {
            case 0xFFA25D:
            case 0xE318261B:
                Serial.print(F("IR_KEY_POWER"));
                break;
            case 0xFFE01F:
                Serial.print(F("PREVIOUS"));
                break;
            case 0xFF906F:
            case 0xE5CFBD7F:
                Serial.print(F("NEXT"));
                break;
            case 0xFFE21D:
            case 0xEE886D7F:
                Serial.print(F("IR_KEY_MENU"));
                break;
            case 0xFF02FD:
            case 0xD7E84B1B:
                Serial.print(F("IR_KEY_PLUS"));
                break;
            case 0xFF9867:
            case 0x97483BFB:
                Serial.print(F("IR_KEY_MINUS"));
                break;
            case 0xFFC23D:
            case 0x20FE4DBB:
                Serial.print(F("IR_KEY_EXIT"));
                break;
            case 0xFF22DD:
            case 0x52A3D41F:
                Serial.print(F("IR_KEY_TEST"));
                break;
            case 0xFFB04F:
            case 0xF0C41643:
                Serial.print(F("IR_KEY_CONTROL"));
                break;
            case 0xFFA857:
            case 0xA3C8EDDB:
                Serial.print(F("IR_KEY_ENTER"));
                break;
            case 0xFF6897:
            case 0xC101E57B:
                Serial.print(F("ZERO"));
                numericButton = 0;
                break;
            case 0xFF30CF:
            case 0x9716BE3F:
                Serial.print(F("ONE"));
                numericButton = 1;
                break;
            case 0xFF18E7:
            case 0x3D9AE3F7:
                Serial.print(F("TWO"));
                numericButton = 2;
                break;
            case 0xFF7A85:
            case 0x6182021B:
                Serial.print(F("THREE"));
                numericButton = 3;
                break;
            case 0xFF10EF:
            case 0x8C22657B:
                Serial.print(F("FOUR"));
                numericButton = 4;
                break;
            case 0xFF38C7:
            case 0x488F3CBB:
                Serial.print(F("FIVE"));
                numericButton = 5;
                break;
            case 0xFF5AA5:
            case 0x449E79F:
                Serial.print(F("SIX"));
                numericButton = 6;
                break;
            case 0xFF42BD:
            case 0x32C6FDF7:
                Serial.print(F("SEVEN"));
                numericButton = 7;
                break;
            case 0xFF4AB5:
            case 0x1BC0157B:
                Serial.print(F("EIGHT"));
                numericButton = 8;
                break;
            case 0xFF52AD:
            case 0x3EC3FC1B:
                Serial.print(F("NINE"));
                numericButton = 9;
                break;
            case 0xFFFFFFFF:
                Serial.print(F("REPEATED KEY"));
                break;
            default:
                Serial.print(F("UNKNOWN"));
                break;
        }

        Serial.print(F(" => hex: "));
        Serial.print(results.value, HEX);
        Serial.println();

        //Receive the next value
        irrecv.resume();
    }

    //find double numbers
    if (numericButton != DEFAULT_NUMERIC_VALUE) {
        if (lastNumericButton == DEFAULT_NUMERIC_VALUE) {
            lastNumericButton = numericButton;
            lastNumericButtonTime = millis();
        } else if (newNumericButton == DEFAULT_NUMERIC_VALUE) {
            newNumericButton = numericButton;
        }
    }

    if (lastNumericButton != DEFAULT_NUMERIC_VALUE
            && (millis() - lastNumericButtonTime > NUMBER_TIMEOUT)) {
        if (newNumericButton != DEFAULT_NUMERIC_VALUE) {
            Serial.print(F("selected number: "));
            Serial.println(lastNumericButton * 10 + newNumericButton);
        } else if (lastNumericButton > 0) {
            Serial.print(F("selected number: "));
            Serial.println(lastNumericButton);
        }
        newNumericButton = DEFAULT_NUMERIC_VALUE;
        lastNumericButton = DEFAULT_NUMERIC_VALUE;
    }

    delay(10);
}



Źródła:
1) Dokumentacja HX1838
2) Opis protokołu NEC
3) Biblioteka IRemote
4) Biblioteka IRLib
5) Arduino: 10 common pitfalls with Infrared Remote Control

2 komentarze:

  1. A czy ten kondensator to nie jest przypadkiem odwrotnie wlutowany? Wg schematu minus powinien być chyba przy GND. Czy się mylę?

    OdpowiedzUsuń
    Odpowiedzi
    1. Minus powinien być przy GND. Teraz z fotki trudno to ustalić. Inna sprawa, że mam też elektrolity, dla których polaryzacja nie ma znaczenia.

      Usuń