Socket-based command server for remote microscope control and QuPath integration.
Part of the QPSC (QuPath Scope Control) system. For complete installation and setup instructions, see the QPSC Installation Guide.
- Socket Server: TCP/IP server for remote microscope control
- QuPath Integration: Designed for QuPath annotation-driven acquisition
- Client Library: Python functions for stage control and acquisition
- Acquisition Workflows: Multi-tile, multi-modality acquisition orchestration
- Real-time Monitoring: Progress tracking and cancellation support
- Multi-Channel Widefield IF / BF+IF: Vendor-agnostic channel library driven by Micro-Manager ConfigGroup presets and device property writes
The BGACQUIRE command now supports a vendor-agnostic channel-based acquisition
branch used by widefield immunofluorescence (IF) and combined brightfield + IF
(BF+IF) workflows. When a command carries --channels and --channel-exposures
in place of --angles / --exposures, the single-image tile loop iterates the
resolved channel plan once per tile position, writing one TIFF per channel per
tile. The Python server does not know anything about specific illuminators; it
drives the hardware entirely through core.setConfig(group, preset) and
core.setProperty(device, property, value), so the same code path serves
CoolLED, Lumencor, DLED, Colibri, and custom builds without modification.
See the cross-repo overview at
QPSC/docs/multichannel-if-overview.md for the full pipeline description,
YAML schema, and end-to-end BF+IF example. This section covers only the
Python server's slice of the pipeline.
The BGACQUIRE (and ACQUIRE) acquisition message parser accepts two optional flags on top of the existing angle-based flags:
--channels "(id1,id2,...)"-- ordered list of channel ids to acquire at every tile position. Ids must match entries in the modality's channel library declared in the microscope YAML.--channel-exposures "(exp1,exp2,...)"-- per-channel exposures in milliseconds. Must be the same length and order as--channels; missing or non-positive entries fall back to the channel library's defaultexposure_ms.
When --channels is present the server takes the channel acquisition branch
in acquisition/workflow.py. --channels is mutually exclusive with
--angles. If both are supplied (for example, a stale angle field from an
older client), the server logs a warning and clears the angles so the
channel path is the single source of truth.
Three helpers in microscope_command_server/acquisition/workflow.py implement
the channel path:
resolve_channel_plan(ppm_settings, scan_type, channel_ids, channel_exposures)-- resolves the profile (acquisition_profiles.<scan_type>), looks up itsmodality, then readsmodalities.<modality>.channelsfrom the YAML and filters / reorders to the requested ids. For each channel it merges in the profile'schannel_overrides.<id>.device_propertiesand returns an ordered list of channel plan dicts containingid,display_name,exposure_ms,mm_setup_presets,device_properties, and optionalsettle_ms._merge_device_property_overrides(library_props, override_props)-- private helper mirroring the Java-side merge rule (MicroscopeConfigManager.mergeDevicePropertyOverrides) exactly: match by(device, property)tuple, replace the value in place when matched, append to the end of the list otherwise. This lets a profile tune one property on one channel with a single YAML line without redeclaring the whole channel.apply_channel_hardware_state(hardware, channel_plan_entry, logger_)-- appliesmm_setup_presetsviacore.setConfig(group, preset)followed bycore.waitForConfig, then appliesdevice_propertiesviacore.setProperty(device, property, value)and callscore.waitForDeviceon every touched device. This is the critical settle pass that stops back-to-back channel transitions from racing the camera snap on serial LED controllers. An optionalsettle_msfield on the channel entry adds a dumb-sleep fallback for hardware whoseisBusy()reports complete too early (some filter turrets, reflector wheels, serial LED controllers).
Inside the "Single image acquisition: no rotation angles" block of the tile
loop, the server checks for a non-empty channel plan. If present, it iterates
the plan for the current tile position: apply channel state, set the channel
exposure, snap, (optionally) flat-field correct, saturation-check, and write
the per-channel TIFF. The tile loop then continues past the default
single-snap path. If no channel plan is resolved, the tile loop falls back to
the existing single-snap behavior -- see "Backward compatibility" below.
Per-tile the channel branch writes one TIFF per channel into a per-channel subdirectory under the existing annotation output folder:
{projectsFolder}/{sample}/{scan_type}/{annotation}/
{channel_id_1}/tile_0_0.tif
{channel_id_1}/tile_0_1.tif
{channel_id_2}/tile_0_0.tif
{channel_id_2}/tile_0_1.tif
...
TileConfiguration.txt
This mirrors the PPM per-angle layout exactly -- channel ids double as
subdirectory names. The stitcher
(qupath-extension-tiles-to-pyramid) can then isolate each channel at
stitch time by pointing its existing per-axis stitching helper at each
channel subdirectory, without any channel-aware logic in the stitcher
itself.
When the tile loop loads background images for an acquisition, the channel branch additionally looks for per-channel flat-field images under the background directory:
{background_dir}/{channel_id}/background.tif
Any channel whose file is present is flat-field corrected via
BackgroundCorrectionUtils.apply_flat_field_correction using the configured
method (divide by default). Channels whose file is missing are skipped
silently -- they are acquired without correction. The loader also accepts
the flat alternates {background_dir}/{channel_id}.tif and
{channel_id}.tiff for convenience.
This is the channel-axis analog of the PPM per-angle background path: the key is the channel id rather than the rotation angle, but the correction call and the missing-file behavior are the same.
BGACQUIRE commands that do not pass --channels fall through unchanged:
- Commands with
--anglestake the multi-angle branch (PPM and similar). - Commands with neither angles nor channels take the default single-snap branch (brightfield, single-snap fluorescence, laser scanning).
Modalities whose YAML has no channels: library never enter the channel
branch, so existing profiles keep working without any YAML edits.
Part of QPSC (QuPath Scope Control)
Requirements:
- Python 3.9 or later
- pip (Python package installer)
- Git (for
pip install git+https://...commands)
Important: This package depends on microscope-imageprocessing (required) and microscope-control (required).
ppm-library is an optional dependency, only needed for PPM (polarized light) modality support.
See the QPSC Installation Guide for complete setup instructions.
Install dependencies first:
# 1. Install microscope-imageprocessing (required - background correction, OME-TIFF I/O)
pip install git+https://github.com/uw-loci/microscope_imageprocessing.git
# 2. Install microscope-control (required - hardware abstraction)
pip install git+https://github.com/uw-loci/microscope_control.git
# 3. (Optional) Install ppm-library for PPM modality support
pip install git+https://github.com/uw-loci/ppm_library.git
# 4. Then install microscope_command_server
pip install git+https://github.com/uw-loci/microscope_command_server.gitgit clone https://github.com/uw-loci/microscope_command_server.git
cd microscope_command_server
pip install -e .For automated setup, use the QPSC setup script.
Cause: Package not installed correctly or virtual environment not activated.
Solution:
-
Ensure virtual environment is activated:
# Windows path\to\venv_qpsc\Scripts\Activate.ps1 # Linux/macOS source path/to/venv_qpsc/bin/activate
-
Reinstall the package:
pip install -e . --force-reinstall -
Verify installation:
pip show microscope-command-server
Cause: Entry points not registered or PATH not updated.
Solution:
Try running the server directly:
# Using Python module
python -m microscope_command_server.server.qp_server
# Or with PYTHONPATH set (if needed)
export PYTHONPATH="/path/to/parent/directory:$PYTHONPATH"
microscope-command-serverSymptom: OSError: [Errno 48] Address already in use
Cause: Another server instance or application is using port 5000.
Solution:
# Find process using port 5000
# Windows:
netstat -ano | findstr :5000
# macOS/Linux:
lsof -i :5000
# Kill the process if safeFor more troubleshooting, see the QPSC Installation Guide.
from microscope_command_server.server.qp_server import run_server
# Start server
run_server(host='0.0.0.0', port=5000)Or run from command line:
# Option 1: Entry point command (NOTE: uses hyphens, not underscores!)
microscope-command-server
# Option 2: Python module syntax
python -m microscope_command_server.server.qp_serverCommon mistake: The command is microscope-command-server (with hyphens), not microscope_command_server (with underscores).
from microscope_command_server.client import get_stageXY, move_stageXY
# Get current position
x, y = get_stageXY()
# Move stage
move_stageXY(x + 1000, y + 1000)The server coordinates between QuPath (Java) and the microscope hardware (Python/Micro-Manager):
QuPath Extension -> Socket Client -> Microscope Server
|
+---------------+---------------+
| | |
Microscope Microscope PPM Library
Control ImageProcessing (optional)
| | |
v v v
Micro-Manager Debayering, PPM-specific
Hardware Background, analysis and
OME-TIFF I/O, calibration
Z-stack projections
The microscope command server uses a dynamic configuration approach:
- Server loads a minimal generic configuration (
config_generic.yml) - Connects to Micro-Manager (hardware must be available)
- Waits for client connections
- Client sends ACQUIRE command with
--yaml /path/to/config.ymlparameter - Server loads microscope-specific config from the provided path
- Hardware settings are updated dynamically
- Microscope-specific methods (e.g., PPM rotation) are initialized
Commands like GETXY, MOVE, GETZ use the most recently loaded config:
- Before first ACQUIRE: Uses generic startup config with permissive stage limits
- After ACQUIRE: Uses the microscope-specific config from that acquisition
Note: Always provide the --yaml parameter in ACQUIRE commands to ensure correct microscope configuration.
This package includes automated unit tests for components that can be tested without hardware.
Pytest-compatible unit tests are located in the tests/ directory:
tests/test_tiles.py- Tests for TileConfiguration.txt parsing and generation
These tests:
- Run without hardware (use synthetic test data and temp files)
- Can be integrated into CI/CD pipelines
- Test protocol handling, tile configuration, and utility functions
Running Unit Tests:
# Install dev dependencies
pip install -e ".[dev]"
# Run all tests
pytest
# Run specific test file
pytest tests/test_tiles.py
# Run with coverage report
pytest --cov=microscope_command_server --cov-report=html
# View coverage report
open htmlcov/index.html # or xdg-open on LinuxTest Coverage:
Current automated tests achieve ~60-70% coverage for testable components:
- ✅ TileConfiguration parsing (coordinates extraction)
- ✅ TileConfiguration generation (2D pixel coordinates and 3D stage coordinates)
- ⏸️ Socket protocol (future test expansion)
- ⏸️ Server communication (requires integration testing)
Hardware Diagnostic Tools:
This package does not include standalone diagnostic tools. Hardware testing is performed via:
- The
TESTAFandTESTADAFserver commands (call diagnostic functions frommicroscope_control) - The
PPMSENSandPPMBIREFserver commands (call diagnostic functions fromppm_library)
See the microscope_control and ppm_library documentation for details on these diagnostic tools.
MIT License - see LICENSE for details.
This project was developed with assistance from Claude (Anthropic). Claude was used as a development tool for code generation, architecture design, debugging, and documentation throughout the project.