Compare commits

...

11 Commits

5 changed files with 454 additions and 170 deletions
+1
View File
@@ -0,0 +1 @@
.vscode
+247
View File
@@ -0,0 +1,247 @@
# Battery-Powered 28BYJ-48 Stepper Controller
This project uses one 18650 cell, a TP4056 charger module, an MT3608 boost
converter, an Arduino Nano, and a ULN2003 driver board to power a 28BYJ-48
stepper motor.
A single-pole KCD11 rocker switch turns the battery-to-boost path on and off.
Turning the switch off removes the boost input, so the whole 5 V rail (Nano and
motor) is unpowered. The Arduino Nano clone used here has **no USB VBUS Schottky
diode**, so the USB and battery-powered 5 V supply must not be connected to the
Nano at the same time.
> This document describes wiring and power only. It does not include Arduino
> code.
---
## 1. Components
| Component | Notes |
|---|---|
| 18650 Li-ion cell | Single cell, suitable holder or secure battery connection |
| TP4056 charger module | Use a protected module with separate `B+`, `B−`, `OUT+`, and `OUT−` terminals |
| MT3608 boost converter | Adjustable; set its output to 5.0 V before connecting the loads |
| KCD11 rocker switch | Single pole, on/off; see the variant note below |
| Arduino Nano clone | This clone has no VBUS Schottky diode |
| 28BYJ-48 stepper motor | Use the 5 V version |
| ULN2003 driver board | Matches the motor’s 5-pin plug |
| Optional fuse | Near TP4056 `OUT+`; rated above normal load current and below safe limits of the cell, protection board, and wiring |
| Optional capacitor | 100–470 µF across the 5 V rail; observe capacitor polarity and voltage rating |
Check the labels on the actual modules: board layouts and terminal names vary.
Some TP4056 boards do not have a protection circuit or separate `OUT` terminals.
This diagram requires a protected module with separate load terminals. Do not
assume an unprotected TP4056 board provides battery over-discharge or
over-current protection, and do not use its `B+`/`B−` pads as though they were
protected `OUT+`/`OUT−` pads.
### KCD11 switch variants
The KCD11 family ships in several contact configurations and often shares one
housing across them. Terminals labeled `1`, `1a`, and `1b` describe a single
pole with two throws; the unused throw may be molded into the housing with no
metal lug. This build needs only **one on/off pole with two usable terminals**.
On the switch used here, `1` is the **center lug (common)** and `1b` is the
populated throw; `1a` is an empty pocket and stays **unconnected**. Wire the two
switch wires across `1` and `1b`. A mechanical contact has no polarity, so
either wire may go to either lug.
Confirm the switch with a multimeter before wiring: there should be continuity
between `1` and `1b` when the rocker is on, and none when it is off. A single
pole is sufficient because the motor has no independent supply of its own (see
section 4).
---
## 2. Power flow
```text
18650 ──► TP4056 protected battery/load terminals
│
└── OUT+ ──► KCD11 ──► MT3608 ──► 5 V rail
├── Nano 5V
└── ULN2003 VCC
```
The TP4056 is a **charger**, not a 5 V power supply. Its `IN+`/`IN−` terminals
are the USB charging input. The `B+`/`B−` terminals connect to the battery.
The protected module’s `OUT+`/`OUT−` terminals provide the battery-voltage load
connection.
The MT3608 boosts the cell voltage (approximately 3.0–4.2 V) to 5.0 V.
---
## 3. Wiring schematic
```text
Battery:
18650 (+) ──► TP4056 B+
18650 (−) ──► TP4056 B−
Charging input:
USB 5 V (+) ──► TP4056 IN+
USB GND ──► TP4056 IN−
Battery-powered load path:
TP4056 OUT+ ──[optional fuse]──► KCD11 terminal 1 (common, center lug)
KCD11 terminal 1b ──► MT3608 IN+
KCD11 terminal 1a (if present) ──► leave unconnected
TP4056 OUT− ──► common system GND
MT3608 IN− ──► common system GND
MT3608 OUT− ──► common system GND
Nano GND ──► common system GND
ULN2003 GND ──► common system GND
5 V and motor supply:
MT3608 OUT+ ──► 5 V rail ──┬──► Arduino Nano 5V
└──► ULN2003 VCC
Control and motor:
Arduino Nano D9 ──► ULN2003 IN1
Arduino Nano D10 ──► ULN2003 IN2
Arduino Nano D11 ──► ULN2003 IN3
Arduino Nano D12 ──► ULN2003 IN4
28BYJ-48 5-pin plug ──► ULN2003 motor connector
```
### Important TP4056 ground detail
Connect the cell’s negative terminal to **`B−` only**. Connect system ground to
**`OUT−`**. Do **not** add an external wire directly joining `B−` to `OUT−`;
that can bypass the protection circuit on a protected TP4056 module.
The battery negative and system ground may be connected internally through the
module’s protection circuitry. Use the module’s labeled load terminals for the
load.
---
## 4. KCD11 switch operation
The KCD11 is a single-pole on/off switch in the battery-to-boost path, wired
across terminals `1` (center common) and `1b`:
- **ON (closed):** connects TP4056 `OUT+` to MT3608 `IN+`.
- **OFF (open):** disconnects the battery from the boost input.
| Switch state | Contact | Result |
|---|---|---|
| **ON** | Closed | Battery powers the MT3608; the 5 V rail feeds the Nano and ULN2003 |
| **OFF** | Open | Boost input is disconnected; the 5 V rail (and therefore the motor) is unpowered |
The ULN2003 `VCC` is tied directly to the 5 V rail, so there is no separate
motor cutoff. The motor is unpowered whenever the switch is off because the
whole rail is dead. The Nano can still be powered over USB while the switch is
off; in that case the rail—and therefore ULN2003 `VCC`—is fed from USB. See the
debug mode in section 6.
---
## 5. MT3608 setup
The MT3608 is adjustable; it is not a fixed 5 V supply.
1. Turn the KCD11 switch **OFF**.
2. Leave the Nano and ULN2003 disconnected.
3. Connect the cell and measure MT3608 `OUT+` to `OUT−`.
4. Adjust the module’s potentiometer until the output is **5.0 V**.
5. Power-cycle and recheck the output.
6. Connect the Nano and ULN2003 only after confirming the voltage.
The module’s current capability depends on its components, input voltage, and
cooling. The MT3608 IC’s “2 A” switch-current rating does not mean every module
can deliver 2 A at 5 V.
---
## 6. Operating modes
### Run from battery
- KCD11: **ON**
- Nano USB: **unplugged**
- TP4056 charging USB: **unplugged**
The Nano and motor are powered from the MT3608’s 5 V output.
### Charge the battery
- KCD11: **OFF**
- TP4056 USB input: connected to a 5 V charger
- Nano USB: **unplugged**
With the switch open, the battery-powered load path is disconnected. Keep the
Nano USB unplugged during charging as the conservative procedure.
### Debug over USB serial
- KCD11: **OFF**
- TP4056 charging USB: **unplugged**
- Nano USB: connected to the computer
- Disconnect the MT3608 `OUT+` wire from the 5 V rail while debugging.
The Nano clone has no VBUS Schottky diode. Disconnecting the boost output during
USB debugging avoids applying USB 5 V to the MT3608 output. With the switch open
the boost is off, but the ULN2003 `VCC` still follows the 5 V rail, which USB now
powers through the Nano’s 5 V pin. Hold the Nano in reset (or do not run the
sketch) so the driver does not drive the motor from USB.
**Never connect Nano USB while the KCD11 is ON.** In that state, the MT3608
supplies the Nano’s 5 V pin, which could back-feed the computer’s USB port.
---
## 7. First power-on checklist
1. Confirm the cell goes to TP4056 `B+` and `B−`.
2. Confirm the circuit ground goes to TP4056 `OUT−`; do not bridge `B−` to
`OUT−`.
3. With the switch OFF and loads disconnected, set the MT3608 output to 5.0 V.
4. Recheck polarity and wiring, including the KCD11 continuity check.
5. Connect the Nano and ULN2003.
6. Run from battery only with the switch ON and Nano USB unplugged.
---
## 8. Design notes
- Use the Nano’s **`5V`** pin for the regulated 5 V rail. Do not use `VIN` for
this 5 V supply.
- The 28BYJ-48 plugs directly into the ULN2003 board; individual coil wires
should not need to be jumpered.
- Standard 28BYJ-48 ULN2003 boards do not have a current-adjustment trimmer.
Leave their bypass capacitors installed.
- An optional 100–470 µF capacitor can be placed across the 5 V rail and GND
near the loads to help with supply dips. Observe its polarity.
- Keep the Nano, MT3608, and ULN2003 grounds connected to the TP4056
`OUT−`-referenced system ground.
- The single KCD11 pole removes the whole 5 V rail when off, so there is no
independent motor-supply cutoff. During USB debugging the rail and ULN2003
`VCC` are fed from USB, so hold the Nano in reset and disconnect the MT3608
`OUT+` wire.
- Do not charge and run the load simultaneously with this simple TP4056
arrangement. Supporting that requires a proper power-path/load-sharing
design.
---
## 9. References
These tutorials cover the stepper/driver portion of the project:
- 28BYJ-48 + ULN2003 + Arduino walkthrough:
https://lastminuteengineers.com/28byj48-stepper-motor-arduino-tutorial/
- element14 stepper wiring walkthrough:
https://community.element14.com/products/arduino/arduino-projects/b/blog/posts/how-to-control-28byj-48-stepper-motor-with-uln2003-driver-and-arduino
TP4056 load-sharing background:
- Arduino discussion of TP4056, boost converter, and Nano:
https://forum.arduino.cc/t/using-li-ion-battery-with-tp4056-to-power-arduino-nano/1324112
- TP4056 load-sharing modification discussion:
https://forum.arduino.cc/t/tp4056-lithium-charger-module-modification-to-add-power-sharing/1008473
+2
View File
@@ -0,0 +1,2 @@
[tool.black]
line-length = 80
-170
View File
@@ -1,170 +0,0 @@
// #include <Arduino.h>
// // --- 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;
// }
// }
// }
// }
+204
View File
@@ -0,0 +1,204 @@
"""
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.
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
manage rotation speed and movement.
3. Executing a sequenced loop of image capture, delay cycles, and incremental
rotation steps.
Hardware Requirements:
-----------------------
- Camera: Olympus OM-D EM-10 Mk III (connected via its internal WiFi network).
- Stand: Arduino-based rotation table (connected via USB/Serial).
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
to your local system (e.g., /dev/ttyUSB0 or COMx).
"""
import requests
import serial
import time
# ================= 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
TIMEOUT = 10.0
MOVE_TO_PHOTO_DELAY = ROTATION_SPEED_DPS / ROTATION_STEP_SIZE + 2.0
# ================================================
class SerialTimeoutError(Exception):
pass
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
)
time.sleep(0.3)
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
until the Arduino confirms these states have been successfully accepted.
Args:
ser (serial.Serial): The open serial connection to the Arduino controller.
"""
print("[STAND] Switching to MANUAL mode.")
ser.write(f"m1\n".encode())
time.sleep(0.3)
await_completion(ser, "Mode: MANUAL")
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}")
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
)
time.sleep(0.3)
requests.get(
f"http://{CAMERA_IP}/exec_shutter.cgi?com=2nd1strelease",
timeout=TIMEOUT,
)
time.sleep(PHOTO_TO_MOVE_DELAY) # Buffer to account for shutter speed
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
movement has finished or is ready for the next instruction.
Args:
ser (serial.Serial): The open serial connection to the Arduino controller.
"""
print(f"[STAND] Rotating stand by {ROTATION_STEP_SIZE} degrees.")
ser.write(f"a{ROTATION_STEP_SIZE}\n".encode())
time.sleep(0.3)
await_completion(ser, "Waiting")
time.sleep(MOVE_TO_PHOTO_DELAY) # Buffer to account for movement time
def flush_buffer(ser):
"""
Reads all currently stored bytes from the serial port until the buffer is empty.
Args:
ser (serial.Serial): The active serial connection to the Arduino.
"""
while ser.in_waiting > 0:
line_bytes = ser.readline().decode("utf-8").strip()
line_str = line_bytes.decode("utf-8").strip()
print(f"[SERIAL] {line_str}")
def await_completion(ser, s):
"""
Blocks the execution of the script until a specific confirmation
string is detected in the serial output from the Arduino.
Args:
ser (serial.Serial): The open serial connection to the Arduino.
s (str): The substring expected in the response to indicate completion.
Raises:
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()
print(f"[SERIAL] {line_str}")
if not line_str:
raise SerialTimeoutError
if s in line_str:
break
time.sleep(0.1)
flush_buffer(ser)
def main():
with serial.Serial(ARDUINO_PORT, BAUD_RATE, timeout=TIMEOUT) as ser:
try:
time.sleep(2.0) # Buffer to account for Arduino startup
print("[MAIN] Flushing serial buffers.")
ser.reset_input_buffer()
ser.reset_output_buffer()
initialize_stand(ser)
initialize_camera()
time.sleep(2.0) # Buffer to ensure the stand is not moving
current_angle = 0.0
while current_angle < TOTAL_TARGET_DEGREES:
take_photo()
rotate_stand(ser)
current_angle += ROTATION_STEP_SIZE
except requests.Timeout:
print("Connection to camera API lost.")
except SerialTimeoutError:
print("Arduino failed to respond in time.")
print("[MAIN] Done!")
if __name__ == "__main__":
main()
# Send `m1` command
# Response: "Mode: MANUAL | State: WAIT | Steps Left: 0 | Speed: 45.00 deg/s | Wait: 8.00s"
# Send `s45` command
# 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)."