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:
- An observation is submitted through the web app or the API.
- The observability service evaluates whether and when the target is reachable from this telescope under current constraints.
- The task scheduler generates one or more
ObservationTasks with start times. - SkyNode's task manager fetches eligible tasks for its bound telescope. The telescope controller spins up a task manager per observing session.
- 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. - 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 (requiresconnect_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:
- Implement the relevant
interface.py. - Register the implementation in the device-type's
__init__.py/ loader. - Add a driver descriptor — name, parameter schema — that the GUI's driver-config form can render against.
- Add (or extend) the simulator if you want a paper version for testing.
- Add a wiring page under
owners/skynode/wiring/(Phase 1C follow-up) describing the install steps.