Die Reaktionsmurmelbahn. Fang die Murmel, die über die freie Fläche rollt, mit einer Spülbürste. Bist Du schnell genug???

Liebe Besucherinnen und Besucher der Maker Fair 2025. Vielen Dank für die vielen freudigen Kommentare und leuchtenden Augen bei meiner ReMuBa. Die Anleitung ist jetzt halbwegs fertig. Ich wollte aber noch ein Video der Anlage im Betrieb einstellen. Das wird aber noch etwas dauern. Bei Fragen oder Wünschen zu Hilfestellungen oder Verbesserungen, bitte einfach eine Mail an info@lerntwas.de schicken. Ich versuche, so schnell wie möglich zu antworten.
Diese „Murmelbahn“ besteht aus vielen einzelnen Komponenten und lässt sich ganz individuell gestalten. Daher gibt es hier auch keine „Bauanleitung“, sondern lediglich die Bauteile in einer Zip-Datei zum eigenständigen Zusammenstellen. In der bald folgenden Bilderfolge werden dabei einzelne Komponenten beleuchtet und gezeigt. Das Grundgerüst wird dabei aus DN50 Abwasserrohren gebildet, die es in jedem Baumarkt zu erwerben gibt. Auch abgesägte Rohrreste können hier noch sinnvoll weiterverwendet werden.
Das „Spielprinzip“ ist dabei ganz einfach. Eine Murmel rollt durch einen (transparenten) Gartenschlauch herab und baut Geschwindigkeit auf. Am Ende rollt sie über eine Holzplatte in eine Auffangvorrichtung. Ziel ist es, sie auf dem kurzen Weg vom Ende des Schlaches bis zur Auffangvorrichtung mit einer Spülbürste zu fangen. Zählen tut das nur, wenn man die Murmel wirklich trifft. Ihr soll nicht nur den Weg versperrt werden oder sie nach dem „Treffer“ noch weiter rollen können.
Früher wurde dieses Spiel mit Erbsen und einem Hammer gespielt. Wer die Erbse am frühesten traf, hatte gewonnen. Danach gab es dann Erbsensuppe. 😉
Da es heutzutage nicht mehr tragbar ist mit Lebensmitten zu spielen, wurden die Erbsen durch Murmeln ersetzt. Der Spaß ist hoffentlich der Gleiche.
Man kann das Ganze auch sehr viel einfacher und ohne Technik gestalten. Eine Holzstange auf einem Brett, einen Gartenschlauch drum rumgewickelt und einen Eimer am Ende der Holzplatte. Fertig. Der „aufwendige“ Transport der Murmeln von unten nach oben wird dann von fleißigen Kinderhänden übernommen. Das ist sehr viel einfacher und macht genau so viel Spaß. Also… ran an die Murmeln…
Für alle, die es gerne aufwändiger hätten, hier die gesammelten STL-Dateien für die Murmelbahn als ZIP-Datei.
Die Förderschnecke besteht dabei aus mehreren einzelnen Komponenten von 10cm Höhe, da es sonst nicht möglich ist, diese sinnvoll zu drucken. Sie werden über eine Alu Vierkantstange mit 6x6mm Kantenlänge verbunden, die es in einigen Bauhäusern gibt. Wer mehr als einen Meter benötigt, kann einfach eine Verlängerung in der Mitte eines 10cm Bauteils weiterführen. Die Schnecke wird in eine 50mm Röhre gesteckt, entweder aus Abwasserrohr oder aus Acrylrohr. Bei letzterem kann man den Murmeln bei ihrem Weg noch oben zuschauen. Ein Nema 17 Schrittmotor treibt das Ganze an. Er kann auch durch einen modifizierten Servomotor ersetzt werden, der so beschnitten wurde, dass er dauerhaft in eine Richtung drehen kann. Anleitungen dazu gibt es haufenweise im Netz, eine passende Halterung ist bei den Druckdateien mit dabei.
Wer ein passendes Schrittmotorboard mit Servokomponente für einen ESP8266 Wemos D1 mini sucht, der mit der beigefügten Halterung in das DN50er Rohr passt, findet unter Boards die passende Gerber-Datei dazu.
Der Code für die Steuerung ist auf die Anschlüsse der Schrittmotorsteuerung bezogen. Es wird dabei eine rudientäre Website erzeugt, über die man die einzelnen Komponenten steuern kann. Den Code habe ich mir dabei größtenteils von einer KI generieren lassen. Sorry. Das ging irgenwie schneller. 🙂
#include <stdlib.h> #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> #include <Servo.h> // Pin-Definitionen #define STEP_PIN D5 #define DIR_PIN D6 #define ENABLE_PIN D7 #define SERVO_PIN D2 // WLAN-Zugangsdaten const char* ssid = "Deine-SSID"; const char* password = "Dein-PW"; // Hostname (ohne ".local") const char* hostname = "remuba"; // Webserver auf Port 80 ESP8266WebServer server(80); // Variablen für die Schalter und Eingabefelder int sliderValue = 2; // Wert für den Schieberegler (0, 1, oder 2) bool switch1 = false; // Erster Ein/Aus Schalter bool switch2 = false; // Zweiter Ein/Aus Schalter String input1 = "3000"; // Erstes Eingabefeld String input2 = "10000"; // Zweites Eingabefeld int motor; int pause; bool motorIsRunning = false; bool motorRight = true; int accelAnfang = 10000; //Anfangsverzögerung der Beschleunigung int accelSchritte = 1000; //Anzahl der Nachlaufschritte int accelAnzahl = 500; //Anzahl der Beschleunigungsschritte //accelerateMotor(accelAnfang, stepInterval, accelSchritte, accelAnzahl); // Schrittmotor-Variablen unsigned long lastStepTime = 0; unsigned long stepInterval = 3000; // Mikrosekunden zwischen Steps bool stepState = LOW; // Servo-Variablen Servo myServo; unsigned long lastServoChange = 0; int servoStep = 0; // 0=0°, 1=Pause bei 90°, 2=180°, 3=Pause bei 90° void setup() { motor = atoi(input1.c_str()); pause = atoi(input2.c_str()); Serial.begin(115200); delay(1000); //WiFi.softAP("ESP-Allerlei", "ESP-4711"); // Hostname setzen WiFi.hostname(hostname); // WLAN verbinden WiFi.begin(ssid, password); Serial.print("🔌 Verbinde mit WLAN"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\n✅ WLAN verbunden"); Serial.print("📶 IP-Adresse: "); Serial.println(WiFi.localIP()); // Webserver Routen definieren server.on("/", handleRoot); server.on("/update", handleUpdate); server.on("/status", handleStatus); // Webserver starten server.begin(); Serial.println("✓ Webserver gestartet auf Port 80"); Serial.println("\n=== Setup abgeschlossen ==="); Serial.println("Suchen Sie nach dem WLAN: " + String(ssid)); Serial.println("=========================================\n"); // Schrittmotor initialisieren pinMode(STEP_PIN, OUTPUT); pinMode(DIR_PIN, OUTPUT); pinMode(ENABLE_PIN, OUTPUT); digitalWrite(DIR_PIN, HIGH); // Motor läuft im Uhrzeigersinn digitalWrite(ENABLE_PIN, HIGH); // Motor aktiviert digitalWrite(STEP_PIN, LOW); // Servo initialisieren myServo.attach(SERVO_PIN); myServo.write(0); // Start bei 0° lastServoChange = millis(); } void loop() { server.handleClient(); // Status-Check alle 30 Sekunden static unsigned long lastCheck = 0; if (millis() - lastCheck > 30000) { lastCheck = millis(); Serial.print("Verbundene Clients: "); Serial.println(WiFi.softAPgetStationNum()); } unsigned long currentMicros = micros(); if (currentMicros - lastStepTime >= stepInterval) { stepState = !stepState; digitalWrite(STEP_PIN, stepState); lastStepTime = currentMicros; } // Servo-Sequenz aufrufen if (sliderValue == 0) handleServoSequence(); if (sliderValue == 1) handleServoSequence2(); if (sliderValue == 2); } void accelerateMotor(int startDelayMicros, int targetDelayMicros, int steps, int accelSteps) { //digitalWrite(ENABLE_PIN, LOW); // Motor aktivieren //digitalWrite(DIR_PIN, HIGH); // Drehrichtung setzen (nach Bedarf anpassen) int delayStep = (startDelayMicros - targetDelayMicros) / accelSteps; for (int i = 0; i < steps; i++) { digitalWrite(STEP_PIN, HIGH); delayMicroseconds(startDelayMicros); digitalWrite(STEP_PIN, LOW); delayMicroseconds(startDelayMicros); yield(); // gibt dem System Zeit, den Watchdog zurückzusetzen // Delay verringern (beschleunigen), bis Zielwert erreicht ist if (i < accelSteps && startDelayMicros > targetDelayMicros) { startDelayMicros -= delayStep; if (startDelayMicros < targetDelayMicros) startDelayMicros = targetDelayMicros; } } //digitalWrite(ENABLE_PIN, HIGH); // Motor deaktivieren (optional) motorIsRunning = true; } void handleServoSequence() { unsigned long currentTime = millis(); switch (servoStep) { case 0: // Bei 0° - warte 2 Sekunden, dann zu 90° if (currentTime - lastServoChange >= 1000) { myServo.write(90); lastServoChange = currentTime; servoStep = 1; Serial.println("Servo: 90° (Pause)"); } break; case 1: // Bei 90° - warte 1 Sekunde, dann zu 180° if (currentTime - lastServoChange >= pause) { myServo.write(180); lastServoChange = currentTime; servoStep = 0; Serial.println("Servo: 180°"); } break; } } void handleServoSequence2() { unsigned long currentTime = millis(); switch (servoStep) { case 0: // Bei 180° - warte 2 Sekunden, dann zu 90° if (currentTime - lastServoChange >= 1000) { myServo.write(90); lastServoChange = currentTime; servoStep = 1; Serial.println("Servo: 90° (Pause)"); } break; case 1: // Bei 90° - warte 1 Sekunde, dann zu 0° if (currentTime - lastServoChange >= pause) { myServo.write(0); lastServoChange = currentTime; servoStep = 0; Serial.println("Servo: 0°"); } break; } } // Hauptseite mit HTML Interface void handleRoot() { String html = F(R"rawliteral( <!DOCTYPE html> <html><head> <meta charset='UTF-8'> <title>ReMuBa Control Panel</title> <style> body { font-family: Arial; margin: 20px; background-color: #f0f0f0; } .container { max-width: 600px; margin: 0 auto; background: white; padding: 20px; border-radius: 10px; } .section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; } h1 { color: #333; text-align: center; } h3 { color: #666; } button { padding: 10px 15px; margin: 5px; border: none; border-radius: 5px; cursor: pointer; } .on { background-color: #4CAF50; color: white; } .off { background-color: #f44336; color: white; } input[type='range'] { width: 300px; height: 30px; } input[type='text'] { width: 200px; padding: 8px; margin: 5px; } input[type='submit'] { background-color: #008CBA; color: white; padding: 10px 20px; } .status { margin-top: 20px; padding: 10px; background-color: #e7f3ff; border-radius: 5px; } .slider-value { font-size: 18px; font-weight: bold; color: #2196F3; } </style> </head><body> <div class='container'> <h1>ReMuBa Control Panel</h1> <div class='section'> <h3>Schalterwahl (Wert: <span id='sliderDisplay' class='slider-value'>)rawliteral"); html += String(sliderValue); html += F(R"rawliteral(</span>)</h3> <input type='range' id='slider' min='0' max='2' value=')rawliteral"); html += String(sliderValue); html += F(R"rawliteral(' onchange='updateSlider(this.value)'> <div>0 = links 1 = rechts 2 = Zufall</div> </div> <div class='section'> <h3>Motorsteuerung</h3> <button class=')rawliteral"); html += (switch1 ? "on" : "off"); html += F(R"rawliteral(' onclick='toggleSwitch(1)'>Motor Enable: )rawliteral"); html += (switch1 ? "EIN" : "AUS"); html += F(R"rawliteral(</button> <button class=')rawliteral"); html += (switch2 ? "on" : "off"); html += F(R"rawliteral(' onclick='toggleSwitch(2)'>Richtung: )rawliteral"); html += (switch2 ? "EIN" : "AUS"); html += F(R"rawliteral(</button> </div> <div class='section'> <h3>Werte eingeben</h3> <form onsubmit='updateInputs(); return false;'> Motorsteps: <input type='text' id='input1' value=')rawliteral"); html += input1; html += F(R"rawliteral('><br> Pausenlänge: <input type='text' id='input2' value=')rawliteral"); html += input2; html += F(R"rawliteral('><br> <input type='submit' value='Aktualisieren'> </form> </div> <div class='status'> <h3>Aktueller Status:</h3> Schalterwahl: )rawliteral"); html += String(sliderValue); html += F(R"rawliteral(<br> Motor Enable: )rawliteral"); html += (switch1 ? "true" : "false"); html += F(R"rawliteral(<br> Richtung: )rawliteral"); html += (switch2 ? "true" : "false"); html += F(R"rawliteral(<br> Motorsteps: )rawliteral"); html += input1; html += F(R"rawliteral(<br> Pausenlänge: )rawliteral"); html += input2; html += F(R"rawliteral( </div> </div> <script> function updateSlider(value) { document.getElementById('sliderDisplay').innerText = value; fetch('/update?slider=' + value); } function toggleSwitch(num) { fetch('/update?switch=' + num).then(() => location.reload()); } function updateInputs() { var input1 = document.getElementById('input1').value; var input2 = document.getElementById('input2').value; fetch('/update?input1=' + encodeURIComponent(input1) + '&input2=' + encodeURIComponent(input2)) .then(() => location.reload()); } </script> </body></html> )rawliteral"); server.send(200, "text/html", html); } // Update Handler für Schalter und Eingaben void handleUpdate() { // Schieberegler Update if (server.hasArg("slider")) { sliderValue = server.arg("slider").toInt(); Serial.print("Schieberegler Wert: "); Serial.println(sliderValue); } // Schalter Update if (server.hasArg("switch")) { int switchNum = server.arg("switch").toInt(); if (switchNum == 1) { switch1 = !switch1; Serial.print("Schalter 1: "); Serial.println(switch1 ? "EIN" : "AUS"); } else if (switchNum == 2) { switch2 = !switch2; Serial.print("Schalter 2: "); Serial.println(switch2 ? "EIN" : "AUS"); } } // Meine Werte motor = atoi(input1.c_str()); pause = atoi(input2.c_str()); stepInterval = motor; if(switch1) { if(motorIsRunning == false){ digitalWrite(ENABLE_PIN, LOW); //Motor aktiviert accelerateMotor(accelAnfang, stepInterval, accelSchritte, accelAnzahl); } motorIsRunning = true; } else { digitalWrite(ENABLE_PIN, HIGH); // Motor deaktiviert motorIsRunning = false; } if(switch2) { digitalWrite(DIR_PIN, LOW); if(motorRight == true) accelerateMotor(accelAnfang, stepInterval, accelSchritte, accelAnzahl); motorRight = false; } else { digitalWrite(DIR_PIN, HIGH); if(motorRight == false) accelerateMotor(accelAnfang, stepInterval, accelSchritte, accelAnzahl); motorRight = true; } Serial.println(stepInterval); Serial.println(motor); Serial.println(pause); // Eingabefeld Updates if (server.hasArg("input1")) { input1 = server.arg("input1"); Serial.print("Eingabe 1: "); Serial.println(input1); } if (server.hasArg("input2")) { input2 = server.arg("input2"); Serial.print("Eingabe 2: "); Serial.println(input2); } server.send(200, "text/plain", "OK"); } // Status Handler für API Zugriff void handleStatus() { String json = "{"; json += "\"sliderValue\":"; json += sliderValue; json += ",\"switch1\":"; json += (switch1 ? "true" : "false"); json += ",\"switch2\":"; json += (switch2 ? "true" : "false"); json += ",\"input1\":\""; json += input1; json += "\",\"input2\":\""; json += input2; json += "\"}"; server.send(200, "application/json", json); }
Jetzt aber viel Spaß beim Murmen fangen… 🙂