Skip to content

Commit 107b4c4

Browse files
committed
Add delay_time param to move
Allow for optional delay before the start of a move. Especially useful for sequences of moves where pauses with no movement are desired between moves. Corresponding and general fixes to examples.
1 parent adc3083 commit 107b4c4

8 files changed

+86
-62
lines changed

README.md

+17-9
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
## Description
44
This Python library is descended from the VarspeedServo library (https://github.com/netlabtoolkit/VarSpeedServo), originally written for the Arduino in C++ (which was itself built on an early Arduino servo library). Unlike the old Arduino library, VarSpeedPython is not tied to servos, and can be used more generally for timed moves from one value to another. It is also **not** bound to any processor architecture with hardware interrupts etc.
55

6-
The library is designed for projects that need to control values over time. For example: setting the new angle of a servo in 1.5 seconds; setting the brightness of an LED by fading up; or moving a graphic on a screen. You can set the amount of time for a change in value, and apply easing to each move. It also provides a function for running sequences of moves (that can be looped or repeated if desired), where each move in the sequence has a new position and speed. More than one move or sequence can be run at the same time.
6+
The library is designed for projects that need to control values over time. For example: setting the new angle of a servo in 3.0 seconds; setting the brightness of an LED by fading up in 2.5 seconds; or moving a graphic on a screen. You can set the amount of time for a change in value, and apply easing to each move to make it seem more natural.
7+
8+
It also provides a function for running **sequences** of moves (that can be looped or repeated if desired), where each move in the sequence has a new position and speed. More than one move or sequence can be run at the same time.
79

810
VarSpeedPython objects are designed to be called repeatedly from within an event loop and do not block execution.
911

@@ -25,11 +27,11 @@ VarSpeedPython objects are designed to be called repeatedly from within an event
2527

2628
## Circuit Python Setup
2729

28-
To set up on CircuitPython:
30+
To set up on a CircuitPython hardware device:
2931

30-
* Put varspeed.py in the lib directory on CIRCUITPY
31-
* Put easing_functions.py in the lib directory on CIRCUITPY
32-
* Copy the python from any of the examples into code.py at the top of CIRCUITPY
32+
* Put varspeed/varspeed.py in the **lib** directory on CIRCUITPY
33+
* Put varspeed/easing_functions.py in the **lib** directory on CIRCUITPY
34+
* Copy the python code from any of the examples/ into code.py or main.py at the top of CIRCUITPY
3335

3436
## Code Examples - non-CircuitPython
3537

@@ -46,10 +48,10 @@ To set up on CircuitPython:
4648

4749
## Easing Types
4850

49-
For any move (even within a sequence), you can set an easing function using the following classic Robert Penner easing types. For an animated and graphed visualization of each easing type, see https://easings.net. For an explanation of the use of easing, see this article: [Animation Principles in UI Design: Understanding Easing](https://medium.com/motion-in-interaction/animation-principles-in-ui-design-understanding-easing-bea05243fe3)
51+
For any move (even within a sequence), you can set an easing function using the following classic Robert Penner easing types. For an animated and graphed visualization of each easing type, see [https://easings.net](https://easings.net). For an explanation of the use of easing, see this article: [Animation Principles in UI Design: Understanding Easing](https://medium.com/motion-in-interaction/animation-principles-in-ui-design-understanding-easing-bea05243fe3)
5052

5153
> [!WARNING]
52-
> The names on https://easings.net do not reflect the names used in the `easing_functions.py` file. The names used in this project start with the type of easing used by the function (e.g Linear, Quad, Circular) followed by the word _Ease_ and finally whether it's "In", "Out" or "InOut". For example: the function listed on the website as _easeInOutCubic_ is called _CubicEaseInOut_ in this library. The reason behind this naming scheme is to be consistent with python classes naming conventions. The list with all easing functions with the correct names is listed here below.
54+
> The names on https://easings.net do not reflect the names used in the `easing_functions.py` file. The names used in this project start with the type of easing used by the function (e.g Linear, Quad, Circular) followed by the word _Ease_ and finally whether it's "In", "Out" or "InOut". For example: the function listed on the website as _easeInOutCubic_ is called _CubicEaseInOut_ in this library. The reason behind this naming scheme is to be consistent with python classes naming conventions. The list with all this library's easing functions with the correct names are listed below. Note also that the new gamma functions are not listed on the easings.net page
5355
5456
* LinearInOut (essentially no easing)
5557
* QuadEaseInOut, QuadEaseIn, QuadEaseOut
@@ -62,6 +64,11 @@ For any move (even within a sequence), you can set an easing function using the
6264
* ElasticEaseIn, ElasticEaseOut, ElasticEaseInOut
6365
* BackEaseIn, BackEaseOut, BackEaseInOut
6466
* BounceEaseIn, BounceEaseOut, BounceEaseInOut
67+
* GammaEaseIn, GammaEaseOut, GammaEaseInOut
68+
69+
70+
* **NEW: GAMMA EASING** - provides gamma correction of 2.8 for dimming LEDs and other lighting to match human vision characteristics. **EXPLANATION** of gamma correction: https://www.advateklighting.com/blog/guides/dithering-and-gamma-correction
71+
6572

6673
## CLASS: Vspeed
6774
```python
@@ -92,7 +99,7 @@ Creates and initializes a Vspeed object.
9299

93100
## move
94101
```python
95-
def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearInOut"):
102+
def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearInOut", delay_start = 0.0):
96103
```
97104

98105
---
@@ -104,6 +111,7 @@ Generates a series of values that transition from the current position to a new_
104111
* **time_secs** (int) : time for the transition to the new_position
105112
* **steps** (int) : number of steps to change from the start position to the new_position
106113
* **easing** (string) : the easing function to use for the transition
114+
* **delay_start** (float) : the number of seconds to delay the start of the move - this helps when putting several moves together in a sequence so there can be pauses between moves
107115

108116
### Returns
109117
* **position** (int or float) : each new position, marked by changed being True
@@ -120,7 +128,7 @@ def sequence(self, sequence, loop_max = 1):
120128
Creates a series of values in a sequence of moves as specified in the sequence array.
121129

122130
### Args
123-
* **sequence** (array of tuples) : perform a sequence of moves (position,time,steps,easing) in the array.
131+
* **sequence** (array of tuples) : perform a sequence of moves (position,time,steps,easing,delay_start[optiona]) in the array.
124132
* **loop_max** (int) : how many times to loop the sequence, zero means loop forever.
125133

126134
### Returns

examples/move_simple.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
# move_simple.py
22
#
33
# a non-hardware dependent example of using the VarSpeedPython class
4-
# to ramp a value from one value to another
4+
# to ramp a value from one level to another
55
#
6-
import time
7-
86
from varspeed import Vspeed
97

108
MIN = 0
@@ -15,6 +13,7 @@
1513
# init_position = initial start position
1614
# result = float, int
1715
# debug = False, True # set if varspeed will output debug info
16+
1817
vs = Vspeed(init_position=MIN, result="int", debug=False)
1918

2019
running = True

examples/move_simple_servo.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# move_simple_servo.py
22
#
33
# changes the position of a servo over time
4-
#
5-
import time
4+
65
import board
76
import pwmio
87
from adafruit_motor import servo
@@ -20,26 +19,35 @@
2019
# make the output of the function be within the MIN MAX bounds set
2120
vs.set_bounds(lower_bound=MIN, upper_bound=MAX)
2221

23-
# create a PWMOut object on Pin D4
22+
# create a PWMOut object on Pin D13
2423
pwm = pwmio.PWMOut(board.D13, duty_cycle=2 ** 15, frequency=50)
2524

2625
# Create a servo object
2726
my_servo = servo.Servo(pwm)
28-
29-
print(f'Paused for 5 seconds at {MIN}...')
3027
# set the servo to a known starting point
3128
my_servo.angle = MIN
32-
time.sleep(5)
29+
print(f'Starting at position {MIN}, in 3 seconds starting move to {MAX}...')
3330

34-
print(f'Moving to {MAX}...')
3531
running = True
3632
while running:
37-
# move(new_position,time_secs of move,steps in move,easing function)
33+
# move(new_position,time_secs of move,steps in move,easing function, delay_start in seconds)
3834
# for more into on easing, see: https://github.com/semitable/easing-functions
39-
# for a visual repesentation of the easing options see: http://www.emix8.org/forum/viewtopic.php?t=1063
35+
# for a visual repesentation of the easing options see: https://easings.net
4036
#
41-
# move from the current position
37+
# move from the current MIN position
4238
position, running, changed = vs.move(
43-
new_position=MAX, time_secs=2, steps=180, easing="CubicEaseInOut")
39+
new_position=MAX, time_secs=2, steps=180, easing="ExponentialEaseInOut", delay_start=3.0)
4440
if changed: # only act if the output changed
4541
my_servo.angle = position
42+
43+
running = True
44+
while running:
45+
# move(new_position,time_secs of move,steps in move,easing function, delay_start in seconds)
46+
# for more into on easing, see: https://github.com/semitable/easing-functions
47+
# for a visual repesentation of the easing options see: https://easings.net
48+
#
49+
# move from the current MIN position
50+
position, running, changed = vs.move(
51+
new_position=MIN, time_secs=2, steps=180, easing="ExponentialEaseInOut", delay_start=3.0)
52+
if changed: # only act if the output changed
53+
my_servo.angle = position

examples/sequence_simple.py

-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
# a non-hardware dependent example of using the VarSpeedPython class
44
# to have a series of moves in a sequence
55
#
6-
import time
7-
86
from varspeed import Vspeed
97

108
MIN = 0.0

examples/sequence_simple_servo.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
# set the servo to a known starting point
3030
my_servo.angle = MIN
3131

32-
my_sequence = [(MAX / 2, 2.0, 100, "QuadEaseIn"),
33-
(MIN, 2.0, 100, "QuadEaseOut"),
34-
(MAX, 2.0, 100, "SineEaseInOut")]
32+
# note that these sequence moves may include a last delay_start parameter
33+
my_sequence = [(MAX, 2.0, 100, "QuadEaseIn"), # missing delay_start parameter defaults to 0.0
34+
(MIN, 2.0, 100, "QuadEaseOut",3), # waits 3 seconds to start
35+
(MAX, 2.0, 100, "SineEaseInOut",2), # waits 2 seconds to start
36+
]
3537

3638
running = True
3739
while running:
@@ -44,10 +46,7 @@
4446
# if 1, play once
4547
# if >1, loop sequence that many times
4648
#
47-
position, running, changed = vs.sequence(sequence=my_sequence, loop_max=2)
49+
position, running, changed = vs.sequence(sequence=my_sequence, loop_max=1)
4850

49-
#print(position, running, changed)
5051
if changed:
51-
# print(
52-
# f'Sequence Num: {vs.seq_pos}, Step: {vs.step}, Position: {position}')
5352
my_servo.angle = position

examples/two_sequences_at_once.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
(MAX, 1.0, 10, "QuadEaseOut")]
2323

2424
my_sequence2 = [(MAX, 1.0, 10, "QuadEaseOut"),
25-
(MIN, 1.0, 10, "SineEaseInOut")]
25+
(MIN, 1.0, 10, "QuadEaseIn")]
2626

2727
running1 = True
2828
running2 = True
@@ -41,10 +41,10 @@
4141
sequence=my_sequence1, loop_max=2)
4242
if changed1:
4343
print(
44-
f'Sequence 1 Num: {vs1.seq_pos}, Step: {vs1.step}, Position: {position1}')
44+
f'Sequence 1 Move: {vs1.seq_pos}, Step: {vs1.step}, Position: {position1}')
4545

4646
position2, running2, changed2 = vs2.sequence(
4747
sequence=my_sequence2, loop_max=2)
4848
if changed2:
4949
print(
50-
f'Sequence 2 Num: {vs2.seq_pos}, Step: {vs2.step}, Position: {position2}')
50+
f'Sequence 2 Move: {vs2.seq_pos}, Step: {vs2.step}, Position: {position2}')

examples/two_sequences_at_once_servo.py

+15-15
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
#
33
# an example of how to use two sequences at the same time without blocking either one
44
#
5-
import time
65
import board
76
import pwmio
87
from adafruit_motor import servo
98
from varspeed import Vspeed
109

11-
MIN = 20
12-
MAX = 180
10+
MIN = 15
11+
MAX = 165
1312

1413
# create a PWMOut object on Pin D13
1514
pwm1 = pwmio.PWMOut(board.D13, duty_cycle=2 ** 15, frequency=50)
@@ -33,36 +32,37 @@
3332

3433
# set the servo to a known starting point
3534
my_servo1.angle = vs1.position
35+
my_servo2.angle = vs2.position
3636

37-
my_sequence1 = [(MIN, 1, 100, "QuadEaseIn"),
38-
(MAX, 1, 100, "QuadEaseOut")]
37+
my_sequence1 = [(MIN, 1, 100, "QuadEaseIn",2),
38+
(MAX, 1, 100, "QuadEaseIn",2)]
3939

40-
my_sequence2 = [(MAX, 1, 100, "QuadEaseIn"),
41-
(MIN, 1, 100, "QuadEaseOut")]
40+
my_sequence2 = [(MAX, 1, 100, "QuadEaseIn",2),
41+
(MIN, 1, 100, "QuadEaseIn",2)]
4242

4343
running1 = True
4444
running2 = True
4545
#print("starting position",vs.position)
4646
while running1 and running2:
4747
# run a sequence of moves
4848
# sequence(sequence,loop,loop_max)
49-
# sequence = moves in this format: (next-position,secs-to-move,number-of-steps,easing function)
50-
# example: [(90,5,10,LinearInOut),(0,8,10,QuadEaseInOut),(180,5,10,CubicEaseIn)]
49+
# sequence = moves in this format: (next-position,secs-to-move,number-of-steps,easing function,delay_start)
50+
# example: [(90,5,10,LinearInOut,2),(0,8,10,QuadEaseInOut,2),(180,5,10,CubicEaseIn,2)]
5151
# loop_max = number of times to run the sequence in a loop
5252
# if zero, loop forever
5353
# if 1, play once
5454
# if >1, loop sequence that many times
5555
#
5656
position1, running1, changed1 = vs1.sequence(
57-
sequence=my_sequence1, loop_max=5)
57+
sequence=my_sequence1, loop_max=3)
5858
if changed1:
59-
print(
60-
f'Sequence Num: {vs1.seq_pos}, Step: {vs1.step}, Position: {position1}')
59+
#print(
60+
# f'Sequence Num: {vs1.seq_pos}, Step: {vs1.step}, Position: {position1}')
6161
my_servo1.angle = position1
6262

6363
position2, running2, changed2 = vs2.sequence(
64-
sequence=my_sequence2, loop_max=5)
64+
sequence=my_sequence2, loop_max=3)
6565
if changed2:
66-
print(
67-
f'Sequence Num: {vs2.seq_pos}, Step: {vs2.step}, Position: {position2}')
66+
#print(
67+
# f'Sequence Num: {vs2.seq_pos}, Step: {vs2.step}, Position: {position2}')
6868
my_servo2.angle = position2

varspeed/varspeed.py

+24-12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def __init__(self, init_position = 0, result = "int", debug=False):
1111
Args:
1212
init_position (int/float): sets the initial position of the object.
1313
result (string = "int" or "float"): sets the type of the returned position.
14+
# debug (boolean False or True # set if varspeed will output debug info
1415
1516
Returns:
1617
object: returns a varspeed object
@@ -30,7 +31,6 @@ def __init__(self, init_position = 0, result = "int", debug=False):
3031
self.upper_bound = 1000
3132
self.lower_bound = 0
3233
self.last_reported_position = init_position
33-
self.easing_correction = 1
3434
self.result = result
3535
self.step = 0
3636
self.increment_seq_num = True
@@ -41,14 +41,16 @@ def __init__(self, init_position = 0, result = "int", debug=False):
4141
self.seq_loop_count = 0
4242
self.debug = debug
4343

44-
def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearInOut"):
45-
"""Generates a series of values that transition from the current position to a new_position
44+
def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearInOut", delay_start=0.0):
45+
"""MOVE: Generates a series of values that transition from the current position to a new_position,
46+
with a delay at the start of the move
4647
4748
Args:
4849
new_position (float): new position output will change to over time_secs
4950
time_secs (int): time for the transition to the new_position
5051
steps (int): number of steps to change from the start position to the new_position
5152
easing (string): the easing function to use for the transition
53+
delay_start (float): the number of seconds to delay the start of the move
5254
5355
Returns:
5456
position (int or float): each new position, marked by changed being True
@@ -57,9 +59,8 @@ def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearIn
5759
5860
"""
5961
if not self.started or new_position != self.new_position:
60-
if (self.debug): ("new move")
62+
if self.debug: print("new move")
6163
self.new_position = new_position
62-
#self.last_position = self.position
6364
self.start_time = time.monotonic()
6465
self.end_time = self.start_time + time_secs
6566
self.step_delay = time_secs / steps
@@ -70,17 +71,22 @@ def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearIn
7071
self.easing = easing
7172
self.easing_method = getattr(ease, self.easing)
7273
self.ease = self.easing_method(start=self.position, end=self.new_position, duration=steps)
74+
self.delay_start = delay_start
75+
self.delay_start_complete = False
76+
if self.debug: print("Delaying move by",self.delay_start,"secs")
7377

7478
changed = False
7579
running = True
7680
self.started = True
7781

78-
#if self.new_position != self.position or self.end_time < time.monotonic():
7982
diff_time = time.monotonic() - self.start_time
80-
if diff_time > self.step_delay:
83+
if diff_time >= self.delay_start and self.delay_start_complete == False:
84+
self.delay_start_complete = True
85+
diff_time = time.monotonic() - self.start_time
86+
if diff_time > self.step_delay and self.delay_start_complete:
8187
# time to change
8288
self.step += 1
83-
# if (self.debug): print("new step " ,self.step,diff_time,self.step_delay)
89+
# if self.debug: print("new step " ,self.step,diff_time,self.step_delay)
8490
self.start_time = time.monotonic()
8591
self.position = self.ease(self.step)
8692
# are we there yet?
@@ -93,7 +99,7 @@ def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearIn
9399
else:
94100
changed = True
95101

96-
# restrict the output to integer if needed
102+
# restrict the output to integer if needed (e.g. for a servo)
97103
if self.result == "int":
98104
position = round(self.position)
99105

@@ -112,10 +118,10 @@ def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearIn
112118
return self.position, running, changed
113119

114120
def sequence(self, sequence, loop_max = 1):
115-
"""Creates a series of values in a sequence of moves as specified in the sequence array
121+
"""SEQUENCE: Creates a series of values in a sequence of moves as specified in the sequence array
116122
117123
Args:
118-
sequence (array of tuples): perform a sequence of moves (position,time,steps,easing) in the array
124+
sequence (array of tuples): perform a sequence of moves (position,time,steps,easing,delay_start) in the array
119125
loop_max (int): how many time to loop the sequence, zero means loop forever
120126
121127
Returns:
@@ -154,11 +160,17 @@ def sequence(self, sequence, loop_max = 1):
154160
if self.debug: print("START sequence move",self.seq_pos,sequence[self.seq_pos])
155161
self.increment_seq_num = False
156162

163+
# if this sequence does not include a delay_start value (5th element), append it to the tupple
164+
if len(sequence[self.seq_pos]) < 5:
165+
sequence[self.seq_pos] = sequence[self.seq_pos] + (0.0,)
166+
# initiate the move
157167
position, running, changed = self.move(
158168
new_position=sequence[self.seq_pos][0],
159169
time_secs=sequence[self.seq_pos][1],
160170
steps=sequence[self.seq_pos][2],
161-
easing=sequence[self.seq_pos][3])
171+
easing=sequence[self.seq_pos][3],
172+
delay_start=sequence[self.seq_pos][4],
173+
)
162174

163175
if not running: # finished with move
164176
self.increment_seq_num = True

0 commit comments

Comments
 (0)