Blockscript

The safety-automation scripting engine. A blockscript defines a whole unattended observing session as a state machine: it brings the observatory up, runs the night, reacts to weather and device failures, and tears everything down to a safe state — all without a human present. It is the layer that turns "the sky went unsafe" into "park, close the roof, warm the cooler, notify, and stand by."

A blockscript is built from short, named action lists — one per state. The engine moves between states in response to events (safety verdicts, device health, body completion, manual overrides) and runs the matching action list on entry to each state.

What it is

A blockscript runs as a state machine on its own thread (BlockscriptEngine). Each state has an associated action list that runs once on entry to that state. Only one blockscript runs at a time — the observatory is a single resource, and BlockscriptManager enforces this.

States

Happy path: STARTUP -> RUNNING -> SHUTDOWN -> DONE. From RUNNING the engine can divert to WARNING, UNSAFE (then back via RESUME), or EMERGENCY at any time.

How to use it

You author a blockscript object with a name and one action list per state name (startup, running, warning, unsafe, resume, shutdown, emergency). Each list is a list of action dicts. A minimal action is {"type": "<action>", ...params}.

The engine is event-driven. Once started it:

  1. Enters STARTUP and runs the startup list.
  2. On success, enters RUNNING and runs the running list.
  3. Reacts to events posted from the rest of Starship (safety ticks, device-lost/recovered, body-done) and to manual overrides (force emergency / force shutdown / hold resume).

You normally don't drive the engine by hand — BlockscriptManager.start(blockscript) wires it to the safety bus and runs it. The manager exposes force_emergency(), force_shutdown(), hold_resume(held), status(), and stop().

Example block


name: "deep_sky_night"

startup:
  - type: unpark
  - type: open_roof
  - type: cool_to
    target_c: -10.0
    policy:
      retry_count: 2
      wait_s: 30
      on_error: emergency
  - type: run_sequence
    sequence: "M51_LRGB"

running:
  - type: notify
    severity: info
    message: "Session running"

warning:
  - type: notify
    severity: warning
    message: "Conditions degraded - holding"

unsafe:
  - type: abort_exposure
  - type: park
  - type: close_roof

resume:
  - type: unpark
  - type: open_roof

shutdown:
  - type: park
  - type: close_roof
  - type: warm_cooler
  - type: notify
    severity: info
    message: "Session complete"

emergency:
  - type: abort_exposure
  - type: park
  - type: close_roof
  - type: warm_cooler
  - type: notify
    severity: critical
    message: "EMERGENCY safe-state engaged"

Action types

Every action type maps to a _do_<type> handler on the runner, which calls a real Console method. Unknown types fail with unknown action '<type>'. An action that raises is caught and reported as a failure — it never crashes the engine.

Mount

Roof

- False (default) — mounts clear the roof in any position, so it closes regardless of park state (confirm_unparked=True). - True — it parks first, then closes. If the park fails it still closes (safety beats optics) but sends a critical notification. - On success it reads back shutter_status == 1 (Closed); on failure it sends a critical notify noting the hardware interlock still applies. The close is never blocked.

Guiding (not wired)

Cooler

Capture

Sequences

Scheduler

Device recovery

Control flow

- duration (default) — sleeps seconds (float, default 0). Back-compat: an action with no mode and just seconds still means a plain sleep. - until_safe — holds until the safety supervisor reports SAFE. With no supervisor configured it returns immediately. - until_time — holds until until. Accepts "HH:MM" (the next local occurrence — so 05:30 waits to tomorrow morning if it's already past) or a full ISO datetime (2026-06-30T22:00). An unparseable value fails the action. - until_dark — holds until astronomical darkness (Sun at/below −18° at the configured site). - until_safe_dark — holds until it is both SAFE and astronomically dark — the natural "open the roof and start imaging" gate. - The gated modes (everything but duration) take an optional timeout_min (float) that caps the wait; on timeout the action fails (so an on_error policy can react). If the site location is unset, the darkness modes can't compute dusk — they skip the darkness half rather than block forever (and still honour the safe half of until_safe_dark).

External scripts

On-fail policy

Every action may carry an optional policy dict. With no policy, behaviour is one attempt; on failure the error is logged and the rest of the list keeps running (the safe-state lists must run to completion — you don't want a failed park to skip the close_roof after it). Policy is opt-in and changes nothing unless present. Fields:

- continue (default) — record the failure, keep running the list. - raise / emergency — enter EMERGENCY (both behave identically). - goto — enter the state named in goto (case-insensitive: e.g. SHUTDOWN, unsafe). An unknown target logs a warning and falls through to continue.

This is a uniform per-action error contract, modelled on ROCK's per-block "On Error" options.

Emergency and force-emergency

EMERGENCY is reachable from any state and is where the observatory is forced to a safe state:

How safety ties in

BlockscriptManager.start() subscribes to the safety.state_changed bus event and bridges each verdict into the engine via on_safety(state). The engine reacts to SafetyState:

Resume guards (leaving UNSAFE)

_maybe_resume only promotes UNSAFE -> RESUME when all hold:

Weather re-entry is UNLIMITED — an unstable night keeps recovering as long as it stays dark. The never-clears case is owned by the absolute next-dawn deadline (weather_suspend_dawn_alt_deg), not an attempt cap: if the sky stays UNSAFE until dawn, the engine escalates to a verified teardown (SHUTDOWN -> warm/park/close -> DONE) and notifies. max_resumes is now advisory/inert (kept for back-compat).

The engine also re-checks resume on its idle tick (every ~0.5 s) — and checks the dawn deadline there too — so neither depends on a fresh safety event arriving.

Device health

on_device_lost(role) / on_device_recovered(role) feed device health in. As of v20.9 the manager wires these for you: it subscribes to the equipment.device bus event and bridges each snapshot into the engine. A loss (connected False with an error from a failed poll) escalates; a deliberate disconnect (connected False, no error) is ignored so you can take a device offline without tripping EMERGENCY; healthy polls are de-duplicated so a steady device never spams recovery. On a loss the engine increments a per-role retry counter:

body_done (posted via signal_body_done()) moves RUNNING -> SHUTDOWN ("session body complete"). A supervised run_sequence posts this automatically when imaging finishes; if it arrives while transiently in WARNING/UNSAFE/RESUME it is latched and applied on the return to RUNNING, so a finished session never hangs past dawn.

The safety model for this site

These hardware facts shape why the engine is "command-and-report" rather than a hard gate:

Config knobs

On config.blockscript:

On the blockscript object itself:

On config.observatory:

Safety and gotchas

How it ties into the rest of Starship

What's deferred

See the Roadmap topic for the broader list.

Astroworx Starship v22.2.10 · this page is the in-app help of that buildDownload · HTTP API