Field rotator
Starship can drive a connected field rotator to set the camera's framing angle — either to a raw mechanical angle or, more usefully, to a true sky position angle (PA) verified by plate-solving. Rotator support landed in v19.8; the per-target sequencer rotate step followed in v19.9, and mechanical travel limits on manual moves in v20.3. This page reflects the current build.
What it is
A field rotator turns the camera (and filter wheel/OAG) about the optical axis so you can frame a target at a chosen angle and keep that framing consistent night to night. Starship talks to the rotator over either the ASCOM or Alpaca transport — whichever has a device in the rotator slot connected. There is no simulated rotator; if no rotator is connected, the rotator commands fail cleanly with "no rotator connected".
Two operations are exposed:
rotator_move— a plain absolute move to a mechanical angle (0–360°).rotate_to_pa— a closed-loop, plate-solve-verified rotation to a sky position angle.
Both run as background jobs (you get a job id and can watch progress / cancel), and both honour the same RotatorConfig knobs.
Where you drive it
The rotator lives on its own Rotator settings page (Settings → Rotator). That page holds the connection/transport picker, all the config knobs below, and a small control panel with three buttons and a live status line:
- Move — enter a mechanical angle and fire a
rotator_move. - Rotate to PA — enter a sky PA and fire a
rotate_to_pa; the status line reports the final PA, residual error, and iteration count when it converges. - Halt — stop the current move (
rotator_halt).
The same Move / Solve & rotate to PA / Halt controls also appear inline on the Camera block, greyed out unless a rotator is connected. On both surfaces the buttons are gated by the rotator-move capability; a viewer-only session can still see rotator status but not command a move.
Rotator status
rotator_status() reports what the driver exposes:
- connected / transport — whether a rotator is in the connected
rotatorslot, and which transport (alpacaorascom) owns it. - position — the sky-synced angle in degrees (ASCOM
Position). This is the angle Starship steersrotate_to_paagainst. - mechanical_position — the raw mechanical angle (ASCOM
MechanicalPosition, IRotatorV3). Reported when the driver supplies it. - is_moving — motion flag. If the driver does not report it, Starship falls back to a fixed settle wait instead of polling (see below).
- target_position — the driver's commanded target during a move.
Other properties read from the driver but not surfaced in status include can_reverse, reverse, and step_size.
Absolute move — rotator_move
rotator_move(position_deg) sends MoveAbsolute to the rotator. The angle is wrapped into 0–360° first, so -10 becomes 350. The worker then:
- Commands the absolute move.
- Waits for the rotator to go idle by polling
is_movingevery 0.5 s, up tomove_timeout_s. - Sleeps
settle_sto let vibration die down. - Reports the final
position.
This is the right command when you already know the mechanical angle you want (e.g. reproducing a saved framing on a rotator you trust to be sky-calibrated). It does not verify the result against the sky.
Solve-and-rotate — rotate_to_pa
rotate_to_pa(target_pa_deg) is the precise path: it rotates until a plate solve confirms the camera is at the requested sky PA, within tolerance. The loop, per iteration:
- Capture a frame (
exposure_s,binning, object namerotate-to-pa, prefixrot). These are solve-only frames, so they also pick up the camera's plate-solve overrides: if you've set[camera] plate_solve_gain_overridethe solve frames use that gain, and if you've set[camera] plate_solve_binning_overrideit wins over the rotator's ownbinningknob. Leave those blank to fall through to the science defaults / the[rotator] binningvalue. - Plate-solve it. The solver (ASTAP) returns
rotation_deg, computed from the WCS CD matrix asatan2(cd2_1, cd1_1)— the actual sky PA of the frame. - Compute the PA error toward
target_pa_deg. - If
|error| <= tolerance_deg, done. - Otherwise rotate the mechanical axis by
gain error(absolute move tocurrent_position + gainerror, wrapped 0–360°), wait for idle, settle, and loop.
It iterates up to max_iterations (clamped to 1–10 internally). If it never gets inside tolerance it fails with "did not converge in N iterations (last error …°)" and returns the full per-iteration history.
Direction self-calibration
The sign between a mechanical move and the resulting change in sky PA depends on the optical train — mirrors flip it, and the rotator's own direction convention varies. Rather than assume, rotate_to_pa measures the response: after the first move it computes gain = ΔPA / Δmove and snaps it to +1 or -1. So the first iteration may move the "wrong" way, but the loop corrects on the next one. This is why even a fresh, uncalibrated rotator converges without you setting a direction flag.
accept_180
For imaging, framing at PA and PA+180° is visually identical (the field is just rotated halfway around — same stars, same crop). When accept_180 is true (the default), the loop treats the two as equivalent and rotates to whichever is the shorter move. This avoids a needless near-180° swing when the rotator happens to sit on the far side. Set it false only if up/down orientation matters to you (e.g. matching a specific published frame or a guider/OAG geometry constraint).
Where it plugs in
- Console.
rotator_move,rotate_to_pa,rotator_halt, androtator_statusare console commands, exposed over the web API at/api/console/rotator_moveand/api/console/rotate_to_pa. - Plate solver.
rotate_to_pais built directly on the same ASTAP solve used everywhere else; it needs the solver available and a camera connected, or it fails fast ("ASTAP not available" / "no camera connected"). - Camera position angle. Every successful plate solve (not just rotator runs) updates
[camera] camera_position_angle_degfrom the solvedrotation_deg, so the rest of the app always knows the current framing angle. - Sequencer (framing). A plan target may carry an optional
pa_deg. When present, the sequencer inserts a rotate step (after the slew, before exposures) that callsrotate_to_pafor that target — so an unattended run frames each target to its intended angle automatically. Targets withoutpa_degskip the step entirely.
Config knobs (RotatorConfig)
Set under [rotator] in the config. Defaults shown.
settle_s(1.0) — pause after every rotator move (bothrotator_moveand eachrotate_to_paiteration) before measuring, to let the train settle.move_timeout_s(120.0) — ceiling on theis_movingpoll. If the rotator hasn't gone idle in this long, the move fails with "rotator move timed out".exposure_s(4.0) — exposure for eachrotate_to_pasolve frame.binning(1) — binning for those solve frames. If[camera] plate_solve_binning_overrideis set, that value takes precedence over this one.tolerance_deg(0.5) — convergence target. The loop stops once the solved PA is within this many degrees of the requested PA.max_iterations(4) — maximum capture-solve-rotate cycles before giving up (internally clamped to 1–10).accept_180(true) — treat PA and PA+180° as equivalent and take the short way (see above).min_position_deg(unset) — mechanical lower travel limit. Blank means no limit. Enforced onrotator_move(see gotchas for therotate_to_pacaveat).max_position_deg(unset) — mechanical upper travel limit. Blank means no limit. Enforced onrotator_move(see gotchas for therotate_to_pacaveat).
Safety and gotchas
min_position_deg/max_position_degare enforced onrotator_move, but not onrotate_to_pa. Since v20.3, a plainrotator_movewhose wrapped target falls belowmin_position_degor abovemax_position_degis refused before the job starts ("rotator move refused: …° is below/beyond the … limit"); a blank limit is no limit.rotate_to_padoes not consult these limits — its self-calibrating loop commands whatever absolute angle the PA error implies (wrapped to 0–360°). So the limits guard a manual move, but if your rotator has hard mechanical stops or cable-wrap limits, do not rely on them to fence a solve-and-rotate run.- Cable wrap. Because
rotate_to_paignores the travel limits and angles are wrapped to 0–360°, that loop always takes the absolute move the driver computes; Starship does not track or prevent multi-turn cable wrap. Mind your cabling, especially across a meridian flip or repeatedrotate_to_paruns over a long night. rotate_to_pais exclusive. It refuses to start if anotherrotate_to_pa,rotator_move,precise_slew, orpolar_alignjob is already running (those share the camera/solve path).rotator_movedoes not take that lock, so don't fire a plain move into a running solve loop.- Needs solver + camera.
rotate_to_paaborts immediately if ASTAP is unavailable or no camera is connected.rotator_moveneeds neither — only the rotator. - Unknown
is_moving. If the driver doesn't report a motion flag,wait_rotator_idletreats "unknown" as idle and relies onsettle_salone. On such a rotator, setsettle_sgenerously so a move has really finished before the next solve. rotator_halt. SendsHaltto the driver and cancels any in-flightrotator_move/rotate_to_pajob. Use it to stop a runaway move; a cancelledrotate_to_paalso halts the rotator mid-move.- Convergence vs. iterations. A tight
tolerance_degwith a lowmax_iterationscan fail to converge on a slow or backlashy rotator. If you see "did not converge", either loosentolerance_deg, raisemax_iterations, or check thehistoryto see whether it was oscillating (direction/backlash) or just out of cycles.
Tying it together
For unattended runs, the intended flow is: precise slew to the target, then rotate_to_pa to its pa_deg, then expose. The rotate step verifies framing against the real sky, self-calibrates its own direction, and (by default) avoids needless 180° swings — so each target comes up framed the same way every night without a per-rotator direction setting. See Sequencer for plan-level framing and Plate solving / Pointing for the solve and slew steps it builds on.