High Power LEDs für Arduino

IoT: mit Arduino Webzugriffe visualisieren

In diesem IoT (Internet-of-Things) Beitrag möchte ich euch zeigen wie man mit einem Linux Server einen Arduino Mikrocontroller mit GPIO Ports, LEDs und einem Piezo/Buzzer ansteuert. Mein DIY Projekt hat das Ziel die Zugriffe auf unterschiedliche Web-Anwendungen zu erkennen und dann für eine definierte Zeit einzelne LEDs oder den Buzzer zu schalten.

In meinem Beispiel werde ich beim Zugriff auf das vtiger CRM für 5 Minuten das grüne LED einschalten und beim Zugriff auf den ownCloud 9 Dienst die blaue LED für 5 Minuten aktivieren. Beim Zugriff auf phpmyadmin oder die WordPress Administration ertönt ein Warnton in unterschiedlichen Frequenzen.

Für das Projekt werden folgende Bauteile benötigt: Arduino Nano/Uno/Mega o.ä., pro Web-Dienst zusätzlich: 1x Transistor BC547 oder vergleichbar, 1x Widerstand ca. 2,2KΩ, 1x Widerstand für die LED ca. 220Ω, 1x LED, Breadboard oder Prototypen Leiterplatte, mehrerer Steckbrücken

Am Arduino installiere ich folgendes Programm.

Code für den Arduino: DOWNLOAD Source-CODE
/*
   My own Linux to Arduino project with simple
   communication protocol on /dev/ttyUSB0
   V1.01 (c) 2016 Andreas Schuster
*/
String inputString = "";         // a string to hold incoming data
String cmdString = "";
String valueString = "";
boolean stringComplete = false;  // whether the string is complete
boolean cmdComplete = false;
boolean valueComplete = false;

const int ledPin = 13;
int ledstatus = 0;
int startcmd = 0;
int valuecmd = 0;
int endcmd = 0;
int serialcounter = 0;
int timer = 0;
int pin = 0;
int val = 0;
int freq = 0;

void setup() {
  // initialize serial:
  Serial.begin(38400);
  // reserve 200 bytes for the inputString:
  inputString.reserve(200);
  cmdString.reserve(40);
  valueString.reserve(40);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // change the onboard LED when a newline arrives:
  if (stringComplete) {
    //Serial.print(inputString);
    //Serial.print("\n");
    timer = 0;
    if (ledstatus == 0) {
      digitalWrite(ledPin, HIGH);
      ledstatus = 1;
    } else {
      digitalWrite(ledPin, LOW);
      ledstatus = 0;
    }
    // handle cmdString & valueString
    if (cmdComplete) {
      //      Serial.print("cmd=");
      //      Serial.print(cmdString);
      //      Serial.print(";");
    }
    if (valueComplete) {
      //      Serial.print("value=");
      //      Serial.print(valueString);
      //      Serial.print(";");
    }
    pin = 255; // false pin at start
    if (cmdString.length() >= 6) {                // typical command "out_d4" or "out_d12"
      if (cmdString.substring(0, 4) == "out_") { // found an output command
        if (cmdString.charAt(4) == 'd') {        // found to write to a digital pin
          if (isAlphaNumeric(cmdString.charAt(5))) {
            pin = cmdString.charAt(5) - char('0');
            if (cmdString.length() >= 7) {
              if (isAlphaNumeric(cmdString.charAt(6))) {
                pin = (pin * 10) + (cmdString.charAt(6) - char('0'));
              }
            }
            // write to pin
            if (pin < 100) {
              if (valueString == "HIGH") {
                pinMode(pin, OUTPUT);
                digitalWrite(pin, HIGH);
              } else if (valueString == "LOW") {
                pinMode(pin, OUTPUT);
                digitalWrite(pin, LOW);
              } else {
                Serial.print("ERROR:cannot understand value!\n");
              }
                Serial.print("OK:");
                Serial.print(cmdString);
                Serial.print(" set to ");
                Serial.print(valueString);
                Serial.print("\n");
            } else {
                Serial.print("ERROR:cannot detect pin number!\n");
            }
          }
        }
      }
    }
    if (cmdString.length() >= 5) {              // typical command "in_d4" or "in_d12"
      if (cmdString.substring(0, 3) == "in_") { // found an input command
        if (cmdString.charAt(3) == 'd') {       // found to write to a digital pin
          if (isAlphaNumeric(cmdString.charAt(4))) {
            pin = cmdString.charAt(4) - char('0');
            if (cmdString.length() >= 6) {
              if (isAlphaNumeric(cmdString.charAt(5))) {
                pin = (pin * 10) + (cmdString.charAt(5) - char('0'));
              }
            }
            // readpin here
            if (pin < 100) {
              pinMode(pin, INPUT);
              val = digitalRead(pin);
              if (val == HIGH) {
                Serial.print("HIGH\n");
              } else if (val == LOW) {
                Serial.print("LOW\n");
              } else {
                Serial.print("undefined\n");
              }
            } else {
               Serial.print("ERROR:cannot detect pin number!\n");
            }
          }
        }
      }
    }
    if (cmdString == "tone") {
      freq = valueString.toInt();
      if (freq < 40) {
        freq = 2600;
      }
      Serial.print("OK:play tone ");
      Serial.print(freq,DEC);
      Serial.print("\n");
      tone(3, freq, 2000);
    }
    if (inputString == "help\n") {
      Serial.print("output commands:\n");
      Serial.print(" ^out_d[pin]%[HIGH|LOW]$\n");
      Serial.print(" return: none\n");
      Serial.print("input commands:\n");
      Serial.print(" ^in_d[pin]$\n");
      Serial.print(" return: HIGH | LOW\n");
      Serial.print("tone command (only pin 3!):\n");
      Serial.print(" ^tone$ or ^tone%[frequency]$\n");
      Serial.print(" return: none\n");
    }
    serialcounter = 0;
    startcmd = 0;
    valuecmd = 0;
    endcmd = 0;
    // clear the string:
    inputString = "";
    cmdString = "";
    valueString = "";
    stringComplete = false;
    cmdComplete = false;
    valueComplete = false;
  }
  delay(2);
  timer++;
  if (timer > 5000) {  //periodically change the onboard LED
    timer = 0;
    if (ledstatus == 0) {
      digitalWrite(ledPin, HIGH);
      ledstatus = 1;
    } else {
      digitalWrite(ledPin, LOW);
      ledstatus = 0;
    }
  }
}

void serialEvent() {
  /* seperate a typical serial command
      e.g. "^out_d4%HIGH$"
      into
      cmdString = "out_d4"
      value = "HIGH"
  */
  while (Serial.available()) {
    // get the new byte:
    serialcounter++;
    char inChar = (char)Serial.read();
    // add it to the inputString:
    //Serial.print(inChar);
    inputString += inChar;
    // if the incoming character is a newline, set a flag
    // so the main loop can do something about it:
    if (startcmd > 0 && inChar != '%' && inChar != '$') {
      cmdString += inChar;
    }
    if (valuecmd > 0 && inChar != '$') {
      valueString += inChar;
    }
    if (inChar == '^') {
      startcmd = serialcounter;
      valuecmd = 0;
    }
    if (inChar == '%') {
      cmdComplete = true;
      valuecmd = serialcounter;
      startcmd = 0;
    }
    if (inChar == '$') {
      valuecmd = 0;
      startcmd = 0;
      cmdComplete = true;
      valueComplete = true;
      endcmd = serialcounter;
    }
    if (inChar == '\n') {
      stringComplete = true;
    }
  }
}

Nach der Programmierung des Arduino, in meinem Fall ein 5V Arduino Nano, widme ich mich nun der LED Schaltung. Die Stromversorgung meiner zwei 0,5W Power LEDs nehme ich aus der USB Stromversorgung des Arduino. Bei mehr als 4 Power LEDs muss ich rechnerisch entweder einen High-Power USB Anschluss nutzen, oder meine Schaltung um eine zusätzliche Stromquelle erweitern. Zusätzlich habe ich über den Pin D3 einen Piezo/Buzzer verbunden.

Arduino NANO LED Schaltung mit BC547 und Buzzer

Bei der Berechnung der Vorwiderstände der LEDs musste ich berücksichtigen, dass mein USB Port nur 4,7V liefert und ich hinter dem BC547 Transistor einen Spannungsabfall von 0,88V gemessen habe. Hinter dem Transistor hatte ich somit nur 3,82V, wodurch der LED Vorwiderstand deutlich geringer ist.

Hier meine Schaltung:

Arduino NANO LED Sketch mit BC547 und Buzzer
Arduino NANO LED Sketch mit BC547 und Buzzer

Ich möchte die einzelnen LEDs bei der Nutzung der Apache Anwendungen ein- und wieder ausschalten, ohne jedoch die Anwendungen selbst zu ändern. Daher beschränke ich mich rein auf die Auswertung der Apache Logs, den die Anwendungen bei der Benutzung generieren. Daher möchte ich den Apache Log der Anwendungen in separate Log-Dateien trennen und regelmäßig prüfen wann der Log zuletzt geschrieben wurde.

Änderung in der Apache Konfiguration

Hier die Änderung in der Datei /etc/apache2/sites-enabled/000-default.conf

<VirtualHost *:443>
    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/apache.crt
    SSLCertificateKeyFile /etc/ssl/private/apache.key
    ServerAdmin webmaster@localhost
    ErrorLog ${APACHE_LOG_DIR}/error.log

    SetEnvIf Request_URI ^/vtiger(/|$) crm
    SetEnvIf Request_URI ^/oc(/|$) owncloud

    CustomLog ${APACHE_LOG_DIR}/crm_access.log combined env=crm
    CustomLog ${APACHE_LOG_DIR}/owncloud_access.log combined env=owncloud

    CustomLog ${APACHE_LOG_DIR}/access.log combined

    # Pfad zu den Webinhalten
    DocumentRoot /var/www/html

    <IfModule mod_headers.c>
      Header always set Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"
    </IfModule>

    <Directory /usr/local/httpd/htdocs>
      Options -Indexes +FollowSymLinks
    </Directory>
</VirtualHost>

im Betrieb sieht das dann so aus:

Apache2 Log Separierung per SetEnvIf und CustomLog
Apache2 Log Separierung per SetEnvIf und CustomLog

Die Überwachung der einzelnen Log Dateien übernimmt ein kleines C++ Programm, das ich regelmäßig über einen Cron Job aufrufe. Das Programm lege ich nach dem Kompilieren unter /usr/local/sbin/web-visual/ ab, und rufe mit den definierten Parametern auf

C++ Sourcecode “web-visual.cc” für Linux: DOWNLOAD Linux CODE
/* web-visual.cc (c) 2016 Andreas Schuster
   compile with: g++ -std=c++11 web-visual.cc -o web-visual
*/
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <string>
#include <cstring>
#include <sstream>
#include <iostream>
#include <cstdlib>
#include <cerrno>


#define BAUDRATE B38400
#define DEVICE "/dev/ttyUSB0"
#define FALSE 0
#define TRUE 1

using namespace std;

int USB;
int n_written;
int action = 0;
std::string status;
int pin = -1;

int main( int argc, char *argv[] )
{
    struct stat file_stats;
    time_t rawtime;
    time (&rawtime );
    int param = 1;

/*    if(argc < 4) {
        std::cerr << "Usage:\n" << argv[0] << " checkfile write pin# {0|1}\n";
        std::cerr << "or " << argv[0] << " checkfile [tone]{%[frequency]}\n";
        std::cerr << "or " << argv[0] << " read pin#\n";
        return 100;
    }
*/
    std::string filename = argv[param];
    if (filename != "write" && filename != "read" && filename.substr(0,4) != "tone") {
        // check for filename if modified within the last 300 seconds
        if((stat(argv[param], &file_stats)) == -1) {
            std::cerr << "file or command " << filename << " not found!\n";
        } else { // file found named argv[1]
            // if last modification is less than 300 sec -> HIGH else LOW
            param++;
            if ( (rawtime - file_stats.st_mtime) < 300 ) {
                status = "HIGH";
            } else {
                status = "LOW";
            }
        }
    }
        std::string command  = argv[param];
        if ( command.substr(0,5) == "write" || command.substr(0,4) == "read" ) {
        param++;
                std::string pinparam = argv[param];
        pin = atoi(pinparam.c_str());
            action = 1;
        }
    if ( command.substr(0,4) == "tone") {
        action = 2;
    }
    param++;
    if ( argc > param ) {
        std::string mandatory = argv[param];
        if ( mandatory == "1" ) {
            status = "HIGH";
            std::cout << "mandatory HIGH set!\n";
        } else if ( mandatory  == "0" ) {
            status = "LOW";
            std::cout << "mandatory LOW set!\n";
        }
    }

    if (action > 0) {
        /* Open File Descriptor */
        USB = open( DEVICE, O_RDWR| O_NOCTTY );
        if (!USB) {
            std::cerr << "Error " << errno << " from open: " << std::strerror(errno) << std::endl;
            return 1;
        }

        struct termios tty;
        struct termios tty_old;
        memset (&tty, 0, sizeof tty);

        /* Error Handling */
        if ( tcgetattr ( USB, &tty ) != 0 ) {
            std::cerr << "Error " << errno << " from tcgetattr: " << strerror(errno) << std::endl;
        }

        /* Save old tty parameters */
        tty_old = tty;

        /* Set Baud Rate */
        if(cfsetospeed (&tty, (speed_t)BAUDRATE) != 0)
            std::cout << "Error " << errno << " from cfsetospeed: " << strerror(errno) << std::endl;
        if(cfsetispeed (&tty, (speed_t)BAUDRATE) != 0)
            std::cout << "Error " << errno << " from cfsetispeed: " << strerror(errno) << std::endl;

        /* Setting other Port Stuff */ 
        tty.c_cflag = BAUDRATE | CS8 | CLOCAL | CREAD;
        tty.c_cflag &= ~HUPCL;

        tty.c_cc[VINTR]    = 0;     /* Ctrl-c */
        tty.c_cc[VQUIT]    = 0;     /* Ctrl-\ */
        tty.c_cc[VERASE]   = 0;     /* del */
        tty.c_cc[VKILL]    = 0;     /* @ */
        tty.c_cc[VEOF]     = 4;     /* Ctrl-d */
        tty.c_cc[VTIME]    = 35;     /* inter-character timer unused */
        tty.c_cc[VMIN]     = 0;     /* blocking read until 1 character arrives */
        tty.c_cc[VSWTC]    = 0;     /* '\0' */
        tty.c_cc[VSTART]   = 0;     /* Ctrl-q */
        tty.c_cc[VSTOP]    = 0;     /* Ctrl-s */
        tty.c_cc[VSUSP]    = 0;     /* Ctrl-z */
        tty.c_cc[VEOL]     = 0;     /* '\0' */
        tty.c_cc[VREPRINT] = 0;     /* Ctrl-r */
        tty.c_cc[VDISCARD] = 0;     /* Ctrl-u */
        tty.c_cc[VWERASE]  = 0;     /* Ctrl-w */
        tty.c_cc[VLNEXT]   = 0;     /* Ctrl-v */
        tty.c_cc[VEOL2]    = 0;     /* '\0' */

        tty.c_iflag = IGNPAR | ICRNL;

        tty.c_oflag = 0;  // raw output

        //tty.c_lflag = ICANON;
        tty.c_lflag = ~ICANON;

        /* Flush Port, then applies attributes */
        if (tcflush( USB, TCIFLUSH ) != 0) 
            std::cerr << "Error " << errno << " from tcflush: " << strerror(errno) << std::endl;
        if ( tcsetattr ( USB, TCSANOW, &tty ) != 0) {
            std::cerr << "Error " << errno << " from tcsetattr" << std::endl;
        }

        /* Write */
        usleep(1700000); // wait 0,7 sec if Arduino need a reset on USB connect

        std::string cmd = "";
        if ( command.substr(0,5) == "write") {
            cmd = "^out_d" + std::to_string(pin) + "%" + status + "$\n";
        } else if (command.substr(0,4) == "read") {
            cmd = "^in_d" + std::to_string(pin) + "%" + status + "$\n";
        } else if (command.substr(0,4) == "tone" && status == "HIGH") {
            //cmd = "^tone%3000$\n";
            cmd = "^"+command+"$\n";
        }

                //std::cerr << "executing on Arduino: " << cmd;
        n_written = 0,

        n_written = write( USB, cmd.c_str(), cmd.size() );
        if (!n_written) 
            std::cerr << "Error " << errno << " from write: " << strerror(errno) << std::endl;

        /* Read result */
        int n = 0;
        char buf[255];
        memset(buf, '\0', sizeof buf);

        n = read( USB, buf, 255 );
        buf[n]=0;

        if (n < 0) {
            std::cerr << "Error reading: " << strerror(errno) << std::endl;
        }
        else if (n == 0) {
            std::cerr << "Read nothing!" << std::endl;
        }
        else {
            std::cerr << "Read: " << buf << std::endl;
        }

        close(USB);
    } else {               // else action < 1
        std::cerr << "Error: no valid command given!" << std::endl;
        return 2;
    }
}

Die Einbindung in den Cron Job, in meinem Fall unter Ubuntu, habe ich dem User root zugewiesen. Da die Programme nur rw Zugriff auf /dev/ttyUSB0 benötigen kann das auch mit beliebigen anderen Usern erfolgen, sofern diese User der Gruppe “dialout” angehören.

cron-jobs

Die Prüfung der Logs erfolgt jede Minute. Über den sleep Befehl verhindere ich aber, dass mehrere Befehle gleichzeitig an den Arduino gesandt werden. Alle 5 Sekunden per web-visual zu aktualisieren sollte jedoch kein Problem sein.

Im Prinzip war es das. Leider hat jedoch mein Arduino eine hässliche Eigenschaft, nämlich, dass der Mikrocontroller bei jedem USB-Connect einen Reset ausführt (in meinem Fall der open() Befehl in web-visual.cc) und somit alle vorherigen GPIO Einstellungen – also welche LED bereits leuchtet und welche nicht – löscht.

Daher habe ich einen kleinen C++ Helper namens keep_open.cc geschrieben, der den seriellen Port über das USB Device zum Arduino schreibend öffnet und nicht mehr schließt. Die Verbindung zum Arduino bleibt somit permanent aufrecht und es erfolgt kein Reset mehr. Auf der vorigen Seite seht ihr die Einbindung von keep_open in den Cronjob über die cron Steuerung @reboot (einmaliger Start des Programmes nach jedem Reboot).

C++ Sourcecode “keep_open.cc” für Linux: DOWNLOAD Linux CODE
/* web-visual.cc (c) 2016 Andreas Schuster
   compile with: g++ -std=c++11 keep_open.cc -o keep_open
*/
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <string>
#include <cstring>
#include <sstream>
#include <iostream>
#include <cstdlib>
#include <cerrno>


#define BAUDRATE B38400
#define DEVICE "/dev/ttyUSB0"
#define FALSE 0
#define TRUE 1

using namespace std;

int USB;

int main( int argc, char *argv[] )
{
        /* Open File Descriptor */
        USB = open( DEVICE, O_RDONLY| O_NOCTTY );
        if (!USB) {
            std::cerr << "Error " << errno << " from open: " << std::strerror(errno) << std::endl;
            return 1;
        }

        struct termios tty;
        struct termios tty_old;
        memset (&tty, 0, sizeof tty);

        /* Error Handling */
        if ( tcgetattr ( USB, &tty ) != 0 ) {
            std::cerr << "Error " << errno << " from tcgetattr: " << strerror(errno) << std::endl;
        }

        /* Save old tty parameters */
        tty_old = tty;

        /* Set Baud Rate */
        if(cfsetospeed (&tty, (speed_t)BAUDRATE) != 0)
            std::cout << "Error " << errno << " from cfsetospeed: " << strerror(errno) << std::endl;
        if(cfsetispeed (&tty, (speed_t)BAUDRATE) != 0)
            std::cout << "Error " << errno << " from cfsetispeed: " << strerror(errno) << std::endl;

        /* Setting other Port Stuff */ 
        tty.c_cflag = BAUDRATE | CS8 | CLOCAL | CREAD;
        tty.c_cflag &= ~HUPCL;

        tty.c_cc[VINTR]    = 0;     /* Ctrl-c */
        tty.c_cc[VQUIT]    = 0;     /* Ctrl-\ */
        tty.c_cc[VERASE]   = 0;     /* del */
        tty.c_cc[VKILL]    = 0;     /* @ */
        tty.c_cc[VEOF]     = 4;     /* Ctrl-d */
        tty.c_cc[VTIME]    = 20;     /* inter-character timer unused */
        tty.c_cc[VMIN]     = 0;     /* blocking read until 1 character arrives */
        tty.c_cc[VSWTC]    = 0;     /* '\0' */
        tty.c_cc[VSTART]   = 0;     /* Ctrl-q */
        tty.c_cc[VSTOP]    = 0;     /* Ctrl-s */
        tty.c_cc[VSUSP]    = 0;     /* Ctrl-z */
        tty.c_cc[VEOL]     = 0;     /* '\0' */
        tty.c_cc[VREPRINT] = 0;     /* Ctrl-r */
        tty.c_cc[VDISCARD] = 0;     /* Ctrl-u */
        tty.c_cc[VWERASE]  = 0;     /* Ctrl-w */
        tty.c_cc[VLNEXT]   = 0;     /* Ctrl-v */
        tty.c_cc[VEOL2]    = 0;     /* '\0' */

        tty.c_iflag = IGNPAR | ICRNL;

        tty.c_oflag = 0;  // raw output

        //tty.c_lflag = ICANON;
        tty.c_lflag = ~ICANON;

        /* Flush Port, then applies attributes */
        if (tcflush( USB, TCIFLUSH ) != 0) 
            std::cerr << "Error " << errno << " from tcflush: " << strerror(errno) << std::endl;
        if ( tcsetattr ( USB, TCSANOW, &tty ) != 0) {
            std::cerr << "Error " << errno << " from tcsetattr" << std::endl;
        }
        while (1 < 2) {
           usleep(60000000); // wait 60 sec
        }

        close(USB);
}

Fertig! Viel Spaß mit dem DIY Projekt, bei dem die Nutzung eurer installierten Web-Anwendungen nun mit hoher LED Intensität ins Auge springt.

Ich freue mich auf Eure Kommentare und Optimierungsvorschläge der Software- und Hardware-Skizzen!

Ein Gedanke zu „IoT: mit Arduino Webzugriffe visualisieren“

  1. Hi Andreas,

    toller Artikel. Ich hatte letztens eine ähnliche Idee, nur leider mit dem Problem, dass der Server am anderen Ende des Hauses stand.
    Klar, es gibt teile wie Raspberry und co, die aber immer ein komplettes Linux-System mit sich rumschleppen und mit um die 40 Euro nicht gerade billig sind.
    Da hab ich mir gedacht: warum den µC nicht direkt mit Ethernet versorgen und hab mein Projekt mittels dem Ethernet-Controller ENC28J60 von Microchip realisiert
    (http://www.microchip.com/wwwproducts/en/en022889)

    Das Ding läst sich über SPI beispielsweise von ATMEGA328 (der auf den Arduino-Board verbaut ist) super ansteuern. TCP/IP-Stack sowie MAC-Adresse sind schon onboard.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert