Viele (const) Variablen an Objekt übergeben - Vorschläge?

Ich bastle gerade an einer Lib und da sollte ich einem Objekt diverse Werte übergeben können.
"Initialisierungsliste" im Constructor ist mir bekannt und verwende ich auch in anderen Projekten.

Die Frage die sich mir nun stellt:
Was ist den eigentlich der bevorzugte Weg "viele" Einstellungen an ein Objekt zu geben?

"Viele" sind aktuell knapp 20 Werte die ich beim Anlegen des Objektes definieren möchte, aber in Zukunft mehr werden könnten.

Die Werte müssen zur Laufzeit nicht geändert werden können. Können also const sein.

Aber ich denke mir eine Initialisierungsliste mit 20 Werten ist ja nicht mehr wirklich für den Anwender übersichtlich.

20 setter schreiben und die Werte übergeben finde ich auch nicht übersichtlich (und wie gesagt, könnten eigentlich alle konstant sein).

Ich habe es nun über ein struct probiert (ähnlich wie in in der MCCI bzw. LMIC Library).

Im Usersketch beispiel.ino

const ModbusConf modbusConf = {
  addrWrite : 42, 
  addrWriteOld : 1,
  addrBegin : 2,
  addrSetDelay : 3,
  addrCommand : 4,
  addrClear : 5,
  addrHome : 6,
  addrSetCursor : 7,
  addrCursor : 8,
  addrBlink : 9,
  addrSetBacklight : 10,
  addrStream : 11,       
  addrCreateChar : 12
};

LiquidCrystal_Modbus lcd(node);          // you must hand over the Modbus Node Object

in der neuelib.h deklariere ich das struct und verweise auf die externe Struktur (weils ja im Usersketch ist).

struct ModbusConf {
  uint16_t addrWrite;
  uint16_t addrWriteOld;
  uint16_t addrBegin;     
  uint16_t addrSetDelay;    
  uint16_t addrCommand;    
  uint16_t addrClear;      
  uint16_t addrHome;      
  uint16_t addrSetCursor;  
  uint16_t addrCursor;     
  uint16_t addrBlink;      
  uint16_t addrSetBacklight;
  uint16_t addrStream;     
// uint16_t addrStreamExpand;  // intent: Master don't use addrStreamExpand in parallel. The sending interface is always a Stream interface
  uint16_t addrCreateChar;    
};

extern const ModbusConf modbusConf;  // define the Modbus Configuration in your main program (in your Arduino sketch)

// und später dann die Klasse + Konstruktor:

class LiquidCrystal_Modbus : public BufferPrint {
  protected:
    static const byte rwWrite = 0;                         // read/write WRITE
    static const byte rwRead = 1;                          // read/write READ
    byte wait = 5;                                         // use 5 for 9600 Baud - set some delay time
    ModbusMaster &node; 
    const uint8_t cols;                                    // real LCD colums - tbc: not needed
    const uint8_t rows;                                    // real LCD rows   - tbc: not needed   

  public:
    LiquidCrystal_Modbus(ModbusMaster &node, uint8_t cols = 16, uint8_t rows = 2) : 
      node(node),
      cols{cols},
      rows{rows}
      {}

kompiliert eigentlich auch. "Nachteil" ist nur, dass mit den aktuellen Arduino Einstellungen/C++11 die Reihenfolge der Membervariablen eingehalten werden muss und auch vollständig sein muss.
Damit kannn ich noch leben. "Vorteil" ist für mich, dass die Werte und deren Bedeutung lesbar sind.

Ich frage mich aber, ob es einen besseren Weg gibt viele Variablen übersichtlich zu übergeben.

Das finde ich grundsätzlich völlig ok.
Deine Art der Initialisierung könnte dir allerdings in (ferner?) Zukunft einen Streich spielen.
warning: ISO C++ does not allow GNU designated initializers [-Wpedantic]
C++11 kann es also nicht, gnu++11 schon.

Ansonsten finde ich, sind deine Möglichkeiten sowieso arg eingeschränkt.
Denn viele Möglichkeiten gibt es nicht, Abhängigkeiten in eine Funktion/Objekt/Methode einzuschleusen.

  1. Constructor Injekction
  2. Setter Injection
  3. Über Funktions/Methoden Parameter
  4. Template Parameter
  5. Zugriff auf globale Variablen in der Funktion/Methode

Du hast dich für Weg 1 entschieden!

const ModbusConf modbusConf 
{
  addrWrite:        42, 
  addrWriteOld:      1,
  addrBegin:         2,
  addrSetDelay:      3,
  addrCommand:       4,
  addrClear:         5,
  addrHome:          6,
  addrSetCursor:     7,
  addrCursor:        8,
  addrBlink:         9,
  addrSetBacklight: 10,
  addrStream:       11,       
  addrCreateChar:   12
};

C++ erzeugt den passenden Konstruktor automatisch.
Verstehe ich! Würde das vermutlich auch so (ähnlich) tun.

Wo ich aber Verständnisschwierigkeiten habe ist, dass du dann

auf 5 zurückfällst.....

Meiner Ansicht nach darf eine Lib keine Annahmen über das Hauptprogramm machen.

Vergleich:
Das extern TwoWire Wire; in Wire.h führt im Endeffekt dazu, dass es immense Schwierigkeiten gibt, wenn man Wire1 für einen Sensor verwenden möchte, die Lib aber Wire nutzt.
Weil eben dort mit diesen Abhängigkeiten derbe geschlampt wurde. Das scheint es einfacher zu machen. Ist aber eher sowas wie eine Fette Kugel am Bein, wenn man vom ausgetretenen Pfad abweichen möchte/muss

Die Struktur scheint mir ok, glaube nicht, dass man das "schöner" hinbekommt
Das extern gefällt mir eher nicht

Hier, in der Klasse, scheinst du ja deine Struktur zu gebrauchen.
Also rein damit, über die Konstruktor Injektion oder Setter Injection, und dann im Objekt halten(Aggregation?/Komposition!).
Schlimmstenfalls über das Funktions/Methoden Parameter Verfahren. Was dir aber vermutlich gründlich das Interface deiner Klasse versaut.

Ganz allgemein:
Wenn die die Erzeugung eines Objektes arg kompliziert ist, oder sehr häufig geschehen muss, baut man sich eine Fabrik, so kann man dann die unvermeidliche Komplexität auslagern.
Falls du AVR verwendest, wird man die dynamische Speicherverwaltung vermeiden wollen.
Dann über einen Copy Constructor. Der wird auch automatisch von C++ gebaut.


Habe ich dein Problem überhaupt verstanden?

vermutlich schon.
Ich weis nicht ob es mehr zur Klärung beiträgt, der Ausschnitt ist aus einem Teil "LCD Sender Library". Es soll dem Anwender auf einem Modbus-Master ein LCD API 1.0 like Interface geben, die Daten dann über RS485 an einen Slave senden, der seinesgleichen dann die Daten empfängt und dann tatsächlich am LCD anzeigt.

Gröbere Baustelle, funktioniert aber soweit, aber jetzt geht es halt ans hübsch machen.

Wenn ich dich recht verstehe, ist also mein Ansatz nun auch nur eine globale im Usersketch, und ich dachte schon ich hätt' da was Gutes... ;-(

Wenn ich die ganze extern Zeile weglasse bekomme ich das beinhart quittiert:
cd_Modbus_BufferPrint.h:113:46: error: 'modbusConf' was not declared in this scope

Ich muss jetzt aber ein wenig googeln was du mir da alles geschrieben haben.

Verständlich.

------
Wenn modbusConf eine feste Abhängigkeit ist, und nirgendwo anders gebraucht wird, dann gehört es Thematisch in die Klasse rein.
(Hmmm.... Ich glaube, das war eine Frage)

Irgendwie fehlt mir der Sinn hinter dem Beispiel. Wie werden die vielen Parameter nachher benutzt? Für Aufrufe von Methoden über Kommandocodes hätte ich einen switch benutzt, keine Datenstruktur.

Dein Ansatz mit der struct ModbusConf sieht auf den ersten Blick gut aus.

Diese const ModbusConf modbusConf sollte dann aber (als reference) an den Konstruktor übergeben werden.

In deinem Beispiel verstehe ich allerdings die Art der Parameter nicht. Sind das Adressen oder Indizes von Funktionen (callback)?
Speziell im Zusammenhang mit deiner Modbus-Verbindung... Was gehen Interna den Benutzer eines remote-Objekts "auf der anderen Seite" an?
(Allerdings, wie gesagt, vermutlich nur Unverständnis meinerseits)

Da bin ich mir nicht so ganz sicher....
Denn dann wird eine globale Instanz erzeugt werden müssen.
Nur zu dem Zweck, eine (einmalige?) Kopie zu vermeiden?
Dann ist es wieder nicht kompakt.

Aber was solls... ist nicht wirklich mein Problem.

Ich lasse die beiden Varianten mal gegeneinander antreten.....

modbusConf in Kopie vom Stack
und node per Referenz
Nur mal so zum Vergleich... kann man ja das "schönere" umsetzen.

#include <Streaming.h> // die Lib findest du selber ;-)
Stream &cout = Serial; // cout Emulation für "Arme"

// ab hier in der Lib
struct ModbusConf
{
  uint16_t addrWrite;
  uint16_t addrWriteOld;
  uint16_t addrBegin;
  uint16_t addrSetDelay;
  uint16_t addrCommand;
  uint16_t addrClear;
  uint16_t addrHome;
  uint16_t addrSetCursor;
  uint16_t addrCursor;
  uint16_t addrBlink;
  uint16_t addrSetBacklight;
  uint16_t addrStream;
  // uint16_t addrStreamExpand;  // intent: Master don't use addrStreamExpand in parallel. The sending interface is always a Stream interface
  uint16_t addrCreateChar;
};

class BufferPrint  {}; //dummy
class ModbusMaster {}; //dummy


class LiquidCrystal_Modbus : public BufferPrint
{
  protected:
    const byte rwWrite = 0;                         // read/write WRITE
    const byte rwRead = 1;                          // read/write READ
    byte wait = 5;                                         // use 5 for 9600 Baud - set some delay time
    ModbusMaster &node;
    const ModbusConf modbusConf;
    const uint8_t cols;                                    // real LCD colums - tbc: not needed
    const uint8_t rows;                                    // real LCD rows   - tbc: not needed

  public:
    LiquidCrystal_Modbus(ModbusMaster &node, ModbusConf modbusConf, uint8_t cols = 16, uint8_t rows = 2) :
      node(node),
      modbusConf{modbusConf},
      cols{cols},
      rows{rows}
    {}
    const ModbusConf &getConf() const {return modbusConf;}
};

// Dieses in der Projekt Initialisierungs Datei *.h + *.cpp
LiquidCrystal_Modbus modbusLCDFabrik(ModbusMaster &node)
{
  ModbusConf modbusConf
  {
    addrWrite:        42,
    addrWriteOld:      1,
    addrBegin:         2,
    addrSetDelay:      3,
    addrCommand:       4,
    addrClear:         5,
    addrHome:          6,
    addrSetCursor:     7,
    addrCursor:        8,
    addrBlink:         9,
    addrSetBacklight: 10,
    addrStream:       11,
    addrCreateChar:   12
  };
  return LiquidCrystal_Modbus(node, modbusConf);
}

// Hauptprogramm/Hauptdatei
ModbusMaster node; // muss global, da per Referenz uebergeben
LiquidCrystal_Modbus lcd = modbusLCDFabrik(node);

void setup()
{
  Serial.begin(9600);
  cout << F("Start: ") << F(__FILE__) << endl;
  cout << lcd.getConf().addrStream    << endl;
}

void loop()
{
}
1 Like

Ist schon akzeptiert.
Ich brauch das Gegenstück zwar auch auf der Slave Seite. Aber das ist schon ok.

Das sind die "Adressen" - Modbus Register.
Für jede Funktion die ich an den Slave sende, verwende ich eine eigene Addresse.

zwei Beispiele vom LCD-Sender:

/*  
    LCD API: Clear the display and place cursor at 0,0
*/
    int clear()
    {
      byte result = node.writeSingleRegister(modbusConf.addrClear, 0);
      delay(wait);
      return result;
    };  
    
/*  
    LCD API: Home the cursor to 0,0 and leave displayed characters
*/
    int home()
    {
      byte result = node.writeSingleRegister(modbusConf.addrHome, 0);
      delay(wait);
      return result;
    }

auf der anderen Seite gibts dann den Empfänger (Slave). Den muss ich aber erst ähnlich umbauen.

/*
   this functions will be called,
   when a Modbus Master sends a value with FC6 to a valid address without internal reg Buffer.
*/
void modbusRegisterReceived(const uint16_t reg, const uint16_t value)
{
  DEBUG(F("received:")); DEBUG(reg); DEBUG(F(" value:")); DEBUGLN(value);
  byte hi = value >> 8;
  byte lo = value & 0xFF;
  switch (reg)
  {
    case addrStreamExpand :
      if (lo >= 0x20)        // only write printable values MISSING
      {
        modbusWrite(lo);
      }
      break;
    case addrStream :
    case addrWrite :
      //if (lo >= 0x20)        // only write printable values MISSING
      //{
      //  modbusWrite(lo);
      // }
      //break;
    case addrWriteOld :      // write any value (incl. special chars)
      modbusWrite(lo);           
      break;
    case addrBegin : 
      lcd.begin(); 
      break;
    case addrClear :
      lcd.clear();
      lcdWasUpdated = true;
      break;
    case addrHome :
      lcd.setCursor(0, 0);   // on most LCD faster than .home()
      break; 
    case addrCursor :
      if (lo == 0) 
        lcd.noCursor(); 
      else 
        lcd.cursor();
      break;
    case addrBlink :
      if (lo == 0) 
        lcd.noBlink(); 
      else 
        lcd.blink(); 
      break;
    case addrSetCursor :
      lcd.setCursor(hi, lo);
      lcdWasUpdated = true;
      break;
    case addrSetBacklight : 
      if (lo == 0) 
        lcd.noBacklight(); 
      else 
        lcd.backlight(); 
      break;
  }
}

Ich will halt die Adressen nicht fix im LCD-Sender und LCD-Empfänger verdrahten sondern (theoretisch) dem User die Möglichkeit geben,
andere Adressen zu nutzen. Vor dem struct war es eine zentrale .h mit simplen constexpr.

Warum verwendest du nicht Modbus Funktionscodes aus dem User Defined-Bereich? Da brauchst du keine Parameter übertragen, wenn der Befehl keine braucht.

Normalerweise macht man das mit einem einzelnen Kommando-Register, in das man die Kommando-Nummer reinschreibt. Die Nummern müssen für beide Seiten gleich sein, da empfiehlt sich eine gemeinsame enum.

weil ich mit einer Lib begonnen habe die nur FC3/6/16 kann.
Und ob ich bei einigen Befehlen ein uint16_t mit 0 mitschicke oder nicht - das geht halt unter.

Das ist kein Nachteil und ist auch mit neueren Versionen des Compilers/Toolchains so. Das ist nun einmal genauso erforderlich. Darum schreibe ich immer eine Initialisierungsliste und klatsche nicht alles nebeneinander. Ist zudem leichter lesbar und änderbar.

Allerdings wage ich zu bezweifeln das ein Nutzer all diese Parameter wirklich einstellen muss. Ich meine ein lcd clear oder lcd home muss doch kein User einstellen. Oder? Das kennt doch die lcd Lib. Wenn es eine Verknüpfung mit der Board-Adresse werden soll, dann sollte das intern zusammengebaut und/oder übertragen werden. Der Nutzer gibt nur eine Adresse an und hat im Grunde mit dem LCD im Speziellen nichts zu tun. Oder ich habe das Problem nicht verstanden.