Compare commits
14 Commits
279a5cf539
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cf732d65b | |||
| 9508f6a81d | |||
| 55d774d75b | |||
| 07dbfe4570 | |||
| 2a5174c5d1 | |||
| 55c70ca2a8 | |||
| 7922354cec | |||
| 200a500289 | |||
| 49a2a5b3bd | |||
| e06737fcd1 | |||
| 4030241716 | |||
| 82a2c26bee | |||
| 6e490ed822 | |||
| ff9da87a23 |
@@ -0,0 +1 @@
|
||||
.vscode
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
[tool.black]
|
||||
line-length = 80
|
||||
@@ -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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -18,13 +18,17 @@ int stepSequence[4][4] = {
|
||||
const int STEPS_PER_REVOLUTION = 2048; // 28BYJ-48 standard (512 steps * 4 gear reduction)
|
||||
const float DEGREES_PER_STEP = 360.0 / STEPS_PER_REVOLUTION;
|
||||
|
||||
// Speed Limits (Clamping)
|
||||
const float MAX_TURN_SPEED = 45.0; // Maximum safe speed in deg/s
|
||||
const float MIN_TURN_SPEED = 0.1; // Minimum safe speed to avoid div by zero
|
||||
|
||||
// --- Global Variables ---
|
||||
enum State { MOVE, WAIT };
|
||||
enum Mode { AUTO, MANUAL };
|
||||
|
||||
Mode autoMode = AUTO; // Default: AUTO
|
||||
float targetAngle = 0.0; // Target angle in degrees
|
||||
float turnSpeed = 5.0; // Speed in degrees per second
|
||||
float targetAngle = 15.0; // Target angle in degrees
|
||||
float turnSpeed = 45.0; // Speed in degrees per second
|
||||
|
||||
State _currState = MOVE; // Default: MOVE
|
||||
|
||||
@@ -49,10 +53,6 @@ void setStep(int a, int b, int c, int d) {
|
||||
}
|
||||
|
||||
void updateStepInterval() {
|
||||
if (turnSpeed <= 0) {
|
||||
_stepInterval = 0.1; // Fallback to avoid div by zero
|
||||
return;
|
||||
}
|
||||
// Calculate steps per second: speed / degrees_per_step
|
||||
float stepsPerSecond = turnSpeed / DEGREES_PER_STEP;
|
||||
_stepInterval = 1.0 / stepsPerSecond; // Seconds per step
|
||||
@@ -104,6 +104,34 @@ void runStep() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Serial Output Helpers ---
|
||||
|
||||
void printStatus() {
|
||||
Serial.print("Mode: ");
|
||||
Serial.print(autoMode == AUTO ? "AUTO" : "MANUAL");
|
||||
Serial.print(" | State: ");
|
||||
Serial.print(_currState == MOVE ? "MOVE" : "WAIT");
|
||||
Serial.print(" | Steps Left: ");
|
||||
Serial.print(_stepsRemaining);
|
||||
Serial.print(" | Speed: ");
|
||||
Serial.print(turnSpeed);
|
||||
Serial.print(" deg/s | Wait: ");
|
||||
Serial.print(waitInterval);
|
||||
Serial.println("s");
|
||||
}
|
||||
|
||||
void printCommandAck(char cmd, String payload) {
|
||||
Serial.print("> Command '");
|
||||
Serial.print(cmd);
|
||||
Serial.print(payload);
|
||||
Serial.println("' received.");
|
||||
}
|
||||
|
||||
void printError(String msg) {
|
||||
Serial.print("ERROR: ");
|
||||
Serial.println(msg);
|
||||
}
|
||||
|
||||
// --- Setup ---
|
||||
void setup() {
|
||||
// Initialize pins
|
||||
@@ -116,6 +144,8 @@ void setup() {
|
||||
Serial.begin(9600);
|
||||
while (!Serial) { ; } // Wait for serial port (optional for native USB)
|
||||
|
||||
Serial.println("=== Stepper Motor Controller Initialized ===");
|
||||
|
||||
// Initial State Setup
|
||||
autoMode = AUTO;
|
||||
waitInterval = 8.0;
|
||||
@@ -127,10 +157,16 @@ void setup() {
|
||||
_waitTime = millis();
|
||||
|
||||
// Initial calculation (move 0 degrees to set defaults)
|
||||
calculateStepsForAngle(0.0);
|
||||
// Ensure we start in MOVE if we want immediate action, or WAIT if idle
|
||||
// Based on your logic: "initialized in MOVE state"
|
||||
calculateStepsForAngle(targetAngle);
|
||||
_currState = MOVE;
|
||||
|
||||
Serial.println("Ready. Commands: m[0|1], a[deg], s[deg/s], i[sec]");
|
||||
Serial.println(" (Speed clamped between ");
|
||||
Serial.print(MIN_TURN_SPEED);
|
||||
Serial.print(" and ");
|
||||
Serial.print(MAX_TURN_SPEED);
|
||||
Serial.println(" deg/s)");
|
||||
printStatus();
|
||||
}
|
||||
|
||||
// --- Loop ---
|
||||
@@ -140,27 +176,46 @@ void loop() {
|
||||
String command = Serial.readString();
|
||||
command.trim(); // Remove whitespace
|
||||
|
||||
if (command.length() < 2) return; // Need at least 'a' + number
|
||||
if (command.length() < 2) {
|
||||
printError("Invalid command format (too short).");
|
||||
return;
|
||||
}
|
||||
|
||||
char cmdType = command.charAt(0);
|
||||
String payload = command.substring(1);
|
||||
|
||||
printCommandAck(cmdType, payload);
|
||||
|
||||
switch (cmdType) {
|
||||
case 'm':
|
||||
// 'm0' for AUTO, 'm1' for MANUAL
|
||||
if (payload.length() > 0) {
|
||||
char modeChar = payload.charAt(0);
|
||||
if (modeChar == '0' && autoMode != AUTO) {
|
||||
autoMode = AUTO;
|
||||
_currState = MOVE;
|
||||
_stepsRemaining = _stepCount;
|
||||
_stepTime = millis();
|
||||
} else if (modeChar == '1' && autoMode != MANUAL) {
|
||||
autoMode = MANUAL;
|
||||
_currState = MOVE; // Start moving immediately on switch
|
||||
_stepsRemaining = _stepCount;
|
||||
_stepTime = millis();
|
||||
if (modeChar == '0') {
|
||||
if (autoMode != AUTO) {
|
||||
autoMode = AUTO;
|
||||
_currState = MOVE;
|
||||
_stepsRemaining = _stepCount;
|
||||
_stepTime = millis();
|
||||
Serial.println(" -> Mode switched to AUTO. Starting move cycle.");
|
||||
} else {
|
||||
Serial.println(" -> Already in AUTO mode.");
|
||||
}
|
||||
} else if (modeChar == '1') {
|
||||
if (autoMode != MANUAL) {
|
||||
autoMode = MANUAL;
|
||||
_currState = MOVE; // Start moving immediately on switch
|
||||
_stepsRemaining = _stepCount;
|
||||
_stepTime = millis();
|
||||
Serial.println(" -> Mode switched to MANUAL. Moving once, then waiting.");
|
||||
} else {
|
||||
Serial.println(" -> Already in MANUAL mode.");
|
||||
}
|
||||
} else {
|
||||
printError("Invalid mode. Use 'm0' (AUTO) or 'm1' (MANUAL).");
|
||||
}
|
||||
} else {
|
||||
printError("Missing mode argument (0 or 1).");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -168,7 +223,20 @@ void loop() {
|
||||
// 'a90' -> move 90 degrees
|
||||
{
|
||||
float angle = payload.toFloat();
|
||||
calculateStepsForAngle(angle);
|
||||
if (payload.length() == 0 || (angle == 0 && payload != "0")) {
|
||||
printError("Invalid angle format.");
|
||||
} else {
|
||||
calculateStepsForAngle(angle);
|
||||
targetAngle = angle;
|
||||
Serial.print(" -> Moving ");
|
||||
Serial.print(_stepCount);
|
||||
Serial.print(" steps (");
|
||||
Serial.print(targetAngle);
|
||||
Serial.println(" degrees).");
|
||||
if (_stepCount == 0) {
|
||||
Serial.println(" -> No movement required (0 degrees).");
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -176,10 +244,27 @@ void loop() {
|
||||
// 's10' -> 10 deg/s
|
||||
{
|
||||
float speed = payload.toFloat();
|
||||
if (speed > 0) {
|
||||
if (payload.length() == 0 || speed <= 0) {
|
||||
printError("Invalid speed. Must be > 0.");
|
||||
} else {
|
||||
turnSpeed = speed;
|
||||
|
||||
// Logic to check if clamping occurred
|
||||
float actualSpeed = turnSpeed;
|
||||
if (actualSpeed > MAX_TURN_SPEED) {
|
||||
turnSpeed = MAX_TURN_SPEED;
|
||||
Serial.print(" -> WARNING: Speed clamped to max ");
|
||||
} else if (actualSpeed < MIN_TURN_SPEED) {
|
||||
turnSpeed = MIN_TURN_SPEED;
|
||||
Serial.print(" -> WARNING: Speed clamped to min ");
|
||||
}
|
||||
|
||||
updateStepInterval();
|
||||
// If currently in MOVE, the new interval applies on next step
|
||||
Serial.print(" -> Speed set to ");
|
||||
Serial.print(turnSpeed);
|
||||
Serial.print(" deg/s (Step interval: ");
|
||||
Serial.print(_stepInterval * 1000, 2);
|
||||
Serial.println(" ms).");
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -188,12 +273,24 @@ void loop() {
|
||||
// 'i5' -> 5 seconds wait
|
||||
{
|
||||
float interval = payload.toFloat();
|
||||
if (interval >= 0) {
|
||||
if (payload.length() == 0 || interval < 0) {
|
||||
printError("Invalid interval. Must be >= 0.");
|
||||
} else {
|
||||
waitInterval = interval;
|
||||
Serial.print(" -> Wait interval set to ");
|
||||
Serial.print(waitInterval);
|
||||
Serial.println(" seconds.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
printError("Unknown command. Use m, a, s, or i.");
|
||||
break;
|
||||
}
|
||||
|
||||
// Print status after every command for immediate feedback
|
||||
printStatus();
|
||||
}
|
||||
|
||||
// --- EXECUTION LOGIC ---
|
||||
@@ -202,6 +299,14 @@ void loop() {
|
||||
if (_currState == MOVE && _stepsRemaining == 0) {
|
||||
_currState = WAIT;
|
||||
_waitTime = millis();
|
||||
Serial.println("--- State: MOVE -> WAIT ---");
|
||||
if (autoMode == AUTO) {
|
||||
Serial.print(" Waiting ");
|
||||
Serial.print(waitInterval);
|
||||
Serial.println(" seconds before next cycle...");
|
||||
} else {
|
||||
Serial.println(" Waiting indefinitely for new command (MANUAL mode).");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check if we should restart in AUTO mode
|
||||
@@ -210,6 +315,10 @@ void loop() {
|
||||
_currState = MOVE;
|
||||
_stepsRemaining = _stepCount;
|
||||
_stepTime = millis(); // Reset step timer to prevent burst
|
||||
Serial.println("--- State: WAIT -> MOVE ---");
|
||||
Serial.print(" Auto-restart! Moving ");
|
||||
Serial.print(_stepsRemaining);
|
||||
Serial.println(" steps.");
|
||||
}
|
||||
// If MANUAL: Stay in WAIT indefinitely until new command
|
||||
}
|
||||
@@ -221,6 +330,14 @@ void loop() {
|
||||
runStep();
|
||||
_stepTime = currentTime;
|
||||
_stepsRemaining--;
|
||||
|
||||
// Optional: Print progress every 10 steps to avoid spam
|
||||
// if (_stepCount > 0 && (_stepCount - _stepsRemaining) % 10 == 0) {
|
||||
// Serial.print(" Step: ");
|
||||
// Serial.print(_stepCount - _stepsRemaining);
|
||||
// Serial.print("/");
|
||||
// Serial.println(_stepCount);
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
+204
@@ -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)."
|
||||
Reference in New Issue
Block a user