Gestensteuerung für den Magic Mirror
Zunächst habe ich mit dem Modul Simple Swiper gearbeitet, jedoch wollte ich auch eine Präsenz-Detektion. Da meine JS Kenntnisse sich nur auf etwas mehr als garkeine beschränken habe ich ein für mich leichter Verständliches Modul als Basis gewählt.
Also habe ich auf das Modul MMM-Gestures gewechselt. Um es zu Installieren müssen wir in der Konsole ins Modulverzeichnis navigieren und das GitHub Repository klonen.
cd MagicMirror/modules
git clone https://github.com/thobach/MMM-Gestures.git
Danach gehen wir in das neue Verzeichnis des Moduls und installieren die Dependencies.
cd MMM-Gestures && npm install
cd ..
MMM-Gestures
Das Modul besteht aus drei Komponenten. Es hat eine JavaScript Datei für das eigentliche Modul, eine node_helper.js die sich um das Empfangen und Senden der Befehle kümmert und einen Arduino Sketch, der die Sensoren verwaltet. Im original werden zwei Typen von IR-Sensoren verwendet. Ich hatte aber schon alles für die Nutzung der zwei HC-SR04 vorbereitet. Also der Spiegel ist ja fertig Aufgebaut. Also habe ich ein paar kleine Modifikationen vorgenommen.
Ein paar Tips für das Vorgehen beim Verändern, wenn man sich nicht so richtig mit der Materie auskennt, habe ich <hier/> mit rein geschrieben.
Das Modul steuert eigentlich nur das Newsfeed und das Compliments Modul an. Ich möchte aber weiterhin das Pages Modul für den Seitenaufbau nutzen, da hier schon alles eingerichtet ist und ich das ganze so auch ganz schick finde. Dazu kann man sich im SimpleSwiper Modul angucken, was gesendet wird um das Pages Modul anzusprechen. Oder man guckt direkt in der Repo des Pages Modul in die Dokumentation.
Die entsprechenden Befehle für das wechseln der Seite lauten also:
PAGE_INCREMENT bzw. PAGE_DECREMENT
Doch wie werden diese an das Pages Modul verschickt?
Das MM² System unterscheidet zwei Arten von Notifications. Es gibt sogenannte SocketNotifications, die aber nur zwischen Modul und node_helper gesendet werden. Und es gibt Notifications, die allgemein im Back-End an alle Module geschickt werden. Wenn ein Modul sich angesprochen fühlt, kann es eine Nachricht verarbeiten. Genau das brauchen wir hier.
Gucken wir uns also den Code des Moduls etwas an:
socketNotificationReceived: function (notification, payload) {
Log.info('Received message from gesture server: ' + notification + ' - ' + payload);
// forward gesture to other modules
this.sendNotification('GESTURE', { gesture: payload });
Übersetzt bedeutet das soviel wie: Wenn eine SockenNotification angekommen ist, schicke die Log.Info an das Log System und leite die Nachricht an alle weiter. SocketNotifications müssen eine Nachricht und einen Inhalt (Payload) haben. Im Log wird das ganze in einen String gepackt, damit Menschen das lesen können. Die normale Notification wird noch mit dem Hinweis Gesture versehen.
Danach folgt im Code ein Abschnitt für die ansteuerung des compliments Moduls:
// interact with compliments module upon PRESENT and AWAY gesture
var complimentModules = MM.getModules().withClass('compliments');
if (complimentModules && complimentModules.length === 1) {
var compliment = complimentModules[0];
if (payload === 'PRESENT') {
Log.info('Showing compliment after having received PRESENT gesture.');
compliment.show();
Hier wird Abgefragt ob das Modul mit der Klasse compliments registriert ist. Wenn ja soll die SocketNotification auf den Inhalt Present überprüft werden. Wenn das der Fall ist, soll das Modul eingeblendet werden. Die SocketNachricht stammt dabei vom node_helper der diese vom Arduino bekommen hat. An diesen Aufbau halten wir uns auch.
Also ergänzen wir in der Datei (MMM-Gestures.js) eine Abfrage ob das Pages Modul tatsächlich geladen ist. Gerne direkt unter den Compliments Abschnitt. Ich markiere meine Änderungen immer gerne mit einem Kommentar, damit ich weiß, welchen Bereich ich verbockt hab.
//Customized Part for pages Module
var pagesModules = MM.getModules().withClass('MMM-pages');
console.log(new Date() + ': Found Module: ' + pagesModules);
if (pagesModules) {
var notification = "UNKNOWN";
if (payload === 'LEFT') {
Log.info("Swiping Left because of LEFT Gesture")
this.sendNotification("PAGE_DECREMENT", 1);
}
if (payload === 'RIGHT') {
Log.info("Swiping Right because of RIGHT Gesture")
this.sendNotification("PAGE_INCREMENT", 1);
}
else {
Log.info('Not handling received gesture in this module directly:');
Log.info(payload);
}
}
//--- End Custom Code ---
Wenn unsere Arduino Nachricht Left beinhaltet, schicken wir den Page_Decrement Befehl raus, wenn right Page_Increment. Achtet auf die korrekten Schreibweisen im Code, sonst versteht das Modul euch nicht.
Die Log-Infos sind zur Hilfe, um zu sehen ob auch alles Funktioniert. Auf der Magic Mirror Seite sind wir damit vorerst fertig.
Arduino
Ich habe einen Arduino Nano verwendet. Dieser hängt einfach mit einem USB Kabel am RaspberryPi. Die beiden reden über die Serielle Schnittstelle miteinander. Nunja, eigentlich redet nur der Arduino.
Man kann die Arduino IDE direkt auf dem Raspberry Installieren, dann muss man nicht ständig den Rechner wechseln, wenn man den Sketch entwickelt. Die Version in der Paketverwaltung ist aber schon ziemlich überholt. Deshalb ladet euch am besten auf der Arduino Website einfach die aktuelle Version für Linux ARM 32 herunter und installiert diese.
Ich habe mich am originalen Arduino Sketch orientiert, welche Befehle an den Raspi gehen, damit ich hier nicht all zu viel anpassen muss.
Für die Ansteuerung der Sensoren nutze ich die NewPing Library. Der Sketch sieht bei mir im Moment noch so aus, da einer der Sensoren nicht läuft. Ich tippe auf einen Kabelfehler, war aber bisher zu faul alles wieder auf zu machen. Also habe ich erstmal nur für einen Sensor zu Ende entwickelt.
#include <NewPing.h>
#define TRIGGER_PIN 4 // Arduino pin tied to trigger pin on ping sensor.
#define ECHO_PIN 5 // Arduino pin tied to echo pin on ping sensor.
#define MAX_DISTANCE 200 // Maximum distance we want to ping for (in centimeters). Maximum sensor distance is rated at 400-500cm.
#define presenceThreshold 95
#define awayThreshold 105
#define gestureThreshold 40
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.
unsigned int pingSpeed = 50; // How frequently are we going to send out a ping (in milliseconds). 50ms would be 20 times a second.
unsigned long pingTimer; // Holds the next ping time.
int zwSpeicher = MAX_DISTANCE;
boolean inUse = false;
boolean Update = false;
boolean gestureUpdate = false;
boolean gestureReceived = false;
boolean gestureActive = false;
int countAway = 0;
int gestureTimer = 0;
void setup() {
Serial.begin(115200); // Open serial monitor at 115200 baud to see ping results.
pingTimer = millis(); // Start now.
}
void loop() {
measureDistance();
checkPresence();
if (inUse) {
checkGesture();
}
}
void measureDistance() {
// Notice how there's no delays in this sketch to allow you to do other processing in-line while doing distance pings.
if (millis() >= pingTimer) { // pingSpeed milliseconds since last ping, do another ping.
pingTimer += pingSpeed; // Set the next ping time.
sonar.ping_timer(echoCheck); // Send out the ping, calls "echoCheck" function every 24uS where you can check the ping status.
}
}
void echoCheck() { // Timer2 interrupt calls this function every 24uS where you can check the ping status.
// Don't do anything here!
if (sonar.check_timer()) { // This is how you check to see if the ping was received.
// Here's where you can add code.
Serial.print("Ping: ");
zwSpeicher = (sonar.ping_result / US_ROUNDTRIP_CM); // Ping returned, uS result in ping_result, convert to cm with US_ROUNDTRIP_CM.
Serial.print(zwSpeicher);
Serial.println("cm");
}
// Don't do anything here!
}
void checkPresence() {
if (zwSpeicher <= presenceThreshold) {
if (inUse) {
Update = false;
}
if (!inUse) {
Update = true;
}
inUse = true;
countAway = 0;
}
if (zwSpeicher >= awayThreshold) {
if (inUse) {
Update = true;
}
if (!inUse) {
Update = false;
}
countAway++;
Serial.print("Away Counter: ");
Serial.println(countAway);
if (countAway > 50) {
inUse = false;
}
}
if (Update) {
handlePresence();
}
}
void checkGesture() {
if (zwSpeicher <= gestureThreshold) {
if (gestureReceived) {
gestureUpdate = false;
}
if (!gestureReceived) {
gestureUpdate = true;
}
gestureReceived = true;
Serial.println(F("Info: A Gesture was received"));
Serial.print(F("Info: Sensor detected Distance of: "));
Serial.print(zwSpeicher);
Serial.println(F(" cm"));
gestureTimer = 0;
}
if (zwSpeicher >= gestureThreshold) {
if (gestureReceived) {
gestureUpdate = true;
}
if (!gestureReceived) {
gestureUpdate = false;
}
gestureReceived = false;
}
if (gestureUpdate) {
handleGesture();
}
}
void handleGesture() {
// if (zwSpeicher <= gestureThreshold) { //switch this to sensor Left later
// }
if (zwSpeicher <= gestureThreshold) { //switch this to sensor Right later
Serial.println(F("Gesture: RIGHT"));
gestureActive = true;
}
else {
Serial.println(F("Gesture: NONE"));
checkPresence();
}
while (gestureActive) {
gestureTimer++;
Serial.print("Gesture is still active: ");
Serial.println(gestureTimer);
if (sonar.ping_cm() >= gestureThreshold ) {
gestureActive = false;
}
}
Serial.println();
}
Der Code
Was bedeuten die Code-Schnipsel?
#include <NewPing.h>
Mit dem include Befehl, wird die Bibliothek eingebunden. Diese ist mittlerweile Teil des Arduino-Universum und kann direkt in der IDE über über die Bibliothekverwaltung eingebunden werden.
#define TRIGGER_PIN 4 // Arduino pin tied to trigger pin on ping sensor.
#define ECHO_PIN 5 // Arduino pin tied to echo pin on ping sensor.
#define MAX_DISTANCE 200 // Maximum distance we want to ping for (in centimeters). Maximum sensor distance is rated at 400-500cm.
#define presenceThreshold 95
#define awayThreshold 105
#define gestureThreshold 40
Diese Defines legen fest, das überall im Code wo zum Beispiel TRIGGER_PIN steht, dieser durch eine 4 ersetzt werden soll. Man kann die Pins auch mit int Variablen Festlegen, doch sind defines Platzsparender. Den die Pins wollen wir ja ohnehin nicht verändern.
Also alle festen Werte können gerne mit #define festgelegt werden.
NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DISTANCE); // NewPing setup of pins and maximum distance.
Hier wird das Objekt für die NewPing Bibliothek deklariert.
unsigned int pingSpeed = 50; // How frequently are we going to send out a ping (in milliseconds). 50ms would be 20 times a second.
unsigned long pingTimer; // Holds the next ping time.
Es gibt für Variablen einen sogenannten Modifikator. In diesem fall nutzen wir unsigned. Das bedeutet einfach, dass die int pingSpeed und die long pingTimer ohne Vorzeichen auskommen müssen.
int zwSpeicher = MAX_DISTANCE;
boolean inUse = false;
boolean Update = false;
boolean gestureUpdate = false;
boolean gestureReceived = false;
boolean gestureActive = false;
int countAway = 0;
int gestureTimer = 0;
Als nächstes folgt die klassische Variablendeklaration. Mit diesen Rechnen wir. Damit keine zufälligen Speicherwerte unsere Berechnungen beeinflussen, werden alle Variablen mit einem günstigen Startwert vordefiniert. Daher will ich zum Beispiel meinen Mess-Zwischenspeicher zwSpeicher mit der maximalen Messdistanz überschreiben und nicht mit 0.
void setup() {
Serial.begin(115200); // Open serial monitor at 115200 baud to see ping results.
pingTimer = millis(); // Start now.
}
Arduino Programme durchlaufen nach dem Start immer einmal die Setup-Funktion (vom Typ void). Hier wird bei uns der Serielle Monitor geöffnet. Achtung: Wenn ihr hier auf einer Baudrate von 115200 funkt, müsst ihr das auch in der node_helper.js anpassen. Diesen Fehler hatte ich zu Anfang drin. Könnt ihr auch auf 9600 lassen. Im Seriellen Monitor muss auch immer die Baudrate passend eingestellt werden.
Dann machen wir die erste Zeitmessung in dem wir die aktuelle ms Zeit seit boot in die Variable pingTimer schreiben.
void loop() {
measureDistance();
checkPresence();
if (inUse) {
checkGesture();
}
}
Bei Arduino Programmen wird nach der SetUp in einer Endlosschleife die Loop Funktion bearbeitet. Hier rufe ich einfach nur meine Unterfunktionen auf.
void measureDistance() {
// Notice how there's no delays in this sketch to allow you to do other processing in-line while doing distance pings.
if (millis() >= pingTimer) { // pingSpeed milliseconds since last ping, do another ping.
pingTimer += pingSpeed; // Set the next ping time.
sonar.ping_timer(echoCheck); // Send out the ping, calls "echoCheck" function every 24uS where you can check the ping status.
}
}
Hier wird eine Funktion gebaut, mit der die Entfernung durch den Sensor gemessen wird. Ich ignoriere alles was garkeinen Ping macht. Das ist notwendig, weil sonst 0cm als Distanz ausgegeben wird. Also kein Signal würde sonst meine Geste triggern.
void echoCheck() { // Timer2 interrupt calls this function every 24uS where you can check the ping status.
// Don't do anything here!
if (sonar.check_timer()) { // This is how you check to see if the ping was received.
// Here's where you can add code.
Serial.print("Ping: ");
zwSpeicher = (sonar.ping_result / US_ROUNDTRIP_CM); // Ping returned, uS result in ping_result, convert to cm with US_ROUNDTRIP_CM.
Serial.print(zwSpeicher);
Serial.println("cm");
}
// Don't do anything here!
}
In der measureDistance funktion rufe ich echoCheck auf. Wenn es einen Ping gibt, will ich die genaue Distanz wissen.
void checkPresence() {
if (zwSpeicher <= presenceThreshold) {
if (inUse) {
Update = false;
}
if (!inUse) {
Update = true;
}
inUse = true;
countAway = 0;
}
if (zwSpeicher >= awayThreshold) {
if (inUse) {
Update = true;
}
if (!inUse) {
Update = false;
}
countAway++;
Serial.print("Away Counter: ");
Serial.println(countAway);
if (countAway > 50) {
inUse = false;
}
}
if (Update) {
handlePresence();
}
}
Und dieser Abschnitt überprüft nur noch ob jemand näher als der Grenzwert am Spiegel steht. Wenn ja, ob schon länger jemand da steht oder es das erste mal ist. So senden wir nicht ständig den Befehl aus, sondern nur wenn sich der Status auch wirklich ändert. Das gleiche gilt für den awayThreshold. Wenn es wirklich was zum Updaten gibt, wird die funktion handlePresence aufgerufen.
void handlePresence() {
if (inUse) {
Serial.println(F("Person: PRESENT"));
pingSpeed = 50;
}
if (!inUse) {
Serial.println(F("Person: AWAY"));
pingSpeed = 500;
}
}
Diese bestimmt zum einen den Messintervall. Wenn niemand da ist messe ich nur alle 500ms, wenn jemand da ist, alle 50. Außerdem soll das Statusupdate an den Raspi gesendet werden.
Ich unterscheide dann einfach ob inUse oder !inUse. Diese Variable nutze ich auch um die Funktion Check Gesture aufzurufen. Denn wenn niemand da ist, muss ich auch keine Gesten prüfen.
void checkGesture() {
if (zwSpeicher <= gestureThreshold) {
if (gestureReceived) {
gestureUpdate = false;
}
if (!gestureReceived) {
gestureUpdate = true;
}
gestureReceived = true;
Serial.println(F("Info: A Gesture was received"));
Serial.print(F("Info: Sensor detected Distance of: "));
Serial.print(zwSpeicher);
Serial.println(F(" cm"));
gestureTimer = 0;
}
if (zwSpeicher >= gestureThreshold) {
if (gestureReceived) {
gestureUpdate = true;
}
if (!gestureReceived) {
gestureUpdate = false;
}
gestureReceived = false;
}
if (gestureUpdate) {
handleGesture();
}
}
Diese Funktion prüft dann nur noch ob der Sensor Messwert den Grenzwert für Gesten unterschritten hat. Später wird hier zwischen links und rechts unterschieden. Auch hier unterscheide ich ob es wirklich was zu Berichten gibt, damit ich nicht zu Oft den Befehl rausschicke.
void handleGesture() {
// if (zwSpeicher <= gestureThreshold) { //switch this to sensor Left later
// }
if (zwSpeicher <= gestureThreshold) { //switch this to sensor Right later
Serial.println(F("Gesture: RIGHT"));
gestureActive = true;
}
else {
Serial.println(F("Gesture: NONE"));
checkPresence();
}
while (gestureActive) {
gestureTimer++;
Serial.print("Gesture is still active: ");
Serial.println(gestureTimer);
if (sonar.ping_cm() >= gestureThreshold ) {
gestureActive = false;
}
}
Serial.println();
}
Und hier gebe ich dem Arduino noch etwas nutzloses zu tun, während die Geste aktiv ist. Testen können wir den Sketch direkt mit dem Seriellen Monitor in der Arduino IDE.
MagicMirror
Jetzt können wir den Spiegel wieder starten und testen ob alles zu unserer Zufriedenheit läuft.