Skip to content

About

OpenFlexure Microscope UniteLabs Connector: SiLA 2 interface to the OpenFlexure Microscope Server v2 (stage, camera, captures, autofocus)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

OpenFlexure Microscope — UniteLabs Connector

A SiLA 2 connector for the OpenFlexure Microscope, built with the UniteLabs CDK in the same style as the Acceleration Consortium's opentrons-ot2 connector.

It wraps the HTTP API of the OpenFlexure Microscope Server v2 (http://<microscope>:5000/api/v2, server 2.x on the Raspbian-OpenFlexure image) as SiLA 2 features: stage movement, image acquisition, stored captures, camera settings, autofocus and device information. The connector starts in simulation mode by default and can switch to the real microscope at runtime.

Status

Version 0.1.0, not yet released.

  • Verified on a real microscope (server 2.11.0, Raspberry Pi camera v2, 2026-10-21): identity and status, camera settings, preview and full-resolution images, stored captures (create, list, download, tag, delete), sharpness measurement, simulation/real mode switching, and the defined errors for unavailable hardware. It was driven over SiLA and from UniteLabs Ground Control. Capture annotations, ExportCapture and the travel-limit refusals were checked over SiLA on the same microscope with the current code. Its backlash setting is 0 and the stage was detached, so the backlash-overshoot check, the one-at-a-time rule and Stop during a move are so far tested in simulation only.
  • Not yet verified on hardware: stage motion, StageController.Stop, autofocus (RunFastAutofocus, RunAutofocus, AutofocusController.Stop) and the server's busy/error responses. The test microscope's motors were detached, so the connector correctly refused these commands.
  • Not yet tried: the Docker image and registration with the UniteLabs cloud platform.

Features

SiLA feature Commands Properties OpenFlexure v2 endpoints
StageController MoveTo, MoveBy, SetZeroPosition, Stop Position, LivePosition†, StageType, Backlash, TravelLimits actions/stage/move, actions/stage/zero, instrument/state/stage/position
ImageAcquisitionController GrabPreviewFrame, AcquireImage, CaptureToStorage MJPEGStreamURL streams/snapshot, actions/camera/ram-capture, actions/camera/capture
CaptureStorageService DownloadCapture, DownloadThumbnail, ReadCaptureMetadata, SetCaptureTags, DeleteCapture, ExportCapture Captures, ExportFolder captures, captures/{id}, captures/{id}/download/{name}, captures/{id}/tags
CameraController SetExposureTime, SetAnalogGain, SetWhiteBalanceGains, SetDigitalGain‡, SetExposureMode‡, SetWhiteBalanceMode‡, SetJPEGQuality‡ ExposureTime, AnalogGain, WhiteBalanceGains, StreamResolution, ImageResolution, DigitalGain‡, ExposureMode‡, WhiteBalanceMode‡, JPEGQuality‡ instrument/settings/camera/...
AutofocusController RunFastAutofocus, RunAutofocus, MeasureSharpness, Stop — extensions/org.openflexure.autofocus/{fast_autofocus,autofocus,measure_sharpness}
MicroscopeProvider — MicroscopeName, MicroscopeId, ServerVersion, CameraType, InstalledExtensions, APIBaseURL, Reachable† instrument/configuration, instrument/settings, extensions
SimulationController (SiLA core) StartSimulationMode, StartRealMode SimulationMode —
LockController (SiLA core) LockServer, UnlockServer IsLocked — (see note §)

* observable command (progress + intermediate responses, execution mode SINGLE) † observable property ‡ only offered when the microscope's camera supports it (Raspberry Pi camera) § the lock is not enforced: no command requires the lock identifier yet, so locking does not block other clients

Conventions on the SiLA interface:

  • Stage positions and distances are motor steps (integers), exactly as the OpenFlexure server reports them.
  • Images are SiLA Binary values with content type image/jpeg. Images larger than 2 MiB (full-resolution captures) travel through SiLA binary transfer automatically.
  • Enumerations travel as their member names in lower case with spaces, e.g. ExposureMode accepts off, auto, night preview, …; Resolution accepts preview or full; StageType reports sanga stage, sanga delta stage or missing stage.
  • The SiLA 2 base protocol has no cancel call, and this connector does not implement the optional CancelController feature, so a client cannot abort the command it started. Use StageController.Stop or AutofocusController.Stop instead: they cancel the running action on the microscope, and also stop a move or autofocus that has not reached the microscope yet (it is still reading the stage, or waiting for a previous one). The stopped command finishes with a MoveFailedError / AutofocusFailedError that says so, and Stop returns once it has ended (at most about 2 s), so the same command can be sent again straight away. StoppedOperations counts only what the microscope actually cancelled: an action whose cancellation fails (no answer, or an HTTP error) keeps running, stays tracked, and the next Stop tries again.
  • Every failure is a Defined Execution Error with an actionable message (MicroscopeUnreachableError, StageUnavailableError, StageBusyError, MoveFailedError, CameraUnavailableError, CaptureFailedError, CaptureNotFoundError, CameraSettingUnsupportedError, AutofocusUnavailableError, AutofocusFailedError, TravelLimitExceededError, ZeroPositionBlockedError, ExportFailedError).

Travel limits, annotations and exporting images

  • Travel limits. The OpenFlexure stage has no limit switches. Set stage_{x,y,z}_min and stage_{x,y,z}_max in config.json (motor steps, both bounds of an axis together) and the connector refuses any MoveTo, MoveBy, RunFastAutofocus or RunAutofocus that would leave the range, with TravelLimitExceededError, before any move is sent to the microscope (it reads the stage position and backlash first). StageController.TravelLimits lists the limited axes (empty when none are set).
    • Only the axes a move changes are checked, so an axis that is already outside its range does not block moves of the others; move it back inside with a MoveTo or MoveBy that ends inside the range.
    • The stage's backlash correction counts too: moving against the sign of the Backlash setting first overshoots the target by that many steps, and that overshoot must stay inside the limits. The slow RunAutofocus always corrects 256 steps of backlash (the server forces it), so its sweep is checked 256 steps further down. A fast autofocus is checked as a sweep of ZRange steps centred on the current z (it corrects no backlash). The overshoot check follows the standard Sanga stage. A delta stage corrects backlash per motor, so its overshoot cannot be checked: while limits are set on a delta stage, moves are refused unless its Backlash is 0, and the slow RunAutofocus is refused (use RunFastAutofocus).
    • Moves are sent to the microscope as absolute targets for the axes that change, so the end point stays inside the limits even if the stage was moved elsewhere after the connector read its position, once that other move has finished. While limits are set, the connector runs its own moves and autofocus runs one at a time: a second one waits up to 1 s for the first to finish (enough for one that was just stopped), then fails with StageBusyError.
    • The limits are positions in the stage's current coordinates, so SetZeroPosition is refused with ZeroPositionBlockedError while any limit is configured. Zeroing from the microscope's web interface or power cycling the Sangaboard also resets the coordinates and shifts the protected range with them.
    • The limits only cover moves made through this connector. Moves from the microscope's web interface or other programs are not checked, and while one of those is running at the same time, neither the end point nor the backlash overshoot of a move from the connector is guaranteed.
  • Annotations. CaptureToStorage takes an Annotations list of Key/Value pairs (may be empty; keys must be non-empty and unique). They are stored with the capture on the microscope and reported in Captures.
  • Exporting. CaptureStorageService.ExportCapture saves a stored capture as a JPEG file in CaptureStorageService.ExportFolder on the computer running the connector, and returns the saved path. The file keeps the capture's name, made safe for Windows, macOS and Linux: invalid characters are replaced with _, leading and trailing dots and spaces are removed (no hidden files), .jpeg is added unless the name ends in .jpeg or .jpg, long names are shortened and reserved device names get a _ prefix. An existing file or folder is never overwritten (name (1).jpeg, name (2).jpeg, … instead). DownloadCapture still returns the image bytes to the client.

Getting started

Prerequisites

  • Python 3.10 or newer (the repository pins 3.13 in .python-version) and uv.
  • For real mode: an OpenFlexure Microscope running server v2.x reachable on the network (default microscope.local:5000; the IP address works as well if .local names do not resolve on your machine).

Install and run (simulation)

uv sync --all-extras
uv run config create --app unitelabs.openflexure_microscope:create_app   # writes ./config.json (git-ignored)
uv run connector start --app unitelabs.openflexure_microscope:create_app

The log shows Server bound to '0.0.0.0:<port>' and Registering service 'OpenFlexure Microscope'. Use the SiLA Browser to discover and try the server, or run the smoke client:

uv run python scripts/smoke_client.py --port <port>              # read identity, position, grab a preview frame
uv run python scripts/smoke_client.py --port <port> --move-z 20  # also move the stage up 20 steps and back

Windows note. --app is required on Windows (the CDK cannot auto-detect the factory). If your machine blocks the .venv\Scripts\*.exe launchers (Windows Application Control), call the CDK commands through Python instead:

uv run python -c "from unitelabs.cdk.cli import config; config()" create --app unitelabs.openflexure_microscope:create_app
uv run python -c "from unitelabs.cdk.cli import connector; connector()" start --app unitelabs.openflexure_microscope:create_app
uv run python -m pytest

Connect to the real microscope

Edit the config.json that config create wrote: it holds this installation's own sila_server.uuid, its identity in Ground Control. config/openflexure_config.example.json shows every key but has no uuid. If you copy it instead, add a "uuid" under sila_server (for example from python -c "import uuid; print(uuid.uuid4())"), or the connector gets a new identity on every start. The keys to change:

{
  "simulation_mode": false,
  "host": "microscope.local",
  "port": 5000
}

Then start the connector again. In real mode it probes GET /api/v2/ and GET /api/v2/instrument/configuration at startup and refuses to start (no silent fallback to simulation) if the microscope does not answer or does not serve the v2 API. You can also keep simulation_mode: true and switch at runtime with SimulationController.StartRealMode; if the microscope is unreachable the command fails with StartRealModeFailed and the connector stays simulated. Switching in either direction is refused while a move, capture or autofocus is still running (stop it first).

Configuration

Key Default Meaning
simulation_mode true Serve the in-memory simulated microscope instead of talking to hardware.
api_version "v2" OpenFlexure server API generation. Only v2 is implemented.
host / port "microscope.local" / 5000 Address of the OpenFlexure Microscope server.
request_timeout_s 10.0 Timeout of a single HTTP request.
action_poll_interval_s 0.25 Poll interval for long-running server actions (move, capture, autofocus).
position_poll_interval_s 0.5 Poll interval behind the LivePosition subscription.
stage_x_min / stage_x_max, stage_y_min / stage_y_max, stage_z_min / stage_z_max null Software travel limits in motor steps; set both bounds of an axis or neither. null means the axis is not limited.
export_folder ~/Downloads/OpenFlexure Folder where ExportCapture saves images, on the computer running the connector; created when needed. ~ is the home folder of the user running the connector, and a relative path is relative to the folder the connector was started from. Must not be empty.
discovery.network_interfaces null IPv4 addresses the connector announces itself on for discovery in Ground Control. Left null, the connector picks them at each start (see below). A list is used as given; [] lets the operating system pick one interface.
sila_server, cloud_servers, discovery, logging CDK defaults Standard UniteLabs settings (local port, UniteLabs cloud endpoints, mDNS, logging).

Set sila_server.port to a fixed value (e.g. 50052) for a stable local endpoint. To register the connector with the UniteLabs platform, add your tenant's cloud endpoint under cloud_servers.endpoints (not yet tried with this connector). Each installation generates its own sila_server.uuid; keep it stable once the connector is registered.

Discovery on the lab network. Ground Control finds connectors through mDNS announcements. Left to itself, the SiLA library sends them through one interface the operating system picks, and Windows often picks a virtual adapter (Tailscale or another VPN, WSL, Docker Desktop): the connector then shows up only in Ground Control on the same computer. So when discovery.network_interfaces is null, the connector announces on the default route's address and the other IPv4 addresses the operating system reports for the computer's name (loopback and 169.254.x.x addresses are skipped), and logs them at startup (Announcing the connector for discovery on ...).

  • On Windows the list can include virtual adapters such as WSL's. Announcing there is harmless, and an error logged for one address does not mean the others failed. On Linux usually only the default route's address is used, so a computer on two lab networks should list its addresses in discovery.network_interfaces.
  • The addresses are looked up at each start and never written to config.json, but they are fixed while the connector runs. After the computer gets a new address (a new DHCP lease, another network), discovery goes quiet until the connector restarts. With no network connection at start, the connector logs a warning and the operating system keeps choosing; restart it once the network is up. For an always-on installation, reserve the address on the router or set discovery.network_interfaces.
  • Ground Control on Windows can have the same problem with its own discovery queries, so a connector that started before Ground Control may only appear after the connector restarts.

Testing

uv run python -m pytest                       # unit tests, simulated backend, gRPC round trips (hardware tests skipped)
uv run python -m pytest tests/integration --microscope-host microscope.local   # against a real microscope
uv run ruff check src tests scripts && uv run ruff format --check src tests scripts

The hardware tests read identity, position and settings, grab a preview frame and in-memory captures (preview and full resolution), store one test capture and delete it again, and measure sharpness. With a stage attached they also move z by 20 steps and back. They are the final verification step for a new microscope; everything else runs without hardware.

Add --allow-autofocus to also run the autofocus Stop test. It starts a fast autofocus that sweeps z by up to 2000 steps, stops it after a second and moves z back to where it started. Make sure the objective and sample have room for that before using the flag.

Docker

Not yet tested: the image has not been built and run against a microscope.

uv run config create --app unitelabs.openflexure_microscope:create_app   # config.json is mounted into the container
docker compose up --build

docker-compose.yml uses host networking so mDNS discovery and microscope.local work; that requires a Linux Docker host. On Docker Desktop (Windows/macOS) publish the SiLA port instead and set host to the microscope's IP address.

ExportCapture saves into the container's ~/Downloads/OpenFlexure (/root/Downloads/OpenFlexure) by default. docker-compose.yml mounts the exports folder next to it there, so exported images appear in ./exports on the host and survive rebuilding the container. If you set export_folder in the container's config.json, mount that folder instead; ExportFolder and SavedPath report paths inside the container.

Limitations and roadmap

  • Server v2 only. The v3 beta server (labthings-fastapi, /api/v3) has a different API. The io layer is built around a version-neutral MicroscopeApi contract (io/api.py), so a v3 implementation can be added as io/v3/ without touching the SiLA features; api_version selects the implementation.
  • No illumination control. The v2 HTTP API has no LED endpoint; it only exists on v3 (planned follow-up via Capability.ILLUMINATION).
  • Autofocus needs the org.openflexure.autofocus extension on the microscope (installed by default).
  • config show crashes in UniteLabs CDK 0.16.0 on the base sila_server.version field (a CDK bug); use config create / config schema or the example config instead.

Development

uv sync --all-extras
uv run pre-commit install
uv run ruff format src tests && uv run ruff check src tests --fix
uv run python -m pytest

Layout (see AGENTS.md for the design rules):

src/unitelabs/openflexure_microscope/
  __init__.py            create_app(): builds the API client, registers the features
  config.py              OpenflexureMicroscopeConfig
  io/api.py              MicroscopeApi contract + Capability + build_api()
  io/types.py, errors.py, units.py
  io/v2/backend.py       HttpBackend (httpx) and the Backend protocol
  io/v2/simulation.py    SimulatedBackend: in-memory OpenFlexure v2 server
  io/v2/client.py        V2Client: MicroscopeApi over the v2 HTTP API (LabThings actions, polling, cancellation)
  features/              one SiLA feature per module, thin wrappers over MicroscopeApi
tests/                   unit tests, SiLA definition and gRPC round-trip tests, opt-in hardware tests

The project was generated from the UniteLabs connector-factory template (.cruft.json) and depends only on PyPI packages (unitelabs-cdk, httpx).

Contact

Issues: https://github.com/AccelerationConsortium/openflexure-microscope-connector/issues

About

OpenFlexure Microscope UniteLabs Connector: SiLA 2 interface to the OpenFlexure Microscope Server v2 (stage, camera, captures, autofocus)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages