Objekt - Hat ein Beziehung - Wie Methoden aufrufen?

Hallo zusammen,

ich bräuchte mal Hilfe zur objektorientierten Programmierung.

Es geht darum, ein oder mehrere Magnetventile zu steuern. Ihre Steuerung ist abhängig von dazugehörigen Bodenfeuchtemessern. Darum habe ich zwei Klassen definiert. Einmal eine für den Feuchtemesser, welche nur die Kalibrierungswerte des Sensors enthält (Min (100% Nass), Max (Trocken Luftfeuchte) Werte vom ADC) und einen errechneten Grenzwert. Die zweite Klasse enthält Werte und Methoden zur Steuerung des Magnetventils.

Zu jedem Magnetventil soll ein Feuchtemesser zugeordnet werden. Darum habe ich versucht eine "Hat ein" Komposition zu bauen, indem ich in der Magnetventilklasse eine Membervariable anlege, die eine Feuchtemesserinstanz enthalten soll.

Nun meine Frage, wie rufe ich die Methoden des Objektes auf welches eine Membervariable der Magnetventilklasse ist, um die Min, Max Werte zu initialisieren?

Die zwei Klassen sehen so aus:

#include <Arduino.h>
#include <stdint.h>

class MoistureMeter {
  private:
    uint16_t _maxValue;
    uint16_t _minValue;
    uint16_t _threshold;

  public:
    MoistureMeter(uint16_t max = 6, uint16_t min = 3)
      : _maxValue{max}, _minValue{min} {}
    void setMinMax(uint16_t, uint16_t);
    uint16_t getMoistureMax(void);
    uint16_t getMoistureMin(void);
    uint16_t getMoistureThreshold(void);
};


class SolenoidValve  {
  private:
    uint8_t _pin;
    uint32_t _timeDuration;
    uint32_t _timeMarker;
    bool _pinStatus;
    MoistureMeter _mMeter;

  public:
    enum class Status {CLOSED, OPEN, WAIT};
  
  protected:
    Status _opStatus = Status::CLOSED;

  public:
    SolenoidValve(uint8_t pin = 3, uint32_t timeDuration = 30000)
     : _pin{pin}, _timeDuration{timeDuration} {}

    void begin();
    void begin(uint8_t);
    Status getValveStatus(void);
    void setValveStatus(SolenoidValve::Status);
    uint32_t getTimeMarker(void);
    void setTimeMarker(void);
    uint32_t getTimeDuration(void);
    bool getPinStatus(void);
};

Es gibt in der Magnetventilklasse den Member "MoistureMeter _mMeter;". Zur Zeit noch private!?. Wenn ich nun eine Instanz der Magnetventilklasse anlege, wird darüber eine Feuchtemesserinstanz _mMeter mit den Defaultwerten angelegt?!

Wie kann man denn nun die Methoden wie z.B. setMinMax dieser Feuchtemesserinstanz aufrufen um die Kalibrierungswerte zu setzen?

Geht das oder bin ich komplett auf dem Holzweg?

Da es 2 Klassen sind, solltest Du den Feuchtemesser doch erst mal der Magnetventilklasse zuweisen, sonst kennen die sich ja nicht.

und dann könntest Du in der Magnetventilklasse Zugriffsmethoden (getter/setter) auf die zugewiesene Feuchtemesserklasse implementieren (prüfen, ob diese null ist, wenn keiner zugewiesen)

So mal als Denkansatz.

Gruß Tommy

Hmm... das mit dem Zuweisen verstehe ich noch nicht so ganz.

Momentan sieht mein Programm wie folgt aus:

// Soil moisture meter
constexpr uint16_t MOISTURE_THRESHOLD_DRY {832};
constexpr uint16_t MOISTURE_THRESHOLD_WET {403};
constexpr uint16_t VALVE_OPEN_THRESHOLD   {MOISTURE_THRESHOLD_DRY-((MOISTURE_THRESHOLD_DRY - MOISTURE_THRESHOLD_WET) / 3)};          

constexpr uint8_t PINS_MOISTURE_METER[]   {PIN_A0, PIN_A1};     // One or more analog pins for
constexpr uint8_t PIN_SOLENOID_VALVE_ONE  {3};          // Default Pin. Can be overwritten with begin()
constexpr uint8_t PIN_SOLENOID_VALVE_TWO  {4};          
constexpr uint32_t VALVE_OPEN_DURATION    {30000};      // Default 0,5 Min

constexpr uint8_t MAX_SOLENOIDVALVES      {2};
SolenoidValve solValve[MAX_SOLENOIDVALVES];             // Two solenoid valves with Standard parameters

//////////////////////////////////////////////////////////////////////////////
/// @brief Setup for main program
/// 
//////////////////////////////////////////////////////////////////////////////
void setup() 
{
#ifdef SERIAL_OUTPUT
  Serial.begin(115200);
#endif
  solValve[0].begin(PIN_SOLENOID_VALVE_ONE); 
  solValve[1].begin(PIN_SOLENOID_VALVE_TWO);
}

//////////////////////////////////////////////////////////////////////////////
/// @brief main program
/// 
//////////////////////////////////////////////////////////////////////////////
void loop() 
{
  static uint8_t idx {0};

  for (auto pin : PINS_MOISTURE_METER) {
    uint16_t adcResult = adcMeasurement(pin);
#ifdef SERIAL_OUTPUT
    Serial.print(F("Pin  : ")); Serial.print(adcResult);
    Serial.print(F(" Valve: ")); Serial.print(idx+1);
    Serial.print(F(" "));
#endif
    checkSolenoidValve(solValve[idx], adcResult );
    idx = (idx + 1) % MAX_SOLENOIDVALVES;
  }
  delay(2000);
}


//////////////////////////////////////////////////////////////////////////////
/// @brief This function checks whether a solenoid valve must be 
///        opened (soil too dry) or whether it can be closed 
///        again (soil moist enough).
/// 
/// @param sv            Reference to a Solenoid valve object
/// @param measurement   Measured value from soil moisture sensor
//////////////////////////////////////////////////////////////////////////////
void checkSolenoidValve(SolenoidValve &sv, uint16_t measurement ) {
  SolenoidValve::Status statusValve = sv.getValveStatus();
  switch(statusValve) {
  case SolenoidValve::Status::CLOSED:
    if (measurement > VALVE_OPEN_THRESHOLD) {     
#ifdef SERIAL_OUTPUT
      Serial.println(F("Bodenfeuchte gering: Ventil öffnen."));
#endif
      sv.setValveStatus(SolenoidValve::Status::OPEN);
      sv.setTimeMarker();                                                  // Start for minimum opening time 
    } else {
#ifdef SERIAL_OUTPUT
      Serial.println(F("Bodenfeuchte ausreichend: Ventil geschlossen."));
#endif
    }
    break;
  case SolenoidValve::Status::OPEN:
    if (measurement < VALVE_OPEN_THRESHOLD) {
#ifdef SERIAL_OUTPUT
      Serial.println(F("Bodenfeuchte OK. Ventil schließen."));
#endif
      sv.setValveStatus(SolenoidValve::Status::WAIT);
    } else {
#ifdef SERIAL_OUTPUT
      Serial.println(F("Ventil offen!"));
#endif
    }
    break;
  case SolenoidValve::Status::WAIT:
    if (measurement > VALVE_OPEN_THRESHOLD) {    // Test moisture twice
      sv.setValveStatus(SolenoidValve::Status::OPEN);
    } else if ((millis() - sv.getTimeMarker()) > sv.getTimeDuration() ) {
#ifdef SERIAL_OUTPUT
      Serial.println(F("Ventil schließen"));
#endif      
      sv.setValveStatus(SolenoidValve::Status::CLOSED); 
    } else {
#ifdef SERIAL_OUTPUT
      Serial.print(F("Mindesöffungszeit für Ventil noch nicht abgelaufen."));
      Serial.print(F(" Rest: "));
      Serial.print((sv.getTimeDuration() - (millis() - sv.getTimeMarker())) / 1000 );
      Serial.println(F(" Sekunden"));
#endif
    }
    break;
  }
}

Das funktioniert soweit auch ganz gut. Ich möchte aber von den einzelnen Constexpressions für die Kalibrierungswerte der Feuchtemesser wegkommen, weil sich sonst die checkSolenoidValve(solValve[idx], adcResult ); Funktion verkomplizieren würde, weil sie nur für einen Kalibrierungswert ausgelegt ist. Darum dachte ich mir, ich lege eine Klasse dafür an und wollte das damit verbinden, dass ich die "neue" Klasse als Member der bestehenden verwende.

Mein, wahrscheinlich abwegiger Gedanke war, dass ich z.B. mit einem "erweiterten" solValve[0].begin(PIN_SOLENOID_VALVE_ONE); auch gleich die Feuchtemesserklasse mit Werten versorgen kann und in der check Funktion auf de Methoden zur Abfrage der zugehörigen Kalibrierungswerte zugreifen kann.

Oder wäre Vererbung doch der einfachere Weg?

Ich bin verwirrt!
Aber grundsätzlich gilt "Komposition vor Vererbung!"

Ich bin auch verwirrt.
Allerdings weil du nirgends was mit dem MoistureMeter-Objekt machst.
Und da es private ist muss das in einer der Methoden von SolenoidValve passieren. Da sehe ich nur die Deklaration. Das aktuelle Messen des Feuchtesensors könnte gern eine Methode von MoistureMeter sein. Sehe ich auch nicht.

Zusammenfassung: alles gut soweit, das Interessante fehlt aber noch...

Du hast ja recht. Weil das Bestandteil meiner Frage ist. Wie verknüpfe ich die MoistureMeter Klasse am sinnvollsten mit der SolenoidValve Klasse?

Was du tust kann ich dir nicht sagen...
Aber ich bevorzuge die lose Koppelung und die Betrachtung aus der Datenfluss Perspektive.

/**
 *    Ablaufdiagramm - Zeitdiagramm
 *    
 *        S1    _----------_____  Schalterstellung
 *        OUT1  _-------------__  Verzoegertes abschalten
 *        OUT2  ____-------_____  Verzoegertes einschalten
 *        Der Schalter S1 arbeitet invers und ist entprellt
 *        Alle Zeiten in ms
 *
*/

#include <CombieTimer.h>
#include <CombiePin.h>

#include <CombieTypeMangling.h>
using namespace Combie::Millis;

Combie::Pin::InvInputPin<2>   S1; // Taster zwischen Pin und GND(invertierend)
Combie::Pin::OutputPin<3>   OUT1; // Absaugung
Combie::Pin::OutputPin<4>   OUT2; // Kreissäge


Combie::Timer::EntprellTimer    entprell { 20_ms};  // Schalter entprellen
Combie::Timer::RisingEdgeTimer  ton      {500_ms};  // steigende Flanke wird verzoegert
Combie::Timer::FallingEdgeTimer toff     {500_ms};  // abfallende Flanke wird verzoegert
 
void setup(void) 
{
  S1.initPullup();
  OUT1.init();
  OUT2.init();
}

void loop(void) 
{
  bool schalter = entprell = S1; 
  OUT1 = toff = schalter;
  OUT2 = ton  = schalter;
}

Keine Klasse kennt die andere!
Informationen werden über Zuweisungen ausgetauscht.

Natürlich ist dieser Weg nicht der einzige!
Mein Rat: Schau mal bei den "GoF OOP Design Pattern" nach, vielleicht findet sich da das richtige für dich.

@combie ...du hast doch sicher irgendwo einen Thread mit deinen Libs.
Willst nicht beginnen, in deinen Muster-Sketchen neben dem Include auch den Link zu einem derartigen Thread / homepage / download Möglichkeit zu setzen.
Kostet dich nur einmal die Zeit zum Raussuchen, würde aber die Chance erhöhen, dass ein TO das auch ausprobiert.

Das ist gar nicht mein primäres Ziel.
Im Grunde ist es nur ein Schuss mit der Blickwinkelkanone

Aber um dir einen Gefallen zu tun:

Die Libs finden sich mehrfach hier im Forum.
Und hier jetzt nochmal ganz explizit: CombieLib.zip (376,5 KB)

1 Like

Hallo,

vielleicht meinst du das hier. Habe mal den Code meiner Weichensteuerung zusammengekürzt, hat nichts mehr mit Weichensteuerung zu tun, aber die Objektbeziehungen werden besser sichtbar. Die Klasse Steuerung nutzt die Klasse Sensor. In der Klasse Steuerung werden 2 Sensor Instanzen initialisiert. sensorGerade und sensorAbzweig. Die Parameter werden beim Initialisieren der Steuerungs Objekte durchgereicht. Ich kann in der Steuerung alle public Methoden der Sensor Klasse verwenden.

Ein Hinweis noch. Lasse bitte bei den Variablennamen die Unterstriche weg. Das wird dadurch nicht lesbarer. Man kann im Konstruktor auch 2x die gleichen Variablennamen verwenden. Ich "nummeriere" häufig alphabetisch durch oder kürze pin mit p ab o.ä.

Sketch
/*
  Doc_Arduino - german Arduino Forum
  IDE 1.8.15
  Arduino Mega2560
  20.05.2021
  https://forum.arduino.cc/t/anleitung-weichensteuerung-mit-klasse/856934/93
  https://forum.arduino.cc/t/objekt-hat-ein-beziehung-wie-methoden-aufrufen/1016073

*/

class Sensor    // oder Taster
{
  private:
    const byte pin;
    bool state {false};
    bool oldRead {true};
    uint32_t lastMillis {0};

    bool updateDebounce() {
      bool status = false;
      if (millis() - lastMillis >= 20) {  // ggf. anpassen
        lastMillis += 20;
        status = true;
      }
      return status;
    }

  public:
    Sensor(byte p) : pin {p}
    {}

    void init (void)    { pinMode(pin, INPUT_PULLUP); }
    bool isActiv (void) { return !digitalRead(pin); }

    bool updateNoRetrigger (void)
    {
      state = false;
      if (updateDebounce() ) {
        bool read = digitalRead(pin);     
        if (!read && oldRead ) {
          state = true;
        }
        oldRead = read;                 
      }
      return state;
    }
};


class Steuerung
{
  private:  // Die folgenden Objekte sind nur innerhalb der Klasse zugänglich. Am Anfang einer Klasse überflüssig.
    // Objekt Deklarationen  
    Sensor sensorGerade;            
    Sensor sensorAbzweig;  
       
  public:
  /*               SensorGerade
                   |       SensorAbzweig      
                   |       |           */
    Steuerung(byte a, byte b):
      // Initialisierungsliste, Objektreihenfolge wie Deklaration
      sensorGerade{a},
      sensorAbzweig{b}
    {}
   
    void init(void)
    {
      sensorGerade.init();
      sensorAbzweig.init();
    }   
};

Steuerung steuerung[]
{
/*  Pin SensorGerade
    |   Pin SensorAbzweig   
    |   |            */
  { 2,  3},  // 1. Weiche
  { 4,  5},  // 2. Weiche
}; 


void setup(void)
{
  Serial.begin(250000);
  Serial.println(F("\nuReset ### ###"));
  for (Steuerung &s : steuerung) s.init();
}

void loop(void)
{
  
}
1 Like

bin ich auch dafür.
Nur, wie gehst du vor bei einer member function / setter für eine Variable ?

Ich setze da meist ein new davor, aber bin mir nicht sicher obs da nicht was anderes/best practice gäbe...

void setValue(int newValue)
{
  value = newValue;
}
void setValue(int value)
{
  this->value = value;
}
1 Like

Hallo,

ich denke an der Stelle benötigt man nicht wirklich einen kompletten neuen Variablennamen. :slight_smile: Es würde ggf. ein einziger Buchstabe ausreichen würde ich meinen.

void setValue(int a)
{
  value = a;
}

Der Vorschlag von @combie passt schon, hätte ich selber draufkommen sollen.Das kann man auch selbstsprechend für mehrere Variablen verwenden. Komplett andere - oder gar nur a,b,c finde ich nicht praktikabel. Stell dir einen setter für mehrere Variablen vor: a, b, c ...

Erst mal Danke für die Antworten und Hinweise. Das jetzt brauche ich erst mal ein bisschen das anzuschauen.

Kommt selten vor und da sowas meist recht kompakt ist, finde ich einbuchstabige Parameter akzeptabel.

void setPos (int x, int y, int z=0) {
   pos.x = x; pos.y = y;
   hoehe = z;
}

x y z im Kontext von pos ist halt schon was anderes als ein a b c. :wink:

Für mich haben Bezeichner "sprechend" zu sein.
Auch in Funktionssignaturen. Werden doch gerade diese auch automatisch verarbeitet und landen dann in Dokumentationen, Fehlermeldungen und als Vorschläge unter dem Schreibcursor.

Ich peil doch noch einiges nicht so... braucht noch Zeit. Ich habe es jetzt erst einmal so gelöst, dass die Magnetventil Klasse eine Membervariable vom Typ Feuchtemesser hat. So wie es im Eröffnungspost auch schon vorgesehen war. Man muss erst einmal pro Feuchtemesser eine Instanz anlegen. Eine Referenz dieser Instanz wird dann mit einer begin() Methode der Magnetventilklasse an diese übergeben.

Zwei Getter-Methoden habe ich doppelt, damit ich über die jeweilige Magnetventilklasse auf die Daten der Feuchtemesserklasse zugreifen kann.

So sieht das jetzt aus:

Klassen:

#ifndef _SOLENOIDVALVE_
#define _SOLENOIDVALVE_

#include <Arduino.h>
#include <stdint.h>

class MoistureMeter {
  private:
    uint8_t analogPin_;
    uint16_t maxValue_;
    uint16_t minValue_;
    uint16_t threshold_;

  public:
    MoistureMeter() {}
    MoistureMeter(uint8_t analogPin, uint16_t min = 3, uint16_t max = 6)
      : analogPin_{analogPin}, maxValue_{min}, minValue_{max}, threshold_{max - ((max - min)/3)}{} 
    void setMinMax(uint16_t, uint16_t);
    uint8_t getAnalogPin(void);
    uint16_t getMoistureMax(void);
    uint16_t getMoistureMin(void);
    uint16_t getMoistureThreshold(void);
};

class SolenoidValve  {
  private:
    uint8_t pin_;
    uint32_t timeDuration_;
    uint32_t timeMarker_;
    bool pinStatus_;
    MoistureMeter mMeter_;

  public:
    enum class Status {CLOSED, OPEN, WAIT};
  
  protected:
    Status opStatus_ = Status::CLOSED;

  public:
    SolenoidValve(uint8_t pin = 3, uint32_t timeDuration = 30000)
     : pin_{pin}, timeDuration_{timeDuration} {}

    void begin(uint8_t, MoistureMeter&);
    Status getValveStatus(void);
    void setValveStatus(SolenoidValve::Status);
    uint32_t getTimeMarker(void);
    void setTimeMarker(void);
    uint32_t getTimeDuration(void);
    bool getPinStatus(void);
    void setMoistureMeter(MoistureMeter);
    uint16_t getMoistureTheshold(void);
    uint8_t getAnalogPin(void);
};

Hauptprogramm

#include <Arduino.h>
#include "adc.hpp"
#include "SolenoidValve.h"

//////////////////////////////////////////////////
// Functions forward declaration(s)
//////////////////////////////////////////////////
uint16_t adcMeasurement(uint8_t analogPin);
void checkSolenoidValve(SolenoidValve &sv, uint16_t measurement );

//////////////////////////////////////////////////
// Global constants and variables
//////////////////////////////////////////////////

// Soil moisture meter
MoistureMeter mMeter_1(PIN_A0, 403, 832);
MoistureMeter mMeter_2(PIN_A1, 399, 847);

// SolenoidValve
constexpr uint8_t PIN_SOLENOID_VALVE_ONE  {3};          // Default Pin. Can be overwritten with begin()
constexpr uint8_t PIN_SOLENOID_VALVE_TWO  {4};          
constexpr uint8_t MAX_SOLENOIDVALVES      {2};
SolenoidValve solValve[MAX_SOLENOIDVALVES];             // Two solenoid valves with Standard parameters

//////////////////////////////////////////////////////////////////////////////
/// @brief Setup for main program
/// 
//////////////////////////////////////////////////////////////////////////////
void setup() 
{
#ifdef SERIAL_OUTPUT
  Serial.begin(115200);
#endif
  solValve[0].begin(PIN_SOLENOID_VALVE_ONE, mMeter_1); 
  solValve[1].begin(PIN_SOLENOID_VALVE_TWO, mMeter_2);
}

//////////////////////////////////////////////////////////////////////////////
/// @brief main program
/// 
//////////////////////////////////////////////////////////////////////////////
void loop() 
{
  for (uint8_t idx = 0; idx < MAX_SOLENOIDVALVES; idx++) {
    uint16_t adcResult = adcMeasurement(solValve[idx].getAnalogPin());
#ifdef SERIAL_OUTPUT
    Serial.print(F("Pin ")); Serial.print(solValve[idx].getAnalogPin());
    Serial.print(F(" : ")); Serial.print(adcResult);
    Serial.print(F(" Valve: ")); Serial.print(idx+1);
    Serial.print(F(" "));
#endif
    checkSolenoidValve(solValve[idx], adcResult );
  }
  delay(2000);
}

//////////////////////////////////////////////////////////////////////////////
/// @brief This function checks whether a solenoid valve must be 
///        opened (soil too dry) or whether it can be closed 
///        again (soil moist enough).
/// 
/// @param sv            Reference to a Solenoid valve object
/// @param measurement   Measured value from soil moisture sensor
//////////////////////////////////////////////////////////////////////////////
void checkSolenoidValve(SolenoidValve &sv, uint16_t measurement ) {
  SolenoidValve::Status statusValve = sv.getValveStatus();
  switch(statusValve) {
  case SolenoidValve::Status::CLOSED:  
    if (measurement > sv.getMoistureTheshold()) {     
#ifdef SERIAL_OUTPUT
      Serial.println(F("Bodenfeuchte gering: Ventil öffnen."));
#endif
      sv.setValveStatus(SolenoidValve::Status::OPEN);
      sv.setTimeMarker();                                                  // Start for minimum opening time 
    } else {
#ifdef SERIAL_OUTPUT
      Serial.println(F("Bodenfeuchte ausreichend: Ventil geschlossen."));
#endif
    }
    break;
  case SolenoidValve::Status::OPEN:
    if (measurement < sv.getMoistureTheshold()) {
#ifdef SERIAL_OUTPUT
      Serial.println(F("Bodenfeuchte OK. Ventil schließen."));
#endif
      sv.setValveStatus(SolenoidValve::Status::WAIT);
    } else {
#ifdef SERIAL_OUTPUT
      Serial.println(F("Ventil offen!"));
#endif
    }
    break;
  case SolenoidValve::Status::WAIT:
    if (measurement > sv.getMoistureTheshold()) {    // Test moisture twice
      sv.setValveStatus(SolenoidValve::Status::OPEN);
    } else if ((millis() - sv.getTimeMarker()) > sv.getTimeDuration() ) {
#ifdef SERIAL_OUTPUT
      Serial.println(F("Ventil schließen"));
#endif      
      sv.setValveStatus(SolenoidValve::Status::CLOSED); 
    } else {
#ifdef SERIAL_OUTPUT
      Serial.print(F("Mindesöffungszeit für Ventil noch nicht abgelaufen."));
      Serial.print(F(" Rest: "));
      Serial.print((sv.getTimeDuration() - (millis() - sv.getTimeMarker())) / 1000 );
      Serial.println(F(" Sekunden"));
#endif
    }
    break;
  }
}

Damit habe ich eigentlich mein Ziel erreicht. Ich nehme an es geht eleganter, aber das bekomme ich mit meinem Momentanen Wissenstand nicht hin...

Wenn es also noch Tipps gibt nehme ich die gerne auf..

Vielen Dank auch für Deinen Beitrag. Ich habe das quasi aus einem Lehrbuch. Darum habe ich mir das angewöhnt, weil ich so seht schnell Membervariablen erkennen kann. Allerdings habe ich die Unterstriche jetzt hinten angefügt, was die Lesbarkeit vielleicht doch etwas verbessert. :wink:

wenn es das ist was du willst ok.
Aber warum du nicht einfach dein MoistureMeter im SolenoidValve anlegst - schon im Konstruktor hast nicht erklärt.

Deinen Sketch kann ich nicht ausprobieren, weil ich kein adc.hpp habe.

vorsicht halbwissen:
enum class Status {CLOSED, OPEN, WAIT};

das würde ich einmal global definieren und nicht für jede Instanz.
Oder statt enum class nur enum ohne class und dafür static.

Tippfehler, weil ich ihn grad seh:

 Serial.print(F("Mindesöffungszeit für Ventil noch nicht abgelaufen."));

Die trailing _ kann ich nicht umkommentiert lassen: nein das geht gar nicht :wink:
Was willst du mit dem _ (hinten oder vorne) ausdrücken? Eine Membervariable? ja mei, was sonst?