Release Notes: 2.0.0 implements the following changes:
- async version: Added a new version of the library (varspeed_async.py) that makes the library async compatible. For backward compatibility, the old sync version is still available as varspeed.py. Corresponding sync and async examples are included.
- map_range(): Added a new utility function map_range() that maps one value range to another. Useful for example in the case where an output requires a range of 0-180, and a sensor controlling that output returns in a range of zero to one.
- easing cheatsheet: New easing cheatsheet that correctly matches the library's easing function names. This new version (inspired by, and adapted from the old one. This new version shows a simple animation of a servo using that easing function), allows the user to change the target position, the number of steps, and the length of the move. Plus, the call needed for those parameters is displayed under the diagram.
- Examples: The examples for both sync and async libraries have been updated to be more consistent. There are also more async examples now to cover more use cases.
- Documentation Updated documention covering async and other improvements
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.
It also provides a function for running sequences of moves where each move in the sequence has a new position and speed. Sequences can be looped or repeated if desired.
More than one move or sequence can be run at the same time.
VarSpeedPython objects are designed to be called repeatedly and do not block execution. It is compatible with standard Python and CircuitPython (v8.0+ to support asyncio, more info here).
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.
Sync vs Async — which should I use?
Sync (
varspeed.py): best for beginners, existing curriculum, or CircuitPython without asyncio. Simple event-loop style — callmove()on every loop iteration.Async (
varspeed_async.py): use when you need concurrent actuators (e.g. a servo and an LED moving independently at the same time), or when you want to learnasync/awaitpatterns. Requires Python 3.9+ or CircuitPython withadafruit_asynciov3+.
- Description
- Installation
- Quick Start (async — start here)
- Quick Start (sync — start here)
- API Reference
- Easing Types
- Examples
- Circuit Python Setup
- Migration: Sync → Async
Computer (simple) — Copy the files you need from the varspeed/ directory into your project folder:
| You want | Files to copy |
|---|---|
| Sync | varspeed/varspeed.py + varspeed/easing_functions.py |
| Async | varspeed/varspeed_async.py + varspeed/easing_functions.py |
Then import directly:
from varspeed import Vspeed # sync
from varspeed_async import Vspeed # asyncComputer (repo clone) — Clone the repo and point PYTHONPATH at the varspeed/ directory:
git clone https://github.com/pvanallen/VarSpeedPython.git
cd VarSpeedPython
python -m venv .venv
source .venv/bin/activate
echo 'export PYTHONPATH="/path/to/VarSpeedPython/varspeed"' >> .venv/bin/activate
deactivate && source .venv/bin/activateDevice with CircuitPython — Copy varspeed/varspeed.py (sync) or varspeed/varspeed_async.py (async) and varspeed/easing_functions.py into the CIRCUITPY/lib/ directory. For the async version also add the asyncio lib folder from the Adafruit CircuitPython bundle. asyncio Requires CircuitPython v8+.
Uses varspeed_async.py — requires: from varspeed_async import Vspeed
import asyncio
from varspeed_async import Vspeed
vs = Vspeed(init_position=0, result="int")
async def main():
async for position, running, changed in vs.move(
new_position=100, time_secs=2.0, steps=20, easing="SineEaseInOut"
):
if changed:
print(position) # drive your actuator here
asyncio.run(main())Uses varspeed.py — requires: from varspeed import Vspeed
from varspeed import Vspeed
vs = Vspeed(init_position=0, result="int")
while True:
position, running, changed = vs.move(new_position=100, time_secs=2.0, steps=20, easing="SineEaseInOut")
if changed:
print(position) # drive your actuator here
if not running:
breakclass Vspeed():Provides a non-blocking object that can be called repeatedly from an event loop with the move() and sequence() functions to generate a timed series of values from a current position to a new position(s).
def __init__(self, init_position = 0, result = "int", debug = False):Creates and initializes a Vspeed object.
- init_position (int/float) : sets the initial position of the object.
- result (string = "int" or "float") : sets the type of the returned position.
- debug (True/False) : sets debugging mode on or off, resulting in prints on or off.
- object : returns a Vspeed object
sync only — requires varspeed.py
from varspeed import Vspeed
def move(self, new_position = 0, time_secs = 2.0, steps = 20, easing = "LinearInOut", delay_start = 0.0):Generates a series of values that transition from the current position to a new_position. Call this repeatedly from your event loop.
- new_position (float or int) : position output will change to over time_secs
- time_secs (int) : time for the transition to the new_position
- steps (int) : number of steps to change from the start position to the new_position
- easing (string) : the easing function to use for the transition
- delay_start (float) : the number of seconds to delay the start of the move
- position (int or float) : each new position, marked by changed being True
- running (Boolean) : if true there are more steps to go in the transition/move
- changed (Boolean) : indicates if the latest value is different from the previous value
async only — requires varspeed_async.py
from varspeed_async import Vspeed
async for position, running, changed in vs.move(new_position, time_secs, steps, easing):
...Async iterator that yields (position, running, changed) on every step, sleeping between steps. Replaces the sync move() — instead of calling in a loop, use async for.
Same as move() above (new_position, time_secs, steps, easing, delay_start).
- position (int or float) : each new position
- running (Boolean) : True if more steps remain
- changed (Boolean) : True if the value changed this step
def sequence(self, sequence, loop_max = 1):Creates a series of values in a sequence of moves as specified in the sequence array. In the async version (varspeed_async.py), this is an async iterator used with async for. In the sync version (varspeed.py), call it repeatedly from your event loop.
- sequence (array of tuples) : perform a sequence of moves — each tuple is
(position, time_secs, steps, easing[, delay_start]). - loop_max (int) : how many times to loop the sequence; zero means loop forever.
- position (int or float) : each new position, marked by changed being True
- running (Boolean) : if true there are more steps to go in the transition/move
- changed (Boolean) : indicates if the latest position value is different from the previous value
def sequence_change_seq_num(self, seq_position = 0):Sets the current sequence number.
- seq_position (int) : jump to the element in the sequence[seq_position] array.
nothing
def sequence_run(self, value = True):Pauses or unpauses a running sequence.
- value (Boolean) : pauses (False) or unpauses (True) the run of the sequence.
nothing
def set_position(self, position = 0):Sets the current position from which the next move will proceed.
- position (int or float) : sets the current position of the object
nothing
def set_bounds(self, lower_bound = 0, upper_bound = 1000, bounded=True):Sets the lower and upper bounds of values returned by a move or sequence.
- lower_bound (int) : sets the lower allowed bound of the output of a move or sequence
- upper_bound (int) : sets the upper allowed bound of the output of a move or sequence
- bounded (Boolean) : turns bounds checking on (True — Default) or off (False)
nothing
from varspeed import map_range # sync
from varspeed_async import map_range # async
map_range(value, in_min, in_max, out_min, out_max, result="float")Maps a value from one range to another. Useful for converting sensor readings to actuator ranges.
- value (int or float) : input value to map
- in_min (int or float) : lower bound of the input range
- in_max (int or float) : upper bound of the input range
- out_min (int or float) : lower bound of the output range
- out_max (int or float) : upper bound of the output range
- result (string) :
"int"to return a rounded integer,"float"for a float (default)
- int or float : the mapped value in the output range as a rounded int or float
For any move (even within a sequence), you can set an easing function using any of the following classic Robert Penner easing types. For an animated and graphed visualization of each easing type, see https://philvanallen.com/easings_cheatsheet/.
For an explanation of the use of easing, see this article: Animation Principles in UI Design: Understanding Easing
Easing names in this library start with the family name (e.g. Linear, Quad, Cubic) followed by Ease and the direction (In, Out, or InOut). For example, CubicEaseInOut. The Gamma functions are unique to this library and not found on other easing references.
-
LinearInOut (essentially no easing)
-
QuadEaseInOut, QuadEaseIn, QuadEaseOut
-
CubicEaseIn, CubicEaseOut, CubicEaseInOut
-
QuarticEaseIn, QuarticEaseOut, QuarticEaseInOut
-
QuinticEaseIn, QuinticEaseOut, QuinticEaseInOut
-
SineEaseIn, SineEaseOut, SineEaseInOut
-
CircularEaseIn, CircularEaseOut, CircularEaseInOut
-
ExponentialEaseIn, ExponentialEaseOut, ExponentialEaseInOut
-
ElasticEaseIn, ElasticEaseOut, ElasticEaseInOut
-
BackEaseIn, BackEaseOut, BackEaseInOut
-
BounceEaseIn, BounceEaseOut, BounceEaseInOut
-
GammaEaseIn, GammaEaseOut, GammaEaseInOut
-
GAMMA EASING NOTES - 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
async only — requires varspeed_async.py (from varspeed_async import Vspeed)
- async/move_simple.py — minimal async move, no hardware
- async/move_simple_led.py — fade an LED asynchronously
- async/move_simple_servo.py — move a servo asynchronously
- async/sequence_simple.py — run a sequence asynchronously, no hardware
- async/sequence_simple_servo.py — sequence of servo moves
- async/two_sequences_at_once.py — two sequences running simultaneously, no hardware
- async/two_sequences_at_once_led.py — two LED sequences simultaneously
- async/two_sequences_at_once_servo.py — two servos running opposite sequences
- async/concurrent_actuators.py — two actuators with different speeds and easings
- async/concurrent_sequences.py — a servo and LED each running their own sequence
- async/move_servo_and_led.py — servo and LED moving simultaneously
- async/sensor-drive-servo.py — read an analog sensor and drive a servo concurrently
- async/sensor-determine-sequence.py — use a sensor reading to select and run a varspeed sequence
Uses varspeed.py (from varspeed import Vspeed)
- basic/move_simple.py — a non-CircuitPython dependent example that can be run in any Python environment
- basic/sequence_simple.py — a non-CircuitPython dependent example that can be run in any Python environment
- basic/move_simple_led.py — changes the brightness of an LED
- basic/move_simple_servo.py — changes the angle of a servo
- basic/sequence_simple_servo.py — runs a sequence of moves for a servo
- basic/two_sequences_at_once_led.py — shows how to do two sequences simultaneously, for example dimming two LEDs in opposite directions
- basic/two_sequences_at_once_servo.py — runs different sequences for two servos simultaneously
To set up on a CircuitPython hardware device:
- Put
varspeed/varspeed.py(sync) orvarspeed/varspeed_async.py(async) in the lib directory on CIRCUITPY - Put
varspeed/easing_functions.pyin the lib directory on CIRCUITPY - Put the asyncio library from the CircuitPython Library bundle in the lib directory on CIRCUITPY
- Copy the python code from any of the examples into
code.pyormain.pyat the top of CIRCUITPY
If you have existing sync code and want to move to the async version, here is the key change:
Before (sync):
from varspeed import Vspeed
vs = Vspeed(init_position=0, result="int")
while True:
position, running, changed = vs.move(new_position=100, time_secs=2.0, steps=20, easing="SineEaseInOut")
if changed:
print(position)
if not running:
breakAfter (async):
import asyncio
from varspeed_async import Vspeed
vs = Vspeed(init_position=0, result="int")
async def main():
# iterate over the move() call
async for position, running, changed in vs.move(
new_position=100, time_secs=2.0, steps=20, easing="SineEaseInOut"
):
if changed:
print(position)
asyncio.run(main())Key differences for the new async version:
move()is now an async iterator — useasync forinstead of calling it in a while loop- Wrap logic in
async def main()and call withasyncio.run(main()) sequence()works withasync forin the same way- Use
asyncio.create_task()to run multiple actuators concurrently — this is the main reason to migrate