Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
76b366c
feat: add squid_service package skeleton and fastapi dependency
hongquanli Jul 2, 2026
71acf95
feat: add canonical fault model, codes, and HTTP mapping for core ser…
hongquanli Jul 2, 2026
5e82b7f
fix: move test_faults.py into software/tests
hongquanli Jul 2, 2026
4c66b72
feat: add instrument state machine for core service
hongquanli Jul 2, 2026
ed400e7
feat: add event bus with replay buffer for SSE stream
hongquanli Jul 2, 2026
a4817cf
feat: add job records and store with last-job persistence
hongquanli Jul 2, 2026
11abbea
fix: return deep copies from JobStore to prevent torn reads; drop unu…
hongquanli Jul 2, 2026
df7bc45
feat: add well parsing with GUI-consistent plate offsets
hongquanli Jul 2, 2026
6085de5
feat: add REST request models, service config, and CORE_SERVICE INI s…
hongquanli Jul 2, 2026
2c669ac
feat: add SquidCoreService facade (system, motion, imaging, autofocus)
hongquanli Jul 2, 2026
973c14c
test: cover hardware-failure fault branches in core service
hongquanli Jul 2, 2026
09a3970
feat: add named acquisition-method registry (URS API-METH-001..005)
hongquanli Jul 2, 2026
ad91cfc
feat: add acquisition jobs, preflight, method/grid routing, and contr…
hongquanli Jul 2, 2026
60d336c
fix: derive acquisition end reason without slack notifier (outcome/ER…
hongquanli Jul 2, 2026
3fe6169
feat: add REST+SSE API for core service with canonical fault responses
hongquanli Jul 3, 2026
039301c
test: cover SSE resume_gap emission and replay/live dedupe
hongquanli Jul 3, 2026
8e77355
feat: add opt-in python_exec debug endpoint and /v1/debug/settings
hongquanli Jul 3, 2026
0abdad1
fix: fault-wrap python_exec image save and cover autosave path
hongquanli Jul 3, 2026
4bba18f
feat: serve core service REST API from the GUI process (port 5060)
hongquanli Jul 3, 2026
d488782
fix: guard squid_service imports so GUI starts if core service unavai…
hongquanli Jul 3, 2026
af44b4d
feat: rewrite MCP bridge as curated-tool client of the REST API
hongquanli Jul 3, 2026
2fe5802
feat: restore laser-AF image capture via REST and MCP bridge
hongquanli Jul 3, 2026
eb700f4
feat: port run_acquisition.py to REST API and update automation docs
hongquanli Jul 3, 2026
b33b0de
docs: deprecate legacy TCP control server in favor of core service
hongquanli Jul 3, 2026
ed5082d
fix: address final whole-branch review findings
hongquanli Jul 3, 2026
30f79ce
fix: change default REST port 5060 -> 8060 (browsers block 5060 as un…
hongquanli Jul 3, 2026
f0acf61
docs: add copy-paste plate-scan quickstart
hongquanli Jul 3, 2026
5716e07
docs: add plain-English Claude/MCP quickstart
hongquanli Jul 3, 2026
9972592
docs: rename quickstart-plate-scan.md -> quickstart-api.md
hongquanli Jul 3, 2026
d4764ae
feat(core-service): add wells-by-name loader field and z_reference re…
hongquanli Jul 4, 2026
eac93f7
feat(core-service): wells-by-name acquisitions and run-time z_referen…
hongquanli Jul 4, 2026
74cc011
docs(core-service): document wells-by-name methods and z_reference po…
hongquanli Jul 4, 2026
00702d1
feat: Add headless mode — run the core service without the GUI
hongquanli Jul 5, 2026
2e076ab
fix: declare core-service deps explicitly in setup_22.04.sh
hongquanli Jul 10, 2026
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
36 changes: 36 additions & 0 deletions software/control/_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,6 +1277,14 @@ def _validate_objective_changer_flags(use_xeryon: bool, use_turret: bool) -> Non
CONTROL_SERVER_PORT = 5050
ANTHROPIC_API_KEY = None # Set via GUI (Settings > Set Anthropic API Key...)

# Core Service (REST + SSE API) settings; overridable via [CORE_SERVICE] in the machine INI
CORE_SERVICE_ENABLED = True
CORE_SERVICE_HOST = "127.0.0.1"
CORE_SERVICE_PORT = 8060
CORE_SERVICE_AUTH_ENABLED = False
CORE_SERVICE_AUTH_TOKEN = ""
CORE_SERVICE_METHODS_DIR = "machine_configs/acquisition_methods"


# Slack Notifications - send real-time notifications during acquisition
class SlackNotifications:
Expand Down Expand Up @@ -1443,6 +1451,34 @@ class SlackNotifications:
except Exception as e:
log.warning(f"Failed to load Views settings from config: {e}")

# Load Core Service settings from config file
try:
_core_service_config = ConfigParser()
_core_service_config.read(CACHED_CONFIG_FILE_PATH)
if _core_service_config.has_section("CORE_SERVICE"):
if _core_service_config.has_option("CORE_SERVICE", "enabled"):
CORE_SERVICE_ENABLED = _core_service_config.get("CORE_SERVICE", "enabled").lower() in (
"true",
"1",
"yes",
)
if _core_service_config.has_option("CORE_SERVICE", "host"):
CORE_SERVICE_HOST = _core_service_config.get("CORE_SERVICE", "host")
if _core_service_config.has_option("CORE_SERVICE", "port"):
CORE_SERVICE_PORT = _core_service_config.getint("CORE_SERVICE", "port")
if _core_service_config.has_option("CORE_SERVICE", "auth_enabled"):
CORE_SERVICE_AUTH_ENABLED = _core_service_config.get("CORE_SERVICE", "auth_enabled").lower() in (
"true",
"1",
"yes",
)
if _core_service_config.has_option("CORE_SERVICE", "auth_token"):
CORE_SERVICE_AUTH_TOKEN = _core_service_config.get("CORE_SERVICE", "auth_token")
if _core_service_config.has_option("CORE_SERVICE", "methods_dir"):
CORE_SERVICE_METHODS_DIR = _core_service_config.get("CORE_SERVICE", "methods_dir")
except Exception as e:
log.warning(f"Failed to load Core Service settings from config: {e}")

# Load GENERAL settings from config file
try:
_general_config = ConfigParser()
Expand Down
20 changes: 20 additions & 0 deletions software/control/acquisition_yaml_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ class AcquisitionYAMLData:
overlap_percent: float = 10.0
scan_shape: Optional[str] = None
wellplate_regions: Optional[List[Dict]] = None # [{name, center_mm, shape}, ...]
# Wells-by-name (additive): X/Y derived from the plate definition at run time.
# Mutually exclusive with wellplate_regions. Normalized to a comma-separated string
# ("A1:B3" or "A1,B2,C3"); None when the method uses explicit regions instead.
wells: Optional[str] = None

# Flexible-specific
nx: int = 1
Expand Down Expand Up @@ -108,6 +112,21 @@ def parse_acquisition_yaml(file_path: str) -> AcquisitionYAMLData:
if wellplate_regions and len(wellplate_regions) > 0:
scan_shape = wellplate_regions[0].get("shape")

# Wells-by-name (additive): accept a string ("A1:B3", "A1,B2") or a YAML list of
# names (joined with ","). Normalize empty/absent to None. Mutually exclusive with
# a non-empty regions list.
wells_raw = wellplate_scan.get("wells")
if isinstance(wells_raw, (list, tuple)):
wells = ",".join(str(w).strip() for w in wells_raw)
elif wells_raw is not None:
wells = str(wells_raw).strip()
else:
wells = None
if not wells:
wells = None
if wells and wellplate_regions:
raise ValueError("wellplate_scan: specify either 'wells' or 'regions', not both")

return AcquisitionYAMLData(
widget_type=widget_type,
xy_mode=acq.get("xy_mode", "Select Wells"),
Expand All @@ -134,6 +153,7 @@ def parse_acquisition_yaml(file_path: str) -> AcquisitionYAMLData:
overlap_percent=overlap,
scan_shape=scan_shape,
wellplate_regions=wellplate_regions,
wells=wells,
# Flexible-specific
nx=flexible_scan.get("nx", 1),
ny=flexible_scan.get("ny", 1),
Expand Down
4 changes: 4 additions & 0 deletions software/control/microscope_control_server.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""
DEPRECATED: superseded by the Squid Core Service REST API (squid_service/, port 8060)
and the rewritten MCP bridge. Kept for one release for external TCP clients.
See docs/core-service-api.md. No new commands should be added here.

TCP Control Server for Squid Microscope

This module provides a TCP socket server that runs inside the GUI process,
Expand Down
80 changes: 63 additions & 17 deletions software/docs/automation.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
# Automated Acquisition via Scripts

This document describes how to run automated acquisitions using the `run_acquisition.py` script. This approach is ideal for batch processing, CI pipelines, or headless operation.
This document describes how to run automated acquisitions using the `run_acquisition.py` script, or directly via the REST API. This approach is ideal for batch processing, CI pipelines, or headless operation.

For AI-assisted control via Claude Code, see [MCP Integration](mcp_integration.md).
**New here?** The [API Quickstart](quickstart-api.md) is the fastest way in — three ways to launch a scan, copy-paste examples, verified against the simulator.

For AI-assisted control via Claude Code, see [MCP Integration](mcp_integration.md). For the full REST API reference (endpoints, faults, jobs, SSE), see [Core Service API](core-service-api.md).

## Overview

The automation workflow:
1. Configure and save an acquisition in the GUI (creates `acquisition.yaml`)
2. Run the acquisition programmatically using the saved YAML
1. Configure and save an acquisition in the GUI (creates `acquisition.yaml`), or define a named server-side method under `machine_configs/acquisition_methods/`
2. Run the acquisition programmatically using the saved YAML (or method name) via the REST API
3. Optionally override parameters like wells or save location

**Note:** Only wellplate mode acquisitions are supported via scripting. FlexibleMultiPoint acquisitions must be run from the GUI.
**Note:** Only wellplate mode acquisitions are supported via scripting/the REST API. FlexibleMultiPoint acquisitions must be run from the GUI.

## Prerequisites

- Squid software installed and configured
- Python environment with Squid dependencies
- Python environment with Squid dependencies (includes `httpx`)

## Enabling the Control Server

The TCP control server must be running to accept commands.
The Squid GUI process serves the automation API on two ports:

- **REST API (port 8060)** — the current API; used by `run_acquisition.py`, the MCP bridge, and any `curl`/`httpx` client. See [Core Service API](core-service-api.md).
- **Legacy TCP control server (port 5050)** — newline-delimited JSON protocol; **deprecated**, kept only for backward compatibility with older integrations.

Both start together.

**Option 1: Via command line (recommended for automation)**
```bash
Expand All @@ -30,22 +37,45 @@ python3 main_hcs.py --start-server
**Option 2: Via GUI**
- Go to Settings and check "Enable MCP Control Server"

### curl quick-start

```bash
# Is the service alive?
curl http://127.0.0.1:8060/v1/healthz

# Instrument state, active job, latest fault
curl http://127.0.0.1:8060/v1/system/status

# Start an acquisition from a saved YAML (returns 202 + job handle)
curl -X POST http://127.0.0.1:8060/v1/acquisitions \
-H "Content-Type: application/json" \
-d '{"yaml_path": "/path/to/acquisition.yaml"}'
```

## Basic Usage

### Run an acquisition
```bash
python scripts/run_acquisition.py --yaml /path/to/acquisition.yaml --wait
```

### Run from a named server-side method
```bash
python scripts/run_acquisition.py --method my_method --wait
```

### Run in simulation mode
```bash
python scripts/run_acquisition.py --yaml /path/to/acquisition.yaml --simulation --wait
```

### Validate YAML without running (dry run)
### Validate against the live instrument without running (dry run)
```bash
python scripts/run_acquisition.py --yaml /path/to/acquisition.yaml --dry-run
python scripts/run_acquisition.py --yaml /path/to/acquisition.yaml --no-launch --dry-run
```
This runs the server-side preflight checks (`POST /v1/acquisitions/preflight`) — YAML parsing, widget type,
hardware match, channel names, regions, and output path — without starting the acquisition. It requires a
reachable server (launch the GUI first, or omit `--no-launch` to let the script launch it).

## Parameter Overrides

Expand Down Expand Up @@ -74,23 +104,33 @@ python scripts/run_acquisition.py --yaml acquisition.yaml --no-launch --wait

### Custom host/port
```bash
python scripts/run_acquisition.py --yaml acquisition.yaml --host 192.168.1.100 --port 5050
python scripts/run_acquisition.py --yaml acquisition.yaml --host 192.168.1.100 --port 8060
```

Non-loopback binds **require** authentication (the service refuses to start without `auth_enabled=true` +
`auth_token`; see [Core Service API — Authentication](core-service-api.md#authentication)). The script reads
the bearer token from the `SQUID_API_TOKEN` environment variable and sends it on every request:

```bash
SQUID_API_TOKEN=your-token python scripts/run_acquisition.py --yaml acquisition.yaml --host 192.168.1.100 --port 8060
```

## Command Line Options

| Option | Description |
|--------|-------------|
| `--yaml`, `-y` | Path to acquisition.yaml file (required) |
| `--yaml`, `-y` | Path to acquisition.yaml file (exactly one of `--yaml`/`--method` required) |
| `--method` | Name of a server-side acquisition method under `machine_configs/acquisition_methods/` (alternative to `--yaml`) |
| `--wells`, `-w` | Override wells from YAML (e.g., 'A1:B3' or 'A1,A2,B1') |
| `--base-path` | Override save location |
| `--simulation` | Run in simulation mode (no hardware) |
| `--wait` | Wait for acquisition to complete |
| `--timeout` | Acquisition timeout in seconds (only with `--wait`) |
| `--no-launch` | Don't launch GUI, connect to existing one |
| `--dry-run` | Validate YAML without running |
| `--dry-run` | Run server-side preflight checks only; don't start the acquisition |
| `--verbose`, `-v` | Show detailed output |
| `--host` | Server host (default: localhost) |
| `--port` | Server port (default: 5050) |
| `--host` | REST API host (default: 127.0.0.1) |
| `--port` | REST API port (default: 8060) |

## Exit Codes

Expand Down Expand Up @@ -148,11 +188,11 @@ jobs:
### "Control server did not become available"
- Ensure the GUI is running with `--start-server` flag
- Or enable via Settings → Enable MCP Control Server
- Check that port 5050 is not blocked
- Check that port 8060 (REST API) is not blocked

### "TCP command only supports wellplate mode"
### "Only wellplate-mode YAMLs are supported by the API"
- The YAML was saved from FlexibleMultiPoint mode
- Re-save the acquisition using wellplate mode, or run from GUI
- FlexibleMultiPoint acquisitions must be run from the GUI, not via the script/REST API

### "Hardware configuration mismatch"
- The current objective or camera binning differs from when YAML was saved
Expand All @@ -162,7 +202,13 @@ jobs:
- The script will retry up to 10 consecutive errors before failing
- Check network connectivity and GUI status

### 401 Unauthorized
- Auth is only required when the server is bound to a non-loopback host; see
[Core Service API — Authentication](core-service-api.md#authentication)
- Pass a token with `-H "Authorization: Bearer <token>"` (curl) or set `SQUID_API_TOKEN` (MCP bridge)

## See Also

- [Core Service API](core-service-api.md) - Full REST API reference (endpoints, faults, jobs, SSE)
- [MCP Integration](mcp_integration.md) - Control via Claude Code / AI agents
- [Configuration System](configuration-system.md) - Setting up imaging channels and profiles
Loading
Loading