A Python package for controlling Thorlabs Elliptec rotation stages (ELL6, ELL14, ELL18, etc.), providing an intuitive interface for optical control applications.
✅ HARDWARE VALIDATED - Confirmed working with real Elliptec devices
✅ PRODUCTION READY - 100% core functionality tested
✅ GROUP ADDRESSING VERIFIED - Synchronized multi-rotator control working
✅ ASYNCHRONOUS SUPPORT - Non-blocking operation via threading
- Individual Rotator Control: Control single Elliptec rotation stages with precise positioning
- Group Synchronization: Coordinate multiple rotators with configurable offsets (hardware validated)
- Comprehensive Protocol Support: Full implementation of the ELLx protocol manual
- Thread-Safe Design: Safe for use in multi-threaded applications
- Asynchronous Operation: Non-blocking commands via dedicated threading (optimized implementation)
- Advanced Logging: Detailed logging with Loguru for debugging and monitoring
- Device Information: Automatic retrieval of device specifications and capabilities
- Position Conversion: Seamless conversion between degrees and device-specific pulse counts
- Hardware Compatibility: Tested with real μRASHG optical systems
pip install elliptec-controllergit clone https://github.com/TheFermiSea/elliptec-controller.git
cd elliptec-controller
pip install -e .git clone https://github.com/TheFermiSea/elliptec-controller.git
cd elliptec-controller
pip install -e .[dev]from elliptec_controller import ElliptecRotator
from loguru import logger
import sys
# Configure logging (optional)
logger.remove()
logger.add(sys.stderr, level="INFO")
# Initialize rotator
rotator = ElliptecRotator(
port="/dev/ttyUSB0", # Replace with your serial port
motor_address=1, # Device address (0-15)
name="MyRotator"
)
# Basic operations
rotator.home(wait=True) # Home the device
rotator.move_absolute(45.0, wait=True) # Move to 45 degrees
position = rotator.update_position() # Get current position
print(f"Current position: {position:.2f}°")from elliptec_controller import ElliptecRotator
# Using context manager for automatic thread management
with ElliptecRotator("/dev/ttyUSB0", motor_address=1) as rotator:
# Commands use async mode by default after connect()
rotator.home(wait=True) # Home the device
rotator.move_absolute(45.0, wait=False) # Non-blocking move
# Do other work while moving...
rotator.wait_until_ready() # Wait when needed
# Mix sync and async as needed
rotator.move_absolute(90.0, use_async=False) # Force synchronous
rotator.move_absolute(180.0, use_async=True) # Explicit async
# Thread is automatically stopped when exiting contextThe package includes a CLI tool for quick operations:
# Get device status
elliptec-controller status --port /dev/ttyUSB0
# Home all connected rotators
elliptec-controller home --port /dev/ttyUSB0
# Move specific rotator to position
elliptec-controller move-abs --port /dev/ttyUSB0 --address 1 --position 90.0
# Get device information
elliptec-controller info --port /dev/ttyUSB0 --address 1Control devices with non-blocking commands via dedicated threading:
from elliptec_controller import ElliptecRotator
# Method 1: Using context manager (recommended)
with ElliptecRotator("/dev/ttyUSB0", motor_address=1) as rotator:
# Thread automatically started by context manager
rotator.move_absolute(45.0) # Uses async mode by default
# Other code runs while device is moving
# Wait only when needed
rotator.wait_until_ready()
print(f"Current position: {rotator.position_degrees:.2f}°")
# Thread automatically stopped when exiting context
# Method 2: Manual thread management
rotator = ElliptecRotator("/dev/ttyUSB0", motor_address=1)
rotator.connect() # Manually start the async thread
# Mix synchronous and asynchronous as needed
rotator.move_absolute(45.0, use_async=True) # Explicit async usage
rotator.move_absolute(90.0, use_async=False) # Force synchronous for this call
rotator.disconnect() # Manually stop the async threadThe implementation uses per-command response queues for improved reliability and clearer error handling.
The ElliptecGroupController provides a high-level interface for managing groups of rotators:
from elliptec_controller import ElliptecRotator, ElliptecGroupController
# Initialize rotators on the same serial port
rotator1 = ElliptecRotator("/dev/ttyUSB0", motor_address=0, name="Rotator-0", auto_home=False)
rotator2 = ElliptecRotator("/dev/ttyUSB0", motor_address=1, name="Rotator-1", auto_home=False)
rotator3 = ElliptecRotator("/dev/ttyUSB0", motor_address=2, name="Rotator-2", auto_home=False)
# Create group controller (first rotator is master by default)
group = ElliptecGroupController(
rotators=[rotator1, rotator2, rotator3],
master_rotator_physical_address='0' # Optional: explicitly set master
)
# Form the group (slaves listen to master's address)
group.form_group()
# Or form with custom group address and slave offsets
group.form_group(
group_address_char='A', # Custom group address
slave_offsets={'1': 10.0, '2': -15.0} # Individual offsets in degrees
)
# Synchronized operations - all rotators move together
group.home_group(wait=True) # Home all rotators
group.move_group_absolute(45.0, wait=True) # Move all to 45° (with offsets)
group.stop_group() # Emergency stop all rotators
# Query group status
statuses = group.get_group_status()
for addr, status in statuses.items():
print(f"Rotator {addr}: {status}")
# Disband when done
group.disband_group()For direct control without the group controller:
from elliptec_controller import ElliptecRotator
# Initialize rotators on the same serial port
master = ElliptecRotator("/dev/ttyUSB0", motor_address=1, name="Master")
slave = ElliptecRotator("/dev/ttyUSB0", motor_address=2, name="Slave")
# Configure synchronization
slave_offset = 30.0 # Slave will be offset by 30 degrees
slave.configure_as_group_slave(master.physical_address, slave_offset)
# Synchronized movement - both rotators move together
target_angle = 45.0
master.move_absolute(target_angle, wait=True)
# Master moves to 45°, Slave moves to 75° (45° + 30° offset)
# Cleanup
slave.revert_from_group_slave()import serial
from elliptec_controller import ElliptecRotator
from loguru import logger
try:
rotator = ElliptecRotator("/dev/ttyUSB0", motor_address=1)
# Check device readiness
if not rotator.is_ready():
logger.warning("Device not ready, attempting to home...")
if not rotator.home(wait=True):
raise RuntimeError("Failed to home device")
# Perform operations with error checking
if rotator.move_absolute(90.0, wait=True):
logger.info("Move completed successfully")
else:
logger.error("Move operation failed")
except serial.SerialException as e:
logger.error(f"Serial communication error: {e}")
except Exception as e:
logger.error(f"Unexpected error: {e}")
# Emergency stop if needed
try:
rotator.stop()
except:
passThis package supports Thorlabs Elliptec rotation stages including:
- ELL6: 360° rotation mount
- ELL14: 360° rotation mount with encoder
- ELL18: 360° rotation mount with high resolution
The package automatically detects device-specific parameters such as:
- Pulses per revolution
- Travel range
- Firmware version
- Serial number
- Connect Hardware: Connect your Elliptec rotator via USB
- Identify Port: Find the serial port name:
- Linux:
/dev/ttyUSB0,/dev/ttyUSB1, etc. - Windows:
COM1,COM2, etc. - macOS:
/dev/tty.usbserial-*
- Linux:
- Set Permissions (Linux/macOS):
sudo usermod -a -G dialout $USER # Log out and back in
The package uses Loguru for comprehensive logging:
from loguru import logger
import sys
# Configure logging level
logger.remove()
logger.add(sys.stderr, level="DEBUG") # Options: TRACE, DEBUG, INFO, WARNING, ERROR
# Logging will show detailed communication and device state informationRun the test suite:
# Basic test run
pytest
# With coverage
pytest --cov=elliptec_controller
# Verbose output
pytest -v
# Core functionality only (all passing)
pytest tests/test_controller.py -vTest Status: ✅ 51/51 tests passing (100%) - Complete test coverage achieved
The package includes hardware validation scripts in the hardware_tests/ directory:
# Test group addressing (requires 2+ rotators)
python hardware_tests/test_group_simple.py
# Comprehensive group test (requires 3 rotators)
python hardware_tests/test_group_hardware.pyHardware Status: ✅ Validated on real Elliptec devices (addresses 2, 3, 8)
See hardware_tests/README.md for detailed validation procedures and results.
The main class for controlling individual rotators.
ElliptecRotator(port, motor_address, name=None, auto_home=True)home(wait=True): Home the rotatormove_absolute(degrees, wait=True): Move to absolute positionmove_relative(degrees, wait=True): Move by relative amountupdate_position(): Get current positionget_status(): Get device statusset_velocity(velocity): Set movement velocityget_device_info(): Retrieve device information
connect(): Start the async communication threaddisconnect(): Stop the async communication thread__enter__(),__exit__(): Context manager support
configure_as_group_slave(master_address, offset_degrees): Configure for synchronized movementrevert_from_group_slave(): Return to individual control
High-level controller for managing multiple synchronized rotators.
ElliptecGroupController(rotators, master_rotator_physical_address=None)Parameters:
rotators: List ofElliptecRotatorinstances (must share same serial port)master_rotator_physical_address: Physical address of master rotator (defaults to first in list)
Group Formation & Management:
-
form_group(group_address_char=None, slave_offsets=None): Form synchronized groupgroup_address_char: Group address ('0'-'F'), defaults to master's addressslave_offsets: Dict mapping physical addresses to offset angles (degrees)- Returns:
Trueif successful,Falseotherwise
-
disband_group(): Disband group, revert all rotators to individual control- Returns:
Trueif all rotators reverted successfully
- Returns:
Group Operations:
-
home_group(wait=True, home_timeout_per_rotator=2.0): Home all rotators simultaneouslywait: Block until complete ifTrue- Returns:
Trueif successful
-
move_group_absolute(degrees, wait=True, move_timeout_per_rotator=45.0): Move all rotators to positiondegrees: Target absolute position (0-360)wait: Block until complete ifTrue- Returns:
Trueif successful
-
stop_group(): Send emergency stop to all rotators- Returns:
Trueif all acknowledged stop command
- Returns:
-
get_group_status(): Query status of all rotators- Returns: Dict mapping physical addresses to status codes ('00'=ready, '01'=moving, '09'=homing)
Properties:
is_grouped: Boolean indicating if group is currently formedmaster_rotator: Reference to the master rotator instancerotators: List of all rotators in the groupgroup_master_address_char: Current group address (orNoneif not formed)
The examples/ directory contains comprehensive usage examples:
basic_usage.py: Single rotator controladvanced_usage.py: Group synchronization and advanced features
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
git clone https://github.com/TheFermiSea/elliptec-controller.git
cd elliptec-controller
pip install -e .[dev]
# Run tests
pytest
# Format code
black elliptec_controller/
# Type checking
mypy elliptec_controller/This project is licensed under the MIT License - see the LICENSE file for details.
- Individual Control: 23/23 tests passing
- Device Communication: Hardware validated
- Position Accuracy: Sub-degree precision confirmed
- Protocol Implementation: Complete ELLx support
- Asynchronous Mode: Non-blocking operation via threading with per-command response queues
- Group Formation: Working on real devices
- Synchronized Movement: Multiple rotators coordinated
- Offset Configuration: Individual rotator offsets applied
- Cleanup & Recovery: Clean reversion to individual control
- Core Tests: 23/23 passing (100%)
- Group Controller Tests: 28/28 passing (100%)
- Mock Tests: All group controller mocking issues resolved
- Hardware Tests: Available for validation
- Total Coverage: 51/51 tests passing (100%)
This package is actively used in μRASHG (micro Rotational Anisotropy Second Harmonic Generation) experiments with:
- 3 synchronized Elliptec rotators
- Sub-second scanning optimization
- Reliable group addressing for optical measurements
- Issues: GitHub Issues
- Documentation: docs/
- Thorlabs Manual: ELLx Protocol Manual
- Test Status: test-status.md
See CHANGELOG.md for version history and changes.