argos.core.alpaca

Devices

Low-level ASCOM Alpaca HTTP client.

Handles GET and PUT requests to any Alpaca device endpoint, manages ClientID / ClientTransactionID, and maps Alpaca error numbers to typed Python exceptions.

All methods are synchronous and intended to run inside a QThread worker. Never call them from the Qt main thread.

class argos.core.alpaca.client.AlpacaClient(host: str, port: int, get_timeout: float = 5.0, put_timeout: float = 10.0)[source]

Bases: object

Synchronous ASCOM Alpaca client for a single device endpoint.

Parameters:
  • host – IP address or hostname of the Alpaca server.

  • port – TCP port of the Alpaca server (default 4700 for Seestar).

  • get_timeout – Seconds before a GET request times out.

  • put_timeout – Seconds before a PUT request times out.

BASE_PATH = '/api/v1'
property base_url: str
close() → None[source]

Release the underlying HTTP session.

get(device_type: str, device_number: int, attribute: str, **extra_params: Any) → Any[source]

Send a GET request and return the Alpaca Value field.

Parameters:
  • device_type – e.g. “telescope”, “camera”, “focuser”, “filterwheel”

  • device_number – Usually 0 for the Seestar.

  • attribute – Alpaca property name, e.g. “rightascension”.

  • **extra_params – Additional query parameters.

Returns:

The Value field from the Alpaca JSON response.

Raises:
put(device_type: str, device_number: int, method: str, **body_params: Any) → Any[source]

Send a PUT request and return the Alpaca Value field (if any).

Parameters:
  • device_type – e.g. “telescope”, “camera”.

  • device_number – Usually 0.

  • method – Alpaca method name, e.g. “tracking”.

  • **body_params – Form fields to include in the request body.

Returns:

The Value field from the Alpaca JSON response, or None.

Raises:
exception argos.core.alpaca.client.AlpacaConnectionError(host: str, port: int, reason: str = '')[source]

Bases: AlpacaError

Could not reach the Alpaca device.

exception argos.core.alpaca.client.AlpacaError(number: int, message: str)[source]

Bases: Exception

Alpaca device returned a non-zero ErrorNumber.

exception argos.core.alpaca.client.AlpacaTimeoutError(host: str, port: int)[source]

Bases: AlpacaError

Request to the Alpaca device timed out.

ASCOM Alpaca discovery — UDP broadcast plus TCP fallbacks.

Layered strategy (discover_all), designed for field networks where the UDP broadcast often fails (phone hotspots block it, some APs isolate clients):

  1. UDP broadcast on port 32227 (the standard Alpaca discovery protocol).

  2. Direct HTTP probes of known candidates — the last-used host and the Seestar’s fixed address in AP mode.

  3. A TCP sweep of the local /24 subnet, confirming hits against the Alpaca management endpoint.

This module is pure Python with no Qt dependency. It is designed to be called from a QThread worker (DiscoveryWorker).

class argos.core.alpaca.discovery.AlpacaDevice(host: str, port: int)[source]

Bases: object

A discovered Alpaca server on the local network.

host: str
port: int
argos.core.alpaca.discovery.discover(timeout: float = 8.0) → list[AlpacaDevice][source]

Broadcast a discovery packet and return all responding Alpaca devices.

Parameters:

timeout – How long to wait for responses (seconds).

Returns:

List of discovered AlpacaDevice instances (may be empty).

argos.core.alpaca.discovery.discover_all(port: int, candidates: tuple[str, ...] = (), broadcast_timeout: float = 8.0) → list[AlpacaDevice][source]

Layered discovery: UDP broadcast, then candidate probes, then /24 scan.

Parameters:
  • port – Alpaca HTTP port for the probe/scan fallbacks (the broadcast reply carries its own port).

  • candidates – Hosts worth probing directly when the broadcast fails — typically the last-used host and the profile’s AP-mode address.

  • broadcast_timeout – How long to wait for broadcast replies.

Returns:

Discovered devices, possibly empty. The first entry is the best pick.

argos.core.alpaca.discovery.local_ipv4() → str | None[source]

Best-guess primary local IPv4, or None when no route exists.

UDP connect() sends no packet — it only asks the OS which source address it would route through, so this works with no internet.

argos.core.alpaca.discovery.probe_alpaca(host: str, port: int, timeout: float = 2.0) → bool[source]

True if host:port answers like an Alpaca server.

Hits the management API (the one endpoint every Alpaca server must serve) rather than a bare TCP connect, so a random web server on the same port is not mistaken for the Seestar.

argos.core.alpaca.discovery.scan_subnet(port: int, timeout: float = 0.4) → list[AlpacaDevice][source]

Sweep the local /24 for an open Alpaca port, then confirm via HTTP.

~254 TCP connects across a thread pool finish in a couple of seconds; only hosts with the port open get the (slower) management probe.

ASCOM Alpaca telescope wrapper — backed by alpyca.

Uses the official alpyca library (ASCOM Initiative) instead of raw HTTP calls. Key benefits: - Automatic ImageBytes support (8x faster image transfers) - Typed exceptions with error codes - Thread-safe (safe to call from QThread workers)

All methods are synchronous and must run inside a QThread worker. Never call them from the Qt main thread.

Seestar S30 Pro known limitations:
  • SlewToAltAzAsync→ DriverException (error 1024) — not implemented

  • Unpark → does not open the arm physically

  • MoveAxis → confirmed working on firmware 7.18+ (tested 2026-05)

class argos.core.alpaca.telescope.MountPosition(ra: float, dec: float, altitude: float, azimuth: float, tracking: bool, slewing: bool)[source]

Bases: object

Current pointing position of the mount.

alt_str() → str[source]
altitude: float
az_str() → str[source]
azimuth: float
dec: float
dec_str() → str[source]
ra: float
ra_str() → str[source]
slewing: bool
tracking: bool
class argos.core.alpaca.telescope.Telescope(host: str, port: int)[source]

Bases: object

ASCOM Alpaca telescope controller, backed by alpyca.

Parameters:
  • host – IP address of the Seestar.

  • port – Alpaca HTTP port (discovered via UDP, typically 4700).

abort_slew() → None[source]

Immediately stop any ongoing slew.

property alignment_mode: str | None

"Alt-Az", "EQ", "EQ (GEM)", or None when the firmware doesn’t say.

Type:

Mount geometry reported at connect

connect() → str[source]

Connect to the mount and return its name.

Sequence:
  1. Connected = True

  2. Sync UTC date (fixes Seestar clock-sync bug on firmware < 3.0.2)

  3. Check CanSlewAsync

Raises:

AlpacaError – Connection or device error.

disconnect() → None[source]

Disconnect from the mount.

get_altaz() → tuple[float, float][source]

Return current (altitude, azimuth) in degrees.

get_position() → MountPosition[source]

Read the current position from the mount.

Raises:

AlpacaError – Communication or device error.

get_tracking_rate() → int | None[source]

Return the current ASCOM DriveRates value, or None if unsupported.

property is_connected: bool
is_parked() → bool[source]

Return True if the mount reports being parked.

move_axis(axis: int, rate: float) → None[source]

Start continuous jog on one axis at the given rate (deg/s).

The mount keeps moving until stop_axis() is called.

Parameters:
  • axis – 0 = Primary (RA/Az), 1 = Secondary (Dec/Alt).

  • rate – Speed in deg/s. Positive = North/East, negative = South/West. Pass 0.0 to stop.

Raises:

AlpacaError – Device error or axis not supported.

park() → None[source]

Park the mount (closes the mechanical arm on Seestar S30 Pro).

If atpark=True (e.g. native app opened arm without Alpaca Unpark), sends Unpark first so the subsequent Park triggers physical movement.

Raises:

AlpacaError – Mount does not support Park, or driver error.

pulse_guide(direction: int, duration_ms: int) → None[source]

Send a PulseGuide command (guide-rate only — not for manual jogging).

Parameters:
  • direction – 0=North, 1=South, 2=East, 3=West (ASCOM constants).

  • duration_ms – Duration in milliseconds.

set_target(ra: float, dec: float) → None[source]

Set the mount’s TargetRightAscension/TargetDeclination without slewing.

Useful before a slew or sync so the mount’s pointing model records what we were aiming at — independent of the actual pointing it eventually achieves. Failures are logged and re-raised; some firmwares do not allow writing these properties (in which case the slew code path tolerates the failure on its own).

set_tracking(enabled: bool) → None[source]

Enable or disable sidereal tracking.

set_tracking_rate(rate: int) → None[source]

Select the tracking rate model.

Parameters:

rate – ASCOM DriveRates enum value: 0=Sidereal, 1=Lunar, 2=Solar, 3=King.

Raises:

AlpacaError – Mount rejected the rate (e.g. only Sidereal supported).

side_of_pier() → str | None[source]

Return ‘EAST’ / ‘WEST’ / ‘UNKNOWN’, or None if the mount can’t tell.

For an alt-az like the Seestar this concept is meaningless — the property may simply not be exposed.

slew_to(ra: float, dec: float) → None[source]

Start an asynchronous GoTo slew (mirrors NINA sequence).

  1. Check AtPark — raise if parked

  2. Enable tracking

  3. Set TargetRightAscension / TargetDeclination (log failures, don’t hide them)

  4. Call SlewToCoordinatesAsync

  5. Verify Slewing=True was set (warn if not — some mounts report False when already at target, which is acceptable)

Parameters:
  • ra – Target RA in decimal hours (J2000), 0–24.

  • dec – Target Dec in decimal degrees (J2000), -90 to +90.

Raises:

AlpacaError – Mount parked or slew rejected.

stop_axis(axis: int) → None[source]

Stop a jogging axis (sets rate to 0).

Parameters:

axis – 0 = Primary (RA/Az), 1 = Secondary (Dec/Alt).

sync_to(ra: float, dec: float) → None[source]

Sync the mount’s pointing model to the given coordinates.

Used after plate solving to correct the pointing model.

Raises:

AlpacaError – Tracking not enabled, or sync rejected by mount.

unpark() → None[source]

Unpark via Alpaca.

Note: does not open the arm physically on the Seestar S30 Pro.

ASCOM Alpaca camera wrapper — backed by alpyca.

Uses alpyca which automatically handles:
  • ImageBytes (binary, 8x faster than JSON) vs JSON imagearray

  • Typed exceptions

  • Thread-safe HTTP session

All methods are synchronous — run inside a QThread worker only.

Seestar constraints:
  • Do NOT use ROI/subframing (firmware bug in Alpaca driver)

  • Sensor geometry comes from the active telescope profile; the driver’s CameraXSize/CameraYSize win over it at connect.

class argos.core.alpaca.camera.Camera(host: str, port: int)[source]

Bases: object

ASCOM Alpaca camera controller, backed by alpyca.

Parameters:
  • host – IP address of the Seestar.

  • port – Alpaca HTTP port.

connect() → str[source]

Connect and return the camera name.

Also reads sensor dimensions and gain range for the UI.

Raises:

AlpacaError – Connection or device error.

disconnect() → None[source]

Disconnect from the camera.

get_binning() → int[source]

Return the current symmetric binning factor (BinX), or 1.

get_ccd_temperature() → float | None[source]

Return sensor temperature in °C, or None if unsupported.

Falls back to HeatSinkTemperature if CCDTemperature is absent (the Seestar is air-cooled — no Peltier — but exposes a sensor probe).

get_electrons_per_adu() → float | None[source]

Return ElectronsPerADU from the driver, or None if not exposed.

get_full_well() → int | None[source]

Return full-well capacity in electrons, or None.

get_gain() → int[source]

Return the current gain, or gain_min if the driver lacks Gain.

get_image_array() → ndarray[source]

Download the last captured image as a 2-D numpy uint16 array.

Tries ImageArrayRaw first (binary ImageBytes, zero-copy numpy path) and falls back to ImageArray (JSON) if the device does not support ImageBytes.

Returns:

2-D numpy array, shape (height, width), dtype uint16.

Raises:

AlpacaError – On communication or device error.

get_offset() → int | None[source]

Return the electronic offset (bias level setting), or None.

get_readout_mode_name() → str | None[source]

Return current readout mode name (e.g. ‘Normal’, ‘HCG’), or None.

get_sensor_metadata() → dict[source]

Snapshot of static sensor metadata for FITS / diagnostics.

Each field is tolerant — missing values are simply absent from the dict.

get_state() → int[source]

Return the current camera state (0–5).

property is_connected: bool
is_image_ready() → bool[source]

Return True when a new image is available to download.

set_binning(value: int) → None[source]

Set symmetric binning (BinX = BinY, clamped to max_bin) and retarget the frame size so the next exposure fills the binned sensor.

set_gain(gain: int) → None[source]

Set camera gain (clamped to range). No-op if the driver doesn’t implement Gain — some cameras / the ASCOM sim don’t expose it, and that must not break capture.

set_offset(value: int) → None[source]

Set the electronic offset (clamped to the driver range). No-op if the driver doesn’t implement Offset — that must not break capture.

start_exposure(duration: float, light: bool = True) → None[source]

Start a single exposure.

Parameters:
  • duration – Exposure time in seconds.

  • light – True for light frame, False for dark/bias.

Raises:

AlpacaError – If the camera rejects the command.

stop_exposure() → None[source]

Abort any running exposure.

try_get_gain() → int | None[source]

Gain as the driver reports it, or None when the driver has no Gain.

Unlike get_gain() this never substitutes a default — it exists for header truthfulness (P1): a value we cannot read back must not be presented as verified.

ASCOM Alpaca focuser wrapper — backed by alpyca.

The Seestar S30 Pro exposes two focusers via Alpaca (configureddevices discovery): index 0 is the telephoto focuser used by the science camera, index 1 is the wide-angle finder. Argos only drives the telephoto focuser — the wide-angle is for the native Seestar app’s plate-solve loop.

All methods are synchronous and must run inside a QThread worker; never call them from the Qt main thread (network round-trips can take 200+ ms).

class argos.core.alpaca.focuser.Focuser(host: str, port: int, device_index: int = 0)[source]

Bases: object

ASCOM Alpaca focuser controller, backed by alpyca.

Parameters:
  • host – IP address of the Seestar.

  • port – Alpaca HTTP port.

  • device_index – 0 = telephoto (science), 1 = wide-angle. Default 0.

connect() → str[source]

Connect and cache the focuser’s capability flags.

disconnect() → None[source]
get_position() → int[source]
get_temperature() → float | None[source]

Return the focuser-reported temperature, or None if unsupported.

halt() → None[source]

Stop any motion in progress. Safe to call when idle.

property is_connected: bool
is_moving() → bool[source]
move_to(position: int) → None[source]

Send an absolute Move command.

Clamped to [0, max_step] so the caller can pass adjusted values from a “step by N” helper without worrying about over/underflow.

step(delta: int) → int[source]

Move by delta steps relative to the current position.

Returns the target position that was commanded so the caller can immediately reflect it in the UI without re-polling.

ASCOM Alpaca filter wheel wrapper — backed by alpyca.

Seestar S30 Pro filter wheel (device 0):

Position 0 — Dark (closed / calibration) Position 1 — IR (infrared pass) Position 2 — LP (light pollution filter)

All methods are synchronous. Run from a QThread worker only.

class argos.core.alpaca.filterwheel.FilterWheel(host: str, port: int)[source]

Bases: object

ASCOM Alpaca filter wheel controller.

Parameters:
  • host – IP address of the Seestar.

  • port – Alpaca HTTP port (32323).

connect() → None[source]

Connect to the filter wheel.

Raises:

AlpacaError – Connection or device error.

disconnect() → None[source]

Disconnect from the filter wheel.

get_position() → int[source]

Return the current filter position (0–2).

Returns -1 while the wheel is moving.

Raises:

AlpacaError – Device error.

property is_connected: bool
position_name() → str[source]

Return the name of the current filter position.

set_position(position: int) → None[source]

Command a filter change.

The move is asynchronous — the wheel returns immediately and get_position() returns -1 while moving.

Parameters:

position – Target position (0=Dark, 1=IR, 2=LP).

Raises:

AlpacaError – Invalid position or device error.

argos.core.alpaca.filterwheel.position_count() → int[source]

Number of slots on the active profile’s internal wheel (0 = no wheel).

argos.core.alpaca.filterwheel.position_names() → dict[int, str][source]

Slot index → filter name, for the active telescope profile.

Was a module constant fixed to the S30 Pro’s Dark/IR/LP wheel. Call it rather than binding it: a model with a different wheel — or none — must be able to change the answer.