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.

Bild Octopus mit Mikro

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)

Die Programmierung des IoT-Kits wird unter dem gleichnamigen Tab behandelt.

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 Machine Learning Algorithmen beschrieben.

Downloads

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 zunächst in einer Trainingsphase jeweils 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.

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. 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:

Einbindung kNN Skript

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

Pseudocode kNN (aus Modulkonzept)

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. Die Arbeitsweise von k-Means:

Einbindung k-Means Skript

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

Pseudocode kMeans (aus Modulkonzept)

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

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);
  }
}

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 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 output100hz1ms.wav – .wav-Datei mit Geräusch für Lautsprecher

Die zum Versuch gehörenden Skripte ermöglichen die Betrachtung der Aspekte der genutzten maschinellen Lernalgorithmen sowie die Datenvorverarbeitung.

Das Skript kMeans_mit_toolbox dient dazu, die erfassten Daten vorzuverarbeiten. Die Aufzeichnung der Raumimpulsantwort werden mit den Koordinaten Ihrer Aufzeichnung eingelesen und in Plots betrachtet. Erkannte Fehler werden bereinigt und irrelevante Teile der Aufzeichnung werden gelöscht. Die modifizierten Rohdaten werden normiert und bilden so die Arbeitsdaten für den Matlab eigenen kMeans Algorithmus zum Clustering. Durch die Durchführung des Clusterings mit verschiedenen Cluster Anzahlen lässt sich intuitiv der Unterscheidung verschiedener Bereiche auf dem Tisch erkennen.

Das Skript clustering_ohne_toolbox erfordert die eigenständige Umsetzung eines kMeans Algorithmus. Zur Realisierung des Clusterings sind zudem alle Tätigkeiten der Datenvorverarbeitung aus der vorhergehenden Übung auf alternativen Daten erforderlich.

Das Skript knn_mit_toolbox setzt den k-Nächste-Nachbar (kNN) Vergleich der Raumimpulsantworten um. Zunächst werden die Daten in Trainings- und Testmenge getrennt, die Trainingsdaten werden gelabelt und dienen als Modell zur Klassifikation und Visualisierung der Testdaten.

Im Skript knn_ohne_toolbox wird die Aufgabe um die eigenständige Implementierung des kNN Algorithmus erweitert.

Durch Umsetzung der Punkte Datenerfassung, Programmierung und Node-RED kann der gesamte Versuchsaufbau realisiert werden, um eigene Daten zu erfassen bzw. eine Innenraumortung durchzuführen.

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.