Klasser og Objekter: Forskelle mellem versioner

Fra HTX Arduino
Spring til navigation Spring til søgning
m 1 version importeret
Linje 20: Linje 20:
Man kan betragte en klasses metoder som de funktioner der udfører noget på/for/ved objektet.
Man kan betragte en klasses metoder som de funktioner der udfører noget på/for/ved objektet.


==Opbygningen af en klasse==
==Opbygningen af en klasse i Arduino C==


For at forklare opbygningen af en klasse, så vises et eksempel på hvordan man kan lave en simpel klasse. Eksemplet er vist som udviklingen af klassen, og de forskellige stadier er gem i denne [[media:classExample.zip|ZIP-fil]]
For at forklare opbygningen af en klasse, så vises et eksempel på hvordan man kan lave en simpel klasse. Eksemplet er vist som udviklingen af klassen, og de forskellige stadier er gem i denne [[media:classExample.zip|ZIP-fil]]

Versionen fra 20. okt. 2022, 13:28

Arduino C kan fungerer som objektorienteret sprog, og det udnyttes specielt til at lave biblioteker med, men kan også være med til at give en god struktur i koden.

Her på siden gennemgås først den metode man skal anvende i objektorienteret programkode, når man vil lave en klasse, som kan anvendes som et bibliotek. Metoden kræver lidt ekstra, men den giver en god og overskuelig grundkode[1].

Sidst på siden vises et eksempel på en simplere måde at lave klasser og objekter på[2], men metoden giver en mere uoverskuelig kode, så den anbefales ikke.

Ud fra en klasse kan man lave flere separate objekter, som har hvert deres indhold.

Klasser

Når man arbejder objektorienteret, så er det en klasse der definerer et objekt. Klassen beskriver altså hvad objektet indeholder og hvad det kan.

Egenskaber

Indholdet i en klasse er klassens egenskaber.

Man kan betragte en klasses egenskaber som de variabler der ligger inde i objektet - normalt vil man ikke kunne arbejde direkte med variablerne i objektet. Klassen vil normalt afskærme variablerne, så man skal anvende metoder til at rette i variablerne.

Metoder

Klassens metoder er det der kan gøre noget ved objektet eller få det til at ændre ved sin indre tilstand.

Man kan betragte en klasses metoder som de funktioner der udfører noget på/for/ved objektet.

Opbygningen af en klasse i Arduino C

For at forklare opbygningen af en klasse, så vises et eksempel på hvordan man kan lave en simpel klasse. Eksemplet er vist som udviklingen af klassen, og de forskellige stadier er gem i denne ZIP-fil

Klassen er lavet ud fra princippet Stepwise Improvement og ideen bag er udviklet af og dokumenteret[3] af Michael Caspersen ved Århus Universitet.

Det vi vil have klassen til at løse er simple knapfunktioner, så klassen kaldes Button.

Den oplagte egenskab der skal være er hvilken pin knappen er tilsluttet.

De metoder vi gerne vil have er en simpel isPressed, der angiver om knappen er trykket ned, og så to metoder som kan registrere forkant (rising) og bagkant (falling).

For at få rising og falling til at fungere fornuftigt, så skal der etableres noget debounce, som kan eliminere prel fra kontakten. Dette kræver flere egenskaber, som blot skal anvendes internt i metoderne.

Grundkoden der startes med

Selve klassen består af to filer:

  • En Header-fil med fil-typen .h - Den fil indeholder de definitioner man skal kende for at kunne anvende klassen.
  • en C++ fil med fil-typen .cpp - den indeholder selve definitionen af klassen

For at kunne oversætte og afvikle koden på en Arduino, så skal der være en hovedfil med setup() og loop(). Den første hovedfil der arbejdes med et classStart, der kan findes i ZIP-filen.

Koden ser ud som følger:

// Her laves referencen til Button-klassen
#include "Button.h"

// Her defineres knappen som objektet knap1
Button knap1;

void setup() {
  // put your setup code here, to run once:

}

void loop() {
  // put your main code here, to run repeatedly:

}

Det der kan ses i koden er, at der henvises til en klasse i en includefil (button.h).

Der oprettes desuden et objekt ud fra klassen Button, men ellers sker der ikke noget i koden - det vigtige her er, at man får oprettet de to filer "button.h" og "button.cpp", så man kan se at koden kan oversættes.

Include-filen har nogle definitioner, som gør at man kan hente den ind i et program flere gange, uden at den ødelægger oversættelsen ved at der er flere elementer af samme navn.

Koden til includefilen ser ud som følger:

// Definitions fil til en klasse
// Her defineres de ting som skal til for at man kan bruge klassen
// Her er lavet et eksempel der definerer en knap som et input

// Definitioner der sørger for at include-filen kan sættes ind flere gange
#ifndef button_h
#define button_h
// Henter de specielle Arduino-ting, så man kan skrive normal Arduinokode i C++ filen
#include "Arduino.h"

// Navngivning af klassen
class Button {
  // Definitioner der kan ses offentligt
  public:
    Button();   // Definition af constructoren
    
  // Definitioner der kun kan ses inde i klassen
  private:
};

// Slutningen af definitionen, som gør at det kan indsættes flere gange
#endif

Selve klassefilen er også ret simpel, og har kun lige defineret constructoren, som er den funktion der kan oprette et nyt objekt.

Koden i klassefilen ser ud som følger:

// Kodefilen til en klasse
// Her skrives de metoder der skal anvendes i klassen
// Dette er opbygget som et eksempel på en knap

// Her hentes definitionerne på klassen
#include "Button.h"

// Constructoren skal være til stede, for at der kan laves et objekt
Button::Button () {
}

Formålet med dette er at få etableret filerne, så man sikrer sig at de kan oversættes korrekt - lige nu har filerne ingen funktion, så der er ikke noget at teste endnu.

Oprettelse af Stubbe på klassen

Det næste trin er at man har udtænkt hvad hvilke egenskaber klassen skal indeholde samt hvilke metoder der skal tilknyttes. Disse ligger i classStub, der kan findes i ZIP-filen.

Egenskaberne oprettes typisk som private variabler - det gør man i .h filen

Metoderne erklæres også i .h filen, og samtidigt oprettes funktionerne som passive funktioner (fungerende funktioner der evt. returnere noget passivt, men ellers ingen funktion har.

Button.h kan nu have følgende udseende:

#ifndef button_h
#define button_h
// Henter de specielle Arduino-ting, så man kan skrive normal Arduinokode i C++ filen
#include "Arduino.h"

// Navngivning af klassen
class Button {
  // Definitioner der kan ses offentligt
  public:
    Button();   // Definition af constructoren
    void begin(byte pin);
    boolean isPressed();
    boolean rising();
    boolean falling();

  // Definitioner der kun kan ses inde i klassen
  private:
    byte _pin;
    boolean lastState;
    boolean rise, fall;
    unsigned long shiftTime;
    unsigned long debounceTime = 10;
};

// Slutningen af definitionen, som gør at det kan indsættes flere gange
#endif

I Button.cpp er metoderne oprettet som funktioner der ikke laver noget:

// Her hentes definitionerne på klassen
#include "Button.h"

// Constructoren skal være til stede, for at der kan laves et objekt
Button::Button () {
}

void Button::begin(byte pin) {
  _pin = pin;
}

boolean Button::isPressed() {
  return false;
}

boolean Button::rising() {
  return false;
}

boolean Button::falling() {
  return false;
}

For at kunne teste at tingene oversættes rigtigt, så laves der nogle kald til alle metoder i main-koden (classStub.ino)

Mainkoden kan have følgende udseende:

// Her laves referencen til Button-klassen
#include "Button.h"

// Her defineres knappen som objektet knap1
Button knap1;

void setup() {
  // put your setup code here, to run once:
  knap1.begin(8);

}

void loop() {
  // put your main code here, to run repeatedly:
  if (knap1.isPressed()) {
    ;
  }
  if (knap1.rising()) {
    ;
  }
  if (knap1.falling()) {
    ;
  }
}

Nu har vi fået etableret det der er interfacet/grænsefladen til klassen, så hvis vi har fået alle egenskaber og metoder korrekt ind, så skulle det ikke være nødvendigt at rette mere i .h filen

Etablering af den første simple metode

Der videreudvikles til at teste metoderne. Disse ligger i classTest1, der kan findes i ZIP-filen.

For at etablere noget funktionalitet i en metode startes med en simpel metode, hvor der skrives ekstra kode ind i Button.cpp i begin- og isPressed-metoderne som vist her:

void Button::begin(byte pin) {
  _pin = pin;
  pinMode(_pin, INPUT);
}

boolean Button::isPressed() {
  return digitalRead(_pin);
}

For at teste dette er der i mainprogrammet (classTest1.ino) sat en udgang op, der defineres som output og der skrives kode, der tester af om isPressed metoden virker, som vist her:

// Lysdioder til test
byte LED1 = 3;

void setup() {
  // put your setup code here, to run once:
  knap1.begin(8);
  pinMode(LED1, OUTPUT);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (knap1.isPressed()) {
    digitalWrite(LED1, HIGH);
  } else {
    digitalWrite(LED1, LOW);
  }

På dette stadie giver det mening at man faktisk ikke bare tester at det kan oversættes, men at koden rent faktisk laver det den skal. Her skal blot testes at lysdioden følger knappen.

Opbygning af en metode til forkant

Det næste der giver mening at arbejde med er en lidt mere kompliceret metode, som kan registrere en forkant på knappen. Disse ligger i classTest2, der kan findes i ZIP-filen.

Igen er .h filen etableret, så der skal kun rettes i Button.cpp - her tilføjes indholdet til metoden rising, men for at det fungerer laves lidt tilføjelser i constructoren og begin-metoden

// Constructoren skal være til stede, for at der kan laves et objekt
Button::Button () {
  fall = true;
}

void Button::begin(byte pin) {
  _pin = pin;
  pinMode(_pin, INPUT);
  shiftTime = millis();
}

Seleve metoden rising etableres som følger:

boolean Button::rising() {
  boolean state = digitalRead(_pin);
  if (state != lastState) {
    shiftTime = millis();
  }
  lastState = state;
  if (state) {
    if ((millis() - shiftTime) > debounceTime) {
      if (!rise) {
        rise = true;
        return true;
      }
    }  
  } else {
    rise = false;
  }
  return false;
}

For at teste dette sættes en lysdiode mere på, og der laves kode der kan fange resultatet fra metoden:

// Lysdioder til test
byte LED1 = 3;
byte LED2 = 4;

void setup() {
  // put your setup code here, to run once:
  knap1.begin(8);
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
}

boolean modeRise = false;

void loop() {
  // put your main code here, to run repeatedly:
  if (knap1.isPressed()) {
    digitalWrite(LED1, HIGH);
  } else {
    digitalWrite(LED1, LOW);
  }
  
  if (knap1.rising()) {
    modeRise = !modeRise;
    digitalWrite(LED2, modeRise);
  }

Der kan nu testes om knappen kan flippe niveauet på LED2, så den skifter hver gang knappen trykkes ned.

Den endelige klasse, som den blev beskrevet i starten

Til sidst laves falling metoden, der faktisk bare er en kopi af rising metoden, hvor den bare venderde elektriske signalers tolkning.

Det endelige resultat er lavet i classTest3, der kan findes i ZIP-filen.

Det at både rising og falling metoderne skal kunne arbejde samtidigt gør at der skal registreres lidt ekstra egenskaber, hvor lastState, shiftTime og bedounceTime kan anvendes af begge metoder, så har det været nødvendigt have egenskaberne rise og fall for at kunne fastholde om man har sendt dem af sted før i dette knaptryk. Det fungerer i koden.

Button.h er stadig samme kode som vist her:

// Definitions fil til en klasse
// Her defineres de ting som skal til for at man kan bruge klassen
// Her er lavet et eksempel der definerer en knap som et input

// Definitioner der sørger for at include-filen kan sættes ind flere gange
#ifndef button_h
#define button_h
// Henter de specielle Arduino-ting, så man kan skrive normal Arduinokode i C++ filen
#include "Arduino.h"

// Navngivning af klassen
class Button {
  // Definitioner der kan ses offentligt
  public:
    Button();   // Definition af constructoren
    void begin(byte pin);
    boolean isPressed();
    boolean rising();
    boolean falling();

  // Definitioner der kun kan ses inde i klassen
  private:
    byte _pin;
    boolean lastState;
    boolean rise, fall;
    unsigned long shiftTime;
    unsigned long debounceTime = 10;
};

// Slutningen af definitionen, som gør at det kan indsættes flere gange
#endif

Button.cpp har fået tilføjet indholdet i falling metoden, så den samlede kode ser således ud:

// Kodefilen til en klasse
// Her skrives de metoder der skal anvendes i klassen
// Dette er opbygget som et eksempel på en knap

// Her hentes definitionerne på klassen
#include "Button.h"

// Constructoren skal være til stede, for at der kan laves et objekt
Button::Button () {
  fall = true;
}

void Button::begin(byte pin) {
  _pin = pin;
  pinMode(_pin, INPUT);
  shiftTime = millis();
}

boolean Button::isPressed() {
  return digitalRead(_pin);
}

boolean Button::rising() {
  boolean state = digitalRead(_pin);
  if (state != lastState) {
    shiftTime = millis();
  }
  lastState = state;
  if (state) {
    if ((millis() - shiftTime) > debounceTime) {
      if (!rise) {
        rise = true;
        return true;
      }
    }  
  } else {
    rise = false;
  }
  return false;
}

boolean Button::falling() {
  boolean state = digitalRead(_pin);
  if (state != lastState) {
    shiftTime = millis();
  }
  lastState = state;
  if (! state) {
    if ((millis() - shiftTime) > debounceTime) {
      if (!fall) {
        fall = true;
        return true;
      }
    }  
  } else {
    fall = false;
  }
  return false;
}

classTest3.ino er udvidet med en tredje lysdiode, så falling metoden kan testes og kode til at opfange den, så anvendelsen af hele klassen på et objekt er vist her:

// Her laves referencen til Button-klassen
#include "Button.h"

// Her defineres knappen som objektet knap1
Button knap1;

// Lysdioder til test
byte LED1 = 3;
byte LED2 = 4;
byte LED3 = 5;

void setup() {
  // put your setup code here, to run once:
  knap1.begin(8);
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);
  pinMode(LED3, OUTPUT);
}

boolean modeRise = false, modeFall = false;

void loop() {
  // put your main code here, to run repeatedly:
  if (knap1.isPressed()) {
    digitalWrite(LED1, HIGH);
  } else {
    digitalWrite(LED1, LOW);
  }
  
  if (knap1.rising()) {
    modeRise = !modeRise;
    digitalWrite(LED2, modeRise);
  }
  
  if (knap1.falling()) {
    modeFall = !modeFall;
    digitalWrite(LED3, modeFall);
  }

}

Ved testen registreres at LED1 følger knappens status, og at LED2 og LED3 skifter på henholdsvis forkant og bagkant af trykket på knappen.

Reduktion i starten af klassen

For at gøre det endnu simplere at oprette objektet kan man i constructoren få kaldt begin-metoden, så man ikke får spredt oprettesen ud over flere områder, at der først skal oprettes knap1, og at der i setup skal defineres at knap1 sidder på ben 8.

Den reducerede version er lavet i classTest4, der kan findes i ZIP-filen.

Denne rettelse kræver at der rettes i constructor-definitionen i Button.h som vist her:

  public:
    Button(byte pin);   // Definition af constructoren

Der før så selvfølgelig at constructoren rettes til i Button.cpp, så den kalder begin-metoden, som vist her:

Button::Button (byte pin) {
  fall = true;
  this->begin(pin);
}

Dette gør at man kan lave hele oprettelsen af knappen i én linje i main-filen (classTest4.ino), og at man kan fjerne den knap1.begin(8) der er nede i setup().

Button knap1 = Button(8);

En vigtig ting som testen har vist er, at man ikke skriver Button knap1 = new Button(8);
Hvad der lige går galt er svært at sige, men det er som om objektet knap1 forsvinder igen - den glemmer i hvert fald hvilken pin den ligger på, så programmet ikke virker efter hensigten.

Problemet ser ud til at være at man ikke kan allokere dynamiske objekter før setup er kørt[4]

Mere objektorienteret

For at gøre programmet mere objektorienteret (og lave et simplere main-program), så laves der en helt ny klasse, som kan håndtere en LED

Klassen er endnu simplere, da den ikke kræver så mange egenskaber - bare en pin, men der er stadig krav om 3 metoder: on, off og setLevel, der giver sig selv ud fra navnene.

Det resultatet er lavet i classTest5, der kan findes i ZIP-filen.


Dette giver en Led.h fil som følger:

// Definitioner der sørger for at include-filen kan sættes ind flere gange
#ifndef led_h
#define led_h
// Henter de specielle Arduino-ting, så man kan skrive normal Arduinokode i C++ filen
#include "Arduino.h"

// Navngivning af klassen
class Led {
  // Definitioner der kan ses offentligt
  public:
    Led(byte pin);   // Definition af constructoren
    void begin(byte pin);
    void on();
    void off();
    void setLevel(boolean level);

  // Definitioner der kun kan ses inde i klassen
  private:
    byte _pin;
};

// Slutningen af definitionen, som gør at det kan indsættes flere gange
#endif

Selve koden der skal til for at lave de 3 metoder er heller ikke særlig kompliceret, så Led.cpp kommer til at se ud som følger:

#include "Led.h"

// Constructoren skal være til stede, for at der kan laves et objekt
Led::Led (byte pin) {
  this->begin(pin);
}

void Led::begin(byte pin) {
  _pin = pin;
  pinMode(_pin, OUTPUT);
  digitalWrite(_pin, LOW);
}

void Led::on() {
  digitalWrite(_pin, HIGH);
}

void Led::off() {
  digitalWrite(_pin, LOW);
}

void Led::setLevel(boolean level) {
  digitalWrite(_pin, level);
}

Igen kan man tænke at det er ret meget arbejde, bare for at kunne håndtere et par lysdioder. Det kan man mene, men det er heldigvis kode der kan genbruges andre steder.

Den store fordel er at main-koden bliver mere simpel og lettere at overskue, som det kan ses i classTest5.ino:

// Her laves referencen til Button-klassen
#include "Button.h"
#include "Led.h"

// Her defineres knappen som objektet knap1 og objektet oprettes
// Man må ikke skrive knap1 = new Button(8);
Button knap1 = Button(8);

// Lysdioder til test
Led led1 = Led(3);
Led led2 = Led(4);
Led led3 = Led(5);

void setup() {
  // put your setup code here, to run once:
}

boolean modeRise = false, modeFall = false;

void loop() {
  // put your main code here, to run repeatedly:
  if (knap1.isPressed()) {
    led1.on();
  } else {
    led1.off();
  }
  
  if (knap1.rising()) {
    modeRise = !modeRise;
    led2.setLevel(modeRise);
  }
  
  if (knap1.falling()) {
    modeFall = !modeFall;
    led3.setLevel(modeFall);
  }

}

Mulige tilføjelser

Skulle man tilføje noget til klassen, så kunne det være en metode til at sætte en ny debounceTime - den kunne fx. hedde setDebounceTime, og den skulle så have antal millisekunder som parameter.

En anden måde at programmere med klasser på

Det kan lade sig gøre at definere klasser direkte i Arduinos sprog, uden at skulle lave en include-fil og en C++ fil til klassen.

Problemet ved denne metode er, at definitionen af klassen skal ske inde i samme arduinofil som hovedprogrammet er skrevet i, ellers vil compileren ikke acceptere det.

Dette eksempel, som også ligger i ZIP-filen som classExample.ino er opbygget på følgende måde:

Som det første der står i filen er der defineret en Led klasse, som har pin-nummeret som egenskab, og så er der constructoren (Led funktionen der kaldes når man opretter objektet), en intern init metode, og så to offentlige metoder on og off, som ligesom giver sig selv hvad de laver.

class Led {
  private:
    byte pin;
  public:
    Led(byte pin) {
      // Use 'this->' to make the difference between the
      // 'pin' attribute of the class and the 
      // local variable 'pin' created from the parameter.
      this->pin = pin;
      init();
    }
    void init() {
      pinMode(pin, OUTPUT);
      // Always try to avoid duplicate code.
      // Instead of writing digitalWrite(pin, LOW) here,
      // call the function off() which already does that
      off();
    }
    void on() {
      digitalWrite(pin, HIGH);
    }
    void off() {
      digitalWrite(pin, LOW);
    }
}; // don't forget the semicolon at the end of the class

Dernæst er der en Button klasse som har pin-nummeret som egenskab samt nogle interne egenskaber, som er med til at sikre at der først angives et tryk på knappe, når debounceTime er overstået, så man ikke vil kunne registrere støj på knappen.

Metoderne til klassen er constructoren og to interne metoder, init og update samt getState, der svarer med en byte (0 eller 1) om der er trykket på knappen og en isPressed, der svarer med true hvis der er trykket på knappen.

class Button {
  private:
    byte pin;
    byte state;
    byte lastReading;
    unsigned long lastDebounceTime = 0;
    unsigned long debounceDelay = 50;
  public:
    Button(byte pin) {
      this->pin = pin;
      lastReading = LOW;
      init();
    }
    void init() {
      pinMode(pin, INPUT);
      update();
    }
    void update() {
      // You can handle the debounce of the button directly
      // in the class, so you don't have to think about it
      // elsewhere in your code
      byte newReading = digitalRead(pin);
      
      if (newReading != lastReading) {
        lastDebounceTime = millis();
      }
      if (millis() - lastDebounceTime > debounceDelay) {
        // Update the 'state' attribute only if debounce is checked
        state = newReading;
      }
      lastReading = newReading;
    }
    byte getState() {
      update();
      return state;
    }
    bool isPressed() {
      return (getState() == HIGH);
    }
}; // don't forget the semicolon at the end of the class

Nederst i koden er main-delen med en setup og en loop samt definitionen af 4 LED'er og en knap.

Det man kan se er at setup slet ikke skal forholde sig til om det tilsluttede er input eller output - det sker inde i objekterne når de defineres.

loop-koden bliver også relativ simpel, og ret beskrivende, da man kan give objekterne meningsfulde navne og man har metoder der har sigende navne.

Main-delen af koden ser ud som følger:

// Definitions for the different pins - could also have been made directly in the object creations
#define LED_1_PIN 9
#define LED_2_PIN 10
#define LED_3_PIN 11
#define LED_4_PIN 12
#define BUTTON_PIN 5

// Create your objects in the global scope so you can
// get access to them in the setup() and loop() functions
Led led1(LED_1_PIN);
Led led2(LED_2_PIN);
Led led3(LED_3_PIN);
Led led4(LED_4_PIN);
Button button1(BUTTON_PIN);

void setup() { }

void loop() {
  if (button1.isPressed()) {
    led1.on();
    led2.off();
    led3.on();
    led4.off();
  }
  else {
    led1.off();
    led2.on();
    led3.off();
    led4.on();
  }
}

Referencer