non-blocking function mit millis()

Hallo!

Kurze Vorstellung des Projekts:

  • Sequenzielles Öffnen und Schliessen von 4 Relays an die Magnetventile angeschlossen werden
  • Die Schaltzeiten werden in Sekundenkommagetrennt an an die serielle Schnittstelle geschickt (zb. "10,20,30,40\n" -> Ventil 1 für 10 Sekunden offen, dann Ventil 2 für 20 Sekunden usw.)
  • "Not-Aus"- Funktion: wird zb. 'S' geschickt, soll der loop abbrechen und alle Relais sollen auf OFF geschaltet werden

Derzeit arbeitet das Ganze mit delays, was es mit nicht möglich macht, das "Not-Aus"- Signal während des loops zu empfangen und darauf zu reagieren.
Hab mir schon das BlinkWithoutDelay angesehen, weiss aber noch nicht, wie ich es auf mein Projekt adaptieren kann...bin erst vor Kurzem in die Arduino-Welt eingetaucht.

Als Anfänger würde ich mich über Unterstützung bei der Umsetzung und Feedback über etwaige Optimierungsmöglichkeiten freuen!

Hier der aktuelle Stand:

char buffer[300];
long data[4];
int bindex,dindex = 0;
char delimiter = ',';
char newline = '\n';
char stopSign = 'S';

int R1 = 10;
int R2 = 11;
int R3 = 12;
int R4 = 13;

int R_ON = LOW;
int R_OFF = HIGH;

void setup() {
  //relais
  pinMode(R1, OUTPUT);
  pinMode(R2, OUTPUT);
  pinMode(R3, OUTPUT);
  pinMode(R4, OUTPUT);
  digitalWrite(R1, R_OFF);
  digitalWrite(R2, R_OFF);
  digitalWrite(R3, R_OFF);
  digitalWrite(R4, R_OFF);

  Serial.begin(9600);
}
void loop() {
  while(Serial.available() > 0) {
    parseInput();
  }
}


void parseInput() {
  //aktuelles Zeichen einlesen
  buffer[bindex] = Serial.read();

  //Not-Aus ('S')
  if (buffer[bindex] == stopSign){
    stopIt();
  }

  //Trenner (,) oder Zeilenende (\n) gefunden
  if(buffer[bindex] == delimiter || buffer[bindex] == newline) {
    //Null-Terminierung
    buffer[bindex] = 0;

    //Umwandeln in eine Zahl
    data[dindex] = atol(buffer);

    //Datenindex rotieren
    dindex = ++dindex % 4;

    //wenn index wieder auf 0 gesetzt wird, dann wurden 4 werte eingelesen
    if(dindex==0) {
      doIt();
    }

    //Buffer Index zurücksetzen
    bindex = 0;
  } 
  else {
    bindex++;
  }

}


void doIt() {
  Serial.println("=== Bewaesserung an ===");

  printState(1, data[0]);
  openValve(R1, data[0]);

  printState(2, data[1]);
  openValve(R2, data[1]);

  printState(3, data[2]);
  openValve(R3, data[2]);

  printState(4, data[3]);
  openValve(R4, data[3]);

  Serial.println("=== Bewaesserung aus ==="); 
}


void printState(int relais, int sekunden) {
  Serial.print("Ventil ");
  Serial.print(relais);
  Serial.print(" offen fuer ");
  Serial.print(sekunden);
  Serial.print(" Sekunden");
  Serial.print("\n");
}


void openValve(int relais, long seconds) {
  //  //blocking!!!
  //  digitalWrite(relais, R_ON);
  //  delay(seconds*1000);  
  //  digitalWrite(relais, R_OFF);

  //Versuch non-blocking, blockt aber noch :-(
  unsigned long startTime = millis();
  unsigned long duration = seconds * 1000;
  unsigned long endTime = startTime + duration;

  digitalWrite(relais, R_ON);

  while (millis() < endTime) {
    //hmmm...
  }

  digitalWrite(relais, R_OFF);
}


void stopIt() {
  Serial.println("=== Abbruch ===");
  digitalWrite(R1, R_OFF);
  digitalWrite(R2, R_OFF);
  digitalWrite(R3, R_OFF);
  digitalWrite(R4, R_OFF);
}

losh:
Als Anfänger würde ich mich über Unterstützung bei der Umsetzung und Feedback über etwaige Optimierungsmöglichkeiten freuen!

Sollen auch Sekundenbruchteile unterstützt werden, z.B.

10.5,20.3,30.456,40.123\n

Oder nur ganze Sekunden?

nö im Grunde reiche Sekunden, da Magnetventile für die Gartenbewässerung angesteuert werden...also Werte so im Bereich 300 - 3600

... und sogar nur jeweils 1 Relais und nie mehrere gleichzeitig ...
Da braucht es noch nicht mal so bewährte Lösungen wie die Leuchtfeuer - Landkarte.

Das BlinkWithoutDelay reicht schon eigentlich.
Der grundlegend andere Ansatz: "loop ist sofort wieder fertig, macht aber fast nie was"
ist dir wohl noch unklar, bzw. erfordert, deine Funktionen komplett umzuschreiben
Zusätzlich zum BlinkWithoutDelay

  • Musst dir nur noch merken, an welchem Ventil du grade bist.
  • vergleichst du die aktuelle Laufzeit mit einer von 4 Laufzeiten
  • machst du kein endloses Blinken, sondern sowas wie eine Schrittsteuerung mit Zeitgesteuerten Schritten

Ausserdem ist unklar, was passieren soll

  • wenn alle 4 durch sind :
  • Alle Ventile zu bis ein neues Kommando seriell reinkommt ?
  • sofort wieder von vorne ?
  • eine 5. Zeit, in der aber kein Ventil auf ist ?
  • ( diese Zeit ist der Rest der Summe aller Zeiten auf 86400 ) so dass das Programm 24 Stunden später wieder losgeht
  • wenn ein Kommando kommt während die Steuerung noch läuft:
  • Die Steuerung schliesst das aktuelle Ventil und startet das neue Programm
  • oder ? ( kann man sich komplizierte Sachen ausdenken )

ja, michael_x...da hast du mich ertappt :*
So asynchrone Dinge haben mir immer schon Knoten im Kopf bereitet :drooling_face:
Vielleicht kannst du ein wenig (pseudo-) Code posten, um mir auf die Sprünge zu helfen...

Wegen der Unklarheiten:

Wenn alle 4 durch sind sollen alle 4 Ventile wieder zu sein bis ein neues serielles Kommando kommt.
Wenn ein Kommando kommt während die Steuerung noch läuft, soll es ignoriert werden AUSSER es handelt sich um das "Abbruchsignal" ('S' im konkreten Fall)

losh:
nö im Grunde reiche Sekunden, da Magnetventile für die Gartenbewässerung angesteuert werden...also Werte so im Bereich 300 - 3600

OK, ich habe da mal ein nichtblockierendes Demoprogramm erstellt:

byte relaisPins[]={10,11,12,13};
#define anzahlRelais sizeof(relaisPins)
long relaisZeiten[anzahlRelais];
unsigned long relaisZyklusStart;
#define EIN HIGH
#define AUS LOW


char* SerialStringRead(char EOLchar)
{
  // Parameter EOLchar: welches Zeichen als "Zeilenende" interpretiert werden soll
  static char text[81]; // Stringpuffer für maximale Zeilenlänge + 1
  static byte charcount=0; // Zeichenzähler
  if (!Serial.available()) return NULL; // kein Zeichen im seriellen Eingangspuffer
  if (charcount==0) memset(text,0,sizeof(text)); // Stringpuffer löschen
  char c=Serial.read(); // ein Zeichen aus dem seriellen Eingangspuffer lesen
  if (c>=32 && charcount<sizeof(text)-1) 
  {
    text[charcount]=c;
    charcount++;
  }
  else if (c==EOLchar) 
  {
    charcount=0;
    return text;
  }
  return NULL;
}

void showStatus()
{
  // gesetzten Schaltstatus der Relais anzeigen 
  Serial.print(millis()/1000); // Zeit seit Controllerstart in Sekunden
  Serial.print('\t');
  for (int i=0;i<anzahlRelais;i++)
  {
    if (digitalRead(relaisPins[i])==EIN)
      Serial.print("EIN\t");
    else 
      Serial.print("AUS\t");
  }
  Serial.println();
}


void relaisAbSchaltenNachZeit()
{
  for (int i=0;i<anzahlRelais;i++)
  {
    if (relaisZeiten[i]>0 && millis()-relaisZyklusStart>relaisZeiten[i]*1000L)
    {
      relaisZeiten[i]=0;
      digitalWrite(relaisPins[i],AUS);
      showStatus(); // den neuen Status anzeigen
    }
  }
}

void einSchalten()
{
  // alle Relais  mit einer Schaltzeit>0 einschalten
  for (int i=0;i<anzahlRelais;i++)
  {
    if (relaisZeiten[i]>0) digitalWrite(relaisPins[i],EIN);
    else  digitalWrite(relaisPins[i],AUS);
  }
  relaisZyklusStart=millis(); // neuer Schaltzyklus startet jetzt
  showStatus();
}

void notaus()
{
  // Alle Relais ausschalten 
  for (int i=0;i<anzahlRelais;i++)
  {
    relaisZeiten[i]=0;
    digitalWrite(relaisPins[i],AUS);
  }
  Serial.println("Not-Ausschaltung");
  showStatus();
}

void auswerten(char* strPtr)
{
  if (strcmp(strPtr,"S")==0) notaus();
  else
  {
    strPtr=strtok(strPtr,",");
    relaisZeiten[0]=atoi(strPtr);
    strPtr=strtok(NULL,",");
    relaisZeiten[1]=atoi(strPtr);
    strPtr=strtok(NULL,",");
    relaisZeiten[2]=atoi(strPtr);
    strPtr=strtok(NULL,",");
    relaisZeiten[3]=atoi(strPtr);
    // Neue Schaltzeiten anzeigen
    Serial.print("Neue Schaltzeiten: ");
    for (int i=0;i<anzahlRelais;i++)
    {
      Serial.print(relaisZeiten[i]);
      Serial.print('\t');
    }
    Serial.println();
  }
}


void setup() {
  Serial.begin(9600);
  Serial.println("Relais without delay by 'jurs' for German Arduino Forum");
  Serial.println();
  Serial.println("Zeit\tR1\tR2\tR3\tR4\t");
  Serial.println("----\t---\t---\t---\t---\t");
  for (int i=0;i<anzahlRelais;i++) 
  {
    if (AUS==HIGH) digitalWrite(relaisPins[i],HIGH);
    pinMode(relaisPins[i],OUTPUT);
  }
  showStatus();
}


void loop() 
{
  char* strPtr=SerialStringRead('\n');
  if (strPtr!=NULL)
  {
    auswerten(strPtr);
    einSchalten();
  }
  relaisAbSchaltenNachZeit();
}

Der Code ist so wie er ist für billige "Active LOW" schaltende Relaismodule mit Optokoppler-Eingang vorgesehen, falls Deine Relais stattdessen auf "HIGH" einschalten sollen, ändere den Code auf:

#define EIN HIGH
#define AUS LOW

Der Trick an nichtblockierendem Code ist, dass er nur auf bestimmte Ereignisse reagiert, z.B. wenn eine bestimmte Zeit erreicht ist oder ein bestimmtes Zeichen empfangen wurde. Und bis dahin erledigt das Programm nur das, was gerade zu tun ist: Also ein Zeichen verarbeiten, wenn ein Zeichen im seriellen Eingangspuffer verfügbar ist und eine Schaltung am Relais ausführen, wenn eine Schaltung am Relais durchzuführen ist, weil entweder eine bestimmte Zeit erreicht ist oder weil ein neuer Schaltbefehl komplett ist.

losh:
Wenn ein Kommando kommt während die Steuerung noch läuft, soll es ignoriert werden AUSSER es handelt sich um das "Abbruchsignal" ('S' im konkreten Fall)

Diese Bedingung ist in meinem Code nicht umgesetzt, sondern im Demoprogramm ist die Programmlogik so, dass durch einen neuen seriellen Befehl während eines noch laufenden Befehls eine Umprogrammierung auf neue Steuerzeiten erfolgt, ab dem Zeitpunkt, an dem der neue Befehl empfangen wird. D.h. alte Restlaufzeiten des letzten Befehls entfallen, ausgeführt werden die neuen Steuerzeiten. Aber das läßt sich an einem so schön modular aufgebauten Demoprogramm ja jederzeit leicht ändern. :smiley:

Übrigens: "Not-AUS" ist bei meiner Programmversion einfach auch möglich, indem eine "leere Zeile" gesendet wird. Wenn man eine "leere Zeile" sendet, wird das nämlich jeweils als Steuerzeit von 0 für jedes Relais gewertet, also "keine Zahl gesendet" wird wie "0 gesendet" betrachtet, und daher für auch das Senden einer leeren Zeile umgehend zur Abschaltung, sprich "AUS für Alle". Der Unterschied zu "Aus für Alle" und der richtigen Not-AUS Funktion ist also nur, dass bei richtigen Not-Aus noch eine Not-Aus Meldung auf Serial ausgegeben wird. Aber wie gesagt: Bei so einem modularen Programm kann man natürlich die Logik leicht ändern, wie man es braucht.

Die Logik die dem Wait Without Delay zugrundeliegt ist einfach.
Ein Vergleich

  1. delay ist wenn Du vor dem Zimmer Deines Chefs wartest bis er fertig telefoniert hat weil Du ihm etwas sagen mußt.
  2. Millis: Du kontrollierst jedesmal wenn Du am Zimmer des Chefs vorbeikommst, ob er nicht mehr telefoniert. wenn er frei ist kannst Du ihm was sagen.
  3. Interrupt: Du stürmst in das Zimmer Deines Chefs und sagst ihm was Du sagen mußt weil wollte daß Du ihm sofort benachrichtigt :wink: :wink:

Grüße Uwe

Das letztere würde ich bevorzugen :smiley:

Und was ist mit:
Ich höre durch die Wand wenn mein Chef mit dem telefonieren fertig ist und gehe dann rüber ?

Schlecte Bauweise ?
8)

Wenn du dabei sonst nicht machst ist es 1.)
Wenn du dabei weiter arbeitest ist es eine Variante von 2.)

Es deutet deutet aber auf eine mangelnde Kapselung zwischen verschiedenen Programmteilen hin :stuck_out_tongue:

Du kannst Dir auch mal dieses Beispiel hier anschauen: Lighthouses | Blinkenlight oder dieses: Flexible Sweep | Blinkenlight. Allerdings wäre mir ein Softwarebasierter Not Aus zu windig. Was ist wenn Deine Software einen Fehler hat? Was ist wenn der Controller aufgrund eines Versorgungsspannungstransienten in einen undefinierten Zustand kommt?