(noch in Arbeit, Code kommt noch)

In dem Workshop ging es darum, Sensoren über einen Arduino (Bastel-)Mikrocontroller an einen Freifunk Router unter Openwrt(Gluon) anzuschließen.

Die Messwerte werden auf diesem Router mit Hilfe von Shell und Lua Skripten über Collectd an einen Server geschickt (Influxdb) und dort mit Grafana angezeigt.

Das ganze lief über die IPv6 Adressen von Freifunk, hier Freifunk Frankfurt. Der Server ist als Client im Freifunk drin, natürlich könnte man ihn auch an zentraler Stelle oder im Internet aufstellen.

###Sensoren

Wir haben einen DHT11 / DHT22 und einen OneWire Dallas DS18B20 an den Arduino angeschlossen. Das sind digitale Sensoren, d.h. man spricht sie über ein Bitprotokoll an und bekommt die Messwerte zurückgeliefert. Über die Arduino ADCs (Analog Digital Wandler) könnte man auch analoge Sensoren (Helligkeit oder Temperatur bei denen sich Widerstandswerte abhängig von der Messgröße andern) anschließen. Hier ist dann auch eine Kalibrierung des Messsystems notwendig und die Kabellängen oder Verbindungsstecker habe einen Einfluß. Bei den digitalen Sensoren wir versucht, dies komplett zu vermeiden und eine Kalibrierung direkt im Werk durchzuführen und dieGeräte so zu konstruieren, dass diese für 5 oder 10 Jahre keine größeren Veränderungen zeigen.

Der DHT11/22 hat ein proprietäres Protokoll und benötigt einen PIN am Mikrocontroller. Zusätzlich wird er per Masse und 5V angeschlossen, der PIN per 10k Ohm Pullup Widerstand zusätzlich an 5V.

###Dallas OneWire Bus

Der OneWire DS18B20 nutzt den Dallas OneWire Bus als Kommunikationsmedium. Ein-Draht-Interface ist natürlich nicht ganz richtig, denn Masse benötigt man zusätzlich und diese Zweidraht-Lösung funktioniert nur, wenn man den Sensor vor dem Auslösen der Messung über die Datenleitung gewissermaßen „auflädt“, dort bekannt unter dem Stickwort „Parasite-Power". Alternativ kann man die Sensoren auch regulär mit Masse und 5V anschließen und eine Datenleitung verwenden, welche ebenfalls per Pullup, hier 4,7k Ohm, mit 5 V verbunden wird.

Das Gute ist, dass es für die Protokolle viele schöne Libraries für die Arduino gibt. Einmal kümmern sich diese um das korrekte Timing der Protokolle, außerdem um die Umrechnung der Rohdaten in z.B. Grad Celsius, relative Luftfeuchte oder auch Luftdruck umgerechnet auf diverse Höhenreferenzen.

###Andere Bussysteme

I2C - ein zweidraht Bus. Hier können Geräte ma Bus mit Kennungen angesprochen werden. Im Gegensatz zum Onewire haben die Geräte keine eindeutige ID sondern alle Geräte einer Modellreihe dieselbe ID. In Industriellen Anwendungen kann diese Vorteile haben, da dann jeder Bus identisch ist. Bestimmt hat der Zweidraht Bus auch noch elektrische Vorteile, davon habe zumindest ich keine Ahnung.

SPI - Serial Peripherie Interfae

Diese Schnittstelle ist schnell und kein Protokoll, t.b dpi dispalyHigh Speed

##Arduino Sketch (Programm):

Das Programm für den Arduino hat zwei Aufgaben: Auf der einen Seite spricht es mit den Sensoren in ihren Protokollen, auf der anderen mit dem OpenWrt Router über USB oder seriell.

In der Regel muss mann eine Messung auf einem Sensoren auslösen, einen kleinen Moment (abhängig vom Gerätetyp, Versorgungsspannung und manchmal auch Maßgenauigkeit) abwarten und dann die Daten empfangen oder aus Registern des Sensors auslesen. Diese werden dann in die wahren Messwerte umgerechnet, manchmal sogar unter Berücksichtigung von Kalibrierungsparametern, die im Werk fest im Sensor hinterlegt werden.

Im Prinzip läuft das Programm in folgender Endlosschleife:

forever:
- warten auf Eingabe (CR/LF)
  für jeden Sensor:
    - Auslösen des Messvorganges
    - Auslesen des Ergebnisses
    - Ausgabe der Ergebnisse
  end
  - Ausgabe eines Endemarkers, hier EOF
end

Der steuernde Rechner kann damit einen Messvorgang auslösen und die Daten empfangen und weiterverarbeiten.

arduino-freifunk.ino
#include <OneWire.h> 
OneWire  ds(10);  // on pin 10 (a 4.7K resistor is necessary) 
 
#include "DHT.h" 
#define DHTPIN 2     // what digital pin we're connected to 
#define DHTTYPE DHT11   // DHT 11 
//#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321 
//#define DHTTYPE DHT21   // DHT 21 (AM2301) 
DHT dht(DHTPIN, DHTTYPE); 
 
bool running = false; 
 
void setup(void) { 
  pinMode(13, OUTPUT); 
  digitalWrite(13, HIGH); 
  Serial.begin(9600); 
  Serial.setTimeout(1000); 
  dht.begin(); 
  digitalWrite(13, LOW); 
} 
 
void loop(void) { 
  byte i; 
  byte present = 0; 
  byte type_s; 
  byte data[12]; 
  byte addr[8]; 
  float celsius, fahrenheit; 
  float h,t; 
 
  if( !running ){ 
    digitalWrite(13, LOW); 
    Serial.flush(); //flush all previous received and transmitted data 
    while(!Serial.available()){ 
      delay(500); 
    } 
    digitalWrite(13, HIGH); 
 
    Serial.readString(); 
    Serial.flush(); //flush all previous received and transmitted data 
 
    running = true; // start execution 
  } 
 
if ( !ds.search(addr)) { 
    ds.reset_search(); 
 
    //DHT11 Code 
    h = dht.readHumidity(); 
    t = dht.readTemperature(); 
    if (!isnan(h) && !isnan(t)) { 
      Serial.print("DHT "); 
      Serial.print(t); 
      Serial.print(" "); 
      Serial.println(h); 
    } 
    Serial.println("EOF"); 
 
    running = false; // done with this cycle, wait again 
    return; 
  } 
 
  if (OneWire::crc8(addr, 7) != addr[7]) { 
      Serial.println("CRC is not valid!"); 
      return; 
  }
 
  // the first ROM byte indicates which chip 
  switch (addr[0]) { 
    case 0x10: 
//      Serial.println("  Chip = DS18S20");  // or old DS1820 
      type_s = 1; 
      break; 
    case 0x28: 
//      Serial.println("  Chip = DS18B20"); 
      type_s = 0; 
      break; 
    case 0x22: 
//      Serial.println("  Chip = DS1822"); 
      type_s = 0; 
      break; 
    default: 
//      Serial.println("Device is not a DS18x20 family device."); 
      return; 
  }
 
  ds.reset(); 
  ds.select(addr); 
  ds.write(0x44, 1); // start conversion, without parasite power 
 Serial.println(celsius); 
}

Alternative: der Arduino sammelt im Hintergrund regelmäßig die Messwerte und liefert auf Anfrage die jeweils neuesten aus.

##LUA Skript zum Auslesen

Warum kein Bash: jedes öffnen der seriellen Schnittstelle erzeugt auf dem Arduino ein Reset. Bestimm kann ein bash Gott mein gruseliges Lua Skript locker in Bash/ash/Dash des Openwrt umschreiben.

Ich bin ja kein Fan von einer weiteren Skriptsprache, aber das gilt auch umgekehrt und da Lua auf den Routern für das Web UI verwendet und damit schon drauf ist bleibt mir keine Wahl - Lua lernen (doer besser: copy with pride)

Was tun wir

- öffnen der seriellen Schnittstelle
forever:
- merken eines Zeitpunktes
- senden des Startbefehls an den Arduino
- empfangen der Messwerte sowie der Kennungen, Ende bei EOF (siehe oben)
- Umschreiben und Ausgabe im Format des Collectd Plugins (siehe weiter unten)
- Berechnung der abgelaufenen Zeit
- Berechnung der noch abzuwartenden Zeit bis zum nächsten Messpunkt
- abwarten
end.

Lua ist echt unterbelichtet in Bezug auf Timestamps und Sleeps, alles in Sekundenintervallen und dann noch Sleep per Shell ausführen. Und wenn man mehr will muss man Libraries laden, schönen Gruß an die Entwickler auch. Aber Hauptsache Code als Parameter übergeben können und Variablensätze als Rückgabewerte….

arduino.lua
hostname = os.getenv("COLLECTD_HOSTNAME") 
if hostname==nil then hostname="undefined-hostname" end 
interval = os.getenv("COLLECTD_INTERVAL") 
if interval==nil then interval=10 end 
 
rserial=io.open("/dev/ttyACM0","r") 
wserial=io.open("/dev/ttyACM0","w") 
rserial:flush() 
 
while true do 
  wserial:write(".") 
  tstart = os.time() 
  repeat 
    line=rserial:read() 
    line=string.gsub(line,"\r$","") 
 
    temp = nil 
    humidity = nil 
    temp, humidity = string.match(line, 'DHT%s+([%.,%d]+)%s+([%.,%d]+)') 
    if temp ~= nil and humidity ~= nil then 
      print("PUTVAL "..hostname.."/dht/temperature-temperature interval="..interval.." N:"..temp) 
      print("PUTVAL "..hostname.."/dht/humidity-humidity interval="..interval.." N:"..humidity) 
      line="" 
    end 
 
    romid = nil 
    temp = nil 
    romid, temp = string.match(line, '(%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x)%s+([%.,%d]+)') 
    if romid ~= nil and temp ~= nil then 
      print("PUTVAL "..hostname.."/onewire/"..romid.."-temperature interval="..interval.." N:"..temp) 
    end 
 
  until line=="EOF" 
 
  towait = interval - os.difftime(os.time(),tstart) 
  if towait>0 then 
    os.execute("sleep "..tonumber(towait)) 
  end 
 
end

##Collectd als Sammelprogramm

Collectd sammelt Metriken auf Hosts und unterstützt über Plugins eine Vielzahl von Servierprogrammen und läßt sich über generische Plugins leicht erweitern. Das Plugin das ich hier verwende ist das „exec“ plugin, welches beliebige Shell Skripte ausführen kann.

collectd.conf
#Hostname   "localhost"
#FQDNLookup  true
BaseDir "/var/run/collectd"
#Include "/etc/collectd/conf.d"
PIDFile "/var/run/collectd.pid"
PluginDir "/usr/lib/collectd"
TypesDB "/usr/share/collectd/types.db"
#Interval    30
#ReadThreads 2
 
LoadPlugin load
LoadPlugin cpu
LoadPlugin wireless
LoadPlugin network
 
<Plugin network>
  MaxPacketSize 1024
  <Server "fddd:5d16:b5dd:0:dacb:8aff:fe00:2bdb" "25826">
    Interface "br-client"
  </Server>
</Plugin>
 
LoadPlugin exec
<Plugin exec>
  Exec "nobody" "/root/arduino.sh"
</Plugin>

Collectd kann verschlüsseln und/oder authentisieren. Dabei werden vom Clients die Pakete mit Benutzernamen und Passwort versehen und optional auch verschlüsselt. Leider sind die openwrt collectd Pakete ohne die Crypto gebaut worden.

Caveat: das Skript muss unter einem anderen User als „root“ ausgeführt werden. Was auf einem großen Host sinnvoll ist macht auf einem Freifunk Router weniger Sinn. In unserem spezielle Fall müssten man z.B. einen neuen User anlegen und die ttyACM0 mit entsprechenden Rechten versehen. Oder die Schnittstelle für alle User verfügbar machen, beides am besten im Collectd Start Skript.

Alternative: eine NodeMCU könnte z.B. die Messwerte direkt im Format von Collectd (es sind UDP Pakete) an den Server senden. Problem dabei: der ESP kann nicht meshen und unterstützt im Moment kein IPv6. Die Sicherheit des Gerätes bzw. des IP Stacks wäre sicherlich zu hinterfragen…

Weitere Alternative: Direktes Senden der Metriken an den Server, Influx unterstützt einige Datenformate für die Datenanlieferung. Das Graphite Plugin versteht z.B. HTTP PUTs in einen festen Format.

Noch eine Alternative: Senden von HTTP PUT an einen der vielen IoT TimeSerien Anbieter im Markt. Aber wir wollen ja Dinge selbst tun.

##Influx als TimeSeries Database

influx generell

influx collectd speziell wird hinzugefügt unter hostname, metric, value etc

##Grafana zur Darstellung

Query - DB, Host, Parameter etc

OpenWrt hat ein OneWire Kernelmodul, welches sich mit GPIO PINs direkt auf dem Router verbinden lässt.

Weitere Infos zu Mikrocontrollern: - verwendet haben wir einen Arduino Leonardo Clone, ca. 10-12 EUR auf E-Bay. Dieser hat den Vorteil, dass die USB Funktion vom Controller selbst ausgeführt wird. Die Clones haben zum Teil merkwürdige USB Seriell Adapter, die Probleme mit MacOS und z.T. mit Windows machen können - der Arduino nutzt in der Regel 5V. Viele der klassischen Sensoren (DHT, 18B20 etc) arbeiten über einen weiten Spannungsbereich und sind unkritisch. Modernere Sensoren arbeiten im 3,3V Bereich (BMP180, BM085) und sind z.T. auch empfindlich gegenüber Steuersignalen mit 5V. - andere Mikrocontroller arbeiten komplett im 3.3V Bereich uns sind nur Teilweise spezifiziert für 5V Steuersignale. - wir machen hier kein Low Power Design, d.h. es gibt viele Komponenten, die in batteriebetriebenen Umgebungen unnötigen Strom verbraten. - kleinere Arduino gibt es, die dann auch keine PINs oder Kontaktleiste haben. Diese haben z.T. geringere Taktfrequenz (8 statt 16 MHz), auch geringere Spannung oder keinen Quarz als Oszillator. Diese sind dann weniger „genau“ in der Taktgebung. - Ältere Libraries korrigieren ihre Wartezyklen unter der Annahme von 16 MHz. Dies sind aber zunehmende weniger.

Also immer: Datenblatt lesen

Andere Mikrocontroller im Basteluniversum - unvollständige Liste - ESP8266 nutzt auch die Arduino IDE, hat eingebautes Wifi und ist deutlich performanter (80MHz). Je nach Peripherie günstiger als ein Arduino so dass einige Leute schon anfangen, ihn mit abgeschaltetem Wifi als Controller zu betreiben. Benötigt einen USB-Seriell Adapter und 3,3V - NodeMCU - ein ESP8266 mit USB und Breadboard Friendly Anschlüssen - PIC http://www.instructables.com/id/Arduino-Nano-CH340/ busware