[Arduino Micro] (beste) Kapselung einer Library? // USB & Serial gleichzeitig?

Guten Morgen Leute,

ich habe vor einiger Zeit angefangen, eine kleine Library zu schreiben, mit der man verschiedene (Retro-)Controller am Arduino nutzen kann. Momentan werden folgende Controller unterstützt.

Eine Demo kann man sich hier und hier ansehen. Dank des USB-Modus des Arduino Micro und der Joystick-Lib, die ich im Netz gefunden habe, kann ich die Controller auch als USB-Pad nutzen (Meine Lib sorgt für das Einlesen, die Joystick-Lib setzt die eingelesenen Werte in USB um). Momentan ist der Aufbau so, dass es eine Gamepad.cpp gibt, in der alle Grundfunktionen vereint sind. (u.a. Pin states und Button states verwalten, ButtonBuffer befüllen). Darauf aufbauend gibt es drei weitere Klassen (GamepadAtari.cpp, GamepadSega.cpp, GamepadNintendo.cpp), die sich davon ableiten, die wiederum das eigentliche Einlesen der Controllereingaben (readButtons) übernehmen.

[u]Meine Fragen sind nun:[/u]

Macht es Sinn, diese Aufteilung beizubehalten, oder wäre es besser, wenn ich alle "ReadButtons"-Methoden in die Hauptklasse verlagere und dann in der öffentlichen ReadButtons-Methode auf private _ReadButtonsXY-Methoden verweise?

Momentan kann ich den Controller über die Eigenschaft controllerLayout definieren. Ein Wunschziel wäre es nämlich, wenn ich per serieller Kommunikation und einer selbstgeschriebenen PC-Konfig-Software, dem Arduino sagen könnte, welcher Controller gerade angeschlossen ist.

Alternativ wäre es doch auch möglich, die drei Klassen als "Subklassen" zu definieren und diese dann entsprechend an die Hauptklasse anzuhängen? So habe ich es zumindest mit meiner Buffer-Klasse gemacht, oder ist das ein schlechter Stil?

[u]Beispiel:[/u]

void Gamepad::readButtons()
{
    switch(controllerLayout)
    {
        case CL_ATARI_2600_KEY:
            atari.readButtons();
        break;  

        case CL_NINTENDO_NES:
        case CL_NINTENDO_SNES:
            nintendo.readButtons();
        break;
        ...
        ...
    }
}

Dadurch könnte ich die drei Klassen weiterhin in einzelnen Dateien aufbewahren.

Das bringt mich nun zu meiner zweiten Frage: Ist es bei einem Arduino Micro überhaupt möglich, gleichzeitig den USB- und den Seriellen (COM)-Modus zu nutzen?

Schon einmal vielen Dank im Voraus :)

ZU 1) das übersteigt mein Wissen. Darum kann ich nicht antworten.

Zu 2) ja, der Arduino /Genuino Mikro hat einen ATmega32U4 der dei USB-Schnittstelle nicht über den Serial Port macht. Man kann beides gleichzeitig verwenden.

Grüße Uwe

KeinTollerNick: Darauf aufbauend gibt es drei weitere Klassen (GamepadAtari.cpp, GamepadSega.cpp, GamepadNintendo.cpp), die sich davon ableiten, die wiederum das eigentliche Einlesen der Controllereingaben (readButtons) übernehmen.

Das sieht nach einer vernünftigen Anwendung von Vererbung aus. Mit Kapselung an sich hat das aber nichts zu tun

Ich würde noch eins weiter gehen und die readButtons() Methode in der Oberklasse rein virtuell/pure virtual machen (der C++ Begriff für abstrakt). Und dann in der jeweiligen Unterklasse die Methode implementieren. Muss aber nicht unbedingt sein.

Alternativ wäre es doch auch möglich, die drei Klassen als "Subklassen" zu definieren und diese dann entsprechend an die Hauptklasse anzuhängen? So habe ich es zumindest mit meiner Buffer-Klasse gemacht, oder ist das ein schlechter Stil?

Möglich ist es schon, aber es hört sich mehr nach einer "is-a" Beziehung an. Nicht "has-a".

Erstmal danke für die Antworten soweit :)

Serenifly: Das sieht nach einer vernünftigen Anwendung von Vererbung aus. Mit Kapselung an sich hat das aber nichts zu tun

Ich würde noch eins weiter gehen und die readButtons() Methode in der Oberklasse rein virtuell/pure virtual machen (der C++ Begriff für abstrakt). Und dann in der jeweiligen Unterklasse die Methode implementieren. Muss aber nicht unbedingt sein.

Möglich ist es schon, aber es hört sich mehr nach einer "is-a" Beziehung an. Nicht "has-a".

Okay, zu (meinem) Verständnis. Ein Beispielsketch für die Benutzung von SEGA-Controllern sieht so aus. Die in der Oberklasse gibt es keine ReadButtons()-Methode, die existiert nur in den Kindklassen.

Für den Buffer ich es so gemacht, dass ich eine eigene Bufferklasse geschrieben habe und diese dann als Datentyp der Buffer-Eigenschaft der Oberklasse zugewiesen habe.

class Gamepad
{
    public:
        Gamepad();
        /*
            Variables
        */
[i]     gpConfig cfg;
        byte hasChanged = false;
        unsigned int buttonFlag;
        gamepadPin pins[];[/i] // gamePadPin ist ein typedef struct
        [b]_ButtonBuffer buffer;[/b] // _ButtonBuffer ist eine eigene Klasse
        /* 
            Methods
        */
       [...]

Das Gleiche würde ich auch gerne mit den Childklassen GamepadSega, GamepadAtari und GamepadNintendo machen. Dagegen ist doch nichts einzuwenden?

Dadurch könnte der Nutzer, während des Betriebs, unterschiedliche Controller verwenden, auch systemfremde. Momentan müsste man dafür das Sketch anpassen z.B. von GamepadSega gp1 auf GamepadAtari gp1 und dieses dann neu hochladen. Wenn ich das entsprechend meiner Idee "umbaue", könnte ich am Ende das Verhalten des Arduino Micro, also welcher Controller angeschlossen ist, bequem über eine PC-Software steuern, ohne das Sketch anpassen zu müssen. Ich würde dann über den seriellen Port dem Arduino Mikro mitteilen, welche Controller der Nutzer angeschlossen hat.

KeinTollerNick: Die in der Oberklasse gibt es keine ReadButtons()-Methode, die existiert nur in den Kindklassen.

Eine reine virtuelle Methode in der Oberklasse erzwingt die Implementierung in den Unterklassen und verhindert dass man ein Objekt der Oberklasse erstellt (weil sie abstrakt ist). Außerdem kann man Objekte der Unterklasse über einen Zeiger auf die Oberklasse ansprechen.

Das Gleiche würde ich auch gerne mit den Childklassen GamepadSega, GamepadAtari und GamepadNintendo machen.

Wieso? Wenn der Puffer in der Oberklasse existiert, dann hat auch jede Unterklasse Zugriff darauf, sofern er als public oder besser protected deklariert ist.

Das mit public, protected und private solltest du dir nochmal ansehen. Erst redest du (wenn auch fälschlich) von Kapselung. Dann machst du alles public.

Dadurch könnte der Nutzer, während des Betriebs, unterschiedliche Controller verwenden, auch systemfremde.

Auch das spricht ganz deutlich für virtuelle Methoden. Allgemeines Beispiel:

class MainClass
{
public:
  virtual void doSomething() = 0;

  virtual ~MainClass() {}   //virtueller Destruktor
};

class SubClass1 : public MainClass
{
public:
  void doSomething()
  {
    Serial.println("Class 1");
  }
};

class SubClass2 : public MainClass
{
public:
  void doSomething()
  {
    Serial.println("Class 2");
  }
};


void setup()
{
  Serial.begin(9600);

  MainClass* obj = new SubClass1();
  obj->doSomething();
  delete obj;
  obj = new SubClass2();
  obj->doSomething();
  delete obj;
}

void loop()
{
}

Zwei verschiedene Objekte, aber beide werden durch einen Zeiger auf die gemeinsame Oberklasse angesprochen! Man ruft doSomething() über einen Zeiger auf die Oberklasse auf, aber es wird die Implementierung in der Unterklasse aufgerufen (dynamisch dispatch). Geht auch statisch ohne dynamischem Speicher!

Eine andere sehr nützliche Sache die damit geht ist das:

void func(MainClass& obj)
{
   obj.doSomething();
}

Dieser Funktion kannst du ein Objekt jeder Unterklasse übergeben und es wird automatisch die entsprechende doSomething() Implementierung aufgerufen

Das wird auch in der Arduino Software verwendet. Ganz besonders in der Print Klasse. Wenn man die Funktionalität der Print Klasse - d.h. print()/println()/write() - in einer eigenen Klasse haben will, erbt man von Print. Dann implementiert man die Version von write() die ein einzelnes Zeichen schreibt (da diese virtuell ist). Dann kann hat man in seiner Klasse alle Varianten von print() und diese greifen letztlich auf die eigene Implementierung von write(char) zu; egal wie die Ausgabe erfolgt.

Wieso? Wenn der Puffer in der Oberklasse existiert, dann hat auch jede Unterklasse Zugriff darauf, sofern er als public oder besser protected deklariert ist.

Das mit public, protected und private solltest du dir nochmal ansehen. Erst redest du (wenn auch fälschlich) von Kapselung. Dann machst du alles public.

Ich glaube, ich habe mich missverständlich ausgedrückt bzw. mir unter Kapselung etwas falsches vorgestellt, weshalb es wohl zu etwas Verwirrung kommt.

Momentan ist es so, wenn man eine bestimmte Subclass nutzen will, dass man Folgendes machen muss:

#include <SoftwareSerial.h>

#include <Timer.h>
#include <Gamepad.h>
#include <GamepadAtari.h>

GamepadAtari gp1;
[…]

void setup()
{
  byte mcPins = {2,3,4,5,6,7, PIN_VAL_NU, 8};
  gp1.begin(CL_ATARI_2600_KEY, mcPins);
}

void loop()
{
  gp1.readButtons();
[…]
}

Mein Ziel wäre es nun, dass am Ende das herauskommt:

#include <SoftwareSerial.h>

#include <Timer.h>
#include <Gamepad.h>

Gamepad gp1;
[…]

void setup()
{
  byte mcPins = {2,3,4,5,6,7, PIN_VAL_NU, 8};
  gp1.begin(CL_ATARI_2600_KEY, mcPins);
}

void loop()
{
  gp1.readButtons();
[…]
}

ich wollte es nun so umsetzen, dass Gamepad.h am Ende so aussieht:

class Gamepad

{
  public:
      Gamepad();
      /*
        Variables
      /
      byte controllerLayout;
      […]
      GamepadAtari atari;
      GamepadNintendo nintendo;
      GamepadSega sega;
      /

        Methods
      */
      […]
      void readButtons();

Die Klassen werde ich entsprechend anpassen und die Vererbung entfernen, sie also zu “eigenständigen” Klassen machen. Diese beinhalten aktuell sowieso nur eine eigene readButtons()-Methode für das plattformabhängige Einlesen der Controller. (Jeder Hersteller nutzt dazu andere Verfahren.)

Am Ende läuft es dann in der Gamepad.cpp darauf hinaus:

void readButtons()

{
switch(controllerLayout)
{
case CL_NINTENDO_NES:
case CL_NINTENDO_SNES:
nintendo.readButtons();
break;

	case CL_ATARI_2600_KEY:
	case CL_ATARI_7800_1BTN:
	case CL_ATARI_7800_2BTN:
		atari.readButtons();
	break;
	
	case CL_SEGA_SMS:
	case CL_SEGA_GENESIS_3BTN:
	case CL_SEGA_GENESIS_6BTN:
		sega.readButtons();
	break;
}

return;
}

Dagegen ist doch eigentlich nichts einzuwenden? Es gibt dadurch nur eine Klasse, trotzdem sind die spieleplattformspezifischen “Tätigkeiten” in eigenen “Dateien / Klassen” ausgelagert.

Wenn du meinst