Kategorie-Archiv: Software

Arduino delay() considered harmful

Das Arduino Blinky Programm ist das Hello World der embedded Anwender. Es wird auch von Entwicklern verwendet, die sich gut in C oder C++ auskennen – es dient als Test für die Entwicklungsumgebung und Anbindung an den jeweiligen Microcontroller. Für Einsteiger trägt es aber ein hohes Risiko, dass diese frühzeitig auf einen falschen Pfad geleitet werden.

Das Blinky Programm enthält zwei delay() Aufrufe mit einer erheblichen Wartezeit:

void loop() {
  digitalWrite(LED_BUILTIN, true);
  delay(500);
  digitalWrite(LED_BUILTIN, false);
  delay(500);
}

In diesem einfachen Fall ist das noch nicht mal falsch oder problematisch. Aber es ist einer der wenigen Sonderfälle in denen so ein delay() vernünftig einsetzbar ist. Der Sonderfall hier: es gibt nur einen einzigen Handlungsstrang, jede Sekunde wird eine LED aus- und wieder eingeschaltet. Soweit so gut. Aber wie sieht es aus, wenn eine zweite LED mit halber Zeitverzögerung geschaltet werden soll:

void loop() {
  digitalWrite(LED_1, true);
  digitalWrite(LED_2, true);
  delay(250);
  digitalWrite(LED_2, false);
  delay(250);
  digitalWrite(LED_1, false);
  digitalWrite(LED_2, true);
  delay(250);
  digitalWrite(LED_2, false);
  delay(250);
  digitalWrite(LED_1, false);
}

Und das ist noch einfach. Wie es aussieht, wenn die Zeit für die zweite LED 144 Millisekunden beträgt, mag ich gar nicht hinschreiben. Man sieht, dass man mit delay() schnell in eine Sackgasse gerät wenn es mehrere Handlungsstränge mit unterschiedlichen Zeitanforderungen gibt.

Ein erster Gedanke wäre der Einsatz eines real time operating systems (RTOS) und mehreren Threads. Das ist aber ein größeres Unterfangen. Zum Einen muss man sich in das System einarbeiten. Zum Anderen belegt jeder Thread nicht unerhebliche Ressourcen. Insbesondere bei den kleinen Microcontrollern ist der RAM Bereich oft sehr begrenzt, sodass sich der Einsatz nicht unbedingt lohnt.

Eine flexiblere Lösung in C

Eine naheliegende Lösung wäre z.B., dass man für jede Aktion eine Zeitvariable anlegt, die angibt, wann sie das nächste Mal ausgeführt werden soll.

void loop() {
  unsigned long now = millis();

  if (now > nextLed1) {
    nextLed1 = now + 500;
    digitalWrite(LED_1, led1State);
    led1State = !led1State;
  }

  if (now > nextLed2) {
    nextLed2 = now + 250;
    digitalWrite(LED_2, led2State);
    led2State = !led2State;
  }
}

Jetzt kann man leicht mehrere Handlungsstränge mit beliebigen Zeitverhalten ausführen. Die delay() Funktion wird gar nicht mehr verwendet, jede Teilaktion ist also in sehr kurzer Zeit abgeschlossen und wird erst wieder nach Ablauf der angegeben Zeit wieder aktiv. Aus meiner Sicht ist das für kleinere Projekte und insbesondere für Anfängerprojekte durchaus eine sinnvolle Vorgehensweise. Wenn das Projekt wächst, bekommt man im Laufe der Zeit aber eine unschöne Kette von if Abfragen in der loop Schleife.

Eine Variante für C++

Wenn man sich entschlossen hat, das Projekt nicht in C, sondern in C++ zu erstellen, bietet sich die Möglichkeit an, die Handlungsstränge jeweils in eine eigene Klasse zu packen. Der Konstruktor der Klasse meldet den Strang dann selbständig in der loop Schleife an. Die Aktion wird in einer Methode hinterlegt. Ich habe einen Rahmen hierzu erstellt, welcher einfach als #include tmux.hpp in das Programm eingebunden werden kann. Für jeden Handlungsstrang legt man dann eine Klasse an, die die jeweiligen Aktionen enthält.

https://github.com/Matthias-Thiele/TMux/tree/main

Der Rahmen für so einen Strang kann in der Entwicklungsumgebung als Template hinterlegt werden und somit leicht eingefügt werden:

class Handlungsstrang: public TMWorker {
  private:

  public:
    void setup() {
    }

    void loop() {
    }

} strang1;

Die Klasse muss im Anschluss einen sinnvollen Namen bekommen und die setup Methode mit dem Code zur Initialisierung (analog zur Setup() Funktion im Arduino) gefüllt werden. In der loop() Methode werden dann die Befehle ausgeführt, die zu dem gewünschten Zeitpunkt ausgeführt werden sollen. Der lokale Status wird im private: Abschnitt gespeichert. Blinky sieht dann so aus:

class LEDBlink: public TMWorker {
  private:
    bool m_LEDstate;

  public:
    void setup() {
      setDelay(500);
    }

    void loop() {
      digitalWrite(LED_BUILTIN, m_LEDstate);
      m_LEDstate = !m_LEDstate;
    }

} ledBlink;

In der setup Methode wird die Wartezeit von 500 Millisekunden voreingestellt, die zwischen zwei Aufrufen eingehalten werden soll. In der loop Methode wird dann die LED ein- oder ausgeschaltet, der Zustand wird über die Membervariable m_LEDstate bestimmt.

Das ist erst mal deutlich mehr Code als in der einfachen Variante mit den Zeitvariablen. Der Vorteil dieser Lösung ist, dass die einzelnen Handlungsstränge besser voneinander entkoppelt sind und auf den ersten Blick sichtbar wird, was zum jeweiligen Handlungsstrang gehört. Und der größte Teil des Codes kann durch ein Template automatisch eingefügt werden. Bei einfachen Aktionen kann man die Klassen direkt in der main.cpp Datei unterbringen. Wenn die Aktionen komplexer werden und deutlich mehr Code benötigen, kann es sinnvoll sein, diese in jeweils eigene Dateien aufzuteilen. Diese Aufteilung sorgt dann ebenfalls dazu, dass schneller sichtbar wird, was zu der jeweiligen Aktion dazu gehört oder nicht.

Die Basisklasse TMWorker kümmert sich im Konstruktor darum, dass jede Instanz dieser Klasse in einer Liste gespeichert wird. Diese ist als Array und nicht als eine variabel lange Liste angelegt, da ich keinen Heap verwenden möchte. Das Array enthält nur Zeiger auf die Instanzen, ist also nicht besonders groß und kann bei Bedarf verlängert werden. Die Liste wird in einer Variablen tmux gespeichert, welche automatisch durch das include der hpp Datei angelegt wird. Ein Verständnis dieser Internas ist aber nicht notwendig zur Verwendung der Klasse.

Auf der Arduino Seite muss in der loop Schleife nur noch die Methode tmux.loop() aufgerufen werden und in der Arduino setup Methode tmux.setup(). Die loop Methode durchläuft die Liste der registrierten Handlungsstränge und führt diese nacheinander aus.

void loop() {
  tmux.loop();
}

Weitere Funktionen

Falls die Zeiten zwischen den Aufrufen nicht konstant sind, kann die nächste Ausführung in der loop() Methode explizit eingestellt werden. Die geänderte Wartezeit gilt dann für alle nachfolgenden Aufrufe bis zur nächsten Änderung.

class LEDBlink: public TMWorker {
  private:
    bool m_LEDstate;
    int  m_actDelay = 250;

  public:
    void setup() {
      setDelay(m_actDelay);
    }

    void loop() {
      digitalWrite(LED_BUILTIN, m_LEDstate);
      m_LEDstate = !m_LEDstate;

      // Wartezeit bei jedem Durchlauf um 100 Millisekunden verlängern
      m_actDelay += 100;
      setDelay(m_actDelay);
    }

} ledBlink;

Ebenfalls kann man eine Startverzögerung einstellen, die dafür sorgt, dass der erste Aufruf der Aktion erst später stattfindet. Das kann z.B. sinnvoll sein, wenn sich der Zustand des Systems erst stabilisieren soll bevor eine Messung ausgeführt wird. Hierfür gibt es die Methode setStartupDelay():

    void setup() {
      setDelay(250);
      setStartupDelay(3000);
    }

Externe Ereignisse per Interrupt einbinden

Das Arduino Framework unterstützt die Verwendung von Interrupts zur schnellen Reaktion auf externe Ereignisse. Die Interrupt-Routine sollte aber möglichst kurz gehalten werden, andernfalls kann es Störungen in anderen Teilen des Programms oder des Frameworks geben. Falls die Aktion etwas aufwändiger ist, kann es deshalb sinnvoll sein, die Aktion als eine TMWorker Klasse (genau genommen – von TMWorker abgeleitete Klasse) mit ‚unendlicher‘ Wartezeit anzulegen. In der Interrupt Routine wird dann nur die Ausführung getriggert, sie ist also in sehr kurzer Zeit wieder beendet. Die Kopplung erfolgt dabei über die Methode attachWorker() welche das Worker Objekt mit einer Interrupt-Quelle verbindet.

class InterruptButton: public TMWorker {
  public:
    void setup() {
      pinMode(PA8, INPUT_PULLUP);
      attachWorker(0, PA8, CHANGE);
    }

    void loop() {
      Serial1.println("Button 2 changed.");
    }
} interruptButton;

In der setup Methode wird der Pin PA8 als Input mit pullup Widerstand definiert. Mittels attachWorker wird dieses Objekt als Interrupt-Quelle 0 zum Port PA8 angemeldet. CHANGE definiert, dass der Interrupt bei jedem Pegelwechsel ausgeführt werden soll. Vordefiniert sind drei Interrupt-Quellen (0, 1 und 2), falls weitere benötigt werden, muss die hpp Datei angepasst werden. In der setup Methode sieht man keinen Aufruf von setDelay() – das führt dazu, dass die default Wartezeit ‚unendlich‘ verwendet wird. Die Aktion soll schließlich nur beim Auftreten einer Port Änderung ausgeführt werden.

Damit die Aktion zu einem Interrupt möglichst schnell ausgeführt wird, speichert das tmux Objekt den Handlungsstrang mit dem letzten aktiven Interrupt und führt diesen beim nächsten loop als erstes aus, auch wenn er in der normalen Reihenfolge noch nicht dran wäre.

Verwendung einer State Machine

Im einfachsten Fall führt ein Handlungsstrang periodisch eine Aktion immer wieder aus. In der Praxis ist es aber oft komplizierter. In Abhängigkeit von der Vorgeschichte sollen unterschiedliche Aktionen ausgeführt werden. Hierfür werden gerne endliche Automaten (State Machine) eingesetzt. Sie sind einfach zu implementieren und können Handlungsketten gut und übersichtlich abbilden. So eine State Machine setzt sich im Wesentlichen aus zwei Informationen zusammen: welche Zustände gibt es und welche Übergänge zwischen den Zuständen sind möglich.

Eine einfache Möglichkeit der Implementierung ist ein switch Statement über alle Zustände. Dazu kann man zur besseren Lesbarkeit ein enum definieren, welche alle Zustände mit sprechenden Namen versieht. Im folgenden möchte ich es anhand einer einfachen Ampelsteuerung zeigen. Das enum bildet die Zustände Warten, Bereit zum Fahren, Fahren, Halten erwarten ab. Zusätzlich gibt es eine Initialisierung, dabei ist es eine Geschmacksfrage, ob man diesen Code in den Konstruktor legt oder in einen eigenen Zustand. Wenn es Übergänge gibt, die eine erneute Initialisierung erfordern (z.B. ein Reset im Fehlerfall), dann würde ich die Initialisierung immer in die State Machine übernehmen.

enum TrafficLightStates {
  INIT,
  STOP,
  PREPARE_GO,
  GO,
  PREPARE_STOP
};

In der Klasse TrafficLight wird eine lokale Member Variable angelegt welche den aktuellen Zustand speichert – oder genauer gesagt: den Zustand, der beim nächsten Schritt ausgeführt werden soll. Sie wird mit INIT belegt, damit wird die Ampel dann in den Grundzustand gesetzt.

class TrafficLight: public TMWorker {
  private:
    TrafficLightStates nextState = INIT;

In der setup Methode werden die Microcontroller Ausgänge für die LEDs gesetzt. An dieser Stelle soll die State Machine noch ohne Verzögerung zum nächsten Zustand übergehen, setDelay(0) übernimmt diese Aufgabe.

    void setup() {
      pinMode(TLED_RED, OUTPUT);
      pinMode(TLED_YELLOW, OUTPUT);
      pinMode(TLED_GREEN, OUTPUT);
      setDelay(0);
    }

In der loop() Methode gibt es dann das switch Statement über den nextState.

    void loop() {
      switch(nextState) {

Für jeden Zustand aus den TrafficLightStates gibt es dann ein case Label innerhalb des switch Statements. In dem INIT Zustand werden alle Lichter dunkel geschaltet und als nächster Zustand wird Halten (STOP: Rot) gewählt.

        case INIT:
          digitalWrite(TLED_RED, false);
          digitalWrite(TLED_YELLOW, false);
          digitalWrite(TLED_GREEN, false);
          nextState = STOP;
          break;

Im STOP Zustand wird die rote LED eingeschaltet. Da dieser State normalerweise aus der Gelb-Phase kommt, muss die gelbe LED noch abgeschaltet werden. Wenn man auf Nummer Sicher gehen will, kann man auch die grüne LED noch abschalten, sie ist an dieser Stelle aber immer aus.

        case STOP:
          Serial1.println("RED");
          digitalWrite(TLED_RED, true);
          digitalWrite(TLED_YELLOW, false);
          setDelay(5000);
          nextState = PREPARE_GO;
          break;

Die Rot-Phase soll 5 Sekunden dauern, das wird mittels setDelay(5000) erreicht. Nach diesen 5 Sekunden soll die Ampel in die Rot-Gelb Phase übergehen, der nextState wird deshalb auf PREPARE_GO gesetzt. Wichtig: nicht das break am Ende jedes Zustands vergessen!

So werden nach und nach alle Zustände implementiert. Die komplette State Machine könnte dann so aussehen:

int TLED_RED = PA_10, TLED_YELLOW = PA_11, TLED_GREEN = PA_12;

enum TrafficLightStates {
  INIT,
  STOP,
  PREPARE_GO,
  GO,
  PREPARE_STOP
};

class TrafficLight: public TMWorker {
  private:
    TrafficLightStates nextState = INIT;

  public:
    void setup() {
      pinMode(TLED_RED, OUTPUT);
      pinMode(TLED_YELLOW, OUTPUT);
      pinMode(TLED_GREEN, OUTPUT);
      setDelay(0);
    }

    void loop() {
      switch(nextState) {
        case INIT:
          digitalWrite(TLED_RED, false);
          digitalWrite(TLED_YELLOW, false);
          digitalWrite(TLED_GREEN, false);
          nextState = STOP;
          break;

        case STOP:
          digitalWrite(TLED_RED, true);
          digitalWrite(TLED_YELLOW, false);
          setDelay(5000);
          nextState = PREPARE_GO;
          break;

        case PREPARE_GO:
          digitalWrite(TLED_YELLOW, true);
          setDelay(1000);
          nextState = GO;
          break;

        case GO:
          digitalWrite(TLED_RED, false);
          digitalWrite(TLED_YELLOW, false);
          digitalWrite(TLED_GREEN, true);
          setDelay(4000);
          nextState = PREPARE_STOP;
          break;

        case PREPARE_STOP:
          digitalWrite(TLED_YELLOW, true);
          digitalWrite(TLED_GREEN, false);
          setDelay(1000);
          nextState = STOP;
          break;
      }
    }
} trafficLight;

Das Schreiben der Aktionen direkt in das switch Statement ist nur sinnvoll, wenn die Zahl der Zustände klein bleibt und die jeweiligen Aktionen überschaubar sind. Andernfalls bekommt man ein langes switch Statement welches unübersichtlich ist und schnell zu Fehlern führt. In diesem Fall sollte man jede Zustandsaktion in eine eigene Methode auslagern und aus dem switch Statement heraus nur die jeweilige Methode aufrufen.

    void loop() {
      switch(nextState) {
        case INIT:
          processInit();
          break;

        case STOP:
          processStop();
          break;

        case PREPARE_GO:
          processPrepareGo();
          break;

        case GO:
          processGo();
          break;

        case PREPARE_STOP:
          processPrepareStop();
          break;
      }
    }

Jetzt hat man schon wieder zusätzlichen Code geschrieben, dafür aber den Vorteil, dass die Aktionen der Zustände deutlicher getrennt werden. Im richtigen Leben sollte man die einzelnen Teile natürlich auch mit Dokumentation versehen. Die komplette Ampelsteuerung sieht dann so aus:

int TLED_RED = PA_10, TLED_YELLOW = PA_11, TLED_GREEN = PA_12;

enum TrafficLightStates {
  INIT,
  STOP,
  PREPARE_GO,
  GO,
  PREPARE_STOP
};

class TrafficLight: public TMWorker {
  private:
    TrafficLightStates nextState = INIT;

    void processInit() {
      digitalWrite(TLED_RED, false);
      digitalWrite(TLED_YELLOW, false);
      digitalWrite(TLED_GREEN, false);
      nextState = STOP;
    }

    void processStop() {
      digitalWrite(TLED_RED, true);
      digitalWrite(TLED_YELLOW, false);
      setDelay(5000);
      nextState = PREPARE_GO;
    }

    void processPrepareGo() {
      digitalWrite(TLED_YELLOW, true);
      setDelay(1000);
      nextState = GO;
    }

    void processGo() {
      digitalWrite(TLED_RED, false);
      digitalWrite(TLED_YELLOW, false);
      digitalWrite(TLED_GREEN, true);
      setDelay(4000);
      nextState = PREPARE_STOP;
    }

    void processPrepareStop() {
      digitalWrite(TLED_YELLOW, true);
      digitalWrite(TLED_GREEN, false);
      setDelay(1000);
      nextState = STOP;
    }
    
  public:
    void setup() {
      pinMode(TLED_RED, OUTPUT);
      pinMode(TLED_YELLOW, OUTPUT);
      pinMode(TLED_GREEN, OUTPUT);
      setDelay(0);
    }

    void loop() {
      switch(nextState) {
        case INIT:
          processInit();
          break;

        case STOP:
          processStop();
          break;

        case PREPARE_GO:
          processPrepareGo();
          break;

        case GO:
          processGo();
          break;

        case PREPARE_STOP:
          processPrepareStop();
          break;
      }
    }
} trafficLight;

Hier befindet sich die GitHub Seite zu der hpp Datei:

https://github.com/Matthias-Thiele/TMux/tree/main

Für PlatformIO habe ich ein Snippet erstellt, welches man sich in der CPP Snippet Datei hinterlegen kann. Wenn man tmuxw (für TMux Worker) eintippt und Enter drückt, wird ein komplettes Worker Termplate angelegt.

	"TMux Task":{
		"prefix": "tmuxw",
		"body": [
			"class $1: public TMWorker {",
			"  private:",
			"",
			"  public:",
			"    void setup() {",
			"    }",
			"",
			"    void loop() {",
			"    }",
			"",
			"} $0story1;",
			""
		],
		"description": "Task Multiplexer action template"
	}

Ein einfacher Slider für HTML Seiten

Da ich auf meiner Hauptseite viele Bilder in Galerieform habe, bin ich mit dem Standard-Slider unzufrieden. Er ist nicht einfach zu bedienen und zeigt die Bilder nicht leicht durchblätterbar und in voller Bildschirmgröße an. Ich habe mir deshalb mal einige Slider aus dem Internet angesehen und habe nicht das richtige gefunden. Zum einen arbeiten einige Slider nicht sauber mit dem Firefox Browser zusammen. Insbesondere die reinen CSS Slider machen so viele Verrenkungen, dass es mich nicht wundert, das es an allen Ecken und Kanten hakt. Ich denke, dass einige Web Entwickler nur noch auf Chrome testen. Zum anderen muss man bei fast allen Slidern die html Seite anpassen. Ich habe keinen generischen Slider gesehen, der über Parameter gesteuert werden kann.

Im Nachhinein muss ich erkennen, dass ich länger nach einem brauchbaren Slider gesucht habe als die Eigenentwicklung Zeit gekostet hat. Wichtig für mich war, dass der Slider die Bilder aus dem WordPress Media Pool anzeigen kann, er sollte nicht für jede Galerie eine eigene Web Seite benötigen und er sollte per Maus und Tastatur gesteuert werden können. Und das Wichtigste: er sollte die Bilder in voller Größe anzeigen. Ein reiner CSS Slider war für mich hingegen nicht erstrebenswert. Wegen der Parametrisierbarkeit benötige ich ohnehin JavaScript. Ein einfacher CSS Aufbau hingegen ist ein echter Vorteil.

Das komplette Projekt besteht nur aus einer Datei, sie kann von meinem Github Repository herunter geladen werden: https://github.com/Matthias-Thiele/HTML-Image-Slider

Der Grundgedanke beruht wie bei vielen Slidern darauf, dass die Bilder übereinander gestapelt werden und immer nur ein Bild angezeigt wird. Das kann man über CSS leicht mit „display: none;“ steuern. Der Image Bereich ist bei mir zum Start leer, er wird beim Aufruf der Seite aus der Parameterliste heraus gefüllt.

          <div id="imagelist">
          </div>

Der JavaScript Code dazu ist überschaubar. Die Bilder können entweder durchnummeriert kommen oder mit einem beschreibenden Namen. Es wird ein IMG Element erzeugt, der Pfad zum Bild eingefügt und das Element in die imagelist eingehangen.

for (var i = imgStart; i <= imgEnd; i++) {
        var img = document.createElement("img");
        var imgSrc;

        if (descriptionName) {
            imgSrc = this.startPath + imgName + this.description[i] + "." + imgExt;
        } else {
            var num = (i < 10) ? (pendingZero + i) : i;
            imgSrc = this.startPath + imgName + num + "." + imgExt;
        }

        img.src = imgSrc;
        if (i === imgStart) {
            img.className = "active";
        }
        listRoot.appendChild(img);
        this.items.push(img);
    }

Die Statuszeile überlagert halb-transparent den unteren Bildteil. Vielleicht erweitere ich es später mal so, dass man sie auch ausblenden kann. In den meisten Fällen wird sie aber nicht stören.

          <nav class="slider-nav">
              <button class="previous" data-key="true" style="width:52%; text-align: right">
              <span>
                  <i>&lt;</i>
              </span>
            </button>
            <button class="next" data-key="false" style="text-align: left">
              <span>
                <i>&gt;</i>
              </span>
            </button>
              <span id="counter" style="float:right; width: 100pt; padding: 2pt;">1/6</span>
          </nav>
        </div>

Die Bild-Weiterschaltung läuft im Kreis. Wenn man am letzten Bild angekommen wird, wird als nächstes das erste Bild angezeigt und umgekehrt. Damit man die Orientierung nicht verliert und X-mal im Kreis läuft, wird in der Statuszeile angezeigt, wie viele Bilder es gibt und auf welchem Bild man gerade steht.

            Slider.prototype.advance = function(leftRight) {
                this.items[this.count].classList.remove('active');
                this.count += (leftRight) ? -1 : 1;
                
                if (this.count < 0) {
                    this.count = this.items.length -1;
                } else if (this.count >= this.items.length) {
                    this.count = 0;
                }
                
                this.items[this.count].classList.add('active');
                this.updateView();
            };
            
            Slider.prototype.updateView = function() {    
                var status = document.getElementById("counter");
                status.innerText = " " + (this.count + 1) + " / " + this.items.length; 
                
                var desc = document.getElementById("description");
                if (this.count >= this.description.lengt || !this.description[this.count]) {
                    desc.style = "display: none";
                } else {
                    desc.innerText = this.description[this.count];
                    desc.style = "display: inline-block";
                }
            };

Die Tastatursteuerung ist einfach. Es gibt nur zwei Kommandos – vorwärts und zurück. Diese können über Pfeil links/ rechts und über Bild hoch/ runter ausgelöst werden.

            Slider.prototype.keyPress = function(event) {
                event = event || window.event;
                var slider = document.querySelector('.next').slider;
                var keyCode = event.keyCode;
                
                if (keyCode === 33 || keyCode === 37) {
                  slider.advance(true);
                } else if (keyCode === 34 || keyCode === 39) {
                  slider.advance(false);
                }
            };

Die HTML Datei enthält keine Informationen zu den Bildern. Diese wird beim Aufruf der Seite über Parameter mitgegeben. Somit kann man die Seite irgendwo hinterlegen und aus beliebigen Anwendungen heraus aufrufen. Innerhalb der Web Seite kann man optional den Start des Pfades der Quelle hinterlegen. Damit kann man erzwingen, dass ein bestimmter Bereich nicht einfach verlassen werden kann und zusätzlich ist dann der Parametersatz etwas kürzer, da dieser Teil nicht jedes mal mit angegeben werden muss.

            var slider = new Slider("imagelist", "https://mmth.de/wp-content/uploads/");

Der erste Parameter gibt die id des DIV Elements an welches die Imageliste enthalten soll und der zweite Parameter den Start der URL jedes Bildes. Man kann diesen auch auf einen Leerstring setzen (nicht einfach weglassen).

http://localhost:8080/SliderTest/slider.html?name=2023/09/&ext=jpg&desc=Mainau1~Mainau2~Mainau3~Mainau5

Folgende Parameter gibt es:

nameEnthält den festen Namensanteil des Bildes. Bei durchnummerierten Bildern wird die Nummer am Ende automatisch angefügt. Falls der Startpfad aus der html Datei noch um Unterverzeichnisse ergänzt werden muss, kommen diese hier vor den Namen. Bei Bildern deren Name aus der Beschreibung generiert wird, kann man hier den Verzeichnispfad hinterlegen.name=Mainau
name=2023/Mainau
startBei durchnummerierten Bilder wird hier die Nummer des ersten Bilds angegeben. Im Normalfall wird das 1 sein, das muss aber nicht zwingend so sein. Wenn man nur eine Teilsequenz anzeigen möchte, kann man hier auch eine höhere Zahl angeben.
Falls die Bilder nicht Bild1, Bild2, .., Bild9, Bild10 durchnummeriert sind, sondern mit fester Nummernbreite Bild01, Bild 02, .., Bild09, Bild10 bezeichnet wurden, muss man beim Startwert auch eine führende 0 einfügen: start=01
start=1
endHier wird die Nummer des letzten Bildes eingetragenend=5
extAlle Bilddateien müssen vom gleichen Typ sein, da man nur eine Extension (ohne Punkt) angeben kann. Der Name setzt sich dann aus Startpfad + Name + Nummer + Punkt + Extension zusammen.ext=jpg
descBei durchnummerierten Bildern ist der Beschreibungsteil optional. Wenn er vorhanden ist, wird die Beschreibung zu jedem Bild oben links eingeblendet. Bei Bilddateinamen aus der Beschreibung muss dieser Parameter eine Liste aller Namen der Bilddateien enthalten.desc=Haus~Auto~Boot

Test eines einfachen Ultraschall Sensors für 80 Cent

In diesem Video teste ich einen einfachen Ultraschallsensor, der bei Aliexpress gerade mal 80 Cent kostet. Die Ansteuerung ist sehr einfach und schnell (wenn man keine unnötigen Fehler macht) und die Ergebnisse sind erstaunlich gut.

#include <Arduino.h>

const int TRIG_PIN = 12;
const int ECHO_PIN = 13;
const int ITEM_COUNT = 50;

long simpleMultiMeasure();

void setup() {
  Serial.begin(9600);
  pinMode(TRIG_PIN,OUTPUT);
  digitalWrite(TRIG_PIN, LOW);

  pinMode(ECHO_PIN,INPUT);
}

void loop() {
  long duration = simpleMultiMeasure();
  long distanceMm;
 
  // convert the time into a distance
  distanceMm = duration * 100l / 582l;
 
  if (distanceMm <= 0)
  {
    Serial.println("Out of range");
  }
  else 
  {
    Serial.print(duration);
    Serial.print(" ticks, ");
    Serial.print(distanceMm);
    Serial.print(" mm");
    Serial.println();
  }
  
  delay(500);
}

long simpleMultiMeasure() {
  long sum = 0;

  for (int i = 0; i < ITEM_COUNT; i++) {
    digitalWrite(TRIG_PIN, HIGH);
    delayMicroseconds(20);
    digitalWrite(TRIG_PIN, LOW);
    sum += pulseIn(ECHO_PIN,HIGH);
    delayMicroseconds(20000);
  }

  return sum / ITEM_COUNT;
}


Ein RISC-V Test mit dem Sipeed Longan Nano Board

Ich wollte schon lange mal mit einem RISC-V Prozessor experimentieren. Mit dem Longan Nano Board für 2,50 Euro steht in Development Board zur Verfügung bei dem man nicht lange nachdenken muss. Für ein Euro Aufpreis bekommt man noch ein farbiges LCD dazu.

Falls jemand an dem winzigen Assembler Programm aus dem Video interessiert ist, kann man es hier kopieren:

.section .text
.align 2
.global toggle

.equ GPIO_PORT_B_CONTROL, 0x40010c10
.equ SET_ALL_BITS, 0x0000ffff
.equ RESET_ALL_BITS, 0xffff0000


toggle:
  li t0, GPIO_PORT_B_CONTROL
  li t1, SET_ALL_BITS
  li t2, RESET_ALL_BITS

loop:
  sw t1, (t0)
  sw t2, (t0)
  sw t1, (t0)
  sw t2, (t0)
  j loop

Startbild: RISC V prototype chip – crop of File:Yunsup Lee holding RISC V prototype chip.jpg. Wikimedia, Creative-Commons-Lizenz „CC0 1.0 Verzicht auf das Copyright“

Ein computergesteuerter Messplatz mit einem ESP 32, Teil 2

In diesem zweiten Teil wird die Software zu dem Projekt beschrieben. Sie besteht aus drei Teilen.

  1. Ein Platform IO Projekt mit einem C++ Programm welches auf dem ESP32 läuft.
  2. Ein Netbeans Projekt in Java welches auf dem PC ein Framework zur Verwendung des Messplatzes zur Verfügung stellt. Weiterhin enthält es einen komfortablen Weg zur Erstellung von CSV (comma separated values) Dateien.
  3. Ein Netbeans Projekt welches die Verwendung der DataIO jar Datei zeigt.

Touchsensor TTP223 Test

Ich wollte mal ausprobieren, ob sich die Touchsensoren für den Aufbau von Stellpulten eignen. Die Verwendung ist einfach und sie funktionieren gut, es gibt aber auch ein paar Schattenseiten.

#define TOUCH_INPUT 2
#define SIGNAL 3
#define SETTLE_TIME 100

void setup() {
pinMode( TOUCH_INPUT, INPUT );
pinMode( SIGNAL, OUTPUT );
}

void loop() {
int isTriggered = digitalRead( TOUCH_INPUT );
digitalWrite( SIGNAL, switchState( isTriggered ) );
}

boolean actState = false;
boolean lastActive = false;
boolean switchState( boolean signal ) {
boolean deb = debounce( signal );
if ( deb ) {
if ( !lastActive ) {
lastActive = true;
actState = !actState;
}
} else {
lastActive = false;
}

return actState;
}

int trig = 0;
boolean debounce( boolean signal ) {
if ( signal ) {
if (trig == 0) {
trig = millis();
} else {
int now = millis();
if ( now – trig > SETTLE_TIME ) {
return true;
}
}
} else {
trig = 0;
}

return false;
}