Skip to content

TheFermiSea/elliptec-controller

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Elliptec Controller

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

Features

  • 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

Installation

From PyPI (Recommended)

pip install elliptec-controller

From Source

git clone https://github.com/TheFermiSea/elliptec-controller.git
cd elliptec-controller
pip install -e .

Development Installation

git clone https://github.com/TheFermiSea/elliptec-controller.git
cd elliptec-controller
pip install -e .[dev]

Quick Start

Basic Single Rotator Control

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}°")

Asynchronous Control (Non-blocking)

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 context

Command Line Interface

The 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 1

Advanced Usage

Asynchronous Operation

Control 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 thread

The implementation uses per-command response queues for improved reliability and clearer error handling.

Synchronized Group Movement

Method 1: Using ElliptecGroupController (Recommended)

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()

Method 2: Manual Group Configuration (Low-Level)

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()

Error Handling and Robustness

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:
        pass

Device Compatibility

This 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

Hardware Setup

  1. Connect Hardware: Connect your Elliptec rotator via USB
  2. Identify Port: Find the serial port name:
    • Linux: /dev/ttyUSB0, /dev/ttyUSB1, etc.
    • Windows: COM1, COM2, etc.
    • macOS: /dev/tty.usbserial-*
  3. Set Permissions (Linux/macOS):
    sudo usermod -a -G dialout $USER
    # Log out and back in

Logging

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 information

Testing

Unit Tests

Run 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 -v

Test Status: ✅ 51/51 tests passing (100%) - Complete test coverage achieved

Hardware Validation

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.py

Hardware Status: ✅ Validated on real Elliptec devices (addresses 2, 3, 8)

See hardware_tests/README.md for detailed validation procedures and results.

API Documentation

ElliptecRotator Class

The main class for controlling individual rotators.

Initialization

ElliptecRotator(port, motor_address, name=None, auto_home=True)

Key Methods

  • home(wait=True): Home the rotator
  • move_absolute(degrees, wait=True): Move to absolute position
  • move_relative(degrees, wait=True): Move by relative amount
  • update_position(): Get current position
  • get_status(): Get device status
  • set_velocity(velocity): Set movement velocity
  • get_device_info(): Retrieve device information

Thread Management Methods

  • connect(): Start the async communication thread
  • disconnect(): Stop the async communication thread
  • __enter__(), __exit__(): Context manager support

Group Control Methods

  • configure_as_group_slave(master_address, offset_degrees): Configure for synchronized movement
  • revert_from_group_slave(): Return to individual control

ElliptecGroupController Class

High-level controller for managing multiple synchronized rotators.

Initialization

ElliptecGroupController(rotators, master_rotator_physical_address=None)

Parameters:

  • rotators: List of ElliptecRotator instances (must share same serial port)
  • master_rotator_physical_address: Physical address of master rotator (defaults to first in list)

Key Methods

Group Formation & Management:

  • form_group(group_address_char=None, slave_offsets=None): Form synchronized group

    • group_address_char: Group address ('0'-'F'), defaults to master's address
    • slave_offsets: Dict mapping physical addresses to offset angles (degrees)
    • Returns: True if successful, False otherwise
  • disband_group(): Disband group, revert all rotators to individual control

    • Returns: True if all rotators reverted successfully

Group Operations:

  • home_group(wait=True, home_timeout_per_rotator=2.0): Home all rotators simultaneously

    • wait: Block until complete if True
    • Returns: True if successful
  • move_group_absolute(degrees, wait=True, move_timeout_per_rotator=45.0): Move all rotators to position

    • degrees: Target absolute position (0-360)
    • wait: Block until complete if True
    • Returns: True if successful
  • stop_group(): Send emergency stop to all rotators

    • Returns: True if all acknowledged stop command
  • 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 formed
  • master_rotator: Reference to the master rotator instance
  • rotators: List of all rotators in the group
  • group_master_address_char: Current group address (or None if not formed)

Examples

The examples/ directory contains comprehensive usage examples:

  • basic_usage.py: Single rotator control
  • advanced_usage.py: Group synchronization and advanced features

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Development Setup

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/

License

This project is licensed under the MIT License - see the LICENSE file for details.

Validation Status

✅ Core Functionality (Production Ready)

  • 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 Addressing (Hardware Validated)

  • Group Formation: Working on real devices
  • Synchronized Movement: Multiple rotators coordinated
  • Offset Configuration: Individual rotator offsets applied
  • Cleanup & Recovery: Clean reversion to individual control

✅ Test Infrastructure (Complete Coverage)

  • 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%)

Real-World Usage

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

Support

Changelog

See CHANGELOG.md for version history and changes.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages