Wasserfluss per ISR ermitteln > Loopdauer

Grüß Gott.
Vor etwa einem Jahr hatte ich einen Beispiel-Sketch gefunden, den ich bei mir (ESP32 Devkit) implementieren wollte. Ein Wasserfluss-Sensor soll per Interrupt Service Routine im Hintergrund den Wasserverbrauch des Gartenwassers messen. Dafür hatte ich mehrere Messungen durchgeführt, um meinen Sensor "zu eichen". Und nun stelle ich fest, dass sich die erfasste Quantität verringert, wenn die Dauer der Loop zunimmt. Kurzum: nur bei stets gleichem delay(1000) am Ende des Sketches stimmen sind die Messwerte korrekt. Könnte mir jemand sagen wie ich den Sketch umzuschreiben habe, um die Messung unabhängig von der Dauer einer Schleife zu garantieren?

#define WATERFLOWPIN 27                   // Pin water flow sensor is connected to
float calibrationFactor = 5.85;           // [Original: "4.5"]
volatile byte waterPulseCount;
float flowRate;
float flowGardenLiters;
float dayGardenLiters;
unsigned long oldTime;


void IRAM_ATTR waterConsumption_ISR() {   // Interrupt Service Routine (ISR)
  // Increment the pulse counter
  waterPulseCount++;
}


void setup() {
  Serial.begin(115200);
  
  pinMode(WATERFLOWPIN, INPUT_PULLUP);
  waterPulseCount   = 0;
  flowRate          = 0.0;
  flowGardenLiters  = 0;
  dayGardenLiters   = 0.0;
  oldTime           = 0;
  attachInterrupt(digitalPinToInterrupt(WATERFLOWPIN), waterConsumption_ISR, FALLING);
  delay(250);
}


void loop() {

   if((millis() - oldTime) > 1000) {      // Only process counters once per second
    // Disable the interrupt while calculating flow rate and sending the value to the host
    detachInterrupt(WATERFLOWPIN);

    // Because this loop may not complete in exactly 1 second intervals we calculate
    // the number of milliseconds that have passed since the last execution and use
    // that to scale the output. We also apply the calibrationFactor to scale the output
    // based on the number of pulses per second per units of measure (litres/minute in
    // this case) coming from the sensor.
    flowRate = ((1000.0 / (millis() - oldTime)) * waterPulseCount) / calibrationFactor;
    
    // Note the time this processing pass was executed. Note that because we've
    // disabled interrupts the millis() function won't actually be incrementing right
    // at this point, but it will still return the value it was set to just before
    // interrupts went away.
    oldTime = millis();
    
    // Divide the flow rate in litres/minute by 60 to determine how many litres have passed through the sensor in this 1 second interval, then multiply by 1000 to convert to millilitres.
    flowGardenLiters = (flowRate / 60) * 1000;

    // Reset the pulse counter so we can start incrementing again
    waterPulseCount = 0;
    
    // Enable the interrupt again now that we've finished sending output
    // attachInterrupt(WATERFLOWPIN, waterConsumption_ISR, FALLING); // Siehe #811 !!
    attachInterrupt(digitalPinToInterrupt(WATERFLOWPIN), waterConsumption_ISR, FALLING);
  }
  dayGardenLiters += flowRate / 60;
  Serial.print("   GARTENWASSER "); Serial.print("Aktueller Wasserfluss: "); Serial.print(int(flowRate)); Serial.println(" Liter/Minute. ");
  Serial.print("                "); Serial.print(dayGardenLiters, 2); Serial.println(" l/Tag  \t");
  Serial.println("");
  delay(1000);
}

Vielen Dank und einen schönen Sonntag!

a)
bist dir sicher dass dein 1 byte großer Zähler groß genug ist für die Impulse die in einer Sekunden reinkommen können?

b)
du hast so ein schönes Millis Konstrukt zur Übernahme der Variable,
warum verwendest dann für deine Serielle Ausgabe wieder einen blockierenden delay()?

Zu a) Das habe ich übersehen, da der Sketch ja prinzipiell funktionierte. Reicht hier ein volatile int?

Zu b) Das delay am Ende der Loop soll andere Aufgaben in der Loop simulieren, z.B. das Abwarten einer OLED-Anzeige oder anderes.

Das musst du wissen.

Und können deine Impulse negativ werden? Nein. Was willst du dann mit int?

Worauf soll das OLED warten ?
Etwas zu verzögern ist hier schlecht. Da nimmt man einen Timer mit millis() oder Interval.h der gezielt eine Aktion startet.

wir kennen deinen Sensor nicht.
Klar ist, deine Variable muss groß genug sein um zwischen deinen Blockaden alle Impulse aufnehmen zu können.
Bei Byte wirds wenig.
Negative Werte wirst nicht brauchen nehme ich an.
uint16_t oder uint32_t ...

Das ist auch Mist.
Prinzipiell müsste es detachInterrupt(digitalPinToInterrupt(WATERFLOWPIN)); heißen. Ausserdem stellst du so sicher, dass Interrupts verloren gehen.

Richtig wäre, sich die Impulse zu merken und den Zähler zurückzusetzen, während Interrupts geschlossen sind, dann beliebig langsam mit der gemerkten Zahl weiterarbeiten.

const float ppl=5.85; // Pulse pro Liter
const uint16_t intervall = 1000; // einmal je Sekunde
...
void loop () {
 if (millis() - oldTime >= intervall)  {
  noInterrupts();
  byte pulsesTemp = waterPulseCount; 
  waterPulseCount = 0;
  interrupts();
  flowRate = pulsesTemp / ppl * 60000 / (millis() - oldTime); // liter pro minute  
  oldTime += intervall;  
  // ... alles was noch einmal je Sekunde (oder seltener) zu machen ist
 }
}

Vermutlich ist die Anzahl Pulse je Intervall ( hier 1 sec ) deutlich unter 255. Dumm ist's nur, wenn waterPulseCount je Sekunde zwischen 0 und 1 hin und her springt. Dann sollte man intervall erhöhen, um eine einigermaßen ruhige flowRate zu erhalten.

Na das kann Zufall sein. Denn Du gibst ja keine Messwerte aus, sondern schon errechnete. Und da ist Dein Problem.
Du benutzt einen Calibrierfaktor, der angepasst an Deine Situation ist.

Ich sehe mehrere Schwachstellen und hab die mal reinkommentiert:

if ((millis() - oldTime) > 1000) // hier ist millis()-oldTime mindestens 1001
{
  detachInterrupt(WATERFLOWPIN);
  flowRate = ((1000.0 / (millis() - oldTime)) * waterPulseCount) / calibrationFactor; // rechnest aber mit 1000 und hier ist noch mehr Zeit vergangen
  oldTime = millis(); // hier merkst Du Dir die Zeit
  flowGardenLiters = (flowRate / 60) * 1000;  // was ist das für eine 1000?
  waterPulseCount = 0;
  attachInterrupt(digitalPinToInterrupt(WATERFLOWPIN), waterConsumption_ISR, FALLING); // aber erst ab hier misst Du wieder
}

Baue Deinen code blockadefrei und ohne delay().
Alles was Du an Zeit verbratest, und auch wenn nicht genau auf Deine 1000ms kommst, kannst Du mit rechnen lassen!

Grundsätzlich schon mal als Ansatz:

tikTime = millis();
lastCount = 0;
intervall = 1000;
if (tikTime - oldTime >= intervall)
{
  detachInterrupt(WATERFLOWPIN);
  lastCount = waterPulseCount;
  waterPulseCount = 0;
  oldTime = millis();
  attachInterrupt(digitalPinToInterrupt(WATERFLOWPIN), waterConsumption_ISR, FALLING);
  flowRate = (float)((intervall / (tikTime - oldTime)) * lastCount) / calibrationFactor;
  flowGardenLiters = (flowRate / 60) * 1000;
}

Du musst, so wie in der ISR, auch hier schnellst möglich Deinen Wert loswerden und den Interrupt wieder aktivieren.

Versuchs mal mit.

Vielen Dank für die Antworten.

Ich habe die letzten Stunden reichlich probiert und ebenso ein Zeitfenster für Serial.prints eingebaut, damit nur zwei Mal pro Minute Daten an den Serial gesendet werden. Damit habe ich den Sensor auch neu Eichen müssen. Das aktuelle Zwischenergebnis sieht nun so aus:

#define WATERFLOWPIN 27
float ppl = 6.89;
float waterInterval = 1000;
volatile unsigned int waterPulseCount;
float flowRate;
float dayGardenLiters;
unsigned long oldWaterTime;

unsigned long oldSerialTime;
float serialInterval = 30000;


void IRAM_ATTR waterConsumption_ISR() {   // Interrupt Service Routine (ISR)
  waterPulseCount++;              // Increment the pulse counter
}


void setup() {
  Serial.begin(115200);
  oldSerialTime = serialInterval;
  Serial.println("");

  pinMode(WATERFLOWPIN, INPUT_PULLUP);
  waterPulseCount = 0;
  flowRate = 0.0;
  dayGardenLiters = 0.0;
  oldWaterTime = 0;
  attachInterrupt(digitalPinToInterrupt(WATERFLOWPIN), waterConsumption_ISR, FALLING);

  delay(100);
}


void loop() {
  unsigned long nowTime = millis();
  if(nowTime - oldWaterTime >= waterInterval) {      // Only process counters once per <waterInterval>
    detachInterrupt(digitalPinToInterrupt(WATERFLOWPIN));
    unsigned int pulsesTemp = waterPulseCount;
    waterPulseCount = 0;

    attachInterrupt(digitalPinToInterrupt(WATERFLOWPIN), waterConsumption_ISR, FALLING);

    flowRate = ((waterInterval / (nowTime - oldWaterTime)) * pulsesTemp) / ppl;
    dayGardenLiters += flowRate / 60;
    oldWaterTime = millis();
  }

  if(nowTime - oldSerialTime >= serialInterval) {      // Only process counters once per <waterInterval>
    Serial.print("   GARTENWASSER "); Serial.print("Aktueller Wasserfluss: "); Serial.print(flowRate, 1); Serial.println(" Liter/Minute");
    Serial.print("                "); Serial.print(dayGardenLiters, 2); Serial.println(" l/Tag  \t");
    Serial.println("");
    oldSerialTime = millis();
  }
  delay(250);
}

Eine feine Sache, jede Aufgabe nur alle X-Minuten auszuführen. Eingangs fragte ich noch, wie nun diverse OLED-Anzeigen, denen ein Zeitfenster zur Einblendung gegeben wird, so in den Sketch eingebaut werden können, dass die Loop sich eben nicht verlangsamt. Bisher hatte ich das z.B. mit der U8g2lib.h-Bibliothek und 2 OLEDs wie im enthaltenen Beispielsketch so umgesetzt:

  u8g2_1.firstPage();
  do {
    u8g2_1.setFont(u8g2_font_7x14B_tr);
    u8g2_1.setCursor(5, 10);
    u8g2_1.print("LINKS EINS");
    u8g2_1.setCursor(5, 35);
    u8g2_1.print("LINKS ZWEI");
    u8g2_1.setCursor(5, 52);
    u8g2_1.print("LINKS DREI");
  } while (u8g2_1.nextPage());
  delay(5);

  u8g2_2.firstPage();
  do {
    u8g2_2.setFont(u8g2_font_7x14B_tr);
    u8g2_2.setCursor(5, 10);
    u8g2_2.print("RECHTS EINS");
    u8g2_2.setCursor(5, 35);
    u8g2_2.print("RECHTS ZWEI");
    u8g2_2.setCursor(5, 52);
    u8g2_2.print("RECHTS DREI");
  } while (u8g2_2.nextPage());
  delay(SCREEN_MILLISECONDS);

Die SCREEN_MILLISECONDS sind z.B. 5 Sekunden, bei der die Loop dann hängt. Wie kann ich diese zeitlich von der Loop trennen?

sinngemäßt doch wieder :

wir wär's mit einer
oldDisplayTime und einer displayInterval?
dafür keine do/while und kein delay...

Sehr gerne, nur sind es zahlreiche OLED-Anzeigen, denen doch jeweils eine gewisse Zeit gegeben werden muss, um abgelesen werden zu können. Wie ist das dann zu programmieren?

Dann arbeite das in einer Statemachine ab.
Dann hast einen "blink without delay" Zeitgeber mit den millis
und drinnen eine zähler der bestimmt, welche Anzeige angezeigt werden soll.

Bei BlinkWithoutDelay liegt der "Trick" darin, dass ein einzelner loop - Durchlauf fast keine Zeit braucht, weil kein delay drin ist. In jeder Millisekunde läuft dann loop unendlich oft durch und macht fast nie etwas.
Dass - bei jeweils eigenen "oldTime" - Merkern (mit unterschiedlichen Namen) beliebig viele Zyklen gleichzeitig und unabhängig realisiert werden können, sollte dir klar sein ...


Und nimm endlich das falsche detachInterrupt raus :)

Du hast den nicht geeicht. Ganz bestimmt.
Du hast mir aber auch nicht zugehört:

    attachInterrupt(digitalPinToInterrupt(WATERFLOWPIN), waterConsumption_ISR, FALLING);

    flowRate = ((waterInterval / (nowTime - oldWaterTime)) * pulsesTemp) / ppl;
    dayGardenLiters += flowRate / 60;
    oldWaterTime = millis();

oldWaterTime wird zu spät gesetzt.

So und nu bin ich mal ganz nett und bitte Dich, Deinen Code zu zeigen - vollständig.
Es macht keinen Sinn, das hier stückweit aufzudröseln.
Weil es grad so schön aktuell ist: 4 Post - Erledigt: Elemente eines Stringarrays in Befehle einbauen - #7 by my_xy_projekt

Das mit dem Umlauf benutz Du ja schon:

  if(nowTime - oldWaterTime >= waterInterval) {      // Only process counters once per <waterInterval>

Das muss nur noch angepasst werden. Aber dazu zeige was Du hast - alles andere ist raten!

Deinen Vorschlag hatte ich zuerst übernommen. In der ersten Loop konnte der Wasserfluss auch berechnet werden, ab der Zweiten jedoch nicht mehr (nan). Der Zeitstempel von oldWaterTime ist ja in der Berechnung von "flowRate" entscheidend, weshalb ich direkt nach der Berechnung oldWaterTime = millis setze. Und so funktioniert das auch nach der ersten Programmschleife.

Vielen Dank für die Erinnerung: der eingangs erwähnte command noInterrupts(); und interrupts(); setzt doch alle Interrupts (sofern vorhanden) aus, richtig? Wenn also ein weiteres z.B. Hardware-Interrupt vom ESP32 verwaltet wird, ist dieses mit dem Befehl noInterrupts mit einbegriffen?

Damit werde ich mich auseinandersetzen, noch bevor ich hier im Stundentakt Anfängerfragen stelle. :slight_smile:

Richtig. in der (möglichst kurzen) noInterrupts - Phase läuft der loop-Thread wie ein Interrupt-Thread, kommt sich also mit der ISR, die deine Pulse zählt, nicht ins Gehege. Wenn in dieser Zeit ein weiterer Interrupt kommt, wird er erst hinterher gezählt, geht aber nicht verloren (wie es mit detachInterrupt der Fall wäre).
Ob ESP verschiedene Interrupt-Prioritäten kennt, weiß ich nicht. Arduino und avr-Controller kennen das nicht.

Dass ein ESP kein avr-Controller ist, ist klar. Dank ESP core geht die simple Arduino-Programmierung sogar mit einem versteckten freeRTOS im Hintergrund. Wenn man nicht zu großen Unsinn in loop() macht.

Bei dem Versuch, die Statemachine zu umgehen, habe ich mir für die OLED-Anzeigen nun nach etwas Tüftelei folgendes ausgedacht (Wasserfluss-Interrupt aus Gründen der Übersichtlichkeit temporär herausgenommen):

// OLED Test-Sketch without delay on ESP32 Devkit v1
#define MIN_LOOP_DELAY 900
#define SCREEN_MILLISECONDS 50

float serialInterval = 30000;             // Einmal alle 30 Sekunden = 30000ms
unsigned long oldSerialTime;              // Timer for serial-prints

float oledInterval = 2500;                // Einmal alle 2,5 Sekunden = 2500ms
unsigned long oldOledTime;                // Timer for OLEDs
byte oledChange;

unsigned long resetLoops;
//
// OLED /////////////////////////////////////////////////////////////////////////////////////////////////
#include <U8g2lib.h>                      // OLED U8G-Library
U8G2_SSD1306_128X64_NONAME_2_HW_I2C u8g2_1(U8G2_R0);
U8G2_SSD1306_128X64_NONAME_2_HW_I2C u8g2_2(U8G2_R0);
//
// WIFI /////////////////////////////////////////////////////////////////////////////////////////////////
#include <WiFi.h>
#include <HTTPClient.h>
const char* ssid = "ssid1";
const char* password = "password1";
const char* ssid2 = "ssid2"               // Alternative WiFi
const char* password2 = "password2";      // Alternative Password
IPAddress ip(192, 178, 18, 12);           // ESP32 static IP address configuration
IPAddress gateway(192, 178, 18, 1);       // IP Address of WiFi Router
IPAddress subnet(255, 255, 255, 0);       // Subnet mask
IPAddress dns1(192, 178, 18, 1);          // DNS1
IPAddress dns2(188, 229, 114, 28);        // DNS2
//
// TIME LIBRARY /////////////////////////////////////////////////////////////////////////////////////////////////
#include <time.h>
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3600;          // Adjust the UTC offset for your timezone in milliseconds
const int daylightOffset_sec = 3600;      // If daylight saving time: set it to 3600. Otherwise, set it to 0.
int second;
int minute;
int hour;
int day;
int month;
int year;
int weekday;
struct tm timeinfo;
//
/////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {
  Serial.begin(115200);
  oldSerialTime = serialInterval;
  oldOledTime = oledInterval;
  oledChange = 1;
  resetLoops = 1;

  // OLED /////////////////////////////////////////////////////////////////////////////////////////////////
  u8g2_1.setI2CAddress(0x3C * 2);         // I2C address OLED1
  u8g2_2.setI2CAddress(0x3D * 2);         // I2C address OLED2
  u8g2_1.begin();
  u8g2_2.begin();
  u8g2_1.clearBuffer();                   // Clear the internal memory
  u8g2_2.clearBuffer();

  WiFi.mode(WIFI_OFF);
  WiFi.config(ip, gateway, subnet, dns1, dns2);
  WiFi.mode(WIFI_STA);
  delay(50);

  Serial.println("");
  Serial.println("============================================");
  Serial.print("Connecting to WiFi ONE..");
  WiFi.begin(ssid, password);
  unsigned int wifitrials = 0;
  while (WiFi.status() != WL_CONNECTED && wifitrials <= 5) {
    delay(1000);
    Serial.print(".");
    wifitrials++;
  }
  Serial.println(" ");
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("ESP32 connected to the WiFi network ONE with IP: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("WiFi network NOT connected!");
    delay(500);
    Serial.print("Trying alternative WiFi network TWO..");
    WiFi.begin(ssid2, password2);
    unsigned int wifi2trials = 0;
    while (WiFi.status() != WL_CONNECTED && wifi2trials <= 5) {
      delay(1000);
      Serial.print(".");
      wifi2trials++;
    }
    Serial.println(" ");
    if (WiFi.status() == WL_CONNECTED) {
      Serial.print("ESP32 connected to the WiFi network TWO with IP: ");
      Serial.println(WiFi.localIP());
    } else {
      Serial.println("WiFi network STILL NOT connected!");
    }
  }
  Serial.println(" ");
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}

//
/////////////////////////////////////////////////////////////////////////////////////////////////
void loop() {
  unsigned long nowTime = millis();

  if (nowTime - oldSerialTime >= serialInterval) {
    printLocalTime();
    Serial.println("==============  TESTSKETCH  ========================================================");
    Serial.print("Schleifen: "); Serial.println(resetLoops);
    Serial.println("");
    oldSerialTime = millis();
  }

  if(nowTime - oldOledTime >= oledInterval) {
    if(oledChange == 1) {
      u8g2_1.firstPage();
      do {
        u8g2_1.setFont(u8g2_font_7x14B_tr);
        u8g2_1.setCursor(5, 20);
        u8g2_1.print("LINKS 1 EINS");
        u8g2_1.setCursor(5, 35);
        u8g2_1.print("LINKS 1 ZWEI");
        u8g2_1.setCursor(5, 50);
        u8g2_1.print("LINKS 1 DREI");
      } while(u8g2_1.nextPage());

      u8g2_2.firstPage();
      do {
        u8g2_2.setFont(u8g2_font_7x14B_tr);
        u8g2_2.setCursor(5, 20);
        u8g2_2.print("RECHTS 1 EINS");
        u8g2_2.setCursor(5, 35);
        u8g2_2.print("RECHTS 1 ZWEI");
        u8g2_2.setCursor(5, 50);
        u8g2_2.print("RECHTS 1 DREI");
      } while(u8g2_2.nextPage());
      oledChange++;

    } else if(oledChange == 2) {
      u8g2_1.firstPage();
      do {
        u8g2_1.setCursor(5, 20);
        u8g2_1.print("LINKS 2 EINS");
        u8g2_1.setCursor(5, 35);
        u8g2_1.print("LINKS 2 ZWEI");
        u8g2_1.setCursor(5, 50);
        u8g2_1.print("LINKS 2 DREI");
      } while(u8g2_1.nextPage());

      u8g2_2.firstPage();
      do {
        u8g2_2.setCursor(5, 20);
        u8g2_2.print("RECHTS 2 EINS");
        u8g2_2.setCursor(5, 35);
        u8g2_2.print("RECHTS 2 ZWEI");
        u8g2_2.setCursor(5, 50);
        u8g2_2.print("RECHTS 2 DREI");
      } while(u8g2_2.nextPage());
      oledChange++;

    } else if(oledChange == 3) {
      u8g2_1.firstPage();
      do {
        u8g2_1.setCursor(5, 20);
        u8g2_1.print("LINKS 3 EINS");
        u8g2_1.setCursor(5, 35);
        u8g2_1.print("LINKS 3 ZWEI");
        u8g2_1.setCursor(5, 50);
        u8g2_1.print("LINKS 3 DREI");
      } while(u8g2_1.nextPage());

      u8g2_2.firstPage();
      do {
        u8g2_2.setCursor(5, 20);
        u8g2_2.print("RECHTS 3 EINS");
        u8g2_2.setCursor(5, 35);
        u8g2_2.print("RECHTS 3 ZWEI");
        u8g2_2.setCursor(5, 50);
        u8g2_2.print("RECHTS 3 DREI");
      } while(u8g2_2.nextPage());
      oledChange = 1;
    }

    oldOledTime = millis();
  }
  resetLoops++;
  delay(MIN_LOOP_DELAY);
}
/////////////////////////////////////////////////////////////////////////////////////////////////

void printLocalTime() {
  if(!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    //return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S"); // %A: day of week, %B: month of year, %D: day of month, %Y: year, %H: hour, %M: minutes, %S: seconds
  second = timeinfo.tm_sec;
  minute = timeinfo.tm_min;
  hour = timeinfo.tm_hour;
  day = timeinfo.tm_mday;
  month = timeinfo.tm_mon + 1;
  year = timeinfo.tm_year + 1900;
  weekday = timeinfo.tm_wday;
}

Um die schnell zunehmenden Loops auszubremsen, verwende ich MIN_LOOP_DELAY mit z.B. 900ms Zeitverzögerung: Wenn die Loops und aktive Schaltungen gezählt werden und als Rechengrundlage für den Prozentsatz der aktiven Laufzeit herangezogen wird, müssen die Variablen entsprechend in der Lage sein, die hohe Anzahl der Durchläufe auch aufnehmen zu können.

Kann das obige Beispiel mit festen Zeitfenstern für Serial.prints und den OLED-Anzeigen optimiert werden?

Hallo,
mit ist nicht klar warum Du den loop künstlich lang machen willst. Normalerweise ist man bemüht ihn möglichst schnell zu machen um auf alles schnell reagieren zu können. Ich denke Du hast die Verwendung von millis() noch nicht ganz verstanden. Die do.. while Shleifen sind auch völliger Unsinn. Insofern hast Du den Aufbau einer Statemachine nicht verstanden. Stell Dir einfach mal vor Du willst gleichzeitig noch etwas anderes machen. z.B einen Schalter abfragen, wie soll das gehen. Insofern JA das kannst Du verbessern. Aber eigentlich wurde Dir das alles schon gesagt.

Heinz

einen Kaffee kochen :grinning: