Skip to content

optotune-lens

A robust, modern Python SDK for interacting with Optotune electrically tunable lenses over a serial (RS-232/USB COM port) interface. Supports both the Lens Driver 4 (Lens) and the ICC-1C industrial controller (ICC1C).


Features

  • Context Manager Support: Safely and automatically open and close serial connections.
  • Robust Exception Hierarchy: Specialized exceptions (LensConnectionError, LensCRCError, LensTimeoutError, etc.) replace generic Python Exceptions.
  • Input Validation: Safeguard hardware with diopter and current bounds checking prior to sending commands.
  • Logging Integration: Uses Python's standard logging library instead of verbose print flags.
  • Lens Driver 4 (Lens): binary protocol with CRC-16 Modbus verification on all TX/RX commands, plus EEPROM read/write/dump access.
  • ICC-1C (ICC1C): ASCII "Simple Mode" protocol; the controller does its own raw-current↔diopter conversion from the lens's onboard calibration.
  • Modern Packaging: Conforms to modern PEP 517/518 build standards using pyproject.toml.

Installation

Prerequisites

  • Python >= 3.8
  • uv

Setting Up a Development Environment

From the root of the project, create the virtual environment and install the package with its dev dependencies (pytest, etc.) from uv.lock:

uv sync --extra dev

Run any command inside that environment with uv run, e.g. uv run pytest or uv run python example.py.


Quick Start

Basic Usage with Context Manager

import logging
from optotune_lens import Lens

# Set logging to see debug TX/RX logs
logging.basicConfig(level=logging.INFO)

# Use context manager to guarantee connection cleanup
with Lens(port='COM7') as lens:
    print(f"Connected to Lens Serial: {lens.lens_serial}")
    print(f"Firmware Version: {lens.firmware_version}")
    print(f"Current Temperature: {lens.get_temperature()} °C")

    # 1. Focal Power (Diopter) Mode Example
    min_fp, max_fp = lens.to_focal_power_mode()
    print(f"Focal Power Limits: {min_fp} to {max_fp} diopters")

    # Validate temperature limits (adjusts diopter range mapping)
    lens.set_temperature_limits(lower=20.0, upper=45.0)

    # Set optical power (raises LensValidationError if outside min_fp / max_fp)
    lens.set_diopter(3.0)
    lens.set_diopter(-0.2)

    # 2. Current Mode Example
    lens.to_current_mode()
    lens.set_current(100.0)  # set target current in mA

ICC-1C Controller

from optotune_lens import ICC1C

with ICC1C(port='COM7') as icc:
    print(f"Connected to: {icc.device_type} (Serial: {icc.device_serial})")
    print(f"Temperature: {icc.get_temperature()} °C")

    # Focal power mode — no explicit mode switch needed, SETFP is
    # always available; this just validates the lens has calibration data.
    icc.to_focal_power_mode()
    icc.set_diopter(3.0)  # raises LensValidationError if outside the lens's range

    # Current mode — SETCURRENT is independent of SETFP, no mode switch needed.
    icc.set_current(100.0)  # mA

API Documentation

Class: Lens

__init__(port: str, debug: bool = False)

Initializes the serial connection at 115200 baud, performs the handshake, and loads device metadata (Max Current, Serial, Firmware). If debug=True, sets the logger level to DEBUG and routes output to stdout.

to_focal_power_mode() -> Tuple[float, float]

Switches the lens to Focal Power mode (Mode 5) and updates/returns the diopter physical range (min_diopter, max_diopter).

to_current_mode()

Switches the lens to Current mode (Mode 1).

set_diopter(diopter: float) / get_diopter() -> float

Sets/reads the target focal power in diopters. Validates input against safety bounds.

set_current(current: float) / get_current() -> float

Sets/reads the target current in mA. Validates against the device's maximum output current.

get_temperature() -> float

Reads the internal temperature sensor (resolution 0.0625 °C).

set_temperature_limits(lower: float, upper: float) -> Tuple[int, float, float]

Configures safety temperature limits. Returns new focal power limits at those temperatures.

eeprom_dump() -> List[int]

Dumps the complete 256-byte internal EEPROM contents.

eeprom_print()

Helper method to dump and pretty-print the EEPROM in a 16x16 hex grid.

eeprom_write_byte(address: int, byte: int) -> int

Writes a single byte to the given EEPROM address (0–255). Returns the device's error code.

Enum: OperatingMode

An IntEnum with two members: - CURRENT = 1 — current control mode - FOCAL_POWER = 5 — focal power (diopter) control mode

Function: crc_16(data: bytes) -> int

Computes the CRC-16 Modbus checksum (polynomial 0xA001) for the given data bytes.

Exceptions

All exceptions derive from LensError. The hierarchy:

  • LensError — base exception for all package errors
  • LensConnectionError(LensError) — serial connection issues
  • LensTimeoutError(LensConnectionError) — read/write timeouts
  • LensCRCError(LensError) — CRC-16 checksum mismatch
  • LensCommandError(LensError) — hardware error codes or unexpected responses
  • LensValidationError(LensError, ValueError) — out-of-range parameter validation

Class: ICC1C

__init__(port: str, baudrate: int = 115200, debug: bool = False)

Opens the serial connection, performs the START handshake, and confirms a lens is detected via DETECTDEVICE. The ICC-1C auto-detects baud rate, so baudrate is arbitrary.

to_focal_power_mode()

Validates the connected lens has focal power calibration data (GETFPMIN not NO). Simple Mode has no real mode-switch command — SETFP/SETCURRENT are independent and always available.

to_current_mode()

No-op compatibility shim; SETCURRENT is always available.

set_diopter(diopter: float) / get_diopter() -> Optional[float]

Sets/reads the target focal power in diopters. The device converts to/from raw current using the lens's own EEPROM calibration.

get_diopter_min() / get_diopter_max() -> Optional[float]

Reads the lower/upper focal power limit of the connected lens in diopters, or None if no lens is detected.

set_current(current_mA: float) / get_current() -> float

Sets/reads the target current in mA.

get_temperature() -> float

Reads the connected device's temperature sensor.

set_temperature_limit(limit: float)

Sets the operational temperature limit in Celsius.

Note: Pro Mode features (Smart Step, the signal generator, vectors) are not covered by this class — they're binary register-protocol only and are configured once via Optotune Cockpit, then persisted on the device across power cycles.

Class: FirmwareVersion

NamedTuple (major, minor, build, patch) returned in Lens.firmware_version; str() renders it as "major.minor.build.patch".


Development and Testing

Unit tests are written with pytest and use a mock serial implementation.

Running Tests

uv run pytest

License

This project is licensed under the GNU General Public License v3.0 (GPLv3) — see the LICENSE file for details.