Skip to main content

tl_elliptec

Docs License: MIT Buy Me A Coffee

Python library for Thorlabs Elliptec (ELLx) resonant piezo motor modules — rotary stages, linear stages, multi-position sliders, filter wheels, and the motorized iris. Implements the ASCII-hex serial protocol from Thorlabs' own protocol manual, including a priority-based request broker so background position polling never delays a move you're waiting on.

Full documentation: tl-elliptec.readthedocs.io

This is an unofficial, community-maintained library. It is not affiliated with, endorsed by, or sponsored by Thorlabs, Inc. "Thorlabs" and "Elliptec" are trademarks of Thorlabs, Inc., used here only to identify the hardware this library talks to.

Supported devices

ELL6, ELL6B, ELL9, ELL12, ELL14, ELL15, ELL16, ELL17, ELL18, ELL20, ELL21, ELL22.

Install

pip install tl_elliptec

Or for local development:

git clone https://github.com/TapyrLabs/tl_elliptec
pip install -e ".[test]"

Quick start

One device wired straight to its controller (no hub) — construct the model class directly from the serial port; it opens and owns the port itself (Make sure to use the correct COM port for your setup):

from tl_elliptec import ELL20

with ELL20("COM5") as stage:      # "/dev/ttyUSB0" on Linux/macOS
    stage.home()
    stage.move_absolute(10)       # 10 mm — physical units, not raw pulses
    stage.move_relative(-2.5)     # -2.5 mm from wherever it now is
    print(stage.get_position())   # 7.5

Several devices on a shared bus

Create one ElliptecBus and hand it to each device — reading and moving works exactly the same as above, just addressed:

from tl_elliptec import ElliptecBus, discover_devices

with ElliptecBus("COM5") as bus:
    devices = discover_devices(bus)   # probes addresses 0-F, builds the right class for each
    for address, device in devices.items():
        print(address, type(device).__name__, device.serial_number)

    stage = devices["0"]
    stage.home()
    stage.move_absolute(45)           # 45 degrees, if it's an ELL14

First-time address setup

Every ELLx module ships from the factory at address "0". Wire two or more onto the same bus before giving them unique addresses and they all answer to "0" at once — their replies collide on the wire, so neither scan() nor discover_devices() finds anything. This has to be fixed once, per device, before they share the bus. Either let the library walk you through it, always connect one device at a time:

from tl_elliptec import ElliptecBus, setup_devices

with ElliptecBus("COM5") as bus:
    # Connect ONE new, unaddressed device at a time when prompted.
    assigned = setup_devices(bus, count=2)   # e.g. ["1", "2"]

...or do it by hand, one device connected at a time:

from tl_elliptec import ElliptecBus, ELL14

with ElliptecBus("COM5") as bus:
    stage = ELL14(bus, address="0")   # factory default, must be the only device on the bus
    stage.change_address("2")         # non-volatile -- a one-time step

Addresses persist across power cycles, so this only needs to happen once per device — after that, discover_devices() finds everything normally, every session.

Broker-handled communication and live position streaming

Normally, when many devices are connected to a single serial port, and with incessent polling, the communicaiton might go into conflict and brake. tl_elliptec implements a serial communication broker in the ElliptecBus class, the broker implements a read/write priority queue for safe command serialization. Explicitly issued commands (moves, reads, ...) always jump ahead of background polling, so polling a device's position never delays a move you're waiting on:

for position in stage.poll_position():   # a generator -- only work while it's iterated
    print(position)                       # yields only when the position actually changes

This scales to several devices on one bus without extra locking — run one poll_position() per device on its own thread, and issue moves from another thread (or the main one) whenever you like.

Full command reference are all in the docs: tl-elliptec.readthedocs.io

Running the tests

pip install -e ".[test]"
pytest

No hardware required — tests run against a scripted fake bus and check values against the manual's own worked examples.

License

MIT © Matteo Michiardi


If this library saved you some time, consider buying me a coffee ☕

Buy Me A Coffee

Release files for tl-elliptec 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tl-elliptec 1.0.1
File Size Uploaded
tl_elliptec-1.0.1.tar.gz 40.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tl-elliptec 1.0.1
File Interpreter ABI Platform
tl_elliptec-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 75.3 kB

Release files / tl_elliptec-1.0.1.tar.gz

Download URL tl_elliptec-1.0.1.tar.gz
Size 40.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d49e3e1d456fa53b2b1b6bf4a031d92a2478767487c0430fbbb92a6bf479e04a
BLAKE2b-256 checksum
How to use checksums
0083e9e77c5e7432e75dd95d86ae4b643071cfe7f3255076de544aa79b705de8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 22, 2026.

Transparency log

Release files / tl_elliptec-1.0.1-py3-none-any.whl

Download URL tl_elliptec-1.0.1-py3-none-any.whl
Size 34.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e552d8c7dec537ca5d4525ad0dbe9cfa2db0cdbf327440afc9437c68225d61d1
BLAKE2b-256 checksum
How to use checksums
524482dcc3bf179c6a233d0bc9e9ab17cdadba7ec937ee0e670c5f174c885118
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 22, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page