Esp32-sensors-and-servos

From 3c2mWiki
Revision as of 11:19, 26 August 2025 by Cklas (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Arduino Code for ESP32 servo control and sensor reading via website and websocket

/*
  ESP32 Servo Steuerung über Weboberfläche und Websocket

  Dieser Sketch realisiert eine drahtlose Steuerung von RC-Servos und mehr über WLAN.
  Der ESP32 fungiert dabei sowohl als Access Point als auch als WLAN-Client
  und stellt eine einfache Weboberfläche zur Verfügung.

  Hauptfunktionen:
  - Steuerung von 9 Servos über Slider in der Weboberfläche
  - 3 LEDS über Slider in der Weboberfläche
  - Umschaltbar auf Poti-Steuerung (ein Poti kontrolliert alle Servos + LEDs)
  - Einstellbarer Low-Pass-Filter für sanfte Bewegungen
  - Auslesen und Anzeigen von:
    * 4 Potentiometern (analog)
    * 3 Touch-Pins (kapazitiv)
    * 1 Schalter (mit internem Pull-Up)
  - Soundausgabe über einen Piezo-Buzzer mit konfigurierbarer Tonfolge:
    * Eingabeformat über Web: "frequenz,lautstärke,dauer;..."
    * Beispiel: "1000,80,200;800,60,300;"
    * Nicht-blockierende Wiedergabe im loop()
  - Schalter löst (bei aktivierter Poti-Steuerung) automatisches Abspielen    einer vorgegebenen Tonfolge aus
  - Speicherung von WLAN-Zugangsdaten im EEPROM
  - Verbindung zu bekanntem WLAN + fallback Access Point
  - Anzeige von WLAN-Status, IP-Adresse und ESP-ID
  - WebSocket-Kommunikation für sofortige Reaktion auf Nutzerinteraktionen
  - MDNS-Unterstützung: Zugriff über http://esp-xxxx.local möglich

  - Beim Start und bei poti=true Verknüpfung der eingabe mit leds und servos
    - 3 potis mit 3 servos
    - 2 touch mit 2 servos
    - 1 touch mit 3 leds
    - schalter mit sound (x)


  Hinweise:
  - PWM-Ausgabe für Buzzer erfolgt für espressif systems version 3.x via 
    ledcAttach(buzzerPin, 1000, BUZZER_RES); // // Ton-Setup mit neuer API, Start mit 1 kHz, 8 Bit
    ledcWrite(buzzerPin, vol);  // Lautstärke (Duty Cycle)
    ledcChangeFrequency(buzzerPin, freq, BUZZER_RES);    

 ToDo:
 - schieberegler stimmen nicht wenn "alle servos 90" gedrückt wird und wenn sie extern betätigt werden
 
*/


#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <ESP32Servo.h>
#include <Preferences.h>
#include <ESPmDNS.h>

const int servoPins[] = {23, 22, 21, 19, 18, 5}; //, 17, 16, 4};
const int potiPins[] = {36, 39, 34, 35};
const int schalterPins[] = {25};
const int touchPins[] = {32, 33, 27};
const int ledPins[] = {14, 12, 13};
const int buzzerPin = 26;

#define BUZZER_RES 8
#define BUZZER_DUTY 128 // 50% bei 8 Bit
#define BUZZER_CHANNEL 7

#define NUM_SERVOS (sizeof(servoPins) / sizeof(servoPins[0]))
#define NUM_POTIS (sizeof(potiPins) / sizeof(potiPins[0]))
#define NUM_SCHALTER (sizeof(schalterPins) / sizeof(schalterPins[0]))
#define NUM_TOUCH (sizeof(touchPins) / sizeof(touchPins[0]))
#define NUM_LEDS (sizeof(ledPins) / sizeof(ledPins[0]))

Servo servos[NUM_SERVOS];
int servoTargets[NUM_SERVOS] = {90};
float currentAngles[NUM_SERVOS] = {90};
int potiValues[NUM_POTIS];
int schalterValues[NUM_SCHALTER];
int touchValues[NUM_TOUCH];
bool ledStates[NUM_LEDS] = {false};
String soundSequence = "";
bool playSound = false;

bool potiControl = true;
float filter = 0.9;

AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
Preferences preferences;

String chipID = "";
String currentSSID = "";
String currentPASS = "";
bool wifiConnected = false;
IPAddress wifiIP;

unsigned long nextToneTime = 0;
int toneIndex = 0;

void generateChipID() {
  uint64_t mac = ESP.getEfuseMac();
  chipID = String((uint32_t)(mac >> 32), HEX) + String((uint32_t)(mac & 0xFFFFFFFF), HEX);
  chipID.toUpperCase();
}

void saveWiFiCredentials(const String &ssid, const String &pass) {
  preferences.begin("wifi", false);
  preferences.putString("ssid", ssid);
  preferences.putString("pass", pass);
  preferences.end();
}

bool loadWiFiCredentials(String &ssid, String &pass) {
  preferences.begin("wifi", true);
  ssid = preferences.getString("ssid", "");
  pass = preferences.getString("pass", "");
  preferences.end();
  return ssid.length() > 0;
}

void tryConnectToWiFi() {
  if (!loadWiFiCredentials(currentSSID, currentPASS)) return;
  WiFi.begin(currentSSID.c_str(), currentPASS.c_str());
  Serial.print("Verbinde mit WLAN: ");
  Serial.println(currentSSID);
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 8000) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();

  if (WiFi.status() == WL_CONNECTED) {
    wifiConnected = true;
    wifiIP = WiFi.localIP();
    Serial.println("Verbunden! IP: " + wifiIP.toString());
  } else {
    Serial.println("Verbindung fehlgeschlagen.");
  }
}

void setupDualWiFi() {
  generateChipID();
  String apName = "ESP32_Servo_" + chipID.substring(chipID.length() - 4);

  WiFi.mode(WIFI_AP_STA);
  WiFi.softAP(apName.c_str());
  Serial.println("Access Point gestartet: " + WiFi.softAPIP().toString());

  tryConnectToWiFi();

  if (wifiConnected) {
    String hostName = "esp-" + chipID.substring(chipID.length() - 4);
    if (MDNS.begin(hostName.c_str())) {
      Serial.println("mDNS aktiv unter: http://" + hostName + ".local");
    } else {
      Serial.println("mDNS konnte nicht gestartet werden");
    }
  }
}

void updateToneSequence() {
  if (!playSound || millis() < nextToneTime) return;

  if (toneIndex >= soundSequence.length()) {
    //ledcWrite(buzzerPin, 0);
    ledcWriteChannel(BUZZER_CHANNEL,0);
    playSound = false;
    toneIndex = 0;
    return;
  }

  int sep1 = soundSequence.indexOf(',', toneIndex);
  int sep2 = soundSequence.indexOf(',', sep1 + 1);
  int sep3 = soundSequence.indexOf(';', sep2 + 1);

  if (sep1 == -1 || sep2 == -1) {
    playSound = false;
    return;
  }

  int freq = soundSequence.substring(toneIndex, sep1).toInt();
  int vol = soundSequence.substring(sep1 + 1, sep2).toInt();
  int dur = soundSequence.substring(sep2 + 1, sep3 == -1 ? soundSequence.length() : sep3).toInt();

  ledcChangeFrequency(buzzerPin, freq, BUZZER_RES);
  //ledcWrite(buzzerPin, vol);
  ledcWriteChannel(BUZZER_CHANNEL,vol);

  nextToneTime = millis() + dur;
  toneIndex = (sep3 == -1) ? soundSequence.length() : sep3 + 1;
}

void handleSensoren(AsyncWebServerRequest *request){
    String json = "{";
    
    // Potis
    for (int i = 0; i < NUM_POTIS; i++) {
        json += "\"poti" + String(i) + "\":" + String(potiValues[i]);
        if (i < NUM_POTIS-1 || NUM_TOUCH > 0 || NUM_SCHALTER > 0) json += ",";
    }

    // Touch
    for (int i = 0; i < NUM_TOUCH; i++) {
        json += "\"touch" + String(i) + "\":" + String(touchValues[i]);
        if (i < NUM_TOUCH-1 || NUM_SCHALTER > 0) json += ",";
    }

    // Schalter
    for (int i = 0; i < NUM_SCHALTER; i++) {
        json += "\"schalter" + String(i) + "\":" + String(schalterValues[i]);
        if (i < NUM_SCHALTER-1) json += ",";
    }

    json += "}";
    request->send(200, "application/json", json);
}


void setup() {
  Serial.begin(115200);

  //sound laden
  preferences.begin("sound", true);
  soundSequence = preferences.getString("sequence", "");
  preferences.end();
  
  // beim ersten start leer, das soll nicht sein:
  if (soundSequence == "") { soundSequence = "440,100,300;0,0,100;660,100,300;";  }

  for (int i = 0; i < NUM_SERVOS; i++) {
    servos[i].attach(servoPins[i]);
    servos[i].write(servoTargets[i]);
    currentAngles[i] = servoTargets[i];
  }

  for (int i = 0; i < NUM_SCHALTER; i++) {
    pinMode(schalterPins[i], INPUT_PULLUP);
  }

  for (int i = 0; i < NUM_LEDS; i++) {
    pinMode(ledPins[i], OUTPUT);
    digitalWrite(ledPins[i], LOW);
  }

  //ledcAttach(buzzerPin, 1000, BUZZER_RES);
  ledcAttachChannel(buzzerPin, 1000, BUZZER_RES,BUZZER_CHANNEL); //direkt auf anderem kanal um konflikte mit servos zu vermeiden
  //ledcWrite(buzzerPin, 0);
  ledcWriteChannel(BUZZER_CHANNEL,0);

  setupDualWiFi();
  
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>ESP32</title></head><body>";
  html += "<h1>ESP32 Steuerung</h1>";
  html += "<p><strong>IP:</strong> " + wifiIP.toString() + "</p>";
  html += "<p><strong>Name:</strong> esp-" + chipID.substring(chipID.length() - 4) + ".local</p>";
  html += "<p><a href='/wlan'>WLAN-Einstellungen</a></p>";

  html += "<label><input type='checkbox' id='potiToggle' onchange='togglePoti(this.checked)'> Poti-Steuerung aktivieren</label><br>";
  html += "Filter: <input type='range' min='0' max='1' step='0.01' value='0.9' id='filterSlider' oninput='sendFilter(this.value)'><br><br>";

  html += "<h2>Servo Steuerung</h2>";
  for (int i = 0; i < NUM_SERVOS; i++) {
    html += "<label>Servo " + String(i) + " (Pin " + String(servoPins[i]) + "):</label> ";
    html += "<input type='range' min='0' max='180' value='" + String(servoTargets[i]) +
            "' id='servo" + i + "' oninput='send(this)'><br>";
  }
  html += "<p></p>";
  html += "<button onclick='setAllServos90()'>Alle Servos auf 90°</button><br><br>";

  html += "<h2>LED Steuerung</h2>";
  for (int i = 0; i < NUM_LEDS; i++) {
    html += "<label>LED " + String(i) + " (Pin " + String(ledPins[i]) + "):</label> ";
    html += "<input type='range' min='0' max='1' value='" + String(ledStates[i] ? 1 : 0) +
            "' id='led" + i + "' oninput='sendLed(this)'><br>";
  }

  html += "<h2>Tonfolge</h2>";
  html += "<form action='/sound' method='get'>Tonfolge (freq,vol,dur;...): Beispiel 1100,80,200;1800,80,200;1800,80,200; <br>";
  html += "<input name='seq' size='60'><br><input type='submit' value='Abspielen'></form>";

  html += "<h2>Sensorwerte</h2><ul>";
  html += "<p><a href='/sensoren'>Sensorwerte</a></p>";
  
  html += "</ul>";

  html += "<script>";
  html += "const numServos = " + String(NUM_SERVOS) + ";";
  html += "var socket = new WebSocket('ws://' + location.host + '/ws');";
  html += R"rawliteral(
  function send(el) {
    let id = el.id.replace("servo", "");
    socket.send("servo:" + id + ":" + el.value);
  }
  function sendLed(el) {
    let id = el.id.replace("led", "");
    socket.send("led:" + id + ":" + el.value);
  }
  function togglePoti(state) {
    socket.send("poti:" + (state ? "on" : "off"));
  }
  function sendFilter(val) {
    socket.send("filter:" + val);
  }
  function setAllServos90() {
    if(socket && socket.readyState === WebSocket.OPEN) {
      for (let i = 0; i < numServos; i++) {
        socket.send(`servo:${i}:90`);
      }
    }
  }
  </script>
  )rawliteral";

  html += "</body></html>";
  request->send(200, "text/html", html);
});

server.on("/wlan", HTTP_GET, [](AsyncWebServerRequest *request) {
  request->send(200, "text/html", R"rawliteral(
    <h2>WLAN verbinden</h2>
    <form action="/join" method="get">
      SSID: <input name="ssid"><br>
      Passwort: <input name="pass" type="password"><br>
      <input type="submit" value="Verbinden">
    </form>
  )rawliteral");
});

server.on("/join", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (request->hasParam("ssid") && request->hasParam("pass")) {
    String ssid = request->getParam("ssid")->value();
    String pass = request->getParam("pass")->value();
    saveWiFiCredentials(ssid, pass);
    request->send(200, "text/html", "<p>WLAN gespeichert. Starte neu...</p>");
    delay(1000);
    ESP.restart();
  } else {
    request->send(400, "text/plain", "Fehlende Parameter");
  }
});

server.on("/sound", HTTP_GET, [](AsyncWebServerRequest *request) {
  if (request->hasParam("seq")) {
    soundSequence = request->getParam("seq")->value();
    playSound = true;
    toneIndex = 0;

    //sequenz speichern
    preferences.begin("sound", false);
    preferences.putString("sequence", soundSequence);
    preferences.end();
    
    request->send(200, "text/html", "<p>Tonfolge wird abgespielt. <a href='/'>Zurück</a></p>");
  } else {
    request->send(400, "text/plain", "Fehlender Parameter");
  }
});

server.on("/sensoren", HTTP_GET, handleSensoren);

server.on("/test", HTTP_GET, [](AsyncWebServerRequest *request) {
  request->send(200, "text/plain", "Test OK");
});

ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  if (type == WS_EVT_CONNECT) {
      Serial.println("WebSocket verbunden → Poti aus");
      potiControl = false;
  }              
  else if (type == WS_EVT_DATA) {
    AwsFrameInfo *info = (AwsFrameInfo *)arg;
    if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
      String msg = "";
      for (size_t i = 0; i < len; i++) msg += (char)data[i];

      if (msg == "poti:on") potiControl = true;
      else if (msg == "poti:off") potiControl = false;
      else if (msg.startsWith("filter:")) {
        float f = msg.substring(7).toFloat();
        if (f >= 0 && f <= 1) filter = f;
      } else if (msg.startsWith("servo:")) {
        int idx = msg.substring(6, msg.indexOf(':', 6)).toInt();
        int val = msg.substring(msg.lastIndexOf(':') + 1).toInt();
        if (idx >= 0 && idx < NUM_SERVOS) {
          servoTargets[idx] = constrain(val, 0, 180);
        }
      } else if (msg.startsWith("led:")) {
        int idx = msg.substring(4, msg.indexOf(':', 4)).toInt();
        int val = msg.substring(msg.lastIndexOf(':') + 1).toInt();
        if (idx >= 0 && idx < NUM_LEDS) {
          ledStates[idx] = val > 0;
        }
      } else if (msg.startsWith("sound:")) {
          // NEU: Befehl für die Tonfolge verarbeiten
          soundSequence = msg.substring(6); // Schneidet "sound:" vom String ab
          playSound = true;
          toneIndex = 0;
          Serial.println("Sound-Befehl von TurboWarp erhalten."); 
      }
    }
  }
});


server.addHandler(&ws);

// CORS-Header senden, um die Browser-Verbindung zu erlauben FÜR ONLINE SCRATCH?. To Test!
//client->text("Access-Control-Allow-Origin: *");

server.begin();

}

void loop() {
  ws.cleanupClients();

  for (int i = 0; i < NUM_POTIS; i++) {
    potiValues[i] = analogRead(potiPins[i]);
  }
  for (int i = 0; i < NUM_TOUCH; i++) {
    touchValues[i] = touchRead(touchPins[i]);
  }
  for (int i = 0; i < NUM_SCHALTER; i++) {
    schalterValues[i] = digitalRead(schalterPins[i]);
  }
  
  // Sensorwerte über WebSocket an den Client senden
  // Sende alle 30 Millisekunden
  static unsigned long lastSendTime = 0;
    const long interval = 50;
    if (millis() - lastSendTime > interval) {
        lastSendTime = millis();
        String json = "{";
        
        // Potis
        for (int i = 0; i < NUM_POTIS; i++) {
            json += "\"poti" + String(i) + "\":" + String(potiValues[i]);
            if (i < NUM_POTIS - 1 || NUM_TOUCH > 0 || NUM_SCHALTER > 0) json += ",";
        }

        // Touch
        for (int i = 0; i < NUM_TOUCH; i++) {
            json += "\"touch" + String(i) + "\":" + String(touchValues[i]);
            if (i < NUM_TOUCH - 1 || NUM_SCHALTER > 0) json += ",";
        }

        // Schalter
        for (int i = 0; i < NUM_SCHALTER; i++) {
            json += "\"schalter" + String(i) + "\":" + String(schalterValues[i]);
            if (i < NUM_SCHALTER - 1) json += ",";
        }

        json += "}";
        
        ws.textAll(json);
    }

  if (potiControl) {
      // Potis 0–3 → Servos 0–3
      for (int i = 0; i < 4 && i < NUM_POTIS && i < NUM_SERVOS; i++) {
        int mapped = map(potiValues[i], 0, 4095, 0, 180);
        servoTargets[i] = mapped;
      }
    
      // Touch → Servos 4 und 5
      int threshold = 40;
    
      // Touch 0 → Servo 4
      if (NUM_TOUCH > 0 && NUM_SERVOS > 4) {
        servoTargets[4] = (touchValues[0] > threshold) ? 0 : 90;
      }
    
      // Touch 1 → Servo 5
      if (NUM_TOUCH > 1 && NUM_SERVOS > 5) {
        servoTargets[5] = (touchValues[1] > threshold) ? 90 : 0;
      }
    
      // Touch 2 → LED 0
      if (NUM_TOUCH > 2 && NUM_LEDS > 0) {
        ledStates[0] = (touchValues[2] > threshold);
        ledStates[1]=ledStates[0];
        ledStates[2]=ledStates[0];
      }
    
      // Restliche Servos kopieren Servo 0 (falls mehr als 6 vorhanden)
      for (int i = 6; i < NUM_SERVOS; i++) {
        servoTargets[i] = servoTargets[0];
      }
    
      // Wenn Schalter gedrückt → Sound abspielen
      if (schalterValues[0] == LOW && !playSound) {
        
        //soundSequence = "1000,100,200;1200,80,200;800,50,300;";
        //astronomia soundSequence = "146.83, 79, 112; 0.0, 0, 188; 146.83, 79, 112; 0.0, 0, 38; 220.0, 79, 112; 0.0, 0, 38; 196.0, 79, 112; 0.0, 0, 188; 174.61, 79, 112; 0.0, 0, 188; 164.81, 79, 112; 0.0, 0, 188; 164.81, 79, 112; 0.0, 0, 38; 174.61, 79, 112; 0.0, 0, 38; 196.0, 79, 112; 0.0, 0, 188; 174.61, 79, 112; 0.0, 0, 38; 164.81, 79, 112; 0.0, 0, 38; 146.83, 79, 112; 0.0, 0, 188; 146.83, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 329.63, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 329.63, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 146.83, 79, 112; 0.0, 0, 188; 146.83, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 329.63, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 329.63, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 146.83, 79, 112; 0.0, 0, 188; 146.83, 79, 112; 0.0, 0, 38; 220.0, 79, 112; 0.0, 0, 38; 196.0, 79, 112; 0.0, 0, 188; 174.61, 79, 112; 0.0, 0, 188; 164.81, 79, 112; 0.0, 0, 188; 164.81, 79, 112; 0.0, 0, 38; 174.61, 79, 112; 0.0, 0, 38; 196.0, 79, 112; 0.0, 0, 188; 174.61, 79, 112; 0.0, 0, 38; 164.81, 79, 112; 0.0, 0, 38; 146.83, 79, 112; 0.0, 0, 188; 146.83, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 329.63, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 329.63, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 146.83, 79, 112; 0.0, 0, 188; 146.83, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 329.63, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 329.63, 79, 112; 0.0, 0, 38; 349.23, 79, 112; 0.0, 0, 38; 174.61, 79, 112; 0.0, 0, 38; 174.61, 79, 56; 0.0, 0, 94; 174.61, 79, 112; 0.0, 0, 38; 174.61, 79, 56; 0.0, 0, 94; 220.0, 79, 112; 0.0, 0, 38; 220.0, 79, 56; 0.0, 0, 94; 220.0, 79, 131; 0.0, 0, 19; 220.0, 79, 56; 0.0, 0, 94; 196.0, 79, 112; 0.0, 0, 38; 196.0, 79, 56; 0.0, 0, 94; 196.0, 79, 112; 0.0, 0, 38; 196.0, 79, 56; 0.0, 0, 94; 261.63, 79, 112; 0.0, 0, 38; 261.63, 79, 56; 0.0, 0, 94; 261.63, 79, 112; 0.0, 0, 38; 261.63, 79, 56; 0.0, 0, 94; 293.66, 79, 112; 0.0, 0, 38; 293.66, 79, 56; 0.0, 0, 94; 293.66, 79, 112; 0.0, 0, 38; 293.66, 79, 56; 0.0, 0, 94; 293.66, 79, 112; 0.0, 0, 38; 293.66, 79, 56; 0.0, 0, 94; 293.66, 79, 112; 0.0, 0, 38; 293.66, 79, 56; 0.0, 0, 94; 293.66, 79, 112; 0.0, 0, 38; 293.66, 79, 56; 0.0, 0, 94; 293.66, 79, 112; 0.0, 0, 38; 293.66, 79, 56; 0.0, 0, 94; 196.0, 79, 112; 0.0, 0, 38; 174.61, 79, 112; 0.0, 0, 38; 164.81, 79, 112; 0.0, 0, 38; 130.81, 79, 112;";
        playSound = true;
        toneIndex = 0;
      }
    }

  for (int i = 0; i < NUM_SERVOS; i++) {
    currentAngles[i] = filter * currentAngles[i] + (1.0 - filter) * servoTargets[i];
    servos[i].write((int)currentAngles[i]);
  }

  for (int i = 0; i < NUM_LEDS; i++) {
    digitalWrite(ledPins[i], ledStates[i]);
  }

  updateToneSequence();

  delay(10);
}