Dinodialogomat: Audio von MP3-Player steuert Mundbewegung mit Servos (🚧)
Zwei Dinofiguren treten in Dialog und ihre Münder werden über Servos synchron zur Sprache bewegt. Ein MP3-DFPlayer Mini liefert das Audio-Signal, das über Analogeingänge des Arduino in Servosignale umgewandelt wird (Schwätzomat).
Der Start des Automaten geschieht über einen Ultraschallsensor.
Um bei 8 Bit-µC wie Arduino UNO, NANO und MEGA Speicher- und Rechenzeit zu sparen wurden die Speicherung und Berechnungen überwiegend mit 8 Bit realisiert. Z.B. werden die Servopositionen durch 10 geteilt gespeichert: 1500µs -> 150 Speicherwert mit 1 Byte statt 1500 mit 2 Byte.
- Hardware
- DFPlayer Mini
- Schätzomat steuert mit Audio Mundbewegungen
- Automatensoftware des Dinodialogomat (🚧)
Blockschaltbild der Prototypschaltung
Die Pin-Belegungen sind hier nicht hübsch geraten, bis auf die Analog-Eingänge aber frei wählbar. Die Münder der Figuren werden durch Servos bewegt. Der DFPlayer Mini ist über eine Software Serielle Schnittstelle verbunden, ein Widerstand R1 ist notwendig, weil der Player intern auf 3,3V läuft, der Arduino UNO liefert jedoch 5V, durch den Widerstand wird der Strom begrenzt. Die Stereoausgänge des Players sind mit den Analogeingängen des Arduino verbunden. Mit einem Ultraschallsensor wird gemessen, ob sich eine Person vor dem Automat befindet. Das Poti R4 dient zur Lautstärke-Einstellung und um ggfs. die Endpositionen der Servos in einem Setup justieren zu können. Der Taster S1 steuert den Servojustage-Ablauf.
Servos
Die verwendeten Servos benötigen einen kräftigen Strom >1A, daher bekommen sie eine unabhängige 5V Spannungsversorgung.
Beim Einschalten bekommen die Servos noch kein Servosignal, normalerweise tun sie dann auch nichts, leider fahren meine Servos nach dem Einschalten in eine Position, bei der das Getriebe krachende Geräusche von sich gibt.
DFPlayer Mini und Library DFPlayer Mini Fast
Links zum Player: [🔗 wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299] [🔗 Datenblatt]
[https://wolles-elektronikkiste.de/dfplayer-mini-ansteuerung-mit-dem-arduino] (Gute Beschreibung der Verzeichnisstruktur der SD-Karte)
Bei den meisten Anwendungen des Players speichere ich die Audiodateien im mp3-Ordner. Die Anzahl der Dateien ist bekannt, bzw. es ist klar welche Datei abgespielt werden soll. Bei diesem Projekt soll der Automat jedoch selber aus einer beliebigen Anzahl von Stücken auswählen können. Auch wäre es praktisch bei der Inbetriebnahme für die Servo-Justage Ansagen zu hören, statt einen Rechner für die Ausgabe der Justage-Anweisungen zu benötigen. Die Justage-Ansagen sollen in einem Unterordner untergebracht werden.
Dateien auf SD-Karte im „mp3“ Verzeichnis speichern
Dateistruktur: Dateien sollten mit Nummern wie 0001.mp3 gespeichert sein.
Wenn die Dateien im Root- (Haupt-) Verzeichnis abgelegt werden, gibt die Reihenfolge beim Kopieren auf die SD-Karte die Nummerierung vor, unabhängig vom Dateinamen. In Unterverzeichnissen wie „mp3“ wird dagegen die Nummer im Dateinamen beachtet. Hinter den Nummern dürfen durchaus weitere beschreibende Texte stehen, siehe Beispiel rechts.
Dateien im Unterverzeichnis mp3 werden mit playFromMP3Folder(nummer) abgespielt.
Die Benennung ist nach dem Schema 0001xxx.mp3.
Verwenden von Unterverzeichnissen „01“..“99″
Da ich Audio-Dateien zur Justage der Servos vorgeben möchte und die Anzahl der Spielstücke nicht bekannt ist, dachte ich mir, dass zwei Verzeichnisse dafür sinnvoll wären. Ein Verzeichnis 01 mit den Spielstücken und ein Verzeichnis 02 mit meinen Justageanweisungen.
Unterverzeichnisse werden mit 01..99 benannt. In den Verzeichnissen werden die Stücke mit diesem Schema benannt: 001xxx.mp3
Abgespielt werden die Stücke mit playFolder(verzeichnis,stück).
Die Anzahl der Spielstücke wollte ich mit numTracksInFolder(i) ermitteln, das funktionierte nicht wie erwartet, es wird nur die Anzahl des Ordners zurück gegeben, der zuletzt gespielt wurde, auch bei der DFRobotDFPlayerMini-Lib war das nicht anders.
MacOS Dot-Clean
Eine blöde Sache ist das Hinterlegen von „unsichtbaren“ Dateien auf der SD-Speicherkarte durch das Betriebssystem, der Player lässt sich dadurch irritieren.
Eine Lösung ist hier zu finden: wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299#target_6🔗
Welche Arduino Library zur Ansteuerung des Players soll ich nehmen?
Die Kommunikation zum Player läuft über eine serielle Schnittstelle. Es werden Kommandos wie Play Next: [7E FF 06 01 00 00 00 FE FA EF] darüber gesendet und es werden im gleichen Format Antworten vom Player zurück gesendet siehe [🔗 Datenblatt].
Die Librarys erledigen dies Nutzerfreundlich, man muss sich nicht selber um die Kommandos kümmern. Allerdings kann es dabei Probleme mit der seriellen Kommunikation geben, das Timing ist nicht ohne. Ausserdem verhalten sich einige Kommandos nicht wie erwartet. In den Issues auf Github gibt es einige „Tränen“..
Z.B. Anzahl der Dateien in einem Verzeichnis bestimmen funktioniert nicht richtig: [🔗 forum.arduino.cc/t/dfplayer-anzahl-der-dateien-im-verzeichnis/926305/11]
- DFPlayerMini Fast🔗 verwende ich vorerst weiter
- DFRobotDFPlayerMini🔗
- DFMiniMp3🔗 von Makuna, noch nicht getestet..
Abfragen ob der Player gerade spielt
Es gibt zwei Möglichkeiten dafür:
- myMP3.isPlaying() wird über die Serielle Schnittstelle gemacht, braucht aber 64ms!
- Über den BUSY-Pin am Player, solange er Low ist spielt der Player (gewählt).
Schwätzomat steuert mit Audio Mundbewegungen
Wikipedia: schwätzen🔗 Seit 1989 baue ich Schwätzomaten-Elektronik, damals wurden noch mit Analogtechnik Hubmagneten angesteuert. Schwätzenden Figuren soll der Mund möglichst passend zur Sprache animiert werden. Eine Herausforderung war und ist die Qualität des Audiomaterials, schwankende Lautstärke und Rauschen, damals mit Kassettenspielern🔗 heute mit MP3 müssen ausgeglichen werden.
AD-Wandler Referenzspannung einstellen
Die Analogsignale des Players und des Potentiometers werden mit dem AD-Wandler des Arduino gemessen. Als Spannungsreferenz dient normalerweise die Versorgungsspannung des µC, beim Uno 5V. Siehe [arduino.cc/reference/en/language/functions/analog-io/analogreference/]
Die Auflösung ist dabei typischerweise 10 Bit -> 0..1023 als Messwert. Siehe [https://docs.arduino.cc/language-reference/de/funktionen/analog-io/analogRead/]
Nachteilig ist dabei, dass für kleine Messwerte die AD-Wandlerwerte klein sind und ausserdem Spannungsschwankungen und Rauschen der Versorgungsspannung die Messungen verschlechtern. Daher verwende ich die interne 1,1V Referenz des ATmega328P (UNO, NANO).
Im setup() eingestellt mit analogReference(INTERNAL); Die Referenzspannung wird dabei auf dem AREF-Pin des µC ausgegeben an den ich das Poti angeschlossen habe.
Bei einem anderen µC ist die Referenzspannungseinstellung entsprechend an zu passen, z.B. beim Arduino MEGA auf INTERNAL1V1.
Die Lautstärke des DFPlayers-Players sollte nicht <20 eingestellt werden, da die Signale dann zu klein werden.
Schwätzomat Audio-Messprinzip
- Die Servos werden mit 50Hz PWM-Signal angesteuert, öfter als 50 mal pro Sekunde einen neuen Wert ein zu stellen macht keinen Sinn.
- Eine AD-Wandlung mit analogRead() und Verarbeitung braucht ca. 130 µs. Bei zwei Kanälen sind das 260 µs, die Abtastfrequenz ist ca. 3,8 kHz. Bis ein neues Servosignal ausgegeben werden soll können ca. 76 Messungen vorgenommen werden- nenne ich Epoche.
- Soll der Mittelwert der Messungen oder der Maximalwert einer Epoche verwendet werden? Ich habe mit dem Maximalwert bessere Ergebnisse erzielt.
- Die Lautstärke der Stücke ist nicht gleich, die Lautstärke des MP3-Players kann verändert werden. Während das Stück läuft ermittle ich ständig das Maximum des Kanals und passe die Servoausschläge entsprechend an.
- Es gibt ein Grundrauschen (DA-Wandler des DFPlayers rauscht!) bei den Stücken, die Münder würden dadurch nicht geschlossen. Ausserdem ermittle ich den Minimalwert und ziehe ihn vom Epochen-Audiowert ab.
- Leisere Passagen führten zu sehr geringen Servoausschlägen, der Mund bewegte sich kaum. Bei niedrigen Audio-Werten (<maxAudio/2) wird nun der Audiowert mit 2 multipliziert verarbeitet.
- Der Audiowert einer Epoche wird mit dem Maximalwert des Stückes auf die Servopositionen mundZu, mundOffen umgerechnet (mapping).
Ausgabe der Epochen-Werte mit Seriellem Plotter.
Siehe Software-Doku..
Um die Messungen auch mit 8 Bit µC effizient durchführen zu können habe ich ein wenig gebastelt. ToDo: 16-Bit Variante erstellen und testen ob sie wirklich schlechter ist.
Die Servos bekommen nur während ein Stück abgespielt wird ein PWM-Signal. Analog-Servos sind dadurch zwischen den Stücken „entspannt“ und verbrauchen weniger Strom. Bei Digital-Servos muss das Verhalten überprüft und ggfs. angepasst werden.
Mund-Servos justieren
Die Servos bewegen die Münder recht dynamisch, daher ist es wichtig die Endpositionen mundZu und mundOffen so ein zu stellen, dass kein Verschleiß provoziert wird. Typischerweise befinden sich die Servos bei 1,5 ms bzw. 1500 µs in der Mitte. Folgend zwei Varianten die Servos zu justieren:
Werte mit Servotester ermitteln und einprogrammieren
Mit einem Servotester wie [3.2 Servo steuern mit PWM] oder [mezdata.de/avr/370_servotester/index.php] oder aus dem Handel können die Endpositionen ermittelt werden.
Der Servo wird mit 1500 µs angesteuert und dann der Servohebel so aufgesetzt, dass der Mund dabei in der Mitte steht. Danach werden die Werte für mundZu und mundOffen ermittelt und können in der Software eingetragen werden.
// Endpositionen der Servos für mundZu und mundAuf kennen z.B. 1800µs -> 180 eintragen
Schwaetzo schwaetzoL(A1,9,180,140); // AudioIn, ServoOut, mundZu, mundOffen
Schwaetzo schwaetzoR(A0,8,170,130);
Werte durch Justage am Automaten ermitteln und in EEPROM speichern
Bei dieser Methode sind die Servodaten noch nicht bekannt, in der Software wird dieser Code verwendet:
Schwaetzo schwaetzoL(A1,9); // AudioIn, ServoOut, noch unjustiert
Schwaetzo schwaetzoR(A0,8);
Hinweis: In dieser Version gehe ich davon aus, dass eine Verbindung zum Seriellen Monitor des Arduino (115200 Baud) besteht. Angedacht aber noch nicht realisiert ist eine Version, die die Anweisungen als MP3 Ausgabe spricht.
Bei dieser Methode wird bei Erstinbetriebnahme entweder vor Einschalten der Spannung der Taster gedrückt oder im Seriellen Monitor ein j zum Automaten gesendet, danach befindet sich der Automat im Servo-Justage-Modus, hier die Ausgaben auf dem Seriellen Monitor (bzw. Audioausgaben):
- „Bitte bei Erstjustage alle Servohebel von den Achsen abziehen! Dann Taste drücken.“ Nach Tastendruck werden die Servos in die Mitte gefahren.
- „Beide Servohebel so aufsetzen, dass der jeweilige Mund halb geöffnet ist. Dann Taste drücken.“
- „Bitte Poti in die Mitte stellen. Dann Taste drücken.“ Das Poti steuert die Servos bis zu den Maximalpositionen, um Beschädigungen zu vermeiden sollte es dabei nahe der Mitte 160..140 stehen.
- „Poti steht nicht in der Mitte: 170“ Poti zu Mitte bewegen
- „Linken Servo in MundZu Position einstellen. Dann Taste drücken.“ Mund sollte zu sein, aber die Lippen müssen nicht aufeinander gepresst sein 😉.
- „Linken Servo in MundOffen Position einstellen. Dann Taste drücken.“ Mund sollte offen sein, aber den Kiefer nicht ausrenken.
- „Bitte Poti in die Mitte stellen. Dann Taste drücken.“ Siehe 3.
- „Poti steht nicht in der Mitte: 130“
- „Rechten Servo in MundZu Position einstellen. Dann Taste drücken.“
- „Rechten Servo in MundOffen Position einstellen. Dann Taste drücken.“
- „Servos justiert.“ Die ermittelten Werte werden im EEPROM gespeichert und bei jedem Neustart des Automaten wieder daraus geladen.
Synopsis: [arduino.cc/en/Tutorial/LibraryExamples/EEPROMWrite] [docs.arduino.cc/learn/built-in-libraries/eeprom/]
Schwaetzo-Audio-Library
// SchwaetzoLib.h V1.2 © 29.12.2024 Oliver Mezger MezMedia.de CC BY-SA 4.0
#ifndef SchwaetzoLib_h
#define SchwaetzoLib_h
#endif
#include "Arduino.h"
#include <Servo.h>
#define DEFAULT_MIN_AUDIO 20 // Voreinstellung Minimaler Audiolevel bei rauschenden Aufnahmen
#define DEFAULT_MAX_AUDIO 60 // Voreinstellung Maximaler Audiolevel
#define STILL_SCHWELLE 3 // Schwelle 1/4 Maximum (nicht verwendet)
class Schwaetzo{
public:
Schwaetzo(byte ein,byte aus); // AudioEingang ServoAusgang
Schwaetzo(byte ein,byte aus,byte mzu, byte moffen); // AudioEingang ServoAusgang Servopositionen
void setMundZu(byte zu); // Servoposition für mundZu einstellen
void setMundOffen(byte auf); // Servoposition für mundAuf einstellen
void go(); // periodisch aufrufen um Analog einzulesen und Servo zu steueren
void an(); // Servo wird angeschlossen
void aus();// Servo wird deaktiviert
byte getMaxAudio(); // Audio Maximalwert
byte getLastAudio(); // letzer ermittelter Audiowert für Servo
void moveServo(byte n); // Servo an Position n bewegen zur Justage
void schreibeEEPROM(); // Servodaten ins EEPROM schreiben
void leseEEPROM(); // Servodaten aus EEPROM lesen
private:
static byte instanzen; // Klassenvariable
byte instanzNummer; // die Schwaetzoinstanzen werden durchnummeriert für EEPROM Adressen
Servo servo; // Servo-Instanz
byte audioEingang; // AnalogPin fuer Sound
byte servoAusgang; // Servoanschluss
byte mundZu = 150; // Servoposition wenn Mund zu {100..200}
byte mundOffen = 150; // Servoposition wenn Mund offen {100..200}
byte servoWeg = 0; // mundOffen-nunZu
byte soundSampels = 0 ; // Zaehler fuer Messungen in einer Epoche
byte minAudio = DEFAULT_MIN_AUDIO; // leisester Wert eines Stueckes (Rauschen)
byte maxAudio = DEFAULT_MAX_AUDIO; // lautester Wert eines Stueckes
uint16_t audio = 0; // lautester Wert von n Messungen
byte audioShift = 1; // analogReadWert >> 1 wird bei lautem Audio erhöht um innerhalb 8 Bit zu bleiben
byte lastAudio = 0; // für Ausgabe auf Seriellem Plotter
};
// SchwaetzoLib.cpp V1.2 © 29.12.2024 Oliver Mezger MezMedia.de CC BY-SA 4.0
#include "Arduino.h"
#include "SchwaetzoLib.h"
#include <EEPROM.h>
byte Schwaetzo::instanzen = 0; // Klassenvariable für Instanznummer
Schwaetzo::Schwaetzo(byte ein,byte aus){ // AudioEingang ServoAusgang für Verfahren Servo-Justage mit Automat
instanzNummer = instanzen++; // die Instanzen nummerieren wegen EEPROM
audioEingang = ein;
servoAusgang = aus;
pinMode(aus, OUTPUT); // Servosignal = 0
digitalWrite(aus,LOW); // einstellen
}
Schwaetzo::Schwaetzo(byte ein,byte aus,byte mzu, byte moffen){ // AudioEingang ServoAusgang MundZu MundOffen
Schwaetzo(ein,aus);
setMundZu(mzu);
setMundOffen(moffen);
}
void Schwaetzo::setMundZu(byte zu){ // Servoposition für mundZu einstellen
if (zu>=100 && zu<= 200) mundZu=zu;
else{
Serial.print(F("setMundZu: Wert passt nicht: "));
Serial.println(zu);
}
servoWeg = abs(mundOffen-mundZu);
}
void Schwaetzo::setMundOffen(byte auf){ // Servoposition für mundOffen einstellen
if (auf>=100 && auf<= 200) mundOffen = auf;
else{
Serial.print(F("setMundOffen: Wert passt nicht: "));
Serial.println(auf);
}
servoWeg = abs(mundOffen-mundZu);
}
void Schwaetzo::go(){ // Sample aufnehmen braucht 130µs somit bei 2 Kanälen 3,846 kHz Abtastfrequenz
static uint16_t n;
if (soundSampels >=100){ // wurden genug Sampels in Epoche aufgenommen 3846Hz/50Hz=77
soundSampels = 0;
if (audio > 255){ // wenn es zu laut ist Messungen abschwächen
audio = 255;
audioShift++; // mehr Vorteilen, Messwert halbieren
minAudio = DEFAULT_MIN_AUDIO;
maxAudio = DEFAULT_MAX_AUDIO;
} else {
if (audio > maxAudio) maxAudio = audio; // Maximallautstärke des Stückes merken
if (audio < minAudio) minAudio = audio; // Minimallautstärke des Stückes merken
if(audio>=minAudio) audio -= minAudio; // Rauschen abziehen
if (audio < maxAudio/2){ // leise Stellen verstärken
n = audio * servoWeg * 2 / maxAudio; // Servoausschlag berechnen
} else {
n = audio * servoWeg / maxAudio; // Servoausschlag berechnen
}
if(n>servoWeg) n = servoWeg; // Servoausschlag begrenzen
if(mundOffen>mundZu) n = mundZu + n;
else n = mundZu - n;
servo.writeMicroseconds(n*10); // Servo Pulsweite ausgeben
}
/*if (audio > (maxAudio>>stillSchwelle)){ // Schwelle 1/8 Maximum
//sprachSituation |= 1;
//ausTimer =0;
}*/
lastAudio = audio; // letzten Wert merken
audio=0;
}
else{ // Audio-Messung
n = analogRead(audioEingang)>>audioShift; // Audiosignal lesen
if(n>audio) audio=n; // Maximum finden
soundSampels++;
lastAudio=0;
}
}
void Schwaetzo::an(){ // Servo wird angeschlossen, bekommt Signal
minAudio = DEFAULT_MIN_AUDIO; // neues Stück neues Glück
maxAudio = DEFAULT_MAX_AUDIO;
audio = 0;
soundSampels = 0;
lastAudio = 0;
audioShift = 1; // Abschwächen von analogRead
servo.attach(servoAusgang,1000,2000); // Servo anschließen
Serial.print(F("Servo angeschlossen "));
Serial.println(servoAusgang);
}
void Schwaetzo::aus(){ // Servosignal wird abgeschaltet
servo.writeMicroseconds(mundZu*10); // Mund schließen
delay(100); // warten bis ausgeführt
servo.detach(); // Servosignal abschalten
Serial.print(F("Servo aus, audioShift: ")); // Infos über das Stück ausgeben
Serial.print(audioShift);
Serial.print(F(" MinLevel: "));
Serial.print(minAudio);
Serial.print(F(" MaxLevel: "));
Serial.println(maxAudio);
}
byte Schwaetzo::getMaxAudio(){ // maximaler Audiowert
return maxAudio;
}
byte Schwaetzo::getLastAudio(){ // letzter Audiowert, 0 während Messung
return lastAudio;
}
void Schwaetzo::moveServo(byte n){ // Servo einstellen während Justage
servo.writeMicroseconds(n*10);
}
void Schwaetzo::schreibeEEPROM(){ // Servodaten ins EEPROM schreiben
EEPROM.update((instanzNummer+1)*2, mundZu);
EEPROM.update((instanzNummer+1)*2+1, mundOffen);
}
void Schwaetzo::leseEEPROM(){ // Servodaten aus EEPROM lesen
byte i;
i = EEPROM.read((instanzNummer+1)*2);
setMundZu(i);
i = EEPROM.read((instanzNummer+1)*2+1);
setMundOffen(i);
}
Automatensoftware des Dinodialogomat
Die Audiostücke werden aus dem mp3-Verzeichnis wieder gegeben, Audio-Anweisungen zur Servo-Justage sind noch nicht implementiert.
- Entfernungsmessung mit Ultraschall
- Einbinden Schwaetzo
- Wecker, nichtunterbrechende Zeitverzögerung
- Zustandsautomat, enum
Bedienung über Seriellen Monitor
Über die serielle Schnittstelle (115200 Baud) kann der Automat gesteuert werden:
? Hilfe Alle Kommandos werden angezeigt
p Pause Der Player bekommt Pause-Befehl
s Stopp Der Player bekommt Stopp-Befehl
a1,1 Audiotest mit Ausgabe auf Serieller Plotter aus Verzeichnis 1, Stück 1
v1,1 Spiele aus Verzeichnis 1, Stück 1 a0,1 würde aus mp3-Verzeichnis spielen
j Servojustage
Rauschender DFPlayerMini
Wenn ein Stück mit Pause-Befehl gestoppt wird zeigt die Ausgabe einen MinLevel von ca. 8..13 an. Läuft das Stück regulär zu Ende oder wird mit dem Stopp-Befehl gestoppt ist MinLevel 0. Bei Pause wird der DA-Wandler nicht abgeschaltet und sein Rauschwert, bzw. der Rauschwert der Aufnahme wird ausgegeben. Sonst wird der DA-Wandler abgeschaltet und dabei wird MinLevel korrekterweise 0. Zum Testen wie stark eine Aufnahme zusätzlich zum DA-Wandler rauscht während der Wiedergabe den Pause-Befehl senden. Beim Abspielen oder Audiotesten wird der Test ob der Player noch spielt und die Lautstärkeeinstellung durch die Potiposition nur jede Sekunde durchgeführt und die Sampling-Rate durchgängig hoch zu halten.
// Dinodialogomat V0.5 © 29.12.2024 Oliver Mezger MezMedia.de CC BY-SA 4.0
#include <SoftwareSerial.h>
#include <DFPlayerMini_Fast.h>
#include <EEPROM.h>
#include "SchwaetzoLib.h"
//#define DEBUG_ME 1 // Zum Messen der Ausführungsdauer von Schwaetzo:go()
#define TRIGGER 12 // Ultraschall: Digitalpin 12 zum Auslösen einer Entfernungsmessung
#define ECHO 13 // Ultraschall: Pin 13 empfängt den Messimpuls
#define BUSY 2 // MP3-Player: Busy-Pin, low aktiv
#define POTI A2 // Pin des Potentiometers
#define TASTER 3 // Taster gegen GND zu Justieren
#define NAHE_ENTFERNUNG 60 // beim Unterschreiten ist Person nahe
#define EEPROM_ERKENNUNG 42 // falls schon programmiert dann hat EEPROM Adresse 0 diesen Wert
SoftwareSerial mySerial(10, 11); // RX, TX, fuer DF-Player
DFPlayerMini_Fast myMP3;
// Endpositionen der Servos für mundZu und mundAuf kennen z.B. 1800µs -> 180 eintragen
//Schwaetzo schwaetzoL(A1,9,180,140); // AudioIn, ServoOut, mundZu, mundOffen
//Schwaetzo schwaetzoR(A0,8,170,130);
Schwaetzo schwaetzoL(A1,9); // AudioIn, ServoOut, noch unjustiert
Schwaetzo schwaetzoR(A0,8);
enum zustandstyp {RUHE_ENTRY,RUHE,NAHE,SPIELEN,AUDIO_TEST,J_ANFANG,J_SERVO_ABGEZOGEN,J_SERVO_AUFSETZEN,J_POTI_MITTE,J_LINKS_ZU,J_LINKS_AUF,J_POTI_MITTE2,J_RECHTS_ZU,J_RECHTS_AUF}; // definiere Aufzählungstyp
enum zustandstyp zustand = RUHE_ENTRY; // Definiere und initialisiere Variable
byte lautstaerke = 20; // 0..30
byte stueck=1; // das zu spielende Stueck
void einstellenLautstaerke(){ // Poti einlesen und MP3 Lautstärke einstellen
unsigned int l = 20+analogRead(POTI)/100; // POTI einlesen und 0..1023 auf 20..30 umsetzen
if (l>30) l=30;
if (l!=lautstaerke){
lautstaerke=l;
Serial.print(F("Neue Lautstaerke: "));
Serial.println(l);
myMP3.volume(l);
}
}
void setup() {
byte eeAdr = 0; // EEPROM-Adresse
Serial.begin(115200); // Ausgabe ueber serieller Monitor
mySerial.begin(9600); // Anbindung des DF-Player
myMP3.begin(mySerial); // gibt immer true aus
analogReference(INTERNAL); // A/D Referenzspannung https://www.arduino.cc/reference/en/language/functions/analog-io/analogreference/
einstellenLautstaerke();
delay(1000);
pinMode(TRIGGER, OUTPUT); // Trigger-Pin ist ein Ausgang
pinMode(ECHO, INPUT); // Echo-Pin ist ein Eingang
pinMode(BUSY,INPUT); // Abfragen ob MP3-Player spielt
pinMode(TASTER,INPUT_PULLUP);
Serial.print(F("Anzahl Ordner auf SD-Karte: "));
Serial.println(myMP3.numFolders());
if (!digitalRead(TASTER)){ // wenn Taster gedrückt nach Reset
zustand = J_ANFANG;
//gedruecktTaster();
}
if (EEPROM.read(0)==EEPROM_ERKENNUNG){ // falls EEPROM Servodaten hat
Serial.print(F("EEPROM hat Servodaten"));
Serial.print(F("EEPROM: "));
for (eeAdr=0;eeAdr<6;eeAdr++){ // alle Daten ausgeben
Serial.print(EEPROM.read(eeAdr));
Serial.print(F(" "));
}
Serial.println();
schwaetzoL.leseEEPROM(); // Servodaten lesen
schwaetzoR.leseEEPROM();
} else {
Serial.print(F("EEPROM ist Jungfrau"));
}
}
unsigned int messeEntfernung(){ // Messung ob jemand vor dem Automat steht Rückgabe in cm
static unsigned int entfernung=0; // persistente lokale Variable für Entfernung in cm
unsigned int mess;
digitalWrite(TRIGGER, HIGH); // Trigger-Pin high zum Start der Messung
delayMicroseconds(10); // 10 µs reichen
digitalWrite(TRIGGER, LOW); // Trigger-Pin low
mess = pulseIn(ECHO, HIGH,12000); // auf 12ms begrenzen für max 206cm
mess = mess/58; // näherungsweise Berechnung der cm
if (mess==0||mess>200){
entfernung = 200;
}
else{
entfernung = (entfernung+mess)/2; // Messungen glätten
}
//Serial.println(entfernung);
return entfernung;
}
unsigned long weckzeit; // wann der Wecker "klingeln" soll
void stelleWecker(unsigned int n){ // n ist Zeit in ms
weckzeit = millis()+n;
}
bool abgelaufenWecker(){ // true, wenn Zeit abgelaufen ist
return millis()>=weckzeit;
}
bool gedruecktTaster(){ // true wenn Taster gedrückt wurde 1->0, entprellt
static byte oldTaster=0;
bool ausgabe = false;
byte test = digitalRead(TASTER);
if (oldTaster != test){ // hat sich was getan?
delay(10); // 10 ms warten
test=digitalRead(TASTER); // noch mal einlesen
if (oldTaster != test){ // immer noch anders?
ausgabe = oldTaster & !test; // fallende Flanke
oldTaster = test;
}
}
return ausgabe;
}
#ifdef DEBUG_ME
unsigned long maxSchwaetzoTime = 0;
unsigned long tmp=0;
#endif
void starteSpiel(){
schwaetzoL.an(); // Servosignal anschalten
schwaetzoR.an();
#ifdef DEBUG_ME
maxSchwaetzoTime = 0;
#endif
stelleWecker(1000); // 1 Sekunde
}
byte lesePoti(){ // liest Potentiometer und setzt auf Servowerte um
unsigned int p = analogRead(POTI); // POTI einlesen und 0..1023 auf 20..30 umsetzen
return map(p,0,1023,100,200); // map(value, fromLow, fromHigh, toLow, toHigh)
}
String serIn; // für Serielle Eingabe
void loop() {
static unsigned char naheZyklen=0;
static byte i;
switch (zustand){
case RUHE_ENTRY:
stelleWecker(400); // 0,4 Sekunden
schwaetzoL.aus();
schwaetzoR.aus();
zustand=RUHE;
break;
case RUHE: // Warten auf Person, alle 0,4s Entfernung messen
if (abgelaufenWecker()){ // wenn Zeit um ist
if(messeEntfernung()<NAHE_ENTFERNUNG){
zustand=NAHE;
naheZyklen=0;
}
stelleWecker(400); // 0,4s
}
break;
case NAHE:
if (abgelaufenWecker()){
if(messeEntfernung()<NAHE_ENTFERNUNG){
naheZyklen++;
}
else naheZyklen=0;
stelleWecker(400);
}
if(naheZyklen>3){ // Person steht vor Automat
starteSpiel();
Serial.print(F("Spiele Stueck: "));
Serial.print(stueck);
myMP3.playFromMP3Folder(stueck);
delay(700);
i = myMP3.numTracksInFolder(0); // folder wird nicht beachtet, gibt Anzahl aus aktuellem Verzeichnis
if (i>254){ // falls keine brauchbare Antwort
delay(500); // warten..
Serial.print(F(", "));
i = myMP3.numTracksInFolder(0); // nochmal versuchen
}
Serial.print(F(", Stücke im Verzeichnis: "));
Serial.println(i);
stueck++;
if (stueck>i)stueck=1; // wenn alle Stücke durch sind wieder das erste Stück
zustand=SPIELEN;
}
break;
case SPIELEN:
#ifdef DEBUG_ME
tmp = micros(); // Microsekunden
#endif
schwaetzoL.go(); // Audio messen und auf Servo ausgeben
schwaetzoR.go();
#ifdef DEBUG_ME
tmp = micros()-tmp; // Zeitdifferenz
if (tmp>maxSchwaetzoTime) maxSchwaetzoTime=tmp;
#endif
if(abgelaufenWecker()){ // wenn Zeit um ist mal nach dem Rechten sehen
/*if(!myMP3.isPlaying()){ // Stück zu ende? Braucht 64ms für Abfrage
zustand=RUHE_ENTRY;
}*/
if(digitalRead(BUSY)){ // MP3 spielt? Low aktiv
zustand=RUHE_ENTRY;
#ifdef DEBUG_ME
Serial.print(F("MaxSchwätzoTime: "));
Serial.println(maxSchwaetzoTime);
#endif
}
stelleWecker(1000); // 1 Sekunde
einstellenLautstaerke();
}
break;
case AUDIO_TEST: // Schwätzofunktion visualisieren mit Seriellem Plotter
schwaetzoL.go(); // Audio messen und auf Servo ausgeben
schwaetzoR.go();
if(schwaetzoL.getLastAudio()){ // wenn Audiosamples ausgewertet
Serial.print("Lmax:");
Serial.print(schwaetzoL.getMaxAudio());
Serial.print(",");
Serial.print("L:");
Serial.print(schwaetzoL.getLastAudio());
Serial.print(",");
Serial.print("Rmax:");
Serial.print(schwaetzoR.getMaxAudio());
Serial.print(",");
Serial.print("R:");
Serial.println(schwaetzoR.getLastAudio());
}
if(abgelaufenWecker()){
if(digitalRead(BUSY)){ // MP3 spielt? Low aktiv
zustand=RUHE_ENTRY;
}
stelleWecker(1000); // 1 Sekunde
einstellenLautstaerke();
}
break;
case J_ANFANG:
Serial.println(F(", bitte bei Erstjustage alle Servohebel von den Achsen abziehen! Dann Taste drücken."));
zustand = J_SERVO_ABGEZOGEN;
break;
case J_SERVO_ABGEZOGEN:
if(gedruecktTaster()){
Serial.println(F("Beide Servohebel so aufsetzen, dass der jeweilige Mund halb geöffnet ist."));
schwaetzoL.an(); // Servosignal anschalten
schwaetzoR.an();
schwaetzoL.moveServo(150); // Servo auf Mitte stellen
schwaetzoR.moveServo(150); // Servo auf Mitte stellen
zustand = J_SERVO_AUFSETZEN;
}
break;
case J_SERVO_AUFSETZEN:
if(gedruecktTaster()){
Serial.println(F("Bitte Poti in die Mitte stellen. Dann Taste drücken."));
zustand = J_POTI_MITTE;
}
break;
case J_POTI_MITTE:
if(gedruecktTaster()){
i = lesePoti();
if (i<140||i>160){
Serial.print(F("Poti steht nicht in der Mitte: "));
Serial.println(i);
} else {
Serial.println(F("Linken Servo in MundZu Position einstellen. Dann Taste drücken."));
zustand = J_LINKS_ZU;
}
}
break;
case J_LINKS_ZU:
i = lesePoti();
schwaetzoL.moveServo(i);
if(gedruecktTaster()){
Serial.print(F("ServoL zu: "));
Serial.println(i);
Serial.println(F("Linken Servo in MundOffen Position einstellen. Dann Taste drücken."));
schwaetzoL.setMundZu(i);
zustand = J_LINKS_AUF;
}
break;
case J_LINKS_AUF:
i = lesePoti();
schwaetzoL.moveServo(i);
if(gedruecktTaster()){
Serial.print(F("ServoL auf: "));
Serial.println(i);
Serial.println(F("Bitte Poti in die Mitte stellen. Dann Taste drücken."));
EEPROM.update(0,42);
schwaetzoL.setMundOffen(i);
schwaetzoL.schreibeEEPROM();
zustand = J_POTI_MITTE2;
}
break;
case J_POTI_MITTE2:
if(gedruecktTaster()){
i = lesePoti();
if (i<140||i>160){
Serial.print(F("Poti steht nicht in der Mitte: "));
Serial.println(i);
} else {
Serial.println(F("Rechten Servo in MundZu Position einstellen. Dann Taste drücken."));
zustand = J_RECHTS_ZU;
}
}
break;
case J_RECHTS_ZU:
i = lesePoti();
schwaetzoR.moveServo(i);
if(gedruecktTaster()){
Serial.print(F("ServoR zu: "));
Serial.println(i);
Serial.println(F("Rechten Servo in MundOffen Position einstellen. Dann Taste drücken."));
schwaetzoR.setMundZu(i);
zustand = J_RECHTS_AUF;
}
break;
case J_RECHTS_AUF:
i = lesePoti();
schwaetzoR.moveServo(i);
if(gedruecktTaster()){
Serial.print(F("ServoR auf: "));
Serial.println(i);
Serial.println(F("Servos justiert."));
schwaetzoR.setMundOffen(i);
schwaetzoR.schreibeEEPROM();
zustand = RUHE_ENTRY;
}
break;
}
if(Serial.available()){ // wenn Zeichen über Seriell Monitor gesendet wurden
serIn = Serial.readStringUntil('\n'); // lese Daten in String ein
Serial.print(serIn); // gib String aus
switch (serIn[0]){
case '?':
Serial.println(F(" Hilfe"));
Serial.println(F("p Pause"));
Serial.println(F("s Stopp"));
Serial.println(F("a1,1 Audiotest mit Ausgabe auf Serieller Plotter aus Verzeichnis 1, Stück 1"));
Serial.println(F("v1,1 Spiele aus Verzeichnis 1, Stück 1"));
Serial.println(F("j Servojustage"));
break;
case 'p': // Pause
Serial.println(F(" Pause"));
myMP3.pause();
break;
case 's': // Stopp
Serial.println(F(" Stopp"));
myMP3.stop();
break;
case 'j':
Serial.print(F(" Servojustage"));
zustand = J_ANFANG;
break;
case 'a': // Audiotest
case 'v': // playFolderTrack
byte folder=0,track=0;
i=1;
while (i<serIn.length() && (serIn[i]<'0'||serIn[i]>'9')) i++; // Nichtdigits überspringen
while (i<serIn.length() && serIn[i]>='0'&& serIn[i]<='9'){ // Digits einlesen
folder = folder*10+ (serIn[i]-'0');
i++;
}
while (i<serIn.length() && (serIn[i]<'0'||serIn[i]>'9')) i++; // Nichtdigits überspringen
while (i<serIn.length() && serIn[i]>='0'&&serIn[i]<='9'){ // Digits einlesen
track = track*10+ (serIn[i]-'0');
i++;
}
if(folder>=0 && track>0){
Serial.print(F("Spiele aus Verzeichnis "));
Serial.print(folder);
Serial.print(F(" Stück "));
Serial.print(track);
if(folder==0) myMP3.playFromMP3Folder(track); // MP3-Verzeichnis
else myMP3.playFolder(folder,track);
delay(1000); // warten bis verarbeitet
i = myMP3.numTracksInFolder(folder); // folder wird nicht beachtet, gibt Anzahl aus aktuellem Verzeichnis
if (i>254){ // falls keine brauchbare Antwort
delay(500); // warten..
Serial.print(F(", "));
i = myMP3.numTracksInFolder(folder); // nochmal versuchen
}
Serial.print(F(", Stücke im Verzeichnis: "));
Serial.println(i);
if(serIn[0]=='a') zustand = AUDIO_TEST;
else zustand = SPIELEN;
starteSpiel();
} else {
Serial.print(F("Fehler beim Parsen: "));
Serial.println(serIn);
}
break;
}
}
}