Skip to content

Commit 19956d7

Browse files
committed
Add comprehensive Stepper motor class with multiple API approaches
Core Implementation: • Complete Stepper class supporting 4-pin stepper motors (28BYJ-48 + ULN2003) • Three step sequences: wave, full, half-step for different torque/smoothness needs • Position tracking with step counting and angle calculation • Configurable steps-per-revolution and step delays for different motors API Design Philosophy - Four Complementary Approaches: 1. DEFAULT DIRECTION (Simplest) • step(10) - Uses default clockwise direction • Minimizes cognitive load for basic use cases 2. CONVENIENCE METHODS (Explicit Intent) • step_clockwise(10), step_counterclockwise(10) • rotate_clockwise(90), rotate_counterclockwise(90) • revolve_clockwise(2), revolve_counterclockwise(2) • Short aliases: step_cw(), rotate_ccw(), revolve_cw() • Clear, readable, self-documenting code 3. PARAMETERIZED METHODS (Flexible Control) • step(10, direction='cw'|'ccw'|1|-1) • rotate(90, direction='clockwise'|'counter-clockwise') • revolution(2, direction=1|-1) • Supports both string and numeric direction parameters • Flexible _normalize_direction() handles multiple formats 4. ALIAS SUPPORT (Compatibility) • revolve() as alias for revolution() • StepperMotor as backward-compatible class alias • Maintains consistency with existing picozero patterns Technical Features: • Polymorphic direction parameters (string/numeric) with validation • Real-time speed control via step_delay property • Position reset capability for homing/calibration • Proper resource management with off() and close() methods • Comprehensive error handling with descriptive messages Testing Strategy: • Validates all API approaches for consistency • Covers edge cases and error conditions Documentation: • Added Stepper class to API documentation • Exported in __init__.py for public access • Comprehensive docstrings with parameter details • Usage examples showing all API patterns
1 parent dcd67a4 commit 19956d7

File tree

6 files changed

+634
-0
lines changed

6 files changed

+634
-0
lines changed

docs/api.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ Servo
6363
:inherited-members:
6464
:members:
6565

66+
Stepper
67+
-------
68+
69+
.. autoclass:: Stepper
70+
:show-inheritance:
71+
:inherited-members:
72+
:members:
73+
6674
Motor
6775
-----
6876

docs/examples/stepper.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from picozero import Stepper
2+
from time import sleep
3+
4+
stepper = Stepper((1, 2, 3, 4))
5+
6+
print(f"Starting position: {stepper.step_count} steps, {stepper.angle:.1f} degrees")
7+
sleep(0.5)
8+
print()
9+
10+
print("Using step() with default clockwise direction:")
11+
stepper.step(10)
12+
print(f"Position after step(10): {stepper.step_count} steps")
13+
sleep(0.5)
14+
print()
15+
16+
print("step_clockwise(20):")
17+
stepper.step_clockwise(20)
18+
print(f"Position: {stepper.step_count} steps")
19+
sleep(0.5)
20+
print()
21+
22+
print("step_counterclockwise(15):")
23+
stepper.step_counterclockwise(15)
24+
print(f"Position: {stepper.step_count} steps")
25+
sleep(0.5)
26+
print()
27+
28+
print("step_ccw(5) - short alias:")
29+
stepper.step_ccw(5)
30+
print(f"Position: {stepper.step_count} steps")
31+
sleep(0.5)
32+
print()
33+
34+
print("step(10, direction=1) - numeric:")
35+
stepper.step(10, direction=1)
36+
print(f"Position: {stepper.step_count} steps")
37+
print()
38+
39+
print("step(10, direction='cw') - string abbreviation:")
40+
stepper.step(10, direction='cw')
41+
print(f"Position: {stepper.step_count} steps")
42+
sleep(0.5)
43+
print()
44+
45+
print("step(10, direction='clockwise') - full string:")
46+
stepper.step(10, direction='clockwise')
47+
print(f"Position: {stepper.step_count} steps")
48+
sleep(0.5)
49+
print()
50+
51+
print("rotate_clockwise(90) - 90 degrees CW:")
52+
stepper.rotate_clockwise(90)
53+
print(f"Position: {stepper.step_count} steps, {stepper.angle:.1f} degrees")
54+
sleep(0.5)
55+
print()
56+
57+
print("rotate(45, direction='ccw') - 45 degrees CCW:")
58+
stepper.rotate(45, direction='ccw')
59+
print(f"Position: {stepper.step_count} steps, {stepper.angle:.1f} degrees")
60+
sleep(0.5)
61+
print()
62+
63+
print("revolve_clockwise() - 1 full revolution CW:")
64+
stepper.revolve_clockwise() # Default is 1 revolution
65+
print(f"Position: {stepper.step_count} steps, {stepper.angle:.1f} degrees")
66+
sleep(0.5)
67+
print()
68+
69+
print("revolve(0.5, direction=-1) - half revolution CCW:")
70+
stepper.revolve(0.5, direction=-1)
71+
print(f"Position: {stepper.step_count} steps, {stepper.angle:.1f} degrees")
72+
sleep(0.5)
73+
print()
74+
75+
print("Resetting position to home (0 steps, 0°)...")
76+
stepper.reset_position()
77+
print(f"After reset: {stepper.step_count} steps, {stepper.angle:.1f} degrees")
78+
sleep(0.5)
79+
print()
80+
81+
print()
82+
print("Final demonstration - all methods achieve the same result:")
83+
for i, (method_name, method_call) in enumerate([
84+
("Default direction", lambda: stepper.step(5)),
85+
("Numeric direction", lambda: stepper.step(5, direction=1)),
86+
("String direction", lambda: stepper.step(5, direction='cw')),
87+
("Convenience method", lambda: stepper.step_clockwise(5))
88+
], 1):
89+
print(f"{i}. {method_name}: 5 steps clockwise")
90+
method_call()
91+
92+
print(f"All methods reached: {stepper.step_count} steps total")
93+
94+
# Turn off motor when done
95+
stepper.off()
96+
stepper.close()
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from picozero import Stepper, pico_led
2+
from time import sleep
3+
4+
# Create devices
5+
stepper = Stepper((1, 2, 3, 4), step_sequence="half", step_delay=0.003)
6+
7+
print("Stepper Motor Positioning Demo")
8+
print("Press Ctrl+C to exit")
9+
print()
10+
11+
# Define preset positions in degrees
12+
positions = [0, 90, 180, 270, 360, 270, 180, 90] # Forward and return cycle
13+
position_names = ["Home", "90° CW", "180° CW", "270° CW", "Full rotation", "270° CCW", "180° CCW", "90° CCW"]
14+
15+
def move_to_position(target_angle, description=""):
16+
"""Move stepper to target angle from current position."""
17+
current_angle = stepper.angle
18+
angle_diff = target_angle - current_angle
19+
20+
print(f"Target: {description} ({target_angle}°)")
21+
print(f" Moving from {current_angle:.1f}° to {target_angle}°")
22+
pico_led.on() # Indicate movement
23+
24+
if angle_diff > 0:
25+
print(f" → Rotating {angle_diff}° clockwise...")
26+
stepper.rotate_clockwise(angle_diff)
27+
elif angle_diff < 0:
28+
print(f" → Rotating {-angle_diff}° counter-clockwise...")
29+
stepper.rotate_counterclockwise(-angle_diff)
30+
else:
31+
print(" → Already at target position")
32+
33+
pico_led.off() # Movement complete
34+
print(f" ✓ Position: {stepper.angle:.1f}° ({stepper.step_count} steps)")
35+
print()
36+
37+
def demonstrate_positioning_methods():
38+
"""Demonstrate different positioning approaches."""
39+
print("=== POSITIONING METHOD COMPARISON ===")
40+
print()
41+
42+
# Method 1: Using convenience methods (current approach)
43+
print("Method 1: Convenience methods (rotate_clockwise/rotate_counterclockwise)")
44+
stepper.reset_position()
45+
move_to_position(90, "90° using rotate_clockwise")
46+
move_to_position(45, "45° using rotate_counterclockwise")
47+
48+
sleep(1)
49+
50+
# Method 2: Using parameterized methods
51+
print("Method 2: Parameterized methods (rotate with direction parameter)")
52+
stepper.reset_position()
53+
54+
print("Target: 120° using rotate(120, direction='cw')")
55+
stepper.rotate(120, direction='cw')
56+
print(f" ✓ Position: {stepper.angle:.1f}°")
57+
58+
print("Target: 60° using rotate(60, direction='ccw')")
59+
stepper.rotate(60, direction='ccw')
60+
print(f" ✓ Position: {stepper.angle:.1f}°")
61+
print()
62+
63+
sleep(1)
64+
65+
# Start demonstration
66+
stepper.reset_position()
67+
print(f"Starting position: {stepper.angle}° ({stepper.step_count} steps)")
68+
print()
69+
70+
try:
71+
# Demonstrate positioning methods
72+
demonstrate_positioning_methods()
73+
74+
# Main positioning sequence
75+
print("=== AUTOMATIC POSITIONING SEQUENCE ===")
76+
print("Moving through preset positions...")
77+
print()
78+
79+
for i, (target_pos, name) in enumerate(zip(positions, position_names)):
80+
print(f"Step {i+1}/8:")
81+
move_to_position(target_pos, name)
82+
sleep(1.5)
83+
84+
print("=== ADVANCED POSITIONING DEMO ===")
85+
print("Demonstrating precise positioning capabilities...")
86+
print()
87+
88+
# Reset and demonstrate fractional positioning
89+
stepper.reset_position()
90+
91+
# Small precise movements
92+
precise_positions = [22.5, 67.5, 112.5, 157.5, 202.5]
93+
for pos in precise_positions:
94+
move_to_position(pos, f"Precise positioning to {pos}°")
95+
sleep(1)
96+
97+
# Return home
98+
move_to_position(0, "Return to home")
99+
100+
print("Positioning demonstration complete!")
101+
102+
except KeyboardInterrupt:
103+
print("\nInterrupted by user")
104+
105+
finally:
106+
# Clean shutdown
107+
stepper.off()
108+
pico_led.off()
109+
stepper.close()
110+
pico_led.close()

picozero/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
RGBLED,
2424
Motor,
2525
Robot,
26+
Stepper,
2627
Servo,
2728

2829
DigitalInputDevice,

0 commit comments

Comments
 (0)