/* 3D Photogrammetry Scanner Controller Hardware: Arduino Nano, L298N Stepper Driver, Mitsumi Stepper, 2SC1740 NPN switching a 150 ohm resistor across the phone's mic/ground pair (2.15k detect resistor sits permanently across the same pair, at the jack). Pin Out Summary: - D2: Button 1 (4-Photo Quick Scan) -> GND - D3: Button 2 (36-Photo Full Scan) -> GND - D4: Transistor base (via 987 ohm) -> camera shutter - D5: Button 3 (Continuous Showcase) -> GND - D8: L298N IN1 - D9: L298N IN2 - D10: L298N IN3 - D11: L298N IN4 - D12: L298N ENA/ENB SETUP ORDER: power the Arduino first, then plug the phone in, then open the camera app. There is no base pulldown fitted, so D4 floats for ~1-2 s during boot and can trigger a spurious shot if the phone is already connected. */ // --- Pin Definitions --- const int BTN_4_PHOTO = 2; const int BTN_36_PHOTO = 3; const int SHUTTER_PIN = 4; const int BTN_SHOWCASE = 5; const int IN1 = 8; const int IN2 = 9; const int IN3 = 10; const int IN4 = 11; const int ENABLE_PIN = 12; // --- Timing (tune these) --- // SHUTTER_MS must be long enough for the phone to register the press but // short enough to stay below the camera app's burst threshold. Find the // burst threshold by hand, then use about half of it. const int SHUTTER_MS = 120; // Time for the turntable to stop oscillating after a move. Start high, // walk it down while checking images at 100% zoom until blur appears. const int SETTLE_MS = 800; // Time for the phone to capture and write the file before we move again. const int CAPTURE_MS = 2200; const int STEP_DELAY_US = 12000; // scan speed const int SHOWCASE_DELAY_US = 8000; // showcase spin speed // Release the coils while waiting, to keep the motor cool. The 11:1 // reduction means there is effectively no backdrive. Set false if you see // the table creep between positions. const bool RELEASE_COILS_WHEN_IDLE = true; // --- Stepper Motor Parameters --- const int STEPS_PER_REV = 48; // Mitsumi 7.5-degree step angle const float GEAR_RATIO = 11.0; // 176 ring teeth / 16 spur teeth const long TOTAL_STEPS = (long)(STEPS_PER_REV * GEAR_RATIO); // 528 // Stepper Sequence (Full Step Mode) const int stepMatrix[4][4] = { {HIGH, LOW, HIGH, LOW}, {LOW, HIGH, HIGH, LOW}, {LOW, HIGH, LOW, HIGH}, {HIGH, LOW, LOW, HIGH} }; int stepIndex = 0; void setup() { pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT); pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT); pinMode(ENABLE_PIN, OUTPUT); pinMode(SHUTTER_PIN, OUTPUT); digitalWrite(SHUTTER_PIN, LOW); // transistor off before anything else pinMode(BTN_4_PHOTO, INPUT_PULLUP); pinMode(BTN_36_PHOTO, INPUT_PULLUP); pinMode(BTN_SHOWCASE, INPUT_PULLUP); disableMotor(); } void loop() { if (pressed(BTN_4_PHOTO)) { runScanSequence(4); waitForRelease(BTN_4_PHOTO); } if (pressed(BTN_36_PHOTO)) { runScanSequence(36); waitForRelease(BTN_36_PHOTO); } if (pressed(BTN_SHOWCASE)) { waitForRelease(BTN_SHOWCASE); runShowcaseMode(); } } // --- Photogrammetry Scan Sequence --- // Shoots at position 0 first, then moves-settles-shoots for the rest, and // returns home at the end. Positions are computed absolutely so rounding // never accumulates: 36 photos still covers a full 528-step revolution. void runScanSequence(int photoCount) { long previous = 0; triggerShutter(); delay(CAPTURE_MS); for (int i = 1; i < photoCount; i++) { long target = ((long)i * TOTAL_STEPS) / photoCount; enableMotor(); moveSteps(target - previous, STEP_DELAY_US); if (RELEASE_COILS_WHEN_IDLE) disableMotor(); previous = target; delay(SETTLE_MS); triggerShutter(); delay(CAPTURE_MS); } // Return to the starting position enableMotor(); moveSteps(TOTAL_STEPS - previous, STEP_DELAY_US); disableMotor(); } // --- Showcase / Presentation Continuous Rotation --- void runShowcaseMode() { enableMotor(); delay(200); while (true) { stepMotor(1); delayMicroseconds(SHOWCASE_DELAY_US); if (pressed(BTN_SHOWCASE)) { waitForRelease(BTN_SHOWCASE); break; } } disableMotor(); } // --- Helper Functions --- bool pressed(int pin) { if (digitalRead(pin) != LOW) return false; delay(50); // debounce return digitalRead(pin) == LOW; } void waitForRelease(int pin) { while (digitalRead(pin) == LOW) delay(10); delay(50); // debounce the release } void triggerShutter() { digitalWrite(SHUTTER_PIN, HIGH); // 150 ohm across mic/gnd -> volume up delay(SHUTTER_MS); digitalWrite(SHUTTER_PIN, LOW); } void moveSteps(long steps, int speedDelayUs) { for (long i = 0; i < steps; i++) { stepMotor(1); delayMicroseconds(speedDelayUs); } } void stepMotor(int dir) { stepIndex += dir; if (stepIndex > 3) stepIndex = 0; if (stepIndex < 0) stepIndex = 3; digitalWrite(IN1, stepMatrix[stepIndex][0]); digitalWrite(IN2, stepMatrix[stepIndex][1]); digitalWrite(IN3, stepMatrix[stepIndex][2]); digitalWrite(IN4, stepMatrix[stepIndex][3]); } void enableMotor() { digitalWrite(ENABLE_PIN, HIGH); } void disableMotor() { digitalWrite(ENABLE_PIN, LOW); digitalWrite(IN1, LOW); digitalWrite(IN2, LOW); digitalWrite(IN3, LOW); digitalWrite(IN4, LOW); }