diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..627a23c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +line-length = 80 \ No newline at end of file diff --git a/rotating-display-controller.ino.old b/rotating-display-controller.ino.old deleted file mode 100644 index de1837e..0000000 --- a/rotating-display-controller.ino.old +++ /dev/null @@ -1,170 +0,0 @@ -// #include - -// // --- Hardware Configuration --- -// #define IN1 9 -// #define IN2 10 -// #define IN3 11 -// #define IN4 12 - -// // The step sequence (standard for 28BYJ-48 motors) -// int stepSequence[8][4] = { -// { 1, 0, 0, 0 }, -// { 1, 1, 0, 0 }, -// { 0, 1, 0, 0 }, -// { 0, 1, 1, 0 }, -// { 0, 0, 1, 0 }, -// { 0, 0, 1, 1 }, -// { 0, 0, 0, 1 }, -// { 1, 0, 0, 1 } -// }; - -// // --- Configuration Constants --- -// const int STEPS_PER_REV = 4096; -// int stepInterval = 1; -// bool autoMode = true; - -// // --- State Variables --- -// long targetSteps = 0; -// long currentStepCount = 0; -// unsigned long lastMoveTime = 0; // Timer for movement steps -// int speedScale = 1; - -// // --- Auto Mode Specific Variables --- -// unsigned long autoTimer = 0; // Tracks time since last auto-move -// unsigned long autoInterval = 5000; // 5 seconds delay between moves -// int autoState = 0; // 0 = Waiting, 1 = Moving -// int autoStepsToMove = 0; // Calculated steps for 15 degrees -// int autoStepIndex = 0; // Current step index within the move - -// void setStep(int a, int b, int c, int d) { -// digitalWrite(IN1, a); -// digitalWrite(IN2, b); -// digitalWrite(IN3, c); -// digitalWrite(IN4, d); -// } - -// void setup() { -// pinMode(IN1, OUTPUT); -// pinMode(IN2, OUTPUT); -// pinMode(IN3, OUTPUT); -// pinMode(IN4, OUTPUT); - -// Serial.begin(9600); -// while (!Serial) {}; - -// stepInterval = map(speedScale, 1, 50, 1, 100); - -// // Calculate steps for 15 degrees once -// autoStepsToMove = (int)((15.0 / 360.0) * STEPS_PER_REV); - -// autoTimer = millis(); // Start the 5s timer immediately -// Serial.println("System Started."); -// Serial.println("Current Mode: AUTO (15 degrees every 5 seconds)"); -// Serial.println("Send a number or 's[1-50]' to switch to MANUAL mode."); -// } - -// void loop() { -// // --- 1. SERIAL INPUT PROCESSING (HIGHEST PRIORITY) --- -// // If ANY input is received, switch to manual mode IMMEDIATELY. -// if (Serial.available() > 0) { -// String input = Serial.readString(); -// input.trim(); - -// if (input.length() > 0) { -// if (input.startsWith("s")) { -// // Handle Speed change -// int newSpeed = input.substring(1).toInt(); -// if (newSpeed >= 1 && newSpeed <= 50) { -// speedScale = newSpeed; -// stepInterval = map(speedScale, 1, 50, 1, 100); - -// // If we were moving, stop immediately -// currentStepCount = targetSteps; -// autoStepIndex = autoStepsToMove; // Force auto move to finish logic (or reset) - -// Serial.print("Success: Speed set to "); -// Serial.println(speedScale); -// } else { -// Serial.println("Error: Use scale 1-50."); -// } -// } else { -// // Handle Degree change -// float val = input.toFloat(); -// if (val != 0) { -// targetSteps = (long)((val / 360.0) * STEPS_PER_REV); -// currentStepCount = 0; // Reset count to start from zero -// lastMoveTime = millis(); -// autoMode = false; // SWITCH TO MANUAL MODE -// autoState = 0; // Reset auto state machine - -// Serial.print("Switching to Manual Mode. Target: "); -// Serial.print(val); -// Serial.println(" degrees."); -// } -// } -// } -// } - -// // --- 2. EXECUTION LOGIC --- - -// if (autoMode) { -// /* -// NON-BLOCKING AUTO MODE: -// State Machine: -// 0: Waiting for 5 seconds -// 1: Moving the 15 degrees -// */ - -// if (autoState == 0) { -// // Check if 5 seconds have passed -// if (millis() - autoTimer >= autoInterval) { -// autoTimer = millis(); // Reset timer -// autoState = 1; // Start moving -// autoStepIndex = 0; // Reset step counter for this move -// lastMoveTime = millis(); // Start the step timer -// } -// } -// else if (autoState == 1) { -// // Move one step if enough time passed -// if (millis() - lastMoveTime >= (unsigned long)stepInterval) { -// lastMoveTime = millis(); - -// // Calculate sequence index -// int seqIdx = autoStepIndex % 8; -// setStep(stepSequence[seqIdx][0], stepSequence[seqIdx][1], -// stepSequence[seqIdx][2], stepSequence[seqIdx][3]); - -// autoStepIndex++; - -// // Check if we finished the 15-degree move -// if (autoStepIndex >= autoStepsToMove) { -// autoState = 0; // Back to waiting -// Serial.println("Auto move complete. Waiting 5s..."); -// } -// } -// } -// } -// else { -// /* -// MANUAL MODE: -// Non-blocking movement toward target. -// */ -// if (currentStepCount != targetSteps) { -// if (millis() - lastMoveTime >= (unsigned long)stepInterval) { -// lastMoveTime = millis(); - -// // Determine direction -// long direction = (targetSteps > currentStepCount) ? 1 : -1; - -// // Calculate sequence index (handling negative modulo correctly) -// int seqIdx = (int)((currentStepCount % 8 + 8) % 8); -// setStep(stepSequence[seqIdx][0], -// stepSequence[seqIdx][1], -// stepSequence[seqIdx][2], -// stepSequence[seqIdx][3]); - -// currentStepCount += direction; -// } -// } -// } -// } \ No newline at end of file diff --git a/run_scan.py b/run_scan.py index 1e36a0b..e821116 100644 --- a/run_scan.py +++ b/run_scan.py @@ -1,14 +1,14 @@ """ Automated Photogrammetry Capture Suite ===================================== -This module automates a 360-degree photography sequence using an Olympus OM-D -EM-10 Mk III camera and an Arduino-controlled rotation stand. +This module automates a 360-degree photography sequence using an Olympus OM-D +EM-10 Mk III camera and an Arduino-controlled rotation stand. The script synchronizes the two systems by: 1. Initializing the camera's internal API to enter shutter mode. -2. Establishing a persistent serial connection with the Arduino controller to +2. Establishing a persistent serial connection with the Arduino controller to manage rotation speed and movement. -3. Executing a sequenced loop of image capture, delay cycles, and incremental +3. Executing a sequenced loop of image capture, delay cycles, and incremental rotation steps. Hardware Requirements: @@ -18,8 +18,8 @@ Hardware Requirements: Usage Note: ---------- -Ensure the Camera's WiFi is active and the script 'CAMERA_IP' matches its -assigned address on your network. Ensure the correct 'ARDUINO_PORT' is mapped +Ensure the Camera's WiFi is active and the script 'CAMERA_IP' matches its +assigned address on your network. Ensure the correct 'ARDUINO_PORT' is mapped to your local system (e.g., /dev/ttyUSB0 or COMx). """ @@ -52,7 +52,9 @@ def initialize_camera(): Configures the camera's hardware state for photography. """ print("[CAMERA] Switching to SHUTTER mode.") - requests.get(f"http://{CAMERA_IP}/switch_cammode.cgi?mode=shutter", timeout=TIMEOUT) + requests.get( + f"http://{CAMERA_IP}/switch_cammode.cgi?mode=shutter", timeout=TIMEOUT + ) time.sleep(0.3) @@ -60,8 +62,8 @@ def initialize_stand(ser): """ Configures the Arduino-controlled rotation stand's parameters. - This is a blocking function. It sends commands to set the stand into - MANUAL mode and defines the rotation speed (deg/sec). It will not return + This is a blocking function. It sends commands to set the stand into + MANUAL mode and defines the rotation speed (deg/sec). It will not return until the Arduino confirms these states have been successfully accepted. Args: @@ -72,7 +74,9 @@ def initialize_stand(ser): time.sleep(0.3) await_completion(ser, "Mode: MANUAL") - print(f"[STAND] Setting rotation speed to {ROTATION_SPEED_DPS} degrees/second.") + print( + f"[STAND] Setting rotation speed to {ROTATION_SPEED_DPS} degrees/second." + ) ser.write(f"s{ROTATION_SPEED_DPS}\n".encode()) time.sleep(0.3) await_completion(ser, f"Speed: {ROTATION_SPEED_DPS:.0f}") @@ -83,10 +87,15 @@ def take_photo(): Triggers the camera's shutter and waits for capture completion. """ print("[CAMERA] Activating electronic shutter.") - requests.get(f"http://{CAMERA_IP}/exec_shutter.cgi?type=1st2ndpush", timeout=TIMEOUT) + requests.get( + f"http://{CAMERA_IP}/exec_shutter.cgi?type=1st2ndpush", timeout=TIMEOUT + ) time.sleep(0.3) - - requests.get(f"http://{CAMERA_IP}/exec_shutter.cgi?com=2nd1strelease", timeout=TIMEOUT) + + requests.get( + f"http://{CAMERA_IP}/exec_shutter.cgi?com=2nd1strelease", + timeout=TIMEOUT, + ) time.sleep(0.3) @@ -94,8 +103,8 @@ def rotate_stand(ser): """ Commands the stand to perform a specific rotation. - This is a blocking function. It sends the degrees-to-rotate command - and monitors the serial line until the Arduino confirms that the + This is a blocking function. It sends the degrees-to-rotate command + and monitors the serial line until the Arduino confirms that the movement has finished or is ready for the next instruction. Args: @@ -109,7 +118,7 @@ def rotate_stand(ser): def await_completion(ser, s): """ - Blocks the execution of the script until a specific confirmation + Blocks the execution of the script until a specific confirmation string is detected in the serial output from the Arduino. Args: @@ -117,16 +126,16 @@ def await_completion(ser, s): s (str): The substring expected in the response to indicate completion. Raises: - SerialTimeoutError: If no data is received or if a + SerialTimeoutError: If no data is received or if a non-matching line is found repeatedly. """ while True: line_bytes = ser.readline() - line_str = line_bytes.decode('utf-8').strip() - + line_str = line_bytes.decode("utf-8").strip() + if not line_str: raise SerialTimeoutError - + if s in line_str: break @@ -136,7 +145,7 @@ def await_completion(ser, s): def main(): with serial.Serial(ARDUINO_PORT, BAUD_RATE, timeout=TIMEOUT) as ser: try: - time.sleep(2.0) # Buffer to account for Arduino startup + time.sleep(2.0) # Buffer to account for Arduino startup print("Flushing serial buffers.") ser.reset_input_buffer() @@ -144,13 +153,14 @@ def main(): initialize_stand(ser) initialize_camera() - time.sleep(2.0) # Buffer to ensure the display is not moving + time.sleep(2.0) # Buffer to ensure the display is not moving current_angle = 0.0 while current_angle < TOTAL_TARGET_DEGREES: take_photo() - time.sleep(PHOTO_TO_MOVE_DELAY) # Buffer to account for shutter speed + # Buffer to account for shutter speed + time.sleep(PHOTO_TO_MOVE_DELAY) rotate_stand(ser) current_angle += ROTATION_STEP_SIZE @@ -173,4 +183,4 @@ if __name__ == "__main__": # Response: "Mode: MANUAL | State: WAIT | Steps Left: 0 | Speed: 45.00 deg/s | Wait: 8.00s" # Send `a15` command -# Response: "Waiting indefinitely for new command (MANUAL mode)." \ No newline at end of file +# Response: "Waiting indefinitely for new command (MANUAL mode)." diff --git a/run_scan.py.old b/run_scan.py.old deleted file mode 100644 index a139b91..0000000 --- a/run_scan.py.old +++ /dev/null @@ -1,182 +0,0 @@ - -import requests -import serial -import time -import sys -import re - -# ================= CONFIGURATION ================= -CAMERA_IP = "192.168.0.10" -ARDUINO_PORT = "/dev/ttyUSB0" -BAUD_RATE = 9600 - -# Motion Control -ROTATION_SPEED_DPS = 45.0 -ROTATION_STEP_SIZE = 15.0 -TOTAL_TARGET_DEGREES = 360.0 - -# Timing -PHOTO_TO_MOVE_DELAY = 8.0 # Wait after photo before rotation starts -SERIAL_TIMEOUT = 10.0 # Increased timeout for long moves -# ================================================ - -def emergency_shutdown(error_msg, ser=None): - """ - Failsafe routine: Attempts to switch camera to play mode and exits. - """ - print(f"\n[!] ERROR DETECTED: {error_msg}") - print("Initiating failsafe: Switching camera back to default state...") - - try: - requests.get(f"http://{CAMERA_IP}/switch_cammode.cgi?mode=play", timeout=5) - print("Camera switched to play mode.") - except Exception as e: - print(f"Failed to switch camera mode during shutdown: {e}") - - if ser: - try: - ser.close() - print("Serial connection closed.") - except: - pass - - print("System halted.") - sys.exit(1) - -def send_arduino_command(ser, command): - """ - Sends a command to the Arduino. - Does NOT wait for a response here to keep the flow simple, - but the response is consumed in the main loop. - """ - try: - ser.write(f"{command}\n".encode()) - # Small delay to ensure Arduino receives command before next action - time.sleep(0.1) - except Exception as e: - raise Exception(f"Failed to send command '{command}': {e}") - -def wait_for_move_completion(ser, expected_angle, speed_dps): - """ - Waits until the Arduino reports 'Steps Left: 0'. - Falls back to a calculated timeout if serial parsing fails. - """ - # Calculate expected time for the move as a safety fallback - # angle / speed = seconds. Add 10% buffer. - expected_time = (expected_angle / speed_dps) * 1.2 - start_time = time.time() - - print(f"Waiting for movement ({expected_angle}°) to complete...") - - while True: - if ser.in_waiting > 0: - try: - line = ser.readline().decode('utf-8').strip() - if not line: - continue - - # Check for "Steps Left: 0" in the Arduino status output - # Your Arduino prints: " | Steps Left: 0" - if "Steps Left: 0" in line: - print("Movement complete (Steps Left: 0 detected).") - return True - - # Optional: Debug print if needed - # print(f"Arduino: {line}") - except Exception as e: - print(f"Warning: Error reading serial: {e}") - - # Fallback timeout - elapsed = time.time() - start_time - if elapsed > max(expected_time + 5, 30): # Max 30s or calculated time + 5s - print(f"Timeout reached ({elapsed:.1f}s). Assuming move finished.") - return False - - time.sleep(0.1) # Prevent CPU spinning - -def main(): - ser = None - try: - # 1. Initialize Serial Connection (Persistent) - print("Opening serial port...") - ser = serial.Serial(ARDUINO_PORT, BAUD_RATE, timeout=1) - time.sleep(2) # Wait for Arduino to reset and boot - - # Flush any garbage data from boot - ser.reset_input_buffer() - ser.reset_output_buffer() - - # 2. Initialize Table Parameters - print("Initializing table parameters...") - send_arduino_command(ser, f"s{ROTATION_SPEED_DPS}") - time.sleep(0.5) - send_arduino_command(ser, "m1") # Set to Manual mode (controlled by script) - time.sleep(0.5) - - # 3. Prepare Camera State - print("Switching camera to shutter mode...") - requests.get(f"http://{CAMERA_IP}/switch_cammode.cgi?mode=shutter", timeout=5) - print("System ready.") - - current_angle = 0.0 - total_steps = int(TOTAL_TARGET_DEGREES / ROTATION_STEP_SIZE) - - # Ensure we cover the full circle if 360 is the target - # If 360/15 = 24 steps, we do 24 moves. - # The loop runs while current_angle < TOTAL (0 to 345), then we do one last check? - # Actually, simpler: Loop for exactly the number of steps needed. - - steps_taken = 0 - while steps_taken <= total_steps: - # A. Capture Image - angle_label = int(current_angle) - print(f"\n--- Step {steps_taken + 1}/{total_steps + 1} ---") - print(f"Capturing image at {angle_label}°...") - - try: - # 1. Push Shutter (Start exposure) - req1 = requests.get(f"http://{CAMERA_IP}/exec_shutter.cgi?type=1st2ndpush", timeout=10) - time.sleep(0.1) - - # 2. Release Shutter (End exposure) - CRITICAL STEP - # The user specified this command is required to actually take the photo - req2 = requests.get(f"http://{CAMERA_IP}/exec_shutter.cgi?com=2nd1strelease", timeout=10) - - if req1.status_code != 200 or req2.status_code != 200: - raise Exception(f"Camera returned status {req1.status_code} / {req2.status_code}") - - print("Image captured successfully.") - except Exception as e: - raise Exception(f"Camera shutter sequence failed: {e}") - - # If this is the last step, we don't need to move or wait - if steps_taken == total_steps: - print("All steps completed.") - break - - # B. Wait for camera processing - print(f"Waiting {PHOTO_TO_MOVE_DELAY}s for camera processing...") - time.sleep(PHOTO_TO_MOVE_DELAY) - - # C. Rotate Table - print(f"Rotating {ROTATION_STEP_SIZE}°...") - send_arduino_command(ser, f"a{ROTATION_STEP_SIZE}") - - # D. Wait for movement to finish - # We pass the step size and speed to calculate a safe wait time - wait_for_move_completion(ser, ROTATION_STEP_SIZE, ROTATION_SPEED_DPS) - - current_angle += ROTATION_STEP_SIZE - steps_taken += 1 - print(f"Progress: {int(current_angle)}/{TOTAL_TARGET_DEGREES} degrees.") - - print("\n[SUCCESS] Full rotation completed successfully.") - - except Exception as e: - emergency_shutdown(str(e), ser) - finally: - if ser and ser.is_open: - ser.close() - -if __name__ == "__main__": - main() \ No newline at end of file