#include <WiFi.h> #include <WebServer.h> // Motor and encoder pins const int in1Pin = 33; const int in2Pin = 27; const int enAPin = 32; const int encoderPinA = 34; const int encoderPinB = 35; volatile int pulseCount = 0; int lastEncoded = 0; int speedVal = 0; String direction = "stop"; WebServer server(80); // WiFi credentials const char* ssid = "ESP32_Motor"; const char* password = "12345678"; // Encoder ISR void updateEncoder() { int MSB = digitalRead(encoderPinA); int LSB = digitalRead(encoderPinB); int encoded = (MSB << 1) | LSB; int sum = (lastEncoded << 2) | encoded; if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) pulseCount++; else if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) pulseCount--; lastEncoded = encoded; } // HTML interface (RoundSlider) void handleRoot() { server.send(200, "text/html", R"rawliteral( <!DOCTYPE html> <html> <head> <title>Motor Control</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Serif:wght@600&display=swap" rel="stylesheet"> <style> body { font-family: 'IBM Plex Serif', serif; background-color: #fffef7; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; } .container { width: 420px; border: 4px solid #9c9494; padding: 20px; box-shadow: 0 0 12px rgba(0,0,0,0.2); background: white; } .position { color: #387478; font-size: 22px; font-weight: bold; margin-bottom: 10px; } .position span { display: inline-block; border: 1px solid #000; padding: 4px 10px; min-width: 60px; text-align: center; } .speed-label { font-size: 20px; color: #09336e; margin-top: 15px; } #speedSlider { -webkit-appearance: none; width: 100%; height: 12px; background: #e9e9e9; border-radius: 8px; outline: none; position: relative; overflow: visible; /* ✅ allow thumb to overflow */ } #speedSlider::-webkit-slider-runnable-track { height: 12px; background: linear-gradient(to right, #0f2f35 var(--val, 0%), #e9e9e9 var(--val, 0%)); border-radius: 8px; } #speedSlider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -6px; /* this centers the thumb */ width: 24px; height: 24px; background: white; border-radius: 50%; border: 2px solid #ccc; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); cursor: pointer; position: relative; z-index: 2; } #speedSlider::-moz-range-track { background: #e9e9e9; height: 12px; border-radius: 8px; } #speedSlider::-moz-range-progress { background-color: #0f2f35; height: 12px; border-radius: 8px; } #speedSlider::-moz-range-thumb { width: 24px; height: 24px; background: white; border-radius: 50%; border: 2px solid #ccc; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); cursor: pointer; } .direction { font-size: 20px; color: #2c3037; font-weight: bold; margin-top: 15px; } .label-title { font-size: 20px; color: #2c3037; font-weight: bold; margin-top: 15px; } .arrow-buttons { display: flex; justify-content: space-around; margin-top: 10px; } .arrow-buttons button { font-size: 32px; font-weight: bold; padding: 0; width: 70px; height: 70px; border-radius: 20px; /* Rounded square. Change to 50% for perfect circle */ background-color: white; color: #333333; border: 4px solid #333333; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; } .arrow-buttons button:hover { background-color: #eeeeee; } .arrow-buttons button.active { background-color: #333333; color: white; border-color: #333333; } canvas { margin-top: 20px; } </style> </head> <body> <div class="container"> <div class="position">Position: <span id="pos">0</span></div> <div class="speed-label">Speed: <span id="speedVal">0</span></div> <input type="range" min="0" max="255" value="0" id="speedSlider" oninput="setSpeed(this.value)"> <div class="direction"></div> <div class="arrow-buttons"> <button id="leftBtn" onclick="setDir('left')">←</button> <button id="stopBtn" onclick="setDir('stop')">■</button> <button id="rightBtn" onclick="setDir('right')">→</button> </div> <canvas id="speedChart" width="400" height="200"></canvas> </div> <script> let currentSpeed = 0; let labels = []; let data = []; const speedSlider = document.getElementById("speedSlider"); const speedDisplay = document.getElementById("speedVal"); const ctx = document.getElementById('speedChart').getContext('2d'); const chart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: 'Speed', data: data, borderColor: '#629584', tension: 0.2, fill: false }] }, options: { scales: { x: { display: false }, y: { min: 0, max: 255 } } } }); function setSpeed(val) { currentSpeed = parseInt(val); speedDisplay.textContent = val; document.getElementById("speedSlider").style.setProperty('--val', (val / 255) * 100 + '%'); fetch("/setSpeed?value=" + val); } function setDir(dir) { fetch("/setDir?value=" + dir); // Reset all button states document.getElementById("leftBtn").classList.remove("active"); document.getElementById("rightBtn").classList.remove("active"); document.getElementById("stopBtn").classList.remove("active"); // Apply active style if (dir === "left") { document.getElementById("leftBtn").classList.add("active"); } else if (dir === "right") { document.getElementById("rightBtn").classList.add("active"); } else if (dir === "stop") { document.getElementById("stopBtn").classList.add("active"); } } function updateGraph() { const now = new Date().toLocaleTimeString(); if (labels.length >= 20) { labels.shift(); data.shift(); } labels.push(now); data.push(currentSpeed); chart.update(); } setInterval(() => { fetch("/getPos") .then(res => res.text()) .then(val => { document.getElementById("pos").textContent = val; }); updateGraph(); }, 1000); </script> </body> </html> )rawliteral"); } // REST endpoints void handleSetSpeed() { if (server.hasArg("value")) { speedVal = server.arg("value").toInt(); analogWrite(enAPin, speedVal); } server.send(200, "text/plain", "OK"); } void handleSetDir() { if (server.hasArg("value")) { direction = server.arg("value"); if (direction == "left") { digitalWrite(in1Pin, HIGH); digitalWrite(in2Pin, LOW); } else if (direction == "right") { digitalWrite(in1Pin, LOW); digitalWrite(in2Pin, HIGH); } else { digitalWrite(in1Pin, LOW); digitalWrite(in2Pin, LOW); } } server.send(200, "text/plain", "OK"); } void handleGetPos() { server.send(200, "text/plain", String(pulseCount)); } void setup() { Serial.begin(115200); pinMode(in1Pin, OUTPUT); pinMode(in2Pin, OUTPUT); pinMode(enAPin, OUTPUT); pinMode(encoderPinA, INPUT); pinMode(encoderPinB, INPUT); attachInterrupt(digitalPinToInterrupt(encoderPinA), updateEncoder, CHANGE); attachInterrupt(digitalPinToInterrupt(encoderPinB), updateEncoder, CHANGE); WiFi.softAP(ssid, password); Serial.println("Access Point Started: ESP32_Motor"); server.on("/", handleRoot); server.on("/setSpeed", handleSetSpeed); server.on("/setDir", handleSetDir); server.on("/getPos", handleGetPos); server.begin(); } void loop() { server.handleClient(); }