Maker project · Arduino · Mechatronics

Self-Serve Dog Fetch Launcher v2

A living-room ball launcher that lets my dog play fetch by herself. She drops a tennis ball into a hopper, a countdown gives her three seconds to clear the exit, and the ball is thrown a dialed-in distance of 3 to 15 feet.

Status: design phase, build in progress Dual flywheel launcher Arduino state machine 12V DC, PWM speed control Estimated parts cost $93 to $155

The problem

Most automatic ball launchers are built for yards, not living rooms. They throw 30 feet or more, and the dog has no safe way to trigger them. I wanted something that fits in a living room, is operated entirely by the dog, and gives her time to get out of the way before the ball leaves the machine. This revision features a drop in hopper vs the initial design that was essentially a large pvc pipe with a funnel at one end and flywheels on the other.

Adjustable range

A single dial sets launch speed so throws can be tuned from about 3 ft up to a hard cap of 15 ft, the most the room allows.

Dog-operated hopper

A funnel sized for a standard tennis ball. A beam sensor notices the drop, so no button press or human help is needed.

3 second delay

The launch waits three seconds after the drop, with a beep each second, so she learns the rhythm and clears the exit path.

How it works

The ball takes a short, simple path. It drops into the hopper and rests on a servo-driven gate, where an infrared beam detects it. That starts a three second countdown. Just before the gate opens, two counter-rotating wheels spin up to the speed set by the dial. The ball rolls down a short ramp into the gap between the wheels, which pinch it and throw it out of the front of the enclosure.

System flow HopperDog drops ball Ball sensorIR beam is broken 3 s delayBeeps at 3, 2, 1 FlywheelsSpin up, then throw Ball out3 to 15 ft Servo gateOpens at 3.0 s Speed dialSets wheel PWM
Figure 1. System flow. The speed dial is read when the ball is detected, so changing the dial between throws changes the next distance.

Control logic

The firmware is a four-state machine with no blocking delays, so the sensor and timers always stay responsive.

State machine 1. IDLE Gate closedWheels off 2. COUNTDOWN Beep each secondWheels spin up 3. FIRE Gate opensBall is launched 4. RESET Gate closesWheels off 1.0 s pause, then ready for the next ball
Figure 2. Firmware state machine.
TransitionTrigger
IDLE to COUNTDOWNBeam stays broken for 100 ms (debounce). The dial position is read and locked in for this throw.
Inside COUNTDOWNBuzzer chirps at 0, 1 and 2 seconds. Wheels ramp to the selected speed during the last 0.8 s so the machine is not spinning at full speed while the dog is still nearby.
COUNTDOWN to FIRE3.0 seconds elapsed. The servo opens the gate and the ball rolls into the wheels.
FIRE to RESET0.8 seconds later the gate closes and the wheels are switched off.
RESET to IDLE1.0 second pause, then the sensor is armed again.

Why dual flywheels

OptionDistance controlComplexityFit for a living room
Dual flywheels (chosen)Continuous, just change motor PWMTwo motors, one driverGood. Low power, easy to enclose, easy to cap in software.
Spring-loaded armCoarse, usually a few fixed positionsMechanical, needs a re-cocking mechanismFair. Hard to dial in short distances.
PneumaticGood, via pressure regulatorCompressor, tank, valvesPoor. Noisy and overkill for 15 ft.

The physics, and the 15 ft cap

A flywheel launcher throws the ball at roughly the speed of the wheel rim. That makes the sizing a short calculation. These are ideal numbers that ignore air drag, wheel slip and the small height of the exit above the floor, so they are a starting point for calibration, not a spec.

QuantityValueNotes
Maximum range15 ft (4.57 m)Hard limit from the room
Speed needed at a 45° launchabout 6.7 m/s (15 mph)Theoretical minimum, but the ball would arc toward the ceiling
Speed needed at a 20° launchabout 8.4 m/s (19 mph)Chosen exit angle. Flatter, safer indoors.
Speed needed for a 3 ft throw at 20°about 3.7 m/s (8 mph)Low end of the dial
Wheel diameter3 in (7.6 cm)Rubber or urethane tread
Wheel speed rangeabout 900 to 2,100 RPMRim speed divided by wheel radius
Motors12V, 3,000 to 6,000 RPM no-loadHeadroom for slip and for the speed sag when the ball loads the wheels
Software cap. The dial maps linearly between a minimum and a maximum PWM value in firmware. The maximum is set during calibration to the value that produces a 15 ft throw on a tape measure, and the dial can never exceed it.

Enclosure and mock-ups

The enclosure has three jobs: guide the ball, keep paws and noses away from the wheels, and stay put when it fires. The wheels sit in a closed box whose only openings are the ball-sized entry from the hopper and the ball-sized exit slot at the front.

Cross-section Electronics bay Arduino, driver, buck 1 2 3 4 5 6 7
Figure 3. Side cross-section. The motors sit behind the wheels, with their shafts pointing out of the page.
  1. Funnel hopper. A 3.5 in inside diameter tube with a flared lip. Wide enough for a tennis ball, too narrow for a snout to reach anything.
  2. Ball at rest on the gate. The IR break-beam crosses the tube at mid-ball height, so only a ball resting on the gate can trip it.
  3. Servo gate. A small SG90 servo swings a flap that holds the ball until the 3 second timer ends.
  4. Feed ramp. A short ramp, about 20 to 25 degrees, rolls the released ball into the wheel gap.
  5. Counter-rotating wheels. The gap is set slightly smaller than the ball so both wheels grip and compress it.
  6. Exit chute. Fixed at a shallow 15 to 20 degree upward angle so even a full-power throw stays well below the ceiling.
  7. Electronics bay. Behind a removable panel, away from the ball path.

All angles and gaps are starting values to tune on the bench. Wheel grip on the ball and the ramp slope are the two things most likely to need adjustment.

Front mock-up Ball exit Beeper Range 3 ft15 ft 321 Wide, weighted base with rubber feet
Figure 4. Front mock-up: funnel hopper on top, a recessed exit port, a range dial, three countdown LEDs and a beeper grille.

Materials. Quarter-inch plywood is easy to cut and stiff enough. 3D-printed PETG panels are a good alternative because wheel spacing can be tuned between prints. The base is wide and weighted, with non-slip feet, so the launcher does not walk across the floor when it fires.

Electronics

An Arduino Uno handles everything. A microcontroller with no operating system is a better fit than a single-board Linux computer here, because PWM, servo timing and sensor debouncing all need predictable timing.

Wiring IR break-beamOUT to D2 Speed pot, 10kWiper to A0 Buck converter12V in, 5V out Arduino Uno D2A05V D9D4D5, D6 (PWM)D7, D8, D10, D11 Servo SG90Signal D9, power 5V Piezo buzzerSignal D4 L298N driverENA, ENB, IN1 to IN4 Motor A Motor B 12V 3A supplyWall adapter 6 lines Signal 5V power 12V power
Figure 5. Wiring overview. All grounds (12V supply, buck converter, L298N, Arduino) are tied together.
Arduino pinConnects to
D2IR break-beam receiver output (internal pull-up, LOW means beam broken)
A0Speed potentiometer wiper
D4Piezo buzzer
D5, D6L298N ENA and ENB (PWM speed for each motor)
D7, D8, D10, D11L298N IN1 to IN4 (motor direction)
D9Gate servo signal
Power notes. The 12V supply feeds the L298N motor input and the buck converter. The buck converter's 5V output powers the Arduino 5V pin, the servo, the IR sensor and the potentiometer. Powering the servo from its own 5V rail keeps its current spikes from resetting the Arduino. If the logic is powered from the buck converter, remove the L298N's onboard 5V-enable jumper. If the wheels throw the ball backward, swap the two wires on one motor.

Parts and cost

PartSpecEstimate
2x DC motors12V, 3,000 to 6,000 RPM no-load, 380/390 class, shaft that fits the wheel hubs$14 to $24
2x wheels3 to 3.5 in diameter, rubber or urethane tread$10 to $20
Motor driverL298N dual H-bridge$6 to $8
Micro servoSG90, 9 g, for the gate$3 to $5
IR break-beam sensorEmitter and receiver pair$5 to $8
MicrocontrollerArduino Uno or Nano$8 to $15
Potentiometer10k linear$1 to $2
Piezo buzzer5V$1 to $2
Power12V 3A adapter and buck converter$10 to $16
Hopper3.5 in ID PVC pipe or printed funnel$10 to $15
Frame and basePlywood or printed panels$15 to $25
MiscellaneousWire, connectors, screws, rubber feet$10 to $15
TotalEverything bought newabout $93 to $155

Spare motors, scrap plywood or a 3D printer bring that down. Plan on roughly 8 to 15 hours for the whole build, with wheel spacing and range calibration taking the most trial and error.

Firmware

The sketch below runs the state machine. MIN_LAUNCH_PWM and MAX_LAUNCH_PWM are deliberately conservative starting values. Raise the maximum in small steps during testing until a throw lands at 15 ft, then leave it there.

dog_ball_launcher.ino
#include <Servo.h>

// Pin definitions
const int IR_SENSOR_PIN  = 2;   // break-beam receiver, LOW = beam broken (ball waiting at gate)
const int BUZZER_PIN     = 4;
const int MOTOR_A_PWM    = 5;   // driver enable A (PWM)
const int MOTOR_B_PWM    = 6;   // driver enable B (PWM)
const int MOTOR_A_IN1    = 7;
const int MOTOR_A_IN2    = 8;
const int SERVO_GATE_PIN = 9;
const int MOTOR_B_IN1    = 10;
const int MOTOR_B_IN2    = 11;
const int SPEED_POT_PIN  = A0;

Servo gateServo;

// Gate positions in degrees, tune to your mechanism
const int GATE_CLOSED = 0;
const int GATE_OPEN   = 90;

// Timing in milliseconds
const unsigned long COUNTDOWN_MS     = 3000;  // delay before the ball is released
const unsigned long SPINUP_LEAD_MS   = 800;   // wheels start this long before release
const unsigned long FIRE_DURATION_MS = 800;   // gate stays open while the ball drops and launches
const unsigned long RESET_DELAY_MS   = 1000;  // pause before accepting another ball
const unsigned long BEAM_DEBOUNCE_MS = 100;   // beam must stay broken this long to count

// Speed limits, PWM 0 to 255
// Conservative starting values. Calibrate with a tape measure:
// raise MAX_LAUNCH_PWM in small steps until the first landing point is 15 ft,
// then stop. The dial can never exceed this cap.
const int MIN_LAUNCH_PWM = 50;    // calibrate so the dial minimum lands about 3 ft out
const int MAX_LAUNCH_PWM = 150;   // calibrate so the dial maximum lands about 15 ft out
const int IDLE_PWM       = 0;

enum State { IDLE, COUNTDOWN, FIRE, RESET_STATE };
State currentState = IDLE;

unsigned long stateStartTime = 0;
unsigned long beamSince = 0;
int lastBeepSecond = -1;
int launchSpeed = MIN_LAUNCH_PWM;

void setup() {
  pinMode(IR_SENSOR_PIN, INPUT_PULLUP);
  pinMode(MOTOR_A_IN1, OUTPUT);
  pinMode(MOTOR_A_IN2, OUTPUT);
  pinMode(MOTOR_B_IN1, OUTPUT);
  pinMode(MOTOR_B_IN2, OUTPUT);
  pinMode(BUZZER_PIN, OUTPUT);

  // The two wheels counter-rotate so both rims push the ball forward.
  // If the ball is driven backward, swap the wires on one motor.
  digitalWrite(MOTOR_A_IN1, HIGH);
  digitalWrite(MOTOR_A_IN2, LOW);
  digitalWrite(MOTOR_B_IN1, LOW);
  digitalWrite(MOTOR_B_IN2, HIGH);

  gateServo.attach(SERVO_GATE_PIN);
  gateServo.write(GATE_CLOSED);

  setMotorSpeed(IDLE_PWM);

  Serial.begin(9600);
  Serial.println("Ball launcher ready.");
}

void loop() {
  switch (currentState) {

    case IDLE:
      if (digitalRead(IR_SENSOR_PIN) == LOW) {
        if (beamSince == 0) beamSince = millis();
        if (millis() - beamSince >= BEAM_DEBOUNCE_MS) {
          launchSpeed = readSpeedDial();
          stateStartTime = millis();
          lastBeepSecond = -1;
          beamSince = 0;
          currentState = COUNTDOWN;
          Serial.print("Ball detected. Launch PWM: ");
          Serial.println(launchSpeed);
        }
      } else {
        beamSince = 0;
      }
      break;

    case COUNTDOWN: {
      unsigned long elapsed = millis() - stateStartTime;

      // One beep per second at 0 s, 1 s and 2 s
      int secondMark = elapsed / 1000;
      if (secondMark != lastBeepSecond && secondMark < 3) {
        lastBeepSecond = secondMark;
        tone(BUZZER_PIN, 1000, 150);
      }

      // Spin the wheels up just before the gate opens
      if (elapsed >= COUNTDOWN_MS - SPINUP_LEAD_MS) {
        setMotorSpeed(launchSpeed);
      }

      if (elapsed >= COUNTDOWN_MS) {
        gateServo.write(GATE_OPEN);
        stateStartTime = millis();
        currentState = FIRE;
      }
      break;
    }

    case FIRE:
      if (millis() - stateStartTime >= FIRE_DURATION_MS) {
        gateServo.write(GATE_CLOSED);
        setMotorSpeed(IDLE_PWM);
        stateStartTime = millis();
        currentState = RESET_STATE;
      }
      break;

    case RESET_STATE:
      if (millis() - stateStartTime >= RESET_DELAY_MS) {
        currentState = IDLE;
        Serial.println("Ready for next ball.");
      }
      break;
  }
}

void setMotorSpeed(int pwmValue) {
  analogWrite(MOTOR_A_PWM, pwmValue);
  analogWrite(MOTOR_B_PWM, pwmValue);
}

int readSpeedDial() {
  int raw = analogRead(SPEED_POT_PIN);  // 0 to 1023
  return map(raw, 0, 1023, MIN_LAUNCH_PWM, MAX_LAUNCH_PWM);
}

Calibration and safety

Calibration procedure

Safety design choices

Training note. The machine only works if the dog learns the routine. The plan is to reward the drop with a treat first, then fade the treat once the ball launches reliably and she starts returning to the hopper on her own.

Next steps