Free DIY Controller

DIY 3D Printed Robotic Panorama Head for Panoramas, Gigapixels & Timelapse

This is a Arduino-based DIY controller designed for Eclíck — an easy-to-build solution for hobbyists and makers.
This controller provides a reliable and customizable way to operate Eclíck, offering essential functionality with the flexibility to expand based on your needs. The design is straightforward, using widely available components, making it perfect for both beginners and experienced makers.
Or, if you prefer a fully built, ready-to-use controller with advanced functionality and wireless control via Wifi check out the Eclíck WiFi Pro Controller

DIY 3D Printed Robotic Panorama Head for Panoramas, Gigapixels & Timelapse

Download the Eagle schematic & board files

Code Functionality:

  1. Start Button Press Handling :
    • When the startButtonPress flag is set, the system starts the shooting process.
    • Debug message confirms the start action: "DEBUG: Shooting process started!".
  2. Startup Delay :
    • The system applies a startup delay ( startupDelaySec) before beginning the shooting process.
    • Debug message logs the delay duration: "DEBUG: Waiting for startup delay of X seconds...".
  3. Main Shooting Loop :
    • Iterates through each elevation in the shootingPositions array.
    • For each elevation, calculates the adjusted number of pan positions and step size based on the panorama width and direction mode.
  4. Pause/Resume Handling :
    • Before each critical action (moving motors, applying delays, taking shots), the system calls waitWhilePaused() to check for and handle pauses.
    • Debug messages confirm pausing and resuming actions:
      • "DEBUG: Pause Button Pressed".
      • "DEBUG: Resume Button Pressed".
  5. Bracketing Logic :
    • For each pan position, takes multiple shots ( bracketing) with appropriate delays ( delayAfterMs).
    • Focus is triggered only before the first shot in the bracketing sequence if autoFocusEnabled is true.
    • Debug messages confirm the start and completion of bracketing:
      • "DEBUG: Starting bracketing...".
      • "DEBUG: Finished bracketing.".
  6. Return to Start :
    • After completing all shots, the system moves both motors back to their starting positions if returnToStartEnabled is true.
    • Debug message confirms this action: "DEBUG: Returning to starting position...".
  7. Completion :
    • Once the shooting process is complete, the system updates the LEDs and logs a debug message: "DEBUG: Shooting process completed!".

/*
Programmable Robotic Head for:
	- Panoramas,
	- Gigapixel,
	- Motion Timelapse,
	- Static Timelapse/Intervalometer
Website: www.eclick.org
*/

// Include the AccelStepper library for stepper motor control
#include 

// Step 1: Pin Declarations

// Input Pins
const int PUSH_BUTTON = 2; // Push button connected to digital pin D2 (interrupt-capable pin)
const int INPUT_PORT = 3; // Input port (2.5mm jack) connected to digital pin D3

// Output Pins for Pan Motor
const int PAN_DIR_PIN = 4; // Direction pin for the pan motor connected to digital pin D4
const int PAN_STEP_PIN = 5; // Step pin for the pan motor connected to digital pin D5

// Output Pins for Tilt Motor
const int TILT_DIR_PIN = 7; // Direction pin for the tilt motor connected to digital pin D7
const int TILT_STEP_PIN = 8; // Step pin for the tilt motor connected to digital pin D8

// Output Pins for LEDs
const int GREEN_LED = A0; // Green LED connected to analog pin A0
const int RED_LED = A1; // Red LED connected to analog pin A1

// Output Pins for Camera Control
const int CAMERA_SHUTTER = A3; // Camera shutter pin connected to analog pin A3
const int CAMERA_FOCUS = A4; // Camera focus pin connected to analog pin A4

// Step 2: Stepper Motor Configuration (Declarations)

// Define the stepper motor objects using AccelStepper library
AccelStepper panMotor(1, PAN_STEP_PIN, PAN_DIR_PIN); // Create pan motor object
AccelStepper tiltMotor(1, TILT_STEP_PIN, TILT_DIR_PIN); // Create tilt motor object

// Step 3: Motor Parameters (Steps, Microstepping, Gear Ratio)

// Pan Motor Parameters
const int PAN_MOTOR_STEPS = 200; // Number of steps per revolution for the pan motor
const int PAN_MICROSTEPS = 16; // Microstepping value for the pan motor
const float PAN_GEAR_RATIO = 3.0; // Gear ratio for the pan motor

// Tilt Motor Parameters
const int TILT_MOTOR_STEPS = 200; // Number of steps per revolution for the tilt motor
const int TILT_MICROSTEPS = 4; // Microstepping value for the tilt motor
const float TILT_GEAR_RATIO = 50.0; // Gear ratio for the tilt motor

// Function to calculate steps per second from RPM
float rpmToStepsPerSecond(int motorSteps, int microsteps, float gearRatio, float rpm) {
	return (rpm * motorSteps * microsteps * gearRatio) / 60.0;
}

// Step 4: Shooting Profile Parameters

// Timing Parameters (in milliseconds unless specified otherwise)
const int focusSignalDurationMs = 100; // Duration of the focus signal in milliseconds
const int focusDelayAfterMs = 2000; // Delay after focus signal in milliseconds
const int shutterSignalDurationMs = 100; // Duration of the shutter signal in milliseconds
const int delayBeforeMs = 500; // Delay before taking a shot in a sequence
const int delayAfterMs = 1000; // Delay after each shot in a sequence
const int startupDelaySec = 2; // Startup delay in seconds
const int flashIntervalMs = 500; // Interval for flashing the red LED when paused
const int debounceDelayMs = 2000; // Debounce delay (2 seconds)

// Panorama Parameters
const int panoramaWidthDegrees = 360; // Total width of the panorama in degrees
const String tiltArmInitialization = "Zenith"; // Tilt arm initialization: "Horizontal" or "Zenith"
const int bracketing = 1; // Number of shots (bracketing) at each shooting position
const String directionMode = "Normal"; // Direction mode: "Normal" or "Reverse"

// Array of shooting positions [elevation, number of pan positions]
const int shootingPositions[][2] = {
    {0, 6},   // Elevation 0°, 6 pan positions
    {45, 4},  // Elevation +45°, 4 pan positions
    {90, 1},  // Elevation +90°, 1 pan position
    {-45, 4}, // Elevation -45°, 4 pan positions
    {-90, 1}  // Elevation -90°, 1 pan position
};

const int numElevations = sizeof(shootingPositions) / sizeof(shootingPositions[0]); // Number of elevations

// Shooting Mode Parameters
const bool autoFocusEnabled = false; // Enable or disable auto-focus
const bool returnToStartEnabled = true; // Return to start position after shooting

// Motor Speeds and Acceleration
const float panSpeedRPM = 12; // Desired speed for the pan motor in RPM
const float panAccelerationStepsPerSec2 = 2000; // Acceleration for the pan motor in steps/sec^2
const float tiltSpeedRPM = 4; // Desired speed for the tilt motor in RPM
const float tiltAccelerationStepsPerSec2 = 2000; // Acceleration for the tilt motor in steps/sec^2

// Global Variables for Steps Per Degree
float panTotalSteps; // Total steps for the pan motor
float panStepsPerDegree; // Steps per degree for the pan motor
float tiltTotalSteps; // Total steps for the tilt motor
float tiltStepsPerDegree; // Steps per degree for the tilt motor

// Variables for pause/resume functionality
volatile bool buttonPressedFlag = false; // Flag set by interrupt
bool isPaused = false; // Tracks if the system is paused
unsigned long lastButtonPressTime = 0; // Timestamp for the last button press
unsigned long lastFlashTime = 0; // Timestamp for flashing the red LED
volatile bool startButtonPress = false;

// Variables to track starting position
float startingPanAngle = 0; // Starting pan angle (always 0°)
float startingTiltAngle = 0; // Starting tilt angle (determined by tiltArmInitialization)

// Function to move the pan motor to a specific angle
void movePanMotorToAngle(float targetAngle) {
	long targetSteps = round(targetAngle * panStepsPerDegree);
	panMotor.moveTo(targetSteps);

	Serial.print("DEBUG: Moving pan motor to position ");
	Serial.println(targetAngle);

	while (panMotor.distanceToGo() != 0 && !isPaused) {
		panMotor.run();
	}

	Serial.print("DEBUG: Arrived at pan position ");
	Serial.println(targetAngle);
}

// Function to move the tilt motor to a specific angle
void moveTiltMotorToAngle(float targetAngle) {
	long targetSteps = round(targetAngle * tiltStepsPerDegree);
	tiltMotor.moveTo(targetSteps);

	Serial.print("DEBUG: Moving tilt motor to position ");
	Serial.println(targetAngle);

	while (tiltMotor.distanceToGo() != 0 && !isPaused) {
		tiltMotor.run();
	}

	Serial.print("DEBUG: Arrived at tilt position ");
	Serial.println(targetAngle);
}

// Function to trigger the camera focus
void triggerFocus() {
	digitalWrite(CAMERA_FOCUS, HIGH);
	delay(focusSignalDurationMs);
	digitalWrite(CAMERA_FOCUS, LOW);
	delay(focusDelayAfterMs);

	Serial.println("DEBUG: Focus triggered.");
}

// Function to trigger the camera shutter
void triggerShutter() {
	digitalWrite(CAMERA_SHUTTER, HIGH);
	delay(shutterSignalDurationMs);
	digitalWrite(CAMERA_SHUTTER, LOW);

	Serial.println("DEBUG: Shutter triggered.");
}

// Function to set LED states
void setLEDs(bool greenOn, bool redOn) {
	if (greenOn) {
		digitalWrite(GREEN_LED, HIGH);
	} else {
		digitalWrite(GREEN_LED, LOW);
	}

	if (redOn) {
		digitalWrite(RED_LED, HIGH);
	} else {
		digitalWrite(RED_LED, LOW);
	}
}

// Function to handle LED flashing when paused
void flashRedLED() {
	unsigned long currentTime = millis();
	if (currentTime - lastFlashTime >= flashIntervalMs) {
		lastFlashTime = currentTime;

		if (digitalRead(RED_LED) == HIGH) {
			digitalWrite(RED_LED, LOW); // Turn off the red LED
		} else {
			digitalWrite(RED_LED, HIGH); // Turn on the red LED
		}
	}
}

// ISR to detect button presses with debouncing
void button_ISR() {
	unsigned long currentTime = millis();

	if (currentTime - lastButtonPressTime > debounceDelayMs) {
		lastButtonPressTime = currentTime; // Update debounce time

		bool buttonState = digitalRead(GREEN_LED);

		if (!isPaused && buttonState == HIGH) { // Start button pressed
			startButtonPress = true;
			isPaused = false;
			Serial.println("DEBUG: Start Button Pressed.");
		} else if (!isPaused && buttonState == LOW) { // Pause button pressed
			startButtonPress = false;
			isPaused = true;
			Serial.println("DEBUG: Pause Button Pressed.");
		} else if (isPaused) { // Resume button pressed
			startButtonPress = false;
			isPaused = false;
			Serial.println("DEBUG: Resume Button Pressed.");
		}
	}
}

// Function to wait while the system is paused
void waitWhilePaused() {
	while (isPaused) {
		flashRedLED(); // Flash the red LED while paused
	}

	setLEDs(false, true); // Green LED OFF, Red LED ON
	Serial.println("DEBUG: Resumed shooting process.");
}

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

	// Initialize input pins with internal pull-up resistors
	pinMode(PUSH_BUTTON, INPUT_PULLUP); // Set push button as an input with internal pull-up
	attachInterrupt(digitalPinToInterrupt(PUSH_BUTTON), button_ISR, FALLING); // Attach interrupt to button pin

	// Initialize output pins
	pinMode(PAN_DIR_PIN, OUTPUT); // Set pan motor direction pin as an output
	pinMode(PAN_STEP_PIN, OUTPUT); // Set pan motor step pin as an output
	pinMode(TILT_DIR_PIN, OUTPUT); // Set tilt motor direction pin as an output
	pinMode(TILT_STEP_PIN, OUTPUT); // Set tilt motor step pin as an output
	pinMode(GREEN_LED, OUTPUT); // Set green LED pin as an output
	pinMode(RED_LED, OUTPUT); // Set red LED pin as an output
	pinMode(CAMERA_SHUTTER, OUTPUT); // Set camera shutter pin as an output
	pinMode(CAMERA_FOCUS, OUTPUT); // Set camera focus pin as an output

	// Convert RPM to steps per second
	float panSpeedStepsPerSec = rpmToStepsPerSecond(PAN_MOTOR_STEPS, PAN_MICROSTEPS, PAN_GEAR_RATIO, panSpeedRPM);
	float tiltSpeedStepsPerSec = rpmToStepsPerSecond(TILT_MOTOR_STEPS, TILT_MICROSTEPS, TILT_GEAR_RATIO, tiltSpeedRPM);

	// Set maximum speeds for the motors
	panMotor.setMaxSpeed(panSpeedStepsPerSec);
	tiltMotor.setMaxSpeed(tiltSpeedStepsPerSec);

	// Set acceleration for smoother motion
	panMotor.setAcceleration(panAccelerationStepsPerSec2);
	tiltMotor.setAcceleration(tiltAccelerationStepsPerSec2);

	// Pre-calculate total steps and steps per degree for pan and tilt motors
	panTotalSteps = PAN_MOTOR_STEPS * PAN_MICROSTEPS * PAN_GEAR_RATIO;
	panStepsPerDegree = panTotalSteps / 360.0;

	tiltTotalSteps = TILT_MOTOR_STEPS * TILT_MICROSTEPS * TILT_GEAR_RATIO;
	tiltStepsPerDegree = tiltTotalSteps / 360.0;

	// Tilt Arm Initialization
	if (tiltArmInitialization == "Horizontal") {
		startingTiltAngle = 0; // Assume the tilt arm is at 0° (horizontal)
		tiltMotor.setCurrentPosition(0);
		Serial.println("DEBUG: Tilt arm initialized to Horizontal (0°).");
	} else if (tiltArmInitialization == "Zenith") {
		startingTiltAngle = 90; // Assume the tilt arm is at +90° (zenith)
		tiltMotor.setCurrentPosition(tiltTotalSteps / 4); // Set current position to +90°
		Serial.println("DEBUG: Tilt arm initialized to Zenith (+90°).");
	} else {
		Serial.println("DEBUG: Invalid tiltArmInitialization parameter. Defaulting to Zenith (+90°).");
		startingTiltAngle = 90; // Default to horizontal if invalid parameter
		tiltMotor.setCurrentPosition(tiltTotalSteps / 4);
	}

	// Store the starting pan angle (assumed to be 0° at the beginning)
	startingPanAngle = 0;

	// Set LEDs to indicate readiness
	setLEDs(true, false); // Green LED ON, Red LED OFF
	Serial.println("DEBUG: System ready. Press the button to start the shooting process...");
}

void loop() {
	// Check if the Start button is pressed to start the process
	if (startButtonPress) {
		startButtonPress = false;

		Serial.println("DEBUG: Shooting process started!");
		setLEDs(false, true); // Green LED OFF, Red LED ON

		// Apply startup delay
		Serial.print("DEBUG: Waiting for startup delay of ");
		Serial.print(startupDelaySec);
		Serial.println(" seconds...");
		delay(startupDelaySec * 1000); // Convert seconds to milliseconds

		// Main shooting loop
		for (int k = 0; k < numElevations; k++) {
			int elevation = shootingPositions[k][0]; // Current elevation angle
			int numPanPositions = shootingPositions[k][1]; // Number of pan positions for this elevation

			// Adjusted number of pan positions for panoramas less than 360°
			int adjustedNumPanPositions = numPanPositions;
			if (panoramaWidthDegrees < 360) {
				adjustedNumPanPositions = numPanPositions + 1; // Add one extra position
			}

			// Calculate step size between pan positions using numPanPositions
			float stepSize = panoramaWidthDegrees / numPanPositions;

			// Wait for any pending pause state before proceeding
			waitWhilePaused();

			// Move the tilt motor to the current elevation
			Serial.print("DEBUG: Moving to elevation ");
			Serial.println(elevation);
			moveTiltMotorToAngle(elevation);

			// Inner loop for pan positions
			for (int m = 0; m < adjustedNumPanPositions; m++) {
				// Wait for any pending pause state before proceeding
				waitWhilePaused();

				// Calculate the current pan position
				float panPosition = 0;
				if (directionMode == "Normal") {
					panPosition = m * stepSize;
				} else if (directionMode == "Reverse") {
					panPosition = panoramaWidthDegrees - m * stepSize;
				}

				// Move the pan motor to the current pan position
				Serial.print("DEBUG: Moving to pan position ");
				Serial.println(panPosition);
				movePanMotorToAngle(panPosition);

				// Wait for any pending pause state before proceeding
				waitWhilePaused();

				// Wait for microvibrations to dissipate (before the first shot)
				Serial.println("DEBUG: Applying delayBeforeMs...");
				delay(delayBeforeMs);
				Serial.println("DEBUG: Finished applying delayBeforeMs.");

				// Take shots with bracketing
				Serial.println("DEBUG: Starting bracketing...");
				for (int b = 0; b < bracketing; b++) {
					if (b == 0) {
						// Trigger focus only before the first shot in the bracketing sequence
						if (autoFocusEnabled) {
							triggerFocus(); // Trigger focus if enabled
						}
					}

					// Trigger the camera shutter
					triggerShutter();

					// Wait for the delay after the shot
					if (b < bracketing - 1) {
						delay(delayAfterMs);
					}
				}
				Serial.println("DEBUG: Finished bracketing.");

				// Wait for any pending pause state before proceeding
				waitWhilePaused();
			}

			Serial.println("DEBUG: Moving to next elevation...");
		}

		// Return to the starting position if enabled
		if (returnToStartEnabled) {
			Serial.println("DEBUG: Returning to starting position...");

			// Move the pan motor back to the starting pan angle
			movePanMotorToAngle(startingPanAngle);

			// Move the tilt motor back to the starting tilt angle
			moveTiltMotorToAngle(startingTiltAngle);
		}

		// Update LEDs to indicate readiness
		setLEDs(true, false); // Green LED ON, Red LED OFF
		Serial.println("DEBUG: Shooting process completed!");
	}
}