Bónusz projekt: okosabb távvezérlős lámpa

Az előző részben láthattuk, hogy minden gombhoz tartozó infra kódot meg lehet fejteni. De azt nem tudom, hogy Neked milyen távvezérlőd van. Egyelőre ne akarjuk a kódokat megfejteni, egyszerűen csak érzékeljük, ha bármely gombot nyomnak.

Az absztrakciós szint

Vegyük elő az előző programot!

const int LED=13;
const int SENSOR=0;

bool isOn;

void setup() {
    pinMode(LED, OUTPUT);
    pinMode(SENSOR, INPUT_PULLUP);
}

void loop() {
    if (digitalRead(SENSOR)==0) { 
      // lámpa be vagy ki
      isOn=!isOn;
      if (isOn) {
        digitalWrite(LED,HIGH);
      }
      else {
        digitalWrite(LED,LOW);
      }
      delay(500); // Kicsit nem figyelünk a többi apró jelre        
    }
}

Mi ebben a baj? A baj nem látszik, de benne van! A baj az, hogy a program tényleges logikája - azaz hogy be-ki kell kapcsolni a lámpát - és az egyéb trükkök (az, hogy kicsit várni kell) össze vannak keverve. Ráadásul, ha több, mint 500ms-ig nyomva tartod a távvezérlőt, a lámpa be-ki kapcsolgatni kezd, hiszen 500ms elteltével azt hiszi a program, hogy újabb gombnyomást kapott.

Miért is hiszi azt, hogy újabb gombnyomást kapott? Azért, mert a digitalRead(SENSOR) 0-át ad vissza a legelső infra jelre, de emlékezz: nagyon sok további kis infra villanás fog érkezni. Sőt, ha nyomva tartják a gombot, akkor folyamatosan érkeznek a villanások, és amikor a delay(500)-nak vége van, a loop()-ban újra kezdi előröl, az if észreveszi a villanást, és megint átkapcsolja a lámpát.

Vágjunk rendet! Válasszuk el ezt a gombnyomás problémát!

Mi az az absztrakció? Azt jelenti, hogy elvonatkoztatás, hogy egy dolgot egyszerűbbnek mutatunk, mint valójában. A telefonod egy absztrakció: csak be kell pötyögni egy számot, és lehet beszélni. Nem kell tudnod az érintőképernyő, az LCD, a mobiltelefon működését, semmit sem kell tudni a mobil hálózatokról, és az sem a Te gondod, hogy éppen milyen műholdakat kell igénybe venni ahhoz, hogy amerikai haverodat felhívd. Az absztrakció csak annyi, hogy telefonszám. Szép trükk, ugye?
const int LED=13;
const int SENSOR=0;

bool isOn;

void setup() {
    pinMode(LED, OUTPUT);
    pinMode(SENSOR, INPUT_PULLUP);
}

void loop() {
    if (infraButton()) { // csinálunk egy rendes függvényt erre
      // lámpa be vagy ki
      isOn=!isOn;
      if (isOn) {
        digitalWrite(LED,HIGH);
      }
      else {
        digitalWrite(LED,LOW);
      }

      while (infraButton()) {
            // nem csinálunk semmit, amíg nyomják a gombot 
      }

      // ok, most már elengedték a gombot, 
      // ha újra megnyomják, arra már reagálni kell
    }
}

Szóval mi változott? Nem a bazseváló bemenetet akarjuk olvasni, hanem elképzeljük, milyen jó lenne egy infraButton() függvény, ami true-et ad vissza ha volt gombnyomás. A bazseválás innentől nem a mi problémánk, hanem az infraButton() függvényé.

Az infraButton() függvény egy absztrakció: elrejti, hogy bazsevál az infra jel. Innentől ezzel nem kell törődni, és ugyanúgy használhatjuk az infrát, mint egy sima gombot, ha ezt a függvényt használjuk.

De hogy nézhet ki ez az infraButton() függvény? Nos, infra jel akkor van, ha mondjuk 1 ms-ként ránézünk a bemenetre, és úgy találjuk, hogy 50ms alatt legalább pár alkalommal fogtunk valami jelet.

Vajon honnan vettem ezeket a számokat? Az előző lapon lemértük, hogy mi szokott történni egy infra cuccal, és úgy találtuk, hogy ha nyomva van a gomb, akkor hasracsapva 50ms alatt biztosan kell pár jelnek jönnie. Persze minden távvezérlő más, de mi is lazák vagyunk: általában elég lenne 20ms-ig figyelni, de mi 50ms-ig figyelünk. Általában 10-20 jel érkezik, de mi megelégszünk azzal, ha úgy 10-et kapunk.

Viszont egyetlen jellel nem elégszünk meg: előfordul, hogy a fénycsöves lámpák hirtelen felkapcsolására is ad egy jelet az infravevő. No, azt nem fogadjuk el, mert az csak 1 és nem 10 jel.

bool infraButton() {
    int cnt=0;
    for (int b=0;b<50;b++) {
        if (digitalRead(SENSOR)==0) {
            cnt++;
        } 
        delay(1);
    }
    if (cnt>10) {
        return true; // volt infra
    }
    else {
        return false; // nem volt infra
    }
}

Érthető a megoldás? Számolunk 50-ig a for ciklussal. Mivel a for ciklusban van egy delay(1) ez kb. 50 ms-ig fog tartani. Azért pont ennyi ideig, mert úgy saccoljuk, hogy 10-20ms hosszú egy infrakód, és utána van egy kis szünet, majd ismétlődik, így akkor is elkapunk valamit, ha épp a szünetben kezdünk figyelni.

Minden milliszekundumban megnézzük, hogy mit mond az infravevő. Ha volt jel, akkor növeljük a cnt értékét. A cnt változó egy számláló (angolul counter, röviden cnt), és ha egy pici jelet fogtunk a sok bazseválásból, akkor megnöveljük. A ++ műveleti jel azt jelenti, hogy valaminek az értékét növeljük meg 1-el.

A végén azt mondom, hogy saccra ha volt 10 jel, akkor volt gombnyomás, és true-t fogok visszaadni, máskülönben false-t. (A függvény logikai értéket, azaz true-false-t ad vissza - hiszen vagy van gombnyomás, vagy nincs.) A bazseválás probléma leküzdve.

Bele a progiba!

Rakjuk ezt be a progiba! Ugye emlékszel, hogy bárhova, ahova számot lehet írni, írhatunk függyvényt is, vagy tetszőleges kifejezést, ami kiszámolva számot eredményez?

Az if és a while belsejébe pedig valójában olyan kifejezést lehet írni, aminek az eredménye logikai, azaz igaz (true) vagy hamis (false). Amikor azt írod, hogy if (valami==2) akkor a == művelettel összehasonlítod a valami értékét kettővel, és ennek az eredménye true vagy false.

A mi kis infraButton() metódusunk eredménye viszont eleve true vagy false. Ezért könnyen és olvashatóan használhatjuk:

const int LED=13;
const int SENSOR=0;

bool isOn;

void setup() {
    pinMode(LED, OUTPUT);
    pinMode(SENSOR, INPUT_PULLUP);
}

bool infraButton() {
    int cnt=0;
    for (int b=0;b<50;b++) {
        if (digitalRead(SENSOR)==0) {
            cnt++;
        } 
        delay(1);
    }
    if (cnt>10) {
        return true; // volt infra
    }
    else {
        return false; // nem volt infra
    }
} 

void loop() {
    if (infraButton()) { // ha rendesen megnyomták
      // lámpa be vagy ki
      isOn=!isOn;
      if (isOn) {
        digitalWrite(LED,HIGH);
      }
      else {
        digitalWrite(LED,LOW);
      }
      while (infraButton()) {
            // nem csinálunk semmit, amíg nyomják a gombot 
      }
    }
}

Sokkal kulturáltabb lenne, ha nem csak úgy bekapcsolna, hanem szépen növekedne a fényereje bekapcskor, és csökkenne kikapcsor. A fényerő finom változásához remek megoldás a for-ciklus, ami szépen számolgat. Hogy a program kicsit olvashatóbb legyen, ezért ezeket kiszedtem egy fadeUp() és egy fadeDown() függvénybe.

const int LED=13;
const int SENSOR=0;

bool isOn;

void setup() {
    pinMode(LED, OUTPUT);
    pinMode(SENSOR, INPUT_PULLUP);
}

bool infraButton() {
    int cnt=0;
    for (int b=0;b<50;b++) {
        if (digitalRead(SENSOR)==0) {
            cnt++;
        } 
        delay(1);
    }
    if (cnt>10) {
        return true; // volt infra
    }
    else {
        return false; // nem volt infra
    }   
} 

void fadeUp() {
    for (int b=0;b<=255;b++) {
        analogWrite(LED,b);
        delay(1);
    }
}

void fadeDown() {
    for (int b=255;b>=0;b--) {
        analogWrite(LED,b);
        delay(1);
    }
}

void loop() {
    if (infraButton()) { // ha rendesen megnyomták
        // lámpa be vagy ki
        isOn=!isOn;
        if (isOn) {
            // szépen felfelé
            fadeUp();
        }
        else {
            // szépen lefelé
            fadeDown();
        }
        while (infraButton()); // addig várunk, amíg el nem engedik
    }
}

Hogyan lesz szebb a programunk a függvényektől?

Nos, rövidebb éppen nem lett, vagy igen? Igaziból rövidebb is lett, hiszen az infraButton() az legalább 10 sor. Igaz, hogy ezt kétszer használjuk a loop() belsejében, de ott már csak a nevét írjuk le, és nem kell 2x 10 sort belepötyögni.

Minden programban minden sort felcimkézhetünk két kategóriába:

Térjünk vissza egy pillanatra a legelső programunkra:

const int LED=13;                  // mit
const int SENSOR=0;                 // mit

bool isOn;                          // mit

void setup() {
    pinMode(LED, OUTPUT);           // hogyan
    pinMode(SENSOR, INPUT_PULLUP);  // hogyan
}

void loop() {
    if (digitalRead(SENSOR)==0) {   // mit (az if) és hogyan (digitalRead)
      // lámpa be vagy ki
      isOn=!isOn;                   // mit
      if (isOn) {                   // mit  
        digitalWrite(LED,HIGH);     // hogyan
      }
      else {                        // mit
        digitalWrite(LED,LOW);      // hogyan
      }
      delay(500);                   // hogyan - nem figyelünk a bazseválásra        
    }
}

A mit és a hogyan dolgok keverednek benne! De ha egy programrészben csak MIT dolgok vannak, az sokkal könnyebben érthető:

void loop() {
    if (infraButton()) {            // mit
        // lámpa be vagy ki
        isOn=!isOn;                 // mit
        if (isOn) {                 // mit
            // szépen felfelé
            fadeUp();               // mit
        }
        else {                      // mit
            // szépen lefelé        
            fadeDown();             // mit
        }
        while (infraButton()) {     // mit
        }
    }
}

A hogyan és a mit szétválogatása nekünk segít, mert rövidebb, olvashatóbb, és könnyebben áttekinthető programunk lesz.

Lássunk egy különbséget!

Egy HOGYAN kód                          Egy MIT kód
---------------------------------------------------  
for (int b=255;b>=0;b--) {               fadeDown()
    analogWrite(LED,b);
    delay(1);
}

Ugye, Te is látod a különbséget? Ha valahol azt látod, hogy fadeDown() akkor tudod, hogy a fade azt jelenti, hogy fínom áttűnés, a down meg hogy lefelé, szóval valami fény itt el fog tűnni.

De ha valami közepére be van dobva egy for-ciklus érdekes számokkal meg analogWrite-okkal, nos, azon el kell gondolkodni, hogy tényleg mit is csinál!

Mit tanultunk ebből?

Függvényekkel jól el lehet rejteni a komplexitást. Használj Te is függvényeket! A komplexitás elrejtésén túl a program is olvashatóbb lesz. Az infraButton() értelmeseb, mint a digitalRead blah.. blah... A fadeUp() (fény-tűnj-elő) olvashatóbb, mint egy analogWrite-os for ciklus a program közepén.