SNAKE
RESCUE
ROBOT
A 3D-printed flexible snake robot that slithers through earthquake rubble to find survivors — with live video, gas detection, temperature sensing, and two-way voice communication.
When an earthquake strikes and buildings collapse, rescuers face a terrifying problem: how do you find a person trapped in a tiny gap between concrete slabs — a space too small for any human or even a dog to enter? This is exactly the problem that engineers and roboticists are solving with snake robots!
A snake robot can squeeze through gaps as narrow as 10 cm, curl around corners, right itself if it tips over, and carry a camera and sensors deep into rubble that no one can safely enter. Real search-and-rescue teams at NASA, NTNU (Norway), and Carnegie Mellon University use snake robots for exactly this mission.
Today, you will build your very own 3D-printed flexible snake robot powered by a Raspberry Pi 4 — with live video streaming, gas leak detection, temperature sensing, and two-way voice communication. It's one of the most advanced robotics projects a young maker can build, and every single skill you learn directly applies to real rescue engineering!
How the Snake Robot Works
The snake robot moves using lateral undulation — the same motion a real snake uses! Each joint in the body bends a little, and when you bend all the joints in a smooth wave pattern, the whole body moves forward. Here are its four superpowers:
The Pi Camera V2 at the snake's head streams live 1080p video over Wi-Fi to your laptop or phone using a Flask web server. Operators see exactly what the snake sees — even in pitch-dark rubble with an IR LED ring.
The MQ-2 sensor on the snout detects methane, LPG, smoke, and carbon monoxide — gases that leak from broken pipes after earthquakes. A loud alarm sounds and the video feed shows a red overlay when danger levels are detected.
The DHT22 temperature sensor measures the air around the robot. A warm spot in cold rubble could mean a living person nearby! The temperature reading is displayed live on the operator's control screen.
A mini USB microphone records the operator's voice and a small speaker inside the robot plays it out — survivors can hear rescuers telling them help is on the way! A second mic on the robot sends sounds back to the operator.
Parts You'll Need
3D Printing the Snake Segments
Each snake body module is a short cylinder with a ball-and-socket joint at each end, plus a servo mount slot and a channel for wires to pass through. You print 8–12 identical modules plus one special head module for the camera.
🖨️ Print Settings
| Setting | Value | Why |
|---|---|---|
| Material | PLA (Orange + Black) | Easy to print, rigid enough, and matches rescue orange colour scheme |
| Layer Height | 0.2 mm | Good balance of speed and strength; 0.1mm for head (more detail) |
| Infill | 20–25% | Light enough for SG90 servos to move; strong enough for rubble |
| Perimeters | 3 walls | Extra wall thickness = better servo mount strength |
| Supports | Yes (tree supports) | Needed for the socket recess and servo mount overhang |
| Print Speed | 40–50 mm/s | Slower = better layer adhesion for the flexible joint areas |
Circuit Diagram
Everything connects to the Raspberry Pi 4 GPIO. Servos are driven by the PCA9685 PWM board (I²C). The MQ-2 analog signal goes through an MCP3008 ADC chip (SPI). DHT22 connects directly to one GPIO pin.
Complete Wiring Table
🔌 PCA9685 Servo Driver → Raspberry Pi GPIO
| PCA9685 Pin | Raspberry Pi Pin | Wire |
|---|---|---|
| SDA | GPIO 2 (Pin 3 — SDA) | ORANGE |
| SCL | GPIO 3 (Pin 5 — SCL) | ORANGE |
| VCC | Pin 2 — 5V | RED |
| GND | Pin 6 — GND | BLACK |
| V+ (servo power) | Power Bank 5V rail (NOT Pi 5V) | RED |
| CH0–CH7 (signal) | SG90 Servo Signal wires 1–8 | YELLOW |
⚗️ MCP3008 ADC → Raspberry Pi SPI
| MCP3008 Pin | Raspberry Pi Pin | Wire |
|---|---|---|
| VDD | Pin 17 — 3.3V | RED |
| VREF | Pin 17 — 3.3V | RED |
| AGND / DGND | Pin 20 — GND | BLACK |
| CLK | GPIO 11 (Pin 23 — SCLK) | BLUE |
| DOUT (MISO) | GPIO 9 (Pin 21 — MISO) | GREEN |
| DIN (MOSI) | GPIO 10 (Pin 19 — MOSI) | GREEN |
| CS / SHDN | GPIO 8 (Pin 24 — CE0) | YELLOW |
| CH0 | MQ-2 AOUT pin | YELLOW |
🌡️ DHT22 + MQ-2 Direct Connections
| Module | Pin | Raspberry Pi | Wire |
|---|---|---|---|
| DHT22 | VCC | Pin 1 — 3.3V | RED |
| DHT22 | DATA | GPIO 4 (Pin 7) + 10kΩ to 3.3V | GREEN |
| DHT22 | GND | Pin 9 — GND | BLACK |
| MQ-2 | VCC | Pin 4 — 5V | RED |
| MQ-2 | GND | GND | BLACK |
| MQ-2 | AOUT | MCP3008 CH0 | YELLOW |
| MQ-2 | DOUT | Not used (analog preferred) | — |
Step-by-Step Build Guide
Set Up the Raspberry Pi
Flash Raspberry Pi OS Lite (64-bit) onto the microSD card using Raspberry Pi Imager. During the flash, enable SSH and configure your Wi-Fi in the Imager settings. Boot the Pi, then SSH in and run:
sudo apt update && sudo apt upgrade -y sudo apt install python3-pip python3-dev -y pip3 install flask adafruit-circuitpython-pca9685 pip3 install adafruit-circuitpython-motor Adafruit_DHT pip3 install spidev picamera2 # Enable I2C and SPI sudo raspi-config # → Interface Options → I2C ON, SPI ON, Camera ON
Print and Assemble the Snake Body
Print all segments. While printing, dry-fit the ball joints — they should click together with moderate resistance. If too loose, print at 101% scale; too tight, try 99%. Use a light dab of white lithium grease on ball joints for smooth movement. Thread a ribbon of jumper wires through all body modules before assembling — it's impossible to add them later!
Mount Servos into Joints
Press each SG90 servo body into the servo mount slot — it should snap in with the mounting tabs. Thread the three servo wires through the channel toward the tail. Alternate servo orientation: joints 1, 3, 5, 7 bend horizontally (left/right); joints 2, 4, 6, 8 bend vertically (up/down). This gives full 3D movement.
Assemble the Head Module
Seat the Pi Camera into the camera slot and secure with two M2 screws. Route the CSI ribbon cable through the body toward the tail where the Pi lives. Mount the MQ-2 module in the sensor slot with hot glue. If using an IR LED ring, press it into the ring slot around the camera lens — wire its power to the Pi's 5V and GND pins.
Wire the PCA9685 Servo Board
Connect all 8 servo signal/VCC/GND wires to PCA9685 channels 0–7. Connect PCA9685 SDA/SCL/VCC/GND to the Pi. Power the servo V+ from the power bank USB-A port via a 5V rail — never from the Pi's own 5V pin. Connect PCA GND to the same common GND as the Pi.
Connect MCP3008, DHT22 and Sensors
Wire the MCP3008 SPI pins to the Pi as per the wiring table. Connect MQ-2 AOUT to MCP3008 CH0. Connect the DHT22 data pin to GPIO4 with a 10kΩ pull-up resistor to 3.3V. Mount the DHT22 in a small hole on any body segment for ambient temperature reading.
Mount USB Microphone and Speaker in Tail
The tail segment holds the Raspberry Pi 4. Velcro or strap the Pi to the inside of the wide tail section. Plug the USB microphone and USB speaker into the Pi's USB ports. Hot-glue the speaker cone facing outward through a hole in the tail panel so sound projects clearly.
Final Assembly and Cable Management
Connect all body segments together — ball into socket, snap firmly. Zip-tie excess wire every 2 segments to prevent snagging on rubble. Power bank goes inside the widest tail segment or in an external pouch zip-tied to the tail. Plug USB-C into Pi and USB-A servo rail. You're ready to upload the code!
Full Python Code
This single Python file runs a Flask web server on the Pi that streams live video, reads sensors, controls servos, and handles two-way audio — all accessible from any browser on the same Wi-Fi network.
#!/usr/bin/env python3 # ================================================================ # 🐍 Disaster Snake Robot — Raspberry Pi 4 # Live Video · Gas Detection · Temp Sensing · Two-Way Audio # Robo Rescue Labs — Kid-Friendly Commented Version # ================================================================ import time, threading, math, subprocess, os import board, busio, spidev import adafruit_pca9685 import Adafruit_DHT from picamera2 import Picamera2 from flask import Flask, Response, render_template_string, jsonify, request import io, cv2 import numpy as np # ---------------------------------------------------------------- # HARDWARE SETUP # ---------------------------------------------------------------- # PCA9685 servo controller over I2C i2c = busio.I2C(board.SCL, board.SDA) pca = adafruit_pca9685.PCA9685(i2c) pca.frequency = 50 # 50Hz = standard servo frequency # SPI for MCP3008 ADC (reads MQ-2 analog value) spi = spidev.SpiDev() spi.open(0, 0) # Bus 0, Device 0 (CE0) spi.max_speed_hz = 1350000 # DHT22 sensor on GPIO 4 DHT_SENSOR = Adafruit_DHT.DHT22 DHT_PIN = 4 # Gas alarm threshold (0–1023 scale) GAS_DANGER_THRESHOLD = 600 # Adjust after calibration! GAS_WARMUP_SECONDS = 30 # MQ-2 preheat time boot_time = time.time() # Pi Camera picam2 = Picamera2() config = picam2.create_video_configuration( main={"size": (640, 480)}) # Lower res = faster streaming picam2.configure(config) picam2.start() # ---------------------------------------------------------------- # SERVO LOCOMOTION # ---------------------------------------------------------------- NUM_JOINTS = 8 # Number of servo joints in the snake def angle_to_duty(angle): """Convert angle (0-180°) to PCA9685 duty cycle value.""" min_pulse = 1000 # µs (0°) max_pulse = 2000 # µs (180°) pulse_us = min_pulse + (max_pulse - min_pulse) * angle / 180 return int(pulse_us / 1000000 * 50 * 65535) def set_servo(channel, angle): """Move servo on PCA9685 channel to given angle.""" angle = max(30, min(150, angle)) # Clamp to safe range pca.channels[channel].duty_cycle = angle_to_duty(angle) def slither_forward(speed=1.0, amplitude=30): """ Lateral undulation — the same motion a real snake uses! Each joint gets a sine wave with a phase offset. The phase offset propagates the wave backward → body moves forward. """ t = time.time() * speed # Time controls wave position for i in range(NUM_JOINTS): phase = i * (math.pi / 2) # 90° offset between joints angle = 90 + amplitude * math.sin(t + phase) set_servo(i, angle) def turn_left(strength=25): """Bias the sine wave to curve the snake leftward.""" t = time.time() for i in range(NUM_JOINTS): phase = i * (math.pi / 2) bias = strength if i % 2 == 0 else 0 angle = (90 - bias) + 30 * math.sin(t + phase) set_servo(i, angle) def turn_right(strength=25): """Bias the sine wave to curve the snake rightward.""" t = time.time() for i in range(NUM_JOINTS): phase = i * (math.pi / 2) bias = strength if i % 2 == 0 else 0 angle = (90 + bias) + 30 * math.sin(t + phase) set_servo(i, angle) def stop_snake(): """Straighten all joints to neutral 90°.""" for i in range(NUM_JOINTS): set_servo(i, 90) # Locomotion state current_move = "stop" move_lock = threading.Lock() def locomotion_thread(): """Background thread: continuously runs movement pattern.""" while True: with move_lock: cmd = current_move if cmd == "forward": slither_forward() elif cmd == "left": turn_left() elif cmd == "right": turn_right() else: stop_snake() time.sleep(0.02) # 50 Hz update rate # ---------------------------------------------------------------- # SENSOR READING # ---------------------------------------------------------------- def read_mcp3008(channel): """Read one analog channel from MCP3008 ADC via SPI.""" r = spi.xfer2([1, (8 + channel) << 4, 0]) return ((r[1] & 3) << 8) + r[2] # 0–1023 def read_gas(): """Return gas reading and status string.""" if time.time() - boot_time < GAS_WARMUP_SECONDS: return {"value": 0, "status": "WARMING UP", "safe": True} val = read_mcp3008(0) # MQ-2 on CH0 return { "value": val, "ppm": round(val * 0.5), # Rough conversion "status": "DANGER! GAS LEAK!" if val > GAS_DANGER_THRESHOLD else "SAFE", "safe": val <= GAS_DANGER_THRESHOLD } def read_temperature(): """Read DHT22 temperature and humidity.""" humidity, temperature = Adafruit_DHT.read_retry(DHT_SENSOR, DHT_PIN) if temperature is not None: return {"temp_c": round(temperature, 1), "humidity": round(humidity, 1) if humidity else 0, "warm_spot": temperature > 35} return {"temp_c": 0, "humidity": 0, "warm_spot": False} # ---------------------------------------------------------------- # VIDEO STREAMING # ---------------------------------------------------------------- def generate_frames(): """Generator that yields MJPEG frames for the live stream.""" while True: frame = picam2.capture_array() # Capture frame from Pi Camera frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) # Gas danger overlay — red tint if gas detected gas = read_gas() if not gas["safe"]: overlay = frame_bgr.copy() overlay[:] = (0, 0, 180) # BGR red frame_bgr = cv2.addWeighted(frame_bgr, 0.7, overlay, 0.3, 0) cv2.putText(frame_bgr, "⚠ GAS DANGER!", (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 0, 255), 3) # Temperature overlay on frame temp_data = read_temperature() label = f"Temp: {temp_data['temp_c']}°C Gas: {gas['ppm']} ppm" cv2.putText(frame_bgr, label, (10, frame_bgr.shape[0] - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 120), 2) # RESCUE badge cv2.putText(frame_bgr, "SNAKE CAM LIVE", (10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 200, 255), 2) # Encode to JPEG and yield as MJPEG chunk _, buffer = cv2.imencode(".jpg", frame_bgr, [cv2.IMWRITE_JPEG_QUALITY, 80]) yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + buffer.tobytes() + b"\r\n") # ---------------------------------------------------------------- # FLASK WEB SERVER # ---------------------------------------------------------------- app = Flask(__name__) # Web operator interface (HTML served from Pi) DASHBOARD_HTML = """ <!DOCTYPE html><html> <head><title>🐍 Snake Robot Control</title> <style> body{background:#0D0D0D;color:#fff;font-family:monospace;text-align:center;margin:0;} h1{color:#FF5722;font-size:2rem;margin:16px 0 4px;} .stream{width:640px;max-width:100%;border:3px solid #FF5722;border-radius:10px;} .btn{background:#FF5722;color:#fff;border:none;border-radius:8px; padding:14px 28px;font-size:1.1rem;font-weight:bold;cursor:pointer;margin:6px;} .btn:hover{background:#BF360C;} .btn-stop{background:#333;} .sensors{display:flex;gap:16px;justify-content:center;flex-wrap:wrap;margin:14px;} .sensor-box{background:#1A1A1A;border:2px solid #444;border-radius:10px;padding:14px 20px;} .sensor-box .val{font-size:1.5rem;color:#FFD600;font-weight:bold;} .danger{border-color:#FF1744!important;color:#FF1744;} .warm{border-color:#FF9800!important;} </style></head> <body> <h1>🐍 SNAKE RESCUE ROBOT</h1> <img src="/video_feed" class="stream" alt="Live Camera"> <br> <div class="sensors"> <div class="sensor-box" id="gasBox">☣️ GAS<br><span id="gasVal" class="val">...</span></div> <div class="sensor-box" id="tempBox">🌡️ TEMP<br><span id="tempVal" class="val">...</span></div> <div class="sensor-box">💧 HUMIDITY<br><span id="humVal" class="val">...</span></div> </div> <div> <button class="btn" onclick="move('forward')">⬆ FORWARD</button><br> <button class="btn" onclick="move('left')">⬅ LEFT</button> <button class="btn btn-stop" onclick="move('stop')">⏹ STOP</button> <button class="btn" onclick="move('right')">RIGHT ➡</button><br> </div> <div style="margin:14px;"> <input id="msgInp" style="padding:10px;width:240px;border-radius:6px;border:2px solid #FF5722;background:#111;color:#fff;" placeholder="Type message to survivor..."> <button class="btn" onclick="sendVoice()">🔊 SPEAK</button> </div> <script> function move(dir){fetch('/move/'+dir);} function sendVoice(){ const msg=document.getElementById('msgInp').value; if(msg) fetch('/speak?text='+encodeURIComponent(msg)); } setInterval(()=>{ fetch('/sensors').then(r=>r.json()).then(d=>{ document.getElementById('gasVal').textContent=d.gas.ppm+' ppm'; document.getElementById('gasBox').className='sensor-box'+(d.gas.safe?'':' danger'); document.getElementById('tempVal').textContent=d.temp.temp_c+'°C'; document.getElementById('tempBox').className='sensor-box'+(d.temp.warm_spot?' warm':''); document.getElementById('humVal').textContent=d.temp.humidity+'%'; }); },1000); document.addEventListener('keydown',(e)=>{ if(e.key==='ArrowUp') move('forward'); if(e.key==='ArrowLeft') move('left'); if(e.key==='ArrowRight') move('right'); if(e.key===' ') move('stop'); }); </script></body></html> """ @app.route("/") def index(): return render_template_string(DASHBOARD_HTML) @app.route("/video_feed") def video_feed(): return Response(generate_frames(), mimetype="multipart/x-mixed-replace; boundary=frame") @app.route("/move/<direction>") def move(direction): global current_move with move_lock: current_move = direction return "OK" @app.route("/sensors") def sensors(): return jsonify(gas=read_gas(), temp=read_temperature()) @app.route("/speak") def speak(): """Use espeak to speak a text message through the robot's speaker.""" text = request.args.get("text", "Help is on the way!") subprocess.Popen(["espeak", "-v", "en", "-s", "120", text]) return "Speaking" # ---------------------------------------------------------------- # MAIN — START THREADS AND SERVER # ---------------------------------------------------------------- if __name__ == "__main__": print("🐍 Snake Robot booting...") # Install espeak if needed os.system("which espeak || sudo apt install espeak -y") # Start locomotion in background thread t = threading.Thread(target=locomotion_thread, daemon=True) t.start() print("✅ Open http://[PI-IP-ADDRESS]:5000 in your browser") app.run(host="0.0.0.0", port=5000, threaded=True)
▶ How to Run It
# On the Raspberry Pi (via SSH or direct terminal) python3 snake_robot.py # On your laptop/phone — open browser at: http://192.168.x.x:5000 # Replace with your Pi's IP address # Find Pi's IP address: hostname -I
Testing Your Robot
Servo Range Test (Before Closing Body)
Before sealing all segments, run set_servo(0, 90) through set_servo(7, 90) to confirm all 8 servos centre correctly. Then test the full 30°–150° range. Any servo that buzzes without moving has a wiring issue — check its PCA channel connection.
Serpentine Motion Test
Run the slither_forward() function with the snake body on a flat smooth surface. You should see a beautiful S-curve propagate from head to tail, pushing the snake forward. If it spins in circles instead, the servo orientation alternation (H/V joints) may be wrong — flip every second servo 180° in the code by changing its angle calculation to 180 - angle.
Gas Sensor Calibration
After 30 seconds warm-up, check the Serial Monitor for the raw MQ-2 reading in clean air — it should be around 150–300. Hold a lighter (without igniting) near the sensor and watch the value spike to 600+ as it detects butane. Set GAS_DANGER_THRESHOLD to something between your clean-air reading and the gas reading.
Live Video and Dashboard Test
Navigate to http://[PI-IP]:5000 from your laptop. You should see the live camera feed with temperature and gas overlay text at the bottom. Test each arrow key to control the snake. Type a message and click SPEAK — listen for the robot's voice through its speaker!
Rubble Navigation Test
Build a small obstacle course from cardboard boxes, books, and rolled towels simulating rubble gaps. Drive your snake robot through it! Rubble navigation is the ultimate test — if it gets stuck, experiment with different amplitude values (try 20 for tighter gaps) in the slither_forward() call.
Take It Further
Thermal Imaging Camera
Replace Pi Camera with an AMG8833 thermal sensor to detect human body heat through darkness and smoke — just like real rescue robots.
SLAM Mapping
Add an RPLidar A1 and run ROS (Robot Operating System) to build a live map of the rubble tunnel as the snake explores — for bigger makers!
Long-Range LoRa Radio
Replace Wi-Fi with LoRa modules (SX1276) for 1–2 km range — Wi-Fi won't penetrate deep concrete rubble but LoRa can.
Autonomous Survivor Detection
Train a TensorFlow Lite model on the Pi to detect faces or breathing sounds and automatically navigate toward detected survivors.
Emergency Supply Carrier
Add a small payload compartment at the head that can carry water pouches or medicine through the rubble to a trapped person.
Gamepad Control
Pair a Bluetooth gamepad to the Pi using pygame for much more intuitive analogue stick control — both speed and turning at once.
Frequently Asked Questions
espeak (a text-to-speech engine) to convert it to audio and play it through the robot's speaker inside the rubble. Survivors can hear the rescuer's words! A microphone on the robot can also pick up sounds from inside and stream them back.
Comments
Post a Comment