argos.workers¶
Workers¶
QThread worker for Alpaca discovery (UDP broadcast + TCP fallbacks).
Runs the blocking layered scan (argos.core.alpaca.discovery.discover_all())
in a background thread and emits signals when results are available or an
error occurs.
- Usage:
worker = DiscoveryWorker(port=32323, candidates=(“192.168.1.42”,)) worker.devices_found.connect(self._on_devices_found) worker.error_occurred.connect(self._on_error) worker.finished.connect(worker.deleteLater) worker.start()
QThread worker for continuous mount status polling.
Reads position, tracking, and slewing state from the mount every POLL_INTERVAL_MS milliseconds and emits a signal with fresh data.
The worker runs until stop() is called, then exits cleanly.
- Usage:
worker = MountPollingWorker(telescope) worker.position_updated.connect(self._on_position) worker.error_occurred.connect(self._on_poll_error) worker.start() # … worker.stop() worker.wait()
Live preview worker — continuous short exposures for camera feed.
- Runs in a QThread. Loop:
Set gain
Start exposure
Poll imageready every 100 ms
Download imagearray
Emit frame_ready(arr, start_dt, end_dt)
Repeat until stopped
- Signals:
frame_ready(np.ndarray, datetime, datetime) — (array, exposure_start_utc, exposure_end_utc) status_updated(str) — human-readable state string error_occurred(str) — fatal error, worker stops finished() — loop ended cleanly
PreviewProcessor — off-thread display compute so the preview never freezes.
The heavy per-frame work (debayer/render, star-detection metrics, per-channel
histograms) runs here on a QThread instead of the UI thread; the UI applies only
the cheap final stretch (kept on the UI thread so the sliders stay instant).
Latest-frame-wins: if frames arrive faster than they can be processed, stale
ones are dropped. See notes/capture_panel.md (threading note).
- class argos.workers.preview_processor.ProcessedFrame(display: ndarray, metrics: FrameMetrics, stars: StarField, green_shape: tuple[int, int], centers: ndarray, r: ndarray, g: ndarray, b: ndarray, lo: float, hi: float, vmin: float, vmax: float, vmean: float, vmedian: float, vstd: float, vmad: float, bit_depth: int)[source]¶
Bases:
objectEverything the UI needs to refresh, computed off the UI thread.
- metrics: FrameMetrics¶
- argos.workers.preview_processor.build_processed_frame(raw: ndarray, view: str, radius: int = 5) ProcessedFrame[source]¶
Render + analyse one raw frame (Qt-free, reusable).
Shared by the live
PreviewProcessor(off-thread) and the standalone analysis window (one-shot). Does the heavy work: debayer/render, frame metrics, star detection with the chosen apertureradius, and per-channel histograms.
SequenceWorker — executes a multi-step acquisition plan in a QThread.
Consumes a SequencePlan, expands it
to frames, drives the camera/filter wheel, and writes science-grade FITS files
into the Siril-compatible session folder. All blocking work (network, disk,
polling) happens here, off the UI thread; the UI subscribes to the signals.
- argos.workers.sequence_worker.FrameContextProvider¶
Callback that builds a fresh FrameContext for the current frame. Called with keyword args
object_nameandfilter_name; returns a FrameContext.alias of
Callable[[…],FrameContext]
Autofocus worker — two-stage HFD V-curve sweep with verification.
compute_hfd saturates in far defocus (its ROI is smaller than the donut),
so a single full-range sweep is a plateau with one dip — a parabola fitted to
it lands on noise (field-validated 2026-07-11: fit sent a focused scope from
1305 to 1055). Hence two stages:
Coarse:
num_stepsevenly-spaced positions across ±``half_range`` from the start. Only the raw minimum is trusted at this stage.Degenerate-sweep guard (P6): flat curve or edge minimum → restore the starting position and report, never move focus on a non-V sweep.
Fine:
refine_positionssamples inside ±one coarse interval around the raw minimum — the zone where HFD is informative — and the parabola is fitted on those samples only (with the vertex-vs-raw guard).Verify: move to the best position, take one last exposure, and report the measured HFD there, not the fit’s prediction.
- Signals:
step_done(step, total, position, hfd) — after each measurement best_found(position, hfd) — measured HFD at the final position error_occurred(message) finished()
SolveWorker — runs an ASTAP plate-solve off the UI thread (§6).
Solving spawns an external process that can take seconds; doing it on the UI
thread would freeze the app. This QThread runs solve_array() and reports
the result back via a signal.
AstrometryController — the live page’s solve lifecycle + auto-solve policy.
See notes/photometry_plan.md §4 (Workstream A3). A QObject that lives on the
UI thread and owns a SolveWorker. It centralises when and how the live
frame is plate-solved so the page doesn’t grow ad-hoc solve state:
at most one solve in flight (new requests are dropped while busy);
auto mode re-solves only when it’s worth it — there’s no WCS yet, the mount moved more than
astrometry.live_resolve_arcmin, orastrometry.live_resolve_selapsed (between solves a tracked grid stays valid, so we don’t thrash);a failed solve keeps the last good WCS — the grid never blanks on one miss;
live solves use a small search radius + bounded timeout and skip the slow whole-sky blind retry (via
build_solve_settings()), so the cadence can’t stall the UI.
- Signals:
solved(FrameWCS, WCSOverlay, str): a fresh solution + grid overlay + summary. failed(str): the last solve attempt failed (the previous WCS, if any, is kept). state(str): short human status for the page (“Plate-solving…”, “Idle”, …).
- argos.workers.astrometry_controller.RaDec¶
(ra_hours, dec_deg)of a pointing or target, orNone.
CatalogWorker — fetch VSX/VSP catalog objects off the UI thread.
A catalog query is an HTTP round-trip that can take seconds; on the UI thread it
would freeze the app. This QThread runs the queries and emits the bundled result
back via a signal, mirroring argos.workers.solve_worker.
- class argos.workers.catalog_worker.CatalogRequest(ra_deg: float, dec_deg: float, radius_deg: float, fov_arcmin: float, mag_limit: float = 15.0, max_results: int = 250, include_suspected: bool = True, want_comparisons: bool = True, comparison_target_name: str | None = None, comparison_ra_deg: float | None = None, comparison_dec_deg: float | None = None, want_field_stars: bool = True, field_star_mag_limit: float = 18.0, field_star_max_results: int = 2000, want_named_objects: bool = True, named_object_max_results: int = 500, named_objects_allow_network: bool = True, want_exoplanet_hosts: bool = True, exoplanet_hosts_allow_network: bool = True)[source]¶
Bases:
objectA field query, derived from a solved frame’s WCS.
- class argos.workers.catalog_worker.CatalogResult(variables: list[VariableStar] = <factory>, comparisons: list[ComparisonStar] = <factory>, field_stars: list[GaiaStar] = <factory>, named_objects: list[NamedFieldObject] = <factory>, exoplanet_hosts: list[CachedExoplanetHost] = <factory>, field_star_limit: int = 0, named_object_limit: int = 0, warnings: list[str] = <factory>, error: str = '')[source]¶
Bases:
objectOutcome of a
CatalogRequest.erroris empty on success.- comparisons: list[ComparisonStar]¶
- exoplanet_hosts: list[CachedExoplanetHost]¶
- named_objects: list[NamedFieldObject]¶
- variables: list[VariableStar]¶
Background bridge for the CDS object-name resolver.
Background bridge for NASA Exoplanet Archive lookups.
Background worker for an explicit observing-site search.
PhotometryBatchWorker — re-run differential photometry over saved FITS (WS7).
The one genuinely-unique capability of the deleted Photometry Setup window:
measure a whole folder of already-saved subs against a target set, off the UI
thread. Uses the SAME measurement core as the live path
(argos.core.photometry.params.measure_frame() → measure_targets), so
comps get their catalog magnitudes and the physics is identical; the only
intentional live/batch difference is the aperture FWHM (a saved sub carries no
measured FWHM, so the aperture floors to aperture_min_px).
Emits progress + a per-frame point stream and writes the canonical 9-column
CSVs (LightCurve.to_csv()) so Analyze can reload the result. Cancellable
between frames.
- class argos.workers.photometry_batch_worker.BatchRequest(fits_paths: list[Path], wcs: object, target_set: TargetSet, params: PhotometryParams, out_dir: Path, object_name: str = 'untitled', site: tuple[float | None, float | None, float] = (None, None, 0.0), diagnostics: bool = True)[source]¶
Bases:
objectInputs for one batch re-run.
- params: PhotometryParams¶
- class argos.workers.photometry_batch_worker.BatchResult(curves: dict[str, ~argos.core.photometry.lightcurve.LightCurve]=<factory>, frames_done: int = 0, error: str = '', rotation_deg: float = 0.0, shift_px: float = 0.0)[source]¶
Bases:
objectOutcome of a batch run.
erroris empty on success.- curves: dict[str, LightCurve]¶
CameraService — the single camera-ownership state machine.
Exactly one activity may drive the camera at a time. Before WS3, every
handler in the imaging page re-derived “who owns the camera” from scattered
isRunning() checks over three workers — each new feature had to remember
every guard. This service makes contention bugs impossible by construction:
callers acquire the camera for a CameraState and release it when
done; the service is the one source of truth.
It deliberately does not re-implement exposures. LivePreviewWorker, SequenceWorker and AutofocusWorker keep their acquisition loops; the service arbitrates ownership and centralises start/stop of the LivePreviewWorker via injected hooks (so it stays hardware-free and unit-testable).
Priority (highest wins): SEQUENCE > AUTOFOCUS > LIVE / SINGLE.
State-transition policy:
acquire(requested)
IDLE → granted
same state → refused ("already running")
LIVE/SINGLE, higher-priority requestor
→ granted, the preview loop is preempted
SINGLE → LIVE → granted in place (the transient
Take-shot loop is upgraded to Live;
the preview worker keeps running)
anything else → refused with a human-readable reason
begin_sequence_autofocus()
SEQUENCE → AUTOFOCUS, remembering to resume SEQUENCE on release —
the mid-sequence AF handshake (the sequence worker is blocked on
``resume_after_autofocus`` while AF owns the camera).
release(owner)
Only the current owner releases; stale/double releases are safe
no-ops. Releasing AUTOFOCUS returns to SEQUENCE when the handshake
is active, else to IDLE.
Signals¶
state_changed(CameraState)— emitted after every ownership transition.acquire_refused(str)— human-readable reason to log as WARN.preempted(str)— a live/single preview was stopped for a higher-priority owner; log this as INFO.
- class argos.workers.camera_service.CameraState(value)[source]¶
Bases:
EnumWho owns the camera right now.
- AUTOFOCUS = 'autofocus'¶
- IDLE = 'idle'¶
- LIVE = 'live'¶
- SEQUENCE = 'sequence'¶
- SINGLE = 'single'¶
QThread worker for quiet background network checks.
Every cycle it answers two questions the field observer actually has: is the Seestar reachable, and is there internet for the AAVSO catalogs? Both are plain TCP connects with short timeouts — no HTTP, no payload — so a cycle costs a few packets at most.
The Seestar check targets the configured Alpaca host/port (re-read every cycle, so a profile switch is picked up without a restart). The internet check connects to well-known public resolvers (Cloudflare, then Google).
- Usage:
monitor = NetworkMonitor(config) monitor.state_changed.connect(status_bar.set_network) monitor.start() # … monitor.stop() monitor.wait()
QThread bridge between the asyncio Stellarium server and the Qt UI.
Owns its own asyncio event loop running inside QThread.run(). The UI
thread interacts with the worker only through Qt signals/slots; all calls
that cross into the asyncio world are routed through call_soon_threadsafe
so the loop never sees a foreign-thread access.
The worker is restartable: stop it, change host/port, start it again.