Allgemeine Beschreibung


Die Kenntnis der eigenen Position bzw. der Position eines Kommunikationspartners spielt im Umfeld der CPS (Cyberphysische Systeme), speziell zur Navigation, eine wesentliche Rolle bei der Entscheidungsfindung. Positionsdaten sind Grundlage der Berechnung, auf welche Art und Weise neu gesetzte Ziele am effizientesten erreicht werden können. Da industrielle Produktionsanlagen zumeist in Hallen bzw. Räumen verbaut sind, besteht erhöhter Bedarf an zuverlässigen und kostengünstigen Innenraum-Ortungsmechanismen, die zudem möglichst leicht und kostengünstig zu realisieren sein sollten. Da die meisten bisherigen Methoden eine Vielzahl an Sensoren und Aktoren benötigen, deren Stückpreise zudem häufig teuer sind, stellt die Schallortung durch Auswertung der RIA (Raumimpulsantwort) eine vielversprechende Alternative dar.

Versuchsaufbau

Ziel des Versuchs ist die Ermittlung der Position eines Mikrophons in einem Raum anhand der Aufzeichnung der Reflektionen eines Schallimpulses von den Wänden und Gegenständen im Raum.

Die Aufzeichnung der sogenannten Raumimpulsantwort erfolgt mithilfe des IoT-Kit Octopus, an welchem ein Elektretmikrophon am ADC Eingang betrieben wird, welches den Schalldruck misst. Näheres hierzu erfahren Sie unter dem Tab Versuchsaufbau.

 

 

Da der Schalldruck mit einer hohen Abtastrate (10.000 Messungen pro Sekunde) aufgezeichnet werden muss, um die Positionsbestimmung vornehmen zu können, werden die Aufgaben sinnvoll auf die beteiligten Geräte verteilt (siehe Tab Datenverarbeitung)

Der Tab Abläufe IoT-Kit beschreibt die Programmierung des IoT-Kits.

Im Bereich Node-RED wird das Zusammenspiel der benötigten Programme genauer erläutert.

Die so erfassten Daten können auf einem leistungsfähigen (Cloud-)System mithilfe von Matlab oder Python genutzt werden, um die Position des Mikrophons festzustellen. Die Vorgänge werden unter dem Tab Anwendung beschrieben.

Kernaufgabe ist die Ermittlung der Position eines Mikrophons mithilfe maschineller Lernalgorithmen. Hierzu wird ein Lautsprecher in einem Raum ortsfest auf dem Versuchstisch platziert. Zur Ortung wird die Eigenschaft der Schallreflexion genutzt, die das ausgesendete Schallsignal von den Wänden eines Raumes bzw. den Objekten die sich im Raum befinden reflektiert.

Bild 1 zeigt schematisch den Zusammenhang zwischen der RIA und der Position des aufzeichnenden Gerätes. Mit der Entfernung des Mikrophons zum Lautsprecher erhöht sich die benötigte Zeit des Signales zum Erreichen des Mikrophons, zeitgleich nimmt die gemessene Intensität ab. Der Direktschall (roter Impuls) erreicht das Mikrophon zuerst, die Ersten Reflexionen (grüner und blauer Impuls) werden aufgrund der größeren zurückgelegten Distanz später und schwächer empfangen. Anhand der unterschiedlichen RIA s1(t) und s2(t) lassen sich die Standorte der Mikrophone 1 und 2 unterscheiden. Das Mikrophon nimmt, in einer Trainingsphase, ein immer gleiches Signal des Lautsprechers an verschiedenen Positionen auf dem Tisch auf. Diese Aufnahmen werden zusammen mit den jeweiligen Positionen des Mikrophons an einen Algorithmus übergeben. Der Algorithmus ist so konzipiert, dass er in der Lage ist zu lernen welche Aufnahmen welcher Position zugehörig sind. Nach Abschluss der Trainingsphase wird die Funktionsweise des Algorithmus in einer Testphase überprüft, indem weitere Aufnahmen ohne die zugehörigen Positionen übergeben werden. Die Aufgabe des Algorithmus ist es, anhand des Vergleichs der neuen Aufnahmen mit den zuvor erlernten, die unbekannte Position richtig zu bestimmen.

Bild 2 zeigt den Versuchsaufbau mit den benötigten Geräten. Hier wird eine Unterscheidung von 16 verschiedenen quadratischen Bereichen mit einer Kantenlänge von 15 cm gezeigt, zur Vereinfachung kann der Versuch auch auf 2 zu unterscheidende Bereiche (vordere/hintere Tischhälfte) begrenzt werden.

Die Datenerfassung wird mithilfe des IoT-Kits Octopus realisiert. Das eingebettete System wurde zusammen mit der Expertengruppe „Internet of Things“ auf dem Digital Gipfel entwickelt und bietet zahlreiche nützliche Funktionen zum schnellen Entwurf von IoT-Anwendungen. Der Octopus basiert auf einem ESP8266-12F mit WLAN Unterstützung und liefert zusätzliche Sensoren und Aktoren, darunter zwei LED’s und einen Bosch Umweltsensor. Über den ADC des Octopus werden die Eingangssignale des MAX4466 Elektret-Mikrophons digitalisiert und zur Auswertung per MQTT über WLAN an einen Peripherierechner gesendet.

Anschluß MAX4466 an IoT-Kit

MAX4466                  IoT-Kit
OUT               =          GELB
GND               =          schwarz
VCC               =          ROT

Das weisse Kabel des ADC wird nicht benötigt.

Zur Generierung der Trainings- und Testdaten wird der in Bild 3 gezeigte Vorgang für verschiedene Positionen des Mikrophons auf dem Tisch wiederholt durchgeführt, wobei die Position des Mikrophons bei den einzelnen Aufnahmen gesondert erfasst wird. Nach dem Senden des Startkommandos per MQTT durch den Anwender beginnt der Prozess der Datenerfassung. Das IoT-Kit sendet zunächst eine MQTT-Nachricht zum Peripherierechner, die das Abspielen der Audio-Datei auslöst. Da dies eine gewisse Sende- und Verarbeitungszeit in Anspruch nimmt, wartet das IoT-Kit eine heuristisch ermittelte Wartezeit bis zum Start der Aufnahme. Durch diese Wartezeit wird die Synchronisation zwischen Aufzeichnung und Abspielen des Signales gewährleistet. Nach der Aufzeichnung werden die Daten zur Speicherung und Weiterverarbeitung über MQTT in die Cloud gesendet. Ist die Datenübertragung beendet, sendet das IoT-Kit ein Kommando zur Formatierung der empfangenen Daten in ein geeignetes Format zur Weiterverarbeitung. Das Python-Skript prepare_data_format.py verarbeitet die eingehenden Daten zu einer .csv-Datei.

Die so generierten Daten werden danach in Trainings- und Testmenge aufgeteilt. Aus den Trainingsdaten erstellt das Python-Skript ein Modell, das zur späteren Klassifizierung (Ortung) der Testdaten dient. Konkrete Übungen zur Umsetzung bzw. Datenvorverarbeitung und Datenanalyse werden in den Tabs Abläufe IoT-Kit, Node-RED und Anwendung behandelt.

Zunächst müssen Bibliotheken für die WLAN und MQTT Kommunikation eingebunden werden.

#include <ESP8266WiFi.h>
#include <PubSubClient.h>

Zur vereinfachten Bearbeitung empfiehlt es sich die Zugangsdaten zu Beginn des Programms in Variablen abzulegen.

const char* mqtt_server = "192.168.137.1"; 
const char* ssid = "IHRNetz"; 
const char* password = "IHRPasswort";

Nun werden verschiedene Werte für die Abtastung des Geräuschs und zur Datenübertragung festgelegt, um eine Sekunde mit einer Abtastrate von 10 kHz aufzuzeichnen werden 10000 Messwerte (SAMPLES) benötigt. Da der ADC (Analog-Digital Converter) Werte bis zu einer Größe von 1024 zurück gibt werden diese zur nachfolgenden Übertragung in einem Byte Array der Größe 20000 (dataCollection[SAMPLES*2]) zwischengespeichert. Ein Takt dauert dabei 100 µs (MICRODELAY). Da die Größe einer einzelnen MQTT Datenübertragung begrenzt ist, wird die Größe der einzelnen Pakete (mqttpack[MQTTPACSIZE]) mithilfe von MQTTPACSIZE festgelegt. Eventuell muss in der Datei PubSubClient.h (C:\jeweiliger Pfad zur Arduino Installation\arduino-1.8.6\portable\sketchbook\libraries\PubSubClient\src) die Zeile #define MQTT_MAX_PACKET_SIZE 4096 angepasst werden. Die Variable controlByte sorgt für einen stabilen wiederholten Ablauf des Programms und verhindert die mehrfache Ausführung. Zur Kontrolle des jeweiligen bearbeiteten Bereich des Datenspeichers wird die Variable currSample genutzt.

//Länge Aufnahme 10000*100µs = 1sec, Größe Bytearray 2*SAMPLES (1  Messwert momentan noch 2 Bytes)
#define SAMPLES                10000
//Größe der einzelnen MQTT Pakete
#define MQTTPACSIZE             500
//Zeitspanne zwischen Messungen
#define MICRODELAY 100
//Steuerungsparameter über Seriellen Monitor  
int controlByte = 0; 
//Laufvariable für Timer etc 
int currSample = 0; 
//Zwischenspeicher für Messwerte 
int wert = 0; 
byte dataCollection[SAMPLES*2]; 
byte mqttpack[MQTTPACSIZE];

Nun wird die MQTT Kommunikation über WLAN eingerichtet.

//-------------- definition mqtt-object ueber WiFi
WiFiClient   espClient;
PubSubClient mqttclient(espClient);

 

//--------- list of mqtt callback functions 
#define MAX_MQTT_SUB 10 // maximal 10 subscriptions erlaubt
typedef void (*mqtthandle) (byte*,unsigned int);
typedef struct {       // Typdeklaration Callback
  String topic;        // mqtt-topic
  mqtthandle fun;      // callback function 
} subscribe_type;
subscribe_type mqtt_sub[MAX_MQTT_SUB];
int mqtt_sub_count=0;
String MQTT_Rx_Payload = "" ; //--------- mqtt callback function 
void mqttcallback(char* to, byte* pay, unsigned int len) { 
  String topic   = String(to); 
  String payload = String((char*)pay);  
  MQTT_Rx_Payload=payload.substring(0,len);  
  Serial.println("\ncallback topic:" + topic + ", payload:" + MQTT_Rx_Payload);  
  for (int i=0;i<mqtt_sub_count;i++) { // durchsuche alle subscriptions, bis topic passt     
    if (topic==mqtt_sub[i].topic)     
      mqtt_sub[i].fun(pay,len);         // Aufruf der richtigen callback-Funktion
  }
}

Das IoT-Kit abonniert das Topic start.

//------------ reconnect mqtt-client
void mqttreconnect() { // Loop until we're reconnected 
  if (!mqttclient.connected()) { 
    while (!mqttclient.connected()) { 
      Serial.print("Attempting MQTT connection...");
      if (mqttclient.connect("Oct1" , "user", "passwort" )) {
        Serial.println("connected");
        for (int i=0;i<mqtt_sub_count;i++) { // subscribe topic
          mqttclient.subscribe(mqtt_sub[i].topic.c_str());
          Serial.println("\nsubscribe");
          Serial.print(mqtt_sub[i].topic);
        }
      } 
      else { 
        Serial.print("failed, rc=");
        Serial.print(mqttclient.state());
        Serial.println(" try again in 5 seconds");
        delay(5000);
      }
    }
  } 
  else { 
    mqttclient.loop(); 
  }
}

Wird im Topic start ein Signal gesendet, ruft die entsprechende Callback-Funktion das Programm start() auf, um den Messvorgang durchzuführen.

// ---------- my callbackfunction mqtt
void mqtt_callback_topic_start(byte* pay, unsigned int len){ 
  String payload = String((char*)pay); // payload als String interpretieren
  MQTT_Rx_Payload=payload.substring(0,len);    // mit Länge von len Zeichen
  Serial.println("\n in callback payload:" + MQTT_Rx_Payload +"len:"+String(len));
  if (controlByte == 0){
    start();
  }
}

Zudem werden Funktionen zum Senden der Ablaufkommandos und der erfassten Daten eingerichtet.

//MQTT - Steuerung Audio Ausgabe RPi
void mqttSendSoundCommand() 
{
  String pay = "x";
  mqttreconnect();
  Serial.println("Start Ton abspielen");
  mqttclient.publish("PlaySound",pay.c_str());
  Serial.println("Sent");
}
//MQTT - Senden Messwerte, Aufnahme in MQTT-Pakete aufteilen (PubSubClient.h max.
//MQTT Größe ändern: #define MQTT_MAX_PACKET_SIZE 1024)
void mqttSendRecord() 
{
  mqttreconnect();
  for(int i = 0; i<SAMPLES*2/MQTTPACSIZE; i++){
    for(int j = 0; j < MQTTPACSIZE; j++){
      mqttpack[j] = dataCollection[(i*MQTTPACSIZE)+j];
    }
    mqttclient.publish("SensorData",mqttpack,MQTTPACSIZE);
  }
}

//MQTT - Steuerung Messdatenformatierung
void mqttSendFormatCommand() 
{
  String pay = "x";
  mqttreconnect();
  mqttclient.publish("FormatOutput",pay.c_str());
  Serial.println("Ende Daten formatieren");
}

Um konstante Zyklen der Messungen zu erreichen wird eine Timerfunktion (timer1_isr_init();) im Rahmen der Funktion zur Aufnahme (record()) eingerichtet. Sie sorgt dafür, dass die Erfassung der Aufnahme im zeitlichen Abstand des oben festgelegten MICRODELAY durch die Funktion myIsrTimer() stattfindet.

//IsrTimer für Taktung Aufnahme
void record()
{
  timer1_isr_init();
  timer1_attachInterrupt(myIsrTimer);
  timer1_enable(1,0,1); 
  timer1_write((clockCyclesPerMicrosecond() / 16) * MICRODELAY);
}

Die Funktion myIsrTimer()speichert den aus 2 Byte bestehenden Messwert wert in 2 einzelnen Bytes, um die MQTT Datenübertragung zu vereinfachen.

//Byteweise Erfassung Messwerte - 1 int16 in 2 byte
void myIsrTimer()
{
  wert = (int)analogRead(A0);
  dataCollection[currSample++] |= byte(wert >> 8);
  dataCollection[currSample++] |= byte(wert & 0x00FF);
  if (currSample == (SAMPLES*2)-2)
  {
    // only one more tick (0 at end is "one time" - timer mode)
    timer1_enable(1,0,0);
  }
}

Zur Fehlervermeidung setzt die Funktion setZero()alle Werte des Array dataCollection zurück.

void setZero(){
  for(int i=0;i<SAMPLES*2;i++){
    dataCollection[i] = 0;
  }
}

Die Funktion steuert den Gesamtablauf nach Anstoß über das MQTT-Topic start oder nach Betätigung des Buttons am Octopus.

void start(){
  delayMicroseconds(100000);
  for (int i = 0; i < 1; i++){
    //RPi spiele Audio Datei ab (Node Red Kommando)
    controlByte = 1;
    mqttSendSoundCommand();
    delay(100);
    currSample = 0;
    //Starte Aufnahme
    Serial.println("Start record");
    record();
    delay(1200);
    //Sende Messdaten
    Serial.println("Ende record");
    mqttSendRecord();
    delay(50);
    //RPi formatiere Messdaten 
    mqttSendFormatCommand();
    delay(500);
    //Setze Bytes zurück
    setZero();
    controlByte = 0;
  }
}

Im setup()Bereich werden verschiedene Dienst wie WLAN oder MQTT initialisert. Dieser Programmteil wird zu Beginn des Programms einmal ausgeführt.

void setup(){ // Einmalige Initialisierung
  Serial.begin(115200);
  //------------ WLAN initialisieren 
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  delay(100);
  Serial.print ("\nWLAN connect to:");
  Serial.print(ssid);
  WiFi.begin(ssid,password);
  while (WiFi.status() != WL_CONNECTED) { // Warte bis Verbindung steht 
    delay(500); 
    Serial.print(".");
  };
  Serial.println ("\nconnected, meine IP:"+ WiFi.localIP().toString());
  
  //----------------------------------MQTT-Client 
  mqttclient.setServer(mqtt_server, 1883);
  mqttclient.setCallback(mqttcallback);

  //--------- prepare mqtt subscription 
  mqtt_sub_count++; // add new element 
  if (mqtt_sub_count < MAX_MQTT_SUB) { 
    mqtt_sub[mqtt_sub_count-1].topic = "start";
    mqtt_sub[mqtt_sub_count-1].fun = mqtt_callback_topic_start; //callback function
  } 
  else Serial.println(" err max. mqtt subscription");

  mqttreconnect();
  controlByte = 0;
}

Der loop() Bereich wird über die gesamte Laufzeit des Programms immer wieder wiederholt. Er prüft hier lediglich, ob das Button am Octopus gedrückt wurde und noch kein Aufnahmevorgang stattfindet.

void loop(){
  mqttclient.loop();
  
  if (digitalRead(2)==LOW && controlByte == 0)
  {
    start();
  }
  else
  {
    controlByte = 0;
    delayMicroseconds(60);
  }
}

Die .ino Datei erhalten Sie in der Musterlösung zur IoT-Toolchain zur Datenerfassung.

Zur Nutzung unseres Node-RED Flows müssen zunächst die Programme node.js und Python installiert sein.

Der abgebildete Fluss kann über das Menu /import/clipboard und Auswahl der bereitgestellten Datei (node-RED.txt) generiert werden, jedoch müssen die Pfade in den einzelnen Knoten auf Ihren Rechner angepasst werden. Falls dies noch nicht geschehen ist, bittet Node-RED um die Installation des Pakets node-red-contrib-mqtt-broker, das einen MQTT Broker einrichtet. Damit die Kommunikation zwischen IoT-Kit und Node-RED per MQTT funktioniert, muss das IoT-Kit dem Netz beitreten, indem der MQTT-Broker betrieben wird. Der Einfachheit halber haben wir mit unserem Rechner einen Hotspot geöffnet, zu dem sich das IoT-Kit anmeldet und haben die IP dieses Hotspots als MQTT Server eingetragen. Über den blauen inject-Knoten kann nun im Topic start das Kommando zu dem IoT-Kit gesendet werden, um eine Aufnahme einzuleiten. Das IoT-Kit bestätigt den Empfang und gibt das Kommando zum Abspielen der Sounddatei über das Topic play. Wir öffnen den Windows Media Player um eine Datei mit einem 1 ms dauernden 100 Hz Geräusch abzuspielen. Über das Topic transmit erreichen die Messwerte den Cloud-Rechner in Form von Bytes und werden durch die JavaScript Funktion convert_byte_to_int wieder zu Integer Werten zusammengesetzt, bevor sie in einer Datei zwischengespeichert werden. Ist die Datenübertragung abgeschlossen, wird die erstellte Datei zur vereinfachten nachfolgenden Bearbeitung über das Python-Skript prepare_data_format.py in eine .csv Datei umgewandelt.

Um die beschrieben Vorgänge zu gewährleisten wird folgende Ordnerstruktur empfohlen:

Ordner v1:
Ordner data (leer) – hier werden die .csv Dateien vom Python-Skript abgelegt
Datei data.csv – leere .csv Datei die eingehende Daten bevorratet
Datei output100hz1ms.wav – .wav-Datei mit Geräusch für Lautsprecher
Datei prepare_data_format.py – formatiert unsaubere data.csv zu valider .csv-Datei im ordner data und benennt diese mit dem aktuellen Datum-Zeit Stempel.
Datei soundGenerator.m – erstellt .wav-Datei mithilfe von Matlab.
Datei v1.ino – Arduino Pogramm aus Tab Abläufe Iot-Kit
Datei v1_node-red.txt – Beinhaltet Flussdiagramm zum Import in Node-RED.

Weitere Informationen zur IoT-Toolchain zur Datenerfassung und zur Generierung erhalten Sie im gleichnamigen Modulkonzept. Die beschriebenen Dateien erhalten Sie in der Musterlösung zur IoT-Toolchain zur Datenerfassung.

Downloads

Nachdem Bereiche definiert wurden, zwischen denen unterschieden werden soll, können die Messungen aus diesen Bereichen genutzt werden, um neue Messungen mit Ihnen zu vergleichen. Hierzu dient ein Supervised Learning (Überwachtes Lernen) Algorithmus, bei dem der Nutzer dem Algorithmus zusätzliche Daten, häufig sogenannte Labels zuführt. Mehr zu Supervised Learning erfahren Sie im Modulkonzept zu Supervised Learning. Die algorithmische Umsetzung wird zunächst in Übung 1 behandelt.

Der wesentliche Baustein hierzu ist der k-nächste Nachbarn (k-nearest neigbor kNN) Algorithmus, der Datenvektoren anhand eines gewählten Kriteriums vergleicht, und die ähnlichsten Messungen ermittelt. Die Arbeitsweise von kNN:

Visualisierung KNN

Mithilfe von kNN können die erfassten Daten dazu genutzt werden, um den Ursprung der Signale zwischen 2 Tischhälften zu unterscheiden. Erfahrene und interessierte können in Übung 2 versuchen den Pseudocode in Matlab umzusetzen, weitere Informationen zu kNN finden Sie auch im Modulkonzept zu kNN.

 

 

Der Versuch lässt sich auf mehrere Bereiche erweitern. Der Instanz-basierte kNN Algorithmus eignet sich sehr gut, um zwischen einer Vielzahl von Klassen zu unterscheiden. Hierzu kann es jedoch hilfreich sein, die Menge der Messungen zu begrenzen. Dazu eignet sich der k-Means Algorithmus, der eine gewünschte Anzahl Schwerpunkte aus einer Anzahl Messungen errechnen kann. k-Means gehört zu den Unsupervised Learning (Nicht Überwachtes Lernen) Algorithmen, bei dem der Algorithmus ohne zusätzliche Nutzerinformationen versucht, Muster in Daten zu erkennen. Mehr zu Unsupervised Learning erfahren Sie im Modulkonzept zu Unsupervised Learning. Die Arbeitsweise von k-Means:

Visualisierung k-Means

Mithilfe von kMeans werden in Übung 3 Cluster (Gruppen) aus Daten berechnet, um die grundsätzliche Funktionsweise zu demonstrieren. Erfahrene und interessierte können in Übung 4 versuchen den Pseudocode in Matlab umzusetzen, weitere Informationen zu kNN finden Sie auch im Modulkonzept zu k-Means.

 

 

Durch eine geschickte Kombination beider Verfahren lässt sich mit den Daten aus Übung 2 in Übung 5 zwischen 16 Bereichen unterscheiden. Alle Übungen erhalten Sie hier.

Downloads

Dieser Versuch demonstriert, dass eine Positionsbestimmung in einem Raum durch Maschinelles Lernen prinzipiell möglich ist. Durch die Aufzeichnung verschiedener Raumimpulsantworten an bekannten Positionen lässt sich durch Vergleich mit einer Aufzeichnung unbekannten Ursprungs auf deren Position schliessen. Die in diesem Versuch gewählte Kombination aus Lernalgorithmen wurde im Hinblick auf die Daten, die Laufzeit der Algorithmen und der Anzahl zu unterscheidender Fälle gewählt, in anderen Anwendungsfällen können durchaus andere Algorithmen sinnvoller sein. Die Verteilung der Anwendung in eine mobile und eine stationäre Einheit zur Zentralisierung rechenintensiver Aufgaben auf der Cloud-Plattformen wurde hier über den IoT-Kommunikationskanal MQTT realisiert.