[087] Arduino hackuje panele urządzeń - cz. 3

[087] Arduino hackuje panele urządzeń - cz. 3

W poprzednich artykułach toczyliśmy boje z pewnym monitorem, chcąc przejąć nad nim kontrolę za pomocą interfejsu podpinającego się pod klawiaturę urządzenia. W końcu udało się nam i możemy zająć się oprogramowaniem naszego interfejsu. Ponieważ jednak odrzuciliśmy potencjometr, musimy jeszcze ustalić najkrótszy czas pomiędzy wciśnięciami przycisków, które będą rejestrowane jako oddzielne. Musimy więc powrócić na chwilę do poprzedniego układu z potencjometrem i ustalić minimalny czas między impulsami, ignorując ich sporadyczne gubienie, a skupiając się na sytuacji, gdy impulsy masowo zaczną się ze sobą scalać. Poznaną już metodą dzieleń przez dwa określamy okolice, w których wyraźnie zwiększa się ilość gubionych impulsów. Szukamy wartości, która nie wykazuje tej cechy, lekko ją powiększamy i otrzymujemy wynik: w wypadku tego urządzenia jest to 20 ms. Co oznacza, że przewinięcie całego zakresu zajmie nieco ponad dwie sekundy (20 ms przerwy plus 3 ms na wciśnięcie razy 101). W przypadku użycia klawiaturki monitora trwa to znacznie dłużej i tutaj już mamy konkretny profit.


Dane owe zapamiętujemy: 3 ms na wcisk, 20 ms na pauzę i zakładamy nowy szkic.

#include            // Załaduj bibliotekę obsługującą enkoder.
Encoder enkoder(2, 3);         // Nazwij enkoder i zadeklaruj porty.
long enkoderWartoscStara = 0;  // Zmienne związane z identyfikowaniem kierunku obrotów enkodera.
long enkoderWartoscNowa = 0;
const byte wPrawo = 9;  // Numer pinu, do którego podłączony jest przycisk "W prawo".
const byte wLewo = 10;  // Numer pinu, do którego podłączony jest przycisk "W lewo".

void setup() {
  pinMode(wPrawo, OUTPUT);  // Zadeklaruj porty przycisków jako wyjścia.
  pinMode(wLewo, OUTPUT);
}

void loop() {
  enkoderWartoscNowa = enkoder.read();  // Załaduj warość wirtualnej pozycji enkodera.

  if (enkoderWartoscNowa > enkoderWartoscStara) {  // Jeśli enkoder obrócono w prawo...
    enkoderWartoscStara = enkoderWartoscNowa;      // Zapamiętaj bieżacą wartość dla późniejszych porównań.
    digitalWrite(wPrawo, HIGH);                    // Wciśnij przycisk.
    delay(3);                                      // Dobierz najkrótszy czas, przy którym za każdym razem wartość się zwiększa.
    digitalWrite(wPrawo, LOW);                     // Puść przycisk.
    delay(20);                                     // Dobierz najkrótszy czas, przy którym system interpretuje osobne wciśnięcia.
  }

  if (enkoderWartoscNowa < enkoderWartoscStara) {  // Jeśli enkoder obrócono w lewo...
    enkoderWartoscStara = enkoderWartoscNowa;      // Zapamiętaj bieżacą wartość dla późniejszych porównań.
    digitalWrite(wLewo, HIGH);                     // Wciśnij przycisk.
    delay(3);                                      // Dobierz najkrótszy czas, przy którym za każdym razem wartość się zwiększa.
    digitalWrite(wLewo, LOW);                      // Puść przycisk.
    delay(20);                                     // Dobierz najkrótszy czas, przy którym system interpretuje osobne wciśnięcia.
  }
}

Teraz już wykorzystamy enkoder, więc przyda się nam biblioteka. Co prawda można się obejść bez niej, ale napisanie sprawnej obsługi tanich enkoderów to wyższa sztuka, zwłaszcza jeśli te będą już nieco zużyte. Nic tak nie denerwuje jak przeskakujące wartości, zwłaszcza cofające się przy szybszym kręceniu gałką, co zna chyba każdy, kto obsługiwał piekarnik albo inny sprzęt AGD. Bibliotek jest sporo, użyję polecanej o nazwie Encoder. Wymaga ona zadeklarowania pinów – dla Arduino Uno 2 oraz 3 i nazwania samej biblioteki. Domyślnie zwraca ona jakąś swoją wirtualną wartość bieżącą, ale nam ta wartość nie będzie potrzebna, a tylko stwierdzenie faktu obracania gałki w prawo bądź w lewo.

Etap V i pół - gałka udaje przyciski

W głównej pętli uczynimy klasyczną pułapkę na zmieniającą się wartość bieżącą enkodera względem zapamiętanej w poprzednim odczycie. Jeśli bieżąca jest większa od poprzedniej – gałka została przekręcona w prawo, jeśli mniejsza – w lewo. Jeśli taka sama, gałka stoi w miejscu i nic robić nie będziemy. Każda ze zmian pozycji enkodera wykona dokładnie to samo, co już wykonywaliśmy w pętli testowej. Więc każdy impuls wciśnie przycisk na 3 ms, puści go, zaczeka 20 ms i wróci do pętli. Pozostaje sprawdzić jak to teraz działa.

Działa wyśmienicie, ponieważ użyłem znakomitego, optycznego enkodera. Jest on bardzo czuły, jeśli nam to przeszkadza, przy porównaniach należy dodać stałą. Każda jednostka skradnie po jednym impulsie. Tanie enkodery, o 24 impulsach na obrót nie będą potrzebować zmniejszania rozdzielczości.

No cóż, jesteśmy już przygotowani do napisania szkicu docelowego i chyba żadna niespodzianka już się nam nie zdarzy. Od tej porty program będzie się rozrastał. Będzie on prosty jeśli chodzi o sztuczki informatyczne, ale skomplikowany ilościowo, bo poruszanie się po menu wymaga czasem szeregu niekonwencjonalnych działań, które robią programistyczny chaod. Spróbujemy to zorganizować tak, żeby w miarę nie było bałaganu.

#include            // Załaduj bibliotekę obsługującą enkoder.
Encoder enkoder(2, 3);         // Nazwij enkoder i zadeklaruj porty.
long enkoderWartoscStara = 0;  // Zmienne związane z identyfikowaniem kierunku obrotów enkodera.
long enkoderWartoscNowa = 0;
const byte przyciskPrawo = 9;       // Numer pinu, do którego podłączony jest przycisk "W prawo".
const byte przyciskLewo = 10;       // Numer pinu, do którego podłączony jest przycisk "W lewo".
const byte przyciskGora = 11;       // Numer pinu, do którego podłączony jest przycisk "W górę".
const byte przyciskMenu = 12;       // Numer pinu, do którego podłączony jest przycisk "Menu".
const byte przyciskZrodlo = 13;     // Numer pinu, do którego podłączony jest przycisk "Źródło".
const byte przyciskKontrast = 4;    // Numer pinu, który będzie aktywował regulację kontrastu.
const byte przyciskJasnosc = 8;     // Numer pinu, który będzie aktywował regulację jasności.
const byte przyciskNasycenie = 7;   // Numer pinu, który będzie aktywował regulację nasycenia.
const byte przyciskBlending = 6;    // Numer pinu, który będzie wybierał wartość blendingu.
const byte przyciskOdszumiacz = 5;  // Numer pinu, który będzie wybierał stopień odszumienia obrazu.
const byte pauzaKrotka = 3;         // Najkrótszy czas, przy którym za każdym razem wartość się zmienia.
const byte pauzaSrednia = 20;       // Najkrótszy czas, przy którym system interpretuje osobne wciśnięcia.
const byte pauzaDluga = 250;        // Najkrótszy czas, przy którym system nie zawiesza się przy niektórych działaniach w menu.

void setup() {
  pinMode(przyciskPrawo, OUTPUT);  // Zadeklaruj porty przycisków jako wyjścia.
  pinMode(przyciskLewo, OUTPUT);
  pinMode(przyciskGora, OUTPUT);
  pinMode(przyciskMenu, OUTPUT);
  pinMode(przyciskZrodlo, OUTPUT);
  pinMode(przyciskKontrast, INPUT_PULLUP);  // Zadeklaruj porty przycisków jako wejścia.
  pinMode(przyciskJasnosc, INPUT_PULLUP);
  pinMode(przyciskNasycenie, INPUT_PULLUP);
  pinMode(przyciskBlending, INPUT_PULLUP);
  pinMode(przyciskOdszumiacz, INPUT_PULLUP);
  reset();
}
void loop() {
  if (digitalRead(przyciskKontrast) == HIGH) {  // Obsługa zmiany kontrastu.
    reset();
    delay(pauzaDluga);
    wcisnijGora();
    wcisnijMenu();
    wcisnijGora();
    wcisnijGora();
    wcisnijGora();
    wcisnijGora();
  }
  if (digitalRead(przyciskJasnosc) == HIGH) {  // Obsługa zmiany jasności.
    reset();
    delay(pauzaDluga);
    wcisnijGora();
    wcisnijMenu();
    wcisnijGora();
    wcisnijGora();
    wcisnijGora();
  }
  if (digitalRead(przyciskNasycenie) == HIGH) {  // Obsługa zmiany nasycenia.
    reset();
    delay(pauzaDluga);
    wcisnijGora();
    wcisnijMenu();
    wcisnijGora();
    wcisnijGora();
  }
  enkoderWartoscNowa = enkoder.read();             // Załaduj warość wirtualnej pozycji enkodera.
  if (enkoderWartoscNowa > enkoderWartoscStara) {  // Jeśli enkoder obrócono w prawo...
    enkoderWartoscStara = enkoderWartoscNowa;      // Zapamiętaj bieżacą wartość dla późniejszych porównań.
    wcisnijPrawo();                                // Wciśnij przycisk "W prawo".
  }
  if (enkoderWartoscNowa < enkoderWartoscStara) {  // Jeśli enkoder obrócono w lewo...
    enkoderWartoscStara = enkoderWartoscNowa;      // Zapamiętaj bieżacą wartość dla późniejszych porównań.
    wcisnijLewo();                                 // Wciśnij przycisk "W lewo".
  }
  if (digitalRead(przyciskOdszumiacz) == HIGH) {  // Obsługa wyboru stopnia odszumienia obrazu.
    reset();
    delay(pauzaDluga);
    wcisnijGora();
    delay(pauzaDluga);
    wcisnijGora();
    delay(pauzaDluga);
    wcisnijGora();
    delay(pauzaSrednia);
    wcisnijMenu();
    delay(pauzaSrednia);
    wcisnijGora();
    delay(pauzaSrednia);
  }
  if (digitalRead(przyciskBlending) == HIGH) {  // Obsługa wyboru wartości blendingu.
    reset();
    delay(pauzaDluga);
    wcisnijPrawo();
    delay(pauzaDluga);
    wcisnijPrawo();
    delay(pauzaSrednia);
    wcisnijPrawo();
    delay(pauzaSrednia);
    wcisnijPrawo();
    delay(pauzaSrednia);
    wcisnijGora();
    wcisnijGora();
    wcisnijGora();
    wcisnijGora();
    wcisnijMenu();
  }
}
void wcisnijPrawo() {                 // Wciśnięcie przycisku "W prawo".
  digitalWrite(przyciskPrawo, HIGH);  // Wciśnij przycisk.
  delay(pauzaKrotka);                 // Zaczekaj.
  digitalWrite(przyciskPrawo, LOW);   // Puść przycisk.
  delay(pauzaSrednia);                // Zaczekaj.
}
void wcisnijLewo() {  // Wciśnięcie przycisku "W lewo".
  digitalWrite(przyciskLewo, HIGH);
  delay(pauzaKrotka);
  digitalWrite(przyciskLewo, LOW);
  delay(pauzaSrednia);
}
void wcisnijGora() {  // Wciśnięcie przycisku "W górę".
  digitalWrite(przyciskGora, HIGH);
  delay(pauzaKrotka);
  digitalWrite(przyciskGora, LOW);
  delay(pauzaSrednia);
}
void wcisnijZrodlo() {  // Wciśnięcie przycisku "Źródło".
  digitalWrite(przyciskZrodlo, HIGH);
  delay(pauzaKrotka);
  digitalWrite(przyciskZrodlo, LOW);
  delay(pauzaDluga);
}
void wcisnijMenu() {  // Wciśnięcie przycisku "Menu".
  digitalWrite(przyciskMenu, HIGH);
  delay(pauzaKrotka);
  digitalWrite(przyciskMenu, LOW);
  delay(pauzaDluga);
}
void reset() {  // Makro sekwencji startowej.
  wcisnijZrodlo();
  wcisnijMenu();
  wcisnijMenu();
}

Na początek zaimportujmy nasze dobra. Zaczniemy od użycia trzech przycisków płytki testowej, które będą wybierać kontrast (przyciskKontrast), jasność (przyciskJasnosc) i nasycenie (przyciskNasycenie), do regulacji enkoderem. Dołożymy także resztę klawiaturki monitora. Ale nie całą. Dlaczego? Otóż okazuje się, że nie ma potrzeby stosować obu przycisków ruchu w pionie. Ruch ten jest zapętlony, to znaczy, że gdy znajdziemy się na dole, kolejną pozycją jest pierwsza na górze – i na odwrót. Zatem wystarczy, że cały czas będziemy się poruszać w jedną tylko stronę, oszczędzając na połączeniach i enkoderze. Do tego poruszanie się w dół ma znany już błąd systemowy i czasem „nie załapuje”. Wędrówka w górę wolna jest od tej wady, więc wiadomo już która opcja wyleci. W programie zmieniłem także nazwy zmiennych, by wszystko było bardziej czytelne. Czas na kolejny etap: należy stworzyć tak zwane makro startowe.

Etap VI - makra startowe

Spójrzmy na jeden szczegół: nasze urządzenie nie daje żadnej informacji zwrotnej. Arduino nie wie co się wyświetla na ekranie i może tylko w ciemno wysyłać impulsy, licząc że stanie się dokładnie to, na co liczymy. Ale by to miało sens, należy przewidzieć, że monitorek może znajdować się w dowolnym modzie jak również ktoś mógł gmerać w menu z poziomu lokalnej klawiatury, więc przewidzieć należy najgorsze – nie wiemy co się dzieje.

Bardzo ważne jest znalezienie sekwencji startowej klawiszy, czyli takiego układu wcisków, po których urządzenie znajdzie się zawsze w ustalonym miejscu. Różne urządzenia mogą mieć różne sekwencje, czasem może być to skomplikowane. Na szczęście nasz monitor ma cudownie prosty sposób na wyjście z najbardziej zakopanego menu.

Wystarczy wejść w wybór źródła, po czym wcisnąć dwukrotnie menu. Po tej sekwencji zawsze znajdziemy się w tym samym miejscu. Cóż, napiszmy owo makro startowe.

void reset() {  // Makro sekwencji startowej.
  wcisnijZrodlo();
  wcisnijMenu();
  wcisnijMenu();
}
void wcisnijZrodlo() {  // Wciśnięcie przycisku "Źródło".
  digitalWrite(przyciskZrodlo, HIGH);
  delay(pauzaKrotka);
  digitalWrite(przyciskZrodlo, LOW);
  delay(pauzaDluga);
}
void wcisnijMenu() {  // Wciśnięcie przycisku "Menu".
  digitalWrite(przyciskMenu, HIGH);
  delay(pauzaKrotka);
  digitalWrite(przyciskMenu, LOW);
  delay(pauzaDluga);
}

Nazwiemy go reset. W podprogramie najpierw wciśniemy przycisk przyciskZrodlo, a następnie dwukrotnie przyciskMenu. Oczywiście te wciśnięcia będą siedzieć w kolejnych podprogramach. Niestety chińska myśl techniczna nadal się broni: nie możemy tutaj wstawiać tak krótkich pauz, jak w przypadku obsługi przycisków poruszania się po menu. Metodą już znaną ustaliłem te dłuższe czasy na 250 ms. Wszystkie pauzy trafiły teraz do bloku deklaracji, na początku. W ten sposób można je łatwo zmienić.

Mamy już makro startowe. Wszelkie operacje winny się zaczynać od wywołania tego makra, łącznie z resetem samego Arduino. Stwórzmy teraz regulację kontrastu.

if (digitalRead(przyciskKontrast) == HIGH) {  // Obsługa zmiany kontrastu.
  reset();
  delay(pauzaDluga);
  wcisnijGora();
  wcisnijMenu();
  wcisnijGora();
  wcisnijGora();
  wcisnijGora();
  wcisnijGora();
}

Będzie ją wywoływać wciśnięcie lewego przycisku na płytce startowej (przyciskKontrast). W głównej pętli stworzymy blok zależny od wciśnięcia tego przycisku. Następnie wywołamy sekwencje startową i kolejno sekwencje wciśnięć klawiaturki urządzenia, już konkretne do dostania się do menu regulacji kontrastu. Obsługa enkodera, który dostanie teraz już przygotowane menu, jest taka sama jak poprzednio, z tym że wydzieliłem tutaj elementy wciskania klawiszy do podprogramów. Mogą się przydać do innych zadań.

Pozostaje sklonować sekwencje dostania się do menu jasności i kontrastu. Różnią się one wyłącznie coraz to mniejszą ilością wciśnięć klawisza „w górę”, zgodnie z umieszczeniem tych nastaw w menu przez producenta monitora. Na koniec – przykład procedur nieużywających enkodera.

Etap VII - makra użytkowe

Użyjemy ostatnich dwóch przycisków klawiatury płytki testowej: przyciskBlending i przyciskOdszumiacz. Tutaj już nie ma niczego nadzwyczajnego, a cały szereg sekwencji wciśnięć klawiatury monitora, przedzielonych dodatkowymi pauzami.

if (digitalRead(przyciskOdszumiacz) == HIGH) {  // Obsługa wyboru stopnia odszumienia obrazu.
  reset();
  delay(pauzaDluga);
  wcisnijGora();
  delay(pauzaDluga);
  wcisnijGora();
  delay(pauzaDluga);
  wcisnijGora();
  delay(pauzaSrednia);
  wcisnijMenu();
  delay(pauzaSrednia);
  wcisnijGora();
  delay(pauzaSrednia);
}
if (digitalRead(przyciskBlending) == HIGH) {  // Obsługa wyboru wartości blendingu.
  reset();
  delay(pauzaDluga);
  wcisnijPrawo();
  delay(pauzaDluga);
  wcisnijPrawo();
  delay(pauzaSrednia);
  wcisnijPrawo();
  delay(pauzaSrednia);
  wcisnijPrawo();
  delay(pauzaSrednia);
  wcisnijGora();
  wcisnijGora();
  wcisnijGora();
  wcisnijGora();
  wcisnijMenu();
}

Niestety soft tego urządzenia jest napisany marnie i cechuje się wieloma lagami. Przy poruszaniu się po menu wszystkie opóźnienia są niezbędne dla zachowania powtarzalności i z takim problemem będziemy się spotykać często. Przy okazji wyjaśniła się zagadka powszechnego braku precyzji w tego typu urządzeniach: klawiaturki lokalne nie uwzględniają takich pętli opóźniających i czasem klikając w coś, przeskakujemy menu do jakiejś zupełnie innej zakładki. W naszym przypadku, kosztem czasu dostaniemy precyzję.

Natenczas to wszystko. Jak pisałem, treść artykułów nie ma sensu użytkowego wprost, a pokazuje metody przejmowania kontroli nad urządzeniami na przykładzie pewnego monitorka filmowców. I właściwie za każdym razem czekają nas podobne boje, a tutaj opisałem jak sobie radzić z typowymi problemami spotykanymi podczas adaptacji takich sterowników. Ale to nie koniec.

Powiązane tematy

Płytka edukacyjna TME-EDU-ARD-2Płytka edukacyjna TME-EDU-ARD-2Sprawdź tutaj

Przeczytaj również

Nasi partnerzy

TMETech Master EventTME EducationPoweredby
Copyright © 2025 arduino.pl