# An observatory of INDI simulators, with a device of every kind that
# observatory-operator has. Every driver is an `indi_simulator_*` in
# the `indi-simulators` image, so the whole stack runs with no
# hardware.
#
# Apply the CRDs first, then this file to a namespace of your choice,
# such as `observatory`:
#
#   kubectl apply -k deploy/
#   kubectl create namespace observatory
#   kubectl apply -n observatory -f examples/simulators.yaml
#   kubectl get astro -n observatory
#
# The tree, from the top down:
#
#   Observatory lab                 Dome, WeatherStation, SkyQualityMeter
#    ├─ Telescope east              Mount, GPS, PolarAligner, Switch,
#    │   │                          Receiver, Guider
#    │   ├─ OpticalTube east-refractor
#    │   │   └─ OpticalTrain east-imaging
#    │   │        Camera, FilterWheel, Focuser, Rotator, DustCap, FlatPanel
#    │   └─ OpticalTube east-guidescope
#    │       └─ OpticalTrain east-guiding
#    │            Camera
#    └─ Telescope west              Mount
#        └─ OpticalTube west-newtonian
#            └─ OpticalTrain west-imaging
#                 Camera
#
#   the shelf                       Focuser spare
#
# Each resource names its parent in its spec. A device takes the name
# of its telescope or its observatory, and a second device of one kind
# adds a word, such as the cameras `east-main` and `east-guide`. The
# operator names each pod and `Service` `<resource-name>-<kind>`, so
# `kubectl get pods` lists `east-mount`, `east-main-camera`, and the
# server `east-telescope` together.
#
# The operator starts nothing for these resources until a Reservation
# activates their telescope.
#
# Each resource states its own procedures: what the operator does with
# the equipment when the resource's Telescope or Observatory turns
# `Active`, in `activation`, and when it stops being `Active`, in
# `deactivation`. An action is a target state, such as `state: Parked`,
# so the operator can run it twice, and run it again after a restart,
# with no harm. The tree orders the procedures: activation runs the
# Observatory and its devices first, then each Telescope and its
# devices, then the devices of each OpticalTrain, and deactivation runs
# in the reverse order. The Reservation's `Activation` and
# `Deactivation` steps wait for them, and the Reservation's status
# copies their actions.
#
# A resource can also react to a condition while it is active, in
# `triggers`: each trigger names a condition and the status that fires
# it, and runs its actions once for each transition to that status.
#
# The Reservation at the end starts the `east` telescope, and `west`
# stays down, because each telescope has its own INDI server. Each
# device of `west` is `Idle`, and the focuser on the shelf is
# `Inventory`.

# The site. The location is Cerro Tololo Inter-American Observatory in
# Chile, a public observatory. The operator writes it to each mount and
# GPS. The observatory has a dome, so the park locks hold: a mount does
# not unpark while the dome is parked, and the dome does not park while
# the mount of `east` or of `west` is unparked. The dome closes its
# shutter when it parks, and opens it when it unparks. The dome runs on
# the observatory's server and each mount on its telescope's, so the
# operator relays each park state that a lock needs between the
# servers. The condition `LocksRelayed` on the `Observatory` reports
# the relay.
#
# The observatory's activation runs a `job`: a container that the
# operator runs once as a Kubernetes `Job`, for what no other action
# does. This one prints the variables that the operator adds to its
# environment: the observatory, the resource, the trigger, and the
# address of the observatory's INDI server. Read them with
# `kubectl logs -n observatory -l app.kubernetes.io/part-of=observatory,observatory.liken.sh/role=job`.
# A real site would switch on a dew heater's relay here, or post to a
# webhook that tells the people of the site that the roof opens. The
# action ends when the `Job` succeeds, and fails the `Activation` step
# when the `Job` fails.
apiVersion: observatory.liken.sh/v1alpha1
kind: Observatory
metadata:
  name: lab
spec:
  location:
    latitude: -30.169
    longitude: -70.806
    elevation: 2207
  activation:
  - job:
      image: busybox:1.37
      command: [sh, -c, 'env | grep -E "^(LIKEN|INDI)_" | sort']
---
# The devices that no telescope owns run on the observatory's own INDI
# server. That server runs while at least one reservation here is
# active.
#
# The dome unparks on activation, and so opens its shutter, but only
# while the weather station reports `Safe`: `requires` makes the action
# wait for the condition, up to the action's timeout of 10 minutes. It
# parks on deactivation. The tree runs the dome's deactivation after
# the telescopes', so the mounts are parked first, and the park lock
# lets the dome park.
#
# The dome also parks when the weather turns unsafe, for any length of
# time. A trigger has no tree order, so `after` makes the park wait for
# the run of every mount's own trigger on the same transition: the park
# lock refuses a park while a mount is unparked. The dome unparks again
# after the weather has been safe for 20 minutes, so a station that
# flaps between safe and unsafe does not open and close the roof each
# minute.
apiVersion: observatory.liken.sh/v1alpha1
kind: Dome
metadata:
  name: lab
spec:
  observatory: lab
  driver: {name: indi_simulator_dome}
  activation:
  - state: Unparked
    requires:
    - {kind: WeatherStation, name: lab, type: Safe}
  deactivation:
  - state: Parked
  triggers:
  - when: {kind: WeatherStation, name: lab, type: Safe, status: "False"}
    run:
    - state: Parked
      after: [{kind: Mount}]
  - when: {kind: WeatherStation, name: lab, type: Safe, for: 20m}
    run:
    - state: Unparked
---
apiVersion: observatory.liken.sh/v1alpha1
kind: WeatherStation
metadata:
  name: lab
spec:
  observatory: lab
  driver: {name: indi_simulator_weather}
---
# A sky quality meter can belong to the site or to one telescope. This
# one is on the roof, so it belongs to the site.
apiVersion: observatory.liken.sh/v1alpha1
kind: SkyQualityMeter
metadata:
  name: lab
spec:
  observatory: lab
  driver: {name: indi_simulator_sqm}
---
# The first telescope has a device of every kind. Its mount, its trains,
# and its other devices run on one INDI server, because a camera snoops
# the mount and the focuser of its train only on the same server.
apiVersion: observatory.liken.sh/v1alpha1
kind: Telescope
metadata:
  name: east
spec:
  observatory: lab
---
# The mount unparks on activation, after the dome unparked, and parks on
# deactivation, before the dome parks. The operator leaves tracking off,
# because the holder aligns and calibrates the mount first. It parks
# when the weather turns unsafe, and stays parked when the weather
# turns safe again: the holder decides when to unpark it.
apiVersion: observatory.liken.sh/v1alpha1
kind: Mount
metadata:
  name: east
spec:
  telescope: east
  driver: {name: indi_simulator_telescope}
  activation:
  - state: Unparked
  deactivation:
  - state: Parked
  triggers:
  - when: {kind: WeatherStation, name: lab, type: Safe, status: "False"}
    run:
    - state: Parked
---
apiVersion: observatory.liken.sh/v1alpha1
kind: GPS
metadata:
  name: east
spec:
  telescope: east
  driver: {name: indi_simulator_gps}
---
apiVersion: observatory.liken.sh/v1alpha1
kind: PolarAligner
metadata:
  name: east
spec:
  telescope: east
  driver: {name: indi_simulator_pac}
---
# A relay board with four outputs, which powers the imaging train. The
# devices below name an output in spec.power. Activation switches each
# named output on before the device's pod starts, and deactivation
# switches it off after the pod stops.
apiVersion: observatory.liken.sh/v1alpha1
kind: Switch
metadata:
  name: east
spec:
  telescope: east
  driver: {name: indi_simulator_io}
---
# A radio receiver on the same mount, such as an RTL-SDR on a dish.
apiVersion: observatory.liken.sh/v1alpha1
kind: Receiver
metadata:
  name: east
spec:
  telescope: east
  driver: {name: indi_simulator_receiver}
---
# Two tubes on one mount: an 80 mm refractor for imaging and a 50 mm
# guide scope. The aperture and the focal length are in millimeters.
apiVersion: observatory.liken.sh/v1alpha1
kind: OpticalTube
metadata:
  name: east-refractor
spec:
  telescope: east
  aperture: 80
  focalLength: 480
---
apiVersion: observatory.liken.sh/v1alpha1
kind: OpticalTube
metadata:
  name: east-guidescope
spec:
  telescope: east
  aperture: 50
  focalLength: 200
---
# A train is one light path. The operator writes each camera's
# ACTIVE_DEVICES from its train, so the main camera's frames name the
# focuser, the filter wheel, and the rotator of this train.
apiVersion: observatory.liken.sh/v1alpha1
kind: OpticalTrain
metadata:
  name: east-imaging
spec:
  telescope: east
  opticalTube: east-refractor
---
apiVersion: observatory.liken.sh/v1alpha1
kind: OpticalTrain
metadata:
  name: east-guiding
spec:
  telescope: east
  opticalTube: east-guidescope
---
# The main camera, on output 1. The `Configure` step writes the gain and
# the offset. Activation cools the sensor to -10 degrees Celsius and
# waits until it is within 0.5 degrees of that. Deactivation warms the
# sensor to 5 degrees before the cooler goes off, because a sensor that
# loses its cooler at -10 degrees warms by tens of degrees in seconds,
# and that stress can crack it. A cooler cannot warm a sensor above the
# air around it, so the warm-up ends at its timeout and does not fail.
apiVersion: observatory.liken.sh/v1alpha1
kind: Camera
metadata:
  name: east-main
spec:
  opticalTrain: east-imaging
  driver: {name: indi_simulator_ccd}
  power: {switch: east, output: 1}
  gain: 100
  offset: 10
  activation:
  - cool: {celsius: -10, within: 0.5}
  deactivation:
  - warm: {celsius: 5}
---
# The filter wheel, the focuser, and the rotator share output 2, the
# way a USB hub on one 12 V line does.
apiVersion: observatory.liken.sh/v1alpha1
kind: FilterWheel
metadata:
  name: east
spec:
  opticalTrain: east-imaging
  driver: {name: indi_simulator_wheel}
  power: {switch: east, output: 2}
  # The filter in each slot, from slot 1. The simulator has 8 slots.
  filters: [Luminance, Red, Green, Blue, H_Alpha, OIII, SII, Dark]
---
apiVersion: observatory.liken.sh/v1alpha1
kind: Focuser
metadata:
  name: east
spec:
  opticalTrain: east-imaging
  driver: {name: indi_simulator_focus}
  power: {switch: east, output: 2}
---
apiVersion: observatory.liken.sh/v1alpha1
kind: Rotator
metadata:
  name: east
spec:
  opticalTrain: east-imaging
  driver: {name: indi_simulator_rotator}
  power: {switch: east, output: 2}
---
# The dust cap and the flat panel are on output 3. Activation opens the
# cap, and deactivation closes it. The devices of a train run after the
# mount on activation, and before it on deactivation.
apiVersion: observatory.liken.sh/v1alpha1
kind: DustCap
metadata:
  name: east
spec:
  opticalTrain: east-imaging
  driver: {name: indi_simulator_dustcover}
  power: {switch: east, output: 3}
  activation:
  - state: Open
  deactivation:
  - state: Closed
---
# Deactivation switches the panel's light off: the panel goes `Dark`.
apiVersion: observatory.liken.sh/v1alpha1
kind: FlatPanel
metadata:
  name: east
spec:
  opticalTrain: east-imaging
  driver: {name: indi_simulator_lightpanel}
  power: {switch: east, output: 3}
  deactivation:
  - state: Dark
---
# The guide camera, behind the guide scope. It has no power output, so
# a person powers it by hand. The guide simulator draws 2.4 µm pixels
# and 6 arc-seconds of seeing, so behind the 200 mm guide scope a star
# spans about 2.4 pixels at 2.5 arc-seconds per pixel, which PHD2 can
# find and follow. The CCD simulator's 5.2 µm pixels and 3.5
# arc-seconds would draw stars smaller than a pixel there.
apiVersion: observatory.liken.sh/v1alpha1
kind: Camera
metadata:
  name: east-guide
spec:
  opticalTrain: east-guiding
  driver: {name: indi_simulator_guide}
---
# PHD2 guides the east telescope with the guide train's camera, and
# sends its corrections to the mount. The operator runs PHD2 in the pod
# east-guider, connects it to the guide camera and the mount, and
# leaves it idle: the holder calibrates and guides through PHD2's event
# server at status.endpoint, east-guider.observatory.svc:4400. The
# guider's pod and the guide camera's pod run on the node of the
# telescope's server, so a guide frame crosses no link between nodes.
apiVersion: observatory.liken.sh/v1alpha1
kind: Guider
metadata:
  name: east
spec:
  telescope: east
  opticalTrain: east-guiding
  pulses: Mount
---
# The second telescope has its own INDI server. Its camera runs the same
# simulator as the east camera, and both register "CCD Simulator", on
# two servers with no conflict.
apiVersion: observatory.liken.sh/v1alpha1
kind: Telescope
metadata:
  name: west
spec:
  observatory: lab
---
apiVersion: observatory.liken.sh/v1alpha1
kind: Mount
metadata:
  name: west
spec:
  telescope: west
  driver: {name: indi_simulator_telescope}
  activation:
  - state: Unparked
  deactivation:
  - state: Parked
  triggers:
  - when: {kind: WeatherStation, name: lab, type: Safe, status: "False"}
    run:
    - state: Parked
---
apiVersion: observatory.liken.sh/v1alpha1
kind: OpticalTube
metadata:
  name: west-newtonian
spec:
  telescope: west
  aperture: 200
  focalLength: 1000
---
apiVersion: observatory.liken.sh/v1alpha1
kind: OpticalTrain
metadata:
  name: west-imaging
spec:
  telescope: west
  opticalTube: west-newtonian
---
# This camera has a cooler and no procedure that cools it, so the
# `Activation` step's summary notes it.
apiVersion: observatory.liken.sh/v1alpha1
kind: Camera
metadata:
  name: west-main
spec:
  opticalTrain: west-imaging
  driver: {name: indi_simulator_ccd}
---
# A spare focuser on the shelf. It names no optical train, so it is
# installed nowhere: its phase is `Inventory`, and the operator creates
# no pod, no `Service`, and no `ResourceClaim` for it, even while a
# reservation is active. A `ResourceClaim` would reserve the real
# hardware, so the operator creates none for a device on the shelf,
# even when its spec has a `claim`. To install it, set `opticalTrain`.
# While the train's telescope is reserved, the operator starts the
# focuser's driver on the running INDI server and connects it. The
# other devices on that server stay connected. On a train of `east`,
# the spare reports `Error` and does not start, because `Focuser east`
# runs `indi_simulator_focus` on that server already. Installed there
# before a reservation of `east` activates, it fails that reservation's
# `PowerOn` step instead, which names both focusers and the driver.
apiVersion: observatory.liken.sh/v1alpha1
kind: Focuser
metadata:
  name: spare
spec:
  driver: {name: indi_simulator_focus}
---
# The reservation that starts the east telescope. With no spec.start,
# activation begins when it is created. With no spec.end, it lasts
# until it is deleted. Add both as RFC 3339 times to reserve a night:
#
#   start: 2026-10-05T20:00:00-04:00
#   end: 2026-10-06T05:00:00-04:00
#
# Watch the steps, and wait for the telescope:
#
#   kubectl get reservation east-tonight -n observatory -w
#   kubectl wait --for=condition=Ready reservation/east-tonight -n observatory
#
# Then connect KStars to the host and port in status.endpoint, and its
# guiding to PHD2 at the Guider's status.endpoint.
apiVersion: observatory.liken.sh/v1alpha1
kind: Reservation
metadata:
  name: east-tonight
spec:
  telescope: east
  holder: desktop
