Skip to content

Driver interfaces

SkyNode integrates with physical hardware through pluggable driver modules, one per device type. Every driver implements a per-device interface that the rest of SkyNode (the device manager, telescope controller, task executors, manual command executor) talks to without caring which underlying control system is in use.

This page is the conceptual reference for those interfaces and what each driver kind provides. For picking and wiring up drivers on a specific install, see Configuration → device_configs.

For a primer on how observations actually execute against these drivers, see Operations.


Driver tree at a glance

The implementations live under apps/sky-node-desktop/backend/sky_node/drivers/, one subdirectory per device type. Each device type has an interface.py that declares the contract, plus zero or more concrete implementations. Each non-empty subdirectory's driver is registered by the loader and exposed to the local GUI as a selectable option in the device configuration page.

Driver support status

Driver maturity falls into a few tiers:

  • Production — actively maintained, used on live telescopes, the default recommendation.
  • Experimental — works but isn't a mainline target; functionality may lag, edge cases may not be handled. Talk to the Skynet team before relying on one in production.
  • Simulator — paper-only; for testing configurations without hardware. Not a production option.
  • Placeholder — interface-only stub; no concrete implementation yet. The relevant task executors won't run meaningfully against these.
Device type Driver Status
mount/ ascom/ Production
mount/ gbtwenty.py (UNC GBTwenty radio mount) Experimental
mount/ simulator/ Simulator
camera/ ascom/ Production
camera/ maxim/ (MaxIm DL) Production
camera/ simulator/ Simulator
filter_wheel/ ascom/ Production
filter_wheel/ maxim/ Production
filter_wheel/ simulator/ Simulator
focuser/ ascom/ Production
focuser/ simulator/ Simulator
enclosure/ ascom/ Production
enclosure/ simulator/ Simulator
ota/ simulator/ Simulator
instrument_rotator/ simulator/ Simulator
weather_sensor/ aag_cloudwatcher/ Experimental
weather_sensor/ aurora_eurotech/ Experimental
weather_sensor/ boltwood_i/ Experimental
weather_sensor/ boltwood_ii/ Experimental
weather_sensor/ cumulus/ Experimental
weather_sensor/ davis_weatherlink/ Experimental
weather_sensor/ simulator/ Simulator
weather_sensor/ old_csharp_drivers/ Legacy — not used in new installs
receiver/ interface only Placeholder
radio_backend/ interface only Placeholder

The TL;DR: for optical telescopes, ASCOM is the primary production path; MaxIm is the production alternative for cameras and filter wheels. Everything else (the GBTwenty radio mount, the native weather sensors, any new hardware family) is experimental right now — workable, but not the well-trodden path.

The per-device wiring pages under Wiring cover what the JSON-config block looks like for each device kind and which drivers each one exposes.


How a task threads through the driver layer

A typical observation lifecycle, top-down:

  1. An observation is submitted through the web app or the API.
  2. The observability service evaluates whether and when the target is reachable from this telescope under current constraints.
  3. The task scheduler generates one or more ObservationTasks with start times.
  4. SkyNode's task manager fetches eligible tasks for its bound telescope. The telescope controller spins up a task manager per observing session.
  5. The target acquisition manager converts the observation's target into a SkyCoord (scalar for fixed frames, time-series for ephemeral / moving targets) and wraps it in an ephemeris provider.
  6. A task executor specific to the observation type (optical_imaging, optical_imaging_calibration, radio_mapping, radio_tracking) drives the per-device drivers — slewing the mount, opening the enclosure, configuring the filter wheel and camera, taking exposures — and reports results back.

The driver interfaces are designed so the same observation type implementation works against any compliant driver. Adding a new mount, for instance, only requires implementing drivers/mount/interface.py; the optical imaging executor does not change.


Mount driver interface

The mount interface is the most surface-area-rich because mounts have to track moving targets, apply pointing corrections, and play scan patterns.

The contract (drivers/mount/interface.py) exposes:

  • point(ephemeris_provider, offset_pattern=None) — slew to the target at the current time.
  • track(ephemeris_provider, offset_pattern=None) — slew and continuously update pointing to follow the target.
  • set_offset_pattern(pattern) — swap the active offset pattern while keeping the same ephemeris provider; used to start, stop, or retarget a raster/daisy scan mid-track.
  • set_fixed_offset(...) — apply a small persistent trim (for a pointing model, instrument offset, autoguider correction).
  • stop_tracking() — disengage the drive.
  • commanded_pointing_altaz(t) — report the composite (target + fixed offset + pattern) pointing at a given time, used for limit checks and telemetry.

For mounts that don't support offset patterns natively, the simple offset player schedules and plays patterns in software by advancing a playhead in device time and feeding the result through set_fixed_offset.

Target representation

Inside the driver, the target is a plain astropy.coordinates.SkyCoord — scalar for fixed frames (equatorial, galactic, horizontal), time series for everything else. This is deliberately simpler than the hub's Target schema (which also captures the position kind: fixed, catalog, ephemeral, orbital, …). The conversion happens in the target acquisition manager.

Fixed offsets vs. offset patterns

  • Fixed offsets are constant adjustments — pointing model corrections, instrument centering. They persist until you change them.
  • Offset patterns evolve over time. A pattern evaluates to a longitude/latitude delta given an elapsed time, and is the foundation of raster, daisy, and other mapping scans.

When both are active, they compose: the mount points at target + fixed_offset + pattern_offset(t).

GBTwenty driver — production reference

The GBTwenty mount driver is the canonical native-driver reference. It streams position-velocity (PD) commands to the Observatory Control Unit (OCU), keeping the PD buffer filled, and applies fixed offsets and the active pattern after transforming coordinates into the mount's native frame.

Open issues today: invalid-velocity guards and proximity-to-limit checks need to be implemented at the driver level. The Skynet task scheduler respects observability windows, but the driver should provide secondary safeguards in case higher-level software has bugs. The mount's own control system is still the authoritative limit enforcer.


Enclosure driver interface

Enclosure drivers control rotation, shutter state, and (where present) heater / cover automation. The contract is at drivers/enclosure/interface.py.

Enclosures can be bound at two levels in Configuration:

  • connect_locally: true — SkyNode is the active controller for this enclosure.
  • connect_locally: false — SkyNode subscribes to state but does not attempt control. Use this when the enclosure has its own controller and SkyNode is only listening.
  • allow_control: true — actively command rotation and shutter from SkyNode (requires connect_locally: true).

The split exists because some sites run a single physical enclosure controlled by an observatory node, with telescope nodes only observing its state.


Weather sensor driver interface

Weather sensors connect over many heterogeneous transports — TCP, file tail, serial, vendor SDK. Each native driver adapts one of these transports to the common interface, which normalizes readings into the fields SkyNode's WeatherSensorConstraint evaluator understands (cloudiness, wind speed, rain status, humidity, ambient temperature, sky temperature where applicable).

Like enclosures, weather sensors have a connect_locally flag — the observatory node that owns a sensor connects locally; telescope nodes that need to see its readings consume them via the hub.


Camera, filter wheel, focuser, OTA, instrument rotator

These follow the same pattern: an interface.py declaring the operations the task executors need (start exposure, set filter, move focuser, set rotator angle, get/set cooling, …) and one or more backend implementations (ASCOM, MaxIm, simulator). Detail-page coverage per device type is deferred until the driver-support taxonomy is set.


Receiver and radio backend — placeholders

The interfaces at drivers/receiver/interface.py and drivers/radio_backend/interface.py exist but are placeholders — there are no concrete implementations yet. As radio hardware support comes online, this is where new drivers will land.

The radio task executors (task_executor/radio_tracking.py, task_executor/radio_mapping.py) reference these interfaces but won't execute meaningfully until the driver layer is filled in.


Adding a new driver

When a new piece of hardware lands, the high-level recipe is:

  1. Implement the relevant interface.py.
  2. Register the implementation in the device-type's __init__.py / loader.
  3. Add a driver descriptor — name, parameter schema — that the GUI's driver-config form can render against.
  4. Add (or extend) the simulator if you want a paper version for testing.
  5. Add a wiring page under owners/skynode/wiring/ (Phase 1C follow-up) describing the install steps.