Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,15 @@ RUN \

# Place executables in the environment at the front of the path
ENV PATH="/opt/imswitch/.venv/bin:$PATH"

# Drop-in plugin directory. Created in the image so the mount point always
# exists, even when nothing is mounted over it — otherwise a typo'd bind mount
# and "no plugins installed" look identical in the logs.
# Mount it read-only: anything that can write here executes arbitrary Python in
# the microscope process. See docs/plugins/DEPLOYMENT.md.
ENV IMSWITCH_PLUGIN_DIR="/opt/imswitch/plugins"
RUN mkdir -p /opt/imswitch/plugins

# Expose HTTP port and Jupyter server port
EXPOSE 8001 8888 8889

Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,27 @@ ImSwitch provides comprehensive Docker support for containerized deployments, en
### End-to-End Operating System
- **[rpi-imswitch-os](https://github.com/openUC2/rpi-imswitch-os)**: A complete Raspberry Pi-based operating system image with ImSwitch and all UC2 components pre-installed and configured. This is the officially-supported way to deploy and use ImSwitch.

## Extending ImSwitch

Building on ImSwitch from outside this repository — a control panel, an analysis
view, an instrument integration? Start at
**[docs/INTEGRATION.md](docs/INTEGRATION.md)**, which picks the right extension
point for what you are building:

- **REST / Socket.IO client** — the default. Everything ImSwitch exposes with
`@APIExport` is already an HTTP endpoint; browse it live at
`/imswitch/api/docs` on any running instance.
- **[Plugin](docs/plugins/README.md)** — for in-process frame access, tight
hardware loops, or a widget that lives inside the ImSwitch window. Start from
the [plugin template](https://github.com/openUC2/imswitch-plugin-template);
a plugin bind-mounts into the stock container with no rebuild and no
`pip install`.
- **Upstream contribution** — for a new camera, stage or laser driver.

Forking the core is not a supported extension path; see
[docs/INTEGRATION.md](docs/INTEGRATION.md#do-not-fork-the-core) for what to do
instead.

## Documentation

Documentation for the upstream project is at
Expand Down
49 changes: 47 additions & 2 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@ services:
- /etc/machine-id:/etc/machine-id:ro
- /home/pi/ImSwitchConfig:/home/pi/ImSwitchConfig
- /home/pi/Datasets:/home/pi/Datasets
#- /Users/bene/ImSwitchConfig:/home/pi/ImSwitchConfig
#- /Users/bene/ImSwitchConfig:/home/pi/ImSwitchConfig
#- /Users/bene/ImSwitchConfig/Datasets:/home/pi/Datasets
#- /Volumes:/media
- /media:/media
# Drop-in plugins: one subdirectory per plugin, each containing a Python
# package with register(). Read-only on purpose — anything that can write
# here runs arbitrary Python in the microscope process and arbitrary
# JavaScript in the operator's browser. See docs/plugins/DEPLOYMENT.md.
- /home/pi/ImSwitchPlugins:/opt/imswitch/plugins:ro
- /run/dbus/system_bus_socket:/run/dbus/system_bus_socket
- /run/udev:/run/udev:ro
- /dev/dma_heap:/dev/dma_heap
Expand All @@ -28,6 +33,8 @@ services:
- SCAN_EXT_DATA_PATH=1
- EXT_DATA_PATH=/media
- SSL=0
# Exported to the app as IMSWITCH_PLUGIN_DIR by the entrypoint.
- PLUGIN_PATH=/opt/imswitch/plugins
restart: always

volume-setup:
Expand All @@ -38,4 +45,42 @@ services:
volumes:
#- /Users/bene/ImSwitchConfig/Datasets:/home/pi/Datasets
- /home/pi/ImSwitchConfig/Datasets:/home/pi/Datasets
command: 'sh -c "chown -R 1000:1000 /home/pi/Datasets; sleep 60"'
# Docker auto-creates a missing bind-mount source as root-owned. The
# plugin directory is mounted read-only, so ownership does not affect the
# server — but creating it here means the operator sees an empty
# directory to drop plugins into rather than having to guess the path.
- /home/pi/ImSwitchPlugins:/home/pi/ImSwitchPlugins
command: 'sh -c "chown -R 1000:1000 /home/pi/Datasets /home/pi/ImSwitchPlugins; sleep 60"'

# ─────────────────────────────────────────────────────────────────────────────
# ALTERNATIVE PLUGIN DELIVERY: a container image used as a volume source.
#
# The bind mount above is right for development and for a single
# self-hosted instrument: you rsync a directory and restart.
#
# This second pattern is right for a fleet, where you want the plugin version
# pinned in the same file as everything else and rolled out by digest rather
# than by whoever last ran rsync. The plugin image is FROM scratch — it holds
# the plugin tree and nothing else — and simply copies itself into a named
# volume before the server starts.
#
# Do not use both for the same plugin: a bind mount over /opt/imswitch/plugins
# hides the named volume.
#
# services:
# plugin-goniometer:
# image: ghcr.io/openuc2/imswitch-plugin-goniometer:0.2.0
# volumes:
# - plugins:/out
# command: sh -c "cp -a /plugin /out/goniometer"
#
# imswitch-docker-noqt:
# depends_on:
# plugin-goniometer:
# condition: service_completed_successfully
# volumes:
# - plugins:/opt/imswitch/plugins:ro # replaces the bind mount above
#
# volumes:
# plugins:
# ─────────────────────────────────────────────────────────────────────────────
29 changes: 29 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
# DATA_PATH - Default data storage path
# SCAN_EXT_DATA_PATH - Enable external storage scanning: "true"/"1" or "false" (default)
# EXT_DATA_PATH - Mount point directory for external drives (e.g., /media, /Volumes)
# PLUGIN_PATH - Drop-in plugin directory (default: /opt/imswitch/plugins).
# Exported to the app as IMSWITCH_PLUGIN_DIR. Mount it
# read-only; see docs/plugins/DEPLOYMENT.md.
#
# Storage Management:
# The new storage management system automatically handles:
Expand Down Expand Up @@ -76,6 +79,7 @@ log() { echo "[$(date +'%F %T')] $*"; }
CONFIG_PATH="${CONFIG_PATH:-}"
SSL=${SSL:-false}
SCAN_EXT_DATA_PATH=${SCAN_EXT_DATA_PATH:-false}
PLUGIN_PATH="${PLUGIN_PATH:-/opt/imswitch/plugins}"

# ============================================================================
# Server Mode - Start ImSwitch
Expand Down Expand Up @@ -120,6 +124,31 @@ if [[ -f "${CONFIG_PATH}/config/imcontrol_options.json" ]]; then
cat "${CONFIG_PATH}/config/imcontrol_options.json"
fi

# ============================================================================
# Plugin Path Setup
# ============================================================================
# The v2 PluginManager scans $IMSWITCH_PLUGIN_DIR for drop-in plugins: one
# subdirectory per plugin, each containing a Python package with a register()
# function. Nothing is pip-installed.
#
# Deliberately non-fatal. A missing or empty plugin directory is the normal
# case for most instruments, and a plugin problem must never stop a microscope
# from booting. The listing below is what tells an operator whether a bind
# mount actually landed — when it is wrong, this is the log line to read.
export IMSWITCH_PLUGIN_DIR="$PLUGIN_PATH"
log "Using PLUGIN_PATH: $PLUGIN_PATH"

if [[ ! -d "$PLUGIN_PATH" ]]; then
log "Note: plugin directory '$PLUGIN_PATH' does not exist — continuing with no plugins."
else
log 'Available plugins:'
if [[ -n "$(ls -A "$PLUGIN_PATH" 2>/dev/null)" ]]; then
ls -la "$PLUGIN_PATH"
else
log ' (none — directory is empty)'
fi
fi

# ============================================================================
# Data Paths Setup
# ============================================================================
Expand Down
154 changes: 154 additions & 0 deletions docs/INTEGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Integrating with ImSwitch

For partners and external developers building on top of ImSwitch.

This document answers one question: **given what you want to build, which
extension point should you use?** Each option below links to its own reference.

---

## Pick an integration path

```
Does your code need to run inside the microscope process —
in-process frame access, or a control loop tighter than ~100 ms?
│
├─ No ─→ Are you adding a UI that must live inside the ImSwitch window?
│ │
│ ├─ No ──→ (1) REST / Socket.IO client ← start here
│ └─ Yes ─→ (2) Plugin
│
└─ Yes ─→ Are you adding a new DEVICE TYPE (camera, stage, laser driver)?
│
├─ No ──→ (2) Plugin
└─ Yes ─→ (3) Upstream contribution
```

Most integrations are (1). Reach for (2) when latency or UI placement forces it.

---

## 1. REST / Socket.IO client — the default

Every controller method ImSwitch decorates with `@APIExport` is already an HTTP
endpoint. Nothing to install on the microscope, nothing to keep in version step,
and your code can run anywhere.

```
http://<host>:8001/imswitch/api/docs Swagger UI, live on any instance
http://<host>:8001/imswitch/openapi.json machine-readable schema
```

Endpoints follow `/imswitch/api/<Controller>/<method>`. Live data (camera
frames, stage positions, experiment progress) is pushed over Socket.IO on the
same origin.

A Python client is published as [`imswitchclient`](https://pypi.org/project/imswitchclient/).

**Choose this when:** driving acquisitions, moving stages, pulling results,
batch analysis, LIMS or scheduler integration, or anything that should survive
an ImSwitch upgrade untouched.

**Trade-off:** every frame crosses a network boundary. If you need pixels in
process, see below.

### Related surfaces

ImSwitch also speaks several standard protocols, if one already fits your
ecosystem: **SiLA 2** and **Arkitekt** (both surfaced through the setup file as
`availableWidgets` entries), an **OMERO** exporter, and an embedded **Jupyter**
server for scripting against the running instrument.

---

## 2. Plugin — for in-process work and embedded UI

A plugin adds a backend controller and a React widget to a running ImSwitch. You
write two files, build, and drop the result into a directory the container
already watches: **no rebuild of ImSwitch, no `pip install`, no fork.**

Your endpoints appear under `/imswitch/plugin/<name>/api`, your widget in the
sidebar — rendering inside the host's React tree, so it uses the host's MUI
theme, Redux store and socket connection with no props and no bridge object.

**Choose this when:** you need in-process frame access, a sub-100 ms hardware
loop, or a UI that belongs inside the ImSwitch window.

**Trade-off — read this before committing:** a plugin runs in the microscope
process. There is no sandbox. A plugin that blocks holds a worker thread; a
plugin that bundles a second copy of NumPy produces wrong numbers rather than a
clean crash. That is why the plugin contract is strict about dependencies.

| | |
|---|---|
| **Start here** | [docs/plugins/README.md](plugins/README.md) |
| Template repo | [imswitch-plugin-template](https://github.com/openUC2/imswitch-plugin-template) |
| Writing guide | [WRITING_A_PLUGIN.md](https://github.com/openUC2/imswitch-plugin-template/blob/main/docs/WRITING_A_PLUGIN.md) |
| Deploying | [docs/plugins/DEPLOYMENT.md](plugins/DEPLOYMENT.md) |
| Design rationale, stability guarantees | [docs/plugins/DECISIONS.md](plugins/DECISIONS.md) |
| Reference implementation | [imswitch-plugin-goniometer](https://github.com/openUC2/imswitch-plugin-goniometer) |

```bash
git clone https://github.com/openUC2/imswitch-plugin-template my-plugin
cd my-plugin && make install-ui && make build check
```

> **Note for existing integrations.** The older `imswitch.implugins` entry-point
> mechanism has been **removed**. Packages built on it no longer load. The
> migration is a `plugin.toml`, one `register(ctx)` function and a
> `PluginController` — see
> [ADR-001](plugins/DECISIONS.md#adr-001--v2-pluginmanager-is-the-only-plugin-mechanism).

---

## 3. Upstream contribution — for new device types

A plugin deliberately **cannot** register a new `Manager` class. Supporting a new
camera, stage or laser is not a controller concern: it also requires a setup-file
schema change and device instantiation through `MultiManager`, both of which are
host-private surfaces that still move between minor releases. Publishing them
now would freeze a contract we would immediately want to break.

So the driver goes upstream, into the ImSwitch tree, where we maintain it
alongside the rest. The *logic built on top of it* can still live in your plugin.

Open an issue at [ImSwitch issues](https://github.com/openuc2/ImSwitch/issues)
describing the device and we will point you at the right manager base class.

Rationale in full:
[ADR-002](plugins/DECISIONS.md#adr-002--plugins-are-controller-only).

---

## Do not fork the core

Forking ImSwitch to add functionality is not a supported integration path, and
we would rather you did not: a fork stops receiving hardware support, bug fixes
and security updates, and every ImSwitch release widens the gap.

Everything a fork was previously needed for now has a supported route:

| You forked to… | Do this instead |
|---|---|
| add a control panel or analysis view | Plugin (§2) |
| add an endpoint for your own tooling | Plugin (§2), or a REST client (§1) |
| script an acquisition | REST client (§1) or the embedded Jupyter server |
| support a new camera or stage | Upstream contribution (§3) |
| change a default or a setup layout | Setup file — no code change needed |

If you find something none of these covers, that is a gap in the extension
surface and we want to hear about it. Open an issue rather than a fork.

---

## Version compatibility

Plugins declare `imswitch_min` and `sdk_min` in their manifest. **Be aware that
neither is enforced by the host today** — they are recorded but not checked, so
do not rely on ImSwitch rejecting a mismatched plugin. The per-surface stability
table, including what is frozen and what is still provisional, is in
[DECISIONS.md §2](plugins/DECISIONS.md#2-stable-surface).

The REST API surface is generated from the running instance, so the OpenAPI
document at `/imswitch/openapi.json` is always the accurate answer for a given
deployment.
Loading
Loading